@jax-data-science/sci-image-visualizer 0.2.8 → 0.2.9
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/esm2022/index.mjs +5 -1
- package/esm2022/lib/contracts/instance-segmenter.contract.mjs +12 -0
- package/esm2022/lib/toolbar/segmentation/yolo-model-registry.mjs +99 -0
- package/esm2022/lib/toolbar/segmentation/yolo-segmenter.service.mjs +165 -0
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs +271 -1
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs.map +1 -1
- package/index.d.ts +3 -0
- package/lib/contracts/instance-segmenter.contract.d.ts +100 -0
- package/lib/toolbar/segmentation/yolo-model-registry.d.ts +59 -0
- package/lib/toolbar/segmentation/yolo-segmenter.service.d.ts +50 -0
- package/package.json +3 -2
|
@@ -395,6 +395,276 @@ const CELL_SEGMENTER = new InjectionToken('CELL_SEGMENTER', {
|
|
|
395
395
|
factory: () => inject(CellposeSegmenterService),
|
|
396
396
|
});
|
|
397
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Registry of YOLOv8-seg instance-segmentation models.
|
|
400
|
+
*
|
|
401
|
+
* Mirrors {@link SAM_MODELS}: public HF-hosted ONNX by default so the tool works
|
|
402
|
+
* out of the box in any host, repointable at private hosting via
|
|
403
|
+
* {@link setYoloModelUrls}.
|
|
404
|
+
*
|
|
405
|
+
* Each entry carries the per-model *defaults* the server-side worker uses for
|
|
406
|
+
* that checkpoint. These are properties of how the model was trained (object
|
|
407
|
+
* scale, expected crowding), not UI preferences — the embryo models want heavy
|
|
408
|
+
* 60% tile overlap and loose merge thresholds because their objects are large
|
|
409
|
+
* and touch, while the eye models were trained on sparse, downsampled fields.
|
|
410
|
+
* Keeping them here means a host that just picks a model gets sane behaviour
|
|
411
|
+
* without having to know any of that.
|
|
412
|
+
*
|
|
413
|
+
* Weights point at the `model.fp16w.onnx` variant deliberately. The `.fp16`
|
|
414
|
+
* variant accumulates in half precision on WebGPU and, at the x-scale these
|
|
415
|
+
* models use, produces materially wrong boxes; `fp16w` stores weights in fp16
|
|
416
|
+
* and computes in fp32, so it is half the download at identical accuracy.
|
|
417
|
+
*
|
|
418
|
+
* Export tooling: the sibling `browser-onnx-tools` project.
|
|
419
|
+
*/
|
|
420
|
+
const HF = 'https://huggingface.co/Ballon999';
|
|
421
|
+
const EYE_DEFAULTS = {
|
|
422
|
+
confidence: 0.6,
|
|
423
|
+
iouThreshold: 0.5,
|
|
424
|
+
mergeThreshold: 0.3,
|
|
425
|
+
overlapX: 0,
|
|
426
|
+
overlapY: 0,
|
|
427
|
+
};
|
|
428
|
+
// Embryos are large, touch each other, and are imaged at native scale — hence
|
|
429
|
+
// the heavy overlap and the far looser merge thresholds.
|
|
430
|
+
const EMBRYO_DEFAULTS = {
|
|
431
|
+
confidence: 0.6,
|
|
432
|
+
iouThreshold: 0.8,
|
|
433
|
+
mergeThreshold: 0.8,
|
|
434
|
+
overlapX: 60,
|
|
435
|
+
overlapY: 60,
|
|
436
|
+
};
|
|
437
|
+
const YOLO_MODELS = [
|
|
438
|
+
{
|
|
439
|
+
id: 'yolov8x-seg-opticnerve',
|
|
440
|
+
label: 'Optic nerve',
|
|
441
|
+
modelUrl: `${HF}/yolov8x-seg-opticnerve-onnx/resolve/main/model.fp16w.onnx`,
|
|
442
|
+
defaults: { ...EYE_DEFAULTS },
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
id: 'yolov8x-seg-retina',
|
|
446
|
+
label: 'Retina',
|
|
447
|
+
modelUrl: `${HF}/yolov8x-seg-retina-onnx/resolve/main/model.fp16w.onnx`,
|
|
448
|
+
defaults: { ...EYE_DEFAULTS },
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
id: 'yolov8x-seg-embryo-m2',
|
|
452
|
+
label: 'Embryo (M2)',
|
|
453
|
+
modelUrl: `${HF}/yolov8x-seg-embryo-m2-onnx/resolve/main/model.fp16w.onnx`,
|
|
454
|
+
defaults: { ...EMBRYO_DEFAULTS },
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
id: 'yolov8x-seg-embryo-m3',
|
|
458
|
+
label: 'Embryo (M3)',
|
|
459
|
+
modelUrl: `${HF}/yolov8x-seg-embryo-m3-onnx/resolve/main/model.fp16w.onnx`,
|
|
460
|
+
defaults: { ...EMBRYO_DEFAULTS },
|
|
461
|
+
},
|
|
462
|
+
];
|
|
463
|
+
const DEFAULT_YOLO_MODEL_ID = 'yolov8x-seg-opticnerve';
|
|
464
|
+
let defaultModelId = DEFAULT_YOLO_MODEL_ID;
|
|
465
|
+
/** Look up a model definition by id. Returns undefined for an unknown id. */
|
|
466
|
+
function getYoloModel(id) {
|
|
467
|
+
return YOLO_MODELS.find((m) => m.id === id);
|
|
468
|
+
}
|
|
469
|
+
/** Whether a model has a usable weights URL configured. */
|
|
470
|
+
function isYoloModelReady(id) {
|
|
471
|
+
const m = getYoloModel(id);
|
|
472
|
+
return !!m && m.modelUrl.trim().length > 0;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Repoint a model at different hosting (e.g. a GCS bucket or a same-origin
|
|
476
|
+
* proxy). Call once at app init, before the first run. An empty `modelUrl`
|
|
477
|
+
* disables the model.
|
|
478
|
+
*/
|
|
479
|
+
function setYoloModelUrls(id, urls) {
|
|
480
|
+
const m = getYoloModel(id);
|
|
481
|
+
if (!m)
|
|
482
|
+
return;
|
|
483
|
+
if (urls.modelUrl !== undefined)
|
|
484
|
+
m.modelUrl = urls.modelUrl;
|
|
485
|
+
if (urls.metaUrl !== undefined)
|
|
486
|
+
m.metaUrl = urls.metaUrl;
|
|
487
|
+
}
|
|
488
|
+
/** Change which model runs when a caller does not name one. */
|
|
489
|
+
function setDefaultYoloModel(id) {
|
|
490
|
+
if (getYoloModel(id))
|
|
491
|
+
defaultModelId = id;
|
|
492
|
+
}
|
|
493
|
+
function getDefaultYoloModelId() {
|
|
494
|
+
return defaultModelId;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Default in-library {@link IInstanceSegmenter}, backed by yolo-segdetect-js
|
|
499
|
+
* (YOLOv8-seg on WebGPU, falling back to WASM).
|
|
500
|
+
*
|
|
501
|
+
* `yolo-segdetect-js` is a regular dependency but is **lazy-imported** on first
|
|
502
|
+
* use, so apps that never run YOLO pay nothing for it — or for onnxruntime-web —
|
|
503
|
+
* in their initial bundle. This mirrors {@link CellposeSegmenterService}.
|
|
504
|
+
*
|
|
505
|
+
* The one structural difference from the Cellpose service: there are four YOLO
|
|
506
|
+
* checkpoints, not one, so instances are cached **per model id** rather than as
|
|
507
|
+
* a single singleton. Each entry owns its own worker and ORT session, so a host
|
|
508
|
+
* that switches models pays a second load but keeps the first one warm. Nothing
|
|
509
|
+
* evicts automatically — these are ~144 MB each, so a host that cycles through
|
|
510
|
+
* all four should call {@link dispose} for the ones it is done with.
|
|
511
|
+
*/
|
|
512
|
+
class YoloSegmenterService {
|
|
513
|
+
constructor() {
|
|
514
|
+
this.instances = new Map();
|
|
515
|
+
this.loading = new Map();
|
|
516
|
+
}
|
|
517
|
+
async segmentInstances(image, opts = {}, progress) {
|
|
518
|
+
const modelId = opts.modelId ?? getDefaultYoloModelId();
|
|
519
|
+
const def = this.requireModel(modelId);
|
|
520
|
+
let announcedDownload = false;
|
|
521
|
+
const seg = await this.getModel(modelId, (loaded, total) => {
|
|
522
|
+
if (total)
|
|
523
|
+
progress?.onProgress?.(loaded / total);
|
|
524
|
+
if (loaded > 0 && !announcedDownload) {
|
|
525
|
+
announcedDownload = true;
|
|
526
|
+
progress?.onStatus?.(`Downloading ${def.label} model…`);
|
|
527
|
+
}
|
|
528
|
+
}, (status) => progress?.onStatus?.(status));
|
|
529
|
+
progress?.onStatus?.('Running inference…');
|
|
530
|
+
const result = await seg.segment({ data: image.data, width: image.width, height: image.height }, {
|
|
531
|
+
confThreshold: opts.confidence ?? def.defaults.confidence,
|
|
532
|
+
iouThreshold: opts.iouThreshold ?? def.defaults.iouThreshold,
|
|
533
|
+
threshold: opts.mergeThreshold ?? def.defaults.mergeThreshold,
|
|
534
|
+
overlapX: opts.overlapX ?? def.defaults.overlapX,
|
|
535
|
+
overlapY: opts.overlapY ?? def.defaults.overlapY,
|
|
536
|
+
...(opts.maskThreshold !== undefined ? { maskThreshold: opts.maskThreshold } : {}),
|
|
537
|
+
...(opts.maxDetections !== undefined ? { maxDetections: opts.maxDetections } : {}),
|
|
538
|
+
...(opts.classFilter ? { classFilter: opts.classFilter } : {}),
|
|
539
|
+
tracePolygons: true,
|
|
540
|
+
traceOptions: {
|
|
541
|
+
...(opts.simplifyTolerance !== undefined ? { simplifyTolerance: opts.simplifyTolerance } : {}),
|
|
542
|
+
...(opts.minArea !== undefined ? { minArea: opts.minArea } : {}),
|
|
543
|
+
},
|
|
544
|
+
onTileProgress: (done, total) => progress?.onStatus?.(done < total ? `Running inference (tile ${done}/${total})…` : 'Tracing outlines…'),
|
|
545
|
+
});
|
|
546
|
+
// Detections without geometry are dropped rather than surfaced as empty
|
|
547
|
+
// regions — a detection whose mask vanished under thresholding has nothing
|
|
548
|
+
// meaningful to draw or edit.
|
|
549
|
+
const detections = [];
|
|
550
|
+
for (const d of result.detections) {
|
|
551
|
+
if (!d.polygons || d.polygons.length === 0)
|
|
552
|
+
continue;
|
|
553
|
+
detections.push({
|
|
554
|
+
polygons: d.polygons.map((p) => ({
|
|
555
|
+
exterior: p.exterior.map(([x, y]) => [x, y]),
|
|
556
|
+
holes: p.holes.map((h) => h.map(([x, y]) => [x, y])),
|
|
557
|
+
})),
|
|
558
|
+
box: [d.box[0], d.box[1], d.box[2], d.box[3]],
|
|
559
|
+
score: d.score,
|
|
560
|
+
classId: d.classId,
|
|
561
|
+
className: d.className ?? `class${d.classId}`,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
detections,
|
|
566
|
+
width: result.width,
|
|
567
|
+
height: result.height,
|
|
568
|
+
classNames: result.classNames,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* The shared segmenter for a model id, created on first use and reused
|
|
573
|
+
* thereafter (deduped across concurrent callers). Exposed so a host's
|
|
574
|
+
* processing pipeline can drive richer `segment()` options on the same loaded
|
|
575
|
+
* session instead of loading the model twice.
|
|
576
|
+
*/
|
|
577
|
+
getModel(modelId = getDefaultYoloModelId(), onProgress, onStatus) {
|
|
578
|
+
const existing = this.instances.get(modelId);
|
|
579
|
+
if (existing)
|
|
580
|
+
return Promise.resolve(existing);
|
|
581
|
+
const inFlight = this.loading.get(modelId);
|
|
582
|
+
if (inFlight)
|
|
583
|
+
return inFlight;
|
|
584
|
+
const def = this.requireModel(modelId);
|
|
585
|
+
const load = (async () => {
|
|
586
|
+
// Lazy: keep yolo-segdetect-js + its ORT runtime out of the initial bundle.
|
|
587
|
+
const { YoloSegmenter, configureOrt } = await import('yolo-segdetect-js');
|
|
588
|
+
configureOrt({ wasmPaths: getOrtWasmBase() });
|
|
589
|
+
const seg = await YoloSegmenter.fromPretrained(def.modelUrl, {
|
|
590
|
+
preload: true,
|
|
591
|
+
...(def.metaUrl ? { metaUrl: def.metaUrl } : {}),
|
|
592
|
+
onProgress: ({ loaded, total }) => onProgress?.(loaded, total),
|
|
593
|
+
onStatus: (s) => onStatus?.(s),
|
|
594
|
+
});
|
|
595
|
+
this.instances.set(modelId, seg);
|
|
596
|
+
return seg;
|
|
597
|
+
})();
|
|
598
|
+
this.loading.set(modelId, load);
|
|
599
|
+
// A failed load must not poison the cache — the next caller should retry
|
|
600
|
+
// rather than inherit the rejected promise forever.
|
|
601
|
+
load.catch(() => this.loading.delete(modelId));
|
|
602
|
+
return load;
|
|
603
|
+
}
|
|
604
|
+
/** Whether a model is already loaded (warm). */
|
|
605
|
+
isLoaded(modelId = getDefaultYoloModelId()) {
|
|
606
|
+
return this.instances.has(modelId);
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Release one model, or all of them. Safe to call on a model never loaded.
|
|
610
|
+
*
|
|
611
|
+
* In-flight loads are torn down too, not just warm ones. Simply dropping the
|
|
612
|
+
* pending promise and returning would let the load finish afterwards,
|
|
613
|
+
* re-populate {@link instances}, and strand a live worker and ORT session that
|
|
614
|
+
* nothing owns — disposal would have reported success while leaking. So a load
|
|
615
|
+
* that is still running is awaited and then released.
|
|
616
|
+
*/
|
|
617
|
+
async dispose(modelId) {
|
|
618
|
+
const ids = modelId ? [modelId] : [...new Set([...this.instances.keys(), ...this.loading.keys()])];
|
|
619
|
+
await Promise.all(ids.map((id) => this.disposeOne(id)));
|
|
620
|
+
}
|
|
621
|
+
async disposeOne(id) {
|
|
622
|
+
const pending = this.loading.get(id);
|
|
623
|
+
const loaded = this.instances.get(id);
|
|
624
|
+
this.instances.delete(id);
|
|
625
|
+
this.loading.delete(id);
|
|
626
|
+
if (loaded)
|
|
627
|
+
await loaded.dispose();
|
|
628
|
+
if (!pending)
|
|
629
|
+
return;
|
|
630
|
+
// A pending load assigns instances[id] before its promise resolves, so by
|
|
631
|
+
// the time this await returns the entry is back. Clear it only if it still
|
|
632
|
+
// points at what *this* load produced — a newer load started in the
|
|
633
|
+
// meantime owns its own entry and must not be evicted.
|
|
634
|
+
const seg = await pending.catch(() => null);
|
|
635
|
+
if (!seg || seg === loaded)
|
|
636
|
+
return;
|
|
637
|
+
if (this.instances.get(id) === seg)
|
|
638
|
+
this.instances.delete(id);
|
|
639
|
+
await seg.dispose();
|
|
640
|
+
}
|
|
641
|
+
requireModel(modelId) {
|
|
642
|
+
const def = getYoloModel(modelId);
|
|
643
|
+
if (!def)
|
|
644
|
+
throw new Error(`Unknown YOLO model id "${modelId}".`);
|
|
645
|
+
if (!def.modelUrl.trim()) {
|
|
646
|
+
throw new Error(`YOLO model "${modelId}" is not configured — its modelUrl is empty.`);
|
|
647
|
+
}
|
|
648
|
+
return def;
|
|
649
|
+
}
|
|
650
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: YoloSegmenterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
651
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: YoloSegmenterService, providedIn: 'root' }); }
|
|
652
|
+
}
|
|
653
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: YoloSegmenterService, decorators: [{
|
|
654
|
+
type: Injectable,
|
|
655
|
+
args: [{ providedIn: 'root' }]
|
|
656
|
+
}] });
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* DI token for the automatic instance segmenter. Defaults to the in-library
|
|
660
|
+
* {@link YoloSegmenterService} (yolo-segdetect-js, lazy-loaded); override it in
|
|
661
|
+
* the host to swap implementations — for example to delegate to the server.
|
|
662
|
+
*/
|
|
663
|
+
const INSTANCE_SEGMENTER = new InjectionToken('INSTANCE_SEGMENTER', {
|
|
664
|
+
providedIn: 'root',
|
|
665
|
+
factory: () => inject(YoloSegmenterService),
|
|
666
|
+
});
|
|
667
|
+
|
|
398
668
|
/**
|
|
399
669
|
* Unified image container used throughout the processing pipeline.
|
|
400
670
|
* Decouples pipeline logic from any specific library's image format.
|
|
@@ -21784,5 +22054,5 @@ function provideVisualization() {
|
|
|
21784
22054
|
* Generated bundle index. Do not edit.
|
|
21785
22055
|
*/
|
|
21786
22056
|
|
|
21787
|
-
export { ALL_TOOLBAR_TOOLS, CELL_SEGMENTER, CHANNEL_HISTOGRAM_API, CellposeSegmenterService, ChannelHistogramComponent, DEFAULT_FALLBACK_PALETTE, DEFAULT_SAM_MODEL_ID, HexColorPickerComponent, IMAGE_STATE_PORT, ImageConverterService, LUT_COLORS, MultiPolygon, NAPARI_DECIMATE_OPTIONS, NAPARI_DEFAULT_DECIMATE, PLOT_TYPE_DESCRIPTORS, PREFERENCES_PORT, PlotType, Polygon, ProcessingImage, REGION_EDITOR_API, REGION_IO_PORT, Rectangle, Region, RegionEditorComponent, RoutingVisualizerService, SAM_MODELS, ShapeSelection, TILE_ACCESS_PORT, VISUALIZER, VIZ_CONFIG, ViewerFeature, VisualizationModule, VisualizerComponent, capabilitiesOf, cropImage, defaultPresetSet, getDefaultSamModelId, getOrtWasmBase, getPlotTypeDescriptor, getSamModel, isEmptyPresetSet, isNapari3d, isNapariIsosurface, isNapariScatter, isNapariScatter3d, isNapariSurface, isNapariVolume, isSamModelReady, isThreeDimensional, provideVisualization, setDefaultSamModel, setOrtWasmBase, setSamModelUrls };
|
|
22057
|
+
export { ALL_TOOLBAR_TOOLS, CELL_SEGMENTER, CHANNEL_HISTOGRAM_API, CellposeSegmenterService, ChannelHistogramComponent, DEFAULT_FALLBACK_PALETTE, DEFAULT_SAM_MODEL_ID, DEFAULT_YOLO_MODEL_ID, HexColorPickerComponent, IMAGE_STATE_PORT, INSTANCE_SEGMENTER, ImageConverterService, LUT_COLORS, MultiPolygon, NAPARI_DECIMATE_OPTIONS, NAPARI_DEFAULT_DECIMATE, PLOT_TYPE_DESCRIPTORS, PREFERENCES_PORT, PlotType, Polygon, ProcessingImage, REGION_EDITOR_API, REGION_IO_PORT, Rectangle, Region, RegionEditorComponent, RoutingVisualizerService, SAM_MODELS, ShapeSelection, TILE_ACCESS_PORT, VISUALIZER, VIZ_CONFIG, ViewerFeature, VisualizationModule, VisualizerComponent, YOLO_MODELS, YoloSegmenterService, capabilitiesOf, cropImage, defaultPresetSet, getDefaultSamModelId, getDefaultYoloModelId, getOrtWasmBase, getPlotTypeDescriptor, getSamModel, getYoloModel, isEmptyPresetSet, isNapari3d, isNapariIsosurface, isNapariScatter, isNapariScatter3d, isNapariSurface, isNapariVolume, isSamModelReady, isThreeDimensional, isYoloModelReady, provideVisualization, setDefaultSamModel, setDefaultYoloModel, setOrtWasmBase, setSamModelUrls, setYoloModelUrls };
|
|
21788
22058
|
//# sourceMappingURL=jax-data-science-sci-image-visualizer.mjs.map
|