diff --git a/src/components/Camera/CameraScreen.tsx b/src/components/Camera/CameraScreen.tsx index e441c1f..f009c77 100644 --- a/src/components/Camera/CameraScreen.tsx +++ b/src/components/Camera/CameraScreen.tsx @@ -42,9 +42,12 @@ const CameraScreen: React.FC = () => { }; }, []); + const [showDebug, setShowDebug] = React.useState(false); + const [liveCards, setLiveCards] = React.useState([]); + const handleCardsDetected = (cards: any[]) => { scanTable(cards); - showResults(); + setShowDebug(true); }; const scanTableHandler = async () => { @@ -86,6 +89,7 @@ const CameraScreen: React.FC = () => { ]; handleCardsDetected(detectedCards); } + setShowDebug(false); }; return ( @@ -107,6 +111,38 @@ const CameraScreen: React.FC = () => { /> + {showDebug && ( +
+ {(showDebug ? liveCards : gameState.detectedCards).map((card) => ( +
+ {card.suit} {card.value} +
+ ))} +
+ )} + {/* Placeholder for radial sectors visualization */} {gameState.players.length > 0 && (
@@ -136,6 +172,15 @@ const CameraScreen: React.FC = () => {
Ready to scan. Tap "SCAN TABLE" when cards are arranged. + {showDebug && ( + + )}
@@ -148,11 +193,13 @@ const CameraScreen: React.FC = () => {
{/* Detection component that handles actual detection logic */} - + setLiveCards(cards)} + /> {/* Assignment component that handles radial sector assignment */} {gameState.detectedCards.length > 0 && ( diff --git a/src/components/Detection/Detection.tsx b/src/components/Detection/Detection.tsx index 8cdcf98..7c60648 100644 --- a/src/components/Detection/Detection.tsx +++ b/src/components/Detection/Detection.tsx @@ -6,10 +6,13 @@ interface DetectionProps { videoRef: React.RefObject; canvasRef: React.RefObject; onCardsDetected: (cards: Card[]) => void; + live?: boolean; + onLiveCardsDetected?: (cards: Card[]) => void; } -const Detection: React.FC = ({ videoRef, canvasRef, onCardsDetected }) => { +const Detection: React.FC = ({ videoRef, canvasRef, onCardsDetected, live, onLiveCardsDetected }) => { const isDetectingRef = useRef(false); + const requestRef = useRef(); // Expose detection method for external calls const detectCards = async () => { @@ -29,19 +32,61 @@ const Detection: React.FC = ({ videoRef, canvasRef, onCardsDetec 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); onCardsDetected(detectedCards); } catch (error) { console.error('Card detection failed:', error); - // Still call onCardsDetected with empty array in case of error onCardsDetected([]); } finally { 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 const processImageForCards = async (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D): Promise => { // Card detection logic optimized for Swiss/German-style cards @@ -96,82 +141,87 @@ const Detection: React.FC = ({ videoRef, canvasRef, onCardsDetec // 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 regions = []; - const step = 24; // Increased step to reduce noise and overlapping detections - - for (let y = 0; y < height; y += step) { - for (let x = 0; x < width; x += step) { - const i = (y * width + x) * 4; - const r = imageData.data[i]; - const g = imageData.data[i + 1]; - const b = imageData.data[i + 2]; - const brightness = (r + g + b) / 3; - - if (brightness > 200 && brightness < 255) { - const region = getCardRegionWithShapeAnalysis(imageData, width, height, x, y, step); - if (region && region.width > 60 && region.height > 80) { - const aspectRatio = region.width / region.height; - if (aspectRatio > 0.6 && aspectRatio < 1.4) { - regions.push(region); - } - } - } - } - } - - // Remove overlapping regions to avoid "dozens of nonsense cards" - const uniqueRegions = []; - regions.sort((a, b) => (a.width * a.height) - (b.width * b.height)); - - for (const region of regions) { - const isOverlapping = uniqueRegions.some(u => - Math.abs(u.x - region.x) < 30 && Math.abs(u.y - region.y) < 30 - ); - if (!isOverlapping) { - uniqueRegions.push(region); - } - } - - 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, 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 minY = y, maxY = y; + const findCardRegions = (imageData: ImageData, width: number, height: number): {x: number, y: number, width: number, height: number}[] => { + const regions = []; + const step = 24; - // Search a broader area around the potential card center - const searchRadius = 40; - for (let dy = -searchRadius; dy < searchRadius; dy += step) { - for (let dx = -searchRadius; dx < searchRadius; dx += step) { - const nx = x + dx; - const ny = y + dy; + for (let y = 0; y < height; y += step) { + for (let x = 0; x < width; x += step) { + const i = (y * width + x) * 4; + const brightness = (imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2]) / 3; - if (nx >= 0 && nx < width && ny >= 0 && ny < height) { - const i = (ny * width + nx) * 4; - const r = imageData.data[i]; - const g = imageData.data[i + 1]; - const b = imageData.data[i + 2]; - const brightness = (r + g + b) / 3; - - // If area is card-like (light colors), expand the region boundaries - if (brightness > 180 && brightness < 250) { - minX = Math.min(minX, nx); - maxX = Math.max(maxX, nx); - minY = Math.min(minY, ny); - maxY = Math.max(maxY, ny); + if (brightness > 120 && brightness < 255) { + const region = getCardRegionWithShapeAnalysis(imageData, width, height, x, y); + if (region && region.width > 50 && region.height > 80) { + const aspectRatio = region.width / region.height; + if (aspectRatio > 0.3 && aspectRatio < 1.8) { + regions.push(region); + } } } } } - // Return the region if we found a reasonable card area + const uniqueRegions = []; + regions.sort((a, b) => (b.width * b.height) - (a.width * a.height)); + + for (const region of regions) { + const isOverlapping = uniqueRegions.some(u => { + 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) { + uniqueRegions.push(region); + } + } + + return uniqueRegions; + }; + + const getCardRegionWithShapeAnalysis = (imageData: ImageData, width: number, height: number, x: number, y: number): {x: number, y: number, width: number, height: number} | null => { + let minX = x, maxX = x; + let minY = y, maxY = y; + let pixelCount = 0; + + const stack = [[x, y]]; + const visited = new Int32Array(width * height).fill(-1); + const searchLimit = 10000; + + let visitedCount = 0; + while (stack.length > 0 && visitedCount < searchLimit) { + const [cx, cy] = stack.pop()!; + const idx = cy * width + cx; + if (visited[idx] !== -1) continue; + visited[idx] = 1; + visitedCount++; + + if (cx >= 0 && cx < width && cy >= 0 && cy < height) { + const i = idx * 4; + const brightness = (imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2]) / 3; + + if (brightness > 120 && brightness < 250) { + 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]); + } + } + } + const widthDiff = maxX - minX; const heightDiff = maxY - minY; - if (widthDiff > 30 && heightDiff > 40) { + if (pixelCount > 200 && widthDiff > 50 && heightDiff > 80) { return { x: minX, y: minY, @@ -183,6 +233,8 @@ const Detection: React.FC = ({ videoRef, canvasRef, onCardsDetec return null; }; + + // 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' => { // Extract the region of interest (focus mainly on the suit area)