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

Substantial changes. Back to storing straight image data on the GPU...

Substantial changes. Back to storing straight image data on the GPU (normalised to lie between 0.0 and 1.0). Colours are now provided by a 1D texture buffer, which can be dynamically updated. If you want to change the colour map, all you need to do is replace the texture. Easy1
parent 2067257e
No related branches found
No related tags found
No related merge requests found
...@@ -10,39 +10,21 @@ import nibabel as nib ...@@ -10,39 +10,21 @@ import nibabel as nib
import matplotlib.cm as mplcm import matplotlib.cm as mplcm
import matplotlib.colors as mplcolors import matplotlib.colors as mplcolors
import fsl.props as props
import fsl.data.imagefile as imagefile import fsl.data.imagefile as imagefile
class Colours(object): class Image(props.HasProperties):
def __init__(self, image): alpha = props.Double(minval=0.0, maxval=1.0, default=1.0)
self.image = image displayMin = props.Double()
displayMax = props.Double()
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 _view = props.VGroup(('displayMin', 'displayMax', 'alpha'))
return colData.take(range(3), axis=0) _labels = {
'displayMin' : 'Min.',
'displayMax' : 'Max.',
class Image(object): 'alpha' : 'Opacity'
}
@property
def colour(self):
return self._colour[:]
def __init__(self, image): def __init__(self, image):
...@@ -74,13 +56,8 @@ class Image(object): ...@@ -74,13 +56,8 @@ class Image(object):
# Attributes controlling image display # Attributes controlling image display
self.cmap = mplcm.Greys_r self.cmap = mplcm.Greys_r
self.alpha = 1.0 self.alpha = 1.0
self.displaymin = self.data.min()# use cal_min/cal_max instead? self.datamin = self.data.min()
self.displaymax = self.data.max() self.datamax = self.data.max()
self.datamin = self.data.min() # use cal_min/cal_max instead? self.displayMin = self.datamin # use cal_min/cal_max instead?
self.datamax = self.data.max() self.displayMax = self.datamax
self._colour = Colours(self)
def __getitem__(self, key):
return self.data.__getitem__(key)
...@@ -683,10 +683,17 @@ def buildGUI(parent, ...@@ -683,10 +683,17 @@ def buildGUI(parent,
- tooltips: Dict specifying tooltips - tooltips: Dict specifying tooltips
""" """
if view is None: view = _defaultView(hasProps) if view is None:
if hasattr(hasProps, '_view'): view = hasProps._view
if labels is None: labels = {} else: view = _defaultView(hasProps)
if tooltips is None: tooltips = {} if labels is None:
if hasattr(hasProps, '_labels'): labels = hasProps._labels
else: labels = {}
if tooltips is None:
if hasattr(hasProps, '_tooltips'): tooltips = hasProps._tooltips
else: tooltips = {}
print labels
propGui = PropGUI() propGui = PropGUI()
view = _prepareView(view, labels, tooltips) view = _prepareView(view, labels, tooltips)
......
...@@ -7,9 +7,13 @@ ...@@ -7,9 +7,13 @@
# Author: Paul McCarthy <pauldmccarthy@gmail.com> # Author: Paul McCarthy <pauldmccarthy@gmail.com>
# #
import sys
import wx import wx
import wx.lib.newevent as wxevent import wx.lib.newevent as wxevent
import fsl.props as props
import fsl.data.fslimage as fslimage
import fsl.utils.slicecanvas as slicecanvas import fsl.utils.slicecanvas as slicecanvas
LocationEvent, EVT_LOCATION_EVENT = wxevent.NewEvent() LocationEvent, EVT_LOCATION_EVENT = wxevent.NewEvent()
...@@ -25,22 +29,33 @@ class ImageView(wx.Panel): ...@@ -25,22 +29,33 @@ class ImageView(wx.Panel):
wx.Panel.__init__(self, parent, *args, **kwargs) wx.Panel.__init__(self, parent, *args, **kwargs)
self.SetMinSize((300,100)) self.SetMinSize((300,100))
self.shape = image.shape self.shape = image.data.shape
self.canvasPanel = wx.Panel(self)
self.controlPanel = props.buildGUI(self, image)
self.xcanvas = slicecanvas.SliceCanvas(
self.canvasPanel, image, zax=0)
self.ycanvas = slicecanvas.SliceCanvas(
self.canvasPanel, image, zax=1, master=self.xcanvas)
self.zcanvas = slicecanvas.SliceCanvas(
self.canvasPanel, image, zax=2, master=self.xcanvas)
self.xcanvas = slicecanvas.SliceCanvas(self, image, zax=0) self.mainSizer = wx.BoxSizer(wx.VERTICAL)
self.ycanvas = slicecanvas.SliceCanvas(self, image, zax=1, self.canvasSizer = wx.BoxSizer(wx.HORIZONTAL)
master=self.xcanvas)
self.zcanvas = slicecanvas.SliceCanvas(self, image, zax=2,
master=self.xcanvas)
self.sizer = wx.BoxSizer(wx.HORIZONTAL) self.SetSizer(self.mainSizer)
self.SetSizer(self.sizer) self.mainSizer.Add(self.canvasPanel, flag=wx.EXPAND, proportion=1)
self.mainSizer.Add(self.controlPanel, flag=wx.EXPAND)
self.canvasPanel.SetSizer(self.canvasSizer)
self.sizer.Add(self.xcanvas, flag=wx.EXPAND, proportion=1) self.canvasSizer.Add(self.xcanvas, flag=wx.EXPAND, proportion=1)
self.sizer.Add(self.ycanvas, flag=wx.EXPAND, proportion=1) self.canvasSizer.Add(self.ycanvas, flag=wx.EXPAND, proportion=1)
self.sizer.Add(self.zcanvas, flag=wx.EXPAND, proportion=1) self.canvasSizer.Add(self.zcanvas, flag=wx.EXPAND, proportion=1)
self.canvasPanel.Layout()
self.Layout() self.Layout()
self.xcanvas.Bind(wx.EVT_LEFT_DOWN, self._setCanvasPosition) self.xcanvas.Bind(wx.EVT_LEFT_DOWN, self._setCanvasPosition)
...@@ -153,18 +168,15 @@ class ImageFrame(wx.Frame): ...@@ -153,18 +168,15 @@ class ImageFrame(wx.Frame):
if __name__ == '__main__': if __name__ == '__main__':
import sys
import nibabel as nb
if len(sys.argv) != 2: if len(sys.argv) != 2:
print 'usage: imageview.py filename' print 'usage: imageview.py filename'
sys.exit(1) sys.exit(1)
app = wx.App() app = wx.App()
image = nb.load(sys.argv[1]) image = fslimage.Image(sys.argv[1])
frame = ImageFrame( frame = ImageFrame(
None, None,
image.get_data(), image,
title=sys.argv[1]) title=sys.argv[1])
frame.Show() frame.Show()
......
...@@ -16,39 +16,43 @@ import OpenGL.GL.shaders as shaders ...@@ -16,39 +16,43 @@ import OpenGL.GL.shaders as shaders
import OpenGL.arrays.vbo as vbo import OpenGL.arrays.vbo as vbo
# Under OS X, I don't think I can request an OpenGL 3.2 core profile # Under OS X, I don't think I can request an OpenGL 3.2 core profile
# - I'm stuck with OpenGL 2.1 I'm using these ARB extensions for # using wx - I'm stuck with OpenGL 2.1 I'm using these ARB extensions
# functionality which is standard in 3.2. # for functionality which is standard in 3.2.
import OpenGL.GL.ARB.instanced_arrays as arbia import OpenGL.GL.ARB.instanced_arrays as arbia
import OpenGL.GL.ARB.draw_instanced as arbdi import OpenGL.GL.ARB.draw_instanced as arbdi
import fsl.data.fslimage as fslimage import fsl.data.fslimage as fslimage
# A slice is rendered using three buffers. The first buffer, # A slice is rendered using three buffers and one texture. The first
# the 'geometry buffer' simply contains four vertices, which # buffer, the 'geometry buffer' simply contains four vertices, which
# define the geometry of a single voxel (using triangle # define the geometry of a single voxel (using triangle strips).
# strips).
# The second buffer, the 'position buffer', contains the location # The second buffer, the 'position buffer', contains the location of
# of every voxel in one slice of the image (these locations are # every voxel in one slice of the image (these locations are identical
# identical for every slice of the image, so we can re-use the # for every slice of the image, so we can re-use the location
# location information for every slice). The third buffer, the # information for every slice).
# 'image buffer' contains colour data (3 uint8s per voxel,
# representing rgb values), for the entire image, and is used to # The third buffer, the 'image buffer' contains the image data itself,
# colour each voxel. This image buffer may be shared between # scaled to lie between 0.0 and 1.0. It is used to calculate voxel
# multiple SliceCanvas objects which are displaying the same # colours, and may be shared between multiple SliceCanvas objects
# image - see the 'master' parameter to the SliceCanvas # which are displaying the same image - see the 'master' parameter to
# constructor. # the SliceCanvas constructor.
#
# Finally, the texture, the 'colour buffer', is used to store a
# lookup table containing colours.
# The vertex shader positions and colours a single vertex. # The vertex shader positions and colours a single vertex.
vertex_shader = """ vertex_shader = """
#version 120 #version 120
uniform float alpha; /* Opacity - constant for a whole image */ uniform float alpha; /* Opacity - constant for a whole image */
attribute vec2 inVertex; /* Current vertex */
attribute vec2 inPosition; /* Position of the current voxel */ attribute vec2 inVertex; /* Current vertex */
attribute vec3 inColour; /* Value of the current voxel */ attribute vec2 inPosition; /* Position of the current voxel */
varying vec4 outColour; /* Colour, generated from the value */ attribute float voxelValue; /* Value of the current voxel (in range [0,1]) */
varying float fragVoxelValue; /* Voxel value passed through to fragment shader */
void main(void) { void main(void) {
...@@ -60,18 +64,25 @@ void main(void) { ...@@ -60,18 +64,25 @@ void main(void) {
gl_Position = gl_ModelViewProjectionMatrix * \ gl_Position = gl_ModelViewProjectionMatrix * \
vec4(inVertex+inPosition, 0.0, 1.0); vec4(inVertex+inPosition, 0.0, 1.0);
outColour = vec4(inColour, alpha); /* Pass the voxel value through to the shader. */
fragVoxelValue = voxelValue;
} }
""" """
# Default fragment shader, does nothing special.
# Fragment shader. Given the current voxel value, looks
# up the appropriate colour in the colour buffer.
fragment_shader = """ fragment_shader = """
#version 120 #version 120
varying vec4 outColour; uniform float alpha;
uniform sampler1D colourMap; /* RGB colour map, stored as a 1D texture */
varying float fragVoxelValue;
void main(void) { void main(void) {
gl_FragColor = outColour;
vec3 color = texture1D(colourMap, fragVoxelValue).rgb;
gl_FragColor = vec4(color, alpha);
} }
""" """
...@@ -92,7 +103,8 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -92,7 +103,8 @@ class SliceCanvas(wxgl.GLCanvas):
@zpos.setter @zpos.setter
def zpos(self, zpos): def zpos(self, zpos):
""" """
Change the slice being displayed. You need to manually call Refresh(). Change the slice being displayed. You will need to manually call
Refresh() after changing the zpos.
""" """
zpos = int(round(zpos)) zpos = int(round(zpos))
...@@ -112,7 +124,8 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -112,7 +124,8 @@ class SliceCanvas(wxgl.GLCanvas):
@xpos.setter @xpos.setter
def xpos(self, xpos): def xpos(self, xpos):
""" """
Change the x cursor position. You need to manually call Refresh(). Change the x cursor position. You will need to manually call
Refresh() after changing the xpos.
""" """
xpos = int(round(xpos)) xpos = int(round(xpos))
...@@ -132,7 +145,8 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -132,7 +145,8 @@ class SliceCanvas(wxgl.GLCanvas):
@ypos.setter @ypos.setter
def ypos(self, ypos): def ypos(self, ypos):
""" """
Change the y cursor position. You need to manually call Refresh(). Change the y cursor position. You will need to manually call
Refresh() after changing the ypos.
""" """
ypos = int(round(ypos)) ypos = int(round(ypos))
...@@ -143,6 +157,28 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -143,6 +157,28 @@ class SliceCanvas(wxgl.GLCanvas):
self._ypos = ypos self._ypos = ypos
@property
def colourResolution(self):
"""
Total number of possible colours that will be used when rendering
a slice.
"""
return self._colourResolution
@colourResolution.setter
def colourResolution(self, colourResolution):
"""
Updates the colour resolution. You will need to manually call
updateColourBuffer(), and then Refresh(), after changing the
colour resolution.
"""
if colourResolution <= 0: return
if colourResolution > 4096: return # this upper limit is arbitrary.
self._colourResolution = colourResolution
def __init__(self, parent, image, zax=0, zpos=None, master=None, **kwargs): def __init__(self, parent, image, zax=0, zpos=None, master=None, **kwargs):
""" """
Creates a canvas object. The OpenGL data buffers are set up in Creates a canvas object. The OpenGL data buffers are set up in
...@@ -203,6 +239,8 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -203,6 +239,8 @@ class SliceCanvas(wxgl.GLCanvas):
self._ypos = self.ydim / 2 self._ypos = self.ydim / 2
self._zpos = zpos self._zpos = zpos
self._colourResolution = 256
# these attributes are created by _initGLData, # these attributes are created by _initGLData,
# which is called on the first EVT_PAINT event # which is called on the first EVT_PAINT event
self.geomBuffer = None self.geomBuffer = None
...@@ -234,10 +272,12 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -234,10 +272,12 @@ class SliceCanvas(wxgl.GLCanvas):
shaders.compileShader(vertex_shader, gl.GL_VERTEX_SHADER), shaders.compileShader(vertex_shader, gl.GL_VERTEX_SHADER),
shaders.compileShader(fragment_shader, gl.GL_FRAGMENT_SHADER)) shaders.compileShader(fragment_shader, gl.GL_FRAGMENT_SHADER))
# Indexes of all vertex/fragment shader parameters
self.inVertexPos = gl.glGetAttribLocation( self.shaders, 'inVertex') self.inVertexPos = gl.glGetAttribLocation( self.shaders, 'inVertex')
self.inColourPos = gl.glGetAttribLocation( self.shaders, 'inColour') self.voxelValuePos = gl.glGetAttribLocation( self.shaders, 'voxelValue')
self.inPositionPos = gl.glGetAttribLocation( self.shaders, 'inPosition') self.inPositionPos = gl.glGetAttribLocation( self.shaders, 'inPosition')
self.alphaPos = gl.glGetUniformLocation(self.shaders, 'alpha') self.alphaPos = gl.glGetUniformLocation(self.shaders, 'alpha')
self.colourMapPos = gl.glGetUniformLocation(self.shaders, 'colourMap')
# Data stored in the geometry buffer. Defines # Data stored in the geometry buffer. Defines
# the geometry of a single voxel, rendered as # the geometry of a single voxel, rendered as
...@@ -262,7 +302,12 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -262,7 +302,12 @@ class SliceCanvas(wxgl.GLCanvas):
# The image buffer, containing the image data itself # The image buffer, containing the image data itself
self.imageBuffer = self._initImageBuffer() self.imageBuffer = self._initImageBuffer()
# The colour buffer, containing a map of
# colours (stored on the GPU as a 1D texture)
self.colourBuffer = gl.glGenTextures(1)
self.updateColourBuffer()
def _initImageBuffer(self): def _initImageBuffer(self):
""" """
Initialises the buffer used to store the image data. If a 'master' Initialises the buffer used to store the image data. If a 'master'
...@@ -270,28 +315,73 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -270,28 +315,73 @@ class SliceCanvas(wxgl.GLCanvas):
image buffer is used instead. image buffer is used instead.
""" """
# If a master canvas was passed to the
# constructor, let's share its image data.
if self.master is not None: if self.master is not None:
return self.master.imageBuffer return self.master.imageBuffer
# The image data is normalised to lie between 0 and 255 # The image data is cast to single precision floating
colourData = self.image.colour # point, and normalised to lie between 0.0 and 1.0
colourData = 255.0*colourData imageData = np.array(self.image.data, dtype=np.float32)
imageData = (imageData - imageData.min()) / \
(imageData.max() - imageData.min())
# Then cast to uint8 and flattened, with fortran # Then flattened, with fortran dimension ordering,
# dimension ordering, so the data, as stored on # so the data, as stored on the GPU, has its first
# the GPU, has its first dimension as the fastest # dimension as the fastest changing.
# changing. imageData = imageData.ravel(order='F')
colourData = np.array(colourData, dtype=np.uint8) imageBuffer = vbo.VBO(imageData, gl.GL_STATIC_DRAW)
colourData = colourData.ravel(order='F')
imageBuffer = vbo.VBO(colourData, gl.GL_STATIC_DRAW)
return imageBuffer return imageBuffer
def updateColourBuffer(self):
"""
Regenerates the colour buffer used to colour a slice. After
calling this method, you will need to call Refresh() for the
change to take effect.
"""
# Create [self.colourResolution] rgb values,
# spanning the entire range of the image
# colour map (see fsl.data.fslimage.Image)
colourmap = self.image.cmap(
np.linspace(0.0, 1.0, self.colourResolution))
# Strip the alpha values (we use an image wide
# alpha constant - fsl.data.fslimage.Image.alpha),
colourmap = colourmap[:,:3]
colourmap = np.floor(colourmap * 255)
# The colour data is stored on
# the GPU as 8 bit rgb triplets
colourmap = np.array(colourmap, dtype=np.uint8)
colourmap = colourmap.ravel(order='C')
# GL texture creation stuff
gl.glBindTexture(gl.GL_TEXTURE_1D, self.colourBuffer)
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_RGB8,
self.colourResolution,
0,
gl.GL_RGB,
gl.GL_UNSIGNED_BYTE,
colourmap)
def resize(self): def resize(self):
""" """
Sets up the GL canvas size, viewport, and Sets up the GL canvas size, viewport, and
projection. This method is called by draw(). projection. This method is called by draw(),
so does not need to be called manually.
""" """
try: self.context.SetCurrent(self) try: self.context.SetCurrent(self)
...@@ -325,6 +415,12 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -325,6 +415,12 @@ class SliceCanvas(wxgl.GLCanvas):
gl.glUseProgram(self.shaders) gl.glUseProgram(self.shaders)
# Set up the colour buffer
gl.glEnable(gl.GL_TEXTURE_1D)
gl.glActiveTexture(gl.GL_TEXTURE0)
gl.glBindTexture(gl.GL_TEXTURE_1D, self.colourBuffer)
gl.glUniform1i(self.colourMapPos, 0)
gl.glUniform1f(self.alphaPos, self.image.alpha) gl.glUniform1f(self.alphaPos, self.image.alpha)
# We draw each horizontal row of voxels one at a time. # We draw each horizontal row of voxels one at a time.
...@@ -333,7 +429,9 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -333,7 +429,9 @@ class SliceCanvas(wxgl.GLCanvas):
# objects, we cannot re-arrange the image data, as # objects, we cannot re-arrange the image data, as
# stored in GPU memory. So while the memory offset # stored in GPU memory. So while the memory offset
# between values in the same row (or column) is # between values in the same row (or column) is
# consistent, the offset between rows (columns) is not. # consistent, the offset between rows (columns) is
# not. And drawing rows seems to be faster than
# drawing columns, for reasons unknown to me.
for yi in range(self.ydim): for yi in range(self.ydim):
imageOffset = self.zpos * self.zstride + yi * self.ystride imageOffset = self.zpos * self.zstride + yi * self.ystride
...@@ -370,15 +468,15 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -370,15 +468,15 @@ class SliceCanvas(wxgl.GLCanvas):
# the colour value at each voxel. # the colour value at each voxel.
self.imageBuffer.bind() self.imageBuffer.bind()
gl.glVertexAttribPointer( gl.glVertexAttribPointer(
self.inColourPos, self.voxelValuePos,
3, 1,
gl.GL_UNSIGNED_BYTE, gl.GL_FLOAT,
gl.GL_TRUE, gl.GL_FALSE,
imageStride*3, imageStride*4,
self.imageBuffer + imageOffset*3) self.imageBuffer + imageOffset*4)
gl.glEnableVertexAttribArray(self.inColourPos) gl.glEnableVertexAttribArray(self.voxelValuePos)
arbia.glVertexAttribDivisorARB(self.inColourPos, 1) arbia.glVertexAttribDivisorARB(self.voxelValuePos, 1)
# Draw all of the triangles! # Draw all of the triangles!
arbdi.glDrawArraysInstancedARB( arbdi.glDrawArraysInstancedARB(
...@@ -386,7 +484,8 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -386,7 +484,8 @@ class SliceCanvas(wxgl.GLCanvas):
gl.glDisableVertexAttribArray(self.inVertexPos) gl.glDisableVertexAttribArray(self.inVertexPos)
gl.glDisableVertexAttribArray(self.inPositionPos) gl.glDisableVertexAttribArray(self.inPositionPos)
gl.glDisableVertexAttribArray(self.inColourPos) gl.glDisableVertexAttribArray(self.voxelValuePos)
gl.glDisable(gl.GL_TEXTURE_1D)
gl.glUseProgram(0) 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