Skip to content

Shear transformation is incorrect when done on more than one axis #8450

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

Open
ABotond opened this issue May 12, 2025 · 0 comments · May be fixed by #8452
Open

Shear transformation is incorrect when done on more than one axis #8450

ABotond opened this issue May 12, 2025 · 0 comments · May be fixed by #8452

Comments

@ABotond
Copy link

ABotond commented May 12, 2025

Currently, the 2D shearing transformation matrix is defined as: $\begin{pmatrix} 1 & S_x \\ S_y & 1 \end{pmatrix}$.
However, as shearing on the x-axis is defined as $\begin{pmatrix} 1 & S_x \\ 0 & 1 \end{pmatrix}$, on the y-axis as $\begin{pmatrix} 1 & 0 \\ S_y & 1 \end{pmatrix}$, their composition is either $\begin{pmatrix} 1 + S_xS_y & S_x \\ S_y & 1 \end{pmatrix}$ or $\begin{pmatrix} 1 & S_x \\ S_y & 1 + S_xS_y \end{pmatrix}$.

Firstly, this means that if a shearing is done on both axis at the same time, the resulting operation is not a real shearing, as it is not area-preserving. Secondly, if two separate shearings are done after each other, the end result is not the same as if the transformation was done in one step.

Reproduction
A very minimal example:

import cv2
import monai.transforms
import numpy as np

if __name__ == "__main__":
    dd = {"image": np.expand_dims(cv2.imread("input.png", flags=cv2.IMREAD_GRAYSCALE), 0)}

    xy_transform= monai.transforms.Compose([
        monai.transforms.Affined(["image"], shear_params=(0.3, 0.0), padding_mode="zeros"),
        monai.transforms.Affined(["image"], shear_params=(0.0, 0.3), padding_mode="zeros"),
    ])

    yx_transform = monai.transforms.Compose([
        monai.transforms.Affined(["image"], shear_params=(0.0, 0.3), padding_mode="zeros"),
        monai.transforms.Affined(["image"], shear_params=(0.3, 0.0), padding_mode="zeros"),
    ])

    joined_transform = monai.transforms.Affined(["image"], shear_params=(0.3, 0.3), padding_mode="zeros")
    xy_image = xy_transform(dd)["image"]
    yx_image = yx_transform(dd)["image"]
    joined_image = joined_transform(dd)["image"]

    cv2.imwrite("variant-1.png", xy_image[0, :, :].astype(np.uint8))
    cv2.imwrite("variant-2.png", yx_image[0, :, :].astype(np.uint8))
    cv2.imwrite("variant-3.png", joined_image[0, :, :].astype(np.uint8))
Original x-shear - y-shear y-shear - x-shear MONAI

Expected behavior
I expect the output of the composed shearing to be the same when I first shear only one axis, and then in a succinct step on another.

Proposed solution
In monai/transforms/utils.py in the _create_shear function line 988 should be changed either to
out[0, 1], out[1, 0], out[1, 1] = coefs[0], coefs[1], 1 + coefs[0] * coefs[1]
or to
out[0, 0], out[0, 1], out[1, 0] = 1 + coefs[0] * coefs[1], coefs[0], coefs[1]
depening on the desired order of the shearing operations.

The 3D version is also wrong, however, I did not derive the correct matrix right now.

Environment
I tested it on MONAI 0.9.0 and on MONAI 1.4.0

Edit: changed the images to make the differences easier to see.

EloiNavet added a commit to EloiNavet/MONAI that referenced this issue May 15, 2025
@EloiNavet EloiNavet linked a pull request May 15, 2025 that will close this issue
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant