From 9a25d1d27afb08c75009666503ce0fa04e34ab8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reinhard=20Ni=C3=9Fl?= Date: Sat, 19 Sep 2026 00:07:08 +0200 Subject: [PATCH 1/4] Ignore indoor GPS9 default dates when finding video t=0 Hero 11 and Max 2 start on a fake 2021-03 calendar. Treat GPS time as usable only after day 8000 and a 10/100 ms-aligned clock. CAMM extraction can stop at the first good point. Co-authored-by: Cursor --- mapillary_tools/camm/camm_parser.py | 8 +++++- mapillary_tools/geo.py | 43 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/mapillary_tools/camm/camm_parser.py b/mapillary_tools/camm/camm_parser.py index b5f42049..c380d79a 100644 --- a/mapillary_tools/camm/camm_parser.py +++ b/mapillary_tools/camm/camm_parser.py @@ -60,7 +60,11 @@ class CAMMInfo: model: str = "" -def extract_camm_info(fp: T.BinaryIO, telemetry_only: bool = False) -> CAMMInfo | None: +def extract_camm_info( + fp: T.BinaryIO, + telemetry_only: bool = False, + first_gps_only: bool = False, +) -> CAMMInfo | None: moov = MovieBoxParser.parse_stream(fp) make, model = "", "" @@ -122,6 +126,8 @@ def extract_camm_info(fp: T.BinaryIO, telemetry_only: bool = False) -> CAMMInfo gps.append(measurement) elif isinstance(measurement, geo.Point): mini_gps.append(measurement) + if first_gps_only and geo.point_has_usable_gps_clock(measurement): + break _normalize_gps_epochs(gps, make, moov) diff --git a/mapillary_tools/geo.py b/mapillary_tools/geo.py index d070c82a..357ac25a 100644 --- a/mapillary_tools/geo.py +++ b/mapillary_tools/geo.py @@ -19,6 +19,49 @@ WGS84_b = 6356752.314245 WGS84_b_SQ = WGS84_b**2 +# GPS9 days-since-2000-01-01. 8000 is a round lid above the indoor +# default clock seen on Hero 11 (GX010081) and MAX 2 (GS010131): +# both start at day 7736 (2021-03-07); MAX 2 then sits on 7742 +# (2021-03-13, time-of-day already right) before jumping to a real +# date. 8000 d = 2021-11-26, still before Hero 11 launch (2022-09-14). +GPS9_EPOCH = datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc) +GPS9_MIN_DAYS = 8000 + + +def gps_datetime_is_valid(epoch: float) -> bool: + """True if the GPS clock is usable for back-calculating video t=0. + + Empirically (Hero 11 GX010081 and MAX 2 GS010131: started indoors, + walked outside until the GPS indicator lit, walked back inside): + both cameras open on GPS9 day 7736 (2021-03-07). MAX 2 then parks + on day 7742 (2021-03-13) with a plausible time-of-day before the + calendar jumps. A usable date appears next; fix and DOP come later. + Back indoors, fix and DOP drop out while the time keeps running — + the camera then synthesizes it. + + ``8000`` is a round threshold above those indoor defaults + (2000-01-01 + 8000 d = 2021-11-26) and still before Hero 11 + launched (2022-09-14). readmp4 ``valid`` is ``day >= 8000`` and + milliseconds-of-day agreeing when rounded to 100 ms and to 10 ms. + Fix and DOP are ignored. + """ + try: + dt = datetime.datetime.fromtimestamp(epoch, tz=datetime.timezone.utc) + except (OSError, OverflowError, ValueError): + return False + if (dt - GPS9_EPOCH).days < GPS9_MIN_DAYS: + return False + msec = ( + ((dt.hour * 60 + dt.minute) * 60 + dt.second) * 1000 + + dt.microsecond // 1000 + ) + return 100 * int(round(msec / 100.0)) == 10 * int(round(msec / 10.0)) + + +def point_has_usable_gps_clock(point: Point) -> bool: + epoch = point.get_gps_epoch_time() + return epoch is not None and gps_datetime_is_valid(epoch) + @dataclasses.dataclass(order=True) class Point: From 321bde6f114f3ed7f88896e10dfb3ff99aeebb4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reinhard=20Ni=C3=9Fl?= Date: Sat, 19 Sep 2026 00:44:17 +0200 Subject: [PATCH 2/4] Drop the readmp4 reference from the GPS9 clock comment Spell out the 10 ms time-of-day grid so the check does not depend on an internal dump tool. Co-authored-by: Cursor --- mapillary_tools/geo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mapillary_tools/geo.py b/mapillary_tools/geo.py index 357ac25a..780510d6 100644 --- a/mapillary_tools/geo.py +++ b/mapillary_tools/geo.py @@ -41,9 +41,9 @@ def gps_datetime_is_valid(epoch: float) -> bool: ``8000`` is a round threshold above those indoor defaults (2000-01-01 + 8000 d = 2021-11-26) and still before Hero 11 - launched (2022-09-14). readmp4 ``valid`` is ``day >= 8000`` and - milliseconds-of-day agreeing when rounded to 100 ms and to 10 ms. - Fix and DOP are ignored. + launched (2022-09-14). Usable samples also need a time-of-day + that sits on a 10 ms grid (real GPS does; the indoor placeholder + often does not). Fix quality and DOP are ignored. """ try: dt = datetime.datetime.fromtimestamp(epoch, tz=datetime.timezone.utc) From 29383b1e4ad8bf934e2167c4d65617b426979479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reinhard=20Ni=C3=9Fl?= Date: Sun, 20 Sep 2026 22:06:20 +0200 Subject: [PATCH 3/4] ruff format GPS9 clock tests Co-authored-by: Cursor --- mapillary_tools/geo.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mapillary_tools/geo.py b/mapillary_tools/geo.py index 780510d6..888b96f0 100644 --- a/mapillary_tools/geo.py +++ b/mapillary_tools/geo.py @@ -51,10 +51,7 @@ def gps_datetime_is_valid(epoch: float) -> bool: return False if (dt - GPS9_EPOCH).days < GPS9_MIN_DAYS: return False - msec = ( - ((dt.hour * 60 + dt.minute) * 60 + dt.second) * 1000 - + dt.microsecond // 1000 - ) + msec = ((dt.hour * 60 + dt.minute) * 60 + dt.second) * 1000 + dt.microsecond // 1000 return 100 * int(round(msec / 100.0)) == 10 * int(round(msec / 10.0)) From 2e5ce4734518183c53cfa10366a0adafed71e213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reinhard=20Ni=C3=9Fl?= Date: Sun, 20 Sep 2026 22:14:03 +0200 Subject: [PATCH 4/4] Fix GPS9 helper to use get_unix_time for mypy Co-authored-by: Cursor --- mapillary_tools/camm/camm_parser.py | 6 +++++- mapillary_tools/geo.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/mapillary_tools/camm/camm_parser.py b/mapillary_tools/camm/camm_parser.py index c380d79a..013c54e4 100644 --- a/mapillary_tools/camm/camm_parser.py +++ b/mapillary_tools/camm/camm_parser.py @@ -126,7 +126,11 @@ def extract_camm_info( gps.append(measurement) elif isinstance(measurement, geo.Point): mini_gps.append(measurement) - if first_gps_only and geo.point_has_usable_gps_clock(measurement): + if ( + first_gps_only + and isinstance(measurement, geo.Point) + and geo.point_has_usable_gps_clock(measurement) + ): break _normalize_gps_epochs(gps, make, moov) diff --git a/mapillary_tools/geo.py b/mapillary_tools/geo.py index 888b96f0..160320ef 100644 --- a/mapillary_tools/geo.py +++ b/mapillary_tools/geo.py @@ -56,7 +56,7 @@ def gps_datetime_is_valid(epoch: float) -> bool: def point_has_usable_gps_clock(point: Point) -> bool: - epoch = point.get_gps_epoch_time() + epoch = point.get_unix_time() return epoch is not None and gps_datetime_is_valid(epoch)