@talmolab/sleap-io.js 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/chunk-CDU5QGU6.js +590 -0
- package/dist/index.d.ts +347 -125
- package/dist/index.js +1182 -436
- package/dist/instance-D_5PPN1y.d.ts +126 -0
- package/dist/lite.d.ts +209 -0
- package/dist/lite.js +178 -0
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -42,6 +42,25 @@ const video = await loadVideo("/path/to/video.mp4", { openBackend: false });
|
|
|
42
42
|
video.close();
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
### Lite mode (Workers-compatible)
|
|
46
|
+
|
|
47
|
+
For environments that don't support WebAssembly compilation (e.g., Cloudflare Workers), use the `/lite` entry point:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { loadSlpMetadata, validateSlpBuffer } from "@talmolab/sleap-io.js/lite";
|
|
51
|
+
|
|
52
|
+
// Load metadata without pose coordinates
|
|
53
|
+
const metadata = await loadSlpMetadata(buffer);
|
|
54
|
+
console.log(metadata.skeletons); // Full skeleton definitions
|
|
55
|
+
console.log(metadata.counts); // { labeledFrames, instances, points, predictedPoints }
|
|
56
|
+
console.log(metadata.provenance); // { sleap_version, ... }
|
|
57
|
+
|
|
58
|
+
// Quick validation
|
|
59
|
+
validateSlpBuffer(buffer); // throws on invalid
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The lite module uses [jsfive](https://github.com/usnistgov/jsfive) (pure JavaScript) instead of h5wasm (WebAssembly), enabling use in restricted environments. It can read all metadata but not pose coordinates or video frames.
|
|
63
|
+
|
|
45
64
|
## Architecture
|
|
46
65
|
|
|
47
66
|
- **I/O layer**: `loadSlp`/`saveSlp` wrap the HDF5 reader/writer in `src/codecs/slp`.
|
|
@@ -0,0 +1,590 @@
|
|
|
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/skeleton.ts
|
|
238
|
+
var Node = class {
|
|
239
|
+
name;
|
|
240
|
+
constructor(name) {
|
|
241
|
+
this.name = name;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
var Edge = class {
|
|
245
|
+
source;
|
|
246
|
+
destination;
|
|
247
|
+
constructor(source, destination) {
|
|
248
|
+
this.source = source;
|
|
249
|
+
this.destination = destination;
|
|
250
|
+
}
|
|
251
|
+
at(index) {
|
|
252
|
+
if (index === 0) return this.source;
|
|
253
|
+
if (index === 1) return this.destination;
|
|
254
|
+
throw new Error("Edge only has 2 nodes (source and destination).");
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
var Symmetry = class {
|
|
258
|
+
nodes;
|
|
259
|
+
constructor(nodes) {
|
|
260
|
+
const set = new Set(nodes);
|
|
261
|
+
if (set.size !== 2) {
|
|
262
|
+
throw new Error("Symmetry must contain exactly 2 nodes.");
|
|
263
|
+
}
|
|
264
|
+
this.nodes = set;
|
|
265
|
+
}
|
|
266
|
+
at(index) {
|
|
267
|
+
let i = 0;
|
|
268
|
+
for (const node of this.nodes) {
|
|
269
|
+
if (i === index) return node;
|
|
270
|
+
i += 1;
|
|
271
|
+
}
|
|
272
|
+
throw new Error("Symmetry index out of range.");
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
var Skeleton = class {
|
|
276
|
+
nodes;
|
|
277
|
+
edges;
|
|
278
|
+
symmetries;
|
|
279
|
+
name;
|
|
280
|
+
nameToNode;
|
|
281
|
+
nodeToIndex;
|
|
282
|
+
constructor(options) {
|
|
283
|
+
const resolved = Array.isArray(options) ? { nodes: options } : options;
|
|
284
|
+
this.nodes = resolved.nodes.map((node) => typeof node === "string" ? new Node(node) : node);
|
|
285
|
+
this.edges = [];
|
|
286
|
+
this.symmetries = [];
|
|
287
|
+
this.name = resolved.name;
|
|
288
|
+
this.nameToNode = /* @__PURE__ */ new Map();
|
|
289
|
+
this.nodeToIndex = /* @__PURE__ */ new Map();
|
|
290
|
+
this.rebuildCache();
|
|
291
|
+
if (resolved.edges) {
|
|
292
|
+
this.edges = resolved.edges.map((edge) => edge instanceof Edge ? edge : this.edgeFrom(edge));
|
|
293
|
+
}
|
|
294
|
+
if (resolved.symmetries) {
|
|
295
|
+
this.symmetries = resolved.symmetries.map(
|
|
296
|
+
(symmetry) => symmetry instanceof Symmetry ? symmetry : this.symmetryFrom(symmetry)
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
rebuildCache(nodes = this.nodes) {
|
|
301
|
+
this.nameToNode = new Map(nodes.map((node) => [node.name, node]));
|
|
302
|
+
this.nodeToIndex = new Map(nodes.map((node, index) => [node, index]));
|
|
303
|
+
}
|
|
304
|
+
get nodeNames() {
|
|
305
|
+
return this.nodes.map((node) => node.name);
|
|
306
|
+
}
|
|
307
|
+
index(node) {
|
|
308
|
+
if (typeof node === "number") return node;
|
|
309
|
+
if (typeof node === "string") {
|
|
310
|
+
const found = this.nameToNode.get(node);
|
|
311
|
+
if (!found) throw new Error(`Node '${node}' not found in skeleton.`);
|
|
312
|
+
return this.nodeToIndex.get(found) ?? -1;
|
|
313
|
+
}
|
|
314
|
+
const idx = this.nodeToIndex.get(node);
|
|
315
|
+
if (idx === void 0) throw new Error("Node not found in skeleton.");
|
|
316
|
+
return idx;
|
|
317
|
+
}
|
|
318
|
+
node(node) {
|
|
319
|
+
if (node instanceof Node) return node;
|
|
320
|
+
if (typeof node === "number") return this.nodes[node];
|
|
321
|
+
const found = this.nameToNode.get(node);
|
|
322
|
+
if (!found) throw new Error(`Node '${node}' not found in skeleton.`);
|
|
323
|
+
return found;
|
|
324
|
+
}
|
|
325
|
+
get edgeIndices() {
|
|
326
|
+
return this.edges.map((edge) => [this.index(edge.source), this.index(edge.destination)]);
|
|
327
|
+
}
|
|
328
|
+
get symmetryNames() {
|
|
329
|
+
return this.symmetries.map((symmetry) => {
|
|
330
|
+
const nodes = Array.from(symmetry.nodes).map((node) => node.name);
|
|
331
|
+
return [nodes[0], nodes[1]];
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
matches(other) {
|
|
335
|
+
if (this.nodeNames.length !== other.nodeNames.length) return false;
|
|
336
|
+
for (let i = 0; i < this.nodeNames.length; i += 1) {
|
|
337
|
+
if (this.nodeNames[i] !== other.nodeNames[i]) return false;
|
|
338
|
+
}
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
addEdge(source, destination) {
|
|
342
|
+
this.edges.push(new Edge(this.node(source), this.node(destination)));
|
|
343
|
+
}
|
|
344
|
+
addSymmetry(left, right) {
|
|
345
|
+
this.symmetries.push(new Symmetry([this.node(left), this.node(right)]));
|
|
346
|
+
}
|
|
347
|
+
edgeFrom(edge) {
|
|
348
|
+
const [source, destination] = edge;
|
|
349
|
+
return new Edge(this.node(source), this.node(destination));
|
|
350
|
+
}
|
|
351
|
+
symmetryFrom(symmetry) {
|
|
352
|
+
const [a, b] = symmetry;
|
|
353
|
+
return new Symmetry([this.node(a), this.node(b)]);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// src/codecs/slp/parsers.ts
|
|
358
|
+
var textDecoder = new TextDecoder();
|
|
359
|
+
function parseJsonAttr(attr) {
|
|
360
|
+
if (!attr) return null;
|
|
361
|
+
const value = attr.value ?? attr;
|
|
362
|
+
if (typeof value === "string") return JSON.parse(value);
|
|
363
|
+
if (value instanceof Uint8Array) return JSON.parse(textDecoder.decode(value));
|
|
364
|
+
if (value && typeof value === "object" && "buffer" in value) {
|
|
365
|
+
return JSON.parse(textDecoder.decode(new Uint8Array(value.buffer)));
|
|
366
|
+
}
|
|
367
|
+
return JSON.parse(String(value));
|
|
368
|
+
}
|
|
369
|
+
function trimHdf5String(str) {
|
|
370
|
+
return str.trim().replace(/\0+$/, "");
|
|
371
|
+
}
|
|
372
|
+
function parseJsonEntry(entry) {
|
|
373
|
+
if (typeof entry === "string") return JSON.parse(trimHdf5String(entry));
|
|
374
|
+
if (entry instanceof Uint8Array) return JSON.parse(trimHdf5String(textDecoder.decode(entry)));
|
|
375
|
+
if (entry && typeof entry === "object" && "buffer" in entry) {
|
|
376
|
+
return JSON.parse(trimHdf5String(textDecoder.decode(new Uint8Array(entry.buffer))));
|
|
377
|
+
}
|
|
378
|
+
return entry;
|
|
379
|
+
}
|
|
380
|
+
function resolveEdgeType(edgeType, cache, state) {
|
|
381
|
+
if (!edgeType || typeof edgeType !== "object") return 1;
|
|
382
|
+
const et = edgeType;
|
|
383
|
+
if (et["py/reduce"]) {
|
|
384
|
+
const reduce = et["py/reduce"];
|
|
385
|
+
const tuple = reduce[1]?.["py/tuple"];
|
|
386
|
+
const typeId = tuple?.[0] ?? 1;
|
|
387
|
+
cache.set(state.nextId, typeId);
|
|
388
|
+
state.nextId += 1;
|
|
389
|
+
return typeId;
|
|
390
|
+
}
|
|
391
|
+
if (et["py/tuple"]) {
|
|
392
|
+
const tuple = et["py/tuple"];
|
|
393
|
+
const typeId = tuple[0] ?? 1;
|
|
394
|
+
cache.set(state.nextId, typeId);
|
|
395
|
+
state.nextId += 1;
|
|
396
|
+
return typeId;
|
|
397
|
+
}
|
|
398
|
+
if (et["py/id"]) {
|
|
399
|
+
const pyId = et["py/id"];
|
|
400
|
+
return cache.get(pyId) ?? pyId;
|
|
401
|
+
}
|
|
402
|
+
return 1;
|
|
403
|
+
}
|
|
404
|
+
function parseSkeletons(metadataJson) {
|
|
405
|
+
if (!metadataJson || typeof metadataJson !== "object") return [];
|
|
406
|
+
const meta = metadataJson;
|
|
407
|
+
const nodeNames = (meta.nodes ?? []).map(
|
|
408
|
+
(node) => typeof node === "object" ? node.name ?? "" : String(node)
|
|
409
|
+
);
|
|
410
|
+
const skeletonEntries = meta.skeletons ?? [];
|
|
411
|
+
const skeletons = [];
|
|
412
|
+
for (const entry of skeletonEntries) {
|
|
413
|
+
const edges = [];
|
|
414
|
+
const symmetries = [];
|
|
415
|
+
const typeCache = /* @__PURE__ */ new Map();
|
|
416
|
+
const typeState = { nextId: 1 };
|
|
417
|
+
const entryNodes = entry.nodes ?? [];
|
|
418
|
+
const skeletonNodeIds = entryNodes.map(
|
|
419
|
+
(node) => Number(typeof node === "object" ? node.id ?? 0 : node)
|
|
420
|
+
);
|
|
421
|
+
const nodeOrder = skeletonNodeIds.length ? skeletonNodeIds : nodeNames.map((_, index) => index);
|
|
422
|
+
const nodes = nodeOrder.map((nodeId) => nodeNames[nodeId]).filter((name) => name !== void 0).map((name) => new Node(name));
|
|
423
|
+
const nodeIndexById = /* @__PURE__ */ new Map();
|
|
424
|
+
nodeOrder.forEach((nodeId, index) => {
|
|
425
|
+
nodeIndexById.set(Number(nodeId), index);
|
|
426
|
+
});
|
|
427
|
+
const links = entry.links ?? [];
|
|
428
|
+
for (const link of links) {
|
|
429
|
+
const source = Number(link.source);
|
|
430
|
+
const target = Number(link.target);
|
|
431
|
+
const edgeType = resolveEdgeType(link.type, typeCache, typeState);
|
|
432
|
+
if (edgeType === 2) {
|
|
433
|
+
symmetries.push([source, target]);
|
|
434
|
+
} else {
|
|
435
|
+
edges.push([source, target]);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const remapPair = (pair) => {
|
|
439
|
+
const sourceIndex = nodeIndexById.get(pair[0]);
|
|
440
|
+
const targetIndex = nodeIndexById.get(pair[1]);
|
|
441
|
+
if (sourceIndex === void 0 || targetIndex === void 0) return null;
|
|
442
|
+
return [sourceIndex, targetIndex];
|
|
443
|
+
};
|
|
444
|
+
const mappedEdges = edges.map(remapPair).filter((pair) => pair !== null);
|
|
445
|
+
const seenSymmetries = /* @__PURE__ */ new Set();
|
|
446
|
+
const mappedSymmetries = symmetries.map(remapPair).filter((pair) => pair !== null).filter(([a, b]) => {
|
|
447
|
+
const key = a < b ? `${a}-${b}` : `${b}-${a}`;
|
|
448
|
+
if (seenSymmetries.has(key)) return false;
|
|
449
|
+
seenSymmetries.add(key);
|
|
450
|
+
return true;
|
|
451
|
+
});
|
|
452
|
+
const graph = entry.graph;
|
|
453
|
+
const skeleton = new Skeleton({
|
|
454
|
+
nodes,
|
|
455
|
+
edges: mappedEdges,
|
|
456
|
+
symmetries: mappedSymmetries,
|
|
457
|
+
name: graph?.name ?? entry.name
|
|
458
|
+
});
|
|
459
|
+
skeletons.push(skeleton);
|
|
460
|
+
}
|
|
461
|
+
return skeletons;
|
|
462
|
+
}
|
|
463
|
+
function parseTracks(values) {
|
|
464
|
+
const tracks = [];
|
|
465
|
+
for (const entry of values) {
|
|
466
|
+
let parsed = entry;
|
|
467
|
+
if (typeof entry === "string") {
|
|
468
|
+
try {
|
|
469
|
+
parsed = JSON.parse(trimHdf5String(entry));
|
|
470
|
+
} catch {
|
|
471
|
+
parsed = trimHdf5String(entry);
|
|
472
|
+
}
|
|
473
|
+
} else if (entry instanceof Uint8Array) {
|
|
474
|
+
try {
|
|
475
|
+
parsed = JSON.parse(trimHdf5String(textDecoder.decode(entry)));
|
|
476
|
+
} catch {
|
|
477
|
+
parsed = trimHdf5String(textDecoder.decode(entry));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (Array.isArray(parsed)) {
|
|
481
|
+
tracks.push(new Track(String(parsed[1] ?? parsed[0])));
|
|
482
|
+
} else if (parsed && typeof parsed === "object" && "name" in parsed) {
|
|
483
|
+
tracks.push(new Track(String(parsed.name)));
|
|
484
|
+
} else {
|
|
485
|
+
tracks.push(new Track(String(parsed)));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return tracks;
|
|
489
|
+
}
|
|
490
|
+
function parseVideosMetadata(values, labelsPath) {
|
|
491
|
+
const videos = [];
|
|
492
|
+
for (const entry of values) {
|
|
493
|
+
if (!entry) continue;
|
|
494
|
+
let parsed;
|
|
495
|
+
if (typeof entry === "string") {
|
|
496
|
+
parsed = JSON.parse(trimHdf5String(entry));
|
|
497
|
+
} else if (entry instanceof Uint8Array) {
|
|
498
|
+
parsed = JSON.parse(trimHdf5String(textDecoder.decode(entry)));
|
|
499
|
+
} else {
|
|
500
|
+
parsed = entry;
|
|
501
|
+
}
|
|
502
|
+
const backendMeta = parsed.backend ?? {};
|
|
503
|
+
let filename = backendMeta.filename ?? parsed.filename ?? "";
|
|
504
|
+
const dataset = backendMeta.dataset ?? null;
|
|
505
|
+
let embedded = false;
|
|
506
|
+
if (filename === ".") {
|
|
507
|
+
embedded = true;
|
|
508
|
+
filename = labelsPath ?? "embedded";
|
|
509
|
+
}
|
|
510
|
+
const shape = backendMeta.shape;
|
|
511
|
+
videos.push({
|
|
512
|
+
filename,
|
|
513
|
+
dataset: dataset ?? void 0,
|
|
514
|
+
format: backendMeta.format,
|
|
515
|
+
width: shape?.[2],
|
|
516
|
+
height: shape?.[1],
|
|
517
|
+
channels: shape?.[3],
|
|
518
|
+
frameCount: shape?.[0],
|
|
519
|
+
fps: backendMeta.fps,
|
|
520
|
+
channelOrder: backendMeta.channel_order,
|
|
521
|
+
embedded,
|
|
522
|
+
sourceVideo: parsed.source_video
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
return videos;
|
|
526
|
+
}
|
|
527
|
+
function parseSuggestions(values) {
|
|
528
|
+
const suggestions = [];
|
|
529
|
+
for (const entry of values) {
|
|
530
|
+
const parsed = parseJsonEntry(entry);
|
|
531
|
+
suggestions.push({
|
|
532
|
+
video: Number(parsed.video ?? 0),
|
|
533
|
+
frameIdx: parsed.frame_idx ?? parsed.frameIdx ?? 0,
|
|
534
|
+
metadata: parsed
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
return suggestions;
|
|
538
|
+
}
|
|
539
|
+
function parseSessionsMetadata(values) {
|
|
540
|
+
const sessions = [];
|
|
541
|
+
for (const entry of values) {
|
|
542
|
+
const parsed = parseJsonEntry(entry);
|
|
543
|
+
const calibration = parsed.calibration ?? {};
|
|
544
|
+
const cameras = [];
|
|
545
|
+
for (const [key, data] of Object.entries(calibration)) {
|
|
546
|
+
if (key === "metadata") continue;
|
|
547
|
+
const cameraData = data;
|
|
548
|
+
cameras.push({
|
|
549
|
+
name: cameraData.name ?? key,
|
|
550
|
+
rvec: cameraData.rotation ?? [0, 0, 0],
|
|
551
|
+
tvec: cameraData.translation ?? [0, 0, 0],
|
|
552
|
+
matrix: cameraData.matrix,
|
|
553
|
+
distortions: cameraData.distortions
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
const videosByCamera = {};
|
|
557
|
+
const map = parsed.camcorder_to_video_idx_map ?? {};
|
|
558
|
+
for (const [cameraKey, videoIdx] of Object.entries(map)) {
|
|
559
|
+
videosByCamera[cameraKey] = Number(videoIdx);
|
|
560
|
+
}
|
|
561
|
+
sessions.push({
|
|
562
|
+
cameras,
|
|
563
|
+
videosByCamera,
|
|
564
|
+
metadata: parsed.metadata
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
return sessions;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export {
|
|
571
|
+
Track,
|
|
572
|
+
pointsEmpty,
|
|
573
|
+
predictedPointsEmpty,
|
|
574
|
+
pointsFromArray,
|
|
575
|
+
predictedPointsFromArray,
|
|
576
|
+
Instance,
|
|
577
|
+
PredictedInstance,
|
|
578
|
+
pointsFromDict,
|
|
579
|
+
predictedPointsFromDict,
|
|
580
|
+
Node,
|
|
581
|
+
Edge,
|
|
582
|
+
Symmetry,
|
|
583
|
+
Skeleton,
|
|
584
|
+
parseJsonAttr,
|
|
585
|
+
parseSkeletons,
|
|
586
|
+
parseTracks,
|
|
587
|
+
parseVideosMetadata,
|
|
588
|
+
parseSuggestions,
|
|
589
|
+
parseSessionsMetadata
|
|
590
|
+
};
|