Skip to content

Commit

Permalink
Find data for volumes (#4847)
Browse files Browse the repository at this point in the history
* first version of frontend and backend

* update changelog

* Merge branch 'master' of github.com:scalableminds/webknossos into find-volume-data

* extract find methods into new trait

* apply pr feedback
  • Loading branch information
youri-k authored Oct 7, 2020
1 parent ff87756 commit bb73244
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 57 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released

### Added
- Hybrid tracings can now be imported directly in the tracing view via drag'n'drop. [#4837](https://github.com/scalableminds/webknossos/pull/4837)
- The find data function now works for volume tracings, too. [#4847](https://github.com/scalableminds/webknossos/pull/4847)

### Changed
- Brush circles are now connected with rectangles to provide a continuous stroke even if the brush is moved quickly. [#4785](https://github.com/scalableminds/webknossos/pull/4822)
Expand Down
10 changes: 10 additions & 0 deletions frontend/javascripts/admin/admin_rest_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,16 @@ export async function findDataPositionForLayer(
return { position, resolution };
}

export async function findDataPositionForVolumeTracing(
tracingstoreUrl: string,
tracingId: string,
): Promise<{ position: ?Vector3, resolution: ?Vector3 }> {
const { position, resolution } = await doWithToken(token =>
Request.receiveJSON(`${tracingstoreUrl}/tracings/volume/${tracingId}/findData?token=${token}`),
);
return { position, resolution };
}

export async function getHistogramForLayer(
datastoreUrl: string,
datasetId: APIDatasetId,
Expand Down
65 changes: 41 additions & 24 deletions frontend/javascripts/oxalis/view/settings/dataset_settings_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import {
DropdownSetting,
ColorSetting,
} from "oxalis/view/settings/setting_input_views";
import { findDataPositionForLayer, clearCache } from "admin/admin_rest_api";
import {
clearCache,
findDataPositionForLayer,
findDataPositionForVolumeTracing,
} from "admin/admin_rest_api";
import { getGpuFactorsWithLabels } from "oxalis/model/bucket_data_handling/data_rendering_logic";
import { getMaxZoomValueForResolution } from "oxalis/model/accessors/flycam_accessor";
import {
Expand Down Expand Up @@ -77,18 +81,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 +94,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 @@ -405,14 +402,34 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps> {
this.props.onChangeUser("gpuMemoryFactor", gpuFactor);
};

handleFindData = async (layerName: string) => {
handleFindData = async (layerName: string, isDataLayer: 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 && !isDataLayer) {
const { position, resolution } = await findDataPositionForVolumeTracing(
tracingStore.url,
volume.tracingId,
);
if ((!position || !resolution) && volume.fallbackLayer) {
await this.handleFindData(volume.fallbackLayer, true);
return;
}
foundPosition = position;
foundResolution = resolution;
} else {
const { position, resolution } = await findDataPositionForLayer(
dataset.dataStore.url,
dataset,
layerName,
);
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 @@ -423,10 +440,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 @@ -5,7 +5,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.util.tools.ExtendedTypes.ExtendedString
import com.scalableminds.webknossos.tracingstore.VolumeTracing.{VolumeTracing, VolumeTracingOpt, VolumeTracings}
import com.scalableminds.webknossos.datastore.models.{WebKnossosDataRequest, WebKnossosIsosurfaceRequest}
Expand Down Expand Up @@ -155,6 +155,22 @@ class VolumeTracingController @Inject()(val tracingService: VolumeTracingService
}
}

def importVolumeData(tracingId: String): Action[MultipartFormData[TemporaryFile]] =
Action.async(parse.multipartFormData) { implicit request =>
log {
accessTokenService.validateAccess(UserAccessRequest.writeTracing(tracingId)) {
AllowRemoteOrigin {
for {
tracing <- tracingService.find(tracingId)
currentVersion <- request.body.dataParts("currentVersion").headOption.flatMap(_.toIntOpt).toFox
zipFile <- request.body.files.headOption.map(f => new File(f.ref.path.toString)).toFox
largestSegmentId <- tracingService.importVolumeData(tracingId, tracing, zipFile, currentVersion)
} yield Ok(Json.toJson(largestSegmentId))
}
}
}
}

def updateActionLog(tracingId: String) = Action.async { implicit request =>
log {
accessTokenService.validateAccess(UserAccessRequest.readTracing(tracingId)) {
Expand Down Expand Up @@ -194,19 +210,16 @@ class VolumeTracingController @Inject()(val tracingService: VolumeTracingService
private def formatNeighborList(neighbors: List[Int]): String =
"[" + neighbors.mkString(", ") + "]"

def importVolumeData(tracingId: String): Action[MultipartFormData[TemporaryFile]] =
Action.async(parse.multipartFormData) { implicit request =>
log {
accessTokenService.validateAccess(UserAccessRequest.writeTracing(tracingId)) {
AllowRemoteOrigin {
for {
tracing <- tracingService.find(tracingId)
currentVersion <- request.body.dataParts("currentVersion").headOption.flatMap(_.toIntOpt).toFox
zipFile <- request.body.files.headOption.map(f => new File(f.ref.path.toString)).toFox
largestSegmentId <- tracingService.importVolumeData(tracingId, tracing, zipFile, currentVersion)
} yield Ok(Json.toJson(largestSegmentId))
}
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 @@ -415,6 +416,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 @@ -3,7 +3,7 @@
# ~~~~

# Health endpoint
GET /health @com.scalableminds.webknossos.tracingstore.controllers.Application.health
GET /health @com.scalableminds.webknossos.tracingstore.controllers.Application.health

# Volume tracings
POST /volume/save @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.save
Expand All @@ -19,6 +19,7 @@ POST /volume/:tracingId/duplicate @com.scalablemin
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)
POST /volume/:tracingId/importVolumeData @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.importVolumeData(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

0 comments on commit bb73244

Please sign in to comment.