From 63b511a4be058922264bdfbc1fcd3b94513ee13c Mon Sep 17 00:00:00 2001 From: Tao Peng Date: Mon, 24 Mar 2025 22:49:01 -0700 Subject: [PATCH 1/7] fix geotag_videos --- tests/cli/exif_read.py | 43 +++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/tests/cli/exif_read.py b/tests/cli/exif_read.py index 618da2b8c..1b8f4ea64 100644 --- a/tests/cli/exif_read.py +++ b/tests/cli/exif_read.py @@ -40,32 +40,28 @@ def as_dict(exif: ExifReadABC): } -def _round(v): - if isinstance(v, float): - return round(v, 6) - elif isinstance(v, tuple): - return tuple(round(x, 6) for x in v) - else: - return v +def _approximate(left, right): + if isinstance(left, float) and isinstance(right, float): + return abs(left - right) < 0.000001 + if isinstance(left, tuple) and isinstance(right, tuple): + return all(abs(l - r) < 0.000001 for l, r in zip(left, right)) + return left == right -def compare_exif(left: dict, right: dict) -> bool: +def compare_exif(left: dict, right: dict) -> str: RED_COLOR = "\x1b[31;20m" RESET_COLOR = "\x1b[0m" - diff = False + diff = [] for key in left: if key in ["width", "height"]: continue if key in ["lon_lat", "altitude", "direction"]: - left_value = _round(left[key]) - right_value = _round(right[key]) + same = _approximate(left[key], right[key]) else: - left_value = left[key] - right_value = right[key] - if left_value != right_value: - print(f"{RED_COLOR}{key}: {left_value} != {right_value}{RESET_COLOR}") - diff = True - return diff + same = left[key] == right[key] + if not same: + diff.append(f"{RED_COLOR}{key}: {left[key]} != {right[key]}{RESET_COLOR}") + return "\n".join(diff) def extract_and_show_from_exiftool(fp, compare: bool = False): @@ -82,10 +78,19 @@ def extract_and_show_from_exiftool(fp, compare: bool = False): native_exif = ExifRead(image_path) diff = compare_exif(as_dict(exif), as_dict(native_exif)) if diff: - print(f"======== ExifTool Outuput {image_path} ========") + print(f"======== {image_path} ========") + + print("ExifTool Outuput:") pprint.pprint(as_dict(exif)) - print(f"======== ExifRead Output {image_path} ========") + print() + + print("ExifRead Output:") pprint.pprint(as_dict(native_exif)) + print() + + print("DIFF:") + print(diff) + print() else: print(f"======== ExifTool Outuput {image_path} ========") pprint.pprint(as_dict(exif)) From eac05bd8c96f45615fc95bcef38c7be0c4f89986 Mon Sep 17 00:00:00 2001 From: Tao Peng Date: Tue, 25 Mar 2025 11:35:22 -0700 Subject: [PATCH 2/7] fix --- tests/cli/blackvue_parser.py | 13 +++++++++++-- tests/cli/gpmf_parser.py | 37 ++++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/tests/cli/blackvue_parser.py b/tests/cli/blackvue_parser.py index a6f5a5bdc..7433dd102 100644 --- a/tests/cli/blackvue_parser.py +++ b/tests/cli/blackvue_parser.py @@ -4,13 +4,22 @@ import gpxpy import gpxpy.gpx -from mapillary_tools import utils +from mapillary_tools import geo, utils from mapillary_tools.geotag import blackvue_parser, utils as geotag_utils +def _parse_gpx(path: pathlib.Path) -> list[geo.Point] | None: + with path.open("rb") as fp: + points = blackvue_parser.extract_points(fp) + return points + + def _convert_to_track(path: pathlib.Path): track = gpxpy.gpx.GPXTrack() - points = blackvue_parser.parse_gps_points(path) + points = _parse_gpx(path) + if points is None: + raise ValueError("Not a valid BlackVue video") + segment = geotag_utils.convert_points_to_gpx_segment(points) track.segments.append(segment) with path.open("rb") as fp: diff --git a/tests/cli/gpmf_parser.py b/tests/cli/gpmf_parser.py index 1cbd11b16..cd46ccdb4 100644 --- a/tests/cli/gpmf_parser.py +++ b/tests/cli/gpmf_parser.py @@ -66,26 +66,34 @@ def _convert_points_to_gpx_track_segment( return gpx_segment +def _parse_gpx(path: pathlib.Path) -> list[telemetry.GPSPoint]: + with path.open("rb") as fp: + info = gpmf_parser.extract_gopro_info(fp) + if info is None: + return [] + return info.gps or [] + + def _convert_gpx(gpx: gpxpy.gpx.GPX, path: pathlib.Path): - points = gpmf_parser.parse_gpx(path) + points = _parse_gpx(path) gpx_track = gpxpy.gpx.GPXTrack() gpx_track_segment = _convert_points_to_gpx_track_segment(points) gpx_track.segments.append(gpx_track_segment) gpx_track.name = path.name gpx_track.comment = f"#points: {len(points)}" with path.open("rb") as fp: - device_names = gpmf_parser.extract_all_device_names(fp) - with path.open("rb") as fp: - model = gpmf_parser.extract_camera_model(fp) + info = gpmf_parser.extract_gopro_info(fp) + if info is None: + return gpx_track.description = ( - f'Extracted from model "{model}" among these devices {device_names}' + f'Extracted from model "{info.model}" and make "{info.make}"' ) gpx.tracks.append(gpx_track) def _convert_geojson(path: pathlib.Path): features = [] - points = gpmf_parser.parse_gpx(path) + points = _parse_gpx(path) for idx, p in enumerate(points): geomtry = {"type": "Point", "coordinates": [p.lon, p.lat]} @@ -138,24 +146,33 @@ def main(): imu_option = parsed_args.imu.split(",") for path in video_paths: with path.open("rb") as fp: - telemetry_data = gpmf_parser.extract_telemetry_data(fp) + telemetry_data = gpmf_parser.extract_gopro_info(fp, telemetry_only=True) if telemetry_data: if "accl" in imu_option: print( json.dumps( - [dataclasses.asdict(accl) for accl in telemetry_data.accl] + [ + dataclasses.asdict(accl) + for accl in telemetry_data.accl or [] + ] ) ) if "gyro" in imu_option: print( json.dumps( - [dataclasses.asdict(gyro) for gyro in telemetry_data.gyro] + [ + dataclasses.asdict(gyro) + for gyro in telemetry_data.gyro or [] + ] ) ) if "magn" in imu_option: print( json.dumps( - [dataclasses.asdict(magn) for magn in telemetry_data.magn] + [ + dataclasses.asdict(magn) + for magn in telemetry_data.magn or [] + ] ) ) From ca650953c83d6488652600e5c84a29bec5985e03 Mon Sep 17 00:00:00 2001 From: Tao Peng Date: Tue, 25 Mar 2025 14:54:51 -0700 Subject: [PATCH 3/7] check types --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 67a44a2d8..a7515e58f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -81,7 +81,7 @@ jobs: - name: Type check with mypy run: | - mypy mapillary_tools + mypy mapillary_tools tests/cli - name: Test with pytest run: | From 7587d5484934f623b4b5d427e6ec4309ce5959fa Mon Sep 17 00:00:00 2001 From: Tao Peng Date: Tue, 25 Mar 2025 15:24:26 -0700 Subject: [PATCH 4/7] chore --- tests/cli/upload_api_v4.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/cli/upload_api_v4.py b/tests/cli/upload_api_v4.py index b1ec80d75..54718aa5a 100644 --- a/tests/cli/upload_api_v4.py +++ b/tests/cli/upload_api_v4.py @@ -83,10 +83,10 @@ def main(): ) initial_offset = service.fetch_offset() - LOG.info(f"Session key: %s", session_key) - LOG.info(f"Entity size: %d", entity_size) - LOG.info(f"Initial offset: %s", initial_offset) - LOG.info(f"Chunk size: %s MB", service.chunk_size / (1024 * 1024)) + LOG.info("Session key: %s", session_key) + LOG.info("Entity size: %d", entity_size) + LOG.info("Initial offset: %s", initial_offset) + LOG.info("Chunk size: %s MB", service.chunk_size / (1024 * 1024)) with open(parsed.filename, "rb") as fp: with tqdm.tqdm( From d18722fd9a098ee77329328a4732591688dc54fe Mon Sep 17 00:00:00 2001 From: Tao Peng Date: Tue, 25 Mar 2025 16:14:56 -0700 Subject: [PATCH 5/7] fix --- tests/cli/camm_parser.py | 10 +++- tests/cli/exif_read.py | 11 ++++- tests/cli/simple_mp4_parser.py | 83 ++++++++++++++-------------------- 3 files changed, 53 insertions(+), 51 deletions(-) diff --git a/tests/cli/camm_parser.py b/tests/cli/camm_parser.py index 1e80676f9..d9fd0a5fe 100644 --- a/tests/cli/camm_parser.py +++ b/tests/cli/camm_parser.py @@ -11,8 +11,16 @@ from mapillary_tools.geotag import utils as geotag_utils +def _parse_gpx(path: pathlib.Path): + with path.open("rb") as fp: + return camm_parser.extract_points(fp) + + def _convert(path: pathlib.Path): - points = camm_parser.parse_gpx(path) + points = _parse_gpx(path) + if points is None: + raise RuntimeError(f"Invalid CAMM video {path}") + track = gpxpy.gpx.GPXTrack() track.name = path.name track.segments.append(geotag_utils.convert_points_to_gpx_segment(points)) diff --git a/tests/cli/exif_read.py b/tests/cli/exif_read.py index 1b8f4ea64..723261d19 100644 --- a/tests/cli/exif_read.py +++ b/tests/cli/exif_read.py @@ -25,12 +25,19 @@ def extract_and_show_exif(image_path): def as_dict(exif: ExifReadABC): + if isinstance(exif, (ExifToolRead, ExifRead)): + gps_datetime = exif.extract_gps_datetime() + exif_time = exif.extract_exif_datetime() + else: + gps_datetime = None + exif_time = None + return { "altitude": exif.extract_altitude(), "capture_time": exif.extract_capture_time(), "direction": exif.extract_direction(), - "exif_time": exif.extract_exif_datetime(), - "gps_time": exif.extract_gps_datetime(), + "exif_time": exif_time, + "gps_time": gps_datetime, "lon_lat": exif.extract_lon_lat(), "make": exif.extract_make(), "model": exif.extract_model(), diff --git a/tests/cli/simple_mp4_parser.py b/tests/cli/simple_mp4_parser.py index da8e3f294..11aa60d8d 100644 --- a/tests/cli/simple_mp4_parser.py +++ b/tests/cli/simple_mp4_parser.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import io import logging @@ -31,40 +33,29 @@ } -def _validate_samples( - path: pathlib.Path, filters: T.Optional[T.Container[bytes]] = None -): - samples: T.List[sample_parser.RawSample] = [] - - with open(path, "rb") as fp: - for h, s in sparser.parse_path( - fp, [b"moov", b"trak", b"mdia", b"minf", b"stbl"] - ): - ( - descriptions, - raw_samples, - ) = sample_parser.parse_raw_samples_from_stbl_DEPRECATED( - s, maxsize=h.maxsize - ) - samples.extend( - sample - for sample in raw_samples - if filters is None - or descriptions[sample.description_idx]["format"] in filters - ) - samples.sort(key=lambda s: s.offset) - if not samples: +def _validate_samples(path: pathlib.Path, filters: T.Container[bytes] | None = None): + raw_samples: list[sample_parser.RawSample] = [] + + parser = sample_parser.MovieBoxParser.parse_file(path) + for track in parser.extract_tracks(): + for sample in track.extract_samples(): + if filters is None or sample.description["format"] in filters: + raw_samples.append(sample.raw_sample) + + raw_samples.sort(key=lambda s: s.offset) + if not raw_samples: return + last_sample = None - last_read = samples[0].offset - for sample in samples: - if sample.offset < last_read: + last_read = raw_samples[0].offset + for raw_sample in raw_samples: + if raw_sample.offset < last_read: LOG.warning(f"overlap found:\n{last_sample}\n{sample}") - elif sample.offset == last_read: + elif raw_sample.offset == last_read: pass else: LOG.warning(f"gap found:\n{last_sample}\n{sample}") - last_read = sample.offset + sample.size + last_read = raw_sample.offset + raw_sample.size last_sample = sample @@ -87,7 +78,7 @@ def _parse_structs(fp: T.BinaryIO): print(margin, header, data) -def _dump_box_data_at(fp: T.BinaryIO, box_type_path: T.List[bytes]): +def _dump_box_data_at(fp: T.BinaryIO, box_type_path: list[bytes]): for h, s in sparser.parse_path(fp, box_type_path): max_chunk_size = 1024 read = 0 @@ -104,30 +95,26 @@ def _dump_box_data_at(fp: T.BinaryIO, box_type_path: T.List[bytes]): break -def _parse_samples(fp: T.BinaryIO, filters: T.Optional[T.Container[bytes]] = None): - for h, s in sparser.parse_path(fp, [b"moov", b"trak"]): - offset = s.tell() - for h1, s1 in sparser.parse_path(s, [b"mdia", b"mdhd"], maxsize=h.maxsize): - box = cparser.MediaHeaderBox.parse(s1.read(h.maxsize)) - LOG.info(box) - LOG.info(sample_parser.to_datetime(box.creation_time)) - LOG.info(box.duration / box.timescale) - s.seek(offset, io.SEEK_SET) - for sample in sample_parser.parse_samples_from_trak_DEPRECATED( - s, maxsize=h.maxsize - ): +def _parse_samples(fp: T.BinaryIO, filters: T.Container[bytes] | None = None): + parser = sample_parser.MovieBoxParser.parse_stream(fp) + for track in parser.extract_tracks(): + box = track.extract_mdhd_boxdata() + LOG.info(box) + LOG.info(sample_parser.to_datetime(box["creation_time"])) + LOG.info(box["duration"] / box["timescale"]) + + for sample in track.extract_samples(): if filters is None or sample.description["format"] in filters: print(sample) -def _dump_samples(fp: T.BinaryIO, filters: T.Optional[T.Container[bytes]] = None): - for h, s in sparser.parse_path(fp, [b"moov", b"trak"]): - for sample in sample_parser.parse_samples_from_trak_DEPRECATED( - s, maxsize=h.maxsize - ): +def _dump_samples(fp: T.BinaryIO, filters: T.Container[bytes] | None = None): + parser = sample_parser.MovieBoxParser.parse_stream(fp) + for track in parser.extract_tracks(): + for sample in track.extract_samples(): if filters is None or sample.description["format"] in filters: - fp.seek(sample.offset, io.SEEK_SET) - data = fp.read(sample.size) + fp.seek(sample.raw_sample.offset, io.SEEK_SET) + data = fp.read(sample.raw_sample.size) sys.stdout.buffer.write(data) From a3932460dfa85dd2d01eed97a724e098412abac7 Mon Sep 17 00:00:00 2001 From: Tao Peng Date: Tue, 25 Mar 2025 16:20:32 -0700 Subject: [PATCH 6/7] fix --- tests/cli/blackvue_parser.py | 3 ++- tests/cli/gpmf_parser.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/cli/blackvue_parser.py b/tests/cli/blackvue_parser.py index 7433dd102..c84fc71c8 100644 --- a/tests/cli/blackvue_parser.py +++ b/tests/cli/blackvue_parser.py @@ -1,3 +1,4 @@ +from __future__ import annotations import argparse import pathlib @@ -18,7 +19,7 @@ def _convert_to_track(path: pathlib.Path): track = gpxpy.gpx.GPXTrack() points = _parse_gpx(path) if points is None: - raise ValueError("Not a valid BlackVue video") + raise RuntimeError(f"Invalid BlackVue video {path}") segment = geotag_utils.convert_points_to_gpx_segment(points) track.segments.append(segment) diff --git a/tests/cli/gpmf_parser.py b/tests/cli/gpmf_parser.py index cd46ccdb4..83c65bc13 100644 --- a/tests/cli/gpmf_parser.py +++ b/tests/cli/gpmf_parser.py @@ -1,3 +1,4 @@ +from __future__ import annotations import argparse import dataclasses import datetime @@ -66,16 +67,18 @@ def _convert_points_to_gpx_track_segment( return gpx_segment -def _parse_gpx(path: pathlib.Path) -> list[telemetry.GPSPoint]: +def _parse_gpx(path: pathlib.Path) -> list[telemetry.GPSPoint] | None: with path.open("rb") as fp: info = gpmf_parser.extract_gopro_info(fp) if info is None: - return [] + return None return info.gps or [] def _convert_gpx(gpx: gpxpy.gpx.GPX, path: pathlib.Path): points = _parse_gpx(path) + if points is None: + raise RuntimeError(f"Invalid GoPro video {path}") gpx_track = gpxpy.gpx.GPXTrack() gpx_track_segment = _convert_points_to_gpx_track_segment(points) gpx_track.segments.append(gpx_track_segment) @@ -92,9 +95,11 @@ def _convert_gpx(gpx: gpxpy.gpx.GPX, path: pathlib.Path): def _convert_geojson(path: pathlib.Path): - features = [] points = _parse_gpx(path) + if points is None: + raise RuntimeError(f"Invalid GoPro video {path}") + features = [] for idx, p in enumerate(points): geomtry = {"type": "Point", "coordinates": [p.lon, p.lat]} properties = { From 2b99543d51fa6c5176f81cfe2d1473677ca67cf2 Mon Sep 17 00:00:00 2001 From: Tao Peng Date: Tue, 25 Mar 2025 16:21:27 -0700 Subject: [PATCH 7/7] usort --- tests/cli/blackvue_parser.py | 1 + tests/cli/gpmf_parser.py | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/cli/blackvue_parser.py b/tests/cli/blackvue_parser.py index c84fc71c8..e1e1c8380 100644 --- a/tests/cli/blackvue_parser.py +++ b/tests/cli/blackvue_parser.py @@ -1,4 +1,5 @@ from __future__ import annotations + import argparse import pathlib diff --git a/tests/cli/gpmf_parser.py b/tests/cli/gpmf_parser.py index 83c65bc13..2cadeb362 100644 --- a/tests/cli/gpmf_parser.py +++ b/tests/cli/gpmf_parser.py @@ -1,4 +1,5 @@ from __future__ import annotations + import argparse import dataclasses import datetime