Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "merge" blend mode #6936

Merged
merged 25 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
71e511e
WIP add new blend mode
MichaelBuessemeyer Mar 15, 2023
3490baf
WIP add cover blend mode
MichaelBuessemeyer Mar 21, 2023
9710973
Merge branch 'master' of github.com:scalableminds/webknossos into cov…
MichaelBuessemeyer Mar 21, 2023
3f871db
Merge branch 'master' of github.com:scalableminds/webknossos into cov…
MichaelBuessemeyer Apr 14, 2023
ce1fff2
fix uint24 rendering in merge mode
MichaelBuessemeyer Apr 14, 2023
c22b91c
remove unused imports
MichaelBuessemeyer Apr 14, 2023
4821ce5
rename to render mode to blend mode
MichaelBuessemeyer Apr 20, 2023
11020f5
improve in line comments
MichaelBuessemeyer Apr 20, 2023
1f5b217
Merge branch 'master' of github.com:scalableminds/webknossos into cov…
MichaelBuessemeyer Apr 20, 2023
0dbf947
decide whether a color is valid by alpha < 0.
MichaelBuessemeyer Apr 20, 2023
6b4da6a
Merge branch 'master' of github.com:scalableminds/webknossos into cov…
MichaelBuessemeyer Apr 27, 2023
820bd9d
multiple fixes in cover blend mode
MichaelBuessemeyer Apr 27, 2023
2ca4a81
fix reading blendmode from backend on inital load
MichaelBuessemeyer Apr 27, 2023
f9cdd4d
Merge branch 'master' into cover-blend-mode
MichaelBuessemeyer Apr 27, 2023
ebadcb8
refactoring & make changes ready for review
MichaelBuessemeyer Apr 28, 2023
134acf8
Merge branch 'cover-blend-mode' of github.com:scalableminds/webknosso…
MichaelBuessemeyer Apr 28, 2023
30b268f
add changelog entry
MichaelBuessemeyer Apr 28, 2023
f9fe4b9
Merge branch 'master' of github.com:scalableminds/webknossos into cov…
MichaelBuessemeyer Apr 28, 2023
777c923
add blend mode setting to docs
MichaelBuessemeyer Apr 28, 2023
fd9a454
Merge branch 'master' of github.com:scalableminds/webknossos into cov…
MichaelBuessemeyer May 4, 2023
a23718e
Apply suggestions from code review
MichaelBuessemeyer May 4, 2023
9ff0aa4
Merge branch 'cover-blend-mode' of github.com:scalableminds/webknosso…
MichaelBuessemeyer May 4, 2023
56e971a
refactor shader code for blending
MichaelBuessemeyer May 4, 2023
96cb1b2
Merge branch 'master' of github.com:scalableminds/webknossos into cov…
MichaelBuessemeyer May 4, 2023
c4df6c2
Merge branch 'master' into cover-blend-mode
MichaelBuessemeyer May 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released

