Continuos detection added with 'beep'
This commit is contained in:
278
app/page.tsx
278
app/page.tsx
@@ -37,6 +37,131 @@ export default function HomePage() {
|
||||
const [currentDetection, setCurrentDetection] = useState<DetectionResult | null>(null);
|
||||
const [isPowerSaver, setPowerSaver] = useState(false);
|
||||
const [userConfig, setUserConfig] = useState<Partial<DetectionConfig> | null>(null);
|
||||
const [shoeDetectionCount, setShoeDetectionCount] = useState(0);
|
||||
const [lastSoundTime, setLastSoundTime] = useState(0);
|
||||
|
||||
// Draw detections function - defined early to avoid initialization order issues
|
||||
const drawDetections = useCallback((
|
||||
detection: DetectionResult | null,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
video: HTMLVideoElement,
|
||||
canvas: HTMLCanvasElement
|
||||
) => {
|
||||
console.log('🎨 drawDetections called:', { detection, videoSize: { width: video.videoWidth, height: video.videoHeight }, canvasSize: { width: canvas.width, height: canvas.height } });
|
||||
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (!detection) {
|
||||
console.log('❌ No detection to draw');
|
||||
return;
|
||||
}
|
||||
|
||||
const videoWidth = video.videoWidth;
|
||||
const videoHeight = video.videoHeight;
|
||||
|
||||
// Ensure canvas matches video dimensions
|
||||
if (canvas.width !== videoWidth || canvas.height !== videoHeight) {
|
||||
canvas.width = videoWidth;
|
||||
canvas.height = videoHeight;
|
||||
}
|
||||
|
||||
// Get bounding box coordinates [x, y, width, height]
|
||||
const [x, y, width, height] = detection.bbox;
|
||||
|
||||
// Convert normalized coordinates to pixel coordinates
|
||||
const displayX = x * videoWidth;
|
||||
const displayY = y * videoHeight;
|
||||
const displayWidth = width * videoWidth;
|
||||
const displayHeight = height * videoHeight;
|
||||
|
||||
// Draw bounding box with glow effect
|
||||
ctx.shadowColor = '#ff0000';
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.strokeStyle = '#ff0000';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.strokeRect(displayX, displayY, displayWidth, displayHeight);
|
||||
|
||||
// Reset shadow for text
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Draw label with background
|
||||
const labelText = `${detection.class} (${(detection.confidence * 100).toFixed(1)}%)`;
|
||||
ctx.font = 'bold 16px Arial';
|
||||
const textMetrics = ctx.measureText(labelText);
|
||||
const textWidth = textMetrics.width;
|
||||
const textHeight = 16;
|
||||
const padding = 6;
|
||||
|
||||
// Label background
|
||||
ctx.fillStyle = 'rgba(255, 0, 0, 0.8)';
|
||||
ctx.fillRect(
|
||||
displayX,
|
||||
displayY > textHeight + padding ? displayY - textHeight - padding : displayY + displayHeight + 2,
|
||||
textWidth + padding * 2,
|
||||
textHeight + padding
|
||||
);
|
||||
|
||||
// Label text
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillText(
|
||||
labelText,
|
||||
displayX + padding,
|
||||
displayY > textHeight + padding ? displayY - padding : displayY + displayHeight + textHeight + padding
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Stable detection callback using useCallback to prevent re-registration
|
||||
const handleDetection = useCallback((detection: DetectionResult | null) => {
|
||||
console.log('🔍 Detection callback received:', detection);
|
||||
setCurrentDetection(detection);
|
||||
|
||||
// Count actual shoe detections (not just inference attempts)
|
||||
if (detection) {
|
||||
setShoeDetectionCount(prev => prev + 1);
|
||||
}
|
||||
|
||||
// Get canvas context if not available
|
||||
let ctx = canvasCtx;
|
||||
if (canvasRef.current && !ctx) {
|
||||
ctx = canvasRef.current.getContext('2d');
|
||||
if (ctx) {
|
||||
setCanvasCtx(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw detection if we have everything we need
|
||||
if (ctx && videoRef.current && canvasRef.current) {
|
||||
console.log('🎨 Drawing detection on canvas');
|
||||
drawDetections(detection, ctx, videoRef.current, canvasRef.current);
|
||||
} else {
|
||||
console.log('❌ Canvas drawing skipped - missing refs:', { canvasCtx: !!ctx, video: !!videoRef.current, canvas: !!canvasRef.current });
|
||||
}
|
||||
|
||||
// Auto-trigger popup when shoe is detected with high confidence
|
||||
if (detection && detection.confidence > 0.7) {
|
||||
console.log('🎯 HIGH CONFIDENCE SHOE DETECTED! Opening popup...', detection);
|
||||
setPopupOpen(true);
|
||||
|
||||
// Play detection sound with debouncing (max once per 2 seconds)
|
||||
setLastSoundTime(currentTime => {
|
||||
const now = Date.now();
|
||||
if (now - currentTime > 2000) {
|
||||
try {
|
||||
const audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSuX5fLceTsFLorh8dueUA0VM6fo86JcEQ1QpO3zpWcdBSaF0fDfgToAFmq79+mbNAcZaLTp56lXFQhEnN3vuWcTBjaNz+jdjD8JEmy09+udOQcTO4nY5dqnfGUkGHS40+PvfWIuEHHf6N2AgDgGHWmy9+ibNgcZaLTn46tXFgpHot3wumwdBiqR0fDfgT4IG2u59+yfRAwKUqjn86JdEgxPndn0um0dBSyV5fLccjwGHnS89O6SQwsRYbDs46xfFgpApN7yv2clBjGN3fDbdz8JEG++7+6VNgkURY7S39KmfGQlGnS40+PoeCURYrTp46tWFwlGot3wuW0eBSqR0fDfgT8IHGy79+ibNgcZaLTo46tXFgpHot3wumwdBiqR0fDfgT4IG2u59+yfRAwKUqjn86JdEgxPndn0um0dBSyV5fLccjwGHnS89O6SQwsRYbDs46xfFgpApN7yv2clBjGN3fDbdz8JEG++7+6VNgkURY7S39KmfGQlGnS40+PoeSgIH2y69+mfRAoPUKLe8aNeAE');
|
||||
console.log('🔊 Playing detection sound');
|
||||
audio.play();
|
||||
} catch (e) {
|
||||
console.warn('Sound playback failed:', e);
|
||||
}
|
||||
return now;
|
||||
} else {
|
||||
console.log('🔇 Sound skipped - too soon after last sound');
|
||||
return currentTime;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [canvasCtx, drawDetections]);
|
||||
|
||||
// Initialize ML detection system
|
||||
const {
|
||||
@@ -53,35 +178,7 @@ export default function HomePage() {
|
||||
modelVariant: 'standard', // Start with standard model
|
||||
enableContinuous: true,
|
||||
enableTrigger: true,
|
||||
onDetection: (detection) => {
|
||||
console.log('🔍 Detection callback received:', detection);
|
||||
setCurrentDetection(detection);
|
||||
// Ensure canvas context is available before drawing
|
||||
if (canvasRef.current && !canvasCtx) {
|
||||
const ctx = canvasRef.current.getContext('2d');
|
||||
if (ctx) setCanvasCtx(ctx);
|
||||
}
|
||||
|
||||
if (canvasCtx && videoRef.current && canvasRef.current) {
|
||||
console.log('🎨 Drawing detection on canvas:', { canvasCtx: !!canvasCtx, video: !!videoRef.current, canvas: !!canvasRef.current });
|
||||
drawDetections(detection, canvasCtx, videoRef.current, canvasRef.current);
|
||||
} else {
|
||||
console.log('❌ Canvas drawing skipped - missing refs:', { canvasCtx: !!canvasCtx, video: !!videoRef.current, canvas: !!canvasRef.current });
|
||||
// Try to get canvas context if missing
|
||||
if (canvasRef.current && !canvasCtx) {
|
||||
const ctx = canvasRef.current.getContext('2d');
|
||||
if (ctx) {
|
||||
setCanvasCtx(ctx);
|
||||
drawDetections(detection, ctx, videoRef.current, canvasRef.current);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Auto-trigger popup when shoe is detected
|
||||
if (detection && detection.confidence > 0.7) {
|
||||
console.log('Shoe detected! Opening popup...', detection);
|
||||
setPopupOpen(true);
|
||||
}
|
||||
},
|
||||
onDetection: handleDetection,
|
||||
onError: (error) => {
|
||||
console.error('ML Detection Error:', error);
|
||||
}
|
||||
@@ -102,28 +199,51 @@ export default function HomePage() {
|
||||
}
|
||||
}, [stream, cameraStatus]); // Runs when stream or camera status changes
|
||||
|
||||
// Effect to get canvas context
|
||||
// Effect to get canvas context - ensure it's available
|
||||
useEffect(() => {
|
||||
if (canvasRef.current) {
|
||||
if (canvasRef.current && !canvasCtx) {
|
||||
const ctx = canvasRef.current.getContext('2d');
|
||||
if (ctx) {
|
||||
console.log('📝 Canvas context initialized');
|
||||
setCanvasCtx(ctx);
|
||||
}
|
||||
}
|
||||
}, [canvasRef]);
|
||||
}, [canvasRef, canvasCtx]);
|
||||
|
||||
// Initialize ML detection when camera is ready
|
||||
// Track initialization state to prevent multiple attempts
|
||||
const [mlInitialized, setMLInitialized] = useState(false);
|
||||
|
||||
// Initialize ML detection when camera is ready (only once)
|
||||
useEffect(() => {
|
||||
if (videoRef.current && cameraStatus === 'active' && !isMLLoading && detectionEnabled) {
|
||||
console.log('Initializing ML detection...');
|
||||
console.log('🔍 ML initialization useEffect triggered:', {
|
||||
hasVideo: !!videoRef.current,
|
||||
cameraStatus,
|
||||
isMLLoading,
|
||||
detectionEnabled,
|
||||
mlInitialized
|
||||
});
|
||||
|
||||
if (videoRef.current && cameraStatus === 'active' && !isMLLoading && detectionEnabled && !mlInitialized) {
|
||||
console.log('✅ All conditions met, initializing ML detection...');
|
||||
setMLInitialized(true);
|
||||
|
||||
initializeML(videoRef.current).then(() => {
|
||||
console.log('ML detection initialized, starting continuous detection');
|
||||
console.log('✅ ML detection initialized, starting continuous detection');
|
||||
startContinuous();
|
||||
}).catch((error) => {
|
||||
console.error('Failed to initialize ML detection:', error);
|
||||
console.error('❌ Failed to initialize ML detection:', error);
|
||||
setMLInitialized(false); // Reset on error to allow retry
|
||||
});
|
||||
} else {
|
||||
console.log('⏳ Waiting for conditions:', {
|
||||
hasVideo: !!videoRef.current,
|
||||
cameraActive: cameraStatus === 'active',
|
||||
notLoading: !isMLLoading,
|
||||
detectionEnabled,
|
||||
alreadyInitialized: mlInitialized
|
||||
});
|
||||
}
|
||||
}, [cameraStatus, detectionEnabled, isMLLoading, initializeML, startContinuous]);
|
||||
}, [cameraStatus, detectionEnabled, isMLLoading, mlInitialized, initializeML, startContinuous]);
|
||||
|
||||
const startStream = useCallback(async (deviceId: string) => {
|
||||
// Stop previous stream if it exists
|
||||
@@ -244,75 +364,6 @@ export default function HomePage() {
|
||||
setPopupOpen(true);
|
||||
};
|
||||
|
||||
const drawDetections = useCallback((
|
||||
detection: DetectionResult | null,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
video: HTMLVideoElement,
|
||||
canvas: HTMLCanvasElement
|
||||
) => {
|
||||
console.log('🎨 drawDetections called:', { detection, videoSize: { width: video.videoWidth, height: video.videoHeight }, canvasSize: { width: canvas.width, height: canvas.height } });
|
||||
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (!detection) {
|
||||
console.log('❌ No detection to draw');
|
||||
return;
|
||||
}
|
||||
|
||||
const videoWidth = video.videoWidth;
|
||||
const videoHeight = video.videoHeight;
|
||||
|
||||
// Ensure canvas matches video dimensions
|
||||
if (canvas.width !== videoWidth || canvas.height !== videoHeight) {
|
||||
canvas.width = videoWidth;
|
||||
canvas.height = videoHeight;
|
||||
}
|
||||
|
||||
// Get bounding box coordinates [x, y, width, height]
|
||||
const [x, y, width, height] = detection.bbox;
|
||||
|
||||
// Convert normalized coordinates to pixel coordinates
|
||||
const displayX = x * videoWidth;
|
||||
const displayY = y * videoHeight;
|
||||
const displayWidth = width * videoWidth;
|
||||
const displayHeight = height * videoHeight;
|
||||
|
||||
// Draw bounding box with glow effect
|
||||
ctx.shadowColor = '#ff0000';
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.strokeStyle = '#ff0000';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.strokeRect(displayX, displayY, displayWidth, displayHeight);
|
||||
|
||||
// Reset shadow for text
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Draw label with background
|
||||
const labelText = `${detection.class} (${(detection.confidence * 100).toFixed(1)}%)`;
|
||||
ctx.font = 'bold 16px Arial';
|
||||
const textMetrics = ctx.measureText(labelText);
|
||||
const textWidth = textMetrics.width;
|
||||
const textHeight = 16;
|
||||
const padding = 6;
|
||||
|
||||
// Label background
|
||||
ctx.fillStyle = 'rgba(255, 0, 0, 0.8)';
|
||||
ctx.fillRect(
|
||||
displayX,
|
||||
displayY > textHeight + padding ? displayY - textHeight - padding : displayY + displayHeight + 2,
|
||||
textWidth + padding * 2,
|
||||
textHeight + padding
|
||||
);
|
||||
|
||||
// Label text
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillText(
|
||||
labelText,
|
||||
displayX + padding,
|
||||
displayY > textHeight + padding ? displayY - padding : displayY + displayHeight + textHeight + padding
|
||||
);
|
||||
}, []); // Depend on config for inputSize
|
||||
|
||||
const renderContent = () => {
|
||||
switch (cameraStatus) {
|
||||
@@ -415,15 +466,22 @@ export default function HomePage() {
|
||||
{/* ML Detection Status Indicator */}
|
||||
{detectionEnabled && (
|
||||
<div className="absolute top-4 right-4 flex items-center gap-2 bg-black/60 backdrop-blur-sm rounded-lg px-3 py-2 text-white text-sm">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<span>ML Activo</span>
|
||||
<div className={`w-2 h-2 rounded-full ${currentDetection ? 'bg-red-500 animate-ping' : 'bg-green-500 animate-pulse'}`}></div>
|
||||
<span>{currentDetection ? '👟 SHOE DETECTED!' : 'ML Activo'}</span>
|
||||
{currentDetection && (
|
||||
<span className="text-green-400 font-bold">
|
||||
<span className="text-red-400 font-bold">
|
||||
{(currentDetection.confidence * 100).toFixed(0)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Detection Counters */}
|
||||
<div className="absolute top-16 right-4 bg-black/60 backdrop-blur-sm rounded-lg px-3 py-2 text-white text-xs space-y-1">
|
||||
<div className="text-green-400">👟 Shoes Found: {shoeDetectionCount}</div>
|
||||
<div className="text-gray-400">🔄 Inference Runs: {metrics?.detectionCount || 0}</div>
|
||||
<div className="text-blue-400">⚡ Avg Speed: {metrics?.inferenceTime ? `${metrics.inferenceTime.toFixed(0)}ms` : 'N/A'}</div>
|
||||
</div>
|
||||
|
||||
{/* Settings Panel */}
|
||||
<div className={`absolute left-0 top-0 bottom-0 w-80 bg-black/80 backdrop-blur-xl border-r border-white/20 transform transition-transform duration-500 ease-out z-40 ${
|
||||
|
||||
@@ -42,6 +42,7 @@ export class DetectionEngine {
|
||||
private lastDetectionCallbackTime?: number;
|
||||
|
||||
constructor() {
|
||||
console.log('🏗️ DetectionEngine constructor called');
|
||||
this.workerManager = new DetectionWorkerManager();
|
||||
|
||||
|
||||
@@ -49,7 +50,7 @@ export class DetectionEngine {
|
||||
const capabilities = detectDeviceCapabilities();
|
||||
this.config = getRecommendedConfig(capabilities);
|
||||
|
||||
console.log('Detection engine initialized', { capabilities, config: this.config });
|
||||
console.log('✅ Detection engine initialized', { capabilities, config: this.config });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,17 +59,21 @@ export class DetectionEngine {
|
||||
async initialize(modelVariant?: 'quantized' | 'standard' | 'full', onProgress?: (progress: number) => void): Promise<void> {
|
||||
const variant = modelVariant || this.config.modelVariant;
|
||||
|
||||
console.log(`🔧 Initializing detection engine with ${variant} model...`);
|
||||
|
||||
try {
|
||||
// Load the model into the worker
|
||||
console.log('📥 Loading model into worker...');
|
||||
await this.workerManager.loadModel(variant, onProgress);
|
||||
|
||||
// Configure the worker with current settings
|
||||
console.log('⚙️ Configuring worker...');
|
||||
await this.workerManager.configure(this.config);
|
||||
|
||||
console.log(`Detection engine initialized with ${variant} model`);
|
||||
console.log(`✅ Detection engine initialized with ${variant} model`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize detection engine:', error);
|
||||
console.error('❌ Failed to initialize detection engine:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -77,6 +82,12 @@ export class DetectionEngine {
|
||||
* Start continuous detection
|
||||
*/
|
||||
startContinuousDetection(videoElement: HTMLVideoElement): void {
|
||||
console.log('🚀 startContinuousDetection called:', {
|
||||
isRunning: this.isRunning,
|
||||
enableContinuous: this.config.enableContinuous,
|
||||
videoElement: !!videoElement
|
||||
});
|
||||
|
||||
if (this.isRunning) {
|
||||
console.warn('Detection already running');
|
||||
return;
|
||||
@@ -86,10 +97,11 @@ export class DetectionEngine {
|
||||
this.detectionMode = this.config.enableContinuous ? 'continuous' : 'trigger';
|
||||
|
||||
if (this.config.enableContinuous) {
|
||||
console.log('🔄 Starting continuous detection loop...');
|
||||
this.runContinuousLoop(videoElement);
|
||||
}
|
||||
|
||||
console.log(`Started detection in ${this.detectionMode} mode`);
|
||||
console.log(`✅ Started detection in ${this.detectionMode} mode`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,64 +123,26 @@ export class DetectionEngine {
|
||||
try {
|
||||
console.log('🎯 Starting trigger detection (high quality)');
|
||||
|
||||
// Load TensorFlow.js if not already loaded
|
||||
if (!window.tf) {
|
||||
const tf = await import('@tensorflow/tfjs');
|
||||
await import('@tensorflow/tfjs-backend-webgl');
|
||||
await tf.setBackend('webgl');
|
||||
await tf.ready();
|
||||
window.tf = tf;
|
||||
console.log('✅ TensorFlow.js loaded in main thread');
|
||||
}
|
||||
// Capture image data for trigger detection (high quality)
|
||||
const imageData = this.captureVideoFrame(videoElement, true);
|
||||
|
||||
// Load model if not already loaded
|
||||
if (!this.model) {
|
||||
console.log('📥 Loading model in main thread...');
|
||||
this.model = await window.tf.loadGraphModel('/models/model.json');
|
||||
console.log('✅ Model loaded in main thread');
|
||||
}
|
||||
|
||||
// Capture and preprocess image with higher quality
|
||||
const tensor = window.tf.tidy(() => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
canvas.width = 300;
|
||||
canvas.height = 300;
|
||||
ctx.drawImage(videoElement, 0, 0, 300, 300);
|
||||
|
||||
const img = window.tf.browser.fromPixels(canvas);
|
||||
return img.expandDims(0);
|
||||
});
|
||||
|
||||
console.log('📸 Trigger detection - Input tensor shape:', tensor.shape);
|
||||
|
||||
// Run model inference
|
||||
const result = await this.model.executeAsync(tensor);
|
||||
tensor.dispose();
|
||||
|
||||
console.log('🔬 Trigger detection - Model output:', result);
|
||||
|
||||
// Return high-confidence detection for manual triggers
|
||||
const triggerDetection: DetectionResult = {
|
||||
bbox: [0.25, 0.25, 0.5, 0.5], // Different position than continuous
|
||||
confidence: 0.92, // Higher confidence for trigger
|
||||
class: 'shoe',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
// Use worker manager for detection
|
||||
const detections = await this.workerManager.detect(imageData);
|
||||
const detection = detections.length > 0 ? detections[0] : null;
|
||||
|
||||
// Update metrics
|
||||
this.metrics.inferenceTime = performance.now() - startTime;
|
||||
this.metrics.detectionCount++;
|
||||
this.metrics.timestamp = Date.now();
|
||||
|
||||
console.log('✅ Trigger detection completed:', triggerDetection);
|
||||
console.log('✅ Trigger detection completed:', detection);
|
||||
|
||||
// Temporarily update the current detection to show trigger result
|
||||
if (this.onDetectionCallback) {
|
||||
this.onDetectionCallback(triggerDetection);
|
||||
// Trigger callbacks for immediate display
|
||||
if (this.onDetectionCallback && detection) {
|
||||
this.onDetectionCallback(detection);
|
||||
}
|
||||
|
||||
return triggerDetection;
|
||||
return detection;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Trigger detection failed:', error);
|
||||
@@ -194,61 +168,30 @@ export class DetectionEngine {
|
||||
console.log('🔄 Running continuous detection frame...');
|
||||
|
||||
try {
|
||||
// Load TensorFlow.js if not already loaded
|
||||
if (!window.tf) {
|
||||
const tf = await import('@tensorflow/tfjs');
|
||||
await import('@tensorflow/tfjs-backend-webgl');
|
||||
await tf.setBackend('webgl');
|
||||
await tf.ready();
|
||||
window.tf = tf;
|
||||
}
|
||||
|
||||
// Load model if not already loaded
|
||||
if (!this.model) {
|
||||
this.model = await window.tf.loadGraphModel('/models/model.json');
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// Capture and preprocess image (lower quality for continuous)
|
||||
const tensor = window.tf.tidy(() => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
canvas.width = 300;
|
||||
canvas.height = 300;
|
||||
ctx.drawImage(videoElement, 0, 0, 300, 300);
|
||||
|
||||
const img = window.tf.browser.fromPixels(canvas);
|
||||
return img.expandDims(0);
|
||||
});
|
||||
// Capture image data for continuous detection (lower quality)
|
||||
const imageData = this.captureVideoFrame(videoElement, false);
|
||||
|
||||
// Run model inference
|
||||
const result = await this.model.executeAsync(tensor);
|
||||
tensor.dispose();
|
||||
|
||||
// Return low-confidence detection for continuous mode (below popup threshold)
|
||||
const mockDetection: DetectionResult = {
|
||||
bbox: [0.1, 0.1, 0.3, 0.3],
|
||||
confidence: 0.5, // Medium confidence - shows bounding box but won't trigger popup
|
||||
class: 'shoe',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
// Use worker manager for detection
|
||||
const detections = await this.workerManager.detect(imageData);
|
||||
const detection = detections.length > 0 ? detections[0] : null;
|
||||
|
||||
const inferenceTime = performance.now() - startTime;
|
||||
console.log('⚡ Continuous detection completed:', { time: inferenceTime, confidence: mockDetection.confidence });
|
||||
console.log('⚡ Continuous detection completed:', { time: inferenceTime, detection });
|
||||
|
||||
// Apply temporal filtering
|
||||
const validDetection = this.applyTemporalFiltering(mockDetection);
|
||||
const validDetection = this.applyTemporalFiltering(detection);
|
||||
|
||||
// Update metrics
|
||||
this.updateMetrics(inferenceTime);
|
||||
|
||||
// Trigger callbacks (only if we have a valid detection)
|
||||
// Use a debounced approach to avoid too frequent updates
|
||||
if (this.onDetectionCallback && validDetection) {
|
||||
// Only update if it's been at least 500ms since last detection callback
|
||||
if (this.onDetectionCallback) {
|
||||
// Only update if it's been at least 100ms since last detection callback for continuous
|
||||
const now = Date.now();
|
||||
if (!this.lastDetectionCallbackTime || now - this.lastDetectionCallbackTime > 500) {
|
||||
if (!this.lastDetectionCallbackTime || now - this.lastDetectionCallbackTime > 100) {
|
||||
this.onDetectionCallback(validDetection);
|
||||
this.lastDetectionCallbackTime = now;
|
||||
}
|
||||
|
||||
@@ -33,18 +33,22 @@ export function detectDeviceCapabilities(): DeviceCapabilities {
|
||||
/**
|
||||
* Gets a recommended configuration based on the detected device tier.
|
||||
* @param capabilities The detected device capabilities.
|
||||
* @returns A partial DetectionConfig with recommended settings.
|
||||
* @returns A full DetectionConfig with recommended settings.
|
||||
*/
|
||||
export function getRecommendedConfig(capabilities: DeviceCapabilities): Partial<typeof DEFAULT_CONFIG> {
|
||||
export function getRecommendedConfig(capabilities: DeviceCapabilities): typeof DEFAULT_CONFIG {
|
||||
const baseConfig = { ...DEFAULT_CONFIG };
|
||||
|
||||
switch (capabilities.tier) {
|
||||
case 'high':
|
||||
return {
|
||||
...baseConfig,
|
||||
modelVariant: 'standard',
|
||||
frameSkip: 3,
|
||||
confidenceThreshold: 0.6,
|
||||
};
|
||||
case 'mid':
|
||||
return {
|
||||
...baseConfig,
|
||||
modelVariant: 'standard',
|
||||
frameSkip: 5,
|
||||
confidenceThreshold: 0.5,
|
||||
@@ -52,6 +56,7 @@ export function getRecommendedConfig(capabilities: DeviceCapabilities): Partial<
|
||||
case 'low':
|
||||
default:
|
||||
return {
|
||||
...baseConfig,
|
||||
modelVariant: 'quantized',
|
||||
frameSkip: 8,
|
||||
confidenceThreshold: 0.4,
|
||||
|
||||
@@ -48,22 +48,28 @@ export class InferencePipeline {
|
||||
process(boxes: number[], scores: number[], classes: number[], confidenceThreshold: number): DetectionResult | null {
|
||||
const detections: DetectionResult[] = [];
|
||||
|
||||
for (let i = 0; i < scores.length; i++) {
|
||||
// Process up to 5 detections like the working implementation
|
||||
for (let i = 0; i < Math.min(5, scores.length); i++) {
|
||||
const score = scores[i];
|
||||
if (score < confidenceThreshold) continue;
|
||||
|
||||
// Convert to percentage and check threshold like working implementation
|
||||
const scorePercent = score * 100;
|
||||
if (scorePercent < (confidenceThreshold * 100)) continue;
|
||||
|
||||
const classIndex = classes[i];
|
||||
const className = CLASS_LABELS[classIndex];
|
||||
if (className !== 'shoe') continue;
|
||||
|
||||
// Extract bounding box [y_min, x_min, y_max, x_max]
|
||||
const [yMin, xMin, yMax, xMax] = boxes.slice(i * 4, (i + 1) * 4);
|
||||
// Extract bounding box [y_min, x_min, y_max, x_max] like working implementation
|
||||
const yMin = boxes[i * 4];
|
||||
const xMin = boxes[i * 4 + 1];
|
||||
const yMax = boxes[i * 4 + 2];
|
||||
const xMax = boxes[i * 4 + 3];
|
||||
|
||||
// Convert to [x, y, width, height] format
|
||||
const bbox: [number, number, number, number] = [xMin, yMin, xMax - xMin, yMax - yMin];
|
||||
|
||||
const detection: DetectionResult = {
|
||||
bbox,
|
||||
confidence: score,
|
||||
class: className,
|
||||
class: 'shoe', // Assume all detections are shoes
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
if (this.isValid(detection)) {
|
||||
|
||||
@@ -35,7 +35,7 @@ export const DEFAULT_CONFIG: DetectionConfig = {
|
||||
frameSkip: 5,
|
||||
confidenceThreshold: 0.3, // Match the working implementation (30%)
|
||||
modelVariant: 'standard',
|
||||
maxDetections: 1,
|
||||
maxDetections: 5, // Match the working implementation (process up to 5 detections)
|
||||
inputSize: [300, 300], // Match the working implementation
|
||||
enableContinuous: true,
|
||||
enableTrigger: true,
|
||||
|
||||
@@ -56,8 +56,11 @@ export function useDetection(options: UseDetectionOptions = {}): UseDetectionRet
|
||||
|
||||
// Initialize detection engine
|
||||
const initialize = useCallback(async (videoElement: HTMLVideoElement): Promise<void> => {
|
||||
console.log('🚀 useDetection.initialize called:', { videoElement: !!videoElement });
|
||||
|
||||
// Prevent multiple initializations
|
||||
if (initializationPromiseRef.current) {
|
||||
console.log('⚠️ Initialization already in progress, returning existing promise');
|
||||
return initializationPromiseRef.current;
|
||||
}
|
||||
|
||||
@@ -66,6 +69,7 @@ export function useDetection(options: UseDetectionOptions = {}): UseDetectionRet
|
||||
|
||||
const initPromise = (async () => {
|
||||
try {
|
||||
console.log('🏗️ Creating detection engine...');
|
||||
// Create detection engine
|
||||
const engine = new DetectionEngine();
|
||||
detectionEngineRef.current = engine;
|
||||
@@ -110,6 +114,12 @@ export function useDetection(options: UseDetectionOptions = {}): UseDetectionRet
|
||||
|
||||
// Start continuous detection
|
||||
const startContinuous = useCallback(() => {
|
||||
console.log('🔄 useDetection.startContinuous called:', {
|
||||
hasEngine: !!detectionEngineRef.current,
|
||||
hasVideo: !!videoElementRef.current,
|
||||
enableContinuous
|
||||
});
|
||||
|
||||
if (!detectionEngineRef.current || !videoElementRef.current) {
|
||||
console.warn('Detection engine or video element not available');
|
||||
return;
|
||||
@@ -121,12 +131,14 @@ export function useDetection(options: UseDetectionOptions = {}): UseDetectionRet
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('🚀 Starting continuous detection...');
|
||||
detectionEngineRef.current.startContinuousDetection(videoElementRef.current);
|
||||
setIsDetecting(true);
|
||||
setError(null);
|
||||
console.log('✅ Continuous detection started successfully');
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Failed to start continuous detection');
|
||||
console.error('Start continuous detection failed:', error);
|
||||
console.error('❌ Start continuous detection failed:', error);
|
||||
setError(error.message);
|
||||
onError?.(error);
|
||||
}
|
||||
|
||||
@@ -44,18 +44,24 @@ async function loadModelWorker(variant: 'quantized' | 'standard' | 'full', model
|
||||
model = await tfGlobal.loadGraphModel(modelUrls[variant]);
|
||||
console.log('Worker: Real model loaded successfully', model);
|
||||
|
||||
// Warm up the model (assuming config is available by now)
|
||||
// Warm up the model like the working implementation
|
||||
if (model && config) {
|
||||
console.log('Worker: Warming up model with input size:', config.inputSize);
|
||||
const dummyInput = tfGlobal.zeros([1, ...config.inputSize, 3]);
|
||||
const result = model.execute(dummyInput);
|
||||
const dummyFloat = tfGlobal.zeros([1, ...config.inputSize, 3]);
|
||||
const dummyInput = tfGlobal.cast(dummyFloat, 'int32');
|
||||
dummyFloat.dispose();
|
||||
|
||||
const result = await model.executeAsync(
|
||||
{ image_tensor: dummyInput },
|
||||
['detection_boxes', 'num_detections', 'detection_classes', 'detection_scores']
|
||||
);
|
||||
console.log('Worker: Warmup result:', result);
|
||||
dummyInput.dispose();
|
||||
if (Array.isArray(result)) {
|
||||
result.forEach(t => t.dispose());
|
||||
} else {
|
||||
} else if (result) {
|
||||
result.dispose();
|
||||
}
|
||||
dummyInput.dispose();
|
||||
console.log('Worker: Model warmed up successfully.');
|
||||
}
|
||||
self.postMessage({ type: 'LOADED_MODEL', id });
|
||||
@@ -95,14 +101,22 @@ async function detect(imageData: ImageData, id: string) {
|
||||
// Create tensor from RGB data
|
||||
const img = tfGlobal.tensor3d(rgbData, [height, width, 3]);
|
||||
|
||||
// Resize to model input size (300x300) and add batch dimension
|
||||
// Resize to model input size (300x300) - this returns float32
|
||||
const resized = tfGlobal.image.resizeBilinear(img, config!.inputSize);
|
||||
return resized.expandDims(0); // Keep values in [0,255] range like the working implementation
|
||||
|
||||
// Cast to int32 as required by the model
|
||||
const int32Tensor = tfGlobal.cast(resized, 'int32');
|
||||
|
||||
return int32Tensor.expandDims(0); // Now properly int32
|
||||
});
|
||||
|
||||
try {
|
||||
console.log('Worker: About to execute model with tensor shape:', tensor.shape);
|
||||
const result = await model.executeAsync(tensor);
|
||||
console.log('Worker: About to execute model with tensor shape:', tensor.shape, 'dtype:', tensor.dtype);
|
||||
// Use the same input format as the working implementation
|
||||
const result = await model.executeAsync(
|
||||
{ image_tensor: tensor },
|
||||
['detection_boxes', 'num_detections', 'detection_classes', 'detection_scores']
|
||||
);
|
||||
tensor.dispose();
|
||||
|
||||
console.log('Worker: Model execution result:', result);
|
||||
@@ -118,13 +132,14 @@ async function detect(imageData: ImageData, id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!result || !Array.isArray(result) || result.length < 3) {
|
||||
if (!result || !Array.isArray(result) || result.length < 4) {
|
||||
console.error('Worker: Invalid model output:', result);
|
||||
self.postMessage({ type: 'DETECTION_RESULT', result: null, id });
|
||||
return;
|
||||
}
|
||||
|
||||
const [boxes, scores, classes] = result;
|
||||
// Match the working implementation: [boxes, num_detections, classes, scores]
|
||||
const [boxes, numDetections, classes, scores] = result;
|
||||
console.log('Worker: Extracting data from tensors...');
|
||||
|
||||
const boxesData = await boxes.data();
|
||||
@@ -140,7 +155,10 @@ async function detect(imageData: ImageData, id: string) {
|
||||
classesLength: classesData.length,
|
||||
firstFewBoxes: Array.from(boxesData.slice(0, 8)),
|
||||
firstFewScores: Array.from(scoresData.slice(0, 4)),
|
||||
firstFewClasses: Array.from(classesData.slice(0, 4))
|
||||
firstFewClasses: Array.from(classesData.slice(0, 4)),
|
||||
maxScore: Math.max(...Array.from(scoresData.slice(0, 10))),
|
||||
scoresAbove20: Array.from(scoresData.slice(0, 10)).filter(s => s > 0.2).length,
|
||||
scoresAbove30: Array.from(scoresData.slice(0, 10)).filter(s => s > 0.3).length
|
||||
});
|
||||
|
||||
result.forEach(t => t.dispose());
|
||||
|
||||
Reference in New Issue
Block a user