@talmolab/sleap-io.js 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +94 -19
- package/dist/index.js +518 -69
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -18,13 +18,16 @@ declare class Video {
|
|
|
18
18
|
backendMetadata: Record<string, unknown>;
|
|
19
19
|
sourceVideo: Video | null;
|
|
20
20
|
openBackend: boolean;
|
|
21
|
+
private _embedded;
|
|
21
22
|
constructor(options: {
|
|
22
23
|
filename: string | string[];
|
|
23
24
|
backend?: VideoBackend | null;
|
|
24
25
|
backendMetadata?: Record<string, unknown>;
|
|
25
26
|
sourceVideo?: Video | null;
|
|
26
27
|
openBackend?: boolean;
|
|
28
|
+
embedded?: boolean;
|
|
27
29
|
});
|
|
30
|
+
get hasEmbeddedImages(): boolean;
|
|
28
31
|
get originalVideo(): Video | null;
|
|
29
32
|
get shape(): [number, number, number, number] | null;
|
|
30
33
|
get fps(): number | null;
|
|
@@ -259,6 +262,10 @@ interface StreamingH5Options {
|
|
|
259
262
|
/** Filename hint for the HDF5 file. */
|
|
260
263
|
filenameHint?: string;
|
|
261
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Source types supported by the streaming HDF5 file.
|
|
267
|
+
*/
|
|
268
|
+
type StreamingH5Source = string | ArrayBuffer | Uint8Array | File;
|
|
262
269
|
/**
|
|
263
270
|
* A streaming HDF5 file handle that uses a Web Worker for range request access.
|
|
264
271
|
*
|
|
@@ -280,12 +287,33 @@ declare class StreamingH5File {
|
|
|
280
287
|
*/
|
|
281
288
|
init(options?: StreamingH5Options): Promise<void>;
|
|
282
289
|
/**
|
|
283
|
-
* Open a remote HDF5 file for streaming access.
|
|
290
|
+
* Open a remote HDF5 file for streaming access via URL.
|
|
284
291
|
*
|
|
285
292
|
* @param url - URL to the HDF5 file (must support HTTP range requests)
|
|
286
293
|
* @param options - Optional settings
|
|
287
294
|
*/
|
|
288
295
|
open(url: string, options?: StreamingH5Options): Promise<void>;
|
|
296
|
+
/**
|
|
297
|
+
* Open a local File object using WORKERFS (zero-copy).
|
|
298
|
+
*
|
|
299
|
+
* @param file - File object from file input or drag-and-drop
|
|
300
|
+
* @param options - Optional settings
|
|
301
|
+
*/
|
|
302
|
+
openLocal(file: File, options?: StreamingH5Options): Promise<void>;
|
|
303
|
+
/**
|
|
304
|
+
* Open an HDF5 file from an ArrayBuffer or Uint8Array.
|
|
305
|
+
*
|
|
306
|
+
* @param buffer - ArrayBuffer or Uint8Array containing the HDF5 file data
|
|
307
|
+
* @param options - Optional settings
|
|
308
|
+
*/
|
|
309
|
+
openBuffer(buffer: ArrayBuffer | Uint8Array, options?: StreamingH5Options): Promise<void>;
|
|
310
|
+
/**
|
|
311
|
+
* Open an HDF5 file from any supported source.
|
|
312
|
+
*
|
|
313
|
+
* @param source - URL string, File, ArrayBuffer, or Uint8Array
|
|
314
|
+
* @param options - Optional settings
|
|
315
|
+
*/
|
|
316
|
+
openAny(source: StreamingH5Source, options?: StreamingH5Options): Promise<void>;
|
|
289
317
|
/**
|
|
290
318
|
* Whether a file is currently open.
|
|
291
319
|
*/
|
|
@@ -341,6 +369,29 @@ declare function isStreamingSupported(): boolean;
|
|
|
341
369
|
* @returns A StreamingH5File instance
|
|
342
370
|
*/
|
|
343
371
|
declare function openStreamingH5(url: string, options?: StreamingH5Options): Promise<StreamingH5File>;
|
|
372
|
+
/**
|
|
373
|
+
* Open an HDF5 file from any supported source using a Web Worker.
|
|
374
|
+
*
|
|
375
|
+
* This is the recommended way to open HDF5 files in the browser as it
|
|
376
|
+
* offloads all h5wasm operations to a Web Worker, avoiding main thread blocking.
|
|
377
|
+
*
|
|
378
|
+
* @param source - URL string, File object, ArrayBuffer, or Uint8Array
|
|
379
|
+
* @param options - Optional settings
|
|
380
|
+
* @returns A StreamingH5File instance
|
|
381
|
+
*
|
|
382
|
+
* @example
|
|
383
|
+
* ```typescript
|
|
384
|
+
* // From URL
|
|
385
|
+
* const file = await openH5Worker("https://example.com/data.h5");
|
|
386
|
+
*
|
|
387
|
+
* // From File (file input)
|
|
388
|
+
* const file = await openH5Worker(inputElement.files[0]);
|
|
389
|
+
*
|
|
390
|
+
* // From ArrayBuffer
|
|
391
|
+
* const file = await openH5Worker(arrayBuffer);
|
|
392
|
+
* ```
|
|
393
|
+
*/
|
|
394
|
+
declare function openH5Worker(source: StreamingH5Source, options?: StreamingH5Options): Promise<StreamingH5File>;
|
|
344
395
|
|
|
345
396
|
/**
|
|
346
397
|
* Video backend for embedded images in HDF5 files accessed via streaming.
|
|
@@ -348,6 +399,10 @@ declare function openStreamingH5(url: string, options?: StreamingH5Options): Pro
|
|
|
348
399
|
* This backend uses StreamingH5File (Web Worker + range requests) instead of
|
|
349
400
|
* a synchronous h5wasm File object, making it suitable for browser environments
|
|
350
401
|
* where the SLP file is loaded via HTTP range requests.
|
|
402
|
+
*
|
|
403
|
+
* Supports two data storage formats:
|
|
404
|
+
* 1. vlen-encoded: Array of individual frame blobs (each index = one frame)
|
|
405
|
+
* 2. Contiguous buffer: Single buffer with all frames concatenated
|
|
351
406
|
*/
|
|
352
407
|
declare class StreamingHdf5VideoBackend implements VideoBackend {
|
|
353
408
|
filename: string;
|
|
@@ -356,10 +411,11 @@ declare class StreamingHdf5VideoBackend implements VideoBackend {
|
|
|
356
411
|
fps?: number;
|
|
357
412
|
private h5file;
|
|
358
413
|
private datasetPath;
|
|
359
|
-
private
|
|
414
|
+
private frameNumberToIndex;
|
|
360
415
|
private format;
|
|
361
416
|
private channelOrder;
|
|
362
417
|
private cachedData;
|
|
418
|
+
private frameOffsets;
|
|
363
419
|
constructor(options: {
|
|
364
420
|
filename: string;
|
|
365
421
|
h5file: StreamingH5File;
|
|
@@ -391,27 +447,36 @@ type OpenH5Options = {
|
|
|
391
447
|
/**
|
|
392
448
|
* Load an SLP file.
|
|
393
449
|
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
450
|
+
* In browser environments, this function automatically uses a Web Worker for all
|
|
451
|
+
* HDF5 operations, keeping the main thread responsive. For URLs, it uses HTTP
|
|
452
|
+
* range requests to download only the data needed rather than the entire file.
|
|
453
|
+
*
|
|
454
|
+
* In Node.js, this uses the native h5wasm bindings directly.
|
|
397
455
|
*
|
|
398
456
|
* @param source - Path, URL, ArrayBuffer, File, or FileSystemFileHandle
|
|
399
457
|
* @param options - Loading options
|
|
400
|
-
* @param options.openVideos - Whether to open video backends (default: true
|
|
458
|
+
* @param options.openVideos - Whether to open video backends (default: true)
|
|
401
459
|
* @param options.h5 - HDF5 options including streaming mode
|
|
402
460
|
* @param options.h5.stream - 'auto' | 'range' | 'download' (default: 'auto')
|
|
403
461
|
*
|
|
404
462
|
* @example
|
|
405
463
|
* ```typescript
|
|
406
|
-
* // Load from URL
|
|
407
|
-
* const labels = await loadSlp('https://example.com/labels.slp'
|
|
408
|
-
*
|
|
409
|
-
*
|
|
464
|
+
* // Browser: Load from URL (automatically uses Worker + range requests)
|
|
465
|
+
* const labels = await loadSlp('https://example.com/labels.slp');
|
|
466
|
+
*
|
|
467
|
+
* // Browser: Load from file input (automatically uses Worker)
|
|
468
|
+
* const labels = await loadSlp(fileInput.files[0]);
|
|
469
|
+
*
|
|
470
|
+
* // Browser: Load from ArrayBuffer (automatically uses Worker)
|
|
471
|
+
* const labels = await loadSlp(arrayBuffer);
|
|
410
472
|
*
|
|
411
|
-
* // Force full download
|
|
473
|
+
* // Force full download instead of range requests
|
|
412
474
|
* const labels = await loadSlp('https://example.com/labels.slp', {
|
|
413
475
|
* h5: { stream: 'download' }
|
|
414
476
|
* });
|
|
477
|
+
*
|
|
478
|
+
* // Node.js: Load from file path
|
|
479
|
+
* const labels = await loadSlp('/path/to/file.slp');
|
|
415
480
|
* ```
|
|
416
481
|
*/
|
|
417
482
|
declare function loadSlp(source: SlpSource, options?: {
|
|
@@ -738,28 +803,38 @@ interface StreamingSlpOptions {
|
|
|
738
803
|
openVideos?: boolean;
|
|
739
804
|
}
|
|
740
805
|
/**
|
|
741
|
-
* Read an SLP file using
|
|
806
|
+
* Read an SLP file using a Web Worker for efficient, non-blocking HDF5 access.
|
|
742
807
|
*
|
|
743
|
-
* This function
|
|
744
|
-
*
|
|
808
|
+
* This function offloads all h5wasm operations to a Web Worker, keeping the
|
|
809
|
+
* main thread responsive. For URLs, it uses HTTP range requests to download
|
|
810
|
+
* only the data needed rather than the entire file.
|
|
745
811
|
*
|
|
746
812
|
* When `openVideos` is true, video backends are created for embedded videos,
|
|
747
813
|
* allowing frame data to be retrieved. The underlying HDF5 file remains open
|
|
748
814
|
* until all video backends are closed.
|
|
749
815
|
*
|
|
750
|
-
* @param
|
|
816
|
+
* @param source - URL, File, ArrayBuffer, or Uint8Array containing the SLP file
|
|
751
817
|
* @param options - Optional settings
|
|
752
818
|
* @returns Labels object with all annotation data
|
|
753
819
|
*
|
|
754
820
|
* @example
|
|
755
821
|
* ```typescript
|
|
756
|
-
* // Load with video backends
|
|
822
|
+
* // Load from URL with video backends
|
|
757
823
|
* const labels = await readSlpStreaming('https://example.com/labels.slp', {
|
|
758
824
|
* openVideos: true
|
|
759
825
|
* });
|
|
760
|
-
*
|
|
826
|
+
*
|
|
827
|
+
* // Load from File object (file input)
|
|
828
|
+
* const labels = await readSlpStreaming(fileInput.files[0], {
|
|
829
|
+
* openVideos: true
|
|
830
|
+
* });
|
|
831
|
+
*
|
|
832
|
+
* // Load from ArrayBuffer
|
|
833
|
+
* const labels = await readSlpStreaming(arrayBuffer, {
|
|
834
|
+
* filenameHint: 'data.slp'
|
|
835
|
+
* });
|
|
761
836
|
* ```
|
|
762
837
|
*/
|
|
763
|
-
declare function readSlpStreaming(
|
|
838
|
+
declare function readSlpStreaming(source: StreamingH5Source, options?: StreamingSlpOptions): Promise<Labels>;
|
|
764
839
|
|
|
765
|
-
export { Camera, CameraGroup, type ColorScheme, type ColorSpec, FrameGroup, Instance, InstanceContext, InstanceGroup, LabeledFrame, Labels, type LabelsDict, LabelsSet, MARKER_FUNCTIONS, type MarkerShape, Mp4BoxVideoBackend, NAMED_COLORS, PALETTES, type PaletteName, PredictedInstance, type RGB, type RGBA, RecordingSession, RenderContext, type RenderOptions, Skeleton, StreamingH5File, StreamingHdf5VideoBackend, SuggestionFrame, Track, Video, type VideoBackend, type VideoFrame, type VideoOptions, checkFfmpeg, decodeYamlSkeleton, determineColorScheme, drawCircle, drawCross, drawDiamond, drawSquare, drawTriangle, encodeYamlSkeleton, fromDict, fromNumpy, getMarkerFunction, getPalette, isStreamingSupported, labelsFromNumpy, loadSlp, loadVideo, makeCameraFromDict, openStreamingH5, readSlpStreaming, renderImage, renderVideo, resolveColor, rgbToCSS, rodriguesTransformation, saveImage, saveSlp, toDataURL, toDict, toJPEG, toNumpy, toPNG };
|
|
840
|
+
export { Camera, CameraGroup, type ColorScheme, type ColorSpec, FrameGroup, Instance, InstanceContext, InstanceGroup, LabeledFrame, Labels, type LabelsDict, LabelsSet, MARKER_FUNCTIONS, type MarkerShape, Mp4BoxVideoBackend, NAMED_COLORS, PALETTES, type PaletteName, PredictedInstance, type RGB, type RGBA, RecordingSession, RenderContext, type RenderOptions, Skeleton, StreamingH5File, type StreamingH5Source, StreamingHdf5VideoBackend, SuggestionFrame, Track, Video, type VideoBackend, type VideoFrame, type VideoOptions, checkFfmpeg, decodeYamlSkeleton, determineColorScheme, drawCircle, drawCross, drawDiamond, drawSquare, drawTriangle, encodeYamlSkeleton, fromDict, fromNumpy, getMarkerFunction, getPalette, isStreamingSupported, labelsFromNumpy, loadSlp, loadVideo, makeCameraFromDict, openH5Worker, openStreamingH5, readSlpStreaming, renderImage, renderVideo, resolveColor, rgbToCSS, rodriguesTransformation, saveImage, saveSlp, toDataURL, toDict, toJPEG, toNumpy, toPNG };
|
package/dist/index.js
CHANGED
|
@@ -85,12 +85,17 @@ var Video = class {
|
|
|
85
85
|
backendMetadata;
|
|
86
86
|
sourceVideo;
|
|
87
87
|
openBackend;
|
|
88
|
+
_embedded;
|
|
88
89
|
constructor(options) {
|
|
89
90
|
this.filename = options.filename;
|
|
90
91
|
this.backend = options.backend ?? null;
|
|
91
92
|
this.backendMetadata = options.backendMetadata ?? {};
|
|
92
93
|
this.sourceVideo = options.sourceVideo ?? null;
|
|
93
94
|
this.openBackend = options.openBackend ?? true;
|
|
95
|
+
this._embedded = options.embedded ?? false;
|
|
96
|
+
}
|
|
97
|
+
get hasEmbeddedImages() {
|
|
98
|
+
return this._embedded;
|
|
94
99
|
}
|
|
95
100
|
get originalVideo() {
|
|
96
101
|
if (!this.sourceVideo) return null;
|
|
@@ -1208,6 +1213,8 @@ var Mp4BoxVideoBackend = class {
|
|
|
1208
1213
|
|
|
1209
1214
|
// src/video/streaming-hdf5-video.ts
|
|
1210
1215
|
var isBrowser3 = typeof window !== "undefined" && typeof document !== "undefined";
|
|
1216
|
+
var PNG_MAGIC = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
1217
|
+
var JPEG_MAGIC = new Uint8Array([255, 216, 255]);
|
|
1211
1218
|
var StreamingHdf5VideoBackend = class {
|
|
1212
1219
|
filename;
|
|
1213
1220
|
dataset;
|
|
@@ -1215,39 +1222,58 @@ var StreamingHdf5VideoBackend = class {
|
|
|
1215
1222
|
fps;
|
|
1216
1223
|
h5file;
|
|
1217
1224
|
datasetPath;
|
|
1218
|
-
|
|
1225
|
+
frameNumberToIndex;
|
|
1219
1226
|
format;
|
|
1220
1227
|
channelOrder;
|
|
1221
1228
|
cachedData;
|
|
1229
|
+
frameOffsets;
|
|
1230
|
+
// For contiguous buffer: byte offsets of each frame
|
|
1222
1231
|
constructor(options) {
|
|
1223
1232
|
this.filename = options.filename;
|
|
1224
1233
|
this.h5file = options.h5file;
|
|
1225
1234
|
this.datasetPath = options.datasetPath;
|
|
1226
1235
|
this.dataset = options.datasetPath;
|
|
1227
|
-
|
|
1236
|
+
const frameNumbers = options.frameNumbers ?? [];
|
|
1237
|
+
this.frameNumberToIndex = new Map(frameNumbers.map((num, idx) => [num, idx]));
|
|
1228
1238
|
this.format = options.format ?? "png";
|
|
1229
1239
|
this.channelOrder = options.channelOrder ?? "RGB";
|
|
1230
1240
|
this.shape = options.shape;
|
|
1231
1241
|
this.fps = options.fps;
|
|
1232
1242
|
this.cachedData = null;
|
|
1243
|
+
this.frameOffsets = null;
|
|
1233
1244
|
}
|
|
1234
1245
|
async getFrame(frameIndex) {
|
|
1235
|
-
const index = this.
|
|
1236
|
-
if (index
|
|
1246
|
+
const index = this.frameNumberToIndex.size > 0 ? this.frameNumberToIndex.get(frameIndex) : frameIndex;
|
|
1247
|
+
if (index === void 0) return null;
|
|
1237
1248
|
if (!this.cachedData) {
|
|
1238
1249
|
try {
|
|
1239
1250
|
const data = await this.h5file.getDatasetValue(this.datasetPath);
|
|
1240
|
-
this.cachedData = data.value;
|
|
1251
|
+
this.cachedData = normalizeVideoData(data.value, data.shape);
|
|
1252
|
+
if (isContiguousEncodedBuffer(this.cachedData, this.format, this.shape)) {
|
|
1253
|
+
this.frameOffsets = findEncodedFrameOffsets(
|
|
1254
|
+
this.cachedData,
|
|
1255
|
+
this.format,
|
|
1256
|
+
this.shape?.[0] ?? 0
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1241
1259
|
} catch {
|
|
1242
1260
|
return null;
|
|
1243
1261
|
}
|
|
1244
1262
|
}
|
|
1245
|
-
|
|
1246
|
-
if (
|
|
1247
|
-
|
|
1248
|
-
|
|
1263
|
+
let rawBytes;
|
|
1264
|
+
if (this.frameOffsets && this.frameOffsets.length > index) {
|
|
1265
|
+
const buffer = this.cachedData;
|
|
1266
|
+
const start = this.frameOffsets[index];
|
|
1267
|
+
const end = index + 1 < this.frameOffsets.length ? this.frameOffsets[index + 1] : buffer.length;
|
|
1268
|
+
rawBytes = buffer.slice(start, end);
|
|
1269
|
+
} else {
|
|
1270
|
+
const entry = this.cachedData[index];
|
|
1271
|
+
if (entry == null) return null;
|
|
1272
|
+
rawBytes = toUint8Array(entry);
|
|
1273
|
+
}
|
|
1274
|
+
if (!rawBytes || rawBytes.length === 0) return null;
|
|
1249
1275
|
if (isEncodedFormat(this.format)) {
|
|
1250
|
-
const decoded = await decodeImageBytes(rawBytes, this.format);
|
|
1276
|
+
const decoded = await decodeImageBytes(rawBytes, this.format, this.channelOrder);
|
|
1251
1277
|
return decoded ?? rawBytes;
|
|
1252
1278
|
}
|
|
1253
1279
|
const image = decodeRawFrame(rawBytes, this.shape, this.channelOrder);
|
|
@@ -1255,8 +1281,55 @@ var StreamingHdf5VideoBackend = class {
|
|
|
1255
1281
|
}
|
|
1256
1282
|
close() {
|
|
1257
1283
|
this.cachedData = null;
|
|
1284
|
+
this.frameOffsets = null;
|
|
1258
1285
|
}
|
|
1259
1286
|
};
|
|
1287
|
+
function normalizeVideoData(value, _shape) {
|
|
1288
|
+
if (Array.isArray(value)) {
|
|
1289
|
+
return value;
|
|
1290
|
+
}
|
|
1291
|
+
if (ArrayBuffer.isView(value)) {
|
|
1292
|
+
const arr = value;
|
|
1293
|
+
return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
1294
|
+
}
|
|
1295
|
+
return [];
|
|
1296
|
+
}
|
|
1297
|
+
function isContiguousEncodedBuffer(data, format, shape) {
|
|
1298
|
+
if (!isEncodedFormat(format)) return false;
|
|
1299
|
+
if (!(data instanceof Uint8Array)) return false;
|
|
1300
|
+
if (data.length < 8) return false;
|
|
1301
|
+
const isPng = matchesMagic(data, PNG_MAGIC);
|
|
1302
|
+
const isJpeg = matchesMagic(data, JPEG_MAGIC);
|
|
1303
|
+
if (!isPng && !isJpeg) return false;
|
|
1304
|
+
if (shape) {
|
|
1305
|
+
const frameCount = shape[0];
|
|
1306
|
+
if (frameCount > 1 && data.length > 1e4) {
|
|
1307
|
+
return true;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
return true;
|
|
1311
|
+
}
|
|
1312
|
+
function matchesMagic(buffer, magic) {
|
|
1313
|
+
if (buffer.length < magic.length) return false;
|
|
1314
|
+
for (let i = 0; i < magic.length; i++) {
|
|
1315
|
+
if (buffer[i] !== magic[i]) return false;
|
|
1316
|
+
}
|
|
1317
|
+
return true;
|
|
1318
|
+
}
|
|
1319
|
+
function findEncodedFrameOffsets(buffer, format, expectedFrameCount) {
|
|
1320
|
+
const offsets = [];
|
|
1321
|
+
const magic = format.toLowerCase() === "png" ? PNG_MAGIC : JPEG_MAGIC;
|
|
1322
|
+
for (let i = 0; i <= buffer.length - magic.length; i++) {
|
|
1323
|
+
if (matchesMagic(buffer.subarray(i), magic)) {
|
|
1324
|
+
offsets.push(i);
|
|
1325
|
+
i += magic.length - 1;
|
|
1326
|
+
if (expectedFrameCount > 0 && offsets.length >= expectedFrameCount) {
|
|
1327
|
+
break;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return offsets;
|
|
1332
|
+
}
|
|
1260
1333
|
function toUint8Array(entry) {
|
|
1261
1334
|
if (entry instanceof Uint8Array) return entry;
|
|
1262
1335
|
if (entry instanceof ArrayBuffer) return new Uint8Array(entry);
|
|
@@ -1271,12 +1344,29 @@ function isEncodedFormat(format) {
|
|
|
1271
1344
|
const normalized = format.toLowerCase();
|
|
1272
1345
|
return normalized === "png" || normalized === "jpg" || normalized === "jpeg";
|
|
1273
1346
|
}
|
|
1274
|
-
async function decodeImageBytes(bytes, format) {
|
|
1347
|
+
async function decodeImageBytes(bytes, format, channelOrder) {
|
|
1275
1348
|
if (!isBrowser3 || typeof createImageBitmap === "undefined") return null;
|
|
1276
1349
|
const mime = format.toLowerCase() === "png" ? "image/png" : "image/jpeg";
|
|
1277
1350
|
const safeBytes = new Uint8Array(bytes);
|
|
1278
1351
|
const blob = new Blob([safeBytes.buffer], { type: mime });
|
|
1279
|
-
|
|
1352
|
+
const bitmap = await createImageBitmap(blob);
|
|
1353
|
+
const useBgr = channelOrder.toUpperCase() === "BGR";
|
|
1354
|
+
if (!useBgr) {
|
|
1355
|
+
return bitmap;
|
|
1356
|
+
}
|
|
1357
|
+
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
1358
|
+
const ctx = canvas.getContext("2d");
|
|
1359
|
+
if (!ctx) return bitmap;
|
|
1360
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
1361
|
+
const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
1362
|
+
const data = imageData.data;
|
|
1363
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
1364
|
+
const r = data[i];
|
|
1365
|
+
const b = data[i + 2];
|
|
1366
|
+
data[i] = b;
|
|
1367
|
+
data[i + 2] = r;
|
|
1368
|
+
}
|
|
1369
|
+
return imageData;
|
|
1280
1370
|
}
|
|
1281
1371
|
function decodeRawFrame(bytes, shape, channelOrder) {
|
|
1282
1372
|
if (!isBrowser3 || !shape) return null;
|
|
@@ -1304,7 +1394,8 @@ function decodeRawFrame(bytes, shape, channelOrder) {
|
|
|
1304
1394
|
// src/codecs/slp/h5-worker.ts
|
|
1305
1395
|
var H5_WORKER_CODE = `
|
|
1306
1396
|
// h5wasm streaming worker
|
|
1307
|
-
//
|
|
1397
|
+
// Handles all HDF5 operations in a Web Worker to avoid main thread blocking
|
|
1398
|
+
// Supports: URL streaming (range requests), local files (WORKERFS), and ArrayBuffers
|
|
1308
1399
|
|
|
1309
1400
|
let h5wasmModule = null;
|
|
1310
1401
|
let FS = null;
|
|
@@ -1322,8 +1413,18 @@ self.onmessage = async function(e) {
|
|
|
1322
1413
|
break;
|
|
1323
1414
|
|
|
1324
1415
|
case 'openUrl':
|
|
1325
|
-
const
|
|
1326
|
-
respond(id,
|
|
1416
|
+
const urlResult = await openRemoteFile(payload.url, payload.filename);
|
|
1417
|
+
respond(id, urlResult);
|
|
1418
|
+
break;
|
|
1419
|
+
|
|
1420
|
+
case 'openLocal':
|
|
1421
|
+
const localResult = await openLocalFile(payload.file, payload.filename);
|
|
1422
|
+
respond(id, localResult);
|
|
1423
|
+
break;
|
|
1424
|
+
|
|
1425
|
+
case 'openBuffer':
|
|
1426
|
+
const bufferResult = await openBufferFile(payload.buffer, payload.filename);
|
|
1427
|
+
respond(id, bufferResult);
|
|
1327
1428
|
break;
|
|
1328
1429
|
|
|
1329
1430
|
case 'getKeys':
|
|
@@ -1414,6 +1515,66 @@ async function openRemoteFile(url, filename = 'data.h5') {
|
|
|
1414
1515
|
};
|
|
1415
1516
|
}
|
|
1416
1517
|
|
|
1518
|
+
async function openLocalFile(file, filename) {
|
|
1519
|
+
if (!h5wasmModule) {
|
|
1520
|
+
throw new Error('h5wasm not initialized');
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// Close any existing file
|
|
1524
|
+
closeFile();
|
|
1525
|
+
|
|
1526
|
+
// Use provided filename or file.name
|
|
1527
|
+
const fname = filename || file.name || 'local.h5';
|
|
1528
|
+
|
|
1529
|
+
// Create mount point for WORKERFS
|
|
1530
|
+
mountPath = '/local-' + Date.now();
|
|
1531
|
+
FS.mkdir(mountPath);
|
|
1532
|
+
|
|
1533
|
+
// Mount the file using WORKERFS (zero-copy access)
|
|
1534
|
+
FS.mount(FS.filesystems.WORKERFS, { files: [file] }, mountPath);
|
|
1535
|
+
|
|
1536
|
+
// Open with h5wasm
|
|
1537
|
+
const filePath = mountPath + '/' + fname;
|
|
1538
|
+
currentFile = new h5wasm.File(filePath, 'r');
|
|
1539
|
+
|
|
1540
|
+
return {
|
|
1541
|
+
success: true,
|
|
1542
|
+
path: currentFile.path,
|
|
1543
|
+
filename: currentFile.filename,
|
|
1544
|
+
keys: currentFile.keys()
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
async function openBufferFile(buffer, filename = 'data.h5') {
|
|
1549
|
+
if (!h5wasmModule) {
|
|
1550
|
+
throw new Error('h5wasm not initialized');
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// Close any existing file
|
|
1554
|
+
closeFile();
|
|
1555
|
+
|
|
1556
|
+
// Write buffer to virtual filesystem
|
|
1557
|
+
const data = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
1558
|
+
mountPath = '/buffer-' + Date.now() + '/' + filename;
|
|
1559
|
+
|
|
1560
|
+
// Create parent directory
|
|
1561
|
+
const dir = mountPath.substring(0, mountPath.lastIndexOf('/'));
|
|
1562
|
+
FS.mkdir(dir);
|
|
1563
|
+
|
|
1564
|
+
// Write file to virtual FS
|
|
1565
|
+
FS.writeFile(mountPath, data);
|
|
1566
|
+
|
|
1567
|
+
// Open with h5wasm
|
|
1568
|
+
currentFile = new h5wasm.File(mountPath, 'r');
|
|
1569
|
+
|
|
1570
|
+
return {
|
|
1571
|
+
success: true,
|
|
1572
|
+
path: currentFile.path,
|
|
1573
|
+
filename: currentFile.filename,
|
|
1574
|
+
keys: currentFile.keys()
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1417
1578
|
function getKeys(path) {
|
|
1418
1579
|
if (!currentFile) throw new Error('No file open');
|
|
1419
1580
|
const item = path === '/' || !path ? currentFile : currentFile.get(path);
|
|
@@ -1421,19 +1582,38 @@ function getKeys(path) {
|
|
|
1421
1582
|
return item.keys ? item.keys() : [];
|
|
1422
1583
|
}
|
|
1423
1584
|
|
|
1585
|
+
function serializeAttrValue(attr) {
|
|
1586
|
+
if (!attr) return null;
|
|
1587
|
+
// h5wasm Attribute objects have a .value property
|
|
1588
|
+
const val = attr.value !== undefined ? attr.value : attr;
|
|
1589
|
+
// Convert Uint8Array to string for JSON attributes
|
|
1590
|
+
if (val instanceof Uint8Array) {
|
|
1591
|
+
return { value: new TextDecoder().decode(val) };
|
|
1592
|
+
}
|
|
1593
|
+
// Wrap primitive values to preserve structure
|
|
1594
|
+
return { value: val };
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1424
1597
|
function getAttr(path, name) {
|
|
1425
1598
|
if (!currentFile) throw new Error('No file open');
|
|
1426
1599
|
const item = path === '/' || !path ? currentFile : currentFile.get(path);
|
|
1427
1600
|
if (!item) throw new Error('Path not found: ' + path);
|
|
1428
1601
|
const attrs = item.attrs;
|
|
1429
|
-
|
|
1602
|
+
const attr = attrs?.[name];
|
|
1603
|
+
return serializeAttrValue(attr);
|
|
1430
1604
|
}
|
|
1431
1605
|
|
|
1432
1606
|
function getAttrs(path) {
|
|
1433
1607
|
if (!currentFile) throw new Error('No file open');
|
|
1434
1608
|
const item = path === '/' || !path ? currentFile : currentFile.get(path);
|
|
1435
1609
|
if (!item) throw new Error('Path not found: ' + path);
|
|
1436
|
-
|
|
1610
|
+
const rawAttrs = item.attrs || {};
|
|
1611
|
+
// Serialize all attributes for proper transfer through postMessage
|
|
1612
|
+
const serialized = {};
|
|
1613
|
+
for (const key of Object.keys(rawAttrs)) {
|
|
1614
|
+
serialized[key] = serializeAttrValue(rawAttrs[key]);
|
|
1615
|
+
}
|
|
1616
|
+
return serialized;
|
|
1437
1617
|
}
|
|
1438
1618
|
|
|
1439
1619
|
function getDatasetMeta(path) {
|
|
@@ -1586,7 +1766,7 @@ var StreamingH5File = class {
|
|
|
1586
1766
|
await this.send("init", { h5wasmUrl: options?.h5wasmUrl });
|
|
1587
1767
|
}
|
|
1588
1768
|
/**
|
|
1589
|
-
* Open a remote HDF5 file for streaming access.
|
|
1769
|
+
* Open a remote HDF5 file for streaming access via URL.
|
|
1590
1770
|
*
|
|
1591
1771
|
* @param url - URL to the HDF5 file (must support HTTP range requests)
|
|
1592
1772
|
* @param options - Optional settings
|
|
@@ -1598,6 +1778,51 @@ var StreamingH5File = class {
|
|
|
1598
1778
|
this._keys = result.keys || [];
|
|
1599
1779
|
this._isOpen = true;
|
|
1600
1780
|
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Open a local File object using WORKERFS (zero-copy).
|
|
1783
|
+
*
|
|
1784
|
+
* @param file - File object from file input or drag-and-drop
|
|
1785
|
+
* @param options - Optional settings
|
|
1786
|
+
*/
|
|
1787
|
+
async openLocal(file, options) {
|
|
1788
|
+
await this.init(options);
|
|
1789
|
+
const filename = options?.filenameHint || file.name || "data.h5";
|
|
1790
|
+
const result = await this.send("openLocal", { file, filename });
|
|
1791
|
+
this._keys = result.keys || [];
|
|
1792
|
+
this._isOpen = true;
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Open an HDF5 file from an ArrayBuffer or Uint8Array.
|
|
1796
|
+
*
|
|
1797
|
+
* @param buffer - ArrayBuffer or Uint8Array containing the HDF5 file data
|
|
1798
|
+
* @param options - Optional settings
|
|
1799
|
+
*/
|
|
1800
|
+
async openBuffer(buffer, options) {
|
|
1801
|
+
await this.init(options);
|
|
1802
|
+
const filename = options?.filenameHint || "data.h5";
|
|
1803
|
+
const data = buffer instanceof Uint8Array ? buffer.buffer : buffer;
|
|
1804
|
+
const result = await this.send("openBuffer", { buffer: data, filename });
|
|
1805
|
+
this._keys = result.keys || [];
|
|
1806
|
+
this._isOpen = true;
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Open an HDF5 file from any supported source.
|
|
1810
|
+
*
|
|
1811
|
+
* @param source - URL string, File, ArrayBuffer, or Uint8Array
|
|
1812
|
+
* @param options - Optional settings
|
|
1813
|
+
*/
|
|
1814
|
+
async openAny(source, options) {
|
|
1815
|
+
if (typeof source === "string") {
|
|
1816
|
+
return this.open(source, options);
|
|
1817
|
+
}
|
|
1818
|
+
if (typeof File !== "undefined" && source instanceof File) {
|
|
1819
|
+
return this.openLocal(source, options);
|
|
1820
|
+
}
|
|
1821
|
+
if (source instanceof ArrayBuffer || source instanceof Uint8Array) {
|
|
1822
|
+
return this.openBuffer(source, options);
|
|
1823
|
+
}
|
|
1824
|
+
throw new Error("Unsupported source type for StreamingH5File");
|
|
1825
|
+
}
|
|
1601
1826
|
/**
|
|
1602
1827
|
* Whether a file is currently open.
|
|
1603
1828
|
*/
|
|
@@ -1677,6 +1902,14 @@ async function openStreamingH5(url, options) {
|
|
|
1677
1902
|
await file.open(url, options);
|
|
1678
1903
|
return file;
|
|
1679
1904
|
}
|
|
1905
|
+
async function openH5Worker(source, options) {
|
|
1906
|
+
if (!isStreamingSupported()) {
|
|
1907
|
+
throw new Error("Web Worker HDF5 access requires Worker/Blob/URL support");
|
|
1908
|
+
}
|
|
1909
|
+
const file = new StreamingH5File();
|
|
1910
|
+
await file.openAny(source, options);
|
|
1911
|
+
return file;
|
|
1912
|
+
}
|
|
1680
1913
|
|
|
1681
1914
|
// src/codecs/slp/h5.ts
|
|
1682
1915
|
var isNode = typeof process !== "undefined" && !!process.versions?.node;
|
|
@@ -1815,6 +2048,8 @@ function getH5FileSystem(module) {
|
|
|
1815
2048
|
|
|
1816
2049
|
// src/video/hdf5-video.ts
|
|
1817
2050
|
var isBrowser4 = typeof window !== "undefined" && typeof document !== "undefined";
|
|
2051
|
+
var PNG_MAGIC2 = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
2052
|
+
var JPEG_MAGIC2 = new Uint8Array([255, 216, 255]);
|
|
1818
2053
|
var Hdf5VideoBackend = class {
|
|
1819
2054
|
filename;
|
|
1820
2055
|
dataset;
|
|
@@ -1822,36 +2057,55 @@ var Hdf5VideoBackend = class {
|
|
|
1822
2057
|
fps;
|
|
1823
2058
|
file;
|
|
1824
2059
|
datasetPath;
|
|
1825
|
-
|
|
2060
|
+
frameNumberToIndex;
|
|
1826
2061
|
format;
|
|
1827
2062
|
channelOrder;
|
|
1828
2063
|
cachedData;
|
|
2064
|
+
frameOffsets;
|
|
1829
2065
|
constructor(options) {
|
|
1830
2066
|
this.filename = options.filename;
|
|
1831
2067
|
this.file = options.file;
|
|
1832
2068
|
this.datasetPath = options.datasetPath;
|
|
1833
2069
|
this.dataset = options.datasetPath;
|
|
1834
|
-
|
|
2070
|
+
const frameNumbers = options.frameNumbers ?? [];
|
|
2071
|
+
this.frameNumberToIndex = new Map(frameNumbers.map((num, idx) => [num, idx]));
|
|
1835
2072
|
this.format = options.format ?? "png";
|
|
1836
2073
|
this.channelOrder = options.channelOrder ?? "RGB";
|
|
1837
2074
|
this.shape = options.shape;
|
|
1838
2075
|
this.fps = options.fps;
|
|
1839
2076
|
this.cachedData = null;
|
|
2077
|
+
this.frameOffsets = null;
|
|
1840
2078
|
}
|
|
1841
2079
|
async getFrame(frameIndex) {
|
|
1842
2080
|
const dataset = this.file.get(this.datasetPath);
|
|
1843
2081
|
if (!dataset) return null;
|
|
1844
|
-
const index = this.
|
|
1845
|
-
if (index
|
|
2082
|
+
const index = this.frameNumberToIndex.size > 0 ? this.frameNumberToIndex.get(frameIndex) : frameIndex;
|
|
2083
|
+
if (index === void 0) return null;
|
|
1846
2084
|
if (!this.cachedData) {
|
|
1847
|
-
|
|
2085
|
+
const value = dataset.value;
|
|
2086
|
+
this.cachedData = normalizeVideoData2(value);
|
|
2087
|
+
if (isContiguousEncodedBuffer2(this.cachedData, this.format, this.shape)) {
|
|
2088
|
+
this.frameOffsets = findEncodedFrameOffsets2(
|
|
2089
|
+
this.cachedData,
|
|
2090
|
+
this.format,
|
|
2091
|
+
this.shape?.[0] ?? 0
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
let rawBytes;
|
|
2096
|
+
if (this.frameOffsets && this.frameOffsets.length > index) {
|
|
2097
|
+
const buffer = this.cachedData;
|
|
2098
|
+
const start = this.frameOffsets[index];
|
|
2099
|
+
const end = index + 1 < this.frameOffsets.length ? this.frameOffsets[index + 1] : buffer.length;
|
|
2100
|
+
rawBytes = buffer.slice(start, end);
|
|
2101
|
+
} else {
|
|
2102
|
+
const entry = this.cachedData[index];
|
|
2103
|
+
if (entry == null) return null;
|
|
2104
|
+
rawBytes = toUint8Array2(entry);
|
|
1848
2105
|
}
|
|
1849
|
-
|
|
1850
|
-
if (entry == null) return null;
|
|
1851
|
-
const rawBytes = toUint8Array2(entry);
|
|
1852
|
-
if (!rawBytes) return null;
|
|
2106
|
+
if (!rawBytes || rawBytes.length === 0) return null;
|
|
1853
2107
|
if (isEncodedFormat2(this.format)) {
|
|
1854
|
-
const decoded = await decodeImageBytes2(rawBytes, this.format);
|
|
2108
|
+
const decoded = await decodeImageBytes2(rawBytes, this.format, this.channelOrder);
|
|
1855
2109
|
return decoded ?? rawBytes;
|
|
1856
2110
|
}
|
|
1857
2111
|
const image = decodeRawFrame2(rawBytes, this.shape, this.channelOrder);
|
|
@@ -1859,8 +2113,55 @@ var Hdf5VideoBackend = class {
|
|
|
1859
2113
|
}
|
|
1860
2114
|
close() {
|
|
1861
2115
|
this.cachedData = null;
|
|
2116
|
+
this.frameOffsets = null;
|
|
1862
2117
|
}
|
|
1863
2118
|
};
|
|
2119
|
+
function normalizeVideoData2(value) {
|
|
2120
|
+
if (Array.isArray(value)) {
|
|
2121
|
+
return value;
|
|
2122
|
+
}
|
|
2123
|
+
if (ArrayBuffer.isView(value)) {
|
|
2124
|
+
const arr = value;
|
|
2125
|
+
return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
2126
|
+
}
|
|
2127
|
+
return [];
|
|
2128
|
+
}
|
|
2129
|
+
function isContiguousEncodedBuffer2(data, format, shape) {
|
|
2130
|
+
if (!isEncodedFormat2(format)) return false;
|
|
2131
|
+
if (!(data instanceof Uint8Array)) return false;
|
|
2132
|
+
if (data.length < 8) return false;
|
|
2133
|
+
const isPng = matchesMagic2(data, PNG_MAGIC2);
|
|
2134
|
+
const isJpeg = matchesMagic2(data, JPEG_MAGIC2);
|
|
2135
|
+
if (!isPng && !isJpeg) return false;
|
|
2136
|
+
if (shape) {
|
|
2137
|
+
const frameCount = shape[0];
|
|
2138
|
+
if (frameCount > 1 && data.length > 1e4) {
|
|
2139
|
+
return true;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
return true;
|
|
2143
|
+
}
|
|
2144
|
+
function matchesMagic2(buffer, magic) {
|
|
2145
|
+
if (buffer.length < magic.length) return false;
|
|
2146
|
+
for (let i = 0; i < magic.length; i++) {
|
|
2147
|
+
if (buffer[i] !== magic[i]) return false;
|
|
2148
|
+
}
|
|
2149
|
+
return true;
|
|
2150
|
+
}
|
|
2151
|
+
function findEncodedFrameOffsets2(buffer, format, expectedFrameCount) {
|
|
2152
|
+
const offsets = [];
|
|
2153
|
+
const magic = format.toLowerCase() === "png" ? PNG_MAGIC2 : JPEG_MAGIC2;
|
|
2154
|
+
for (let i = 0; i <= buffer.length - magic.length; i++) {
|
|
2155
|
+
if (matchesMagic2(buffer.subarray(i), magic)) {
|
|
2156
|
+
offsets.push(i);
|
|
2157
|
+
i += magic.length - 1;
|
|
2158
|
+
if (expectedFrameCount > 0 && offsets.length >= expectedFrameCount) {
|
|
2159
|
+
break;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
return offsets;
|
|
2164
|
+
}
|
|
1864
2165
|
function toUint8Array2(entry) {
|
|
1865
2166
|
if (entry instanceof Uint8Array) return entry;
|
|
1866
2167
|
if (entry instanceof ArrayBuffer) return new Uint8Array(entry);
|
|
@@ -1873,12 +2174,29 @@ function isEncodedFormat2(format) {
|
|
|
1873
2174
|
const normalized = format.toLowerCase();
|
|
1874
2175
|
return normalized === "png" || normalized === "jpg" || normalized === "jpeg";
|
|
1875
2176
|
}
|
|
1876
|
-
async function decodeImageBytes2(bytes, format) {
|
|
2177
|
+
async function decodeImageBytes2(bytes, format, channelOrder) {
|
|
1877
2178
|
if (!isBrowser4 || typeof createImageBitmap === "undefined") return null;
|
|
1878
2179
|
const mime = format.toLowerCase() === "png" ? "image/png" : "image/jpeg";
|
|
1879
2180
|
const safeBytes = new Uint8Array(bytes);
|
|
1880
2181
|
const blob = new Blob([safeBytes.buffer], { type: mime });
|
|
1881
|
-
|
|
2182
|
+
const bitmap = await createImageBitmap(blob);
|
|
2183
|
+
const useBgr = channelOrder.toUpperCase() === "BGR";
|
|
2184
|
+
if (!useBgr) {
|
|
2185
|
+
return bitmap;
|
|
2186
|
+
}
|
|
2187
|
+
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
2188
|
+
const ctx = canvas.getContext("2d");
|
|
2189
|
+
if (!ctx) return bitmap;
|
|
2190
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
2191
|
+
const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
2192
|
+
const data = imageData.data;
|
|
2193
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
2194
|
+
const r = data[i];
|
|
2195
|
+
const b = data[i + 2];
|
|
2196
|
+
data[i] = b;
|
|
2197
|
+
data[i + 2] = r;
|
|
2198
|
+
}
|
|
2199
|
+
return imageData;
|
|
1882
2200
|
}
|
|
1883
2201
|
function decodeRawFrame2(bytes, shape, channelOrder) {
|
|
1884
2202
|
if (!isBrowser4 || !shape) return null;
|
|
@@ -1942,7 +2260,7 @@ async function readSlp(source, options) {
|
|
|
1942
2260
|
const labelsPath = typeof source === "string" ? source : options?.h5?.filenameHint ?? "slp-data.slp";
|
|
1943
2261
|
const skeletons = parseSkeletons(metadataJson);
|
|
1944
2262
|
const tracks = readTracks(file.get("tracks_json"));
|
|
1945
|
-
const videos = await readVideos(file.get("videos_json"), labelsPath, options?.openVideos ?? true, file);
|
|
2263
|
+
const videos = await readVideos(file.get("videos_json"), labelsPath, options?.openVideos ?? true, file, formatId);
|
|
1946
2264
|
const suggestions = readSuggestions(file.get("suggestions_json"), videos);
|
|
1947
2265
|
const framesData = normalizeStructDataset(file.get("frames"));
|
|
1948
2266
|
const instancesData = normalizeStructDataset(file.get("instances"));
|
|
@@ -1995,11 +2313,12 @@ function readTracks(dataset) {
|
|
|
1995
2313
|
}
|
|
1996
2314
|
return tracks;
|
|
1997
2315
|
}
|
|
1998
|
-
async function readVideos(dataset, labelsPath, openVideos, file) {
|
|
2316
|
+
async function readVideos(dataset, labelsPath, openVideos, file, formatId) {
|
|
1999
2317
|
if (!dataset) return [];
|
|
2000
2318
|
const values = dataset.value ?? [];
|
|
2001
2319
|
const videos = [];
|
|
2002
|
-
for (
|
|
2320
|
+
for (let videoIndex = 0; videoIndex < values.length; videoIndex++) {
|
|
2321
|
+
const entry = values[videoIndex];
|
|
2003
2322
|
if (!entry) continue;
|
|
2004
2323
|
const parsed = typeof entry === "string" ? JSON.parse(entry) : JSON.parse(textDecoder.decode(entry));
|
|
2005
2324
|
const backendMeta = parsed.backend ?? {};
|
|
@@ -2010,14 +2329,26 @@ async function readVideos(dataset, labelsPath, openVideos, file) {
|
|
|
2010
2329
|
embedded = true;
|
|
2011
2330
|
filename = labelsPath;
|
|
2012
2331
|
}
|
|
2332
|
+
if (embedded && !datasetPath) {
|
|
2333
|
+
datasetPath = findVideoDataset(file, videoIndex);
|
|
2334
|
+
}
|
|
2335
|
+
const channelOrder = backendMeta.channel_order ?? (formatId < 1.4 ? "BGR" : "RGB");
|
|
2336
|
+
let format = backendMeta.format;
|
|
2337
|
+
if (!format && datasetPath) {
|
|
2338
|
+
const videoDs = file.get(datasetPath);
|
|
2339
|
+
if (videoDs) {
|
|
2340
|
+
const attrs = videoDs.attrs ?? {};
|
|
2341
|
+
format = attrs.format?.value ?? attrs.format;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2013
2344
|
let backend = null;
|
|
2014
2345
|
if (openVideos) {
|
|
2015
2346
|
backend = await createVideoBackend(filename, {
|
|
2016
2347
|
dataset: datasetPath ?? void 0,
|
|
2017
2348
|
embedded,
|
|
2018
2349
|
frameNumbers: readFrameNumbers(file, datasetPath),
|
|
2019
|
-
format
|
|
2020
|
-
channelOrder
|
|
2350
|
+
format,
|
|
2351
|
+
channelOrder,
|
|
2021
2352
|
shape: backendMeta.shape,
|
|
2022
2353
|
fps: backendMeta.fps
|
|
2023
2354
|
});
|
|
@@ -2029,7 +2360,8 @@ async function readVideos(dataset, labelsPath, openVideos, file) {
|
|
|
2029
2360
|
backend,
|
|
2030
2361
|
backendMetadata: backendMeta,
|
|
2031
2362
|
sourceVideo,
|
|
2032
|
-
openBackend: openVideos
|
|
2363
|
+
openBackend: openVideos,
|
|
2364
|
+
embedded
|
|
2033
2365
|
})
|
|
2034
2366
|
);
|
|
2035
2367
|
}
|
|
@@ -2043,6 +2375,28 @@ function readFrameNumbers(file, datasetPath) {
|
|
|
2043
2375
|
const values = frameDataset.value ?? [];
|
|
2044
2376
|
return Array.from(values).map((v) => Number(v));
|
|
2045
2377
|
}
|
|
2378
|
+
function findVideoDataset(file, videoIndex) {
|
|
2379
|
+
const explicitPath = `video${videoIndex}/video`;
|
|
2380
|
+
if (file.get(explicitPath)) {
|
|
2381
|
+
return explicitPath;
|
|
2382
|
+
}
|
|
2383
|
+
const keys = file.keys?.() ?? [];
|
|
2384
|
+
for (const key of keys) {
|
|
2385
|
+
if (key.startsWith("video")) {
|
|
2386
|
+
const candidatePath = `${key}/video`;
|
|
2387
|
+
if (file.get(candidatePath)) {
|
|
2388
|
+
if (videoIndex === 0) {
|
|
2389
|
+
return candidatePath;
|
|
2390
|
+
}
|
|
2391
|
+
const keyIndex = parseInt(key.slice(5), 10);
|
|
2392
|
+
if (!isNaN(keyIndex) && keyIndex === videoIndex) {
|
|
2393
|
+
return candidatePath;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
return null;
|
|
2399
|
+
}
|
|
2046
2400
|
function readSuggestions(dataset, videos) {
|
|
2047
2401
|
if (!dataset) return [];
|
|
2048
2402
|
const values = dataset.value ?? [];
|
|
@@ -2322,17 +2676,18 @@ function slicePoints(data, start, end, predicted = false) {
|
|
|
2322
2676
|
}
|
|
2323
2677
|
|
|
2324
2678
|
// src/codecs/slp/read-streaming.ts
|
|
2325
|
-
async function readSlpStreaming(
|
|
2679
|
+
async function readSlpStreaming(source, options) {
|
|
2326
2680
|
if (!isStreamingSupported()) {
|
|
2327
2681
|
throw new Error("Streaming HDF5 requires Web Worker support (browser environment)");
|
|
2328
2682
|
}
|
|
2329
|
-
const file = await
|
|
2683
|
+
const file = await openH5Worker(source, {
|
|
2330
2684
|
h5wasmUrl: options?.h5wasmUrl,
|
|
2331
2685
|
filenameHint: options?.filenameHint
|
|
2332
2686
|
});
|
|
2333
2687
|
const openVideos = options?.openVideos ?? false;
|
|
2688
|
+
const sourcePath = typeof source === "string" ? source : typeof File !== "undefined" && source instanceof File ? source.name : options?.filenameHint ?? "slp-data.slp";
|
|
2334
2689
|
try {
|
|
2335
|
-
return await readFromStreamingFile(file,
|
|
2690
|
+
return await readFromStreamingFile(file, sourcePath, options?.filenameHint, openVideos);
|
|
2336
2691
|
} finally {
|
|
2337
2692
|
if (!openVideos) {
|
|
2338
2693
|
await file.close();
|
|
@@ -2348,7 +2703,7 @@ async function readFromStreamingFile(file, url, filenameHint, openVideos = false
|
|
|
2348
2703
|
const labelsPath = filenameHint ?? url.split("/").pop()?.split("?")[0] ?? "slp-data.slp";
|
|
2349
2704
|
const skeletons = parseSkeletons(metadataJson);
|
|
2350
2705
|
const tracks = await readTracksStreaming(file);
|
|
2351
|
-
const videos = await readVideosStreaming(file, labelsPath, openVideos);
|
|
2706
|
+
const videos = await readVideosStreaming(file, labelsPath, openVideos, formatId);
|
|
2352
2707
|
const suggestions = await readSuggestionsStreaming(file, videos);
|
|
2353
2708
|
const framesData = await readStructDatasetStreaming(file, "frames");
|
|
2354
2709
|
const instancesData = await readStructDatasetStreaming(file, "instances");
|
|
@@ -2386,45 +2741,123 @@ async function readTracksStreaming(file) {
|
|
|
2386
2741
|
return [];
|
|
2387
2742
|
}
|
|
2388
2743
|
}
|
|
2389
|
-
async function readVideosStreaming(file, labelsPath, openVideos = false) {
|
|
2744
|
+
async function readVideosStreaming(file, labelsPath, openVideos = false, formatId = 1) {
|
|
2390
2745
|
try {
|
|
2391
2746
|
const keys = file.keys();
|
|
2392
2747
|
if (!keys.includes("videos_json")) return [];
|
|
2393
2748
|
const data = await file.getDatasetValue("videos_json");
|
|
2394
2749
|
const values = normalizeDatasetArray(data.value);
|
|
2395
2750
|
const metadataList = parseVideosMetadata(values, labelsPath);
|
|
2396
|
-
|
|
2751
|
+
const videos = [];
|
|
2752
|
+
for (let videoIndex = 0; videoIndex < metadataList.length; videoIndex++) {
|
|
2753
|
+
const meta = metadataList[videoIndex];
|
|
2397
2754
|
const shape = meta.frameCount && meta.height && meta.width && meta.channels ? [meta.frameCount, meta.height, meta.width, meta.channels] : void 0;
|
|
2755
|
+
let datasetPath = meta.dataset;
|
|
2756
|
+
if (meta.embedded && !datasetPath) {
|
|
2757
|
+
datasetPath = await findVideoDatasetStreaming(file, videoIndex) ?? void 0;
|
|
2758
|
+
}
|
|
2759
|
+
const channelOrder = meta.channelOrder ?? (formatId < 1.4 ? "BGR" : "RGB");
|
|
2760
|
+
let format = meta.format;
|
|
2761
|
+
if (!format && datasetPath) {
|
|
2762
|
+
try {
|
|
2763
|
+
const attrs = await file.getAttrs(datasetPath);
|
|
2764
|
+
const formatAttr = attrs.format;
|
|
2765
|
+
if (formatAttr) {
|
|
2766
|
+
format = typeof formatAttr === "string" ? formatAttr : formatAttr?.value;
|
|
2767
|
+
}
|
|
2768
|
+
} catch {
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2398
2771
|
let backend = null;
|
|
2399
|
-
if (openVideos && meta.embedded &&
|
|
2772
|
+
if (openVideos && meta.embedded && datasetPath) {
|
|
2773
|
+
const frameNumbers = await readFrameNumbersStreaming(file, datasetPath);
|
|
2400
2774
|
backend = new StreamingHdf5VideoBackend({
|
|
2401
2775
|
filename: meta.filename,
|
|
2402
2776
|
h5file: file,
|
|
2403
|
-
datasetPath
|
|
2404
|
-
|
|
2405
|
-
|
|
2777
|
+
datasetPath,
|
|
2778
|
+
frameNumbers,
|
|
2779
|
+
format: format ?? "png",
|
|
2780
|
+
channelOrder,
|
|
2406
2781
|
shape,
|
|
2407
2782
|
fps: meta.fps
|
|
2408
2783
|
});
|
|
2409
2784
|
}
|
|
2410
|
-
|
|
2785
|
+
videos.push(new Video({
|
|
2411
2786
|
filename: meta.filename,
|
|
2412
2787
|
backend,
|
|
2413
2788
|
backendMetadata: {
|
|
2414
|
-
dataset:
|
|
2415
|
-
format
|
|
2789
|
+
dataset: datasetPath,
|
|
2790
|
+
format,
|
|
2416
2791
|
shape,
|
|
2417
2792
|
fps: meta.fps,
|
|
2418
|
-
channel_order:
|
|
2793
|
+
channel_order: channelOrder
|
|
2419
2794
|
},
|
|
2420
2795
|
sourceVideo: meta.sourceVideo ? new Video({ filename: meta.sourceVideo.filename }) : null,
|
|
2421
|
-
openBackend: openVideos && meta.embedded
|
|
2422
|
-
|
|
2423
|
-
|
|
2796
|
+
openBackend: openVideos && meta.embedded,
|
|
2797
|
+
embedded: meta.embedded
|
|
2798
|
+
}));
|
|
2799
|
+
}
|
|
2800
|
+
return videos;
|
|
2424
2801
|
} catch {
|
|
2425
2802
|
return [];
|
|
2426
2803
|
}
|
|
2427
2804
|
}
|
|
2805
|
+
async function readFrameNumbersStreaming(file, datasetPath) {
|
|
2806
|
+
try {
|
|
2807
|
+
const groupPath = datasetPath.endsWith("/video") ? datasetPath.slice(0, -6) : datasetPath;
|
|
2808
|
+
const frameNumbersPath = `${groupPath}/frame_numbers`;
|
|
2809
|
+
const groupKeys = await file.getKeys(groupPath);
|
|
2810
|
+
if (!groupKeys.includes("frame_numbers")) {
|
|
2811
|
+
return [];
|
|
2812
|
+
}
|
|
2813
|
+
const data = await file.getDatasetValue(frameNumbersPath);
|
|
2814
|
+
const values = data.value;
|
|
2815
|
+
if (Array.isArray(values)) {
|
|
2816
|
+
return values.map((v) => Number(v));
|
|
2817
|
+
}
|
|
2818
|
+
if (ArrayBuffer.isView(values)) {
|
|
2819
|
+
return Array.from(values).map(Number);
|
|
2820
|
+
}
|
|
2821
|
+
return [];
|
|
2822
|
+
} catch {
|
|
2823
|
+
return [];
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
async function findVideoDatasetStreaming(file, videoIndex) {
|
|
2827
|
+
try {
|
|
2828
|
+
const explicitPath = `video${videoIndex}/video`;
|
|
2829
|
+
const explicitGroupPath = `video${videoIndex}`;
|
|
2830
|
+
try {
|
|
2831
|
+
const groupKeys = await file.getKeys(explicitGroupPath);
|
|
2832
|
+
if (groupKeys.includes("video")) {
|
|
2833
|
+
return explicitPath;
|
|
2834
|
+
}
|
|
2835
|
+
} catch {
|
|
2836
|
+
}
|
|
2837
|
+
const rootKeys = file.keys();
|
|
2838
|
+
for (const key of rootKeys) {
|
|
2839
|
+
if (key.startsWith("video")) {
|
|
2840
|
+
try {
|
|
2841
|
+
const groupKeys = await file.getKeys(key);
|
|
2842
|
+
if (groupKeys.includes("video")) {
|
|
2843
|
+
const candidatePath = `${key}/video`;
|
|
2844
|
+
if (videoIndex === 0) {
|
|
2845
|
+
return candidatePath;
|
|
2846
|
+
}
|
|
2847
|
+
const keyIndex = parseInt(key.slice(5), 10);
|
|
2848
|
+
if (!isNaN(keyIndex) && keyIndex === videoIndex) {
|
|
2849
|
+
return candidatePath;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
} catch {
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
return null;
|
|
2857
|
+
} catch {
|
|
2858
|
+
return null;
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2428
2861
|
async function readSuggestionsStreaming(file, videos) {
|
|
2429
2862
|
try {
|
|
2430
2863
|
const keys = file.keys();
|
|
@@ -2936,29 +3369,44 @@ function createMatrixDataset(file, name, rows, fieldNames, dtype) {
|
|
|
2936
3369
|
}
|
|
2937
3370
|
|
|
2938
3371
|
// src/io/main.ts
|
|
2939
|
-
function
|
|
2940
|
-
return typeof
|
|
3372
|
+
function isNode3() {
|
|
3373
|
+
return typeof process !== "undefined" && !!process.versions?.node;
|
|
2941
3374
|
}
|
|
2942
|
-
function
|
|
2943
|
-
return typeof window !== "undefined" &&
|
|
3375
|
+
function isBrowserWithWorkerSupport() {
|
|
3376
|
+
return typeof window !== "undefined" && isStreamingSupported();
|
|
2944
3377
|
}
|
|
2945
3378
|
async function loadSlp(source, options) {
|
|
2946
3379
|
const streamMode = options?.h5?.stream ?? "auto";
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
3380
|
+
const openVideos = options?.openVideos ?? true;
|
|
3381
|
+
if (isBrowserWithWorkerSupport() && !isNode3() && streamMode !== "download") {
|
|
3382
|
+
let streamingSource;
|
|
3383
|
+
if (typeof source === "string") {
|
|
3384
|
+
streamingSource = source;
|
|
3385
|
+
} else if (source instanceof ArrayBuffer || source instanceof Uint8Array) {
|
|
3386
|
+
streamingSource = source;
|
|
3387
|
+
} else if (typeof File !== "undefined" && source instanceof File) {
|
|
3388
|
+
streamingSource = source;
|
|
3389
|
+
} else if (typeof FileSystemFileHandle !== "undefined" && "getFile" in source) {
|
|
3390
|
+
streamingSource = await source.getFile();
|
|
3391
|
+
} else {
|
|
3392
|
+
streamingSource = null;
|
|
3393
|
+
}
|
|
3394
|
+
if (streamingSource !== null) {
|
|
3395
|
+
try {
|
|
3396
|
+
return await readSlpStreaming(streamingSource, {
|
|
3397
|
+
filenameHint: options?.h5?.filenameHint,
|
|
3398
|
+
openVideos
|
|
3399
|
+
});
|
|
3400
|
+
} catch (e) {
|
|
3401
|
+
if (streamMode === "auto") {
|
|
3402
|
+
console.warn("[sleap-io] Worker-based loading failed, falling back to main thread:", e);
|
|
3403
|
+
} else {
|
|
3404
|
+
throw e;
|
|
3405
|
+
}
|
|
2958
3406
|
}
|
|
2959
3407
|
}
|
|
2960
3408
|
}
|
|
2961
|
-
return readSlp(source, { openVideos
|
|
3409
|
+
return readSlp(source, { openVideos, h5: options?.h5 });
|
|
2962
3410
|
}
|
|
2963
3411
|
async function saveSlp(labels, filename, options) {
|
|
2964
3412
|
await writeSlp(filename, labels, {
|
|
@@ -3850,6 +4298,7 @@ export {
|
|
|
3850
4298
|
loadSlp,
|
|
3851
4299
|
loadVideo,
|
|
3852
4300
|
makeCameraFromDict,
|
|
4301
|
+
openH5Worker,
|
|
3853
4302
|
openStreamingH5,
|
|
3854
4303
|
pointsEmpty,
|
|
3855
4304
|
pointsFromArray,
|