Skip to content

Commit b6fdcf1

Browse files
Revert "Revert "Allow permanent dataset layer rotation in dataset settings (#…"
This reverts commit e20c218.
1 parent e20c218 commit b6fdcf1

36 files changed

+933
-322
lines changed

CHANGELOG.unreleased.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
1111
[Commits](https://github.com/scalableminds/webknossos/compare/25.01.0...HEAD)
1212

1313
### Added
14+
- 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)
1415

1516
### Changed
1617

frontend/javascripts/admin/dataset/composition_wizard/04_configure_new_dataset.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ import _ from "lodash";
2828
import messages from "messages";
2929
import { WkDevFlags } from "oxalis/api/wk_dev";
3030
import type { Vector3 } from "oxalis/constants";
31-
import { flatToNestedMatrix, getReadableURLPart } from "oxalis/model/accessors/dataset_accessor";
31+
import { getReadableURLPart } from "oxalis/model/accessors/dataset_accessor";
32+
import { flatToNestedMatrix } from "oxalis/model/accessors/dataset_layer_transformation_accessor";
3233
import { checkLandmarksForThinPlateSpline } from "oxalis/model/helpers/transformation_helpers";
3334
import type { OxalisState } from "oxalis/store";
3435
import React, { useState } from "react";
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { InfoCircleOutlined } from "@ant-design/icons";
2+
import { Col, Form, type FormInstance, InputNumber, Row, Slider, Tooltip, Typography } from "antd";
3+
import FormItem from "antd/es/form/FormItem";
4+
import {
5+
AXIS_TO_TRANSFORM_INDEX,
6+
EXPECTED_TRANSFORMATION_LENGTH,
7+
IDENTITY_TRANSFORM,
8+
doAllLayersHaveTheSameRotation,
9+
fromCenterToOrigin,
10+
fromOriginToCenter,
11+
getRotationMatrixAroundAxis,
12+
} from "oxalis/model/accessors/dataset_layer_transformation_accessor";
13+
import BoundingBox from "oxalis/model/bucket_data_handling/bounding_box";
14+
import { useCallback, useEffect, useMemo } from "react";
15+
import type { APIDataLayer } from "types/api_flow_types";
16+
import { FormItemWithInfo } from "./helper_components";
17+
18+
const { Text } = Typography;
19+
20+
type AxisRotationFormItemProps = {
21+
form: FormInstance | undefined;
22+
axis: "x" | "y" | "z";
23+
};
24+
25+
function getDatasetBoundingBoxFromLayers(layers: APIDataLayer[]): BoundingBox | undefined {
26+
if (!layers || layers.length === 0) {
27+
return undefined;
28+
}
29+
let datasetBoundingBox = BoundingBox.fromBoundBoxObject(layers[0].boundingBox);
30+
for (let i = 1; i < layers.length; i++) {
31+
datasetBoundingBox = datasetBoundingBox.extend(
32+
BoundingBox.fromBoundBoxObject(layers[i].boundingBox),
33+
);
34+
}
35+
return datasetBoundingBox;
36+
}
37+
38+
export const AxisRotationFormItem: React.FC<AxisRotationFormItemProps> = ({
39+
form,
40+
axis,
41+
}: AxisRotationFormItemProps) => {
42+
const dataLayers: APIDataLayer[] = Form.useWatch(["dataSource", "dataLayers"], form);
43+
const datasetBoundingBox = useMemo(
44+
() => getDatasetBoundingBoxFromLayers(dataLayers),
45+
[dataLayers],
46+
);
47+
// Update the transformations in case the user changes the dataset bounding box.
48+
useEffect(() => {
49+
if (
50+
datasetBoundingBox == null ||
51+
dataLayers[0].coordinateTransformations?.length !== EXPECTED_TRANSFORMATION_LENGTH ||
52+
!form
53+
) {
54+
return;
55+
}
56+
const rotationValues = form.getFieldValue(["datasetRotation"]);
57+
const transformations = [
58+
fromCenterToOrigin(datasetBoundingBox),
59+
getRotationMatrixAroundAxis("x", rotationValues["x"]),
60+
getRotationMatrixAroundAxis("y", rotationValues["y"]),
61+
getRotationMatrixAroundAxis("z", rotationValues["z"]),
62+
fromOriginToCenter(datasetBoundingBox),
63+
];
64+
const dataLayersWithUpdatedTransforms = dataLayers.map((layer) => {
65+
return {
66+
...layer,
67+
coordinateTransformations: transformations,
68+
};
69+
});
70+
form.setFieldValue(["dataSource", "dataLayers"], dataLayersWithUpdatedTransforms);
71+
}, [datasetBoundingBox, dataLayers, form]);
72+
73+
const setMatrixRotationsForAllLayer = useCallback(
74+
(rotationInDegrees: number): void => {
75+
if (!form) {
76+
return;
77+
}
78+
const dataLayers: APIDataLayer[] = form.getFieldValue(["dataSource", "dataLayers"]);
79+
const datasetBoundingBox = getDatasetBoundingBoxFromLayers(dataLayers);
80+
if (datasetBoundingBox == null) {
81+
return;
82+
}
83+
84+
const rotationInRadians = rotationInDegrees * (Math.PI / 180);
85+
const rotationMatrix = getRotationMatrixAroundAxis(axis, rotationInRadians);
86+
const dataLayersWithUpdatedTransforms: APIDataLayer[] = dataLayers.map((layer) => {
87+
let transformations = layer.coordinateTransformations;
88+
if (transformations == null || transformations.length !== EXPECTED_TRANSFORMATION_LENGTH) {
89+
transformations = [
90+
fromCenterToOrigin(datasetBoundingBox),
91+
IDENTITY_TRANSFORM,
92+
IDENTITY_TRANSFORM,
93+
IDENTITY_TRANSFORM,
94+
fromOriginToCenter(datasetBoundingBox),
95+
];
96+
}
97+
transformations[AXIS_TO_TRANSFORM_INDEX[axis]] = rotationMatrix;
98+
return {
99+
...layer,
100+
coordinateTransformations: transformations,
101+
};
102+
});
103+
form.setFieldValue(["dataSource", "dataLayers"], dataLayersWithUpdatedTransforms);
104+
},
105+
[axis, form],
106+
);
107+
return (
108+
<Row gutter={24}>
109+
<Col span={16}>
110+
<FormItemWithInfo
111+
name={["datasetRotation", axis]}
112+
label={`${axis.toUpperCase()} Axis Rotation`}
113+
info={`Change the datasets rotation around the ${axis}-axis.`}
114+
colon={false}
115+
>
116+
<Slider min={0} max={270} step={90} onChange={setMatrixRotationsForAllLayer} />
117+
</FormItemWithInfo>
118+
</Col>
119+
<Col span={8} style={{ marginRight: -12 }}>
120+
<FormItem
121+
name={["datasetRotation", axis]}
122+
colon={false}
123+
label=" " /* Whitespace label is needed for correct formatting*/
124+
>
125+
<InputNumber
126+
min={0}
127+
max={270}
128+
step={90}
129+
precision={0}
130+
onChange={(value: number | null) =>
131+
// InputNumber might be called with null, so we need to check for that.
132+
value != null && setMatrixRotationsForAllLayer(value)
133+
}
134+
/>
135+
</FormItem>
136+
</Col>
137+
</Row>
138+
);
139+
};
140+
141+
type AxisRotationSettingForDatasetProps = {
142+
form: FormInstance | undefined;
143+
};
144+
145+
export type DatasetRotation = {
146+
x: number;
147+
y: number;
148+
z: number;
149+
};
150+
151+
export const AxisRotationSettingForDataset: React.FC<AxisRotationSettingForDatasetProps> = ({
152+
form,
153+
}: AxisRotationSettingForDatasetProps) => {
154+
const dataLayers: APIDataLayer[] = form?.getFieldValue(["dataSource", "dataLayers"]);
155+
const isRotationOnly = useMemo(() => doAllLayersHaveTheSameRotation(dataLayers), [dataLayers]);
156+
157+
if (!isRotationOnly) {
158+
return (
159+
<Tooltip
160+
title={
161+
<div>
162+
Each layers transformations must be equal and each layer needs exactly 5 affine
163+
transformation with the following schema:
164+
<ul>
165+
<li>Translation to the origin</li>
166+
<li>Rotation around the x-axis</li>
167+
<li>Rotation around the y-axis</li>
168+
<li>Rotation around the z-axis</li>
169+
<li>Translation back to the original position</li>
170+
</ul>
171+
To easily enable this setting, delete all coordinateTransformations of all layers in the
172+
advanced tab, save and reload the dataset settings.
173+
</div>
174+
}
175+
>
176+
<Text type="secondary">
177+
Setting a dataset's rotation is only supported when all layers have the same rotation
178+
transformation. <InfoCircleOutlined />
179+
</Text>
180+
</Tooltip>
181+
);
182+
}
183+
184+
return (
185+
<div>
186+
<AxisRotationFormItem form={form} axis="x" />
187+
<AxisRotationFormItem form={form} axis="y" />
188+
<AxisRotationFormItem form={form} axis="z" />
189+
</div>
190+
);
191+
};

frontend/javascripts/dashboard/dataset/dataset_settings_data_tab.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { type APIDataLayer, type APIDataset, APIJobType } from "types/api_flow_t
3434
import type { ArbitraryObject } from "types/globals";
3535
import type { DataLayer } from "types/schemas/datasource.types";
3636
import { isValidJSON, syncValidator, validateDatasourceJSON } from "types/validation";
37+
import { AxisRotationSettingForDataset } from "./dataset_rotation_form_item";
3738

3839
const FormItem = Form.Item;
3940

@@ -267,6 +268,12 @@ function SimpleDatasetForm({
267268
</FormItemWithInfo>
268269
</Col>
269270
</Row>
271+
<Row gutter={48}>
272+
<Col span={24} xl={12} />
273+
<Col span={24} xl={6}>
274+
<AxisRotationSettingForDataset form={form} />
275+
</Col>
276+
</Row>
270277
</div>
271278
</List.Item>
272279
</List>

frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ import _ from "lodash";
2525
import messages from "messages";
2626
import { Unicode } from "oxalis/constants";
2727
import { getReadableURLPart } from "oxalis/model/accessors/dataset_accessor";
28+
import {
29+
EXPECTED_TRANSFORMATION_LENGTH,
30+
doAllLayersHaveTheSameRotation,
31+
getRotationFromTransformationIn90DegreeSteps,
32+
} from "oxalis/model/accessors/dataset_layer_transformation_accessor";
2833
import type { DatasetConfiguration, OxalisState } from "oxalis/store";
2934
import * as React from "react";
3035
import { connect } from "react-redux";
@@ -37,6 +42,7 @@ import type {
3742
MutableAPIDataset,
3843
} from "types/api_flow_types";
3944
import { enforceValidatedDatasetViewConfiguration } from "types/schemas/dataset_view_configuration_defaults";
45+
import type { DatasetRotation } from "./dataset_rotation_form_item";
4046
import DatasetSettingsDataTab, { syncDataSourceFields } from "./dataset_settings_data_tab";
4147
import DatasetSettingsDeleteTab from "./dataset_settings_delete_tab";
4248
import DatasetSettingsMetadataTab from "./dataset_settings_metadata_tab";
@@ -76,6 +82,7 @@ export type FormData = {
7682
dataset: APIDataset;
7783
defaultConfiguration: DatasetConfiguration;
7884
defaultConfigurationLayersJson: string;
85+
datasetRotation?: DatasetRotation;
7986
};
8087

8188
class DatasetSettingsView extends React.PureComponent<PropsWithFormAndRouter, State> {
@@ -194,6 +201,32 @@ class DatasetSettingsView extends React.PureComponent<PropsWithFormAndRouter, St
194201
form.setFieldsValue({
195202
dataSource,
196203
});
204+
// Retrieve the initial dataset rotation settings from the data source config.
205+
if (doAllLayersHaveTheSameRotation(dataSource.dataLayers)) {
206+
const firstLayerTransformations = dataSource.dataLayers[0].coordinateTransformations;
207+
let initialDatasetRotationSettings: DatasetRotation;
208+
if (
209+
!firstLayerTransformations ||
210+
firstLayerTransformations.length !== EXPECTED_TRANSFORMATION_LENGTH
211+
) {
212+
initialDatasetRotationSettings = {
213+
x: 0,
214+
y: 0,
215+
z: 0,
216+
};
217+
} else {
218+
initialDatasetRotationSettings = {
219+
// First transformation is a translation to the coordinate system origin.
220+
x: getRotationFromTransformationIn90DegreeSteps(firstLayerTransformations[1], "x"),
221+
y: getRotationFromTransformationIn90DegreeSteps(firstLayerTransformations[2], "y"),
222+
z: getRotationFromTransformationIn90DegreeSteps(firstLayerTransformations[3], "z"),
223+
// Fifth transformation is a translation back to the original position.
224+
};
225+
}
226+
form.setFieldsValue({
227+
datasetRotation: initialDatasetRotationSettings,
228+
});
229+
}
197230
const datasetDefaultConfiguration = await getDatasetDefaultConfiguration(
198231
this.props.datasetId,
199232
);

frontend/javascripts/dashboard/dataset/dataset_settings_viewconfig_tab.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,8 @@ export default function DatasetSettingsViewConfigTab(props: {
223223
<Col span={6}>
224224
<FormItemWithInfo
225225
name={["defaultConfiguration", "rotation"]}
226-
label="Rotation"
227-
info="The default rotation that will be used in oblique and arbitrary view mode."
226+
label="Rotation - Arbitrary View Modes"
227+
info="The default rotation that will be used in oblique and flight view mode."
228228
>
229229
<Vector3Input />
230230
</FormItemWithInfo>

frontend/javascripts/libs/mjs.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,10 @@ const M4x4 = {
220220
r[2] = m[14];
221221
return r;
222222
},
223+
224+
identity(): Matrix4x4 {
225+
return BareM4x4.identity;
226+
},
223227
};
224228

225229
const V2 = {

frontend/javascripts/oxalis/api/api_latest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,13 @@ import {
4545
import UrlManager from "oxalis/controller/url_manager";
4646
import type { OxalisModel } from "oxalis/model";
4747
import {
48-
flatToNestedMatrix,
4948
getLayerBoundingBox,
5049
getLayerByName,
5150
getMagInfo,
5251
getMappingInfo,
5352
getVisibleSegmentationLayer,
5453
} from "oxalis/model/accessors/dataset_accessor";
54+
import { flatToNestedMatrix } from "oxalis/model/accessors/dataset_layer_transformation_accessor";
5555
import {
5656
getActiveMagIndexForLayer,
5757
getPosition,

frontend/javascripts/oxalis/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export type Vector4 = [number, number, number, number];
1515
export type Vector5 = [number, number, number, number, number];
1616
export type Vector6 = [number, number, number, number, number, number];
1717

18+
export type NestedMatrix4 = [Vector4, Vector4, Vector4, Vector4]; // Represents a row major matrix.
19+
1820
// For 3D data BucketAddress = x, y, z, mag
1921
// For higher dimensional data, BucketAddress = x, y, z, mag, [{name: "t", value: t}, ...]
2022
export type BucketAddress =

0 commit comments

Comments
 (0)