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

Atlas panel is slightly functional. User can choose which atlases to

display information for, and can show/hide label masks for individual
regions

Fixes to GLVolume/GLMask colour map creation in situations where the
image data only has a single value.
parent ebb2067b
No related branches found
No related tags found
No related merge requests found
......@@ -10,6 +10,8 @@
Instances of the :class:`Atlas` class is a
MNI152
<atlas>
<header>
......@@ -75,7 +77,18 @@ def listAtlases():
atlasFiles = glob.glob(op.join(ATLAS_DIR, '*.xml'))
atlasDescs = map(AtlasDescription, atlasFiles)
return {d.key: d for d in atlasDescs}
return {d.atlasID: d for d in atlasDescs}
def loadAtlas(atlasDesc, loadSummary=False):
if loadSummary or atlasDesc.atlasType == 'label':
return LabelAtlas(atlasDesc)
if atlasDesc.atlasType == 'probabilistic':
return ProbabilisticAtlas(atlasDesc)
else:
raise ValueError('Unknown atlas type: {}'.format(atlasDesc.atlasType))
class AtlasDescription(object):
......@@ -92,9 +105,13 @@ class AtlasDescription(object):
header = root.find('header')
data = root.find('data')
self.key = op.splitext(op.basename(filename))[0]
self.atlasID = op.splitext(op.basename(filename))[0]
self.name = header.find('name').text
self.atlasType = header.find('type').text
self.atlasType = header.find('type').text.lower()
# Spelling error in some of the atlas.xml files.
if self.atlasType == 'probabalistic':
self.atlasType = 'probabilistic'
images = header.findall('images')
self.images = []
......@@ -141,11 +158,12 @@ class AtlasDescription(object):
# Load the appropriate transformation matrix
# and transform all those voxel coordinates
xform = fslimage.Image(self.images[0], loadData=False).voxToWorldMat
coords = transform.transform(coords, xform)
coords = transform.transform(coords, xform.T)
# Update the coordinates
# in our label objects
for i, label in enumerate(self.labels):
label.x, label.y, label.z = coords[i]
......@@ -164,34 +182,39 @@ class Atlas(fslimage.Image):
if imgRes < minImageRes:
minImageRes = imgRes
imageIdx = i
if isLabel: imageFile = atlasDesc.summaryImages[imageIdx]
else: imageFile = atlasDesc.images[ imageIdx]
fslimage.Image.__init__(self, imageFile)
self.desc = atlasDesc
class LabelAtlas(fslimage.Image):
class LabelAtlas(Atlas):
def __init__(self, atlasDesc):
Atlas.__init__(self, atlasDesc, isLabel=True)
def label(self, voxelLoc):
def label(self, worldLoc):
voxelLoc = transform.transform([worldLoc], self.worldToVoxMat.T)[0]
val = self.data[voxelLoc[0], voxelLoc[1], voxelLoc[2]]
if self.atlasDesc.atlasType == 'Label':
return self.atlasDesc.label[val]
if self.desc.atlasType == 'label':
return val
elif self.atlasDesc.atlasType == 'Probabilistic':
return self.atlasDesc.label[val - 1]
elif self.desc.atlasType == 'probabilistic':
return val - 1
class ProbabilisticAtlas(fslimage.Image):
class ProbabilisticAtlas(Atlas):
def __init__(self, atlasDesc):
Atlas.__init__(self, atlasDesc, isLabel=False)
def proportions(self, voxelLoc):
props = self.data[voxelLoc[0], voxelLoc[1], voxelLoc[2], :]
return zip(self.atlasDesc.labels, props)
def proportions(self, worldLoc):
voxelLoc = transform.transform([worldLoc], self.worldToVoxMat.T)[0]
return self.data[voxelLoc[0], voxelLoc[1], voxelLoc[2], :]
......@@ -252,7 +252,6 @@ class Image(props.HasProperties):
def getXFormCode(self):
"""This method returns the code contained in the NIFTI1 header,
indicating the space to which the (transformed) image is oriented.
"""
sform_code = self.nibImage.get_header()['sform_code']
......@@ -272,13 +271,13 @@ class Image(props.HasProperties):
This method returns one of the following values, indicating the
direction in which coordinates along the specified axis increase:
- :attr:`~fsl.data.image.ORIENT_L2R`: Left to right
- :attr:`~fsl.data.image.ORIENT_R2L`: Right to left
- :attr:`~fsl.data.image.ORIENT_A2P`: Anterior to posterior
- :attr:`~fsl.data.image.ORIENT_P2A`: Posterior to anterior
- :attr:`~fsl.data.image.ORIENT_I2S`: Inferior to superior
- :attr:`~fsl.data.image.ORIENT_S2I`: Superior to inferior
- :attr:`~fsl.data.image.ORIENT_UNKNOWN`: Orientation is unknown
- :attr:`~fsl.data.constants.ORIENT_L2R`: Left to right
- :attr:`~fsl.data.constants.ORIENT_R2L`: Right to left
- :attr:`~fsl.data.constants.ORIENT_A2P`: Anterior to posterior
- :attr:`~fsl.data.constants.ORIENT_P2A`: Posterior to anterior
- :attr:`~fsl.data.constants.ORIENT_I2S`: Inferior to superior
- :attr:`~fsl.data.constants.ORIENT_S2I`: Superior to inferior
- :attr:`~fsl.data.constants.ORIENT_UNKNOWN`: Orientation is unknown
The returned value is dictated by the XForm code contained in the
image file header (see the :meth:`getXFormCode` method). Basically,
......@@ -385,6 +384,16 @@ class ImageList(props.HasProperties):
return iio.addImages(self, fromDir, addToEnd)
def find(self, name):
"""Returns the first image with the given name, or ``None`` if
there is no image with said name.
"""
for image in self.images:
if image.name == name:
return image
return None
# Wrappers around the images list property, allowing this
# ImageList object to be used as if it is actually a list.
def __len__( self): return self.images.__len__()
......@@ -400,6 +409,7 @@ class ImageList(props.HasProperties):
def extend( self, iterable): return self.images.extend(iterable)
def pop( self, index=-1): return self.images.pop(index)
def move( self, from_, to): return self.images.move(from_, to)
def remove( self, item): return self.images.remove(item)
def insert( self, index, item): return self.images.insert(index,
item)
def insertAll( self, index, items): return self.images.insertAll(index,
......
......@@ -29,7 +29,9 @@ messages = TypeDict({
'actions.loadcolourmap.invalidname' : 'Please use only letters, '
'numbers, and underscores.',
'actions.loadcolourmap.installerror' : 'An error occurred while '
'installing the colour map',
'installing the colour map',
'atlaspanel.unknownLocation' : 'Unknown location'
})
......@@ -59,6 +61,7 @@ actions = TypeDict({
'CanvasPanel.toggleImageList' : 'Show/hide image list',
'CanvasPanel.toggleDisplayProperties' : 'Show/hide display properties',
'CanvasPanel.toggleLocationPanel' : 'Show/hide location panel',
'CanvasPanel.toggleAtlasPanel' : 'Show/hide atlas panel',
'CanvasPanel.toggleCanvasProperties' : 'Show/hide canvas properties',
......
......@@ -7,15 +7,18 @@
import logging
import wx
import wx
import wx.html as wxhtml
import numpy as np
import pwidgets.elistbox as elistbox
import pwidgets.notebook as notebook
import fsl.utils.transform as transform
import fsl.data.image as fslimage
import fsl.data.atlases as atlases
import fsl.data.strings as strings
import fsl.fslview.widgets.swappanel as swappanel
import fsl.data.constants as constants
import fsl.fslview.panel as fslpanel
......@@ -24,25 +27,29 @@ log = logging.getLogger(__name__)
class AtlasListWidget(wx.Panel):
def __init__(self, parent, atlasDesc, imageList, displayCtx, listBox):
def __init__(self, parent, atlasDesc, atlasPanel):
wx.Panel.__init__(self, parent)
self.atlasDesc = atlasDesc
self.enableBox = wx.CheckBox(self)
self.atlasDesc = atlasDesc
self.atlasPanel = atlasPanel
self.enableBox = wx.CheckBox(self)
self.sizer = wx.BoxSizer(wx.HORIZONTAL)
self.sizer.Add(self.enableBox, flag=wx.EXPAND)
self.enableBox.Bind(wx.EVT_CHECKBOX, self.onEnable)
class AtlasPanel(fslpanel.FSLViewPanel):
def onEnable(self, ev):
if self.enableBox.GetValue():
self.atlasPanel.enableAtlasInfo(self.atlasDesc.atlasID)
else:
self.atlasPanel.disableAtlasInfo(self.atlasDesc.atlasID)
# actions
def addSummaryOverlay( self): pass
def addProbabilisticOverlay(self): pass
def addLabelOverlay( self): pass
class AtlasPanel(fslpanel.FSLViewPanel):
def __init__(self, parent, imageList, displayCtx):
......@@ -58,13 +65,12 @@ class AtlasPanel(fslpanel.FSLViewPanel):
# Info panel, containing atlas-based regional
# proportions/labels for the current location
self.infoPanel = wx.TextCtrl(self.notebook, style=(wx.TE_MULTILINE |
wx.TE_READONLY))
self.infoPanel = wxhtml.HtmlWindow(self.notebook)
# Atlas list, containing a list of atlases
# that the user can choose from
self.atlasList = elistbox.EditableListBox(
self.atlasListPanel,
self.notebook,
style=(elistbox.ELB_NO_ADD |
elistbox.ELB_NO_REMOVE |
elistbox.ELB_NO_MOVE))
......@@ -73,9 +79,163 @@ class AtlasPanel(fslpanel.FSLViewPanel):
# allowing the user to add/remove overlays
self.overlayPanel = wx.Panel(self.notebook)
self.notebook.Add(self.infoPanel,
strings.labels['AtlasPanel.infoPanel'])
self.notebook.Add(self.atlasListPanel,
strings.labels['AtlasPanel.atlasListPanel'])
self.notebook.Add(self.overlayPanel,
strings.labels['AtlasPanel.overlayPanel'])
self.notebook.AddPage(self.infoPanel,
strings.labels['AtlasPanel.infoPanel'])
self.notebook.AddPage(self.atlasList,
strings.labels['AtlasPanel.atlasListPanel'])
self.notebook.AddPage(self.overlayPanel,
strings.labels['AtlasPanel.overlayPanel'])
# The info panel contains clickable links
# for the currently displayed regions -
# when a link is clicked, the location
# is centred at the corresponding region
self.infoPanel.Bind(wxhtml.EVT_HTML_LINK_CLICKED,
self._infoPanelLinkClicked)
# Set up the list of atlases to choose from
self.atlasDescs = atlases.listAtlases()
self.enabledAtlases = {}
listItems = sorted(self.atlasDescs.items(), key=lambda (a, d): d.name)
for i, (atlasID, desc) in enumerate(listItems):
self.atlasList.Append(desc.name, atlasID)
widget = AtlasListWidget(self.atlasList, desc, self)
self.atlasList.SetItemWidget(i, widget)
displayCtx.addListener('location', self._name, self._locationChanged)
def _infoPanelLinkClicked(self, ev):
atlasID, labelIndex = ev.GetLinkInfo().GetHref().split()
labelIndex = int(labelIndex)
atlas = self.enabledAtlases[atlasID]
label = atlas.desc.labels[labelIndex]
log.debug('{}/{} clicked'.format(atlasID, label.name))
if isinstance(atlas, atlases.ProbabilisticAtlas):
pass
elif isinstance(atlas, atlases.LabelAtlas):
self.toggleLabelOverlay(atlasID, labelIndex)
def enableAtlasInfo(self, atlasID):
desc = self.atlasDescs[atlasID]
atlasImage = atlases.loadAtlas(desc)
self.enabledAtlases[atlasID] = atlasImage
self._locationChanged()
def disableAtlasInfo(self, atlasID):
self.enabledAtlases.pop(atlasID, None)
self._locationChanged()
def toggleSummaryOverlay(self, atlasID):
pass
def toggleProbabilisticOverlay(self, atlasID, labelIndex):
pass
def toggleLabelOverlay(self, atlasID, labelIndex):
desc = self.atlasDescs[atlasID]
overlayName = '{}/{}'.format(atlasID, desc.labels[labelIndex].name)
overlay = self._imageList.find(overlayName)
if overlay is not None:
self._imageList.remove(overlay)
log.debug('Removing overlay {}'.format(overlayName))
else:
atlas = self.enabledAtlases.get(atlasID, None)
if atlas is None:
atlas = atlases.loadAtlas(self.atlasDescs[atlasID], True)
if desc.atlasType == 'probabilistic': labelVal = labelIndex + 1
elif desc.atlasType == 'label': labelVal = labelIndex
mask = np.zeros(atlas.shape, dtype=np.uint8)
mask[atlas.data == labelIndex] = labelVal
overlay = fslimage.Image(
mask,
atlas.voxToWorldMat,
name=overlayName)
overlay.imageType = 'mask'
log.debug('Adding overlay {}'.format(overlayName))
self._imageList.append(overlay)
display = self._displayCtx.getDisplayProperties(overlay)
display.getDisplayOpts().colour = np.random.random(3)
def _locationChanged(self, *a):
image = self._displayCtx.getSelectedImage()
display = self._displayCtx.getDisplayProperties(image)
loc = self._displayCtx.location
text = self.infoPanel
loc = transform.transform([loc], display.displayToWorldMat)[0]
if image.getXFormCode() != constants.NIFTI_XFORM_MNI_152:
text.SetPage(strings.messages['atlaspanel.unknownLocation'])
return
lines = []
labelTemplate = """{}
(<a href="{} {}">Show/Hide</a>)
"""
probTemplate = """
{:0.2f}% {}
(<a href="{} {}">Show/Hide</a>)
"""
for atlasID, atlas in self.enabledAtlases.items():
lines.append('<b>{}</b>'.format(atlas.desc.name))
if isinstance(atlas, atlases.ProbabilisticAtlas):
proportions = atlas.proportions(loc)
for label, prop in zip(atlas.desc.labels, proportions):
if prop == 0.0:
continue
lines.append(probTemplate.format(prop,
label.name,
atlasID,
label.index,
atlasID,
label.index))
elif isinstance(atlas, atlases.LabelAtlas):
labelVal = atlas.label(loc)
label = atlas.desc.labels[labelVal]
lines.append(labelTemplate.format(label.name,
atlasID,
label.index,
atlasID,
label.index))
text.SetPage('<br>'.join(lines))
......@@ -45,5 +45,5 @@ class MaskOpts(fsldisplay.DisplayOpts):
self.threshold.setMin( 0, self.dataMin - 0.5 * dRangeLen)
self.threshold.setMax( 0, self.dataMax + 0.5 * dRangeLen)
self.threshold.setRange(0, self.dataMin, self.dataMax)
self.threshold.setRange(0, 0.1, self.dataMax + 0.1)
self.setConstraint('threshold', 'minDistance', dMinDistance)
......@@ -52,7 +52,7 @@ class GLMask(glvolume.GLVolume):
self.setAxes(self.xax, self.yax)
def colourUpdate(*a):
self.refreshColourTexture(self.colourResolution)
self.refreshColourTexture()
lnrName = '{}_{}'.format(type(self).__name__, id(self))
......@@ -109,17 +109,20 @@ class GLMask(glvolume.GLVolume):
# or above the current display range will be mapped
# to texture coordinate values less than 0.0 or greater
# than 1.0 respectively.
if imax == imin: scale = 1
else: scale = imax - imin
cmapXform = np.identity(4, dtype=np.float32)
cmapXform[0, 0] = 1.0 / (imax - imin)
cmapXform[0, 0] = 1.0 / scale
cmapXform[3, 0] = -imin * cmapXform[0, 0]
self.colourMapXform = cmapXform
if opts.invert:
colourmap = np.tile([[0.0, 0.0, 0.0, 0.0]], (2, 1))
colourmap = np.tile([[0.0, 0.0, 0.0, 0.0]], (16, 1))
border = np.array(opts.colour, dtype=np.float32)
else:
colourmap = np.tile([[opts.colour]], (2, 1))
colourmap = np.tile([[opts.colour]], (16, 1))
border = np.array([0.0, 0.0, 0.0, 0.0], dtype=np.float32)
colourmap = np.floor(colourmap * 255)
......@@ -145,7 +148,7 @@ class GLMask(glvolume.GLVolume):
gl.glTexImage1D(gl.GL_TEXTURE_1D,
0,
gl.GL_RGBA8,
2,
16,
0,
gl.GL_RGBA,
gl.GL_UNSIGNED_BYTE,
......
......@@ -249,8 +249,11 @@ class GLVolume(globject.GLImageObject):
# or above the current display range will be mapped
# to texture coordinate values less than 0.0 or greater
# than 1.0 respectively.
if imax == imin: scale = 1
else: scale = imax - imin
cmapXform = np.identity(4, dtype=np.float32)
cmapXform[0, 0] = 1.0 / (imax - imin)
cmapXform[0, 0] = 1.0 / scale
cmapXform[3, 0] = -imin * cmapXform[0, 0]
self.colourMapXform = cmapXform
......
......@@ -103,6 +103,7 @@ CanvasPanelActionLayout = props.HGroup(
actionButton('toggleImageList', CanvasPanel),
actionButton('toggleDisplayProperties', CanvasPanel),
actionButton('toggleLocationPanel', CanvasPanel),
actionButton('toggleAtlasPanel', CanvasPanel),
actionButton('toggleCanvasProperties', CanvasPanel)),
wrap=True,
showLabels=False)
......
......@@ -30,6 +30,7 @@ import fsl.fslview.displaycontext as displayctx
import fsl.fslview.controls.imagelistpanel as imagelistpanel
import fsl.fslview.controls.imagedisplaypanel as imagedisplaypanel
import fsl.fslview.controls.locationpanel as locationpanel
import fsl.fslview.controls.atlaspanel as atlaspanel
import fsl.fslview.widgets.togglepanel as togp
import colourbarpanel
......@@ -183,6 +184,7 @@ class CanvasPanel(fslpanel.FSLViewPanel):
'screenshot' : self.screenshot,
'toggleColourBar' : self.toggleColourBar,
'toggleImageList' : self.toggleImageList,
'toggleAtlasPanel' : self.toggleAtlasPanel,
'toggleDisplayProperties' : self.toggleDisplayProperties,
'toggleLocationPanel' : self.toggleLocationPanel,
'toggleCanvasProperties' : self.toggleCanvasProperties}
......@@ -245,6 +247,9 @@ class CanvasPanel(fslpanel.FSLViewPanel):
self.__listLocContainer, imageList, displayCtx)
self.__locationPanel = locationpanel.LocationPanel(
self.__listLocContainer, imageList, displayCtx)
self.__atlasPanel = atlaspanel.AtlasPanel(
self.__listLocContainer, imageList, displayCtx)
self.__displayPropsPanel = imagedisplaypanel.ImageDisplayPanel(
......@@ -259,6 +264,9 @@ class CanvasPanel(fslpanel.FSLViewPanel):
self.__listLocSizer.Add(self.__locationPanel,
flag=wx.EXPAND,
proportion=1)
self.__listLocSizer.Add(self.__atlasPanel,
flag=wx.EXPAND,
proportion=1)
self.__dispSetSizer = wx.BoxSizer(wx.HORIZONTAL)
self.__dispSetContainer.SetSizer(self.__dispSetSizer)
......@@ -289,6 +297,7 @@ class CanvasPanel(fslpanel.FSLViewPanel):
self.__locationPanel .Show(False)
self.__canvasPropsPanel .Show(False)
self.__displayPropsPanel.Show(False)
self.__atlasPanel .Show(False)
# Use a different listener name so that subclasses
# can register on the same properties with self._name
......@@ -398,6 +407,10 @@ class CanvasPanel(fslpanel.FSLViewPanel):
def toggleLocationPanel(self, *a):
self.__locationPanel.Show(not self.__locationPanel.IsShown())
self.__layout()
def toggleAtlasPanel(self, *a):
self.__atlasPanel.Show(not self.__atlasPanel.IsShown())
self.__layout()
def toggleDisplayProperties(self, *a):
self.__displayPropsPanel.Show(not self.__displayPropsPanel.IsShown())
......
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