Repository navigation
Fail the sample instead of silently producing no frames - #839
Conversation
Distance sampling warned and returned when it could not read a video's GPS, so the command exited 0 having written nothing: WARNING - GPS is too noisy ==> Processing 0 files with source gpx... ==> Validating 0 metadatas... ==> Process summary No error, no frames, no non-zero exit -- the sample directory is never created and the geotag stage then runs over zero files. The user is told the run succeeded and has nothing to upload. Reported for a GoPro MAX 2 whose embedded GPS is rejected as noise, where attaching a GPX made no difference, but nothing about it is specific to noisy GPS: a video with no GPS at all takes the same path, which is every "camera without embedded GPS, bring your own GPX" workflow. Distance sampling needs positions to decide which frames to cut, so failing to read them is a failed sample. Raise MapillaryVideoError, the same error the rest of this function already raises for an unreadable start time or a frame count mismatch, and which exits 7 rather than dumping a traceback. sample_video() already funnels sampling errors through --skip_sample_errors, so callers who want to tolerate this keep a supported way to do it and the default stops lying. Two neighbouring silent paths get the same treatment: a missing video stream, which also returned after a warning, and an empty point list, which was an assert and so disappeared under `python -O`, leaving an IndexError further down instead. Note this changes batch behaviour. Sampling a directory containing one unreadable video now aborts unless --skip_sample_errors is passed. That matches what every other sampling error in this function already does, and the alternative is continuing to hide the failure, but it is a behaviour change for callers who relied on the skip. Three integration tests covered directories containing hero8.mp4, whose 32 embedded points are all dropped by remove_noisy_points(); they were passing while it contributed no frames at all. They now pass --skip_sample_errors, which is what they always meant. A new test pins the loud behaviour, mirroring test_sample_video_without_video_time. This does not make --geotag_source reach distance sampling; that call site still hardcodes GeotagVideosFromVideo() and is a separate fix. It only stops the failure from being silent.
The raise told users the sample failed but not how to get past it, and the obvious guess is wrong: --skip_process_errors governs the later geotagging stage and does not cover sampling, so reaching for it leaves the run failing with the same message. Append the hint, matching the existing wording in process_geotag_properties.py. The message now reads: MapillaryVideoError: Unable to sample GS018205.360 by distance: GPS is too noisy. To skip these errors, specify --skip_sample_errors
|
Pushed The raise said the sample failed but not how to get past it, and the obvious guess is the wrong one: The message now reads: Applied to all three raises in this PR, wording matching 760 unit + integration tests pass, mypy clean. |
Four other raises sit inside the same try block that --skip_sample_errors guards and said nothing about it: _sample_single_video_by_interval unable to extract video start time _sample_single_video_by_distance unable to extract video start time _sample_single_video_by_distance expect N samples but extracted M _sample_single_video_by_distance expect X to be Nth sample but got M Half the errors naming the flag is worse than none naming it: a user who lands on a silent one reaches for --skip_process_errors, which governs the later geotagging stage and leaves the run failing with the same message. Rather than append the hint at seven call sites, where it can drift out of sync again, construct these errors through _sampling_error(), so a raise added later cannot forget it. MapillaryFFmpegNotFoundError keeps its plain message. It is re-raised by its own handler before the skip check, so --skip_sample_errors really does not suppress it and it must not say otherwise. There is a test for that, alongside ones covering both newly hinted start-time paths.
|
Pushed You were right that half the errors naming the flag is worse than none: a user who lands on a silent one reaches for Rather than append the hint at seven call sites, where it can drift out of sync again, they go through a small constructor: def _sampling_error(message: str) -> exceptions.MapillaryVideoError:
return exceptions.MapillaryVideoError(f"{message}. {_SKIP_HINT}")so a raise added later cannot forget it. One deliberate exception: 713 unit + 57 integration pass, mypy and ruff clean. |
Raising out of the first video that failed to sample made the result of
a directory depend on which video that was. find_videos() does not sort,
so the videos sampled before the failure depended on the order the
filesystem lists them in. The command exited 7 with their frames written
to the sample directory but never processed, because video_process never
reached the process step.
The same directory exits 0 on main, where the failure was silent.
Do what the process command does with the files it cannot process:
- Sort the videos, so they are sampled and reported in a stable order.
- Carry on past a failed video, log why it failed, and log a sample
summary that counts the failures by type, like the process summary.
- Then raise a single MapillaryVideoSampleError, still exit code 7:
Failed to sample 1 videos. To skip these errors, specify
--skip_sample_errors
- video_process processes the frames of the videos that did sample
before re-raising it, as process writes out the descriptions it did
produce before failing. video_process_and_upload then stops before
uploading, as process_and_upload does on a process error.
With --skip_sample_errors the failures are logged as warnings and the
command exits 0.
Since the aggregated error names the flag, the per-raise hint and
_sampling_error() are gone. The errors read from the video's metadata
are raised as they are, so the summary counts them by their own type,
MapillaryGPSNoiseError or MapillaryVideoGPSNotFoundError, instead of all
of them as MapillaryVideoError.
Skipping every video used to end in "Import file or directory not
found" for the sample directory, which nothing had created. Create it
once there are videos to sample, so the process step finds it empty. A
directory without videos still fails as before.
A GPS track that covers none of the video, for example a GPX from another trip, puts no frame within the track, so distance sampling selected nothing. The command exited 0 with an empty sample directory: the same silent success as the unreadable GPS fixed earlier, reached through a readable track instead. Treat it as a failed sample, so the summary counts it and the command fails unless --skip_sample_errors is specified: No video frames found within the GPS track from 100.000s to 109.000s
The flag was missing from the README. Say what happens without it when a video cannot be sampled, and that it does not cover the errors that --skip_process_errors skips.
CI type-checks with mypy on Python 3.9, which rejects the `MapillaryVideoSampleError | None` annotation added to video_process unless annotations are postponed, and so failed every build. Add `from __future__ import annotations`, as the other modules do. The annotation is on a local variable, which Python does not evaluate, so the command itself was not affected.
mapillary#839 now samples every video before failing and reports the failures together (38ded6a), and fails distance sampling that selects no frames (3649f34). Both meet this branch in sample_video.py: - 38ded6a removed _sampling_error(): errors are raised as they are, and the summary error names --skip_sample_errors. The one call this branch added, in its check for a track that misses the video, now raises a plain MapillaryVideoError, and leaves out the video path, which the summary logs. - 3649f34 added the same check at the same spot. Keep this branch's, whose message says where the track lies relative to the video. Three tests follow: 3649f34's test of the message; this branch's unit test of a missed track, which reads the log now that the error raised is the summary; and its integration test, which checks for no frames rather than no sample directory, now that the directory is created up front.
When a video fails to sample, video_process_and_upload exits 7 without
uploading the videos that did sample, as process_and_upload does with
the files it fails to process. But the last thing it logged before the
error was the process summary, with their sample images "ready", so it
read as if they were about to be uploaded:
==> Process summary
3 (406.2 kB) ready
MapillaryVideoSampleError: Failed to sample 1 videos. To skip these
errors, specify --skip_sample_errors
Log that nothing was uploaded, and that --skip_sample_errors uploads the
others, before failing.
Brings in 7cd3025, which has video_process_and_upload log that it uploaded nothing when a video fails to sample. No conflicts.
mapillary#839 reached main through its own merge commit, so this branch and main had two merge bases, mapillary#839's tip and the main merged in before it. git merges them cleanly, but GitHub reported the pull request as conflicting. Merging main again leaves it a single base. It brings in mapillary#843, which merges without conflicts, and nothing else changes.
The previous commit reads a video's start time from its telemetry and otherwise takes creation_time as the start. That fixes the reported dashcam, and Sony and Insta360, which also stamp the start. But it moves every video from a camera that stamps the end one full duration late. Those are most dashcams: Viofo, Vantrue, AZDome and others without GPS telemetry, which people geotag from a GPX, plus the Ricoh Theta X and the Labpano PanoX V2. With interval sampling and a GPX of their own track, sample videos from a Viofo A129 and an AZDome GS63H went from 119 and 57 geotagged frames to 1 and 0. Nothing in the metadata says which of the two a camera writes, so look for evidence, in this order: - a camera known to stamp the end: the Theta X and the PanoX V2 by their make and model tags, BlackVue by the boxes it writes its GPS log into, which it does whether or not it ever gets a fix - a date and time in the file name that matches only one of the two, allowing for a whole UTC offset because cameras name files in local time - otherwise the start, with a warning that prints the --video_start_time value to use if the camera stamps the end Also: - Take the median of the first 5 plausible GPS timestamps instead of the first one. The PanoX V2 can record a stale first fix seconds older than the rest, and a GPS clock that was never set (GoPro writes 2000-01-01 until its first fix) must not become the start time. The hero8 fixture moves by 0.1s because the GPS5 samples of one payload share a timestamp. - Fall back to the creation time when the telemetry fails to parse, instead of failing to sample the video. - Measure BlackVue point times from the first NMEA line, which is the start of the recording, instead of from the first valid fix. Until the receiver gets a fix the camera logs lines without positions, so every point, and with the previous commit every frame sampled by interval, was late by the time to first fix. Across 433 BlackVue sample videos 7 took more than 5s to get a fix, one 60s. - Distance sampling resolves the start time only when a sampled frame has no absolute GPS time of its own, instead of requiring a creation time up front for every video. - Ignore a malformed creation_time with a warning instead of raising. On top of mapillary#839 and mapillary#841: the start time raises a plain MapillaryVideoError, which the sample summary reports, as mapillary#839's other sampling errors do. mapillary#839's test of an unreadable start time patched probe_video_start_time(), which this removes; it now covers interval sampling through _extract_video_start_time(), and distance sampling through a track without a GPS clock, which is when distance sampling looks the start time up. mapillary#841's comment on why only --video_start_time places a GPX gave creation_time - duration as the reason, which no longer holds; the reason now is that a video without a GPS clock would get its start time from its creation time, which is stamped at the start or the end of the recording, some in local time.
Problem
Distance sampling warned and returned when it could not read a video's GPS, so the command exited 0 having written nothing:
No error, no frames, no non-zero exit. The sample directory is never created and the geotag stage then runs over zero files, so the user is told the run succeeded and has nothing to upload.
Reported for a GoPro MAX 2 whose embedded GPS is rejected as noise, but nothing about it is specific to noisy GPS — a video with no GPS at all takes the same path. Same clip, same GPX, only the sampling mode differs:
--video_sample_distance 3--video_sample_interval 2Fix
Distance sampling needs positions to decide which frames to cut, so failing to read them is a failed sample. It now raises instead of warning and returning, with the error read from the video's metadata as it is (
MapillaryGPSNoiseError,MapillaryVideoGPSNotFoundError, ...). Two neighbouring silent paths get the same treatment: a missing video stream (also returned after a warning), and an empty point list (anassert, so it disappeared underpython -O, leaving anIndexErrorfurther down).A track that covers none of the video, such as a GPX from another trip, selected no frames and exited 0 with an empty sample directory, the same silent success reached through a readable track. That is a failed sample too (
3649f34).A video that fails no longer stops the others (
38ded6a). Raising out of the first failure made the result for a directory depend on which video that was, and left the frames of the videos sampled before it unprocessed. Sampling now does whatprocessdoes with the files it cannot process:MapillaryVideoSampleError, exit code 7, naming the flag that skips it.video_processprocesses the frames of the videos that did sample before re-raising it, asprocesswrites out the descriptions it did produce before failing.video_process_and_uploadthen stops before uploading, and says so.With
--skip_sample_errorsthe failures are logged as warnings, and the command exits 0. A missing ffmpeg still fails at once, and its message does not offer the flag, which does not suppress it. The sample directory is created as soon as there are videos to sample, so skipping every video no longer ends in "Import file or directory not found".The README now documents
--skip_sample_errors, and that it covers sampling only, not the errors--skip_process_errorsskips (8423f1d).Behaviour change worth flagging (for the release notes)
A directory with a video that cannot be sampled now exits 7 unless
--skip_sample_errorsis specified, where it used to exit 0 having silently left that video out. The other videos are still sampled and processed, and their descriptions written.video_process_and_uploadthen uploads nothing, where it used to upload the other videos. That is deliberate: a run that failed should not pass for one that worked, andprocess_and_uploaddoes the same with the files it fails to process. Because the last thing logged before the error is that the sample images are ready, it says that nothing was uploaded (7cd3025):With
--skip_sample_errorsit uploads the videos that did sample and exits 0, as it used to.Three integration tests covered directories containing
hero8.mp4, whose 32 embedded points are all dropped byremove_noisy_points(). They were passing while it contributed no frames at all. They now pass--skip_sample_errors, which is what they always meant, and new tests pin the loud behaviour, and that the other videos are still processed.Scope
This does not make
--geotag_sourcereach distance sampling. That is #841, which is stacked on this PR. This one only stops failures from being silent.Independent of #831 (different file, branches off
main).Verification
Full suite passes (773), with mypy (for Python 3.9, as CI checks, and 3.13), ruff and usort clean.