mirror of
https://github.com/koush/scrypted.git
synced 2026-02-17 20:22:14 +00:00
690 lines
23 KiB
TypeScript
690 lines
23 KiB
TypeScript
import { MixinProvider, ScryptedDeviceType, ScryptedInterface, MediaObject, VideoCamera, Settings, Setting, Camera, EventListenerRegister, ObjectDetector, ObjectDetection, ScryptedDevice, ObjectDetectionResult, ObjectDetectionTypes, ObjectsDetected, MotionSensor, MediaStreamOptions, MixinDeviceBase, ScryptedNativeId, DeviceState } from '@scrypted/sdk';
|
|
import sdk from '@scrypted/sdk';
|
|
import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
|
|
import { alertRecommendedPlugins } from '@scrypted/common/src/alert-recommended-plugins';
|
|
import { DenoisedDetectionEntry, denoiseDetections } from './denoise';
|
|
import { AutoenableMixinProvider } from "../../../common/src/autoenable-mixin-provider"
|
|
|
|
const polygonOverlap = require('polygon-overlap');
|
|
|
|
export interface DetectionInput {
|
|
jpegBuffer?: Buffer;
|
|
input: any;
|
|
}
|
|
|
|
const { mediaManager, systemManager, log } = sdk;
|
|
|
|
const defaultDetectionDuration = 60;
|
|
const defaultDetectionInterval = 60;
|
|
const defaultDetectionTimeout = 10;
|
|
const defaultMotionDuration = 10;
|
|
|
|
const DETECT_PERIODIC_SNAPSHOTS = "Periodic Snapshots";
|
|
const DETECT_MOTION_SNAPSHOTS = "Motion Snapshots";
|
|
const DETECT_VIDEO_MOTION = "Video Motion";
|
|
|
|
type ClipPath = [number, number][];
|
|
type Zones = { [zone: string]: ClipPath };
|
|
|
|
class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera & MotionSensor & ObjectDetector> implements ObjectDetector, Settings {
|
|
released = false;
|
|
motionListener: EventListenerRegister;
|
|
detectionListener: EventListenerRegister;
|
|
detectorListener: EventListenerRegister;
|
|
detections = new Map<string, DetectionInput>();
|
|
cameraDevice: ScryptedDevice & Camera & VideoCamera & MotionSensor;
|
|
detectSnapshotsOnly = this.storage.getItem('detectionMode');
|
|
detectionModes = this.getDetectionModes();
|
|
detectionTimeout = parseInt(this.storage.getItem('detectionTimeout')) || defaultDetectionTimeout;
|
|
detectionDuration = parseInt(this.storage.getItem('detectionDuration')) || defaultDetectionDuration;
|
|
motionDuration = parseInt(this.storage.getItem('motionDuration')) || defaultMotionDuration;
|
|
motionAsObjects = this.storage.getItem('motionAsObjects') === 'true';
|
|
motionTimeout: NodeJS.Timeout;
|
|
detectionInterval = parseInt(this.storage.getItem('detectionInterval')) || defaultDetectionInterval;
|
|
zones = this.getZones();
|
|
detectionIntervalTimeout: NodeJS.Timeout;
|
|
currentDetections: DenoisedDetectionEntry<ObjectDetectionResult>[] = [];
|
|
detectionId: string;
|
|
running = false;
|
|
hasMotionType: boolean;
|
|
settings: Setting[];
|
|
|
|
constructor(mixinDevice: VideoCamera & Settings, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string, public objectDetection: ObjectDetection & ScryptedDevice, modelName: string, group: string, public internal: boolean) {
|
|
super(mixinDevice, mixinDeviceState, {
|
|
providerNativeId,
|
|
mixinDeviceInterfaces,
|
|
group,
|
|
groupKey: "objectdetectionplugin:" + objectDetection.id,
|
|
mixinStorageSuffix: objectDetection.id,
|
|
});
|
|
|
|
this.cameraDevice = systemManager.getDeviceById<Camera & VideoCamera & MotionSensor>(this.id);
|
|
this.detectionId = internal ? modelName : modelName + '-' + this.cameraDevice.id;
|
|
|
|
this.bindObjectDetection();
|
|
this.register();
|
|
this.resetDetectionTimeout();
|
|
}
|
|
|
|
getDetectionModes(): string[] {
|
|
try {
|
|
return JSON.parse(this.storage.getItem('detectionModes'));
|
|
}
|
|
catch (e) {
|
|
return [
|
|
DETECT_PERIODIC_SNAPSHOTS,
|
|
DETECT_VIDEO_MOTION,
|
|
DETECT_MOTION_SNAPSHOTS,
|
|
];
|
|
}
|
|
}
|
|
|
|
clearDetectionTimeout() {
|
|
clearTimeout(this.detectionIntervalTimeout);
|
|
this.detectionIntervalTimeout = undefined;
|
|
}
|
|
|
|
resetDetectionTimeout() {
|
|
this.clearDetectionTimeout();
|
|
this.detectionIntervalTimeout = setInterval(() => {
|
|
if ((!this.running && this.detectionModes.includes(DETECT_PERIODIC_SNAPSHOTS)) || this.hasMotionType) {
|
|
this.snapshotDetection();
|
|
}
|
|
}, this.detectionInterval * 1000);
|
|
}
|
|
|
|
clearMotionTimeout() {
|
|
clearTimeout(this.motionTimeout);
|
|
this.motionTimeout = undefined;
|
|
}
|
|
|
|
resetMotionTimeout() {
|
|
this.clearMotionTimeout();
|
|
this.motionTimeout = setTimeout(() => {
|
|
this.motionDetected = false;
|
|
}, this.motionDuration * 1000);
|
|
}
|
|
|
|
async ensureSettings(): Promise<Setting[]> {
|
|
if (this.hasMotionType !== undefined)
|
|
return;
|
|
this.hasMotionType = false;
|
|
const model = await this.objectDetection.getDetectionModel();
|
|
this.hasMotionType = model.classes.includes('motion');
|
|
this.settings = model.settings;
|
|
this.motionDetected = false;
|
|
}
|
|
|
|
async getCurrentSettings() {
|
|
await this.ensureSettings();
|
|
if (!this.settings)
|
|
return;
|
|
|
|
const ret: any = {};
|
|
for (const setting of this.settings) {
|
|
ret[setting.key] = this.storage.getItem(setting.key) || setting.value;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
async snapshotDetection() {
|
|
await this.ensureSettings();
|
|
|
|
if (this.hasMotionType) {
|
|
await this.startVideoDetection();
|
|
return;
|
|
}
|
|
|
|
const picture = await this.cameraDevice.takePicture();
|
|
const detections = await this.objectDetection.detectObjects(picture, {
|
|
detectionId: this.detectionId,
|
|
settings: await this.getCurrentSettings(),
|
|
});
|
|
this.objectsDetected(detections, true);
|
|
this.reportObjectDetections(detections, undefined);
|
|
}
|
|
|
|
bindObjectDetection() {
|
|
this.running = false;
|
|
this.detectionListener?.removeListener();
|
|
this.detectionListener = undefined;
|
|
this.detectorListener?.removeListener();
|
|
this.detectorListener = undefined;
|
|
this.objectDetection?.detectObjects(undefined, {
|
|
detectionId: this.detectionId,
|
|
});
|
|
|
|
this.detectionListener = this.objectDetection.listen({
|
|
event: ScryptedInterface.ObjectDetection,
|
|
watch: false,
|
|
}, (eventSource, eventDetails, eventData: ObjectsDetected) => {
|
|
if (eventData?.detectionId !== this.detectionId)
|
|
return;
|
|
this.objectsDetected(eventData);
|
|
this.reportObjectDetections(eventData, undefined);
|
|
|
|
this.running = eventData.running;
|
|
});
|
|
|
|
if (this.detectionModes.includes(DETECT_PERIODIC_SNAPSHOTS))
|
|
this.snapshotDetection();
|
|
|
|
if (this.detectionModes.includes(DETECT_MOTION_SNAPSHOTS)) {
|
|
this.detectorListener = this.cameraDevice.listen(ScryptedInterface.ObjectDetector, async (eventSource, eventDetails, eventData: ObjectsDetected) => {
|
|
if (!eventData?.detections?.find(d => d.className === 'motion'))
|
|
return;
|
|
if (!eventData?.eventId)
|
|
return;
|
|
const od = eventSource as any as ObjectDetector;
|
|
const mo = await od.getDetectionInput(eventData.detectionId, eventData.eventId);
|
|
const detections = await this.objectDetection.detectObjects(mo, {
|
|
detectionId: this.detectionId,
|
|
settings: await this.getCurrentSettings(),
|
|
});
|
|
this.objectsDetected(detections, true);
|
|
this.reportObjectDetections(detections, eventData.detectionId);
|
|
});
|
|
}
|
|
}
|
|
|
|
async register() {
|
|
this.motionListener = this.cameraDevice.listen(ScryptedInterface.MotionSensor, async () => {
|
|
if (!this.cameraDevice.motionDetected)
|
|
return;
|
|
|
|
if (this.detectionModes.includes(DETECT_VIDEO_MOTION))
|
|
await this.startVideoDetection();
|
|
});
|
|
}
|
|
|
|
async startVideoDetection() {
|
|
try {
|
|
// prevent stream retrieval noise until notified that the detection is no longer running.
|
|
if (this.running) {
|
|
const session = await this.objectDetection?.detectObjects(undefined, {
|
|
detectionId: this.detectionId,
|
|
duration: this.getDetectionDuration(),
|
|
settings: await this.getCurrentSettings(),
|
|
});
|
|
this.running = session.running;
|
|
if (this.running)
|
|
return;
|
|
}
|
|
this.running = true;
|
|
|
|
let selectedStream: MediaStreamOptions;
|
|
let stream: MediaObject;
|
|
|
|
// intenral streams must implicitly be available.
|
|
if (!this.internal) {
|
|
const streamingChannel = this.storage.getItem('streamingChannel');
|
|
if (streamingChannel) {
|
|
const msos = await this.cameraDevice.getVideoStreamOptions();
|
|
selectedStream = msos.find(mso => mso.name === streamingChannel);
|
|
}
|
|
|
|
stream = await this.cameraDevice.getVideoStream(selectedStream);
|
|
}
|
|
else {
|
|
stream = mediaManager.createMediaObject(Buffer.alloc(0), 'x-scrypted/x-internal-media-object');
|
|
}
|
|
|
|
const session = await this.objectDetection?.detectObjects(stream, {
|
|
detectionId: this.detectionId,
|
|
duration: this.getDetectionDuration(),
|
|
settings: await this.getCurrentSettings(),
|
|
});
|
|
|
|
this.running = session.running;
|
|
}
|
|
catch (e) {
|
|
this.console.log('failure retrieving stream', e);
|
|
this.running = false;
|
|
}
|
|
}
|
|
|
|
getDetectionDuration() {
|
|
// when motion type, the detection interval is a keepalive reset.
|
|
// the duration needs to simply be an arbitrarily longer time.
|
|
return this.hasMotionType ? this.detectionInterval * 1000 * 5 : this.detectionDuration * 1000;
|
|
}
|
|
|
|
reportObjectDetections(detection: ObjectsDetected, detectionInput?: DetectionInput) {
|
|
if (detectionInput)
|
|
this.setDetection(this.detectionId, detectionInput);
|
|
|
|
if (this.hasMotionType) {
|
|
const found = detection.detections?.find(d => d.className === 'motion');
|
|
if (found) {
|
|
if (!this.motionDetected)
|
|
this.motionDetected = true;
|
|
this.resetMotionTimeout();
|
|
|
|
const areas = detection.detections.filter(d => d.className === 'motion' && d.score !== 1).map(d => d.score)
|
|
if (areas.length)
|
|
this.console.log('detection areas', areas);
|
|
}
|
|
}
|
|
|
|
if (!this.hasMotionType || this.motionAsObjects) {
|
|
if (detection.detections && Object.keys(this.zones).length) {
|
|
for (const o of detection.detections) {
|
|
if (!o.boundingBox)
|
|
continue;
|
|
o.zones = []
|
|
let [x, y, width, height] = o.boundingBox;
|
|
let x2 = x + width;
|
|
let y2 = y + height;
|
|
// the zones are point paths in percentage format
|
|
x = x * 100 / detection.inputDimensions[0];
|
|
y = y * 100 / detection.inputDimensions[1];
|
|
x2 = x2 * 100 / detection.inputDimensions[0];
|
|
y2 = y2 * 100 / detection.inputDimensions[1];
|
|
const box = [[x, y], [x2, y], [x2, y2], [x, y2]];
|
|
for (const [zone, zoneValue] of Object.entries(this.zones)) {
|
|
if (polygonOverlap(box, zoneValue)) {
|
|
this.console.log(o.className, 'inside', zone);
|
|
o.zones.push(zone);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.onDeviceEvent(ScryptedInterface.ObjectDetector, detection);
|
|
}
|
|
}
|
|
|
|
async extendedObjectDetect() {
|
|
try {
|
|
await this.objectDetection?.detectObjects(undefined, {
|
|
detectionId: this.detectionId,
|
|
duration: this.getDetectionDuration(),
|
|
});
|
|
}
|
|
catch (e) {
|
|
// ignore any
|
|
}
|
|
}
|
|
|
|
async objectsDetected(detectionResult: ObjectsDetected, showAll?: boolean) {
|
|
// do not denoise
|
|
if (this.hasMotionType) {
|
|
return;
|
|
}
|
|
|
|
if (!detectionResult?.detections) {
|
|
// detection session ended.
|
|
return;
|
|
}
|
|
|
|
const { detections } = detectionResult;
|
|
|
|
const found: DenoisedDetectionEntry<ObjectDetectionResult>[] = [];
|
|
denoiseDetections<ObjectDetectionResult>(this.currentDetections, detections.map(detection => ({
|
|
id: detection.id,
|
|
name: detection.className,
|
|
detection,
|
|
})), {
|
|
timeout: this.detectionTimeout * 1000,
|
|
added: d => found.push(d),
|
|
removed: d => {
|
|
this.console.log('expired detection:', `${d.detection.className} (${d.detection.score}, ${d.detection.id})`);
|
|
if (detectionResult.running)
|
|
this.extendedObjectDetect();
|
|
}
|
|
});
|
|
if (found.length) {
|
|
this.console.log('new detection:', found.map(d => `${d.detection.className} (${d.detection.score}, ${d.detection.id})`).join(', '));
|
|
if (detectionResult.running)
|
|
this.extendedObjectDetect();
|
|
}
|
|
if (found.length || showAll) {
|
|
this.console.log('current detections:', this.currentDetections.map(d => `${d.detection.className} (${d.detection.score}, ${d.detection.id})`).join(', '));
|
|
}
|
|
}
|
|
|
|
setDetection(detectionId: string, detectionInput: DetectionInput) {
|
|
// this.detections.set(detectionId, detectionInput);
|
|
// setTimeout(() => {
|
|
// this.detections.delete(detectionId);
|
|
// detectionInput?.input?.dispose();
|
|
// }, DISPOSE_TIMEOUT);
|
|
}
|
|
|
|
async getNativeObjectTypes(): Promise<ObjectDetectionTypes> {
|
|
if (this.mixinDeviceInterfaces.includes(ScryptedInterface.ObjectDetector))
|
|
return this.mixinDevice.getObjectTypes();
|
|
return {};
|
|
}
|
|
|
|
async getObjectTypes(): Promise<ObjectDetectionTypes> {
|
|
const ret = await this.getNativeObjectTypes();
|
|
if (!ret.classes)
|
|
ret.classes = [];
|
|
ret.classes.push(...(await this.objectDetection.getDetectionModel()).classes);
|
|
return ret;
|
|
}
|
|
|
|
async getDetectionInput(detectionId: any): Promise<MediaObject> {
|
|
const detection = this.detections.get(detectionId);
|
|
if (!detection) {
|
|
if (this.mixinDeviceInterfaces.includes(ScryptedInterface.ObjectDetector))
|
|
return this.mixinDevice.getDetectionInput(detectionId);
|
|
return;
|
|
}
|
|
// if (!detection.jpegBuffer) {
|
|
// detection.jpegBuffer = Buffer.from(await encodeJpeg(detection.input));
|
|
// }
|
|
return mediaManager.createMediaObject(detection.jpegBuffer, 'image/jpeg');
|
|
}
|
|
|
|
async getMixinSettings(): Promise<Setting[]> {
|
|
const settings: Setting[] = [];
|
|
|
|
if (this.hasMotionType && this.mixinDeviceInterfaces.includes(ScryptedInterface.MotionSensor)) {
|
|
settings.push({
|
|
title: 'Existing Motion Sensor',
|
|
description: 'This camera has a built in motion sensor. Using OpenCV Motion Sensing may be unnecessary and will use additional CPU.',
|
|
readonly: true,
|
|
value: 'WARNING',
|
|
key: 'existingMotionSensor',
|
|
})
|
|
}
|
|
|
|
let msos: MediaStreamOptions[] = [];
|
|
try {
|
|
msos = await this.cameraDevice.getVideoStreamOptions();
|
|
}
|
|
catch (e) {
|
|
}
|
|
|
|
if (!this.hasMotionType) {
|
|
settings.push({
|
|
title: 'Detection Modes',
|
|
description: 'Configure when to analyze the video stream. Video Motion can be CPU intensive.',
|
|
key: 'detectionModes',
|
|
type: 'string',
|
|
multiple: true,
|
|
choices: [
|
|
DETECT_PERIODIC_SNAPSHOTS,
|
|
DETECT_VIDEO_MOTION,
|
|
DETECT_MOTION_SNAPSHOTS,
|
|
],
|
|
value: this.detectionModes,
|
|
});
|
|
|
|
if (this.detectionModes.includes(DETECT_VIDEO_MOTION)) {
|
|
if (msos?.length && !this.internal) {
|
|
settings.push({
|
|
title: 'Video Stream',
|
|
key: 'streamingChannel',
|
|
value: this.storage.getItem('streamingChannel') || msos[0].name,
|
|
description: 'The media stream to analyze.',
|
|
choices: msos.map(mso => mso.name),
|
|
});
|
|
}
|
|
|
|
settings.push(
|
|
{
|
|
title: 'Detection Duration',
|
|
description: 'The duration in seconds to analyze video when motion occurs.',
|
|
key: 'detectionDuration',
|
|
type: 'number',
|
|
value: this.detectionDuration.toString(),
|
|
}
|
|
);
|
|
}
|
|
|
|
if (this.detectionModes.includes(DETECT_PERIODIC_SNAPSHOTS)) {
|
|
settings.push(
|
|
{
|
|
title: 'Idle Detection Interval',
|
|
description: 'The interval in seconds to analyze snapshots when there is no motion.',
|
|
key: 'detectionInterval',
|
|
type: 'number',
|
|
value: this.detectionInterval.toString(),
|
|
}
|
|
);
|
|
}
|
|
|
|
settings.push(
|
|
{
|
|
title: 'Detection Timeout',
|
|
description: 'Timeout in seconds before removing an object that is no longer detected.',
|
|
key: 'detectionTimeout',
|
|
type: 'number',
|
|
value: this.detectionTimeout.toString(),
|
|
},
|
|
);
|
|
}
|
|
else {
|
|
if (msos?.length && !this.internal) {
|
|
settings.push({
|
|
title: 'Video Stream',
|
|
key: 'streamingChannel',
|
|
value: this.storage.getItem('streamingChannel') || msos[0].name,
|
|
description: 'The media stream to analyze.',
|
|
choices: msos.map(mso => mso.name),
|
|
});
|
|
}
|
|
|
|
settings.push({
|
|
title: 'Motion Duration',
|
|
description: 'The duration in seconds to wait to reset the motion sensor.',
|
|
key: 'motionDuration',
|
|
type: 'number',
|
|
value: this.motionDuration.toString(),
|
|
},
|
|
{
|
|
title: 'Motion Detection Objects',
|
|
description: 'Report motion detections as objects (useful for debugging).',
|
|
key: 'motionAsObjects',
|
|
type: 'boolean',
|
|
value: this.motionAsObjects,
|
|
}
|
|
);
|
|
}
|
|
|
|
if (this.settings) {
|
|
settings.push(...this.settings.map(setting =>
|
|
Object.assign({}, setting, {
|
|
placeholder: setting.placeholder?.toString(),
|
|
value: this.storage.getItem(setting.key) || setting.value,
|
|
} as Setting))
|
|
);
|
|
}
|
|
|
|
if (!this.hasMotionType) {
|
|
settings.push(
|
|
{
|
|
title: 'Analyze',
|
|
description: 'Analyzes the video stream for 1 minute. Results will be shown in the Console.',
|
|
key: 'analyzeButton',
|
|
type: 'button',
|
|
}
|
|
);
|
|
}
|
|
|
|
settings.push({
|
|
key: 'zones',
|
|
title: 'Zones',
|
|
type: 'string',
|
|
multiple: true,
|
|
value: Object.keys(this.zones),
|
|
choices: Object.keys(this.zones),
|
|
combobox: true,
|
|
});
|
|
|
|
for (const [name, value] of Object.entries(this.zones)) {
|
|
settings.push({
|
|
key: `zone-${name}`,
|
|
title: `Edit Zone: ${name}`,
|
|
type: 'clippath',
|
|
value: JSON.stringify(value),
|
|
});
|
|
}
|
|
|
|
return settings;
|
|
}
|
|
|
|
getZones(): Zones {
|
|
try {
|
|
return JSON.parse(this.storage.getItem('zones'));
|
|
}
|
|
catch (e) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
async putMixinSetting(key: string, value: string | number | boolean | string[] | number[]): Promise<void> {
|
|
const vs = value?.toString();
|
|
|
|
if (key === 'zones') {
|
|
const newZones: Zones = {};
|
|
for (const name of value as string[]) {
|
|
newZones[name] = this.zones[name] || [];
|
|
}
|
|
this.zones = newZones;
|
|
this.storage.setItem('zones', JSON.stringify(newZones));
|
|
return;
|
|
}
|
|
if (key.startsWith('zone-')) {
|
|
this.zones[key.substring(5)] = JSON.parse(vs);
|
|
this.storage.setItem('zones', JSON.stringify(this.zones));
|
|
return;
|
|
}
|
|
|
|
this.storage.setItem(key, vs);
|
|
if (key === 'detectionDuration') {
|
|
this.detectionDuration = parseInt(vs) || defaultDetectionDuration;
|
|
}
|
|
else if (key === 'detectionInterval') {
|
|
this.detectionInterval = parseInt(vs) || defaultDetectionInterval;
|
|
this.resetDetectionTimeout();
|
|
}
|
|
else if (key === 'detectionTimeout') {
|
|
this.detectionTimeout = parseInt(vs) || defaultDetectionTimeout;
|
|
}
|
|
else if (key === 'motionDuration') {
|
|
this.motionDuration = parseInt(vs) || defaultMotionDuration;
|
|
}
|
|
else if (key === 'motionAsObjects') {
|
|
this.motionAsObjects = vs === 'true';
|
|
}
|
|
else if (key === 'streamingChannel') {
|
|
this.bindObjectDetection();
|
|
}
|
|
else if (key === 'analyzeButton') {
|
|
await this.snapshotDetection();
|
|
await this.startVideoDetection();
|
|
await this.extendedObjectDetect();
|
|
}
|
|
else if (key === 'detectionModes') {
|
|
this.storage.setItem(key, JSON.stringify(value));
|
|
this.detectionModes = this.getDetectionModes();
|
|
this.bindObjectDetection();
|
|
}
|
|
else {
|
|
const settings = await this.getCurrentSettings();
|
|
if (settings && settings[key]) {
|
|
settings[key] = value;
|
|
}
|
|
this.bindObjectDetection();
|
|
}
|
|
}
|
|
|
|
release() {
|
|
super.release();
|
|
this.released = true;
|
|
this.clearDetectionTimeout();
|
|
this.clearMotionTimeout();
|
|
this.motionListener?.removeListener();
|
|
this.detectionListener?.removeListener();
|
|
this.detectorListener?.removeListener();
|
|
this.objectDetection?.detectObjects(undefined, {
|
|
detectionId: this.detectionId,
|
|
});
|
|
}
|
|
}
|
|
|
|
class ObjectDetectorMixin extends MixinDeviceBase<ObjectDetection> implements MixinProvider {
|
|
constructor(mixinDevice: ObjectDetection, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: DeviceState, mixinProviderNativeId: ScryptedNativeId, public modelName: string, public internal?: boolean) {
|
|
super(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, mixinProviderNativeId);
|
|
|
|
// trigger mixin creation. todo: fix this to not be stupid hack.
|
|
for (const id of Object.keys(systemManager.getSystemState())) {
|
|
const device = systemManager.getDeviceById<VideoCamera & Settings>(id);
|
|
if (!device.mixins?.includes(this.id))
|
|
continue;
|
|
device.probe();
|
|
|
|
}
|
|
}
|
|
|
|
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
|
|
// filter out
|
|
for (const iface of interfaces) {
|
|
if (iface.startsWith(`${ScryptedInterface.ObjectDetection}:`)) {
|
|
const deviceMatch = this.mixinDeviceInterfaces.find(miface => miface.startsWith(iface));
|
|
if (deviceMatch)
|
|
continue;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if ((type === ScryptedDeviceType.Camera || type === ScryptedDeviceType.Doorbell) && (interfaces.includes(ScryptedInterface.VideoCamera) || interfaces.includes(ScryptedInterface.Camera))) {
|
|
return [ScryptedInterface.ObjectDetector, ScryptedInterface.MotionSensor, ScryptedInterface.Settings];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }) {
|
|
let objectDetection = systemManager.getDeviceById<ObjectDetection>(this.id);
|
|
const group = objectDetection.name;
|
|
return new ObjectDetectionMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.mixinProviderNativeId, objectDetection, this.modelName, group, this.internal);
|
|
}
|
|
|
|
async releaseMixin(id: string, mixinDevice: any) {
|
|
this.console.log('releasing ObjectDetection mixin', id);
|
|
mixinDevice.release();
|
|
}
|
|
}
|
|
|
|
class ObjectDetectionPlugin extends AutoenableMixinProvider {
|
|
constructor(nativeId?: ScryptedNativeId) {
|
|
super(nativeId);
|
|
|
|
alertRecommendedPlugins({
|
|
'@scrypted/opencv': "OpenCV Motion Detection Plugin",
|
|
// '@scrypted/tensorflow': 'TensorFlow Face Recognition Plugin',
|
|
// '@scrypted/tensorflow-lite': 'TensorFlow Lite Object Detection Plugin',
|
|
});
|
|
}
|
|
|
|
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
|
|
if (!interfaces.includes(ScryptedInterface.ObjectDetection))
|
|
return;
|
|
return [ScryptedInterface.MixinProvider];
|
|
}
|
|
|
|
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise<any> {
|
|
for (const iface of mixinDeviceInterfaces) {
|
|
if (iface.startsWith(`${ScryptedInterface.ObjectDetection}:`)) {
|
|
const model = await mixinDevice.getDetectionModel();
|
|
|
|
return new ObjectDetectorMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.nativeId, model.name, true);
|
|
}
|
|
}
|
|
|
|
const model = await mixinDevice.getDetectionModel();
|
|
return new ObjectDetectorMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.nativeId, model.name);
|
|
}
|
|
|
|
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
|
|
// what does this mean to make a mixin provider no longer available?
|
|
// just ignore it until reboot?
|
|
}
|
|
}
|
|
|
|
export default new ObjectDetectionPlugin();
|