From cb4ad63a593557c8d7c4ca2dfeb359a0be4ea082 Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 29 Nov 2018 14:41:41 +0100 Subject: [PATCH 1/9] [WIP] thumbnails for segmentation layer --- app/controllers/DataSetController.scala | 4 +- build.sbt | 1 + .../util/image/ImageCreator.scala | 105 ++++++++++-------- 3 files changed, 60 insertions(+), 50 deletions(-) diff --git a/app/controllers/DataSetController.scala b/app/controllers/DataSetController.scala index 68ee33118e3..de2da0d0fa4 100755 --- a/app/controllers/DataSetController.scala +++ b/app/controllers/DataSetController.scala @@ -54,8 +54,8 @@ class DataSetController @Inject()(userService: UserService, val width = Math.clamp(w.getOrElse(DefaultThumbnailWidth), 1, MaxThumbnailHeight) val height = Math.clamp(h.getOrElse(DefaultThumbnailHeight), 1, MaxThumbnailHeight) cache.get[Array[Byte]](s"thumbnail-$organizationName*$dataSetName*$dataLayerName-$width-$height") match { - case Some(a) => - Fox.successful(a) + /*case Some(a) => + Fox.successful(a)*/ case _ => { val defaultCenterOpt = dataSet.defaultConfiguration.flatMap(c => c.configuration.get("position").flatMap(jsValue => JsonHelper.jsResultToOpt(jsValue.validate[Point3D]))) diff --git a/build.sbt b/build.sbt index 8500f0eaa9d..39060cbda29 100644 --- a/build.sbt +++ b/build.sbt @@ -8,6 +8,7 @@ ThisBuild / scapegoatVersion := "1.3.8" ThisBuild / scalacOptions ++= Seq( "-Xmax-classfile-name","100", "-target:jvm-1.8", + "-verbose", "-feature", "-deprecation", "-language:implicitConversions", diff --git a/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala b/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala index 4ad35cd4bb6..da3d3753b18 100644 --- a/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala +++ b/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala @@ -16,16 +16,16 @@ case class CombinedImage(pages: List[CombinedPage]) case class CombinedPage(image: BufferedImage, info: List[ImagePartInfo], pageInfo: PageInfo) case class ImageCreatorParameters( - bytesPerElement: Int, - useHalfBytes: Boolean, - slideWidth: Int = 128, - slideHeight: Int = 128, - imagesPerRow: Int = 8, - imagesPerColumn: Int = Int.MaxValue, - imageWidth: Option[Int] = None, - imageHeight: Option[Int] = None, - blackAndWhite: Boolean - ) + bytesPerElement: Int, + useHalfBytes: Boolean, + slideWidth: Int = 128, + slideHeight: Int = 128, + imagesPerRow: Int = 8, + imagesPerColumn: Int = Int.MaxValue, + imageWidth: Option[Int] = None, + imageHeight: Option[Int] = None, + blackAndWhite: Boolean +) object ImageCreator extends LazyLogging { @@ -39,26 +39,28 @@ object ImageCreator extends LazyLogging { def calculateSprites(data: Array[Byte], params: ImageCreatorParameters, targetType: Int): List[BufferedImage] = { val imageData = - if(params.useHalfBytes) { + if (params.useHalfBytes) { val r = new Array[Byte](data.length * 2) - data.zipWithIndex.foreach{ + data.zipWithIndex.foreach { case (b, idx) => - r(2*idx) =(b & 0xF0).toByte - r(2*idx + 1)=(b & 0x0F << 4).toByte + r(2 * idx) = (b & 0xF0).toByte + r(2 * idx + 1) = (b & 0x0F << 4).toByte } r - } else if(params.blackAndWhite) { - data.map(d => if(d != 0x00) 0xFF.toByte else 0x00.toByte) + } else if (params.blackAndWhite) { + data.map(d => if (d != 0x00) 0xFF.toByte else 0x00.toByte) } else data + val slidingSize = params.slideHeight * params.slideWidth * params.bytesPerElement - imageData.sliding(slidingSize, slidingSize).toList.flatMap { - slice => - createBufferedImageFromBytes(slice, targetType, params) + imageData.sliding(slidingSize, slidingSize).toList.flatMap { slice => + createBufferedImageFromBytes(slice, targetType, params) } } - def createSpriteSheet(bufferedImages: List[BufferedImage], params: ImageCreatorParameters, targetType: Int): Option[CombinedImage] = { + def createSpriteSheet(bufferedImages: List[BufferedImage], + params: ImageCreatorParameters, + targetType: Int): Option[CombinedImage] = if (bufferedImages.isEmpty) { logger.warn("No images supplied for sprite sheet generation.") None @@ -67,41 +69,41 @@ object ImageCreator extends LazyLogging { val subpartHeight = params.slideHeight val imagesPerPage = math.min(params.imagesPerColumn.toLong * params.imagesPerRow, Int.MaxValue).toInt - val pages = bufferedImages.sliding(imagesPerPage, imagesPerPage).zipWithIndex.map { - case (pageImages, page) => - val depth = math.ceil(pageImages.size.toFloat / params.imagesPerRow).toInt - val imageWidth = params.imageWidth.getOrElse(subpartWidth * params.imagesPerRow) - val imageHeight = params.imageHeight.getOrElse(subpartHeight * depth) - - val finalImage = new BufferedImage(imageWidth, imageHeight, targetType) - - val info = pageImages.zipWithIndex.map { - case (image, idx) => - assert(image.getWidth() == subpartWidth, "Wrong image size!") - assert(image.getHeight() == subpartHeight, "Wrong image size!") - val w = idx % params.imagesPerRow * params.slideWidth - val h = idx / params.imagesPerRow * params.slideHeight - finalImage.createGraphics().drawImage( - image, w, h, null) - ImagePartInfo(page, w, h, subpartHeight, subpartWidth) - } - CombinedPage(finalImage, info, PageInfo(page, page * imagesPerPage, pageImages.size)) - }.toList + val pages = bufferedImages + .sliding(imagesPerPage, imagesPerPage) + .zipWithIndex + .map { + case (pageImages, page) => + val depth = math.ceil(pageImages.size.toFloat / params.imagesPerRow).toInt + val imageWidth = params.imageWidth.getOrElse(subpartWidth * params.imagesPerRow) + val imageHeight = params.imageHeight.getOrElse(subpartHeight * depth) + + val finalImage = new BufferedImage(imageWidth, imageHeight, targetType) + + val info = pageImages.zipWithIndex.map { + case (image, idx) => + assert(image.getWidth() == subpartWidth, "Wrong image size!") + assert(image.getHeight() == subpartHeight, "Wrong image size!") + val w = idx % params.imagesPerRow * params.slideWidth + val h = idx / params.imagesPerRow * params.slideHeight + finalImage.createGraphics().drawImage(image, w, h, null) + ImagePartInfo(page, w, h, subpartHeight, subpartWidth) + } + CombinedPage(finalImage, info, PageInfo(page, page * imagesPerPage, pageImages.size)) + } + .toList Some(CombinedImage(pages)) } - } - def convertToType(sourceImage: BufferedImage, targetType: Int): BufferedImage = { + def convertToType(sourceImage: BufferedImage, targetType: Int): BufferedImage = sourceImage.getType() match { case e if e == targetType => sourceImage case _ => - val image = new BufferedImage(sourceImage.getWidth(), - sourceImage.getHeight(), targetType) + val image = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), targetType) image.getGraphics().drawImage(sourceImage, 0, 0, null) image } - } def toRGBArray(b: Array[Byte], bytesPerElement: Int) = { val colored = new Array[Int](b.length / bytesPerElement) @@ -129,15 +131,22 @@ object ImageCreator extends LazyLogging { colored } - def createBufferedImageFromBytes(b: Array[Byte], targetType: Int, params: ImageCreatorParameters): Option[BufferedImage] = { + def createBufferedImageFromBytes(b: Array[Byte], + targetType: Int, + params: ImageCreatorParameters): Option[BufferedImage] = try { val bufferedImage = new BufferedImage(params.slideWidth, params.slideHeight, targetType) - bufferedImage.setRGB(0, 0, params.slideWidth, params.slideHeight, toRGBArray(b, params.bytesPerElement), 0, params.slideWidth) + bufferedImage.setRGB(0, + 0, + params.slideWidth, + params.slideHeight, + toRGBArray(b, params.bytesPerElement), + 0, + params.slideWidth) Some(bufferedImage) } catch { case e: IOException => logger.error("IOException while converting byte array to buffered image.", e) None } - } } From 85b9112372ac2500729dcf9a6bdabe97fc3e7573 Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 29 Nov 2018 18:07:17 +0100 Subject: [PATCH 2/9] [WIP] add segmentation thumbnail and blend it over normal thumbnail on hover #3489 --- .../javascripts/dashboard/dataset_panel.js | 28 +++++++++++++++- .../model/accessors/dataset_accessor.js | 12 +++++++ app/assets/stylesheets/_dashboard.less | 7 ++++ .../util/image/ImageCreator.scala | 32 ++++++++++++++++++- 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/dashboard/dataset_panel.js b/app/assets/javascripts/dashboard/dataset_panel.js index c4da6b65b5e..0d808ddabb1 100644 --- a/app/assets/javascripts/dashboard/dataset_panel.js +++ b/app/assets/javascripts/dashboard/dataset_panel.js @@ -6,7 +6,11 @@ import _ from "lodash"; import type { APIDataset } from "admin/api_flow_types"; import { formatScale } from "libs/format_utils"; -import { getThumbnailURL, hasSegmentation } from "oxalis/model/accessors/dataset_accessor"; +import { + getThumbnailURL, + hasSegmentation, + getSegmentationThumbnailURL, +} from "oxalis/model/accessors/dataset_accessor"; const columnSpan = { xs: 24, sm: 24, md: 24, lg: 12, xl: 12, xxl: 8 }; const thumbnailDimension = 500; @@ -57,10 +61,12 @@ function ThumbnailAndDescription({ thumbnailURL, description, name, + segmentationThumbnailURL, }: { thumbnailURL: string, name: string, description: React.Element<*> | string, + segmentationThumbnailURL: ?string, }) { return ( @@ -74,6 +80,20 @@ function ThumbnailAndDescription({ height: "100%", }} /> + {segmentationThumbnailURL ? ( +
+ ) : null}
@@ -91,6 +111,9 @@ function ThumbnailAndDescriptionFromDataset({ dataset }: { dataset: APIDataset } thumbnailURL={getThumbnailURL(dataset)} name={getDisplayName(dataset)} description={getDescription(dataset)} + segmentationThumbnailURL={ + hasSegmentation(dataset) ? getSegmentationThumbnailURL(dataset) : null + } /> ); } @@ -149,6 +172,9 @@ class DatasetPanel extends React.PureComponent { thumbnailURL={getThumbnailURL(datasets[0])} name={groupName} description={multiDescription} + segmentationThumbnailURL={ + hasSegmentation(datasets[0]) ? getSegmentationThumbnailURL(datasets[0]) : null + } /> ); diff --git a/app/assets/javascripts/oxalis/model/accessors/dataset_accessor.js b/app/assets/javascripts/oxalis/model/accessors/dataset_accessor.js index a8b2a112276..ad667e0b6c3 100644 --- a/app/assets/javascripts/oxalis/model/accessors/dataset_accessor.js +++ b/app/assets/javascripts/oxalis/model/accessors/dataset_accessor.js @@ -183,4 +183,16 @@ export function getThumbnailURL(dataset: APIDataset): string { return ""; } +export function getSegmentationThumbnailURL(dataset: APIDataset): string { + const datasetName = dataset.name; + const organizationName = dataset.owningOrganization; + const segmentationLayer = getSegmentationLayer(dataset); + if (segmentationLayer) { + return `/api/datasets/${organizationName}/${datasetName}/layers/${ + segmentationLayer.name + }/thumbnail`; + } + return ""; +} + export default {}; diff --git a/app/assets/stylesheets/_dashboard.less b/app/assets/stylesheets/_dashboard.less index 5b9064402cc..b84abb04aee 100644 --- a/app/assets/stylesheets/_dashboard.less +++ b/app/assets/stylesheets/_dashboard.less @@ -32,6 +32,13 @@ transition: all 0.3s ease-in-out; } + .dataset-thumbnail-image.segmentation { + opacity: 0; + &:hover { + opacity: 0.2; + } + } + @media @smartphones { height: 55%; width: 100%; diff --git a/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala b/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala index da3d3753b18..6e82567b150 100644 --- a/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala +++ b/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala @@ -121,7 +121,8 @@ object ImageCreator extends LazyLogging { case 3 => (0xFF << 24) | ((b(idx) & 0xFF) << 16) | ((b(idx + 1) & 0xFF) << 8) | ((b(idx + 2) & 0xFF) << 0) case 4 => - ((b(idx + 3) & 0xFF) << 24) | ((b(idx) & 0xFF) << 16) | ((b(idx + 1) & 0xFF) << 8) | ((b(idx + 2) & 0xFF) << 0) + toTestRGB(b(idx)) + //((b(idx + 3) & 0xFF) << 24) | ((b(idx) & 0xFF) << 16) | ((b(idx + 1) & 0xFF) << 8) | ((b(idx + 2) & 0xFF) << 0) case _ => throw new Exception("Can't handle " + bytesPerElement + " bytes per element in Image creator.") } @@ -131,6 +132,35 @@ object ImageCreator extends LazyLogging { colored } + def hsvToRgb(hsv: Array[Double]): Int = { + val K = Array(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0) + val p = Array.ofDim[Double](3) + for (i <- 0 to 2) { + val x = hsv(0) + K(i) + val decimal = x.toInt + val fract = x - decimal + p(i) = Math.abs(fract * 6.0 - K(3)) + } + val returnVal = Array.ofDim[Double](3) + for (i <- 0 to 2) { + val x = K(0) + val cl = com.scalableminds.util.tools.Math.clamp(p(i) - K(0), 0.0, 1.0) + val ret = x * (1 - hsv(1)) + cl * hsv(1) + returnVal(i) = hsv(2) * ret + } + val end = returnVal.map(x => (x * 255).toByte) + (0xFF << 24) | ((end(0) & 0xFF) << 16) | ((end(1) & 0xFF) << 8) | ((end(2) & 0xFF) << 0) + } + + def toTestRGB(b: Byte) = + b match { + case 0 => (0x64 << 24) | (0x64 << 16) | (0x64 << 8) | (0x64 << 0) + case _ => + val golden_ratio = 0.618033988749895 + val value = ((b & 0xFF) * golden_ratio) % 1.0 + hsvToRgb(Array(value, 1.0, 1.0, 1.0)) + } + def createBufferedImageFromBytes(b: Array[Byte], targetType: Int, params: ImageCreatorParameters): Option[BufferedImage] = From fb53d2b0d9a1f1e58624fe33a3950412c4f0afb2 Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 6 Dec 2018 10:25:00 +0100 Subject: [PATCH 3/9] update snapshots #3507 --- .../test/enzyme/snapshot.e2e.js.md | 20 +++++++++++------- .../test/enzyme/snapshot.e2e.js.snap | Bin 31393 -> 31512 bytes 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/test/snapshots/public/test-bundle/test/enzyme/snapshot.e2e.js.md b/app/assets/javascripts/test/snapshots/public/test-bundle/test/enzyme/snapshot.e2e.js.md index 4e715e9ba8b..17ff8030c53 100644 --- a/app/assets/javascripts/test/snapshots/public/test-bundle/test/enzyme/snapshot.e2e.js.md +++ b/app/assets/javascripts/test/snapshots/public/test-bundle/test/enzyme/snapshot.e2e.js.md @@ -121,9 +121,10 @@ Generated by [AVA](https://ava.li).
␊ - ␊ +
␊ +
␊ ␊
␊ @@ -158,7 +159,7 @@ Generated by [AVA](https://ava.li).
␊ - ␊ +
␊ ␊ @@ -348,9 +349,10 @@ Generated by [AVA](https://ava.li).
␊ - ␊ +
␊ +
␊ ␊
␊ @@ -385,7 +387,7 @@ Generated by [AVA](https://ava.li).
␊ - ␊ +
␊ ␊ @@ -1594,9 +1596,10 @@ Generated by [AVA](https://ava.li).
␊ - ␊ +
␊ +
␊ ␊
␊ @@ -1631,7 +1634,7 @@ Generated by [AVA](https://ava.li).
␊ - ␊ +
␊ ␊ @@ -3827,9 +3830,10 @@ Generated by [AVA](https://ava.li).
␊ - ␊ +
␊ +
␊ ␊
␊ @@ -3864,7 +3868,7 @@ Generated by [AVA](https://ava.li).
␊ - ␊ +
␊ ␊ diff --git a/app/assets/javascripts/test/snapshots/public/test-bundle/test/enzyme/snapshot.e2e.js.snap b/app/assets/javascripts/test/snapshots/public/test-bundle/test/enzyme/snapshot.e2e.js.snap index 79d27bbd90e71f4af2be85a2f6e54d37500f05e8..1225e5cd40082db25f5b1c3d05f8a328f3fab449 100644 GIT binary patch delta 28160 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 delta 27999 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 From 975b28a1c0b216d7a2f9a08b8db42755dc890ac2 Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 6 Dec 2018 13:19:47 +0100 Subject: [PATCH 4/9] improve implementation for hsvTorgb #3507 --- .../util/image/ImageCreator.scala | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala b/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala index 6e82567b150..f942cb1ecef 100644 --- a/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala +++ b/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala @@ -121,8 +121,7 @@ object ImageCreator extends LazyLogging { case 3 => (0xFF << 24) | ((b(idx) & 0xFF) << 16) | ((b(idx + 1) & 0xFF) << 8) | ((b(idx + 2) & 0xFF) << 0) case 4 => - toTestRGB(b(idx)) - //((b(idx + 3) & 0xFF) << 24) | ((b(idx) & 0xFF) << 16) | ((b(idx + 1) & 0xFF) << 8) | ((b(idx + 2) & 0xFF) << 0) + idToRGB(b(idx)) case _ => throw new Exception("Can't handle " + bytesPerElement + " bytes per element in Image creator.") } @@ -132,34 +131,35 @@ object ImageCreator extends LazyLogging { colored } - def hsvToRgb(hsv: Array[Double]): Int = { - val K = Array(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0) - val p = Array.ofDim[Double](3) - for (i <- 0 to 2) { - val x = hsv(0) + K(i) - val decimal = x.toInt - val fract = x - decimal - p(i) = Math.abs(fract * 6.0 - K(3)) - } - val returnVal = Array.ofDim[Double](3) - for (i <- 0 to 2) { - val x = K(0) - val cl = com.scalableminds.util.tools.Math.clamp(p(i) - K(0), 0.0, 1.0) - val ret = x * (1 - hsv(1)) + cl * hsv(1) - returnVal(i) = hsv(2) * ret + def idToRGB(b: Byte) = { + def hueToRGB(h: Double): Int = { + + val i: Double = Math.floor(h * 6f) + val f: Double = h * 6f - i + + val (r, g, b) = i % 6 match { + case 0 => (1.0, f, 0.0) + case 1 => (1.0 - f, 1.0, 0.0) + case 2 => (0.0, 1.0, f) + case 3 => (0.0, 1.0 - f, 1.0) + case 4 => (f, 0.0, 1.0) + case 5 => (1.0, 0.0, 1.0 - f) + } + + val rByte = (r * 255).toByte + val gByte = (g * 255).toByte + val bByte = (b * 255).toByte + (0xFF << 24) | ((rByte & 0xFF) << 16) | ((gByte & 0xFF) << 8) | ((bByte & 0xFF) << 0) } - val end = returnVal.map(x => (x * 255).toByte) - (0xFF << 24) | ((end(0) & 0xFF) << 16) | ((end(1) & 0xFF) << 8) | ((end(2) & 0xFF) << 0) - } - def toTestRGB(b: Byte) = b match { case 0 => (0x64 << 24) | (0x64 << 16) | (0x64 << 8) | (0x64 << 0) case _ => val golden_ratio = 0.618033988749895 - val value = ((b & 0xFF) * golden_ratio) % 1.0 - hsvToRgb(Array(value, 1.0, 1.0, 1.0)) + val hue = ((b & 0xFF) * golden_ratio) % 1.0 + hueToRGB(hue) } + } def createBufferedImageFromBytes(b: Array[Byte], targetType: Int, From 3578d04d5d2a1a3e720a83f631a2c5163f3b753c Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 6 Dec 2018 13:23:10 +0100 Subject: [PATCH 5/9] remove development changes #3507 --- app/controllers/DataSetController.scala | 4 ++-- build.sbt | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/controllers/DataSetController.scala b/app/controllers/DataSetController.scala index de2da0d0fa4..68ee33118e3 100755 --- a/app/controllers/DataSetController.scala +++ b/app/controllers/DataSetController.scala @@ -54,8 +54,8 @@ class DataSetController @Inject()(userService: UserService, val width = Math.clamp(w.getOrElse(DefaultThumbnailWidth), 1, MaxThumbnailHeight) val height = Math.clamp(h.getOrElse(DefaultThumbnailHeight), 1, MaxThumbnailHeight) cache.get[Array[Byte]](s"thumbnail-$organizationName*$dataSetName*$dataLayerName-$width-$height") match { - /*case Some(a) => - Fox.successful(a)*/ + case Some(a) => + Fox.successful(a) case _ => { val defaultCenterOpt = dataSet.defaultConfiguration.flatMap(c => c.configuration.get("position").flatMap(jsValue => JsonHelper.jsResultToOpt(jsValue.validate[Point3D]))) diff --git a/build.sbt b/build.sbt index 39060cbda29..8500f0eaa9d 100644 --- a/build.sbt +++ b/build.sbt @@ -8,7 +8,6 @@ ThisBuild / scapegoatVersion := "1.3.8" ThisBuild / scalacOptions ++= Seq( "-Xmax-classfile-name","100", "-target:jvm-1.8", - "-verbose", "-feature", "-deprecation", "-language:implicitConversions", From eb067a1b70d629a05079b9338eb13bcb4695f3b3 Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 6 Dec 2018 13:31:42 +0100 Subject: [PATCH 6/9] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 037ba0f1868..aca616f0f10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.md). ### Added - Added the possibility to specify a recommended user configuration in a task type. The recommended configuration will be shown to users when they trace a task with a different task type and the configuration can be accepted or declined. [#3466](https://github.com/scalableminds/webknossos/pull/3466) +- Added a second thumbnail for the datasets which have a segmentation layer and this segmentation thumbnail will be shown on hover over the other thumbnail. [#3507](https://github.com/scalableminds/webknossos/pull/3507) ### Changed From 2e120b30ebf582c8b237419d2f898a08729fb325 Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 6 Dec 2018 14:01:45 +0100 Subject: [PATCH 7/9] also make a colorful segmentation thumbnail for 2 byte segmentations #3507 --- .../scala/com/scalableminds/util/image/ImageCreator.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala b/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala index f942cb1ecef..42ebacf93d4 100644 --- a/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala +++ b/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala @@ -116,8 +116,9 @@ object ImageCreator extends LazyLogging { val gray = b(idx) (0xFF << 24) | ((gray & 0xFF) << 16) | ((gray & 0xFF) << 8) | ((gray & 0xFF) << 0) case 2 => - val gray = b(idx) - (b(idx + 1) << 24) | ((gray & 0xFF) << 16) | ((gray & 0xFF) << 8) | ((gray & 0xFF) << 0) + idToRGB(b(idx)) + //val gray = b(idx) + //(b(idx + 1) << 24) | ((gray & 0xFF) << 16) | ((gray & 0xFF) << 8) | ((gray & 0xFF) << 0) case 3 => (0xFF << 24) | ((b(idx) & 0xFF) << 16) | ((b(idx + 1) & 0xFF) << 8) | ((b(idx + 2) & 0xFF) << 0) case 4 => From a9fd26573942ea7a0009754b2cb9ba695c04b60a Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 6 Dec 2018 15:16:11 +0100 Subject: [PATCH 8/9] make overlay work at the same time as the zoom effect and support 8 byte segmentations #3507 --- app/assets/stylesheets/_dashboard.less | 22 +++++++++---------- .../util/image/ImageCreator.scala | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/assets/stylesheets/_dashboard.less b/app/assets/stylesheets/_dashboard.less index b84abb04aee..121a0a08fa7 100644 --- a/app/assets/stylesheets/_dashboard.less +++ b/app/assets/stylesheets/_dashboard.less @@ -28,17 +28,6 @@ background-color: #e8e8e8; position: relative; - .dataset-thumbnail-image { - transition: all 0.3s ease-in-out; - } - - .dataset-thumbnail-image.segmentation { - opacity: 0; - &:hover { - opacity: 0.2; - } - } - @media @smartphones { height: 55%; width: 100%; @@ -46,6 +35,17 @@ } } +.dataset-thumbnail-image { + transition: all 0.3s ease-in-out; + &.segmentation { + opacity: 0; + } +} + +.spotlight-item-card:hover .dataset-thumbnail-image.segmentation { + opacity: 0.2; +} + .dataset-description { position: absolute; display: inline-block; diff --git a/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala b/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala index 42ebacf93d4..1a61ac3e6c8 100644 --- a/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala +++ b/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala @@ -117,12 +117,12 @@ object ImageCreator extends LazyLogging { (0xFF << 24) | ((gray & 0xFF) << 16) | ((gray & 0xFF) << 8) | ((gray & 0xFF) << 0) case 2 => idToRGB(b(idx)) - //val gray = b(idx) - //(b(idx + 1) << 24) | ((gray & 0xFF) << 16) | ((gray & 0xFF) << 8) | ((gray & 0xFF) << 0) case 3 => (0xFF << 24) | ((b(idx) & 0xFF) << 16) | ((b(idx + 1) & 0xFF) << 8) | ((b(idx + 2) & 0xFF) << 0) case 4 => idToRGB(b(idx)) + case 8 => + idToRGB(b(idx)) case _ => throw new Exception("Can't handle " + bytesPerElement + " bytes per element in Image creator.") } From f9461b213f4e53d559ee186dfbf2127f044d9531 Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 20 Dec 2018 11:27:05 +0100 Subject: [PATCH 9/9] check if requested layer is segmentation so only there the data gets interpreted as id #3507 --- .../util/image/ImageCreator.scala | 40 +- .../controllers/BinaryDataController.scala | 505 +++++++++--------- 2 files changed, 288 insertions(+), 257 deletions(-) diff --git a/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala b/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala index 1a61ac3e6c8..62c94e38ee8 100644 --- a/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala +++ b/util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala @@ -24,7 +24,8 @@ case class ImageCreatorParameters( imagesPerColumn: Int = Int.MaxValue, imageWidth: Option[Int] = None, imageHeight: Option[Int] = None, - blackAndWhite: Boolean + blackAndWhite: Boolean, + isSegmentation: Boolean = false ) object ImageCreator extends LazyLogging { @@ -105,27 +106,30 @@ object ImageCreator extends LazyLogging { image } - def toRGBArray(b: Array[Byte], bytesPerElement: Int) = { + def toRGBArray(b: Array[Byte], bytesPerElement: Int, isSegmentation: Boolean) = { val colored = new Array[Int](b.length / bytesPerElement) var idx = 0 val l = b.length while (idx + bytesPerElement <= l) { colored(idx / bytesPerElement) = { - bytesPerElement match { - case 1 => - val gray = b(idx) - (0xFF << 24) | ((gray & 0xFF) << 16) | ((gray & 0xFF) << 8) | ((gray & 0xFF) << 0) - case 2 => - idToRGB(b(idx)) - case 3 => - (0xFF << 24) | ((b(idx) & 0xFF) << 16) | ((b(idx + 1) & 0xFF) << 8) | ((b(idx + 2) & 0xFF) << 0) - case 4 => - idToRGB(b(idx)) - case 8 => - idToRGB(b(idx)) - case _ => - throw new Exception("Can't handle " + bytesPerElement + " bytes per element in Image creator.") - } + if (isSegmentation) + idToRGB(b(idx)) + else + bytesPerElement match { + case 1 => + val gray = b(idx) + (0xFF << 24) | ((gray & 0xFF) << 16) | ((gray & 0xFF) << 8) | ((gray & 0xFF) << 0) + case 2 => + val gray = b(idx) + (b(idx + 1) << 24) | ((gray & 0xFF) << 16) | ((gray & 0xFF) << 8) | ((gray & 0xFF) << 0) + case 3 => + (0xFF << 24) | ((b(idx) & 0xFF) << 16) | ((b(idx + 1) & 0xFF) << 8) | ((b(idx + 2) & 0xFF) << 0) + case 4 => + ((b(idx + 3) & 0xFF) << 24) | ((b(idx) & 0xFF) << 16) | ((b(idx + 1) & 0xFF) << 8) | ((b(idx + 2) & 0xFF) << 0) + case _ => + throw new Exception( + "Can't handle " + bytesPerElement + " bytes per element in Image creator for a color layer.") + } } idx += bytesPerElement } @@ -171,7 +175,7 @@ object ImageCreator extends LazyLogging { 0, params.slideWidth, params.slideHeight, - toRGBArray(b, params.bytesPerElement), + toRGBArray(b, params.bytesPerElement, params.isSegmentation), 0, params.slideWidth) Some(bufferedImage) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/BinaryDataController.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/BinaryDataController.scala index ddc879cce0f..547d9854040 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/BinaryDataController.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/BinaryDataController.scala @@ -9,8 +9,12 @@ import com.google.inject.Inject import com.scalableminds.util.geometry.Point3D import com.scalableminds.webknossos.datastore.services._ import com.scalableminds.webknossos.datastore.models._ -import com.scalableminds.webknossos.datastore.models.datasource.{DataLayer, DataSource, DataSourceId, SegmentationLayer} -import com.scalableminds.webknossos.datastore.models.requests.{DataServiceDataRequest, DataServiceMappingRequest, DataServiceRequestSettings} +import com.scalableminds.webknossos.datastore.models.datasource._ +import com.scalableminds.webknossos.datastore.models.requests.{ + DataServiceDataRequest, + DataServiceMappingRequest, + DataServiceRequestSettings +} import com.scalableminds.webknossos.datastore.models.DataRequestCollection._ import com.scalableminds.webknossos.datastore.models.{DataRequest, ImageThumbnail, WebKnossosDataRequest} import com.scalableminds.util.image.{ImageCreator, ImageCreatorParameters, JPEGWriter} @@ -26,13 +30,11 @@ import play.api.mvc.{PlayBodyParsers, ResponseHeader, Result} import scala.concurrent.{ExecutionContext, Future} class BinaryDataController @Inject()( - dataSourceRepository: DataSourceRepository, - config: DataStoreConfig, - accessTokenService: DataStoreAccessTokenService, - binaryDataServiceHolder: BinaryDataServiceHolder) - (implicit ec: ExecutionContext, - bodyParsers: PlayBodyParsers) - extends Controller { + dataSourceRepository: DataSourceRepository, + config: DataStoreConfig, + accessTokenService: DataStoreAccessTokenService, + binaryDataServiceHolder: BinaryDataServiceHolder)(implicit ec: ExecutionContext, bodyParsers: PlayBodyParsers) + extends Controller { val binaryDataService = binaryDataServiceHolder.binaryDataService @@ -40,279 +42,302 @@ class BinaryDataController @Inject()( * Handles requests for raw binary data via HTTP POST from webKnossos. */ def requestViaWebKnossos( - organizationName: String, - dataSetName: String, - dataLayerName: String - ) = Action.async(validateJson[List[WebKnossosDataRequest]]) { - implicit request => - accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName))) { - AllowRemoteOrigin { - for { - (dataSource, dataLayer) <- getDataSourceAndDataLayer(organizationName, dataSetName, dataLayerName) - (data, indices) <- requestData(dataSource, dataLayer, request.body) - } yield Ok(data).withHeaders(getMissingBucketsHeaders(indices): _*) - } + organizationName: String, + dataSetName: String, + dataLayerName: String + ) = Action.async(validateJson[List[WebKnossosDataRequest]]) { implicit request => + accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName))) { + AllowRemoteOrigin { + for { + (dataSource, dataLayer) <- getDataSourceAndDataLayer(organizationName, dataSetName, dataLayerName) + (data, indices) <- requestData(dataSource, dataLayer, request.body) + } yield Ok(data).withHeaders(getMissingBucketsHeaders(indices): _*) } + } } - def getMissingBucketsHeaders(indices: List[Int]): Seq[(String, String)] = { - List(("MISSING-BUCKETS" -> formatMissingBucketList(indices)), ("Access-Control-Expose-Headers" -> "MISSING-BUCKETS")) - } + def getMissingBucketsHeaders(indices: List[Int]): Seq[(String, String)] = + List(("MISSING-BUCKETS" -> formatMissingBucketList(indices)), + ("Access-Control-Expose-Headers" -> "MISSING-BUCKETS")) - def formatMissingBucketList(indices: List[Int]): String = { + def formatMissingBucketList(indices: List[Int]): String = "[" + indices.mkString(", ") + "]" - } /** * Handles requests for raw binary data via HTTP GET. */ def requestRawCuboid( - organizationName: String, - dataSetName: String, - dataLayerName: String, - x: Int, - y: Int, - z: Int, - width: Int, - height: Int, - depth: Int, - resolution: Int, - halfByte: Boolean - ) = Action.async { - implicit request => - accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName))) { - AllowRemoteOrigin { - for { - (dataSource, dataLayer) <- getDataSourceAndDataLayer(organizationName, dataSetName, dataLayerName) - request = DataRequest( - new VoxelPosition(x, y, z, dataLayer.lookUpResolution(resolution)), - width, - height, - depth, - DataServiceRequestSettings(halfByte = halfByte) - ) - (data, indices) <- requestData(dataSource, dataLayer, request) - } yield Ok(data).withHeaders(getMissingBucketsHeaders(indices): _*) - } + organizationName: String, + dataSetName: String, + dataLayerName: String, + x: Int, + y: Int, + z: Int, + width: Int, + height: Int, + depth: Int, + resolution: Int, + halfByte: Boolean + ) = Action.async { implicit request => + accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName))) { + AllowRemoteOrigin { + for { + (dataSource, dataLayer) <- getDataSourceAndDataLayer(organizationName, dataSetName, dataLayerName) + request = DataRequest( + new VoxelPosition(x, y, z, dataLayer.lookUpResolution(resolution)), + width, + height, + depth, + DataServiceRequestSettings(halfByte = halfByte) + ) + (data, indices) <- requestData(dataSource, dataLayer, request) + } yield Ok(data).withHeaders(getMissingBucketsHeaders(indices): _*) } + } } /** * Handles requests for raw binary data via HTTP GET for debugging. */ def requestViaAjaxDebug( - organizationName: String, - dataSetName: String, - dataLayerName: String, - cubeSize: Int, - x: Int, - y: Int, - z: Int, - resolution: Int, - halfByte: Boolean - ) = - requestRawCuboid(organizationName, dataSetName, dataLayerName, x, y, z, cubeSize, cubeSize, cubeSize, resolution, halfByte) + organizationName: String, + dataSetName: String, + dataLayerName: String, + cubeSize: Int, + x: Int, + y: Int, + z: Int, + resolution: Int, + halfByte: Boolean + ) = + requestRawCuboid(organizationName, + dataSetName, + dataLayerName, + x, + y, + z, + cubeSize, + cubeSize, + cubeSize, + resolution, + halfByte) /** * Handles a request for raw binary data via a HTTP GET. Used by knossos. */ - def requestViaKnossos( - organizationName: String, - dataSetName: String, - dataLayerName: String, - resolution: Int, - x: Int, y: Int, z: Int, - cubeSize: Int) = Action.async { - implicit request => - accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName))) { - AllowRemoteOrigin { - for { - (dataSource, dataLayer) <- getDataSourceAndDataLayer(organizationName, dataSetName, dataLayerName) - request = DataRequest( - new VoxelPosition(x * cubeSize * resolution, - y * cubeSize * resolution, - z * cubeSize * resolution, - Point3D(resolution, resolution, resolution)), - cubeSize, - cubeSize, - cubeSize) - (data, indices) <- requestData(dataSource, dataLayer, request) - } yield Ok(data).withHeaders(getMissingBucketsHeaders(indices): _*) - } + def requestViaKnossos(organizationName: String, + dataSetName: String, + dataLayerName: String, + resolution: Int, + x: Int, + y: Int, + z: Int, + cubeSize: Int) = Action.async { implicit request => + accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName))) { + AllowRemoteOrigin { + for { + (dataSource, dataLayer) <- getDataSourceAndDataLayer(organizationName, dataSetName, dataLayerName) + request = DataRequest( + new VoxelPosition(x * cubeSize * resolution, + y * cubeSize * resolution, + z * cubeSize * resolution, + Point3D(resolution, resolution, resolution)), + cubeSize, + cubeSize, + cubeSize + ) + (data, indices) <- requestData(dataSource, dataLayer, request) + } yield Ok(data).withHeaders(getMissingBucketsHeaders(indices): _*) } + } } /** * Handles requests for data sprite sheets. */ def requestSpriteSheet( - organizationName: String, - dataSetName: String, - dataLayerName: String, - cubeSize: Int, - imagesPerRow: Int, - x: Int, - y: Int, - z: Int, - resolution: Int, - halfByte: Boolean - ) = Action.async(parse.raw) { - implicit request => - accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName))) { - AllowRemoteOrigin { - for { - (dataSource, dataLayer) <- getDataSourceAndDataLayer(organizationName, dataSetName, dataLayerName) - dataRequest = DataRequest( - new VoxelPosition(x, y, z, dataLayer.lookUpResolution(resolution)), - cubeSize, - cubeSize, - cubeSize, - DataServiceRequestSettings(halfByte = halfByte)) - imageProvider <- respondWithSpriteSheet(dataSource, dataLayer, dataRequest, imagesPerRow, blackAndWhite = false) - } yield { - Result( - header = ResponseHeader(200), - body = HttpEntity.Streamed(StreamConverters.asOutputStream().mapMaterializedValue { - outputStream => imageProvider(outputStream) - }, None, Some(contentTypeJpeg))) - } + organizationName: String, + dataSetName: String, + dataLayerName: String, + cubeSize: Int, + imagesPerRow: Int, + x: Int, + y: Int, + z: Int, + resolution: Int, + halfByte: Boolean + ) = Action.async(parse.raw) { implicit request => + accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName))) { + AllowRemoteOrigin { + for { + (dataSource, dataLayer) <- getDataSourceAndDataLayer(organizationName, dataSetName, dataLayerName) + dataRequest = DataRequest(new VoxelPosition(x, y, z, dataLayer.lookUpResolution(resolution)), + cubeSize, + cubeSize, + cubeSize, + DataServiceRequestSettings(halfByte = halfByte)) + imageProvider <- respondWithSpriteSheet(dataSource, + dataLayer, + dataRequest, + imagesPerRow, + blackAndWhite = false) + } yield { + Result( + header = ResponseHeader(200), + body = HttpEntity.Streamed(StreamConverters.asOutputStream().mapMaterializedValue { outputStream => + imageProvider(outputStream) + }, None, Some(contentTypeJpeg)) + ) } } + } } /** * Handles requests for data images. */ - def requestImage( - organizationName: String, - dataSetName: String, - dataLayerName: String, - width: Int, - height: Int, - x: Int, - y: Int, - z: Int, - resolution: Int, - halfByte: Boolean, - blackAndWhite: Boolean) = Action.async(parse.raw) { - implicit request => - accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName))) { - AllowRemoteOrigin { - for { - (dataSource, dataLayer) <- getDataSourceAndDataLayer(organizationName, dataSetName, dataLayerName) - dataRequest = DataRequest( - new VoxelPosition(x, y, z, dataLayer.lookUpResolution(resolution)), - width, - height, - 1, - DataServiceRequestSettings(halfByte = halfByte)) - imageProvider <- respondWithSpriteSheet(dataSource, dataLayer, dataRequest, 1, blackAndWhite) - } yield { - Result( - header = ResponseHeader(200), - body = HttpEntity.Streamed(StreamConverters.asOutputStream().mapMaterializedValue { - outputStream => imageProvider(outputStream) - }, None, Some(contentTypeJpeg))) - } + def requestImage(organizationName: String, + dataSetName: String, + dataLayerName: String, + width: Int, + height: Int, + x: Int, + y: Int, + z: Int, + resolution: Int, + halfByte: Boolean, + blackAndWhite: Boolean) = Action.async(parse.raw) { implicit request => + accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName))) { + AllowRemoteOrigin { + for { + (dataSource, dataLayer) <- getDataSourceAndDataLayer(organizationName, dataSetName, dataLayerName) + dataRequest = DataRequest(new VoxelPosition(x, y, z, dataLayer.lookUpResolution(resolution)), + width, + height, + 1, + DataServiceRequestSettings(halfByte = halfByte)) + imageProvider <- respondWithSpriteSheet(dataSource, dataLayer, dataRequest, 1, blackAndWhite) + } yield { + Result( + header = ResponseHeader(200), + body = HttpEntity.Streamed(StreamConverters.asOutputStream().mapMaterializedValue { outputStream => + imageProvider(outputStream) + }, None, Some(contentTypeJpeg)) + ) } } + } } /** * Handles requests for dataset thumbnail images as JPEG. */ - def requestImageThumbnailJpeg( - organizationName: String, - dataSetName: String, - dataLayerName: String, - width: Int, - height: Int, - centerX: Option[Int], - centerY: Option[Int], - centerZ: Option[Int], - zoom: Option[Double]) = Action.async(parse.raw) { - implicit request => - accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName))) { - AllowRemoteOrigin { - for { - thumbnailProvider <- respondWithImageThumbnail(organizationName, dataSetName, dataLayerName, width, height, centerX, centerY, centerZ, zoom) - } yield { - Result( - header = ResponseHeader(200), - body = HttpEntity.Streamed(StreamConverters.asOutputStream().mapMaterializedValue { - outputStream => thumbnailProvider(outputStream) - }, None, Some(contentTypeJpeg))) - } + def requestImageThumbnailJpeg(organizationName: String, + dataSetName: String, + dataLayerName: String, + width: Int, + height: Int, + centerX: Option[Int], + centerY: Option[Int], + centerZ: Option[Int], + zoom: Option[Double]) = Action.async(parse.raw) { implicit request => + accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName))) { + AllowRemoteOrigin { + for { + thumbnailProvider <- respondWithImageThumbnail(organizationName, + dataSetName, + dataLayerName, + width, + height, + centerX, + centerY, + centerZ, + zoom) + } yield { + Result( + header = ResponseHeader(200), + body = HttpEntity.Streamed(StreamConverters.asOutputStream().mapMaterializedValue { outputStream => + thumbnailProvider(outputStream) + }, None, Some(contentTypeJpeg)) + ) } } + } } /** * Handles requests for dataset thumbnail images as base64-encoded JSON. */ def requestImageThumbnailJson( - organizationName: String, - dataSetName: String, - dataLayerName: String, - width: Int, - height: Int, - centerX: Option[Int], - centerY: Option[Int], - centerZ: Option[Int], - zoom: Option[Double] - ) = Action.async(parse.raw) { - implicit request => - accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName))) { - AllowRemoteOrigin { - for { - thumbnailProvider <- respondWithImageThumbnail(organizationName, dataSetName, dataLayerName, width, height, centerX, centerY, centerZ, zoom) - } yield { - val os = new ByteArrayOutputStream() - thumbnailProvider(Base64.getEncoder.wrap(os)) - Ok(Json.toJson(ImageThumbnail(contentTypeJpeg, os.toString))) - } + organizationName: String, + dataSetName: String, + dataLayerName: String, + width: Int, + height: Int, + centerX: Option[Int], + centerY: Option[Int], + centerZ: Option[Int], + zoom: Option[Double] + ) = Action.async(parse.raw) { implicit request => + accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName))) { + AllowRemoteOrigin { + for { + thumbnailProvider <- respondWithImageThumbnail(organizationName, + dataSetName, + dataLayerName, + width, + height, + centerX, + centerY, + centerZ, + zoom) + } yield { + val os = new ByteArrayOutputStream() + thumbnailProvider(Base64.getEncoder.wrap(os)) + Ok(Json.toJson(ImageThumbnail(contentTypeJpeg, os.toString))) } } + } } /** * Handles mapping requests. */ def requestMapping( - organizationName: String, - dataSetName: String, - dataLayerName: String, - mappingName: String - ) = Action.async { - implicit request => - accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName))) { - AllowRemoteOrigin { - for { - (dataSource, dataLayer) <- getDataSourceAndDataLayer(organizationName, dataSetName, dataLayerName) - segmentationLayer <- tryo(dataLayer.asInstanceOf[SegmentationLayer]).toFox ?~> Messages("dataLayer.notFound") - mappingRequest = DataServiceMappingRequest(dataSource, segmentationLayer, mappingName) - result <- binaryDataService.handleMappingRequest(mappingRequest) - } yield { - Ok(result) - } + organizationName: String, + dataSetName: String, + dataLayerName: String, + mappingName: String + ) = Action.async { implicit request => + accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName))) { + AllowRemoteOrigin { + for { + (dataSource, dataLayer) <- getDataSourceAndDataLayer(organizationName, dataSetName, dataLayerName) + segmentationLayer <- tryo(dataLayer.asInstanceOf[SegmentationLayer]).toFox ?~> Messages("dataLayer.notFound") + mappingRequest = DataServiceMappingRequest(dataSource, segmentationLayer, mappingName) + result <- binaryDataService.handleMappingRequest(mappingRequest) + } yield { + Ok(result) } } + } } - private def getDataSourceAndDataLayer(organizationName: String, dataSetName: String, dataLayerName: String)(implicit m: MessagesProvider): Fox[(DataSource, DataLayer)] = { + private def getDataSourceAndDataLayer(organizationName: String, dataSetName: String, dataLayerName: String)( + implicit m: MessagesProvider): Fox[(DataSource, DataLayer)] = for { - dataSource <- dataSourceRepository.findUsable(DataSourceId(dataSetName, organizationName)).toFox ?~> Messages("dataSource.notFound") ~> 404 + dataSource <- dataSourceRepository.findUsable(DataSourceId(dataSetName, organizationName)).toFox ?~> Messages( + "dataSource.notFound") ~> 404 dataLayer <- dataSource.getDataLayer(dataLayerName) ?~> Messages("dataLayer.notFound", dataLayerName) ~> 404 } yield { (dataSource, dataLayer) } - } private def requestData( - dataSource: DataSource, - dataLayer: DataLayer, - dataRequests: DataRequestCollection - ): Fox[(Array[Byte], List[Int])] = { + dataSource: DataSource, + dataLayer: DataLayer, + dataRequests: DataRequestCollection + ): Fox[(Array[Byte], List[Int])] = { val requests = dataRequests.map(r => DataServiceDataRequest(dataSource, dataLayer, r.cuboid(dataLayer), r.settings)) binaryDataService.handleDataRequests(requests) } @@ -320,22 +345,26 @@ class BinaryDataController @Inject()( private def contentTypeJpeg = "image/jpeg" private def respondWithSpriteSheet( - dataSource: DataSource, - dataLayer: DataLayer, - request: DataRequest, - imagesPerRow: Int, - blackAndWhite: Boolean - )(implicit m: MessagesProvider): Fox[(OutputStream) => Unit] = { + dataSource: DataSource, + dataLayer: DataLayer, + request: DataRequest, + imagesPerRow: Int, + blackAndWhite: Boolean + )(implicit m: MessagesProvider): Fox[(OutputStream) => Unit] = { val params = ImageCreatorParameters( dataLayer.bytesPerElement, request.settings.halfByte, request.cuboid(dataLayer).width, request.cuboid(dataLayer).height, imagesPerRow, - blackAndWhite = blackAndWhite) + blackAndWhite = blackAndWhite, + isSegmentation = dataLayer.category == Category.segmentation + ) for { (data, indices) <- requestData(dataSource, dataLayer, request) - dataWithFallback = if (data.length == 0) new Array[Byte](params.slideHeight * params.slideWidth * params.bytesPerElement) else data + dataWithFallback = if (data.length == 0) + new Array[Byte](params.slideHeight * params.slideWidth * params.bytesPerElement) + else data spriteSheet <- ImageCreator.spriteSheetFor(dataWithFallback, params) ?~> Messages("image.create.failed") firstSheet <- spriteSheet.pages.headOption ?~> Messages("image.page.failed") } yield { @@ -344,16 +373,16 @@ class BinaryDataController @Inject()( } private def respondWithImageThumbnail( - organizationName: String, - dataSetName: String, - dataLayerName: String, - width: Int, - height: Int, - centerX: Option[Int], - centerY: Option[Int], - centerZ: Option[Int], - zoom: Option[Double] - )(implicit m: MessagesProvider): Fox[(OutputStream) => Unit] = { + organizationName: String, + dataSetName: String, + dataLayerName: String, + width: Int, + height: Int, + centerX: Option[Int], + centerY: Option[Int], + centerZ: Option[Int], + zoom: Option[Double] + )(implicit m: MessagesProvider): Fox[(OutputStream) => Unit] = for { (dataSource, dataLayer) <- getDataSourceAndDataLayer(organizationName, dataSetName, dataLayerName) position = ImageThumbnail.goodThumbnailParameters(dataLayer, width, height, centerX, centerY, centerZ, zoom) @@ -362,16 +391,14 @@ class BinaryDataController @Inject()( } yield { image } - } - def clearCache(organizationName: String, dataSetName: String) = Action.async { - implicit request => - accessTokenService.validateAccess(UserAccessRequest.administrateDataSources) { - AllowRemoteOrigin { - val count = binaryDataService.clearCache(organizationName, dataSetName) - Future.successful(Ok("Closed " + count + " file handles")) - } + def clearCache(organizationName: String, dataSetName: String) = Action.async { implicit request => + accessTokenService.validateAccess(UserAccessRequest.administrateDataSources) { + AllowRemoteOrigin { + val count = binaryDataService.clearCache(organizationName, dataSetName) + Future.successful(Ok("Closed " + count + " file handles")) } + } } }