Holgers Sammelsurium

Fotografie, Reisen, Nerdkrams

Data-Mining im Foto-Ordner

Seit bestimmt 10 Jahren tagge ich praktisch jedes Foto, das ich mache, halbmanuell mit den GPS-Koordinaten des Aufnahmeortes. Mittlerweile geht das auch sehr einfach, da es für jedes Smartphone OS unzählige GPS Tracker gibt, die auf Reisen bequem GPX Tracks mit Zeitstempel und GPS Koordinaten erzeugen. Solche Tracks lassen sich in Software wie Lightroom über den Zeitstempel im Foto recht einfach mit den Fotos verknüpfen. Bilder, die mit dem Smartphone geschossen werden, haben praktisch immer GPS Koordinaten, auch ohne eigenes Zutun.

Gestern hatte ich die Idee, ein kleines Skript zu schreiben, das meinen Server nach Bildern durchsucht und – falls vorhanden – die GPS-Koordinaten extrahiert und sich „merkt“. Da der Scanvorgang je nach Geschwindigkeit des Servers und der Menge der Bilder sehr lange dauern kann, speichere ich die gefundenen Koordinaten in einer Datei. Auf meinem Raspberry Pi 4 dauerte der Scan durch zehntausende Bilder knapp eine Stunde. Auf diese Weise entsteht quasi eine Datenbank, in der jede Position verzeichnet ist, an der ich ein Foto gemacht habe.

Das Script sieht wie folgt aus:

import os, exifread, contextlib, numpy
from progress.bar import Bar

def convert_to_degress(value):
    d = float(Fraction(str(value[0])))
    m = float(Fraction(str(value[1])))
    s = float(Fraction(str(value[2])))
    return d + (m / 60.0) + (s / 3600.0)

def get_gps_info(image_path):
    with open(image_path, 'rb') as f:
        try:
            with contextlib.redirect_stderr(None):
                tags = exifread.process_file(f)
                gps_latitude = tags.get('GPS GPSLatitude')
                gps_latitude_ref = tags.get('GPS GPSLatitudeRef')
                gps_longitude = tags.get('GPS GPSLongitude')
                gps_longitude_ref = tags.get('GPS GPSLongitudeRef')
                if gps_latitude and gps_latitude_ref and gps_longitude and gps_longitude_ref:
                    latitude = convert_to_degress([float(x.num) / float(x.den) for x in gps_latitude.values])
                    longitude = convert_to_degress([float(x.num) / float(x.den) for x in gps_longitude.values])
                    if gps_latitude_ref.values[0] == 'S':
                        latitude = -latitude
                    if gps_longitude_ref.values[0] == 'W':
                        longitude = -longitude
                    return latitude, longitude
                else:
                    return None, None
        except Exception as e:
            return None, None

def fileNameTester(filename):
    extensions = ["jpg", "jpeg", "heic", "arw", "dng"]
    filename = filename.lower().split(".")
    if filename[-1] in extensions:
        return True
    else:
        return False

def getPins(dir, outputFile):
    pins = []
    roots = []
    for root, dirs, files in os.walk(dir):
        for filename in files:
            if root not in roots:
                roots.append(root)
                bar = Bar(f"Scanning {root:<80}", max=len(files))
            bar.next()
            if fileNameTester(filename):
                latitude, longitude = get_gps_info(os.path.join(root, filename))
                if longitude and latitude:
                    if (latitude, longitude) not in pins:
                        pins.append((latitude, longitude))
        bar.finish()
    pins_NP = numpy.array(pins)
    numpy.save(outputFile, pins_NP)

outputFile = "pins.npy"
inputDir = "/media/disk/Fotos/JPEG/2023"
getPins(inputDir, outputFile)

Ein zweites kleines Script liest diese Datei wieder ein und markiert die Koordinaten auf einer Karte. Das Ergebnis dieses Prozesses ist eine html-Datei, in die man bequem im Browser hineinzoomen kann.

Je nach Anzahl der gefundenen Koordinaten ist es sinnvoll, nicht alle Koordinaten in die Karte einzutragen. Stattdessen empfiehlt es sich, die Koordinaten z.B. nach ein oder zwei Dezimalstellen zu „runden“ bzw. abzuschneiden. Die im Umkreis aufgenommenen Bilder werden so quasi an einem Ort zusammengefasst.

Dieses Script sieht wie folgt aus:

import folium, matplotlib, numpy

def cleanPins(file):
    cleanedPins = {}
    pins = numpy.load(file)
    for pin in pins:
        lat, lon = pin
        latc = round(lat, 2) #Anzahl der Nachkommastellen
        lonc = round(lon, 2) #Anzahl der Nachkommastellen
        pinc = (latc, lonc)
        if pinc not in cleanedPins:
            cleanedPins[pinc] = 1 #Anzahl der Bilder an der "gerundeten" Position
        else:
            cleanedPins[pinc] += 1
    return cleanedPins

def map_values_to_colors(value, vmin, vmax):
    norm = matplotlib.colors.Normalize(vmin=vmin, vmax=vmax)
    cmap = matplotlib.colormaps['coolwarm']
    r, g, b , x = cmap(norm(value))
    r = round(r * 255)
    g = round(g * 255)
    b = round(b * 255)
    return f"#{r:x}{g:x}{b:x}"

def makeMap(pins, outputMap):
    map = folium.Map(location=[48.17, 11.75], zoom_start=8)
    for pin in pins:
        lat, lon = pin
        n = pins[pin]
        max = 200
        if n <= max:
            ic = map_values_to_colors(n, 1, max)
        else:
            ic = "red"
        marker = folium.CircleMarker(location=[lat, lon], popup=f"{n}", color=ic, fill=True)
        marker.add_to(map)
    map.save(outputMap)

inputFile ="pins.npy"
outputMap = "map.html"
cleanedPins = cleanPins(inputFile)
makeMap(cleanedPins, outputMap)

Beitrag veröffentlicht am

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert