def OnMeasureClusters(self, event=None):
        """

        Calculates various measures for clusters using PYME.recipes.localisations.MeasureClusters

        Parameters
        ----------
        labelsKey: pipeline key to access array of label assignments. Measurements will be calculated for each label.


        """
        from PYME.recipes import localisations
        from PYME.recipes import Recipe

        # build a recipe programatically
        measrec = Recipe()

        measrec.add_module(
            localisations.MeasureClusters3D(measrec,
                                            inputName='input',
                                            labelsKey='dbscanClustered',
                                            outputName='output'))

        measrec.namespace['input'] = self.pipeline.output
        #configure parameters
        if not measrec.configure_traits(view=measrec.pipeline_view,
                                        kind='modal'):
            return  # handle cancel

        # run recipe
        meas = measrec.execute()

        # For now, don't make this a data source, as that requires (for multicolor) clearing the pipeline mappings.
        self.clusterMeasures.append(meas)
    def OnClustersInTime(self, event=None):
        #FIXME - this would probably be better in an addon module outside of the core project
        from PYME.recipes import localisations
        from PYME.recipes import Recipe
        import matplotlib.pyplot as plt

        # build a recipe programatically
        rec = Recipe()

        # split input according to colour channel selected
        rec.add_module(
            localisations.ExtractTableChannel(rec,
                                              inputName='input',
                                              outputName='chan0',
                                              channel='chan0'))

        rec.add_module(
            localisations.ClusterCountVsImagingTime(rec,
                                                    inputName='chan0',
                                                    stepSize=3000,
                                                    outputName='output'))

        rec.namespace[
            'input'] = self.pipeline.output  #do before configuring so that we already have the channel names populated
        #configure parameters
        if not rec.configure_traits(view=rec.pipeline_view, kind='modal'):
            return  #handle cancel

        incrementedClumps = rec.execute()

        plt.figure()
        plt.scatter(incrementedClumps['t'],
                    incrementedClumps['N_labelsWithLowMinPoints'],
                    label=('clusters with Npoints > %i' %
                           rec.modules[-1].lowerMinPtsPerCluster),
                    c='b',
                    marker='s')
        plt.scatter(incrementedClumps['t'],
                    incrementedClumps['N_labelsWithHighMinPoints'],
                    label=('clusters with Npoints > %i' %
                           rec.modules[-1].higherMinPtsPerCluster),
                    c='g',
                    marker='o')

        plt.legend(loc=4, scatterpoints=1)
        plt.xlabel('Number of frames included')
        plt.ylabel('Number of Clusters')
def test_queue_acquisitions():
    from PYME.IO.tabular import DictSource
    from PYME.recipes import Recipe
    import numpy as np
    import time

    action_manager.paused = True

    d = DictSource({'x': np.arange(10), 'y': np.arange(10)})
    rec = Recipe()
    rec.namespace['input'] = d

    spool_settings = {'extra_metadata': {'Sample.Well': '{file_stub}'}}

    rec.add_module(
        acquisition.QueueAcquisitions(rec, spool_settings=spool_settings))
    rec.save(context={'file_stub': 'A1'})
    time.sleep(1)
    task = action_manager.actionQueue.get_nowait()
    assert 'A1' == task[1]._then.params['extra_metadata']['Sample.Well']
Beispiel #4
0
def test_stats_by_frame():
    recipe = Recipe()
    test_length = 10
    x, y = np.meshgrid(range(test_length), range(test_length))
    mask = x > test_length/2  # mask out everything but 6, 7, 8, 9

    # check 2D
    recipe.namespace['input'] = ImageStack(data=x)
    recipe.namespace['mask'] = ImageStack(data=mask)
    stats_mod = processing.StatisticsByFrame(input_name='input', mask='mask', output_name='output')
    recipe.add_module(stats_mod)
    stats = recipe.execute()
    # check results
    assert len(stats['mean']) == 1
    assert stats['mean'] == 7.5

    # test 3D with 2D mask
    recipe.namespace.clear()
    x3, y3, z3 = np.meshgrid(range(test_length), range(test_length), range(test_length))
    recipe.namespace['input'] = ImageStack(data=z3)
    # reuse the same mask from before, which will now take the right 4 columns at each slice
    recipe.namespace['mask'] = ImageStack(data=mask)
    stats = recipe.execute()
    # check results
    np.testing.assert_array_almost_equal(stats['mean'], range(test_length))

    # test 3D with 3D mask
    mask = x3 > test_length / 2
    recipe.namespace['mask'] = ImageStack(data=mask)
    stats = recipe.execute()
    # check results
    np.testing.assert_array_almost_equal(stats['mean'], np.ma.masked_array(z3, mask=~(x3 > test_length / 2)).mean(axis=(0, 1)))

    # test no mask
    stats_mod.mask = ''
    stats = recipe.execute()
    np.testing.assert_array_almost_equal(stats['mean'], np.mean(z3, axis=(0, 1)))
