diff --git a/CHANGELOG.released.md b/CHANGELOG.released.md index d7ea72ac318..859768e4e36 100644 --- a/CHANGELOG.released.md +++ b/CHANGELOG.released.md @@ -77,7 +77,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - Fixed a rendering error which could make some layers disappear in certain circumstances. [#4556](https://github.com/scalableminds/webknossos/pull/4556) - Fixed a rendering error which caused negative float data to be rendered white. [#4556](https://github.com/scalableminds/webknossos/pull/4571) - Fixed the histogram creation if some sampled positions don't contain data. [#4584](https://github.com/scalableminds/webknossos/pull/4584) -- Fixed a rendering exception which could occur in rare circumstandes. [#4588](https://github.com/scalableminds/webknossos/pull/4588) +- Fixed a rendering exception which could occur in rare circumstances. [#4588](https://github.com/scalableminds/webknossos/pull/4588) ## [20.04.0](https://github.com/scalableminds/webknossos/releases/tag/20.04.0) - 2020-03-23 [Commits](https://github.com/scalableminds/webknossos/compare/20.03.0...20.04.0) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index a65288c2569..a0b418c6868 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -12,9 +12,10 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released ### Added -- +- Added a warning to the segmentation tab when viewing `uint64` bit segmentation data. [#4598](https://github.com/scalableminds/webknossos/pull/4598) ### Changed + - ### Fixed diff --git a/app/models/annotation/AnnotationService.scala b/app/models/annotation/AnnotationService.scala index 65402a813b6..e7054aa4608 100755 --- a/app/models/annotation/AnnotationService.scala +++ b/app/models/annotation/AnnotationService.scala @@ -14,6 +14,7 @@ import com.scalableminds.util.tools.{BoxImplicits, Fox, FoxImplicits, TextUtils} import com.scalableminds.webknossos.tracingstore.SkeletonTracing._ import com.scalableminds.webknossos.tracingstore.VolumeTracing.{VolumeTracing, VolumeTracingOpt, VolumeTracings} import com.scalableminds.webknossos.datastore.models.datasource.{ + ElementClass, DataSourceLike => DataSource, SegmentationLayerLike => SegmentationLayer } @@ -102,18 +103,11 @@ class AnnotationService @Inject()(annotationInformationProvider: AnnotationInfor private def createVolumeTracing( dataSource: DataSource, - withFallback: Boolean, + fallbackLayer: Option[SegmentationLayer], boundingBox: Option[BoundingBox] = None, startPosition: Option[Point3D] = None, startRotation: Option[Vector3D] = None - ): VolumeTracing = { - val fallbackLayer: Option[SegmentationLayer] = if (withFallback) { - dataSource.dataLayers.flatMap { - case layer: SegmentationLayer => Some(layer) - case _ => None - }.headOption - } else None - + ): VolumeTracing = VolumeTracing( None, boundingBoxToProto(boundingBox.getOrElse(dataSource.boundingBox)), @@ -127,14 +121,21 @@ class AnnotationService @Inject()(annotationInformationProvider: AnnotationInfor 0, VolumeTracingDefaults.zoomLevel ) - } def createTracings( dataSet: DataSet, dataSource: DataSource, tracingType: TracingType.Value, withFallback: Boolean, - oldTracingId: Option[String] = None)(implicit ctx: DBAccessContext): Fox[(Option[String], Option[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 + tracingType match { case TracingType.skeleton => for { @@ -148,23 +149,29 @@ class AnnotationService @Inject()(annotationInformationProvider: AnnotationInfor case TracingType.volume => for { client <- tracingStoreService.clientFor(dataSet) - volumeTracingId <- client.saveVolumeTracing(createVolumeTracing(dataSource, withFallback)) + fallbackLayer = getFallbackLayer() + _ <- bool2Fox(fallbackLayer.forall(_.elementClass != ElementClass.uint64)) ?~> "annotation.volume.uint64" + volumeTracingId <- client.saveVolumeTracing(createVolumeTracing(dataSource, fallbackLayer)) } yield (None, Some(volumeTracingId)) case TracingType.hybrid => for { client <- tracingStoreService.clientFor(dataSet) + fallbackLayer = getFallbackLayer() + _ <- bool2Fox(fallbackLayer.forall(_.elementClass != ElementClass.uint64)) ?~> "annotation.volume.uint64" skeletonTracingId <- client.saveSkeletonTracing( SkeletonTracingDefaults.createInstance.copy(dataSetName = dataSet.name, editPosition = dataSource.center)) - volumeTracingId <- client.saveVolumeTracing(createVolumeTracing(dataSource, withFallback)) + volumeTracingId <- client.saveVolumeTracing(createVolumeTracing(dataSource, fallbackLayer)) } yield (Some(skeletonTracingId), Some(volumeTracingId)) } + } def createExplorationalFor(user: User, _dataSet: ObjectId, tracingType: TracingType.Value, withFallback: Boolean)( - implicit ctx: DBAccessContext): Fox[Annotation] = + implicit ctx: DBAccessContext, + m: MessagesProvider): Fox[Annotation] = for { - dataSet <- dataSetDAO.findOne(_dataSet) + dataSet <- dataSetDAO.findOne(_dataSet) ?~> "dataSet.noAccessById" dataSource <- dataSetService.dataSourceFor(dataSet) - usableDataSource <- dataSource.toUsable ?~> "DataSet is not imported." + usableDataSource <- dataSource.toUsable ?~> Messages("dataSet.notImported", dataSource.id.name) tracingIds <- createTracings(dataSet, usableDataSource, tracingType, withFallback) teamId <- selectSuitableTeam(user, dataSet) annotation = Annotation( @@ -338,9 +345,18 @@ class AnnotationService @Inject()(annotationInformationProvider: AnnotationInfor dataSet <- dataSetDAO.findOneByNameAndOrganization(dataSetName, organizationId) ?~> Messages("dataset.notFound", dataSetName) dataSource <- dataSetService.dataSourceFor(dataSet).flatMap(_.toUsable) + + fallbackLayer = if (volumeShowFallbackLayer) { + dataSource.dataLayers.flatMap { + case layer: SegmentationLayer => Some(layer) + case _ => None + }.headOption + } else None + _ <- bool2Fox(fallbackLayer.forall(_.elementClass != ElementClass.uint64)) ?~> "annotation.volume.uint64" + volumeTracing = createVolumeTracing( dataSource, - withFallback = volumeShowFallbackLayer, + fallbackLayer = fallbackLayer, boundingBox = boundingBox.flatMap { box => if (box.isEmpty) None else Some(box) }, diff --git a/app/models/binary/DataSetService.scala b/app/models/binary/DataSetService.scala index 50d1de7d964..78b265ca244 100644 --- a/app/models/binary/DataSetService.scala +++ b/app/models/binary/DataSetService.scala @@ -321,13 +321,12 @@ class DataSetService @Inject()(organizationDAO: OrganizationDAO, lastUsedByUser <- lastUsedTimeFor(dataSet._id, requestingUserOpt) dataStoreJs <- dataStoreService.publicWrites(dataStore) dataSource <- dataSourceFor(dataSet, Some(organization), skipResolutions) - dataSourceWith64BitSupport = dataSource.toUsable.map(replaceUint64Layers).getOrElse(dataSource) publicationOpt <- Fox.runOptional(dataSet._publication)(publicationDAO.findOne(_)) publicationJson <- Fox.runOptional(publicationOpt)(publicationService.publicWrites) } yield { Json.obj( "name" -> dataSet.name, - "dataSource" -> dataSourceWith64BitSupport, + "dataSource" -> dataSource, "dataStore" -> dataStoreJs, "owningOrganization" -> organization.name, "allowedTeams" -> teamsJs, @@ -346,16 +345,4 @@ class DataSetService @Inject()(organizationDAO: OrganizationDAO, "isForeign" -> dataStore.isForeign ) } - - private def replaceUint64Layers(dataSource: GenericDataSource[DataLayer]) = { - val newLayers = dataSource.dataLayers.map { - case l: WKWSegmentationLayer if l.elementClass == ElementClass.uint64 => - l.copy(elementClass = ElementClass.uint32) - case l: AbstractSegmentationLayer if l.elementClass == ElementClass.uint64 => - l.copy(elementClass = ElementClass.uint32) - case l => l - } - - dataSource.copy(dataLayers = newLayers) - } } diff --git a/conf/messages b/conf/messages index db56bf53d30..940fad9b334 100644 --- a/conf/messages +++ b/conf/messages @@ -85,6 +85,7 @@ dataSet.notFound=Dataset {0} does not exist or could not be accessed dataSet.notFoundConsiderLogin=Dataset {0} does not exist or could not be accessed. You may need to log in. dataSet.notFoundForAnnotation=The Dataset for this annotation does not exist or could not be accessed. dataSet.noAccess=Could not access DataSet {0}. Does your team have access? +dataSet.noAccessById=Could not access the corresponding DataSet. This is likely because you are not a member of a team that has access to it. dataSet.notImported=Dataset {0} is not imported dataSet.name.invalid=A dataset name can only contain letters, digits and underscores dataSet.import.impossible.name=Import impossible. Dataset name can only consist of a-Z, 0-9, "-" and "_". @@ -183,7 +184,8 @@ role.added=Added role to {0} tracing=Annotation tracing.notFound=Tracing couldn’t be found -annotation.create.failed=Failed to create annotation. This is likely because you are not a member of a team that has access to the dataset. +annotation.create.failed=Failed to create annotation. +annotation.volume.uint64=Creating volume tracings on a uint64 fallback layer is not allowed. annotation.notFound=Annotation couldn’t be found annotation.notFound.considerLoggingIn=Annotation couldn’t be found. If the annotation is not public, you need to log in to see it. annotation.invalid=Invalid annotation diff --git a/frontend/javascripts/admin/api_flow_types.js b/frontend/javascripts/admin/api_flow_types.js index 0afa20b9167..2bb4f2a9c28 100644 --- a/frontend/javascripts/admin/api_flow_types.js +++ b/frontend/javascripts/admin/api_flow_types.js @@ -54,6 +54,7 @@ export type APISegmentationLayer = {| ...APIDataLayerBase, +category: "segmentation", +largestSegmentId: number, + +originalElementClass?: ElementClass, +mappings?: Array, +agglomerates?: Array, +fallbackLayer?: ?string, diff --git a/frontend/javascripts/libs/toast.js b/frontend/javascripts/libs/toast.js index 0e7359fd81f..8171367e982 100644 --- a/frontend/javascripts/libs/toast.js +++ b/frontend/javascripts/libs/toast.js @@ -41,7 +41,7 @@ const Toast = { style={{ background: "transparent", marginLeft: -16 }} > {errorChain} diff --git a/frontend/javascripts/messages.js b/frontend/javascripts/messages.js index 07e1403ee74..b3940fae729 100644 --- a/frontend/javascripts/messages.js +++ b/frontend/javascripts/messages.js @@ -96,6 +96,8 @@ instead. Only enable this option if you understand its effect. All layers will n "You didn't add a node after jumping to this branchpoint, do you really want to jump again?", "tracing.segmentation_zoom_warning": "Segmentation data and volume annotation is only fully supported at a smaller zoom level.", + "tracing.uint64_segmentation_warning": + "This is an unsigned 64-bit segmentation. The displayed ids are truncated to 32-bit. Thus, they might not match the ids on the server.", "tracing.segmentation_zoom_warning_agglomerate": "Segmentation data which is mapped using an agglomerate file cannot be rendered in this magnification. Please zoom in further.", "tracing.no_access": "You are not allowed to access this annotation.", diff --git a/frontend/javascripts/oxalis/model_initialization.js b/frontend/javascripts/oxalis/model_initialization.js index a64de2e341b..532b19d2da9 100644 --- a/frontend/javascripts/oxalis/model_initialization.js +++ b/frontend/javascripts/oxalis/model_initialization.js @@ -295,6 +295,24 @@ function initializeDataset( dataSet: dataset.dataSource.id.name, }); + // Add the originalElementClass property to the segmentation layer if it exists. + // Also set the elementClass to uint32 because uint64 segmentation data is truncated to uint32 by the backend. + const updatedDataLayers = dataset.dataSource.dataLayers.map(dataLayer => { + const { elementClass } = dataLayer; + if (dataLayer.category === "segmentation") { + const adjustedElementClass = elementClass === "uint64" ? "uint32" : elementClass; + return { + ...dataLayer, + originalElementClass: elementClass, + elementClass: adjustedElementClass, + }; + } else { + return dataLayer; + } + }); + // $FlowFixMe assigning the adjusted dataset layers, although this property is not writable. + dataset.dataSource.dataLayers = updatedDataLayers; + serverTracingAsVolumeTracingMaybe(tracing).map(volumeTracing => { const newDataLayers = setupLayerForVolumeTracing(dataset, volumeTracing); // $FlowFixMe We mutate the dataset here to avoid that an outdated version is used somewhere else diff --git a/frontend/javascripts/oxalis/view/right-menu/mapping_info_view.js b/frontend/javascripts/oxalis/view/right-menu/mapping_info_view.js index 6a474af3fa8..73bd89ecc7a 100644 --- a/frontend/javascripts/oxalis/view/right-menu/mapping_info_view.js +++ b/frontend/javascripts/oxalis/view/right-menu/mapping_info_view.js @@ -262,12 +262,28 @@ class MappingInfoView extends React.Component { title, dataIndex, }); + const showSegmentation64bitWarning = + segmentationLayer && segmentationLayer.originalElementClass === "uint64"; + const maybeWithTooltipWarningTitle = title => + showSegmentation64bitWarning ? ( + + {title}{" "} + + + + + ) : ( + title + ); const idColumns = hasMapping && this.props.isMappingEnabled ? // Show an unmapped and mapped id column if there's a mapping - [columnHelper("Unmapped", "unmapped"), columnHelper("Mapped", "mapped")] + [ + columnHelper(maybeWithTooltipWarningTitle("Unmapped"), "unmapped"), + columnHelper(maybeWithTooltipWarningTitle("Mapped"), "mapped"), + ] : // Otherwise, only show an ID column - [columnHelper("ID", "unmapped")]; + [columnHelper(maybeWithTooltipWarningTitle("ID"), "unmapped")]; const columns = [ columnHelper("", "name"), ...idColumns,