diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index f3fb34ed075..a6bd2d51935 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -20,11 +20,13 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released ### Changed - Improved the scrolling behaviour of sliders: Sliders must be focused to scroll them. Additionally, parent element scrolling is prevented when using the slider. [#8321](https://github.com/scalableminds/webknossos/pull/8321) [#8321](https://github.com/scalableminds/webknossos/pull/8321) +- Increase the flood fill maximum bounding box size limits for segmentation layers restricted to coarser mags. [#8382](https://github.com/scalableminds/webknossos/pull/8382) ### Fixed - Fixed a bug that lead to trees being dropped when merging to trees together. [#8359](https://github.com/scalableminds/webknossos/pull/8359) - Fixed that the onboarding screen incorrectly appeared when a certain request failed. [#8356](https://github.com/scalableminds/webknossos/pull/8356) - Fixed the segment registering in coarser mags for non-mag-aligned bounding boxes. [#8364](https://github.com/scalableminds/webknossos/pull/8364) +- Fixed using the flood fill tool in 2D mode for mags other than the finest one. [#8382](https://github.com/scalableminds/webknossos/pull/8382) ### Removed - Removed the feature to downsample existing volume annotations. All new volume annotations had a whole mag stack since [#4755](https://github.com/scalableminds/webknossos/pull/4755) (four years ago). [#7917](https://github.com/scalableminds/webknossos/pull/7917) diff --git a/biome.json b/biome.json index 6a10b4cb84d..4d06ed32dde 100644 --- a/biome.json +++ b/biome.json @@ -64,6 +64,7 @@ }, "correctness": { "noUnusedVariables": "error", + "noUnusedImports": "error", "noRenderReturnValue": "off", "useHookAtTopLevel": "error" }, diff --git a/frontend/javascripts/components/slider.tsx b/frontend/javascripts/components/slider.tsx index 0d3f389b575..5574196fe9e 100644 --- a/frontend/javascripts/components/slider.tsx +++ b/frontend/javascripts/components/slider.tsx @@ -1,8 +1,7 @@ import { Slider as AntdSlider, type SliderSingleProps } from "antd"; import type { SliderRangeProps } from "antd/lib/slider"; import { clamp } from "libs/utils"; -import _ from "lodash"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef } from "react"; const DEFAULT_WHEEL_FACTOR = 0.02; const DEFAULT_STEP = 1; diff --git a/frontend/javascripts/oxalis/model.ts b/frontend/javascripts/oxalis/model.ts index b38554c910d..89e40b56746 100644 --- a/frontend/javascripts/oxalis/model.ts +++ b/frontend/javascripts/oxalis/model.ts @@ -20,7 +20,6 @@ import Store from "oxalis/store"; import type { APICompoundType } from "types/api_flow_types"; import { globalToLayerTransformedPosition } from "./model/accessors/dataset_layer_transformation_accessor"; -import { invertTransform, transformPointUnscaled } from "./model/helpers/transformation_helpers"; import { initialize } from "./model_initialization"; // TODO: Non-reactive diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts index 722eacac8e1..8ca61c1512c 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts @@ -696,22 +696,27 @@ class DataCube { currentGlobalBucketPosition, V3.scale3(adjustedNeighbourVoxelXyz, currentMag), ); + // When flood filling in a coarser mag, a voxel in the coarse mag is more than one voxel in mag 1 + const voxelBoundingBoxInMag1 = new BoundingBox({ + min: currentGlobalPosition, + max: V3.add(currentGlobalPosition, currentMag), + }); if (bucketData[neighbourVoxelIndex] === sourceSegmentId) { - if (floodfillBoundingBox.containsPoint(currentGlobalPosition)) { + if (floodfillBoundingBox.intersectedWith(voxelBoundingBoxInMag1).getVolume() > 0) { 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]); + coveredBBoxMin[0] = Math.min(coveredBBoxMin[0], voxelBoundingBoxInMag1.min[0]); + coveredBBoxMin[1] = Math.min(coveredBBoxMin[1], voxelBoundingBoxInMag1.min[1]); + coveredBBoxMin[2] = Math.min(coveredBBoxMin[2], voxelBoundingBoxInMag1.min[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); + coveredBBoxMax[0] = Math.max(coveredBBoxMax[0], voxelBoundingBoxInMag1.max[0] + 1); + coveredBBoxMax[1] = Math.max(coveredBBoxMax[1], voxelBoundingBoxInMag1.max[1] + 1); + coveredBBoxMax[2] = Math.max(coveredBBoxMax[2], voxelBoundingBoxInMag1.max[2] + 1); if (labeledVoxelCount % 1000000 === 0) { console.log(`Labeled ${labeledVoxelCount} Vx. Continuing...`); diff --git a/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.ts b/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.ts index 4b695bd63a7..5908374ceb4 100644 --- a/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.ts +++ b/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.ts @@ -10,7 +10,6 @@ import { getNodeAndTree, getSkeletonTracing, getTree, - getTreesWithType, isSkeletonLayerTransformed, } from "oxalis/model/accessors/skeletontracing_accessor"; import type { Action } from "oxalis/model/actions/actions"; diff --git a/frontend/javascripts/oxalis/model/reducers/ui_reducer.ts b/frontend/javascripts/oxalis/model/reducers/ui_reducer.ts index 57fdb7d8cc5..f00602cc1eb 100644 --- a/frontend/javascripts/oxalis/model/reducers/ui_reducer.ts +++ b/frontend/javascripts/oxalis/model/reducers/ui_reducer.ts @@ -1,4 +1,4 @@ -import { AnnotationToolEnum, AvailableToolsInViewMode, ControlModeEnum } from "oxalis/constants"; +import { AnnotationToolEnum, AvailableToolsInViewMode } from "oxalis/constants"; import type { Action } from "oxalis/model/actions/actions"; import { updateKey, updateKey2 } from "oxalis/model/helpers/deep_update"; import { diff --git a/frontend/javascripts/oxalis/model/sagas/proofread_saga.ts b/frontend/javascripts/oxalis/model/sagas/proofread_saga.ts index 29eb482e2b0..5f534f1c5a4 100644 --- a/frontend/javascripts/oxalis/model/sagas/proofread_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/proofread_saga.ts @@ -66,7 +66,7 @@ import { updateSegmentAction, } from "oxalis/model/actions/volumetracing_actions"; import type { Saga } from "oxalis/model/sagas/effect-generators"; -import { select, take } from "oxalis/model/sagas/effect-generators"; +import { select } from "oxalis/model/sagas/effect-generators"; import { type UpdateActionWithoutIsolationRequirement, mergeAgglomerate, diff --git a/frontend/javascripts/oxalis/model/sagas/saga_helpers.ts b/frontend/javascripts/oxalis/model/sagas/saga_helpers.ts index 7d9aff4d8af..228eefac7f4 100644 --- a/frontend/javascripts/oxalis/model/sagas/saga_helpers.ts +++ b/frontend/javascripts/oxalis/model/sagas/saga_helpers.ts @@ -1,4 +1,4 @@ -import type { ActionPattern, Predicate } from "@redux-saga/types"; +import type { ActionPattern } from "@redux-saga/types"; import { Modal } from "antd"; import Toast from "libs/toast"; import messages from "messages"; diff --git a/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx b/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx index ebd8dda8c85..20866747d9c 100644 --- a/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx +++ b/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx @@ -27,7 +27,7 @@ import { import BoundingBox from "oxalis/model/bucket_data_handling/bounding_box"; import Dimensions from "oxalis/model/dimensions"; import type { Saga } from "oxalis/model/sagas/effect-generators"; -import { select, take } from "oxalis/model/sagas/effect-generators"; +import { select } from "oxalis/model/sagas/effect-generators"; import { requestBucketModificationInVolumeTracing } from "oxalis/model/sagas/saga_helpers"; import { Model } from "oxalis/singletons"; import { call, put, takeEvery } from "typed-redux-saga"; @@ -41,7 +41,24 @@ export function* floodFill(): Saga { yield* takeEvery("FLOOD_FILL", handleFloodFill); } -function* getBoundingBoxForFloodFillWhenRestricted(position: Vector3, currentViewport: OrthoView) { +function getMaximumBoundingBoxSizeForFloodfill( + fillMode: FillMode, + finestSegmentationLayerMag: Vector3, +): Vector3 { + const maximumBoundingBoxMag1 = Constants.FLOOD_FILL_EXTENTS[fillMode]; + // If the finest mag of the segmentation layer is not Mag(1), the maximum bounding box needs to be scaled accordingly + const maximumBoundingBoxInFinestMag = V3.scale3( + maximumBoundingBoxMag1, + finestSegmentationLayerMag, + ); + return maximumBoundingBoxInFinestMag; +} + +function* getBoundingBoxForFloodFillWhenRestricted( + position: Vector3, + currentViewport: OrthoView, + finestSegmentationLayerMag: Vector3, +) { const fillMode = yield* select((state) => state.userConfiguration.fillMode); const bboxes = yield* select((state) => getUserBoundingBoxesThatContainPosition(state, position)); if (bboxes.length === 0) { @@ -52,9 +69,12 @@ function* getBoundingBoxForFloodFillWhenRestricted(position: Vector3, currentVie } const smallestBbox = _.sortBy(bboxes, (bbox) => new BoundingBox(bbox.boundingBox).getVolume())[0]; - const maximumVoxelSize = - Constants.FLOOD_FILL_MULTIPLIER_FOR_BBOX_RESTRICTION * - V3.prod(Constants.FLOOD_FILL_EXTENTS[fillMode]); + const maxBboxSize = yield* call( + getMaximumBoundingBoxSizeForFloodfill, + fillMode, + finestSegmentationLayerMag, + ); + const maxBboxVolume = Constants.FLOOD_FILL_MULTIPLIER_FOR_BBOX_RESTRICTION * V3.prod(maxBboxSize); const bboxObj = new BoundingBox(smallestBbox.boundingBox); const bboxVolume = @@ -64,9 +84,9 @@ function* getBoundingBoxForFloodFillWhenRestricted(position: Vector3, currentVie V2.prod( Dimensions.getIndices(currentViewport).map((idx) => bboxObj.getSize()[idx]) as Vector2, ); - if (bboxVolume > maximumVoxelSize) { + if (bboxVolume > maxBboxVolume) { return { - failureReason: `The bounding box that encloses the clicked position is too large. Shrink its size so that it does not contain more than ${maximumVoxelSize} voxels.`, + failureReason: `The bounding box that encloses the clicked position is too large, containing ${bboxVolume} voxels. Shrink its size so that it does not contain more than ${maxBboxVolume} voxels.`, }; } return smallestBbox.boundingBox; @@ -75,18 +95,24 @@ function* getBoundingBoxForFloodFillWhenRestricted(position: Vector3, currentVie function* getBoundingBoxForFloodFillWhenUnrestricted( position: Vector3, currentViewport: OrthoView, + finestSegmentationLayerMag: Vector3, ) { const fillMode = yield* select((state) => state.userConfiguration.fillMode); - const halfBoundingBoxSizeUVW = V3.scale(Constants.FLOOD_FILL_EXTENTS[fillMode], 0.5); + const maxBoundingBoxSize = yield* call( + getMaximumBoundingBoxSizeForFloodfill, + fillMode, + finestSegmentationLayerMag, + ); + const halfBoundingBoxSize = V3.scale(maxBoundingBoxSize, 0.5); const currentViewportBounding = { - min: V3.sub(position, halfBoundingBoxSizeUVW), - max: V3.add(position, halfBoundingBoxSizeUVW), + min: V3.sub(position, halfBoundingBoxSize), + max: V3.add(position, halfBoundingBoxSize), }; if (fillMode === FillModeEnum._2D) { // Only use current plane const thirdDimension = Dimensions.thirdDimensionForPlane(currentViewport); - const numberOfSlices = 1; + const numberOfSlices = finestSegmentationLayerMag[thirdDimension]; currentViewportBounding.min[thirdDimension] = position[thirdDimension]; currentViewportBounding.max[thirdDimension] = position[thirdDimension] + numberOfSlices; } @@ -104,14 +130,25 @@ function* getBoundingBoxForFloodFillWhenUnrestricted( function* getBoundingBoxForFloodFill( position: Vector3, currentViewport: OrthoView, + finestSegmentationLayerMag: Vector3, ): Saga { const isRestrictedToBoundingBox = yield* select( (state) => state.userConfiguration.isFloodfillRestrictedToBoundingBox, ); if (isRestrictedToBoundingBox) { - return yield* call(getBoundingBoxForFloodFillWhenRestricted, position, currentViewport); + return yield* call( + getBoundingBoxForFloodFillWhenRestricted, + position, + currentViewport, + finestSegmentationLayerMag, + ); } else { - return yield* call(getBoundingBoxForFloodFillWhenUnrestricted, position, currentViewport); + return yield* call( + getBoundingBoxForFloodFillWhenUnrestricted, + position, + currentViewport, + finestSegmentationLayerMag, + ); } } @@ -172,7 +209,13 @@ function* handleFloodFill(floodFillAction: FloodFillAction): Saga { if (!isModificationAllowed) { return; } - const boundingBoxForFloodFill = yield* call(getBoundingBoxForFloodFill, seedPosition, planeId); + const finestSegmentationLayerMag = magInfo.getFinestMag(); + const boundingBoxForFloodFill = yield* call( + getBoundingBoxForFloodFill, + seedPosition, + planeId, + finestSegmentationLayerMag, + ); if ("failureReason" in boundingBoxForFloodFill) { Toast.warning(boundingBoxForFloodFill.failureReason, { key: NO_FLOODFILL_BBOX_TOAST_KEY, diff --git a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx index 0d301c4ab0c..deee9254d3c 100644 --- a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx +++ b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx @@ -26,7 +26,6 @@ import { document } from "libs/window"; import { type AnnotationTool, AnnotationToolEnum, - ControlModeEnum, FillModeEnum, type InterpolationMode, InterpolationModeEnum, diff --git a/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.ts b/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.ts index bcf54e7a07e..0d798bf9412 100644 --- a/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.ts +++ b/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.ts @@ -1,6 +1,6 @@ import _ from "lodash"; import { getDefaultValueRangeOfLayer, isColorLayer } from "oxalis/model/accessors/dataset_accessor"; -import type { APIDataLayer, APIDataset, APIMaybeUnimportedDataset } from "types/api_flow_types"; +import type { APIDataset, APIMaybeUnimportedDataset } from "types/api_flow_types"; import { defaultDatasetViewConfiguration, getDefaultLayerViewConfiguration,