class Pipeline:
    def __init__(self, filename=None, visFr=None):
        self.filter = None
        self.mapping = None
        self.colourFilter = None
        self.events = None

        self.recipe = Recipe(execute_on_invalidation=True)
        self.recipe.recipe_executed.connect(self.Rebuild)

        self.selectedDataSourceKey = None
        self.filterKeys = {
            'error_x': (0, 30),
            'error_y': (0, 30),
            'A': (5, 20000),
            'sig': (95, 200)
        }

        self.blobSettings = BlobSettings()
        self.objects = None

        self.imageBounds = ImageBounds(0, 0, 0, 0)
        self.mdh = MetaDataHandler.NestedClassMDHandler()

        self.Triangles = None
        self.edb = None
        self.Quads = None
        self.GeneratedMeasures = {}

        self.QTGoalPixelSize = 5

        self._extra_chan_num = 0

        self.filesToClose = []

        self.ev_mappings = {}

        #define a signal which a GUI can hook if the pipeline is rebuilt (i.e. the output changes)
        self.onRebuild = dispatch.Signal()

        #a cached list of our keys to be used to decide whether to fire a keys changed signal
        self._keys = None
        #define a signal which can be hooked if the pipeline keys have changed
        self.onKeysChanged = dispatch.Signal()

        self.ready = False
        #self.visFr = visFr

        if not filename is None:
            self.OpenFile(filename)

        #renderers.renderMetadataProviders.append(self.SaveMetadata)

    @property
    def output(self):
        return self.colourFilter

    def __getitem__(self, keys):
        """gets values from the 'tail' of the pipeline (ie the colourFilter)"""

        return self.output[keys]

    def keys(self):
        return self.output.keys()

    def __getattr__(self, item):
        try:
            #if 'colourFilter in '
            if self.output is None:
                raise AttributeError('colourFilter not yet created')

            return self.output[item]
        except KeyError:
            raise AttributeError("'%s' has not attribute '%s'" %
                                 (self.__class__, item))

    def __dir__(self):
        if self.output is None:
            return list(self.__dict__.keys()) + list(dir(type(self)))
        else:
            return list(self.output.keys()) + list(
                self.__dict__.keys()) + list(dir(type(self)))

    #compatibility redirects
    @property
    def fluorSpecies(self):
        #warnings.warn(DeprecationWarning('Use colour_mapper.species_ratios instead'))
        raise DeprecationWarning('Use colour_mapper.species_ratios instead')
        return self.colour_mapper.species_ratios

    @property
    def fluorSpeciesDyes(self):
        #warnings.warn(DeprecationWarning('Use colour_mapper.species_dyes instead'))
        raise DeprecationWarning('Use colour_mapper.species_dyes instead')
        return self.colour_mapper.species_dyes

    @property
    def chromaticShifts(self):
        return self.colourFilter.chromaticShifts

    #end compatibility redirects

    @property
    def dataSources(self):
        return self.recipe.namespace

    @property
    def layer_datasources(self):
        lds = {'output': self.colourFilter}
        lds.update(self.dataSources)
        return lds

    @property
    def layer_data_source_names(self):
        """
        Return a list of names of datasources we can use with dotted channel selection

        There is a little bit of magic here as we augment the names with dotted names for colour channel selection
        """
        names = []  #'']
        for k, v in self.layer_datasources.items():
            names.append(k)
            if isinstance(v, tabular.ColourFilter):
                for c in v.getColourChans():
                    names.append('.'.join([k, c]))

        return names

    def get_layer_data(self, dsname):
        """
        Returns layer data for a given name. The principle difference to just accessing self.dataSources directly is that
        we do some magic relating to allow colour channels to be accessed with the dot notation e.g. dsname.colour_channel

        """
        if dsname == '':
            return self

        parts = dsname.split('.')
        if len(parts) == 2:
            # special case - permit access to channels using dot notation
            # NB: only works if our underlying datasource is a ColourFilter
            ds, channel = parts
            if ds == 'output':
                return self.colourFilter.get_channel_ds(channel)
            else:
                return self.dataSources.get(ds, None).get_channel_ds(channel)
        else:
            if dsname == 'output':
                return self.colourFilter
            else:
                return self.dataSources.get(dsname, None)

    @property
    def selectedDataSource(self):
        """

        The currently selected data source (an instance of tabular.inputFilter derived class)

        """
        if self.selectedDataSourceKey is None:
            return None
        else:
            return self.dataSources[self.selectedDataSourceKey]

    def selectDataSource(self, dskey):
        """
        Set the currently selected data source

        Parameters
        ----------
        dskey : string
            The data source name

        """
        if not dskey in self.dataSources.keys():
            raise KeyError('Data Source "%s" not found' % dskey)

        self.selectedDataSourceKey = dskey

        #remove any keys from the filter which are not present in the data
        for k in list(self.filterKeys.keys()):
            if not k in self.selectedDataSource.keys():
                self.filterKeys.pop(k)

        self.Rebuild()

    def new_ds_name(self, stub, return_count=False):
        """
        Generate a name for a new, unused, pipeline step output based on a stub
        
        FIXME - should this be in ModuleCollection instead?
        FIXME - should this use recipe.outputs as well?
        
        Parameters
        ----------
        stub - string to start the name with

        Returns
        -------

        """
        count = 0
        pattern = stub + '%d'

        name = pattern % count
        while name in self.dataSources.keys():
            count += 1
            name = pattern % count

        if return_count:
            return name, count

        return name

    def addColumn(self, name, values, default=0):
        """
        Adds a column to the currently selected data source. Attempts to guess whether the size matches the input or
        the output, and adds padding values appropriately if it matches the output.

        Parameters
        ----------
        name : str
            The column name
        values : array like
            The values
        default : float
            The default value to pad with if we've given an output-sized array

        """
        import warnings
        warnings.warn(
            'Deprecated. You should not add columns to the pipeline as this injects data and is not captured by the recipe',
            DeprecationWarning)

        ds_len = len(
            self.selectedDataSource[self.selectedDataSource.keys()[0]])
        val_len = len(values)

        if val_len == ds_len:
            #length matches the length of our input data source - do a simple add
            self.selectedDataSource.addColumn(name, values)
        elif val_len == len(self[self.keys()[0]]):

            col_index = self.colourFilter.index

            idx = np.copy(self.filter.Index)
            idx[self.filter.Index] = col_index

            ds_vals = np.zeros(ds_len) + default
            ds_vals[idx] = np.array(values)

            self.selectedDataSource.addColumn(name, ds_vals)
        else:
            raise RuntimeError(
                "Length of new column doesn't match either the input or output lengths"
            )

    def addDataSource(self, dskey, ds, add_missing_vars=True):
        """
        Add a new data source

        Parameters
        ----------
        dskey : str
            The name of the new data source
        ds : an tabular.inputFilter derived class
            The new data source

        """
        #check that we have a suitable object - note that this could potentially be relaxed
        assert isinstance(ds, tabular.TabularBase)

        if not isinstance(ds, tabular.MappingFilter):
            #wrap with a mapping filter
            ds = tabular.MappingFilter(ds)

        #add keys which might not already be defined
        if add_missing_vars:
            _add_missing_ds_keys(ds, self.ev_mappings)

        if getattr(ds, 'mdh', None) is None:
            try:
                ds.mdh = self.mdh
            except AttributeError:
                logger.error('No metadata defined in pipeline')
                pass

        self.dataSources[dskey] = ds

    def Rebuild(self, **kwargs):
        """
        Rebuild the pipeline. Called when the selected data source is changed/modified and/or the filter is changed.

        """
        for s in self.dataSources.values():
            if 'setMapping' in dir(s):
                #keep raw measurements available
                s.setMapping('x_raw', 'x')
                s.setMapping('y_raw', 'y')

                if 'z' in s.keys():
                    s.setMapping('z_raw', 'z')

        if not self.selectedDataSource is None:
            if not self.mapping is None:
                # copy any mapping we might have made across to the new mapping filter (should fix drift correction)
                # TODO - make drift correction a recipe module so that we don't need this code. Long term we should be
                # ditching the mapping filter here.
                old_mapping = self.mapping
                self.mapping = tabular.MappingFilter(self.selectedDataSource)
                self.mapping.mappings.update(old_mapping.mappings)
            else:
                self.mapping = tabular.MappingFilter(self.selectedDataSource)

            #the filter, however needs to be re-generated with new keys and or data source
            self.filter = tabular.ResultsFilter(self.mapping,
                                                **self.filterKeys)

            #we can also recycle the colour filter
            if self.colourFilter is None:
                self.colourFilter = tabular.ColourFilter(self.filter)
            else:
                self.colourFilter.resultsSource = self.filter

            #self._process_colour()

            self.ready = True

        self.ClearGenerated()

    def ClearGenerated(self):
        self.Triangles = None
        self.edb = None
        self.GeneratedMeasures = {}
        self.Quads = None

        self.onRebuild.send_robust(sender=self)

        #check to see if any of the keys have changed - if so, fire a keys changed event so the GUI can update
        newKeys = self.keys()
        if not newKeys == self._keys:
            self.onKeysChanged.send_robust(sender=self)

    def CloseFiles(self):
        while len(self.filesToClose) > 0:
            self.filesToClose.pop().close()

    def _ds_from_file(self, filename, **kwargs):
        """
        loads a data set from a file

        Parameters
        ----------
        filename : str
        kwargs : any additional arguments (see OpenFile)

        Returns
        -------
        ds : tabular.TabularBase
            the datasource, complete with metadatahandler and events if found.

        """
        mdh = MetaDataHandler.NestedClassMDHandler()
        events = None
        if os.path.splitext(filename)[1] == '.h5r':
            import tables
            h5f = tables.open_file(filename)
            self.filesToClose.append(h5f)

            try:
                ds = tabular.H5RSource(h5f)

                if 'DriftResults' in h5f.root:
                    driftDS = tabular.H5RDSource(h5f)
                    self.driftInputMapping = tabular.MappingFilter(driftDS)
                    #self.dataSources['Fiducials'] = self.driftInputMapping
                    self.addDataSource('Fiducials', self.driftInputMapping)

                    if len(ds['x']) == 0:
                        self.selectDataSource('Fiducials')

            except:  #fallback to catch series that only have drift data
                logger.exception('No fitResults table found')
                ds = tabular.H5RDSource(h5f)

                self.driftInputMapping = tabular.MappingFilter(ds)
                #self.dataSources['Fiducials'] = self.driftInputMapping
                self.addDataSource('Fiducials', self.driftInputMapping)
                #self.selectDataSource('Fiducials')

            # really old files might not have metadata, so test for it before assuming
            if 'MetaData' in h5f.root:
                mdh = MetaDataHandler.HDFMDHandler(h5f)

            if ('Events' in h5f.root) and ('StartTime' in mdh.keys()):
                events = h5f.root.Events[:]

        elif filename.endswith('.hdf'):
            #recipe output - handles generically formatted .h5
            import tables
            h5f = tables.open_file(filename)
            self.filesToClose.append(h5f)

            #defer our IO to the recipe IO method - TODO - do this for other file types as well
            self.recipe._inject_tables_from_hdf5('', h5f, filename, '.hdf')

            for dsname, ds_ in self.dataSources.items():
                #loop through tables until we get one which defines x. If no table defines x, take the last table to be added
                #TODO make this logic better.
                ds = ds_
                if 'x' in ds.keys():
                    # TODO - get rid of some of the grossness here
                    mdh = getattr(ds, 'mdh', mdh)
                    events = getattr(ds, 'events', events)
                    break

        elif os.path.splitext(filename)[1] == '.mat':  #matlab file
            if 'VarName' in kwargs.keys():
                #old style matlab import
                ds = tabular.MatfileSource(filename, kwargs['FieldNames'],
                                           kwargs['VarName'])
            else:
                if kwargs.get('Multichannel', False):
                    ds = tabular.MatfileMultiColumnSource(filename)
                else:
                    ds = tabular.MatfileColumnSource(filename)

                # check for column name mapping
                field_names = kwargs.get('FieldNames', None)
                if field_names:
                    if kwargs.get('Multichannel', False):
                        field_names.append(
                            'probe')  # don't forget to copy this field over
                    ds = tabular.MappingFilter(
                        ds, **{
                            new_field: old_field
                            for new_field, old_field in zip(
                                field_names, ds.keys())
                        })

        elif os.path.splitext(filename)[1] == '.csv':
            #special case for csv files - tell np.loadtxt to use a comma rather than whitespace as a delimeter
            if 'SkipRows' in kwargs.keys():
                ds = tabular.TextfileSource(filename,
                                            kwargs['FieldNames'],
                                            delimiter=',',
                                            skiprows=kwargs['SkipRows'])
            else:
                ds = tabular.TextfileSource(filename,
                                            kwargs['FieldNames'],
                                            delimiter=',')

        else:  #assume it's a tab (or other whitespace) delimited text file
            if 'SkipRows' in kwargs.keys():
                ds = tabular.TextfileSource(filename,
                                            kwargs['FieldNames'],
                                            skiprows=kwargs['SkipRows'])
            else:
                ds = tabular.TextfileSource(filename, kwargs['FieldNames'])

        # make sure mdh is writable (file-based might not be)
        ds.mdh = MetaDataHandler.NestedClassMDHandler(mdToCopy=mdh)
        if events is not None:
            # only set the .events attribute if we actually have events.
            # ensure that events are sorted in increasing time order
            ds.events = events[np.argsort(events['Time'])]

        return ds

    def OpenFile(self, filename='', ds=None, clobber_recipe=True, **kwargs):
        """Open a file - accepts optional keyword arguments for use with files
        saved as .txt and .mat. These are:
            
            FieldNames: a list of names for the fields in the text file or
                        matlab variable.
            VarName:    the name of the variable in the .mat file which 
                        contains the data.
            SkipRows:   Number of header rows to skip for txt file data
            
            PixelSize:  Pixel size if not in nm
            
        """

        #close any files we had open previously
        while len(self.filesToClose) > 0:
            self.filesToClose.pop().close()

        # clear our state
        # nb - equivalent to clearing recipe namespace
        self.dataSources.clear()

        if clobber_recipe:
            # clear any processing modules from the pipeline
            # call with clobber_recipe = False in a 'Open a new file with the processing pipeline I've set up' use case
            # TODO: Add an "File-->Open [preserving recipe]" menu option or similar
            self.recipe.modules = []

        if 'zm' in dir(self):
            del self.zm
        self.filter = None
        self.mapping = None
        self.colourFilter = None
        self.events = None
        self.mdh = MetaDataHandler.NestedClassMDHandler()

        self.filename = filename

        if ds is None:
            from PYME.IO import unifiedIO  # TODO - what is the launch time penalty here for importing clusterUI and finding a nameserver?

            # load from file(/cluster, downloading a copy of the file if needed)
            with unifiedIO.local_or_temp_filename(filename) as fn:
                # TODO - check that loading isn't lazy (i.e. we need to make a copy of data in memory whilst in the
                # context manager in order to be safe with unifiedIO and cluster data). From a quick look, it would seem
                # that _ds_from_file() copies the data, but potentially keeps the file open which could be problematic.
                # This won't effect local file loading even if loading is lazy (i.e. shouldn't cause a regression)
                ds = self._ds_from_file(fn, **kwargs)
                self.events = getattr(ds, 'events', None)
                self.mdh.copyEntriesFrom(ds.mdh)

        # skip the MappingFilter wrapping, etc. in self.addDataSource and add this datasource as-is
        self.dataSources['FitResults'] = ds

        # Fit module specific filter settings
        # TODO - put all the defaults here and use a local variable rather than in __init__ (self.filterKeys is largely an artifact of pre-recipe based pipeline)
        if 'Analysis.FitModule' in self.mdh.getEntryNames():
            fitModule = self.mdh['Analysis.FitModule']
            if 'Interp' in fitModule:
                self.filterKeys['A'] = (5, 100000)
            if fitModule == 'SplitterShiftEstFR':
                self.filterKeys['fitError_dx'] = (0, 10)
                self.filterKeys['fitError_dy'] = (0, 10)

        if clobber_recipe:
            from PYME.recipes.localisations import ProcessColour, Pipelineify
            from PYME.recipes.tablefilters import FilterTable

            add_pipeline_variables = Pipelineify(
                self.recipe,
                inputFitResults='FitResults',
                pixelSizeNM=kwargs.get('PixelSize', 1.),
                outputLocalizations='Localizations')
            self.recipe.add_module(add_pipeline_variables)

            #self._get_dye_ratios_from_metadata()

            colour_mapper = ProcessColour(self.recipe,
                                          input='Localizations',
                                          output='colour_mapped')
            self.recipe.add_module(colour_mapper)
            self.recipe.add_module(
                FilterTable(self.recipe,
                            inputName='colour_mapped',
                            outputName='filtered_localizations',
                            filters={
                                k: list(v)
                                for k, v in self.filterKeys.items()
                                if k in ds.keys()
                            }))
        else:
            logger.warn(
                'Opening file without clobbering recipe, filter and ratiometric colour settings might not be handled properly'
            )
            # FIXME - should we update filter keys and/or make the filter more robust
            # FIXME - do we need to do anything about colour settings?

        self.recipe.execute()
        self.filterKeys = {}
        if 'filtered_localizations' in self.dataSources.keys():
            self.selectDataSource(
                'filtered_localizations')  #NB - this rebuilds the pipeline
        else:
            # TODO - replace / remove this fallback with something better. This is currently required
            # when we use/abuse the pipeline in dh5view, but that should ideally be replaced with
            # something cleaner. This (and case above) should probably also be conditional on `clobber_recipe`
            # as if opening with an existing recipe we would likely want to keep selectedDataSource constant as well.
            self.selectDataSource('FitResults')

        # FIXME - we do this already in pipelinify, maybe we can avoid doubling up?
        self.ev_mappings, self.eventCharts = _processEvents(
            ds, self.events, self.mdh)  # extract information from any events
        # Retrieve or estimate image bounds
        if False:  # 'imgBounds' in kwargs.keys():
            # TODO - why is this disabled? Current usage would appear to be when opening from LMAnalysis
            # during real-time localization, to force image bounds to match raw data, but also potentially useful
            # for other scenarios where metadata is not fully present.
            self.imageBounds = kwargs['imgBounds']
        elif ('scanx' not in self.selectedDataSource.keys()
              or 'scany' not in self.selectedDataSource.keys()
              ) and 'Camera.ROIWidth' in self.mdh.getEntryNames():
            self.imageBounds = ImageBounds.extractFromMetadata(self.mdh)
        else:
            self.imageBounds = ImageBounds.estimateFromSource(
                self.selectedDataSource)

        #self._process_colour()

    @property
    def colour_mapper(self):
        """ Search for a colour mapper rather than use a hard coded reference - allows loading of saved pipelines with colour mapping"""
        from PYME.recipes.localisations import ProcessColour

        # find ProcessColour instance(s) in the pipeline
        mappers = [
            m for m in self.recipe.modules if isinstance(m, ProcessColour)
        ]

        if len(mappers) > 0:
            #return the first mapper we find
            return mappers[0]

        else:
            return None

    def OpenChannel(self, filename='', ds=None, channel_name='', **kwargs):
        """Open a file - accepts optional keyword arguments for use with files
        saved as .txt and .mat. These are:

            FieldNames: a list of names for the fields in the text file or
                        matlab variable.
            VarName:    the name of the variable in the .mat file which
                        contains the data.
            SkipRows:   Number of header rows to skip for txt file data

            PixelSize:  Pixel size if not in nm

        """
        if channel_name == '' or channel_name is None:
            #select a channel name automatically
            channel_name = 'Channel%d' % self._extra_chan_num
            self._extra_chan_num += 1

        if ds is None:
            #load from file
            ds = self._ds_from_file(filename, **kwargs)

        #wrap the data source with a mapping so we can fiddle with things
        #e.g. combining z position and focus
        mapped_ds = tabular.MappingFilter(ds)

        if 'PixelSize' in kwargs.keys():
            mapped_ds.addVariable('pixelSize', kwargs['PixelSize'])
            mapped_ds.setMapping('x', 'x*pixelSize')
            mapped_ds.setMapping('y', 'y*pixelSize')

        self.addDataSource(channel_name, mapped_ds)

    def _process_colour(self):
        """
        Locate any colour / channel information and munge it into a format that the colourFilter understands.

        We currently accept 3 ways of specifying channels:

         - ratiometric colour, where 'gFrac' is defined to be the ratio between our observation channels
         - defining a 'probe' column in the input data which gives a channel index for each point
         - specifying colour ranges in the metadata

         All of these get munged into the p_dye type entries that the colour filter needs.

        """
        #clear out old colour keys
        warnings.warn(
            DeprecationWarning(
                'This should not be called (colour now handled by the ProcessColour recipe module)'
            ))
        for k in self.mapping.mappings.keys():
            if k.startswith('p_'):
                self.mapping.mappings.pop(k)

        if 'gFrac' in self.selectedDataSource.keys():
            #ratiometric
            for structure, ratio in self.fluorSpecies.items():
                if not ratio is None:
                    self.mapping.setMapping(
                        'p_%s' % structure,
                        'exp(-(%f - gFrac)**2/(2*error_gFrac**2))/(error_gFrac*sqrt(2*numpy.pi))'
                        % ratio)
        else:
            if 'probe' in self.mapping.keys():
                #non-ratiometric (i.e. sequential) colour
                #color channel is given in 'probe' column
                self.mapping.setMapping('ColourNorm', '1.0 + 0*probe')

                for i in range(int(self.mapping['probe'].min()),
                               int(self.mapping['probe'].max() + 1)):
                    self.mapping.setMapping('p_chan%d' % i,
                                            '1.0*(probe == %d)' % i)

            nSeqCols = self.mdh.getOrDefault('Protocol.NumberSequentialColors',
                                             1)
            if nSeqCols > 1:
                for i in range(nSeqCols):
                    self.mapping.setMapping('ColourNorm', '1.0 + 0*t')
                    cr = self.mdh['Protocol.ColorRange%d' % i]
                    self.mapping.setMapping('p_chan%d' % i,
                                            '(t>= %d)*(t<%d)' % cr)

        #self.ClearGenerated()

    def _get_dye_ratios_from_metadata(self):
        warnings.warn(
            DeprecationWarning(
                'This should not be called (colour now handled by the ProcessColour recipe module)'
            ))
        labels = self.mdh.getOrDefault('Sample.Labelling', [])
        seen_structures = []

        for structure, dye in labels:
            if structure in seen_structures:
                strucname = structure + '_1'
            else:
                strucname = structure
            seen_structures.append(structure)

            ratio = dyeRatios.getRatio(dye, self.mdh)

            if not ratio is None:
                self.fluorSpecies[strucname] = ratio
                self.fluorSpeciesDyes[strucname] = dye
                #self.mapping.setMapping('p_%s' % structure, '(1.0/(ColourNorm*2*numpy.pi*fitError_Ag*fitError_Ar))*exp(-(fitResults_Ag - %f*A)**2/(2*fitError_Ag**2) - (fitResults_Ar - %f*A)**2/(2*fitError_Ar**2))' % (ratio, 1-ratio))
                #self.mapping.setMapping('p_%s' % structure, 'exp(-(%f - gFrac)**2/(2*error_gFrac**2))/(error_gFrac*sqrt(2*numpy.pi))' % ratio)

    def getNeighbourDists(self, forceRetriang=False):
        from PYME.LMVis import visHelpers

        if forceRetriang or not 'neighbourDistances' in self.GeneratedMeasures.keys(
        ):
            statNeigh = statusLog.StatusLogger(
                "Calculating mean neighbour distances ...")
            self.GeneratedMeasures['neighbourDistances'] = np.array(
                visHelpers.calcNeighbourDists(
                    self.getTriangles(forceRetriang)))

        return self.GeneratedMeasures['neighbourDistances']

    def getTriangles(self, recalc=False):
        from matplotlib import tri

        if self.Triangles is None or recalc:
            statTri = statusLog.StatusLogger("Generating Triangulation ...")
            self.Triangles = tri.Triangulation(
                self.colourFilter['x'] +
                .1 * np.random.normal(size=len(self.colourFilter['x'])),
                self.colourFilter['y'] +
                .1 * np.random.normal(size=len(self.colourFilter['x'])))

            #reset things which will have changed
            self.edb = None
            try:
                self.GeneratedMeasures.pop('neighbourDistances')
            except KeyError:
                pass

        return self.Triangles

    def getEdb(self):
        from PYME.Analysis.points.EdgeDB import edges
        if self.edb is None:
            self.edb = edges.EdgeDB(self.getTriangles())

        return self.edb

    def getBlobs(self):
        from PYME.Analysis.points.EdgeDB import edges

        tri = self.getTriangles()
        edb = self.getEdb()

        if self.blobSettings.jittering == 0:
            self.objIndices = edges.objectIndices(
                edb.segment(self.blobSettings.distThreshold),
                self.blobSettings.minSize)
            self.objects = [
                np.vstack((tri.x[oi], tri.y[oi])).T for oi in self.objIndices
            ]
        else:
            from matplotlib import tri

            ndists = self.getNeighbourDists()

            x_ = np.hstack([
                self['x'] + 0.5 * ndists * np.random.normal(size=ndists.size)
                for i in range(self.blobSettings.jittering)
            ])
            y_ = np.hstack([
                self['y'] + 0.5 * ndists * np.random.normal(size=ndists.size)
                for i in range(self.blobSettings.jittering)
            ])

            T = tri.Triangulation(x_, y_)
            edb = edges.EdgeDB(T)

            objIndices = edges.objectIndices(
                edb.segment(self.blobSettings.distThreshold),
                self.blobSettings.minSize)
            self.objects = [
                np.vstack((T.x[oi], T.y[oi])).T for oi in objIndices
            ]

        return self.objects, self.blobSettings.distThreshold

    def GenQuads(self, max_leaf_size=10):
        from PYME.Analysis.points.QuadTree import pointQT

        di = max(self.imageBounds.x1 - self.imageBounds.x0,
                 self.imageBounds.y1 - self.imageBounds.y0)

        numPixels = di / self.QTGoalPixelSize

        di = self.QTGoalPixelSize * 2**np.ceil(np.log2(numPixels))

        self.Quads = pointQT.qtRoot(self.imageBounds.x0,
                                    self.imageBounds.x0 + di,
                                    self.imageBounds.y0,
                                    self.imageBounds.y0 + di)

        for xi, yi in zip(self['x'], self['y']):
            self.Quads.insert(pointQT.qtRec(xi, yi, None), max_leaf_size)

    def measureObjects(self):
        from PYME.Analysis.points import objectMeasure

        self.objectMeasures = objectMeasure.measureObjects(
            self.objects, self.objThreshold)

        return self.objectMeasures

    def save_txt(self, outFile, keys=None):
        if outFile.endswith('.csv'):
            delim = ', '
        else:
            delim = '\t'

        if keys is None:
            keys = self.keys()

        #nRecords = len(ds[keys[0]])

        of = open(outFile, 'w')

        of.write('#' + delim.join(['%s' % k for k in keys]) + '\n')

        for row in zip(*[self[k] for k in keys]):
            of.write(delim.join(['%e' % c for c in row]) + '\n')

        of.close()

    def save_hdf(self, filename):
        self.colourFilter.to_hdf(filename,
                                 tablename='Localizations',
                                 metadata=self.mdh)

    def to_recarray(self, keys=None):
        return self.colourFilter.to_recarray(keys=keys)

    def toDataFrame(self, keys=None):
        import pandas as pd
        if keys is None:
            keys = self.keys()

        d = {k: self[k] for k in keys}

        return pd.DataFrame(d)

    @property
    def dtypes(self):
        return {k: str(self[k, :2].dtype) for k in self.keys()}

    def _repr_html_(self):
        import jinja2
        TEMPLATE = """
        <h3> LMVis.pipeline.Pipeline viewing {{ pipe.filename }} </h3>
        <br>
        {{ recipe_svg }}
        <b> Data Sources: </b> {% for k in  pipe.dataSources.keys() %} {% if k != pipe.selectedDataSourceKey %} {{ k }} - [{{ pipe.dataSources[k]|length }} evts], {% endif %} {% endfor %} <b> {{ pipe.selectedDataSourceKey }} - [{{ pipe.dataSources[pipe.selectedDataSourceKey]|length }} evts]</b>
        <br>
        <b> Columns: </b> {{ grouped_keys }}
        """

        try:
            svg = self.recipe.to_svg()
        except:
            svg = None

        fr_keys = []
        fe_keys = []
        sl_keys = []
        st_keys = []

        for k in self.keys():
            if k.startswith('fitResults'):
                fr_keys.append(k)
            elif k.startswith('fitError'):
                fe_keys.append(k)
            elif k.startswith('slicesUsed'):
                sl_keys.append(k)
            else:
                st_keys.append(k)

        grouped_keys = sorted(st_keys) + sorted(fr_keys) + sorted(
            fe_keys) + sorted(sl_keys)

        return jinja2.Template(TEMPLATE).render(
            pipe=self, recipe_svg=svg, grouped_keys=', '.join(grouped_keys))
    def OnPairwiseDistanceHistogram(self, event=None):
        from PYME.recipes import tablefilters, localisations, measurement
        from PYME.recipes import Recipe
        import matplotlib.pyplot as plt
        import wx
        import os

        # build a recipe programatically
        distogram = Recipe()

        # split input according to colour channels selected
        distogram.add_module(
            localisations.ExtractTableChannel(distogram,
                                              inputName='input',
                                              outputName='chan0',
                                              channel='chan0'))
        distogram.add_module(
            localisations.ExtractTableChannel(distogram,
                                              inputName='input',
                                              outputName='chan1',
                                              channel='chan0'))

        # Histogram
        distogram.add_module(
            measurement.PairwiseDistanceHistogram(distogram,
                                                  inputPositions='chan0',
                                                  inputPositions2='chan1',
                                                  outputName='output'))

        distogram.namespace[
            'input'] = self.pipeline.output  #do before configuring so that we already have the channel names populated
        #configure parameters
        if not distogram.configure_traits(view=distogram.pipeline_view,
                                          kind='modal'):
            return  #handle cancel
        selectedChans = (distogram.modules[-1].inputPositions,
                         distogram.modules[-1].inputPositions2)
        #run recipe
        distances = distogram.execute()

        binsz = (distances['bins'][1] - distances['bins'][0])
        self.pairwiseDistances[selectedChans] = {
            'counts': np.array(distances['counts']),
            'bins': np.array(distances['bins'] + 0.5 * binsz)
        }

        plt.figure()
        plt.bar(self.pairwiseDistances[selectedChans]['bins'] - 0.5 * binsz,
                self.pairwiseDistances[selectedChans]['counts'],
                width=binsz)

        hist_dlg = wx.FileDialog(
            None,
            message="Save histogram as csv...",
            #  defaultDir=os.getcwd(),
            defaultFile='disthist_{}.csv'.format(
                os.path.basename(self.pipeline.filename)),
            wildcard='CSV (*.csv)|*.csv',
            style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)

        if hist_dlg.ShowModal() == wx.ID_OK:
            histfn = hist_dlg.GetPath()
            np.savetxt(histfn,
                       np.vstack([
                           self.pairwiseDistances[selectedChans]['bins'] -
                           0.5 * binsz,
                           self.pairwiseDistances[selectedChans]['counts']
                       ]).T,
                       delimiter=',',
                       header='Bins [nm],Counts')
    def OnFindMixedClusters(self, event=None):
        """
        FindMixedClusters first uses DBSCAN clustering on two color channels separately for denoising purposes, then
        after having removed noisy points, DBSCAN is run again on both channels combined, and the fraction of clumps
        containing both colors is determined.
        """
        from PYME.recipes import tablefilters, localisations
        from PYME.recipes import Recipe
        import wx

        chans = self.pipeline.colourFilter.getColourChans()
        nchan = len(chans)
        if nchan < 2:
            raise RuntimeError(
                'FindMixedClusters requires at least two color channels')
        else:
            selectedChans = [0, 1]

        #rad_dlg = wx.NumberEntryDialog(None, 'Search Radius For Core Points', 'rad [nm]', 'rad [nm]', 125, 0, 9e9)
        #rad_dlg.ShowModal()
        searchRadius = 125.0  #rad_dlg.GetValue()
        #minPt_dlg = wx.NumberEntryDialog(None, 'Minimum Points To Be Core Point', 'min pts', 'min pts', 3, 0, 9e9)
        #minPt_dlg.ShowModal()
        minClumpSize = 3  #minPt_dlg.GetValue()

        #build a recipe programatically
        rec = Recipe()
        #split input according to colour channels
        rec.add_module(
            localisations.ExtractTableChannel(rec,
                                              inputName='input',
                                              outputName='chan0',
                                              channel=chans[0]))
        rec.add_module(
            localisations.ExtractTableChannel(rec,
                                              inputName='input',
                                              outputName='chan1',
                                              channel=chans[1]))

        #clump each channel
        rec.add_module(
            localisations.DBSCANClustering(rec,
                                           inputName='chan0',
                                           outputName='chan0_clumped',
                                           searchRadius=searchRadius,
                                           minClumpSize=minClumpSize))
        rec.add_module(
            localisations.DBSCANClustering(rec,
                                           inputName='chan1',
                                           outputName='chan1_clumped',
                                           searchRadius=searchRadius,
                                           minClumpSize=minClumpSize))

        #filter unclumped points
        rec.add_module(
            tablefilters.FilterTable(
                rec,
                inputName='chan0_clumped',
                outputName='chan0_cleaned',
                filters={'dbscanClumpID': [.5, sys.maxsize]}))
        rec.add_module(
            tablefilters.FilterTable(
                rec,
                inputName='chan1_clumped',
                outputName='chan1_cleaned',
                filters={'dbscanClumpID': [.5, sys.maxsize]}))

        #rejoin cleaned datasets
        rec.add_module(
            tablefilters.ConcatenateTables(rec,
                                           inputName0='chan0_cleaned',
                                           inputName1='chan1_cleaned',
                                           outputName='joined'))

        #clump on cleaded and rejoined data
        rec.add_module(
            localisations.DBSCANClustering(rec,
                                           inputName='joined',
                                           outputName='output',
                                           searchRadius=searchRadius,
                                           minClumpSize=minClumpSize))

        rec.namespace[
            'input'] = self.pipeline.output  #do it before configuring so that we already have the channe; names populated
        if not rec.configure_traits(view=rec.pipeline_view, kind='modal'):
            return  #handle cancel

        #run recipe
        joined_clumps = rec.execute()

        joined_clump_IDs = np.unique(joined_clumps['dbscanClumpID'])
        joined_clump_IDs = joined_clump_IDs[joined_clump_IDs >
                                            .5]  #reject unclumped points

        chan0_clump_IDs = np.unique(
            joined_clumps['dbscanClumpID'][joined_clumps['concatSource'] < .5])
        chan0_clump_IDs = chan0_clump_IDs[chan0_clump_IDs > .5]

        chan1_clump_IDs = np.unique(
            joined_clumps['dbscanClumpID'][joined_clumps['concatSource'] > .5])
        chan1_clump_IDs = chan1_clump_IDs[chan1_clump_IDs > .5]

        both_chans_IDS = [c for c in chan0_clump_IDs if c in chan1_clump_IDs]

        n_total_clumps = len(joined_clump_IDs)

        print('Total clumps: %i' % n_total_clumps)
        c0Ratio = float(len(chan0_clump_IDs)) / n_total_clumps
        print('fraction clumps with channel %i present: %f' %
              (selectedChans[0], c0Ratio))
        self.colocalizationRatios['Channel%iin%i%i' %
                                  (selectedChans[0], selectedChans[0],
                                   selectedChans[1])] = c0Ratio

        c1Ratio = float(len(chan1_clump_IDs)) / n_total_clumps
        print('fraction clumps with channel %i present: %f' %
              (selectedChans[1], c1Ratio))
        self.colocalizationRatios['Channel%iin%i%i' %
                                  (selectedChans[1], selectedChans[0],
                                   selectedChans[1])] = c1Ratio

        bothChanRatio = float(len(both_chans_IDS)) / n_total_clumps
        print('fraction of clumps with both channel %i and %i present: %f' %
              (selectedChans[0], selectedChans[1], bothChanRatio))
        self.colocalizationRatios['mixedClumps%i%i' %
                                  tuple(selectedChans)] = bothChanRatio

        self._rec = rec