Skip to content

Commit 7453c97

Browse files
authored
Ome ngff volume annotation (#6242)
* add routes for zattrs for volume annotations * get data source for tracing to extract scale * add changelog entry * reformat
1 parent 77775e4 commit 7453c97

File tree

5 files changed

+56
-20
lines changed

5 files changed

+56
-20
lines changed

CHANGELOG.unreleased.md

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
2121
- Adding a New Volume Layer via the left border tab now gives the option to restrict resolutions for the new layer. [#6229](https://github.com/scalableminds/webknossos/pull/6229)
2222
- Added support for segment interpolation with depths > 2. Also, the feature was changed to work on an explicit trigger (either via the button in the toolbar or via the shortcut V). When triggering the interpolation, the current segment id is interpolated between the current slice and the least-recently annotated slice. [#6235](https://github.com/scalableminds/webknossos/pull/6235)
2323
- Added Route to get OME-NGFF Headers for a data layer (.zattrs file) following the corresponding [spec](https://ngff.openmicroscopy.org/latest/). [#6226](https://github.com/scalableminds/webknossos/pull/6226)
24+
- Added Route to get OME-NGFF Headers for Volume annotation. [#6242](https://github.com/scalableminds/webknossos/pull/6242)
2425

2526
### Changed
2627
- When creating a new annotation with a volume layer (without fallback) for a dataset which has an existing segmentation layer, the original segmentation layer is still listed (and viewable) in the left sidebar. Earlier versions simply hid the original segmentation layer. [#6186](https://github.com/scalableminds/webknossos/pull/6186)

webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/ZarrStreamingController.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,9 @@ class ZarrStreamingController @Inject()(
174174
"dataSource.notFound") ~> 404
175175
existingMags = dataLayer.resolutions
176176

177-
omeNgffHeader = OmeNgffHeader.createFromDataLayerName(dataLayerName,
178-
dataSourceScale = dataSource.scale,
179-
mags = existingMags)
177+
omeNgffHeader = OmeNgffHeader.fromDataLayerName(dataLayerName,
178+
dataSourceScale = dataSource.scale,
179+
mags = existingMags)
180180
} yield Ok(Json.toJson(omeNgffHeader))
181181
}
182182
}

webknossos-datastore/app/com/scalableminds/webknossos/datastore/jzarr/OmeNgffHeader.scala

+1-3
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@ object OmeNgffOneHeader {
4040
case class OmeNgffHeader(multiscales: List[OmeNgffOneHeader])
4141

4242
object OmeNgffHeader {
43-
def createFromDataLayerName(dataLayerName: String,
44-
dataSourceScale: Vec3Double,
45-
mags: List[Vec3Int]): OmeNgffHeader = {
43+
def fromDataLayerName(dataLayerName: String, dataSourceScale: Vec3Double, mags: List[Vec3Int]): OmeNgffHeader = {
4644
val datasets = mags.map(
4745
mag =>
4846
OmeNgffDataset(

webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingController.scala

+50-14
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@ import java.io.File
77
import java.nio.{ByteBuffer, ByteOrder}
88
import akka.stream.scaladsl.Source
99
import com.google.inject.Inject
10-
import com.scalableminds.util.geometry.{BoundingBox, Vec3Int}
10+
import com.scalableminds.util.geometry.{BoundingBox, Vec3Double, Vec3Int}
1111
import com.scalableminds.util.tools.ExtendedTypes.ExtendedString
1212
import com.scalableminds.util.tools.Fox
13-
import com.scalableminds.webknossos.datastore.models.datasource.{DataLayer, ElementClass}
13+
import com.scalableminds.webknossos.datastore.models.datasource.{DataLayer, DataSourceId, ElementClass}
1414
import com.scalableminds.webknossos.datastore.models.{WebKnossosDataRequest, WebKnossosIsosurfaceRequest}
1515
import com.scalableminds.webknossos.datastore.services.UserAccessRequest
1616
import com.scalableminds.webknossos.datastore.VolumeTracing.{VolumeTracing, VolumeTracingOpt, VolumeTracings}
1717
import com.scalableminds.webknossos.datastore.dataformats.zarr.ZarrCoordinatesParser
1818
import com.scalableminds.webknossos.datastore.helpers.ProtoGeometryImplicits
19+
import com.scalableminds.webknossos.datastore.jzarr.{ArrayOrder, OmeNgffHeader, ZarrHeader}
1920
import com.scalableminds.webknossos.datastore.rpc.RPC
2021
import com.scalableminds.webknossos.tracingstore.slacknotification.TSSlackNotificationService
2122
import com.scalableminds.webknossos.tracingstore.tracings.volume.{ResolutionRestrictions, VolumeTracingService}
@@ -270,24 +271,36 @@ class VolumeTracingController @Inject()(
270271
(channels, dtype) = ElementClass.toChannelAndZarrString(tracing.elementClass)
271272
// data request method always decompresses before sending
272273
compressor = None
274+
275+
shape = Array(
276+
channels,
277+
// Zarr can't handle data sets that don't start at 0, so we extend shape to include "true" coords
278+
(tracing.boundingBox.width + tracing.boundingBox.topLeft.x) / magParsed.x,
279+
(tracing.boundingBox.height + tracing.boundingBox.topLeft.y) / magParsed.y,
280+
(tracing.boundingBox.depth + tracing.boundingBox.topLeft.z) / magParsed.z
281+
)
282+
283+
chunks = Array(channels, cubeLength, cubeLength, cubeLength)
284+
285+
zarrHeader = ZarrHeader(zarr_format = 2,
286+
shape = shape,
287+
chunks = chunks,
288+
compressor = compressor,
289+
dtype = dtype,
290+
order = ArrayOrder.F)
273291
} yield
274292
Ok(
293+
// Json.toJson doesn't work on zarrHeader at the moment, because it doesn't write None values in Options
275294
Json.obj(
276-
"dtype" -> dtype,
295+
"dtype" -> zarrHeader.dtype,
277296
"fill_value" -> 0,
278-
"zarr_format" -> 2,
279-
"order" -> "F",
280-
"chunks" -> List(channels, cubeLength, cubeLength, cubeLength),
297+
"zarr_format" -> zarrHeader.zarr_format,
298+
"order" -> zarrHeader.order,
299+
"chunks" -> zarrHeader.chunks,
281300
"compressor" -> compressor,
282301
"filters" -> None,
283-
"shape" -> List(
284-
channels,
285-
// Zarr can't handle data sets that don't start at 0, so we extend shape to include "true" coords
286-
(tracing.boundingBox.width + tracing.boundingBox.topLeft.x) / magParsed.x,
287-
(tracing.boundingBox.height + tracing.boundingBox.topLeft.y) / magParsed.y,
288-
(tracing.boundingBox.depth + tracing.boundingBox.topLeft.z) / magParsed.z
289-
),
290-
"dimension_seperator" -> "."
302+
"shape" -> zarrHeader.shape,
303+
"dimension_seperator" -> zarrHeader.dimension_separator
291304
))
292305
}
293306
}
@@ -298,6 +311,29 @@ class VolumeTracingController @Inject()(
298311
}
299312
}
300313

314+
/**
315+
* Handles a request for .zattrs file for a Volume Tracing via a HTTP GET.
316+
* Uses the OME-NGFF standard (see https://ngff.openmicroscopy.org/latest/)
317+
* Used by zarr-streaming.
318+
*/
319+
def zAttrs(
320+
token: Option[String],
321+
tracingId: String,
322+
): Action[AnyContent] = Action.async { implicit request =>
323+
accessTokenService.validateAccess(UserAccessRequest.readTracing(tracingId), urlOrHeaderToken(token, request)) {
324+
for {
325+
tracing <- tracingService.find(tracingId) ?~> Messages("tracing.notFound")
326+
327+
existingMags = tracing.resolutions.map(vec3IntFromProto)
328+
dataSource <- remoteWebKnossosClient.getDataSource(tracing.organizationName, tracing.dataSetName)
329+
330+
omeNgffHeader = OmeNgffHeader.fromDataLayerName(tracingId,
331+
dataSourceScale = dataSource.scale,
332+
mags = existingMags.toList)
333+
} yield Ok(Json.toJson(omeNgffHeader))
334+
}
335+
}
336+
301337
def rawZarrCube(token: Option[String], tracingId: String, mag: String, cxyz: String): Action[AnyContent] =
302338
Action.async { implicit request =>
303339
{

webknossos-tracingstore/conf/com.scalableminds.webknossos.tracingstore.routes

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ POST /volume/mergedFromContents @com.scalablemin
2727
# Zarr endpoints for volume annotations
2828
GET /volume/zarr/:tracingId @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.volumeTracingFolderContent(token: Option[String], tracingId: String)
2929
GET /volume/zarr/:tracingId/.zgroup @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.zGroup(token: Option[String], tracingId: String)
30+
GET /volume/zarr/:tracingId/.zattrs @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.zAttrs(token: Option[String], tracingId: String)
3031
GET /volume/zarr/:tracingId/:mag @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.volumeTracingMagFolderContent(token: Option[String], tracingId: String, mag: String)
3132
GET /volume/zarr/:tracingId/:mag/.zarray @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.zArray(token: Option[String], tracingId: String, mag: String)
3233
GET /volume/zarr/:tracingId/:mag/:cxyz @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.rawZarrCube(token: Option[String], tracingId: String, mag: String, cxyz: String)

0 commit comments

Comments
 (0)