113 lines
4.2 KiB
Python
113 lines
4.2 KiB
Python
"""
|
||
tests/test_fingerprinting.py — Unit tests for src/cv/fingerprinting.py
|
||
|
||
Tests run WITHOUT requiring real video files.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import numpy as np
|
||
import pytest
|
||
|
||
from src.cv.fingerprinting import (
|
||
text_safe_crop,
|
||
extract_hs_histograms,
|
||
compare_histograms,
|
||
hist_to_bytes,
|
||
bytes_to_hist,
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Fixtures
|
||
# ---------------------------------------------------------------------------
|
||
|
||
@pytest.fixture
|
||
def solid_blue_frame() -> np.ndarray:
|
||
"""256×256 solid blue BGR frame."""
|
||
frame = np.zeros((256, 256, 3), dtype=np.uint8)
|
||
frame[:, :] = (255, 0, 0) # BGR blue
|
||
return frame
|
||
|
||
|
||
@pytest.fixture
|
||
def solid_red_frame() -> np.ndarray:
|
||
"""256×256 solid red BGR frame."""
|
||
frame = np.zeros((256, 256, 3), dtype=np.uint8)
|
||
frame[:, :] = (0, 0, 255) # BGR red
|
||
return frame
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# text_safe_crop
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestTextSafeCrop:
|
||
def test_removes_correct_rows(self, solid_blue_frame: np.ndarray) -> None:
|
||
cropped = text_safe_crop(solid_blue_frame, crop_top=0.15, crop_bottom=0.30)
|
||
h = solid_blue_frame.shape[0] # 256
|
||
expected_h = int(h * (1.0 - 0.30)) - int(h * 0.15)
|
||
assert cropped.shape[0] == expected_h
|
||
|
||
def test_zero_crop_returns_same_size(self, solid_blue_frame: np.ndarray) -> None:
|
||
cropped = text_safe_crop(solid_blue_frame, crop_top=0.0, crop_bottom=0.0)
|
||
assert cropped.shape == solid_blue_frame.shape
|
||
|
||
def test_invalid_top_raises(self, solid_blue_frame: np.ndarray) -> None:
|
||
with pytest.raises(ValueError, match="crop_top"):
|
||
text_safe_crop(solid_blue_frame, crop_top=1.0, crop_bottom=0.0)
|
||
|
||
def test_invalid_bottom_raises(self, solid_blue_frame: np.ndarray) -> None:
|
||
with pytest.raises(ValueError, match="crop_bottom"):
|
||
text_safe_crop(solid_blue_frame, crop_top=0.0, crop_bottom=-0.1)
|
||
|
||
def test_overlapping_crops_raise(self, solid_blue_frame: np.ndarray) -> None:
|
||
with pytest.raises(ValueError, match="must be < 1.0"):
|
||
text_safe_crop(solid_blue_frame, crop_top=0.6, crop_bottom=0.5)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Histograms
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestHistograms:
|
||
def test_output_shape(self, solid_blue_frame: np.ndarray) -> None:
|
||
luma, sat = extract_hs_histograms(solid_blue_frame, bins_hue=50, bins_sat=60)
|
||
assert luma.shape == (50,)
|
||
assert sat.shape == (60,)
|
||
|
||
def test_normalised(self, solid_blue_frame: np.ndarray) -> None:
|
||
import numpy as np
|
||
luma, sat = extract_hs_histograms(solid_blue_frame, bins_hue=50, bins_sat=60)
|
||
# L2-normalised → norm ≈ 1.0
|
||
assert np.linalg.norm(luma) == pytest.approx(1.0, abs=1e-5)
|
||
assert np.linalg.norm(sat) == pytest.approx(1.0, abs=1e-5)
|
||
|
||
def test_same_frame_correl_is_one(self, solid_blue_frame: np.ndarray) -> None:
|
||
import cv2
|
||
luma, _ = extract_hs_histograms(solid_blue_frame, bins_hue=50, bins_sat=60)
|
||
score = compare_histograms(luma, luma, method=cv2.HISTCMP_CORREL)
|
||
assert score == pytest.approx(1.0, abs=1e-5)
|
||
|
||
def test_different_frames_correl_lower(
|
||
self,
|
||
solid_blue_frame: np.ndarray,
|
||
solid_red_frame: np.ndarray,
|
||
) -> None:
|
||
import cv2
|
||
luma_b, _ = extract_hs_histograms(solid_blue_frame, 50, 60)
|
||
luma_r, _ = extract_hs_histograms(solid_red_frame, 50, 60)
|
||
score = compare_histograms(luma_b, luma_r, method=cv2.HISTCMP_CORREL)
|
||
assert score < 1.0
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Serialisation round-trip
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestSerialisation:
|
||
def test_round_trip(self, solid_blue_frame: np.ndarray) -> None:
|
||
luma, _ = extract_hs_histograms(solid_blue_frame, 50, 60)
|
||
restored = bytes_to_hist(hist_to_bytes(luma))
|
||
np.testing.assert_array_almost_equal(luma, restored)
|