Skip to content

Commit

Permalink
Support multiple segmentation layers (#5683)
Browse files Browse the repository at this point in the history
* [WIP] don't crash when multiple segmentation layers exist; start introducing getActiveSegmentationLayer() methods

* [WIP] fix all flow errors

* [WIP]: implement and use getVisibleSegmentationLayer, getSomeSegmentationLayer and getEnforcedSomeSegmentationLayer everywhere

* [WIP]: implement and use getFirstSegmentationLayer which only needs a dataset reference and fits some cases better

* remove deprecated functions

* fix wrong parameter

* fix isColorLayer

* adapt shader code to multiple segmentation layers

* fix cell hovering

* support mappings for multiple segmentation layers

* fix reducer for initial changes of mapping objects

* fix runtime exceptions

* make shader code and materials compatible with mappings (only one mapping can be active at given time)

* fix some mapping issues

* fix hovered segment id in status bar

* fix multiple mapping bugs (e.g., race condition when activating a mapping)

* fix wrong attachment of mapping-related textures

* fix callHandlerOnSubscribe behavior in listenToStoreProperty if initial value is null

* update threeJS to 110 so that we can use  to avoid discarded updates; fix switching between mappings of multiple segmentation layers by adapting getSegmentationLayerWithMappingSupport logic

* support volume annotation in dataset with multiple segmentation layers

* adapt more volume code to multiple segmentation layers

* remove outdated comment

* adapt UI for creation of explorative so that fallback layer can be chosen

* change back-end route from boolean withFallback to optional string fallbackLayerName

* clean up GET parameter construction

* further clean up in CreateExplorativeModal logic

* fix creation of volume tracing when viewing dataset and only show remove button for fallback layer

* ensure that only one segmentation layer is visible

* remove activeSegmentationLayerIndex code, as it's not needed anymore

* trigger agglomeration warning for correct segmentation layer

* adapt data export to multiple segmentation layers

* remove unused isRefreshingIsosurfaces store property

* fix passing fallbackLayerName when creating explorational

* make mesh management dependent on concrete segmentation layer

* fix ad-hoc and precomputed mesh usages

* rename mesh/isosurface variables to ...ByLayer where appropriate

* get rid of getEnforcedSomeSegmentationLayer function

* add better typing for mesh view and remove some dead code

* change getResolutionInfoOfSomeSegmentationLayer to getResolutionInfoOfVisibleSegmentationLayer

* remove some usages of getFirstSegmentationLayer

* fix default 'Create Annotation' link for fallback layers

* fix linting

* use correct segmentation layer in version view

* remove redundant if-null check

* make 3-shortcut work better with multiple segmentation layers

* fix unit tests

* improve the api_latest functions

* further improvements of api_latest

* format back-end code

* fail if non-existing fallback layer is selected, add fallbackLayerName to makeHybrid route

* legacy api

* only allow volume annotation in actual tracing layer

* resolve todo affecting features.publicDemoDatasetUrl

* use currently visible segmentation layer as fallback layer when creating an annotation from view mode or when making a hybrid

* rename activeMapping to activeMappingByLayer

* fix CI

* integrate some PR feedback

* Apply suggestions from code review

Co-authored-by: Daniel <[email protected]>

* rename getRequestedOrVisibleSegmentationLayer, getRequestedOrVisibleSegmentationLayerEnforced and getNameOfRequestedOrVisibleSegmentationLayer

* integrate further PR feedback

* improve docstrings in api_latest

* remove other segmentation layers for volume annotations without fallback data

* update changelog

* make getVolumeTracingLayerName backwards compatible

* fix merger mode for hybrids and for skeleton-only annotations

* fix segmentationOpacity dictated by recommended settings in tasks when there is no volume layer and when there are multiple segmentation layers

Co-authored-by: Florian M <[email protected]>
Co-authored-by: Daniel <[email protected]>
  • Loading branch information
3 people authored Sep 8, 2021
1 parent cda0fe9 commit c3cbc7f
Show file tree
Hide file tree
Showing 72 changed files with 1,562 additions and 848 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
- Added a rudimentary version of openAPI docs for some routes. Available at `/swagger.json`. [#5693](https://github.com/scalableminds/webknossos/pull/5693)
- Added support for datasets that have multiple segmentation layers. Note that only one segmentation layer can be rendered at a time, currently. [#5683](https://github.com/scalableminds/webknossos/pull/5683)
- Added shortcuts K and L for toggling the left and right sidebars. [#5709](https://github.com/scalableminds/webknossos/pull/5709)

### Changed
Expand Down
2 changes: 2 additions & 0 deletions MIGRATIONS.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ User-facing changes are documented in the [changelog](CHANGELOG.released.md).

## Unreleased

- For webknossos.org: Change `publicDemoDatasetUrl` in the `features`-block within `application.conf` to be an actionable URL. For example, append `/createExplorative/hybrid?fallbackLayerName=segmentation` to the URL so that a new annotation is created if a user clicks on `Open a Demo Dataset` in the dashboard.

### Postgres Evolutions:

- [075-tasktype-remove-hovered-cell-id.sql](conf/evolutions/075-tasktype-remove-hovered-cell-id.sql)
25 changes: 13 additions & 12 deletions app/controllers/AnnotationController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import scala.concurrent.ExecutionContext
import scala.concurrent.duration._

case class CreateExplorationalParameters(typ: String,
withFallback: Option[Boolean],
fallbackLayerName: Option[String],
resolutionRestrictions: Option[ResolutionRestrictions])
object CreateExplorationalParameters {
implicit val jsonFormat: OFormat[CreateExplorationalParameters] = Json.format[CreateExplorationalParameters]
Expand Down Expand Up @@ -178,7 +178,7 @@ class AnnotationController @Inject()(
request.identity,
dataSet._id,
tracingType,
request.body.withFallback.getOrElse(true),
request.body.fallbackLayerName,
request.body.resolutionRestrictions.getOrElse(ResolutionRestrictions.empty)
) ?~> "annotation.create.failed"
_ = analyticsService.track(CreateAnnotationEvent(request.identity: User, annotation: Annotation))
Expand All @@ -188,16 +188,17 @@ class AnnotationController @Inject()(
}

@ApiOperation(hidden = true, value = "")
def makeHybrid(typ: String, id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
for {
_ <- bool2Fox(AnnotationType.Explorational.toString == typ) ?~> "annotation.makeHybrid.explorationalsOnly"
annotation <- provider.provideAnnotation(typ, id, request.identity)
organization <- organizationDAO.findOne(request.identity._organization)
_ <- annotationService.makeAnnotationHybrid(annotation, organization.name) ?~> "annotation.makeHybrid.failed"
updated <- provider.provideAnnotation(typ, id, request.identity)
json <- annotationService.publicWrites(updated, Some(request.identity)) ?~> "annotation.write.failed"
} yield JsonOk(json)
}
def makeHybrid(typ: String, id: String, fallbackLayerName: Option[String]): Action[AnyContent] =
sil.SecuredAction.async { implicit request =>
for {
_ <- bool2Fox(AnnotationType.Explorational.toString == typ) ?~> "annotation.makeHybrid.explorationalsOnly"
annotation <- provider.provideAnnotation(typ, id, request.identity)
organization <- organizationDAO.findOne(request.identity._organization)
_ <- annotationService.makeAnnotationHybrid(annotation, organization.name, fallbackLayerName) ?~> "annotation.makeHybrid.failed"
updated <- provider.provideAnnotation(typ, id, request.identity)
json <- annotationService.publicWrites(updated, Some(request.identity)) ?~> "annotation.write.failed"
} yield JsonOk(json)
}

@ApiOperation(hidden = true, value = "")
def downsample(typ: String, id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/LegacyApiController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class LegacyApiController @Inject()(annotationController: AnnotationController,

def annotationMakeHybrid(typ: String, id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
for {
result <- annotationController.makeHybrid(typ, id)(request)
result <- annotationController.makeHybrid(typ, id, None)(request)
} yield replaceVisibilityInResultJson(result)
}

Expand Down
52 changes: 29 additions & 23 deletions app/models/annotation/AnnotationService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -139,17 +139,23 @@ class AnnotationService @Inject()(
dataSet: DataSet,
dataSource: DataSource,
tracingType: TracingType.Value,
withFallback: Boolean,
fallbackLayerNameOpt: Option[String],
resolutionRestrictions: ResolutionRestrictions,
organizationName: String,
oldTracingId: Option[String] = None)(implicit ctx: DBAccessContext): Fox[(Option[String], Option[String])] = {
def getFallbackLayer: Option[SegmentationLayer] =
if (withFallback) {
dataSource.dataLayers.flatMap {
case layer: SegmentationLayer => Some(layer)
case _ => None
}.headOption
} else None

def getFallbackLayer(fallbackLayerName: String): Fox[SegmentationLayer] =
for {
fallbackLayer <- dataSource.dataLayers
.filter(dl => dl.name == fallbackLayerName)
.flatMap {
case layer: SegmentationLayer => Some(layer)
case _ => None
}
.headOption
.toFox
_ <- bool2Fox(fallbackLayer.elementClass != ElementClass.uint64) ?~> "annotation.volume.uint64"
} yield fallbackLayer

tracingType match {
case TracingType.skeleton =>
Expand All @@ -170,8 +176,7 @@ class AnnotationService @Inject()(
case TracingType.volume =>
for {
client <- tracingStoreService.clientFor(dataSet)
fallbackLayer = getFallbackLayer
_ <- bool2Fox(fallbackLayer.forall(_.elementClass != ElementClass.uint64)) ?~> "annotation.volume.uint64"
fallbackLayer <- Fox.runOptional(fallbackLayerNameOpt)(getFallbackLayer)
volumeTracing <- createVolumeTracing(dataSource,
organizationName,
fallbackLayer,
Expand All @@ -181,8 +186,7 @@ class AnnotationService @Inject()(
case TracingType.hybrid =>
for {
client <- tracingStoreService.clientFor(dataSet)
fallbackLayer = getFallbackLayer
_ <- bool2Fox(fallbackLayer.forall(_.elementClass != ElementClass.uint64)) ?~> "annotation.volume.uint64"
fallbackLayer <- Fox.runOptional(fallbackLayerNameOpt)(getFallbackLayer)
skeletonTracingId <- client.saveSkeletonTracing(
SkeletonTracingDefaults.createInstance.copy(dataSetName = dataSet.name,
editPosition = dataSource.center,
Expand All @@ -199,7 +203,7 @@ class AnnotationService @Inject()(
def createExplorationalFor(user: User,
_dataSet: ObjectId,
tracingType: TracingType.Value,
withFallback: Boolean,
fallbackLayerName: Option[String],
resolutionRestrictions: ResolutionRestrictions)(implicit ctx: DBAccessContext,
m: MessagesProvider): Fox[Annotation] =
for {
Expand All @@ -210,7 +214,7 @@ class AnnotationService @Inject()(
tracingIds <- createTracingsForExplorational(dataSet,
usableDataSource,
tracingType,
withFallback,
fallbackLayerName,
resolutionRestrictions,
organization.name)
teamId <- selectSuitableTeam(user, dataSet) ?~> "annotation.create.forbidden"
Expand All @@ -228,27 +232,29 @@ class AnnotationService @Inject()(
annotation
}

def makeAnnotationHybrid(annotation: Annotation, organizationName: String)(
def makeAnnotationHybrid(annotation: Annotation, organizationName: String, fallbackLayerName: Option[String])(
implicit ctx: DBAccessContext): Fox[Unit] = {
def createNewTracings(dataSet: DataSet, dataSource: DataSource) = annotation.tracingType match {
case TracingType.skeleton =>
createTracingsForExplorational(dataSet,
dataSource,
TracingType.volume,
withFallback = true,
fallbackLayerName,
ResolutionRestrictions.empty,
organizationName).flatMap {
case (_, Some(volumeId)) => annotationDAO.updateVolumeTracingId(annotation._id, volumeId)
case _ => Fox.failure("unexpectedReturn")
}
case TracingType.volume =>
createTracingsForExplorational(dataSet,
dataSource,
TracingType.skeleton,
withFallback = false,
ResolutionRestrictions.empty,
organizationName,
annotation.volumeTracingId).flatMap {
createTracingsForExplorational(
dataSet,
dataSource,
TracingType.skeleton,
fallbackLayerNameOpt = None, // unused when creating skeleton
ResolutionRestrictions.empty,
organizationName,
annotation.volumeTracingId
).flatMap {
case (Some(skeletonId), _) => annotationDAO.updateSkeletonTracingId(annotation._id, skeletonId)
case _ => Fox.failure("unexpectedReturn")
}
Expand Down
2 changes: 1 addition & 1 deletion conf/webknossos.latest.routes
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ PUT /annotations/:typ/:id/reset c
PATCH /annotations/:typ/:id/transfer controllers.AnnotationController.transfer(typ: String, id: String)

GET /annotations/:typ/:id/info controllers.AnnotationController.info(typ: String, id: String, timestamp: Long)
PATCH /annotations/:typ/:id/makeHybrid controllers.AnnotationController.makeHybrid(typ: String, id: String)
PATCH /annotations/:typ/:id/makeHybrid controllers.AnnotationController.makeHybrid(typ: String, id: String, fallbackLayerName: Option[String])
PATCH /annotations/:typ/:id/downsample controllers.AnnotationController.downsample(typ: String, id: String)
PATCH /annotations/:typ/:id/unlinkFallback controllers.AnnotationController.unlinkFallback(typ: String, id: String)
DELETE /annotations/:typ/:id controllers.AnnotationController.cancel(typ: String, id: String)
Expand Down
10 changes: 7 additions & 3 deletions frontend/javascripts/admin/admin_rest_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -675,14 +675,14 @@ export function getAnnotationInformation(
export function createExplorational(
datasetId: APIDatasetId,
typ: TracingType,
withFallback: boolean,
fallbackLayerName: ?string,
resolutionRestrictions: ?APIResolutionRestrictions,
options?: RequestOptions = {},
): Promise<APIAnnotation> {
const url = `/api/datasets/${datasetId.owningOrganization}/${datasetId.name}/createExplorational`;
return Request.sendJSONReceiveJSON(
url,
Object.assign({}, { data: { typ, withFallback, resolutionRestrictions } }, options),
Object.assign({}, { data: { typ, fallbackLayerName, resolutionRestrictions } }, options),
);
}

Expand Down Expand Up @@ -761,9 +761,13 @@ export async function importVolumeTracing(tracing: Tracing, dataFile: File): Pro
);
}

export function convertToHybridTracing(annotationId: string): Promise<void> {
export function convertToHybridTracing(
annotationId: string,
fallbackLayerName: ?string,
): Promise<void> {
return Request.receiveJSON(`/api/annotations/Explorational/${annotationId}/makeHybrid`, {
method: "PATCH",
fallbackLayerName,
});
}

Expand Down
Loading

0 comments on commit c3cbc7f

Please sign in to comment.