import logging
import os
import numpy as np
import networkx as nx
from hytra.pluginsystem.plugin_manager import TrackingPluginManager
import hytra.core.probabilitygenerator as probabilitygenerator
import hytra.core.jsongraph
from hytra.core.jsongraph import negLog, listify, JsonTrackingGraph
from hytra.util.progressbar import DefaultProgressVisitor
from hytra.core.splittracking import SplitTracking


logger = logging.getLogger(__name__)


class MergerResolverException(Exception):
    pass


class MergerResolver(object):
    """
    Base class for all merger resolving implementations. Use one of the derived classes
    that handle reading/writing data to the respective sources.
    """

    def __init__(
        self,
        pluginPaths=[os.path.abspath("../hytra/plugins")],
        numSplits=None,
        verbose=False,
        progressVisitor=DefaultProgressVisitor(),
    ):
        self.unresolvedGraph = None
        self.resolvedGraph = None
        self.mergersPerTimestep = None
        self.detectionsPerTimestep = None
        self.pluginManager = TrackingPluginManager(verbose=verbose, pluginPaths=pluginPaths)
        self.mergerResolverPlugin = self.pluginManager.getMergerResolver()
        self.numSplits = numSplits

        # should be filled by constructors of derived classes!
        self.model = None
        self.result = None
        self.progressVisitor = progressVisitor

    def _createUnresolvedGraph(self, divisionsPerTimestep, mergersPerTimestep, mergerLinks, withFullGraph=False):
        """
        Set up a networkx graph consisting of mergers that need to be resolved (not resolved yet!)
        and their direct neighbors.

        ** returns ** the `unresolvedGraph`
        """

        self.unresolvedGraph = nx.DiGraph()

        def source(timestep, link):
            return int(timestep) - 1, link[0]

        def target(timestep, link):
            return int(timestep), link[1]

        # Recompute full graph
        if withFullGraph:
            self.unresolvedGraph = self.hypothesesGraph._graph.copy()

            # Add division parameter to nodes
            # TODO: Add the division parameter only to nodes that contain divisions (we're already doing these with 'count')
            lastframe = max(divisionsPerTimestep.keys(), key=int)
            for node in self.unresolvedGraph.nodes():
                timestep, idx = node

                if divisionsPerTimestep is not None and int(timestep) < int(lastframe):
                    division = idx in divisionsPerTimestep[str(timestep + 1)]  # +1 screams for lastframe condition.
                else:
                    division = False

                self.unresolvedGraph.nodes[node]["division"] = division

            # Add count parameter to nodes
            for t, link in mergerLinks:
                for node in [source(t, link), target(t, link)]:
                    timestep, idx = node
                    if idx in mergersPerTimestep[str(timestep)]:
                        count = mergersPerTimestep[str(timestep)][idx]
                        self.unresolvedGraph.nodes[node]["count"] = count

        # Recompute graph only with merger nodes and neighbors
        else:

            def addNode(node):
                """add a node to the unresolved graph and fill in the properties `division` and `count`"""
                intT, idx = node

                lastframe = max(divisionsPerTimestep.keys(), key=int)
                if divisionsPerTimestep is not None and int(intT) < int(lastframe):
                    division = idx in divisionsPerTimestep[str(intT + 1)]  # +1 screams for lastframe condition.
                else:
                    division = False
                count = 1
                if idx in mergersPerTimestep[str(intT)]:
                    if division is True:
                        raise MergerResolverException(
                            "Condition violation: Encountered object classified as merger AND division. "
                            "Retrain your classifiers using Uncertainty Layer to obtain less ambiguous probabilities "
                            "for divisions and mergers (object-count > 1)."
                        )

                    count = mergersPerTimestep[str(intT)][idx]
                self.unresolvedGraph.add_node(node, division=division, count=count)

            # add nodes
            for t, link in mergerLinks:
                for n in [source(t, link), target(t, link)]:
                    if not self.unresolvedGraph.has_node(n):
                        addNode(n)
                self.unresolvedGraph.add_edge(source(t, link), target(t, link))

        return self.unresolvedGraph

    def _prepareResolvedGraph(self):
        """

        ** returns ** the `resolvedGraph`
        """
        self.resolvedGraph = self.unresolvedGraph.copy()

        return self.resolvedGraph

    def _readLabelImage(self, timeframe):
        """
        Should return the labelimage for the given timeframe
        """
        raise NotImplementedError()

    def _fitAndRefineNodes(self, detectionsPerTimestep, mergersPerTimestep, timesteps):
        """
        Update segmentation of mergers (nodes in unresolvedGraph) from first timeframe to last
        and create new nodes in `resolvedGraph`. Links to merger nodes are duplicated to all new nodes.

        Uses the mergerResolver plugin to update the segmentations in the labelImages.
        """

        intTimesteps = [int(t) for t in timesteps]
        intTimesteps.sort()

        for intT in intTimesteps:
            t = str(intT)
            # use image provider plugin to load labelimage
            labelImage = self._readLabelImage(int(t))
            nextObjectId = labelImage.max() + 1

            for idx in detectionsPerTimestep[t]:
                node = (intT, idx)
                if node not in self.resolvedGraph:
                    continue

                count = 1
                if idx in mergersPerTimestep[t]:
                    count = mergersPerTimestep[t][idx]
                logger.debug("Looking at node {} in timestep {} with count {}".format(idx, t, count))

                # collect initializations from incoming
                initializations = []
                for predecessor, _ in self.unresolvedGraph.in_edges(node):
                    initializations.extend(self.unresolvedGraph.nodes[predecessor]["fits"])
                # TODO: what shall we do if e.g. a 2-merger and a single object merge to 2 + 1,
                # so there are 3 initializations for the 2-merger, and two initializations for the 1 merger?
                # What does pgmlink do in that case?

                # use merger resolving plugin to fit `count` objects, also updates labelimage!
                fittedObjects = list(
                    self.mergerResolverPlugin.resolveMerger(labelImage, idx, nextObjectId, count, initializations)
                )
                assert len(fittedObjects) == count

                # split up node if count > 1, duplicate incoming and outgoing arcs
                if count > 1:
                    for idx in range(nextObjectId, nextObjectId + count):
                        newNode = (intT, idx)
                        self.resolvedGraph.add_node(newNode, division=False, count=1, origin=node)

                        for e in self.unresolvedGraph.out_edges(node):
                            self.resolvedGraph.add_edge(newNode, e[1])
                        for e in self.unresolvedGraph.in_edges(node):
                            if "newIds" in self.unresolvedGraph.nodes[e[0]]:
                                for newId in self.unresolvedGraph.nodes[e[0]]["newIds"]:
                                    self.resolvedGraph.add_edge((e[0][0], newId), newNode)
                            else:
                                self.resolvedGraph.add_edge(e[0], newNode)

                    self.resolvedGraph.remove_node(node)
                    self.unresolvedGraph.nodes[node]["newIds"] = range(nextObjectId, nextObjectId + count)
                    nextObjectId += count

                # each unresolved node stores its fitted shape(s) to be used
                # as initialization in the next frame, this way division duplicates
                # and de-merged nodes in the resolved graph do not need to store a fit as well
                self.unresolvedGraph.nodes[node]["fits"] = fittedObjects

        # import matplotlib.pyplot as plt
        # nx.draw_networkx(resolvedGraph)
        # plt.savefig("/Users/chaubold/test.pdf")

    def _minCostMaxFlowMergerResolving(self, objectFeatures, transitionClassifier=None, transitionParameter=5.0):
        """
        Find the optimal assignments within the `resolvedGraph` by running min-cost max-flow from the
        `dpct` module.

        Converts the `resolvedGraph` to our JSON model structure, predicts the transition probabilities
        either using the given transitionClassifier, or using distance-based probabilities.

        **returns** a `nodeFlowMap` and `arcFlowMap` holding information on the usage of the respective nodes and links

        **Note:** cannot use `networkx` flow methods because they don't work with floating point weights.
        """

        trackingGraph = JsonTrackingGraph(progressVisitor=self.progressVisitor)
        for node in self.resolvedGraph.nodes():
            additionalFeatures = {}
            additionalFeatures["nid"] = node

            # nodes with no in/out
            numStates = 2

            if len(self.resolvedGraph.in_edges(node)) == 0:
                # division nodes with no incoming arcs offer 2 units of flow without the need to de-merge
                if (
                    node in self.unresolvedGraph.nodes()
                    and self.unresolvedGraph.nodes[node]["division"]
                    and len(self.unresolvedGraph.out_edges(node)) == 2
                ):
                    numStates = 3
                additionalFeatures["appearanceFeatures"] = [[i ** 2 * 0.01] for i in range(numStates)]
            if len(self.resolvedGraph.out_edges(node)) == 0:
                assert (
                    numStates == 2
                )  # division nodes with no incoming should have outgoing, or they shouldn't show up in resolved graph
                additionalFeatures["disappearanceFeatures"] = [[i ** 2 * 0.01] for i in range(numStates)]

            features = [[i ** 2] for i in range(numStates)]
            uuid = trackingGraph.addDetectionHypotheses(features, **additionalFeatures)
            self.resolvedGraph.nodes[node]["id"] = uuid

        for edge in self.resolvedGraph.edges():
            src = self.resolvedGraph.nodes[edge[0]]["id"]
            dest = self.resolvedGraph.nodes[edge[1]]["id"]

            featuresAtSrc = objectFeatures[edge[0]]
            featuresAtDest = objectFeatures[edge[1]]

            if transitionClassifier is not None:
                try:
                    featVec = self.pluginManager.applyTransitionFeatureVectorConstructionPlugins(
                        featuresAtSrc,
                        featuresAtDest,
                        transitionClassifier.selectedFeatures,
                    )
                except:
                    logger.error("Could not compute transition features of link {}->{}:".format(src, dest))
                    logger.error(featuresAtSrc)
                    logger.error(featuresAtDest)
                    raise
                featVec = np.expand_dims(np.array(featVec), axis=0)
                probs = transitionClassifier.predictProbabilities(featVec)[0]
            else:
                dist = np.linalg.norm(featuresAtDest["RegionCenter"] - featuresAtSrc["RegionCenter"])
                prob = np.exp(-dist / transitionParameter)
                probs = [1.0 - prob, prob]

            trackingGraph.addLinkingHypotheses(src, dest, listify(negLog(probs)))

        # Set TraxelToUniqueId on resolvedGraph's json graph
        uuidToTraxelMap = {}
        traxelIdPerTimestepToUniqueIdMap = {}

        for node in self.resolvedGraph.nodes():
            uuid = self.resolvedGraph.nodes[node]["id"]
            uuidToTraxelMap[uuid] = [node]

            for t in uuidToTraxelMap[uuid]:
                traxelIdPerTimestepToUniqueIdMap.setdefault(str(t[0]), {})[str(t[1])] = uuid

        trackingGraph.setTraxelToUniqueId(traxelIdPerTimestepToUniqueIdMap)

        # track
        import dpct

        weights = {"weights": [1, 1, 1, 1]}

        if not self.numSplits:
            mergerResult = dpct.trackMaxFlow(trackingGraph.model, weights)
        else:
            logger.info("Running split tracking with {} splits.".format(self.numSplits))
            mergerResult = SplitTracking.trackFlowBasedWithSplits(
                trackingGraph.model,
                weights,
                numSplits=self.numSplits,
                withMergerResolver=True,
            )

        # transform results to dictionaries that can be indexed by id or (src,dest)
        nodeFlowMap = dict([(int(d["id"]), int(d["value"])) for d in mergerResult["detectionResults"]])
        arcFlowMap = dict([((int(l["src"]), int(l["dest"])), int(l["value"])) for l in mergerResult["linkingResults"]])

        return nodeFlowMap, arcFlowMap

    def _refineModel(
        self,
        uuidToTraxelMap,
        traxelIdPerTimestepToUniqueIdMap,
        mergerNodeFilter,
        mergerLinkFilter,
    ):
        """
        Take the `self.model` (JSON format) with mergers, remove the merger nodes, but add new
        de-merged nodes and links. Also updates `traxelIdPerTimestepToUniqueIdMap` locally and in the resulting file,
        such that the traxel IDs match the new connected component IDs in the refined images.

        `mergerNodeFilter` and `mergerLinkFilter` are methods that can filter merger detections
        and links from the respective lists in the `model` dict.

        **Returns** the updated `model` dictionary, which is the same as the input `model` (works in-place)
        """

        # remove merger detections
        self.model["segmentationHypotheses"] = [
            seg for seg in self.model["segmentationHypotheses"] if mergerNodeFilter(seg)
        ]

        # remove merger links
        self.model["linkingHypotheses"] = [link for link in self.model["linkingHypotheses"] if mergerLinkFilter(link)]

        # insert new nodes and update UUID to traxel map
        nextUuid = max(uuidToTraxelMap.keys()) + 1
        for node in self.unresolvedGraph.nodes():
            if "count" in self.unresolvedGraph.nodes[node] and self.unresolvedGraph.nodes[node]["count"] > 1:
                newIds = self.unresolvedGraph.nodes[node]["newIds"]
                del traxelIdPerTimestepToUniqueIdMap[str(node[0])][str(node[1])]
                for newId in newIds:
                    newDetection = {}
                    newDetection["id"] = nextUuid
                    newDetection["timestep"] = [node[0], node[0]]
                    self.model["segmentationHypotheses"].append(newDetection)
                    traxelIdPerTimestepToUniqueIdMap[str(node[0])][str(newId)] = nextUuid
                    nextUuid += 1

        # insert new links
        for edge in self.resolvedGraph.edges():
            newLink = {}
            newLink["src"] = traxelIdPerTimestepToUniqueIdMap[str(edge[0][0])][str(edge[0][1])]
            newLink["dest"] = traxelIdPerTimestepToUniqueIdMap[str(edge[1][0])][str(edge[1][1])]
            self.model["linkingHypotheses"].append(newLink)

        # save
        return self.model

    def _refineResult(
        self,
        nodeFlowMap,
        arcFlowMap,
        traxelIdPerTimestepToUniqueIdMap,
        mergerNodeFilter,
        mergerLinkFilter,
    ):
        """
        Update the `self.result` dict by removing the mergers and adding the refined nodes and links.

        Operates on a `result` dictionary in our JSON result style with mergers,
        the resolved and unresolved graph as well as
        the `nodeFlowMap` and `arcFlowMap` obtained by running tracking on the `resolvedGraph`.

        Updates the `result` dictionary so that all merger nodes are removed but the new nodes
        are contained with the appropriate links and values.

        `mergerNodeFilter` and `mergerLinkFilter` are methods that can filter merger detections
        and links from the respective lists in the `result` dict.

        **Returns** the updated `result` dict, which is the same as the input `result` (works in-place)
        """

        # filter merger edges
        self.result["detectionResults"] = [r for r in self.result["detectionResults"] if mergerNodeFilter(r)]
        self.result["linkingResults"] = [r for r in self.result["linkingResults"] if mergerLinkFilter(r)]

        # add new nodes
        for node in self.unresolvedGraph.nodes():
            if "count" in self.unresolvedGraph.nodes[node] and self.unresolvedGraph.nodes[node]["count"] > 1:
                newIds = self.unresolvedGraph.nodes[node]["newIds"]
                for newId in newIds:
                    uuid = traxelIdPerTimestepToUniqueIdMap[str(node[0])][str(newId)]
                    resolvedNode = (node[0], newId)
                    resolvedResultId = self.resolvedGraph.nodes[resolvedNode]["id"]
                    newDetection = {"id": uuid, "value": nodeFlowMap[resolvedResultId]}
                    self.result["detectionResults"].append(newDetection)

        # add new links
        for edge in self.resolvedGraph.edges():
            newLink = {}
            newLink["src"] = traxelIdPerTimestepToUniqueIdMap[str(edge[0][0])][str(edge[0][1])]
            newLink["dest"] = traxelIdPerTimestepToUniqueIdMap[str(edge[1][0])][str(edge[1][1])]
            srcId = self.resolvedGraph.nodes[edge[0]]["id"]
            destId = self.resolvedGraph.nodes[edge[1]]["id"]
            newLink["value"] = arcFlowMap[(srcId, destId)]
            self.result["linkingResults"].append(newLink)

        return self.result

    def _exportRefinedSegmentation(self, timesteps):
        """
        Store the resulting label images, if needed.

        `labelImages` is a dictionary with str(timestep) as keys.
        """
        pass

    def _computeObjectFeatures(self, timesteps):
        """
        Return the features per object as nested dictionaries:
        { (int(Timestep), int(Id)):{ "FeatureName" : np.array(value), "NextFeature": ...} }
        """
        pass

    # ------------------------------------------------------------
    def run(self, transition_classifier_filename=None, transition_classifier_path=None):
        """
        Run merger resolving

        1. find mergers in the given model and result
        2. build graph of the unresolved (merger) nodes and their direct neighbors
        3. use a mergerResolving plugin to refine the merger nodes and their segmentation
        4. run min-cost max-flow tracking to find the fate of all the de-merged objects
        5. export refined segmentation, update member variables `model` and `result`

        **Returns** a nested dictionary, indexed first by time, then object Id, containing a list of new segmentIDs per merger
        """

        traxelIdPerTimestepToUniqueIdMap, uuidToTraxelMap = hytra.core.jsongraph.getMappingsBetweenUUIDsAndTraxels(
            self.model
        )
        # timesteps = [t for t in traxelIdPerTimestepToUniqueIdMap.keys()]
        # there might be empty frames. We want them as output too.
        timesteps = [
            str(t)
            for t in range(
                int(min(traxelIdPerTimestepToUniqueIdMap.keys())),
                max([int(idx) for idx in traxelIdPerTimestepToUniqueIdMap.keys()]) + 1,
            )
        ]

        mergers, detections, links, divisions = hytra.core.jsongraph.getMergersDetectionsLinksDivisions(
            self.result, uuidToTraxelMap
        )

        # ------------------------------------------------------------

        # it may be, that there are no mergers, so do basically nothing, just copy all the ingoing data
        if len(mergers) == 0:
            logger.info("The maximum number of objects is 1, so nothing to be done. Writing the output...")
            self._exportRefinedSegmentation(timesteps)

        else:
            self.mergersPerTimestep = hytra.core.jsongraph.getMergersPerTimestep(mergers, timesteps)
            self.detectionsPerTimestep = hytra.core.jsongraph.getDetectionsPerTimestep(detections, timesteps)

            linksPerTimestep = hytra.core.jsongraph.getLinksPerTimestep(links, timesteps)
            divisionsPerTimestep = hytra.core.jsongraph.getDivisionsPerTimestep(divisions, linksPerTimestep, timesteps)
            mergerLinks = hytra.core.jsongraph.getMergerLinks(linksPerTimestep, self.mergersPerTimestep, timesteps)

            # set up unresolved graph and then refine the nodes to get the resolved graph
            self._createUnresolvedGraph(divisionsPerTimestep, self.mergersPerTimestep, mergerLinks)
            self._prepareResolvedGraph()
            self._fitAndRefineNodes(self.detectionsPerTimestep, self.mergersPerTimestep, timesteps)

            # ------------------------------------------------------------
            # compute new object features
            objectFeatures = self._computeObjectFeatures(timesteps)

            # ------------------------------------------------------------
            # load transition classifier if any
            if transition_classifier_filename is not None:
                logger.info("\tLoading transition classifier")
                transitionClassifier = probabilitygenerator.RandomForestClassifier(
                    transition_classifier_path, transition_classifier_filename
                )
            else:
                logger.info("\tUsing distance based transition energies")
                transitionClassifier = None

            # ------------------------------------------------------------
            # run min-cost max-flow to find merger assignments
            logger.info("Running min-cost max-flow to find resolved merger assignments")

            nodeFlowMap, arcFlowMap = self._minCostMaxFlowMergerResolving(objectFeatures, transitionClassifier)

            # ------------------------------------------------------------
            # fuse results into a new solution

            # 1.) replace merger nodes in JSON graph by their replacements -> new JSON graph
            #     update UUID to traxel map.
            #     a) how do we deal with the smaller number of states?
            #        Does it matter as we're done with tracking anyway..?

            def mergerNodeFilter(jsonNode):
                uuid = int(jsonNode["id"])
                traxels = uuidToTraxelMap[uuid]
                return not any(t[1] in self.mergersPerTimestep[str(t[0])] for t in traxels)

            def mergerLinkFilter(jsonLink):
                srcUuid = int(jsonLink["src"])
                destUuid = int(jsonLink["dest"])
                srcTraxels = uuidToTraxelMap[srcUuid]
                destTraxels = uuidToTraxelMap[destUuid]

                # return True if there was no traxel in either source or target node that was a merger.
                return not (
                    any(t[1] in self.mergersPerTimestep[str(t[0])] for t in srcTraxels)
                    or any(t[1] in self.mergersPerTimestep[str(t[0])] for t in destTraxels)
                )

            self.model = self._refineModel(
                uuidToTraxelMap,
                traxelIdPerTimestepToUniqueIdMap,
                mergerNodeFilter,
                mergerLinkFilter,
            )

            # 2.) new result = union(old result, resolved mergers) - old mergers

            self.result = self._refineResult(
                nodeFlowMap,
                arcFlowMap,
                traxelIdPerTimestepToUniqueIdMap,
                mergerNodeFilter,
                mergerLinkFilter,
            )

            # 3.) export refined segmentation
            self._exportRefinedSegmentation(timesteps)

            # return a dictionary telling about which mergers were resolved into what
            mergerDict = {}
            for n in self.unresolvedGraph.nodes():
                # skip non-mergers
                if not "newIds" in self.unresolvedGraph.nodes[n] or len(self.unresolvedGraph.nodes[n]["newIds"]) < 2:
                    continue
                mergerDict.setdefault(n[0], {})[n[1]] = self.unresolvedGraph.nodes[n]["newIds"]

            return mergerDict

    def relabelMergers(self, labelImage, time):
        """
        Calls the merger resolving plugin to relabel the mergers based on a previously found fit,
        which is stored in the hypotheses graph node
        """
        t = str(time)

        if self.detectionsPerTimestep is not None and t in self.detectionsPerTimestep:
            for idx in self.detectionsPerTimestep[t]:
                node = (time, idx)

                if idx not in self.mergersPerTimestep[t]:
                    continue

                # use fits stored in graph
                fits = self.unresolvedGraph.nodes[node]["fits"]
                newIds = self.unresolvedGraph.nodes[node]["newIds"]

                # use merger resolving plugin to update labelImage with merger IDs
                self.mergerResolverPlugin.updateLabelImage(labelImage, idx, fits, newIds)

        return labelImage

