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

BUG: Fix computation of offset direction label in slicer view controller #6781

Merged
merged 1 commit into from
Jan 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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