improve card detection sensitivity and performance

This commit is contained in:
10x Developer 2026-05-10 15:00:26 +02:00
parent a084777e64
commit a94524a816
2 changed files with 174 additions and 75 deletions

View file

@ -42,9 +42,12 @@ const CameraScreen: React.FC = () => {
}; };
}, []); }, []);
const [showDebug, setShowDebug] = React.useState(false);
const [liveCards, setLiveCards] = React.useState<any[]>([]);
const handleCardsDetected = (cards: any[]) => { const handleCardsDetected = (cards: any[]) => {
scanTable(cards); scanTable(cards);
showResults(); setShowDebug(true);
}; };
const scanTableHandler = async () => { const scanTableHandler = async () => {
@ -86,6 +89,7 @@ const CameraScreen: React.FC = () => {
]; ];
handleCardsDetected(detectedCards); handleCardsDetected(detectedCards);
} }
setShowDebug(false);
}; };
return ( return (
@ -107,6 +111,38 @@ const CameraScreen: React.FC = () => {
/> />
<canvas ref={canvasRef} className="hidden" /> <canvas ref={canvasRef} className="hidden" />
{showDebug && (
<div style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
zIndex: 10
}}>
{(showDebug ? liveCards : gameState.detectedCards).map((card) => (
<div
key={card.id}
style={{
position: 'absolute',
left: card.x,
top: card.y,
width: card.width,
height: card.height,
border: '2px solid red',
boxSizing: 'border-box',
color: 'red',
fontSize: '10px',
fontWeight: 'bold'
}}
>
{card.suit} {card.value}
</div>
))}
</div>
)}
{/* Placeholder for radial sectors visualization */} {/* Placeholder for radial sectors visualization */}
{gameState.players.length > 0 && ( {gameState.players.length > 0 && (
<div className="radial-sectors"> <div className="radial-sectors">
@ -136,6 +172,15 @@ const CameraScreen: React.FC = () => {
<div className="camera-status"> <div className="camera-status">
Ready to scan. Tap "SCAN TABLE" when cards are arranged. Ready to scan. Tap "SCAN TABLE" when cards are arranged.
{showDebug && (
<button
onClick={() => setShowDebug(false)}
className="secondary"
style={{ marginLeft: '10px', padding: '4px 8px', fontSize: '12px' }}
>
Hide Debug
</button>
)}
</div> </div>
<div className="scan-button-container"> <div className="scan-button-container">
@ -152,6 +197,8 @@ const CameraScreen: React.FC = () => {
videoRef={videoRef} videoRef={videoRef}
canvasRef={canvasRef} canvasRef={canvasRef}
onCardsDetected={handleCardsDetected} onCardsDetected={handleCardsDetected}
live={showDebug}
onLiveCardsDetected={(cards) => setLiveCards(cards)}
/> />
{/* Assignment component that handles radial sector assignment */} {/* Assignment component that handles radial sector assignment */}

View file

@ -6,10 +6,13 @@ interface DetectionProps {
videoRef: React.RefObject<HTMLVideoElement>; videoRef: React.RefObject<HTMLVideoElement>;
canvasRef: React.RefObject<HTMLCanvasElement>; canvasRef: React.RefObject<HTMLCanvasElement>;
onCardsDetected: (cards: Card[]) => void; onCardsDetected: (cards: Card[]) => void;
live?: boolean;
onLiveCardsDetected?: (cards: Card[]) => void;
} }
const Detection: React.FC<DetectionProps> = ({ videoRef, canvasRef, onCardsDetected }) => { const Detection: React.FC<DetectionProps> = ({ videoRef, canvasRef, onCardsDetected, live, onLiveCardsDetected }) => {
const isDetectingRef = useRef(false); const isDetectingRef = useRef(false);
const requestRef = useRef<number>();
// Expose detection method for external calls // Expose detection method for external calls
const detectCards = async () => { const detectCards = async () => {
@ -29,19 +32,61 @@ const Detection: React.FC<DetectionProps> = ({ videoRef, canvasRef, onCardsDetec
ctx.drawImage(video, 0, 0, canvas.width, canvas.height); ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// Card detection using image processing techniques with enhanced card-specific logic
const detectedCards = await processImageForCards(canvas, ctx); const detectedCards = await processImageForCards(canvas, ctx);
onCardsDetected(detectedCards); onCardsDetected(detectedCards);
} catch (error) { } catch (error) {
console.error('Card detection failed:', error); console.error('Card detection failed:', error);
// Still call onCardsDetected with empty array in case of error
onCardsDetected([]); onCardsDetected([]);
} finally { } finally {
isDetectingRef.current = false; isDetectingRef.current = false;
} }
}; };
const runLiveDetection = async () => {
if (!videoRef.current || !canvasRef.current || isDetectingRef.current) return;
isDetectingRef.current = true;
try {
const video = videoRef.current;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
if (!ctx) return;
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const detectedCards = await processImageForCards(canvas, ctx);
if (onLiveCardsDetected) {
onLiveCardsDetected(detectedCards);
}
} catch (error) {
console.error('Live detection failed:', error);
} finally {
isDetectingRef.current = false;
}
};
useEffect(() => {
if (live) {
const loop = async () => {
await runLiveDetection();
requestRef.current = requestAnimationFrame(loop);
};
requestRef.current = requestAnimationFrame(loop);
return () => {
if (requestRef.current) {
cancelAnimationFrame(requestRef.current);
}
};
}
}, [live]);
// Enhanced card detection using image processing specialized for Jass cards // Enhanced card detection using image processing specialized for Jass cards
const processImageForCards = async (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D): Promise<Card[]> => { const processImageForCards = async (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D): Promise<Card[]> => {
// Card detection logic optimized for Swiss/German-style cards // Card detection logic optimized for Swiss/German-style cards
@ -98,21 +143,18 @@ const Detection: React.FC<DetectionProps> = ({ videoRef, canvasRef, onCardsDetec
// Enhanced card region detection specialized for Jass cards // Enhanced card region detection specialized for Jass cards
const findCardRegions = (imageData: ImageData, width: number, height: number): {x: number, y: number, width: number, height: number}[] => { const findCardRegions = (imageData: ImageData, width: number, height: number): {x: number, y: number, width: number, height: number}[] => {
const regions = []; const regions = [];
const step = 24; // Increased step to reduce noise and overlapping detections const step = 24;
for (let y = 0; y < height; y += step) { for (let y = 0; y < height; y += step) {
for (let x = 0; x < width; x += step) { for (let x = 0; x < width; x += step) {
const i = (y * width + x) * 4; const i = (y * width + x) * 4;
const r = imageData.data[i]; const brightness = (imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2]) / 3;
const g = imageData.data[i + 1];
const b = imageData.data[i + 2];
const brightness = (r + g + b) / 3;
if (brightness > 200 && brightness < 255) { if (brightness > 120 && brightness < 255) {
const region = getCardRegionWithShapeAnalysis(imageData, width, height, x, y, step); const region = getCardRegionWithShapeAnalysis(imageData, width, height, x, y);
if (region && region.width > 60 && region.height > 80) { if (region && region.width > 50 && region.height > 80) {
const aspectRatio = region.width / region.height; const aspectRatio = region.width / region.height;
if (aspectRatio > 0.6 && aspectRatio < 1.4) { if (aspectRatio > 0.3 && aspectRatio < 1.8) {
regions.push(region); regions.push(region);
} }
} }
@ -120,14 +162,18 @@ const Detection: React.FC<DetectionProps> = ({ videoRef, canvasRef, onCardsDetec
} }
} }
// Remove overlapping regions to avoid "dozens of nonsense cards"
const uniqueRegions = []; const uniqueRegions = [];
regions.sort((a, b) => (a.width * a.height) - (b.width * b.height)); regions.sort((a, b) => (b.width * b.height) - (a.width * a.height));
for (const region of regions) { for (const region of regions) {
const isOverlapping = uniqueRegions.some(u => const isOverlapping = uniqueRegions.some(u => {
Math.abs(u.x - region.x) < 30 && Math.abs(u.y - region.y) < 30 const overlapX = Math.max(0, Math.min(region.x + region.width, u.x + u.width) - Math.max(region.x, u.x));
); const overlapY = Math.max(0, Math.min(region.y + region.height, u.y + u.height) - Math.max(region.y, u.y));
const overlapArea = overlapX * overlapY;
const regionArea = region.width * region.height;
const uArea = u.width * u.height;
return overlapArea > Math.min(regionArea, uArea) * 0.5;
});
if (!isOverlapping) { if (!isOverlapping) {
uniqueRegions.push(region); uniqueRegions.push(region);
} }
@ -136,42 +182,46 @@ const Detection: React.FC<DetectionProps> = ({ videoRef, canvasRef, onCardsDetec
return uniqueRegions; return uniqueRegions;
}; };
// Improved card region extraction with shape analysis for more accurate detection const getCardRegionWithShapeAnalysis = (imageData: ImageData, width: number, height: number, x: number, y: number): {x: number, y: number, width: number, height: number} | null => {
const getCardRegionWithShapeAnalysis = (imageData: ImageData, width: number, height: number, x: number, y: number, step: number): {x: number, y: number, width: number, height: number} | null => {
// Look in a wider area to detect the complete card including edges and suit symbols
let minX = x, maxX = x; let minX = x, maxX = x;
let minY = y, maxY = y; let minY = y, maxY = y;
let pixelCount = 0;
// Search a broader area around the potential card center const stack = [[x, y]];
const searchRadius = 40; const visited = new Int32Array(width * height).fill(-1);
for (let dy = -searchRadius; dy < searchRadius; dy += step) { const searchLimit = 10000;
for (let dx = -searchRadius; dx < searchRadius; dx += step) {
const nx = x + dx;
const ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) { let visitedCount = 0;
const i = (ny * width + nx) * 4; while (stack.length > 0 && visitedCount < searchLimit) {
const r = imageData.data[i]; const [cx, cy] = stack.pop()!;
const g = imageData.data[i + 1]; const idx = cy * width + cx;
const b = imageData.data[i + 2]; if (visited[idx] !== -1) continue;
const brightness = (r + g + b) / 3; visited[idx] = 1;
visitedCount++;
// If area is card-like (light colors), expand the region boundaries if (cx >= 0 && cx < width && cy >= 0 && cy < height) {
if (brightness > 180 && brightness < 250) { const i = idx * 4;
minX = Math.min(minX, nx); const brightness = (imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2]) / 3;
maxX = Math.max(maxX, nx);
minY = Math.min(minY, ny); if (brightness > 120 && brightness < 250) {
maxY = Math.max(maxY, ny); pixelCount++;
} minX = Math.min(minX, cx);
maxX = Math.max(maxX, cx);
minY = Math.min(minY, cy);
maxY = Math.max(maxY, cy);
if (cx + 1 < width) stack.push([cx + 1, cy]);
if (cx - 1 >= 0) stack.push([cx - 1, cy]);
if (cy + 1 < height) stack.push([cy + 1, cy]);
if (cy - 1 >= 0) stack.push([cy, cy - 1]);
} }
} }
} }
// Return the region if we found a reasonable card area
const widthDiff = maxX - minX; const widthDiff = maxX - minX;
const heightDiff = maxY - minY; const heightDiff = maxY - minY;
if (widthDiff > 30 && heightDiff > 40) { if (pixelCount > 200 && widthDiff > 50 && heightDiff > 80) {
return { return {
x: minX, x: minX,
y: minY, y: minY,
@ -183,6 +233,8 @@ const Detection: React.FC<DetectionProps> = ({ videoRef, canvasRef, onCardsDetec
return null; return null;
}; };
// Enhanced suit detection optimized for Jass card suit symbols // Enhanced suit detection optimized for Jass card suit symbols
const detectCardSuit = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, region: {x: number, y: number, width: number, height: number}): 'Schellen' | 'Schilten' | 'Eicheln' | 'Rosen' => { const detectCardSuit = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, region: {x: number, y: number, width: number, height: number}): 'Schellen' | 'Schilten' | 'Eicheln' | 'Rosen' => {
// Extract the region of interest (focus mainly on the suit area) // Extract the region of interest (focus mainly on the suit area)