Skip to content

Commit 777f294

Browse files
authored
Allow selective segment visibility regardless of proofreading tool (#8281)
* allow selective segment visibility regardless of proofreading tool * remove unused imports * allow double click in move tool to activate a segment id * update changelog * update docs * clean up dblclick code that is only supported for the left mouse button anyway
1 parent 4e11a14 commit 777f294

File tree

10 files changed

+138
-51
lines changed

10 files changed

+138
-51
lines changed

CHANGELOG.unreleased.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
1313
### Added
1414
- Added the total volume of a dataset to a tooltip in the dataset info tab. [#8229](https://github.com/scalableminds/webknossos/pull/8229)
1515
- Optimized performance of data loading with “fill value“ chunks. [#8271](https://github.com/scalableminds/webknossos/pull/8271)
16+
- Added the option for "Selective Segment Visibility" for segmentation layers. Select this option in the left sidebar to only show segments that are currently active or hovered. [#8281](https://github.com/scalableminds/webknossos/pull/8281)
1617

1718
### Changed
1819
- Renamed "resolution" to "magnification" in more places within the codebase, including local variables. [#8168](https://github.com/scalableminds/webknossos/pull/8168)

docs/ui/layers.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ In addition to the general layer properties mentioned above, `color` and `segmen
3131

3232
- `Color`: Every `color` layer can be re-colored to make it easily identifiable. By default, all layers have a white overlay, showing the true, raw black & white data. Clicking on the square color indicator brings up your system's color palette to choose from. Note, there is an icon button for inverting all color values in this layer.
3333
- `Pattern Opacity`: Adjust the visibility of the texture/pattern on each segment. To make segments easier to distinguish and more unique, a pattern is applied to each in addition to its base color. 0% hides the pattern. 100% makes the pattern very prominent. Great for increasing the visual contrast between segments.
34+
- `Selective Visibility`: When activated, only segments are shown that are currently active or hovered.
3435
- `ID Mapping`: WEBKNOSSOS supports applying pre-computed agglomerations/groupings of segmentation IDs for a given segmentation layer. This is a very powerful feature to explore and compare different segmentation strategies for a given segmentation layer. Mappings need to be pre-computed and stored together with a dataset for WEBKNOSSOS to download and apply. [Read more about this here](../proofreading/segmentation_mappings.md).
3536

3637

frontend/javascripts/libs/input.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ export class InputKeyboard {
340340
}
341341

342342
// The mouse module.
343-
// Events: over, out, leftClick, rightClick, leftDownMove
343+
// Events: over, out, {left,right}Click, {left,right}DownMove, leftDoubleClick
344344
class InputMouseButton {
345345
mouse: InputMouse;
346346
name: MouseButtonString;
@@ -393,6 +393,19 @@ class InputMouseButton {
393393
}
394394
}
395395

396+
handleDoubleClick(event: MouseEvent, triggeredByTouch: boolean): void {
397+
// DoubleClick is only supported for the left mouse button
398+
if (this.name === "left" && this.moveDelta <= MOUSE_MOVE_DELTA_THRESHOLD) {
399+
this.mouse.emitter.emit(
400+
"leftDoubleClick",
401+
this.mouse.lastPosition,
402+
this.id,
403+
event,
404+
triggeredByTouch,
405+
);
406+
}
407+
}
408+
396409
handleMouseMove(event: MouseEvent, delta: Point2): void {
397410
if (this.down) {
398411
this.moveDelta += Math.abs(delta.x) + Math.abs(delta.y);
@@ -446,6 +459,7 @@ export class InputMouse {
446459
document.addEventListener("mousemove", this.mouseMove);
447460
document.addEventListener("mouseup", this.mouseUp);
448461
document.addEventListener("touchend", this.touchEnd);
462+
document.addEventListener("dblclick", this.doubleClick);
449463

450464
this.delegatedEvents = {
451465
...Utils.addEventListenerWithDelegation(
@@ -498,6 +512,7 @@ export class InputMouse {
498512
document.removeEventListener("mousemove", this.mouseMove);
499513
document.removeEventListener("mouseup", this.mouseUp);
500514
document.removeEventListener("touchend", this.touchEnd);
515+
document.removeEventListener("dblclick", this.doubleClick);
501516

502517
for (const [eventName, eventHandler] of Object.entries(this.delegatedEvents)) {
503518
document.removeEventListener(eventName, eventHandler);
@@ -551,6 +566,12 @@ export class InputMouse {
551566
}
552567
};
553568

569+
doubleClick = (event: MouseEvent): void => {
570+
if (this.isHit(event)) {
571+
this.leftMouseButton.handleDoubleClick(event, false);
572+
}
573+
};
574+
554575
touchEnd = (): void => {
555576
// The order of events during a click on a touch enabled device is:
556577
// touch events -> mouse events -> click

frontend/javascripts/oxalis/controller/combinations/tool_controls.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,20 @@ export class MoveTool {
137137
}
138138
handleClickSegment(pos);
139139
},
140+
leftDoubleClick: (pos: Point2, _plane: OrthoView, _event: MouseEvent, _isTouch: boolean) => {
141+
const { uiInformation } = Store.getState();
142+
const isMoveToolActive = uiInformation.activeTool === AnnotationToolEnum.MOVE;
143+
144+
if (isMoveToolActive) {
145+
// We want to select the clicked segment ID only in the MOVE tool. This method is
146+
// implemented within the Move tool, but other tool controls will fall back to this one
147+
// if they didn't define the double click hook. However, for most other tools, this behavior
148+
// would be suboptimal, because when doing a double click, the first click will also be registered
149+
// as a simple left click. For example, doing a double click with the brush tool would brush something
150+
// and then immediately select the id again which is weird.
151+
VolumeHandlers.handlePickCell(pos);
152+
}
153+
},
140154
middleClick: (pos: Point2, _plane: OrthoView, event: MouseEvent) => {
141155
if (event.shiftKey) {
142156
handleAgglomerateSkeletonAtClick(pos);

frontend/javascripts/oxalis/geometries/materials/plane_material_factory.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ class PlaneMaterialFactory {
146146
selectiveVisibilityInProofreading: {
147147
value: true,
148148
},
149+
selectiveSegmentVisibility: {
150+
value: false,
151+
},
149152
is3DViewBeingRendered: {
150153
value: true,
151154
},
@@ -562,6 +565,17 @@ class PlaneMaterialFactory {
562565
true,
563566
),
564567
);
568+
569+
this.storePropertyUnsubscribers.push(
570+
listenToStoreProperty(
571+
(storeState) => storeState.datasetConfiguration.selectiveSegmentVisibility,
572+
(selectiveSegmentVisibility) => {
573+
this.uniforms.selectiveSegmentVisibility.value = selectiveSegmentVisibility;
574+
},
575+
true,
576+
),
577+
);
578+
565579
this.storePropertyUnsubscribers.push(
566580
listenToStoreProperty(
567581
(storeState) => getMagInfoByLayer(storeState.dataset),

frontend/javascripts/oxalis/shaders/main_data_shaders.glsl.ts

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
convertCellIdToRGB,
77
getBrushOverlay,
88
getCrossHairOverlay,
9+
getSegmentationAlphaIncrement,
910
getSegmentId,
1011
} from "./segmentation.glsl";
1112
import { getMaybeFilteredColorOrFallback } from "./filtering.glsl";
@@ -110,6 +111,7 @@ uniform highp uint LOOKUP_CUCKOO_TWIDTH;
110111
111112
uniform float sphericalCapRadius;
112113
uniform bool selectiveVisibilityInProofreading;
114+
uniform bool selectiveSegmentVisibility;
113115
uniform float viewMode;
114116
uniform float alpha;
115117
uniform bool renderBucketIndices;
@@ -178,6 +180,7 @@ ${compileShader(
178180
hasSegmentation ? getBrushOverlay : null,
179181
hasSegmentation ? getSegmentId : null,
180182
hasSegmentation ? getCrossHairOverlay : null,
183+
hasSegmentation ? getSegmentationAlphaIncrement : null,
181184
almostEq,
182185
)}
183186
@@ -291,25 +294,13 @@ void main() {
291294
&& hoveredUnmappedSegmentIdHigh == <%= segmentationName %>_unmapped_id_high;
292295
bool isActiveCell = activeCellIdLow == <%= segmentationName %>_id_low
293296
&& activeCellIdHigh == <%= segmentationName %>_id_high;
294-
// Highlight cell only if it's hovered or active during proofreading
295-
// and if segmentation opacity is not zero
296-
float alphaIncrement = isProofreading
297-
? (isActiveCell
298-
? (isHoveredUnmappedSegment
299-
? 0.4 // Highlight the hovered super-voxel of the active segment
300-
: (isHoveredSegment
301-
? 0.15 // Highlight the not-hovered super-voxels of the hovered segment
302-
: 0.0
303-
)
304-
)
305-
: (isHoveredSegment
306-
? 0.2
307-
// We are in proofreading mode, but the current voxel neither belongs
308-
// to the active segment nor is it hovered. When selective visibility
309-
// is enabled, lower the opacity.
310-
: (selectiveVisibilityInProofreading ? -<%= segmentationName %>_alpha : 0.0)
311-
)
312-
) : (isHoveredSegment ? 0.2 : 0.0);
297+
float alphaIncrement = getSegmentationAlphaIncrement(
298+
<%= segmentationName %>_alpha,
299+
isHoveredSegment,
300+
isHoveredUnmappedSegment,
301+
isActiveCell
302+
);
303+
313304
gl_FragColor = vec4(mix(
314305
data_color.rgb,
315306
convertCellIdToRGB(<%= segmentationName %>_id_high, <%= segmentationName %>_id_low),

frontend/javascripts/oxalis/shaders/segmentation.glsl.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,44 @@ export const getSegmentId: ShaderModule = {
351351
<% }) %>
352352
`,
353353
};
354+
355+
export const getSegmentationAlphaIncrement: ShaderModule = {
356+
requirements: [],
357+
code: `
358+
float getSegmentationAlphaIncrement(float alpha, bool isHoveredSegment, bool isHoveredUnmappedSegment, bool isActiveCell) {
359+
// Highlight segment only if
360+
// - it's hovered or
361+
// - active during proofreading
362+
// Also, make segments invisible if selective visibility is turned on (unless the segment
363+
// is active or hovered).
364+
365+
if (isProofreading) {
366+
if (isActiveCell) {
367+
return (isHoveredUnmappedSegment
368+
? 0.4 // Highlight the hovered super-voxel of the active segment
369+
: (isHoveredSegment
370+
? 0.15 // Highlight the not-hovered super-voxels of the hovered segment
371+
: 0.0
372+
)
373+
);
374+
} else {
375+
return (isHoveredSegment
376+
? 0.2
377+
// We are in proofreading mode, but the current voxel neither belongs
378+
// to the active segment nor is it hovered. When selective visibility
379+
// is enabled, lower the opacity.
380+
: (selectiveVisibilityInProofreading ? -alpha : 0.0)
381+
);
382+
}
383+
}
384+
385+
if (isHoveredSegment) {
386+
return 0.2;
387+
} else if (selectiveSegmentVisibility) {
388+
return isActiveCell ? 0.15 : -alpha;
389+
} else {
390+
return 0.;
391+
}
392+
}
393+
`,
394+
};

frontend/javascripts/oxalis/store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ export type DatasetConfiguration = {
330330
readonly renderMissingDataBlack: boolean;
331331
readonly loadingStrategy: LoadingStrategy;
332332
readonly segmentationPatternOpacity: number;
333+
readonly selectiveSegmentVisibility: boolean;
333334
readonly blendMode: BLEND_MODES;
334335
// If nativelyRenderedLayerName is not-null, the layer with
335336
// that name (or id) should be rendered without any transforms.

frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ import {
8686
reloadHistogramAction,
8787
} from "oxalis/model/actions/settings_actions";
8888
import { userSettings } from "types/schemas/user_settings.schema";
89-
import type { Vector3, ControlMode } from "oxalis/constants";
89+
import type { Vector3 } from "oxalis/constants";
9090
import Constants, { ControlModeEnum, MappingStatusEnum } from "oxalis/constants";
9191
import EditableTextLabel from "oxalis/view/components/editable_text_label";
9292
import LinkButton from "components/link_button";
@@ -97,9 +97,6 @@ import type {
9797
DatasetLayerConfiguration,
9898
OxalisState,
9999
UserConfiguration,
100-
HistogramDataForAllLayers,
101-
Tracing,
102-
Task,
103100
} from "oxalis/store";
104101
import Store from "oxalis/store";
105102
import Toast from "libs/toast";
@@ -132,33 +129,8 @@ import {
132129
} from "types/schemas/dataset_view_configuration.schema";
133130
import defaultState from "oxalis/default_state";
134131

135-
type DatasetSettingsProps = {
136-
userConfiguration: UserConfiguration;
137-
datasetConfiguration: DatasetConfiguration;
138-
dataset: APIDataset;
139-
onChange: (propertyName: keyof DatasetConfiguration, value: any) => void;
140-
onChangeLayer: (
141-
layerName: string,
142-
propertyName: keyof DatasetLayerConfiguration,
143-
value: any,
144-
) => void;
145-
onClipHistogram: (layerName: string, shouldAdjustClipRange: boolean) => Promise<void>;
146-
histogramData: HistogramDataForAllLayers;
147-
onChangeRadius: (value: number) => void;
148-
onChangeShowSkeletons: (arg0: boolean) => void;
149-
onSetPosition: (arg0: Vector3) => void;
150-
onZoomToMag: (layerName: string, arg0: Vector3) => number;
151-
onChangeUser: (key: keyof UserConfiguration, value: any) => void;
152-
reloadHistogram: (layerName: string) => void;
153-
tracing: Tracing;
154-
task: Task | null | undefined;
155-
onEditAnnotationLayer: (tracingId: string, layerProperties: EditableLayerProperties) => void;
156-
controlMode: ControlMode;
157-
isArbitraryMode: boolean;
158-
isAdminOrDatasetManager: boolean;
159-
isAdminOrManager: boolean;
160-
isSuperUser: boolean;
161-
};
132+
type DatasetSettingsProps = ReturnType<typeof mapStateToProps> &
133+
ReturnType<typeof mapDispatchToProps>;
162134

163135
type State = {
164136
// If this is set to not-null, the downsampling modal
@@ -927,9 +899,37 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps, State> {
927899
defaultValue={defaultDatasetViewConfigurationWithoutNull.segmentationPatternOpacity}
928900
/>
929901
);
902+
903+
const isProofreadingMode = this.props.activeTool === "PROOFREAD";
904+
const isSelectiveVisibilityDisabled = isProofreadingMode;
905+
906+
const selectiveVisibilitySwitch = (
907+
<FastTooltip
908+
title={
909+
isSelectiveVisibilityDisabled
910+
? "This behavior is overriden by the 'selective segment visibility' button in the toolbar, because the proofreading tool is active."
911+
: "When enabled, only hovered or active segments will be shown."
912+
}
913+
>
914+
<div
915+
style={{
916+
marginBottom: 6,
917+
}}
918+
>
919+
<SwitchSetting
920+
onChange={_.partial(this.props.onChange, "selectiveSegmentVisibility")}
921+
value={this.props.datasetConfiguration.selectiveSegmentVisibility}
922+
label="Selective Visibility"
923+
disabled={isSelectiveVisibilityDisabled}
924+
/>
925+
</div>
926+
</FastTooltip>
927+
);
928+
930929
return (
931930
<div>
932931
{segmentationOpacitySetting}
932+
{selectiveVisibilitySwitch}
933933
<MappingSettingsView layerName={layerName} />
934934
</div>
935935
);
@@ -1338,6 +1338,7 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps, State> {
13381338
"loadingStrategy",
13391339
"segmentationPatternOpacity",
13401340
"blendMode",
1341+
"selectiveSegmentVisibility",
13411342
] as Array<keyof RecommendedConfiguration>
13421343
).map((key) => ({
13431344
name: settings[key] as string,
@@ -1585,6 +1586,7 @@ const mapStateToProps = (state: OxalisState) => ({
15851586
state.activeUser != null ? Utils.isUserAdminOrDatasetManager(state.activeUser) : false,
15861587
isAdminOrManager: state.activeUser != null ? Utils.isUserAdminOrManager(state.activeUser) : false,
15871588
isSuperUser: state.activeUser?.isSuperUser || false,
1589+
activeTool: state.uiInformation.activeTool,
15881590
});
15891591

15901592
const mapDispatchToProps = (dispatch: Dispatch<any>) => ({

frontend/javascripts/types/schemas/dataset_view_configuration.schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export const defaultDatasetViewConfigurationWithoutNull: DatasetConfiguration =
8989
blendMode: BLEND_MODES.Additive,
9090
colorLayerOrder: [],
9191
nativelyRenderedLayerName: null,
92+
selectiveSegmentVisibility: false,
9293
};
9394
export const defaultDatasetViewConfiguration = {
9495
...defaultDatasetViewConfigurationWithoutNull,

0 commit comments

Comments
 (0)