Skip to content

Commit

Permalink
BUG: Fix computation of offset direction label in slicer view controller
Browse files Browse the repository at this point in the history
Sagittal plane offset direction label "R" was incorrect: moving the slider to the right did not move the slice in "R" direction (while for axial and coronal slices, moving the slider to the right moved the slice according to the direction specified in the label).

Instead of changing the incorrect "R" label to "L", this commit removes the hardcoded labels and instead computes the correct label dynamically.
If the slice normal direction is not aligned with an axis then the label contains a combination of directions. The order of axes reflects the dominance of the axis.
For example, if the plane normal points to anterior and slightly left then the label is AL.

fixes #6778
  • Loading branch information
lassoan committed Jan 15, 2023
1 parent 594702a commit 5e3a9c3
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 37 deletions.
2 changes: 1 addition & 1 deletion Docs/user_guide/user_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ View Controllers module provides an alternate way of displaying these controller
- **Slice orientation** displays allows you to choose the orientation for this slice view.
- **Lightbox** to select a mosiac (a.k.a. contact sheet) view. Not all operations work in this mode and it may be removed in the future.
- **Reformat** allows interactive manipulation of the slice orientation.
- **Slice offset slider** allows slicing through the volume. Step size is set to the background volume's spacing by default but can be modified by clicking on "Spacing and field of view" button.
- **Slice offset slider** allows slicing through the volume. Step size is set to the background volume's spacing by default but can be modified by clicking on "Spacing and field of view" button. The label next to the offset value (e.g., `S`, `L`, `A`, `IL`, `IRP`) reflects the slice normal direction. If the offset slider moved to the right then the slice moves in this normal direction. If the slice normal direction is not aligned with an axis then the label contains a combination of directions, with the order of axes reflecting the dominance of the axis. For example, if the plane normal points to anterior and slightly left then the label is `AL`, while if the plane normal mostly left and slightly anterior then the label is `LA`.
- **Blending mode** specifies how foreground and background layers are mixed.
- **Spacing and field of view** Spacing defines the increment for the slice offset slider. Field of view sets the zoom level for the slice.
- **Rotate to volume plane** changes the orientation of the slice to match the closest acquisition orientation of the displayed volume.
Expand Down
59 changes: 59 additions & 0 deletions Libs/MRML/Core/vtkMRMLAbstractViewNode.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -665,3 +665,62 @@ vtkMRMLLayoutNode* vtkMRMLAbstractViewNode::GetMaximizedState(bool& maximized, b
}
return layoutNode;
}

//------------------------------------------------------------------------------
std::string vtkMRMLAbstractViewNode::GetDirectionLabel(double direction[3], bool positive/*=true*/, double toleranceDeg/*=1.0*/)
{
if (vtkMath::Norm(direction) == 0.0)
{
return "?";
}
if (this->AxisLabels->GetNumberOfValues() < 6)
{
return "?";
}
double toleranceRad = vtkMath::RadiansFromDegrees(toleranceDeg);

// Compute labels and angles
std::string axisLabels[3];
double absoluteNormalAngleDiffsRad[3] = { 0.0, 0.0, 0.0 };
double axisSign = positive ? 1.0 : -1.0;
for (int axisIndex = 0; axisIndex < 3; ++axisIndex)
{
double axisDirection[3] = { axisIndex == 0 ? axisSign : 0.0, axisIndex == 1 ? axisSign : 0.0, axisIndex == 2 ? axisSign : 0.0 };
double normalAngleDiffRad = (vtkMath::AngleBetweenVectors(direction, axisDirection) - vtkMath::Pi() / 2.0);
double absoluteNormalAngleDiffRad = fabs(normalAngleDiffRad);
if (absoluteNormalAngleDiffRad < toleranceRad)
{
// orthogonal to this axis, do not add its label
continue;
}
absoluteNormalAngleDiffsRad[axisIndex] = absoluteNormalAngleDiffRad;
axisLabels[axisIndex] = this->AxisLabels->GetValue(axisIndex * 2 + ((positive == (normalAngleDiffRad > 0)) ? 0 : 1));
}

// Concatenate labels, ordered by angle difference
std::string label;
for (int axisIndex = 0; axisIndex < 3; ++axisIndex)
{
int axisA = axisIndex;
int axisB = (axisIndex + 1) % 3;
int axisC = (axisIndex + 2) % 3;
if (absoluteNormalAngleDiffsRad[axisA] >= absoluteNormalAngleDiffsRad[axisB] && absoluteNormalAngleDiffsRad[axisA] >= absoluteNormalAngleDiffsRad[axisC])
{
label += axisLabels[axisA];
if (absoluteNormalAngleDiffsRad[axisB] >= absoluteNormalAngleDiffsRad[axisC])
{
label += axisLabels[axisB];
label += axisLabels[axisC];
}
else
{
label += axisLabels[axisC];
label += axisLabels[axisB];
}
return label;
}
}

// should not happen, but the compiler may complain if this case is not handled
return "?";
}
6 changes: 6 additions & 0 deletions Libs/MRML/Core/vtkMRMLAbstractViewNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,12 @@ class VTK_MRML_EXPORT vtkMRMLAbstractViewNode
const char* GetAxisLabel(int labelIndex);
void SetAxisLabel(int labelIndex, const char* label);

/// Returns label for the specified direction. For example, (1,0,0) direction returns
/// the positive x axis label "R".
/// toleranceDeg specifies the tolerance when when determining if the direction
/// is parallel with an axis.
std::string GetDirectionLabel(double direction[3], bool positive=true, double toleranceDeg=1.0);

