@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/dist/index.js CHANGED
@@ -1,238 +1,20 @@
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
- }
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 = readSkeletons(metadataJson);
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
- toNumpy
3436
+ toJPEG,
3437
+ toNumpy,
3438
+ toPNG
2693
3439
  };