improve card detection sensitivity and performance
This commit is contained in:
parent
a084777e64
commit
a94524a816
2 changed files with 174 additions and 75 deletions
|
|
@ -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">
|
||||||
|
|
@ -148,11 +193,13 @@ const CameraScreen: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Detection component that handles actual detection logic */}
|
{/* Detection component that handles actual detection logic */}
|
||||||
<Detection
|
<Detection
|
||||||
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 */}
|
||||||
{gameState.detectedCards.length > 0 && (
|
{gameState.detectedCards.length > 0 && (
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -96,82 +141,87 @@ 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 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;
|
|
||||||
|
|
||||||
// Search a broader area around the potential card center
|
for (let y = 0; y < height; y += step) {
|
||||||
const searchRadius = 40;
|
for (let x = 0; x < width; x += step) {
|
||||||
for (let dy = -searchRadius; dy < searchRadius; dy += step) {
|
const i = (y * width + x) * 4;
|
||||||
for (let dx = -searchRadius; dx < searchRadius; dx += step) {
|
const brightness = (imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2]) / 3;
|
||||||
const nx = x + dx;
|
|
||||||
const ny = y + dy;
|
|
||||||
|
|
||||||
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
|
if (brightness > 120 && brightness < 255) {
|
||||||
const i = (ny * width + nx) * 4;
|
const region = getCardRegionWithShapeAnalysis(imageData, width, height, x, y);
|
||||||
const r = imageData.data[i];
|
if (region && region.width > 50 && region.height > 80) {
|
||||||
const g = imageData.data[i + 1];
|
const aspectRatio = region.width / region.height;
|
||||||
const b = imageData.data[i + 2];
|
if (aspectRatio > 0.3 && aspectRatio < 1.8) {
|
||||||
const brightness = (r + g + b) / 3;
|
regions.push(region);
|
||||||
|
}
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue