Skip to content

Commit a324d04

Browse files
authored
Make isosurfaces more robust (#5102)
* don't crash front-end if isosurface request fails; retry isosurface requests; indicate loading state of isosurfaces also on initial request * use exponential back off * improve seed position when importing isosurface via STL file * update changelog * ensure the same isosurface is not added twice to the store * Merge branch 'master' of github.com:scalableminds/webknossos into robust-isosurfaces * fix CI
1 parent 33a467b commit a324d04

File tree

5 files changed

+74
-40
lines changed

5 files changed

+74
-40
lines changed

CHANGELOG.unreleased.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
1111
[Commits](https://github.com/scalableminds/webknossos/compare/21.02.0...HEAD)
1212

1313
### Added
14+
- The "Meshes" tab was overhauled, so that it displays generated isosurfaces and imported meshes. Generated isosurfaces can be jumped to, reloaded, downloaded and removed. [#4917](https://github.com/scalableminds/webknossos/pull/4917)
1415
- Added an explicit `/signup` (or `/auth/signup`) route. [#5091](https://github.com/scalableminds/webknossos/pull/5091/files)
1516

1617
### Changed
18+
- Make the isosurface feature in the meshes tab more robust. If a request fails, a retry is initiated. [#5102](https://github.com/scalableminds/webknossos/pull/5102)
1719
- Support for the old invite links was removed. These contained the organization name in the URL. The new links contain a token (can be generated in the users view). For instances with a single organization the old invite links should still work. [#5091](https://github.com/scalableminds/webknossos/pull/5091/files)
1820
- Users are no longer allowed to deactivate their own accounts. [#5070](https://github.com/scalableminds/webknossos/pull/5070)
1921

2022
### Fixed
2123
-
2224

2325
### Removed
24-
-
26+
- The isosurface setting was removed. Instead, isosurfaces can be generated via the "Meshes" tab. Also note that the Shift+Click binding for generating an isosurface was removed (for now). Please refer to the "Meshes" tab, too. [#4917](https://github.com/scalableminds/webknossos/pull/4917)

frontend/javascripts/oxalis/model/reducers/annotation_reducer.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -116,22 +116,26 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState {
116116

117117
case "ADD_ISOSURFACE": {
118118
const { cellId, seedPosition } = action;
119-
return updateKey2(state, "isosurfaces", cellId.toString(), {
119+
// $FlowIgnore[incompatible-call] updateKey has problems with updating Objects as Dictionaries
120+
return updateKey2(state, "isosurfaces", cellId, {
120121
segmentId: cellId,
121122
seedPosition,
123+
isLoading: false,
122124
});
123125
}
124126

125127
case "START_REFRESHING_ISOSURFACE": {
126128
const { cellId } = action;
127-
return updateKey2(state, "isosurfaces", cellId.toString(), {
129+
// $FlowIgnore[incompatible-call] updateKey has problems with updating Objects as Dictionaries
130+
return updateKey2(state, "isosurfaces", cellId, {
128131
isLoading: true,
129132
});
130133
}
131134

132135
case "FINISHED_REFRESHING_ISOSURFACE": {
133136
const { cellId } = action;
134-
return updateKey2(state, "isosurfaces", cellId.toString(), {
137+
// $FlowIgnore[incompatible-call] updateKey has problems with updating Objects as Dictionaries
138+
return updateKey2(state, "isosurfaces", cellId, {
135139
isLoading: false,
136140
});
137141
}

frontend/javascripts/oxalis/model/sagas/isosurface_saga.js

+56-29
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// @flow
22
import { saveAs } from "file-saver";
33

4+
import { sleep } from "libs/utils";
5+
import ErrorHandling from "libs/error_handling";
46
import type { APIDataset } from "types/api_flow_types";
57
import { ResolutionInfo, getResolutionInfo } from "oxalis/model/accessors/dataset_accessor";
68
import {
@@ -46,6 +48,9 @@ import { saveNowAction } from "oxalis/model/actions/save_actions";
4648
import Toast from "libs/toast";
4749
import messages from "messages";
4850

51+
const MAX_RETRY_COUNT = 5;
52+
const RETRY_WAIT_TIME = 5000;
53+
4954
const isosurfacesMap: Map<number, ThreeDMap<boolean>> = new Map();
5055
const cubeSize = [256, 256, 256];
5156
const modifiedCells: Set<number> = new Set();
@@ -207,9 +212,13 @@ function* loadIsosurfaceWithNeighbors(
207212
seedPosition != null ? seedPosition : yield* select(state => getFlooredPosition(state.flycam));
208213
const clippedPosition = clipPositionToCubeBoundary(position, zoomStep, resolutionInfo);
209214
let positionsToRequest = [clippedPosition];
210-
if (seedPosition) {
211-
yield* put(addIsosurfaceAction(segmentId, seedPosition));
215+
216+
const hasIsosurface = yield* select(state => state.isosurfaces[segmentId] != null);
217+
if (!hasIsosurface) {
218+
yield* put(addIsosurfaceAction(segmentId, position));
212219
}
220+
yield* put(startRefreshingIsosurfaceAction(segmentId));
221+
213222
while (positionsToRequest.length > 0) {
214223
const currentPosition = positionsToRequest.shift();
215224
const neighbors = yield* call(
@@ -225,6 +234,8 @@ function* loadIsosurfaceWithNeighbors(
225234
isInitialRequest = false;
226235
positionsToRequest = positionsToRequest.concat(neighbors);
227236
}
237+
238+
yield* put(finishedRefreshingIsosurfaceAction(segmentId));
228239
}
229240

230241
function hasBatchCounterExceededLimit(segmentId: number): boolean {
@@ -267,34 +278,46 @@ function* maybeLoadIsosurface(
267278
const volumeTracing = yield* select(state => state.tracing.volume);
268279
// Fetch from datastore if no volumetracing exists or if the tracing has a fallback layer.
269280
const useDataStore = volumeTracing == null || volumeTracing.fallbackLayer != null;
270-
const { buffer: responseBuffer, neighbors } = yield* call(
271-
computeIsosurface,
272-
useDataStore ? dataStoreUrl : tracingStoreUrl,
273-
layer,
274-
{
275-
position: clippedPosition,
276-
zoomStep,
277-
segmentId,
278-
voxelDimensions,
279-
cubeSize,
280-
scale,
281-
},
282-
);
283281

284-
// Check again whether the limit was exceeded, since this variable could have been
285-
// set in the mean time by ctrl-clicking the segment to remove it
286-
if (hasBatchCounterExceededLimit(segmentId)) {
287-
return [];
288-
}
289-
const vertices = new Float32Array(responseBuffer);
290-
if (removeExistingIsosurface) {
291-
getSceneController().removeIsosurfaceById(segmentId);
282+
let retryCount = 0;
283+
while (retryCount < MAX_RETRY_COUNT) {
284+
try {
285+
const { buffer: responseBuffer, neighbors } = yield* call(
286+
computeIsosurface,
287+
useDataStore ? dataStoreUrl : tracingStoreUrl,
288+
layer,
289+
{
290+
position: clippedPosition,
291+
zoomStep,
292+
segmentId,
293+
voxelDimensions,
294+
cubeSize,
295+
scale,
296+
},
297+
);
298+
299+
// Check again whether the limit was exceeded, since this variable could have been
300+
// set in the mean time by ctrl-clicking the segment to remove it
301+
if (hasBatchCounterExceededLimit(segmentId)) {
302+
return [];
303+
}
304+
const vertices = new Float32Array(responseBuffer);
305+
if (removeExistingIsosurface) {
306+
getSceneController().removeIsosurfaceById(segmentId);
307+
}
308+
getSceneController().addIsosurfaceFromVertices(vertices, segmentId);
309+
310+
return neighbors.map(neighbor =>
311+
getNeighborPosition(clippedPosition, neighbor, zoomStep, resolutionInfo),
312+
);
313+
} catch (exception) {
314+
retryCount++;
315+
ErrorHandling.notify(exception);
316+
console.warn("Retrying isosurface generation...");
317+
yield* call(sleep, RETRY_WAIT_TIME * 2 ** retryCount);
318+
}
292319
}
293-
getSceneController().addIsosurfaceFromVertices(vertices, segmentId);
294-
295-
return neighbors.map(neighbor =>
296-
getNeighborPosition(clippedPosition, neighbor, zoomStep, resolutionInfo),
297-
);
320+
return [];
298321
}
299322

300323
function* downloadIsosurfaceCellById(cellId: number): Saga<void> {
@@ -334,7 +357,11 @@ function* importIsosurfaceFromStl(action: ImportIsosurfaceFromStlAction): Saga<v
334357
const geometry = yield* call(parseStlBuffer, buffer);
335358
getSceneController().addIsosurfaceFromGeometry(geometry, segmentId);
336359
yield* put(setImportingMeshStateAction(false));
337-
yield* put(addIsosurfaceAction(segmentId, [0, 0, 0])); // TODO: use good position as seed
360+
361+
// TODO: Ideally, persist the seed position in the STL file. As a workaround,
362+
// we simply use the current position as a seed position.
363+
const seedPosition = yield* select(state => getFlooredPosition(state.flycam));
364+
yield* put(addIsosurfaceAction(segmentId, seedPosition));
338365
}
339366

340367
function* removeIsosurface(

frontend/javascripts/oxalis/store.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -453,11 +453,11 @@ type UiInformation = {
453453
+isRefreshingIsosurfaces: boolean,
454454
};
455455

456-
type IsosurfaceInformation = {
456+
export type IsosurfaceInformation = {|
457457
+segmentId: number,
458458
+seedPosition: Vector3,
459459
+isLoading: boolean,
460-
};
460+
|};
461461

462462
export type OxalisState = {|
463463
+datasetConfiguration: DatasetConfiguration,
@@ -471,7 +471,7 @@ export type OxalisState = {|
471471
+viewModeData: ViewModeData,
472472
+activeUser: ?APIUser,
473473
+uiInformation: UiInformation,
474-
+isosurfaces: { [segmentId: string]: IsosurfaceInformation },
474+
+isosurfaces: { [segmentId: number]: IsosurfaceInformation },
475475
|};
476476

477477
const sagaMiddleware = createSagaMiddleware();

frontend/javascripts/oxalis/view/right-menu/meshes_view.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import _ from "lodash";
88

99
import type { ExtractReturn } from "libs/type_helpers";
1010
import type { MeshMetaData, RemoteMeshMetaData } from "types/api_flow_types";
11-
import type { OxalisState } from "oxalis/store";
11+
import type { OxalisState, IsosurfaceInformation } from "oxalis/store";
1212
import Store from "oxalis/store";
1313
import Model from "oxalis/model";
1414
import type { Vector3 } from "oxalis/constants";
@@ -137,7 +137,7 @@ type StateProps = {|
137137
|};
138138
type DispatchProps = ExtractReturn<typeof mapDispatchToProps>;
139139

140-
type Props = { ...OwnProps, ...DispatchProps, ...StateProps };
140+
type Props = {| ...OwnProps, ...DispatchProps, ...StateProps |};
141141

142142
const getCheckboxStyle = isLoaded =>
143143
isLoaded
@@ -263,10 +263,11 @@ class MeshesView extends React.Component<
263263
</div>
264264
);
265265

266-
const renderIsosurfaceListItem = (isosurface: Object) => {
266+
const renderIsosurfaceListItem = (isosurface: IsosurfaceInformation) => {
267267
const { segmentId, seedPosition, isLoading } = isosurface;
268268
const centeredCell = getIdForPos(getPosition(this.props.flycam));
269-
const actionVisibility = segmentId === this.state.hoveredListItem ? "visible" : "hidden";
269+
const actionVisibility =
270+
isLoading || segmentId === this.state.hoveredListItem ? "visible" : "hidden";
270271
return (
271272
<List.Item
272273
style={{

0 commit comments

Comments
 (0)