From 9bde21d99db0d0c6d19ff9ed9f8706a36440c014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 28 Apr 2025 16:01:28 +0200 Subject: [PATCH 1/2] Revert "In Data Loading, Clip to Layer BoundingBox (#8551)" This reverts commit 7275b8b636a734c10c4fd03beb4b26f30afdd49f. --- CHANGELOG.unreleased.md | 1 - .../util/geometry/BoundingBox.scala | 8 +--- .../scalableminds/util/geometry/Vec3Int.scala | 5 +- .../datastore/models/requests/Cuboid.scala | 8 +--- .../models/requests/DataServiceRequests.scala | 2 - .../services/BinaryDataService.scala | 47 +++---------------- 6 files changed, 10 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index efefa71082e..69a0e507c6f 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -18,7 +18,6 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - Updated E2E tests to use `vitest` framework instead of `ava`. [#8543](https://github.com/scalableminds/webknossos/pull/8543) - Adjusted the names of custom model inference jobs and train model jobs to match the worker's naming. [#8524](https://github.com/scalableminds/webknossos/pull/8524) - Updated screenshot tests to use `vitest` framework instead of `ava`. [#8553](https://github.com/scalableminds/webknossos/pull/8553) -- When loading data from a data layer that has data stored beyond the bounding box specified in the datasource-properties.json, data outside of the bounding box is now zeroed. (the layer is “clipped”). [#8551](https://github.com/scalableminds/webknossos/pull/8551) ### Fixed - Fixed a bug in the trees tab where the color change of a tree would affect the tree on which the context menu was previously opened. [#8562](https://github.com/scalableminds/webknossos/pull/8562) diff --git a/util/src/main/scala/com/scalableminds/util/geometry/BoundingBox.scala b/util/src/main/scala/com/scalableminds/util/geometry/BoundingBox.scala index ef7aa90e70a..b74c8d9b7f7 100644 --- a/util/src/main/scala/com/scalableminds/util/geometry/BoundingBox.scala +++ b/util/src/main/scala/com/scalableminds/util/geometry/BoundingBox.scala @@ -6,7 +6,7 @@ import play.api.libs.json.{JsObject, Json} case class BoundingBox(topLeft: Vec3Int, width: Int, height: Int, depth: Int) { - lazy val bottomRight: Vec3Int = topLeft.move(width, height, depth) + val bottomRight: Vec3Int = topLeft.move(width, height, depth) def intersects(other: BoundingBox): Boolean = math.max(topLeft.x, other.topLeft.x) < math.min(bottomRight.x, other.bottomRight.x) && @@ -45,9 +45,6 @@ case class BoundingBox(topLeft: Vec3Int, width: Int, height: Int, depth: Int) { BoundingBox(Vec3Int(x, y, z), w, h, d) } - def isFullyContainedIn(other: BoundingBox): Boolean = - this.intersection(other).contains(this) - def isEmpty: Boolean = width <= 0 || height <= 0 || depth <= 0 @@ -64,9 +61,6 @@ case class BoundingBox(topLeft: Vec3Int, width: Int, height: Int, depth: Int) { // Since floorDiv is used for topLeft, ceilDiv is used for the size to avoid voxels being lost at the border BoundingBox(topLeft / that, ceilDiv(width, that.x), ceilDiv(height, that.y), ceilDiv(depth, that.z)) - def move(delta: Vec3Int): BoundingBox = - this.copy(topLeft = this.topLeft + delta) - def toSql: List[Int] = List(topLeft.x, topLeft.y, topLeft.z, width, height, depth) diff --git a/util/src/main/scala/com/scalableminds/util/geometry/Vec3Int.scala b/util/src/main/scala/com/scalableminds/util/geometry/Vec3Int.scala index 196f3cea78a..758327662f6 100644 --- a/util/src/main/scala/com/scalableminds/util/geometry/Vec3Int.scala +++ b/util/src/main/scala/com/scalableminds/util/geometry/Vec3Int.scala @@ -23,9 +23,6 @@ case class Vec3Int(x: Int, y: Int, z: Int) { def /(that: Vec3Int): Vec3Int = Vec3Int(x / that.x, y / that.y, z / that.z) - def unary_- : Vec3Int = - Vec3Int(-x, -y, -z) - def scale(s: Float): Vec3Int = Vec3Int((x * s).toInt, (y * s).toInt, (z * s).toInt) @@ -56,6 +53,8 @@ case class Vec3Int(x: Int, y: Int, z: Int) { def move(other: Vec3Int): Vec3Int = move(other.x, other.y, other.z) + def negate: Vec3Int = Vec3Int(-x, -y, -z) + def to(bottomRight: Vec3Int): Seq[Vec3Int] = range(bottomRight, _ to _) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/requests/Cuboid.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/requests/Cuboid.scala index 9196f4652a5..11bceadc1d9 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/requests/Cuboid.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/requests/Cuboid.scala @@ -1,6 +1,6 @@ package com.scalableminds.webknossos.datastore.models.requests -import com.scalableminds.util.geometry.{BoundingBox, Vec3Int} +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.webknossos.datastore.models.{BucketPosition, VoxelPosition} /** @@ -51,10 +51,4 @@ case class Cuboid(topLeft: VoxelPosition, width: Int, height: Int, depth: Int) { height * mag.y, depth * mag.z ) - - def toBoundingBoxInMag: BoundingBox = - BoundingBox(Vec3Int(topLeft.voxelXInMag, topLeft.voxelYInMag, topLeft.voxelZInMag), width, height, depth) - - def toMag1BoundingBox: BoundingBox = - toMag1.toBoundingBoxInMag } diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/requests/DataServiceRequests.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/requests/DataServiceRequests.scala index 76761ec92e3..96cbbb82132 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/requests/DataServiceRequests.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/requests/DataServiceRequests.scala @@ -1,6 +1,5 @@ package com.scalableminds.webknossos.datastore.models.requests -import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.webknossos.datastore.models.{AdditionalCoordinate, BucketPosition} import com.scalableminds.webknossos.datastore.models.datasource.{DataLayer, DataSource, SegmentationLayer} @@ -22,7 +21,6 @@ case class DataServiceDataRequest( settings: DataServiceRequestSettings ) { def isSingleBucket: Boolean = cuboid.isSingleBucket(DataLayer.bucketLength) - def mag: Vec3Int = cuboid.mag } case class DataReadInstruction( diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/BinaryDataService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/BinaryDataService.scala index 9711965eaec..d17b500f621 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/BinaryDataService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/BinaryDataService.scala @@ -108,52 +108,16 @@ class BinaryDataService(val dataBaseDir: Path, conversionFunc(inputArray)) else Fox.successful(inputArray) - /* - * Everything outside of the layer bounding box is set to black (zero) so data outside of the specified - * bounding box is not exposed to the user - */ - private def clipToLayerBoundingBox(request: DataServiceDataRequest)(inputArray: Array[Byte]): Box[Array[Byte]] = { - val bytesPerElement = request.dataLayer.bytesPerElement - val requestBboxInMag = request.cuboid.toBoundingBoxInMag - val layerBboxInMag = request.dataLayer.boundingBox / request.mag // Note that this div is implemented to round to the bigger bbox so we don’t lose voxels inside. - val intersectionOpt = requestBboxInMag.intersection(layerBboxInMag).map(_.move(-requestBboxInMag.topLeft)) - val outputArray = Array.fill[Byte](inputArray.length)(0) - intersectionOpt.foreach { intersection => - for { - z <- intersection.topLeft.z until intersection.bottomRight.z - y <- intersection.topLeft.y until intersection.bottomRight.y - // We can bulk copy a row of voxels and do not need to iterate in the x dimension - } { - val offset = - (intersection.topLeft.x + - y * requestBboxInMag.width + - z * requestBboxInMag.width * requestBboxInMag.height) * bytesPerElement - System.arraycopy(inputArray, - offset, - outputArray, - offset, - (intersection.bottomRight.x - intersection.topLeft.x) * bytesPerElement) - } - } - Full(outputArray) - } - private def convertAccordingToRequest(request: DataServiceDataRequest, inputArray: Array[Byte]): Fox[Array[Byte]] = for { - clippedData <- convertIfNecessary( - !request.cuboid.toMag1BoundingBox.isFullyContainedIn(request.dataLayer.boundingBox), - inputArray, - data => clipToLayerBoundingBox(request)(data).toFox, - request - ) mappedDataFox <- agglomerateServiceOpt.map { agglomerateService => convertIfNecessary( request.settings.appliedAgglomerate.isDefined && request.dataLayer.category == Category.segmentation && request.cuboid.mag.maxDim <= MaxMagForAgglomerateMapping, - clippedData, + inputArray, data => agglomerateService.applyAgglomerate(request)(data).toFox, request ) - }.toFox.fillEmpty(Fox.successful(clippedData)) ?~> "Failed to apply agglomerate mapping" + }.toFox.fillEmpty(Fox.successful(inputArray)) ?~> "Failed to apply agglomerate mapping" mappedData <- mappedDataFox resultData <- convertIfNecessary(request.settings.halfByte, mappedData, convertToHalfByte, request) } yield resultData @@ -204,6 +168,7 @@ class BinaryDataService(val dataBaseDir: Path, private def cutOutCuboid(request: DataServiceDataRequest, rs: List[(BucketPosition, Array[Byte])]): Array[Byte] = { val bytesPerElement = request.dataLayer.bytesPerElement val cuboid = request.cuboid + val subsamplingStrides = Vec3Int.ones val resultShape = Vec3Int(cuboid.width, cuboid.height, cuboid.depth) val result = new Array[Byte](cuboid.volume * bytesPerElement) @@ -229,9 +194,9 @@ class BinaryDataService(val dataBaseDir: Path, y % bucketLength * bucketLength + z % bucketLength * bucketLength * bucketLength) * bytesPerElement - val rx = xMin - cuboid.topLeft.voxelXInMag - val ry = y - cuboid.topLeft.voxelYInMag - val rz = z - cuboid.topLeft.voxelZInMag + val rx = (xMin - cuboid.topLeft.voxelXInMag) / subsamplingStrides.x + val ry = (y - cuboid.topLeft.voxelYInMag) / subsamplingStrides.y + val rz = (z - cuboid.topLeft.voxelZInMag) / subsamplingStrides.z val resultOffset = (rx + ry * resultShape.x + rz * resultShape.x * resultShape.y) * bytesPerElement System.arraycopy(data, dataOffset, result, resultOffset, (xMax - xMin) * bytesPerElement) From 5627da73d8200c923d7ed1160b8abcd5e5d8a2e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 28 Apr 2025 16:12:36 +0200 Subject: [PATCH 2/2] remove changelog entry --- CHANGELOG.unreleased.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index f793032c737..d9b47112fc7 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -19,7 +19,6 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - Adjusted the names of custom model inference jobs and train model jobs to match the worker's naming. [#8524](https://github.com/scalableminds/webknossos/pull/8524) - Updated screenshot tests to use `vitest` framework instead of `ava`. [#8553](https://github.com/scalableminds/webknossos/pull/8553) - The mapping dropdown for segmentation is wider now so that mapping names are fully readable. [#8570](https://github.com/scalableminds/webknossos/pull/8570) -- When loading data from a data layer that has data stored beyond the bounding box specified in the datasource-properties.json, data outside of the bounding box is now zeroed. (the layer is “clipped”). [#8551](https://github.com/scalableminds/webknossos/pull/8551) ### Fixed - Fixed a bug in the trees tab where the color change of a tree would affect the tree on which the context menu was previously opened. [#8562](https://github.com/scalableminds/webknossos/pull/8562)