| """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): |
| |
| img = np.array([[[255, 0, 0]]], dtype=np.uint8) |
| rgb = to_rgb(img) |
| assert rgb[0, 0, 0] == 0 |
| assert rgb[0, 0, 1] == 0 |
| assert rgb[0, 0, 2] == 255 |
|
|
| 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] |
| 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) |
| |
| 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) |
| |
| 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) |
|
|