-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow permanent dataset layer rotation in dataset settings (#8159)
* 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
1 parent
71d1936
commit 1fed6d3
Showing
36 changed files
with
933 additions
and
322 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
191 changes: 191 additions & 0 deletions
191
frontend/javascripts/dashboard/dataset/dataset_rotation_form_item.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.