@camstack/addon-post-analysis 1.1.38 → 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-CSMfGdnz.mjs → dist-CO07v2sR.mjs} +43 -5
- package/dist/{dist-_h-RM5Lr.js → dist-DUcGHr9E.js} +43 -5
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-BiZGDArd.js → node-B7HfyyIy.js} +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-B2MjF8pA.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DrIydlxk.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-Ckp6XV52.mjs → hostInit-DnOjDm_e.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +331 -56
- package/dist/pipeline-analytics/index.mjs +330 -55
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-DUcGHr9E.js");
|
|
6
6
|
let node_crypto = require("node:crypto");
|
|
7
7
|
let sharp = require("sharp");
|
|
8
8
|
sharp = require_dist.__toESM(sharp);
|
|
@@ -1141,7 +1141,13 @@ var DEFAULT_TRACKER_CONFIG = {
|
|
|
1141
1141
|
vehicle: .45
|
|
1142
1142
|
},
|
|
1143
1143
|
confirmBypassEnabled: false,
|
|
1144
|
-
confirmBypassScore: .9
|
|
1144
|
+
confirmBypassScore: .9,
|
|
1145
|
+
byteTrackEnabled: true,
|
|
1146
|
+
byteTrackHighThreshold: .4,
|
|
1147
|
+
byteTrackLowIouThreshold: .2,
|
|
1148
|
+
minSpawnAreaEnabled: true,
|
|
1149
|
+
classSpawnAreaFracs: { person: 5e-4 },
|
|
1150
|
+
classGroupAssoc: true
|
|
1145
1151
|
};
|
|
1146
1152
|
/** Macro-classes the duplicate resolver may collapse across (people in
|
|
1147
1153
|
* non-upright poses misdetected as animals). Any other cross-class pair is
|
|
@@ -1198,6 +1204,31 @@ var SortTracker = class SortTracker {
|
|
|
1198
1204
|
static pairKey(a, b) {
|
|
1199
1205
|
return a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
1200
1206
|
}
|
|
1207
|
+
/** Association-gating group of a class. When `classGroupAssoc` is on the
|
|
1208
|
+
* {person,animal} pair collapses to one group so a per-frame class flip
|
|
1209
|
+
* (crouch person → animal) re-matches the existing track instead of spawning a
|
|
1210
|
+
* concurrent id. Every other class is its own group. */
|
|
1211
|
+
assocGroup(cls) {
|
|
1212
|
+
if (this.config.classGroupAssoc && (cls === PERSON_CLASS || cls === ANIMAL_CLASS)) return `${PERSON_CLASS}|${ANIMAL_CLASS}`;
|
|
1213
|
+
return cls;
|
|
1214
|
+
}
|
|
1215
|
+
/** Whether a detection may associate to a track under the current class gate:
|
|
1216
|
+
* exact class when `classGating` is off (no gate at all) — matching the legacy
|
|
1217
|
+
* contract — otherwise same association group (group == exact class unless
|
|
1218
|
+
* `classGroupAssoc` widened it to {person,animal}). */
|
|
1219
|
+
associable(trackClass, detClass) {
|
|
1220
|
+
if (!this.config.classGating) return true;
|
|
1221
|
+
return this.assocGroup(trackClass) === this.assocGroup(detClass);
|
|
1222
|
+
}
|
|
1223
|
+
/** Whether a spawn candidate clears its per-class minimum bbox-area floor
|
|
1224
|
+
* (fraction of frame area). Ungated when disabled, when the class has no floor,
|
|
1225
|
+
* or when the caller supplied no frame dimensions (fraction is uncomputable). */
|
|
1226
|
+
meetsSpawnAreaFloor(det, frameArea) {
|
|
1227
|
+
if (!this.config.minSpawnAreaEnabled) return true;
|
|
1228
|
+
const floor = this.config.classSpawnAreaFracs[det.class] ?? 0;
|
|
1229
|
+
if (floor <= 0 || frameArea <= 0) return true;
|
|
1230
|
+
return det.bbox.w * det.bbox.h / frameArea >= floor;
|
|
1231
|
+
}
|
|
1201
1232
|
/**
|
|
1202
1233
|
* True when spawning a fresh track for `det` would duplicate an already-live
|
|
1203
1234
|
* track: (a) it overlaps a concurrent compatible-class track by
|
|
@@ -1286,13 +1317,16 @@ var SortTracker = class SortTracker {
|
|
|
1286
1317
|
};
|
|
1287
1318
|
}
|
|
1288
1319
|
/**
|
|
1289
|
-
* Loose same-
|
|
1320
|
+
* Loose same-group gate used by the rescue and resurrection passes: accept
|
|
1290
1321
|
* when the detection overlaps the track's LAST-KNOWN bbox by `rescueIou`, or
|
|
1291
|
-
* its centroid is within `rescueCentroidFactor × bbox-diagonal`. Always
|
|
1292
|
-
* gated (a rescue must never cross
|
|
1322
|
+
* its centroid is within `rescueCentroidFactor × bbox-diagonal`. Always group-
|
|
1323
|
+
* gated (a rescue must never cross association groups) independent of
|
|
1324
|
+
* `classGating`, so a coasting person track can be rescued by its own
|
|
1325
|
+
* crouch→animal flip when `classGroupAssoc` is on, but never by an unrelated
|
|
1326
|
+
* class.
|
|
1293
1327
|
*/
|
|
1294
1328
|
looseMatch(track, det) {
|
|
1295
|
-
if (track.class !== det.class) return false;
|
|
1329
|
+
if (this.assocGroup(track.class) !== this.assocGroup(det.class)) return false;
|
|
1296
1330
|
if (iou$2(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return true;
|
|
1297
1331
|
const tc = bboxCentroid(track.bbox);
|
|
1298
1332
|
const dc = bboxCentroid(det.bbox);
|
|
@@ -1340,29 +1374,25 @@ var SortTracker = class SortTracker {
|
|
|
1340
1374
|
if (t.hits >= this.config.minHits) return true;
|
|
1341
1375
|
return this.config.confirmBypassEnabled && t.score >= this.config.confirmBypassScore;
|
|
1342
1376
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
this.tracks = alive;
|
|
1353
|
-
}
|
|
1354
|
-
this.lostTracks = this.lostTracks.filter((t) => timestamp - t.lostAt <= this.config.resurrectionWindowMs);
|
|
1355
|
-
const used = /* @__PURE__ */ new Set();
|
|
1356
|
-
const matchedTracks = /* @__PURE__ */ new Set();
|
|
1357
|
-
const matched = /* @__PURE__ */ new Map();
|
|
1377
|
+
/**
|
|
1378
|
+
* One greedy IoU association pass over a candidate detection subset against the
|
|
1379
|
+
* currently-UNMATCHED tracks, on each track's PREDICTED box. Highest-IoU pairs
|
|
1380
|
+
* bind first; a track/detection binds at most once. Shared by the high-score
|
|
1381
|
+
* first stage and the low-score second stage (different candidate sets + IoU
|
|
1382
|
+
* gates) so the two-tier ByteTrack association stays a single implementation.
|
|
1383
|
+
* Mutates `used` / `matchedTracks` / `matched` in place.
|
|
1384
|
+
*/
|
|
1385
|
+
greedyAssociate(detections, candidateIdxs, iouThreshold, used, matchedTracks, matched) {
|
|
1358
1386
|
const pairs = [];
|
|
1359
1387
|
for (const track of this.tracks) {
|
|
1388
|
+
if (matchedTracks.has(track)) continue;
|
|
1360
1389
|
const pbox = this.predicted(track);
|
|
1361
|
-
for (
|
|
1390
|
+
for (const di of candidateIdxs) {
|
|
1391
|
+
if (used.has(di)) continue;
|
|
1362
1392
|
const det = detections[di];
|
|
1363
|
-
if (this.
|
|
1393
|
+
if (!this.associable(track.class, det.class)) continue;
|
|
1364
1394
|
const score = iou$2(pbox, det.bbox);
|
|
1365
|
-
if (score >=
|
|
1395
|
+
if (score >= iouThreshold) pairs.push({
|
|
1366
1396
|
track,
|
|
1367
1397
|
detIdx: di,
|
|
1368
1398
|
score
|
|
@@ -1376,11 +1406,34 @@ var SortTracker = class SortTracker {
|
|
|
1376
1406
|
matchedTracks.add(pair.track);
|
|
1377
1407
|
used.add(pair.detIdx);
|
|
1378
1408
|
}
|
|
1409
|
+
}
|
|
1410
|
+
update(detections, timestamp, frameContext) {
|
|
1411
|
+
const frameArea = frameContext ? frameContext.frameWidth * frameContext.frameHeight : 0;
|
|
1412
|
+
if (this.config.maxTrackLifetimeMs > 0) {
|
|
1413
|
+
const alive = [];
|
|
1414
|
+
for (const track of this.tracks) if (timestamp - track.firstSeen > this.config.maxTrackLifetimeMs) {
|
|
1415
|
+
track.lost = true;
|
|
1416
|
+
track.lostAt = timestamp;
|
|
1417
|
+
track.resurrectable = false;
|
|
1418
|
+
this.lostTracks.push(track);
|
|
1419
|
+
} else alive.push(track);
|
|
1420
|
+
this.tracks = alive;
|
|
1421
|
+
}
|
|
1422
|
+
this.lostTracks = this.lostTracks.filter((t) => timestamp - t.lostAt <= this.config.resurrectionWindowMs);
|
|
1423
|
+
const used = /* @__PURE__ */ new Set();
|
|
1424
|
+
const matchedTracks = /* @__PURE__ */ new Set();
|
|
1425
|
+
const matched = /* @__PURE__ */ new Map();
|
|
1426
|
+
const highIdxs = [];
|
|
1427
|
+
const lowIdxs = [];
|
|
1428
|
+
for (let di = 0; di < detections.length; di++) if (this.config.byteTrackEnabled && detections[di].score < this.config.byteTrackHighThreshold) lowIdxs.push(di);
|
|
1429
|
+
else highIdxs.push(di);
|
|
1430
|
+
const lowSet = new Set(lowIdxs);
|
|
1431
|
+
this.greedyAssociate(detections, highIdxs, this.config.iouThreshold, used, matchedTracks, matched);
|
|
1379
1432
|
const rescuePairs = [];
|
|
1380
1433
|
for (const track of this.tracks) {
|
|
1381
1434
|
if (matchedTracks.has(track)) continue;
|
|
1382
1435
|
for (let di = 0; di < detections.length; di++) {
|
|
1383
|
-
if (used.has(di)) continue;
|
|
1436
|
+
if (used.has(di) || lowSet.has(di)) continue;
|
|
1384
1437
|
const det = detections[di];
|
|
1385
1438
|
if (!this.looseMatch(track, det)) continue;
|
|
1386
1439
|
rescuePairs.push({
|
|
@@ -1397,6 +1450,7 @@ var SortTracker = class SortTracker {
|
|
|
1397
1450
|
matchedTracks.add(pair.track);
|
|
1398
1451
|
used.add(pair.detIdx);
|
|
1399
1452
|
}
|
|
1453
|
+
if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, used, matchedTracks, matched);
|
|
1400
1454
|
for (const [track, det] of matched) {
|
|
1401
1455
|
const prevCenter = bboxCentroid({
|
|
1402
1456
|
x: track.bbox.x,
|
|
@@ -1493,6 +1547,10 @@ var SortTracker = class SortTracker {
|
|
|
1493
1547
|
used.add(di);
|
|
1494
1548
|
continue;
|
|
1495
1549
|
}
|
|
1550
|
+
if (!this.meetsSpawnAreaFloor(det, frameArea)) {
|
|
1551
|
+
used.add(di);
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1496
1554
|
if (this.config.occlusionEnabled && occluderBoxes.some((ob) => containment(det.bbox, ob) >= this.config.occlusionContainment)) continue;
|
|
1497
1555
|
if (this.config.dedupEnabled && this.isDuplicateSpawn(det, surviving)) {
|
|
1498
1556
|
used.add(di);
|
|
@@ -1529,7 +1587,8 @@ var SortTracker = class SortTracker {
|
|
|
1529
1587
|
trackId: t.id,
|
|
1530
1588
|
trackAge: t.hits,
|
|
1531
1589
|
velocity: t.velocity,
|
|
1532
|
-
path: [...t.path]
|
|
1590
|
+
path: [...t.path],
|
|
1591
|
+
matchedThisFrame: t.lastSeen === timestamp
|
|
1533
1592
|
}));
|
|
1534
1593
|
}
|
|
1535
1594
|
/**
|
|
@@ -2111,7 +2170,10 @@ var FrameProcessor = class {
|
|
|
2111
2170
|
wokenEntryIds: []
|
|
2112
2171
|
};
|
|
2113
2172
|
const trackerInput = gate.suppressedIndices.size > 0 ? filteredDetections.filter((_, i) => !gate.suppressedIndices.has(i)) : filteredDetections;
|
|
2114
|
-
const trackedDetections = this.tracker.update(trackerInput, timestamp
|
|
2173
|
+
const trackedDetections = this.tracker.update(trackerInput, timestamp, {
|
|
2174
|
+
frameWidth,
|
|
2175
|
+
frameHeight
|
|
2176
|
+
});
|
|
2115
2177
|
const objectStates = this.stateAnalyzer.analyze(trackedDetections, timestamp);
|
|
2116
2178
|
const rawEvents = this.eventEmitter.emit(trackedDetections, objectStates, [], [], String(this.deviceId));
|
|
2117
2179
|
const zonesByTrack = /* @__PURE__ */ new Map();
|
|
@@ -2140,6 +2202,7 @@ var FrameProcessor = class {
|
|
|
2140
2202
|
bbox: { ...td.bbox },
|
|
2141
2203
|
zones: zonesByTrack.get(td.trackId) ?? [],
|
|
2142
2204
|
state,
|
|
2205
|
+
matchedThisFrame: td.matchedThisFrame !== false,
|
|
2143
2206
|
...label ? { label } : {},
|
|
2144
2207
|
...emb !== void 0 ? {
|
|
2145
2208
|
embedding: emb.embedding,
|
|
@@ -4412,6 +4475,26 @@ var FACE_MEDIA_OWNER_PREFIX = "face-";
|
|
|
4412
4475
|
* `plate-<trackId>`. Used by `deleteByTracks` to derive the plate crop owners
|
|
4413
4476
|
* of a set of tracks without a `trackId` column on media rows. */
|
|
4414
4477
|
var PLATE_MEDIA_OWNER_PREFIX = "plate-";
|
|
4478
|
+
/**
|
|
4479
|
+
* Kinds that hold exactly ONE row per `(ownerKind, ownerId)` — the current
|
|
4480
|
+
* "best"/rolling/first artefact, never a filmstrip. Their row id + blob path are
|
|
4481
|
+
* DETERMINISTIC (`owner:kind`, NO timestamp) and written via an UPSERT
|
|
4482
|
+
* (`store.set`, atomic `INSERT … ON CONFLICT DO UPDATE`), so N concurrent
|
|
4483
|
+
* captures for the same `(track, kind)` all address the SAME row/blob — one can
|
|
4484
|
+
* never become thirteen regardless of interleaving (RC-1). Accumulating kinds
|
|
4485
|
+
* (`snapshot`, event `crop`/`fullFrame*`, `faceCrop`/`plateCrop`) keep their
|
|
4486
|
+
* per-instance timestamped id so the filmstrip / per-event / per-enrollment rows
|
|
4487
|
+
* still accumulate.
|
|
4488
|
+
*/
|
|
4489
|
+
var SINGLE_INSTANCE_KINDS = new Set([
|
|
4490
|
+
"keyFrame",
|
|
4491
|
+
"thumbnail",
|
|
4492
|
+
"firstFrame",
|
|
4493
|
+
"lastFrame"
|
|
4494
|
+
]);
|
|
4495
|
+
function isSingleInstanceKind(kind) {
|
|
4496
|
+
return SINGLE_INSTANCE_KINDS.has(kind);
|
|
4497
|
+
}
|
|
4415
4498
|
var MEDIA_COLUMNS = [
|
|
4416
4499
|
{
|
|
4417
4500
|
name: "id",
|
|
@@ -4463,10 +4546,11 @@ var MEDIA_INDEXES = [{
|
|
|
4463
4546
|
columns: ["deviceId", "timestamp"]
|
|
4464
4547
|
}];
|
|
4465
4548
|
function buildKey(params) {
|
|
4466
|
-
return `${params.ownerKind}:${params.ownerId}:${params.kind}:${params.timestamp}`;
|
|
4549
|
+
return isSingleInstanceKind(params.kind) ? `${params.ownerKind}:${params.ownerId}:${params.kind}` : `${params.ownerKind}:${params.ownerId}:${params.kind}:${params.timestamp}`;
|
|
4467
4550
|
}
|
|
4468
4551
|
function buildPath(params) {
|
|
4469
|
-
|
|
4552
|
+
const base = `pipeline-analytics/${params.deviceId}/${params.ownerKind}/${params.ownerId}`;
|
|
4553
|
+
return isSingleInstanceKind(params.kind) ? `${base}/${params.kind}.jpg` : `${base}/${params.kind}-${params.timestamp}.jpg`;
|
|
4470
4554
|
}
|
|
4471
4555
|
var MediaStore = class {
|
|
4472
4556
|
storage;
|
|
@@ -4489,25 +4573,31 @@ var MediaStore = class {
|
|
|
4489
4573
|
async put(params) {
|
|
4490
4574
|
const key = buildKey(params);
|
|
4491
4575
|
const path = buildPath(params);
|
|
4576
|
+
const record = {
|
|
4577
|
+
deviceId: params.deviceId,
|
|
4578
|
+
ownerKind: params.ownerKind,
|
|
4579
|
+
ownerId: params.ownerId,
|
|
4580
|
+
kind: params.kind,
|
|
4581
|
+
timestamp: params.timestamp,
|
|
4582
|
+
path,
|
|
4583
|
+
sizeBytes: params.data.length
|
|
4584
|
+
};
|
|
4492
4585
|
try {
|
|
4493
4586
|
await this.storage.write({
|
|
4494
4587
|
location: "eventMedia",
|
|
4495
4588
|
relativePath: path,
|
|
4496
4589
|
data: params.data
|
|
4497
4590
|
});
|
|
4498
|
-
await this.store.
|
|
4591
|
+
if (isSingleInstanceKind(params.kind)) await this.store.set.mutate({
|
|
4592
|
+
collection: MEDIA_COLLECTION,
|
|
4593
|
+
key,
|
|
4594
|
+
value: record
|
|
4595
|
+
});
|
|
4596
|
+
else await this.store.insert.mutate({
|
|
4499
4597
|
collection: MEDIA_COLLECTION,
|
|
4500
4598
|
record: {
|
|
4501
4599
|
id: key,
|
|
4502
|
-
data:
|
|
4503
|
-
deviceId: params.deviceId,
|
|
4504
|
-
ownerKind: params.ownerKind,
|
|
4505
|
-
ownerId: params.ownerId,
|
|
4506
|
-
kind: params.kind,
|
|
4507
|
-
timestamp: params.timestamp,
|
|
4508
|
-
path,
|
|
4509
|
-
sizeBytes: params.data.length
|
|
4510
|
-
}
|
|
4600
|
+
data: record
|
|
4511
4601
|
}
|
|
4512
4602
|
});
|
|
4513
4603
|
return key;
|
|
@@ -4526,8 +4616,14 @@ var MediaStore = class {
|
|
|
4526
4616
|
* Overwrite semantics: keep exactly ONE media entry per (ownerKind, ownerId,
|
|
4527
4617
|
* kind). Used for the rolling `lastFrame` and the best `thumbnail` per track —
|
|
4528
4618
|
* each new capture replaces the previous one (blob + index row) rather than
|
|
4529
|
-
* accumulating a filmstrip the way `put` does.
|
|
4530
|
-
*
|
|
4619
|
+
* accumulating a filmstrip the way `put` does.
|
|
4620
|
+
*
|
|
4621
|
+
* These kinds are {@link SINGLE_INSTANCE_KINDS}, so `put` writes them to a
|
|
4622
|
+
* DETERMINISTIC `owner:kind` row/blob via an UPSERT — N concurrent racing
|
|
4623
|
+
* calls therefore all collapse onto the SAME row (RC-1), no query/insert/delete
|
|
4624
|
+
* interleaving can leave survivors. The query+delete pass below is retained
|
|
4625
|
+
* ONLY to reap LEGACY timestamped rows/blobs written before the deterministic
|
|
4626
|
+
* scheme (transition back-compat); once migrated it is a no-op.
|
|
4531
4627
|
*/
|
|
4532
4628
|
async putReplacing(params) {
|
|
4533
4629
|
const existing = await this.store.query.query({
|
|
@@ -7926,7 +8022,35 @@ var TrackingSettingsSchema = require_dist.object({
|
|
|
7926
8022
|
dropoutSkipEnabled: require_dist.boolean().default(true),
|
|
7927
8023
|
/** Max consecutive all-zero frames absorbed as a glitch before the scene is
|
|
7928
8024
|
* treated as genuinely empty. */
|
|
7929
|
-
dropoutMaxSkipFrames: require_dist.number().int().min(0).default(5)
|
|
8025
|
+
dropoutMaxSkipFrames: require_dist.number().int().min(0).default(5),
|
|
8026
|
+
/** Two-stage (ByteTrack) association: match high-confidence detections first,
|
|
8027
|
+
* then recover coasting tracks with the low-confidence leftovers before they
|
|
8028
|
+
* die. The single biggest fragmentation reducer. Off = legacy single pass. */
|
|
8029
|
+
byteTrackEnabled: require_dist.boolean().default(true),
|
|
8030
|
+
/** Score at/above which a detection is HIGH (matched first); below it a detection
|
|
8031
|
+
* is LOW and only gets the second-stage recovery match. Does not gate spawning
|
|
8032
|
+
* (the per-class score/area floors do). */
|
|
8033
|
+
byteTrackHighThreshold: require_dist.number().min(0).max(1).default(.4),
|
|
8034
|
+
/** Looser IoU gate for the low-score second (recovery) association stage — a
|
|
8035
|
+
* coasting box is stale, and the match only re-attaches an existing track. */
|
|
8036
|
+
byteTrackLowIouThreshold: require_dist.number().min(0).max(1).default(.2),
|
|
8037
|
+
/** Enforce a per-class minimum bbox-area (fraction of frame) at track SPAWN —
|
|
8038
|
+
* kills tiny far-field / vanishing-point noise tracks. An established track
|
|
8039
|
+
* still matches a shrinking box; only spawns are gated. */
|
|
8040
|
+
minSpawnAreaEnabled: require_dist.boolean().default(true),
|
|
8041
|
+
/** Min spawn bbox area for `person` as a fraction of the frame (0 = ungated).
|
|
8042
|
+
* Person is the only spawn-score-ungated class, so far-field person noise is
|
|
8043
|
+
* the worst offender; a conservative 0.05% rejects only vanishing-point blips. */
|
|
8044
|
+
minSpawnAreaFracPerson: require_dist.number().min(0).max(1).default(5e-4),
|
|
8045
|
+
/** Min spawn bbox area for `animal` as a fraction of the frame (0 = ungated). */
|
|
8046
|
+
minSpawnAreaFracAnimal: require_dist.number().min(0).max(1).default(0),
|
|
8047
|
+
/** Min spawn bbox area for `vehicle` as a fraction of the frame (0 = ungated). */
|
|
8048
|
+
minSpawnAreaFracVehicle: require_dist.number().min(0).max(1).default(0),
|
|
8049
|
+
/** Gate association on a class GROUP ({person,animal}) instead of the exact
|
|
8050
|
+
* per-frame class, so a person that flips to `animal` for a frame re-matches
|
|
8051
|
+
* its existing track (no concurrent animal id). Reported label stays via the
|
|
8052
|
+
* class vote. Off = exact per-frame class gating. */
|
|
8053
|
+
classGroupAssoc: require_dist.boolean().default(true)
|
|
7930
8054
|
});
|
|
7931
8055
|
var TRACKING_DEFAULTS = TrackingSettingsSchema.parse({});
|
|
7932
8056
|
/**
|
|
@@ -7972,7 +8096,15 @@ function resolveTrackingSettings(raw) {
|
|
|
7972
8096
|
confirmBypassEnabled: s.confirmBypassEnabled.catch(TRACKING_DEFAULTS.confirmBypassEnabled).parse(raw.confirmBypassEnabled),
|
|
7973
8097
|
confirmBypassScore: s.confirmBypassScore.catch(TRACKING_DEFAULTS.confirmBypassScore).parse(raw.confirmBypassScore),
|
|
7974
8098
|
dropoutSkipEnabled: s.dropoutSkipEnabled.catch(TRACKING_DEFAULTS.dropoutSkipEnabled).parse(raw.dropoutSkipEnabled),
|
|
7975
|
-
dropoutMaxSkipFrames: s.dropoutMaxSkipFrames.catch(TRACKING_DEFAULTS.dropoutMaxSkipFrames).parse(raw.dropoutMaxSkipFrames)
|
|
8099
|
+
dropoutMaxSkipFrames: s.dropoutMaxSkipFrames.catch(TRACKING_DEFAULTS.dropoutMaxSkipFrames).parse(raw.dropoutMaxSkipFrames),
|
|
8100
|
+
byteTrackEnabled: s.byteTrackEnabled.catch(TRACKING_DEFAULTS.byteTrackEnabled).parse(raw.byteTrackEnabled),
|
|
8101
|
+
byteTrackHighThreshold: s.byteTrackHighThreshold.catch(TRACKING_DEFAULTS.byteTrackHighThreshold).parse(raw.byteTrackHighThreshold),
|
|
8102
|
+
byteTrackLowIouThreshold: s.byteTrackLowIouThreshold.catch(TRACKING_DEFAULTS.byteTrackLowIouThreshold).parse(raw.byteTrackLowIouThreshold),
|
|
8103
|
+
minSpawnAreaEnabled: s.minSpawnAreaEnabled.catch(TRACKING_DEFAULTS.minSpawnAreaEnabled).parse(raw.minSpawnAreaEnabled),
|
|
8104
|
+
minSpawnAreaFracPerson: s.minSpawnAreaFracPerson.catch(TRACKING_DEFAULTS.minSpawnAreaFracPerson).parse(raw.minSpawnAreaFracPerson),
|
|
8105
|
+
minSpawnAreaFracAnimal: s.minSpawnAreaFracAnimal.catch(TRACKING_DEFAULTS.minSpawnAreaFracAnimal).parse(raw.minSpawnAreaFracAnimal),
|
|
8106
|
+
minSpawnAreaFracVehicle: s.minSpawnAreaFracVehicle.catch(TRACKING_DEFAULTS.minSpawnAreaFracVehicle).parse(raw.minSpawnAreaFracVehicle),
|
|
8107
|
+
classGroupAssoc: s.classGroupAssoc.catch(TRACKING_DEFAULTS.classGroupAssoc).parse(raw.classGroupAssoc)
|
|
7976
8108
|
};
|
|
7977
8109
|
}
|
|
7978
8110
|
//#endregion
|
|
@@ -8444,11 +8576,12 @@ function isPlausibleThumbnailBox(bbox, frameWidth, frameHeight) {
|
|
|
8444
8576
|
var KEYFRAME_NATIVE_MAX_WIDTH = 1920;
|
|
8445
8577
|
/**
|
|
8446
8578
|
* The tracks whose native `keyFrame` should be (re)captured THIS frame: exactly
|
|
8447
|
-
* the tracks that hit a new best-frame moment (`
|
|
8448
|
-
* downstream keeps one `keyFrame`
|
|
8579
|
+
* the tracks that hit a GENUINE new best-frame moment (`keyFrame`), NOT the
|
|
8580
|
+
* per-frame best-thumbnail retry. `putReplacing` downstream keeps one `keyFrame`
|
|
8581
|
+
* per track (the current peak).
|
|
8449
8582
|
*/
|
|
8450
8583
|
function selectKeyFrameTrackIds(targets) {
|
|
8451
|
-
return targets.filter((t) => t.
|
|
8584
|
+
return targets.filter((t) => t.keyFrame).map((t) => t.trackId);
|
|
8452
8585
|
}
|
|
8453
8586
|
/**
|
|
8454
8587
|
* Build the `captureCrop` request for a track's native `keyFrame`: the FULL
|
|
@@ -8468,6 +8601,15 @@ function buildKeyFrameCaptureRequest(frameWidth, frameHeight) {
|
|
|
8468
8601
|
maxWidth: KEYFRAME_NATIVE_MAX_WIDTH
|
|
8469
8602
|
};
|
|
8470
8603
|
}
|
|
8604
|
+
/**
|
|
8605
|
+
* True when a native subject crop of `width` px is a plausible native hit worth
|
|
8606
|
+
* keeping, rather than a sub-threshold fallback stamp. Clamps the floor to the
|
|
8607
|
+
* requested `maxWidth` so a caller that legitimately asked for a narrow crop
|
|
8608
|
+
* (`maxWidth < MIN`) is not rejected for honouring its own cap.
|
|
8609
|
+
*/
|
|
8610
|
+
function nativeSubjectCropMeetsFloor(width, maxWidth) {
|
|
8611
|
+
return width >= Math.min(320, maxWidth);
|
|
8612
|
+
}
|
|
8471
8613
|
//#endregion
|
|
8472
8614
|
//#region src/pipeline-analytics/track-retention-sweep.ts
|
|
8473
8615
|
/**
|
|
@@ -9550,6 +9692,7 @@ var FaceRecognizer = class {
|
|
|
9550
9692
|
bbox: input.parentBbox,
|
|
9551
9693
|
zones: [],
|
|
9552
9694
|
state: "moving",
|
|
9695
|
+
matchedThisFrame: true,
|
|
9553
9696
|
embedding: input.embedding,
|
|
9554
9697
|
embeddingModelId: modelId,
|
|
9555
9698
|
...input.faceBbox !== void 0 ? { faceBbox: input.faceBbox } : {},
|
|
@@ -11905,6 +12048,19 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
11905
12048
|
* (recycled/blank live frame) still gets a subject crop for the gallery
|
|
11906
12049
|
* instead of degrading to a full-scene tile. Cleared on track end + reset. */
|
|
11907
12050
|
thumbnailLandedTracks = /* @__PURE__ */ new Set();
|
|
12051
|
+
/** Tracks with a best-`thumbnail` capture CURRENTLY in flight (RC-1). The
|
|
12052
|
+
* #27-A retry re-fired a fresh best-thumbnail capture every frame while one
|
|
12053
|
+
* was still resolving (capture latency stacks 0.1–3s under the native path),
|
|
12054
|
+
* so a short track issued N overlapping captures. While a track sits here
|
|
12055
|
+
* `buildSnapshotTargets` suppresses a new best-thumbnail request; the pending
|
|
12056
|
+
* result clears the flag (and, if it landed, sets `thumbnailLandedTracks`).
|
|
12057
|
+
* Cleared on track end + reset. */
|
|
12058
|
+
thumbnailInFlight = /* @__PURE__ */ new Set();
|
|
12059
|
+
/** Tracks with a native `keyFrame` capture CURRENTLY in flight (RC-1). Guards
|
|
12060
|
+
* the fire-and-forget `persistKeyFrames` so a burst of new-best frames issues
|
|
12061
|
+
* at most ONE outstanding 1920px native capture per track instead of one per
|
|
12062
|
+
* frame. Cleared when the capture settles (+ on track end / reset). */
|
|
12063
|
+
keyFrameInFlight = /* @__PURE__ */ new Set();
|
|
11908
12064
|
/** Per-track update-gate memory for `PipelineAnalyticsTrackLifecycle`
|
|
11909
12065
|
* `phase:'update'` — the last-emitted best (confidence / label / crop
|
|
11910
12066
|
* area) + emit time, so a material improvement is measured against the
|
|
@@ -11962,7 +12118,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
11962
12118
|
let storage = this.ctx.kernel.storage;
|
|
11963
12119
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
11964
12120
|
if (mediaRoot) {
|
|
11965
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
12121
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-B7HfyyIy.js"));
|
|
11966
12122
|
storage = new FilesystemStorageProvider(mediaRoot);
|
|
11967
12123
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
11968
12124
|
}
|
|
@@ -12146,7 +12302,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
12146
12302
|
detectionFrameFallbacks: nativeFallbacks
|
|
12147
12303
|
} });
|
|
12148
12304
|
};
|
|
12149
|
-
const
|
|
12305
|
+
const fetchNativeCropRgb = async (frameHandle, paddedNorm, maxWidth) => {
|
|
12150
12306
|
if (!pipelineRunnerApi?.getNativeCrop) return null;
|
|
12151
12307
|
try {
|
|
12152
12308
|
const native = await pipelineRunnerApi.getNativeCrop.query({
|
|
@@ -12155,14 +12311,33 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
12155
12311
|
...maxWidth !== void 0 ? { maxWidth } : {}
|
|
12156
12312
|
}, require_dist.nodePin(frameHandle.nodeId));
|
|
12157
12313
|
if (!native || native.width <= 0 || native.height <= 0) return null;
|
|
12158
|
-
return
|
|
12314
|
+
return {
|
|
12315
|
+
bytes: Buffer.from(native.bytes),
|
|
12316
|
+
width: native.width,
|
|
12317
|
+
height: native.height
|
|
12318
|
+
};
|
|
12159
12319
|
} catch (err) {
|
|
12160
12320
|
cropMetricLogger.debug("native crop request failed — falling back", { meta: { error: require_dist.errMsg(err) } });
|
|
12161
12321
|
return null;
|
|
12162
12322
|
}
|
|
12163
12323
|
};
|
|
12324
|
+
const tryNativeCrop = async (frameHandle, paddedNorm, maxWidth) => {
|
|
12325
|
+
const native = await fetchNativeCropRgb(frameHandle, paddedNorm, maxWidth);
|
|
12326
|
+
if (!native) return null;
|
|
12327
|
+
return await encodeRgbCropToJpeg(native.bytes, native.width, native.height);
|
|
12328
|
+
};
|
|
12164
12329
|
const getNativeCropJpeg = async (frameHandle, normalizedBbox, maxWidth) => {
|
|
12165
|
-
const
|
|
12330
|
+
const native = await fetchNativeCropRgb(frameHandle, normalizedBbox, maxWidth);
|
|
12331
|
+
if (!native || !nativeSubjectCropMeetsFloor(native.width, maxWidth)) {
|
|
12332
|
+
if (native) cropMetricLogger.debug("native subject crop below floor — local fallback", { meta: {
|
|
12333
|
+
width: native.width,
|
|
12334
|
+
height: native.height,
|
|
12335
|
+
maxWidth
|
|
12336
|
+
} });
|
|
12337
|
+
bumpCropMetric(false);
|
|
12338
|
+
return null;
|
|
12339
|
+
}
|
|
12340
|
+
const jpeg = await encodeRgbCropToJpeg(native.bytes, native.width, native.height);
|
|
12166
12341
|
bumpCropMetric(jpeg !== null);
|
|
12167
12342
|
return jpeg;
|
|
12168
12343
|
};
|
|
@@ -12715,6 +12890,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
12715
12890
|
this.bestFrameTracker.clear();
|
|
12716
12891
|
this.lastFrameAtByTrack.clear();
|
|
12717
12892
|
this.thumbnailLandedTracks.clear();
|
|
12893
|
+
this.thumbnailInFlight.clear();
|
|
12894
|
+
this.keyFrameInFlight.clear();
|
|
12718
12895
|
this.trackLifecycleUpdateMem.clear();
|
|
12719
12896
|
this.objectEmbeddingBestSelector.clear();
|
|
12720
12897
|
this.levelStateByDevice.clear();
|
|
@@ -13035,6 +13212,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13035
13212
|
frames: captureAgg.frames,
|
|
13036
13213
|
...captureAgg.sums
|
|
13037
13214
|
} });
|
|
13215
|
+
const thumbInFlightTrackIds = snapshotTargets.filter((t) => t.bestThumbnail).map((t) => t.trackId);
|
|
13216
|
+
for (const trackId of thumbInFlightTrackIds) this.thumbnailInFlight.add(trackId);
|
|
13038
13217
|
this.eventMediaDispatcher.captureForFrame({
|
|
13039
13218
|
deviceId,
|
|
13040
13219
|
frameHandle,
|
|
@@ -13054,7 +13233,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13054
13233
|
mediaKey: s.mediaKey
|
|
13055
13234
|
});
|
|
13056
13235
|
for (const trackId of res.thumbnailTrackIds) this.thumbnailLandedTracks.add(trackId);
|
|
13057
|
-
}).catch(() => {})
|
|
13236
|
+
}).catch(() => {}).finally(() => {
|
|
13237
|
+
for (const trackId of thumbInFlightTrackIds) this.thumbnailInFlight.delete(trackId);
|
|
13238
|
+
});
|
|
13058
13239
|
}
|
|
13059
13240
|
}
|
|
13060
13241
|
if (this.faceRecognizer && faceGloballyEnabled && faceSettings) this.faceRecognizer.processFrame({
|
|
@@ -13499,7 +13680,10 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13499
13680
|
const mediaStore = this.mediaStore;
|
|
13500
13681
|
if (!capture || !mediaStore) return;
|
|
13501
13682
|
const req = buildKeyFrameCaptureRequest(frameWidth, frameHeight);
|
|
13502
|
-
|
|
13683
|
+
const pending = trackIds.filter((id) => !this.keyFrameInFlight.has(id));
|
|
13684
|
+
if (pending.length === 0) return;
|
|
13685
|
+
for (const id of pending) this.keyFrameInFlight.add(id);
|
|
13686
|
+
await Promise.all(pending.map(async (trackId) => {
|
|
13503
13687
|
try {
|
|
13504
13688
|
const keyFrame = await capture(frameHandle, req.bbox, frameWidth, frameHeight, req.padding, req.maxWidth);
|
|
13505
13689
|
if (!keyFrame) return;
|
|
@@ -13520,6 +13704,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13520
13704
|
error: require_dist.errMsg(err)
|
|
13521
13705
|
}
|
|
13522
13706
|
});
|
|
13707
|
+
} finally {
|
|
13708
|
+
this.keyFrameInFlight.delete(trackId);
|
|
13523
13709
|
}
|
|
13524
13710
|
}));
|
|
13525
13711
|
}
|
|
@@ -13581,6 +13767,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13581
13767
|
buildSnapshotTargets(deviceId, tracked, timestamp, media, frameWidth, frameHeight) {
|
|
13582
13768
|
const targets = [];
|
|
13583
13769
|
for (const t of tracked) {
|
|
13770
|
+
if (t.matchedThisFrame === false) continue;
|
|
13584
13771
|
const lastSnap = this.trackStore.lastSnapshotAt(t.trackId);
|
|
13585
13772
|
const dueSnapshot = media.saveThumbnails && evaluatePeriodicSnapshot({
|
|
13586
13773
|
lastSnapshotAt: lastSnap,
|
|
@@ -13612,8 +13799,10 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13612
13799
|
});
|
|
13613
13800
|
if (plan.rollingLastFrame) this.lastFrameAtByTrack.set(t.trackId, timestamp);
|
|
13614
13801
|
if (plan.appendSnapshot) this.trackStore.markSnapshotPending(t.trackId, timestamp, t.bbox);
|
|
13615
|
-
const
|
|
13616
|
-
|
|
13802
|
+
const plausibleBox = isPlausibleThumbnailBox(t.bbox, frameWidth, frameHeight);
|
|
13803
|
+
const bestThumbnail = plan.bestThumbnail && plausibleBox && !this.thumbnailInFlight.has(t.trackId);
|
|
13804
|
+
const keyFrame = isNewBest && plausibleBox;
|
|
13805
|
+
if (!plan.appendSnapshot && !plan.rollingLastFrame && !bestThumbnail && !keyFrame) continue;
|
|
13617
13806
|
targets.push({
|
|
13618
13807
|
trackId: t.trackId,
|
|
13619
13808
|
timestamp,
|
|
@@ -13621,7 +13810,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13621
13810
|
...t.label ? { label: t.label } : {},
|
|
13622
13811
|
appendSnapshot: plan.appendSnapshot,
|
|
13623
13812
|
rollingLastFrame: plan.rollingLastFrame,
|
|
13624
|
-
bestThumbnail
|
|
13813
|
+
bestThumbnail,
|
|
13814
|
+
keyFrame
|
|
13625
13815
|
});
|
|
13626
13816
|
}
|
|
13627
13817
|
return targets;
|
|
@@ -13928,6 +14118,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13928
14118
|
this.objectEmbeddingBestSelector.delete(t.trackId);
|
|
13929
14119
|
this.lastFrameAtByTrack.delete(t.trackId);
|
|
13930
14120
|
this.thumbnailLandedTracks.delete(t.trackId);
|
|
14121
|
+
this.thumbnailInFlight.delete(t.trackId);
|
|
14122
|
+
this.keyFrameInFlight.delete(t.trackId);
|
|
13931
14123
|
this.detailDispatcher?.onTrackEnded(t.deviceId, t.trackId);
|
|
13932
14124
|
this.overlayState.onTrackEnded(t.deviceId, t.trackId);
|
|
13933
14125
|
if ((this.trackStore?.getActive(t.deviceId).length ?? 0) === 0) {
|
|
@@ -14190,6 +14382,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
14190
14382
|
this.objectEmbeddingBestSelector.delete(track.trackId);
|
|
14191
14383
|
this.lastFrameAtByTrack.delete(track.trackId);
|
|
14192
14384
|
this.thumbnailLandedTracks.delete(track.trackId);
|
|
14385
|
+
this.thumbnailInFlight.delete(track.trackId);
|
|
14386
|
+
this.keyFrameInFlight.delete(track.trackId);
|
|
14193
14387
|
this.trackLifecycleUpdateMem.delete(track.trackId);
|
|
14194
14388
|
this.detailDispatcher?.onTrackEnded(deviceId, track.trackId);
|
|
14195
14389
|
this.overlayState.onTrackEnded(deviceId, track.trackId);
|
|
@@ -14240,7 +14434,17 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
14240
14434
|
vehicle: trk.minScoreVehicle
|
|
14241
14435
|
},
|
|
14242
14436
|
confirmBypassEnabled: trk.confirmBypassEnabled,
|
|
14243
|
-
confirmBypassScore: trk.confirmBypassScore
|
|
14437
|
+
confirmBypassScore: trk.confirmBypassScore,
|
|
14438
|
+
byteTrackEnabled: trk.byteTrackEnabled,
|
|
14439
|
+
byteTrackHighThreshold: trk.byteTrackHighThreshold,
|
|
14440
|
+
byteTrackLowIouThreshold: trk.byteTrackLowIouThreshold,
|
|
14441
|
+
minSpawnAreaEnabled: trk.minSpawnAreaEnabled,
|
|
14442
|
+
classSpawnAreaFracs: {
|
|
14443
|
+
person: trk.minSpawnAreaFracPerson,
|
|
14444
|
+
animal: trk.minSpawnAreaFracAnimal,
|
|
14445
|
+
vehicle: trk.minSpawnAreaFracVehicle
|
|
14446
|
+
},
|
|
14447
|
+
classGroupAssoc: trk.classGroupAssoc
|
|
14244
14448
|
}, { stationaryThresholdSec }, {
|
|
14245
14449
|
minTrackAge,
|
|
14246
14450
|
cooldownSec,
|
|
@@ -15692,6 +15896,77 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
15692
15896
|
max: 1,
|
|
15693
15897
|
step: .05,
|
|
15694
15898
|
default: TRACKING_DEFAULTS.confirmBypassScore
|
|
15899
|
+
},
|
|
15900
|
+
{
|
|
15901
|
+
type: "boolean",
|
|
15902
|
+
key: "byteTrackEnabled",
|
|
15903
|
+
label: "Two-stage association (ByteTrack)",
|
|
15904
|
+
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.",
|
|
15905
|
+
default: TRACKING_DEFAULTS.byteTrackEnabled
|
|
15906
|
+
},
|
|
15907
|
+
{
|
|
15908
|
+
type: "number",
|
|
15909
|
+
key: "byteTrackHighThreshold",
|
|
15910
|
+
label: "Two-stage high threshold",
|
|
15911
|
+
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.",
|
|
15912
|
+
min: 0,
|
|
15913
|
+
max: 1,
|
|
15914
|
+
step: .05,
|
|
15915
|
+
default: TRACKING_DEFAULTS.byteTrackHighThreshold
|
|
15916
|
+
},
|
|
15917
|
+
{
|
|
15918
|
+
type: "number",
|
|
15919
|
+
key: "byteTrackLowIouThreshold",
|
|
15920
|
+
label: "Two-stage low IoU",
|
|
15921
|
+
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.",
|
|
15922
|
+
min: 0,
|
|
15923
|
+
max: 1,
|
|
15924
|
+
step: .05,
|
|
15925
|
+
default: TRACKING_DEFAULTS.byteTrackLowIouThreshold
|
|
15926
|
+
},
|
|
15927
|
+
{
|
|
15928
|
+
type: "boolean",
|
|
15929
|
+
key: "minSpawnAreaEnabled",
|
|
15930
|
+
label: "Minimum spawn size",
|
|
15931
|
+
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.",
|
|
15932
|
+
default: TRACKING_DEFAULTS.minSpawnAreaEnabled
|
|
15933
|
+
},
|
|
15934
|
+
{
|
|
15935
|
+
type: "number",
|
|
15936
|
+
key: "minSpawnAreaFracPerson",
|
|
15937
|
+
label: "Min spawn area — person",
|
|
15938
|
+
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.",
|
|
15939
|
+
min: 0,
|
|
15940
|
+
max: 1,
|
|
15941
|
+
step: 5e-4,
|
|
15942
|
+
default: TRACKING_DEFAULTS.minSpawnAreaFracPerson
|
|
15943
|
+
},
|
|
15944
|
+
{
|
|
15945
|
+
type: "number",
|
|
15946
|
+
key: "minSpawnAreaFracAnimal",
|
|
15947
|
+
label: "Min spawn area — animal",
|
|
15948
|
+
description: "Smallest animal box (fraction of frame area) allowed to start a NEW track. 0 = ungated (animal already has a spawn-score floor).",
|
|
15949
|
+
min: 0,
|
|
15950
|
+
max: 1,
|
|
15951
|
+
step: 5e-4,
|
|
15952
|
+
default: TRACKING_DEFAULTS.minSpawnAreaFracAnimal
|
|
15953
|
+
},
|
|
15954
|
+
{
|
|
15955
|
+
type: "number",
|
|
15956
|
+
key: "minSpawnAreaFracVehicle",
|
|
15957
|
+
label: "Min spawn area — vehicle",
|
|
15958
|
+
description: "Smallest vehicle box (fraction of frame area) allowed to start a NEW track. 0 = ungated (vehicle already has a spawn-score floor).",
|
|
15959
|
+
min: 0,
|
|
15960
|
+
max: 1,
|
|
15961
|
+
step: 5e-4,
|
|
15962
|
+
default: TRACKING_DEFAULTS.minSpawnAreaFracVehicle
|
|
15963
|
+
},
|
|
15964
|
+
{
|
|
15965
|
+
type: "boolean",
|
|
15966
|
+
key: "classGroupAssoc",
|
|
15967
|
+
label: "Person/animal group matching",
|
|
15968
|
+
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.",
|
|
15969
|
+
default: TRACKING_DEFAULTS.classGroupAssoc
|
|
15695
15970
|
}
|
|
15696
15971
|
]
|
|
15697
15972
|
},
|