Skip to content

Commit

Permalink
videoanalysis: smart occupancy sensor
Browse files Browse the repository at this point in the history
  • Loading branch information
koush committed Dec 19, 2024
1 parent c7ab908 commit 68cbe9a
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 58 deletions.
6 changes: 5 additions & 1 deletion plugins/objectdetector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ benefits to HomeKit, which does its own detection processing.

## Smart Motion Sensors

This plugin can be used to create smart motion sensors that trigger when a specific type of object (car, person, dog, etc) triggers movement on a camera. Created sensors can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This feature requires cameras with hardware or software object detection capability.
This plugin can be used to create smart motion sensors that trigger when a specific type of object (vehicle, person, animal, etc) triggers movement on a camera. Created sensors can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires cameras with hardware or software object detection capability.

## Smart Occupancy Sensors

This plugin can be used to create smart occupancy sensors remains triggered when a specific type of object (vehicle, person, animal, etc) is detected on a camera. Created sensors can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires an object detector plugin such as Scrypted NVR, OpenVINO, CoreML, ONNX, or Tensorflow-lite.
4 changes: 2 additions & 2 deletions plugins/objectdetector/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion plugins/objectdetector/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@scrypted/objectdetector",
"version": "0.1.52",
"version": "0.1.55",
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
"author": "Scrypted",
"license": "Apache-2.0",
Expand Down
100 changes: 75 additions & 25 deletions plugins/objectdetector/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import crypto from 'crypto';
import { AutoenableMixinProvider } from "../../../common/src/autoenable-mixin-provider";
import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
import { FFmpegVideoFrameGenerator } from './ffmpeg-videoframes';
import { insidePolygon, normalizeBox, polygonOverlap } from './polygon';
import { SMART_MOTIONSENSOR_PREFIX, SmartMotionSensor, createObjectDetectorStorageSetting } from './smart-motionsensor';
import { fixLegacyClipPath, insidePolygon, normalizeBoxToClipPath, polygonOverlap } from './polygon';
import { SMART_MOTIONSENSOR_PREFIX, SmartMotionSensor } from './smart-motionsensor';
import { SMART_OCCUPANCYSENSOR_PREFIX, SmartOccupancySensor } from './smart-occupancy-sensor';
import { getAllDevices, safeParseJson } from './util';


Expand Down Expand Up @@ -542,15 +543,16 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (!o.boundingBox)
continue;

const box = normalizeBox(o.boundingBox, detection.inputDimensions);
const box = normalizeBoxToClipPath(o.boundingBox, detection.inputDimensions);

