Skip to content

Commit

Permalink
OME-Zarr NGFF v0.5 dataset exploration (#8122)
Browse files Browse the repository at this point in the history
  • Loading branch information
frcroth authored Oct 23, 2024
1 parent 60f77ac commit bedb16e
Show file tree
Hide file tree
Showing 19 changed files with 649 additions and 370 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- It is now possible to search for unnamed segments with the full default name instead of only their id. [#8133](https://github.com/scalableminds/webknossos/pull/8133)
- Increased loading speed for precomputed meshes. [#8110](https://github.com/scalableminds/webknossos/pull/8110)
- Unified wording in UI and code: “Magnification”/“mag” is now used in place of “Resolution“ most of the time, compare [https://docs.webknossos.org/webknossos/terminology.html](terminology document). [#8111](https://github.com/scalableminds/webknossos/pull/8111)
- Added support for adding remote OME-Zarr NGFF version 0.5 datasets. [#8122](https://github.com/scalableminds/webknossos/pull/8122)

### Changed
- Some mesh-related actions were disabled in proofreading-mode when using meshfiles that were created for a mapping rather than an oversegmentation. [#8091](https://github.com/scalableminds/webknossos/pull/8091)
Expand Down
43 changes: 43 additions & 0 deletions docs/data/zarr.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,49 @@ WEBKNOSSOS expects the following file structure for OME-Zarr (v0.4) datasets:

See [OME-Zarr 0.4 spec](https://ngff.openmicroscopy.org/latest/index.html#image-layout) for details.

### Zarr Folder Structure (v0.5)

For OME-Zarr (v0.5) datasets, the structure is slightly different (See [OME-Zarr 0.5 spec](https://ngff--242.org.readthedocs.build/latest/index.html#image-layout)):

```
├── 123.zarr # One OME-Zarr image (id=123).
│ ...
└── 456.zarr # Another OME-Zarr image (id=456).
├── zarr.json # Each image is a Zarr group of other groups and arrays.
│ # Group level attributes are stored in the zarr.json file and include
│ # "multiscales" and "omero" (see below).
├── 0 # Each multiscale level is stored as a separate Zarr array,
│ ... # which is a folder containing chunk files which compose the array.
├── n # The name of the array is arbitrary with the ordering defined by
│ │ # by the "multiscales" metadata, but is often a sequence starting at 0.
│ │
│ ├── zarr.json # All image arrays must be up to 5-dimensional
│ │ # with the axis of type time before type channel, before spatial axes.
│ │
│ └─ ... # Chunks are stored conforming to the Zarr array specification and
│ # metadata as specified in the array’s zarr.json.
└── labels
├── zarr.json # The labels group is a container which holds a list of labels to make the objects easily discoverable
│ # All labels will be listed in zarr.json e.g. { "labels": [ "original/0" ] }
│ # Each dimension of the label should be either the same as the
│ # corresponding dimension of the image, or 1 if that dimension of the label
│ # is irrelevant.
└── original # Intermediate folders are permitted but not necessary and currently contain no extra metadata.
└── 0 # Multiscale, labeled image. The name is unimportant but is registered in the "labels" group above.
├── zarr.json # Zarr Group which is both a multiscaled image as well as a labeled image.
│ # Metadata of the related image and as well as display information under the "image-label" key.
├── 0 # Each multiscale level is stored as a separate Zarr array, as above, but only integer values
└── ... # are supported.
```

## Conversion to Zarr

You can easily convert image stacks manually with the [WEBKNOSSOS CLI](https://docs.webknossos.org/cli).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ case class NgffMultiscalesItemV0_5(

object NgffMultiscalesItemV0_5 {
implicit val jsonFormat: OFormat[NgffMultiscalesItemV0_5] = Json.format[NgffMultiscalesItemV0_5]

def asV0_4(multiscalesItemV0_5: NgffMultiscalesItemV0_5): NgffMultiscalesItem =
NgffMultiscalesItem(
version = "0.5",
name = multiscalesItemV0_5.name,
axes = multiscalesItemV0_5.axes,
datasets = multiscalesItemV0_5.datasets
)
}

case class NgffMetadataV0_5(version: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import com.scalableminds.webknossos.datastore.helpers.JsonImplicits
import com.scalableminds.webknossos.datastore.models.datasource.{AdditionalAxis, DataLayer}
import net.liftweb.common.Box.tryo
import net.liftweb.common.{Box, Full}
import play.api.libs.json.{Format, JsArray, JsResult, JsString, JsSuccess, JsValue, Json, OFormat}
import play.api.libs.json.{Format, JsArray, JsObject, JsResult, JsString, JsSuccess, JsValue, Json, OFormat}

import java.nio.ByteOrder

Expand All @@ -29,7 +29,7 @@ case class Zarr3ArrayHeader(
chunk_grid: Either[ChunkGridSpecification, ExtensionChunkGridSpecification],
chunk_key_encoding: ChunkKeyEncoding,
fill_value: Either[String, Number], // Boolean not supported
attributes: Option[Map[String, String]],
attributes: Option[JsObject],
codecs: Seq[CodecConfiguration],
storage_transformers: Option[Seq[StorageTransformerSpecification]],
dimension_names: Option[Array[String]]
Expand Down Expand Up @@ -173,7 +173,7 @@ object Zarr3ArrayHeader extends JsonImplicits {
chunk_grid <- (json \ "chunk_grid").validate[ChunkGridSpecification]
chunk_key_encoding <- (json \ "chunk_key_encoding").validate[ChunkKeyEncoding]
fill_value <- (json \ "fill_value").validate[Either[String, Number]]
attributes = (json \ "attributes").validate[Map[String, String]].asOpt
attributes = (json \ "attributes").validate[JsObject].asOpt
codecsJsValue <- (json \ "codecs").validate[JsValue]
codecs = readCodecs(codecsJsValue)
dimension_names <- (json \ "dimension_names").validate[Array[String]].orElse(JsSuccess(Array[String]()))
Expand Down Expand Up @@ -242,7 +242,7 @@ object Zarr3ArrayHeader extends JsonImplicits {
ChunkGridConfiguration(Array(1, 1, 1))))), // Extension not supported for now
"chunk_key_encoding" -> zarrArrayHeader.chunk_key_encoding,
"fill_value" -> zarrArrayHeader.fill_value,
"attributes" -> Json.toJsFieldJsValueWrapper(zarrArrayHeader.attributes.getOrElse(Map.empty)),
"attributes" -> Json.toJsFieldJsValueWrapper(zarrArrayHeader.attributes.getOrElse(JsObject.empty)),
"codecs" -> zarrArrayHeader.codecs.map { codec: CodecConfiguration =>
val configurationJson = if (codec.includeConfiguration) Json.obj("configuration" -> codec) else Json.obj()
Json.obj("name" -> codec.name) ++ configurationJson
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ package com.scalableminds.webknossos.datastore.datavault
import com.aayushatharva.brotli4j.Brotli4jLoader
import com.aayushatharva.brotli4j.decoder.BrotliInputStream
import com.scalableminds.util.io.ZipIO
import com.scalableminds.util.tools.Fox
import com.scalableminds.util.tools.{Fox, JsonHelper}
import com.scalableminds.util.tools.Fox.box2Fox
import com.typesafe.scalalogging.LazyLogging
import net.liftweb.common.Box.tryo
import org.apache.commons.lang3.builder.HashCodeBuilder
import play.api.libs.json.Reads

import java.io.{ByteArrayInputStream, ByteArrayOutputStream, IOException}
import java.net.URI
import java.nio.charset.StandardCharsets
import scala.collection.immutable.NumericRange
import scala.concurrent.ExecutionContext

Expand Down Expand Up @@ -90,4 +92,11 @@ class VaultPath(uri: URI, dataVault: DataVault) extends LazyLogging {

override def hashCode(): Int =
new HashCodeBuilder(17, 31).append(uri.toString).append(dataVault).toHashCode

def parseAsJson[T: Reads](implicit ec: ExecutionContext): Fox[T] =
for {
fileBytes <- this.readBytes().toFox
fileAsString <- tryo(new String(fileBytes, StandardCharsets.UTF_8)).toFox
parsed <- JsonHelper.parseAndValidateJson[T](fileAsString)
} yield parsed
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ class ExploreLocalLayerService @Inject()(dataVaultService: DataVaultService)
for {
_ <- Fox.successful(())
explored = Seq(
exploreLocalNgffArray(path, dataSourceId),
exploreLocalNgffV0_4Array(path, dataSourceId),
exploreLocalNgffV0_5Array(path, dataSourceId),
exploreLocalZarrArray(path, dataSourceId, layerDirectory),
exploreLocalNeuroglancerPrecomputed(path, dataSourceId, layerDirectory),
exploreLocalN5Multiscales(path, dataSourceId, layerDirectory),
Expand All @@ -57,11 +58,18 @@ class ExploreLocalLayerService @Inject()(dataVaultService: DataVaultService)
dataSource = new DataSourceWithMagLocators(dataSourceId, relativeLayers, voxelSize)
} yield dataSource

private def exploreLocalNgffArray(path: Path, dataSourceId: DataSourceId)(
private def exploreLocalNgffV0_4Array(path: Path, dataSourceId: DataSourceId)(
implicit ec: ExecutionContext): Fox[DataSourceWithMagLocators] =
exploreLocalLayer(
layers => layers.map(selectLastTwoDirectories),
new NgffExplorer
new NgffV0_4Explorer
)(path, dataSourceId, "")

private def exploreLocalNgffV0_5Array(path: Path, dataSourceId: DataSourceId)(
implicit ec: ExecutionContext): Fox[DataSourceWithMagLocators] =
exploreLocalLayer(
layers => layers.map(selectLastTwoDirectories),
new NgffV0_5Explorer
)(path, dataSourceId, "")

private def exploreLocalNeuroglancerPrecomputed(path: Path, dataSourceId: DataSourceId, layerDirectory: String)(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ class ExploreRemoteLayerService @Inject()(dataVaultService: DataVaultService,
credentialId,
List(
// Explorers are ordered to prioritize the explorer reading meta information over raw Zarr, N5, ... data.
new NgffExplorer,
new NgffV0_4Explorer,
new NgffV0_5Explorer,
new WebknossosZarrExplorer,
new Zarr3ArrayExplorer,
new ZarrArrayExplorer(Vec3Int.ones),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class N5ArrayExplorer(implicit val ec: ExecutionContext) extends RemoteLayerExpl
for {
headerPath <- Fox.successful(remotePath / N5Header.FILENAME_ATTRIBUTES_JSON)
name = guessNameFromPath(remotePath)
n5Header <- parseJsonFromPath[N5Header](headerPath) ?~> s"failed to read n5 header at $headerPath"
n5Header <- headerPath.parseAsJson[N5Header] ?~> s"failed to read n5 header at $headerPath"
elementClass <- n5Header.elementClass ?~> "failed to read element class from n5 header"
guessedAxisOrder = AxisOrder.asZyxFromRank(n5Header.rank)
boundingBox <- n5Header.boundingBox(guessedAxisOrder) ?~> "failed to read bounding box from zarr header. Make sure data is in (T/C)ZYX format"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class N5MultiscalesExplorer(implicit val ec: ExecutionContext) extends RemoteLay
override def explore(remotePath: VaultPath, credentialId: Option[String]): Fox[List[(N5Layer, VoxelSize)]] =
for {
metadataPath <- Fox.successful(remotePath / N5Metadata.FILENAME_ATTRIBUTES_JSON)
n5Metadata <- parseJsonFromPath[N5Metadata](metadataPath) ?~> s"Failed to read N5 header at $metadataPath"
n5Metadata <- metadataPath.parseAsJson[N5Metadata] ?~> s"Failed to read N5 header at $metadataPath"
layers <- Fox.serialCombined(n5Metadata.multiscales)(layerFromN5MultiscalesItem(_, remotePath, credentialId))
} yield layers

Expand Down Expand Up @@ -105,7 +105,7 @@ class N5MultiscalesExplorer(implicit val ec: ExecutionContext) extends RemoteLay
mag <- magFromTransform(voxelSize, n5Dataset.transform) ?~> "Could not extract mag from transforms"
magPath = layerPath / n5Dataset.path
headerPath = magPath / N5Header.FILENAME_ATTRIBUTES_JSON
n5Header <- parseJsonFromPath[N5Header](headerPath) ?~> s"failed to read n5 header at $headerPath"
n5Header <- headerPath.parseAsJson[N5Header] ?~> s"failed to read n5 header at $headerPath"
elementClass <- n5Header.elementClass ?~> s"failed to read element class from n5 header at $headerPath"
boundingBox <- n5Header.boundingBox(axisOrder) ?~> s"failed to read bounding box from n5 header at $headerPath"
} yield
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,11 @@ class NeuroglancerUriExplorer(dataVaultService: DataVaultService)(implicit val e
case "precomputed" => new PrecomputedExplorer().explore(remotePath, None)
case "zarr" | "zarr2" =>
Fox.firstSuccess(
Seq(new NgffExplorer().explore(remotePath, None),
new ZarrArrayExplorer(Vec3Int.ones).explore(remotePath, None)))
Seq(
new NgffV0_4Explorer().explore(remotePath, None),
new NgffV0_5Explorer().explore(remotePath, None),
new ZarrArrayExplorer(Vec3Int.ones).explore(remotePath, None)
))
case "zarr3" => new Zarr3ArrayExplorer().explore(remotePath, None)
case _ => Fox.failure(f"Can not explore layer of $layerType type")
}
Expand Down
Loading

0 comments on commit bedb16e

Please sign in to comment.