Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
fc75c5e
Add new camera perspectives for animations (#8909)
hotzenklotz Sep 10, 2025
32dae79
Merge branch 'master' into adhoc-datastore-segment-index
fm3 Sep 15, 2025
ad0fb3d
Merge branch 'master' into adhoc-datastore-segment-index
fm3 Sep 23, 2025
c7127ba
WIP: use segment index for static segmentation layers with segment index
knollengewaechs Sep 26, 2025
bc96b43
memoize hasSegmentIndexInDataStore
knollengewaechs Sep 26, 2025
66089d0
Merge branch 'master' into adhoc-datastore-segment-index
knollengewaechs Sep 26, 2025
46fa4df
apply coderabbit review
knollengewaechs Sep 26, 2025
5e58f24
changelog
knollengewaechs Sep 26, 2025
2cfdbb3
use layer.name for static segmentation layers
knollengewaechs Sep 29, 2025
a55cfb8
Also use segment index in datastore fullMesh.stl route, if available
fm3 Sep 29, 2025
458a36c
Merge branch 'master' into adhoc-datastore-segment-index
fm3 Sep 29, 2025
be864da
changelog
fm3 Sep 29, 2025
c5f4b80
WIP not tested: address review 1/2
knollengewaechs Sep 30, 2025
c97b9df
WIP: move function to get request url to rest_api
knollengewaechs Oct 2, 2025
82821e3
WIP: remove building requestUrl from ad_hoc_mesh_saga
knollengewaechs Oct 2, 2025
3786af5
fix adhoc mesh loading
knollengewaechs Oct 2, 2025
76091f9
move getURL function to adapter and rename some vars
knollengewaechs Oct 2, 2025
30f80f6
add yield* to function call
knollengewaechs Oct 2, 2025
75d920d
Merge branch 'master' into adhoc-datastore-segment-index
knollengewaechs Oct 2, 2025
f0911f7
fix cyclic dependency
knollengewaechs Oct 2, 2025
c5fd2be
ensure visible segmentation layer is not null
knollengewaechs Oct 2, 2025
a459350
rename props of LayerSourceInfo
knollengewaechs Oct 7, 2025
189617f
fix usage of layerSourceInfo
knollengewaechs Oct 7, 2025
09c9074
merge master
knollengewaechs Oct 7, 2025
1765442
lint
knollengewaechs Oct 8, 2025
086162f
Merge branch 'master' into adhoc-datastore-segment-index
knollengewaechs Oct 8, 2025
399241e
Merge branch 'master' into adhoc-datastore-segment-index
knollengewaechs Oct 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions frontend/javascripts/admin/rest_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,10 @@ export function hasSegmentIndexInDataStore(
);
}

export const hasSegmentIndexInDataStoreCached = _.memoize(hasSegmentIndexInDataStore, (...args) =>
args.join("::"),
);

