"""Unit tests for cores.vision — the shared image-operations layer.""" from __future__ import annotations import base64 import hashlib import cv2 import numpy as np import pytest from cores.vision import ( bytes_to_numpy, base64_to_numpy, numpy_to_base64, numpy_to_bytes, url_to_bytes, sniff_format, BBox, crop_region, resize_with_aspect, clamp_box, boxes_iou, to_gray, to_rgb, to_bgr, guess_color_profile, dominant_colors, sha256_bytes, sha256_image, phash, dhash, ahash, whash, hamming_distance, brightness, contrast, sharpness, noise_level, quality_score, draw_boxes, ) class TestDecode: def test_bytes_to_numpy_valid(self, sample_image_bytes): img = bytes_to_numpy(sample_image_bytes) assert img.ndim == 3 assert img.shape[2] == 3 def test_bytes_to_numpy_invalid(self): with pytest.raises(ValueError): bytes_to_numpy(b"not an image") def test_base64_to_numpy_with_data_uri(self, sample_image_b64): img = base64_to_numpy("data:image/jpeg;base64," + sample_image_b64) assert img is not None def test_numpy_to_bytes_roundtrip(self, sample_image_bytes): img = bytes_to_numpy(sample_image_bytes) raw = numpy_to_bytes(img) img2 = bytes_to_numpy(raw) assert img.shape == img2.shape def test_numpy_to_base64(self, sample_image_bytes): img = bytes_to_numpy(sample_image_bytes) b64 = numpy_to_base64(img) assert isinstance(b64, str) def test_sniff_format_jpeg(self, sample_image_bytes): assert sniff_format(sample_image_bytes) == "jpeg" def test_sniff_format_png(self): png = b"\x89PNG\r\n\x1a\n" + b"\x00" * 50 assert sniff_format(png) == "png" def test_sniff_format_unknown(self): assert sniff_format(b"\x00\x01\x02\x03") is None def test_sniff_format_empty(self): assert sniff_format(b"") is None class TestGeometry: def test_bbox_to_dict(self): b = BBox(10, 20, 100, 200) assert b.to_dict() == {"x": 10, "y": 20, "w": 100, "h": 200} def test_bbox_area(self): assert BBox(0, 0, 100, 50).area == 5000 def test_crop_region_no_margin(self): img = np.zeros((300, 300, 3), dtype=np.uint8) img[100:200, 100:200] = 255 crop = crop_region(img, BBox(100, 100, 100, 100), margin=0.0) assert crop.shape == (100, 100, 3) assert (crop == 255).all() def test_crop_region_with_margin(self): img = np.zeros((300, 300, 3), dtype=np.uint8) crop = crop_region(img, BBox(100, 100, 50, 50), margin=0.2) assert crop.shape == (70, 70, 3) def test_crop_region_clamps_bounds(self): img = np.zeros((100, 100, 3), dtype=np.uint8) crop = crop_region(img, BBox(0, 0, 80, 80), margin=0.5) assert crop.shape[0] <= 100 assert crop.shape[1] <= 100 def test_resize_no_resize_needed(self): img = np.zeros((100, 200, 3), dtype=np.uint8) assert resize_with_aspect(img, max_dim=300).shape == img.shape def test_resize_landscape(self): img = np.zeros((100, 400, 3), dtype=np.uint8) r = resize_with_aspect(img, max_dim=200) assert r.shape[1] == 200 assert r.shape[0] == 50 def test_clamp_box(self): b = clamp_box(BBox(-10, -10, 100, 100), width=50, height=50) assert b.x == 0 assert b.y == 0 assert b.w == 50 assert b.h == 50 def test_boxes_iou_identical(self): b = BBox(0, 0, 100, 100) assert boxes_iou(b, b) == 1.0 def test_boxes_iou_disjoint(self): a = BBox(0, 0, 10, 10) b = BBox(100, 100, 10, 10) assert boxes_iou(a, b) == 0.0 class TestColor: def test_to_gray_from_bgr(self): img = np.zeros((10, 10, 3), dtype=np.uint8) gray = to_gray(img) assert gray.shape == (10, 10) def test_to_gray_passthrough(self): gray = np.zeros((10, 10), dtype=np.uint8) assert to_gray(gray).shape == (10, 10) def test_to_rgb_swaps_channels(self): # BGR [255, 0, 0] = blue → RGB should be [0, 0, 255] img = np.array([[[255, 0, 0]]], dtype=np.uint8) # BGR: blue rgb = to_rgb(img) assert rgb[0, 0, 0] == 0 # R assert rgb[0, 0, 1] == 0 # G assert rgb[0, 0, 2] == 255 # B def test_guess_color_profile_bgr(self): assert guess_color_profile(np.zeros((10, 10, 3), dtype=np.uint8)) == "BGR" def test_guess_color_profile_gray(self): assert guess_color_profile(np.zeros((10, 10), dtype=np.uint8)) == "grayscale" def test_dominant_colors_returns_hex(self): img = np.zeros((100, 100, 3), dtype=np.uint8) img[:] = [255, 0, 0] # all blue in BGR colors = dominant_colors(img, k=3) assert len(colors) == 3 for c in colors: assert c.startswith("#") class TestHashing: def test_sha256_bytes_stable(self): assert sha256_bytes(b"hello") == sha256_bytes(b"hello") assert sha256_bytes(b"hello") != sha256_bytes(b"world") def test_sha256_image_stable(self, sample_image_bytes): img = bytes_to_numpy(sample_image_bytes) assert sha256_image(img) == sha256_image(img) def test_phash_stable(self, sample_image_bytes): img = bytes_to_numpy(sample_image_bytes) assert phash(img) == phash(img) def test_phash_hex_length(self, sample_image_bytes): img = bytes_to_numpy(sample_image_bytes) # 8x8 = 64 bits = 16 hex chars assert len(phash(img)) == 16 def test_dhash_stable(self, sample_image_bytes): img = bytes_to_numpy(sample_image_bytes) assert dhash(img) == dhash(img) def test_ahash_stable(self, sample_image_bytes): img = bytes_to_numpy(sample_image_bytes) assert ahash(img) == ahash(img) def test_whash_stable(self, sample_image_bytes): img = bytes_to_numpy(sample_image_bytes) # whash falls back to phash if pywt not installed result = whash(img) assert isinstance(result, str) def test_hamming_distance_identical(self): assert hamming_distance("ffff", "ffff") == 0 def test_hamming_distance_different(self): assert hamming_distance("0000", "ffff") == 16 def test_hamming_distance_unequal_length(self): assert hamming_distance("ff", "ffff") == 4 def test_phash_differs_for_different_images(self, sample_image_bytes, sample_face_image_bytes): img1 = bytes_to_numpy(sample_image_bytes) img2 = bytes_to_numpy(sample_face_image_bytes) assert phash(img1) != phash(img2) class TestQuality: def test_brightness_black(self): img = np.zeros((100, 100, 3), dtype=np.uint8) assert brightness(img) < 5.0 def test_brightness_white(self): img = np.full((100, 100, 3), 255, dtype=np.uint8) assert brightness(img) > 250.0 def test_contrast_uniform(self): img = np.full((100, 100, 3), 128, dtype=np.uint8) assert contrast(img) < 1.0 def test_sharpness_uniform(self): img = np.full((100, 100, 3), 128, dtype=np.uint8) assert sharpness(img) < 1.0 def test_quality_score_in_range(self, sample_image_bytes): img = bytes_to_numpy(sample_image_bytes) q = quality_score(img) assert 0.0 <= q <= 1.0 class TestDrawing: def test_draw_boxes_does_not_modify_original(self): img = np.zeros((300, 300, 3), dtype=np.uint8) boxes = [BBox(50, 50, 100, 100)] out = draw_boxes(img, boxes) assert (img == 0).all() assert not (out == 0).all() def test_draw_boxes_accepts_dict(self): img = np.zeros((300, 300, 3), dtype=np.uint8) boxes = [{"x": 50, "y": 50, "w": 100, "h": 100}] out = draw_boxes(img, boxes) assert not np.array_equal(img, out)