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

Automatically assign color and channel name for omero ngff #7251

Merged
merged 9 commits into from
Aug 24, 2023
3 changes: 2 additions & 1 deletion CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- OpenID Connect authorization can now use a client secret for added security. [#7263](https://github.com/scalableminds/webknossos/pull/7263)

### Changed
- Small messages during annotating (e.g. “finished undo”, “applying mapping…”) are now click-through so they do not block users from selecting tools. [7239](https://github.com/scalableminds/webknossos/pull/7239)
- Small messages during annotating (e.g. “finished undo”, “applying mapping…”) are now click-through, so they do not block users from selecting tools. [#7239](https://github.com/scalableminds/webknossos/pull/7239)
- When exploring remote NGFF datasets with channels, layer names and colors are automatically imported if available in the metadata. [#7251](https://github.com/scalableminds/webknossos/pull/7251)
- Annotating volume data uses a transaction-based mechanism now. As a result, WK is more robust against partial saves (i.e., due to a crashing tab). [#7264](https://github.com/scalableminds/webknossos/pull/7264)
- Improved speed of saving volume data. [#7264](https://github.com/scalableminds/webknossos/pull/7264)
- Improved progress indicator when saving volume data. [#7264](https://github.com/scalableminds/webknossos/pull/7264)
Expand Down
2 changes: 1 addition & 1 deletion MIGRATIONS.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ UPDATE webknossos.multiUsers SET isEmailVerified = false;

- When interacting with webknossos via the python library, make sure you update to the latest version, as the task and project api have changed. Compare [webknossos-libs#930](https://github.com/scalableminds/webknossos-libs/pull/930). [#7220](https://github.com/scalableminds/webknossos/pull/7220)

- If you have OIDC authentication set up, you can now remove the config keys `singleSignOn.openIdConnect.publicKey` and `singleSignOn.openIdConnect.publicKeyAlgorithm`, as the server’s public key is now automatically fetched. [7267](https://github.com/scalableminds/webknossos/pull/7267)
- If you have OIDC authentication set up, you can now remove the config keys `singleSignOn.openIdConnect.publicKey` and `singleSignOn.openIdConnect.publicKeyAlgorithm`, as the server’s public key is now automatically fetched. [#7267](https://github.com/scalableminds/webknossos/pull/7267)

### Postgres Evolutions:
- [105-verify-email.sql](conf/evolutions/105-verify-email.sql)
Expand Down
54 changes: 48 additions & 6 deletions app/models/binary/explore/NgffExplorer.scala
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package models.binary.explore

import com.scalableminds.util.geometry.{Vec3Double, Vec3Int}
import com.scalableminds.util.tools.Fox
import com.scalableminds.util.image.Color
import com.scalableminds.util.tools.{Fox, TextUtils}
import com.scalableminds.webknossos.datastore.dataformats.MagLocator
import com.scalableminds.webknossos.datastore.dataformats.zarr.{ZarrDataLayer, ZarrLayer, ZarrSegmentationLayer}
import com.scalableminds.webknossos.datastore.datareaders.AxisOrder
import com.scalableminds.webknossos.datastore.datareaders.zarr._
import com.scalableminds.webknossos.datastore.datavault.VaultPath
import com.scalableminds.webknossos.datastore.models.datasource.{Category, ElementClass}
import com.scalableminds.webknossos.datastore.models.datasource.LayerViewConfiguration.LayerViewConfiguration
import com.scalableminds.webknossos.datastore.models.datasource.{Category, ElementClass, LayerViewConfiguration}
import play.api.libs.json.{JsArray, JsNumber}

import scala.concurrent.ExecutionContext

Expand All @@ -25,7 +28,8 @@ class NgffExplorer(implicit val ec: ExecutionContext) extends RemoteLayerExplore
layerLists: List[List[(ZarrLayer, Vec3Double)]] <- Fox.serialCombined(ngffHeader.multiscales)(multiscale => {
for {
channelCount: Int <- getNgffMultiscaleChannelCount(multiscale, remotePath)
layers <- layersFromNgffMultiscale(multiscale, remotePath, credentialId, channelCount)
channelAttributes = getChannelAttributes(ngffHeader)
layers <- layersFromNgffMultiscale(multiscale, remotePath, credentialId, channelCount, channelAttributes)
} yield layers
})
layers: List[(ZarrLayer, Vec3Double)] = layerLists.flatten
Expand All @@ -48,6 +52,7 @@ class NgffExplorer(implicit val ec: ExecutionContext) extends RemoteLayerExplore
remotePath: VaultPath,
credentialId: Option[String],
channelCount: Int,
channelAttributes: Option[Seq[ChannelAttributes]] = None,
isSegmentation: Boolean = false): Fox[List[(ZarrLayer, Vec3Double)]] =
for {
axisOrder <- extractAxisOrder(multiscale.axes) ?~> "Could not extract XYZ axis order mapping. Does the data have x, y and z axes, stated in multiscales metadata?"
Expand All @@ -66,18 +71,55 @@ class NgffExplorer(implicit val ec: ExecutionContext) extends RemoteLayerExplore
elementClassRaw <- elementClassFromMags(magsWithAttributes) ?~> "Could not extract element class from mags"
elementClass = if (isSegmentation) ensureElementClassForSegmentationLayer(elementClassRaw)
else elementClassRaw

(viewConfig: LayerViewConfiguration, channelName: String) = channelAttributes match {
case Some(attributes) => {
val color = attributes(channelIndex).color
val attributeName: String =
attributes(channelIndex).name
.map(TextUtils.normalizeStrong(_).getOrElse(name).replaceAll(" ", ""))
.getOrElse(name)
(color match {
case Some(c) => Seq(("color" -> JsArray(c.toArrayOfInts.map(i => JsNumber(BigDecimal(i)))))).toMap
case None => LayerViewConfiguration.empty
}, attributeName)
}
case None => (LayerViewConfiguration.empty, name)
}

boundingBox = boundingBoxFromMags(magsWithAttributes)
layer: ZarrLayer = if (looksLikeSegmentationLayer(name, elementClass) || isSegmentation) {
ZarrSegmentationLayer(name,
ZarrSegmentationLayer(channelName,
boundingBox,
elementClass,
magsWithAttributes.map(_.mag),
largestSegmentId = None)
} else ZarrDataLayer(name, Category.color, boundingBox, elementClass, magsWithAttributes.map(_.mag))
largestSegmentId = None,
defaultViewConfiguration = Some(viewConfig))
} else
ZarrDataLayer(channelName,
Category.color,
boundingBox,
elementClass,
magsWithAttributes.map(_.mag),
defaultViewConfiguration = Some(viewConfig))
} yield (layer, voxelSizeNanometers)
})
} yield layerTuples

private case class ChannelAttributes(color: Option[Color], name: Option[String])

private def getChannelAttributes(
ngffHeader: NgffMetadata
): Option[Seq[ChannelAttributes]] =
ngffHeader.omero match {
case Some(value) =>
Some(
value.channels.map(omeroChannelAttributes =>
ChannelAttributes(omeroChannelAttributes.color.map(Color.fromHTML(_).getOrElse(Color(1, 1, 1, 0))),
omeroChannelAttributes.label)))
case None => None
}

private def exploreLabelLayers(remotePath: VaultPath,
credentialId: Option[String]): Fox[List[(ZarrLayer, Vec3Double)]] =
for {
Expand Down
13 changes: 12 additions & 1 deletion util/src/main/scala/com/scalableminds/util/image/Color.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.scalableminds.util.image

import com.scalableminds.util.tools.ExtendedTypes._
import net.liftweb.common.Box.tryo
import play.api.libs.json.Json._
import play.api.libs.json.{Format, JsValue, _}

case class Color(r: Double, g: Double, b: Double, a: Double) {
def toHtml = "#%02x%02x%02x".format((r * 255).toInt, (g * 255).toInt, (b * 255).toInt)
def toHtml: String = "#%02x%02x%02x".format((r * 255).toInt, (g * 255).toInt, (b * 255).toInt)
def toArrayOfInts: Array[Int] = Array((r * 255).toInt, (g * 255).toInt, (b * 255).toInt)
}

object Color {
Expand All @@ -21,6 +23,15 @@ object Color {
)
}

def fromHTML(htmlCode: String): Option[Color] =
tryo({
val code = if (!htmlCode.startsWith("#")) s"#$htmlCode" else htmlCode
val r = Integer.valueOf(code.substring(1, 3), 16) / 255d
val g = Integer.valueOf(code.substring(3, 5), 16) / 255d
val b = Integer.valueOf(code.substring(5, 7), 16) / 255d
Color(r, g, b, 0)
}).toOption

implicit object ColorFormat extends Format[Color] {

def writes(c: Color) = if (c.a == 1) Json.arr(c.r, c.g, c.b) else Json.arr(c.r, c.g, c.b, c.a)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ object NgffMultiscalesItem {
implicit val jsonFormat: OFormat[NgffMultiscalesItem] = Json.format[NgffMultiscalesItem]
}

case class NgffMetadata(multiscales: List[NgffMultiscalesItem])
case class NgffMetadata(multiscales: List[NgffMultiscalesItem], omero: Option[NgffOmeroMetadata])

object NgffMetadata {
def fromNameScaleAndMags(dataLayerName: String, dataSourceScale: Vec3Double, mags: List[Vec3Int]): NgffMetadata = {
Expand All @@ -93,7 +93,7 @@ object NgffMetadata {
NgffDataset(path = mag.toMagLiteral(allowScalar = true),
List(NgffCoordinateTransformation(
scale = Some(List[Double](1.0) ++ (dataSourceScale * Vec3Double(mag)).toList)))))
NgffMetadata(multiscales = List(NgffMultiscalesItem(name = Some(dataLayerName), datasets = datasets)))
NgffMetadata(multiscales = List(NgffMultiscalesItem(name = Some(dataLayerName), datasets = datasets)), None)
}

implicit val jsonFormat: OFormat[NgffMetadata] = Json.format[NgffMetadata]
Expand All @@ -107,3 +107,13 @@ object NgffLabelsGroup {
implicit val jsonFormat: OFormat[NgffLabelsGroup] = Json.format[NgffLabelsGroup]
val LABEL_PATH = "labels/.zattrs"
}

case class NgffOmeroMetadata(channels: List[NgffChannelAttributes])
object NgffOmeroMetadata {
implicit val jsonFormat: OFormat[NgffOmeroMetadata] = Json.format[NgffOmeroMetadata]
}

case class NgffChannelAttributes(color: Option[String], label: Option[String])
object NgffChannelAttributes {
implicit val jsonFormat: OFormat[NgffChannelAttributes] = Json.format[NgffChannelAttributes]
}
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ object ElementClass extends ExtendedEnumeration {
object LayerViewConfiguration {
type LayerViewConfiguration = Map[String, JsValue]

def empty: LayerViewConfiguration = Map()

implicit val jsonFormat: Format[LayerViewConfiguration] = Format.of[LayerViewConfiguration]
}

Expand Down