class RuleBasedDispatcher(Dispatcher):
    """A dispatcher which dispatches polygon evaluating them with dispatching rules"""
    def __init__(self, rules, labels=None, timing=None, logger=SilentLogger()):
        """
        Parameters
        ----------
        rules: iterable (subtype: DispatchingRule)
        labels:
        timing:
        logger:
        """
        labels = labels if labels is not None else list(range(len(rules)))
        super(RuleBasedDispatcher, self).__init__(labels, timing=timing, logger=logger)
        self._rules = rules
        self._labels = labels

    def dispatch(self, image, polygon):
        return self.dispatch_batch(image, [polygon])[0]

    def dispatch_batch(self, image, polygons):
        poly_count = len(polygons)
        dispatch_labels = np.full((poly_count,), None, dtype=object)  # dispatch indexes
        remaining = np.arange(poly_count)  # remaining indexes to process
        # check which rule matched the polygons
        for i, (rule, label) in enumerate(zip(self._rules, self._labels)):
            if remaining.shape[0] == 0:  # if there are no more elements to evaluate
                break
            match, no_match = self._split_by_rule(image, rule, polygons, remaining)
            if len(match) > 0:  # skip if there are no match
                dispatch_labels[match] = label
                remaining = np.setdiff1d(remaining, match, assume_unique=True)
        return dispatch_labels

    @staticmethod
    def _split_by_rule(image, rule, polygons, poly_indexes):
        """Given a rule, splits all the poly_indexes list into two lists. The first list contains
        the indexes corresponding to the polygons that were evaluated True by the rule, the indexes that
        were evaluated False by the rule.

        Parameters
        ----------
        image: Image
            The image from which were extracted the polygons
        rule: DispatchingRule
            The rule with which the polygons must be evaluated
        polygons: iterable
            The list of polygons
        poly_indexes: iterable
            The indexes of the polygons from the list polygons to process
        timing: WorkflowTiming
            The timing object for computing the dispatching time

        Returns
        -------
        true_list: iterable (subtype: int)
            The indexes that were evaluated true
        false_list: iterable (subtype: int)
            The indexes that were evaluated false
        """
        polygons_to_evaluate = take(polygons, poly_indexes)
        eval_results = rule.evaluate_batch(image, polygons_to_evaluate)
        np_indexes = np.array(poly_indexes)
        return np_indexes[np.where(eval_results)], np_indexes[np.where(np.logical_not(eval_results))] class CatchAllRule(DispatchingRule):
    """A rule which evaluates all the polygons to True"""
    def evaluate(self, image, polygon):
        return True class SLDCWorkflowBuilder(WorkflowBuilder):
    """A class for building SLDC Workflow objects. When several instances of SLDCWorkflow should be built, they should
    be with the same Builder object, especially if the workflows should work in parallel.
    """
    def __init__(self):
        """Constructor for WorkflowBuilderObjects
        Parameters
        ----------
        n_jobs: int
            Number of jobs to use for executing the workflow
        """
        # Fields below are reset after each get()
        super(SLDCWorkflowBuilder, self).__init__()
        self._segmenter = None
        self._rules = None
        self._dispatching_labels = None
        self._one_shot_dispatcher = None
        self._classifiers = None
        self._parallel_dc = None
        self._reset()

    def _reset(self):
        """Reset the sldc workflow fields to their default values"""
        super(SLDCWorkflowBuilder, self)._reset()
        self._segmenter = None
        self._rules = []
        self._dispatching_labels = []
        self._one_shot_dispatcher = None
        self._classifiers = []
        self._parallel_dc = False

    def get_kwargs(self):
        """extends parent method with SLDCWorkflow specifics"""
        # define the dispatcher and classifier
        if self._one_shot_dispatcher is None:
            dispatcher = RuleBasedDispatcher(
                self._rules,
                labels=self._dispatching_labels,
                logger=self._logger
            )
        else:
            dispatcher = self._one_shot_dispatcher

        dispatcher_classifier = DispatcherClassifier(
            dispatcher,
            self._classifiers,
            logger=self._logger
        )

        return {
            "segmenter": self._segmenter,
            "dispatcher_classifier": dispatcher_classifier,
            "parallel_dispatch_classify": self._parallel_dc,
            **super().get_kwargs()
        }

    def set_parallel_dc(self, parallel_dc):
        """Specify whether the dispatching and classification will be parallelized at the workflow level (optional)
        Parameters
        ----------
        parallel_dc: boolean
            True for enabling parallelization of dispatching and classification at the workflow level

        Returns
        -------
        builder: SLDCWorkflowBuilder
            The builder
        """
        self._parallel_dc = parallel_dc
        return self

    def set_segmenter(self, segmenter):
        """Set the segmenter (mandatory)
        Parameters
        ----------
        segmenter: Segmenter
            The segmenter

        Returns
        -------
        builder: SLDCWorkflowBuilder
            The builder
        """
        self._segmenter = segmenter
        return self

    def add_classifier(self, rule, classifier, dispatching_label=None):
        """Add a classifier to which polygons can be dispatched (mandatory, at least on time).

        Parameters
        ----------
        rule: DispatchingRule
            The dispatching rule that matches the polygons to be dispatched to the classifier
        classifier: PolygonClassifier
            The polygon that classifies polygons
        dispatching_label: key (optional, default: None)
            The dispatching label for this classifier. By default, (n) is used where n is the number of rules and
            classifiers added before (n is formatted as a string)

        Returns
        -------
        builder: SLDCWorkflowBuilder
            The builder
        """
        if self._one_shot_dispatcher is not None:
            raise InvalidBuildingException("Cannot use a rule based dispatcher alongside a one shot dispatcher.")
        self._dispatching_labels.append(dispatching_label if dispatching_label is not None else str(len(self._rules)))
        self._rules.append(rule)
        self._classifiers.append(classifier)
        return self

    def add_catchall_classifier(self, classifier, dispatching_label="catchall"):
        """Add a classifier which is dispatched all the polygons that were note dispatched by the previously added
        classifiers.

        Parameters
        ----------
        classifier: PolygonClassifier
            The classifier
        dispatching_label: key (optional, default: "catchall")
            The dispatching label
        """
        if self._one_shot_dispatcher is not None:
            raise InvalidBuildingException("Cannot use a rule based dispatcher alongside a one shot dispatcher.")
        return self.add_classifier(CatchAllRule(), classifier, dispatching_label=dispatching_label)

    def set_one_shot_dispatcher(self, dispatcher, classifier_mapping):
        """Use the one shot user dispatching strategy and sets the dispatcher and classifiers.

        Parameters
        ----------
        dispatcher: Dispatcher
            A dispatcher
        classifier_mapping: dict (key: hashable, subtype: PolygonClassifier)
            Maps labels returned by the dispatcher with their corresponding classifiers.
        """
        if len(self._rules) > 0:
            raise InvalidBuildingException("Cannot use a one shot dispatcher alongside "
                                           "a rule based one (already defined {} rules).".format(len(self._rules)))

        # extract mapping and classifiers
        self._one_shot_dispatcher = dispatcher
        dispatcher.mapping, self._classifiers = zip(*[
            (label, classifier) for label, classifier in classifier_mapping.items()
        ])

    def get(self):
        if self._segmenter is None:
            raise MissingComponentException("Missing segmenter.")
        if self._tile_builder is None:
            raise MissingComponentException("Missing tile builder.")
        if self._one_shot_dispatcher is None and len(self._rules) == 0:
            raise MissingComponentException("Missing dispatching strategy. Either one shot or rule based "
                                            "dispatching must be used.")
        if len(self._classifiers) == 0:
            raise MissingComponentException("Missing classifiers.")
        workflow = SLDCWorkflow(**self.get_kwargs())
        self._reset()
        return workflow class WorkflowChainBuilder(object):
    """A class for building workflow chains objects
    """
    def __init__(self):
        self._first_workflow = None
        self._executors = None
        self._filters = None
        self._labels = None
        self._logger = None
        self._reset()

    def _reset(self):
        """Resets the builder so that it can build a new workflow chain
        """
        self._first_workflow = None
        self._executors = []
        self._filters = []
        self._labels = []
        self._logger = SilentLogger()

    def set_first_workflow(self, workflow, label=None):
        """Set the workflow that will process the full image
        Parameters
        ----------
        workflow: Workflow
            The workflow
        label: hashable (optional)
            The label identifying the workflow. If not set, this label is set to 0.

        Returns
        -------
        builder: WorkflowChainBuilder
            The builder
        """
        actual_label = 0 if label is None else label
        if self._first_workflow is None:
            self._labels.insert(0, actual_label)
        else:
            self._labels[0] = actual_label
        self._first_workflow = workflow
        return self

    def add_executor(self, workflow, filter=DefaultFilter(), label=None, logger=SilentLogger(), n_jobs=1):
        """Adds a workflow executor to be executed by the workflow chain.

        Parameters
        ----------
        workflow: Workflow
            The workflow object
        filter: PolygonFilter (optional, default: DefaultFilter)
            The polygon filter implementing the filtering of polygons of which the windows will be processed to
            the workflow.
        label: hashable (optional)
            The label identifying the executor. If not set, the number of the executor is used instead (starting at 1)
        logger: Logger (optional, default: SilentLogger)
            The logger to be used by the executor object
        n_jobs: int (optional, default: 1)
            The number of jobs for executing the workflow on the images

        Returns
        -------
        builder: WorkflowChainBuilder
            The builder
        """
        self._executors.append(WorkflowExecutor(workflow, logger=logger, n_jobs=n_jobs))
        self._filters.append(filter)
        actual_label = len(self._executors) if label is None else label
        self._labels.append(actual_label)
        return self

    def set_logger(self, logger):
        """Set the logger of the workflow chain

        Parameters
        ----------
        logger: Logger
            The logger

        Returns
        -------
        builder: WorkflowChainBuilder
            The builder
        """
        self._logger = logger
        return self

    def get(self):
        """Build the workflow chain with the set parameters
        Returns
        -------
        workflow: WorkflowChain
            The workflow chain

        Raises
        ------
        MissingComponentException:
            If some mandatory elements were not provided to the builder
        """
        if self._first_workflow is None:
            raise MissingComponentException("Missing first workflow.")
        if len(self._labels) != len(self._executors) + 1:
            raise MissingComponentException("The number of labels ({}) should be the".format(len(self._labels)) +
                                            " same as the number of workflows ({}).".format(len(self._executors) + 1))
        if len(self._filters) != len(self._executors):
            raise MissingComponentException("The number of filters ({}) should be the".format(len(self._filters)) +
                                            " same as the number of executors ({}).".format(len(self._executors)))

        chain = WorkflowChain(self._first_workflow, self._executors, self._filters, self._labels, logger=self._logger)
        self._reset()
        return chain class SSLWorkflowBuilder(WorkflowBuilder):
    """For building ssl workflows"""
    def __init__(self):
        """Constructor for WorkflowBuilderObjects
        Parameters
        ----------
        n_jobs: int
            Number of jobs to use for executing the workflow
        """
        # Fields below are reset after each get()
        super(SSLWorkflowBuilder, self).__init__()
        self._segmenter = None
        self._background_class = None
        self._reset()

    def _reset(self):
        """Reset the sldc workflow fields to their default values"""
        super(SSLWorkflowBuilder, self)._reset()
        self._segmenter = None
        self._background_class = -1

    def get_kwargs(self):
        """extends parent method with SLDCWorkflow specifics"""
        return {
            "segmenter": self._segmenter,
            "background_class": self._background_class,
            **super().get_kwargs()
        }

    def set_segmenter(self, segmenter):
        """Set the segmenter
        Parameters
        ----------
        segmenter: SemanticSegmenter
            The segmenter
        """
        self._segmenter = segmenter
        return self

    def set_background_class(self, background_class):
        """Set the background class
        Parameters
        ----------
        background_class: int
            The background class
        """
        self._background_class = background_class
        return self

    def get(self):
        if self._segmenter is None:
            raise MissingComponentException("Missing segmenter.")
        if self._tile_builder is None:
            raise MissingComponentException("Missing tile builder.")
        workflow = SSLWorkflow(**self.get_kwargs())
        self._reset()
        return workflow class WorkflowChain(Loggable):
    """A class for chaining the execution of several workflows on images
    """

    def __init__(self, workflow, executors, filters, workflow_labels=None, logger=SilentLogger()):
        """Constructor for WorkflowChain objects

        Parameters
        ----------
        workflow: Workflow
            The first workflow to apply to the full image
        executors: iterable (subtype: WorkflowExecutor, size: N)
            The executors encapsulating the execution of the workflows to apply after the first one;
        filters: iterable (subtype: PolygonFilter, size: N)
            The filters to use for filtering polygons before passing their windows to a workflow executor.
            Especially, the ith filter corresponds to the ith executor in 'executors'
        workflow_labels: iterable (subtype: hashable, size: N+1, optional, default: range(1, N+2))
            The labels identifying the various recorded workflows. The first one corresponds to the first workflow.
        logger: Logger (optional, default: a SilentLogger instance)
            A logger object
        """
        Loggable.__init__(self, logger)
        self._first_workflow = workflow
        self._executors = executors
        self._filters = filters
        self._labels = workflow_labels if workflow_labels is not None else range(1, len(filters)+2)
        self._chain_information = ChainInformation()

    def process(self, image):
        """Apply the processing chain on a given image.

        Parameters
        ----------
        image: Image
            The image to process

        Returns
        -------
        chain_information: ChainInformation
            A chain information object containing the generated objects
        """
        # Run 1st workflow and save its results in the collection
        self.logger.info("WorkflowChain : start chain on image ({} x {} x {}).".format(image.height, image.width,
                                                                                       image.channels))
        workflow_information = self._first_workflow.process(image)
        chain_info = ChainInformation()
        chain_info.append(self._labels[0], workflow_information)

        # Run subsequent workflows
        for i, executor in enumerate(self._executors):
            self.logger.info("WorkflowChain : run workflow executor {}.".format(i + 1))
            filtered = self._filters[i].windows(image, chain_info)
            workflow_information = executor.execute(filtered)
            chain_info.append(self._labels[i+1], workflow_information)

        return chain_info

    def process_batch(self, images):
        """Apply the processing chain to a bunch of images

        Parameters
        ----------
        images: iterable (subtype: Image, size: N)
            The image to process (can be a list, or an ImageProvider)

        Returns
        -------
        chain_infos: iterable (subtype: ChainInformation, size: N)
            The chain information objects generated by processing the given images
        """
        return [self.process(image) for image in images] class DefaultFilter(PolygonFilter):
    """A polygon filter which filters no polygon"""
    def filter(self, chain_information):
        return [p for p in chain_information.polygons] class PolygonClassifier(object):
    """A classifier that classifies polygons
    """
    __metaclass__ = ABCMeta

    @abstractmethod
    def predict(self, image, polygon):
        """Predict the class associated with the given polygon

        Parameters
        ----------
        image: Image
            The image the object of interest delimited by the polygon
        polygon: shapely.geometry.Polygon
            The polygon of which the class must be predicted

        Returns
        -------
        prediction: int
            An integer code indicating the predicted class
        probability: float (in [0,1])
            The prediction probability
        """
        pass

    def predict_batch(self, image, polygons):
        """Predict the classes associated with the given polygons

        Parameters
        ----------
        image: Image
            The image the objects of interest delimited by the polygons
        polygons: iterable (subtype: shapely.geometry.Polygon, size: N)
            The polygons of which the classes must be predicted

        Returns
        -------
        predictions: iterable (subtype: int, size: N)
            An iterable of integer codes indicating the predicted classes
        probabilities: iterable (subtype: int, range: [0,1], size: N)
            The probabilities associated with the classes predicted for each polygon
        """
        return zip(*[self.predict(image, polygon) for polygon in polygons]) class DispatchingRule(object):
    """An interface to be implemented by any class that defined a dispatching rule for polygons
    """
    __metaclass__ = ABCMeta

    @abstractmethod
    def evaluate(self, image, polygon):
        """Evaluate a polygon

        Parameters
        ----------
        image: Image
            The image from which is extracted the polygon
        polygon: shapely.geometry.Polygon
            The polygon to evaluate

        Returns
        -------
        result: bool
            True if the dispatching rule matched the polygon, False otherwise.
        """
        pass

    def evaluate_batch(self, image, polygons):
        """Evaluate the polygons

        Parameters
        ----------
        image: Image
            The image from which is extracted the polygons
        polygons: iterable (subtype: shapely.geometry.Polygon)
            The list of polygons to dispatch

        Returns
        -------
        result: iterable (subtype: bool)
            Iterable of which the ith element is True if the ith polygon is evaluated to True, False otherwise
        """
        return [self.evaluate(image, polygon) for polygon in polygons] class Dispatcher(Loggable):
    """A dispatcher is an object that for a given set of polygons and an image returns a dispatch index
    computed using some user defined logic.
    """
    __metaclass__ = ABCMeta

    def __init__(self, mapping=None, timing=None, logger=SilentLogger()):
        """
        Parameters
        ----------
        mapping: iterable (subtype: hashable)
            List containing the dispatch labels/indexes returned by the user defined procedure.
            This list is used by the dispatch_map to produce the dispatch indexes returned by
            the dispatcher (see dispatch_map documentation). By default assumes no mapping.
        timing: WorkflowTiming
            An optional workflow timing for computing dispatching time
        """
        super(Dispatcher, self).__init__(logger=logger)
        self._mapping, self._inverse_mapping = None, None  # to avoid duplicate initialization
        self.mapping = mapping
        self._timing = WorkflowTiming() if timing is None else timing

    @property
    def label_count(self):
        """Return the number of possible dispatch indexes/labels"""
        return len(self._mapping)

    def __len__(self):
        """Return the number of possible dispatch indexes/labels"""
        return self.label_count

    @property
    def timing(self):
        return self._timing

    @timing.setter
    def timing(self, timing):
        self._timing = timing

    @property
    def mapping(self):
        return self._mapping.keys()

    @mapping.setter
    def mapping(self, mapping):
        self._mapping = self._transform_mapping(mapping)
        self._inverse_mapping = mapping

    @staticmethod
    def _transform_mapping(mapping):
        return None if mapping is None else {v: i for i, v in enumerate(mapping)}

    @abstractmethod
    def dispatch(self, image, polygon):
        """Return the dispatch index/label for the given polygon and image

        Parameters
        ----------
        image: Image
            The image from which is extracted the polygon
        polygon: shapely.geometry.Polygon
            The polygon to dispatch

        Returns
        -------
        index: hashable
            The dispatch index/label. None if the polygon cannot be dispatched
        """
        pass

    def dispatch_batch(self, image, polygons):
        """Return the dispatch index/label for the all the given polygons from the image

        Parameters
        ----------
        image: Image
            The image from which is extracted the polygon
        polygons: iterable (subtype: shapely.geometry.Polygon)
            The polygons to dispatch

        Returns
        -------
        index: ndarray (subtype: hashable)
            The dispatch index/label
        """
        return np.array([self.dispatch(image, polygon) for polygon in polygons])

    def dispatch_map(self, image, polygons):
        """Transforms the dispatch indexes/labels returned by dispatch_batch to actual integer indexes defined by the
        mapping. Especially, let there be a mapping array M of size N containing mapped values M[i] (i in [0, N[), a
        polygon P dispatched to index k, then the corresponding index c returned by dispatch_map will be:
            - if k in M, then c will be i where M[i] = k
            - if k not in M, then c will be -1

        If no mapping is defined, then dispatch_map simply forwards the result of dispatch_batch.

        Parameters
        ----------
        image: Image
            The image from which is extracted the polygon
        polygons: iterable (subtype: shapely.geometry.Polygon)
            The polygons to dispatch

        Returns
        -------
        labels: ndarray (subtype: hashable)
            The dispatch indexes/labels
        index: ndarray (subtype: int)
            The mapped dispatch indexes/labels

        Notes
        -----
        If a timing object was provided at construction, it is used to compute the dispatching time
        """
        self.logger.i("Dispatcher: start dispatching.")
        dispatch_labels = self.dispatch_batch(image, polygons)
        self.logger.i("Dispatcher: end dispatching.")

        if self._mapping is None:  # no mapping defined
            return dispatch_labels, dispatch_labels

        # compute mapping indexes
        indexes = np.full(dispatch_labels.shape, -1, dtype=np.int32)
        for label, index in self._mapping.items():
            indexes[dispatch_labels == label] = index

        # report dispatching (TODO is it relevant not to report when the implementer hasn't defined any labels ?)
        not_dispatched = np.equal(indexes, -1)
        dispatched = np.logical_not(not_dispatched)
        dispatched_indexes, counts = np.unique(indexes[dispatched], return_counts=True)
        all_count = len(polygons)

        for index, count in zip(dispatched_indexes, counts):
            label = self._inverse_mapping[index]
            self.logger.i("Dispatcher: {}/{} polygons dispatched to '{}'.".format(count, all_count, label))
        self.logger.w("Dispatcher: {}/{} polygons not dispatched.".format(np.count_nonzero(not_dispatched), all_count))

        return dispatch_labels, indexes class MissingComponentException(Exception):
    """Thrown when a component is missing for building an object"""
    pass class InvalidBuildingException(Exception):
    """Thrown when a builder is about the get in an inconsistent state"""
    pass class DefaultTileBuilder(TileBuilder):
    """A tile builder which builds Tile objects"""
    def build(self, image, offset, width, height, polygon_mask=None):
        return Tile(image, offset, width, height, polygon_mask=polygon_mask) class Logger(object):
    """A base class loggers. A logger is an object which print or not the messages it is passed according to
    the verbosity level. Base class are responsible for defining the printing policy.
    """
    SILENT = 0
    ERROR = 1
    WARNING = 2
    INFO = 3
    DEBUG = 4

    def __init__(self, level, prefix=True, pid=True):
        """Build a logger object with the given level of verbosity
        Parameters
        ----------
        level: int
            Verbosity level
        prefix: boolean:
            True for displaying the prefix, false otherwise
        pid: boolean
            True for displaying the pid in the prefix, false for the tid
        """
        self._level = level
        self._prefix = prefix
        self._pid = pid

    @property
    def level(self):
        return self._level

    @level.setter
    def level(self, level):
        self._level = level

    def d(self, msg):
        """Alias for self.debug
        Parameters
        ----------
        msg: string
            The message to log
        """
        self.debug(msg)

    def debug(self, msg):
        """Logs a information message if the level of verbosity is above or equal DEBUG
        Parameters
        ----------
        msg: string
            The message to log
        """
        self._log(Logger.DEBUG, msg)

    def i(self, msg):
        """Alias for self.info
        Parameters
        ----------
        msg: string
            The message to log
        """
        self.info(msg)

    def info(self, msg):
        """Logs a information message if the level of verbosity is above or equal INFO
        Parameters
        ----------
        msg: string
            The message to log
        """
        self._log(Logger.INFO, msg)

    def w(self, msg):
        """Alias for self.warning
        Parameters
        ----------
        msg: string
            The message to log
        """
        self.warning(msg)

    def warning(self, msg):
        """Logs a information message if the level of verbosity is above or equal WARNING
        Parameters
        ----------
        msg: string
            The message to log
        """
        self._log(Logger.WARNING, msg)

    def e(self, msg):
        """Alias for self.error
        Parameters
        ----------
        msg: string
            The message to log
        """
        self.error(msg)

    def error(self, msg):
        """Logs a information message if the level of verbosity is above or equal ERROR
        Parameters
        ----------
        msg: string
            The message to log
        """
        self._log(Logger.ERROR, msg)

    def _log(self, level, msg):
        """Check the verbosity level, if it is above, actually prints the message
        Parameters
        ----------
        level: int
            Verbosity level of the message
        msg: string
            The message
        """
        if self._level >= level:
            formatted = self._format_msg(level, msg)
            self._print(formatted)

    @abstractmethod
    def _print(self, formatted_msg):
        pass

    def prefix(self, level):
        from datetime import datetime
        now = datetime.now().isoformat()
        if self._pid:
            pid = "{}".format(os.getpid()).zfill(6)
            fid = "pid:{}".format(pid)
        else:
            tid = "{}".format(threading.current_thread().ident).zfill(6)
            fid = "tid:{}".format(tid)
        return "[{}][{}][{}]".format(fid, now, self.level2str(level))

    @classmethod
    def level2str(cls, level):
        if level == cls.DEBUG:
            return "DEBUG"
        elif level == cls.WARNING:
            return "WARN "
        elif level == cls.ERROR:
            return "ERROR"
        else:  # info
            return "INFO "

    def _format_msg(self, level, msg):
        if self._prefix:
            rows = ["{} {}".format(self.prefix(level), row) for row in msg.splitlines()]
            return os.linesep.join(rows)
        else:
            return msg class StandardOutputLogger(Logger):
    """A logger printing the messages on the standard output
    """
    def _print(self, formatted_msg):
        print (formatted_msg) class SilentLogger(Logger):
    """A logger that ignore messages
    """
    def __init__(self, prefix=True):
        Logger.__init__(self, Logger.SILENT, prefix=prefix)

    def _print(self, formatted_msg):
        pass class Segmenter(SemanticSegmenter):
    """For backward-compatibility 
    
    Interface to be implemented by any class which implements a (binary) segmentation algorithm.
    
    Background pixels are represented by the value 0 ('black') while foreground ones are represented 
    by the value 255 ('white') by default.
    """
    __metaclass__ = ABCMeta

    def __init__(self, classes=None):
        super(Segmenter, self).__init__(classes=[0, 255] if classes is None else classes) class SLDCWorkflow(Workflow):
    """A class that coordinates various components of the SLDC workflow in order to detect objects and return
    their information.
    """
    TIMING_ROOT = "workflow.sldc"
    TIMING_DETECT = "detect"
    TIMING_DETECT_LOAD = "load"
    TIMING_DETECT_SEGMENT = "segment"
    TIMING_DETECT_LOCATE = "locate"
    TIMING_MERGE = "merge"
    TIMING_DC = "dispatch_classify"

    def __init__(self, segmenter, dispatcher_classifier, tile_builder, parallel_dispatch_classify=False, **kwargs):
        """Constructor for SLDCWorkflow objects

        Parameters
        ----------
        segmenter: Segmenter
            The segmenter implementing segmentation procedure to apply on tiles.
        tile_builder: TileBuilder
            A tile builder
        dispatcher_classifier: DispatcherClassifier
            The dispatcher classifier object for dispatching polygons and classify them.
        parallel_dispatch_classify: boolean (optional, default: False)
            True for executing dispatching and classification in parallel, False for sequential.
        """
        super(SLDCWorkflow, self).__init__(tile_builder, **kwargs)
        self._segmenter = segmenter
        self._locator = BinaryLocator()
        self._merger = SemanticMerger(self.dist_tolerance)
        self._dispatch_classifier = dispatcher_classifier
        self._parallel_dispatch_classify = parallel_dispatch_classify

    def process(self, image):
        """Process function"""
        timing = WorkflowTiming(root=SLDCWorkflow.TIMING_ROOT)
        tile_topology = self._tile_topology(image)

        # segment locate
        self.logger.info("SLDCWorkflow : start segment/locate.")
        with timing.cm(SLDCWorkflow.TIMING_DETECT):
            tiles, tile_polygons = self._segment_locate(tile_topology, timing)
        self.logger.info(
            "SLDCWorkflow : end segment/locate." + os.linesep +
            "SLDCWorkflow : {} tile(s) processed in {} s.".format(len(tiles), timing.total(SLDCWorkflow.TIMING_DETECT)) + os.linesep +
            "SLDCWorkflow : {} polygon(s) found on those tiles.".format(sum([len(polygons) for polygons in tile_polygons]))
        )

        # merge
        self.logger.info("SLDCWorkflow : start merging")
        with timing.cm(SLDCWorkflow.TIMING_MERGE):
            polygons = self._merger.merge(tiles, tile_polygons, tile_topology)

        self.logger.info(
            "SLDCWorkflow : end merging." + os.linesep +
            "SLDCWorkflow : {} polygon(s) found.".format(len(polygons)) + os.linesep +
            "SLDCWorkflow : executed in {} s.".format(timing.total(SLDCWorkflow.TIMING_MERGE))
        )

        # dispatch classify
        self.logger.info("SLDCWorkflow : start dispatch/classify.")
        with timing.cm(SLDCWorkflow.TIMING_DC):
            pred, proba, dispatch_indexes = self._dispatch_classify(image, polygons, timing)
        self.logger.info(
            "SLDCWorkflow : end dispatch/classify.\n" +
            "SLDCWorkflow : executed in {} s.".format(timing.total(SLDCWorkflow.TIMING_DC))
        )

        return WorkflowInformation(polygons, pred, timing, dispatches=(dispatch_indexes, "dispatch"), probas=(proba, "proba"))

    def _segment_locate(self, tile_topology, timing):
        """Execute the segment locate phase
        Parameters
        ----------
        tile_topology: TileTopology
            A tile topology
        timing: WorkflowTiming
            A workflow timing object for computing time

        Returns
        -------
        tiles: iterable (size: n, subtype: int) 
            Iterable containing the tiles ids
        tile_polygons: iterable (size: n, subtype: iterable of Polygon objects))
            The iterable at index i contains the polygons and pixel values found in the tile having index tiles[i]
        """
        # partition the tiles into batches for submitting them to processes
        tiles, tile_polygons = _parallel_segment_locate(
            self.pool,
            segmenter=self._segmenter,
            locator=self._locator,
            logger=self.logger,
            tile_topology=tile_topology,
            timing=timing
        )
        return tiles, list(map(lambda l: [t[0] for t in l], tile_polygons))

    def _dispatch_classify(self, image, polygons, timing):
        """Execute dispatching and classification on several processes

        Parameters
        ----------
        image: Image
            The image to process
        polygons: iterable (subtype: shapely.geometry.Polygon, size: N)
            The polygons to process
        timing: WorkflowTiming
            The workflow timing object to which must be appended the execution times

        Returns
        -------
        predictions: iterable (subtype: int|None, size: N)
            A list of integer codes indicating the predicted classes.
            If none of the dispatching rules matched the polygon, the prediction associated with it is the one produced
            by the fail callback for the given polygon. Especially, if no fail callback was registered, None is
            returned.
        probabilities: iterable (subtype: float, range: [0,1], size: N)
            The probabilities associated with each predicted classes (0.0 for polygons that were not dispatched)
        dispatches: iterable (size: N)
            An iterable containing the identifiers of the rule that matched the polygons. If dispatching labels were
            provided at construction, those are used to identify the rules. Otherwise, the integer indexes of the rules
            in the list provided at construction are used. Polygons that weren't matched by any rule are returned -1 as
            dispatch index.
        """
        if len(polygons) == 0:
            return np.array([]), np.array([]), np.array([])

        timing_root = ".".join([SLDCWorkflow.TIMING_ROOT, SLDCWorkflow.TIMING_DC])

        # disable parallel processing if user
        pool = self.pool if self._parallel_dispatch_classify else Parallel(n_jobs=1)
        n_jobs = self.n_jobs if self._parallel_dispatch_classify else 1

        batches = batch_split(n_jobs, polygons)
        results = pool(delayed(_dc_with_timing)(self._dispatch_classifier, image, batch, timing_root) for batch in batches)
        predictions, probabilities, dispatch, timings = zip(*results)

        # flatten
        predictions = [pred for preds in predictions for pred in preds]
        probabilities = [proba for probs in probabilities for proba in probs]
        dispatch = [disp for disps in dispatch for disp in disps]

        # merge timings
        for curr_timing in timings:
            timing.merge(curr_timing)

        return predictions, probabilities, dispatch class SSLWorkflow(Workflow):
    """SSL stands for Semantic-Segment-Locate. Detection is performed by a semantic segmentation algorithm.
    """
    TIMING_ROOT = "workflow.ssl"
    TIMING_DETECT = "detect"
    TIMING_DETECT_LOAD = "load"
    TIMING_DETECT_SEGMENT = "segment"
    TIMING_DETECT_LOCATE = "locate"
    TIMING_MERGE = "merge"

    def __init__(self, segmenter, tile_builder, background_class=-1, **kwargs):
        """Constructor
        Parameters
        ----------
        segmenter: SemanticSegmenter
            The semantic segmenter
        tile_builder: TileBuilder
            A tile builder
        background_class: int (default: -1)
            The background class not to locate (-1)
        """
        super(SSLWorkflow, self).__init__(tile_builder, **kwargs)
        self._segmenter = segmenter
        self._locator = SemanticLocator(background=background_class)
        self._merger = SemanticMerger(tolerance=self.dist_tolerance)

    def process(self, image):
        """Process function"""
        timing = WorkflowTiming(root=SSLWorkflow.TIMING_ROOT)
        tile_topology = self._tile_topology(image)

        # segment locate
        self.logger.info("SLDCWorkflow : start segment/locate.")
        with timing.cm(SSLWorkflow.TIMING_DETECT):
            tiles, tile_polygons, tile_labels = self._segment_locate(tile_topology, timing)
        self.logger.info(
            "SLDCWorkflow : end segment/locate." + os.linesep +
            "SLDCWorkflow : {} tile(s) processed in {} s.".format(len(tiles), timing.total(SSLWorkflow.TIMING_DETECT)) + os.linesep +
            "SLDCWorkflow : {} polygon(s) found on those tiles.".format(sum([len(polygons) for polygons in tile_polygons]))
        )

        # merge
        self.logger.info("SLDCWorkflow : start merging")
        with timing.cm(SSLWorkflow.TIMING_MERGE):
            polygons, labels = self._merger.merge(tiles, tile_polygons, tile_topology, labels=tile_labels)
        self.logger.info(
            "SLDCWorkflow : end merging." + os.linesep +
            "SLDCWorkflow : {} polygon(s) found.".format(len(polygons)) + os.linesep +
            "SLDCWorkflow : executed in {} s.".format(timing.total(SSLWorkflow.TIMING_MERGE))
        )

        return WorkflowInformation(polygons, labels, timing)

    def _segment_locate(self, tile_topology, timing):
        """Execute the segment locate phase
        Parameters
        ----------
        tile_topology: TileTopology
            A tile topology
        timing: WorkflowTiming
            A workflow timing object for computing time

        Returns
        -------
        tiles: iterable (size: n, subtype: int) 
            Iterable containing the tiles ids
        tile_polygons: iterable (size: n, subtype: iterable of Polygon objects))
            The iterable at index i contains the polygons and pixel values found in the tile having index tiles[i]
        """
        tiles, tile_polygons_labels = _parallel_segment_locate(
            self.pool,
            segmenter=self._segmenter,
            locator=self._locator,
            logger=self.logger,
            tile_topology=tile_topology,
            timing=timing
        )
        tile_polygons = list(map(lambda l: [t[0] for t in l], tile_polygons_labels))
        tile_labels = list(map(lambda l: [t[1] for t in l], tile_polygons_labels))
        return tiles, tile_polygons, tile_labels
