Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

N5 support #6466

Merged
merged 19 commits into from
Sep 22, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Added a context menu option to separate an agglomerate skeleton using Min-Cut. Activate the Proofreading tool, select the source node and open the context menu by right-clicking on the target node which you would like to separate through Min-Cut. [#6361](https://github.com/scalableminds/webknossos/pull/6361)
- Added a "clear" button to reset skeletons/meshes after successful mergers/split. [#6459](https://github.com/scalableminds/webknossos/pull/6459)
- The proofreading tool now supports merging and splitting (via min-cut) agglomerates by rightclicking a segment (and not a node). Note that there still has to be an active node so that both partners of the operation are defined. [#6464](https://github.com/scalableminds/webknossos/pull/6464)
- Added possibility to read N5 datasets. [#6466](https://github.com/scalableminds/webknossos/pull/6466)

### Changed

Expand Down
3 changes: 2 additions & 1 deletion app/models/binary/ExploreRemoteLayerService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import java.nio.file.{Files, Path}
import com.scalableminds.util.geometry.{BoundingBox, Vec3Double, Vec3Int}
import com.scalableminds.util.tools.{Fox, FoxImplicits, JsonHelper}
import com.scalableminds.webknossos.datastore.dataformats.zarr._
import com.scalableminds.webknossos.datastore.jzarr._
import com.scalableminds.webknossos.datastore.datareaders.AxisOrder
import com.scalableminds.webknossos.datastore.datareaders.jzarr._
import com.scalableminds.webknossos.datastore.models.datasource._
import com.scalableminds.webknossos.datastore.storage.FileSystemsHolder
import com.typesafe.scalalogging.LazyLogging
Expand Down
5 changes: 3 additions & 2 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ object Dependencies {
private val awsS3 = "com.amazonaws" % "aws-java-sdk-s3" % "1.12.288"
private val tika = "org.apache.tika" % "tika-core" % "1.5"
private val jackson = "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.12.7"

private val commonsCompress = "org.apache.commons" % "commons-compress" % "1.21"

private val sql = Seq(
"com.typesafe.slick" %% "slick" % "3.2.3",
Expand Down Expand Up @@ -100,7 +100,8 @@ object Dependencies {
awsS3,
tika,
jblosc,
scalajHttp
scalajHttp,
commonsCompress
)

val webknossosTracingstoreDependencies: Seq[ModuleID] = Seq(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import play.api.libs.json.{Format, Json}

abstract class ExtendedEnumeration extends Enumeration {
implicit val format: Format[Value] = Json.formatEnum(this)
def fromString(s: String): Option[Value] = values.find(_.toString == s)
def fromString(s: String): Option[Value] =
values.find(_.toString == s)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import com.scalableminds.webknossos.datastore.dataformats.zarr.{
ZarrMag,
ZarrSegmentationLayer
}
import com.scalableminds.webknossos.datastore.jzarr.{AxisOrder, OmeNgffGroupHeader, OmeNgffHeader, ZarrHeader}
import com.scalableminds.webknossos.datastore.datareaders.AxisOrder
import com.scalableminds.webknossos.datastore.datareaders.jzarr.{OmeNgffGroupHeader, OmeNgffHeader, ZarrHeader}
import com.scalableminds.webknossos.datastore.models.VoxelPosition
import com.scalableminds.webknossos.datastore.models.annotation.AnnotationLayerType
import com.scalableminds.webknossos.datastore.models.datasource._
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.scalableminds.webknossos.datastore.dataformats

import com.scalableminds.util.tools.{Fox, FoxImplicits}
import com.scalableminds.webknossos.datastore.dataformats.zarr.RemoteSourceDescriptor
import com.scalableminds.webknossos.datastore.models.BucketPosition
import com.scalableminds.webknossos.datastore.models.requests.DataReadInstruction
import com.scalableminds.webknossos.datastore.storage.DataCubeCache
import com.scalableminds.webknossos.datastore.storage.{DataCubeCache, FileSystemsHolder}
import com.typesafe.scalalogging.LazyLogging
import net.liftweb.common.{Box, Empty}

import java.nio.file.{FileSystem, Path}
import scala.concurrent.ExecutionContext

trait BucketProvider extends FoxImplicits with LazyLogging {
Expand Down Expand Up @@ -38,4 +40,20 @@ trait BucketProvider extends FoxImplicits with LazyLogging {
def bucketStream(version: Option[Long] = None): Iterator[(BucketPosition, Array[Byte])] =
Iterator.empty

protected def remotePathFrom(remoteSource: RemoteSourceDescriptor): Option[Path] =
FileSystemsHolder.getOrCreate(remoteSource).map { fileSystem: FileSystem =>
fileSystem.getPath(remoteSource.remotePath)
}

protected def localPathFrom(readInstruction: DataReadInstruction, relativeMagPath: String): Option[Path] = {
val magPath = readInstruction.baseDir
.resolve(readInstruction.dataSource.id.team)
.resolve(readInstruction.dataSource.id.name)
.resolve(readInstruction.dataLayer.name)
.resolve(relativeMagPath)
if (magPath.toFile.exists()) {
Some(magPath)
} else None
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.scalableminds.webknossos.datastore.dataformats.n5

import com.scalableminds.util.geometry.Vec3Int
import com.scalableminds.util.requestlogging.RateLimitedErrorLogging
import com.scalableminds.util.tools.Fox
import com.scalableminds.webknossos.datastore.dataformats.zarr.{N5Layer, N5Mag}
import com.scalableminds.webknossos.datastore.dataformats.{BucketProvider, DataCubeHandle}
import com.scalableminds.webknossos.datastore.models.BucketPosition
import com.scalableminds.webknossos.datastore.models.requests.DataReadInstruction
import com.scalableminds.webknossos.datastore.datareaders.n5.N5Array
import com.typesafe.scalalogging.LazyLogging
import net.liftweb.common.{Box, Empty, Failure, Full}
import net.liftweb.util.Helpers.tryo

import java.nio.file.Path
import scala.concurrent.ExecutionContext

class N5CubeHandle(n5Array: N5Array) extends DataCubeHandle with LazyLogging with RateLimitedErrorLogging {

def cutOutBucket(bucket: BucketPosition)(implicit ec: ExecutionContext): Fox[Array[Byte]] = {
val shape = Vec3Int.full(bucket.bucketLength)
val offset = Vec3Int(bucket.voxelXInMag, bucket.voxelYInMag, bucket.voxelZInMag)
n5Array.readBytesXYZ(shape, offset).recover {
case t: Throwable => logError(t); Failure(t.getMessage, Full(t), Empty)
}
}

override protected def onFinalize(): Unit = ()

}

class N5BucketProvider(layer: N5Layer) extends BucketProvider with LazyLogging with RateLimitedErrorLogging {

override def loadFromUnderlying(readInstruction: DataReadInstruction): Box[N5CubeHandle] = {
val n5MagOpt: Option[N5Mag] =
layer.mags.find(_.mag == readInstruction.bucket.mag)

n5MagOpt match {
case None => Empty
case Some(n5Mag) =>
val magPathOpt: Option[Path] = {
n5Mag.remoteSource match {
case Some(remoteSource) => remotePathFrom(remoteSource)
case None => localPathFrom(readInstruction, n5Mag.pathWithFallback)
}
}
magPathOpt match {
case None => Empty
case Some(magPath) =>
tryo(onError = e => logError(e))(N5Array.open(magPath, n5Mag.axisOrder)).map(new N5CubeHandle(_))
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.scalableminds.webknossos.datastore.dataformats.zarr

import java.net.URI
import com.scalableminds.util.geometry.{BoundingBox, Vec3Int}
import com.scalableminds.webknossos.datastore.dataformats.n5.N5BucketProvider
import com.scalableminds.webknossos.datastore.datareaders.AxisOrder
import com.scalableminds.webknossos.datastore.models.datasource.LayerViewConfiguration.LayerViewConfiguration
import com.scalableminds.webknossos.datastore.models.datasource._
import com.scalableminds.webknossos.datastore.storage.FileSystemsHolder
import play.api.libs.json.{Json, OFormat}

case class N5Mag(mag: Vec3Int,
path: Option[String],
credentials: Option[FileSystemCredentials],
axisOrder: Option[AxisOrder]) {

lazy val pathWithFallback: String =
path.getOrElse(if (mag.isIsotropic) s"${mag.x}" else s"${mag.x}-${mag.y}-${mag.z}")
private lazy val uri: URI = new URI(pathWithFallback)
private lazy val isRemote: Boolean = FileSystemsHolder.isSupportedRemoteScheme(uri.getScheme)
lazy val remoteSource: Option[RemoteSourceDescriptor] =
if (isRemote)
Some(RemoteSourceDescriptor(uri, credentials.map(_.user), credentials.flatMap(_.password)))
else
None
Copy link
Member

Choose a reason for hiding this comment

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

This reads a lot like ZarrMag. Maybe there can be a common trait? Not sure about a good name, though, ExtendedMag? Maybe you have a better Idea :D

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is exactly ZarrMag. I went ahead and used hte new class in both cases (DatasetLocatorMag)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you have better ideas for the name, feel free to share ;)


}

object N5Mag extends ResolutionFormatHelper {
implicit val jsonFormat: OFormat[N5Mag] = Json.format[N5Mag]
}

trait N5Layer extends DataLayer {

val dataFormat: DataFormat.Value = DataFormat.n5

lazy val bucketProvider = new N5BucketProvider(this)

def resolutions: List[Vec3Int] = mags.map(_.mag)

def mags: List[N5Mag]

def lengthOfUnderlyingCubes(resolution: Vec3Int): Int = Int.MaxValue // Prevents the wkw-shard-specific handle caching

def numChannels: Option[Int] = Some(if (elementClass == ElementClass.uint24) 3 else 1)

}

case class N5DataLayer(
name: String,
category: Category.Value,
boundingBox: BoundingBox,
elementClass: ElementClass.Value,
mags: List[N5Mag],
defaultViewConfiguration: Option[LayerViewConfiguration] = None,
adminViewConfiguration: Option[LayerViewConfiguration] = None,
override val numChannels: Option[Int] = Some(1)
) extends N5Layer

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

case class N5SegmentationLayer(
name: String,
boundingBox: BoundingBox,
elementClass: ElementClass.Value,
mags: List[N5Mag],
largestSegmentId: Long,
mappings: Option[Set[String]] = None,
defaultViewConfiguration: Option[LayerViewConfiguration] = None,
adminViewConfiguration: Option[LayerViewConfiguration] = None,
override val numChannels: Option[Int] = Some(1)
) extends SegmentationLayer
with N5Layer

object N5SegmentationLayer {
implicit val jsonFormat: OFormat[N5SegmentationLayer] = Json.format[N5SegmentationLayer]
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
package com.scalableminds.webknossos.datastore.dataformats.zarr

import java.nio.file.{FileSystem, Path}

import java.nio.file.Path
import com.scalableminds.util.geometry.Vec3Int
import com.scalableminds.util.requestlogging.RateLimitedErrorLogging
import com.scalableminds.util.tools.Fox
import com.scalableminds.webknossos.datastore.dataformats.{BucketProvider, DataCubeHandle}
import com.scalableminds.webknossos.datastore.jzarr.ZarrArray
import com.scalableminds.webknossos.datastore.datareaders.jzarr.ZarrArray
import com.scalableminds.webknossos.datastore.models.BucketPosition
import com.scalableminds.webknossos.datastore.models.requests.DataReadInstruction
import com.scalableminds.webknossos.datastore.storage.FileSystemsHolder
import com.typesafe.scalalogging.LazyLogging
import net.liftweb.common.Box.tryo
import net.liftweb.common.{Box, Empty, Failure, Full}
import net.liftweb.util.Helpers.tryo

import scala.concurrent.ExecutionContext

Expand Down Expand Up @@ -53,21 +51,4 @@ class ZarrBucketProvider(layer: ZarrLayer) extends BucketProvider with LazyLoggi
}

}

private def remotePathFrom(remoteSource: RemoteSourceDescriptor): Option[Path] =
FileSystemsHolder.getOrCreate(remoteSource).map { fileSystem: FileSystem =>
fileSystem.getPath(remoteSource.remotePath)
}

private def localPathFrom(readInstruction: DataReadInstruction, relativeMagPath: String): Option[Path] = {
val magPath = readInstruction.baseDir
.resolve(readInstruction.dataSource.id.team)
.resolve(readInstruction.dataSource.id.name)
.resolve(readInstruction.dataLayer.name)
.resolve(relativeMagPath)
if (magPath.toFile.exists()) {
Some(magPath)
} else None
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.scalableminds.webknossos.datastore.dataformats.zarr
import java.net.URI

import com.scalableminds.util.geometry.{BoundingBox, Vec3Int}
import com.scalableminds.webknossos.datastore.jzarr.AxisOrder
import com.scalableminds.webknossos.datastore.datareaders.AxisOrder
import com.scalableminds.webknossos.datastore.models.datasource.LayerViewConfiguration.LayerViewConfiguration
import com.scalableminds.webknossos.datastore.models.datasource._
import com.scalableminds.webknossos.datastore.storage.FileSystemsHolder
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.scalableminds.webknossos.datastore.datareaders

import com.scalableminds.util.enumeration.ExtendedEnumeration

object ArrayDataType extends ExtendedEnumeration {
type ArrayDataType = Value
val f8, f4, i8, u8, i4, u4, i2, u2, i1, u1 = Value

def bytesPerElementFor(dataType: ArrayDataType): Int =
dataType match {
case ArrayDataType.f8 => 8
case ArrayDataType.f4 => 4
case ArrayDataType.i8 => 8
case ArrayDataType.u8 => 8
case ArrayDataType.i4 => 4
case ArrayDataType.u4 => 4
case ArrayDataType.i2 => 2
case ArrayDataType.u2 => 2
case ArrayDataType.i1 => 1
case ArrayDataType.u1 => 1
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.scalableminds.webknossos.datastore.jzarr
package com.scalableminds.webknossos.datastore.datareaders

import com.scalableminds.util.enumeration.ExtendedEnumeration

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.scalableminds.webknossos.datastore.jzarr
package com.scalableminds.webknossos.datastore.datareaders

import play.api.libs.json.{Json, OFormat}

Expand All @@ -24,7 +24,7 @@ case class AxisOrder(x: Int, y: Int, z: Int, c: Option[Int] = None, t: Option[In
}

object AxisOrder {
// assumes that the last three elements of the shapre are z,y,x (standard in OME NGFF)
// assumes that the last three elements of the shape are z,y,x (standard in OME NGFF)
def asZyxFromRank(rank: Int): AxisOrder = AxisOrder(rank - 1, rank - 2, rank - 3)

def cxyz: AxisOrder = asCxyzFromRank(rank = 4)
Expand Down
Loading