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

Allow permanent dataset layer rotation in dataset settings #8159

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
235a133
WIP: add rotations settings for each axis
Oct 29, 2024
1a7ced3
WIP: first version allowing to set a rotation for each layer
Oct 30, 2024
a51608b
reduce rotation max to 270 degrees as 0 == 360
Oct 30, 2024
4fe4645
keep axis rotation as put in by the user
Nov 11, 2024
492e590
Merge branch 'master' of github.com:scalableminds/webknossos into all…
Nov 12, 2024
61a554f
WIP: allow multiple transformations per layer
Nov 12, 2024
56c8de9
Merge branch 'master' of github.com:scalableminds/webknossos into all…
Nov 12, 2024
6942141
remove combining affine transformations only
Nov 13, 2024
2855272
fix flycam dummy matrix
Nov 13, 2024
3beae60
clean up
Nov 13, 2024
423faa8
WIP: make rotation setting for complete dataset & add translation to …
Nov 14, 2024
99f1688
add debugging code & notes for bug that no data layer are transforme…
Nov 18, 2024
9132f87
fix rotation seting by using storing transformations in row major order
Nov 19, 2024
b007368
Merge branch 'master' of github.com:scalableminds/webknossos into all…
Nov 21, 2024
3e650a6
WIP: allow multiple layer to be rendered natively
Nov 21, 2024
930295f
finish allowing to toggle all transformations off and on
Nov 21, 2024
073c3da
Merge branch 'master' of github.com:scalableminds/webknossos into all…
Dec 3, 2024
53fb553
fix transformations for annotations
Dec 3, 2024
82bdc8a
fix linting
Dec 3, 2024
86d0b42
undo change to save natively rendered layer names. Instead only save …
Dec 4, 2024
5f9a301
fix rendering transforms for volume layers without fallback & fix ini…
Dec 5, 2024
47e5173
Merge branch 'master' of github.com:scalableminds/webknossos into all…
Dec 6, 2024
1cd5578
clean up code for pr review
Dec 6, 2024
9485c13
add changelog entry
Dec 6, 2024
7f32183
Merge branch 'master' of github.com:scalableminds/webknossos into all…
Dec 17, 2024
9c08ab0
apply pr feedback
Dec 17, 2024
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 @@ -11,6 +11,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
[Commits](https://github.com/scalableminds/webknossos/compare/24.12.0...HEAD)

### Added
- Added the possibility to configure a rotation for a dataset which can be toggled off and on when viewing and annotating data. [#8159](https://github.com/scalableminds/webknossos/pull/8159)
- Added the total volume of a dataset to a tooltip in the dataset info tab. [#8229](https://github.com/scalableminds/webknossos/pull/8229)
- Optimized performance of data loading with “fill value“ chunks. [#8271](https://github.com/scalableminds/webknossos/pull/8271)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import Toast, { guardedWithErrorToast } from "libs/toast";
import * as Utils from "libs/utils";
import _ from "lodash";
import messages from "messages";
import { flatToNestedMatrix, getReadableURLPart } from "oxalis/model/accessors/dataset_accessor";
import { getReadableURLPart } from "oxalis/model/accessors/dataset_accessor";
import { flatToNestedMatrix } from "oxalis/model/accessors/dataset_layer_rotation_accessor";
import type { OxalisState } from "oxalis/store";
import React, { useState } from "react";
import { useSelector } from "react-redux";
Expand Down
189 changes: 189 additions & 0 deletions frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { InfoCircleOutlined } from "@ant-design/icons";
import { type FormInstance, Row, Col, Slider, InputNumber, Tooltip, Typography, Form } from "antd";
import FormItem from "antd/es/form/FormItem";
import { useCallback, useEffect, useMemo } from "react";
import type { APIDataLayer } from "types/api_flow_types";
import { FormItemWithInfo } from "./helper_components";
import {
getRotationMatrixAroundAxis,
fromCenterToOrigin,
IDENTITY_TRANSFORM,
fromOriginToCenter,
AXIS_TO_TRANSFORM_INDEX,
doAllLayersHaveTheSameRotation,
} from "oxalis/model/accessors/dataset_layer_rotation_accessor";
import BoundingBox from "oxalis/model/bucket_data_handling/bounding_box";

const { Text } = Typography;

type AxisRotationFormItemProps = {
form: FormInstance | undefined;
axis: "x" | "y" | "z";
};

function getDatasetBoundingBoxFromLayers(layers: APIDataLayer[]): BoundingBox | undefined {
if (!layers || layers.length === 0) {
return undefined;
}
let datasetBoundingBox = BoundingBox.fromBoundBoxObject(layers[0].boundingBox);
for (let i = 1; i < layers.length; i++) {
datasetBoundingBox = datasetBoundingBox.extend(
BoundingBox.fromBoundBoxObject(layers[i].boundingBox),
);
}
return datasetBoundingBox;
}

export const AxisRotationFormItem: React.FC<AxisRotationFormItemProps> = ({
form,
axis,
}: AxisRotationFormItemProps) => {
const dataLayers: APIDataLayer[] = Form.useWatch(["dataSource", "dataLayers"], form);
const datasetBoundingBox = useMemo(
() => getDatasetBoundingBoxFromLayers(dataLayers),
[dataLayers],
);
// Update the transformations in case the user changes the dataset bounding box.
useEffect(() => {
if (
datasetBoundingBox == null ||
dataLayers[0].coordinateTransformations?.length !== 5 ||
!form
) {
return;
}
const rotationValues = form.getFieldValue(["datasetRotation"]);
const transformations = [
fromCenterToOrigin(datasetBoundingBox),
getRotationMatrixAroundAxis("x", rotationValues["x"]),
getRotationMatrixAroundAxis("y", rotationValues["y"]),
getRotationMatrixAroundAxis("z", rotationValues["z"]),
fromOriginToCenter(datasetBoundingBox),
];
const dataLayersWithUpdatedTransforms = dataLayers.map((layer) => {
return {
...layer,
coordinateTransformations: transformations,
};
});
form.setFieldValue(["dataSource", "dataLayers"], dataLayersWithUpdatedTransforms);
}, [datasetBoundingBox, dataLayers, form]);

const setMatrixRotationsForAllLayer = useCallback(
(rotationInDegrees: number): void => {
if (!form) {
return;
}
const dataLayers: APIDataLayer[] = form.getFieldValue(["dataSource", "dataLayers"]);
const datasetBoundingBox = getDatasetBoundingBoxFromLayers(dataLayers);
if (datasetBoundingBox == null) {
return;
}

const rotationInRadians = rotationInDegrees * (Math.PI / 180);
const rotationMatrix = getRotationMatrixAroundAxis(axis, rotationInRadians);
const dataLayersWithUpdatedTransforms: APIDataLayer[] = dataLayers.map((layer) => {
let transformations = layer.coordinateTransformations;
if (transformations == null || transformations.length !== 5) {
transformations = [
fromCenterToOrigin(datasetBoundingBox),
IDENTITY_TRANSFORM,
IDENTITY_TRANSFORM,
IDENTITY_TRANSFORM,
fromOriginToCenter(datasetBoundingBox),
];
}
transformations[AXIS_TO_TRANSFORM_INDEX[axis]] = rotationMatrix;
return {
...layer,
coordinateTransformations: transformations,
};
});
form.setFieldValue(["dataSource", "dataLayers"], dataLayersWithUpdatedTransforms);
},
[axis, form],
);
return (
<Row gutter={24}>
<Col span={16}>
<FormItemWithInfo
name={["datasetRotation", axis]}
label={`${axis.toUpperCase()} Axis Rotation`}
info={`Change the datasets rotation around the ${axis}-axis.`}
colon={false}
>
<Slider min={0} max={270} step={90} onChange={setMatrixRotationsForAllLayer} />
</FormItemWithInfo>
</Col>
<Col span={8} style={{ marginRight: -12 }}>
<FormItem
name={["datasetRotation", axis]}
colon={false}
label=" " /* Whitespace label is needed for correct formatting*/
>
<InputNumber
min={0}
max={270}
step={90}
precision={0}
onChange={(value: number | null) =>
value != null && setMatrixRotationsForAllLayer(value)
}
/>
</FormItem>
</Col>
</Row>
);
};

type AxisRotationSettingForDatasetProps = {
form: FormInstance | undefined;
};

export type DatasetRotation = {
x: number;
y: number;
z: number;
};

export const AxisRotationSettingForDataset: React.FC<AxisRotationSettingForDatasetProps> = ({
form,
}: AxisRotationSettingForDatasetProps) => {
const dataLayers: APIDataLayer[] = form?.getFieldValue(["dataSource", "dataLayers"]);
const isRotationOnly = useMemo(() => doAllLayersHaveTheSameRotation(dataLayers), [dataLayers]);

if (!isRotationOnly) {
return (
<Tooltip
title={
<div>
Each layers transformations must be equal and each layer needs exactly 5 affine
transformation with the following schema:
<ul>
<li>Translation to the origin</li>
<li>Rotation around the x-axis</li>
<li>Rotation around the y-axis</li>
<li>Rotation around the z-axis</li>
<li>Translation back to the original position</li>
</ul>
To easily enable this setting, delete all coordinateTransformations of all layers in the
advanced tab, save and reload the dataset settings.
</div>
}
>
<Text type="secondary">
Setting a dataset's rotation is only supported when all layers have the same rotation
transformation. <InfoCircleOutlined />
</Text>
</Tooltip>
);
}

return (
<div>
<AxisRotationFormItem form={form} axis="x" />
<AxisRotationFormItem form={form} axis="y" />
<AxisRotationFormItem form={form} axis="z" />
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { type APIDataLayer, type APIDataset, APIJobType } from "types/api_flow_t
import { useStartAndPollJob } from "admin/job/job_hooks";
import { AllUnits, LongUnitToShortUnitMap, type Vector3 } from "oxalis/constants";
import Toast from "libs/toast";
import { AxisRotationSettingForDataset } from "./dataset_rotation_form_item";
import type { ArbitraryObject } from "types/globals";

const FormItem = Form.Item;
Expand Down Expand Up @@ -267,6 +268,12 @@ function SimpleDatasetForm({
</FormItemWithInfo>
</Col>
</Row>
<Row gutter={48}>
<Col span={24} xl={12} />
<Col span={24} xl={6}>
<AxisRotationSettingForDataset form={form} />
</Col>
</Row>
</div>
</List.Item>
</List>
Expand Down
29 changes: 29 additions & 0 deletions frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ import DatasetSettingsDeleteTab from "./dataset_settings_delete_tab";
import DatasetSettingsDataTab, { syncDataSourceFields } from "./dataset_settings_data_tab";
import { defaultContext } from "@tanstack/react-query";
import { getReadableURLPart } from "oxalis/model/accessors/dataset_accessor";
import type { DatasetRotation } from "./dataset_rotation_form_item";
import {
doAllLayersHaveTheSameRotation,
getRotationFromTransformationIn90DegreeSteps,
} from "oxalis/model/accessors/dataset_layer_rotation_accessor";

const FormItem = Form.Item;
const notImportedYetStatus = "Not imported yet.";
Expand Down Expand Up @@ -76,6 +81,7 @@ export type FormData = {
dataset: APIDataset;
defaultConfiguration: DatasetConfiguration;
defaultConfigurationLayersJson: string;
datasetRotation?: DatasetRotation;
};

class DatasetSettingsView extends React.PureComponent<PropsWithFormAndRouter, State> {
Expand Down Expand Up @@ -194,6 +200,29 @@ class DatasetSettingsView extends React.PureComponent<PropsWithFormAndRouter, St
form.setFieldsValue({
dataSource,
});
// Retrieve the initial dataset rotation settings from the data source config.
if (doAllLayersHaveTheSameRotation(dataSource.dataLayers)) {
const firstLayerTransformations = dataSource.dataLayers[0].coordinateTransformations;
let initialDatasetRotationSettings: DatasetRotation;
if (!firstLayerTransformations || firstLayerTransformations.length !== 5) {
initialDatasetRotationSettings = {
x: 0,
y: 0,
z: 0,
};
} else {
initialDatasetRotationSettings = {
// First transformation is a translation to the coordinate system origin.
x: getRotationFromTransformationIn90DegreeSteps(firstLayerTransformations[1], "x"),
y: getRotationFromTransformationIn90DegreeSteps(firstLayerTransformations[2], "y"),
z: getRotationFromTransformationIn90DegreeSteps(firstLayerTransformations[3], "z"),
// Fifth transformation is a translation back to the original position.
};
}
form.setFieldsValue({
datasetRotation: initialDatasetRotationSettings,
});
}
const datasetDefaultConfiguration = await getDatasetDefaultConfiguration(
this.props.datasetId,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,8 @@ export default function DatasetSettingsViewConfigTab(props: {
<Col span={6}>
<FormItemWithInfo
name={["defaultConfiguration", "rotation"]}
label="Rotation"
info="The default rotation that will be used in oblique and arbitrary view mode."
label="Rotation - Arbitrary View Modes"
info="The default rotation that will be used in oblique and flight view mode."
philippotto marked this conversation as resolved.
Show resolved Hide resolved
>
<Vector3Input />
</FormItemWithInfo>
Expand Down
4 changes: 4 additions & 0 deletions frontend/javascripts/libs/mjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,10 @@ const M4x4 = {
r[2] = m[14];
return r;
},

identity(): Matrix4x4 {
return BareM4x4.identity;
},
philippotto marked this conversation as resolved.
Show resolved Hide resolved
};

const V2 = {
Expand Down
2 changes: 1 addition & 1 deletion frontend/javascripts/oxalis/api/api_latest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ import {
getMagInfo,
getVisibleSegmentationLayer,
getMappingInfo,
flatToNestedMatrix,
} from "oxalis/model/accessors/dataset_accessor";
import { flatToNestedMatrix } from "oxalis/model/accessors/dataset_layer_rotation_accessor";
import {
getPosition,
getActiveMagIndexForLayer,
Expand Down
2 changes: 2 additions & 0 deletions frontend/javascripts/oxalis/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export type Vector4 = [number, number, number, number];
export type Vector5 = [number, number, number, number, number];
export type Vector6 = [number, number, number, number, number, number];

export type NestedMatrix4 = [Vector4, Vector4, Vector4, Vector4]; // Represents a row major matrix.

// For 3D data BucketAddress = x, y, z, mag
// For higher dimensional data, BucketAddress = x, y, z, mag, [{name: "t", value: t}, ...]
export type BucketAddress =
Expand Down
2 changes: 1 addition & 1 deletion frontend/javascripts/oxalis/controller/scene_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import {
getDatasetBoundingBox,
getLayerBoundingBox,
getLayerNameToIsDisabled,
getTransformsForLayerOrNull,
} from "oxalis/model/accessors/dataset_accessor";
import { getActiveMagIndicesForLayers, getPosition } from "oxalis/model/accessors/flycam_accessor";
import { getSkeletonTracing } from "oxalis/model/accessors/skeletontracing_accessor";
Expand All @@ -47,6 +46,7 @@ import { Model } from "oxalis/singletons";
import type { OxalisState, SkeletonTracing, UserBoundingBox } from "oxalis/store";
import Store from "oxalis/store";
import SegmentMeshController from "./segment_mesh_controller";
import { getTransformsForLayerOrNull } from "oxalis/model/accessors/dataset_layer_rotation_accessor";

const CUBE_COLOR = 0x999999;
const LAYER_CUBE_COLOR = 0xffff99;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { listenToStoreProperty } from "oxalis/model/helpers/listener_helpers";
import shaderEditor from "oxalis/model/helpers/shader_editor";
import { Store } from "oxalis/singletons";
import _ from "lodash";
import { getTransformsForSkeletonLayer } from "oxalis/model/accessors/dataset_accessor";
import { M4x4 } from "libs/mjs";
import {
generateCalculateTpsOffsetFunction,
generateTpsInitialization,
} from "oxalis/shaders/thin_plate_spline.glsl";
import type TPS3D from "libs/thin_plate_spline";
import { getTransformsForSkeletonLayer } from "oxalis/model/accessors/dataset_layer_rotation_accessor";

class EdgeShader {
material: THREE.RawShaderMaterial;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import { Store } from "oxalis/singletons";
import shaderEditor from "oxalis/model/helpers/shader_editor";
import _ from "lodash";
import { formatNumberAsGLSLFloat } from "oxalis/shaders/utils.glsl";
import { getTransformsForSkeletonLayer } from "oxalis/model/accessors/dataset_accessor";
import { M4x4 } from "libs/mjs";
import {
generateCalculateTpsOffsetFunction,
generateTpsInitialization,
} from "oxalis/shaders/thin_plate_spline.glsl";
import type TPS3D from "libs/thin_plate_spline";
import { getTransformsForSkeletonLayer } from "oxalis/model/accessors/dataset_layer_rotation_accessor";

export const NodeTypes = {
INVALID: 0.0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,8 @@ import {
getMappingInfoForSupportedLayer,
getVisibleSegmentationLayer,
getLayerByName,
invertAndTranspose,
getTransformsForLayer,
getMagInfoByLayer,
getMagInfo,
getTransformsPerLayer,
} from "oxalis/model/accessors/dataset_accessor";
import {
getActiveMagIndicesForLayers,
Expand All @@ -53,6 +50,11 @@ import { CuckooTableVec3 } from "libs/cuckoo/cuckoo_table_vec3";
import { getGlobalLayerIndexForLayerName } from "oxalis/model/bucket_data_handling/layer_rendering_manager";
import { V3 } from "libs/mjs";
import type TPS3D from "libs/thin_plate_spline";
import {
invertAndTranspose,
getTransformsForLayer,
getTransformsPerLayer,
} from "oxalis/model/accessors/dataset_layer_rotation_accessor";

type ShaderMaterialOptions = {
polygonOffset?: boolean;
Expand Down Expand Up @@ -242,8 +244,7 @@ class PlaneMaterialFactory {
this.uniforms.activeMagIndices = {
value: Object.values(activeMagIndices),
};
const nativelyRenderedLayerName =
Store.getState().datasetConfiguration.nativelyRenderedLayerName;
const { nativelyRenderedLayerName } = Store.getState().datasetConfiguration;
const dataset = Store.getState().dataset;
for (const dataLayer of Model.getAllLayers()) {
const layerName = sanitizeName(dataLayer.name);
Expand Down
Loading