@camstack/lib-pipeline-analysis 0.1.7 → 0.1.10
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.d.mts +333 -0
- package/dist/index.d.ts +333 -0
- package/dist/index.js +1222 -1412
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1208 -1473
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -5,9 +5,6 @@ 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
|
-
};
|
|
11
8
|
var __export = (target, all) => {
|
|
12
9
|
for (var name in all)
|
|
13
10
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -20,7 +17,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
20
17
|
}
|
|
21
18
|
return to;
|
|
22
19
|
};
|
|
23
|
-
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
24
20
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
21
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
22
|
// file that has been converted to a CommonJS file using a Babel-
|
|
@@ -31,1479 +27,1288 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
31
27
|
));
|
|
32
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
33
29
|
|
|
34
|
-
// src/
|
|
35
|
-
var
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
return inside;
|
|
60
|
-
}
|
|
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
|
-
};
|
|
79
|
-
}
|
|
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 };
|
|
92
|
-
}
|
|
93
|
-
function bboxCentroid(bbox) {
|
|
94
|
-
return {
|
|
95
|
-
x: bbox.x + bbox.w / 2,
|
|
96
|
-
y: bbox.y + bbox.h / 2
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
function normalizeToPixel(point, imageWidth, imageHeight) {
|
|
100
|
-
return {
|
|
101
|
-
x: point.x * imageWidth,
|
|
102
|
-
y: point.y * imageHeight
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
}
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
AnalysisPipeline: () => AnalysisPipeline,
|
|
34
|
+
AnalyticsProvider: () => AnalyticsProvider,
|
|
35
|
+
DEFAULT_EVENT_FILTER_CONFIG: () => DEFAULT_EVENT_FILTER_CONFIG,
|
|
36
|
+
DEFAULT_SNAPSHOT_CONFIG: () => DEFAULT_SNAPSHOT_CONFIG,
|
|
37
|
+
DEFAULT_STATE_ANALYZER_CONFIG: () => DEFAULT_STATE_ANALYZER_CONFIG,
|
|
38
|
+
DEFAULT_TRACKER_CONFIG: () => DEFAULT_TRACKER_CONFIG,
|
|
39
|
+
DetectionEventEmitter: () => DetectionEventEmitter,
|
|
40
|
+
EventFilter: () => EventFilter,
|
|
41
|
+
HeatmapAggregator: () => HeatmapAggregator,
|
|
42
|
+
LiveStateManager: () => LiveStateManager,
|
|
43
|
+
SnapshotManager: () => SnapshotManager,
|
|
44
|
+
SortTracker: () => SortTracker,
|
|
45
|
+
StateAnalyzer: () => StateAnalyzer,
|
|
46
|
+
TrackStore: () => TrackStore,
|
|
47
|
+
ZoneEvaluator: () => ZoneEvaluator,
|
|
48
|
+
bboxCentroid: () => bboxCentroid,
|
|
49
|
+
greedyAssignment: () => greedyAssignment,
|
|
50
|
+
lineIntersection: () => lineIntersection,
|
|
51
|
+
normalizeToPixel: () => normalizeToPixel,
|
|
52
|
+
pointInPolygon: () => pointInPolygon,
|
|
53
|
+
tripwireCrossing: () => tripwireCrossing
|
|
106
54
|
});
|
|
55
|
+
module.exports = __toCommonJS(src_exports);
|
|
107
56
|
|
|
108
|
-
// src/
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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 });
|
|
146
125
|
}
|
|
147
|
-
return { matches, unmatchedTracks, unmatchedDetections };
|
|
148
126
|
}
|
|
149
127
|
}
|
|
150
|
-
|
|
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
|
+
}
|
|
151
148
|
|
|
152
|
-
// src/tracker/sort-tracker.
|
|
153
|
-
var
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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);
|
|
210
|
+
}
|
|
211
|
+
return this.getConfirmedTracks();
|
|
182
212
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
};
|
|
213
|
+
if (detections.length === 0) {
|
|
214
|
+
this.ageTracksAndPruneLost(frameTimestamp);
|
|
215
|
+
return this.getConfirmedTracks();
|
|
195
216
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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 };
|
|
201
241
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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 };
|
|
246
|
-
}
|
|
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);
|
|
258
|
-
}
|
|
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);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
this.tracks = survived;
|
|
312
|
-
void frameTimestamp;
|
|
242
|
+
for (const trackIdx of unmatchedTracks) {
|
|
243
|
+
const track = this.tracks[trackIdx];
|
|
244
|
+
track.age++;
|
|
245
|
+
}
|
|
246
|
+
const survived = [];
|
|
247
|
+
for (const track of this.tracks) {
|
|
248
|
+
if (track.age > this.config.maxAge) {
|
|
249
|
+
track.lost = true;
|
|
250
|
+
this.lostTracks.push(track);
|
|
251
|
+
} else {
|
|
252
|
+
survived.push(track);
|
|
313
253
|
}
|
|
314
|
-
}
|
|
315
|
-
|
|
254
|
+
}
|
|
255
|
+
this.tracks = survived;
|
|
256
|
+
for (const detIdx of unmatchedDetections) {
|
|
257
|
+
const det = detections[detIdx];
|
|
258
|
+
this.createTrack(det, frameTimestamp);
|
|
259
|
+
}
|
|
260
|
+
return this.getConfirmedTracks();
|
|
316
261
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
262
|
+
getActiveTracks() {
|
|
263
|
+
return this.tracks.map(trackToTrackedDetection);
|
|
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
|
|
330
288
|
};
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
}
|
|
369
|
-
}
|
|
370
|
-
const newInternal = {
|
|
371
|
-
enteredAt,
|
|
372
|
-
stationarySince,
|
|
373
|
-
totalDistancePx,
|
|
374
|
-
lastPosition: centroid
|
|
375
|
-
};
|
|
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
|
|
397
|
-
});
|
|
398
|
-
this.states.delete(id);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
return results;
|
|
402
|
-
}
|
|
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";
|
|
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);
|
|
420
304
|
}
|
|
421
|
-
}
|
|
422
|
-
|
|
305
|
+
}
|
|
306
|
+
this.tracks = survived;
|
|
307
|
+
void frameTimestamp;
|
|
423
308
|
}
|
|
424
|
-
}
|
|
309
|
+
};
|
|
425
310
|
|
|
426
|
-
// src/
|
|
427
|
-
var
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
}
|
|
311
|
+
// src/state/state-analyzer.ts
|
|
312
|
+
var DEFAULT_STATE_ANALYZER_CONFIG = {
|
|
313
|
+
stationaryThresholdSec: 10,
|
|
314
|
+
loiteringThresholdSec: 60,
|
|
315
|
+
velocityThreshold: 2,
|
|
316
|
+
enteringFrames: 5
|
|
317
|
+
};
|
|
318
|
+
function magnitude(v) {
|
|
319
|
+
return Math.sqrt(v.dx * v.dx + v.dy * v.dy);
|
|
320
|
+
}
|
|
321
|
+
var StateAnalyzer = class {
|
|
322
|
+
states = /* @__PURE__ */ new Map();
|
|
323
|
+
config;
|
|
324
|
+
constructor(config = {}) {
|
|
325
|
+
this.config = { ...DEFAULT_STATE_ANALYZER_CONFIG, ...config };
|
|
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;
|
|
508
355
|
}
|
|
509
|
-
return events;
|
|
510
356
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
357
|
+
const newInternal = {
|
|
358
|
+
enteredAt,
|
|
359
|
+
stationarySince,
|
|
360
|
+
totalDistancePx,
|
|
361
|
+
lastPosition: centroid
|
|
362
|
+
};
|
|
363
|
+
this.states.set(track.trackId, newInternal);
|
|
364
|
+
const dwellTimeMs = timestamp - enteredAt;
|
|
365
|
+
const state = this.computeObjectState(track.trackAge, speed, stationarySince, timestamp);
|
|
366
|
+
results.push({
|
|
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);
|
|
514
386
|
}
|
|
515
|
-
}
|
|
516
|
-
|
|
387
|
+
}
|
|
388
|
+
return results;
|
|
517
389
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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;
|
|
545
|
-
}
|
|
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
|
-
}
|
|
390
|
+
computeObjectState(trackAge, speed, stationarySince, timestamp) {
|
|
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";
|
|
574
398
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
this.lastEmitted.clear();
|
|
399
|
+
if (stationaryDurationSec >= this.config.stationaryThresholdSec) {
|
|
400
|
+
return "stationary";
|
|
578
401
|
}
|
|
579
|
-
}
|
|
580
|
-
|
|
402
|
+
}
|
|
403
|
+
if (speed > this.config.velocityThreshold) {
|
|
404
|
+
return "moving";
|
|
405
|
+
}
|
|
406
|
+
return "moving";
|
|
581
407
|
}
|
|
582
|
-
}
|
|
408
|
+
};
|
|
583
409
|
|
|
584
|
-
// src/
|
|
585
|
-
var
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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);
|
|
633
|
-
}
|
|
410
|
+
// src/zones/zone-evaluator.ts
|
|
411
|
+
var ZoneEvaluator = class {
|
|
412
|
+
/** Track which zones each track was in last frame */
|
|
413
|
+
trackZoneState = /* @__PURE__ */ new Map();
|
|
414
|
+
evaluate(tracks, zones, imageWidth, imageHeight, timestamp) {
|
|
415
|
+
const events = [];
|
|
416
|
+
const activeTrackIds = /* @__PURE__ */ new Set();
|
|
417
|
+
for (const track of tracks) {
|
|
418
|
+
activeTrackIds.add(track.trackId);
|
|
419
|
+
const centroid = bboxCentroid(track.bbox);
|
|
420
|
+
const prevZones = this.trackZoneState.get(track.trackId) ?? /* @__PURE__ */ new Set();
|
|
421
|
+
const currZones = /* @__PURE__ */ new Set();
|
|
422
|
+
for (const zone of zones) {
|
|
423
|
+
if (zone.alertOnClasses && zone.alertOnClasses.length > 0) {
|
|
424
|
+
if (!zone.alertOnClasses.includes(track.class)) continue;
|
|
634
425
|
}
|
|
635
|
-
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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";
|
|
426
|
+
if (zone.type === "polygon") {
|
|
427
|
+
const pixelPoints = zone.points.map((p) => normalizeToPixel(p, imageWidth, imageHeight));
|
|
428
|
+
const inside = pointInPolygon(centroid, pixelPoints);
|
|
429
|
+
if (inside) {
|
|
430
|
+
currZones.add(zone.id);
|
|
649
431
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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();
|
|
675
|
-
}
|
|
676
|
-
};
|
|
677
|
-
exports2.DetectionEventEmitter = DetectionEventEmitter2;
|
|
678
|
-
}
|
|
679
|
-
});
|
|
680
|
-
|
|
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 = {
|
|
432
|
+
if (inside && !prevZones.has(zone.id)) {
|
|
433
|
+
events.push({
|
|
434
|
+
type: "zone-enter",
|
|
435
|
+
zoneId: zone.id,
|
|
436
|
+
zoneName: zone.name,
|
|
702
437
|
trackId: track.trackId,
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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();
|
|
438
|
+
detection: track,
|
|
439
|
+
timestamp
|
|
440
|
+
});
|
|
727
441
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
442
|
+
if (!inside && prevZones.has(zone.id)) {
|
|
443
|
+
events.push({
|
|
444
|
+
type: "zone-exit",
|
|
445
|
+
zoneId: zone.id,
|
|
446
|
+
zoneName: zone.name,
|
|
447
|
+
trackId: track.trackId,
|
|
448
|
+
detection: track,
|
|
449
|
+
timestamp
|
|
450
|
+
});
|
|
738
451
|
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
452
|
+
} else if (zone.type === "tripwire" && track.path.length >= 1) {
|
|
453
|
+
const prevBbox = track.path[track.path.length - 1];
|
|
454
|
+
if (!prevBbox) continue;
|
|
455
|
+
const prevCentroid = bboxCentroid(prevBbox);
|
|
456
|
+
const p0 = zone.points[0];
|
|
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)
|
|
462
|
+
};
|
|
463
|
+
const cross = tripwireCrossing(prevCentroid, centroid, tripwireLine);
|
|
464
|
+
if (cross?.crossed) {
|
|
465
|
+
events.push({
|
|
466
|
+
type: "tripwire-cross",
|
|
467
|
+
zoneId: zone.id,
|
|
468
|
+
zoneName: zone.name,
|
|
469
|
+
trackId: track.trackId,
|
|
470
|
+
detection: track,
|
|
471
|
+
direction: cross.direction,
|
|
472
|
+
timestamp
|
|
750
473
|
});
|
|
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
474
|
}
|
|
769
475
|
}
|
|
770
476
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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());
|
|
477
|
+
this.trackZoneState.set(track.trackId, currZones);
|
|
478
|
+
}
|
|
479
|
+
for (const trackId of this.trackZoneState.keys()) {
|
|
480
|
+
if (!activeTrackIds.has(trackId)) {
|
|
481
|
+
this.trackZoneState.delete(trackId);
|
|
800
482
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
483
|
+
}
|
|
484
|
+
return events;
|
|
485
|
+
}
|
|
486
|
+
/** Returns a snapshot of the current track → zones mapping */
|
|
487
|
+
getTrackZones() {
|
|
488
|
+
return new Map(this.trackZoneState);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// src/events/event-filters.ts
|
|
493
|
+
var DEFAULT_EVENT_FILTER_CONFIG = {
|
|
494
|
+
minTrackAge: 3,
|
|
495
|
+
cooldownSec: 5,
|
|
496
|
+
enabledTypes: [
|
|
497
|
+
"object.entering",
|
|
498
|
+
"object.leaving",
|
|
499
|
+
"object.stationary",
|
|
500
|
+
"object.loitering",
|
|
501
|
+
"zone.enter",
|
|
502
|
+
"zone.exit",
|
|
503
|
+
"tripwire.cross"
|
|
504
|
+
]
|
|
505
|
+
};
|
|
506
|
+
var EventFilter = class {
|
|
507
|
+
constructor(config) {
|
|
508
|
+
this.config = config;
|
|
509
|
+
}
|
|
510
|
+
/** key: `${trackId}:${eventType}` → last emitted timestamp */
|
|
511
|
+
lastEmitted = /* @__PURE__ */ new Map();
|
|
512
|
+
shouldEmit(trackId, eventType, trackAge, timestamp) {
|
|
513
|
+
if (!this.config.enabledTypes.includes(eventType)) return false;
|
|
514
|
+
if (trackAge < this.config.minTrackAge) return false;
|
|
515
|
+
const key = `${trackId}:${eventType}`;
|
|
516
|
+
const last = this.lastEmitted.get(key);
|
|
517
|
+
if (last !== void 0 && timestamp - last < this.config.cooldownSec * 1e3) return false;
|
|
518
|
+
this.lastEmitted.set(key, timestamp);
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
/** Record an emission without a gate check — for events that bypass normal cooldown logic */
|
|
522
|
+
recordEmission(trackId, eventType, timestamp) {
|
|
523
|
+
const key = `${trackId}:${eventType}`;
|
|
524
|
+
this.lastEmitted.set(key, timestamp);
|
|
525
|
+
}
|
|
526
|
+
/** Remove cooldown entries for tracks that are no longer active */
|
|
527
|
+
cleanup(activeTrackIds) {
|
|
528
|
+
for (const key of this.lastEmitted.keys()) {
|
|
529
|
+
const colonIdx = key.indexOf(":");
|
|
530
|
+
if (colonIdx === -1) continue;
|
|
531
|
+
const trackId = key.slice(0, colonIdx);
|
|
532
|
+
if (!activeTrackIds.has(trackId)) {
|
|
533
|
+
this.lastEmitted.delete(key);
|
|
804
534
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/** Reset all cooldown state */
|
|
538
|
+
reset() {
|
|
539
|
+
this.lastEmitted.clear();
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
// src/events/event-emitter.ts
|
|
544
|
+
var import_node_crypto = require("crypto");
|
|
545
|
+
var DetectionEventEmitter = class {
|
|
546
|
+
filter;
|
|
547
|
+
/** trackId → last known ObjectState */
|
|
548
|
+
previousStates = /* @__PURE__ */ new Map();
|
|
549
|
+
constructor(filterConfig = {}) {
|
|
550
|
+
this.filter = new EventFilter({ ...DEFAULT_EVENT_FILTER_CONFIG, ...filterConfig });
|
|
551
|
+
}
|
|
552
|
+
emit(tracks, states, zoneEvents, classifications, deviceId) {
|
|
553
|
+
const events = [];
|
|
554
|
+
const timestamp = Date.now();
|
|
555
|
+
const trackMap = /* @__PURE__ */ new Map();
|
|
556
|
+
for (const t of tracks) trackMap.set(t.trackId, t);
|
|
557
|
+
const stateMap = /* @__PURE__ */ new Map();
|
|
558
|
+
for (const s of states) stateMap.set(s.trackId, s);
|
|
559
|
+
const classifMap = /* @__PURE__ */ new Map();
|
|
560
|
+
for (const c of classifications) classifMap.set(c.trackId, c.classifications);
|
|
561
|
+
for (const state of states) {
|
|
562
|
+
const track = trackMap.get(state.trackId);
|
|
563
|
+
if (!track) continue;
|
|
564
|
+
const prevState = this.previousStates.get(state.trackId);
|
|
565
|
+
const classifs = classifMap.get(state.trackId) ?? [];
|
|
566
|
+
if (state.state === "entering" && prevState === void 0) {
|
|
567
|
+
this.tryEmit(events, "object.entering", track, state, classifs, [], deviceId, timestamp);
|
|
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);
|
|
822
582
|
}
|
|
823
|
-
}
|
|
824
|
-
|
|
583
|
+
}
|
|
584
|
+
for (const ze of zoneEvents) {
|
|
585
|
+
const track = trackMap.get(ze.trackId);
|
|
586
|
+
if (!track) continue;
|
|
587
|
+
const state = stateMap.get(ze.trackId);
|
|
588
|
+
if (!state) continue;
|
|
589
|
+
let eventType;
|
|
590
|
+
if (ze.type === "tripwire-cross") {
|
|
591
|
+
eventType = "tripwire.cross";
|
|
592
|
+
} else if (ze.type === "zone-enter") {
|
|
593
|
+
eventType = "zone.enter";
|
|
594
|
+
} else {
|
|
595
|
+
eventType = "zone.exit";
|
|
596
|
+
}
|
|
597
|
+
const classifs = classifMap.get(ze.trackId) ?? [];
|
|
598
|
+
this.tryEmit(events, eventType, track, state, classifs, [ze], deviceId, timestamp);
|
|
599
|
+
}
|
|
600
|
+
const activeIds = new Set(tracks.map((t) => t.trackId));
|
|
601
|
+
this.filter.cleanup(activeIds);
|
|
602
|
+
return events;
|
|
825
603
|
}
|
|
826
|
-
|
|
604
|
+
tryEmit(events, eventType, track, state, classifications, zoneEvents, deviceId, timestamp) {
|
|
605
|
+
if (!this.filter.shouldEmit(track.trackId, eventType, track.trackAge, timestamp)) return;
|
|
606
|
+
events.push({
|
|
607
|
+
id: (0, import_node_crypto.randomUUID)(),
|
|
608
|
+
type: eventType,
|
|
609
|
+
timestamp,
|
|
610
|
+
deviceId,
|
|
611
|
+
detection: track,
|
|
612
|
+
classifications,
|
|
613
|
+
objectState: state,
|
|
614
|
+
zoneEvents,
|
|
615
|
+
trackPath: track.path
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
reset() {
|
|
619
|
+
this.previousStates.clear();
|
|
620
|
+
this.filter.reset();
|
|
621
|
+
}
|
|
622
|
+
};
|
|
827
623
|
|
|
828
|
-
// src/analytics/
|
|
829
|
-
var
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
624
|
+
// src/analytics/track-store.ts
|
|
625
|
+
var MAX_PATH_LENGTH2 = 300;
|
|
626
|
+
var TrackStore = class {
|
|
627
|
+
activeTracks = /* @__PURE__ */ new Map();
|
|
628
|
+
update(tracks, states, zoneEvents, classifications) {
|
|
629
|
+
const stateMap = /* @__PURE__ */ new Map();
|
|
630
|
+
for (const s of states) stateMap.set(s.trackId, s);
|
|
631
|
+
const classifMap = /* @__PURE__ */ new Map();
|
|
632
|
+
for (const c of classifications) classifMap.set(c.trackId, c.classifications);
|
|
633
|
+
const now = Date.now();
|
|
634
|
+
for (const track of tracks) {
|
|
635
|
+
let acc = this.activeTracks.get(track.trackId);
|
|
636
|
+
if (!acc) {
|
|
637
|
+
acc = {
|
|
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 };
|
|
842
672
|
}
|
|
843
673
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
};
|
|
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
|
-
};
|
|
674
|
+
}
|
|
675
|
+
for (const ze of zoneEvents) {
|
|
676
|
+
const acc = this.activeTracks.get(ze.trackId);
|
|
677
|
+
if (!acc) continue;
|
|
678
|
+
if (ze.type === "zone-enter") {
|
|
679
|
+
acc.zoneTransitions.push({
|
|
680
|
+
zoneId: ze.zoneId,
|
|
681
|
+
zoneName: ze.zoneName,
|
|
682
|
+
entered: ze.timestamp,
|
|
683
|
+
dwellMs: 0
|
|
886
684
|
});
|
|
887
|
-
|
|
888
|
-
let
|
|
889
|
-
for (
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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);
|
|
685
|
+
} else if (ze.type === "zone-exit") {
|
|
686
|
+
let openIdx = -1;
|
|
687
|
+
for (let i = acc.zoneTransitions.length - 1; i >= 0; i--) {
|
|
688
|
+
const t = acc.zoneTransitions[i];
|
|
689
|
+
if (t.zoneId === ze.zoneId && t.exited === void 0) {
|
|
690
|
+
openIdx = i;
|
|
691
|
+
break;
|
|
907
692
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
trackIds: tracksInZone.map((t) => t.trackId),
|
|
916
|
-
avgDwellTimeMs,
|
|
917
|
-
totalEntrancesToday: 0,
|
|
918
|
-
// requires DB — left for server orchestrator
|
|
919
|
-
totalExitsToday: 0,
|
|
920
|
-
loiteringTracks
|
|
693
|
+
}
|
|
694
|
+
if (openIdx >= 0) {
|
|
695
|
+
const open = acc.zoneTransitions[openIdx];
|
|
696
|
+
acc.zoneTransitions[openIdx] = {
|
|
697
|
+
...open,
|
|
698
|
+
exited: ze.timestamp,
|
|
699
|
+
dwellMs: ze.timestamp - open.entered
|
|
921
700
|
};
|
|
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;
|
|
701
|
+
}
|
|
940
702
|
}
|
|
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
|
|
941
752
|
};
|
|
942
|
-
exports2.LiveStateManager = LiveStateManager2;
|
|
943
753
|
}
|
|
944
|
-
}
|
|
754
|
+
};
|
|
945
755
|
|
|
946
|
-
// src/analytics/
|
|
947
|
-
var
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
756
|
+
// src/analytics/live-state-manager.ts
|
|
757
|
+
var FpsCounter = class {
|
|
758
|
+
frameTimes = [];
|
|
759
|
+
windowMs = 5e3;
|
|
760
|
+
tick(timestamp) {
|
|
761
|
+
this.frameTimes.push(timestamp);
|
|
762
|
+
const cutoff = timestamp - this.windowMs;
|
|
763
|
+
while (this.frameTimes.length > 0 && this.frameTimes[0] < cutoff) {
|
|
764
|
+
this.frameTimes.shift();
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
getFps() {
|
|
768
|
+
if (this.frameTimes.length < 2) return 0;
|
|
769
|
+
const oldest = this.frameTimes[0];
|
|
770
|
+
const newest = this.frameTimes[this.frameTimes.length - 1];
|
|
771
|
+
const durationSec = (newest - oldest) / 1e3;
|
|
772
|
+
if (durationSec <= 0) return 0;
|
|
773
|
+
return (this.frameTimes.length - 1) / durationSec;
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
var LiveStateManager = class {
|
|
777
|
+
zones = [];
|
|
778
|
+
fpsCounter = new FpsCounter();
|
|
779
|
+
pipelineMsHistory = [];
|
|
780
|
+
maxPipelineHistory = 30;
|
|
781
|
+
setZones(zones) {
|
|
782
|
+
this.zones = [...zones];
|
|
783
|
+
}
|
|
784
|
+
buildState(deviceId, tracks, states, trackZones, pipelineStatus, pipelineMs, lastEvent, timestamp) {
|
|
785
|
+
this.fpsCounter.tick(timestamp);
|
|
786
|
+
if (this.pipelineMsHistory.length >= this.maxPipelineHistory) {
|
|
787
|
+
this.pipelineMsHistory.shift();
|
|
788
|
+
}
|
|
789
|
+
this.pipelineMsHistory.push(pipelineMs);
|
|
790
|
+
const avgPipelineMs = this.pipelineMsHistory.length === 0 ? 0 : this.pipelineMsHistory.reduce((sum, v) => sum + v, 0) / this.pipelineMsHistory.length;
|
|
791
|
+
const stateMap = /* @__PURE__ */ new Map();
|
|
792
|
+
for (const s of states) stateMap.set(s.trackId, s);
|
|
793
|
+
const trackSummaries = tracks.map((track) => {
|
|
794
|
+
const state = stateMap.get(track.trackId);
|
|
795
|
+
const inZones = Array.from(trackZones.get(track.trackId) ?? []);
|
|
796
|
+
return {
|
|
797
|
+
trackId: track.trackId,
|
|
798
|
+
class: track.class,
|
|
799
|
+
originalClass: track.originalClass,
|
|
800
|
+
state: state?.state ?? "moving",
|
|
801
|
+
bbox: track.bbox,
|
|
802
|
+
velocity: track.velocity ?? { dx: 0, dy: 0 },
|
|
803
|
+
dwellTimeMs: state?.dwellTimeMs ?? 0,
|
|
804
|
+
inZones
|
|
805
|
+
};
|
|
806
|
+
});
|
|
807
|
+
let movingObjects = 0;
|
|
808
|
+
let stationaryObjects = 0;
|
|
809
|
+
for (const summary of trackSummaries) {
|
|
810
|
+
if (summary.state === "moving" || summary.state === "entering") movingObjects++;
|
|
811
|
+
else if (summary.state === "stationary" || summary.state === "loitering") stationaryObjects++;
|
|
812
|
+
}
|
|
813
|
+
const objectCounts = {};
|
|
814
|
+
for (const summary of trackSummaries) {
|
|
815
|
+
objectCounts[summary.class] = (objectCounts[summary.class] ?? 0) + 1;
|
|
816
|
+
}
|
|
817
|
+
const zoneLiveStates = this.zones.map((zone) => {
|
|
818
|
+
const tracksInZone = trackSummaries.filter((t) => t.inZones.includes(zone.id));
|
|
819
|
+
const objectsByClass = {};
|
|
820
|
+
const loiteringTracks = [];
|
|
821
|
+
for (const t of tracksInZone) {
|
|
822
|
+
objectsByClass[t.class] = (objectsByClass[t.class] ?? 0) + 1;
|
|
823
|
+
if (t.state === "loitering") loiteringTracks.push(t.trackId);
|
|
824
|
+
}
|
|
825
|
+
const avgDwellTimeMs = tracksInZone.length === 0 ? 0 : tracksInZone.reduce((sum, t) => sum + t.dwellTimeMs, 0) / tracksInZone.length;
|
|
826
|
+
return {
|
|
827
|
+
zoneId: zone.id,
|
|
828
|
+
zoneName: zone.name,
|
|
829
|
+
occupied: tracksInZone.length > 0,
|
|
830
|
+
objectCount: tracksInZone.length,
|
|
831
|
+
objectsByClass,
|
|
832
|
+
trackIds: tracksInZone.map((t) => t.trackId),
|
|
833
|
+
avgDwellTimeMs,
|
|
834
|
+
totalEntrancesToday: 0,
|
|
835
|
+
// requires DB — left for server orchestrator
|
|
836
|
+
totalExitsToday: 0,
|
|
837
|
+
loiteringTracks
|
|
838
|
+
};
|
|
839
|
+
});
|
|
840
|
+
return {
|
|
841
|
+
deviceId,
|
|
842
|
+
lastFrameTimestamp: timestamp,
|
|
843
|
+
activeObjects: trackSummaries.length,
|
|
844
|
+
movingObjects,
|
|
845
|
+
stationaryObjects,
|
|
846
|
+
objectCounts,
|
|
847
|
+
tracks: trackSummaries,
|
|
848
|
+
zones: zoneLiveStates,
|
|
849
|
+
lastEvent,
|
|
850
|
+
pipelineStatus,
|
|
851
|
+
avgPipelineMs,
|
|
852
|
+
currentFps: this.fpsCounter.getFps()
|
|
1004
853
|
};
|
|
1005
|
-
exports2.HeatmapAggregator = HeatmapAggregator2;
|
|
1006
854
|
}
|
|
1007
|
-
|
|
855
|
+
reset() {
|
|
856
|
+
this.pipelineMsHistory.length = 0;
|
|
857
|
+
}
|
|
858
|
+
};
|
|
1008
859
|
|
|
1009
|
-
// src/analytics/
|
|
1010
|
-
var
|
|
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
|
-
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] : [];
|
|
1078
|
-
}
|
|
860
|
+
// src/analytics/heatmap-aggregator.ts
|
|
861
|
+
var HeatmapAggregator = class {
|
|
862
|
+
constructor(width, height, gridSize) {
|
|
863
|
+
this.width = width;
|
|
864
|
+
this.height = height;
|
|
865
|
+
this.gridSize = gridSize;
|
|
866
|
+
if (gridSize <= 0) throw new Error("gridSize must be > 0");
|
|
867
|
+
this.grid = new Float32Array(gridSize * gridSize);
|
|
868
|
+
}
|
|
869
|
+
grid;
|
|
870
|
+
maxCount = 0;
|
|
871
|
+
/**
|
|
872
|
+
* Add a normalized point (0–1 range) to the heatmap.
|
|
873
|
+
* Points outside [0, 1] are clamped to the grid boundary.
|
|
874
|
+
*/
|
|
875
|
+
addPoint(x, y) {
|
|
876
|
+
const col = Math.min(Math.floor(x * this.gridSize), this.gridSize - 1);
|
|
877
|
+
const row = Math.min(Math.floor(y * this.gridSize), this.gridSize - 1);
|
|
878
|
+
const idx = row * this.gridSize + col;
|
|
879
|
+
this.grid[idx] += 1;
|
|
880
|
+
if (this.grid[idx] > this.maxCount) {
|
|
881
|
+
this.maxCount = this.grid[idx];
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
/** Add a pixel-coordinate point. Normalizes against the configured width/height. */
|
|
885
|
+
addPixelPoint(px, py) {
|
|
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
|
|
1079
895
|
};
|
|
1080
|
-
exports2.AnalyticsProvider = AnalyticsProvider2;
|
|
1081
896
|
}
|
|
1082
|
-
|
|
897
|
+
reset() {
|
|
898
|
+
this.grid.fill(0);
|
|
899
|
+
this.maxCount = 0;
|
|
900
|
+
}
|
|
901
|
+
/** Total number of points accumulated */
|
|
902
|
+
get totalPoints() {
|
|
903
|
+
let total = 0;
|
|
904
|
+
for (let i = 0; i < this.grid.length; i++) {
|
|
905
|
+
total += this.grid[i];
|
|
906
|
+
}
|
|
907
|
+
return total;
|
|
908
|
+
}
|
|
909
|
+
};
|
|
1083
910
|
|
|
1084
|
-
// src/
|
|
1085
|
-
var
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
911
|
+
// src/analytics/analytics-provider.ts
|
|
912
|
+
var AnalyticsProvider = class {
|
|
913
|
+
constructor(liveStateManager, trackStore, getZoneHistoryCb, getHeatmapCb, lastLiveState = null) {
|
|
914
|
+
this.liveStateManager = liveStateManager;
|
|
915
|
+
this.trackStore = trackStore;
|
|
916
|
+
this.getZoneHistoryCb = getZoneHistoryCb;
|
|
917
|
+
this.getHeatmapCb = getHeatmapCb;
|
|
918
|
+
this.lastLiveState = lastLiveState;
|
|
919
|
+
}
|
|
920
|
+
/** Called by the orchestrator each frame to update the cached live state */
|
|
921
|
+
updateLiveState(state) {
|
|
922
|
+
this.lastLiveState = state;
|
|
923
|
+
}
|
|
924
|
+
getLiveState(_deviceId) {
|
|
925
|
+
return this.lastLiveState;
|
|
926
|
+
}
|
|
927
|
+
getTracks(_deviceId, filter) {
|
|
928
|
+
const tracks = this.lastLiveState?.tracks ?? [];
|
|
929
|
+
if (!filter) return [...tracks];
|
|
930
|
+
return tracks.filter((t) => {
|
|
931
|
+
if (filter.class && !filter.class.includes(t.class)) return false;
|
|
932
|
+
if (filter.state && !filter.state.includes(t.state)) return false;
|
|
933
|
+
if (filter.inZone && !t.inZones.includes(filter.inZone)) return false;
|
|
934
|
+
if (filter.minDwellMs !== void 0 && t.dwellTimeMs < filter.minDwellMs) return false;
|
|
935
|
+
return true;
|
|
1105
936
|
});
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
exports2.DEFAULT_SNAPSHOT_CONFIG = {
|
|
1128
|
-
enabled: false,
|
|
1129
|
-
saveThumbnail: true,
|
|
1130
|
-
saveAnnotatedFrame: false,
|
|
1131
|
-
saveDebugThumbnails: false
|
|
937
|
+
}
|
|
938
|
+
getZoneState(_deviceId, zoneId) {
|
|
939
|
+
return this.lastLiveState?.zones.find((z) => z.zoneId === zoneId) ?? null;
|
|
940
|
+
}
|
|
941
|
+
async getZoneHistory(deviceId, zoneId, options) {
|
|
942
|
+
if (this.getZoneHistoryCb) {
|
|
943
|
+
return this.getZoneHistoryCb(deviceId, zoneId, options);
|
|
944
|
+
}
|
|
945
|
+
return [];
|
|
946
|
+
}
|
|
947
|
+
async getHeatmap(deviceId, options) {
|
|
948
|
+
if (this.getHeatmapCb) {
|
|
949
|
+
return this.getHeatmapCb(deviceId, options);
|
|
950
|
+
}
|
|
951
|
+
const gridSize = options.resolution;
|
|
952
|
+
return {
|
|
953
|
+
width: 1920,
|
|
954
|
+
height: 1080,
|
|
955
|
+
gridSize,
|
|
956
|
+
cells: new Float32Array(gridSize * gridSize),
|
|
957
|
+
maxCount: 0
|
|
1132
958
|
};
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
959
|
+
}
|
|
960
|
+
getTrackDetail(_deviceId, trackId) {
|
|
961
|
+
return this.trackStore.getTrackDetail(trackId);
|
|
962
|
+
}
|
|
963
|
+
getCameraStatus(_deviceId) {
|
|
964
|
+
return this.lastLiveState ? [...this.lastLiveState.pipelineStatus] : [];
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
// src/snapshots/snapshot-manager.ts
|
|
969
|
+
var DEFAULT_SNAPSHOT_CONFIG = {
|
|
970
|
+
enabled: false,
|
|
971
|
+
saveThumbnail: true,
|
|
972
|
+
saveAnnotatedFrame: false,
|
|
973
|
+
saveDebugThumbnails: false
|
|
974
|
+
};
|
|
975
|
+
var SnapshotManager = class {
|
|
976
|
+
constructor(config) {
|
|
977
|
+
this.config = config;
|
|
978
|
+
}
|
|
979
|
+
async capture(frame, detection, allDetections, eventId) {
|
|
980
|
+
if (!this.config.enabled) return void 0;
|
|
981
|
+
await this.ensureOutputDir();
|
|
982
|
+
let thumbnailPath;
|
|
983
|
+
let annotatedFramePath;
|
|
984
|
+
if (this.config.saveThumbnail) {
|
|
985
|
+
thumbnailPath = await this.saveThumbnail(frame, detection.bbox, eventId);
|
|
986
|
+
}
|
|
987
|
+
if (this.config.saveAnnotatedFrame) {
|
|
988
|
+
annotatedFramePath = await this.saveAnnotatedFrame(frame, allDetections, eventId);
|
|
989
|
+
}
|
|
990
|
+
if (!thumbnailPath && !annotatedFramePath) return void 0;
|
|
991
|
+
return {
|
|
992
|
+
thumbnailPath: thumbnailPath ?? "",
|
|
993
|
+
annotatedFramePath: annotatedFramePath ?? ""
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
async saveThumbnail(frame, bbox, eventId) {
|
|
997
|
+
const sharp = await import("sharp");
|
|
998
|
+
const { frameWidth, frameHeight } = await this.resolveFrameDimensions(frame);
|
|
999
|
+
const left = Math.max(0, Math.round(bbox.x));
|
|
1000
|
+
const top = Math.max(0, Math.round(bbox.y));
|
|
1001
|
+
const width = Math.min(Math.round(bbox.w), frameWidth - left);
|
|
1002
|
+
const height = Math.min(Math.round(bbox.h), frameHeight - top);
|
|
1003
|
+
if (width <= 0 || height <= 0) {
|
|
1004
|
+
throw new Error(`Invalid crop dimensions for event ${eventId}: ${JSON.stringify({ left, top, width, height, frameWidth, frameHeight })}`);
|
|
1005
|
+
}
|
|
1006
|
+
const relativePath = `${eventId}_thumb.jpg`;
|
|
1007
|
+
const inputOptions = this.sharpInputOptions(frame);
|
|
1008
|
+
const buffer = await sharp.default(frame.data, inputOptions).extract({ left, top, width, height }).jpeg({ quality: 85 }).toBuffer();
|
|
1009
|
+
await this.writeOutput(relativePath, buffer);
|
|
1010
|
+
return relativePath;
|
|
1011
|
+
}
|
|
1012
|
+
async saveAnnotatedFrame(frame, detections, eventId) {
|
|
1013
|
+
const sharp = await import("sharp");
|
|
1014
|
+
const { frameWidth, frameHeight } = await this.resolveFrameDimensions(frame);
|
|
1015
|
+
const relativePath = `${eventId}_annotated.jpg`;
|
|
1016
|
+
const svgBoxes = detections.map((det) => {
|
|
1017
|
+
const x = Math.max(0, Math.round(det.bbox.x));
|
|
1018
|
+
const y = Math.max(0, Math.round(det.bbox.y));
|
|
1019
|
+
const w = Math.min(Math.round(det.bbox.w), frameWidth - x);
|
|
1020
|
+
const h = Math.min(Math.round(det.bbox.h), frameHeight - y);
|
|
1021
|
+
const label = `${det.class} ${(det.score * 100).toFixed(0)}%`;
|
|
1022
|
+
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="none" stroke="lime" stroke-width="2"/>
|
|
1184
1023
|
<text x="${x + 2}" y="${Math.max(y - 2, 10)}" font-size="12" fill="lime" font-family="sans-serif">${label}</text>`;
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
1024
|
+
}).join("\n");
|
|
1025
|
+
const svgOverlay = Buffer.from(
|
|
1026
|
+
`<svg width="${frameWidth}" height="${frameHeight}" xmlns="http://www.w3.org/2000/svg">${svgBoxes}</svg>`
|
|
1027
|
+
);
|
|
1028
|
+
const inputOptions = this.sharpInputOptions(frame);
|
|
1029
|
+
const buffer = await sharp.default(frame.data, inputOptions).composite([{ input: svgOverlay, top: 0, left: 0 }]).jpeg({ quality: 85 }).toBuffer();
|
|
1030
|
+
await this.writeOutput(relativePath, buffer);
|
|
1031
|
+
return relativePath;
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Write output using mediaStorage if available, otherwise fall back to fs.writeFile.
|
|
1035
|
+
*/
|
|
1036
|
+
async writeOutput(relativePath, data) {
|
|
1037
|
+
if (this.config.mediaStorage) {
|
|
1038
|
+
await this.config.mediaStorage.writeFile(relativePath, data);
|
|
1039
|
+
} else if (this.config.outputDir) {
|
|
1040
|
+
const { writeFile } = await import("fs/promises");
|
|
1041
|
+
const { join } = await import("path");
|
|
1042
|
+
await writeFile(join(this.config.outputDir, relativePath), data);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Resolve actual frame dimensions. JPEG frames from decoders may report
|
|
1047
|
+
* width=0, height=0 — in that case read the real size from sharp metadata.
|
|
1048
|
+
*/
|
|
1049
|
+
async resolveFrameDimensions(frame) {
|
|
1050
|
+
if (frame.width > 0 && frame.height > 0) {
|
|
1051
|
+
return { frameWidth: frame.width, frameHeight: frame.height };
|
|
1052
|
+
}
|
|
1053
|
+
if (frame.format === "jpeg") {
|
|
1054
|
+
const sharp = await import("sharp");
|
|
1055
|
+
const meta = await sharp.default(frame.data).metadata();
|
|
1056
|
+
return {
|
|
1057
|
+
frameWidth: meta.width ?? 0,
|
|
1058
|
+
frameHeight: meta.height ?? 0
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
return { frameWidth: frame.width, frameHeight: frame.height };
|
|
1062
|
+
}
|
|
1063
|
+
sharpInputOptions(frame) {
|
|
1064
|
+
if (frame.format === "jpeg") {
|
|
1065
|
+
return {};
|
|
1066
|
+
}
|
|
1067
|
+
const channels = frame.format === "rgb" ? 3 : 3;
|
|
1068
|
+
return {
|
|
1069
|
+
raw: {
|
|
1070
|
+
width: frame.width,
|
|
1071
|
+
height: frame.height,
|
|
1072
|
+
channels
|
|
1240
1073
|
}
|
|
1241
1074
|
};
|
|
1242
|
-
exports2.SnapshotManager = SnapshotManager2;
|
|
1243
1075
|
}
|
|
1244
|
-
|
|
1076
|
+
async ensureOutputDir() {
|
|
1077
|
+
if (this.config.outputDir) {
|
|
1078
|
+
const { mkdir } = await import("fs/promises");
|
|
1079
|
+
await mkdir(this.config.outputDir, { recursive: true });
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1245
1083
|
|
|
1246
|
-
// src/pipeline/analysis-pipeline.
|
|
1247
|
-
var
|
|
1248
|
-
"
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
config
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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;
|
|
1337
|
-
}
|
|
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());
|
|
1355
|
-
}
|
|
1356
|
-
this.listeners.get(deviceId).add(callback);
|
|
1357
|
-
return () => {
|
|
1358
|
-
this.listeners.get(deviceId)?.delete(callback);
|
|
1359
|
-
};
|
|
1360
|
-
}
|
|
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);
|
|
1084
|
+
// src/pipeline/analysis-pipeline.ts
|
|
1085
|
+
var AnalysisPipeline = class {
|
|
1086
|
+
id = "pipeline-analysis";
|
|
1087
|
+
manifest = {
|
|
1088
|
+
id: "pipeline-analysis",
|
|
1089
|
+
name: "Pipeline Analysis",
|
|
1090
|
+
version: "0.1.0",
|
|
1091
|
+
description: "Object tracking, state analysis, zone evaluation, and event emission",
|
|
1092
|
+
packageName: "@camstack/lib-pipeline-analysis"
|
|
1093
|
+
};
|
|
1094
|
+
config;
|
|
1095
|
+
cameras = /* @__PURE__ */ new Map();
|
|
1096
|
+
knownFaces = [];
|
|
1097
|
+
knownPlates = [];
|
|
1098
|
+
listeners = /* @__PURE__ */ new Map();
|
|
1099
|
+
/** Optional callback: server orchestrator can hook in to persist finished tracks */
|
|
1100
|
+
onTrackFinished;
|
|
1101
|
+
constructor(config = {}) {
|
|
1102
|
+
this.config = {
|
|
1103
|
+
tracker: config.tracker ?? {},
|
|
1104
|
+
stateAnalyzer: config.stateAnalyzer ?? {},
|
|
1105
|
+
eventFilter: config.eventFilter ?? {},
|
|
1106
|
+
snapshot: config.snapshot ?? {},
|
|
1107
|
+
heatmapGridSize: config.heatmapGridSize ?? 32,
|
|
1108
|
+
defaultFrameWidth: config.defaultFrameWidth ?? 1920,
|
|
1109
|
+
defaultFrameHeight: config.defaultFrameHeight ?? 1080
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
async initialize(_ctx) {
|
|
1113
|
+
}
|
|
1114
|
+
async shutdown() {
|
|
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);
|
|
1408
1159
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
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;
|
|
1160
|
+
}
|
|
1161
|
+
const pipelineMs = pipelineResult?.totalMs ?? 0;
|
|
1162
|
+
const pipelineStatus = [];
|
|
1163
|
+
const liveState = camera.liveStateManager.buildState(
|
|
1164
|
+
deviceId,
|
|
1165
|
+
tracked,
|
|
1166
|
+
states,
|
|
1167
|
+
camera.zoneEvaluator.getTrackZones(),
|
|
1168
|
+
pipelineStatus,
|
|
1169
|
+
pipelineMs,
|
|
1170
|
+
events[events.length - 1],
|
|
1171
|
+
timestamp
|
|
1172
|
+
);
|
|
1173
|
+
camera.lastLiveState = liveState;
|
|
1174
|
+
camera.analyticsProvider.updateLiveState(liveState);
|
|
1175
|
+
const cameraListeners = this.listeners.get(deviceId);
|
|
1176
|
+
if (cameraListeners) {
|
|
1177
|
+
for (const cb of cameraListeners) {
|
|
1178
|
+
cb(liveState);
|
|
1440
1179
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
// ICameraAnalyticsProvider delegates to per-camera AnalyticsProvider
|
|
1207
|
+
getLiveState(deviceId) {
|
|
1208
|
+
return this.cameras.get(deviceId)?.lastLiveState ?? null;
|
|
1209
|
+
}
|
|
1210
|
+
getTracks(deviceId, filter) {
|
|
1211
|
+
const camera = this.cameras.get(deviceId);
|
|
1212
|
+
if (!camera) return [];
|
|
1213
|
+
return camera.analyticsProvider.getTracks(deviceId, filter);
|
|
1214
|
+
}
|
|
1215
|
+
getZoneState(deviceId, zoneId) {
|
|
1216
|
+
const camera = this.cameras.get(deviceId);
|
|
1217
|
+
if (!camera) return null;
|
|
1218
|
+
return camera.analyticsProvider.getZoneState(deviceId, zoneId);
|
|
1219
|
+
}
|
|
1220
|
+
async getZoneHistory(deviceId, zoneId, options) {
|
|
1221
|
+
const camera = this.cameras.get(deviceId);
|
|
1222
|
+
if (!camera) return [];
|
|
1223
|
+
return camera.analyticsProvider.getZoneHistory(deviceId, zoneId, options);
|
|
1224
|
+
}
|
|
1225
|
+
async getHeatmap(deviceId, options) {
|
|
1226
|
+
const camera = this.cameras.get(deviceId);
|
|
1227
|
+
if (!camera) {
|
|
1228
|
+
const gridSize = options.resolution;
|
|
1229
|
+
return {
|
|
1230
|
+
width: this.config.defaultFrameWidth,
|
|
1231
|
+
height: this.config.defaultFrameHeight,
|
|
1232
|
+
gridSize,
|
|
1233
|
+
cells: new Float32Array(gridSize * gridSize),
|
|
1234
|
+
maxCount: 0
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
return camera.heatmapAggregator.getHeatmap();
|
|
1238
|
+
}
|
|
1239
|
+
getTrackDetail(deviceId, trackId) {
|
|
1240
|
+
const camera = this.cameras.get(deviceId);
|
|
1241
|
+
if (!camera) return null;
|
|
1242
|
+
return camera.trackStore.getTrackDetail(trackId);
|
|
1243
|
+
}
|
|
1244
|
+
getCameraStatus(deviceId) {
|
|
1245
|
+
const camera = this.cameras.get(deviceId);
|
|
1246
|
+
if (!camera) return [];
|
|
1247
|
+
return camera.analyticsProvider.getCameraStatus(deviceId);
|
|
1248
|
+
}
|
|
1249
|
+
getOrCreateCamera(deviceId) {
|
|
1250
|
+
const existing = this.cameras.get(deviceId);
|
|
1251
|
+
if (existing) return existing;
|
|
1252
|
+
const tracker = new SortTracker(this.config.tracker);
|
|
1253
|
+
const stateAnalyzer = new StateAnalyzer(this.config.stateAnalyzer);
|
|
1254
|
+
const zoneEvaluator = new ZoneEvaluator();
|
|
1255
|
+
const eventEmitter = new DetectionEventEmitter(this.config.eventFilter);
|
|
1256
|
+
const trackStore = new TrackStore();
|
|
1257
|
+
const liveStateManager = new LiveStateManager();
|
|
1258
|
+
const heatmapAggregator = new HeatmapAggregator(
|
|
1259
|
+
this.config.defaultFrameWidth,
|
|
1260
|
+
this.config.defaultFrameHeight,
|
|
1261
|
+
this.config.heatmapGridSize
|
|
1262
|
+
);
|
|
1263
|
+
const analyticsProvider = new AnalyticsProvider(liveStateManager, trackStore);
|
|
1264
|
+
const snapshotManager = new SnapshotManager({
|
|
1265
|
+
...DEFAULT_SNAPSHOT_CONFIG,
|
|
1266
|
+
...this.config.snapshot
|
|
1267
|
+
});
|
|
1268
|
+
const cameraState = {
|
|
1269
|
+
tracker,
|
|
1270
|
+
stateAnalyzer,
|
|
1271
|
+
zoneEvaluator,
|
|
1272
|
+
eventEmitter,
|
|
1273
|
+
trackStore,
|
|
1274
|
+
liveStateManager,
|
|
1275
|
+
heatmapAggregator,
|
|
1276
|
+
analyticsProvider,
|
|
1277
|
+
snapshotManager,
|
|
1278
|
+
zones: [],
|
|
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);
|
|
1450
1291
|
}
|
|
1451
|
-
return detections;
|
|
1452
1292
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1293
|
+
}
|
|
1294
|
+
return detections;
|
|
1295
|
+
}
|
|
1296
|
+
matchClassifications(pipelineResult, trackIds) {
|
|
1297
|
+
const classifierResults = [];
|
|
1298
|
+
for (const stepResult of pipelineResult.results) {
|
|
1299
|
+
if (stepResult.slot === "classifier") {
|
|
1300
|
+
const output = stepResult.output;
|
|
1301
|
+
if (output.classifications) {
|
|
1302
|
+
classifierResults.push([...output.classifications]);
|
|
1462
1303
|
}
|
|
1463
|
-
return trackIds.slice(0, classifierResults.length).map((trackId, i) => ({
|
|
1464
|
-
trackId,
|
|
1465
|
-
classifications: classifierResults[i] ?? []
|
|
1466
|
-
}));
|
|
1467
1304
|
}
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1305
|
+
}
|
|
1306
|
+
return trackIds.slice(0, classifierResults.length).map((trackId, i) => ({
|
|
1307
|
+
trackId,
|
|
1308
|
+
classifications: classifierResults[i] ?? []
|
|
1309
|
+
}));
|
|
1470
1310
|
}
|
|
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());
|
|
1311
|
+
};
|
|
1507
1312
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1508
1313
|
0 && (module.exports = {
|
|
1509
1314
|
AnalysisPipeline,
|
|
@@ -1521,6 +1326,11 @@ var import_analysis_pipeline = __toESM(require_analysis_pipeline());
|
|
|
1521
1326
|
StateAnalyzer,
|
|
1522
1327
|
TrackStore,
|
|
1523
1328
|
ZoneEvaluator,
|
|
1524
|
-
|
|
1329
|
+
bboxCentroid,
|
|
1330
|
+
greedyAssignment,
|
|
1331
|
+
lineIntersection,
|
|
1332
|
+
normalizeToPixel,
|
|
1333
|
+
pointInPolygon,
|
|
1334
|
+
tripwireCrossing
|
|
1525
1335
|
});
|
|
1526
1336
|
//# sourceMappingURL=index.js.map
|