def findNearestTrackpoint(list, time, interpolate, threshold): """Search the list of trackpoints, and return the one with the time nearest to time possibly interpolating between closest trackpoints Return None if no trackpoint exists within threshold seconds """ closestPoints = [None, None] # the closest point before and after closestTimes = [threshold, threshold] # Python datetimes are seconds # iterate over the list (binary search would work too) for trackpoint in list: delta = trackpoint.time - time after = (delta >= 0) # if this point is closer than the closest recorded yet if abs(delta) < closestTimes[after]: closestTimes[after] = abs(delta) closestPoints[after] = trackpoint for i in [0,1]: if closestPoints[i] is None: closestPoints[i] = closestPoints[1-i] #if both are None then there had been no points at all and we're screwed #reduce the !interpolate case to the interpolate case if not interpolate: for i in [0,1]: if abs(closestPoints[i].time - time)<=abs(closestPoints[1-i].time-time): closestPoints[1-i]=closestPoints[i] #we use normal vectors for interpolation purposes (http://en.wikipedia.org/wiki/N-vector) normals = [ (cos(point.lat/90.*pi)*cos(point.lon/90.*pi), cos(point.lat/90.*pi)*sin(point.lon/90.*pi), sin(point.lat/90.*pi)) for point in closestPoints ] deltas = [ abs(time-point.time) for point in closestPoints ] #interpolate the normal vectors normal = [ interpolate_n(deltas, values) for values in zip(*normals) ] #normalize the result normal = [ value/sum(v*v for v in normal) for value in normal ] #interpolate the elevation elevation = interpolate_n(deltas, (closestPoints[0].ele, closestPoints[1].ele)) #convert everything back to lat/lon coordinates ret = Trackpoint( atan2(normal[2],sqrt(normal[0]**2+normal[1]**2))/pi*90, atan2(normal[1],normal[0])/pi*90, elevation); ret.time = time return ret
def main(): # Parse the options parser = ArgumentParser() parser.add_argument("args", metavar="PHOTO", nargs='*', help='photos to be processed') parser.add_argument("-g", "--gps", dest="gps", required=True, help="The input GPS track file in .gpx format", metavar="FILE") parser.add_argument("-p", "--photos", dest="photos", help="The directory of photos", metavar="DIR") # MPickering added next option; this offset is added to the JPG values (which don't have # native timezone information) parser.add_argument("-t", "--timediff", dest="timediff", type=int, default=0, help="Add this number of hours to the JPEG times") parser.add_argument("-o", "--output", dest="output", help="The output filename for the GPX file", metavar="FILE") parser.add_argument("-u", "--update-photos", action="store_true", dest="updatephotos", help="Update the photos with GPS information") parser.add_argument("-v", "--verbose", action="store_true", dest="verbose") # not used; could be useful parser.add_argument("-i", "--interpolate", action="store_true", dest="interpolate", help="interpolate coordinates linearily between closest track points") parser.add_argument("--threshold", dest="threshold", type=int, default=5*60, help="threshold in seconds that a track point may differ from a photos timestamp still allowing them to get associated; set to -1 to allow arbitrary threshold.") options = parser.parse_args() args = options.args if options.threshold==-1: options.threshold = float("inf") # Load and Parse the GPX file to retrieve all the trackpoints xmldoc = minidom.parse(options.gps) gpx = xmldoc.getElementsByTagName("gpx") # get all trackpoints, irrespective of their track trackpointElements = gpx[0].getElementsByTagName("trkpt") photos = [] trackpoints = [] # Iterate over the trackpoints; put them in a list sorted by time for pt in trackpointElements: timeElement = pt.getElementsByTagName("time") time = "" if timeElement: timeString = timeElement[0].firstChild.data # times are in xsd:dateTime: <time>2006-12-20T15:01:06Z</time> time = mktime(strptime(timeString[0:len(timeString)-1], "%Y-%m-%dT%H:%M:%S")) trackpoint = Trackpoint() trackpoint.lat = float(pt.attributes["lat"].value) trackpoint.lon = float(pt.attributes["lon"].value) trackpoint.ele = float(pt.getElementsByTagName("ele")[0].firstChild.data) trackpoint.time = time trackpoints.append(trackpoint) trackpoints.sort(key=lambda obj:obj.time) # prepare the list of photos if options.photos: photolist = filter(lambda x: os.path.isfile(x) and \ os.path.splitext(x)[1].lower() == '.jpg', \ [os.path.join(options.photos, photo) for photo in os.listdir(options.photos)]) else: photolist = args photolist.sort() for file in photolist: photo = Photo() photo.filename = file photo.shortfilename = os.path.split(file)[1] # Parse the EXIF data and find the closest matching trackpoint tags = getExif(photo) try: photo.time = mktime(strptime(bytes.decode(tags[b'Image timestamp']), "%Y:%m:%d %H:%M:%S")) # account for time difference (GPX uses UTC; EXIF uses local time) photo.time += options.timediff * 3600 photo.trackpoint = findNearestTrackpoint(trackpoints, photo.time, options.interpolate, options.threshold) if photo.trackpoint: photos.append(photo) except: # picture may have been unreadable, may not have had timestamp, etc. print(photo.filename, traceback.format_exc()) # ready to output the photo listing impl = getDOMImplementation() gpxdoc = impl.createDocument(None, "gpx", None) doc_element = gpxdoc.documentElement # <gpx> (top-level) element attributes doc_element.setAttribute("version", "1.0") doc_element.setAttribute("creator", "gpspoint_to_gpx.py") doc_element.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") doc_element.setAttribute("xmlns", "http://www.topografix.com/GPX/1/0") doc_element.setAttribute("xsi:schemaLocation", \ "http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd") # <gpx> contains <time> element time_element = gpxdoc.createElement("time") time_element.appendChild(gpxdoc.createTextNode( \ strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()))) doc_element.appendChild(time_element) # <bounds> element needs to go next; we'll fill in the attributes later bounds_element = gpxdoc.createElement("bounds") doc_element.appendChild(bounds_element) # header is complete; now need to iterate over the photomap dictionary # and add waypoints minlat = 90.0 maxlat = -90.0 minlon = 180.0 maxlon = -180.0 for photo in photos: lat = photo.trackpoint.lat lon = photo.trackpoint.lon ele = photo.trackpoint.ele # track minimum and maximum for the <bounds> element if (lat < minlat): minlat = lat if (lat > maxlat): maxlat = lat if (lon < minlon): minlon = lon if (lon > maxlon): maxlon = lon wpt_element = gpxdoc.createElement("wpt") doc_element.appendChild(wpt_element) wpt_element.setAttribute("lat", str(lat)) wpt_element.setAttribute("lon", str(lon)) ele_element = gpxdoc.createElement("ele") wpt_element.appendChild(ele_element) ele_element.appendChild(gpxdoc.createTextNode(str(ele))) name_element = gpxdoc.createElement("name") wpt_element.appendChild(name_element) name_element.appendChild(gpxdoc.createTextNode(photo.shortfilename)) cmt_element = gpxdoc.createElement("cmt") wpt_element.appendChild(cmt_element) cmt_element.appendChild(gpxdoc.createTextNode(photo.shortfilename)) # use filename as the description # we could check the photo comment, if it exists, and use that... desc_element = gpxdoc.createElement("desc") wpt_element.appendChild(desc_element) desc_element.appendChild(gpxdoc.createTextNode(photo.shortfilename)) # now, assemble and execute the exiv2 command if options.updatephotos: setExif(photo); # finish the bounds element bounds_element.setAttribute("minlat", str(minlat)) bounds_element.setAttribute("minlon", str(minlon)) bounds_element.setAttribute("maxlat", str(maxlat)) bounds_element.setAttribute("maxlon", str(maxlon)) # dump the document if options.output: outfile = open(options.output, "w") else: outfile = sys.stdout try: PrettyPrint(gpxdoc, outfile) except: outfile.write(gpxdoc.toprettyxml(" ")) outfile.close()