Slicer doesn't show rotation slider value corresponding to a transform after rotation axis is not cannonical

I would suggest we use this formula as a default to get the rotation slider values

It may be useful for the users…

Do you think a pull request allowing a scale, rotationAxis and pivot point to define a the transform would be useful?

Also there is a button to switch between global and local coordinate systems… Why not allow any homogeneous transform as local?

Hope this comments are useful

Yes, it comes up from time to time that people wonder why the rotation sliders get reset. Even if the rotation decomposition is not unique it may still be useful to show it and use it as the basis for modification. An improved GUI to edit transforms would be very welcome.

1 Like

I’ll try to implement it

I think a third widget on the transforms module should show a scale-rotationAxisVector, a pivotPoint and and an angle (even if the pivotPoint ends up at infinity for some homogeneous transforms)

I’ve started diving into it but it’s hard. Should I create a new ctkvtkabstractmatrixwidget? Maybe with a checkbox to normalize the scale-rotatoonAxisVector
How do I change the default number of rows and columns?

More importantly how do I make VS Community recognize my changes to the S4 code folder?

Probably start first with a list of features you want to implement. Ideally yes, in a reusable widget at the ctk level, but possibly this is better as a mrml widget because you may rather use markups to define the pivot point, rotation lines, or reflection planes.

Not sure what you mean here. Aren’t you wanting to edit 4x4 matrices?

This seems like a completely unrelated question - maybe start a new thread with more detail.

I implemented an early version but I’m not able to make it work for sure, it’s unstable. I may need some help. Maybe I can show you all part of the implementation tomorrow on the developers call

I’ve played around with this a few times because indeed it seems to make sense to have Euler angle sliders. However, in practice it works very poorly. You can find a complete implementation in Python in this post that you can easily test (and try to fix):

It appears this works:

import math
import numpy as np

transformNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLTransformNode')
# transformNode = getNode('Transform')

def rotationToVtk(R):
    '''
    Concert a rotation matrix into the Mayavi/Vtk rotation paramaters (pitch, roll, yaw)
    '''
    def euler_from_matrix(matrix):
        """Return Euler angles (syxz) from rotation matrix for specified axis sequence.
        :Author:
          `Christoph Gohlke <http://www.lfd.uci.edu/~gohlke/>`_

        full library with coplete set of euler triplets (combinations of  s/r x-y-z) at
            http://www.lfd.uci.edu/~gohlke/code/transformations.py.html

        Note that many Euler angle triplets can describe one matrix.
        """
        # epsilon for testing whether a number is close to zero
        _EPS = np.finfo(float).eps * 5.0
        #
        # axis sequences for Euler angles
        _NEXT_AXIS = [1, 2, 0, 1]
        firstaxis, parity, repetition, frame = (1, 1, 0, 0) # ''
        #
        i = firstaxis
        j = _NEXT_AXIS[i+parity]
        k = _NEXT_AXIS[i-parity+1]
        #
        M = np.array(matrix, dtype='float', copy=False)[:3, :3]
        if repetition:
            sy = np.sqrt(M[i, j]*M[i, j] + M[i, k]*M[i, k])
            if sy > _EPS:
                ax = np.arctan2( M[i, j],  M[i, k])
                ay = np.arctan2( sy,       M[i, i])
                az = np.arctan2( M[j, i], -M[k, i])
            else:
                ax = np.arctan2(-M[j, k],  M[j, j])
                ay = np.arctan2( sy,       M[i, i])
                az = 0.0
        else:
            cy = np.sqrt(M[i, i]*M[i, i] + M[j, i]*M[j, i])
            if cy > _EPS:
                ax = np.arctan2( M[k, j],  M[k, k])
                ay = np.arctan2(-M[k, i],  cy)
                az = np.arctan2( M[j, i],  M[i, i])
            else:
                ax = np.arctan2(-M[j, k],  M[j, j])
                ay = np.arctan2(-M[k, i],  cy)
                az = 0.0
        #
        if parity:
            ax, ay, az = -ax, -ay, -az
        if frame:
            ax, az = az, ax
        return ax, ay, az
    #
    r_yxz = np.array(euler_from_matrix(R))*180/math.pi
    r_xyz = r_yxz[[1, 0, 2]]
    return r_xyz


