diff --git a/fsl/fsleyes/controls/clusterpanel.py b/fsl/fsleyes/controls/clusterpanel.py index 45fc578b41f70dcda1fb112e24af880495696fcb..01cc671e8d4053c376f5329f6e1755912daef6aa 100644 --- a/fsl/fsleyes/controls/clusterpanel.py +++ b/fsl/fsleyes/controls/clusterpanel.py @@ -8,15 +8,17 @@ panel for viewing cluster results from a FEAT analysis. """ -import logging -import wx +import logging +import wx -import pwidgets.widgetgrid as widgetgrid +import pwidgets.widgetgrid as widgetgrid -import fsl.fsleyes.panel as fslpanel -import fsl.utils.dialog as fsldlg -import fsl.data.strings as strings -import fsl.data.featimage as featimage +import fsl.fsleyes.panel as fslpanel +import fsl.utils.dialog as fsldlg +import fsl.data.strings as strings +import fsl.data.image as fslimage +import fsl.data.featimage as featimage +import fsl.data.featresults as featresults log = logging.getLogger(__name__) @@ -64,18 +66,29 @@ class ClusterPanel(fslpanel.FSLEyesPanel): self.__addClustMask = wx.Button( self) self.__statSelect = wx.Choice( self) + # The featImages dictionary is a mapping + # of { overlay : FEATImage } pairs. The + # ClusterPanel will work with any overlay, + # if that overlay is contained within a + # feat analysis. The selectedOverlayChanged + # method identifies the FEAT analysis + # associated with the newly selected overlay, + # and stores the mapping here. Note that the + # FEATImage associated with an overlay might + # not be in the overlay list. + self.__featImages = {} + # A WidgetGrid is created for each # contrast of a FEAT image, and cached # in this dictionary. This is because # it is quite expensive to create the # grid widgets. This dictionary contains - # {overlay : [WidgetGrid]} mappings. + # {FEATImage : [WidgetGrid]} mappings. self.__clusterGrids = {} self.__addZStats .SetLabel(strings.labels[self, 'addZStats']) self.__addClustMask.SetLabel(strings.labels[self, 'addClustMask']) - self.__sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(self.__sizer) @@ -125,6 +138,7 @@ class ClusterPanel(fslpanel.FSLEyesPanel): grid.Destroy() self.__clusterGrids = None + self.__featImages = None self._overlayList.removeListener('overlays', self._name) self._displayCtx .removeListener('selectedOverlay', self._name) @@ -173,13 +187,14 @@ class ClusterPanel(fslpanel.FSLEyesPanel): self.__sizer.Show(self.__disabledText, False) self.__sizer.Show(self.__mainSizer, True) - overlay = self.__selectedOverlay - contrast = self.__statSelect.GetSelection() + overlay = self.__selectedOverlay + featImage = self.__featImages[overlay] + contrast = self.__statSelect.GetSelection() - for ovl, grids in self.__clusterGrids.items(): + for fimg, grids in self.__clusterGrids.items(): for i, grid in enumerate(grids): if grid is not None: - show = ovl is overlay and i == contrast + show = fimg is featImage and i == contrast self.__mainSizer.Show(grid, show) self.Layout() @@ -190,21 +205,22 @@ class ClusterPanel(fslpanel.FSLEyesPanel): displays clusters for the newly selected COPE (see the :meth:`__displayClusterData` method) """ - overlay = self.__selectedOverlay - idx = self.__statSelect.GetSelection() - data = self.__statSelect.GetClientData(idx) + overlay = self.__selectedOverlay + featImage = self.__featImages[overlay] + idx = self.__statSelect.GetSelection() + data = self.__statSelect.GetClientData(idx) self.Refresh() self.Update() - if overlay not in self.__clusterGrids: - self.__clusterGrids[overlay] = [None] * overlay.numContrasts() + if featImage not in self.__clusterGrids: + self.__clusterGrids[featImage] = [None] * featImage.numContrasts() - grid = self.__clusterGrids[overlay][idx] + grid = self.__clusterGrids[featImage][idx] if grid is None: - grid = self.__genClusterGrid(overlay, idx, data) - self.__clusterGrids[overlay][idx] = grid + grid = self.__genClusterGrid(overlay, featImage, idx, data) + self.__clusterGrids[featImage][idx] = grid self.__mainSizer.Add(grid, flag=wx.EXPAND, proportion=1) self.__enable() @@ -218,9 +234,10 @@ class ClusterPanel(fslpanel.FSLEyesPanel): to the :class:`.OverlayList`. """ - overlay = self.__selectedOverlay - contrast = self.__statSelect.GetSelection() - zstats = overlay.getZStats(contrast) + overlay = self.__selectedOverlay + featImage = self.__featImages[overlay] + contrast = self.__statSelect.GetSelection() + zstats = featImage.getZStats(contrast) for ol in self._overlayList: @@ -232,7 +249,7 @@ class ClusterPanel(fslpanel.FSLEyesPanel): self._overlayList.append(zstats) opts = self._displayCtx.getOpts(zstats) - zthres = float(overlay.thresholds()['z']) + zthres = float(featImage.thresholds()['z']) # Set some display parameters if # we have a z value threshold @@ -241,7 +258,8 @@ class ClusterPanel(fslpanel.FSLEyesPanel): absmax = max(map(abs, (opts.dataMin, opts.dataMax))) opts.cmap = 'Render3' - opts.invertClipping = True + opts.invertClipping = True + opts.centreRanges = True opts.displayRange.x = -absmax, absmax opts.clippingRange.x = -zthres, zthres @@ -252,10 +270,11 @@ class ClusterPanel(fslpanel.FSLEyesPanel): :meth:`.FEATImage.getClusterMask` method) """ - overlay = self.__selectedOverlay - contrast = self.__statSelect.GetSelection() - mask = overlay.getClusterMask(contrast) - + overlay = self.__selectedOverlay + featImage = self.__featImages[overlay] + contrast = self.__statSelect.GetSelection() + mask = featImage.getClusterMask(contrast) + for ol in self._overlayList: # Already in overlay list @@ -267,15 +286,28 @@ class ClusterPanel(fslpanel.FSLEyesPanel): self._displayCtx.getDisplay(mask).overlayType = 'label' - def __genClusterGrid(self, overlay, contrast, clusters): + def __genClusterGrid(self, overlay, featImage, contrast, clusters): """Creates and returns a :class:`.WidgetGrid` which contains the given list of clusters, which are related to the given contrast. - :arg contrast: The (0-indexed) number of the contrast to which the - clusters are related. - :arg clusters: A sequence of objects, each representing one cluster. - See the :meth:`.FEATImage.clusterResults` method. + .. note:: This method assumes that the given ``overlay`` is an + :class:`.Image` which has the same voxel dimensions as, + and shares the the same world coordinate system as the + ``featImage``. + + + :arg overlay: The overlay for which clusters are currently being + displayed. + + :arg featImage: The :class:`.FEATImage` to which the clusters are + related. + + :arg contrast: The (0-indexed) number of the contrast to which the + clusters are related. + + :arg clusters: A sequence of objects, each representing one cluster. + See the :meth:`.FEATImage.clusterResults` method. """ cols = {'index' : 0, @@ -290,7 +322,7 @@ class ClusterPanel(fslpanel.FSLEyesPanel): 'copemean' : 9} grid = widgetgrid.WidgetGrid(self) - conName = overlay.contrastNames()[contrast] + conName = featImage.contrastNames()[contrast] opts = self._displayCtx.getOpts(overlay) grid.SetGridSize(len(clusters), 10) @@ -365,9 +397,12 @@ class ClusterPanel(fslpanel.FSLEyesPanel): # WidgetGrid panels for overlays # that have been removed from the # list. - for overlay in self.__clusterGrids.keys(): + for overlay in self.__featImages.keys(): if overlay not in self._overlayList: - grids = self.__clusterGrids.pop(overlay) + + featImage = self.__featImages .pop(overlay) + grids = self.__clusterGrids.pop(featImage) + for grid in grids: self.__mainSizer.Detach(grid) grid.Destroy() @@ -385,11 +420,12 @@ class ClusterPanel(fslpanel.FSLEyesPanel): if self.__selectedOverlay is None: return - overlay = self.__selectedOverlay - contrast = self.__statSelect.GetSelection() + overlay = self.__selectedOverlay + featImage = self.__featImages[overlay] + contrast = self.__statSelect.GetSelection() - zstat = overlay.getZStats( contrast) - clustMask = overlay.getClusterMask(contrast) + zstat = featImage.getZStats( contrast) + clustMask = featImage.getClusterMask(contrast) dss = [ovl.dataSource for ovl in self._overlayList] @@ -415,42 +451,92 @@ class ClusterPanel(fslpanel.FSLEyesPanel): return overlay = self._displayCtx.getSelectedOverlay() - - # Not a FEAT image, can't - # do anything with that - if not isinstance(overlay, featimage.FEATImage): + featDir = featresults.getAnalysisDir(overlay.dataSource) + + # Not a FEAT analysis image, + # can't do anything with that + if featDir is None or not isinstance(overlay, fslimage.Image): + log.debug('Overlay {} is not part of a feat ' + 'analysis, or is not an Image'.format(overlay)) self.__disable(strings.messages[self, 'notFEAT']) return - + # Selected overlay is either the # same one (maybe the overlay list, # rather than the selected overlay, # changed) or the newly selected # overlay is from the same FEAT # analysis. No need to do anything. - if prevOverlay is not None and \ - (prevOverlay is overlay or - prevOverlay.getFEATDir() == overlay.getFEATDir()): - self.__selectedOverlay = overlay - return + if prevOverlay is not None: - self.__statSelect.Clear() + prevFeatImage = self.__featImages.get(prevOverlay) + + if prevOverlay is overlay or \ + (prevFeatImage is not None and + featDir == prevFeatImage.getFEATDir()): + + log.debug('Overlay {} is already selected.'.format(overlay)) + # Make sure the overlay -> FEATImage + # mapping is present, and (re-)cache + # a reference to the selected overlay. + self.__featImages[overlay] = prevFeatImage + self.__selectedOverlay = overlay + + return + + # We're in business. The newly selected + # overlay is a part of a FEAT analysis + # which is not currently being displayed. self.__selectedOverlay = overlay + # Clear the stat selection combo box. + self.__statSelect.Clear() + + # Get the FEATImage associated with + # this overlay, so we can get + # information about the FEAT analysis + featImage = self.__featImages.get(overlay) + + if featImage is None: + + # If the overlay itself is a FEATImage, + # then we have nothing to do. + if isinstance(overlay, featimage.FEATImage): + featImage = overlay + else: + # The FEATImage might already + # be in the overlay list - + # let's search for it. + for ovl in self._overlayList: + if isinstance(ovl, featimage.FEATImage) and \ + ovl.getFEATDir() == featDir: + featImage = ovl + + # As a last resort, if the FEATImage is not + # in the overlay list, we'll create one. + if featImage is None: + featImage = featimage.FEATImage(featDir, loadData=False) + + self.__featImages[overlay] = featImage + + log.debug('FEAT analysis associated with overlay {}: {}'.format( + overlay, + featImage.getFEATDir())) + display = self._displayCtx.getDisplay(overlay) - numCons = overlay.numContrasts() - conNames = overlay.contrastNames() + numCons = featImage.numContrasts() + conNames = featImage.contrastNames() try: # clusts is a list of (contrast, clusterList) tuples - clusts = [(c, overlay.clusterResults(c)) for c in range(numCons)] + clusts = [(c, featImage.clusterResults(c)) for c in range(numCons)] clusts = filter(lambda (con, clust): clust is not None, clusts) # Error parsing the cluster data except Exception as e: log.warning('Error parsing cluster data for ' - '{}: {}'.format(overlay.name, str(e)), exc_info=True) + '{}: {}'.format(featImage.name, str(e)), exc_info=True) self.__disable(strings.messages[self, 'badData']) return @@ -460,6 +546,7 @@ class ClusterPanel(fslpanel.FSLEyesPanel): self.__disable(strings.messages[self, 'noClusters']) return + # Populate the stat selection combo box for contrast, clusterList in clusts: name = conNames[contrast] name = strings.labels[self, 'clustName'].format(contrast + 1, name) @@ -468,6 +555,7 @@ class ClusterPanel(fslpanel.FSLEyesPanel): self.__overlayName.SetLabel(display.name) + # Refresh the widget grid self.__statSelect.SetSelection(0) self.__statSelected()