@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.
@@ -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 (!LOW_SCORE_CONTINUATION_CLASSES.has(d.macroClass)) continue;
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 total when
47162
- * this decision should be reported, or `undefined` when it is sampled out. */
47163
- next(stage, gate) {
47164
- const key = `${stage}|${gate}`;
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.config.resurrectionTimeScaled ? resurrectionReachPx(track.bbox, timestamp - track.lastSeen) : this.config.rescueCentroidFactor * diag;
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 gate = this.looseMatch(lost, det, timestamp, "resurrection");
48520
- if (gate === null) continue;
48521
- const vetoLifted = gate === "centroid" && this.isPlausibleBirth(det, frameArea);
48522
- if (vetoLifted && !(this.config.resurrectionTimeScaled && this.isSoleSubjectResurrection(lost, det, detections, surviving, lowScore))) continue;
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 (!LOW_SCORE_CONTINUATION_CLASSES.has(d.macroClass)) continue;
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 total when
47137
- * this decision should be reported, or `undefined` when it is sampled out. */
47138
- next(stage, gate) {
47139
- const key = `${stage}|${gate}`;
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.config.resurrectionTimeScaled ? resurrectionReachPx(track.bbox, timestamp - track.lastSeen) : this.config.rescueCentroidFactor * diag;
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 gate = this.looseMatch(lost, det, timestamp, "resurrection");
48495
- if (gate === null) continue;
48496
- const vetoLifted = gate === "centroid" && this.isPlausibleBirth(det, frameArea);
48497
- if (vetoLifted && !(this.config.resurrectionTimeScaled && this.isSoleSubjectResurrection(lost, det, detections, surviving, lowScore))) continue;
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.296",
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",