171 lines
4.7 KiB
TypeScript
171 lines
4.7 KiB
TypeScript
import { EventEmitter } from 'events';
|
|
|
|
// TensorFlow.js imports - will be loaded dynamically
|
|
let tf: typeof import('@tensorflow/tfjs') | null = null;
|
|
let cocoSsd: typeof import('@tensorflow-models/coco-ssd') | null = null;
|
|
|
|
interface Detection {
|
|
class: string;
|
|
score: number;
|
|
bbox: [number, number, number, number];
|
|
}
|
|
|
|
export class PresenceDetector extends EventEmitter {
|
|
private model: Awaited<ReturnType<typeof import('@tensorflow-models/coco-ssd').load>> | null = null;
|
|
private video: HTMLVideoElement | null = null;
|
|
private canvas: HTMLCanvasElement | null = null;
|
|
private context: CanvasRenderingContext2D | null = null;
|
|
private stream: MediaStream | null = null;
|
|
private detectionInterval: NodeJS.Timeout | null = null;
|
|
private isRunning: boolean = false;
|
|
private confidenceThreshold: number = 0.6;
|
|
private personDetected: boolean = false;
|
|
private noPersonCount: number = 0;
|
|
private readonly NO_PERSON_THRESHOLD = 5; // Frames without person before clearing
|
|
|
|
constructor(confidenceThreshold: number = 0.6) {
|
|
super();
|
|
this.confidenceThreshold = confidenceThreshold;
|
|
}
|
|
|
|
async start(): Promise<void> {
|
|
if (this.isRunning) return;
|
|
|
|
try {
|
|
// Dynamically import TensorFlow.js
|
|
tf = await import('@tensorflow/tfjs');
|
|
cocoSsd = await import('@tensorflow-models/coco-ssd');
|
|
|
|
// Set backend
|
|
await tf.setBackend('webgl');
|
|
await tf.ready();
|
|
|
|
// Load COCO-SSD model
|
|
console.log('Loading COCO-SSD model...');
|
|
this.model = await cocoSsd.load({
|
|
base: 'lite_mobilenet_v2', // Faster, lighter model
|
|
});
|
|
console.log('Model loaded');
|
|
|
|
// Set up video capture
|
|
await this.setupCamera();
|
|
|
|
// Start detection loop
|
|
this.isRunning = true;
|
|
this.startDetectionLoop();
|
|
} catch (error) {
|
|
console.error('Failed to start presence detection:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private async setupCamera(): Promise<void> {
|
|
try {
|
|
this.stream = await navigator.mediaDevices.getUserMedia({
|
|
video: {
|
|
width: { ideal: 640 },
|
|
height: { ideal: 480 },
|
|
facingMode: 'user',
|
|
},
|
|
audio: false,
|
|
});
|
|
|
|
// Create video element
|
|
this.video = document.createElement('video');
|
|
this.video.srcObject = this.stream;
|
|
this.video.autoplay = true;
|
|
this.video.playsInline = true;
|
|
|
|
// Create canvas for processing
|
|
this.canvas = document.createElement('canvas');
|
|
this.canvas.width = 640;
|
|
this.canvas.height = 480;
|
|
this.context = this.canvas.getContext('2d');
|
|
|
|
// Wait for video to be ready
|
|
await new Promise<void>((resolve) => {
|
|
if (this.video) {
|
|
this.video.onloadedmetadata = () => {
|
|
this.video?.play();
|
|
resolve();
|
|
};
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to setup camera:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private startDetectionLoop(): void {
|
|
// Run detection every 500ms
|
|
this.detectionInterval = setInterval(async () => {
|
|
await this.detectPerson();
|
|
}, 500);
|
|
}
|
|
|
|
private async detectPerson(): Promise<void> {
|
|
if (!this.model || !this.video || !this.context || !this.canvas) return;
|
|
|
|
try {
|
|
// Draw current frame to canvas
|
|
this.context.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
|
|
|
|
// Run detection
|
|
const predictions: Detection[] = await this.model.detect(this.canvas);
|
|
|
|
// Check for person with sufficient confidence
|
|
const personDetection = predictions.find(
|
|
(p) => p.class === 'person' && p.score >= this.confidenceThreshold
|
|
);
|
|
|
|
if (personDetection) {
|
|
this.noPersonCount = 0;
|
|
if (!this.personDetected) {
|
|
this.personDetected = true;
|
|
this.emit('personDetected', personDetection);
|
|
}
|
|
} else {
|
|
this.noPersonCount++;
|
|
if (this.personDetected && this.noPersonCount >= this.NO_PERSON_THRESHOLD) {
|
|
this.personDetected = false;
|
|
this.emit('noPersonDetected');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Detection error:', error);
|
|
}
|
|
}
|
|
|
|
async stop(): Promise<void> {
|
|
this.isRunning = false;
|
|
|
|
if (this.detectionInterval) {
|
|
clearInterval(this.detectionInterval);
|
|
this.detectionInterval = null;
|
|
}
|
|
|
|
if (this.stream) {
|
|
this.stream.getTracks().forEach((track) => track.stop());
|
|
this.stream = null;
|
|
}
|
|
|
|
if (this.video) {
|
|
this.video.srcObject = null;
|
|
this.video = null;
|
|
}
|
|
|
|
this.canvas = null;
|
|
this.context = null;
|
|
this.model = null;
|
|
}
|
|
|
|
isDetecting(): boolean {
|
|
return this.isRunning;
|
|
}
|
|
|
|
hasPersonPresent(): boolean {
|
|
return this.personDetected;
|
|
}
|
|
}
|