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 all 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 @@ -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);
youri-k marked this conversation as resolved.
Show resolved Hide resolved
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