@camstack/addon-pipeline-analysis 0.1.2 → 0.1.4

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/index.mjs CHANGED
@@ -1,940 +1,22 @@
1
- // src/zones/geometry.ts
2
- function pointInPolygon(point, polygon) {
3
- if (polygon.length < 3) return false;
4
- let inside = false;
5
- for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
6
- const xi = polygon[i].x, yi = polygon[i].y;
7
- const xj = polygon[j].x, yj = polygon[j].y;
8
- const intersect = yi > point.y !== yj > point.y && point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi;
9
- if (intersect) inside = !inside;
10
- }
11
- return inside;
12
- }
13
- function lineIntersection(a, b) {
14
- const dx1 = a.p2.x - a.p1.x;
15
- const dy1 = a.p2.y - a.p1.y;
16
- const dx2 = b.p2.x - b.p1.x;
17
- const dy2 = b.p2.y - b.p1.y;
18
- const denom = dx1 * dy2 - dy1 * dx2;
19
- if (Math.abs(denom) < 1e-10) return null;
20
- const t = ((b.p1.x - a.p1.x) * dy2 - (b.p1.y - a.p1.y) * dx2) / denom;
21
- const u = ((b.p1.x - a.p1.x) * dy1 - (b.p1.y - a.p1.y) * dx1) / denom;
22
- if (t < 0 || t > 1 || u < 0 || u > 1) return null;
23
- return { x: a.p1.x + t * dx1, y: a.p1.y + t * dy1 };
24
- }
25
- function tripwireCrossing(prev, curr, wire) {
26
- const movement = { p1: prev, p2: curr };
27
- const intersection = lineIntersection(movement, wire);
28
- if (!intersection) return null;
29
- const cross = (curr.x - prev.x) * (wire.p2.y - wire.p1.y) - (curr.y - prev.y) * (wire.p2.x - wire.p1.x);
30
- return { crossed: true, direction: cross > 0 ? "right" : "left" };
31
- }
32
- function bboxCentroid(bbox) {
33
- return { x: bbox.x + bbox.w / 2, y: bbox.y + bbox.h / 2 };
34
- }
35
- function normalizeToPixel(p, width, height) {
36
- return { x: p.x * width, y: p.y * height };
37
- }
38
-
39
- // src/tracker/sort-tracker.ts
40
- var DEFAULT_TRACKER_CONFIG = {
41
- iouThreshold: 0.3,
42
- maxMissedFrames: 30,
43
- minHits: 3
44
- };
45
- var MAX_PATH_LENGTH = 300;
46
- var nextTrackId = 1;
47
- function iou(a, b) {
48
- const ax1 = a.x, ay1 = a.y, ax2 = a.x + a.w, ay2 = a.y + a.h;
49
- const bx1 = b.x, by1 = b.y, bx2 = b.x + b.w, by2 = b.y + b.h;
50
- const ix1 = Math.max(ax1, bx1), iy1 = Math.max(ay1, by1);
51
- const ix2 = Math.min(ax2, bx2), iy2 = Math.min(ay2, by2);
52
- const iw = Math.max(0, ix2 - ix1), ih = Math.max(0, iy2 - iy1);
53
- const interArea = iw * ih;
54
- const aArea = a.w * a.h;
55
- const bArea = b.w * b.h;
56
- const unionArea = aArea + bArea - interArea;
57
- return unionArea > 0 ? interArea / unionArea : 0;
58
- }
59
- var SortTracker = class {
60
- config;
61
- tracks = [];
62
- lostTracks = [];
63
- constructor(config = {}) {
64
- this.config = { ...DEFAULT_TRACKER_CONFIG, ...config };
65
- }
66
- update(detections, timestamp) {
67
- const used = /* @__PURE__ */ new Set();
68
- const matchedTracks = /* @__PURE__ */ new Set();
69
- const matched = /* @__PURE__ */ new Map();
70
- const pairs = [];
71
- for (const track of this.tracks) {
72
- for (let di = 0; di < detections.length; di++) {
73
- const score = iou(track.bbox, detections[di].bbox);
74
- if (score >= this.config.iouThreshold) {
75
- pairs.push({ track, detIdx: di, score });
76
- }
77
- }
78
- }
79
- pairs.sort((a, b) => b.score - a.score);
80
- for (const pair of pairs) {
81
- if (matchedTracks.has(pair.track) || used.has(pair.detIdx)) continue;
82
- matched.set(pair.track, detections[pair.detIdx]);
83
- matchedTracks.add(pair.track);
84
- used.add(pair.detIdx);
85
- }
86
- for (const [track, det] of matched) {
87
- const prevCenter = bboxCentroid({ x: track.bbox.x, y: track.bbox.y, w: track.bbox.w, h: track.bbox.h });
88
- const newCenter = bboxCentroid({ x: det.bbox.x, y: det.bbox.y, w: det.bbox.w, h: det.bbox.h });
89
- track.bbox = det.bbox;
90
- track.class = det.class;
91
- track.originalClass = det.originalClass;
92
- track.score = det.score;
93
- track.age = 0;
94
- track.hits++;
95
- track.lastSeen = timestamp;
96
- track.velocity = { dx: newCenter.x - prevCenter.x, dy: newCenter.y - prevCenter.y };
97
- track.path.push(det.bbox);
98
- if (track.path.length > MAX_PATH_LENGTH) track.path.shift();
99
- }
100
- const surviving = [];
101
- for (const track of this.tracks) {
102
- if (matchedTracks.has(track)) {
103
- surviving.push(track);
104
- } else {
105
- track.age++;
106
- if (track.age > this.config.maxMissedFrames) {
107
- track.lost = true;
108
- this.lostTracks.push(track);
109
- } else {
110
- surviving.push(track);
111
- }
112
- }
113
- }
114
- for (let di = 0; di < detections.length; di++) {
115
- if (used.has(di)) continue;
116
- const det = detections[di];
117
- surviving.push({
118
- id: `track-${nextTrackId++}`,
119
- bbox: det.bbox,
120
- class: det.class,
121
- originalClass: det.originalClass,
122
- score: det.score,
123
- age: 0,
124
- hits: 1,
125
- path: [det.bbox],
126
- firstSeen: timestamp,
127
- lastSeen: timestamp,
128
- velocity: { dx: 0, dy: 0 },
129
- lost: false
130
- });
131
- }
132
- this.tracks = surviving;
133
- return this.tracks.filter((t) => t.hits >= this.config.minHits).map((t) => ({
134
- class: t.class,
135
- originalClass: t.originalClass,
136
- score: t.score,
137
- bbox: t.bbox,
138
- trackId: t.id,
139
- trackAge: t.hits,
140
- velocity: t.velocity,
141
- path: [...t.path]
142
- }));
143
- }
144
- getActiveTracks() {
145
- return this.tracks;
146
- }
147
- getLostTracks() {
148
- return this.lostTracks;
149
- }
150
- reset() {
151
- this.tracks = [];
152
- this.lostTracks = [];
153
- }
154
- };
155
-
156
- // src/tracker/state-analyzer.ts
157
- var DEFAULT_STATE_ANALYZER_CONFIG = {
158
- stationaryThresholdSec: 10,
159
- loiteringThresholdSec: 60,
160
- velocityThreshold: 2,
161
- enteringFrames: 5
162
- };
163
- var StateAnalyzer = class {
164
- config;
165
- states = /* @__PURE__ */ new Map();
166
- constructor(config = {}) {
167
- this.config = { ...DEFAULT_STATE_ANALYZER_CONFIG, ...config };
168
- }
169
- analyze(tracks, timestamp) {
170
- const currentIds = new Set(tracks.map((t) => t.trackId));
171
- const results = [];
172
- for (const track of tracks) {
173
- let ts = this.states.get(track.trackId);
174
- const center = bboxCentroid({ x: track.bbox.x, y: track.bbox.y, w: track.bbox.w, h: track.bbox.h });
175
- if (!ts) {
176
- ts = {
177
- enteredAt: timestamp,
178
- stationarySince: void 0,
179
- totalDistancePx: 0,
180
- lastPosition: center,
181
- frameCount: 1
182
- };
183
- this.states.set(track.trackId, ts);
184
- } else {
185
- const dx = center.x - ts.lastPosition.x;
186
- const dy = center.y - ts.lastPosition.y;
187
- const dist = Math.sqrt(dx * dx + dy * dy);
188
- ts.totalDistancePx += dist;
189
- ts.lastPosition = center;
190
- ts.frameCount++;
191
- }
192
- const velocity = track.velocity ? Math.sqrt(track.velocity.dx ** 2 + track.velocity.dy ** 2) : 0;
193
- const dwellTimeMs = timestamp - ts.enteredAt;
194
- let state;
195
- if (ts.frameCount <= this.config.enteringFrames) {
196
- state = "entering";
197
- } else if (velocity <= this.config.velocityThreshold) {
198
- if (!ts.stationarySince) {
199
- ts.stationarySince = timestamp;
200
- }
201
- const stationaryDurationSec = (timestamp - ts.stationarySince) / 1e3;
202
- if (stationaryDurationSec >= this.config.loiteringThresholdSec) {
203
- state = "loitering";
204
- } else if (stationaryDurationSec >= this.config.stationaryThresholdSec) {
205
- state = "stationary";
206
- } else {
207
- state = "moving";
208
- }
209
- } else {
210
- ts.stationarySince = void 0;
211
- state = "moving";
212
- }
213
- results.push({
214
- trackId: track.trackId,
215
- state,
216
- stationarySince: ts.stationarySince,
217
- enteredAt: ts.enteredAt,
218
- totalDistancePx: ts.totalDistancePx,
219
- dwellTimeMs
220
- });
221
- }
222
- for (const [trackId, ts] of this.states) {
223
- if (!currentIds.has(trackId)) {
224
- results.push({
225
- trackId,
226
- state: "leaving",
227
- stationarySince: ts.stationarySince,
228
- enteredAt: ts.enteredAt,
229
- totalDistancePx: ts.totalDistancePx,
230
- dwellTimeMs: timestamp - ts.enteredAt
231
- });
232
- this.states.delete(trackId);
233
- }
234
- }
235
- return results;
236
- }
237
- reset() {
238
- this.states.clear();
239
- }
240
- };
241
-
242
- // src/events/event-filter.ts
243
- var DEFAULT_EVENT_EMITTER_CONFIG = {
244
- minTrackAge: 3,
245
- cooldownSec: 5,
246
- enabledTypes: [
247
- "object.entering",
248
- "object.leaving",
249
- "object.stationary",
250
- "object.loitering",
251
- "zone.enter",
252
- "zone.exit",
253
- "tripwire.cross"
254
- ]
255
- };
256
-
257
- // src/events/event-emitter.ts
258
- var STATE_TO_EVENT = {
259
- entering: "object.entering",
260
- leaving: "object.leaving",
261
- stationary: "object.stationary",
262
- loitering: "object.loitering"
263
- };
264
- var ZONE_TYPE_TO_EVENT = {
265
- "zone-enter": "zone.enter",
266
- "zone-exit": "zone.exit",
267
- "zone-loiter": "zone.enter",
268
- "tripwire-cross": "tripwire.cross"
269
- };
270
- var eventIdCounter = 0;
271
- var DetectionEventEmitter = class {
272
- config;
273
- previousStates = /* @__PURE__ */ new Map();
274
- lastEmitted = /* @__PURE__ */ new Map();
275
- constructor(config = {}) {
276
- this.config = { ...DEFAULT_EVENT_EMITTER_CONFIG, ...config };
277
- }
278
- emit(tracks, states, zoneEvents, classifications, deviceId) {
279
- const events = [];
280
- const now = Date.now();
281
- const stateMap = new Map(states.map((s) => [s.trackId, s]));
282
- const trackMap = new Map(tracks.map((t) => [t.trackId, t]));
283
- for (const state of states) {
284
- const track = trackMap.get(state.trackId);
285
- if (!track) continue;
286
- if (track.trackAge < this.config.minTrackAge) continue;
287
- const eventType = STATE_TO_EVENT[state.state];
288
- if (!eventType) continue;
289
- if (!this.config.enabledTypes.includes(eventType)) continue;
290
- const prevState = this.previousStates.get(state.trackId);
291
- if (prevState === state.state) continue;
292
- const cooldownKey = `${state.trackId}:${eventType}`;
293
- const lastTime = this.lastEmitted.get(cooldownKey) ?? 0;
294
- if ((now - lastTime) / 1e3 < this.config.cooldownSec) continue;
295
- this.previousStates.set(state.trackId, state.state);
296
- this.lastEmitted.set(cooldownKey, now);
297
- events.push({
298
- id: `evt-${++eventIdCounter}`,
299
- type: eventType,
300
- timestamp: now,
301
- deviceId,
302
- detection: track,
303
- classifications,
304
- objectState: state,
305
- zoneEvents: zoneEvents.filter((z) => z.trackId === state.trackId),
306
- trackPath: [...track.path]
307
- });
308
- }
309
- for (const ze of zoneEvents) {
310
- const eventType = ZONE_TYPE_TO_EVENT[ze.type];
311
- if (!eventType) continue;
312
- if (!this.config.enabledTypes.includes(eventType)) continue;
313
- const track = trackMap.get(ze.trackId);
314
- if (!track || track.trackAge < this.config.minTrackAge) continue;
315
- const state = stateMap.get(ze.trackId);
316
- events.push({
317
- id: `evt-${++eventIdCounter}`,
318
- type: eventType,
319
- timestamp: now,
320
- deviceId,
321
- detection: track,
322
- classifications,
323
- objectState: state ?? {
324
- trackId: ze.trackId,
325
- state: "moving",
326
- enteredAt: now,
327
- totalDistancePx: 0,
328
- dwellTimeMs: 0
329
- },
330
- zoneEvents: [ze],
331
- trackPath: [...track.path]
332
- });
333
- }
334
- for (const state of states) {
335
- if (state.state === "leaving") {
336
- this.previousStates.delete(state.trackId);
337
- }
338
- }
339
- return events;
340
- }
341
- reset() {
342
- this.previousStates.clear();
343
- this.lastEmitted.clear();
344
- }
345
- };
346
-
347
- // src/zones/overlap.ts
348
- function bboxPolygonOverlap(bbox, polygon) {
349
- const area = bbox.w * bbox.h;
350
- if (area <= 0) return 0;
351
- const gridSize = 8;
352
- let inside = 0;
353
- const total = gridSize * gridSize;
354
- for (let row = 0; row < gridSize; row++) {
355
- for (let col = 0; col < gridSize; col++) {
356
- const px = bbox.x + (col + 0.5) * (bbox.w / gridSize);
357
- const py = bbox.y + (row + 0.5) * (bbox.h / gridSize);
358
- if (pointInPolygon({ x: px, y: py }, polygon)) {
359
- inside++;
360
- }
361
- }
362
- }
363
- return inside / total;
364
- }
365
- function maskPolygonOverlap(mask, maskWidth, maskHeight, bbox, polygon, _frameWidth, _frameHeight) {
366
- let totalMaskPixels = 0;
367
- let insidePolygon = 0;
368
- for (let my = 0; my < maskHeight; my++) {
369
- for (let mx = 0; mx < maskWidth; mx++) {
370
- if (mask[my * maskWidth + mx] === 0) continue;
371
- totalMaskPixels++;
372
- const frameX = bbox.x + mx / maskWidth * bbox.w;
373
- const frameY = bbox.y + my / maskHeight * bbox.h;
374
- if (pointInPolygon({ x: frameX, y: frameY }, polygon)) {
375
- insidePolygon++;
376
- }
377
- }
378
- }
379
- if (totalMaskPixels === 0) return 0;
380
- return insidePolygon / totalMaskPixels;
381
- }
382
-
383
- // src/zones/zone-engine.ts
384
- var ZoneEngine = class {
385
- /**
386
- * Annotate a single detection with its zone memberships.
387
- * Returns zones where the detection overlaps above threshold.
388
- */
389
- annotateDetection(bbox, zones, frameWidth, frameHeight, mask, maskWidth, maskHeight, className) {
390
- const memberships = [];
391
- for (const zone of zones) {
392
- if (zone.classFilter && zone.classFilter.length > 0 && className) {
393
- if (!zone.classFilter.includes(className)) continue;
394
- }
395
- const pixelPolygon = zone.polygon.map((p) => normalizeToPixel(p, frameWidth, frameHeight));
396
- let overlap;
397
- if (zone.preferMask && mask && maskWidth && maskHeight) {
398
- overlap = maskPolygonOverlap(mask, maskWidth, maskHeight, bbox, pixelPolygon, frameWidth, frameHeight);
399
- } else {
400
- overlap = bboxPolygonOverlap(bbox, pixelPolygon);
401
- }
402
- if (overlap >= zone.overlapThreshold) {
403
- memberships.push({
404
- zoneId: zone.id,
405
- zoneName: zone.name,
406
- overlap,
407
- mode: zone.detectionMode
408
- });
409
- }
410
- }
411
- return memberships;
412
- }
413
- /**
414
- * Filter detections based on zone include/exclude/monitor logic for a given stage.
415
- */
416
- filterDetections(detections, zones, frameWidth, frameHeight, stage, getClassName, getMask) {
417
- if (zones.length === 0) {
418
- return {
419
- passed: detections,
420
- excluded: [],
421
- annotations: new Map(detections.map((d) => [d, []]))
422
- };
423
- }
424
- const modeKey = stage === "motion" ? "motionMode" : "detectionMode";
425
- const hasInclude = zones.some((z) => z[modeKey] === "include");
426
- const passed = [];
427
- const excluded = [];
428
- const annotations = /* @__PURE__ */ new Map();
429
- for (const det of detections) {
430
- const className = getClassName?.(det);
431
- const maskInfo = getMask?.(det);
432
- const memberships = this.annotateDetection(
433
- det,
434
- zones,
435
- frameWidth,
436
- frameHeight,
437
- maskInfo?.mask,
438
- maskInfo?.width,
439
- maskInfo?.height,
440
- className
441
- );
442
- annotations.set(det, memberships);
443
- const stageMemberships = memberships.filter((m) => m.mode !== "monitor");
444
- if (hasInclude) {
445
- const inInclude = stageMemberships.some((m) => m.mode === "include");
446
- if (inInclude) {
447
- passed.push(det);
448
- } else {
449
- excluded.push(det);
450
- }
451
- } else {
452
- const inExclude = stageMemberships.some((m) => m.mode === "exclude");
453
- if (inExclude) {
454
- excluded.push(det);
455
- } else {
456
- passed.push(det);
457
- }
458
- }
459
- }
460
- return { passed, excluded, annotations };
461
- }
462
- /**
463
- * Generate an auto-border exclusion zone as a polygon with a hole.
464
- */
465
- generateAutoBorderZone(percent) {
466
- const p = Math.max(0, Math.min(0.2, percent));
467
- return {
468
- id: "__auto-border-exclude",
469
- name: "Auto Border Exclusion",
470
- polygon: [
471
- { x: 0, y: 0 },
472
- { x: 0, y: 1 },
473
- { x: 1, y: 1 },
474
- { x: 1, y: 0 },
475
- { x: p, y: p },
476
- { x: 1 - p, y: p },
477
- { x: 1 - p, y: 1 - p },
478
- { x: p, y: 1 - p }
479
- ],
480
- motionMode: "exclude",
481
- detectionMode: "exclude",
482
- overlapThreshold: 0.1,
483
- preferMask: false,
484
- color: "#888888"
485
- };
486
- }
487
- };
488
-
489
- // src/analytics/track-store.ts
490
- var MAX_PATH_LENGTH2 = 300;
491
- var TrackStore = class {
492
- tracks = /* @__PURE__ */ new Map();
493
- update(tracked, zoneEvents, classifications, timestamp) {
494
- for (const t of tracked) {
495
- let at = this.tracks.get(t.trackId);
496
- if (!at) {
497
- at = {
498
- trackId: t.trackId,
499
- class: t.class,
500
- originalClass: t.originalClass,
501
- state: "entering",
502
- firstSeen: timestamp,
503
- lastSeen: timestamp,
504
- path: [],
505
- zoneTransitions: [],
506
- events: []
507
- };
508
- this.tracks.set(t.trackId, at);
509
- }
510
- at.lastSeen = timestamp;
511
- at.class = t.class;
512
- at.path.push({
513
- timestamp,
514
- bbox: t.bbox,
515
- velocity: t.velocity ?? { dx: 0, dy: 0 }
516
- });
517
- if (at.path.length > MAX_PATH_LENGTH2) at.path.shift();
518
- }
519
- for (const ze of zoneEvents) {
520
- const at = this.tracks.get(ze.trackId);
521
- if (!at) continue;
522
- if (ze.type === "zone-enter") {
523
- at.zoneTransitions.push({
524
- zoneId: ze.zoneId,
525
- zoneName: ze.zoneName,
526
- entered: ze.timestamp,
527
- dwellMs: 0
528
- });
529
- } else if (ze.type === "zone-exit") {
530
- const transition = [...at.zoneTransitions].reverse().find(
531
- (zt) => zt.zoneId === ze.zoneId && !zt.exited
532
- );
533
- if (transition) {
534
- transition.exited = ze.timestamp;
535
- transition.dwellMs = ze.timestamp - transition.entered;
536
- }
537
- }
538
- }
539
- }
540
- addEvent(trackId, event) {
541
- const at = this.tracks.get(trackId);
542
- if (at) {
543
- at.events.push(event);
544
- if (!at.bestSnapshot && event.snapshot) {
545
- at.bestSnapshot = event.snapshot;
546
- }
547
- }
548
- }
549
- getTrackDetail(trackId) {
550
- const at = this.tracks.get(trackId);
551
- if (!at) return null;
552
- return this.toDetail(at);
553
- }
554
- finishTrack(trackId) {
555
- const at = this.tracks.get(trackId);
556
- if (!at) return null;
557
- this.tracks.delete(trackId);
558
- return this.toDetail(at);
559
- }
560
- getActiveTrackIds() {
561
- return [...this.tracks.keys()];
562
- }
563
- reset() {
564
- this.tracks.clear();
565
- }
566
- toDetail(at) {
567
- return {
568
- trackId: at.trackId,
569
- class: at.class,
570
- originalClass: at.originalClass,
571
- state: at.state,
572
- firstSeen: at.firstSeen,
573
- lastSeen: at.lastSeen,
574
- totalDwellMs: at.lastSeen - at.firstSeen,
575
- path: at.path,
576
- identity: at.identity,
577
- plateText: at.plateText,
578
- subClass: at.subClass,
579
- zoneTransitions: at.zoneTransitions,
580
- bestSnapshot: at.bestSnapshot,
581
- events: at.events
582
- };
583
- }
584
- };
585
-
586
- // src/analytics/heatmap.ts
587
- var HeatmapAggregator = class {
588
- gridSize;
589
- cells;
590
- maxCount = 0;
591
- _totalPoints = 0;
592
- constructor(gridSize = 32) {
593
- this.gridSize = gridSize;
594
- this.cells = new Float32Array(gridSize * gridSize);
595
- }
596
- get totalPoints() {
597
- return this._totalPoints;
598
- }
599
- addPoint(x, y) {
600
- const col = Math.min(this.gridSize - 1, Math.max(0, Math.floor(x * this.gridSize)));
601
- const row = Math.min(this.gridSize - 1, Math.max(0, Math.floor(y * this.gridSize)));
602
- const idx = row * this.gridSize + col;
603
- this.cells[idx]++;
604
- if (this.cells[idx] > this.maxCount) this.maxCount = this.cells[idx];
605
- this._totalPoints++;
606
- }
607
- getHeatmap() {
608
- return {
609
- width: this.gridSize,
610
- height: this.gridSize,
611
- gridSize: this.gridSize,
612
- cells: new Float32Array(this.cells),
613
- maxCount: this.maxCount
614
- };
615
- }
616
- reset() {
617
- this.cells.fill(0);
618
- this.maxCount = 0;
619
- this._totalPoints = 0;
620
- }
621
- };
622
-
623
- // src/analytics/analytics-provider.ts
624
- var AnalyticsProvider = class {
625
- liveStates = /* @__PURE__ */ new Map();
626
- trackStores = /* @__PURE__ */ new Map();
627
- heatmaps = /* @__PURE__ */ new Map();
628
- updateLiveState(deviceId, state) {
629
- this.liveStates.set(deviceId, state);
630
- }
631
- getTrackStore(deviceId) {
632
- let store = this.trackStores.get(deviceId);
633
- if (!store) {
634
- store = new TrackStore();
635
- this.trackStores.set(deviceId, store);
636
- }
637
- return store;
638
- }
639
- getHeatmapAggregator(deviceId, gridSize = 32) {
640
- let hm = this.heatmaps.get(deviceId);
641
- if (!hm) {
642
- hm = new HeatmapAggregator(gridSize);
643
- this.heatmaps.set(deviceId, hm);
644
- }
645
- return hm;
646
- }
647
- getLiveState(deviceId) {
648
- return this.liveStates.get(deviceId) ?? null;
649
- }
650
- getTracks(deviceId, filter) {
651
- const state = this.liveStates.get(deviceId);
652
- if (!state) return [];
653
- let tracks = [...state.tracks];
654
- if (filter?.class) tracks = tracks.filter((t) => filter.class.includes(t.class));
655
- if (filter?.state) tracks = tracks.filter((t) => filter.state.includes(t.state));
656
- if (filter?.minDwellMs) tracks = tracks.filter((t) => t.dwellTimeMs >= filter.minDwellMs);
657
- return tracks;
658
- }
659
- getZoneState(deviceId, zoneId) {
660
- const state = this.liveStates.get(deviceId);
661
- return state?.zones.find((z) => z.zoneId === zoneId) ?? null;
662
- }
663
- async getZoneHistory(_deviceId, _zoneId, _options) {
664
- return [];
665
- }
666
- async getHeatmap(deviceId, _options) {
667
- const hm = this.heatmaps.get(deviceId);
668
- return hm?.getHeatmap() ?? { width: 32, height: 32, gridSize: 32, cells: new Float32Array(32 * 32), maxCount: 0 };
669
- }
670
- getTrackDetail(deviceId, trackId) {
671
- const store = this.trackStores.get(deviceId);
672
- return store?.getTrackDetail(trackId) ?? null;
673
- }
674
- getCameraStatus(_deviceId) {
675
- return [];
676
- }
677
- };
678
-
679
- // src/analytics/live-state.ts
680
- var FPS_WINDOW_MS = 5e3;
681
- var LiveStateManager = class {
682
- frameTimes = [];
683
- pipelineTimes = [];
684
- buildState(deviceId, tracks, states, pipelineStatus, timestamp, lastEvent, pipelineMs) {
685
- this.frameTimes.push(timestamp);
686
- const cutoff = timestamp - FPS_WINDOW_MS;
687
- while (this.frameTimes.length > 0 && this.frameTimes[0] < cutoff) this.frameTimes.shift();
688
- const currentFps = this.frameTimes.length > 1 ? (this.frameTimes.length - 1) / ((this.frameTimes[this.frameTimes.length - 1] - this.frameTimes[0]) / 1e3) : 0;
689
- if (pipelineMs !== void 0) {
690
- this.pipelineTimes.push(pipelineMs);
691
- if (this.pipelineTimes.length > 100) this.pipelineTimes.shift();
692
- }
693
- const avgPipelineMs = this.pipelineTimes.length > 0 ? this.pipelineTimes.reduce((a, b) => a + b, 0) / this.pipelineTimes.length : 0;
694
- const stateMap = new Map(states.map((s) => [s.trackId, s]));
695
- const objectCounts = {};
696
- let moving = 0, stationary = 0;
697
- const trackSummaries = [];
698
- for (const t of tracks) {
699
- objectCounts[t.class] = (objectCounts[t.class] ?? 0) + 1;
700
- const s = stateMap.get(t.trackId);
701
- if (s?.state === "moving" || s?.state === "entering") moving++;
702
- if (s?.state === "stationary" || s?.state === "loitering") stationary++;
703
- trackSummaries.push({
704
- trackId: t.trackId,
705
- class: t.class,
706
- originalClass: t.originalClass,
707
- state: s?.state ?? "entering",
708
- bbox: t.bbox,
709
- velocity: t.velocity ?? { dx: 0, dy: 0 },
710
- dwellTimeMs: s?.dwellTimeMs ?? 0,
711
- inZones: []
712
- });
713
- }
714
- return {
715
- deviceId,
716
- lastFrameTimestamp: timestamp,
717
- activeObjects: tracks.length,
718
- movingObjects: moving,
719
- stationaryObjects: stationary,
720
- objectCounts,
721
- tracks: trackSummaries,
722
- zones: [],
723
- lastEvent,
724
- pipelineStatus: [...pipelineStatus],
725
- avgPipelineMs,
726
- currentFps
727
- };
728
- }
729
- reset() {
730
- this.frameTimes.length = 0;
731
- this.pipelineTimes.length = 0;
732
- }
733
- };
734
-
735
- // src/pipeline/analysis-pipeline.ts
736
- var AnalysisPipeline = class {
737
- manifest = {
738
- id: "analysis-pipeline",
739
- name: "Analysis Pipeline",
740
- version: "0.1.0",
741
- description: "Per-camera zone engine, tracker, event emitter and analytics",
742
- capabilities: [{ name: "analysis-pipeline", mode: "singleton" }],
743
- passive: true
744
- };
745
- cameraStates = /* @__PURE__ */ new Map();
746
- analytics = new AnalyticsProvider();
747
- liveStateCallbacks = /* @__PURE__ */ new Map();
748
- // --------------------------------------------------------------------------
749
- // ICamstackAddon
750
- // --------------------------------------------------------------------------
751
- async initialize(_context) {
752
- }
753
- async shutdown() {
754
- this.cameraStates.clear();
755
- this.liveStateCallbacks.clear();
756
- }
757
- getCapabilityProvider(name) {
758
- if (name === "analysis-pipeline") {
759
- return this;
760
- }
761
- return null;
762
- }
763
- // --------------------------------------------------------------------------
764
- // IAnalysisAddon — configuration
765
- // --------------------------------------------------------------------------
766
- setCameraPipeline(deviceId, _config) {
767
- this.ensureCameraState(deviceId);
768
- }
769
- setCameraZones(deviceId, zones) {
770
- const state = this.ensureCameraState(deviceId);
771
- this.cameraStates.set(deviceId, { ...state, zones });
772
- }
773
- setCameraMotionConfig(_deviceId, _source) {
774
- }
775
- setKnownFaces(_faces) {
776
- }
777
- setKnownPlates(_plates) {
778
- }
779
- onLiveStateChange(deviceId, callback) {
780
- let callbacks = this.liveStateCallbacks.get(deviceId);
781
- if (!callbacks) {
782
- callbacks = /* @__PURE__ */ new Set();
783
- this.liveStateCallbacks.set(deviceId, callbacks);
784
- }
785
- callbacks.add(callback);
786
- return () => {
787
- this.liveStateCallbacks.get(deviceId)?.delete(callback);
788
- };
789
- }
790
- // --------------------------------------------------------------------------
791
- // IAnalysisAddon — frame processing
792
- // The interface declares (deviceId, frame) but callers may pass an optional
793
- // PipelineRunResult as a third argument; we accept it here.
794
- // --------------------------------------------------------------------------
795
- async processFrame(deviceId, frame, pipelineResult) {
796
- const state = this.ensureCameraState(deviceId);
797
- const timestamp = frame.timestamp;
798
- const frameWidth = pipelineResult?.imageWidth ?? frame.width;
799
- const frameHeight = pipelineResult?.imageHeight ?? frame.height;
800
- const flatDetections = pipelineResult ? pipelineResult.detections.map((det) => {
801
- const bbox = this.convertBbox(det.bbox);
802
- const detection = {
803
- class: det.className,
804
- originalClass: det.originalClass ?? det.className,
805
- score: det.confidence,
806
- bbox
807
- };
808
- return { ...bbox, detection };
809
- }) : [];
810
- const zoneEngine = new ZoneEngine();
811
- const { passed: filteredFlat } = zoneEngine.filterDetections(
812
- flatDetections,
813
- state.zones,
814
- frameWidth,
815
- frameHeight,
816
- "detection",
817
- (fd) => fd.detection.class
818
- );
819
- const filteredDetections = filteredFlat.map((fd) => fd.detection);
820
- const trackedDetections = state.tracker.update(filteredDetections, timestamp);
821
- const objectStates = state.stateAnalyzer.analyze(trackedDetections, timestamp);
822
- const events = state.eventEmitter.emit(
823
- trackedDetections,
824
- objectStates,
825
- [],
826
- [],
827
- deviceId
828
- );
829
- const trackStore = this.analytics.getTrackStore(deviceId);
830
- trackStore.update(trackedDetections, [], [], timestamp);
831
- const heatmap = this.analytics.getHeatmapAggregator(deviceId);
832
- for (const t of trackedDetections) {
833
- const cx = (t.bbox.x + t.bbox.w / 2) / frameWidth;
834
- const cy = (t.bbox.y + t.bbox.h / 2) / frameHeight;
835
- heatmap.addPoint(cx, cy);
836
- }
837
- for (const event of events) {
838
- trackStore.addEvent(event.detection.trackId, event);
839
- }
840
- const liveState = state.liveStateManager.buildState(
841
- deviceId,
842
- trackedDetections,
843
- objectStates,
844
- state.pipelineStatus,
845
- timestamp,
846
- events[events.length - 1],
847
- pipelineResult?.totalMs
848
- );
849
- this.analytics.updateLiveState(deviceId, liveState);
850
- const callbacks = this.liveStateCallbacks.get(deviceId);
851
- if (callbacks && callbacks.size > 0) {
852
- for (const cb of callbacks) {
853
- cb(liveState);
854
- }
855
- }
856
- return events;
857
- }
858
- // --------------------------------------------------------------------------
859
- // ICameraAnalyticsProvider
860
- // --------------------------------------------------------------------------
861
- getLiveState(deviceId) {
862
- return this.analytics.getLiveState(deviceId);
863
- }
864
- getTracks(deviceId, filter) {
865
- return this.analytics.getTracks(deviceId, filter);
866
- }
867
- getZoneState(deviceId, zoneId) {
868
- return this.analytics.getZoneState(deviceId, zoneId);
869
- }
870
- async getZoneHistory(deviceId, zoneId, options) {
871
- return this.analytics.getZoneHistory(deviceId, zoneId, options);
872
- }
873
- async getHeatmap(deviceId, options) {
874
- return this.analytics.getHeatmap(deviceId, options);
875
- }
876
- getTrackDetail(deviceId, trackId) {
877
- return this.analytics.getTrackDetail(deviceId, trackId);
878
- }
879
- getCameraStatus(deviceId) {
880
- return this.analytics.getCameraStatus(deviceId);
881
- }
882
- // --------------------------------------------------------------------------
883
- // Private helpers
884
- // --------------------------------------------------------------------------
885
- ensureCameraState(deviceId) {
886
- let state = this.cameraStates.get(deviceId);
887
- if (!state) {
888
- state = {
889
- tracker: new SortTracker(),
890
- stateAnalyzer: new StateAnalyzer(),
891
- eventEmitter: new DetectionEventEmitter(),
892
- liveStateManager: new LiveStateManager(),
893
- zones: [],
894
- pipelineStatus: []
895
- };
896
- this.cameraStates.set(deviceId, state);
897
- }
898
- return state;
899
- }
900
- /**
901
- * Convert PipelineDetection bbox [x1, y1, x2, y2] to BoundingBox {x, y, w, h}.
902
- * Pipeline bboxes are in pixel coordinates.
903
- */
904
- convertBbox(bbox) {
905
- const [x1, y1, x2, y2] = bbox;
906
- return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
907
- }
908
- };
909
-
910
- // src/addon.ts
911
- var AnalysisPipelineAddon = class {
912
- manifest = {
913
- id: "pipeline-analysis",
914
- name: "Pipeline Analysis",
915
- version: "0.1.0",
916
- description: "Zone engine, object tracker, and analytics for detection pipeline",
917
- capabilities: [
918
- { name: "analysis-pipeline", mode: "singleton" }
919
- ]
920
- };
921
- pipeline = null;
922
- async initialize(context) {
923
- this.pipeline = new AnalysisPipeline();
924
- await this.pipeline.initialize(context);
925
- context.logger.info("Pipeline Analysis addon initialized");
926
- }
927
- async shutdown() {
928
- await this.pipeline?.shutdown();
929
- this.pipeline = null;
930
- }
931
- getCapabilityProvider(name) {
932
- if (name === "analysis-pipeline") {
933
- return this.pipeline;
934
- }
935
- return null;
936
- }
937
- };
1
+ import {
2
+ AnalysisPipeline,
3
+ AnalysisPipelineAddon,
4
+ DEFAULT_STATE_ANALYZER_CONFIG,
5
+ DEFAULT_TRACKER_CONFIG,
6
+ DetectionEventEmitter,
7
+ HeatmapAggregator,
8
+ SortTracker,
9
+ StateAnalyzer,
10
+ TrackStore,
11
+ ZoneEngine,
12
+ bboxCentroid,
13
+ bboxPolygonOverlap,
14
+ lineIntersection,
15
+ maskPolygonOverlap,
16
+ normalizeToPixel,
17
+ pointInPolygon,
18
+ tripwireCrossing
19
+ } from "./chunk-QMP6KSXZ.mjs";
938
20
  export {
939
21
  AnalysisPipeline,
940
22
  AnalysisPipelineAddon,