From 8dadd71f9e62d5677d2c0e7432c1a137e0f2c9d1 Mon Sep 17 00:00:00 2001 From: NoahC Date: Fri, 26 Jun 2026 10:13:07 +0200 Subject: [PATCH] Surface exiftool write failures so thermal stretch can't silently drop GPS On a (near-)full disk, exiftool's temp-copy write partially fails, producing stretched thermal PNGs with most tags but no GPS. The stretch step was ignoring exiftool's exit code, so this happened silently. stretch_thermal._embed_exif now verifies GPS landed (spot-checks first/middle/ last frames), retries once on a transient miss, and otherwise raises a clear "free up disk space and re-run" error (the pixels are written; only metadata is missing). embed_metadata.embed_session's exit-code error gets the same hint. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 4 ++++ embed_metadata.py | 4 +++- stretch_thermal.py | 27 ++++++++++++++++++++++++--- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b042ead..a1abb0b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -97,6 +97,10 @@ writable in stock exiftool). It uses this geometry — **not** the raw IMU. `tools/exiftool.exe`, the OliverBetz user install (`%LOCALAPPDATA%\Programs\ExifTool`), else auto-installed via `winget install OliverBetz.ExifTool`. (SourceForge auto-download is unreliable — GDPR/consent wall.) +- **Disk space:** exiftool writes metadata via a temp copy, so a (near-)full disk causes + partial/failed writes — symptom is an image with most tags but **no GPS**. The embed and + stretch steps now check exiftool's exit code, verify GPS landed, retry once, and raise a + clear "full disk" error instead of silently producing GPS-less files. - **GPS source:** cam frames use the fused INS `position` block (falls back to raw `gps`); thermal has only `gps`. UTC time = GPS week/tow − 18 leap seconds. - **Thermal stretch window:** default = pooled 1–99th percentile across the whole session diff --git a/embed_metadata.py b/embed_metadata.py index 90a073c..3f18dee 100644 --- a/embed_metadata.py +++ b/embed_metadata.py @@ -280,7 +280,9 @@ def embed_session(session, out_root=None, gps_source="position", exiftool=None, proc.wait() argfile.unlink(missing_ok=True) if proc.returncode != 0: - raise RuntimeError(f"exiftool exited with code {proc.returncode}") + raise RuntimeError( + f"exiftool exited with code {proc.returncode}. The usual cause is a FULL DISK " + "(metadata is written via a temp copy) - free up space and re-run.") progress(1.0, "Done") log(f"\nDone. {updated} files tagged.\nOutput: {out_root}") diff --git a/stretch_thermal.py b/stretch_thermal.py index 214005b..c4a0e32 100644 --- a/stretch_thermal.py +++ b/stretch_thermal.py @@ -164,7 +164,7 @@ def _embed_exif(pngs, out_dir: Path, exiftool=None, log=None): except BaseException as e: # noqa: BLE001 (ensure_exiftool may sys.exit) log(f" ! skipping EXIF embed - exiftool unavailable ({e})") return - arg_lines = [] + arg_lines, dsts = [], [] for p in pngs: sidecar = p.with_suffix(".json") if not sidecar.exists(): @@ -174,17 +174,38 @@ def _embed_exif(pngs, out_dir: Path, exiftool=None, log=None): except Exception: # noqa: BLE001 continue dst = out_dir / (p.stem + ".png") + dsts.append(dst) arg_lines += ["-overwrite_original", *embed.build_tags(data, {}, None, "gps"), str(dst), "-execute"] if not arg_lines: return argfile = out_dir / "_exif_args.txt" argfile.write_text("\n".join(arg_lines) + "\n", encoding="utf-8") + cmd = [et, "-m", "-charset", "UTF8", "-charset", "filename=UTF8", "-@", str(argfile)] + samples = {dsts[0], dsts[len(dsts) // 2], dsts[-1]} # spot-check first/mid/last + + def gps_written(): + return all(subprocess.run([et, "-s", "-s", "-s", "-GPSLatitude", str(s)], + capture_output=True, text=True).stdout.strip() for s in samples) + log("Embedding GPS / orientation EXIF into the stretched frames ...") - subprocess.run([et, "-m", "-charset", "UTF8", "-charset", "filename=UTF8", - "-@", str(argfile)], capture_output=True, text=True) + proc = subprocess.run(cmd, capture_output=True, text=True) + if proc.returncode == 0 and not gps_written(): + log(" ! GPS not present after first pass - retrying once ...") + proc = subprocess.run(cmd, capture_output=True, text=True) argfile.unlink(missing_ok=True) + err = (proc.stderr or "").strip() + if err: + log(err) + if proc.returncode != 0 or not gps_written(): + raise RuntimeError( + "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. " + "The stretched images were written; only the embedded metadata is missing." + + (f"\nexiftool: {err}" if err else "")) + log(f"EXIF embedded into {len(dsts)} stretched frames.") + def stretch_session(session, out_dir=None, colormap="inferno", lo=None, hi=None, embed_exif=True, exiftool=None, progress=None, log=None) -> dict: