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

ENH: Add widget to specify labelmap geometry in segmentations #975

Closed

Conversation

cpinter
Copy link
Member

@cpinter cpinter commented Jun 20, 2018

  • Add qMRMLSegmentationGeometryWidget that allows the user to specify a volume/segmentation/model/ROI as source, and set geometry details
    • For segmentation with labelmap master it is possible to set isotropic spacing and oversampling factor. Spacing cannot be set manually in these cases
    • For model, ROI, or segmentation with poly data master the parent transform is used for orientation, user input for spacing, and the bounding box for origin and extent
  • Add qMRMLSegmentationGeometryDialog that has a geometry widget in it, and OK and Cancel buttons if editing is enabled, and an OK button otherwise. If resampling is requested, then besides setting the reference image geometry conversion parameter, the existing segments are resampled to the specified geometry
  • Add button in row of master volume in Segment Editor widget that opens the geometry dialog
  • Replace button in conversion parameters dialog for specifying reference image geometry. Before it was a simple dialog that allowed selecting a volume, now the new geometry dialog shows up
  • Add python test for testing all kinds of scenarios for segmentation labelmap geometry calculation
  • Crash fixed in vtkCalculateOversamplingFactor: when factor was very low, invalid extent was calculated and then crashed when calculating spacing based on that

@cpinter
Copy link
Member Author

cpinter commented Jun 20, 2018

Button in Segment Editor. Brings up the geometry dialog. When OK is clicked then resampling happens:
image

Button in conversion parameters dialog. Brings up the geometry dialog. Only the conversion parameter is set when OK is clicked:
image

When opening the dialog, the current geometry is shown:
image

If volume or segmentation with labelmap master is chosen as source. Spacing and directions come from the volume, but isotropic spacing or oversampling can be specified:
image

Segmentation with closed surface master, model node, or ROI node can also be selected. Spacing can be manually specified, and the orientation (parent transform) of the source node is considered:
image

@lassoan
Copy link
Contributor

lassoan commented Jun 21, 2018

