@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/dist/index.js
CHANGED
|
@@ -1,238 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
Edge,
|
|
3
|
+
Instance,
|
|
4
|
+
Node,
|
|
5
|
+
PredictedInstance,
|
|
6
|
+
Skeleton,
|
|
7
|
+
Symmetry,
|
|
8
|
+
Track,
|
|
9
|
+
parseJsonAttr,
|
|
10
|
+
parseSkeletons,
|
|
11
|
+
pointsEmpty,
|
|
12
|
+
pointsFromArray,
|
|
13
|
+
pointsFromDict,
|
|
14
|
+
predictedPointsEmpty,
|
|
15
|
+
predictedPointsFromArray,
|
|
16
|
+
predictedPointsFromDict
|
|
17
|
+
} from "./chunk-CDU5QGU6.js";
|
|
236
18
|
|
|
237
19
|
// src/model/labeled-frame.ts
|
|
238
20
|
var LabeledFrame = class {
|
|
@@ -293,126 +75,6 @@ var LabeledFrame = class {
|
|
|
293
75
|
}
|
|
294
76
|
};
|
|
295
77
|
|
|
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
78
|
// src/model/video.ts
|
|
417
79
|
var Video = class {
|
|
418
80
|
filename;
|
|
@@ -1541,6 +1203,383 @@ var Mp4BoxVideoBackend = class {
|
|
|
1541
1203
|
}
|
|
1542
1204
|
};
|
|
1543
1205
|
|
|
1206
|
+
// src/codecs/slp/h5-worker.ts
|
|
1207
|
+
var H5_WORKER_CODE = `
|
|
1208
|
+
// h5wasm streaming worker
|
|
1209
|
+
// Uses createLazyFile for HTTP range request streaming
|
|
1210
|
+
|
|
1211
|
+
let h5wasmModule = null;
|
|
1212
|
+
let FS = null;
|
|
1213
|
+
let currentFile = null;
|
|
1214
|
+
let mountPath = null;
|
|
1215
|
+
|
|
1216
|
+
self.onmessage = async function(e) {
|
|
1217
|
+
const { type, payload, id } = e.data;
|
|
1218
|
+
|
|
1219
|
+
try {
|
|
1220
|
+
switch (type) {
|
|
1221
|
+
case 'init':
|
|
1222
|
+
await initH5Wasm(payload?.h5wasmUrl);
|
|
1223
|
+
respond(id, { success: true });
|
|
1224
|
+
break;
|
|
1225
|
+
|
|
1226
|
+
case 'openUrl':
|
|
1227
|
+
const result = await openRemoteFile(payload.url, payload.filename);
|
|
1228
|
+
respond(id, result);
|
|
1229
|
+
break;
|
|
1230
|
+
|
|
1231
|
+
case 'getKeys':
|
|
1232
|
+
const keys = getKeys(payload.path);
|
|
1233
|
+
respond(id, { success: true, keys });
|
|
1234
|
+
break;
|
|
1235
|
+
|
|
1236
|
+
case 'getAttr':
|
|
1237
|
+
const attr = getAttr(payload.path, payload.name);
|
|
1238
|
+
respond(id, { success: true, value: attr });
|
|
1239
|
+
break;
|
|
1240
|
+
|
|
1241
|
+
case 'getAttrs':
|
|
1242
|
+
const attrs = getAttrs(payload.path);
|
|
1243
|
+
respond(id, { success: true, attrs });
|
|
1244
|
+
break;
|
|
1245
|
+
|
|
1246
|
+
case 'getDatasetMeta':
|
|
1247
|
+
const meta = getDatasetMeta(payload.path);
|
|
1248
|
+
respond(id, { success: true, meta });
|
|
1249
|
+
break;
|
|
1250
|
+
|
|
1251
|
+
case 'getDatasetValue':
|
|
1252
|
+
const data = getDatasetValue(payload.path, payload.slice);
|
|
1253
|
+
respond(id, { success: true, data }, data.transferables);
|
|
1254
|
+
break;
|
|
1255
|
+
|
|
1256
|
+
case 'close':
|
|
1257
|
+
closeFile();
|
|
1258
|
+
respond(id, { success: true });
|
|
1259
|
+
break;
|
|
1260
|
+
|
|
1261
|
+
default:
|
|
1262
|
+
respond(id, { success: false, error: 'Unknown message type: ' + type });
|
|
1263
|
+
}
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
respond(id, { success: false, error: error.message || String(error) });
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
function respond(id, data, transferables) {
|
|
1270
|
+
if (transferables) {
|
|
1271
|
+
self.postMessage({ id, ...data }, transferables);
|
|
1272
|
+
} else {
|
|
1273
|
+
self.postMessage({ id, ...data });
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
async function initH5Wasm(h5wasmUrl) {
|
|
1278
|
+
if (h5wasmModule) return;
|
|
1279
|
+
|
|
1280
|
+
// Default to CDN if no URL provided
|
|
1281
|
+
const url = h5wasmUrl || 'https://cdn.jsdelivr.net/npm/h5wasm@0.8.8/dist/iife/h5wasm.js';
|
|
1282
|
+
|
|
1283
|
+
// Import h5wasm IIFE
|
|
1284
|
+
importScripts(url);
|
|
1285
|
+
|
|
1286
|
+
// Wait for module to be ready
|
|
1287
|
+
const Module = await h5wasm.ready;
|
|
1288
|
+
h5wasmModule = h5wasm;
|
|
1289
|
+
FS = Module.FS;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
async function openRemoteFile(url, filename = 'data.h5') {
|
|
1293
|
+
if (!h5wasmModule) {
|
|
1294
|
+
throw new Error('h5wasm not initialized');
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Close any existing file
|
|
1298
|
+
closeFile();
|
|
1299
|
+
|
|
1300
|
+
// Create mount point
|
|
1301
|
+
mountPath = '/remote-' + Date.now();
|
|
1302
|
+
FS.mkdir(mountPath);
|
|
1303
|
+
|
|
1304
|
+
// Create lazy file - this enables range request streaming!
|
|
1305
|
+
FS.createLazyFile(mountPath, filename, url, true, false);
|
|
1306
|
+
|
|
1307
|
+
// Open with h5wasm
|
|
1308
|
+
const filePath = mountPath + '/' + filename;
|
|
1309
|
+
currentFile = new h5wasm.File(filePath, 'r');
|
|
1310
|
+
|
|
1311
|
+
return {
|
|
1312
|
+
success: true,
|
|
1313
|
+
path: currentFile.path,
|
|
1314
|
+
filename: currentFile.filename,
|
|
1315
|
+
keys: currentFile.keys()
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function getKeys(path) {
|
|
1320
|
+
if (!currentFile) throw new Error('No file open');
|
|
1321
|
+
const item = path === '/' || !path ? currentFile : currentFile.get(path);
|
|
1322
|
+
if (!item) throw new Error('Path not found: ' + path);
|
|
1323
|
+
return item.keys ? item.keys() : [];
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function getAttr(path, name) {
|
|
1327
|
+
if (!currentFile) throw new Error('No file open');
|
|
1328
|
+
const item = path === '/' || !path ? currentFile : currentFile.get(path);
|
|
1329
|
+
if (!item) throw new Error('Path not found: ' + path);
|
|
1330
|
+
const attrs = item.attrs;
|
|
1331
|
+
return attrs?.[name] || null;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function getAttrs(path) {
|
|
1335
|
+
if (!currentFile) throw new Error('No file open');
|
|
1336
|
+
const item = path === '/' || !path ? currentFile : currentFile.get(path);
|
|
1337
|
+
if (!item) throw new Error('Path not found: ' + path);
|
|
1338
|
+
return item.attrs || {};
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function getDatasetMeta(path) {
|
|
1342
|
+
if (!currentFile) throw new Error('No file open');
|
|
1343
|
+
const dataset = currentFile.get(path);
|
|
1344
|
+
if (!dataset) throw new Error('Dataset not found: ' + path);
|
|
1345
|
+
return {
|
|
1346
|
+
shape: dataset.shape,
|
|
1347
|
+
dtype: dataset.dtype,
|
|
1348
|
+
metadata: dataset.metadata
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function getDatasetValue(path, slice) {
|
|
1353
|
+
if (!currentFile) throw new Error('No file open');
|
|
1354
|
+
const dataset = currentFile.get(path);
|
|
1355
|
+
if (!dataset) throw new Error('Dataset not found: ' + path);
|
|
1356
|
+
|
|
1357
|
+
// Get value or slice
|
|
1358
|
+
let value;
|
|
1359
|
+
if (slice && Array.isArray(slice)) {
|
|
1360
|
+
value = dataset.slice(slice);
|
|
1361
|
+
} else {
|
|
1362
|
+
value = dataset.value;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// Prepare for transfer
|
|
1366
|
+
const transferables = [];
|
|
1367
|
+
let transferValue = value;
|
|
1368
|
+
|
|
1369
|
+
if (ArrayBuffer.isView(value)) {
|
|
1370
|
+
// TypedArray - transfer the underlying buffer
|
|
1371
|
+
transferValue = {
|
|
1372
|
+
type: 'typedarray',
|
|
1373
|
+
dtype: value.constructor.name,
|
|
1374
|
+
buffer: value.buffer,
|
|
1375
|
+
byteOffset: value.byteOffset,
|
|
1376
|
+
length: value.length
|
|
1377
|
+
};
|
|
1378
|
+
transferables.push(value.buffer);
|
|
1379
|
+
} else if (value instanceof ArrayBuffer) {
|
|
1380
|
+
transferValue = { type: 'arraybuffer', buffer: value };
|
|
1381
|
+
transferables.push(value);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
return {
|
|
1385
|
+
value: transferValue,
|
|
1386
|
+
shape: dataset.shape,
|
|
1387
|
+
dtype: dataset.dtype,
|
|
1388
|
+
transferables
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function closeFile() {
|
|
1393
|
+
if (currentFile) {
|
|
1394
|
+
try { currentFile.close(); } catch (e) {}
|
|
1395
|
+
currentFile = null;
|
|
1396
|
+
}
|
|
1397
|
+
if (mountPath && FS) {
|
|
1398
|
+
try { FS.rmdir(mountPath); } catch (e) {}
|
|
1399
|
+
mountPath = null;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
`;
|
|
1403
|
+
function createH5Worker() {
|
|
1404
|
+
const blob = new Blob([H5_WORKER_CODE], { type: "application/javascript" });
|
|
1405
|
+
const url = URL.createObjectURL(blob);
|
|
1406
|
+
const worker = new Worker(url);
|
|
1407
|
+
worker.addEventListener(
|
|
1408
|
+
"error",
|
|
1409
|
+
() => {
|
|
1410
|
+
URL.revokeObjectURL(url);
|
|
1411
|
+
},
|
|
1412
|
+
{ once: true }
|
|
1413
|
+
);
|
|
1414
|
+
return worker;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// src/codecs/slp/h5-streaming.ts
|
|
1418
|
+
function reconstructValue(data) {
|
|
1419
|
+
if (data && typeof data === "object" && "type" in data) {
|
|
1420
|
+
const typed = data;
|
|
1421
|
+
if (typed.type === "typedarray" && typed.buffer) {
|
|
1422
|
+
const TypedArrayConstructor = getTypedArrayConstructor(typed.dtype || "Uint8Array");
|
|
1423
|
+
return new TypedArrayConstructor(typed.buffer, typed.byteOffset || 0, typed.length);
|
|
1424
|
+
}
|
|
1425
|
+
if (typed.type === "arraybuffer" && typed.buffer) {
|
|
1426
|
+
return typed.buffer;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
return data;
|
|
1430
|
+
}
|
|
1431
|
+
function getTypedArrayConstructor(name) {
|
|
1432
|
+
const constructors = {
|
|
1433
|
+
Int8Array,
|
|
1434
|
+
Uint8Array,
|
|
1435
|
+
Uint8ClampedArray,
|
|
1436
|
+
Int16Array,
|
|
1437
|
+
Uint16Array,
|
|
1438
|
+
Int32Array,
|
|
1439
|
+
Uint32Array,
|
|
1440
|
+
Float32Array,
|
|
1441
|
+
Float64Array,
|
|
1442
|
+
BigInt64Array,
|
|
1443
|
+
BigUint64Array
|
|
1444
|
+
};
|
|
1445
|
+
return constructors[name] || Uint8Array;
|
|
1446
|
+
}
|
|
1447
|
+
var StreamingH5File = class {
|
|
1448
|
+
worker;
|
|
1449
|
+
messageId = 0;
|
|
1450
|
+
pendingMessages = /* @__PURE__ */ new Map();
|
|
1451
|
+
_keys = [];
|
|
1452
|
+
_isOpen = false;
|
|
1453
|
+
constructor() {
|
|
1454
|
+
this.worker = createH5Worker();
|
|
1455
|
+
this.worker.onmessage = this.handleMessage.bind(this);
|
|
1456
|
+
this.worker.onerror = this.handleError.bind(this);
|
|
1457
|
+
}
|
|
1458
|
+
handleMessage(e) {
|
|
1459
|
+
const { id, ...data } = e.data;
|
|
1460
|
+
const pending = this.pendingMessages.get(id);
|
|
1461
|
+
if (pending) {
|
|
1462
|
+
this.pendingMessages.delete(id);
|
|
1463
|
+
if (data.success) {
|
|
1464
|
+
pending.resolve(e.data);
|
|
1465
|
+
} else {
|
|
1466
|
+
pending.reject(new Error(data.error || "Worker operation failed"));
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
handleError(e) {
|
|
1471
|
+
console.error("[StreamingH5File] Worker error:", e.message);
|
|
1472
|
+
for (const [id, pending] of this.pendingMessages) {
|
|
1473
|
+
pending.reject(new Error(`Worker error: ${e.message}`));
|
|
1474
|
+
this.pendingMessages.delete(id);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
send(type, payload) {
|
|
1478
|
+
return new Promise((resolve, reject) => {
|
|
1479
|
+
const id = ++this.messageId;
|
|
1480
|
+
this.pendingMessages.set(id, { resolve, reject });
|
|
1481
|
+
this.worker.postMessage({ type, payload, id });
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Initialize the h5wasm module in the worker.
|
|
1486
|
+
*/
|
|
1487
|
+
async init(options) {
|
|
1488
|
+
await this.send("init", { h5wasmUrl: options?.h5wasmUrl });
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Open a remote HDF5 file for streaming access.
|
|
1492
|
+
*
|
|
1493
|
+
* @param url - URL to the HDF5 file (must support HTTP range requests)
|
|
1494
|
+
* @param options - Optional settings
|
|
1495
|
+
*/
|
|
1496
|
+
async open(url, options) {
|
|
1497
|
+
await this.init(options);
|
|
1498
|
+
const filename = options?.filenameHint || url.split("/").pop()?.split("?")[0] || "data.h5";
|
|
1499
|
+
const result = await this.send("openUrl", { url, filename });
|
|
1500
|
+
this._keys = result.keys || [];
|
|
1501
|
+
this._isOpen = true;
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Whether a file is currently open.
|
|
1505
|
+
*/
|
|
1506
|
+
get isOpen() {
|
|
1507
|
+
return this._isOpen;
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Get the root-level keys in the file.
|
|
1511
|
+
*/
|
|
1512
|
+
keys() {
|
|
1513
|
+
return this._keys;
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Get the keys (children) at a given path.
|
|
1517
|
+
*/
|
|
1518
|
+
async getKeys(path) {
|
|
1519
|
+
const result = await this.send("getKeys", { path });
|
|
1520
|
+
return result.keys || [];
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Get an attribute value.
|
|
1524
|
+
*/
|
|
1525
|
+
async getAttr(path, name) {
|
|
1526
|
+
const result = await this.send("getAttr", { path, name });
|
|
1527
|
+
return result.value?.value ?? result.value;
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Get all attributes at a path.
|
|
1531
|
+
*/
|
|
1532
|
+
async getAttrs(path) {
|
|
1533
|
+
const result = await this.send("getAttrs", { path });
|
|
1534
|
+
return result.attrs || {};
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Get dataset metadata (shape, dtype) without reading values.
|
|
1538
|
+
*/
|
|
1539
|
+
async getDatasetMeta(path) {
|
|
1540
|
+
const result = await this.send("getDatasetMeta", { path });
|
|
1541
|
+
const meta = result.meta;
|
|
1542
|
+
return meta;
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Read a dataset's value.
|
|
1546
|
+
*
|
|
1547
|
+
* @param path - Path to the dataset
|
|
1548
|
+
* @param slice - Optional slice specification (array of [start, end] pairs)
|
|
1549
|
+
*/
|
|
1550
|
+
async getDatasetValue(path, slice) {
|
|
1551
|
+
const result = await this.send("getDatasetValue", { path, slice });
|
|
1552
|
+
const data = result.data;
|
|
1553
|
+
return {
|
|
1554
|
+
value: reconstructValue(data.value),
|
|
1555
|
+
shape: data.shape,
|
|
1556
|
+
dtype: data.dtype
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Close the file and terminate the worker.
|
|
1561
|
+
*/
|
|
1562
|
+
async close() {
|
|
1563
|
+
if (this._isOpen) {
|
|
1564
|
+
await this.send("close");
|
|
1565
|
+
this._isOpen = false;
|
|
1566
|
+
}
|
|
1567
|
+
this.worker.terminate();
|
|
1568
|
+
this._keys = [];
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
function isStreamingSupported() {
|
|
1572
|
+
return typeof Worker !== "undefined" && typeof Blob !== "undefined" && typeof URL !== "undefined";
|
|
1573
|
+
}
|
|
1574
|
+
async function openStreamingH5(url, options) {
|
|
1575
|
+
if (!isStreamingSupported()) {
|
|
1576
|
+
throw new Error("Streaming HDF5 requires Web Worker support");
|
|
1577
|
+
}
|
|
1578
|
+
const file = new StreamingH5File();
|
|
1579
|
+
await file.open(url, options);
|
|
1580
|
+
return file;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1544
1583
|
// src/codecs/slp/h5.ts
|
|
1545
1584
|
var isNode = typeof process !== "undefined" && !!process.versions?.node;
|
|
1546
1585
|
var modulePromise = null;
|
|
@@ -1803,7 +1842,7 @@ async function readSlp(source, options) {
|
|
|
1803
1842
|
const formatId = Number(metadataAttrs["format_id"]?.value ?? metadataAttrs["format_id"] ?? 1);
|
|
1804
1843
|
const metadataJson = parseJsonAttr(metadataAttrs["json"]);
|
|
1805
1844
|
const labelsPath = typeof source === "string" ? source : options?.h5?.filenameHint ?? "slp-data.slp";
|
|
1806
|
-
const skeletons =
|
|
1845
|
+
const skeletons = parseSkeletons(metadataJson);
|
|
1807
1846
|
const tracks = readTracks(file.get("tracks_json"));
|
|
1808
1847
|
const videos = await readVideos(file.get("videos_json"), labelsPath, options?.openVideos ?? true, file);
|
|
1809
1848
|
const suggestions = readSuggestions(file.get("suggestions_json"), videos);
|
|
@@ -1835,85 +1874,6 @@ async function readSlp(source, options) {
|
|
|
1835
1874
|
close();
|
|
1836
1875
|
}
|
|
1837
1876
|
}
|
|
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
1877
|
function readTracks(dataset) {
|
|
1918
1878
|
if (!dataset) return [];
|
|
1919
1879
|
const values = dataset.value ?? [];
|
|
@@ -2653,41 +2613,827 @@ function encodeYamlSkeleton(skeletons) {
|
|
|
2653
2613
|
});
|
|
2654
2614
|
return YAML.stringify(payload);
|
|
2655
2615
|
}
|
|
2616
|
+
|
|
2617
|
+
// src/rendering/colors.ts
|
|
2618
|
+
var NAMED_COLORS = {
|
|
2619
|
+
black: [0, 0, 0],
|
|
2620
|
+
white: [255, 255, 255],
|
|
2621
|
+
red: [255, 0, 0],
|
|
2622
|
+
green: [0, 255, 0],
|
|
2623
|
+
blue: [0, 0, 255],
|
|
2624
|
+
yellow: [255, 255, 0],
|
|
2625
|
+
cyan: [0, 255, 255],
|
|
2626
|
+
magenta: [255, 0, 255],
|
|
2627
|
+
gray: [128, 128, 128],
|
|
2628
|
+
grey: [128, 128, 128],
|
|
2629
|
+
orange: [255, 165, 0],
|
|
2630
|
+
purple: [128, 0, 128],
|
|
2631
|
+
pink: [255, 192, 203],
|
|
2632
|
+
brown: [139, 69, 19]
|
|
2633
|
+
};
|
|
2634
|
+
var PALETTES = {
|
|
2635
|
+
// MATLAB default colors
|
|
2636
|
+
standard: [
|
|
2637
|
+
[0, 114, 189],
|
|
2638
|
+
[217, 83, 25],
|
|
2639
|
+
[237, 177, 32],
|
|
2640
|
+
[126, 47, 142],
|
|
2641
|
+
[119, 172, 48],
|
|
2642
|
+
[77, 190, 238],
|
|
2643
|
+
[162, 20, 47]
|
|
2644
|
+
],
|
|
2645
|
+
// Tableau 10
|
|
2646
|
+
tableau10: [
|
|
2647
|
+
[31, 119, 180],
|
|
2648
|
+
[255, 127, 14],
|
|
2649
|
+
[44, 160, 44],
|
|
2650
|
+
[214, 39, 40],
|
|
2651
|
+
[148, 103, 189],
|
|
2652
|
+
[140, 86, 75],
|
|
2653
|
+
[227, 119, 194],
|
|
2654
|
+
[127, 127, 127],
|
|
2655
|
+
[188, 189, 34],
|
|
2656
|
+
[23, 190, 207]
|
|
2657
|
+
],
|
|
2658
|
+
// High-contrast distinct colors (Glasbey-inspired, for many instances)
|
|
2659
|
+
distinct: [
|
|
2660
|
+
[230, 25, 75],
|
|
2661
|
+
[60, 180, 75],
|
|
2662
|
+
[255, 225, 25],
|
|
2663
|
+
[67, 99, 216],
|
|
2664
|
+
[245, 130, 49],
|
|
2665
|
+
[145, 30, 180],
|
|
2666
|
+
[66, 212, 244],
|
|
2667
|
+
[240, 50, 230],
|
|
2668
|
+
[191, 239, 69],
|
|
2669
|
+
[250, 190, 212],
|
|
2670
|
+
[70, 153, 144],
|
|
2671
|
+
[220, 190, 255],
|
|
2672
|
+
[154, 99, 36],
|
|
2673
|
+
[255, 250, 200],
|
|
2674
|
+
[128, 0, 0],
|
|
2675
|
+
[170, 255, 195],
|
|
2676
|
+
[128, 128, 0],
|
|
2677
|
+
[255, 216, 177],
|
|
2678
|
+
[0, 0, 117],
|
|
2679
|
+
[169, 169, 169]
|
|
2680
|
+
],
|
|
2681
|
+
// Viridis (10 samples)
|
|
2682
|
+
viridis: [
|
|
2683
|
+
[68, 1, 84],
|
|
2684
|
+
[72, 40, 120],
|
|
2685
|
+
[62, 74, 137],
|
|
2686
|
+
[49, 104, 142],
|
|
2687
|
+
[38, 130, 142],
|
|
2688
|
+
[31, 158, 137],
|
|
2689
|
+
[53, 183, 121],
|
|
2690
|
+
[110, 206, 88],
|
|
2691
|
+
[181, 222, 43],
|
|
2692
|
+
[253, 231, 37]
|
|
2693
|
+
],
|
|
2694
|
+
// Rainbow for node coloring
|
|
2695
|
+
rainbow: [
|
|
2696
|
+
[255, 0, 0],
|
|
2697
|
+
[255, 127, 0],
|
|
2698
|
+
[255, 255, 0],
|
|
2699
|
+
[127, 255, 0],
|
|
2700
|
+
[0, 255, 0],
|
|
2701
|
+
[0, 255, 127],
|
|
2702
|
+
[0, 255, 255],
|
|
2703
|
+
[0, 127, 255],
|
|
2704
|
+
[0, 0, 255],
|
|
2705
|
+
[127, 0, 255],
|
|
2706
|
+
[255, 0, 255],
|
|
2707
|
+
[255, 0, 127]
|
|
2708
|
+
],
|
|
2709
|
+
// Warm colors
|
|
2710
|
+
warm: [
|
|
2711
|
+
[255, 89, 94],
|
|
2712
|
+
[255, 146, 76],
|
|
2713
|
+
[255, 202, 58],
|
|
2714
|
+
[255, 154, 0],
|
|
2715
|
+
[255, 97, 56],
|
|
2716
|
+
[255, 50, 50]
|
|
2717
|
+
],
|
|
2718
|
+
// Cool colors
|
|
2719
|
+
cool: [
|
|
2720
|
+
[67, 170, 139],
|
|
2721
|
+
[77, 144, 142],
|
|
2722
|
+
[87, 117, 144],
|
|
2723
|
+
[97, 90, 147],
|
|
2724
|
+
[107, 63, 149],
|
|
2725
|
+
[117, 36, 152]
|
|
2726
|
+
],
|
|
2727
|
+
// Pastel colors
|
|
2728
|
+
pastel: [
|
|
2729
|
+
[255, 179, 186],
|
|
2730
|
+
[255, 223, 186],
|
|
2731
|
+
[255, 255, 186],
|
|
2732
|
+
[186, 255, 201],
|
|
2733
|
+
[186, 225, 255],
|
|
2734
|
+
[219, 186, 255]
|
|
2735
|
+
],
|
|
2736
|
+
// Seaborn-inspired
|
|
2737
|
+
seaborn: [
|
|
2738
|
+
[76, 114, 176],
|
|
2739
|
+
[221, 132, 82],
|
|
2740
|
+
[85, 168, 104],
|
|
2741
|
+
[196, 78, 82],
|
|
2742
|
+
[129, 114, 179],
|
|
2743
|
+
[147, 120, 96],
|
|
2744
|
+
[218, 139, 195],
|
|
2745
|
+
[140, 140, 140],
|
|
2746
|
+
[204, 185, 116],
|
|
2747
|
+
[100, 181, 205]
|
|
2748
|
+
]
|
|
2749
|
+
};
|
|
2750
|
+
function getPalette(name, n) {
|
|
2751
|
+
const palette = PALETTES[name];
|
|
2752
|
+
if (!palette) {
|
|
2753
|
+
throw new Error(`Unknown palette: ${name}`);
|
|
2754
|
+
}
|
|
2755
|
+
if (n <= palette.length) {
|
|
2756
|
+
return palette.slice(0, n);
|
|
2757
|
+
}
|
|
2758
|
+
return Array.from({ length: n }, (_, i) => palette[i % palette.length]);
|
|
2759
|
+
}
|
|
2760
|
+
function resolveColor(color) {
|
|
2761
|
+
if (Array.isArray(color)) {
|
|
2762
|
+
if (color.length >= 3) {
|
|
2763
|
+
return [color[0], color[1], color[2]];
|
|
2764
|
+
}
|
|
2765
|
+
throw new Error(`Invalid color array: ${color}`);
|
|
2766
|
+
}
|
|
2767
|
+
if (typeof color === "number") {
|
|
2768
|
+
const v = Math.round(color);
|
|
2769
|
+
return [v, v, v];
|
|
2770
|
+
}
|
|
2771
|
+
if (typeof color === "string") {
|
|
2772
|
+
const s = color.trim().toLowerCase();
|
|
2773
|
+
if (s in NAMED_COLORS) {
|
|
2774
|
+
return NAMED_COLORS[s];
|
|
2775
|
+
}
|
|
2776
|
+
if (s.startsWith("#")) {
|
|
2777
|
+
return hexToRgb(s);
|
|
2778
|
+
}
|
|
2779
|
+
const paletteMatch = s.match(/^(\w+)\[(\d+)\]$/);
|
|
2780
|
+
if (paletteMatch) {
|
|
2781
|
+
const [, paletteName, indexStr] = paletteMatch;
|
|
2782
|
+
const palette = PALETTES[paletteName];
|
|
2783
|
+
if (palette) {
|
|
2784
|
+
const index = parseInt(indexStr, 10) % palette.length;
|
|
2785
|
+
return palette[index];
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
const rgbMatch = s.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
|
2789
|
+
if (rgbMatch) {
|
|
2790
|
+
return [
|
|
2791
|
+
parseInt(rgbMatch[1], 10),
|
|
2792
|
+
parseInt(rgbMatch[2], 10),
|
|
2793
|
+
parseInt(rgbMatch[3], 10)
|
|
2794
|
+
];
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
throw new Error(`Cannot resolve color: ${color}`);
|
|
2798
|
+
}
|
|
2799
|
+
function hexToRgb(hex) {
|
|
2800
|
+
const h = hex.replace("#", "");
|
|
2801
|
+
if (h.length === 3) {
|
|
2802
|
+
return [
|
|
2803
|
+
parseInt(h[0] + h[0], 16),
|
|
2804
|
+
parseInt(h[1] + h[1], 16),
|
|
2805
|
+
parseInt(h[2] + h[2], 16)
|
|
2806
|
+
];
|
|
2807
|
+
}
|
|
2808
|
+
if (h.length === 6) {
|
|
2809
|
+
return [
|
|
2810
|
+
parseInt(h.slice(0, 2), 16),
|
|
2811
|
+
parseInt(h.slice(2, 4), 16),
|
|
2812
|
+
parseInt(h.slice(4, 6), 16)
|
|
2813
|
+
];
|
|
2814
|
+
}
|
|
2815
|
+
throw new Error(`Invalid hex color: ${hex}`);
|
|
2816
|
+
}
|
|
2817
|
+
function rgbToCSS(rgb, alpha = 1) {
|
|
2818
|
+
return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha})`;
|
|
2819
|
+
}
|
|
2820
|
+
function determineColorScheme(scheme, hasTracks, isSingleImage) {
|
|
2821
|
+
if (scheme !== "auto") {
|
|
2822
|
+
return scheme;
|
|
2823
|
+
}
|
|
2824
|
+
if (hasTracks) {
|
|
2825
|
+
return "track";
|
|
2826
|
+
}
|
|
2827
|
+
if (isSingleImage) {
|
|
2828
|
+
return "instance";
|
|
2829
|
+
}
|
|
2830
|
+
return "node";
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
// src/rendering/shapes.ts
|
|
2834
|
+
function drawCircle(ctx, x, y, size, fillColor, edgeColor, edgeWidth = 1) {
|
|
2835
|
+
ctx.beginPath();
|
|
2836
|
+
ctx.arc(x, y, size, 0, Math.PI * 2);
|
|
2837
|
+
ctx.fillStyle = fillColor;
|
|
2838
|
+
ctx.fill();
|
|
2839
|
+
if (edgeColor) {
|
|
2840
|
+
ctx.strokeStyle = edgeColor;
|
|
2841
|
+
ctx.lineWidth = edgeWidth;
|
|
2842
|
+
ctx.stroke();
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
function drawSquare(ctx, x, y, size, fillColor, edgeColor, edgeWidth = 1) {
|
|
2846
|
+
const half = size;
|
|
2847
|
+
ctx.fillStyle = fillColor;
|
|
2848
|
+
ctx.fillRect(x - half, y - half, half * 2, half * 2);
|
|
2849
|
+
if (edgeColor) {
|
|
2850
|
+
ctx.strokeStyle = edgeColor;
|
|
2851
|
+
ctx.lineWidth = edgeWidth;
|
|
2852
|
+
ctx.strokeRect(x - half, y - half, half * 2, half * 2);
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
function drawDiamond(ctx, x, y, size, fillColor, edgeColor, edgeWidth = 1) {
|
|
2856
|
+
ctx.beginPath();
|
|
2857
|
+
ctx.moveTo(x, y - size);
|
|
2858
|
+
ctx.lineTo(x + size, y);
|
|
2859
|
+
ctx.lineTo(x, y + size);
|
|
2860
|
+
ctx.lineTo(x - size, y);
|
|
2861
|
+
ctx.closePath();
|
|
2862
|
+
ctx.fillStyle = fillColor;
|
|
2863
|
+
ctx.fill();
|
|
2864
|
+
if (edgeColor) {
|
|
2865
|
+
ctx.strokeStyle = edgeColor;
|
|
2866
|
+
ctx.lineWidth = edgeWidth;
|
|
2867
|
+
ctx.stroke();
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
function drawTriangle(ctx, x, y, size, fillColor, edgeColor, edgeWidth = 1) {
|
|
2871
|
+
const h = size * 0.866;
|
|
2872
|
+
ctx.beginPath();
|
|
2873
|
+
ctx.moveTo(x, y - size);
|
|
2874
|
+
ctx.lineTo(x + size, y + h);
|
|
2875
|
+
ctx.lineTo(x - size, y + h);
|
|
2876
|
+
ctx.closePath();
|
|
2877
|
+
ctx.fillStyle = fillColor;
|
|
2878
|
+
ctx.fill();
|
|
2879
|
+
if (edgeColor) {
|
|
2880
|
+
ctx.strokeStyle = edgeColor;
|
|
2881
|
+
ctx.lineWidth = edgeWidth;
|
|
2882
|
+
ctx.stroke();
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
function drawCross(ctx, x, y, size, fillColor, _edgeColor, edgeWidth = 2) {
|
|
2886
|
+
ctx.strokeStyle = fillColor;
|
|
2887
|
+
ctx.lineWidth = edgeWidth;
|
|
2888
|
+
ctx.lineCap = "round";
|
|
2889
|
+
ctx.beginPath();
|
|
2890
|
+
ctx.moveTo(x - size, y);
|
|
2891
|
+
ctx.lineTo(x + size, y);
|
|
2892
|
+
ctx.stroke();
|
|
2893
|
+
ctx.beginPath();
|
|
2894
|
+
ctx.moveTo(x, y - size);
|
|
2895
|
+
ctx.lineTo(x, y + size);
|
|
2896
|
+
ctx.stroke();
|
|
2897
|
+
}
|
|
2898
|
+
var MARKER_FUNCTIONS = {
|
|
2899
|
+
circle: drawCircle,
|
|
2900
|
+
square: drawSquare,
|
|
2901
|
+
diamond: drawDiamond,
|
|
2902
|
+
triangle: drawTriangle,
|
|
2903
|
+
cross: drawCross
|
|
2904
|
+
};
|
|
2905
|
+
function getMarkerFunction(shape) {
|
|
2906
|
+
return MARKER_FUNCTIONS[shape];
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
// src/rendering/context.ts
|
|
2910
|
+
var RenderContext = class {
|
|
2911
|
+
constructor(canvas, frameIdx, frameSize, instances, skeletonEdges, nodeNames, scale = 1, offset = [0, 0]) {
|
|
2912
|
+
this.canvas = canvas;
|
|
2913
|
+
this.frameIdx = frameIdx;
|
|
2914
|
+
this.frameSize = frameSize;
|
|
2915
|
+
this.instances = instances;
|
|
2916
|
+
this.skeletonEdges = skeletonEdges;
|
|
2917
|
+
this.nodeNames = nodeNames;
|
|
2918
|
+
this.scale = scale;
|
|
2919
|
+
this.offset = offset;
|
|
2920
|
+
}
|
|
2921
|
+
/**
|
|
2922
|
+
* Transform world coordinates to canvas coordinates.
|
|
2923
|
+
*/
|
|
2924
|
+
worldToCanvas(x, y) {
|
|
2925
|
+
return [
|
|
2926
|
+
(x - this.offset[0]) * this.scale,
|
|
2927
|
+
(y - this.offset[1]) * this.scale
|
|
2928
|
+
];
|
|
2929
|
+
}
|
|
2930
|
+
};
|
|
2931
|
+
var InstanceContext = class {
|
|
2932
|
+
constructor(canvas, instanceIdx, points, skeletonEdges, nodeNames, trackIdx = null, trackName = null, confidence = null, scale = 1, offset = [0, 0]) {
|
|
2933
|
+
this.canvas = canvas;
|
|
2934
|
+
this.instanceIdx = instanceIdx;
|
|
2935
|
+
this.points = points;
|
|
2936
|
+
this.skeletonEdges = skeletonEdges;
|
|
2937
|
+
this.nodeNames = nodeNames;
|
|
2938
|
+
this.trackIdx = trackIdx;
|
|
2939
|
+
this.trackName = trackName;
|
|
2940
|
+
this.confidence = confidence;
|
|
2941
|
+
this.scale = scale;
|
|
2942
|
+
this.offset = offset;
|
|
2943
|
+
}
|
|
2944
|
+
/**
|
|
2945
|
+
* Transform world coordinates to canvas coordinates.
|
|
2946
|
+
*/
|
|
2947
|
+
worldToCanvas(x, y) {
|
|
2948
|
+
return [
|
|
2949
|
+
(x - this.offset[0]) * this.scale,
|
|
2950
|
+
(y - this.offset[1]) * this.scale
|
|
2951
|
+
];
|
|
2952
|
+
}
|
|
2953
|
+
/**
|
|
2954
|
+
* Get centroid of valid (non-NaN) points.
|
|
2955
|
+
*/
|
|
2956
|
+
getCentroid() {
|
|
2957
|
+
let sumX = 0, sumY = 0, count = 0;
|
|
2958
|
+
for (const pt of this.points) {
|
|
2959
|
+
const x = pt[0];
|
|
2960
|
+
const y = pt[1];
|
|
2961
|
+
if (!isNaN(x) && !isNaN(y)) {
|
|
2962
|
+
sumX += x;
|
|
2963
|
+
sumY += y;
|
|
2964
|
+
count++;
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
if (count === 0) return null;
|
|
2968
|
+
return [sumX / count, sumY / count];
|
|
2969
|
+
}
|
|
2970
|
+
/**
|
|
2971
|
+
* Get bounding box of valid points.
|
|
2972
|
+
* Returns [x1, y1, x2, y2] or null if no valid points.
|
|
2973
|
+
*/
|
|
2974
|
+
getBbox() {
|
|
2975
|
+
let minX = Infinity, minY = Infinity;
|
|
2976
|
+
let maxX = -Infinity, maxY = -Infinity;
|
|
2977
|
+
let hasValid = false;
|
|
2978
|
+
for (const pt of this.points) {
|
|
2979
|
+
const x = pt[0];
|
|
2980
|
+
const y = pt[1];
|
|
2981
|
+
if (!isNaN(x) && !isNaN(y)) {
|
|
2982
|
+
minX = Math.min(minX, x);
|
|
2983
|
+
minY = Math.min(minY, y);
|
|
2984
|
+
maxX = Math.max(maxX, x);
|
|
2985
|
+
maxY = Math.max(maxY, y);
|
|
2986
|
+
hasValid = true;
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
if (!hasValid) return null;
|
|
2990
|
+
return [minX, minY, maxX, maxY];
|
|
2991
|
+
}
|
|
2992
|
+
};
|
|
2993
|
+
|
|
2994
|
+
// src/rendering/render.ts
|
|
2995
|
+
import { Canvas } from "skia-canvas";
|
|
2996
|
+
var DEFAULT_OPTIONS = {
|
|
2997
|
+
colorBy: "auto",
|
|
2998
|
+
palette: "standard",
|
|
2999
|
+
markerShape: "circle",
|
|
3000
|
+
markerSize: 4,
|
|
3001
|
+
lineWidth: 2,
|
|
3002
|
+
alpha: 1,
|
|
3003
|
+
showNodes: true,
|
|
3004
|
+
showEdges: true,
|
|
3005
|
+
scale: 1,
|
|
3006
|
+
background: "transparent"
|
|
3007
|
+
};
|
|
3008
|
+
var DEFAULT_COLOR = PALETTES.standard[0];
|
|
3009
|
+
async function renderImage(source, options = {}) {
|
|
3010
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
3011
|
+
const { instances, skeleton, frameSize, frameIdx, tracks } = extractSourceData(source, opts);
|
|
3012
|
+
if (instances.length === 0 && !opts.image) {
|
|
3013
|
+
throw new Error(
|
|
3014
|
+
"No instances to render and no background image provided"
|
|
3015
|
+
);
|
|
3016
|
+
}
|
|
3017
|
+
const width = opts.image?.width ?? opts.width ?? frameSize[0];
|
|
3018
|
+
const height = opts.image?.height ?? opts.height ?? frameSize[1];
|
|
3019
|
+
if (!width || !height) {
|
|
3020
|
+
throw new Error(
|
|
3021
|
+
"Cannot determine frame size. Provide image, width/height options, or ensure source has frame data."
|
|
3022
|
+
);
|
|
3023
|
+
}
|
|
3024
|
+
const scaledWidth = Math.round(width * opts.scale);
|
|
3025
|
+
const scaledHeight = Math.round(height * opts.scale);
|
|
3026
|
+
const canvas = new Canvas(scaledWidth, scaledHeight);
|
|
3027
|
+
const ctx = canvas.getContext("2d");
|
|
3028
|
+
if (opts.image) {
|
|
3029
|
+
ctx.putImageData(opts.image, 0, 0);
|
|
3030
|
+
if (opts.scale !== 1) {
|
|
3031
|
+
const tempCanvas = new Canvas(opts.image.width, opts.image.height);
|
|
3032
|
+
const tempCtx = tempCanvas.getContext("2d");
|
|
3033
|
+
tempCtx.putImageData(opts.image, 0, 0);
|
|
3034
|
+
ctx.clearRect(0, 0, scaledWidth, scaledHeight);
|
|
3035
|
+
ctx.drawImage(tempCanvas, 0, 0, scaledWidth, scaledHeight);
|
|
3036
|
+
}
|
|
3037
|
+
} else if (opts.background !== "transparent") {
|
|
3038
|
+
const bgColor = resolveColor(opts.background);
|
|
3039
|
+
ctx.fillStyle = rgbToCSS(bgColor);
|
|
3040
|
+
ctx.fillRect(0, 0, scaledWidth, scaledHeight);
|
|
3041
|
+
}
|
|
3042
|
+
const edgeInds = skeleton?.edgeIndices ?? [];
|
|
3043
|
+
const nodeNames = skeleton?.nodeNames ?? [];
|
|
3044
|
+
const hasTracks = instances.some((inst) => inst.track != null);
|
|
3045
|
+
const colorScheme = determineColorScheme(opts.colorBy, hasTracks, true);
|
|
3046
|
+
const colors = buildColorMap(
|
|
3047
|
+
colorScheme,
|
|
3048
|
+
instances,
|
|
3049
|
+
nodeNames.length,
|
|
3050
|
+
opts.palette,
|
|
3051
|
+
tracks
|
|
3052
|
+
);
|
|
3053
|
+
const renderCtx = new RenderContext(
|
|
3054
|
+
ctx,
|
|
3055
|
+
frameIdx,
|
|
3056
|
+
[width, height],
|
|
3057
|
+
instances,
|
|
3058
|
+
edgeInds,
|
|
3059
|
+
nodeNames,
|
|
3060
|
+
opts.scale,
|
|
3061
|
+
[0, 0]
|
|
3062
|
+
);
|
|
3063
|
+
if (opts.preRenderCallback) {
|
|
3064
|
+
opts.preRenderCallback(renderCtx);
|
|
3065
|
+
}
|
|
3066
|
+
const drawMarker = getMarkerFunction(opts.markerShape);
|
|
3067
|
+
const scaledMarkerSize = opts.markerSize * opts.scale;
|
|
3068
|
+
const scaledLineWidth = opts.lineWidth * opts.scale;
|
|
3069
|
+
for (let instIdx = 0; instIdx < instances.length; instIdx++) {
|
|
3070
|
+
const instance = instances[instIdx];
|
|
3071
|
+
const points = getInstancePoints(instance);
|
|
3072
|
+
const instanceColor = colors.instanceColors?.[instIdx] ?? colors.instanceColors?.[0] ?? DEFAULT_COLOR;
|
|
3073
|
+
if (opts.showEdges) {
|
|
3074
|
+
for (const [srcIdx, dstIdx] of edgeInds) {
|
|
3075
|
+
const srcPt = points[srcIdx];
|
|
3076
|
+
const dstPt = points[dstIdx];
|
|
3077
|
+
if (!srcPt || !dstPt) continue;
|
|
3078
|
+
const [x1, y1] = srcPt;
|
|
3079
|
+
const [x2, y2] = dstPt;
|
|
3080
|
+
if (isNaN(x1) || isNaN(y1) || isNaN(x2) || isNaN(y2)) {
|
|
3081
|
+
continue;
|
|
3082
|
+
}
|
|
3083
|
+
const edgeColor = colorScheme === "node" ? colors.nodeColors?.[dstIdx] ?? instanceColor : instanceColor;
|
|
3084
|
+
ctx.strokeStyle = rgbToCSS(edgeColor, opts.alpha);
|
|
3085
|
+
ctx.lineWidth = scaledLineWidth;
|
|
3086
|
+
ctx.lineCap = "round";
|
|
3087
|
+
ctx.beginPath();
|
|
3088
|
+
ctx.moveTo(x1 * opts.scale, y1 * opts.scale);
|
|
3089
|
+
ctx.lineTo(x2 * opts.scale, y2 * opts.scale);
|
|
3090
|
+
ctx.stroke();
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
if (opts.showNodes) {
|
|
3094
|
+
for (let nodeIdx = 0; nodeIdx < points.length; nodeIdx++) {
|
|
3095
|
+
const pt = points[nodeIdx];
|
|
3096
|
+
if (!pt) continue;
|
|
3097
|
+
const [x, y] = pt;
|
|
3098
|
+
if (isNaN(x) || isNaN(y)) {
|
|
3099
|
+
continue;
|
|
3100
|
+
}
|
|
3101
|
+
const nodeColor = colorScheme === "node" ? colors.nodeColors?.[nodeIdx] ?? instanceColor : instanceColor;
|
|
3102
|
+
drawMarker(
|
|
3103
|
+
ctx,
|
|
3104
|
+
x * opts.scale,
|
|
3105
|
+
y * opts.scale,
|
|
3106
|
+
scaledMarkerSize,
|
|
3107
|
+
rgbToCSS(nodeColor, opts.alpha)
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
if (opts.perInstanceCallback) {
|
|
3112
|
+
const trackIdx = instance.track ? tracks.indexOf(instance.track) : null;
|
|
3113
|
+
const instCtx = new InstanceContext(
|
|
3114
|
+
ctx,
|
|
3115
|
+
instIdx,
|
|
3116
|
+
points,
|
|
3117
|
+
edgeInds,
|
|
3118
|
+
nodeNames,
|
|
3119
|
+
trackIdx !== -1 ? trackIdx : null,
|
|
3120
|
+
instance.track?.name ?? null,
|
|
3121
|
+
"score" in instance ? instance.score : null,
|
|
3122
|
+
opts.scale,
|
|
3123
|
+
[0, 0]
|
|
3124
|
+
);
|
|
3125
|
+
opts.perInstanceCallback(instCtx);
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
if (opts.postRenderCallback) {
|
|
3129
|
+
opts.postRenderCallback(renderCtx);
|
|
3130
|
+
}
|
|
3131
|
+
return ctx.getImageData(0, 0, scaledWidth, scaledHeight);
|
|
3132
|
+
}
|
|
3133
|
+
function extractSourceData(source, options) {
|
|
3134
|
+
if (Array.isArray(source)) {
|
|
3135
|
+
const instances = source;
|
|
3136
|
+
const skeleton2 = instances.length > 0 ? instances[0].skeleton : null;
|
|
3137
|
+
const trackSet = /* @__PURE__ */ new Set();
|
|
3138
|
+
for (const inst of instances) {
|
|
3139
|
+
if (inst.track) trackSet.add(inst.track);
|
|
3140
|
+
}
|
|
3141
|
+
const tracks = Array.from(trackSet);
|
|
3142
|
+
return {
|
|
3143
|
+
instances,
|
|
3144
|
+
skeleton: skeleton2,
|
|
3145
|
+
frameSize: [options.width ?? 0, options.height ?? 0],
|
|
3146
|
+
frameIdx: 0,
|
|
3147
|
+
tracks
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
if ("instances" in source && "frameIdx" in source && !("labeledFrames" in source)) {
|
|
3151
|
+
const frame = source;
|
|
3152
|
+
const skeleton2 = frame.instances.length > 0 ? frame.instances[0].skeleton : null;
|
|
3153
|
+
const trackSet = /* @__PURE__ */ new Set();
|
|
3154
|
+
for (const inst of frame.instances) {
|
|
3155
|
+
if (inst.track) trackSet.add(inst.track);
|
|
3156
|
+
}
|
|
3157
|
+
const tracks = Array.from(trackSet);
|
|
3158
|
+
let frameSize2 = [options.width ?? 0, options.height ?? 0];
|
|
3159
|
+
if (frame.video) {
|
|
3160
|
+
const video = frame.video;
|
|
3161
|
+
if ("width" in video && "height" in video) {
|
|
3162
|
+
const w = video.width;
|
|
3163
|
+
const h = video.height;
|
|
3164
|
+
if (w && h) {
|
|
3165
|
+
frameSize2 = [w, h];
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
return {
|
|
3170
|
+
instances: frame.instances,
|
|
3171
|
+
skeleton: skeleton2,
|
|
3172
|
+
frameSize: frameSize2,
|
|
3173
|
+
frameIdx: frame.frameIdx,
|
|
3174
|
+
tracks
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
const labels = source;
|
|
3178
|
+
if (labels.labeledFrames.length === 0) {
|
|
3179
|
+
return {
|
|
3180
|
+
instances: [],
|
|
3181
|
+
skeleton: labels.skeletons?.[0] ?? null,
|
|
3182
|
+
frameSize: [options.width ?? 0, options.height ?? 0],
|
|
3183
|
+
frameIdx: 0,
|
|
3184
|
+
tracks: labels.tracks ?? []
|
|
3185
|
+
};
|
|
3186
|
+
}
|
|
3187
|
+
const firstFrame = labels.labeledFrames[0];
|
|
3188
|
+
const skeleton = labels.skeletons?.[0] ?? (firstFrame.instances.length > 0 ? firstFrame.instances[0].skeleton : null);
|
|
3189
|
+
let frameSize = [options.width ?? 0, options.height ?? 0];
|
|
3190
|
+
if (firstFrame.video) {
|
|
3191
|
+
const video = firstFrame.video;
|
|
3192
|
+
if ("width" in video && "height" in video) {
|
|
3193
|
+
const w = video.width;
|
|
3194
|
+
const h = video.height;
|
|
3195
|
+
if (w && h) {
|
|
3196
|
+
frameSize = [w, h];
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
return {
|
|
3201
|
+
instances: firstFrame.instances,
|
|
3202
|
+
skeleton,
|
|
3203
|
+
frameSize,
|
|
3204
|
+
frameIdx: firstFrame.frameIdx,
|
|
3205
|
+
tracks: labels.tracks ?? []
|
|
3206
|
+
};
|
|
3207
|
+
}
|
|
3208
|
+
function getInstancePoints(instance) {
|
|
3209
|
+
return instance.points.map((point) => [point.xy[0], point.xy[1]]);
|
|
3210
|
+
}
|
|
3211
|
+
function buildColorMap(scheme, instances, nNodes, paletteName, tracks) {
|
|
3212
|
+
switch (scheme) {
|
|
3213
|
+
case "instance":
|
|
3214
|
+
return {
|
|
3215
|
+
instanceColors: getPalette(
|
|
3216
|
+
paletteName,
|
|
3217
|
+
Math.max(1, instances.length)
|
|
3218
|
+
)
|
|
3219
|
+
};
|
|
3220
|
+
case "track": {
|
|
3221
|
+
const nTracks = Math.max(1, tracks.length);
|
|
3222
|
+
const trackPalette = getPalette(paletteName, nTracks);
|
|
3223
|
+
const instanceColors = instances.map((inst) => {
|
|
3224
|
+
if (inst.track) {
|
|
3225
|
+
const trackIdx = tracks.indexOf(inst.track);
|
|
3226
|
+
if (trackIdx >= 0) {
|
|
3227
|
+
return trackPalette[trackIdx % trackPalette.length];
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
return trackPalette[0];
|
|
3231
|
+
});
|
|
3232
|
+
return { instanceColors };
|
|
3233
|
+
}
|
|
3234
|
+
case "node":
|
|
3235
|
+
return {
|
|
3236
|
+
instanceColors: getPalette(paletteName, 1),
|
|
3237
|
+
nodeColors: getPalette(paletteName, Math.max(1, nNodes))
|
|
3238
|
+
};
|
|
3239
|
+
default:
|
|
3240
|
+
return {
|
|
3241
|
+
instanceColors: getPalette(
|
|
3242
|
+
paletteName,
|
|
3243
|
+
Math.max(1, instances.length)
|
|
3244
|
+
)
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
async function toPNG(imageData) {
|
|
3249
|
+
const canvas = new Canvas(imageData.width, imageData.height);
|
|
3250
|
+
const ctx = canvas.getContext("2d");
|
|
3251
|
+
ctx.putImageData(imageData, 0, 0);
|
|
3252
|
+
return canvas.toBuffer("png");
|
|
3253
|
+
}
|
|
3254
|
+
async function toJPEG(imageData, quality = 0.9) {
|
|
3255
|
+
const canvas = new Canvas(imageData.width, imageData.height);
|
|
3256
|
+
const ctx = canvas.getContext("2d");
|
|
3257
|
+
ctx.putImageData(imageData, 0, 0);
|
|
3258
|
+
return canvas.toBuffer("jpeg", { quality });
|
|
3259
|
+
}
|
|
3260
|
+
function toDataURL(imageData, format = "png") {
|
|
3261
|
+
const canvas = new Canvas(imageData.width, imageData.height);
|
|
3262
|
+
const ctx = canvas.getContext("2d");
|
|
3263
|
+
ctx.putImageData(imageData, 0, 0);
|
|
3264
|
+
return canvas.toDataURL(`image/${format}`);
|
|
3265
|
+
}
|
|
3266
|
+
async function saveImage(imageData, path) {
|
|
3267
|
+
const canvas = new Canvas(imageData.width, imageData.height);
|
|
3268
|
+
const ctx = canvas.getContext("2d");
|
|
3269
|
+
ctx.putImageData(imageData, 0, 0);
|
|
3270
|
+
await canvas.saveAs(path);
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
// src/rendering/video.ts
|
|
3274
|
+
import { spawn } from "child_process";
|
|
3275
|
+
async function checkFfmpeg() {
|
|
3276
|
+
return new Promise((resolve) => {
|
|
3277
|
+
const proc = spawn("ffmpeg", ["-version"]);
|
|
3278
|
+
proc.on("error", () => resolve(false));
|
|
3279
|
+
proc.on("close", (code) => resolve(code === 0));
|
|
3280
|
+
});
|
|
3281
|
+
}
|
|
3282
|
+
async function renderVideo(source, outputPath, options = {}) {
|
|
3283
|
+
const hasFfmpeg = await checkFfmpeg();
|
|
3284
|
+
if (!hasFfmpeg) {
|
|
3285
|
+
throw new Error(
|
|
3286
|
+
"ffmpeg not found. Please install ffmpeg and ensure it is in your PATH.\nInstallation: https://ffmpeg.org/download.html"
|
|
3287
|
+
);
|
|
3288
|
+
}
|
|
3289
|
+
const frames = Array.isArray(source) ? source : source.labeledFrames;
|
|
3290
|
+
let selectedFrames = frames;
|
|
3291
|
+
if (options.frameInds) {
|
|
3292
|
+
selectedFrames = options.frameInds.map((i) => frames[i]).filter((f) => f !== void 0);
|
|
3293
|
+
} else if (options.start !== void 0 || options.end !== void 0) {
|
|
3294
|
+
const start = options.start ?? 0;
|
|
3295
|
+
const end = options.end ?? frames.length;
|
|
3296
|
+
selectedFrames = frames.slice(start, end);
|
|
3297
|
+
}
|
|
3298
|
+
if (selectedFrames.length === 0) {
|
|
3299
|
+
throw new Error("No frames to render");
|
|
3300
|
+
}
|
|
3301
|
+
const firstImage = await renderImage(selectedFrames[0], options);
|
|
3302
|
+
const width = firstImage.width;
|
|
3303
|
+
const height = firstImage.height;
|
|
3304
|
+
const fps = options.fps ?? 30;
|
|
3305
|
+
const codec = options.codec ?? "libx264";
|
|
3306
|
+
const crf = options.crf ?? 25;
|
|
3307
|
+
const preset = options.preset ?? "superfast";
|
|
3308
|
+
const ffmpegArgs = [
|
|
3309
|
+
"-y",
|
|
3310
|
+
// Overwrite output
|
|
3311
|
+
"-f",
|
|
3312
|
+
"rawvideo",
|
|
3313
|
+
// Input format
|
|
3314
|
+
"-pix_fmt",
|
|
3315
|
+
"rgba",
|
|
3316
|
+
// Input pixel format
|
|
3317
|
+
"-s",
|
|
3318
|
+
`${width}x${height}`,
|
|
3319
|
+
// Frame size
|
|
3320
|
+
"-r",
|
|
3321
|
+
String(fps),
|
|
3322
|
+
// Frame rate
|
|
3323
|
+
"-i",
|
|
3324
|
+
"pipe:0",
|
|
3325
|
+
// Read from stdin
|
|
3326
|
+
"-c:v",
|
|
3327
|
+
codec,
|
|
3328
|
+
// Video codec
|
|
3329
|
+
"-pix_fmt",
|
|
3330
|
+
"yuv420p"
|
|
3331
|
+
// Output pixel format
|
|
3332
|
+
];
|
|
3333
|
+
if (codec === "libx264") {
|
|
3334
|
+
ffmpegArgs.push("-crf", String(crf), "-preset", preset);
|
|
3335
|
+
}
|
|
3336
|
+
ffmpegArgs.push(outputPath);
|
|
3337
|
+
const ffmpeg = spawn("ffmpeg", ffmpegArgs, {
|
|
3338
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3339
|
+
});
|
|
3340
|
+
let ffmpegError = null;
|
|
3341
|
+
ffmpeg.on("error", (err) => {
|
|
3342
|
+
ffmpegError = err;
|
|
3343
|
+
});
|
|
3344
|
+
const total = selectedFrames.length;
|
|
3345
|
+
for (let i = 0; i < selectedFrames.length; i++) {
|
|
3346
|
+
if (ffmpegError) {
|
|
3347
|
+
throw ffmpegError;
|
|
3348
|
+
}
|
|
3349
|
+
const frame = selectedFrames[i];
|
|
3350
|
+
const imageData = await renderImage(frame, options);
|
|
3351
|
+
const buffer = Buffer.from(imageData.data.buffer);
|
|
3352
|
+
if (!ffmpeg.stdin) {
|
|
3353
|
+
throw new Error("ffmpeg stdin not available");
|
|
3354
|
+
}
|
|
3355
|
+
const canWrite = ffmpeg.stdin.write(buffer);
|
|
3356
|
+
if (!canWrite) {
|
|
3357
|
+
await new Promise(
|
|
3358
|
+
(resolve) => ffmpeg.stdin?.once("drain", resolve)
|
|
3359
|
+
);
|
|
3360
|
+
}
|
|
3361
|
+
if (options.onProgress) {
|
|
3362
|
+
options.onProgress(i + 1, total);
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
ffmpeg.stdin?.end();
|
|
3366
|
+
return new Promise((resolve, reject) => {
|
|
3367
|
+
ffmpeg.on("close", (code) => {
|
|
3368
|
+
if (code === 0) {
|
|
3369
|
+
resolve();
|
|
3370
|
+
} else {
|
|
3371
|
+
reject(new Error(`ffmpeg exited with code ${code}`));
|
|
3372
|
+
}
|
|
3373
|
+
});
|
|
3374
|
+
ffmpeg.on("error", reject);
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
2656
3377
|
export {
|
|
2657
3378
|
Camera,
|
|
2658
3379
|
CameraGroup,
|
|
2659
3380
|
Edge,
|
|
2660
3381
|
FrameGroup,
|
|
2661
3382
|
Instance,
|
|
3383
|
+
InstanceContext,
|
|
2662
3384
|
InstanceGroup,
|
|
2663
3385
|
LabeledFrame,
|
|
2664
3386
|
Labels,
|
|
2665
3387
|
LabelsSet,
|
|
3388
|
+
MARKER_FUNCTIONS,
|
|
2666
3389
|
Mp4BoxVideoBackend,
|
|
3390
|
+
NAMED_COLORS,
|
|
2667
3391
|
Node,
|
|
3392
|
+
PALETTES,
|
|
2668
3393
|
PredictedInstance,
|
|
2669
3394
|
RecordingSession,
|
|
3395
|
+
RenderContext,
|
|
2670
3396
|
Skeleton,
|
|
3397
|
+
StreamingH5File,
|
|
2671
3398
|
SuggestionFrame,
|
|
2672
3399
|
Symmetry,
|
|
2673
3400
|
Track,
|
|
2674
3401
|
Video,
|
|
3402
|
+
checkFfmpeg,
|
|
2675
3403
|
decodeYamlSkeleton,
|
|
3404
|
+
determineColorScheme,
|
|
3405
|
+
drawCircle,
|
|
3406
|
+
drawCross,
|
|
3407
|
+
drawDiamond,
|
|
3408
|
+
drawSquare,
|
|
3409
|
+
drawTriangle,
|
|
2676
3410
|
encodeYamlSkeleton,
|
|
2677
3411
|
fromDict,
|
|
2678
3412
|
fromNumpy,
|
|
3413
|
+
getMarkerFunction,
|
|
3414
|
+
getPalette,
|
|
3415
|
+
isStreamingSupported,
|
|
2679
3416
|
labelsFromNumpy,
|
|
2680
3417
|
loadSlp,
|
|
2681
3418
|
loadVideo,
|
|
2682
3419
|
makeCameraFromDict,
|
|
3420
|
+
openStreamingH5,
|
|
2683
3421
|
pointsEmpty,
|
|
2684
3422
|
pointsFromArray,
|
|
2685
3423
|
pointsFromDict,
|
|
2686
3424
|
predictedPointsEmpty,
|
|
2687
3425
|
predictedPointsFromArray,
|
|
2688
3426
|
predictedPointsFromDict,
|
|
3427
|
+
renderImage,
|
|
3428
|
+
renderVideo,
|
|
3429
|
+
resolveColor,
|
|
3430
|
+
rgbToCSS,
|
|
2689
3431
|
rodriguesTransformation,
|
|
3432
|
+
saveImage,
|
|
2690
3433
|
saveSlp,
|
|
3434
|
+
toDataURL,
|
|
2691
3435
|
toDict,
|
|
2692
|
-
|
|
3436
|
+
toJPEG,
|
|
3437
|
+
toNumpy,
|
|
3438
|
+
toPNG
|
|
2693
3439
|
};
|