diff --git a/fsl/data/fslimage.py b/fsl/data/fslimage.py new file mode 100644 index 0000000000000000000000000000000000000000..0005ddc3e96958ab7c936b5a230f7a8ddb8b3f77 --- /dev/null +++ b/fsl/data/fslimage.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# +# fslimage.py - Object representing a 3D image. +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import numpy as np +import nibabel as nib +import matplotlib.cm as mplcm +import matplotlib.colors as mplcolors + +import fsl.data.imagefile as imagefile + +class Colours(object): + + def __init__(self, image): + self.image = image + + def __getitem__(self, key): + + image = self.image + cmap = image.cmap + + cmap.set_under('k') + cmap.set_over( 'k') + + norm = mplcolors.Normalize(image.displaymin,image.displaymax) + data = image.data.__getitem__(key) + colData = cmap(norm(data)) + + # move the colour dimension to the front so, e.g. + # colData[:,a,b,c] will return the colour data for + # voxel [a,b,c] + colData = np.rollaxis(colData, len(colData.shape)-1) + + # trim the alpha values, as we use an image wide alpha + return colData.take(range(3), axis=0) + + +class Image(object): + + @property + def colour(self): + return self._colour[:] + + def __init__(self, image): + + # The image parameter may be the name of an image file + if isinstance(image, str): + image = nib.load(imagefile.addExt(image)) + + # Or a numpy array - we wrap it in a nibabel image, + # with an identity transformation (each voxel maps + # to 1mm^3 in real world space) + elif isinstance(image, np.ndarray): + image = nib.nifti1.Nifti1Image(image, np.identity(4)) + + # otherwise, we assume that it is a nibabel image + self.nibImage = image + self.data = image.get_data() + + xdim,ydim,zdim = self.nibImage.get_shape() + xlen,ylen,zlen = self.nibImage.get_header().get_zooms() + + self.xdim = xdim + self.ydim = ydim + self.zdim = zdim + + self.xlen = xlen + self.ylen = ylen + self.zlen = zlen + + # Attributes controlling image display + self.cmap = mplcm.Greys_r + self.alpha = 1.0 + self.displaymin = self.data.min()# use cal_min/cal_max instead? + self.displaymax = self.data.max() + self.datamin = self.data.min() # use cal_min/cal_max instead? + self.datamax = self.data.max() + + self._colour = Colours(self) + + def __getitem__(self, key): + return self.data.__getitem__(key) + diff --git a/fsl/data/image.py b/fsl/data/image.py deleted file mode 100644 index 29428e7237189c54a0abfbf3bf3291154db6c4e4..0000000000000000000000000000000000000000 --- a/fsl/data/image.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python -# -# image.py - Object representing a 3D image. -# -# Author: Paul McCarthy <pauldmccarthy@gmail.com> -# - -import numpy as np -import nibabel as nib -import matplotlib.colors as colors -import matplotlib.cm as cm - -import fsl.data.filename as filename - -class Image(object): - - def __init__(self, image): - - if isinstance(image, str): - image = nib.load(filename.addExt(image)) - - self.image = image - self.imageData = image.get_data() - - xdim,ydim,zdim = self.image.get_shape() - xlen,ylen,zlen = self.image.get_header().get_zooms() - - self.xdim = xdim - self.ydim = ydim - self.zdim = zdim - - self.xlen = xlen - self.ylen = ylen - self.zlen = zlen - - # Attributes controlling image display - self.cmap = cm.Greys - self.alpha = 1.0 - self.rangemin = imageData.min() # use cal_min/cal_max instead? - self.rangemax = imageData.max() diff --git a/fsl/utils/slicecanvas.py b/fsl/utils/slicecanvas.py index e94620533ff76622034849e3ca2a61631bbe8acc..76fb793fddfa576dac7e45b6045c7a7c4a875080 100644 --- a/fsl/utils/slicecanvas.py +++ b/fsl/utils/slicecanvas.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # slicecanvas.py - A wx.GLCanvas canvas which displays a single -# slice from a 3D numpy array. +# slice from a 3D image. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # @@ -21,6 +21,8 @@ import OpenGL.arrays.vbo as vbo import OpenGL.GL.ARB.instanced_arrays as arbia import OpenGL.GL.ARB.draw_instanced as arbdi +import fsl.data.fslimage as fslimage + # A slice is rendered using three buffers. The first buffer, # the 'geometry buffer' simply contains four vertices, which # define the geometry of a single voxel (using triangle @@ -30,20 +32,23 @@ import OpenGL.GL.ARB.draw_instanced as arbdi # of every voxel in one slice of the image (these locations are # identical for every slice of the image, so we can re-use the # location information for every slice). The third buffer, the -# 'image buffer' contains data for the entire image, and is used -# to colour each voxel. This image buffer may be shared between +# 'image buffer' contains colour data (3 uint8s per voxel, +# representing rgb values), for the entire image, and is used to +# colour each voxel. This image buffer may be shared between # multiple SliceCanvas objects which are displaying the same -# image - see the 'master' parameter to the SliceCanvas constructor. -# +# image - see the 'master' parameter to the SliceCanvas +# constructor. + # The vertex shader positions and colours a single vertex. vertex_shader = """ #version 120 -attribute vec2 inVertex; /* Current vertex */ -attribute vec2 inPosition; /* Position of the current voxel */ -attribute float inColour; /* Value of the current voxel */ -varying vec4 outColour; /* Colour, generated from the value */ +uniform float alpha; /* Opacity - constant for a whole image */ +attribute vec2 inVertex; /* Current vertex */ +attribute vec2 inPosition; /* Position of the current voxel */ +attribute vec3 inColour; /* Value of the current voxel */ +varying vec4 outColour; /* Colour, generated from the value */ void main(void) { @@ -55,8 +60,7 @@ void main(void) { gl_Position = gl_ModelViewProjectionMatrix * \ vec4(inVertex+inPosition, 0.0, 1.0); - /* Greyscale only for the time being. */ - outColour = vec4(inColour, inColour, inColour, 1.0); + outColour = vec4(inColour, alpha); } """ @@ -146,16 +150,30 @@ class SliceCanvas(wxgl.GLCanvas): Parameters: parent - WX parent object - image - 3D numpy array to be displayed - zax - Axis to be displayed (the 'depth' axis), default 0 + + image - A fsl.data.image.Image object, or a 3D numpy array. + + zax - Axis perpendicular to the plane to be displayed + (the 'depth' axis), default 0. + zpos - Initial slice to be displayed. If not provided, the middle slice is used. + master - Another SliceCanvas object with which to share the GL context and the image buffer data. """ + if not isinstance(image, fslimage.Image): + image = fslimage.Image(image) + wxgl.GLCanvas.__init__(self, parent, **kwargs) + if master is not None: context = master.context + else: context = wxgl.GLContext(self) + + self.master = master + self.context = context + # TODO Currently, the displayed x/horizontal and # y/vertical axes are defined by their order in # the image. Allow the caller to specify which @@ -163,26 +181,20 @@ class SliceCanvas(wxgl.GLCanvas): dims = range(3) dims.pop(zax) - if master is not None: context = master.context - else: context = wxgl.GLContext(self) - - self.master = master - self.context = context - self.image = image self.xax = dims[0] self.yax = dims[1] self.zax = zax - self.xdim = self.image.shape[self.xax] - self.ydim = self.image.shape[self.yax] - self.zdim = self.image.shape[self.zax] + self.xdim = self.image.data.shape[self.xax] + self.ydim = self.image.data.shape[self.yax] + self.zdim = self.image.data.shape[self.zax] - dsize = self.image.dtype.itemsize + dsize = self.image.data.dtype.itemsize - self.xstride = self.image.strides[self.xax] / dsize - self.ystride = self.image.strides[self.yax] / dsize - self.zstride = self.image.strides[self.zax] / dsize + self.xstride = self.image.data.strides[self.xax] / dsize + self.ystride = self.image.data.strides[self.yax] / dsize + self.zstride = self.image.data.strides[self.zax] / dsize if zpos is None: zpos = self.zdim / 2 @@ -222,9 +234,10 @@ class SliceCanvas(wxgl.GLCanvas): shaders.compileShader(vertex_shader, gl.GL_VERTEX_SHADER), shaders.compileShader(fragment_shader, gl.GL_FRAGMENT_SHADER)) - self.rawVertexPos = gl.glGetAttribLocation(self.shaders, 'inVertex') - self.rawColourPos = gl.glGetAttribLocation(self.shaders, 'inColour') - self.rawPositionPos = gl.glGetAttribLocation(self.shaders, 'inPosition') + self.inVertexPos = gl.glGetAttribLocation( self.shaders, 'inVertex') + self.inColourPos = gl.glGetAttribLocation( self.shaders, 'inColour') + self.inPositionPos = gl.glGetAttribLocation( self.shaders, 'inPosition') + self.alphaPos = gl.glGetUniformLocation(self.shaders, 'alpha') # Data stored in the geometry buffer. Defines # the geometry of a single voxel, rendered as @@ -257,23 +270,20 @@ class SliceCanvas(wxgl.GLCanvas): image buffer is used instead. """ - imageData = self.image - del self.image - if self.master is not None: return self.master.imageBuffer - - # The image data is normalised to lie between 0 and 256 - imageData = 255.0*(imageData - imageData.min()) / \ - (imageData.max() - imageData.min()) - # Then cast to uint8 and flattened (with dimension - # ordering preserved - very important, as numpy - # always defaults to C style ordering!) - imageData = np.array(imageData, dtype=np.uint8) - imageData = imageData.ravel(order='A') + # The image data is normalised to lie between 0 and 255 + colourData = self.image.colour + colourData = 255.0*colourData - imageBuffer = vbo.VBO(imageData, gl.GL_STATIC_DRAW) + # Then cast to uint8 and flattened, with fortran + # dimension ordering, so the data, as stored on + # the GPU, has its first dimension as the fastest + # changing. + colourData = np.array(colourData, dtype=np.uint8) + colourData = colourData.ravel(order='F') + imageBuffer = vbo.VBO(colourData, gl.GL_STATIC_DRAW) return imageBuffer @@ -315,6 +325,8 @@ class SliceCanvas(wxgl.GLCanvas): gl.glUseProgram(self.shaders) + gl.glUniform1f(self.alphaPos, self.image.alpha) + # We draw each horizontal row of voxels one at a time. # This is necessary because, in order to allow image # buffers to be shared between different SliceCanvas @@ -332,49 +344,49 @@ class SliceCanvas(wxgl.GLCanvas): # single vertex (4 vertices, drawn as a triangle strip) self.geomBuffer.bind() gl.glVertexAttribPointer( - self.rawVertexPos, + self.inVertexPos, 2, gl.GL_UNSIGNED_BYTE, gl.GL_FALSE, 0, None) - gl.glEnableVertexAttribArray(self.rawVertexPos) - arbia.glVertexAttribDivisorARB(self.rawVertexPos, 0) + gl.glEnableVertexAttribArray(self.inVertexPos) + arbia.glVertexAttribDivisorARB(self.inVertexPos, 0) # The position buffer, which defines # the location of every voxel self.positionBuffer.bind() gl.glVertexAttribPointer( - self.rawPositionPos, + self.inPositionPos, 2, gl.GL_UNSIGNED_SHORT, gl.GL_FALSE, 0, self.positionBuffer + posOffset) - gl.glEnableVertexAttribArray(self.rawPositionPos) - arbia.glVertexAttribDivisorARB(self.rawPositionPos, 1) + gl.glEnableVertexAttribArray(self.inPositionPos) + arbia.glVertexAttribDivisorARB(self.inPositionPos, 1) # The image buffer, which defines - # the value at each voxel. + # the colour value at each voxel. self.imageBuffer.bind() gl.glVertexAttribPointer( - self.rawColourPos, - 1, + self.inColourPos, + 3, gl.GL_UNSIGNED_BYTE, gl.GL_TRUE, - imageStride, - self.imageBuffer + imageOffset) + imageStride*3, + self.imageBuffer + imageOffset*3) - gl.glEnableVertexAttribArray(self.rawColourPos) - arbia.glVertexAttribDivisorARB(self.rawColourPos, 1) + gl.glEnableVertexAttribArray(self.inColourPos) + arbia.glVertexAttribDivisorARB(self.inColourPos, 1) # Draw all of the triangles! arbdi.glDrawArraysInstancedARB( gl.GL_TRIANGLE_STRIP, 0, 4, self.xdim) - gl.glDisableVertexAttribArray(self.rawVertexPos) - gl.glDisableVertexAttribArray(self.rawPositionPos) - gl.glDisableVertexAttribArray(self.rawColourPos) + gl.glDisableVertexAttribArray(self.inVertexPos) + gl.glDisableVertexAttribArray(self.inPositionPos) + gl.glDisableVertexAttribArray(self.inColourPos) gl.glUseProgram(0)