Skip to content

Commit e95351c

Browse files
fm3hotzenklotzknollengewaechs
authored
Use datastore segment index when loading ad-hoc meshes (#8922)
### URL of deployed dev instance (used for testing): - https://adhocdatastoresegmentindex.webknossos.xyz ### Steps to test: - Get a segment index file for l4_sample from https://www.notion.so/scalableminds/Test-Datasets-c0563be9c4a4499dae4e16d9b2497cfb?source=copy_link#27db51644c63800ea207ee7674502373 and register it in the datasource-properties.json (merge with existing attachments block if needed) ``` "attachments" : { "segmentIndex" : { "name" : "segment_index_file", "path" : "./segmentation/segmentIndex/l4_sample_segment_index_file.hdf5", "dataFormat" : "hdf5" }, } ``` - Load ad-hoc mesh from position `3629, 3489, 1024` (Segment 41) - mesh should now show the second unconnected piece <img width="305" height="262" alt="image" src="https://github.com/user-attachments/assets/9717192f-e38b-4f33-8c26-19a020def3d5" /> - Also load adhoc mesh from volume annotation with some brushed segments, should work without errors - For static segmentation layer without segment index file, adhoc meshes should still be loadable (they won’t show unconnected pieces though) - Also load the same mesh via the backend fullMesh.stl route with `curl 'http://localhost:9000/data/datasets/68da42fbd90000f3132f9921/layers/segmentation/meshes/fullMesh.stl?token=secretSampleUserToken' -X POST -H 'Accept-Encoding: gzip, deflate, br' -H 'content-type: application/json' --data-raw '{"segmentId": 41, "mag": [8, 8, 2]}' --output ~/out.stl` (insert dataset id from your local wk instance), inspect result, e.g. in meshlab. Should also show both pieces. ### Issues: - fixes #7615 some context: https://scm.slack.com/archives/C5AKLAV0B/p1757940808856039 ------ - [x] Added changelog entry (create a `$PR_NUMBER.md` file in `unreleased_changes` or use `./tools/create-changelog-entry.py`) - [x] Removed dev-only changes like prints and application.conf edits - [x] Considered [common edge cases](../blob/master/.github/common_edge_cases.md) - [x] Needs datastore update after deployment --------- Co-authored-by: Tom Herold <[email protected]> Co-authored-by: Charlie Meister <[email protected]> Co-authored-by: Charlie Meister <[email protected]>
1 parent 6f640c1 commit e95351c

File tree

12 files changed

+190
-84
lines changed

12 files changed

+190
-84
lines changed

frontend/javascripts/admin/rest_api.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ import type { DatasourceConfiguration } from "types/schemas/datasource.types";
8080
import type { AnnotationTypeFilterEnum, LOG_LEVELS, Vector3 } from "viewer/constants";
8181
import Constants, { ControlModeEnum, AnnotationStateFilterEnum } from "viewer/constants";
8282
import type BoundingBox from "viewer/model/bucket_data_handling/bounding_box";
83+
import {
84+
type LayerSourceInfo,
85+
getDataOrTracingStoreUrl,
86+
} from "viewer/model/bucket_data_handling/wkstore_helper";
8387
import {
8488
parseProtoAnnotation,
8589
parseProtoListOfLong,
@@ -855,13 +859,18 @@ export function hasSegmentIndexInDataStore(
855859
);
856860
}
857861

862+
export const hasSegmentIndexInDataStoreCached = _.memoize(hasSegmentIndexInDataStore, (...args) =>
863+
args.join("::"),
864+
);
865+
858866
export function getSegmentVolumes(
859-
requestUrl: string,
867+
layerSourceInfo: LayerSourceInfo,
860868
mag: Vector3,
861869
segmentIds: Array<number>,
862870
additionalCoordinates: AdditionalCoordinate[] | undefined | null,
863871
mappingName: string | null | undefined,
864872
): Promise<number[]> {
873+
const requestUrl = getDataOrTracingStoreUrl(layerSourceInfo);
865874
return doWithToken((token) =>
866875
Request.sendJSONReceiveJSON(`${requestUrl}/segmentStatistics/volume?token=${token}`, {
867876
data: { additionalCoordinates, mag, segmentIds, mappingName },
@@ -871,12 +880,13 @@ export function getSegmentVolumes(
871880
}
872881

873882
export function getSegmentBoundingBoxes(
874-
requestUrl: string,
883+
layerSourceInfo: LayerSourceInfo,
875884
mag: Vector3,
876885
segmentIds: Array<number>,
877886
additionalCoordinates: AdditionalCoordinate[] | undefined | null,
878887
mappingName: string | null | undefined,
879888
): Promise<Array<{ topLeft: Vector3; width: number; height: number; depth: number }>> {
889+
const requestUrl = getDataOrTracingStoreUrl(layerSourceInfo);
880890
return doWithToken((token) =>
881891
Request.sendJSONReceiveJSON(`${requestUrl}/segmentStatistics/boundingBox?token=${token}`, {
882892
data: { additionalCoordinates, mag, segmentIds, mappingName },
@@ -1877,12 +1887,13 @@ type MeshRequest = {
18771887
};
18781888

18791889
export function computeAdHocMesh(
1880-
requestUrl: string,
1890+
layerSourceInfo: LayerSourceInfo,
18811891
meshRequest: MeshRequest,
18821892
): Promise<{
18831893
buffer: ArrayBuffer;
18841894
neighbors: Array<number>;
18851895
}> {
1896+
const requestUrl = getDataOrTracingStoreUrl(layerSourceInfo);
18861897
const {
18871898
positionWithPadding,
18881899
additionalCoordinates,
@@ -1924,23 +1935,25 @@ export function computeAdHocMesh(
19241935
}
19251936

19261937
export function getBucketPositionsForAdHocMesh(
1927-
tracingStoreUrl: string,
1928-
tracingId: string,
1938+
layerSourceInfo: LayerSourceInfo,
19291939
segmentId: number,
19301940
cubeSize: Vector3,
19311941
mag: Vector3,
19321942
additionalCoordinates: AdditionalCoordinate[] | null | undefined,
1943+
mappingName: string | null | undefined,
19331944
): Promise<Vector3[]> {
1945+
const requestUrl = getDataOrTracingStoreUrl(layerSourceInfo);
19341946
return doWithToken(async (token) => {
19351947
const params = new URLSearchParams();
19361948
params.set("token", token);
19371949
const positions = await Request.sendJSONReceiveJSON(
1938-
`${tracingStoreUrl}/tracings/volume/${tracingId}/segmentIndex/${segmentId}?${params}`,
1950+
`${requestUrl}/segmentIndex/${segmentId}?${params}`,
19391951
{
19401952
data: {
19411953
cubeSize,
19421954
mag,
19431955
additionalCoordinates,
1956+
mappingName,
19441957
},
19451958
method: "POST",
19461959
},
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { APIDataset } from "types/api_types";
2+
import type { StoreAnnotation } from "viewer/store";
3+
4+
export type LayerSourceInfo = {
5+
dataset: APIDataset;
6+
annotation: StoreAnnotation | null;
7+
tracingId: string | undefined;
8+
segmentationLayerName: string;
9+
useDataStore?: boolean | undefined | null;
10+
};
11+
12+
export function getDataOrTracingStoreUrl(layerSourceInfo: LayerSourceInfo) {
13+
const { dataset, annotation, segmentationLayerName, tracingId, useDataStore } = layerSourceInfo;
14+
if (annotation == null || tracingId == null || useDataStore) {
15+
return `${dataset.dataStore.url}/data/datasets/${dataset.id}/layers/${segmentationLayerName}`;
16+
} else {
17+
const tracingStoreHost = annotation?.tracingStore.url;
18+
return `${tracingStoreHost}/tracings/volume/${tracingId}`;
19+
}
20+
}

frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
computeAdHocMesh,
33
getBucketPositionsForAdHocMesh,
4+
hasSegmentIndexInDataStoreCached,
45
sendAnalyticsEvent,
56
} from "admin/rest_api";
67
import ThreeDMap from "libs/ThreeDMap";
@@ -41,13 +42,14 @@ import {
4142
type LoadAdHocMeshAction,
4243
loadPrecomputedMeshAction,
4344
} from "viewer/model/actions/segmentation_actions";
45+
import type { LayerSourceInfo } from "viewer/model/bucket_data_handling/wkstore_helper";
4446
import type DataLayer from "viewer/model/data_layer";
4547
import type { MagInfo } from "viewer/model/helpers/mag_info";
4648
import { zoomedAddressToAnotherZoomStepWithInfo } from "viewer/model/helpers/position_converter";
4749
import type { Saga } from "viewer/model/sagas/effect-generators";
4850
import { select } from "viewer/model/sagas/effect-generators";
4951
import { Model } from "viewer/singletons";
50-
import Store from "viewer/store";
52+
import Store, { type StoreDataset, type VolumeTracing } from "viewer/store";
5153
import { getAdditionalCoordinatesAsString } from "../../accessors/flycam_accessor";
5254
import { ensureSceneControllerReady, ensureWkReady } from "../ready_sagas";
5355

@@ -293,6 +295,25 @@ function removeMeshWithoutVoxels(
293295
}
294296
}
295297

298+
function* getUsePositionsFromSegmentIndex(
299+
volumeTracing: VolumeTracing | null | undefined,
300+
dataset: StoreDataset,
301+
layerName: string,
302+
maybeTracingId?: string | null,
303+
): Saga<boolean> {
304+
if (volumeTracing == null) {
305+
return yield* call(
306+
hasSegmentIndexInDataStoreCached,
307+
dataset.dataStore.url,
308+
dataset.id,
309+
layerName,
310+
);
311+
}
312+
return (
313+
volumeTracing?.hasSegmentIndex && !volumeTracing.hasEditableMapping && maybeTracingId != null
314+
);
315+
}
316+
296317
function* loadFullAdHocMesh(
297318
layer: DataLayer,
298319
segmentId: number,
@@ -320,36 +341,54 @@ function* loadFullAdHocMesh(
320341
yield* put(startedLoadingMeshAction(layer.name, segmentId));
321342

322343
const cubeSize = marchingCubeSizeInTargetMag();
323-
const tracingStoreHost = yield* select((state) => state.annotation.tracingStore.url);
344+
const dataset = yield* select((state) => state.dataset);
324345
const mag = magInfo.getMagByIndexOrThrow(zoomStep);
325346

326347
const volumeTracing = yield* select((state) => getActiveSegmentationTracing(state));
348+
const annotation = yield* select((state) => state.annotation);
327349
const visibleSegmentationLayer = yield* select((state) => getVisibleSegmentationLayer(state));
350+
if (visibleSegmentationLayer == null) {
351+
throw new Error(
352+
"Loading the ad-hoc mesh failed because the visible segmentation layer must not be null.",
353+
);
354+
}
328355
// Fetch from datastore if no volumetracing ...
329-
let useDataStore = volumeTracing == null || visibleSegmentationLayer?.tracingId == null;
356+
let forceUsingDataStore = volumeTracing == null || visibleSegmentationLayer.tracingId == null;
330357
if (meshExtraInfo.useDataStore != null) {
331358
// ... except if the caller specified whether to use the data store ...
332-
useDataStore = meshExtraInfo.useDataStore;
359+
forceUsingDataStore = meshExtraInfo.useDataStore;
333360
} else if (volumeTracing?.hasEditableMapping) {
334361
// ... or if an editable mapping is active.
335-
useDataStore = false;
362+
forceUsingDataStore = false;
336363
}
337364

338-
// Segment stats can only be used for volume tracings that have a segment index
365+
// Segment stats can only be used for segmentation layers that have a segment index
339366
// and that don't have editable mappings.
340-
const usePositionsFromSegmentIndex =
341-
volumeTracing?.hasSegmentIndex &&
342-
!volumeTracing.hasEditableMapping &&
343-
visibleSegmentationLayer?.tracingId != null;
367+
const usePositionsFromSegmentIndex = yield* call(
368+
getUsePositionsFromSegmentIndex,
369+
volumeTracing,
370+
dataset,
371+
layer.name,
372+
visibleSegmentationLayer.tracingId,
373+
);
374+
375+
const layerSourceInfo: LayerSourceInfo = {
376+
dataset,
377+
annotation,
378+
tracingId: visibleSegmentationLayer.tracingId,
379+
segmentationLayerName: visibleSegmentationLayer.fallbackLayer ?? visibleSegmentationLayer.name,
380+
useDataStore: forceUsingDataStore,
381+
};
382+
344383
let positionsToRequest = usePositionsFromSegmentIndex
345384
? yield* getChunkPositionsFromSegmentIndex(
346-
tracingStoreHost,
347-
layer,
385+
layerSourceInfo,
348386
segmentId,
349387
cubeSize,
350388
mag,
351389
clippedPosition,
352390
additionalCoordinates,
391+
mappingName,
353392
)
354393
: [clippedPosition];
355394

@@ -373,7 +412,7 @@ function* loadFullAdHocMesh(
373412
magInfo,
374413
isInitialRequest,
375414
removeExistingMesh && isInitialRequest,
376-
useDataStore,
415+
layerSourceInfo,
377416
!usePositionsFromSegmentIndex,
378417
);
379418
isInitialRequest = false;
@@ -390,22 +429,22 @@ function* loadFullAdHocMesh(
390429
}
391430

392431
function* getChunkPositionsFromSegmentIndex(
393-
tracingStoreHost: string,
394-
layer: DataLayer,
432+
layerSourceInfo: LayerSourceInfo,
395433
segmentId: number,
396434
cubeSize: Vector3,
397435
mag: Vector3,
398436
clippedPosition: Vector3,
399437
additionalCoordinates: AdditionalCoordinate[] | null | undefined,
438+
mappingName: string | null | undefined,
400439
) {
401440
const targetMagPositions = yield* call(
402441
getBucketPositionsForAdHocMesh,
403-
tracingStoreHost,
404-
layer.name,
442+
layerSourceInfo,
405443
segmentId,
406444
cubeSize,
407445
mag,
408446
additionalCoordinates,
447+
mappingName,
409448
);
410449
const mag1Positions = targetMagPositions.map((pos) => V3.scale3(pos, mag));
411450
return sortByDistanceTo(mag1Positions, clippedPosition) as Vector3[];
@@ -424,7 +463,7 @@ function* maybeLoadMeshChunk(
424463
magInfo: MagInfo,
425464
isInitialRequest: boolean,
426465
removeExistingMesh: boolean,
427-
useDataStore: boolean,
466+
layerSourceInfo: LayerSourceInfo,
428467
findNeighbors: boolean,
429468
): Saga<Vector3[]> {
430469
const additionalCoordinates = yield* select((state) => state.flycam.additionalCoordinates);
@@ -445,17 +484,10 @@ function* maybeLoadMeshChunk(
445484
batchCounterPerSegment[segmentId]++;
446485
threeDMap.set(paddedPositionWithinLayer, true);
447486
const scaleFactor = yield* select((state) => state.dataset.dataSource.scale.factor);
448-
const dataStoreHost = yield* select((state) => state.dataset.dataStore.url);
449-
const datasetId = yield* select((state) => state.dataset.id);
450-
const tracingStoreHost = yield* select((state) => state.annotation.tracingStore.url);
451-
const dataStoreUrl = `${dataStoreHost}/data/datasets/${datasetId}/layers/${
452-
layer.fallbackLayer != null ? layer.fallbackLayer : layer.name
453-
}`;
454-
const tracingStoreUrl = `${tracingStoreHost}/tracings/volume/${layer.name}`;
455487

456488
if (isInitialRequest) {
457489
sendAnalyticsEvent("request_isosurface", {
458-
mode: useDataStore ? "view" : "annotation",
490+
mode: layerSourceInfo.useDataStore ? "view" : "annotation",
459491
});
460492
}
461493

@@ -472,7 +504,7 @@ function* maybeLoadMeshChunk(
472504
context: null,
473505
fn: computeAdHocMesh,
474506
},
475-
useDataStore ? dataStoreUrl : tracingStoreUrl,
507+
layerSourceInfo,
476508
{
477509
positionWithPadding: paddedPositionWithinLayer,
478510
additionalCoordinates: additionalCoordinates || undefined,

frontend/javascripts/viewer/view/context_menu.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,7 @@ import type {
146146
import { deleteNodeAsUserAction } from "viewer/model/actions/skeletontracing_actions_with_effects";
147147
import { type MutableNode, type Tree, TreeMap } from "viewer/model/types/tree_types";
148148
import Store from "viewer/store";
149-
import {
150-
getVolumeRequestUrl,
151-
withMappingActivationConfirmation,
152-
} from "viewer/view/right-border-tabs/segments_tab/segments_view_helper";
149+
import { withMappingActivationConfirmation } from "viewer/view/right-border-tabs/segments_tab/segments_view_helper";
153150
import { LayoutEvents, layoutEmitter } from "./layouting/layout_persistence";
154151
import { LoadMeshMenuItemLabel } from "./right-border-tabs/segments_tab/load_mesh_menu_item_label";
155152

@@ -1690,26 +1687,26 @@ function ContextMenuInner() {
16901687
if (visibleSegmentationLayer == null || !isSegmentIndexAvailable) return [];
16911688
const tracingId = volumeTracing?.tracingId;
16921689
const additionalCoordinates = flycam.additionalCoordinates;
1693-
const requestUrl = getVolumeRequestUrl(
1690+
const layerSourceInfo = {
16941691
dataset,
16951692
annotation,
16961693
tracingId,
1697-
visibleSegmentationLayer,
1698-
);
1694+
segmentationLayerName: visibleSegmentationLayer.name,
1695+
};
16991696
const magInfo = getMagInfo(visibleSegmentationLayer.mags);
17001697
const layersFinestMag = magInfo.getFinestMag();
17011698
const voxelSize = dataset.dataSource.scale;
17021699

17031700
try {
17041701
const [segmentSize] = await getSegmentVolumes(
1705-
requestUrl,
1702+
layerSourceInfo,
17061703
layersFinestMag,
17071704
[clickedSegmentOrMeshId],
17081705
additionalCoordinates,
17091706
mappingName,
17101707
);
17111708
const [boundingBoxInRequestedMag] = await getSegmentBoundingBoxes(
1712-
requestUrl,
1709+
layerSourceInfo,
17131710
layersFinestMag,
17141711
[clickedSegmentOrMeshId],
17151712
additionalCoordinates,

frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,7 @@ import { getBoundingBoxInMag1 } from "viewer/model/sagas/volume/helpers";
1717
import { voxelToVolumeInUnit } from "viewer/model/scaleinfo";
1818
import { api } from "viewer/singletons";
1919
import type { Segment } from "viewer/store";
20-
import {
21-
type SegmentHierarchyGroup,
22-
type SegmentHierarchyNode,
23-
getVolumeRequestUrl,
24-
} from "./segments_view_helper";
20+
import type { SegmentHierarchyGroup, SegmentHierarchyNode } from "./segments_view_helper";
2521

2622
const MODAL_ERROR_MESSAGE =
2723
"Segment statistics could not be fetched. Check the console for more details.";
@@ -104,12 +100,12 @@ export function SegmentStatisticsModal({
104100
const voxelSize = dataset.dataSource.scale;
105101
// Omit checking that all prerequisites for segment stats (such as a segment index) are
106102
// met right here because that should happen before opening the modal.
107-
const requestUrl = getVolumeRequestUrl(
103+
const storeInfoType = {
108104
dataset,
109105
annotation,
110-
visibleSegmentationLayer.tracingId,
111-
visibleSegmentationLayer,
112-
);
106+
tracingId: visibleSegmentationLayer.tracingId,
107+
segmentationLayerName: visibleSegmentationLayer.name,
108+
};
113109
const additionalCoordinates = useWkSelector((state) => state.flycam.additionalCoordinates);
114110
const hasAdditionalCoords = hasAdditionalCoordinates(additionalCoordinates);
115111
const additionalCoordinateStringForModal = getAdditionalCoordinatesAsString(
@@ -119,7 +115,6 @@ export function SegmentStatisticsModal({
119115
const segmentStatisticsObjects = useFetch(
120116
async () => {
121117
await api.tracing.save();
122-
if (requestUrl == null) return;
123118
const maybeVolumeTracing =
124119
tracingId != null ? getVolumeTracingById(annotation, tracingId) : null;
125120
const maybeGetMappingName = () => {
@@ -132,14 +127,14 @@ export function SegmentStatisticsModal({
132127
};
133128
const segmentStatisticsObjects = await Promise.all([
134129
getSegmentVolumes(
135-
requestUrl,
130+
storeInfoType,
136131
layersFinestMag,
137132
segments.map((segment) => segment.id),
138133
additionalCoordinates,
139134
maybeGetMappingName(),
140135
),
141136
getSegmentBoundingBoxes(
142-
requestUrl,
137+
storeInfoType,
143138
layersFinestMag,
144139
segments.map((segment) => segment.id),
145140
additionalCoordinates,

0 commit comments

Comments
 (0)