@jax-data-science/sci-image-visualizer 0.2.7 → 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.
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Registry of YOLOv8-seg instance-segmentation models.
3
+ *
4
+ * Mirrors {@link SAM_MODELS}: public HF-hosted ONNX by default so the tool works
5
+ * out of the box in any host, repointable at private hosting via
6
+ * {@link setYoloModelUrls}.
7
+ *
8
+ * Each entry carries the per-model *defaults* the server-side worker uses for
9
+ * that checkpoint. These are properties of how the model was trained (object
10
+ * scale, expected crowding), not UI preferences — the embryo models want heavy
11
+ * 60% tile overlap and loose merge thresholds because their objects are large
12
+ * and touch, while the eye models were trained on sparse, downsampled fields.
13
+ * Keeping them here means a host that just picks a model gets sane behaviour
14
+ * without having to know any of that.
15
+ *
16
+ * Weights point at the `model.fp16w.onnx` variant deliberately. The `.fp16`
17
+ * variant accumulates in half precision on WebGPU and, at the x-scale these
18
+ * models use, produces materially wrong boxes; `fp16w` stores weights in fp16
19
+ * and computes in fp32, so it is half the download at identical accuracy.
20
+ *
21
+ * Export tooling: the sibling `browser-onnx-tools` project.
22
+ */
23
+ const HF = 'https://huggingface.co/Ballon999';
24
+ const EYE_DEFAULTS = {
25
+ confidence: 0.6,
26
+ iouThreshold: 0.5,
27
+ mergeThreshold: 0.3,
28
+ overlapX: 0,
29
+ overlapY: 0,
30
+ };
31
+ // Embryos are large, touch each other, and are imaged at native scale — hence
32
+ // the heavy overlap and the far looser merge thresholds.
33
+ const EMBRYO_DEFAULTS = {
34
+ confidence: 0.6,
35
+ iouThreshold: 0.8,
36
+ mergeThreshold: 0.8,
37
+ overlapX: 60,
38
+ overlapY: 60,
39
+ };
40
+ export const YOLO_MODELS = [
41
+ {
42
+ id: 'yolov8x-seg-opticnerve',
43
+ label: 'Optic nerve',
44
+ modelUrl: `${HF}/yolov8x-seg-opticnerve-onnx/resolve/main/model.fp16w.onnx`,
45
+ defaults: { ...EYE_DEFAULTS },
46
+ },
47
+ {
48
+ id: 'yolov8x-seg-retina',
49
+ label: 'Retina',
50
+ modelUrl: `${HF}/yolov8x-seg-retina-onnx/resolve/main/model.fp16w.onnx`,
51
+ defaults: { ...EYE_DEFAULTS },
52
+ },
53
+ {
54
+ id: 'yolov8x-seg-embryo-m2',
55
+ label: 'Embryo (M2)',
56
+ modelUrl: `${HF}/yolov8x-seg-embryo-m2-onnx/resolve/main/model.fp16w.onnx`,
57
+ defaults: { ...EMBRYO_DEFAULTS },
58
+ },
59
+ {
60
+ id: 'yolov8x-seg-embryo-m3',
61
+ label: 'Embryo (M3)',
62
+ modelUrl: `${HF}/yolov8x-seg-embryo-m3-onnx/resolve/main/model.fp16w.onnx`,
63
+ defaults: { ...EMBRYO_DEFAULTS },
64
+ },
65
+ ];
66
+ export const DEFAULT_YOLO_MODEL_ID = 'yolov8x-seg-opticnerve';
67
+ let defaultModelId = DEFAULT_YOLO_MODEL_ID;
68
+ /** Look up a model definition by id. Returns undefined for an unknown id. */
69
+ export function getYoloModel(id) {
70
+ return YOLO_MODELS.find((m) => m.id === id);
71
+ }
72
+ /** Whether a model has a usable weights URL configured. */
73
+ export function isYoloModelReady(id) {
74
+ const m = getYoloModel(id);
75
+ return !!m && m.modelUrl.trim().length > 0;
76
+ }
77
+ /**
78
+ * Repoint a model at different hosting (e.g. a GCS bucket or a same-origin
79
+ * proxy). Call once at app init, before the first run. An empty `modelUrl`
80
+ * disables the model.
81
+ */
82
+ export function setYoloModelUrls(id, urls) {
83
+ const m = getYoloModel(id);
84
+ if (!m)
85
+ return;
86
+ if (urls.modelUrl !== undefined)
87
+ m.modelUrl = urls.modelUrl;
88
+ if (urls.metaUrl !== undefined)
89
+ m.metaUrl = urls.metaUrl;
90
+ }
91
+ /** Change which model runs when a caller does not name one. */
92
+ export function setDefaultYoloModel(id) {
93
+ if (getYoloModel(id))
94
+ defaultModelId = id;
95
+ }
96
+ export function getDefaultYoloModelId() {
97
+ return defaultModelId;
98
+ }
99
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoieW9sby1tb2RlbC1yZWdpc3RyeS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy9saWIvdG9vbGJhci9zZWdtZW50YXRpb24veW9sby1tb2RlbC1yZWdpc3RyeS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBcUJHO0FBc0JILE1BQU0sRUFBRSxHQUFHLGtDQUFrQyxDQUFDO0FBRTlDLE1BQU0sWUFBWSxHQUFHO0lBQ25CLFVBQVUsRUFBRSxHQUFHO0lBQ2YsWUFBWSxFQUFFLEdBQUc7SUFDakIsY0FBYyxFQUFFLEdBQUc7SUFDbkIsUUFBUSxFQUFFLENBQUM7SUFDWCxRQUFRLEVBQUUsQ0FBQztDQUNaLENBQUM7QUFFRiw4RUFBOEU7QUFDOUUseURBQXlEO0FBQ3pELE1BQU0sZUFBZSxHQUFHO0lBQ3RCLFVBQVUsRUFBRSxHQUFHO0lBQ2YsWUFBWSxFQUFFLEdBQUc7SUFDakIsY0FBYyxFQUFFLEdBQUc7SUFDbkIsUUFBUSxFQUFFLEVBQUU7SUFDWixRQUFRLEVBQUUsRUFBRTtDQUNiLENBQUM7QUFFRixNQUFNLENBQUMsTUFBTSxXQUFXLEdBQW1CO0lBQ3pDO1FBQ0UsRUFBRSxFQUFFLHdCQUF3QjtRQUM1QixLQUFLLEVBQUUsYUFBYTtRQUNwQixRQUFRLEVBQUUsR0FBRyxFQUFFLDREQUE0RDtRQUMzRSxRQUFRLEVBQUUsRUFBRSxHQUFHLFlBQVksRUFBRTtLQUM5QjtJQUNEO1FBQ0UsRUFBRSxFQUFFLG9CQUFvQjtRQUN4QixLQUFLLEVBQUUsUUFBUTtRQUNmLFFBQVEsRUFBRSxHQUFHLEVBQUUsd0RBQXdEO1FBQ3ZFLFFBQVEsRUFBRSxFQUFFLEdBQUcsWUFBWSxFQUFFO0tBQzlCO0lBQ0Q7UUFDRSxFQUFFLEVBQUUsdUJBQXVCO1FBQzNCLEtBQUssRUFBRSxhQUFhO1FBQ3BCLFFBQVEsRUFBRSxHQUFHLEVBQUUsMkRBQTJEO1FBQzFFLFFBQVEsRUFBRSxFQUFFLEdBQUcsZUFBZSxFQUFFO0tBQ2pDO0lBQ0Q7UUFDRSxFQUFFLEVBQUUsdUJBQXVCO1FBQzNCLEtBQUssRUFBRSxhQUFhO1FBQ3BCLFFBQVEsRUFBRSxHQUFHLEVBQUUsMkRBQTJEO1FBQzFFLFFBQVEsRUFBRSxFQUFFLEdBQUcsZUFBZSxFQUFFO0tBQ2pDO0NBQ0YsQ0FBQztBQUVGLE1BQU0sQ0FBQyxNQUFNLHFCQUFxQixHQUFHLHdCQUF3QixDQUFDO0FBRTlELElBQUksY0FBYyxHQUFHLHFCQUFxQixDQUFDO0FBRTNDLDZFQUE2RTtBQUM3RSxNQUFNLFVBQVUsWUFBWSxDQUFDLEVBQVU7SUFDckMsT0FBTyxXQUFXLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDO0FBQzlDLENBQUM7QUFFRCwyREFBMkQ7QUFDM0QsTUFBTSxVQUFVLGdCQUFnQixDQUFDLEVBQVU7SUFDekMsTUFBTSxDQUFDLEdBQUcsWUFBWSxDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBQzNCLE9BQU8sQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsUUFBUSxDQUFDLElBQUksRUFBRSxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUM7QUFDN0MsQ0FBQztBQUVEOzs7O0dBSUc7QUFDSCxNQUFNLFVBQVUsZ0JBQWdCLENBQUMsRUFBVSxFQUFFLElBQTZDO0lBQ3hGLE1BQU0sQ0FBQyxHQUFHLFlBQVksQ0FBQyxFQUFFLENBQUMsQ0FBQztJQUMzQixJQUFJLENBQUMsQ0FBQztRQUFFLE9BQU87SUFDZixJQUFJLElBQUksQ0FBQyxRQUFRLEtBQUssU0FBUztRQUFFLENBQUMsQ0FBQyxRQUFRLEdBQUcsSUFBSSxDQUFDLFFBQVEsQ0FBQztJQUM1RCxJQUFJLElBQUksQ0FBQyxPQUFPLEtBQUssU0FBUztRQUFFLENBQUMsQ0FBQyxPQUFPLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQztBQUMzRCxDQUFDO0FBRUQsK0RBQStEO0FBQy9ELE1BQU0sVUFBVSxtQkFBbUIsQ0FBQyxFQUFVO0lBQzVDLElBQUksWUFBWSxDQUFDLEVBQUUsQ0FBQztRQUFFLGNBQWMsR0FBRyxFQUFFLENBQUM7QUFDNUMsQ0FBQztBQUVELE1BQU0sVUFBVSxxQkFBcUI7SUFDbkMsT0FBTyxjQUFjLENBQUM7QUFDeEIsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogUmVnaXN0cnkgb2YgWU9MT3Y4LXNlZyBpbnN0YW5jZS1zZWdtZW50YXRpb24gbW9kZWxzLlxuICpcbiAqIE1pcnJvcnMge0BsaW5rIFNBTV9NT0RFTFN9OiBwdWJsaWMgSEYtaG9zdGVkIE9OTlggYnkgZGVmYXVsdCBzbyB0aGUgdG9vbCB3b3Jrc1xuICogb3V0IG9mIHRoZSBib3ggaW4gYW55IGhvc3QsIHJlcG9pbnRhYmxlIGF0IHByaXZhdGUgaG9zdGluZyB2aWFcbiAqIHtAbGluayBzZXRZb2xvTW9kZWxVcmxzfS5cbiAqXG4gKiBFYWNoIGVudHJ5IGNhcnJpZXMgdGhlIHBlci1tb2RlbCAqZGVmYXVsdHMqIHRoZSBzZXJ2ZXItc2lkZSB3b3JrZXIgdXNlcyBmb3JcbiAqIHRoYXQgY2hlY2twb2ludC4gVGhlc2UgYXJlIHByb3BlcnRpZXMgb2YgaG93IHRoZSBtb2RlbCB3YXMgdHJhaW5lZCAob2JqZWN0XG4gKiBzY2FsZSwgZXhwZWN0ZWQgY3Jvd2RpbmcpLCBub3QgVUkgcHJlZmVyZW5jZXMg4oCUIHRoZSBlbWJyeW8gbW9kZWxzIHdhbnQgaGVhdnlcbiAqIDYwJSB0aWxlIG92ZXJsYXAgYW5kIGxvb3NlIG1lcmdlIHRocmVzaG9sZHMgYmVjYXVzZSB0aGVpciBvYmplY3RzIGFyZSBsYXJnZVxuICogYW5kIHRvdWNoLCB3aGlsZSB0aGUgZXllIG1vZGVscyB3ZXJlIHRyYWluZWQgb24gc3BhcnNlLCBkb3duc2FtcGxlZCBmaWVsZHMuXG4gKiBLZWVwaW5nIHRoZW0gaGVyZSBtZWFucyBhIGhvc3QgdGhhdCBqdXN0IHBpY2tzIGEgbW9kZWwgZ2V0cyBzYW5lIGJlaGF2aW91clxuICogd2l0aG91dCBoYXZpbmcgdG8ga25vdyBhbnkgb2YgdGhhdC5cbiAqXG4gKiBXZWlnaHRzIHBvaW50IGF0IHRoZSBgbW9kZWwuZnAxNncub25ueGAgdmFyaWFudCBkZWxpYmVyYXRlbHkuIFRoZSBgLmZwMTZgXG4gKiB2YXJpYW50IGFjY3VtdWxhdGVzIGluIGhhbGYgcHJlY2lzaW9uIG9uIFdlYkdQVSBhbmQsIGF0IHRoZSB4LXNjYWxlIHRoZXNlXG4gKiBtb2RlbHMgdXNlLCBwcm9kdWNlcyBtYXRlcmlhbGx5IHdyb25nIGJveGVzOyBgZnAxNndgIHN0b3JlcyB3ZWlnaHRzIGluIGZwMTZcbiAqIGFuZCBjb21wdXRlcyBpbiBmcDMyLCBzbyBpdCBpcyBoYWxmIHRoZSBkb3dubG9hZCBhdCBpZGVudGljYWwgYWNjdXJhY3kuXG4gKlxuICogRXhwb3J0IHRvb2xpbmc6IHRoZSBzaWJsaW5nIGBicm93c2VyLW9ubngtdG9vbHNgIHByb2plY3QuXG4gKi9cblxuZXhwb3J0IGludGVyZmFjZSBZb2xvTW9kZWxEZWYge1xuICBpZDogc3RyaW5nO1xuICBsYWJlbDogc3RyaW5nO1xuICAvKiogVVJMIG9mIHRoZSBgLm9ubnhgIHdlaWdodHMuIEVtcHR5IGRpc2FibGVzIHRoZSBtb2RlbC4gKi9cbiAgbW9kZWxVcmw6IHN0cmluZztcbiAgLyoqXG4gICAqIFVSTCBvZiB0aGUgYG1vZGVsLmpzb25gIHNpZGVjYXIuIE9taXQgdG8gdXNlIGBtb2RlbC5qc29uYCBhbG9uZ3NpZGUgdGhlXG4gICAqIHdlaWdodHMsIHdoaWNoIGlzIGhvdyBldmVyeSBwdWJsaXNoZWQgcmVwbyBpcyBsYWlkIG91dC5cbiAgICovXG4gIG1ldGFVcmw/OiBzdHJpbmc7XG4gIC8qKiBQZXItbW9kZWwgZGVmYXVsdHMsIGZyb20gdGhlIHNlcnZlciB3b3JrZXIncyBtb2RlbCB0YWJsZS4gKi9cbiAgZGVmYXVsdHM6IHtcbiAgICBjb25maWRlbmNlOiBudW1iZXI7XG4gICAgaW91VGhyZXNob2xkOiBudW1iZXI7XG4gICAgbWVyZ2VUaHJlc2hvbGQ6IG51bWJlcjtcbiAgICBvdmVybGFwWDogbnVtYmVyO1xuICAgIG92ZXJsYXBZOiBudW1iZXI7XG4gIH07XG59XG5cbmNvbnN0IEhGID0gJ2h0dHBzOi8vaHVnZ2luZ2ZhY2UuY28vQmFsbG9uOTk5JztcblxuY29uc3QgRVlFX0RFRkFVTFRTID0ge1xuICBjb25maWRlbmNlOiAwLjYsXG4gIGlvdVRocmVzaG9sZDogMC41LFxuICBtZXJnZVRocmVzaG9sZDogMC4zLFxuICBvdmVybGFwWDogMCxcbiAgb3ZlcmxhcFk6IDAsXG59O1xuXG4vLyBFbWJyeW9zIGFyZSBsYXJnZSwgdG91Y2ggZWFjaCBvdGhlciwgYW5kIGFyZSBpbWFnZWQgYXQgbmF0aXZlIHNjYWxlIOKAlCBoZW5jZVxuLy8gdGhlIGhlYXZ5IG92ZXJsYXAgYW5kIHRoZSBmYXIgbG9vc2VyIG1lcmdlIHRocmVzaG9sZHMuXG5jb25zdCBFTUJSWU9fREVGQVVMVFMgPSB7XG4gIGNvbmZpZGVuY2U6IDAuNixcbiAgaW91VGhyZXNob2xkOiAwLjgsXG4gIG1lcmdlVGhyZXNob2xkOiAwLjgsXG4gIG92ZXJsYXBYOiA2MCxcbiAgb3ZlcmxhcFk6IDYwLFxufTtcblxuZXhwb3J0IGNvbnN0IFlPTE9fTU9ERUxTOiBZb2xvTW9kZWxEZWZbXSA9IFtcbiAge1xuICAgIGlkOiAneW9sb3Y4eC1zZWctb3B0aWNuZXJ2ZScsXG4gICAgbGFiZWw6ICdPcHRpYyBuZXJ2ZScsXG4gICAgbW9kZWxVcmw6IGAke0hGfS95b2xvdjh4LXNlZy1vcHRpY25lcnZlLW9ubngvcmVzb2x2ZS9tYWluL21vZGVsLmZwMTZ3Lm9ubnhgLFxuICAgIGRlZmF1bHRzOiB7IC4uLkVZRV9ERUZBVUxUUyB9LFxuICB9LFxuICB7XG4gICAgaWQ6ICd5b2xvdjh4LXNlZy1yZXRpbmEnLFxuICAgIGxhYmVsOiAnUmV0aW5hJyxcbiAgICBtb2RlbFVybDogYCR7SEZ9L3lvbG92OHgtc2VnLXJldGluYS1vbm54L3Jlc29sdmUvbWFpbi9tb2RlbC5mcDE2dy5vbm54YCxcbiAgICBkZWZhdWx0czogeyAuLi5FWUVfREVGQVVMVFMgfSxcbiAgfSxcbiAge1xuICAgIGlkOiAneW9sb3Y4eC1zZWctZW1icnlvLW0yJyxcbiAgICBsYWJlbDogJ0VtYnJ5byAoTTIpJyxcbiAgICBtb2RlbFVybDogYCR7SEZ9L3lvbG92OHgtc2VnLWVtYnJ5by1tMi1vbm54L3Jlc29sdmUvbWFpbi9tb2RlbC5mcDE2dy5vbm54YCxcbiAgICBkZWZhdWx0czogeyAuLi5FTUJSWU9fREVGQVVMVFMgfSxcbiAgfSxcbiAge1xuICAgIGlkOiAneW9sb3Y4eC1zZWctZW1icnlvLW0zJyxcbiAgICBsYWJlbDogJ0VtYnJ5byAoTTMpJyxcbiAgICBtb2RlbFVybDogYCR7SEZ9L3lvbG92OHgtc2VnLWVtYnJ5by1tMy1vbm54L3Jlc29sdmUvbWFpbi9tb2RlbC5mcDE2dy5vbm54YCxcbiAgICBkZWZhdWx0czogeyAuLi5FTUJSWU9fREVGQVVMVFMgfSxcbiAgfSxcbl07XG5cbmV4cG9ydCBjb25zdCBERUZBVUxUX1lPTE9fTU9ERUxfSUQgPSAneW9sb3Y4eC1zZWctb3B0aWNuZXJ2ZSc7XG5cbmxldCBkZWZhdWx0TW9kZWxJZCA9IERFRkFVTFRfWU9MT19NT0RFTF9JRDtcblxuLyoqIExvb2sgdXAgYSBtb2RlbCBkZWZpbml0aW9uIGJ5IGlkLiBSZXR1cm5zIHVuZGVmaW5lZCBmb3IgYW4gdW5rbm93biBpZC4gKi9cbmV4cG9ydCBmdW5jdGlvbiBnZXRZb2xvTW9kZWwoaWQ6IHN0cmluZyk6IFlvbG9Nb2RlbERlZiB8IHVuZGVmaW5lZCB7XG4gIHJldHVybiBZT0xPX01PREVMUy5maW5kKChtKSA9PiBtLmlkID09PSBpZCk7XG59XG5cbi8qKiBXaGV0aGVyIGEgbW9kZWwgaGFzIGEgdXNhYmxlIHdlaWdodHMgVVJMIGNvbmZpZ3VyZWQuICovXG5leHBvcnQgZnVuY3Rpb24gaXNZb2xvTW9kZWxSZWFkeShpZDogc3RyaW5nKTogYm9vbGVhbiB7XG4gIGNvbnN0IG0gPSBnZXRZb2xvTW9kZWwoaWQpO1xuICByZXR1cm4gISFtICYmIG0ubW9kZWxVcmwudHJpbSgpLmxlbmd0aCA+IDA7XG59XG5cbi8qKlxuICogUmVwb2ludCBhIG1vZGVsIGF0IGRpZmZlcmVudCBob3N0aW5nIChlLmcuIGEgR0NTIGJ1Y2tldCBvciBhIHNhbWUtb3JpZ2luXG4gKiBwcm94eSkuIENhbGwgb25jZSBhdCBhcHAgaW5pdCwgYmVmb3JlIHRoZSBmaXJzdCBydW4uIEFuIGVtcHR5IGBtb2RlbFVybGBcbiAqIGRpc2FibGVzIHRoZSBtb2RlbC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHNldFlvbG9Nb2RlbFVybHMoaWQ6IHN0cmluZywgdXJsczogeyBtb2RlbFVybD86IHN0cmluZzsgbWV0YVVybD86IHN0cmluZyB9KTogdm9pZCB7XG4gIGNvbnN0IG0gPSBnZXRZb2xvTW9kZWwoaWQpO1xuICBpZiAoIW0pIHJldHVybjtcbiAgaWYgKHVybHMubW9kZWxVcmwgIT09IHVuZGVmaW5lZCkgbS5tb2RlbFVybCA9IHVybHMubW9kZWxVcmw7XG4gIGlmICh1cmxzLm1ldGFVcmwgIT09IHVuZGVmaW5lZCkgbS5tZXRhVXJsID0gdXJscy5tZXRhVXJsO1xufVxuXG4vKiogQ2hhbmdlIHdoaWNoIG1vZGVsIHJ1bnMgd2hlbiBhIGNhbGxlciBkb2VzIG5vdCBuYW1lIG9uZS4gKi9cbmV4cG9ydCBmdW5jdGlvbiBzZXREZWZhdWx0WW9sb01vZGVsKGlkOiBzdHJpbmcpOiB2b2lkIHtcbiAgaWYgKGdldFlvbG9Nb2RlbChpZCkpIGRlZmF1bHRNb2RlbElkID0gaWQ7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBnZXREZWZhdWx0WW9sb01vZGVsSWQoKTogc3RyaW5nIHtcbiAgcmV0dXJuIGRlZmF1bHRNb2RlbElkO1xufVxuIl19
@@ -0,0 +1,165 @@
1
+ import { Injectable } from '@angular/core';
2
+ import { getOrtWasmBase } from './ort-runtime-config';
3
+ import { getDefaultYoloModelId, getYoloModel } from './yolo-model-registry';
4
+ import * as i0 from "@angular/core";
5
+ /**
6
+ * Default in-library {@link IInstanceSegmenter}, backed by yolo-segdetect-js
7
+ * (YOLOv8-seg on WebGPU, falling back to WASM).
8
+ *
9
+ * `yolo-segdetect-js` is a regular dependency but is **lazy-imported** on first
10
+ * use, so apps that never run YOLO pay nothing for it — or for onnxruntime-web —
11
+ * in their initial bundle. This mirrors {@link CellposeSegmenterService}.
12
+ *
13
+ * The one structural difference from the Cellpose service: there are four YOLO
14
+ * checkpoints, not one, so instances are cached **per model id** rather than as
15
+ * a single singleton. Each entry owns its own worker and ORT session, so a host
16
+ * that switches models pays a second load but keeps the first one warm. Nothing
17
+ * evicts automatically — these are ~144 MB each, so a host that cycles through
18
+ * all four should call {@link dispose} for the ones it is done with.
19
+ */
20
+ export class YoloSegmenterService {
21
+ constructor() {
22
+ this.instances = new Map();
23
+ this.loading = new Map();
24
+ }
25
+ async segmentInstances(image, opts = {}, progress) {
26
+ const modelId = opts.modelId ?? getDefaultYoloModelId();
27
+ const def = this.requireModel(modelId);
28
+ let announcedDownload = false;
29
+ const seg = await this.getModel(modelId, (loaded, total) => {
30
+ if (total)
31
+ progress?.onProgress?.(loaded / total);
32
+ if (loaded > 0 && !announcedDownload) {
33
+ announcedDownload = true;
34
+ progress?.onStatus?.(`Downloading ${def.label} model…`);
35
+ }
36
+ }, (status) => progress?.onStatus?.(status));
37
+ progress?.onStatus?.('Running inference…');
38
+ const result = await seg.segment({ data: image.data, width: image.width, height: image.height }, {
39
+ confThreshold: opts.confidence ?? def.defaults.confidence,
40
+ iouThreshold: opts.iouThreshold ?? def.defaults.iouThreshold,
41
+ threshold: opts.mergeThreshold ?? def.defaults.mergeThreshold,
42
+ overlapX: opts.overlapX ?? def.defaults.overlapX,
43
+ overlapY: opts.overlapY ?? def.defaults.overlapY,
44
+ ...(opts.maskThreshold !== undefined ? { maskThreshold: opts.maskThreshold } : {}),
45
+ ...(opts.maxDetections !== undefined ? { maxDetections: opts.maxDetections } : {}),
46
+ ...(opts.classFilter ? { classFilter: opts.classFilter } : {}),
47
+ tracePolygons: true,
48
+ traceOptions: {
49
+ ...(opts.simplifyTolerance !== undefined ? { simplifyTolerance: opts.simplifyTolerance } : {}),
50
+ ...(opts.minArea !== undefined ? { minArea: opts.minArea } : {}),
51
+ },
52
+ onTileProgress: (done, total) => progress?.onStatus?.(done < total ? `Running inference (tile ${done}/${total})…` : 'Tracing outlines…'),
53
+ });
54
+ // Detections without geometry are dropped rather than surfaced as empty
55
+ // regions — a detection whose mask vanished under thresholding has nothing
56
+ // meaningful to draw or edit.
57
+ const detections = [];
58
+ for (const d of result.detections) {
59
+ if (!d.polygons || d.polygons.length === 0)
60
+ continue;
61
+ detections.push({
62
+ polygons: d.polygons.map((p) => ({
63
+ exterior: p.exterior.map(([x, y]) => [x, y]),
64
+ holes: p.holes.map((h) => h.map(([x, y]) => [x, y])),
65
+ })),
66
+ box: [d.box[0], d.box[1], d.box[2], d.box[3]],
67
+ score: d.score,
68
+ classId: d.classId,
69
+ className: d.className ?? `class${d.classId}`,
70
+ });
71
+ }
72
+ return {
73
+ detections,
74
+ width: result.width,
75
+ height: result.height,
76
+ classNames: result.classNames,
77
+ };
78
+ }
79
+ /**
80
+ * The shared segmenter for a model id, created on first use and reused
81
+ * thereafter (deduped across concurrent callers). Exposed so a host's
82
+ * processing pipeline can drive richer `segment()` options on the same loaded
83
+ * session instead of loading the model twice.
84
+ */
85
+ getModel(modelId = getDefaultYoloModelId(), onProgress, onStatus) {
86
+ const existing = this.instances.get(modelId);
87
+ if (existing)
88
+ return Promise.resolve(existing);
89
+ const inFlight = this.loading.get(modelId);
90
+ if (inFlight)
91
+ return inFlight;
92
+ const def = this.requireModel(modelId);
93
+ const load = (async () => {
94
+ // Lazy: keep yolo-segdetect-js + its ORT runtime out of the initial bundle.
95
+ const { YoloSegmenter, configureOrt } = await import('yolo-segdetect-js');
96
+ configureOrt({ wasmPaths: getOrtWasmBase() });
97
+ const seg = await YoloSegmenter.fromPretrained(def.modelUrl, {
98
+ preload: true,
99
+ ...(def.metaUrl ? { metaUrl: def.metaUrl } : {}),
100
+ onProgress: ({ loaded, total }) => onProgress?.(loaded, total),
101
+ onStatus: (s) => onStatus?.(s),
102
+ });
103
+ this.instances.set(modelId, seg);
104
+ return seg;
105
+ })();
106
+ this.loading.set(modelId, load);
107
+ // A failed load must not poison the cache — the next caller should retry
108
+ // rather than inherit the rejected promise forever.
109
+ load.catch(() => this.loading.delete(modelId));
110
+ return load;
111
+ }
112
+ /** Whether a model is already loaded (warm). */
113
+ isLoaded(modelId = getDefaultYoloModelId()) {
114
+ return this.instances.has(modelId);
115
+ }
116
+ /**
117
+ * Release one model, or all of them. Safe to call on a model never loaded.
118
+ *
119
+ * In-flight loads are torn down too, not just warm ones. Simply dropping the
120
+ * pending promise and returning would let the load finish afterwards,
121
+ * re-populate {@link instances}, and strand a live worker and ORT session that
122
+ * nothing owns — disposal would have reported success while leaking. So a load
123
+ * that is still running is awaited and then released.
124
+ */
125
+ async dispose(modelId) {
126
+ const ids = modelId ? [modelId] : [...new Set([...this.instances.keys(), ...this.loading.keys()])];
127
+ await Promise.all(ids.map((id) => this.disposeOne(id)));
128
+ }
129
+ async disposeOne(id) {
130
+ const pending = this.loading.get(id);
131
+ const loaded = this.instances.get(id);
132
+ this.instances.delete(id);
133
+ this.loading.delete(id);
134
+ if (loaded)
135
+ await loaded.dispose();
136
+ if (!pending)
137
+ return;
138
+ // A pending load assigns instances[id] before its promise resolves, so by
139
+ // the time this await returns the entry is back. Clear it only if it still
140
+ // points at what *this* load produced — a newer load started in the
141
+ // meantime owns its own entry and must not be evicted.
142
+ const seg = await pending.catch(() => null);
143
+ if (!seg || seg === loaded)
144
+ return;
145
+ if (this.instances.get(id) === seg)
146
+ this.instances.delete(id);
147
+ await seg.dispose();
148
+ }
149
+ requireModel(modelId) {
150
+ const def = getYoloModel(modelId);
151
+ if (!def)
152
+ throw new Error(`Unknown YOLO model id "${modelId}".`);
153
+ if (!def.modelUrl.trim()) {
154
+ throw new Error(`YOLO model "${modelId}" is not configured — its modelUrl is empty.`);
155
+ }
156
+ return def;
157
+ }
158
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: YoloSegmenterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
159
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: YoloSegmenterService, providedIn: 'root' }); }
160
+ }
161
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: YoloSegmenterService, decorators: [{
162
+ type: Injectable,
163
+ args: [{ providedIn: 'root' }]
164
+ }] });
165
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoieW9sby1zZWdtZW50ZXIuc2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy9saWIvdG9vbGJhci9zZWdtZW50YXRpb24veW9sby1zZWdtZW50ZXIuc2VydmljZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sZUFBZSxDQUFDO0FBQzNDLE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUN0RCxPQUFPLEVBQUUscUJBQXFCLEVBQUUsWUFBWSxFQUFxQixNQUFNLHVCQUF1QixDQUFDOztBQVcvRjs7Ozs7Ozs7Ozs7Ozs7R0FjRztBQUVILE1BQU0sT0FBTyxvQkFBb0I7SUFEakM7UUFFbUIsY0FBUyxHQUFHLElBQUksR0FBRyxFQUF5QixDQUFDO1FBQzdDLFlBQU8sR0FBRyxJQUFJLEdBQUcsRUFBa0MsQ0FBQztLQTRKdEU7SUExSkMsS0FBSyxDQUFDLGdCQUFnQixDQUNwQixLQUFpRSxFQUNqRSxPQUErQixFQUFFLEVBQ2pDLFFBQWtDO1FBRWxDLE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxPQUFPLElBQUkscUJBQXFCLEVBQUUsQ0FBQztRQUN4RCxNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUMsWUFBWSxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBRXZDLElBQUksaUJBQWlCLEdBQUcsS0FBSyxDQUFDO1FBQzlCLE1BQU0sR0FBRyxHQUFHLE1BQU0sSUFBSSxDQUFDLFFBQVEsQ0FDN0IsT0FBTyxFQUNQLENBQUMsTUFBTSxFQUFFLEtBQUssRUFBRSxFQUFFO1lBQ2hCLElBQUksS0FBSztnQkFBRSxRQUFRLEVBQUUsVUFBVSxFQUFFLENBQUMsTUFBTSxHQUFHLEtBQUssQ0FBQyxDQUFDO1lBQ2xELElBQUksTUFBTSxHQUFHLENBQUMsSUFBSSxDQUFDLGlCQUFpQixFQUFFLENBQUM7Z0JBQ3JDLGlCQUFpQixHQUFHLElBQUksQ0FBQztnQkFD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RXhCLElBQUksTUFBTTtZQUFFLE1BQU0sTUFBTSxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBQ25DLElBQUksQ0FBQyxPQUFPO1lBQUUsT0FBTztRQUVyQiwwRUFBMEU7UUFDMUUsMkVBQTJFO1FBQzNFLG9FQUFvRTtRQUNwRSx1REFBdUQ7UUFDdkQsTUFBTSxHQUFHLEdBQUcsTUFBTSxPQUFPLENBQUMsS0FBSyxDQUFDLEdBQUcsRUFBRSxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQzVDLElBQUksQ0FBQyxHQUFHLElBQUksR0FBRyxLQUFLLE1BQU07WUFBRSxPQUFPO1FBQ25DLElBQUksSUFBSSxDQUFDLFNBQVMsQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLEtBQUssR0FBRztZQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxDQUFDO1FBQzlELE1BQU0sR0FBRyxDQUFDLE9BQU8sRUFBRSxDQUFDO0lBQ3RCLENBQUM7SUFFTyxZQUFZLENBQUMsT0FBZTtRQUNsQyxNQUFNLEdBQUcsR0FBRyxZQUFZLENBQUMsT0FBTyxDQUFDLENBQUM7UUFDbEMsSUFBSSxDQUFDLEdBQUc7WUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLDBCQUEwQixPQUFPLElBQUksQ0FBQyxDQUFDO1FBQ2pFLElBQUksQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLElBQUksRUFBRSxFQUFFLENBQUM7WUFDekIsTUFBTSxJQUFJLEtBQUssQ0FBQyxlQUFlLE9BQU8sOENBQThDLENBQUMsQ0FBQztRQUN4RixDQUFDO1FBQ0QsT0FBTyxHQUFHLENBQUM7SUFDYixDQUFDOzhHQTdKVSxvQkFBb0I7a0hBQXBCLG9CQUFvQixjQURQLE1BQU07OzJGQUNuQixvQkFBb0I7a0JBRGhDLFVBQVU7bUJBQUMsRUFBRSxVQUFVLEVBQUUsTUFBTSxFQUFFIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgSW5qZWN0YWJsZSB9IGZyb20gJ0Bhbmd1bGFyL2NvcmUnO1xuaW1wb3J0IHsgZ2V0T3J0V2FzbUJhc2UgfSBmcm9tICcuL29ydC1ydW50aW1lLWNvbmZpZyc7XG5pbXBvcnQgeyBnZXREZWZhdWx0WW9sb01vZGVsSWQsIGdldFlvbG9Nb2RlbCwgdHlwZSBZb2xvTW9kZWxEZWYgfSBmcm9tICcuL3lvbG8tbW9kZWwtcmVnaXN0cnknO1xuXG5pbXBvcnQgdHlwZSB7XG4gIElJbnN0YW5jZVNlZ21lbnRlcixcbiAgSW5zdGFuY2VEZXRlY3Rpb24sXG4gIEluc3RhbmNlU2VnbWVudGF0aW9uLFxuICBJbnN0YW5jZVNlZ21lbnRPcHRpb25zLFxuICBJbnN0YW5jZVNlZ21lbnRQcm9ncmVzcyxcbn0gZnJvbSAnLi4vLi4vY29udHJhY3RzL2luc3RhbmNlLXNlZ21lbnRlci5jb250cmFjdCc7XG5pbXBvcnQgdHlwZSB7IFlvbG9TZWdtZW50ZXIgfSBmcm9tICd5b2xvLXNlZ2RldGVjdC1qcyc7XG5cbi8qKlxuICogRGVmYXVsdCBpbi1saWJyYXJ5IHtAbGluayBJSW5zdGFuY2VTZWdtZW50ZXJ9LCBiYWNrZWQgYnkgeW9sby1zZWdkZXRlY3QtanNcbiAqIChZT0xPdjgtc2VnIG9uIFdlYkdQVSwgZmFsbGluZyBiYWNrIHRvIFdBU00pLlxuICpcbiAqIGB5b2xvLXNlZ2RldGVjdC1qc2AgaXMgYSByZWd1bGFyIGRlcGVuZGVuY3kgYnV0IGlzICoqbGF6eS1pbXBvcnRlZCoqIG9uIGZpcnN0XG4gKiB1c2UsIHNvIGFwcHMgdGhhdCBuZXZlciBydW4gWU9MTyBwYXkgbm90aGluZyBmb3IgaXQg4oCUIG9yIGZvciBvbm54cnVudGltZS13ZWIg4oCUXG4gKiBpbiB0aGVpciBpbml0aWFsIGJ1bmRsZS4gVGhpcyBtaXJyb3JzIHtAbGluayBDZWxscG9zZVNlZ21lbnRlclNlcnZpY2V9LlxuICpcbiAqIFRoZSBvbmUgc3RydWN0dXJhbCBkaWZmZXJlbmNlIGZyb20gdGhlIENlbGxwb3NlIHNlcnZpY2U6IHRoZXJlIGFyZSBmb3VyIFlPTE9cbiAqIGNoZWNrcG9pbnRzLCBub3Qgb25lLCBzbyBpbnN0YW5jZXMgYXJlIGNhY2hlZCAqKnBlciBtb2RlbCBpZCoqIHJhdGhlciB0aGFuIGFzXG4gKiBhIHNpbmdsZSBzaW5nbGV0b24uIEVhY2ggZW50cnkgb3ducyBpdHMgb3duIHdvcmtlciBhbmQgT1JUIHNlc3Npb24sIHNvIGEgaG9zdFxuICogdGhhdCBzd2l0Y2hlcyBtb2RlbHMgcGF5cyBhIHNlY29uZCBsb2FkIGJ1dCBrZWVwcyB0aGUgZmlyc3Qgb25lIHdhcm0uIE5vdGhpbmdcbiAqIGV2aWN0cyBhdXRvbWF0aWNhbGx5IOKAlCB0aGVzZSBhcmUgfjE0NCBNQiBlYWNoLCBzbyBhIGhvc3QgdGhhdCBjeWNsZXMgdGhyb3VnaFxuICogYWxsIGZvdXIgc2hvdWxkIGNhbGwge0BsaW5rIGRpc3Bvc2V9IGZvciB0aGUgb25lcyBpdCBpcyBkb25lIHdpdGguXG4gKi9cbkBJbmplY3RhYmxlKHsgcHJvdmlkZWRJbjogJ3Jvb3QnIH0pXG5leHBvcnQgY2xhc3MgWW9sb1NlZ21lbnRlclNlcnZpY2UgaW1wbGVtZW50cyBJSW5zdGFuY2VTZWdtZW50ZXIge1xuICBwcml2YXRlIHJlYWRvbmx5IGluc3RhbmNlcyA9IG5ldyBNYXA8c3RyaW5nLCBZb2xvU2VnbWVudGVyPigpO1xuICBwcml2YXRlIHJlYWRvbmx5IGxvYWRpbmcgPSBuZXcgTWFwPHN0cmluZywgUHJvbWlzZTxZb2xvU2VnbWVudGVyPj4oKTtcblxuICBhc3luYyBzZWdtZW50SW5zdGFuY2VzKFxuICAgIGltYWdlOiB7IGRhdGE6IFVpbnQ4Q2xhbXBlZEFycmF5OyB3aWR0aDogbnVtYmVyOyBoZWlnaHQ6IG51bWJlciB9LFxuICAgIG9wdHM6IEluc3RhbmNlU2VnbWVudE9wdGlvbnMgPSB7fSxcbiAgICBwcm9ncmVzcz86IEluc3RhbmNlU2VnbWVudFByb2dyZXNzLFxuICApOiBQcm9taXNlPEluc3RhbmNlU2VnbWVudGF0aW9uPiB7XG4gICAgY29uc3QgbW9kZWxJZCA9IG9wdHMubW9kZWxJZCA/PyBnZXREZWZhdWx0WW9sb01vZGVsSWQoKTtcbiAgICBjb25zdCBkZWYgPSB0aGlzLnJlcXVpcmVNb2RlbChtb2RlbElkKTtcblxuICAgIGxldCBhbm5vdW5jZWREb3dubG9hZCA9IGZhbHNlO1xuICAgIGNvbnN0IHNlZyA9IGF3YWl0IHRoaXMuZ2V0TW9kZWwoXG4gICAgICBtb2RlbElkLFxuICAgICAgKGxvYWRlZCwgdG90YWwpID0+IHtcbiAgICAgICAgaWYgKHRvdGFsKSBwcm9ncmVzcz8ub25Qcm9ncmVzcz8uKGxvYWRlZCAvIHRvdGFsKTtcbiAgICAgICAgaWYgKGxvYWRlZCA+IDAgJiYgIWFubm91bmNlZERvd25sb2FkKSB7XG4gICAgICAgICAgYW5ub3VuY2VkRG93bmxvYWQgPSB0cnVlO1xuICAgICAgICAgIHByb2dyZXNzPy5vblN0YXR1cz8uKGBEb3dubG9hZGluZyAke2RlZi5sYWJlbH0gbW9kZWzigKZgKTtcbiAgICAgICAgfVxuICAgICAgfSxcbiAgICAgIChzdGF0dXMpID0+IHByb2dyZXNzPy5vblN0YXR1cz8uKHN0YXR1cyksXG4gICAgKTtcblxuICAgIHByb2dyZXNzPy5vblN0YXR1cz8uKCdSdW5uaW5nIGluZmVyZW5jZeKApicpO1xuICAgIGNvbnN0IHJlc3VsdCA9IGF3YWl0IHNlZy5zZWdtZW50KFxuICAgICAgeyBkYXRhOiBpbWFnZS5kYXRhLCB3aWR0aDogaW1hZ2Uud2lkdGgsIGhlaWdodDogaW1hZ2UuaGVpZ2h0IH0sXG4gICAgICB7XG4gICAgICAgIGNvbmZUaHJlc2hvbGQ6IG9wdHMuY29uZmlkZW5jZSA/PyBkZWYuZGVmYXVsdHMuY29uZmlkZW5jZSxcbiAgICAgICAgaW91VGhyZXNob2xkOiBvcHRzLmlvdVRocmVzaG9sZCA/PyBkZWYuZGVmYXVsdHMuaW91VGhyZXNob2xkLFxuICAgICAgICB0aHJlc2hvbGQ6IG9wdHMubWVyZ2VUaHJlc2hvbGQgPz8gZGVmLmRlZmF1bHRzLm1lcmdlVGhyZXNob2xkLFxuICAgICAgICBvdmVybGFwWDogb3B0cy5vdmVybGFwWCA/PyBkZWYuZGVmYXVsdHMub3ZlcmxhcFgsXG4gICAgICAgIG92ZXJsYXBZOiBvcHRzLm92ZXJsYXBZID8/IGRlZi5kZWZhdWx0cy5vdmVybGFwWSxcbiAgICAgICAgLi4uKG9wdHMubWFza1RocmVzaG9sZCAhPT0gdW5kZWZpbmVkID8geyBtYXNrVGhyZXNob2xkOiBvcHRzLm1hc2tUaHJlc2hvbGQgfSA6IHt9KSxcbiAgICAgICAgLi4uKG9wdHMubWF4RGV0ZWN0aW9ucyAhPT0gdW5kZWZpbmVkID8geyBtYXhEZXRlY3Rpb25zOiBvcHRzLm1heERldGVjdGlvbnMgfSA6IHt9KSxcbiAgICAgICAgLi4uKG9wdHMuY2xhc3NGaWx0ZXIgPyB7IGNsYXNzRmlsdGVyOiBvcHRzLmNsYXNzRmlsdGVyIH0gOiB7fSksXG4gICAgICAgIHRyYWNlUG9seWdvbnM6IHRydWUsXG4gICAgICAgIHRyYWNlT3B0aW9uczoge1xuICAgICAgICAgIC4uLihvcHRzLnNpbXBsaWZ5VG9sZXJhbmNlICE9PSB1bmRlZmluZWQgPyB7IHNpbXBsaWZ5VG9sZXJhbmNlOiBvcHRzLnNpbXBsaWZ5VG9sZXJhbmNlIH0gOiB7fSksXG4gICAgICAgICAgLi4uKG9wdHMubWluQXJlYSAhPT0gdW5kZWZpbmVkID8geyBtaW5BcmVhOiBvcHRzLm1pbkFyZWEgfSA6IHt9KSxcbiAgICAgICAgfSxcbiAgICAgICAgb25UaWxlUHJvZ3Jlc3M6IChkb25lLCB0b3RhbCkgPT5cbiAgICAgICAgICBwcm9ncmVzcz8ub25TdGF0dXM/Lihkb25lIDwgdG90YWwgPyBgUnVubmluZyBpbmZlcmVuY2UgKHRpbGUgJHtkb25lfS8ke3RvdGFsfSnigKZgIDogJ1RyYWNpbmcgb3V0bGluZXPigKYnKSxcbiAgICAgIH0sXG4gICAgKTtcblxuICAgIC8vIERldGVjdGlvbnMgd2l0aG91dCBnZW9tZXRyeSBhcmUgZHJvcHBlZCByYXRoZXIgdGhhbiBzdXJmYWNlZCBhcyBlbXB0eVxuICAgIC8vIHJlZ2lvbnMg4oCUIGEgZGV0ZWN0aW9uIHdob3NlIG1hc2sgdmFuaXNoZWQgdW5kZXIgdGhyZXNob2xkaW5nIGhhcyBub3RoaW5nXG4gICAgLy8gbWVhbmluZ2Z1bCB0byBkcmF3IG9yIGVkaXQuXG4gICAgY29uc3QgZGV0ZWN0aW9uczogSW5zdGFuY2VEZXRlY3Rpb25bXSA9IFtdO1xuICAgIGZvciAoY29uc3QgZCBvZiByZXN1bHQuZGV0ZWN0aW9ucykge1xuICAgICAgaWYgKCFkLnBvbHlnb25zIHx8IGQucG9seWdvbnMubGVuZ3RoID09PSAwKSBjb250aW51ZTtcbiAgICAgIGRldGVjdGlvbnMucHVzaCh7XG4gICAgICAgIHBvbHlnb25zOiBkLnBvbHlnb25zLm1hcCgocCkgPT4gKHtcbiAgICAgICAgICBleHRlcmlvcjogcC5leHRlcmlvci5tYXAoKFt4LCB5XSk6IFtudW1iZXIsIG51bWJlcl0gPT4gW3gsIHldKSxcbiAgICAgICAgICBob2xlczogcC5ob2xlcy5tYXAoKGgpID0+IGgubWFwKChbeCwgeV0pOiBbbnVtYmVyLCBudW1iZXJdID0+IFt4LCB5XSkpLFxuICAgICAgICB9KSksXG4gICAgICAgIGJveDogW2QuYm94WzBdLCBkLmJveFsxXSwgZC5ib3hbMl0sIGQuYm94WzNdXSxcbiAgICAgICAgc2NvcmU6IGQuc2NvcmUsXG4gICAgICAgIGNsYXNzSWQ6IGQuY2xhc3NJZCxcbiAgICAgICAgY2xhc3NOYW1lOiBkLmNsYXNzTmFtZSA/PyBgY2xhc3Mke2QuY2xhc3NJZH1gLFxuICAgICAgfSk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHtcbiAgICAgIGRldGVjdGlvbnMsXG4gICAgICB3aWR0aDogcmVzdWx0LndpZHRoLFxuICAgICAgaGVpZ2h0OiByZXN1bHQuaGVpZ2h0LFxuICAgICAgY2xhc3NOYW1lczogcmVzdWx0LmNsYXNzTmFtZXMsXG4gICAgfTtcbiAgfVxuXG4gIC8qKlxuICAgKiBUaGUgc2hhcmVkIHNlZ21lbnRlciBmb3IgYSBtb2RlbCBpZCwgY3JlYXRlZCBvbiBmaXJzdCB1c2UgYW5kIHJldXNlZFxuICAgKiB0aGVyZWFmdGVyIChkZWR1cGVkIGFjcm9zcyBjb25jdXJyZW50IGNhbGxlcnMpLiBFeHBvc2VkIHNvIGEgaG9zdCdzXG4gICAqIHByb2Nlc3NpbmcgcGlwZWxpbmUgY2FuIGRyaXZlIHJpY2hlciBgc2VnbWVudCgpYCBvcHRpb25zIG9uIHRoZSBzYW1lIGxvYWRlZFxuICAgKiBzZXNzaW9uIGluc3RlYWQgb2YgbG9hZGluZyB0aGUgbW9kZWwgdHdpY2UuXG4gICAqL1xuICBnZXRNb2RlbChcbiAgICBtb2RlbElkOiBzdHJpbmcgPSBnZXREZWZhdWx0WW9sb01vZGVsSWQoKSxcbiAgICBvblByb2dyZXNzPzogKGxvYWRlZDogbnVtYmVyLCB0b3RhbDogbnVtYmVyIHwgbnVsbCkgPT4gdm9pZCxcbiAgICBvblN0YXR1cz86IChzdGF0dXM6IHN0cmluZykgPT4gdm9pZCxcbiAgKTogUHJvbWlzZTxZb2xvU2VnbWVudGVyPiB7XG4gICAgY29uc3QgZXhpc3RpbmcgPSB0aGlzLmluc3RhbmNlcy5nZXQobW9kZWxJZCk7XG4gICAgaWYgKGV4aXN0aW5nKSByZXR1cm4gUHJvbWlzZS5yZXNvbHZlKGV4aXN0aW5nKTtcblxuICAgIGNvbnN0IGluRmxpZ2h0ID0gdGhpcy5sb2FkaW5nLmdldChtb2RlbElkKTtcbiAgICBpZiAoaW5GbGlnaHQpIHJldHVybiBpbkZsaWdodDtcblxuICAgIGNvbnN0IGRlZiA9IHRoaXMucmVxdWlyZU1vZGVsKG1vZGVsSWQpO1xuICAgIGNvbnN0IGxvYWQgPSAoYXN5bmMgKCkgPT4ge1xuICAgICAgLy8gTGF6eToga2VlcCB5b2xvLXNlZ2RldGVjdC1qcyArIGl0cyBPUlQgcnVudGltZSBvdXQgb2YgdGhlIGluaXRpYWwgYnVuZGxlLlxuICAgICAgY29uc3QgeyBZb2xvU2VnbWVudGVyLCBjb25maWd1cmVPcnQgfSA9IGF3YWl0IGltcG9ydCgneW9sby1zZWdkZXRlY3QtanMnKTtcbiAgICAgIGNvbmZpZ3VyZU9ydCh7IHdhc21QYXRoczogZ2V0T3J0V2FzbUJhc2UoKSB9KTtcbiAgICAgIGNvbnN0IHNlZyA9IGF3YWl0IFlvbG9TZWdtZW50ZXIuZnJvbVByZXRyYWluZWQoZGVmLm1vZGVsVXJsLCB7XG4gICAgICAgIHByZWxvYWQ6IHRydWUsXG4gICAgICAgIC4uLihkZWYubWV0YVVybCA/IHsgbWV0YVVybDogZGVmLm1ldGFVcmwgfSA6IHt9KSxcbiAgICAgICAgb25Qcm9ncmVzczogKHsgbG9hZGVkLCB0b3RhbCB9KSA9PiBvblByb2dyZXNzPy4obG9hZGVkLCB0b3RhbCksXG4gICAgICAgIG9uU3RhdHVzOiAocykgPT4gb25TdGF0dXM/LihzKSxcbiAgICAgIH0pO1xuICAgICAgdGhpcy5pbnN0YW5jZXMuc2V0KG1vZGVsSWQsIHNlZyk7XG4gICAgICByZXR1cm4gc2VnO1xuICAgIH0pKCk7XG5cbiAgICB0aGlzLmxvYWRpbmcuc2V0KG1vZGVsSWQsIGxvYWQpO1xuICAgIC8vIEEgZmFpbGVkIGxvYWQgbXVzdCBub3QgcG9pc29uIHRoZSBjYWNoZSDigJQgdGhlIG5leHQgY2FsbGVyIHNob3VsZCByZXRyeVxuICAgIC8vIHJhdGhlciB0aGFuIGluaGVyaXQgdGhlIHJlamVjdGVkIHByb21pc2UgZm9yZXZlci5cbiAgICBsb2FkLmNhdGNoKCgpID0+IHRoaXMubG9hZGluZy5kZWxldGUobW9kZWxJZCkpO1xuICAgIHJldHVybiBsb2FkO1xuICB9XG5cbiAgLyoqIFdoZXRoZXIgYSBtb2RlbCBpcyBhbHJlYWR5IGxvYWRlZCAod2FybSkuICovXG4gIGlzTG9hZGVkKG1vZGVsSWQ6IHN0cmluZyA9IGdldERlZmF1bHRZb2xvTW9kZWxJZCgpKTogYm9vbGVhbiB7XG4gICAgcmV0dXJuIHRoaXMuaW5zdGFuY2VzLmhhcyhtb2RlbElkKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBSZWxlYXNlIG9uZSBtb2RlbCwgb3IgYWxsIG9mIHRoZW0uIFNhZmUgdG8gY2FsbCBvbiBhIG1vZGVsIG5ldmVyIGxvYWRlZC5cbiAgICpcbiAgICogSW4tZmxpZ2h0IGxvYWRzIGFyZSB0b3JuIGRvd24gdG9vLCBub3QganVzdCB3YXJtIG9uZXMuIFNpbXBseSBkcm9wcGluZyB0aGVcbiAgICogcGVuZGluZyBwcm9taXNlIGFuZCByZXR1cm5pbmcgd291bGQgbGV0IHRoZSBsb2FkIGZpbmlzaCBhZnRlcndhcmRzLFxuICAgKiByZS1wb3B1bGF0ZSB7QGxpbmsgaW5zdGFuY2VzfSwgYW5kIHN0cmFuZCBhIGxpdmUgd29ya2VyIGFuZCBPUlQgc2Vzc2lvbiB0aGF0XG4gICAqIG5vdGhpbmcgb3ducyDigJQgZGlzcG9zYWwgd291bGQgaGF2ZSByZXBvcnRlZCBzdWNjZXNzIHdoaWxlIGxlYWtpbmcuIFNvIGEgbG9hZFxuICAgKiB0aGF0IGlzIHN0aWxsIHJ1bm5pbmcgaXMgYXdhaXRlZCBhbmQgdGhlbiByZWxlYXNlZC5cbiAgICovXG4gIGFzeW5jIGRpc3Bvc2UobW9kZWxJZD86IHN0cmluZyk6IFByb21pc2U8dm9pZD4ge1xuICAgIGNvbnN0IGlkcyA9IG1vZGVsSWQgPyBbbW9kZWxJZF0gOiBbLi4ubmV3IFNldChbLi4udGhpcy5pbnN0YW5jZXMua2V5cygpLCAuLi50aGlzLmxvYWRpbmcua2V5cygpXSldO1xuICAgIGF3YWl0IFByb21pc2UuYWxsKGlkcy5tYXAoKGlkKSA9PiB0aGlzLmRpc3Bvc2VPbmUoaWQpKSk7XG4gIH1cblxuICBwcml2YXRlIGFzeW5jIGRpc3Bvc2VPbmUoaWQ6IHN0cmluZyk6IFByb21pc2U8dm9pZD4ge1xuICAgIGNvbnN0IHBlbmRpbmcgPSB0aGlzLmxvYWRpbmcuZ2V0KGlkKTtcbiAgICBjb25zdCBsb2FkZWQgPSB0aGlzLmluc3RhbmNlcy5nZXQoaWQpO1xuICAgIHRoaXMuaW5zdGFuY2VzLmRlbGV0ZShpZCk7XG4gICAgdGhpcy5sb2FkaW5nLmRlbGV0ZShpZCk7XG5cbiAgICBpZiAobG9hZGVkKSBhd2FpdCBsb2FkZWQuZGlzcG9zZSgpO1xuICAgIGlmICghcGVuZGluZykgcmV0dXJuO1xuXG4gICAgLy8gQSBwZW5kaW5nIGxvYWQgYXNzaWducyBpbnN0YW5jZXNbaWRdIGJlZm9yZSBpdHMgcHJvbWlzZSByZXNvbHZlcywgc28gYnlcbiAgICAvLyB0aGUgdGltZSB0aGlzIGF3YWl0IHJldHVybnMgdGhlIGVudHJ5IGlzIGJhY2suIENsZWFyIGl0IG9ubHkgaWYgaXQgc3RpbGxcbiAgICAvLyBwb2ludHMgYXQgd2hhdCAqdGhpcyogbG9hZCBwcm9kdWNlZCDigJQgYSBuZXdlciBsb2FkIHN0YXJ0ZWQgaW4gdGhlXG4gICAgLy8gbWVhbnRpbWUgb3ducyBpdHMgb3duIGVudHJ5IGFuZCBtdXN0IG5vdCBiZSBldmljdGVkLlxuICAgIGNvbnN0IHNlZyA9IGF3YWl0IHBlbmRpbmcuY2F0Y2goKCkgPT4gbnVsbCk7XG4gICAgaWYgKCFzZWcgfHwgc2VnID09PSBsb2FkZWQpIHJldHVybjtcbiAgICBpZiAodGhpcy5pbnN0YW5jZXMuZ2V0KGlkKSA9PT0gc2VnKSB0aGlzLmluc3RhbmNlcy5kZWxldGUoaWQpO1xuICAgIGF3YWl0IHNlZy5kaXNwb3NlKCk7XG4gIH1cblxuICBwcml2YXRlIHJlcXVpcmVNb2RlbChtb2RlbElkOiBzdHJpbmcpOiBZb2xvTW9kZWxEZWYge1xuICAgIGNvbnN0IGRlZiA9IGdldFlvbG9Nb2RlbChtb2RlbElkKTtcbiAgICBpZiAoIWRlZikgdGhyb3cgbmV3IEVycm9yKGBVbmtub3duIFlPTE8gbW9kZWwgaWQgXCIke21vZGVsSWR9XCIuYCk7XG4gICAgaWYgKCFkZWYubW9kZWxVcmwudHJpbSgpKSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoYFlPTE8gbW9kZWwgXCIke21vZGVsSWR9XCIgaXMgbm90IGNvbmZpZ3VyZWQg4oCUIGl0cyBtb2RlbFVybCBpcyBlbXB0eS5gKTtcbiAgICB9XG4gICAgcmV0dXJuIGRlZjtcbiAgfVxufVxuIl19
@@ -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.
@@ -16392,14 +16662,29 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
16392
16662
  // crisp nearest-neighbour blocks).
