-
Notifications
You must be signed in to change notification settings - Fork 459
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: VectorToScalar optionally extract single components. #967
Conversation
681ebd0
to
31a4d45
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, except for the checkboxes that disable each other (see comment in code).
def onRGBCheckBox(self, state): | ||
if (state == 2): # Checked | ||
# Disable other box. | ||
self.singleComponentBox.setChecked(False) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know exactly how the UI looks like, but if you have to do this, then probably radio button would be a better widget to use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely right, thanks @cpinter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can then use two QRadioButton
, if they have the same parent, the radio buttons will be autoExclusive by default. Only one will be checked at a time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't get the ratio buttons to be exclusive just with the parent (maybe because there are frames?). Anyway, I created an exclusive QButtonGroup, and add the buttons there. It is a few lines more verbose but explicit. Let me know what you think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Common parent and setAutoExclusive(true) usually does the trick. Maybe you forgot the latter? Jc says it's not needed, but maybe it depends on the order you do things. What happens if you set it to true explicitly? That said I wouldn't mind the button group if that's how you can make it work.
LGTM - thanks! 👍 |
self.componentsComboBox.setEnabled(False) | ||
|
||
def onSingleComponentCheckBox(self, state): | ||
if (state == 2): # Checked |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case like this one, parenthesis are not needed in python.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For future reference, Qt enum value can be accessed using qt.Qt.Checked
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
qt.Qt.Checked
good to know.
@@ -76,6 +103,26 @@ def setup(self): | |||
# Add vertical spacer | |||
self.layout.addStretch(1) | |||
|
|||
def onRGBCheckBox(self, state): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would then be named onRGBRadioButtonToggled(self, checked):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed one slot, and just work with a RadioButton toggle, checking the state of the buttons themselves, not the boolean signal.
31a4d45
to
854f29a
Compare
Excellent, thanks for the contribution! |
If you only select between integers then use a spinbox instead of a combobox. But I think it would be even better to make Luminance option the first option in the combobox. Then we could get rid of all the radiobuttons and have a simple label (Component) and a combobox (Luminance, 0, 1, 2,...). Note that luminance is not the same as mean(r, g, b). I think we use the latter. If you really need luminance then it could be an additional entry in the combobox, which is only displayed for 3 or maybe 4 component images (assuming rgb and rgba). Number of components should be updated whenever a new volume is selected. We could also add an observer to the image change event of the node so that we can update the widget when some module changes the volume in the background or the user changes things in the Python interactor. |
854f29a
to
c009f67
Compare
@lassoan thanks a lot for the input.
That is/was already implemented.
Sounds good, maybe in another PR?
This module was using the vtkLuminanceImage, so it was computing luminance, not mean. But adding mean or any other function that reduces dimensionality to scalar sounds good.
Nice, I'll do.
Yeah, I liked the idea, and @jcfr also mentioned it. |
The GUI looks nice now, so you can leave it like this. If you consider using a single combobox: Using combox is easy, as you can set custom data for each item, so you don't need to maintain an external mapping. Observing image data should be quite simple, too (about 6-8 lines of code) and all core modules have this kind of always-up-to-date GUI (you don't need to click around the GUI to get it updated). If you don't have the time to add this small amount of logic then use a spinbox with very high max value instead of enumerating specific values. Unfortunately, this scripted module has other fundamental issues (it does not use parameter node, so user selections are not saved into the scene; there is no logic class - everything is just dumped into the widget class), which should be addressed at some point in the future. Thank you for working on this and sorry for being picky, but we try to keep Slicer core modules quality as high as possible, as they may be used in many other modules and extensions; and they also often serve as example for custom modules. |
@lassoan Thanks for the careful review 👍
@phcerdan I suggest you add support for these two features. Let me know if you need help.
Good news is that this PR now add a logic class |
New commit because there was quite a few changes, and didn't want to lose the original changes. I have added: And the new method to compute the average of all components. |
707fe3c
to
583e96b
Compare
I have an issue with the automatic visualization of the generated output. Is there any way to force re-compute the map between pixel-value range <-> visualization at the application level, or any method of the VolumeNode? Thanks! |
You can add a vtkImageCast filter to convert double pixel type to the same pixel type as the input vector volume. If automatic window width/leve (brightness/contrast) settings are not good then you can copy window width/level values from the display node of the input vector volume. |
This commit improves the user interface adding support for choosing the conversion mode. Available conversion modes are now: * vector (RGB, RGBA) images to luminance (default mode keeping backward compatibility with original module implementation) * vector images to single component (user select which component to extract) To support reusing of components by other scripted modules, a logic and widget have also been created: * VectorToScalarVolumeConversionModeWidget * VectorToScalarVolumeLogic Co-authored-by: Jean-Christophe Fillion-Robin <[email protected]> Reviewed-by: Andras Lasso <[email protected]> Reviewed-by: Csaba Pinter <[email protected]> Reviewed-by: Steve Pieper <[email protected]>
90104c6
to
6016aab
Compare
Thanks @lassoan, I casted it to the same Scalar Type than input for consistency with the other methods. And now it uses a good window with/level when changing methods. |
6016aab
to
4319889
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, the module is very nice and a great improvement over how it was implemented before. There are a couple of smaller things that would be great if you could improve - see in the in the code comments.
@contextmanager | ||
def ScopedQtPropertySetter(qobject, properties): | ||
""" Context manager to set/reset properties""" | ||
previsousValues = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo - previsous
if not (inputVolume and outputVolume): | ||
slicer.util.errorDisplay('Input and output volumes are required for conversion', windowTitle='Luminance') | ||
|
||
with ScopedQtPropertySetter(self.applyButton, {"enabled": False, "text": "Working..."}): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a very interesting idea. I would normally use try/except/finally for this sort of thing, but this setter seems simpler to use. It might make sense to move it to slicer.util so that it can be used elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move it to slicer.util so that it can be used elsewhere.
That was indeed the plan. I suggest we do not use this call in this PR and introduced it only later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just renamed it (it is too handy now to not use it!). If you want to move it to slicer.util afterwards with the good name, I can remove the local function here afterwards.
|
||
def __init__(self, parent=None): | ||
qt.QWidget.__init__(self, parent) | ||
self._inputVolumeNode = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The widget already stores the nodes in node selectors and component to extract in components combobox. It is redundant to also store them in member variables. I would remove them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
return self._inputVolumeNode | ||
|
||
def setInputVolumeNode(self, node): | ||
if isinstance(node, basestring): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This kind of ambiguity - accepting a node or some string (node id? node name? what if there are multiple nodes with the same name?) - is acceptable in testing and development tools for convenience, but not in production code.
For example, slicer.util.getNode has this behavior exactly because it is often used in the Python console to interactively get a node. However, in modules you would use slicer.mrmlScene.GetNode()or slicer.mrmlScene.GetFirstNodeByName().
Since the widget is not supposed to be used from any other modules (you only interact through MRML nodes and sometimes by using another module's logic), I would remove this method completely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The getNode
function only lookup node ID (defined at the top of the file). There are no risk of finding a node by name.
The issue is that the name of the function is the same as slicer.util.getNode
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK then. Still, I find all these set/get functions unnecessary, as the updateMRML… and updateGUI… methods take care of keeping the widget and MRML node in sync.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The widget was originally designed to reusable. But I agree this is overkill in that situation and truly reusable widget should be implemented in c++.
👍 for simplifying and removing the extra book keeping
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed variables in widget, the values are only stored in the ComboBoxes. And the 'upper' widget makes the connections to keep updated the parameterNode.
node = getNode(node) | ||
self._inputVolumeNode = node | ||
|
||
def componentToExtract(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need for get/set methods - widget is not supposed to be used from anywhere else
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The time invested here was motivated to integrate this functionality in a external module. So I do need to reuse the GUI. Kept the get method for convenience, but removed the set methods.
def cleanup(self): | ||
self.removeObservers() | ||
|
||
def setDefaultParameters(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be possible to create a parameter node (with default values) by only using the logic class, so default value initialization should be done in the logic class. See createParameterNode method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, moved to logic overriding the createParameterNode.
self.applyButton.enabled = all([self.inputVolumeNode(), self.outputVolumeNode(), self.parameterNode()]) | ||
if self.parameterNode is None: | ||
return | ||
self.setInputVolumeNode(self._parameterNode.GetParameter("InputVectorVolume")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
need to call blocksignals for each widget when modifying from parameter node to make sure updateMRMLFromGUI method is not called in the middle of updateGUIFromMRML.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the widget is setting the value of the parameter node to what it is already, no node modified event should be invoked.
As a precautionary measure, I suggest we introduce a context managed named like BlockedObjectSignal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't block anything, but created a context manager to block signals in a list of qobjects.
@staticmethod | ||
def isValidInputOutputData(inputVolumeNode, outputVolumeNode, conversionMethod, componentToExtract): | ||
""" | ||
Validate parameters using the parameterNode. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
document what is the return value, especially when it is not trivial (not just a simple Boolean)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
return True, None | ||
|
||
|
||
def run(self, inputVolumeNode, outputVolumeNode, conversionMethod, componentToExtract): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we use a parameter node then the primary method for running the processing should take a parameter node as input.
In addition to that method, you may add a convenience method, which takes, inputVolume, outputVolume, conversionMethod, etc., creates a parameter node and calls the primary method with that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
weightedSum = vtk.vtkImageWeightedSum() | ||
weights = vtk.vtkDoubleArray() | ||
weights.SetNumberOfValues(nrbOfComponents) | ||
evenWeight = 1.0/nrbOfComponents |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
numberOfComponents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
4319889
to
df63531
Compare
Thanks a lot @lassoan for the careful review, really appreciated, let me know what you think about my latest change. I think I addressed all your suggestions, except the blocking signals. I have tested it, and there is no recursive calling in either case, changing from MRML or from GUI. |
As a note, I really do need to reuse the GUI widget to justify the time spent on this. I have removed the internal variables, but I am sure it is still usable from external modules with a little bit of plumbing and connections. |
It all looks good! Thank you very much for taking the time to implement this so nicely. One last thing: I did not understand what XXX: meant. If it is a todo then rename to TODO: (if something else then rename to something that is more clear or more standard). |
I guess I influenced this. This is an other convention. See https://en.wikipedia.org/wiki/Comment_(computer_programming)#Tags |
df63531
to
20582f0
Compare
Changed XXX to TODO. |
There are hundreds of TODO tags in VTK and ITK, and maybe 4-5 XXX. I think it is better to use TODO if that code must be changed, or use plain comments if you just want to tell something to future developers. |
Thank you, I have no more comments! |
Thanks for all the work, LGTM! |
Reuse widget integrated in slicer core in: Slicer/Slicer#967 Fix Kitware#2
Reuse widget integrated in slicer core in: Slicer/Slicer#967 Fix Kitware#2
Reuse widget integrated in slicer core in: Slicer/Slicer#967 Fix Kitware#2
Reuse widget integrated in slicer core in: Slicer/Slicer#967 Fix Kitware#2
Add new method to compute the average of all the components of a VectorVolume. Also add parameterNode awareness, and general reformat that deserve a new commit. Co-authored-by: Jean-Christophe Fillion-Robin <[email protected]>
20582f0
to
9c40117
Compare
Reuse widget integrated in slicer core in: Slicer/Slicer#967 Fix Kitware#2
Reuse widget integrated in slicer core in: Slicer/Slicer#967 Fix Kitware#2
@phcerdan In the gif above, it looks like the input combobox is updated ? Is this expected ? |
In the external module, yes.
So, the last gif is this widget plugged into an external module.
The current state would be close to the other gif I uploaded here:
![peekslicervectortoscalarupdateaverage](https://user-images.githubusercontent.com/3021667/41502206-c8203a0e-7182-11e8-9f06-d9153ad481bc.gif)
|
Gotcha. Makes sense.
Will integrate later today
On Fri, Jun 29, 2018, 7:48 PM Pablo Hernandez-Cerdan <
[email protected]> wrote:
… In the extension, yes.
On Fri, Jun 29, 2018 at 2:36 PM, Jean-Christophe Fillion-Robin <
***@***.***> wrote:
> @phcerdan <https://github.com/phcerdan> In the gif above, it looks like
> the input combobox is updated ? Is this expected ?
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <Slicer/Slicer#967 (comment)>, or
mute
> the thread
> <
https://github.com/notifications/unsubscribe-auth/AC4bY-m2lfFP7ldzw1EXmef1rBcdQbkqks5uBnOagaJpZM4UfKZ-
>
> .
>
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<Slicer/Slicer#967 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AANXo0P0PLiUwIm1Vnz0RiHVPs9YVLedks5uBnaDgaJpZM4UfKZ->
.
|
Thank you! Integrated in 27278. |
Reuse widget integrated in slicer core in: Slicer/Slicer#967 Fix Kitware#2
This commit improves the user interface adding support for choosing
the conversion mode.
Available conversion modes are now:
compatibility with original module implementation)
To support reusing of components by other scripted modules, a logic
and widget have also been created:
Co-authored-by: Jean-Christophe Fillion-Robin [email protected]