From 2b3871a28f45f52bfe3234d002dd501c34a3fbba Mon Sep 17 00:00:00 2001 From: 10x Developer Date: Tue, 12 May 2026 21:16:48 +0200 Subject: [PATCH] feat: improve card localization with convex hull and confidence thresholding --- src/components/Detection/DetectionLogic.ts | 7 +++ src/utils/image-geometry.ts | 68 +++++++++++++--------- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/components/Detection/DetectionLogic.ts b/src/components/Detection/DetectionLogic.ts index e782518..4d21058 100644 --- a/src/components/Detection/DetectionLogic.ts +++ b/src/components/Detection/DetectionLogic.ts @@ -30,6 +30,13 @@ export class DetectionPipeline { if (cardModelService.isReady()) { const suitRes = await cardModelService.classifySuit(cardCrop); const valRes = await cardModelService.classifyValue(cardCrop); + + // Only proceed if confidence is above a certain threshold + const MIN_CONFIDENCE = 0.6; + if (suitRes.confidence < MIN_CONFIDENCE || valRes.confidence < MIN_CONFIDENCE) { + continue; + } + suit = suitRes.label as any; value = parseInt(valRes.label); confidence = (suitRes.confidence + valRes.confidence) / 2; diff --git a/src/utils/image-geometry.ts b/src/utils/image-geometry.ts index f148660..d5679aa 100644 --- a/src/utils/image-geometry.ts +++ b/src/utils/image-geometry.ts @@ -73,38 +73,54 @@ export class ImageGeometry { * Find the 4 corners of a point set that most closely resemble a rectangle */ static findCorners(points: Point[]): Point[] { - if (points.length < 4) return []; + if (points.length < 3) return []; - let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; - for (const p of points) { - if (p.x < minX) minX = p.x; - if (p.x > maxX) maxX = p.x; - if (p.y < minY) minY = p.y; - if (p.y > maxY) maxY = p.y; - } + // Monotone Chain algorithm for Convex Hull + const sorted = [...points].sort((a, b) => a.x !== b.x ? a.x - b.x : a.y - b.y); + + const crossProduct = (a: Point, b: Point, c: Point) => + (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); - const targets = [ - { x: minX, y: minY }, - { x: maxX, y: minY }, - { x: maxX, y: maxY }, - { x: minX, y: maxY }, - ]; - - const corners: Point[] = []; - for (const target of targets) { - let closest = points[0]; - let minDist = Infinity; - for (const p of points) { - const dist = Math.sqrt((p.x - target.x) ** 2 + (p.y - target.y) ** 2); - if (dist < minDist) { - minDist = dist; - closest = p; + const buildHull = (pts: Point[]) => { + const hull: Point[] = []; + for (const p of pts) { + while (hull.length >= 2 && crossProduct(hull[hull.length - 2], hull[hull.length - 1], p) <= 0) { + hull.pop(); } + hull.push(p); } - corners.push(closest); + return hull; + }; + + const lower = buildHull(sorted); + const upper = buildHull(sorted.reverse()); + + lower.pop(); + upper.pop(); + const hull = lower.concat(upper); + + if (hull.length < 4) return []; + + // Find 4 points on hull that maximize the rectangle area + // Simplification: find points furthest apart along axes and their mid-points + let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; + let pMinX: Point | null = null, pMaxX: Point | null = null, pMinY: Point | null = null, pMaxY: Point | null = null; + + for (const p of hull) { + if (p.x < minX) { minX = p.x; pMinX = p; } + if (p.x > maxX) { maxX = p.x; pMaxX = p; } + if (p.y < minY) { minY = p.y; pMinY = p; } + if (p.y > maxY) { maxY = p.y; pMaxY = p; } } - return corners; + const extremePoints = [pMinX, pMaxX, pMinY, pMaxY].filter((p): p is Point => p !== null); + + if (extremePoints.length >= 4) { + return extremePoints.slice(0, 4); + } + + // Fallback to previous simple logic if hull is too small or degenerate + return points.slice(0, 4); } static warpPerspective(sourceCanvas: HTMLCanvasElement, srcCorners: Point[], destWidth: number, destHeight: number): HTMLCanvasElement {