From 27805a458621bca50fed6f905eaa9c649458a7a1 Mon Sep 17 00:00:00 2001 From: Chouffe Date: Wed, 15 Jul 2026 16:56:57 +0200 Subject: [PATCH] fix(preprocess): eliminate MOG2 cold-start frame-0 whiteout MOG2 has no background model on its first apply(), so frame 0 of every clip is flagged ~100% motion and the red channel whites out. Feed frame 0 to MOG2 so it still learns from it, but emit frame 1's processed output in its place (duplicated) so the whiteout never reaches the video while the frame count and index alignment stay intact. Applies to both stabilize_and_preprocess_sonar_video.py and preprocess_sonar_video.py. Refs: lumax-eco/sonar-smolt-detection#107 --- src/aris/scripts/preprocess_sonar_video.py | 8 +++++++ .../stabilize_and_preprocess_sonar_video.py | 24 +++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/aris/scripts/preprocess_sonar_video.py b/src/aris/scripts/preprocess_sonar_video.py index 26620ed..e38b548 100644 --- a/src/aris/scripts/preprocess_sonar_video.py +++ b/src/aris/scripts/preprocess_sonar_video.py @@ -311,6 +311,14 @@ def main(): cap.release() logger.info(f"Successfully processed {frame_count} frames") + # Frame 0 has no MOG2 background model on the subtractor's first apply(), so + # ~100% of pixels are flagged as motion and the red channel whites out. MOG2 + # still learned from frame 0 above; here we replace frame 0's output with + # frame 1's so the whiteout never reaches the video, keeping the frame count + # and index alignment intact. See issue #107. + if len(processed_frames) > 1: + processed_frames[0] = processed_frames[1] + # Save the processed video filepath_save.parent.mkdir(parents=True, exist_ok=True) logger.info(f"Saving preprocessed video to: {filepath_save}") diff --git a/src/aris/scripts/stabilize_and_preprocess_sonar_video.py b/src/aris/scripts/stabilize_and_preprocess_sonar_video.py index 68f59d6..905d075 100644 --- a/src/aris/scripts/stabilize_and_preprocess_sonar_video.py +++ b/src/aris/scripts/stabilize_and_preprocess_sonar_video.py @@ -360,6 +360,22 @@ def main(): frame_idx = 0 frames_written = 0 + # Frame 0 has no MOG2 background model on the subtractor's first apply(), so + # ~100% of pixels are flagged as motion and the red channel whites out. Feed + # frame 0 to MOG2 as usual (it still helps build the model), but emit frame + # 1's output in its place, duplicated, so the whiteout never reaches the video + # while the frame count and index alignment are preserved. See issue #107. + pending_first_output = [None] + + def write_output(frame): + if frames_written == 0: + pending_first_output[0] = frame # stash frame 0; frame 1 will replace it + return + if pending_first_output[0] is not None: + video_writer.write(frame) # slot 0 <- frame 1's output + pending_first_output[0] = None + video_writer.write(frame) + logger.info( "Processing frames with combined stabilization and preprocessing (memory-efficient)..." ) @@ -441,7 +457,7 @@ def main(): ) # Write frame immediately (memory-efficient!) - video_writer.write(output_frame) + write_output(output_frame) frames_written += 1 pbar.update(1) @@ -497,7 +513,7 @@ def main(): output_frame = aris.preprocessing.create_visualization( result.blurred, result.edges, result.motion ) - video_writer.write(output_frame) + write_output(output_frame) frames_written += 1 pbar.update(1) @@ -507,6 +523,10 @@ def main(): else: break + # Degenerate single-frame clip: no frame 1 to substitute, so emit frame 0. + if pending_first_output[0] is not None: + video_writer.write(pending_first_output[0]) + # Cleanup cap.release() video_writer.release()