export function getSegmentVolumes(
requestUrl: string,
mag: Vector3,
Expand Down Expand Up @@ -1927,23 +1931,24 @@ export function computeAdHocMesh(
}

export function getBucketPositionsForAdHocMesh(
tracingStoreUrl: string,
tracingId: string,
requestUrl: string,
segmentId: number,
cubeSize: Vector3,
mag: Vector3,
additionalCoordinates: AdditionalCoordinate[] | null | undefined,
mappingName: string | null | undefined,
): Promise<Vector3[]> {
return doWithToken(async (token) => {
const params = new URLSearchParams();
params.set("token", token);
const positions = await Request.sendJSONReceiveJSON(
`${tracingStoreUrl}/tracings/volume/${tracingId}/segmentIndex/${segmentId}?${params}`,
`${requestUrl}/segmentIndex/${segmentId}?${params}`,
{
data: {
cubeSize,
mag,
additionalCoordinates,
mappingName,
},
method: "POST",
},
Expand Down
36 changes: 29 additions & 7 deletions frontend/javascripts/viewer/model/sagas/meshes/ad_hoc_mesh_saga.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
computeAdHocMesh,
getBucketPositionsForAdHocMesh,
hasSegmentIndexInDataStoreCached,
sendAnalyticsEvent,
} from "admin/rest_api";
import ThreeDMap from "libs/ThreeDMap";
Expand Down Expand Up @@ -321,6 +322,9 @@ function* loadFullAdHocMesh(

const cubeSize = marchingCubeSizeInTargetMag();
const tracingStoreHost = yield* select((state) => state.annotation.tracingStore.url);
const dataset = yield* select((state) => state.dataset);
const datasetId = dataset.id;
const dataStoreHost = dataset.dataStore.url;
const mag = magInfo.getMagByIndexOrThrow(zoomStep);

const volumeTracing = yield* select((state) => getActiveSegmentationTracing(state));
Expand All @@ -335,21 +339,39 @@ function* loadFullAdHocMesh(
useDataStore = false;
}

let isStaticSegmentationLayerWithSegmentIndex = false;

if (volumeTracing == null) {
isStaticSegmentationLayerWithSegmentIndex = yield* call(
hasSegmentIndexInDataStoreCached,
dataset.dataStore.url,
dataset.id,
layer.name,
);
}
// Segment stats can only be used for volume tracings that have a segment index
// and that don't have editable mappings.
const usePositionsFromSegmentIndex =
const usePositionsFromSegmentIndexForVolumeTracing =
volumeTracing?.hasSegmentIndex &&
!volumeTracing.hasEditableMapping &&
visibleSegmentationLayer?.tracingId != null;

const usePositionsFromSegmentIndex =
usePositionsFromSegmentIndexForVolumeTracing || isStaticSegmentationLayerWithSegmentIndex;

const dataStoreUrl = `${dataStoreHost}/data/datasets/${datasetId}/layers/${layer.name}`;
const tracingStoreUrl = `${tracingStoreHost}/tracings/volume/${layer.name}`;
const requestUrl = useDataStore ? dataStoreUrl : tracingStoreUrl;

let positionsToRequest = usePositionsFromSegmentIndex
? yield* getChunkPositionsFromSegmentIndex(
tracingStoreHost,
layer,
requestUrl,
segmentId,
cubeSize,
mag,
clippedPosition,
additionalCoordinates,
mappingName,
)
: [clippedPosition];

Expand Down Expand Up @@ -390,22 +412,22 @@ function* loadFullAdHocMesh(
}

function* getChunkPositionsFromSegmentIndex(
tracingStoreHost: string,
layer: DataLayer,
requestUrl: string,
segmentId: number,
cubeSize: Vector3,
mag: Vector3,
clippedPosition: Vector3,
additionalCoordinates: AdditionalCoordinate[] | null | undefined,
mappingName: string | null | undefined,
) {
const targetMagPositions = yield* call(
getBucketPositionsForAdHocMesh,
tracingStoreHost,
layer.name,
requestUrl,
segmentId,
cubeSize,
mag,
additionalCoordinates,
mappingName,
);
const mag1Positions = targetMagPositions.map((pos) => V3.scale3(pos, mag));
return sortByDistanceTo(mag1Positions, clippedPosition) as Vector3[];
Expand Down
2 changes: 2 additions & 0 deletions unreleased_changes/8922.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
### Added
- The segment index file is now used while rendering ad-hoc meshes for static segmentation layers, e.g. when viewing a dataset.
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ class BinaryDataController @Inject()(
adHocMeshRequest = AdHocMeshRequest(
Some(dataSource.id),
segmentationLayer,
request.body.cuboid(dataLayer),
request.body.cuboid,
request.body.segmentId,
request.body.voxelSizeFactorInUnit,
tokenContextForRequest(request),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ case class WebknossosAdHocMeshRequest(
additionalCoordinates: Option[Seq[AdditionalCoordinate]] = None,
findNeighbors: Boolean = true
) {
def cuboid(dataLayer: DataLayer): Cuboid =
def cuboid: Cuboid =
Cuboid(VoxelPosition(position.x, position.y, position.z, mag), cubeSize.x, cubeSize.y, cubeSize.z)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.scalableminds.webknossos.datastore.services._
import com.typesafe.scalalogging.LazyLogging
import com.scalableminds.util.tools.Box.tryo
import com.scalableminds.webknossos.datastore.services.mapping.MappingService
import com.scalableminds.webknossos.datastore.services.segmentindex.SegmentIndexFileService
import play.api.i18n.MessagesProvider
import play.api.libs.json.{Json, OFormat}

Expand Down Expand Up @@ -40,6 +41,7 @@ class DSFullMeshService @Inject()(meshFileService: MeshFileService,
val dsRemoteTracingstoreClient: DSRemoteTracingstoreClient,
mappingService: MappingService,
config: DataStoreConfig,
segmentIndexFileService: SegmentIndexFileService,
adHocMeshServiceHolder: AdHocMeshServiceHolder)
extends LazyLogging
with FullMeshHelper
Expand All @@ -66,26 +68,85 @@ class DSFullMeshService @Inject()(meshFileService: MeshFileService,
fullMeshRequest: FullMeshRequest)(implicit ec: ExecutionContext, tc: TokenContext): Fox[Array[Byte]] =
for {
mag <- fullMeshRequest.mag.toFox ?~> "mag.neededForAdHoc"
seedPosition <- fullMeshRequest.seedPosition.toFox ?~> "seedPosition.neededForAdHoc"
segmentationLayer <- tryo(dataLayer.asInstanceOf[SegmentationLayer]).toFox ?~> "dataLayer.mustBeSegmentation"
hasSegmentIndexFile = segmentationLayer.attachments.flatMap(_.segmentIndex).isDefined
before = Instant.now
verticesForChunks <- getAllAdHocChunks(dataSource,
segmentationLayer,
fullMeshRequest,
VoxelPosition(seedPosition.x, seedPosition.y, seedPosition.z, mag),
adHocChunkSize)
verticesForChunks <- if (hasSegmentIndexFile)
getAllAdHocChunksWithSegmentIndex(dataSource, segmentationLayer, fullMeshRequest, mag)
else {
for {
seedPosition <- fullMeshRequest.seedPosition.toFox ?~> "seedPosition.neededForAdHocWithoutSegmentIndex"
chunks <- getAllAdHocChunksWithNeighborLogic(
dataSource,
segmentationLayer,
fullMeshRequest,
VoxelPosition(seedPosition.x, seedPosition.y, seedPosition.z, mag),
adHocChunkSize)
} yield chunks
}

encoded = verticesForChunks.map(adHocMeshToStl)
array = combineEncodedChunksToStl(encoded)
_ = logMeshingDuration(before, "ad-hoc meshing", array.length)
} yield array

private def getAllAdHocChunks(
private def getAllAdHocChunksWithSegmentIndex(
dataSource: UsableDataSource,
segmentationLayer: SegmentationLayer,
fullMeshRequest: FullMeshRequest,
topLeft: VoxelPosition,
chunkSize: Vec3Int,
visited: collection.mutable.Set[VoxelPosition] = collection.mutable.Set[VoxelPosition]())(
mag: Vec3Int,
)(implicit ec: ExecutionContext, tc: TokenContext): Fox[List[Array[Float]]] =
for {
segmentIndexFileKey <- segmentIndexFileService.lookUpSegmentIndexFileKey(dataSource.id, segmentationLayer)
segmentIds <- segmentIdsForAgglomerateIdIfNeeded(
dataSource.id,
segmentationLayer,
fullMeshRequest.mappingName,
fullMeshRequest.editableMappingTracingId,
fullMeshRequest.segmentId,
mappingNameForMeshFile = None,
omitMissing = false
)
topLeftsNested: Seq[Array[Vec3Int]] <- Fox.serialCombined(segmentIds)(sId =>
segmentIndexFileService.readSegmentIndex(segmentIndexFileKey, sId))
topLefts: Array[Vec3Int] = topLeftsNested.toArray.flatten
targetMagPositions = segmentIndexFileService.topLeftsToDistinctTargetMagBucketPositions(topLefts, mag)
vertexChunksWithNeighbors: List[(Array[Float], List[Int])] <- Fox.serialCombined(targetMagPositions) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought: We are using serial combined here and got the feedback that adhoc mesh generation is rather slow. So should we maybe parallelize these requests?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that’s something to keep in mind. But for the moment, I’d like to stick with the policy that answering a single request should not use all the parallelization it can get, especially for compute-heavy stuff like mesh generation. Otherwise, a single request could busy the server and other user’s (simple) requests might not get answered quickly. But that’s certainly something to validate and consider.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we open an issue tagged with discussion to not forget this?

targetMagPosition =>
val adHocMeshRequest = AdHocMeshRequest(
Some(dataSource.id),
segmentationLayer,
Cuboid(
VoxelPosition(
targetMagPosition.x * mag.x * DataLayer.bucketLength,
targetMagPosition.y * mag.y * DataLayer.bucketLength,
targetMagPosition.z * mag.z * DataLayer.bucketLength,
mag
),
DataLayer.bucketLength + 1,
DataLayer.bucketLength + 1,
DataLayer.bucketLength + 1
),
fullMeshRequest.segmentId,
dataSource.scale.factor,
tc,
fullMeshRequest.mappingName,
fullMeshRequest.mappingType,
fullMeshRequest.additionalCoordinates,
findNeighbors = false,
)
adHocMeshService.requestAdHocMeshViaActor(adHocMeshRequest)
}
allVertices = vertexChunksWithNeighbors.map(_._1)
} yield allVertices

private def getAllAdHocChunksWithNeighborLogic(dataSource: UsableDataSource,
segmentationLayer: SegmentationLayer,
fullMeshRequest: FullMeshRequest,
topLeft: VoxelPosition,
chunkSize: Vec3Int,
visited: collection.mutable.Set[VoxelPosition] =
collection.mutable.Set[VoxelPosition]())(
implicit ec: ExecutionContext,
tc: TokenContext): Fox[List[Array[Float]]] = {
val adHocMeshRequest = AdHocMeshRequest(
Expand All @@ -105,7 +166,7 @@ class DSFullMeshService @Inject()(meshFileService: MeshFileService,
nextPositions: List[VoxelPosition] = generateNextTopLeftsFromNeighbors(topLeft, neighbors, chunkSize, visited)
_ = visited ++= nextPositions
neighborVerticesNested <- Fox.serialCombined(nextPositions) { position: VoxelPosition =>
getAllAdHocChunks(dataSource, segmentationLayer, fullMeshRequest, position, chunkSize, visited)
getAllAdHocChunksWithNeighborLogic(dataSource, segmentationLayer, fullMeshRequest, position, chunkSize, visited)
}
allVertices: List[Array[Float]] = vertices +: neighborVerticesNested.flatten
} yield allVertices
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ class EditableMappingService @Inject()(
val adHocMeshRequest = AdHocMeshRequest(
dataSourceId = None,
dataLayer = editableMappingLayer,
cuboid = request.cuboid(editableMappingLayer),
cuboid = request.cuboid,
segmentId = request.segmentId,
voxelSizeFactor = request.voxelSizeFactorInUnit,
tokenContext = tc,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ class VolumeTracingService @Inject()(
adHocMeshRequest = AdHocMeshRequest(
None,
volumeLayer,
request.cuboid(volumeLayer),
request.cuboid,
request.segmentId,
request.voxelSizeFactorInUnit,
tc,
Expand Down