scrypted-detection-trainer 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.nodejs.js +1 -1
- package/dist/main.nodejs.js.map +1 -1
- package/dist/plugin.zip +0 -0
- package/out/main.nodejs.js +49 -50
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/main.ts +55 -53
package/out/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/main.ts
CHANGED
|
@@ -291,15 +291,15 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
|
|
|
291
291
|
const url = new URL(request.url, 'http://localhost');
|
|
292
292
|
const path = url.pathname.replace(request.rootPath, '');
|
|
293
293
|
|
|
294
|
-
// Serve browse event image via
|
|
294
|
+
// Serve browse event image via getDetectionInput
|
|
295
295
|
if (path === '/api/browse-img' && request.body) {
|
|
296
296
|
const rawBody = request.body as any;
|
|
297
297
|
const body = JSON.parse(typeof rawBody === 'string' ? rawBody : Buffer.isBuffer(rawBody) ? rawBody.toString() : String(rawBody));
|
|
298
|
-
const { cameraId,
|
|
299
|
-
if (!cameraId ||
|
|
298
|
+
const { cameraId, detectionId } = body;
|
|
299
|
+
if (!cameraId || !detectionId) return response.send('Missing params', { code: 400 });
|
|
300
300
|
try {
|
|
301
|
-
const cam = systemManager.getDeviceById(cameraId) as
|
|
302
|
-
const mo = await cam.
|
|
301
|
+
const cam = systemManager.getDeviceById(cameraId) as unknown as ObjectDetector;
|
|
302
|
+
const mo = await cam.getDetectionInput(detectionId);
|
|
303
303
|
const jpeg = await mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
|
|
304
304
|
return response.send(jpeg, { headers: { 'Content-Type': 'image/jpeg', 'Cache-Control': 'max-age=3600' } });
|
|
305
305
|
} catch (e) {
|
|
@@ -349,6 +349,7 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
|
|
|
349
349
|
const params = new URL(request.url, 'http://localhost').searchParams;
|
|
350
350
|
const cameraId = params.get('cameraId');
|
|
351
351
|
const hours = parseInt(params.get('hours') || '24');
|
|
352
|
+
const limit = parseInt(params.get('limit') || '100');
|
|
352
353
|
if (!cameraId) return response.send('Missing cameraId', { code: 400 });
|
|
353
354
|
|
|
354
355
|
try {
|
|
@@ -357,25 +358,35 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
|
|
|
357
358
|
|
|
358
359
|
const endTime = Date.now();
|
|
359
360
|
const startTime = endTime - hours * 3600 * 1000;
|
|
360
|
-
const clips = await cam.getVideoClips({ startTime, endTime });
|
|
361
361
|
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
|
|
362
|
+
// Use getRecordedEvents to get ObjectDetector events with bounding boxes
|
|
363
|
+
const recorded = await cam.getRecordedEvents({ startTime, endTime });
|
|
364
|
+
|
|
365
|
+
const detectionEvents = (recorded || [])
|
|
366
|
+
.filter((e: any) =>
|
|
367
|
+
e.details?.eventInterface === 'ObjectDetector' &&
|
|
368
|
+
e.data?.detections?.length &&
|
|
369
|
+
e.data?.detectionId &&
|
|
370
|
+
e.data?.inputDimensions
|
|
371
|
+
)
|
|
365
372
|
.slice(0, limit)
|
|
366
|
-
.map((
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
373
|
+
.map((e: any) => {
|
|
374
|
+
const ts = e.details?.eventTime || e.data?.timestamp;
|
|
375
|
+
return {
|
|
376
|
+
detectionId: e.data.detectionId,
|
|
377
|
+
timestamp: ts,
|
|
378
|
+
detections: (e.data.detections || []).map((d: any) => ({
|
|
379
|
+
className: d.className,
|
|
380
|
+
score: d.score,
|
|
381
|
+
boundingBox: d.boundingBox,
|
|
382
|
+
})),
|
|
383
|
+
inputDimensions: e.data.inputDimensions,
|
|
384
|
+
cameraId,
|
|
385
|
+
cameraName: cam.name,
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
return response.send(JSON.stringify(detectionEvents), { headers: { 'Content-Type': 'application/json' } });
|
|
379
390
|
} catch (e: any) {
|
|
380
391
|
return response.send(JSON.stringify({ error: e.message }), { headers: { 'Content-Type': 'application/json' }, code: 500 });
|
|
381
392
|
}
|
|
@@ -385,30 +396,29 @@ class DetectionTrainer extends ScryptedDeviceBase implements Settings, HttpReque
|
|
|
385
396
|
if (path === '/api/add-event' && request.body) {
|
|
386
397
|
const rawBody = request.body as any;
|
|
387
398
|
const body = JSON.parse(typeof rawBody === 'string' ? rawBody : Buffer.isBuffer(rawBody) ? rawBody.toString() : String(rawBody));
|
|
388
|
-
const { cameraId, cameraName,
|
|
399
|
+
const { cameraId, cameraName, detectionId, timestamp, detectedClass, score, boundingBox, inputDimensions, label } = body;
|
|
389
400
|
|
|
390
401
|
if (!label || label === 'discard') return response.send(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' } });
|
|
391
402
|
|
|
392
|
-
// Get image via thumbnail
|
|
393
403
|
let jpeg: Buffer | undefined;
|
|
394
404
|
try {
|
|
395
|
-
const cam = systemManager.getDeviceById(cameraId) as
|
|
396
|
-
const mo = await cam.
|
|
405
|
+
const cam = systemManager.getDeviceById(cameraId) as unknown as ObjectDetector;
|
|
406
|
+
const mo = await cam.getDetectionInput(detectionId);
|
|
397
407
|
jpeg = await mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
|
|
398
408
|
} catch (e) {
|
|
399
|
-
this.console.warn(`Could not get
|
|
409
|
+
this.console.warn(`Could not get image for browse event:`, e);
|
|
400
410
|
}
|
|
401
411
|
|
|
402
412
|
if (!jpeg) return response.send(JSON.stringify({ error: 'Could not retrieve image' }), { headers: { 'Content-Type': 'application/json' }, code: 500 });
|
|
403
413
|
|
|
404
414
|
const id = `browse-${timestamp}-${Math.random().toString(36).slice(2, 6)}`;
|
|
405
|
-
// For clips we don't have per-detection bounding boxes — store full frame dimensions
|
|
406
415
|
const record: CaptureRecord = {
|
|
407
416
|
id, cameraId, cameraName, timestamp,
|
|
408
417
|
detectedClass: detectedClass || 'unknown',
|
|
409
|
-
score: 1,
|
|
418
|
+
score: score || 1,
|
|
410
419
|
boundingBox: boundingBox || [0, 0, inputDimensions?.[0] || 1920, inputDimensions?.[1] || 1080],
|
|
411
420
|
inputDimensions: inputDimensions || [1920, 1080],
|
|
421
|
+
detectionId,
|
|
412
422
|
reviewed: true, label,
|
|
413
423
|
};
|
|
414
424
|
this.captures.set(id, record);
|
|
@@ -941,7 +951,7 @@ function loadBrowseImage(i, ev) {
|
|
|
941
951
|
fetch(BASE + '/api/browse-img', {
|
|
942
952
|
method: 'POST',
|
|
943
953
|
headers: { 'Content-Type': 'application/json' },
|
|
944
|
-
body: JSON.stringify({ cameraId: ev.cameraId,
|
|
954
|
+
body: JSON.stringify({ cameraId: ev.cameraId, detectionId: ev.detectionId }),
|
|
945
955
|
})
|
|
946
956
|
.then(r => r.ok ? r.blob() : null)
|
|
947
957
|
.then(blob => {
|
|
@@ -949,28 +959,18 @@ function loadBrowseImage(i, ev) {
|
|
|
949
959
|
const url = URL.createObjectURL(blob);
|
|
950
960
|
const img = new Image();
|
|
951
961
|
img.onload = () => {
|
|
962
|
+
const primary = (ev.detections || [])[0];
|
|
963
|
+
if (!primary?.boundingBox) return;
|
|
952
964
|
imgCache.set('browse-' + i, img);
|
|
965
|
+
// Use drawDetection by temporarily remapping canvas IDs
|
|
953
966
|
const fullCanvas = document.getElementById('bcanvas-full-' + i);
|
|
954
967
|
const cropCanvas = document.getElementById('bcanvas-crop-' + i);
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
const dw = iw * scale, dh = ih * scale;
|
|
962
|
-
ctx.fillStyle = '#111'; ctx.fillRect(0, 0, cw, ch);
|
|
963
|
-
ctx.drawImage(img, (cw-dw)/2, (ch-dh)/2, dw, dh);
|
|
964
|
-
// Label classes
|
|
965
|
-
const labels = (ev.detectionClasses || []).join(', ');
|
|
966
|
-
ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(0, ch-20, cw, 20);
|
|
967
|
-
ctx.fillStyle = '#f90'; ctx.font = 'bold 11px sans-serif';
|
|
968
|
-
ctx.fillText(labels, 4, ch-5);
|
|
969
|
-
fullCanvas.onclick = () => openLightboxImg(img, ev.cameraName, ev.timestamp);
|
|
970
|
-
}
|
|
971
|
-
// Hide crop panel — no bounding box available
|
|
972
|
-
const cropPanel = document.getElementById('bcanvas-crop-' + i)?.closest('.img-panel');
|
|
973
|
-
if (cropPanel) cropPanel.style.display = 'none';
|
|
968
|
+
if (fullCanvas) fullCanvas.id = 'canvas-full-' + i;
|
|
969
|
+
if (cropCanvas) cropCanvas.id = 'canvas-crop-' + i;
|
|
970
|
+
const fakeR = { id: String(i), boundingBox: primary.boundingBox, detectedClass: primary.className, score: primary.score, inputDimensions: ev.inputDimensions };
|
|
971
|
+
drawDetection(img, fakeR);
|
|
972
|
+
if (fullCanvas) { fullCanvas.id = 'bcanvas-full-' + i; fullCanvas.onclick = () => openLightbox(fakeR); }
|
|
973
|
+
if (cropCanvas) { cropCanvas.id = 'bcanvas-crop-' + i; cropCanvas.onclick = () => openLightbox(fakeR); }
|
|
974
974
|
URL.revokeObjectURL(url);
|
|
975
975
|
};
|
|
976
976
|
img.src = url;
|
|
@@ -985,17 +985,19 @@ async function addBrowseEvent(i, label) {
|
|
|
985
985
|
|
|
986
986
|
if (label !== 'discard') {
|
|
987
987
|
try {
|
|
988
|
+
const primary = (ev.detections || [])[0] || {};
|
|
988
989
|
const res = await fetch(BASE + '/api/add-event', {
|
|
989
990
|
method: 'POST',
|
|
990
991
|
headers: { 'Content-Type': 'application/json' },
|
|
991
992
|
body: JSON.stringify({
|
|
992
993
|
cameraId: ev.cameraId,
|
|
993
994
|
cameraName: ev.cameraName,
|
|
994
|
-
|
|
995
|
+
detectionId: ev.detectionId,
|
|
995
996
|
timestamp: ev.timestamp,
|
|
996
|
-
detectedClass:
|
|
997
|
-
|
|
998
|
-
|
|
997
|
+
detectedClass: primary.className || 'unknown',
|
|
998
|
+
score: primary.score || 1,
|
|
999
|
+
boundingBox: primary.boundingBox || null,
|
|
1000
|
+
inputDimensions: ev.inputDimensions || null,
|
|
999
1001
|
label,
|
|
1000
1002
|
}),
|
|
1001
1003
|
});
|