Fix false edge-whitening on die-cut/mixed-material cards (e.g. SPx clear acetate window)

The pooled baseline that scores edge whitening was global across all four
edges, so a card that's normal printed border on three sides and clear
acetate on the fourth had the three normal edges anchor a baseline that made
the clear side read as extreme whitening -- correctly detecting a real pixel
difference, just the wrong one. Now excludes an edge from both the pool and
its own scoring when its median brightness/saturation reads as a
fundamentally different material, and surfaces why in the prompt instead of
silently dropping it.
This commit is contained in:
Barely Removable 2026-08-22 11:09:44 -07:00
parent f464faca43
commit 552f862f4c
2 changed files with 84 additions and 8 deletions

View file

@ -415,6 +415,13 @@ def edge_wear_profile(image_bytes):
and which are immune to the thing that kept defeating it: telling a
genuine pale band apart from the cut line and the border's own
anti-aliasing.
An individual edge can come back None with an entry in edge_notes rather
than a score, when that edge's own material reads as fundamentally
different from the rest of the card's border (a die-cut clear window, a
foil accent strip on one side only) see the outlier detection below for
why scoring it against the other edges' baseline would be actively wrong,
not just imprecise.
"""
if Image is None:
return None
@ -450,15 +457,62 @@ def edge_wear_profile(image_bytes):
if all(v is None for v in collected.values()):
return None
# Baseline from ALL four edges pooled, not each edge against itself.
# Whitening only ever raises luma and lowers saturation, so unworn
# border sits at the low end of one and the high end of the other,
# and quartiles across the whole card find it. Scoring an edge
# Before pooling, catch an edge whose FINISH differs from the rest of
# the card outright — a die-cut window of clear acetate, a foil
# accent strip on one side only — rather than one that's simply worn.
# This is different from the whole-card foil/silver check below: that
# one catches a card that's uniformly pale everywhere, but a die-cut
# insert is normal printed border on three sides and something else
# entirely on the fourth, so the whole-card check never trips — the
# three normal edges keep the pooled baseline looking sane, which is
# exactly what then makes the fourth edge look catastrophically
# whitened. Wear doesn't produce this: even a badly frayed edge is
# still mostly the same border material with patches of paper
# showing through, so its median luma/saturation barely moves. A
# genuinely different material moves the median far more than
# ordinary wear or lighting ever does.
edge_medians = {}
for name, cols in collected.items():
if not cols:
continue
ls = sorted(c[0] for c in cols)
ss = sorted(c[1] for c in cols)
edge_medians[name] = (ls[len(ls) // 2], ss[len(ss) // 2])
outliers = {}
for name, (l_med, s_med) in edge_medians.items():
others = [v for n, v in edge_medians.items() if n != name]
if len(others) < 2:
continue
other_l = sorted(v[0] for v in others)[len(others) // 2]
other_s = sorted(v[1] for v in others)[len(others) // 2]
if (l_med - other_l) >= 45 and (other_s - s_med) >= 35:
outliers[name] = (
"this edge's finish reads as a different material from "
"the card's other edges (much brighter and less "
"saturated) — likely a clear acetate window, a die-cut "
"insert, or a foil accent on this side only, not "
"whitening. A paper-showing-through measurement doesn't "
"apply to a material that was never opaque to begin with."
)
# Baseline from the non-outlier edges pooled, not each edge against
# itself. Whitening only ever raises luma and lowers saturation, so
# unworn border sits at the low end of one and the high end of the
# other, and quartiles across the pool find it. Scoring an edge
# against only its own length silently fails on the case that
# matters most — an edge worn evenly end to end, where the baseline
# becomes the wear and the damage cancels itself out. Pooling means
# three clean edges anchor the fourth.
pooled = [c for cols in collected.values() if cols for c in cols]
# the clean edges anchor the rest — an outlier edge left in this pool
# would drag the baseline toward itself and make the genuinely normal
# edges misread in turn, so it's excluded here.
pooled = [c for name, cols in collected.items()
if cols and name not in outliers for c in cols]
if len(pooled) < 40:
# Nothing survived exclusion (or everything was already sparse) —
# fall back to the full pool rather than giving up outright.
pooled = [c for cols in collected.values() if cols for c in cols]
outliers = {}
if len(pooled) < 40:
return None
lumas = sorted(c[0] for c in pooled)
@ -485,12 +539,16 @@ def edge_wear_profile(image_bytes):
"— typical of a refractor or prismatic finish — for a "
"whitening measurement to mean anything")
edges = {name: (_score_edge(cols, base_l, base_s) if cols else None)
for name, cols in collected.items()}
edges = {
name: (None if name in outliers
else (_score_edge(cols, base_l, base_s) if cols else None))
for name, cols in collected.items()
}
return {
"edges": edges,
"reliable": reason is None,
"reason": reason,
"edge_notes": outliers,
"border_luma": round(base_l, 1),
"border_saturation": round(base_s, 1),
}