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

Allow to restrict floodfill to a bounding box #8267

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8e656ad
avoid high frequency logs of awaited-missing-bucket messages
philippotto Dec 9, 2024
ee6b663
improve comments
philippotto Dec 9, 2024
070b5f5
move useSelector so that it's only active when the context menu is op…
philippotto Dec 9, 2024
41f139f
move floodfill saga code into own module
philippotto Dec 9, 2024
d921995
hardcode floodfill constraint to current bbox
philippotto Dec 9, 2024
f9ff2ae
add toggle to navbar to enable/disable bbox restriction for floodfill
philippotto Dec 9, 2024
4b29cec
fix linting
philippotto Dec 9, 2024
fbcbd6c
debug test
philippotto Dec 10, 2024
daf2c0c
only create bbox after *3d* floodfill terminated early
philippotto Dec 12, 2024
b667220
fix test and creation of bbox in exceeding case
philippotto Dec 12, 2024
f714568
format
philippotto Dec 12, 2024
20ec8c4
remove only modifier again
philippotto Dec 12, 2024
be9df82
fix floodfill saga termination; improve ui
philippotto Dec 13, 2024
8349c0c
fix flickering of undo/redo buttons when clicking them
philippotto Dec 13, 2024
ee03e68
integrate new icon and don't create bbox in 3d case when the bbox res…
philippotto Dec 17, 2024
0e26bb0
avoid frequent vector3 allocation in floodfill
philippotto Dec 17, 2024
a267052
ignore changed md and snap files in yarn test-changed
philippotto Dec 17, 2024
445e4d9
fix bounding box creation outside of DS bbox
philippotto Dec 17, 2024
aabbfbe
even when using isFloodfillRestrictedToBoundingBox, check that the bb…
philippotto Dec 17, 2024
35a4913
Merge branch 'master' of github.com:scalableminds/webknossos into flo…
philippotto Dec 17, 2024
637ce46
update changelog
philippotto Dec 17, 2024
3959e9b
update docs
philippotto Dec 18, 2024
b056759
fix colliding hide instructions of progress callback when doing multi…
philippotto Dec 18, 2024
d13abc2
immediately hide floodfill success toast if the operation has finishe…
philippotto Dec 18, 2024
da9cead
fix messages mock in tests
philippotto Dec 18, 2024
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
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
### Added
- Added the total volume of a dataset to a tooltip in the dataset info tab. [#8229](https://github.com/scalableminds/webknossos/pull/8229)
- Optimized performance of data loading with “fill value“ chunks. [#8271](https://github.com/scalableminds/webknossos/pull/8271)
- The fill tool can now be adapted so that it only acts within a specified bounding box. Use the new "Restrict Floodfill" mode for that in the toolbar. [#8267](https://github.com/scalableminds/webknossos/pull/8267)

### Changed
- Renamed "resolution" to "magnification" in more places within the codebase, including local variables. [#8168](https://github.com/scalableminds/webknossos/pull/8168)
Expand Down
Binary file added docs/images/icon_restricted_floodfill.jpg
Copy link
Contributor

Choose a reason for hiding this comment

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

This file looks unused to me

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions docs/volume_annotation/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ The following interactions and modes become available when working with some of
![3D Fill Modifier](./images/3d-modifier.jpg){align=left width="60"}
**2D/3D Fill**: Modifies the flood filling tool to work in 2D (in-plane only) or 3D (volumetric fill/re-labeling). 3D flood fill is constrained to a small, regional bounding box for performance reasons. Read more about [flood fills](#volume-flood-fills) below.

![Restrict Fill](./images/icon_restricted_floodfill.jpg){align=left width="60"}
**Restrict Fill by Bounding Box**: When enabled, the fill operation will be restricted by the smallest bounding box that encloses the clicked position. This feature can be useful when correcting segmentation in a small bounding box (e.g., when curating training data).

## Quick-select tool
The Quick Select tool offers AI-powered automatic segmentation, powered by [Segment Anything Model 2](https://ai.meta.com/blog/segment-anything-2/). Simply draw a selection around your target structure, and WEBKNOSSOS will automatically segment it for you.
Expand Down
11 changes: 7 additions & 4 deletions frontend/javascripts/components/async_clickables.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, type ButtonProps } from "antd";
import { Button, ConfigProvider, type ButtonProps } from "antd";
import { LoadingOutlined } from "@ant-design/icons";
import * as React from "react";
import FastTooltip from "./fast_tooltip";
Expand Down Expand Up @@ -47,9 +47,12 @@ export function AsyncButton(props: AsyncButtonProps) {
const effectiveChildren = hideContentWhenLoading && isLoading ? null : children;
return (
<FastTooltip title={title}>
<Button {...rest} loading={isLoading} onClick={onClick}>
{effectiveChildren}
</Button>
{/* Avoid weird animation when icons swap */}
<ConfigProvider theme={{ token: { motion: false } }}>
<Button {...rest} loading={isLoading} onClick={onClick}>
{effectiveChildren}
</Button>
</ConfigProvider>
</FastTooltip>
);
}
Expand Down
3 changes: 3 additions & 0 deletions frontend/javascripts/libs/mjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ const V2 = {
clone(a: Vector2): Vector2 {
return [a[0], a[1]];
},
prod(a: Vector2) {
return a[0] * a[1];
},
};

const _tmpVec: Vector3 = [0, 0, 0];
Expand Down
7 changes: 6 additions & 1 deletion frontend/javascripts/libs/progress_callback.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { message } from "antd";
import { sleep } from "libs/utils";

type HideFn = () => void;

export type ProgressCallback = (
isDone: boolean,
progressState: string | React.ReactNode,
Expand All @@ -9,11 +11,14 @@ export type ProgressCallback = (
) => Promise<{
hideFn: HideFn;
}>;

type Options = {
pauseDelay: number;
successMessageDelay: number;
key?: string;
}; // This function returns another function which can be called within a longer running
};

// 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 });
// await progressCallback(false, "Beginning work...")
Expand Down
3 changes: 3 additions & 0 deletions frontend/javascripts/oxalis/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,9 @@ const Constants = {
_2D: (process.env.IS_TESTING ? [512, 512, 1] : [768, 768, 1]) as Vector3,
_3D: (process.env.IS_TESTING ? [64, 64, 32] : [96, 96, 96]) as Vector3,
},
// When the user uses the "isFloodfillRestrictedToBoundingBox" setting,
// we are more lax with the flood fill extent.
FLOOD_FILL_MULTIPLIER_FOR_BBOX_RESTRICTION: 10,
MAXIMUM_DATE_TIMESTAMP: 8640000000000000,
SCALEBAR_HEIGHT: 22,
SCALEBAR_OFFSET: 10,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,9 @@ export function createBoundingBoxAndGetEdges(
addUserBoundingBoxAction({
boundingBox: {
min: globalPosition,
max: V3.add(globalPosition, [1, 1, 1]),
// The last argument ensures that a Vector3 is used and not a
// Float32Array.
max: V3.add(globalPosition, [1, 1, 1], [0, 0, 0]),
},
}),
);
Expand Down
1 change: 1 addition & 0 deletions frontend/javascripts/oxalis/default_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const defaultState: OxalisState = {
gpuMemoryFactor: Constants.DEFAULT_GPU_MEMORY_FACTOR,
overwriteMode: OverwriteModeEnum.OVERWRITE_ALL,
fillMode: FillModeEnum._2D,
isFloodfillRestrictedToBoundingBox: false,
interpolationMode: InterpolationModeEnum.INTERPOLATE,
useLegacyBindings: false,
quickSelect: {
Expand Down
12 changes: 12 additions & 0 deletions frontend/javascripts/oxalis/model/accessors/tracing_accessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import type {
import type { ServerTracing, TracingType } from "types/api_flow_types";
import { TracingTypeEnum } from "types/api_flow_types";
import type { SaveQueueType } from "oxalis/model/actions/save_actions";
import BoundingBox from "../bucket_data_handling/bounding_box";
import type { Vector3 } from "oxalis/constants";

export function maybeGetSomeTracing(
tracing: Tracing,
Expand Down Expand Up @@ -86,7 +88,17 @@ export function selectTracing(

return tracing;
}

export const getUserBoundingBoxesFromState = (state: OxalisState): Array<UserBoundingBox> => {
const maybeSomeTracing = maybeGetSomeTracing(state.tracing);
return maybeSomeTracing != null ? maybeSomeTracing.userBoundingBoxes : [];
};

export const getUserBoundingBoxesThatContainPosition = (
state: OxalisState,
position: Vector3,
): Array<UserBoundingBox> => {
const bboxes = getUserBoundingBoxesFromState(state);

return bboxes.filter((el) => new BoundingBox(el.boundingBox).containsPoint(position));
};
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ const warnMergeWithoutPendingOperations = _.throttle(() => {
);
}, WARNING_THROTTLE_THRESHOLD);

const warnAwaitedMissingBucket = _.throttle(() => {
ErrorHandling.notify(new Error("Awaited missing bucket"));
}, WARNING_THROTTLE_THRESHOLD);

export function assertNonNullBucket(bucket: Bucket): asserts bucket is DataBucket {
if (bucket.type === "null") {
throw new Error("Unexpected null bucket.");
Expand Down Expand Up @@ -773,7 +777,7 @@ export class DataBucket {
// In the past, ensureLoaded() never returned if the bucket
// was MISSING. This log might help to discover potential
// bugs which could arise in combination with MISSING buckets.
console.warn("Awaited missing bucket.");
warnAwaitedMissingBucket();
}
}
}
69 changes: 37 additions & 32 deletions frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ class DataCube {
additionalCoordinates: AdditionalCoordinate[] | null,
segmentIdNumber: number,
dimensionIndices: DimensionMap,
floodfillBoundingBox: BoundingBoxType,
_floodfillBoundingBox: BoundingBoxType,
zoomStep: number,
progressCallback: ProgressCallback,
use3D: boolean,
Expand All @@ -498,14 +498,17 @@ class DataCube {
}> {
// This flood-fill algorithm works in two nested levels and uses a list of buckets to flood fill.
// On the inner level a bucket is flood-filled and if the iteration of the buckets data
// reaches an neighbour bucket, this bucket is added to this list of buckets to flood fill.
// reaches a neighbour bucket, this bucket is added to this list of buckets to flood fill.
// The outer level simply iterates over all buckets in the list and triggers the bucket-wise flood fill.
// Additionally a map is created that saves all labeled voxels for each bucket. This map is returned at the end.
//
// Note: It is possible that a bucket is multiple times added to the list of buckets. This is intended
// Note: It is possible that a bucket is added multiple times to the list of buckets. This is intended
// because a border of the "neighbour volume shape" might leave the neighbour bucket and enter it somewhere else.
// If it would not be possible to have the same neighbour bucket in the list multiple times,
// not all of the target area in the neighbour bucket might be filled.

const floodfillBoundingBox = new BoundingBox(_floodfillBoundingBox);

// Helper function to convert between xyz and uvw (both directions)
const transpose = (voxel: Vector3): Vector3 =>
Dimensions.transDimWithIndices(voxel, dimensionIndices);
Expand All @@ -517,12 +520,12 @@ class DataCube {
zoomStep,
);
const seedBucket = this.getOrCreateBucket(seedBucketAddress);
let coveredBBoxMin: Vector3 = [
const coveredBBoxMin: Vector3 = [
Number.POSITIVE_INFINITY,
Number.POSITIVE_INFINITY,
Number.POSITIVE_INFINITY,
];
let coveredBBoxMax: Vector3 = [0, 0, 0];
const coveredBBoxMax: Vector3 = [0, 0, 0];

if (seedBucket.type === "null") {
return {
Expand Down Expand Up @@ -689,35 +692,37 @@ class DataCube {
} else {
// Label the current neighbour and add it to the neighbourVoxelStackUvw to iterate over its neighbours.
const neighbourVoxelIndex = this.getVoxelIndexByVoxelOffset(neighbourVoxelXyz);
const currentGlobalPosition = V3.add(
currentGlobalBucketPosition,
V3.scale3(adjustedNeighbourVoxelXyz, currentMag),
);

if (bucketData[neighbourVoxelIndex] === sourceSegmentId) {
bucketData[neighbourVoxelIndex] = segmentId;
markUvwInSliceAsLabeled(neighbourVoxelUvw);
neighbourVoxelStackUvw.pushVoxel(neighbourVoxelUvw);
labeledVoxelCount++;
const currentGlobalPosition = V3.add(
currentGlobalBucketPosition,
V3.scale3(adjustedNeighbourVoxelXyz, currentMag),
);
coveredBBoxMin = [
Math.min(coveredBBoxMin[0], currentGlobalPosition[0]),
Math.min(coveredBBoxMin[1], currentGlobalPosition[1]),
Math.min(coveredBBoxMin[2], currentGlobalPosition[2]),
];
// The maximum is exclusive which is why we add 1 to the position
coveredBBoxMax = [
Math.max(coveredBBoxMax[0], currentGlobalPosition[0] + 1),
Math.max(coveredBBoxMax[1], currentGlobalPosition[1] + 1),
Math.max(coveredBBoxMax[2], currentGlobalPosition[2] + 1),
];

if (labeledVoxelCount % 1000000 === 0) {
console.log(`Labeled ${labeledVoxelCount} Vx. Continuing...`);

await progressCallback(
false,
`Labeled ${labeledVoxelCount / 1000000} MVx. Continuing...`,
);
if (floodfillBoundingBox.containsPoint(currentGlobalPosition)) {
bucketData[neighbourVoxelIndex] = segmentId;
markUvwInSliceAsLabeled(neighbourVoxelUvw);
neighbourVoxelStackUvw.pushVoxel(neighbourVoxelUvw);
labeledVoxelCount++;

coveredBBoxMin[0] = Math.min(coveredBBoxMin[0], currentGlobalPosition[0]);
coveredBBoxMin[1] = Math.min(coveredBBoxMin[1], currentGlobalPosition[1]);
coveredBBoxMin[2] = Math.min(coveredBBoxMin[2], currentGlobalPosition[2]);

// The maximum is exclusive which is why we add 1 to the position
coveredBBoxMax[0] = Math.max(coveredBBoxMax[0], currentGlobalPosition[0] + 1);
coveredBBoxMax[1] = Math.max(coveredBBoxMax[1], currentGlobalPosition[1] + 1);
coveredBBoxMax[2] = Math.max(coveredBBoxMax[2], currentGlobalPosition[2] + 1);

if (labeledVoxelCount % 1000000 === 0) {
console.log(`Labeled ${labeledVoxelCount} Vx. Continuing...`);

await progressCallback(
false,
`Labeled ${labeledVoxelCount / 1000000} MVx. Continuing...`,
);
}
} else {
wasBoundingBoxExceeded = true;
}
}
}
Expand Down
Loading