Skip to content

Commit

Permalink
3D Flood fill, more robust volume editing and fix several volume bugs (
Browse files Browse the repository at this point in the history
…#5728 + #5733)

* remove dead and unnecessary code

* comment out uncalled push function

* add more volume-related tests and fix two bugs

* add console.error if async task queue fails

* fix tests, add comments and add crash-provoking test

* make dynamc overwriting of MAXIMUM_BUCKET_COUNT_PER_LAYER work in spec

* fix test.sh

* reference issue for bug/todo

* update changelog

* fix linting

* fix tests

* fix --timeout param for test.sh

* use snapshot tests in volume tracing tests and clean up test.sh

* Update tools/test.sh

Co-authored-by: Daniel <[email protected]>

* fix race condition in unsetting dirty flag

* WIP: implement first version of flood fill (resampling is still broken)

* fix z index

* refactor flood fill code and fix some bugs

* fix flow

* fix more xyz/uvw bugs

* simplify code which increments dirtyCount

* limit 3d flood fill to certain bounding box, inform user whether the limit was reached, show progress for filling and further fixes for 3d flood fill

* build a test which crashes undo because getData() was called on a gc-ed bucket

* fix crashing undo if bucket was collected in meantime (see #5729) and add progress indicator to undo/redo

* fix incorrectly mocked bucket data, properly await undo/redo and clean up

* use redux channels to avoid missing critical actions, such as undo or volume-related actions, when being busy; also make undo/redo buttons AsyncClickable

* clean up

* fix flow

* fix building docker image in CI

* add more comments to applyAndGetRevertingVolumeBatch

* rename action channels

* rename VoxelNeighborStack3D to VoxelNeighborQueue3D

* run all volume saga integration specs

* clean up tests

* allow to switch between 2D and 3D for fill tool

* fix failing mesh loading for hybrid tracings

* implement smart bbox growing for flood fill and create bbox for flood fill limit if it was reached

* fix volumetracing_saga.spec

* fix abort criterium in flood fill; add warning if bucket was given too small array buffer; fix off-by-one in covered floodfill bbox

* fix test for bucket garbage collection

* mark UI as busy when undo/redo/floodfill are computing

* fix tests by clearing stale push and pull queues properly when reinitializing the model

* update snapshot

* fix flow

* incorporate PR feedback (1/x)

* incorporate more feedback

* don't show progress handling if undo stack is empty and refactor corresponding code

* inform user if floodfill bbox was added and show that message for 10s

* update progress UI in-place, also write source_id into bbox name, don't add to undo stack if floodfill is no-op, fix flow and clean up

* only show undo/redo progress message for volume actions

* minor tweaks

* add a comment for USE_FLOODFILL_VOXEL_THRESHOLD

* intersect floodfill bbox with datasets bounding box and add snapshot test

* decrease snapshot size

* fix style of disabled undo/redo buttons

* improve comments

* make floodfill bbox message closable and deduplicate debug info code

* adapt antd typing according to webknossos/pull/5738

Co-authored-by: Daniel <[email protected]>
  • Loading branch information
philippotto and daniel-wer authored Oct 12, 2021
1 parent fa3422e commit 48bd317
Show file tree
Hide file tree
Showing 61 changed files with 18,052 additions and 399 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
"no-use-before-define": ["error", { "functions": false, "classes": false }],
"one-var": ["error", "never"],
"operator-assignment": "warn",
"prefer-destructuring": "warn",
"prefer-destructuring": "off",
"quote-props": ["error", "as-needed", { "numbers": true }],
"quotes": ["error", "double", { "avoidEscape": true }],
"radix": "off",
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
[Commits](https://github.com/scalableminds/webknossos/compare/21.09.0...HEAD)

### Added
-
- Enhanced the volume fill tool to so that it operates beyond the dimensions of the current viewport. Additionally, the fill tool can also be changed to perform in 3D instead of 2D. [#5733](https://github.com/scalableminds/webknossos/pull/5733)

### Changed
-

### Fixed
- Fixed two volume tracing related bugs which could occur when using undo with a slow internet connection or when volume-annotating more than 5000 buckets (32**3 vx) in one session. [#5728](https://github.com/scalableminds/webknossos/pull/5728)
- Jobs status is no longer polled if jobs are not enabled, avoiding backend logging spam [#5761](https://github.com/scalableminds/webknossos/pull/5761)
- Fixed a bug that windows user could not open the context menu as it instantly closed after opening. [#5756](https://github.com/scalableminds/webknossos/pull/5756).
- Fixed a bug where the health check of public datasets failed if no cookie/token was supplied. [#5768](https://github.com/scalableminds/webknossos/pull/5768).
Expand Down
24 changes: 18 additions & 6 deletions flow-typed/npm/antd_vx.x.x.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
// @flow

type MessageConfig = {
content?: string | React$Node,
key?: string,
duration?: number,
onClose?: Function,
};
type MessageContent = string | React$Node | MessageConfig;

declare module "antd" {
declare export class Alert<P> extends React$Component<P> {}
declare export class AutoComplete<P> extends React$Component<P> {
Expand Down Expand Up @@ -72,13 +82,15 @@ declare module "antd" {
static ItemGroup: typeof MenuItemGroup;
static SubMenu: typeof MenuSubMenu;
}

declare export var message: {
success(content: string | React$Node, duration?: number, onClose?: Function): Function,
error(content: string | React$Node, duration?: number, onClose?: Function): Function,
info(content: string | React$Node, duration?: number, onClose?: Function): Function,
warning(content: string | React$Node, duration?: number, onClose?: Function): Function,
warn(content: string | React$Node, duration?: number, onClose?: Function): Function,
loading(content: string | React$Node, duration?: number, onClose?: Function): Function,
success(content: MessageContent, duration?: number, onClose?: Function): Function,
error(content: MessageContent, duration?: number, onClose?: Function): Function,
info(content: MessageContent, duration?: number, onClose?: Function): Function,
warning(content: MessageContent, duration?: number, onClose?: Function): Function,
warn(content: MessageContent, duration?: number, onClose?: Function): Function,
loading(content: MessageContent, duration?: number, onClose?: Function): Function,
destroy(key: string): Function,
};
declare export class Modal<P> extends React$Component<P> {
static confirm: Function;
Expand Down
9 changes: 7 additions & 2 deletions frontend/javascripts/components/async_clickables.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const { useState, useEffect, useRef } = React;

type Props = {
onClick: (SyntheticInputEvent<>) => Promise<any>,
hideContentWhenLoading?: boolean,
children?: React.Node,
};

function useLoadingClickHandler(originalOnClick: (SyntheticInputEvent<>) => Promise<any>) {
Expand Down Expand Up @@ -38,7 +40,10 @@ function useLoadingClickHandler(originalOnClick: (SyntheticInputEvent<>) => Prom

export function AsyncButton(props: Props) {
const [isLoading, onClick] = useLoadingClickHandler(props.onClick);
return <Button {...props} loading={isLoading} onClick={onClick} />;
const { children, hideContentWhenLoading, ...rest } = props;
const effectiveChildren = hideContentWhenLoading && isLoading ? null : children;
// eslint-disable-next-line react/no-children-prop
return <Button {...rest} children={effectiveChildren} loading={isLoading} onClick={onClick} />;
}

export function AsyncIconButton(props: Props & { icon: React.Element<*> }) {
Expand All @@ -49,7 +54,7 @@ export function AsyncIconButton(props: Props & { icon: React.Element<*> }) {
});
}

export function AsyncLink(props: Props & { children: React.Node, icon: React.Node }) {
export function AsyncLink(props: Props & { icon: React.Node }) {
const [isLoading, onClick] = useLoadingClickHandler(props.onClick);
const icon = isLoading ? <LoadingOutlined key="loading-icon" /> : props.icon;
const content = [icon, props.children];
Expand Down
1 change: 1 addition & 0 deletions frontend/javascripts/libs/async_task_queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class AsyncTaskQueue {
this.retryCount++;
this.tasks.unshift(currentTask);
if (this.retryCount > this.failureEventThreshold) {
console.error("AsyncTaskQueue failed with error", error);
this.trigger("failure", this.retryCount);
}
if (this.retryCount >= this.maxRetry) {
Expand Down
3 changes: 3 additions & 0 deletions frontend/javascripts/libs/error_handling.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ class ErrorHandling {
}

notify(maybeError: Object | Error, optParams: Object = {}) {
if (process.env.BABEL_ENV === "test") {
return;
}
const actionLog = getActionLog();
const error = maybeError instanceof Error ? maybeError : new Error(JSON.stringify(maybeError));

Expand Down
35 changes: 25 additions & 10 deletions frontend/javascripts/libs/progress_callback.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ import { sleep } from "libs/utils";
type HideFn = () => void;
export type ProgressCallback = (
isDone: boolean,
progressState: string,
progressState: string | React$Node,
) => Promise<{ hideFn: HideFn }>;

type Options = {
pauseDelay: number,
successMessageDelay: number,
key?: string,
};

// This function returns another function which can be called within a longer running
// process to update the UI with progress information. Example usage:
// const progressCallback = createProgressCallback({ pauseDelay: 100, successMessageDelay: 5000 });
Expand All @@ -20,29 +26,38 @@ export type ProgressCallback = (
//
// The `progressCallback` should be awaited so that the UI can catch up
// with rendering the actual feedback.
export default function createProgressCallback(options: {
pauseDelay: number,
successMessageDelay: number,
}): ProgressCallback {
const { pauseDelay, successMessageDelay } = options;
export default function createProgressCallback(options: Options): ProgressCallback {
let hideFn = null;
return async (isDone: boolean, status: string): Promise<{ hideFn: HideFn }> => {
return async (
isDone: boolean,
status: string | React$Node,
overridingOptions: $Shape<Options> = {},
): Promise<{ hideFn: HideFn }> => {
if (hideFn != null) {
// Clear old progress message
hideFn();
hideFn = null;
}
if (!isDone) {
// Show new progress message
hideFn = message.loading(status, 0);
hideFn = message.loading({
content: status,
duration: 0,
key: overridingOptions.key || options.key,
});
// Allow the browser to catch up with rendering the progress
// indicator.
const pauseDelay = overridingOptions.pauseDelay || options.pauseDelay;
await sleep(pauseDelay);
} else {
// Show success message and clear that after
// ${successDelay} ms
const successDelay = successMessageDelay;
hideFn = message.success(status, 0);
const successDelay = overridingOptions.successMessageDelay || options.successMessageDelay;
hideFn = message.success({
content: status,
duration: 0,
key: overridingOptions.key || options.key,
});
setTimeout(() => {
if (hideFn == null) {
return;
Expand Down
3 changes: 3 additions & 0 deletions frontend/javascripts/libs/window.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ const _window =
location: dummyLocation,
requestAnimationFrame: resolve => resolve(),
document,
navigator: {
onLine: true,
},
}
: window;

Expand Down
9 changes: 6 additions & 3 deletions frontend/javascripts/oxalis/api/api_latest.js
Original file line number Diff line number Diff line change
Expand Up @@ -1010,7 +1010,10 @@ class DataApi {
/**
* Invalidates all downloaded buckets so that they are reloaded.
*/
reloadAllBuckets(): void {
async reloadAllBuckets(): Promise<void> {
if (this.model.getSegmentationTracingLayer() != null) {
await Model.ensureSavedState();
}
_.forEach(this.model.dataLayers, dataLayer => {
dataLayer.cube.collectAllBuckets();
dataLayer.layerRenderingManager.refresh();
Expand Down Expand Up @@ -1202,7 +1205,8 @@ class DataApi {
});
}
// Bucket has been loaded by now or was loaded already
return cube.getDataValue(position, null, zoomStep);
const dataValue = cube.getDataValue(position, null, zoomStep);
return dataValue;
}

getRenderedZoomStepAtPosition(layerName: string, position: ?Vector3): number {
Expand Down Expand Up @@ -1385,7 +1389,6 @@ class DataApi {
}

segmentationLayer.cube.pushQueue.push();
segmentationLayer.cube.trigger("volumeLabeled");
}

/**
Expand Down
1 change: 0 additions & 1 deletion frontend/javascripts/oxalis/api/api_v2.js
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,6 @@ class DataApi {
}

segmentationLayer.cube.pushQueue.push();
segmentationLayer.cube.trigger("volumeLabeled");
}

/**
Expand Down
18 changes: 17 additions & 1 deletion frontend/javascripts/oxalis/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,16 @@ export const OverwriteModeEnum = {
OVERWRITE_ALL: "OVERWRITE_ALL",
OVERWRITE_EMPTY: "OVERWRITE_EMPTY", // In case of deleting, empty === current cell id
};

export type OverwriteMode = $Keys<typeof OverwriteModeEnum>;

export const FillModeEnum = {
// The leading underscore is a workaround, since leading numbers are not valid identifiers
// in JS.
_2D: "_2D",
_3D: "_3D",
};
export type FillMode = $Keys<typeof FillModeEnum>;

export const TDViewDisplayModeEnum = {
NONE: "NONE",
WIREFRAME: "WIREFRAME",
Expand Down Expand Up @@ -219,6 +226,10 @@ export const Unicode = {
// are stored in a Uint8Array in a binary way (which cell
// id the voxels should be changed to is not encoded).
export type LabeledVoxelsMap = Map<Vector4, Uint8Array>;
// LabelMasksByBucketAndW is similar to LabeledVoxelsMap with the difference
// that it can hold multiple slices per bucket (keyed by the W component,
// e.g., z in XY viewport).
export type LabelMasksByBucketAndW = Map<Vector4, Map<number, Uint8Array>>;

export type ShowContextMenuFunction = (number, number, ?number, Vector3, OrthoView) => void;

Expand Down Expand Up @@ -265,6 +276,11 @@ const Constants = {

// Maximum of how many buckets will be held in RAM (per layer)
MAXIMUM_BUCKET_COUNT_PER_LAYER: 5000,

FLOOD_FILL_EXTENTS: {
_2D: process.env.BABEL_ENV === "test" ? [512, 512, 1] : [768, 768, 1],
_3D: process.env.BABEL_ENV === "test" ? [64, 64, 32] : [96, 96, 96],
},
};

export default Constants;
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,11 @@ import {
import ArbitraryPlane from "oxalis/geometries/arbitrary_plane";
import ArbitraryView from "oxalis/view/arbitrary_view";
import Crosshair from "oxalis/geometries/crosshair";
import Model from "oxalis/model";
import Store from "oxalis/store";
import TDController from "oxalis/controller/td_controller";
import Toast from "libs/toast";
import * as Utils from "libs/utils";
import api from "oxalis/api/internal_api";
import app from "app";
import constants, { ArbitraryViewport, type ViewMode, type Point2 } from "oxalis/constants";
import getSceneController from "oxalis/controller/scene_controller_provider";
import messages from "messages";
Expand Down Expand Up @@ -274,8 +272,6 @@ class ArbitraryController extends React.PureComponent<Props> {
}

bindToEvents(): void {
this.startListeningToBuckets();

this.storePropertyUnsubscribers.push(
listenToStoreProperty(
state => state.userConfiguration,
Expand Down Expand Up @@ -309,23 +305,6 @@ class ArbitraryController extends React.PureComponent<Props> {
);
}

onBucketLoaded = () => {
this.arbitraryView.draw();
app.vent.trigger("rerender");
};

startListeningToBuckets() {
for (const dataLayer of Model.getAllLayers()) {
dataLayer.cube.on("bucketLoaded", this.onBucketLoaded);
}
}

stopListeningToBuckets() {
for (const dataLayer of Model.getAllLayers()) {
dataLayer.cube.off("bucketLoaded", this.onBucketLoaded);
}
}

start(): void {
this.arbitraryView = new ArbitraryView();
this.arbitraryView.start();
Expand Down Expand Up @@ -359,7 +338,6 @@ class ArbitraryController extends React.PureComponent<Props> {
}

stop(): void {
this.stopListeningToBuckets();
this.unsubscribeStoreListeners();

if (this.isStarted) {
Expand Down
5 changes: 5 additions & 0 deletions frontend/javascripts/oxalis/default_state.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Constants, {
ControlModeEnum,
OrthoViews,
OverwriteModeEnum,
FillModeEnum,
TDViewDisplayModeEnum,
} from "oxalis/constants";
import { document } from "libs/window";
Expand Down Expand Up @@ -80,6 +81,7 @@ const defaultState: OxalisState = {
tdViewDisplayDatasetBorders: true,
gpuMemoryFactor: Constants.DEFAULT_GPU_MEMORY_FACTOR,
overwriteMode: OverwriteModeEnum.OVERWRITE_ALL,
fillMode: FillModeEnum._2D,
useLegacyBindings: false,
},
temporaryConfiguration: {
Expand Down Expand Up @@ -209,6 +211,9 @@ const defaultState: OxalisState = {
primaryStylesheetElement != null && primaryStylesheetElement.href.includes("dark.css")
? "dark"
: "light",
busyBlockingInfo: {
isBusy: false,
},
},
isosurfacesByLayer: {},
currentMeshFileByLayer: {},
Expand Down
3 changes: 3 additions & 0 deletions frontend/javascripts/oxalis/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ export class OxalisModel {
isMappingSupported,
maximumTextureCountForLayer,
} = initializationInformation;
if (this.dataLayers != null) {
_.values(this.dataLayers).forEach(layer => layer.destroy());
}
this.dataLayers = dataLayers;
this.connectionInfo = connectionInfo;
this.isMappingSupported = isMappingSupported;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ export class ResolutionInfo {
}
}

getDenseResolutions(): Array<Vector3> {
return convertToDenseResolution(this.getResolutionList());
}

getResolutionList(): Array<Vector3> {
return Array.from(this.resolutionMap.entries()).map(entry => entry[1]);
}
Expand Down
Loading

0 comments on commit 48bd317

Please sign in to comment.