From 408be1540e37c32abf7d64bff27d9fd50f7ec05b Mon Sep 17 00:00:00 2001 From: Chouffe Date: Wed, 22 Oct 2025 13:17:14 +0200 Subject: [PATCH 1/5] feat: use green channel for edges --- src/aris/__init__.py | 6 +- src/aris/preprocessing.py | 132 ++++++++++++++---- src/aris/scripts/preprocess_sonar_video.py | 79 +++++++---- .../stabilize_and_preprocess_sonar_video.py | 70 +++++++--- 4 files changed, 212 insertions(+), 75 deletions(-) diff --git a/src/aris/__init__.py b/src/aris/__init__.py index c9efd0e..ddbe99b 100644 --- a/src/aris/__init__.py +++ b/src/aris/__init__.py @@ -67,9 +67,10 @@ # Preprocessing functions from aris.preprocessing import ( - create_dual_channel_visualization, + create_visualization, create_gaussian_kernel, preprocess_frame, + PreprocessingResult, smooth_frames_temporal, ) from aris.pyARIS.pyARIS import ARIS_File as ARISFile @@ -109,8 +110,9 @@ "get_video_duration", "save_frames_to_video", # Preprocessing - "create_dual_channel_visualization", + "create_visualization", "create_gaussian_kernel", "preprocess_frame", + "PreprocessingResult", "smooth_frames_temporal", ] diff --git a/src/aris/preprocessing.py b/src/aris/preprocessing.py index 8e47de5..dc6cb87 100644 --- a/src/aris/preprocessing.py +++ b/src/aris/preprocessing.py @@ -12,11 +12,37 @@ - stabilize_and_preprocess_sonar_video.py """ +from dataclasses import dataclass + import cv2 import numpy as np from numpy.typing import NDArray +@dataclass +class PreprocessingResult: + """ + Result of preprocessing a single sonar frame. + + This dataclass encapsulates the outputs of the preprocessing pipeline, + providing clear names for each component of the result. + + Attributes: + blurred: Gaussian-blurred input frame (used for blue channel visualization) + edges: Edge intersection mask showing high-confidence fish boundaries + (used for green channel visualization) + motion: Refined motion detection result after guided filtering and temporal + smoothing (used for red channel visualization) + history: Copy of motion result to be used as history for the next frame's + temporal smoothing + """ + + blurred: NDArray[np.uint8] + edges: NDArray[np.uint8] + motion: NDArray[np.uint8] + history: NDArray[np.uint8] + + def create_gaussian_kernel(window_size: int, sigma: float) -> NDArray[np.float32]: """ Create a 1D Gaussian kernel for temporal weighting. @@ -85,14 +111,19 @@ def preprocess_frame( frame_history: NDArray[np.uint8] | None, frame_count: int, temporal_weight: float, -) -> tuple[NDArray[np.uint8], NDArray[np.uint8], NDArray[np.uint8]]: + edge_canny_low: int = 200, + edge_canny_high: int = 255, + edge_dilation_size: int = 2, +) -> PreprocessingResult: """ Process a single frame with preprocessing pipeline. 1. Apply Gaussian blur to reduce noise in sonar imagery 2. Apply MOG2 background subtraction to isolate moving objects - 3. Apply guided filter to refine MOG2 mask using frame edge information - 4. Apply temporal smoothing to reduce frame-to-frame noise + 3. Apply bidirectional guided filtering (frame-guided MOG and MOG-guided frame) + 4. Apply edge detection to both guided outputs (Canny) + 5. Compute edge intersection for high-confidence fish boundaries + 6. Apply temporal smoothing to reduce frame-to-frame noise Args: frame: Input frame (grayscale) @@ -104,12 +135,16 @@ def preprocess_frame( frame_history: Previous processed frame for temporal smoothing (None for first frame) frame_count: Current frame number (0-indexed) temporal_weight: Weight for current frame in temporal blending (0-1) + edge_canny_low: Canny edge detection lower threshold (default: 200) + edge_canny_high: Canny edge detection upper threshold (default: 255) + edge_dilation_size: Dilation size for edge tolerance in pixels (default: 2) Returns: - tuple: (blurred_frame, preprocessed_frame, preprocessed_frame_for_next_history) - - blurred_frame: Output of step 1 (for blue channel visualization) - - preprocessed_frame: Output of full pipeline (for red channel visualization) - - preprocessed_frame_for_next_history: Same as preprocessed_frame, for history tracking + PreprocessingResult: Dataclass containing: + - blurred: Gaussian-blurred input (for blue channel) + - edges: High-confidence fish edges (for green channel) + - motion: Refined motion detection (for red channel) + - history: Motion result for next frame's temporal smoothing """ # Step 1: Apply Gaussian blur to reduce noise # This is critical for sonar imagery which has significant speckle noise @@ -121,13 +156,14 @@ def preprocess_frame( # This isolates moving objects (fish) from the static background mog_mask = bg_subtractor.apply(frame_blurred) - # Step 3: Apply guided filter - # Convert grayscale MOG mask to RGB for guided filter + # Step 3: Apply bidirectional guided filtering + # Convert grayscale inputs to RGB for guided filter mog_mask_rgb = cv2.cvtColor(mog_mask, cv2.COLOR_GRAY2RGB) + frame_blurred_rgb = cv2.cvtColor(frame_blurred, cv2.COLOR_GRAY2RGB) - # Apply guided filter: use original frame as guide to refine MOG mask + # Direction 1: Frame-guided MOG (original direction) + # Use original frame as guide to refine MOG mask # This preserves edges from the original frame while smoothing the MOG mask - # Requires opencv-contrib-python for cv2.ximgproc.guidedFilter guided_mog_rgb = cv2.ximgproc.guidedFilter( guide=frame_blurred, src=mog_mask_rgb, @@ -135,10 +171,38 @@ def preprocess_frame( eps=guided_eps, ) + # Direction 2: MOG-guided frame (reverse direction) + # Use MOG mask as guide to extract frame structure in motion regions + # This highlights frame details where motion was detected + guided_img_rgb = cv2.ximgproc.guidedFilter( + guide=mog_mask, + src=frame_blurred_rgb, + radius=guided_radius, + eps=guided_eps, + ) + # Convert back to grayscale guided_mog = cv2.cvtColor(guided_mog_rgb, cv2.COLOR_BGR2GRAY) + guided_img = cv2.cvtColor(guided_img_rgb, cv2.COLOR_BGR2GRAY) + + # Step 4: Edge detection on both guided outputs + # Detect edges in the MOG-guided frame + edge_img = cv2.Canny(guided_img, edge_canny_low, edge_canny_high) + + # Detect edges in the frame-guided MOG + edge_mog = cv2.Canny(guided_mog, edge_canny_low, edge_canny_high) + + # Step 5: Compute edge intersection + # Dilate one edge map to allow slight misalignment tolerance + kernel_size = edge_dilation_size * 2 + 1 # Convert radius to kernel size + kernel = np.ones((kernel_size, kernel_size), np.uint8) + edge_mog_dilated = cv2.dilate(edge_mog, kernel, iterations=1) + + # Intersection: only keep edges that appear in BOTH filtered versions + # This eliminates spurious edges and keeps only high-confidence fish boundaries + edge_intersection = cv2.bitwise_and(edge_img, edge_mog_dilated) - # Step 4: Apply temporal smoothing + # Step 6: Apply temporal smoothing to motion channel # Blend current frame with history to reduce temporal noise if frame_count < 2 or frame_history is None: # For first couple frames, no history available @@ -151,31 +215,43 @@ def preprocess_frame( + (1 - temporal_weight) * frame_history.astype(np.float32) ).astype(np.uint8) - return frame_blurred, processed_frame, processed_frame + return PreprocessingResult( + blurred=frame_blurred, + edges=edge_intersection, + motion=processed_frame, + history=processed_frame, + ) -def create_dual_channel_visualization( - frame_blurred: NDArray[np.uint8], frame_preprocessed: NDArray[np.uint8] +def create_visualization( + frame_blurred: NDArray[np.uint8], + edge_intersection: NDArray[np.uint8], + frame_preprocessed: NDArray[np.uint8], ) -> NDArray[np.uint8]: """ - Create RGB dual-channel visualization for sonar preprocessing. + Create RGB visualization for sonar preprocessing with edge detection. - Combines the Gaussian-blurred input and preprocessed output into a single - RGB frame for easy visual comparison. + Combines the Gaussian-blurred input, edge intersection, and preprocessed output + into a single RGB frame for comprehensive visual analysis. Channel assignment: - Blue channel: Gaussian-blurred input (shows sonar structure after noise reduction) - - Green channel: Empty (zeros/black) - - Red channel: Preprocessed output (shows detected motion) - - Visual interpretation: - - Pure blue regions: Static sonar background - - Pure red regions: Detected moving objects (e.g., fish) - - Magenta/purple regions: Motion overlapping with sonar structures - - Black regions: No signal in either channel + - Green channel: Edge intersection (high-confidence fish boundaries from bidirectional filtering) + - Red channel: Preprocessed output (shows detected motion after guided filter + temporal smoothing) + + Visual interpretation (colors are additive): + - Pure blue: Static sonar background structure + - Pure green: High-confidence object edges (fish boundaries detected by both filters) + - Pure red: Detected moving regions (motion blobs) + - Cyan (blue + green): Static edges in the scene + - Yellow (green + red): Moving objects with clear boundaries → FISH (highest confidence) + - Magenta (blue + red): Motion over background structure + - White (all channels): Bright moving object with edges over structure + - Black: No signal in any channel Args: frame_blurred: Gaussian-blurred input frame (grayscale) + edge_intersection: Edge intersection mask (grayscale, binary edges) frame_preprocessed: Preprocessed output frame (grayscale) Returns: @@ -184,7 +260,9 @@ def create_dual_channel_visualization( return np.dstack( [ frame_blurred, # Blue channel - np.zeros_like(frame_blurred), # Green channel (empty) + edge_intersection, # Green channel (fish edges) frame_preprocessed, # Red channel ] ) + + diff --git a/src/aris/scripts/preprocess_sonar_video.py b/src/aris/scripts/preprocess_sonar_video.py index 54139b7..26620ed 100644 --- a/src/aris/scripts/preprocess_sonar_video.py +++ b/src/aris/scripts/preprocess_sonar_video.py @@ -5,19 +5,23 @@ This script applies a preprocessing pipeline to sonar videos: 1. Gaussian blur to reduce noise in sonar imagery 2. MOG2 (Mixture of Gaussians) background subtraction to isolate moving objects (fish) -3. Guided filter to refine MOG2 mask using original frame edge information -4. Temporal smoothing to reduce frame-to-frame noise (0.8*current + 0.2*history) +3. Bidirectional guided filtering (frame-guided MOG and MOG-guided frame) +4. Edge detection with Canny on both guided outputs +5. Edge intersection to identify high-confidence fish boundaries +6. Temporal smoothing to reduce frame-to-frame noise (0.8*current + 0.2*history) -The output is an RGB video with dual-channel visualization: +The output is an RGB video with triple-channel visualization: - Blue channel: Gaussian-blurred sonar frames (shows input structure after noise reduction) -- Green channel: Empty (black) +- Green channel: Edge intersection (high-confidence fish boundaries) - Red channel: Fully preprocessed frames (shows detected motion) -This visualization makes it easy to compare input and output: -- Pure blue regions: Static sonar background -- Pure red regions: Detected moving objects (fish) -- Magenta/purple regions: Motion overlapping with sonar structures -- Black regions: No signal in either channel +This visualization makes it easy to identify fish: +- Pure blue: Static sonar background +- Pure green: High-confidence object edges (fish boundaries) +- Pure red: Detected moving regions +- Yellow (green + red): Moving objects with clear boundaries → FISH (highest confidence) +- Cyan (blue + green): Static edges in the scene +- Magenta (blue + red): Motion over background structure Usage: # Process a single video with default parameters @@ -118,6 +122,24 @@ def make_cli_parser() -> argparse.ArgumentParser: default=0.8, help="Weight for current frame in temporal smoothing (default: 0.8, meaning 80%% current + 20%% history)", ) + parser.add_argument( + "--edge-canny-low", + type=int, + default=200, + help="Canny edge detection lower threshold (default: 200)", + ) + parser.add_argument( + "--edge-canny-high", + type=int, + default=255, + help="Canny edge detection upper threshold (default: 255)", + ) + parser.add_argument( + "--edge-dilation-size", + type=int, + default=2, + help="Edge dilation size in pixels for tolerance (default: 2)", + ) parser.add_argument( "--max-frames", nargs="?", @@ -209,6 +231,9 @@ def main(): guided_radius = args["guided_radius"] guided_eps = args["guided_eps"] temporal_weight = args["temporal_weight"] + edge_canny_low = args["edge_canny_low"] + edge_canny_high = args["edge_canny_high"] + edge_dilation_size = args["edge_dilation_size"] max_frames = args["max_frames"] # Get video metadata @@ -230,6 +255,9 @@ def main(): logger.info( f"Temporal smoothing: weight={temporal_weight} (current={temporal_weight * 100}%, history={(1 - temporal_weight) * 100}%)" ) + logger.info( + f"Edge detection: Canny thresholds=({edge_canny_low}, {edge_canny_high}), dilation={edge_dilation_size}px" + ) cap = cv2.VideoCapture(str(filepath_video)) total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) @@ -253,26 +281,27 @@ def main(): frame_gray = frame_rgb_input[:, :, 0] # Process the frame with full pipeline - frame_blurred, frame_preprocessed, frame_for_history = ( - aris.preprocessing.preprocess_frame( - frame=frame_gray, - bg_subtractor=mog_subtractor, - gaussian_kernel=gaussian_kernel, - gaussian_sigma=gaussian_sigma, - guided_radius=guided_radius, - guided_eps=guided_eps, - frame_history=frame_history, - frame_count=frame_count, - temporal_weight=temporal_weight, - ) + result = aris.preprocessing.preprocess_frame( + frame=frame_gray, + bg_subtractor=mog_subtractor, + gaussian_kernel=gaussian_kernel, + gaussian_sigma=gaussian_sigma, + guided_radius=guided_radius, + guided_eps=guided_eps, + frame_history=frame_history, + frame_count=frame_count, + temporal_weight=temporal_weight, + edge_canny_low=edge_canny_low, + edge_canny_high=edge_canny_high, + edge_dilation_size=edge_dilation_size, ) # Update history for next frame - frame_history = frame_for_history + frame_history = result.history - # Create dual-channel RGB visualization - visualization_frame = aris.preprocessing.create_dual_channel_visualization( - frame_blurred, frame_preprocessed + # Create triple-channel RGB visualization + visualization_frame = aris.preprocessing.create_visualization( + result.blurred, result.edges, result.motion ) processed_frames.append(visualization_frame) diff --git a/src/aris/scripts/stabilize_and_preprocess_sonar_video.py b/src/aris/scripts/stabilize_and_preprocess_sonar_video.py index fe4772d..68f59d6 100644 --- a/src/aris/scripts/stabilize_and_preprocess_sonar_video.py +++ b/src/aris/scripts/stabilize_and_preprocess_sonar_video.py @@ -12,21 +12,24 @@ 3. Apply preprocessing pipeline on stabilized frame: - Gaussian blur - MOG2 background subtraction - - Guided filter + - Bidirectional guided filtering + - Edge detection and intersection - Preprocessing temporal smoothing -4. Create dual-channel visualization (blue=input, red=motion) +4. Create triple-channel visualization (blue=input, green=fish edges, red=motion) 5. Write output frame immediately (memory-efficient) Output Format: - Blue channel: Gaussian-blurred stabilized frames -- Green channel: Empty (black) +- Green channel: Edge intersection (high-confidence fish boundaries) - Red channel: Preprocessed frames showing detected motion Visual interpretation: - Pure blue: Static sonar background -- Pure red: Detected moving objects (fish) -- Magenta/purple: Motion overlapping with sonar structures -- Black: No signal +- Pure green: High-confidence object edges (fish boundaries) +- Pure red: Detected moving regions +- Yellow (green + red): Moving objects with clear boundaries → FISH (highest confidence) +- Cyan (blue + green): Static edges in the scene +- Magenta (blue + red): Motion over background structure Usage: # Standard usage with default parameters @@ -158,6 +161,24 @@ def make_cli_parser() -> argparse.ArgumentParser: default=0.8, help="Preprocessing temporal smoothing weight (default: 0.8)", ) + parser.add_argument( + "--edge-canny-low", + type=int, + default=200, + help="Canny edge detection lower threshold (default: 200)", + ) + parser.add_argument( + "--edge-canny-high", + type=int, + default=255, + help="Canny edge detection upper threshold (default: 255)", + ) + parser.add_argument( + "--edge-dilation-size", + type=int, + default=2, + help="Edge dilation size in pixels for tolerance (default: 2)", + ) # Common parameters parser.add_argument( @@ -272,6 +293,9 @@ def main(): guided_radius = args["guided_radius"] guided_eps = args["guided_eps"] temporal_weight = args["temporal_weight"] + edge_canny_low = args["edge_canny_low"] + edge_canny_high = args["edge_canny_high"] + edge_dilation_size = args["edge_dilation_size"] max_frames = args["max_frames"] # Get video metadata @@ -393,11 +417,7 @@ def main(): stabilized_gray = stabilized_frame[:, :, 0] # Apply preprocessing pipeline - ( - frame_blurred, - frame_preprocessed, - preprocessing_history, - ) = aris.preprocessing.preprocess_frame( + result = aris.preprocessing.preprocess_frame( frame=stabilized_gray, bg_subtractor=mog_subtractor, gaussian_kernel=gaussian_kernel, @@ -407,11 +427,17 @@ def main(): frame_history=preprocessing_history, frame_count=frames_written, temporal_weight=temporal_weight, + edge_canny_low=edge_canny_low, + edge_canny_high=edge_canny_high, + edge_dilation_size=edge_dilation_size, ) - # Create dual-channel visualization - output_frame = aris.preprocessing.create_dual_channel_visualization( - frame_blurred, frame_preprocessed + # Update history for next frame + preprocessing_history = result.history + + # Create triple-channel visualization + output_frame = aris.preprocessing.create_visualization( + result.blurred, result.edges, result.motion ) # Write frame immediately (memory-efficient!) @@ -449,11 +475,7 @@ def main(): # Extract blue channel and preprocess stabilized_gray = stabilized_frame[:, :, 0] - ( - frame_blurred, - frame_preprocessed, - preprocessing_history, - ) = aris.preprocessing.preprocess_frame( + result = aris.preprocessing.preprocess_frame( frame=stabilized_gray, bg_subtractor=mog_subtractor, gaussian_kernel=gaussian_kernel, @@ -463,11 +485,17 @@ def main(): frame_history=preprocessing_history, frame_count=frames_written, temporal_weight=temporal_weight, + edge_canny_low=edge_canny_low, + edge_canny_high=edge_canny_high, + edge_dilation_size=edge_dilation_size, ) + # Update history for next frame + preprocessing_history = result.history + # Create visualization and write - output_frame = aris.preprocessing.create_dual_channel_visualization( - frame_blurred, frame_preprocessed + output_frame = aris.preprocessing.create_visualization( + result.blurred, result.edges, result.motion ) video_writer.write(output_frame) frames_written += 1 From 8ba4059770a55a28a0beb442ef36f16e80e533c2 Mon Sep 17 00:00:00 2001 From: Chouffe Date: Wed, 22 Oct 2025 13:19:52 +0200 Subject: [PATCH 2/5] feat: improve default values for params --- src/aris/preprocessing.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/aris/preprocessing.py b/src/aris/preprocessing.py index dc6cb87..abb207e 100644 --- a/src/aris/preprocessing.py +++ b/src/aris/preprocessing.py @@ -104,13 +104,13 @@ def smooth_frames_temporal( def preprocess_frame( frame: NDArray[np.uint8], bg_subtractor: cv2.BackgroundSubtractorMOG2, - gaussian_kernel: int, - gaussian_sigma: float, - guided_radius: int, - guided_eps: float, frame_history: NDArray[np.uint8] | None, frame_count: int, - temporal_weight: float, + gaussian_kernel: int = 3, + gaussian_sigma: float = 1.4, + guided_radius: int = 10, + guided_eps: float = 0.01, + temporal_weight: float = 0.8, edge_canny_low: int = 200, edge_canny_high: int = 255, edge_dilation_size: int = 2, @@ -128,13 +128,13 @@ def preprocess_frame( Args: frame: Input frame (grayscale) bg_subtractor: MOG2 background subtractor instance - gaussian_kernel: Gaussian blur kernel size (must be odd) - gaussian_sigma: Gaussian blur sigma value - guided_radius: Guided filter radius - guided_eps: Guided filter epsilon (regularization) frame_history: Previous processed frame for temporal smoothing (None for first frame) frame_count: Current frame number (0-indexed) - temporal_weight: Weight for current frame in temporal blending (0-1) + gaussian_kernel: Gaussian blur kernel size (must be odd, default: 3) + gaussian_sigma: Gaussian blur sigma value (default: 1.4) + guided_radius: Guided filter radius (default: 10) + guided_eps: Guided filter epsilon/regularization (default: 0.01) + temporal_weight: Weight for current frame in temporal blending, 0-1 (default: 0.8) edge_canny_low: Canny edge detection lower threshold (default: 200) edge_canny_high: Canny edge detection upper threshold (default: 255) edge_dilation_size: Dilation size for edge tolerance in pixels (default: 2) From a5afc98a89aef0f4678a7ced67e8e35e3d630fe9 Mon Sep 17 00:00:00 2001 From: Chouffe Date: Wed, 22 Oct 2025 13:21:46 +0200 Subject: [PATCH 3/5] chore: apply ruff formatting --- src/aris/__init__.py | 4 ++-- src/aris/preprocessing.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/aris/__init__.py b/src/aris/__init__.py index ddbe99b..32260c0 100644 --- a/src/aris/__init__.py +++ b/src/aris/__init__.py @@ -67,10 +67,10 @@ # Preprocessing functions from aris.preprocessing import ( - create_visualization, + PreprocessingResult, create_gaussian_kernel, + create_visualization, preprocess_frame, - PreprocessingResult, smooth_frames_temporal, ) from aris.pyARIS.pyARIS import ARIS_File as ARISFile diff --git a/src/aris/preprocessing.py b/src/aris/preprocessing.py index abb207e..9764721 100644 --- a/src/aris/preprocessing.py +++ b/src/aris/preprocessing.py @@ -264,5 +264,3 @@ def create_visualization( frame_preprocessed, # Red channel ] ) - - From 76363b0610ff5beb28d0f32bf0ee89234e87e20c Mon Sep 17 00:00:00 2001 From: Chouffe Date: Wed, 22 Oct 2025 13:57:52 +0200 Subject: [PATCH 4/5] test: update preprocessing tests for triple-channel visualization --- tests/unit/test_preprocessing.py | 72 ++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/tests/unit/test_preprocessing.py b/tests/unit/test_preprocessing.py index c43a134..635476d 100644 --- a/tests/unit/test_preprocessing.py +++ b/tests/unit/test_preprocessing.py @@ -2,13 +2,13 @@ Unit tests for aris.preprocessing module. Tests preprocessing functions including Gaussian kernel creation, temporal smoothing, -and dual-channel visualization for sonar video processing. +and triple-channel visualization for sonar video processing. """ import numpy as np from aris.preprocessing import ( - create_dual_channel_visualization, + create_visualization, create_gaussian_kernel, smooth_frames_temporal, ) @@ -167,77 +167,95 @@ def test_white_frames_stay_white(self): assert np.array_equal(result, np.full((10, 10), 255, dtype=np.uint8)) -class TestCreateDualChannelVisualization: - """Tests for create_dual_channel_visualization() function.""" +class TestCreateVisualization: + """Tests for create_visualization() function.""" def test_returns_numpy_array(self): """Test that function returns a NumPy array.""" blurred = np.zeros((100, 100), dtype=np.uint8) + edges = np.zeros((100, 100), dtype=np.uint8) preprocessed = np.zeros((100, 100), dtype=np.uint8) - result = create_dual_channel_visualization(blurred, preprocessed) + result = create_visualization(blurred, edges, preprocessed) assert isinstance(result, np.ndarray) def test_correct_shape(self): """Test that output is RGB with shape (H, W, 3).""" blurred = np.zeros((50, 80), dtype=np.uint8) + edges = np.zeros((50, 80), dtype=np.uint8) preprocessed = np.zeros((50, 80), dtype=np.uint8) - result = create_dual_channel_visualization(blurred, preprocessed) + result = create_visualization(blurred, edges, preprocessed) assert result.shape == (50, 80, 3) def test_correct_dtype(self): """Test that output has uint8 dtype.""" blurred = np.zeros((10, 10), dtype=np.uint8) + edges = np.zeros((10, 10), dtype=np.uint8) preprocessed = np.zeros((10, 10), dtype=np.uint8) - result = create_dual_channel_visualization(blurred, preprocessed) + result = create_visualization(blurred, edges, preprocessed) assert result.dtype == np.uint8 def test_blue_channel_is_blurred(self): """Test that blue channel (index 0) contains blurred frame.""" blurred = np.full((10, 10), 100, dtype=np.uint8) + edges = np.full((10, 10), 50, dtype=np.uint8) preprocessed = np.full((10, 10), 200, dtype=np.uint8) - result = create_dual_channel_visualization(blurred, preprocessed) + result = create_visualization(blurred, edges, preprocessed) assert np.array_equal(result[:, :, 0], blurred) - def test_green_channel_is_zero(self): - """Test that green channel (index 1) is all zeros.""" + def test_green_channel_is_edges(self): + """Test that green channel (index 1) contains edge intersection.""" blurred = np.full((10, 10), 100, dtype=np.uint8) + edges = np.full((10, 10), 150, dtype=np.uint8) preprocessed = np.full((10, 10), 200, dtype=np.uint8) - result = create_dual_channel_visualization(blurred, preprocessed) - assert np.array_equal(result[:, :, 1], np.zeros((10, 10), dtype=np.uint8)) + result = create_visualization(blurred, edges, preprocessed) + assert np.array_equal(result[:, :, 1], edges) def test_red_channel_is_preprocessed(self): """Test that red channel (index 2) contains preprocessed frame.""" blurred = np.full((10, 10), 100, dtype=np.uint8) + edges = np.full((10, 10), 50, dtype=np.uint8) preprocessed = np.full((10, 10), 200, dtype=np.uint8) - result = create_dual_channel_visualization(blurred, preprocessed) + result = create_visualization(blurred, edges, preprocessed) assert np.array_equal(result[:, :, 2], preprocessed) def test_black_inputs_produce_black_output(self): """Test that black inputs produce black RGB output.""" blurred = np.zeros((10, 10), dtype=np.uint8) + edges = np.zeros((10, 10), dtype=np.uint8) preprocessed = np.zeros((10, 10), dtype=np.uint8) - result = create_dual_channel_visualization(blurred, preprocessed) + result = create_visualization(blurred, edges, preprocessed) assert np.array_equal(result, np.zeros((10, 10, 3), dtype=np.uint8)) def test_visual_color_interpretation(self): """Test color interpretation for visual validation.""" - # Pure blue: blurred=255, preprocessed=0 + # Pure blue: blurred=255, edges=0, preprocessed=0 blurred = np.full((5, 5), 255, dtype=np.uint8) + edges = np.zeros((5, 5), dtype=np.uint8) preprocessed = np.zeros((5, 5), dtype=np.uint8) - result = create_dual_channel_visualization(blurred, preprocessed) + result = create_visualization(blurred, edges, preprocessed) # Should be [255, 0, 0] = blue in BGR assert np.array_equal(result[0, 0], [255, 0, 0]) - # Pure red: blurred=0, preprocessed=255 + # Pure green: blurred=0, edges=255, preprocessed=0 blurred2 = np.zeros((5, 5), dtype=np.uint8) - preprocessed2 = np.full((5, 5), 255, dtype=np.uint8) - result2 = create_dual_channel_visualization(blurred2, preprocessed2) - # Should be [0, 0, 255] = red in BGR - assert np.array_equal(result2[0, 0], [0, 0, 255]) - - # Magenta: blurred=255, preprocessed=255 - blurred3 = np.full((5, 5), 255, dtype=np.uint8) + edges2 = np.full((5, 5), 255, dtype=np.uint8) + preprocessed2 = np.zeros((5, 5), dtype=np.uint8) + result2 = create_visualization(blurred2, edges2, preprocessed2) + # Should be [0, 255, 0] = green in BGR + assert np.array_equal(result2[0, 0], [0, 255, 0]) + + # Pure red: blurred=0, edges=0, preprocessed=255 + blurred3 = np.zeros((5, 5), dtype=np.uint8) + edges3 = np.zeros((5, 5), dtype=np.uint8) preprocessed3 = np.full((5, 5), 255, dtype=np.uint8) - result3 = create_dual_channel_visualization(blurred3, preprocessed3) - # Should be [255, 0, 255] = magenta in BGR - assert np.array_equal(result3[0, 0], [255, 0, 255]) + result3 = create_visualization(blurred3, edges3, preprocessed3) + # Should be [0, 0, 255] = red in BGR + assert np.array_equal(result3[0, 0], [0, 0, 255]) + + # Yellow (green + red): blurred=0, edges=255, preprocessed=255 + blurred4 = np.zeros((5, 5), dtype=np.uint8) + edges4 = np.full((5, 5), 255, dtype=np.uint8) + preprocessed4 = np.full((5, 5), 255, dtype=np.uint8) + result4 = create_visualization(blurred4, edges4, preprocessed4) + # Should be [0, 255, 255] = yellow in BGR + assert np.array_equal(result4[0, 0], [0, 255, 255]) From e24a29b544283123a49ef86f7d9968195ad694c2 Mon Sep 17 00:00:00 2001 From: Chouffe Date: Wed, 22 Oct 2025 13:59:37 +0200 Subject: [PATCH 5/5] chore: fix import ordering in test_preprocessing.py --- tests/unit/test_preprocessing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_preprocessing.py b/tests/unit/test_preprocessing.py index 635476d..e2da44f 100644 --- a/tests/unit/test_preprocessing.py +++ b/tests/unit/test_preprocessing.py @@ -8,8 +8,8 @@ import numpy as np from aris.preprocessing import ( - create_visualization, create_gaussian_kernel, + create_visualization, smooth_frames_temporal, )