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

Add possibility to edit min and max of intensity range #4630

Merged
merged 13 commits into from
Jul 8, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released

### Added

- Added the possibility to adjust the minimum and maximum value of the histogram for a layer. This option can be opened in the top right corner of the histogram. [#4630](https://github.com/scalableminds/webknossos/pull/4630)

- Added a warning to the segmentation tab when viewing `uint64` bit segmentation data. [#4598](https://github.com/scalableminds/webknossos/pull/4598)

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ function SettingsReducer(state: OxalisState, action: Action): OxalisState {
intensityRange,
isDisabled: false,
isInverted: false,
isInEditMode: false,
},
initialLayerSettings[layer.name],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ export default function* loadHistogramData(): Saga<void> {
newIntensityRange = [minimumInHistogramData, maximumInHistogramData];
}
yield* put(updateLayerSettingAction(layerName, "intensityRange", newIntensityRange));
// Here we also set the minium and maximum values for the intensity range that the user can enter.
// If values already exist, we skip this step.
if (layerConfigurations[layerName] == null || layerConfigurations[layerName].min == null) {
yield* put(updateLayerSettingAction(layerName, "min", minimumInHistogramData));
}
if (layerConfigurations[layerName] == null || layerConfigurations[layerName].max == null) {
yield* put(updateLayerSettingAction(layerName, "max", maximumInHistogramData));
}
}
yield* put(setHistogramDataAction(histograms));
}
3 changes: 3 additions & 0 deletions frontend/javascripts/oxalis/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,11 @@ export type DatasetLayerConfiguration = {|
+contrast: number,
+alpha: number,
+intensityRange: Vector2,
+min?: number,
+max?: number,
+isDisabled: boolean,
+isInverted: boolean,
+isInEditMode: boolean,
|};

export type LoadingStrategy = "BEST_QUALITY_FIRST" | "PROGRESSIVE_QUALITY";
Expand Down
44 changes: 38 additions & 6 deletions frontend/javascripts/oxalis/view/settings/dataset_settings_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,27 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps> {
);
};

getEditMinMaxButton = (layerName: string, isInEditMode: boolean) => {
const tooltipText = isInEditMode
? "Stop editing the possible range of the histogram."
: "Manually set the possible range of the histogram.";
return (
<Tooltip title={tooltipText}>
<Icon
type="edit"
onClick={() => this.props.onChangeLayer(layerName, "isInEditMode", !isInEditMode)}
style={{
position: "absolute",
top: 4,
right: 30,
cursor: "pointer",
color: isInEditMode ? "rgb(24, 144, 255)" : null,
}}
/>
</Tooltip>
);
};

