From 1a54c6c216512313a0aaad705884538cce2508db Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Tue, 22 Apr 2014 13:42:51 +0100 Subject: [PATCH] ImageView supports multiple images. Pretty hacky at this stage, needs cleaning up. Have gone back to storing image data as uint8s. --- fsl/data/fslimage.py | 25 ++++++++++++++++---- fsl/utils/imageview.py | 51 ++++++++++++++++++++++------------------ fsl/utils/slicecanvas.py | 20 +++++++++------- 3 files changed, 59 insertions(+), 37 deletions(-) diff --git a/fsl/data/fslimage.py b/fsl/data/fslimage.py index 3717e76a9..217903eda 100644 --- a/fsl/data/fslimage.py +++ b/fsl/data/fslimage.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -# fslimage.py - Classes representing a 3D image, the display -# properties of a 3D image, and a collection of 3D images. +# fslimage.py - Classes for representing 3D images, display +# properties of 3D images, and collections of 3D images. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # @@ -39,6 +39,7 @@ class Image(object): # otherwise, we assume that it is a nibabel image self.nibImage = image self.data = image.get_data() + self.name = image.get_filename() xdim,ydim,zdim = self.nibImage.get_shape() xlen,ylen,zlen = self.nibImage.get_header().get_zooms() @@ -51,6 +52,10 @@ class Image(object): self.ylen = ylen self.zlen = zlen + # This attribute may be used to point to an OpenGL + # buffer which is to be shared between multiple users + # (e.g. two SliceCanvas instances which are displaying + # a different view of the same image) self.glBuffer = None @@ -59,7 +64,6 @@ class ImageDisplay(props.HasProperties): A class which describes how an image should be displayed. """ - def updateColourMap(self, newVal): """ When a colour property changes, this method is called - @@ -76,6 +80,8 @@ class ImageDisplay(props.HasProperties): cmap.set_under(cmap(0.0), alpha=1.0) cmap.set_over( cmap(1.0), alpha=1.0) + # The display properties of an image + enabled = props.Boolean() alpha = props.Double(minval=0.0, maxval=1.0, default=1.0) displayMin = props.Double() displayMax = props.Double() @@ -85,9 +91,14 @@ class ImageDisplay(props.HasProperties): cmap = props.ColourMap(default=mplcm.Greys_r, preNotifyFunc=updateColourMap) - - _view = props.VGroup(('displayMin', 'displayMax', 'alpha', 'rangeClip', 'cmap')) + _view = props.VGroup(('enabled', + 'displayMin', + 'displayMax', + 'alpha', + 'rangeClip', + 'cmap')) _labels = { + 'enabled' : 'Enabled', 'displayMin' : 'Min.', 'displayMax' : 'Max.', 'alpha' : 'Opacity', @@ -112,6 +123,9 @@ class ImageDisplay(props.HasProperties): class ImageList(object): + """ + Class representing a collection of images to be displayed together. + """ def __init__(self, images=None, displays=None): @@ -120,3 +134,4 @@ class ImageList(object): self.images = images self.displays = displays + diff --git a/fsl/utils/imageview.py b/fsl/utils/imageview.py index d651bb597..5e75097c2 100644 --- a/fsl/utils/imageview.py +++ b/fsl/utils/imageview.py @@ -1,8 +1,8 @@ #!/usr/bin/env python # -# imgshow.py - A wx/OpenGL widget for displaying and interacting with a 3D -# image. Displays three canvases, each of which shows a slice of the image -# along each dimension. +# imgshow.py - A wx/OpenGL widget for displaying and interacting with a +# collection of 3D image. Displays three canvases, each of which shows +# a slice of the images along each dimension. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # @@ -29,27 +29,21 @@ LocationEvent, EVT_LOCATION_EVENT = wxevent.NewEvent() class ImageView(wx.Panel): - def __init__(self, parent, image, *args, **kwargs): + def __init__(self, parent, imageList, *args, **kwargs): """ Creates three SliceCanvas objects, each displaying a different axis of the given 3D numpy image. """ - if not isinstance(image, fslimage.Image): - image = fslimage.Image(image) - - self.imageDisplay = fslimage.ImageDisplay(image) - - imageList = fslimage.ImageList([image], [self.imageDisplay]) + self.imageList = imageList wx.Panel.__init__(self, parent, *args, **kwargs) self.SetMinSize((300,100)) - self.shape = image.data.shape + self.shape = imageList.images[0].data.shape + + self.canvasPanel = wx.Panel(self) - self.canvasPanel = wx.Panel(self) - self.controlPanel = props.buildGUI(self, self.imageDisplay) - self.xcanvas = slicecanvas.SliceCanvas( self.canvasPanel, imageList, zax=0) self.ycanvas = slicecanvas.SliceCanvas( @@ -57,6 +51,13 @@ class ImageView(wx.Panel): self.zcanvas = slicecanvas.SliceCanvas( self.canvasPanel, imageList, zax=2, context=self.xcanvas.context) + + self.controlPanel = wx.Notebook(self) + for i in range(len(imageList.images)): + + controlPanel = props.buildGUI(self.controlPanel, self.imageList.displays[i]) + self.controlPanel.AddPage(controlPanel, '{}'.format(i)) + self.mainSizer = wx.BoxSizer(wx.VERTICAL) self.canvasSizer = wx.BoxSizer(wx.HORIZONTAL) @@ -170,28 +171,32 @@ class ImageView(wx.Panel): class ImageFrame(wx.Frame): """ - Convenience class for displaying an image in a standalone window. + Convenience class for displaying a collection of images in a standalone + window. """ - def __init__(self, parent, image, title=None): + def __init__(self, parent, imageList, title=None): wx.Frame.__init__(self, parent, title=title) - self.image = image - self.panel = ImageView(self, image) + self.imageList = imageList + self.panel = ImageView(self, imageList) self.Layout() if __name__ == '__main__': - if len(sys.argv) != 2: - print 'usage: imageview.py filename' + if len(sys.argv) < 2: + print 'usage: imageview.py filename [filename]' sys.exit(1) - app = wx.App() - image = fslimage.Image(sys.argv[1]) + app = wx.App() + images = map(fslimage.Image, sys.argv[1:]) + displays = map(fslimage.ImageDisplay, images) + imageList = fslimage.ImageList(images, displays) + frame = ImageFrame( None, - image, + imageList, title=sys.argv[1]) frame.Show() diff --git a/fsl/utils/slicecanvas.py b/fsl/utils/slicecanvas.py index 4f052fba6..30de77b4c 100644 --- a/fsl/utils/slicecanvas.py +++ b/fsl/utils/slicecanvas.py @@ -44,7 +44,7 @@ class GLImageData(object): slice). The third buffer, the 'image buffer' contains the image data itself, - scaled to lie between 0.0 and 1.0. It is used to calculate voxel colours. + scaled to lie between 0 and 255. It is used to calculate voxel colours. Finally, the texture, the 'colour buffer', is used to store a lookup table containing colours. @@ -375,11 +375,13 @@ class SliceCanvas(wxgl.GLCanvas): if image.glBuffer is not None: return image.glBuffer - # The image data is cast to single precision floating - # point, and normalised to lie between 0.0 and 1.0 + # The image data is normalised to lie + # between 0 and 256, and cast to uint8 imageData = np.array(image.data, dtype=np.float32) - imageData = (imageData - imageData.min()) / \ - (imageData.max() - imageData.min()) + imageData = 255.0*(imageData - imageData.min()) / \ + (imageData.max() - imageData.min()) + imageData = np.array(imageData, dtype=np.uint8) + # Then flattened, with fortran dimension ordering, # so the data, as stored on the GPU, has its first @@ -558,10 +560,10 @@ class SliceCanvas(wxgl.GLCanvas): gl.glVertexAttribPointer( self.voxelValuePos, 1, - gl.GL_FLOAT, - gl.GL_FALSE, - imageStride*4, - imageBuffer + imageOffset*4) + gl.GL_UNSIGNED_BYTE, + gl.GL_TRUE, + imageStride, + imageBuffer + imageOffset) gl.glEnableVertexAttribArray(self.voxelValuePos) arbia.glVertexAttribDivisorARB(self.voxelValuePos, 1) -- GitLab