from __future__ import annotations import unittest from datetime import datetime, timedelta, timezone from cold_display_guard.vision import ( Frame, Region, RegionMetrics, RuntimeVisionSettings, ZoneOccupancyDetector, load_runtime_vision_settings, point_in_polygon, ) def solid_frame(width: int, height: int, value: int) -> Frame: return Frame(width=width, height=height, rgb=bytes([value, value, value]) * width * height) def patched_frame(width: int, height: int, base: int, patch: tuple[int, int, int, int, int]) -> Frame: x1, y1, x2, y2, value = patch pixels = bytearray(bytes([base, base, base]) * width * height) for y in range(y1, y2): for x in range(x1, x2): offset = (y * width + x) * 3 pixels[offset : offset + 3] = bytes([value, value, value]) return Frame(width=width, height=height, rgb=bytes(pixels)) def multi_patched_frame(width: int, height: int, base: int, patches: list[tuple[int, int, int, int, int]]) -> Frame: pixels = bytearray(bytes([base, base, base]) * width * height) for x1, y1, x2, y2, value in patches: for y in range(y1, y2): for x in range(x1, x2): offset = (y * width + x) * 3 pixels[offset : offset + 3] = bytes([value, value, value]) return Frame(width=width, height=height, rgb=bytes(pixels)) class VisionTests(unittest.TestCase): def test_point_in_polygon(self) -> None: polygon = ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)) self.assertTrue(point_in_polygon(0.5, 0.5, polygon)) self.assertFalse(point_in_polygon(1.5, 0.5, polygon)) def test_detector_reports_occupied_after_baseline_changes(self) -> None: detector = ZoneOccupancyDetector( [Region("r1c1", ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)))], trash_region=None, settings=RuntimeVisionSettings( baseline_frames=1, sample_stride_pixels=4, occupancy_mean_delta=10, occupancy_texture_delta=10, occupancy_confirm_frames=1, empty_confirm_frames=1, ), ) now = datetime(2026, 4, 28, 10, 0, tzinfo=timezone.utc) baseline_counts, _, _ = detector.observe(solid_frame(32, 32, 30), now) changed_counts, _, _ = detector.observe(patched_frame(32, 32, 30, (0, 0, 32, 32, 90)), now) self.assertEqual(baseline_counts, {"r1c1": 0}) self.assertEqual(changed_counts, {"r1c1": 1}) def test_detector_reports_trash_motion(self) -> None: trash = Region("trash", ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0))) detector = ZoneOccupancyDetector( [], trash_region=trash, settings=RuntimeVisionSettings(sample_stride_pixels=4, trash_motion_delta=10), ) now = datetime(2026, 4, 28, 10, 0, tzinfo=timezone.utc) _, first_deposit, _ = detector.observe(solid_frame(32, 32, 30), now) _, second_deposit, _ = detector.observe(solid_frame(32, 32, 90), now) self.assertEqual(first_deposit, 0) self.assertEqual(second_deposit, 1) def test_detector_reports_sustained_trash_motion_below_single_frame_threshold(self) -> None: trash = Region("trash", ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0))) detector = ZoneOccupancyDetector( [], trash_region=trash, settings=RuntimeVisionSettings( sample_stride_pixels=4, trash_motion_delta=18, trash_sustained_motion_delta=8, trash_sustained_motion_frames=2, ), ) now = datetime(2026, 4, 28, 10, 0, tzinfo=timezone.utc) _, first_deposit, _ = detector.observe(solid_frame(32, 32, 30), now) _, second_deposit, second_diagnostics = detector.observe(solid_frame(32, 32, 39), now) _, third_deposit, third_diagnostics = detector.observe(solid_frame(32, 32, 48), now) self.assertEqual(first_deposit, 0) self.assertEqual(second_deposit, 0) self.assertEqual(second_diagnostics["trash"]["motion_streak"], 1) self.assertEqual(third_deposit, 1) self.assertEqual(third_diagnostics["trash"]["motion_streak"], 2) def test_detector_allows_quick_sequential_strong_trash_motions(self) -> None: trash = Region("trash", ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0))) detector = ZoneOccupancyDetector( [], trash_region=trash, settings=RuntimeVisionSettings(sample_stride_pixels=4, trash_motion_delta=18), ) now = datetime(2026, 4, 28, 10, 0, tzinfo=timezone.utc) _, first_deposit, _ = detector.observe(solid_frame(32, 32, 30), now) _, second_deposit, _ = detector.observe(solid_frame(32, 32, 90), now + timedelta(seconds=1)) _, third_deposit, third_diagnostics = detector.observe(solid_frame(32, 32, 30), now + timedelta(seconds=7)) self.assertEqual(first_deposit, 0) self.assertEqual(second_deposit, 1) self.assertEqual(third_deposit, 1) self.assertFalse(third_diagnostics["trash"]["in_cooldown"]) def test_detector_requires_consecutive_occupied_frames(self) -> None: detector = ZoneOccupancyDetector( [Region("1", ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)))], trash_region=None, settings=RuntimeVisionSettings( baseline_frames=1, sample_stride_pixels=4, occupancy_mean_delta=10, occupancy_texture_delta=10, occupancy_confirm_frames=2, empty_confirm_frames=2, ), ) now = datetime(2026, 4, 28, 10, 0, tzinfo=timezone.utc) detector.observe(solid_frame(32, 32, 30), now) first_counts, _, first_diagnostics = detector.observe(solid_frame(32, 32, 90), now) second_counts, _, second_diagnostics = detector.observe(solid_frame(32, 32, 90), now) first_empty_counts, _, _ = detector.observe(solid_frame(32, 32, 30), now) second_empty_counts, _, _ = detector.observe(solid_frame(32, 32, 30), now) self.assertEqual(first_counts, {"1": 0}) self.assertTrue(first_diagnostics["zones"]["1"]["raw_occupied"]) self.assertEqual(first_diagnostics["zones"]["1"]["occupied_streak"], 1) self.assertEqual(second_counts, {"1": 1}) self.assertTrue(second_diagnostics["zones"]["1"]["occupied"]) self.assertEqual(first_empty_counts, {"1": 1}) self.assertEqual(second_empty_counts, {"1": 0}) def test_runtime_vision_defaults_raise_brightness_reflection_threshold(self) -> None: settings = load_runtime_vision_settings({}) self.assertEqual(settings.sample_stride_pixels, 4) self.assertEqual(settings.occupancy_mean_delta, 55.0) self.assertEqual(settings.occupancy_confirm_frames, 2) self.assertEqual(settings.empty_confirm_frames, 2) self.assertEqual(settings.trash_motion_delta, 18.0) self.assertEqual(settings.trash_sustained_motion_delta, 8.0) self.assertEqual(settings.trash_sustained_motion_frames, 2) self.assertEqual(settings.trash_motion_cooldown_seconds, 3) def test_detector_can_seed_previous_baseline_and_occupancy(self) -> None: detector = ZoneOccupancyDetector( [Region("1", ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)))], trash_region=None, settings=RuntimeVisionSettings( baseline_frames=10, sample_stride_pixels=4, occupancy_mean_delta=55, occupancy_texture_delta=18, occupancy_confirm_frames=2, empty_confirm_frames=2, ), ) detector.seed_baseline({"1": RegionMetrics(mean_luma=30.0, texture=0.0, sample_count=1)}) detector.seed_occupancy({"1": 1}) counts, _, diagnostics = detector.observe(solid_frame(32, 32, 90), datetime(2026, 4, 28, 10, 0, tzinfo=timezone.utc)) self.assertTrue(diagnostics["baseline_ready"]) self.assertEqual(counts, {"1": 1}) self.assertTrue(diagnostics["zones"]["1"]["occupied"]) def test_detector_reports_compact_dark_object_as_occupied(self) -> None: detector = ZoneOccupancyDetector( [Region("1", ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)))], trash_region=None, settings=RuntimeVisionSettings( baseline_frames=1, sample_stride_pixels=4, occupancy_mean_delta=55, occupancy_texture_delta=100, occupancy_dark_luma_threshold=80, occupancy_dark_fraction=0.06, occupancy_confirm_frames=1, empty_confirm_frames=1, ), ) now = datetime(2026, 4, 28, 10, 0, tzinfo=timezone.utc) detector.observe(solid_frame(32, 32, 180), now) counts, _, diagnostics = detector.observe(patched_frame(32, 32, 180, (0, 0, 8, 32, 20)), now) self.assertEqual(counts, {"1": 1}) self.assertGreaterEqual(diagnostics["zones"]["1"]["dark_fraction"], 0.06) def test_detector_ignores_bright_reflection_without_dark_object(self) -> None: detector = ZoneOccupancyDetector( [Region("1", ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)))], trash_region=None, settings=RuntimeVisionSettings( baseline_frames=1, sample_stride_pixels=4, occupancy_mean_delta=55, occupancy_texture_delta=10, occupancy_dark_luma_threshold=80, occupancy_dark_fraction=0.06, occupancy_texture_dark_fraction=0.04, occupancy_confirm_frames=1, empty_confirm_frames=1, ), ) now = datetime(2026, 4, 28, 10, 0, tzinfo=timezone.utc) detector.observe(solid_frame(32, 32, 160), now) counts, _, diagnostics = detector.observe(patched_frame(32, 32, 160, (0, 0, 8, 32, 255)), now) self.assertEqual(counts, {"1": 0}) self.assertGreaterEqual(diagnostics["zones"]["1"]["texture_delta"], 10) self.assertLess(diagnostics["zones"]["1"]["dark_fraction"], 0.04) def test_detector_ignores_bright_reflection_with_small_dark_edge(self) -> None: detector = ZoneOccupancyDetector( [Region("1", ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)))], trash_region=None, settings=RuntimeVisionSettings( baseline_frames=1, sample_stride_pixels=4, occupancy_mean_delta=55, occupancy_texture_delta=18, occupancy_dark_luma_threshold=80, occupancy_dark_fraction=0.06, occupancy_texture_dark_fraction=0.04, occupancy_confirm_frames=1, empty_confirm_frames=1, ), ) now = datetime(2026, 4, 28, 10, 0, tzinfo=timezone.utc) detector.observe(solid_frame(40, 40, 180), now) counts, _, diagnostics = detector.observe( multi_patched_frame( 40, 40, 180, [ (0, 0, 12, 40, 255), (12, 0, 16, 32, 20), ], ), now, ) zone = diagnostics["zones"]["1"] self.assertEqual(counts, {"1": 0}) self.assertGreaterEqual(zone["dark_fraction"], 0.06) self.assertGreaterEqual(zone["bright_fraction"], 0.18) if __name__ == "__main__": unittest.main()