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

Fix clip histogram button if ideal mag does not exist #6433

Merged
merged 8 commits into from
Sep 15, 2022
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Fixed the duplicate function for annotations with an editable mapping (a.k.a. supervoxel proofreading) layer. [#6446](https://github.com/scalableminds/webknossos/pull/6446)
- Fixed isosurface loading for volume annotations with mappings. [#6458](https://github.com/scalableminds/webknossos/pull/6458)
- Fixed importing of remote datastore (e.g., zarr) when datastore is set up separately. [#6462](https://github.com/scalableminds/webknossos/pull/6462)
- Fixed a crash which could happen when using the "Automatically clip histogram" feature in certain scenarios. [#6433](https://github.com/scalableminds/webknossos/pull/6433)

### Removed

Expand Down
31 changes: 20 additions & 11 deletions frontend/javascripts/oxalis/api/api_latest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1388,31 +1388,40 @@ class DataApi {
maybeResolutionIndex: number | null | undefined,
) {
const state = Store.getState();
const [curX, curY, curZ] = dimensions.transDim(
const [curU, curV, curW] = dimensions.transDim(
dimensions.roundCoordinate(getPosition(state.flycam)),
viewport,
);
const [halfViewportExtentX, halfViewportExtentY] = getHalfViewportExtentsFromState(
const [halfViewportExtentU, halfViewportExtentV] = getHalfViewportExtentsFromState(
state,
viewport,
);
const layer = getLayerByName(state.dataset, layerName);
const resolutionInfo = getResolutionInfo(layer.resolutions);
if (maybeResolutionIndex == null) {
maybeResolutionIndex = getRequestLogZoomStep(state);
}
const zoomStep = resolutionInfo.getClosestExistingIndex(maybeResolutionIndex);

const min = dimensions.transDim(
V3.sub([curX, curY, curZ], [halfViewportExtentX, halfViewportExtentY, 0]),
V3.sub([curU, curV, curW], [halfViewportExtentU, halfViewportExtentV, 0]),
viewport,
);
const max = dimensions.transDim(
V3.add([curX, curY, curZ], [halfViewportExtentX, halfViewportExtentY, 1]),
V3.add([curU, curV, curW], [halfViewportExtentU, halfViewportExtentV, 1]),
viewport,
);

let zoomStep;
if (maybeResolutionIndex == null) {
zoomStep = getRequestLogZoomStep(state);
} else {
const layer = getLayerByName(state.dataset, layerName);
const resolutionInfo = getResolutionInfo(layer.resolutions);
zoomStep = resolutionInfo.getClosestExistingIndex(maybeResolutionIndex);
const resolution = resolutionInfo.getResolutionByIndexOrThrow(zoomStep);
const resolutionUVX = dimensions.transDim(resolution, viewport);
const widthInVoxel = Math.ceil(halfViewportExtentU / resolutionUVX[0]);
const heightInVoxel = Math.ceil(halfViewportExtentV / resolutionUVX[1]);
if (widthInVoxel * heightInVoxel > 1024 ** 2) {
throw new Error(
"Requested data for viewport cannot be loaded, since the amount of data is too large for the available resolution. Please zoom in further or ensure that coarser magnifications are available.",
);
}

const cuboid = await this.getDataForBoundingBox(
layerName,
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,10 @@ export class ResolutionInfo {
});

const bestIndexWithDistance = _.head(_.sortBy(indicesWithDistances, (entry) => entry[1]));
if (bestIndexWithDistance == null) {
throw new Error("Couldn't find any resolution.");
}

// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
return bestIndexWithDistance[0];
}

Expand Down
70 changes: 42 additions & 28 deletions frontend/javascripts/oxalis/model/sagas/clip_histogram_saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { takeEvery } from "typed-redux-saga";
import type { ClipHistogramAction } from "oxalis/model/actions/settings_actions";
import { updateLayerSettingAction } from "oxalis/model/actions/settings_actions";
import Toast from "libs/toast";
import { OrthoViews } from "oxalis/constants";
import { OrthoViews, Vector3 } from "oxalis/constants";
import { getConstructorForElementClass } from "oxalis/model/bucket_data_handling/bucket";
import { getLayerByName } from "oxalis/model/accessors/dataset_accessor";
import api from "oxalis/api/internal_api";
Expand All @@ -18,33 +18,41 @@ function onThresholdChange(layerName: string, [firstVal, secVal]: [number, numbe
}
}

async function getClippingValues(layerName: string, thresholdRatio: number = 0.0001) {
const { elementClass } = getLayerByName(Store.getState().dataset, layerName);
async function getClippingValues(
layerName: string,
thresholdRatio: number = 0.0001,
): Promise<{ values: Vector3 } | { message: string }> {
const state = Store.getState();
const { dataset } = state;
const { elementClass } = getLayerByName(dataset, layerName);
const [TypedArrayClass] = getConstructorForElementClass(elementClass);

// Find a viable resolution to compute the histogram on
// Ideally, we want to avoid resolutions 1 and 2 to keep
// the amount of data that has to be loaded small and
// to de-noise the data
const state = Store.getState();
const maybeResolutionIndex = Math.max(2, getRequestLogZoomStep(state) + 1);

const [cuboidXY, cuboidXZ, cuboidYZ] = await Promise.all([
api.data.getViewportData(OrthoViews.PLANE_XY, layerName, maybeResolutionIndex),
api.data.getViewportData(OrthoViews.PLANE_XZ, layerName, maybeResolutionIndex),
api.data.getViewportData(OrthoViews.PLANE_YZ, layerName, maybeResolutionIndex),
]);
const dataForAllViewPorts = new TypedArrayClass(
cuboidXY.length + cuboidXZ.length + cuboidYZ.length,
);

// If getViewportData returned a BigUint array, dataForAllViewPorts will be an BigUint array, too.
// @ts-ignore
dataForAllViewPorts.set(cuboidXY);
// @ts-ignore
dataForAllViewPorts.set(cuboidXZ, cuboidXY.length);
// @ts-ignore
dataForAllViewPorts.set(cuboidYZ, cuboidXY.length + cuboidXZ.length);
const desiredResolutionIndex = Math.max(2, getRequestLogZoomStep(state) + 1);

let dataForAllViewPorts;
try {
const [cuboidXY, cuboidXZ, cuboidYZ] = await Promise.all([
api.data.getViewportData(OrthoViews.PLANE_XY, layerName, desiredResolutionIndex),
api.data.getViewportData(OrthoViews.PLANE_XZ, layerName, desiredResolutionIndex),
api.data.getViewportData(OrthoViews.PLANE_YZ, layerName, desiredResolutionIndex),
]);
dataForAllViewPorts = new TypedArrayClass(cuboidXY.length + cuboidXZ.length + cuboidYZ.length);
// If getViewportData returned a BigUint array, dataForAllViewPorts will be an BigUint array, too.
// @ts-ignore
dataForAllViewPorts.set(cuboidXY);
// @ts-ignore
dataForAllViewPorts.set(cuboidXZ, cuboidXY.length);
// @ts-ignore
dataForAllViewPorts.set(cuboidYZ, cuboidXY.length + cuboidXZ.length);
} catch (exception) {
console.error("Could not clip histogram due to", exception);
return { message: "Could not clip the histogram. Zoom in further and try again." };
}

const localHist = new Map();
for (let i = 0; i < dataForAllViewPorts.length; i++) {
if (dataForAllViewPorts[i] !== 0) {
Expand Down Expand Up @@ -86,18 +94,23 @@ async function getClippingValues(layerName: string, thresholdRatio: number = 0.0
}
}

if (lowerClip === -1 || upperClip === -1) {
return {
message:
"The histogram could not be clipped, because the data did not contain any brightness values greater than 0.",
};
}

// largest brightness value is first after the keys were reversed
const wiggleRoom = Math.floor(thresholdRatio * sortedHistKeys[0]);
return [lowerClip, upperClip, wiggleRoom];
return { values: [lowerClip, upperClip, wiggleRoom] };
}

async function clipHistogram(action: ClipHistogramAction) {
const [lowerClip, upperClip, wiggleRoom] = await getClippingValues(action.layerName);
const result = await getClippingValues(action.layerName);

if (lowerClip === -1 || upperClip === -1) {
Toast.warning(
"The histogram could not be clipped, because the data did not contain any brightness values greater than 0.",
);
if ("message" in result) {
Toast.warning(result.message);

// this is required to correctly reset the state of the AsyncButton initiating this action
if (action.callback != null) {
Expand All @@ -106,6 +119,7 @@ async function clipHistogram(action: ClipHistogramAction) {

return;
}
const [lowerClip, upperClip, wiggleRoom] = result.values;

if (!action.shouldAdjustClipRange) {
onThresholdChange(action.layerName, [lowerClip, upperClip]);
Expand Down