Skip to content

Readd fixed version of "Allow permanent dataset layer rotation in dataset settings"" #8355

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/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)
- When using the “Restore older Version” feature, there are no longer separate tabs for the different annotation layers. Only one linear annotation history is now used, and if you revert to an older version, all layers are reverted. If layers were added/deleted since then, that is also reverted. This also means that proofreading annotations can now be reverted to older versions as well. The description text of annotations is now versioned as well. [#7917](https://github.com/scalableminds/webknossos/pull/7917)
- Added the possibility to use the "merger mode" even when the user has annotated volume data in the current layer (as long as no other mapping is active). [#8335](https://github.com/scalableminds/webknossos/pull/8335)

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
194 changes: 194 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,194 @@
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) => {
// form -> dataSource -> dataLayers can be undefined in case of the add remote dataset form which is initially empty.
const dataLayers: APIDataLayer[] | undefined = form?.getFieldValue(["dataSource", "dataLayers"]);
const isRotationOnly = useMemo(
() => (dataLayers ? doAllLayersHaveTheSameRotation(dataLayers) : false),
[dataLayers],
);
Comment on lines +154 to +159
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should be the acutal fix

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 @@ -44,13 +44,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