diff --git a/TODO b/TODO index 7674d753e8637a3a27e03f51c94e9dc87f4feb67..7796c8ed3074c7a116120b4168bb1e76125c85e0 100644 --- a/TODO +++ b/TODO @@ -82,17 +82,18 @@ Also, redo the way that global HasProps validation works - it is currently perfo ** PyInstaller menu bar not showing http://dvitonis.net/blog/2015/01/07/menu-bar-not-visible-when-building-pyqt-app-bundle-pyinstaller-mac-osx-mavericks-yosemite/ * Bugs to fix -** VolumeOpts DisplayRange<->Bricon synchronisation. -Breaks when brightness/contrast/displayRange sync (between multiple displays) -is turned off .. I think the sync logic has to occur somewhere other than the -VolumeOpts class ... +** Clipping range not working for some volumes in ~/analysis_prac_2015/rest/ICA/Group/groupmelodic_fix.ica/melodic_IC.nii.gz ** LightBox - grid lines are drawn below canvas area where slices are drawn ** LocationPanel in lightbox view - voxel value lookup is wrong? ** Lightbox scrollbar under linux not visible? - ** WONTFIX OSMesa render doesn't work with circle voxels Circle voxels were just an experiment anyway. Removed from code ** WONTFIX World location is clamped to voxel centre when voxel location changes. Not sure if this really needs to be fixed. +** DONE Spline intrpolation doing weird things +** DONE VolumeOpts DisplayRange<->Bricon synchronisation. +Breaks when brightness/contrast/displayRange sync (between multiple displays) +is turned off .. I think the sync logic has to occur somewhere other than the +VolumeOpts class ... ** DONE SpacePanel throwing errors on av.nii.gz (and seemingly not deregistering listeners) ** DONE Image order synchronisation ** DONE Ortho canvas resizing @@ -160,6 +161,15 @@ perhaps into a standalone module... ** DONE Graceful handling of bad input filenames ** DONE Aspect ratio on slicecanvas zoom, and panning is broken. * Little things +** Arrow keys on number widgets +** Buffer sharing +A gl/buffers.py module which checks for buffer capability. If buffer-capable, +vertex data is copied to a buffer, and the buffer name/ID returned. Otherwise, +the vertex data is returned unmodified. +** Absolute clipping (e.g. clip [-3, 3]) +** Make zoom more flexible +** Make ortho aspect ratio/zoom thing better +** RunWindow should print to stdout ** Startup - ability to set $FSLDIR ** Time Series - option to 'hold' voxel time course You could have a list panel which lists the image and voxel coordinates of the @@ -372,3 +382,61 @@ on the image display object. ** (Ged). First/third angle orthographic projection. ** (Paul). Image groups - may make life easier for grouping images which belong together for analysis purposes. ** (Paul). An integrated python shell with images available in-memory to manipulate. + +* February/March 2015 internal release +** DONE Installation instructions on wiki +*** Make sure they're up to date - test in a VM +*** Usage - keyboard shortcuts +** DONE OSX installer +*** Make sure you disable GL error checking/logging +*** Test screenshots +*** Test GL14 and GL21 (shaders) +*** Test two-stage rendering +*** Test installing colour maps +*** Test saving edited images +*** Test atlas tools (this will only be available when it is run from command line) + +** DONE Linux compatibility +*** DONE Combobox drop down lists in dialogs are shown beneath the dialog?!? This may only be occurring under X11/SSH/OSX +*** WONTFIX Toolbars not being resized appropriately when the parent viewpanel is resized? +Can't reproduce right now .. +** WONTFIX Ludoweird interpolation effect - wtf? +It's an artifact of the fact that interpolation is applied in voxel space, rather than mm space. +** DONE Fix all 'listener still registered' warnings. +** DONE Progress dialog during screen shot +** DONE VNC/X11 ortho panning +** DONE Change 'profile' to 'mode' +** DONE Volume Option to invert colour map +** DONE Make ImageDisplayPanel change interpolation when transformation is changed +** DONE Disable image display widgets for disabled images +** DONE Rename 'sync image order' -> 'sync overlay order' +** DONE Remove 'sync volume' option +** DONE -ISH Make VNC two stage render approach stablish +** DONE Add 'blue-lightblue' colour map +** DONE Rename 'autumn' to 'red-yellow' (change colour map) +** DONE Colour map ordering +** DONE Change 'ss' back to 'twostage' +** DONE Command line +** DONE? Little things +*** DONE ViewPanel: New toolbars in a lower layer +*** DONE Number spinboxes are no longer clamped +*** Keyboard on number widgets .. Should be working by default? +** DONE Default layout +** DONE UI design +Good enough for the time being +** DONE Support for double precision images +** DONE Histogram +** DONE Atlas tools +** DONE Keyboard shortcut for pan mode - can't use middle click when using a shitty laptop trackpad +** DONE Git release pipeline +We now have an 'oxford' branch, which is linked to the jalapeno installation. +When you want to 'release' something, merge from the master branch to the +oxford branch, and push to jalapeno + +** DONE Offscreen rendering +** Other things/stretch objectives: +*** Tooltips +*** Save/restore window layout +*** Movie mode +*** Document all the code +*** Fix all the bugs diff --git a/fsl/data/image.py b/fsl/data/image.py index e2af8913c643ed3714d5ae9b654f50f15f0f6c63..e757d020deb6a2f6d83fc2ca8c06bdba0b17bb12 100644 --- a/fsl/data/image.py +++ b/fsl/data/image.py @@ -67,9 +67,10 @@ class Image(props.HasProperties): imageType = props.Choice( collections.OrderedDict([ - ('volume', '3D/4D volume'), - ('mask', '3D/4D mask image'), - ('vector', '3-direction vector image')]), + ('volume', '3D/4D volume'), + ('mask', '3D/4D mask image'), + ('rgbvector', '3-direction vector image (RGB)'), + ('linevector', '3-direction vector image (Line)')]), default='volume') """This property defines the type of image data.""" @@ -211,8 +212,8 @@ class Image(props.HasProperties): shape = data.shape for i in reversed(range(len(shape))): - if shape[i - 1] == 1: - data = data.squeeze(axis=i - 1) + if shape[i] == 1: data = data.squeeze(axis=i) + else: break data.flags.writeable = False diff --git a/fsl/data/strings.py b/fsl/data/strings.py index 6a25de1e7eb7c4a0900800d85c687a42e6bff66b..187056ab5d7bbca42f2d181360b41a3382f9d461 100644 --- a/fsl/data/strings.py +++ b/fsl/data/strings.py @@ -157,7 +157,7 @@ properties = TypeDict({ 'SceneOpts.showCursor' : 'Show location cursor', 'SceneOpts.showColourBar' : 'Show colour bar', - 'SceneOpts.twoStageRender' : 'Two-stage rendering', + 'SceneOpts.performance' : 'Rendering performance', 'SceneOpts.zoom' : 'Zoom', 'SceneOpts.colourBarLocation' : 'Colour bar location', 'SceneOpts.colourBarLabelSide' : 'Colour bar label side', @@ -212,7 +212,6 @@ properties = TypeDict({ 'MaskOpts.invert' : 'Invert', 'MaskOpts.threshold' : 'Threshold', - 'VectorOpts.displayMode' : 'Display mode', 'VectorOpts.xColour' : 'X Colour', 'VectorOpts.yColour' : 'Y Colour', 'VectorOpts.zColour' : 'Z Colour', @@ -222,6 +221,9 @@ properties = TypeDict({ 'VectorOpts.suppressZ' : 'Suppress Z value', 'VectorOpts.modulate' : 'Modulate by', 'VectorOpts.modThreshold' : 'Modulation threshold', + + 'LineVectorOpts.directed' : 'Interpret vectors as directed', + 'LineVectorOpts.lineWidth' : 'Line width', }) @@ -257,6 +259,12 @@ choices = TypeDict({ 'SceneOpts.colourBarLocation.left' : 'Left', 'SceneOpts.colourBarLocation.right' : 'Right', + 'SceneOpts.performance.1' : 'Fastest', + 'SceneOpts.performance.2' : 'Faster', + 'SceneOpts.performance.3' : 'Good looking', + 'SceneOpts.performance.4' : 'Better looking', + 'SceneOpts.performance.5' : 'Best looking', + 'HistogramPanel.dataRange.min' : 'Min.', 'HistogramPanel.dataRange.max' : 'Max.', diff --git a/fsl/fslview/colourmaps.py b/fsl/fslview/colourmaps.py index de39a83b869ba25540f8c024d9e1ec7bea60c1bc..d69dca402d452923d91e6079c569c04191db38c5 100644 --- a/fsl/fslview/colourmaps.py +++ b/fsl/fslview/colourmaps.py @@ -39,6 +39,16 @@ This module provides a number of functions, the most important of which are: loads the data and registers it with :mod:`matplotlib`. + +Some utility functions are also kept in this module, related to calculating +the relationship between a data display range, and brightness/contrast +scales: + + - :func:`displayRangeToBricon`: Given a data range, converts a display range + to brightness/contrast values. + + - :func:`briconToDisplayRange`: Given a data range, converts brigtness/ + contrast values to a display range. """ import glob @@ -238,3 +248,81 @@ def initColourMaps(): except: log.warn('Error processing custom colour ' 'map file: {}'.format(cmapFile)) + + +def briconToDisplayRange(dataRange, brightness, contrast): + """Converts the given brightness/contrast values to a display range, + given the data range. + + :arg dataRange: The full range of the data being displayed, a + (min, max) tuple. + + :arg brightness: A brightness value between 0 and 1. + + :arg contrast: A contrast value between 0 and 1. + """ + + # Turn the given bricon values into + # values between 1 and 0 (inverted) + brightness = 1.0 - brightness + contrast = 1.0 - contrast + + dmin, dmax = dataRange + drange = dmax - dmin + dmid = dmin + 0.5 * drange + + # The brightness is applied as a linear offset, + # with 0.5 equivalent to an offset of 0.0. + offset = (brightness * 2 - 1) * drange + + # If the contrast lies between 0.0 and 0.5, it is + # applied to the colour as a linear scaling factor. + scale = contrast * 2 + + # If the contrast lies between 0.5 and 1, it + # is applied as an exponential scaling factor, + # so lower values (closer to 0.5) have less of + # 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 + + return dlo, dhi + + +def displayRangeToBricon(dataRange, displayRange): + """Converts the given brightness/contrast values to a display range, + given the data range. + + :arg dataRange: The full range of the data being displayed, a + (min, max) tuple. + + :arg displayRange: A (min, max) tuple containing the display range. + """ + + dmin, dmax = dataRange + dlo, dhi = displayRange + drange = dmax - dmin + dmid = dmin + 0.5 * drange + + # These are inversions of the equations in + # the briconToDisplayRange function 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 + + brightness = 1.0 - brightness + contrast = 1.0 - contrast + + return brightness, contrast diff --git a/fsl/fslview/displaycontext/display.py b/fsl/fslview/displaycontext/display.py index 81367cac7057a5b92e236da03a209902c925caf5..80b63f148e93a93976d228b077a97c97ead4eb83 100644 --- a/fsl/fslview/displaycontext/display.py +++ b/fsl/fslview/displaycontext/display.py @@ -125,6 +125,10 @@ class Display(props.SyncableHasProperties): contrast = props.Percentage() + + softwareMode = props.Boolean(default=False) + """If possible, optimise for software-based rendering.""" + def is4DImage(self): """Returns ``True`` if this image is 4D, ``False`` otherwise. @@ -185,8 +189,7 @@ class Display(props.SyncableHasProperties): 'volume', 'resolution', 'transform', - 'brightness', - 'contrast', + 'softwareMode', 'imageType']) # Set up listeners after caling Syncabole.__init__, @@ -350,9 +353,10 @@ class Display(props.SyncableHasProperties): oParent = self.getParent().getDisplayOpts() optsMap = { - 'volume' : volumeopts.VolumeOpts, - 'vector' : vectoropts.VectorOpts, - 'mask' : maskopts. MaskOpts + 'volume' : volumeopts.VolumeOpts, + 'rgbvector' : vectoropts.VectorOpts, + 'linevector' : vectoropts.LineVectorOpts, + 'mask' : maskopts. MaskOpts } optType = optsMap[self.imageType] diff --git a/fsl/fslview/displaycontext/sceneopts.py b/fsl/fslview/displaycontext/sceneopts.py index d5191df213b2974f6b21d52e964f5aaff0bfd6e6..7e725c8cb258de603009caf2294a30bea2a2d953 100644 --- a/fsl/fslview/displaycontext/sceneopts.py +++ b/fsl/fslview/displaycontext/sceneopts.py @@ -9,18 +9,26 @@ import copy import props +import fsl.fslview.gl.slicecanvas as slicecanvas import fsl.fslview.gl.colourbarcanvas as colourbarcanvas import fsl.data.strings as strings class SceneOpts(props.HasProperties): + """The ``SceneOpts`` class defines settings which are applied to + :class:`.CanvasPanel` views. + """ + showCursor = props.Boolean(default=True) + zoom = props.Percentage(minval=10, maxval=1000, default=100, clamped=True) + showColourBar = props.Boolean(default=False) + colourBarLocation = props.Choice( ('top', 'bottom', 'left', 'right'), labels=[strings.choices['SceneOpts.colourBarLocation.top'], @@ -31,9 +39,92 @@ class SceneOpts(props.HasProperties): colourBarLabelSide = copy.copy(colourbarcanvas.ColourBarCanvas.labelSide) - twoStageRender = props.Boolean(default=False) + + performance = props.Choice( + (1, 2, 3, 4, 5), + default=5, + labels=[strings.choices['SceneOpts.performance.1'], + strings.choices['SceneOpts.performance.2'], + strings.choices['SceneOpts.performance.3'], + strings.choices['SceneOpts.performance.4'], + strings.choices['SceneOpts.performance.5']]) + """User controllable performacne setting. + + This property is linked to the :attr:`twoStageRender`, + :attr:`resolutionLimit`, and :attr:`softwareMode` properties. Setting the + performance to a low value will result in faster rendering time, at the + cost of reduced features, and poorer rendering quality. + + See the :meth:`_onPerformanceChange` method. + """ + + + resolutionLimit = copy.copy(slicecanvas.SliceCanvas.resolutionLimit) + """The highest resolution at which any image should be displayed. + + See :attr:`~fsl.fslview.gl.slicecanvas.SliceCanvas.resolutionLimit` and + :attr:`~fsl.fslview.displaycontext.display.Display.resolution`. + """ + + + renderMode = copy.copy(slicecanvas.SliceCanvas.renderMode) """Enable two-stage rendering, useful for low-performance graphics cards/ software rendering. See :attr:`~fsl.fslview.gl.slicecanvas.SliceCanvas.twoStageRender`. """ + + + + softwareMode = copy.copy(slicecanvas.SliceCanvas.softwareMode) + """If ``True``, all images should be displayed in a mode optimised for + software based rendering. + + The definition of 'software mode' is intentionally left unspecified, but + will generally mean using GL vertex/fragment shaders which are optimised + for speed, possibly at the cost of omitting some features. + + See :attr:`.SliceCanvas.softwareMode` and :attr:`.Display.softwareMode`. + """ + + + def __init__(self): + + name = '{}_{}'.format(type(self).__name__, id(self)) + self.addListener('performance', name, self._onPerformanceChange) + + self._onPerformanceChange() + + + def _onPerformanceChange(self, *a): + """Called when the :attr:`performance` property changes. + + Changes the values of the :attr:`renderMode`, :attr:`softwareMode` + and :attr:`resolutionLimit` properties accoridng to the performance + setting. + """ + + if self.performance == 5: + self.renderMode = 'onscreen' + self.softwareMode = False + self.resolutionLimit = 0 + + elif self.performance == 4: + self.renderMode = 'onscreen' + self.softwareMode = True + self.resolutionLimit = 0 + + elif self.performance == 3: + self.renderMode = 'offscreen' + self.softwareMode = True + self.resolutionLimit = 0 + + elif self.performance == 2: + self.renderMode = 'prerender' + self.softwareMode = True + self.resolutionLimit = 0 + + elif self.performance == 1: + self.renderMode = 'prerender' + self.softwareMode = True + self.resolutionLimit = 1 diff --git a/fsl/fslview/displaycontext/vectoropts.py b/fsl/fslview/displaycontext/vectoropts.py index 47cc6e1b779eb2842c4b6f6b30e7b80064df795c..4fb40e7a588831dc2b3e17aa3542c0827fd58ea3 100644 --- a/fsl/fslview/displaycontext/vectoropts.py +++ b/fsl/fslview/displaycontext/vectoropts.py @@ -16,13 +16,6 @@ import display as fsldisplay class VectorOpts(fsldisplay.DisplayOpts): - displayMode = props.Choice( - ('line', 'rgb'), - default='rgb', - labels=[strings.choices['VectorOpts.displayType.line'], - strings.choices['VectorOpts.displayType.rgb']]) - """Mode in which the ``GLVector`` instance is to be displayed.""" - xColour = props.Colour(default=(1.0, 0.0, 0.0)) """Colour used to represent the X vector magnitude.""" @@ -59,7 +52,14 @@ class VectorOpts(fsldisplay.DisplayOpts): """Hide voxels for which the modulation value is below this threshold.""" - def __init__(self, image, display, imageList, displayCtx, parent=None): + def __init__(self, + image, + display, + imageList, + displayCtx, + parent=None, + *args, + **kwargs): """Create a ``VectorOpts`` instance for the given image. See the :class:`~fsl.fslview.displaycontext.display.DisplayOpts` @@ -70,7 +70,9 @@ class VectorOpts(fsldisplay.DisplayOpts): display, imageList, displayCtx, - parent) + parent, + *args, + **kwargs) imageList.addListener('images', self.name, self.imageListChanged) self.imageListChanged() @@ -120,3 +122,24 @@ class VectorOpts(fsldisplay.DisplayOpts): if modVal in images: self.modulate = modVal else: self.modulate = 'none' + + +# TODO RGBVector/LineVector subclasses for any type +# specific options (e.g. line width for linevector) + +class LineVectorOpts(VectorOpts): + + lineWidth = props.Int(minval=1, maxval=10, default=1) + + directed = props.Boolean(default=False) + """ + + The directed property cannot be unbound across multiple LineVectorOpts + instances, as it affects the OpenGL representation + """ + + def __init__(self, *args, **kwargs): + + kwargs['nounbind'] = ['directed'] + + VectorOpts.__init__(self, *args, **kwargs) diff --git a/fsl/fslview/displaycontext/volumeopts.py b/fsl/fslview/displaycontext/volumeopts.py index 9b0a1d0db9d9130dacd91f4bc48f7de7d2e6a5cb..91b24539f38425fd88418b87eee2c54be6994b32 100644 --- a/fsl/fslview/displaycontext/volumeopts.py +++ b/fsl/fslview/displaycontext/volumeopts.py @@ -139,121 +139,127 @@ class VolumeOpts(fsldisplay.DisplayOpts): display, imageList, displayCtx, - 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: + parent) + + # The displayRange property of every child VolumeOpts + # instance is linked to the corresponding + # Display.brightness/contrast properties, so changes + # in one are reflected in the other. + if parent is not None: display.addListener('brightness', self.name, self.briconChanged) display.addListener('contrast', self.name, self.briconChanged) self .addListener('displayRange', self.name, self.displayRangeChanged) + # Because displayRange and bri/con are intrinsically + # linked, it makes no sense to let the user sync/unsync + # them independently. So here we are binding the boolean + # sync properties which control whether the dRange/bricon + # properties are synced with their parent. So when one + # property is synced/unsynced, the other ones are too. + self.bindProps(self .getSyncPropertyName('displayRange'), + display, + display.getSyncPropertyName('brightness')) + self.bindProps(self .getSyncPropertyName('displayRange'), + display, + display.getSyncPropertyName('contrast')) + def destroy(self): - self.display.removeListener('brightness', self.name) - self.display.removeListener('contrast', self.name) - self .removeListener('displayRange', self.name) + if self.getParent() is not None: + display = self.display + display.removeListener('brightness', self.name) + display.removeListener('contrast', self.name) + self .removeListener('displayRange', self.name) + self.unbindProps(self .getSyncPropertyName('displayRange'), + display, + display.getSyncPropertyName('brightness')) + self.unbindProps(self .getSyncPropertyName('displayRange'), + display, + display.getSyncPropertyName('contrast')) + + + def __toggleListeners(self, enable=True): + """This method enables/disables the property listeners which + are registered on the :attr:`displayRange` and + :attr:`.Display.brightness`/:attr:`.Display.contrast`/ properties. + + Because these properties are linked via the :meth:`displayRangeChanged` + and :meth:`briconChanged` methods, we need to be careful about avoiding + recursive callbacks. + + Furthermore, because the properties of both :class:`VolumeOpts` and + :class:`.Display` instances are possibly synchronised to a parent + instance (which in turn is synchronised to other children), we need to + make sure that the property listeners on these other sibling instances + are not called when our own property values change. So this method + disables/enables the property listeners on all sibling ``VolumeOpts`` + and ``Display`` instances. + """ + + parent = self.getParent() + + # this is the parent instance + if parent is None: + return + + # The parent.getChildren() method will + # contain this VolumeOpts instance, + # so the below loop toggles listeners + # for this instance, the parent instance, + # and all of the other children of the + # parent + peers = [parent] + parent.getChildren() + + for peer in peers: + + if enable: + peer.display.enableListener('brightness', peer.name) + peer.display.enableListener('contrast', peer.name) + peer .enableListener('displayRange', peer.name) + else: + peer.display.disableListener('brightness', peer.name) + peer.display.disableListener('contrast', peer.name) + peer .disableListener('displayRange', peer.name) + def briconChanged(self, *a): """Called when the ``brightness``/``contrast`` properties of the :class:`~fsl.fslview.displaycontext.display.Display` instance change. Updates the :attr:`displayRange` property accordingly. + + See :func:`.colourmaps.briconToDisplayRange`. """ - display = self.display - - # Turn the bricon percentages into - # values between 1 and 0 (inverted) - brightness = 1 - self.display.brightness / 100.0 - contrast = 1 - self.display.contrast / 100.0 - - dmin, dmax = self.dataMin, self.dataMax - drange = dmax - dmin - dmid = dmin + 0.5 * drange - - # The brightness is applied as a linear offset, - # with 0.5 equivalent to an offset of 0.0. - offset = (brightness * 2 - 1) * drange - - # If the contrast lies between 0.0 and 0.5, it is - # applied to the colour as a linear scaling factor. - scale = contrast * 2 - - # If the contrast lies between 0.5 and 0.1, it - # is applied as an exponential scaling factor, - # so lower values (closer to 0.5) have less of - # 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 .disableListener('displayRange', self.name) - display.disableListener('brightness', self.name) - display.disableListener('contrast', self.name) - + dlo, dhi = fslcm.briconToDisplayRange( + (self.dataMin, self.dataMax), + self.display.brightness / 100.0, + self.display.contrast / 100.0) + + self.__toggleListeners(False) self.displayRange.x = [dlo, dhi] - - self .enableListener('displayRange', self.name) - display.enableListener('brightness', self.name) - display.enableListener('contrast', self.name) + self.__toggleListeners(True) def displayRangeChanged(self, *a): + """Called when the `attr`:displayRange: property changes. - display = self.display - - dmin, dmax = self.dataMin, self.dataMax - drange = dmax - dmin - dmid = dmin + 0.5 * drange - - dlo, dhi = self.displayRange.x + Updates the :attr:`.Display.brightness` and :attr:`.Display.contrast` + properties accordingly. - # 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 + See :func:`.colourmaps.displayRangeToBricon`. + """ - self .disableListener('displayRange', self.name) - display.disableListener('brightness', self.name) - display.disableListener('contrast', self.name) + brightness, contrast = fslcm.displayRangeToBricon( + (self.dataMin, self.dataMax), + self.displayRange.x) + + self.__toggleListeners(False) # update bricon - display.brightness = 100 - brightness * 100 - display.contrast = 100 - contrast * 100 + self.display.brightness = 100 - brightness * 100 + self.display.contrast = 100 - contrast * 100 - self .enableListener('displayRange', self.name) - display.enableListener('brightness', self.name) - display.enableListener('contrast', self.name) + self.__toggleListeners(True) diff --git a/fsl/fslview/frame.py b/fsl/fslview/frame.py index 1ed409d944f8e4824af59bdd3627412608a9cbf4..4b2429eb261824b7cb127440c806fad13ce4b380 100644 --- a/fsl/fslview/frame.py +++ b/fsl/fslview/frame.py @@ -354,11 +354,26 @@ class FSLViewFrame(wx.Frame): else: self.Centre() - # TODO Restore the previous view panel layout if restore: + self.addViewPanel(views.OrthoPanel) + viewPanel = self.getViewPanels()[0][0] + + # Set up a default for ortho views + # layout (this will hopefully eventually + # be done by the FSLViewFrame instance) + import fsl.fslview.controls.imagelistpanel as ilp + import fsl.fslview.controls.locationpanel as lop + import fsl.fslview.controls.imagedisplaytoolbar as idt + import fsl.fslview.controls.orthotoolbar as ot + + viewPanel.togglePanel(ilp.ImageListPanel) + viewPanel.togglePanel(lop.LocationPanel) + viewPanel.togglePanel(idt.ImageDisplayToolBar, False, viewPanel) + viewPanel.togglePanel(ot .OrthoToolBar, False, viewPanel) + def _makeMenuBar(self): """Constructs a bunch of menu items for working with the given diff --git a/fsl/fslview/gl/__init__.py b/fsl/fslview/gl/__init__.py index 76f26233f6541287cd2a957186f7cc8745ea81bd..c39f170321c82b039e30dc441a01ef8ad61d20cd 100644 --- a/fsl/fslview/gl/__init__.py +++ b/fsl/fslview/gl/__init__.py @@ -179,9 +179,7 @@ def bootstrap(glVersion=None): verstr, renderer)) # If we're using a software based renderer, - # make two-stage rendering (off-screen rendering - # to a texture, then rendering said texture to the - # screen) the default. + # reduce the default performance settings # # There doesn't seem to be any quantitative # method for determining whether we are using @@ -189,29 +187,25 @@ def bootstrap(glVersion=None): # necessary. if 'software' in renderer.lower(): - log.debug('Software-based rendering detected - two-stage ' - 'rendering will be the default method.') + log.debug('Software-based rendering detected - ' + 'lowering default performance settings.') - import fsl.fslview.displaycontext.display as di - import fsl.fslview.displaycontext.vectoropts as vo - import fsl.fslview.displaycontext.sceneopts as so - import fsl.fslview.gl.slicecanvas as sc + import fsl.fslview.displaycontext.display as di + import fsl.fslview.displaycontext.sceneopts as so - # Make two-stage rendering the default - so.SceneOpts .twoStageRender.setConstraint(None, 'default', True) - sc.SliceCanvas.twoStageRender.setConstraint(None, 'default', True) + so.SceneOpts.performance.setConstraint(None, 'default', 2) # And disable some fancy options - spline # may have been disabled above, so absorb - # the IndexError if it occurs - vo.VectorOpts .displayMode .removeChoice('line') + # the ValueError if it occurs try: di.Display.interpolation.removeChoice('spline') - except IndexError: pass + except ValueError: pass - thismod.GL_VERSION = verstr - thismod.glvolume_funcs = glpkg.glvolume_funcs - thismod.glvector_funcs = glpkg.glvector_funcs - thismod._bootstrapped = True + thismod.GL_VERSION = verstr + thismod.glvolume_funcs = glpkg.glvolume_funcs + thismod.glrgbvector_funcs = glpkg.glrgbvector_funcs + thismod.gllinevector_funcs = glpkg.gllinevector_funcs + thismod._bootstrapped = True def getWXGLContext(parent=None): diff --git a/fsl/fslview/gl/annotations.py b/fsl/fslview/gl/annotations.py index 521b8ad43f10d274e236fa6aa66de0112d85b2a5..b88850cd053dd0cca8eba5f0b192fe0f16e199fd 100644 --- a/fsl/fslview/gl/annotations.py +++ b/fsl/fslview/gl/annotations.py @@ -21,6 +21,7 @@ import OpenGL.GL as gl import fsl.fslview.gl.globject as globject +import fsl.fslview.gl.routines as glroutines import fsl.fslview.gl.textures as textures import fsl.utils.transform as transform @@ -166,9 +167,6 @@ class Annotations(object): gl.glMultMatrixf(xform.ravel('C')) for obj in objs: - - if not obj.ready(): - continue obj.setAxes(self._xax, self._yax) @@ -388,7 +386,7 @@ class VoxelGrid(AnnotationObject): off = 0 voxels[:, ax] += off + self.offsets[ax] - verts, idxs = globject.voxelGrid(voxels, xax, yax, 1, 1) + verts, idxs = glroutines.voxelGrid(voxels, xax, yax, 1, 1) gl.glVertexPointer(3, gl.GL_FLOAT, 0, verts.ravel('C')) gl.glDrawElements(gl.GL_LINES, len(idxs), gl.GL_UNSIGNED_INT, idxs) @@ -417,35 +415,33 @@ class VoxelSelection(AnnotationObject): self.voxToDisplayMat = voxToDisplayMat self.offsets = offsets - self.texture = textures.getTexture( - selection, - '{}_{}'.format(type(self).__name__, id(selection))) + self.texture = textures.SelectionTexture( + '{}_{}'.format(type(self).__name__, id(selection)), + selection) + + def destroy(self): + self.texture.destroy() + self.texture = None def draw(self, zpos): xax = self.xax yax = self.yax - zax = self.zax shape = self.selection.selection.shape - verts, _ = globject.slice2D(shape, - xax, - yax, - self.voxToDisplayMat) - - verts[:, zax] = zpos - - texs = transform.transform(verts, self.displayToVoxMat) + 0.5 - texs /= shape + verts, _, texs = glroutines.slice2D(shape, + xax, + yax, + zpos, + self.voxToDisplayMat, + self.displayToVoxMat) verts = np.array(verts, dtype=np.float32).ravel('C') texs = np.array(texs, dtype=np.float32).ravel('C') - idxs = np.arange(4, dtype=np.uint32) - gl.glActiveTexture(gl.GL_TEXTURE0) - gl.glEnable(gl.GL_TEXTURE_3D) - gl.glBindTexture(gl.GL_TEXTURE_3D, self.texture.texture) + self.texture.bindTexture(gl.GL_TEXTURE0) + gl.glTexEnvf(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_MODULATE) # gl.glEnableClientState(gl.GL_VERTEX_ARRAY) @@ -453,13 +449,12 @@ class VoxelSelection(AnnotationObject): gl.glVertexPointer( 3, gl.GL_FLOAT, 0, verts) gl.glTexCoordPointer(3, gl.GL_FLOAT, 0, texs) - gl.glDrawElements(gl.GL_TRIANGLE_STRIP, 4, gl.GL_UNSIGNED_INT, idxs) + gl.glDrawArrays(gl.GL_TRIANGLES, 0, 6) # gl.glDisableClientState(gl.GL_VERTEX_ARRAY) - gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY) + gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY) - gl.glBindTexture(gl.GL_TEXTURE_3D, 0) - gl.glDisable(gl.GL_TEXTURE_3D) + self.texture.unbindTexture() # class Text(AnnotationObject) ? diff --git a/fsl/fslview/gl/colourbarcanvas.py b/fsl/fslview/gl/colourbarcanvas.py index 0ff1e882afa46c4f8fb3779387b796d5dbf919a5..84995b1aed4e282ab4f8263f7627007d165eca44 100644 --- a/fsl/fslview/gl/colourbarcanvas.py +++ b/fsl/fslview/gl/colourbarcanvas.py @@ -24,6 +24,7 @@ import props import fsl.utils.colourbarbitmap as cbarbmp import fsl.data.strings as strings +import fsl.fslview.gl.textures as textures class ColourBarCanvas(props.HasProperties): @@ -116,42 +117,18 @@ class ColourBarCanvas(props.HasProperties): self.label, self.orientation, labelSide) - bitmap = np.flipud(bitmap) if self._tex is None: - self._tex = gl.glGenTextures(1) - log.debug('Created GL texture: {}'.format(self._tex)) - - # Allow textures of any size - gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) - gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) - - gl.glBindTexture( gl.GL_TEXTURE_2D, self._tex) - gl.glTexParameteri(gl.GL_TEXTURE_2D, - gl.GL_TEXTURE_MAG_FILTER, - gl.GL_LINEAR) - gl.glTexParameteri(gl.GL_TEXTURE_2D, - gl.GL_TEXTURE_MIN_FILTER, - gl.GL_LINEAR) - gl.glTexParameteri(gl.GL_TEXTURE_2D, - gl.GL_TEXTURE_WRAP_S, - gl.GL_CLAMP_TO_BORDER) - gl.glTexParameteri(gl.GL_TEXTURE_2D, - gl.GL_TEXTURE_WRAP_T, - gl.GL_CLAMP_TO_BORDER) - - bitmap = bitmap.ravel('C') - - gl.glTexImage2D(gl.GL_TEXTURE_2D, - 0, - gl.GL_RGBA8, - w, - h, - 0, - gl.GL_RGBA, - gl.GL_UNSIGNED_BYTE, - bitmap) - gl.glBindTexture(gl.GL_TEXTURE_2D, 0) + self._tex = textures.Texture2D('{}_{}'.format( + type(self).__name__, id(self)), gl.GL_LINEAR) + + # The bitmap has shape W*H*4, but the + # Texture2D instance needs it in shape + # 4*W*H + bitmap = np.fliplr(bitmap).transpose([2, 0, 1]) + + self._tex.setData(bitmap) + self._tex.refresh() def _draw(self): @@ -175,26 +152,6 @@ class ColourBarCanvas(props.HasProperties): gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) gl.glShadeModel(gl.GL_FLAT) - gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) - gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) - - gl.glActiveTexture(gl.GL_TEXTURE0) - gl.glEnable(gl.GL_TEXTURE_2D) - gl.glTexEnvf(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_REPLACE) - gl.glBindTexture(gl.GL_TEXTURE_2D, self._tex) - - gl.glBegin(gl.GL_QUADS) - gl.glTexCoord2f(0, 0) - gl.glVertex3f( 0, 0, 0) - gl.glTexCoord2f(0, 1) - gl.glVertex3f( 0, 1, 0) - gl.glTexCoord2f(1, 1) - gl.glVertex3f( 1, 1, 0) - gl.glTexCoord2f(1, 0) - gl.glVertex3f( 1, 0, 0) - gl.glEnd() - - gl.glBindTexture(gl.GL_TEXTURE_2D, 0) - gl.glDisable(gl.GL_TEXTURE_2D) - + self._tex.drawOnBounds(0, 0, 1, 0, 1, 0, 1) + self._postDraw() diff --git a/fsl/fslview/gl/gl14/__init__.py b/fsl/fslview/gl/gl14/__init__.py index 5f5a552bc887d6b2249a90bd70cc2f40004c756b..360c9cafe687bebc15183151878599ac7ae73b12 100644 --- a/fsl/fslview/gl/gl14/__init__.py +++ b/fsl/fslview/gl/gl14/__init__.py @@ -6,4 +6,5 @@ # import glvolume_funcs -import glvector_funcs +import glrgbvector_funcs +import gllinevector_funcs diff --git a/fsl/fslview/gl/gl14/briconalpha.prog b/fsl/fslview/gl/gl14/briconalpha.prog deleted file mode 100644 index 3dbbc4fa26a09f590ccd03746b33da58a3250a8b..0000000000000000000000000000000000000000 --- a/fsl/fslview/gl/gl14/briconalpha.prog +++ /dev/null @@ -1,122 +0,0 @@ -# Fragment program routine which applies brightness, contrast, -# and opacity values to a given colour, and writes the result -# to result.color. -# -# Inputs: -# bca - Vector which contains brightness (x), contrast (y), -# and alpha (z) levels, as values in the range [0.0, 1.0] -# -# fragColour - The colour to be adjusted. -# -# Outputs: -# result.color - The resulting fragment colour. -# -# Author: Paul McCarthy <pauldmccarthy@gmail.com> -# - -TEMP brightness; -TEMP contrast; -TEMP alpha; -TEMP tmpAlpha; - -TEMP offset; -TEMP scale; -TEMP linScale; -TEMP expScale; -TEMP expTemp; - -TEMP tempColour; - -MOV brightness, bca.x; -MOV contrast, bca.y; -MOV alpha, bca.z; - -# -# Calculate brightness -# - -# The brightness is applied as -# a linear offset, with 0.5 -# equivalent to an offset of 0.0. -MOV offset, brightness; -MUL offset, offset, { 2, 2, 2, 0 }; -SUB offset, offset, { 1, 1, 1, 0 }; - -# -# Calculate contrast -# - -# If the contrast lies between 0.0 -# and 0.5, it is applied to the -# colour as a linear scaling factor. -MOV linScale, contrast; -MUL linScale, linScale, { 2, 2, 2, 0 }; - -# If the contrast lies between 0.5 and 0.1, it -# is applied as an exponential scaling factor, -# so lower values (closer to 0.5) have less of -# an effect than higher values (closer to 1.0). -MOV expScale, linScale; -MOV expTemp, contrast; -SUB expTemp, expTemp, { 0.5, 0.5, 0.5, 0 }; -MUL expTemp, expTemp, { 6, 6, 6, 0 }; -EX2 expTemp, expTemp.x; -SUB expTemp, expTemp, { 1, 1, 1, 0 }; -ADD expScale, expScale, expTemp; - -# If contrast is <= 0.5, apply the -# linear scaling factor, otherwise -# apply the exponential factor. -SUB contrast, contrast, { 0.5, 0.5, 0.5, 0.0 }; -CMP scale, contrast, linScale, expScale; -MAD scale, scale, { 1, 1, 1, 0 }, { 0, 0, 0, 1 }; - -# -# Prepare to apply the calculated -# brightness/contrast/alpha settings -# -MOV tempColour, fragColour; - -# -# Apply alpha. -# - -# If the existing colour -# has an alpha less than 1.0, use it -# instead of the global alpha. -MOV tmpAlpha, fragColour.a; -SUB tmpAlpha, tmpAlpha, { 1, 1, 1, 1 }; -CMP tmpAlpha, tmpAlpha, fragColour.a, alpha; - -MUL tmpAlpha, tmpAlpha, { 0.0, 0.0, 0.0, 1.0 }; -MAD tempColour, tempColour, { 1.0, 1.0, 1.0, 0.0 }, tmpAlpha; - -# -# Apply brightness -# - -ADD tempColour, tempColour, offset; - -# Clamp the RGBA values -# to the range [0, 1] -MIN tempColour, tempColour, { 1.0, 1.0, 1.0, 1.0 }; -MAX tempColour, tempColour, { 0.0, 0.0, 0.0, 0.0 }; - -# -# Apply contrast -# - -# Apply the scaling factor, but -# keep the new range centred at 0.5. -SUB tempColour, tempColour, { 0.5, 0.5, 0.5, 0.0 }; -MAD tempColour, tempColour, scale, { 0.5, 0.5, 0.5, 0.0 }; - -# Clamp again to [0, 1] -MIN tempColour, tempColour, { 1.0, 1.0, 1.0, 1.0 }; -MAX tempColour, tempColour, { 0.0, 0.0, 0.0, 0.0 }; - -# -# Write the final colour -# - -MOV result.color, tempColour; diff --git a/fsl/fslview/gl/gl14/common_vert.prog b/fsl/fslview/gl/gl14/common_vert.prog deleted file mode 100644 index 3c8e1d04b65795e3e1d37c67cda0d21aec0aa119..0000000000000000000000000000000000000000 --- a/fsl/fslview/gl/gl14/common_vert.prog +++ /dev/null @@ -1,107 +0,0 @@ -# -# Vertex program routine used for rendering GLObject instances. -# -# This routine does three things: -# -# - Transforms vertex coordinates from display coordinates into screen -# coordinates. -# -# - Transforms vertex coordinates from display coordinates into voxel -# coordinates. -# -# - Sets the vertex texture coordinate from its display coordinates. -# -# Required inputs: -# -# state.matrix.mvp - Matrix which transforms from the display coordinate -# system to the screen coordinate system. -# -# program.local[0] -# program.local[1] -# program.local[2] -# program.local[3] - Matrix which transforms from the display coordinate -# system to the image voxel coordinate system. -# -# program.local[4] -# program.local[5] -# program.local[6] -# program.local[7] - Matrix which performs an arbitrary transformation in -# the display coordinate system. -# -# vertex.texcoord[0] - Optional texture coordinates - these are transformed -# to voxel coordinates, and passed through to the -# fragment shader in result.texcoord[2]. This gives -# the option of using the vertex coordinates as texture -# coordinates, or to use independent texture -# coordinates, for the image texture lookup. Fragment -# programs get passed both, and will need to decide -# which coordinates to use. -# -# Outputs: -# -# result.position - vertex position in the screen coordinate system -# result.texcoord[0] - vertex position in the display coordinate system -# result.texcoord[1] - vertex position in the image voxel coordinate system -# result.texcoord[2] - texture coordinates in the image voxel coordinate -# system -# -# Author: Paul McCarthy <pauldmccarthy@gmail.com> -# - -TEMP vertexScreenPos; -TEMP vertexVoxelPos; -TEMP vertexTexCoord; - -PARAM dispToScreenMat[4] = { state.matrix.mvp }; -PARAM dispToVoxMat[ 4] = { program.local[0], - program.local[1], - program.local[2], - program.local[3] }; -PARAM worldToWorldMat[4] = { program.local[4], - program.local[5], - program.local[6], - program.local[7] }; - - -# Transform the vertex position -# from display coordinates to -# screen coordinates, incorporating -# the arbitrary display space -# transformation -DP4 vertexScreenPos.x, worldToWorldMat[0], vertex.position; -DP4 vertexScreenPos.y, worldToWorldMat[1], vertex.position; -DP4 vertexScreenPos.z, worldToWorldMat[2], vertex.position; -DP4 vertexScreenPos.w, worldToWorldMat[3], vertex.position; - -DP4 vertexScreenPos.x, dispToScreenMat[0], vertexScreenPos; -DP4 vertexScreenPos.y, dispToScreenMat[1], vertexScreenPos; -DP4 vertexScreenPos.z, dispToScreenMat[2], vertexScreenPos; -DP4 vertexScreenPos.w, dispToScreenMat[3], vertexScreenPos; - -# Transform the vertex position -# from display coordinates to -# voxel coordinates -DP4 vertexVoxelPos.x, dispToVoxMat[0], vertex.position; -DP4 vertexVoxelPos.y, dispToVoxMat[1], vertex.position; -DP4 vertexVoxelPos.z, dispToVoxMat[2], vertex.position; -DP4 vertexVoxelPos.w, dispToVoxMat[3], vertex.position; - -# And do the same for the -# texture0 coordinates -DP4 vertexTexCoord.x, dispToVoxMat[0], vertex.texcoord[0]; -DP4 vertexTexCoord.y, dispToVoxMat[1], vertex.texcoord[0]; -DP4 vertexTexCoord.z, dispToVoxMat[2], vertex.texcoord[0]; -DP4 vertexTexCoord.w, dispToVoxMat[3], vertex.texcoord[0]; - -# Offset voxel coordinates by 0.5 -# so they are centred within a voxel. -# See comments in gl21/common_vert.glsl -# for an explanation. -ADD vertexVoxelPos, vertexVoxelPos, { 0.5, 0.5, 0.5, 0.0 }; -ADD vertexTexCoord, vertexTexCoord, { 0.5, 0.5, 0.5, 0.0 }; - -# Write the outputs -MOV result.position, vertexScreenPos; -MOV result.texcoord[0], vertex.position; -MOV result.texcoord[1], vertexVoxelPos; -MOV result.texcoord[2], vertexTexCoord; diff --git a/fsl/fslview/gl/gl14/gllinevector_funcs.py b/fsl/fslview/gl/gl14/gllinevector_funcs.py new file mode 100644 index 0000000000000000000000000000000000000000..7204194878c9f619544fca69763d09bf0086c7de --- /dev/null +++ b/fsl/fslview/gl/gl14/gllinevector_funcs.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python +# +# gllinevector_funcs.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import logging + +import numpy as np + +import OpenGL.GL as gl +import OpenGL.GL.ARB.fragment_program as arbfp +import OpenGL.GL.ARB.vertex_program as arbvp +import OpenGL.raw.GL._types as gltypes + +import fsl.utils.transform as transform +import fsl.fslview.gl.gllinevector as gllinevector +import fsl.fslview.gl.resources as glresources +import fsl.fslview.gl.shaders as shaders + + +log = logging.getLogger(__name__) + + +def init(self): + + self.vertexProgram = None + self.fragmentProgram = None + self.lineVertices = None + + self._vertexResourceName = '{}_{}_vertices'.format( + type(self).__name__, id(self.image)) + + compileShaders( self) + updateShaderState(self) + updateVertices( self) + + display = self.display + opts = self.opts + + def vertexUpdate(*a): + updateVertices(self) + self.onUpdate() + + display.addListener('resolution', self.name, vertexUpdate) + opts .addListener('directed', self.name, vertexUpdate) + + +def destroy(self): + arbvp.glDeleteProgramsARB(1, gltypes.GLuint(self.vertexProgram)) + arbfp.glDeleteProgramsARB(1, gltypes.GLuint(self.fragmentProgram)) + + self.display.removeListener('resolution', self.name) + self.opts .removeListener('directed', self.name) + + glresources.delete(self._vertexResourceName) + + +def compileShaders(self): + if self.vertexProgram is not None: + arbvp.glDeleteProgramsARB(1, gltypes.GLuint(self.vertexProgram)) + + if self.fragmentProgram is not None: + arbfp.glDeleteProgramsARB(1, gltypes.GLuint(self.fragmentProgram)) + + vertShaderSrc = shaders.getVertexShader( self, + sw=self.display.softwareMode) + fragShaderSrc = shaders.getFragmentShader(self, + sw=self.display.softwareMode) + + vertexProgram, fragmentProgram = shaders.compilePrograms( + vertShaderSrc, fragShaderSrc) + + self.vertexProgram = vertexProgram + self.fragmentProgram = fragmentProgram + + updateVertices(self) + + +def updateVertices(self): + + image = self.image + display = self.display + opts = self.opts + + if self.lineVertices is None: + self.lineVertices = glresources.get( + self._vertexResourceName, gllinevector.GLLineVertices, self) + + newHash = (hash(display.transform) ^ + hash(display.resolution) ^ + hash(opts .directed)) + + if hash(self.lineVertices) != newHash: + + log.debug('Re-generating line vertices for {}'.format(image)) + self.lineVertices.refresh(self) + glresources.set(self._vertexResourceName, + self.lineVertices, + overwrite=True) + + +def updateShaderState(self): + opts = self.displayOpts + + gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB) + gl.glEnable(arbfp.GL_FRAGMENT_PROGRAM_ARB) + + arbvp.glBindProgramARB(arbvp.GL_VERTEX_PROGRAM_ARB, + self.vertexProgram) + arbfp.glBindProgramARB(arbfp.GL_FRAGMENT_PROGRAM_ARB, + self.fragmentProgram) + + voxValXform = self.imageTexture.voxValXform + cmapXform = self.xColourTexture.getCoordinateTransform() + shape = np.array(list(self.image.shape[:3]) + [0], dtype=np.float32) + invShape = 1.0 / shape + modThreshold = [opts.modThreshold / 100.0, 0.0, 0.0, 0.0] + + # Vertex program inputs + shaders.setVertexProgramVector( 0, invShape) + + # Fragment program inputs + shaders.setFragmentProgramMatrix(0, voxValXform) + shaders.setFragmentProgramMatrix(4, cmapXform) + shaders.setFragmentProgramVector(8, shape) + shaders.setFragmentProgramVector(9, modThreshold) + + gl.glDisable(arbvp.GL_VERTEX_PROGRAM_ARB) + gl.glDisable(arbfp.GL_FRAGMENT_PROGRAM_ARB) + + +def preDraw(self): + gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB) + gl.glEnable(arbfp.GL_FRAGMENT_PROGRAM_ARB) + + gl.glEnableClientState(gl.GL_VERTEX_ARRAY) + + if self.display.softwareMode: + gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY) + + arbvp.glBindProgramARB(arbvp.GL_VERTEX_PROGRAM_ARB, + self.vertexProgram) + arbfp.glBindProgramARB(arbfp.GL_FRAGMENT_PROGRAM_ARB, + self.fragmentProgram) + + +def draw(self, zpos, xform=None): + + display = self.display + opts = self.displayOpts + vertices, texCoords = self.lineVertices.getVertices(self, zpos) + + if vertices.size == 0: + return + + if display.softwareMode: + texCoords = texCoords.ravel('C') + gl.glClientActiveTexture(gl.GL_TEXTURE0) + gl.glTexCoordPointer(3, gl.GL_FLOAT, 0, texCoords) + + vertices = vertices.ravel('C') + v2d = self.display.getTransform('voxel', 'display') + + if xform is None: xform = v2d + else: xform = transform.concat(v2d, xform) + + gl.glPushMatrix() + gl.glMultMatrixf(np.array(xform, dtype=np.float32).ravel('C')) + + gl.glVertexPointer(3, gl.GL_FLOAT, 0, vertices) + + gl.glLineWidth(opts.lineWidth) + gl.glDrawArrays(gl.GL_LINES, 0, vertices.size / 3) + + gl.glPopMatrix() + + +def drawAll(self, zposes, xforms): + for zpos, xform in zip(zposes, xforms): + draw(self, zpos, xform) + + +def postDraw(self): + + gl.glDisable(arbvp.GL_VERTEX_PROGRAM_ARB) + gl.glDisable(arbfp.GL_FRAGMENT_PROGRAM_ARB) + + gl.glDisableClientState(gl.GL_VERTEX_ARRAY) + + if self.display.softwareMode: + gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY) diff --git a/fsl/fslview/gl/gl14/gllinevector_sw_vert.prog b/fsl/fslview/gl/gl14/gllinevector_sw_vert.prog new file mode 100644 index 0000000000000000000000000000000000000000..15f1280bcac9b3484cbc79d2e1eb0534348cb577 --- /dev/null +++ b/fsl/fslview/gl/gl14/gllinevector_sw_vert.prog @@ -0,0 +1,29 @@ +!!ARBvp1.0 +# +# Vertex program for rendering GLLineVector instances. Identical to +# gllinevector_vert.prog, but does not calculate voxel coordinates. +# +# Inputs: +# state.matrix.mvp - MVP transformation matrix +# vertex.position - Vertex position in the voxel coordinate system. +# vertex texcoord[0] - Texture coordinates +# +# Outputs: +# result.position - the vertex position +# result.texcoord[0] - the texture coordinates +# + +TEMP texCoord; + +# Transform the vertex position (which is in voxel +# coordinates) into display coordinates. It is +# assumed that a voxel->display transformation has +# been encoded into the mvp matrix. +DP4 result.position.x, state.matrix.mvp.row[0], vertex.position; +DP4 result.position.y, state.matrix.mvp.row[1], vertex.position; +DP4 result.position.z, state.matrix.mvp.row[2], vertex.position; +DP4 result.position.w, state.matrix.mvp.row[3], vertex.position; + +MOV result.texcoord[0], vertex.texcoord[0]; + +END diff --git a/fsl/fslview/gl/gl14/gllinevector_vert.prog b/fsl/fslview/gl/gl14/gllinevector_vert.prog new file mode 100644 index 0000000000000000000000000000000000000000..88cf875568b08046ba203c74695a3b73f63ea86d --- /dev/null +++ b/fsl/fslview/gl/gl14/gllinevector_vert.prog @@ -0,0 +1,45 @@ +!!ARBvp1.0 +# +# Vertex program for rendering GLLineVector instances. +# +# Inputs: +# state.matrix.mvp - MVP transformation matrix +# vertex.position - Vertex position in the voxel coordinate system. +# +# program.local[0] - (first three components) inverse of image shape +# +# Outputs: +# result.position - the vertex position +# result.texcoord[0] - the texture coordinates +# result.texcoord[1] - the voxel coordinates +# + +TEMP texCoord; + +PARAM invImageShape = program.local[0]; + +# Transform the vertex position (which is in voxel +# coordinates) into display coordinates. It is +# assumed that a voxel->display transformation has +# been encoded into the mvp matrix. +DP4 result.position.x, state.matrix.mvp.row[0], vertex.position; +DP4 result.position.y, state.matrix.mvp.row[1], vertex.position; +DP4 result.position.z, state.matrix.mvp.row[2], vertex.position; +DP4 result.position.w, state.matrix.mvp.row[3], vertex.position; + +# Transform the vertex coordinates +# into integer voxel coordinates +MOV texCoord, vertex.position; +ADD texCoord, texCoord, { 0.5, 0.5, 0.5, 0.0 }; +FLR texCoord, texCoord; + +MOV result.texcoord[1], texCoord; + +# Transform those integer voxel +# coordinates into texture coordinates +ADD texCoord, texCoord, { 0.5, 0.5, 0.5, 0.0 }; +MUL texCoord, texCoord, invImageShape; + +MOV result.texcoord[0], texCoord; + +END diff --git a/fsl/fslview/gl/gl14/glrgbvector_funcs.py b/fsl/fslview/gl/gl14/glrgbvector_funcs.py new file mode 100644 index 0000000000000000000000000000000000000000..22fba25ccdd849583b3f1fdb67cb7c5406c84859 --- /dev/null +++ b/fsl/fslview/gl/gl14/glrgbvector_funcs.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# +# glrgbvector_funcs.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import OpenGL.GL as gl +import OpenGL.GL.ARB.fragment_program as arbfp +import OpenGL.GL.ARB.vertex_program as arbvp +import OpenGL.raw.GL._types as gltypes + +import glvolume_funcs +import fsl.fslview.gl.shaders as shaders + + +def init(self): + + self.vertexProgram = None + self.fragmentProgram = None + + compileShaders( self) + updateShaderState(self) + + +def destroy(self): + arbvp.glDeleteProgramsARB(1, gltypes.GLuint(self.vertexProgram)) + arbfp.glDeleteProgramsARB(1, gltypes.GLuint(self.fragmentProgram)) + + +def compileShaders(self): + if self.vertexProgram is not None: + arbvp.glDeleteProgramsARB(1, gltypes.GLuint(self.vertexProgram)) + + if self.fragmentProgram is not None: + arbfp.glDeleteProgramsARB(1, gltypes.GLuint(self.fragmentProgram)) + + vertShaderSrc = shaders.getVertexShader( self, + sw=self.display.softwareMode) + fragShaderSrc = shaders.getFragmentShader(self, + sw=self.display.softwareMode) + + vertexProgram, fragmentProgram = shaders.compilePrograms( + vertShaderSrc, fragShaderSrc) + + self.vertexProgram = vertexProgram + self.fragmentProgram = fragmentProgram + + +def updateShaderState(self): + + opts = self.displayOpts + + gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB) + gl.glEnable(arbfp.GL_FRAGMENT_PROGRAM_ARB) + + arbvp.glBindProgramARB(arbvp.GL_VERTEX_PROGRAM_ARB, + self.vertexProgram) + arbfp.glBindProgramARB(arbfp.GL_FRAGMENT_PROGRAM_ARB, + self.fragmentProgram) + + voxValXform = self.imageTexture.voxValXform + cmapXform = self.xColourTexture.getCoordinateTransform() + shape = list(self.image.shape[:3]) + [0] + modThreshold = [opts.modThreshold / 100.0, 0.0, 0.0, 0.0] + + shaders.setFragmentProgramMatrix(0, voxValXform) + shaders.setFragmentProgramMatrix(4, cmapXform) + shaders.setFragmentProgramVector(8, shape + [0]) + shaders.setFragmentProgramVector(9, modThreshold) + + gl.glDisable(arbvp.GL_VERTEX_PROGRAM_ARB) + gl.glDisable(arbfp.GL_FRAGMENT_PROGRAM_ARB) + + +preDraw = glvolume_funcs.preDraw +draw = glvolume_funcs.draw +drawAll = glvolume_funcs.drawAll +postDraw = glvolume_funcs.postDraw diff --git a/fsl/fslview/gl/gl14/glvector_frag.prog b/fsl/fslview/gl/gl14/glvector_frag.prog index 86df8554c3be11ae87afdec9d8a784169368be2a..8cb39dc43229449a7d76eb1b719953ea6a5646e4 100644 --- a/fsl/fslview/gl/gl14/glvector_frag.prog +++ b/fsl/fslview/gl/gl14/glvector_frag.prog @@ -17,6 +17,9 @@ # - Uses those voxel values to colour the fragment. # # Required inputs: +# +# fragment.texcoord[0] - Fragment texture coordinates +# fragment.texcoord[1] - Fragment voxel coordinates # # program.local[0] # program.local[1] @@ -25,22 +28,20 @@ # image voxel values from their texture value # to the original data range. # -# program.local[4] - Image shape - number of voxels along the xyz +# program.local[4] +# program.local[5] +# program.local[6] +# program.local[7] - Transformation matrix which transforms the vector +# image voxel values from their texture value +# to the original data range. +# +# program.local[8] - Image shape - number of voxels along the xyz # dimensions in the image -# program.local[5] - Inverse of image shape # -# program.local[6] - Modulation threshold (x component) - if the +# program.local[9] - Modulation threshold (x component) - if the # modulation value is less than this, the fragment # is set to fully transparent. # -# program.local[7] - Vector which contains global brightness, contrast, -# and alpha settings, to pass to briconalpha.prog. -# -# program.local[8] - If greater than or equal to 0, fragment.texcoord[1] -# is used as the coordinates for the texture lookup. -# Otherwise, fragment.texcoord[2] is used for the -# texture lookup. -# # Outputs: # # result.color - The fragment colour (written by briconalpha.prog) @@ -49,57 +50,50 @@ # TEMP voxCoord; -TEMP normVoxCoord; TEMP modValue; TEMP voxValue; TEMP xColour; TEMP yColour; TEMP zColour; TEMP fragColour; -PARAM imageValueXform[4] = { program.local[0], - program.local[1], - program.local[2], - program.local[3] }; -PARAM imageShape = program.local[4]; -PARAM imageShapeInv = program.local[5]; -PARAM modThres = program.local[6]; -PARAM bca = program.local[7]; -PARAM useTexCoords = program.local[8]; +PARAM voxValXform[4] = { program.local[0], + program.local[1], + program.local[2], + program.local[3] }; +PARAM cmapXform[4] = { program.local[4], + program.local[5], + program.local[6], + program.local[7] }; + +PARAM imageShape = program.local[8]; +PARAM modThres = program.local[9]; # retrieve the voxel coordinates -CMP voxCoord, useTexCoords, fragment.texcoord[2], fragment.texcoord[1]; +MOV voxCoord, fragment.texcoord[1]; # Bail if the voxel coordinate # is out of the image space #pragma include test_in_bounds.prog -# Normalise voxel coordinates to -# lie in the range (0, 1), so they -# can be used for texture lookup -MUL normVoxCoord, voxCoord, imageShapeInv; - # look up vector values # from the 3D RGB texture -TEX voxValue, normVoxCoord, texture[0], 3D; +TEX voxValue, fragment.texcoord[0], texture[0], 3D; + +# Look up the modulation value +# from the modulation texture +TEX modValue, fragment.texcoord[0], texture[1], 3D; # Transform vector values from their normalised # texture range to their original data range, # and take the absolue value -MAD voxValue, voxValue, imageValueXform[0].x, imageValueXform[0].w; +MAD voxValue, voxValue, voxValXform[0].x, voxValXform[3].x; ABS voxValue, voxValue; +MAD voxValue, voxValue, cmapXform[ 0].x, cmapXform[ 3].x; -# Reset the opacity component -MAD voxValue, voxValue, { 1, 1, 1, 0 }, { 0, 0, 0, 1 }; +# Apply the modulation value +MUL voxValue, voxValue, modValue.x; -# Look up the modulation value -# from the modulation texture - -# initialise modValue transparency -# to 1.0, so it doesn't corrupt -# our voxel colour value later on -TEX modValue, normVoxCoord, texture[1], 3D; -MAD modValue, modValue, { 1, 1, 1, 0 }, { 0, 0, 0, 1 }; - -# Use those values to look up the +# Use the vector values to look up the # colours for each xyz direction TEX xColour, voxValue.x, texture[2], 1D; TEX yColour, voxValue.y, texture[3], 1D; @@ -111,20 +105,18 @@ MOV fragColour, xColour; ADD fragColour, fragColour, yColour; ADD fragColour, fragColour, zColour; -# Take the average of the alpha channel -MUL fragColour, fragColour, { 1.0, 1.0, 1.0, 0.333333 }; - -# Apply the modulation factor -MUL fragColour, fragColour, modValue; +# Take the highest alpha of the three colour maps +MAX fragColour.a, xColour.a, yColour.a; +MAX fragColour.a, fragColour.a, zColour.a; -# But clear the fragment if the modulation +# Clear the fragment if the modulation # vaue does not meet the threshold MOV modValue, modValue.x; SUB modValue, modValue, modThres.x; CMP fragColour, modValue.x, { 0, 0, 0, 0 }, fragColour; # Colour the pixel! -#pragma include briconalpha.prog +MOV result.color, fragColour; END diff --git a/fsl/fslview/gl/gl14/glvector_funcs.py b/fsl/fslview/gl/gl14/glvector_funcs.py deleted file mode 100644 index bedf6358dbcbd3e26e7004115381c9cbf15dd055..0000000000000000000000000000000000000000 --- a/fsl/fslview/gl/gl14/glvector_funcs.py +++ /dev/null @@ -1,288 +0,0 @@ -#!/usr/bin/env python -# -# glvector_funcs.py - Logic for rendering GLVector instances in an OpenGL 1.4 -# compatible manner. -# -# Author: Paul McCarthy <pauldmccarthy@gmail.com> -# -"""This module contains functions used by the -:class:`~fsl.fslview.gl.glvector.GLVector` class, for rendering -:class:`~fsl.data.image.Image` instances as vectors in an OpenGL 1.4 -compatible manner. - -See the ``GLVector`` documentation for more details. -""" - -import numpy as np -import scipy.ndimage as ndi - -import OpenGL.GL as gl -import OpenGL.raw.GL._types as gltypes -import OpenGL.GL.ARB.fragment_program as arbfp -import OpenGL.GL.ARB.vertex_program as arbvp - -import fsl.utils.transform as transform -import fsl.fslview.gl.shaders as shaders -import fsl.fslview.gl.globject as globject - - -def init(self): - """Compiles the vertex and fragment programs used for rendering. The - same programs are used for both ``line`` and ``rgb`` mode. - """ - - vertShaderSrc = shaders.getVertexShader( self) - fragShaderSrc = shaders.getFragmentShader(self) - - vertexProgram, fragmentProgram = shaders.compilePrograms( - vertShaderSrc, fragShaderSrc) - - self.vertexProgram = vertexProgram - self.fragmentProgram = fragmentProgram - - -def destroy(self): - """Deletes the vertex/fragment programs. """ - - arbvp.glDeleteProgramsARB(1, gltypes.GLuint(self.vertexProgram)) - arbfp.glDeleteProgramsARB(1, gltypes.GLuint(self.fragmentProgram)) - - -def setAxes(self): - """Calls one of :func:`rgbModeSetAxes` or :func:`lineModeSetAxes`, - depending upon the current display mode. - """ - mode = self.displayOpts.displayMode - - if mode == 'rgb': rgbModeSetAxes( self) - elif mode == 'line': lineModeSetAxes(self) - - -def rgbModeSetAxes(self): - """Creates four vertices which represent a slice through the image - texture, oriented according to the plane defined by - ``self.xax`` and ``self.yax``. - - See :func:`~fsl.fslview.globject.slice2D` for more details. - """ - worldCoords, idxs = globject.slice2D( - self.image.shape, - self.xax, - self.yax, - self.display.getTransform('voxel', 'display')) - - self.worldCoords = worldCoords - self.indices = idxs - - -def lineModeSetAxes(self): - """Creates an array of points forming a rectangular grid, one point - in the centre of each voxel, oriented according to the plane defined by - ``self.xax`` and ``self.yax``. These points are used by - :func:`lineModeDraw` to generate lines representing vectors at every - voxel. - - See :func:`~fsl.fslview.globject.calculateSamplePoints` for more details. - """ - worldCoords, xpixdim, ypixdim, lenx, leny = \ - globject.calculateSamplePoints( - self.image, - self.display, - self.xax, - self.yax) - - self.worldCoords = worldCoords - self.xpixdim = xpixdim - self.ypixdim = ypixdim - - -def preDraw(self): - """Loads the vertex/fragment programs, and sets some program parameters. - """ - - gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB) - gl.glEnable(arbfp.GL_FRAGMENT_PROGRAM_ARB) - - gl.glEnableClientState(gl.GL_VERTEX_ARRAY) - gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY) - - arbvp.glBindProgramARB(arbvp.GL_VERTEX_PROGRAM_ARB, - self.vertexProgram) - arbfp.glBindProgramARB(arbfp.GL_FRAGMENT_PROGRAM_ARB, - self.fragmentProgram) - - # the vertex program needs to be able to - # transform from display space to voxel - # space - shaders.setVertexProgramMatrix( - 0, self.display.getTransform('display', 'voxel').T) - - if self.displayOpts.displayMode == 'line': - shaders.setFragmentProgramMatrix(0, self.imageTexture.voxValXform.T) - else: - shaders.setFragmentProgramMatrix(0, np.eye(4)) - - # The fragment program needs to know the image - # shape and its inverse, so it can scale voxel - # coordinates to the range [0.0, 1.0], and so - # it can clip fragments outside of the image - # space. It also needs to know the global - # brightness, contrast, and alpha values. - shape = list(self.image.shape) - invshape = [1.0 / s for s in shape] - bca = [self.display.brightness / 100.0, - self.display.contrast / 100.0, - self.display.alpha / 100.0] - modThres = [self.displayOpts.modThreshold / 100.0] - - shaders.setFragmentProgramVector(4, shape + [0]) - shaders.setFragmentProgramVector(5, invshape + [0]) - shaders.setFragmentProgramVector(6, modThres + [0, 0, 0]) - shaders.setFragmentProgramVector(7, bca + [0]) - - -def draw(self, zpos, xform=None): - """Calls one of :func:`lineModeDraw` or :func:`rgbModeDraw`, depending - upon the current display mode. - """ - if xform is None: - xform = np.eye(4, dtype=np.float32) - - drawAll(self, [zpos], [xform]) - - -def drawAll(self, zposes, xforms): - - if not self.display.enabled: - return - - if self.displayOpts.displayMode == 'line': - lineModeDrawAll(self, zposes, xforms) - - elif self.displayOpts.displayMode == 'rgb': - rgbModeDrawAll( self, zposes, xforms) - - -def lineModeDrawAll(self, zposes, xforms): - """Creates a line, representing a vector, at each voxel at the specified - ``zpos``, and renders them. - """ - - image = self.image - display = self.display - worldCoords = self.worldCoords - nVerts = worldCoords.shape[0] - nSlices = len(zposes) - - worldCoords = np.vstack([worldCoords] * nSlices) - worldCoords[:, self.zax] = np.repeat(zposes, nVerts) - texCoords = np.array(worldCoords) - - # Transform the world coordinates to - # floating point voxel coordinates - dToVMat = display.getTransform('display', 'voxel') - vToDMat = display.getTransform('voxel', 'display') - - voxCoords = transform.transform(worldCoords, dToVMat).transpose() - imageData = image.data - nVoxels = worldCoords.shape[0] - - # Get the image data at those - # voxel coordinates, using - # nearest neighbour interpolation. - # - # Three separate calls to map_coordinates - # is generally faster than constructing - # a 4D coordinate array, and performing - # one call to map_coordinates. - xvals = ndi.map_coordinates(imageData[:, :, :, 0], - voxCoords, - order=0, - mode='nearest', - prefilter=False) - yvals = ndi.map_coordinates(imageData[:, :, :, 1], - voxCoords, - order=0, - mode='nearest', - prefilter=False) - zvals = ndi.map_coordinates(imageData[:, :, :, 2], - voxCoords, - order=0, - mode='nearest', - prefilter=False) - - # make a N*3 list of vectors - vecs = np.array([xvals, yvals, zvals]).transpose() - - # make a bunch of vertices which represent lines - # (two vertices per line), centered at the origin - # and scaled appropriately - vecs *= 0.5 - vecs = np.hstack((-vecs, vecs)).reshape((2 * nVoxels, 3)) - - # Scale the vector by the minimum voxel length, - # so it is a unit vector within real world space - vecs /= (image.pixdim[:3] / min(image.pixdim[:3])) - - # Offset each of those vertices by - # their original voxel coordinates - vecs += voxCoords.T.repeat(2, 0) - - # Translate those line vertices - # into display coordinates - worldCoords = transform.transform(vecs, vToDMat) - - for i, (zpos, xform) in enumerate(zip(zposes, xforms)): - - start = i * (nVerts * 2) - end = start + (nVerts * 2) - c = worldCoords[start:end, :] - - c[:, self.zax] = zpos - - worldCoords[start:end, :] = transform.transform(c, xform) - - texCoords = np.repeat(texCoords, 2, axis=0) - worldCoords = np.array(worldCoords, dtype=np.float32).ravel('C') - - # Draw all the lines! - shaders.setVertexProgramMatrix( 4, np.eye( 4, dtype=np.float32)) - shaders.setFragmentProgramVector(8, -np.ones(4, dtype=np.float32)) - - gl.glLineWidth(2) - gl.glVertexPointer( 3, gl.GL_FLOAT, 0, worldCoords) - gl.glTexCoordPointer(3, gl.GL_FLOAT, 0, texCoords) - - gl.glDrawArrays(gl.GL_LINES, 0, 2 * nVoxels) - - -def rgbModeDrawAll(self, zposes, xforms): - """Renders a rectangular slice through the vector image texture at the - specified ``zpos``. - """ - - worldCoords = np.array(self.worldCoords) - indices = np.array(self.indices) - - worldCoords[[2, 3], :] = worldCoords[[3, 2], :] - - worldCoords, texCoords, indices = globject.broadcast( - worldCoords, indices, zposes, xforms, self.zax) - - shaders.setVertexProgramMatrix( 4, np.eye( 4, dtype=np.float32)) - shaders.setFragmentProgramVector(8, -np.ones(4, dtype=np.float32)) - - gl.glVertexPointer( 3, gl.GL_FLOAT, 0, worldCoords) - gl.glTexCoordPointer(3, gl.GL_FLOAT, 0, texCoords) - - gl.glDrawElements(gl.GL_QUADS, len(indices), gl.GL_UNSIGNED_INT, indices) - - -def postDraw(self): - """Disables the vertex/fragment programs used for drawing.""" - - gl.glDisable(arbfp.GL_FRAGMENT_PROGRAM_ARB) - gl.glDisable(arbvp.GL_VERTEX_PROGRAM_ARB) - - gl.glDisableClientState(gl.GL_VERTEX_ARRAY) - gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY) diff --git a/fsl/fslview/gl/gl14/glvector_sw_frag.prog b/fsl/fslview/gl/gl14/glvector_sw_frag.prog new file mode 100644 index 0000000000000000000000000000000000000000..235a73b7ed11b5a130725e946bbf2e2bec52f553 --- /dev/null +++ b/fsl/fslview/gl/gl14/glvector_sw_frag.prog @@ -0,0 +1,118 @@ +!!ARBfp1.0 +# +# Fragment program used for rendering GLVector instances. +# Identical to glvector_frag.prog, but omits voxel coordinate +# bounds checking. +# +# This fragment program does the following: +# +# - Retrieves the voxel coordinates corresponding to the fragment +# +# - Uses those voxel coordinates to look up the corresponding xyz +# directions value in the 3D RGB image texture. +# +# - Looks up the colours corresponding to those xyz directions. +# +# - Modulates those colours by the modulation texture. +# +# - Uses those voxel values to colour the fragment. +# +# Required inputs: +# +# fragment.texcoord[0] - Fragment texture coordinates +# +# program.local[0] +# program.local[1] +# program.local[2] +# program.local[3] - Transformation matrix which transforms the vector +# image voxel values from their texture value +# to the original data range. +# +# program.local[4] +# program.local[5] +# program.local[6] +# program.local[7] - Transformation matrix which transforms the vector +# image voxel values from their texture value +# to the original data range. +# +# program.local[8] - Image shape - number of voxels along the xyz +# dimensions in the image +# +# program.local[9] - Modulation threshold (x component) - if the +# modulation value is less than this, the fragment +# is set to fully transparent. +# +# Outputs: +# +# result.color - The fragment colour (written by briconalpha.prog) +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +TEMP voxCoord; +TEMP modValue; +TEMP voxValue; +TEMP xColour; +TEMP yColour; +TEMP zColour; +TEMP fragColour; +PARAM voxValXform[4] = { program.local[0], + program.local[1], + program.local[2], + program.local[3] }; +PARAM cmapXform[4] = { program.local[4], + program.local[5], + program.local[6], + program.local[7] }; + +PARAM imageShape = program.local[8]; +PARAM modThres = program.local[9]; + +# retrieve the voxel coordinates +MOV voxCoord, fragment.texcoord[1]; + +# look up vector values +# from the 3D RGB texture +TEX voxValue, fragment.texcoord[0], texture[0], 3D; + +# Look up the modulation value +# from the modulation texture +TEX modValue, fragment.texcoord[0], texture[1], 3D; + +# Transform vector values from their normalised +# texture range to their original data range, +# and take the absolue value +MAD voxValue, voxValue, voxValXform[0].x, voxValXform[3].x; +ABS voxValue, voxValue; +MAD voxValue, voxValue, cmapXform[ 0].x, cmapXform[ 3].x; + +# Apply the modulation value +MUL voxValue, voxValue, modValue.x; + +# Use the vector values to look up the +# colours for each xyz direction +TEX xColour, voxValue.x, texture[2], 1D; +TEX yColour, voxValue.y, texture[3], 1D; +TEX zColour, voxValue.z, texture[4], 1D; + +# Cumulatively combine the rgb +# channels of those three colours +MOV fragColour, xColour; +ADD fragColour, fragColour, yColour; +ADD fragColour, fragColour, zColour; + +# Take the highest alpha of the three colour maps +MAX fragColour.a, xColour.a, yColour.a; +MAX fragColour.a, fragColour.a, zColour.a; + +# Clear the fragment if the modulation +# vaue does not meet the threshold +MOV modValue, modValue.x; +SUB modValue, modValue, modThres.x; +CMP fragColour, modValue.x, { 0, 0, 0, 0 }, fragColour; + +# Colour the pixel! +MOV result.color, fragColour; + +END + diff --git a/fsl/fslview/gl/gl14/glvector_vert.prog b/fsl/fslview/gl/gl14/glvector_vert.prog deleted file mode 100644 index f8210528d8b58960418a4317d99684b79c30463e..0000000000000000000000000000000000000000 --- a/fsl/fslview/gl/gl14/glvector_vert.prog +++ /dev/null @@ -1,6 +0,0 @@ -!!ARBvp1.0 -# -# Vertex program for rendering GLVector instances. -# -#pragma include common_vert.prog -END diff --git a/fsl/fslview/gl/gl14/glvolume_frag.prog b/fsl/fslview/gl/gl14/glvolume_frag.prog index 0e9bf8e0139c14d6ad3e77074c1937aa5d8d160f..4de3ebdece1915d5c16066d6c1aad06fff870beb 100644 --- a/fsl/fslview/gl/gl14/glvolume_frag.prog +++ b/fsl/fslview/gl/gl14/glvolume_frag.prog @@ -17,11 +17,8 @@ # # Required inputs: # -# fragment.texcoord[0] - Fragment position in the display coordinate system -# fragment.texcoord[1] - Fragment position in the image voxel coordinate -# system -# fragment.texcoord[2] - Optionally used as texture coordinates (see -# program.local[7]). +# fragment.texcoord[0] - Fragment texture coordinates +# fragment.texcoord[1] - Fragment voxel coordinates # # program.local[0] # program.local[1] @@ -31,40 +28,26 @@ # # program.local[4] - Image shape - number of voxels along the xyz # dimensions in the image -# program.local[5] - Inverse of image shape # -# program.local[6] - Vector containing global brightness (x), contrast -# (y), and alpha (z) values to pass to the -# briconalpha.prog routine. -# -# program.local[7] - Vector containing clipping values - voxels with a +# program.local[5] - Vector containing clipping values - voxels with a # value below the low threshold (x), or above the # high threshold (y) will not be shown. Assumed to # be normalised to the image texture value range. # -# program.local[8] - If greater than or equal to 0, fragment.texcoord[1] -# is used as the coordinates for the texture lookup. -# Otherwise, fragment.texcoord[2] is used for the -# texture lookup. # # Outputs: # -# result.color - The fragment colour (written by briconalpha.prog) +# result.color - The fragment colour # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # TEMP voxCoord; TEMP voxClip; -TEMP normVoxCoord; TEMP voxValue; -TEMP fragColour; -PARAM imageShape = program.local[4]; -PARAM imageShapeInv = program.local[5]; -PARAM bca = program.local[6]; -PARAM clipping = program.local[7]; -PARAM useTexCoords = program.local[8]; +PARAM imageShape = program.local[4]; +PARAM clipping = program.local[5]; # This matrix scales the voxel value to # lie in a range which is appropriate to @@ -75,27 +58,14 @@ PARAM voxValXform[4] = { program.local[0], program.local[3] }; # retrieve the voxel coordinates, -# which should have been calculated -# by the vertex program -# -# If useTexCoords is < 0, we use -# fragment.texcoord[2] for the -# texture lookup, otherwise we use -# fragment.texcoord[1]. -CMP voxCoord, useTexCoords, fragment.texcoord[2], fragment.texcoord[1]; +# bail if they are are out of bounds +MOV voxCoord, fragment.texcoord[1]; -# bail if the voxel coordinates -# are out of bounds #pragma include test_in_bounds.prog -# Normalise voxel coordinates to -# lie in the range (0, 1), so they -# can be used for texture lookup -MUL normVoxCoord, voxCoord, imageShapeInv; - # look up image voxel value # from 3D image texture -TEX voxValue, normVoxCoord, texture[0], 3D; +TEX voxValue, fragment.texcoord[0], texture[0], 3D; # If the voxel value is outside the # clipping range, don't draw it @@ -110,7 +80,7 @@ KIL voxClip; # Scale voxel value according # to the current display range -MAD voxValue, voxValue, voxValXform[0].x, voxValXform[0].w; +MAD voxValue, voxValue, voxValXform[0].x, voxValXform[3].x; # look up the appropriate colour # in the 1D colour map texture diff --git a/fsl/fslview/gl/gl14/glvolume_funcs.py b/fsl/fslview/gl/gl14/glvolume_funcs.py index b18f7a2c3f976ba4a7c9cfb9a7fd84b8b0a69564..0d42cd78be04ae5b7bebf65d2333e95ad46f75dc 100644 --- a/fsl/fslview/gl/gl14/glvolume_funcs.py +++ b/fsl/fslview/gl/gl14/glvolume_funcs.py @@ -14,20 +14,7 @@ This module depends upon two OpenGL ARB extensions, ARB_vertex_program and ARB_fragment_program which, being ancient (2002) technology, should be available on pretty much any graphics card in the wild today. -This module provides the following functions: - - - :func:`init`: Compiles the vertex/fragment programs used in rendering. - - - :func:`genVertexData`: Generates and returns vertex and texture coordinates - for rendering a single 2D slice of a 3D image. - - - :func:`preDraw: Prepares the GL state for drawing. - - - :func:`draw`: Renders the current image slice. - - - :func:`postDraw: Resets the GL state after drawing. - - - :func:`destroy`: Deletes handles to the vertex/fragment programs +See the :mod:`.gl21.glvolume_funcs` module for more details. This PDF is quite useful: - http://www.renderguild.com/gpuguide.pdf @@ -41,25 +28,41 @@ import OpenGL.raw.GL._types as gltypes import OpenGL.GL.ARB.fragment_program as arbfp import OpenGL.GL.ARB.vertex_program as arbvp -import fsl.utils.transform as transform -import fsl.fslview.gl.globject as globject -import fsl.fslview.gl.shaders as shaders +import fsl.utils.transform as transform +import fsl.fslview.gl.shaders as shaders log = logging.getLogger(__name__) -def init(self): - """Compiles the vertex and fragment programs used for rendering.""" +def compileShaders(self): - vertShaderSrc = shaders.getVertexShader( self) - fragShaderSrc = shaders.getFragmentShader(self) + if self.vertexProgram is not None: + arbvp.glDeleteProgramsARB(1, gltypes.GLuint(self.vertexProgram)) + + if self.fragmentProgram is not None: + arbfp.glDeleteProgramsARB(1, gltypes.GLuint(self.fragmentProgram)) + + vertShaderSrc = shaders.getVertexShader( self, + sw=self.display.softwareMode) + fragShaderSrc = shaders.getFragmentShader(self, + sw=self.display.softwareMode) vertexProgram, fragmentProgram = shaders.compilePrograms( vertShaderSrc, fragShaderSrc) self.vertexProgram = vertexProgram - self.fragmentProgram = fragmentProgram + self.fragmentProgram = fragmentProgram + + +def init(self): + """Compiles the vertex and fragment programs used for rendering.""" + + self.vertexProgram = None + self.fragmentProgram = None + + compileShaders( self) + updateShaderState(self) def destroy(self): @@ -68,27 +71,9 @@ def destroy(self): arbvp.glDeleteProgramsARB(1, gltypes.GLuint(self.vertexProgram)) arbfp.glDeleteProgramsARB(1, gltypes.GLuint(self.fragmentProgram)) - -def genVertexData(self): - """Generates vertex coordinates required to render the image. See - :func:`fsl.fslview.gl.glvolume.genVertexData`. - """ - - worldCoords, indices = self.genVertexData() - return worldCoords, indices, len(indices) - - -def preDraw(self): - """Prepares to draw a slice from the given - :class:`~fsl.fslview.gl.glvolume.GLVolume` instance. - """ - - display = self.display - opts = self.displayOpts - # enable drawing from a vertex array - gl.glEnableClientState(gl.GL_VERTEX_ARRAY) - gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY) +def updateShaderState(self): + opts = self.displayOpts # enable the vertex and fragment programs gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB) @@ -99,12 +84,6 @@ def preDraw(self): arbfp.glBindProgramARB(arbfp.GL_FRAGMENT_PROGRAM_ARB, self.fragmentProgram) - # The vertex program needs to be - # able to transform from display - # coordinates to voxel coordinates - shaders.setVertexProgramMatrix( - 0, display.getTransform('display', 'voxel').T) - # The voxValXform transformation turns # an image texture value into a raw # voxel value. The colourMapXform @@ -112,25 +91,11 @@ def preDraw(self): # into a value between 0 and 1, suitable # for looking up an appropriate colour # in the 1D colour map texture - cmapXForm = transform.concat(self.imageTexture.voxValXform, - self.colourMapXform) - - shaders.setFragmentProgramMatrix(0, cmapXForm.T) - - # The fragment program needs to know - # the image shape, and its inverse, - # because there's no division operation, - # and the RCP operation only works on - # scalars + voxValXform = transform.concat(self.imageTexture.voxValXform, + self.colourTexture.getCoordinateTransform()) - # We also need to pass the global - # brightness/contrast/alpha values to - # the fragment program - shape = list(self.image.shape) - invshape = [1.0 / s for s in shape] - bca = [display.brightness / 100.0, - display.contrast / 100.0, - display.alpha / 100.0] + # The fragment program needs to know the image shape + shape = list(self.image.shape[:3]) # And the clipping range, normalised # to the image texture value range @@ -140,36 +105,55 @@ def preDraw(self): clipHi = opts.clippingRange[1] * \ self.imageTexture.invVoxValXform[0, 0] + \ self.imageTexture.invVoxValXform[3, 0] + + shaders.setVertexProgramVector( 0, shape + [0]) - shaders.setFragmentProgramVector(4, shape + [0]) - shaders.setFragmentProgramVector(5, invshape + [0]) - shaders.setFragmentProgramVector(6, bca + [0]) - shaders.setFragmentProgramVector(7, [clipLo, clipHi, 0, 0]) + shaders.setFragmentProgramMatrix(0, voxValXform) + shaders.setFragmentProgramVector(4, shape + [0]) + shaders.setFragmentProgramVector(5, [clipLo, clipHi, 0, 0]) + gl.glDisable(arbvp.GL_VERTEX_PROGRAM_ARB) + gl.glDisable(arbfp.GL_FRAGMENT_PROGRAM_ARB) -def draw(self, zpos, xform=None): - """Draws a slice of the image at the given Z location. """ - worldCoords = self.worldCoords - indices = self.indices - worldCoords[:, self.zax] = zpos +def preDraw(self): + """Prepares to draw a slice from the given + :class:`~fsl.fslview.gl.glvolume.GLVolume` instance. + """ + + # enable drawing from a vertex array + gl.glEnableClientState(gl.GL_VERTEX_ARRAY) + + gl.glClientActiveTexture(gl.GL_TEXTURE0) + gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY) + + # enable the vertex and fragment programs + gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB) + gl.glEnable(arbfp.GL_FRAGMENT_PROGRAM_ARB) + + arbvp.glBindProgramARB(arbvp.GL_VERTEX_PROGRAM_ARB, + self.vertexProgram) + arbfp.glBindProgramARB(arbfp.GL_FRAGMENT_PROGRAM_ARB, + self.fragmentProgram) - # Apply the custom xform if provided. - if xform is None: - xform = np.eye(4) - shaders.setVertexProgramMatrix(4, xform.T) +def draw(self, zpos, xform=None): + """Draws a slice of the image at the given Z location. """ - worldCoords = worldCoords.ravel('C') + + vertices, voxCoords, texCoords = self.generateVertices(zpos, xform) + + # Tex coords are texture 0 coords + # Vox coords are texture 1 coords + vertices = np.array(vertices, dtype=np.float32).ravel('C') + texCoords = np.array(texCoords, dtype=np.float32).ravel('C') - gl.glActiveTexture(gl.GL_TEXTURE0) - gl.glTexCoordPointer(3, gl.GL_FLOAT, 0, worldCoords) - gl.glVertexPointer(3, gl.GL_FLOAT, 0, worldCoords) + gl.glVertexPointer(3, gl.GL_FLOAT, 0, vertices) - gl.glDrawElements(gl.GL_TRIANGLE_STRIP, - len(indices), - gl.GL_UNSIGNED_INT, - indices) + gl.glClientActiveTexture(gl.GL_TEXTURE0) + gl.glTexCoordPointer(3, gl.GL_FLOAT, 0, texCoords) + + gl.glDrawArrays(gl.GL_TRIANGLES, 0, 6) def drawAll(self, zposes, xforms): @@ -177,38 +161,25 @@ def drawAll(self, zposes, xforms): applying the corresponding transformation to each of the slices. """ - # Don't use a custom world-to-world - # transformation matrix. - shaders.setVertexProgramMatrix(4, np.eye(4)) - - # Instead, tell the vertex - # program to use texture coordinates - shaders.setFragmentProgramVector(8, -np.ones(4)) + nslices = len(zposes) + vertices = np.zeros((nslices * 6, 3), dtype=np.float32) + texCoords = np.zeros((nslices * 6, 3), dtype=np.float32) - worldCoords = np.array(self.worldCoords) - indices = np.array(self.indices) - - # The world coordinates are ordered to - # be rendered as a triangle strip, but - # we want to render as quads. So we - # need to re-order them - worldCoords[[2, 3], :] = worldCoords[[3, 2], :] - - # Replicate the world coordinates - # across all z positions, and with - # each corresponding transformation - worldCoords, texCoords, indices = globject.broadcast( - worldCoords, indices, zposes, xforms, self.zax) - - worldCoords = worldCoords.ravel('C') - texCoords = texCoords .ravel('C') - - # Draw all of the slices with - # these four function calls. - gl.glActiveTexture(gl.GL_TEXTURE0) + for i, (zpos, xform) in enumerate(zip(zposes, xforms)): + + v, vc, tc = self.generateVertices(zpos, xform) + vertices[ i * 6: i * 6 + 6, :] = v + texCoords[i * 6: i * 6 + 6, :] = tc + + vertices = vertices .ravel('C') + texCoords = texCoords.ravel('C') + + gl.glVertexPointer(3, gl.GL_FLOAT, 0, vertices) + + gl.glClientActiveTexture(gl.GL_TEXTURE0) gl.glTexCoordPointer(3, gl.GL_FLOAT, 0, texCoords) - gl.glVertexPointer( 3, gl.GL_FLOAT, 0, worldCoords) - gl.glDrawElements(gl.GL_QUADS, len(indices), gl.GL_UNSIGNED_INT, indices) + + gl.glDrawArrays(gl.GL_TRIANGLES, 0, nslices * 6) def postDraw(self): @@ -217,6 +188,7 @@ def postDraw(self): """ gl.glDisableClientState(gl.GL_VERTEX_ARRAY) + gl.glClientActiveTexture(gl.GL_TEXTURE0) gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY) gl.glDisable(arbfp.GL_FRAGMENT_PROGRAM_ARB) diff --git a/fsl/fslview/gl/gl14/glvolume_sw_frag.prog b/fsl/fslview/gl/gl14/glvolume_sw_frag.prog new file mode 100644 index 0000000000000000000000000000000000000000..f7c28841ec5b2f153793d6d775b05c311a3a0d63 --- /dev/null +++ b/fsl/fslview/gl/gl14/glvolume_sw_frag.prog @@ -0,0 +1,67 @@ +!!ARBfp1.0 +# +# Fragment program used for rendering GLVolume instances. +# +# This fragment program does the following: +# +# - Retrieves the display space/voxel coordinates corresponding to the +# fragment +# +# - Uses those voxel coordinates to look up the corresponding voxel +# value in the 3D image texture. +# +# - Uses that voxel value to look up the corresponding colour in the +# 1D colour map texture. +# +# - Sets the fragment colour. +# +# Required inputs: +# +# fragment.texcoord[0] - Fragment texture coordinates +# +# program.local[0] +# program.local[1] +# program.local[2] +# program.local[3] - Matrix which transforms voxel values into the range +# [0, 1], for use as a colour map texture coordinate +# +# program.local[5] - Vector containing clipping values - voxels with a +# value below the low threshold (x), or above the +# high threshold (y) will not be shown. Assumed to +# be normalised to the image texture value range. +# +# +# Outputs: +# +# result.color - The fragment colour +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +TEMP voxClip; +TEMP voxValue; + +# look up image voxel value +# from 3D image texture +TEX voxValue, fragment.texcoord[0], texture[0], 3D; + +# If the voxel value is outside the +# clipping range, don't draw it + +# Test the low clipping range +SUB voxClip, voxValue.x, program.local[5].x; +KIL voxClip; + +# And the high clipping range +SUB voxClip, program.local[5].y, voxValue.x; +KIL voxClip; + +# Scale voxel value according +# to the current display range +MAD voxValue, voxValue, program.local[0].x, program.local[3].x; + +# look up the appropriate colour +# in the 1D colour map texture +TEX result.color, voxValue.x, texture[1], 1D; + +END diff --git a/fsl/fslview/gl/gl14/glvolume_sw_vert.prog b/fsl/fslview/gl/gl14/glvolume_sw_vert.prog new file mode 100644 index 0000000000000000000000000000000000000000..c5040438129255d30ff815c9d0d98b565eab04c9 --- /dev/null +++ b/fsl/fslview/gl/gl14/glvolume_sw_vert.prog @@ -0,0 +1,23 @@ +!!ARBvp1.0 +# +# Vertex program for rendering GLVolume instances. +# +# Inputs: +# state.matrix.mvp +# vertex.position +# vertex.texcoord[0] +# +# +# Outputs: +# result.position +# + +# Transform the vertex position into display coordinates +DP4 result.position.x, state.matrix.mvp.row[0], vertex.position; +DP4 result.position.y, state.matrix.mvp.row[1], vertex.position; +DP4 result.position.z, state.matrix.mvp.row[2], vertex.position; +DP4 result.position.w, state.matrix.mvp.row[3], vertex.position; + +MOV result.texcoord[0], vertex.texcoord[0]; + +END diff --git a/fsl/fslview/gl/gl14/glvolume_vert.prog b/fsl/fslview/gl/gl14/glvolume_vert.prog index 3d0bbff43755ee4c1d43499a3282da2d90e9ba58..91fa583fc3174431174b13ffd8cd345c3fd924cb 100644 --- a/fsl/fslview/gl/gl14/glvolume_vert.prog +++ b/fsl/fslview/gl/gl14/glvolume_vert.prog @@ -2,5 +2,35 @@ # # Vertex program for rendering GLVolume instances. # -#pragma include common_vert.prog +# Inputs: +# state.matrix.mvp +# vertex.position +# vertex.texcoord[0] +# program.local[0] - image shape +# +# +# Outputs: +# result.position +# result.texcoord[0] +# result.texcoord[1] +# + +PARAM imageShape = program.local[0]; + +TEMP voxCoord; + +# Transform the vertex position into display coordinates +DP4 result.position.x, state.matrix.mvp.row[0], vertex.position; +DP4 result.position.y, state.matrix.mvp.row[1], vertex.position; +DP4 result.position.z, state.matrix.mvp.row[2], vertex.position; +DP4 result.position.w, state.matrix.mvp.row[3], vertex.position; + +MOV result.texcoord[0], vertex.texcoord[0]; + +# Transform the voxel coordinates into texture coordinates +MOV voxCoord, vertex.texcoord[0]; +MUL voxCoord, voxCoord, imageShape; + +MOV result.texcoord[1], voxCoord; + END diff --git a/fsl/fslview/gl/gl21/__init__.py b/fsl/fslview/gl/gl21/__init__.py index 5f5a552bc887d6b2249a90bd70cc2f40004c756b..360c9cafe687bebc15183151878599ac7ae73b12 100644 --- a/fsl/fslview/gl/gl21/__init__.py +++ b/fsl/fslview/gl/gl21/__init__.py @@ -6,4 +6,5 @@ # import glvolume_funcs -import glvector_funcs +import glrgbvector_funcs +import gllinevector_funcs diff --git a/fsl/fslview/gl/gl21/briconalpha.glsl b/fsl/fslview/gl/gl21/briconalpha.glsl deleted file mode 100644 index 16c5b95a3bd957ce1f5e4a3ad9cd59317f995fb5..0000000000000000000000000000000000000000 --- a/fsl/fslview/gl/gl21/briconalpha.glsl +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Provides the briconalpha function, which applies global brightness, - * contrast and alpha settings to a specified colour. - * - * Author: Paul McCarthy <pauldmccarthy@gmail.com> - */ - -/* - * Brightness factor, assumed to lie between 0.0 and 1.0. - */ -uniform float brightness; - -/* - * Contrast factor, assumed to lie between 0.0 and 1.0. - */ -uniform float contrast; - -/* - * Opacity, assumed to lie between 0.0 and 1.0. - */ -uniform float alpha; - - -vec4 briconalpha(vec4 inputColour) { - - float scale; - float offset; - vec4 outputColour = vec4(inputColour); - - if (outputColour.a >= 1.0) - outputColour.a = alpha; - - /* - * The brightness is applied as a linear offset, - * with 0.5 equivalent to an offset of 0.0. - */ - offset = (brightness * 2 - 1); - - /* - * If the contrast lies between 0.0 and 0.5, it is - * applied to the colour as a linear scaling factor. - */ - scale = contrast * 2; - - /* - * If the contrast lies between 0.5 and 0.1, it - * is applied as an exponential scaling factor, - * so lower values (closer to 0.5) have less of - * an effect than higher values (closer to 1.0). - */ - if (contrast > 0.5) - scale += exp((contrast - 0.5) * 6) - 1; - - /* - * The contrast factor scales the existing colour - * range, but keeps the new range centred at 0.5. - */ - outputColour.rgb += offset; - - outputColour.rgb = clamp(outputColour.rgb, 0.0, 1.0); - outputColour.rgb = (outputColour.rgb - 0.5) * scale + 0.5; - - return clamp(outputColour, 0.0, 1.0); -} diff --git a/fsl/fslview/gl/gl21/common_vert.glsl b/fsl/fslview/gl/gl21/common_vert.glsl deleted file mode 100644 index 37246e94908236a9df2e528dee206ba7d0813e47..0000000000000000000000000000000000000000 --- a/fsl/fslview/gl/gl21/common_vert.glsl +++ /dev/null @@ -1,85 +0,0 @@ -/** - * The common_vert function (and associated uniform/attribute/varying - * definitions) contains logic which is common to all vertex shader - * programs. - * - * The common_vert function calculates and sets vertex/texture - * coordinates, for pass-through to the fragment shader, in both - * screen space and voxel space. - * - * Author: Paul McCarthy <pauldmccarthy@gmail.com> - */ - -uniform mat4 displayToVoxMat; - -/* - * Optional transformation matrix which is applied to all - * vertex coordinates (used by the e.g. lightbox canvas to - * organise individual slices in a row/column fashion). - */ -uniform mat4 worldToWorldMat; - -/* - * Display axes (xax = horizontal, yax = vertical, zax = depth) - */ -uniform int xax; -uniform int yax; -uniform int zax; - -/* - * X/Y vertex location - */ -attribute vec2 worldCoords; - -/* - * Z location - */ -uniform float zCoord; - -/* - * Vertex display coordinates passed through to fragment shader. - */ -varying vec3 fragDisplayCoords; - -/* - * Image voxel coordinates corresponding to this vertex. - */ -varying vec3 fragVoxCoords; - - -void common_vert(void) { - - vec4 worldLoc = vec4(0, 0, 0, 1); - worldLoc[xax] = worldCoords.x; - worldLoc[yax] = worldCoords.y; - worldLoc[zax] = zCoord; - - /* - * Transform the texture coordinates into voxel coordinates - */ - fragVoxCoords = (displayToVoxMat * worldLoc).xyz; - - /* - * Centre voxel coordinates - the display space of a voxel - * at a particular location (x, y, z) extends from - * - * (x-0.5, y-0.5, z-0.5) - * - * to - * - * (x+0.5, y+0.5, z+0.5), - * - * so we need to offset the coordinates by 0.5 to - * make the coordinates usable as voxel indices - */ - fragVoxCoords += 0.5; - - /* - * Pass the vertex coordinates as texture - * coordinates to the fragment shader - */ - fragDisplayCoords = worldLoc.xyz; - - /* Transform the vertex coordinates to display space */ - gl_Position = gl_ModelViewProjectionMatrix * worldToWorldMat * worldLoc; -} diff --git a/fsl/fslview/gl/gl21/gllinevector_funcs.py b/fsl/fslview/gl/gl21/gllinevector_funcs.py new file mode 100644 index 0000000000000000000000000000000000000000..b0967f35cf79212e171a8b00b4de0b740d39c92a --- /dev/null +++ b/fsl/fslview/gl/gl21/gllinevector_funcs.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python +# +# gllinevector_funcs.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import logging + +import numpy as np +import OpenGL.GL as gl +import OpenGL.raw.GL._types as gltypes + +import fsl.utils.transform as transform +import fsl.fslview.gl.resources as glresources +import fsl.fslview.gl.routines as glroutines +import fsl.fslview.gl.gllinevector as gllinevector +import fsl.fslview.gl.shaders as shaders + + +log = logging.getLogger(__name__) + + +def init(self): + + self.shaders = None + self.vertexBuffer = gl.glGenBuffers(1) + self.texCoordBuffer = gl.glGenBuffers(1) + self.vertexIDBuffer = gl.glGenBuffers(1) + self.lineVertices = None + + self._vertexResourceName = '{}_{}_vertices'.format( + type(self).__name__, id(self.image)) + + display = self.display + opts = self.opts + + def vertexUpdate(*a): + + updateVertices(self) + self.updateShaderState() + self.onUpdate() + + display.addListener('transform', self.name, vertexUpdate) + display.addListener('resolution', self.name, vertexUpdate) + opts .addListener('directed', self.name, vertexUpdate) + + compileShaders( self) + updateShaderState(self) + + +def destroy(self): + gl.glDeleteBuffers(1, gltypes.GLuint(self.vertexBuffer)) + gl.glDeleteBuffers(1, gltypes.GLuint(self.vertexIDBuffer)) + gl.glDeleteBuffers(1, gltypes.GLuint(self.texCoordBuffer)) + gl.glDeleteProgram(self.shaders) + + self.display.removeListener('transform', self.name) + self.display.removeListener('resolution', self.name) + self.opts .removeListener('directed', self.name) + + if self.display.softwareMode: + glresources.delete(self._vertexResourceName) + + +def compileShaders(self): + + if self.shaders is not None: + gl.glDeleteProgram(self.shaders) + + vertShaderSrc = shaders.getVertexShader( self, + sw=self.display.softwareMode) + fragShaderSrc = shaders.getFragmentShader(self, + sw=self.display.softwareMode) + + self.shaders = shaders.compileShaders(vertShaderSrc, fragShaderSrc) + + self.vertexPos = gl.glGetAttribLocation( self.shaders, + 'vertex') + self.vertexIDPos = gl.glGetAttribLocation( self.shaders, + 'vertexID') + self.texCoordPos = gl.glGetAttribLocation( self.shaders, + 'texCoord') + self.imageShapePos = gl.glGetUniformLocation(self.shaders, + 'imageShape') + self.imageDimsPos = gl.glGetUniformLocation(self.shaders, + 'imageDims') + self.directedPos = gl.glGetUniformLocation(self.shaders, + 'directed') + self.imageTexturePos = gl.glGetUniformLocation(self.shaders, + 'imageTexture') + self.modTexturePos = gl.glGetUniformLocation(self.shaders, + 'modTexture') + self.xColourTexturePos = gl.glGetUniformLocation(self.shaders, + 'xColourTexture') + self.yColourTexturePos = gl.glGetUniformLocation(self.shaders, + 'yColourTexture') + self.zColourTexturePos = gl.glGetUniformLocation(self.shaders, + 'zColourTexture') + self.modThresholdPos = gl.glGetUniformLocation(self.shaders, + 'modThreshold') + self.useSplinePos = gl.glGetUniformLocation(self.shaders, + 'useSpline') + self.voxValXformPos = gl.glGetUniformLocation(self.shaders, + 'voxValXform') + self.voxToDisplayMatPos = gl.glGetUniformLocation(self.shaders, + 'voxToDisplayMat') + self.displayToVoxMatPos = gl.glGetUniformLocation(self.shaders, + 'displayToVoxMat') + self.cmapXformPos = gl.glGetUniformLocation(self.shaders, + 'cmapXform') + + updateVertices(self) + + +def updateShaderState(self): + + display = self.display + opts = self.displayOpts + + # The coordinate transformation matrices for + # each of the three colour textures are identical, + # so we'll just use the xColourTexture matrix + cmapXform = self.xColourTexture.getCoordinateTransform() + voxValXform = self.imageTexture.voxValXform + useSpline = display.interpolation == 'spline' + imageShape = np.array(self.image.shape[:3], dtype=np.float32) + + voxValXform = np.array(voxValXform, dtype=np.float32).ravel('C') + cmapXform = np.array(cmapXform, dtype=np.float32).ravel('C') + + gl.glUseProgram(self.shaders) + + gl.glUniform1f( self.useSplinePos, useSpline) + gl.glUniform3fv(self.imageShapePos, 1, imageShape) + + gl.glUniformMatrix4fv(self.voxValXformPos, 1, False, voxValXform) + gl.glUniformMatrix4fv(self.cmapXformPos, 1, False, cmapXform) + + gl.glUniform1f(self.modThresholdPos, opts.modThreshold / 100.0) + + gl.glUniform1i(self.imageTexturePos, 0) + gl.glUniform1i(self.modTexturePos, 1) + gl.glUniform1i(self.xColourTexturePos, 2) + gl.glUniform1i(self.yColourTexturePos, 3) + gl.glUniform1i(self.zColourTexturePos, 4) + + if not display.softwareMode: + + directed = opts.directed + imageDims = self.image.pixdim[:3] + d2vMat = display.getTransform('display', 'voxel') + v2dMat = display.getTransform('voxel', 'display') + + imageDims = np.array(imageDims, dtype=np.float32) + d2vMat = np.array(d2vMat, dtype=np.float32).ravel('C') + v2dMat = np.array(v2dMat, dtype=np.float32).ravel('C') + + gl.glUniform1f( self.directedPos, directed) + gl.glUniform3fv(self.imageDimsPos, 1, imageDims) + + gl.glUniformMatrix4fv(self.displayToVoxMatPos, 1, False, d2vMat) + gl.glUniformMatrix4fv(self.voxToDisplayMatPos, 1, False, v2dMat) + + gl.glUseProgram(0) + + +def updateVertices(self): + + image = self.image + display = self.display + opts = self.opts + + if not display.softwareMode: + + self.lineVertices = None + + if glresources.exists(self._vertexResourceName): + log.debug('Clearing any cached line vertices for {}'.format(image)) + glresources.delete(self._vertexResourceName) + return + + if self.lineVertices is None: + self.lineVertices = glresources.get( + self._vertexResourceName, gllinevector.GLLineVertices, self) + + newHash = (hash(display.transform) ^ + hash(display.resolution) ^ + hash(opts .directed)) + + if hash(self.lineVertices) != newHash: + + log.debug('Re-generating line vertices for {}'.format(image)) + self.lineVertices.refresh(self) + glresources.set(self._vertexResourceName, + self.lineVertices, + overwrite=True) + + +def preDraw(self): + gl.glUseProgram(self.shaders) + + +def draw(self, zpos, xform=None): + if self.display.softwareMode: softwareDraw(self, zpos, xform) + else: hardwareDraw(self, zpos, xform) + + +def softwareDraw(self, zpos, xform=None): + + opts = self.displayOpts + vertices, texCoords = self.lineVertices.getVertices(self, zpos) + + if vertices.size == 0: + return + + vertices = vertices .ravel('C') + texCoords = texCoords.ravel('C') + + v2d = self.display.getTransform('voxel', 'display') + + if xform is None: xform = v2d + else: xform = transform.concat(v2d, xform) + + gl.glPushMatrix() + gl.glMultMatrixf(np.array(xform, dtype=np.float32).ravel('C')) + + # upload the vertices + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertexBuffer) + gl.glBufferData( + gl.GL_ARRAY_BUFFER, vertices.nbytes, vertices, gl.GL_STATIC_DRAW) + gl.glVertexAttribPointer( + self.vertexPos, 3, gl.GL_FLOAT, gl.GL_FALSE, 0, None) + gl.glEnableVertexAttribArray(self.vertexPos) + + # and the texture coordinates + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.texCoordBuffer) + gl.glBufferData( + gl.GL_ARRAY_BUFFER, texCoords.nbytes, texCoords, gl.GL_STATIC_DRAW) + gl.glVertexAttribPointer( + self.texCoordPos, 3, gl.GL_FLOAT, gl.GL_FALSE, 0, None) + gl.glEnableVertexAttribArray(self.texCoordPos) + + gl.glLineWidth(opts.lineWidth) + gl.glDrawArrays(gl.GL_LINES, 0, vertices.size / 3) + + gl.glPopMatrix() + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0) + gl.glDisableVertexAttribArray(self.vertexPos) + + +def hardwareDraw(self, zpos, xform=None): + + image = self.image + display = self.display + opts = self.displayOpts + v2dMat = self.display.getTransform('voxel', 'display') + resolution = np.array([display.resolution] * 3) + + if display.transform == 'id': + resolution = resolution / min(image.pixdim[:3]) + elif display.transform == 'pixdim': + resolution = map(lambda r, p: max(r, p), resolution, image.pixdim[:3]) + + vertices = glroutines.calculateSamplePoints( + image.shape, + resolution, + v2dMat, + self.xax, + self.yax)[0] + + vertices[:, self.zax] = zpos + + vertices = np.repeat(vertices, 2, 0) + indices = np.arange(vertices.shape[0], dtype=np.uint32) + vertices = vertices.ravel('C') + + if xform is None: xform = v2dMat + else: xform = transform.concat(v2dMat, xform) + + xform = np.array(xform, dtype=np.float32).ravel('C') + gl.glUniformMatrix4fv(self.voxToDisplayMatPos, 1, False, xform) + + # bind the vertex ID buffer + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertexIDBuffer) + gl.glBufferData( + gl.GL_ARRAY_BUFFER, indices.nbytes, indices, gl.GL_STATIC_DRAW) + gl.glVertexAttribPointer( + self.vertexIDPos, 1, gl.GL_UNSIGNED_INT, gl.GL_FALSE, 0, None) + gl.glEnableVertexAttribArray(self.vertexIDPos) + + # and the vertex buffer + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertexBuffer) + gl.glBufferData( + gl.GL_ARRAY_BUFFER, vertices.nbytes, vertices, gl.GL_STATIC_DRAW) + gl.glVertexAttribPointer( + self.vertexPos, 3, gl.GL_FLOAT, gl.GL_FALSE, 0, None) + + gl.glEnableVertexAttribArray(self.vertexPos) + gl.glEnableVertexAttribArray(self.vertexIDPos) + + gl.glLineWidth(opts.lineWidth) + gl.glDrawArrays(gl.GL_LINES, 0, vertices.size / 3) + + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0) + gl.glDisableVertexAttribArray(self.vertexPos) + gl.glDisableVertexAttribArray(self.vertexIDPos) + + +def drawAll(self, zposes, xforms): + + for zpos, xform in zip(zposes, xforms): + self.draw(zpos, xform) + + +def postDraw(self): + gl.glUseProgram(0) diff --git a/fsl/fslview/gl/gl21/gllinevector_sw_vert.glsl b/fsl/fslview/gl/gl21/gllinevector_sw_vert.glsl new file mode 100644 index 0000000000000000000000000000000000000000..b82395d2b5d3b50eebe8e1ecdc709867a9377154 --- /dev/null +++ b/fsl/fslview/gl/gl21/gllinevector_sw_vert.glsl @@ -0,0 +1,19 @@ +/* + * OpenGL vertex shader used for rendering GLLineVector instances. + * + * Author: Paul McCarthy <pauldmccarthy@gmail.com> + */ +#version 120 + +attribute vec3 vertex; + +attribute vec3 texCoord; + +varying vec3 fragTexCoord; + +void main(void) { + + fragTexCoord = texCoord; + + gl_Position = gl_ModelViewProjectionMatrix * vec4(vertex, 1); +} diff --git a/fsl/fslview/gl/gl21/gllinevector_vert.glsl b/fsl/fslview/gl/gl21/gllinevector_vert.glsl new file mode 100644 index 0000000000000000000000000000000000000000..ad5865da1719f205352cf6d884c35cf530550543 --- /dev/null +++ b/fsl/fslview/gl/gl21/gllinevector_vert.glsl @@ -0,0 +1,130 @@ +/* + * OpenGL vertex shader used for rendering GLVector instances in + * line mode. + * + * Author: Paul McCarthy <pauldmccarthy@gmail.com> + */ +#version 120 + +#pragma include spline_interp.glsl + +/* + * Vector image containing XYZ vector data. + */ +uniform sampler3D imageTexture; + + +uniform mat4 displayToVoxMat; +uniform mat4 voxToDisplayMat; + + +/* + * Transformation matrix which transforms the + * vector texture data to its original data range. + */ +uniform mat4 voxValXform; + + +/* + * Shape of the image texture. + */ +uniform vec3 imageShape; + + +uniform bool directed; + +/* + * Dimensions of one voxel in the image texture. + */ +uniform vec3 imageDims; + + +attribute vec3 vertex; + +/* + * Vertex index - the built-in gl_VertexID + * variable is not available in GLSL 120 + */ +attribute float vertexID; + + +varying vec3 fragVoxCoord; +varying vec3 fragTexCoord; + + +void main(void) { + + vec3 texCoord; + vec3 vector; + vec3 voxCoord; + vec3 vertVoxCoord; + + /* + * The vertVoxCoord vector contains the floating + * point voxel coordinates which correspond to the + * display coordinates of the current vertex. + */ + vertVoxCoord = (displayToVoxMat * vec4(vertex, 1)).xyz; + + /* + * The voxCoord vector contains the exact integer + * voxel coordinates - we cannot interpolate vector + * directions. + * + * There is no round function in GLSL 1.2, so we use + * floor(x + 0.5). + */ + voxCoord = floor(vertVoxCoord + 0.5); + + /* + * Normalise the voxel coordinates to [0.0, 1.0], + * so they can be used for texture lookup. Add + * 0.5 to the voxel coordinates first, to re-centre + * voxel coordinates from from [i - 0.5, i + 0.5] + * to [i, i + 1]. + */ + texCoord = (voxCoord + 0.5) / imageShape; + + /* + * Retrieve the vector values for this voxel + */ + vector = texture3D(imageTexture, texCoord).xyz; + + /* + * Transform the vector values from their + * texture range of [0,1] to the original + * data range + */ + vector *= voxValXform[0].x; + vector += voxValXform[3].x; + + // Scale the vector so it has length 0.5 + vector /= 2 * length(vector); + + /* + * Scale the vector by the minimum voxel length, + * so it is a unit vector within real world space + */ + vector /= imageDims / min(imageDims.x, min(imageDims.y, imageDims.z)); + + /* + * Vertices are coming in as line pairs - flip + * every second vertex about the origin + */ + if (mod(vertexID, 2) == 1) { + if (directed) vector = vec3(0, 0, 0); + else vector = -vector; + } + + /* + * Output the final vertex position - offset + * the voxel coordinates by the vector values, + * and transform back to display coordinates + */ + gl_Position = gl_ModelViewProjectionMatrix * + voxToDisplayMat * + vec4(vertVoxCoord + vector, 1); + + fragVoxCoord = voxCoord; + fragTexCoord = texCoord; +} diff --git a/fsl/fslview/gl/gl21/glrgbvector_funcs.py b/fsl/fslview/gl/gl21/glrgbvector_funcs.py new file mode 100644 index 0000000000000000000000000000000000000000..65378fd73906d1a6ffade780efc00a7a476b554d --- /dev/null +++ b/fsl/fslview/gl/gl21/glrgbvector_funcs.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +# +# glrgbvector_funcs.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import numpy as np +import OpenGL.GL as gl +import OpenGL.raw.GL._types as gltypes + +import fsl.fslview.gl.shaders as shaders +import glvolume_funcs + + +def init(self): + self.shaders = None + + compileShaders( self) + updateShaderState(self) + + self.vertexAttrBuffer = gl.glGenBuffers(1) + + +def destroy(self): + gl.glDeleteBuffers(1, gltypes.GLuint(self.vertexAttrBuffer)) + gl.glDeleteProgram(self.shaders) + + +def compileShaders(self): + + if self.shaders is not None: + gl.glDeleteProgram(self.shaders) + + vertShaderSrc = shaders.getVertexShader( self, + sw=self.display.softwareMode) + fragShaderSrc = shaders.getFragmentShader(self, + sw=self.display.softwareMode) + + self.shaders = shaders.compileShaders(vertShaderSrc, fragShaderSrc) + + self.vertexPos = gl.glGetAttribLocation( self.shaders, + 'vertex') + self.voxCoordPos = gl.glGetAttribLocation( self.shaders, + 'voxCoord') + self.texCoordPos = gl.glGetAttribLocation( self.shaders, + 'texCoord') + self.imageTexturePos = gl.glGetUniformLocation(self.shaders, + 'imageTexture') + self.modTexturePos = gl.glGetUniformLocation(self.shaders, + 'modTexture') + self.xColourTexturePos = gl.glGetUniformLocation(self.shaders, + 'xColourTexture') + self.yColourTexturePos = gl.glGetUniformLocation(self.shaders, + 'yColourTexture') + self.zColourTexturePos = gl.glGetUniformLocation(self.shaders, + 'zColourTexture') + self.modThresholdPos = gl.glGetUniformLocation(self.shaders, + 'modThreshold') + self.useSplinePos = gl.glGetUniformLocation(self.shaders, + 'useSpline') + self.imageShapePos = gl.glGetUniformLocation(self.shaders, + 'imageShape') + self.voxValXformPos = gl.glGetUniformLocation(self.shaders, + 'voxValXform') + self.cmapXformPos = gl.glGetUniformLocation(self.shaders, + 'cmapXform') + + +def updateShaderState(self): + + display = self.display + opts = self.displayOpts + + # The coordinate transformation matrices for + # each of the three colour textures are identical + voxValXform = self.imageTexture.voxValXform + cmapXform = self.xColourTexture.getCoordinateTransform() + useSpline = display.interpolation == 'spline' + imageShape = np.array(self.image.shape, dtype=np.float32) + + gl.glUseProgram(self.shaders) + + gl.glUniform1f( self.useSplinePos, useSpline) + gl.glUniform3fv(self.imageShapePos, 1, imageShape) + + gl.glUniformMatrix4fv(self.voxValXformPos, 1, False, voxValXform) + gl.glUniformMatrix4fv(self.cmapXformPos, 1, False, cmapXform) + + gl.glUniform1f(self.modThresholdPos, opts.modThreshold / 100.0) + gl.glUniform1i(self.imageTexturePos, 0) + gl.glUniform1i(self.modTexturePos, 1) + gl.glUniform1i(self.xColourTexturePos, 2) + gl.glUniform1i(self.yColourTexturePos, 3) + gl.glUniform1i(self.zColourTexturePos, 4) + + gl.glUseProgram(0) + + +preDraw = glvolume_funcs.preDraw +draw = glvolume_funcs.draw +drawAll = glvolume_funcs.drawAll +postDraw = glvolume_funcs.postDraw diff --git a/fsl/fslview/gl/gl21/glvector_frag.glsl b/fsl/fslview/gl/gl21/glvector_frag.glsl index 5e21fdf2e667a17853315ff705012a014cb41824..971f5c9f4557b79cf3c6484f9130ad5cc2e19504 100644 --- a/fsl/fslview/gl/gl21/glvector_frag.glsl +++ b/fsl/fslview/gl/gl21/glvector_frag.glsl @@ -1,6 +1,6 @@ /* - * OpenGL fragment shader used for colouring GLVector instances in - * both line and rgb modes. + * OpenGL fragment shader used for colouring GLRGBVector and GLLineVector + * instances. * * Author: Paul McCarthy <pauldmccarthy@gmail.com> */ @@ -8,7 +8,6 @@ #pragma include spline_interp.glsl #pragma include test_in_bounds.glsl -#pragma include briconalpha.glsl /* * Vector image containing XYZ vector data. @@ -47,7 +46,10 @@ uniform sampler1D zColourTexture; * Matrix which transforms from vector image * texture values to their original data range. */ -uniform mat4 imageValueXform; +uniform mat4 voxValXform; + + +uniform mat4 cmapXform; /* * Shape of the image texture. @@ -59,23 +61,21 @@ uniform vec3 imageShape; */ uniform bool useSpline; - /* - * Coordinates of the fragment in display + * Coordinates of the fragment in voxel * coordinates, passed from the vertex shader. */ -varying vec3 fragDisplayCoords; +varying vec3 fragVoxCoord; /* - * Coordinates of the fragment in voxel - * coordinates, passed from the vertex shader. + * Corresponding texture coordinates */ -varying vec3 fragVoxCoords; +varying vec3 fragTexCoord; void main(void) { - vec3 voxCoords = fragVoxCoords; + vec3 voxCoords = fragVoxCoord; if (!test_in_bounds(voxCoords, imageShape)) { @@ -83,41 +83,41 @@ void main(void) { return; } - /* - * Normalise voxel coordinates to (0.0, 1.0) - */ - voxCoords = voxCoords / imageShape; - /* * Look up the xyz vector values */ vec3 voxValue; if (useSpline) { - voxValue.x = spline_interp(imageTexture, voxCoords, imageShape, 0); - voxValue.y = spline_interp(imageTexture, voxCoords, imageShape, 1); - voxValue.z = spline_interp(imageTexture, voxCoords, imageShape, 2); + voxValue.x = spline_interp(imageTexture, fragTexCoord, imageShape, 0); + voxValue.y = spline_interp(imageTexture, fragTexCoord, imageShape, 1); + voxValue.z = spline_interp(imageTexture, fragTexCoord, imageShape, 2); } else { - voxValue = texture3D(imageTexture, voxCoords).xyz; + voxValue = texture3D(imageTexture, fragTexCoord).xyz; + } + + /* Look up the modulation value */ + float modValue; + if (useSpline) { + modValue = spline_interp(modTexture, fragTexCoord, imageShape, 0); } + else { + modValue = texture3D(modTexture, fragTexCoord).x; + } /* * Transform the voxel texture values * into a range suitable for colour texture * lookup, and take the absolute value */ - voxValue *= imageValueXform[0].x; - voxValue += imageValueXform[0].w; + voxValue *= voxValXform[0].x; + voxValue += voxValXform[3].x; voxValue = abs(voxValue); + voxValue *= cmapXform[0].x; + voxValue += cmapXform[3].x; - /* Look up the modulation value */ - float modValue; - if (useSpline) { - modValue = spline_interp(modTexture, voxCoords, imageShape, 0); - } - else { - modValue = texture3D(modTexture, voxCoords).x; - } + /* Apply the modulation value */ + voxValue *= modValue; /* Look up the colours for the xyz components */ vec4 xColour = texture1D(xColourTexture, voxValue.x); @@ -127,11 +127,12 @@ void main(void) { /* Combine those colours */ vec4 voxColour = xColour + yColour + zColour; - /* Apply the modulation value */ - voxColour.rgb = voxColour.rgb * modValue; + /* Take the highest alpha of the three colour maps */ + voxColour.a = max(max(xColour.a, yColour.a), zColour.a); + /* Knock out voxels where the modulation value is below the threshold */ if (modValue < modThreshold) - voxColour.a = 0.0; + voxColour.a = 0.0; - gl_FragColor = briconalpha(voxColour); + gl_FragColor = voxColour; } diff --git a/fsl/fslview/gl/gl21/glvector_funcs.py b/fsl/fslview/gl/gl21/glvector_funcs.py deleted file mode 100644 index 87324522f813b857e296ccd1536f23bd44c200a4..0000000000000000000000000000000000000000 --- a/fsl/fslview/gl/gl21/glvector_funcs.py +++ /dev/null @@ -1,287 +0,0 @@ -#!/usr/bin/env python -# -# glvector_funcs.py - Logic for rendering GLVector instances in an OpenGL 2.1 -# compatible manner. -# -# Author: Paul McCarthy <pauldmccarthy@gmail.com> -# -"""This module contains functions used by the -:class:`~fsl.fslview.gl.glvector.GLVector` class, for rendering -:class:`~fsl.data.image.Image` instances as vectors in an OpenGL 2.1 -compatible manner. - -See the ``GLVector`` documentation for more details. - -This OpenGL 2.1 implementation improves upon the OpenGL 1.4 implementation -(see :mod:`~fsl.fslview.gl.gl14.glvector_funcs`) in that, in ``line`` mode, -the vertices which represent vector lines are positioned by a custom vertex -shader running on the GPU. -""" - -import numpy as np -import OpenGL.GL as gl -import OpenGL.raw.GL._types as gltypes - -import fsl.fslview.gl.shaders as shaders -import fsl.fslview.gl.globject as globject - - -def init(self): - """Compiles the vertex/fragment shaders used for rendering. A custom - vertex shader is used for ``line`` mode, but the same fragment shader - is used for both ``line`` and ``rgb`` modes. Also creates - vertex and index buffers needed for storing vertices and indices. - """ - mode = self.displayOpts.displayMode - self.mode = mode - - vertShaderSrc = shaders.getVertexShader( 'glvector_{}'.format(mode)) - fragShaderSrc = shaders.getFragmentShader('glvector') - - self.shaderParams = {} - self.shaders = shaders.compileShaders(vertShaderSrc, fragShaderSrc) - - s = self.shaders - p = self.shaderParams - - self.worldCoordBuffer = gl.glGenBuffers(1) - self.indexBuffer = gl.glGenBuffers(1) - - # Line mode needs an extra vertex array - # which contains vertex indices (which - # are not built-in in OpenGL2.1), and - # some extra parameters. - if mode == 'line': - self.vertexIDBuffer = gl.glGenBuffers(1) - p['voxToDisplayMat'] = gl.glGetUniformLocation(s, 'voxToDisplayMat') - p['vertexID'] = gl.glGetAttribLocation( s, 'vertexID') - - # parameers for glvector_rgb_vert.glsl/glvector_line_vert.glsl - p['displayToVoxMat'] = gl.glGetUniformLocation(s, 'displayToVoxMat') - p['worldToWorldMat'] = gl.glGetUniformLocation(s, 'worldToWorldMat') - p['xax'] = gl.glGetUniformLocation(s, 'xax') - p['yax'] = gl.glGetUniformLocation(s, 'yax') - p['zax'] = gl.glGetUniformLocation(s, 'zax') - p['zCoord'] = gl.glGetUniformLocation(s, 'zCoord') - p['worldCoords'] = gl.glGetAttribLocation( s, 'worldCoords') - - # parameters for glvector_frag.glsl - p['imageTexture'] = gl.glGetUniformLocation(s, 'imageTexture') - p['modTexture'] = gl.glGetUniformLocation(s, 'modTexture') - p['xColourTexture'] = gl.glGetUniformLocation(s, 'xColourTexture') - p['yColourTexture'] = gl.glGetUniformLocation(s, 'yColourTexture') - p['zColourTexture'] = gl.glGetUniformLocation(s, 'zColourTexture') - p['imageValueXform'] = gl.glGetUniformLocation(s, 'imageValueXform') - p['imageShape'] = gl.glGetUniformLocation(s, 'imageShape') - p['imageDims'] = gl.glGetUniformLocation(s, 'imageDims') - p['useSpline'] = gl.glGetUniformLocation(s, 'useSpline') - p['modThreshold'] = gl.glGetUniformLocation(s, 'modThreshold') - - p['alpha'] = gl.glGetUniformLocation(s, 'alpha') - p['brightness'] = gl.glGetUniformLocation(s, 'brightness') - p['contrast'] = gl.glGetUniformLocation(s, 'contrast') - - -def destroy(self): - """Deletes the vertex/fragment shader programs, and the vertex/index - buffers created in :func:`init`. - """ - - gl.glDeleteProgram(self.shaders) - - gl.glDeleteBuffers(1, gltypes.GLuint(self.worldCoordBuffer)) - gl.glDeleteBuffers(1, gltypes.GLuint(self.indexBuffer)) - - # extra vertex array buffer for line mode - if self.mode == 'line': - gl.glDeleteBuffers(1, gltypes.GLuint(self.vertexIDBuffer)) - - -def setAxes(self): - """Generates geometry for rendering the vector image in either ``line`` - mode or ``rgb`` mode. - """ - mode = self.mode - - if mode == 'line': - worldCoords, xpixdim, ypixdim, lenx, leny = \ - globject.calculateSamplePoints( - self.image, - self.display, - self.xax, - self.yax) - - worldCoords = np.repeat(worldCoords, 2, 0) - indices = np.arange(worldCoords.shape[0]) - - elif mode == 'rgb': - worldCoords, indices = globject.slice2D( - self.image.shape, - self.xax, - self.yax, - self.display.getTransform('voxel', 'display')) - - worldCoords = worldCoords[:, [self.xax, self.yax]] - worldCoords = np.array(worldCoords, dtype=np.float32).ravel('C') - indices = np.array(indices, dtype=np.uint32) .ravel('C') - self.nVertices = len(indices) - - gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.worldCoordBuffer) - gl.glBufferData(gl.GL_ARRAY_BUFFER, - worldCoords.nbytes, - worldCoords, - gl.GL_STATIC_DRAW) - - gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.indexBuffer) - gl.glBufferData(gl.GL_ELEMENT_ARRAY_BUFFER, - indices.nbytes, - indices, - gl.GL_STATIC_DRAW) - - if mode == 'line': - - vertexIDs = np.array(indices, dtype=np.float32) - gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertexIDBuffer) - gl.glBufferData(gl.GL_ARRAY_BUFFER, - vertexIDs.nbytes, - vertexIDs, - gl.GL_STATIC_DRAW) - - gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0) - gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, 0) - - -def preDraw(self): - """Loads the vertex/fragment shaders, and binds shader parameters and - vertex/index arrays ready for drawing. - """ - - display = self.display - opts = self.displayOpts - mode = self.mode - - modThreshold = opts.modThreshold / 100.0 - - gl.glUseProgram(self.shaders) - - pars = self.shaderParams - imageShape = np.array(self.image.shape[ :3], dtype=np.float32) - imageDims = np.array(self.image.pixdim[:3], dtype=np.float32) - displayToVoxMat = np.array( - display.getTransform('display', 'voxel'), dtype=np.float32) - - if mode == 'line': - useSpline = False - imageValueXform = np.array(self.imageTexture.voxValXform.T, - dtype=np.float32) - elif mode == 'rgb': - useSpline = display.interpolation == 'spline' - imageValueXform = np.eye(4, dtype=np.float32) - - displayToVoxMat = displayToVoxMat.ravel('C') - imageValueXform = imageValueXform.ravel('C') - - gl.glUniform1f( pars['modThreshold'], modThreshold) - gl.glUniform1f( pars['useSpline'], useSpline) - gl.glUniform3fv( pars['imageShape'], 1, imageShape) - gl.glUniform3fv( pars['imageDims'], 1, imageDims) - gl.glUniform1i( pars['xax'], self.xax) - gl.glUniform1i( pars['yax'], self.yax) - gl.glUniform1i( pars['zax'], self.zax) - gl.glUniformMatrix4fv(pars['displayToVoxMat'], 1, False, displayToVoxMat) - gl.glUniformMatrix4fv(pars['imageValueXform'], 1, False, imageValueXform) - - gl.glUniform1i( pars['imageTexture'], 0) - gl.glUniform1i( pars['modTexture'], 1) - gl.glUniform1i( pars['xColourTexture'], 2) - gl.glUniform1i( pars['yColourTexture'], 3) - gl.glUniform1i( pars['zColourTexture'], 4) - - gl.glUniform1f( pars['alpha'], display.alpha / 100.0) - gl.glUniform1f( pars['brightness'], display.brightness / 100.0) - gl.glUniform1f( pars['contrast'], display.contrast / 100.0) - - # Bind the world x/y coordinate buffer - gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.worldCoordBuffer) - gl.glVertexAttribPointer( - pars['worldCoords'], - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, - None) - gl.glEnableVertexAttribArray(pars['worldCoords']) - - # vox to display matrix, and vertex - # index buffer only needed for line mode - if mode == 'line': - - voxToDisplayMat = np.array(display.getTransform('voxel', 'display'), - dtype=np.float32) - voxToDisplayMat = voxToDisplayMat.ravel('C') - gl.glUniformMatrix4fv(pars['voxToDisplayMat'], - 1, - False, - voxToDisplayMat) - - gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertexIDBuffer) - gl.glVertexAttribPointer( - pars['vertexID'], - 1, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, - None) - gl.glEnableVertexAttribArray(pars['vertexID']) - - # Bind the index buffer - gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.indexBuffer) - - -def draw(self, zpos, xform=None): - """Draws the vector image.""" - - if xform is None: xform = np.identity(4) - - xform = np.array(xform, dtype=np.float32).ravel('C') - pars = self.shaderParams - mode = self.mode - - # Bind the current world z position, and - # the xform transformation matrix - gl.glUniform1f( pars['zCoord'], zpos) - gl.glUniformMatrix4fv(pars['worldToWorldMat'], 1, False, xform) - - # Draw all of the triangles! - if mode == 'line': - gl.glLineWidth(2) - gl.glDrawElements(gl.GL_LINES, - self.nVertices, - gl.GL_UNSIGNED_INT, - None) - - elif mode == 'rgb': - gl.glDrawElements(gl.GL_TRIANGLE_STRIP, - self.nVertices, - gl.GL_UNSIGNED_INT, - None) - - -def drawAll(self, zposes, xforms): - """Delegates to the :meth:`~fsl.fslview.gl.globject.GLObject.drawAll` - method. - """ - globject.GLObject.drawAll(self, zposes, xforms) - - -def postDraw(self): - """Unbinds vertex/index buffers.""" - - if self.mode == 'line': - gl.glDisableVertexAttribArray(self.shaderParams['vertexID']) - - gl.glDisableVertexAttribArray(self.shaderParams['worldCoords']) - gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0) - gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, 0) - - gl.glUseProgram(0) diff --git a/fsl/fslview/gl/gl21/glvector_line_vert.glsl b/fsl/fslview/gl/gl21/glvector_line_vert.glsl deleted file mode 100644 index b7240414f1bd3126388ee4c1950a5b0186732f9b..0000000000000000000000000000000000000000 --- a/fsl/fslview/gl/gl21/glvector_line_vert.glsl +++ /dev/null @@ -1,107 +0,0 @@ -/* - * OpenGL vertex shader used for rendering GLVector instances in - * line mode. - * - * Author: Paul McCarthy <pauldmccarthy@gmail.com> - */ -#version 120 - -#pragma include common_vert.glsl -#pragma include spline_interp.glsl - -/* - * Vector image containing XYZ vector data. - */ -uniform sampler3D imageTexture; - - -/* - * Transformation matrix which transforms the - * vector texture data to its original data range. - */ -uniform mat4 imageValueXform; - - -/* - * Matrix which transforms from voxel - * coordinates to display coordinates. - */ -uniform mat4 voxToDisplayMat; - -/* - * Shape of the image texture. - */ -uniform vec3 imageShape; - -/* - * Dimensions of one voxel in the image texture. - */ -uniform vec3 imageDims; - -/* - * Vertex index - the built-in gl_VertexID - * variable is not available in GLSL 120 - */ -attribute float vertexID; - - -void main(void) { - - common_vert(); - - vec3 voxCoords = fragVoxCoords; - vec3 vertexPos = fragVoxCoords - 0.5; - vec3 vector; - - /* - * Normalise the vertex coordinates to [0.0, 1.0], - * so they can be used for texture lookup. And make - * sure the voxel coordinates are exact integers - * (actually, that they are centred within the - * voxel - see common_vert.glsl), as we cannot - * interpolate vector directions. - */ - voxCoords = (floor(voxCoords) + 0.5) / imageShape; - - /* - * Retrieve the vector values for this voxel - */ - vector = texture3D(imageTexture, voxCoords).xyz; - - /* - * Transform the vector values from their - * texture range of [0,1] to the original - * data range - */ - vector *= imageValueXform[0].x; - vector += imageValueXform[0].w; - - vector *= 0.5; - - /* - * Vertices are coming in as line pairs - flip - * every second vertex about the origin - */ - if (mod(vertexID, 2) == 1) - vector = -vector; - - /* - * Scale the vector by the minimum voxel length, - * so it is a unit vector within real world space - */ - vector /= imageDims / min(imageDims.x, min(imageDims.y, imageDims.z)); - - /* - * Offset the vertex position - * by the vector direction - */ - vertexPos += vector; - - /* - * Output the final vertex position - */ - gl_Position = gl_ModelViewProjectionMatrix * - worldToWorldMat * - voxToDisplayMat * - vec4(vertexPos, 1); -} diff --git a/fsl/fslview/gl/gl21/glvector_rgb_vert.glsl b/fsl/fslview/gl/gl21/glvector_rgb_vert.glsl deleted file mode 100644 index 2f3d965b6e22b1c332863d62d9f72adf0382fb7a..0000000000000000000000000000000000000000 --- a/fsl/fslview/gl/gl21/glvector_rgb_vert.glsl +++ /dev/null @@ -1,12 +0,0 @@ -/* - * OpenGL vertex shader for drawing GLVector instances in rgb mode. - * - * Author: Paul McCarthy <pauldmccarthy@gmail.com> - */ -#version 120 - -#pragma include common_vert.glsl - -void main(void) { - common_vert(); -} diff --git a/fsl/fslview/gl/gl21/glvector_sw_frag.glsl b/fsl/fslview/gl/gl21/glvector_sw_frag.glsl new file mode 100644 index 0000000000000000000000000000000000000000..a3fb6cbb17ca22c32908f0ac943b8253d4ac52c1 --- /dev/null +++ b/fsl/fslview/gl/gl21/glvector_sw_frag.glsl @@ -0,0 +1,97 @@ +/* + * OpenGL fragment shader used for colouring GLRGBVector and GLLineVector + * instances. + * + * Author: Paul McCarthy <pauldmccarthy@gmail.com> + */ +#version 120 + +/* + * Vector image containing XYZ vector data. + */ +uniform sampler3D imageTexture; + +/* + * Modulation texture containing values by + * which the vector colours are to be modulated. + */ +uniform sampler3D modTexture; + +/* + * If the modulation value is below this + * threshold, the fragment is made + * transparent. + */ +uniform float modThreshold; + +/* + * Colour map for the X vector component. + */ +uniform sampler1D xColourTexture; + +/* + * Colour map for the Y vector component. + */ +uniform sampler1D yColourTexture; + +/* + * Colour map for the Z vector component. + */ +uniform sampler1D zColourTexture; + +/* + * Matrix which transforms from vector image + * texture values to their original data range. + */ +uniform mat4 voxValXform; + + +uniform mat4 cmapXform; + +/* + * Corresponding texture coordinates + */ +varying vec3 fragTexCoord; + + +void main(void) { + + /* + * Look up the xyz vector values + */ + vec3 voxValue = texture3D(imageTexture, fragTexCoord).xyz; + + /* Look up the modulation value */ + float modValue = texture3D(modTexture, fragTexCoord).x; + + /* + * Transform the voxel texture values + * into a range suitable for colour texture + * lookup, and take the absolute value + */ + voxValue *= voxValXform[0].x; + voxValue += voxValXform[3].x; + voxValue = abs(voxValue); + voxValue *= cmapXform[0].x; + voxValue += cmapXform[3].x; + + /* Apply the modulation value */ + voxValue *= modValue; + + /* Look up the colours for the xyz components */ + vec4 xColour = texture1D(xColourTexture, voxValue.x); + vec4 yColour = texture1D(yColourTexture, voxValue.y); + vec4 zColour = texture1D(zColourTexture, voxValue.z); + + /* Combine those colours */ + vec4 voxColour = xColour + yColour + zColour; + + /* Take the highest alpha of the three colour maps */ + voxColour.a = max(max(xColour.a, yColour.a), zColour.a); + + /* Knock out voxels where the modulation value is below the threshold */ + if (modValue < modThreshold) + voxColour.a = 0.0; + + gl_FragColor = voxColour; +} diff --git a/fsl/fslview/gl/gl21/glvolume_frag.glsl b/fsl/fslview/gl/gl21/glvolume_frag.glsl index 4481549d2166fc955f98851b0351c6fcb27af317..d24efa14d0d59847f23003312fcd5ad728a249bb 100644 --- a/fsl/fslview/gl/gl21/glvolume_frag.glsl +++ b/fsl/fslview/gl/gl21/glvolume_frag.glsl @@ -7,22 +7,22 @@ #pragma include spline_interp.glsl #pragma include test_in_bounds.glsl -#pragma include briconalpha.glsl /* - * image data texture + * image data texture. */ uniform sampler3D imageTexture; /* - * Image/texture dimensions + * Texture containing the colour map. */ -uniform vec3 imageShape; +uniform sampler1D colourTexture; + /* - * Texture containing the colour map + * Shape of the imageTexture. */ -uniform sampler1D colourTexture; +uniform vec3 imageShape; /* * Use spline interpolation? @@ -30,7 +30,9 @@ uniform sampler1D colourTexture; uniform bool useSpline; /* - * Transformation matrix to apply to the 1D texture coordinate. + * Transformation matrix to apply to the voxel value, + * so it can be used as a texture coordinate in the + * colourTexture. */ uniform mat4 voxValXform; @@ -45,42 +47,40 @@ uniform float clipLow; uniform float clipHigh; /* - * Image display coordinates. + * Image voxel coordinates. */ -varying vec3 fragDisplayCoords; +varying vec3 fragVoxCoord; /* - * Image voxel coordinates + * Corresponding texture coordinates. */ -varying vec3 fragVoxCoords; +varying vec3 fragTexCoord; void main(void) { - vec3 voxCoords = fragVoxCoords; + vec3 voxCoord = fragVoxCoord; - if (!test_in_bounds(voxCoords, imageShape)) { + /* + * Skip voxels that are out of the image bounds + */ + if (!test_in_bounds(voxCoord, imageShape)) { gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); return; } - /* - * Normalise voxel coordinates to (0.0, 1.0) - */ - voxCoords = voxCoords / imageShape; - /* * Look up the voxel value */ float voxValue; if (useSpline) voxValue = spline_interp(imageTexture, - voxCoords, + fragTexCoord, imageShape, 0); else voxValue = texture3D( imageTexture, - voxCoords).r; + fragTexCoord).r; /* * Clip out of range voxel values diff --git a/fsl/fslview/gl/gl21/glvolume_funcs.py b/fsl/fslview/gl/gl21/glvolume_funcs.py index 3171547f85f0de8b5a3932f969247fbadde64faa..36066ab9f4a0b75dba363e7d4b90e95ea78a9d61 100644 --- a/fsl/fslview/gl/gl21/glvolume_funcs.py +++ b/fsl/fslview/gl/gl21/glvolume_funcs.py @@ -14,156 +14,111 @@ program ``glvolume_frag.glsl``. This module provides the following functions: - - :func:`init`: Compiles vertex and fragment shaders. + - :func:`init`: Initialises GL objects and memory buffers. - - :func:`genVertexData`: Generates and returns vertex and texture coordinates - for rendering a single 2D slice of a 3D image. Actually returns handles to - VBOs which encapsulate the vertex and texture coordinates. + - :func:`compileShaders`: Compiles vertex/fragment shaders. - - :func:`preDraw`: Prepares the GL state for drawing. + - :func:`updateShaderState`: Refreshes the parameters used by the shader + programs, controlling clipping and interpolation. - - :func:`draw`: Draws the scene. + - :func:`preDraw`: Prepares the GL state for drawing. - - :func:`postDraw`: Resets the GL state after drawing. + - :func:`draw`: Draws the scene. - - :func:`destroy`: Deletes the vertex and texture coordinate VBOs. + - :func:`drawAll`: Draws multiple scenes. + + - :func:`postDraw`: Resets the GL state after drawing. + + - :func:`destroy`: Deletes the vertex and texture coordinate VBOs. """ import logging +import ctypes import numpy as np import OpenGL.GL as gl import OpenGL.raw.GL._types as gltypes -import fsl.fslview.gl.globject as globject import fsl.fslview.gl.shaders as shaders import fsl.utils.transform as transform log = logging.getLogger(__name__) -def _compileShaders(self): + +def compileShaders(self): """Compiles and links the OpenGL GLSL vertex and fragment shader programs, and attaches a reference to the resulting program, and - all GLSL variables, to the given GLVolume object. + all GLSL variables, to the given :class:`.GLVolume` object. """ - vertShaderSrc = shaders.getVertexShader( self) - fragShaderSrc = shaders.getFragmentShader(self) + if self.shaders is not None: + gl.glDeleteProgram(self.shaders) + + vertShaderSrc = shaders.getVertexShader( self, + sw=self.display.softwareMode) + fragShaderSrc = shaders.getFragmentShader(self, + sw=self.display.softwareMode) self.shaders = shaders.compileShaders(vertShaderSrc, fragShaderSrc) # indices of all vertex/fragment shader parameters - self.worldToWorldMatPos = gl.glGetUniformLocation(self.shaders, - 'worldToWorldMat') - self.xaxPos = gl.glGetUniformLocation(self.shaders, - 'xax') - self.yaxPos = gl.glGetUniformLocation(self.shaders, - 'yax') - self.zaxPos = gl.glGetUniformLocation(self.shaders, - 'zax') - self.worldCoordPos = gl.glGetAttribLocation( self.shaders, - 'worldCoords') - self.zCoordPos = gl.glGetUniformLocation(self.shaders, - 'zCoord') - + self.vertexPos = gl.glGetAttribLocation( self.shaders, + 'vertex') + self.voxCoordPos = gl.glGetAttribLocation( self.shaders, + 'voxCoord') + self.texCoordPos = gl.glGetAttribLocation( self.shaders, + 'texCoord') self.imageTexturePos = gl.glGetUniformLocation(self.shaders, - 'imageTexture') - self.imageShapePos = gl.glGetUniformLocation(self.shaders, - 'imageShape') + 'imageTexture') self.colourTexturePos = gl.glGetUniformLocation(self.shaders, - 'colourTexture') + 'colourTexture') + self.imageShapePos = gl.glGetUniformLocation(self.shaders, + 'imageShape') self.useSplinePos = gl.glGetUniformLocation(self.shaders, - 'useSpline') - self.displayToVoxMatPos = gl.glGetUniformLocation(self.shaders, - 'displayToVoxMat') + 'useSpline') self.voxValXformPos = gl.glGetUniformLocation(self.shaders, - 'voxValXform') + 'voxValXform') self.clipLowPos = gl.glGetUniformLocation(self.shaders, - 'clipLow') + 'clipLow') self.clipHighPos = gl.glGetUniformLocation(self.shaders, - 'clipHigh') - - # self.alphaPos = gl.glGetUniformLocation(self.shaders, 'alpha') - # self.brightnessPos = gl.glGetUniformLocation(self.shaders, 'brightness') - # self.contrastPos = gl.glGetUniformLocation(self.shaders, 'contrast') + 'clipHigh') def init(self): """Compiles the vertex and fragment shaders used to render image slices. """ - _compileShaders(self) - - self.worldCoordBuffer = gl.glGenBuffers(1) - self.indexBuffer = gl.glGenBuffers(1) + self.shaders = None + + compileShaders( self) + updateShaderState(self) + + self.vertexAttrBuffer = gl.glGenBuffers(1) + def destroy(self): - """Cleans up VBO handles.""" + """Cleans up the vertex buffer handle and shader programs.""" - gl.glDeleteBuffers(1, gltypes.GLuint(self.worldCoordBuffer)) - gl.glDeleteBuffers(1, gltypes.GLuint(self.indexBuffer)) + gl.glDeleteBuffers(1, gltypes.GLuint(self.vertexAttrBuffer)) gl.glDeleteProgram(self.shaders) -def genVertexData(self): - """Generates vertex and texture coordinates required to render the - image, and associates them with OpenGL VBOs . See - :func:`fsl.fslview.gl.glvolume.genVertexData`. - """ - xax = self.xax - yax = self.yax - - worldCoordBuffer = self.worldCoordBuffer - indexBuffer = self.indexBuffer - worldCoords, indices = self.genVertexData() - - worldCoords = worldCoords[:, [xax, yax]] - - worldCoords = worldCoords.ravel('C') - indices = indices .ravel('C') - - gl.glBindBuffer(gl.GL_ARRAY_BUFFER, worldCoordBuffer) - gl.glBufferData(gl.GL_ARRAY_BUFFER, - worldCoords.nbytes, - worldCoords, - gl.GL_STATIC_DRAW) - - gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, indexBuffer) - gl.glBufferData(gl.GL_ELEMENT_ARRAY_BUFFER, - indices.nbytes, - indices, - gl.GL_STATIC_DRAW) - - gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0) - gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, 0) - - return worldCoordBuffer, indexBuffer, len(indices) - - -def preDraw(self): - """Sets up the GL state to draw a slice from the given - :class:`~fsl.fslview.gl.glvolume.GLVolume` instance. +def updateShaderState(self): + """Updates the parameters used by the shader programs, reflecting the + current display properties. """ display = self.display opts = self.displayOpts - # load the shaders gl.glUseProgram(self.shaders) - + # bind the current interpolation setting, # image shape, and image->screen axis # mappings gl.glUniform1f( self.useSplinePos, display.interpolation == 'spline') gl.glUniform3fv(self.imageShapePos, 1, np.array(self.image.shape, dtype=np.float32)) - gl.glUniform1i( self.xaxPos, self.xax) - gl.glUniform1i( self.yaxPos, self.yax) - gl.glUniform1i( self.zaxPos, self.zax) - - # gl.glUniform1f( self.alphaPos, display.alpha / 100.0) - # gl.glUniform1f( self.brightnessPos, display.brightness / 100.0) - # gl.glUniform1f( self.contrastPos, display.contrast / 100.0) # The clipping range options are in the voxel value # range, but the shader needs them to be in image @@ -183,39 +138,71 @@ def preDraw(self): # display coordinates to voxel coordinates, # and to scale voxel values to colour map # texture coordinates - tcx = transform.concat(self.imageTexture.voxValXform, - self.colourMapXform) - w2v = np.array( - display.getTransform('display', 'voxel'), dtype=np.float32).ravel('C') - - vvx = np.array(tcx, dtype=np.float32).ravel('C') + vvx = transform.concat(self.imageTexture.voxValXform, + self.colourTexture.getCoordinateTransform()) + vvx = np.array(vvx, dtype=np.float32).ravel('C') - gl.glUniformMatrix4fv(self.displayToVoxMatPos, 1, False, w2v) - gl.glUniformMatrix4fv(self.voxValXformPos, 1, False, vvx) + gl.glUniformMatrix4fv(self.voxValXformPos, 1, False, vvx) # Set up the colour and image textures gl.glUniform1i(self.imageTexturePos, 0) - gl.glUniform1i(self.colourTexturePos, 1) + gl.glUniform1i(self.colourTexturePos, 1) + + gl.glUseProgram(0) + + +def preDraw(self): + """Sets up the GL state to draw a slice from the given + :class:`~fsl.fslview.gl.glvolume.GLVolume` instance. + """ + + # load the shaders + gl.glUseProgram(self.shaders) + + +def _prepareVertexAttributes(self, vertices, voxCoords, texCoords): + """Prepares a data buffer which contains the given vertices, + voxel coordinates, and texture coordinates, ready to be passed in to + the shader programs. + """ + + buf = np.zeros((vertices.shape[0] * 3, 3), dtype=np.float32) + verPos = self.vertexPos + voxPos = self.voxCoordPos + texPos = self.texCoordPos + + # We store each of the three coordinate + # sets in a single interleaved buffer + buf[ ::3, :] = vertices + buf[1::3, :] = voxCoords + buf[2::3, :] = texCoords + + buf = buf.ravel('C') + + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertexAttrBuffer) + gl.glBufferData(gl.GL_ARRAY_BUFFER, buf.nbytes, buf, gl.GL_STATIC_DRAW) - # Bind the world x/y coordinate buffer - gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.worldCoords) gl.glVertexAttribPointer( - self.worldCoordPos, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, - None) - gl.glEnableVertexAttribArray(self.worldCoordPos) + verPos, 3, gl.GL_FLOAT, gl.GL_FALSE, 36, None) + gl.glVertexAttribPointer( + texPos, 3, gl.GL_FLOAT, gl.GL_FALSE, 36, ctypes.c_void_p(24)) - # Bind the vertex index buffer - gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.indices) + # The fast shader does not use voxel coordinates + # so, on some GL drivers, attempting to bind it + # will cause an error + if not self.display.softwareMode: + gl.glVertexAttribPointer( + voxPos, 3, gl.GL_FLOAT, gl.GL_FALSE, 36, ctypes.c_void_p(12)) + gl.glEnableVertexAttribArray(self.voxCoordPos) + gl.glEnableVertexAttribArray(self.vertexPos) + gl.glEnableVertexAttribArray(self.texCoordPos) + def draw(self, zpos, xform=None): """Draws the specified slice from the specified image on the canvas. - :arg image: The :class:`~fsl.fslview.gl.glvolume.GLVolume` object which + :arg self: The :class:`~fsl.fslview.gl.glvolume.GLVolume` object which is managing the image to be drawn. :arg zpos: World Z position of slice to be drawn. @@ -223,27 +210,32 @@ def draw(self, zpos, xform=None): :arg xform: A 4*4 transformation matrix to be applied to the vertex data. """ - - if xform is None: xform = np.identity(4) - - w2w = np.array(xform, dtype=np.float32).ravel('C') - # Bind the current world z position, and - # the xform transformation matrix - gl.glUniform1f( self.zCoordPos, zpos) - gl.glUniformMatrix4fv(self.worldToWorldMatPos, 1, False, w2w) + vertices, voxCoords, texCoords = self.generateVertices(zpos, xform) + _prepareVertexAttributes(self, vertices, voxCoords, texCoords) + + gl.glDrawArrays(gl.GL_TRIANGLES, 0, 6) - # Draw all of the triangles! - gl.glDrawElements(gl.GL_TRIANGLE_STRIP, - self.nVertices, - gl.GL_UNSIGNED_INT, - None) def drawAll(self, zposes, xforms): """Delegates to the default implementation in :meth:`~fsl.fslview.gl.globject.GLObject.drawAll`. """ - globject.GLObject.drawAll(self, zposes, xforms) + + nslices = len(zposes) + vertices = np.zeros((nslices * 6, 3), dtype=np.float32) + voxCoords = np.zeros((nslices * 6, 3), dtype=np.float32) + texCoords = np.zeros((nslices * 6, 3), dtype=np.float32) + + for i, (zpos, xform) in enumerate(zip(zposes, xforms)): + + v, vc, tc = self.generateVertices(zpos, xform) + vertices[ i * 6: i * 6 + 6, :] = v + voxCoords[i * 6: i * 6 + 6, :] = vc + texCoords[i * 6: i * 6 + 6, :] = tc + + _prepareVertexAttributes(self, vertices, voxCoords, texCoords) + gl.glDrawArrays(gl.GL_TRIANGLES, 0, 6 * nslices) def postDraw(self): @@ -251,7 +243,11 @@ def postDraw(self): :class:`~fsl.fslview.gl.glvolume.GLVolume` instance. """ - gl.glDisableVertexAttribArray(self.worldCoordPos) - gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0) - gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, 0) + gl.glDisableVertexAttribArray(self.vertexPos) + gl.glDisableVertexAttribArray(self.texCoordPos) + + if not self.display.softwareMode: + gl.glDisableVertexAttribArray(self.voxCoordPos) + + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0) gl.glUseProgram(0) diff --git a/fsl/fslview/gl/gl21/glvolume_sw_frag.glsl b/fsl/fslview/gl/gl21/glvolume_sw_frag.glsl new file mode 100644 index 0000000000000000000000000000000000000000..441aa999b3272a22eadd334c1cac812c0836aedc --- /dev/null +++ b/fsl/fslview/gl/gl21/glvolume_sw_frag.glsl @@ -0,0 +1,74 @@ +/* + * Fast but limited OpenGL fragment shader used by + * fsl/fslview/gl/gl21/glvolume_funcs.py. + * + * Author: Paul McCarthy <pauldmccarthy@gmail.com> + */ +#version 120 + +#pragma include spline_interp.glsl +#pragma include test_in_bounds.glsl + +/* + * image data texture. + */ +uniform sampler3D imageTexture; + +/* + * Texture containing the colour map. + */ +uniform sampler1D colourTexture; + + +/* + * Shape of the imageTexture. + */ +uniform vec3 imageShape; + +/* + * Transformation matrix to apply to the voxel value, + * so it can be used as a texture coordinate in the + * colourTexture. + */ +uniform mat4 voxValXform; + +/* + * Clip voxels below this value. + */ +uniform float clipLow; + +/* + * Clip voxels above this value. + */ +uniform float clipHigh; + + +/* + * Corresponding texture coordinates. + */ +varying vec3 fragTexCoord; + + +void main(void) { + + /* + * Look up the voxel value + */ + float voxValue = texture3D(imageTexture, fragTexCoord).r; + + /* + * Clip out of range voxel values + */ + if (voxValue < clipLow || voxValue > clipHigh) { + + gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); + return; + } + + /* + * Transform the voxel value to a colour map texture + * coordinate, and look up the colour for the voxel value + */ + voxValue = voxValXform[0].x * voxValue + voxValXform[3].x; + gl_FragColor = texture1D(colourTexture, voxValue); +} diff --git a/fsl/fslview/gl/gl21/glvolume_sw_vert.glsl b/fsl/fslview/gl/gl21/glvolume_sw_vert.glsl new file mode 100644 index 0000000000000000000000000000000000000000..f7f4d0f8e587382fb764a3cfd1413d46c268ec8e --- /dev/null +++ b/fsl/fslview/gl/gl21/glvolume_sw_vert.glsl @@ -0,0 +1,16 @@ +/* + * OpenGL vertex shader used for rendering GLVolume instances. + * + * Author: Paul McCarthy <pauldmccarthy@gmail.com> + */ +#version 120 + +attribute vec3 vertex; +attribute vec3 texCoord; +varying vec3 fragTexCoord; + +void main(void) { + + fragTexCoord = texCoord; + gl_Position = gl_ModelViewProjectionMatrix * vec4(vertex, 1); +} diff --git a/fsl/fslview/gl/gl21/glvolume_vert.glsl b/fsl/fslview/gl/gl21/glvolume_vert.glsl index 2a4739bb4b4200ac22b51603becf7b3b9f0d672d..474f98c4853aedbcaa76438a2d09d66682bc8531 100644 --- a/fsl/fslview/gl/gl21/glvolume_vert.glsl +++ b/fsl/fslview/gl/gl21/glvolume_vert.glsl @@ -1,12 +1,23 @@ /* * OpenGL vertex shader used for rendering GLVolume instances. + * All this shader does is set the vertex position, and pass the + * voxel and texture coordinates through to the fragment shader. * * Author: Paul McCarthy <pauldmccarthy@gmail.com> */ #version 120 -#pragma include common_vert.glsl +attribute vec3 vertex; +attribute vec3 voxCoord; +attribute vec3 texCoord; + +varying vec3 fragVoxCoord; +varying vec3 fragTexCoord; void main(void) { - common_vert(); + + fragVoxCoord = voxCoord; + fragTexCoord = texCoord; + + gl_Position = gl_ModelViewProjectionMatrix * vec4(vertex, 1); } diff --git a/fsl/fslview/gl/gllinevector.py b/fsl/fslview/gl/gllinevector.py new file mode 100644 index 0000000000000000000000000000000000000000..949a62b4869df4c10e0b9f295434b6317a036381 --- /dev/null +++ b/fsl/fslview/gl/gllinevector.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python +# +# gllinevector.py - Displays vector data as lines. +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import logging + +import numpy as np + +import fsl.utils.transform as transform +import fsl.fslview.gl as fslgl +import fsl.fslview.gl.glvector as glvector +import fsl.fslview.gl.routines as glroutines + + +log = logging.getLogger(__name__) + +class GLLineVertices(object): + + def __init__(self, glvec): + + self.__hash = None + self.refresh(glvec) + + + def destroy(self): + self.vertices = None + self.texCoords = None + self.starts = None + self.steps = None + + + def __hash__(self): + return self.__hash + + + def refresh(self, glvec): + + display = glvec.display + opts = glvec.opts + image = glvec.image + + # Extract a sub-sample of the vector image + # at the current display resolution + data, starts, steps = glroutines.subsample(image.data, + display.resolution, + image.pixdim) + + # Pull out the xyz components of the + # vectors, and calculate vector lengths + vertices = np.array(data, dtype=np.float32) + x = vertices[:, :, :, 0] + y = vertices[:, :, :, 1] + z = vertices[:, :, :, 2] + lens = np.sqrt(x ** 2 + y ** 2 + z ** 2) + + # scale the vector lengths to 0.5 + vertices[:, :, :, 0] = 0.5 * x / lens + vertices[:, :, :, 1] = 0.5 * y / lens + vertices[:, :, :, 2] = 0.5 * z / lens + + # Scale the vector data by the minimum + # voxel length, so it is a unit vector + # within real world space + vertices /= (image.pixdim[:3] / min(image.pixdim[:3])) + + # Duplicate vector data so that each + # vector is represented by two vertices, + # representing a line through the origin. + # Or, if displaying directed vectors, + # add an origin point for each vector. + if opts.directed: + origins = np.zeros(vertices.shape, dtype=np.float32) + vertices = np.concatenate((origins, vertices), axis=3) + else: + vertices = np.concatenate((-vertices, vertices), axis=3) + + vertices = vertices.reshape((data.shape[0], + data.shape[1], + data.shape[2], + 2, + 3)) + + # Offset each vertex by the corresponding + # voxel coordinates, making sure to + # transform from the sub-sampled indices + # to the original data indices (offseting + # and scaling by the starts and steps) + for i in range(data.shape[0]): + vertices[i, :, :, :, 0] += starts[0] + i * steps[0] + + for i in range(data.shape[1]): + vertices[:, i, :, :, 1] += starts[1] + i * steps[1] + + for i in range(data.shape[2]): + vertices[:, :, i, :, 2] += starts[2] + i * steps[2] + + texCoords = vertices.round() + texCoords = (texCoords + 0.5) / np.array(image.shape[:3], + dtype=np.float32) + + self.vertices = vertices + self.texCoords = texCoords + self.starts = starts + self.steps = steps + self.__hash = (hash(display.transform) ^ + hash(display.resolution) ^ + hash(opts .directed)) + + + def getVertices(self, glvec, zpos): + + display = glvec.display + image = glvec.image + xax = glvec.xax + yax = glvec.yax + zax = glvec.zax + + vertices = self.vertices + texCoords = self.texCoords + starts = self.starts + steps = self.steps + + # If in id/pixdim space, the display + # coordinate system axes are parallel + # to the voxeld coordinate system axes + if display.transform in ('id', 'pixdim'): + + # Turn the z position into a voxel index + if display.transform == 'pixdim': + zpos = zpos / image.pixdim[zax] + + zpos = round(zpos) + + # Return no vertices if the requested z + # position is out of the image bounds + if zpos < 0 or zpos >= image.shape[zax]: + return (np.array([], dtype=np.float32), + np.array([], dtype=np.float32)) + + # Extract a slice at the requested + # z position from the vertex matrix + coords = [slice(None)] * 3 + coords[zax] = np.floor((zpos - starts[zax]) / steps[zax]) + + # If in affine space, the display + # coordinate system axes may not + # be parallel to the voxel + # coordinate system axes + else: + # Create a coordinate grid through + # a plane at the requested z pos + # in the display coordinate system + coords = glroutines.calculateSamplePoints( + image.shape[ :3], + [display.resolution] * 3, + display.getTransform('voxel', 'display'), + xax, + yax)[0] + + coords[:, zax] = zpos + + # transform that plane of display + # coordinates into voxel coordinates + coords = transform.transform( + coords, display.getTransform('display', 'voxel')) + + # The voxel vertex matrix may have + # been sub-sampled (see the + # generateLineVertices method), + # so we need to transform the image + # data voxel coordinates to the + # sub-sampled data voxel coordinates. + coords = (coords - starts) / steps + + # remove any out-of-bounds voxel coordinates + shape = vertices.shape[:3] + coords = np.array(coords.round(), dtype=np.int32) + coords = coords[((coords >= [0, 0, 0]) & + (coords < shape)).all(1), :].T + + # pull out the vertex data, and the + # corresponding texture coordinates + vertices = vertices[ coords[0], coords[1], coords[2], :, :] + texCoords = texCoords[coords[0], coords[1], coords[2], :, :] + + return vertices, texCoords + + +class GLLineVector(glvector.GLVector): + + + def __init__(self, image, display): + + glvector.GLVector.__init__(self, image, display) + + self.opts = display.getDisplayOpts() + + fslgl.gllinevector_funcs.init(self) + + def update(*a): + self.onUpdate() + + self.opts.addListener('lineWidth', self.name, update) + + + def destroy(self): + glvector.GLVector.destroy(self) + fslgl.gllinevector_funcs.destroy(self) + + self.opts.removeListener('lineWidth', self.name) + + + def getDataResolution(self, xax, yax): + + res = list(glvector.GLVector.getDataResolution(self, xax, yax)) + res[xax] *= 16 + res[yax] *= 16 + + return res + + + def compileShaders(self): + fslgl.gllinevector_funcs.compileShaders(self) + + + def updateShaderState(self): + fslgl.gllinevector_funcs.updateShaderState(self) + + + def preDraw(self): + glvector.GLVector.preDraw(self) + fslgl.gllinevector_funcs.preDraw(self) + + + def draw(self, zpos, xform=None): + fslgl.gllinevector_funcs.draw(self, zpos, xform) + + + def drawAll(self, zposes, xforms): + fslgl.gllinevector_funcs.drawAll(self, zposes, xforms) + + + def postDraw(self): + glvector.GLVector.postDraw(self) + fslgl.gllinevector_funcs.postDraw(self) diff --git a/fsl/fslview/gl/glmask.py b/fsl/fslview/gl/glmask.py index 36231c7c0e21a5e99f2db8cc1f6960edf4ddca2a..26ee689e6c2a893c9f97a39a110ca38286ccd766 100644 --- a/fsl/fslview/gl/glmask.py +++ b/fsl/fslview/gl/glmask.py @@ -21,7 +21,6 @@ import logging import numpy as np -import OpenGL.GL as gl import fsl.fslview.gl.glvolume as glvolume @@ -50,9 +49,11 @@ class GLMask(glvolume.GLVolume): """ def vertexUpdate(*a): self.setAxes(self.xax, self.yax) + self.onUpdate() def colourUpdate(*a): self.refreshColourTexture() + self.onUpdate() lnrName = '{}_{}'.format(type(self).__name__, id(self)) @@ -96,61 +97,23 @@ class GLMask(glvolume.GLVolume): is set). """ - opts = self.displayOpts - colour = opts.colour - imin = opts.threshold[0] - imax = opts.threshold[1] + display = self.display + opts = self.displayOpts + alpha = display.alpha / 100.0 + colour = opts.colour + dmin = opts.threshold[0] + dmax = opts.threshold[1] colour[3] = 1.0 - # This transformation is used to transform voxel values - # from their native range to the range [0.0, 1.0], which - # is required for texture colour lookup. Values below - # or above the current display range will be mapped - # to texture coordinate values less than 0.0 or greater - # than 1.0 respectively. - if imax == imin: scale = 1 - else: scale = imax - imin - - cmapXform = np.identity(4, dtype=np.float32) - cmapXform[0, 0] = 1.0 / scale - cmapXform[3, 0] = -imin * cmapXform[0, 0] - - self.colourMapXform = cmapXform - if opts.invert: - colourmap = np.tile([[0.0, 0.0, 0.0, 0.0]], (16, 1)) - border = np.array(opts.colour, dtype=np.float32) + cmap = np.tile([0.0, 0.0, 0.0, 0.0], (4, 1)) + border = np.array(opts.colour, dtype=np.float32) else: - colourmap = np.tile([[opts.colour]], (16, 1)) - border = np.array([0.0, 0.0, 0.0, 0.0], dtype=np.float32) - - colourmap = np.floor(colourmap * 255) - colourmap = np.array(colourmap, dtype=np.uint8) - colourmap = colourmap.ravel('C') + cmap = np.tile([opts.colour], (4, 1)) + border = np.array([0.0, 0.0, 0.0, 0.0], dtype=np.float32) - gl.glBindTexture(gl.GL_TEXTURE_1D, self.colourTexture) - - gl.glTexParameterfv(gl.GL_TEXTURE_1D, - gl.GL_TEXTURE_BORDER_COLOR, - border) - - gl.glTexParameteri(gl.GL_TEXTURE_1D, - gl.GL_TEXTURE_MAG_FILTER, - gl.GL_NEAREST) - gl.glTexParameteri(gl.GL_TEXTURE_1D, - gl.GL_TEXTURE_MIN_FILTER, - gl.GL_NEAREST) - gl.glTexParameteri(gl.GL_TEXTURE_1D, - gl.GL_TEXTURE_WRAP_S, - gl.GL_CLAMP_TO_BORDER) - - gl.glTexImage1D(gl.GL_TEXTURE_1D, - 0, - gl.GL_RGBA8, - 16, - 0, - gl.GL_RGBA, - gl.GL_UNSIGNED_BYTE, - colourmap) - gl.glBindTexture(gl.GL_TEXTURE_1D, 0) + self.colourTexture.set(cmap=cmap, + border=border, + displayRange=(dmin, dmax), + alpha=alpha) diff --git a/fsl/fslview/gl/globject.py b/fsl/fslview/gl/globject.py index d89500263f65cb5c22592eea9024b3ecae006e1e..88e10133759c808c69bdf4634cfd79c9a5a6ae29 100644 --- a/fsl/fslview/gl/globject.py +++ b/fsl/fslview/gl/globject.py @@ -6,25 +6,15 @@ # Author: Paul McCarthy <pauldmccarthy@gmail.com> # """This module defines the :class:`GLObject` class, which is a superclass for -all 2D OpenGL representations of :class:`fsl.data.image.Image` instances. +all 2D representations of objects in OpenGL. This module also provides the :func:`createGLObject` function, which provides mappings between :class:`~fsl.data.image.Image` types, and their corresponding OpenGL representation. - -Some other convenience functions are also provided, for generating -OpenGL vertex data. """ -import logging - -import itertools as it -import numpy as np -import fsl.utils.transform as transform - - -log = logging.getLogger(__name__) +import numpy as np def createGLObject(image, display): @@ -35,14 +25,16 @@ def createGLObject(image, display): :arg display: A :class:`~fsl.fslview.displaycontext.Display` instance. """ - import fsl.fslview.gl.glvolume as glvolume - import fsl.fslview.gl.glmask as glmask - import fsl.fslview.gl.glvector as glvector + import fsl.fslview.gl.glvolume as glvolume + import fsl.fslview.gl.glmask as glmask + import fsl.fslview.gl.glrgbvector as glrgbvector + import fsl.fslview.gl.gllinevector as gllinevector _objectmap = { - 'volume' : glvolume.GLVolume, - 'mask' : glmask .GLMask, - 'vector' : glvector.GLVector + 'volume' : glvolume .GLVolume, + 'mask' : glmask .GLMask, + 'rgbvector' : glrgbvector .GLRGBVector, + 'linevector' : gllinevector.GLLineVector } ctr = _objectmap.get(display.imageType, None) @@ -60,23 +52,49 @@ class GLObject(object): """Create a :class:`GLObject`. The constructor adds one attribute to this instance, ``name``, which is simply a unique name for this instance. + + Subclass implementations must call this method, and should also + perform any necessary OpenGL initialisation, such as creating + textures. """ self.name = '{}_{}'.format(type(self).__name__, id(self)) - + self.__updateListeners = {} + + + def addUpdateListener(self, name, listener): + """Adds a listener function which will be called whenever this + ``GLObject`` representation changes. - def init(self): - """Perform any necessary OpenGL initialisation, such as - creating textures and vertices. + The listener function must accept a single parameter, which is + a reference to this ``GLObject``. """ - raise NotImplementedError() + self.__updateListeners[name] = listener - - def ready(self): - """This method should return ``False`` if this :class:`GLObject` is - not yet ready to be displayed, ``True`` otherwise. + + def removeUpdateListener(self, name): + """Removes a listener previously registered via + :meth:`addUpdateListener`. """ - raise NotImplementedError() + self.__updateListeners.pop(name, None) + + + def onUpdate(self): + """This method must be called by subclasses whenever the GL object + representation changes - it notifies any registered listeners of the + change. + """ + for name, listener in self.__updateListeners.items(): + listener(self) + + + def getDisplayBounds(self): + raise NotImplementedError('The getDisplayBounds method must be ' + 'implemented by GLObject subclasses') + + + def getDataResolution(self, xax, yax): + return None def setAxes(self, xax, yax): @@ -167,18 +185,14 @@ class GLSimpleObject(GLObject): def __init__(self): GLObject.__init__(self) - self.__ready = False - - def init( self): pass - def destroy(self): pass - def ready( self): return self.__ready def setAxes(self, xax, yax): - self.xax = xax - self.yax = yax - self.zax = 3 - xax - yax - self.__ready = True + self.xax = xax + self.yax = yax + self.zax = 3 - xax - yax + + def destroy(self): pass def preDraw( self): pass def postDraw(self): pass @@ -209,466 +223,25 @@ class GLImageObject(GLObject): self.displayOpts = display.getDisplayOpts() -def calculateSamplePoints(image, display, xax, yax): - """Calculates a uniform grid of points, in the display coordinate system - (as specified by the given - :class:`~fsl.fslview.displaycontext.ImageDisplay` object properties) along - the x-y plane (as specified by the xax/yax indices), at which the given - image should be sampled for display purposes. - - This function returns a tuple containing: - - - a numpy array of shape `(N, 3)`, containing the coordinates of the - centre of every sampling point in real world space. + def getDisplayBounds(self): + return self.display.getDisplayBounds() - - the horizontal distance (along xax) between adjacent points - - the vertical distance (along yax) between adjacent points + def getDataResolution(self, xax, yax): - - The number of samples along the horizontal axis (xax) - - - The number of samples along the vertical axis (yax) - - :arg image: The :class:`~fsl.data.image.Image` object to - generate vertex and texture coordinates for. - - :arg display: A :class:`~fsl.fslview.displaycontext.ImageDisplay` - object which defines how the image is to be - rendered. - - :arg xax: The world space axis which corresponds to the - horizontal screen axis (0, 1, or 2). - - :arg yax: The world space axis which corresponds to the - vertical screen axis (0, 1, or 2). - """ - - transformCode = display.transform - transformMat = display.getTransform('voxel', 'display') - worldRes = display.resolution - - xVoxelRes = np.round(worldRes / image.pixdim[xax]) - yVoxelRes = np.round(worldRes / image.pixdim[yax]) - - if xVoxelRes < 1: xVoxelRes = 1 - if yVoxelRes < 1: yVoxelRes = 1 - - # These values give the min/max x/y values - # of a bounding box which encapsulates - # the entire image - xmin, xmax = transform.axisBounds(image.shape, transformMat, xax) - ymin, ymax = transform.axisBounds(image.shape, transformMat, yax) - - - # The width/height of a displayed voxel. - # If we are displaying in real world space, - # we use the world display resolution - if transformCode == 'affine': - - xpixdim = worldRes - ypixdim = worldRes - - # But if we're just displaying the data (the - # transform is 'id' or 'pixdim'), we display - # it in the resolution of said data. - elif transformCode == 'pixdim': - xpixdim = image.pixdim[xax] * xVoxelRes - ypixdim = image.pixdim[yax] * yVoxelRes + image = self.image + display = self.display + res = display.resolution - elif transformCode == 'id': - xpixdim = 1.0 * xVoxelRes - ypixdim = 1.0 * yVoxelRes - - # Number of samples across each dimension, - # given the current sample rate - xNumSamples = np.floor((xmax - xmin) / xpixdim) - yNumSamples = np.floor((ymax - ymin) / ypixdim) - - # the adjusted width/height of our sample points - xpixdim = (xmax - xmin) / xNumSamples - ypixdim = (ymax - ymin) / yNumSamples - - log.debug('Generating coordinate buffers for {} ' - '({} resolution {}/({}, {}), num samples {})'.format( - image.name, transformCode, worldRes, xVoxelRes, yVoxelRes, - xNumSamples * yNumSamples)) - - # The location of every displayed - # point in real world space - worldX = np.linspace(xmin + 0.5 * xpixdim, - xmax - 0.5 * xpixdim, - xNumSamples) - worldY = np.linspace(ymin + 0.5 * ypixdim, - ymax - 0.5 * ypixdim, - yNumSamples) - - worldX, worldY = np.meshgrid(worldX, worldY) - - coords = np.zeros((worldX.size, 3), dtype=np.float32) - coords[:, xax] = worldX.flatten() - coords[:, yax] = worldY.flatten() - - return coords, xpixdim, ypixdim, xNumSamples, yNumSamples - - -def samplePointsToTriangleStrip(coords, - xpixdim, - ypixdim, - xlen, - ylen, - xax, - yax): - """Given a regular 2D grid of points at which an image is to be sampled - (for example, that generated by the :func:`calculateSamplePoints` function - above), converts those points into an OpenGL vertex triangle strip. - - A grid of M*N points is represented by M*2*(N + 1) vertices. For example, - this image represents a 4*3 grid, with periods representing vertex - locations:: - - .___.___.___.___. - | | | | | - | | | | | - .---.---.---.---. - .___.___.__ .___. - | | | | | - | | | | | - .---.---.---.---. - .___.___.___.___. - | | | | | - | | | | | - .___.___.___.___. - - - Vertex locations which are vertically adjacent represent the same point in - space. Such vertex pairs are unable to be combined because, in OpenGL, - they must be represented by distinct vertices (we can't apply multiple - colours/texture coordinates to a single vertex location) So we have to - repeat these vertices in order to achieve accurate colouring of each - voxel. - - We draw each horizontal row of samples one by one, using two triangles to - draw each voxel. In order to eliminate the need to specify six vertices - for every voxel, and hence to reduce the amount of memory used, we are - using a triangle strip to draw each row of voxels. This image depicts a - triangle strip used to draw a row of three samples (periods represent - vertex locations):: - - - 1 3 5 7 - . . . . - |\ |\ |\ | - | \| \| \| - . . . . - 0 2 4 6 - - In order to use a single OpenGL call to draw multiple non-contiguous voxel - rows, between every column we add a couple of 'dummy' vertices, which will - then be interpreted by OpenGL as 'degenerate triangles', and will not be - drawn. So in reality, a 4*3 slice would be drawn as follows (with vertices - labelled from [a-z0-9]: - - v x z 1 33 - |\ |\ |\ |\ | - | \| \| \| \| - uu w y 0 2 - l n p r tt - |\ |\ |\ |\ | - | \| \| \| \| - kk m o q s - b d f h jj - |\ |\ |\ |\ | - | \| \| \| \| - a c e g i - - These repeated/degenerate vertices are dealt with by using a vertex index - array. See these links for good overviews of triangle strips and - degenerate triangles in OpenGL: - - - http://www.learnopengles.com/tag/degenerate-triangles/ - - http://en.wikipedia.org/wiki/Triangle_strip - - A tuple is returned containing: - - - A 2D `numpy.float32` array of shape `(2 * (xlen + 1) * ylen), 3)`, - containing the coordinates of all triangle strip vertices which - represent the entire grid of sample points. - - - A 2D `numpy.float32` array of shape `(2 * (xlen + 1) * ylen), 3)`, - containing the centre of every grid, to be used for texture - coordinates/colour lookup. - - - A 1D `numpy.uint32` array of size `ylen * (2 * (xlen + 1) + 2) - 2` - containing indices into the first array, defining the order in which - the vertices need to be rendered. There are more indices than vertex - coordinates due to the inclusion of repeated/degenerate vertices. - - :arg coords: N*3 array of points, the sampling locations. - - :arg xpixdim: Length of one sample along the horizontal axis. - - :arg ypixdim: Length of one sample along the vertical axis. - - :arg xlen: Number of samples along the horizontal axis. - - :arg ylen: Number of samples along the vertical axis. - - :arg xax: Display coordinate system axis which corresponds to the - horizontal screen axis. - - :arg yax: Display coordinate system axis which corresponds to the - vertical screen axis. - """ - - coords = coords.reshape(ylen, xlen, 3) - - xlen = int(xlen) - ylen = int(ylen) - - # Duplicate every row - each voxel - # is defined by two vertices - coords = coords.repeat(2, 0) - - texCoords = np.array(coords, dtype=np.float32) - worldCoords = np.array(coords, dtype=np.float32) - - # Add an extra column at the end - # of the world coordinates - worldCoords = np.append(worldCoords, worldCoords[:, -1:, :], 1) - worldCoords[:, -1, xax] += xpixdim - - # Add an extra column at the start - # of the texture coordinates - texCoords = np.append(texCoords[:, :1, :], texCoords, 1) - - # Move the x/y world coordinates to the - # sampling point corners (the texture - # coordinates remain in the voxel centres) - worldCoords[ :, :, xax] -= 0.5 * xpixdim - worldCoords[ ::2, :, yax] -= 0.5 * ypixdim - worldCoords[1::2, :, yax] += 0.5 * ypixdim - - vertsPerRow = 2 * (xlen + 1) - dVertsPerRow = 2 * (xlen + 1) + 2 - nindices = ylen * dVertsPerRow - 2 - - indices = np.zeros(nindices, dtype=np.uint32) - - for yi, xi in it.product(range(ylen), range(xlen + 1)): - - ii = yi * dVertsPerRow + 2 * xi - vi = yi * vertsPerRow + xi - - indices[ii] = vi - indices[ii + 1] = vi + xlen + 1 - - # add degenerate vertices at the end - # every row (but not needed for last - # row) - if xi == xlen and yi < ylen - 1: - indices[ii + 2] = vi + xlen + 1 - indices[ii + 3] = (yi + 1) * vertsPerRow - - worldCoords = worldCoords.reshape((xlen + 1) * (2 * ylen), 3) - texCoords = texCoords .reshape((xlen + 1) * (2 * ylen), 3) - - return worldCoords, texCoords, indices - - -def voxelGrid(points, xax, yax, xpixdim, ypixdim): - """Given a ``N*3`` array of ``points`` (assumed to be voxel - coordinates), creates an array of vertices which can be used - to render each point as an unfilled rectangle. - - :arg points: An ``N*3`` array of voxel xyz coordinates - - :arg xax: XYZ axis index that maps to the horizontal scren axis - - :arg yax: XYZ axis index that maps to the vertical scren axis - - :arg xpixdim: Length of a voxel along the x axis. - - :arg ypixdim: Length of a voxel along the y axis. - """ - - if len(points.shape) == 1: - points = points.reshape(1, 3) - - npoints = points.shape[0] - vertices = np.repeat(np.array(points, dtype=np.float32), 4, axis=0) - - xpixdim = xpixdim / 2.0 - ypixdim = ypixdim / 2.0 - - # bottom left corner - vertices[ ::4, xax] -= xpixdim - vertices[ ::4, yax] -= ypixdim - - # bottom right - vertices[1::4, xax] += xpixdim - vertices[1::4, yax] -= ypixdim - - # top left - vertices[2::4, xax] -= xpixdim - vertices[2::4, yax] += ypixdim - - # top right - vertices[3::4, xax] += xpixdim - vertices[3::4, yax] += ypixdim - - # each square is rendered as four lines - indices = np.array([0, 1, 0, 2, 1, 3, 2, 3], dtype=np.uint32) - indices = np.tile(indices, npoints) - - indices = (indices.T + - np.repeat(np.arange(0, npoints * 4, 4, dtype=np.uint32), 8)).T - - return vertices, indices - - -def slice2D(dataShape, xax, yax, voxToDisplayMat): - """Generates and returns four vertices which denote a slice through an - array of the given ``dataShape``, parallel to the plane defined by the - given ``xax`` and ``yax``, in the space defined by the given - ``voxToDisplayMat``. - - :arg dataShape: Number of elements along each dimension in the - image data. - - :arg xax: Index of display axis which corresponds to the - horizontal screen axis. - - :arg yax: Index of display axis which corresponds to the - vertical screen axis. - - :arg voxToDisplayMat: Affine transformation matrix which transforms from - voxel/array indices into the display coordinate - system. - - Returns a tuple containing: - - - A ``4*3`` ``numpy.float32`` array containing the vertex locations - of a slice through the data. The values along the ``Z`` axis are set - to ``0``. - - - A ``numpy.uint32`` array to be used as vertex indices. - """ - - xmin, xmax = transform.axisBounds(dataShape, voxToDisplayMat, xax) - ymin, ymax = transform.axisBounds(dataShape, voxToDisplayMat, yax) - - worldCoords = np.zeros((4, 3), dtype=np.float32) - - worldCoords[0, [xax, yax]] = (xmin, ymin) - worldCoords[1, [xax, yax]] = (xmin, ymax) - worldCoords[2, [xax, yax]] = (xmax, ymin) - worldCoords[3, [xax, yax]] = (xmax, ymax) - - indices = np.arange(0, 4, dtype=np.uint32) - - return worldCoords, indices - - -def subsample(data, resolution, pixdim=None, volume=None): - """Samples the given data according to the given resolution. - - :arg data: The data to be sampled. - - :arg resolution: Sampling resolution, proportional to the values in - ``pixdim``. - - :arg pixdim: Length of each dimension in the input data (defaults to - ``(1.0, 1.0, 1.0)``). - - :arg volume: If the image is a 4D volume, the volume index of the 3D - image to be sampled. - """ - - if pixdim is None: - pixdim = (1.0, 1.0, 1.0) - - xstep = np.round(resolution / pixdim[0]) - ystep = np.round(resolution / pixdim[1]) - zstep = np.round(resolution / pixdim[2]) - - if xstep < 1: xstep = 1 - if ystep < 1: ystep = 1 - if zstep < 1: zstep = 1 - - xstart = xstep / 2 - ystart = ystep / 2 - zstart = zstep / 2 - - if volume is not None: - if len(data.shape) > 3: sample = data[xstart::xstep, - ystart::ystep, - zstart::zstep, - volume] - else: sample = data[xstart::xstep, - ystart::ystep, - zstart::zstep] - else: - if len(data.shape) > 3: sample = data[xstart::xstep, - ystart::ystep, - zstart::zstep, - :] - else: sample = data[xstart::xstep, - ystart::ystep, - zstart::zstep] - - return sample - - -def broadcast(vertices, indices, zposes, xforms, zax): - """Given a set of vertices and indices (assumed to be 2D representations - of some geometry in a 3D space, with the depth axis specified by ``zax``), - replicates them across all of the specified Z positions, applying the - corresponding transformation to each set of vertices. - - :arg vertices: Vertex array (a ``N*3`` numpy array). - - :arg indices: Index array. - - :arg zposes: Positions along the depth axis at which the vertices - are to be replicated. - - :arg xforms: Sequence of transformation matrices, one for each - Z position. - - :arg zax: Index of the 'depth' axis - - Returns three values: - - - A numpy array containing all of the generated vertices - - - A numpy array containing the original vertices for each of the - generated vertices, which may be used as texture coordinates - - - A new numpy array containing all of the generated indices. - """ - - vertices = np.array(vertices) - indices = np.array(indices) - - nverts = vertices.shape[0] - nidxs = indices.shape[ 0] - - allTexCoords = np.zeros((nverts * len(zposes), 3), dtype=np.float32) - allVertCoords = np.zeros((nverts * len(zposes), 3), dtype=np.float32) - allIndices = np.zeros( nidxs * len(zposes), dtype=np.uint32) - - for i, (zpos, xform) in enumerate(zip(zposes, xforms)): - - vertices[:, zax] = zpos - - vStart = i * nverts - vEnd = vStart + nverts - - iStart = i * nidxs - iEnd = iStart + nidxs + if display.transform in ('id', 'pixdim'): - allIndices[ iStart:iEnd] = indices + i * nverts - allTexCoords[ vStart:vEnd, :] = vertices - allVertCoords[vStart:vEnd, :] = transform.transform(vertices, xform) + pixdim = np.array(image.pixdim[:3]) + steps = [res, res, res] / pixdim + res = image.shape[:3] / steps + + return np.array(res.round(), dtype=np.uint32) - return allVertCoords, allTexCoords, allIndices + else: + lo, hi = display.getDisplayBounds() + minres = int(round(((hi - lo) / res).min())) + return [minres] * 3 diff --git a/fsl/fslview/gl/glrgbvector.py b/fsl/fslview/gl/glrgbvector.py new file mode 100644 index 0000000000000000000000000000000000000000..6d78c29f045d1c86c501f9358432e680c1307427 --- /dev/null +++ b/fsl/fslview/gl/glrgbvector.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# +# glrgbvector.py - Display vector images in RGB mode. +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# +"""This mdoule provides the :class:`GLRGBVector` class, for displaying 3D +vector images in RGB mode. +""" + +import numpy as np +import fsl.fslview.gl as fslgl +import fsl.fslview.gl.routines as glroutines +import fsl.fslview.gl.glvector as glvector +import fsl.utils.transform as transform + + +class GLRGBVector(glvector.GLVector): + + + def __prefilter(self, data): + return np.abs(data) + + + def __init__(self, image, display): + + glvector.GLVector.__init__(self, image, display, self.__prefilter) + fslgl.glrgbvector_funcs.init(self) + + + def generateVertices(self, zpos, xform): + vertices, voxCoords, texCoords = glroutines.slice2D( + self.image.shape[:3], + self.xax, + self.yax, + zpos, + self.display.getTransform('voxel', 'display'), + self.display.getTransform('display', 'voxel')) + + if xform is not None: + vertices = transform.transform(vertices, xform) + + return vertices, voxCoords, texCoords + + + def compileShaders(self): + fslgl.glrgbvector_funcs.compileShaders(self) + + + def updateShaderState(self): + fslgl.glrgbvector_funcs.updateShaderState(self) + + + def preDraw(self): + glvector.GLVector.preDraw(self) + fslgl.glrgbvector_funcs.preDraw(self) + + + def draw(self, zpos, xform=None): + fslgl.glrgbvector_funcs.draw(self, zpos, xform) + + + def drawAll(self, zposes, xforms): + fslgl.glrgbvector_funcs.drawAll(self, zposes, xforms) + + + def postDraw(self): + glvector.GLVector.postDraw(self) + fslgl.glrgbvector_funcs.postDraw(self) diff --git a/fsl/fslview/gl/glvector.py b/fsl/fslview/gl/glvector.py index 04f50585811485360fd81e79d21de83c817aa3ef..dfd6ffc029fbf3bdc7150970963d8ae75cd2cd3c 100644 --- a/fsl/fslview/gl/glvector.py +++ b/fsl/fslview/gl/glvector.py @@ -8,94 +8,40 @@ """Defines the :class:`GLVector` class, which encapsulates the logic for rendering 2D slices of a ``X*Y*Z*3`` image as a vector. The ``GLVector`` class provides the interface defined by the -:class:`~fsl.fslview.gl.globject.GLObject` class. +:class:`.GLObject` class. -A ``GLVector`` instance may be used to render an -:class:`~fsl.data.image.Image` instance which has an ``imageType`` of -``vector``. It is assumed that this ``Image`` instance is associated with -a :class:`~fsl.fslview.displaycontext.display.Display` instance which contains -a :class:`~fsl.fslview.dislpaycontext.vectoropts.VectorOpts` instance, -containing options specific to vector rendering. -Vectors can be displayed in one of several 'modes', defined by the -``VectorOpts.displayMode`` option: - - - ``rgb``: The magnitude of the vector at each voxel along each axis is - displayed as a combination of three colours. - - - `line``: The vector at each voxel is rendered as an undirected line. - -The :class:`GLVector` class makes use of the functions defined in the -:mod:`fsl.fslview.gl.gl14.glvector_funcs` or the -:mod:`fsl.fslview.gl.gl21.glvector_funcs` modules, which provide OpenGL -version specific details for creation/storage of vertex data, and for -rendering. - -These version dependent modules must provide the following functions: - - - ``init(GLVector)``: Initialise any necessary OpenGL shaders, textures, - buffers, etc. - - - ``destroy(GLVector)``: Clean up any OpenGL data/state. - - - ``setAxes(GLVector)``: Create the necessary geometry, ensuring that it is - oriented in such a way as to be displayed with ``GLVector.xax`` mapping to - the horizontal screen axis, ``GLVector.yax`` to the vertical axis, and - ``GLVector.yax`` to the depth axis. - - - ``preDraw(GLVector)``: Prepare GL state ready for drawing. - - - ``draw(GLVector, zpos, xform=None)``: Draw a slice of the vector image - at the given ``zpos``. - - - ``postDraw(GLVector)``: Clean up GL state after drawing. +The ``GLVector`` class is a base class whcih is not intended to be +instantiated directly. The :class:`.GLRGBVector` and :class:`.GLLineVector` +subclasses should be used instead. These two subclasses share the +functionality provided by this class. The vector image is stored on the GPU as a 3D RGB texture, where the ``R`` channel contains the ``x`` vector values, the ``G`` channel the ``y`` values, -and the ``B`` channel the ``z`` values. In ``line`` mode, this 3D texture -contains the vector data normalised, but unchanged. However, in ``rgb`` mode, -the absolute vaues of the vector data are stored. This is necessary to -allow for GPU based interpolation of RGB vector images. - -In both ``rgb`` and ``line`` mode, three 1D textures are used to store a -colour table for each of the ``x``, ``y`` and ``z`` components. A custom -fragment shader program looks up the ``xyz`` vector values, looks up colours -for each of them, and combines the three colours to form the final fragment -colour. - -When in ``rgb`` mode, 2D slices of the image are rendered using a simple -rectangular slice through the texture. When in ``line`` mode, each voxel -is rendered using two vertices, which are aligned in the direction of the -vector. +and the ``B`` channel the ``z`` values. -The colour of each vector may be modulated by another image, specified by the -:attr:`~fsl.fslview.displaycontext.vectoropts.VectorOpts.modulate` property. -This modulation image is stored as a 3D single-channel texture. -""" -import numpy as np -import OpenGL.GL as gl +Three 1D textures are used to store a colour table for each of the ``x``, +``y`` and ``z`` components. A custom fragment shader program looks up the +``xyz`` vector values, looks up colours for each of them, and combines the +three colours to form the final fragment colour. -import fsl.data.image as fslimage -import fsl.fslview.gl as fslgl -import fsl.fslview.gl.textures as fsltextures -import fsl.fslview.gl.globject as globject +The colour of each vector may be modulated by another image, specified by the +:attr:`.VectorOpts.modulate` property. This modulation image is stored as a +3D single-channel texture. +""" -def _lineModePrefilter(data): - """Prefilter method for the vector image texture, when it is being - displayed in ``line`` mode - see - :meth:`~fsl.fslview.gl.textures.ImageTexture.setPrefilter`. - """ - return data.transpose((3, 0, 1, 2)) +import numpy as np +import OpenGL.GL as gl +import fsl.data.image as fslimage +import fsl.fslview.colourmaps as fslcm +import fsl.fslview.gl.resources as glresources +import fsl.fslview.gl.textures as textures +import fsl.fslview.gl.globject as globject -def _rgbModePrefilter(data): - """Prefilter method for the vector image texture, when it is being - displayed in ``rgb`` mode. - """ - return np.abs(data.transpose((3, 0, 1, 2))) class GLVector(globject.GLImageObject): @@ -103,175 +49,160 @@ class GLVector(globject.GLImageObject): to render 2D slices of a ``X*Y*Z*3`` image as vectors. """ - def __init__(self, image, display): + def __init__(self, image, display, prefilter=None): """Create a :class:`GLVector` object bound to the given image and display. - :arg image: A :class:`~fsl.data.image.Image` object. - - :arg imageDisplay: A :class:`~fsl.fslview.displaycontext.Display` - object which describes how the image is to be - displayed . - """ - - if not image.is4DImage() or image.shape[3] != 3: - raise ValueError('Image must be 4 dimensional, with 3 volumes ' - 'representing the XYZ vector angles') - - globject.GLImageObject.__init__(self, image, display) - self._ready = False - - - def init(self): - """Initialise the OpenGL data required to render the given image. - + Initialises the OpenGL data required to render the given image. This method does the following: - Creates the image texture, the modulate texture, and the three colour map textures. - - Adds listeners to the - :class:`~fsl.fslview.displaycontext.display.Display` and - :class:`~fsl.fslview.displaycontext.vectoropts.VectorOpts` + - Adds listeners to the :class:`.Display` and :class:`.VectorOpts` instances, so the textures and geometry can be updated when necessary. - - Calls the GTL version specific ``glvector_funcs.init`` function. + :arg image: An :class:`.Image` object. + + :arg display: A :class:`.Display` object which describes how the + image is to be displayed. + + :arg prefilter: An optional function which filters the data before it + is stored as a 3D texture. See :class:`.ImageTexture`. + Whether or not this function is provided, the data is + transposed so that the fourth dimension is the fastest + changing. """ + if not image.is4DImage() or image.shape[3] != 3: + raise ValueError('Image must be 4 dimensional, with 3 volumes ' + 'representing the XYZ vector angles') + + globject.GLImageObject.__init__(self, image, display) + display = self.display opts = self.displayOpts name = self.name - self.xColourTexture = gl.glGenTextures(1) - self.yColourTexture = gl.glGenTextures(1) - self.zColourTexture = gl.glGenTextures(1) + self.xColourTexture = textures.ColourMapTexture('{}_x'.format(name)) + self.yColourTexture = textures.ColourMapTexture('{}_y'.format(name)) + self.zColourTexture = textures.ColourMapTexture('{}_z'.format(name)) self.modTexture = None self.imageTexture = None def modUpdate( *a): self.refreshModulateTexture() + self.updateShaderState() + self.onUpdate() def cmapUpdate(*a): self.refreshColourTextures() + self.updateShaderState() + self.onUpdate() + + def shaderUpdate(*a): + self.updateShaderState() + self.onUpdate() + + def shaderCompile(*a): + self.compileShaders() + self.updateShaderState() + self.onUpdate() + + display.addListener('interpolation', name, shaderUpdate) + display.addListener('softwareMode', name, shaderCompile) + display.addListener('alpha', name, cmapUpdate) + display.addListener('brightness', name, cmapUpdate) + display.addListener('contrast', name, cmapUpdate) + opts .addListener('xColour', name, cmapUpdate) + opts .addListener('yColour', name, cmapUpdate) + opts .addListener('zColour', name, cmapUpdate) + opts .addListener('suppressX', name, cmapUpdate) + opts .addListener('suppressY', name, cmapUpdate) + opts .addListener('suppressZ', name, cmapUpdate) + opts .addListener('modulate', name, modUpdate) + opts .addListener('modThreshold', name, shaderUpdate) + + # the fourth dimension (the vector directions) + # must be the fastest changing in the texture data + if prefilter is None: + realPrefilter = lambda d: d.transpose((3, 0, 1, 2)) + else: + realPrefilter = lambda d: prefilter(d.transpose((3, 0, 1, 2))) - def modeChange(*a): - self._onModeChange() - - def coordUpdate(*a): - self.setAxes(self.xax, self.yax) - - display.addListener('alpha', name, cmapUpdate) - display.addListener('transform', name, coordUpdate) - display.addListener('resolution', name, coordUpdate) - opts .addListener('xColour', name, cmapUpdate) - opts .addListener('yColour', name, cmapUpdate) - opts .addListener('zColour', name, cmapUpdate) - opts .addListener('suppressX', name, cmapUpdate) - opts .addListener('suppressY', name, cmapUpdate) - opts .addListener('suppressZ', name, cmapUpdate) - opts .addListener('modulate', name, modUpdate) - opts .addListener('displayMode', name, modeChange) - - if opts.displayMode == 'line': prefilter = _lineModePrefilter - elif opts.displayMode == 'rgb': prefilter = _rgbModePrefilter - - self.imageTexture = fsltextures.getTexture( + texName = '{}_{}'.format(type(self).__name__, id(self.image)) + self.imageTexture = glresources.get( + texName, + textures.ImageTexture, + texName, self.image, - type(self).__name__, display=self.display, nvals=3, normalise=True, - prefilter=prefilter) + prefilter=realPrefilter) self.refreshModulateTexture() self.refreshColourTextures() - fslgl.glvector_funcs.init(self) - - self._ready = True - def destroy(self): - """Deletes the GL textures, deregisters the listeners - configured in :meth:`init`, and calls the GL version specific - ``glvector_funcs.destroy`` function. - """ - - gl.glDeleteTextures(self.xColourTexture) - gl.glDeleteTextures(self.yColourTexture) - gl.glDeleteTextures(self.zColourTexture) + """Deletes the GL textures, and deregisters the listeners configured in + :meth:`__init__`. - fsltextures.deleteTexture(self.imageTexture) - fsltextures.deleteTexture(self.modTexture) - - self.display .removeListener('alpha', self.name) - self.display .removeListener('transform', self.name) - self.display .removeListener('resolution', self.name) - self.displayOpts.removeListener('xColour', self.name) - self.displayOpts.removeListener('yColour', self.name) - self.displayOpts.removeListener('zColour', self.name) - self.displayOpts.removeListener('suppressX', self.name) - self.displayOpts.removeListener('suppressY', self.name) - self.displayOpts.removeListener('suppressZ', self.name) - self.displayOpts.removeListener('modulate', self.name) - self.displayOpts.removeListener('displayMode', self.name) + This method must be called by subclass implementations. + """ - fslgl.glvector_funcs.destroy(self) + self.xColourTexture.destroy() + self.yColourTexture.destroy() + self.zColourTexture.destroy() - - def ready(self): - """Returns `True` when the OpenGL data/state has been initialised, - and the image is ready to be drawn, `False` before. - """ - return self._ready + glresources.delete(self.imageTexture.getTextureName()) + glresources.delete(self.modTexture .getTextureName()) + self.imageTexture = None + self.modTexture = None - def _onModeChange(self, *a): - """Called when the - :attr:`~fsl.fslview.displaycontext.vectoropts.VectorOpts.displayMode` - property changes. + self.display .removeListener('interpolation', self.name) + self.display .removeListener('softwareMode', self.name) + self.display .removeListener('alpha', self.name) + self.display .removeListener('brightness', self.name) + self.display .removeListener('contrast', self.name) + self.displayOpts.removeListener('xColour', self.name) + self.displayOpts.removeListener('yColour', self.name) + self.displayOpts.removeListener('zColour', self.name) + self.displayOpts.removeListener('suppressX', self.name) + self.displayOpts.removeListener('suppressY', self.name) + self.displayOpts.removeListener('suppressZ', self.name) + self.displayOpts.removeListener('modulate', self.name) + self.displayOpts.removeListener('modThreshold', self.name) - Initialises data and GL state for the newly selected vector display - mode. - """ - mode = self.displayOpts.displayMode + def updateShaderState(self): + """This method must be provided by subclasses.""" + raise NotImplementedError('updateShaderState must be implemented by ' + '{} subclasses'.format(type(self).__name__)) - # Disable atexture interpolation in line mode - if mode == 'line': - - if self.display.interpolation != 'none': - self.display.interpolation = 'none' - - self.display.disableProperty('interpolation') - - elif mode == 'rgb': - self.display.enableProperty('interpolation') + + def compileShaders(self): + """This method must be provided by subclasses.""" + raise NotImplementedError('compileShaders must be implemented by ' + '{} subclasses'.format(type(self).__name__)) - if mode == 'line': prefilter = _lineModePrefilter - elif mode == 'rgb': prefilter = _rgbModePrefilter - - fslgl.glvector_funcs.destroy(self) - self.imageTexture.setPrefilter(prefilter) - fslgl.glvector_funcs.init(self) - self.setAxes(self.xax, self.yax) - def refreshModulateTexture(self): - """Called when the - :attr`~fsl.fslview.displaycontext.vectoropts.VectorOpts.modulate` - property changes. + """Called when the :attr`.VectorOpts.modulate` property changes. Reconfigures the modulation texture. If no modulation image is selected, a 'dummy' texture is creatad, which contains all white values (and which result in the modulation texture having no effect). """ - modImage = self.displayOpts.modulate - if self.modTexture is not None: - fsltextures.deleteTexture(self.modTexture) + glresources.delete(self.modTexture.getTextureName()) + self.modTexture = None + + modImage = self.displayOpts.modulate if modImage == 'none': textureData = np.zeros((5, 5, 5), dtype=np.uint8) @@ -284,39 +215,49 @@ class GLVector(globject.GLImageObject): modDisplay = self.display norm = True - self.modTexture = fsltextures.getTexture( + texName = '{}_{}_{}_modulate'.format( + type(self).__name__, id(self.image), id(modImage)) + self.modTexture = glresources.get( + texName, + textures.ImageTexture, + texName, modImage, - '{}_{}_modulate'.format(type(self).__name__, id(self.image)), display=modDisplay, normalise=norm) def refreshColourTextures(self, colourRes=256): - """Called when the component colour maps need to be updated, when - one of the - :attr:`~fsl.fslview.displaycontext.vectoropts.VectorOpts.xColour`, - ``yColour``, ``zColour``, ``suppressX``, ``suppressY``, or - ``suppressZ`` properties change. + """Called when the component colour maps need to be updated, when one + of the :attr:`.VectorOpts.xColour`, ``yColour``, ``zColour``, + ``suppressX``, ``suppressY``, or ``suppressZ`` properties change. Regenerates the colour textures. """ - xcol = self.displayOpts.xColour - ycol = self.displayOpts.yColour - zcol = self.displayOpts.zColour + display = self.display + opts = self.displayOpts + + xcol = opts.xColour + ycol = opts.yColour + zcol = opts.zColour xcol[3] = 1.0 ycol[3] = 1.0 zcol[3] = 1.0 - xsup = self.displayOpts.suppressX - ysup = self.displayOpts.suppressY - zsup = self.displayOpts.suppressZ + xsup = opts.suppressX + ysup = opts.suppressY + zsup = opts.suppressZ xtex = self.xColourTexture ytex = self.yColourTexture ztex = self.zColourTexture + drange = fslcm.briconToDisplayRange( + (0.0, 1.0), + display.brightness / 100.0, + display.contrast / 100.0) + for colour, texture, suppress in zip( (xcol, ycol, zcol), (xtex, ytex, ztex), @@ -325,127 +266,50 @@ class GLVector(globject.GLImageObject): if not suppress: cmap = np.array( - [np.linspace(0.0, i, colourRes) for i in colour]) + [np.linspace(0.0, i, colourRes) for i in colour]).T # Component magnitudes of 0 are # transparent, but any other # magnitude is fully opaque - cmap[3, :] = 1.0 - cmap[3, 0] = 0.0 + cmap[:, 3] = display.alpha / 100.0 + cmap[0, 3] = 0.0 else: - cmap = np.zeros((4, colourRes)) - - cmap = np.array(np.floor(cmap * 255), dtype=np.uint8).ravel('F') - - gl.glBindTexture(gl.GL_TEXTURE_1D, texture) - gl.glTexParameteri(gl.GL_TEXTURE_1D, - gl.GL_TEXTURE_MAG_FILTER, - gl.GL_NEAREST) - gl.glTexParameteri(gl.GL_TEXTURE_1D, - gl.GL_TEXTURE_MIN_FILTER, - gl.GL_NEAREST) - gl.glTexParameteri(gl.GL_TEXTURE_1D, - gl.GL_TEXTURE_WRAP_S, - gl.GL_CLAMP_TO_EDGE) - - gl.glTexImage1D(gl.GL_TEXTURE_1D, - 0, - gl.GL_RGBA8, - colourRes, - 0, - gl.GL_RGBA, - gl.GL_UNSIGNED_BYTE, - cmap) - - gl.glBindTexture(gl.GL_TEXTURE_1D, 0) - + cmap = np.zeros((colourRes, 4)) + + texture.set(cmap=cmap, displayRange=drange) + def setAxes(self, xax, yax): - """Calls the GL version-specific ``glvector_funcs.setAxes`` function, - which should make sure that the GL geometry representation is up - to date. - """ + """Stores the new x/y/z axes.""" self.xax = xax self.yax = yax self.zax = 3 - xax - yax - fslgl.glvector_funcs.setAxes(self) - def preDraw(self): - """Ensures that the five textures (the vector and modulation images, - and the three colour textures) are bound, then calls - ``glvector_funcs.preDraw``. - """ - if not self.display.enabled: - return - - gl.glActiveTexture(gl.GL_TEXTURE0) - gl.glEnable(gl.GL_TEXTURE_3D) - gl.glBindTexture(gl.GL_TEXTURE_3D, self.imageTexture.texture) - - gl.glActiveTexture(gl.GL_TEXTURE1) - gl.glEnable(gl.GL_TEXTURE_3D) - gl.glBindTexture(gl.GL_TEXTURE_3D, self.modTexture.texture) - - gl.glActiveTexture(gl.GL_TEXTURE2) - gl.glEnable(gl.GL_TEXTURE_1D) - gl.glBindTexture(gl.GL_TEXTURE_1D, self.xColourTexture) - - gl.glActiveTexture(gl.GL_TEXTURE3) - gl.glEnable(gl.GL_TEXTURE_1D) - gl.glBindTexture(gl.GL_TEXTURE_1D, self.yColourTexture) - - gl.glActiveTexture(gl.GL_TEXTURE4) - gl.glEnable(gl.GL_TEXTURE_1D) - gl.glBindTexture(gl.GL_TEXTURE_1D, self.zColourTexture) - - fslgl.glvector_funcs.preDraw(self) - - - def draw(self, zpos, xform=None): - """Calls the ``glvector_funcs.draw`` function. """ - - if not self.display.enabled: - return - - fslgl.glvector_funcs.draw(self, zpos, xform) + """Must be called by subclass implementations. - def drawAll(self, zposes, xforms=None): - """Calls the ``glvector_funcs.drawAll`` function. """ - - if not self.display.enabled: - return + Ensures that the five textures (the vector and modulation images, + and the three colour textures) are bound to texture units 0-4 + respectively. + """ - fslgl.glvector_funcs.drawAll(self, zposes, xforms) + self.imageTexture .bindTexture(gl.GL_TEXTURE0) + self.modTexture .bindTexture(gl.GL_TEXTURE1) + self.xColourTexture.bindTexture(gl.GL_TEXTURE2) + self.yColourTexture.bindTexture(gl.GL_TEXTURE3) + self.zColourTexture.bindTexture(gl.GL_TEXTURE4) def postDraw(self): - """Unbindes the five GL textures, and calls the - ``glvector_funcs.postDraw`` function. - """ - if not self.display.enabled: - return + """Must be called by subclass implementations. - gl.glActiveTexture(gl.GL_TEXTURE0) - gl.glBindTexture(gl.GL_TEXTURE_3D, 0) - gl.glDisable(gl.GL_TEXTURE_3D) - - gl.glActiveTexture(gl.GL_TEXTURE1) - gl.glBindTexture(gl.GL_TEXTURE_3D, 0) - gl.glDisable(gl.GL_TEXTURE_3D) - - gl.glActiveTexture(gl.GL_TEXTURE2) - gl.glBindTexture(gl.GL_TEXTURE_1D, 0) - gl.glDisable(gl.GL_TEXTURE_1D) - - gl.glActiveTexture(gl.GL_TEXTURE3) - gl.glBindTexture(gl.GL_TEXTURE_1D, 0) - gl.glDisable(gl.GL_TEXTURE_1D) + Unbindes the five GL textures. + """ - gl.glActiveTexture(gl.GL_TEXTURE4) - gl.glBindTexture(gl.GL_TEXTURE_1D, 0) - gl.glDisable(gl.GL_TEXTURE_1D) - - fslgl.glvector_funcs.postDraw(self) + self.imageTexture .unbindTexture() + self.modTexture .unbindTexture() + self.xColourTexture.unbindTexture() + self.yColourTexture.unbindTexture() + self.zColourTexture.unbindTexture() diff --git a/fsl/fslview/gl/glvolume.py b/fsl/fslview/gl/glvolume.py index f0eb5bbefb7fb4024cf1362136d9254e2d02bfc6..55800ec6818f96ed3f1a56ed1ad90368ffe654fc 100644 --- a/fsl/fslview/gl/glvolume.py +++ b/fsl/fslview/gl/glvolume.py @@ -29,8 +29,10 @@ These version dependent modules must provide the following functions: - ``destroy(GLVolume)``: Perform any necessary clean up. - - ``genVertexData(GLVolume)``: Create and prepare vertex coordinates, using - the :meth:`GLVolume.genVertexData` method. + - ``compileShaders(GLVolume)``: (Re-)Compile the shader programs. + + - ``updateShaderState(GLVolume)``: Updates the shader program states + when display parameters are changed. - ``preDraw()``: Initialise the GL state, ready for drawing. @@ -38,25 +40,29 @@ These version dependent modules must provide the following functions: given Z position. If xform is not None, it must be applied as a transformation on the vertex coordinates. + - ``drawAll(Glvolume, zposes, xforms)`` - Draws slices at each of the + specified ``zposes``, applying the corresponding ``xforms`` to each. + - ``postDraw()``: Clear the GL state after drawing. Images are rendered in essentially the same way, regardless of which OpenGL version-specific module is used. The image data itself is stored on the GPU as a 3D texture, and the current colour map as a 1D texture. A slice through -the texture is rendered using four vertices, located at the respective corners +the texture is rendered using six vertices, located at the respective corners of the image bounds. - """ import logging log = logging.getLogger(__name__) -import OpenGL.GL as gl -import numpy as np +import OpenGL.GL as gl -import fsl.fslview.gl as fslgl -import fsl.fslview.gl.textures as fsltextures -import fsl.fslview.gl.globject as globject +import fsl.utils.transform as transform +import fsl.fslview.gl as fslgl +import fsl.fslview.gl.textures as textures +import fsl.fslview.gl.resources as glresources +import fsl.fslview.gl.globject as globject +import fsl.fslview.gl.routines as glroutines class GLVolume(globject.GLImageObject): @@ -68,6 +74,8 @@ class GLVolume(globject.GLImageObject): """Creates a GLVolume object bound to the given image, and associated image display. + Initialises the OpenGL data required to render the given image. + :arg image: A :class:`~fsl.data.image.Image` object. :arg display: A :class:`~fsl.fslview.displaycontext.Display` @@ -76,57 +84,37 @@ class GLVolume(globject.GLImageObject): """ globject.GLImageObject.__init__(self, image, display) - - self._ready = False - - - def ready(self): - """Returns `True` when the OpenGL data/state has been initialised, - and the image is ready to be drawn, `False` before. - """ - return self._ready - - def init(self): - """Initialise the OpenGL data required to render the given image. - - The real initialisation takes place in this method - it must - only be called after an OpenGL context has been created. - """ - # Add listeners to this image so the view can be # updated when its display properties are changed self.addDisplayListeners() - fslgl.glvolume_funcs.init(self) + # Create an image texture and a colour map texture + texName = '{}_{}'.format(type(self).__name__, id(self.image)) - self.imageTexture = fsltextures.getTexture( - self.image, type(self).__name__, self.display) + # The image texture may be used elsewhere, + # so we'll use the resource management + # module rather than creating one directly + self.imageTexture = glresources.get( + texName, + textures.ImageTexture, + texName, + self.image, + self.display) - # The colour map, used for converting - # image data to a RGBA colour. - self.colourTexture = gl.glGenTextures(1) - log.debug('Created GL texture: {}'.format(self.colourTexture)) + self.colourTexture = textures.ColourMapTexture(texName) - self.colourResolution = 256 - self.refreshColourTexture(self.colourResolution) + self.refreshColourTexture() - self._ready = True + fslgl.glvolume_funcs.init(self) def setAxes(self, xax, yax): - """This method should be called when the image display axes change. + """This method should be called when the image display axes change.""" - It regenerates vertex information accordingly. - """ - - self.xax = xax - self.yax = yax - self.zax = 3 - xax - yax - wc, idxs, nverts = fslgl.glvolume_funcs.genVertexData(self) - self.worldCoords = wc - self.indices = idxs - self.nVertices = nverts + self.xax = xax + self.yax = yax + self.zax = 3 - xax - yax def preDraw(self): @@ -134,21 +122,43 @@ class GLVolume(globject.GLImageObject): instance. """ - if not self.display.enabled: - return - - # Set up the image data texture - gl.glActiveTexture(gl.GL_TEXTURE0) - gl.glEnable(gl.GL_TEXTURE_3D) - gl.glBindTexture(gl.GL_TEXTURE_3D, self.imageTexture.texture) - - # Set up the colour map texture - gl.glActiveTexture(gl.GL_TEXTURE1) - gl.glEnable(gl.GL_TEXTURE_1D) - gl.glBindTexture(gl.GL_TEXTURE_1D, self.colourTexture) - + # Set up the image and colour textures + self.imageTexture .bindTexture(gl.GL_TEXTURE0) + self.colourTexture.bindTexture(gl.GL_TEXTURE1) + fslgl.glvolume_funcs.preDraw(self) + + def generateVertices(self, zpos, xform=None): + """Generates vertex coordinates for a 2D slice through the given + ``zpos``, with the optional ``xform`` applied to the coordinates. + + This method is called by the :mod:`.gl14.glvolume_funcs` and + :mod:`.gl21.glvolume_funcs` modules. + + A tuple of three values is returned, containing: + + - A ``6*3 numpy.float32`` array containing the vertex coordinates + + - A ``6*3 numpy.float32`` array containing the voxel coordinates + corresponding to each vertex + + - A ``6*3 numpy.float32`` array containing the texture coordinates + corresponding to each vertex + """ + vertices, voxCoords, texCoords = glroutines.slice2D( + self.image.shape[:3], + self.xax, + self.yax, + zpos, + self.display.getTransform('voxel', 'display'), + self.display.getTransform('display', 'voxel')) + + if xform is not None: + vertices = transform.transform(vertices, xform) + + return vertices, voxCoords, texCoords + def draw(self, zpos, xform=None): """Draws a 2D slice of the image at the given real world Z location. @@ -163,17 +173,12 @@ class GLVolume(globject.GLImageObject): :meth:`preDraw`, and followed by a call to :meth:`postDraw`. """ - if not self.display.enabled: - return - fslgl.glvolume_funcs.draw(self, zpos, xform) def drawAll(self, zposes, xforms): """Calls the module-specific ``drawAll`` function. """ - if not self.display.enabled: - return fslgl.glvolume_funcs.drawAll(self, zposes, xforms) @@ -181,16 +186,9 @@ class GLVolume(globject.GLImageObject): """Clears the GL state after drawing from this :class:`GLVolume` instance. """ - if not self.display.enabled: - return - gl.glActiveTexture(gl.GL_TEXTURE0) - gl.glBindTexture(gl.GL_TEXTURE_3D, 0) - gl.glDisable(gl.GL_TEXTURE_3D) - - gl.glActiveTexture(gl.GL_TEXTURE1) - gl.glBindTexture(gl.GL_TEXTURE_1D, 0) - gl.glDisable(gl.GL_TEXTURE_1D) + self.imageTexture .unbindTexture() + self.colourTexture.unbindTexture() fslgl.glvolume_funcs.postDraw(self) @@ -201,111 +199,33 @@ class GLVolume(globject.GLImageObject): deleting texture handles). """ - fsltextures.deleteTexture(self.imageTexture) + glresources.delete(self.imageTexture.getTextureName()) + + self.colourTexture.destroy() + + self.imageTexture = None + self.colourTexture = None + self.removeDisplayListeners() fslgl.glvolume_funcs.destroy(self) - - def genVertexData(self): - """Generates coordinates at the corners of the image bounds, along the - xax/yax plane, which define a slice through the 3D image. - - This method is provided for use by the version-dependent - :mod:`fsl.fslview.gl.gl14.glvolume_funcs` and - :mod:`fsl.fslview.gl.gl21.glvolume_funcs` modules, in their - implemntation of the ``genVertexData` function. - - :arg image: The :class:`~fsl.data.image.Image` object to - generate vertex and texture coordinates for. - - :arg display: A :class:`~fsl.fslview.displaycontext.ImageDisplay` - object which defines how the image is to be - rendered. - - :arg xax: The world space axis which corresponds to the - horizontal screen axis (0, 1, or 2). - - :arg yax: The world space axis which corresponds to the - vertical screen axis (0, 1, or 2). - """ - - return globject.slice2D( - self.image.shape, - self.xax, - self.yax, - self.display.getTransform('voxel', 'display')) - - def refreshColourTexture(self, colourResolution): - """Configures the colour texture used to colour image voxels. - - Also createss a transformation matrix which transforms an image voxel - value to the range (0-1), which may then be used as a texture - coordinate into the colour map texture. This matrix is stored as an - attribute of this :class:`GLVolume` object called - :attr:`colourMapXForm`. See also the :meth:`genImageTexture` method - for more details. - """ + def refreshColourTexture(self): + """Configures the colour texture used to colour image voxels.""" display = self.display opts = self.displayOpts - imin = opts.displayRange[0] - imax = opts.displayRange[1] - - # This transformation is used to transform voxel values - # from their native range to the range [0.0, 1.0], which - # is required for texture colour lookup. Values below - # or above the current display range will be mapped - # to texture coordinate values less than 0.0 or greater - # than 1.0 respectively. - if imax == imin: scale = 1 - else: scale = imax - imin - - cmapXform = np.identity(4, dtype=np.float32) - cmapXform[0, 0] = 1.0 / scale - cmapXform[3, 0] = -imin * cmapXform[0, 0] - - self.colourMapXform = cmapXform - - # Create [self.colourResolution] rgb values, - # spanning the entire range of the image - # colour map - if opts.invert: colourRange = np.linspace(1.0, 0.0, colourResolution) - else: colourRange = np.linspace(0.0, 1.0, colourResolution) - - colourmap = opts.cmap(colourRange) + alpha = display.alpha / 100.0 + cmap = opts.cmap + invert = opts.invert + dmin = opts.displayRange[0] + dmax = opts.displayRange[1] - # Apply global transparency - colourmap[:, 3] = display.alpha / 100.0 - - # The colour data is stored on - # the GPU as 8 bit rgba tuples - colourmap = np.floor(colourmap * 255) - colourmap = np.array(colourmap, dtype=np.uint8) - colourmap = colourmap.ravel(order='C') - - # GL texture creation stuff - gl.glBindTexture(gl.GL_TEXTURE_1D, self.colourTexture) - gl.glTexParameteri(gl.GL_TEXTURE_1D, - gl.GL_TEXTURE_MAG_FILTER, - gl.GL_NEAREST) - gl.glTexParameteri(gl.GL_TEXTURE_1D, - gl.GL_TEXTURE_MIN_FILTER, - gl.GL_NEAREST) - gl.glTexParameteri(gl.GL_TEXTURE_1D, - gl.GL_TEXTURE_WRAP_S, - gl.GL_CLAMP_TO_EDGE) - - gl.glTexImage1D(gl.GL_TEXTURE_1D, - 0, - gl.GL_RGBA8, - colourResolution, - 0, - gl.GL_RGBA, - gl.GL_UNSIGNED_BYTE, - colourmap) - gl.glBindTexture(gl.GL_TEXTURE_1D, 0) + self.colourTexture.set(cmap=cmap, + invert=invert, + alpha=alpha, + displayRange=(dmin, dmax)) def addDisplayListeners(self): @@ -322,18 +242,31 @@ class GLVolume(globject.GLImageObject): display = self.display opts = self.displayOpts - - lName = self.name + lName = self.name - def vertexUpdate(*a): - self.setAxes(self.xax, self.yax) - def colourUpdate(*a): - self.refreshColourTexture(self.colourResolution) + self.refreshColourTexture() + fslgl.glvolume_funcs.updateShaderState(self) + self.onUpdate() + + def shaderUpdate(*a): + fslgl.glvolume_funcs.updateShaderState(self) + self.onUpdate() + + def shaderCompile(*a): + fslgl.glvolume_funcs.compileShaders( self) + fslgl.glvolume_funcs.updateShaderState(self) + self.onUpdate() + + def update(*a): + self.onUpdate() - display.addListener('transform', lName, vertexUpdate) + display.addListener('resolution', lName, update) + display.addListener('interpolation', lName, shaderUpdate) + display.addListener('softwareMode', lName, shaderCompile) display.addListener('alpha', lName, colourUpdate) opts .addListener('displayRange', lName, colourUpdate) + opts .addListener('clippingRange', lName, shaderUpdate) opts .addListener('cmap', lName, colourUpdate) opts .addListener('invert', lName, colourUpdate) @@ -348,8 +281,11 @@ class GLVolume(globject.GLImageObject): lName = self.name - display.removeListener('transform', lName) + display.removeListener('resolution', lName) + display.removeListener('interpolation', lName) + display.removeListener('softwareMode', lName) display.removeListener('alpha', lName) opts .removeListener('displayRange', lName) + opts .removeListener('clippingRange', lName) opts .removeListener('cmap', lName) opts .removeListener('invert', lName) diff --git a/fsl/fslview/gl/lightboxcanvas.py b/fsl/fslview/gl/lightboxcanvas.py index 8bca8c62a1a143932f772e7363927f60bd8b3992..7a15fe3fe458de0f638b2ebef9e985626e348dc7 100644 --- a/fsl/fslview/gl/lightboxcanvas.py +++ b/fsl/fslview/gl/lightboxcanvas.py @@ -9,17 +9,20 @@ slices along a single axis from a collection of 3D images. """ +import sys import logging -log = logging.getLogger(__name__) - import numpy as np import OpenGL.GL as gl import props import fsl.fslview.gl.slicecanvas as slicecanvas -import fsl.fslview.gl.textures as fsltextures +import fsl.fslview.gl.resources as glresources +import fsl.fslview.gl.textures as textures + + +log = logging.getLogger(__name__) class LightBoxCanvas(slicecanvas.SliceCanvas): @@ -74,33 +77,6 @@ class LightBoxCanvas(slicecanvas.SliceCanvas): """If True, a box will be drawn around the slice containing the current location. """ - - - _labels = dict( - slicecanvas.SliceCanvas._labels.items() + - [('sliceSpacing', 'Slice spacing'), - ('ncols', 'Number of columns'), - ('nrows', 'Number of rows'), - ('topRow', 'Top row'), - ('zrange', 'Slice range'), - ('showGridLines', 'Show grid lines'), - ('highlightSlice', 'Highlight current slice')]) - """Labels for the properties which are intended to be user editable.""" - - - _tooltips = dict( - slicecanvas.SliceCanvas._tooltips.items() + - [('sliceSpacing', 'Distance (mm) between consecutive slices'), - ('ncols', 'Number of slices to display on one row'), - ('nrows', 'Number of rows to display on the canvas'), - ('topRow', 'Index number of top row (from ' - '0 to nrows-1) to display'), - ('zrange', 'Range (mm) along Z axis of slices to display'), - ('showGridLines', 'Show grid lines between slices'), - ('highlightSlice', 'Highlights the currently selected slice')]) - """Tooltips to be used as help text.""" - - _propHelp = _tooltips def worldToCanvas(self, xpos, ypos, zpos): @@ -204,6 +180,10 @@ class LightBoxCanvas(slicecanvas.SliceCanvas): self._nslices = 0 self._totalRows = 0 + # This will point to a RenderTexture if + # the offscreen render mode is enabled + self.__offscreenRenderTexture = None + slicecanvas.SliceCanvas.__init__(self, imageList, displayCtx, zax) # default to showing the entire slice range @@ -254,33 +234,76 @@ class LightBoxCanvas(slicecanvas.SliceCanvas): self._refresh() - def _onTwoStageRenderChange(self, *args, **kwargs): - """Overrides - :class:`~fsl.fslview.gl.slicecanvas.SliceCanvas._onTwoStageRenderChange`. + def _renderModeChange(self, *a): + """Overrides :meth:`.SliceCanvas._renderModeChange`. + """ + + if self.__offscreenRenderTexture is not None: + self.__offscreenRenderTexture.destroy() + self.__offscreenRenderTexture = None + + slicecanvas.SliceCanvas._renderModeChange(self, *a) + + + def _updateRenderTextures(self): + """Overrides :meth:`.SliceCanvas._updateRenderTextures`. """ - # The LightBoxCanvas does two-stage rendering + if self.renderMode == 'onscreen': + return + + # The LightBoxCanvas does offscreen rendering # a bit different to the SliceCanvas. The latter # uses a separate render texture for each image, # whereas here we're going to use a single - # render texture for all images. - if self._renderTextures == {}: - self._renderTextures = None + # render texture for all images. + elif self.renderMode == 'offscreen': + if self.__offscreenRenderTexture is not None: + self.__offscreenRenderTexture.destroy() + + self.__offscreenRenderTexture = textures.RenderTexture( + '{}_{}'.format(type(self).__name__, id(self)), + gl.GL_LINEAR) + + self.__offscreenRenderTexture.setSize(768, 768) + + # The LightBoxCanvas handles re-render mode + # the same way as the SliceCanvas - a separate + # RenderTextureStack for eacn globject + elif self.renderMode == 'prerender': - # nothing to be done - if self.twoStageRender and self._renderTextures is not None: - return - - if not self.twoStageRender and self._renderTextures is None: - return + # Delete any RenderTextureStack instances for + # images which have been removed from the list + for image, (tex, name) in self._prerenderTextures.items(): + if image not in self.imageList: + self._prerenderTextures.pop(image) + glresources.delete(name) + + # Create a RendeTextureStack for images + # which have been added to the list + for image in self.imageList: + if image in self._prerenderTextures: + continue + + globj = self._glObjects.get(image, None) + + if globj is None: + continue + + name = '{}_{}_zax{}'.format( + id(image), + textures.RenderTextureStack.__name__, + self.zax) - if not self.twoStageRender: - self._renderTextures.destroy() - self._renderTextures = None + if glresources.exists(name): + rt = glresources.get(name) + else: - else: - self._renderTextures = fsltextures.RenderTexture( - 256, 256, gl.GL_LINEAR) + rt = textures.RenderTextureStack(globj) + rt.setAxes(self.xax, self.yax) + glresources.set(name, rt) + + self._prerenderTextures[image] = rt, name self._refresh() @@ -381,8 +404,25 @@ class LightBoxCanvas(slicecanvas.SliceCanvas): # Pick a sensible default for the # slice spacing - the smallest pixdim - # across all images in the list - newZGap = min([i.pixdim[self.zax] for i in self.imageList]) + # across all images in the list + newZGap = sys.float_info.max + + for image in self.imageList: + display = self.displayCtx.getDisplayProperties(image) + + # TODO this is specific to the Image type, + # and shouldn't be. We're going to need to + # support other overlay types soon... + if display.transform == 'id': + zgap = 1 + elif display.transform == 'pixdim': + zgap = image.pixdim[self.zax] + else: + zgap = min(image.pixdim[:3]) + + if zgap < newZGap: + newZGap = zgap + newZRange = self.displayCtx.bounds.getRange(self.zax) # Changing the zrange/sliceSpacing properties will, in most cases, @@ -634,8 +674,10 @@ class LightBoxCanvas(slicecanvas.SliceCanvas): yverts[0, 0] = xmin + (col) * xlen yverts[1, 0] = xmin + (col + 1) * xlen - self.getAnnotations().line(xverts[0], xverts[1], colour=(0, 1, 0)) - self.getAnnotations().line(yverts[0], yverts[1], colour=(0, 1, 0)) + annot = self.getAnnotations() + + annot.line(xverts[0], xverts[1], colour=(0, 1, 0), width=1) + annot.line(yverts[0], yverts[1], colour=(0, 1, 0), width=1) def _draw(self, *a): @@ -645,16 +687,28 @@ class LightBoxCanvas(slicecanvas.SliceCanvas): if not self._setGLContext(): return - if self.twoStageRender: - log.debug('Rendering to off-screen frame buffer') - self._renderTextures.bindAsRenderTarget() - self._setViewport(size=self._renderTextures.getSize()) + if self.renderMode == 'offscreen': + + log.debug('Rendering to off-screen texture') + + rt = self.__offscreenRenderTexture + + lo = [None] * 3 + hi = [None] * 3 + + lo[self.xax], hi[self.xax] = self.displayBounds.x + lo[self.yax], hi[self.yax] = self.displayBounds.y + lo[self.zax], hi[self.zax] = self.zrange + + rt.bindAsRenderTarget() + rt.setRenderViewport(self.xax, self.yax, lo, hi) + gl.glClear(gl.GL_COLOR_BUFFER_BIT) else: self._setViewport() - startSlice = self.ncols * self.topRow - endSlice = startSlice + self.nrows * self.ncols + startSlice = self.ncols * self.topRow + endSlice = startSlice + self.nrows * self.ncols if endSlice > self._nslices: endSlice = self._nslices @@ -666,25 +720,42 @@ class LightBoxCanvas(slicecanvas.SliceCanvas): globj = self._glObjects.get(image, None) - if (globj is None) or (not globj.ready()) or not display.enabled: + if (globj is None) or (not display.enabled): continue - log.debug('Drawing {} slices ({} - {}) for image {}'.format( - endSlice - startSlice, startSlice, endSlice, image)) - - globj.preDraw() + log.debug('Drawing {} slices ({} - {}) for ' + 'image {} directly to canvas'.format( + endSlice - startSlice, startSlice, endSlice, image)) zposes = self._sliceLocs[ image][startSlice:endSlice] xforms = self._transforms[image][startSlice:endSlice] - globj.drawAll(zposes, xforms) + if self.renderMode == 'prerender': + rt, name = self._prerenderTextures.get(image, (None, None)) + + if rt is None: + continue + + log.debug('Drawing {} slices ({} - {}) for image {} ' + 'from pre-rendered texture'.format( + endSlice - startSlice, + startSlice, + endSlice, + image)) + + for zpos, xform in zip(zposes, xforms): + rt.draw(zpos, xform) + else: - globj.postDraw() + globj.preDraw() + globj.drawAll(zposes, xforms) + globj.postDraw() - if self.twoStageRender: - self._renderTextures.unbind() + if self.renderMode == 'offscreen': + rt.unbindAsRenderTarget() self._setViewport() - self._renderTextures.drawRender( + rt.drawOnBounds( + 0, self.displayBounds.xlo, self.displayBounds.xhi, self.displayBounds.ylo, diff --git a/fsl/fslview/gl/resources.py b/fsl/fslview/gl/resources.py new file mode 100644 index 0000000000000000000000000000000000000000..de9735671a0f98e83a6219f88440705e38178307 --- /dev/null +++ b/fsl/fslview/gl/resources.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# +# resources.py - Simple manager for shared OpenGL resources. +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import logging +log = logging.getLogger(__name__) + + +class _Resource(object): + + def __init__(self, key, resource): + self.key = key + self.resource = resource + self.refcount = 0 + + +_resources = {} + +def exists(key): + return key in _resources + + +def get(key, createFunc=None, *args, **kwargs): + + r = _resources.get(key, None) + + if r is None and createFunc is None: + raise KeyError('Resource {} does not exist'.format(str(key))) + + if r is not None: + r.refcount += 1 + + log.debug('Resource {} reference count ' + 'increased to {}'.format(str(key), r.refcount)) + + return r.resource + + if createFunc is not None: + return set(key, createFunc(*args, **kwargs)) + + +def set(key, resource, overwrite=False): + + if (not overwrite) and (key in _resources): + raise KeyError('Resource {} already exists'.format(str(key))) + + if not overwrite: + log.debug('Adding resource {}'.format(str(key))) + + r = _Resource(key, resource) + r.refcount += 1 + _resources[key] = r + + log.debug('Resource {} reference count ' + 'increased to {}'.format(str(key), r.refcount)) + + else: + log.debug('Updating resource {}'.format(str(key))) + + _resources[key].resource = resource + + return resource + + +def delete(key): + + r = _resources[key] + r.refcount -= 1 + + log.debug('Resource {} reference count ' + 'decreased to {}'.format(str(key), r.refcount)) + + if r.refcount <= 0: + + log.debug('Destroying resource {}'.format(str(key))) + + _resources.pop(key) + r.resource.destroy() diff --git a/fsl/fslview/gl/routines.py b/fsl/fslview/gl/routines.py new file mode 100644 index 0000000000000000000000000000000000000000..6b1ff056212090eec1300706456304b1b87d4e27 --- /dev/null +++ b/fsl/fslview/gl/routines.py @@ -0,0 +1,542 @@ +#!/usr/bin/env python +# +# routines.py - A collection of disparate utility functions related to OpenGL. +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import itertools as it + +import OpenGL.GL as gl +import numpy as np + +import fsl.utils.transform as transform + + +def show2D(xax, yax, width, height, lo, hi): + + zax = 3 - xax - yax + + xmin, xmax = lo[xax], hi[xax] + ymin, ymax = lo[yax], hi[yax] + zmin, zmax = lo[zax], hi[zax] + + gl.glViewport(0, 0, width, height) + gl.glMatrixMode(gl.GL_PROJECTION) + gl.glLoadIdentity() + + zdist = max(abs(zmin), abs(zmax)) + + gl.glOrtho(xmin, xmax, ymin, ymax, -zdist, zdist) + gl.glMatrixMode(gl.GL_MODELVIEW) + gl.glLoadIdentity() + + # Rotate world space so the displayed slice + # is visible and correctly oriented + # TODO There's got to be a more generic way + # to perform this rotation. This will break + # if I add functionality allowing the user + # to specifty the x/y axes on initialisation. + if zax == 0: + gl.glRotatef(-90, 1, 0, 0) + gl.glRotatef(-90, 0, 0, 1) + elif zax == 1: + gl.glRotatef(270, 1, 0, 0) + + +def calculateSamplePoints(shape, resolution, xform, xax, yax): + """Calculates a uniform grid of points, in the display coordinate system + (as specified by the given + :class:`~fsl.fslview.displaycontext.Display` object properties) along the + x-y plane (as specified by the xax/yax indices), at which the given image + should be sampled for display purposes. + + This function returns a tuple containing: + + - a numpy array of shape `(N, 3)`, containing the coordinates of the + centre of every sampling point in the display coordinate system. + + - the horizontal distance (along xax) between adjacent points + + - the vertical distance (along yax) between adjacent points + + - The number of samples along the horizontal axis (xax) + + - The number of samples along the vertical axis (yax) + + :arg shape: The shape of the data to be sampled. + + :arg xform: A transformation matrix which converts from data + coordinates to the display coordinate system. + + :arg resolution: The desired resolution in display coordinates, along + each display axis. + + :arg xax: The horizontal display coordinate system axis (0, 1, or + 2). + + :arg yax: The vertical display coordinate system axis (0, 1, or 2). + """ + + xres = resolution[xax] + yres = resolution[yax] + + # These values give the min/max x/y + # values of a bounding box which + # encapsulates the entire image, + # in the display coordinate system + xmin, xmax = transform.axisBounds(shape, xform, xax) + ymin, ymax = transform.axisBounds(shape, xform, yax) + + # Number of samples along each display + # axis, given the requested resolution + xNumSamples = np.floor((xmax - xmin) / xres) + yNumSamples = np.floor((ymax - ymin) / yres) + + # adjust the x/y resolution so + # the samples fit exactly into + # the data bounding box + xres = (xmax - xmin) / xNumSamples + yres = (ymax - ymin) / yNumSamples + + # Calculate the locations of every + # sample point in display space + worldX = np.linspace(xmin + 0.5 * xres, + xmax - 0.5 * xres, + xNumSamples) + worldY = np.linspace(ymin + 0.5 * yres, + ymax - 0.5 * yres, + yNumSamples) + + worldX, worldY = np.meshgrid(worldX, worldY) + + coords = np.zeros((worldX.size, 3), dtype=np.float32) + coords[:, xax] = worldX.flatten() + coords[:, yax] = worldY.flatten() + + return coords, xres, yres, xNumSamples, yNumSamples + + +def samplePointsToTriangleStrip(coords, + xpixdim, + ypixdim, + xlen, + ylen, + xax, + yax): + """Given a regular 2D grid of points at which an image is to be sampled + (for example, that generated by the :func:`calculateSamplePoints` function + above), converts those points into an OpenGL vertex triangle strip. + + A grid of M*N points is represented by M*2*(N + 1) vertices. For example, + this image represents a 4*3 grid, with periods representing vertex + locations:: + + .___.___.___.___. + | | | | | + | | | | | + .---.---.---.---. + .___.___.__ .___. + | | | | | + | | | | | + .---.---.---.---. + .___.___.___.___. + | | | | | + | | | | | + .___.___.___.___. + + + Vertex locations which are vertically adjacent represent the same point in + space. Such vertex pairs are unable to be combined because, in OpenGL, + they must be represented by distinct vertices (we can't apply multiple + colours/texture coordinates to a single vertex location) So we have to + repeat these vertices in order to achieve accurate colouring of each + voxel. + + We draw each horizontal row of samples one by one, using two triangles to + draw each voxel. In order to eliminate the need to specify six vertices + for every voxel, and hence to reduce the amount of memory used, we are + using a triangle strip to draw each row of voxels. This image depicts a + triangle strip used to draw a row of three samples (periods represent + vertex locations):: + + + 1 3 5 7 + . . . . + |\ |\ |\ | + | \| \| \| + . . . . + 0 2 4 6 + + In order to use a single OpenGL call to draw multiple non-contiguous voxel + rows, between every column we add a couple of 'dummy' vertices, which will + then be interpreted by OpenGL as 'degenerate triangles', and will not be + drawn. So in reality, a 4*3 slice would be drawn as follows (with vertices + labelled from [a-z0-9]: + + v x z 1 33 + |\ |\ |\ |\ | + | \| \| \| \| + uu w y 0 2 + l n p r tt + |\ |\ |\ |\ | + | \| \| \| \| + kk m o q s + b d f h jj + |\ |\ |\ |\ | + | \| \| \| \| + a c e g i + + These repeated/degenerate vertices are dealt with by using a vertex index + array. See these links for good overviews of triangle strips and + degenerate triangles in OpenGL: + + - http://www.learnopengles.com/tag/degenerate-triangles/ + - http://en.wikipedia.org/wiki/Triangle_strip + + A tuple is returned containing: + + - A 2D `numpy.float32` array of shape `(2 * (xlen + 1) * ylen), 3)`, + containing the coordinates of all triangle strip vertices which + represent the entire grid of sample points. + + - A 2D `numpy.float32` array of shape `(2 * (xlen + 1) * ylen), 3)`, + containing the centre of every grid, to be used for texture + coordinates/colour lookup. + + - A 1D `numpy.uint32` array of size `ylen * (2 * (xlen + 1) + 2) - 2` + containing indices into the first array, defining the order in which + the vertices need to be rendered. There are more indices than vertex + coordinates due to the inclusion of repeated/degenerate vertices. + + :arg coords: N*3 array of points, the sampling locations. + + :arg xpixdim: Length of one sample along the horizontal axis. + + :arg ypixdim: Length of one sample along the vertical axis. + + :arg xlen: Number of samples along the horizontal axis. + + :arg ylen: Number of samples along the vertical axis. + + :arg xax: Display coordinate system axis which corresponds to the + horizontal screen axis. + + :arg yax: Display coordinate system axis which corresponds to the + vertical screen axis. + """ + + coords = coords.reshape(ylen, xlen, 3) + + xlen = int(xlen) + ylen = int(ylen) + + # Duplicate every row - each voxel + # is defined by two vertices + coords = coords.repeat(2, 0) + + texCoords = np.array(coords, dtype=np.float32) + worldCoords = np.array(coords, dtype=np.float32) + + # Add an extra column at the end + # of the world coordinates + worldCoords = np.append(worldCoords, worldCoords[:, -1:, :], 1) + worldCoords[:, -1, xax] += xpixdim + + # Add an extra column at the start + # of the texture coordinates + texCoords = np.append(texCoords[:, :1, :], texCoords, 1) + + # Move the x/y world coordinates to the + # sampling point corners (the texture + # coordinates remain in the voxel centres) + worldCoords[ :, :, xax] -= 0.5 * xpixdim + worldCoords[ ::2, :, yax] -= 0.5 * ypixdim + worldCoords[1::2, :, yax] += 0.5 * ypixdim + + vertsPerRow = 2 * (xlen + 1) + dVertsPerRow = 2 * (xlen + 1) + 2 + nindices = ylen * dVertsPerRow - 2 + + indices = np.zeros(nindices, dtype=np.uint32) + + for yi, xi in it.product(range(ylen), range(xlen + 1)): + + ii = yi * dVertsPerRow + 2 * xi + vi = yi * vertsPerRow + xi + + indices[ii] = vi + indices[ii + 1] = vi + xlen + 1 + + # add degenerate vertices at the end + # every row (but not needed for last + # row) + if xi == xlen and yi < ylen - 1: + indices[ii + 2] = vi + xlen + 1 + indices[ii + 3] = (yi + 1) * vertsPerRow + + worldCoords = worldCoords.reshape((xlen + 1) * (2 * ylen), 3) + texCoords = texCoords .reshape((xlen + 1) * (2 * ylen), 3) + + return worldCoords, texCoords, indices + + +def voxelGrid(points, xax, yax, xpixdim, ypixdim): + """Given a ``N*3`` array of ``points`` (assumed to be voxel + coordinates), creates an array of vertices which can be used + to render each point as an unfilled rectangle. + + :arg points: An ``N*3`` array of voxel xyz coordinates + + :arg xax: XYZ axis index that maps to the horizontal scren axis + + :arg yax: XYZ axis index that maps to the vertical scren axis + + :arg xpixdim: Length of a voxel along the x axis. + + :arg ypixdim: Length of a voxel along the y axis. + """ + + if len(points.shape) == 1: + points = points.reshape(1, 3) + + npoints = points.shape[0] + vertices = np.repeat(np.array(points, dtype=np.float32), 4, axis=0) + + xpixdim = xpixdim / 2.0 + ypixdim = ypixdim / 2.0 + + # bottom left corner + vertices[ ::4, xax] -= xpixdim + vertices[ ::4, yax] -= ypixdim + + # bottom right + vertices[1::4, xax] += xpixdim + vertices[1::4, yax] -= ypixdim + + # top left + vertices[2::4, xax] -= xpixdim + vertices[2::4, yax] += ypixdim + + # top right + vertices[3::4, xax] += xpixdim + vertices[3::4, yax] += ypixdim + + # each square is rendered as four lines + indices = np.array([0, 1, 0, 2, 1, 3, 2, 3], dtype=np.uint32) + indices = np.tile(indices, npoints) + + indices = (indices.T + + np.repeat(np.arange(0, npoints * 4, 4, dtype=np.uint32), 8)).T + + return vertices, indices + + +def slice2D(dataShape, + xax, + yax, + zpos, + voxToDisplayMat, + displayToVoxMat, + geometry='triangles'): + """Generates and returns vertices which denote a slice through an + array of the given ``dataShape``, parallel to the plane defined by the + given ``xax`` and ``yax`` and at the given z position, in the space + defined by the given ``voxToDisplayMat``. + + If ``geometry`` is ``triangles`` (the default), six vertices are returned, + arranged as follows: + + 4---5 + 1 \ | + |\ \ | + | \ \| + | \ 3 + 0---2 + + Otherwise, if geometry is ``square``, four vertices are returned, arranged + as follows: + + + 3---2 + | | + | | + | | + 0---1 + + :arg dataShape: Number of elements along each dimension in the + image data. + + :arg xax: Index of display axis which corresponds to the + horizontal screen axis. + + :arg yax: Index of display axis which corresponds to the + vertical screen axis. + + :arg zpos: Position of the slice along the screen z axis. + + :arg voxToDisplayMat: Affine transformation matrix which transforms from + voxel/array indices into the display coordinate + system. + + :arg displayToVoxMat: Inverse of the ``voxToDisplayMat``. + + Returns a tuple containing: + + - A ``N*3`` ``numpy.float32`` array containing the vertex locations + of a slice through the data, where ``N=6`` if ``geometry=triangles``, + or ``N=4`` if ``geometry=square``, + + - A ``N*3`` ``numpy.float32`` array containing the voxel coordinates + that correspond to the vertex locations. + + - A ``N*3`` ``numpy.float32`` array containing the texture coordinates + that correspond to the voxel coordinates. + + """ + + zax = 3 - xax - yax + xmin, xmax = transform.axisBounds(dataShape, voxToDisplayMat, xax) + ymin, ymax = transform.axisBounds(dataShape, voxToDisplayMat, yax) + + if geometry == 'triangles': + + vertices = np.zeros((6, 3), dtype=np.float32) + + vertices[ 0, [xax, yax]] = [xmin, ymin] + vertices[ 1, [xax, yax]] = [xmin, ymax] + vertices[ 2, [xax, yax]] = [xmax, ymin] + vertices[ 3, [xax, yax]] = [xmax, ymin] + vertices[ 4, [xax, yax]] = [xmin, ymax] + vertices[ 5, [xax, yax]] = [xmax, ymax] + + elif geometry == 'square': + vertices = np.zeros((4, 3), dtype=np.float32) + + vertices[ 0, [xax, yax]] = [xmin, ymin] + vertices[ 1, [xax, yax]] = [xmax, ymin] + vertices[ 2, [xax, yax]] = [xmax, ymax] + vertices[ 3, [xax, yax]] = [xmin, ymax] + else: + raise ValueError('Unrecognised geometry type: {}'.format(geometry)) + + vertices[:, zax] = zpos + + voxCoords = transform.transform(vertices, displayToVoxMat) + + # offset by 0.5, because voxel coordinates are by + # default centered at 0 (i.e. the space of a voxel + # lies in the range [-0.5, 0.5]), but we want voxel + # coordinates to map to the effective range [0, 1] + voxCoords = voxCoords + 0.5 + texCoords = voxCoords / dataShape + + return vertices, voxCoords, texCoords + + +def subsample(data, resolution, pixdim=None, volume=None): + """Samples the given 3D data according to the given resolution. + + Returns a tuple containing: + + - A 3D numpy array containing the sub-sampled data. + + - A tuple containing the ``(x, y, z)`` starting indices of the + sampled data. + + - A tuple containing the ``(x, y, z)`` steps of the sampled data. + + :arg data: The data to be sampled. + + :arg resolution: Sampling resolution, proportional to the values in + ``pixdim``. + + :arg pixdim: Length of each dimension in the input data (defaults to + ``(1.0, 1.0, 1.0)``). + + :arg volume: If the image is a 4D volume, the volume index of the 3D + image to be sampled. + """ + + if pixdim is None: + pixdim = (1.0, 1.0, 1.0) + + if volume is None: + volume = slice(None, None, None) + + xstep = np.round(resolution / pixdim[0]) + ystep = np.round(resolution / pixdim[1]) + zstep = np.round(resolution / pixdim[2]) + + if xstep < 1: xstep = 1 + if ystep < 1: ystep = 1 + if zstep < 1: zstep = 1 + + xstart = np.floor(xstep / 2) + ystart = np.floor(ystep / 2) + zstart = np.floor(zstep / 2) + + if len(data.shape) > 3: sample = data[xstart::xstep, + ystart::ystep, + zstart::zstep, + volume] + else: sample = data[xstart::xstep, + ystart::ystep, + zstart::zstep] + + return sample, (xstart, ystart, zstart), (xstep, ystep, zstep) + + +def broadcast(vertices, indices, zposes, xforms, zax): + """Given a set of vertices and indices (assumed to be 2D representations + of some geometry in a 3D space, with the depth axis specified by ``zax``), + replicates them across all of the specified Z positions, applying the + corresponding transformation to each set of vertices. + + :arg vertices: Vertex array (a ``N*3`` numpy array). + + :arg indices: Index array. + + :arg zposes: Positions along the depth axis at which the vertices + are to be replicated. + + :arg xforms: Sequence of transformation matrices, one for each + Z position. + + :arg zax: Index of the 'depth' axis + + Returns three values: + + - A numpy array containing all of the generated vertices + + - A numpy array containing the original vertices for each of the + generated vertices, which may be used as texture coordinates + + - A new numpy array containing all of the generated indices. + """ + + vertices = np.array(vertices) + indices = np.array(indices) + + nverts = vertices.shape[0] + nidxs = indices.shape[ 0] + + allTexCoords = np.zeros((nverts * len(zposes), 3), dtype=np.float32) + allVertCoords = np.zeros((nverts * len(zposes), 3), dtype=np.float32) + allIndices = np.zeros( nidxs * len(zposes), dtype=np.uint32) + + for i, (zpos, xform) in enumerate(zip(zposes, xforms)): + + vertices[:, zax] = zpos + + vStart = i * nverts + vEnd = vStart + nverts + + iStart = i * nidxs + iEnd = iStart + nidxs + + allIndices[ iStart:iEnd] = indices + i * nverts + allTexCoords[ vStart:vEnd, :] = vertices + allVertCoords[vStart:vEnd, :] = transform.transform(vertices, xform) + + return allVertCoords, allTexCoords, allIndices diff --git a/fsl/fslview/gl/shaders.py b/fsl/fslview/gl/shaders.py index 17f8967ca9f55a73147cb55ddac5f2004565073c..548723fdb0bcca086f3a50dd787c46cef21eaf5d 100644 --- a/fsl/fslview/gl/shaders.py +++ b/fsl/fslview/gl/shaders.py @@ -24,12 +24,57 @@ import logging import os.path as op -import fsl.fslview.gl as fslgl +import fsl.fslview.gl as fslgl +import fsl.utils.typedict as td log = logging.getLogger(__name__) +_shaderTypePrefixMap = td.TypeDict({ + + ('GLVolume', 'vert', False) : 'glvolume', + ('GLVolume', 'vert', True) : 'glvolume_sw', + ('GLVolume', 'frag', False) : 'glvolume', + ('GLVolume', 'frag', True) : 'glvolume_sw', + + ('GLRGBVector', 'vert', False) : 'glvolume', + ('GLRGBVector', 'vert', True) : 'glvolume_sw', + + ('GLRGBVector', 'frag', False) : 'glvector', + ('GLRGBVector', 'frag', True) : 'glvector_sw', + + ('GLLineVector', 'vert', False) : 'gllinevector', + ('GLLineVector', 'vert', True) : 'gllinevector_sw', + + ('GLLineVector', 'frag', False) : 'glvector', + ('GLLineVector', 'frag', True) : 'glvector_sw', +}) +"""This dictionary provides a mapping between :class:`.GLObject` types, +and file name prefixes, identifying the shader programs to use. +""" + + +def getShaderPrefix(globj, shaderType, sw): + """Returns the prefix identifying the vertex/fragment shader programs to use + for the given :class:`.GLObject` instance. If ``globj`` is a string, it is + returned unchanged. + """ + + if isinstance(globj, str): + return globj + + return _shaderTypePrefixMap[globj, shaderType, sw] + + +def setShaderPrefix(globj, shaderType, sw, prefix): + """Updates the prefix identifying the vertex/fragment shader programs to use + for the given :class:`.GLObject` type or instance. + """ + + _shaderTypePrefixMap[globj, shaderType, sw] = prefix + + def setVertexProgramVector(index, vector): """Convenience function which sets the vertex program local parameter at the given index to the given 4 component vector. @@ -181,28 +226,29 @@ def compileShaders(vertShaderSrc, fragShaderSrc): return program -def getVertexShader(globj): +def getVertexShader(globj, sw=False): """Returns the vertex shader source for the given GL object.""" - return _getShader(globj, 'vert') + return _getShader(globj, 'vert', sw) -def getFragmentShader(globj): +def getFragmentShader(globj, sw=False): """Returns the fragment shader source for the given GL object.""" - return _getShader(globj, 'frag') + return _getShader(globj, 'frag', sw) -def _getShader(globj, shaderType): +def _getShader(globj, shaderType, sw): """Returns the shader source for the given GL object and the given shader type ('vert' or 'frag'). """ - fname = _getFileName(globj, shaderType) + fname = _getFileName(globj, shaderType, sw) with open(fname, 'rt') as f: src = f.read() return _preprocess(src) -def _getFileName(globj, shaderType): +def _getFileName(globj, shaderType, sw): """Returns the file name of the shader program for the given GL object - and shader type. + and shader type. The ``globj`` parameter may alternately be a string, + in which case it is used as the prefix for the shader program file name. """ if fslgl.GL_VERSION == '2.1': @@ -215,18 +261,7 @@ def _getFileName(globj, shaderType): if shaderType not in ('vert', 'frag'): raise RuntimeError('Invalid shader type: {}'.format(shaderType)) - # callers can request a specific - # shader by passing the name, rather - # than passing a GLObject instance - import fsl.fslview.gl.glvolume as glvolume - import fsl.fslview.gl.glvector as glvector - - if isinstance(globj, str): prefix = globj - elif isinstance(globj, glvolume.GLVolume): prefix = 'glvolume' - elif isinstance(globj, glvector.GLVector): prefix = 'glvector' - else: - raise RuntimeError('Unknown GL object type: ' - '{}'.format(type(globj))) + prefix = getShaderPrefix(globj, shaderType, sw) return op.join(op.dirname(__file__), subdir, '{}_{}.{}'.format( prefix, shaderType, suffix)) diff --git a/fsl/fslview/gl/slicecanvas.py b/fsl/fslview/gl/slicecanvas.py index eb204d46f326a25238d2d395866d294f05205715..f645a4e0d1eb6c3a2ea8d39ddd53583f33c96f96 100644 --- a/fsl/fslview/gl/slicecanvas.py +++ b/fsl/fslview/gl/slicecanvas.py @@ -29,8 +29,10 @@ import OpenGL.GL as gl import props import fsl.data.image as fslimage +import fsl.fslview.gl.routines as glroutines +import fsl.fslview.gl.resources as glresources import fsl.fslview.gl.globject as globject -import fsl.fslview.gl.textures as fsltextures +import fsl.fslview.gl.textures as textures import fsl.fslview.gl.annotations as annotations @@ -39,6 +41,7 @@ class SliceCanvas(props.HasProperties): collection of 3D images (see :class:`fsl.data.image.ImageList`). """ + pos = props.Point(ndims=3) """The currently displayed position. The ``pos.x`` and ``pos.y`` positions denote the position of a 'cursor', which is highlighted with green @@ -50,6 +53,7 @@ class SliceCanvas(props.HasProperties): to 'depth'. """ + zoom = props.Percentage(minval=100.0, maxval=1000.0, default=100.0, @@ -67,15 +71,6 @@ class SliceCanvas(props.HasProperties): """ - twoStageRender = props.Boolean(default=False) - """If ``True``, the scene is rendered off-screen to a fixed-size texture; - this texture is then rendered to the canvas. If ``False``, the scene is - rendered directly to the canvas. Two-stage rendering will probably give - better performance on old graphics cards, and when software-based - rendering is being used. - """ - - showCursor = props.Boolean(default=True) """If ``False``, the green crosshairs which show the current cursor location will not be drawn. @@ -93,27 +88,24 @@ class SliceCanvas(props.HasProperties): invertY = props.Boolean(default=False) """If True, the display is inverted along the Y (vertical screen) axis. - """ + """ - _labels = { - 'zoom' : 'Zoom level', - 'showCursor' : 'Show cursor', - 'zax' : 'Z axis', - 'invertX' : 'Invert X axis', - 'invertY' : 'Invert Y axis'} - """Labels for the properties which are intended to be user editable.""" - + renderMode = props.Choice(('onscreen', 'offscreen', 'prerender')) + """How the GLObjects are rendered to the canvas - onscreen is the + default, but the other options will give better performance on + slower platforms. + """ - _tooltips = { - 'zoom' : 'Zoom level (min: 1, max: 10)', - 'showCursor' : 'Show/hide a green cursor indicating ' - 'the currently displayed location', - 'zax' : 'Image axis which is used as the \'depth\' axis'} - """Property descriptions to be used as help text.""" + + softwareMode = props.Boolean(default=False) + """If ``True``, the :attr:`.Display.softwareMode` property for every + displayed image is set to ``True``. + """ - _propHelp = _tooltips + resolutionLimit = props.Real(default=0, minval=0, maxval=5, clamped=True) + """The minimum resolution at which image types should be drawn.""" def calcPixelDims(self): @@ -280,10 +272,12 @@ class SliceCanvas(props.HasProperties): # stored in this dictionary self._glObjects = {} - # If two stage rendering is enabled, this - # attribute will contain a RenderTexture - # instance for each image in the image list - self._renderTextures = {} + # If render mode is offscren or prerender, these + # dictionaries will contain a RenderTexture or + # RenderTextureStack instance for each image in + # the image list + self._offscreenTextures = {} + self._prerenderTextures = {} # The zax property is the image axis which maps to the # 'depth' axis of this canvas. The _zAxisChanged method @@ -307,13 +301,13 @@ class SliceCanvas(props.HasProperties): self.name, lambda *a: self._updateDisplayBounds()) - # When the two stage rendering option changes, - # make sure that, if it has been enabled, a - # rendering texture exists for every image - # in the list - self.addListener('twoStageRender', + self.addListener('renderMode', self.name, - self._onTwoStageRenderChange) + self._renderModeChange) + + self.addListener('resolutionLimit', + self.name, + self._resolutionLimitChange) # When the image list changes, refresh the # display, and update the display bounds @@ -329,56 +323,141 @@ class SliceCanvas(props.HasProperties): def _initGL(self): - # Call the _imageListChanged method - it will generate - # any necessary GL data for each of the images + """Call the _imageListChanged method - it will generate + any necessary GL data for each of the images + """ self._imageListChanged() + + def _updateRenderTextures(self): + """Called when the :attr:`renderMode` changes, when the image + list changes, or when the GLObject representation of an image + changes. - def _onTwoStageRenderChange( - self, - value=None, - valid=None, - ctx=None, - name=None, - recreate=False): - """Called when the :attr:`twoStageRender` property changes. - """ - - needRefresh = False - if recreate or not self.twoStageRender: + If the :attr:`renderMode` property is ``onscreen``, this method does + nothing. - for image, texture in self._renderTextures.items(): - self._renderTextures.pop(image) - texture.destroy() - needRefresh = True + Otherwise, creates/destroys :class:`.RenderTexture` or + :class:`.RenderTextureStack` instances for newly added/removed images. + """ - if self.twoStageRender: + if self.renderMode == 'onscreen': + return - # If any images have been removed from the image - # list, destroy the associated render texture - for image, texture in self._renderTextures.items(): + # If any images have been removed from the image + # list, destroy the associated render texture stack + if self.renderMode == 'offscreen': + for image, texture in self._offscreenTextures.items(): if image not in self.imageList: - self._renderTextures.pop(image) + self._offscreenTextures.pop(image) texture.destroy() - needRefresh = True + + elif self.renderMode == 'prerender': + for image, (texture, name) in self._prerenderTextures.items(): + if image not in self.imageList: + self._prerenderTextures.pop(image) + glresources.delete(name) - # If any images have been added to the list, - # create a new render textures for them - for image in self.imageList: + # If any images have been added to the list, + # create a new render textures for them + for image in self.imageList: - if image in self._renderTextures: + if self.renderMode == 'offscreen': + if image in self._offscreenTextures: continue + + elif self.renderMode == 'prerender': + if image in self._prerenderTextures: + continue + + globj = self._glObjects.get(image, None) + + if globj is None: + continue - display = self.displayCtx.getDisplayProperties(image) - rt = fsltextures.ImageRenderTexture( - image, display, self.xax, self.yax) + # For offscreen render mode, GLObjects are + # first rendered to an offscreen texture, + # and then that texture is rendered to the + # screen. The off-screen texture is managed + # by a RenderTexture object. + if self.renderMode == 'offscreen': + + name = '{}_{}_{}'.format(image.name, self.xax, self.yax) + rt = textures.GLObjectRenderTexture( + name, + globj, + self.xax, + self.yax) + + self._offscreenTextures[image] = rt - self._renderTextures[image] = rt + # For prerender mode, slices of the + # GLObjects are pre-rendered on a + # stack of off-screen textures, which + # is managed by a RenderTextureStack + # object. + elif self.renderMode == 'prerender': + name = '{}_{}_zax{}'.format( + id(image), + textures.RenderTextureStack.__name__, + self.zax) + + if glresources.exists(name): + rt = glresources.get(name) + + else: + rt = textures.RenderTextureStack(globj) + rt.setAxes(self.xax, self.yax) + glresources.set(name, rt) + + self._prerenderTextures[image] = rt, name + + self._refresh() + - needRefresh = True + def _renderModeChange(self, *a): + """Called when the :attr:`renderMode` property changes.""" - if needRefresh: + log.debug('Render mode changed: {}'.format(self.renderMode)) + + # destroy any existing render textures + for image, texture in self._offscreenTextures.items(): + self._offscreenTextures.pop(image) + texture.destroy() + + for image, (texture, name) in self._prerenderTextures.items(): + self._prerenderTextures.pop(image) + glresources.delete(name) + + # Onscreen rendering - each GLObject + # is rendered directly to the canvas + # displayed on the screen, so render + # textures are not needed. + if self.renderMode == 'onscreen': self._refresh() + return + + # Off-screen or prerender rendering - update + # the render textures for every GLObject + self._updateRenderTextures() + + + def _resolutionLimitChange(self, *a): + """Called when the :attr:`resolutionLimit` property changes. + + Updates the minimum resolution of all images in the image list. + """ + + for image in self.imageList: + + display = self.displayCtx.getDisplayProperties(image) + minres = min(image.pixdim[:3]) + + if self.resolutionLimit > minres: + minres = self.resolutionLimit + + if display.resolution < minres: + display.resolution = minres def _zAxisChanged(self, *a): @@ -421,11 +500,12 @@ class SliceCanvas(props.HasProperties): pos[self.yax], pos[self.zax]] - # If two stage rendering is enabled, the + # If pre-rendering is enabled, the # render textures need to be updated, as # they are configured in terms of the - # display axes - self._onTwoStageRenderChange(recreate=True) + # display axes. Easiest way to do this + # is to destroy and re-create them + self._renderModeChange() def _imageListChanged(self, *a): @@ -463,7 +543,8 @@ class SliceCanvas(props.HasProperties): ctx=None, name=None, disp=display, - img=image): + img=image, + updateRenderTextures=True): log.debug('GLObject representation for {} ' 'changed to {}'.format(disp.name, @@ -477,24 +558,43 @@ class SliceCanvas(props.HasProperties): globj = self._glObjects.get(img, None) if globj is not None: globj.destroy() - + + if updateRenderTextures: + if self.renderMode == 'offscreen': + tex = self._offscreenTextures.pop(img, None) + if tex is not None: + tex.destroy() + + elif self.renderMode == 'prerender': + tex, name = self._prerenderTextures.pop( + img, (None, None)) + if tex is not None: + glresources.delete(name) + globj = globject.createGLObject(img, disp) - self._glObjects[img] = globj if globj is not None: - globj.init() globj.setAxes(self.xax, self.yax) + self._glObjects[img] = globj + + if updateRenderTextures: + self._updateRenderTextures() + opts = disp.getDisplayOpts() opts.addGlobalListener(self.name, self._refresh) self._refresh() - genGLObject() + genGLObject(updateRenderTextures=False) + + # Bind Display.softwareMode to SliceCanvas.softwareMode + display.bindProps('softwareMode', self) image .addListener('data', self.name, self._refresh) display.addListener('imageType', self.name, genGLObject) display.addListener('enabled', self.name, self._refresh) display.addListener('transform', self.name, self._refresh) + display.addListener('softwareMode', self.name, self._refresh) display.addListener('interpolation', self.name, self._refresh) display.addListener('alpha', self.name, self._refresh) display.addListener('brightness', self.name, self._refresh) @@ -502,7 +602,8 @@ class SliceCanvas(props.HasProperties): display.addListener('resolution', self.name, self._refresh) display.addListener('volume', self.name, self._refresh) - self._onTwoStageRenderChange() + self._updateRenderTextures() + self._resolutionLimitChange() self._refresh() @@ -667,13 +768,17 @@ class SliceCanvas(props.HasProperties): :arg zmin: Minimum z (depth) location :arg zmax: Maximum z location """ + + xax = self.xax + yax = self.yax + zax = self.zax if xmin is None: xmin = self.displayBounds.xlo if xmax is None: xmax = self.displayBounds.xhi if ymin is None: ymin = self.displayBounds.ylo if ymax is None: ymax = self.displayBounds.yhi - if zmin is None: zmin = self.displayCtx.bounds.getLo(self.zax) - if zmax is None: zmax = self.displayCtx.bounds.getHi(self.zax) + if zmin is None: zmin = self.displayCtx.bounds.getLo(zax) + if zmax is None: zmax = self.displayCtx.bounds.getHi(zax) # If there are no images to be displayed, # or no space to draw, do nothing @@ -698,49 +803,23 @@ class SliceCanvas(props.HasProperties): log.debug('Setting canvas bounds (size {}, {}): ' 'X {: 5.1f} - {: 5.1f},' - 'Y {: 5.1f} - {: 5.1f}'.format( - width, height, xmin, xmax, ymin, ymax)) - - # set up 2D orthographic drawing - gl.glViewport(0, 0, width, height) - gl.glMatrixMode(gl.GL_PROJECTION) - gl.glLoadIdentity() + 'Y {: 5.1f} - {: 5.1f},' + 'Z {: 5.1f} - {: 5.1f}'.format( + width, height, xmin, xmax, ymin, ymax, zmin, zmax)) # Flip the viewport if necessary if self.invertX: xmin, xmax = xmax, xmin if self.invertY: ymin, ymax = ymax, ymin - - gl.glOrtho(xmin, xmax, - ymin, ymax, - zmin - 1000, zmax + 1000) - # I don't know why the above +/-1000 is necessary :( - # The '1000' is empirically arbitrary, but it seems - # that I need to extend the depth clipping range - # beyond the range of the data. This is despite the - # fact that below, I'm actually translating the - # displayed slice to Z=0! I don't understand OpenGL - # sometimes. Most of the time. - - gl.glMatrixMode(gl.GL_MODELVIEW) - gl.glLoadIdentity() - - # Rotate world space so the displayed slice - # is visible and correctly oriented - # TODO There's got to be a more generic way - # to perform this rotation. This will break - # if I add functionality allowing the user - # to specifty the x/y axes on initialisation. - if self.zax == 0: - gl.glRotatef(-90, 1, 0, 0) - gl.glRotatef(-90, 0, 0, 1) - - elif self.zax == 1: - gl.glRotatef(270, 1, 0, 0) - # move the currently displayed slice to screen Z coord 0 - trans = [0, 0, 0] - trans[self.zax] = -self.pos.z - gl.glTranslatef(*trans) + lo = [None] * 3 + hi = [None] * 3 + + lo[xax], hi[xax] = xmin, xmax + lo[yax], hi[yax] = ymin, ymax + lo[zax], hi[zax] = zmin, zmax + + # set up 2D orthographic drawing + glroutines.show2D(xax, yax, width, height, lo, hi) def _drawCursor(self): @@ -771,24 +850,26 @@ class SliceCanvas(props.HasProperties): yverts[:, 0] = [xmin, xmax] yverts[:, 1] = y - self._annotations.line(xverts[0], xverts[1], colour=(0, 1, 0)) - self._annotations.line(yverts[0], yverts[1], colour=(0, 1, 0)) + self._annotations.line(xverts[0], xverts[1], colour=(0, 1, 0), width=1) + self._annotations.line(yverts[0], yverts[1], colour=(0, 1, 0), width=1) - def _drawRenderTextures(self): - """ + def _drawOffscreenTextures(self): + """Draws all of the off-screen :class:`.ImageRenderTexture` instances to the + canvas. + + This method is called by :meth:`_draw` if :attr:`twoStageRender` is + enabled. """ log.debug('Combining off-screen image textures, and rendering ' 'to canvas (size {})'.format(self._getSize())) - self._setViewport() - for image in self.displayCtx.getOrderedImages(): - renderTexture = self._renderTextures.get(image, None) - display = self.displayCtx.getDisplayProperties(image) - lo, hi = display.getDisplayBounds() + rt = self._offscreenTextures.get(image, None) + display = self.displayCtx.getDisplayProperties(image) + lo, hi = display.getDisplayBounds() - if renderTexture is None or not display.enabled: + if rt is None or not display.enabled: continue xmin, xmax = lo[self.xax], hi[self.xax] @@ -798,10 +879,10 @@ class SliceCanvas(props.HasProperties): '{:0.3f}-{:0.3f}'.format( image, xmin, xmax, ymin, ymax)) - renderTexture.drawRender( - xmin, xmax, ymin, ymax, self.xax, self.yax) - + rt.drawOnBounds( + self.pos.z, xmin, xmax, ymin, ymax, self.xax, self.yax) + def _draw(self, *a): """Draws the current scene to the canvas. """ @@ -812,9 +893,9 @@ class SliceCanvas(props.HasProperties): if not self._setGLContext(): return - # If normal rendering, set the viewport to match - # the current display bounds and canvas size - if not self.twoStageRender: + # Set the viewport to match the current + # display bounds and canvas size + if self.renderMode is not 'offscreen': self._setViewport() for image in self.displayCtx.getOrderedImages(): @@ -822,50 +903,72 @@ class SliceCanvas(props.HasProperties): display = self.displayCtx.getDisplayProperties(image) globj = self._glObjects.get(image, None) - if (globj is None) or (not globj.ready()) or not display.enabled: + if (globj is None) or (not display.enabled): continue - # Two-stage rendering - each image is + # On-screen rendering - the globject is + # rendered directly to the screen canvas + if self.renderMode == 'onscreen': + log.debug('Drawing {} slice for image {} ' + 'directly to canvas'.format( + self.zax, image.name)) + + globj.preDraw() + globj.draw(self.pos.z) + globj.postDraw() + + # Off-screen rendering - each image is # rendered to an off-screen texture - # these textures are combined below. - # Set up this texture as the rendering - # target - if self.twoStageRender: - renderTexture = self._renderTextures.get(image, None) - lo, hi = display.getDisplayBounds() + # Set up the texture as the rendering + # target, and draw to it + elif self.renderMode == 'offscreen': + + rt = self._offscreenTextures.get(image, None) + lo, hi = display.getDisplayBounds() # Assume that all is well - the texture # just has not yet been created - if renderTexture is None: - return + if rt is None: + continue + + log.debug('Drawing {} slice for image {} ' + 'to off-screen texture'.format( + self.zax, image.name)) - renderTexture.bindAsRenderTarget() + rt.bindAsRenderTarget() + rt.setRenderViewport(self.xax, self.yax, lo, hi) + + gl.glClear(gl.GL_COLOR_BUFFER_BIT) - self._setViewport( - xmin=lo[self.xax], - xmax=hi[self.xax], - ymin=lo[self.yax], - ymax=hi[self.yax], - size=renderTexture.getSize()) + globj.preDraw() + globj.draw(self.pos.z) + globj.postDraw() - log.debug('Rendering image {} to offscreen ' - 'texture {} (size {})'.format( - image, - renderTexture.texture, - renderTexture.getSize())) - - log.debug('Drawing {} slice for image {}'.format( - self.zax, image.name)) + # Pre-rendering - a pre-generated 2D + # texture of the current z position + # is rendered to the screen canvas + elif self.renderMode == 'prerender': - globj.preDraw() - globj.draw(self.pos.z) - globj.postDraw() + rt, name = self._prerenderTextures.get(image, (None, None)) - if self.twoStageRender: - fsltextures.ImageRenderTexture.unbind() + if rt is None: + continue + + log.debug('Drawing {} slice for image {} ' + 'from pre-rendered texture'.format( + self.zax, image.name)) - if self.twoStageRender: - self._drawRenderTextures() + rt.draw(self.pos.z) + + # For off-screen rendering, all of the globjects + # were rendered to off-screen textures - here, + # those off-screen textures are all rendered on + # to the screen canvas. + if self.renderMode == 'offscreen': + textures.GLObjectRenderTexture.unbindAsRenderTarget() + self._setViewport() + self._drawOffscreenTextures() if self.showCursor: self._drawCursor() diff --git a/fsl/fslview/gl/textures/__init__.py b/fsl/fslview/gl/textures/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a10bec9ac935815a9a1cb983b6339d76dcaebcd5 --- /dev/null +++ b/fsl/fslview/gl/textures/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# +# textures.py - Management of OpenGL image textures. +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# +"""This package is a container for a collection of classes which use OpenGL +textures for various purposes. + + +The :mod:`.texture` sub-module contains the definition of the :class:`Texture` +class, the base class for all texture types. +""" + + +# All *Texture classes are made available at the +# textures package level due to these imports +from texture import Texture +from texture import Texture2D +from imagetexture import ImageTexture +from colourmaptexture import ColourMapTexture +from selectiontexture import SelectionTexture +from rendertexture import RenderTexture +from rendertexture import GLObjectRenderTexture +from rendertexturestack import RenderTextureStack diff --git a/fsl/fslview/gl/textures/colourmaptexture.py b/fsl/fslview/gl/textures/colourmaptexture.py new file mode 100644 index 0000000000000000000000000000000000000000..ef150ab3499212f7023d613d8ae4e87d0bf66730 --- /dev/null +++ b/fsl/fslview/gl/textures/colourmaptexture.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python +# +# colourmaptexture.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import logging + + +import numpy as np +import OpenGL.GL as gl + +import texture + + +log = logging.getLogger(__name__) + + +class ColourMapTexture(texture.Texture): + + + def __init__(self, name, resolution=256): + + texture.Texture.__init__(self, name, 1) + + self.__resolution = resolution + self.__cmap = None + self.__invert = False + self.__alpha = None + self.__displayRange = None + self.__border = None + self.__coordXform = None + + + # CMAP can be either a function which transforms + # values to RGBA, or a N*4 numpy array containing + # RGBA values + def setColourMap( self, cmap): self.set(cmap=cmap) + def setAlpha( self, alpha): self.set(alpha=alpha) + def setInvert( self, invert): self.set(invert=invert) + def setDisplayRange(self, drange): self.set(displayRange=drange) + def setBorder( self, border): self.set(border=border) + + + def getCoordinateTransform(self): + return self.__coordXform + + + def set(self, **kwargs): + + + # None is a valid value for any attributes, + # so we are using 'self' to test whether + # or not an attribute value was passed in + cmap = kwargs.get('cmap', self) + invert = kwargs.get('invert', self) + alpha = kwargs.get('alpha', self) + displayRange = kwargs.get('displayRange', self) + border = kwargs.get('border', self) + + if cmap is not self: self.__cmap = cmap + if invert is not self: self.__invert = invert + if alpha is not self: self.__alpha = alpha + if displayRange is not self: self.__displayRange = displayRange + if border is not self: self.__border = border + + self.__refresh() + + + def __refresh(self): + + if self.__displayRange is None: + imin = 0.0 + imax = 1.0 + else: + imin = self.__displayRange[0] + imax = self.__displayRange[1] + + if self.__cmap is None: cmap = np.zeros((4, 4), dtype=np.float32) + else: cmap = self.__cmap + + invert = self.__invert + resolution = self.__resolution + alpha = self.__alpha + border = self.__border + + # This transformation is used to transform input values + # from their native range to the range [0.0, 1.0], which + # is required for texture colour lookup. Values below + # or above the current display range will be mapped + # to texture coordinate values less than 0.0 or greater + # than 1.0 respectively. + if imax == imin: scale = 0.000000000001 + else: scale = imax - imin + + coordXform = np.identity(4, dtype=np.float32) + coordXform[0, 0] = 1.0 / scale + coordXform[3, 0] = -imin * coordXform[0, 0] + + self.__coordXform = coordXform + + # Assume that the provided cmap + # value contains rgb values + if isinstance(cmap, np.ndarray): + resolution = cmap.shape[0] + colourmap = cmap + + # Create [self.colourResolution] + # rgb values, spanning the entire + # range of the image colour map + else: + colourmap = cmap(np.linspace(0.0, 1.0, resolution)) + + # Apply global transparency + if alpha is not None: + colourmap[:, 3] = alpha + + # invert if needed + if invert: + colourmap = colourmap[::-1, :] + + # The colour data is stored on + # the GPU as 8 bit rgba tuples + colourmap = np.floor(colourmap * 255) + colourmap = np.array(colourmap, dtype=np.uint8) + colourmap = colourmap.ravel(order='C') + + # GL texture creation stuff + self.bindTexture() + + if border is not None: + if alpha is not None: + border[3] = alpha + + gl.glTexParameterfv(gl.GL_TEXTURE_1D, + gl.GL_TEXTURE_BORDER_COLOR, + border) + gl.glTexParameteri( gl.GL_TEXTURE_1D, + gl.GL_TEXTURE_WRAP_S, + gl.GL_CLAMP_TO_BORDER) + else: + gl.glTexParameteri(gl.GL_TEXTURE_1D, + gl.GL_TEXTURE_WRAP_S, + gl.GL_CLAMP_TO_EDGE) + + gl.glTexParameteri(gl.GL_TEXTURE_1D, + gl.GL_TEXTURE_MAG_FILTER, + gl.GL_NEAREST) + gl.glTexParameteri(gl.GL_TEXTURE_1D, + gl.GL_TEXTURE_MIN_FILTER, + gl.GL_NEAREST) + + gl.glTexImage1D(gl.GL_TEXTURE_1D, + 0, + gl.GL_RGBA8, + resolution, + 0, + gl.GL_RGBA, + gl.GL_UNSIGNED_BYTE, + colourmap) + self.unbindTexture() diff --git a/fsl/fslview/gl/textures.py b/fsl/fslview/gl/textures/imagetexture.py similarity index 51% rename from fsl/fslview/gl/textures.py rename to fsl/fslview/gl/textures/imagetexture.py index 17e0be28fb8660d44aff8e40920e455adbb42968..78d275bc8d6cb13a2071b377f8e6f52cd3063fc6 100644 --- a/fsl/fslview/gl/textures.py +++ b/fsl/fslview/gl/textures/imagetexture.py @@ -1,84 +1,26 @@ #!/usr/bin/env python # -# textures.py - Management of OpenGL image textures. +# imagetexture.py - # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # -"""This module contains logic for creating OpenGL 3D textures, which contain -3D image data, and which will potentially be shared between multiple GL -canvases. - -The main interface to this module comprises two functions: - - - :func:`getTexture`: Return an :class:`ImageTexture` instance for a - particular :class:`~fsl.data.image.Image` instance, - creating one if it does not already exist. - - - :func:`deleteTexture`: Cleans up the resources used by an - :class:`ImageTexture` instance when it is no longer - needed. -""" import logging -import OpenGL.GL as gl -import OpenGL.raw.GL._types as gltypes -import OpenGL.GL.EXT.framebuffer_object as glfbo -import numpy as np +import numpy as np +import OpenGL.GL as gl import fsl.utils.transform as transform -import fsl.utils.typedict as typedict -import fsl.fslview.gl.globject as globject +import fsl.fslview.gl.routines as glroutines +import texture log = logging.getLogger(__name__) -_allTextures = {} -"""This dictionary contains all of the textures which currently exist. The -key is the texture tag (see :func:`getTexture`), and the value is the -corresponding :class:`ImageTexture` object. -""" - - -def getTexture(target, tag, *args, **kwargs): - """Retrieve a texture object for the given target object (with - the given tag), creating one if it does not exist. - - :arg target: - :arg tag: An application-unique string associated with the given image. - Future requests for a texture with the same image and tag - will return the same :class:`ImageTexture` instance. - """ - - textureMap = typedict.TypeDict({ - 'Image' : ImageTexture, - 'Selection' : SelectionTexture - }) - - tag = '{}_{}'.format(id(target), tag) - textureObj = _allTextures.get(tag, None) - - if textureObj is None: - textureObj = textureMap[type(target)](target, tag, *args, **kwargs) - _allTextures[tag] = textureObj - - return textureObj - - -def deleteTexture(texture): - """Releases the OpenGL memory associated with the given - :class:`ImageTexture` instance, and removes it from the - :attr:`_allTextures` dictionary. - """ - - texture.destroy() - _allTextures.pop(texture.tag, None) - - -class ImageTexture(object): +class ImageTexture(texture.Texture): """This class contains the logic required to create and manage a 3D texture which represents a :class:`~fsl.data.image.Image` instance. @@ -93,7 +35,7 @@ class ImageTexture(object): Once created, the following attributes are available on an :class:`ImageTexture` object: - - ``texture``: The OpenGL texture identifier. + - todo - ``voxValXform``: An affine transformation matrix which encodes an offset and scale, for transforming from the @@ -102,17 +44,17 @@ class ImageTexture(object): """ def __init__(self, + name, image, - tag, display=None, nvals=1, normalise=False, prefilter=None): """Create an :class:`ImageTexture`. - - :arg image: The :class:`~fsl.data.image.Image` instance. - :arg tag: The texture tag (see the :func:`getTexture` function). + :arg name: A name for the texture. + + :arg image: The :class:`~fsl.data.image.Image` instance. :arg display: A :class:`~fsl.fslview.displaycontext.Display` instance which defines how the image is to be @@ -130,6 +72,8 @@ class ImageTexture(object): GPU - see the :meth:`_prepareTextureData` method. """ + texture.Texture.__init__(self, name, 3) + try: if nvals > 1 and image.shape[3] != nvals: raise RuntimeError() @@ -140,7 +84,6 @@ class ImageTexture(object): self.image = image self.display = display - self.tag = tag self.nvals = nvals self.prefilter = prefilter @@ -163,10 +106,6 @@ class ImageTexture(object): self.texDtype = texDtype self.voxValXform = voxValXform self.invVoxValXform = transform.invert(voxValXform) - self.texture = gl.glGenTextures(1) - - log.debug('Created GL texture for {}: {}'.format(self.tag, - self.texture)) self._addListeners() self.refresh() @@ -174,20 +113,13 @@ class ImageTexture(object): def destroy(self): """Deletes the texture identifier, and removes any property - listeners which were registered on the ``Image`` and ``Display`` + listeners which were registered on the ``.Image`` and ``.Display`` instances. """ - if self.texture is None: - return - + texture.Texture.destroy(self) self._removeListeners() - log.debug('Deleting GL texture for {}: {}'.format(self.tag, - self.texture)) - gl.glDeleteTextures(self.texture) - self.texture = None - def setPrefilter(self, prefilter): """Updates the method used to pre-filter the data, and refreshes the @@ -215,7 +147,6 @@ class ImageTexture(object): def refreshInterp(*a): self._updateInterpolationMethod() - name = '{}_{}'.format(type(self).__name__, id(self)) image.addListener('data', name, self.refresh) @@ -286,6 +217,10 @@ class ImageTexture(object): """ data = self.image.data + + if self.prefilter is not None: + data = self.prefilter(data) + dtype = data.dtype dmin = float(data.min()) dmax = float(data.max()) @@ -413,7 +348,7 @@ class ImageTexture(object): This process potentially involves: - Resampling to a different resolution (see the - :mod:`~fsl.fslview.gl.globject.subsample` function). + :mod:`~fsl.fslview.gl.routines.subsample` function). - Pre-filtering (see the ``prefilter`` parameter to :meth:`__init__`). @@ -432,14 +367,14 @@ class ImageTexture(object): else: if self.nvals == 1 and self.image.is4DImage(): - data = globject.subsample(self.image.data, - self.display.resolution, - self.image.pixdim, - self.display.volume) + data = glroutines.subsample(self.image.data, + self.display.resolution, + self.image.pixdim, + self.display.volume)[0] else: - data = globject.subsample(self.image.data, - self.display.resolution, - self.image.pixdim) + data = glroutines.subsample(self.image.data, + self.display.resolution, + self.image.pixdim)[0] if self.prefilter is not None: data = self.prefilter(data) @@ -475,16 +410,12 @@ class ImageTexture(object): else: interp = gl.GL_LINEAR - gl.glBindTexture(gl.GL_TEXTURE_3D, self.texture) + self.bindTexture() - gl.glTexParameteri(gl.GL_TEXTURE_3D, - gl.GL_TEXTURE_MAG_FILTER, - interp) - gl.glTexParameteri(gl.GL_TEXTURE_3D, - gl.GL_TEXTURE_MIN_FILTER, - interp) + gl.glTexParameteri(gl.GL_TEXTURE_3D, gl.GL_TEXTURE_MAG_FILTER, interp) + gl.glTexParameteri(gl.GL_TEXTURE_3D, gl.GL_TEXTURE_MIN_FILTER, interp) - gl.glBindTexture(gl.GL_TEXTURE_3D, 0) + self.unbindTexture() def refresh(self, *a): @@ -503,8 +434,8 @@ class ImageTexture(object): log.debug('Refreshing 3D texture (id {}) for ' '{} (data shape: {})'.format( - self.texture, - self.tag, + self.getTextureHandle(), + self.getTextureName(), self.textureShape)) # The image data is flattened, with fortran dimension @@ -521,7 +452,7 @@ class ImageTexture(object): # (interpolation) method self._updateInterpolationMethod() - gl.glBindTexture(gl.GL_TEXTURE_3D, self.texture) + self.bindTexture() # Clamp texture borders to the edge # values - it is the responsibility @@ -551,366 +482,4 @@ class ImageTexture(object): self.texDtype, data) - gl.glBindTexture(gl.GL_TEXTURE_3D, 0) - - -class SelectionTexture(object): - - def __init__(self, selection, tag): - - self.tag = tag - self.selection = selection - self.texture = gl.glGenTextures(1) - - log.debug('Created GL texture for {}: {}'.format(self.tag, - self.texture)) - - selection.addListener('selection', tag, self._selectionChanged) - - self._init() - self.refresh() - - - def _init(self): - gl.glBindTexture(gl.GL_TEXTURE_3D, self.texture) - - gl.glTexParameteri(gl.GL_TEXTURE_3D, - gl.GL_TEXTURE_MAG_FILTER, - gl.GL_NEAREST) - gl.glTexParameteri(gl.GL_TEXTURE_3D, - gl.GL_TEXTURE_MIN_FILTER, - gl.GL_NEAREST) - - gl.glTexParameterfv(gl.GL_TEXTURE_3D, - gl.GL_TEXTURE_BORDER_COLOR, - np.array([0, 0, 0, 0], dtype=np.float32)) - - gl.glTexParameteri(gl.GL_TEXTURE_3D, - gl.GL_TEXTURE_WRAP_S, - gl.GL_CLAMP_TO_BORDER) - gl.glTexParameteri(gl.GL_TEXTURE_3D, - gl.GL_TEXTURE_WRAP_T, - gl.GL_CLAMP_TO_BORDER) - gl.glTexParameteri(gl.GL_TEXTURE_3D, - gl.GL_TEXTURE_WRAP_R, - gl.GL_CLAMP_TO_BORDER) - - shape = self.selection.selection.shape - gl.glTexImage3D(gl.GL_TEXTURE_3D, - 0, - gl.GL_ALPHA8, - shape[0], - shape[1], - shape[2], - 0, - gl.GL_ALPHA, - gl.GL_UNSIGNED_BYTE, - None) - - gl.glBindTexture(gl.GL_TEXTURE_3D, 0) - - - def refresh(self, block=None, offset=None): - - if block is None or offset is None: - data = self.selection.selection - offset = [0, 0, 0] - else: - data = block - - data = data * 255 - - log.debug('Updating selection texture (offset {}, size {})'.format( - offset, data.shape)) - - gl.glBindTexture(gl.GL_TEXTURE_3D, self.texture) - gl.glTexSubImage3D(gl.GL_TEXTURE_3D, - 0, - offset[0], - offset[1], - offset[2], - data.shape[0], - data.shape[1], - data.shape[2], - gl.GL_ALPHA, - gl.GL_UNSIGNED_BYTE, - data.ravel('F')) - gl.glBindTexture(gl.GL_TEXTURE_3D, 0) - - - def _selectionChanged(self, *a): - - old, new, offset = self.selection.getLastChange() - - if old is None or new is None: - data = self.selection.selection - offset = [0, 0, 0] - else: - data = new - - self.refresh(data, offset) - - -class RenderTexture(object): - """A 2D texture and frame buffer, intended to be used as a target for - off-screen rendering of a scene. - """ - - def __init__(self, width, height, defaultInterp=gl.GL_NEAREST): - """ - - Note that a current target must have been set for the GL context - before a frameBuffer can be created ... in other words, call - ``context.SetCurrent`` before creating a ``RenderTexture``). - """ - - - self.texture = gl .glGenTextures(1) - self.frameBuffer = glfbo.glGenFramebuffersEXT(1) - - log.debug('Created GL texture {} and fbo: {}'.format( - self.texture, self.frameBuffer)) - - self.defaultInterp = defaultInterp - self.width = width - self.height = height - self.refresh() - - - def destroy(self): - - log.debug('Deleting GL texture {} and fbo {}'.format( - self.texture, self.frameBuffer)) - gl .glDeleteTextures( self.texture) - glfbo.glDeleteFramebuffersEXT(gltypes.GLuint(self.frameBuffer)) - - - def setSize(self, width, height): - self.width = width - self.height = height - self.refresh() - - - def getSize(self): - return self.width, self.height - - - def bindAsRenderTarget(self): - glfbo.glBindFramebufferEXT(glfbo.GL_FRAMEBUFFER_EXT, self.frameBuffer) - - - @classmethod - def unbind(cls): - glfbo.glBindFramebufferEXT(glfbo.GL_FRAMEBUFFER_EXT, 0) - - - def refresh(self, interp=None): - if interp is None: - interp = self.defaultInterp - - log.debug('Configuring texture {}, fbo {}, size {}'.format( - self.texture, self.frameBuffer, (self.width, self.height))) - - # Configure the texture - gl.glBindTexture(gl.GL_TEXTURE_2D, self.texture) - - gl.glTexImage2D(gl.GL_TEXTURE_2D, - 0, - gl.GL_RGBA8, - self.width, - self.height, - 0, - gl.GL_RGBA, - gl.GL_UNSIGNED_BYTE, - None) - - gl.glTexParameteri(gl.GL_TEXTURE_2D, - gl.GL_TEXTURE_MIN_FILTER, - interp) - gl.glTexParameteri(gl.GL_TEXTURE_2D, - gl.GL_TEXTURE_MAG_FILTER, - interp) - - # And configure the frame buffer - glfbo.glBindFramebufferEXT( glfbo.GL_FRAMEBUFFER_EXT, - self.frameBuffer) - glfbo.glFramebufferTexture2DEXT(glfbo.GL_FRAMEBUFFER_EXT, - glfbo.GL_COLOR_ATTACHMENT0_EXT, - gl .GL_TEXTURE_2D, - self.texture, - 0) - - if glfbo.glCheckFramebufferStatusEXT(glfbo.GL_FRAMEBUFFER_EXT) != \ - glfbo.GL_FRAMEBUFFER_COMPLETE_EXT: - raise RuntimeError('An error has occurred while ' - 'configuring the frame buffer') - - glfbo.glBindFramebufferEXT(glfbo.GL_FRAMEBUFFER_EXT, 0) - gl.glBindTexture(gl.GL_TEXTURE_2D, 0) - - - def drawRender(self, xmin, xmax, ymin, ymax, xax, yax): - - # gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) - # gl.glEnable(gl.GL_BLEND) - # gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) - - indices = np.arange(6, dtype=np.uint32) - vertices = np.zeros((6, 3), dtype=np.float32) - texCoords = np.zeros((6, 2), dtype=np.float32) - - vertices[ 0, [xax, yax]] = [xmin, ymin] - texCoords[0, :] = [0, 0] - vertices[ 1, [xax, yax]] = [xmin, ymax] - texCoords[1, :] = [0, 1] - vertices[ 2, [xax, yax]] = [xmax, ymin] - texCoords[2, :] = [1, 0] - vertices[ 3, [xax, yax]] = [xmax, ymin] - texCoords[3, :] = [1, 0] - vertices[ 4, [xax, yax]] = [xmin, ymax] - texCoords[4, :] = [0, 1] - vertices[ 5, [xax, yax]] = [xmax, ymax] - texCoords[5, :] = [1, 1] - - texCoords = texCoords.ravel('C') - vertices = vertices .ravel('C') - - gl.glEnableClientState(gl.GL_VERTEX_ARRAY) - gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY) - - gl.glActiveTexture(gl.GL_TEXTURE0) - gl.glEnable(gl.GL_TEXTURE_2D) - - gl.glBindTexture(gl.GL_TEXTURE_2D, self.texture) - - gl.glTexEnvf(gl.GL_TEXTURE_ENV, - gl.GL_TEXTURE_ENV_MODE, - gl.GL_REPLACE) - - gl.glVertexPointer( 3, gl.GL_FLOAT, 0, vertices) - gl.glTexCoordPointer(2, gl.GL_FLOAT, 0, texCoords) - - gl.glDrawElements(gl.GL_TRIANGLES, 6, gl.GL_UNSIGNED_INT, indices) - - gl.glBindTexture(gl.GL_TEXTURE_2D, 0) - gl.glDisable(gl.GL_TEXTURE_2D) - - gl.glDisableClientState(gl.GL_VERTEX_ARRAY) - gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY) - - -class ImageRenderTexture(RenderTexture): - """A :class:`RenderTexture` for off-screen volumetric rendering of a 3D - image. - """ - - def __init__(self, image, display, xax, yax): - """ - - Note that a current target must have been set for the GL context - before a frameBuffer can be created ... in other words, call - ``context.SetCurrent`` before creating a ``RenderTexture``). - """ - - self.name = '{}_{}'.format(type(self).__name__, id(self)) - self.image = image - self.display = display - self.xax = xax - self.yax = yax - - self._addListeners() - self._updateSize() - - RenderTexture.__init__(self, self.width, self.height) - - - def destroy(self): - - RenderTexture.destroy(self) - self.display.removeListener('resolution', self.name) - self.display.removeListener('interpolation', self.name) - self.display.removeListener('transform', self.name) - - - def _addListeners(self): - - # TODO Could change the resolution when - # the image type changes - vector - # images will need a higher - # resolution than voxel space - - self.display.addListener('resolution', self.name, self._updateSize) - self.display.addListener('interpolation', self.name, self.refresh) - self.display.addListener('transform', self.name, self._updateSize) - - - def _updateSize(self, *a): - image = self.image - display = self.display - - resolution = display.resolution / np.array(image.pixdim) - resolution = np.round(resolution) - - if resolution[0] < 1: resolution[0] = 1 - if resolution[1] < 1: resolution[1] = 1 - if resolution[2] < 1: resolution[2] = 1 - - # If the display transformation is 'id' or - # 'pixdim', then the display coordinate system - # axes line up with the voxel coordinate system - # axes, so we can just match the voxel resolution - if display.transform in ('id', 'pixdim'): - - width = image.shape[self.xax] / resolution[self.xax] - height = image.shape[self.yax] / resolution[self.yax] - - # However, if we're displaying in world coordinates, - # we cannot assume any correspondence between the - # voxel coordinate system and the display coordinate - # system. So we'll use a fixed size render texture - # instead. - elif display.transform == 'affine': - width = 256 / resolution.min() - height = 256 / resolution.min() - - # Limit the width/height to an arbitrary maximum - if width > 256 or height > 256: - oldWidth, oldHeight = width, height - ratio = min(width, height) / max(width, height) - - if width > height: - width = 256 - height = width * ratio - else: - height = 256 - width = height * ratio - - log.debug('Limiting texture resolution to {}x{} ' - '(for image resolution {}x{})'.format( - *map(int, (width, height, oldWidth, oldHeight)))) - - width = int(round(width)) - height = int(round(height)) - - self.width = width - self.height = height - - - def setSize(self, width, height): - raise NotImplementedError( - 'Texture size cannot be set for {} instances'.format( - type(self).__name__)) - - - def setAxes(self, xax, yax): - self.xax = xax - self.yax = yax - self.refresh() - - - def refresh(self, *a): - - if self.display.interpolation == 'none': interp = gl.GL_NEAREST - else: interp = gl.GL_LINEAR - - RenderTexture.refresh(self, interp) + self.unbindTexture() diff --git a/fsl/fslview/gl/textures/rendertexture.py b/fsl/fslview/gl/textures/rendertexture.py new file mode 100644 index 0000000000000000000000000000000000000000..fe216b85f59d95cc84bb22e622e165d316c26710 --- /dev/null +++ b/fsl/fslview/gl/textures/rendertexture.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python +# +# rendertexture.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import logging + +import OpenGL.GL as gl +import OpenGL.raw.GL._types as gltypes +import OpenGL.GL.EXT.framebuffer_object as glfbo + +import texture +import fsl.fslview.gl.routines as glroutines + + +log = logging.getLogger(__name__) + + +class RenderTexture(texture.Texture2D): + """A 2D texture and frame buffer, intended to be used as a target for + off-screen rendering of a scene. + """ + + def __init__(self, name, interp=gl.GL_NEAREST): + """ + + Note that a current target must have been set for the GL context + before a frameBuffer can be created ... in other words, call + ``context.SetCurrent`` before creating a ``RenderTexture``). + """ + + texture.Texture2D.__init__(self, name, interp) + + self.__frameBuffer = glfbo.glGenFramebuffersEXT(1) + log.debug('Created fbo: {}'.format(self.__frameBuffer)) + + + def destroy(self): + texture.Texture.destroy(self) + + log.debug('Deleting fbo {}'.format(self.__frameBuffer)) + glfbo.glDeleteFramebuffersEXT(gltypes.GLuint(self.__frameBuffer)) + + + def setData(self, data): + raise NotImplementedError('Texture data cannot be set for {} ' + 'instances'.format(type(self).__name__)) + + + def bindAsRenderTarget(self): + glfbo.glBindFramebufferEXT(glfbo.GL_FRAMEBUFFER_EXT, + self.__frameBuffer) + + + def setRenderViewport(self, xax, yax, lo, hi): + + width, height = self.getSize() + + self.__oldSize = gl.glGetIntegerv(gl.GL_VIEWPORT) + self.__oldProjMat = gl.glGetFloatv( gl.GL_PROJECTION_MATRIX) + self.__oldMVMat = gl.glGetFloatv( gl.GL_MODELVIEW_MATRIX) + + glroutines.show2D(xax, yax, width, height, lo, hi) + + + def restoreViewport(self): + + gl.glViewport(*self.__oldSize) + gl.glMatrixMode(gl.GL_PROJECTION) + gl.glLoadMatrixf(self.__oldProjMat) + gl.glMatrixMode(gl.GL_MODELVIEW) + gl.glLoadMatrixf(self.__oldMVMat) + + + @classmethod + def unbindAsRenderTarget(cls): + glfbo.glBindFramebufferEXT(glfbo.GL_FRAMEBUFFER_EXT, 0) + + + def refresh(self): + texture.Texture2D.refresh(self) + + # Configure the frame buffer + self.bindTexture() + self.bindAsRenderTarget() + glfbo.glFramebufferTexture2DEXT(glfbo.GL_FRAMEBUFFER_EXT, + glfbo.GL_COLOR_ATTACHMENT0_EXT, + gl .GL_TEXTURE_2D, + self.getTextureHandle(), + 0) + + if glfbo.glCheckFramebufferStatusEXT(glfbo.GL_FRAMEBUFFER_EXT) != \ + glfbo.GL_FRAMEBUFFER_COMPLETE_EXT: + raise RuntimeError('An error has occurred while ' + 'configuring the frame buffer') + + self.unbindAsRenderTarget() + self.unbindTexture() + + +class GLObjectRenderTexture(RenderTexture): + + def __init__(self, name, globj, xax, yax, maxResolution=1024): + """ + """ + + self.__globj = globj + self.__xax = xax + self.__yax = yax + self.__maxResolution = maxResolution + + RenderTexture.__init__(self, name) + + name = '{}_{}'.format(self.getTextureName(), id(self)) + globj.addUpdateListener(name, self.__updateSize) + + self.__updateSize() + + + def setAxes(self, xax, yax): + self.__xax = xax + self.__yax = yax + self.__updateSize() + + + def destroy(self): + + name = '{}_{}'.format(self.getTextureName(), id(self)) + self.__globj.removeUpdateListener(name) + RenderTexture.destroy(self) + + + def setSize(self, width, height): + raise NotImplementedError( + 'Texture size cannot be set for {} instances'.format( + type(self).__name__)) + + + def __updateSize(self, *a): + globj = self.__globj + maxRes = self.__maxResolution + + resolution = globj.getDataResolution(self.__xax, self.__yax) + + width = resolution[self.__xax] + height = resolution[self.__yax] + + if width > maxRes or height > maxRes: + oldWidth, oldHeight = width, height + ratio = min(width, height) / max(width, height) + + if width > height: + width = maxRes + height = width * ratio + else: + height = maxRes + width = height * ratio + + width = int(round(width)) + height = int(round(height)) + + log.debug('Limiting texture resolution to {}x{} ' + '(for {} resolution {}x{})'.format( + width, + height, + type(globj).__name__, + oldWidth, + oldHeight)) + + RenderTexture.setSize(self, width, height) diff --git a/fsl/fslview/gl/textures/rendertexturestack.py b/fsl/fslview/gl/textures/rendertexturestack.py new file mode 100644 index 0000000000000000000000000000000000000000..560bca0beef92a2086733ff33a14f223674ec109 --- /dev/null +++ b/fsl/fslview/gl/textures/rendertexturestack.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python +# +# rendertexturelist.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import logging + +import wx +import numpy as np +import OpenGL.GL as gl + +import fsl.fslview.gl.routines as glroutines +import fsl.utils.transform as transform +import rendertexture + + +log = logging.getLogger(__name__) + + +class RenderTextureStack(object): + + def __init__(self, globj): + + + self.name = '{}_{}_{}'.format( + type(self).__name__, + type(globj).__name__, id(self)) + + self.__globj = globj + self.__maxNumTextures = 256 + self.__maxWidth = 1024 + self.__maxHeight = 1024 + self.__defaultNumTextures = 64 + self.__defaultWidth = 256 + self.__defaultHeight = 256 + + self.__textureDirty = [] + self.__textures = [] + + self.__lastDrawnTexture = None + self.__updateQueue = [] + + self.__globj.addUpdateListener( + '{}_{}'.format(type(self).__name__, id(self)), + self.__refreshAllTextures) + + wx.GetApp().Bind(wx.EVT_IDLE, self.__textureUpdateLoop) + + + def __refreshAllTextures(self, *a): + + if self.__lastDrawnTexture is not None: + lastIdx = self.__lastDrawnTexture + else: + lastIdx = len(self.__textures) / 2 + + aboveIdxs = range(lastIdx, len(self.__textures)) + belowIdxs = range(lastIdx, 0, -1) + + idxs = [0] * len(self.__textures) + + for i in range(len(self.__textures)): + + if len(aboveIdxs) > 0 and len(belowIdxs) > 0: + if i % 2: idxs[i] = aboveIdxs.pop(0) + else: idxs[i] = belowIdxs.pop(0) + + elif len(aboveIdxs) > 0: idxs[i] = aboveIdxs.pop(0) + else: idxs[i] = belowIdxs.pop(0) + + self.__textureDirty = [True] * len(self.__textures) + self.__updateQueue = idxs + + + def __zposToIndex(self, zpos): + zmin = self.__zmin + zmax = self.__zmax + ntexs = len(self.__textures) + index = ntexs * (zpos - zmin) / (zmax - zmin) + + limit = len(self.__textures) - 1 + + if index > limit and index <= limit + 1: + index = limit + + return int(index) + + + def __indexToZpos(self, index): + zmin = self.__zmin + zmax = self.__zmax + ntexs = len(self.__textures) + return index * (zmax - zmin) / ntexs + zmin + + + def __textureUpdateLoop(self, ev): + ev.Skip() + + if len(self.__updateQueue) == 0 or len(self.__textures) == 0: + return + + idx = self.__updateQueue.pop(0) + + if not self.__textureDirty[idx]: + return + + tex = self.__textures[idx] + + log.debug('Refreshing texture slice {} (zax {})'.format( + idx, self.__zax)) + + self.__refreshTexture(tex, idx) + + if len(self.__updateQueue) > 0: + ev.RequestMore() + + + def getGLObject(self): + return self.__globj + + + def setAxes(self, xax, yax): + + zax = 3 - xax - yax + self.__xax = xax + self.__yax = yax + self.__zax = zax + + lo, hi = self.__globj.getDisplayBounds() + res = self.__globj.getDataResolution(xax, yax) + + if res is not None: numTextures = res[zax] + else: numTextures = self.__defaultNumTextures + + if numTextures > self.__maxNumTextures: + numTextures = self.__maxNumTextures + + self.__zmin = lo[zax] + self.__zmax = hi[zax] + + self.__destroyTextures() + + for i in range(numTextures): + self.__textures.append( + rendertexture.RenderTexture('{}_{}'.format(self.name, i))) + + self.__textureDirty = [True] * numTextures + self.__refreshAllTextures() + + + def __destroyTextures(self): + texes = self.__textures + self.__textures = [] + for tex in texes: + wx.CallLater(50, tex.destroy) + + + def destroy(self): + self.__destroyTextures() + + + def __refreshTexture(self, tex, idx): + + zpos = self.__indexToZpos(idx) + xax = self.__xax + yax = self.__yax + + lo, hi = self.__globj.getDisplayBounds() + res = self.__globj.getDataResolution(xax, yax) + + if res is not None: + width = res[xax] + height = res[yax] + else: + width = self.__defaultWidth + height = self.__defaultHeight + + if width > self.__maxWidth: width = self.__maxWidth + if height > self.__maxHeight: height = self.__maxHeight + + log.debug('Refreshing render texture for slice {} (zpos {}, ' + 'zax {}): {} x {}'.format(idx, zpos, self.__zax, + width, height)) + + tex.setSize(width, height) + + oldSize = gl.glGetIntegerv(gl.GL_VIEWPORT) + oldProjMat = gl.glGetFloatv( gl.GL_PROJECTION_MATRIX) + oldMVMat = gl.glGetFloatv( gl.GL_MODELVIEW_MATRIX) + + glroutines.show2D(xax, yax, width, height, lo, hi) + + tex.bindAsRenderTarget() + gl.glClear(gl.GL_COLOR_BUFFER_BIT) + self.__globj.preDraw() + self.__globj.draw(zpos) + self.__globj.postDraw() + tex.unbindAsRenderTarget() + + gl.glViewport(*oldSize) + gl.glMatrixMode(gl.GL_PROJECTION) + gl.glLoadMatrixf(oldProjMat) + gl.glMatrixMode(gl.GL_MODELVIEW) + gl.glLoadMatrixf(oldMVMat) + + self.__textureDirty[idx] = False + + + def draw(self, zpos, xform=None): + + xax = self.__xax + yax = self.__yax + zax = self.__zax + + texIdx = self.__zposToIndex(zpos) + self.__lastDrawnTexture = texIdx + + if texIdx < 0 or texIdx >= len(self.__textures): + return + + lo, hi = self.__globj.getDisplayBounds() + texture = self.__textures[texIdx] + + if self.__textureDirty[texIdx]: + self.__refreshTexture(texture, texIdx) + + vertices = np.zeros((6, 3), dtype=np.float32) + vertices[:, zax] = zpos + vertices[0, [xax, yax]] = lo[xax], lo[yax] + vertices[1, [xax, yax]] = lo[xax], hi[yax] + vertices[2, [xax, yax]] = hi[xax], lo[yax] + vertices[3, [xax, yax]] = hi[xax], lo[yax] + vertices[4, [xax, yax]] = lo[xax], hi[yax] + vertices[5, [xax, yax]] = hi[xax], hi[yax] + + if xform is not None: + vertices = transform.transform(vertices, xform=xform) + + texture.draw(vertices) diff --git a/fsl/fslview/gl/textures/selectiontexture.py b/fsl/fslview/gl/textures/selectiontexture.py new file mode 100644 index 0000000000000000000000000000000000000000..8911949c4a26da38780f198d97d88023c28f315c --- /dev/null +++ b/fsl/fslview/gl/textures/selectiontexture.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# +# selectiontexture.py - see fsl.fslview.editor.selection.Selection +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import logging + +import numpy as np +import OpenGL.GL as gl + +import texture + + +log = logging.getLogger(__name__) + + +class SelectionTexture(texture.Texture): + + def __init__(self, name, selection): + + texture.Texture.__init__(self, name, 3) + + self.selection = selection + + selection.addListener('selection', name, self._selectionChanged) + + self._init() + self.refresh() + + + def _init(self): + + self.bindTexture() + + gl.glTexParameteri(gl.GL_TEXTURE_3D, + gl.GL_TEXTURE_MAG_FILTER, + gl.GL_NEAREST) + gl.glTexParameteri(gl.GL_TEXTURE_3D, + gl.GL_TEXTURE_MIN_FILTER, + gl.GL_NEAREST) + + gl.glTexParameterfv(gl.GL_TEXTURE_3D, + gl.GL_TEXTURE_BORDER_COLOR, + np.array([0, 0, 0, 0], dtype=np.float32)) + + gl.glTexParameteri(gl.GL_TEXTURE_3D, + gl.GL_TEXTURE_WRAP_S, + gl.GL_CLAMP_TO_BORDER) + gl.glTexParameteri(gl.GL_TEXTURE_3D, + gl.GL_TEXTURE_WRAP_T, + gl.GL_CLAMP_TO_BORDER) + gl.glTexParameteri(gl.GL_TEXTURE_3D, + gl.GL_TEXTURE_WRAP_R, + gl.GL_CLAMP_TO_BORDER) + + shape = self.selection.selection.shape + gl.glTexImage3D(gl.GL_TEXTURE_3D, + 0, + gl.GL_ALPHA8, + shape[0], + shape[1], + shape[2], + 0, + gl.GL_ALPHA, + gl.GL_UNSIGNED_BYTE, + None) + + self.unbindTexture() + + + def refresh(self, block=None, offset=None): + + if block is None or offset is None: + data = self.selection.selection + offset = [0, 0, 0] + else: + data = block + + data = data * 255 + + log.debug('Updating selection texture (offset {}, size {})'.format( + offset, data.shape)) + + self.bindTexture() + gl.glTexSubImage3D(gl.GL_TEXTURE_3D, + 0, + offset[0], + offset[1], + offset[2], + data.shape[0], + data.shape[1], + data.shape[2], + gl.GL_ALPHA, + gl.GL_UNSIGNED_BYTE, + data.ravel('F')) + self.unbindTexture() + + + def _selectionChanged(self, *a): + + old, new, offset = self.selection.getLastChange() + + if old is None or new is None: + data = self.selection.selection + offset = [0, 0, 0] + else: + data = new + + self.refresh(data, offset) diff --git a/fsl/fslview/gl/textures/texture.py b/fsl/fslview/gl/textures/texture.py new file mode 100644 index 0000000000000000000000000000000000000000..0fac58404796aba126c27375f311d5c2b263c9dd --- /dev/null +++ b/fsl/fslview/gl/textures/texture.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python +# +# texture.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import logging + +import numpy as np +import OpenGL.GL as gl + + +log = logging.getLogger(__name__) + + +class Texture(object): + """All subclasses must accept a ``name`` as the first parameter to their + ``__init__`` method, and must pass said ``name`` through to this + ``__init__`` method. + """ + + def __init__(self, name, ndims): + + self.__texture = gl.glGenTextures(1) + self.__name = name + self.__ndims = ndims + + self.__textureUnit = None + + if ndims == 1: self.__ttype = gl.GL_TEXTURE_1D + elif ndims == 2: self.__ttype = gl.GL_TEXTURE_2D + elif ndims == 3: self.__ttype = gl.GL_TEXTURE_3D + + else: raise ValueError('Invalid number of dimensions') + + log.debug('Created {} ({}) for {}: {}'.format(type(self).__name__, + id(self), + self.__name, + self.__texture)) + + def getTextureName(self): + return self.__name + + + def getTextureHandle(self): + return self.__texture + + + def destroy(self): + + log.debug('Deleting {} ({}) for {}: {}'.format(type(self).__name__, + id(self), + self.__name, + self.__texture)) + + gl.glDeleteTextures(self.__texture) + self.__texture = None + + + def bindTexture(self, textureUnit=None): + + if textureUnit is not None: + gl.glActiveTexture(textureUnit) + gl.glEnable(self.__ttype) + + gl.glBindTexture(self.__ttype, self.__texture) + + self.__textureUnit = textureUnit + + + def unbindTexture(self): + + if self.__textureUnit is not None: + gl.glActiveTexture(self.__textureUnit) + gl.glDisable(self.__ttype) + + gl.glBindTexture(self.__ttype, 0) + + self.__textureUnit = None + + +class Texture2D(Texture): + + def __init__(self, name, interp=gl.GL_NEAREST): + Texture.__init__(self, name, 2) + + self.__data = None + self.__width = None + self.__height = None + self.__oldWidth = None + self.__oldHeight = None + self.__interp = interp + + + def setInterpolation(self, interp): + self.__interp = interp + self.refresh() + + + def setSize(self, width, height): + """ + Sets the width/height for this texture. + + This method also clears the data for this texture, if it has been + previously set via the :meth:`setData` method. + """ + + self.__setSize(width, height) + self.__data = None + + self.refresh() + + + def __setSize(self, width, height): + """Sets the width/height attributes for this texture, and saves a + reference to the old width/height - see comments in the refresh + method. + """ + self.__oldWidth = self.__width + self.__oldHeight = self.__height + self.__width = width + self.__height = height + + + def getSize(self): + """ + """ + return self.__width, self.__height + + + def setData(self, data): + """ + Sets the data for this texture - the width and height are determined + from data shape (which is assumed to be 4*width*height). + """ + + self.__setSize(data.shape[1], data.shape[2]) + self.__data = data + + self.refresh() + + + def refresh(self): + + if self.__width is None or self.__height is None: + return + + self.bindTexture() + gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) + gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) + + gl.glTexParameteri(gl.GL_TEXTURE_2D, + gl.GL_TEXTURE_MAG_FILTER, + self.__interp) + gl.glTexParameteri(gl.GL_TEXTURE_2D, + gl.GL_TEXTURE_MIN_FILTER, + self.__interp) + gl.glTexParameteri(gl.GL_TEXTURE_2D, + gl.GL_TEXTURE_WRAP_S, + gl.GL_CLAMP_TO_BORDER) + gl.glTexParameteri(gl.GL_TEXTURE_2D, + gl.GL_TEXTURE_WRAP_T, + gl.GL_CLAMP_TO_BORDER) + + data = self.__data + + if data is not None: + print data.shape, data.dtype + data = data.ravel('F') + + log.debug('Configuring {} ({}) with size {}x{}'.format( + type(self).__name__, + self.getTextureHandle(), + self.__width, + self.__height)) + + # If the width and height have not changed, + # then we don't need to re-define the texture. + if self.__width == self.__oldWidth and \ + self.__height == self.__oldHeight: + + # But we can use glTexSubImage2D + # if we have data to upload + if data is not None: + gl.glTexSubImage2D(gl.GL_TEXTURE_2D, + 0, + 0, + 0, + self.__width, + self.__height, + gl.GL_RGBA, + gl.GL_UNSIGNED_BYTE, + data) + + # If the width and/or height have + # changed, we need to re-define + # the texture properties + else: + gl.glTexImage2D(gl.GL_TEXTURE_2D, + 0, + gl.GL_RGBA8, + self.__width, + self.__height, + 0, + gl.GL_RGBA, + gl.GL_UNSIGNED_BYTE, + data) + self.unbindTexture() + + + def draw(self, vertices): + + if vertices.shape != (6, 3): + raise ValueError('Six vertices must be provided') + + vertices = np.array(vertices, dtype=np.float32) + texCoords = np.zeros((6, 2), dtype=np.float32) + indices = np.arange(6, dtype=np.uint32) + + texCoords[0, :] = [0, 0] + texCoords[1, :] = [0, 1] + texCoords[2, :] = [1, 0] + texCoords[3, :] = [1, 0] + texCoords[4, :] = [0, 1] + texCoords[5, :] = [1, 1] + + vertices = vertices .ravel('C') + texCoords = texCoords.ravel('C') + + gl.glEnableClientState(gl.GL_VERTEX_ARRAY) + + self.bindTexture(gl.GL_TEXTURE0) + + gl.glClientActiveTexture(gl.GL_TEXTURE0) + gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY) + + gl.glTexEnvf(gl.GL_TEXTURE_ENV, + gl.GL_TEXTURE_ENV_MODE, + gl.GL_REPLACE) + + gl.glVertexPointer( 3, gl.GL_FLOAT, 0, vertices) + gl.glTexCoordPointer(2, gl.GL_FLOAT, 0, texCoords) + + gl.glDrawElements(gl.GL_TRIANGLES, 6, gl.GL_UNSIGNED_INT, indices) + + self.unbindTexture() + + gl.glDisableClientState(gl.GL_VERTEX_ARRAY) + gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY) + + + def drawOnBounds(self, zpos, xmin, xmax, ymin, ymax, xax, yax): + + zax = 3 - xax - yax + vertices = np.zeros((6, 3), dtype=np.float32) + vertices[:, zax] = zpos + + vertices[ 0, [xax, yax]] = [xmin, ymin] + vertices[ 1, [xax, yax]] = [xmin, ymax] + vertices[ 2, [xax, yax]] = [xmax, ymin] + vertices[ 3, [xax, yax]] = [xmax, ymin] + vertices[ 4, [xax, yax]] = [xmin, ymax] + vertices[ 5, [xax, yax]] = [xmax, ymax] + + self.draw(vertices) diff --git a/fsl/fslview/gl/wxgllightboxcanvas.py b/fsl/fslview/gl/wxgllightboxcanvas.py index 2ea95cf5b980f8de77be2d0e2cadbb499db0fb87..06fedc2fa4a948422d8c6dac808720818568d6c7 100644 --- a/fsl/fslview/gl/wxgllightboxcanvas.py +++ b/fsl/fslview/gl/wxgllightboxcanvas.py @@ -64,6 +64,7 @@ class WXGLLightBoxCanvas(lightboxcanvas.LightBoxCanvas, self.removeListener('showGridLines', self.name) self.removeListener('highlightSlice', self.name) self.removeListener('topRow', self.name) + self.removeListener('renderMode', self.name) self.imageList .removeListener('images', self.name) self.displayCtx.removeListener('bounds', self.name) @@ -77,6 +78,7 @@ class WXGLLightBoxCanvas(lightboxcanvas.LightBoxCanvas, disp .removeListener('imageType', self.name) disp .removeListener('enabled', self.name) disp .removeListener('transform', self.name) + disp .removeListener('softwareMode', self.name) disp .removeListener('interpolation', self.name) disp .removeListener('alpha', self.name) disp .removeListener('brightness', self.name) diff --git a/fsl/fslview/gl/wxglslicecanvas.py b/fsl/fslview/gl/wxglslicecanvas.py index 4a0bd189eea5e7170b3af4bfa0e1bea156ba0bd1..69b192001b942c0830a9fd4fee80e32302a73099 100644 --- a/fsl/fslview/gl/wxglslicecanvas.py +++ b/fsl/fslview/gl/wxglslicecanvas.py @@ -50,14 +50,14 @@ class WXGLSliceCanvas(slicecanvas.SliceCanvas, if ev.GetEventObject() is not self: return - self.removeListener('zax', self.name) - self.removeListener('pos', self.name) - self.removeListener('displayBounds', self.name) - self.removeListener('showCursor', self.name) - self.removeListener('invertX', self.name) - self.removeListener('invertY', self.name) - self.removeListener('zoom', self.name) - self.removeListener('twoStageRender', self.name) + self.removeListener('zax', self.name) + self.removeListener('pos', self.name) + self.removeListener('displayBounds', self.name) + self.removeListener('showCursor', self.name) + self.removeListener('invertX', self.name) + self.removeListener('invertY', self.name) + self.removeListener('zoom', self.name) + self.removeListener('renderMode', self.name) self.imageList .removeListener('images', self.name) self.displayCtx.removeListener('bounds', self.name) @@ -69,6 +69,7 @@ class WXGLSliceCanvas(slicecanvas.SliceCanvas, disp .removeListener('imageType', self.name) disp .removeListener('enabled', self.name) disp .removeListener('transform', self.name) + disp .removeListener('softwareMode', self.name) disp .removeListener('interpolation', self.name) disp .removeListener('alpha', self.name) disp .removeListener('brightness', self.name) diff --git a/fsl/fslview/layouts.py b/fsl/fslview/layouts.py index 21ac7637087c6de4fddb791c9487536350f24617..c7f6e5f09405a9d19b3c082b63d58b4d0166d14c 100644 --- a/fsl/fslview/layouts.py +++ b/fsl/fslview/layouts.py @@ -30,6 +30,7 @@ from fsl.fslview.displaycontext import Display from fsl.fslview.displaycontext.volumeopts import VolumeOpts from fsl.fslview.displaycontext.maskopts import MaskOpts from fsl.fslview.displaycontext.vectoropts import VectorOpts +from fsl.fslview.displaycontext.vectoropts import LineVectorOpts from fsl.fslview.displaycontext.sceneopts import SceneOpts from fsl.fslview.displaycontext.orthoopts import OrthoOpts @@ -37,10 +38,9 @@ from fsl.fslview.displaycontext.lightboxopts import LightBoxOpts def widget(labelCls, name, *args, **kwargs): - return props.Widget(name, - label=strings.properties[labelCls, name], - *args, - **kwargs) + + label = strings.properties.get((labelCls, name), name) + return props.Widget(name, label=label, *args, **kwargs) ######################################## @@ -99,7 +99,7 @@ CanvasPanelLayout = props.VGroup(( SceneOptsLayout = props.VGroup(( widget(SceneOpts, 'showCursor'), - widget(SceneOpts, 'twoStageRender'), + widget(SceneOpts, 'performance', spin=False, showLimits=False), widget(SceneOpts, 'showColourBar'), widget(SceneOpts, 'colourBarLabelSide'), widget(SceneOpts, 'colourBarLocation'))) @@ -162,7 +162,8 @@ MaskOptsToolBarLayout = [ VectorOptsToolBarLayout = [ - widget(VectorOpts, 'displayMode'), + widget(VectorOpts, 'modulate'), + widget(VectorOpts, 'modThreshold', showLimits=False, spin=False), actions.ActionButton(ImageDisplayToolBar, 'more')] @@ -193,7 +194,6 @@ MaskOptsLayout = props.VGroup( VectorOptsLayout = props.VGroup(( - widget(VectorOpts, 'displayMode'), props.HGroup(( widget(VectorOpts, 'xColour'), widget(VectorOpts, 'yColour'), @@ -205,7 +205,23 @@ VectorOptsLayout = props.VGroup(( widget(VectorOpts, 'suppressZ')), vertLabels=True), widget(VectorOpts, 'modulate'), - widget(VectorOpts, 'modThreshold', showLimits=False))) + widget(VectorOpts, 'modThreshold', showLimits=False, spin=False))) + +LineVectorOptsLayout = props.VGroup(( + props.HGroup(( + widget(LineVectorOpts, 'xColour'), + widget(LineVectorOpts, 'yColour'), + widget(LineVectorOpts, 'zColour')), + vertLabels=True), + props.HGroup(( + widget(LineVectorOpts, 'suppressX'), + widget(LineVectorOpts, 'suppressY'), + widget(LineVectorOpts, 'suppressZ')), + vertLabels=True), + widget(LineVectorOpts, 'directed'), + widget(LineVectorOpts, 'lineWidth', showLimits=False), + widget(LineVectorOpts, 'modulate'), + widget(LineVectorOpts, 'modThreshold', showLimits=False, spin=False))) ########################## @@ -232,15 +248,16 @@ layouts = td.TypeDict({ 'SceneOpts' : SceneOptsLayout, - ('ImageDisplayToolBar', 'Display') : DisplayToolBarLayout, - ('ImageDisplayToolBar', 'VolumeOpts') : VolumeOptsToolBarLayout, - ('ImageDisplayToolBar', 'MaskOpts') : MaskOptsToolBarLayout, - ('ImageDisplayToolBar', 'VectorOpts') : VectorOptsToolBarLayout, + ('ImageDisplayToolBar', 'Display') : DisplayToolBarLayout, + ('ImageDisplayToolBar', 'VolumeOpts') : VolumeOptsToolBarLayout, + ('ImageDisplayToolBar', 'MaskOpts') : MaskOptsToolBarLayout, + ('ImageDisplayToolBar', 'VectorOpts') : VectorOptsToolBarLayout, - ('ImageDisplayPanel', 'Display') : DisplayLayout, - ('ImageDisplayPanel', 'VolumeOpts') : VolumeOptsLayout, - ('ImageDisplayPanel', 'MaskOpts') : MaskOptsLayout, - ('ImageDisplayPanel', 'VectorOpts') : VectorOptsLayout, + ('ImageDisplayPanel', 'Display') : DisplayLayout, + ('ImageDisplayPanel', 'VolumeOpts') : VolumeOptsLayout, + ('ImageDisplayPanel', 'MaskOpts') : MaskOptsLayout, + ('ImageDisplayPanel', 'VectorOpts') : VectorOptsLayout, + ('ImageDisplayPanel', 'LineVectorOpts') : LineVectorOptsLayout, 'OrthoToolBar' : OrthoToolBarLayout, 'LightBoxToolBar' : LightBoxToolBarLayout, diff --git a/fsl/fslview/profiles/__init__.py b/fsl/fslview/profiles/__init__.py index b3b75b916ca19e80be3113546b05b6fe25c98f0f..a8e955de2e7dbfc8b1bad020dab9fb74fe8c820f 100644 --- a/fsl/fslview/profiles/__init__.py +++ b/fsl/fslview/profiles/__init__.py @@ -235,6 +235,9 @@ class Profile(actions.ActionProvider): def register(self): """This method must be called to register this :class:`Profile` instance as the target for mouse/keyboard events. + + Subclasses may override this method to performa any initialisation, + but must make sure to call this implementation. """ for t in self.getEventTargets(): t.Bind(wx.EVT_LEFT_DOWN, self.__onMouseDown) @@ -251,7 +254,10 @@ class Profile(actions.ActionProvider): def deregister(self): """This method de-registers this :class:`Profile` instance from receiving mouse/keybouard events. - """ + + Subclasses may override this method to performa any initialisation, + but must make sure to call this implementation. + """ for t in self.getEventTargets(): t.Bind(wx.EVT_LEFT_DOWN, None) t.Bind(wx.EVT_MIDDLE_UP, None) diff --git a/fsl/fslview/profiles/orthoeditprofile.py b/fsl/fslview/profiles/orthoeditprofile.py index cd07bb17b4f062c56f8c5f8e359a5c4d9d3433c1..72ca026bd623db3147f2b84979718140a7dfbfdc 100644 --- a/fsl/fslview/profiles/orthoeditprofile.py +++ b/fsl/fslview/profiles/orthoeditprofile.py @@ -150,10 +150,12 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile): # If there's already an existing # selection object, clear it if self._selAnnotation is not None: - xannot.dequeue(self._selAnnotation, hold=True) - yannot.dequeue(self._selAnnotation, hold=True) - zannot.dequeue(self._selAnnotation, hold=True) - self._selAnnotation = None + xannot.dequeue(self._selAnnotation, hold=True) + yannot.dequeue(self._selAnnotation, hold=True) + zannot.dequeue(self._selAnnotation, hold=True) + + self._selAnnotation.destroy() + self._selAnnotation = None self._currentImage = image @@ -203,9 +205,13 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile): def deregister(self): - self._xcanvas.getAnnotations().dequeue(self._selAnnotation, hold=True) - self._ycanvas.getAnnotations().dequeue(self._selAnnotation, hold=True) - self._zcanvas.getAnnotations().dequeue(self._selAnnotation, hold=True) + if self._selAnnotation is not None: + sa = self._selAnnotation + self._xcanvas.getAnnotations().dequeue(sa, hold=True) + self._ycanvas.getAnnotations().dequeue(sa, hold=True) + self._zcanvas.getAnnotations().dequeue(sa, hold=True) + sa.destroy() + orthoviewprofile.OrthoViewProfile.deregister(self) diff --git a/fsl/fslview/profiles/orthoviewprofile.py b/fsl/fslview/profiles/orthoviewprofile.py index df0da9003138457238217f90ad39acb3b9798ae5..7589bd647b0123adc9511a2e7c3c5f04fc04722c 100644 --- a/fsl/fslview/profiles/orthoviewprofile.py +++ b/fsl/fslview/profiles/orthoviewprofile.py @@ -70,6 +70,11 @@ class OrthoViewProfile(profiles.Profile): self._ycanvas = canvasPanel.getYCanvas() self._zcanvas = canvasPanel.getZCanvas() + # This attribute will occasionally store a + # reference to a gl.annotations.Rectangle - + # see the _zoomModeLeftMouse* handlers + self._lastRect = None + def getEventTargets(self): """ @@ -229,7 +234,9 @@ class OrthoViewProfile(profiles.Profile): mouseDownPos, canvasDownPos = self.getMouseDownLocation() - canvas.getAnnotations().dequeue(self._lastRect) + if self._lastRect is not None: + canvas.getAnnotations().dequeue(self._lastRect) + self._lastRect = None rectXlen = abs(canvasPos[canvas.xax] - canvasDownPos[canvas.xax]) rectYlen = abs(canvasPos[canvas.yax] - canvasDownPos[canvas.yax]) diff --git a/fsl/fslview/toolbar.py b/fsl/fslview/toolbar.py index 34400efe167c7a492561db237a8d0d7c5c666a4a..26a2dd595cf83305f812a764e94f06a2afbef277 100644 --- a/fsl/fslview/toolbar.py +++ b/fsl/fslview/toolbar.py @@ -206,7 +206,9 @@ class FSLViewToolBar(fslpanel._FSLViewPanel, wx.PyPanel): if isinstance(toolSpec, actions.ActionButton): label = None else: - label = strings.properties[targets[toolSpec.key], toolSpec.key] + + label = strings.properties.get( + (targets[toolSpec.key], toolSpec.key), toolSpec.key) tools .append(tool) labels.append(label) diff --git a/fsl/fslview/views/canvaspanel.py b/fsl/fslview/views/canvaspanel.py index 611e1424df7a97de050fd7b011b98f506ebe01f3..42d12d57bcaff97e8bb91b26402769d0b5c71f3a 100644 --- a/fsl/fslview/views/canvaspanel.py +++ b/fsl/fslview/views/canvaspanel.py @@ -188,19 +188,28 @@ class CanvasPanel(viewpanel.ViewPanel): self.__opts = sceneOpts - # If the provided DisplayContext does not - # have a parent, this will raise an error. - # But I don't think a CanvasPanel will ever - # be created with a 'master' DisplayContext. - self.bindProps('syncLocation', - displayCtx, - displayCtx.getSyncPropertyName('location')) - self.bindProps('syncImageOrder', - displayCtx, - displayCtx.getSyncPropertyName('imageOrder')) - self.bindProps('syncVolume', - displayCtx, - displayCtx.getSyncPropertyName('volume')) + # Bind the sync* properties of this + # CanvasPanel to the corresponding + # properties on the DisplayContext + # instance. + if displayCtx.getParent() is not None: + self.bindProps('syncLocation', + displayCtx, + displayCtx.getSyncPropertyName('location')) + self.bindProps('syncImageOrder', + displayCtx, + displayCtx.getSyncPropertyName('imageOrder')) + self.bindProps('syncVolume', + displayCtx, + displayCtx.getSyncPropertyName('volume')) + + # If the displayCtx instance does not + # have a parent, this means that it is + # a top level instance + else: + self.disableProperty('syncLocation') + self.disableProperty('syncImageOrder') + self.disableProperty('syncVolume') self.__canvasContainer = wx.Panel(self) self.__canvasPanel = wx.Panel(self.__canvasContainer) diff --git a/fsl/fslview/views/lightboxpanel.py b/fsl/fslview/views/lightboxpanel.py index e5cc029b89ced6da1a2fb5d6997095e80525bd5d..4c63de0ef7a2f1965315f5c2d4b56e115ef2a56d 100644 --- a/fsl/fslview/views/lightboxpanel.py +++ b/fsl/fslview/views/lightboxpanel.py @@ -62,16 +62,18 @@ class LightBoxPanel(canvaspanel.CanvasPanel): displayCtx) # My properties are the canvas properties - sceneOpts.bindProps('zax', self._lbCanvas) - sceneOpts.bindProps('nrows', self._lbCanvas) - sceneOpts.bindProps('ncols', self._lbCanvas) - sceneOpts.bindProps('topRow', self._lbCanvas) - sceneOpts.bindProps('sliceSpacing', self._lbCanvas) - sceneOpts.bindProps('zrange', self._lbCanvas) - sceneOpts.bindProps('showCursor', self._lbCanvas) - sceneOpts.bindProps('showGridLines', self._lbCanvas) - sceneOpts.bindProps('highlightSlice', self._lbCanvas) - sceneOpts.bindProps('twoStageRender', self._lbCanvas) + self._lbCanvas.bindProps('zax', sceneOpts) + self._lbCanvas.bindProps('nrows', sceneOpts) + self._lbCanvas.bindProps('ncols', sceneOpts) + self._lbCanvas.bindProps('topRow', sceneOpts) + self._lbCanvas.bindProps('sliceSpacing', sceneOpts) + self._lbCanvas.bindProps('zrange', sceneOpts) + self._lbCanvas.bindProps('showCursor', sceneOpts) + self._lbCanvas.bindProps('showGridLines', sceneOpts) + self._lbCanvas.bindProps('highlightSlice', sceneOpts) + self._lbCanvas.bindProps('renderMode', sceneOpts) + self._lbCanvas.bindProps('softwareMode', sceneOpts) + self._lbCanvas.bindProps('resolutionLimit', sceneOpts) self._canvasSizer = wx.BoxSizer(wx.HORIZONTAL) self.getCanvasPanel().SetSizer(self._canvasSizer) diff --git a/fsl/fslview/views/orthopanel.py b/fsl/fslview/views/orthopanel.py index bcb145611ed61f0090e530906b1cb20862d05fdf..acd51969ef640dcd2c7dca9f24b1e8f1e94e2dd6 100644 --- a/fsl/fslview/views/orthopanel.py +++ b/fsl/fslview/views/orthopanel.py @@ -110,9 +110,17 @@ class OrthoPanel(canvaspanel.CanvasPanel): self._ycanvas.bindProps('zoom', sceneOpts, 'yzoom') self._zcanvas.bindProps('zoom', sceneOpts, 'zzoom') - self._xcanvas.bindProps('twoStageRender', sceneOpts) - self._ycanvas.bindProps('twoStageRender', sceneOpts) - self._zcanvas.bindProps('twoStageRender', sceneOpts) + self._xcanvas.bindProps('renderMode', sceneOpts) + self._ycanvas.bindProps('renderMode', sceneOpts) + self._zcanvas.bindProps('renderMode', sceneOpts) + + self._xcanvas.bindProps('softwareMode', sceneOpts) + self._ycanvas.bindProps('softwareMode', sceneOpts) + self._zcanvas.bindProps('softwareMode', sceneOpts) + + self._xcanvas.bindProps('resolutionLimit', sceneOpts) + self._ycanvas.bindProps('resolutionLimit', sceneOpts) + self._zcanvas.bindProps('resolutionLimit', sceneOpts) # And a global zoom which controls all canvases at once def onZoom(*a): @@ -169,17 +177,6 @@ class OrthoPanel(canvaspanel.CanvasPanel): self._locationChanged() self.initProfile() - # Set up a default layout (this is probably temporary) - import fsl.fslview.controls.imagelistpanel as ilp - import fsl.fslview.controls.locationpanel as lop - import fsl.fslview.controls.imagedisplaytoolbar as idt - import fsl.fslview.controls.orthotoolbar as ot - - self.togglePanel(ilp.ImageListPanel) - self.togglePanel(lop.LocationPanel) - self.togglePanel(idt.ImageDisplayToolBar, False, self) - self.togglePanel(ot .OrthoToolBar, False, self) - def destroy(self): """Called when this panel is closed. diff --git a/fsl/tools/bet.py b/fsl/tools/bet.py index adeeb30d53b6553d7f023238090385bbddef5e06..69f519311cbc18507cb944f7b7f792f597970305 100644 --- a/fsl/tools/bet.py +++ b/fsl/tools/bet.py @@ -13,6 +13,7 @@ import fsl.data.image as fslimage import fsl.data.imageio as iio import fsl.utils.transform as transform import fsl.fslview.displaycontext as displaycontext +import fsl.fslview.gl as fslgl runChoices = OrderedDict(( @@ -187,6 +188,10 @@ def selectHeadCentre(opts, button): import wx import fsl.fslview.views.orthopanel as orthopanel + # make sure that GL is initialised + fslgl.getWXGLContext(button.GetTopLevelParent()) + fslgl.bootstrap() + image = fslimage.Image(opts.inputImage) imageList = fslimage.ImageList([image]) displayCtx = displaycontext.DisplayContext(imageList) @@ -197,7 +202,6 @@ def selectHeadCentre(opts, button): displayCtx, opts.inputImage, style=wx.RESIZE_BORDER) - panel = frame.panel v2dMat = display.getTransform('voxel', 'display') d2vMat = display.getTransform('display', 'voxel') @@ -222,12 +226,11 @@ def selectHeadCentre(opts, button): displayCtx.addListener('location', 'BETHeadCentre', updateOpts) # Set the initial location on the orthopanel. - # TODO this ain't working, as it needs to be - # done after the frame has been displayed, i.e - # via wx.CallAfter or similar. - voxCoords = [opts.xCoordinate, opts.yCoordinate, opts.zCoordinate] - worldCoords = transform.transform([voxCoords], v2dMat)[0] - panel.pos = worldCoords + voxCoords = [opts.xCoordinate, + opts.yCoordinate, + opts.zCoordinate] + worldCoords = transform.transform([voxCoords], v2dMat)[0] + displayCtx.location = worldCoords # Position the dialog by the button that was clicked pos = button.GetScreenPosition() @@ -272,15 +275,14 @@ def interface(parent, args, opts): import wx - frame = wx.Frame(parent) - betPanel = props.buildGUI( - frame, opts, betView, optLabels, optTooltips) + frame = wx.Frame(parent) + + props.buildGUI(frame, opts, betView, optLabels, optTooltips) frame.Layout() frame.Fit() return frame - def runBet(parent, opts): @@ -292,22 +294,25 @@ def runBet(parent, opts): if exitCode != 0: return + # make sure that GL is initialised + fslgl.getWXGLContext(window.GetTopLevelParent()) + fslgl.bootstrap() + inImage = fslimage.Image(opts.inputImage) outImage = fslimage.Image(opts.outputImage) imageList = fslimage.ImageList([inImage, outImage]) displayCtx = displaycontext.DisplayContext(imageList) outDisplay = displayCtx.getDisplayProperties(outImage) + outOpts = outDisplay.getDisplayOpts() - outDisplay.cmap = 'Reds' - outDisplay.displayRange.xlo = 1 - outDisplay.clipLow = True - outDisplay.clipHigh = True + outOpts.cmap = 'Red' + outOpts.clippingRange.xlo = 1 - frame = orthopanel.OrthoFrame(parent, - imageList, - displayCtx, - title=opts.outputImage) + frame = orthopanel.OrthoFrame(parent, + imageList, + displayCtx, + title=opts.outputImage) frame.Show() runwindow.checkAndRun('BET', opts, parent, Options.genBetCmd, diff --git a/fsl/tools/fslview_parseargs.py b/fsl/tools/fslview_parseargs.py index 14cd94f855dcf635ddb7d51711a1bcbfb0c71916..ce24cf676fcf6af1b34e9ea26a14b3df967efa4a 100644 --- a/fsl/tools/fslview_parseargs.py +++ b/fsl/tools/fslview_parseargs.py @@ -87,7 +87,7 @@ OPTIONS = td.TypeDict({ 'showColourBar', 'colourBarLocation', 'colourBarLabelSide', - 'twoStageRender'], + 'performance'], # From here on, all of the keys are # the names of HasProperties classes, @@ -129,8 +129,7 @@ OPTIONS = td.TypeDict({ 'MaskOpts' : ['colour', 'invert', 'threshold'], - 'VectorOpts' : ['displayMode', - 'xColour', + 'VectorOpts' : ['xColour', 'yColour', 'zColour', 'suppressX', @@ -172,7 +171,7 @@ ARGUMENTS = td.TypeDict({ 'SceneOpts.colourBarLocation' : ('cbl', 'colourBarLocation'), 'SceneOpts.colourBarLabelSide' : ('cbs', 'colourBarLabelSide'), 'SceneOpts.showCursor' : ('hc', 'hideCursor'), - 'SceneOpts.twoStageRender' : ('tr', 'twoStageRendering'), + 'SceneOpts.performance' : ('p', 'performance'), 'OrthoOpts.xzoom' : ('xz', 'xzoom'), 'OrthoOpts.yzoom' : ('yz', 'yzoom'), @@ -214,7 +213,6 @@ ARGUMENTS = td.TypeDict({ 'MaskOpts.invert' : ('mi', 'maskInvert'), 'MaskOpts.threshold' : ('t', 'threshold'), - 'VectorOpts.displayMode' : ('d', 'displayMode'), 'VectorOpts.xColour' : ('xc', 'xColour'), 'VectorOpts.yColour' : ('yc', 'yColour'), 'VectorOpts.zColour' : ('zc', 'zColour'), @@ -242,7 +240,8 @@ HELP = td.TypeDict({ 'SceneOpts.showColourBar' : 'Show colour bar', 'SceneOpts.colourBarLocation' : 'Colour bar location', 'SceneOpts.colourBarLabelSide' : 'Colour bar label orientation', - 'SceneOpts.twoStageRender' : 'Enable two-stage rendering', + 'SceneOpts.performance' : 'Rendering performance ' + '(1=fastest, 5=best looking)', 'OrthoOpts.xzoom' : 'X canvas zoom', 'OrthoOpts.yzoom' : 'Y canvas zoom', @@ -284,7 +283,6 @@ HELP = td.TypeDict({ 'MaskOpts.invert' : 'Invert', 'MaskOpts.threshold' : 'Threshold', - 'VectorOpts.displayMode' : 'Display mode', 'VectorOpts.xColour' : 'X colour', 'VectorOpts.yColour' : 'Y colour', 'VectorOpts.zColour' : 'Z colour', @@ -880,8 +878,7 @@ def applyImageArgs(args, imageList, displayCtx, **kwargs): try: modImage = fslimage.Image(args.images[i].modulate) - if modImage.shape != image.shape[ :3] or \ - modImage.pixdim != image.pixdim[:3]: + if modImage.shape != image.shape[ :3]: raise RuntimeError( 'Image {} cannot be used to modulate {} - ' 'dimensions don\'t match'.format(modImage, image)) diff --git a/fsl/utils/colourbarbitmap.py b/fsl/utils/colourbarbitmap.py index 1cef1b234be328513f073a178dcb043910ed96bd..f46d36ad8c8bf5a6bc31b575d17104dd5610441d 100644 --- a/fsl/utils/colourbarbitmap.py +++ b/fsl/utils/colourbarbitmap.py @@ -34,6 +34,9 @@ def colourBarBitmap(cmap, textColour='#ffffff'): """Plots a colour bar using matplotlib, and returns a RGBA bitmap of the specified width/height. + + The bitmap is returned as a W*H*4 numpy array, with the top-left + pixel located at index ``[0, 0, :]``. """ if orientation not in ['vertical', 'horizontal']: @@ -112,12 +115,15 @@ def colourBarBitmap(cmap, ncols, nrows = canvas.get_width_height() bitmap = np.fromstring(buf, dtype=np.uint8) - bitmap = bitmap.reshape(nrows, ncols, 4) + bitmap = bitmap.reshape(nrows, ncols, 4).transpose([1, 0, 2]) + # the bitmap is in argb order, + # but we want it in rgba rgb = bitmap[:, :, 1:] a = bitmap[:, :, 0] bitmap = np.dstack((rgb, a)) if orientation == 'vertical': bitmap = np.flipud(bitmap.transpose([1, 0, 2])) + return bitmap