@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/README.md +19 -0
- package/dist/chunk-CDU5QGU6.js +590 -0
- package/dist/index.d.ts +239 -125
- package/dist/index.js +802 -436
- package/dist/instance-D_5PPN1y.d.ts +126 -0
- package/dist/lite.d.ts +209 -0
- package/dist/lite.js +178 -0
- package/package.json +12 -3
package/dist/index.js
CHANGED
|
@@ -1,238 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return pts;
|
|
19
|
-
}
|
|
20
|
-
function predictedPointsEmpty(length, names) {
|
|
21
|
-
const pts = [];
|
|
22
|
-
for (let i = 0; i < length; i += 1) {
|
|
23
|
-
pts.push({
|
|
24
|
-
xy: [Number.NaN, Number.NaN],
|
|
25
|
-
visible: false,
|
|
26
|
-
complete: false,
|
|
27
|
-
score: Number.NaN,
|
|
28
|
-
name: names?.[i]
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
return pts;
|
|
32
|
-
}
|
|
33
|
-
function pointsFromArray(array, names) {
|
|
34
|
-
const pts = [];
|
|
35
|
-
for (let i = 0; i < array.length; i += 1) {
|
|
36
|
-
const row = array[i] ?? [Number.NaN, Number.NaN];
|
|
37
|
-
const visible = row.length > 2 ? Boolean(row[2]) : !Number.isNaN(row[0]);
|
|
38
|
-
const complete = row.length > 3 ? Boolean(row[3]) : false;
|
|
39
|
-
pts.push({ xy: [row[0] ?? Number.NaN, row[1] ?? Number.NaN], visible, complete, name: names?.[i] });
|
|
40
|
-
}
|
|
41
|
-
return pts;
|
|
42
|
-
}
|
|
43
|
-
function predictedPointsFromArray(array, names) {
|
|
44
|
-
const pts = [];
|
|
45
|
-
for (let i = 0; i < array.length; i += 1) {
|
|
46
|
-
const row = array[i] ?? [Number.NaN, Number.NaN, Number.NaN];
|
|
47
|
-
const visible = row.length > 3 ? Boolean(row[3]) : !Number.isNaN(row[0]);
|
|
48
|
-
const complete = row.length > 4 ? Boolean(row[4]) : false;
|
|
49
|
-
pts.push({
|
|
50
|
-
xy: [row[0] ?? Number.NaN, row[1] ?? Number.NaN],
|
|
51
|
-
score: row[2] ?? Number.NaN,
|
|
52
|
-
visible,
|
|
53
|
-
complete,
|
|
54
|
-
name: names?.[i]
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return pts;
|
|
58
|
-
}
|
|
59
|
-
var Instance = class _Instance {
|
|
60
|
-
points;
|
|
61
|
-
skeleton;
|
|
62
|
-
track;
|
|
63
|
-
fromPredicted;
|
|
64
|
-
trackingScore;
|
|
65
|
-
constructor(options) {
|
|
66
|
-
this.skeleton = options.skeleton;
|
|
67
|
-
this.track = options.track ?? null;
|
|
68
|
-
this.fromPredicted = options.fromPredicted ?? null;
|
|
69
|
-
this.trackingScore = options.trackingScore ?? 0;
|
|
70
|
-
if (Array.isArray(options.points)) {
|
|
71
|
-
this.points = options.points;
|
|
72
|
-
} else {
|
|
73
|
-
this.points = pointsFromDict(options.points, options.skeleton);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
static fromArray(points, skeleton) {
|
|
77
|
-
return new _Instance({ points: pointsFromArray(points, skeleton.nodeNames), skeleton });
|
|
78
|
-
}
|
|
79
|
-
static fromNumpy(options) {
|
|
80
|
-
return new _Instance({
|
|
81
|
-
points: pointsFromArray(options.pointsData, options.skeleton.nodeNames),
|
|
82
|
-
skeleton: options.skeleton,
|
|
83
|
-
track: options.track ?? null,
|
|
84
|
-
fromPredicted: options.fromPredicted ?? null,
|
|
85
|
-
trackingScore: options.trackingScore
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
static empty(options) {
|
|
89
|
-
return new _Instance({ points: pointsEmpty(options.skeleton.nodeNames.length, options.skeleton.nodeNames), skeleton: options.skeleton });
|
|
90
|
-
}
|
|
91
|
-
get length() {
|
|
92
|
-
return this.points.length;
|
|
93
|
-
}
|
|
94
|
-
get nVisible() {
|
|
95
|
-
return this.points.filter((point) => point.visible).length;
|
|
96
|
-
}
|
|
97
|
-
getPoint(target) {
|
|
98
|
-
if (typeof target === "number") {
|
|
99
|
-
if (target < 0 || target >= this.points.length) throw new Error("Point index out of range.");
|
|
100
|
-
return this.points[target];
|
|
101
|
-
}
|
|
102
|
-
if (typeof target === "string") {
|
|
103
|
-
const index2 = this.skeleton.index(target);
|
|
104
|
-
return this.points[index2];
|
|
105
|
-
}
|
|
106
|
-
const index = this.skeleton.index(target.name);
|
|
107
|
-
return this.points[index];
|
|
108
|
-
}
|
|
109
|
-
numpy(options) {
|
|
110
|
-
const invisibleAsNaN = options?.invisibleAsNaN ?? true;
|
|
111
|
-
return this.points.map((point) => {
|
|
112
|
-
if (invisibleAsNaN && !point.visible) {
|
|
113
|
-
return [Number.NaN, Number.NaN];
|
|
114
|
-
}
|
|
115
|
-
return [point.xy[0], point.xy[1]];
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
toString() {
|
|
119
|
-
const trackName = this.track ? `"${this.track.name}"` : "None";
|
|
120
|
-
return `Instance(points=${JSON.stringify(this.numpy({ invisibleAsNaN: false }))}, track=${trackName})`;
|
|
121
|
-
}
|
|
122
|
-
get isEmpty() {
|
|
123
|
-
return this.points.every((point) => !point.visible || Number.isNaN(point.xy[0]));
|
|
124
|
-
}
|
|
125
|
-
overlapsWith(other, iouThreshold = 0.1) {
|
|
126
|
-
const boxA = this.boundingBox();
|
|
127
|
-
const boxB = other.boundingBox();
|
|
128
|
-
if (!boxA || !boxB) return false;
|
|
129
|
-
const iou = intersectionOverUnion(boxA, boxB);
|
|
130
|
-
return iou >= iouThreshold;
|
|
131
|
-
}
|
|
132
|
-
boundingBox() {
|
|
133
|
-
const xs = [];
|
|
134
|
-
const ys = [];
|
|
135
|
-
for (const point of this.points) {
|
|
136
|
-
if (Number.isNaN(point.xy[0]) || Number.isNaN(point.xy[1])) continue;
|
|
137
|
-
xs.push(point.xy[0]);
|
|
138
|
-
ys.push(point.xy[1]);
|
|
139
|
-
}
|
|
140
|
-
if (!xs.length || !ys.length) return null;
|
|
141
|
-
const minX = Math.min(...xs);
|
|
142
|
-
const maxX = Math.max(...xs);
|
|
143
|
-
const minY = Math.min(...ys);
|
|
144
|
-
const maxY = Math.max(...ys);
|
|
145
|
-
return [minX, minY, maxX, maxY];
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
var PredictedInstance = class _PredictedInstance extends Instance {
|
|
149
|
-
score;
|
|
150
|
-
constructor(options) {
|
|
151
|
-
const { score = 0, ...rest } = options;
|
|
152
|
-
const pts = Array.isArray(rest.points) ? rest.points : predictedPointsFromDict(rest.points, rest.skeleton);
|
|
153
|
-
super({
|
|
154
|
-
points: pts,
|
|
155
|
-
skeleton: rest.skeleton,
|
|
156
|
-
track: rest.track,
|
|
157
|
-
trackingScore: rest.trackingScore
|
|
158
|
-
});
|
|
159
|
-
this.score = score;
|
|
160
|
-
}
|
|
161
|
-
static fromArray(points, skeleton, score) {
|
|
162
|
-
return new _PredictedInstance({
|
|
163
|
-
points: predictedPointsFromArray(points, skeleton.nodeNames),
|
|
164
|
-
skeleton,
|
|
165
|
-
score
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
static fromNumpy(options) {
|
|
169
|
-
return new _PredictedInstance({
|
|
170
|
-
points: predictedPointsFromArray(options.pointsData, options.skeleton.nodeNames),
|
|
171
|
-
skeleton: options.skeleton,
|
|
172
|
-
track: options.track ?? null,
|
|
173
|
-
score: options.score,
|
|
174
|
-
trackingScore: options.trackingScore
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
static empty(options) {
|
|
178
|
-
return new _PredictedInstance({ points: predictedPointsEmpty(options.skeleton.nodeNames.length, options.skeleton.nodeNames), skeleton: options.skeleton });
|
|
179
|
-
}
|
|
180
|
-
numpy(options) {
|
|
181
|
-
const invisibleAsNaN = options?.invisibleAsNaN ?? true;
|
|
182
|
-
return this.points.map((point) => {
|
|
183
|
-
const xy = invisibleAsNaN && !point.visible ? [Number.NaN, Number.NaN] : [point.xy[0], point.xy[1]];
|
|
184
|
-
if (options?.scores) {
|
|
185
|
-
return [xy[0], xy[1], point.score ?? 0];
|
|
186
|
-
}
|
|
187
|
-
return xy;
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
toString() {
|
|
191
|
-
const trackName = this.track ? `"${this.track.name}"` : "None";
|
|
192
|
-
return `PredictedInstance(points=${JSON.stringify(this.numpy({ invisibleAsNaN: false }))}, track=${trackName}, score=${this.score.toFixed(2)}, tracking_score=${this.trackingScore ?? "None"})`;
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
function pointsFromDict(pointsDict, skeleton) {
|
|
196
|
-
const points = pointsEmpty(skeleton.nodeNames.length, skeleton.nodeNames);
|
|
197
|
-
for (const [nodeName, data] of Object.entries(pointsDict)) {
|
|
198
|
-
const index = skeleton.index(nodeName);
|
|
199
|
-
points[index] = {
|
|
200
|
-
xy: [data[0] ?? Number.NaN, data[1] ?? Number.NaN],
|
|
201
|
-
visible: data.length > 2 ? Boolean(data[2]) : !Number.isNaN(data[0]),
|
|
202
|
-
complete: data.length > 3 ? Boolean(data[3]) : false,
|
|
203
|
-
name: nodeName
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
return points;
|
|
207
|
-
}
|
|
208
|
-
function predictedPointsFromDict(pointsDict, skeleton) {
|
|
209
|
-
const points = predictedPointsEmpty(skeleton.nodeNames.length, skeleton.nodeNames);
|
|
210
|
-
for (const [nodeName, data] of Object.entries(pointsDict)) {
|
|
211
|
-
const index = skeleton.index(nodeName);
|
|
212
|
-
points[index] = {
|
|
213
|
-
xy: [data[0] ?? Number.NaN, data[1] ?? Number.NaN],
|
|
214
|
-
score: data[2] ?? Number.NaN,
|
|
215
|
-
visible: data.length > 3 ? Boolean(data[3]) : !Number.isNaN(data[0]),
|
|
216
|
-
complete: data.length > 4 ? Boolean(data[4]) : false,
|
|
217
|
-
name: nodeName
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
return points;
|
|
221
|
-
}
|
|
222
|
-
function intersectionOverUnion(boxA, boxB) {
|
|
223
|
-
const [ax1, ay1, ax2, ay2] = boxA;
|
|
224
|
-
const [bx1, by1, bx2, by2] = boxB;
|
|
225
|
-
const interX1 = Math.max(ax1, bx1);
|
|
226
|
-
const interY1 = Math.max(ay1, by1);
|
|
227
|
-
const interX2 = Math.min(ax2, bx2);
|
|
228
|
-
const interY2 = Math.min(ay2, by2);
|
|
229
|
-
const interArea = Math.max(0, interX2 - interX1) * Math.max(0, interY2 - interY1);
|
|
230
|
-
const areaA = Math.max(0, ax2 - ax1) * Math.max(0, ay2 - ay1);
|
|
231
|
-
const areaB = Math.max(0, bx2 - bx1) * Math.max(0, by2 - by1);
|
|
232
|
-
const union = areaA + areaB - interArea;
|
|
233
|
-
if (union === 0) return 0;
|
|
234
|
-
return interArea / union;
|
|
235
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
Edge,
|
|
3
|
+
Instance,
|
|
4
|
+
Node,
|
|
5
|
+
PredictedInstance,
|
|
6
|
+
Skeleton,
|
|
7
|
+
Symmetry,
|
|
8
|
+
Track,
|
|
9
|
+
parseJsonAttr,
|
|
10
|
+
parseSkeletons,
|
|
11
|
+
pointsEmpty,
|
|
12
|
+
pointsFromArray,
|
|
13
|
+
pointsFromDict,
|
|
14
|
+
predictedPointsEmpty,
|
|
15
|
+
predictedPointsFromArray,
|
|
16
|
+
predictedPointsFromDict
|
|
17
|
+
} from "./chunk-CDU5QGU6.js";
|
|
236
18
|
|
|
237
19
|
// src/model/labeled-frame.ts
|
|
238
20
|
var LabeledFrame = class {
|
|
@@ -293,126 +75,6 @@ var LabeledFrame = class {
|
|
|
293
75
|
}
|
|
294
76
|
};
|
|
295
77
|
|
|
296
|
-
// src/model/skeleton.ts
|
|
297
|
-
var Node = class {
|
|
298
|
-
name;
|
|
299
|
-
constructor(name) {
|
|
300
|
-
this.name = name;
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
|
-
var Edge = class {
|
|
304
|
-
source;
|
|
305
|
-
destination;
|
|
306
|
-
constructor(source, destination) {
|
|
307
|
-
this.source = source;
|
|
308
|
-
this.destination = destination;
|
|
309
|
-
}
|
|
310
|
-
at(index) {
|
|
311
|
-
if (index === 0) return this.source;
|
|
312
|
-
if (index === 1) return this.destination;
|
|
313
|
-
throw new Error("Edge only has 2 nodes (source and destination).");
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
var Symmetry = class {
|
|
317
|
-
nodes;
|
|
318
|
-
constructor(nodes) {
|
|
319
|
-
const set = new Set(nodes);
|
|
320
|
-
if (set.size !== 2) {
|
|
321
|
-
throw new Error("Symmetry must contain exactly 2 nodes.");
|
|
322
|
-
}
|
|
323
|
-
this.nodes = set;
|
|
324
|
-
}
|
|
325
|
-
at(index) {
|
|
326
|
-
let i = 0;
|
|
327
|
-
for (const node of this.nodes) {
|
|
328
|
-
if (i === index) return node;
|
|
329
|
-
i += 1;
|
|
330
|
-
}
|
|
331
|
-
throw new Error("Symmetry index out of range.");
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
var Skeleton = class {
|
|
335
|
-
nodes;
|
|
336
|
-
edges;
|
|
337
|
-
symmetries;
|
|
338
|
-
name;
|
|
339
|
-
nameToNode;
|
|
340
|
-
nodeToIndex;
|
|
341
|
-
constructor(options) {
|
|
342
|
-
const resolved = Array.isArray(options) ? { nodes: options } : options;
|
|
343
|
-
this.nodes = resolved.nodes.map((node) => typeof node === "string" ? new Node(node) : node);
|
|
344
|
-
this.edges = [];
|
|
345
|
-
this.symmetries = [];
|
|
346
|
-
this.name = resolved.name;
|
|
347
|
-
this.nameToNode = /* @__PURE__ */ new Map();
|
|
348
|
-
this.nodeToIndex = /* @__PURE__ */ new Map();
|
|
349
|
-
this.rebuildCache();
|
|
350
|
-
if (resolved.edges) {
|
|
351
|
-
this.edges = resolved.edges.map((edge) => edge instanceof Edge ? edge : this.edgeFrom(edge));
|
|
352
|
-
}
|
|
353
|
-
if (resolved.symmetries) {
|
|
354
|
-
this.symmetries = resolved.symmetries.map(
|
|
355
|
-
(symmetry) => symmetry instanceof Symmetry ? symmetry : this.symmetryFrom(symmetry)
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
rebuildCache(nodes = this.nodes) {
|
|
360
|
-
this.nameToNode = new Map(nodes.map((node) => [node.name, node]));
|
|
361
|
-
this.nodeToIndex = new Map(nodes.map((node, index) => [node, index]));
|
|
362
|
-
}
|
|
363
|
-
get nodeNames() {
|
|
364
|
-
return this.nodes.map((node) => node.name);
|
|
365
|
-
}
|
|
366
|
-
index(node) {
|
|
367
|
-
if (typeof node === "number") return node;
|
|
368
|
-
if (typeof node === "string") {
|
|
369
|
-
const found = this.nameToNode.get(node);
|
|
370
|
-
if (!found) throw new Error(`Node '${node}' not found in skeleton.`);
|
|
371
|
-
return this.nodeToIndex.get(found) ?? -1;
|
|
372
|
-
}
|
|
373
|
-
const idx = this.nodeToIndex.get(node);
|
|
374
|
-
if (idx === void 0) throw new Error("Node not found in skeleton.");
|
|
375
|
-
return idx;
|
|
376
|
-
}
|
|
377
|
-
node(node) {
|
|
378
|
-
if (node instanceof Node) return node;
|
|
379
|
-
if (typeof node === "number") return this.nodes[node];
|
|
380
|
-
const found = this.nameToNode.get(node);
|
|
381
|
-
if (!found) throw new Error(`Node '${node}' not found in skeleton.`);
|
|
382
|
-
return found;
|
|
383
|
-
}
|
|
384
|
-
get edgeIndices() {
|
|
385
|
-
return this.edges.map((edge) => [this.index(edge.source), this.index(edge.destination)]);
|
|
386
|
-
}
|
|
387
|
-
get symmetryNames() {
|
|
388
|
-
return this.symmetries.map((symmetry) => {
|
|
389
|
-
const nodes = Array.from(symmetry.nodes).map((node) => node.name);
|
|
390
|
-
return [nodes[0], nodes[1]];
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
matches(other) {
|
|
394
|
-
if (this.nodeNames.length !== other.nodeNames.length) return false;
|
|
395
|
-
for (let i = 0; i < this.nodeNames.length; i += 1) {
|
|
396
|
-
if (this.nodeNames[i] !== other.nodeNames[i]) return false;
|
|
397
|
-
}
|
|
398
|
-
return true;
|
|
399
|
-
}
|
|
400
|
-
addEdge(source, destination) {
|
|
401
|
-
this.edges.push(new Edge(this.node(source), this.node(destination)));
|
|
402
|
-
}
|
|
403
|
-
addSymmetry(left, right) {
|
|
404
|
-
this.symmetries.push(new Symmetry([this.node(left), this.node(right)]));
|
|
405
|
-
}
|
|
406
|
-
edgeFrom(edge) {
|
|
407
|
-
const [source, destination] = edge;
|
|
408
|
-
return new Edge(this.node(source), this.node(destination));
|
|
409
|
-
}
|
|
410
|
-
symmetryFrom(symmetry) {
|
|
411
|
-
const [a, b] = symmetry;
|
|
412
|
-
return new Symmetry([this.node(a), this.node(b)]);
|
|
413
|
-
}
|
|
414
|
-
};
|
|
415
|
-
|
|
416
78
|
// src/model/video.ts
|
|
417
79
|
var Video = class {
|
|
418
80
|
filename;
|
|
@@ -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 ?? [];
|
|
@@ -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
|
-
|
|
3056
|
+
toJPEG,
|
|
3057
|
+
toNumpy,
|
|
3058
|
+
toPNG
|
|
2693
3059
|
};
|