from unittest import TestCase
from sldc.dispatcher import RuleBasedDispatcher, CatchAllRule
from sldc import SLDCWorkflowBuilder, MissingComponentException, Segmenter, DefaultTileBuilder, Dispatcher, \
    PolygonClassifier, SLDCWorkflow, DispatchingRule, InvalidBuildingException, StandardOutputLogger, Logger, \
    SSLWorkflow, SSLWorkflowBuilder, SilentLogger, WorkflowChainBuilder, DefaultFilter, WorkflowChain
        builder = SLDCWorkflowBuilder()
        builder.set_tile_size(512, 768)
        builder.set_overlap(3)
        builder.set_distance_tolerance(5)
        builder.set_n_jobs(5)
        builder.set_logger(logger)
        builder.set_parallel_dc(True)
        builder.set_tile_builder(None)

        with self.assertRaises(MissingComponentException):
            builder.get()

        builder.set_segmenter(segmenter)

        with self.assertRaises(MissingComponentException):
            builder.get()

        builder.set_default_tile_builder()

        with self.assertRaises(MissingComponentException):
            builder.get()

        builder.set_one_shot_dispatcher(dispatcher, {"default": classifier})

        with self.assertRaises(InvalidBuildingException):
            builder.add_classifier(rule, classifier, dispatching_label="default")

        with self.assertRaises(InvalidBuildingException):
            builder.add_catchall_classifier(classifier, dispatching_label="default")

        workflow = builder.get()
        self.assertIsInstance(workflow, SLDCWorkflow)
        self.assertEqual(workflow._segmenter, segmenter)
        self.assertEqual(workflow._n_jobs, 5)
        self.assertEqual(workflow._tile_overlap, 3)
        self.assertEqual(workflow._tile_max_height, 512)
        self.assertEqual(workflow._tile_max_width, 768)
        self.assertEqual(workflow.logger, logger)
        self.assertIsInstance(workflow._tile_builder, DefaultTileBuilder)
        self.assertEqual(workflow._dispatch_classifier._dispatcher, dispatcher)
        self.assertEqual(len(workflow._dispatch_classifier._classifiers), 1)
        self.assertEqual(workflow._dispatch_classifier._classifiers[0], classifier)

    def testSLDCWorkflowWithRuleDispatcher(self):
        # pre build components
        segmenter = DumbSegmenter()
        tile_builder = DefaultTileBuilder()
        dispatcher = DumbDispatcher()
        classifier1 = DumbClassifier()
        classifier2 = DumbClassifier()
        rule1 = DumbRule()
        rule2 = DumbRule()

        builder = SLDCWorkflowBuilder()
        builder.set_segmenter(segmenter)
        builder.set_tile_builder(tile_builder)
        builder.add_classifier(rule1, classifier1)
        builder.add_classifier(rule2, classifier2)

        with self.assertRaises(InvalidBuildingException):
            builder.set_one_shot_dispatcher(dispatcher, {"default": classifier1})

        workflow = builder.get()
        self.assertIsInstance(workflow, SLDCWorkflow)
        self.assertEqual(len(workflow._dispatch_classifier._classifiers), 2)
        self.assertEqual(workflow._dispatch_classifier._classifiers[0], classifier1)
        self.assertEqual(workflow._dispatch_classifier._classifiers[1], classifier2)
        self.assertIsInstance(workflow._dispatch_classifier._dispatcher, RuleBasedDispatcher)
        self.assertEqual(len(workflow._dispatch_classifier._dispatcher._rules), 2)
        self.assertEqual(workflow._dispatch_classifier._dispatcher._rules[0], rule1)
        self.assertEqual(workflow._dispatch_classifier._dispatcher._rules[1], rule2)

    def testSLDCWorkflowWithCatchAllClassifier(self):
        # pre build components
        segmenter = DumbSegmenter()
        tile_builder = DefaultTileBuilder()
        dispatcher = DumbDispatcher()
        classifier = DumbClassifier()

        builder = SLDCWorkflowBuilder()
        builder.set_segmenter(segmenter)
        builder.set_tile_builder(tile_builder)

        builder.add_catchall_classifier(classifier)

        with self.assertRaises(InvalidBuildingException):
            builder.set_one_shot_dispatcher(dispatcher, {"default": classifier})

        workflow = builder.get()
        self.assertIsInstance(workflow, SLDCWorkflow)
        self.assertIsInstance(workflow._dispatch_classifier._dispatcher, RuleBasedDispatcher)
        self.assertEqual(len(workflow._dispatch_classifier._classifiers), 1)
        self.assertEqual(workflow._dispatch_classifier._classifiers[0], classifier)
        self.assertIsInstance(workflow._dispatch_classifier._dispatcher, RuleBasedDispatcher)
        self.assertEqual(len(workflow._dispatch_classifier._dispatcher._rules), 1)
        self.assertIsInstance(workflow._dispatch_classifier._dispatcher._rules[0], CatchAllRule)

    def testSSLWorkflowBuilder(self):
        segmenter = DumbSegmenter()
        builder = SSLWorkflowBuilder()
        builder.set_n_jobs(5)
        builder.set_tile_size(512, 768)
        builder.set_overlap(3)
        builder.set_distance_tolerance(7)
        builder.set_background_class(5)
        builder.set_logger(StandardOutputLogger(Logger.DEBUG))

        with self.assertRaises(MissingComponentException):
            builder.get()

        builder.set_segmenter(segmenter)
        builder.set_default_tile_builder()

        workflow = builder.get()
        self.assertIsInstance(workflow, SSLWorkflow)
        self.assertEqual(workflow._n_jobs, 5)
        self.assertEqual(workflow._tile_overlap, 3)
        self.assertEqual(workflow._tile_max_height, 512)
        self.assertEqual(workflow._tile_max_width, 768)
        self.assertIsInstance(workflow._tile_builder, DefaultTileBuilder)

        self.assertIsInstance(workflow.logger, StandardOutputLogger)