@camstack/addon-post-analysis 1.2.198 → 1.2.200
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 +152 -35
- package/dist/pipeline-analytics/index.mjs +152 -35
- package/package.json +1 -1
|
@@ -12814,6 +12814,23 @@ function recordToRow$2(subject, data) {
|
|
|
12814
12814
|
};
|
|
12815
12815
|
}
|
|
12816
12816
|
//#endregion
|
|
12817
|
+
//#region src/notification-center/no-match-report-key.ts
|
|
12818
|
+
/**
|
|
12819
|
+
* Stand-in for "this subject named no class". A distinct token rather than an
|
|
12820
|
+
* empty string so a class-less subject occupies its own bucket instead of
|
|
12821
|
+
* sharing one with every other class-less kind by accident.
|
|
12822
|
+
*/
|
|
12823
|
+
var NO_CLASSES = "∅";
|
|
12824
|
+
/**
|
|
12825
|
+
* The bucket key. Stable across calls, insensitive to class ORDER, DUPLICATES
|
|
12826
|
+
* and CASE — a detector that re-orders its class list, repeats one, or changes
|
|
12827
|
+
* its capitalisation must not fork the bucket and double the log volume.
|
|
12828
|
+
*/
|
|
12829
|
+
function noMatchReportKey(subject) {
|
|
12830
|
+
const classes = [...new Set(subject.classNames.map((c) => c.toLowerCase()))].sort();
|
|
12831
|
+
return `${subject.deviceId}|${subject.kind}|${classes.length > 0 ? classes.join(",") : NO_CLASSES}`;
|
|
12832
|
+
}
|
|
12833
|
+
//#endregion
|
|
12817
12834
|
//#region src/notification-center/occupancy-watcher.ts
|
|
12818
12835
|
/** Sentinel key segments for the "no zone" (whole-frame) and "no class" scopes. */
|
|
12819
12836
|
var FRAME_SCOPE = "@frame";
|
|
@@ -21945,10 +21962,15 @@ var NotificationCenter = class NotificationCenter {
|
|
|
21945
21962
|
* which leaves those notifications text-only rather than broken.
|
|
21946
21963
|
*/
|
|
21947
21964
|
stills;
|
|
21948
|
-
/**
|
|
21965
|
+
/**
|
|
21966
|
+
* Rate limit for the "matched NO rule" report — see `reportNoMatch`. Keyed by
|
|
21967
|
+
* {@link noMatchReportKey} (device + subject kind + class set), NOT by device:
|
|
21968
|
+
* a person's verdict must never consume the window a cat's verdict needed on
|
|
21969
|
+
* the same camera ([D390]).
|
|
21970
|
+
*/
|
|
21949
21971
|
lastNoMatchReportAt = /* @__PURE__ */ new Map();
|
|
21950
21972
|
noMatchSuppressed = /* @__PURE__ */ new Map();
|
|
21951
|
-
/** Same rate-limit pair for the per-camera mute drop — see `reportMutedDrop`. */
|
|
21973
|
+
/** Same rate-limit pair, same key, for the per-camera mute drop — see `reportMutedDrop`. */
|
|
21952
21974
|
lastMutedReportAt = /* @__PURE__ */ new Map();
|
|
21953
21975
|
mutedSuppressed = /* @__PURE__ */ new Map();
|
|
21954
21976
|
/**
|
|
@@ -24719,14 +24741,19 @@ var NotificationCenter = class NotificationCenter {
|
|
|
24719
24741
|
* is identical: "why did this camera not notify me?".
|
|
24720
24742
|
*/
|
|
24721
24743
|
reportMutedDrop(subject, kind, now) {
|
|
24722
|
-
const
|
|
24723
|
-
|
|
24744
|
+
const bucket = noMatchReportKey({
|
|
24745
|
+
deviceId: subject.deviceId,
|
|
24746
|
+
kind,
|
|
24747
|
+
classNames: subject.classNames
|
|
24748
|
+
});
|
|
24749
|
+
const last = this.lastMutedReportAt.get(bucket) ?? 0;
|
|
24750
|
+
const suppressed = this.mutedSuppressed.get(bucket) ?? 0;
|
|
24724
24751
|
if (now - last < NO_MATCH_REPORT_INTERVAL_MS) {
|
|
24725
|
-
this.mutedSuppressed.set(
|
|
24752
|
+
this.mutedSuppressed.set(bucket, suppressed + 1);
|
|
24726
24753
|
return;
|
|
24727
24754
|
}
|
|
24728
|
-
this.lastMutedReportAt.set(
|
|
24729
|
-
this.mutedSuppressed.set(
|
|
24755
|
+
this.lastMutedReportAt.set(bucket, now);
|
|
24756
|
+
this.mutedSuppressed.set(bucket, 0);
|
|
24730
24757
|
this.logger.info("notifications muted for this camera — event dropped before rule evaluation", {
|
|
24731
24758
|
tags: { deviceId: subject.deviceId },
|
|
24732
24759
|
meta: {
|
|
@@ -24738,16 +24765,23 @@ var NotificationCenter = class NotificationCenter {
|
|
|
24738
24765
|
}
|
|
24739
24766
|
});
|
|
24740
24767
|
}
|
|
24741
|
-
/** See the call site. Bounded to one line per
|
|
24768
|
+
/** See the call site. Bounded to one line per {@link noMatchReportKey} bucket
|
|
24769
|
+
* per window — device, subject kind and class set, so one subject's verdict
|
|
24770
|
+
* cannot stand in for another's on the same camera ([D390]). */
|
|
24742
24771
|
reportNoMatch(subject, kind, rejections, now) {
|
|
24743
|
-
const
|
|
24744
|
-
|
|
24772
|
+
const bucket = noMatchReportKey({
|
|
24773
|
+
deviceId: subject.deviceId,
|
|
24774
|
+
kind,
|
|
24775
|
+
classNames: subject.classNames
|
|
24776
|
+
});
|
|
24777
|
+
const last = this.lastNoMatchReportAt.get(bucket) ?? 0;
|
|
24778
|
+
const suppressed = this.noMatchSuppressed.get(bucket) ?? 0;
|
|
24745
24779
|
if (now - last < NO_MATCH_REPORT_INTERVAL_MS) {
|
|
24746
|
-
this.noMatchSuppressed.set(
|
|
24780
|
+
this.noMatchSuppressed.set(bucket, suppressed + 1);
|
|
24747
24781
|
return;
|
|
24748
24782
|
}
|
|
24749
|
-
this.lastNoMatchReportAt.set(
|
|
24750
|
-
this.noMatchSuppressed.set(
|
|
24783
|
+
this.lastNoMatchReportAt.set(bucket, now);
|
|
24784
|
+
this.noMatchSuppressed.set(bucket, 0);
|
|
24751
24785
|
this.logger.info("event matched NO rule", {
|
|
24752
24786
|
tags: { deviceId: subject.deviceId },
|
|
24753
24787
|
meta: {
|
|
@@ -34027,6 +34061,22 @@ function pairRiders(detections) {
|
|
|
34027
34061
|
}
|
|
34028
34062
|
return pairs;
|
|
34029
34063
|
}
|
|
34064
|
+
/** Per-(stage, gate) counter + sampling decision. One instance per tracker, so
|
|
34065
|
+
* the counts are per camera and per detection source. */
|
|
34066
|
+
var LooseBindSampler = class {
|
|
34067
|
+
counts = /* @__PURE__ */ new Map();
|
|
34068
|
+
/** Increment the (stage, gate) counter and answer with the running total when
|
|
34069
|
+
* this decision should be reported, or `undefined` when it is sampled out. */
|
|
34070
|
+
next(stage, gate) {
|
|
34071
|
+
const key = `${stage}|${gate}`;
|
|
34072
|
+
const count = (this.counts.get(key) ?? 0) + 1;
|
|
34073
|
+
this.counts.set(key, count);
|
|
34074
|
+
if (count === 1 || count % 50 === 0) return count;
|
|
34075
|
+
}
|
|
34076
|
+
reset() {
|
|
34077
|
+
this.counts.clear();
|
|
34078
|
+
}
|
|
34079
|
+
};
|
|
34030
34080
|
//#endregion
|
|
34031
34081
|
//#region src/pipeline-analytics/pipeline/tracker/rescue-growth.ts
|
|
34032
34082
|
/**
|
|
@@ -34084,22 +34134,6 @@ function growthAdjacentMatch(track, det, ctx) {
|
|
|
34084
34134
|
const dc = bboxCentroid(det);
|
|
34085
34135
|
return Math.hypot(tc.x - dc.x, tc.y - dc.y) <= GROWTH_CENTROID_FACTOR * Math.hypot(track.w, track.h);
|
|
34086
34136
|
}
|
|
34087
|
-
/** Per-(stage, gate) counter + sampling decision. One instance per tracker, so
|
|
34088
|
-
* the counts are per camera and per detection source. */
|
|
34089
|
-
var LooseBindSampler = class {
|
|
34090
|
-
counts = /* @__PURE__ */ new Map();
|
|
34091
|
-
/** Increment the (stage, gate) counter and answer with the running total when
|
|
34092
|
-
* this decision should be reported, or `undefined` when it is sampled out. */
|
|
34093
|
-
next(stage, gate) {
|
|
34094
|
-
const key = `${stage}|${gate}`;
|
|
34095
|
-
const count = (this.counts.get(key) ?? 0) + 1;
|
|
34096
|
-
this.counts.set(key, count);
|
|
34097
|
-
if (count === 1 || count % 50 === 0) return count;
|
|
34098
|
-
}
|
|
34099
|
-
reset() {
|
|
34100
|
-
this.counts.clear();
|
|
34101
|
-
}
|
|
34102
|
-
};
|
|
34103
34137
|
/** Per-reason counter + sampling decision. One instance per tracker, so the
|
|
34104
34138
|
* counts are per camera and per detection source. */
|
|
34105
34139
|
var SpawnRefusalSampler = class {
|
|
@@ -34243,7 +34277,35 @@ var SortTracker = class SortTracker {
|
|
|
34243
34277
|
againstTrackClass: extra.against.class
|
|
34244
34278
|
} : {},
|
|
34245
34279
|
refusalCount,
|
|
34246
|
-
sampledEvery: 10
|
|
34280
|
+
sampledEvery: 10,
|
|
34281
|
+
...extra.droppedTrackId !== void 0 ? { droppedTrackId: extra.droppedTrackId } : {},
|
|
34282
|
+
...extra.trackLifeMs !== void 0 ? { trackLifeMs: extra.trackLifeMs } : {}
|
|
34283
|
+
});
|
|
34284
|
+
}
|
|
34285
|
+
/**
|
|
34286
|
+
* Report a TRACK that ends having emitted nothing — the last silent branch
|
|
34287
|
+
* on the detection→position path (D391).
|
|
34288
|
+
*
|
|
34289
|
+
* The subject is the track, not one box, so the record carries the track's
|
|
34290
|
+
* own last class/score/bbox: those ARE the detection that fed it last, and
|
|
34291
|
+
* they are what an operator matches against the recovery line that produced
|
|
34292
|
+
* it. Reported once (`lossReported`) and only where the track can no longer
|
|
34293
|
+
* come back — a graveyarded track may still resurrect, and a line about work
|
|
34294
|
+
* that was not lost is worse than no line.
|
|
34295
|
+
*/
|
|
34296
|
+
reportTrackLoss(track) {
|
|
34297
|
+
if (track.emitted || track.lossReported) return;
|
|
34298
|
+
track.lossReported = true;
|
|
34299
|
+
this.reportRefusal("unconfirmed-spawn-lost", {
|
|
34300
|
+
class: track.class,
|
|
34301
|
+
originalClass: track.originalClass,
|
|
34302
|
+
score: track.score,
|
|
34303
|
+
bbox: track.bbox
|
|
34304
|
+
}, {
|
|
34305
|
+
threshold: this.config.minHits,
|
|
34306
|
+
measured: track.hits,
|
|
34307
|
+
droppedTrackId: track.id,
|
|
34308
|
+
trackLifeMs: track.lastSeen - track.firstSeen
|
|
34247
34309
|
});
|
|
34248
34310
|
}
|
|
34249
34311
|
/** Emit one salvage decision, subject to {@link LOOSE_BIND_LOG_SAMPLE_EVERY}.
|
|
@@ -34438,7 +34500,22 @@ var SortTracker = class SortTracker {
|
|
|
34438
34500
|
}
|
|
34439
34501
|
const streak = (this.dupStreaks.get(key) ?? 0) + 1;
|
|
34440
34502
|
if (streak >= this.config.dedupMergeFrames) {
|
|
34441
|
-
|
|
34503
|
+
const loser = SortTracker.duplicateLoser(a, b);
|
|
34504
|
+
const winner = loser === a ? b : a;
|
|
34505
|
+
dropIds.add(loser.id);
|
|
34506
|
+
this.reportRefusal("merged-duplicate", {
|
|
34507
|
+
class: loser.class,
|
|
34508
|
+
originalClass: loser.originalClass,
|
|
34509
|
+
score: loser.score,
|
|
34510
|
+
bbox: loser.bbox
|
|
34511
|
+
}, {
|
|
34512
|
+
threshold: this.config.dedupMergeIou,
|
|
34513
|
+
measured: iou$3(a.bbox, b.bbox),
|
|
34514
|
+
against: winner,
|
|
34515
|
+
droppedTrackId: loser.id,
|
|
34516
|
+
trackLifeMs: loser.lastSeen - loser.firstSeen
|
|
34517
|
+
});
|
|
34518
|
+
loser.lossReported = true;
|
|
34442
34519
|
this.dupStreaks.delete(key);
|
|
34443
34520
|
} else this.dupStreaks.set(key, streak);
|
|
34444
34521
|
}
|
|
@@ -34620,6 +34697,7 @@ var SortTracker = class SortTracker {
|
|
|
34620
34697
|
track.lost = true;
|
|
34621
34698
|
track.lostAt = timestamp;
|
|
34622
34699
|
track.resurrectable = false;
|
|
34700
|
+
this.reportTrackLoss(track);
|
|
34623
34701
|
this.lostTracks.push(track);
|
|
34624
34702
|
} else alive.push(track);
|
|
34625
34703
|
this.tracks = alive;
|
|
@@ -34631,11 +34709,15 @@ var SortTracker = class SortTracker {
|
|
|
34631
34709
|
track.lost = true;
|
|
34632
34710
|
track.lostAt = track.lastSeen;
|
|
34633
34711
|
track.resurrectable = false;
|
|
34712
|
+
this.reportTrackLoss(track);
|
|
34634
34713
|
this.lostTracks.push(track);
|
|
34635
34714
|
} else alive.push(track);
|
|
34636
34715
|
this.tracks = alive;
|
|
34637
34716
|
}
|
|
34638
|
-
|
|
34717
|
+
const stillResurrectable = [];
|
|
34718
|
+
for (const t of this.lostTracks) if (timestamp - t.lostAt <= this.config.resurrectionWindowMs) stillResurrectable.push(t);
|
|
34719
|
+
else this.reportTrackLoss(t);
|
|
34720
|
+
this.lostTracks = stillResurrectable;
|
|
34639
34721
|
const used = /* @__PURE__ */ new Set();
|
|
34640
34722
|
const matchedTracks = /* @__PURE__ */ new Set();
|
|
34641
34723
|
const matched = /* @__PURE__ */ new Map();
|
|
@@ -34833,12 +34915,15 @@ var SortTracker = class SortTracker {
|
|
|
34833
34915
|
lost: false,
|
|
34834
34916
|
lostAt: 0,
|
|
34835
34917
|
resurrectable: true,
|
|
34836
|
-
classVotes: new Map([[det.class, det.score > 0 ? det.score : Number.EPSILON]])
|
|
34918
|
+
classVotes: new Map([[det.class, det.score > 0 ? det.score : Number.EPSILON]]),
|
|
34919
|
+
emitted: false,
|
|
34920
|
+
lossReported: false
|
|
34837
34921
|
});
|
|
34838
34922
|
}
|
|
34839
34923
|
this.tracks = surviving;
|
|
34840
34924
|
this.resolveDuplicates();
|
|
34841
34925
|
return this.tracks.filter((t) => this.isConfirmedForEmit(t)).map((t) => {
|
|
34926
|
+
t.emitted = true;
|
|
34842
34927
|
const scene = resurrectedScene.get(t);
|
|
34843
34928
|
return {
|
|
34844
34929
|
class: this.resolveReportedClass(t),
|
|
@@ -35206,6 +35291,10 @@ var FrameProcessor = class {
|
|
|
35206
35291
|
*/
|
|
35207
35292
|
reportedRefusalByTrack = /* @__PURE__ */ new Map();
|
|
35208
35293
|
zoneEngine = new ZoneEngine();
|
|
35294
|
+
/** Sink + sampler for the drops this stage owns (the detection-stage zone
|
|
35295
|
+
* filter). Shares the tracker's vocabulary — see {@link SpawnRefusalReason}. */
|
|
35296
|
+
spawnRefusalObserver = null;
|
|
35297
|
+
zoneRefusalSampler = new SpawnRefusalSampler();
|
|
35209
35298
|
/** Optional stationary-object gate (parked-object suppression). Null until
|
|
35210
35299
|
* the addon wires it via {@link setStationaryGate}. */
|
|
35211
35300
|
stationaryGate = null;
|
|
@@ -35300,6 +35389,7 @@ var FrameProcessor = class {
|
|
|
35300
35389
|
*/
|
|
35301
35390
|
setSpawnRefusalObserver(observer) {
|
|
35302
35391
|
this.tracker.setSpawnRefusalObserver(observer);
|
|
35392
|
+
this.spawnRefusalObserver = observer;
|
|
35303
35393
|
}
|
|
35304
35394
|
setBindObserver(observer) {
|
|
35305
35395
|
this.tracker.setBindObserver(observer);
|
|
@@ -35309,6 +35399,32 @@ var FrameProcessor = class {
|
|
|
35309
35399
|
dropTrack(trackId) {
|
|
35310
35400
|
this.tracker.dropTrack(trackId);
|
|
35311
35401
|
}
|
|
35402
|
+
/**
|
|
35403
|
+
* Say which detections a detection-stage zone rule excluded (D391).
|
|
35404
|
+
*
|
|
35405
|
+
* `filterDetections` has always ANSWERED this — it returns `excluded` — and
|
|
35406
|
+
* this stage dropped the answer on the floor, so a camera whose subject the
|
|
35407
|
+
* rules exclude was indistinguishable in the log from a camera whose tracker
|
|
35408
|
+
* ate it. Sampled like every other refusal: an excluded box is excluded on
|
|
35409
|
+
* every frame of a passage.
|
|
35410
|
+
*/
|
|
35411
|
+
reportZoneFiltered(excluded) {
|
|
35412
|
+
const observer = this.spawnRefusalObserver;
|
|
35413
|
+
if (!observer || excluded.length === 0) return;
|
|
35414
|
+
for (const fd of excluded) {
|
|
35415
|
+
const refusalCount = this.zoneRefusalSampler.next("zone-filtered");
|
|
35416
|
+
if (refusalCount === void 0) continue;
|
|
35417
|
+
observer({
|
|
35418
|
+
reason: "zone-filtered",
|
|
35419
|
+
detClass: fd.detection.class,
|
|
35420
|
+
detScore: fd.detection.score,
|
|
35421
|
+
bbox: fd.detection.bbox,
|
|
35422
|
+
...fd.detection.nativeRecovery !== void 0 ? { nativeRecoveryScore: fd.detection.nativeRecovery.score } : {},
|
|
35423
|
+
refusalCount,
|
|
35424
|
+
sampledEvery: 10
|
|
35425
|
+
});
|
|
35426
|
+
}
|
|
35427
|
+
}
|
|
35312
35428
|
process(input) {
|
|
35313
35429
|
const { timestamp, frame } = input;
|
|
35314
35430
|
const frameWidth = frame.width;
|
|
@@ -35394,7 +35510,8 @@ var FrameProcessor = class {
|
|
|
35394
35510
|
}
|
|
35395
35511
|
});
|
|
35396
35512
|
}
|
|
35397
|
-
const { passed } = this.zoneEngine.filterDetections(flatDetections, this.zones, this.detectionRules, frameWidth, frameHeight, (fd) => fd.detection.class);
|
|
35513
|
+
const { passed, excluded } = this.zoneEngine.filterDetections(flatDetections, this.zones, this.detectionRules, frameWidth, frameHeight, (fd) => fd.detection.class);
|
|
35514
|
+
this.reportZoneFiltered(excluded);
|
|
35398
35515
|
let filteredDetections = passed.map((fd) => fd.detection);
|
|
35399
35516
|
const riderPairs = pairRiders(filteredDetections.map((d, i) => ({
|
|
35400
35517
|
id: String(i),
|
|
@@ -74304,7 +74421,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
74304
74421
|
});
|
|
74305
74422
|
});
|
|
74306
74423
|
p.setSpawnRefusalObserver((record) => {
|
|
74307
|
-
this.ctx.logger.info("tracker: detection dropped —
|
|
74424
|
+
this.ctx.logger.info("tracker: detection dropped — nothing downstream received it", {
|
|
74308
74425
|
tags: { deviceId },
|
|
74309
74426
|
meta: {
|
|
74310
74427
|
source,
|
|
@@ -12807,6 +12807,23 @@ function recordToRow$2(subject, data) {
|
|
|
12807
12807
|
};
|
|
12808
12808
|
}
|
|
12809
12809
|
//#endregion
|
|
12810
|
+
//#region src/notification-center/no-match-report-key.ts
|
|
12811
|
+
/**
|
|
12812
|
+
* Stand-in for "this subject named no class". A distinct token rather than an
|
|
12813
|
+
* empty string so a class-less subject occupies its own bucket instead of
|
|
12814
|
+
* sharing one with every other class-less kind by accident.
|
|
12815
|
+
*/
|
|
12816
|
+
var NO_CLASSES = "∅";
|
|
12817
|
+
/**
|
|
12818
|
+
* The bucket key. Stable across calls, insensitive to class ORDER, DUPLICATES
|
|
12819
|
+
* and CASE — a detector that re-orders its class list, repeats one, or changes
|
|
12820
|
+
* its capitalisation must not fork the bucket and double the log volume.
|
|
12821
|
+
*/
|
|
12822
|
+
function noMatchReportKey(subject) {
|
|
12823
|
+
const classes = [...new Set(subject.classNames.map((c) => c.toLowerCase()))].sort();
|
|
12824
|
+
return `${subject.deviceId}|${subject.kind}|${classes.length > 0 ? classes.join(",") : NO_CLASSES}`;
|
|
12825
|
+
}
|
|
12826
|
+
//#endregion
|
|
12810
12827
|
//#region src/notification-center/occupancy-watcher.ts
|
|
12811
12828
|
/** Sentinel key segments for the "no zone" (whole-frame) and "no class" scopes. */
|
|
12812
12829
|
var FRAME_SCOPE = "@frame";
|
|
@@ -21923,10 +21940,15 @@ var NotificationCenter = class NotificationCenter {
|
|
|
21923
21940
|
* which leaves those notifications text-only rather than broken.
|
|
21924
21941
|
*/
|
|
21925
21942
|
stills;
|
|
21926
|
-
/**
|
|
21943
|
+
/**
|
|
21944
|
+
* Rate limit for the "matched NO rule" report — see `reportNoMatch`. Keyed by
|
|
21945
|
+
* {@link noMatchReportKey} (device + subject kind + class set), NOT by device:
|
|
21946
|
+
* a person's verdict must never consume the window a cat's verdict needed on
|
|
21947
|
+
* the same camera ([D390]).
|
|
21948
|
+
*/
|
|
21927
21949
|
lastNoMatchReportAt = /* @__PURE__ */ new Map();
|
|
21928
21950
|
noMatchSuppressed = /* @__PURE__ */ new Map();
|
|
21929
|
-
/** Same rate-limit pair for the per-camera mute drop — see `reportMutedDrop`. */
|
|
21951
|
+
/** Same rate-limit pair, same key, for the per-camera mute drop — see `reportMutedDrop`. */
|
|
21930
21952
|
lastMutedReportAt = /* @__PURE__ */ new Map();
|
|
21931
21953
|
mutedSuppressed = /* @__PURE__ */ new Map();
|
|
21932
21954
|
/**
|
|
@@ -24697,14 +24719,19 @@ var NotificationCenter = class NotificationCenter {
|
|
|
24697
24719
|
* is identical: "why did this camera not notify me?".
|
|
24698
24720
|
*/
|
|
24699
24721
|
reportMutedDrop(subject, kind, now) {
|
|
24700
|
-
const
|
|
24701
|
-
|
|
24722
|
+
const bucket = noMatchReportKey({
|
|
24723
|
+
deviceId: subject.deviceId,
|
|
24724
|
+
kind,
|
|
24725
|
+
classNames: subject.classNames
|
|
24726
|
+
});
|
|
24727
|
+
const last = this.lastMutedReportAt.get(bucket) ?? 0;
|
|
24728
|
+
const suppressed = this.mutedSuppressed.get(bucket) ?? 0;
|
|
24702
24729
|
if (now - last < NO_MATCH_REPORT_INTERVAL_MS) {
|
|
24703
|
-
this.mutedSuppressed.set(
|
|
24730
|
+
this.mutedSuppressed.set(bucket, suppressed + 1);
|
|
24704
24731
|
return;
|
|
24705
24732
|
}
|
|
24706
|
-
this.lastMutedReportAt.set(
|
|
24707
|
-
this.mutedSuppressed.set(
|
|
24733
|
+
this.lastMutedReportAt.set(bucket, now);
|
|
24734
|
+
this.mutedSuppressed.set(bucket, 0);
|
|
24708
24735
|
this.logger.info("notifications muted for this camera — event dropped before rule evaluation", {
|
|
24709
24736
|
tags: { deviceId: subject.deviceId },
|
|
24710
24737
|
meta: {
|
|
@@ -24716,16 +24743,23 @@ var NotificationCenter = class NotificationCenter {
|
|
|
24716
24743
|
}
|
|
24717
24744
|
});
|
|
24718
24745
|
}
|
|
24719
|
-
/** See the call site. Bounded to one line per
|
|
24746
|
+
/** See the call site. Bounded to one line per {@link noMatchReportKey} bucket
|
|
24747
|
+
* per window — device, subject kind and class set, so one subject's verdict
|
|
24748
|
+
* cannot stand in for another's on the same camera ([D390]). */
|
|
24720
24749
|
reportNoMatch(subject, kind, rejections, now) {
|
|
24721
|
-
const
|
|
24722
|
-
|
|
24750
|
+
const bucket = noMatchReportKey({
|
|
24751
|
+
deviceId: subject.deviceId,
|
|
24752
|
+
kind,
|
|
24753
|
+
classNames: subject.classNames
|
|
24754
|
+
});
|
|
24755
|
+
const last = this.lastNoMatchReportAt.get(bucket) ?? 0;
|
|
24756
|
+
const suppressed = this.noMatchSuppressed.get(bucket) ?? 0;
|
|
24723
24757
|
if (now - last < NO_MATCH_REPORT_INTERVAL_MS) {
|
|
24724
|
-
this.noMatchSuppressed.set(
|
|
24758
|
+
this.noMatchSuppressed.set(bucket, suppressed + 1);
|
|
24725
24759
|
return;
|
|
24726
24760
|
}
|
|
24727
|
-
this.lastNoMatchReportAt.set(
|
|
24728
|
-
this.noMatchSuppressed.set(
|
|
24761
|
+
this.lastNoMatchReportAt.set(bucket, now);
|
|
24762
|
+
this.noMatchSuppressed.set(bucket, 0);
|
|
24729
24763
|
this.logger.info("event matched NO rule", {
|
|
24730
24764
|
tags: { deviceId: subject.deviceId },
|
|
24731
24765
|
meta: {
|
|
@@ -34002,6 +34036,22 @@ function pairRiders(detections) {
|
|
|
34002
34036
|
}
|
|
34003
34037
|
return pairs;
|
|
34004
34038
|
}
|
|
34039
|
+
/** Per-(stage, gate) counter + sampling decision. One instance per tracker, so
|
|
34040
|
+
* the counts are per camera and per detection source. */
|
|
34041
|
+
var LooseBindSampler = class {
|
|
34042
|
+
counts = /* @__PURE__ */ new Map();
|
|
34043
|
+
/** Increment the (stage, gate) counter and answer with the running total when
|
|
34044
|
+
* this decision should be reported, or `undefined` when it is sampled out. */
|
|
34045
|
+
next(stage, gate) {
|
|
34046
|
+
const key = `${stage}|${gate}`;
|
|
34047
|
+
const count = (this.counts.get(key) ?? 0) + 1;
|
|
34048
|
+
this.counts.set(key, count);
|
|
34049
|
+
if (count === 1 || count % 50 === 0) return count;
|
|
34050
|
+
}
|
|
34051
|
+
reset() {
|
|
34052
|
+
this.counts.clear();
|
|
34053
|
+
}
|
|
34054
|
+
};
|
|
34005
34055
|
//#endregion
|
|
34006
34056
|
//#region src/pipeline-analytics/pipeline/tracker/rescue-growth.ts
|
|
34007
34057
|
/**
|
|
@@ -34059,22 +34109,6 @@ function growthAdjacentMatch(track, det, ctx) {
|
|
|
34059
34109
|
const dc = bboxCentroid(det);
|
|
34060
34110
|
return Math.hypot(tc.x - dc.x, tc.y - dc.y) <= GROWTH_CENTROID_FACTOR * Math.hypot(track.w, track.h);
|
|
34061
34111
|
}
|
|
34062
|
-
/** Per-(stage, gate) counter + sampling decision. One instance per tracker, so
|
|
34063
|
-
* the counts are per camera and per detection source. */
|
|
34064
|
-
var LooseBindSampler = class {
|
|
34065
|
-
counts = /* @__PURE__ */ new Map();
|
|
34066
|
-
/** Increment the (stage, gate) counter and answer with the running total when
|
|
34067
|
-
* this decision should be reported, or `undefined` when it is sampled out. */
|
|
34068
|
-
next(stage, gate) {
|
|
34069
|
-
const key = `${stage}|${gate}`;
|
|
34070
|
-
const count = (this.counts.get(key) ?? 0) + 1;
|
|
34071
|
-
this.counts.set(key, count);
|
|
34072
|
-
if (count === 1 || count % 50 === 0) return count;
|
|
34073
|
-
}
|
|
34074
|
-
reset() {
|
|
34075
|
-
this.counts.clear();
|
|
34076
|
-
}
|
|
34077
|
-
};
|
|
34078
34112
|
/** Per-reason counter + sampling decision. One instance per tracker, so the
|
|
34079
34113
|
* counts are per camera and per detection source. */
|
|
34080
34114
|
var SpawnRefusalSampler = class {
|
|
@@ -34218,7 +34252,35 @@ var SortTracker = class SortTracker {
|
|
|
34218
34252
|
againstTrackClass: extra.against.class
|
|
34219
34253
|
} : {},
|
|
34220
34254
|
refusalCount,
|
|
34221
|
-
sampledEvery: 10
|
|
34255
|
+
sampledEvery: 10,
|
|
34256
|
+
...extra.droppedTrackId !== void 0 ? { droppedTrackId: extra.droppedTrackId } : {},
|
|
34257
|
+
...extra.trackLifeMs !== void 0 ? { trackLifeMs: extra.trackLifeMs } : {}
|
|
34258
|
+
});
|
|
34259
|
+
}
|
|
34260
|
+
/**
|
|
34261
|
+
* Report a TRACK that ends having emitted nothing — the last silent branch
|
|
34262
|
+
* on the detection→position path (D391).
|
|
34263
|
+
*
|
|
34264
|
+
* The subject is the track, not one box, so the record carries the track's
|
|
34265
|
+
* own last class/score/bbox: those ARE the detection that fed it last, and
|
|
34266
|
+
* they are what an operator matches against the recovery line that produced
|
|
34267
|
+
* it. Reported once (`lossReported`) and only where the track can no longer
|
|
34268
|
+
* come back — a graveyarded track may still resurrect, and a line about work
|
|
34269
|
+
* that was not lost is worse than no line.
|
|
34270
|
+
*/
|
|
34271
|
+
reportTrackLoss(track) {
|
|
34272
|
+
if (track.emitted || track.lossReported) return;
|
|
34273
|
+
track.lossReported = true;
|
|
34274
|
+
this.reportRefusal("unconfirmed-spawn-lost", {
|
|
34275
|
+
class: track.class,
|
|
34276
|
+
originalClass: track.originalClass,
|
|
34277
|
+
score: track.score,
|
|
34278
|
+
bbox: track.bbox
|
|
34279
|
+
}, {
|
|
34280
|
+
threshold: this.config.minHits,
|
|
34281
|
+
measured: track.hits,
|
|
34282
|
+
droppedTrackId: track.id,
|
|
34283
|
+
trackLifeMs: track.lastSeen - track.firstSeen
|
|
34222
34284
|
});
|
|
34223
34285
|
}
|
|
34224
34286
|
/** Emit one salvage decision, subject to {@link LOOSE_BIND_LOG_SAMPLE_EVERY}.
|
|
@@ -34413,7 +34475,22 @@ var SortTracker = class SortTracker {
|
|
|
34413
34475
|
}
|
|
34414
34476
|
const streak = (this.dupStreaks.get(key) ?? 0) + 1;
|
|
34415
34477
|
if (streak >= this.config.dedupMergeFrames) {
|
|
34416
|
-
|
|
34478
|
+
const loser = SortTracker.duplicateLoser(a, b);
|
|
34479
|
+
const winner = loser === a ? b : a;
|
|
34480
|
+
dropIds.add(loser.id);
|
|
34481
|
+
this.reportRefusal("merged-duplicate", {
|
|
34482
|
+
class: loser.class,
|
|
34483
|
+
originalClass: loser.originalClass,
|
|
34484
|
+
score: loser.score,
|
|
34485
|
+
bbox: loser.bbox
|
|
34486
|
+
}, {
|
|
34487
|
+
threshold: this.config.dedupMergeIou,
|
|
34488
|
+
measured: iou$3(a.bbox, b.bbox),
|
|
34489
|
+
against: winner,
|
|
34490
|
+
droppedTrackId: loser.id,
|
|
34491
|
+
trackLifeMs: loser.lastSeen - loser.firstSeen
|
|
34492
|
+
});
|
|
34493
|
+
loser.lossReported = true;
|
|
34417
34494
|
this.dupStreaks.delete(key);
|
|
34418
34495
|
} else this.dupStreaks.set(key, streak);
|
|
34419
34496
|
}
|
|
@@ -34595,6 +34672,7 @@ var SortTracker = class SortTracker {
|
|
|
34595
34672
|
track.lost = true;
|
|
34596
34673
|
track.lostAt = timestamp;
|
|
34597
34674
|
track.resurrectable = false;
|
|
34675
|
+
this.reportTrackLoss(track);
|
|
34598
34676
|
this.lostTracks.push(track);
|
|
34599
34677
|
} else alive.push(track);
|
|
34600
34678
|
this.tracks = alive;
|
|
@@ -34606,11 +34684,15 @@ var SortTracker = class SortTracker {
|
|
|
34606
34684
|
track.lost = true;
|
|
34607
34685
|
track.lostAt = track.lastSeen;
|
|
34608
34686
|
track.resurrectable = false;
|
|
34687
|
+
this.reportTrackLoss(track);
|
|
34609
34688
|
this.lostTracks.push(track);
|
|
34610
34689
|
} else alive.push(track);
|
|
34611
34690
|
this.tracks = alive;
|
|
34612
34691
|
}
|
|
34613
|
-
|
|
34692
|
+
const stillResurrectable = [];
|
|
34693
|
+
for (const t of this.lostTracks) if (timestamp - t.lostAt <= this.config.resurrectionWindowMs) stillResurrectable.push(t);
|
|
34694
|
+
else this.reportTrackLoss(t);
|
|
34695
|
+
this.lostTracks = stillResurrectable;
|
|
34614
34696
|
const used = /* @__PURE__ */ new Set();
|
|
34615
34697
|
const matchedTracks = /* @__PURE__ */ new Set();
|
|
34616
34698
|
const matched = /* @__PURE__ */ new Map();
|
|
@@ -34808,12 +34890,15 @@ var SortTracker = class SortTracker {
|
|
|
34808
34890
|
lost: false,
|
|
34809
34891
|
lostAt: 0,
|
|
34810
34892
|
resurrectable: true,
|
|
34811
|
-
classVotes: new Map([[det.class, det.score > 0 ? det.score : Number.EPSILON]])
|
|
34893
|
+
classVotes: new Map([[det.class, det.score > 0 ? det.score : Number.EPSILON]]),
|
|
34894
|
+
emitted: false,
|
|
34895
|
+
lossReported: false
|
|
34812
34896
|
});
|
|
34813
34897
|
}
|
|
34814
34898
|
this.tracks = surviving;
|
|
34815
34899
|
this.resolveDuplicates();
|
|
34816
34900
|
return this.tracks.filter((t) => this.isConfirmedForEmit(t)).map((t) => {
|
|
34901
|
+
t.emitted = true;
|
|
34817
34902
|
const scene = resurrectedScene.get(t);
|
|
34818
34903
|
return {
|
|
34819
34904
|
class: this.resolveReportedClass(t),
|
|
@@ -35181,6 +35266,10 @@ var FrameProcessor = class {
|
|
|
35181
35266
|
*/
|
|
35182
35267
|
reportedRefusalByTrack = /* @__PURE__ */ new Map();
|
|
35183
35268
|
zoneEngine = new ZoneEngine();
|
|
35269
|
+
/** Sink + sampler for the drops this stage owns (the detection-stage zone
|
|
35270
|
+
* filter). Shares the tracker's vocabulary — see {@link SpawnRefusalReason}. */
|
|
35271
|
+
spawnRefusalObserver = null;
|
|
35272
|
+
zoneRefusalSampler = new SpawnRefusalSampler();
|
|
35184
35273
|
/** Optional stationary-object gate (parked-object suppression). Null until
|
|
35185
35274
|
* the addon wires it via {@link setStationaryGate}. */
|
|
35186
35275
|
stationaryGate = null;
|
|
@@ -35275,6 +35364,7 @@ var FrameProcessor = class {
|
|
|
35275
35364
|
*/
|
|
35276
35365
|
setSpawnRefusalObserver(observer) {
|
|
35277
35366
|
this.tracker.setSpawnRefusalObserver(observer);
|
|
35367
|
+
this.spawnRefusalObserver = observer;
|
|
35278
35368
|
}
|
|
35279
35369
|
setBindObserver(observer) {
|
|
35280
35370
|
this.tracker.setBindObserver(observer);
|
|
@@ -35284,6 +35374,32 @@ var FrameProcessor = class {
|
|
|
35284
35374
|
dropTrack(trackId) {
|
|
35285
35375
|
this.tracker.dropTrack(trackId);
|
|
35286
35376
|
}
|
|
35377
|
+
/**
|
|
35378
|
+
* Say which detections a detection-stage zone rule excluded (D391).
|
|
35379
|
+
*
|
|
35380
|
+
* `filterDetections` has always ANSWERED this — it returns `excluded` — and
|
|
35381
|
+
* this stage dropped the answer on the floor, so a camera whose subject the
|
|
35382
|
+
* rules exclude was indistinguishable in the log from a camera whose tracker
|
|
35383
|
+
* ate it. Sampled like every other refusal: an excluded box is excluded on
|
|
35384
|
+
* every frame of a passage.
|
|
35385
|
+
*/
|
|
35386
|
+
reportZoneFiltered(excluded) {
|
|
35387
|
+
const observer = this.spawnRefusalObserver;
|
|
35388
|
+
if (!observer || excluded.length === 0) return;
|
|
35389
|
+
for (const fd of excluded) {
|
|
35390
|
+
const refusalCount = this.zoneRefusalSampler.next("zone-filtered");
|
|
35391
|
+
if (refusalCount === void 0) continue;
|
|
35392
|
+
observer({
|
|
35393
|
+
reason: "zone-filtered",
|
|
35394
|
+
detClass: fd.detection.class,
|
|
35395
|
+
detScore: fd.detection.score,
|
|
35396
|
+
bbox: fd.detection.bbox,
|
|
35397
|
+
...fd.detection.nativeRecovery !== void 0 ? { nativeRecoveryScore: fd.detection.nativeRecovery.score } : {},
|
|
35398
|
+
refusalCount,
|
|
35399
|
+
sampledEvery: 10
|
|
35400
|
+
});
|
|
35401
|
+
}
|
|
35402
|
+
}
|
|
35287
35403
|
process(input) {
|
|
35288
35404
|
const { timestamp, frame } = input;
|
|
35289
35405
|
const frameWidth = frame.width;
|
|
@@ -35369,7 +35485,8 @@ var FrameProcessor = class {
|
|
|
35369
35485
|
}
|
|
35370
35486
|
});
|
|
35371
35487
|
}
|
|
35372
|
-
const { passed } = this.zoneEngine.filterDetections(flatDetections, this.zones, this.detectionRules, frameWidth, frameHeight, (fd) => fd.detection.class);
|
|
35488
|
+
const { passed, excluded } = this.zoneEngine.filterDetections(flatDetections, this.zones, this.detectionRules, frameWidth, frameHeight, (fd) => fd.detection.class);
|
|
35489
|
+
this.reportZoneFiltered(excluded);
|
|
35373
35490
|
let filteredDetections = passed.map((fd) => fd.detection);
|
|
35374
35491
|
const riderPairs = pairRiders(filteredDetections.map((d, i) => ({
|
|
35375
35492
|
id: String(i),
|
|
@@ -74230,7 +74347,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
74230
74347
|
});
|
|
74231
74348
|
});
|
|
74232
74349
|
p.setSpawnRefusalObserver((record) => {
|
|
74233
|
-
this.ctx.logger.info("tracker: detection dropped —
|
|
74350
|
+
this.ctx.logger.info("tracker: detection dropped — nothing downstream received it", {
|
|
74234
74351
|
tags: { deviceId },
|
|
74235
74352
|
meta: {
|
|
74236
74353
|
source,
|
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.200",
|
|
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",
|