DJI Mavic, Air and Mini Drones
Friendly, Helpful & Knowledgeable Community
Join Us Now

Simple Tool with GUI for Extracting GeoTagged images from Videos+srt files (useful for photogrammetry)

Miro_Rava

Active Member
Joined
Jul 17, 2023
Messages
44
Reactions
15
Age
22
Location
Italy
Hi all, I just made this tool in python that given a folder containing videos (with srt files), images or both, extracts georeferenced frames from all the present videos at the specified interval, and updates the present photos with the absolute altitude above sea level if you know the altitude of the starting point.

the tool consists in two files, a gui (made with customtkinter, you can save this gui in a .pyw file to open it on double click) and a image/video processor (must be saved in the same folder of the gui and called alt_changer.py).

For the ones who do not want to install python and the necessary libraries , i've packaged this tool in an one-click exe with gui that is hosted on my website at this link: https://www.miro-rava.com/documents/DJI_GPS_Video_Editor.exe

1689609818614.png
If you want, you can package the exe by yourself using something like auto-py-to-exe (remember to install all the necessary libraries for the files to work using pip or conda before packaging the exe: customtkinter, opencv-python, exif, piexif, pillow)

this are the two files:
the gui:
Python:
import customtkinter
import subprocess
import sys
import os

customtkinter.set_appearance_mode("light")  # Modes: "System" (standard), "Dark", "Light"
customtkinter.set_default_color_theme("green")  # Themes: "blue" (standard), "green", "dark-blue"

app = customtkinter.CTk()
app.geometry("600x280")
app.title("Altitude Image Processor")


def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    try:
        # PyInstaller creates a temp folder and stores path in _MEIPASS
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)


def start_batch():

    subprocess.Popen(["python", resource_path("alt_changer.py"), folder_path.get(), entry_1.get(),entry_2.get()])

def browse_folder():
    # Allow user to select a directory and store it in global var
    # called folder_path
    global folder_path
    filename = customtkinter.filedialog.askdirectory()
    folder_path.set(filename)


frame_1 = customtkinter.CTkFrame(master=app)
frame_1.pack(pady=20, padx=20, fill="both", expand=True)

folder_path = customtkinter.StringVar()
folder_path.set("No Folder Selected")

button_1 = customtkinter.CTkButton(text="Chose Image Folder", master=frame_1, command=browse_folder)
button_1.pack(pady=10, padx=10)

label_2 = customtkinter.CTkLabel(master=frame_1, textvariable=folder_path, justify=customtkinter.LEFT)
label_2.pack(pady=10, padx=10)

entry_1 = customtkinter.CTkEntry(master=frame_1, placeholder_text="Enter Altitude in m")
entry_1.pack(pady=10, padx=10)

entry_2 = customtkinter.CTkEntry(master=frame_1, placeholder_text="Frames Interval in s")
entry_2.pack(pady=10, padx=10)

button_2 = customtkinter.CTkButton(text="Start Batch Processing", master=frame_1, command=start_batch)
button_2.pack(pady=10, padx=10)

if __name__ == '__main__':
    app.mainloop()

and the photo/video processor (needs to be named alt_changer.py in order to work with the gui, otherwise rename it in function start_batch() in the gui):
Python:
import os
import sys
import cv2
import piexif
import re
import exif
from PIL import Image
from fractions import Fraction

files = os.listdir(sys.argv[1])
percentage = 0

def progress_bar(current, total, bar_length=20):
    fraction = current / total

    arrow = int(fraction * bar_length - 1) * '-' + '>'
    padding = int(bar_length - len(arrow)) * ' '

    ending = '\n' if current == total else '\r'

    print(f'Progress: [{arrow}{padding}] {int(fraction*100)}%', end=ending)

def parse_srt_file(srt_path):
    latitudes = []
    longitudes = []
    altitudes = []

    with open(srt_path, 'r') as file:
        srt_content = file.read()

    pattern = r'\[latitude: ([\d.-]+)\] \[longitude: ([\d.-]+)\] \[altitude: ([\d.-]+)\]'
    matches = re.findall(pattern, srt_content)
    for match in matches:
        latitude = float(match[0])
        longitude = float(match[1])
        altitude = float(match[2])

        altitudes.append(altitude)
        latitudes.append(latitude)
        longitudes.append(longitude)

    return latitudes, longitudes, altitudes

# Convert the latitude and longitude to the required format (Rational)
def degrees_to_rational(number):
    degrees = int(abs(number))
    minutes = int((abs(number) - degrees) * 60)
    seconds = int(((abs(number) - degrees - minutes / 60) * 3600) * 100)

    return [(degrees, 1), (minutes, 1), (seconds, 100)]

print("Elaborating Videos with Timestamps if present:")

