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:
parent
f464faca43
commit
552f862f4c
2 changed files with 84 additions and 8 deletions
72
cardimage.py
72
cardimage.py
|
|
@ -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.
|
||||
# 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),
|
||||
}
|
||||
|
|
|
|||
18
vision.py
18
vision.py
|
|
@ -676,11 +676,29 @@ def grade_card(images, api_key=None, model=None, effort=None, zoom_details=True)
|
|||
measured.get("reason", "of its finish")))
|
||||
elif measured:
|
||||
rows = []
|
||||
note_rows = []
|
||||
edge_notes = measured.get("edge_notes") or {}
|
||||
for side in ("top", "right", "bottom", "left"):
|
||||
data = (measured.get("edges") or {}).get(side)
|
||||
if data:
|
||||
rows.append(" {} edge: {:.1f}% of its length".format(
|
||||
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:
|
||||
prompt_parts.append(
|
||||
"MEASURED EDGE WHITENING — computed directly from the pixels, "
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue