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 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 genuine pale band apart from the cut line and the border's own
anti-aliasing. 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: if Image is None:
return None return None
@ -450,15 +457,62 @@ def edge_wear_profile(image_bytes):
if all(v is None for v in collected.values()): if all(v is None for v in collected.values()):
return None return None
# Baseline from ALL four edges pooled, not each edge against itself. # Before pooling, catch an edge whose FINISH differs from the rest of
# Whitening only ever raises luma and lowers saturation, so unworn # the card outright — a die-cut window of clear acetate, a foil
# border sits at the low end of one and the high end of the other, # accent strip on one side only — rather than one that's simply worn.
# and quartiles across the whole card find it. Scoring an edge # 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 # against only its own length silently fails on the case that
# matters most — an edge worn evenly end to end, where the baseline # matters most — an edge worn evenly end to end, where the baseline
# becomes the wear and the damage cancels itself out. Pooling means # becomes the wear and the damage cancels itself out. Pooling means
# three clean edges anchor the fourth. # 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] pooled = [c for cols in collected.values() if cols for c in cols]
outliers = {}
if len(pooled) < 40: if len(pooled) < 40:
return None return None
lumas = sorted(c[0] for c in pooled) 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 " "— typical of a refractor or prismatic finish — for a "
"whitening measurement to mean anything") "whitening measurement to mean anything")
edges = {name: (_score_edge(cols, base_l, base_s) if cols else None) edges = {
for name, cols in collected.items()} 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 { return {
"edges": edges, "edges": edges,
"reliable": reason is None, "reliable": reason is None,
"reason": reason, "reason": reason,
"edge_notes": outliers,
"border_luma": round(base_l, 1), "border_luma": round(base_l, 1),
"border_saturation": round(base_s, 1), "border_saturation": round(base_s, 1),
} }

View file

@ -676,11 +676,29 @@ def grade_card(images, api_key=None, model=None, effort=None, zoom_details=True)
measured.get("reason", "of its finish"))) measured.get("reason", "of its finish")))
elif measured: elif measured:
rows = [] rows = []
note_rows = []
edge_notes = measured.get("edge_notes") or {}
for side in ("top", "right", "bottom", "left"): for side in ("top", "right", "bottom", "left"):
data = (measured.get("edges") or {}).get(side) data = (measured.get("edges") or {}).get(side)
if data: if data:
rows.append(" {} edge: {:.1f}% of its length".format( rows.append(" {} edge: {:.1f}% of its length".format(
side, data["percent"])) side, data["percent"]))
elif side in edge_notes:
note_rows.append(" {} edge: NOT MEASURED — {}".format(
side, edge_notes[side]))
if note_rows:
prompt_parts.append(
"\n".join(note_rows) + "\n"
"For any edge noted above as not measured due to a different "
"material, judge it from its strip image alone, on its own "
"terms as whatever that material is — do not read its "
"brightness or lack of saturation as whitening just because "
"it looks nothing like the card's other edges. Look instead "
"for actual physical damage on that material: a crack or "
"chip in a clear acetate window, foil peeling or flaking, a "
"scuff that catches light differently than the surrounding "
"area — the same kind of judgement as any other edge, just "
"without a number to anchor it.")
if rows: if rows:
prompt_parts.append( prompt_parts.append(
"MEASURED EDGE WHITENING — computed directly from the pixels, " "MEASURED EDGE WHITENING — computed directly from the pixels, "