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
|
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),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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")))
|
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, "
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue