From 41c22187ef656da11bde8ba4f3992208bf16285f Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Thu, 26 Mar 2015 12:25:54 +0000 Subject: [PATCH] VolumeOpts.displayRange and Display.bricon properties are synchronsied, but at the cost of not being able to disconnect them across different views. Hopefully will fix this in the future. --- fsl/fslview/displaycontext/display.py | 16 +++- fsl/fslview/displaycontext/volumeopts.py | 103 ++++++++++++++++++++--- fsl/fslview/layouts.py | 1 + 3 files changed, 104 insertions(+), 16 deletions(-) diff --git a/fsl/fslview/displaycontext/display.py b/fsl/fslview/displaycontext/display.py index 95f830122..ba4e18cee 100644 --- a/fsl/fslview/displaycontext/display.py +++ b/fsl/fslview/displaycontext/display.py @@ -31,8 +31,18 @@ log = logging.getLogger(__name__) class DisplayOpts(props.SyncableHasProperties): - def __init__(self, image, display, imageList, displayCtx, parent=None): - props.SyncableHasProperties.__init__(self, parent) + def __init__( + self, + image, + display, + imageList, + displayCtx, + parent=None, + *args, + **kwargs): + + props.SyncableHasProperties.__init__(self, parent, *args, **kwargs) + self.image = image self.display = display self.imageList = imageList @@ -171,6 +181,8 @@ class Display(props.SyncableHasProperties): 'volume', 'resolution', 'transform', + 'brightness', + 'contrast', 'imageType']) # Set up listeners after caling Syncabole.__init__, diff --git a/fsl/fslview/displaycontext/volumeopts.py b/fsl/fslview/displaycontext/volumeopts.py index 9939d3000..2a981c930 100644 --- a/fsl/fslview/displaycontext/volumeopts.py +++ b/fsl/fslview/displaycontext/volumeopts.py @@ -114,17 +114,23 @@ class VolumeOpts(fsldisplay.DisplayOpts): dRangeLen = abs(self.dataMax - self.dataMin) dMinDistance = dRangeLen / 10000.0 - self.clippingRange.setLimits(0, - self.dataMin - dMinDistance, - self.dataMax + dMinDistance) - + self.clippingRange.xmin = self.dataMin - dMinDistance + self.clippingRange.xmax = self.dataMax + dMinDistance + # By default, the lowest values # in the image are clipped - self.clippingRange.setRange( 0, - self.dataMin + dMinDistance, - self.dataMax + dMinDistance) + self.clippingRange.xlo = self.dataMin + dMinDistance + self.clippingRange.xhi = self.dataMax + dMinDistance + + self.displayRange.xlo = self.dataMin + self.displayRange.xhi = self.dataMax - self.displayRange.setRange(0, self.dataMin, self.dataMax) + # The Display.contrast property expands/contracts + # the display range, by a scaling factor up to + # approximately 10. + self.displayRange.xmin = self.dataMin - 10 * dRangeLen + self.displayRange.xmax = self.dataMax + 10 * dRangeLen + self.setConstraint('displayRange', 'minDistance', dMinDistance) fsldisplay.DisplayOpts.__init__(self, @@ -132,10 +138,35 @@ class VolumeOpts(fsldisplay.DisplayOpts): display, imageList, displayCtx, - parent) - - display.addListener('brightness', self.name, self.briconChanged) - display.addListener('contrast', self.name, self.briconChanged) + parent, + nounbind=('displayRange')) + + # Bricon values are synchronised with + # displayRange values on the parent + # VolumeOpts instance. If these listeners + # are registered on child instances, and + # there are multiple children, horrible + # semi-infniite recursive listener callback + # madness will entail ... + # + # TODO Problem here is that if a child + # instance disables synchronisation + # on brightness/contrast with the + # parent, the bricon-displayRange + # synchronsiation no longer occurs + # on the child .. This is why + # displayRange currently cannot be + # unbound between parent/child + # instances (constructor above). + # Same goes for Display.bricon + # properties + + if parent is None: + display.addListener('brightness', self.name, self.briconChanged) + display.addListener('contrast', self.name, self.briconChanged) + self .addListener('displayRange', + self.name, + self.displayRangeChanged) def briconChanged(self, *a): @@ -145,6 +176,8 @@ class VolumeOpts(fsldisplay.DisplayOpts): Updates the :attr:`displayRange` property accordingly. """ + display = self.display + # Turn the bricon percentages into # values between 1 and 0 (inverted) brightness = 1 - self.display.brightness / 100.0 @@ -168,11 +201,53 @@ class VolumeOpts(fsldisplay.DisplayOpts): # an effect than higher values (closer to 1.0). if contrast > 0.5: scale += np.exp((contrast - 0.5) * 6) - 1 - + # Calculate the new display range, keeping it # centered in the middle of the data range # (but offset according to the brightness) dlo = (dmid + offset) - 0.5 * drange * scale dhi = (dmid + offset) + 0.5 * drange * scale - self.displayRange.setRange(0, dlo, dhi) + self .disableListener('displayRange', self.name) + display.disableListener('brightness', self.name) + display.disableListener('contrast', self.name) + + self.displayRange.x = [dlo, dhi] + + self .enableListener('displayRange', self.name) + display.enableListener('brightness', self.name) + display.enableListener('contrast', self.name) + + + def displayRangeChanged(self, *a): + + display = self.display + + dmin, dmax = self.dataMin, self.dataMax + drange = dmax - dmin + dmid = dmin + 0.5 * drange + + dlo, dhi = self.displayRange.x + + # Inversions of the equations in briconChanged + # above, which calculate the display ranges + # from the bricon offset/scale + offset = dlo + 0.5 * (dhi - dlo) - dmid + scale = (dhi - dlo) / drange + + brightness = 0.5 * (offset / drange + 1) + + if scale <= 1: contrast = scale / 2.0 + else: contrast = np.log(scale + 1) / 6.0 + 0.5 + + self .disableListener('displayRange', self.name) + display.disableListener('brightness', self.name) + display.disableListener('contrast', self.name) + + # update bricon + display.brightness = 100 - brightness * 100 + display.contrast = 100 - contrast * 100 + + self .enableListener('displayRange', self.name) + display.enableListener('brightness', self.name) + display.enableListener('contrast', self.name) diff --git a/fsl/fslview/layouts.py b/fsl/fslview/layouts.py index f64fc64bc..7a97c2a0b 100644 --- a/fsl/fslview/layouts.py +++ b/fsl/fslview/layouts.py @@ -182,6 +182,7 @@ DisplayLayout = props.VGroup( VolumeOptsLayout = props.VGroup( (widget(VolumeOpts, 'cmap'), widget(VolumeOpts, 'invert'), + widget(VolumeOpts, 'displayRange', slider=True), widget(VolumeOpts, 'clippingRange', slider=True))) -- GitLab