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

Find data for volumes #4847

Merged
merged 8 commits into from
Oct 7, 2020
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
[Commits](https://github.com/scalableminds/webknossos/compare/20.10.0...HEAD)

### Added
-
- The find data function now works for volume tracings, too. [#4847](https://github.com/scalableminds/webknossos/pull/4847)

### Changed
- The position input of tracings now accepts decimal input. When losing focus the values are cut off at the comma. [#4803](https://github.com/scalableminds/webknossos/pull/4803)
Expand Down
10 changes: 2 additions & 8 deletions frontend/javascripts/admin/admin_rest_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -981,16 +981,10 @@ export async function getOrganizationForDataset(datasetName: string): Promise<st
}

export async function findDataPositionForLayer(
datastoreUrl: string,
datasetId: APIDatasetId,
layerName: string,
requestUrl: string,
): Promise<{ position: ?Vector3, resolution: ?Vector3 }> {
const { position, resolution } = await doWithToken(token =>
Request.receiveJSON(
`${datastoreUrl}/data/datasets/${datasetId.owningOrganization}/${
datasetId.name
}/layers/${layerName}/findData?token=${token}`,
),
Request.receiveJSON(`${requestUrl}/findData?token=${token}`),
youri-k marked this conversation as resolved.
Show resolved Hide resolved
);
return { position, resolution };
}
Expand Down
56 changes: 33 additions & 23 deletions frontend/javascripts/oxalis/view/settings/dataset_settings_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,11 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps> {
let tooltipText = isDisabled
? "You cannot search for data when the layer is disabled."
: "If you are having trouble finding your data, webKnossos can try to find a position which contains data.";
// If the tracing contains a volume tracing, the backend can only
// search in the fallback layer of the segmentation layer for data.
let layerNameToSearchDataIn = layerName;
if (!isColorLayer) {
const { volume } = Store.getState().tracing;
if (volume && volume.fallbackLayer) {
layerNameToSearchDataIn = volume.fallbackLayer;
} else if (volume && !volume.fallbackLayer && !isDisabled) {
isDisabled = true;
tooltipText =
"You do not have a fallback layer for this segmentation layer. It is only possible to search in fallback layers.";
}

const { volume } = Store.getState().tracing;
if (!isColorLayer && volume && volume.fallbackLayer) {
tooltipText =
"webKnossos will try to find data in your volume tracing first and in the fallback layer afterwards.";
}

return (
Expand All @@ -97,7 +90,7 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps> {
type="scan"
onClick={
!isDisabled
? () => this.handleFindData(layerNameToSearchDataIn)
? () => this.handleFindData(layerName, isColorLayer)
: () => Promise.resolve()
}
style={{
Expand Down Expand Up @@ -407,14 +400,31 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps> {
this.props.onChangeUser("gpuMemoryFactor", gpuFactor);
};

handleFindData = async (layerName: string) => {
handleFindData = async (layerName: string, isColorLayer: boolean) => {
const { volume, tracingStore } = Store.getState().tracing;
const { dataset } = this.props;
const { position, resolution } = await findDataPositionForLayer(
dataset.dataStore.url,
dataset,
layerName,
);
if (!position || !resolution) {
let foundPosition;
let foundResolution;

if (volume && !isColorLayer) {
const requestUrl = `${tracingStore.url}/tracings/volume/${volume.tracingId}`;
const { position, resolution } = await findDataPositionForLayer(requestUrl);
if ((!position || !resolution) && volume.fallbackLayer) {
await this.handleFindData(volume.fallbackLayer, true);
youri-k marked this conversation as resolved.
Show resolved Hide resolved
return;
}
foundPosition = position;
foundResolution = resolution;
} else {
const requestUrl = `${dataset.dataStore.url}/data/datasets/${dataset.owningOrganization}/${
dataset.name
}/layers/${layerName}`;
const { position, resolution } = await findDataPositionForLayer(requestUrl);
foundPosition = position;
foundResolution = resolution;
}

if (!foundPosition || !foundResolution) {
const { upperBoundary, lowerBoundary } = getLayerBoundaries(dataset, layerName);
const centerPosition = V3.add(lowerBoundary, upperBoundary).map(el => el / 2);

Expand All @@ -425,10 +435,10 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps> {
return;
}

this.props.onSetPosition(position);
const zoomValue = this.props.onZoomToResolution(resolution);
this.props.onSetPosition(foundPosition);
const zoomValue = this.props.onZoomToResolution(foundResolution);
Toast.success(
`Jumping to position ${position.join(", ")} and zooming to ${zoomValue.toFixed(2)}`,
`Jumping to position ${foundPosition.join(", ")} and zooming to ${zoomValue.toFixed(2)}`,
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.scalableminds.webknossos.datastore.services

import com.scalableminds.util.geometry.Point3D
import com.scalableminds.webknossos.datastore.models.datasource.DataLayer

trait DataFinder {
private def getExactDataOffset(data: Array[Byte], bytesPerElement: Int): Point3D = {
val bucketLength = DataLayer.bucketLength
for {
z <- 0 until bucketLength
y <- 0 until bucketLength
x <- 0 until bucketLength
scaledX = x * bytesPerElement
scaledY = y * bytesPerElement * bucketLength
scaledZ = z * bytesPerElement * bucketLength * bucketLength
} {
val voxelOffset = scaledX + scaledY + scaledZ
if (data.slice(voxelOffset, voxelOffset + bytesPerElement).exists(_ != 0)) return Point3D(x, y, z)
}
Point3D(0, 0, 0)
}

def getPositionOfNonZeroData(data: Array[Byte],
globalPositionOffset: Point3D,
bytesPerElement: Int): Option[Point3D] =
if (data.nonEmpty && data.exists(_ != 0)) Some(globalPositionOffset.move(getExactDataOffset(data, bytesPerElement)))
else None
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ object Histogram { implicit val jsonFormat = Json.format[Histogram] }

class FindDataService @Inject()(dataServicesHolder: BinaryDataServiceHolder)(implicit ec: ExecutionContext)
extends DataConverter
with DataFinder
with FoxImplicits {
val binaryDataService: BinaryDataService = dataServicesHolder.binaryDataService

Expand Down Expand Up @@ -98,23 +99,6 @@ class FindDataService @Inject()(dataServicesHolder: BinaryDataServiceHolder)(imp
private def checkAllPositionsForData(dataSource: DataSource,
dataLayer: DataLayer): Fox[Option[(Point3D, Point3D)]] = {

def getExactDataOffset(data: Array[Byte]): Point3D = {
val bytesPerElement = dataLayer.bytesPerElement
val cubeLength = DataLayer.bucketLength / bytesPerElement
for {
z <- 0 until cubeLength
y <- 0 until cubeLength
x <- 0 until cubeLength
scaledX = x * bytesPerElement
scaledY = y * bytesPerElement
scaledZ = z * bytesPerElement
} {
val voxelOffset = scaledX + scaledY * cubeLength + scaledZ * cubeLength * cubeLength
if (data.slice(voxelOffset, voxelOffset + bytesPerElement).exists(_ != 0)) return Point3D(x, y, z)
}
Point3D(0, 0, 0)
}

def searchPositionIter(positions: List[Point3D], resolution: Point3D): Fox[Option[Point3D]] =
positions match {
case List() => Fox.successful(None)
Expand All @@ -128,8 +112,8 @@ class FindDataService @Inject()(dataServicesHolder: BinaryDataServiceHolder)(imp
def checkIfPositionHasData(position: Point3D, resolution: Point3D) =
for {
data <- getDataFor(dataSource, dataLayer, position, resolution)
if data.nonEmpty && data.exists(_ != 0)
} yield position.move(getExactDataOffset(data))
position <- getPositionOfNonZeroData(data, position, dataLayer.bytesPerElement)
} yield position

def resolutionIter(positions: List[Point3D], remainingResolutions: List[Point3D]): Fox[Option[(Point3D, Point3D)]] =
remainingResolutions match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import java.nio.{ByteBuffer, ByteOrder}

import akka.stream.scaladsl.Source
import com.google.inject.Inject
import com.scalableminds.util.geometry.BoundingBox
import com.scalableminds.util.geometry.{BoundingBox, Point3D}
import com.scalableminds.webknossos.tracingstore.VolumeTracing.{VolumeTracing, VolumeTracingOpt, VolumeTracings}
import com.scalableminds.webknossos.datastore.models.{WebKnossosDataRequest, WebKnossosIsosurfaceRequest}
import com.scalableminds.webknossos.datastore.services.UserAccessRequest
Expand Down Expand Up @@ -190,4 +190,16 @@ class VolumeTracingController @Inject()(val tracingService: VolumeTracingService

private def formatNeighborList(neighbors: List[Int]): String =
"[" + neighbors.mkString(", ") + "]"

def findData(tracingId: String) = Action.async { implicit request =>
accessTokenService.validateAccess(UserAccessRequest.readTracing(tracingId)) {
AllowRemoteOrigin {
for {
positionOpt <- tracingService.findData(tracingId)
} yield {
Ok(Json.obj("position" -> positionOpt, "resolution" -> positionOpt.map(_ => Point3D(1, 1, 1))))
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class VolumeTracingService @Inject()(
) extends TracingService[VolumeTracing]
with VolumeTracingBucketHelper
with WKWDataFormatHelper
with DataFinder
with ProtoGeometryImplicits
with FoxImplicits
with LazyLogging {
Expand Down Expand Up @@ -405,6 +406,20 @@ class VolumeTracingService @Inject()(
result <- isosurfaceService.requestIsosurfaceViaActor(isosurfaceRequest)
} yield result

def findData(tracingId: String): Fox[Option[Point3D]] =
for {
tracing <- find(tracingId) ?~> "tracing.notFound"
volumeLayer = volumeTracingLayer(tracingId, tracing)
bucketStream = volumeLayer.bucketProvider.bucketStream(1, Some(tracing.version))
bucketPosOpt = if (bucketStream.hasNext) {
val bucket = bucketStream.next()
val bucketPos = bucket._1
getPositionOfNonZeroData(bucket._2,
Point3D(bucketPos.globalX, bucketPos.globalY, bucketPos.globalZ),
volumeLayer.bytesPerElement)
} else None
} yield bucketPosOpt

def merge(tracings: Seq[VolumeTracing]): VolumeTracing = tracings.reduceLeft(mergeTwo)

def mergeTwo(tracingA: VolumeTracing, tracingB: VolumeTracing): VolumeTracing = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ POST /volume/:tracingId/data @com.scalablemin
POST /volume/:tracingId/duplicate @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.duplicate(tracingId: String, fromTask: Option[Boolean])
GET /volume/:tracingId/updateActionLog @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.updateActionLog(tracingId: String)
POST /volume/:tracingId/isosurface @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.requestIsosurface(tracingId: String)
GET /volume/:tracingId/findData @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.findData(tracingId: String)
POST /volume/getMultiple @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.getMultiple
POST /volume/mergedFromIds @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.mergedFromIds(persist: Boolean)
POST /volume/mergedFromContents @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.mergedFromContents(persist: Boolean)
Expand Down