import logging
import os
import numpy as np
import networkx as nx
from hytra.pluginsystem.plugin_manager import TrackingPluginManager
import hytra.core.probabilitygenerator as probabilitygenerator
import hytra.core.jsongraph
from hytra.core.jsongraph import negLog, listify, JsonTrackingGraph
from hytra.util.progressbar import DefaultProgressVisitor
from hytra.core.splittracking import SplitTracking


logger = logging.getLogger(__name__)


class MergerResolverException(Exception):
    pass


class MergerResolver(object):
    """
    Base class for all merger resolving implementations. Use one of the derived classes
    that handle reading/writing data to the respective sources.
    """

    def __init__(
        self,
        pluginPaths=[os.path.abspath("../hytra/plugins")],
        numSplits=None,
        verbose=False,
        progressVisitor=DefaultProgressVisitor(),
    ):
        self.unresolvedGraph = None
        self.resolvedGraph = None
        self.mergersPerTimestep = None
        self.detectionsPerTimestep = None
        self.pluginManager = TrackingPluginManager(verbose=verbose, pluginPaths=pluginPaths)
        self.mergerResolverPlugin = self.pluginManager.getMergerResolver()
        self.numSplits = numSplits

        # should be filled by constructors of derived classes!
        self.model = None
        self.result = None
        self.progressVisitor = progressVisitor

    def _createUnresolvedGraph(self, divisionsPerTimestep, mergersPerTimestep, mergerLinks, withFullGraph=False):
        """
        Set up a networkx graph consisting of mergers that need to be resolved (not resolved yet!)
        and their direct neighbors.

        ** returns ** the `unresolvedGraph`
        """

        self.unresolvedGraph = nx.DiGraph()

        def source(timestep, link):
            return int(timestep) - 1, link[0]

        def target(timestep, link):
            return int(timestep), link[1]

        # Recompute full graph
        if withFullGraph:
            self.unresolvedGraph = self.hypothesesGraph._graph.copy()

            # Add division parameter to nodes
            # TODO: Add the division parameter only to nodes that contain divisions (we're already doing these with 'count')
            lastframe = max(divisionsPerTimestep.keys(), key=int)
            for node in self.unresolvedGraph.nodes():
                timestep, idx = node

                if divisionsPerTimestep is not None and int(timestep) < int(lastframe):
                    division = idx in divisionsPerTimestep[str(timestep + 1)]  # +1 screams for lastframe condition.
                else:
                    division = False

                self.unresolvedGraph.nodes[node]["division"] = division

            # Add count parameter to nodes
            for t, link in mergerLinks:
                for node in [source(t, link), target(t, link)]:
                    timestep, idx = node
                    if idx in mergersPerTimestep[str(timestep)]:
                        count = mergersPerTimestep[str(timestep)][idx]
                        self.unresolvedGraph.nodes[node]["count"] = count

        # Recompute graph only with merger nodes and neighbors
        else:

            def addNode(node):
                """add a node to the unresolved graph and fill in the properties `division` and `count`"""
                intT, idx = node

                lastframe = max(divisionsPerTimestep.keys(), key=int)
                if divisionsPerTimestep is not None and int(intT) < int(lastframe):
                    division = idx in divisionsPerTimestep[str(intT + 1)]  # +1 screams for lastframe condition.
                else:
                    division = False
                count = 1
                if idx in mergersPerTimestep[str(intT)]:
                    if division is True:
                        raise MergerResolverException(
                            "Condition violation: Encountered object classified as merger AND division. "
                            "Retrain your classifiers using Uncertainty Layer to obtain less ambiguous probabilities "
                            "for divisions and mergers (object-count > 1)."
                        )

                    count = mergersPerTimestep[str(intT)][idx]
                self.unresolvedGraph.add_node(node, division=division, count=count)

            # add nodes
            for t, link in mergerLinks:
                for n in [source(t, link), target(t, link)]:
                    if not self.unresolvedGraph.has_node(n):
                        addNode(n)
                self.unresolvedGraph.add_edge(source(t, link), target(t, link))

        return self.unresolvedGraph

    def _prepareResolvedGraph(self):
        """

        ** returns ** the `resolvedGraph`
        """
        self.resolvedGraph = self.unresolvedGraph.copy()

        return self.resolvedGraph

    def _readLabelImage(self, timeframe):
        """
        Should return the labelimage for the given timeframe
        """
        raise NotImplementedError()

    def _fitAndRefineNodes(self, detectionsPerTimestep, mergersPerTimestep, timesteps):
        """
        Update segmentation of mergers (nodes in unresolvedGraph) from first timeframe to last
        and create new nodes in `resolvedGraph`. Links to merger nodes are duplicated to all new nodes.

        Uses the mergerResolver plugin to update the segmentations in the labelImages.
        """

        intTimesteps = [int(t) for t in timesteps]
        intTimesteps.sort()

        for intT in intTimesteps:
            t = str(intT)
            # use image provider plugin to load labelimage
            labelImage = self._readLabelImage(int(t))
            nextObjectId = labelImage.max() + 1

            for idx in detectionsPerTimestep[t]:
                node = (intT, idx)
                if node not in self.resolvedGraph:
                    continue

                count = 1
                if idx in mergersPerTimestep[t]:
                    count = mergersPerTimestep[t][idx]
                logger.debug("Looking at node {} in timestep {} with count {}".format(idx, t, count))

                # collect initializations from incoming
                initializations = []
                for predecessor, _ in self.unresolvedGraph.in_edges(node):
                    initializations.extend(self.unresolvedGraph.nodes[predecessor]["fits"])
                # TODO: what shall we do if e.g. a 2-merger and a single object merge to 2 + 1,
                # so there are 3 initializations for the 2-merger, and two initializations for the 1 merger?
                # What does pgmlink do in that case?

                # use merger resolving plugin to fit `count` objects, also updates labelimage!
                fittedObjects = list(
                    self.mergerResolverPlugin.resolveMerger(labelImage, idx, nextObjectId, count, initializations)
                )
                assert len(fittedObjects) == count

                # split up node if count > 1, duplicate incoming and outgoing arcs
                if count > 1:
                    for idx in range(nextObjectId, nextObjectId + count):
                        newNode = (intT, idx)
                        self.resolvedGraph.add_node(newNode, division=False, count=1, origin=node)

                        for e in self.unresolvedGraph.out_edges(node):
                            self.resolvedGraph.add_edge(newNode, e[1])
                        for e in self.unresolvedGraph.in_edges(node):
                            if "newIds" in self.unresolvedGraph.nodes[e[0]]:
                                for newId in self.unresolvedGraph.nodes[e[0]]["newIds"]:
                                    self.resolvedGraph.add_edge((e[0][0], newId), newNode)
                            else:
                                self.resolvedGraph.add_edge(e[0], newNode)

                    self.resolvedGraph.remove_node(node)
                    self.unresolvedGraph.nodes[node]["newIds"] = range(nextObjectId, nextObjectId + count)
                    nextObjectId += count

                # each unresolved node stores its fitted shape(s) to be used
                # as initialization in the next frame, this way division duplicates
                # and de-merged nodes in the resolved graph do not need to store a fit as well
                self.unresolvedGraph.nodes[node]["fits"] = fittedObjects

        # import matplotlib.pyplot as plt
        # nx.draw_networkx(resolvedGraph)
        # plt.savefig("/Users/chaubold/test.pdf")

    def _minCostMaxFlowMergerResolving(self, objectFeatures, transitionClassifier=None, transitionParameter=5.0):
        """
        Find the optimal assignments within the `resolvedGraph` by running min-cost max-flow from the
        `dpct` module.

        Converts the `resolvedGraph` to our JSON model structure, predicts the transition probabilities
        either using the given transitionClassifier, or using distance-based probabilities.

        **returns** a `nodeFlowMap` and `arcFlowMap` holding information on the usage of the respective nodes and links

        **Note:** cannot use `networkx` flow methods because they don't work with floating point weights.
        """

        trackingGraph = JsonTrackingGraph(progressVisitor=self.progressVisitor)
        for node in self.resolvedGraph.nodes():
            additionalFeatures = {}
            additionalFeatures["nid"] = node

            # nodes with no in/out
            numStates = 2

            if len(self.resolvedGraph.in_edges(node)) == 0:
                # division nodes with no incoming arcs offer 2 units of flow without the need to de-merge
                if (
                    node in self.unresolvedGraph.nodes()
                    and self.unresolvedGraph.nodes[node]["division"]
                    and len(self.unresolvedGraph.out_edges(node)) == 2
                ):
                    numStates = 3
                additionalFeatures["appearanceFeatures"] = [[i ** 2 * 0.01] for i in range(numStates)]
            if len(self.resolvedGraph.out_edges(node)) == 0:
                assert (
                    numStates == 2
                )  # division nodes with no incoming should have outgoing, or they shouldn't show up in resolved graph
                additionalFeatures["disappearanceFeatures"] = [[i ** 2 * 0.01] for i in range(numStates)]

            features = [[i ** 2] for i in range(numStates)]
            uuid = trackingGraph.addDetectionHypotheses(features, **additionalFeatures)
            self.resolvedGraph.nodes[node]["id"] = uuid

        for edge in self.resolvedGraph.edges():
            src = self.resolvedGraph.nodes[edge[0]]["id"]
            dest = self.resolvedGraph.nodes[edge[1]]["id"]

            featuresAtSrc = objectFeatures[edge[0]]
            featuresAtDest = objectFeatures[edge[1]]

            if transitionClassifier is not None:
                try:
                    featVec = self.pluginManager.applyTransitionFeatureVectorConstructionPlugins(
                        featuresAtSrc,
                        featuresAtDest,
                        transitionClassifier.selectedFeatures,
                    )
                except:
                    logger.error("Could not compute transition features of link {}->{}:".format(src, dest))
                    logger.error(featuresAtSrc)
                    logger.error(featuresAtDest)
                    raise
                featVec = np.expand_dims(np.array(featVec), axis=0)
                probs = transitionClassifier.predictProbabilities(featVec)[0]
            else:
                dist = np.linalg.norm(featuresAtDest["RegionCenter"] - featuresAtSrc["RegionCenter"])
                prob = np.exp(-dist / transitionParameter)
                probs = [1.0 - prob, prob]

            trackingGraph.addLinkingHypotheses(src, dest, listify(negLog(probs)))

        # Set TraxelToUniqueId on resolvedGraph's json graph
        uuidToTraxelMap = {}
        traxelIdPerTimestepToUniqueIdMap = {}

        for node in self.resolvedGraph.nodes():
            uuid = self.resolvedGraph.nodes[node]["id"]
            uuidToTraxelMap[uuid] = [node]

            for t in uuidToTraxelMap[uuid]:
                traxelIdPerTimestepToUniqueIdMap.setdefault(str(t[0]), {})[str(t[1])] = uuid

        trackingGraph.setTraxelToUniqueId(traxelIdPerTimestepToUniqueIdMap)

        # track
        import dpct

        weights = {"weights": [1, 1, 1, 1]}

        if not self.numSplits:
            mergerResult = dpct.trackMaxFlow(trackingGraph.model, weights)
        else:
            logger.info("Running split tracking with {} splits.".format(self.numSplits))
            mergerResult = SplitTracking.trackFlowBasedWithSplits(
                trackingGraph.model,
                weights,
                numSplits=self.numSplits,
                withMergerResolver=True,
            )

        # transform results to dictionaries that can be indexed by id or (src,dest)
        nodeFlowMap = dict([(int(d["id"]), int(d["value"])) for d in mergerResult["detectionResults"]])
        arcFlowMap = dict([((int(l["src"]), int(l["dest"])), int(l["value"])) for l in mergerResult["linkingResults"]])

        return nodeFlowMap, arcFlowMap

    def _refineModel(
        self,
        uuidToTraxelMap,
        traxelIdPerTimestepToUniqueIdMap,
        mergerNodeFilter,
        mergerLinkFilter,
    ):
        """
        Take the `self.model` (JSON format) with mergers, remove the merger nodes, but add new
        de-merged nodes and links. Also updates `traxelIdPerTimestepToUniqueIdMap` locally and in the resulting file,
        such that the traxel IDs match the new connected component IDs in the refined images.

        `mergerNodeFilter` and `mergerLinkFilter` are methods that can filter merger detections
        and links from the respective lists in the `model` dict.

        **Returns** the updated `model` dictionary, which is the same as the input `model` (works in-place)
        """

        # remove merger detections
        self.model["segmentationHypotheses"] = [
            seg for seg in self.model["segmentationHypotheses"] if mergerNodeFilter(seg)
        ]

        # remove merger links
        self.model["linkingHypotheses"] = [link for link in self.model["linkingHypotheses"] if mergerLinkFilter(link)]

        # insert new nodes and update UUID to traxel map
        nextUuid = max(uuidToTraxelMap.keys()) + 1
        for node in self.unresolvedGraph.nodes():
            if "count" in self.unresolvedGraph.nodes[node] and self.unresolvedGraph.nodes[node]["count"] > 1:
                newIds = self.unresolvedGraph.nodes[node]["newIds"]
                del traxelIdPerTimestepToUniqueIdMap[str(node[0])][str(node[1])]
                for newId in newIds:
                    newDetection = {}
                    newDetection["id"] = nextUuid
                    newDetection["timestep"] = [node[0], node[0]]
                    self.model["segmentationHypotheses"].append(newDetection)
                    traxelIdPerTimestepToUniqueIdMap[str(node[0])][str(newId)] = nextUuid
                    nextUuid += 1

        # insert new links
        for edge in self.resolvedGraph.edges():
            newLink = {}
            newLink["src"] = traxelIdPerTimestepToUniqueIdMap[str(edge[0][0])][str(edge[0][1])]
            newLink["dest"] = traxelIdPerTimestepToUniqueIdMap[str(edge[1][0])][str(edge[1][1])]
            self.model["linkingHypotheses"].append(newLink)

        # save
        return self.model

    def _refineResult(
        self,
        nodeFlowMap,
        arcFlowMap,
        traxelIdPerTimestepToUniqueIdMap,
        mergerNodeFilter,
        mergerLinkFilter,
    ):
        """
        Update the `self.result` dict by removing the mergers and adding the refined nodes and links.

        Operates on a `result` dictionary in our JSON result style with mergers,
        the resolved and unresolved graph as well as
        the `nodeFlowMap` and `arcFlowMap` obtained by running tracking on the `resolvedGraph`.

        Updates the `result` dictionary so that all merger nodes are removed but the new nodes
        are contained with the appropriate links and values.

        `mergerNodeFilter` and `mergerLinkFilter` are methods that can filter merger detections
        and links from the respective lists in the `result` dict.

        **Returns** the updated `result` dict, which is the same as the input `result` (works in-place)
        """

        # filter merger edges
        self.result["detectionResults"] = [r for r in self.result["detectionResults"] if mergerNodeFilter(r)]
        self.result["linkingResults"] = [r for r in self.result["linkingResults"] if mergerLinkFilter(r)]

        # add new nodes
        for node in self.unresolvedGraph.nodes():
            if "count" in self.unresolvedGraph.nodes[node] and self.unresolvedGraph.nodes[node]["count"] > 1:
                newIds = self.unresolvedGraph.nodes[node]["newIds"]
                for newId in newIds:
                    uuid = traxelIdPerTimestepToUniqueIdMap[str(node[0])][str(newId)]
                    resolvedNode = (node[0], newId)
                    resolvedResultId = self.resolvedGraph.nodes[resolvedNode]["id"]
                    newDetection = {"id": uuid, "value": nodeFlowMap[resolvedResultId]}
                    self.result["detectionResults"].append(newDetection)

        # add new links
        for edge in self.resolvedGraph.edges():
            newLink = {}
            newLink["src"] = traxelIdPerTimestepToUniqueIdMap[str(edge[0][0])][str(edge[0][1])]
            newLink["dest"] = traxelIdPerTimestepToUniqueIdMap[str(edge[1][0])][str(edge[1][1])]
            srcId = self.resolvedGraph.nodes[edge[0]]["id"]
            destId = self.resolvedGraph.nodes[edge[1]]["id"]
            newLink["value"] = arcFlowMap[(srcId, destId)]
            self.result["linkingResults"].append(newLink)

        return self.result

    def _exportRefinedSegmentation(self, timesteps):
        """
        Store the resulting label images, if needed.

        `labelImages` is a dictionary with str(timestep) as keys.
        """
        pass

    def _computeObjectFeatures(self, timesteps):
        """
        Return the features per object as nested dictionaries:
        { (int(Timestep), int(Id)):{ "FeatureName" : np.array(value), "NextFeature": ...} }
        """
        pass

    # ------------------------------------------------------------
    def run(self, transition_classifier_filename=None, transition_classifier_path=None):
        """
        Run merger resolving

        1. find mergers in the given model and result
        2. build graph of the unresolved (merger) nodes and their direct neighbors
        3. use a mergerResolving plugin to refine the merger nodes and their segmentation
        4. run min-cost max-flow tracking to find the fate of all the de-merged objects
        5. export refined segmentation, update member variables `model` and `result`

        **Returns** a nested dictionary, indexed first by time, then object Id, containing a list of new segmentIDs per merger
        """

        traxelIdPerTimestepToUniqueIdMap, uuidToTraxelMap = hytra.core.jsongraph.getMappingsBetweenUUIDsAndTraxels(
            self.model
        )
        # timesteps = [t for t in traxelIdPerTimestepToUniqueIdMap.keys()]
        # there might be empty frames. We want them as output too.
        timesteps = [
            str(t)
            for t in range(
                int(min(traxelIdPerTimestepToUniqueIdMap.keys())),
                max([int(idx) for idx in traxelIdPerTimestepToUniqueIdMap.keys()]) + 1,
            )
        ]

        mergers, detections, links, divisions = hytra.core.jsongraph.getMergersDetectionsLinksDivisions(
            self.result, uuidToTraxelMap
        )

        # ------------------------------------------------------------

        # it may be, that there are no mergers, so do basically nothing, just copy all the ingoing data
        if len(mergers) == 0:
            logger.info("The maximum number of objects is 1, so nothing to be done. Writing the output...")
            self._exportRefinedSegmentation(timesteps)

        else:
            self.mergersPerTimestep = hytra.core.jsongraph.getMergersPerTimestep(mergers, timesteps)
            self.detectionsPerTimestep = hytra.core.jsongraph.getDetectionsPerTimestep(detections, timesteps)

            linksPerTimestep = hytra.core.jsongraph.getLinksPerTimestep(links, timesteps)
            divisionsPerTimestep = hytra.core.jsongraph.getDivisionsPerTimestep(divisions, linksPerTimestep, timesteps)
            mergerLinks = hytra.core.jsongraph.getMergerLinks(linksPerTimestep, self.mergersPerTimestep, timesteps)

            # set up unresolved graph and then refine the nodes to get the resolved graph
            self._createUnresolvedGraph(divisionsPerTimestep, self.mergersPerTimestep, mergerLinks)
            self._prepareResolvedGraph()
            self._fitAndRefineNodes(self.detectionsPerTimestep, self.mergersPerTimestep, timesteps)

            # ------------------------------------------------------------
            # compute new object features
            objectFeatures = self._computeObjectFeatures(timesteps)

            # ------------------------------------------------------------
            # load transition classifier if any
            if transition_classifier_filename is not None:
                logger.info("\tLoading transition classifier")
                transitionClassifier = probabilitygenerator.RandomForestClassifier(
                    transition_classifier_path, transition_classifier_filename
                )
            else:
                logger.info("\tUsing distance based transition energies")
                transitionClassifier = None

            # ------------------------------------------------------------
            # run min-cost max-flow to find merger assignments
            logger.info("Running min-cost max-flow to find resolved merger assignments")

            nodeFlowMap, arcFlowMap = self._minCostMaxFlowMergerResolving(objectFeatures, transitionClassifier)

            # ------------------------------------------------------------
            # fuse results into a new solution

            # 1.) replace merger nodes in JSON graph by their replacements -> new JSON graph
            #     update UUID to traxel map.
            #     a) how do we deal with the smaller number of states?
            #        Does it matter as we're done with tracking anyway..?

            def mergerNodeFilter(jsonNode):
                uuid = int(jsonNode["id"])
                traxels = uuidToTraxelMap[uuid]
                return not any(t[1] in self.mergersPerTimestep[str(t[0])] for t in traxels)

            def mergerLinkFilter(jsonLink):
                srcUuid = int(jsonLink["src"])
                destUuid = int(jsonLink["dest"])
                srcTraxels = uuidToTraxelMap[srcUuid]
                destTraxels = uuidToTraxelMap[destUuid]

                # return True if there was no traxel in either source or target node that was a merger.
                return not (
                    any(t[1] in self.mergersPerTimestep[str(t[0])] for t in srcTraxels)
                    or any(t[1] in self.mergersPerTimestep[str(t[0])] for t in destTraxels)
                )

            self.model = self._refineModel(
                uuidToTraxelMap,
                traxelIdPerTimestepToUniqueIdMap,
                mergerNodeFilter,
                mergerLinkFilter,
            )

            # 2.) new result = union(old result, resolved mergers) - old mergers

            self.result = self._refineResult(
                nodeFlowMap,
                arcFlowMap,
                traxelIdPerTimestepToUniqueIdMap,
                mergerNodeFilter,
                mergerLinkFilter,
            )

            # 3.) export refined segmentation
            self._exportRefinedSegmentation(timesteps)

            # return a dictionary telling about which mergers were resolved into what
            mergerDict = {}
            for n in self.unresolvedGraph.nodes():
                # skip non-mergers
                if not "newIds" in self.unresolvedGraph.nodes[n] or len(self.unresolvedGraph.nodes[n]["newIds"]) < 2:
                    continue
                mergerDict.setdefault(n[0], {})[n[1]] = self.unresolvedGraph.nodes[n]["newIds"]

            return mergerDict

    def relabelMergers(self, labelImage, time):
        """
        Calls the merger resolving plugin to relabel the mergers based on a previously found fit,
        which is stored in the hypotheses graph node
        """
        t = str(time)

        if self.detectionsPerTimestep is not None and t in self.detectionsPerTimestep:
            for idx in self.detectionsPerTimestep[t]:
                node = (time, idx)

                if idx not in self.mergersPerTimestep[t]:
                    continue

                # use fits stored in graph
                fits = self.unresolvedGraph.nodes[node]["fits"]
                newIds = self.unresolvedGraph.nodes[node]["newIds"]

                # use merger resolving plugin to update labelImage with merger IDs
                self.mergerResolverPlugin.updateLabelImage(labelImage, idx, fits, newIds)

        return labelImage