transformObserver = 0
# Create widget
widget = qt.QFrame()
layout = qt.QFormLayout()
widget.setLayout(layout)
axisSliderWidgets = []
for i in range(3):
    axisSliderWidget = ctk.ctkSliderWidget()
    axisSliderWidget.singleStep = 1.0
    if i==1:
        axisSliderWidget.minimum = -90
        axisSliderWidget.maximum = 90
    else:
        axisSliderWidget.minimum = -180
        axisSliderWidget.maximum = 180
    axisSliderWidget.value = 0
    axisSliderWidget.tracking = False
    layout.addRow(f"Axis {i+1}: ", axisSliderWidget)
    axisSliderWidgets.append(axisSliderWidget)


def updateTransformFromWidget(value):
    global transformObserver
    transform = vtk.vtkTransform()
    transform.RotateY(axisSliderWidgets[1].value)
    axisOfRotation = [1,0,0,0]
    transform.GetMatrix().MultiplyPoint(axisOfRotation,axisOfRotation)
    transform.RotateWXYZ(axisSliderWidgets[0].value,axisOfRotation[0],axisOfRotation[1],axisOfRotation[2])
    #transform.RotateX(axisSliderWidgets[0].value)
    axisOfRotation = [0,0,1,0]
    transform.GetMatrix().MultiplyPoint(axisOfRotation,axisOfRotation)
    transform.RotateWXYZ(axisSliderWidgets[2].value,axisOfRotation[0],axisOfRotation[1],axisOfRotation[2])
    #transform.RotateZ(axisSliderWidgets[1].value)
    transformNode.RemoveObserver(transformObserver)
    transformNode.SetMatrixTransformToParent(transform.GetMatrix())
    transformObserver = transformNode.AddObserver(slicer.vtkMRMLTransformableNode.TransformModifiedEvent, updateWidgetFromTransform)

def updateWidgetFromTransform(caller, event):
    transformMatrix = transformNode.GetMatrixTransformToParent()
    transformMatrix_np = arrayFromVTKMatrix(transformMatrix)
    angles_xyz = rotationToVtk(transformMatrix_np)
    for i in range(3):
        axisSliderWidget = axisSliderWidgets[i]
        wasBlocked = axisSliderWidget.blockSignals(True)
        axisSliderWidget.value = angles_xyz[i]
        axisSliderWidget.blockSignals(wasBlocked)

def resetTransform():
    transformNode.SetMatrixTransformToParent(vtk.vtkMatrix4x4())

for i in range(3):
    axisSliderWidgets[i].connect("valueChanged(double)", updateTransformFromWidget)

resetButton = qt.QPushButton("Reset")
layout.addWidget(resetButton)
resetButton.connect("clicked()", resetTransform)

widget.show()

transformObserver = transformNode.AddObserver(slicer.vtkMRMLTransformableNode.TransformModifiedEvent, updateWidgetFromTransform)

# transformNode.RemoveObserver(transformObserver)

Your feedback would be great

This script works beautifully in the sense that the rotation matrix to sliders mapping is invertible.

However, I could not make out any sense of the transform that is created from the sliders. For example, set rotation around Axis3 to 40 deg (nothing extreme) and see the wild spinning around a rotating axis:

How would you use these sliders to achieve a desired orientation?

In contrast, Slicer’s rotations are really simple and meaningful and you can easily achieve any orientation:

Rotation around the head’s RAS axes:

Rotation around the world’s RAS axes:

It seems that Blender struggles with this, too - see their documentation. No good solution exists, so they offer multiple orientation parameterization options, each with its own problems. Blender seem to store the orientation using the chosen parametrization - probably because it needs to make those parameters adjustable in animations. But this has a very high price - modifying orientation becomes a really complicated task (you could not simply set an absolute transform, but you would need to know how to make incremental changes to a transform to achieve the desired orientation, with each parametrization option), so I don’t think we should do this in Slicer.

If we used the Gohlke parametrization then we could keep displaying the slider values (we would not need to reset the sliders to 0) but for me the displayed angle values don’t seem to be meaningful, so I’m not sure if it is worth the trouble. It is also not a standard parametrization, so it would not be portable to other applications (it would not correspond to angles displayed in Blender, ParaView, etc.).

1 Like

Hi Andras.

Please try this one:

import math
import numpy as np

transformNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLTransformNode')
# transformNode = getNode('Transform')

def rotationToVtk(R):
    '''
    Concert a rotation matrix into the Mayavi/Vtk rotation paramaters (pitch, roll, yaw)
    '''
    def euler_from_matrix(matrix):
        """Return Euler angles (syxz) from rotation matrix for specified axis sequence.
        :Author:
          `Christoph Gohlke <http://www.lfd.uci.edu/~gohlke/>`_

        full library with coplete set of euler triplets (combinations of  s/r x-y-z) at
            http://www.lfd.uci.edu/~gohlke/code/transformations.py.html

        Note that many Euler angle triplets can describe one matrix.
        """
        # epsilon for testing whether a number is close to zero
        _EPS = np.finfo(float).eps * 5.0
        #
        # axis sequences for Euler angles
        _NEXT_AXIS = [0, 0, 1, 2]
        #
        #inner axis: code of axis (‘x’:0, ‘y’:1, ‘z’:2) of rightmost matrix.
        #parity : even (0) if inner axis ‘x’ is followed by ‘y’, ‘y’ is followed by ‘z’, or ‘z’ is followed by ‘x’. Otherwise odd (1).
        #repetition : first and last axis are same (1) or different (0).
        #frame : rotations are applied to static (0) or rotating (1) frame.
        #
        firstaxis, parity, repetition, frame = (2, 0, 0, 1) # ''
        #
        i = firstaxis
        j = _NEXT_AXIS[i+parity]
        k = _NEXT_AXIS[i-parity+1]
        #
        M = np.array(matrix, dtype='float', copy=False)[:3, :3]
        if repetition:
            sy = np.sqrt(M[i, j]*M[i, j] + M[i, k]*M[i, k])
            if sy > _EPS:
                ax = np.arctan2( M[i, j],  M[i, k])
                ay = np.arctan2( sy,       M[i, i])
                az = np.arctan2( M[j, i], -M[k, i])
            else:
                ax = np.arctan2(-M[j, k],  M[j, j])
                ay = np.arctan2( sy,       M[i, i])
                az = 0.0
        else:
            cy = np.sqrt(M[i, i]*M[i, i] + M[j, i]*M[j, i])
            if cy > _EPS:
                ax = np.arctan2( M[k, j],  M[k, k])
                ay = np.arctan2(-M[k, i],  cy)
                az = np.arctan2( M[j, i],  M[i, i])
            else:
                ax = np.arctan2(-M[j, k],  M[j, j])
                ay = np.arctan2(-M[k, i],  cy)
                az = 0.0
        #
        if parity:
            ax, ay, az = -ax, -ay, -az
        if frame:
            ax, az = az, ax
        return ax, ay, az
    #
    r_yxz = np.array(euler_from_matrix(R))*180/math.pi
    r_xyz = r_yxz[[1, 0, 2]]
    return r_xyz


transformObserver = 0
# Create widget
widget = qt.QFrame()
layout = qt.QFormLayout()
widget.setLayout(layout)
axisSliderWidgets = []
for i in range(3):
    axisSliderWidget = ctk.ctkSliderWidget()
    axisSliderWidget.singleStep = 1.0
    axisSliderWidget.minimum = -180
    axisSliderWidget.maximum = 180
    axisSliderWidget.value = 0
    axisSliderWidget.tracking = False
    layout.addRow(f"Axis {i+1}: ", axisSliderWidget)
    axisSliderWidgets.append(axisSliderWidget)


def updateTransformFromWidget(value):
    global transformObserver
    transform = vtk.vtkTransform()
    transform.RotateY(axisSliderWidgets[1].value)
    axisOfRotation = [1,0,0,0]
    transform.GetMatrix().MultiplyPoint(axisOfRotation,axisOfRotation)
    transform.RotateWXYZ(axisSliderWidgets[0].value,axisOfRotation[0],axisOfRotation[1],axisOfRotation[2])
    #transform.RotateX(axisSliderWidgets[0].value)
    axisOfRotation = [0,0,1,0]
    transform.GetMatrix().MultiplyPoint(axisOfRotation,axisOfRotation)
    transform.RotateWXYZ(axisSliderWidgets[2].value,axisOfRotation[0],axisOfRotation[1],axisOfRotation[2])
    #transform.RotateZ(axisSliderWidgets[1].value)
    transformNode.RemoveObserver(transformObserver)
    transformNode.SetMatrixTransformToParent(transform.GetMatrix())
    transformObserver = transformNode.AddObserver(slicer.vtkMRMLTransformableNode.TransformModifiedEvent, updateWidgetFromTransform)

def updateWidgetFromTransform(caller, event):
    transformMatrix = transformNode.GetMatrixTransformToParent()
    transformMatrix_np = arrayFromVTKMatrix(transformMatrix)
    angles_xyz = rotationToVtk(transformMatrix_np)
    for i in range(3):
        axisSliderWidget = axisSliderWidgets[i]
        wasBlocked = axisSliderWidget.blockSignals(True)
        axisSliderWidget.value = angles_xyz[i]
        axisSliderWidget.blockSignals(wasBlocked)

def resetTransform():
    transformNode.SetMatrixTransformToParent(vtk.vtkMatrix4x4())

for i in range(3):
    axisSliderWidgets[i].connect("valueChanged(double)", updateTransformFromWidget)

resetButton = qt.QPushButton("Reset")
layout.addWidget(resetButton)
resetButton.connect("clicked()", resetTransform)

widget.show()

transformObserver = transformNode.AddObserver(slicer.vtkMRMLTransformableNode.TransformModifiedEvent, updateWidgetFromTransform)

# transformNode.RemoveObserver(transformObserver)

I think it works

Thanks for the update. Maybe it works slightly differently, but the main problems remain the same: when you set one angle to non-zero (e.g., axis3=40) then adjust rotation around a different axis then it spins the object around a rotating axis, which results in an very complicated motion pattern and non-meaningful angle values.

1 Like

I think there is a solution to this problem using euler intrinsic angles:

Let’s say there are n sliders (could be 3) controlling the angle of rotation and a combobox defining the relative x, y or z axis of rotation (to the previous transform).
That way the transformation T for rotations in post-multiply order of for example: z’‘’=15,x’‘=30,y’=20,x=25

T = R(z''',15)*R(x'',30)*R(y',20)*R(x,25)

This is slider acculation is cumbersome but confortable for sequencial rotation transform definition. It could be compressed to only 3 sliders with angle values update according to the current transform when another of the 3 sliders is hovered. And changes to the last hovered slider (ie last rotation applied) would still be meaningful to a rotation axis that matches one of transformed, original axis of the polydata. Maybe some logic could be applied to hide this jumpy behavior of the angle values (as rotation axis vectors change but are not visible to the user when the previously explained behavior is executed)

The problem is that it would be impossible to compute the value of those 4 sliders from a 4x4 matrix. To summarize the requirements:

  1. invertible conversion between parameters (adjusted by the sliders) and 3x3 orientation matrix
  2. meaningful parameter values
  3. rotation along a stationary axis when any of the parameters are changed
  4. slight change of orientation when parameters are slightly changed (smooth orientation change, no jumps)

I believe it is impossible to fulfill all these requirements. I don’t think any other software managed to solve this. There are many ways to solve this if we relax a few requirements, but which requirements to relax depends on the application (probably you would not use the same parameterization for a fluoroscopy C-arm as for a knee joint angle). Application-specific transform editing sliders are added in extensions (e.g., we’ll release a C-arm simulator in SlicerHeart soon and it has meaningful C-arm angle display and editing with sliders).

Currently in Slicer core we avoid any particular parametrization. Using slider widgets for this is probably a bit confusing, the proper representation would be a jogwheel control, but there is no suitable built-in control in Qt.

Horizontal jogwheel control (looks similar to a slider but it can be dragged at any point, there is no handle; would be usable instead of a Slicer, but it is not a built-in Qt feature):

image

Round jogwheel control (available in Qt as QDial - unfortunately, it is very inconvenient to use, as the user needs to move the mouse pointer in a circular pattern to adjust it):

image

Please check this example:

It achieves 3 and 4. More or less 2 and 1.
It selects the most convenient intrinsic-euler angles representation and uses it for new calculations but shows the default Roll, Yaw, Pitch sliders to define the angles so it is comfortable and not jumpy.

Hope you consider it useful

1 Like