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

Support for multiple images, from a predefined list. Am now going to try and...

Support for multiple images, from a predefined list. Am now going to try and add support for adding/removing images at runtime.
parent 1a54c6c2
No related branches found
No related tags found
No related merge requests found
......@@ -6,6 +6,8 @@
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
import os.path as op
import numpy as np
import nibabel as nib
import matplotlib.cm as mplcm
......@@ -39,7 +41,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()
self.name = op.basename(image.get_filename())
xdim,ydim,zdim = self.nibImage.get_shape()
xlen,ylen,zlen = self.nibImage.get_header().get_zooms()
......@@ -52,6 +54,10 @@ class Image(object):
self.ylen = ylen
self.zlen = zlen
# ImageDisplay instance used to describe
# how this image is to be displayed
self.display = ImageDisplay(self)
# 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
......@@ -61,7 +67,9 @@ class Image(object):
class ImageDisplay(props.HasProperties):
"""
A class which describes how an image should be displayed.
A class which describes how an image should be displayed. There should
be no need to manually instantiate ImageDisplay objects - one is created
for each Image object, and is accessed via the Image.display attribute.
"""
def updateColourMap(self, newVal):
......@@ -81,7 +89,7 @@ class ImageDisplay(props.HasProperties):
cmap.set_over( cmap(1.0), alpha=1.0)
# The display properties of an image
enabled = props.Boolean()
enabled = props.Boolean(default=True)
alpha = props.Double(minval=0.0, maxval=1.0, default=1.0)
displayMin = props.Double()
displayMax = props.Double()
......@@ -125,13 +133,51 @@ class ImageDisplay(props.HasProperties):
class ImageList(object):
"""
Class representing a collection of images to be displayed together.
Provides basic list-like functionality, and a listener interface
allowing callback functions to be notified when the image collection
changes (e.g. an image is added or removed).
"""
def __init__(self, images=None, displays=None):
if images is None: images = []
if displays is None: displays = []
def __init__(self, images=None):
"""
"""
if images is None: images = []
self._images = images
self._listeners = []
def __len__ (self): return self._images.__len__()
def __getitem__ (self, key): return self._images.__getitem__(key)
def __iter__ (self): return self._images.__iter__()
def __contains__(self, image): return self._images.__contains__(image)
def append(self, image):
self._images.append(image)
self.notify()
def pop(self, index=-1):
self._images.pop(index)
self.notify()
def insert(self, index, image):
self._images.insert(index, image)
self.notify()
def extend(self, images):
self._images.extend(images)
self.notify()
self.images = images
self.displays = displays
def addListener (self, listener): self._listeners.append(listener)
def removeListener(self, listener): self._listeners.remove(listener)
def notify (self):
for listener in self._listeners:
listener(self)
......@@ -315,6 +315,7 @@ def _Number(parent, hasProps, propObj, propVal):
params['min'] = minval
params['max'] = maxval
params['initial'] = value
params['value'] = '{}'.format(value)
# The minval and maxval attributes have not both
# been set, so we create a spinbox instead of a slider.
......
#!/usr/bin/env python
#
# imgshow.py - A wx/OpenGL widget for displaying and interacting with a
# collection of 3D image. Displays three canvases, each of which shows
# collection of 3D images. Displays three canvases, each of which shows
# a slice of the images along each dimension.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
......@@ -9,14 +9,15 @@
import sys
# import logging
# logging.basicConfig(
# format='%(levelname)8s '\
# '%(filename)20s '\
# '%(lineno)4d: '\
# '%(funcName)s - '\
# '%(message)s',
# level=logging.DEBUG)
if False:
import logging
logging.basicConfig(
format='%(levelname)8s '\
'%(filename)20s '\
'%(lineno)4d: '\
'%(funcName)s - '\
'%(message)s',
level=logging.DEBUG)
import wx
import wx.lib.newevent as wxevent
......@@ -32,15 +33,22 @@ class ImageView(wx.Panel):
def __init__(self, parent, imageList, *args, **kwargs):
"""
Creates three SliceCanvas objects, each displaying a
different axis of the given 3D numpy image.
different axis of the given image list.
"""
if isinstance(imageList, fslimage.Image):
imageList = fslimage.ImageList(imageList)
if not isinstance(imageList, fslimage.ImageList):
raise TypeError(
'imageList must be a fsl.data.fslimage.ImageList instance')
self.imageList = imageList
wx.Panel.__init__(self, parent, *args, **kwargs)
self.SetMinSize((300,100))
self.shape = imageList.images[0].data.shape
self.shape = imageList[0].data.shape
self.canvasPanel = wx.Panel(self)
......@@ -51,12 +59,12 @@ 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)):
for image in imageList:
controlPanel = props.buildGUI(self.controlPanel, self.imageList.displays[i])
self.controlPanel.AddPage(controlPanel, '{}'.format(i))
displayProps = props.buildGUI(self.controlPanel, image.display)
self.controlPanel.AddPage(displayProps, image.name)
self.mainSizer = wx.BoxSizer(wx.VERTICAL)
self.canvasSizer = wx.BoxSizer(wx.HORIZONTAL)
......@@ -178,8 +186,7 @@ class ImageFrame(wx.Frame):
def __init__(self, parent, imageList, title=None):
wx.Frame.__init__(self, parent, title=title)
self.imageList = imageList
self.panel = ImageView(self, imageList)
self.panel = ImageView(self, imageList)
self.Layout()
......@@ -191,8 +198,7 @@ if __name__ == '__main__':
app = wx.App()
images = map(fslimage.Image, sys.argv[1:])
displays = map(fslimage.ImageDisplay, images)
imageList = fslimage.ImageList(images, displays)
imageList = fslimage.ImageList(images)
frame = ImageFrame(
None,
......
......@@ -211,9 +211,20 @@ class SliceCanvas(wxgl.GLCanvas):
wxgl.GLCanvas.__init__(self, parent, **kwargs)
# Use the provided shared GL
# context, or create a new one
if context is None: self.context = wxgl.GLContext(self)
else: self.context = context
# If a single image has been provided,
# make it into a list of length 1
if isinstance(imageList, fslimage.Image):
imageList = fslimage.ImageList(imageList)
if not isinstance(imageList, fslimage.ImageList):
raise TypeError(
'imageList must be a fsl.data.fslimage.ImageList instance')
# TODO Currently, the displayed x/horizontal and
# y/vertical axes are defined by their order in
# the image. Allow the caller to specify which
......@@ -227,7 +238,7 @@ class SliceCanvas(wxgl.GLCanvas):
self.zax = zax
# Currently all images must be of the same dimensions
ref = self.imageList.images[0]
ref = self.imageList[0]
self.xdim = ref.data.shape[self.xax]
self.ydim = ref.data.shape[self.yax]
......@@ -248,55 +259,19 @@ class SliceCanvas(wxgl.GLCanvas):
self._colourResolution = 256
# This flag is set by the _initGLData method when it
# These fields are set by the _initGLData method when it
# has finished initialising the OpenGL data buffers
self.glReady = False
self.glReady = False
self.glImageData = None
self.Bind(wx.EVT_PAINT, self.draw)
for i in range(len(self.imageList.images)):
self._configDisplayListeners(i)
def _configDisplayListeners(self, idx):
"""
"""
image = self.imageList.images [idx]
display = self.imageList.displays[idx]
# Add a bunch of listeners to the image display
# object for each image, so we can update the
# view when image display properties are changed.
def refreshNeeded(*a):
self.Refresh()
def colourUpdateNeeded(*a):
self.updateColourBuffer(idx)
self.Refresh()
lnrName = 'SliceCanvas_{{}}_{}'.format(id(self))
display.addListener(
'alpha', lnrName.format('alpha'), refreshNeeded)
display.addListener(
'displayMin', lnrName.format('displayMin'), colourUpdateNeeded)
display.addListener(
'displayMax', lnrName.format('displayMax'), colourUpdateNeeded)
display.addListener(
'rangeClip', lnrName.format('rangeClip'), colourUpdateNeeded)
display.addListener(
'cmap', lnrName.format('cmap'), colourUpdateNeeded)
def _initGLData(self):
"""
Initialises the GL buffers which are copied to the GPU,
and used to render the voxel data.
and used to render the voxel data. This method is only
called once, on the first draw.
"""
# A bit hacky. We can only set the GL context (and create
......@@ -322,54 +297,65 @@ class SliceCanvas(wxgl.GLCanvas):
self.alphaPos = gl.glGetUniformLocation(self.shaders, 'alpha')
self.colourMapPos = gl.glGetUniformLocation(self.shaders, 'colourMap')
self.imageData = []
self.glImageData = []
for idx,image in enumerate(self.imageList.images):
for idx in range(len(self.imageList)):
self.glImageData.append(self._initGLImageData(idx))
self._configDisplayListeners(idx)
self.updateColourBuffer(idx)
# Data stored in the geometry buffer. Defines
# the geometry of a single voxel, rendered as
# a triangle strip.
geomData = np.array([0, 0,
1, 0,
0, 1,
1, 1], dtype=np.uint8)
self.glReady = True
# Data stored in the position buffer. Defines
# the location of every voxel in a single slice.
positionData = np.zeros((self.xdim*self.ydim, 2), dtype=np.uint16)
yidxs,xidxs = np.meshgrid(np.arange(self.ydim),
np.arange(self.xdim),
indexing='ij')
positionData[:,0] = xidxs.ravel()
positionData[:,1] = yidxs.ravel()
geomBuffer = vbo.VBO(geomData, gl.GL_STATIC_DRAW)
positionBuffer = vbo.VBO(positionData, gl.GL_STATIC_DRAW)
def _initGLImageData(self, idx):
"""
"""
# The image buffer, containing the image data itself
imageBuffer = self._initImageBuffer(image)
image = self.imageList[idx]
# Data stored in the geometry buffer. Defines
# the geometry of a single voxel, rendered as
# a triangle strip.
geomData = np.array([0, 0,
1, 0,
0, 1,
1, 1], dtype=np.uint8)
# The colour buffer, containing a map of
# colours (stored on the GPU as a 1D texture)
colourBuffer = gl.glGenTextures(1)
# Data stored in the position buffer. Defines
# the location of every voxel in a single slice.
positionData = np.zeros((self.xdim*self.ydim, 2), dtype=np.uint16)
yidxs,xidxs = np.meshgrid(np.arange(self.ydim),
np.arange(self.xdim),
indexing='ij')
positionData[:,0] = xidxs.ravel()
positionData[:,1] = yidxs.ravel()
imageData = GLImageData(
image, imageBuffer, colourBuffer, positionBuffer, geomBuffer)
geomBuffer = vbo.VBO(geomData, gl.GL_STATIC_DRAW)
positionBuffer = vbo.VBO(positionData, gl.GL_STATIC_DRAW)
self.imageData.append(imageData)
# The image buffer, containing the image data itself
imageBuffer = self._initImageBuffer(idx)
self.updateColourBuffer(idx)
# The colour buffer, containing a map of
# colours (stored on the GPU as a 1D texture)
colourBuffer = gl.glGenTextures(1)
glImageData = GLImageData(
image, imageBuffer, colourBuffer, positionBuffer, geomBuffer)
self.glReady = True
return glImageData
def _initImageBuffer(self, image):
def _initImageBuffer(self, idx):
"""
Initialises the buffer used to store the image data. If a 'master'
canvas was set when this SliceCanvas object was constructed, its
image buffer is used instead.
"""
image = self.imageList[idx]
# If a master canvas was passed to the
# constructor, let's share its image data.
if image.glBuffer is not None:
......@@ -394,15 +380,52 @@ class SliceCanvas(wxgl.GLCanvas):
return imageBuffer
def _configDisplayListeners(self, idx):
"""
"""
image = self.imageList[idx]
display = image.display
# Add a bunch of listeners to the image display
# object for each image, so we can update the
# view when image display properties are changed.
def refreshNeeded(*a):
self.Refresh()
def colourUpdateNeeded(*a):
self.updateColourBuffer(idx)
self.Refresh()
lnrName = 'SliceCanvas_{{}}_{}'.format(id(self))
display.addListener(
'alpha', lnrName.format('alpha'), refreshNeeded)
display.addListener(
'enabled', lnrName.format('enabled'), refreshNeeded)
display.addListener(
'displayMin', lnrName.format('displayMin'), colourUpdateNeeded)
display.addListener(
'displayMax', lnrName.format('displayMax'), colourUpdateNeeded)
display.addListener(
'rangeClip', lnrName.format('rangeClip'), colourUpdateNeeded)
display.addListener(
'cmap', lnrName.format('cmap'), colourUpdateNeeded)
def updateColourBuffer(self, idx):
"""
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.
Regenerates the colour buffer used to colour a slice of the
specified image. After calling this method, you will need to
call Refresh() for the change to take effect.
"""
iDisplay = self.imageList.displays[idx]
colourBuffer = self.imageData[idx].colourBuffer
iDisplay = self.imageList [idx].display
colourBuffer = self.glImageData[idx].colourBuffer
# Here we are creating a range of values to be passed
# to the matplotlib.colors.Colormap instance of the
......@@ -496,18 +519,21 @@ class SliceCanvas(wxgl.GLCanvas):
gl.glEnable(gl.GL_BLEND)
gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
for i in range(len(self.imageData)):
for i in range(len(self.glImageData)):
image = self.imageList[i]
imageDisplay = image.display
geomBuffer = self.glImageData[i].geomBuffer
imageBuffer = self.glImageData[i].imageBuffer
positionBuffer = self.glImageData[i].positionBuffer
colourBuffer = self.glImageData[i].colourBuffer
image = self.imageList.images[i]
imageDisplay = self.imageList.displays[i]
geomBuffer = self.imageData[i].geomBuffer
imageBuffer = self.imageData[i].imageBuffer
positionBuffer = self.imageData[i].positionBuffer
colourBuffer = self.imageData[i].colourBuffer
if not imageDisplay.enabled:
continue
# Set up the colour buffer
gl.glEnable(gl.GL_TEXTURE_1D)
gl.glActiveTexture(gl.GL_TEXTURE0)
gl.glActiveTexture(gl.GL_TEXTURE0)
gl.glBindTexture(gl.GL_TEXTURE_1D, colourBuffer)
gl.glUniform1i(self.colourMapPos, 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