/// Total number of coordinate system axis labels
static const int AxisLabelsCount;

Expand Down
35 changes: 9 additions & 26 deletions Libs/MRML/Widgets/qMRMLSliceControllerWidget.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
#include <vtkMRMLUnitNode.h>

// VTK includes
#include <vtkMatrix4x4.h>
#include <vtkNew.h>
#include <vtkStringArray.h>

Expand All @@ -76,16 +77,6 @@ qMRMLSliceControllerWidgetPrivate::qMRMLSliceControllerWidgetPrivate(qMRMLSliceC

this->ControllerButtonGroup = nullptr;

qMRMLOrientation axialOrientation = {qMRMLSliceControllerWidget::tr("S: "), qMRMLSliceControllerWidget::tr("I <-----> S")};
qMRMLOrientation sagittalOrientation = {qMRMLSliceControllerWidget::tr("R: "), qMRMLSliceControllerWidget::tr("L <-----> R")};
qMRMLOrientation coronalOrientation = {qMRMLSliceControllerWidget::tr("A: "), qMRMLSliceControllerWidget::tr("P <-----> A")};
qMRMLOrientation obliqueOrientation = {"", qMRMLSliceControllerWidget::tr("Oblique")};

this->SliceOrientationToDescription["Axial"] = axialOrientation;
this->SliceOrientationToDescription["Sagittal"] = sagittalOrientation;
this->SliceOrientationToDescription["Coronal"] = coronalOrientation;
this->SliceOrientationToDescription["Reformat"] = obliqueOrientation;

this->LastLabelMapOpacity = 1.;
this->LastForegroundOpacity = 1.;
this->LastBackgroundOpacity = 1.;
Expand Down Expand Up @@ -892,10 +883,14 @@ void qMRMLSliceControllerWidgetPrivate::updateWidgetFromMRMLSliceNode()
Self::updateSliceOrientationSelector(sliceNode, this->SliceOrientationSelector);

// Update slice offset slider tooltip
qMRMLOrientation orientation = this->mrmlOrientation(
QString::fromStdString(sliceNode->GetOrientation().c_str()));
this->SliceOffsetSlider->setToolTip(orientation.ToolTip);
this->SliceOffsetSlider->setPrefix(orientation.Prefix);
vtkMatrix4x4* sliceToRas = sliceNode->GetSliceToRAS();
double planeNormal[3] = { sliceToRas->GetElement(0, 2), sliceToRas->GetElement(1, 2), sliceToRas->GetElement(2, 2) };
std::string positiveAxisLabel = sliceNode->GetDirectionLabel(planeNormal, true);
std::string negativeAxisLabel = sliceNode->GetDirectionLabel(planeNormal, false);
this->SliceOffsetSlider->setToolTip(QString("%1 <-----> %2")
.arg(QString::fromStdString(positiveAxisLabel))
.arg(QString::fromStdString(negativeAxisLabel)));
this->SliceOffsetSlider->setPrefix(QString("%1: ").arg(QString::fromStdString(positiveAxisLabel)));

// Update slice visibility toggle
this->actionShow_in_3D->setChecked(sliceNode->GetSliceVisible());
Expand Down Expand Up @@ -1553,18 +1548,6 @@ void qMRMLSliceControllerWidgetPrivate::setupRulerMenu()
rulerMenu->addActions(rulerColorActions->actions());
}

// --------------------------------------------------------------------------
qMRMLOrientation qMRMLSliceControllerWidgetPrivate::mrmlOrientation(const QString &name)
{
QHash<QString, qMRMLOrientation>::iterator it = this->SliceOrientationToDescription.find(name);
if (it != this->SliceOrientationToDescription.end())
{
return it.value();
}
qMRMLOrientation obliqueOrientation = {"", qMRMLSliceControllerWidget::tr("Oblique")};
return obliqueOrientation;
}

// --------------------------------------------------------------------------
void qMRMLSliceControllerWidgetPrivate::onSegmentVisibilitySelectionChanged(QStringList selectedSegmentIDs)
{
Expand Down
10 changes: 0 additions & 10 deletions Libs/MRML/Widgets/qMRMLSliceControllerWidget_p.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,6 @@ class vtkObject;
class vtkMRMLSegmentationDisplayNode;
class vtkMRMLSelectionNode;

//-----------------------------------------------------------------------------
struct QMRML_WIDGETS_EXPORT qMRMLOrientation
{
QString Prefix;
QString ToolTip;
};

//-----------------------------------------------------------------------------
class QMRML_WIDGETS_EXPORT qMRMLSliceControllerWidgetPrivate
: public qMRMLViewControllerBarPrivate
Expand Down Expand Up @@ -99,8 +92,6 @@ class QMRML_WIDGETS_EXPORT qMRMLSliceControllerWidgetPrivate
void setupOrientationMarkerMenu();
void setupRulerMenu();

qMRMLOrientation mrmlOrientation(const QString& name);

vtkSmartPointer<vtkCollection> saveNodesForUndo(const QString& nodeTypes);

void enableLayerWidgets();
Expand Down Expand Up @@ -178,7 +169,6 @@ public slots:
vtkSmartPointer<vtkMRMLSliceLogic> SliceLogic;
vtkCollection* SliceLogics;
vtkWeakPointer<vtkAlgorithmOutput> ImageDataConnection;
QHash<QString, qMRMLOrientation> SliceOrientationToDescription;
QButtonGroup* ControllerButtonGroup;

QToolButton* FitToWindowToolButton;
Expand Down

0 comments on commit 5e3a9c3

Please sign in to comment.