Add aspect-ratio measurement as a soft trimming signal, reported against every standard card size

This commit is contained in:
Barely Removable 2026-08-22 11:03:54 -07:00
parent b28305dd25
commit f464faca43
5 changed files with 139 additions and 2 deletions

View file

@ -66,6 +66,17 @@ MIN_SOURCE_PX = 600
CORNERS = ("top-left", "top-right", "bottom-left", "bottom-right")
EDGES = ("top-edge", "right-edge", "bottom-edge", "left-edge")
# Short-side/long-side ratios for card stock sizes actually in circulation.
# Trimming is compared against whichever of these is closest, not one fixed
# number — treating every card as one standard size would flag genuinely
# factory-cut cards (a tobacco-era T206, a wide 1930s strip card) as trimmed
# just for being a different shape than a modern card.
STANDARD_ASPECT_RATIOS = {
"modern (2.5\" x 3.5\", most post-1957 issues)": 2.5 / 3.5,
"tobacco-era (roughly 1.5\" x 2.5\", T206 and similar pre-1920s)": 1.5 / 2.5,
"wide vintage (roughly 2.0\" x 3.0\", some 1930s-50s strip/premium issues)": 2.0 / 3.0,
}
def available():
return Image is not None
@ -612,6 +623,70 @@ def centering_profile(image_bytes):
return None
def aspect_profile(image_bytes):
"""Measure the card's own width:height ratio, as a soft signal for trimming.
Unlike centering and edge whitening, this deliberately does NOT gate
itself off with a reliability check the way those do there isn't one
available. Those two can tell a structurally bad photo apart from a bad
card (a foil border, an angled shot) from pixel evidence alone. This
measurement can't: an axis-aligned bounding box can't distinguish "this
card is genuinely a non-standard shape" from "this card was photographed
slightly rotated in frame", since both inflate the box the same way. That
judgement needs the photo itself, which only the vision model has so
the number is always returned, and the prompt is the place trimming vs.
photo-angle gets decided, the same way it already decides a print line
from a crease.
Returns the deviation against EVERY standard size, not just the nearest
one. Collapsing to "closest standard" was tried first and measurably
backfired: the three standards sit only 5-11% apart, close enough that a
real few-percent trim on a modern card lands nearer the vintage standard
than its own, and reports as clean. Which standard is actually relevant
depends on the card's era — something only the vision model determines,
from the same photo, after this function has already run so the
honest fix is hand over all three deviations and let it pick the one
that matches the card it can see, the same division of labour as every
other measurement here.
Only catches UNEVEN trimming shaving more off one side than another
distorts the ratio. A trim taken symmetrically off all four sides
preserves the ratio while shrinking the whole card, and nothing here can
catch that without a size reference (a ruler, a coin) in the photo.
"""
if Image is None:
return None
try:
img = Image.open(io.BytesIO(image_bytes))
img.load()
box = _detect_card_box(img)
if not box:
return None
bw, bh = box[2] - box[0], box[3] - box[1]
if bw < 40 or bh < 40:
return None
ratio = min(bw, bh) / float(max(bw, bh))
against_standards = {
name: {
"standard_ratio": round(std_ratio, 4),
"deviation_percent": round(abs(ratio - std_ratio) / std_ratio * 100.0, 1),
}
for name, std_ratio in STANDARD_ASPECT_RATIOS.items()
}
best_name = min(against_standards, key=lambda n: against_standards[n]["deviation_percent"])
return {
"measured_ratio": round(ratio, 4),
"width_px": bw,
"height_px": bh,
"against_standards": against_standards,
"best_match": best_name,
}
except Exception:
return None
def _edge_enhanced(strip):
"""Whitening map of an edge strip, keyed on colour saturation.