@camstack/addon-post-analysis 1.2.296 → 1.2.297
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/pipeline-analytics/index.js +179 -107
- package/dist/pipeline-analytics/index.mjs +179 -107
- package/package.json +1 -1
|
@@ -47053,10 +47053,127 @@ function pairRiders(detections) {
|
|
|
47053
47053
|
}
|
|
47054
47054
|
return pairs;
|
|
47055
47055
|
}
|
|
47056
|
+
/**
|
|
47057
|
+
* Floor on the predictive-drift clamp, in box sides.
|
|
47058
|
+
*
|
|
47059
|
+
* The scaled term collapses at short intervals — at the 56 ms minimum measured
|
|
47060
|
+
* on camera 615 it is 0.11 sides — and a clamp that tight turns ordinary
|
|
47061
|
+
* detector box jitter into a refused match. A quarter of a side is slack, not
|
|
47062
|
+
* travel.
|
|
47063
|
+
*/
|
|
47064
|
+
var DRIFT_MIN_SIDES = .25;
|
|
47065
|
+
/**
|
|
47066
|
+
* Re-acquisition reach, in box DIAGONALS per second.
|
|
47067
|
+
*
|
|
47068
|
+
* Answers `267840a1` again, on the gate that actually refused it: 908.4 px of
|
|
47069
|
+
* centroid travel in 880 ms against a `hypot(513.6, 721.2)` = 885.5 px diagonal
|
|
47070
|
+
* is 1.166 diagonals per second. 1.5 clears it with ~29 % margin and is
|
|
47071
|
+
* physically bounded — 1.5 of a subject's own body-diagonal per second is a
|
|
47072
|
+
* brisk run, and being scale-free it means the same thing for a far-field
|
|
47073
|
+
* person with a 90-px box as for a near one with a 900-px box.
|
|
47074
|
+
*
|
|
47075
|
+
* The number it replaces (`rescueCentroidFactor` 0.75 × diagonal, flat) was
|
|
47076
|
+
* doing two jobs at once: at a 2135 ms poll gap it was far too tight to bridge
|
|
47077
|
+
* a walking subject, and at a 150 ms burst interval it was loose enough to hand
|
|
47078
|
+
* one subject's id to another 452 px away (track `27855a56`, a second car, at
|
|
47079
|
+
* IoU 0.058). One number, two opposite failures.
|
|
47080
|
+
*/
|
|
47081
|
+
var REACH_DIAGONALS_PER_SEC = 1.5;
|
|
47082
|
+
/**
|
|
47083
|
+
* Floor on the re-acquisition reach, in box diagonals.
|
|
47084
|
+
*
|
|
47085
|
+
* Deliberately TIGHTER than the 0.75 flat factor it replaces. At a 150 ms burst
|
|
47086
|
+
* interval the scaled term is 0.225 diagonals; the floor lifts it to 0.3, which
|
|
47087
|
+
* is 221 px on `27855a56`'s 738-px diagonal — comfortably below the 452 px that
|
|
47088
|
+
* bought that track a second car. A short gap is evidence that the subject
|
|
47089
|
+
* cannot have gone far, and the gate now says so.
|
|
47090
|
+
*/
|
|
47091
|
+
var REACH_MIN_DIAGONALS = .3;
|
|
47092
|
+
/**
|
|
47093
|
+
* Ceiling on the re-acquisition reach, in box diagonals.
|
|
47094
|
+
*
|
|
47095
|
+
* `REACH_DIAGONALS_PER_SEC × BUDGET_CEILING_MS`. A subject unseen for the full
|
|
47096
|
+
* `maxMissedMs` gets 1.5 diagonals of reach, not 6.
|
|
47097
|
+
*/
|
|
47098
|
+
var REACH_MAX_DIAGONALS = 1.5;
|
|
47099
|
+
function clampRange(value, min, max) {
|
|
47100
|
+
return Math.max(min, Math.min(max, value));
|
|
47101
|
+
}
|
|
47102
|
+
/**
|
|
47103
|
+
* How far a coasting box's velocity extrapolation may carry it on one axis.
|
|
47104
|
+
*
|
|
47105
|
+
* `side` is the box's width for the x drift and its height for the y drift, so
|
|
47106
|
+
* the allowance keeps the same meaning for a tall person and a wide car.
|
|
47107
|
+
*/
|
|
47108
|
+
function driftAllowancePx(side, elapsedMs) {
|
|
47109
|
+
const elapsed = Math.max(0, elapsedMs);
|
|
47110
|
+
return clampRange(side * 2 * (elapsed / 1e3), side * DRIFT_MIN_SIDES, side * 2);
|
|
47111
|
+
}
|
|
47112
|
+
/**
|
|
47113
|
+
* How far from where a track is EXPECTED a detection may sit and still be
|
|
47114
|
+
* claimed as that track by the loose (rescue / resurrection) reach clause.
|
|
47115
|
+
*
|
|
47116
|
+
* Scale-free: the budget is a multiple of the track's own diagonal, so it does
|
|
47117
|
+
* not have to know the frame size, the camera, or how far away the subject is.
|
|
47118
|
+
*/
|
|
47119
|
+
function reacquireReachPx(bbox, elapsedMs) {
|
|
47120
|
+
const diag = Math.hypot(bbox.w, bbox.h);
|
|
47121
|
+
const elapsed = Math.max(0, elapsedMs);
|
|
47122
|
+
return clampRange(diag * REACH_DIAGONALS_PER_SEC * (elapsed / 1e3), diag * REACH_MIN_DIAGONALS, diag * REACH_MAX_DIAGONALS);
|
|
47123
|
+
}
|
|
47124
|
+
/**
|
|
47125
|
+
* Resurrection reach, in box DIAGONALS per second of gap (D665).
|
|
47126
|
+
*
|
|
47127
|
+
* A corpse is reached only after `maxMissedMs` (4 s) without a box, so the
|
|
47128
|
+
* rescue rate above — which is at its 1.5-diagonal ceiling by 1 s — says
|
|
47129
|
+
* nothing useful here. A subject the detector has not boxed for four seconds
|
|
47130
|
+
* is, on this fleet's evidence, one that stood still or stood half-hidden,
|
|
47131
|
+
* not one striding across the frame (a striding person is detected): 0.25
|
|
47132
|
+
* diagonals per second is a slow walk for a mid-field person and a pause for
|
|
47133
|
+
* a near one. At the 4 s floor of the graveyard band it gives 1.0 diagonal;
|
|
47134
|
+
* camera 590's refused returns sat at 1.05–1.82× the old flat 0.75 reach,
|
|
47135
|
+
* i.e. 0.79–1.37 diagonals, which this covers from about 5.5 s of gap.
|
|
47136
|
+
*/
|
|
47137
|
+
var RESURRECTION_REACH_DIAGONALS_PER_SEC = .25;
|
|
47138
|
+
/** Never tighter than the flat `rescueCentroidFactor` default it replaces. */
|
|
47139
|
+
var RESURRECTION_REACH_MIN_DIAGONALS = .75;
|
|
47140
|
+
/**
|
|
47141
|
+
* How far from a corpse's last box a detection may sit and still resurrect it
|
|
47142
|
+
* on the proximity clause, for a corpse last seen `gapMs` ago.
|
|
47143
|
+
*/
|
|
47144
|
+
function resurrectionReachPx(bbox, gapMs) {
|
|
47145
|
+
const diag = Math.hypot(bbox.w, bbox.h);
|
|
47146
|
+
return clampRange(diag * RESURRECTION_REACH_DIAGONALS_PER_SEC * (Math.max(0, gapMs) / 1e3), diag * RESURRECTION_REACH_MIN_DIAGONALS, diag * 2);
|
|
47147
|
+
}
|
|
47148
|
+
/**
|
|
47149
|
+
* The classes D665's two relaxations are for: the low-score continuation tier
|
|
47150
|
+
* and the time-scaled resurrection gate. Defined ONCE, here, and read by both
|
|
47151
|
+
* (`low-score-continuation.ts`, `SortTracker.resurrectionTimeScaledFor`),
|
|
47152
|
+
* because the argument for each is the same argument about the same subjects
|
|
47153
|
+
* (D665, D678).
|
|
47154
|
+
*
|
|
47155
|
+
* D665 grew the graveyard reach with the gap on the argument that a subject
|
|
47156
|
+
* nobody has boxed for more than 4 s stood still or stood half-hidden — true
|
|
47157
|
+
* of a person and of an animal, and what the refused returns on camera 590
|
|
47158
|
+
* were. A car does not stand half-hidden for ten seconds and then reappear
|
|
47159
|
+
* one and a half diagonals away: the next car arrives. Camera 617,
|
|
47160
|
+
* 2026-09-28 08:47:32Z: a car at the gate inherited the id of the car that
|
|
47161
|
+
* had left the lot 9.85 s earlier, 1.51 diagonals away. A `vehicle` corpse
|
|
47162
|
+
* keeps the flat `rescueCentroidFactor` reach and the plausible-birth veto
|
|
47163
|
+
* it had before D665 — the same exclusion D665 made for the low-score tier,
|
|
47164
|
+
* where a sub-floor box on a parked car beside a passing one is the D410 hop.
|
|
47165
|
+
*/
|
|
47166
|
+
var LINGERING_SUBJECT_CLASSES = new Set(["person", "animal"]);
|
|
47167
|
+
/** Larger area / smaller area; `Infinity` when either box is empty. */
|
|
47168
|
+
function boxAreaRatio(a, b) {
|
|
47169
|
+
const areaA = a.w * a.h;
|
|
47170
|
+
const areaB = b.w * b.h;
|
|
47171
|
+
const smaller = Math.min(areaA, areaB);
|
|
47172
|
+
return smaller > 0 ? Math.max(areaA, areaB) / smaller : Number.POSITIVE_INFINITY;
|
|
47173
|
+
}
|
|
47056
47174
|
//#endregion
|
|
47057
47175
|
//#region src/pipeline-analytics/pipeline/tracker/low-score-continuation.ts
|
|
47058
47176
|
var LOW_SCORE_CONTINUATION_FLOOR_DEFAULT = .35;
|
|
47059
|
-
var LOW_SCORE_CONTINUATION_CLASSES = new Set(["person", "animal"]);
|
|
47060
47177
|
/** `[x1, y1, x2, y2]` (the discard trail's tuple) → the tracker's rectangle. */
|
|
47061
47178
|
function toRect(bbox) {
|
|
47062
47179
|
const [x1, y1, x2, y2] = bbox;
|
|
@@ -47077,7 +47194,7 @@ function selectLowScoreContinuations(discarded, floor) {
|
|
|
47077
47194
|
for (const d of discarded) {
|
|
47078
47195
|
if (d.reason !== "below-threshold") continue;
|
|
47079
47196
|
if (d.score < floor) continue;
|
|
47080
|
-
if (!
|
|
47197
|
+
if (!LINGERING_SUBJECT_CLASSES.has(d.macroClass)) continue;
|
|
47081
47198
|
const bbox = toRect(d.bbox);
|
|
47082
47199
|
if (bbox.w <= 0 || bbox.h <= 0) continue;
|
|
47083
47200
|
out.push({
|
|
@@ -47158,10 +47275,12 @@ function newTrackId() {
|
|
|
47158
47275
|
* the counts are per camera and per detection source. */
|
|
47159
47276
|
var LooseBindSampler = class {
|
|
47160
47277
|
counts = /* @__PURE__ */ new Map();
|
|
47161
|
-
/** Increment the (stage, gate) counter and answer with the running
|
|
47162
|
-
* this decision should be reported, or `undefined` when it is
|
|
47163
|
-
|
|
47164
|
-
|
|
47278
|
+
/** Increment the (stage, gate, refusal) counter and answer with the running
|
|
47279
|
+
* total when this decision should be reported, or `undefined` when it is
|
|
47280
|
+
* sampled out. The refusal is part of the key so a rare refusal is not
|
|
47281
|
+
* sampled away behind a frequent one on the same gate. */
|
|
47282
|
+
next(stage, gate, refusal) {
|
|
47283
|
+
const key = `${stage}|${gate}|${refusal ?? ""}`;
|
|
47165
47284
|
const count = (this.counts.get(key) ?? 0) + 1;
|
|
47166
47285
|
this.counts.set(key, count);
|
|
47167
47286
|
if (count === 1 || count % 50 === 0) return count;
|
|
@@ -47170,98 +47289,6 @@ var LooseBindSampler = class {
|
|
|
47170
47289
|
this.counts.clear();
|
|
47171
47290
|
}
|
|
47172
47291
|
};
|
|
47173
|
-
/**
|
|
47174
|
-
* Floor on the predictive-drift clamp, in box sides.
|
|
47175
|
-
*
|
|
47176
|
-
* The scaled term collapses at short intervals — at the 56 ms minimum measured
|
|
47177
|
-
* on camera 615 it is 0.11 sides — and a clamp that tight turns ordinary
|
|
47178
|
-
* detector box jitter into a refused match. A quarter of a side is slack, not
|
|
47179
|
-
* travel.
|
|
47180
|
-
*/
|
|
47181
|
-
var DRIFT_MIN_SIDES = .25;
|
|
47182
|
-
/**
|
|
47183
|
-
* Re-acquisition reach, in box DIAGONALS per second.
|
|
47184
|
-
*
|
|
47185
|
-
* Answers `267840a1` again, on the gate that actually refused it: 908.4 px of
|
|
47186
|
-
* centroid travel in 880 ms against a `hypot(513.6, 721.2)` = 885.5 px diagonal
|
|
47187
|
-
* is 1.166 diagonals per second. 1.5 clears it with ~29 % margin and is
|
|
47188
|
-
* physically bounded — 1.5 of a subject's own body-diagonal per second is a
|
|
47189
|
-
* brisk run, and being scale-free it means the same thing for a far-field
|
|
47190
|
-
* person with a 90-px box as for a near one with a 900-px box.
|
|
47191
|
-
*
|
|
47192
|
-
* The number it replaces (`rescueCentroidFactor` 0.75 × diagonal, flat) was
|
|
47193
|
-
* doing two jobs at once: at a 2135 ms poll gap it was far too tight to bridge
|
|
47194
|
-
* a walking subject, and at a 150 ms burst interval it was loose enough to hand
|
|
47195
|
-
* one subject's id to another 452 px away (track `27855a56`, a second car, at
|
|
47196
|
-
* IoU 0.058). One number, two opposite failures.
|
|
47197
|
-
*/
|
|
47198
|
-
var REACH_DIAGONALS_PER_SEC = 1.5;
|
|
47199
|
-
/**
|
|
47200
|
-
* Floor on the re-acquisition reach, in box diagonals.
|
|
47201
|
-
*
|
|
47202
|
-
* Deliberately TIGHTER than the 0.75 flat factor it replaces. At a 150 ms burst
|
|
47203
|
-
* interval the scaled term is 0.225 diagonals; the floor lifts it to 0.3, which
|
|
47204
|
-
* is 221 px on `27855a56`'s 738-px diagonal — comfortably below the 452 px that
|
|
47205
|
-
* bought that track a second car. A short gap is evidence that the subject
|
|
47206
|
-
* cannot have gone far, and the gate now says so.
|
|
47207
|
-
*/
|
|
47208
|
-
var REACH_MIN_DIAGONALS = .3;
|
|
47209
|
-
/**
|
|
47210
|
-
* Ceiling on the re-acquisition reach, in box diagonals.
|
|
47211
|
-
*
|
|
47212
|
-
* `REACH_DIAGONALS_PER_SEC × BUDGET_CEILING_MS`. A subject unseen for the full
|
|
47213
|
-
* `maxMissedMs` gets 1.5 diagonals of reach, not 6.
|
|
47214
|
-
*/
|
|
47215
|
-
var REACH_MAX_DIAGONALS = 1.5;
|
|
47216
|
-
function clampRange(value, min, max) {
|
|
47217
|
-
return Math.max(min, Math.min(max, value));
|
|
47218
|
-
}
|
|
47219
|
-
/**
|
|
47220
|
-
* How far a coasting box's velocity extrapolation may carry it on one axis.
|
|
47221
|
-
*
|
|
47222
|
-
* `side` is the box's width for the x drift and its height for the y drift, so
|
|
47223
|
-
* the allowance keeps the same meaning for a tall person and a wide car.
|
|
47224
|
-
*/
|
|
47225
|
-
function driftAllowancePx(side, elapsedMs) {
|
|
47226
|
-
const elapsed = Math.max(0, elapsedMs);
|
|
47227
|
-
return clampRange(side * 2 * (elapsed / 1e3), side * DRIFT_MIN_SIDES, side * 2);
|
|
47228
|
-
}
|
|
47229
|
-
/**
|
|
47230
|
-
* How far from where a track is EXPECTED a detection may sit and still be
|
|
47231
|
-
* claimed as that track by the loose (rescue / resurrection) reach clause.
|
|
47232
|
-
*
|
|
47233
|
-
* Scale-free: the budget is a multiple of the track's own diagonal, so it does
|
|
47234
|
-
* not have to know the frame size, the camera, or how far away the subject is.
|
|
47235
|
-
*/
|
|
47236
|
-
function reacquireReachPx(bbox, elapsedMs) {
|
|
47237
|
-
const diag = Math.hypot(bbox.w, bbox.h);
|
|
47238
|
-
const elapsed = Math.max(0, elapsedMs);
|
|
47239
|
-
return clampRange(diag * REACH_DIAGONALS_PER_SEC * (elapsed / 1e3), diag * REACH_MIN_DIAGONALS, diag * REACH_MAX_DIAGONALS);
|
|
47240
|
-
}
|
|
47241
|
-
/**
|
|
47242
|
-
* Resurrection reach, in box DIAGONALS per second of gap (D665).
|
|
47243
|
-
*
|
|
47244
|
-
* A corpse is reached only after `maxMissedMs` (4 s) without a box, so the
|
|
47245
|
-
* rescue rate above — which is at its 1.5-diagonal ceiling by 1 s — says
|
|
47246
|
-
* nothing useful here. A subject the detector has not boxed for four seconds
|
|
47247
|
-
* is, on this fleet's evidence, one that stood still or stood half-hidden,
|
|
47248
|
-
* not one striding across the frame (a striding person is detected): 0.25
|
|
47249
|
-
* diagonals per second is a slow walk for a mid-field person and a pause for
|
|
47250
|
-
* a near one. At the 4 s floor of the graveyard band it gives 1.0 diagonal;
|
|
47251
|
-
* camera 590's refused returns sat at 1.05–1.82× the old flat 0.75 reach,
|
|
47252
|
-
* i.e. 0.79–1.37 diagonals, which this covers from about 5.5 s of gap.
|
|
47253
|
-
*/
|
|
47254
|
-
var RESURRECTION_REACH_DIAGONALS_PER_SEC = .25;
|
|
47255
|
-
/** Never tighter than the flat `rescueCentroidFactor` default it replaces. */
|
|
47256
|
-
var RESURRECTION_REACH_MIN_DIAGONALS = .75;
|
|
47257
|
-
/**
|
|
47258
|
-
* How far from a corpse's last box a detection may sit and still resurrect it
|
|
47259
|
-
* on the proximity clause, for a corpse last seen `gapMs` ago.
|
|
47260
|
-
*/
|
|
47261
|
-
function resurrectionReachPx(bbox, gapMs) {
|
|
47262
|
-
const diag = Math.hypot(bbox.w, bbox.h);
|
|
47263
|
-
return clampRange(diag * RESURRECTION_REACH_DIAGONALS_PER_SEC * (Math.max(0, gapMs) / 1e3), diag * RESURRECTION_REACH_MIN_DIAGONALS, diag * 2);
|
|
47264
|
-
}
|
|
47265
47292
|
//#endregion
|
|
47266
47293
|
//#region src/pipeline-analytics/pipeline/tracker/rescue-growth.ts
|
|
47267
47294
|
/**
|
|
@@ -47714,10 +47741,10 @@ var SortTracker = class SortTracker {
|
|
|
47714
47741
|
}
|
|
47715
47742
|
/** Emit one salvage decision, subject to {@link LOOSE_BIND_LOG_SAMPLE_EVERY}.
|
|
47716
47743
|
* Nothing is computed when no observer is wired. */
|
|
47717
|
-
reportBind(stage, gate, track, det, timestamp) {
|
|
47744
|
+
reportBind(stage, gate, track, det, timestamp, refusal) {
|
|
47718
47745
|
const observer = this.bindObserver;
|
|
47719
47746
|
if (!observer) return;
|
|
47720
|
-
const bindCount = this.bindSampler.next(stage, gate);
|
|
47747
|
+
const bindCount = this.bindSampler.next(stage, gate, refusal);
|
|
47721
47748
|
if (bindCount === void 0) return;
|
|
47722
47749
|
const tc = bboxCentroid(track.bbox);
|
|
47723
47750
|
const dc = bboxCentroid(det.bbox);
|
|
@@ -47751,6 +47778,7 @@ var SortTracker = class SortTracker {
|
|
|
47751
47778
|
frameHeight: this.frameHeightPx,
|
|
47752
47779
|
dtMs: timestamp - track.lastSeen,
|
|
47753
47780
|
trackAgeMs: timestamp - track.firstSeen,
|
|
47781
|
+
...refusal !== void 0 ? { refusal } : {},
|
|
47754
47782
|
bindCount,
|
|
47755
47783
|
sampledEvery: 50
|
|
47756
47784
|
});
|
|
@@ -48091,15 +48119,45 @@ var SortTracker = class SortTracker {
|
|
|
48091
48119
|
* was written for; not of the pass as it runs: on camera 590 (2026-09-27)
|
|
48092
48120
|
* 38% of all re-splits were PEOPLE returning after 4–12 s, further than the
|
|
48093
48121
|
* flat gate, with frames flowing the whole time. `resurrectionTimeScaled`
|
|
48094
|
-
* off restores the flat gate
|
|
48122
|
+
* off restores the flat gate — and a `vehicle` corpse always has it (D678,
|
|
48123
|
+
* {@link resurrectionTimeScaledFor}).
|
|
48095
48124
|
*/
|
|
48096
48125
|
reacquireReach(track, timestamp, pass) {
|
|
48097
48126
|
const diag = Math.hypot(track.bbox.w, track.bbox.h);
|
|
48098
|
-
if (pass === "resurrection") return this.
|
|
48127
|
+
if (pass === "resurrection") return this.resurrectionTimeScaledFor(track.class) ? resurrectionReachPx(track.bbox, timestamp - track.lastSeen) : this.config.rescueCentroidFactor * diag;
|
|
48099
48128
|
if (!this.config.reacquireTimeScaled) return this.config.rescueCentroidFactor * diag;
|
|
48100
48129
|
return reacquireReachPx(track.bbox, timestamp - track.lastSeen);
|
|
48101
48130
|
}
|
|
48102
48131
|
/**
|
|
48132
|
+
* Whether D665's graveyard gate — the reach that grows with the gap, and the
|
|
48133
|
+
* plausible-birth veto lifted for a sole subject — applies to a corpse of
|
|
48134
|
+
* `cls`. People and animals only (D678): a car gone for ten seconds did not
|
|
48135
|
+
* stand half-hidden, the next one arrived.
|
|
48136
|
+
*/
|
|
48137
|
+
resurrectionTimeScaledFor(cls) {
|
|
48138
|
+
return this.config.resurrectionTimeScaled && LINGERING_SUBJECT_CLASSES.has(cls);
|
|
48139
|
+
}
|
|
48140
|
+
/**
|
|
48141
|
+
* The size check a PROXIMITY-only resurrection needs (D678), for a pair the
|
|
48142
|
+
* loose gate accepted on its `centroid` clause and the plausible-birth veto
|
|
48143
|
+
* let through.
|
|
48144
|
+
*
|
|
48145
|
+
* The `centroid` clause says where a box is and nothing about what it is;
|
|
48146
|
+
* a box more than {@link RESURRECTION_MAX_AREA_RATIO} larger or smaller than
|
|
48147
|
+
* the corpse's last box is refused on it, and the refusal is returned so the
|
|
48148
|
+
* caller can name it. The pair may still bind on `growth`, the partial-crop
|
|
48149
|
+
* rule whose whole subject IS a size change and which `looseMatch` would
|
|
48150
|
+
* have reached had the centroid clause not answered first. With the default
|
|
48151
|
+
* coast budget that never happens — `growth` refuses a pair more than
|
|
48152
|
+
* `GROWTH_MAX_GAP_MS` (1 s) apart and a corpse has been unseen for
|
|
48153
|
+
* `maxMissedMs` (4 s) — but `maxMissedMs` is a per-camera setting down to 0.
|
|
48154
|
+
*/
|
|
48155
|
+
proximitySizeVerdict(corpse, det, timestamp) {
|
|
48156
|
+
if (boxAreaRatio(corpse.bbox, det.bbox) <= 2.5) return "centroid";
|
|
48157
|
+
if (growthAdjacentMatch(corpse.bbox, det.bbox, this.growthContext(corpse, timestamp))) return "growth";
|
|
48158
|
+
return "size-incompatible";
|
|
48159
|
+
}
|
|
48160
|
+
/**
|
|
48103
48161
|
* Whether a REACH bind would be unambiguous: this coasting track is the only
|
|
48104
48162
|
* subject of its association group the scene holds, and `det` is the only box
|
|
48105
48163
|
* of that group on the frame.
|
|
@@ -48513,13 +48571,26 @@ var SortTracker = class SortTracker {
|
|
|
48513
48571
|
let refused;
|
|
48514
48572
|
let refusedGate = "iou";
|
|
48515
48573
|
let refusedScore = -1;
|
|
48574
|
+
let refusedBy = "crowd-guard";
|
|
48516
48575
|
for (const lost of this.lostTracks) {
|
|
48517
48576
|
if (!lost.resurrectable) continue;
|
|
48518
48577
|
if (timestamp - lost.lostAt > this.config.resurrectionWindowMs) continue;
|
|
48519
|
-
const
|
|
48520
|
-
if (
|
|
48521
|
-
const
|
|
48522
|
-
if (
|
|
48578
|
+
const loose = this.looseMatch(lost, det, timestamp, "resurrection");
|
|
48579
|
+
if (loose === null) continue;
|
|
48580
|
+
const vetoApplies = loose === "centroid" && this.isPlausibleBirth(det, frameArea);
|
|
48581
|
+
if (vetoApplies && !(this.resurrectionTimeScaledFor(lost.class) && this.isSoleSubjectResurrection(lost, det, detections, surviving, lowScore))) continue;
|
|
48582
|
+
const gate = loose === "centroid" ? this.proximitySizeVerdict(lost, det, timestamp) : loose;
|
|
48583
|
+
if (gate === "size-incompatible") {
|
|
48584
|
+
const score = iou$2(lost.bbox, det.bbox);
|
|
48585
|
+
if (score > refusedScore) {
|
|
48586
|
+
refused = lost;
|
|
48587
|
+
refusedGate = "centroid";
|
|
48588
|
+
refusedScore = score;
|
|
48589
|
+
refusedBy = "size-incompatible";
|
|
48590
|
+
}
|
|
48591
|
+
continue;
|
|
48592
|
+
}
|
|
48593
|
+
const vetoLifted = vetoApplies && gate === "centroid";
|
|
48523
48594
|
const score = iou$2(lost.bbox, det.bbox);
|
|
48524
48595
|
const peers = this.sameClassPeerCount(lost, surviving);
|
|
48525
48596
|
if (this.config.resurrectionCrowdGuard && peers > 0 && score < this.config.resurrectionCrowdedIouThreshold) {
|
|
@@ -48527,6 +48598,7 @@ var SortTracker = class SortTracker {
|
|
|
48527
48598
|
refused = lost;
|
|
48528
48599
|
refusedGate = gate;
|
|
48529
48600
|
refusedScore = score;
|
|
48601
|
+
refusedBy = "crowd-guard";
|
|
48530
48602
|
}
|
|
48531
48603
|
continue;
|
|
48532
48604
|
}
|
|
@@ -48539,7 +48611,7 @@ var SortTracker = class SortTracker {
|
|
|
48539
48611
|
}
|
|
48540
48612
|
}
|
|
48541
48613
|
if (!best) {
|
|
48542
|
-
if (refused !== void 0) this.reportBind("resurrection-refused", refusedGate, refused, det, timestamp);
|
|
48614
|
+
if (refused !== void 0) this.reportBind("resurrection-refused", refusedGate, refused, det, timestamp, refusedBy);
|
|
48543
48615
|
continue;
|
|
48544
48616
|
}
|
|
48545
48617
|
resurrectedScene.set(best, bestPeers > 0 ? "multi-subject" : bestVetoLifted ? "sole-subject-veto-lifted" : "sole-subject");
|
|
@@ -47028,10 +47028,127 @@ function pairRiders(detections) {
|
|
|
47028
47028
|
}
|
|
47029
47029
|
return pairs;
|
|
47030
47030
|
}
|
|
47031
|
+
/**
|
|
47032
|
+
* Floor on the predictive-drift clamp, in box sides.
|
|
47033
|
+
*
|
|
47034
|
+
* The scaled term collapses at short intervals — at the 56 ms minimum measured
|
|
47035
|
+
* on camera 615 it is 0.11 sides — and a clamp that tight turns ordinary
|
|
47036
|
+
* detector box jitter into a refused match. A quarter of a side is slack, not
|
|
47037
|
+
* travel.
|
|
47038
|
+
*/
|
|
47039
|
+
var DRIFT_MIN_SIDES = .25;
|
|
47040
|
+
/**
|
|
47041
|
+
* Re-acquisition reach, in box DIAGONALS per second.
|
|
47042
|
+
*
|
|
47043
|
+
* Answers `267840a1` again, on the gate that actually refused it: 908.4 px of
|
|
47044
|
+
* centroid travel in 880 ms against a `hypot(513.6, 721.2)` = 885.5 px diagonal
|
|
47045
|
+
* is 1.166 diagonals per second. 1.5 clears it with ~29 % margin and is
|
|
47046
|
+
* physically bounded — 1.5 of a subject's own body-diagonal per second is a
|
|
47047
|
+
* brisk run, and being scale-free it means the same thing for a far-field
|
|
47048
|
+
* person with a 90-px box as for a near one with a 900-px box.
|
|
47049
|
+
*
|
|
47050
|
+
* The number it replaces (`rescueCentroidFactor` 0.75 × diagonal, flat) was
|
|
47051
|
+
* doing two jobs at once: at a 2135 ms poll gap it was far too tight to bridge
|
|
47052
|
+
* a walking subject, and at a 150 ms burst interval it was loose enough to hand
|
|
47053
|
+
* one subject's id to another 452 px away (track `27855a56`, a second car, at
|
|
47054
|
+
* IoU 0.058). One number, two opposite failures.
|
|
47055
|
+
*/
|
|
47056
|
+
var REACH_DIAGONALS_PER_SEC = 1.5;
|
|
47057
|
+
/**
|
|
47058
|
+
* Floor on the re-acquisition reach, in box diagonals.
|
|
47059
|
+
*
|
|
47060
|
+
* Deliberately TIGHTER than the 0.75 flat factor it replaces. At a 150 ms burst
|
|
47061
|
+
* interval the scaled term is 0.225 diagonals; the floor lifts it to 0.3, which
|
|
47062
|
+
* is 221 px on `27855a56`'s 738-px diagonal — comfortably below the 452 px that
|
|
47063
|
+
* bought that track a second car. A short gap is evidence that the subject
|
|
47064
|
+
* cannot have gone far, and the gate now says so.
|
|
47065
|
+
*/
|
|
47066
|
+
var REACH_MIN_DIAGONALS = .3;
|
|
47067
|
+
/**
|
|
47068
|
+
* Ceiling on the re-acquisition reach, in box diagonals.
|
|
47069
|
+
*
|
|
47070
|
+
* `REACH_DIAGONALS_PER_SEC × BUDGET_CEILING_MS`. A subject unseen for the full
|
|
47071
|
+
* `maxMissedMs` gets 1.5 diagonals of reach, not 6.
|
|
47072
|
+
*/
|
|
47073
|
+
var REACH_MAX_DIAGONALS = 1.5;
|
|
47074
|
+
function clampRange(value, min, max) {
|
|
47075
|
+
return Math.max(min, Math.min(max, value));
|
|
47076
|
+
}
|
|
47077
|
+
/**
|
|
47078
|
+
* How far a coasting box's velocity extrapolation may carry it on one axis.
|
|
47079
|
+
*
|
|
47080
|
+
* `side` is the box's width for the x drift and its height for the y drift, so
|
|
47081
|
+
* the allowance keeps the same meaning for a tall person and a wide car.
|
|
47082
|
+
*/
|
|
47083
|
+
function driftAllowancePx(side, elapsedMs) {
|
|
47084
|
+
const elapsed = Math.max(0, elapsedMs);
|
|
47085
|
+
return clampRange(side * 2 * (elapsed / 1e3), side * DRIFT_MIN_SIDES, side * 2);
|
|
47086
|
+
}
|
|
47087
|
+
/**
|
|
47088
|
+
* How far from where a track is EXPECTED a detection may sit and still be
|
|
47089
|
+
* claimed as that track by the loose (rescue / resurrection) reach clause.
|
|
47090
|
+
*
|
|
47091
|
+
* Scale-free: the budget is a multiple of the track's own diagonal, so it does
|
|
47092
|
+
* not have to know the frame size, the camera, or how far away the subject is.
|
|
47093
|
+
*/
|
|
47094
|
+
function reacquireReachPx(bbox, elapsedMs) {
|
|
47095
|
+
const diag = Math.hypot(bbox.w, bbox.h);
|
|
47096
|
+
const elapsed = Math.max(0, elapsedMs);
|
|
47097
|
+
return clampRange(diag * REACH_DIAGONALS_PER_SEC * (elapsed / 1e3), diag * REACH_MIN_DIAGONALS, diag * REACH_MAX_DIAGONALS);
|
|
47098
|
+
}
|
|
47099
|
+
/**
|
|
47100
|
+
* Resurrection reach, in box DIAGONALS per second of gap (D665).
|
|
47101
|
+
*
|
|
47102
|
+
* A corpse is reached only after `maxMissedMs` (4 s) without a box, so the
|
|
47103
|
+
* rescue rate above — which is at its 1.5-diagonal ceiling by 1 s — says
|
|
47104
|
+
* nothing useful here. A subject the detector has not boxed for four seconds
|
|
47105
|
+
* is, on this fleet's evidence, one that stood still or stood half-hidden,
|
|
47106
|
+
* not one striding across the frame (a striding person is detected): 0.25
|
|
47107
|
+
* diagonals per second is a slow walk for a mid-field person and a pause for
|
|
47108
|
+
* a near one. At the 4 s floor of the graveyard band it gives 1.0 diagonal;
|
|
47109
|
+
* camera 590's refused returns sat at 1.05–1.82× the old flat 0.75 reach,
|
|
47110
|
+
* i.e. 0.79–1.37 diagonals, which this covers from about 5.5 s of gap.
|
|
47111
|
+
*/
|
|
47112
|
+
var RESURRECTION_REACH_DIAGONALS_PER_SEC = .25;
|
|
47113
|
+
/** Never tighter than the flat `rescueCentroidFactor` default it replaces. */
|
|
47114
|
+
var RESURRECTION_REACH_MIN_DIAGONALS = .75;
|
|
47115
|
+
/**
|
|
47116
|
+
* How far from a corpse's last box a detection may sit and still resurrect it
|
|
47117
|
+
* on the proximity clause, for a corpse last seen `gapMs` ago.
|
|
47118
|
+
*/
|
|
47119
|
+
function resurrectionReachPx(bbox, gapMs) {
|
|
47120
|
+
const diag = Math.hypot(bbox.w, bbox.h);
|
|
47121
|
+
return clampRange(diag * RESURRECTION_REACH_DIAGONALS_PER_SEC * (Math.max(0, gapMs) / 1e3), diag * RESURRECTION_REACH_MIN_DIAGONALS, diag * 2);
|
|
47122
|
+
}
|
|
47123
|
+
/**
|
|
47124
|
+
* The classes D665's two relaxations are for: the low-score continuation tier
|
|
47125
|
+
* and the time-scaled resurrection gate. Defined ONCE, here, and read by both
|
|
47126
|
+
* (`low-score-continuation.ts`, `SortTracker.resurrectionTimeScaledFor`),
|
|
47127
|
+
* because the argument for each is the same argument about the same subjects
|
|
47128
|
+
* (D665, D678).
|
|
47129
|
+
*
|
|
47130
|
+
* D665 grew the graveyard reach with the gap on the argument that a subject
|
|
47131
|
+
* nobody has boxed for more than 4 s stood still or stood half-hidden — true
|
|
47132
|
+
* of a person and of an animal, and what the refused returns on camera 590
|
|
47133
|
+
* were. A car does not stand half-hidden for ten seconds and then reappear
|
|
47134
|
+
* one and a half diagonals away: the next car arrives. Camera 617,
|
|
47135
|
+
* 2026-09-28 08:47:32Z: a car at the gate inherited the id of the car that
|
|
47136
|
+
* had left the lot 9.85 s earlier, 1.51 diagonals away. A `vehicle` corpse
|
|
47137
|
+
* keeps the flat `rescueCentroidFactor` reach and the plausible-birth veto
|
|
47138
|
+
* it had before D665 — the same exclusion D665 made for the low-score tier,
|
|
47139
|
+
* where a sub-floor box on a parked car beside a passing one is the D410 hop.
|
|
47140
|
+
*/
|
|
47141
|
+
var LINGERING_SUBJECT_CLASSES = new Set(["person", "animal"]);
|
|
47142
|
+
/** Larger area / smaller area; `Infinity` when either box is empty. */
|
|
47143
|
+
function boxAreaRatio(a, b) {
|
|
47144
|
+
const areaA = a.w * a.h;
|
|
47145
|
+
const areaB = b.w * b.h;
|
|
47146
|
+
const smaller = Math.min(areaA, areaB);
|
|
47147
|
+
return smaller > 0 ? Math.max(areaA, areaB) / smaller : Number.POSITIVE_INFINITY;
|
|
47148
|
+
}
|
|
47031
47149
|
//#endregion
|
|
47032
47150
|
//#region src/pipeline-analytics/pipeline/tracker/low-score-continuation.ts
|
|
47033
47151
|
var LOW_SCORE_CONTINUATION_FLOOR_DEFAULT = .35;
|
|
47034
|
-
var LOW_SCORE_CONTINUATION_CLASSES = new Set(["person", "animal"]);
|
|
47035
47152
|
/** `[x1, y1, x2, y2]` (the discard trail's tuple) → the tracker's rectangle. */
|
|
47036
47153
|
function toRect(bbox) {
|
|
47037
47154
|
const [x1, y1, x2, y2] = bbox;
|
|
@@ -47052,7 +47169,7 @@ function selectLowScoreContinuations(discarded, floor) {
|
|
|
47052
47169
|
for (const d of discarded) {
|
|
47053
47170
|
if (d.reason !== "below-threshold") continue;
|
|
47054
47171
|
if (d.score < floor) continue;
|
|
47055
|
-
if (!
|
|
47172
|
+
if (!LINGERING_SUBJECT_CLASSES.has(d.macroClass)) continue;
|
|
47056
47173
|
const bbox = toRect(d.bbox);
|
|
47057
47174
|
if (bbox.w <= 0 || bbox.h <= 0) continue;
|
|
47058
47175
|
out.push({
|
|
@@ -47133,10 +47250,12 @@ function newTrackId() {
|
|
|
47133
47250
|
* the counts are per camera and per detection source. */
|
|
47134
47251
|
var LooseBindSampler = class {
|
|
47135
47252
|
counts = /* @__PURE__ */ new Map();
|
|
47136
|
-
/** Increment the (stage, gate) counter and answer with the running
|
|
47137
|
-
* this decision should be reported, or `undefined` when it is
|
|
47138
|
-
|
|
47139
|
-
|
|
47253
|
+
/** Increment the (stage, gate, refusal) counter and answer with the running
|
|
47254
|
+
* total when this decision should be reported, or `undefined` when it is
|
|
47255
|
+
* sampled out. The refusal is part of the key so a rare refusal is not
|
|
47256
|
+
* sampled away behind a frequent one on the same gate. */
|
|
47257
|
+
next(stage, gate, refusal) {
|
|
47258
|
+
const key = `${stage}|${gate}|${refusal ?? ""}`;
|
|
47140
47259
|
const count = (this.counts.get(key) ?? 0) + 1;
|
|
47141
47260
|
this.counts.set(key, count);
|
|
47142
47261
|
if (count === 1 || count % 50 === 0) return count;
|
|
@@ -47145,98 +47264,6 @@ var LooseBindSampler = class {
|
|
|
47145
47264
|
this.counts.clear();
|
|
47146
47265
|
}
|
|
47147
47266
|
};
|
|
47148
|
-
/**
|
|
47149
|
-
* Floor on the predictive-drift clamp, in box sides.
|
|
47150
|
-
*
|
|
47151
|
-
* The scaled term collapses at short intervals — at the 56 ms minimum measured
|
|
47152
|
-
* on camera 615 it is 0.11 sides — and a clamp that tight turns ordinary
|
|
47153
|
-
* detector box jitter into a refused match. A quarter of a side is slack, not
|
|
47154
|
-
* travel.
|
|
47155
|
-
*/
|
|
47156
|
-
var DRIFT_MIN_SIDES = .25;
|
|
47157
|
-
/**
|
|
47158
|
-
* Re-acquisition reach, in box DIAGONALS per second.
|
|
47159
|
-
*
|
|
47160
|
-
* Answers `267840a1` again, on the gate that actually refused it: 908.4 px of
|
|
47161
|
-
* centroid travel in 880 ms against a `hypot(513.6, 721.2)` = 885.5 px diagonal
|
|
47162
|
-
* is 1.166 diagonals per second. 1.5 clears it with ~29 % margin and is
|
|
47163
|
-
* physically bounded — 1.5 of a subject's own body-diagonal per second is a
|
|
47164
|
-
* brisk run, and being scale-free it means the same thing for a far-field
|
|
47165
|
-
* person with a 90-px box as for a near one with a 900-px box.
|
|
47166
|
-
*
|
|
47167
|
-
* The number it replaces (`rescueCentroidFactor` 0.75 × diagonal, flat) was
|
|
47168
|
-
* doing two jobs at once: at a 2135 ms poll gap it was far too tight to bridge
|
|
47169
|
-
* a walking subject, and at a 150 ms burst interval it was loose enough to hand
|
|
47170
|
-
* one subject's id to another 452 px away (track `27855a56`, a second car, at
|
|
47171
|
-
* IoU 0.058). One number, two opposite failures.
|
|
47172
|
-
*/
|
|
47173
|
-
var REACH_DIAGONALS_PER_SEC = 1.5;
|
|
47174
|
-
/**
|
|
47175
|
-
* Floor on the re-acquisition reach, in box diagonals.
|
|
47176
|
-
*
|
|
47177
|
-
* Deliberately TIGHTER than the 0.75 flat factor it replaces. At a 150 ms burst
|
|
47178
|
-
* interval the scaled term is 0.225 diagonals; the floor lifts it to 0.3, which
|
|
47179
|
-
* is 221 px on `27855a56`'s 738-px diagonal — comfortably below the 452 px that
|
|
47180
|
-
* bought that track a second car. A short gap is evidence that the subject
|
|
47181
|
-
* cannot have gone far, and the gate now says so.
|
|
47182
|
-
*/
|
|
47183
|
-
var REACH_MIN_DIAGONALS = .3;
|
|
47184
|
-
/**
|
|
47185
|
-
* Ceiling on the re-acquisition reach, in box diagonals.
|
|
47186
|
-
*
|
|
47187
|
-
* `REACH_DIAGONALS_PER_SEC × BUDGET_CEILING_MS`. A subject unseen for the full
|
|
47188
|
-
* `maxMissedMs` gets 1.5 diagonals of reach, not 6.
|
|
47189
|
-
*/
|
|
47190
|
-
var REACH_MAX_DIAGONALS = 1.5;
|
|
47191
|
-
function clampRange(value, min, max) {
|
|
47192
|
-
return Math.max(min, Math.min(max, value));
|
|
47193
|
-
}
|
|
47194
|
-
/**
|
|
47195
|
-
* How far a coasting box's velocity extrapolation may carry it on one axis.
|
|
47196
|
-
*
|
|
47197
|
-
* `side` is the box's width for the x drift and its height for the y drift, so
|
|
47198
|
-
* the allowance keeps the same meaning for a tall person and a wide car.
|
|
47199
|
-
*/
|
|
47200
|
-
function driftAllowancePx(side, elapsedMs) {
|
|
47201
|
-
const elapsed = Math.max(0, elapsedMs);
|
|
47202
|
-
return clampRange(side * 2 * (elapsed / 1e3), side * DRIFT_MIN_SIDES, side * 2);
|
|
47203
|
-
}
|
|
47204
|
-
/**
|
|
47205
|
-
* How far from where a track is EXPECTED a detection may sit and still be
|
|
47206
|
-
* claimed as that track by the loose (rescue / resurrection) reach clause.
|
|
47207
|
-
*
|
|
47208
|
-
* Scale-free: the budget is a multiple of the track's own diagonal, so it does
|
|
47209
|
-
* not have to know the frame size, the camera, or how far away the subject is.
|
|
47210
|
-
*/
|
|
47211
|
-
function reacquireReachPx(bbox, elapsedMs) {
|
|
47212
|
-
const diag = Math.hypot(bbox.w, bbox.h);
|
|
47213
|
-
const elapsed = Math.max(0, elapsedMs);
|
|
47214
|
-
return clampRange(diag * REACH_DIAGONALS_PER_SEC * (elapsed / 1e3), diag * REACH_MIN_DIAGONALS, diag * REACH_MAX_DIAGONALS);
|
|
47215
|
-
}
|
|
47216
|
-
/**
|
|
47217
|
-
* Resurrection reach, in box DIAGONALS per second of gap (D665).
|
|
47218
|
-
*
|
|
47219
|
-
* A corpse is reached only after `maxMissedMs` (4 s) without a box, so the
|
|
47220
|
-
* rescue rate above — which is at its 1.5-diagonal ceiling by 1 s — says
|
|
47221
|
-
* nothing useful here. A subject the detector has not boxed for four seconds
|
|
47222
|
-
* is, on this fleet's evidence, one that stood still or stood half-hidden,
|
|
47223
|
-
* not one striding across the frame (a striding person is detected): 0.25
|
|
47224
|
-
* diagonals per second is a slow walk for a mid-field person and a pause for
|
|
47225
|
-
* a near one. At the 4 s floor of the graveyard band it gives 1.0 diagonal;
|
|
47226
|
-
* camera 590's refused returns sat at 1.05–1.82× the old flat 0.75 reach,
|
|
47227
|
-
* i.e. 0.79–1.37 diagonals, which this covers from about 5.5 s of gap.
|
|
47228
|
-
*/
|
|
47229
|
-
var RESURRECTION_REACH_DIAGONALS_PER_SEC = .25;
|
|
47230
|
-
/** Never tighter than the flat `rescueCentroidFactor` default it replaces. */
|
|
47231
|
-
var RESURRECTION_REACH_MIN_DIAGONALS = .75;
|
|
47232
|
-
/**
|
|
47233
|
-
* How far from a corpse's last box a detection may sit and still resurrect it
|
|
47234
|
-
* on the proximity clause, for a corpse last seen `gapMs` ago.
|
|
47235
|
-
*/
|
|
47236
|
-
function resurrectionReachPx(bbox, gapMs) {
|
|
47237
|
-
const diag = Math.hypot(bbox.w, bbox.h);
|
|
47238
|
-
return clampRange(diag * RESURRECTION_REACH_DIAGONALS_PER_SEC * (Math.max(0, gapMs) / 1e3), diag * RESURRECTION_REACH_MIN_DIAGONALS, diag * 2);
|
|
47239
|
-
}
|
|
47240
47267
|
//#endregion
|
|
47241
47268
|
//#region src/pipeline-analytics/pipeline/tracker/rescue-growth.ts
|
|
47242
47269
|
/**
|
|
@@ -47689,10 +47716,10 @@ var SortTracker = class SortTracker {
|
|
|
47689
47716
|
}
|
|
47690
47717
|
/** Emit one salvage decision, subject to {@link LOOSE_BIND_LOG_SAMPLE_EVERY}.
|
|
47691
47718
|
* Nothing is computed when no observer is wired. */
|
|
47692
|
-
reportBind(stage, gate, track, det, timestamp) {
|
|
47719
|
+
reportBind(stage, gate, track, det, timestamp, refusal) {
|
|
47693
47720
|
const observer = this.bindObserver;
|
|
47694
47721
|
if (!observer) return;
|
|
47695
|
-
const bindCount = this.bindSampler.next(stage, gate);
|
|
47722
|
+
const bindCount = this.bindSampler.next(stage, gate, refusal);
|
|
47696
47723
|
if (bindCount === void 0) return;
|
|
47697
47724
|
const tc = bboxCentroid(track.bbox);
|
|
47698
47725
|
const dc = bboxCentroid(det.bbox);
|
|
@@ -47726,6 +47753,7 @@ var SortTracker = class SortTracker {
|
|
|
47726
47753
|
frameHeight: this.frameHeightPx,
|
|
47727
47754
|
dtMs: timestamp - track.lastSeen,
|
|
47728
47755
|
trackAgeMs: timestamp - track.firstSeen,
|
|
47756
|
+
...refusal !== void 0 ? { refusal } : {},
|
|
47729
47757
|
bindCount,
|
|
47730
47758
|
sampledEvery: 50
|
|
47731
47759
|
});
|
|
@@ -48066,15 +48094,45 @@ var SortTracker = class SortTracker {
|
|
|
48066
48094
|
* was written for; not of the pass as it runs: on camera 590 (2026-09-27)
|
|
48067
48095
|
* 38% of all re-splits were PEOPLE returning after 4–12 s, further than the
|
|
48068
48096
|
* flat gate, with frames flowing the whole time. `resurrectionTimeScaled`
|
|
48069
|
-
* off restores the flat gate
|
|
48097
|
+
* off restores the flat gate — and a `vehicle` corpse always has it (D678,
|
|
48098
|
+
* {@link resurrectionTimeScaledFor}).
|
|
48070
48099
|
*/
|
|
48071
48100
|
reacquireReach(track, timestamp, pass) {
|
|
48072
48101
|
const diag = Math.hypot(track.bbox.w, track.bbox.h);
|
|
48073
|
-
if (pass === "resurrection") return this.
|
|
48102
|
+
if (pass === "resurrection") return this.resurrectionTimeScaledFor(track.class) ? resurrectionReachPx(track.bbox, timestamp - track.lastSeen) : this.config.rescueCentroidFactor * diag;
|
|
48074
48103
|
if (!this.config.reacquireTimeScaled) return this.config.rescueCentroidFactor * diag;
|
|
48075
48104
|
return reacquireReachPx(track.bbox, timestamp - track.lastSeen);
|
|
48076
48105
|
}
|
|
48077
48106
|
/**
|
|
48107
|
+
* Whether D665's graveyard gate — the reach that grows with the gap, and the
|
|
48108
|
+
* plausible-birth veto lifted for a sole subject — applies to a corpse of
|
|
48109
|
+
* `cls`. People and animals only (D678): a car gone for ten seconds did not
|
|
48110
|
+
* stand half-hidden, the next one arrived.
|
|
48111
|
+
*/
|
|
48112
|
+
resurrectionTimeScaledFor(cls) {
|
|
48113
|
+
return this.config.resurrectionTimeScaled && LINGERING_SUBJECT_CLASSES.has(cls);
|
|
48114
|
+
}
|
|
48115
|
+
/**
|
|
48116
|
+
* The size check a PROXIMITY-only resurrection needs (D678), for a pair the
|
|
48117
|
+
* loose gate accepted on its `centroid` clause and the plausible-birth veto
|
|
48118
|
+
* let through.
|
|
48119
|
+
*
|
|
48120
|
+
* The `centroid` clause says where a box is and nothing about what it is;
|
|
48121
|
+
* a box more than {@link RESURRECTION_MAX_AREA_RATIO} larger or smaller than
|
|
48122
|
+
* the corpse's last box is refused on it, and the refusal is returned so the
|
|
48123
|
+
* caller can name it. The pair may still bind on `growth`, the partial-crop
|
|
48124
|
+
* rule whose whole subject IS a size change and which `looseMatch` would
|
|
48125
|
+
* have reached had the centroid clause not answered first. With the default
|
|
48126
|
+
* coast budget that never happens — `growth` refuses a pair more than
|
|
48127
|
+
* `GROWTH_MAX_GAP_MS` (1 s) apart and a corpse has been unseen for
|
|
48128
|
+
* `maxMissedMs` (4 s) — but `maxMissedMs` is a per-camera setting down to 0.
|
|
48129
|
+
*/
|
|
48130
|
+
proximitySizeVerdict(corpse, det, timestamp) {
|
|
48131
|
+
if (boxAreaRatio(corpse.bbox, det.bbox) <= 2.5) return "centroid";
|
|
48132
|
+
if (growthAdjacentMatch(corpse.bbox, det.bbox, this.growthContext(corpse, timestamp))) return "growth";
|
|
48133
|
+
return "size-incompatible";
|
|
48134
|
+
}
|
|
48135
|
+
/**
|
|
48078
48136
|
* Whether a REACH bind would be unambiguous: this coasting track is the only
|
|
48079
48137
|
* subject of its association group the scene holds, and `det` is the only box
|
|
48080
48138
|
* of that group on the frame.
|
|
@@ -48488,13 +48546,26 @@ var SortTracker = class SortTracker {
|
|
|
48488
48546
|
let refused;
|
|
48489
48547
|
let refusedGate = "iou";
|
|
48490
48548
|
let refusedScore = -1;
|
|
48549
|
+
let refusedBy = "crowd-guard";
|
|
48491
48550
|
for (const lost of this.lostTracks) {
|
|
48492
48551
|
if (!lost.resurrectable) continue;
|
|
48493
48552
|
if (timestamp - lost.lostAt > this.config.resurrectionWindowMs) continue;
|
|
48494
|
-
const
|
|
48495
|
-
if (
|
|
48496
|
-
const
|
|
48497
|
-
if (
|
|
48553
|
+
const loose = this.looseMatch(lost, det, timestamp, "resurrection");
|
|
48554
|
+
if (loose === null) continue;
|
|
48555
|
+
const vetoApplies = loose === "centroid" && this.isPlausibleBirth(det, frameArea);
|
|
48556
|
+
if (vetoApplies && !(this.resurrectionTimeScaledFor(lost.class) && this.isSoleSubjectResurrection(lost, det, detections, surviving, lowScore))) continue;
|
|
48557
|
+
const gate = loose === "centroid" ? this.proximitySizeVerdict(lost, det, timestamp) : loose;
|
|
48558
|
+
if (gate === "size-incompatible") {
|
|
48559
|
+
const score = iou$2(lost.bbox, det.bbox);
|
|
48560
|
+
if (score > refusedScore) {
|
|
48561
|
+
refused = lost;
|
|
48562
|
+
refusedGate = "centroid";
|
|
48563
|
+
refusedScore = score;
|
|
48564
|
+
refusedBy = "size-incompatible";
|
|
48565
|
+
}
|
|
48566
|
+
continue;
|
|
48567
|
+
}
|
|
48568
|
+
const vetoLifted = vetoApplies && gate === "centroid";
|
|
48498
48569
|
const score = iou$2(lost.bbox, det.bbox);
|
|
48499
48570
|
const peers = this.sameClassPeerCount(lost, surviving);
|
|
48500
48571
|
if (this.config.resurrectionCrowdGuard && peers > 0 && score < this.config.resurrectionCrowdedIouThreshold) {
|
|
@@ -48502,6 +48573,7 @@ var SortTracker = class SortTracker {
|
|
|
48502
48573
|
refused = lost;
|
|
48503
48574
|
refusedGate = gate;
|
|
48504
48575
|
refusedScore = score;
|
|
48576
|
+
refusedBy = "crowd-guard";
|
|
48505
48577
|
}
|
|
48506
48578
|
continue;
|
|
48507
48579
|
}
|
|
@@ -48514,7 +48586,7 @@ var SortTracker = class SortTracker {
|
|
|
48514
48586
|
}
|
|
48515
48587
|
}
|
|
48516
48588
|
if (!best) {
|
|
48517
|
-
if (refused !== void 0) this.reportBind("resurrection-refused", refusedGate, refused, det, timestamp);
|
|
48589
|
+
if (refused !== void 0) this.reportBind("resurrection-refused", refusedGate, refused, det, timestamp, refusedBy);
|
|
48518
48590
|
continue;
|
|
48519
48591
|
}
|
|
48520
48592
|
resurrectedScene.set(best, bestPeers > 0 ? "multi-subject" : bestVetoLifted ? "sole-subject-veto-lifted" : "sole-subject");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-post-analysis",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.297",
|
|
4
4
|
"description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|