Skip to content

Commit cf8d25b

Browse files
authored
feat: Auto multichannel setup for image layers (#770)
1 parent d15b9aa commit cf8d25b

File tree

10 files changed

+601
-7
lines changed

10 files changed

+601
-7
lines changed

src/datasource/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {
5454
getPrefixMatchesWithDescriptions,
5555
} from "#src/util/completion.js";
5656
import { RefCounted } from "#src/util/disposable.js";
57+
import type { vec3 } from "#src/util/geom.js";
5758
import { type ProgressOptions } from "#src/util/progress_listener.js";
5859
import type { Trackable } from "#src/util/trackable.js";
5960

@@ -173,12 +174,27 @@ export interface DataSubsourceEntry {
173174
default: boolean;
174175
}
175176

177+
export interface ChannelMetadata {
178+
name?: string;
179+
channels: SingleChannelMetadata[];
180+
}
181+
182+
export interface SingleChannelMetadata {
183+
color?: vec3;
184+
label?: string;
185+
active?: boolean;
186+
window?: [number, number];
187+
range?: [number, number];
188+
coefficient?: number;
189+
}
190+
176191
export interface DataSource {
177192
subsources: DataSubsourceEntry[];
178193
modelTransform: CoordinateSpaceTransform;
179194
canChangeModelSpaceRank?: boolean;
180195
state?: Trackable;
181196
canonicalUrl?: string;
197+
channelMetadata?: ChannelMetadata;
182198
}
183199

184200
export interface DataSourceRedirect {

src/datasource/zarr/frontend.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
makeIdentityTransformedBoundingBox,
2727
} from "#src/coordinate_transform.js";
2828
import type {
29+
ChannelMetadata,
2930
DataSource,
3031
GetKvStoreBasedDataSourceOptions,
3132
KvStoreBasedDataSourceProvider,
@@ -509,23 +510,25 @@ export class ZarrDataSource implements KvStoreBasedDataSourceProvider {
509510
zarrVersion: this.zarrVersion,
510511
explicitDimensionSeparator: dimensionSeparator,
511512
});
513+
let channelMetadata: ChannelMetadata | undefined;
512514
if (metadata === undefined) {
513515
throw new Error("No zarr metadata found");
514516
}
515517
let multiscaleInfo: ZarrMultiscaleInfo;
516518
if (metadata.nodeType === "group") {
517519
// May be an OME-zarr multiscale dataset.
518-
const multiscale = parseOmeMetadata(
520+
const omeMetadata = parseOmeMetadata(
519521
kvStoreUrl,
520522
metadata.userAttributes,
521523
metadata.zarrVersion,
522524
);
523-
if (multiscale === undefined) {
525+
if (omeMetadata === undefined) {
524526
throw new Error("Neither array nor OME multiscale metadata found");
525527
}
528+
channelMetadata = omeMetadata.channels;
526529
multiscaleInfo = await resolveOmeMultiscale(
527530
sharedKvStoreContext,
528-
multiscale,
531+
omeMetadata.multiscale,
529532
{
530533
...progressOptions,
531534
zarrVersion: metadata.zarrVersion,
@@ -545,6 +548,7 @@ export class ZarrDataSource implements KvStoreBasedDataSourceProvider {
545548
return {
546549
canonicalUrl: `${kvStoreUrl}|zarr${metadata.zarrVersion}:`,
547550
modelTransform: makeIdentityTransform(volume.modelSpace),
551+
channelMetadata,
548552
subsources: [
549553
{
550554
id: "default",

src/datasource/zarr/ome.ts

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,23 @@
1616

1717
import type { CoordinateSpace } from "#src/coordinate_transform.js";
1818
import { makeCoordinateSpace } from "#src/coordinate_transform.js";
19+
import type {
20+
SingleChannelMetadata,
21+
ChannelMetadata,
22+
} from "#src/datasource/index.js";
23+
import { parseRGBColorSpecification } from "#src/util/color.js";
1924
import {
2025
parseArray,
2126
parseFixedLengthArray,
27+
verifyBoolean,
2228
verifyFiniteFloat,
2329
verifyFinitePositiveFloat,
2430
verifyObject,
2531
verifyObjectProperty,
2632
verifyOptionalObjectProperty,
2733
verifyString,
2834
} from "#src/util/json.js";
35+
import { clampToInterval } from "#src/util/lerp.js";
2936
import * as matrix from "#src/util/matrix.js";
3037
import { allSiPrefixes } from "#src/util/si_units.js";
3138

@@ -39,6 +46,11 @@ export interface OmeMultiscaleMetadata {
3946
coordinateSpace: CoordinateSpace;
4047
}
4148

49+
export interface OmeMetadata {
50+
multiscale: OmeMultiscaleMetadata;
51+
channels: ChannelMetadata | undefined;
52+
}
53+
4254
const SUPPORTED_OME_MULTISCALE_VERSIONS = new Set(["0.4", "0.5-dev", "0.5"]);
4355

4456
const OME_UNITS = new Map<string, { unit: string; scale: number }>([
@@ -72,6 +84,75 @@ interface Axis {
7284
type: string | undefined;
7385
}
7486

87+
function parseOmeroChannel(omeroChannel: unknown): SingleChannelMetadata {
88+
verifyObject(omeroChannel);
89+
90+
const getProp = <T>(
91+
key: string,
92+
verifier: (value: unknown) => T,
93+
): T | undefined => verifyOptionalObjectProperty(omeroChannel, key, verifier);
94+
const inputWindow = getProp("window", verifyObject);
95+
const getWindowProp = <T>(
96+
key: string,
97+
verifier: (value: unknown) => T,
98+
): T | undefined =>
99+
inputWindow
100+
? verifyOptionalObjectProperty(inputWindow, key, verifier)
101+
: undefined;
102+
103+
const active = getProp("active", verifyBoolean);
104+
const coefficient = getProp("coefficient", verifyFiniteFloat);
105+
let colorString = getProp("color", verifyString);
106+
// If six hex digits, needs the # in front of the hex color
107+
if (colorString && /^[0-9a-f]{6}$/i.test(colorString)) {
108+
colorString = `#${colorString}`;
109+
}
110+
const color = parseRGBColorSpecification(colorString);
111+
const inverted = getProp("inverted", verifyBoolean);
112+
const label = getProp("label", verifyString);
113+
114+
const windowMin = getWindowProp("min", verifyFiniteFloat);
115+
const windowMax = getWindowProp("max", verifyFiniteFloat);
116+
const windowStart = getWindowProp("start", verifyFiniteFloat);
117+
const windowEnd = getWindowProp("end", verifyFiniteFloat);
118+
119+
const window =
120+
windowMin !== undefined && windowMax !== undefined
121+
? ([windowMin, windowMax] as [number, number])
122+
: undefined;
123+
124+
const range =
125+
windowStart !== undefined && windowEnd !== undefined
126+
? inverted
127+
? ([windowEnd, windowStart] as [number, number])
128+
: ([windowStart, windowEnd] as [number, number])
129+
: undefined;
130+
// If there is a window, then clamp the range to the window.
131+
if (window !== undefined && range !== undefined) {
132+
range[0] = clampToInterval(window, range[0]) as number;
133+
range[1] = clampToInterval(window, range[1]) as number;
134+
}
135+
136+
return {
137+
active,
138+
label,
139+
color,
140+
coefficient,
141+
range,
142+
window,
143+
};
144+
}
145+
146+
function parseOmeroMetadata(omero: unknown): ChannelMetadata {
147+
verifyObject(omero);
148+
const name = verifyOptionalObjectProperty(omero, "name", verifyString);
149+
const channels = verifyObjectProperty(omero, "channels", (x) =>
150+
parseArray(x, parseOmeroChannel),
151+
);
152+
153+
return { name, channels };
154+
}
155+
75156
function parseOmeAxis(axis: unknown): Axis {
76157
verifyObject(axis);
77158
const name = verifyObjectProperty(axis, "name", verifyString);
@@ -266,9 +347,10 @@ export function parseOmeMetadata(
266347
url: string,
267348
attrs: any,
268349
zarrVersion: number,
269-
): OmeMultiscaleMetadata | undefined {
350+
): OmeMetadata | undefined {
270351
const ome = attrs.ome;
271352
const multiscales = ome == undefined ? attrs.multiscales : ome.multiscales; // >0.4
353+
const omero = attrs.omero;
272354

273355
if (!Array.isArray(multiscales)) return undefined;
274356
const errors: string[] = [];
@@ -301,7 +383,9 @@ export function parseOmeMetadata(
301383
);
302384
continue;
303385
}
304-
return parseOmeMultiscale(url, multiscale);
386+
const multiScaleInfo = parseOmeMultiscale(url, multiscale);
387+
const channelMetadata = omero ? parseOmeroMetadata(omero) : undefined;
388+
return { multiscale: multiScaleInfo, channels: channelMetadata };
305389
}
306390
if (errors.length !== 0) {
307391
throw new Error(errors[0]);

src/display_context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ export class DisplayContext extends RefCounted implements FrameNumberCounter {
435435
updateFinished = new NullarySignal();
436436
continuousCameraMotionStarted = new NullarySignal();
437437
continuousCameraMotionFinished = new NullarySignal();
438+
multiChannelSetupFinished = new NullarySignal();
438439
changed = this.updateFinished;
439440
panels = new Set<RenderedPanel>();
440441
canvasRect: DOMRect | undefined;

src/layer/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
LayerDataSource,
4444
layerDataSourceSpecificationFromJson,
4545
} from "#src/layer/layer_data_source.js";
46+
import { createImageLayerAsMultiChannel } from "#src/layer/multi_channel_setup.js";
4647
import type {
4748
DisplayDimensions,
4849
WatchableDisplayDimensionRenderInfo,
@@ -2468,6 +2469,15 @@ export class AutoUserLayer extends UserLayer {
24682469
detectLayerTypeFromSubsources(subsources)?.layerConstructor;
24692470
if (layerConstructor !== undefined) {
24702471
changeLayerType(this.managedLayer, layerConstructor);
2472+
this.registerDisposer(
2473+
this.managedLayer.readyStateChanged.add(() => {
2474+
if (this.managedLayer.isReady()) {
2475+
// If you want to restore the old auto image setup, pass true here
2476+
// This will then make the previous image layer setup
2477+
createImageLayerAsMultiChannel(this.managedLayer, makeLayer);
2478+
}
2479+
}),
2480+
);
24712481
}
24722482
}
24732483
}

0 commit comments

Comments
 (0)