{"id":1419,"date":"2022-05-21T13:50:37","date_gmt":"2022-05-21T13:50:37","guid":{"rendered":"https:\/\/marcel-jan.eu\/datablog\/?p=1419"},"modified":"2022-05-25T12:10:13","modified_gmt":"2022-05-25T12:10:13","slug":"digging-into-video-files-for-geolocations","status":"publish","type":"post","link":"https:\/\/marcel-jan.eu\/datablog\/2022\/05\/21\/digging-into-video-files-for-geolocations\/","title":{"rendered":"Digging into video files for geolocations"},"content":{"rendered":"<p>So far I&#8217;ve found geolocations in XML metadata that my actioncam stores on disk as seperate .XML files and I&#8217;ve found them in JPG files. When I showed the cool maps I made to my father, he asked if I could create maps from his holiday videos. So that he can show cool maps in his video compilations.<\/p>\n<p>&nbsp;<\/p>\n<h2>Where do locations get stored in video files?<\/h2>\n<p>My father has a Sony PJ650VE video camera that makes videos in <a href=\"https:\/\/en.wikipedia.org\/wiki\/AVCHD\">AVCHD<\/a> format. Even the camera itself can show you a map of a video location. So I knew it should store geolocations somewhere. But looking on disk I saw no handy metadata files for me to read. So where did the locations go?<\/p>\n<p>I learned that video formats like MP4, Quicktime (.mov) and AVCHD have EXIF metadata stored in them, just like JPG files. Luckily I had all the videos my father had made of our trip to the east coast of the USA in 2013. So I had lots of examples of AVCHD files to work with.<\/p>\n<p><!--more--><\/p>\n<h2>Exiftool<\/h2>\n<p>And a familiar tool called <a href=\"https:\/\/www.exiftool.org\/\">exiftool<\/a> by Phil Harvey can read that metadata not just for photos, but also for videos. The geolocation data for AVCHD and MP4 files however, is stored as &#8220;embedded&#8221; metadata. You can get that embedded metadata by running exiftool with the -ee option.<\/p>\n<p>What you see here is the exiftool command to extract embedded geolocation data from an AVCHD file (.MTS extention). For this I had to use a gpx.fmt file to format the geo data. You can find that file here: <a href=\"https:\/\/github.com\/exiftool\/exiftool\/blob\/master\/fmt_files\/gpx.fmt\">https:\/\/github.com\/exiftool\/exiftool\/blob\/master\/fmt_files\/gpx.fmt<\/a>.<\/p>\n<pre>exiftool -p \"C:\\Program Files\\Exiftool\\fmt_files\\gpx.fmt\" -ee .\\00000.MTS &gt; 00000.gpx<\/pre>\n<p>This will produce a .gpx file with not just one geolocation, but several geolocations measured while the video was recorded. As far as I can see it has two geolocation measurements per second. But that might be a setting per camera. Here is an example:<\/p>\n<pre>[..]\r\n&lt;trkpt lat=\"26.4226047222222\" lon=\"-81.9083294444444\"&gt;\r\n&lt;ele&gt;8.806&lt;\/ele&gt;\r\n&lt;time&gt;2013-09-20T20:54:22Z&lt;\/time&gt;\r\n&lt;\/trkpt&gt;\r\n&lt;trkpt lat=\"26.4226047222222\" lon=\"-81.9083294444444\"&gt;\r\n&lt;ele&gt;8.806&lt;\/ele&gt;\r\n&lt;time&gt;2013-09-20T20:54:22Z&lt;\/time&gt;\r\n&lt;\/trkpt&gt;\r\n&lt;trkpt lat=\"26.422605\" lon=\"-81.9083297222222\"&gt;\r\n&lt;ele&gt;8.81&lt;\/ele&gt;\r\n&lt;time&gt;2013-09-20T20:54:23Z&lt;\/time&gt;\r\n&lt;\/trkpt&gt;\r\n&lt;trkpt lat=\"26.422605\" lon=\"-81.9083297222222\"&gt;\r\n&lt;ele&gt;8.81&lt;\/ele&gt;\r\n&lt;time&gt;2013-09-20T20:54:23Z&lt;\/time&gt;\r\n&lt;\/trkpt&gt;<\/pre>\n<p>As you can see it has latitude and longitude, as well as elevation (which is the same as altitude?) and a timestamp. This works with both AVCHD files as well with the MP4 files my Sony actioncam shoots.<\/p>\n<p>&nbsp;<\/p>\n<h2>Exiftool in Python<\/h2>\n<p>That geo data was what I wanted, but how to I get it into Python? I didn&#8217;t want to first write lots of .gpx files all over the place. It should be possible to read the geo data in a variable.<\/p>\n<p>Looking for solutions I found lots of EXIF Python libraries. A lot of them hadn&#8217;t seen updates in years. But there is a <a href=\"https:\/\/pypi.org\/project\/PyExifTool\">PyExifTool library<\/a> and it is up to date. The only problem: I could not find out how to get embedded data out of it. It should be possible with the <a href=\"https:\/\/sylikc.github.io\/pyexiftool\/reference\/2-helper.html\">class ExifTool-Helper<\/a>, but I never found out how.<\/p>\n<p>&nbsp;<\/p>\n<h2>Running an os command from Python<\/h2>\n<p>I decided in the end to go another route: running exiftool.exe from Python. For this I used os.subprocess.run. It can run an OS command for you and return the output to a file, but also to a pipe. And we can direct that pipe to a variable.<\/p>\n<pre>import subprocess<\/pre>\n<p>First I prepare my exiftool.exe command, just like I ran by hand. Except now of course the filename is a variable which makes automation possible.<\/p>\n<pre>exiftool_command = [\"exiftool\", \"-ee\", \"-m\", \"-p\", \"C:\\\\Program Files\\\\Exiftool\\\\fmt_files\\\\gpx.fmt\",\r\n                    self.mp4file_location_disk]<\/pre>\n<p>Then I run this command with os.subprocess.run. The output that would have ran on screen, now is directed to a PIPE (subprocess.PIPE). All of that goes into my variable called exif_metadata.<\/p>\n<pre>exif_metadata = subprocess.run(exiftool_command, stdout=subprocess.PIPE)<\/pre>\n<p>What you get now is the result in byte format. Every line is started with &#8216;b and ended with another single quote and it has lots of carriage return and line feed characters in it (\\r\\n). To clean that up, we can decode it to a string with this command:<\/p>\n<pre>exif_metadata_decoded = exif_metadata.stdout.decode('utf-8')<\/pre>\n<p>The exif_metadata_decoded variable will now hold that same XML metadata that exiftool wrote to that .gpx file.<\/p>\n<p>&nbsp;<\/p>\n<h2>Digging into the XML<\/h2>\n<p>A short version of the XML data looks something like this:<\/p>\n<pre>&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt;\r\n&lt;gpx version=\"1.0\"\r\n creator=\"ExifTool 12.41\"\r\n xmlns:xsi=\"http:\/\/www.w3.org\/2001\/XMLSchema-instance\"\r\n xmlns=\"http:\/\/www.topografix.com\/GPX\/1\/0\"\r\n xsi:schemaLocation=\"http:\/\/www.topografix.com\/GPX\/1\/0 http:\/\/www.topografix.com\/GPX\/1\/0\/gpx.xsd\"&gt;\r\n&lt;trk&gt;\r\n&lt;number&gt;1&lt;\/number&gt;\r\n&lt;trkseg&gt;\r\n&lt;trkpt lat=\"26.4226047222222\" lon=\"-81.9083294444444\"&gt;\r\n  &lt;ele&gt;8.806&lt;\/ele&gt;\r\n  &lt;time&gt;2013-09-20T20:54:22Z&lt;\/time&gt;\r\n&lt;\/trkpt&gt;\r\n&lt;\/trkseg&gt;\r\n&lt;\/trk&gt;\r\n&lt;\/gpx&gt;<\/pre>\n<p>What we want to do, is get in that &lt;trkpt&gt; stuff and pick out the lat, lon, ele and time data. Like we saw before, you can have multiple trkpt&#8217;s per video file. I&#8217;ve done some experiments where I plotted all these trkpt locations for all video files of one holiday. That&#8217;s two locations per second of video. It resulted in a big, big html file that my Firefox was suffering under. So let&#8217;s just pick the first location per video file.<\/p>\n<p>So how do we tell Python to traverse to that &lt;trk&gt;, &lt;trgseg&gt; and pick up the first &lt;trkpt&gt; element it sees? Well I&#8217;m still learning to read XML with Python, but here&#8217;s what I&#8217;ve done. I&#8217;ve used ElementTree from the xml.etree library:<\/p>\n<pre>from xml.etree import ElementTree as ET<\/pre>\n<p>I loaded the decoded exif metadata that I got from exiftool and created an XML root from that:<\/p>\n<pre>exif_metadata_root = ET.fromstring(exif_metadata_decoded)<\/pre>\n<p>Now I could iterate through the elements in that XML tree. Among then I saw my trkpt tags and from there I was able to pick out the lat and lon attributes.<\/p>\n<pre>for exif_element in exif_metadata_root.iter():\r\n    if \"trkpt\" in exif_element.tag:\r\n        lat = exif_element.attrib['lat']\r\n        lon = exif_element.attrib['lon']<\/pre>\n<p>You&#8217;d think that ele and time would be regarded as attributes of trkpt also, but with this iteration they were regarded as different elements. So to read these, I just retrieved the text value of them:<\/p>\n<pre>    if \"ele\" in exif_element.tag:\r\n        elevation = exif_element.text<\/pre>\n<p>The latitude and longitudes were in decimal, so I did not need to convert them.<\/p>\n<p>To pick out only the first location I checked if my lat, lon and ele had data in them. If so, I break out of the for loop. Suggestions to create cleaner solutions are of course welcome.<\/p>\n<p>I then could send the location data straight to a Pandas dataframe to plot them with Folium.<\/p>\n<p>&nbsp;<\/p>\n<h2>The results<\/h2>\n<p>I said that I would only pick out one geolocation per video file. But initially I didn&#8217;t and this is what it looked like. This is a track of videos my father and I made when we came in by ferry from Ocracoke (one of the Outer Banks islands of North Carolina) and we went on over Cedar Island, on to Morehead.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone wp-image-1420\" src=\"https:\/\/marcel-jan.eu\/datablog\/wp-content\/uploads\/2022\/05\/USA2013_gps_tijdens_rit-300x215.png\" alt=\"\" width=\"670\" height=\"480\" \/><\/p>\n<p>After that I&#8217;ve changed my code so that it picks just one geolocation per video. Even generating that map takes a lot of time if you want to show all locations of 2000+ AVCHD videos (1178 of which had actual geolocations in them). Reading the videos themselves instead of the little XML files my Sony actioncam produces takes way more time. I haven&#8217;t timed the complete run, but it was more than 1 hour. And here is the result (the southern half of the journey in 2013 anyway):<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone wp-image-1421\" src=\"https:\/\/marcel-jan.eu\/datablog\/wp-content\/uploads\/2022\/05\/USA2013_videoplot_completer2-300x280.png\" alt=\"\" width=\"988\" height=\"922\" \/><\/p>\n<p>You can find the Python code I&#8217;ve used to create this here:<\/p>\n<p><a href=\"https:\/\/github.com\/Marcel-Jan\/media_gpsplot\/blob\/main\/mp4_gpsplot.py\">https:\/\/github.com\/Marcel-Jan\/media_gpsplot\/blob\/main\/mp4_gpsplot.py<\/a><\/p>\n<p>&nbsp;<\/p>\n<h2>Next steps<\/h2>\n<p>So I hope my father will be happy that I can create maps from his videos. Of course this isn&#8217;t the kind of solution my father can run by himself. He isn&#8217;t a developer. For now I will have to install Python on his PC and run it myself. Maybe I can create a GUI that points to a directory of videos and returns a jpg and that he can run some day. But I never had a lot of success with Python GUIs. So don&#8217;t hold your breath for this one.<\/p>\n<p>&nbsp;<\/p>\n<p>Other blogposts I wrote about geo data in Python:<\/p>\n<p><a href=\"https:\/\/marcel-jan.eu\/datablog\/2022\/05\/23\/adding-the-track-of-my-bike-ride-on-a-folium-map\/\">Adding the track of my bike ride in Folium (Antpaths and Polylines)<\/a><\/p>\n<p><a href=\"https:\/\/marcel-jan.eu\/datablog\/2022\/05\/11\/photo-locations-marker-icons-and-displaying-photos-on-my-map\/\">Photo location markers and displaying photos on a map (Accessing exif data in JPGs with Python, projecting photos on a Folium map).<\/a><\/p>\n<p><a href=\"https:\/\/marcel-jan.eu\/datablog\/2022\/05\/04\/making-my-video-location-map-even-better-with-folium\/\">Making a map of video locations in Folium<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>So far I&#8217;ve found geolocations in XML metadata that my actioncam stores on disk as seperate .XML files and I&#8217;ve found them in JPG files. When I showed the cool maps I made to my father, he asked if I could create maps from his holiday videos. So that he [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[191,55,75],"tags":[329,331,327,330,328,332],"class_list":["post-1419","post","type-post","status-publish","format-standard","hentry","category-data-engineering","category-howto","category-python","tag-avchd","tag-embedded-metadata","tag-exiftool","tag-mp4","tag-os-subprocess-run","tag-trkpt"],"_links":{"self":[{"href":"https:\/\/marcel-jan.eu\/datablog\/wp-json\/wp\/v2\/posts\/1419","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/marcel-jan.eu\/datablog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/marcel-jan.eu\/datablog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/marcel-jan.eu\/datablog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/marcel-jan.eu\/datablog\/wp-json\/wp\/v2\/comments?post=1419"}],"version-history":[{"count":6,"href":"https:\/\/marcel-jan.eu\/datablog\/wp-json\/wp\/v2\/posts\/1419\/revisions"}],"predecessor-version":[{"id":1441,"href":"https:\/\/marcel-jan.eu\/datablog\/wp-json\/wp\/v2\/posts\/1419\/revisions\/1441"}],"wp:attachment":[{"href":"https:\/\/marcel-jan.eu\/datablog\/wp-json\/wp\/v2\/media?parent=1419"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/marcel-jan.eu\/datablog\/wp-json\/wp\/v2\/categories?post=1419"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/marcel-jan.eu\/datablog\/wp-json\/wp\/v2\/tags?post=1419"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}