Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """Tests for Plan 1.3 §4.5 reference admission + the shared pHash (§3.1 signal). | |
| Pure stdlib synthetic PNGs (RGB, 8-bit, non-interlaced — the format read_png parses | |
| natively). No PIL/numpy. | |
| """ | |
| from __future__ import annotations | |
| import struct | |
| import sys | |
| import tempfile | |
| import unittest | |
| import zlib | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parent.parent | |
| sys.path.insert(0, str(ROOT / "stage1_intake")) | |
| sys.path.insert(0, str(ROOT / "_shared")) | |
| from check_reference_admission import check_admission, largest_component_fraction # noqa: E402 | |
| from image_hash import hamming, phash_from_image # noqa: E402 | |
| PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n" | |
| def write_rgb_png(path: Path, w: int, h: int, pixel_fn) -> None: | |
| def chunk(tag: bytes, data: bytes) -> bytes: | |
| c = struct.pack(">I", len(data)) + tag + data | |
| return c + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF) | |
| raw = bytearray() | |
| for y in range(h): | |
| raw.append(0) | |
| for x in range(w): | |
| raw += bytes(pixel_fn(x, y, w, h)) | |
| ihdr = struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0) | |
| png = ( | |
| PNG_SIGNATURE | |
| + chunk(b"IHDR", ihdr) | |
| + chunk(b"IDAT", zlib.compress(bytes(raw), 9)) | |
| + chunk(b"IEND", b"") | |
| ) | |
| path.write_bytes(png) | |
| def _centered_block(cx0, cy0, cx1, cy1, fg=(200, 30, 30), bg=(255, 255, 255)): | |
| def fn(x, y, w, h): | |
| return fg if (cx0 <= x < cx1 and cy0 <= y < cy1) else bg | |
| return fn | |
| class ImageHashTest(unittest.TestCase): | |
| def _split_image(self, vertical: bool, size=64): | |
| px = [] | |
| for y in range(size): | |
| for x in range(size): | |
| lo = (x if vertical else y) < size // 2 | |
| v = 30 if lo else 220 | |
| px.append((v, v, v, 255)) | |
| return px | |
| def test_identical_hamming_zero(self): | |
| px = self._split_image(vertical=True) | |
| self.assertEqual(hamming(phash_from_image(64, 64, px), phash_from_image(64, 64, px)), 0) | |
| def test_brightness_shift_barely_changes_hash(self): | |
| px = self._split_image(vertical=True) | |
| bright = [(min(255, v + 20),) * 3 + (255,) for (v, _g, _b, _a) in px] | |
| bright = [(t[0], t[1], t[2], t[3]) for t in bright] | |
| self.assertLessEqual(hamming(phash_from_image(64, 64, px), phash_from_image(64, 64, bright)), 4) | |
| def test_different_structure_differs_substantially(self): | |
| a = phash_from_image(64, 64, self._split_image(vertical=True)) | |
| b = phash_from_image(64, 64, self._split_image(vertical=False)) | |
| self.assertGreaterEqual(hamming(a, b), 8) | |
| class ReferenceAdmissionTest(unittest.TestCase): | |
| def setUp(self): | |
| self.dir = Path(tempfile.mkdtemp()) | |
| def test_admits_coherent_centered_subject(self): | |
| p = self.dir / "good.png" | |
| write_rgb_png(p, 200, 200, _centered_block(50, 50, 150, 150)) | |
| v = check_admission(p, viewpoint="side") | |
| self.assertTrue(v["admitted"], v["reasons"]) | |
| self.assertGreater(v["provenance"]["largestComponentFraction"], 0.9) | |
| def test_rejects_no_isolable_subject(self): | |
| # all-background: build_foreground_mask's fallback treats every pixel as | |
| # foreground → coverage saturates → "no background to segment" rejection. | |
| p = self.dir / "blank.png" | |
| write_rgb_png(p, 200, 200, lambda x, y, w, h: (255, 255, 255)) | |
| v = check_admission(p) | |
| self.assertFalse(v["admitted"]) | |
| self.assertTrue(any("coverage" in r for r in v["reasons"]), v["reasons"]) | |
| def test_rejects_tiny_resolution(self): | |
| p = self.dir / "tiny.png" | |
| write_rgb_png(p, 40, 40, _centered_block(10, 10, 30, 30)) | |
| v = check_admission(p) | |
| self.assertFalse(v["admitted"]) | |
| self.assertTrue(any("resolution floor" in r for r in v["reasons"]), v["reasons"]) | |
| def test_rejects_fragmented_mask(self): | |
| # 16 separated 16×16 red squares on white → coverage ~0.10 (in band) but the | |
| # largest connected blob is a tiny fraction of foreground → coherence rejection. | |
| def fn(x, y, w, h): | |
| for gx in (10, 55, 100, 145): | |
| for gy in (10, 55, 100, 145): | |
| if gx <= x < gx + 16 and gy <= y < gy + 16: | |
| return (200, 30, 30) | |
| return (255, 255, 255) | |
| p = self.dir / "frag.png" | |
| write_rgb_png(p, 200, 200, fn) | |
| v = check_admission(p) | |
| self.assertFalse(v["admitted"]) | |
| self.assertTrue(any("coherence" in r for r in v["reasons"]), v["reasons"]) | |
| def test_rejects_duplicate(self): | |
| p = self.dir / "dup.png" | |
| write_rgb_png(p, 200, 200, _centered_block(50, 50, 150, 150)) | |
| first = check_admission(p, viewpoint="front") | |
| self.assertTrue(first["admitted"], first["reasons"]) | |
| again = check_admission(p, viewpoint="front", against_hashes=[first["provenance"]["pHash"]]) | |
| self.assertFalse(again["admitted"]) | |
| self.assertTrue(any("duplicate" in r for r in again["reasons"]), again["reasons"]) | |
| def test_rejects_undecodable_cleanly(self): | |
| # a truncated/bogus file must be a clean rejection, never a crash/exit-2. | |
| p = self.dir / "bogus.png" | |
| p.write_bytes(PNG_SIGNATURE + b"\x00\x01\x02garbage") | |
| v = check_admission(p) | |
| self.assertFalse(v["admitted"]) | |
| self.assertTrue(any("cannot decode" in r for r in v["reasons"]), v["reasons"]) | |
| def test_largest_component_fraction_math(self): | |
| # one solid 4×4 blob in a 6×6 grid → fraction 1.0 | |
| w = h = 6 | |
| mask = [(1 <= i % w <= 4 and 1 <= i // w <= 4) for i in range(w * h)] | |
| self.assertAlmostEqual(largest_component_fraction(mask, w, h, grid=6), 1.0, places=3) | |
| # two separated single cells → largest fraction 0.5 | |
| mask2 = [False] * (w * h) | |
| mask2[0] = True | |
| mask2[w * h - 1] = True | |
| self.assertAlmostEqual(largest_component_fraction(mask2, w, h, grid=6), 0.5, places=3) | |
| if __name__ == "__main__": | |
| unittest.main(verbosity=2) | |