diff --git a/fsl/data/fslimage.py b/fsl/data/fslimage.py index 76cf9cfe62cb38b3be7c1afd32e21a5355a1b294..5ba3b0054b2c4e0f63c8e1c39372cb938540e669 100644 --- a/fsl/data/fslimage.py +++ b/fsl/data/fslimage.py @@ -15,8 +15,9 @@ import nibabel as nib import matplotlib.cm as mplcm import matplotlib.colors as mplcolors -import fsl.props as props -import fsl.data.imagefile as imagefile +import fsl.props as props +import fsl.data.imagefile as imagefile +import fsl.utils.notifylist as notifylist class Image(object): @@ -31,7 +32,7 @@ class Image(object): """ # The image parameter may be the name of an image file - if isinstance(image, str): + if isinstance(image, basestring): image = nib.load(imagefile.addExt(image)) # Or a numpy array - we wrap it in a nibabel image, @@ -147,7 +148,7 @@ class ImageDisplay(props.HasProperties): self.displayMax = self.dataMax -class ImageList(object): +class ImageList(notifylist.NotifyList): """ Class representing a collection of images to be displayed together. Provides basic list-like functionality, and a listener interface @@ -165,42 +166,8 @@ class ImageList(object): if not isinstance(images, collections.Iterable): raise TypeError('images must be a sequence of images') - if not all(map(lambda img: isinstance(img, Image), images)): - raise TypeError('images must be a sequence of 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): - popped = self._images.pop(index) - self.notify() - return popped - - - def insert(self, index, image): - self._images.insert(index, image) - self.notify() - - - def extend(self, images): - self._images.extend(images) - self.notify() - + def validate(img): + if not isinstance(img, Image): + raise TypeError('images must be a sequence of images') - 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) + notifylist.NotifyList.__init__(self, images, validate) diff --git a/fsl/data/imagefile.py b/fsl/data/imagefile.py index 4f08f9bb84f55139c66d0d9abc312d8a401ec49b..6b7a51f2e09a36dc56e419f548e40466147b543e 100644 --- a/fsl/data/imagefile.py +++ b/fsl/data/imagefile.py @@ -14,11 +14,37 @@ import os.path as op # to any of the functions in this module. _allowedExts = ['.nii', '.img', '.hdr', '.nii.gz', '.img.gz'] +_descriptions = ['NIFTI1 images', + 'ANALYZE75 images', + 'NIFTI1/ANALYZE75 headers', + 'Compressed NIFTI1 images', + 'Compressed ANALYZE75/NIFTI1 images'] + # The default file extension (TODO read this from $FSLOUTPUTTYPE) _defaultExt = '.nii.gz' +def wildcard(allowedExts=None): + """ + """ + + if allowedExts is None: + allowedExts = _allowedExts + descs = _descriptions + else: + descs = allowedExts + + + exts = ['*{}'.format(ext) for ext in allowedExts] + + wcParts = ['|'.join((desc,ext)) for (desc,ext) in zip(descs, exts)] + + print '|'.join(wcParts) + return '|'.join(wcParts) + + + def isSupported(filename, allowedExts=None): """ Returns True if the given file has a supported extension, False diff --git a/fsl/fslview/fslview.py b/fsl/fslview/fslview.py new file mode 100644 index 0000000000000000000000000000000000000000..cc7465dfc11c42f5c22e0740e32beb80dba605e8 --- /dev/null +++ b/fsl/fslview/fslview.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# +# fslview.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import sys + +import wx + +import fsl.fslview.slicecanvas as slicecanvas +import fsl.fslview.orthopanel as orthopanel +import fsl.fslview.imagelistpanel as imagelistpanel + +import fsl.data.fslimage as fslimage + + +class FslViewFrame(wx.Frame): + + def __init__(self, imageList, title=''): + + wx.Frame.__init__(self, None, title=title) + self.imageList = imageList + + self.orthoPanel = orthopanel .OrthoPanel( self, imageList) + self.listPanel = imagelistpanel.ImageListPanel(self, imageList) + + self.sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self.sizer) + + self.sizer.Add(self.orthoPanel, flag=wx.EXPAND, proportion=1) + self.sizer.Add(self.listPanel, flag=wx.EXPAND) + + self.Layout() + + +if __name__ == '__main__': + + app = wx.App() + images = map(fslimage.Image, sys.argv[1:]) + imageList = fslimage.ImageList(images) + frame = FslViewFrame(imageList) + + frame.Show() + app.MainLoop() diff --git a/fsl/fslview/imagelistpanel.py b/fsl/fslview/imagelistpanel.py new file mode 100644 index 0000000000000000000000000000000000000000..4614190f72f370486efaaccc603e568b959a50a0 --- /dev/null +++ b/fsl/fslview/imagelistpanel.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +# +# imagelistpanel.py - A panel which displays an image list, and a 'console' +# allowing the display properties of each image to be changed, and images +# to be added/removed from the list. +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import os + +import wx + +import fsl.data.fslimage as fslimage +import fsl.data.imagefile as imagefile +import fsl.utils.elistbox as elistbox +import fsl.props as props + + +class ImageListPanel(wx.Panel): + """ + """ + + def __init__(self, parent, imageList): + """ + """ + + wx.Panel.__init__(self, parent) + self.imageList = imageList + + imageNames = [img.name for img in imageList] + + # list box containing the list of images + self.listBox = elistbox.EditableListBox( + self, imageNames, imageList, style=elistbox.ELB_REVERSE) + + self.listBox.Bind(elistbox.EVT_ELB_SELECT_EVENT, self._imageSelected) + self.listBox.Bind(elistbox.EVT_ELB_MOVE_EVENT, self._imageMoved) + self.listBox.Bind(elistbox.EVT_ELB_REMOVE_EVENT, self._imageRemoved) + self.listBox.Bind(elistbox.EVT_ELB_ADD_EVENT, self._addImage) + + self.sizer = wx.BoxSizer(wx.HORIZONTAL) + self.SetSizer(self.sizer) + + self.sizer.Add(self.listBox, flag=wx.EXPAND, proportion=1) + + # a panel for each image, containing widgets + # allowing the image display properties to be + # changed + for i,image in enumerate(imageList): + displayPanel = self._makeDisplayPanel(image) + displayPanel.Show(i == 0) + + self.Layout() + + + def _makeDisplayPanel(self, image): + """ + """ + + displayPanel = props.buildGUI(self, image.display) + self.sizer.Add(displayPanel, flag=wx.EXPAND, proportion=2) + image.setAttribute( + 'displayPanel_{}'.format(id(self)), displayPanel) + return displayPanel + + + def _imageMoved(self, ev): + """ + Called when an image name is moved in the ListBox. Reorders the + ImageList to reflect the change. + """ + + self.imageList.move(ev.oldIdx, ev.newIdx) + self.Refresh() + + + def _imageSelected(self, ev): + """ + Called when an image is selected in the ListBox. Displays the + corresponding image display configuration panel. + """ + + for i,image in enumerate(self.imageList): + + displayPanel = image.getAttribute( + 'displayPanel_{}'.format(id(self))) + + displayPanel.Show(i == ev.idx) + self.Layout() + + + def _addImage(self, ev): + + try: lastDir = self._lastDir + except: lastDir = os.getcwd() + + wildcard = imagefile.wildcard() + + dlg = wx.FileDialog(self.GetParent(), + message='Open image file', + defaultDir=lastDir, + wildcard=wildcard, + style=wx.FD_OPEN) + + if dlg.ShowModal() != wx.ID_OK: return + + image = fslimage.Image(dlg.GetPath()) + self.imageList.insert(0, image) + self._makeDisplayPanel(image) + self.listBox.Insert(0, image.name, image) + + + def _imageRemoved(self, ev): + """ + """ + + image = self.imageList.pop(ev.idx) + displayPanel = image.getAttribute('displayPanel_{}'.format(id(self))) + + displayPanel.Destroy() diff --git a/fsl/fslview/slicecanvas.py b/fsl/fslview/slicecanvas.py index 576bcc72359f9bbb472e77bc7cb9abade6e213d1..0fef081d92f70c031cc3b0a2d77aff0fc4783ed7 100644 --- a/fsl/fslview/slicecanvas.py +++ b/fsl/fslview/slicecanvas.py @@ -298,11 +298,6 @@ class SliceCanvas(wxgl.GLCanvas): glImageData = self._initGLImageData(image) - image.setAttribute('glImageData_{}'.format(id(self)), glImageData) - - self._configDisplayListeners(image) - self.updateColourBuffer(image) - self.glReady = True @@ -340,6 +335,10 @@ class SliceCanvas(wxgl.GLCanvas): glImageData = GLImageData( image, imageBuffer, colourBuffer, positionBuffer, geomBuffer) + image.setAttribute('glImageData_{}'.format(id(self)), glImageData) + self._configDisplayListeners(image) + self.updateColourBuffer(image) + return glImageData @@ -350,7 +349,8 @@ class SliceCanvas(wxgl.GLCanvas): image buffer is used instead. """ - imageBuffer = image.getAttribute('glBuffer') + try: imageBuffer = image.getAttribute('glBuffer') + except: imageBuffer = None if imageBuffer is not None: return imageBuffer @@ -518,7 +518,14 @@ class SliceCanvas(wxgl.GLCanvas): image = self.imageList[i] glImageData = image.getAttribute( - 'glImageData_{}'.format(id(self))) + 'glImageData_{}'.format(id(self))) + + # try: + # glImageData = image.getAttribute( + # 'glImageData_{}'.format(id(self))) + # except: + # wx.CallAfter(lambda : self._initGLImageData(image)) + # return imageDisplay = image.display geomBuffer = glImageData.geomBuffer diff --git a/fsl/utils/notifylist.py b/fsl/utils/notifylist.py new file mode 100644 index 0000000000000000000000000000000000000000..0359f1c6d4b7c99a6dc6f7abfc8a443dabcda890 --- /dev/null +++ b/fsl/utils/notifylist.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# +# notifylist.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import collections +import logging +import unittest + +log = logging.getLogger(__name__) + +class NotifyList(object): + + def __init__(self, items=None, validateFunc=None): + + if items is None: items = [] + if validateFunc is None: validateFunc = lambda v: v + + if not isinstance(items, collections.Iterable): + raise TypeError('items must be a sequence') + + map(validateFunc, items) + + self._validate = validateFunc + self._items = items + self._listeners = [] + + + def __len__ (self): return self._items.__len__() + def __getitem__ (self, key): return self._items.__getitem__(key) + def __iter__ (self): return self._items.__iter__() + def __contains__(self, item): return self._items.__contains__(item) + def __eq__ (self, other): return self._items.__eq__(other) + def __str__ (self): return self._items.__str__() + def __repr__ (self): return self._items.__repr__() + + + def append(self, item): + self._validate(item) + self._items.append(item) + self._notify() + + + def pop(self, index=-1): + popped = self._items.pop(index) + self._notify() + return popped + + + def insert(self, index, item): + self._validate(item) + self._items.insert(index, item) + self._notify() + + + def extend(self, items): + map(self._validate, items) + self._items.extend(items) + self._notify() + + + def move(self, from_, to): + """ + Move the item from 'from_' to 'to'. + """ + + item = self._items.pop(from_) + self._items.insert(to, item) + self._notify() + + + def addListener (self, listener): self._listeners.append(listener) + def removeListener(self, listener): self._listeners.remove(listener) + def _notify (self): + for listener in self._listeners: + try: + listener(self) + except e: + log.debug('Listener raised exception: {}'.format(e.message)) + + +class TestNotifyList(unittest.TestCase): + + def setUp(self): + self.listlen = 5 + self.thelist = NotifyList(range(self.listlen)) + + def test_move(self): + + for i in range(self.listlen): + for j in range(self.listlen): + + self.setUp() + self.thelist.move(i, j) + + demo = range(self.listlen) + + val = demo.pop(i) + demo.insert(j, val) + + print '{} -> {}: {} <-> {}'.format(i, j, self.thelist, demo) + + self.assertEqual(self.thelist, demo)