From 8e656ad881eae0f5dd24598000fb716fa8f9a612 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 9 Dec 2024 10:55:22 +0100 Subject: [PATCH 01/24] avoid high frequency logs of awaited-missing-bucket messages --- .../javascripts/oxalis/model/bucket_data_handling/bucket.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts index 94adfbfea5..6bc025d7b6 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.ts @@ -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."); @@ -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(); } } } From ee6b663295a9898f6f066d1962abbf0a0149412a Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 9 Dec 2024 10:55:33 +0100 Subject: [PATCH 02/24] improve comments --- .../oxalis/model/bucket_data_handling/data_cube.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 479d896288..102142b2f2 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts @@ -498,14 +498,15 @@ 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. + // Helper function to convert between xyz and uvw (both directions) const transpose = (voxel: Vector3): Vector3 => Dimensions.transDimWithIndices(voxel, dimensionIndices); From 070b5f545c9e7444c6faa869205942c212468844 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 9 Dec 2024 10:57:07 +0100 Subject: [PATCH 03/24] move useSelector so that it's only active when the context menu is open; also disable animations for the context menu --- .../javascripts/oxalis/view/context_menu.tsx | 78 +++++++++++-------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/frontend/javascripts/oxalis/view/context_menu.tsx b/frontend/javascripts/oxalis/view/context_menu.tsx index 634de6a7c5..4b9e5a7cd9 100644 --- a/frontend/javascripts/oxalis/view/context_menu.tsx +++ b/frontend/javascripts/oxalis/view/context_menu.tsx @@ -1,6 +1,15 @@ import { CopyOutlined, PushpinOutlined, ReloadOutlined, WarningOutlined } from "@ant-design/icons"; import type { Dispatch } from "redux"; -import { Dropdown, Empty, notification, Popover, Input, type MenuProps, Modal } from "antd"; +import { + Dropdown, + Empty, + notification, + Popover, + Input, + type MenuProps, + Modal, + ConfigProvider, +} from "antd"; import { useSelector } from "react-redux"; import React, { createContext, type MouseEvent, useContext, useEffect, useState } from "react"; import type { @@ -147,7 +156,6 @@ type OwnProps = { globalPosition: Vector3 | null | undefined; additionalCoordinates: AdditionalCoordinate[] | undefined; maybeViewport: OrthoView | null | undefined; - hideContextMenu: () => void; }; type StateProps = { @@ -180,6 +188,8 @@ type NoNodeContextMenuProps = Props & { infoRows: ItemType[]; }; +const hideContextMenu = () => Store.dispatch(hideContextMenuAction()); + export const getNoActionsAvailableMenu = (hideContextMenu: () => void): MenuProps => ({ onClick: hideContextMenu, style: { @@ -760,7 +770,6 @@ function getBoundingBoxMenuOptions({ activeTool, clickedBoundingBoxId, userBoundingBoxes, - hideContextMenu, allowUpdate, }: NoNodeContextMenuProps): ItemType[] { if (globalPosition == null) return []; @@ -1331,14 +1340,46 @@ export function GenericContextMenuContainer(props: { // @ts-ignore ref={inputRef} /> - {props.children} + {/* Disable animations for the context menu (for performance reasons). */} + + + {props.children} + + ); } -const hideContextMenu = () => Store.dispatch(hideContextMenuAction()); function WkContextMenu() { + const contextMenuPosition = useSelector((state: OxalisState) => { + return state.uiInformation.contextInfo.contextMenuPosition; + }); + + return ( + + {contextMenuPosition != null ? :
} + + ); +} + +function getInfoMenuItem( + key: MenuItemType["key"], + label: MenuItemType["label"], +): MenuItemGroupType { + /* + * This component is a work-around. We want antd menu entries that can not be selected + * or otherwise interacted with. An "empty" menu group will only display the group header + * which gives us the desired behavior. + */ + + return { key, label, type: "group" }; +} + +function ContextMenuInner() { const props = useSelector((state: OxalisState) => { const visibleSegmentationLayer = getVisibleSegmentationLayer(state); const mappingInfo = getMappingInfo( @@ -1380,46 +1421,19 @@ function WkContextMenu() { maybeClickedMeshId: contextInfo.meshId, maybeMeshIntersectionPosition: contextInfo.meshIntersectionPosition, maybeUnmappedSegmentId: contextInfo.unmappedSegmentId, - hideContextMenu, }; }); - return ( - - {props.contextMenuPosition != null ? :
} - - ); -} - -function getInfoMenuItem( - key: MenuItemType["key"], - label: MenuItemType["label"], -): MenuItemGroupType { - /* - * This component is a work-around. We want antd menu entries that can not be selected - * or otherwise interacted with. An "empty" menu group will only display the group header - * which gives us the desired behavior. - */ - - return { key, label, type: "group" }; -} - -function ContextMenuInner(propsWithInputRef: Props) { const [lastTimeSegmentInfoShouldBeFetched, setLastTimeSegmentInfoShouldBeFetched] = useState( new Date(), ); const inputRef = useContext(ContextMenuContext); - const { ...props } = propsWithInputRef; const { skeletonTracing, maybeClickedNodeId, maybeClickedMeshId, contextMenuPosition, segments, - hideContextMenu, voxelSize, globalPosition, maybeViewport, From 41f139f881dca8e3ef5cc011f5a0c0c265ae8593 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 9 Dec 2024 14:13:24 +0100 Subject: [PATCH 04/24] move floodfill saga code into own module --- .../model/sagas/volume/floodfill_saga.tsx | 231 ++++++++++++++++ .../oxalis/model/sagas/volumetracing_saga.tsx | 256 +----------------- 2 files changed, 244 insertions(+), 243 deletions(-) create mode 100644 frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx diff --git a/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx b/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx new file mode 100644 index 0000000000..e85050cf8d --- /dev/null +++ b/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx @@ -0,0 +1,231 @@ +import { message } from "antd"; +import { V3 } from "libs/mjs"; +import createProgressCallback from "libs/progress_callback"; +import Toast from "libs/toast"; +import * as Utils from "libs/utils"; +import type { BoundingBoxType, LabeledVoxelsMap, OrthoView, Vector3 } from "oxalis/constants"; +import Constants, { FillModeEnum, Unicode } from "oxalis/constants"; + +import { getDatasetBoundingBox, getMagInfo } from "oxalis/model/accessors/dataset_accessor"; +import { getActiveMagIndexForLayer } from "oxalis/model/accessors/flycam_accessor"; +import { enforceActiveVolumeTracing } from "oxalis/model/accessors/volumetracing_accessor"; +import { addUserBoundingBoxAction } from "oxalis/model/actions/annotation_actions"; +import { setBusyBlockingInfoAction } from "oxalis/model/actions/ui_actions"; +import { + finishAnnotationStrokeAction, + updateSegmentAction, +} from "oxalis/model/actions/volumetracing_actions"; +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 { requestBucketModificationInVolumeTracing } from "oxalis/model/sagas/saga_helpers"; +import { Model } from "oxalis/singletons"; +import { call, put } from "typed-redux-saga"; +import { applyLabeledVoxelMapToAllMissingMags } from "./helpers"; + +function* getBoundingBoxForFloodFill( + position: Vector3, + currentViewport: OrthoView, +): Saga { + const fillMode = yield* select((state) => state.userConfiguration.fillMode); + const halfBoundingBoxSizeUVW = V3.scale(Constants.FLOOD_FILL_EXTENTS[fillMode], 0.5); + const currentViewportBounding = { + min: V3.sub(position, halfBoundingBoxSizeUVW), + max: V3.add(position, halfBoundingBoxSizeUVW), + }; + + if (fillMode === FillModeEnum._2D) { + // Only use current plane + const thirdDimension = Dimensions.thirdDimensionForPlane(currentViewport); + const numberOfSlices = 1; + currentViewportBounding.min[thirdDimension] = position[thirdDimension]; + currentViewportBounding.max[thirdDimension] = position[thirdDimension] + numberOfSlices; + } + + const datasetBoundingBox = yield* select((state) => getDatasetBoundingBox(state.dataset)); + const { min: clippedMin, max: clippedMax } = new BoundingBox( + currentViewportBounding, + ).intersectedWith(datasetBoundingBox); + return { + min: clippedMin, + max: clippedMax, + }; +} + +const FLOODFILL_PROGRESS_KEY = "FLOODFILL_PROGRESS_KEY"; +export function* floodFill(): Saga { + yield* take("INITIALIZE_VOLUMETRACING"); + const allowUpdate = yield* select((state) => state.tracing.restrictions.allowUpdate); + + while (allowUpdate) { + const floodFillAction = yield* take("FLOOD_FILL"); + + if (floodFillAction.type !== "FLOOD_FILL") { + throw new Error("Unexpected action. Satisfy typescript."); + } + + const { position: positionFloat, planeId } = floodFillAction; + const volumeTracing = yield* select(enforceActiveVolumeTracing); + if (volumeTracing.hasEditableMapping) { + const message = "Volume modification is not allowed when an editable mapping is active."; + Toast.error(message); + console.error(message); + continue; + } + const segmentationLayer = yield* call( + [Model, Model.getSegmentationTracingLayer], + volumeTracing.tracingId, + ); + const { cube } = segmentationLayer; + const seedPosition = Dimensions.roundCoordinate(positionFloat); + const activeCellId = volumeTracing.activeCellId; + const dimensionIndices = Dimensions.getIndices(planeId); + const requestedZoomStep = yield* select((state) => + getActiveMagIndexForLayer(state, segmentationLayer.name), + ); + const magInfo = yield* call(getMagInfo, segmentationLayer.mags); + const labeledZoomStep = magInfo.getClosestExistingIndex(requestedZoomStep); + const additionalCoordinates = yield* select((state) => state.flycam.additionalCoordinates); + const oldSegmentIdAtSeed = cube.getDataValue( + seedPosition, + additionalCoordinates, + null, + labeledZoomStep, + ); + + if (activeCellId === oldSegmentIdAtSeed) { + Toast.warning("The clicked voxel's id is already equal to the active segment id."); + continue; + } + + const busyBlockingInfo = yield* select((state) => state.uiInformation.busyBlockingInfo); + + if (busyBlockingInfo.isBusy) { + console.warn(`Ignoring floodfill request (reason: ${busyBlockingInfo.reason || "unknown"})`); + continue; + } + // As the flood fill will be applied to the volume layer, + // the potentially existing mapping should be locked to ensure a consistent state. + const isModificationAllowed = yield* call( + requestBucketModificationInVolumeTracing, + volumeTracing, + ); + if (!isModificationAllowed) { + continue; + } + yield* put(setBusyBlockingInfoAction(true, "Floodfill is being computed.")); + const boundingBoxForFloodFill = yield* call(getBoundingBoxForFloodFill, seedPosition, planeId); + const progressCallback = createProgressCallback({ + pauseDelay: 200, + successMessageDelay: 2000, + // Since only one floodfill operation can be active at any time, + // a hardcoded key is sufficient. + key: "FLOODFILL_PROGRESS_KEY", + }); + yield* call(progressCallback, false, "Performing floodfill..."); + console.time("cube.floodFill"); + const fillMode = yield* select((state) => state.userConfiguration.fillMode); + + const { + bucketsWithLabeledVoxelsMap: labelMasksByBucketAndW, + wasBoundingBoxExceeded, + coveredBoundingBox, + } = yield* call( + { context: cube, fn: cube.floodFill }, + seedPosition, + additionalCoordinates, + activeCellId, + dimensionIndices, + boundingBoxForFloodFill, + labeledZoomStep, + progressCallback, + fillMode === FillModeEnum._3D, + ); + console.timeEnd("cube.floodFill"); + yield* call(progressCallback, false, "Finalizing floodfill..."); + const indexSet: Set = new Set(); + + for (const labelMaskByIndex of labelMasksByBucketAndW.values()) { + for (const zIndex of labelMaskByIndex.keys()) { + indexSet.add(zIndex); + } + } + + console.time("applyLabeledVoxelMapToAllMissingMags"); + + for (const indexZ of indexSet) { + const labeledVoxelMapFromFloodFill: LabeledVoxelsMap = new Map(); + + for (const [bucketAddress, labelMaskByIndex] of labelMasksByBucketAndW.entries()) { + const map = labelMaskByIndex.get(indexZ); + + if (map != null) { + labeledVoxelMapFromFloodFill.set(bucketAddress, map); + } + } + + applyLabeledVoxelMapToAllMissingMags( + labeledVoxelMapFromFloodFill, + labeledZoomStep, + dimensionIndices, + magInfo, + cube, + activeCellId, + indexZ, + true, + ); + } + + yield* put(finishAnnotationStrokeAction(volumeTracing.tracingId)); + yield* put( + updateSegmentAction( + volumeTracing.activeCellId, + { + somePosition: seedPosition, + someAdditionalCoordinates: additionalCoordinates || undefined, + }, + volumeTracing.tracingId, + ), + ); + + console.timeEnd("applyLabeledVoxelMapToAllMissingMags"); + + if (wasBoundingBoxExceeded) { + yield* call( + progressCallback, + true, + <> + Floodfill is done, but terminated since the labeled volume got too large. A bounding box +
+ that represents the labeled volume was added.{Unicode.NonBreakingSpace} + message.destroy(FLOODFILL_PROGRESS_KEY)}> + Close + + , + { + successMessageDelay: 10000, + }, + ); + yield* put( + addUserBoundingBoxAction({ + boundingBox: coveredBoundingBox, + name: `Limits of flood-fill (source_id=${oldSegmentIdAtSeed}, target_id=${activeCellId}, seed=${seedPosition.join( + ",", + )}, timestamp=${new Date().getTime()})`, + color: Utils.getRandomColor(), + isVisible: true, + }), + ); + } else { + yield* call(progressCallback, true, "Floodfill done."); + } + + cube.triggerPushQueue(); + yield* put(setBusyBlockingInfoAction(false)); + + if (floodFillAction.callback != null) { + floodFillAction.callback(); + } + } +} diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx index 12e1919ac3..463e2ea4c1 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx @@ -1,41 +1,27 @@ -import { message } from "antd"; import { diffDiffableMaps } from "libs/diffable_map"; import { V3 } from "libs/mjs"; -import createProgressCallback from "libs/progress_callback"; import Toast from "libs/toast"; -import * as Utils from "libs/utils"; import _ from "lodash"; import memoizeOne from "memoize-one"; import type { AnnotationTool, - BoundingBoxType, ContourMode, - LabeledVoxelsMap, OrthoView, OverwriteMode, Vector3, } from "oxalis/constants"; -import Constants, { +import { AnnotationToolEnum, ContourModeEnum, - FillModeEnum, OrthoViews, - Unicode, OverwriteModeEnum, } from "oxalis/constants"; import getSceneController from "oxalis/controller/scene_controller_provider"; import { CONTOUR_COLOR_DELETE, CONTOUR_COLOR_NORMAL } from "oxalis/geometries/helper_geometries"; -import { - getDatasetBoundingBox, - getMaximumSegmentIdForLayer, - getMagInfo, -} from "oxalis/model/accessors/dataset_accessor"; -import { - getPosition, - getActiveMagIndexForLayer, - getRotation, -} from "oxalis/model/accessors/flycam_accessor"; +import messages from "messages"; +import { getMaximumSegmentIdForLayer } from "oxalis/model/accessors/dataset_accessor"; +import { getPosition, getRotation } from "oxalis/model/accessors/flycam_accessor"; import { isBrushTool, isTraceTool, @@ -56,7 +42,6 @@ import type { AddAdHocMeshAction, AddPrecomputedMeshAction, } from "oxalis/model/actions/annotation_actions"; -import { addUserBoundingBoxAction } from "oxalis/model/actions/annotation_actions"; import { updateTemporarySettingAction, updateUserSettingAction, @@ -64,9 +49,9 @@ import { import { setBusyBlockingInfoAction, setToolAction } from "oxalis/model/actions/ui_actions"; import type { ClickSegmentAction, - SetActiveCellAction, CreateCellAction, DeleteSegmentDataAction, + SetActiveCellAction, } from "oxalis/model/actions/volumetracing_actions"; import { finishAnnotationStrokeAction, @@ -74,9 +59,7 @@ import { setSelectedSegmentsOrGroupAction, updateSegmentAction, } from "oxalis/model/actions/volumetracing_actions"; -import BoundingBox from "oxalis/model/bucket_data_handling/bounding_box"; import { markVolumeTransactionEnd } from "oxalis/model/bucket_data_handling/bucket"; -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 listenToMinCut from "oxalis/model/sagas/min_cut_saga"; @@ -85,34 +68,27 @@ import { requestBucketModificationInVolumeTracing, takeEveryUnlessBusy, } from "oxalis/model/sagas/saga_helpers"; -import { - deleteSegmentDataVolumeAction, - type UpdateAction, - updateSegmentGroups, -} from "oxalis/model/sagas/update_actions"; import { createSegmentVolumeAction, + deleteSegmentDataVolumeAction, deleteSegmentVolumeAction, removeFallbackLayer, + updateMappingName, + updateSegmentGroups, updateSegmentVolumeAction, updateUserBoundingBoxes, updateVolumeTracing, - updateMappingName, + type UpdateAction, } from "oxalis/model/sagas/update_actions"; import type VolumeLayer from "oxalis/model/volumetracing/volumelayer"; import { Model, api } from "oxalis/singletons"; import type { Flycam, SegmentMap, VolumeTracing } from "oxalis/store"; +import type { ActionPattern } from "redux-saga/effects"; import { actionChannel, call, fork, put, takeEvery, takeLatest } from "typed-redux-saga"; -import { - applyLabeledVoxelMapToAllMissingMags, - createVolumeLayer, - labelWithVoxelBuffer2D, - type BooleanBox, -} from "./volume/helpers"; -import maybeInterpolateSegmentationLayer from "./volume/volume_interpolation_saga"; -import messages from "messages"; import { pushSaveQueueTransaction } from "../actions/save_actions"; -import type { ActionPattern } from "redux-saga/effects"; +import { createVolumeLayer, labelWithVoxelBuffer2D, type BooleanBox } from "./volume/helpers"; +import maybeInterpolateSegmentationLayer from "./volume/volume_interpolation_saga"; +import { floodFill } from "./volume/floodfill_saga"; const OVERWRITE_EMPTY_WARNING_KEY = "OVERWRITE-EMPTY-WARNING"; @@ -370,212 +346,6 @@ export function* editVolumeLayerAsync(): Saga { } } -function* getBoundingBoxForFloodFill( - position: Vector3, - currentViewport: OrthoView, -): Saga { - const fillMode = yield* select((state) => state.userConfiguration.fillMode); - const halfBoundingBoxSizeUVW = V3.scale(Constants.FLOOD_FILL_EXTENTS[fillMode], 0.5); - const currentViewportBounding = { - min: V3.sub(position, halfBoundingBoxSizeUVW), - max: V3.add(position, halfBoundingBoxSizeUVW), - }; - - if (fillMode === FillModeEnum._2D) { - // Only use current plane - const thirdDimension = Dimensions.thirdDimensionForPlane(currentViewport); - const numberOfSlices = 1; - currentViewportBounding.min[thirdDimension] = position[thirdDimension]; - currentViewportBounding.max[thirdDimension] = position[thirdDimension] + numberOfSlices; - } - - const datasetBoundingBox = yield* select((state) => getDatasetBoundingBox(state.dataset)); - const { min: clippedMin, max: clippedMax } = new BoundingBox( - currentViewportBounding, - ).intersectedWith(datasetBoundingBox); - return { - min: clippedMin, - max: clippedMax, - }; -} - -const FLOODFILL_PROGRESS_KEY = "FLOODFILL_PROGRESS_KEY"; -export function* floodFill(): Saga { - yield* take("INITIALIZE_VOLUMETRACING"); - const allowUpdate = yield* select((state) => state.tracing.restrictions.allowUpdate); - - while (allowUpdate) { - const floodFillAction = yield* take("FLOOD_FILL"); - - if (floodFillAction.type !== "FLOOD_FILL") { - throw new Error("Unexpected action. Satisfy typescript."); - } - - const { position: positionFloat, planeId } = floodFillAction; - const volumeTracing = yield* select(enforceActiveVolumeTracing); - if (volumeTracing.hasEditableMapping) { - const message = "Volume modification is not allowed when an editable mapping is active."; - Toast.error(message); - console.error(message); - continue; - } - const segmentationLayer = yield* call( - [Model, Model.getSegmentationTracingLayer], - volumeTracing.tracingId, - ); - const { cube } = segmentationLayer; - const seedPosition = Dimensions.roundCoordinate(positionFloat); - const activeCellId = volumeTracing.activeCellId; - const dimensionIndices = Dimensions.getIndices(planeId); - const requestedZoomStep = yield* select((state) => - getActiveMagIndexForLayer(state, segmentationLayer.name), - ); - const magInfo = yield* call(getMagInfo, segmentationLayer.mags); - const labeledZoomStep = magInfo.getClosestExistingIndex(requestedZoomStep); - const additionalCoordinates = yield* select((state) => state.flycam.additionalCoordinates); - const oldSegmentIdAtSeed = cube.getDataValue( - seedPosition, - additionalCoordinates, - null, - labeledZoomStep, - ); - - if (activeCellId === oldSegmentIdAtSeed) { - Toast.warning("The clicked voxel's id is already equal to the active segment id."); - continue; - } - - const busyBlockingInfo = yield* select((state) => state.uiInformation.busyBlockingInfo); - - if (busyBlockingInfo.isBusy) { - console.warn(`Ignoring floodfill request (reason: ${busyBlockingInfo.reason || "unknown"})`); - continue; - } - // As the flood fill will be applied to the volume layer, - // the potentially existing mapping should be locked to ensure a consistent state. - const isModificationAllowed = yield* call( - requestBucketModificationInVolumeTracing, - volumeTracing, - ); - if (!isModificationAllowed) { - continue; - } - yield* put(setBusyBlockingInfoAction(true, "Floodfill is being computed.")); - const boundingBoxForFloodFill = yield* call(getBoundingBoxForFloodFill, seedPosition, planeId); - const progressCallback = createProgressCallback({ - pauseDelay: 200, - successMessageDelay: 2000, - // Since only one floodfill operation can be active at any time, - // a hardcoded key is sufficient. - key: "FLOODFILL_PROGRESS_KEY", - }); - yield* call(progressCallback, false, "Performing floodfill..."); - console.time("cube.floodFill"); - const fillMode = yield* select((state) => state.userConfiguration.fillMode); - - const { - bucketsWithLabeledVoxelsMap: labelMasksByBucketAndW, - wasBoundingBoxExceeded, - coveredBoundingBox, - } = yield* call( - { context: cube, fn: cube.floodFill }, - seedPosition, - additionalCoordinates, - activeCellId, - dimensionIndices, - boundingBoxForFloodFill, - labeledZoomStep, - progressCallback, - fillMode === FillModeEnum._3D, - ); - console.timeEnd("cube.floodFill"); - yield* call(progressCallback, false, "Finalizing floodfill..."); - const indexSet: Set = new Set(); - - for (const labelMaskByIndex of labelMasksByBucketAndW.values()) { - for (const zIndex of labelMaskByIndex.keys()) { - indexSet.add(zIndex); - } - } - - console.time("applyLabeledVoxelMapToAllMissingMags"); - - for (const indexZ of indexSet) { - const labeledVoxelMapFromFloodFill: LabeledVoxelsMap = new Map(); - - for (const [bucketAddress, labelMaskByIndex] of labelMasksByBucketAndW.entries()) { - const map = labelMaskByIndex.get(indexZ); - - if (map != null) { - labeledVoxelMapFromFloodFill.set(bucketAddress, map); - } - } - - applyLabeledVoxelMapToAllMissingMags( - labeledVoxelMapFromFloodFill, - labeledZoomStep, - dimensionIndices, - magInfo, - cube, - activeCellId, - indexZ, - true, - ); - } - - yield* put(finishAnnotationStrokeAction(volumeTracing.tracingId)); - yield* put( - updateSegmentAction( - volumeTracing.activeCellId, - { - somePosition: seedPosition, - someAdditionalCoordinates: additionalCoordinates || undefined, - }, - volumeTracing.tracingId, - ), - ); - - console.timeEnd("applyLabeledVoxelMapToAllMissingMags"); - - if (wasBoundingBoxExceeded) { - yield* call( - progressCallback, - true, - <> - Floodfill is done, but terminated since the labeled volume got too large. A bounding box -
- that represents the labeled volume was added.{Unicode.NonBreakingSpace} - message.destroy(FLOODFILL_PROGRESS_KEY)}> - Close - - , - { - successMessageDelay: 10000, - }, - ); - yield* put( - addUserBoundingBoxAction({ - boundingBox: coveredBoundingBox, - name: `Limits of flood-fill (source_id=${oldSegmentIdAtSeed}, target_id=${activeCellId}, seed=${seedPosition.join( - ",", - )}, timestamp=${new Date().getTime()})`, - color: Utils.getRandomColor(), - isVisible: true, - }), - ); - } else { - yield* call(progressCallback, true, "Floodfill done."); - } - - cube.triggerPushQueue(); - yield* put(setBusyBlockingInfoAction(false)); - - if (floodFillAction.callback != null) { - floodFillAction.callback(); - } - } -} - export function* finishLayer( layer: VolumeLayer, activeTool: AnnotationTool, From d921995f72c4f3311ab84c5d66ce2db54bd0c828 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 9 Dec 2024 15:35:04 +0100 Subject: [PATCH 05/24] hardcode floodfill constraint to current bbox --- .../model/accessors/tracing_accessor.ts | 12 ++++++++++++ .../model/bucket_data_handling/data_cube.ts | 19 ++++++++++++------- .../model/sagas/volume/floodfill_saga.tsx | 7 +++++++ 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/frontend/javascripts/oxalis/model/accessors/tracing_accessor.ts b/frontend/javascripts/oxalis/model/accessors/tracing_accessor.ts index fc559dedd6..67276ba873 100644 --- a/frontend/javascripts/oxalis/model/accessors/tracing_accessor.ts +++ b/frontend/javascripts/oxalis/model/accessors/tracing_accessor.ts @@ -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 { Vector3 } from "oxalis/constants"; export function maybeGetSomeTracing( tracing: Tracing, @@ -86,7 +88,17 @@ export function selectTracing( return tracing; } + export const getUserBoundingBoxesFromState = (state: OxalisState): Array => { const maybeSomeTracing = maybeGetSomeTracing(state.tracing); return maybeSomeTracing != null ? maybeSomeTracing.userBoundingBoxes : []; }; + +export const getUserBoundingBoxesThatContainPosition = ( + state: OxalisState, + position: Vector3, +): Array => { + const bboxes = getUserBoundingBoxesFromState(state); + + return bboxes.filter((el) => new BoundingBox(el.boundingBox).containsPoint(position)); +}; 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 102142b2f2..b9ae7b1b94 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts @@ -487,7 +487,7 @@ class DataCube { additionalCoordinates: AdditionalCoordinate[] | null, segmentIdNumber: number, dimensionIndices: DimensionMap, - floodfillBoundingBox: BoundingBoxType, + _floodfillBoundingBox: BoundingBoxType, zoomStep: number, progressCallback: ProgressCallback, use3D: boolean, @@ -507,6 +507,8 @@ class DataCube { // 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); @@ -690,16 +692,19 @@ class DataCube { } else { // Label the current neighbour and add it to the neighbourVoxelStackUvw to iterate over its neighbours. const neighbourVoxelIndex = this.getVoxelIndexByVoxelOffset(neighbourVoxelXyz); - - if (bucketData[neighbourVoxelIndex] === sourceSegmentId) { + const currentGlobalPosition = V3.add( + currentGlobalBucketPosition, + V3.scale3(adjustedNeighbourVoxelXyz, currentMag), + ); + + if ( + bucketData[neighbourVoxelIndex] === sourceSegmentId && + floodfillBoundingBox.containsPoint(currentGlobalPosition) + ) { 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]), diff --git a/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx b/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx index e85050cf8d..f1c0f644ed 100644 --- a/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx +++ b/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx @@ -22,12 +22,19 @@ import { select, take } from "oxalis/model/sagas/effect-generators"; import { requestBucketModificationInVolumeTracing } from "oxalis/model/sagas/saga_helpers"; import { Model } from "oxalis/singletons"; import { call, put } from "typed-redux-saga"; +import { getUserBoundingBoxesThatContainPosition } from "../../accessors/tracing_accessor"; import { applyLabeledVoxelMapToAllMissingMags } from "./helpers"; function* getBoundingBoxForFloodFill( position: Vector3, currentViewport: OrthoView, ): Saga { + // todop: make this optional somehow + const bboxes = yield* select((state) => getUserBoundingBoxesThatContainPosition(state, position)); + if (bboxes.length > 0) { + return bboxes[0].boundingBox; + } + const fillMode = yield* select((state) => state.userConfiguration.fillMode); const halfBoundingBoxSizeUVW = V3.scale(Constants.FLOOD_FILL_EXTENTS[fillMode], 0.5); const currentViewportBounding = { From f9ff2ae0f49a96bc9b59027941f96a8dd196a7a5 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 9 Dec 2024 15:57:29 +0100 Subject: [PATCH 06/24] add toggle to navbar to enable/disable bbox restriction for floodfill --- frontend/javascripts/oxalis/default_state.ts | 1 + .../model/sagas/volume/floodfill_saga.tsx | 29 ++++++++++++--- frontend/javascripts/oxalis/store.ts | 1 + .../oxalis/view/action-bar/toolbar_view.tsx | 36 ++++++++++++++++++- 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/frontend/javascripts/oxalis/default_state.ts b/frontend/javascripts/oxalis/default_state.ts index cee355bb9a..31406fec4c 100644 --- a/frontend/javascripts/oxalis/default_state.ts +++ b/frontend/javascripts/oxalis/default_state.ts @@ -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: { diff --git a/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx b/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx index f1c0f644ed..25e9fdd8db 100644 --- a/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx +++ b/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx @@ -24,15 +24,30 @@ import { Model } from "oxalis/singletons"; import { call, put } from "typed-redux-saga"; import { getUserBoundingBoxesThatContainPosition } from "../../accessors/tracing_accessor"; import { applyLabeledVoxelMapToAllMissingMags } from "./helpers"; +import _ from "lodash"; function* getBoundingBoxForFloodFill( position: Vector3, currentViewport: OrthoView, -): Saga { - // todop: make this optional somehow - const bboxes = yield* select((state) => getUserBoundingBoxesThatContainPosition(state, position)); - if (bboxes.length > 0) { - return bboxes[0].boundingBox; +): Saga { + const isRestrictedToBoundingBox = yield* select( + (state) => state.userConfiguration.isFloodfillRestrictedToBoundingBox, + ); + if (isRestrictedToBoundingBox) { + const bboxes = yield* select((state) => + getUserBoundingBoxesThatContainPosition(state, position), + ); + if (bboxes.length > 0) { + const smallestBbox = _.sortBy(bboxes, (bbox) => + new BoundingBox(bbox.boundingBox).getVolume(), + )[0]; + return smallestBbox.boundingBox; + } else { + return { + failureReason: + "No bounding box encloses the clicked position. Either disable the bounding box restriction or ensure a bounding box exists around the clicked position.", + }; + } } const fillMode = yield* select((state) => state.userConfiguration.fillMode); @@ -123,6 +138,10 @@ export function* floodFill(): Saga { } yield* put(setBusyBlockingInfoAction(true, "Floodfill is being computed.")); const boundingBoxForFloodFill = yield* call(getBoundingBoxForFloodFill, seedPosition, planeId); + if ("failureReason" in boundingBoxForFloodFill) { + Toast.warning(boundingBoxForFloodFill.failureReason); + return; + } const progressCallback = createProgressCallback({ pauseDelay: 200, successMessageDelay: 2000, diff --git a/frontend/javascripts/oxalis/store.ts b/frontend/javascripts/oxalis/store.ts index 26fb97c0dd..78d4c2c367 100644 --- a/frontend/javascripts/oxalis/store.ts +++ b/frontend/javascripts/oxalis/store.ts @@ -393,6 +393,7 @@ export type UserConfiguration = { // how volume annotations overwrite existing voxels. readonly overwriteMode: OverwriteMode; readonly fillMode: FillMode; + readonly isFloodfillRestrictedToBoundingBox: boolean; readonly interpolationMode: InterpolationMode; readonly useLegacyBindings: boolean; readonly quickSelect: QuickSelectConfig; diff --git a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx index 2693a3d8cc..00b6eb32cb 100644 --- a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx +++ b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx @@ -1285,7 +1285,7 @@ function ToolSpecificSettings({ ) : null} - {adaptedActiveTool === AnnotationToolEnum.FILL_CELL ? : null} + {adaptedActiveTool === AnnotationToolEnum.FILL_CELL ? : null} {adaptedActiveTool === AnnotationToolEnum.PROOFREAD ? : null} @@ -1369,6 +1369,40 @@ const handleSetFillMode = (event: RadioChangeEvent) => { Store.dispatch(updateUserSettingAction("fillMode", event.target.value)); }; +function FloodFillSettings() { + const dispatch = useDispatch(); + const isRestrictedToBoundingBox = useSelector( + (state: OxalisState) => state.userConfiguration.isFloodfillRestrictedToBoundingBox, + ); + const toggleRestrictFloodfillToBoundingBox = () => { + dispatch( + updateUserSettingAction("isFloodfillRestrictedToBoundingBox", !isRestrictedToBoundingBox), + ); + }; + return ( +
+ + + + Bounding Box Icon + +
+ ); +} + function FillModeSwitch() { const fillMode = useSelector((state: OxalisState) => state.userConfiguration.fillMode); return ( From 4b29ceccec7f525883ada30ad909bd39183b7463 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 9 Dec 2024 15:57:56 +0100 Subject: [PATCH 07/24] fix linting --- frontend/javascripts/oxalis/model/accessors/tracing_accessor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/oxalis/model/accessors/tracing_accessor.ts b/frontend/javascripts/oxalis/model/accessors/tracing_accessor.ts index 67276ba873..3cc0b15b08 100644 --- a/frontend/javascripts/oxalis/model/accessors/tracing_accessor.ts +++ b/frontend/javascripts/oxalis/model/accessors/tracing_accessor.ts @@ -11,7 +11,7 @@ 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 { Vector3 } from "oxalis/constants"; +import type { Vector3 } from "oxalis/constants"; export function maybeGetSomeTracing( tracing: Tracing, From fbcbd6c0b8ca682e6657f9b441ac4ad6407c6ac9 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 10 Dec 2024 09:30:46 +0100 Subject: [PATCH 08/24] debug test --- .../oxalis/model/bucket_data_handling/data_cube.ts | 1 + .../javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx | 3 ++- .../volumetracing/volumetracing_saga_integration.spec.ts | 4 +++- 3 files changed, 6 insertions(+), 2 deletions(-) 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 b9ae7b1b94..3fc6ddd1ad 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts @@ -508,6 +508,7 @@ class DataCube { // not all of the target area in the neighbour bucket might be filled. const floodfillBoundingBox = new BoundingBox(_floodfillBoundingBox); + console.log("floodfillBoundingBox", floodfillBoundingBox); // Helper function to convert between xyz and uvw (both directions) const transpose = (voxel: Vector3): Vector3 => diff --git a/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx b/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx index 25e9fdd8db..012d3d426f 100644 --- a/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx +++ b/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx @@ -33,6 +33,7 @@ function* getBoundingBoxForFloodFill( const isRestrictedToBoundingBox = yield* select( (state) => state.userConfiguration.isFloodfillRestrictedToBoundingBox, ); + console.log("########################### isRestrictedToBoundingBox", isRestrictedToBoundingBox); if (isRestrictedToBoundingBox) { const bboxes = yield* select((state) => getUserBoundingBoxesThatContainPosition(state, position), @@ -147,7 +148,7 @@ export function* floodFill(): Saga { successMessageDelay: 2000, // Since only one floodfill operation can be active at any time, // a hardcoded key is sufficient. - key: "FLOODFILL_PROGRESS_KEY", + key: FLOODFILL_PROGRESS_KEY, }); yield* call(progressCallback, false, "Performing floodfill..."); console.time("cube.floodFill"); diff --git a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration.spec.ts b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration.spec.ts index a4ff1868ad..74192efbce 100644 --- a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration.spec.ts +++ b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration.spec.ts @@ -32,6 +32,8 @@ import type { ApiInterface } from "oxalis/api/api_latest"; import type { DataBucket } from "oxalis/model/bucket_data_handling/bucket"; import { MISSING_GROUP_ID } from "oxalis/view/right-border-tabs/tree_hierarchy_view_helpers"; +// + const { dispatchUndoAsync, dispatchRedoAsync, discardSaveQueuesAction } = mockRequire.reRequire( "oxalis/model/actions/save_actions", ); @@ -274,7 +276,7 @@ test.serial("Executing a floodfill in mag 2", async (t) => { ); } }); -test.serial("Executing a floodfill in mag 1 (long operation)", async (t) => { +test.only("Executing a floodfill in mag 1 (long operation)", async (t) => { t.context.mocks.Request.sendJSONReceiveArraybufferWithHeaders = createBucketResponseFunction( Uint16Array, 0, From daf2c0c574e4868dd74dc877ad91ba9b883b1786 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 12 Dec 2024 20:35:04 +0100 Subject: [PATCH 09/24] only create bbox after *3d* floodfill terminated early --- .../model/sagas/volume/floodfill_saga.tsx | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx b/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx index 012d3d426f..88152551ec 100644 --- a/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx +++ b/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx @@ -219,13 +219,12 @@ export function* floodFill(): Saga { console.timeEnd("applyLabeledVoxelMapToAllMissingMags"); if (wasBoundingBoxExceeded) { + const warningDetails = fillMode === FillModeEnum._2D ? "Please check the borders of the filled area manually and use the fill tool again if necessary." : "A bounding box that represents the labeled volume was added so that you can check the borders manually." yield* call( progressCallback, true, <> - Floodfill is done, but terminated since the labeled volume got too large. A bounding box -
- that represents the labeled volume was added.{Unicode.NonBreakingSpace} + Floodfill is done, but terminated since the labeled volume got too large. ${warningDetails} {Unicode.NonBreakingSpace} message.destroy(FLOODFILL_PROGRESS_KEY)}> Close @@ -234,16 +233,20 @@ export function* floodFill(): Saga { successMessageDelay: 10000, }, ); - yield* put( - addUserBoundingBoxAction({ - boundingBox: coveredBoundingBox, - name: `Limits of flood-fill (source_id=${oldSegmentIdAtSeed}, target_id=${activeCellId}, seed=${seedPosition.join( - ",", - )}, timestamp=${new Date().getTime()})`, - color: Utils.getRandomColor(), - isVisible: true, - }), - ); + if (fillMode === FillModeEnum._3D) { + // The bounding box is overkill for the 2D mode because in that case, + // it's trivial to check the borders manually. + yield* put( + addUserBoundingBoxAction({ + boundingBox: coveredBoundingBox, + name: `Limits of flood-fill (source_id=${oldSegmentIdAtSeed}, target_id=${activeCellId}, seed=${seedPosition.join( + ",", + )}, timestamp=${new Date().getTime()})`, + color: Utils.getRandomColor(), + isVisible: true, + }), + ); + } } else { yield* call(progressCallback, true, "Floodfill done."); } From b6672209323814c7901c5604e43cb62b0604c7e0 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 12 Dec 2024 21:42:30 +0100 Subject: [PATCH 10/24] fix test and creation of bbox in exceeding case --- .../model/bucket_data_handling/data_cube.ts | 55 ++++++++++--------- .../volumetracing_saga_integration.spec.ts | 11 ++-- 2 files changed, 35 insertions(+), 31 deletions(-) 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 3fc6ddd1ad..dba50eb995 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts @@ -698,33 +698,34 @@ class DataCube { V3.scale3(adjustedNeighbourVoxelXyz, currentMag), ); - if ( - bucketData[neighbourVoxelIndex] === sourceSegmentId && - floodfillBoundingBox.containsPoint(currentGlobalPosition) - ) { - bucketData[neighbourVoxelIndex] = segmentId; - markUvwInSliceAsLabeled(neighbourVoxelUvw); - neighbourVoxelStackUvw.pushVoxel(neighbourVoxelUvw); - labeledVoxelCount++; - 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 (bucketData[neighbourVoxelIndex] === sourceSegmentId) { + if (floodfillBoundingBox.containsPoint(currentGlobalPosition)) { + bucketData[neighbourVoxelIndex] = segmentId; + markUvwInSliceAsLabeled(neighbourVoxelUvw); + neighbourVoxelStackUvw.pushVoxel(neighbourVoxelUvw); + labeledVoxelCount++; + 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...`, + ); + } + } else { + wasBoundingBoxExceeded = true; } } } diff --git a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration.spec.ts b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration.spec.ts index 74192efbce..a3a5369912 100644 --- a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration.spec.ts +++ b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration.spec.ts @@ -1,7 +1,7 @@ /* eslint-disable no-await-in-loop */ import "test/sagas/saga_integration.mock"; import _ from "lodash"; -import { +import Constants, { AnnotationToolEnum, ContourModeEnum, FillModeEnum, @@ -276,6 +276,7 @@ test.serial("Executing a floodfill in mag 2", async (t) => { ); } }); + test.only("Executing a floodfill in mag 1 (long operation)", async (t) => { t.context.mocks.Request.sendJSONReceiveArraybufferWithHeaders = createBucketResponseFunction( Uint16Array, @@ -293,6 +294,8 @@ test.only("Executing a floodfill in mag 1 (long operation)", async (t) => { Store.dispatch(updateUserSettingAction("fillMode", FillModeEnum._3D)); await dispatchFloodfillAsync(Store.dispatch, paintCenter, OrthoViews.PLANE_XY); + const EXPECTED_HALF_EXTENT = V3.scale(Constants.FLOOD_FILL_EXTENTS[FillModeEnum._3D], 0.5); + async function assertFloodFilledState() { t.is( await t.context.api.data.getDataValue(volumeTracingLayerName, paintCenter, 0), @@ -300,8 +303,8 @@ test.only("Executing a floodfill in mag 1 (long operation)", async (t) => { ); t.false(hasRootSagaCrashed()); const cuboidData = await t.context.api.data.getDataForBoundingBox(volumeTracingLayerName, { - min: [128 - 64, 128 - 64, 128 - 32], - max: [128 + 64, 128 + 64, 128 + 32], + min: V3.sub(paintCenter, EXPECTED_HALF_EXTENT), + max: V3.add(paintCenter, EXPECTED_HALF_EXTENT), }); // There should be no item which does not equal floodingCellId t.is( @@ -326,7 +329,7 @@ test.only("Executing a floodfill in mag 1 (long operation)", async (t) => { // Assert state after flood-fill await assertFloodFilledState(); - // Undo created bounding box by flood fill and flood fill and assert initial state. + // Undo [the bounding box created by the flood fill] and [the flood fill itself] and assert initial state. await dispatchUndoAsync(Store.dispatch); await dispatchUndoAsync(Store.dispatch); await assertInitialState(); From f714568a807aebc67bc5ee68ef333ec4c7f957e3 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 12 Dec 2024 21:43:08 +0100 Subject: [PATCH 11/24] format --- .../oxalis/model/bucket_data_handling/data_cube.ts | 1 - .../oxalis/model/sagas/volume/floodfill_saga.tsx | 8 ++++++-- 2 files changed, 6 insertions(+), 3 deletions(-) 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 dba50eb995..9d011ffe53 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts @@ -508,7 +508,6 @@ class DataCube { // not all of the target area in the neighbour bucket might be filled. const floodfillBoundingBox = new BoundingBox(_floodfillBoundingBox); - console.log("floodfillBoundingBox", floodfillBoundingBox); // Helper function to convert between xyz and uvw (both directions) const transpose = (voxel: Vector3): Vector3 => diff --git a/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx b/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx index 88152551ec..72050e76c8 100644 --- a/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx +++ b/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx @@ -219,12 +219,16 @@ export function* floodFill(): Saga { console.timeEnd("applyLabeledVoxelMapToAllMissingMags"); if (wasBoundingBoxExceeded) { - const warningDetails = fillMode === FillModeEnum._2D ? "Please check the borders of the filled area manually and use the fill tool again if necessary." : "A bounding box that represents the labeled volume was added so that you can check the borders manually." + const warningDetails = + fillMode === FillModeEnum._2D + ? "Please check the borders of the filled area manually and use the fill tool again if necessary." + : "A bounding box that represents the labeled volume was added so that you can check the borders manually."; yield* call( progressCallback, true, <> - Floodfill is done, but terminated since the labeled volume got too large. ${warningDetails} {Unicode.NonBreakingSpace} + Floodfill is done, but terminated since the labeled volume got too large. $ + {warningDetails} {Unicode.NonBreakingSpace} message.destroy(FLOODFILL_PROGRESS_KEY)}> Close From 20ec8c4b9dac6bbcb7db18d0b377cc9db6eb2b5a Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 12 Dec 2024 21:46:35 +0100 Subject: [PATCH 12/24] remove only modifier again --- .../sagas/volumetracing/volumetracing_saga_integration.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration.spec.ts b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration.spec.ts index a3a5369912..d44a8431e9 100644 --- a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration.spec.ts +++ b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga_integration.spec.ts @@ -277,7 +277,7 @@ test.serial("Executing a floodfill in mag 2", async (t) => { } }); -test.only("Executing a floodfill in mag 1 (long operation)", async (t) => { +test.serial("Executing a floodfill in mag 1 (long operation)", async (t) => { t.context.mocks.Request.sendJSONReceiveArraybufferWithHeaders = createBucketResponseFunction( Uint16Array, 0, From be9df8274520b57e1796d36a62f65c3830e2db91 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Fri, 13 Dec 2024 10:58:31 +0100 Subject: [PATCH 13/24] fix floodfill saga termination; improve ui --- .../oxalis/model/sagas/volume/floodfill_saga.tsx | 14 ++++++++++---- .../oxalis/view/action-bar/toolbar_view.tsx | 7 ++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx b/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx index 72050e76c8..86a180b556 100644 --- a/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx +++ b/frontend/javascripts/oxalis/model/sagas/volume/floodfill_saga.tsx @@ -77,6 +77,7 @@ function* getBoundingBoxForFloodFill( } const FLOODFILL_PROGRESS_KEY = "FLOODFILL_PROGRESS_KEY"; +const NO_FLOODFILL_BBOX_TOAST_KEY = "NO_FLOODFILL_BBOX"; export function* floodFill(): Saga { yield* take("INITIALIZE_VOLUMETRACING"); const allowUpdate = yield* select((state) => state.tracing.restrictions.allowUpdate); @@ -137,12 +138,16 @@ export function* floodFill(): Saga { if (!isModificationAllowed) { continue; } - yield* put(setBusyBlockingInfoAction(true, "Floodfill is being computed.")); const boundingBoxForFloodFill = yield* call(getBoundingBoxForFloodFill, seedPosition, planeId); if ("failureReason" in boundingBoxForFloodFill) { - Toast.warning(boundingBoxForFloodFill.failureReason); - return; + Toast.warning(boundingBoxForFloodFill.failureReason, { + key: NO_FLOODFILL_BBOX_TOAST_KEY, + }); + continue; + } else { + Toast.close(NO_FLOODFILL_BBOX_TOAST_KEY); } + yield* put(setBusyBlockingInfoAction(true, "Floodfill is being computed.")); const progressCallback = createProgressCallback({ pauseDelay: 200, successMessageDelay: 2000, @@ -227,7 +232,8 @@ export function* floodFill(): Saga { progressCallback, true, <> - Floodfill is done, but terminated since the labeled volume got too large. $ + Floodfill is done, but terminated since the labeled volume got too large. +
{warningDetails} {Unicode.NonBreakingSpace} message.destroy(FLOODFILL_PROGRESS_KEY)}> Close diff --git a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx index 00b6eb32cb..96e6ed99e0 100644 --- a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx +++ b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx @@ -1388,16 +1388,13 @@ function FloodFillSettings() { opacity: isRestrictedToBoundingBox ? 1 : 0.5, marginLeft: 12, }} + type={isRestrictedToBoundingBox ? "primary" : "default"} onClick={toggleRestrictFloodfillToBoundingBox} title={ "When enabled, the floodfill will be restricted to the bounding box enclosed by the clicked position. If multiple bounding boxes enclose that position, the smallest is used." } > - Bounding Box Icon + Restrict to BBox
); From 8349c0c5aa9cbfcaf5e285075e2e33c42b66fd70 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Fri, 13 Dec 2024 11:06:49 +0100 Subject: [PATCH 14/24] fix flickering of undo/redo buttons when clicking them --- frontend/javascripts/components/async_clickables.tsx | 11 +++++++---- .../oxalis/view/action-bar/tracing_actions_view.tsx | 4 ++-- frontend/stylesheets/trace_view/_action_bar.less | 5 +++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/frontend/javascripts/components/async_clickables.tsx b/frontend/javascripts/components/async_clickables.tsx index 9bbafae969..1b63ccb17c 100644 --- a/frontend/javascripts/components/async_clickables.tsx +++ b/frontend/javascripts/components/async_clickables.tsx @@ -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"; @@ -47,9 +47,12 @@ export function AsyncButton(props: AsyncButtonProps) { const effectiveChildren = hideContentWhenLoading && isLoading ? null : children; return ( - + {/* Avoid weird animation when icons swap */} + + + ); } diff --git a/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.tsx b/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.tsx index a73f91a2e5..ef0fcf610f 100644 --- a/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.tsx +++ b/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.tsx @@ -496,7 +496,7 @@ class TracingActionsView extends React.PureComponent { hasTracing ? [ {