@talmolab/sleap-io.js 0.1.0 → 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/README.md +34 -0
- package/dist/chunk-CDU5QGU6.js +590 -0
- package/dist/index.d.ts +242 -125
- package/dist/index.js +879 -436
- package/dist/instance-D_5PPN1y.d.ts +126 -0
- package/dist/lite.d.ts +209 -0
- package/dist/lite.js +178 -0
- package/package.json +16 -3
package/dist/index.js
CHANGED
|
@@ -1,238 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return pts;
|
|
19
|
-
}
|
|
20
|
-
function predictedPointsEmpty(length, names) {
|
|
21
|
-
const pts = [];
|
|
22
|
-
for (let i = 0; i < length; i += 1) {
|
|
23
|
-
pts.push({
|
|
24
|
-
xy: [Number.NaN, Number.NaN],
|
|
25
|
-
visible: false,
|
|
26
|
-
complete: false,
|
|
27
|
-
score: Number.NaN,
|
|
28
|
-
name: names?.[i]
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
return pts;
|
|
32
|
-
}
|
|
33
|
-
function pointsFromArray(array, names) {
|
|
34
|
-
const pts = [];
|
|
35
|
-
for (let i = 0; i < array.length; i += 1) {
|
|
36
|
-
const row = array[i] ?? [Number.NaN, Number.NaN];
|
|
37
|
-
const visible = row.length > 2 ? Boolean(row[2]) : !Number.isNaN(row[0]);
|
|
38
|
-
const complete = row.length > 3 ? Boolean(row[3]) : false;
|
|
39
|
-
pts.push({ xy: [row[0] ?? Number.NaN, row[1] ?? Number.NaN], visible, complete, name: names?.[i] });
|
|
40
|
-
}
|
|
41
|
-
return pts;
|
|
42
|
-
}
|
|
43
|
-
function predictedPointsFromArray(array, names) {
|
|
44
|
-
const pts = [];
|
|
45
|
-
for (let i = 0; i < array.length; i += 1) {
|
|
46
|
-
const row = array[i] ?? [Number.NaN, Number.NaN, Number.NaN];
|
|
47
|
-
const visible = row.length > 3 ? Boolean(row[3]) : !Number.isNaN(row[0]);
|
|
48
|
-
const complete = row.length > 4 ? Boolean(row[4]) : false;
|
|
49
|
-
pts.push({
|
|
50
|
-
xy: [row[0] ?? Number.NaN, row[1] ?? Number.NaN],
|
|
51
|
-
score: row[2] ?? Number.NaN,
|
|
52
|
-
visible,
|
|
53
|
-
complete,
|
|
54
|
-
name: names?.[i]
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return pts;
|
|
58
|
-
}
|
|
59
|
-
var Instance = class _Instance {
|
|
60
|
-
points;
|
|
61
|
-
skeleton;
|
|
62
|
-
track;
|
|
63
|
-
fromPredicted;
|
|
64
|
-
trackingScore;
|
|
65
|
-
constructor(options) {
|
|
66
|
-
this.skeleton = options.skeleton;
|
|
67
|
-
this.track = options.track ?? null;
|
|
68
|
-
this.fromPredicted = options.fromPredicted ?? null;
|
|
69
|
-
this.trackingScore = options.trackingScore ?? 0;
|
|
70
|
-
if (Array.isArray(options.points)) {
|
|
71
|
-
this.points = options.points;
|
|
72
|
-
} else {
|
|
73
|
-
this.points = pointsFromDict(options.points, options.skeleton);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
static fromArray(points, skeleton) {
|
|
77
|
-
return new _Instance({ points: pointsFromArray(points, skeleton.nodeNames), skeleton });
|
|
78
|
-
}
|
|
79
|
-
static fromNumpy(options) {
|
|
80
|
-
return new _Instance({
|
|
81
|
-
points: pointsFromArray(options.pointsData, options.skeleton.nodeNames),
|
|
82
|
-
skeleton: options.skeleton,
|
|
83
|
-
track: options.track ?? null,
|
|
84
|
-
fromPredicted: options.fromPredicted ?? null,
|
|
85
|
-
trackingScore: options.trackingScore
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
static empty(options) {
|
|
89
|
-
return new _Instance({ points: pointsEmpty(options.skeleton.nodeNames.length, options.skeleton.nodeNames), skeleton: options.skeleton });
|
|
90
|
-
}
|
|
91
|
-
get length() {
|
|
92
|
-
return this.points.length;
|
|
93
|
-
}
|
|
94
|
-
get nVisible() {
|
|
95
|
-
return this.points.filter((point) => point.visible).length;
|
|
96
|
-
}
|
|
97
|
-
getPoint(target) {
|
|
98
|
-
if (typeof target === "number") {
|
|
99
|
-
if (target < 0 || target >= this.points.length) throw new Error("Point index out of range.");
|
|
100
|
-
return this.points[target];
|
|
101
|
-
}
|
|
102
|
-
if (typeof target === "string") {
|
|
103
|
-
const index2 = this.skeleton.index(target);
|
|
104
|
-
return this.points[index2];
|
|
105
|
-
}
|
|
106
|
-
const index = this.skeleton.index(target.name);
|
|
107
|
-
return this.points[index];
|
|
108
|
-
}
|
|
109
|
-
numpy(options) {
|
|
110
|
-
const invisibleAsNaN = options?.invisibleAsNaN ?? true;
|
|
111
|
-
return this.points.map((point) => {
|
|
112
|
-
if (invisibleAsNaN && !point.visible) {
|
|
113
|
-
return [Number.NaN, Number.NaN];
|
|
114
|
-
}
|
|
115
|
-
return [point.xy[0], point.xy[1]];
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
toString() {
|
|
119
|
-
const trackName = this.track ? `"${this.track.name}"` : "None";
|
|
120
|
-
return `Instance(points=${JSON.stringify(this.numpy({ invisibleAsNaN: false }))}, track=${trackName})`;
|
|
121
|
-
}
|
|
122
|
-
get isEmpty() {
|
|
123
|
-
return this.points.every((point) => !point.visible || Number.isNaN(point.xy[0]));
|
|
124
|
-
}
|
|
125
|
-
overlapsWith(other, iouThreshold = 0.1) {
|
|
126
|
-
const boxA = this.boundingBox();
|
|
127
|
-
const boxB = other.boundingBox();
|
|
128
|
-
if (!boxA || !boxB) return false;
|
|
129
|
-
const iou = intersectionOverUnion(boxA, boxB);
|
|
130
|
-
return iou >= iouThreshold;
|
|
131
|
-
}
|
|
132
|
-
boundingBox() {
|
|
133
|
-
const xs = [];
|
|
134
|
-
const ys = [];
|
|
135
|
-
for (const point of this.points) {
|
|
136
|
-
if (Number.isNaN(point.xy[0]) || Number.isNaN(point.xy[1])) continue;
|
|
137
|
-
xs.push(point.xy[0]);
|
|
138
|
-
ys.push(point.xy[1]);
|
|
139
|
-
}
|
|
140
|
-
if (!xs.length || !ys.length) return null;
|
|
141
|
-
const minX = Math.min(...xs);
|
|
142
|
-
const maxX = Math.max(...xs);
|
|
143
|
-
const minY = Math.min(...ys);
|
|
144
|
-
const maxY = Math.max(...ys);
|
|
145
|
-
return [minX, minY, maxX, maxY];
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
var PredictedInstance = class _PredictedInstance extends Instance {
|
|
149
|
-
score;
|
|
150
|
-
constructor(options) {
|
|
151
|
-
const { score = 0, ...rest } = options;
|
|
152
|
-
const pts = Array.isArray(rest.points) ? rest.points : predictedPointsFromDict(rest.points, rest.skeleton);
|
|
153
|
-
super({
|
|
154
|
-
points: pts,
|
|
155
|
-
skeleton: rest.skeleton,
|
|
156
|
-
track: rest.track,
|
|
157
|
-
trackingScore: rest.trackingScore
|
|
158
|
-
});
|
|
159
|
-
this.score = score;
|
|
160
|
-
}
|
|
161
|
-
static fromArray(points, skeleton, score) {
|
|
162
|
-
return new _PredictedInstance({
|
|
163
|
-
points: predictedPointsFromArray(points, skeleton.nodeNames),
|
|
164
|
-
skeleton,
|
|
165
|
-
score
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
static fromNumpy(options) {
|
|
169
|
-
return new _PredictedInstance({
|
|
170
|
-
points: predictedPointsFromArray(options.pointsData, options.skeleton.nodeNames),
|
|
171
|
-
skeleton: options.skeleton,
|
|
172
|
-
track: options.track ?? null,
|
|
173
|
-
score: options.score,
|
|
174
|
-
trackingScore: options.trackingScore
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
static empty(options) {
|
|
178
|
-
return new _PredictedInstance({ points: predictedPointsEmpty(options.skeleton.nodeNames.length, options.skeleton.nodeNames), skeleton: options.skeleton });
|
|
179
|
-
}
|
|
180
|
-
numpy(options) {
|
|
181
|
-
const invisibleAsNaN = options?.invisibleAsNaN ?? true;
|
|
182
|
-
return this.points.map((point) => {
|
|
183
|
-
const xy = invisibleAsNaN && !point.visible ? [Number.NaN, Number.NaN] : [point.xy[0], point.xy[1]];
|
|
184
|
-
if (options?.scores) {
|
|
185
|
-
return [xy[0], xy[1], point.score ?? 0];
|
|
186
|
-
}
|
|
187
|
-
return xy;
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
toString() {
|
|
191
|
-
const trackName = this.track ? `"${this.track.name}"` : "None";
|
|
192
|
-
return `PredictedInstance(points=${JSON.stringify(this.numpy({ invisibleAsNaN: false }))}, track=${trackName}, score=${this.score.toFixed(2)}, tracking_score=${this.trackingScore ?? "None"})`;
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
function pointsFromDict(pointsDict, skeleton) {
|
|
196
|
-
const points = pointsEmpty(skeleton.nodeNames.length, skeleton.nodeNames);
|
|
197
|
-
for (const [nodeName, data] of Object.entries(pointsDict)) {
|
|
198
|
-
const index = skeleton.index(nodeName);
|
|
199
|
-
points[index] = {
|
|
200
|
-
xy: [data[0] ?? Number.NaN, data[1] ?? Number.NaN],
|
|
201
|
-
visible: data.length > 2 ? Boolean(data[2]) : !Number.isNaN(data[0]),
|
|
202
|
-
complete: data.length > 3 ? Boolean(data[3]) : false,
|
|
203
|
-
name: nodeName
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
return points;
|
|
207
|
-
}
|
|
208
|
-
function predictedPointsFromDict(pointsDict, skeleton) {
|
|
209
|
-
const points = predictedPointsEmpty(skeleton.nodeNames.length, skeleton.nodeNames);
|
|
210
|
-
for (const [nodeName, data] of Object.entries(pointsDict)) {
|
|
211
|
-
const index = skeleton.index(nodeName);
|
|
212
|
-
points[index] = {
|
|
213
|
-
xy: [data[0] ?? Number.NaN, data[1] ?? Number.NaN],
|
|
214
|
-
score: data[2] ?? Number.NaN,
|
|
215
|
-
visible: data.length > 3 ? Boolean(data[3]) : !Number.isNaN(data[0]),
|
|
216
|
-
complete: data.length > 4 ? Boolean(data[4]) : false,
|
|
217
|
-
name: nodeName
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
return points;
|
|
221
|
-
}
|
|
222
|
-
function intersectionOverUnion(boxA, boxB) {
|
|
223
|
-
const [ax1, ay1, ax2, ay2] = boxA;
|
|
224
|
-
const [bx1, by1, bx2, by2] = boxB;
|
|
225
|
-
const interX1 = Math.max(ax1, bx1);
|
|
226
|
-
const interY1 = Math.max(ay1, by1);
|
|
227
|
-
const interX2 = Math.min(ax2, bx2);
|
|
228
|
-
const interY2 = Math.min(ay2, by2);
|
|
229
|
-
const interArea = Math.max(0, interX2 - interX1) * Math.max(0, interY2 - interY1);
|
|
230
|
-
const areaA = Math.max(0, ax2 - ax1) * Math.max(0, ay2 - ay1);
|
|
231
|
-
const areaB = Math.max(0, bx2 - bx1) * Math.max(0, by2 - by1);
|
|
232
|
-
const union = areaA + areaB - interArea;
|
|
233
|
-
if (union === 0) return 0;
|
|
234
|
-
return interArea / union;
|
|
235
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
Edge,
|
|
3
|
+
Instance,
|
|
4
|
+
Node,
|
|
5
|
+
PredictedInstance,
|
|
6
|
+
Skeleton,
|
|
7
|
+
Symmetry,
|
|
8
|
+
Track,
|
|
9
|
+
parseJsonAttr,
|
|
10
|
+
parseSkeletons,
|
|
11
|
+
pointsEmpty,
|
|
12
|
+
pointsFromArray,
|
|
13
|
+
pointsFromDict,
|
|
14
|
+
predictedPointsEmpty,
|
|
15
|
+
predictedPointsFromArray,
|
|
16
|
+
predictedPointsFromDict
|
|
17
|
+
} from "./chunk-CDU5QGU6.js";
|
|
236
18
|
|
|
237
19
|
// src/model/labeled-frame.ts
|
|
238
20
|
var LabeledFrame = class {
|
|
@@ -293,126 +75,6 @@ var LabeledFrame = class {
|
|
|
293
75
|
}
|
|
294
76
|
};
|
|
295
77
|
|
|
296
|
-
// src/model/skeleton.ts
|
|
297
|
-
var Node = class {
|
|
298
|
-
name;
|
|
299
|
-
constructor(name) {
|
|
300
|
-
this.name = name;
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
|
-
var Edge = class {
|
|
304
|
-
source;
|
|
305
|
-
destination;
|
|
306
|
-
constructor(source, destination) {
|
|
307
|
-
this.source = source;
|
|
308
|
-
this.destination = destination;
|
|
309
|
-
}
|
|
310
|
-
at(index) {
|
|
311
|
-
if (index === 0) return this.source;
|
|
312
|
-
if (index === 1) return this.destination;
|
|
313
|
-
throw new Error("Edge only has 2 nodes (source and destination).");
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
var Symmetry = class {
|
|
317
|
-
nodes;
|
|
318
|
-
constructor(nodes) {
|
|
319
|
-
const set = new Set(nodes);
|
|
320
|
-
if (set.size !== 2) {
|
|
321
|
-
throw new Error("Symmetry must contain exactly 2 nodes.");
|
|
322
|
-
}
|
|
323
|
-
this.nodes = set;
|
|
324
|
-
}
|
|
325
|
-
at(index) {
|
|
326
|
-
let i = 0;
|
|
327
|
-
for (const node of this.nodes) {
|
|
328
|
-
if (i === index) return node;
|
|
329
|
-
i += 1;
|
|
330
|
-
}
|
|
331
|
-
throw new Error("Symmetry index out of range.");
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
var Skeleton = class {
|
|
335
|
-
nodes;
|
|
336
|
-
edges;
|
|
337
|
-
symmetries;
|
|
338
|
-
name;
|
|
339
|
-
nameToNode;
|
|
340
|
-
nodeToIndex;
|
|
341
|
-
constructor(options) {
|
|
342
|
-
const resolved = Array.isArray(options) ? { nodes: options } : options;
|
|
343
|
-
this.nodes = resolved.nodes.map((node) => typeof node === "string" ? new Node(node) : node);
|
|
344
|
-
this.edges = [];
|
|
345
|
-
this.symmetries = [];
|
|
346
|
-
this.name = resolved.name;
|
|
347
|
-
this.nameToNode = /* @__PURE__ */ new Map();
|
|
348
|
-
this.nodeToIndex = /* @__PURE__ */ new Map();
|
|
349
|
-
this.rebuildCache();
|
|
350
|
-
if (resolved.edges) {
|
|
351
|
-
this.edges = resolved.edges.map((edge) => edge instanceof Edge ? edge : this.edgeFrom(edge));
|
|
352
|
-
}
|
|
353
|
-
if (resolved.symmetries) {
|
|
354
|
-
this.symmetries = resolved.symmetries.map(
|
|
355
|
-
(symmetry) => symmetry instanceof Symmetry ? symmetry : this.symmetryFrom(symmetry)
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
rebuildCache(nodes = this.nodes) {
|
|
360
|
-
this.nameToNode = new Map(nodes.map((node) => [node.name, node]));
|
|
361
|
-
this.nodeToIndex = new Map(nodes.map((node, index) => [node, index]));
|
|
362
|
-
}
|
|
363
|
-
get nodeNames() {
|
|
364
|
-
return this.nodes.map((node) => node.name);
|
|
365
|
-
}
|
|
366
|
-
index(node) {
|
|
367
|
-
if (typeof node === "number") return node;
|
|
368
|
-
if (typeof node === "string") {
|
|
369
|
-
const found = this.nameToNode.get(node);
|
|
370
|
-
if (!found) throw new Error(`Node '${node}' not found in skeleton.`);
|
|
371
|
-
return this.nodeToIndex.get(found) ?? -1;
|
|
372
|
-
}
|
|
373
|
-
const idx = this.nodeToIndex.get(node);
|
|
374
|
-
if (idx === void 0) throw new Error("Node not found in skeleton.");
|
|
375
|
-
return idx;
|
|
376
|
-
}
|
|
377
|
-
node(node) {
|
|
378
|
-
if (node instanceof Node) return node;
|
|
379
|
-
if (typeof node === "number") return this.nodes[node];
|
|
380
|
-
const found = this.nameToNode.get(node);
|
|
381
|
-
if (!found) throw new Error(`Node '${node}' not found in skeleton.`);
|
|
382
|
-
return found;
|
|
383
|
-
}
|
|
384
|
-
get edgeIndices() {
|
|
385
|
-
return this.edges.map((edge) => [this.index(edge.source), this.index(edge.destination)]);
|
|
386
|
-
}
|
|
387
|
-
get symmetryNames() {
|
|
388
|
-
return this.symmetries.map((symmetry) => {
|
|
389
|
-
const nodes = Array.from(symmetry.nodes).map((node) => node.name);
|
|
390
|
-
return [nodes[0], nodes[1]];
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
matches(other) {
|
|
394
|
-
if (this.nodeNames.length !== other.nodeNames.length) return false;
|
|
395
|
-
for (let i = 0; i < this.nodeNames.length; i += 1) {
|
|
396
|
-
if (this.nodeNames[i] !== other.nodeNames[i]) return false;
|
|
397
|
-
}
|
|
398
|
-
return true;
|
|
399
|
-
}
|
|
400
|
-
addEdge(source, destination) {
|
|
401
|
-
this.edges.push(new Edge(this.node(source), this.node(destination)));
|
|
402
|
-
}
|
|
403
|
-
addSymmetry(left, right) {
|
|
404
|
-
this.symmetries.push(new Symmetry([this.node(left), this.node(right)]));
|
|
405
|
-
}
|
|
406
|
-
edgeFrom(edge) {
|
|
407
|
-
const [source, destination] = edge;
|
|
408
|
-
return new Edge(this.node(source), this.node(destination));
|
|
409
|
-
}
|
|
410
|
-
symmetryFrom(symmetry) {
|
|
411
|
-
const [a, b] = symmetry;
|
|
412
|
-
return new Symmetry([this.node(a), this.node(b)]);
|
|
413
|
-
}
|
|
414
|
-
};
|
|
415
|
-
|
|
416
78
|
// src/model/video.ts
|
|
417
79
|
var Video = class {
|
|
418
80
|
filename;
|
|
@@ -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 =
|
|
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 ?? [];
|
|
@@ -2578,27 +2161,879 @@ async function loadVideo(filename, options) {
|
|
|
2578
2161
|
const backend = await createVideoBackend(filename, { dataset: options?.dataset });
|
|
2579
2162
|
return new Video({ filename, backend, openBackend: options?.openBackend ?? true });
|
|
2580
2163
|
}
|
|
2164
|
+
|
|
2165
|
+
// src/codecs/skeleton-yaml.ts
|
|
2166
|
+
import YAML from "yaml";
|
|
2167
|
+
function getNodeName(entry) {
|
|
2168
|
+
if (typeof entry === "string") return entry;
|
|
2169
|
+
if (entry && typeof entry.name === "string") return entry.name;
|
|
2170
|
+
throw new Error("Invalid node entry in skeleton YAML.");
|
|
2171
|
+
}
|
|
2172
|
+
function resolveName(value) {
|
|
2173
|
+
if (typeof value === "string") return value;
|
|
2174
|
+
if (value && typeof value.name === "string") return value.name;
|
|
2175
|
+
throw new Error("Invalid name reference in skeleton YAML.");
|
|
2176
|
+
}
|
|
2177
|
+
function decodeSkeleton(data, fallbackName) {
|
|
2178
|
+
if (!data?.nodes) throw new Error("Skeleton YAML missing nodes.");
|
|
2179
|
+
const nodes = data.nodes.map((entry) => new Node(getNodeName(entry)));
|
|
2180
|
+
const edges = (data.edges ?? []).map((edge) => {
|
|
2181
|
+
if (Array.isArray(edge)) {
|
|
2182
|
+
const [source2, destination] = edge;
|
|
2183
|
+
return new Edge(nodes[Number(source2)], nodes[Number(destination)]);
|
|
2184
|
+
}
|
|
2185
|
+
const sourceName = resolveName(edge.source);
|
|
2186
|
+
const destName = resolveName(edge.destination);
|
|
2187
|
+
const source = nodes.find((node) => node.name === sourceName);
|
|
2188
|
+
const dest = nodes.find((node) => node.name === destName);
|
|
2189
|
+
if (!source || !dest) throw new Error("Edge references unknown node.");
|
|
2190
|
+
return new Edge(source, dest);
|
|
2191
|
+
});
|
|
2192
|
+
const symmetries = (data.symmetries ?? []).map((symmetry) => {
|
|
2193
|
+
if (!Array.isArray(symmetry) || symmetry.length !== 2) {
|
|
2194
|
+
throw new Error("Symmetry must contain exactly 2 nodes.");
|
|
2195
|
+
}
|
|
2196
|
+
const [left, right] = symmetry;
|
|
2197
|
+
const leftName = resolveName(left);
|
|
2198
|
+
const rightName = resolveName(right);
|
|
2199
|
+
const leftNode = nodes.find((node) => node.name === leftName);
|
|
2200
|
+
const rightNode = nodes.find((node) => node.name === rightName);
|
|
2201
|
+
if (!leftNode || !rightNode) throw new Error("Symmetry references unknown node.");
|
|
2202
|
+
return new Symmetry([leftNode, rightNode]);
|
|
2203
|
+
});
|
|
2204
|
+
return new Skeleton({
|
|
2205
|
+
name: data.name ?? fallbackName,
|
|
2206
|
+
nodes,
|
|
2207
|
+
edges,
|
|
2208
|
+
symmetries
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
function decodeYamlSkeleton(yamlData) {
|
|
2212
|
+
const parsed = YAML.parse(yamlData);
|
|
2213
|
+
if (!parsed) throw new Error("Empty skeleton YAML.");
|
|
2214
|
+
if (Object.prototype.hasOwnProperty.call(parsed, "nodes")) {
|
|
2215
|
+
return decodeSkeleton(parsed);
|
|
2216
|
+
}
|
|
2217
|
+
return Object.entries(parsed).map(
|
|
2218
|
+
([name, skeletonData]) => decodeSkeleton(skeletonData, name)
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
function encodeYamlSkeleton(skeletons) {
|
|
2222
|
+
const list = Array.isArray(skeletons) ? skeletons : [skeletons];
|
|
2223
|
+
const payload = {};
|
|
2224
|
+
list.forEach((skeleton, index) => {
|
|
2225
|
+
const name = skeleton.name ?? `Skeleton-${index}`;
|
|
2226
|
+
const nodes = skeleton.nodes.map((node) => ({ name: node.name }));
|
|
2227
|
+
const edges = skeleton.edges.map((edge) => ({
|
|
2228
|
+
source: { name: edge.source.name },
|
|
2229
|
+
destination: { name: edge.destination.name }
|
|
2230
|
+
}));
|
|
2231
|
+
const symmetries = skeleton.symmetries.map((symmetry) => {
|
|
2232
|
+
const pair = Array.from(symmetry.nodes);
|
|
2233
|
+
return [{ name: pair[0].name }, { name: pair[1].name }];
|
|
2234
|
+
});
|
|
2235
|
+
payload[name] = { nodes, edges, symmetries };
|
|
2236
|
+
});
|
|
2237
|
+
return YAML.stringify(payload);
|
|
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
|
+
}
|
|
2581
3000
|
export {
|
|
2582
3001
|
Camera,
|
|
2583
3002
|
CameraGroup,
|
|
2584
3003
|
Edge,
|
|
2585
3004
|
FrameGroup,
|
|
2586
3005
|
Instance,
|
|
3006
|
+
InstanceContext,
|
|
2587
3007
|
InstanceGroup,
|
|
2588
3008
|
LabeledFrame,
|
|
2589
3009
|
Labels,
|
|
2590
3010
|
LabelsSet,
|
|
3011
|
+
MARKER_FUNCTIONS,
|
|
2591
3012
|
Mp4BoxVideoBackend,
|
|
3013
|
+
NAMED_COLORS,
|
|
2592
3014
|
Node,
|
|
3015
|
+
PALETTES,
|
|
2593
3016
|
PredictedInstance,
|
|
2594
3017
|
RecordingSession,
|
|
3018
|
+
RenderContext,
|
|
2595
3019
|
Skeleton,
|
|
2596
3020
|
SuggestionFrame,
|
|
2597
3021
|
Symmetry,
|
|
2598
3022
|
Track,
|
|
2599
3023
|
Video,
|
|
3024
|
+
checkFfmpeg,
|
|
3025
|
+
decodeYamlSkeleton,
|
|
3026
|
+
determineColorScheme,
|
|
3027
|
+
drawCircle,
|
|
3028
|
+
drawCross,
|
|
3029
|
+
drawDiamond,
|
|
3030
|
+
drawSquare,
|
|
3031
|
+
drawTriangle,
|
|
3032
|
+
encodeYamlSkeleton,
|
|
2600
3033
|
fromDict,
|
|
2601
3034
|
fromNumpy,
|
|
3035
|
+
getMarkerFunction,
|
|
3036
|
+
getPalette,
|
|
2602
3037
|
labelsFromNumpy,
|
|
2603
3038
|
loadSlp,
|
|
2604
3039
|
loadVideo,
|
|
@@ -2609,8 +3044,16 @@ export {
|
|
|
2609
3044
|
predictedPointsEmpty,
|
|
2610
3045
|
predictedPointsFromArray,
|
|
2611
3046
|
predictedPointsFromDict,
|
|
3047
|
+
renderImage,
|
|
3048
|
+
renderVideo,
|
|
3049
|
+
resolveColor,
|
|
3050
|
+
rgbToCSS,
|
|
2612
3051
|
rodriguesTransformation,
|
|
3052
|
+
saveImage,
|
|
2613
3053
|
saveSlp,
|
|
3054
|
+
toDataURL,
|
|
2614
3055
|
toDict,
|
|
2615
|
-
|
|
3056
|
+
toJPEG,
|
|
3057
|
+
toNumpy,
|
|
3058
|
+
toPNG
|
|
2616
3059
|
};
|