Thank you, this will be awesome! I've found a couple of smaller problems:

  1. When selecting an ROI as source, spacing widget acts quite randomly. For example:
  • select ROI as source geometry => spacing is 1, 1, 1
  • click in first component spacing, press Ctrl-a to select all, press '2' key => 1.0 appears (I would have expected 2)
  • press Ctrl-a to select all, press '2' key => 2.0 appears (correct)
  1. When selecting an ROI as source, dimensions are always 1,1,1

  2. After resampling, segmentation does not get updated in the slice viewers. Need to switch between slices or zoom in/out, etc..

  3. After resampling, call qMRMLSegmentEditorWidget::updateSliceRotateWarningButtonVisibility() to show rotate to segmentation plane button when needed.

  4. Make default step size for oversampling factor to 0.5 (instead of the current 1.0)

  5. When spacing value =0 or <0 is entered then computed dimensions are out of range. We should probably show 0,0,0 or -, -, - dimensions and disable OK button (and maybe write in the OK button's tooltip or in a label somewhere that input spacing is invalid)

  6. When the window is resized vertically then a large blank space appears above OK+Cancel buttons.

image

@fepegar
Copy link

fepegar commented Jun 21, 2018

This looks great!

@fedorov
Copy link
Member

fedorov commented Jun 21, 2018

I can definitely see how this can be a very helpful functionality! 👍

Did you consider making this widget not limited to segmentations? I think it might also be helpful sometime to do this kind of resampling operations for image volumes, and I don't think there is a straightforward way to do it right now in Slicer.

@lassoan
Copy link
Contributor

lassoan commented Jun 21, 2018

Did you consider making this widget not limited to segmentations? I think it might also be helpful sometime to do this kind of resampling operations for image volumes, and I don't think there is a straightforward way to do it right now in Slicer.

For volume nodes, Crop volume module can do almost the same (I've completely reworked it about a year ago). Is there anything specific that you think should be added there?

@fedorov
Copy link
Member

fedorov commented Jun 21, 2018

@lassoan yes, I agree it can do it, but it requires annotation ROI, while this widget would be more efficient for the tasks when you just want to resample the volume within the original FOV. It is just a more streamlined approach.

I really don't want to stall this PR, just wanted to mention this another potential utility - if you agree, and if it is not a lot of work, might make sense to add. But should not be a stopper, and can be considered later.

@lassoan
Copy link
Contributor

lassoan commented Jun 21, 2018

Yes, this discussion does not affect this PR (maybe we should move it to Discourse or the issue tracker).

If you don't specify a ROI then Crop volume module creates the ROI automatically and fits it to the input volume. Resample scalar volume can be used for this, too, but we've found that Crop volume is a better place, as when you oversample an image, you usually also want to crop it, to keep number of voxels under control. I think the main problem is that there are about 4 modules for resampling volumes and you can also resample using Crop volume and Transforms modules. So there are 6 modules in total, with slightly different capabilities to do the same thing, which is of course quite confusing.

@fedorov
Copy link
Member

fedorov commented Jun 21, 2018

Right, so the issues with CropVolume:

  • it creates ROI even if I don't need it
  • it does not have the same flexibility as the proposed widget for segmentations (see below)

My suggestion was exactly along the lines of reducing the confusion. Geometry update operation is not uncommon for volumes, there is no straightforward way to do it, and now we introduce yet another way to do resampling, but it would only work for segmentations. :-/

image

@cpinter
Copy link
Member Author

cpinter commented Jun 21, 2018

Thanks for the feedback!

@lassoan I'm not sure what's the problem with comment 7. What would you like to see instead? Elongated matrix widget looks very strange, I think the empty space is much better.

I'm not sure when I can work on this, as we leave in a few hours, but hopefully on the bus/plane.

@lassoan
Copy link
Contributor

lassoan commented Jun 21, 2018

I'm not sure what's the problem with comment 7

For me, it looks ugly. If there is no element that would look OK as expanded then we should prevent expansion. This is certainly not a blocking issue, it can be addressed later.

@lassoan
Copy link
Contributor

lassoan commented Jun 21, 2018

now we introduce yet another way to do resampling, but it would only work for segmentations

OK, you can use this to resample a segmentation, but it is not intended for that. It is for defining geometry of labelmap representation before segmentation is started.

@cpinter
Copy link
Member Author

cpinter commented Jun 21, 2018

I could rename the widget to qMRMLVolumeGeometryWidget, and put it to Libs/MRML/Widgets, so that later when we add this to other modules, then it's more proper.

@cpinter
Copy link
Member Author

cpinter commented Jun 21, 2018

For me, it looks ugly. If there is no element that would look OK as expanded then we should prevent expansion.

I cannot really specify a maximum height because of the different DPIs that we support now. If it's ugly, then you shouldn't resize it to so high. Or did it end up like this on its own?

@lassoan
Copy link
Contributor

lassoan commented Jun 21, 2018

We can make the dialog fixed height, with size retrieved from layout->totalMinimumSize().height(). It is done like this in QMessageBox (in QMessageBoxPrivate::updateSize()) when you click the "Show details" button:

slicer.util.errorDisplay("This is some text here", detailedText="some text "*10)

@cpinter
Copy link
Member Author

cpinter commented Jun 21, 2018

  1. I didn't understand the issue completely, but I started to reproduce it, and maybe what I found is the same thing. The problem stems from the automatic axis fitting step that assigns the axes of the ROI to those of the labelmap. So if you enter 2,1,1 for spacing, then the actual spacing after this step may be 1,2,1. This looks like a bug on the UI. We'll need to talk about how to deal with this. Maybe store the axis assignment and consider the permutation when showing the spacing on the UI, but that's not a very good solution either. Let's talk about this in person.

  2. Dimensions for ROI and model sources are 1,1,1, because for non-volume type source nodes dimensions are not computed. Since as opposed to volume type sources, where the dimensions are calculated no matter what, for non-volume sources it would need to be calculated as a separate step. However, the dimensions do not mean much when you resample segments in a segmentation, because the actual segment dimensions will differ, because always just the effective extent of the segment is used (where there are non-zero voxels plus one row padding). I could potentially add the code from vtkOrientedImageDataResample that calculates the extent.
    Of course dimensions are necessary to show when resampling volume nodes, so if we decide to change the name and purpose of the widget to VolumeGeometry, then I'll need to do that. Considering that the project week is upon us, as well as the release, I'd only do this between the project week and the release, because this would take at least a few hours.
    I agree with both Andrey's comment about the general purpose widget, and the usefulness of seeing the dimensions, and so I definitely want to do this, but unfortunately I'll be on vacation after the project week, and I'm not sure yet I'll have time for this before the release. So I'd rather integrate this now so that we have this important feature for 4.10, and implement these changes when I can, not necessarily during my vacation.

6, I'd probably just disallow 0 or negative values for spacing. How about changing the minimum value to 0.05mm?

+1. I cannot move the widget as is to MRML/Widgets, because of the ROI support (the ROI node is defined in the Annotations module). I have to deal with this when I get to doing this refactoring. I would hesitate to move any Annotations class into Libs, because of the "temporary state" of the module. I'm open to suggestions.

+2. I wanted to make the MRMLCoordinateWidgets show mm, as it is done in the Volumes module, but the obvious ways didn't work. Does anyone happen to know what's the trick?

I'll address the rest of Andras' comments now.

@cpinter cpinter force-pushed the segment-editor-geometry-widget branch from e21cd6d to 394c182 Compare June 21, 2018 21:31
@lassoan
Copy link
Contributor

lassoan commented Jun 21, 2018

  1. You only need axis permutation once, when you compute the default output spacing values.

  2. Dimensions are very important. They give you a very important hint about the feasibility of the chosen spacing value. I crashed the application several times while testing the widget, most probably because I accidentally ran out of memory. It should be the same extent that is used for labelmap export. Extent is also relevant because it is used in several effects (e.g., Logical operators effect / invert mode), and when converting models to labelmap you have the option to enlarge extent or crop to current extent.

  3. There is no minimum spacing value. Some people work with volumes of 1e-6 spacing, hardcoding anything would be a killer. We can hide dimensions widget and show a label instead with a text of "invalid spacing" or something like that and disable "OK" button.

+1. We might be able to use displayable node and GetBounds() method, but I think we can generalize later, when we actually need the widget somewhere else.

+2. You need to set the MRML scene (and of course quantity).

Fix whatever you can and let me know when you've run out of time and then I'll take over.

@lassoan
Copy link
Contributor

lassoan commented Jun 22, 2018

There are some build warnings and errors on CI:

/usr/src/Slicer/Modules/Loadable/Segmentations/Widgets/qMRMLSegmentationConversionParametersWidget.cxx: In member function ‘void qMRMLSegmentationConversionParametersWidget::onSpecifyGeometryButtonClicked()’:
/usr/src/Slicer/Modules/Loadable/Segmentations/Widgets/qMRMLSegmentationConversionParametersWidget.cxx:447:8: warning: unused variable ‘result’ [-Wunused-variable]
bool result = geometryDialog.exec();
^
^
/usr/src/Slicer/Modules/Loadable/Segmentations/Widgets/qMRMLSegmentEditorWidget.cxx: In member function ‘void qMRMLSegmentEditorWidget::showSegmentationGeometryDialog()’:
/usr/src/Slicer/Modules/Loadable/Segmentations/Widgets/qMRMLSegmentEditorWidget.cxx:3552:8: warning: unused variable ‘result’ [-Wunused-variable]
bool result = geometryDialog.exec();
^
FAILED: Modules/Loadable/Segmentations/Widgets/CMakeFiles/qSlicerSegmentationsModuleWidgets.dir/qMRMLSegmentationGeometryWidget.cxx.o
/opt/rh/devtoolset-4/root/usr/bin/g++ ... Modules/Loadable/Segmentations/Widgets/CMakeFiles/qSlicerSegmentationsModuleWidgets.dir/qMRMLSegmentationGeometryWidget.cxx.o.d -o Modules/Loadable/Segmentations/Widgets/CMakeFiles/qSlicerSegmentationsModuleWidgets.dir/qMRMLSegmentationGeometryWidget.cxx.o -c /usr/src/Slicer/Modules/Loadable/Segmentations/Widgets/qMRMLSegmentationGeometryWidget.cxx
/usr/src/Slicer/Modules/Loadable/Segmentations/Widgets/qMRMLSegmentationGeometryWidget.cxx:32:38: fatal error: vtKSegmentationConverter.h: No such file or directory
compilation terminated.
[4849/4979] Building CXX object Modules/Loadable/Segmentations/Widgets/CMakeFiles/qSlicerSegmentationsModuleWidgets.dir/qMRMLSegmentationGeometryDialog.cxx.o
[4850/4979] Building CXX object Modules/Loadable/Segmentations/Widgets/CMakeFiles/qSlicerSegmentationsModuleWidgets.dir/qMRMLDoubleSpinBoxDelegate.cxx.o
ninja: build stopped: subcommand failed.
Exited with code 1

@cpinter cpinter force-pushed the segment-editor-geometry-widget branch 2 times, most recently from 34c133f to a61216f Compare July 3, 2018 17:51
@cpinter
Copy link
Member Author

cpinter commented Jul 3, 2018

I believe I addressed all issues. Let me know if further changes are needed. Thanks!

Copy link
Contributor

@lassoan lassoan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be great! I've added a number of comments, they are mostly trivial. It would be nice to make the geometry setting available from logic or MRML (without widget), but if it's hard to do then it is not necessary to implement it now.

@@ -463,7 +463,7 @@ void vtkCalculateOversamplingFactor::ApplyOversamplingOnImageGeometry(vtkOriente
{
int dimension = extent[axis*2+1] - extent[axis*2] + 1;
int extentMin = static_cast<int>(ceil(oversamplingFactor * extent[axis * 2]));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be floor for minimum extent; and ceil for maximum extent?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure as I did it long ago, but back then I tested everything with the tiny patient and checked voxel by voxel, so this should be OK. If you want to double check, feel free to do it, write a test, or add a ticket.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just wondering if it may crop boundary voxels. I don't remember having seen any unwanted cropping, but also I did not use oversampling that much. Now that it'll be more easily available, I'll have more chance to use it and confirm that it works well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, thanks!

static const char* GetSegmentationClosedSurfaceRepresentationName() { return "Closed surface"; };
static const char* GetSegmentationPlanarContourRepresentationName() { return "Planar contour"; };
static const char* GetSegmentationClosedSurfaceRepresentationName() { return "Closed surface"; };
static const char* GetBinaryLabelmapRepresentationName() { return GetSegmentationBinaryLabelmapRepresentationName(); };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should make the old methods deprecated? We should at least document which methods should be used, but even better, we could log a warning if the deprecated methods are used to discourage their usage.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I can do it, but later (when I'm back) and in a separate commit, as these functions are called from several extensions as well, and I don't want to introduce warnings in them.
I did this because I realized that the "segmentation" in their name was unnecessary and they were super long.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. For now, it is enough to add comments that tell which method names are preferred.

return False
import numpy
tolerance = 0.0001
actualSpacing = orientedImageData.GetSpacing()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spacing, orientation, and directions comparison could be simplified by using

static bool vtkAddonMathUtilities::MatrixAreEqual(const vtkMatrix4x4* m1, const vtkMatrix4x4* m2, double tolerance = 1e-3);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know. However I wanted to be able to log error messages that are more punctual and are easier to interpret.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. If this comes up again then we may create a shared helper function in vtkAddon or slicer.util that can provide these more descriptive messages.

#TODO: Come up with proper geometry baselines for these too
vtkSegmentationCore.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
self.assertTrue(self.imageDataContainsData(geometryImageData))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to compare the number of non-empty voxels? It would catch much more potential bugs for sure, but of course it is only usable if it is not too fragile.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. This should be easy to add.

this, SLOT(onSetReferenceImageGeometryFromVolumeClicked()));
geometryLayout->addWidget(setGeometryFromVolumeButton);
QPushButton* specifyGeometryButton = new QPushButton(tr("Specify geometry"), geometryWidget);
//setGeometryFromVolumeButton->setFixedWidth(160);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not needed then it should be probably deleted

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left it there because it was not obvious what will be the proper solution. You hated the long widget after resizing it manually, but I'm not sure this is better.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if you could add a comment explaining why the code was left there, what it can be used for, etc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll remove this. I thought this is for the height issue, but it's not. Thanks

}

//-----------------------------------------------------------------------------
bool qMRMLSegmentationGeometryWidgetPrivate::isSourceASegmentationWithBinaryLabelmapMaster()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep "A" after isSource? I've seen "IsA" in code, but usually there is no "A" or "The" in names. It could be isSourceSegmentationWithBinaryLabelmapMaster.

std::string binaryLabelmapName = vtkSegmentationConverter::GetBinaryLabelmapRepresentationName();
vtkSmartPointer<vtkOrientedImageData> sourceBinaryLabelmap;
if ( sourceSegmentationNode
&& sourceSegmentationNode->GetSegmentation()->GetNumberOfSegments() > 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sourceSegmentationNode->GetSegmentation() can be nullptr, so it would be safer to check that

}

//-----------------------------------------------------------------------------
void qMRMLSegmentationGeometryWidgetPrivate::matchInputAndSourceAxes(vtkMRMLNode* sourceNode)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"match" does not indicate what this method actually does. "ComputeSourceAxisIndexForInputAxis" or something like that would be more informative.

{
qDebug() << Q_FUNC_INFO << "Master representation '" << masterRepresentationName.c_str() << "' in each segment of segmentation "
<< d->SegmentationNode->GetName() << " has been resampled to specified geometry";
d->SegmentationNode->Modified();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't we need to do this even if some of the resamplings fail?

Q_D(qMRMLSegmentationGeometryWidget);
if (!outputGeometry)
{
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we set d->GeometryImageData to identity or something like that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a get function. I don't understand why we should reset it after getting it.

@cpinter cpinter force-pushed the segment-editor-geometry-widget branch from a61216f to 96e332b Compare July 4, 2018 15:00
@cpinter
Copy link
Member Author

cpinter commented Jul 4, 2018

Thanks Andras! I'll think about the logic functions and issue a PR depending on what seems most reasonable.

@cpinter
Copy link
Member Author

cpinter commented Jul 5, 2018

@lassoan How would you imagine adding logic access? A new logic class in Segmentations/Logic or MRML/Logic that the widget would call?

- Add qMRMLSegmentationGeometryWidget that allows the user to specify a volume/segmentation/model/ROI as source, and set geometry details
  - For segmentation with labelmap master it is possible to set isotropic spacing and oversampling factor. Spacing cannot be set manually in these cases
  - For model, ROI, or segmentation with poly data master the parent transform is used for orientation, user input for spacing, and the bounding box for origin and extent
- Add qMRMLSegmentationGeometryDialog that has a geometry widget in it, and OK and Cancel buttons if editing is enabled, and an OK button otherwise. If resampling is requested, then besides setting the reference image geometry conversion parameter, the existing segments are resampled to the specified geometry
- Add button in row of master volume in Segment Editor widget that opens the geometry dialog
- Replace button in conversion parameters dialog for specifying reference image geometry. Before it was a simple dialog that allowed selecting a volume, now the new geometry dialog shows up
- Add python test for testing all kinds of scenarios for segmentation labelmap geometry calculation
- Crash fixed in vtkCalculateOversamplingFactor: when factor was very low, invalid extent was calculated and then crashed when calculating spacing based on that
@cpinter cpinter force-pushed the segment-editor-geometry-widget branch from 96e332b to ed284b2 Compare July 20, 2018 22:15
@cpinter
Copy link
Member Author

cpinter commented Jul 20, 2018

I made a logic class that does the calculation. A quick test didn't bring out any problems, and the automatic test passes so all the cases should still work.

I think I'll only generalize it to volumes later. It won't be as simple, as in MRML/Widgets there are some classes that are not accessible, so it needs some more thought.

If there are no objections, I'll integrate it tomorrow.

@cpinter
Copy link
Member Author

cpinter commented Jul 21, 2018

Integrated in Slicer/Slicer@6a8012e

@cpinter cpinter closed this Jul 21, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
4 participants