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

Segmentation thumbnail #3507

Merged
merged 12 commits into from
Jan 10, 2019
1 change: 1 addition & 0 deletions CHANGELOG.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.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

Expand Down
28 changes: 27 additions & 1 deletion app/assets/javascripts/dashboard/dataset_panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,10 +61,12 @@ function ThumbnailAndDescription({
thumbnailURL,
description,
name,
segmentationThumbnailURL,
}: {
thumbnailURL: string,
name: string,
description: React.Element<*> | string,
segmentationThumbnailURL: ?string,
}) {
return (
<React.Fragment>
Expand All @@ -74,6 +80,20 @@ function ThumbnailAndDescription({
height: "100%",
}}
/>
{segmentationThumbnailURL ? (
<div
className="dataset-thumbnail-image segmentation"
style={{
background: `url('${segmentationThumbnailURL}?w=${thumbnailDimension}&h=${thumbnailDimension}')`,
backgroundSize: "cover",
width: "100%",
height: "100%",
position: "absolute",
left: "0",
top: "0",
}}
/>
) : null}
</span>
<div className="dataset-description">
<div className="description-flex">
Expand All @@ -91,6 +111,9 @@ function ThumbnailAndDescriptionFromDataset({ dataset }: { dataset: APIDataset }
thumbnailURL={getThumbnailURL(dataset)}
name={getDisplayName(dataset)}
description={getDescription(dataset)}
segmentationThumbnailURL={
hasSegmentation(dataset) ? getSegmentationThumbnailURL(dataset) : null
}
/>
);
}
Expand Down Expand Up @@ -149,6 +172,9 @@ class DatasetPanel extends React.PureComponent<Props, State> {
thumbnailURL={getThumbnailURL(datasets[0])}
name={groupName}
description={multiDescription}
segmentationThumbnailURL={
hasSegmentation(datasets[0]) ? getSegmentationThumbnailURL(datasets[0]) : null
}
/>
</Card>
);
Expand Down
12 changes: 12 additions & 0 deletions app/assets/javascripts/oxalis/model/accessors/dataset_accessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {};
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,10 @@ Generated by [AVA](https://ava.li).
<div className="ant-card spotlight-item-card ant-card-bordered">␊
<div className="ant-card-body" style={{...}}>␊
<ThumbnailAndDescriptionFromDataset dataset={{...}}>␊
<ThumbnailAndDescription thumbnailURL="/api/datasets/Organization_X/e2006_knossos/layers/color/thumbnail" name="e2006_knossos" description={{...}}>␊
<ThumbnailAndDescription thumbnailURL="/api/datasets/Organization_X/e2006_knossos/layers/color/thumbnail" name="e2006_knossos" description={{...}} segmentationThumbnailURL="/api/datasets/Organization_X/e2006_knossos/layers/segmentation/thumbnail">␊
<span className="dataset-thumbnail" title="Click to view dataset">␊
<div className="dataset-thumbnail-image" style={{...}} />␊
<div className="dataset-thumbnail-image segmentation" style={{...}} />␊
</span>␊
<div className="dataset-description">␊
<div className="description-flex">␊
Expand Down Expand Up @@ -158,7 +159,7 @@ Generated by [AVA](https://ava.li).
<div className="ant-card spotlight-item-card ant-card-bordered">␊
<div className="ant-card-body" style={{...}}>␊
<ThumbnailAndDescriptionFromDataset dataset={{...}}>␊
<ThumbnailAndDescription thumbnailURL="/api/datasets/Organization_X/confocal-multi_knossos/layers/color_1/thumbnail" name="confocal-multi_knossos" description={{...}}>␊
<ThumbnailAndDescription thumbnailURL="/api/datasets/Organization_X/confocal-multi_knossos/layers/color_1/thumbnail" name="confocal-multi_knossos" description={{...}} segmentationThumbnailURL={{...}}>␊
<span className="dataset-thumbnail" title="Click to view dataset">␊
<div className="dataset-thumbnail-image" style={{...}} />␊
</span>␊
Expand Down Expand Up @@ -348,9 +349,10 @@ Generated by [AVA](https://ava.li).
<div className="ant-card spotlight-item-card ant-card-bordered">␊
<div className="ant-card-body" style={{...}}>␊
<ThumbnailAndDescriptionFromDataset dataset={{...}}>␊
<ThumbnailAndDescription thumbnailURL="/api/datasets/Organization_X/e2006_knossos/layers/color/thumbnail" name="e2006_knossos" description={{...}}>␊
<ThumbnailAndDescription thumbnailURL="/api/datasets/Organization_X/e2006_knossos/layers/color/thumbnail" name="e2006_knossos" description={{...}} segmentationThumbnailURL="/api/datasets/Organization_X/e2006_knossos/layers/segmentation/thumbnail">␊
<span className="dataset-thumbnail" title="Click to view dataset">␊
<div className="dataset-thumbnail-image" style={{...}} />␊
<div className="dataset-thumbnail-image segmentation" style={{...}} />␊
</span>␊
<div className="dataset-description">␊
<div className="description-flex">␊
Expand Down Expand Up @@ -385,7 +387,7 @@ Generated by [AVA](https://ava.li).
<div className="ant-card spotlight-item-card ant-card-bordered">␊
<div className="ant-card-body" style={{...}}>␊
<ThumbnailAndDescriptionFromDataset dataset={{...}}>␊
<ThumbnailAndDescription thumbnailURL="/api/datasets/Organization_X/confocal-multi_knossos/layers/color_1/thumbnail" name="confocal-multi_knossos" description={{...}}>␊
<ThumbnailAndDescription thumbnailURL="/api/datasets/Organization_X/confocal-multi_knossos/layers/color_1/thumbnail" name="confocal-multi_knossos" description={{...}} segmentationThumbnailURL={{...}}>␊
<span className="dataset-thumbnail" title="Click to view dataset">␊
<div className="dataset-thumbnail-image" style={{...}} />␊
</span>␊
Expand Down Expand Up @@ -1594,9 +1596,10 @@ Generated by [AVA](https://ava.li).
<div className="ant-card spotlight-item-card ant-card-bordered">␊
<div className="ant-card-body" style={{...}}>␊
<ThumbnailAndDescriptionFromDataset dataset={{...}}>␊
<ThumbnailAndDescription thumbnailURL="/api/datasets/Organization_X/e2006_knossos/layers/color/thumbnail" name="e2006_knossos" description={{...}}>␊
<ThumbnailAndDescription thumbnailURL="/api/datasets/Organization_X/e2006_knossos/layers/color/thumbnail" name="e2006_knossos" description={{...}} segmentationThumbnailURL="/api/datasets/Organization_X/e2006_knossos/layers/segmentation/thumbnail">␊
<span className="dataset-thumbnail" title="Click to view dataset">␊
<div className="dataset-thumbnail-image" style={{...}} />␊
<div className="dataset-thumbnail-image segmentation" style={{...}} />␊
</span>␊
<div className="dataset-description">␊
<div className="description-flex">␊
Expand Down Expand Up @@ -1631,7 +1634,7 @@ Generated by [AVA](https://ava.li).
<div className="ant-card spotlight-item-card ant-card-bordered">␊
<div className="ant-card-body" style={{...}}>␊
<ThumbnailAndDescriptionFromDataset dataset={{...}}>␊
<ThumbnailAndDescription thumbnailURL="/api/datasets/Organization_X/confocal-multi_knossos/layers/color_1/thumbnail" name="confocal-multi_knossos" description={{...}}>␊
<ThumbnailAndDescription thumbnailURL="/api/datasets/Organization_X/confocal-multi_knossos/layers/color_1/thumbnail" name="confocal-multi_knossos" description={{...}} segmentationThumbnailURL={{...}}>␊
<span className="dataset-thumbnail" title="Click to view dataset">␊
<div className="dataset-thumbnail-image" style={{...}} />␊
</span>␊
Expand Down Expand Up @@ -3827,9 +3830,10 @@ Generated by [AVA](https://ava.li).
<div className="ant-card spotlight-item-card ant-card-bordered">␊
<div className="ant-card-body" style={{...}}>␊
<ThumbnailAndDescriptionFromDataset dataset={{...}}>␊
<ThumbnailAndDescription thumbnailURL="/api/datasets/Organization_X/e2006_knossos/layers/color/thumbnail" name="e2006_knossos" description={{...}}>␊
<ThumbnailAndDescription thumbnailURL="/api/datasets/Organization_X/e2006_knossos/layers/color/thumbnail" name="e2006_knossos" description={{...}} segmentationThumbnailURL="/api/datasets/Organization_X/e2006_knossos/layers/segmentation/thumbnail">␊
<span className="dataset-thumbnail" title="Click to view dataset">␊
<div className="dataset-thumbnail-image" style={{...}} />␊
<div className="dataset-thumbnail-image segmentation" style={{...}} />␊
</span>␊
<div className="dataset-description">␊
<div className="description-flex">␊
Expand Down Expand Up @@ -3864,7 +3868,7 @@ Generated by [AVA](https://ava.li).
<div className="ant-card spotlight-item-card ant-card-bordered">␊
<div className="ant-card-body" style={{...}}>␊
<ThumbnailAndDescriptionFromDataset dataset={{...}}>␊
<ThumbnailAndDescription thumbnailURL="/api/datasets/Organization_X/confocal-multi_knossos/layers/color_1/thumbnail" name="confocal-multi_knossos" description={{...}}>␊
<ThumbnailAndDescription thumbnailURL="/api/datasets/Organization_X/confocal-multi_knossos/layers/color_1/thumbnail" name="confocal-multi_knossos" description={{...}} segmentationThumbnailURL={{...}}>␊
<span className="dataset-thumbnail" title="Click to view dataset">␊
<div className="dataset-thumbnail-image" style={{...}} />␊
</span>␊
Expand Down
Binary file not shown.
7 changes: 7 additions & 0 deletions app/assets/stylesheets/_dashboard.less
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
Expand Down
137 changes: 88 additions & 49 deletions util/src/main/scala/com/scalableminds/util/image/ImageCreator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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
Expand All @@ -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)
Expand All @@ -119,7 +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 =>
((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.")
}
Expand All @@ -129,15 +131,52 @@ object ImageCreator extends LazyLogging {
colored
}

def createBufferedImageFromBytes(b: Array[Byte], targetType: Int, params: ImageCreatorParameters): Option[BufferedImage] = {
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)
}

b match {
case 0 => (0x64 << 24) | (0x64 << 16) | (0x64 << 8) | (0x64 << 0)
case _ =>
val golden_ratio = 0.618033988749895
val hue = ((b & 0xFF) * golden_ratio) % 1.0
hueToRGB(hue)
}
}

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
}
}
}