### Added
- Added segment groups so that segments can be organized in a hierarchy (similar to skeletons). [#6966](https://github.com/scalableminds/webknossos/pull/6966)
- Added a new "cover" blend mode which renders the visible layers on top of each other. The new blend mode can be selected in the Data Rendering settings in the settings tab of the left side bar. [#6936](https://github.com/scalableminds/webknossos/pull/6936)
- In addition to drag and drop, the selected tree(s) in the Skeleton tab can also be moved into another group by right-clicking the target group and selecting "Move selected tree(s) here". [#7005](https://github.com/scalableminds/webknossos/pull/7005)

### Changed
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions docs/tracing_ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ Note, not all control/viewport settings are available in every annotation mode.
#### Data Rendering
- `Hardware Utilization`: Adjusts the quality level used for rendering data. Changing this setting influences how many data is downloaded from the server as well as how much pressure is put on the user's graphics card. Tune this value to your network connection and hardware power. After changing the setting, the page has to be refreshed.
- `Loading Strategy`: You can choose between two different loading strategies. When using "best quality first" it will take a bit longer until you see data, because the highest quality is loaded. Alternatively, "Progressive quality" can be chosen which will improve the quality progressively while loading. As a result, initial data will be visible faster, but it will take more time until the best quality is shown.
- `Blend Mode`: You can switch between two modes of blending the color layer. The default (Additive) simply sums up all color values of all visible color layers. The cover mode renders all color layers on top of each other. Thus the top most color layer covers the color layers below. This blend mode is especially useful for datasets using multi modality layers. Here is an example for such a dataset published by Bosch et al. [1]:

|Additive Blend Mode | &nbsp;&nbsp;&nbsp;&nbsp;Cover Blend Mode &nbsp; &nbsp;|
philippotto marked this conversation as resolved.
Show resolved Hide resolved
|:-------------------------:|:-------------------------:|
|![](./images/blend-mode-example-additive-bosch-et-al.png)|![](./images/blend-mode-example-cover-bosch-et-al.png)|

- `4 Bit`: Toggles data download from the server using only 4 bit instead of 8 bit for each voxel. Use this to reduce the amount of necessary internet bandwidth for WEBKNOSSOS. Useful for showcasing data on the go over cellular networks, e.g 4G.
- `Interpolation`: When interpolation is enabled, bilinear filtering is applied while rendering pixels between two voxels. As a result, data may look "smoother" (or blurry when being zoomed in very far). Without interpolation, data may look more "crisp" (or pixelated when being zoomed in very far).
- `Render Missing Data Black`: If a dataset does not contain data at a specific position, WEBKNOSSOS can either render these voxels in "black" or it can try to render data from another magnification.
Expand All @@ -138,3 +144,7 @@ The status bar at the bottom of the screen serves three functions:
1. It shows context-sensitive mouse and keyboard control hints. Depending on your selected annotation tool amd any pressed modifier keys (Shift, CMD, CTRL, ALT, etc) it provides useful interaction hints and shortcuts.
2. It provides useful information based on your mouse positioning and which objects it hovers over. This includes the current mouse position in the dataset coordinate space, any segment ID that you hover over, and the currently rendered magnification level (MipMap image pyramid) used for displaying any data.
3. When working with skeletons, the active node and tree IDs are listed. Use the little pencil icon to select/mark a specific ID as active if required. For more on the active node ID, [see the section on skeleton annotations](./skeleton_annotation.md#nodes_and_trees).


##### References
[1] Bosch, C., Ackels, T., Pacureanu, A. et al. Functional and multiscale 3D structural investigation of brain tissue through correlative in vivo physiology, synchrotron microtomography and volume electron microscopy. Nat Commun 13, 2923 (2022). https://doi.org/10.1038/s41467-022-30199-6
3 changes: 3 additions & 0 deletions frontend/javascripts/messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const settings: Partial<Record<keyof RecommendedConfiguration, string>> =
gpuMemoryFactor: "Hardware Utilization",
overwriteMode: "Volume Annotation Overwrite Mode",
useLegacyBindings: "Classic Controls",
blendMode: "Blend Mode",
renderWatermark: "Logo in Screenshots",
};
export const settingsTooltips: Partial<Record<keyof RecommendedConfiguration, string>> = {
Expand Down Expand Up @@ -73,6 +74,8 @@ export const settingsTooltips: Partial<Record<keyof RecommendedConfiguration, st
mouseRotateValue: "Rotation speed when using the mouse to drag the rotation.",
zoom: "Zoom in or out in the data viewports",
displayScalebars: "Show a scale in the lower-right corner of each viewport",
blendMode:
"Set the blend mode for the dataset. The additive mode (default) adds the data values of all color layers. In cover mode, color layers are rendered on top of each other so that the data values of lower color layers are hidden by values of higher layers.",
renderWatermark: "Show a WEBKNOSSOS logo in the lower-left corner of each screenshot.",
};
export const layerViewConfigurations: Partial<Record<keyof DatasetLayerConfiguration, string>> = {
Expand Down
5 changes: 5 additions & 0 deletions frontend/javascripts/oxalis/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,8 @@ export enum LOG_LEVELS {
ERROR = "ERROR",
CRITICAL = "CRITICAL",
}

export enum BLEND_MODES {
Additive = "Additive",
Cover = "Cover",
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as THREE from "three";
import _ from "lodash";
import type { OrthoView, Vector3 } from "oxalis/constants";
import { BLEND_MODES, OrthoView, Vector3 } from "oxalis/constants";
import {
ViewModeValues,
OrthoViewValues,
Expand Down Expand Up @@ -216,6 +216,7 @@ class PlaneMaterialFactory {
activeCellIdLow: {
value: new THREE.Vector4(0, 0, 0, 0),
},
blendMode: { value: 1.0 },
};

const activeMagIndices = getActiveMagIndicesForLayers(Store.getState());
Expand Down Expand Up @@ -567,6 +568,15 @@ class PlaneMaterialFactory {
true,
),
);
this.storePropertyUnsubscribers.push(
listenToStoreProperty(
(storeState) => storeState.datasetConfiguration.blendMode,
(blendMode) => {
this.uniforms.blendMode.value = blendMode === BLEND_MODES.Additive ? 1.0 : 0.0;
},
true,
),
);
this.storePropertyUnsubscribers.push(
listenToStoreProperty(
(storeState) => storeState.dataset.dataSource.dataLayers,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function maybePadRgbData(src: Uint8Array | Float32Array, elementClass: ElementCl
tmpPaddingBuffer[idx++] = src[srcIdx++];
tmpPaddingBuffer[idx++] = src[srcIdx++];
tmpPaddingBuffer[idx++] = src[srcIdx++];
idx++;
tmpPaddingBuffer[idx++] = 255;
}

return tmpPaddingBuffer;
Expand Down
36 changes: 36 additions & 0 deletions frontend/javascripts/oxalis/shaders/blending.glsl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { ShaderModule } from "./shader_module_system";
export const getBlendLayersAdditive: ShaderModule = {
code: `

vec4 blendLayersAdditive(
vec4 current_color,
vec4 color_to_add
) {
return current_color + color_to_add;
}
`,
};

export const getBlendLayersCover: ShaderModule = {
code: `
// Applying alpha blending to merge the layers where the top most layer has priority.
// See https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending for details.
vec4 blendLayersCover(
vec4 current_color,
vec4 layer_color,
bool used_fallback_color
) {
float mixed_alpha_factor = (1.0 - current_color.a) * layer_color.a;
float mixed_alpha = mixed_alpha_factor + current_color.a;
vec3 cover_color_rgb = current_color.a * current_color.rgb + mixed_alpha_factor * layer_color.rgb;
// Catching edge case where mixed_alpha is 0.0 and therefore the cover_color would have nan values.
float is_mixed_alpha_zero = float(mixed_alpha == 0.0);
vec4 cover_color = vec4(cover_color_rgb / (mixed_alpha + is_mixed_alpha_zero), mixed_alpha);
cover_color = mix(cover_color, vec4(0.0), is_mixed_alpha_zero);
// Do not overwrite current_color if the fallback color has been used.
float is_current_color_valid = float(!used_fallback_color);
cover_color = mix(current_color, cover_color, is_current_color_valid);
return cover_color;
}
`,
};
19 changes: 13 additions & 6 deletions frontend/javascripts/oxalis/shaders/filtering.glsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,12 @@ const getMaybeFilteredColor: ShaderModule = {
export const getMaybeFilteredColorOrFallback: ShaderModule = {
requirements: [getMaybeFilteredColor],
code: `
vec4 getMaybeFilteredColorOrFallback(
struct MaybeFilteredColor {
vec4 color;
bool used_fallback_color;
};

MaybeFilteredColor getMaybeFilteredColorOrFallback(
float layerIndex,
float d_texture_width,
float packingDegree,
Expand All @@ -113,14 +118,16 @@ export const getMaybeFilteredColorOrFallback: ShaderModule = {
vec4 fallbackColor,
bool supportsPrecomputedBucketAddress
) {
vec4 color = getMaybeFilteredColor(layerIndex, d_texture_width, packingDegree, worldPositionUVW, suppressBilinearFiltering, supportsPrecomputedBucketAddress);
MaybeFilteredColor maybe_filtered_color;
maybe_filtered_color.used_fallback_color = false;
maybe_filtered_color.color = getMaybeFilteredColor(layerIndex, d_texture_width, packingDegree, worldPositionUVW, suppressBilinearFiltering, supportsPrecomputedBucketAddress);

if (color.a < 0.0) {
if (maybe_filtered_color.color.a < 0.0) {
// Render gray for not-yet-existing data
color = fallbackColor;
maybe_filtered_color.color = fallbackColor;
maybe_filtered_color.used_fallback_color = true;
}

return color;
return maybe_filtered_color;
}

vec4[2] getSegmentIdOrFallback(
Expand Down
30 changes: 22 additions & 8 deletions frontend/javascripts/oxalis/shaders/main_data_shaders.glsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import compileShader from "./shader_module_system";
import Constants from "oxalis/constants";
import { PLANE_SUBDIVISION } from "oxalis/geometries/plane";
import { MAX_ZOOM_STEP_DIFF } from "oxalis/model/bucket_data_handling/loading_strategy_logic";
import { getBlendLayersAdditive, getBlendLayersCover } from "./blending.glsl";

type Params = {
globalLayerCount: number;
Expand Down Expand Up @@ -106,6 +107,7 @@ uniform vec3 globalPosition;
uniform vec3 activeSegmentPosition;
uniform float zoomValue;
uniform bool useBilinearFiltering;
uniform float blendMode;
uniform vec3 globalMousePosition;
uniform bool isMouseInCanvas;
uniform float brushSizeInPixel;
Expand Down Expand Up @@ -150,6 +152,8 @@ ${compileShader(
getWorldCoordUVW,
isOutsideOfBoundingBox,
getMaybeFilteredColorOrFallback,
getBlendLayersAdditive,
getBlendLayersCover,
hasSegmentation ? convertCellIdToRGB : null,
hasSegmentation ? getBrushOverlay : null,
hasSegmentation ? getSegmentationId : null,
Expand All @@ -169,7 +173,7 @@ void main() {
gl_FragColor = vec4(bucketPosition, activeMagIdx) / 255.;
return;
}
vec3 data_color = vec3(0.0);
vec4 data_color = vec4(0.0);

<% _.each(segmentationLayerNames, function(segmentationName, layerIndex) { %>
vec4 <%= segmentationName%>_id_low = vec4(0.);
Expand All @@ -193,7 +197,7 @@ void main() {

vec3 transformedCoordUVW = transDim((<%= name %>_transform * vec4(transDim(worldCoordUVW), 1.0)).xyz);
if (!isOutsideOfBoundingBox(transformedCoordUVW)) {
color_value =
MaybeFilteredColor maybe_filtered_color =
getMaybeFilteredColorOrFallback(
<%= formatNumberAsGLSLFloat(layerIndex) %>,
<%= name %>_data_texture_width,
Expand All @@ -202,8 +206,9 @@ void main() {
false,
fallbackGray,
!<%= name %>_has_transform
).xyz;

);
bool used_fallback = maybe_filtered_color.used_fallback_color;
color_value = maybe_filtered_color.color.rgb;
<% if (textureLayerInfos[name].packingDegree === 2.0) { %>
// Workaround for 16-bit color layers
color_value = vec3(color_value.g * 256.0 + color_value.r);
Expand All @@ -222,14 +227,23 @@ void main() {
color_value = abs(color_value - <%= name %>_is_inverted);
// Catch the case where max == min would causes a NaN value and use black as a fallback color.
color_value = mix(color_value, vec3(0.0), is_max_and_min_equal);
// Multiply with color and alpha for <%= name %>
data_color += color_value * <%= name %>_alpha * <%= name %>_color;
color_value = color_value * <%= name %>_alpha * <%= name %>_color;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MichaelBuessemeyer can you give this entire code block a bit of love? variable naming could be more consistent and I'd also hope that the code for the different blend mode could be extracted into helper functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the different blend mode could be extracted into helper functions?

That is a very good suggestion: I added a new glsl module called blending and extracted the methods there. I also renamed all variable to snail case and all methods & struct names to camelCase. Sadly, the name is very inconsistent in the shaders and thus I couldn't find a clear pattern what naming scheme / style to use. I also discussed this shortly with daniel (the naming style)

// Marking the color as invalid by setting alpha to 0.0 if the fallback color has been used
// so the fallback color does not cover other colors.
vec4 layer_color = vec4(color_value, used_fallback ? 0.0 : maybe_filtered_color.color.a * <%= name %>_alpha);
// Calculating the cover color for the current layer in case blendMode == 1.0.
vec4 additive_color = blendLayersAdditive(data_color, layer_color);
// Calculating the cover color for the current layer in case blendMode == 0.0.
vec4 cover_color = blendLayersCover(data_color, layer_color, used_fallback);
// Choose color depending on blendMode.
data_color = mix(cover_color, additive_color, float(blendMode == 1.0));
}
}
<% }) %>
data_color = clamp(data_color, 0.0, 1.0);
data_color.a = 1.0;

gl_FragColor = vec4(data_color, 1.0);
gl_FragColor = data_color;

<% if (hasSegmentation) { %>
<% _.each(segmentationLayerNames, function(segmentationName, layerIndex) { %>
Expand All @@ -246,7 +260,7 @@ void main() {
float hoverAlphaIncrement = isHoveredCell && <%= segmentationName%>_alpha > 0.0 ? 0.2 : 0.0;
float proofreadingAlphaIncrement = isActiveCell && isProofreading && <%= segmentationName%>_alpha > 0.0 ? 0.4 : 0.0;
gl_FragColor = vec4(mix(
data_color,
data_color.rgb,
convertCellIdToRGB(<%= segmentationName%>_id_high, <%= segmentationName%>_id_low),
<%= segmentationName%>_alpha + max(hoverAlphaIncrement, proofreadingAlphaIncrement)
), 1.0);
Expand Down
3 changes: 2 additions & 1 deletion frontend/javascripts/oxalis/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import type {
OrthoViewWithoutTD,
InterpolationMode,
} from "oxalis/constants";
import { ControlModeEnum } from "oxalis/constants";
import { BLEND_MODES, ControlModeEnum } from "oxalis/constants";
import type { Matrix4x4 } from "libs/mjs";
import type { SkeletonTracingStats } from "oxalis/model/accessors/skeletontracing_accessor";
import type { UpdateAction } from "oxalis/model/sagas/update_actions";
Expand Down Expand Up @@ -293,6 +293,7 @@ export type DatasetConfiguration = {
readonly renderMissingDataBlack: boolean;
readonly loadingStrategy: LoadingStrategy;
readonly segmentationPatternOpacity: number;
readonly blendMode: BLEND_MODES;
};
export type PartialDatasetConfiguration = Partial<
DatasetConfiguration & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { setZoomStepAction } from "oxalis/model/actions/flycam_actions";
import messages, { settingsTooltips, settings as settingsLabels } from "messages";
import { userSettings } from "types/schemas/user_settings.schema";
import type { ViewMode } from "oxalis/constants";
import Constants from "oxalis/constants";
import Constants, { BLEND_MODES } from "oxalis/constants";
import { api } from "oxalis/singletons";
import Toast from "libs/toast";
import { ExclamationCircleOutlined } from "@ant-design/icons";
Expand Down Expand Up @@ -311,6 +311,21 @@ class ControlsAndRenderingSettingsTab extends PureComponent<ControlsAndRendering
},
]}
/>
<DropdownSetting
label={<Tooltip title={settingsTooltips.blendMode}>{settingsLabels.blendMode}</Tooltip>}
value={this.props.datasetConfiguration.blendMode}
onChange={this.onChangeDataset.blendMode}
options={[
{
value: BLEND_MODES.Additive,
label: "Additive",
},
{
value: BLEND_MODES.Cover,
label: "Cover",
},
]}
/>
<SwitchSetting
label={<Tooltip title={settingsTooltips.fourBit}>{settingsLabels.fourBit}</Tooltip>}
value={this.props.datasetConfiguration.fourBit}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BLEND_MODES } from "oxalis/constants";
import { type DatasetLayerConfiguration, type DatasetConfiguration } from "oxalis/store";

export function getDefaultLayerViewConfiguration(
Expand Down Expand Up @@ -70,6 +71,7 @@ export const defaultDatasetViewConfigurationWithoutNull: DatasetConfiguration =
loadingStrategy: "PROGRESSIVE_QUALITY",
segmentationPatternOpacity: 40,
layers: {},
blendMode: BLEND_MODES.Additive,
};
export const defaultDatasetViewConfiguration = {
...defaultDatasetViewConfigurationWithoutNull,
Expand Down Expand Up @@ -99,6 +101,9 @@ export const baseDatasetViewConfiguration = {
minimum: 0,
maximum: 100,
},
blendMode: {
enum: Object.values(BLEND_MODES),
},
};
export const datasetViewConfiguration = {
...baseDatasetViewConfiguration,
Expand Down