sprite / tests /test_duplicates.py
Cnass's picture
Upload 5 files
ebd87b7 verified
Raw History Blame Contribute Delete
4.61 kB
"""
tests/test_duplicates.py
--------------------------
Tests for exact (SHA256) and visual (perceptual hash) duplicate detection.
Run with: pytest tests/test_duplicates.py -v
"""
import sys
from pathlib import Path
import numpy as np
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from duplicate_detector import (
TOTAL_HASH_BITS,
compute_dhash,
compute_sha256,
find_duplicates,
hamming_distance,
)
from image_utils import image_to_png_bytes, to_pil
def _sprite(color, size=16):
arr = np.zeros((size, size, 4), dtype=np.uint8)
arr[:, :, 3] = 255
arr[4:size - 4, 4:size - 4] = [*color, 255]
return arr
def test_identical_sprites_have_matching_sha256():
a = _sprite((200, 50, 50))
b = a.copy()
sha_a = compute_sha256(image_to_png_bytes(to_pil(a)))
sha_b = compute_sha256(image_to_png_bytes(to_pil(b)))
assert sha_a == sha_b
def test_different_sprites_have_different_sha256():
a = _sprite((200, 50, 50))
b = _sprite((50, 50, 200))
sha_a = compute_sha256(image_to_png_bytes(to_pil(a)))
sha_b = compute_sha256(image_to_png_bytes(to_pil(b)))
assert sha_a != sha_b
def test_identical_sprites_have_zero_hamming_distance():
a = _sprite((200, 50, 50))
b = a.copy()
assert hamming_distance(compute_dhash(a), compute_dhash(b)) == 0
def test_same_shape_different_color_not_flagged_as_near_duplicate():
"""
CRITICAL: two sprites with the identical silhouette but different
colors (e.g. a green plant vs. a purple plant) must NOT be flagged as
visual duplicates -- color is part of the identity of a sprite.
"""
a = _sprite((50, 180, 50))
b = _sprite((180, 50, 180))
dist = hamming_distance(compute_dhash(a), compute_dhash(b))
threshold = round(0.06 * TOTAL_HASH_BITS)
assert dist > threshold
def test_near_identical_sprite_has_small_distance():
"""A single differing pixel should produce only a small hash distance."""
a = _sprite((50, 180, 50))
b = a.copy()
b[5, 5] = [45, 175, 45, 255]
dist = hamming_distance(compute_dhash(a), compute_dhash(b))
assert dist <= 3
def test_find_duplicates_flags_exact_match():
a = _sprite((200, 50, 50))
b = a.copy()
sha_a = compute_sha256(image_to_png_bytes(to_pil(a)))
sha_b = compute_sha256(image_to_png_bytes(to_pil(b)))
dhash_a = compute_dhash(a)
dhash_b = compute_dhash(b)
existing = [{"id": "sprite_0001", "sha256": sha_a, "phash": dhash_a}]
matches = find_duplicates(sha_b, dhash_b, existing)
assert len(matches) == 1
assert matches[0].exact is True
assert matches[0].sprite_id == "sprite_0001"
assert matches[0].similarity_percent == 100.0
def test_find_duplicates_returns_empty_for_distinct_sprite():
a = _sprite((200, 50, 50))
c = _sprite((30, 200, 30))
sha_a = compute_sha256(image_to_png_bytes(to_pil(a)))
dhash_a = compute_dhash(a)
sha_c = compute_sha256(image_to_png_bytes(to_pil(c)))
dhash_c = compute_dhash(c)
existing = [{"id": "sprite_0001", "sha256": sha_a, "phash": dhash_a}]
matches = find_duplicates(sha_c, dhash_c, existing)
assert matches == []
def test_empty_sprite_does_not_crash_hashing():
empty = np.zeros((0, 0, 4), dtype=np.uint8)
h = compute_dhash(empty)
assert isinstance(h, str)
assert len(h) > 0
def test_tiny_sprite_hashing_does_not_crash():
tiny = _sprite((100, 150, 200), size=4)
h = compute_dhash(tiny)
assert isinstance(h, str)
def test_duplicate_of_accepted_sprite_flagged_across_different_sheets():
"""Duplicate detection must work even when sprites came from different sheets."""
a = _sprite((90, 90, 220))
b = a.copy()
sha_a = compute_sha256(image_to_png_bytes(to_pil(a)))
dhash_a = compute_dhash(a)
sha_b = compute_sha256(image_to_png_bytes(to_pil(b)))
dhash_b = compute_dhash(b)
existing = [{"id": "sprite_from_sheet_1", "sha256": sha_a, "phash": dhash_a}]
matches = find_duplicates(sha_b, dhash_b, existing)
assert len(matches) == 1
assert matches[0].sprite_id == "sprite_from_sheet_1"
if __name__ == "__main__":
import traceback
tests = [(name, fn) for name, fn in list(globals().items()) if name.startswith("test_") and callable(fn)]
passed, failed = 0, 0
for name, fn in tests:
try:
fn()
print(f"PASS {name}")
passed += 1
except Exception:
print(f"FAIL {name}")
traceback.print_exc()
failed += 1
print(f"\n{passed} passed, {failed} failed")
sys.exit(1 if failed else 0)