@talmolab/sleap-io.js 0.1.0
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/LICENSE +29 -0
- package/README.md +75 -0
- package/dist/index.d.ts +436 -0
- package/dist/index.js +2616 -0
- package/package.json +33 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2616 @@
|
|
|
1
|
+
// src/model/instance.ts
|
|
2
|
+
var Track = class {
|
|
3
|
+
name;
|
|
4
|
+
constructor(name) {
|
|
5
|
+
this.name = name;
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
function pointsEmpty(length, names) {
|
|
9
|
+
const pts = [];
|
|
10
|
+
for (let i = 0; i < length; i += 1) {
|
|
11
|
+
pts.push({
|
|
12
|
+
xy: [Number.NaN, Number.NaN],
|
|
13
|
+
visible: false,
|
|
14
|
+
complete: false,
|
|
15
|
+
name: names?.[i]
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return pts;
|
|
19
|
+
}
|
|
20
|
+
function predictedPointsEmpty(length, names) {
|
|
21
|
+
const pts = [];
|
|
22
|
+
for (let i = 0; i < length; i += 1) {
|
|
23
|
+
pts.push({
|
|
24
|
+
xy: [Number.NaN, Number.NaN],
|
|
25
|
+
visible: false,
|
|
26
|
+
complete: false,
|
|
27
|
+
score: Number.NaN,
|
|
28
|
+
name: names?.[i]
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return pts;
|
|
32
|
+
}
|
|
33
|
+
function pointsFromArray(array, names) {
|
|
34
|
+
const pts = [];
|
|
35
|
+
for (let i = 0; i < array.length; i += 1) {
|
|
36
|
+
const row = array[i] ?? [Number.NaN, Number.NaN];
|
|
37
|
+
const visible = row.length > 2 ? Boolean(row[2]) : !Number.isNaN(row[0]);
|
|
38
|
+
const complete = row.length > 3 ? Boolean(row[3]) : false;
|
|
39
|
+
pts.push({ xy: [row[0] ?? Number.NaN, row[1] ?? Number.NaN], visible, complete, name: names?.[i] });
|
|
40
|
+
}
|
|
41
|
+
return pts;
|
|
42
|
+
}
|
|
43
|
+
function predictedPointsFromArray(array, names) {
|
|
44
|
+
const pts = [];
|
|
45
|
+
for (let i = 0; i < array.length; i += 1) {
|
|
46
|
+
const row = array[i] ?? [Number.NaN, Number.NaN, Number.NaN];
|
|
47
|
+
const visible = row.length > 3 ? Boolean(row[3]) : !Number.isNaN(row[0]);
|
|
48
|
+
const complete = row.length > 4 ? Boolean(row[4]) : false;
|
|
49
|
+
pts.push({
|
|
50
|
+
xy: [row[0] ?? Number.NaN, row[1] ?? Number.NaN],
|
|
51
|
+
score: row[2] ?? Number.NaN,
|
|
52
|
+
visible,
|
|
53
|
+
complete,
|
|
54
|
+
name: names?.[i]
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return pts;
|
|
58
|
+
}
|
|
59
|
+
var Instance = class _Instance {
|
|
60
|
+
points;
|
|
61
|
+
skeleton;
|
|
62
|
+
track;
|
|
63
|
+
fromPredicted;
|
|
64
|
+
trackingScore;
|
|
65
|
+
constructor(options) {
|
|
66
|
+
this.skeleton = options.skeleton;
|
|
67
|
+
this.track = options.track ?? null;
|
|
68
|
+
this.fromPredicted = options.fromPredicted ?? null;
|
|
69
|
+
this.trackingScore = options.trackingScore ?? 0;
|
|
70
|
+
if (Array.isArray(options.points)) {
|
|
71
|
+
this.points = options.points;
|
|
72
|
+
} else {
|
|
73
|
+
this.points = pointsFromDict(options.points, options.skeleton);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
static fromArray(points, skeleton) {
|
|
77
|
+
return new _Instance({ points: pointsFromArray(points, skeleton.nodeNames), skeleton });
|
|
78
|
+
}
|
|
79
|
+
static fromNumpy(options) {
|
|
80
|
+
return new _Instance({
|
|
81
|
+
points: pointsFromArray(options.pointsData, options.skeleton.nodeNames),
|
|
82
|
+
skeleton: options.skeleton,
|
|
83
|
+
track: options.track ?? null,
|
|
84
|
+
fromPredicted: options.fromPredicted ?? null,
|
|
85
|
+
trackingScore: options.trackingScore
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
static empty(options) {
|
|
89
|
+
return new _Instance({ points: pointsEmpty(options.skeleton.nodeNames.length, options.skeleton.nodeNames), skeleton: options.skeleton });
|
|
90
|
+
}
|
|
91
|
+
get length() {
|
|
92
|
+
return this.points.length;
|
|
93
|
+
}
|
|
94
|
+
get nVisible() {
|
|
95
|
+
return this.points.filter((point) => point.visible).length;
|
|
96
|
+
}
|
|
97
|
+
getPoint(target) {
|
|
98
|
+
if (typeof target === "number") {
|
|
99
|
+
if (target < 0 || target >= this.points.length) throw new Error("Point index out of range.");
|
|
100
|
+
return this.points[target];
|
|
101
|
+
}
|
|
102
|
+
if (typeof target === "string") {
|
|
103
|
+
const index2 = this.skeleton.index(target);
|
|
104
|
+
return this.points[index2];
|
|
105
|
+
}
|
|
106
|
+
const index = this.skeleton.index(target.name);
|
|
107
|
+
return this.points[index];
|
|
108
|
+
}
|
|
109
|
+
numpy(options) {
|
|
110
|
+
const invisibleAsNaN = options?.invisibleAsNaN ?? true;
|
|
111
|
+
return this.points.map((point) => {
|
|
112
|
+
if (invisibleAsNaN && !point.visible) {
|
|
113
|
+
return [Number.NaN, Number.NaN];
|
|
114
|
+
}
|
|
115
|
+
return [point.xy[0], point.xy[1]];
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
toString() {
|
|
119
|
+
const trackName = this.track ? `"${this.track.name}"` : "None";
|
|
120
|
+
return `Instance(points=${JSON.stringify(this.numpy({ invisibleAsNaN: false }))}, track=${trackName})`;
|
|
121
|
+
}
|
|
122
|
+
get isEmpty() {
|
|
123
|
+
return this.points.every((point) => !point.visible || Number.isNaN(point.xy[0]));
|
|
124
|
+
}
|
|
125
|
+
overlapsWith(other, iouThreshold = 0.1) {
|
|
126
|
+
const boxA = this.boundingBox();
|
|
127
|
+
const boxB = other.boundingBox();
|
|
128
|
+
if (!boxA || !boxB) return false;
|
|
129
|
+
const iou = intersectionOverUnion(boxA, boxB);
|
|
130
|
+
return iou >= iouThreshold;
|
|
131
|
+
}
|
|
132
|
+
boundingBox() {
|
|
133
|
+
const xs = [];
|
|
134
|
+
const ys = [];
|
|
135
|
+
for (const point of this.points) {
|
|
136
|
+
if (Number.isNaN(point.xy[0]) || Number.isNaN(point.xy[1])) continue;
|
|
137
|
+
xs.push(point.xy[0]);
|
|
138
|
+
ys.push(point.xy[1]);
|
|
139
|
+
}
|
|
140
|
+
if (!xs.length || !ys.length) return null;
|
|
141
|
+
const minX = Math.min(...xs);
|
|
142
|
+
const maxX = Math.max(...xs);
|
|
143
|
+
const minY = Math.min(...ys);
|
|
144
|
+
const maxY = Math.max(...ys);
|
|
145
|
+
return [minX, minY, maxX, maxY];
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
var PredictedInstance = class _PredictedInstance extends Instance {
|
|
149
|
+
score;
|
|
150
|
+
constructor(options) {
|
|
151
|
+
const { score = 0, ...rest } = options;
|
|
152
|
+
const pts = Array.isArray(rest.points) ? rest.points : predictedPointsFromDict(rest.points, rest.skeleton);
|
|
153
|
+
super({
|
|
154
|
+
points: pts,
|
|
155
|
+
skeleton: rest.skeleton,
|
|
156
|
+
track: rest.track,
|
|
157
|
+
trackingScore: rest.trackingScore
|
|
158
|
+
});
|
|
159
|
+
this.score = score;
|
|
160
|
+
}
|
|
161
|
+
static fromArray(points, skeleton, score) {
|
|
162
|
+
return new _PredictedInstance({
|
|
163
|
+
points: predictedPointsFromArray(points, skeleton.nodeNames),
|
|
164
|
+
skeleton,
|
|
165
|
+
score
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
static fromNumpy(options) {
|
|
169
|
+
return new _PredictedInstance({
|
|
170
|
+
points: predictedPointsFromArray(options.pointsData, options.skeleton.nodeNames),
|
|
171
|
+
skeleton: options.skeleton,
|
|
172
|
+
track: options.track ?? null,
|
|
173
|
+
score: options.score,
|
|
174
|
+
trackingScore: options.trackingScore
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
static empty(options) {
|
|
178
|
+
return new _PredictedInstance({ points: predictedPointsEmpty(options.skeleton.nodeNames.length, options.skeleton.nodeNames), skeleton: options.skeleton });
|
|
179
|
+
}
|
|
180
|
+
numpy(options) {
|
|
181
|
+
const invisibleAsNaN = options?.invisibleAsNaN ?? true;
|
|
182
|
+
return this.points.map((point) => {
|
|
183
|
+
const xy = invisibleAsNaN && !point.visible ? [Number.NaN, Number.NaN] : [point.xy[0], point.xy[1]];
|
|
184
|
+
if (options?.scores) {
|
|
185
|
+
return [xy[0], xy[1], point.score ?? 0];
|
|
186
|
+
}
|
|
187
|
+
return xy;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
toString() {
|
|
191
|
+
const trackName = this.track ? `"${this.track.name}"` : "None";
|
|
192
|
+
return `PredictedInstance(points=${JSON.stringify(this.numpy({ invisibleAsNaN: false }))}, track=${trackName}, score=${this.score.toFixed(2)}, tracking_score=${this.trackingScore ?? "None"})`;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
function pointsFromDict(pointsDict, skeleton) {
|
|
196
|
+
const points = pointsEmpty(skeleton.nodeNames.length, skeleton.nodeNames);
|
|
197
|
+
for (const [nodeName, data] of Object.entries(pointsDict)) {
|
|
198
|
+
const index = skeleton.index(nodeName);
|
|
199
|
+
points[index] = {
|
|
200
|
+
xy: [data[0] ?? Number.NaN, data[1] ?? Number.NaN],
|
|
201
|
+
visible: data.length > 2 ? Boolean(data[2]) : !Number.isNaN(data[0]),
|
|
202
|
+
complete: data.length > 3 ? Boolean(data[3]) : false,
|
|
203
|
+
name: nodeName
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return points;
|
|
207
|
+
}
|
|
208
|
+
function predictedPointsFromDict(pointsDict, skeleton) {
|
|
209
|
+
const points = predictedPointsEmpty(skeleton.nodeNames.length, skeleton.nodeNames);
|
|
210
|
+
for (const [nodeName, data] of Object.entries(pointsDict)) {
|
|
211
|
+
const index = skeleton.index(nodeName);
|
|
212
|
+
points[index] = {
|
|
213
|
+
xy: [data[0] ?? Number.NaN, data[1] ?? Number.NaN],
|
|
214
|
+
score: data[2] ?? Number.NaN,
|
|
215
|
+
visible: data.length > 3 ? Boolean(data[3]) : !Number.isNaN(data[0]),
|
|
216
|
+
complete: data.length > 4 ? Boolean(data[4]) : false,
|
|
217
|
+
name: nodeName
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return points;
|
|
221
|
+
}
|
|
222
|
+
function intersectionOverUnion(boxA, boxB) {
|
|
223
|
+
const [ax1, ay1, ax2, ay2] = boxA;
|
|
224
|
+
const [bx1, by1, bx2, by2] = boxB;
|
|
225
|
+
const interX1 = Math.max(ax1, bx1);
|
|
226
|
+
const interY1 = Math.max(ay1, by1);
|
|
227
|
+
const interX2 = Math.min(ax2, bx2);
|
|
228
|
+
const interY2 = Math.min(ay2, by2);
|
|
229
|
+
const interArea = Math.max(0, interX2 - interX1) * Math.max(0, interY2 - interY1);
|
|
230
|
+
const areaA = Math.max(0, ax2 - ax1) * Math.max(0, ay2 - ay1);
|
|
231
|
+
const areaB = Math.max(0, bx2 - bx1) * Math.max(0, by2 - by1);
|
|
232
|
+
const union = areaA + areaB - interArea;
|
|
233
|
+
if (union === 0) return 0;
|
|
234
|
+
return interArea / union;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/model/labeled-frame.ts
|
|
238
|
+
var LabeledFrame = class {
|
|
239
|
+
video;
|
|
240
|
+
frameIdx;
|
|
241
|
+
instances;
|
|
242
|
+
constructor(options) {
|
|
243
|
+
this.video = options.video;
|
|
244
|
+
this.frameIdx = options.frameIdx;
|
|
245
|
+
this.instances = options.instances ?? [];
|
|
246
|
+
}
|
|
247
|
+
get length() {
|
|
248
|
+
return this.instances.length;
|
|
249
|
+
}
|
|
250
|
+
[Symbol.iterator]() {
|
|
251
|
+
return this.instances[Symbol.iterator]();
|
|
252
|
+
}
|
|
253
|
+
at(index) {
|
|
254
|
+
return this.instances[index];
|
|
255
|
+
}
|
|
256
|
+
get userInstances() {
|
|
257
|
+
return this.instances.filter((inst) => inst instanceof Instance);
|
|
258
|
+
}
|
|
259
|
+
get predictedInstances() {
|
|
260
|
+
return this.instances.filter((inst) => inst instanceof PredictedInstance);
|
|
261
|
+
}
|
|
262
|
+
get hasUserInstances() {
|
|
263
|
+
return this.userInstances.length > 0;
|
|
264
|
+
}
|
|
265
|
+
get hasPredictedInstances() {
|
|
266
|
+
return this.predictedInstances.length > 0;
|
|
267
|
+
}
|
|
268
|
+
numpy() {
|
|
269
|
+
return this.instances.map((inst) => inst.numpy());
|
|
270
|
+
}
|
|
271
|
+
get image() {
|
|
272
|
+
return this.video.getFrame(this.frameIdx);
|
|
273
|
+
}
|
|
274
|
+
get unusedPredictions() {
|
|
275
|
+
const usedPredicted = /* @__PURE__ */ new Set();
|
|
276
|
+
for (const inst of this.instances) {
|
|
277
|
+
if (inst instanceof Instance && inst.fromPredicted) {
|
|
278
|
+
usedPredicted.add(inst.fromPredicted);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const tracks = this.instances.map((inst) => inst.track).filter((track) => track !== null && track !== void 0);
|
|
282
|
+
if (tracks.length) {
|
|
283
|
+
const usedTracks = new Set(tracks);
|
|
284
|
+
return this.predictedInstances.filter((inst) => !inst.track || !usedTracks.has(inst.track));
|
|
285
|
+
}
|
|
286
|
+
return this.predictedInstances.filter((inst) => !usedPredicted.has(inst));
|
|
287
|
+
}
|
|
288
|
+
removePredictions() {
|
|
289
|
+
this.instances = this.instances.filter((inst) => inst instanceof Instance);
|
|
290
|
+
}
|
|
291
|
+
removeEmptyInstances() {
|
|
292
|
+
this.instances = this.instances.filter((inst) => !inst.isEmpty);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// src/model/skeleton.ts
|
|
297
|
+
var Node = class {
|
|
298
|
+
name;
|
|
299
|
+
constructor(name) {
|
|
300
|
+
this.name = name;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
var Edge = class {
|
|
304
|
+
source;
|
|
305
|
+
destination;
|
|
306
|
+
constructor(source, destination) {
|
|
307
|
+
this.source = source;
|
|
308
|
+
this.destination = destination;
|
|
309
|
+
}
|
|
310
|
+
at(index) {
|
|
311
|
+
if (index === 0) return this.source;
|
|
312
|
+
if (index === 1) return this.destination;
|
|
313
|
+
throw new Error("Edge only has 2 nodes (source and destination).");
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
var Symmetry = class {
|
|
317
|
+
nodes;
|
|
318
|
+
constructor(nodes) {
|
|
319
|
+
const set = new Set(nodes);
|
|
320
|
+
if (set.size !== 2) {
|
|
321
|
+
throw new Error("Symmetry must contain exactly 2 nodes.");
|
|
322
|
+
}
|
|
323
|
+
this.nodes = set;
|
|
324
|
+
}
|
|
325
|
+
at(index) {
|
|
326
|
+
let i = 0;
|
|
327
|
+
for (const node of this.nodes) {
|
|
328
|
+
if (i === index) return node;
|
|
329
|
+
i += 1;
|
|
330
|
+
}
|
|
331
|
+
throw new Error("Symmetry index out of range.");
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
var Skeleton = class {
|
|
335
|
+
nodes;
|
|
336
|
+
edges;
|
|
337
|
+
symmetries;
|
|
338
|
+
name;
|
|
339
|
+
nameToNode;
|
|
340
|
+
nodeToIndex;
|
|
341
|
+
constructor(options) {
|
|
342
|
+
const resolved = Array.isArray(options) ? { nodes: options } : options;
|
|
343
|
+
this.nodes = resolved.nodes.map((node) => typeof node === "string" ? new Node(node) : node);
|
|
344
|
+
this.edges = [];
|
|
345
|
+
this.symmetries = [];
|
|
346
|
+
this.name = resolved.name;
|
|
347
|
+
this.nameToNode = /* @__PURE__ */ new Map();
|
|
348
|
+
this.nodeToIndex = /* @__PURE__ */ new Map();
|
|
349
|
+
this.rebuildCache();
|
|
350
|
+
if (resolved.edges) {
|
|
351
|
+
this.edges = resolved.edges.map((edge) => edge instanceof Edge ? edge : this.edgeFrom(edge));
|
|
352
|
+
}
|
|
353
|
+
if (resolved.symmetries) {
|
|
354
|
+
this.symmetries = resolved.symmetries.map(
|
|
355
|
+
(symmetry) => symmetry instanceof Symmetry ? symmetry : this.symmetryFrom(symmetry)
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
rebuildCache(nodes = this.nodes) {
|
|
360
|
+
this.nameToNode = new Map(nodes.map((node) => [node.name, node]));
|
|
361
|
+
this.nodeToIndex = new Map(nodes.map((node, index) => [node, index]));
|
|
362
|
+
}
|
|
363
|
+
get nodeNames() {
|
|
364
|
+
return this.nodes.map((node) => node.name);
|
|
365
|
+
}
|
|
366
|
+
index(node) {
|
|
367
|
+
if (typeof node === "number") return node;
|
|
368
|
+
if (typeof node === "string") {
|
|
369
|
+
const found = this.nameToNode.get(node);
|
|
370
|
+
if (!found) throw new Error(`Node '${node}' not found in skeleton.`);
|
|
371
|
+
return this.nodeToIndex.get(found) ?? -1;
|
|
372
|
+
}
|
|
373
|
+
const idx = this.nodeToIndex.get(node);
|
|
374
|
+
if (idx === void 0) throw new Error("Node not found in skeleton.");
|
|
375
|
+
return idx;
|
|
376
|
+
}
|
|
377
|
+
node(node) {
|
|
378
|
+
if (node instanceof Node) return node;
|
|
379
|
+
if (typeof node === "number") return this.nodes[node];
|
|
380
|
+
const found = this.nameToNode.get(node);
|
|
381
|
+
if (!found) throw new Error(`Node '${node}' not found in skeleton.`);
|
|
382
|
+
return found;
|
|
383
|
+
}
|
|
384
|
+
get edgeIndices() {
|
|
385
|
+
return this.edges.map((edge) => [this.index(edge.source), this.index(edge.destination)]);
|
|
386
|
+
}
|
|
387
|
+
get symmetryNames() {
|
|
388
|
+
return this.symmetries.map((symmetry) => {
|
|
389
|
+
const nodes = Array.from(symmetry.nodes).map((node) => node.name);
|
|
390
|
+
return [nodes[0], nodes[1]];
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
matches(other) {
|
|
394
|
+
if (this.nodeNames.length !== other.nodeNames.length) return false;
|
|
395
|
+
for (let i = 0; i < this.nodeNames.length; i += 1) {
|
|
396
|
+
if (this.nodeNames[i] !== other.nodeNames[i]) return false;
|
|
397
|
+
}
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
addEdge(source, destination) {
|
|
401
|
+
this.edges.push(new Edge(this.node(source), this.node(destination)));
|
|
402
|
+
}
|
|
403
|
+
addSymmetry(left, right) {
|
|
404
|
+
this.symmetries.push(new Symmetry([this.node(left), this.node(right)]));
|
|
405
|
+
}
|
|
406
|
+
edgeFrom(edge) {
|
|
407
|
+
const [source, destination] = edge;
|
|
408
|
+
return new Edge(this.node(source), this.node(destination));
|
|
409
|
+
}
|
|
410
|
+
symmetryFrom(symmetry) {
|
|
411
|
+
const [a, b] = symmetry;
|
|
412
|
+
return new Symmetry([this.node(a), this.node(b)]);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// src/model/video.ts
|
|
417
|
+
var Video = class {
|
|
418
|
+
filename;
|
|
419
|
+
backend;
|
|
420
|
+
backendMetadata;
|
|
421
|
+
sourceVideo;
|
|
422
|
+
openBackend;
|
|
423
|
+
constructor(options) {
|
|
424
|
+
this.filename = options.filename;
|
|
425
|
+
this.backend = options.backend ?? null;
|
|
426
|
+
this.backendMetadata = options.backendMetadata ?? {};
|
|
427
|
+
this.sourceVideo = options.sourceVideo ?? null;
|
|
428
|
+
this.openBackend = options.openBackend ?? true;
|
|
429
|
+
}
|
|
430
|
+
get originalVideo() {
|
|
431
|
+
if (!this.sourceVideo) return null;
|
|
432
|
+
let current = this.sourceVideo;
|
|
433
|
+
while (current.sourceVideo) {
|
|
434
|
+
current = current.sourceVideo;
|
|
435
|
+
}
|
|
436
|
+
return current;
|
|
437
|
+
}
|
|
438
|
+
get shape() {
|
|
439
|
+
return this.backend?.shape ?? this.backendMetadata.shape ?? null;
|
|
440
|
+
}
|
|
441
|
+
get fps() {
|
|
442
|
+
return this.backend?.fps ?? this.backendMetadata.fps ?? null;
|
|
443
|
+
}
|
|
444
|
+
async getFrame(frameIndex) {
|
|
445
|
+
if (!this.backend) return null;
|
|
446
|
+
return this.backend.getFrame(frameIndex);
|
|
447
|
+
}
|
|
448
|
+
async getFrameTimes() {
|
|
449
|
+
if (!this.backend?.getFrameTimes) return null;
|
|
450
|
+
return this.backend.getFrameTimes();
|
|
451
|
+
}
|
|
452
|
+
close() {
|
|
453
|
+
this.backend?.close();
|
|
454
|
+
}
|
|
455
|
+
matchesPath(other, strict = true) {
|
|
456
|
+
if (Array.isArray(this.filename) || Array.isArray(other.filename)) {
|
|
457
|
+
return JSON.stringify(this.filename) === JSON.stringify(other.filename);
|
|
458
|
+
}
|
|
459
|
+
if (strict) return this.filename === other.filename;
|
|
460
|
+
const basenameA = this.filename.split("/").pop();
|
|
461
|
+
const basenameB = other.filename.split("/").pop();
|
|
462
|
+
return basenameA === basenameB;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// src/model/suggestions.ts
|
|
467
|
+
var SuggestionFrame = class {
|
|
468
|
+
video;
|
|
469
|
+
frameIdx;
|
|
470
|
+
metadata;
|
|
471
|
+
constructor(options) {
|
|
472
|
+
this.video = options.video;
|
|
473
|
+
this.frameIdx = options.frameIdx;
|
|
474
|
+
this.metadata = options.metadata ?? {};
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// src/video/media-video.ts
|
|
479
|
+
var isBrowser = typeof window !== "undefined";
|
|
480
|
+
var MediaVideoBackend = class {
|
|
481
|
+
filename;
|
|
482
|
+
shape;
|
|
483
|
+
fps;
|
|
484
|
+
dataset;
|
|
485
|
+
video;
|
|
486
|
+
canvas;
|
|
487
|
+
ctx;
|
|
488
|
+
ready;
|
|
489
|
+
constructor(filename) {
|
|
490
|
+
if (!isBrowser) {
|
|
491
|
+
throw new Error("MediaVideoBackend requires a browser environment.");
|
|
492
|
+
}
|
|
493
|
+
this.filename = filename;
|
|
494
|
+
this.dataset = null;
|
|
495
|
+
this.video = document.createElement("video");
|
|
496
|
+
this.video.src = filename;
|
|
497
|
+
this.video.crossOrigin = "anonymous";
|
|
498
|
+
this.canvas = document.createElement("canvas");
|
|
499
|
+
this.ctx = this.canvas.getContext("2d");
|
|
500
|
+
this.ready = new Promise((resolve, reject) => {
|
|
501
|
+
this.video?.addEventListener("loadedmetadata", () => {
|
|
502
|
+
if (!this.video || !this.canvas) return;
|
|
503
|
+
this.canvas.width = this.video.videoWidth;
|
|
504
|
+
this.canvas.height = this.video.videoHeight;
|
|
505
|
+
this.fps = this.video.duration ? this.video.videoHeight ? void 0 : void 0 : void 0;
|
|
506
|
+
resolve();
|
|
507
|
+
});
|
|
508
|
+
this.video?.addEventListener("error", () => reject(new Error("Failed to load video")));
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
async getFrame(frameIndex) {
|
|
512
|
+
if (!this.video || !this.ctx || !this.canvas) return null;
|
|
513
|
+
await this.ready;
|
|
514
|
+
const duration = this.video.duration;
|
|
515
|
+
const frameCount = Math.floor(duration * (this.video?.playbackRate || 1) * 30) || 1;
|
|
516
|
+
const fps = duration ? frameCount / duration : 30;
|
|
517
|
+
const targetTime = frameIndex / fps;
|
|
518
|
+
await seekVideo(this.video, targetTime);
|
|
519
|
+
this.ctx.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
|
|
520
|
+
return this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
|
521
|
+
}
|
|
522
|
+
close() {
|
|
523
|
+
if (this.video) {
|
|
524
|
+
this.video.pause();
|
|
525
|
+
this.video.src = "";
|
|
526
|
+
}
|
|
527
|
+
this.video = null;
|
|
528
|
+
this.canvas = null;
|
|
529
|
+
this.ctx = null;
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
function seekVideo(video, time) {
|
|
533
|
+
return new Promise((resolve, reject) => {
|
|
534
|
+
const onSeeked = () => {
|
|
535
|
+
video.removeEventListener("seeked", onSeeked);
|
|
536
|
+
video.removeEventListener("error", onError);
|
|
537
|
+
resolve();
|
|
538
|
+
};
|
|
539
|
+
const onError = () => {
|
|
540
|
+
video.removeEventListener("seeked", onSeeked);
|
|
541
|
+
video.removeEventListener("error", onError);
|
|
542
|
+
reject(new Error("Video seek failed"));
|
|
543
|
+
};
|
|
544
|
+
video.addEventListener("seeked", onSeeked);
|
|
545
|
+
video.addEventListener("error", onError);
|
|
546
|
+
video.currentTime = Math.max(0, time);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/codecs/dictionary.ts
|
|
551
|
+
function toDict(labels, options) {
|
|
552
|
+
const videoFilter = resolveVideoFilter(labels, options?.video);
|
|
553
|
+
const videos = videoFilter ? [videoFilter.video] : labels.videos;
|
|
554
|
+
const tracks = collectTracks(labels, videoFilter?.video);
|
|
555
|
+
const trackIndex = new Map(tracks.map((track, idx) => [track, idx]));
|
|
556
|
+
const skeletons = labels.skeletons.map((skeleton) => {
|
|
557
|
+
const edges = skeleton.edges.map((edge) => [
|
|
558
|
+
skeleton.index(edge.source.name),
|
|
559
|
+
skeleton.index(edge.destination.name)
|
|
560
|
+
]);
|
|
561
|
+
const symmetries = skeleton.symmetries.map((sym) => {
|
|
562
|
+
const [left, right] = sym.nodes;
|
|
563
|
+
return [skeleton.index(left.name), skeleton.index(right.name)];
|
|
564
|
+
});
|
|
565
|
+
return {
|
|
566
|
+
name: skeleton.name ?? void 0,
|
|
567
|
+
nodes: skeleton.nodeNames,
|
|
568
|
+
edges,
|
|
569
|
+
symmetries
|
|
570
|
+
};
|
|
571
|
+
});
|
|
572
|
+
const labeledFrames = [];
|
|
573
|
+
for (const frame of labels.labeledFrames) {
|
|
574
|
+
if (videoFilter && !frame.video.matchesPath(videoFilter.video, true)) continue;
|
|
575
|
+
if (options?.skipEmptyFrames && frame.instances.length === 0) continue;
|
|
576
|
+
const videoIdx = videos.indexOf(frame.video);
|
|
577
|
+
if (videoIdx < 0) continue;
|
|
578
|
+
labeledFrames.push({
|
|
579
|
+
frame_idx: frame.frameIdx,
|
|
580
|
+
video_idx: videoIdx,
|
|
581
|
+
instances: frame.instances.map((instance) => instanceToDict(instance, labels, trackIndex))
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
const suggestions = labels.suggestions.filter((suggestion) => !videoFilter || suggestion.video.matchesPath(videoFilter.video, true)).map((suggestion) => ({
|
|
585
|
+
frame_idx: suggestion.frameIdx,
|
|
586
|
+
video_idx: videos.indexOf(suggestion.video),
|
|
587
|
+
...suggestion.metadata
|
|
588
|
+
}));
|
|
589
|
+
const videoDicts = videos.map((video) => {
|
|
590
|
+
const backendType = resolveBackendType(video);
|
|
591
|
+
const backend = backendType ? { type: backendType } : void 0;
|
|
592
|
+
const shape = video.shape ? Array.from(video.shape) : void 0;
|
|
593
|
+
const fps = video.fps ?? void 0;
|
|
594
|
+
return {
|
|
595
|
+
filename: video.filename,
|
|
596
|
+
shape,
|
|
597
|
+
fps,
|
|
598
|
+
backend
|
|
599
|
+
};
|
|
600
|
+
});
|
|
601
|
+
return {
|
|
602
|
+
version: "1.0.0",
|
|
603
|
+
skeletons,
|
|
604
|
+
videos: videoDicts,
|
|
605
|
+
tracks: tracks.map((track) => trackToDict(track)),
|
|
606
|
+
labeled_frames: labeledFrames,
|
|
607
|
+
suggestions,
|
|
608
|
+
provenance: labels.provenance ?? {}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function fromDict(data) {
|
|
612
|
+
validateDict(data);
|
|
613
|
+
const skeletons = data.skeletons.map((skeleton) => {
|
|
614
|
+
const nodes = skeleton.nodes.map((name) => new Node(name));
|
|
615
|
+
const edges = skeleton.edges.map(([sourceIdx, destIdx]) => new Edge(nodes[sourceIdx], nodes[destIdx]));
|
|
616
|
+
const symmetries = (skeleton.symmetries ?? []).map(
|
|
617
|
+
([leftIdx, rightIdx]) => new Symmetry([nodes[leftIdx], nodes[rightIdx]])
|
|
618
|
+
);
|
|
619
|
+
return new Skeleton({ name: skeleton.name, nodes, edges, symmetries });
|
|
620
|
+
});
|
|
621
|
+
const videos = data.videos.map((video) => new Video({ filename: video.filename }));
|
|
622
|
+
const tracks = data.tracks.map((track) => new Track(String(track.name ?? "")));
|
|
623
|
+
const labeledFrames = data.labeled_frames.map((frame) => {
|
|
624
|
+
const video = videos[frame.video_idx];
|
|
625
|
+
const instances = frame.instances.map((inst) => dictToInstance(inst, skeletons, tracks));
|
|
626
|
+
return new LabeledFrame({ video, frameIdx: frame.frame_idx, instances });
|
|
627
|
+
});
|
|
628
|
+
const suggestions = data.suggestions.map((suggestion) => {
|
|
629
|
+
const entry = suggestion;
|
|
630
|
+
const video = videos[entry.video_idx ?? 0];
|
|
631
|
+
return new SuggestionFrame({ video, frameIdx: entry.frame_idx ?? 0, metadata: entry });
|
|
632
|
+
});
|
|
633
|
+
return new Labels({
|
|
634
|
+
labeledFrames,
|
|
635
|
+
videos,
|
|
636
|
+
skeletons,
|
|
637
|
+
tracks,
|
|
638
|
+
suggestions,
|
|
639
|
+
provenance: data.provenance ?? {}
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
function resolveVideoFilter(labels, video) {
|
|
643
|
+
if (video === void 0) return null;
|
|
644
|
+
if (typeof video === "number") {
|
|
645
|
+
const entry = labels.videos[video];
|
|
646
|
+
if (!entry) throw new Error("Video index out of range.");
|
|
647
|
+
return { video: entry };
|
|
648
|
+
}
|
|
649
|
+
return { video };
|
|
650
|
+
}
|
|
651
|
+
function collectTracks(labels, video) {
|
|
652
|
+
const trackSet = /* @__PURE__ */ new Set();
|
|
653
|
+
for (const track of labels.tracks) {
|
|
654
|
+
trackSet.add(track);
|
|
655
|
+
}
|
|
656
|
+
for (const frame of labels.labeledFrames) {
|
|
657
|
+
if (video && !frame.video.matchesPath(video, true)) continue;
|
|
658
|
+
for (const instance of frame.instances) {
|
|
659
|
+
if (instance.track) trackSet.add(instance.track);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return Array.from(trackSet);
|
|
663
|
+
}
|
|
664
|
+
function instanceToDict(instance, labels, trackIndex) {
|
|
665
|
+
const skeletonIdx = labels.skeletons.indexOf(instance.skeleton);
|
|
666
|
+
const isPredicted = instance instanceof PredictedInstance;
|
|
667
|
+
const points = instance.points.map((point) => {
|
|
668
|
+
const payload2 = {
|
|
669
|
+
x: point.xy[0],
|
|
670
|
+
y: point.xy[1],
|
|
671
|
+
visible: point.visible,
|
|
672
|
+
complete: point.complete
|
|
673
|
+
};
|
|
674
|
+
if (isPredicted && "score" in point) {
|
|
675
|
+
payload2.score = point.score;
|
|
676
|
+
}
|
|
677
|
+
return payload2;
|
|
678
|
+
});
|
|
679
|
+
const payload = {
|
|
680
|
+
type: isPredicted ? "predicted_instance" : "instance",
|
|
681
|
+
skeleton_idx: skeletonIdx,
|
|
682
|
+
points
|
|
683
|
+
};
|
|
684
|
+
if (instance.track) {
|
|
685
|
+
payload.track_idx = trackIndex.get(instance.track);
|
|
686
|
+
}
|
|
687
|
+
if (isPredicted) {
|
|
688
|
+
payload.score = instance.score;
|
|
689
|
+
}
|
|
690
|
+
if (instance.trackingScore !== void 0) {
|
|
691
|
+
payload.tracking_score = instance.trackingScore;
|
|
692
|
+
}
|
|
693
|
+
if (!isPredicted && instance.fromPredicted) {
|
|
694
|
+
payload.has_from_predicted = true;
|
|
695
|
+
}
|
|
696
|
+
return payload;
|
|
697
|
+
}
|
|
698
|
+
function dictToInstance(data, skeletons, tracks) {
|
|
699
|
+
const type = data.type === "predicted_instance" ? "predicted" : "instance";
|
|
700
|
+
const skeleton = skeletons[data.skeleton_idx ?? 0] ?? skeletons[0];
|
|
701
|
+
const trackIdx = data.track_idx;
|
|
702
|
+
const track = trackIdx !== void 0 ? tracks[trackIdx] : void 0;
|
|
703
|
+
const points = Array.isArray(data.points) ? data.points : [];
|
|
704
|
+
if (type === "predicted") {
|
|
705
|
+
const pointRows2 = points.map((point) => [
|
|
706
|
+
Number(point.x),
|
|
707
|
+
Number(point.y),
|
|
708
|
+
Number(point.score ?? Number.NaN),
|
|
709
|
+
point.visible ? 1 : 0,
|
|
710
|
+
point.complete ? 1 : 0
|
|
711
|
+
]);
|
|
712
|
+
return new PredictedInstance({
|
|
713
|
+
points: predictedPointsFromArray(pointRows2, skeleton.nodeNames),
|
|
714
|
+
skeleton,
|
|
715
|
+
track,
|
|
716
|
+
score: Number(data.score ?? 0),
|
|
717
|
+
trackingScore: Number(data.tracking_score ?? 0)
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
const pointRows = points.map((point) => [
|
|
721
|
+
Number(point.x),
|
|
722
|
+
Number(point.y),
|
|
723
|
+
point.visible ? 1 : 0,
|
|
724
|
+
point.complete ? 1 : 0
|
|
725
|
+
]);
|
|
726
|
+
return new Instance({
|
|
727
|
+
points: pointsFromArray(pointRows, skeleton.nodeNames),
|
|
728
|
+
skeleton,
|
|
729
|
+
track,
|
|
730
|
+
trackingScore: Number(data.tracking_score ?? 0)
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
function resolveBackendType(video) {
|
|
734
|
+
if (!video.backend) return null;
|
|
735
|
+
if (video.backend instanceof MediaVideoBackend) return "MediaVideo";
|
|
736
|
+
return video.backend.constructor?.name ?? null;
|
|
737
|
+
}
|
|
738
|
+
function trackToDict(track) {
|
|
739
|
+
const payload = { name: track.name };
|
|
740
|
+
const spawnedOn = track.spawned_on;
|
|
741
|
+
if (spawnedOn !== void 0) {
|
|
742
|
+
payload.spawned_on = spawnedOn;
|
|
743
|
+
}
|
|
744
|
+
return payload;
|
|
745
|
+
}
|
|
746
|
+
function validateDict(data) {
|
|
747
|
+
const required = ["version", "skeletons", "videos", "tracks", "labeled_frames", "suggestions", "provenance"];
|
|
748
|
+
for (const key of required) {
|
|
749
|
+
if (!(key in data)) {
|
|
750
|
+
throw new Error(`Missing required key: ${key}`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/codecs/numpy.ts
|
|
756
|
+
function toNumpy(labels, options) {
|
|
757
|
+
return labels.numpy({ returnConfidence: options?.returnConfidence, video: options?.video });
|
|
758
|
+
}
|
|
759
|
+
function fromNumpy(data, options) {
|
|
760
|
+
if (data.length === 0 || data[0].length === void 0) {
|
|
761
|
+
throw new Error("Input array must have 4 dimensions.");
|
|
762
|
+
}
|
|
763
|
+
const video = options.video ?? options.videos?.[0];
|
|
764
|
+
if (!video) throw new Error("fromNumpy requires a video.");
|
|
765
|
+
if (options.video && options.videos) {
|
|
766
|
+
throw new Error("Cannot specify both video and videos.");
|
|
767
|
+
}
|
|
768
|
+
const skeleton = resolveSkeleton(options);
|
|
769
|
+
const labels = labelsFromNumpy(data, {
|
|
770
|
+
video,
|
|
771
|
+
skeleton,
|
|
772
|
+
trackNames: options.trackNames,
|
|
773
|
+
firstFrame: options.firstFrame,
|
|
774
|
+
returnConfidence: options.returnConfidence
|
|
775
|
+
});
|
|
776
|
+
return labels;
|
|
777
|
+
}
|
|
778
|
+
function labelsFromNumpy(data, options) {
|
|
779
|
+
const frameCount = data.length;
|
|
780
|
+
if (!frameCount || data[0].length === void 0) {
|
|
781
|
+
throw new Error("Input array must have 4 dimensions.");
|
|
782
|
+
}
|
|
783
|
+
const trackCount = data[0].length;
|
|
784
|
+
const nodeCount = data[0][0]?.length ?? 0;
|
|
785
|
+
if (!nodeCount) {
|
|
786
|
+
throw new Error("Input array must have node dimension.");
|
|
787
|
+
}
|
|
788
|
+
const trackNames = options.trackNames ?? Array.from({ length: trackCount }, (_, idx) => `track${idx}`);
|
|
789
|
+
const tracks = trackNames.map((name) => new Track(name));
|
|
790
|
+
const labeledFrames = [];
|
|
791
|
+
const startFrame = options.firstFrame ?? 0;
|
|
792
|
+
for (let frameIdx = 0; frameIdx < frameCount; frameIdx += 1) {
|
|
793
|
+
const instances = [];
|
|
794
|
+
for (let trackIdx = 0; trackIdx < trackCount; trackIdx += 1) {
|
|
795
|
+
const points = data[frameIdx][trackIdx];
|
|
796
|
+
if (!points) continue;
|
|
797
|
+
const hasData = points.some((point) => point.some((value) => !Number.isNaN(value)));
|
|
798
|
+
if (!hasData) continue;
|
|
799
|
+
const arrayPoints = points.map((point) => {
|
|
800
|
+
if (options.returnConfidence) {
|
|
801
|
+
return [point[0], point[1], point[2] ?? Number.NaN, 1, 0];
|
|
802
|
+
}
|
|
803
|
+
return [point[0], point[1], 1, 0];
|
|
804
|
+
});
|
|
805
|
+
const instance = new PredictedInstance({
|
|
806
|
+
points: predictedPointsFromArray(arrayPoints, options.skeleton.nodeNames),
|
|
807
|
+
skeleton: options.skeleton,
|
|
808
|
+
track: tracks[trackIdx]
|
|
809
|
+
});
|
|
810
|
+
instances.push(instance);
|
|
811
|
+
}
|
|
812
|
+
labeledFrames.push(new LabeledFrame({
|
|
813
|
+
video: options.video,
|
|
814
|
+
frameIdx: startFrame + frameIdx,
|
|
815
|
+
instances
|
|
816
|
+
}));
|
|
817
|
+
}
|
|
818
|
+
return new Labels({
|
|
819
|
+
labeledFrames,
|
|
820
|
+
videos: [options.video],
|
|
821
|
+
skeletons: [options.skeleton],
|
|
822
|
+
tracks
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
function resolveSkeleton(options) {
|
|
826
|
+
if (options.skeleton) return options.skeleton;
|
|
827
|
+
if (Array.isArray(options.skeletons) && options.skeletons.length) return options.skeletons[0];
|
|
828
|
+
if (options.skeletons && !Array.isArray(options.skeletons)) return options.skeletons;
|
|
829
|
+
throw new Error("fromNumpy requires a skeleton.");
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// src/model/labels.ts
|
|
833
|
+
var Labels = class {
|
|
834
|
+
labeledFrames;
|
|
835
|
+
videos;
|
|
836
|
+
skeletons;
|
|
837
|
+
tracks;
|
|
838
|
+
suggestions;
|
|
839
|
+
sessions;
|
|
840
|
+
provenance;
|
|
841
|
+
constructor(options) {
|
|
842
|
+
this.labeledFrames = options?.labeledFrames ?? [];
|
|
843
|
+
this.videos = options?.videos ?? [];
|
|
844
|
+
this.skeletons = options?.skeletons ?? [];
|
|
845
|
+
this.tracks = options?.tracks ?? [];
|
|
846
|
+
this.suggestions = options?.suggestions ?? [];
|
|
847
|
+
this.sessions = options?.sessions ?? [];
|
|
848
|
+
this.provenance = options?.provenance ?? {};
|
|
849
|
+
if (!this.videos.length && this.labeledFrames.length) {
|
|
850
|
+
const uniqueVideos = /* @__PURE__ */ new Map();
|
|
851
|
+
for (const frame of this.labeledFrames) {
|
|
852
|
+
uniqueVideos.set(frame.video, frame.video);
|
|
853
|
+
}
|
|
854
|
+
this.videos = Array.from(uniqueVideos.values());
|
|
855
|
+
}
|
|
856
|
+
if (!this.skeletons.length && this.labeledFrames.length) {
|
|
857
|
+
const uniqueSkeletons = /* @__PURE__ */ new Map();
|
|
858
|
+
for (const frame of this.labeledFrames) {
|
|
859
|
+
for (const instance of frame.instances) {
|
|
860
|
+
uniqueSkeletons.set(instance.skeleton, instance.skeleton);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
this.skeletons = Array.from(uniqueSkeletons.values());
|
|
864
|
+
}
|
|
865
|
+
if (!this.tracks.length && this.labeledFrames.length) {
|
|
866
|
+
const uniqueTracks = /* @__PURE__ */ new Map();
|
|
867
|
+
for (const frame of this.labeledFrames) {
|
|
868
|
+
for (const instance of frame.instances) {
|
|
869
|
+
if (instance.track) uniqueTracks.set(instance.track, instance.track);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
this.tracks = Array.from(uniqueTracks.values());
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
get video() {
|
|
876
|
+
if (!this.videos.length) {
|
|
877
|
+
throw new Error("No videos available on Labels.");
|
|
878
|
+
}
|
|
879
|
+
return this.videos[0];
|
|
880
|
+
}
|
|
881
|
+
get length() {
|
|
882
|
+
return this.labeledFrames.length;
|
|
883
|
+
}
|
|
884
|
+
[Symbol.iterator]() {
|
|
885
|
+
return this.labeledFrames[Symbol.iterator]();
|
|
886
|
+
}
|
|
887
|
+
get instances() {
|
|
888
|
+
return this.labeledFrames.flatMap((frame) => frame.instances);
|
|
889
|
+
}
|
|
890
|
+
find(options) {
|
|
891
|
+
return this.labeledFrames.filter((frame) => {
|
|
892
|
+
if (options.video && frame.video !== options.video && !frame.video.matchesPath(options.video, false)) {
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
if (options.frameIdx !== void 0 && frame.frameIdx !== options.frameIdx) {
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
return true;
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
append(frame) {
|
|
902
|
+
this.labeledFrames.push(frame);
|
|
903
|
+
if (!this.videos.includes(frame.video)) {
|
|
904
|
+
this.videos.push(frame.video);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
toDict(options) {
|
|
908
|
+
return toDict(this, options);
|
|
909
|
+
}
|
|
910
|
+
static fromNumpy(data, options) {
|
|
911
|
+
const video = options.video ?? options.videos?.[0];
|
|
912
|
+
if (!video) throw new Error("fromNumpy requires a video.");
|
|
913
|
+
if (options.video && options.videos) {
|
|
914
|
+
throw new Error("Cannot specify both video and videos.");
|
|
915
|
+
}
|
|
916
|
+
const skeletons = Array.isArray(options.skeletons) ? options.skeletons : options.skeletons ? [options.skeletons] : options.skeleton ? [options.skeleton] : [];
|
|
917
|
+
if (!skeletons.length) throw new Error("fromNumpy requires a skeleton.");
|
|
918
|
+
return labelsFromNumpy(data, {
|
|
919
|
+
video,
|
|
920
|
+
skeleton: skeletons[0],
|
|
921
|
+
trackNames: options.trackNames,
|
|
922
|
+
firstFrame: options.firstFrame,
|
|
923
|
+
returnConfidence: options.returnConfidence
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
numpy(options) {
|
|
927
|
+
const targetVideo = options?.video ?? this.video;
|
|
928
|
+
const frames = this.labeledFrames.filter((frame) => frame.video.matchesPath(targetVideo, true));
|
|
929
|
+
if (!frames.length) return [];
|
|
930
|
+
const maxFrame = Math.max(...frames.map((frame) => frame.frameIdx));
|
|
931
|
+
const tracks = this.tracks.length ? this.tracks.length : Math.max(1, ...frames.map((frame) => frame.instances.length));
|
|
932
|
+
const nodes = this.skeletons[0]?.nodes.length ?? 0;
|
|
933
|
+
const channelCount = options?.returnConfidence ? 3 : 2;
|
|
934
|
+
const videoArray = Array.from(
|
|
935
|
+
{ length: maxFrame + 1 },
|
|
936
|
+
() => Array.from(
|
|
937
|
+
{ length: tracks },
|
|
938
|
+
() => Array.from({ length: nodes }, () => Array.from({ length: channelCount }, () => Number.NaN))
|
|
939
|
+
)
|
|
940
|
+
);
|
|
941
|
+
for (const frame of frames) {
|
|
942
|
+
const frameSlot = videoArray[frame.frameIdx];
|
|
943
|
+
if (!frameSlot) continue;
|
|
944
|
+
frame.instances.forEach((inst, idx) => {
|
|
945
|
+
const trackIndex = inst.track ? this.tracks.indexOf(inst.track) : idx;
|
|
946
|
+
const resolvedTrack = trackIndex >= 0 ? trackIndex : idx;
|
|
947
|
+
const trackSlot = frameSlot[resolvedTrack];
|
|
948
|
+
if (!trackSlot) return;
|
|
949
|
+
inst.points.forEach((point, nodeIdx) => {
|
|
950
|
+
if (!trackSlot[nodeIdx]) return;
|
|
951
|
+
const row = [point.xy[0], point.xy[1]];
|
|
952
|
+
if (options?.returnConfidence) {
|
|
953
|
+
const score = "score" in point ? point.score : Number.NaN;
|
|
954
|
+
row.push(score);
|
|
955
|
+
}
|
|
956
|
+
trackSlot[nodeIdx] = row;
|
|
957
|
+
});
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
return videoArray;
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
// src/model/labels-set.ts
|
|
965
|
+
var LabelsSet = class {
|
|
966
|
+
labels;
|
|
967
|
+
constructor(entries) {
|
|
968
|
+
this.labels = new Map(Object.entries(entries ?? {}));
|
|
969
|
+
}
|
|
970
|
+
get size() {
|
|
971
|
+
return this.labels.size;
|
|
972
|
+
}
|
|
973
|
+
get(key) {
|
|
974
|
+
return this.labels.get(key);
|
|
975
|
+
}
|
|
976
|
+
set(key, value) {
|
|
977
|
+
this.labels.set(key, value);
|
|
978
|
+
}
|
|
979
|
+
delete(key) {
|
|
980
|
+
this.labels.delete(key);
|
|
981
|
+
}
|
|
982
|
+
keys() {
|
|
983
|
+
return this.labels.keys();
|
|
984
|
+
}
|
|
985
|
+
values() {
|
|
986
|
+
return this.labels.values();
|
|
987
|
+
}
|
|
988
|
+
entries() {
|
|
989
|
+
return this.labels.entries();
|
|
990
|
+
}
|
|
991
|
+
[Symbol.iterator]() {
|
|
992
|
+
return this.labels.entries();
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
// src/model/camera.ts
|
|
997
|
+
function rodriguesTransformation(input) {
|
|
998
|
+
if (input.length === 3 && Array.isArray(input[0]) === false) {
|
|
999
|
+
const rvec = input;
|
|
1000
|
+
const theta2 = Math.hypot(rvec[0], rvec[1], rvec[2]);
|
|
1001
|
+
if (theta2 === 0) {
|
|
1002
|
+
return { matrix: [[1, 0, 0], [0, 1, 0], [0, 0, 1]], vector: rvec };
|
|
1003
|
+
}
|
|
1004
|
+
const axis = rvec.map((v) => v / theta2);
|
|
1005
|
+
const [x, y, z] = axis;
|
|
1006
|
+
const cos = Math.cos(theta2);
|
|
1007
|
+
const sin = Math.sin(theta2);
|
|
1008
|
+
const K = [
|
|
1009
|
+
[0, -z, y],
|
|
1010
|
+
[z, 0, -x],
|
|
1011
|
+
[-y, x, 0]
|
|
1012
|
+
];
|
|
1013
|
+
const I = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
|
|
1014
|
+
const KK = multiply3x3(K, K);
|
|
1015
|
+
const matrix2 = add3x3(add3x3(I, scale3x3(K, sin)), scale3x3(KK, 1 - cos));
|
|
1016
|
+
return { matrix: matrix2, vector: rvec };
|
|
1017
|
+
}
|
|
1018
|
+
const matrix = input;
|
|
1019
|
+
const trace = matrix[0][0] + matrix[1][1] + matrix[2][2];
|
|
1020
|
+
const cosTheta = Math.min(1, Math.max(-1, (trace - 1) / 2));
|
|
1021
|
+
const theta = Math.acos(cosTheta);
|
|
1022
|
+
if (theta === 0) {
|
|
1023
|
+
return { matrix, vector: [0, 0, 0] };
|
|
1024
|
+
}
|
|
1025
|
+
const rx = (matrix[2][1] - matrix[1][2]) / (2 * Math.sin(theta));
|
|
1026
|
+
const ry = (matrix[0][2] - matrix[2][0]) / (2 * Math.sin(theta));
|
|
1027
|
+
const rz = (matrix[1][0] - matrix[0][1]) / (2 * Math.sin(theta));
|
|
1028
|
+
return { matrix, vector: [rx * theta, ry * theta, rz * theta] };
|
|
1029
|
+
}
|
|
1030
|
+
function multiply3x3(a, b) {
|
|
1031
|
+
const result = Array.from({ length: 3 }, () => [0, 0, 0]);
|
|
1032
|
+
for (let i = 0; i < 3; i += 1) {
|
|
1033
|
+
for (let j = 0; j < 3; j += 1) {
|
|
1034
|
+
result[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return result;
|
|
1038
|
+
}
|
|
1039
|
+
function add3x3(a, b) {
|
|
1040
|
+
return a.map((row, i) => row.map((val, j) => val + b[i][j]));
|
|
1041
|
+
}
|
|
1042
|
+
function scale3x3(a, scale) {
|
|
1043
|
+
return a.map((row) => row.map((val) => val * scale));
|
|
1044
|
+
}
|
|
1045
|
+
var Camera = class {
|
|
1046
|
+
name;
|
|
1047
|
+
rvec;
|
|
1048
|
+
tvec;
|
|
1049
|
+
matrix;
|
|
1050
|
+
distortions;
|
|
1051
|
+
constructor(options) {
|
|
1052
|
+
this.name = options.name;
|
|
1053
|
+
this.rvec = options.rvec;
|
|
1054
|
+
this.tvec = options.tvec;
|
|
1055
|
+
this.matrix = options.matrix;
|
|
1056
|
+
this.distortions = options.distortions;
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
var CameraGroup = class {
|
|
1060
|
+
cameras;
|
|
1061
|
+
metadata;
|
|
1062
|
+
constructor(options) {
|
|
1063
|
+
this.cameras = options?.cameras ?? [];
|
|
1064
|
+
this.metadata = options?.metadata ?? {};
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
var InstanceGroup = class {
|
|
1068
|
+
instanceByCamera;
|
|
1069
|
+
score;
|
|
1070
|
+
points;
|
|
1071
|
+
metadata;
|
|
1072
|
+
constructor(options) {
|
|
1073
|
+
this.instanceByCamera = options.instanceByCamera instanceof Map ? options.instanceByCamera : /* @__PURE__ */ new Map();
|
|
1074
|
+
if (!(options.instanceByCamera instanceof Map)) {
|
|
1075
|
+
for (const [key, value] of Object.entries(options.instanceByCamera)) {
|
|
1076
|
+
const camera = key;
|
|
1077
|
+
this.instanceByCamera.set(camera, value);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
this.score = options.score;
|
|
1081
|
+
this.points = options.points;
|
|
1082
|
+
this.metadata = options.metadata ?? {};
|
|
1083
|
+
}
|
|
1084
|
+
get instances() {
|
|
1085
|
+
return Array.from(this.instanceByCamera.values());
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
var FrameGroup = class {
|
|
1089
|
+
frameIdx;
|
|
1090
|
+
instanceGroups;
|
|
1091
|
+
labeledFrameByCamera;
|
|
1092
|
+
metadata;
|
|
1093
|
+
constructor(options) {
|
|
1094
|
+
this.frameIdx = options.frameIdx;
|
|
1095
|
+
this.instanceGroups = options.instanceGroups;
|
|
1096
|
+
this.labeledFrameByCamera = options.labeledFrameByCamera instanceof Map ? options.labeledFrameByCamera : /* @__PURE__ */ new Map();
|
|
1097
|
+
if (!(options.labeledFrameByCamera instanceof Map)) {
|
|
1098
|
+
for (const [key, value] of Object.entries(options.labeledFrameByCamera)) {
|
|
1099
|
+
const camera = key;
|
|
1100
|
+
this.labeledFrameByCamera.set(camera, value);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
this.metadata = options.metadata ?? {};
|
|
1104
|
+
}
|
|
1105
|
+
get cameras() {
|
|
1106
|
+
return Array.from(this.labeledFrameByCamera.keys());
|
|
1107
|
+
}
|
|
1108
|
+
get labeledFrames() {
|
|
1109
|
+
return Array.from(this.labeledFrameByCamera.values());
|
|
1110
|
+
}
|
|
1111
|
+
getFrame(camera) {
|
|
1112
|
+
return this.labeledFrameByCamera.get(camera);
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
var RecordingSession = class {
|
|
1116
|
+
cameraGroup;
|
|
1117
|
+
frameGroupByFrameIdx;
|
|
1118
|
+
videoByCamera;
|
|
1119
|
+
cameraByVideo;
|
|
1120
|
+
metadata;
|
|
1121
|
+
constructor(options) {
|
|
1122
|
+
this.cameraGroup = options?.cameraGroup ?? new CameraGroup();
|
|
1123
|
+
this.frameGroupByFrameIdx = options?.frameGroupByFrameIdx ?? /* @__PURE__ */ new Map();
|
|
1124
|
+
this.videoByCamera = options?.videoByCamera ?? /* @__PURE__ */ new Map();
|
|
1125
|
+
this.cameraByVideo = options?.cameraByVideo ?? /* @__PURE__ */ new Map();
|
|
1126
|
+
this.metadata = options?.metadata ?? {};
|
|
1127
|
+
}
|
|
1128
|
+
get frameGroups() {
|
|
1129
|
+
return this.frameGroupByFrameIdx;
|
|
1130
|
+
}
|
|
1131
|
+
get videos() {
|
|
1132
|
+
return Array.from(this.videoByCamera.values());
|
|
1133
|
+
}
|
|
1134
|
+
get cameras() {
|
|
1135
|
+
return Array.from(this.videoByCamera.keys());
|
|
1136
|
+
}
|
|
1137
|
+
addVideo(video, camera) {
|
|
1138
|
+
if (!this.cameraGroup.cameras.includes(camera)) {
|
|
1139
|
+
this.cameraGroup.cameras.push(camera);
|
|
1140
|
+
}
|
|
1141
|
+
this.videoByCamera.set(camera, video);
|
|
1142
|
+
this.cameraByVideo.set(video, camera);
|
|
1143
|
+
}
|
|
1144
|
+
getCamera(video) {
|
|
1145
|
+
return this.cameraByVideo.get(video);
|
|
1146
|
+
}
|
|
1147
|
+
getVideo(camera) {
|
|
1148
|
+
return this.videoByCamera.get(camera);
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
function makeCameraFromDict(data) {
|
|
1152
|
+
return new Camera({
|
|
1153
|
+
name: data.name,
|
|
1154
|
+
rvec: data.rotation ?? [0, 0, 0],
|
|
1155
|
+
tvec: data.translation ?? [0, 0, 0],
|
|
1156
|
+
matrix: data.matrix,
|
|
1157
|
+
distortions: data.distortions
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// src/video/mp4box-video.ts
|
|
1162
|
+
var isBrowser2 = typeof window !== "undefined" && typeof document !== "undefined";
|
|
1163
|
+
var hasWebCodecs = isBrowser2 && typeof window.VideoDecoder !== "undefined" && typeof window.EncodedVideoChunk !== "undefined";
|
|
1164
|
+
var MP4BOX_CDN = "https://unpkg.com/mp4box@0.5.4/dist/mp4box.all.min.js";
|
|
1165
|
+
async function loadMp4box() {
|
|
1166
|
+
const globalMp4box = globalThis.MP4Box;
|
|
1167
|
+
if (globalMp4box) return globalMp4box;
|
|
1168
|
+
try {
|
|
1169
|
+
const module = await import("mp4box");
|
|
1170
|
+
return module.default ?? module;
|
|
1171
|
+
} catch {
|
|
1172
|
+
if (!isBrowser2 || typeof document === "undefined") {
|
|
1173
|
+
throw new Error("Failed to load mp4box");
|
|
1174
|
+
}
|
|
1175
|
+
await new Promise((resolve, reject) => {
|
|
1176
|
+
const script = document.createElement("script");
|
|
1177
|
+
script.src = MP4BOX_CDN;
|
|
1178
|
+
script.async = true;
|
|
1179
|
+
script.onload = () => resolve();
|
|
1180
|
+
script.onerror = () => reject(new Error("Failed to load mp4box"));
|
|
1181
|
+
document.head.appendChild(script);
|
|
1182
|
+
});
|
|
1183
|
+
const afterLoad = globalThis.MP4Box;
|
|
1184
|
+
if (afterLoad) return afterLoad;
|
|
1185
|
+
throw new Error("Failed to load mp4box");
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
var DEFAULT_CACHE_SIZE = 120;
|
|
1189
|
+
var DEFAULT_LOOKAHEAD = 60;
|
|
1190
|
+
var PARSE_CHUNK_SIZE = 1024 * 1024;
|
|
1191
|
+
var Mp4BoxVideoBackend = class {
|
|
1192
|
+
filename;
|
|
1193
|
+
shape;
|
|
1194
|
+
fps;
|
|
1195
|
+
dataset;
|
|
1196
|
+
ready;
|
|
1197
|
+
mp4box;
|
|
1198
|
+
mp4boxFile;
|
|
1199
|
+
videoTrack;
|
|
1200
|
+
samples;
|
|
1201
|
+
keyframeIndices;
|
|
1202
|
+
cache;
|
|
1203
|
+
cacheSize;
|
|
1204
|
+
lookahead;
|
|
1205
|
+
decoder;
|
|
1206
|
+
config;
|
|
1207
|
+
fileSize;
|
|
1208
|
+
supportsRangeRequests;
|
|
1209
|
+
fileBlob;
|
|
1210
|
+
isDecoding;
|
|
1211
|
+
pendingFrame;
|
|
1212
|
+
constructor(filename, options) {
|
|
1213
|
+
if (!hasWebCodecs) {
|
|
1214
|
+
throw new Error("Mp4BoxVideoBackend requires WebCodecs support.");
|
|
1215
|
+
}
|
|
1216
|
+
if (!isBrowser2) {
|
|
1217
|
+
throw new Error("Mp4BoxVideoBackend requires a browser environment.");
|
|
1218
|
+
}
|
|
1219
|
+
this.filename = filename;
|
|
1220
|
+
this.dataset = null;
|
|
1221
|
+
this.samples = [];
|
|
1222
|
+
this.keyframeIndices = [];
|
|
1223
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
1224
|
+
this.cacheSize = options?.cacheSize ?? DEFAULT_CACHE_SIZE;
|
|
1225
|
+
this.lookahead = options?.lookahead ?? DEFAULT_LOOKAHEAD;
|
|
1226
|
+
this.decoder = null;
|
|
1227
|
+
this.config = null;
|
|
1228
|
+
this.fileSize = 0;
|
|
1229
|
+
this.supportsRangeRequests = false;
|
|
1230
|
+
this.fileBlob = null;
|
|
1231
|
+
this.isDecoding = false;
|
|
1232
|
+
this.pendingFrame = null;
|
|
1233
|
+
this.ready = this.init();
|
|
1234
|
+
}
|
|
1235
|
+
async getFrame(frameIndex) {
|
|
1236
|
+
await this.ready;
|
|
1237
|
+
if (frameIndex < 0 || frameIndex >= this.samples.length) return null;
|
|
1238
|
+
if (this.cache.has(frameIndex)) {
|
|
1239
|
+
const bitmap = this.cache.get(frameIndex) ?? null;
|
|
1240
|
+
if (bitmap) {
|
|
1241
|
+
this.cache.delete(frameIndex);
|
|
1242
|
+
this.cache.set(frameIndex, bitmap);
|
|
1243
|
+
}
|
|
1244
|
+
return bitmap;
|
|
1245
|
+
}
|
|
1246
|
+
if (this.isDecoding) {
|
|
1247
|
+
this.pendingFrame = frameIndex;
|
|
1248
|
+
await new Promise((resolve) => {
|
|
1249
|
+
const check = () => this.isDecoding ? setTimeout(check, 10) : resolve(null);
|
|
1250
|
+
check();
|
|
1251
|
+
});
|
|
1252
|
+
if (this.cache.has(frameIndex)) {
|
|
1253
|
+
return this.cache.get(frameIndex) ?? null;
|
|
1254
|
+
}
|
|
1255
|
+
if (this.pendingFrame !== null && this.pendingFrame !== frameIndex) {
|
|
1256
|
+
return null;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
const keyframe = this.findKeyframeBefore(frameIndex);
|
|
1260
|
+
const end = Math.min(frameIndex + this.lookahead, this.samples.length - 1);
|
|
1261
|
+
await this.decodeRange(keyframe, end, frameIndex);
|
|
1262
|
+
return this.cache.get(frameIndex) ?? null;
|
|
1263
|
+
}
|
|
1264
|
+
async getFrameTimes() {
|
|
1265
|
+
await this.ready;
|
|
1266
|
+
return this.samples.map((sample) => sample.timestamp / 1e6);
|
|
1267
|
+
}
|
|
1268
|
+
close() {
|
|
1269
|
+
if (this.decoder) {
|
|
1270
|
+
try {
|
|
1271
|
+
this.decoder.close();
|
|
1272
|
+
} catch {
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
this.decoder = null;
|
|
1276
|
+
this.cache.forEach((bitmap) => bitmap.close());
|
|
1277
|
+
this.cache.clear();
|
|
1278
|
+
this.fileBlob = null;
|
|
1279
|
+
}
|
|
1280
|
+
async init() {
|
|
1281
|
+
await this.openSource();
|
|
1282
|
+
this.mp4box = await loadMp4box();
|
|
1283
|
+
this.mp4boxFile = this.mp4box.createFile();
|
|
1284
|
+
const ready = new Promise((resolve, reject) => {
|
|
1285
|
+
this.mp4boxFile.onError = reject;
|
|
1286
|
+
this.mp4boxFile.onReady = resolve;
|
|
1287
|
+
});
|
|
1288
|
+
let offset = 0;
|
|
1289
|
+
let resolved = false;
|
|
1290
|
+
ready.then(() => {
|
|
1291
|
+
resolved = true;
|
|
1292
|
+
});
|
|
1293
|
+
while (offset < this.fileSize && !resolved) {
|
|
1294
|
+
const buffer = await this.readChunk(offset, PARSE_CHUNK_SIZE);
|
|
1295
|
+
buffer.fileStart = offset;
|
|
1296
|
+
const next = this.mp4boxFile.appendBuffer(buffer);
|
|
1297
|
+
offset = next === void 0 ? offset + buffer.byteLength : next;
|
|
1298
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1299
|
+
}
|
|
1300
|
+
const info = await ready;
|
|
1301
|
+
if (!info.videoTracks.length) throw new Error("No video tracks found");
|
|
1302
|
+
this.videoTrack = info.videoTracks[0];
|
|
1303
|
+
const trak = this.mp4boxFile.getTrackById(this.videoTrack.id);
|
|
1304
|
+
const description = this.getCodecDescription(trak);
|
|
1305
|
+
const codec = this.videoTrack.codec.startsWith("vp08") ? "vp8" : this.videoTrack.codec;
|
|
1306
|
+
this.config = {
|
|
1307
|
+
codec,
|
|
1308
|
+
codedWidth: this.videoTrack.video.width,
|
|
1309
|
+
codedHeight: this.videoTrack.video.height,
|
|
1310
|
+
description
|
|
1311
|
+
};
|
|
1312
|
+
const support = await VideoDecoder.isConfigSupported(this.config);
|
|
1313
|
+
if (!support.supported) {
|
|
1314
|
+
throw new Error(`Codec ${codec} not supported`);
|
|
1315
|
+
}
|
|
1316
|
+
this.extractSamples();
|
|
1317
|
+
const duration = this.videoTrack.duration / this.videoTrack.timescale;
|
|
1318
|
+
this.fps = duration ? this.samples.length / duration : void 0;
|
|
1319
|
+
const frameCount = this.samples.length;
|
|
1320
|
+
const height = this.videoTrack.video.height;
|
|
1321
|
+
const width = this.videoTrack.video.width;
|
|
1322
|
+
this.shape = [frameCount, height, width, 3];
|
|
1323
|
+
}
|
|
1324
|
+
async openSource() {
|
|
1325
|
+
if (typeof this.filename !== "string") {
|
|
1326
|
+
throw new Error("Mp4BoxVideoBackend requires a single filename string.");
|
|
1327
|
+
}
|
|
1328
|
+
const response = await fetch(this.filename, { method: "HEAD" });
|
|
1329
|
+
if (!response.ok) throw new Error(`Failed to fetch video: ${response.status}`);
|
|
1330
|
+
const size = response.headers.get("Content-Length");
|
|
1331
|
+
this.fileSize = size ? Number.parseInt(size, 10) : 0;
|
|
1332
|
+
if (this.fileSize > 0) {
|
|
1333
|
+
try {
|
|
1334
|
+
const rangeTest = await fetch(this.filename, { method: "GET", headers: { Range: "bytes=0-0" } });
|
|
1335
|
+
this.supportsRangeRequests = rangeTest.status === 206;
|
|
1336
|
+
} catch {
|
|
1337
|
+
this.supportsRangeRequests = false;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
if (!this.supportsRangeRequests || !this.fileSize) {
|
|
1341
|
+
const full = await fetch(this.filename);
|
|
1342
|
+
const blob = await full.blob();
|
|
1343
|
+
this.fileBlob = blob;
|
|
1344
|
+
this.fileSize = blob.size;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
async readChunk(offset, size) {
|
|
1348
|
+
const end = Math.min(offset + size, this.fileSize);
|
|
1349
|
+
if (this.supportsRangeRequests) {
|
|
1350
|
+
const response = await fetch(this.filename, { headers: { Range: `bytes=${offset}-${end - 1}` } });
|
|
1351
|
+
return await response.arrayBuffer();
|
|
1352
|
+
}
|
|
1353
|
+
if (this.fileBlob) {
|
|
1354
|
+
return await this.fileBlob.slice(offset, end).arrayBuffer();
|
|
1355
|
+
}
|
|
1356
|
+
throw new Error("No video source available");
|
|
1357
|
+
}
|
|
1358
|
+
extractSamples() {
|
|
1359
|
+
const info = this.mp4boxFile.getTrackSamplesInfo(this.videoTrack.id);
|
|
1360
|
+
if (!info?.length) throw new Error("No samples");
|
|
1361
|
+
const ts = this.videoTrack.timescale;
|
|
1362
|
+
const samples = info.map((sample, index) => ({
|
|
1363
|
+
offset: sample.offset,
|
|
1364
|
+
size: sample.size,
|
|
1365
|
+
timestamp: sample.cts * 1e6 / ts,
|
|
1366
|
+
duration: sample.duration * 1e6 / ts,
|
|
1367
|
+
isKeyframe: sample.is_sync,
|
|
1368
|
+
cts: sample.cts,
|
|
1369
|
+
decodeIndex: index
|
|
1370
|
+
}));
|
|
1371
|
+
this.samples = samples.sort((a, b) => {
|
|
1372
|
+
if (a.cts === b.cts) return a.decodeIndex - b.decodeIndex;
|
|
1373
|
+
return a.cts - b.cts;
|
|
1374
|
+
});
|
|
1375
|
+
this.keyframeIndices = [];
|
|
1376
|
+
this.samples.forEach((sample, index) => {
|
|
1377
|
+
if (sample.isKeyframe) this.keyframeIndices.push(index);
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
findKeyframeBefore(frameIndex) {
|
|
1381
|
+
let result = 0;
|
|
1382
|
+
for (const keyframe of this.keyframeIndices) {
|
|
1383
|
+
if (keyframe <= frameIndex) result = keyframe;
|
|
1384
|
+
else break;
|
|
1385
|
+
}
|
|
1386
|
+
return result;
|
|
1387
|
+
}
|
|
1388
|
+
getCodecDescription(trak) {
|
|
1389
|
+
const entries = trak?.mdia?.minf?.stbl?.stsd?.entries ?? [];
|
|
1390
|
+
const dataStream = globalThis.DataStream ?? this.mp4box?.DataStream;
|
|
1391
|
+
if (!dataStream) return void 0;
|
|
1392
|
+
for (const entry of entries) {
|
|
1393
|
+
const box = entry.avcC || entry.hvcC || entry.vpcC || entry.av1C;
|
|
1394
|
+
if (!box) continue;
|
|
1395
|
+
const stream = new dataStream(void 0, 0, dataStream.BIG_ENDIAN);
|
|
1396
|
+
box.write(stream);
|
|
1397
|
+
return new Uint8Array(stream.buffer, 8);
|
|
1398
|
+
}
|
|
1399
|
+
return void 0;
|
|
1400
|
+
}
|
|
1401
|
+
async readSampleDataByDecodeOrder(samplesToFeed) {
|
|
1402
|
+
const results = /* @__PURE__ */ new Map();
|
|
1403
|
+
let i = 0;
|
|
1404
|
+
while (i < samplesToFeed.length) {
|
|
1405
|
+
const first = samplesToFeed[i];
|
|
1406
|
+
let regionEnd = i;
|
|
1407
|
+
let regionBytes = first.sample.size;
|
|
1408
|
+
while (regionEnd < samplesToFeed.length - 1) {
|
|
1409
|
+
const current = samplesToFeed[regionEnd];
|
|
1410
|
+
const next = samplesToFeed[regionEnd + 1];
|
|
1411
|
+
if (next.sample.offset === current.sample.offset + current.sample.size) {
|
|
1412
|
+
regionEnd += 1;
|
|
1413
|
+
regionBytes += next.sample.size;
|
|
1414
|
+
} else {
|
|
1415
|
+
break;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
const buffer = await this.readChunk(first.sample.offset, regionBytes);
|
|
1419
|
+
const bufferView = new Uint8Array(buffer);
|
|
1420
|
+
let bufferOffset = 0;
|
|
1421
|
+
for (let j = i; j <= regionEnd; j += 1) {
|
|
1422
|
+
const { sample } = samplesToFeed[j];
|
|
1423
|
+
results.set(sample.decodeIndex, bufferView.slice(bufferOffset, bufferOffset + sample.size));
|
|
1424
|
+
bufferOffset += sample.size;
|
|
1425
|
+
}
|
|
1426
|
+
i = regionEnd + 1;
|
|
1427
|
+
}
|
|
1428
|
+
return results;
|
|
1429
|
+
}
|
|
1430
|
+
async decodeRange(start, end, target) {
|
|
1431
|
+
if (!this.config) throw new Error("Decoder not configured");
|
|
1432
|
+
this.isDecoding = true;
|
|
1433
|
+
try {
|
|
1434
|
+
if (this.decoder) {
|
|
1435
|
+
try {
|
|
1436
|
+
this.decoder.close();
|
|
1437
|
+
} catch {
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
let minDecodeIndex = Infinity;
|
|
1441
|
+
let maxDecodeIndex = -Infinity;
|
|
1442
|
+
for (let i = start; i <= end; i += 1) {
|
|
1443
|
+
minDecodeIndex = Math.min(minDecodeIndex, this.samples[i].decodeIndex);
|
|
1444
|
+
maxDecodeIndex = Math.max(maxDecodeIndex, this.samples[i].decodeIndex);
|
|
1445
|
+
}
|
|
1446
|
+
const toFeed = [];
|
|
1447
|
+
for (let i = 0; i < this.samples.length; i += 1) {
|
|
1448
|
+
const sample = this.samples[i];
|
|
1449
|
+
if (sample.decodeIndex >= minDecodeIndex && sample.decodeIndex <= maxDecodeIndex) {
|
|
1450
|
+
toFeed.push({ pi: i, sample });
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
toFeed.sort((a, b) => a.sample.decodeIndex - b.sample.decodeIndex);
|
|
1454
|
+
const dataMap = await this.readSampleDataByDecodeOrder(toFeed);
|
|
1455
|
+
const timestampMap = /* @__PURE__ */ new Map();
|
|
1456
|
+
for (const { pi, sample } of toFeed) {
|
|
1457
|
+
timestampMap.set(Math.round(sample.timestamp), pi);
|
|
1458
|
+
}
|
|
1459
|
+
const halfCache = Math.floor(this.cacheSize / 2);
|
|
1460
|
+
const cacheStart = Math.max(start, target - halfCache);
|
|
1461
|
+
const cacheEnd = Math.min(end, target + halfCache);
|
|
1462
|
+
let decodedCount = 0;
|
|
1463
|
+
let resolveComplete;
|
|
1464
|
+
let rejectComplete;
|
|
1465
|
+
const completionPromise = new Promise((resolve, reject) => {
|
|
1466
|
+
resolveComplete = resolve;
|
|
1467
|
+
rejectComplete = reject;
|
|
1468
|
+
});
|
|
1469
|
+
this.decoder = new VideoDecoder({
|
|
1470
|
+
output: (frame) => {
|
|
1471
|
+
const roundedTimestamp = Math.round(frame.timestamp);
|
|
1472
|
+
let frameIndex = timestampMap.get(roundedTimestamp);
|
|
1473
|
+
if (frameIndex === void 0) {
|
|
1474
|
+
let bestDiff = Infinity;
|
|
1475
|
+
for (const [ts, idx] of timestampMap) {
|
|
1476
|
+
const diff = Math.abs(ts - frame.timestamp);
|
|
1477
|
+
if (diff < bestDiff) {
|
|
1478
|
+
bestDiff = diff;
|
|
1479
|
+
frameIndex = idx;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
const handleClose = () => {
|
|
1484
|
+
frame.close();
|
|
1485
|
+
decodedCount += 1;
|
|
1486
|
+
if (decodedCount >= toFeed.length) resolveComplete();
|
|
1487
|
+
};
|
|
1488
|
+
if (frameIndex !== void 0 && frameIndex >= cacheStart && frameIndex <= cacheEnd) {
|
|
1489
|
+
createImageBitmap(frame).then((bitmap) => {
|
|
1490
|
+
this.addToCache(frameIndex, bitmap);
|
|
1491
|
+
handleClose();
|
|
1492
|
+
}).catch(handleClose);
|
|
1493
|
+
} else {
|
|
1494
|
+
handleClose();
|
|
1495
|
+
}
|
|
1496
|
+
},
|
|
1497
|
+
error: (error) => {
|
|
1498
|
+
if (error.name === "AbortError") {
|
|
1499
|
+
resolveComplete();
|
|
1500
|
+
} else {
|
|
1501
|
+
rejectComplete(error);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
this.decoder.configure(this.config);
|
|
1506
|
+
const BATCH_SIZE = 15;
|
|
1507
|
+
for (let i = 0; i < toFeed.length; i += BATCH_SIZE) {
|
|
1508
|
+
const batch = toFeed.slice(i, i + BATCH_SIZE);
|
|
1509
|
+
for (const { sample } of batch) {
|
|
1510
|
+
const data = dataMap.get(sample.decodeIndex);
|
|
1511
|
+
if (!data) continue;
|
|
1512
|
+
this.decoder.decode(
|
|
1513
|
+
new EncodedVideoChunk({
|
|
1514
|
+
type: sample.isKeyframe ? "key" : "delta",
|
|
1515
|
+
timestamp: sample.timestamp,
|
|
1516
|
+
duration: sample.duration,
|
|
1517
|
+
data
|
|
1518
|
+
})
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
if (i + BATCH_SIZE < toFeed.length) {
|
|
1522
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
await this.decoder.flush();
|
|
1526
|
+
await completionPromise;
|
|
1527
|
+
} finally {
|
|
1528
|
+
this.isDecoding = false;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
addToCache(frameIndex, bitmap) {
|
|
1532
|
+
if (this.cache.size >= this.cacheSize) {
|
|
1533
|
+
const first = this.cache.keys().next();
|
|
1534
|
+
if (!first.done) {
|
|
1535
|
+
const evicted = this.cache.get(first.value);
|
|
1536
|
+
if (evicted) evicted.close();
|
|
1537
|
+
this.cache.delete(first.value);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
this.cache.set(frameIndex, bitmap);
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
|
|
1544
|
+
// src/codecs/slp/h5.ts
|
|
1545
|
+
var isNode = typeof process !== "undefined" && !!process.versions?.node;
|
|
1546
|
+
var modulePromise = null;
|
|
1547
|
+
async function getH5Module() {
|
|
1548
|
+
if (!modulePromise) {
|
|
1549
|
+
modulePromise = (async () => {
|
|
1550
|
+
const module = isNode ? await import("h5wasm/node") : await import("h5wasm");
|
|
1551
|
+
await module.ready;
|
|
1552
|
+
return module;
|
|
1553
|
+
})();
|
|
1554
|
+
}
|
|
1555
|
+
return modulePromise;
|
|
1556
|
+
}
|
|
1557
|
+
async function openH5File(source, options) {
|
|
1558
|
+
const module = await getH5Module();
|
|
1559
|
+
if (isNode) {
|
|
1560
|
+
return openH5FileNode(module, source);
|
|
1561
|
+
}
|
|
1562
|
+
return openH5FileBrowser(module, source, options);
|
|
1563
|
+
}
|
|
1564
|
+
function isProbablyUrl(value) {
|
|
1565
|
+
return /^https?:\/\//i.test(value);
|
|
1566
|
+
}
|
|
1567
|
+
function isFileHandle(value) {
|
|
1568
|
+
return typeof value === "object" && value !== null && "getFile" in value;
|
|
1569
|
+
}
|
|
1570
|
+
async function openH5FileNode(module, source) {
|
|
1571
|
+
if (typeof source === "string") {
|
|
1572
|
+
const file = new module.File(source, "r");
|
|
1573
|
+
return { file, close: () => file.close() };
|
|
1574
|
+
}
|
|
1575
|
+
if (source instanceof Uint8Array || source instanceof ArrayBuffer) {
|
|
1576
|
+
const { writeFileSync, unlinkSync } = await import("fs");
|
|
1577
|
+
const { tmpdir } = await import("os");
|
|
1578
|
+
const { join } = await import("path");
|
|
1579
|
+
const data = source instanceof Uint8Array ? source : new Uint8Array(source);
|
|
1580
|
+
const tempPath = join(tmpdir(), `sleap-io-${Date.now()}-${Math.random().toString(16).slice(2)}.slp`);
|
|
1581
|
+
writeFileSync(tempPath, data);
|
|
1582
|
+
const file = new module.File(tempPath, "r");
|
|
1583
|
+
return {
|
|
1584
|
+
file,
|
|
1585
|
+
close: () => {
|
|
1586
|
+
file.close();
|
|
1587
|
+
unlinkSync(tempPath);
|
|
1588
|
+
}
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
throw new Error("Node environments only support string paths or byte buffers for SLP inputs.");
|
|
1592
|
+
}
|
|
1593
|
+
async function openH5FileBrowser(module, source, options) {
|
|
1594
|
+
const fs = getH5FileSystem(module);
|
|
1595
|
+
if (typeof source === "string" && isProbablyUrl(source)) {
|
|
1596
|
+
return openFromUrl(module, fs, source, options);
|
|
1597
|
+
}
|
|
1598
|
+
if (isFileHandle(source)) {
|
|
1599
|
+
const file = await source.getFile();
|
|
1600
|
+
return openFromFile(module, fs, file, options);
|
|
1601
|
+
}
|
|
1602
|
+
if (typeof File !== "undefined" && source instanceof File) {
|
|
1603
|
+
return openFromFile(module, fs, source, options);
|
|
1604
|
+
}
|
|
1605
|
+
if (source instanceof Uint8Array || source instanceof ArrayBuffer) {
|
|
1606
|
+
const data = source instanceof Uint8Array ? source : new Uint8Array(source);
|
|
1607
|
+
const filename = "/tmp-slp.slp";
|
|
1608
|
+
fs.writeFile(filename, data);
|
|
1609
|
+
const file = new module.File(filename, "r");
|
|
1610
|
+
return { file, close: () => file.close() };
|
|
1611
|
+
}
|
|
1612
|
+
if (typeof source === "string") {
|
|
1613
|
+
return openFromUrl(module, fs, source, options);
|
|
1614
|
+
}
|
|
1615
|
+
throw new Error("Unsupported SLP source type for browser environment.");
|
|
1616
|
+
}
|
|
1617
|
+
async function openFromUrl(module, fs, url, options) {
|
|
1618
|
+
const filename = options?.filenameHint ?? url.split("/").pop()?.split("?")[0] ?? "slp-data.slp";
|
|
1619
|
+
const streamMode = options?.stream ?? "auto";
|
|
1620
|
+
if (fs.createLazyFile && (streamMode === "auto" || streamMode === "range")) {
|
|
1621
|
+
const mountPath = `/slp-remote-${Date.now()}`;
|
|
1622
|
+
fs.mkdir?.(mountPath);
|
|
1623
|
+
try {
|
|
1624
|
+
fs.createLazyFile(mountPath, filename, url, true, false);
|
|
1625
|
+
const file2 = new module.File(`${mountPath}/${filename}`, "r");
|
|
1626
|
+
return {
|
|
1627
|
+
file: file2,
|
|
1628
|
+
close: () => {
|
|
1629
|
+
file2.close();
|
|
1630
|
+
fs.unlink?.(`${mountPath}/${filename}`);
|
|
1631
|
+
fs.rmdir?.(mountPath);
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
} catch {
|
|
1635
|
+
fs.rmdir?.(mountPath);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
const response = await fetch(url);
|
|
1639
|
+
if (!response.ok) {
|
|
1640
|
+
throw new Error(`Failed to fetch SLP file: ${response.status} ${response.statusText}`);
|
|
1641
|
+
}
|
|
1642
|
+
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
1643
|
+
const localPath = "/tmp-slp.slp";
|
|
1644
|
+
fs.writeFile(localPath, buffer);
|
|
1645
|
+
const file = new module.File(localPath, "r");
|
|
1646
|
+
return { file, close: () => file.close() };
|
|
1647
|
+
}
|
|
1648
|
+
async function openFromFile(module, fs, file, options) {
|
|
1649
|
+
const mountPath = `/slp-local-${Date.now()}`;
|
|
1650
|
+
fs.mkdir?.(mountPath);
|
|
1651
|
+
const filename = options?.filenameHint ?? file.name ?? "local.slp";
|
|
1652
|
+
if (fs.mount && fs.filesystems && fs.filesystems.WORKERFS) {
|
|
1653
|
+
fs.mount(fs.filesystems.WORKERFS, { files: [file] }, mountPath);
|
|
1654
|
+
const filePath = `${mountPath}/${filename}`;
|
|
1655
|
+
const h5file2 = new module.File(filePath, "r");
|
|
1656
|
+
return {
|
|
1657
|
+
file: h5file2,
|
|
1658
|
+
close: () => {
|
|
1659
|
+
h5file2.close();
|
|
1660
|
+
fs.unmount?.(mountPath);
|
|
1661
|
+
fs.rmdir?.(mountPath);
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
1666
|
+
const localPath = "/tmp-slp.slp";
|
|
1667
|
+
fs.writeFile(localPath, buffer);
|
|
1668
|
+
const h5file = new module.File(localPath, "r");
|
|
1669
|
+
return { file: h5file, close: () => h5file.close() };
|
|
1670
|
+
}
|
|
1671
|
+
function getH5FileSystem(module) {
|
|
1672
|
+
const fs = module.FS;
|
|
1673
|
+
if (!fs) {
|
|
1674
|
+
throw new Error("h5wasm FS is not available.");
|
|
1675
|
+
}
|
|
1676
|
+
return fs;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// src/video/hdf5-video.ts
|
|
1680
|
+
var isBrowser3 = typeof window !== "undefined" && typeof document !== "undefined";
|
|
1681
|
+
var Hdf5VideoBackend = class {
|
|
1682
|
+
filename;
|
|
1683
|
+
dataset;
|
|
1684
|
+
shape;
|
|
1685
|
+
fps;
|
|
1686
|
+
file;
|
|
1687
|
+
datasetPath;
|
|
1688
|
+
frameNumbers;
|
|
1689
|
+
format;
|
|
1690
|
+
channelOrder;
|
|
1691
|
+
cachedData;
|
|
1692
|
+
constructor(options) {
|
|
1693
|
+
this.filename = options.filename;
|
|
1694
|
+
this.file = options.file;
|
|
1695
|
+
this.datasetPath = options.datasetPath;
|
|
1696
|
+
this.dataset = options.datasetPath;
|
|
1697
|
+
this.frameNumbers = options.frameNumbers ?? [];
|
|
1698
|
+
this.format = options.format ?? "png";
|
|
1699
|
+
this.channelOrder = options.channelOrder ?? "RGB";
|
|
1700
|
+
this.shape = options.shape;
|
|
1701
|
+
this.fps = options.fps;
|
|
1702
|
+
this.cachedData = null;
|
|
1703
|
+
}
|
|
1704
|
+
async getFrame(frameIndex) {
|
|
1705
|
+
const dataset = this.file.get(this.datasetPath);
|
|
1706
|
+
if (!dataset) return null;
|
|
1707
|
+
const index = this.frameNumbers.length ? this.frameNumbers.indexOf(frameIndex) : frameIndex;
|
|
1708
|
+
if (index < 0) return null;
|
|
1709
|
+
if (!this.cachedData) {
|
|
1710
|
+
this.cachedData = dataset.value;
|
|
1711
|
+
}
|
|
1712
|
+
const entry = this.cachedData[index];
|
|
1713
|
+
if (entry == null) return null;
|
|
1714
|
+
const rawBytes = toUint8Array(entry);
|
|
1715
|
+
if (!rawBytes) return null;
|
|
1716
|
+
if (isEncodedFormat(this.format)) {
|
|
1717
|
+
const decoded = await decodeImageBytes(rawBytes, this.format);
|
|
1718
|
+
return decoded ?? rawBytes;
|
|
1719
|
+
}
|
|
1720
|
+
const image = decodeRawFrame(rawBytes, this.shape, this.channelOrder);
|
|
1721
|
+
return image ?? rawBytes;
|
|
1722
|
+
}
|
|
1723
|
+
close() {
|
|
1724
|
+
this.cachedData = null;
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1727
|
+
function toUint8Array(entry) {
|
|
1728
|
+
if (entry instanceof Uint8Array) return entry;
|
|
1729
|
+
if (entry instanceof ArrayBuffer) return new Uint8Array(entry);
|
|
1730
|
+
if (ArrayBuffer.isView(entry)) return new Uint8Array(entry.buffer, entry.byteOffset, entry.byteLength);
|
|
1731
|
+
if (Array.isArray(entry)) return new Uint8Array(entry.flat());
|
|
1732
|
+
if (entry?.buffer) return new Uint8Array(entry.buffer);
|
|
1733
|
+
return null;
|
|
1734
|
+
}
|
|
1735
|
+
function isEncodedFormat(format) {
|
|
1736
|
+
const normalized = format.toLowerCase();
|
|
1737
|
+
return normalized === "png" || normalized === "jpg" || normalized === "jpeg";
|
|
1738
|
+
}
|
|
1739
|
+
async function decodeImageBytes(bytes, format) {
|
|
1740
|
+
if (!isBrowser3 || typeof createImageBitmap === "undefined") return null;
|
|
1741
|
+
const mime = format.toLowerCase() === "png" ? "image/png" : "image/jpeg";
|
|
1742
|
+
const safeBytes = new Uint8Array(bytes);
|
|
1743
|
+
const blob = new Blob([safeBytes.buffer], { type: mime });
|
|
1744
|
+
return createImageBitmap(blob);
|
|
1745
|
+
}
|
|
1746
|
+
function decodeRawFrame(bytes, shape, channelOrder) {
|
|
1747
|
+
if (!isBrowser3 || !shape) return null;
|
|
1748
|
+
const [, height, width, channels] = shape;
|
|
1749
|
+
if (!height || !width || !channels) return null;
|
|
1750
|
+
const expectedLength = height * width * channels;
|
|
1751
|
+
if (bytes.length < expectedLength) return null;
|
|
1752
|
+
const rgba = new Uint8ClampedArray(width * height * 4);
|
|
1753
|
+
const useBgr = channelOrder.toUpperCase() === "BGR";
|
|
1754
|
+
for (let i = 0; i < width * height; i += 1) {
|
|
1755
|
+
const base = i * channels;
|
|
1756
|
+
const r = bytes[base + (useBgr ? 2 : 0)] ?? 0;
|
|
1757
|
+
const g = bytes[base + 1] ?? 0;
|
|
1758
|
+
const b = bytes[base + (useBgr ? 0 : 2)] ?? 0;
|
|
1759
|
+
const a = channels === 4 ? bytes[base + 3] ?? 255 : 255;
|
|
1760
|
+
const out = i * 4;
|
|
1761
|
+
rgba[out] = r;
|
|
1762
|
+
rgba[out + 1] = g;
|
|
1763
|
+
rgba[out + 2] = b;
|
|
1764
|
+
rgba[out + 3] = a;
|
|
1765
|
+
}
|
|
1766
|
+
return new ImageData(rgba, width, height);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// src/video/factory.ts
|
|
1770
|
+
async function createVideoBackend(filename, options) {
|
|
1771
|
+
if (options?.embedded || filename.endsWith(".slp") || filename.endsWith(".h5") || filename.endsWith(".hdf5")) {
|
|
1772
|
+
const { file } = await openH5File(filename);
|
|
1773
|
+
const datasetPath = options?.dataset ?? "";
|
|
1774
|
+
return new Hdf5VideoBackend({
|
|
1775
|
+
filename,
|
|
1776
|
+
file,
|
|
1777
|
+
datasetPath,
|
|
1778
|
+
frameNumbers: options?.frameNumbers,
|
|
1779
|
+
format: options?.format,
|
|
1780
|
+
channelOrder: options?.channelOrder,
|
|
1781
|
+
shape: options?.shape,
|
|
1782
|
+
fps: options?.fps
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
const supportsWebCodecs = typeof window !== "undefined" && typeof window.VideoDecoder !== "undefined" && typeof window.EncodedVideoChunk !== "undefined";
|
|
1786
|
+
const normalized = filename.split("?")[0]?.toLowerCase();
|
|
1787
|
+
if (supportsWebCodecs && normalized.endsWith(".mp4")) {
|
|
1788
|
+
return new Mp4BoxVideoBackend(filename);
|
|
1789
|
+
}
|
|
1790
|
+
return new MediaVideoBackend(filename);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// src/codecs/slp/read.ts
|
|
1794
|
+
var textDecoder = new TextDecoder();
|
|
1795
|
+
async function readSlp(source, options) {
|
|
1796
|
+
const { file, close } = await openH5File(source, options?.h5);
|
|
1797
|
+
try {
|
|
1798
|
+
const metadataGroup = file.get("metadata");
|
|
1799
|
+
if (!metadataGroup) {
|
|
1800
|
+
throw new Error("Missing /metadata group in SLP file");
|
|
1801
|
+
}
|
|
1802
|
+
const metadataAttrs = metadataGroup.attrs ?? {};
|
|
1803
|
+
const formatId = Number(metadataAttrs["format_id"]?.value ?? metadataAttrs["format_id"] ?? 1);
|
|
1804
|
+
const metadataJson = parseJsonAttr(metadataAttrs["json"]);
|
|
1805
|
+
const labelsPath = typeof source === "string" ? source : options?.h5?.filenameHint ?? "slp-data.slp";
|
|
1806
|
+
const skeletons = readSkeletons(metadataJson);
|
|
1807
|
+
const tracks = readTracks(file.get("tracks_json"));
|
|
1808
|
+
const videos = await readVideos(file.get("videos_json"), labelsPath, options?.openVideos ?? true, file);
|
|
1809
|
+
const suggestions = readSuggestions(file.get("suggestions_json"), videos);
|
|
1810
|
+
const framesData = normalizeStructDataset(file.get("frames"));
|
|
1811
|
+
const instancesData = normalizeStructDataset(file.get("instances"));
|
|
1812
|
+
const pointsData = normalizeStructDataset(file.get("points"));
|
|
1813
|
+
const predPointsData = normalizeStructDataset(file.get("pred_points"));
|
|
1814
|
+
const labeledFrames = buildLabeledFrames({
|
|
1815
|
+
framesData,
|
|
1816
|
+
instancesData,
|
|
1817
|
+
pointsData,
|
|
1818
|
+
predPointsData,
|
|
1819
|
+
skeletons,
|
|
1820
|
+
tracks,
|
|
1821
|
+
videos,
|
|
1822
|
+
formatId
|
|
1823
|
+
});
|
|
1824
|
+
const sessions = readSessions(file.get("sessions_json"), videos, skeletons, labeledFrames);
|
|
1825
|
+
return new Labels({
|
|
1826
|
+
labeledFrames,
|
|
1827
|
+
videos,
|
|
1828
|
+
skeletons,
|
|
1829
|
+
tracks,
|
|
1830
|
+
suggestions,
|
|
1831
|
+
sessions,
|
|
1832
|
+
provenance: metadataJson?.provenance ?? {}
|
|
1833
|
+
});
|
|
1834
|
+
} finally {
|
|
1835
|
+
close();
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
function parseJsonAttr(attr) {
|
|
1839
|
+
if (!attr) return null;
|
|
1840
|
+
const value = attr.value ?? attr;
|
|
1841
|
+
if (typeof value === "string") return JSON.parse(value);
|
|
1842
|
+
if (value instanceof Uint8Array) return JSON.parse(textDecoder.decode(value));
|
|
1843
|
+
if (value.buffer) return JSON.parse(textDecoder.decode(new Uint8Array(value.buffer)));
|
|
1844
|
+
return JSON.parse(String(value));
|
|
1845
|
+
}
|
|
1846
|
+
function readSkeletons(metadataJson) {
|
|
1847
|
+
if (!metadataJson) return [];
|
|
1848
|
+
const nodeNames = (metadataJson.nodes ?? []).map((node) => node.name ?? node);
|
|
1849
|
+
const skeletonEntries = metadataJson.skeletons ?? [];
|
|
1850
|
+
const skeletons = [];
|
|
1851
|
+
for (const entry of skeletonEntries) {
|
|
1852
|
+
const edges = [];
|
|
1853
|
+
const symmetries = [];
|
|
1854
|
+
const typeCache = /* @__PURE__ */ new Map();
|
|
1855
|
+
const typeState = { nextId: 1 };
|
|
1856
|
+
const skeletonNodeIds = (entry.nodes ?? []).map((node) => Number(node.id ?? node));
|
|
1857
|
+
const nodeOrder = skeletonNodeIds.length ? skeletonNodeIds : nodeNames.map((_, index) => index);
|
|
1858
|
+
const nodes = nodeOrder.map((nodeId) => nodeNames[nodeId]).filter((name) => name !== void 0).map((name) => new Node(name));
|
|
1859
|
+
const nodeIndexById = /* @__PURE__ */ new Map();
|
|
1860
|
+
nodeOrder.forEach((nodeId, index) => {
|
|
1861
|
+
nodeIndexById.set(Number(nodeId), index);
|
|
1862
|
+
});
|
|
1863
|
+
for (const link of entry.links ?? []) {
|
|
1864
|
+
const source = Number(link.source);
|
|
1865
|
+
const target = Number(link.target);
|
|
1866
|
+
const edgeType = resolveEdgeType(link.type, typeCache, typeState);
|
|
1867
|
+
if (edgeType === 2) {
|
|
1868
|
+
symmetries.push([source, target]);
|
|
1869
|
+
} else {
|
|
1870
|
+
edges.push([source, target]);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
const remapPair = (pair) => {
|
|
1874
|
+
const sourceIndex = nodeIndexById.get(pair[0]);
|
|
1875
|
+
const targetIndex = nodeIndexById.get(pair[1]);
|
|
1876
|
+
if (sourceIndex === void 0 || targetIndex === void 0) return null;
|
|
1877
|
+
return [sourceIndex, targetIndex];
|
|
1878
|
+
};
|
|
1879
|
+
const mappedEdges = edges.map(remapPair).filter((pair) => pair !== null);
|
|
1880
|
+
const seenSymmetries = /* @__PURE__ */ new Set();
|
|
1881
|
+
const mappedSymmetries = symmetries.map(remapPair).filter((pair) => pair !== null).filter(([a, b]) => {
|
|
1882
|
+
const key = a < b ? `${a}-${b}` : `${b}-${a}`;
|
|
1883
|
+
if (seenSymmetries.has(key)) return false;
|
|
1884
|
+
seenSymmetries.add(key);
|
|
1885
|
+
return true;
|
|
1886
|
+
});
|
|
1887
|
+
const skeleton = new Skeleton({
|
|
1888
|
+
nodes,
|
|
1889
|
+
edges: mappedEdges,
|
|
1890
|
+
symmetries: mappedSymmetries,
|
|
1891
|
+
name: entry.graph?.name ?? entry.name
|
|
1892
|
+
});
|
|
1893
|
+
skeletons.push(skeleton);
|
|
1894
|
+
}
|
|
1895
|
+
return skeletons;
|
|
1896
|
+
}
|
|
1897
|
+
function resolveEdgeType(edgeType, cache, state) {
|
|
1898
|
+
if (!edgeType) return 1;
|
|
1899
|
+
if (edgeType["py/reduce"]) {
|
|
1900
|
+
const typeId = edgeType["py/reduce"][1]?.["py/tuple"]?.[0] ?? 1;
|
|
1901
|
+
cache.set(state.nextId, typeId);
|
|
1902
|
+
state.nextId += 1;
|
|
1903
|
+
return typeId;
|
|
1904
|
+
}
|
|
1905
|
+
if (edgeType["py/tuple"]) {
|
|
1906
|
+
const typeId = edgeType["py/tuple"][0] ?? 1;
|
|
1907
|
+
cache.set(state.nextId, typeId);
|
|
1908
|
+
state.nextId += 1;
|
|
1909
|
+
return typeId;
|
|
1910
|
+
}
|
|
1911
|
+
if (edgeType["py/id"]) {
|
|
1912
|
+
const pyId = edgeType["py/id"];
|
|
1913
|
+
return cache.get(pyId) ?? pyId;
|
|
1914
|
+
}
|
|
1915
|
+
return 1;
|
|
1916
|
+
}
|
|
1917
|
+
function readTracks(dataset) {
|
|
1918
|
+
if (!dataset) return [];
|
|
1919
|
+
const values = dataset.value ?? [];
|
|
1920
|
+
const tracks = [];
|
|
1921
|
+
for (const entry of values) {
|
|
1922
|
+
let parsed = entry;
|
|
1923
|
+
if (typeof entry === "string") {
|
|
1924
|
+
try {
|
|
1925
|
+
parsed = JSON.parse(entry);
|
|
1926
|
+
} catch {
|
|
1927
|
+
parsed = entry;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
if (Array.isArray(parsed)) {
|
|
1931
|
+
tracks.push(new Track(String(parsed[1] ?? parsed[0])));
|
|
1932
|
+
} else if (parsed?.name) {
|
|
1933
|
+
tracks.push(new Track(String(parsed.name)));
|
|
1934
|
+
} else {
|
|
1935
|
+
tracks.push(new Track(String(parsed)));
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
return tracks;
|
|
1939
|
+
}
|
|
1940
|
+
async function readVideos(dataset, labelsPath, openVideos, file) {
|
|
1941
|
+
if (!dataset) return [];
|
|
1942
|
+
const values = dataset.value ?? [];
|
|
1943
|
+
const videos = [];
|
|
1944
|
+
for (const entry of values) {
|
|
1945
|
+
if (!entry) continue;
|
|
1946
|
+
const parsed = typeof entry === "string" ? JSON.parse(entry) : JSON.parse(textDecoder.decode(entry));
|
|
1947
|
+
const backendMeta = parsed.backend ?? {};
|
|
1948
|
+
let filename = backendMeta.filename ?? parsed.filename ?? "";
|
|
1949
|
+
let datasetPath = backendMeta.dataset ?? null;
|
|
1950
|
+
let embedded = false;
|
|
1951
|
+
if (filename === ".") {
|
|
1952
|
+
embedded = true;
|
|
1953
|
+
filename = labelsPath;
|
|
1954
|
+
}
|
|
1955
|
+
let backend = null;
|
|
1956
|
+
if (openVideos) {
|
|
1957
|
+
backend = await createVideoBackend(filename, {
|
|
1958
|
+
dataset: datasetPath ?? void 0,
|
|
1959
|
+
embedded,
|
|
1960
|
+
frameNumbers: readFrameNumbers(file, datasetPath),
|
|
1961
|
+
format: backendMeta.format,
|
|
1962
|
+
channelOrder: backendMeta.channel_order,
|
|
1963
|
+
shape: backendMeta.shape,
|
|
1964
|
+
fps: backendMeta.fps
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
const sourceVideo = parsed.source_video ? new Video({ filename: parsed.source_video.filename ?? "" }) : null;
|
|
1968
|
+
videos.push(
|
|
1969
|
+
new Video({
|
|
1970
|
+
filename,
|
|
1971
|
+
backend,
|
|
1972
|
+
backendMetadata: backendMeta,
|
|
1973
|
+
sourceVideo,
|
|
1974
|
+
openBackend: openVideos
|
|
1975
|
+
})
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
return videos;
|
|
1979
|
+
}
|
|
1980
|
+
function readFrameNumbers(file, datasetPath) {
|
|
1981
|
+
if (!datasetPath) return [];
|
|
1982
|
+
const groupPath = datasetPath.endsWith("/video") ? datasetPath.slice(0, -6) : datasetPath;
|
|
1983
|
+
const frameDataset = file.get(`${groupPath}/frame_numbers`);
|
|
1984
|
+
if (!frameDataset) return [];
|
|
1985
|
+
const values = frameDataset.value ?? [];
|
|
1986
|
+
return Array.from(values).map((v) => Number(v));
|
|
1987
|
+
}
|
|
1988
|
+
function readSuggestions(dataset, videos) {
|
|
1989
|
+
if (!dataset) return [];
|
|
1990
|
+
const values = dataset.value ?? [];
|
|
1991
|
+
const suggestions = [];
|
|
1992
|
+
for (const entry of values) {
|
|
1993
|
+
const parsed = typeof entry === "string" ? JSON.parse(entry) : JSON.parse(textDecoder.decode(entry));
|
|
1994
|
+
const videoIndex = Number(parsed.video ?? 0);
|
|
1995
|
+
const video = videos[videoIndex];
|
|
1996
|
+
if (!video) continue;
|
|
1997
|
+
suggestions.push(new SuggestionFrame({ video, frameIdx: parsed.frame_idx ?? parsed.frameIdx ?? 0, metadata: parsed }));
|
|
1998
|
+
}
|
|
1999
|
+
return suggestions;
|
|
2000
|
+
}
|
|
2001
|
+
function readSessions(dataset, videos, skeletons, labeledFrames) {
|
|
2002
|
+
if (!dataset) return [];
|
|
2003
|
+
const values = dataset.value ?? [];
|
|
2004
|
+
const sessions = [];
|
|
2005
|
+
for (const entry of values) {
|
|
2006
|
+
const parsed = typeof entry === "string" ? JSON.parse(entry) : JSON.parse(textDecoder.decode(entry));
|
|
2007
|
+
const cameraGroup = new CameraGroup();
|
|
2008
|
+
const cameraMap = /* @__PURE__ */ new Map();
|
|
2009
|
+
const calibration = asRecord(parsed.calibration);
|
|
2010
|
+
for (const [key, data] of Object.entries(calibration)) {
|
|
2011
|
+
if (key === "metadata") continue;
|
|
2012
|
+
const cameraData = asRecord(data);
|
|
2013
|
+
const camera = new Camera({
|
|
2014
|
+
name: cameraData.name ?? key,
|
|
2015
|
+
rvec: cameraData.rotation ?? [0, 0, 0],
|
|
2016
|
+
tvec: cameraData.translation ?? [0, 0, 0],
|
|
2017
|
+
matrix: cameraData.matrix,
|
|
2018
|
+
distortions: cameraData.distortions
|
|
2019
|
+
});
|
|
2020
|
+
cameraGroup.cameras.push(camera);
|
|
2021
|
+
cameraMap.set(String(key), camera);
|
|
2022
|
+
}
|
|
2023
|
+
const session = new RecordingSession({ cameraGroup, metadata: parsed.metadata ?? {} });
|
|
2024
|
+
const map = asRecord(parsed.camcorder_to_video_idx_map);
|
|
2025
|
+
for (const [cameraKey, videoIdx] of Object.entries(map)) {
|
|
2026
|
+
const camera = cameraMap.get(cameraKey);
|
|
2027
|
+
const video = videos[Number(videoIdx)];
|
|
2028
|
+
if (camera && video) {
|
|
2029
|
+
session.addVideo(video, camera);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
const frameGroups = Array.isArray(parsed.frame_group_dicts) ? parsed.frame_group_dicts : [];
|
|
2033
|
+
for (const group of frameGroups) {
|
|
2034
|
+
const groupRecord = asRecord(group);
|
|
2035
|
+
const frameIdx = groupRecord.frame_idx ?? groupRecord.frameIdx ?? 0;
|
|
2036
|
+
const instanceGroups = [];
|
|
2037
|
+
const instanceGroupList = Array.isArray(groupRecord.instance_groups) ? groupRecord.instance_groups : [];
|
|
2038
|
+
for (const instanceGroup of instanceGroupList) {
|
|
2039
|
+
const instanceGroupRecord = asRecord(instanceGroup);
|
|
2040
|
+
const instanceByCamera = /* @__PURE__ */ new Map();
|
|
2041
|
+
const instancesRecord = asRecord(instanceGroupRecord.instances);
|
|
2042
|
+
for (const [cameraKey, points] of Object.entries(instancesRecord)) {
|
|
2043
|
+
const camera = cameraMap.get(cameraKey);
|
|
2044
|
+
if (!camera) continue;
|
|
2045
|
+
const skeleton = skeletons[0] ?? new Skeleton({ nodes: [] });
|
|
2046
|
+
instanceByCamera.set(camera, new Instance({ points, skeleton }));
|
|
2047
|
+
}
|
|
2048
|
+
const rawPoints = instanceGroupRecord.points;
|
|
2049
|
+
const pointsValue = Array.isArray(rawPoints) ? rawPoints : void 0;
|
|
2050
|
+
instanceGroups.push(
|
|
2051
|
+
new InstanceGroup({
|
|
2052
|
+
instanceByCamera,
|
|
2053
|
+
score: instanceGroupRecord.score,
|
|
2054
|
+
points: pointsValue,
|
|
2055
|
+
metadata: instanceGroupRecord.metadata ?? {}
|
|
2056
|
+
})
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
const labeledFrameByCamera = /* @__PURE__ */ new Map();
|
|
2060
|
+
const labeledFrameMap = asRecord(groupRecord.labeled_frame_by_camera);
|
|
2061
|
+
for (const [cameraKey, labeledFrameIdx] of Object.entries(labeledFrameMap)) {
|
|
2062
|
+
const camera = cameraMap.get(cameraKey);
|
|
2063
|
+
const labeledFrame = labeledFrames[Number(labeledFrameIdx)];
|
|
2064
|
+
if (camera && labeledFrame) {
|
|
2065
|
+
labeledFrameByCamera.set(camera, labeledFrame);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
session.frameGroups.set(
|
|
2069
|
+
Number(frameIdx),
|
|
2070
|
+
new FrameGroup({
|
|
2071
|
+
frameIdx: Number(frameIdx),
|
|
2072
|
+
instanceGroups,
|
|
2073
|
+
labeledFrameByCamera,
|
|
2074
|
+
metadata: groupRecord.metadata ?? {}
|
|
2075
|
+
})
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
sessions.push(session);
|
|
2079
|
+
}
|
|
2080
|
+
return sessions;
|
|
2081
|
+
}
|
|
2082
|
+
function asRecord(value) {
|
|
2083
|
+
if (value && typeof value === "object") {
|
|
2084
|
+
return value;
|
|
2085
|
+
}
|
|
2086
|
+
return {};
|
|
2087
|
+
}
|
|
2088
|
+
function normalizeStructDataset(dataset) {
|
|
2089
|
+
if (!dataset) return {};
|
|
2090
|
+
const raw = dataset.value;
|
|
2091
|
+
if (!raw) return {};
|
|
2092
|
+
const fieldNames = getFieldNames(dataset);
|
|
2093
|
+
if (Array.isArray(raw) && raw.length > 0 && Array.isArray(raw[0])) {
|
|
2094
|
+
return mapStructuredRows(raw, fieldNames);
|
|
2095
|
+
}
|
|
2096
|
+
if (raw && ArrayBuffer.isView(raw) && Array.isArray(dataset.shape) && dataset.shape.length === 2) {
|
|
2097
|
+
const [rowCount, colCount] = dataset.shape;
|
|
2098
|
+
const rows = [];
|
|
2099
|
+
for (let i = 0; i < rowCount; i += 1) {
|
|
2100
|
+
const start = i * colCount;
|
|
2101
|
+
const end = start + colCount;
|
|
2102
|
+
const slice = Array.from(raw.slice(start, end));
|
|
2103
|
+
rows.push(slice);
|
|
2104
|
+
}
|
|
2105
|
+
return mapStructuredRows(rows, fieldNames);
|
|
2106
|
+
}
|
|
2107
|
+
if (raw && typeof raw === "object") {
|
|
2108
|
+
return raw;
|
|
2109
|
+
}
|
|
2110
|
+
return {};
|
|
2111
|
+
}
|
|
2112
|
+
function mapStructuredRows(rows, fieldNames) {
|
|
2113
|
+
if (!fieldNames.length) {
|
|
2114
|
+
return rows.reduce((acc, row, idx) => {
|
|
2115
|
+
acc[String(idx)] = row;
|
|
2116
|
+
return acc;
|
|
2117
|
+
}, {});
|
|
2118
|
+
}
|
|
2119
|
+
const data = {};
|
|
2120
|
+
fieldNames.forEach((field, idx) => {
|
|
2121
|
+
data[field] = rows.map((row) => row[idx]);
|
|
2122
|
+
});
|
|
2123
|
+
return data;
|
|
2124
|
+
}
|
|
2125
|
+
function getFieldNames(dataset) {
|
|
2126
|
+
const fields = dataset.dtype?.fields ? Object.keys(dataset.dtype.fields) : [];
|
|
2127
|
+
if (fields.length) return fields;
|
|
2128
|
+
const compoundMembers = dataset.metadata?.compound_type?.members;
|
|
2129
|
+
if (Array.isArray(compoundMembers) && compoundMembers.length) {
|
|
2130
|
+
const names = compoundMembers.map((member) => member.name).filter((name) => !!name);
|
|
2131
|
+
if (names.length) return names;
|
|
2132
|
+
}
|
|
2133
|
+
const attr = dataset.attrs?.field_names ?? dataset.attrs?.fieldNames;
|
|
2134
|
+
if (!attr) return [];
|
|
2135
|
+
const value = attr.value ?? attr;
|
|
2136
|
+
if (Array.isArray(value)) return value.map((entry) => String(entry));
|
|
2137
|
+
if (typeof value === "string") {
|
|
2138
|
+
try {
|
|
2139
|
+
const parsed = JSON.parse(value);
|
|
2140
|
+
if (Array.isArray(parsed)) return parsed.map((entry) => String(entry));
|
|
2141
|
+
} catch {
|
|
2142
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
if (value instanceof Uint8Array) {
|
|
2146
|
+
try {
|
|
2147
|
+
const parsed = JSON.parse(textDecoder.decode(value));
|
|
2148
|
+
if (Array.isArray(parsed)) return parsed.map((entry) => String(entry));
|
|
2149
|
+
} catch {
|
|
2150
|
+
return [];
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
return [];
|
|
2154
|
+
}
|
|
2155
|
+
function buildLabeledFrames(options) {
|
|
2156
|
+
const frames = [];
|
|
2157
|
+
const { framesData, instancesData, pointsData, predPointsData, skeletons, tracks, videos, formatId } = options;
|
|
2158
|
+
const frameIds = framesData.frame_id ?? [];
|
|
2159
|
+
const videoIdToIndex = buildVideoIdMap(framesData, videos);
|
|
2160
|
+
const instanceById = /* @__PURE__ */ new Map();
|
|
2161
|
+
const fromPredictedPairs = [];
|
|
2162
|
+
for (let frameIdx = 0; frameIdx < frameIds.length; frameIdx += 1) {
|
|
2163
|
+
const rawVideoId = Number(framesData.video?.[frameIdx] ?? 0);
|
|
2164
|
+
const videoIndex = videoIdToIndex.get(rawVideoId) ?? rawVideoId;
|
|
2165
|
+
const frameIndex = Number(framesData.frame_idx?.[frameIdx] ?? 0);
|
|
2166
|
+
const instStart = Number(framesData.instance_id_start?.[frameIdx] ?? 0);
|
|
2167
|
+
const instEnd = Number(framesData.instance_id_end?.[frameIdx] ?? 0);
|
|
2168
|
+
const video = videos[videoIndex];
|
|
2169
|
+
if (!video) continue;
|
|
2170
|
+
const instances = [];
|
|
2171
|
+
for (let instIdx = instStart; instIdx < instEnd; instIdx += 1) {
|
|
2172
|
+
const instanceType = Number(instancesData.instance_type?.[instIdx] ?? 0);
|
|
2173
|
+
const skeletonId = Number(instancesData.skeleton?.[instIdx] ?? 0);
|
|
2174
|
+
const trackId = Number(instancesData.track?.[instIdx] ?? -1);
|
|
2175
|
+
const pointStart = Number(instancesData.point_id_start?.[instIdx] ?? 0);
|
|
2176
|
+
const pointEnd = Number(instancesData.point_id_end?.[instIdx] ?? 0);
|
|
2177
|
+
const score = Number(instancesData.score?.[instIdx] ?? 0);
|
|
2178
|
+
const trackingScore = Number(instancesData.tracking_score?.[instIdx] ?? 0);
|
|
2179
|
+
const fromPredicted = Number(instancesData.from_predicted?.[instIdx] ?? -1);
|
|
2180
|
+
const skeleton = skeletons[skeletonId] ?? skeletons[0];
|
|
2181
|
+
const track = trackId >= 0 ? tracks[trackId] : null;
|
|
2182
|
+
let instance;
|
|
2183
|
+
if (instanceType === 0) {
|
|
2184
|
+
const points = slicePoints(pointsData, pointStart, pointEnd);
|
|
2185
|
+
instance = new Instance({ points: pointsFromArray(points, skeleton.nodeNames), skeleton, track, trackingScore });
|
|
2186
|
+
if (formatId < 1.1) {
|
|
2187
|
+
instance.points.forEach((point) => {
|
|
2188
|
+
point.xy = [point.xy[0] - 0.5, point.xy[1] - 0.5];
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2191
|
+
if (fromPredicted >= 0) {
|
|
2192
|
+
fromPredictedPairs.push([instIdx, fromPredicted]);
|
|
2193
|
+
}
|
|
2194
|
+
} else {
|
|
2195
|
+
const points = slicePoints(predPointsData, pointStart, pointEnd, true);
|
|
2196
|
+
instance = new PredictedInstance({ points: predictedPointsFromArray(points, skeleton.nodeNames), skeleton, track, score, trackingScore });
|
|
2197
|
+
if (formatId < 1.1) {
|
|
2198
|
+
instance.points.forEach((point) => {
|
|
2199
|
+
point.xy = [point.xy[0] - 0.5, point.xy[1] - 0.5];
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
instanceById.set(instIdx, instance);
|
|
2204
|
+
instances.push(instance);
|
|
2205
|
+
}
|
|
2206
|
+
frames.push(new LabeledFrame({ video, frameIdx: frameIndex, instances }));
|
|
2207
|
+
}
|
|
2208
|
+
for (const [instanceId, fromPredictedId] of fromPredictedPairs) {
|
|
2209
|
+
const instance = instanceById.get(instanceId);
|
|
2210
|
+
const predicted = instanceById.get(fromPredictedId);
|
|
2211
|
+
if (instance && predicted instanceof PredictedInstance && instance instanceof Instance) {
|
|
2212
|
+
instance.fromPredicted = predicted;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
return frames;
|
|
2216
|
+
}
|
|
2217
|
+
function buildVideoIdMap(framesData, videos) {
|
|
2218
|
+
const videoIds = /* @__PURE__ */ new Set();
|
|
2219
|
+
for (const value of framesData.video ?? []) {
|
|
2220
|
+
videoIds.add(Number(value));
|
|
2221
|
+
}
|
|
2222
|
+
if (!videoIds.size) return /* @__PURE__ */ new Map();
|
|
2223
|
+
const maxId = Math.max(...Array.from(videoIds));
|
|
2224
|
+
if (videoIds.size === videos.length && maxId === videos.length - 1) {
|
|
2225
|
+
const identity = /* @__PURE__ */ new Map();
|
|
2226
|
+
for (let i = 0; i < videos.length; i += 1) {
|
|
2227
|
+
identity.set(i, i);
|
|
2228
|
+
}
|
|
2229
|
+
return identity;
|
|
2230
|
+
}
|
|
2231
|
+
const map = /* @__PURE__ */ new Map();
|
|
2232
|
+
for (let index = 0; index < videos.length; index += 1) {
|
|
2233
|
+
const video = videos[index];
|
|
2234
|
+
const dataset = video.backend?.dataset ?? video.backendMetadata?.dataset ?? "";
|
|
2235
|
+
const parsedId = parseVideoIdFromDataset(dataset);
|
|
2236
|
+
if (parsedId != null) {
|
|
2237
|
+
map.set(parsedId, index);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
return map;
|
|
2241
|
+
}
|
|
2242
|
+
function parseVideoIdFromDataset(dataset) {
|
|
2243
|
+
if (!dataset) return null;
|
|
2244
|
+
const group = dataset.split("/")[0];
|
|
2245
|
+
if (!group.startsWith("video")) return null;
|
|
2246
|
+
const id = Number(group.slice(5));
|
|
2247
|
+
return Number.isNaN(id) ? null : id;
|
|
2248
|
+
}
|
|
2249
|
+
function slicePoints(data, start, end, predicted = false) {
|
|
2250
|
+
const xs = data.x ?? [];
|
|
2251
|
+
const ys = data.y ?? [];
|
|
2252
|
+
const visible = data.visible ?? [];
|
|
2253
|
+
const complete = data.complete ?? [];
|
|
2254
|
+
const scores = data.score ?? [];
|
|
2255
|
+
const points = [];
|
|
2256
|
+
for (let i = start; i < end; i += 1) {
|
|
2257
|
+
if (predicted) {
|
|
2258
|
+
points.push([xs[i], ys[i], scores[i], visible[i], complete[i]]);
|
|
2259
|
+
} else {
|
|
2260
|
+
points.push([xs[i], ys[i], visible[i], complete[i]]);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
return points;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
// src/codecs/slp/write.ts
|
|
2267
|
+
var isNode2 = typeof process !== "undefined" && !!process.versions?.node;
|
|
2268
|
+
var FORMAT_ID = 1.4;
|
|
2269
|
+
var SPAWNED_ON = 0;
|
|
2270
|
+
async function writeSlp(filename, labels, options) {
|
|
2271
|
+
const embedMode = options?.embed ?? false;
|
|
2272
|
+
if (embedMode && embedMode !== "source") {
|
|
2273
|
+
throw new Error("Embedding frames is not supported yet in writeSlp.");
|
|
2274
|
+
}
|
|
2275
|
+
if (!isNode2) {
|
|
2276
|
+
throw new Error("writeSlp currently requires a Node.js environment.");
|
|
2277
|
+
}
|
|
2278
|
+
const module = await getH5Module();
|
|
2279
|
+
const file = new module.File(filename, "w");
|
|
2280
|
+
try {
|
|
2281
|
+
writeMetadata(file, labels);
|
|
2282
|
+
writeVideos(file, labels.videos);
|
|
2283
|
+
writeTracks(file, labels.tracks);
|
|
2284
|
+
writeSuggestions(file, labels.suggestions, labels.videos);
|
|
2285
|
+
writeSessions(file, labels.sessions, labels.videos, labels.labeledFrames);
|
|
2286
|
+
writeLabeledFrames(file, labels);
|
|
2287
|
+
} finally {
|
|
2288
|
+
file.close();
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
function writeMetadata(file, labels) {
|
|
2292
|
+
const { skeletons, nodes } = serializeSkeletons(labels.skeletons);
|
|
2293
|
+
const metadata = {
|
|
2294
|
+
version: "2.0.0",
|
|
2295
|
+
skeletons,
|
|
2296
|
+
nodes,
|
|
2297
|
+
videos: [],
|
|
2298
|
+
tracks: [],
|
|
2299
|
+
suggestions: [],
|
|
2300
|
+
negative_anchors: {},
|
|
2301
|
+
provenance: labels.provenance ?? {}
|
|
2302
|
+
};
|
|
2303
|
+
file.create_group("metadata");
|
|
2304
|
+
const metadataGroup = file.get("metadata");
|
|
2305
|
+
metadataGroup.create_attribute("format_id", FORMAT_ID);
|
|
2306
|
+
metadataGroup.create_attribute("json", JSON.stringify(metadata));
|
|
2307
|
+
}
|
|
2308
|
+
function serializeSkeletons(skeletons) {
|
|
2309
|
+
const nodes = [];
|
|
2310
|
+
const nodeIndex = /* @__PURE__ */ new Map();
|
|
2311
|
+
for (const skeleton of skeletons) {
|
|
2312
|
+
for (const nodeName of skeleton.nodeNames) {
|
|
2313
|
+
if (!nodeIndex.has(nodeName)) {
|
|
2314
|
+
nodeIndex.set(nodeName, nodes.length);
|
|
2315
|
+
nodes.push({ name: nodeName });
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
const serialized = skeletons.map((skeleton) => {
|
|
2320
|
+
const links = [];
|
|
2321
|
+
for (const edge of skeleton.edges) {
|
|
2322
|
+
const source = nodeIndex.get(edge.source.name) ?? 0;
|
|
2323
|
+
const target = nodeIndex.get(edge.destination.name) ?? 0;
|
|
2324
|
+
links.push({ source, target, type: { "py/tuple": [1] } });
|
|
2325
|
+
}
|
|
2326
|
+
for (const [left, right] of skeleton.symmetryNames) {
|
|
2327
|
+
const source = nodeIndex.get(left) ?? 0;
|
|
2328
|
+
const target = nodeIndex.get(right) ?? 0;
|
|
2329
|
+
links.push({ source, target, type: { "py/tuple": [2] } });
|
|
2330
|
+
}
|
|
2331
|
+
return {
|
|
2332
|
+
links,
|
|
2333
|
+
name: skeleton.name ?? void 0,
|
|
2334
|
+
graph: skeleton.name ? { name: skeleton.name } : void 0
|
|
2335
|
+
};
|
|
2336
|
+
});
|
|
2337
|
+
return { skeletons: serialized, nodes };
|
|
2338
|
+
}
|
|
2339
|
+
function writeVideos(file, videos) {
|
|
2340
|
+
const payload = videos.map((video) => JSON.stringify(serializeVideo(video)));
|
|
2341
|
+
file.create_dataset({ name: "videos_json", data: payload });
|
|
2342
|
+
}
|
|
2343
|
+
function serializeVideo(video) {
|
|
2344
|
+
const backend = { ...video.backendMetadata ?? {} };
|
|
2345
|
+
if (backend.filename == null) backend.filename = video.filename;
|
|
2346
|
+
if (backend.dataset == null && video.backend?.dataset) backend.dataset = video.backend.dataset;
|
|
2347
|
+
if (backend.shape == null && video.backend?.shape) backend.shape = video.backend.shape;
|
|
2348
|
+
if (backend.fps == null && video.backend?.fps != null) backend.fps = video.backend.fps;
|
|
2349
|
+
const entry = {
|
|
2350
|
+
filename: video.filename,
|
|
2351
|
+
backend
|
|
2352
|
+
};
|
|
2353
|
+
if (video.sourceVideo) {
|
|
2354
|
+
entry.source_video = { filename: video.sourceVideo.filename };
|
|
2355
|
+
}
|
|
2356
|
+
return entry;
|
|
2357
|
+
}
|
|
2358
|
+
function writeTracks(file, tracks) {
|
|
2359
|
+
const payload = tracks.map((track) => JSON.stringify([SPAWNED_ON, track.name]));
|
|
2360
|
+
file.create_dataset({ name: "tracks_json", data: payload });
|
|
2361
|
+
}
|
|
2362
|
+
function writeSuggestions(file, suggestions, videos) {
|
|
2363
|
+
const payload = suggestions.map(
|
|
2364
|
+
(suggestion) => JSON.stringify({
|
|
2365
|
+
video: String(videos.indexOf(suggestion.video)),
|
|
2366
|
+
frame_idx: suggestion.frameIdx,
|
|
2367
|
+
group: suggestion.metadata?.group ?? 0
|
|
2368
|
+
})
|
|
2369
|
+
);
|
|
2370
|
+
file.create_dataset({ name: "suggestions_json", data: payload });
|
|
2371
|
+
}
|
|
2372
|
+
function writeSessions(file, sessions, videos, labeledFrames) {
|
|
2373
|
+
const labeledFrameIndex = /* @__PURE__ */ new Map();
|
|
2374
|
+
labeledFrames.forEach((lf, idx) => labeledFrameIndex.set(lf, idx));
|
|
2375
|
+
const payload = sessions.map((session) => JSON.stringify(serializeSession(session, videos, labeledFrameIndex)));
|
|
2376
|
+
file.create_dataset({ name: "sessions_json", data: payload });
|
|
2377
|
+
}
|
|
2378
|
+
function serializeSession(session, videos, labeledFrameIndex) {
|
|
2379
|
+
const calibration = { metadata: session.cameraGroup.metadata ?? {} };
|
|
2380
|
+
session.cameraGroup.cameras.forEach((camera, idx) => {
|
|
2381
|
+
const key = camera.name ?? String(idx);
|
|
2382
|
+
calibration[key] = {
|
|
2383
|
+
name: camera.name ?? key,
|
|
2384
|
+
rotation: camera.rvec,
|
|
2385
|
+
translation: camera.tvec,
|
|
2386
|
+
matrix: camera.matrix,
|
|
2387
|
+
distortions: camera.distortions
|
|
2388
|
+
};
|
|
2389
|
+
});
|
|
2390
|
+
const camcorder_to_video_idx_map = {};
|
|
2391
|
+
for (const [camera, video] of session.videoByCamera.entries()) {
|
|
2392
|
+
const cameraKey = cameraKeyForSession(camera, session);
|
|
2393
|
+
const videoIndex = videos.indexOf(video);
|
|
2394
|
+
if (videoIndex >= 0) {
|
|
2395
|
+
camcorder_to_video_idx_map[cameraKey] = videoIndex;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
const frame_group_dicts = [];
|
|
2399
|
+
for (const frameGroup of session.frameGroups.values()) {
|
|
2400
|
+
if (!frameGroup.instanceGroups.length) continue;
|
|
2401
|
+
frame_group_dicts.push(serializeFrameGroup(frameGroup, session, labeledFrameIndex));
|
|
2402
|
+
}
|
|
2403
|
+
return {
|
|
2404
|
+
calibration,
|
|
2405
|
+
camcorder_to_video_idx_map,
|
|
2406
|
+
frame_group_dicts,
|
|
2407
|
+
metadata: session.metadata ?? {}
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
function serializeFrameGroup(frameGroup, session, labeledFrameIndex) {
|
|
2411
|
+
const instance_groups = frameGroup.instanceGroups.map((group) => serializeInstanceGroup(group, session));
|
|
2412
|
+
const labeled_frame_by_camera = {};
|
|
2413
|
+
for (const [camera, labeledFrame] of frameGroup.labeledFrameByCamera.entries()) {
|
|
2414
|
+
const cameraKey = cameraKeyForSession(camera, session);
|
|
2415
|
+
const index = labeledFrameIndex.get(labeledFrame);
|
|
2416
|
+
if (index !== void 0) {
|
|
2417
|
+
labeled_frame_by_camera[cameraKey] = index;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
return {
|
|
2421
|
+
frame_idx: frameGroup.frameIdx,
|
|
2422
|
+
instance_groups,
|
|
2423
|
+
labeled_frame_by_camera,
|
|
2424
|
+
metadata: frameGroup.metadata ?? {}
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
function serializeInstanceGroup(group, session) {
|
|
2428
|
+
const instances = {};
|
|
2429
|
+
for (const [camera, instance] of group.instanceByCamera.entries()) {
|
|
2430
|
+
const cameraKey = cameraKeyForSession(camera, session);
|
|
2431
|
+
instances[cameraKey] = pointsToDict(instance);
|
|
2432
|
+
}
|
|
2433
|
+
const payload = {
|
|
2434
|
+
instances
|
|
2435
|
+
};
|
|
2436
|
+
if (group.score != null) payload.score = group.score;
|
|
2437
|
+
if (group.points != null) payload.points = group.points;
|
|
2438
|
+
if (group.metadata && Object.keys(group.metadata).length) payload.metadata = group.metadata;
|
|
2439
|
+
return payload;
|
|
2440
|
+
}
|
|
2441
|
+
function pointsToDict(instance) {
|
|
2442
|
+
const names = instance.skeleton.nodeNames;
|
|
2443
|
+
const dict = {};
|
|
2444
|
+
instance.points.forEach((point, idx) => {
|
|
2445
|
+
const name = point.name ?? names[idx] ?? String(idx);
|
|
2446
|
+
const row = [
|
|
2447
|
+
point.xy[0],
|
|
2448
|
+
point.xy[1],
|
|
2449
|
+
point.visible ? 1 : 0,
|
|
2450
|
+
point.complete ? 1 : 0
|
|
2451
|
+
];
|
|
2452
|
+
if (point.score != null) {
|
|
2453
|
+
row.push(point.score);
|
|
2454
|
+
}
|
|
2455
|
+
dict[name] = row;
|
|
2456
|
+
});
|
|
2457
|
+
return dict;
|
|
2458
|
+
}
|
|
2459
|
+
function cameraKeyForSession(camera, session) {
|
|
2460
|
+
const index = session.cameraGroup.cameras.indexOf(camera);
|
|
2461
|
+
return camera.name ?? String(index);
|
|
2462
|
+
}
|
|
2463
|
+
function writeLabeledFrames(file, labels) {
|
|
2464
|
+
const frames = [];
|
|
2465
|
+
const instances = [];
|
|
2466
|
+
const points = [];
|
|
2467
|
+
const predPoints = [];
|
|
2468
|
+
const instanceIndex = /* @__PURE__ */ new Map();
|
|
2469
|
+
const predictedLinks = [];
|
|
2470
|
+
for (const labeledFrame of labels.labeledFrames) {
|
|
2471
|
+
const frameId = frames.length;
|
|
2472
|
+
const instanceStart = instances.length;
|
|
2473
|
+
const videoIndex = Math.max(0, labels.videos.indexOf(labeledFrame.video));
|
|
2474
|
+
for (const instance of labeledFrame.instances) {
|
|
2475
|
+
const instanceId = instances.length;
|
|
2476
|
+
instanceIndex.set(instance, instanceId);
|
|
2477
|
+
const skeletonId = Math.max(0, labels.skeletons.indexOf(instance.skeleton));
|
|
2478
|
+
const trackId = instance.track ? labels.tracks.indexOf(instance.track) : -1;
|
|
2479
|
+
const trackingScore = instance.trackingScore ?? 0;
|
|
2480
|
+
let fromPredicted = -1;
|
|
2481
|
+
let score = 0;
|
|
2482
|
+
let pointStart = 0;
|
|
2483
|
+
let pointEnd = 0;
|
|
2484
|
+
if (instance instanceof PredictedInstance) {
|
|
2485
|
+
score = instance.score ?? 0;
|
|
2486
|
+
pointStart = predPoints.length;
|
|
2487
|
+
for (const point of instance.points) {
|
|
2488
|
+
predPoints.push([
|
|
2489
|
+
point.xy[0],
|
|
2490
|
+
point.xy[1],
|
|
2491
|
+
point.visible ? 1 : 0,
|
|
2492
|
+
point.complete ? 1 : 0,
|
|
2493
|
+
point.score ?? 0
|
|
2494
|
+
]);
|
|
2495
|
+
}
|
|
2496
|
+
pointEnd = predPoints.length;
|
|
2497
|
+
} else {
|
|
2498
|
+
pointStart = points.length;
|
|
2499
|
+
for (const point of instance.points) {
|
|
2500
|
+
points.push([
|
|
2501
|
+
point.xy[0],
|
|
2502
|
+
point.xy[1],
|
|
2503
|
+
point.visible ? 1 : 0,
|
|
2504
|
+
point.complete ? 1 : 0
|
|
2505
|
+
]);
|
|
2506
|
+
}
|
|
2507
|
+
pointEnd = points.length;
|
|
2508
|
+
if (instance.fromPredicted) {
|
|
2509
|
+
predictedLinks.push([instanceId, instance.fromPredicted]);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
instances.push([
|
|
2513
|
+
instanceId,
|
|
2514
|
+
instance instanceof PredictedInstance ? 1 : 0,
|
|
2515
|
+
frameId,
|
|
2516
|
+
skeletonId,
|
|
2517
|
+
trackId,
|
|
2518
|
+
fromPredicted,
|
|
2519
|
+
score,
|
|
2520
|
+
pointStart,
|
|
2521
|
+
pointEnd,
|
|
2522
|
+
trackingScore
|
|
2523
|
+
]);
|
|
2524
|
+
}
|
|
2525
|
+
const instanceEnd = instances.length;
|
|
2526
|
+
frames.push([frameId, videoIndex, labeledFrame.frameIdx, instanceStart, instanceEnd]);
|
|
2527
|
+
}
|
|
2528
|
+
for (const [instanceId, fromPredictedInstance] of predictedLinks) {
|
|
2529
|
+
const fromIndex = instanceIndex.get(fromPredictedInstance);
|
|
2530
|
+
if (fromIndex != null) {
|
|
2531
|
+
instances[instanceId][5] = fromIndex;
|
|
2532
|
+
} else {
|
|
2533
|
+
instances[instanceId][5] = -1;
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
createMatrixDataset(file, "frames", frames, ["frame_id", "video", "frame_idx", "instance_id_start", "instance_id_end"], "<i8");
|
|
2537
|
+
createMatrixDataset(
|
|
2538
|
+
file,
|
|
2539
|
+
"instances",
|
|
2540
|
+
instances,
|
|
2541
|
+
[
|
|
2542
|
+
"instance_id",
|
|
2543
|
+
"instance_type",
|
|
2544
|
+
"frame_id",
|
|
2545
|
+
"skeleton",
|
|
2546
|
+
"track",
|
|
2547
|
+
"from_predicted",
|
|
2548
|
+
"score",
|
|
2549
|
+
"point_id_start",
|
|
2550
|
+
"point_id_end",
|
|
2551
|
+
"tracking_score"
|
|
2552
|
+
],
|
|
2553
|
+
"<f8"
|
|
2554
|
+
);
|
|
2555
|
+
createMatrixDataset(file, "points", points, ["x", "y", "visible", "complete"], "<f8");
|
|
2556
|
+
createMatrixDataset(file, "pred_points", predPoints, ["x", "y", "visible", "complete", "score"], "<f8");
|
|
2557
|
+
}
|
|
2558
|
+
function createMatrixDataset(file, name, rows, fieldNames, dtype) {
|
|
2559
|
+
const rowCount = rows.length;
|
|
2560
|
+
const colCount = fieldNames.length;
|
|
2561
|
+
const data = rows.flat();
|
|
2562
|
+
file.create_dataset({ name, data, shape: [rowCount, colCount], dtype });
|
|
2563
|
+
const dataset = file.get(name);
|
|
2564
|
+
dataset.create_attribute("field_names", fieldNames);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
// src/io/main.ts
|
|
2568
|
+
async function loadSlp(source, options) {
|
|
2569
|
+
return readSlp(source, { openVideos: options?.openVideos ?? true, h5: options?.h5 });
|
|
2570
|
+
}
|
|
2571
|
+
async function saveSlp(labels, filename, options) {
|
|
2572
|
+
await writeSlp(filename, labels, {
|
|
2573
|
+
embed: options?.embed ?? false,
|
|
2574
|
+
restoreOriginalVideos: options?.restoreOriginalVideos ?? true
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
async function loadVideo(filename, options) {
|
|
2578
|
+
const backend = await createVideoBackend(filename, { dataset: options?.dataset });
|
|
2579
|
+
return new Video({ filename, backend, openBackend: options?.openBackend ?? true });
|
|
2580
|
+
}
|
|
2581
|
+
export {
|
|
2582
|
+
Camera,
|
|
2583
|
+
CameraGroup,
|
|
2584
|
+
Edge,
|
|
2585
|
+
FrameGroup,
|
|
2586
|
+
Instance,
|
|
2587
|
+
InstanceGroup,
|
|
2588
|
+
LabeledFrame,
|
|
2589
|
+
Labels,
|
|
2590
|
+
LabelsSet,
|
|
2591
|
+
Mp4BoxVideoBackend,
|
|
2592
|
+
Node,
|
|
2593
|
+
PredictedInstance,
|
|
2594
|
+
RecordingSession,
|
|
2595
|
+
Skeleton,
|
|
2596
|
+
SuggestionFrame,
|
|
2597
|
+
Symmetry,
|
|
2598
|
+
Track,
|
|
2599
|
+
Video,
|
|
2600
|
+
fromDict,
|
|
2601
|
+
fromNumpy,
|
|
2602
|
+
labelsFromNumpy,
|
|
2603
|
+
loadSlp,
|
|
2604
|
+
loadVideo,
|
|
2605
|
+
makeCameraFromDict,
|
|
2606
|
+
pointsEmpty,
|
|
2607
|
+
pointsFromArray,
|
|
2608
|
+
pointsFromDict,
|
|
2609
|
+
predictedPointsEmpty,
|
|
2610
|
+
predictedPointsFromArray,
|
|
2611
|
+
predictedPointsFromDict,
|
|
2612
|
+
rodriguesTransformation,
|
|
2613
|
+
saveSlp,
|
|
2614
|
+
toDict,
|
|
2615
|
+
toNumpy
|
|
2616
|
+
};
|