mirror of
https://github.com/koush/scrypted.git
synced 2026-03-20 16:40:24 +00:00
opencv: remove legacy project
This commit is contained in:
4
plugins/opencv/.gitignore
vendored
4
plugins/opencv/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
.DS_Store
|
||||
out/
|
||||
node_modules/
|
||||
dist/
|
||||
@@ -1,4 +0,0 @@
|
||||
.DS_Store
|
||||
out/
|
||||
node_modules/
|
||||
dist/*.map
|
||||
22
plugins/opencv/.vscode/launch.json
vendored
22
plugins/opencv/.vscode/launch.json
vendored
@@ -1,22 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Scrypted Debugger",
|
||||
"address": "${config:scrypted.debugHost}",
|
||||
"port": 10081,
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"preLaunchTask": "scrypted: deploy+debug",
|
||||
"sourceMaps": true,
|
||||
"localRoot": "${workspaceFolder}/out",
|
||||
"remoteRoot": "/plugin/",
|
||||
"type": "pwa-node"
|
||||
}
|
||||
]
|
||||
}
|
||||
3
plugins/opencv/.vscode/settings.json
vendored
3
plugins/opencv/.vscode/settings.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
}
|
||||
20
plugins/opencv/.vscode/tasks.json
vendored
20
plugins/opencv/.vscode/tasks.json
vendored
@@ -1,20 +0,0 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "scrypted: deploy+debug",
|
||||
"type": "shell",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "silent",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": true,
|
||||
"clear": false
|
||||
},
|
||||
"command": "npm run scrypted-vscode-launch ${config:scrypted.debugHost}",
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
# OpenCV Motion detection for VideoCamera devices.
|
||||
|
||||
## npm commands
|
||||
* npm run scrypted-webpack
|
||||
* npm run scrypted-deploy <ipaddress>
|
||||
* npm run scrypted-debug <ipaddress>
|
||||
|
||||
## scrypted distribution via npm
|
||||
1. Ensure package.json is set up properly for publishing on npm.
|
||||
2. npm publish
|
||||
|
||||
## Visual Studio Code configuration
|
||||
|
||||
* If using a remote server, edit [.vscode/settings.json](blob/master/.vscode/settings.json) to specify the IP Address of the Scrypted server.
|
||||
* Launch Scrypted Debugger from the launch menu.
|
||||
2271
plugins/opencv/package-lock.json
generated
2271
plugins/opencv/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"name": "@scrypted/opencv",
|
||||
"version": "0.0.19",
|
||||
"description": "Motion Detection for VideoCameras.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"prepublishOnly": "NODE_ENV=production scrypted-webpack",
|
||||
"prescrypted-vscode-launch": "scrypted-webpack",
|
||||
"scrypted-vscode-launch": "scrypted-deploy-debug",
|
||||
"scrypted-deploy-debug": "scrypted-deploy-debug",
|
||||
"scrypted-debug": "scrypted-debug",
|
||||
"scrypted-deploy": "scrypted-deploy",
|
||||
"scrypted-readme": "scrypted-readme",
|
||||
"scrypted-package-json": "scrypted-package-json",
|
||||
"scrypted-webpack": "scrypted-webpack"
|
||||
},
|
||||
"keywords": [
|
||||
"scrypted",
|
||||
"plugin",
|
||||
"opencv",
|
||||
"motion",
|
||||
"object",
|
||||
"detect",
|
||||
"detection"
|
||||
],
|
||||
"scrypted": {
|
||||
"name": "OpenCV Motion Detection",
|
||||
"singleInstance": true,
|
||||
"type": "API",
|
||||
"interfaces": [
|
||||
"MixinProvider"
|
||||
],
|
||||
"realfs": true
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@koush/opencv4nodejs": "^5.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.17.11"
|
||||
}
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
|
||||
import { MixinProvider, ScryptedDeviceType, ScryptedInterface, VideoCamera, MediaStreamOptions, Settings, Setting, ScryptedMimeTypes, FFMpegInput, MotionSensor, ScryptedDevice } from '@scrypted/sdk';
|
||||
import sdk from '@scrypted/sdk';
|
||||
import { once } from 'events';
|
||||
import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
|
||||
import { FFMpegRebroadcastSession, startRebroadcastSession } from '@scrypted/common/src/ffmpeg-rebroadcast';
|
||||
import { StreamChunk, createRawVideoParser } from '@scrypted/common/src/stream-parser';
|
||||
import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
|
||||
import cv, { Mat, Size } from "@koush/opencv4nodejs";
|
||||
|
||||
const { mediaManager, log, systemManager, deviceManager } = sdk;
|
||||
|
||||
const defaultInterval = 10;
|
||||
const defaultArea = 2000;
|
||||
const defaultThreshold = 25;
|
||||
|
||||
async function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
class OpenCVMixin extends SettingsMixinDeviceBase<VideoCamera> implements MotionSensor, Settings {
|
||||
area: number;
|
||||
threshold: number;
|
||||
released = false;
|
||||
sessionPromise: Promise<FFMpegRebroadcastSession>;
|
||||
|
||||
constructor(mixinDevice: VideoCamera & Settings, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string) {
|
||||
super(mixinDevice, mixinDeviceState, {
|
||||
providerNativeId,
|
||||
mixinDeviceInterfaces,
|
||||
group: "OpenCV Settings",
|
||||
groupKey: "opencv",
|
||||
});
|
||||
|
||||
this.area = parseInt(localStorage.getItem('area')) || defaultArea;
|
||||
this.threshold = parseInt(localStorage.getItem('threshold')) || defaultThreshold;
|
||||
|
||||
// to prevent noisy startup/reload/shutdown, delay the prebuffer starting.
|
||||
this.console.log('session starting in 5 seconds');
|
||||
setTimeout(async () => {
|
||||
while (!this.released) {
|
||||
try {
|
||||
await this.start();
|
||||
this.console.log('shut down gracefully');
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error(this.name, 'session unexpectedly terminated, restarting in 5 seconds', e);
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
async start() {
|
||||
const realDevice = systemManager.getDeviceById<VideoCamera>(this.id);
|
||||
|
||||
let selectedStream: MediaStreamOptions;
|
||||
const motionChannel = this.storage.getItem('motionChannel');
|
||||
if (motionChannel) {
|
||||
const msos = await realDevice.getVideoStreamOptions();
|
||||
selectedStream = msos.find(mso => mso.name === motionChannel);
|
||||
}
|
||||
|
||||
const ffmpegInput = JSON.parse((await mediaManager.convertMediaObjectToBuffer(await realDevice.getVideoStream(selectedStream), ScryptedMimeTypes.FFmpegInput)).toString()) as FFMpegInput;
|
||||
let video = ffmpegInput.mediaStreamOptions?.video;
|
||||
if (!video?.width || !video?.height) {
|
||||
this.console.error("Width and Height were not provided. Defaulting to 1920x1080.");
|
||||
video = {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
};
|
||||
}
|
||||
|
||||
let { width, height } = video;
|
||||
// we'll use an image 1/6 of the dimension in size for motion.
|
||||
// however, opencv also expects that input images are modulo 6.
|
||||
// so make sure both are satisfied.
|
||||
|
||||
if (width > height) {
|
||||
if (width > 318) {
|
||||
height = height / width * 318;
|
||||
width = 318;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (height > 318) {
|
||||
width = width / height * 318;
|
||||
height = 318;
|
||||
}
|
||||
}
|
||||
|
||||
// square em up
|
||||
width = Math.floor(width / 6) * 6;
|
||||
height = Math.floor(height / 6) * 6;
|
||||
|
||||
this.sessionPromise = startRebroadcastSession(ffmpegInput, {
|
||||
console: this.console,
|
||||
parsers: {
|
||||
rawvideo: createRawVideoParser({
|
||||
size: {
|
||||
width,
|
||||
height,
|
||||
},
|
||||
everyNFrames: parseInt(this.storage.getItem('interval')) || 10,
|
||||
}),
|
||||
}
|
||||
});
|
||||
|
||||
const session = await this.sessionPromise;
|
||||
|
||||
let watchdog: NodeJS.Timeout;
|
||||
const restartWatchdog = () => {
|
||||
clearTimeout(watchdog);
|
||||
watchdog = setTimeout(() => {
|
||||
this.console.error('watchdog for raw video parser timed out... killing ffmpeg session');
|
||||
session.kill();
|
||||
}, 60000);
|
||||
}
|
||||
session.events.on('rawvideo-data', restartWatchdog);
|
||||
|
||||
session.events.once('killed', () => {
|
||||
clearTimeout(watchdog);
|
||||
});
|
||||
|
||||
restartWatchdog();
|
||||
|
||||
try {
|
||||
await this.startWrapped(session);
|
||||
}
|
||||
finally {
|
||||
session.kill();
|
||||
}
|
||||
}
|
||||
|
||||
async startWrapped(session: FFMpegRebroadcastSession) {
|
||||
let previousFrame: Mat;
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
const triggerMotion = () => {
|
||||
this.motionDetected = true;
|
||||
clearTimeout(timeout);
|
||||
setTimeout(() => this.motionDetected = false, 10000);
|
||||
}
|
||||
|
||||
this.motionDetected = false;
|
||||
|
||||
while (!this.released) {
|
||||
if (this.motionDetected) {
|
||||
// during motion just eat the frames.
|
||||
previousFrame = undefined;
|
||||
await sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
const args = await once(session.events, 'rawvideo-data');
|
||||
const chunk: StreamChunk = args[0];
|
||||
// should be one chunk from the parser, but let's not assume that.
|
||||
const raw = chunk.chunks.length === 1 ? chunk.chunks[0] : Buffer.concat(chunk.chunks);
|
||||
const mat = new Mat(raw, chunk.height * 3 / 2, chunk.width, cv.CV_8U);
|
||||
|
||||
const gray = await mat.cvtColorAsync(cv.COLOR_YUV420p2GRAY);
|
||||
const curFrame = await gray.gaussianBlurAsync(new Size(21, 21), 0);
|
||||
|
||||
try {
|
||||
if (!previousFrame) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const frameDelta = previousFrame.absdiff(curFrame);
|
||||
const thresh = await frameDelta.thresholdAsync(this.threshold, 255, cv.THRESH_BINARY);
|
||||
const dilated = await thresh.dilateAsync(cv.getStructuringElement(cv.MORPH_ELLIPSE, new cv.Size(4, 4)), new cv.Point2(-1, -1), 2)
|
||||
const contours = await dilated.findContoursAsync(cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);
|
||||
const filteredContours = contours.filter(cnt => cnt.area > this.area).map(cnt => cnt.area);
|
||||
if (filteredContours.length) {
|
||||
this.console.log('motion triggered by area(s)', filteredContours.join(','));
|
||||
triggerMotion();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
previousFrame = curFrame;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getMixinSettings(): Promise<Setting[]> {
|
||||
const settings: Setting[] = [];
|
||||
const realDevice = systemManager.getDeviceById<VideoCamera>(this.id);
|
||||
|
||||
let msos: MediaStreamOptions[] = [];
|
||||
try {
|
||||
msos = await realDevice.getVideoStreamOptions();
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
|
||||
|
||||
if (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',
|
||||
})
|
||||
}
|
||||
|
||||
if (msos?.length) {
|
||||
settings.push({
|
||||
title: 'Motion Stream',
|
||||
key: 'motionChannel',
|
||||
value: this.storage.getItem('motionChannel') || msos[0].name,
|
||||
description: 'The stream to use for detecting motion. Using the lowest resolution stream is recommended.',
|
||||
choices: msos.map(mso => mso.name),
|
||||
});
|
||||
}
|
||||
|
||||
settings.push(
|
||||
{
|
||||
title: "Motion Area",
|
||||
description: "The area size required to trigger motion. Higher values (larger areas) are less sensitive.",
|
||||
value: this.storage.getItem('area') || defaultArea.toString(),
|
||||
key: 'area',
|
||||
placeholder: defaultArea.toString(),
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
title: "Motion Threshold",
|
||||
description: "The threshold required to consider a pixel changed. Higher values (larger changes) are less sensitive.",
|
||||
value: this.storage.getItem('threshold') || defaultThreshold.toString(),
|
||||
key: 'threshold',
|
||||
placeholder: defaultThreshold.toString(),
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
title: "Frame Analysis Interval",
|
||||
description: "The number of frames to wait between motion analysis.",
|
||||
value: this.storage.getItem('interval') || defaultInterval.toString(),
|
||||
key: 'interval',
|
||||
placeholder: defaultInterval.toString(),
|
||||
type: 'number',
|
||||
},
|
||||
);
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
async putMixinSetting(key: string, value: string | number | boolean): Promise<void> {
|
||||
this.storage.setItem(key, value.toString());
|
||||
if (key === 'area')
|
||||
this.area = parseInt(value.toString()) || defaultArea;
|
||||
if (key === 'threshold')
|
||||
this.threshold = parseInt(value.toString()) || defaultThreshold;
|
||||
|
||||
if (key === 'motionChannel') {
|
||||
this.sessionPromise?.then(session => session.kill());
|
||||
}
|
||||
}
|
||||
|
||||
release() {
|
||||
this.released = true;
|
||||
}
|
||||
}
|
||||
|
||||
class OpenCVProvider extends AutoenableMixinProvider implements MixinProvider {
|
||||
constructor(nativeId?: string) {
|
||||
super(nativeId);
|
||||
|
||||
// trigger opencv.
|
||||
for (const id of Object.keys(systemManager.getSystemState())) {
|
||||
const device = systemManager.getDeviceById<VideoCamera>(id);
|
||||
if (!device.mixins?.includes(this.id))
|
||||
continue;
|
||||
device.getVideoStreamOptions();
|
||||
}
|
||||
}
|
||||
|
||||
async shouldEnableMixin(device: ScryptedDevice) {
|
||||
return !device.interfaces.includes(ScryptedInterface.MotionSensor);
|
||||
}
|
||||
|
||||
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
|
||||
if (!interfaces.includes(ScryptedInterface.VideoCamera))
|
||||
return null;
|
||||
return [ScryptedInterface.MotionSensor, ScryptedInterface.Settings];
|
||||
}
|
||||
|
||||
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }) {
|
||||
this.setHasEnabledMixin(mixinDeviceState.id);
|
||||
return new OpenCVMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.nativeId);
|
||||
}
|
||||
async releaseMixin(id: string, mixinDevice: any) {
|
||||
mixinDevice.release();
|
||||
}
|
||||
}
|
||||
|
||||
export default new OpenCVProvider();
|
||||
Reference in New Issue
Block a user