@camstack/addon-post-analysis 1.2.216 → 1.2.217

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.
@@ -36091,6 +36091,17 @@ function mapObjectStateToTrackState(s) {
36091
36091
  default: return "new";
36092
36092
  }
36093
36093
  }
36094
+ /**
36095
+ * How long an identical refusal stays collapsed. A stationary subject is
36096
+ * refused on every analysed frame; at ~2 fps of analysis, 60 s turns a parked
36097
+ * car's 120 lines into one and still reports a subject that comes back later.
36098
+ */
36099
+ var ZONE_REFUSAL_THROTTLE_MS = 6e4;
36100
+ /** Box quantisation for the collapse key: a subject that moves less than this
36101
+ * is the same sighting for diagnostic purposes. */
36102
+ var ZONE_REFUSAL_BOX_CELL_PX = 64;
36103
+ /** Bound on the collapse map; stale keys are swept when it is exceeded. */
36104
+ var ZONE_REFUSAL_KEY_LIMIT = 512;
36094
36105
  var FrameProcessor = class {
36095
36106
  deviceId;
36096
36107
  /** Detection source this processor handles (`pipeline` | `onboard`).
@@ -36162,8 +36173,11 @@ var FrameProcessor = class {
36162
36173
  * as before?" answered first. See `zone-detection-emits-without-a-track`.
36163
36174
  */
36164
36175
  refusalsThisFrame = [];
36165
- /** Running count of events this path has produced — the volume measure. */
36176
+ /** Running count of refusals-in-zone SEEN never the throttled number. */
36166
36177
  zoneRefusalEventCount = 0;
36178
+ zoneRefusalObserver = null;
36179
+ /** Last report time per collapsed key, for {@link ZONE_REFUSAL_THROTTLE_MS}. */
36180
+ zoneRefusalLastReport = /* @__PURE__ */ new Map();
36167
36181
  zoneRefusalSampler = new SpawnRefusalSampler();
36168
36182
  /** Optional stationary-object gate (parked-object suppression). Null until
36169
36183
  * the addon wires it via {@link setStationaryGate}. */
@@ -36264,6 +36278,14 @@ var FrameProcessor = class {
36264
36278
  this.tracker.setSpawnRefusalObserver(observer);
36265
36279
  this.spawnRefusalObserver = observer;
36266
36280
  }
36281
+ /**
36282
+ * Where a refused-inside-a-zone detection goes. Diagnostics only: this is
36283
+ * deliberately NOT the event stream, so nothing downstream can alert on a
36284
+ * box the confirmation gate threw away (D426).
36285
+ */
36286
+ setZoneRefusalObserver(observer) {
36287
+ this.zoneRefusalObserver = observer;
36288
+ }
36267
36289
  setBindObserver(observer) {
36268
36290
  this.tracker.setBindObserver(observer);
36269
36291
  }
@@ -36299,40 +36321,73 @@ var FrameProcessor = class {
36299
36321
  * operator's own detection rules already said the box does not count on this
36300
36322
  * camera, and re-admitting it here would overrule a decision they made.
36301
36323
  */
36302
- eventsForRefusedDetectionsInZones(timestamp, frame, frameWidth, frameHeight) {
36324
+ /**
36325
+ * Report every detection the tracker refused that fell INSIDE a watched
36326
+ * zone. Diagnostics only — see the spec
36327
+ * `zone-refusal-is-observed-never-notified` for the ten hours of production
36328
+ * that decided this: 1473 of 1511 such refusals were one stationary parcel
36329
+ * re-offered every frame, and both notifications this path ever produced
36330
+ * were phantoms the confirmation gate had correctly rejected.
36331
+ *
36332
+ * Drains {@link refusalsThisFrame}. `zone-filtered` is skipped: a box the
36333
+ * zone filter already removed was never in a zone.
36334
+ */
36335
+ reportRefusedDetectionsInZones(timestamp, frameWidth, frameHeight) {
36303
36336
  const refusals = this.refusalsThisFrame;
36304
36337
  this.refusalsThisFrame = [];
36305
- if (refusals.length === 0 || this.zones.length === 0) return [];
36306
- const out = [];
36338
+ if (refusals.length === 0 || this.zones.length === 0) return;
36339
+ const observer = this.zoneRefusalObserver;
36340
+ if (observer === null) return;
36307
36341
  for (const r of refusals) {
36308
36342
  if (r.reason === "zone-filtered") continue;
36309
36343
  const { memberships } = this.zoneEngine.splitDetectionZones(r.bbox, this.zones, frameWidth, frameHeight, void 0, void 0, void 0, this.zoneMembershipMinOverlap);
36310
36344
  if (memberships.length === 0) continue;
36311
36345
  this.zoneRefusalEventCount += 1;
36312
- out.push({
36313
- id: (0, node_crypto.randomUUID)(),
36314
- kind: "object",
36346
+ if (!this.shouldReportZoneRefusal(r, memberships, timestamp)) continue;
36347
+ observer({
36315
36348
  deviceId: this.deviceId,
36316
36349
  timestamp,
36317
- frameId: frame.frameId,
36318
36350
  className: r.detClass,
36319
36351
  confidence: r.detScore,
36320
36352
  bbox: r.bbox,
36321
- frameWidth,
36322
- frameHeight,
36323
- zones: memberships.map((m) => m.zoneId),
36324
- zoneOverlaps: memberships.map((m) => ({
36353
+ reason: r.reason,
36354
+ zones: memberships.map((m) => ({
36325
36355
  zoneId: m.zoneId,
36326
36356
  ...m.zoneName === void 0 ? {} : { zoneName: m.zoneName },
36327
36357
  overlapPct: Math.round(m.overlap * 1e3) / 10
36328
36358
  })),
36329
- spawnRefused: r.reason
36330
- });
36359
+ seenSoFar: this.zoneRefusalEventCount
36360
+ });
36361
+ }
36362
+ }
36363
+ /**
36364
+ * Collapse identical refusals. A refused box is refused on EVERY analysed
36365
+ * frame, so a subject that does not leave — the parcel at the door, the
36366
+ * parked car — is an unbounded emitter. The key includes a coarse box, so a
36367
+ * subject that actually moves keeps reporting while a stationary one does
36368
+ * not.
36369
+ */
36370
+ shouldReportZoneRefusal(r, memberships, timestamp) {
36371
+ const cell = (v) => Math.round(v / ZONE_REFUSAL_BOX_CELL_PX);
36372
+ const key = [
36373
+ r.detClass,
36374
+ r.reason,
36375
+ memberships.map((m) => m.zoneId).join("+"),
36376
+ cell(r.bbox.x),
36377
+ cell(r.bbox.y),
36378
+ cell(r.bbox.width),
36379
+ cell(r.bbox.height)
36380
+ ].join("|");
36381
+ const last = this.zoneRefusalLastReport.get(key);
36382
+ if (last !== void 0 && timestamp - last < ZONE_REFUSAL_THROTTLE_MS) return false;
36383
+ this.zoneRefusalLastReport.set(key, timestamp);
36384
+ if (this.zoneRefusalLastReport.size > ZONE_REFUSAL_KEY_LIMIT) {
36385
+ for (const [k, t] of this.zoneRefusalLastReport) if (timestamp - t >= ZONE_REFUSAL_THROTTLE_MS) this.zoneRefusalLastReport.delete(k);
36331
36386
  }
36332
- return out;
36387
+ return true;
36333
36388
  }
36334
- /** How many zone-refusal events this processor has emitted — the measure that
36335
- * says whether this path is a trickle or a storm. Read by the addon. */
36389
+ /** How many refusals-in-zone this processor has SEEN — the measure that says
36390
+ * whether this path is a trickle or a storm, before any throttling. */
36336
36391
  zoneRefusalEventsEmitted() {
36337
36392
  return this.zoneRefusalEventCount;
36338
36393
  }
@@ -36737,11 +36792,10 @@ var FrameProcessor = class {
36737
36792
  frameHeight
36738
36793
  };
36739
36794
  };
36740
- const zoneRefusalEvents = this.eventsForRefusedDetectionsInZones(timestamp, frame, frameWidth, frameHeight);
36795
+ this.reportRefusedDetectionsInZones(timestamp, frameWidth, frameHeight);
36741
36796
  const withTrackId = rawEvents.filter((e) => e.detection.trackId);
36742
36797
  const objectEvents = withTrackId.filter((e) => e.type !== "object.detected").map((e) => toObjectEvent(e));
36743
36798
  const appearanceEvents = withTrackId.filter((e) => e.type === "object.detected").map((e) => toObjectEvent(e, "entered"));
36744
- objectEvents.push(...zoneRefusalEvents);
36745
36799
  return {
36746
36800
  deviceId: this.deviceId,
36747
36801
  timestamp,
@@ -77554,6 +77608,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
77554
77608
  }
77555
77609
  });
77556
77610
  });
77611
+ p.setZoneRefusalObserver((observation) => {
77612
+ this.ctx.logger.info("detection refused inside a watched zone — not notified", {
77613
+ tags: { deviceId },
77614
+ meta: {
77615
+ source,
77616
+ ...observation
77617
+ }
77618
+ });
77619
+ });
77557
77620
  p.setSpawnBesideCoastingObserver((record) => {
77558
77621
  this.ctx.logger.info("tracker: new id minted beside a live coasting track", {
77559
77622
  tags: { deviceId },
@@ -36066,6 +36066,17 @@ function mapObjectStateToTrackState(s) {
36066
36066
  default: return "new";
36067
36067
  }
36068
36068
  }
36069
+ /**
36070
+ * How long an identical refusal stays collapsed. A stationary subject is
36071
+ * refused on every analysed frame; at ~2 fps of analysis, 60 s turns a parked
36072
+ * car's 120 lines into one and still reports a subject that comes back later.
36073
+ */
36074
+ var ZONE_REFUSAL_THROTTLE_MS = 6e4;
36075
+ /** Box quantisation for the collapse key: a subject that moves less than this
36076
+ * is the same sighting for diagnostic purposes. */
36077
+ var ZONE_REFUSAL_BOX_CELL_PX = 64;
36078
+ /** Bound on the collapse map; stale keys are swept when it is exceeded. */
36079
+ var ZONE_REFUSAL_KEY_LIMIT = 512;
36069
36080
  var FrameProcessor = class {
36070
36081
  deviceId;
36071
36082
  /** Detection source this processor handles (`pipeline` | `onboard`).
@@ -36137,8 +36148,11 @@ var FrameProcessor = class {
36137
36148
  * as before?" answered first. See `zone-detection-emits-without-a-track`.
36138
36149
  */
36139
36150
  refusalsThisFrame = [];
36140
- /** Running count of events this path has produced — the volume measure. */
36151
+ /** Running count of refusals-in-zone SEEN never the throttled number. */
36141
36152
  zoneRefusalEventCount = 0;
36153
+ zoneRefusalObserver = null;
36154
+ /** Last report time per collapsed key, for {@link ZONE_REFUSAL_THROTTLE_MS}. */
36155
+ zoneRefusalLastReport = /* @__PURE__ */ new Map();
36142
36156
  zoneRefusalSampler = new SpawnRefusalSampler();
36143
36157
  /** Optional stationary-object gate (parked-object suppression). Null until
36144
36158
  * the addon wires it via {@link setStationaryGate}. */
@@ -36239,6 +36253,14 @@ var FrameProcessor = class {
36239
36253
  this.tracker.setSpawnRefusalObserver(observer);
36240
36254
  this.spawnRefusalObserver = observer;
36241
36255
  }
36256
+ /**
36257
+ * Where a refused-inside-a-zone detection goes. Diagnostics only: this is
36258
+ * deliberately NOT the event stream, so nothing downstream can alert on a
36259
+ * box the confirmation gate threw away (D426).
36260
+ */
36261
+ setZoneRefusalObserver(observer) {
36262
+ this.zoneRefusalObserver = observer;
36263
+ }
36242
36264
  setBindObserver(observer) {
36243
36265
  this.tracker.setBindObserver(observer);
36244
36266
  }
@@ -36274,40 +36296,73 @@ var FrameProcessor = class {
36274
36296
  * operator's own detection rules already said the box does not count on this
36275
36297
  * camera, and re-admitting it here would overrule a decision they made.
36276
36298
  */
36277
- eventsForRefusedDetectionsInZones(timestamp, frame, frameWidth, frameHeight) {
36299
+ /**
36300
+ * Report every detection the tracker refused that fell INSIDE a watched
36301
+ * zone. Diagnostics only — see the spec
36302
+ * `zone-refusal-is-observed-never-notified` for the ten hours of production
36303
+ * that decided this: 1473 of 1511 such refusals were one stationary parcel
36304
+ * re-offered every frame, and both notifications this path ever produced
36305
+ * were phantoms the confirmation gate had correctly rejected.
36306
+ *
36307
+ * Drains {@link refusalsThisFrame}. `zone-filtered` is skipped: a box the
36308
+ * zone filter already removed was never in a zone.
36309
+ */
36310
+ reportRefusedDetectionsInZones(timestamp, frameWidth, frameHeight) {
36278
36311
  const refusals = this.refusalsThisFrame;
36279
36312
  this.refusalsThisFrame = [];
36280
- if (refusals.length === 0 || this.zones.length === 0) return [];
36281
- const out = [];
36313
+ if (refusals.length === 0 || this.zones.length === 0) return;
36314
+ const observer = this.zoneRefusalObserver;
36315
+ if (observer === null) return;
36282
36316
  for (const r of refusals) {
36283
36317
  if (r.reason === "zone-filtered") continue;
36284
36318
  const { memberships } = this.zoneEngine.splitDetectionZones(r.bbox, this.zones, frameWidth, frameHeight, void 0, void 0, void 0, this.zoneMembershipMinOverlap);
36285
36319
  if (memberships.length === 0) continue;
36286
36320
  this.zoneRefusalEventCount += 1;
36287
- out.push({
36288
- id: randomUUID(),
36289
- kind: "object",
36321
+ if (!this.shouldReportZoneRefusal(r, memberships, timestamp)) continue;
36322
+ observer({
36290
36323
  deviceId: this.deviceId,
36291
36324
  timestamp,
36292
- frameId: frame.frameId,
36293
36325
  className: r.detClass,
36294
36326
  confidence: r.detScore,
36295
36327
  bbox: r.bbox,
36296
- frameWidth,
36297
- frameHeight,
36298
- zones: memberships.map((m) => m.zoneId),
36299
- zoneOverlaps: memberships.map((m) => ({
36328
+ reason: r.reason,
36329
+ zones: memberships.map((m) => ({
36300
36330
  zoneId: m.zoneId,
36301
36331
  ...m.zoneName === void 0 ? {} : { zoneName: m.zoneName },
36302
36332
  overlapPct: Math.round(m.overlap * 1e3) / 10
36303
36333
  })),
36304
- spawnRefused: r.reason
36305
- });
36334
+ seenSoFar: this.zoneRefusalEventCount
36335
+ });
36336
+ }
36337
+ }
36338
+ /**
36339
+ * Collapse identical refusals. A refused box is refused on EVERY analysed
36340
+ * frame, so a subject that does not leave — the parcel at the door, the
36341
+ * parked car — is an unbounded emitter. The key includes a coarse box, so a
36342
+ * subject that actually moves keeps reporting while a stationary one does
36343
+ * not.
36344
+ */
36345
+ shouldReportZoneRefusal(r, memberships, timestamp) {
36346
+ const cell = (v) => Math.round(v / ZONE_REFUSAL_BOX_CELL_PX);
36347
+ const key = [
36348
+ r.detClass,
36349
+ r.reason,
36350
+ memberships.map((m) => m.zoneId).join("+"),
36351
+ cell(r.bbox.x),
36352
+ cell(r.bbox.y),
36353
+ cell(r.bbox.width),
36354
+ cell(r.bbox.height)
36355
+ ].join("|");
36356
+ const last = this.zoneRefusalLastReport.get(key);
36357
+ if (last !== void 0 && timestamp - last < ZONE_REFUSAL_THROTTLE_MS) return false;
36358
+ this.zoneRefusalLastReport.set(key, timestamp);
36359
+ if (this.zoneRefusalLastReport.size > ZONE_REFUSAL_KEY_LIMIT) {
36360
+ for (const [k, t] of this.zoneRefusalLastReport) if (timestamp - t >= ZONE_REFUSAL_THROTTLE_MS) this.zoneRefusalLastReport.delete(k);
36306
36361
  }
36307
- return out;
36362
+ return true;
36308
36363
  }
36309
- /** How many zone-refusal events this processor has emitted — the measure that
36310
- * says whether this path is a trickle or a storm. Read by the addon. */
36364
+ /** How many refusals-in-zone this processor has SEEN — the measure that says
36365
+ * whether this path is a trickle or a storm, before any throttling. */
36311
36366
  zoneRefusalEventsEmitted() {
36312
36367
  return this.zoneRefusalEventCount;
36313
36368
  }
@@ -36712,11 +36767,10 @@ var FrameProcessor = class {
36712
36767
  frameHeight
36713
36768
  };
36714
36769
  };
36715
- const zoneRefusalEvents = this.eventsForRefusedDetectionsInZones(timestamp, frame, frameWidth, frameHeight);
36770
+ this.reportRefusedDetectionsInZones(timestamp, frameWidth, frameHeight);
36716
36771
  const withTrackId = rawEvents.filter((e) => e.detection.trackId);
36717
36772
  const objectEvents = withTrackId.filter((e) => e.type !== "object.detected").map((e) => toObjectEvent(e));
36718
36773
  const appearanceEvents = withTrackId.filter((e) => e.type === "object.detected").map((e) => toObjectEvent(e, "entered"));
36719
- objectEvents.push(...zoneRefusalEvents);
36720
36774
  return {
36721
36775
  deviceId: this.deviceId,
36722
36776
  timestamp,
@@ -77480,6 +77534,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
77480
77534
  }
77481
77535
  });
77482
77536
  });
77537
+ p.setZoneRefusalObserver((observation) => {
77538
+ this.ctx.logger.info("detection refused inside a watched zone — not notified", {
77539
+ tags: { deviceId },
77540
+ meta: {
77541
+ source,
77542
+ ...observation
77543
+ }
77544
+ });
77545
+ });
77483
77546
  p.setSpawnBesideCoastingObserver((record) => {
77484
77547
  this.ctx.logger.info("tracker: new id minted beside a live coasting track", {
77485
77548
  tags: { deviceId },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.216",
3
+ "version": "1.2.217",
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",