exif to gpx cd
This commit is contained in:
parent
291f3113c4
commit
db9261a3d6
85
exif2gpx.py
85
exif2gpx.py
|
|
@ -5,12 +5,14 @@ exif2gpx.py — generuje plik GPX z danych GPS w EXIF zdjęć (JPG/JPEG).
|
|||
|
||||
Użycie:
|
||||
pip install Pillow
|
||||
python exif2gpx.py /sciezka/do/folderu_lub_pliku output.gpx --tz +02:00
|
||||
python exif2gpx.py /sciezka/do/folderu_lub_pliku output.gpx --tz +02:00 --from-date 2024-01-01 --to-date 2024-12-31
|
||||
|
||||
Opcje:
|
||||
--tz Strefa czasowa, w której interpretować czasy EXIF (gdy EXIF nie ma TZ),
|
||||
format +HH:MM lub -HH:MM (np. +02:00). Jeśli podasz, czasy zostaną zapisane
|
||||
w GPX w UTC (z literą 'Z'). Jeśli nie podasz, <time> będzie pominięty.
|
||||
--tz Strefa czasowa, w której interpretować czasy EXIF (gdy EXIF nie ma TZ),
|
||||
format +HH:MM lub -HH:MM (np. +02:00). Jeśli podasz, czasy zostaną zapisane
|
||||
w GPX w UTC (z literą 'Z'). Jeśli nie podasz, <time> będzie pominięty.
|
||||
--from-date Uwzględniaj punkty od tej daty (włącznie). Format: YYYY-MM-DD[THH:MM[:SS]]
|
||||
--to-date Uwzględniaj punkty do tej daty (włącznie). Format: YYYY-MM-DD[THH:MM[:SS]]
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
|
|
@ -39,18 +41,15 @@ def rational_to_float(x) -> float:
|
|||
if x is None:
|
||||
raise ValueError("Brak wartości rational")
|
||||
|
||||
# 1) już liczba
|
||||
if isinstance(x, Real):
|
||||
return float(x)
|
||||
|
||||
# 2) IFDRational (Pillow) albo coś z numerator/denominator
|
||||
if hasattr(x, "numerator") and hasattr(x, "denominator"):
|
||||
den = x.denominator
|
||||
if den == 0:
|
||||
raise ValueError("Mianownik równy zero w rational")
|
||||
return float(x.numerator) / float(den)
|
||||
|
||||
# 3) krotka/lista (num, den)
|
||||
if isinstance(x, (tuple, list)) and len(x) == 2:
|
||||
num, den = x
|
||||
den = float(den)
|
||||
|
|
@ -92,7 +91,6 @@ def parse_exif_datetime(raw_dt: Optional[str], subsec: Optional[str]) -> Optiona
|
|||
return None
|
||||
try:
|
||||
base = datetime.strptime(raw_dt, "%Y:%m:%d %H:%M:%S")
|
||||
# Subsekundy bywają różne: np. "123", "12", czasem z innymi znakami
|
||||
if subsec:
|
||||
only_digits = "".join(ch for ch in str(subsec) if ch.isdigit())
|
||||
if only_digits:
|
||||
|
|
@ -103,6 +101,38 @@ def parse_exif_datetime(raw_dt: Optional[str], subsec: Optional[str]) -> Optiona
|
|||
raise ValueError(f"Nie można sparsować daty EXIF '{raw_dt}' ({subsec=}): {e}")
|
||||
|
||||
|
||||
def parse_cli_datetime(s: str) -> datetime:
|
||||
"""
|
||||
Parsuje datę z CLI. Akceptowane formaty (naive, bez strefy):
|
||||
- YYYY-MM-DD
|
||||
- YYYY-MM-DD HH:MM
|
||||
- YYYY-MM-DD HH:MM:SS
|
||||
- YYYY-MM-DDTHH:MM
|
||||
- YYYY-MM-DDTHH:MM:SS
|
||||
Zwraca naive datetime.
|
||||
"""
|
||||
if not s:
|
||||
raise ValueError("Pusta data")
|
||||
formats = [
|
||||
"%Y-%m-%d",
|
||||
"%Y-%m-%d %H:%M",
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
"%Y-%m-%dT%H:%M",
|
||||
"%Y-%m-%dT%H:%M:%S",
|
||||
]
|
||||
last_err = None
|
||||
for fmt in formats:
|
||||
try:
|
||||
dt = datetime.strptime(s, fmt)
|
||||
if fmt == "%Y-%m-%d":
|
||||
return dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
return dt
|
||||
except Exception as e:
|
||||
last_err = e
|
||||
continue
|
||||
raise ValueError(f"Nie rozpoznano formatu daty: {s!r} ({last_err})")
|
||||
|
||||
|
||||
def apply_tz_and_to_utc(dt: datetime, tz_offset: Optional[str]) -> Optional[datetime]:
|
||||
"""
|
||||
Jeśli podano --tz w formacie +HH:MM lub -HH:MM, interpretuj naive dt w tej strefie,
|
||||
|
|
@ -136,7 +166,6 @@ def extract_exif_gps_and_time(img_path: Path) -> Tuple[Optional[float], Optional
|
|||
except Exception as e:
|
||||
raise ValueError(f"Nie można otworzyć lub odczytać EXIF: {e}")
|
||||
|
||||
# Zamiana kluczy numerycznych na nazwy
|
||||
exif_readable = {}
|
||||
for tag, val in exif.items():
|
||||
name = ExifTags.TAGS.get(tag, tag)
|
||||
|
|
@ -151,7 +180,6 @@ def extract_exif_gps_and_time(img_path: Path) -> Tuple[Optional[float], Optional
|
|||
"GPSLongitude" in gps and "GPSLongitudeRef" in gps:
|
||||
lat = dms_to_decimal(gps["GPSLatitude"], gps["GPSLatitudeRef"])
|
||||
lon = dms_to_decimal(gps["GPSLongitude"], gps["GPSLongitudeRef"])
|
||||
# Wysokość (opcjonalna)
|
||||
if "GPSAltitude" in gps:
|
||||
try:
|
||||
ele = rational_to_float(gps["GPSAltitude"])
|
||||
|
|
@ -160,7 +188,6 @@ def extract_exif_gps_and_time(img_path: Path) -> Tuple[Optional[float], Optional
|
|||
except Exception as e:
|
||||
raise ValueError(f"Nie można zinterpretować wysokości: {e}")
|
||||
|
||||
# Czas
|
||||
raw_dt = exif_readable.get("DateTimeOriginal") or exif_readable.get("DateTime") or exif_readable.get("DateTimeDigitized")
|
||||
subsec = exif_readable.get("SubsecTimeOriginal") or exif_readable.get("SubSecTimeOriginal") or exif_readable.get("SubsecTime")
|
||||
naive_dt = parse_exif_datetime(raw_dt, subsec) if raw_dt else None
|
||||
|
|
@ -206,7 +233,6 @@ def generate_gpx(points: List[dict], creator: str = "exif2gpx") -> ET.ElementTre
|
|||
ele = ET.SubElement(trkpt, "ele")
|
||||
ele.text = f"{p['ele']:.2f}"
|
||||
if p.get("time"):
|
||||
# GPX wymaga ISO 8601 w UTC z 'Z'
|
||||
t = ET.SubElement(trkpt, "time")
|
||||
iso = p["time"].strftime("%Y-%m-%dT%H:%M:%S.%f")
|
||||
iso = iso.rstrip("0").rstrip(".") + "Z"
|
||||
|
|
@ -222,8 +248,29 @@ def main():
|
|||
ap.add_argument("input", help="Plik JPG lub folder ze zdjęciami")
|
||||
ap.add_argument("output", help="Ścieżka wyjściowa pliku GPX")
|
||||
ap.add_argument("--tz", help="Strefa czasowa EXIF w formacie +HH:MM lub -HH:MM (np. +02:00)", default=None)
|
||||
ap.add_argument("--from-date", dest="from_date", help="Uwzględniaj punkty od tej daty (włącznie). Format: YYYY-MM-DD[THH:MM[:SS]]", default=None)
|
||||
ap.add_argument("--to-date", dest="to_date", help="Uwzględniaj punkty do tej daty (włącznie). Format: YYYY-MM-DD[THH:MM[:SS]]", default=None)
|
||||
args = ap.parse_args()
|
||||
|
||||
# Parsowanie filtrów daty
|
||||
from_dt = to_dt = None
|
||||
if args.from_date:
|
||||
try:
|
||||
from_dt = parse_cli_datetime(args.from_date)
|
||||
if args.tz:
|
||||
from_dt = apply_tz_and_to_utc(from_dt, args.tz)
|
||||
except Exception as e:
|
||||
print(f"Ostrzeżenie: nie można zinterpretować --from-date: {e}", file=sys.stderr)
|
||||
from_dt = None
|
||||
if args.to_date:
|
||||
try:
|
||||
to_dt = parse_cli_datetime(args.to_date)
|
||||
if args.tz:
|
||||
to_dt = apply_tz_and_to_utc(to_dt, args.tz)
|
||||
except Exception as e:
|
||||
print(f"Ostrzeżenie: nie można zinterpretować --to-date: {e}", file=sys.stderr)
|
||||
to_dt = None
|
||||
|
||||
src = Path(args.input)
|
||||
out = Path(args.output)
|
||||
|
||||
|
|
@ -252,6 +299,17 @@ def main():
|
|||
except Exception as e:
|
||||
print(f"Ostrzeżenie: nieprawidłowa strefa czasowa dla {fp.name}: {e}", file=sys.stderr)
|
||||
|
||||
# Filtrowanie po datach (inclusive). Jeśli filtry ustawione, wymagamy czasu w zdjęciu.
|
||||
if args.from_date or args.to_date:
|
||||
base_dt = time_utc if args.tz else naive_dt
|
||||
if base_dt is None:
|
||||
print(f"Pomijam {fp}: brak czasu przy aktywnych filtrach daty", file=sys.stderr)
|
||||
continue
|
||||
if from_dt and (base_dt < from_dt):
|
||||
continue
|
||||
if to_dt and (base_dt > to_dt):
|
||||
continue
|
||||
|
||||
points.append({
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
|
|
@ -267,7 +325,6 @@ def main():
|
|||
|
||||
# Sortowanie: po czasie jeśli dostępny; potem po nazwie pliku
|
||||
def sort_key(p):
|
||||
# preferuj p['time'] (UTC) gdy jest; w przeciwnym razie naive_time; w ostateczności nazwa
|
||||
primary = p["time"] if p["time"] is not None else p["naive_time"]
|
||||
return (primary or datetime.max, p["source"])
|
||||
|
||||
|
|
@ -279,4 +336,4 @@ def main():
|
|||
print(f"Zapisano GPX: {out}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
11
link4exif2pgx.sh
Normal file
11
link4exif2pgx.sh
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
from=$1
|
||||
to=$2
|
||||
|
||||
mkdir -p "$to"
|
||||
|
||||
# użycie find z -print0, żeby prawidłowo obsłużyć spacje i dziwne znaki
|
||||
find "$from" -type f -print0 | while IFS= read -r -d '' i; do
|
||||
ln -s "$i" "$to/$(basename "$i")"
|
||||
done
|
||||
|
|
@ -46,4 +46,7 @@ Podsumowanie:
|
|||
|
||||
odwrotnie
|
||||
|
||||
python exif2gpx.py /ścieżka/do/folderu/ze/zdjęciami output.gpx --tz +02:00
|
||||
python exif2gpx.py /ścieżka/do/folderu/ze/zdjęciami output.gpx --tz +02:00
|
||||
|
||||
for i in $(find /home/pi/immich/library/upload/7c9720a6-b22a-44bc-8e3c-f5ed92374732/ -type f); do ln -s $i; done
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue