@talmolab/sleap-io.js 0.1.2 → 0.1.3

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;
@@ -1803,7 +1465,7 @@ async function readSlp(source, options) {
1803
1465
  const formatId = Number(metadataAttrs["format_id"]?.value ?? metadataAttrs["format_id"] ?? 1);
1804
1466
  const metadataJson = parseJsonAttr(metadataAttrs["json"]);
1805
1467
  const labelsPath = typeof source === "string" ? source : options?.h5?.filenameHint ?? "slp-data.slp";
1806
- const skeletons = readSkeletons(metadataJson);
1468
+ const skeletons = parseSkeletons(metadataJson);
1807
1469
  const tracks = readTracks(file.get("tracks_json"));
1808
1470
  const videos = await readVideos(file.get("videos_json"), labelsPath, options?.openVideos ?? true, file);
1809
1471
  const suggestions = readSuggestions(file.get("suggestions_json"), videos);
@@ -1835,85 +1497,6 @@ async function readSlp(source, options) {
1835
1497
  close();
1836
1498
  }
1837
1499
  }
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
1500
  function readTracks(dataset) {
1918
1501
  if (!dataset) return [];
1919
1502
  const values = dataset.value ?? [];
@@ -2653,29 +2236,804 @@ function encodeYamlSkeleton(skeletons) {
2653
2236
  });
2654
2237
  return YAML.stringify(payload);
2655
2238
  }
2239
+
2240
+ // src/rendering/colors.ts
2241
+ var NAMED_COLORS = {
2242
+ black: [0, 0, 0],
2243
+ white: [255, 255, 255],
2244
+ red: [255, 0, 0],
2245
+ green: [0, 255, 0],
2246
+ blue: [0, 0, 255],
2247
+ yellow: [255, 255, 0],
2248
+ cyan: [0, 255, 255],
2249
+ magenta: [255, 0, 255],
2250
+ gray: [128, 128, 128],
2251
+ grey: [128, 128, 128],
2252
+ orange: [255, 165, 0],
2253
+ purple: [128, 0, 128],
2254
+ pink: [255, 192, 203],
2255
+ brown: [139, 69, 19]
2256
+ };
2257
+ var PALETTES = {
2258
+ // MATLAB default colors
2259
+ standard: [
2260
+ [0, 114, 189],
2261
+ [217, 83, 25],
2262
+ [237, 177, 32],
2263
+ [126, 47, 142],
2264
+ [119, 172, 48],
2265
+ [77, 190, 238],
2266
+ [162, 20, 47]
2267
+ ],
2268
+ // Tableau 10
2269
+ tableau10: [
2270
+ [31, 119, 180],
2271
+ [255, 127, 14],
2272
+ [44, 160, 44],
2273
+ [214, 39, 40],
2274
+ [148, 103, 189],
2275
+ [140, 86, 75],
2276
+ [227, 119, 194],
2277
+ [127, 127, 127],
2278
+ [188, 189, 34],
2279
+ [23, 190, 207]
2280
+ ],
2281
+ // High-contrast distinct colors (Glasbey-inspired, for many instances)
2282
+ distinct: [
2283
+ [230, 25, 75],
2284
+ [60, 180, 75],
2285
+ [255, 225, 25],
2286
+ [67, 99, 216],
2287
+ [245, 130, 49],
2288
+ [145, 30, 180],
2289
+ [66, 212, 244],
2290
+ [240, 50, 230],
2291
+ [191, 239, 69],
2292
+ [250, 190, 212],
2293
+ [70, 153, 144],
2294
+ [220, 190, 255],
2295
+ [154, 99, 36],
2296
+ [255, 250, 200],
2297
+ [128, 0, 0],
2298
+ [170, 255, 195],
2299
+ [128, 128, 0],
2300
+ [255, 216, 177],
2301
+ [0, 0, 117],
2302
+ [169, 169, 169]
2303
+ ],
2304
+ // Viridis (10 samples)
2305
+ viridis: [
2306
+ [68, 1, 84],
2307
+ [72, 40, 120],
2308
+ [62, 74, 137],
2309
+ [49, 104, 142],
2310
+ [38, 130, 142],
2311
+ [31, 158, 137],
2312
+ [53, 183, 121],
2313
+ [110, 206, 88],
2314
+ [181, 222, 43],
2315
+ [253, 231, 37]
2316
+ ],
2317
+ // Rainbow for node coloring
2318
+ rainbow: [
2319
+ [255, 0, 0],
2320
+ [255, 127, 0],
2321
+ [255, 255, 0],
2322
+ [127, 255, 0],
2323
+ [0, 255, 0],
2324
+ [0, 255, 127],
2325
+ [0, 255, 255],
2326
+ [0, 127, 255],
2327
+ [0, 0, 255],
2328
+ [127, 0, 255],
2329
+ [255, 0, 255],
2330
+ [255, 0, 127]
2331
+ ],
2332
+ // Warm colors
2333
+ warm: [
2334
+ [255, 89, 94],
2335
+ [255, 146, 76],
2336
+ [255, 202, 58],
2337
+ [255, 154, 0],
2338
+ [255, 97, 56],
2339
+ [255, 50, 50]
2340
+ ],
2341
+ // Cool colors
2342
+ cool: [
2343
+ [67, 170, 139],
2344
+ [77, 144, 142],
2345
+ [87, 117, 144],
2346
+ [97, 90, 147],
2347
+ [107, 63, 149],
2348
+ [117, 36, 152]
2349
+ ],
2350
+ // Pastel colors
2351
+ pastel: [
2352
+ [255, 179, 186],
2353
+ [255, 223, 186],
2354
+ [255, 255, 186],
2355
+ [186, 255, 201],
2356
+ [186, 225, 255],
2357
+ [219, 186, 255]
2358
+ ],
2359
+ // Seaborn-inspired
2360
+ seaborn: [
2361
+ [76, 114, 176],
2362
+ [221, 132, 82],
2363
+ [85, 168, 104],
2364
+ [196, 78, 82],
2365
+ [129, 114, 179],
2366
+ [147, 120, 96],
2367
+ [218, 139, 195],
2368
+ [140, 140, 140],
2369
+ [204, 185, 116],
2370
+ [100, 181, 205]
2371
+ ]
2372
+ };
2373
+ function getPalette(name, n) {
2374
+ const palette = PALETTES[name];
2375
+ if (!palette) {
2376
+ throw new Error(`Unknown palette: ${name}`);
2377
+ }
2378
+ if (n <= palette.length) {
2379
+ return palette.slice(0, n);
2380
+ }
2381
+ return Array.from({ length: n }, (_, i) => palette[i % palette.length]);
2382
+ }
2383
+ function resolveColor(color) {
2384
+ if (Array.isArray(color)) {
2385
+ if (color.length >= 3) {
2386
+ return [color[0], color[1], color[2]];
2387
+ }
2388
+ throw new Error(`Invalid color array: ${color}`);
2389
+ }
2390
+ if (typeof color === "number") {
2391
+ const v = Math.round(color);
2392
+ return [v, v, v];
2393
+ }
2394
+ if (typeof color === "string") {
2395
+ const s = color.trim().toLowerCase();
2396
+ if (s in NAMED_COLORS) {
2397
+ return NAMED_COLORS[s];
2398
+ }
2399
+ if (s.startsWith("#")) {
2400
+ return hexToRgb(s);
2401
+ }
2402
+ const paletteMatch = s.match(/^(\w+)\[(\d+)\]$/);
2403
+ if (paletteMatch) {
2404
+ const [, paletteName, indexStr] = paletteMatch;
2405
+ const palette = PALETTES[paletteName];
2406
+ if (palette) {
2407
+ const index = parseInt(indexStr, 10) % palette.length;
2408
+ return palette[index];
2409
+ }
2410
+ }
2411
+ const rgbMatch = s.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
2412
+ if (rgbMatch) {
2413
+ return [
2414
+ parseInt(rgbMatch[1], 10),
2415
+ parseInt(rgbMatch[2], 10),
2416
+ parseInt(rgbMatch[3], 10)
2417
+ ];
2418
+ }
2419
+ }
2420
+ throw new Error(`Cannot resolve color: ${color}`);
2421
+ }
2422
+ function hexToRgb(hex) {
2423
+ const h = hex.replace("#", "");
2424
+ if (h.length === 3) {
2425
+ return [
2426
+ parseInt(h[0] + h[0], 16),
2427
+ parseInt(h[1] + h[1], 16),
2428
+ parseInt(h[2] + h[2], 16)
2429
+ ];
2430
+ }
2431
+ if (h.length === 6) {
2432
+ return [
2433
+ parseInt(h.slice(0, 2), 16),
2434
+ parseInt(h.slice(2, 4), 16),
2435
+ parseInt(h.slice(4, 6), 16)
2436
+ ];
2437
+ }
2438
+ throw new Error(`Invalid hex color: ${hex}`);
2439
+ }
2440
+ function rgbToCSS(rgb, alpha = 1) {
2441
+ return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha})`;
2442
+ }
2443
+ function determineColorScheme(scheme, hasTracks, isSingleImage) {
2444
+ if (scheme !== "auto") {
2445
+ return scheme;
2446
+ }
2447
+ if (hasTracks) {
2448
+ return "track";
2449
+ }
2450
+ if (isSingleImage) {
2451
+ return "instance";
2452
+ }
2453
+ return "node";
2454
+ }
2455
+
2456
+ // src/rendering/shapes.ts
2457
+ function drawCircle(ctx, x, y, size, fillColor, edgeColor, edgeWidth = 1) {
2458
+ ctx.beginPath();
2459
+ ctx.arc(x, y, size, 0, Math.PI * 2);
2460
+ ctx.fillStyle = fillColor;
2461
+ ctx.fill();
2462
+ if (edgeColor) {
2463
+ ctx.strokeStyle = edgeColor;
2464
+ ctx.lineWidth = edgeWidth;
2465
+ ctx.stroke();
2466
+ }
2467
+ }
2468
+ function drawSquare(ctx, x, y, size, fillColor, edgeColor, edgeWidth = 1) {
2469
+ const half = size;
2470
+ ctx.fillStyle = fillColor;
2471
+ ctx.fillRect(x - half, y - half, half * 2, half * 2);
2472
+ if (edgeColor) {
2473
+ ctx.strokeStyle = edgeColor;
2474
+ ctx.lineWidth = edgeWidth;
2475
+ ctx.strokeRect(x - half, y - half, half * 2, half * 2);
2476
+ }
2477
+ }
2478
+ function drawDiamond(ctx, x, y, size, fillColor, edgeColor, edgeWidth = 1) {
2479
+ ctx.beginPath();
2480
+ ctx.moveTo(x, y - size);
2481
+ ctx.lineTo(x + size, y);
2482
+ ctx.lineTo(x, y + size);
2483
+ ctx.lineTo(x - size, y);
2484
+ ctx.closePath();
2485
+ ctx.fillStyle = fillColor;
2486
+ ctx.fill();
2487
+ if (edgeColor) {
2488
+ ctx.strokeStyle = edgeColor;
2489
+ ctx.lineWidth = edgeWidth;
2490
+ ctx.stroke();
2491
+ }
2492
+ }
2493
+ function drawTriangle(ctx, x, y, size, fillColor, edgeColor, edgeWidth = 1) {
2494
+ const h = size * 0.866;
2495
+ ctx.beginPath();
2496
+ ctx.moveTo(x, y - size);
2497
+ ctx.lineTo(x + size, y + h);
2498
+ ctx.lineTo(x - size, y + h);
2499
+ ctx.closePath();
2500
+ ctx.fillStyle = fillColor;
2501
+ ctx.fill();
2502
+ if (edgeColor) {
2503
+ ctx.strokeStyle = edgeColor;
2504
+ ctx.lineWidth = edgeWidth;
2505
+ ctx.stroke();
2506
+ }
2507
+ }
2508
+ function drawCross(ctx, x, y, size, fillColor, _edgeColor, edgeWidth = 2) {
2509
+ ctx.strokeStyle = fillColor;
2510
+ ctx.lineWidth = edgeWidth;
2511
+ ctx.lineCap = "round";
2512
+ ctx.beginPath();
2513
+ ctx.moveTo(x - size, y);
2514
+ ctx.lineTo(x + size, y);
2515
+ ctx.stroke();
2516
+ ctx.beginPath();
2517
+ ctx.moveTo(x, y - size);
2518
+ ctx.lineTo(x, y + size);
2519
+ ctx.stroke();
2520
+ }
2521
+ var MARKER_FUNCTIONS = {
2522
+ circle: drawCircle,
2523
+ square: drawSquare,
2524
+ diamond: drawDiamond,
2525
+ triangle: drawTriangle,
2526
+ cross: drawCross
2527
+ };
2528
+ function getMarkerFunction(shape) {
2529
+ return MARKER_FUNCTIONS[shape];
2530
+ }
2531
+
2532
+ // src/rendering/context.ts
2533
+ var RenderContext = class {
2534
+ constructor(canvas, frameIdx, frameSize, instances, skeletonEdges, nodeNames, scale = 1, offset = [0, 0]) {
2535
+ this.canvas = canvas;
2536
+ this.frameIdx = frameIdx;
2537
+ this.frameSize = frameSize;
2538
+ this.instances = instances;
2539
+ this.skeletonEdges = skeletonEdges;
2540
+ this.nodeNames = nodeNames;
2541
+ this.scale = scale;
2542
+ this.offset = offset;
2543
+ }
2544
+ /**
2545
+ * Transform world coordinates to canvas coordinates.
2546
+ */
2547
+ worldToCanvas(x, y) {
2548
+ return [
2549
+ (x - this.offset[0]) * this.scale,
2550
+ (y - this.offset[1]) * this.scale
2551
+ ];
2552
+ }
2553
+ };
2554
+ var InstanceContext = class {
2555
+ constructor(canvas, instanceIdx, points, skeletonEdges, nodeNames, trackIdx = null, trackName = null, confidence = null, scale = 1, offset = [0, 0]) {
2556
+ this.canvas = canvas;
2557
+ this.instanceIdx = instanceIdx;
2558
+ this.points = points;
2559
+ this.skeletonEdges = skeletonEdges;
2560
+ this.nodeNames = nodeNames;
2561
+ this.trackIdx = trackIdx;
2562
+ this.trackName = trackName;
2563
+ this.confidence = confidence;
2564
+ this.scale = scale;
2565
+ this.offset = offset;
2566
+ }
2567
+ /**
2568
+ * Transform world coordinates to canvas coordinates.
2569
+ */
2570
+ worldToCanvas(x, y) {
2571
+ return [
2572
+ (x - this.offset[0]) * this.scale,
2573
+ (y - this.offset[1]) * this.scale
2574
+ ];
2575
+ }
2576
+ /**
2577
+ * Get centroid of valid (non-NaN) points.
2578
+ */
2579
+ getCentroid() {
2580
+ let sumX = 0, sumY = 0, count = 0;
2581
+ for (const pt of this.points) {
2582
+ const x = pt[0];
2583
+ const y = pt[1];
2584
+ if (!isNaN(x) && !isNaN(y)) {
2585
+ sumX += x;
2586
+ sumY += y;
2587
+ count++;
2588
+ }
2589
+ }
2590
+ if (count === 0) return null;
2591
+ return [sumX / count, sumY / count];
2592
+ }
2593
+ /**
2594
+ * Get bounding box of valid points.
2595
+ * Returns [x1, y1, x2, y2] or null if no valid points.
2596
+ */
2597
+ getBbox() {
2598
+ let minX = Infinity, minY = Infinity;
2599
+ let maxX = -Infinity, maxY = -Infinity;
2600
+ let hasValid = false;
2601
+ for (const pt of this.points) {
2602
+ const x = pt[0];
2603
+ const y = pt[1];
2604
+ if (!isNaN(x) && !isNaN(y)) {
2605
+ minX = Math.min(minX, x);
2606
+ minY = Math.min(minY, y);
2607
+ maxX = Math.max(maxX, x);
2608
+ maxY = Math.max(maxY, y);
2609
+ hasValid = true;
2610
+ }
2611
+ }
2612
+ if (!hasValid) return null;
2613
+ return [minX, minY, maxX, maxY];
2614
+ }
2615
+ };
2616
+
2617
+ // src/rendering/render.ts
2618
+ import { Canvas } from "skia-canvas";
2619
+ var DEFAULT_OPTIONS = {
2620
+ colorBy: "auto",
2621
+ palette: "standard",
2622
+ markerShape: "circle",
2623
+ markerSize: 4,
2624
+ lineWidth: 2,
2625
+ alpha: 1,
2626
+ showNodes: true,
2627
+ showEdges: true,
2628
+ scale: 1,
2629
+ background: "transparent"
2630
+ };
2631
+ var DEFAULT_COLOR = PALETTES.standard[0];
2632
+ async function renderImage(source, options = {}) {
2633
+ const opts = { ...DEFAULT_OPTIONS, ...options };
2634
+ const { instances, skeleton, frameSize, frameIdx, tracks } = extractSourceData(source, opts);
2635
+ if (instances.length === 0 && !opts.image) {
2636
+ throw new Error(
2637
+ "No instances to render and no background image provided"
2638
+ );
2639
+ }
2640
+ const width = opts.image?.width ?? opts.width ?? frameSize[0];
2641
+ const height = opts.image?.height ?? opts.height ?? frameSize[1];
2642
+ if (!width || !height) {
2643
+ throw new Error(
2644
+ "Cannot determine frame size. Provide image, width/height options, or ensure source has frame data."
2645
+ );
2646
+ }
2647
+ const scaledWidth = Math.round(width * opts.scale);
2648
+ const scaledHeight = Math.round(height * opts.scale);
2649
+ const canvas = new Canvas(scaledWidth, scaledHeight);
2650
+ const ctx = canvas.getContext("2d");
2651
+ if (opts.image) {
2652
+ ctx.putImageData(opts.image, 0, 0);
2653
+ if (opts.scale !== 1) {
2654
+ const tempCanvas = new Canvas(opts.image.width, opts.image.height);
2655
+ const tempCtx = tempCanvas.getContext("2d");
2656
+ tempCtx.putImageData(opts.image, 0, 0);
2657
+ ctx.clearRect(0, 0, scaledWidth, scaledHeight);
2658
+ ctx.drawImage(tempCanvas, 0, 0, scaledWidth, scaledHeight);
2659
+ }
2660
+ } else if (opts.background !== "transparent") {
2661
+ const bgColor = resolveColor(opts.background);
2662
+ ctx.fillStyle = rgbToCSS(bgColor);
2663
+ ctx.fillRect(0, 0, scaledWidth, scaledHeight);
2664
+ }
2665
+ const edgeInds = skeleton?.edgeIndices ?? [];
2666
+ const nodeNames = skeleton?.nodeNames ?? [];
2667
+ const hasTracks = instances.some((inst) => inst.track != null);
2668
+ const colorScheme = determineColorScheme(opts.colorBy, hasTracks, true);
2669
+ const colors = buildColorMap(
2670
+ colorScheme,
2671
+ instances,
2672
+ nodeNames.length,
2673
+ opts.palette,
2674
+ tracks
2675
+ );
2676
+ const renderCtx = new RenderContext(
2677
+ ctx,
2678
+ frameIdx,
2679
+ [width, height],
2680
+ instances,
2681
+ edgeInds,
2682
+ nodeNames,
2683
+ opts.scale,
2684
+ [0, 0]
2685
+ );
2686
+ if (opts.preRenderCallback) {
2687
+ opts.preRenderCallback(renderCtx);
2688
+ }
2689
+ const drawMarker = getMarkerFunction(opts.markerShape);
2690
+ const scaledMarkerSize = opts.markerSize * opts.scale;
2691
+ const scaledLineWidth = opts.lineWidth * opts.scale;
2692
+ for (let instIdx = 0; instIdx < instances.length; instIdx++) {
2693
+ const instance = instances[instIdx];
2694
+ const points = getInstancePoints(instance);
2695
+ const instanceColor = colors.instanceColors?.[instIdx] ?? colors.instanceColors?.[0] ?? DEFAULT_COLOR;
2696
+ if (opts.showEdges) {
2697
+ for (const [srcIdx, dstIdx] of edgeInds) {
2698
+ const srcPt = points[srcIdx];
2699
+ const dstPt = points[dstIdx];
2700
+ if (!srcPt || !dstPt) continue;
2701
+ const [x1, y1] = srcPt;
2702
+ const [x2, y2] = dstPt;
2703
+ if (isNaN(x1) || isNaN(y1) || isNaN(x2) || isNaN(y2)) {
2704
+ continue;
2705
+ }
2706
+ const edgeColor = colorScheme === "node" ? colors.nodeColors?.[dstIdx] ?? instanceColor : instanceColor;
2707
+ ctx.strokeStyle = rgbToCSS(edgeColor, opts.alpha);
2708
+ ctx.lineWidth = scaledLineWidth;
2709
+ ctx.lineCap = "round";
2710
+ ctx.beginPath();
2711
+ ctx.moveTo(x1 * opts.scale, y1 * opts.scale);
2712
+ ctx.lineTo(x2 * opts.scale, y2 * opts.scale);
2713
+ ctx.stroke();
2714
+ }
2715
+ }
2716
+ if (opts.showNodes) {
2717
+ for (let nodeIdx = 0; nodeIdx < points.length; nodeIdx++) {
2718
+ const pt = points[nodeIdx];
2719
+ if (!pt) continue;
2720
+ const [x, y] = pt;
2721
+ if (isNaN(x) || isNaN(y)) {
2722
+ continue;
2723
+ }
2724
+ const nodeColor = colorScheme === "node" ? colors.nodeColors?.[nodeIdx] ?? instanceColor : instanceColor;
2725
+ drawMarker(
2726
+ ctx,
2727
+ x * opts.scale,
2728
+ y * opts.scale,
2729
+ scaledMarkerSize,
2730
+ rgbToCSS(nodeColor, opts.alpha)
2731
+ );
2732
+ }
2733
+ }
2734
+ if (opts.perInstanceCallback) {
2735
+ const trackIdx = instance.track ? tracks.indexOf(instance.track) : null;
2736
+ const instCtx = new InstanceContext(
2737
+ ctx,
2738
+ instIdx,
2739
+ points,
2740
+ edgeInds,
2741
+ nodeNames,
2742
+ trackIdx !== -1 ? trackIdx : null,
2743
+ instance.track?.name ?? null,
2744
+ "score" in instance ? instance.score : null,
2745
+ opts.scale,
2746
+ [0, 0]
2747
+ );
2748
+ opts.perInstanceCallback(instCtx);
2749
+ }
2750
+ }
2751
+ if (opts.postRenderCallback) {
2752
+ opts.postRenderCallback(renderCtx);
2753
+ }
2754
+ return ctx.getImageData(0, 0, scaledWidth, scaledHeight);
2755
+ }
2756
+ function extractSourceData(source, options) {
2757
+ if (Array.isArray(source)) {
2758
+ const instances = source;
2759
+ const skeleton2 = instances.length > 0 ? instances[0].skeleton : null;
2760
+ const trackSet = /* @__PURE__ */ new Set();
2761
+ for (const inst of instances) {
2762
+ if (inst.track) trackSet.add(inst.track);
2763
+ }
2764
+ const tracks = Array.from(trackSet);
2765
+ return {
2766
+ instances,
2767
+ skeleton: skeleton2,
2768
+ frameSize: [options.width ?? 0, options.height ?? 0],
2769
+ frameIdx: 0,
2770
+ tracks
2771
+ };
2772
+ }
2773
+ if ("instances" in source && "frameIdx" in source && !("labeledFrames" in source)) {
2774
+ const frame = source;
2775
+ const skeleton2 = frame.instances.length > 0 ? frame.instances[0].skeleton : null;
2776
+ const trackSet = /* @__PURE__ */ new Set();
2777
+ for (const inst of frame.instances) {
2778
+ if (inst.track) trackSet.add(inst.track);
2779
+ }
2780
+ const tracks = Array.from(trackSet);
2781
+ let frameSize2 = [options.width ?? 0, options.height ?? 0];
2782
+ if (frame.video) {
2783
+ const video = frame.video;
2784
+ if ("width" in video && "height" in video) {
2785
+ const w = video.width;
2786
+ const h = video.height;
2787
+ if (w && h) {
2788
+ frameSize2 = [w, h];
2789
+ }
2790
+ }
2791
+ }
2792
+ return {
2793
+ instances: frame.instances,
2794
+ skeleton: skeleton2,
2795
+ frameSize: frameSize2,
2796
+ frameIdx: frame.frameIdx,
2797
+ tracks
2798
+ };
2799
+ }
2800
+ const labels = source;
2801
+ if (labels.labeledFrames.length === 0) {
2802
+ return {
2803
+ instances: [],
2804
+ skeleton: labels.skeletons?.[0] ?? null,
2805
+ frameSize: [options.width ?? 0, options.height ?? 0],
2806
+ frameIdx: 0,
2807
+ tracks: labels.tracks ?? []
2808
+ };
2809
+ }
2810
+ const firstFrame = labels.labeledFrames[0];
2811
+ const skeleton = labels.skeletons?.[0] ?? (firstFrame.instances.length > 0 ? firstFrame.instances[0].skeleton : null);
2812
+ let frameSize = [options.width ?? 0, options.height ?? 0];
2813
+ if (firstFrame.video) {
2814
+ const video = firstFrame.video;
2815
+ if ("width" in video && "height" in video) {
2816
+ const w = video.width;
2817
+ const h = video.height;
2818
+ if (w && h) {
2819
+ frameSize = [w, h];
2820
+ }
2821
+ }
2822
+ }
2823
+ return {
2824
+ instances: firstFrame.instances,
2825
+ skeleton,
2826
+ frameSize,
2827
+ frameIdx: firstFrame.frameIdx,
2828
+ tracks: labels.tracks ?? []
2829
+ };
2830
+ }
2831
+ function getInstancePoints(instance) {
2832
+ return instance.points.map((point) => [point.xy[0], point.xy[1]]);
2833
+ }
2834
+ function buildColorMap(scheme, instances, nNodes, paletteName, tracks) {
2835
+ switch (scheme) {
2836
+ case "instance":
2837
+ return {
2838
+ instanceColors: getPalette(
2839
+ paletteName,
2840
+ Math.max(1, instances.length)
2841
+ )
2842
+ };
2843
+ case "track": {
2844
+ const nTracks = Math.max(1, tracks.length);
2845
+ const trackPalette = getPalette(paletteName, nTracks);
2846
+ const instanceColors = instances.map((inst) => {
2847
+ if (inst.track) {
2848
+ const trackIdx = tracks.indexOf(inst.track);
2849
+ if (trackIdx >= 0) {
2850
+ return trackPalette[trackIdx % trackPalette.length];
2851
+ }
2852
+ }
2853
+ return trackPalette[0];
2854
+ });
2855
+ return { instanceColors };
2856
+ }
2857
+ case "node":
2858
+ return {
2859
+ instanceColors: getPalette(paletteName, 1),
2860
+ nodeColors: getPalette(paletteName, Math.max(1, nNodes))
2861
+ };
2862
+ default:
2863
+ return {
2864
+ instanceColors: getPalette(
2865
+ paletteName,
2866
+ Math.max(1, instances.length)
2867
+ )
2868
+ };
2869
+ }
2870
+ }
2871
+ async function toPNG(imageData) {
2872
+ const canvas = new Canvas(imageData.width, imageData.height);
2873
+ const ctx = canvas.getContext("2d");
2874
+ ctx.putImageData(imageData, 0, 0);
2875
+ return canvas.toBuffer("png");
2876
+ }
2877
+ async function toJPEG(imageData, quality = 0.9) {
2878
+ const canvas = new Canvas(imageData.width, imageData.height);
2879
+ const ctx = canvas.getContext("2d");
2880
+ ctx.putImageData(imageData, 0, 0);
2881
+ return canvas.toBuffer("jpeg", { quality });
2882
+ }
2883
+ function toDataURL(imageData, format = "png") {
2884
+ const canvas = new Canvas(imageData.width, imageData.height);
2885
+ const ctx = canvas.getContext("2d");
2886
+ ctx.putImageData(imageData, 0, 0);
2887
+ return canvas.toDataURL(`image/${format}`);
2888
+ }
2889
+ async function saveImage(imageData, path) {
2890
+ const canvas = new Canvas(imageData.width, imageData.height);
2891
+ const ctx = canvas.getContext("2d");
2892
+ ctx.putImageData(imageData, 0, 0);
2893
+ await canvas.saveAs(path);
2894
+ }
2895
+
2896
+ // src/rendering/video.ts
2897
+ import { spawn } from "child_process";
2898
+ async function checkFfmpeg() {
2899
+ return new Promise((resolve) => {
2900
+ const proc = spawn("ffmpeg", ["-version"]);
2901
+ proc.on("error", () => resolve(false));
2902
+ proc.on("close", (code) => resolve(code === 0));
2903
+ });
2904
+ }
2905
+ async function renderVideo(source, outputPath, options = {}) {
2906
+ const hasFfmpeg = await checkFfmpeg();
2907
+ if (!hasFfmpeg) {
2908
+ throw new Error(
2909
+ "ffmpeg not found. Please install ffmpeg and ensure it is in your PATH.\nInstallation: https://ffmpeg.org/download.html"
2910
+ );
2911
+ }
2912
+ const frames = Array.isArray(source) ? source : source.labeledFrames;
2913
+ let selectedFrames = frames;
2914
+ if (options.frameInds) {
2915
+ selectedFrames = options.frameInds.map((i) => frames[i]).filter((f) => f !== void 0);
2916
+ } else if (options.start !== void 0 || options.end !== void 0) {
2917
+ const start = options.start ?? 0;
2918
+ const end = options.end ?? frames.length;
2919
+ selectedFrames = frames.slice(start, end);
2920
+ }
2921
+ if (selectedFrames.length === 0) {
2922
+ throw new Error("No frames to render");
2923
+ }
2924
+ const firstImage = await renderImage(selectedFrames[0], options);
2925
+ const width = firstImage.width;
2926
+ const height = firstImage.height;
2927
+ const fps = options.fps ?? 30;
2928
+ const codec = options.codec ?? "libx264";
2929
+ const crf = options.crf ?? 25;
2930
+ const preset = options.preset ?? "superfast";
2931
+ const ffmpegArgs = [
2932
+ "-y",
2933
+ // Overwrite output
2934
+ "-f",
2935
+ "rawvideo",
2936
+ // Input format
2937
+ "-pix_fmt",
2938
+ "rgba",
2939
+ // Input pixel format
2940
+ "-s",
2941
+ `${width}x${height}`,
2942
+ // Frame size
2943
+ "-r",
2944
+ String(fps),
2945
+ // Frame rate
2946
+ "-i",
2947
+ "pipe:0",
2948
+ // Read from stdin
2949
+ "-c:v",
2950
+ codec,
2951
+ // Video codec
2952
+ "-pix_fmt",
2953
+ "yuv420p"
2954
+ // Output pixel format
2955
+ ];
2956
+ if (codec === "libx264") {
2957
+ ffmpegArgs.push("-crf", String(crf), "-preset", preset);
2958
+ }
2959
+ ffmpegArgs.push(outputPath);
2960
+ const ffmpeg = spawn("ffmpeg", ffmpegArgs, {
2961
+ stdio: ["pipe", "pipe", "pipe"]
2962
+ });
2963
+ let ffmpegError = null;
2964
+ ffmpeg.on("error", (err) => {
2965
+ ffmpegError = err;
2966
+ });
2967
+ const total = selectedFrames.length;
2968
+ for (let i = 0; i < selectedFrames.length; i++) {
2969
+ if (ffmpegError) {
2970
+ throw ffmpegError;
2971
+ }
2972
+ const frame = selectedFrames[i];
2973
+ const imageData = await renderImage(frame, options);
2974
+ const buffer = Buffer.from(imageData.data.buffer);
2975
+ if (!ffmpeg.stdin) {
2976
+ throw new Error("ffmpeg stdin not available");
2977
+ }
2978
+ const canWrite = ffmpeg.stdin.write(buffer);
2979
+ if (!canWrite) {
2980
+ await new Promise(
2981
+ (resolve) => ffmpeg.stdin?.once("drain", resolve)
2982
+ );
2983
+ }
2984
+ if (options.onProgress) {
2985
+ options.onProgress(i + 1, total);
2986
+ }
2987
+ }
2988
+ ffmpeg.stdin?.end();
2989
+ return new Promise((resolve, reject) => {
2990
+ ffmpeg.on("close", (code) => {
2991
+ if (code === 0) {
2992
+ resolve();
2993
+ } else {
2994
+ reject(new Error(`ffmpeg exited with code ${code}`));
2995
+ }
2996
+ });
2997
+ ffmpeg.on("error", reject);
2998
+ });
2999
+ }
2656
3000
  export {
2657
3001
  Camera,
2658
3002
  CameraGroup,
2659
3003
  Edge,
2660
3004
  FrameGroup,
2661
3005
  Instance,
3006
+ InstanceContext,
2662
3007
  InstanceGroup,
2663
3008
  LabeledFrame,
2664
3009
  Labels,
2665
3010
  LabelsSet,
3011
+ MARKER_FUNCTIONS,
2666
3012
  Mp4BoxVideoBackend,
3013
+ NAMED_COLORS,
2667
3014
  Node,
3015
+ PALETTES,
2668
3016
  PredictedInstance,
2669
3017
  RecordingSession,
3018
+ RenderContext,
2670
3019
  Skeleton,
2671
3020
  SuggestionFrame,
2672
3021
  Symmetry,
2673
3022
  Track,
2674
3023
  Video,
3024
+ checkFfmpeg,
2675
3025
  decodeYamlSkeleton,
3026
+ determineColorScheme,
3027
+ drawCircle,
3028
+ drawCross,
3029
+ drawDiamond,
3030
+ drawSquare,
3031
+ drawTriangle,
2676
3032
  encodeYamlSkeleton,
2677
3033
  fromDict,
2678
3034
  fromNumpy,
3035
+ getMarkerFunction,
3036
+ getPalette,
2679
3037
  labelsFromNumpy,
2680
3038
  loadSlp,
2681
3039
  loadVideo,
@@ -2686,8 +3044,16 @@ export {
2686
3044
  predictedPointsEmpty,
2687
3045
  predictedPointsFromArray,
2688
3046
  predictedPointsFromDict,
3047
+ renderImage,
3048
+ renderVideo,
3049
+ resolveColor,
3050
+ rgbToCSS,
2689
3051
  rodriguesTransformation,
3052
+ saveImage,
2690
3053
  saveSlp,
3054
+ toDataURL,
2691
3055
  toDict,
2692
- toNumpy
3056
+ toJPEG,
3057
+ toNumpy,
3058
+ toPNG
2693
3059
  };