Base class for all merger resolving implementations. Use one of the derived classes     that handle reading/writing data to the respective sources. # should be filled by constructors of derived classes! Set up a networkx graph consisting of mergers that need to be resolved (not resolved yet!)         and their direct neighbors.          ** returns ** the `unresolvedGraph` # Recompute full graph # Add division parameter to nodes # TODO: Add the division parameter only to nodes that contain divisions (we're already doing these with 'count') # +1 screams for lastframe condition. # Add count parameter to nodes # Recompute graph only with merger nodes and neighbors add a node to the unresolved graph and fill in the properties `division` and `count` # +1 screams for lastframe condition. # add nodes ** returns ** the `resolvedGraph` Should return the labelimage for the given timeframe Update segmentation of mergers (nodes in unresolvedGraph) from first timeframe to last         and create new nodes in `resolvedGraph`. Links to merger nodes are duplicated to all new nodes.          Uses the mergerResolver plugin to update the segmentations in the labelImages. # use image provider plugin to load labelimage # collect initializations from incoming # TODO: what shall we do if e.g. a 2-merger and a single object merge to 2 + 1, # so there are 3 initializations for the 2-merger, and two initializations for the 1 merger? # What does pgmlink do in that case? # use merger resolving plugin to fit `count` objects, also updates labelimage! # split up node if count > 1, duplicate incoming and outgoing arcs # each unresolved node stores its fitted shape(s) to be used # as initialization in the next frame, this way division duplicates # and de-merged nodes in the resolved graph do not need to store a fit as well # import matplotlib.pyplot as plt # nx.draw_networkx(resolvedGraph) # plt.savefig("/Users/chaubold/test.pdf") Find the optimal assignments within the `resolvedGraph` by running min-cost max-flow from the         `dpct` module.          Converts the `resolvedGraph` to our JSON model structure, predicts the transition probabilities         either using the given transitionClassifier, or using distance-based probabilities.          **returns** a `nodeFlowMap` and `arcFlowMap` holding information on the usage of the respective nodes and links          **Note:** cannot use `networkx` flow methods because they don't work with floating point weights. # nodes with no in/out # division nodes with no incoming arcs offer 2 units of flow without the need to de-merge # division nodes with no incoming should have outgoing, or they shouldn't show up in resolved graph # Set TraxelToUniqueId on resolvedGraph's json graph # track # transform results to dictionaries that can be indexed by id or (src,dest) Take the `self.model` (JSON format) with mergers, remove the merger nodes, but add new         de-merged nodes and links. Also updates `traxelIdPerTimestepToUniqueIdMap` locally and in the resulting file,         such that the traxel IDs match the new connected component IDs in the refined images.          `mergerNodeFilter` and `mergerLinkFilter` are methods that can filter merger detections         and links from the respective lists in the `model` dict.          **Returns** the updated `model` dictionary, which is the same as the input `model` (works in-place) # remove merger detections # remove merger links # insert new nodes and update UUID to traxel map # insert new links # save Update the `self.result` dict by removing the mergers and adding the refined nodes and links.          Operates on a `result` dictionary in our JSON result style with mergers,         the resolved and unresolved graph as well as         the `nodeFlowMap` and `arcFlowMap` obtained by running tracking on the `resolvedGraph`.          Updates the `result` dictionary so that all merger nodes are removed but the new nodes         are contained with the appropriate links and values.          `mergerNodeFilter` and `mergerLinkFilter` are methods that can filter merger detections         and links from the respective lists in the `result` dict.          **Returns** the updated `result` dict, which is the same as the input `result` (works in-place) # filter merger edges # add new nodes # add new links Store the resulting label images, if needed.          `labelImages` is a dictionary with str(timestep) as keys. Return the features per object as nested dictionaries:         { (int(Timestep), int(Id)):{ "FeatureName" : np.array(value), "NextFeature": ...} } # ------------------------------------------------------------ Run merger resolving          1. find mergers in the given model and result         2. build graph of the unresolved (merger) nodes and their direct neighbors         3. use a mergerResolving plugin to refine the merger nodes and their segmentation         4. run min-cost max-flow tracking to find the fate of all the de-merged objects         5. export refined segmentation, update member variables `model` and `result`          **Returns** a nested dictionary, indexed first by time, then object Id, containing a list of new segmentIDs per merger # timesteps = [t for t in traxelIdPerTimestepToUniqueIdMap.keys()] # there might be empty frames. We want them as output too. # ------------------------------------------------------------ # it may be, that there are no mergers, so do basically nothing, just copy all the ingoing data # set up unresolved graph and then refine the nodes to get the resolved graph # ------------------------------------------------------------ # compute new object features # ------------------------------------------------------------ # load transition classifier if any # ------------------------------------------------------------ # run min-cost max-flow to find merger assignments # ------------------------------------------------------------ # fuse results into a new solution # 1.) replace merger nodes in JSON graph by their replacements -> new JSON graph #     update UUID to traxel map. #     a) how do we deal with the smaller number of states? #        Does it matter as we're done with tracking anyway..? # return True if there was no traxel in either source or target node that was a merger. # 2.) new result = union(old result, resolved mergers) - old mergers # 3.) export refined segmentation # return a dictionary telling about which mergers were resolved into what # skip non-mergers Calls the merger resolving plugin to relabel the mergers based on a previously found fit,         which is stored in the hypotheses graph node # use fits stored in graph # use merger resolving plugin to update labelImage with merger IDs