Files
temp_SSA_SCAN/lib/ml/detection-engine.ts

425 lines
13 KiB
TypeScript

import type { DetectionConfig, DetectionResult, DetectionMetrics, DetectionMode } from './types';
import { DetectionWorkerManager } from './detection-worker-manager';
import { detectDeviceCapabilities, getRecommendedConfig } from './device-capabilities';
// Extend window interface for TensorFlow.js
declare global {
interface Window {
tf: any;
}
}
/**
* Main detection engine that coordinates continuous and trigger detection
*/
export class DetectionEngine {
private workerManager: DetectionWorkerManager;
private config: DetectionConfig;
private model: any = null; // TensorFlow.js model instance
// Detection state
private isRunning = false;
private detectionMode: DetectionMode = 'hybrid';
private frameSkipCounter = 0;
// Temporal filtering
private detectionHistory: DetectionResult[] = [];
private lastValidDetection: DetectionResult | null = null;
// Performance tracking
private metrics: DetectionMetrics = {
fps: 0,
inferenceTime: 0,
memoryUsage: 0,
detectionCount: 0,
falsePositiveRate: 0,
timestamp: Date.now()
};
// Event callbacks
private onDetectionCallback?: (detection: DetectionResult | null) => void;
private onMetricsCallback?: (metrics: DetectionMetrics) => void;
private lastDetectionCallbackTime?: number;
constructor() {
this.workerManager = new DetectionWorkerManager();
// Get device-optimized configuration
const capabilities = detectDeviceCapabilities();
this.config = getRecommendedConfig(capabilities);
console.log('Detection engine initialized', { capabilities, config: this.config });
}
/**
* Initialize the detection engine with a specific model
*/
async initialize(modelVariant?: 'quantized' | 'standard' | 'full', onProgress?: (progress: number) => void): Promise<void> {
const variant = modelVariant || this.config.modelVariant;
try {
// Load the model into the worker
await this.workerManager.loadModel(variant, onProgress);
// Configure the worker with current settings
await this.workerManager.configure(this.config);
console.log(`Detection engine initialized with ${variant} model`);
} catch (error) {
console.error('Failed to initialize detection engine:', error);
throw error;
}
}
/**
* Start continuous detection
*/
startContinuousDetection(videoElement: HTMLVideoElement): void {
if (this.isRunning) {
console.warn('Detection already running');
return;
}
this.isRunning = true;
this.detectionMode = this.config.enableContinuous ? 'continuous' : 'trigger';
if (this.config.enableContinuous) {
this.runContinuousLoop(videoElement);
}
console.log(`Started detection in ${this.detectionMode} mode`);
}
/**
* Stop continuous detection
*/
stopContinuousDetection(): void {
this.isRunning = false;
this.frameSkipCounter = 0;
this.detectionHistory = [];
console.log('Stopped continuous detection');
}
/**
* Perform single trigger detection - higher quality/confidence than continuous
*/
async triggerDetection(videoElement: HTMLVideoElement): Promise<DetectionResult | null> {
const startTime = performance.now();
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');
}
// 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()
};
// Update metrics
this.metrics.inferenceTime = performance.now() - startTime;
this.metrics.detectionCount++;
this.metrics.timestamp = Date.now();
console.log('✅ Trigger detection completed:', triggerDetection);
// Temporarily update the current detection to show trigger result
if (this.onDetectionCallback) {
this.onDetectionCallback(triggerDetection);
}
return triggerDetection;
} catch (error) {
console.error('❌ Trigger detection failed:', error);
throw error;
}
}
/**
* Continuous detection loop
*/
private async runContinuousLoop(videoElement: HTMLVideoElement): Promise<void> {
if (!this.isRunning) return;
// Frame skipping logic
this.frameSkipCounter++;
if (this.frameSkipCounter < this.config.frameSkip) {
// Skip this frame, schedule next iteration
requestAnimationFrame(() => this.runContinuousLoop(videoElement));
return;
}
this.frameSkipCounter = 0;
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);
});
// 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()
};
const inferenceTime = performance.now() - startTime;
console.log('⚡ Continuous detection completed:', { time: inferenceTime, confidence: mockDetection.confidence });
// Apply temporal filtering
const validDetection = this.applyTemporalFiltering(mockDetection);
// 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
const now = Date.now();
if (!this.lastDetectionCallbackTime || now - this.lastDetectionCallbackTime > 500) {
this.onDetectionCallback(validDetection);
this.lastDetectionCallbackTime = now;
}
}
} catch (error) {
console.error('Continuous detection error:', error);
}
// Schedule next iteration
if (this.isRunning) {
requestAnimationFrame(() => this.runContinuousLoop(videoElement));
}
}
/**
* Capture frame from video element
*/
private captureVideoFrame(videoElement: HTMLVideoElement, highQuality: boolean): ImageData {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
// Use different resolutions based on detection mode
const [targetWidth, targetHeight] = highQuality
? [640, 480] // High quality for trigger detection
: [320, 240]; // Lower quality for continuous detection
canvas.width = targetWidth;
canvas.height = targetHeight;
// Draw video frame to canvas
ctx.drawImage(videoElement, 0, 0, targetWidth, targetHeight);
// Extract image data
const imageData = ctx.getImageData(0, 0, targetWidth, targetHeight);
// Cleanup
canvas.remove();
return imageData;
}
/**
* Apply temporal consistency filtering to reduce false positives
*/
private applyTemporalFiltering(detection: DetectionResult | null): DetectionResult | null {
if (!detection) {
// No detection - decay previous detections
this.detectionHistory = this.detectionHistory.filter(d =>
Date.now() - d.timestamp < 1000 // Keep detections from last second
);
// If we have recent consistent detections, continue showing them
if (this.detectionHistory.length >= 2) {
return this.lastValidDetection;
}
return null;
}
// Add current detection to history
this.detectionHistory.push(detection);
// Keep only recent detections (last 3 seconds)
this.detectionHistory = this.detectionHistory.filter(d =>
Date.now() - d.timestamp < 3000
);
// Check temporal consistency
const recentDetections = this.detectionHistory.filter(d =>
Date.now() - d.timestamp < 500 // Last 500ms
);
if (recentDetections.length >= 2) {
// We have consistent detections - this is likely valid
this.lastValidDetection = detection;
return detection;
}
// Not enough temporal consistency yet
return this.lastValidDetection;
}
/**
* Update performance metrics
*/
private updateMetrics(inferenceTime: number): void {
this.metrics = {
fps: 0, // Placeholder, as PerformanceMonitor is removed
inferenceTime: inferenceTime,
memoryUsage: this.getMemoryUsage(),
detectionCount: this.metrics.detectionCount + 1,
falsePositiveRate: this.calculateFalsePositiveRate(),
timestamp: Date.now()
};
if (this.onMetricsCallback) {
this.onMetricsCallback(this.metrics);
}
}
/**
* Get current memory usage (rough estimate)
*/
private getMemoryUsage(): number {
const memInfo = (performance as Performance & { memory?: { usedJSHeapSize: number } }).memory;
if (memInfo && memInfo.usedJSHeapSize) {
return memInfo.usedJSHeapSize / (1024 * 1024); // MB
}
return 0;
}
/**
* Calculate false positive rate (simplified)
*/
private calculateFalsePositiveRate(): number {
// This would need more sophisticated tracking in a real implementation
return Math.max(0, Math.min(1, this.detectionHistory.length > 10 ? 0.1 : 0.05));
}
/**
* Set detection callback
*/
onDetection(callback: (detection: DetectionResult | null) => void): void {
this.onDetectionCallback = callback;
}
/**
* Set metrics callback
*/
onMetrics(callback: (metrics: DetectionMetrics) => void): void {
this.onMetricsCallback = callback;
}
/**
* Update configuration
*/
async updateConfig(newConfig: Partial<DetectionConfig>): Promise<void> {
this.config = { ...this.config, ...newConfig };
await this.workerManager.configure(this.config);
console.log('Configuration updated:', this.config);
}
/**
* Get current configuration
*/
getConfig(): DetectionConfig {
return { ...this.config };
}
/**
* Get current metrics
*/
getMetrics(): DetectionMetrics {
return { ...this.metrics };
}
/**
* Check if detection is running
*/
isDetectionRunning(): boolean {
return this.isRunning;
}
/**
* Destroy the detection engine
*/
destroy(): void {
this.stopContinuousDetection();
this.workerManager.destroy();
}
}