let included: boolean;
// need a way to explicitly include package zone.
if (o.zones)
included = true;
else
o.zones = [];
for (const [zone, zoneValue] of Object.entries(this.zones)) {
for (let [zone, zoneValue] of Object.entries(this.zones)) {
zoneValue = fixLegacyClipPath(zoneValue);
if (zoneValue.length < 3) {
// this.console.warn(zone, 'Zone is unconfigured, skipping.');
continue;
Expand Down Expand Up @@ -1042,7 +1044,7 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
super(nativeId, 'v5');

this.systemDevice = {
deviceCreator: 'Smart Motion Sensor',
deviceCreator: 'Smart Sensor',
};

process.nextTick(() => {
Expand Down Expand Up @@ -1194,6 +1196,8 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
ret = this.devices.get(nativeId) || new FFmpegVideoFrameGenerator('ffmpeg');
if (nativeId?.startsWith(SMART_MOTIONSENSOR_PREFIX))
ret = this.devices.get(nativeId) || new SmartMotionSensor(this, nativeId);
if (nativeId?.startsWith(SMART_OCCUPANCYSENSOR_PREFIX))
ret = this.devices.get(nativeId) || new SmartOccupancySensor(this, nativeId);

if (ret)
this.devices.set(nativeId, ret);
Expand All @@ -1204,6 +1208,13 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
if (nativeId?.startsWith(SMART_MOTIONSENSOR_PREFIX)) {
const smart = this.devices.get(nativeId) as SmartMotionSensor;
smart?.detectionListener?.removeListener();
smart?.resetMotionTimeout();
}
if (nativeId?.startsWith(SMART_OCCUPANCYSENSOR_PREFIX)) {
const smart = this.devices.get(nativeId) as SmartOccupancySensor;
smart?.detectionListener?.removeListener();
smart?.resetOccupiedTimeout();
smart?.clearOccupancyInterval();
}
}

Expand Down Expand Up @@ -1239,32 +1250,71 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se

async getCreateDeviceSettings(): Promise<Setting[]> {
return [
createObjectDetectorStorageSetting(),
{
key: 'sensorType',
title: 'Sensor Type',
description: 'Select the type of sensor to create.',
choices: [
'Smart Motion Sensor',
'Smart Occupancy Sensor',
],
},
{
key: 'camera',
title: 'Camera',
description: 'Select a camera or doorbell.',
type: 'device',
deviceFilter: `type === '${ScryptedDeviceType.Doorbell}' || type === '${ScryptedDeviceType.Camera}'`,
},
];
}

async createDevice(settings: DeviceCreatorSettings): Promise<string> {
const nativeId = SMART_MOTIONSENSOR_PREFIX + crypto.randomBytes(8).toString('hex');
const objectDetector = sdk.systemManager.getDeviceById(settings.objectDetector as string);
let name = objectDetector.name || 'New';
name += ' Smart Motion Sensor'

const id = await sdk.deviceManager.onDeviceDiscovered({
nativeId,
name,
type: ScryptedDeviceType.Sensor,
interfaces: [
ScryptedInterface.Camera,
ScryptedInterface.MotionSensor,
ScryptedInterface.Settings,
ScryptedInterface.Readme,
]
});
const sensorType = settings.sensorType;
const camera = sdk.systemManager.getDeviceById(settings.camera as string);
if (sensorType === 'Smart Motion Sensor') {
const nativeId = SMART_MOTIONSENSOR_PREFIX + crypto.randomBytes(8).toString('hex');
let name = camera.name || 'New';
name += ' Smart Motion Sensor'

const id = await sdk.deviceManager.onDeviceDiscovered({
nativeId,
name,
type: ScryptedDeviceType.Sensor,
interfaces: [
ScryptedInterface.Camera,
ScryptedInterface.MotionSensor,
ScryptedInterface.Settings,
ScryptedInterface.Readme,
]
});

const sensor = new SmartMotionSensor(this, nativeId);
sensor.storageSettings.values.objectDetector = objectDetector?.id;
const sensor = new SmartMotionSensor(this, nativeId);
sensor.storageSettings.values.objectDetector = camera?.id;

return id;
return id;
}
else if (sensorType === 'Smart Occupancy Sensor') {
const nativeId = SMART_OCCUPANCYSENSOR_PREFIX + crypto.randomBytes(8).toString('hex');
let name = camera.name || 'New';
name += ' Smart Occupancy Sensor'

const id = await sdk.deviceManager.onDeviceDiscovered({
nativeId,
name,
type: ScryptedDeviceType.Sensor,
interfaces: [
ScryptedInterface.OccupancySensor,
ScryptedInterface.Settings,
ScryptedInterface.Readme,
]
});

const sensor = new SmartOccupancySensor(this, nativeId);
sensor.storageSettings.values.camera = camera?.id;

return id;
}
}
}

Expand Down
33 changes: 27 additions & 6 deletions plugins/objectdetector/src/polygon.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Point } from '@scrypted/sdk';
import type { ClipPath, Point } from '@scrypted/sdk';
import polygonClipping from 'polygon-clipping';

// const polygonOverlap = require('polygon-overlap');
Expand All @@ -14,15 +14,36 @@ export function insidePolygon(point: Point, polygon: Point[]) {
return !!intersect.length;
}

export function normalizeBox(boundingBox: [number, number, number, number], inputDimensions: [number, number], scalar = 100): [Point, Point, Point, Point] {
export function fixLegacyClipPath(clipPath: ClipPath): ClipPath {
if (!clipPath)
return;

// if any value is over abs 2, then divide by 100.
// this is a workaround for the old scrypted bug where the path was not normalized.
// this is a temporary workaround until the path is normalized in the UI.
let needNormalize = false;
for (const p of clipPath) {
for (const c of p) {
if (Math.abs(c) >= 2)
needNormalize = true;
}
}

if (!needNormalize)
return clipPath;

return clipPath.map(p => p.map(c => c / 100)) as ClipPath;
}

export function normalizeBoxToClipPath(boundingBox: [number, number, number, number], inputDimensions: [number, number]): [Point, Point, Point, Point] {
let [x, y, width, height] = boundingBox;
let x2 = x + width;
let y2 = y + height;
// the zones are point paths in percentage format
x = x * scalar / inputDimensions[0];
y = y * scalar / inputDimensions[1];
x2 = x2 * scalar / inputDimensions[0];
y2 = y2 * scalar / inputDimensions[1];
x = x / inputDimensions[0];
y = y / inputDimensions[1];
x2 = x2 / inputDimensions[0];
y2 = y2 / inputDimensions[1];
return [[x, y], [x2, y], [x2, y2], [x, y2]];
}

Expand Down
32 changes: 12 additions & 20 deletions plugins/objectdetector/src/smart-motionsensor.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
import sdk, { Camera, EventListenerRegister, MediaObject, MotionSensor, ObjectDetector, ObjectsDetected, Readme, RequestPictureOptions, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, SettingValue, Settings } from "@scrypted/sdk";
import { StorageSetting, StorageSettings } from "@scrypted/sdk/storage-settings";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import { levenshteinDistance } from "./edit-distance";
import type { ObjectDetectionPlugin } from "./main";

export const SMART_MOTIONSENSOR_PREFIX = 'smart-motionsensor-';
export const SMART_OCCUPANCYSENSOR_PREFIX = 'smart-occupancysensor-';

export function createObjectDetectorStorageSetting(): StorageSetting {
return {
key: 'objectDetector',
title: 'Object Detector',
description: 'Select the camera or doorbell that provides smart detection event.',
type: 'device',
deviceFilter: `(type === '${ScryptedDeviceType.Doorbell}' || type === '${ScryptedDeviceType.Camera}') && interfaces.includes('${ScryptedInterface.ObjectDetector}')`,
};
}

export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, Readme, MotionSensor, Camera {
storageSettings = new StorageSettings(this, {
objectDetector: createObjectDetectorStorageSetting(),
objectDetector: {
title: 'Camera',
description: 'Select a camera or doorbell that provides smart detection events.',
type: 'device',
deviceFilter: `(type === '${ScryptedDeviceType.Doorbell}' || type === '${ScryptedDeviceType.Camera}') && interfaces.includes('${ScryptedInterface.ObjectDetector}')`,
},
detections: {
title: 'Detections',
description: 'The detections that will trigger this smart motion sensor.',
Expand Down Expand Up @@ -145,13 +139,13 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
return;
}

resetTrigger() {
resetMotionTimeout() {
clearTimeout(this.timeout);
this.timeout = undefined;
}

trigger() {
this.resetTrigger();
this.resetMotionTimeout();
this.motionDetected = true;
const duration: number = this.storageSettings.values.detectionTimeout;
if (!duration)
Expand All @@ -167,7 +161,7 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
this.detectionListener = undefined;
this.motionListener?.removeListener();
this.motionListener = undefined;
this.resetTrigger();
this.resetMotionTimeout();


const objectDetector: ObjectDetector & MotionSensor & ScryptedDevice = this.storageSettings.values.objectDetector;
Expand All @@ -178,8 +172,6 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
if (!detections?.length)
return;

const console = sdk.deviceManager.getMixinConsole(objectDetector.id, this.nativeId);

this.motionListener = objectDetector.listen({
event: ScryptedInterface.MotionSensor,
watch: true,
Expand Down Expand Up @@ -258,7 +250,7 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R

if (match) {
if (!this.motionDetected)
console.log('Smart Motion Sensor triggered on', match);
this.console.log('Smart Motion Sensor triggered on', match);
if (detected.detectionId)
this.lastPicture = objectDetector.getDetectionInput(detected.detectionId, details.eventId);
this.trigger();
Expand All @@ -278,6 +270,6 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
return `
## Smart Motion Sensor
This Smart Motion Sensor can trigger when a specific type of object (car, person, dog, etc) triggers movement on a camera. The sensor can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires a camera with hardware or software object detection capability.`;
This Smart Motion Sensor can trigger when a specific type of object (vehicle, person, animal, etc) triggers movement on a camera. The sensor can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires a camera with hardware or software object detection capability.`;
}
}
2 changes: 1 addition & 1 deletion plugins/sample-cameraprovider
4 changes: 2 additions & 2 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 68cbe9a

Please sign in to comment.