Refuse centering measurement on die-cut/clear-window cards
Same failure family as the edge-whitening fix: centering_profile assumed a uniform printed border, so on an E-X/SPx-style die-cut the side midpoint samples landed on clear acetate and the border-width walk measured two different materials, producing a confident 70/30 the model then treated as authoritative. Now the four side samples must read as the same material or the measurement refuses with the reason; the prompt tells the model to judge die-cut centering as design-to-cut registration instead.
This commit is contained in:
parent
bb5d50502c
commit
fa5467e55d
3 changed files with 81 additions and 6 deletions
58
cardimage.py
58
cardimage.py
|
|
@ -619,6 +619,16 @@ def centering_profile(image_bytes):
|
||||||
Returns None when the card has no uniform border to measure against
|
Returns None when the card has no uniform border to measure against
|
||||||
(full-bleed modern cards) or the read looks implausible, so the caller
|
(full-bleed modern cards) or the read looks implausible, so the caller
|
||||||
can fall back to the model's own eye rather than trust a bad number.
|
can fall back to the model's own eye rather than trust a bad number.
|
||||||
|
|
||||||
|
Returns {"reliable": False, "reason": ...} — a refusal with an
|
||||||
|
explanation, distinct from a silent None — when the four sides read as
|
||||||
|
DIFFERENT materials from each other. A die-cut card with a clear window
|
||||||
|
(SPx, E-X Century) is the case that forced this: the side midpoints land
|
||||||
|
on acetate showing the background through it, the walk inward measures
|
||||||
|
the widths of two different materials, and the result is a confident
|
||||||
|
70/30 on a card whose ratio was never measurable — which the model then
|
||||||
|
treats as authoritative and caps the grade with. Same failure family as
|
||||||
|
the per-edge outlier check in edge_wear_profile, one measurement over.
|
||||||
"""
|
"""
|
||||||
if Image is None:
|
if Image is None:
|
||||||
return None
|
return None
|
||||||
|
|
@ -633,10 +643,51 @@ def centering_profile(image_bytes):
|
||||||
px = card.load()
|
px = card.load()
|
||||||
|
|
||||||
# The border colour, sampled just inside the cut at the midpoint of
|
# The border colour, sampled just inside the cut at the midpoint of
|
||||||
# each side — far from corners and from any design element.
|
# each side — far from corners and from any design element. A small
|
||||||
|
# patch mean per side rather than one pixel, since a single pixel of
|
||||||
|
# noise shouldn't decide whether the whole measurement runs.
|
||||||
inset = max(2, int(min(w, h) * 0.012))
|
inset = max(2, int(min(w, h) * 0.012))
|
||||||
samples = [px[inset, h // 2], px[w - 1 - inset, h // 2],
|
patch = max(2, int(min(w, h) * 0.008))
|
||||||
px[w // 2, inset], px[w // 2, h - 1 - inset]]
|
|
||||||
|
def patch_mean(cx, cy):
|
||||||
|
total = [0, 0, 0]
|
||||||
|
n = 0
|
||||||
|
for x in range(max(0, cx - patch), min(w, cx + patch + 1)):
|
||||||
|
for y in range(max(0, cy - patch), min(h, cy + patch + 1)):
|
||||||
|
p = px[x, y]
|
||||||
|
total[0] += p[0]; total[1] += p[1]; total[2] += p[2]
|
||||||
|
n += 1
|
||||||
|
return (total[0] // n, total[1] // n, total[2] // n)
|
||||||
|
|
||||||
|
side_samples = {
|
||||||
|
"left": patch_mean(inset, h // 2),
|
||||||
|
"right": patch_mean(w - 1 - inset, h // 2),
|
||||||
|
"top": patch_mean(w // 2, inset),
|
||||||
|
"bottom": patch_mean(w // 2, h - 1 - inset),
|
||||||
|
}
|
||||||
|
|
||||||
|
# All four sides must plausibly be the SAME border before a ratio of
|
||||||
|
# their widths means anything. A side whose colour sits far from the
|
||||||
|
# median of the other three is a different material — a clear die-cut
|
||||||
|
# window showing the background, a foil accent strip — and one such
|
||||||
|
# side invalidates the whole geometric premise, not just its own
|
||||||
|
# number: the walk inward on every side keys off one shared
|
||||||
|
# border_rgb. Refuse with the reason rather than return a ratio.
|
||||||
|
for side, colour in side_samples.items():
|
||||||
|
others = [v for s, v in side_samples.items() if s != side]
|
||||||
|
med = tuple(sorted(o[i] for o in others)[len(others) // 2]
|
||||||
|
for i in range(3))
|
||||||
|
if sum(abs(colour[i] - med[i]) for i in range(3)) > 150:
|
||||||
|
return {
|
||||||
|
"reliable": False,
|
||||||
|
"reason": ("the {} side's border reads as a completely "
|
||||||
|
"different colour/material from the other "
|
||||||
|
"sides — typical of a die-cut or clear-window "
|
||||||
|
"card, where a border-width ratio doesn't "
|
||||||
|
"describe centering at all").format(side),
|
||||||
|
}
|
||||||
|
|
||||||
|
samples = list(side_samples.values())
|
||||||
border_rgb = tuple(sorted(c[i] for c in samples)[len(samples) // 2]
|
border_rgb = tuple(sorted(c[i] for c in samples)[len(samples) // 2]
|
||||||
for i in range(3))
|
for i in range(3))
|
||||||
|
|
||||||
|
|
@ -668,6 +719,7 @@ def centering_profile(image_bytes):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
"reliable": True,
|
||||||
"widths_px": widths,
|
"widths_px": widths,
|
||||||
"horizontal": horizontal,
|
"horizontal": horizontal,
|
||||||
"vertical": vertical,
|
"vertical": vertical,
|
||||||
|
|
|
||||||
|
|
@ -137,8 +137,10 @@ function renderGradeBlock(g, opts = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const cm = g.centering_measurement || null;
|
const cm = g.centering_measurement || null;
|
||||||
const centeringLine = cm
|
const centeringLine = cm && cm.reliable === false
|
||||||
? `measured — left/right ${cm.horizontal_label} · top/bottom ${cm.vertical_label}` : '';
|
? `not measurable — ${cm.reason || "this card's cut"}`
|
||||||
|
: (cm && cm.horizontal_label
|
||||||
|
? `measured — left/right ${cm.horizontal_label} · top/bottom ${cm.vertical_label}` : '');
|
||||||
|
|
||||||
// Only worth surfacing when it's away from noise — most cards sit within
|
// Only worth surfacing when it's away from noise — most cards sit within
|
||||||
// a percent or two of standard and repeating that number on every card
|
// a percent or two of standard and repeating that number on every card
|
||||||
|
|
|
||||||
23
vision.py
23
vision.py
|
|
@ -264,6 +264,14 @@ leniently, 75/25 for a 10 and 90/10 from 9 downwards):
|
||||||
Note that centering is a manufacturing trait, not damage: a badly centred \
|
Note that centering is a manufacturing trait, not damage: a badly centred \
|
||||||
card can still be pristine, and PSA may grade it strongly with an OC \
|
card can still be pristine, and PSA may grade it strongly with an OC \
|
||||||
(off-centre) qualifier rather than a low number.
|
(off-centre) qualifier rather than a low number.
|
||||||
|
DIE-CUT and clear-window cards (SPx, E-X Century, jersey- or \
|
||||||
|
shape-die-cuts) have no uniform rectangular border, so border-width ratios \
|
||||||
|
mostly do not apply to them. Judge centering there as the printed design's \
|
||||||
|
registration to the die-cut shape and the card's physical edges — that is \
|
||||||
|
what PSA keys off for these issues — and do not reconstruct a border ratio \
|
||||||
|
by eye on a card that has no border. If a MEASURED CENTERING block was \
|
||||||
|
supplied anyway on such a card, distrust it outright: the measurement \
|
||||||
|
assumes a uniform printed border this card does not have.
|
||||||
- CORNERS: look for whitening, fraying, softness, or blunting at each of the \
|
- CORNERS: look for whitening, fraying, softness, or blunting at each of the \
|
||||||
four corners. Sharp corners on all four is 9-10 territory; slight whitening \
|
four corners. Sharp corners on all four is 9-10 territory; slight whitening \
|
||||||
visible under magnification but not to the eye is 8-9; obvious whitening or \
|
visible under magnification but not to the eye is 8-9; obvious whitening or \
|
||||||
|
|
@ -646,7 +654,20 @@ def grade_card(images, api_key=None, model=None, effort=None, zoom_details=True)
|
||||||
aspect = cardimage.aspect_profile(images[0][0]) if can_measure else None
|
aspect = cardimage.aspect_profile(images[0][0]) if can_measure else None
|
||||||
|
|
||||||
prompt_parts = []
|
prompt_parts = []
|
||||||
if centering:
|
if centering and centering.get("reliable") is False:
|
||||||
|
prompt_parts.append(
|
||||||
|
"CENTERING COULD NOT BE MEASURED on this card, because {}. "
|
||||||
|
"You are getting no border-width numbers, and on a card like "
|
||||||
|
"this a border-width ratio is the wrong tool anyway: judge "
|
||||||
|
"centering by eye as the printed design's alignment relative to "
|
||||||
|
"the die-cut shape and the card's physical edges — that is what "
|
||||||
|
"PSA actually keys off for die-cut issues. Do not reconstruct a "
|
||||||
|
"left/right border ratio visually on a card with no uniform "
|
||||||
|
"border; describe what you can genuinely see about the cut's "
|
||||||
|
"registration to the printed pattern, and widen the range if "
|
||||||
|
"the photo doesn't settle it.".format(
|
||||||
|
centering.get("reason", "of its cut")))
|
||||||
|
elif centering:
|
||||||
prompt_parts.append(
|
prompt_parts.append(
|
||||||
"MEASURED CENTERING — computed from the pixels by finding the "
|
"MEASURED CENTERING — computed from the pixels by finding the "
|
||||||
"border on each side of the card:\n"
|
"border on each side of the card:\n"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue