Plot Drone Fligt Path

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()
Map has been saved to drone_flight_path.html
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)
{'ApertureValue': 3.356144,
 'BodySerialNumber': 'f0813f7eef720fdad9901591125e2bd8',
 'ColorSpace': 1,
 'Contrast': 0,
 'CustomRendered': 0,
 'DateTime': '2019:09:09 21:47:52',
 'DateTimeDigitized': '2019:08:31 19:04:47',
 'DateTimeOriginal': '2019:08:31 19:04:47',
 'ExifOffset': 256,
 'ExifVersion': b'0230',
 'ExposureBiasValue': 0.0,
 'ExposureMode': 0,
 'ExposureProgram': 4,
 'ExposureTime': 0.01,
 'FNumber': 3.2,
 'FileSource': b'\x03',
 'Flash': 32,
 'FocalLength': 8.8,
 'FocalLengthIn35mmFilm': 24,
 'GPSInfo': {0: b'\x02\x03\x00\x00',
             1: 'N',
             2: (44.0, 16.0306, 0.0),
             3: 'W',
             4: (105.0, 27.6913, 0.0),
             5: b'\x00',
             6: 1418.421},
 'GainControl': 0,
 'ISOSpeedRatings': 100,
 'ImageDescription': 'DCIM\\101MEDIA\\DJI_0347.JPG',
 'LightSource': 1,
 'Make': 'DJI',
 'MaxApertureValue': 2.97,
 'MeteringMode': 1,
 'Model': 'FC6310',
 'ResolutionUnit': 2,
 'Saturation': 0,
 'SceneCaptureType': 0,
 'SceneType': b'\x01',
 'Sharpness': 0,
 'ShutterSpeedValue': 6.643856,
 'Software': 'Adobe Photoshop Lightroom Classic 7.1 (Macintosh)',
 'SubjectDistance': 0.0,
 'SubjectDistanceRange': 0,
 'WhiteBalance': 0,
 'XResolution': 240.0,
 'YResolution': 240.0}

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.")
{'ADJDebugInfo': '(Binary data 4096 bytes, use -b option to extract)',
 'AEDebugInfo': '(Binary data 8192 bytes, use -b option to extract)',
 'AEHistogramInfo': '(Binary data 4096 bytes, use -b option to extract)',
 'AELiveViewHistogramInfo': '(Binary data 2048 bytes, use -b option to '
                            'extract)',
 'AELiveViewLocalHistogram': '(Binary data 10000 bytes, use -b option to '
                             'extract)',
 'AELocalHistogram': '(Binary data 2048 bytes, use -b option to extract)',
 'AFDebugInfo': '(Binary data 1024 bytes, use -b option to extract)',
 'AWBDebugInfo': '(Binary data 10240 bytes, use -b option to extract)',
 'About': 'DJI Meta Data',
 'AbsoluteAltitude': '+1470.528',
 'AlreadyApplied': False,
 'AltitudeType': 'RtkAlt',
 'Aperture': 2.8,
 'ApertureValue': 2.79917173119039,
 'BitsPerSample': 8,
 'CalibratedFocalLength': 3725.151611,
 'CalibratedOpticalCenterX': 2640.0,
 'CalibratedOpticalCenterY': 1978.0,
 'CamReverse': 0,
 'CameraSerialNumber': '493OK924AB0P3D',
 'CaptureUUID': '6ad2f3fda7e14008b71f8a18505161a8',
 'CircleOfConfusion': 0.0153861892970321,
 'ColorComponents': 3,
 'ColorSpace': 1,
 'ComponentsConfiguration': '1 2 3 0',
 'Compression': 7,
 'Contrast': 0,
 'CreateDate': '2023:09:03 16:56:05',
 'CustomRendered': 0,
 'DateTimeOriginal': '2023:09:03 16:56:05',
 'DependentImage1EntryNumber': 0,
 'DependentImage2EntryNumber': 0,
 'DeviceSettingDescription': '(Binary data 4 bytes, use -b option to extract)',
 'DewarpData': '2022-06-08;3713.290000000000,3713.290000000000,7.020000000000,-8.720000000000,-0.112575240000,0.014874430000,-0.000085720000,0.000000100000,-0.027064110000',
 'DewarpFlag': 0,
 'DigitalZoomRatio': 1,
 'Directory': 'C:/Users/beste/Desktop/GIS_DU_cert/GIS4760/WEEK3/2ECSCRoad2023',
 'DroneModel': 'M3M',
 'DroneSerialNumber': '1581F5FKD235N00D695H',
 'EncodingProcess': 0,
 'ExifByteOrder': 'II',
 'ExifImageHeight': 3956,
 'ExifImageWidth': 5280,
 'ExifToolVersion': 13.25,
 'ExifVersion': '0230',
 'ExposureCompensation': 0,
 'ExposureMode': 0,
 'ExposureProgram': 2,
 'ExposureTime': 0.0005,
 'FNumber': 2.8,
 'FOV': 73.7398575770812,
 'FileAccessDate': '2025:04:10 15:40:02-05:00',
 'FileCreateDate': '2025:03:30 16:38:42-05:00',
 'FileModifyDate': '2025:04:09 22:51:42-05:00',
 'FileName': 'DJI_20230903165605_0001_D.JPG',
 'FilePermissions': 100666,
 'FileSize': 6545408,
 'FileSource': 3,
 'FileType': 'JPEG',
 'FileTypeExtension': 'JPG',
 'Flash': 0,
 'FlashpixVersion': '0100',
 'FlightPitchDegree': -2.9,
 'FlightRollDegree': '+5.40',
 'FlightXSpeed': 0.0,
 'FlightYSpeed': 0.3,
 'FlightYawDegree': '+158.20',
 'FlightZSpeed': 0.1,
 'FocalLength': 12.29,
 'FocalLength35efl': 24,
 'FocalLengthIn35mmFormat': 24,
 'Format': 'image/jpg',
 'GPSAltitude': 1470.528,
 'GPSAltitudeRef': 0,
 'GPSLatitude': 44.2700307777778,
 'GPSLatitudeRef': 'N',
 'GPSLongitude': -105.462745138889,
 'GPSLongitudeRef': 'W',
 'GPSMapDatum': 'WGS-84',
 'GPSPosition': '44.2700307777778 -105.462745138889',
 'GPSStatus': 'A',
 'GPSVersionID': '2 3 0 0',
 'GainControl': 0,
 'GimbalPitchDegree': -89.9,
 'GimbalReverse': 0,
 'GimbalRollDegree': '+0.00',
 'GimbalYawDegree': '+158.80',
 'GpsStatus': 'RTK',
 'HasCrop': False,
 'HasSettings': False,
 'Histogram': '(Binary data 1024 bytes, use -b option to extract)',
 'HyperfocalDistance': 3.50602221168415,
 'HyperlapsDebugInfo': '(Binary data 8 bytes, use -b option to extract)',
 'ISO': 100,
 'ImageDescription': 'default',
 'ImageHeight': 3956,
 'ImageSize': '5280 3956',
 'ImageUIDList': '(Binary data 66 bytes, use -b option to extract)',
 'ImageWidth': 5280,
 'InteropIndex': 'R98',
 'InteropVersion': '0100',
 'JFIFVersion': '1 2',
 'LensInfo': '0.24 0.24 2.8 11',
 'LightSource': 2,
 'LightValue': 13.9366379390026,
 'MIMEType': 'image/jpeg',
 'MPFVersion': '0100',
 'MPImageFlags': 8,
 'MPImageFormat': 0,
 'MPImageLength': 785222,
 'MPImageStart': 5758976,
 'MPImageType': 65537,
 'Make': 'DJI',
 'MakerNoteUnknownText': 'DJI MakerNotes',
 'MaxApertureValue': 2.79917173119039,
 'Megapixels': 20.88768,
 'MeteringMode': 1,
 'Model': 'M3M',
 'ModifyDate': '2023:09:03 16:56:05',
 'NumberOfImages': 2,
 'Orientation': 1,
 'PreviewImage': '(Binary data 785222 bytes, use -b option to extract)',
 'RelativeAltitude': '+100.026',
 'ResolutionUnit': 2,
 'RtkDiffAge': 0.0,
 'RtkFlag': 16,
 'RtkStdHgt': 2.68655,
 'RtkStdLat': 1.42011,
 'RtkStdLon': 1.02085,
 'Saturation': 0,
 'ScaleFactor35efl': 1.95280716029292,
 'Scap_Info': '(Binary data 8192 bytes, use -b option to extract)',
 'SceneCaptureType': 0,
 'SceneType': 1,
 'SelfData': '',
 'SensitivityType': 2,
 'SensorID': '493OK924AB0P3D',
 'SerialNumber': '1581F5FKD235N00D695H',
 'Sharpness': 0,
 'ShutterCount': 14511,
 'ShutterSpeed': 0.0005,
 'ShutterSpeedValue': '0.000499994553508582',
 'ShutterType': 'Mechanical',
 'Sisr_Info': '(Binary data 4096 bytes, use -b option to extract)',
 'Software': '10.12.05.45',
 'SourceFile': 'C:/Users/beste/Desktop/GIS_DU_cert/GIS4760/WEEK3/2ECSCRoad2023/DJI_20230903165605_0001_D.JPG',
 'SubjectDistance': 0,
 'SurveyingMode': 1,
 'ThumbnailImage': '(Binary data 25983 bytes, use -b option to extract)',
 'ThumbnailLength': 25983,
 'ThumbnailOffset': 1350,
 'TotalFrames': 1,
 'UTCAtExposure': '2023:09:03 22:54:18.734043',
 'Version': 7.0,
 'WhiteBalance': 0,
 'XPComment': '0.9.142',
 'XPKeywords': 'single',
 'XResolution': 72,
 'YCbCrPositioning': 2,
 'YCbCrSubSampling': '2 2',
 'YResolution': 72,
 'ZoneIdentifier': 'Exists'}
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]:
(44.267176666666664,
 -105.46152166666667,
 datetime.datetime(2019, 8, 31, 19, 4, 47))
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()
Map has been saved to drone_flight_path.html

links

social