setVisibilityForAllLayers = (isVisible: boolean) => {
const { layers } = this.props.datasetConfiguration;
Object.keys(layers).forEach(otherLayerName =>
Expand Down Expand Up @@ -165,9 +186,8 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps> {
);

getHistogram = (layerName: string, layer: DatasetLayerConfiguration) => {
const { intensityRange } = layer;
const { intensityRange, min, max, isInEditMode } = layer;
const defaultIntensityRange = getDefaultIntensityRangeOfLayer(this.props.dataset, layerName);
const highestRangeValue = defaultIntensityRange[1];
let histograms = [];
if (this.props.histogramData && this.props.histogramData[layerName]) {
histograms = this.props.histogramData[layerName];
Expand All @@ -176,8 +196,8 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps> {
{
numberOfElements: 0,
elementCounts: [],
min: 0,
max: highestRangeValue,
min: defaultIntensityRange[0],
max: defaultIntensityRange[1],
},
];
}
Expand All @@ -186,14 +206,19 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps> {
data={histograms}
intensityRangeMin={intensityRange[0]}
intensityRangeMax={intensityRange[1]}
min={min}
max={max}
isInEditMode={isInEditMode}
layerName={layerName}
defaultMinMax={defaultIntensityRange}
/>
);
};

getLayerSettingsHeader = (
isDisabled: boolean,
isColorLayer: boolean,
isInEditMode: boolean,
layerName: string,
elementClass: string,
) => {
Expand Down Expand Up @@ -225,6 +250,7 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps> {
{!isColorLayer && isVolumeTracing ? "Volume Layer" : layerName}
</span>
<Tag style={{ cursor: "default", marginLeft: 8 }}>{elementClass}</Tag>
{this.getEditMinMaxButton(layerName, isInEditMode)}
{this.getFindDataButton(layerName, isDisabled, isColorLayer)}
{this.getReloadDataButton(layerName)}
</Col>
Expand All @@ -242,11 +268,17 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps> {
return null;
}
const elementClass = getElementClass(this.props.dataset, layerName);
const { isDisabled } = layerConfiguration;
const { isDisabled, isInEditMode } = layerConfiguration;

return (
<div key={layerName}>
{this.getLayerSettingsHeader(isDisabled, isColorLayer, layerName, elementClass)}
{this.getLayerSettingsHeader(
isDisabled,
isColorLayer,
isInEditMode,
layerName,
elementClass,
)}
{isDisabled ? null : (
<React.Fragment>
{isHistogramSupported(elementClass) && layerName != null && isColorLayer
Expand Down
152 changes: 131 additions & 21 deletions frontend/javascripts/oxalis/view/settings/histogram_view.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @flow

import { Slider } from "antd";
import { Slider, Row, Col, InputNumber, Tooltip } from "antd";
import * as _ from "lodash";
import * as React from "react";
import type { Dispatch } from "redux";
Expand All @@ -9,29 +9,38 @@ import { type DatasetLayerConfiguration } from "oxalis/store";
import { updateLayerSettingAction } from "oxalis/model/actions/settings_actions";
import { type ElementClass } from "admin/api_flow_types";
import type { APIHistogramData } from "admin/api_flow_types";
import { type Vector3 } from "oxalis/constants";
import type { Vector3, Vector2 } from "oxalis/constants";
import { roundTo } from "libs/utils";

type OwnProps = {|
data: APIHistogramData,
layerName: string,
intensityRangeMin: number,
intensityRangeMax: number,
min?: number,
max?: number,
isInEditMode: boolean,
defaultMinMax: Vector2,
|};

type HistogramProps = {
...OwnProps,
onChangeLayer: (
layerName: string,
propertyName: $Keys<DatasetLayerConfiguration>,
value: [number, number],
value: [number, number] | number,
) => void,
};

const uint24Colors = [[255, 65, 54], [46, 204, 64], [24, 144, 255]];
const canvasHeight = 100;
const canvasWidth = 300;

function isANumber(value: number | string): boolean {
// Code from https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number
return !isNaN(parseFloat(value)) && isFinite(value);
}

export function isHistogramSupported(elementClass: ElementClass): boolean {
return ["int8", "uint8", "int16", "uint16", "float", "uint24"].includes(elementClass);
}
Expand All @@ -55,18 +64,41 @@ class Histogram extends React.PureComponent<HistogramProps> {
this.updateCanvas();
}

getMinAndMax = () => {
const { min, max, data } = this.props;
if (min != null && max != null) {
return { min, max };
}
const dataMin = Math.min(...data.map(({ min: minOfHistPart }) => minOfHistPart));
const dataMax = Math.max(...data.map(({ max: maxOfHistPart }) => maxOfHistPart));
return { min: dataMin, max: dataMax };
};

updateCanvas() {
if (this.canvasRef == null) {
return;
}
const ctx = this.canvasRef.getContext("2d");
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
const { min, max } = this.getMinAndMax();
const { data } = this.props;
// Compute the overall maximum count, so the RGB curves are scaled correctly relative to each other
const maxValue = Math.max(...data.map(({ elementCounts }) => Math.max(...elementCounts)));
// Compute the overall maximum count, so the RGB curves are scaled correctly relative to each other.
const maxValue = Math.max(
...data.map(({ elementCounts, min: histogramMin, max: histogramMax }) => {
// We only take the elements of the array into account that are displayed.
const displayedPartStartOffset = Math.max(histogramMin, min) - histogramMin;
const displayedPartEndOffset = Math.min(histogramMax, max) - histogramMin;
// Here we scale the offsets to the range of the elements array.
const startingIndex =
(displayedPartStartOffset * elementCounts.length) / (histogramMax - histogramMin);
const endingIndex =
(displayedPartEndOffset * elementCounts.length) / (histogramMax - histogramMin);
return Math.max(...elementCounts.slice(startingIndex, endingIndex + 1));
}),
);
for (const [i, histogram] of data.entries()) {
const color = this.props.data.length > 1 ? uint24Colors[i] : uint24Colors[2];
this.drawHistogram(ctx, histogram, maxValue, color);
this.drawHistogram(ctx, histogram, maxValue, color, min, max);
}
}

Expand All @@ -75,10 +107,14 @@ class Histogram extends React.PureComponent<HistogramProps> {
histogram: $ElementType<APIHistogramData, number>,
maxValue: number,
color: Vector3,
minRange: number,
maxRange: number,
) => {
const { intensityRangeMin, intensityRangeMax } = this.props;
const { min: minRange, max: maxRange, elementCounts } = histogram;
const rangeLength = maxRange - minRange;
const { min: histogramMin, max: histogramMax, elementCounts } = histogram;
const histogramLength = histogramMax - histogramMin;
const fullLength = maxRange - minRange;
const xOffset = histogramMin - minRange;
this.drawYAxis(ctx);
ctx.fillStyle = `rgba(${color.join(",")}, 0.1)`;
ctx.strokeStyle = `rgba(${color.join(",")})`;
Expand All @@ -89,20 +125,23 @@ class Histogram extends React.PureComponent<HistogramProps> {
value => Math.log10(downscalingFactor * value + 1) * canvasHeight,
);
const activeRegion = new Path2D();
ctx.moveTo(0, downscaledData[0]);
activeRegion.moveTo(((intensityRangeMin - minRange) / rangeLength) * canvasWidth, 0);
ctx.moveTo(0, 0);
activeRegion.moveTo(((intensityRangeMin - minRange) / fullLength) * canvasWidth, 0);
for (let i = 0; i < downscaledData.length; i++) {
const x = (i / downscaledData.length) * canvasWidth;
const xValue = minRange + i * (rangeLength / downscaledData.length);
const xInHistogramScale = (i * histogramLength) / downscaledData.length;
const xInCanvasScale = ((xOffset + xInHistogramScale) * canvasWidth) / fullLength;
const xValue = histogramMin + xInHistogramScale;
if (xValue >= intensityRangeMin && xValue <= intensityRangeMax) {
activeRegion.lineTo(x, downscaledData[i]);
activeRegion.lineTo(xInCanvasScale, downscaledData[i]);
}
ctx.lineTo(x, downscaledData[i]);
ctx.lineTo(xInCanvasScale, downscaledData[i]);
}
ctx.stroke();
ctx.closePath();
activeRegion.lineTo(((intensityRangeMax - minRange) / rangeLength) * canvasWidth, 0);
activeRegion.lineTo(((intensityRangeMin - minRange) / rangeLength) * canvasWidth, 0);
const activeRegionRightLimit = Math.min(histogramMax, intensityRangeMax);
const activeRegionLeftLimit = Math.max(histogramMin, intensityRangeMin);
activeRegion.lineTo(((activeRegionRightLimit - minRange) / fullLength) * canvasWidth, 0);
activeRegion.lineTo(((activeRegionLeftLimit - minRange) / fullLength) * canvasWidth, 0);
Comment on lines +157 to +160
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MichaelBuessemeyer My assumption would be that histogramMax is always <= intensityRangeMax and therefore, it should be sufficient to simply use histogramMax in the activeRegion.lineTo call. Is that assumption correct?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I was confusing the histogramMax and intensityRangeMax. Hmm, could you distinguish what each of those is again for me? Maybe we can find more descriptive names that make the difference more clear.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you really confused both of them:

  • histogramMax is the maximum color value that is included in the histogram data send by the backend.
  • intensityRangeMax is the maximum color value of the displayed color range and is equal to the higher value of the slide. What's new now is that this value can be greater than histogramMax as you can see it in your example:
    max_range_float

To be honest I think the names are pretty good as the intensityRange is the range given by the slider and thus intensityRangeMax as the upper limit of that range is not far-fetched (in my opinion) -> Feel free to argue about that.

The same goes for histogramMax as this is the highest color value included in the histogram data.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah understood, that makes sense, sorry for the confusion :)

activeRegion.closePath();
ctx.fill(activeRegion);
};
Expand Down Expand Up @@ -132,9 +171,30 @@ class Histogram extends React.PureComponent<HistogramProps> {
}
};

tipFormatter = (value: number) =>
value > 10000 ? value.toExponential() : roundTo(value, 2).toString();

render() {
const { intensityRangeMin, intensityRangeMax, data } = this.props;
const { min: minRange, max: maxRange } = data[0];
const {
intensityRangeMin,
intensityRangeMax,
isInEditMode,
defaultMinMax,
layerName,
onChangeLayer,
} = this.props;
const { min: minRange, max: maxRange } = this.getMinAndMax();
const formatValue = (newValue: string): number | string => {
if (!isANumber(newValue)) {
return newValue;
}
return roundTo(parseFloat(newValue), 2);
};
const tooltipTitleFor = (minimumOrMaximum: string) =>
`Enter the ${minimumOrMaximum} possible value for layer ${layerName}. Scientific (e.g. 9e+10) notation is supported.`;

const minMaxInputStyle = { width: "100%" };

return (
<React.Fragment>
<canvas
Expand All @@ -145,17 +205,67 @@ class Histogram extends React.PureComponent<HistogramProps> {
height={canvasHeight}
/>
<Slider
range
value={[intensityRangeMin, intensityRangeMax]}
min={minRange}
max={maxRange}
range
defaultValue={[minRange, maxRange]}
onChange={this.onThresholdChange}
onAfterChange={this.onThresholdChange}
style={{ width: canvasWidth, margin: 0, marginBottom: 18 }}
step={(maxRange - minRange) / 255}
tipFormatter={val => roundTo(val, 2).toString()}
tipFormatter={this.tipFormatter}
style={{ width: canvasWidth, margin: 0, marginBottom: 18 }}
/>
{isInEditMode ? (
<Row type="flex" align="middle">
<Col span={4}>
<label className="setting-label">min:</label>
</Col>
<Col span={8}>
<Tooltip title={tooltipTitleFor("minimum")}>
<InputNumber
size="small"
min={defaultMinMax[0]}
max={maxRange}
defaultValue={minRange}
value={minRange}
formatter={formatValue}
onChange={value => {
value = parseFloat(value);
if (isANumber(value) && value <= maxRange) {
onChangeLayer(layerName, "min", value);
}
}}
style={minMaxInputStyle}
/>
</Tooltip>
</Col>
<Col span={4}>
<label className="setting-label" style={{ width: "100%", textAlign: "center" }}>
max:
</label>
</Col>
<Col span={8}>
<Tooltip title={tooltipTitleFor("maximum")}>
<InputNumber
size="small"
min={minRange}
max={defaultMinMax[1]}
defaultValue={maxRange}
value={maxRange}
formatter={formatValue}
onChange={value => {
value = parseFloat(value);
if (isANumber(value) && value >= minRange) {
onChangeLayer(layerName, "max", value);
}
}}
style={minMaxInputStyle}
/>
</Tooltip>
</Col>
</Row>
) : null}
</React.Fragment>
);
}
Expand Down
Loading