Stretch: carry over source EXIF/XMP when no JSON sidecar is present
Stretching an already-embedded thermal folder (e.g. Streifen_exif/thermal, which has tagged PNGs but no .json sidecars) produced bare 8-bit PNGs: the old code skipped frames with no sidecar, so nothing was embedded. _embed_exif now always copies the source image's EXIF/XMP via -tagsFromFile and overlays JSON-sidecar tags when available, so metadata transfers whether you stretch the raw session (sidecars) or already-embedded PNGs (source EXIF). GPS verification only fires when there was metadata to embed; otherwise it logs a clear note instead of a false "full disk" error. Verified: raw session and embedded-no-sidecar both yield 0 frames missing GPS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
8dadd71f9e
commit
6e6b675140
|
|
@ -39,7 +39,7 @@ whether you run from the session's parent or from a grouping folder.
|
||||||
| File | What it does | Output |
|
| File | What it does | Output |
|
||||||
|------|--------------|--------|
|
|------|--------------|--------|
|
||||||
| `embed_metadata.py` | Copies each image and writes its JSON sidecar into the copy's EXIF/XMP (GPS, true camera orientation, UTC time, lens/exposure, full JSON in `UserComment`). | `<session>_exif/` (mirrors layout; originals untouched) |
|
| `embed_metadata.py` | Copies each image and writes its JSON sidecar into the copy's EXIF/XMP (GPS, true camera orientation, UTC time, lens/exposure, full JSON in `UserComment`). | `<session>_exif/` (mirrors layout; originals untouched) |
|
||||||
| `stretch_thermal.py` | Rescales all thermal frames to one session-wide brightness window → 8-bit, with a palette (inferno/ironbow/gray). Embeds the sidecar EXIF into the output too. | `<session>/thermal_stretched/` |
|
| `stretch_thermal.py` | Rescales all thermal frames to one session-wide brightness window → 8-bit, with a palette (inferno/ironbow/gray). Carries metadata into each output: from the JSON sidecar if present, otherwise copied from the source image's own EXIF/XMP (so stretching already-embedded PNGs keeps GPS/orientation). | `<session>/thermal_stretched/` |
|
||||||
| `session_map.py` | Shared geometry: trigger points + oblique footprints; also the camera-orientation math used by `embed_metadata`. Not run directly. | — |
|
| `session_map.py` | Shared geometry: trigger points + oblique footprints; also the camera-orientation math used by `embed_metadata`. Not run directly. | — |
|
||||||
| `firemapper_gui.py` | Tkinter GUI wrapping all of the above. Tabs: **1. Embed**, **2. Thermal Stretch**, **3. Map**. | — |
|
| `firemapper_gui.py` | Tkinter GUI wrapping all of the above. Tabs: **1. Embed**, **2. Thermal Stretch**, **3. Map**. | — |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -164,33 +164,43 @@ def _embed_exif(pngs, out_dir: Path, exiftool=None, log=None):
|
||||||
except BaseException as e: # noqa: BLE001 (ensure_exiftool may sys.exit)
|
except BaseException as e: # noqa: BLE001 (ensure_exiftool may sys.exit)
|
||||||
log(f" ! skipping EXIF embed - exiftool unavailable ({e})")
|
log(f" ! skipping EXIF embed - exiftool unavailable ({e})")
|
||||||
return
|
return
|
||||||
arg_lines, dsts = [], []
|
arg_lines, dsts, srcs = [], [], []
|
||||||
|
have_sidecars = False
|
||||||
for p in pngs:
|
for p in pngs:
|
||||||
sidecar = p.with_suffix(".json")
|
|
||||||
if not sidecar.exists():
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
data = json.loads(sidecar.read_text(encoding="utf-8"))
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
continue
|
|
||||||
dst = out_dir / (p.stem + ".png")
|
dst = out_dir / (p.stem + ".png")
|
||||||
|
# 1) copy any metadata already on the SOURCE image (covers stretching
|
||||||
|
# already-embedded PNGs that have no JSON sidecar next to them);
|
||||||
|
# 2) overlay/refresh from the JSON sidecar when it exists (raw session).
|
||||||
|
seg = ["-overwrite_original", "-tagsFromFile", str(p), "-EXIF:all", "-XMP:all"]
|
||||||
|
sidecar = p.with_suffix(".json")
|
||||||
|
if sidecar.exists():
|
||||||
|
try:
|
||||||
|
data = json.loads(sidecar.read_text(encoding="utf-8"))
|
||||||
|
seg += embed.build_tags(data, {}, None, "gps")
|
||||||
|
have_sidecars = True
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
seg += [str(dst), "-execute"]
|
||||||
|
arg_lines += seg
|
||||||
dsts.append(dst)
|
dsts.append(dst)
|
||||||
arg_lines += ["-overwrite_original", *embed.build_tags(data, {}, None, "gps"),
|
srcs.append(p)
|
||||||
str(dst), "-execute"]
|
|
||||||
if not arg_lines:
|
if not arg_lines:
|
||||||
return
|
return
|
||||||
argfile = out_dir / "_exif_args.txt"
|
argfile = out_dir / "_exif_args.txt"
|
||||||
argfile.write_text("\n".join(arg_lines) + "\n", encoding="utf-8")
|
argfile.write_text("\n".join(arg_lines) + "\n", encoding="utf-8")
|
||||||
cmd = [et, "-m", "-charset", "UTF8", "-charset", "filename=UTF8", "-@", str(argfile)]
|
cmd = [et, "-m", "-charset", "UTF8", "-charset", "filename=UTF8", "-@", str(argfile)]
|
||||||
samples = {dsts[0], dsts[len(dsts) // 2], dsts[-1]} # spot-check first/mid/last
|
idx = sorted({0, len(dsts) // 2, len(dsts) - 1}) # spot-check first/mid/last
|
||||||
|
|
||||||
def gps_written():
|
def has_gps(path):
|
||||||
return all(subprocess.run([et, "-s", "-s", "-s", "-GPSLatitude", str(s)],
|
return bool(subprocess.run([et, "-s", "-s", "-s", "-GPSLatitude", str(path)],
|
||||||
capture_output=True, text=True).stdout.strip() for s in samples)
|
capture_output=True, text=True).stdout.strip())
|
||||||
|
|
||||||
|
# only expect GPS in the output if there was any to embed (sidecar or source EXIF)
|
||||||
|
expect_gps = have_sidecars or any(has_gps(srcs[i]) for i in idx)
|
||||||
|
|
||||||
log("Embedding GPS / orientation EXIF into the stretched frames ...")
|
log("Embedding GPS / orientation EXIF into the stretched frames ...")
|
||||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
if proc.returncode == 0 and not gps_written():
|
if expect_gps and proc.returncode == 0 and not all(has_gps(dsts[i]) for i in idx):
|
||||||
log(" ! GPS not present after first pass - retrying once ...")
|
log(" ! GPS not present after first pass - retrying once ...")
|
||||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
argfile.unlink(missing_ok=True)
|
argfile.unlink(missing_ok=True)
|
||||||
|
|
@ -198,13 +208,18 @@ def _embed_exif(pngs, out_dir: Path, exiftool=None, log=None):
|
||||||
err = (proc.stderr or "").strip()
|
err = (proc.stderr or "").strip()
|
||||||
if err:
|
if err:
|
||||||
log(err)
|
log(err)
|
||||||
if proc.returncode != 0 or not gps_written():
|
if proc.returncode != 0 or (expect_gps and not all(has_gps(dsts[i]) for i in idx)):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Could not embed GPS/EXIF into the stretched PNGs (exiftool exit "
|
"Could not embed GPS/EXIF into the stretched PNGs (exiftool exit "
|
||||||
f"{proc.returncode}). The usual cause is a FULL DISK - free up space and re-run. "
|
f"{proc.returncode}). The usual cause is a FULL DISK - free up space and re-run. "
|
||||||
"The stretched images were written; only the embedded metadata is missing."
|
"The stretched images were written; only the embedded metadata is missing."
|
||||||
+ (f"\nexiftool: {err}" if err else ""))
|
+ (f"\nexiftool: {err}" if err else ""))
|
||||||
log(f"EXIF embedded into {len(dsts)} stretched frames.")
|
if not expect_gps:
|
||||||
|
log(" note: no JSON sidecars and the source images carry no GPS - the stretched "
|
||||||
|
"PNGs have no location metadata. Stretch the raw session (with .json sidecars), "
|
||||||
|
"or run Embed first.")
|
||||||
|
else:
|
||||||
|
log(f"EXIF embedded into {len(dsts)} stretched frames.")
|
||||||
|
|
||||||
|
|
||||||
def stretch_session(session, out_dir=None, colormap="inferno", lo=None, hi=None,
|
def stretch_session(session, out_dir=None, colormap="inferno", lo=None, hi=None,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue