@camstack/addon-post-analysis 1.1.37 → 1.1.39
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/{dist-DPBet4IQ.mjs → dist-CO07v2sR.mjs} +56 -5
- package/dist/{dist-CThBV9dq.js → dist-DUcGHr9E.js} +56 -5
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-CDbDhtGa.js → node-B7HfyyIy.js} +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-oxNx9gzC.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DrIydlxk.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B4AjHkeF.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-Do2pU2J8.mjs → hostInit-DnOjDm_e.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +864 -95
- package/dist/pipeline-analytics/index.mjs +862 -95
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DdC1GJfs.mjs +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as hydrateSchema, D as object, E as number, O as string, S as createEvent, T as boolean, _ as videoclipsCapability, a as OpsLogEntrySchema, b as BaseAddon, c as buildEventKindDescriptor, d as faceGalleryCapability, g as subKindsOf, h as plateGalleryCapability, i as MACRO_LABELS, k as EventCategory, l as cosineSimilarity, m as pipelineAnalyticsCapability, n as EVENT_KIND_BY_CAP, o as addonWidgetsSourceCapability, p as nodePin, r as EVENT_PAD_MS, s as audioMetricsCapability, t as DEFAULT_EVENT_COLOR, v as zoneAnalyticsCapability, w as array, x as DeviceType, y as errMsg } from "../dist-
|
|
1
|
+
import { C as hydrateSchema, D as object, E as number, O as string, S as createEvent, T as boolean, _ as videoclipsCapability, a as OpsLogEntrySchema, b as BaseAddon, c as buildEventKindDescriptor, d as faceGalleryCapability, g as subKindsOf, h as plateGalleryCapability, i as MACRO_LABELS, k as EventCategory, l as cosineSimilarity, m as pipelineAnalyticsCapability, n as EVENT_KIND_BY_CAP, o as addonWidgetsSourceCapability, p as nodePin, r as EVENT_PAD_MS, s as audioMetricsCapability, t as DEFAULT_EVENT_COLOR, v as zoneAnalyticsCapability, w as array, x as DeviceType, y as errMsg } from "../dist-CO07v2sR.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import sharp from "sharp";
|
|
4
4
|
//#region src/pipeline-analytics/videoclips-provider.ts
|
|
@@ -1120,8 +1120,35 @@ var DEFAULT_TRACKER_CONFIG = {
|
|
|
1120
1120
|
rescueIouThreshold: .1,
|
|
1121
1121
|
rescueCentroidFactor: .75,
|
|
1122
1122
|
resurrectionWindowMs: 8e3,
|
|
1123
|
-
stationarySpeedPx: 2
|
|
1123
|
+
stationarySpeedPx: 2,
|
|
1124
|
+
dedupEnabled: true,
|
|
1125
|
+
dedupSpawnIou: .6,
|
|
1126
|
+
dedupMergeIou: .6,
|
|
1127
|
+
dedupMergeFrames: 5,
|
|
1128
|
+
personAnimalDedup: true,
|
|
1129
|
+
animalOverPersonMaxScore: .6,
|
|
1130
|
+
animalOverPersonIou: .2,
|
|
1131
|
+
classVotingEnabled: true,
|
|
1132
|
+
classVoteMinFraction: .5,
|
|
1133
|
+
perClassMinScoreEnabled: true,
|
|
1134
|
+
classMinScores: {
|
|
1135
|
+
animal: .45,
|
|
1136
|
+
vehicle: .45
|
|
1137
|
+
},
|
|
1138
|
+
confirmBypassEnabled: false,
|
|
1139
|
+
confirmBypassScore: .9,
|
|
1140
|
+
byteTrackEnabled: true,
|
|
1141
|
+
byteTrackHighThreshold: .4,
|
|
1142
|
+
byteTrackLowIouThreshold: .2,
|
|
1143
|
+
minSpawnAreaEnabled: true,
|
|
1144
|
+
classSpawnAreaFracs: { person: 5e-4 },
|
|
1145
|
+
classGroupAssoc: true
|
|
1124
1146
|
};
|
|
1147
|
+
/** Macro-classes the duplicate resolver may collapse across (people in
|
|
1148
|
+
* non-upright poses misdetected as animals). Any other cross-class pair is
|
|
1149
|
+
* NEVER a duplicate. */
|
|
1150
|
+
var PERSON_CLASS = "person";
|
|
1151
|
+
var ANIMAL_CLASS = "animal";
|
|
1125
1152
|
var MAX_PATH_LENGTH = 300;
|
|
1126
1153
|
function clamp(value, min, max) {
|
|
1127
1154
|
return Math.max(min, Math.min(max, value));
|
|
@@ -1144,16 +1171,127 @@ function containment(inner, outer) {
|
|
|
1144
1171
|
const innerArea = inner.w * inner.h;
|
|
1145
1172
|
return innerArea > 0 ? iw * ih / innerArea : 0;
|
|
1146
1173
|
}
|
|
1147
|
-
var SortTracker = class {
|
|
1174
|
+
var SortTracker = class SortTracker {
|
|
1148
1175
|
config;
|
|
1149
1176
|
tracks = [];
|
|
1150
1177
|
lostTracks = [];
|
|
1178
|
+
/**
|
|
1179
|
+
* Consecutive-frame overlap streak per unordered live-track pair
|
|
1180
|
+
* (`idA|idB`, ids sorted). Drives the sustained-overlap merge: a pair is
|
|
1181
|
+
* merged only once its streak reaches `dedupMergeFrames`. Entries reset the
|
|
1182
|
+
* moment the pair stops overlapping and are pruned when either track dies.
|
|
1183
|
+
*/
|
|
1184
|
+
dupStreaks = /* @__PURE__ */ new Map();
|
|
1151
1185
|
constructor(config = {}) {
|
|
1152
1186
|
this.config = {
|
|
1153
1187
|
...DEFAULT_TRACKER_CONFIG,
|
|
1154
1188
|
...config
|
|
1155
1189
|
};
|
|
1156
1190
|
}
|
|
1191
|
+
/** Whether two macro-classes may be collapsed as the SAME subject. Same class
|
|
1192
|
+
* always; the {person,animal} pair only when `personAnimalDedup` is on. */
|
|
1193
|
+
dedupCompatible(a, b) {
|
|
1194
|
+
if (a === b) return true;
|
|
1195
|
+
if (!this.config.personAnimalDedup) return false;
|
|
1196
|
+
return a === PERSON_CLASS && b === ANIMAL_CLASS || a === ANIMAL_CLASS && b === PERSON_CLASS;
|
|
1197
|
+
}
|
|
1198
|
+
/** Unordered, stable key for a track pair (UUIDs never contain `|`). */
|
|
1199
|
+
static pairKey(a, b) {
|
|
1200
|
+
return a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
1201
|
+
}
|
|
1202
|
+
/** Association-gating group of a class. When `classGroupAssoc` is on the
|
|
1203
|
+
* {person,animal} pair collapses to one group so a per-frame class flip
|
|
1204
|
+
* (crouch person → animal) re-matches the existing track instead of spawning a
|
|
1205
|
+
* concurrent id. Every other class is its own group. */
|
|
1206
|
+
assocGroup(cls) {
|
|
1207
|
+
if (this.config.classGroupAssoc && (cls === PERSON_CLASS || cls === ANIMAL_CLASS)) return `${PERSON_CLASS}|${ANIMAL_CLASS}`;
|
|
1208
|
+
return cls;
|
|
1209
|
+
}
|
|
1210
|
+
/** Whether a detection may associate to a track under the current class gate:
|
|
1211
|
+
* exact class when `classGating` is off (no gate at all) — matching the legacy
|
|
1212
|
+
* contract — otherwise same association group (group == exact class unless
|
|
1213
|
+
* `classGroupAssoc` widened it to {person,animal}). */
|
|
1214
|
+
associable(trackClass, detClass) {
|
|
1215
|
+
if (!this.config.classGating) return true;
|
|
1216
|
+
return this.assocGroup(trackClass) === this.assocGroup(detClass);
|
|
1217
|
+
}
|
|
1218
|
+
/** Whether a spawn candidate clears its per-class minimum bbox-area floor
|
|
1219
|
+
* (fraction of frame area). Ungated when disabled, when the class has no floor,
|
|
1220
|
+
* or when the caller supplied no frame dimensions (fraction is uncomputable). */
|
|
1221
|
+
meetsSpawnAreaFloor(det, frameArea) {
|
|
1222
|
+
if (!this.config.minSpawnAreaEnabled) return true;
|
|
1223
|
+
const floor = this.config.classSpawnAreaFracs[det.class] ?? 0;
|
|
1224
|
+
if (floor <= 0 || frameArea <= 0) return true;
|
|
1225
|
+
return det.bbox.w * det.bbox.h / frameArea >= floor;
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* True when spawning a fresh track for `det` would duplicate an already-live
|
|
1229
|
+
* track: (a) it overlaps a concurrent compatible-class track by
|
|
1230
|
+
* `dedupSpawnIou`, or (b) it is a low-confidence `animal` sitting on a
|
|
1231
|
+
* concurrent `person` track (person-in-odd-pose false animal). `live` is the
|
|
1232
|
+
* set of tracks already surviving this frame (matched, coasting, resurrected,
|
|
1233
|
+
* and earlier same-frame spawns).
|
|
1234
|
+
*/
|
|
1235
|
+
isDuplicateSpawn(det, live) {
|
|
1236
|
+
for (const t of live) if (this.dedupCompatible(t.class, det.class) && iou$2(det.bbox, t.bbox) >= this.config.dedupSpawnIou) return true;
|
|
1237
|
+
if (this.config.personAnimalDedup && det.class === ANIMAL_CLASS && det.score < this.config.animalOverPersonMaxScore) {
|
|
1238
|
+
for (const t of live) if (t.class === PERSON_CLASS && iou$2(det.bbox, t.bbox) >= this.config.animalOverPersonIou) return true;
|
|
1239
|
+
}
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
/** The lower-importance track of a duplicate pair — the one to drop. A
|
|
1243
|
+
* `person` always beats a cross-class `animal`; otherwise more hits wins,
|
|
1244
|
+
* ties break to the older track, then the higher score. */
|
|
1245
|
+
static duplicateLoser(a, b) {
|
|
1246
|
+
if (a.class !== b.class) {
|
|
1247
|
+
if (a.class === PERSON_CLASS && b.class === ANIMAL_CLASS) return b;
|
|
1248
|
+
if (b.class === PERSON_CLASS && a.class === ANIMAL_CLASS) return a;
|
|
1249
|
+
}
|
|
1250
|
+
if (a.hits !== b.hits) return a.hits > b.hits ? b : a;
|
|
1251
|
+
if (a.firstSeen !== b.firstSeen) return a.firstSeen < b.firstSeen ? b : a;
|
|
1252
|
+
return a.score >= b.score ? b : a;
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Merge concurrent duplicate tracks. Two live compatible-class tracks that
|
|
1256
|
+
* stay overlapped (≥ `dedupMergeIou`) for `dedupMergeFrames` consecutive
|
|
1257
|
+
* frames are collapsed: the lower-importance one is dropped (NOT graveyarded
|
|
1258
|
+
* — a confirmed duplicate must not resurrect). This is the fix for a subject
|
|
1259
|
+
* the detector double-fires (two boxes each frame, each feeding its OWN
|
|
1260
|
+
* track, so neither is ever "unmatched" for spawn-suppression to catch) and
|
|
1261
|
+
* for a person misdetected as `animal` alongside the real person track.
|
|
1262
|
+
*/
|
|
1263
|
+
resolveDuplicates() {
|
|
1264
|
+
if (!this.config.dedupEnabled) return;
|
|
1265
|
+
const live = this.tracks;
|
|
1266
|
+
const liveIds = new Set(live.map((t) => t.id));
|
|
1267
|
+
for (const key of this.dupStreaks.keys()) {
|
|
1268
|
+
const [a, b] = key.split("|");
|
|
1269
|
+
if (a === void 0 || b === void 0 || !liveIds.has(a) || !liveIds.has(b)) this.dupStreaks.delete(key);
|
|
1270
|
+
}
|
|
1271
|
+
const dropIds = /* @__PURE__ */ new Set();
|
|
1272
|
+
for (let i = 0; i < live.length; i++) for (let j = i + 1; j < live.length; j++) {
|
|
1273
|
+
const a = live[i];
|
|
1274
|
+
const b = live[j];
|
|
1275
|
+
if (dropIds.has(a.id) || dropIds.has(b.id)) continue;
|
|
1276
|
+
if (!this.dedupCompatible(a.class, b.class)) continue;
|
|
1277
|
+
const key = SortTracker.pairKey(a.id, b.id);
|
|
1278
|
+
if (iou$2(a.bbox, b.bbox) < this.config.dedupMergeIou) {
|
|
1279
|
+
this.dupStreaks.delete(key);
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
const streak = (this.dupStreaks.get(key) ?? 0) + 1;
|
|
1283
|
+
if (streak >= this.config.dedupMergeFrames) {
|
|
1284
|
+
dropIds.add(SortTracker.duplicateLoser(a, b).id);
|
|
1285
|
+
this.dupStreaks.delete(key);
|
|
1286
|
+
} else this.dupStreaks.set(key, streak);
|
|
1287
|
+
}
|
|
1288
|
+
if (dropIds.size === 0) return;
|
|
1289
|
+
this.tracks = this.tracks.filter((t) => !dropIds.has(t.id));
|
|
1290
|
+
for (const key of this.dupStreaks.keys()) {
|
|
1291
|
+
const [a, b] = key.split("|");
|
|
1292
|
+
if (a !== void 0 && dropIds.has(a) || b !== void 0 && dropIds.has(b)) this.dupStreaks.delete(key);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1157
1295
|
/** Where a track is expected this frame — extrapolated by velocity while
|
|
1158
1296
|
* coasting (predictiveCoasting), else its last known bbox. A stationary
|
|
1159
1297
|
* track (speed < stationarySpeedPx) is frozen so bbox jitter can't walk the
|
|
@@ -1174,13 +1312,16 @@ var SortTracker = class {
|
|
|
1174
1312
|
};
|
|
1175
1313
|
}
|
|
1176
1314
|
/**
|
|
1177
|
-
* Loose same-
|
|
1315
|
+
* Loose same-group gate used by the rescue and resurrection passes: accept
|
|
1178
1316
|
* when the detection overlaps the track's LAST-KNOWN bbox by `rescueIou`, or
|
|
1179
|
-
* its centroid is within `rescueCentroidFactor × bbox-diagonal`. Always
|
|
1180
|
-
* gated (a rescue must never cross
|
|
1317
|
+
* its centroid is within `rescueCentroidFactor × bbox-diagonal`. Always group-
|
|
1318
|
+
* gated (a rescue must never cross association groups) independent of
|
|
1319
|
+
* `classGating`, so a coasting person track can be rescued by its own
|
|
1320
|
+
* crouch→animal flip when `classGroupAssoc` is on, but never by an unrelated
|
|
1321
|
+
* class.
|
|
1181
1322
|
*/
|
|
1182
1323
|
looseMatch(track, det) {
|
|
1183
|
-
if (track.class !== det.class) return false;
|
|
1324
|
+
if (this.assocGroup(track.class) !== this.assocGroup(det.class)) return false;
|
|
1184
1325
|
if (iou$2(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return true;
|
|
1185
1326
|
const tc = bboxCentroid(track.bbox);
|
|
1186
1327
|
const dc = bboxCentroid(det.bbox);
|
|
@@ -1188,29 +1329,65 @@ var SortTracker = class {
|
|
|
1188
1329
|
const diag = Math.hypot(track.bbox.w, track.bbox.h);
|
|
1189
1330
|
return dist <= this.config.rescueCentroidFactor * diag;
|
|
1190
1331
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1332
|
+
/** Fold a matched detection's class + score into a track's lifetime vote
|
|
1333
|
+
* tally. A non-positive score still counts as an infinitesimal vote so a
|
|
1334
|
+
* zero-confidence frame contributes to the frame-count tiebreak. */
|
|
1335
|
+
addClassVote(track, det) {
|
|
1336
|
+
const weight = det.score > 0 ? det.score : Number.EPSILON;
|
|
1337
|
+
track.classVotes.set(det.class, (track.classVotes.get(det.class) ?? 0) + weight);
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* The class to REPORT for a track: the confidence-weighted lifetime majority
|
|
1341
|
+
* when voting is on and the winner holds ≥ `classVoteMinFraction` of the total
|
|
1342
|
+
* weight; otherwise the latest-frame class (ambiguous vote or voting off).
|
|
1343
|
+
*/
|
|
1344
|
+
resolveReportedClass(t) {
|
|
1345
|
+
if (!this.config.classVotingEnabled || t.classVotes.size === 0) return t.class;
|
|
1346
|
+
let bestClass = t.class;
|
|
1347
|
+
let bestVote = -1;
|
|
1348
|
+
let total = 0;
|
|
1349
|
+
for (const [cls, v] of t.classVotes) {
|
|
1350
|
+
total += v;
|
|
1351
|
+
if (v > bestVote) {
|
|
1352
|
+
bestVote = v;
|
|
1353
|
+
bestClass = cls;
|
|
1354
|
+
}
|
|
1201
1355
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1356
|
+
if (total <= 0) return t.class;
|
|
1357
|
+
return bestVote / total >= this.config.classVoteMinFraction ? bestClass : t.class;
|
|
1358
|
+
}
|
|
1359
|
+
/** Whether a detection clears its per-class spawn score floor. Only gates NEW
|
|
1360
|
+
* spawns — an established track still matches below its floor. */
|
|
1361
|
+
meetsClassScoreFloor(det) {
|
|
1362
|
+
if (!this.config.perClassMinScoreEnabled) return true;
|
|
1363
|
+
const floor = this.config.classMinScores[det.class] ?? 0;
|
|
1364
|
+
return det.score >= floor;
|
|
1365
|
+
}
|
|
1366
|
+
/** Whether a track is confirmed for EMISSION: it reached `minHits`, or (opt-in)
|
|
1367
|
+
* a single very-high-confidence detection bypassed the hit gate (fast car). */
|
|
1368
|
+
isConfirmedForEmit(t) {
|
|
1369
|
+
if (t.hits >= this.config.minHits) return true;
|
|
1370
|
+
return this.config.confirmBypassEnabled && t.score >= this.config.confirmBypassScore;
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* One greedy IoU association pass over a candidate detection subset against the
|
|
1374
|
+
* currently-UNMATCHED tracks, on each track's PREDICTED box. Highest-IoU pairs
|
|
1375
|
+
* bind first; a track/detection binds at most once. Shared by the high-score
|
|
1376
|
+
* first stage and the low-score second stage (different candidate sets + IoU
|
|
1377
|
+
* gates) so the two-tier ByteTrack association stays a single implementation.
|
|
1378
|
+
* Mutates `used` / `matchedTracks` / `matched` in place.
|
|
1379
|
+
*/
|
|
1380
|
+
greedyAssociate(detections, candidateIdxs, iouThreshold, used, matchedTracks, matched) {
|
|
1206
1381
|
const pairs = [];
|
|
1207
1382
|
for (const track of this.tracks) {
|
|
1383
|
+
if (matchedTracks.has(track)) continue;
|
|
1208
1384
|
const pbox = this.predicted(track);
|
|
1209
|
-
for (
|
|
1385
|
+
for (const di of candidateIdxs) {
|
|
1386
|
+
if (used.has(di)) continue;
|
|
1210
1387
|
const det = detections[di];
|
|
1211
|
-
if (this.
|
|
1388
|
+
if (!this.associable(track.class, det.class)) continue;
|
|
1212
1389
|
const score = iou$2(pbox, det.bbox);
|
|
1213
|
-
if (score >=
|
|
1390
|
+
if (score >= iouThreshold) pairs.push({
|
|
1214
1391
|
track,
|
|
1215
1392
|
detIdx: di,
|
|
1216
1393
|
score
|
|
@@ -1224,11 +1401,34 @@ var SortTracker = class {
|
|
|
1224
1401
|
matchedTracks.add(pair.track);
|
|
1225
1402
|
used.add(pair.detIdx);
|
|
1226
1403
|
}
|
|
1404
|
+
}
|
|
1405
|
+
update(detections, timestamp, frameContext) {
|
|
1406
|
+
const frameArea = frameContext ? frameContext.frameWidth * frameContext.frameHeight : 0;
|
|
1407
|
+
if (this.config.maxTrackLifetimeMs > 0) {
|
|
1408
|
+
const alive = [];
|
|
1409
|
+
for (const track of this.tracks) if (timestamp - track.firstSeen > this.config.maxTrackLifetimeMs) {
|
|
1410
|
+
track.lost = true;
|
|
1411
|
+
track.lostAt = timestamp;
|
|
1412
|
+
track.resurrectable = false;
|
|
1413
|
+
this.lostTracks.push(track);
|
|
1414
|
+
} else alive.push(track);
|
|
1415
|
+
this.tracks = alive;
|
|
1416
|
+
}
|
|
1417
|
+
this.lostTracks = this.lostTracks.filter((t) => timestamp - t.lostAt <= this.config.resurrectionWindowMs);
|
|
1418
|
+
const used = /* @__PURE__ */ new Set();
|
|
1419
|
+
const matchedTracks = /* @__PURE__ */ new Set();
|
|
1420
|
+
const matched = /* @__PURE__ */ new Map();
|
|
1421
|
+
const highIdxs = [];
|
|
1422
|
+
const lowIdxs = [];
|
|
1423
|
+
for (let di = 0; di < detections.length; di++) if (this.config.byteTrackEnabled && detections[di].score < this.config.byteTrackHighThreshold) lowIdxs.push(di);
|
|
1424
|
+
else highIdxs.push(di);
|
|
1425
|
+
const lowSet = new Set(lowIdxs);
|
|
1426
|
+
this.greedyAssociate(detections, highIdxs, this.config.iouThreshold, used, matchedTracks, matched);
|
|
1227
1427
|
const rescuePairs = [];
|
|
1228
1428
|
for (const track of this.tracks) {
|
|
1229
1429
|
if (matchedTracks.has(track)) continue;
|
|
1230
1430
|
for (let di = 0; di < detections.length; di++) {
|
|
1231
|
-
if (used.has(di)) continue;
|
|
1431
|
+
if (used.has(di) || lowSet.has(di)) continue;
|
|
1232
1432
|
const det = detections[di];
|
|
1233
1433
|
if (!this.looseMatch(track, det)) continue;
|
|
1234
1434
|
rescuePairs.push({
|
|
@@ -1245,6 +1445,7 @@ var SortTracker = class {
|
|
|
1245
1445
|
matchedTracks.add(pair.track);
|
|
1246
1446
|
used.add(pair.detIdx);
|
|
1247
1447
|
}
|
|
1448
|
+
if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, used, matchedTracks, matched);
|
|
1248
1449
|
for (const [track, det] of matched) {
|
|
1249
1450
|
const prevCenter = bboxCentroid({
|
|
1250
1451
|
x: track.bbox.x,
|
|
@@ -1272,6 +1473,7 @@ var SortTracker = class {
|
|
|
1272
1473
|
};
|
|
1273
1474
|
track.path.push(det.bbox);
|
|
1274
1475
|
if (track.path.length > MAX_PATH_LENGTH) track.path.shift();
|
|
1476
|
+
this.addClassVote(track, det);
|
|
1275
1477
|
}
|
|
1276
1478
|
const occluderBoxes = [];
|
|
1277
1479
|
for (const track of matchedTracks) occluderBoxes.push(track.bbox);
|
|
@@ -1321,13 +1523,34 @@ var SortTracker = class {
|
|
|
1321
1523
|
};
|
|
1322
1524
|
best.path.push(det.bbox);
|
|
1323
1525
|
if (best.path.length > MAX_PATH_LENGTH) best.path.shift();
|
|
1526
|
+
this.addClassVote(best, det);
|
|
1324
1527
|
surviving.push(best);
|
|
1325
1528
|
used.add(di);
|
|
1326
1529
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1530
|
+
const unmatchedIdx = [];
|
|
1531
|
+
for (let di = 0; di < detections.length; di++) if (!used.has(di)) unmatchedIdx.push(di);
|
|
1532
|
+
const classRank = (cls) => cls === PERSON_CLASS ? 0 : cls === ANIMAL_CLASS ? 2 : 1;
|
|
1533
|
+
unmatchedIdx.sort((ia, ib) => {
|
|
1534
|
+
const da = detections[ia];
|
|
1535
|
+
const db = detections[ib];
|
|
1536
|
+
const r = classRank(da.class) - classRank(db.class);
|
|
1537
|
+
return r !== 0 ? r : db.score - da.score;
|
|
1538
|
+
});
|
|
1539
|
+
for (const di of unmatchedIdx) {
|
|
1329
1540
|
const det = detections[di];
|
|
1541
|
+
if (!this.meetsClassScoreFloor(det)) {
|
|
1542
|
+
used.add(di);
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
if (!this.meetsSpawnAreaFloor(det, frameArea)) {
|
|
1546
|
+
used.add(di);
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1330
1549
|
if (this.config.occlusionEnabled && occluderBoxes.some((ob) => containment(det.bbox, ob) >= this.config.occlusionContainment)) continue;
|
|
1550
|
+
if (this.config.dedupEnabled && this.isDuplicateSpawn(det, surviving)) {
|
|
1551
|
+
used.add(di);
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1331
1554
|
surviving.push({
|
|
1332
1555
|
id: randomUUID(),
|
|
1333
1556
|
bbox: det.bbox,
|
|
@@ -1345,19 +1568,22 @@ var SortTracker = class {
|
|
|
1345
1568
|
},
|
|
1346
1569
|
lost: false,
|
|
1347
1570
|
lostAt: 0,
|
|
1348
|
-
resurrectable: true
|
|
1571
|
+
resurrectable: true,
|
|
1572
|
+
classVotes: new Map([[det.class, det.score > 0 ? det.score : Number.EPSILON]])
|
|
1349
1573
|
});
|
|
1350
1574
|
}
|
|
1351
1575
|
this.tracks = surviving;
|
|
1352
|
-
|
|
1353
|
-
|
|
1576
|
+
this.resolveDuplicates();
|
|
1577
|
+
return this.tracks.filter((t) => this.isConfirmedForEmit(t)).map((t) => ({
|
|
1578
|
+
class: this.resolveReportedClass(t),
|
|
1354
1579
|
originalClass: t.originalClass,
|
|
1355
1580
|
score: t.score,
|
|
1356
1581
|
bbox: t.bbox,
|
|
1357
1582
|
trackId: t.id,
|
|
1358
1583
|
trackAge: t.hits,
|
|
1359
1584
|
velocity: t.velocity,
|
|
1360
|
-
path: [...t.path]
|
|
1585
|
+
path: [...t.path],
|
|
1586
|
+
matchedThisFrame: t.lastSeen === timestamp
|
|
1361
1587
|
}));
|
|
1362
1588
|
}
|
|
1363
1589
|
/**
|
|
@@ -1379,6 +1605,7 @@ var SortTracker = class {
|
|
|
1379
1605
|
reset() {
|
|
1380
1606
|
this.tracks = [];
|
|
1381
1607
|
this.lostTracks = [];
|
|
1608
|
+
this.dupStreaks.clear();
|
|
1382
1609
|
}
|
|
1383
1610
|
};
|
|
1384
1611
|
//#endregion
|
|
@@ -1938,7 +2165,10 @@ var FrameProcessor = class {
|
|
|
1938
2165
|
wokenEntryIds: []
|
|
1939
2166
|
};
|
|
1940
2167
|
const trackerInput = gate.suppressedIndices.size > 0 ? filteredDetections.filter((_, i) => !gate.suppressedIndices.has(i)) : filteredDetections;
|
|
1941
|
-
const trackedDetections = this.tracker.update(trackerInput, timestamp
|
|
2168
|
+
const trackedDetections = this.tracker.update(trackerInput, timestamp, {
|
|
2169
|
+
frameWidth,
|
|
2170
|
+
frameHeight
|
|
2171
|
+
});
|
|
1942
2172
|
const objectStates = this.stateAnalyzer.analyze(trackedDetections, timestamp);
|
|
1943
2173
|
const rawEvents = this.eventEmitter.emit(trackedDetections, objectStates, [], [], String(this.deviceId));
|
|
1944
2174
|
const zonesByTrack = /* @__PURE__ */ new Map();
|
|
@@ -1967,6 +2197,7 @@ var FrameProcessor = class {
|
|
|
1967
2197
|
bbox: { ...td.bbox },
|
|
1968
2198
|
zones: zonesByTrack.get(td.trackId) ?? [],
|
|
1969
2199
|
state,
|
|
2200
|
+
matchedThisFrame: td.matchedThisFrame !== false,
|
|
1970
2201
|
...label ? { label } : {},
|
|
1971
2202
|
...emb !== void 0 ? {
|
|
1972
2203
|
embedding: emb.embedding,
|
|
@@ -4239,6 +4470,26 @@ var FACE_MEDIA_OWNER_PREFIX = "face-";
|
|
|
4239
4470
|
* `plate-<trackId>`. Used by `deleteByTracks` to derive the plate crop owners
|
|
4240
4471
|
* of a set of tracks without a `trackId` column on media rows. */
|
|
4241
4472
|
var PLATE_MEDIA_OWNER_PREFIX = "plate-";
|
|
4473
|
+
/**
|
|
4474
|
+
* Kinds that hold exactly ONE row per `(ownerKind, ownerId)` — the current
|
|
4475
|
+
* "best"/rolling/first artefact, never a filmstrip. Their row id + blob path are
|
|
4476
|
+
* DETERMINISTIC (`owner:kind`, NO timestamp) and written via an UPSERT
|
|
4477
|
+
* (`store.set`, atomic `INSERT … ON CONFLICT DO UPDATE`), so N concurrent
|
|
4478
|
+
* captures for the same `(track, kind)` all address the SAME row/blob — one can
|
|
4479
|
+
* never become thirteen regardless of interleaving (RC-1). Accumulating kinds
|
|
4480
|
+
* (`snapshot`, event `crop`/`fullFrame*`, `faceCrop`/`plateCrop`) keep their
|
|
4481
|
+
* per-instance timestamped id so the filmstrip / per-event / per-enrollment rows
|
|
4482
|
+
* still accumulate.
|
|
4483
|
+
*/
|
|
4484
|
+
var SINGLE_INSTANCE_KINDS = new Set([
|
|
4485
|
+
"keyFrame",
|
|
4486
|
+
"thumbnail",
|
|
4487
|
+
"firstFrame",
|
|
4488
|
+
"lastFrame"
|
|
4489
|
+
]);
|
|
4490
|
+
function isSingleInstanceKind(kind) {
|
|
4491
|
+
return SINGLE_INSTANCE_KINDS.has(kind);
|
|
4492
|
+
}
|
|
4242
4493
|
var MEDIA_COLUMNS = [
|
|
4243
4494
|
{
|
|
4244
4495
|
name: "id",
|
|
@@ -4290,10 +4541,11 @@ var MEDIA_INDEXES = [{
|
|
|
4290
4541
|
columns: ["deviceId", "timestamp"]
|
|
4291
4542
|
}];
|
|
4292
4543
|
function buildKey(params) {
|
|
4293
|
-
return `${params.ownerKind}:${params.ownerId}:${params.kind}:${params.timestamp}`;
|
|
4544
|
+
return isSingleInstanceKind(params.kind) ? `${params.ownerKind}:${params.ownerId}:${params.kind}` : `${params.ownerKind}:${params.ownerId}:${params.kind}:${params.timestamp}`;
|
|
4294
4545
|
}
|
|
4295
4546
|
function buildPath(params) {
|
|
4296
|
-
|
|
4547
|
+
const base = `pipeline-analytics/${params.deviceId}/${params.ownerKind}/${params.ownerId}`;
|
|
4548
|
+
return isSingleInstanceKind(params.kind) ? `${base}/${params.kind}.jpg` : `${base}/${params.kind}-${params.timestamp}.jpg`;
|
|
4297
4549
|
}
|
|
4298
4550
|
var MediaStore = class {
|
|
4299
4551
|
storage;
|
|
@@ -4316,25 +4568,31 @@ var MediaStore = class {
|
|
|
4316
4568
|
async put(params) {
|
|
4317
4569
|
const key = buildKey(params);
|
|
4318
4570
|
const path = buildPath(params);
|
|
4571
|
+
const record = {
|
|
4572
|
+
deviceId: params.deviceId,
|
|
4573
|
+
ownerKind: params.ownerKind,
|
|
4574
|
+
ownerId: params.ownerId,
|
|
4575
|
+
kind: params.kind,
|
|
4576
|
+
timestamp: params.timestamp,
|
|
4577
|
+
path,
|
|
4578
|
+
sizeBytes: params.data.length
|
|
4579
|
+
};
|
|
4319
4580
|
try {
|
|
4320
4581
|
await this.storage.write({
|
|
4321
4582
|
location: "eventMedia",
|
|
4322
4583
|
relativePath: path,
|
|
4323
4584
|
data: params.data
|
|
4324
4585
|
});
|
|
4325
|
-
await this.store.
|
|
4586
|
+
if (isSingleInstanceKind(params.kind)) await this.store.set.mutate({
|
|
4587
|
+
collection: MEDIA_COLLECTION,
|
|
4588
|
+
key,
|
|
4589
|
+
value: record
|
|
4590
|
+
});
|
|
4591
|
+
else await this.store.insert.mutate({
|
|
4326
4592
|
collection: MEDIA_COLLECTION,
|
|
4327
4593
|
record: {
|
|
4328
4594
|
id: key,
|
|
4329
|
-
data:
|
|
4330
|
-
deviceId: params.deviceId,
|
|
4331
|
-
ownerKind: params.ownerKind,
|
|
4332
|
-
ownerId: params.ownerId,
|
|
4333
|
-
kind: params.kind,
|
|
4334
|
-
timestamp: params.timestamp,
|
|
4335
|
-
path,
|
|
4336
|
-
sizeBytes: params.data.length
|
|
4337
|
-
}
|
|
4595
|
+
data: record
|
|
4338
4596
|
}
|
|
4339
4597
|
});
|
|
4340
4598
|
return key;
|
|
@@ -4353,8 +4611,14 @@ var MediaStore = class {
|
|
|
4353
4611
|
* Overwrite semantics: keep exactly ONE media entry per (ownerKind, ownerId,
|
|
4354
4612
|
* kind). Used for the rolling `lastFrame` and the best `thumbnail` per track —
|
|
4355
4613
|
* each new capture replaces the previous one (blob + index row) rather than
|
|
4356
|
-
* accumulating a filmstrip the way `put` does.
|
|
4357
|
-
*
|
|
4614
|
+
* accumulating a filmstrip the way `put` does.
|
|
4615
|
+
*
|
|
4616
|
+
* These kinds are {@link SINGLE_INSTANCE_KINDS}, so `put` writes them to a
|
|
4617
|
+
* DETERMINISTIC `owner:kind` row/blob via an UPSERT — N concurrent racing
|
|
4618
|
+
* calls therefore all collapse onto the SAME row (RC-1), no query/insert/delete
|
|
4619
|
+
* interleaving can leave survivors. The query+delete pass below is retained
|
|
4620
|
+
* ONLY to reap LEGACY timestamped rows/blobs written before the deterministic
|
|
4621
|
+
* scheme (transition back-compat); once migrated it is a no-op.
|
|
4358
4622
|
*/
|
|
4359
4623
|
async putReplacing(params) {
|
|
4360
4624
|
const existing = await this.store.query.query({
|
|
@@ -6096,6 +6360,25 @@ function squareSafeCropRegion(bbox, frame, padding) {
|
|
|
6096
6360
|
h: Math.round(ch)
|
|
6097
6361
|
};
|
|
6098
6362
|
}
|
|
6363
|
+
/**
|
|
6364
|
+
* The same square-safe 16:9 region as {@link squareSafeCropRegion}, expressed in
|
|
6365
|
+
* NORMALIZED [0,1]×[0,1] coordinates instead of pixels.
|
|
6366
|
+
*
|
|
6367
|
+
* A normalized box maps DIRECTLY onto a native-resolution surface of the SAME
|
|
6368
|
+
* aspect ratio (the native crop path downscales while preserving aspect), so the
|
|
6369
|
+
* region computed from the detection frame's dimensions addresses the exact same
|
|
6370
|
+
* ROI on the runner's retained native frame. Reuses the pixel geometry verbatim
|
|
6371
|
+
* (single source of truth) and divides by the frame dimensions.
|
|
6372
|
+
*/
|
|
6373
|
+
function squareSafeCropRegionNormalized(bbox, frame, padding) {
|
|
6374
|
+
const region = squareSafeCropRegion(bbox, frame, padding);
|
|
6375
|
+
return {
|
|
6376
|
+
x: region.x / frame.W,
|
|
6377
|
+
y: region.y / frame.H,
|
|
6378
|
+
w: region.w / frame.W,
|
|
6379
|
+
h: region.h / frame.H
|
|
6380
|
+
};
|
|
6381
|
+
}
|
|
6099
6382
|
//#endregion
|
|
6100
6383
|
//#region src/shared/frame/box-drawer.ts
|
|
6101
6384
|
var DEFAULT_COLOR = DEFAULT_EVENT_COLOR;
|
|
@@ -6173,10 +6456,20 @@ async function drawBoxedFrame(frameData, frameWidth, frameHeight, boxes, opts =
|
|
|
6173
6456
|
* Small downscaled `thumbnail`s are intentionally left out for now — when we
|
|
6174
6457
|
* reintroduce them they'll be a separate small kind. */
|
|
6175
6458
|
var MEDIA_QUALITY = 88;
|
|
6176
|
-
/** Output dimensions for square-safe 16:9 crops
|
|
6459
|
+
/** Output dimensions for the LOCAL-FALLBACK square-safe 16:9 crops
|
|
6460
|
+
* (crop/faceCrop/plateCrop/thumbnail) cut from the resolved ≤640 frame. */
|
|
6177
6461
|
var CROP_WIDTH = 640;
|
|
6178
6462
|
var CROP_HEIGHT = 360;
|
|
6179
6463
|
var CROP_QUALITY = 80;
|
|
6464
|
+
/**
|
|
6465
|
+
* Native-surface crop width cap for subject crops (`crop`/`thumbnail`/
|
|
6466
|
+
* `faceCrop`/`plateCrop`). ~960px yields a sharp gallery/reel tile AND a
|
|
6467
|
+
* ≥224px classifier input straight from the runner's native surface, WITHOUT
|
|
6468
|
+
* hauling a full 1920px frame per subject (that width is reserved for the
|
|
6469
|
+
* full-frame `keyFrame`). It is an UPPER bound only: a native miss falls back to
|
|
6470
|
+
* the ≤640 local crop, so quality never regresses below today's behaviour.
|
|
6471
|
+
*/
|
|
6472
|
+
var NATIVE_SUBJECT_CROP_MAX_WIDTH = 960;
|
|
6180
6473
|
function caption(className, confidence, label) {
|
|
6181
6474
|
const base = label && label !== className ? `${className} ${label}` : className;
|
|
6182
6475
|
return typeof confidence === "number" ? `${base} ${Math.round(confidence * 100)}%` : base;
|
|
@@ -6283,12 +6576,12 @@ var EventMediaDispatcher = class {
|
|
|
6283
6576
|
});
|
|
6284
6577
|
return empty;
|
|
6285
6578
|
}
|
|
6286
|
-
for (const ev of events) await this.writeEventMedia(deviceId, frameData, fw, fh, ev, input.cropPadding);
|
|
6579
|
+
for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, frameData, fw, fh, ev, input.cropPadding);
|
|
6287
6580
|
for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameData, fw, fh, tf);
|
|
6288
6581
|
const storedSnapshots = [];
|
|
6289
6582
|
const thumbnailTrackIds = [];
|
|
6290
6583
|
for (const sn of snapshots) {
|
|
6291
|
-
const res = await this.writeTrackSnapshot(deviceId, frameData, fw, fh, sn, input.cropPadding);
|
|
6584
|
+
const res = await this.writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn, input.cropPadding);
|
|
6292
6585
|
if (res.storedSnapshot) storedSnapshots.push(res.storedSnapshot);
|
|
6293
6586
|
if (res.thumbnailWritten) thumbnailTrackIds.push(sn.trackId);
|
|
6294
6587
|
}
|
|
@@ -6309,7 +6602,7 @@ var EventMediaDispatcher = class {
|
|
|
6309
6602
|
* failed). `thumbnailWritten` reports whether a best `thumbnail` actually
|
|
6310
6603
|
* landed this frame (#27-A) so the caller can stop forcing retries.
|
|
6311
6604
|
*/
|
|
6312
|
-
async writeTrackSnapshot(deviceId, frameData, fw, fh, sn, cropPadding) {
|
|
6605
|
+
async writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn, cropPadding) {
|
|
6313
6606
|
if (!sn.appendSnapshot && !sn.rollingLastFrame && !sn.bestThumbnail) return {
|
|
6314
6607
|
storedSnapshot: null,
|
|
6315
6608
|
thumbnailWritten: false
|
|
@@ -6350,7 +6643,7 @@ var EventMediaDispatcher = class {
|
|
|
6350
6643
|
if (sn.rollingLastFrame && boxed) await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
|
|
6351
6644
|
let thumbnailWritten = false;
|
|
6352
6645
|
if (sn.bestThumbnail) try {
|
|
6353
|
-
const crop = await this.cropSubjectRegion(frameData, fw, fh, sn.bbox, cropPadding);
|
|
6646
|
+
const crop = await this.cropSubjectRegion(frameHandle, frameData, fw, fh, sn.bbox, cropPadding);
|
|
6354
6647
|
thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, crop);
|
|
6355
6648
|
} catch (err) {
|
|
6356
6649
|
this.deps.logger.warn("event media: track thumbnail crop failed", {
|
|
@@ -6369,12 +6662,38 @@ var EventMediaDispatcher = class {
|
|
|
6369
6662
|
};
|
|
6370
6663
|
}
|
|
6371
6664
|
/**
|
|
6372
|
-
* Clean subject-centered crop of `bbox`
|
|
6373
|
-
*
|
|
6374
|
-
* square-safe 16:9 region around the bbox
|
|
6375
|
-
*
|
|
6665
|
+
* Clean subject-centered crop of `bbox` — the shared output contract of the
|
|
6666
|
+
* object-event `crop` kind, the child `faceCrop`/`plateCrop`, and the track
|
|
6667
|
+
* `thumbnail`: the square-safe 16:9 region around the bbox with NO box drawn.
|
|
6668
|
+
*
|
|
6669
|
+
* NATIVE-FIRST: the region is requested from the runner's retained native
|
|
6670
|
+
* surface (normalized [0,1] coords map directly onto it), downscaled to
|
|
6671
|
+
* {@link NATIVE_SUBJECT_CROP_MAX_WIDTH} — a sharp tile at native detail. On any
|
|
6672
|
+
* miss/error (or a runner without the method) it FALLS BACK to cropping the
|
|
6673
|
+
* resolved ≤640 frame locally (today's behaviour), so quality never regresses.
|
|
6674
|
+
* Both paths run inside the live-handle window opened by `captureForFrame`.
|
|
6675
|
+
*/
|
|
6676
|
+
async cropSubjectRegion(frameHandle, frameData, fw, fh, bbox, cropPadding) {
|
|
6677
|
+
if (this.deps.getNativeCropJpeg) try {
|
|
6678
|
+
const norm = squareSafeCropRegionNormalized(bbox, {
|
|
6679
|
+
W: fw,
|
|
6680
|
+
H: fh
|
|
6681
|
+
}, cropPadding);
|
|
6682
|
+
const native = await this.deps.getNativeCropJpeg(frameHandle, norm, NATIVE_SUBJECT_CROP_MAX_WIDTH);
|
|
6683
|
+
if (native) return native;
|
|
6684
|
+
} catch (err) {
|
|
6685
|
+
this.deps.logger.debug("event media: native subject crop failed — local fallback", { meta: {
|
|
6686
|
+
shmId: frameHandle.shmId,
|
|
6687
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6688
|
+
} });
|
|
6689
|
+
}
|
|
6690
|
+
return this.cropSubjectRegionLocal(frameData, fw, fh, bbox, cropPadding);
|
|
6691
|
+
}
|
|
6692
|
+
/**
|
|
6693
|
+
* Local-fallback square-safe 16:9 crop out of the resolved ≤640 frame, resized
|
|
6694
|
+
* to 640×360, JPEG q80. Used when the native surface is unavailable/missed.
|
|
6376
6695
|
*/
|
|
6377
|
-
async
|
|
6696
|
+
async cropSubjectRegionLocal(frameData, fw, fh, bbox, cropPadding) {
|
|
6378
6697
|
const region = squareSafeCropRegion(bbox, {
|
|
6379
6698
|
W: fw,
|
|
6380
6699
|
H: fh
|
|
@@ -6417,13 +6736,13 @@ var EventMediaDispatcher = class {
|
|
|
6417
6736
|
return false;
|
|
6418
6737
|
}
|
|
6419
6738
|
}
|
|
6420
|
-
async writeEventMedia(deviceId, frameData, fw, fh, ev, cropPadding) {
|
|
6739
|
+
async writeEventMedia(deviceId, frameHandle, frameData, fw, fh, ev, cropPadding) {
|
|
6421
6740
|
const box = {
|
|
6422
6741
|
...ev.bbox,
|
|
6423
6742
|
label: caption(ev.className, ev.confidence, ev.label)
|
|
6424
6743
|
};
|
|
6425
6744
|
try {
|
|
6426
|
-
const crop = await this.cropSubjectRegion(frameData, fw, fh, ev.bbox, cropPadding);
|
|
6745
|
+
const crop = await this.cropSubjectRegion(frameHandle, frameData, fw, fh, ev.bbox, cropPadding);
|
|
6427
6746
|
await this.deps.mediaStore.put({
|
|
6428
6747
|
deviceId,
|
|
6429
6748
|
ownerKind: "event",
|
|
@@ -6483,24 +6802,7 @@ var EventMediaDispatcher = class {
|
|
|
6483
6802
|
});
|
|
6484
6803
|
}
|
|
6485
6804
|
if (ev.childCrops) for (const child of ev.childCrops) try {
|
|
6486
|
-
const
|
|
6487
|
-
W: fw,
|
|
6488
|
-
H: fh
|
|
6489
|
-
}, cropPadding);
|
|
6490
|
-
const childLeft = Math.max(0, Math.min(childRegion.x, fw - 1));
|
|
6491
|
-
const childTop = Math.max(0, Math.min(childRegion.y, fh - 1));
|
|
6492
|
-
const childWidth = Math.max(1, Math.min(childRegion.w, fw - childLeft));
|
|
6493
|
-
const childHeight = Math.max(1, Math.min(childRegion.h, fh - childTop));
|
|
6494
|
-
const childCropData = await sharp(frameData, { raw: {
|
|
6495
|
-
width: fw,
|
|
6496
|
-
height: fh,
|
|
6497
|
-
channels: 3
|
|
6498
|
-
} }).extract({
|
|
6499
|
-
left: childLeft,
|
|
6500
|
-
top: childTop,
|
|
6501
|
-
width: childWidth,
|
|
6502
|
-
height: childHeight
|
|
6503
|
-
}).resize(CROP_WIDTH, CROP_HEIGHT).jpeg({ quality: CROP_QUALITY }).toBuffer();
|
|
6805
|
+
const childCropData = await this.cropSubjectRegion(frameHandle, frameData, fw, fh, child.bbox, cropPadding);
|
|
6504
6806
|
await this.deps.mediaStore.put({
|
|
6505
6807
|
deviceId,
|
|
6506
6808
|
ownerKind: "event",
|
|
@@ -7661,11 +7963,89 @@ var TrackingSettingsSchema = object({
|
|
|
7661
7963
|
/** Speed (px/frame) below which a track's prediction is frozen (stationary
|
|
7662
7964
|
* jitter can't drift the box off a sitting object). */
|
|
7663
7965
|
stationarySpeedPx: number().min(0).default(2),
|
|
7966
|
+
/** Suppress + merge concurrent duplicate tracks (one subject the detector
|
|
7967
|
+
* double-fires, or a person misdetected as `animal`, otherwise becomes two
|
|
7968
|
+
* time-overlapping tracks that never re-associate — each firing its own
|
|
7969
|
+
* event). Off = legacy behaviour. */
|
|
7970
|
+
dedupEnabled: boolean().default(true),
|
|
7971
|
+
/** Envelope IoU at/above which a NEW spawn is treated as a duplicate of a
|
|
7972
|
+
* concurrent compatible-class track and suppressed. High so distinct
|
|
7973
|
+
* subjects appearing close together are not collapsed. */
|
|
7974
|
+
dedupSpawnIou: number().min(0).max(1).default(.6),
|
|
7975
|
+
/** Envelope IoU at/above which two concurrent tracks count as overlapping for
|
|
7976
|
+
* the sustained-merge streak. Kept equal to `dedupSpawnIou` by default so a
|
|
7977
|
+
* merged duplicate cannot re-spawn. */
|
|
7978
|
+
dedupMergeIou: number().min(0).max(1).default(.6),
|
|
7979
|
+
/** Consecutive overlapping frames before two live tracks are merged (the
|
|
7980
|
+
* lower-importance one dropped). Higher = more conservative. */
|
|
7981
|
+
dedupMergeFrames: number().int().min(1).default(5),
|
|
7982
|
+
/** Treat the {person,animal} class pair as duplicate-compatible — a person in
|
|
7983
|
+
* a non-upright pose is misdetected as `animal`; the false animal track
|
|
7984
|
+
* collapses into the real person track. */
|
|
7985
|
+
personAnimalDedup: boolean().default(true),
|
|
7986
|
+
/** A low-confidence `animal` (score below this) overlapping a concurrent
|
|
7987
|
+
* person track is suppressed at spawn. */
|
|
7988
|
+
animalOverPersonMaxScore: number().min(0).max(1).default(.6),
|
|
7989
|
+
/** IoU with a concurrent person track that triggers the low-confidence animal
|
|
7990
|
+
* spawn suppression. */
|
|
7991
|
+
animalOverPersonIou: number().min(0).max(1).default(.2),
|
|
7992
|
+
/** Resolve a track's reported class by a confidence-weighted majority over its
|
|
7993
|
+
* lifetime (vs. the latest frame) — kills per-frame class flips (a person
|
|
7994
|
+
* read as `animal` on one crouch frame keeps the `person` label). */
|
|
7995
|
+
classVotingEnabled: boolean().default(true),
|
|
7996
|
+
/** Winning class must hold at least this fraction of a track's total vote
|
|
7997
|
+
* weight to override the latest-frame class; below it the latest wins (so a
|
|
7998
|
+
* genuine mid-life reclassification is never frozen out). */
|
|
7999
|
+
classVoteMinFraction: number().min(0).max(1).default(.5),
|
|
8000
|
+
/** Enforce a per-class minimum detection score at track SPAWN (an established
|
|
8001
|
+
* track still matches below its floor — only new spawns are gated). */
|
|
8002
|
+
perClassMinScoreEnabled: boolean().default(true),
|
|
8003
|
+
/** Minimum spawn score for `person`. 0 = ungated (kept sensitive). */
|
|
8004
|
+
minScorePerson: number().min(0).max(1).default(0),
|
|
8005
|
+
/** Minimum spawn score for `animal` (FP-prone — higher floor). */
|
|
8006
|
+
minScoreAnimal: number().min(0).max(1).default(.45),
|
|
8007
|
+
/** Minimum spawn score for `vehicle` (FP-prone — higher floor). */
|
|
8008
|
+
minScoreVehicle: number().min(0).max(1).default(.45),
|
|
8009
|
+
/** Let a single very-high-confidence detection confirm a track for emission
|
|
8010
|
+
* before it reaches `minHits` (so a fast car crossing in 1-2 frames still
|
|
8011
|
+
* registers). Opt-in — default OFF keeps the strict N-hit gate. */
|
|
8012
|
+
confirmBypassEnabled: boolean().default(false),
|
|
8013
|
+
/** Score at/above which a detection confirms its track immediately (bypasses
|
|
8014
|
+
* `minHits`). Only consulted when `confirmBypassEnabled`. */
|
|
8015
|
+
confirmBypassScore: number().min(0).max(1).default(.9),
|
|
7664
8016
|
/** Skip frames where detections suddenly drop to zero (detector glitch). */
|
|
7665
8017
|
dropoutSkipEnabled: boolean().default(true),
|
|
7666
8018
|
/** Max consecutive all-zero frames absorbed as a glitch before the scene is
|
|
7667
8019
|
* treated as genuinely empty. */
|
|
7668
|
-
dropoutMaxSkipFrames: number().int().min(0).default(5)
|
|
8020
|
+
dropoutMaxSkipFrames: number().int().min(0).default(5),
|
|
8021
|
+
/** Two-stage (ByteTrack) association: match high-confidence detections first,
|
|
8022
|
+
* then recover coasting tracks with the low-confidence leftovers before they
|
|
8023
|
+
* die. The single biggest fragmentation reducer. Off = legacy single pass. */
|
|
8024
|
+
byteTrackEnabled: boolean().default(true),
|
|
8025
|
+
/** Score at/above which a detection is HIGH (matched first); below it a detection
|
|
8026
|
+
* is LOW and only gets the second-stage recovery match. Does not gate spawning
|
|
8027
|
+
* (the per-class score/area floors do). */
|
|
8028
|
+
byteTrackHighThreshold: number().min(0).max(1).default(.4),
|
|
8029
|
+
/** Looser IoU gate for the low-score second (recovery) association stage — a
|
|
8030
|
+
* coasting box is stale, and the match only re-attaches an existing track. */
|
|
8031
|
+
byteTrackLowIouThreshold: number().min(0).max(1).default(.2),
|
|
8032
|
+
/** Enforce a per-class minimum bbox-area (fraction of frame) at track SPAWN —
|
|
8033
|
+
* kills tiny far-field / vanishing-point noise tracks. An established track
|
|
8034
|
+
* still matches a shrinking box; only spawns are gated. */
|
|
8035
|
+
minSpawnAreaEnabled: boolean().default(true),
|
|
8036
|
+
/** Min spawn bbox area for `person` as a fraction of the frame (0 = ungated).
|
|
8037
|
+
* Person is the only spawn-score-ungated class, so far-field person noise is
|
|
8038
|
+
* the worst offender; a conservative 0.05% rejects only vanishing-point blips. */
|
|
8039
|
+
minSpawnAreaFracPerson: number().min(0).max(1).default(5e-4),
|
|
8040
|
+
/** Min spawn bbox area for `animal` as a fraction of the frame (0 = ungated). */
|
|
8041
|
+
minSpawnAreaFracAnimal: number().min(0).max(1).default(0),
|
|
8042
|
+
/** Min spawn bbox area for `vehicle` as a fraction of the frame (0 = ungated). */
|
|
8043
|
+
minSpawnAreaFracVehicle: number().min(0).max(1).default(0),
|
|
8044
|
+
/** Gate association on a class GROUP ({person,animal}) instead of the exact
|
|
8045
|
+
* per-frame class, so a person that flips to `animal` for a frame re-matches
|
|
8046
|
+
* its existing track (no concurrent animal id). Reported label stays via the
|
|
8047
|
+
* class vote. Off = exact per-frame class gating. */
|
|
8048
|
+
classGroupAssoc: boolean().default(true)
|
|
7669
8049
|
});
|
|
7670
8050
|
var TRACKING_DEFAULTS = TrackingSettingsSchema.parse({});
|
|
7671
8051
|
/**
|
|
@@ -7695,8 +8075,31 @@ function resolveTrackingSettings(raw) {
|
|
|
7695
8075
|
rescueCentroidFactor: s.rescueCentroidFactor.catch(TRACKING_DEFAULTS.rescueCentroidFactor).parse(raw.rescueCentroidFactor),
|
|
7696
8076
|
resurrectionWindowMs: s.resurrectionWindowMs.catch(TRACKING_DEFAULTS.resurrectionWindowMs).parse(raw.resurrectionWindowMs),
|
|
7697
8077
|
stationarySpeedPx: s.stationarySpeedPx.catch(TRACKING_DEFAULTS.stationarySpeedPx).parse(raw.stationarySpeedPx),
|
|
8078
|
+
dedupEnabled: s.dedupEnabled.catch(TRACKING_DEFAULTS.dedupEnabled).parse(raw.dedupEnabled),
|
|
8079
|
+
dedupSpawnIou: s.dedupSpawnIou.catch(TRACKING_DEFAULTS.dedupSpawnIou).parse(raw.dedupSpawnIou),
|
|
8080
|
+
dedupMergeIou: s.dedupMergeIou.catch(TRACKING_DEFAULTS.dedupMergeIou).parse(raw.dedupMergeIou),
|
|
8081
|
+
dedupMergeFrames: s.dedupMergeFrames.catch(TRACKING_DEFAULTS.dedupMergeFrames).parse(raw.dedupMergeFrames),
|
|
8082
|
+
personAnimalDedup: s.personAnimalDedup.catch(TRACKING_DEFAULTS.personAnimalDedup).parse(raw.personAnimalDedup),
|
|
8083
|
+
animalOverPersonMaxScore: s.animalOverPersonMaxScore.catch(TRACKING_DEFAULTS.animalOverPersonMaxScore).parse(raw.animalOverPersonMaxScore),
|
|
8084
|
+
animalOverPersonIou: s.animalOverPersonIou.catch(TRACKING_DEFAULTS.animalOverPersonIou).parse(raw.animalOverPersonIou),
|
|
8085
|
+
classVotingEnabled: s.classVotingEnabled.catch(TRACKING_DEFAULTS.classVotingEnabled).parse(raw.classVotingEnabled),
|
|
8086
|
+
classVoteMinFraction: s.classVoteMinFraction.catch(TRACKING_DEFAULTS.classVoteMinFraction).parse(raw.classVoteMinFraction),
|
|
8087
|
+
perClassMinScoreEnabled: s.perClassMinScoreEnabled.catch(TRACKING_DEFAULTS.perClassMinScoreEnabled).parse(raw.perClassMinScoreEnabled),
|
|
8088
|
+
minScorePerson: s.minScorePerson.catch(TRACKING_DEFAULTS.minScorePerson).parse(raw.minScorePerson),
|
|
8089
|
+
minScoreAnimal: s.minScoreAnimal.catch(TRACKING_DEFAULTS.minScoreAnimal).parse(raw.minScoreAnimal),
|
|
8090
|
+
minScoreVehicle: s.minScoreVehicle.catch(TRACKING_DEFAULTS.minScoreVehicle).parse(raw.minScoreVehicle),
|
|
8091
|
+
confirmBypassEnabled: s.confirmBypassEnabled.catch(TRACKING_DEFAULTS.confirmBypassEnabled).parse(raw.confirmBypassEnabled),
|
|
8092
|
+
confirmBypassScore: s.confirmBypassScore.catch(TRACKING_DEFAULTS.confirmBypassScore).parse(raw.confirmBypassScore),
|
|
7698
8093
|
dropoutSkipEnabled: s.dropoutSkipEnabled.catch(TRACKING_DEFAULTS.dropoutSkipEnabled).parse(raw.dropoutSkipEnabled),
|
|
7699
|
-
dropoutMaxSkipFrames: s.dropoutMaxSkipFrames.catch(TRACKING_DEFAULTS.dropoutMaxSkipFrames).parse(raw.dropoutMaxSkipFrames)
|
|
8094
|
+
dropoutMaxSkipFrames: s.dropoutMaxSkipFrames.catch(TRACKING_DEFAULTS.dropoutMaxSkipFrames).parse(raw.dropoutMaxSkipFrames),
|
|
8095
|
+
byteTrackEnabled: s.byteTrackEnabled.catch(TRACKING_DEFAULTS.byteTrackEnabled).parse(raw.byteTrackEnabled),
|
|
8096
|
+
byteTrackHighThreshold: s.byteTrackHighThreshold.catch(TRACKING_DEFAULTS.byteTrackHighThreshold).parse(raw.byteTrackHighThreshold),
|
|
8097
|
+
byteTrackLowIouThreshold: s.byteTrackLowIouThreshold.catch(TRACKING_DEFAULTS.byteTrackLowIouThreshold).parse(raw.byteTrackLowIouThreshold),
|
|
8098
|
+
minSpawnAreaEnabled: s.minSpawnAreaEnabled.catch(TRACKING_DEFAULTS.minSpawnAreaEnabled).parse(raw.minSpawnAreaEnabled),
|
|
8099
|
+
minSpawnAreaFracPerson: s.minSpawnAreaFracPerson.catch(TRACKING_DEFAULTS.minSpawnAreaFracPerson).parse(raw.minSpawnAreaFracPerson),
|
|
8100
|
+
minSpawnAreaFracAnimal: s.minSpawnAreaFracAnimal.catch(TRACKING_DEFAULTS.minSpawnAreaFracAnimal).parse(raw.minSpawnAreaFracAnimal),
|
|
8101
|
+
minSpawnAreaFracVehicle: s.minSpawnAreaFracVehicle.catch(TRACKING_DEFAULTS.minSpawnAreaFracVehicle).parse(raw.minSpawnAreaFracVehicle),
|
|
8102
|
+
classGroupAssoc: s.classGroupAssoc.catch(TRACKING_DEFAULTS.classGroupAssoc).parse(raw.classGroupAssoc)
|
|
7700
8103
|
};
|
|
7701
8104
|
}
|
|
7702
8105
|
//#endregion
|
|
@@ -8103,6 +8506,40 @@ function planPeriodicMedia(input) {
|
|
|
8103
8506
|
};
|
|
8104
8507
|
}
|
|
8105
8508
|
//#endregion
|
|
8509
|
+
//#region src/pipeline-analytics/best-thumbnail-guard.ts
|
|
8510
|
+
/**
|
|
8511
|
+
* Void/envArea guard for best-`thumbnail` selection.
|
|
8512
|
+
*
|
|
8513
|
+
* ## Why this exists (the dawn/night "void" thumbnail)
|
|
8514
|
+
*
|
|
8515
|
+
* At dawn/night a moving subject's tracker box intermittently EXPLODES to
|
|
8516
|
+
* (near-)the whole frame — the "envelope exploded to full-frame" signature. If
|
|
8517
|
+
* that frame happens to win the best-detection race, the gallery/reel best
|
|
8518
|
+
* `thumbnail` becomes a useless full-scene tile (the subject crop is the entire
|
|
8519
|
+
* washed-out frame), not the subject. This guard rejects such a frame from the
|
|
8520
|
+
* best-`thumbnail` decision so the track keeps a real subject-centered tile.
|
|
8521
|
+
*
|
|
8522
|
+
* Conservative by design: it only rejects boxes covering ≥ {@link
|
|
8523
|
+
* NEAR_FULL_FRAME_AREA} of the frame — a genuine large close-up subject stays
|
|
8524
|
+
* well under this. Rejecting a frame does NOT land a thumbnail, so the #27-A
|
|
8525
|
+
* per-frame retry keeps trying until a plausible frame wins.
|
|
8526
|
+
*/
|
|
8527
|
+
/**
|
|
8528
|
+
* Area fraction at/above which a detection bbox is treated as an exploded
|
|
8529
|
+
* "envelope" (near-full-frame) box rather than a real subject. 0.85 keeps the
|
|
8530
|
+
* guard conservative — only boxes covering ≥85% of the frame are rejected.
|
|
8531
|
+
*/
|
|
8532
|
+
var NEAR_FULL_FRAME_AREA = .85;
|
|
8533
|
+
/**
|
|
8534
|
+
* True when `bbox` is a plausible SUBJECT box for a best `thumbnail` — i.e. its
|
|
8535
|
+
* area is below the near-full-frame threshold. Degenerate frame dimensions
|
|
8536
|
+
* (≤0) are treated as plausible (no info to reject on).
|
|
8537
|
+
*/
|
|
8538
|
+
function isPlausibleThumbnailBox(bbox, frameWidth, frameHeight) {
|
|
8539
|
+
if (frameWidth <= 0 || frameHeight <= 0) return true;
|
|
8540
|
+
return bbox.w * bbox.h / (frameWidth * frameHeight) < NEAR_FULL_FRAME_AREA;
|
|
8541
|
+
}
|
|
8542
|
+
//#endregion
|
|
8106
8543
|
//#region src/pipeline-analytics/pipeline/key-frame-capture.ts
|
|
8107
8544
|
/**
|
|
8108
8545
|
* Pure helpers for the per-track NATIVE-resolution `keyFrame` capture
|
|
@@ -8134,11 +8571,12 @@ function planPeriodicMedia(input) {
|
|
|
8134
8571
|
var KEYFRAME_NATIVE_MAX_WIDTH = 1920;
|
|
8135
8572
|
/**
|
|
8136
8573
|
* The tracks whose native `keyFrame` should be (re)captured THIS frame: exactly
|
|
8137
|
-
* the tracks that hit a new best-frame moment (`
|
|
8138
|
-
* downstream keeps one `keyFrame`
|
|
8574
|
+
* the tracks that hit a GENUINE new best-frame moment (`keyFrame`), NOT the
|
|
8575
|
+
* per-frame best-thumbnail retry. `putReplacing` downstream keeps one `keyFrame`
|
|
8576
|
+
* per track (the current peak).
|
|
8139
8577
|
*/
|
|
8140
8578
|
function selectKeyFrameTrackIds(targets) {
|
|
8141
|
-
return targets.filter((t) => t.
|
|
8579
|
+
return targets.filter((t) => t.keyFrame).map((t) => t.trackId);
|
|
8142
8580
|
}
|
|
8143
8581
|
/**
|
|
8144
8582
|
* Build the `captureCrop` request for a track's native `keyFrame`: the FULL
|
|
@@ -8158,6 +8596,15 @@ function buildKeyFrameCaptureRequest(frameWidth, frameHeight) {
|
|
|
8158
8596
|
maxWidth: KEYFRAME_NATIVE_MAX_WIDTH
|
|
8159
8597
|
};
|
|
8160
8598
|
}
|
|
8599
|
+
/**
|
|
8600
|
+
* True when a native subject crop of `width` px is a plausible native hit worth
|
|
8601
|
+
* keeping, rather than a sub-threshold fallback stamp. Clamps the floor to the
|
|
8602
|
+
* requested `maxWidth` so a caller that legitimately asked for a narrow crop
|
|
8603
|
+
* (`maxWidth < MIN`) is not rejected for honouring its own cap.
|
|
8604
|
+
*/
|
|
8605
|
+
function nativeSubjectCropMeetsFloor(width, maxWidth) {
|
|
8606
|
+
return width >= Math.min(320, maxWidth);
|
|
8607
|
+
}
|
|
8161
8608
|
//#endregion
|
|
8162
8609
|
//#region src/pipeline-analytics/track-retention-sweep.ts
|
|
8163
8610
|
/**
|
|
@@ -9240,6 +9687,7 @@ var FaceRecognizer = class {
|
|
|
9240
9687
|
bbox: input.parentBbox,
|
|
9241
9688
|
zones: [],
|
|
9242
9689
|
state: "moving",
|
|
9690
|
+
matchedThisFrame: true,
|
|
9243
9691
|
embedding: input.embedding,
|
|
9244
9692
|
embeddingModelId: modelId,
|
|
9245
9693
|
...input.faceBbox !== void 0 ? { faceBbox: input.faceBbox } : {},
|
|
@@ -11410,6 +11858,35 @@ function toAnalyticsDeviceSections(sections) {
|
|
|
11410
11858
|
}));
|
|
11411
11859
|
}
|
|
11412
11860
|
/**
|
|
11861
|
+
* Global-analytics section ids that are really per-camera DETECTION knobs and
|
|
11862
|
+
* belong in the consolidated `Detection pipeline → Settings` sub-tab alongside
|
|
11863
|
+
* Object Detection — NOT under the generic `Analytics` top-tab:
|
|
11864
|
+
* - `detection-sensitivity` — minHits / cooldown / stationary threshold.
|
|
11865
|
+
* - `tracking` — the tracker-tuning form incl. the FP knobs (dedup, person↔
|
|
11866
|
+
* animal dedup, class voting, confirm-bypass, per-class min score).
|
|
11867
|
+
* - `stationary-objects` — stationary promotion + occupancy tuning.
|
|
11868
|
+
* `DeviceDetail` folds `tab: 'detection-pipeline'` top-tab sections into the
|
|
11869
|
+
* structural Detection pipeline tab, so re-tagging is all that's needed —
|
|
11870
|
+
* there is no admin-ui change and no duplicate render (a section has one tab).
|
|
11871
|
+
*/
|
|
11872
|
+
var DETECTION_PIPELINE_SECTION_IDS = new Set([
|
|
11873
|
+
"detection-sensitivity",
|
|
11874
|
+
"tracking",
|
|
11875
|
+
"stationary-objects"
|
|
11876
|
+
]);
|
|
11877
|
+
/**
|
|
11878
|
+
* Re-home the detection-knob sections from the `Analytics` top-tab onto the
|
|
11879
|
+
* `detection-pipeline` top-tab. Pure copy — only the `tab` of a matched
|
|
11880
|
+
* section changes; every other section (media policy, retention, faces, track
|
|
11881
|
+
* history) stays on Analytics.
|
|
11882
|
+
*/
|
|
11883
|
+
function retagDetectionSections(sections) {
|
|
11884
|
+
return sections.map((s) => s.id !== void 0 && DETECTION_PIPELINE_SECTION_IDS.has(s.id) ? {
|
|
11885
|
+
...s,
|
|
11886
|
+
tab: "detection-pipeline"
|
|
11887
|
+
} : s);
|
|
11888
|
+
}
|
|
11889
|
+
/**
|
|
11413
11890
|
* Fields that live ONLY on the global settings page and must never surface in a
|
|
11414
11891
|
* per-device contribution. The face-recognition `enabled` switch is the GLOBAL
|
|
11415
11892
|
* master kill for the whole subsystem — per-camera face production is governed
|
|
@@ -11566,6 +12043,19 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
11566
12043
|
* (recycled/blank live frame) still gets a subject crop for the gallery
|
|
11567
12044
|
* instead of degrading to a full-scene tile. Cleared on track end + reset. */
|
|
11568
12045
|
thumbnailLandedTracks = /* @__PURE__ */ new Set();
|
|
12046
|
+
/** Tracks with a best-`thumbnail` capture CURRENTLY in flight (RC-1). The
|
|
12047
|
+
* #27-A retry re-fired a fresh best-thumbnail capture every frame while one
|
|
12048
|
+
* was still resolving (capture latency stacks 0.1–3s under the native path),
|
|
12049
|
+
* so a short track issued N overlapping captures. While a track sits here
|
|
12050
|
+
* `buildSnapshotTargets` suppresses a new best-thumbnail request; the pending
|
|
12051
|
+
* result clears the flag (and, if it landed, sets `thumbnailLandedTracks`).
|
|
12052
|
+
* Cleared on track end + reset. */
|
|
12053
|
+
thumbnailInFlight = /* @__PURE__ */ new Set();
|
|
12054
|
+
/** Tracks with a native `keyFrame` capture CURRENTLY in flight (RC-1). Guards
|
|
12055
|
+
* the fire-and-forget `persistKeyFrames` so a burst of new-best frames issues
|
|
12056
|
+
* at most ONE outstanding 1920px native capture per track instead of one per
|
|
12057
|
+
* frame. Cleared when the capture settles (+ on track end / reset). */
|
|
12058
|
+
keyFrameInFlight = /* @__PURE__ */ new Set();
|
|
11569
12059
|
/** Per-track update-gate memory for `PipelineAnalyticsTrackLifecycle`
|
|
11570
12060
|
* `phase:'update'` — the last-emitted best (confidence / label / crop
|
|
11571
12061
|
* area) + emit time, so a material improvement is measured against the
|
|
@@ -11791,11 +12281,6 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
11791
12281
|
timestamp: 0
|
|
11792
12282
|
};
|
|
11793
12283
|
};
|
|
11794
|
-
this.eventMediaDispatcher = new EventMediaDispatcher({
|
|
11795
|
-
getRemoteFrame,
|
|
11796
|
-
mediaStore: this.mediaStore,
|
|
11797
|
-
logger: logger.child("EventMediaDispatcher")
|
|
11798
|
-
});
|
|
11799
12284
|
const cropMetricLogger = logger.child("NativeCrop");
|
|
11800
12285
|
let nativeHits = 0;
|
|
11801
12286
|
let nativeFallbacks = 0;
|
|
@@ -11812,7 +12297,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
11812
12297
|
detectionFrameFallbacks: nativeFallbacks
|
|
11813
12298
|
} });
|
|
11814
12299
|
};
|
|
11815
|
-
const
|
|
12300
|
+
const fetchNativeCropRgb = async (frameHandle, paddedNorm, maxWidth) => {
|
|
11816
12301
|
if (!pipelineRunnerApi?.getNativeCrop) return null;
|
|
11817
12302
|
try {
|
|
11818
12303
|
const native = await pipelineRunnerApi.getNativeCrop.query({
|
|
@@ -11821,12 +12306,42 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
11821
12306
|
...maxWidth !== void 0 ? { maxWidth } : {}
|
|
11822
12307
|
}, nodePin(frameHandle.nodeId));
|
|
11823
12308
|
if (!native || native.width <= 0 || native.height <= 0) return null;
|
|
11824
|
-
return
|
|
12309
|
+
return {
|
|
12310
|
+
bytes: Buffer.from(native.bytes),
|
|
12311
|
+
width: native.width,
|
|
12312
|
+
height: native.height
|
|
12313
|
+
};
|
|
11825
12314
|
} catch (err) {
|
|
11826
12315
|
cropMetricLogger.debug("native crop request failed — falling back", { meta: { error: errMsg(err) } });
|
|
11827
12316
|
return null;
|
|
11828
12317
|
}
|
|
11829
12318
|
};
|
|
12319
|
+
const tryNativeCrop = async (frameHandle, paddedNorm, maxWidth) => {
|
|
12320
|
+
const native = await fetchNativeCropRgb(frameHandle, paddedNorm, maxWidth);
|
|
12321
|
+
if (!native) return null;
|
|
12322
|
+
return await encodeRgbCropToJpeg(native.bytes, native.width, native.height);
|
|
12323
|
+
};
|
|
12324
|
+
const getNativeCropJpeg = async (frameHandle, normalizedBbox, maxWidth) => {
|
|
12325
|
+
const native = await fetchNativeCropRgb(frameHandle, normalizedBbox, maxWidth);
|
|
12326
|
+
if (!native || !nativeSubjectCropMeetsFloor(native.width, maxWidth)) {
|
|
12327
|
+
if (native) cropMetricLogger.debug("native subject crop below floor — local fallback", { meta: {
|
|
12328
|
+
width: native.width,
|
|
12329
|
+
height: native.height,
|
|
12330
|
+
maxWidth
|
|
12331
|
+
} });
|
|
12332
|
+
bumpCropMetric(false);
|
|
12333
|
+
return null;
|
|
12334
|
+
}
|
|
12335
|
+
const jpeg = await encodeRgbCropToJpeg(native.bytes, native.width, native.height);
|
|
12336
|
+
bumpCropMetric(jpeg !== null);
|
|
12337
|
+
return jpeg;
|
|
12338
|
+
};
|
|
12339
|
+
this.eventMediaDispatcher = new EventMediaDispatcher({
|
|
12340
|
+
getRemoteFrame,
|
|
12341
|
+
getNativeCropJpeg,
|
|
12342
|
+
mediaStore: this.mediaStore,
|
|
12343
|
+
logger: logger.child("EventMediaDispatcher")
|
|
12344
|
+
});
|
|
11830
12345
|
const resolveFrameShared = createSharedFrameResolver((frameHandle) => resolveFrame(frameHandle, { getRemoteFrame }));
|
|
11831
12346
|
const captureCrop = async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth) => {
|
|
11832
12347
|
const paddedNorm = padBbox({
|
|
@@ -12370,6 +12885,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12370
12885
|
this.bestFrameTracker.clear();
|
|
12371
12886
|
this.lastFrameAtByTrack.clear();
|
|
12372
12887
|
this.thumbnailLandedTracks.clear();
|
|
12888
|
+
this.thumbnailInFlight.clear();
|
|
12889
|
+
this.keyFrameInFlight.clear();
|
|
12373
12890
|
this.trackLifecycleUpdateMem.clear();
|
|
12374
12891
|
this.objectEmbeddingBestSelector.clear();
|
|
12375
12892
|
this.levelStateByDevice.clear();
|
|
@@ -12690,6 +13207,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12690
13207
|
frames: captureAgg.frames,
|
|
12691
13208
|
...captureAgg.sums
|
|
12692
13209
|
} });
|
|
13210
|
+
const thumbInFlightTrackIds = snapshotTargets.filter((t) => t.bestThumbnail).map((t) => t.trackId);
|
|
13211
|
+
for (const trackId of thumbInFlightTrackIds) this.thumbnailInFlight.add(trackId);
|
|
12693
13212
|
this.eventMediaDispatcher.captureForFrame({
|
|
12694
13213
|
deviceId,
|
|
12695
13214
|
frameHandle,
|
|
@@ -12709,7 +13228,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12709
13228
|
mediaKey: s.mediaKey
|
|
12710
13229
|
});
|
|
12711
13230
|
for (const trackId of res.thumbnailTrackIds) this.thumbnailLandedTracks.add(trackId);
|
|
12712
|
-
}).catch(() => {})
|
|
13231
|
+
}).catch(() => {}).finally(() => {
|
|
13232
|
+
for (const trackId of thumbInFlightTrackIds) this.thumbnailInFlight.delete(trackId);
|
|
13233
|
+
});
|
|
12713
13234
|
}
|
|
12714
13235
|
}
|
|
12715
13236
|
if (this.faceRecognizer && faceGloballyEnabled && faceSettings) this.faceRecognizer.processFrame({
|
|
@@ -13154,7 +13675,10 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13154
13675
|
const mediaStore = this.mediaStore;
|
|
13155
13676
|
if (!capture || !mediaStore) return;
|
|
13156
13677
|
const req = buildKeyFrameCaptureRequest(frameWidth, frameHeight);
|
|
13157
|
-
|
|
13678
|
+
const pending = trackIds.filter((id) => !this.keyFrameInFlight.has(id));
|
|
13679
|
+
if (pending.length === 0) return;
|
|
13680
|
+
for (const id of pending) this.keyFrameInFlight.add(id);
|
|
13681
|
+
await Promise.all(pending.map(async (trackId) => {
|
|
13158
13682
|
try {
|
|
13159
13683
|
const keyFrame = await capture(frameHandle, req.bbox, frameWidth, frameHeight, req.padding, req.maxWidth);
|
|
13160
13684
|
if (!keyFrame) return;
|
|
@@ -13175,6 +13699,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13175
13699
|
error: errMsg(err)
|
|
13176
13700
|
}
|
|
13177
13701
|
});
|
|
13702
|
+
} finally {
|
|
13703
|
+
this.keyFrameInFlight.delete(trackId);
|
|
13178
13704
|
}
|
|
13179
13705
|
}));
|
|
13180
13706
|
}
|
|
@@ -13236,6 +13762,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13236
13762
|
buildSnapshotTargets(deviceId, tracked, timestamp, media, frameWidth, frameHeight) {
|
|
13237
13763
|
const targets = [];
|
|
13238
13764
|
for (const t of tracked) {
|
|
13765
|
+
if (t.matchedThisFrame === false) continue;
|
|
13239
13766
|
const lastSnap = this.trackStore.lastSnapshotAt(t.trackId);
|
|
13240
13767
|
const dueSnapshot = media.saveThumbnails && evaluatePeriodicSnapshot({
|
|
13241
13768
|
lastSnapshotAt: lastSnap,
|
|
@@ -13267,7 +13794,10 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13267
13794
|
});
|
|
13268
13795
|
if (plan.rollingLastFrame) this.lastFrameAtByTrack.set(t.trackId, timestamp);
|
|
13269
13796
|
if (plan.appendSnapshot) this.trackStore.markSnapshotPending(t.trackId, timestamp, t.bbox);
|
|
13270
|
-
|
|
13797
|
+
const plausibleBox = isPlausibleThumbnailBox(t.bbox, frameWidth, frameHeight);
|
|
13798
|
+
const bestThumbnail = plan.bestThumbnail && plausibleBox && !this.thumbnailInFlight.has(t.trackId);
|
|
13799
|
+
const keyFrame = isNewBest && plausibleBox;
|
|
13800
|
+
if (!plan.appendSnapshot && !plan.rollingLastFrame && !bestThumbnail && !keyFrame) continue;
|
|
13271
13801
|
targets.push({
|
|
13272
13802
|
trackId: t.trackId,
|
|
13273
13803
|
timestamp,
|
|
@@ -13275,7 +13805,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13275
13805
|
...t.label ? { label: t.label } : {},
|
|
13276
13806
|
appendSnapshot: plan.appendSnapshot,
|
|
13277
13807
|
rollingLastFrame: plan.rollingLastFrame,
|
|
13278
|
-
bestThumbnail
|
|
13808
|
+
bestThumbnail,
|
|
13809
|
+
keyFrame
|
|
13279
13810
|
});
|
|
13280
13811
|
}
|
|
13281
13812
|
return targets;
|
|
@@ -13582,6 +14113,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13582
14113
|
this.objectEmbeddingBestSelector.delete(t.trackId);
|
|
13583
14114
|
this.lastFrameAtByTrack.delete(t.trackId);
|
|
13584
14115
|
this.thumbnailLandedTracks.delete(t.trackId);
|
|
14116
|
+
this.thumbnailInFlight.delete(t.trackId);
|
|
14117
|
+
this.keyFrameInFlight.delete(t.trackId);
|
|
13585
14118
|
this.detailDispatcher?.onTrackEnded(t.deviceId, t.trackId);
|
|
13586
14119
|
this.overlayState.onTrackEnded(t.deviceId, t.trackId);
|
|
13587
14120
|
if ((this.trackStore?.getActive(t.deviceId).length ?? 0) === 0) {
|
|
@@ -13844,6 +14377,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13844
14377
|
this.objectEmbeddingBestSelector.delete(track.trackId);
|
|
13845
14378
|
this.lastFrameAtByTrack.delete(track.trackId);
|
|
13846
14379
|
this.thumbnailLandedTracks.delete(track.trackId);
|
|
14380
|
+
this.thumbnailInFlight.delete(track.trackId);
|
|
14381
|
+
this.keyFrameInFlight.delete(track.trackId);
|
|
13847
14382
|
this.trackLifecycleUpdateMem.delete(track.trackId);
|
|
13848
14383
|
this.detailDispatcher?.onTrackEnded(deviceId, track.trackId);
|
|
13849
14384
|
this.overlayState.onTrackEnded(deviceId, track.trackId);
|
|
@@ -13877,7 +14412,34 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13877
14412
|
rescueCentroidFactor: trk.rescueCentroidFactor,
|
|
13878
14413
|
resurrectionWindowMs: trk.resurrectionWindowMs,
|
|
13879
14414
|
stationarySpeedPx: trk.stationarySpeedPx,
|
|
13880
|
-
maxTrackLifetimeMs: trk.maxTrackLifetimeSec * 1e3
|
|
14415
|
+
maxTrackLifetimeMs: trk.maxTrackLifetimeSec * 1e3,
|
|
14416
|
+
dedupEnabled: trk.dedupEnabled,
|
|
14417
|
+
dedupSpawnIou: trk.dedupSpawnIou,
|
|
14418
|
+
dedupMergeIou: trk.dedupMergeIou,
|
|
14419
|
+
dedupMergeFrames: trk.dedupMergeFrames,
|
|
14420
|
+
personAnimalDedup: trk.personAnimalDedup,
|
|
14421
|
+
animalOverPersonMaxScore: trk.animalOverPersonMaxScore,
|
|
14422
|
+
animalOverPersonIou: trk.animalOverPersonIou,
|
|
14423
|
+
classVotingEnabled: trk.classVotingEnabled,
|
|
14424
|
+
classVoteMinFraction: trk.classVoteMinFraction,
|
|
14425
|
+
perClassMinScoreEnabled: trk.perClassMinScoreEnabled,
|
|
14426
|
+
classMinScores: {
|
|
14427
|
+
person: trk.minScorePerson,
|
|
14428
|
+
animal: trk.minScoreAnimal,
|
|
14429
|
+
vehicle: trk.minScoreVehicle
|
|
14430
|
+
},
|
|
14431
|
+
confirmBypassEnabled: trk.confirmBypassEnabled,
|
|
14432
|
+
confirmBypassScore: trk.confirmBypassScore,
|
|
14433
|
+
byteTrackEnabled: trk.byteTrackEnabled,
|
|
14434
|
+
byteTrackHighThreshold: trk.byteTrackHighThreshold,
|
|
14435
|
+
byteTrackLowIouThreshold: trk.byteTrackLowIouThreshold,
|
|
14436
|
+
minSpawnAreaEnabled: trk.minSpawnAreaEnabled,
|
|
14437
|
+
classSpawnAreaFracs: {
|
|
14438
|
+
person: trk.minSpawnAreaFracPerson,
|
|
14439
|
+
animal: trk.minSpawnAreaFracAnimal,
|
|
14440
|
+
vehicle: trk.minSpawnAreaFracVehicle
|
|
14441
|
+
},
|
|
14442
|
+
classGroupAssoc: trk.classGroupAssoc
|
|
13881
14443
|
}, { stationaryThresholdSec }, {
|
|
13882
14444
|
minTrackAge,
|
|
13883
14445
|
cooldownSec,
|
|
@@ -15180,6 +15742,69 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
15180
15742
|
default: TRACKING_DEFAULTS.occlusionMaxMissedMs,
|
|
15181
15743
|
unit: "ms"
|
|
15182
15744
|
},
|
|
15745
|
+
{
|
|
15746
|
+
type: "boolean",
|
|
15747
|
+
key: "dedupEnabled",
|
|
15748
|
+
label: "Duplicate-track suppression",
|
|
15749
|
+
description: "Suppress and merge concurrent duplicate tracks. Stops one subject the detector double-fires (two boxes for one object, or a person misdetected as an animal) from becoming several time-overlapping tracks that each fire their own event. Off = legacy behaviour.",
|
|
15750
|
+
default: TRACKING_DEFAULTS.dedupEnabled
|
|
15751
|
+
},
|
|
15752
|
+
{
|
|
15753
|
+
type: "number",
|
|
15754
|
+
key: "dedupSpawnIou",
|
|
15755
|
+
label: "Duplicate spawn IoU",
|
|
15756
|
+
description: "Overlap at/above which a NEW track is treated as a duplicate of a concurrent same-kind track and not created. High so two distinct subjects appearing close together are still tracked separately.",
|
|
15757
|
+
min: 0,
|
|
15758
|
+
max: 1,
|
|
15759
|
+
step: .05,
|
|
15760
|
+
default: TRACKING_DEFAULTS.dedupSpawnIou
|
|
15761
|
+
},
|
|
15762
|
+
{
|
|
15763
|
+
type: "number",
|
|
15764
|
+
key: "dedupMergeIou",
|
|
15765
|
+
label: "Duplicate merge IoU",
|
|
15766
|
+
description: "Overlap at/above which two concurrent same-kind tracks count as overlapping for the sustained-merge test.",
|
|
15767
|
+
min: 0,
|
|
15768
|
+
max: 1,
|
|
15769
|
+
step: .05,
|
|
15770
|
+
default: TRACKING_DEFAULTS.dedupMergeIou
|
|
15771
|
+
},
|
|
15772
|
+
{
|
|
15773
|
+
type: "number",
|
|
15774
|
+
key: "dedupMergeFrames",
|
|
15775
|
+
label: "Duplicate merge frames",
|
|
15776
|
+
description: "Consecutive overlapping frames before two concurrent tracks are merged (the shorter / lower-importance one is dropped). Higher = more conservative (only merge sustained overlaps).",
|
|
15777
|
+
min: 1,
|
|
15778
|
+
step: 1,
|
|
15779
|
+
default: TRACKING_DEFAULTS.dedupMergeFrames
|
|
15780
|
+
},
|
|
15781
|
+
{
|
|
15782
|
+
type: "boolean",
|
|
15783
|
+
key: "personAnimalDedup",
|
|
15784
|
+
label: "Person/animal duplicate merge",
|
|
15785
|
+
description: "Treat an animal track overlapping a person track as the same subject (a person in a crouching / bending pose is often misdetected as an animal). The person track wins.",
|
|
15786
|
+
default: TRACKING_DEFAULTS.personAnimalDedup
|
|
15787
|
+
},
|
|
15788
|
+
{
|
|
15789
|
+
type: "number",
|
|
15790
|
+
key: "animalOverPersonMaxScore",
|
|
15791
|
+
label: "Animal-over-person max score",
|
|
15792
|
+
description: "A low-confidence animal detection (score below this) sitting on a concurrent person track is suppressed — the common person-in-odd-pose false animal.",
|
|
15793
|
+
min: 0,
|
|
15794
|
+
max: 1,
|
|
15795
|
+
step: .05,
|
|
15796
|
+
default: TRACKING_DEFAULTS.animalOverPersonMaxScore
|
|
15797
|
+
},
|
|
15798
|
+
{
|
|
15799
|
+
type: "number",
|
|
15800
|
+
key: "animalOverPersonIou",
|
|
15801
|
+
label: "Animal-over-person IoU",
|
|
15802
|
+
description: "Overlap with a concurrent person track that triggers the low-confidence animal spawn suppression.",
|
|
15803
|
+
min: 0,
|
|
15804
|
+
max: 1,
|
|
15805
|
+
step: .05,
|
|
15806
|
+
default: TRACKING_DEFAULTS.animalOverPersonIou
|
|
15807
|
+
},
|
|
15183
15808
|
{
|
|
15184
15809
|
type: "boolean",
|
|
15185
15810
|
key: "dropoutSkipEnabled",
|
|
@@ -15195,6 +15820,148 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
15195
15820
|
min: 0,
|
|
15196
15821
|
step: 1,
|
|
15197
15822
|
default: TRACKING_DEFAULTS.dropoutMaxSkipFrames
|
|
15823
|
+
},
|
|
15824
|
+
{
|
|
15825
|
+
type: "boolean",
|
|
15826
|
+
key: "classVotingEnabled",
|
|
15827
|
+
label: "Per-track class voting",
|
|
15828
|
+
description: "Report a track’s class by a confidence-weighted majority over its whole life instead of the latest frame — a person read as an animal on a single crouch/bend frame keeps the person label.",
|
|
15829
|
+
default: TRACKING_DEFAULTS.classVotingEnabled
|
|
15830
|
+
},
|
|
15831
|
+
{
|
|
15832
|
+
type: "number",
|
|
15833
|
+
key: "classVoteMinFraction",
|
|
15834
|
+
label: "Class-vote min fraction",
|
|
15835
|
+
description: "The winning class must hold at least this fraction of a track’s total vote weight to override the latest frame; below it the latest frame wins (so a genuine mid-life reclassification is not frozen out).",
|
|
15836
|
+
min: 0,
|
|
15837
|
+
max: 1,
|
|
15838
|
+
step: .05,
|
|
15839
|
+
default: TRACKING_DEFAULTS.classVoteMinFraction
|
|
15840
|
+
},
|
|
15841
|
+
{
|
|
15842
|
+
type: "boolean",
|
|
15843
|
+
key: "perClassMinScoreEnabled",
|
|
15844
|
+
label: "Per-class spawn confidence",
|
|
15845
|
+
description: "Require a per-class minimum detection score before a NEW track is created. Kills static-object / reflection false spawns from the FP-prone classes. An already-tracked object still matches on a low-confidence frame.",
|
|
15846
|
+
default: TRACKING_DEFAULTS.perClassMinScoreEnabled
|
|
15847
|
+
},
|
|
15848
|
+
{
|
|
15849
|
+
type: "number",
|
|
15850
|
+
key: "minScorePerson",
|
|
15851
|
+
label: "Min spawn score — person",
|
|
15852
|
+
description: "Minimum score to spawn a person track. 0 keeps person fully sensitive.",
|
|
15853
|
+
min: 0,
|
|
15854
|
+
max: 1,
|
|
15855
|
+
step: .05,
|
|
15856
|
+
default: TRACKING_DEFAULTS.minScorePerson
|
|
15857
|
+
},
|
|
15858
|
+
{
|
|
15859
|
+
type: "number",
|
|
15860
|
+
key: "minScoreAnimal",
|
|
15861
|
+
label: "Min spawn score — animal",
|
|
15862
|
+
description: "Minimum score to spawn an animal track (FP-prone — a higher floor drops low-confidence static-object false animals).",
|
|
15863
|
+
min: 0,
|
|
15864
|
+
max: 1,
|
|
15865
|
+
step: .05,
|
|
15866
|
+
default: TRACKING_DEFAULTS.minScoreAnimal
|
|
15867
|
+
},
|
|
15868
|
+
{
|
|
15869
|
+
type: "number",
|
|
15870
|
+
key: "minScoreVehicle",
|
|
15871
|
+
label: "Min spawn score — vehicle",
|
|
15872
|
+
description: "Minimum score to spawn a vehicle track (FP-prone — a higher floor drops low-confidence false vehicles).",
|
|
15873
|
+
min: 0,
|
|
15874
|
+
max: 1,
|
|
15875
|
+
step: .05,
|
|
15876
|
+
default: TRACKING_DEFAULTS.minScoreVehicle
|
|
15877
|
+
},
|
|
15878
|
+
{
|
|
15879
|
+
type: "boolean",
|
|
15880
|
+
key: "confirmBypassEnabled",
|
|
15881
|
+
label: "Fast high-confidence confirm",
|
|
15882
|
+
description: "Let a single very-high-confidence detection confirm a track for events before it reaches the min-hits gate, so a fast subject crossing in 1-2 frames (a passing car) still registers. Off by default (strict N-hit confirmation).",
|
|
15883
|
+
default: TRACKING_DEFAULTS.confirmBypassEnabled
|
|
15884
|
+
},
|
|
15885
|
+
{
|
|
15886
|
+
type: "number",
|
|
15887
|
+
key: "confirmBypassScore",
|
|
15888
|
+
label: "Fast-confirm score",
|
|
15889
|
+
description: "Score at/above which a detection confirms its track immediately, bypassing min-hits. Only used when fast high-confidence confirm is on.",
|
|
15890
|
+
min: 0,
|
|
15891
|
+
max: 1,
|
|
15892
|
+
step: .05,
|
|
15893
|
+
default: TRACKING_DEFAULTS.confirmBypassScore
|
|
15894
|
+
},
|
|
15895
|
+
{
|
|
15896
|
+
type: "boolean",
|
|
15897
|
+
key: "byteTrackEnabled",
|
|
15898
|
+
label: "Two-stage association (ByteTrack)",
|
|
15899
|
+
description: "Match high-confidence detections first, then recover coasting tracks with the low-confidence leftovers before they die. The single biggest fix for a subject fragmenting into many tracks. Off = legacy single pass.",
|
|
15900
|
+
default: TRACKING_DEFAULTS.byteTrackEnabled
|
|
15901
|
+
},
|
|
15902
|
+
{
|
|
15903
|
+
type: "number",
|
|
15904
|
+
key: "byteTrackHighThreshold",
|
|
15905
|
+
label: "Two-stage high threshold",
|
|
15906
|
+
description: "Score at/above which a detection is matched in the first stage. Below it a detection is LOW and gets a second, looser recovery match against still-unmatched (coasting) tracks. This does not change which detections can start a track (the spawn confidence/size floors do). Only used with two-stage association.",
|
|
15907
|
+
min: 0,
|
|
15908
|
+
max: 1,
|
|
15909
|
+
step: .05,
|
|
15910
|
+
default: TRACKING_DEFAULTS.byteTrackHighThreshold
|
|
15911
|
+
},
|
|
15912
|
+
{
|
|
15913
|
+
type: "number",
|
|
15914
|
+
key: "byteTrackLowIouThreshold",
|
|
15915
|
+
label: "Two-stage low IoU",
|
|
15916
|
+
description: "Looser overlap gate for re-attaching a low-confidence detection to a coasting track in the second stage. Only used with two-stage association.",
|
|
15917
|
+
min: 0,
|
|
15918
|
+
max: 1,
|
|
15919
|
+
step: .05,
|
|
15920
|
+
default: TRACKING_DEFAULTS.byteTrackLowIouThreshold
|
|
15921
|
+
},
|
|
15922
|
+
{
|
|
15923
|
+
type: "boolean",
|
|
15924
|
+
key: "minSpawnAreaEnabled",
|
|
15925
|
+
label: "Minimum spawn size",
|
|
15926
|
+
description: "Require a new track’s box to cover at least a minimum fraction of the frame before it is created — stops tiny far-field / vanishing-point blips from spawning full tracks. An already-tracked subject that shrinks into the distance is unaffected.",
|
|
15927
|
+
default: TRACKING_DEFAULTS.minSpawnAreaEnabled
|
|
15928
|
+
},
|
|
15929
|
+
{
|
|
15930
|
+
type: "number",
|
|
15931
|
+
key: "minSpawnAreaFracPerson",
|
|
15932
|
+
label: "Min spawn area — person",
|
|
15933
|
+
description: "Smallest person box (as a fraction of the frame area) allowed to start a NEW track. 0 = ungated. Conservative default (0.0005 = 0.05% of frame) drops only vanishing-point noise. Raise on cameras with a distant road / horizon.",
|
|
15934
|
+
min: 0,
|
|
15935
|
+
max: 1,
|
|
15936
|
+
step: 5e-4,
|
|
15937
|
+
default: TRACKING_DEFAULTS.minSpawnAreaFracPerson
|
|
15938
|
+
},
|
|
15939
|
+
{
|
|
15940
|
+
type: "number",
|
|
15941
|
+
key: "minSpawnAreaFracAnimal",
|
|
15942
|
+
label: "Min spawn area — animal",
|
|
15943
|
+
description: "Smallest animal box (fraction of frame area) allowed to start a NEW track. 0 = ungated (animal already has a spawn-score floor).",
|
|
15944
|
+
min: 0,
|
|
15945
|
+
max: 1,
|
|
15946
|
+
step: 5e-4,
|
|
15947
|
+
default: TRACKING_DEFAULTS.minSpawnAreaFracAnimal
|
|
15948
|
+
},
|
|
15949
|
+
{
|
|
15950
|
+
type: "number",
|
|
15951
|
+
key: "minSpawnAreaFracVehicle",
|
|
15952
|
+
label: "Min spawn area — vehicle",
|
|
15953
|
+
description: "Smallest vehicle box (fraction of frame area) allowed to start a NEW track. 0 = ungated (vehicle already has a spawn-score floor).",
|
|
15954
|
+
min: 0,
|
|
15955
|
+
max: 1,
|
|
15956
|
+
step: 5e-4,
|
|
15957
|
+
default: TRACKING_DEFAULTS.minSpawnAreaFracVehicle
|
|
15958
|
+
},
|
|
15959
|
+
{
|
|
15960
|
+
type: "boolean",
|
|
15961
|
+
key: "classGroupAssoc",
|
|
15962
|
+
label: "Person/animal group matching",
|
|
15963
|
+
description: "Let a detection that flips between person and animal for a frame (a crouching / bending person is often read as an animal) re-match its existing track instead of spawning a concurrent animal track. The reported class is still decided by the per-track class vote.",
|
|
15964
|
+
default: TRACKING_DEFAULTS.classGroupAssoc
|
|
15198
15965
|
}
|
|
15199
15966
|
]
|
|
15200
15967
|
},
|
|
@@ -15270,7 +16037,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
15270
16037
|
const raw = await this.ctx?.settings?.readDeviceStore(input.deviceId) ?? {};
|
|
15271
16038
|
const baseSections = schema ? hydrateSchema({
|
|
15272
16039
|
...schema,
|
|
15273
|
-
sections: stripGlobalOnlyFields(toAnalyticsDeviceSections(schema.sections))
|
|
16040
|
+
sections: retagDetectionSections(stripGlobalOnlyFields(toAnalyticsDeviceSections(schema.sections)))
|
|
15274
16041
|
}, raw).sections : [];
|
|
15275
16042
|
const liveStatsSection = {
|
|
15276
16043
|
id: "live-stats",
|
|
@@ -15319,4 +16086,4 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
15319
16086
|
}
|
|
15320
16087
|
};
|
|
15321
16088
|
//#endregion
|
|
15322
|
-
export { PipelineAnalyticsAddon as default, pickCleanMedia, stripGlobalOnlyFields, toAnalyticsDeviceSections };
|
|
16089
|
+
export { DETECTION_PIPELINE_SECTION_IDS, PipelineAnalyticsAddon as default, pickCleanMedia, retagDetectionSections, stripGlobalOnlyFields, toAnalyticsDeviceSections };
|