@camstack/lib-pipeline-analysis 0.1.2 → 0.1.3
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.js +1412 -1222
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1473 -1208
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
};
|
|
8
11
|
var __export = (target, all) => {
|
|
9
12
|
for (var name in all)
|
|
10
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -17,6 +20,7 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
20
|
}
|
|
18
21
|
return to;
|
|
19
22
|
};
|
|
23
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
20
24
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
25
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
26
|
// file that has been converted to a CommonJS file using a Babel-
|
|
@@ -27,1288 +31,1479 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
31
|
));
|
|
28
32
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
33
|
|
|
30
|
-
// src/
|
|
31
|
-
var
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
});
|
|
55
|
-
module.exports = __toCommonJS(src_exports);
|
|
56
|
-
|
|
57
|
-
// src/zones/geometry.ts
|
|
58
|
-
function pointInPolygon(point, polygon) {
|
|
59
|
-
if (polygon.length < 3) return false;
|
|
60
|
-
let inside = false;
|
|
61
|
-
const { x, y } = point;
|
|
62
|
-
const n = polygon.length;
|
|
63
|
-
for (let i = 0, j = n - 1; i < n; j = i++) {
|
|
64
|
-
const xi = polygon[i].x;
|
|
65
|
-
const yi = polygon[i].y;
|
|
66
|
-
const xj = polygon[j].x;
|
|
67
|
-
const yj = polygon[j].y;
|
|
68
|
-
const intersect = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi;
|
|
69
|
-
if (intersect) inside = !inside;
|
|
70
|
-
}
|
|
71
|
-
return inside;
|
|
72
|
-
}
|
|
73
|
-
function lineIntersection(a, b) {
|
|
74
|
-
const dx1 = a.p2.x - a.p1.x;
|
|
75
|
-
const dy1 = a.p2.y - a.p1.y;
|
|
76
|
-
const dx2 = b.p2.x - b.p1.x;
|
|
77
|
-
const dy2 = b.p2.y - b.p1.y;
|
|
78
|
-
const denom = dx1 * dy2 - dy1 * dx2;
|
|
79
|
-
if (Math.abs(denom) < 1e-10) return null;
|
|
80
|
-
const dx3 = b.p1.x - a.p1.x;
|
|
81
|
-
const dy3 = b.p1.y - a.p1.y;
|
|
82
|
-
const t = (dx3 * dy2 - dy3 * dx2) / denom;
|
|
83
|
-
const u = (dx3 * dy1 - dy3 * dx1) / denom;
|
|
84
|
-
if (t < 0 || t > 1 || u < 0 || u > 1) return null;
|
|
85
|
-
return {
|
|
86
|
-
x: a.p1.x + t * dx1,
|
|
87
|
-
y: a.p1.y + t * dy1
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
function tripwireCrossing(prev, curr, tripwire) {
|
|
91
|
-
const movement = { p1: prev, p2: curr };
|
|
92
|
-
const intersection = lineIntersection(movement, tripwire);
|
|
93
|
-
if (intersection === null) return null;
|
|
94
|
-
const twDx = tripwire.p2.x - tripwire.p1.x;
|
|
95
|
-
const twDy = tripwire.p2.y - tripwire.p1.y;
|
|
96
|
-
const movDx = curr.x - prev.x;
|
|
97
|
-
const movDy = curr.y - prev.y;
|
|
98
|
-
const cross = twDx * movDy - twDy * movDx;
|
|
99
|
-
const direction = cross > 0 ? "left" : "right";
|
|
100
|
-
return { crossed: true, direction };
|
|
101
|
-
}
|
|
102
|
-
function bboxCentroid(bbox) {
|
|
103
|
-
return {
|
|
104
|
-
x: bbox.x + bbox.w / 2,
|
|
105
|
-
y: bbox.y + bbox.h / 2
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
function normalizeToPixel(point, imageWidth, imageHeight) {
|
|
109
|
-
return {
|
|
110
|
-
x: point.x * imageWidth,
|
|
111
|
-
y: point.y * imageHeight
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// src/tracker/hungarian.ts
|
|
116
|
-
function greedyAssignment(costMatrix, threshold) {
|
|
117
|
-
const numTracks = costMatrix.length;
|
|
118
|
-
const numDetections = numTracks > 0 ? costMatrix[0]?.length ?? 0 : 0;
|
|
119
|
-
const candidates = [];
|
|
120
|
-
for (let t = 0; t < numTracks; t++) {
|
|
121
|
-
for (let d = 0; d < numDetections; d++) {
|
|
122
|
-
const iou2 = costMatrix[t][d] ?? 0;
|
|
123
|
-
if (iou2 >= threshold) {
|
|
124
|
-
candidates.push({ iou: iou2, trackIdx: t, detIdx: d });
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
candidates.sort((a, b) => b.iou - a.iou);
|
|
129
|
-
const assignedTracks = /* @__PURE__ */ new Set();
|
|
130
|
-
const assignedDets = /* @__PURE__ */ new Set();
|
|
131
|
-
const matches = [];
|
|
132
|
-
for (const { trackIdx, detIdx } of candidates) {
|
|
133
|
-
if (assignedTracks.has(trackIdx) || assignedDets.has(detIdx)) continue;
|
|
134
|
-
matches.push([trackIdx, detIdx]);
|
|
135
|
-
assignedTracks.add(trackIdx);
|
|
136
|
-
assignedDets.add(detIdx);
|
|
137
|
-
}
|
|
138
|
-
const unmatchedTracks = [];
|
|
139
|
-
for (let t = 0; t < numTracks; t++) {
|
|
140
|
-
if (!assignedTracks.has(t)) unmatchedTracks.push(t);
|
|
141
|
-
}
|
|
142
|
-
const unmatchedDetections = [];
|
|
143
|
-
for (let d = 0; d < numDetections; d++) {
|
|
144
|
-
if (!assignedDets.has(d)) unmatchedDetections.push(d);
|
|
145
|
-
}
|
|
146
|
-
return { matches, unmatchedTracks, unmatchedDetections };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// src/tracker/sort-tracker.ts
|
|
150
|
-
var MAX_PATH_LENGTH = 300;
|
|
151
|
-
function iou(a, b) {
|
|
152
|
-
const ax1 = a.x;
|
|
153
|
-
const ay1 = a.y;
|
|
154
|
-
const ax2 = a.x + a.w;
|
|
155
|
-
const ay2 = a.y + a.h;
|
|
156
|
-
const bx1 = b.x;
|
|
157
|
-
const by1 = b.y;
|
|
158
|
-
const bx2 = b.x + b.w;
|
|
159
|
-
const by2 = b.y + b.h;
|
|
160
|
-
const interX1 = Math.max(ax1, bx1);
|
|
161
|
-
const interY1 = Math.max(ay1, by1);
|
|
162
|
-
const interX2 = Math.min(ax2, bx2);
|
|
163
|
-
const interY2 = Math.min(ay2, by2);
|
|
164
|
-
const interW = Math.max(0, interX2 - interX1);
|
|
165
|
-
const interH = Math.max(0, interY2 - interY1);
|
|
166
|
-
const interArea = interW * interH;
|
|
167
|
-
if (interArea === 0) return 0;
|
|
168
|
-
const aArea = a.w * a.h;
|
|
169
|
-
const bArea = b.w * b.h;
|
|
170
|
-
const unionArea = aArea + bArea - interArea;
|
|
171
|
-
return unionArea <= 0 ? 0 : interArea / unionArea;
|
|
172
|
-
}
|
|
173
|
-
function trackToTrackedDetection(track) {
|
|
174
|
-
return {
|
|
175
|
-
class: track.class,
|
|
176
|
-
originalClass: track.originalClass,
|
|
177
|
-
score: track.score,
|
|
178
|
-
bbox: track.bbox,
|
|
179
|
-
landmarks: track.landmarks,
|
|
180
|
-
trackId: track.id,
|
|
181
|
-
trackAge: track.hits,
|
|
182
|
-
velocity: track.velocity,
|
|
183
|
-
path: track.path.slice()
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
function pushToRingBuffer(buffer, item, maxLength) {
|
|
187
|
-
if (buffer.length >= maxLength) {
|
|
188
|
-
buffer.shift();
|
|
189
|
-
}
|
|
190
|
-
buffer.push(item);
|
|
191
|
-
}
|
|
192
|
-
var DEFAULT_TRACKER_CONFIG = {
|
|
193
|
-
maxAge: 30,
|
|
194
|
-
minHits: 3,
|
|
195
|
-
iouThreshold: 0.3
|
|
196
|
-
};
|
|
197
|
-
var SortTracker = class {
|
|
198
|
-
tracks = [];
|
|
199
|
-
lostTracks = [];
|
|
200
|
-
nextTrackId = 1;
|
|
201
|
-
config;
|
|
202
|
-
constructor(config = {}) {
|
|
203
|
-
this.config = { ...DEFAULT_TRACKER_CONFIG, ...config };
|
|
204
|
-
}
|
|
205
|
-
update(detections, frameTimestamp) {
|
|
206
|
-
this.lostTracks = [];
|
|
207
|
-
if (this.tracks.length === 0) {
|
|
208
|
-
for (const det of detections) {
|
|
209
|
-
this.createTrack(det, frameTimestamp);
|
|
34
|
+
// src/zones/geometry.js
|
|
35
|
+
var require_geometry = __commonJS({
|
|
36
|
+
"src/zones/geometry.js"(exports2) {
|
|
37
|
+
"use strict";
|
|
38
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
39
|
+
exports2.pointInPolygon = pointInPolygon;
|
|
40
|
+
exports2.lineIntersection = lineIntersection;
|
|
41
|
+
exports2.tripwireCrossing = tripwireCrossing;
|
|
42
|
+
exports2.bboxCentroid = bboxCentroid;
|
|
43
|
+
exports2.normalizeToPixel = normalizeToPixel;
|
|
44
|
+
function pointInPolygon(point, polygon) {
|
|
45
|
+
if (polygon.length < 3)
|
|
46
|
+
return false;
|
|
47
|
+
let inside = false;
|
|
48
|
+
const { x, y } = point;
|
|
49
|
+
const n = polygon.length;
|
|
50
|
+
for (let i = 0, j = n - 1; i < n; j = i++) {
|
|
51
|
+
const xi = polygon[i].x;
|
|
52
|
+
const yi = polygon[i].y;
|
|
53
|
+
const xj = polygon[j].x;
|
|
54
|
+
const yj = polygon[j].y;
|
|
55
|
+
const intersect = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi;
|
|
56
|
+
if (intersect)
|
|
57
|
+
inside = !inside;
|
|
210
58
|
}
|
|
211
|
-
return
|
|
212
|
-
}
|
|
213
|
-
if (detections.length === 0) {
|
|
214
|
-
this.ageTracksAndPruneLost(frameTimestamp);
|
|
215
|
-
return this.getConfirmedTracks();
|
|
216
|
-
}
|
|
217
|
-
const costMatrix = this.tracks.map(
|
|
218
|
-
(track) => detections.map((det) => iou(track.bbox, det.bbox))
|
|
219
|
-
);
|
|
220
|
-
const { matches, unmatchedTracks, unmatchedDetections } = greedyAssignment(
|
|
221
|
-
costMatrix,
|
|
222
|
-
this.config.iouThreshold
|
|
223
|
-
);
|
|
224
|
-
for (const [trackIdx, detIdx] of matches) {
|
|
225
|
-
const track = this.tracks[trackIdx];
|
|
226
|
-
const det = detections[detIdx];
|
|
227
|
-
const prevCx = track.bbox.x + track.bbox.w / 2;
|
|
228
|
-
const prevCy = track.bbox.y + track.bbox.h / 2;
|
|
229
|
-
const newCx = det.bbox.x + det.bbox.w / 2;
|
|
230
|
-
const newCy = det.bbox.y + det.bbox.h / 2;
|
|
231
|
-
pushToRingBuffer(track.path, track.bbox, MAX_PATH_LENGTH);
|
|
232
|
-
track.bbox = det.bbox;
|
|
233
|
-
track.class = det.class;
|
|
234
|
-
track.originalClass = det.originalClass;
|
|
235
|
-
track.score = det.score;
|
|
236
|
-
track.landmarks = det.landmarks;
|
|
237
|
-
track.age = 0;
|
|
238
|
-
track.hits++;
|
|
239
|
-
track.lastSeen = frameTimestamp;
|
|
240
|
-
track.velocity = { dx: newCx - prevCx, dy: newCy - prevCy };
|
|
59
|
+
return inside;
|
|
241
60
|
}
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
61
|
+
function lineIntersection(a, b) {
|
|
62
|
+
const dx1 = a.p2.x - a.p1.x;
|
|
63
|
+
const dy1 = a.p2.y - a.p1.y;
|
|
64
|
+
const dx2 = b.p2.x - b.p1.x;
|
|
65
|
+
const dy2 = b.p2.y - b.p1.y;
|
|
66
|
+
const denom = dx1 * dy2 - dy1 * dx2;
|
|
67
|
+
if (Math.abs(denom) < 1e-10)
|
|
68
|
+
return null;
|
|
69
|
+
const dx3 = b.p1.x - a.p1.x;
|
|
70
|
+
const dy3 = b.p1.y - a.p1.y;
|
|
71
|
+
const t = (dx3 * dy2 - dy3 * dx2) / denom;
|
|
72
|
+
const u = (dx3 * dy1 - dy3 * dx1) / denom;
|
|
73
|
+
if (t < 0 || t > 1 || u < 0 || u > 1)
|
|
74
|
+
return null;
|
|
75
|
+
return {
|
|
76
|
+
x: a.p1.x + t * dx1,
|
|
77
|
+
y: a.p1.y + t * dy1
|
|
78
|
+
};
|
|
245
79
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
80
|
+
function tripwireCrossing(prev, curr, tripwire) {
|
|
81
|
+
const movement = { p1: prev, p2: curr };
|
|
82
|
+
const intersection = lineIntersection(movement, tripwire);
|
|
83
|
+
if (intersection === null)
|
|
84
|
+
return null;
|
|
85
|
+
const twDx = tripwire.p2.x - tripwire.p1.x;
|
|
86
|
+
const twDy = tripwire.p2.y - tripwire.p1.y;
|
|
87
|
+
const movDx = curr.x - prev.x;
|
|
88
|
+
const movDy = curr.y - prev.y;
|
|
89
|
+
const cross = twDx * movDy - twDy * movDx;
|
|
90
|
+
const direction = cross > 0 ? "left" : "right";
|
|
91
|
+
return { crossed: true, direction };
|
|
254
92
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
93
|
+
function bboxCentroid(bbox) {
|
|
94
|
+
return {
|
|
95
|
+
x: bbox.x + bbox.w / 2,
|
|
96
|
+
y: bbox.y + bbox.h / 2
|
|
97
|
+
};
|
|
259
98
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
getLostTracks() {
|
|
266
|
-
return this.lostTracks.map(trackToTrackedDetection);
|
|
267
|
-
}
|
|
268
|
-
reset() {
|
|
269
|
-
this.tracks = [];
|
|
270
|
-
this.lostTracks = [];
|
|
271
|
-
this.nextTrackId = 1;
|
|
272
|
-
}
|
|
273
|
-
createTrack(det, timestamp) {
|
|
274
|
-
const track = {
|
|
275
|
-
id: String(this.nextTrackId++),
|
|
276
|
-
bbox: det.bbox,
|
|
277
|
-
class: det.class,
|
|
278
|
-
originalClass: det.originalClass,
|
|
279
|
-
score: det.score,
|
|
280
|
-
landmarks: det.landmarks,
|
|
281
|
-
age: 0,
|
|
282
|
-
hits: 1,
|
|
283
|
-
path: [],
|
|
284
|
-
firstSeen: timestamp,
|
|
285
|
-
lastSeen: timestamp,
|
|
286
|
-
velocity: { dx: 0, dy: 0 },
|
|
287
|
-
lost: false
|
|
288
|
-
};
|
|
289
|
-
this.tracks.push(track);
|
|
290
|
-
return track;
|
|
291
|
-
}
|
|
292
|
-
getConfirmedTracks() {
|
|
293
|
-
return this.tracks.filter((t) => t.hits >= this.config.minHits).map(trackToTrackedDetection);
|
|
294
|
-
}
|
|
295
|
-
ageTracksAndPruneLost(frameTimestamp) {
|
|
296
|
-
const survived = [];
|
|
297
|
-
for (const track of this.tracks) {
|
|
298
|
-
track.age++;
|
|
299
|
-
if (track.age > this.config.maxAge) {
|
|
300
|
-
track.lost = true;
|
|
301
|
-
this.lostTracks.push(track);
|
|
302
|
-
} else {
|
|
303
|
-
survived.push(track);
|
|
304
|
-
}
|
|
99
|
+
function normalizeToPixel(point, imageWidth, imageHeight) {
|
|
100
|
+
return {
|
|
101
|
+
x: point.x * imageWidth,
|
|
102
|
+
y: point.y * imageHeight
|
|
103
|
+
};
|
|
305
104
|
}
|
|
306
|
-
this.tracks = survived;
|
|
307
|
-
void frameTimestamp;
|
|
308
105
|
}
|
|
309
|
-
};
|
|
106
|
+
});
|
|
310
107
|
|
|
311
|
-
// src/
|
|
312
|
-
var
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
analyze(tracks, timestamp) {
|
|
328
|
-
const results = [];
|
|
329
|
-
const activeIds = /* @__PURE__ */ new Set();
|
|
330
|
-
for (const track of tracks) {
|
|
331
|
-
activeIds.add(track.trackId);
|
|
332
|
-
const prev = this.states.get(track.trackId);
|
|
333
|
-
const centroid = {
|
|
334
|
-
x: track.bbox.x + track.bbox.w / 2,
|
|
335
|
-
y: track.bbox.y + track.bbox.h / 2
|
|
336
|
-
};
|
|
337
|
-
const speed = track.velocity ? magnitude(track.velocity) : 0;
|
|
338
|
-
let enteredAt;
|
|
339
|
-
let stationarySince;
|
|
340
|
-
let totalDistancePx;
|
|
341
|
-
if (!prev) {
|
|
342
|
-
enteredAt = timestamp;
|
|
343
|
-
stationarySince = speed <= this.config.velocityThreshold ? timestamp : void 0;
|
|
344
|
-
totalDistancePx = 0;
|
|
345
|
-
} else {
|
|
346
|
-
enteredAt = prev.enteredAt;
|
|
347
|
-
const dx = centroid.x - prev.lastPosition.x;
|
|
348
|
-
const dy = centroid.y - prev.lastPosition.y;
|
|
349
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
350
|
-
totalDistancePx = prev.totalDistancePx + dist;
|
|
351
|
-
if (speed > this.config.velocityThreshold) {
|
|
352
|
-
stationarySince = void 0;
|
|
353
|
-
} else {
|
|
354
|
-
stationarySince = prev.stationarySince ?? timestamp;
|
|
108
|
+
// src/tracker/hungarian.js
|
|
109
|
+
var require_hungarian = __commonJS({
|
|
110
|
+
"src/tracker/hungarian.js"(exports2) {
|
|
111
|
+
"use strict";
|
|
112
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
113
|
+
exports2.greedyAssignment = greedyAssignment2;
|
|
114
|
+
function greedyAssignment2(costMatrix, threshold) {
|
|
115
|
+
const numTracks = costMatrix.length;
|
|
116
|
+
const numDetections = numTracks > 0 ? costMatrix[0]?.length ?? 0 : 0;
|
|
117
|
+
const candidates = [];
|
|
118
|
+
for (let t = 0; t < numTracks; t++) {
|
|
119
|
+
for (let d = 0; d < numDetections; d++) {
|
|
120
|
+
const iou = costMatrix[t][d] ?? 0;
|
|
121
|
+
if (iou >= threshold) {
|
|
122
|
+
candidates.push({ iou, trackIdx: t, detIdx: d });
|
|
123
|
+
}
|
|
355
124
|
}
|
|
356
125
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
trackId: track.trackId,
|
|
368
|
-
state,
|
|
369
|
-
stationarySince,
|
|
370
|
-
enteredAt,
|
|
371
|
-
totalDistancePx,
|
|
372
|
-
dwellTimeMs
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
for (const [id, internalState] of this.states) {
|
|
376
|
-
if (!activeIds.has(id)) {
|
|
377
|
-
results.push({
|
|
378
|
-
trackId: id,
|
|
379
|
-
state: "leaving",
|
|
380
|
-
stationarySince: internalState.stationarySince,
|
|
381
|
-
enteredAt: internalState.enteredAt,
|
|
382
|
-
totalDistancePx: internalState.totalDistancePx,
|
|
383
|
-
dwellTimeMs: timestamp - internalState.enteredAt
|
|
384
|
-
});
|
|
385
|
-
this.states.delete(id);
|
|
126
|
+
candidates.sort((a, b) => b.iou - a.iou);
|
|
127
|
+
const assignedTracks = /* @__PURE__ */ new Set();
|
|
128
|
+
const assignedDets = /* @__PURE__ */ new Set();
|
|
129
|
+
const matches = [];
|
|
130
|
+
for (const { trackIdx, detIdx } of candidates) {
|
|
131
|
+
if (assignedTracks.has(trackIdx) || assignedDets.has(detIdx))
|
|
132
|
+
continue;
|
|
133
|
+
matches.push([trackIdx, detIdx]);
|
|
134
|
+
assignedTracks.add(trackIdx);
|
|
135
|
+
assignedDets.add(detIdx);
|
|
386
136
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (trackAge <= this.config.enteringFrames) {
|
|
392
|
-
return "entering";
|
|
393
|
-
}
|
|
394
|
-
if (stationarySince !== void 0) {
|
|
395
|
-
const stationaryDurationSec = (timestamp - stationarySince) / 1e3;
|
|
396
|
-
if (stationaryDurationSec >= this.config.loiteringThresholdSec) {
|
|
397
|
-
return "loitering";
|
|
137
|
+
const unmatchedTracks = [];
|
|
138
|
+
for (let t = 0; t < numTracks; t++) {
|
|
139
|
+
if (!assignedTracks.has(t))
|
|
140
|
+
unmatchedTracks.push(t);
|
|
398
141
|
}
|
|
399
|
-
|
|
400
|
-
|
|
142
|
+
const unmatchedDetections = [];
|
|
143
|
+
for (let d = 0; d < numDetections; d++) {
|
|
144
|
+
if (!assignedDets.has(d))
|
|
145
|
+
unmatchedDetections.push(d);
|
|
401
146
|
}
|
|
147
|
+
return { matches, unmatchedTracks, unmatchedDetections };
|
|
402
148
|
}
|
|
403
|
-
if (speed > this.config.velocityThreshold) {
|
|
404
|
-
return "moving";
|
|
405
|
-
}
|
|
406
|
-
return "moving";
|
|
407
149
|
}
|
|
408
|
-
};
|
|
150
|
+
});
|
|
409
151
|
|
|
410
|
-
// src/
|
|
411
|
-
var
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
const
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
152
|
+
// src/tracker/sort-tracker.js
|
|
153
|
+
var require_sort_tracker = __commonJS({
|
|
154
|
+
"src/tracker/sort-tracker.js"(exports2) {
|
|
155
|
+
"use strict";
|
|
156
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
157
|
+
exports2.SortTracker = exports2.DEFAULT_TRACKER_CONFIG = void 0;
|
|
158
|
+
var hungarian_js_1 = require_hungarian();
|
|
159
|
+
var MAX_PATH_LENGTH = 300;
|
|
160
|
+
function iou(a, b) {
|
|
161
|
+
const ax1 = a.x;
|
|
162
|
+
const ay1 = a.y;
|
|
163
|
+
const ax2 = a.x + a.w;
|
|
164
|
+
const ay2 = a.y + a.h;
|
|
165
|
+
const bx1 = b.x;
|
|
166
|
+
const by1 = b.y;
|
|
167
|
+
const bx2 = b.x + b.w;
|
|
168
|
+
const by2 = b.y + b.h;
|
|
169
|
+
const interX1 = Math.max(ax1, bx1);
|
|
170
|
+
const interY1 = Math.max(ay1, by1);
|
|
171
|
+
const interX2 = Math.min(ax2, bx2);
|
|
172
|
+
const interY2 = Math.min(ay2, by2);
|
|
173
|
+
const interW = Math.max(0, interX2 - interX1);
|
|
174
|
+
const interH = Math.max(0, interY2 - interY1);
|
|
175
|
+
const interArea = interW * interH;
|
|
176
|
+
if (interArea === 0)
|
|
177
|
+
return 0;
|
|
178
|
+
const aArea = a.w * a.h;
|
|
179
|
+
const bArea = b.w * b.h;
|
|
180
|
+
const unionArea = aArea + bArea - interArea;
|
|
181
|
+
return unionArea <= 0 ? 0 : interArea / unionArea;
|
|
182
|
+
}
|
|
183
|
+
function trackToTrackedDetection(track) {
|
|
184
|
+
return {
|
|
185
|
+
class: track.class,
|
|
186
|
+
originalClass: track.originalClass,
|
|
187
|
+
score: track.score,
|
|
188
|
+
bbox: track.bbox,
|
|
189
|
+
landmarks: track.landmarks,
|
|
190
|
+
trackId: track.id,
|
|
191
|
+
trackAge: track.hits,
|
|
192
|
+
velocity: track.velocity,
|
|
193
|
+
path: track.path.slice()
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function pushToRingBuffer(buffer, item, maxLength) {
|
|
197
|
+
if (buffer.length >= maxLength) {
|
|
198
|
+
buffer.shift();
|
|
199
|
+
}
|
|
200
|
+
buffer.push(item);
|
|
201
|
+
}
|
|
202
|
+
exports2.DEFAULT_TRACKER_CONFIG = {
|
|
203
|
+
maxAge: 30,
|
|
204
|
+
minHits: 3,
|
|
205
|
+
iouThreshold: 0.3
|
|
206
|
+
};
|
|
207
|
+
var SortTracker2 = class {
|
|
208
|
+
tracks = [];
|
|
209
|
+
lostTracks = [];
|
|
210
|
+
nextTrackId = 1;
|
|
211
|
+
config;
|
|
212
|
+
constructor(config = {}) {
|
|
213
|
+
this.config = { ...exports2.DEFAULT_TRACKER_CONFIG, ...config };
|
|
214
|
+
}
|
|
215
|
+
update(detections, frameTimestamp) {
|
|
216
|
+
this.lostTracks = [];
|
|
217
|
+
if (this.tracks.length === 0) {
|
|
218
|
+
for (const det of detections) {
|
|
219
|
+
this.createTrack(det, frameTimestamp);
|
|
220
|
+
}
|
|
221
|
+
return this.getConfirmedTracks();
|
|
222
|
+
}
|
|
223
|
+
if (detections.length === 0) {
|
|
224
|
+
this.ageTracksAndPruneLost(frameTimestamp);
|
|
225
|
+
return this.getConfirmedTracks();
|
|
226
|
+
}
|
|
227
|
+
const costMatrix = this.tracks.map((track) => detections.map((det) => iou(track.bbox, det.bbox)));
|
|
228
|
+
const { matches, unmatchedTracks, unmatchedDetections } = (0, hungarian_js_1.greedyAssignment)(costMatrix, this.config.iouThreshold);
|
|
229
|
+
for (const [trackIdx, detIdx] of matches) {
|
|
230
|
+
const track = this.tracks[trackIdx];
|
|
231
|
+
const det = detections[detIdx];
|
|
232
|
+
const prevCx = track.bbox.x + track.bbox.w / 2;
|
|
233
|
+
const prevCy = track.bbox.y + track.bbox.h / 2;
|
|
234
|
+
const newCx = det.bbox.x + det.bbox.w / 2;
|
|
235
|
+
const newCy = det.bbox.y + det.bbox.h / 2;
|
|
236
|
+
pushToRingBuffer(track.path, track.bbox, MAX_PATH_LENGTH);
|
|
237
|
+
track.bbox = det.bbox;
|
|
238
|
+
track.class = det.class;
|
|
239
|
+
track.originalClass = det.originalClass;
|
|
240
|
+
track.score = det.score;
|
|
241
|
+
track.landmarks = det.landmarks;
|
|
242
|
+
track.age = 0;
|
|
243
|
+
track.hits++;
|
|
244
|
+
track.lastSeen = frameTimestamp;
|
|
245
|
+
track.velocity = { dx: newCx - prevCx, dy: newCy - prevCy };
|
|
425
246
|
}
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
247
|
+
for (const trackIdx of unmatchedTracks) {
|
|
248
|
+
const track = this.tracks[trackIdx];
|
|
249
|
+
track.age++;
|
|
250
|
+
}
|
|
251
|
+
const survived = [];
|
|
252
|
+
for (const track of this.tracks) {
|
|
253
|
+
if (track.age > this.config.maxAge) {
|
|
254
|
+
track.lost = true;
|
|
255
|
+
this.lostTracks.push(track);
|
|
256
|
+
} else {
|
|
257
|
+
survived.push(track);
|
|
431
258
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
259
|
+
}
|
|
260
|
+
this.tracks = survived;
|
|
261
|
+
for (const detIdx of unmatchedDetections) {
|
|
262
|
+
const det = detections[detIdx];
|
|
263
|
+
this.createTrack(det, frameTimestamp);
|
|
264
|
+
}
|
|
265
|
+
return this.getConfirmedTracks();
|
|
266
|
+
}
|
|
267
|
+
getActiveTracks() {
|
|
268
|
+
return this.tracks.map(trackToTrackedDetection);
|
|
269
|
+
}
|
|
270
|
+
getLostTracks() {
|
|
271
|
+
return this.lostTracks.map(trackToTrackedDetection);
|
|
272
|
+
}
|
|
273
|
+
reset() {
|
|
274
|
+
this.tracks = [];
|
|
275
|
+
this.lostTracks = [];
|
|
276
|
+
this.nextTrackId = 1;
|
|
277
|
+
}
|
|
278
|
+
createTrack(det, timestamp) {
|
|
279
|
+
const track = {
|
|
280
|
+
id: String(this.nextTrackId++),
|
|
281
|
+
bbox: det.bbox,
|
|
282
|
+
class: det.class,
|
|
283
|
+
originalClass: det.originalClass,
|
|
284
|
+
score: det.score,
|
|
285
|
+
landmarks: det.landmarks,
|
|
286
|
+
age: 0,
|
|
287
|
+
hits: 1,
|
|
288
|
+
path: [],
|
|
289
|
+
firstSeen: timestamp,
|
|
290
|
+
lastSeen: timestamp,
|
|
291
|
+
velocity: { dx: 0, dy: 0 },
|
|
292
|
+
lost: false
|
|
293
|
+
};
|
|
294
|
+
this.tracks.push(track);
|
|
295
|
+
return track;
|
|
296
|
+
}
|
|
297
|
+
getConfirmedTracks() {
|
|
298
|
+
return this.tracks.filter((t) => t.hits >= this.config.minHits).map(trackToTrackedDetection);
|
|
299
|
+
}
|
|
300
|
+
ageTracksAndPruneLost(frameTimestamp) {
|
|
301
|
+
const survived = [];
|
|
302
|
+
for (const track of this.tracks) {
|
|
303
|
+
track.age++;
|
|
304
|
+
if (track.age > this.config.maxAge) {
|
|
305
|
+
track.lost = true;
|
|
306
|
+
this.lostTracks.push(track);
|
|
307
|
+
} else {
|
|
308
|
+
survived.push(track);
|
|
441
309
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
310
|
+
}
|
|
311
|
+
this.tracks = survived;
|
|
312
|
+
void frameTimestamp;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
exports2.SortTracker = SortTracker2;
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// src/state/state-analyzer.js
|
|
320
|
+
var require_state_analyzer = __commonJS({
|
|
321
|
+
"src/state/state-analyzer.js"(exports2) {
|
|
322
|
+
"use strict";
|
|
323
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
324
|
+
exports2.StateAnalyzer = exports2.DEFAULT_STATE_ANALYZER_CONFIG = void 0;
|
|
325
|
+
exports2.DEFAULT_STATE_ANALYZER_CONFIG = {
|
|
326
|
+
stationaryThresholdSec: 10,
|
|
327
|
+
loiteringThresholdSec: 60,
|
|
328
|
+
velocityThreshold: 2,
|
|
329
|
+
enteringFrames: 5
|
|
330
|
+
};
|
|
331
|
+
function magnitude(v) {
|
|
332
|
+
return Math.sqrt(v.dx * v.dx + v.dy * v.dy);
|
|
333
|
+
}
|
|
334
|
+
var StateAnalyzer2 = class {
|
|
335
|
+
states = /* @__PURE__ */ new Map();
|
|
336
|
+
config;
|
|
337
|
+
constructor(config = {}) {
|
|
338
|
+
this.config = { ...exports2.DEFAULT_STATE_ANALYZER_CONFIG, ...config };
|
|
339
|
+
}
|
|
340
|
+
analyze(tracks, timestamp) {
|
|
341
|
+
const results = [];
|
|
342
|
+
const activeIds = /* @__PURE__ */ new Set();
|
|
343
|
+
for (const track of tracks) {
|
|
344
|
+
activeIds.add(track.trackId);
|
|
345
|
+
const prev = this.states.get(track.trackId);
|
|
346
|
+
const centroid = {
|
|
347
|
+
x: track.bbox.x + track.bbox.w / 2,
|
|
348
|
+
y: track.bbox.y + track.bbox.h / 2
|
|
349
|
+
};
|
|
350
|
+
const speed = track.velocity ? magnitude(track.velocity) : 0;
|
|
351
|
+
let enteredAt;
|
|
352
|
+
let stationarySince;
|
|
353
|
+
let totalDistancePx;
|
|
354
|
+
if (!prev) {
|
|
355
|
+
enteredAt = timestamp;
|
|
356
|
+
stationarySince = speed <= this.config.velocityThreshold ? timestamp : void 0;
|
|
357
|
+
totalDistancePx = 0;
|
|
358
|
+
} else {
|
|
359
|
+
enteredAt = prev.enteredAt;
|
|
360
|
+
const dx = centroid.x - prev.lastPosition.x;
|
|
361
|
+
const dy = centroid.y - prev.lastPosition.y;
|
|
362
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
363
|
+
totalDistancePx = prev.totalDistancePx + dist;
|
|
364
|
+
if (speed > this.config.velocityThreshold) {
|
|
365
|
+
stationarySince = void 0;
|
|
366
|
+
} else {
|
|
367
|
+
stationarySince = prev.stationarySince ?? timestamp;
|
|
368
|
+
}
|
|
451
369
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const p1 = zone.points[1];
|
|
458
|
-
if (!p0 || !p1) continue;
|
|
459
|
-
const tripwireLine = {
|
|
460
|
-
p1: normalizeToPixel(p0, imageWidth, imageHeight),
|
|
461
|
-
p2: normalizeToPixel(p1, imageWidth, imageHeight)
|
|
370
|
+
const newInternal = {
|
|
371
|
+
enteredAt,
|
|
372
|
+
stationarySince,
|
|
373
|
+
totalDistancePx,
|
|
374
|
+
lastPosition: centroid
|
|
462
375
|
};
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
376
|
+
this.states.set(track.trackId, newInternal);
|
|
377
|
+
const dwellTimeMs = timestamp - enteredAt;
|
|
378
|
+
const state = this.computeObjectState(track.trackAge, speed, stationarySince, timestamp);
|
|
379
|
+
results.push({
|
|
380
|
+
trackId: track.trackId,
|
|
381
|
+
state,
|
|
382
|
+
stationarySince,
|
|
383
|
+
enteredAt,
|
|
384
|
+
totalDistancePx,
|
|
385
|
+
dwellTimeMs
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
for (const [id, internalState] of this.states) {
|
|
389
|
+
if (!activeIds.has(id)) {
|
|
390
|
+
results.push({
|
|
391
|
+
trackId: id,
|
|
392
|
+
state: "leaving",
|
|
393
|
+
stationarySince: internalState.stationarySince,
|
|
394
|
+
enteredAt: internalState.enteredAt,
|
|
395
|
+
totalDistancePx: internalState.totalDistancePx,
|
|
396
|
+
dwellTimeMs: timestamp - internalState.enteredAt
|
|
473
397
|
});
|
|
398
|
+
this.states.delete(id);
|
|
474
399
|
}
|
|
475
400
|
}
|
|
401
|
+
return results;
|
|
476
402
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
403
|
+
computeObjectState(trackAge, speed, stationarySince, timestamp) {
|
|
404
|
+
if (trackAge <= this.config.enteringFrames) {
|
|
405
|
+
return "entering";
|
|
406
|
+
}
|
|
407
|
+
if (stationarySince !== void 0) {
|
|
408
|
+
const stationaryDurationSec = (timestamp - stationarySince) / 1e3;
|
|
409
|
+
if (stationaryDurationSec >= this.config.loiteringThresholdSec) {
|
|
410
|
+
return "loitering";
|
|
411
|
+
}
|
|
412
|
+
if (stationaryDurationSec >= this.config.stationaryThresholdSec) {
|
|
413
|
+
return "stationary";
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (speed > this.config.velocityThreshold) {
|
|
417
|
+
return "moving";
|
|
418
|
+
}
|
|
419
|
+
return "moving";
|
|
482
420
|
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
/** Returns a snapshot of the current track → zones mapping */
|
|
487
|
-
getTrackZones() {
|
|
488
|
-
return new Map(this.trackZoneState);
|
|
421
|
+
};
|
|
422
|
+
exports2.StateAnalyzer = StateAnalyzer2;
|
|
489
423
|
}
|
|
490
|
-
};
|
|
424
|
+
});
|
|
491
425
|
|
|
492
|
-
// src/
|
|
493
|
-
var
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
426
|
+
// src/zones/zone-evaluator.js
|
|
427
|
+
var require_zone_evaluator = __commonJS({
|
|
428
|
+
"src/zones/zone-evaluator.js"(exports2) {
|
|
429
|
+
"use strict";
|
|
430
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
431
|
+
exports2.ZoneEvaluator = void 0;
|
|
432
|
+
var geometry_js_1 = require_geometry();
|
|
433
|
+
var ZoneEvaluator2 = class {
|
|
434
|
+
/** Track which zones each track was in last frame */
|
|
435
|
+
trackZoneState = /* @__PURE__ */ new Map();
|
|
436
|
+
evaluate(tracks, zones, imageWidth, imageHeight, timestamp) {
|
|
437
|
+
const events = [];
|
|
438
|
+
const activeTrackIds = /* @__PURE__ */ new Set();
|
|
439
|
+
for (const track of tracks) {
|
|
440
|
+
activeTrackIds.add(track.trackId);
|
|
441
|
+
const centroid = (0, geometry_js_1.bboxCentroid)(track.bbox);
|
|
442
|
+
const prevZones = this.trackZoneState.get(track.trackId) ?? /* @__PURE__ */ new Set();
|
|
443
|
+
const currZones = /* @__PURE__ */ new Set();
|
|
444
|
+
for (const zone of zones) {
|
|
445
|
+
if (zone.alertOnClasses && zone.alertOnClasses.length > 0) {
|
|
446
|
+
if (!zone.alertOnClasses.includes(track.class))
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (zone.type === "polygon") {
|
|
450
|
+
const pixelPoints = zone.points.map((p) => (0, geometry_js_1.normalizeToPixel)(p, imageWidth, imageHeight));
|
|
451
|
+
const inside = (0, geometry_js_1.pointInPolygon)(centroid, pixelPoints);
|
|
452
|
+
if (inside) {
|
|
453
|
+
currZones.add(zone.id);
|
|
454
|
+
}
|
|
455
|
+
if (inside && !prevZones.has(zone.id)) {
|
|
456
|
+
events.push({
|
|
457
|
+
type: "zone-enter",
|
|
458
|
+
zoneId: zone.id,
|
|
459
|
+
zoneName: zone.name,
|
|
460
|
+
trackId: track.trackId,
|
|
461
|
+
detection: track,
|
|
462
|
+
timestamp
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
if (!inside && prevZones.has(zone.id)) {
|
|
466
|
+
events.push({
|
|
467
|
+
type: "zone-exit",
|
|
468
|
+
zoneId: zone.id,
|
|
469
|
+
zoneName: zone.name,
|
|
470
|
+
trackId: track.trackId,
|
|
471
|
+
detection: track,
|
|
472
|
+
timestamp
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
} else if (zone.type === "tripwire" && track.path.length >= 1) {
|
|
476
|
+
const prevBbox = track.path[track.path.length - 1];
|
|
477
|
+
if (!prevBbox)
|
|
478
|
+
continue;
|
|
479
|
+
const prevCentroid = (0, geometry_js_1.bboxCentroid)(prevBbox);
|
|
480
|
+
const p0 = zone.points[0];
|
|
481
|
+
const p1 = zone.points[1];
|
|
482
|
+
if (!p0 || !p1)
|
|
483
|
+
continue;
|
|
484
|
+
const tripwireLine = {
|
|
485
|
+
p1: (0, geometry_js_1.normalizeToPixel)(p0, imageWidth, imageHeight),
|
|
486
|
+
p2: (0, geometry_js_1.normalizeToPixel)(p1, imageWidth, imageHeight)
|
|
487
|
+
};
|
|
488
|
+
const cross = (0, geometry_js_1.tripwireCrossing)(prevCentroid, centroid, tripwireLine);
|
|
489
|
+
if (cross?.crossed) {
|
|
490
|
+
events.push({
|
|
491
|
+
type: "tripwire-cross",
|
|
492
|
+
zoneId: zone.id,
|
|
493
|
+
zoneName: zone.name,
|
|
494
|
+
trackId: track.trackId,
|
|
495
|
+
detection: track,
|
|
496
|
+
direction: cross.direction,
|
|
497
|
+
timestamp
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
this.trackZoneState.set(track.trackId, currZones);
|
|
503
|
+
}
|
|
504
|
+
for (const trackId of this.trackZoneState.keys()) {
|
|
505
|
+
if (!activeTrackIds.has(trackId)) {
|
|
506
|
+
this.trackZoneState.delete(trackId);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return events;
|
|
534
510
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
511
|
+
/** Returns a snapshot of the current track → zones mapping */
|
|
512
|
+
getTrackZones() {
|
|
513
|
+
return new Map(this.trackZoneState);
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
exports2.ZoneEvaluator = ZoneEvaluator2;
|
|
540
517
|
}
|
|
541
|
-
};
|
|
518
|
+
});
|
|
542
519
|
|
|
543
|
-
// src/events/event-
|
|
544
|
-
var
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
this.
|
|
568
|
-
}
|
|
569
|
-
if (state.state === "leaving") {
|
|
570
|
-
this.tryEmit(events, "object.leaving", track, state, classifs, [], deviceId, timestamp);
|
|
571
|
-
}
|
|
572
|
-
if (state.state === "stationary" && prevState !== void 0 && prevState !== "stationary" && prevState !== "loitering") {
|
|
573
|
-
this.tryEmit(events, "object.stationary", track, state, classifs, [], deviceId, timestamp);
|
|
574
|
-
}
|
|
575
|
-
if (state.state === "loitering" && prevState === "stationary") {
|
|
576
|
-
this.tryEmit(events, "object.loitering", track, state, classifs, [], deviceId, timestamp);
|
|
577
|
-
}
|
|
578
|
-
if (state.state !== "leaving") {
|
|
579
|
-
this.previousStates.set(state.trackId, state.state);
|
|
580
|
-
} else {
|
|
581
|
-
this.previousStates.delete(state.trackId);
|
|
520
|
+
// src/events/event-filters.js
|
|
521
|
+
var require_event_filters = __commonJS({
|
|
522
|
+
"src/events/event-filters.js"(exports2) {
|
|
523
|
+
"use strict";
|
|
524
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
525
|
+
exports2.EventFilter = exports2.DEFAULT_EVENT_FILTER_CONFIG = void 0;
|
|
526
|
+
exports2.DEFAULT_EVENT_FILTER_CONFIG = {
|
|
527
|
+
minTrackAge: 3,
|
|
528
|
+
cooldownSec: 5,
|
|
529
|
+
enabledTypes: [
|
|
530
|
+
"object.entering",
|
|
531
|
+
"object.leaving",
|
|
532
|
+
"object.stationary",
|
|
533
|
+
"object.loitering",
|
|
534
|
+
"zone.enter",
|
|
535
|
+
"zone.exit",
|
|
536
|
+
"tripwire.cross"
|
|
537
|
+
]
|
|
538
|
+
};
|
|
539
|
+
var EventFilter2 = class {
|
|
540
|
+
config;
|
|
541
|
+
/** key: `${trackId}:${eventType}` → last emitted timestamp */
|
|
542
|
+
lastEmitted = /* @__PURE__ */ new Map();
|
|
543
|
+
constructor(config) {
|
|
544
|
+
this.config = config;
|
|
582
545
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
reset() {
|
|
619
|
-
this.previousStates.clear();
|
|
620
|
-
this.filter.reset();
|
|
546
|
+
shouldEmit(trackId, eventType, trackAge, timestamp) {
|
|
547
|
+
if (!this.config.enabledTypes.includes(eventType))
|
|
548
|
+
return false;
|
|
549
|
+
if (trackAge < this.config.minTrackAge)
|
|
550
|
+
return false;
|
|
551
|
+
const key = `${trackId}:${eventType}`;
|
|
552
|
+
const last = this.lastEmitted.get(key);
|
|
553
|
+
if (last !== void 0 && timestamp - last < this.config.cooldownSec * 1e3)
|
|
554
|
+
return false;
|
|
555
|
+
this.lastEmitted.set(key, timestamp);
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
/** Record an emission without a gate check — for events that bypass normal cooldown logic */
|
|
559
|
+
recordEmission(trackId, eventType, timestamp) {
|
|
560
|
+
const key = `${trackId}:${eventType}`;
|
|
561
|
+
this.lastEmitted.set(key, timestamp);
|
|
562
|
+
}
|
|
563
|
+
/** Remove cooldown entries for tracks that are no longer active */
|
|
564
|
+
cleanup(activeTrackIds) {
|
|
565
|
+
for (const key of this.lastEmitted.keys()) {
|
|
566
|
+
const colonIdx = key.indexOf(":");
|
|
567
|
+
if (colonIdx === -1)
|
|
568
|
+
continue;
|
|
569
|
+
const trackId = key.slice(0, colonIdx);
|
|
570
|
+
if (!activeTrackIds.has(trackId)) {
|
|
571
|
+
this.lastEmitted.delete(key);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/** Reset all cooldown state */
|
|
576
|
+
reset() {
|
|
577
|
+
this.lastEmitted.clear();
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
exports2.EventFilter = EventFilter2;
|
|
621
581
|
}
|
|
622
|
-
};
|
|
582
|
+
});
|
|
623
583
|
|
|
624
|
-
// src/
|
|
625
|
-
var
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
trackId: track.trackId,
|
|
639
|
-
class: track.class,
|
|
640
|
-
originalClass: track.originalClass,
|
|
641
|
-
state: "entering",
|
|
642
|
-
firstSeen: track.path.length === 0 ? now : now,
|
|
643
|
-
lastSeen: now,
|
|
644
|
-
path: [],
|
|
645
|
-
zoneTransitions: [],
|
|
646
|
-
events: []
|
|
647
|
-
};
|
|
648
|
-
this.activeTracks.set(track.trackId, acc);
|
|
649
|
-
}
|
|
650
|
-
acc.class = track.class;
|
|
651
|
-
acc.originalClass = track.originalClass;
|
|
652
|
-
acc.lastSeen = now;
|
|
653
|
-
const state = stateMap.get(track.trackId);
|
|
654
|
-
if (state) acc.state = state.state;
|
|
655
|
-
const pathPoint = {
|
|
656
|
-
timestamp: now,
|
|
657
|
-
bbox: track.bbox,
|
|
658
|
-
velocity: track.velocity ?? { dx: 0, dy: 0 }
|
|
659
|
-
};
|
|
660
|
-
if (acc.path.length >= MAX_PATH_LENGTH2) {
|
|
661
|
-
acc.path.shift();
|
|
662
|
-
}
|
|
663
|
-
acc.path.push(pathPoint);
|
|
664
|
-
const classifs = classifMap.get(track.trackId) ?? [];
|
|
665
|
-
for (const c of classifs) {
|
|
666
|
-
if (c.metadata?.type === "face-recognition" && typeof c.text === "string") {
|
|
667
|
-
acc.identity = { name: c.text, confidence: c.score, matchedAt: now };
|
|
668
|
-
} else if (c.metadata?.type === "plate-recognition" && typeof c.text === "string") {
|
|
669
|
-
acc.plateText = { text: c.text, confidence: c.score, readAt: now };
|
|
670
|
-
} else if (c.metadata?.type === "sub-class") {
|
|
671
|
-
acc.subClass = { class: c.class, confidence: c.score };
|
|
672
|
-
}
|
|
584
|
+
// src/events/event-emitter.js
|
|
585
|
+
var require_event_emitter = __commonJS({
|
|
586
|
+
"src/events/event-emitter.js"(exports2) {
|
|
587
|
+
"use strict";
|
|
588
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
589
|
+
exports2.DetectionEventEmitter = void 0;
|
|
590
|
+
var node_crypto_1 = require("crypto");
|
|
591
|
+
var event_filters_js_1 = require_event_filters();
|
|
592
|
+
var DetectionEventEmitter2 = class {
|
|
593
|
+
filter;
|
|
594
|
+
/** trackId → last known ObjectState */
|
|
595
|
+
previousStates = /* @__PURE__ */ new Map();
|
|
596
|
+
constructor(filterConfig = {}) {
|
|
597
|
+
this.filter = new event_filters_js_1.EventFilter({ ...event_filters_js_1.DEFAULT_EVENT_FILTER_CONFIG, ...filterConfig });
|
|
673
598
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
599
|
+
emit(tracks, states, zoneEvents, classifications, deviceId) {
|
|
600
|
+
const events = [];
|
|
601
|
+
const timestamp = Date.now();
|
|
602
|
+
const trackMap = /* @__PURE__ */ new Map();
|
|
603
|
+
for (const t of tracks)
|
|
604
|
+
trackMap.set(t.trackId, t);
|
|
605
|
+
const stateMap = /* @__PURE__ */ new Map();
|
|
606
|
+
for (const s of states)
|
|
607
|
+
stateMap.set(s.trackId, s);
|
|
608
|
+
const classifMap = /* @__PURE__ */ new Map();
|
|
609
|
+
for (const c of classifications)
|
|
610
|
+
classifMap.set(c.trackId, c.classifications);
|
|
611
|
+
for (const state of states) {
|
|
612
|
+
const track = trackMap.get(state.trackId);
|
|
613
|
+
if (!track)
|
|
614
|
+
continue;
|
|
615
|
+
const prevState = this.previousStates.get(state.trackId);
|
|
616
|
+
const classifs = classifMap.get(state.trackId) ?? [];
|
|
617
|
+
if (state.state === "entering" && prevState === void 0) {
|
|
618
|
+
this.tryEmit(events, "object.entering", track, state, classifs, [], deviceId, timestamp);
|
|
619
|
+
}
|
|
620
|
+
if (state.state === "leaving") {
|
|
621
|
+
this.tryEmit(events, "object.leaving", track, state, classifs, [], deviceId, timestamp);
|
|
622
|
+
}
|
|
623
|
+
if (state.state === "stationary" && prevState !== void 0 && prevState !== "stationary" && prevState !== "loitering") {
|
|
624
|
+
this.tryEmit(events, "object.stationary", track, state, classifs, [], deviceId, timestamp);
|
|
625
|
+
}
|
|
626
|
+
if (state.state === "loitering" && prevState === "stationary") {
|
|
627
|
+
this.tryEmit(events, "object.loitering", track, state, classifs, [], deviceId, timestamp);
|
|
628
|
+
}
|
|
629
|
+
if (state.state !== "leaving") {
|
|
630
|
+
this.previousStates.set(state.trackId, state.state);
|
|
631
|
+
} else {
|
|
632
|
+
this.previousStates.delete(state.trackId);
|
|
692
633
|
}
|
|
693
634
|
}
|
|
694
|
-
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
635
|
+
for (const ze of zoneEvents) {
|
|
636
|
+
const track = trackMap.get(ze.trackId);
|
|
637
|
+
if (!track)
|
|
638
|
+
continue;
|
|
639
|
+
const state = stateMap.get(ze.trackId);
|
|
640
|
+
if (!state)
|
|
641
|
+
continue;
|
|
642
|
+
let eventType;
|
|
643
|
+
if (ze.type === "tripwire-cross") {
|
|
644
|
+
eventType = "tripwire.cross";
|
|
645
|
+
} else if (ze.type === "zone-enter") {
|
|
646
|
+
eventType = "zone.enter";
|
|
647
|
+
} else {
|
|
648
|
+
eventType = "zone.exit";
|
|
649
|
+
}
|
|
650
|
+
const classifs = classifMap.get(ze.trackId) ?? [];
|
|
651
|
+
this.tryEmit(events, eventType, track, state, classifs, [ze], deviceId, timestamp);
|
|
701
652
|
}
|
|
653
|
+
const activeIds = new Set(tracks.map((t) => t.trackId));
|
|
654
|
+
this.filter.cleanup(activeIds);
|
|
655
|
+
return events;
|
|
656
|
+
}
|
|
657
|
+
tryEmit(events, eventType, track, state, classifications, zoneEvents, deviceId, timestamp) {
|
|
658
|
+
if (!this.filter.shouldEmit(track.trackId, eventType, track.trackAge, timestamp))
|
|
659
|
+
return;
|
|
660
|
+
events.push({
|
|
661
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
662
|
+
type: eventType,
|
|
663
|
+
timestamp,
|
|
664
|
+
deviceId,
|
|
665
|
+
detection: track,
|
|
666
|
+
classifications,
|
|
667
|
+
objectState: state,
|
|
668
|
+
zoneEvents,
|
|
669
|
+
trackPath: track.path
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
reset() {
|
|
673
|
+
this.previousStates.clear();
|
|
674
|
+
this.filter.reset();
|
|
702
675
|
}
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
/** Attach a DetectionEvent to its associated track accumulator */
|
|
706
|
-
addEvent(trackId, event) {
|
|
707
|
-
const acc = this.activeTracks.get(trackId);
|
|
708
|
-
if (!acc) return;
|
|
709
|
-
acc.events.push(event);
|
|
710
|
-
if (event.snapshot && !acc.bestSnapshot) {
|
|
711
|
-
acc.bestSnapshot = event.snapshot;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
/** Get accumulated detail for an active track */
|
|
715
|
-
getTrackDetail(trackId) {
|
|
716
|
-
const acc = this.activeTracks.get(trackId);
|
|
717
|
-
if (!acc) return null;
|
|
718
|
-
return this.buildTrackDetail(acc);
|
|
719
|
-
}
|
|
720
|
-
/** Called when a track ends — returns complete TrackDetail and removes from active set */
|
|
721
|
-
finishTrack(trackId) {
|
|
722
|
-
const acc = this.activeTracks.get(trackId);
|
|
723
|
-
if (!acc) return null;
|
|
724
|
-
const detail = this.buildTrackDetail(acc);
|
|
725
|
-
this.activeTracks.delete(trackId);
|
|
726
|
-
return detail;
|
|
727
|
-
}
|
|
728
|
-
/** Get all active track IDs */
|
|
729
|
-
getActiveTrackIds() {
|
|
730
|
-
return Array.from(this.activeTracks.keys());
|
|
731
|
-
}
|
|
732
|
-
/** Clear all accumulated state */
|
|
733
|
-
reset() {
|
|
734
|
-
this.activeTracks.clear();
|
|
735
|
-
}
|
|
736
|
-
buildTrackDetail(acc) {
|
|
737
|
-
return {
|
|
738
|
-
trackId: acc.trackId,
|
|
739
|
-
class: acc.class,
|
|
740
|
-
originalClass: acc.originalClass,
|
|
741
|
-
state: acc.state,
|
|
742
|
-
firstSeen: acc.firstSeen,
|
|
743
|
-
lastSeen: acc.lastSeen,
|
|
744
|
-
totalDwellMs: acc.lastSeen - acc.firstSeen,
|
|
745
|
-
path: acc.path,
|
|
746
|
-
identity: acc.identity,
|
|
747
|
-
plateText: acc.plateText,
|
|
748
|
-
subClass: acc.subClass,
|
|
749
|
-
zoneTransitions: acc.zoneTransitions,
|
|
750
|
-
bestSnapshot: acc.bestSnapshot,
|
|
751
|
-
events: acc.events
|
|
752
676
|
};
|
|
677
|
+
exports2.DetectionEventEmitter = DetectionEventEmitter2;
|
|
753
678
|
}
|
|
754
|
-
};
|
|
679
|
+
});
|
|
755
680
|
|
|
756
|
-
// src/analytics/
|
|
757
|
-
var
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
681
|
+
// src/analytics/track-store.js
|
|
682
|
+
var require_track_store = __commonJS({
|
|
683
|
+
"src/analytics/track-store.js"(exports2) {
|
|
684
|
+
"use strict";
|
|
685
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
686
|
+
exports2.TrackStore = void 0;
|
|
687
|
+
var MAX_PATH_LENGTH = 300;
|
|
688
|
+
var TrackStore2 = class {
|
|
689
|
+
activeTracks = /* @__PURE__ */ new Map();
|
|
690
|
+
update(tracks, states, zoneEvents, classifications) {
|
|
691
|
+
const stateMap = /* @__PURE__ */ new Map();
|
|
692
|
+
for (const s of states)
|
|
693
|
+
stateMap.set(s.trackId, s);
|
|
694
|
+
const classifMap = /* @__PURE__ */ new Map();
|
|
695
|
+
for (const c of classifications)
|
|
696
|
+
classifMap.set(c.trackId, c.classifications);
|
|
697
|
+
const now = Date.now();
|
|
698
|
+
for (const track of tracks) {
|
|
699
|
+
let acc = this.activeTracks.get(track.trackId);
|
|
700
|
+
if (!acc) {
|
|
701
|
+
acc = {
|
|
702
|
+
trackId: track.trackId,
|
|
703
|
+
class: track.class,
|
|
704
|
+
originalClass: track.originalClass,
|
|
705
|
+
state: "entering",
|
|
706
|
+
firstSeen: track.path.length === 0 ? now : now,
|
|
707
|
+
lastSeen: now,
|
|
708
|
+
path: [],
|
|
709
|
+
zoneTransitions: [],
|
|
710
|
+
events: []
|
|
711
|
+
};
|
|
712
|
+
this.activeTracks.set(track.trackId, acc);
|
|
713
|
+
}
|
|
714
|
+
acc.class = track.class;
|
|
715
|
+
acc.originalClass = track.originalClass;
|
|
716
|
+
acc.lastSeen = now;
|
|
717
|
+
const state = stateMap.get(track.trackId);
|
|
718
|
+
if (state)
|
|
719
|
+
acc.state = state.state;
|
|
720
|
+
const pathPoint = {
|
|
721
|
+
timestamp: now,
|
|
722
|
+
bbox: track.bbox,
|
|
723
|
+
velocity: track.velocity ?? { dx: 0, dy: 0 }
|
|
724
|
+
};
|
|
725
|
+
if (acc.path.length >= MAX_PATH_LENGTH) {
|
|
726
|
+
acc.path.shift();
|
|
727
|
+
}
|
|
728
|
+
acc.path.push(pathPoint);
|
|
729
|
+
const classifs = classifMap.get(track.trackId) ?? [];
|
|
730
|
+
for (const c of classifs) {
|
|
731
|
+
if (c.metadata?.type === "face-recognition" && typeof c.text === "string") {
|
|
732
|
+
acc.identity = { name: c.text, confidence: c.score, matchedAt: now };
|
|
733
|
+
} else if (c.metadata?.type === "plate-recognition" && typeof c.text === "string") {
|
|
734
|
+
acc.plateText = { text: c.text, confidence: c.score, readAt: now };
|
|
735
|
+
} else if (c.metadata?.type === "sub-class") {
|
|
736
|
+
acc.subClass = { class: c.class, confidence: c.score };
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
for (const ze of zoneEvents) {
|
|
741
|
+
const acc = this.activeTracks.get(ze.trackId);
|
|
742
|
+
if (!acc)
|
|
743
|
+
continue;
|
|
744
|
+
if (ze.type === "zone-enter") {
|
|
745
|
+
acc.zoneTransitions.push({
|
|
746
|
+
zoneId: ze.zoneId,
|
|
747
|
+
zoneName: ze.zoneName,
|
|
748
|
+
entered: ze.timestamp,
|
|
749
|
+
dwellMs: 0
|
|
750
|
+
});
|
|
751
|
+
} else if (ze.type === "zone-exit") {
|
|
752
|
+
let openIdx = -1;
|
|
753
|
+
for (let i = acc.zoneTransitions.length - 1; i >= 0; i--) {
|
|
754
|
+
const t = acc.zoneTransitions[i];
|
|
755
|
+
if (t.zoneId === ze.zoneId && t.exited === void 0) {
|
|
756
|
+
openIdx = i;
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (openIdx >= 0) {
|
|
761
|
+
const open = acc.zoneTransitions[openIdx];
|
|
762
|
+
acc.zoneTransitions[openIdx] = {
|
|
763
|
+
...open,
|
|
764
|
+
exited: ze.timestamp,
|
|
765
|
+
dwellMs: ze.timestamp - open.entered
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/** Attach a DetectionEvent to its associated track accumulator */
|
|
772
|
+
addEvent(trackId, event) {
|
|
773
|
+
const acc = this.activeTracks.get(trackId);
|
|
774
|
+
if (!acc)
|
|
775
|
+
return;
|
|
776
|
+
acc.events.push(event);
|
|
777
|
+
if (event.snapshot && !acc.bestSnapshot) {
|
|
778
|
+
acc.bestSnapshot = event.snapshot;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
/** Get accumulated detail for an active track */
|
|
782
|
+
getTrackDetail(trackId) {
|
|
783
|
+
const acc = this.activeTracks.get(trackId);
|
|
784
|
+
if (!acc)
|
|
785
|
+
return null;
|
|
786
|
+
return this.buildTrackDetail(acc);
|
|
787
|
+
}
|
|
788
|
+
/** Called when a track ends — returns complete TrackDetail and removes from active set */
|
|
789
|
+
finishTrack(trackId) {
|
|
790
|
+
const acc = this.activeTracks.get(trackId);
|
|
791
|
+
if (!acc)
|
|
792
|
+
return null;
|
|
793
|
+
const detail = this.buildTrackDetail(acc);
|
|
794
|
+
this.activeTracks.delete(trackId);
|
|
795
|
+
return detail;
|
|
796
|
+
}
|
|
797
|
+
/** Get all active track IDs */
|
|
798
|
+
getActiveTrackIds() {
|
|
799
|
+
return Array.from(this.activeTracks.keys());
|
|
800
|
+
}
|
|
801
|
+
/** Clear all accumulated state */
|
|
802
|
+
reset() {
|
|
803
|
+
this.activeTracks.clear();
|
|
804
|
+
}
|
|
805
|
+
buildTrackDetail(acc) {
|
|
806
|
+
return {
|
|
807
|
+
trackId: acc.trackId,
|
|
808
|
+
class: acc.class,
|
|
809
|
+
originalClass: acc.originalClass,
|
|
810
|
+
state: acc.state,
|
|
811
|
+
firstSeen: acc.firstSeen,
|
|
812
|
+
lastSeen: acc.lastSeen,
|
|
813
|
+
totalDwellMs: acc.lastSeen - acc.firstSeen,
|
|
814
|
+
path: acc.path,
|
|
815
|
+
identity: acc.identity,
|
|
816
|
+
plateText: acc.plateText,
|
|
817
|
+
subClass: acc.subClass,
|
|
818
|
+
zoneTransitions: acc.zoneTransitions,
|
|
819
|
+
bestSnapshot: acc.bestSnapshot,
|
|
820
|
+
events: acc.events
|
|
821
|
+
};
|
|
822
|
+
}
|
|
853
823
|
};
|
|
824
|
+
exports2.TrackStore = TrackStore2;
|
|
854
825
|
}
|
|
855
|
-
|
|
856
|
-
this.pipelineMsHistory.length = 0;
|
|
857
|
-
}
|
|
858
|
-
};
|
|
826
|
+
});
|
|
859
827
|
|
|
860
|
-
// src/analytics/
|
|
861
|
-
var
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
this.addPoint(px / this.width, py / this.height);
|
|
887
|
-
}
|
|
888
|
-
getHeatmap() {
|
|
889
|
-
return {
|
|
890
|
-
width: this.width,
|
|
891
|
-
height: this.height,
|
|
892
|
-
gridSize: this.gridSize,
|
|
893
|
-
cells: new Float32Array(this.grid),
|
|
894
|
-
maxCount: this.maxCount
|
|
828
|
+
// src/analytics/live-state-manager.js
|
|
829
|
+
var require_live_state_manager = __commonJS({
|
|
830
|
+
"src/analytics/live-state-manager.js"(exports2) {
|
|
831
|
+
"use strict";
|
|
832
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
833
|
+
exports2.LiveStateManager = void 0;
|
|
834
|
+
var FpsCounter = class {
|
|
835
|
+
frameTimes = [];
|
|
836
|
+
windowMs = 5e3;
|
|
837
|
+
tick(timestamp) {
|
|
838
|
+
this.frameTimes.push(timestamp);
|
|
839
|
+
const cutoff = timestamp - this.windowMs;
|
|
840
|
+
while (this.frameTimes.length > 0 && this.frameTimes[0] < cutoff) {
|
|
841
|
+
this.frameTimes.shift();
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
getFps() {
|
|
845
|
+
if (this.frameTimes.length < 2)
|
|
846
|
+
return 0;
|
|
847
|
+
const oldest = this.frameTimes[0];
|
|
848
|
+
const newest = this.frameTimes[this.frameTimes.length - 1];
|
|
849
|
+
const durationSec = (newest - oldest) / 1e3;
|
|
850
|
+
if (durationSec <= 0)
|
|
851
|
+
return 0;
|
|
852
|
+
return (this.frameTimes.length - 1) / durationSec;
|
|
853
|
+
}
|
|
895
854
|
};
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
855
|
+
var LiveStateManager2 = class {
|
|
856
|
+
zones = [];
|
|
857
|
+
fpsCounter = new FpsCounter();
|
|
858
|
+
pipelineMsHistory = [];
|
|
859
|
+
maxPipelineHistory = 30;
|
|
860
|
+
setZones(zones) {
|
|
861
|
+
this.zones = [...zones];
|
|
862
|
+
}
|
|
863
|
+
buildState(deviceId, tracks, states, trackZones, pipelineStatus, pipelineMs, lastEvent, timestamp) {
|
|
864
|
+
this.fpsCounter.tick(timestamp);
|
|
865
|
+
if (this.pipelineMsHistory.length >= this.maxPipelineHistory) {
|
|
866
|
+
this.pipelineMsHistory.shift();
|
|
867
|
+
}
|
|
868
|
+
this.pipelineMsHistory.push(pipelineMs);
|
|
869
|
+
const avgPipelineMs = this.pipelineMsHistory.length === 0 ? 0 : this.pipelineMsHistory.reduce((sum, v) => sum + v, 0) / this.pipelineMsHistory.length;
|
|
870
|
+
const stateMap = /* @__PURE__ */ new Map();
|
|
871
|
+
for (const s of states)
|
|
872
|
+
stateMap.set(s.trackId, s);
|
|
873
|
+
const trackSummaries = tracks.map((track) => {
|
|
874
|
+
const state = stateMap.get(track.trackId);
|
|
875
|
+
const inZones = Array.from(trackZones.get(track.trackId) ?? []);
|
|
876
|
+
return {
|
|
877
|
+
trackId: track.trackId,
|
|
878
|
+
class: track.class,
|
|
879
|
+
originalClass: track.originalClass,
|
|
880
|
+
state: state?.state ?? "moving",
|
|
881
|
+
bbox: track.bbox,
|
|
882
|
+
velocity: track.velocity ?? { dx: 0, dy: 0 },
|
|
883
|
+
dwellTimeMs: state?.dwellTimeMs ?? 0,
|
|
884
|
+
inZones
|
|
885
|
+
};
|
|
886
|
+
});
|
|
887
|
+
let movingObjects = 0;
|
|
888
|
+
let stationaryObjects = 0;
|
|
889
|
+
for (const summary of trackSummaries) {
|
|
890
|
+
if (summary.state === "moving" || summary.state === "entering")
|
|
891
|
+
movingObjects++;
|
|
892
|
+
else if (summary.state === "stationary" || summary.state === "loitering")
|
|
893
|
+
stationaryObjects++;
|
|
894
|
+
}
|
|
895
|
+
const objectCounts = {};
|
|
896
|
+
for (const summary of trackSummaries) {
|
|
897
|
+
objectCounts[summary.class] = (objectCounts[summary.class] ?? 0) + 1;
|
|
898
|
+
}
|
|
899
|
+
const zoneLiveStates = this.zones.map((zone) => {
|
|
900
|
+
const tracksInZone = trackSummaries.filter((t) => t.inZones.includes(zone.id));
|
|
901
|
+
const objectsByClass = {};
|
|
902
|
+
const loiteringTracks = [];
|
|
903
|
+
for (const t of tracksInZone) {
|
|
904
|
+
objectsByClass[t.class] = (objectsByClass[t.class] ?? 0) + 1;
|
|
905
|
+
if (t.state === "loitering")
|
|
906
|
+
loiteringTracks.push(t.trackId);
|
|
907
|
+
}
|
|
908
|
+
const avgDwellTimeMs = tracksInZone.length === 0 ? 0 : tracksInZone.reduce((sum, t) => sum + t.dwellTimeMs, 0) / tracksInZone.length;
|
|
909
|
+
return {
|
|
910
|
+
zoneId: zone.id,
|
|
911
|
+
zoneName: zone.name,
|
|
912
|
+
occupied: tracksInZone.length > 0,
|
|
913
|
+
objectCount: tracksInZone.length,
|
|
914
|
+
objectsByClass,
|
|
915
|
+
trackIds: tracksInZone.map((t) => t.trackId),
|
|
916
|
+
avgDwellTimeMs,
|
|
917
|
+
totalEntrancesToday: 0,
|
|
918
|
+
// requires DB — left for server orchestrator
|
|
919
|
+
totalExitsToday: 0,
|
|
920
|
+
loiteringTracks
|
|
921
|
+
};
|
|
922
|
+
});
|
|
923
|
+
return {
|
|
924
|
+
deviceId,
|
|
925
|
+
lastFrameTimestamp: timestamp,
|
|
926
|
+
activeObjects: trackSummaries.length,
|
|
927
|
+
movingObjects,
|
|
928
|
+
stationaryObjects,
|
|
929
|
+
objectCounts,
|
|
930
|
+
tracks: trackSummaries,
|
|
931
|
+
zones: zoneLiveStates,
|
|
932
|
+
lastEvent,
|
|
933
|
+
pipelineStatus,
|
|
934
|
+
avgPipelineMs,
|
|
935
|
+
currentFps: this.fpsCounter.getFps()
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
reset() {
|
|
939
|
+
this.pipelineMsHistory.length = 0;
|
|
940
|
+
}
|
|
958
941
|
};
|
|
942
|
+
exports2.LiveStateManager = LiveStateManager2;
|
|
959
943
|
}
|
|
960
|
-
|
|
961
|
-
return this.trackStore.getTrackDetail(trackId);
|
|
962
|
-
}
|
|
963
|
-
getCameraStatus(_deviceId) {
|
|
964
|
-
return this.lastLiveState ? [...this.lastLiveState.pipelineStatus] : [];
|
|
965
|
-
}
|
|
966
|
-
};
|
|
944
|
+
});
|
|
967
945
|
|
|
968
|
-
// src/
|
|
969
|
-
var
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
946
|
+
// src/analytics/heatmap-aggregator.js
|
|
947
|
+
var require_heatmap_aggregator = __commonJS({
|
|
948
|
+
"src/analytics/heatmap-aggregator.js"(exports2) {
|
|
949
|
+
"use strict";
|
|
950
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
951
|
+
exports2.HeatmapAggregator = void 0;
|
|
952
|
+
var HeatmapAggregator2 = class {
|
|
953
|
+
width;
|
|
954
|
+
height;
|
|
955
|
+
gridSize;
|
|
956
|
+
grid;
|
|
957
|
+
maxCount = 0;
|
|
958
|
+
constructor(width, height, gridSize) {
|
|
959
|
+
this.width = width;
|
|
960
|
+
this.height = height;
|
|
961
|
+
this.gridSize = gridSize;
|
|
962
|
+
if (gridSize <= 0)
|
|
963
|
+
throw new Error("gridSize must be > 0");
|
|
964
|
+
this.grid = new Float32Array(gridSize * gridSize);
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Add a normalized point (0–1 range) to the heatmap.
|
|
968
|
+
* Points outside [0, 1] are clamped to the grid boundary.
|
|
969
|
+
*/
|
|
970
|
+
addPoint(x, y) {
|
|
971
|
+
const col = Math.min(Math.floor(x * this.gridSize), this.gridSize - 1);
|
|
972
|
+
const row = Math.min(Math.floor(y * this.gridSize), this.gridSize - 1);
|
|
973
|
+
const idx = row * this.gridSize + col;
|
|
974
|
+
this.grid[idx] += 1;
|
|
975
|
+
if (this.grid[idx] > this.maxCount) {
|
|
976
|
+
this.maxCount = this.grid[idx];
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
/** Add a pixel-coordinate point. Normalizes against the configured width/height. */
|
|
980
|
+
addPixelPoint(px, py) {
|
|
981
|
+
this.addPoint(px / this.width, py / this.height);
|
|
982
|
+
}
|
|
983
|
+
getHeatmap() {
|
|
984
|
+
return {
|
|
985
|
+
width: this.width,
|
|
986
|
+
height: this.height,
|
|
987
|
+
gridSize: this.gridSize,
|
|
988
|
+
cells: new Float32Array(this.grid),
|
|
989
|
+
maxCount: this.maxCount
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
reset() {
|
|
993
|
+
this.grid.fill(0);
|
|
994
|
+
this.maxCount = 0;
|
|
995
|
+
}
|
|
996
|
+
/** Total number of points accumulated */
|
|
997
|
+
get totalPoints() {
|
|
998
|
+
let total = 0;
|
|
999
|
+
for (let i = 0; i < this.grid.length; i++) {
|
|
1000
|
+
total += this.grid[i];
|
|
1001
|
+
}
|
|
1002
|
+
return total;
|
|
1003
|
+
}
|
|
994
1004
|
};
|
|
1005
|
+
exports2.HeatmapAggregator = HeatmapAggregator2;
|
|
995
1006
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
const channels = frame.format === "rgb" ? 3 : 3;
|
|
1068
|
-
return {
|
|
1069
|
-
raw: {
|
|
1070
|
-
width: frame.width,
|
|
1071
|
-
height: frame.height,
|
|
1072
|
-
channels
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
// src/analytics/analytics-provider.js
|
|
1010
|
+
var require_analytics_provider = __commonJS({
|
|
1011
|
+
"src/analytics/analytics-provider.js"(exports2) {
|
|
1012
|
+
"use strict";
|
|
1013
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
1014
|
+
exports2.AnalyticsProvider = void 0;
|
|
1015
|
+
var AnalyticsProvider2 = class {
|
|
1016
|
+
liveStateManager;
|
|
1017
|
+
trackStore;
|
|
1018
|
+
getZoneHistoryCb;
|
|
1019
|
+
getHeatmapCb;
|
|
1020
|
+
lastLiveState;
|
|
1021
|
+
constructor(liveStateManager, trackStore, getZoneHistoryCb, getHeatmapCb, lastLiveState = null) {
|
|
1022
|
+
this.liveStateManager = liveStateManager;
|
|
1023
|
+
this.trackStore = trackStore;
|
|
1024
|
+
this.getZoneHistoryCb = getZoneHistoryCb;
|
|
1025
|
+
this.getHeatmapCb = getHeatmapCb;
|
|
1026
|
+
this.lastLiveState = lastLiveState;
|
|
1027
|
+
}
|
|
1028
|
+
/** Called by the orchestrator each frame to update the cached live state */
|
|
1029
|
+
updateLiveState(state) {
|
|
1030
|
+
this.lastLiveState = state;
|
|
1031
|
+
}
|
|
1032
|
+
getLiveState(_deviceId) {
|
|
1033
|
+
return this.lastLiveState;
|
|
1034
|
+
}
|
|
1035
|
+
getTracks(_deviceId, filter) {
|
|
1036
|
+
const tracks = this.lastLiveState?.tracks ?? [];
|
|
1037
|
+
if (!filter)
|
|
1038
|
+
return [...tracks];
|
|
1039
|
+
return tracks.filter((t) => {
|
|
1040
|
+
if (filter.class && !filter.class.includes(t.class))
|
|
1041
|
+
return false;
|
|
1042
|
+
if (filter.state && !filter.state.includes(t.state))
|
|
1043
|
+
return false;
|
|
1044
|
+
if (filter.inZone && !t.inZones.includes(filter.inZone))
|
|
1045
|
+
return false;
|
|
1046
|
+
if (filter.minDwellMs !== void 0 && t.dwellTimeMs < filter.minDwellMs)
|
|
1047
|
+
return false;
|
|
1048
|
+
return true;
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
getZoneState(_deviceId, zoneId) {
|
|
1052
|
+
return this.lastLiveState?.zones.find((z) => z.zoneId === zoneId) ?? null;
|
|
1053
|
+
}
|
|
1054
|
+
async getZoneHistory(deviceId, zoneId, options) {
|
|
1055
|
+
if (this.getZoneHistoryCb) {
|
|
1056
|
+
return this.getZoneHistoryCb(deviceId, zoneId, options);
|
|
1057
|
+
}
|
|
1058
|
+
return [];
|
|
1059
|
+
}
|
|
1060
|
+
async getHeatmap(deviceId, options) {
|
|
1061
|
+
if (this.getHeatmapCb) {
|
|
1062
|
+
return this.getHeatmapCb(deviceId, options);
|
|
1063
|
+
}
|
|
1064
|
+
const gridSize = options.resolution;
|
|
1065
|
+
return {
|
|
1066
|
+
width: 1920,
|
|
1067
|
+
height: 1080,
|
|
1068
|
+
gridSize,
|
|
1069
|
+
cells: new Float32Array(gridSize * gridSize),
|
|
1070
|
+
maxCount: 0
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
getTrackDetail(_deviceId, trackId) {
|
|
1074
|
+
return this.trackStore.getTrackDetail(trackId);
|
|
1075
|
+
}
|
|
1076
|
+
getCameraStatus(_deviceId) {
|
|
1077
|
+
return this.lastLiveState ? [...this.lastLiveState.pipelineStatus] : [];
|
|
1073
1078
|
}
|
|
1074
1079
|
};
|
|
1080
|
+
exports2.AnalyticsProvider = AnalyticsProvider2;
|
|
1075
1081
|
}
|
|
1076
|
-
|
|
1077
|
-
if (this.config.outputDir) {
|
|
1078
|
-
const { mkdir } = await import("fs/promises");
|
|
1079
|
-
await mkdir(this.config.outputDir, { recursive: true });
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
};
|
|
1082
|
+
});
|
|
1083
1083
|
|
|
1084
|
-
// src/
|
|
1085
|
-
var
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1084
|
+
// src/snapshots/snapshot-manager.js
|
|
1085
|
+
var require_snapshot_manager = __commonJS({
|
|
1086
|
+
"src/snapshots/snapshot-manager.js"(exports2) {
|
|
1087
|
+
"use strict";
|
|
1088
|
+
var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
1089
|
+
if (k2 === void 0) k2 = k;
|
|
1090
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
1091
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
1092
|
+
desc = { enumerable: true, get: function() {
|
|
1093
|
+
return m[k];
|
|
1094
|
+
} };
|
|
1095
|
+
}
|
|
1096
|
+
Object.defineProperty(o, k2, desc);
|
|
1097
|
+
}) : (function(o, m, k, k2) {
|
|
1098
|
+
if (k2 === void 0) k2 = k;
|
|
1099
|
+
o[k2] = m[k];
|
|
1100
|
+
}));
|
|
1101
|
+
var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
|
|
1102
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
1103
|
+
}) : function(o, v) {
|
|
1104
|
+
o["default"] = v;
|
|
1105
|
+
});
|
|
1106
|
+
var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
|
|
1107
|
+
var ownKeys = function(o) {
|
|
1108
|
+
ownKeys = Object.getOwnPropertyNames || function(o2) {
|
|
1109
|
+
var ar = [];
|
|
1110
|
+
for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
|
|
1111
|
+
return ar;
|
|
1112
|
+
};
|
|
1113
|
+
return ownKeys(o);
|
|
1114
|
+
};
|
|
1115
|
+
return function(mod) {
|
|
1116
|
+
if (mod && mod.__esModule) return mod;
|
|
1117
|
+
var result = {};
|
|
1118
|
+
if (mod != null) {
|
|
1119
|
+
for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
1120
|
+
}
|
|
1121
|
+
__setModuleDefault(result, mod);
|
|
1122
|
+
return result;
|
|
1123
|
+
};
|
|
1124
|
+
})();
|
|
1125
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
1126
|
+
exports2.SnapshotManager = exports2.DEFAULT_SNAPSHOT_CONFIG = void 0;
|
|
1127
|
+
exports2.DEFAULT_SNAPSHOT_CONFIG = {
|
|
1128
|
+
enabled: false,
|
|
1129
|
+
saveThumbnail: true,
|
|
1130
|
+
saveAnnotatedFrame: false,
|
|
1131
|
+
saveDebugThumbnails: false
|
|
1110
1132
|
};
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
this.cameras.clear();
|
|
1116
|
-
this.listeners.clear();
|
|
1117
|
-
}
|
|
1118
|
-
async processFrame(deviceId, frame, pipelineResult) {
|
|
1119
|
-
const camera = this.getOrCreateCamera(deviceId);
|
|
1120
|
-
const frameWidth = frame.width > 0 ? frame.width : this.config.defaultFrameWidth;
|
|
1121
|
-
const frameHeight = frame.height > 0 ? frame.height : this.config.defaultFrameHeight;
|
|
1122
|
-
const timestamp = frame.timestamp;
|
|
1123
|
-
const detections = pipelineResult ? this.extractDetections(pipelineResult) : [];
|
|
1124
|
-
const tracked = camera.tracker.update(detections, timestamp);
|
|
1125
|
-
const states = camera.stateAnalyzer.analyze(tracked, timestamp);
|
|
1126
|
-
const zoneEvents = camera.zoneEvaluator.evaluate(
|
|
1127
|
-
tracked,
|
|
1128
|
-
camera.zones,
|
|
1129
|
-
frameWidth,
|
|
1130
|
-
frameHeight,
|
|
1131
|
-
timestamp
|
|
1132
|
-
);
|
|
1133
|
-
const classificationsByTrack = pipelineResult ? this.matchClassifications(pipelineResult, tracked.map((t) => t.trackId)) : [];
|
|
1134
|
-
camera.trackStore.update(tracked, states, zoneEvents, classificationsByTrack);
|
|
1135
|
-
const events = camera.eventEmitter.emit(
|
|
1136
|
-
tracked,
|
|
1137
|
-
states,
|
|
1138
|
-
zoneEvents,
|
|
1139
|
-
classificationsByTrack,
|
|
1140
|
-
deviceId
|
|
1141
|
-
);
|
|
1142
|
-
for (const event of events) {
|
|
1143
|
-
const snapshot = await camera.snapshotManager.capture(frame, event.detection, tracked, event.id);
|
|
1144
|
-
if (snapshot) {
|
|
1145
|
-
;
|
|
1146
|
-
event.snapshot = snapshot;
|
|
1147
|
-
}
|
|
1148
|
-
camera.trackStore.addEvent(event.detection.trackId, event);
|
|
1149
|
-
}
|
|
1150
|
-
for (const track of tracked) {
|
|
1151
|
-
const cx = (track.bbox.x + track.bbox.w / 2) / frameWidth;
|
|
1152
|
-
const cy = (track.bbox.y + track.bbox.h / 2) / frameHeight;
|
|
1153
|
-
camera.heatmapAggregator.addPoint(cx, cy);
|
|
1154
|
-
}
|
|
1155
|
-
for (const lostTrack of camera.tracker.getLostTracks()) {
|
|
1156
|
-
const detail = camera.trackStore.finishTrack(lostTrack.trackId);
|
|
1157
|
-
if (detail && this.onTrackFinished) {
|
|
1158
|
-
this.onTrackFinished(deviceId, detail);
|
|
1133
|
+
var SnapshotManager2 = class {
|
|
1134
|
+
config;
|
|
1135
|
+
constructor(config) {
|
|
1136
|
+
this.config = config;
|
|
1159
1137
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1138
|
+
async capture(frame, detection, allDetections, eventId) {
|
|
1139
|
+
if (!this.config.enabled)
|
|
1140
|
+
return void 0;
|
|
1141
|
+
await this.ensureOutputDir();
|
|
1142
|
+
let thumbnailPath;
|
|
1143
|
+
let annotatedFramePath;
|
|
1144
|
+
if (this.config.saveThumbnail) {
|
|
1145
|
+
thumbnailPath = await this.saveThumbnail(frame, detection.bbox, eventId);
|
|
1146
|
+
}
|
|
1147
|
+
if (this.config.saveAnnotatedFrame) {
|
|
1148
|
+
annotatedFramePath = await this.saveAnnotatedFrame(frame, allDetections, eventId);
|
|
1149
|
+
}
|
|
1150
|
+
if (!thumbnailPath && !annotatedFramePath)
|
|
1151
|
+
return void 0;
|
|
1152
|
+
return {
|
|
1153
|
+
thumbnailPath: thumbnailPath ?? "",
|
|
1154
|
+
annotatedFramePath: annotatedFramePath ?? ""
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
async saveThumbnail(frame, bbox, eventId) {
|
|
1158
|
+
const sharp = await Promise.resolve().then(() => __importStar(require("sharp")));
|
|
1159
|
+
const { frameWidth, frameHeight } = await this.resolveFrameDimensions(frame);
|
|
1160
|
+
const left = Math.max(0, Math.round(bbox.x));
|
|
1161
|
+
const top = Math.max(0, Math.round(bbox.y));
|
|
1162
|
+
const width = Math.min(Math.round(bbox.w), frameWidth - left);
|
|
1163
|
+
const height = Math.min(Math.round(bbox.h), frameHeight - top);
|
|
1164
|
+
if (width <= 0 || height <= 0) {
|
|
1165
|
+
throw new Error(`Invalid crop dimensions for event ${eventId}: ${JSON.stringify({ left, top, width, height, frameWidth, frameHeight })}`);
|
|
1166
|
+
}
|
|
1167
|
+
const relativePath = `${eventId}_thumb.jpg`;
|
|
1168
|
+
const inputOptions = this.sharpInputOptions(frame);
|
|
1169
|
+
const buffer = await sharp.default(frame.data, inputOptions).extract({ left, top, width, height }).jpeg({ quality: 85 }).toBuffer();
|
|
1170
|
+
await this.writeOutput(relativePath, buffer);
|
|
1171
|
+
return relativePath;
|
|
1172
|
+
}
|
|
1173
|
+
async saveAnnotatedFrame(frame, detections, eventId) {
|
|
1174
|
+
const sharp = await Promise.resolve().then(() => __importStar(require("sharp")));
|
|
1175
|
+
const { frameWidth, frameHeight } = await this.resolveFrameDimensions(frame);
|
|
1176
|
+
const relativePath = `${eventId}_annotated.jpg`;
|
|
1177
|
+
const svgBoxes = detections.map((det) => {
|
|
1178
|
+
const x = Math.max(0, Math.round(det.bbox.x));
|
|
1179
|
+
const y = Math.max(0, Math.round(det.bbox.y));
|
|
1180
|
+
const w = Math.min(Math.round(det.bbox.w), frameWidth - x);
|
|
1181
|
+
const h = Math.min(Math.round(det.bbox.h), frameHeight - y);
|
|
1182
|
+
const label = `${det.class} ${(det.score * 100).toFixed(0)}%`;
|
|
1183
|
+
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="none" stroke="lime" stroke-width="2"/>
|
|
1184
|
+
<text x="${x + 2}" y="${Math.max(y - 2, 10)}" font-size="12" fill="lime" font-family="sans-serif">${label}</text>`;
|
|
1185
|
+
}).join("\n");
|
|
1186
|
+
const svgOverlay = Buffer.from(`<svg width="${frameWidth}" height="${frameHeight}" xmlns="http://www.w3.org/2000/svg">${svgBoxes}</svg>`);
|
|
1187
|
+
const inputOptions = this.sharpInputOptions(frame);
|
|
1188
|
+
const buffer = await sharp.default(frame.data, inputOptions).composite([{ input: svgOverlay, top: 0, left: 0 }]).jpeg({ quality: 85 }).toBuffer();
|
|
1189
|
+
await this.writeOutput(relativePath, buffer);
|
|
1190
|
+
return relativePath;
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Write output using mediaStorage if available, otherwise fall back to fs.writeFile.
|
|
1194
|
+
*/
|
|
1195
|
+
async writeOutput(relativePath, data) {
|
|
1196
|
+
if (this.config.mediaStorage) {
|
|
1197
|
+
await this.config.mediaStorage.writeFile(relativePath, data);
|
|
1198
|
+
} else if (this.config.outputDir) {
|
|
1199
|
+
const { writeFile } = await Promise.resolve().then(() => __importStar(require("fs/promises")));
|
|
1200
|
+
const { join } = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1201
|
+
await writeFile(join(this.config.outputDir, relativePath), data);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Resolve actual frame dimensions. JPEG frames from decoders may report
|
|
1206
|
+
* width=0, height=0 — in that case read the real size from sharp metadata.
|
|
1207
|
+
*/
|
|
1208
|
+
async resolveFrameDimensions(frame) {
|
|
1209
|
+
if (frame.width > 0 && frame.height > 0) {
|
|
1210
|
+
return { frameWidth: frame.width, frameHeight: frame.height };
|
|
1211
|
+
}
|
|
1212
|
+
if (frame.format === "jpeg") {
|
|
1213
|
+
const sharp = await Promise.resolve().then(() => __importStar(require("sharp")));
|
|
1214
|
+
const meta = await sharp.default(frame.data).metadata();
|
|
1215
|
+
return {
|
|
1216
|
+
frameWidth: meta.width ?? 0,
|
|
1217
|
+
frameHeight: meta.height ?? 0
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
return { frameWidth: frame.width, frameHeight: frame.height };
|
|
1221
|
+
}
|
|
1222
|
+
sharpInputOptions(frame) {
|
|
1223
|
+
if (frame.format === "jpeg") {
|
|
1224
|
+
return {};
|
|
1225
|
+
}
|
|
1226
|
+
const channels = frame.format === "rgb" ? 3 : 3;
|
|
1227
|
+
return {
|
|
1228
|
+
raw: {
|
|
1229
|
+
width: frame.width,
|
|
1230
|
+
height: frame.height,
|
|
1231
|
+
channels
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
async ensureOutputDir() {
|
|
1236
|
+
if (this.config.outputDir) {
|
|
1237
|
+
const { mkdir } = await Promise.resolve().then(() => __importStar(require("fs/promises")));
|
|
1238
|
+
await mkdir(this.config.outputDir, { recursive: true });
|
|
1239
|
+
}
|
|
1179
1240
|
}
|
|
1180
|
-
}
|
|
1181
|
-
return events;
|
|
1182
|
-
}
|
|
1183
|
-
setCameraPipeline(deviceId, _config) {
|
|
1184
|
-
this.getOrCreateCamera(deviceId);
|
|
1185
|
-
}
|
|
1186
|
-
setCameraZones(deviceId, zones) {
|
|
1187
|
-
const camera = this.getOrCreateCamera(deviceId);
|
|
1188
|
-
camera.zones = [...zones];
|
|
1189
|
-
camera.liveStateManager.setZones(zones);
|
|
1190
|
-
}
|
|
1191
|
-
setKnownFaces(faces) {
|
|
1192
|
-
this.knownFaces = [...faces];
|
|
1193
|
-
}
|
|
1194
|
-
setKnownPlates(plates) {
|
|
1195
|
-
this.knownPlates = [...plates];
|
|
1196
|
-
}
|
|
1197
|
-
onLiveStateChange(deviceId, callback) {
|
|
1198
|
-
if (!this.listeners.has(deviceId)) {
|
|
1199
|
-
this.listeners.set(deviceId, /* @__PURE__ */ new Set());
|
|
1200
|
-
}
|
|
1201
|
-
this.listeners.get(deviceId).add(callback);
|
|
1202
|
-
return () => {
|
|
1203
|
-
this.listeners.get(deviceId)?.delete(callback);
|
|
1204
1241
|
};
|
|
1242
|
+
exports2.SnapshotManager = SnapshotManager2;
|
|
1205
1243
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
height: this.config.defaultFrameHeight,
|
|
1232
|
-
gridSize,
|
|
1233
|
-
cells: new Float32Array(gridSize * gridSize),
|
|
1234
|
-
maxCount: 0
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
// src/pipeline/analysis-pipeline.js
|
|
1247
|
+
var require_analysis_pipeline = __commonJS({
|
|
1248
|
+
"src/pipeline/analysis-pipeline.js"(exports2) {
|
|
1249
|
+
"use strict";
|
|
1250
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
1251
|
+
exports2.AnalysisPipeline = void 0;
|
|
1252
|
+
var sort_tracker_js_1 = require_sort_tracker();
|
|
1253
|
+
var state_analyzer_js_1 = require_state_analyzer();
|
|
1254
|
+
var zone_evaluator_js_1 = require_zone_evaluator();
|
|
1255
|
+
var event_emitter_js_1 = require_event_emitter();
|
|
1256
|
+
var track_store_js_1 = require_track_store();
|
|
1257
|
+
var live_state_manager_js_1 = require_live_state_manager();
|
|
1258
|
+
var heatmap_aggregator_js_1 = require_heatmap_aggregator();
|
|
1259
|
+
var analytics_provider_js_1 = require_analytics_provider();
|
|
1260
|
+
var snapshot_manager_js_1 = require_snapshot_manager();
|
|
1261
|
+
var AnalysisPipeline2 = class {
|
|
1262
|
+
id = "pipeline-analysis";
|
|
1263
|
+
manifest = {
|
|
1264
|
+
id: "pipeline-analysis",
|
|
1265
|
+
name: "Pipeline Analysis",
|
|
1266
|
+
version: "0.1.0",
|
|
1267
|
+
description: "Object tracking, state analysis, zone evaluation, and event emission",
|
|
1268
|
+
packageName: "@camstack/lib-pipeline-analysis"
|
|
1235
1269
|
};
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
lastLiveState: null
|
|
1280
|
-
};
|
|
1281
|
-
this.cameras.set(deviceId, cameraState);
|
|
1282
|
-
return cameraState;
|
|
1283
|
-
}
|
|
1284
|
-
extractDetections(pipelineResult) {
|
|
1285
|
-
const detections = [];
|
|
1286
|
-
for (const stepResult of pipelineResult.results) {
|
|
1287
|
-
if (stepResult.slot === "detector") {
|
|
1288
|
-
const output = stepResult.output;
|
|
1289
|
-
if (output.detections) {
|
|
1290
|
-
detections.push(...output.detections);
|
|
1270
|
+
config;
|
|
1271
|
+
cameras = /* @__PURE__ */ new Map();
|
|
1272
|
+
knownFaces = [];
|
|
1273
|
+
knownPlates = [];
|
|
1274
|
+
listeners = /* @__PURE__ */ new Map();
|
|
1275
|
+
/** Optional callback: server orchestrator can hook in to persist finished tracks */
|
|
1276
|
+
onTrackFinished;
|
|
1277
|
+
constructor(config = {}) {
|
|
1278
|
+
this.config = {
|
|
1279
|
+
tracker: config.tracker ?? {},
|
|
1280
|
+
stateAnalyzer: config.stateAnalyzer ?? {},
|
|
1281
|
+
eventFilter: config.eventFilter ?? {},
|
|
1282
|
+
snapshot: config.snapshot ?? {},
|
|
1283
|
+
heatmapGridSize: config.heatmapGridSize ?? 32,
|
|
1284
|
+
defaultFrameWidth: config.defaultFrameWidth ?? 1920,
|
|
1285
|
+
defaultFrameHeight: config.defaultFrameHeight ?? 1080
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
async initialize(_ctx) {
|
|
1289
|
+
}
|
|
1290
|
+
async shutdown() {
|
|
1291
|
+
this.cameras.clear();
|
|
1292
|
+
this.listeners.clear();
|
|
1293
|
+
}
|
|
1294
|
+
async processFrame(deviceId, frame, pipelineResult) {
|
|
1295
|
+
const camera = this.getOrCreateCamera(deviceId);
|
|
1296
|
+
const frameWidth = frame.width > 0 ? frame.width : this.config.defaultFrameWidth;
|
|
1297
|
+
const frameHeight = frame.height > 0 ? frame.height : this.config.defaultFrameHeight;
|
|
1298
|
+
const timestamp = frame.timestamp;
|
|
1299
|
+
const detections = pipelineResult ? this.extractDetections(pipelineResult) : [];
|
|
1300
|
+
const tracked = camera.tracker.update(detections, timestamp);
|
|
1301
|
+
const states = camera.stateAnalyzer.analyze(tracked, timestamp);
|
|
1302
|
+
const zoneEvents = camera.zoneEvaluator.evaluate(tracked, camera.zones, frameWidth, frameHeight, timestamp);
|
|
1303
|
+
const classificationsByTrack = pipelineResult ? this.matchClassifications(pipelineResult, tracked.map((t) => t.trackId)) : [];
|
|
1304
|
+
camera.trackStore.update(tracked, states, zoneEvents, classificationsByTrack);
|
|
1305
|
+
const events = camera.eventEmitter.emit(tracked, states, zoneEvents, classificationsByTrack, deviceId);
|
|
1306
|
+
for (const event of events) {
|
|
1307
|
+
const snapshot = await camera.snapshotManager.capture(frame, event.detection, tracked, event.id);
|
|
1308
|
+
if (snapshot) {
|
|
1309
|
+
;
|
|
1310
|
+
event.snapshot = snapshot;
|
|
1311
|
+
}
|
|
1312
|
+
camera.trackStore.addEvent(event.detection.trackId, event);
|
|
1291
1313
|
}
|
|
1314
|
+
for (const track of tracked) {
|
|
1315
|
+
const cx = (track.bbox.x + track.bbox.w / 2) / frameWidth;
|
|
1316
|
+
const cy = (track.bbox.y + track.bbox.h / 2) / frameHeight;
|
|
1317
|
+
camera.heatmapAggregator.addPoint(cx, cy);
|
|
1318
|
+
}
|
|
1319
|
+
for (const lostTrack of camera.tracker.getLostTracks()) {
|
|
1320
|
+
const detail = camera.trackStore.finishTrack(lostTrack.trackId);
|
|
1321
|
+
if (detail && this.onTrackFinished) {
|
|
1322
|
+
this.onTrackFinished(deviceId, detail);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
const pipelineMs = pipelineResult?.totalMs ?? 0;
|
|
1326
|
+
const pipelineStatus = [];
|
|
1327
|
+
const liveState = camera.liveStateManager.buildState(deviceId, tracked, states, camera.zoneEvaluator.getTrackZones(), pipelineStatus, pipelineMs, events[events.length - 1], timestamp);
|
|
1328
|
+
camera.lastLiveState = liveState;
|
|
1329
|
+
camera.analyticsProvider.updateLiveState(liveState);
|
|
1330
|
+
const cameraListeners = this.listeners.get(deviceId);
|
|
1331
|
+
if (cameraListeners) {
|
|
1332
|
+
for (const cb of cameraListeners) {
|
|
1333
|
+
cb(liveState);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return events;
|
|
1292
1337
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1338
|
+
setCameraPipeline(deviceId, _config) {
|
|
1339
|
+
this.getOrCreateCamera(deviceId);
|
|
1340
|
+
}
|
|
1341
|
+
setCameraZones(deviceId, zones) {
|
|
1342
|
+
const camera = this.getOrCreateCamera(deviceId);
|
|
1343
|
+
camera.zones = [...zones];
|
|
1344
|
+
camera.liveStateManager.setZones(zones);
|
|
1345
|
+
}
|
|
1346
|
+
setKnownFaces(faces) {
|
|
1347
|
+
this.knownFaces = [...faces];
|
|
1348
|
+
}
|
|
1349
|
+
setKnownPlates(plates) {
|
|
1350
|
+
this.knownPlates = [...plates];
|
|
1351
|
+
}
|
|
1352
|
+
onLiveStateChange(deviceId, callback) {
|
|
1353
|
+
if (!this.listeners.has(deviceId)) {
|
|
1354
|
+
this.listeners.set(deviceId, /* @__PURE__ */ new Set());
|
|
1303
1355
|
}
|
|
1356
|
+
this.listeners.get(deviceId).add(callback);
|
|
1357
|
+
return () => {
|
|
1358
|
+
this.listeners.get(deviceId)?.delete(callback);
|
|
1359
|
+
};
|
|
1304
1360
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1361
|
+
// ICameraAnalyticsProvider delegates to per-camera AnalyticsProvider
|
|
1362
|
+
getLiveState(deviceId) {
|
|
1363
|
+
return this.cameras.get(deviceId)?.lastLiveState ?? null;
|
|
1364
|
+
}
|
|
1365
|
+
getTracks(deviceId, filter) {
|
|
1366
|
+
const camera = this.cameras.get(deviceId);
|
|
1367
|
+
if (!camera)
|
|
1368
|
+
return [];
|
|
1369
|
+
return camera.analyticsProvider.getTracks(deviceId, filter);
|
|
1370
|
+
}
|
|
1371
|
+
getZoneState(deviceId, zoneId) {
|
|
1372
|
+
const camera = this.cameras.get(deviceId);
|
|
1373
|
+
if (!camera)
|
|
1374
|
+
return null;
|
|
1375
|
+
return camera.analyticsProvider.getZoneState(deviceId, zoneId);
|
|
1376
|
+
}
|
|
1377
|
+
async getZoneHistory(deviceId, zoneId, options) {
|
|
1378
|
+
const camera = this.cameras.get(deviceId);
|
|
1379
|
+
if (!camera)
|
|
1380
|
+
return [];
|
|
1381
|
+
return camera.analyticsProvider.getZoneHistory(deviceId, zoneId, options);
|
|
1382
|
+
}
|
|
1383
|
+
async getHeatmap(deviceId, options) {
|
|
1384
|
+
const camera = this.cameras.get(deviceId);
|
|
1385
|
+
if (!camera) {
|
|
1386
|
+
const gridSize = options.resolution;
|
|
1387
|
+
return {
|
|
1388
|
+
width: this.config.defaultFrameWidth,
|
|
1389
|
+
height: this.config.defaultFrameHeight,
|
|
1390
|
+
gridSize,
|
|
1391
|
+
cells: new Float32Array(gridSize * gridSize),
|
|
1392
|
+
maxCount: 0
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
return camera.heatmapAggregator.getHeatmap();
|
|
1396
|
+
}
|
|
1397
|
+
getTrackDetail(deviceId, trackId) {
|
|
1398
|
+
const camera = this.cameras.get(deviceId);
|
|
1399
|
+
if (!camera)
|
|
1400
|
+
return null;
|
|
1401
|
+
return camera.trackStore.getTrackDetail(trackId);
|
|
1402
|
+
}
|
|
1403
|
+
getCameraStatus(deviceId) {
|
|
1404
|
+
const camera = this.cameras.get(deviceId);
|
|
1405
|
+
if (!camera)
|
|
1406
|
+
return [];
|
|
1407
|
+
return camera.analyticsProvider.getCameraStatus(deviceId);
|
|
1408
|
+
}
|
|
1409
|
+
getOrCreateCamera(deviceId) {
|
|
1410
|
+
const existing = this.cameras.get(deviceId);
|
|
1411
|
+
if (existing)
|
|
1412
|
+
return existing;
|
|
1413
|
+
const tracker = new sort_tracker_js_1.SortTracker(this.config.tracker);
|
|
1414
|
+
const stateAnalyzer = new state_analyzer_js_1.StateAnalyzer(this.config.stateAnalyzer);
|
|
1415
|
+
const zoneEvaluator = new zone_evaluator_js_1.ZoneEvaluator();
|
|
1416
|
+
const eventEmitter = new event_emitter_js_1.DetectionEventEmitter(this.config.eventFilter);
|
|
1417
|
+
const trackStore = new track_store_js_1.TrackStore();
|
|
1418
|
+
const liveStateManager = new live_state_manager_js_1.LiveStateManager();
|
|
1419
|
+
const heatmapAggregator = new heatmap_aggregator_js_1.HeatmapAggregator(this.config.defaultFrameWidth, this.config.defaultFrameHeight, this.config.heatmapGridSize);
|
|
1420
|
+
const analyticsProvider = new analytics_provider_js_1.AnalyticsProvider(liveStateManager, trackStore);
|
|
1421
|
+
const snapshotManager = new snapshot_manager_js_1.SnapshotManager({
|
|
1422
|
+
...snapshot_manager_js_1.DEFAULT_SNAPSHOT_CONFIG,
|
|
1423
|
+
...this.config.snapshot
|
|
1424
|
+
});
|
|
1425
|
+
const cameraState = {
|
|
1426
|
+
tracker,
|
|
1427
|
+
stateAnalyzer,
|
|
1428
|
+
zoneEvaluator,
|
|
1429
|
+
eventEmitter,
|
|
1430
|
+
trackStore,
|
|
1431
|
+
liveStateManager,
|
|
1432
|
+
heatmapAggregator,
|
|
1433
|
+
analyticsProvider,
|
|
1434
|
+
snapshotManager,
|
|
1435
|
+
zones: [],
|
|
1436
|
+
lastLiveState: null
|
|
1437
|
+
};
|
|
1438
|
+
this.cameras.set(deviceId, cameraState);
|
|
1439
|
+
return cameraState;
|
|
1440
|
+
}
|
|
1441
|
+
extractDetections(pipelineResult) {
|
|
1442
|
+
const detections = [];
|
|
1443
|
+
for (const stepResult of pipelineResult.results) {
|
|
1444
|
+
if (stepResult.slot === "detector") {
|
|
1445
|
+
const output = stepResult.output;
|
|
1446
|
+
if (output.detections) {
|
|
1447
|
+
detections.push(...output.detections);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
return detections;
|
|
1452
|
+
}
|
|
1453
|
+
matchClassifications(pipelineResult, trackIds) {
|
|
1454
|
+
const classifierResults = [];
|
|
1455
|
+
for (const stepResult of pipelineResult.results) {
|
|
1456
|
+
if (stepResult.slot === "classifier") {
|
|
1457
|
+
const output = stepResult.output;
|
|
1458
|
+
if (output.classifications) {
|
|
1459
|
+
classifierResults.push([...output.classifications]);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
return trackIds.slice(0, classifierResults.length).map((trackId, i) => ({
|
|
1464
|
+
trackId,
|
|
1465
|
+
classifications: classifierResults[i] ?? []
|
|
1466
|
+
}));
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
exports2.AnalysisPipeline = AnalysisPipeline2;
|
|
1310
1470
|
}
|
|
1311
|
-
};
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
// src/index.ts
|
|
1474
|
+
var src_exports = {};
|
|
1475
|
+
__export(src_exports, {
|
|
1476
|
+
AnalysisPipeline: () => import_analysis_pipeline.AnalysisPipeline,
|
|
1477
|
+
AnalyticsProvider: () => import_analytics_provider.AnalyticsProvider,
|
|
1478
|
+
DEFAULT_EVENT_FILTER_CONFIG: () => import_event_filters.DEFAULT_EVENT_FILTER_CONFIG,
|
|
1479
|
+
DEFAULT_SNAPSHOT_CONFIG: () => import_snapshot_manager.DEFAULT_SNAPSHOT_CONFIG,
|
|
1480
|
+
DEFAULT_STATE_ANALYZER_CONFIG: () => import_state_analyzer.DEFAULT_STATE_ANALYZER_CONFIG,
|
|
1481
|
+
DEFAULT_TRACKER_CONFIG: () => import_sort_tracker.DEFAULT_TRACKER_CONFIG,
|
|
1482
|
+
DetectionEventEmitter: () => import_event_emitter.DetectionEventEmitter,
|
|
1483
|
+
EventFilter: () => import_event_filters.EventFilter,
|
|
1484
|
+
HeatmapAggregator: () => import_heatmap_aggregator.HeatmapAggregator,
|
|
1485
|
+
LiveStateManager: () => import_live_state_manager.LiveStateManager,
|
|
1486
|
+
SnapshotManager: () => import_snapshot_manager.SnapshotManager,
|
|
1487
|
+
SortTracker: () => import_sort_tracker.SortTracker,
|
|
1488
|
+
StateAnalyzer: () => import_state_analyzer.StateAnalyzer,
|
|
1489
|
+
TrackStore: () => import_track_store.TrackStore,
|
|
1490
|
+
ZoneEvaluator: () => import_zone_evaluator.ZoneEvaluator,
|
|
1491
|
+
greedyAssignment: () => import_hungarian.greedyAssignment
|
|
1492
|
+
});
|
|
1493
|
+
module.exports = __toCommonJS(src_exports);
|
|
1494
|
+
__reExport(src_exports, __toESM(require_geometry()), module.exports);
|
|
1495
|
+
var import_sort_tracker = __toESM(require_sort_tracker());
|
|
1496
|
+
var import_hungarian = __toESM(require_hungarian());
|
|
1497
|
+
var import_state_analyzer = __toESM(require_state_analyzer());
|
|
1498
|
+
var import_zone_evaluator = __toESM(require_zone_evaluator());
|
|
1499
|
+
var import_event_filters = __toESM(require_event_filters());
|
|
1500
|
+
var import_event_emitter = __toESM(require_event_emitter());
|
|
1501
|
+
var import_track_store = __toESM(require_track_store());
|
|
1502
|
+
var import_live_state_manager = __toESM(require_live_state_manager());
|
|
1503
|
+
var import_heatmap_aggregator = __toESM(require_heatmap_aggregator());
|
|
1504
|
+
var import_analytics_provider = __toESM(require_analytics_provider());
|
|
1505
|
+
var import_snapshot_manager = __toESM(require_snapshot_manager());
|
|
1506
|
+
var import_analysis_pipeline = __toESM(require_analysis_pipeline());
|
|
1312
1507
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1313
1508
|
0 && (module.exports = {
|
|
1314
1509
|
AnalysisPipeline,
|
|
@@ -1326,11 +1521,6 @@ var AnalysisPipeline = class {
|
|
|
1326
1521
|
StateAnalyzer,
|
|
1327
1522
|
TrackStore,
|
|
1328
1523
|
ZoneEvaluator,
|
|
1329
|
-
|
|
1330
|
-
greedyAssignment,
|
|
1331
|
-
lineIntersection,
|
|
1332
|
-
normalizeToPixel,
|
|
1333
|
-
pointInPolygon,
|
|
1334
|
-
tripwireCrossing
|
|
1524
|
+
greedyAssignment
|
|
1335
1525
|
});
|
|
1336
1526
|
//# sourceMappingURL=index.js.map
|