diff --git a/fsl/data/fslimage.py b/fsl/data/fslimage.py index bf4b25650ed05116cbcc199e5bdf96d1cb8abab3..6852e8f1aa096b847901df2ccb76feffef68e8b7 100644 --- a/fsl/data/fslimage.py +++ b/fsl/data/fslimage.py @@ -8,6 +8,7 @@ import sys import logging +import traceback import collections import os.path as op @@ -392,3 +393,4 @@ class ImageList(object): listener(self) except Exception as e: log.debug('Listener raised exception: {}'.format(e.message)) + traceback.print_exc() diff --git a/fsl/fslview/slicecanvas.py b/fsl/fslview/slicecanvas.py index 05e60d3570d3713554cb6d3483d0c77fbe2aa2af..6c1595ace9c5da5a331264b672fc05297ba7230f 100644 --- a/fsl/fslview/slicecanvas.py +++ b/fsl/fslview/slicecanvas.py @@ -20,6 +20,7 @@ import OpenGL.arrays.vbo as vbo # for functionality which is standard in 3.2. import OpenGL.GL.ARB.instanced_arrays as arbia import OpenGL.GL.ARB.draw_instanced as arbdi +import OpenGL.GL.ARB.texture_rg as arbrg import fsl.data.fslimage as fslimage @@ -57,11 +58,6 @@ class GLImageData(object): self.image = image self.canvas = canvas - - self.imageBuffer = None - self.colourBuffer = None - self.positionBuffer = None - self.geomBuffer = None # Here, x,y, and z refer to screen # coordinates, not image coordinates: @@ -79,13 +75,6 @@ class GLImageData(object): self.ylen = image.pixdim[canvas.yax] self.zlen = image.pixdim[canvas.zax] - dsize = image.data.dtype.itemsize - - # byte offset along each axis - self.xstride = image.data.strides[canvas.xax] / dsize - self.ystride = image.data.strides[canvas.yax] / dsize - self.zstride = image.data.strides[canvas.zax] / dsize - # Maximum number of colours used to draw image data self.colourResolution = 256 @@ -131,50 +120,51 @@ class GLImageData(object): # float32 positionData = image.voxToWorld(positionData, axes=(xax, yax)) positionData = np.array(positionData, dtype=np.float32) + positionData = positionData.ravel('C') - # Define GL buffers for the geometry and position - # data containing the data we just created above - geomData = geomData .ravel(order='C') - positionData = positionData.ravel(order='C') - geomBuffer = vbo.VBO(geomData, gl.GL_STATIC_DRAW) - positionBuffer = vbo.VBO(positionData, gl.GL_STATIC_DRAW) - - # The image buffer, containing the image data itself - imageBuffer = self.initImageBuffer() - - # The colour buffer, containing a map of - # colours (stored on the GPU as a 1D texture) - colourBuffer = gl.glGenTextures(1) + # The image buffers, containing the image data + imageBuffers = self.initImageBuffer() + screenPosBuffer = vbo.VBO(positionData, gl.GL_STATIC_DRAW) + geomBuffer = vbo.VBO(geomData, gl.GL_STATIC_DRAW) - self.geomBuffer = geomBuffer - self.positionBuffer = positionBuffer - self.imageBuffer = imageBuffer - self.colourBuffer = colourBuffer + self.dataBuffer = imageBuffers['dataBuffer'] + self.voxXBuffer = imageBuffers['xBuffer'] + self.voxYBuffer = imageBuffers['yBuffer'] + self.voxZBuffer = imageBuffers['zBuffer'] + self.screenPosBuffer = screenPosBuffer + self.geomBuffer = geomBuffer # Add listeners to this image so the view can be # updated when its display properties are changed self.configDisplayListeners() - # Create the colour buffer for the given image - self.updateColourBuffer() - def initImageBuffer(self): """ - Initialises the OpenGL buffer used to store the data for the given - image. The buffer is stored as an attribute of the image and, if it + Initialises the OpenGL buffers used to store the data for the given + image. The buffers are stored as an attribute of the image and, if it has already been created (e.g. by another SliceCanvas object), the - existing buffer is returned. + existing buffer is returned. The value stored on the image, and + the value returned by this method, is a dictionary with the following + keys: + - dataBuffer: + - xBuffer: + - yBuffer: + - zBuffer: """ image = self.image - try: imageBuffer = image.getAttribute('glBuffer') + try: imageBuffer = image.getAttribute('glBuffers') except: imageBuffer = None if imageBuffer is not None: return imageBuffer + xlen = image.shape[0] + ylen = image.shape[1] + zlen = image.shape[2] + # The image data is normalised to lie # between 0 and 255, and cast to uint8 imageData = np.array(image.data, dtype=np.float32) @@ -186,12 +176,61 @@ class GLImageData(object): # so the data, as stored on the GPU, has its first # dimension as the fastest changing. imageData = imageData.ravel(order='F') - imageBuffer = vbo.VBO(imageData, gl.GL_STATIC_DRAW) + + # Image data is stored on the GPU as a 3D texture + dataBuffer = gl.glGenTextures(1) + gl.glBindTexture(gl.GL_TEXTURE_3D, dataBuffer) + 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.glTexParameteri( + gl.GL_TEXTURE_3D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE) + + gl.glTexImage3D(gl.GL_TEXTURE_3D, + 0, + arbrg.GL_R8, + xlen, + ylen, + zlen, + 0, + gl.GL_RED, + gl.GL_UNSIGNED_BYTE, + imageData) + + # x/y/z coordinates are stored as + # VBO arrays in the range [0, 1] + xstep = 1.0 / xlen + ystep = 1.0 / ylen + zstep = 1.0 / zlen + + xData = np.arange(0.0+xstep/2, 1.0+xstep/2, 1.0 / xlen, dtype=np.float32) + yData = np.arange(0.0+ystep/2, 1.0+ystep/2, 1.0 / ylen, dtype=np.float32) + zData = np.arange(0.0+zstep/2, 1.0+zstep/2, 1.0 / zlen, dtype=np.float32) + xBuffer = vbo.VBO(xData, gl.GL_STATIC_DRAW) + yBuffer = vbo.VBO(yData, gl.GL_STATIC_DRAW) + zBuffer = vbo.VBO(zData, gl.GL_STATIC_DRAW) + + + print "Data buffer" + print imageData + print "X buffer" + print xData + print "Y buffer" + print yData + print "Z buffer" + print zData + + imageBuffer = {} + imageBuffer['dataBuffer'] = dataBuffer + imageBuffer['xBuffer'] = xBuffer + imageBuffer['yBuffer'] = yBuffer + imageBuffer['zBuffer'] = zBuffer # And added as an attribute of the image, so # other things which want to render the image - # don't need to create another buffer. - image.setAttribute('glBuffer', imageBuffer) + # don't need to recreate all of those buffers. + image.setAttribute('glBuffers', imageBuffer) return imageBuffer @@ -232,63 +271,6 @@ class GLImageData(object): display.addListener(prop, lnrName.format(prop), colourUpdateNeeded) - def updateColourBuffer(self): - """ - Regenerates the colour buffer used to colour image voxels. - """ - - display = self.image.display - colourBuffer = self.colourBuffer - - # Here we are creating a range of values to be passed - # to the matplotlib.colors.Colormap instance of the - # image display. We scale this range such that data - # values which lie outside the configured display range - # will map to values below 0.0 or above 1.0. It is - # assumed that the Colormap instance is configured to - # generate appropriate colours for these out-of-range - # values. - - normalRange = np.linspace(0.0, 1.0, self.colourResolution) - normalStep = 1.0 / (self.colourResolution - 1) - - normMin = (display.displayMin - display.dataMin) / \ - (display.dataMax - display.dataMin) - normMax = (display.displayMax - display.dataMin) / \ - (display.dataMax - display.dataMin) - - newStep = normalStep / (normMax - normMin) - newRange = (normalRange - normMin) * (newStep / normalStep) - - # Create [self.colourResolution] rgb values, - # spanning the entire range of the image - # colour map - colourmap = display.cmap(newRange) - - # The colour data is stored on - # the GPU as 8 bit rgb triplets - 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, 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_RGBA8, - self.colourResolution, - 0, - gl.GL_RGBA, - gl.GL_UNSIGNED_BYTE, - colourmap) - # The vertex shader positions and colours a single vertex. vertex_shader = """ @@ -297,14 +279,19 @@ vertex_shader = """ /* Opacity - constant for a whole image */ uniform float alpha; +/* image data texture */ +uniform sampler3D dataBuffer; + /* Current vertex */ attribute vec2 inVertex; -/* Position of the current voxel */ -attribute vec2 inPos; +/* Current screen coordinates */ +attribute vec2 screenPos; -/* Value of the current voxel (in range [0,1]) */ -attribute float voxValue; +/* voxel coordinates */ +attribute float voxX; +attribute float voxY; +attribute float voxZ; /* Voxel value passed through to fragment shader */ varying float fragVoxValue; @@ -316,33 +303,28 @@ void main(void) { * (and perform standard transformation from data * coordinates to screen coordinates). */ - gl_Position = gl_ModelViewProjectionMatrix * \ - vec4(inVertex+inPos, 0.0, 1.0); + gl_Position = gl_ModelViewProjectionMatrix * \ + vec4(inVertex + screenPos, 0.0, 1.0); /* Pass the voxel value through to the shader. */ - fragVoxValue = voxValue; + vec4 vt = texture3D(dataBuffer, vec3(voxX, voxY, voxZ)); + fragVoxValue = vt.r; } """ -# Fragment shader. Given the current voxel value, looks +# Buffer shader. Given the current voxel value, looks # up the appropriate colour in the colour buffer. fragment_shader = """ #version 120 uniform float alpha; -uniform sampler1D colourMap; /* RGB colour map, stored as a 1D texture */ varying float fragVoxValue; void main(void) { - vec4 voxTexture = texture1D(colourMap, fragVoxValue); - vec3 voxColour = voxTexture.rgb; - float voxAlpha = voxTexture.a; - - if (voxAlpha > alpha) { - voxAlpha = alpha; - } + vec3 voxColour = vec3(fragVoxValue, fragVoxValue, fragVoxValue); + float voxAlpha = alpha; gl_FragColor = vec4(voxColour, voxAlpha); } @@ -551,16 +533,6 @@ class SliceCanvas(wxgl.GLCanvas): of this SliceCanvas object. This method is only called once, on the first draw. """ - - # A bit hacky. We can only set the GL context (and create - # the GL data) once something is actually displayed on the - # screen. The _initGLData method is called (asynchronously) - # by the draw() method if it sees that the glReady flag has - # not yet been set. But draw() may be called mored than once - # before _initGLData is called. Here, to prevent - # _initGLData from running more than once, the first time - # it is called it simply overwrites itself with a dummy method. - self._initGLData = lambda s: s self.context.SetCurrent(self) @@ -568,17 +540,33 @@ class SliceCanvas(wxgl.GLCanvas): shaders.compileShader(vertex_shader, gl.GL_VERTEX_SHADER), shaders.compileShader(fragment_shader, gl.GL_FRAGMENT_SHADER)) - # Indices of all vertex/fragment shader parameters - self.inVertexPos = gl.glGetAttribLocation( self.shaders, 'inVertex') - self.voxelValuePos = gl.glGetAttribLocation( self.shaders, 'voxValue') - self.inPositionPos = gl.glGetAttribLocation( self.shaders, 'inPos') - self.alphaPos = gl.glGetUniformLocation(self.shaders, 'alpha') - self.colourMapPos = gl.glGetUniformLocation(self.shaders, 'colourMap') + # Indices of all vertex/fragment shader parameters + self.alphaPos = gl.glGetUniformLocation(self.shaders, 'alpha') + self.dataBufferPos = gl.glGetUniformLocation(self.shaders, + 'dataBuffer') + self.inVertexPos = gl.glGetAttribLocation( self.shaders, + 'inVertex') + self.screenPosPos = gl.glGetAttribLocation( self.shaders, + 'screenPos') + self.voxXPos = gl.glGetAttribLocation( self.shaders, 'voxX') + self.voxYPos = gl.glGetAttribLocation( self.shaders, 'voxY') + self.voxZPos = gl.glGetAttribLocation( self.shaders, 'voxZ') # Initialise data for the images that # are already in the image list self._imageListChanged() + + # A bit hacky. We can only set the GL context (and create + # the GL data) once something is actually displayed on the + # screen. The _initGLData method is called (asynchronously) + # by the draw() method if it sees that the glReady flag has + # not yet been set. But draw() may be called mored than once + # before _initGLData is called. Here, to prevent + # _initGLData from running more than once, the first time + # it is called it simply overwrites itself with a dummy method. + self._initGLData = lambda s: s + self.glReady = True @@ -590,7 +578,7 @@ class SliceCanvas(wxgl.GLCanvas): """ size = self.GetSize() - + # set up 2D drawing gl.glViewport(0, 0, size.width, size.height) gl.glMatrixMode(gl.GL_PROJECTION) @@ -616,6 +604,7 @@ class SliceCanvas(wxgl.GLCanvas): # clear the canvas gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) + # load the shaders gl.glUseProgram(self.shaders) # enable transparency @@ -637,17 +626,16 @@ class SliceCanvas(wxgl.GLCanvas): imageDisplay = image.display - geomBuffer = glImageData.geomBuffer - imageBuffer = glImageData.imageBuffer - positionBuffer = glImageData.positionBuffer - colourBuffer = glImageData.colourBuffer - - xdim = glImageData.xdim - ydim = glImageData.ydim - zdim = glImageData.zdim - xstride = glImageData.xstride - ystride = glImageData.ystride - zstride = glImageData.zstride + dataBuffer = glImageData.dataBuffer + voxXBuffer = glImageData.voxXBuffer + voxYBuffer = glImageData.voxYBuffer + voxZBuffer = glImageData.voxZBuffer + geomBuffer = glImageData.geomBuffer + screenPosBuffer = glImageData.screenPosBuffer + + xdim = glImageData.xdim + ydim = glImageData.ydim + zdim = glImageData.zdim # Don't draw the slice if this # image display is disabled @@ -656,79 +644,95 @@ class SliceCanvas(wxgl.GLCanvas): # Figure out which slice we are drawing, # and if it's out of range, don't draw it zi = int(image.worldToVox(self.zpos, self.zax)) - if zi < 0 or zi >= zdim: continue + if zi < 0 or zi >= zdim: continue - # Set up the colour buffer - gl.glEnable(gl.GL_TEXTURE_1D) - gl.glActiveTexture(gl.GL_TEXTURE0) - gl.glBindTexture(gl.GL_TEXTURE_1D, colourBuffer) - gl.glUniform1i(self.colourMapPos, 0) + voxOffs = [0, 0, 0] + voxSteps = [1, 1, 1] + voxOffs[ self.zax] = zi + voxSteps[self.yax] = xdim + voxSteps[self.zax] = xdim * ydim + + # bind the current alpha value to the + # shader alpha variable gl.glUniform1f(self.alphaPos, imageDisplay.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 - # objects, we cannot re-arrange the image data, as - # stored in GPU memory. So while the memory offset - # between values in the same row (or column) is - # 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(ydim): - - imageOffset = zi * zstride + yi * ystride - imageStride = xstride - posOffset = yi * xdim * 8 - - # The geometry buffer, which defines the geometry of a - # single vertex (4 vertices, drawn as a triangle strip) - geomBuffer.bind() - gl.glVertexAttribPointer( - self.inVertexPos, - 2, - gl.GL_FLOAT, - gl.GL_FALSE, - 0, - None) - gl.glEnableVertexAttribArray(self.inVertexPos) - arbia.glVertexAttribDivisorARB(self.inVertexPos, 0) + # bind the transformation matrix + # to the shader variable + # gl.glUniformMatrix4fv(self.voxToWorldMatPos, + # 1, False, transformBuffer) - # The position buffer, which defines - # the location of every voxel - positionBuffer.bind() + # Set up the colour buffer + # gl.glEnable(gl.GL_TEXTURE_1D) + # gl.glActiveTexture(gl.GL_TEXTURE0) + # gl.glBindTexture(gl.GL_TEXTURE_1D, colourBuffer) + # gl.glUniform1i(self.colourMapPos, 0) + + # Set up the image data buffer + gl.glEnable(gl.GL_TEXTURE_3D) + # change to texxture 1 when you get working + gl.glActiveTexture(gl.GL_TEXTURE0) + gl.glBindTexture(gl.GL_TEXTURE_3D, dataBuffer) + gl.glUniform1i(self.dataBufferPos, 0) + + # Screen x positions + screenPosBuffer.bind() + gl.glVertexAttribPointer( + self.screenPosPos, + 2, + gl.GL_FLOAT, + gl.GL_FALSE, + 0, + None) + gl.glEnableVertexAttribArray(self.screenPosPos) + arbia.glVertexAttribDivisorARB(self.screenPosPos, 1) + + for buf, pos, step, off in zip( + (voxXBuffer, voxYBuffer, voxZBuffer), + (self.voxXPos, self.voxYPos, self.voxZPos), + voxSteps, + voxOffs): + + buf.bind() gl.glVertexAttribPointer( - self.inPositionPos, - 2, + pos, + 1, gl.GL_FLOAT, gl.GL_FALSE, 0, - positionBuffer + posOffset) - gl.glEnableVertexAttribArray(self.inPositionPos) - arbia.glVertexAttribDivisorARB(self.inPositionPos, 1) - - # The image buffer, which defines - # the colour value at each voxel. - imageBuffer.bind() - gl.glVertexAttribPointer( - self.voxelValuePos, - 1, - gl.GL_UNSIGNED_BYTE, - gl.GL_TRUE, - imageStride, - imageBuffer + imageOffset) - - gl.glEnableVertexAttribArray(self.voxelValuePos) - arbia.glVertexAttribDivisorARB(self.voxelValuePos, 1) - - # Draw all of the triangles! - arbdi.glDrawArraysInstancedARB( - gl.GL_TRIANGLE_STRIP, 0, 4, xdim) - - gl.glDisableVertexAttribArray(self.inVertexPos) - gl.glDisableVertexAttribArray(self.inPositionPos) - gl.glDisableVertexAttribArray(self.voxelValuePos) - gl.glDisable(gl.GL_TEXTURE_1D) + buf + off * 4) + gl.glEnableVertexAttribArray(pos) + arbia.glVertexAttribDivisorARB(pos, step) + + # The geometry buffer, which defines the geometry of a + # single vertex (4 vertices, drawn as a triangle strip) + geomBuffer.bind() + gl.glVertexAttribPointer( + self.inVertexPos, + 2, + gl.GL_FLOAT, + gl.GL_FALSE, + 0, + None) + gl.glEnableVertexAttribArray(self.inVertexPos) + arbia.glVertexAttribDivisorARB(self.inVertexPos, 0) + + + print 'Draw {} voxels from slice {} ({} {} {})'.format( + xdim * ydim, zi, self.xax, self.yax, self.zax) + print "Offsets: {}".format(voxOffs) + print "Steps: {}".format(voxSteps) + + arbdi.glDrawArraysInstancedARB( + gl.GL_TRIANGLE_STRIP, 0, 4, xdim * ydim) + + gl.glDisableVertexAttribArray(self.inVertexPos) + gl.glDisableVertexAttribArray(self.screenPosPos) + gl.glDisableVertexAttribArray(self.voxXPos) + gl.glDisableVertexAttribArray(self.voxYPos) + gl.glDisableVertexAttribArray(self.voxZPos) + gl.glDisable(gl.GL_TEXTURE_1D) + gl.glDisable(gl.GL_TEXTURE_3D) gl.glUseProgram(0)