Skip to content

Commit c3729ea

Browse files
frcrothphilippottoMichael Büßemeyer
authored
Warn user when using precomputed meshes after brushing (#8218)
* Add key in volumetracing to store if volume bucket data has changed * show warning in context menu for loading precomputed meshes when bucket data has changed * avoid unnecessary rerenders in context menu * ensure SET_VOLUME_BUCKET_DATA_HAS_CHANGED is dispatched when buckets are changed * add missing file * fix linting * rename load_mesh_menu_item_label file * use action creator instead of hardcoding action * fix that volumeBucketDataHashChanged was not handled when the server sent it * add comments * update snapshots * add changelog entry * remove unintentional changelog entry * change action property from layerName to tracingId --------- Co-authored-by: Philipp Otto <philipp.4096@gmail.com> Co-authored-by: Philipp Otto <philippotto@users.noreply.github.com> Co-authored-by: Michael Büßemeyer <frameworklinux+MichaelBuessemeyer@users.noreply.github.com>
1 parent fb1f275 commit c3729ea

File tree

17 files changed

+140
-35
lines changed

17 files changed

+140
-35
lines changed

app/models/annotation/AnnotationService.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@ class AnnotationService @Inject()(
179179
mappingName = mappingName,
180180
mags = magsRestricted.map(vec3IntToProto),
181181
hasSegmentIndex = Some(fallbackLayer.isEmpty || fallbackLayerHasSegmentIndex),
182-
additionalAxes = AdditionalAxis.toProto(additionalAxes)
182+
additionalAxes = AdditionalAxis.toProto(additionalAxes),
183+
volumeBucketDataHasChanged = Some(false)
183184
)
184185
}
185186

frontend/javascripts/oxalis/model/actions/volumetracing_actions.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ export type SetSegmentGroupsAction = ReturnType<typeof setSegmentGroupsAction>;
4545
export type SetExpandedSegmentGroupsAction = ReturnType<typeof setExpandedSegmentGroupsAction>;
4646
export type SetHasEditableMappingAction = ReturnType<typeof setHasEditableMappingAction>;
4747
export type SetMappingIsLockedAction = ReturnType<typeof setMappingIsLockedAction>;
48+
export type SetVolumeBucketDataHasChangedAction = ReturnType<
49+
typeof setVolumeBucketDataHasChangedAction
50+
>;
4851

4952
export type ComputeQuickSelectForRectAction = ReturnType<typeof computeQuickSelectForRectAction>;
5053
export type ComputeQuickSelectForPointAction = ReturnType<typeof computeQuickSelectForPointAction>;
@@ -99,6 +102,7 @@ export type VolumeTracingAction =
99102
| FineTuneQuickSelectAction
100103
| CancelQuickSelectAction
101104
| ConfirmQuickSelectAction
105+
| SetVolumeBucketDataHasChangedAction
102106
| BatchUpdateGroupsAndSegmentsAction;
103107

104108
export const VolumeTracingSaveRelevantActions = [
@@ -437,3 +441,9 @@ export const batchUpdateGroupsAndSegmentsAction = (actions: BatchableUpdateSegme
437441
export const cancelQuickSelectAction = () => ({ type: "CANCEL_QUICK_SELECT" }) as const;
438442

439443
export const confirmQuickSelectAction = () => ({ type: "CONFIRM_QUICK_SELECT" }) as const;
444+
445+
export const setVolumeBucketDataHasChangedAction = (tracingId: string) =>
446+
({
447+
type: "SET_VOLUME_BUCKET_DATA_HAS_CHANGED",
448+
tracingId,
449+
}) as const;

frontend/javascripts/oxalis/model/reducers/volumetracing_reducer.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ export function serverVolumeToClientVolumeTracing(tracing: ServerVolumeTracing):
269269
mappingName: tracing.mappingName,
270270
hasEditableMapping: tracing.hasEditableMapping,
271271
mappingIsLocked: tracing.mappingIsLocked,
272+
volumeBucketDataHasChanged: tracing.volumeBucketDataHasChanged,
272273
hasSegmentIndex: tracing.hasSegmentIndex || false,
273274
additionalAxes: convertServerAdditionalAxesToFrontEnd(tracing.additionalAxes),
274275
};
@@ -377,6 +378,12 @@ function VolumeTracingReducer(
377378
return expandSegmentParents(state, action);
378379
}
379380

381+
case "SET_VOLUME_BUCKET_DATA_HAS_CHANGED": {
382+
return updateVolumeTracing(state, action.tracingId, {
383+
volumeBucketDataHasChanged: true,
384+
});
385+
}
386+
380387
default: // pass
381388
}
382389

frontend/javascripts/oxalis/model/sagas/quick_select_saga.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import performQuickSelectML from "./quick_select_ml_saga";
1515
import getSceneController from "oxalis/controller/scene_controller_provider";
1616
import { getActiveSegmentationTracing } from "../accessors/volumetracing_accessor";
1717
import type { VolumeTracing } from "oxalis/store";
18-
import { ensureMaybeActiveMappingIsLocked } from "./saga_helpers";
18+
import { requestBucketModificationInVolumeTracing } from "./saga_helpers";
1919

2020
function* shouldUseHeuristic() {
2121
const useHeuristic = yield* select((state) => state.userConfiguration.quickSelect.useHeuristic);
@@ -32,11 +32,11 @@ export default function* listenToQuickSelect(): Saga<void> {
3232
);
3333
if (volumeTracing) {
3434
// As changes to the volume layer will be applied, the potentially existing mapping should be locked to ensure a consistent state.
35-
const { isMappingLockedIfNeeded } = yield* call(
36-
ensureMaybeActiveMappingIsLocked,
35+
const isModificationAllowed = yield* call(
36+
requestBucketModificationInVolumeTracing,
3737
volumeTracing,
3838
);
39-
if (!isMappingLockedIfNeeded) {
39+
if (!isModificationAllowed) {
4040
return;
4141
}
4242
}

frontend/javascripts/oxalis/model/sagas/saga_helpers.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import { call, put, takeEvery } from "typed-redux-saga";
99
import Toast from "libs/toast";
1010
import { Store } from "oxalis/singletons";
1111
import type { ActionPattern } from "@redux-saga/types";
12-
import { setMappingIsLockedAction } from "../actions/volumetracing_actions";
12+
import {
13+
setMappingIsLockedAction,
14+
setVolumeBucketDataHasChangedAction,
15+
} from "../actions/volumetracing_actions";
1316
import { MappingStatusEnum } from "oxalis/constants";
1417

1518
export function* takeEveryUnlessBusy<P extends ActionPattern>(
@@ -113,4 +116,33 @@ export function* ensureMaybeActiveMappingIsLocked(
113116
}
114117
}
115118

119+
export function* requestBucketModificationInVolumeTracing(
120+
volumeTracing: VolumeTracing,
121+
): Saga<boolean> {
122+
/*
123+
* Should be called when the modification of bucket data is about to happen. If
124+
* the saga returns false, the modification should be cancelled (this happens if
125+
* the user is not okay with locking the mapping).
126+
*
127+
* In detail: When the bucket data of a volume tracing is supposed to be mutated, we need to do
128+
* two things:
129+
* 1) ensure that the current mapping (or no mapping) is locked so that the mapping is not
130+
* changed later (this would lead to inconsistent data). If the mapping state is not yet
131+
* locked, the user is asked whether it is okay to lock it.
132+
* If the user confirms this, the mapping is locked and we can continue with (2). If the
133+
* user denies the locking request, the original bucket mutation will NOT be executed.
134+
* 2) volumeTracing.volumeBucketDataHasChanged will bet set to true if the user didn't
135+
* deny the request for locking the mapping.
136+
*/
137+
138+
const { isMappingLockedIfNeeded } = yield* call(ensureMaybeActiveMappingIsLocked, volumeTracing);
139+
if (!isMappingLockedIfNeeded) {
140+
return false;
141+
}
142+
143+
// Mark that bucket data has changed
144+
yield* put(setVolumeBucketDataHasChangedAction(volumeTracing.tracingId));
145+
return true;
146+
}
147+
116148
export default {};

frontend/javascripts/oxalis/model/sagas/volume/volume_interpolation_saga.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { Model, api } from "oxalis/singletons";
3838
import type { OxalisState } from "oxalis/store";
3939
import { call, put } from "typed-redux-saga";
4040
import { createVolumeLayer, getBoundingBoxForViewport, labelWithVoxelBuffer2D } from "./helpers";
41-
import { ensureMaybeActiveMappingIsLocked } from "../saga_helpers";
41+
import { requestBucketModificationInVolumeTracing } from "../saga_helpers";
4242

4343
/*
4444
* This saga is capable of doing segment interpolation between two slices.
@@ -436,8 +436,11 @@ export default function* maybeInterpolateSegmentationLayer(): Saga<void> {
436436
return;
437437
}
438438
// As the interpolation will be applied, the potentially existing mapping should be locked to ensure a consistent state.
439-
const { isMappingLockedIfNeeded } = yield* call(ensureMaybeActiveMappingIsLocked, volumeTracing);
440-
if (!isMappingLockedIfNeeded) {
439+
const isModificationAllowed = yield* call(
440+
requestBucketModificationInVolumeTracing,
441+
volumeTracing,
442+
);
443+
if (!isModificationAllowed) {
441444
return;
442445
}
443446

frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ import { select, take } from "oxalis/model/sagas/effect-generators";
8282
import listenToMinCut from "oxalis/model/sagas/min_cut_saga";
8383
import listenToQuickSelect from "oxalis/model/sagas/quick_select_saga";
8484
import {
85-
ensureMaybeActiveMappingIsLocked,
85+
requestBucketModificationInVolumeTracing,
8686
takeEveryUnlessBusy,
8787
} from "oxalis/model/sagas/saga_helpers";
8888
import {
@@ -223,11 +223,11 @@ export function* editVolumeLayerAsync(): Saga<any> {
223223

224224
const activeCellId = yield* select((state) => enforceActiveVolumeTracing(state).activeCellId);
225225
// As changes to the volume layer will be applied, the potentially existing mapping should be locked to ensure a consistent state.
226-
const { isMappingLockedIfNeeded } = yield* call(
227-
ensureMaybeActiveMappingIsLocked,
226+
const isModificationAllowed = yield* call(
227+
requestBucketModificationInVolumeTracing,
228228
volumeTracing,
229229
);
230-
if (!isMappingLockedIfNeeded) {
230+
if (!isModificationAllowed) {
231231
continue;
232232
}
233233

@@ -453,11 +453,11 @@ export function* floodFill(): Saga<void> {
453453
}
454454
// As the flood fill will be applied to the volume layer,
455455
// the potentially existing mapping should be locked to ensure a consistent state.
456-
const { isMappingLockedIfNeeded } = yield* call(
457-
ensureMaybeActiveMappingIsLocked,
456+
const isModificationAllowed = yield* call(
457+
requestBucketModificationInVolumeTracing,
458458
volumeTracing,
459459
);
460-
if (!isMappingLockedIfNeeded) {
460+
if (!isModificationAllowed) {
461461
continue;
462462
}
463463
yield* put(setBusyBlockingInfoAction(true, "Floodfill is being computed."));
@@ -603,6 +603,7 @@ export function* finishLayer(
603603

604604
yield* put(registerLabelPointAction(layer.getUnzoomedCentroid()));
605605
}
606+
606607
export function* ensureToolIsAllowedInMag(): Saga<any> {
607608
yield* take("INITIALIZE_VOLUMETRACING");
608609

frontend/javascripts/oxalis/store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ export type VolumeTracing = TracingBase & {
264264
readonly hasEditableMapping?: boolean;
265265
readonly mappingIsLocked?: boolean;
266266
readonly hasSegmentIndex: boolean;
267+
readonly volumeBucketDataHasChanged?: boolean;
267268
};
268269
export type ReadOnlyTracing = TracingBase & {
269270
readonly type: "readonly";

frontend/javascripts/oxalis/view/context_menu.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ import {
131131
import { hideContextMenuAction, setActiveUserBoundingBoxId } from "oxalis/model/actions/ui_actions";
132132
import { getDisabledInfoForTools } from "oxalis/model/accessors/tool_accessor";
133133
import FastTooltip from "components/fast_tooltip";
134+
import { LoadMeshMenuItemLabel } from "./right-border-tabs/segments_tab/load_mesh_menu_item_label";
134135

135136
type ContextMenuContextValue = React.MutableRefObject<HTMLElement | null> | null;
136137
export const ContextMenuContext = createContext<ContextMenuContextValue>(null);
@@ -1179,7 +1180,9 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[]
11791180
segmentationLayerName,
11801181
mappingInfo,
11811182
),
1182-
label: "Load Mesh (precomputed)",
1183+
label: (
1184+
<LoadMeshMenuItemLabel currentMeshFile={currentMeshFile} volumeTracing={volumeTracing} />
1185+
),
11831186
};
11841187
const computeMeshAdHocItem = {
11851188
key: "compute-mesh-adhc",
@@ -1434,12 +1437,13 @@ function ContextMenuInner(propsWithInputRef: Props) {
14341437
maybeClickedMeshId != null ? maybeClickedMeshId : segmentIdAtPosition;
14351438
const wasSegmentOrMeshClicked = clickedSegmentOrMeshId > 0;
14361439

1437-
const { dataset, tracing, flycam } = useSelector((state: OxalisState) => state);
1440+
const dataset = useSelector((state: OxalisState) => state.dataset);
14381441
useEffect(() => {
14391442
Store.dispatch(ensureSegmentIndexIsLoadedAction(visibleSegmentationLayer?.name));
14401443
}, [visibleSegmentationLayer]);
1441-
const isSegmentIndexAvailable = useSelector((state: OxalisState) =>
1442-
getMaybeSegmentIndexAvailability(state.dataset, visibleSegmentationLayer?.name),
1444+
const isSegmentIndexAvailable = getMaybeSegmentIndexAvailability(
1445+
dataset,
1446+
visibleSegmentationLayer?.name,
14431447
);
14441448
const mappingName: string | null | undefined = useSelector((state: OxalisState) => {
14451449
if (volumeTracing?.mappingName != null) return volumeTracing?.mappingName;
@@ -1453,6 +1457,7 @@ function ContextMenuInner(propsWithInputRef: Props) {
14531457
const isLoadingVolumeAndBB = [isLoadingMessage, isLoadingMessage];
14541458
const [segmentVolumeLabel, boundingBoxInfoLabel] = useFetch(
14551459
async () => {
1460+
const { tracing, flycam } = Store.getState();
14561461
// The value that is returned if the context menu is closed is shown if it's still loading
14571462
if (contextMenuPosition == null || !wasSegmentOrMeshClicked) return isLoadingVolumeAndBB;
14581463
if (visibleSegmentationLayer == null || !isSegmentIndexAvailable) return [];
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { WarningOutlined } from "@ant-design/icons";
2+
import FastTooltip from "components/fast_tooltip";
3+
import type { VolumeTracing } from "oxalis/store";
4+
import type { APIMeshFile } from "types/api_flow_types";
5+
6+
type Props = {
7+
currentMeshFile: APIMeshFile | null | undefined;
8+
volumeTracing: VolumeTracing | null | undefined;
9+
};
10+
11+
export function LoadMeshMenuItemLabel({ currentMeshFile, volumeTracing }: Props) {
12+
const showWarning =
13+
volumeTracing?.volumeBucketDataHasChanged ??
14+
// For older annotations, volumeBucketDataHasChanged can be undefined.
15+
// In that case, we still want to show a warning if no proofreading was
16+
// done, but the mapping is still locked (i.e., the user brushed).
17+
(!volumeTracing?.hasEditableMapping && volumeTracing?.mappingIsLocked);
18+
19+
return (
20+
<span style={{ display: "flex", alignItems: "center", gap: "4px" }}>
21+
<FastTooltip
22+
title={
23+
currentMeshFile != null
24+
? `Load mesh for centered segment from file ${currentMeshFile.meshFileName}`
25+
: "There is no mesh file."
26+
}
27+
>
28+
<span>Load Mesh (precomputed)</span>
29+
</FastTooltip>
30+
{showWarning && (
31+
<FastTooltip title="Warning: The segmentation data has changed since the mesh file was created. The mesh may not match the current data.">
32+
<WarningOutlined style={{ color: "var(--ant-color-warning)" }} />
33+
</FastTooltip>
34+
)}
35+
</span>
36+
);
37+
}

0 commit comments

Comments
 (0)