Plot a drone flight path with folium¶
In [1]:
import os
from datetime import datetime
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS
import folium
In [2]:
# Define the folder containing drone images
photo_folder = r"C:\Users\beste\Desktop\GIS_DU_cert\GIS4760\WEEK3\1ECSCRoad2019" # UPDATE this path
Make a Flight path Map with Markers¶
In [4]:
# -------- Utility Functions for EXIF GPS Extraction --------
def convert_to_degrees(value, ref):
"""
Convert the GPS coordinates stored in the EXIF to degrees in float format.
'value' is a tuple like (degrees, minutes, seconds) and 'ref' is the reference (N, S, E, W).
"""
d, m, s = value
degrees = d + (m / 60.0) + (s / 3600.0)
if ref in ["S", "W"]:
degrees *= -1
return degrees
def get_gps_and_time(image_path):
"""
Extract the GPS coordinates and capture time from an image's EXIF metadata.
Returns a tuple (latitude, longitude, capture_time) if available, otherwise (None, None, None).
"""
try:
img = Image.open(image_path)
exif_data = img._getexif()
if not exif_data:
raise ValueError("No EXIF data found.")
gps_info = {}
capture_time = None
# Extract GPS and capture time from EXIF
for tag, value in exif_data.items():
tag_name = TAGS.get(tag)
# Extract capture time using the tag 'DateTimeOriginal'
if tag_name == "DateTimeOriginal":
capture_time = datetime.strptime(value, "%Y:%m:%d %H:%M:%S")
# Extract GPS info if the tag is GPSInfo
if tag_name == "GPSInfo":
for key, val in value.items():
gps_tag = GPSTAGS.get(key)
gps_info[gps_tag] = val
# Check that the necessary GPS tags exist
if ("GPSLatitude" in gps_info and "GPSLongitude" in gps_info and
"GPSLatitudeRef" in gps_info and "GPSLongitudeRef" in gps_info):
lat = convert_to_degrees(gps_info["GPSLatitude"], gps_info["GPSLatitudeRef"])
lon = convert_to_degrees(gps_info["GPSLongitude"], gps_info["GPSLongitudeRef"])
else:
lat, lon = None, None
return lat, lon, capture_time
except Exception as e:
print(f"Error processing {image_path}: {e}")
return None, None, None
# -------- Main Script --------
def main():
# A list to hold dictionaries with metadata from each photo
photos_data = []
# Loop through files in the folder
for filename in os.listdir(photo_folder):
if filename.lower().endswith(('.jpg', '.jpeg', '.png')):
file_path = os.path.join(photo_folder, filename)
lat, lon, capture_time = get_gps_and_time(file_path)
if lat is not None and lon is not None and capture_time:
photos_data.append({
"filename": filename,
"path": file_path,
"lat": lat,
"lon": lon,
"time": capture_time
})
else:
print(f"Skipping {filename}: Incomplete metadata.")
# Check that we have valid data
if not photos_data:
print("No images with valid GPS and timestamp metadata found.")
return
# Sort photos by capture time to define the flight path order
photos_data.sort(key=lambda x: x["time"])
# Use the first point to center the map
start_lat, start_lon = photos_data[0]["lat"], photos_data[0]["lon"]
folium_map = folium.Map(location=[start_lat, start_lon], zoom_start=16)
# Lists for coordinates (ordered by time) for drawing the flight path
path_coordinates = []
# Add a marker for each image, replacing intermediate markers with CircleMarkers
for idx, photo in enumerate(photos_data):
lat, lon = photo["lat"], photo["lon"]
timestamp = photo["time"].strftime("%Y-%m-%d %H:%M:%S")
popup_text = f"{photo['filename']}<br>{timestamp}"
if idx == 0:
# Starting point marker (using a pin with a green icon)
marker = folium.Marker(
location=[lat, lon],
popup=folium.Popup(f"Start:<br>{popup_text}", max_width=300),
icon=folium.Icon(color='green', icon='play')
)
elif idx == len(photos_data) - 1:
# Ending point marker (using a pin with a red icon)
marker = folium.Marker(
location=[lat, lon],
popup=folium.Popup(f"End:<br>{popup_text}", max_width=300),
icon=folium.Icon(color='red', icon='stop')
)
else:
# Intermediate photo marker as a circle, less visually obstructive
marker = folium.CircleMarker(
location=[lat, lon],
radius=5, # Adjust radius for size
color='blue', # Border color of the circle
fill=True,
fill_color='blue', # Fill color of the circle
fill_opacity=0.7,
popup=folium.Popup(popup_text, max_width=300)
)
marker.add_to(folium_map)
path_coordinates.append((lat, lon))
# Draw a polyline connecting the points to show the flight path
folium.PolyLine(locations=path_coordinates, weight=3, color='blue').add_to(folium_map)
# Optionally, add arrows or direction indicators via plugins if needed.
# For simplicity, this example just connects the points.
# Save the map to an HTML file
output_map = "drone_flight_path.html"
folium_map.save(output_map)
print(f"Map has been saved to {output_map}")
if __name__ == "__main__":
main()
In [ ]:
import os
from datetime import datetime
from PIL import Image,ExifTags
from PIL.ExifTags import TAGS, GPSTAGS
import folium
from folium.plugins import MiniMap, Fullscreen, MeasureControl, Draw
from branca.element import Template, MacroElement
from folium.plugins import FloatImage
import pprint
In [ ]:
test_image= r"C:\Users\beste\Desktop\GIS_DU_cert\GIS4760\WEEK3\1ECSCRoad2019\ECSCRoad20190831FAR3-16.jpg"
In [33]:
def print_full_metadata(image_path):
"""
Opens an image and prints all EXIF metadata tags.
"""
img = Image.open(image_path)
exif_data = img._getexif()
if not exif_data:
print("No EXIF metadata found.")
return
# Map numeric tags to human-readable names
metadata = {}
for tag, value in exif_data.items():
tag_name = ExifTags.TAGS.get(tag, tag)
metadata[tag_name] = value
# Pretty-print the metadata dictionary
pprint.pprint(metadata)
# Example usage - update the path to one of your drone images
print_full_metadata(test_image)
More detailed MetaData Extraction¶
In [35]:
from typing import List, Optional, Tuple, Dict, Any
import subprocess
import json
In [55]:
def extract_metadata_exiftool(image_path: str) -> Optional[Dict[str, Any]]:
"""Extracts metadata from an image using exiftool."""
try:
exiftool_path = r"C:\ExifTool\exiftool-13.25_64\exiftool.exe" # ***YOUR FULL PATH HERE***
command = [
exiftool_path, # Use the full path
"-j",
"-n",
"-m",
image_path,
]
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
creationflags=subprocess.CREATE_NO_WINDOW)
stdout, stderr = process.communicate()
if stderr:
print(f"exiftool error: {stderr.decode()}")
return None
metadata_list = json.loads(stdout.decode())
if metadata_list:
return metadata_list[0]
else:
return None
except FileNotFoundError:
print(f"Error: ExifTool not found at {exiftool_path}. Please check the path.")
return None
except Exception as e:
print(f"Error extracting metadata: {e}")
return None
Extract altitude data if needed¶
In [45]:
def get_gps_alt_time(image_path: str) -> Optional[Dict[str, Any]]:
"""
Extracts the GPS coordinates, altitude, and capture time from an image's EXIF metadata.
Returns a dictionary containing 'lat', 'lon', 'alt', and 'time' if available.
"""
try:
img = Image.open(image_path)
exif_data = img._getexif()
if not exif_data:
raise ValueError("No EXIF data found.")
gps_info = {}
capture_time = None
# Iterate over EXIF tags
for tag, value in exif_data.items():
tag_name = TAGS.get(tag)
# Extract capture time from DateTimeOriginal
if tag_name == "DateTimeOriginal":
capture_time = datetime.strptime(value, "%Y:%m:%d %H:%M:%S")
# Extract GPS info if tag is GPSInfo
if tag_name == "GPSInfo":
for key, val in value.items():
gps_tag = GPSTAGS.get(key)
gps_info[gps_tag] = val
# Extract latitude and longitude from GPS info
if ("GPSLatitude" in gps_info and "GPSLongitude" in gps_info and
"GPSLatitudeRef" in gps_info and "GPSLongitudeRef" in gps_info):
lat = convert_to_degrees(gps_info["GPSLatitude"], gps_info["GPSLatitudeRef"])
lon = convert_to_degrees(gps_info["GPSLongitude"], gps_info["GPSLongitudeRef"])
else:
lat, lon = None, None
# Extract altitude if available
alt = gps_info.get("GPSAltitude", None)
return {"lat": lat, "lon": lon, "alt": alt, "time": capture_time}
except Exception as e:
print(f"Error processing {image_path}: {e}")
return None
Plot MetaData: Altitude¶
In [46]:
import pandas as pd
import matplotlib.pyplot as plt
In [47]:
# -------- Example: Build and Plot Altitude Data --------
def process_images_and_plot_altitude(photo_folder: str):
"""
Iterates through a folder of images, extracts GPS data (including altitude),
and plots altitude vs. capture time in a Jupyter Notebook.
"""
photos_data = []
for filename in os.listdir(photo_folder):
if filename.lower().endswith(('.jpg', '.jpeg', '.png')):
file_path = os.path.join(photo_folder, filename)
data = get_gps_alt_time(file_path)
if data and data["lat"] is not None and data["lon"] is not None and data["time"]:
photos_data.append({
"filename": filename,
"path": file_path,
"lat": data["lat"],
"lon": data["lon"],
"alt": data["alt"],
"time": data["time"]
})
else:
print(f"Skipping {filename}: Incomplete metadata.")
if not photos_data:
print("No images with valid GPS and timestamp metadata found.")
return
# Sort photos by capture time
photos_data.sort(key=lambda x: x["time"])
# Build DataFrame for plotting
df = pd.DataFrame({
"timestamp": [d["time"] for d in photos_data],
"altitude": [d["alt"] for d in photos_data]
})
# Plot altitude vs. time
plt.figure(figsize=(10, 5))
plt.plot(df["timestamp"], df["altitude"], marker='o', linestyle='-', color='blue')
plt.xlabel("Time")
plt.ylabel("Altitude")
plt.title("Drone Altitude Change Over Flight")
plt.xticks(rotation=45)
plt.grid(True)
plt.tight_layout()
plt.show()
In [ ]:
# -------- Example usage 1 --------
# Update this path to your folder containing drone images
photo_folder = r"C:\Users\beste\Desktop\GIS_DU_cert\GIS4760\WEEK3\1ECSCRoad2019"
process_images_and_plot_altitude(photo_folder)
In [49]:
# -------- Example usage 2 --------
# Update this path to your folder containing drone images
photo_folder = r"C:\Users\beste\Desktop\GIS_DU_cert\GIS4760\WEEK3\2ECSCRoad2023"
process_images_and_plot_altitude(photo_folder)
In [52]:
test_image= r"C:\Users\beste\Desktop\GIS_DU_cert\GIS4760\WEEK3\2ECSCRoad2023\DJI_20230903165605_0001_D.JPG"
In [56]:
# Example Usage (replace with your image path)
# image_path = r"C:\Users\beste\Desktop\DroneData\LocalPond\DJI_0073.JPG"
metadata = extract_metadata_exiftool(test_image)
if metadata:
pprint.pprint(metadata)
else:
print("Metadata extraction failed.")
In [ ]:
# Make sure you have a north arrow image file available at the specified path
# north_arrow_image = r"C:\Projects\my_git_pages_website\Py-and-Sky-Labs\content\Drones\OC1KI90.jpg"
In [21]:
def get_gps_and_time(image_path):
"""
Extract the GPS coordinates and capture time from an image's EXIF metadata.
Returns a tuple (latitude, longitude, capture_time) if available, otherwise (None, None, None).
"""
try:
img = Image.open(image_path)
exif_data = img._getexif()
if not exif_data:
raise ValueError("No EXIF data found.")
gps_info = {}
capture_time = None
# Iterate over all EXIF tags to extract GPS and timestamp data
for tag, value in exif_data.items():
tag_name = TAGS.get(tag)
# Extract capture time using the 'DateTimeOriginal' tag
if tag_name == "DateTimeOriginal":
capture_time = datetime.strptime(value, "%Y:%m:%d %H:%M:%S")
# Extract GPS info from the 'GPSInfo' tag
if tag_name == "GPSInfo":
for key, val in value.items():
gps_tag = GPSTAGS.get(key)
gps_info[gps_tag] = val
# Ensure that all necessary GPS tags are available
if ("GPSLatitude" in gps_info and "GPSLongitude" in gps_info and
"GPSLatitudeRef" in gps_info and "GPSLongitudeRef" in gps_info):
lat = convert_to_degrees(gps_info["GPSLatitude"], gps_info["GPSLatitudeRef"])
lon = convert_to_degrees(gps_info["GPSLongitude"], gps_info["GPSLongitudeRef"])
else:
lat, lon = None, None
return lat, lon, capture_time
except Exception as e:
print(f"Error processing {image_path}: {e}")
return None, None, None
In [24]:
test_image= r"C:\Users\beste\Desktop\GIS_DU_cert\GIS4760\WEEK3\1ECSCRoad2019\ECSCRoad20190831FAR3-16.jpg"
In [25]:
get_gps_and_time(test_image)
Out[25]:
In [43]:
# Define the folder containing drone images
photo_folder = r"C:\Users\beste\Desktop\GIS_DU_cert\GIS4760\WEEK3\2ECSCRoad2023" # UPDATE this path
In [51]:
# ------------ Utility Functions for EXIF GPS Extraction ------------
def convert_to_degrees(value, ref):
"""
Convert the GPS coordinates stored in the EXIF to decimal degrees.
'value' is a tuple like (degrees, minutes, seconds) and 'ref' is the directional reference (N, S, E, W).
"""
d, m, s = value
degrees = d + (m / 60.0) + (s / 3600.0)
if ref in ["S", "W"]:
degrees *= -1
return degrees
def get_gps_and_time(image_path):
"""
Extract the GPS coordinates and capture time from an image's EXIF metadata.
Returns a tuple (latitude, longitude, capture_time) if available, otherwise (None, None, None).
"""
try:
img = Image.open(image_path)
exif_data = img._getexif()
if not exif_data:
raise ValueError("No EXIF data found.")
gps_info = {}
capture_time = None
# Iterate over all EXIF tags to extract GPS and timestamp data
for tag, value in exif_data.items():
tag_name = TAGS.get(tag)
# Extract capture time using the 'DateTimeOriginal' tag
if tag_name == "DateTimeOriginal":
capture_time = datetime.strptime(value, "%Y:%m:%d %H:%M:%S")
# Extract GPS info from the 'GPSInfo' tag
if tag_name == "GPSInfo":
for key, val in value.items():
gps_tag = GPSTAGS.get(key)
gps_info[gps_tag] = val
# Ensure that all necessary GPS tags are available
if ("GPSLatitude" in gps_info and "GPSLongitude" in gps_info and
"GPSLatitudeRef" in gps_info and "GPSLongitudeRef" in gps_info):
lat = convert_to_degrees(gps_info["GPSLatitude"], gps_info["GPSLatitudeRef"])
lon = convert_to_degrees(gps_info["GPSLongitude"], gps_info["GPSLongitudeRef"])
else:
lat, lon = None, None
return lat, lon, capture_time
except Exception as e:
print(f"Error processing {image_path}: {e}")
return None, None, None
# ------------ Main Script ------------
def main():
# A list to hold dictionaries with metadata from each photo
photos_data = []
# Loop through files in the photo folder
for filename in os.listdir(photo_folder):
if filename.lower().endswith(('.jpg', '.jpeg', '.png')):
file_path = os.path.join(photo_folder, filename)
lat, lon, capture_time = get_gps_and_time(file_path)
if lat is not None and lon is not None and capture_time:
photos_data.append({
"filename": filename,
"path": file_path,
"lat": lat,
"lon": lon,
"time": capture_time
})
else:
print(f"Skipping {filename}: Incomplete metadata.")
# Check that we have valid data
if not photos_data:
print("No images with valid GPS and timestamp metadata found.")
return
# Sort the photos by capture time to order the flight path
photos_data.sort(key=lambda x: x["time"])
# Use the first point to center the map
start_lat, start_lon = photos_data[0]["lat"], photos_data[0]["lon"]
basemap= 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'
folium_map = folium.Map(location=[start_lat, start_lon], zoom_start=16, control_scale=True, tiles=basemap,attr='Esri World Imagery')
# Lists for coordinates (ordered by time) for drawing the flight path
path_coordinates = []
# Add markers for each image
for idx, photo in enumerate(photos_data):
lat, lon = photo["lat"], photo["lon"]
timestamp = photo["time"].strftime("%Y-%m-%d %H:%M:%S")
popup_text = f"{photo['filename']}<br>{timestamp}"
if idx == 0:
# Starting point marker (pin with a green icon)
marker = folium.Marker(
location=[lat, lon],
popup=folium.Popup(f"Start:<br>{popup_text}", max_width=300),
icon=folium.Icon(color='green', icon='play')
)
elif idx == len(photos_data) - 1:
# Ending point marker (pin with a red icon)
marker = folium.Marker(
location=[lat, lon],
popup=folium.Popup(f"End:<br>{popup_text}", max_width=300),
icon=folium.Icon(color='red', icon='stop')
)
else:
# Intermediate photo marker as a less obstructive circle marker
marker = folium.CircleMarker(
location=[lat, lon],
radius=5, # Adjust radius for size
color='blue', # Border color
fill=True,
fill_color='blue', # Fill color
fill_opacity=0.7,
popup=folium.Popup(popup_text, max_width=300)
)
# float_image = FloatImage(north_arrow_image, bottom=80, left=80) ## UNCOMMENT FOR ADD STOCK IMAGES TO MAP
# float_image.add_to(folium_map) ## UNCOMMENT FOR ADD STOCK IMAGES TO MAP
marker.add_to(folium_map)
path_coordinates.append((lat, lon))
# Draw a polyline connecting the points to show the flight path
folium.PolyLine(locations=path_coordinates, weight=3, color='blue').add_to(folium_map)
# ------------------ Adding Plugins ------------------
# Add a MiniMap (a small overview map)
MiniMap().add_to(folium_map)
# Add a Fullscreen control
Fullscreen().add_to(folium_map)
# Add a Measurement tool to measure distances on the map
MeasureControl().add_to(folium_map)
# Add drawing tools to let users draw shapes on the map (with export enabled)
Draw(export=True).add_to(folium_map)
# ------------------ Adding Custom HTML Elements ------------------
# Update the legend template to have a macro that accepts parameters
legend_html = """
{% macro html(this, kwargs) %}
<div style="
position: fixed;
bottom: 50px; left: 50px; width: 200px; height: 150px;
border:2px solid grey; z-index:9999; font-size:14px;
background-color: white;
opacity: 0.8;
padding: 10px;">
<p style="margin:0; font-weight:bold;">ECSC 2023</p>
<p style="margin:0;"><i style="background:green; width:10px; height:10px; float:left; margin-right:5px; margin-top:2px;"></i>Start</p>
<p style="margin:0;"><i style="background:red; width:10px; height:10px; float:left; margin-right:5px; margin-top:2px;"></i>End</p>
<p style="margin:0;"><i style="background:blue; width:10px; height:10px; border-radius:50%; float:left; margin-right:5px; margin-top:2px;"></i>Intermediate</p>
<p style="margin:0;">Count: {{ point_count }}</p>
</div>
{% endmacro %}
"""
# Inject the count of points dynamically
legend_html = legend_html.replace("{{ point_count }}", str(len(photos_data)))
macro = MacroElement()
macro._template = Template(legend_html)
folium_map.get_root().add_child(macro)
# Add a title at the top of the map
title_html = '''
<h3 align="center" style="font-size:20px"><b>ECSC 2023</b></h3>
'''
folium_map.get_root().html.add_child(folium.Element(title_html))
# ------------------ Save the Map ------------------
output_map = "drone_flight_path.html"
folium_map.save(output_map)
print(f"Map has been saved to {output_map}")
if __name__ == "__main__":
main()