16393
16663
  minZoomImageRatio: 0.01,
16394
16664
  maxZoomPixelRatio: 20,
16395
- // Switch to a finer pyramid level as soon as the current one would be
16396
- // upscaled at all. OSD's default (0.5) tolerates displaying a level at up
16397
- // to 2x magnification before fetching the next, which reads as a soft
16398
- // image that abruptly sharpens when the threshold is crossed. At 1 the
16399
- // displayed level always has at least one tile pixel per screen pixel, so
16400
- // the only visible pixellation is past 1:1 - where the blocks are genuine
16401
- // source pixels (paired with imageSmoothingEnabled=false).
16402
- minPixelRatio: 1,
16665
+ // OSD's stock value, restored. DO NOT RAISE IT: the name reads like
16666
+ // "minimum sharpness" but it is the opposite. From OSD's own
16667
+ // TiledImage._getLevelsInterval:
16668
+ //
16669
+ // highestLevel = floor( log2( currentZeroRatio / minPixelRatio ) )
16670
+ //
16671
+ // minPixelRatio is a floor on how small a tile pixel may shrink, and it
16672
+ // DIVIDES into the ratio - so raising it lowers the chosen level, i.e.
16673
+ // picks a COARSER image. OSD's own comment on the default says as much:
16674
+ // "closer to 0 draws tiles meant for a higher zoom at this zoom".
16675
+ //
16676
+ // Measured on a flat 22304x24528 image (levels 697/1394/2788/5576/11152/
16677
+ // 22304), predicted level matching actual in all nine trials:
16678
+ //
16679
+ // zoom 2 zoom 8 ~1:1
16680
+ // 1.0 1.34x up 1.34x up 1.86x up
16681
+ // 0.5 0.67x (none) 0.67x (none) 0.93x -> full res
16682
+ // 0.25 0.33x 0.33x 0.93x
16683
+ //
16684
+ // 0.5 is what gives "never upscaled below native, full resolution at 1:1".
16685
+ // 0.25 is not better - it jumps two rungs finer and fetches ~4x the tiles
16686
+ // for no visible gain, and tiles are not cached by the ingress.
16687
+ minPixelRatio: 0.5,
16403
16688
  // Wait for the view to settle before pulling new tiles (less churn).
16404
16689
  immediateRender: false,
16405
16690
  animationTime: 0.4,
@@ -21769,5 +22054,5 @@ function provideVisualization() {
21769
22054
  * Generated bundle index. Do not edit.
21770
22055
  */
21771
22056
 
21772
- 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 };
21773
22058
  //# sourceMappingURL=jax-data-science-sci-image-visualizer.mjs.map