diff --git a/fsl/data/melodicresults.py b/fsl/data/melodicresults.py index 89b45a913507d1640da16907158090f582fa753e..6de7dc48f146b196d4ec6f83a271fd37afd09b30 100644 --- a/fsl/data/melodicresults.py +++ b/fsl/data/melodicresults.py @@ -229,35 +229,8 @@ class MelodicClassification(props.HasProperties): def load(self, filename): - """Loads component labels from the specified file. The file is assuemd - to be of the format generated by FIX or Melview; such a file should - have a structure resembling the following:: - - filtered_func_data.ica - 1, Signal, False - 2, Unclassified Noise, True - 3, Unknown, False - 4, Signal, False - 5, Unclassified Noise, True - 6, Unclassified Noise, True - 7, Unclassified Noise, True - 8, Signal, False - [2, 5, 6, 7] - - The first line of the file contains the name of the melodic directory. - Then, one line is present for each component, containing the following, - separated by commas: - - - The component index (starting from 1). - - One or more labels for the component (multiple labels must be - comma-separated). - - ``'True'`` if the component has been classified as *bad*, - ``'False'`` otherwise. - - The last line of the file contains the index (starting from 1) of all - *bad* components, i.e. those components which are not classified as - signal or unknown. - + """Loads component labels from the specified file. See the + :func:`loadMelodicLabelFile` function. .. note:: This method adds to, but does not replace, any existing component classifications stored by this @@ -266,57 +239,27 @@ class MelodicClassification(props.HasProperties): classifications. """ - with open(filename, 'rt') as f: - lines = f.readlines() - - if len(lines) < 3: - raise InvalidFixFileError('Invalid FIX classification ' - 'file - not enough lines') - - lines = [l.strip() for l in lines] - - # Ignore the first and last - # lines - we're only interested - # in the component labels - compLines = lines[1:-1] - - if len(compLines) != self.__ncomps: - raise InvalidFixFileError('Invalid FIX classification ' - 'file - number of components ' - 'do not match') - - # Parse the labels for every component - # We dot not add the labels as we go - # as, if something is wrong with the - # file contents, we don't want this - # MelodicClassification instance to - # be modified. So we'll assign the - # labels afterwards - allLabels = [] - for i, compLine in enumerate(compLines): - - tokens = compLine.split(',') - tokens = [t.strip() for t in tokens] - - if len(tokens) < 3: - raise InvalidFixFileError('Invalid FIX classification ' - 'file - component line {} does ' - 'not have enough ' - 'tokens'.format(i + 1)) - - compIdx = int(tokens[0]) - compLabels = tokens[1:-1] - - if compIdx != i + 1: - raise InvalidFixFileError('Invalid FIX classification ' - 'file - component line {} has ' - 'wrong component number ' - '({})'.format(i, compIdx)) - - allLabels.append(compLabels) - - # Now that all the labels are - # read in, we can store them + # Read the labels in + _, allLabels = loadMelodicLabelFile(filename) + + # More labels in the file than there are in + # melodic_IC - that doesn't make any sense. + if len(allLabels) > self.__ncomps: + raise InvalidFixFileError('The number of components in {} does ' + 'not match the number of components in ' + '{}!'.format(filename, + self.__melimage.dataSource)) + + # Less labels in the file than there are in + # the melodic_IC image - this is ok, as the + # file may have only contained a list of + # noisy components. We'll label the remaining + # components as 'Unknown'. + elif len(allLabels) < self.__ncomps: + for i in range(len(allLabels), self.__ncomps): + allLabels.append(['Unknown']) + + # Add the labels to this melclass object notifState = self.getNotificationState('labels') self.disableNotification('labels') @@ -330,44 +273,19 @@ class MelodicClassification(props.HasProperties): def save(self, filename): """Saves the component classifications stored by this - ``MeloidicClassification`` to the specified file. The classifications - are saved in the format described in the :meth:`load` method. - - .. TODO:: Accept a dictionary of ``{label : display label}`` mappings, - so we can output cased labels (e.g. ``'Signal'`` instead of - ``'signal'``). + ``MeloidicClassification`` to the specified file. See the + :func:`saveMelodicLabelFile` function. """ - lines = [] - badComps = [] - image = self.__melimage - - # The first line - the melodic directory name - lines.append(op.basename(image.getMelodicDir())) - - # A line for each component - for comp in range(self.__ncomps): - - noise = not (self.hasLabel(comp, 'signal') or - self.hasLabel(comp, 'unknown')) - - # Make sure there are no - # commas in any label names - labels = [self.getDisplayLabel(l) for l in self.getLabels(comp)] - labels = [l.replace(',', '_') for l in labels] - - tokens = [str(comp + 1)] + labels + [str(noise)] - - lines.append(', '.join(tokens)) - - if noise: - badComps.append(comp) + allLabels = [] - # A line listing the bad components - lines.append('[' + ', '.join([str(c + 1) for c in badComps]) + ']') + for c in range(self.__ncomps): + labels = [self.getDisplayLabel(l) for l in self.labels[c]] + allLabels.append(labels) - with open(filename, 'wt') as f: - f.write('\n'.join(lines) + '\n') + saveMelodicLabelFile(self.__melImage.getMelodicDir(), + allLabels, + filename) def getLabels(self, component): @@ -483,7 +401,162 @@ class MelodicClassification(props.HasProperties): self.notify('labels') -class InvalidFixFileError(Exception): +def loadMelodicLabelFile(filename): + """Loads component labels from the specified file. The file is assuemd + to be of the format generated by FIX or Melview; such a file should + have a structure resembling the following:: + + filtered_func_data.ica + 1, Signal, False + 2, Unclassified Noise, True + 3, Unknown, False + 4, Signal, False + 5, Unclassified Noise, True + 6, Unclassified Noise, True + 7, Unclassified Noise, True + 8, Signal, False + [2, 5, 6, 7] + + The first line of the file contains the name of the melodic directory. + Then, one line is present for each component, containing the following, + separated by commas: + + - The component index (starting from 1). + - One or more labels for the component (multiple labels must be + comma-separated). + - ``'True'`` if the component has been classified as *bad*, + ``'False'`` otherwise. + + The last line of the file contains the index (starting from 1) of all + *bad* components, i.e. those components which are not classified as + signal or unknown. """ + + with open(filename, 'rt') as f: + lines = f.readlines() + + if len(lines) < 1: + raise InvalidFixFileError('Invalid FIX classification ' + 'file - not enough lines') + + lines = [l.strip() for l in lines] + lines = [l for l in lines if l != ''] + + # If the file contains a single + # line, we assume that it is just + # a list of noise components. + if len(lines) == 1: + + melDir = None + noisyComps = map(int, lines[0][1:-1].split(', ')) + allLabels = [] + + for i in range(max(noisyComps)): + if (i + 1) in noisyComps: allLabels.append(['Unclassified noise']) + else: allLabels.append(['Signal']) + + # Otherwise, we assume that + # it is a full label file. + else: + + melDir = lines[0] + noisyComps = map(int, lines[-1][1:-1].split(', ')) + + # Parse the labels for every component + # We dot not add the labels as we go + # as, if something is wrong with the + # file contents, we don't want this + # MelodicClassification instance to + # be modified. So we'll assign the + # labels afterwards + allLabels = [] + for i, compLine in enumerate(lines[1:-1]): + + tokens = compLine.split(',') + tokens = [t.strip() for t in tokens] + + if len(tokens) < 3: + raise InvalidFixFileError('Invalid FIX classification ' + 'file - component line {} does ' + 'not have enough ' + 'tokens'.format(i + 1)) + + compIdx = int(tokens[0]) + compLabels = tokens[1:-1] + + if compIdx != i + 1: + raise InvalidFixFileError('Invalid FIX classification ' + 'file - component line {} has ' + 'wrong component number ' + '({})'.format(i, compIdx)) + + allLabels.append(compLabels) + + + # Validate the labels against + # the noisy list - all components + # in the noisy list should have + # the label 'unclassified noise'. + for i, labels in enumerate(allLabels): + + for label in labels: + if label.lower() == 'unclassified noise' and \ + (i + 1) not in noisyComps: + + raise InvalidFixFileError('Noisy component {} has an invalid ' + 'label: {}'.format(i + 1, label)) + + for comp in noisyComps: + labels = allLabels[comp - 1] + labels = [l.lower() for l in labels] + + if 'unclassified noise' not in labels: + raise InvalidFixFileError('Noisy component {} is missing ' + 'a noise label'.format(i)) + + return melDir, allLabels + + +def saveMelodicLabelFile(melDir, allLabels, filename): + """Saves the component classifications stored by this + ``MeloidicClassification`` to the specified file. The classifications + are saved in the format described in the :meth:`load` method. + """ + + lines = [] + noisyComps = [] + + # The first line - the melodic directory name + lines.append(op.abspath(melDir)) + + # A line for each component + for i, labels in enumerate(allLabels): + + comp = i + 1 + lowered = [l.lower() for l in labels] + noise = 'signal' not in lowered and 'unknown' not in lowered + + # Make sure there are no + # commas in any label names + labels = [l.replace(',', '_') for l in labels] + + tokens = [str(comp + 1)] + labels + [str(noise)] + + lines.append(', '.join(tokens)) + + if noise: + noisyComps.append(comp) + + # A line listing the bad components + lines.append('[' + ', '.join([str(c + 1) for c in noisyComps]) + ']') + + with open(filename, 'wt') as f: + f.write('\n'.join(lines) + '\n') + + +class InvalidFixFileError(Exception): + """Exception raised by the :meth:`MelodicClassification.load` method and + the :func:`loadMelodicLabelFile` function when an attempt is made to load + an invalid FIX label file. """ pass diff --git a/fsl/fsleyes/luts/melodic-classes.lut b/fsl/fsleyes/luts/melodic-classes.lut index aaf2c07fce35959dea4e16af2be7324c78148f4b..841c3cf0f6a01b8f52a58e4eeaa69f08907f4563 100644 --- a/fsl/fsleyes/luts/melodic-classes.lut +++ b/fsl/fsleyes/luts/melodic-classes.lut @@ -1,11 +1,11 @@ -1 0.419608 0.619608 0.992157 Signal -2 0.490196 0.827451 0.000000 Unknown -3 0.980392 0.478431 0.588235 Unclassified noise -4 0.843137 0.776471 0.000000 Movement -5 0.850980 0.435294 0.000000 Cardiac -6 0.494118 0.964706 0.984314 White matter -7 0.984313 0.564706 0.850980 Non-brain -8 0.858824 0.854902 0.847059 MRI -9 0.666667 0.972549 0.745098 Susceptibility-motion -10 0.780392 0.470588 0.870588 Sagittal sinus -11 0.427450 0.725490 0.603922 Respiratory +1 0.39216 0.82745 0.00000 Signal +2 0.91765 0.86275 0.00000 Unknown +3 1.00000 0.37647 0.26275 Unclassified noise +4 0.08235 0.82353 0.83922 Movement +5 0.70588 0.27059 1.00000 Cardiac +6 0.49412 0.96471 0.98431 White matter +7 0.98431 0.56471 0.85098 Non-brain +8 0.85882 0.85490 0.84706 MRI +9 0.66667 0.97255 0.74510 Susceptibility-motion +10 0.78039 0.47059 0.87059 Sagittal sinus +11 0.42745 0.72549 0.60392 Respiratory diff --git a/fsl/fsleyes/perspectives.py b/fsl/fsleyes/perspectives.py index 03a4a6f537a434898b41b1fd73c44889c0cea801..20db6f42f6c8a209950a8c18e2cb079404b351e7 100644 --- a/fsl/fsleyes/perspectives.py +++ b/fsl/fsleyes/perspectives.py @@ -404,13 +404,13 @@ BUILT_IN_PERSPECTIVES = collections.OrderedDict(( ('melodic', textwrap.dedent(""" LightBoxPanel,TimeSeriesPanel,PowerSpectrumPanel, - layout2|name=LightBoxPanel 1;caption=Lightbox View 1;state=67377088;dir=5;layer=0;row=0;pos=0;prop=100000;bestw=853;besth=-1;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=TimeSeriesPanel 2;caption=Time series 2;state=67377148;dir=3;layer=0;row=0;pos=0;prop=100000;bestw=-1;besth=472;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=PowerSpectrumPanel 3;caption=Power spectra 3;state=67377148;dir=3;layer=0;row=0;pos=1;prop=100000;bestw=-1;besth=472;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|dock_size(5,0,0)=22|dock_size(3,0,0)=493| - OverlayListPanel,LightBoxToolBar,OverlayDisplayToolBar,LocationPanel,MelodicClassificationPanel,LookupTablePanel, - layout2|name=Panel;caption=;state=768;dir=5;layer=0;row=0;pos=0;prop=100000;bestw=20;besth=20;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=OverlayListPanel;caption=Overlay list;state=67373052;dir=3;layer=0;row=0;pos=0;prop=100000;bestw=197;besth=80;minw=197;minh=80;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=197;floath=96;notebookid=-1;transparent=255|name=LightBoxToolBar;caption=Lightbox view toolbar;state=67382012;dir=1;layer=10;row=0;pos=0;prop=100000;bestw=757;besth=43;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=OverlayDisplayToolBar;caption=Display toolbar;state=67382012;dir=1;layer=11;row=0;pos=0;prop=100000;bestw=860;besth=49;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=LocationPanel;caption=Location;state=67373052;dir=3;layer=0;row=0;pos=1;prop=100000;bestw=440;besth=109;minw=440;minh=109;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=440;floath=125;notebookid=-1;transparent=255|name=MelodicClassificationPanel;caption=Melodic IC classification;state=67373052;dir=2;layer=0;row=0;pos=1;prop=100000;bestw=400;besth=100;minw=400;minh=100;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=400;floath=116;notebookid=-1;transparent=255|name=LookupTablePanel;caption=Lookup tables;state=67373052;dir=2;layer=0;row=0;pos=0;prop=100000;bestw=358;besth=140;minw=358;minh=140;maxw=-1;maxh=-1;floatx=3614;floaty=658;floatw=358;floath=156;notebookid=-1;transparent=255|dock_size(5,0,0)=22|dock_size(3,0,0)=130|dock_size(1,10,0)=45|dock_size(1,11,0)=10|dock_size(2,0,0)=402| + layout2|name=LightBoxPanel 1;caption=Lightbox View 1;state=67377088;dir=5;layer=0;row=0;pos=0;prop=100000;bestw=853;besth=-1;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=TimeSeriesPanel 2;caption=Time series 2;state=67377148;dir=3;layer=0;row=0;pos=0;prop=100000;bestw=-1;besth=472;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=PowerSpectrumPanel 3;caption=Power spectra 3;state=67377148;dir=3;layer=0;row=0;pos=1;prop=100000;bestw=-1;besth=472;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|dock_size(5,0,0)=22|dock_size(3,0,0)=195| + OverlayListPanel,OverlayDisplayToolBar,LocationPanel,LightBoxToolBar,MelodicClassificationPanel, + layout2|name=Panel;caption=;state=768;dir=5;layer=0;row=0;pos=0;prop=100000;bestw=20;besth=20;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=OverlayListPanel;caption=Overlay list;state=67373052;dir=3;layer=0;row=0;pos=0;prop=100000;bestw=204;besth=80;minw=197;minh=80;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=204;floath=96;notebookid=-1;transparent=255|name=OverlayDisplayToolBar;caption=Display toolbar;state=67382012;dir=1;layer=11;row=0;pos=0;prop=100000;bestw=810;besth=49;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=LocationPanel;caption=Location;state=67373052;dir=3;layer=0;row=0;pos=1;prop=100000;bestw=440;besth=111;minw=440;minh=109;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=440;floath=127;notebookid=-1;transparent=255|name=LightBoxToolBar;caption=Lightbox view toolbar;state=67382012;dir=1;layer=10;row=0;pos=0;prop=100000;bestw=753;besth=43;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=MelodicClassificationPanel;caption=Melodic IC classification;state=67373052;dir=2;layer=0;row=0;pos=0;prop=100000;bestw=400;besth=100;minw=400;minh=100;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=400;floath=116;notebookid=-1;transparent=255|dock_size(5,0,0)=22|dock_size(3,0,0)=130|dock_size(1,10,0)=45|dock_size(1,11,0)=51|dock_size(2,0,0)=402| , layout2|name=FigureCanvasWxAgg;caption=;state=768;dir=5;layer=0;row=0;pos=0;prop=100000;bestw=640;besth=480;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|dock_size(5,0,0)=642| , - layout2|name=FigureCanvasWxAgg;caption=;state=768;dir=5;layer=0;row=0;pos=0;prop=100000;bestw=640;besth=480;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|dock_size(5,0,0)=642| + layout2|name=FigureCanvasWxAgg;caption=;state=768;dir=5;layer=0;row=0;pos=0;prop=100000;bestw=640;besth=480;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|dock_size(5,0,0)=642|' """)), ('feat',