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

slicecanvas.py now uses an fsl.data.fslimage.Image object, with associated...

slicecanvas.py now uses an fsl.data.fslimage.Image object, with associated colour map information. Image data is now stored on the GPU as r,g,b triplets for each voxel.
parent 25fca14f
No related branches found
No related tags found
No related merge requests found
#!/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)
#!/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()
#!/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)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment