diff --git a/src/layer/segmentation/index.ts b/src/layer/segmentation/index.ts index 43a21b05cd..98e7b7c2c4 100644 --- a/src/layer/segmentation/index.ts +++ b/src/layer/segmentation/index.ts @@ -119,15 +119,22 @@ import { } from "#src/util/color.js"; import type { Borrowed, Owned } from "#src/util/disposable.js"; import { RefCounted } from "#src/util/disposable.js"; -import type { vec3, vec4 } from "#src/util/geom.js"; +import type { vec4 } from "#src/util/geom.js"; +import { vec3 } from "#src/util/geom.js"; import { parseArray, parseUint64, + verifyArray, + verifyBoolean, verifyFiniteNonNegativeFloat, + verifyFloat, + verifyObject, verifyObjectAsMap, + verifyObjectProperty, verifyOptionalObjectProperty, verifyString, } from "#src/util/json.js"; +import { computeInvlerp } from "#src/util/lerp.js"; import { Signal } from "#src/util/signal.js"; import { makeWatchableShaderError } from "#src/webgl/dynamic_shader.js"; import type { DependentViewContext } from "#src/widget/dependent_view_widget.js"; @@ -135,6 +142,33 @@ import { registerLayerShaderControlsTool } from "#src/widget/shader_controls.js" const MAX_LAYER_BAR_UI_INDICATOR_COLORS = 6; +export interface SegmentPropertyColorBase { + type: "tag" | "numeric"; + active: boolean; +} + +export interface SegmentPropertyColorNumeric extends SegmentPropertyColorBase { + type: "numeric"; + active: boolean; + property: string; + options: { + min?: number; + max?: number; + minColor: string; + maxColor: string; + }; +} + +export interface SegmentPropertyColorTag extends SegmentPropertyColorBase { + type: "tag"; + active: boolean; + map: Map; +} + +export type SegmentPropertyColor = + | SegmentPropertyColorNumeric + | SegmentPropertyColorTag; + export class SegmentationUserLayerGroupState extends RefCounted implements SegmentationGroupState @@ -298,9 +332,84 @@ export class SegmentationUserLayerColorGroupState specificationChanged = new Signal(); constructor(public layer: SegmentationUserLayer) { super(); + + this.segmentPropertyColorsMap = this.registerDisposer( + makeCachedDerivedWatchableValue( + (segmentPropertyMap, segmentPropertyColors) => { + const map = new Uint64Map(); + if (!segmentPropertyMap) { + return map; + } + const { tags: tagsProperty } = segmentPropertyMap; + for (const propertyColor of segmentPropertyColors) { + if (!propertyColor.active) continue; + if (propertyColor.type === "tag" && tagsProperty) { + const { tags, values } = tagsProperty; + const bigIntColors = new Map(); + for (const [tag, colorString] of propertyColor.map!.entries()) { + const color = parseRGBColorSpecification(colorString); + bigIntColors.set(tag, BigInt(packColor(color))); + } + for (const [index, id] of ( + segmentPropertyMap.segmentPropertyMap.inlineProperties?.ids || + [] + ).entries()) { + if (map.has(id)) continue; // prioritize first color match + const tagIndices = values[index]; + const segmentTags = new Set( + tagIndices.split("").map((x) => tags[x.charCodeAt(0)]), + ); + for (const [tag, color] of bigIntColors.entries()) { + if (segmentTags.has(tag)) { + map.set(id, color); + break; + } + } + } + } else if (propertyColor.type === "numeric") { + const options = propertyColor.options; + const { numericalProperties } = segmentPropertyMap; + const numericalProperty = numericalProperties.find( + (x) => x.id === propertyColor.property, + ); + if (numericalProperty) { + const minColor = parseRGBColorSpecification(options.minColor); + const maxColor = parseRGBColorSpecification(options.maxColor); + const color = vec3.create(); + const { values, bounds } = numericalProperty; + const min = Number(options.min ?? bounds[0]); + const max = Number(options.max ?? bounds[1]); + for (const [index, id] of ( + segmentPropertyMap.segmentPropertyMap.inlineProperties?.ids || + [] + ).entries()) { + if (map.has(id)) continue; // prioritize first color match + const value = values[index]; + if (Number.isNaN(value)) continue; + const normalized = Math.min( + 1, + Math.max(0, computeInvlerp([min, max], value)), + ); + vec3.lerp(color, minColor, maxColor, normalized); + map.set(id, BigInt(packColor(color))); + } + } + } + } + return map; + }, + [ + this.layer.displayState.originalSegmentationGroupState + .segmentPropertyMap, + this.segmentPropertyColors, + ], + ), + ); + const { specificationChanged } = this; this.segmentColorHash.changed.add(specificationChanged.dispatch); this.segmentStatedColors.changed.add(specificationChanged.dispatch); + this.segmentPropertyColorsMap.changed.add(specificationChanged.dispatch); this.tempSegmentStatedColors2d.changed.add(specificationChanged.dispatch); this.segmentDefaultColor.changed.add(specificationChanged.dispatch); this.tempSegmentDefaultColor2d.changed.add(specificationChanged.dispatch); @@ -332,6 +441,82 @@ export class SegmentationUserLayerColorGroupState } }, ); + verifyOptionalObjectProperty( + specification, + json_keys.SEGMENT_PROPERTY_COLORS_JSON_KEY, + (value) => { + const segmentPropertyColors = verifyArray(value); + for (const propertyColor of segmentPropertyColors) { + verifyObject(propertyColor); + const type = verifyObjectProperty( + propertyColor, + "type", + verifyString, + ); + const active = verifyOptionalObjectProperty( + propertyColor, + "active", + verifyBoolean, + ); + if (type === "tag") { + const map = verifyObjectProperty(propertyColor, "map", (x) => { + verifyObject(x); + const res = new Map(); + for (const [tag, value] of Object.entries(x)) { + const color = verifyString(value); + res.set(tag, color); + } + return res; + }); + this.segmentPropertyColors.value.push({ + type, + active: active ?? true, + map, + }); + } else if (type === "numeric") { + const property = verifyObjectProperty( + propertyColor, + "property", + verifyString, + ); + const options = verifyObjectProperty( + propertyColor, + "options", + (x) => { + verifyObject(x); + const min = verifyOptionalObjectProperty(x, "min", verifyFloat); + const max = verifyOptionalObjectProperty(x, "max", verifyFloat); + const minColor = verifyObjectProperty( + x, + "minColor", + verifyString, + ); + const maxColor = verifyObjectProperty( + x, + "maxColor", + verifyString, + ); + return { + min, + max, + minColor, + maxColor, + }; + }, + ); + this.segmentPropertyColors.value.push({ + type, + property, + active: active ?? true, + options, + }); + } else { + throw new Error(`unsupported type: ${type}`); + } + } + this.segmentPropertyColors.changed.dispatch(); + }, + ); } toJSON() { @@ -339,19 +524,48 @@ export class SegmentationUserLayerColorGroupState x[json_keys.COLOR_SEED_JSON_KEY] = this.segmentColorHash.toJSON(); x[json_keys.SEGMENT_DEFAULT_COLOR_JSON_KEY] = this.segmentDefaultColor.toJSON(); - const { segmentStatedColors } = this; + const { + segmentStatedColors, + segmentPropertyColors: { value: segmentPropertyColors }, + } = this; if (segmentStatedColors.size > 0) { const j: any = (x[json_keys.SEGMENT_STATED_COLORS_JSON_KEY] = {}); for (const [key, value] of segmentStatedColors) { j[key.toString()] = serializeColor(unpackRGB(Number(value))); } } + if (segmentPropertyColors.length > 0) { + x[json_keys.SEGMENT_PROPERTY_COLORS_JSON_KEY] = segmentPropertyColors.map( + (propertyColor) => { + const { type, active } = propertyColor; + if (type === "numeric") { + const { property, options } = propertyColor; + return { + type, + active, + property, + options, + }; + } else { + const { map } = propertyColor; + return { + type, + active, + map: Object.fromEntries( + map.entries().map(([k, v]) => [k.toString(), v]), + ), + }; + } + }, + ); + } return x; } assignFrom(other: SegmentationUserLayerColorGroupState) { this.segmentColorHash.value = other.segmentColorHash.value; this.segmentStatedColors.assignFrom(other.segmentStatedColors); + this.segmentPropertyColors.value = other.segmentPropertyColors.value; this.tempSegmentStatedColors2d.assignFrom(other.tempSegmentStatedColors2d); this.segmentDefaultColor.value = other.segmentDefaultColor.value; this.highlightColor.value = other.highlightColor.value; @@ -359,6 +573,8 @@ export class SegmentationUserLayerColorGroupState segmentColorHash = SegmentColorHash.getDefault(); segmentStatedColors = this.registerDisposer(new Uint64Map()); + segmentPropertyColors = new WatchableValue([]); + segmentPropertyColorsMap: WatchableValueInterface; tempSegmentStatedColors2d = this.registerDisposer(new Uint64Map()); segmentDefaultColor = new TrackableOptionalRGB(); tempSegmentDefaultColor2d = new WatchableValue( @@ -484,6 +700,18 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { (group) => group.segmentStatedColors, ), ); + this.segmentPropertyColors = this.layer.registerDisposer( + new IndirectTrackableValue( + this.segmentationColorGroupState, + (group) => group.segmentPropertyColors, + ), + ); + this.segmentPropertyColorsMap = this.layer.registerDisposer( + new IndirectTrackableValue( + this.segmentationColorGroupState, + (group) => group.segmentPropertyColorsMap, + ), + ); this.tempSegmentStatedColors2d = this.layer.registerDisposer( new IndirectTrackableValue( this.segmentationColorGroupState, @@ -564,6 +792,8 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { segmentColorHash: TrackableValueInterface; segmentStatedColors: WatchableValueInterface; tempSegmentStatedColors2d: WatchableValueInterface; + segmentPropertyColors: WatchableValueInterface; + segmentPropertyColorsMap: WatchableValueInterface; segmentDefaultColor: WatchableValueInterface; tempSegmentDefaultColor2d: WatchableValueInterface; highlightColor: WatchableValueInterface; diff --git a/src/layer/segmentation/json_keys.ts b/src/layer/segmentation/json_keys.ts index 47d603dab4..6231bff57a 100644 --- a/src/layer/segmentation/json_keys.ts +++ b/src/layer/segmentation/json_keys.ts @@ -12,6 +12,7 @@ export const SEGMENTS_JSON_KEY = "segments"; export const EQUIVALENCES_JSON_KEY = "equivalences"; export const COLOR_SEED_JSON_KEY = "colorSeed"; export const SEGMENT_STATED_COLORS_JSON_KEY = "segmentColors"; +export const SEGMENT_PROPERTY_COLORS_JSON_KEY = "segmentPropertyColors"; export const MESH_RENDER_SCALE_JSON_KEY = "meshRenderScale"; export const CROSS_SECTION_RENDER_SCALE_JSON_KEY = "crossSectionRenderScale"; export const SKELETON_RENDERING_JSON_KEY = "skeletonRendering"; diff --git a/src/segmentation_display_state/frontend.ts b/src/segmentation_display_state/frontend.ts index f9397cae52..886a0472dd 100644 --- a/src/segmentation_display_state/frontend.ts +++ b/src/segmentation_display_state/frontend.ts @@ -18,7 +18,10 @@ import type { LayerChunkProgressInfo } from "#src/chunk_manager/base.js"; import type { ChunkManager } from "#src/chunk_manager/frontend.js"; import { ChunkRenderLayerFrontend } from "#src/chunk_manager/frontend.js"; import type { LayerSelectedValues } from "#src/layer/index.js"; -import type { SegmentationUserLayer } from "#src/layer/segmentation/index.js"; +import type { + SegmentationUserLayer, + SegmentPropertyColor, +} from "#src/layer/segmentation/index.js"; import type { PickIDManager } from "#src/object_picking.js"; import type { WatchableRenderLayerTransform } from "#src/render_coordinate_transform.js"; import type { RenderScaleHistogram } from "#src/render_scale_statistics.js"; @@ -178,6 +181,8 @@ export interface SegmentationColorGroupState { segmentColorHash: SegmentColorHash; segmentStatedColors: Uint64Map; tempSegmentStatedColors2d: Uint64Map; + segmentPropertyColors: WatchableValueInterface; + segmentPropertyColorsMap: WatchableValueInterface; segmentDefaultColor: WatchableValueInterface; tempSegmentDefaultColor2d: WatchableValueInterface; } @@ -199,6 +204,7 @@ export interface SegmentationDisplayState { hideSegmentZero: WatchableValueInterface; segmentColorHash: WatchableValueInterface; segmentStatedColors: WatchableValueInterface; + segmentPropertyColorsMap: WatchableValueInterface; tempSegmentStatedColors2d: WatchableValueInterface; useTempSegmentStatedColors2d: WatchableValueInterface; segmentDefaultColor: WatchableValueInterface; @@ -934,7 +940,10 @@ export function getBaseObjectColor( return color; } const colorGroupState = displayState.segmentationColorGroupState.value; - const { segmentStatedColors } = colorGroupState; + const { + segmentStatedColors, + segmentPropertyColorsMap: { value: segmentPropertyColorsMap }, + } = colorGroupState; let statedColor: bigint | undefined; if ( segmentStatedColors.size !== 0 && @@ -947,6 +956,16 @@ export function getBaseObjectColor( color[2] = (Number(statedColor & 0xff0000n) >>> 16) / 255.0; return color; } + let propertyColor: bigint | undefined; + if ( + segmentPropertyColorsMap.size !== 0 && + (propertyColor = segmentPropertyColorsMap.get(objectId)) !== undefined + ) { + color[0] = Number(propertyColor & 0x0000ffn) / 255.0; + color[1] = (Number(propertyColor & 0x00ff00n) >>> 8) / 255.0; + color[2] = (Number(propertyColor & 0xff0000n) >>> 16) / 255.0; + return color; + } const segmentDefaultColor = colorGroupState.segmentDefaultColor.value; if (segmentDefaultColor !== undefined) { color[0] = segmentDefaultColor[0]; diff --git a/src/sliceview/volume/segmentation_renderlayer.ts b/src/sliceview/volume/segmentation_renderlayer.ts index c4a6c44b16..2f11b6f02b 100644 --- a/src/sliceview/volume/segmentation_renderlayer.ts +++ b/src/sliceview/volume/segmentation_renderlayer.ts @@ -46,7 +46,7 @@ import { AggregateWatchableValue, makeCachedDerivedWatchableValue, } from "#src/trackable_value.js"; -import type { Uint64Map } from "#src/uint64_map.js"; +import { Uint64Map } from "#src/uint64_map.js"; import type { DisjointUint64Sets } from "#src/util/disjoint_sets.js"; import type { ShaderBuilder, ShaderProgram } from "#src/webgl/shader.js"; @@ -125,16 +125,20 @@ export class SegmentationRenderLayer extends SliceViewVolumeRenderLayer { const releventMap = useTempSegmentStatedColors2d ? tempSegmentStatedColors2d : segmentStatedColors; - return releventMap.size !== 0; + return ( + releventMap.size !== 0 || segmentPropertyColorsMap.size !== 0 + ); }, [ displayState.segmentStatedColors, + displayState.segmentPropertyColorsMap, displayState.tempSegmentStatedColors2d, displayState.useTempSegmentStatedColors2d, ], @@ -422,15 +426,22 @@ uint64_t getMappedObjectId(uint64_t value) { .value ? displayState.tempSegmentStatedColors2d.value : displayState.segmentStatedColors.value; + + const segmentPropertyColorsMap = + displayState.segmentPropertyColorsMap.value; + + const combinedMap = new Uint64Map(); + combinedMap.assignFrom(segmentStatedColors, false); + combinedMap.assignFrom(segmentPropertyColorsMap, false); + let { gpuSegmentStatedColorHashTable } = this; if ( gpuSegmentStatedColorHashTable === undefined || - gpuSegmentStatedColorHashTable.hashTable !== - segmentStatedColors.hashTable + gpuSegmentStatedColorHashTable.hashTable !== combinedMap.hashTable ) { gpuSegmentStatedColorHashTable?.dispose(); this.gpuSegmentStatedColorHashTable = gpuSegmentStatedColorHashTable = - GPUHashTable.get(gl, segmentStatedColors.hashTable); + GPUHashTable.get(gl, combinedMap.hashTable); } this.segmentStatedColorShaderManager.enable( gl, diff --git a/src/uint64_map.ts b/src/uint64_map.ts index b767ba6f0d..db48fcbbc9 100644 --- a/src/uint64_map.ts +++ b/src/uint64_map.ts @@ -90,8 +90,10 @@ export class Uint64Map return this.hashTable.size; } - assignFrom(other: Uint64Map) { - this.clear(); + assignFrom(other: Uint64Map, clear = true) { + if (clear) { + this.clear(); + } for (const [key, value] of other) { this.set(key, value); } diff --git a/src/util/json.ts b/src/util/json.ts index 5c4d21563f..c0ba609ee1 100644 --- a/src/util/json.ts +++ b/src/util/json.ts @@ -682,28 +682,31 @@ export function verify3dDimensions(obj: any) { return parseFixedLengthArray(vec3.create(), obj, verifyPositiveInt); } -export function verifyStringArray(a: any) { +export function verifyArray(a: any) { if (!Array.isArray(a)) { throw new Error(`Expected array, received: ${JSON.stringify(a)}.`); } - for (const x of a) { + return a; +} + +export function verifyStringArray(a: any) { + const arr = verifyArray(a); + for (const x of arr) { if (typeof x !== "string") { throw new Error(`Expected string, received: ${JSON.stringify(x)}.`); } } - return a; + return arr; } export function verifyIntegerArray(a: unknown) { - if (!Array.isArray(a)) { - throw new Error(`Expected array, received: ${JSON.stringify(a)}.`); - } - for (const x of a) { + const arr = verifyArray(a); + for (const x of arr) { if (!Number.isInteger(x)) { throw new Error(`Expected integer, received: ${JSON.stringify(x)}.`); } } - return a; + return arr; } export function verifyBoolean(x: any) {