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

Merged
merged 46 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
46 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
868585c
adjust logic when toggling transformations is allowed according to di…
Dec 18, 2024
7c1f5db
- rename file with transformation accessors
Dec 19, 2024
c22372d
Merge branch 'master' of github.com:scalableminds/webknossos into all…
Dec 19, 2024
582bdf0
Merge branch 'master' of github.com:scalableminds/webknossos into all…
Jan 8, 2025
3f8fbcb
apply minifeedback fro coderabbit
Jan 8, 2025
0619206
fix cyclic dependency
Jan 8, 2025
b6269a7
organize imports
Jan 10, 2025
d2548e3
Merge remote-tracking branch 'origin' into allow-perma-dataset-rotation
Jan 10, 2025
58dd946
fix auto merge errors
Jan 10, 2025
3f1bdb9
fix hovered segment id highlighting when volume layer is rendered wit…
Jan 10, 2025
47f4037
orga imports
Jan 10, 2025
bae0048
avoid magic number when expecting length of transformation which is r…
Jan 13, 2025
0cb6992
remove additional use of magic number
Jan 13, 2025
7be8f18
Merge branch 'master' of github.com:scalableminds/webknossos into all…
Jan 15, 2025
3c06db5
fix applying segmentation layer transform to hovered cell highlighting
Jan 15, 2025
9809a37
Merge branch 'master' of github.com:scalableminds/webknossos into all…
Jan 20, 2025
f1b92a5
use position in volume layer space when retrieving segmentation id fr…
Jan 20, 2025
44fa5fa
do not allow to toggle transformations on layers that cannot have a t…
Jan 20, 2025
1d00a5d
Merge branch 'master' of github.com:scalableminds/webknossos into all…
Jan 20, 2025
472c1a8
Merge branch 'master' into allow-perma-dataset-rotation
MichaelBuessemeyer Jan 22, 2025
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
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
187 changes: 187 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,187 @@
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,
getTranslationToOrigin,
IDENTITY_TRANSFORM,
getTranslationBackToOriginalPosition,
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 = [
getTranslationToOrigin(datasetBoundingBox),
getRotationMatrixAroundAxis("x", rotationValues["x"]),
getRotationMatrixAroundAxis("y", rotationValues["y"]),
getRotationMatrixAroundAxis("z", rotationValues["z"]),
getTranslationBackToOriginalPosition(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 = [
getTranslationToOrigin(datasetBoundingBox),
IDENTITY_TRANSFORM,
IDENTITY_TRANSFORM,
IDENTITY_TRANSFORM,
getTranslationBackToOriginalPosition(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 && 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 Expand Up @@ -398,8 +405,10 @@ function SimpleLayerForm({
{
validator: syncValidator(
(value: string) =>
dataLayers.filter((someLayer: APIDataLayer) => someLayer.name === value)
.length <= 1,
(dataLayers &&
dataLayers.filter((someLayer: APIDataLayer) => someLayer.name === value)
.length <= 1) ||
dataLayers == null,
"Layer names must be unique.",
),
},
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,
getRotationFromTransformation,
} 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: getRotationFromTransformation(firstLayerTransformations[1], "x"),
y: getRotationFromTransformation(firstLayerTransformations[2], "y"),
z: getRotationFromTransformation(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 @@ -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
Loading