for videoPath in files:
    if videoPath[-4:] in [".MOV", ".mov", ".MP4", ".mp4"]:
        video_path = os.path.join(sys.argv[1], videoPath)
        video_name = os.path.splitext(os.path.basename(video_path))[0]
        srt_path = os.path.join(sys.argv[1], video_name + ".srt")
        capture = cv2.VideoCapture(video_path)
        frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
        frames_to_extract = range(0, frame_count-1, int(float(sys.argv[3]) * capture.get(cv2.CAP_PROP_FPS)))
        try:
            latitudes, longitudes, altitudes = parse_srt_file(srt_path)
        except FileNotFoundError:
            print(f"WARNING ---> Skipping Video: {video_name} ---> NO .SRT File found")
            continue
        for frame_index in frames_to_extract:
            capture.set(cv2.CAP_PROP_POS_FRAMES, frame_index)
            success, frame = capture.read()

            if success:
                frame_path = os.path.normpath(os.path.join(sys.argv[1], f'{video_name}_frame_{frame_index}.jpg'))
                cv2.imwrite(frame_path, frame)
                try:
                    frame_latitude = latitudes[frame_index]
                    frame_longitude = longitudes[frame_index]
                    frame_altitude = altitudes[frame_index]+int(sys.argv[2])
                except Exception as e:
                    print(f"Image not processed because ---> {e}")
                    continue

                if frame_latitude is not None and frame_longitude is not None and frame_altitude is not None:
                    try:
                        # Open the image file
                        image = Image.open(frame_path)

                        # Get the existing EXIF data
                        exif_dict = piexif.load(frame_path)

                        # Preserve existing GPS data
                        gps_info = exif_dict.get("GPS", {})
                        altitude_ref = gps_info.get(piexif.GPSIFD.GPSAltitudeRef, 0)
                        gps_version = exif_dict.get(piexif.GPSIFD.GPSVersionID, (2, 3, 0, 0))

                        new_lat_rational = degrees_to_rational(frame_latitude)
                        new_lon_rational = degrees_to_rational(frame_longitude)

                        # Update the GPS data in the EXIF metadata
                        exif_dict["GPS"] = {
                            piexif.GPSIFD.GPSLatitudeRef: 'N' if frame_latitude >= 0 else 'S',
                            piexif.GPSIFD.GPSLatitude: new_lat_rational,
                            piexif.GPSIFD.GPSLongitudeRef: 'E' if frame_longitude >= 0 else 'W',
                            piexif.GPSIFD.GPSLongitude: new_lon_rational,
                            piexif.GPSIFD.GPSVersionID: (2, 3, 0, 0),
                            piexif.GPSIFD.GPSAltitude: Fraction.from_float(frame_altitude).limit_denominator().as_integer_ratio(),
                            piexif.GPSIFD.GPSAltitudeRef: 0,
                        }

                        # Encode the EXIF data and save it back to the image
                        exif_bytes = piexif.dump(exif_dict)
                        image.save(frame_path, exif=exif_bytes)

                        print(f'{video_name}_frame_{frame_index}.jpg')
                    except Exception as e:
                        print(f"Image not processed because ---> {e}")


                else:
                    print(f"No GPS data found for frame: {frame_path}")
            else:
                print(f"Error extracting frame at index {frame_index}")

        capture.release()

print("Elaborating Images:")

for imagePath in files:
    if imagePath[-4:] in [".JPG", ".PNG", ".jpg", ".png"]:
        percentage += 100/len(files)
        full_imagePath = os.path.join(sys.argv[1], imagePath)
        with open(full_imagePath, 'rb') as image_file:
            img = exif.Image(image_file)
            img.gps_altitude = img.gps_altitude + int(sys.argv[2])
        ext = full_imagePath[-4:]
        tempPath = full_imagePath[:-4]
        with open(f"{tempPath}_mod{ext}", 'wb') as test_image_file:
            test_image_file.write(img.get_file())
        os.remove(full_imagePath)
        print(f"{percentage:.2f}%  --> Changed GPS Altitude of: {full_imagePath[-12:-4]}")
print("100.00% --> Done!!")

this code should work with all types of .SRT files from DJI drones, but in case it doesn't work, you can simply change the regex pattern in this line:
Python:
pattern = r'\[latitude: ([\d.-]+)\] \[longitude: ([\d.-]+)\] \[altitude: ([\d.-]+)\]'
using tome online regex editors and testers

let me know if this tool is helpful and any future improvements/features i can add to it
 
Hi
I'm a complete newbee in python scripting. Can you explain me how to execute your utility (the exe file) on a windows 11 system.
When I try, I get then gui screen and when I hit the batch button, I see a windows starting in the background but nothing happens...
Please help me
 
Hi
I'm a complete newbee in python scripting. Can you explain me how to execute your utility (the exe file) on a windows 11 system.
When I try, I get then gui screen and when I hit the batch button, I see a windows starting in the background but nothing happens...
Please help me
Hi, I tested the gui in exe file only with a Mavic air 2, the fact is that each model uses different subtitles tipes, in such a case you may need to use the python files directly and install all the libraries in order to tweek the regex line I mentioned in the post above.
Anyway, two weeks from now i plan on adding a selector to the utility in order to select your dji drone model type.
Another issue it could be is that your pc is set to block all external calls to other programs in the initial call wasn't run as an admin.
Try to see if this helps

As a reminder, in the gui you need to imput a folder containing the video and the .srt file related to the video, the altitude in meters of the starting point of the drone (if known), and also the delta in seconds ( decimal like 0.33 works also) between frames you want to extract from the video
 
Lycus Tech Mavic Air 3 Case

DJI Drone Deals

New Threads

Forum statistics

Threads
131,132
Messages
1,560,142
Members
160,103
Latest member
volidas