Skip to content
Snippets Groups Projects
Commit 5486fb7b authored by Paul McCarthy's avatar Paul McCarthy
Browse files

Bugfix/workaround - due to the non-guaranteed ordering of property listener...

Bugfix/workaround - due to the non-guaranteed ordering of property listener callbacks, not all SliceCanvas objecst were updating their colour textures on changes to range clipping. You can now add a 'preNotifyFunc' to PropertyBase objects, which will be called as soon as a property value changes, before any listeners. A few other minor commentings and refactorings
parent 6914abc5
No related branches found
No related tags found
No related merge requests found
#!/usr/bin/env python #!/usr/bin/env python
# #
# fslimage.py - Object representing a 3D image. # fslimage.py - Classes representing a 3D image, and the display
# properties of a 3D image.
# #
# Author: Paul McCarthy <pauldmccarthy@gmail.com> # Author: Paul McCarthy <pauldmccarthy@gmail.com>
# #
...@@ -13,9 +14,17 @@ import matplotlib.colors as mplcolors ...@@ -13,9 +14,17 @@ import matplotlib.colors as mplcolors
import fsl.props as props import fsl.props as props
import fsl.data.imagefile as imagefile import fsl.data.imagefile as imagefile
class Image(object): class Image(object):
"""
Class which represents a 3D image. Interally, the image is loaded/stored
using nibabel.
"""
def __init__(self, image): def __init__(self, image):
"""
Initialise an Image object with the given image data or file name.
"""
# The image parameter may be the name of an image file # The image parameter may be the name of an image file
if isinstance(image, str): if isinstance(image, str):
...@@ -43,15 +52,30 @@ class Image(object): ...@@ -43,15 +52,30 @@ class Image(object):
self.zlen = zlen self.zlen = zlen
class ImageDisplay(props.HasProperties): class ImageDisplay(props.HasProperties):
""" """
A class which describes how an image should be displayed.
""" """
def updateColourMap(self, newVal):
"""
When a colour property changes, this method is called -
it reconfigures the colour map accordingly.
"""
if self.rangeClip:
self.cmap.set_under(self.cmap(0.0), alpha=0.0)
self.cmap.set_over( self.cmap(1.0), alpha=0.0)
else:
self.cmap.set_under(self.cmap(0.0), alpha=1.0)
self.cmap.set_over( self.cmap(1.0), alpha=1.0)
alpha = props.Double(minval=0.0, maxval=1.0, default=1.0) alpha = props.Double(minval=0.0, maxval=1.0, default=1.0)
displayMin = props.Double() displayMin = props.Double()
displayMax = props.Double() displayMax = props.Double()
rangeClip = props.Boolean(default=False) rangeClip = props.Boolean(default=False,
preNotifyFunc=updateColourMap)
_view = props.VGroup(('displayMin', 'displayMax', 'alpha', 'rangeClip')) _view = props.VGroup(('displayMin', 'displayMax', 'alpha', 'rangeClip'))
_labels = { _labels = {
...@@ -63,6 +87,10 @@ class ImageDisplay(props.HasProperties): ...@@ -63,6 +87,10 @@ class ImageDisplay(props.HasProperties):
def __init__(self, image): def __init__(self, image):
"""
Create an ImageDisplay for the specified image. The image
parameter should be an Image object (defined above).
"""
self.image = image self.image = image
...@@ -73,18 +101,3 @@ class ImageDisplay(props.HasProperties): ...@@ -73,18 +101,3 @@ class ImageDisplay(props.HasProperties):
self.dataMax = self.image.data.max() self.dataMax = self.image.data.max()
self.displayMin = self.dataMin # use cal_min/cal_max instead? self.displayMin = self.dataMin # use cal_min/cal_max instead?
self.displayMax = self.dataMax self.displayMax = self.dataMax
self.addListener(
'rangeClip',
'ImageRangeClip_{}'.format(id(self)),
self.updateColourMap)
def updateColourMap(self, *a):
if self.rangeClip:
self.cmap.set_under(self.cmap(0.0), alpha=0.0)
self.cmap.set_over( self.cmap(1.0), alpha=0.0)
else:
self.cmap.set_under(self.cmap(0.0), alpha=1.0)
self.cmap.set_over( self.cmap(1.0), alpha=1.0)
...@@ -128,6 +128,8 @@ ...@@ -128,6 +128,8 @@
import types import types
import logging import logging
from collections import OrderedDict
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -149,7 +151,6 @@ class PropertyValue(object): ...@@ -149,7 +151,6 @@ class PropertyValue(object):
- value: Initial value. - value: Initial value.
- name: Variable name - if not provided, a default, - name: Variable name - if not provided, a default,
unique name is created. unique name is created.
""" """
if name is None: name = '{}_{}'.format(prop.label, id(self)) if name is None: name = '{}_{}'.format(prop.label, id(self))
...@@ -157,7 +158,7 @@ class PropertyValue(object): ...@@ -157,7 +158,7 @@ class PropertyValue(object):
self.prop = prop self.prop = prop
self.owner = owner self.owner = owner
self.name = name self.name = name
self.changeListeners = {} self.changeListeners = OrderedDict()
self._value = value self._value = value
self._lastValue = value self._lastValue = value
self._lastValid = self.isValid() self._lastValid = self.isValid()
...@@ -219,6 +220,11 @@ class PropertyValue(object): ...@@ -219,6 +220,11 @@ class PropertyValue(object):
log.debug('Variable {} changed: {}'.format(self.name, newValue)) log.debug('Variable {} changed: {}'.format(self.name, newValue))
# Call the PropertyBase prenotify function first, if there is one
if self.prop.preNotifyFunc is not None:
log.debug('Calling preNotify function for {}'.format(self.name))
self.prop.preNotifyFunc(self.owner, newValue)
# Validate the new value and notify any registered listeners # Validate the new value and notify any registered listeners
self.validateAndNotify() self.validateAndNotify()
...@@ -302,30 +308,40 @@ class PropertyBase(object): ...@@ -302,30 +308,40 @@ class PropertyBase(object):
""" """
def __init__(self, default=None, required=False, validateFunc=None): def __init__(self,
default=None,
required=False,
validateFunc=None,
preNotifyFunc=None):
""" """
Parameters: Parameters:
- default: Default/initial value. - default: Default/initial value.
- required: Boolean determining whether or not this - required: Boolean determining whether or not this
property must have a value. May alternately property must have a value. May alternately
be a function which accepts one parameter, be a function which accepts one parameter,
the owning HasProperties instance, and the owning HasProperties instance, and
returns True or False. returns True or False.
- validateFunc: Custom validation function. Must accept - validateFunc: Custom validation function. Must accept
two parameters: a reference to the two parameters: a reference to the
HasProperties instance, the owner of HasProperties instance, the owner of
this property; and the new property this property; and the new property
value. Should return True if the property value. Should return True if the property
value is valid, False otherwise. value is valid, False otherwise.
- preNotifyFunc: Function to be called whenever the property
value(s) changes. This function is called
by the PropertyValue object(s) before any
listeners are notified.
""" """
self.label = None self.label = None
self.default = default self.default = default
self.required = required self.required = required
self.validateFunc = validateFunc self.validateFunc = validateFunc
self.changeListeners = {} self.preNotifyFunc = preNotifyFunc
self.changeListeners = OrderedDict()
def addListener(self, instance, name, callback): def addListener(self, instance, name, callback):
......
...@@ -197,7 +197,9 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -197,7 +197,9 @@ class SliceCanvas(wxgl.GLCanvas):
parent - WX parent object parent - WX parent object
image - A fsl.data.image.Image object, or a 3D numpy array. image - A fsl.data.fslimage.Image object, or a
fsl.data.fslimage.ImageDisplay object, or a 3D numpy
array.
zax - Axis perpendicular to the plane to be displayed zax - Axis perpendicular to the plane to be displayed
(the 'depth' axis), default 0. (the 'depth' axis), default 0.
...@@ -272,34 +274,34 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -272,34 +274,34 @@ class SliceCanvas(wxgl.GLCanvas):
self.Bind(wx.EVT_PAINT, self.draw) self.Bind(wx.EVT_PAINT, self.draw)
# Add a bunch of listeners to the image display
# object, so we can update the view when image
# display properties are changed.
def refreshNeeded(*a): def refreshNeeded(*a):
self.Refresh() self.Refresh()
def colourUpdateNeeded(*a): def colourUpdateNeeded(*a):
self.updateColourBuffer() self.updateColourBuffer()
self.Refresh() self.Refresh()
lnrName = 'SliceCanvas_{{}}_{}'.format(id(self))
self.imageDisplay.addListener( self.imageDisplay.addListener(
'alpha', 'alpha', lnrName.format('alpha'), refreshNeeded)
'SliceCanvasAlpha_{}'.format(id(self)),
refreshNeeded)
self.imageDisplay.addListener( self.imageDisplay.addListener(
'displayMin', 'displayMin', lnrName.format('displayMin'), colourUpdateNeeded)
'SliceCanvasDisplayMin_{}'.format(id(self)),
colourUpdateNeeded)
self.imageDisplay.addListener( self.imageDisplay.addListener(
'displayMax', 'displayMax', lnrName.format('displayMax'), colourUpdateNeeded)
'SliceCanvasDisplayMax_{}'.format(id(self)),
colourUpdateNeeded)
self.imageDisplay.addListener( self.imageDisplay.addListener(
'rangeClip', 'rangeClip', lnrName.format('rangeClip'), colourUpdateNeeded)
'SliceCanvasRangeClip_{}'.format(id(self)),
colourUpdateNeeded)
def _initGLData(self): def _initGLData(self):
""" """
Initialises the GL buffers which are copied to the GPU, Initialises the GL buffers which are copied to the GPU,
and used to render the voxel data. and used to render the voxel data.
""" """
...@@ -319,7 +321,7 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -319,7 +321,7 @@ class SliceCanvas(wxgl.GLCanvas):
shaders.compileShader(vertex_shader, gl.GL_VERTEX_SHADER), shaders.compileShader(vertex_shader, gl.GL_VERTEX_SHADER),
shaders.compileShader(fragment_shader, gl.GL_FRAGMENT_SHADER)) shaders.compileShader(fragment_shader, gl.GL_FRAGMENT_SHADER))
# Indexes of all vertex/fragment shader parameters # Indices of all vertex/fragment shader parameters
self.inVertexPos = gl.glGetAttribLocation( self.shaders, 'inVertex') self.inVertexPos = gl.glGetAttribLocation( self.shaders, 'inVertex')
self.voxelValuePos = gl.glGetAttribLocation( self.shaders, 'voxelValue') self.voxelValuePos = gl.glGetAttribLocation( self.shaders, 'voxelValue')
self.inPositionPos = gl.glGetAttribLocation( self.shaders, 'inPosition') self.inPositionPos = gl.glGetAttribLocation( self.shaders, 'inPosition')
...@@ -391,16 +393,25 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -391,16 +393,25 @@ class SliceCanvas(wxgl.GLCanvas):
iDisplay = self.imageDisplay iDisplay = self.imageDisplay
# Here we are creating a range of values to be passed
# to the matplotlib.colors.Colormap instance of the
# image display. We scale this range such that data
# values which lie outside the configured display range
# will map to values below 0.0 or above 1.0. It is
# assumed that the Colormap instance is configured to
# generate appropriate colours for these out-of-range
# values.
normalRange = np.linspace(0.0, 1.0, self.colourResolution)
normalStep = 1.0 / (self.colourResolution - 1)
normMin = (iDisplay.displayMin - iDisplay.dataMin) / \ normMin = (iDisplay.displayMin - iDisplay.dataMin) / \
(iDisplay.dataMax - iDisplay.dataMin) (iDisplay.dataMax - iDisplay.dataMin)
normMax = (iDisplay.displayMax - iDisplay.dataMin) / \ normMax = (iDisplay.displayMax - iDisplay.dataMin) / \
(iDisplay.dataMax - iDisplay.dataMin) (iDisplay.dataMax - iDisplay.dataMin)
normalStep = 1.0 / (self.colourResolution - 1) newStep = normalStep / (normMax - normMin)
newStep = normalStep / (normMax - normMin) newRange = (normalRange - normMin) * (newStep / normalStep)
normalRange = np.linspace(0.0, 1.0, self.colourResolution)
newRange = (normalRange - normMin) * (newStep / normalStep)
# Create [self.colourResolution] rgb values, # Create [self.colourResolution] rgb values,
# spanning the entire range of the image # spanning the entire range of the image
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment