feat: a report that shows what the near-dup gates decide about real artwork (4223)
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 3s
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Successful in 6s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 33s
Build images / build-web (push) Successful in 58s
Build images / smoke-web (push) Skipped
Build images / build-ml (push) Successful in 1m52s
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m22s
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 3s
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Successful in 6s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 33s
Build images / build-web (push) Successful in 58s
Build images / smoke-web (push) Skipped
Build images / build-ml (push) Successful in 1m52s
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m22s
The three pixel constants added with the #4223 fix were chosen without ever measuring real files — CI only has synthetic split/solid fixtures, and FC verifies nowhere else. This prints the measurements they should have been chosen from: per pair, the hash distance, the mean drift, the changed-pixel fraction, the verdict, and which gate produced it. It drives the real find_similar with the real confirm rather than restating the decision, so it cannot drift from what the importer does. Read-only: opens files, touches no database. Also splits fingerprint_diff out of fingerprints_match — same computation, now returning the numbers instead of only the boolean, so the report can show how far a pair sat from a limit rather than which side of it it fell on. Runs inside the published :dev image (PIL + imagehash already there, no local env needed) with the art folder mounted read-only — rule 147's channel, so nothing has to reach main to be tried. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
+29
-13
@@ -122,6 +122,31 @@ def fingerprint_path(path) -> Image.Image | None:
|
||||
return None
|
||||
|
||||
|
||||
def fingerprint_diff(
|
||||
a, b, *, changed_level: int = FINGERPRINT_CHANGED_LEVEL,
|
||||
) -> tuple[float, float] | None:
|
||||
"""(mean absolute difference, fraction of pixels past `changed_level`) for
|
||||
two fingerprints. None if either is missing or the comparison fails.
|
||||
|
||||
This is the MEASUREMENT behind `fingerprints_match`, split out so the
|
||||
calibration report (scripts/phash_gate_report.py) can show how far a pair
|
||||
sat from the limits instead of only which side of them it fell on. The
|
||||
constants were chosen without a real-library sample; the numbers this
|
||||
returns are what moves them.
|
||||
"""
|
||||
if a is None or b is None:
|
||||
return None
|
||||
try:
|
||||
diff = ImageChops.difference(a, b)
|
||||
hist = diff.histogram()
|
||||
total = sum(hist)
|
||||
if not total:
|
||||
return None
|
||||
return (ImageStat.Stat(diff).mean[0], sum(hist[changed_level:]) / total)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def fingerprints_match(
|
||||
a, b,
|
||||
*,
|
||||
@@ -132,20 +157,11 @@ def fingerprints_match(
|
||||
"""True when two fingerprints are the same picture: no large global
|
||||
drift AND no meaningful local region that differs. False on any
|
||||
failure."""
|
||||
if a is None or b is None:
|
||||
return False
|
||||
try:
|
||||
diff = ImageChops.difference(a, b)
|
||||
if ImageStat.Stat(diff).mean[0] > max_mean_diff:
|
||||
return False
|
||||
hist = diff.histogram()
|
||||
total = sum(hist)
|
||||
if not total:
|
||||
return False
|
||||
changed = sum(hist[changed_level:])
|
||||
return (changed / total) <= max_changed_fraction
|
||||
except Exception:
|
||||
measured = fingerprint_diff(a, b, changed_level=changed_level)
|
||||
if measured is None:
|
||||
return False
|
||||
mean, changed_fraction = measured
|
||||
return mean <= max_mean_diff and changed_fraction <= max_changed_fraction
|
||||
|
||||
|
||||
def aspect_matches(
|
||||
|
||||
Reference in New Issue
Block a user