Skip to content

Commit

Permalink
Allow permanent dataset layer rotation in dataset settings (#8159)
Browse files Browse the repository at this point in the history
* WIP: add rotations settings for each axis

* WIP: first version allowing to set a rotation for each layer

* reduce rotation max to 270 degrees as 0 == 360

* keep axis rotation as put in by the user

* WIP: allow multiple transformations per layer

* remove combining affine transformations only

* fix flycam dummy matrix

* clean up

* WIP: make rotation setting for complete dataset & add translation to origin and back

* add debugging code & notes for bug that no  data layer are transformed according to affine matrix coordTransform

* fix rotation seting by using storing transformations in row major order
- and some code clean up

* WIP: allow multiple layer to be rendered natively

* finish allowing to toggle all transformations off and on
-  also always translate by dataset bounding box and not by layer bounding box for consistent rotation results

* fix transformations for annotations

* fix linting

* undo change to save natively rendered layer names. Instead only save a single one.
- in case layers have the same transformation, the automatic inverse of the natively rendered layer (applied to all other layers) will cancel out the layers transformation
- and fixing the code according to the logic of only saving one native layer

* fix rendering transforms for volume layers without fallback & fix init datasetconfig when nativelyRenderedLayerName is not present in current view / dataset

* clean up code for pr review

* add changelog entry

* apply pr feedback

* adjust logic when toggling transformations is allowed according to discussion & refactoring

* - rename file with transformation accessors
- fix & refactor positional change when toggling natively rendered layer
- only toggling natively rendered layer name when layer is rendered natively and has not transforms configured

* apply minifeedback fro coderabbit

* fix cyclic dependency

* organize imports

* fix auto merge errors

* fix hovered segment id highlighting when volume layer is rendered with transforms

* orga imports

* avoid magic number when expecting length of transformation which is rotation only

* remove additional use of magic number

* fix applying segmentation layer transform to hovered cell highlighting

* use position in volume layer space when retrieving segmentation id from a position

* do not allow to toggle transformations on layers that cannot have a transform configured

---------

Co-authored-by: Michael Büßemeyer <[email protected]>
  • Loading branch information
MichaelBuessemeyer and Michael Büßemeyer authored Jan 22, 2025
1 parent 71d1936 commit 1fed6d3
Show file tree
Hide file tree
Showing 36 changed files with 933 additions and 322 deletions.
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/25.01.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)

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import _ from "lodash";
import messages from "messages";
import { WkDevFlags } from "oxalis/api/wk_dev";
import type { Vector3 } from "oxalis/constants";
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_transformation_accessor";
import { checkLandmarksForThinPlateSpline } from "oxalis/model/helpers/transformation_helpers";
import type { OxalisState } from "oxalis/store";
import React, { useState } from "react";
Expand Down
191 changes: 191 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,191 @@
import { InfoCircleOutlined } from "@ant-design/icons";
import { Col, Form, type FormInstance, InputNumber, Row, Slider, Tooltip, Typography } from "antd";
import FormItem from "antd/es/form/FormItem";
import {
AXIS_TO_TRANSFORM_INDEX,
EXPECTED_TRANSFORMATION_LENGTH,
IDENTITY_TRANSFORM,
doAllLayersHaveTheSameRotation,
fromCenterToOrigin,
fromOriginToCenter,
getRotationMatrixAroundAxis,
} from "oxalis/model/accessors/dataset_layer_transformation_accessor";
import BoundingBox from "oxalis/model/bucket_data_handling/bounding_box";
import { useCallback, useEffect, useMemo } from "react";
import type { APIDataLayer } from "types/api_flow_types";
import { FormItemWithInfo } from "./helper_components";

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 !== EXPECTED_TRANSFORMATION_LENGTH ||
!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 !== EXPECTED_TRANSFORMATION_LENGTH) {
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) =>
// InputNumber might be called with null, so we need to check for that.
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 @@ -34,6 +34,7 @@ import { type APIDataLayer, type APIDataset, APIJobType } from "types/api_flow_t
import type { ArbitraryObject } from "types/globals";
import type { DataLayer } from "types/schemas/datasource.types";
import { isValidJSON, syncValidator, validateDatasourceJSON } from "types/validation";
import { AxisRotationSettingForDataset } from "./dataset_rotation_form_item";

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
33 changes: 33 additions & 0 deletions frontend/javascripts/dashboard/dataset/dataset_settings_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ import _ from "lodash";
import messages from "messages";
import { Unicode } from "oxalis/constants";
import { getReadableURLPart } from "oxalis/model/accessors/dataset_accessor";
import {
EXPECTED_TRANSFORMATION_LENGTH,
doAllLayersHaveTheSameRotation,
getRotationFromTransformationIn90DegreeSteps,
} from "oxalis/model/accessors/dataset_layer_transformation_accessor";
import type { DatasetConfiguration, OxalisState } from "oxalis/store";
import * as React from "react";
import { connect } from "react-redux";
Expand All @@ -37,6 +42,7 @@ import type {
MutableAPIDataset,
} from "types/api_flow_types";
import { enforceValidatedDatasetViewConfiguration } from "types/schemas/dataset_view_configuration_defaults";
import type { DatasetRotation } from "./dataset_rotation_form_item";
import DatasetSettingsDataTab, { syncDataSourceFields } from "./dataset_settings_data_tab";
import DatasetSettingsDeleteTab from "./dataset_settings_delete_tab";
import DatasetSettingsMetadataTab from "./dataset_settings_metadata_tab";
Expand Down Expand Up @@ -76,6 +82,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 +201,32 @@ 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 !== EXPECTED_TRANSFORMATION_LENGTH
) {
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."
>
<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;
},
};

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 @@ -45,13 +45,13 @@ import {
import UrlManager from "oxalis/controller/url_manager";
import type { OxalisModel } from "oxalis/model";
import {
flatToNestedMatrix,
getLayerBoundingBox,
getLayerByName,
getMagInfo,
getMappingInfo,
getVisibleSegmentationLayer,
} from "oxalis/model/accessors/dataset_accessor";
import { flatToNestedMatrix } from "oxalis/model/accessors/dataset_layer_transformation_accessor";
import {
getActiveMagIndexForLayer,
getPosition,
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
Loading

0 comments on commit 1fed6d3

Please sign in to comment.