示例#1
0
def watershed_hierarchy_by_attribute(graph,
                                     edge_weights,
                                     attribute_functor,
                                     canonize_tree=True):
    """
    Watershed hierarchy by a user defined attributes.

    The definition of hierarchical watershed follows the one given in:

        J. Cousty, L. Najman.
        `Incremental algorithm for hierarchical minimum spanning forests and saliency of watershed cuts <https://hal-upec-upem.archives-ouvertes.fr/hal-00622505/document>`_.
        ISMM 2011: 272-283.

    The algorithm used is described in:

        Laurent Najman, Jean Cousty, Benjamin Perret.
        `Playing with Kruskal: Algorithms for Morphological Trees in Edge-Weighted Graphs <https://hal.archives-ouvertes.fr/file/index/docid/798621/filename/ismm2013-algo.pdf>`_.
        ISMM 2013: 135-146.

    The attribute functor is a function that takes a binary partition tree and an array of altitudes as argument
    and returns an array with the node attribute values for the given tree.

    Example:

    Calling watershed_hierarchy_by_area is equivalent to:

    .. code-block:: python

        tree = watershed_hierarchy_by_attribute(graph, edge_weights, lambda tree, _: hg.attribute_area(tree))

    :param graph: input graph
    :param edge_weights: edge weights of the input graph
    :param attribute_functor: function computing the regional attribute
    :param canonize_tree: if ``True`` (default), the resulting hierarchy is canonized (see function :func:`~higra.canonize_hierarchy`),
           otherwise the returned hierarchy is a binary tree
    :return: a tree (Concept :class:`~higra.CptHierarchy` is ``True`` and :class:`~higra.CptBinaryHierarchy` otherwise)
             and its node altitudes
    """
    def helper_functor(tree, altitudes):
        hg.CptHierarchy.link(tree, graph)

        return attribute_functor(tree, altitudes)

    tree, altitudes, mst_edge_map = hg.cpp._watershed_hierarchy_by_attribute(
        graph, edge_weights, helper_functor)

    hg.CptHierarchy.link(tree, graph)

    if canonize_tree:
        tree, altitudes = hg.canonize_hierarchy(tree, altitudes)
    else:
        mst = hg.subgraph(graph, mst_edge_map)
        hg.CptMinimumSpanningTree.link(mst, graph, mst_edge_map)
        hg.CptBinaryHierarchy.link(tree, mst_edge_map, mst)

    return tree, altitudes
示例#2
0
    def test_subgraph_spanning(self):
        graph = hg.get_4_adjacency_graph((2, 2))
        edge_indices = np.asarray((3, 0))
        subgraph = hg.subgraph(graph, edge_indices, spanning=True)

        self.assertTrue(subgraph.num_vertices() == graph.num_vertices())
        self.assertTrue(subgraph.num_edges() == len(edge_indices))
        sources, targets = subgraph.edge_list()
        self.assertTrue(np.all(sources == (2, 0)))
        self.assertTrue(np.all(targets == (3, 1)))
示例#3
0
    def test_subgraph_spanning(self):
        graph = hg.UndirectedGraph(6)
        graph.add_edges(np.arange(5), np.arange(1, 6))
        edge_indices = np.asarray((4, 0, 3))
        subgraph, vertex_map = hg.subgraph(graph, edge_indices, spanning=False, return_vertex_map=True)

        self.assertTrue(subgraph.num_vertices() == 5)
        self.assertTrue(subgraph.num_edges() == len(edge_indices))
        sources, targets = subgraph.edge_list()
        self.assertTrue(np.all(vertex_map == (0, 1, 3, 4, 5)))
        self.assertTrue(np.all(vertex_map[sources] == (4, 0, 3)))
        self.assertTrue(np.all(vertex_map[targets] == (5, 1, 4)))
示例#4
0
    def get_mst(tree):
        """
        The minimum spanning tree of the leaf graph of the hierarchy.

        :param tree:
        :return:
        """
        mst = hg.get_attribute(tree, "mst")
        if mst is None:
            mst_edge_map = hg.CptBinaryHierarchy.get_mst_edge_map(tree)
            leaf_graph = hg.CptHierarchy.get_leaf_graph(tree)
            mst = hg.subgraph(leaf_graph, mst_edge_map, spanning=True)
            hg.CptMinimumSpanningTree.link(mst, leaf_graph, mst_edge_map)
            hg.set_attribute(tree, "mst", mst)
        return mst
示例#5
0
def watershed_hierarchy_by_minima_ordering(graph,
                                           edge_weights,
                                           minima_ranks,
                                           minima_altitudes=None,
                                           canonize_tree=True):
    """
    Watershed hierarchy for the given minima ordering.

    The definition used follows the one given in:

        J. Cousty, L. Najman.
        `Incremental algorithm for hierarchical minimum spanning forests and saliency of watershed cuts <https://hal-upec-upem.archives-ouvertes.fr/hal-00622505/document>`_.
        ISMM 2011: 272-283.

    and in,

        J. Cousty, L. Najman, B. Perret.
        `Constructive links between some morphological hierarchies on edge-weighted graphs <https://hal.archives-ouvertes.fr/file/index/docid/806851/filename/ismm2013.pdf>`_..
        ISMM 2013: 86-97.
     
    The algorithm used is adapted from the algorithm described in:
     
        Laurent Najman, Jean Cousty, Benjamin Perret.
        `Playing with Kruskal: Algorithms for Morphological Trees in Edge-Weighted Graphs <https://hal.archives-ouvertes.fr/file/index/docid/798621/filename/ismm2013-algo.pdf>`_.
        ISMM 2013: 135-146.
     
     
    The ranking ranking of the minima of the given edge weighted graph :math:`(G,w)` is given as vertex weights with values in
    :math:`\{0, \ldots, n\}` with :math:`n` the number of minima of :math:`(G,w)`. It must satisfy the following pre-conditions:

        - each minimum of :math:`(G,w)` contains at least one non zero vertex,
        - all non zero vertices in a minimum have the same weight,
        - there is no non zero value vertex outside minima, and
        - no two minima contain non zero vertices with the same weight.
     
    :attr:`minima_altitudes` is an optional non decreasing 1d array of size :math:`n + 1` with non negative values such that
    :math:`minima\_altitudes[i]` indicates the altitude of the minima of rank :math:`i`.
    Note that the first entry of the minima altitudes array, ie. the value at index 0, does not represent a minimum and
    its value should be 0.

    The altitude of a node of the computed watershed corresponds to the altitude (respectively the rank) of the minima it
    is associated to if :attr:`minima_altitudes` is provided (respectively not provided).

    :param graph: input graph
    :param edge_weights: edge weights of the input graph
    :param minima_ranks: input graph vertex weights containing the rank of each minima of the input edge weighted graph
    :param minima_altitudes: array mapping each minima rank to its altitude (optional)
    :param canonize_tree: if ``True`` (default), the resulting hierarchy is canonized (see function :func:`~higra.canonize_hierarchy`),
           otherwise the returned hierarchy is a binary tree
    :return: a tree (Concept :class:`~higra.CptHierarchy` is ``True`` and :class:`~higra.CptBinaryHierarchy` otherwise)
             and its node altitudes
    """

    minima_ranks = hg.cast_to_dtype(minima_ranks, np.uint64)
    tree, altitudes, mst_edge_map = hg.cpp._watershed_hierarchy_by_minima_ordering(
        graph, edge_weights, minima_ranks)

    hg.CptHierarchy.link(tree, graph)

    if canonize_tree:
        tree, altitudes = hg.canonize_hierarchy(tree, altitudes)
    else:
        mst = hg.subgraph(graph, mst_edge_map)
        hg.CptMinimumSpanningTree.link(mst, graph, mst_edge_map)
        hg.CptBinaryHierarchy.link(tree, mst_edge_map, mst)

    if minima_altitudes is not None:
        altitudes = minima_altitudes[altitudes]

    return tree, altitudes
示例#6
0
def bpt_canonical(graph,
                  edge_weights=None,
                  sorted_edge_indices=None,
                  return_altitudes=True,
                  compute_mst=True):
    """
    Computes the *canonical binary partition tree*, also called *binary partition tree by altitude ordering* or
    *connectivity constrained single min/linkage clustering* of the given graph.

    :Definition:

    The following definition is adapted from:

        Cousty, Jean, Laurent Najman, Yukiko Kenmochi, and Silvio Guimarães.
        `"Hierarchical segmentations with graphs: quasi-flat zones, minimum spanning trees, and saliency maps."
        <https://hal.archives-ouvertes.fr/hal-01344727/document>`_
        Journal of Mathematical Imaging and Vision 60, no. 4 (2018): 479-502.

    Let :math:`G=(V,E)` be an undirected graph, let :math:`\\prec` be a total order on :math:`E`, and let :math:`e_k` be
    the edge in :math:`E` that has exactly :math:`k` smaller edges according to :math:`\\prec`: we then say that :math:`k`
    is the rank of :math:`e_k` (for :math:`\\prec`).
    The *canonical binary partition hierarchy* of :math:`G` for :math:`\\prec` is defined as the sequence of nested partitions:

    - :math:`P_0 = \{\{v\}, v\in V\}`, the finest partion is composed of every singleton of :math:`V`; and
    - :math:`P_n = (P_{n-1} \\backslash \{P_{n-1}^x, P_{n-1}^y\}) \cup (P_{n-1}^x \cup P_{n-1}^y)` where :math:`e_n=\{x,y\}`
      and :math:`P_{n-1}^x` and :math:`P_{n-1}^y` are the regions of  :math:`P_{n-1}` that contain :math:`x` and :math:`y`
      respectively.

    At the step :math:`n`, we remove the regions at the two extremities of the :math:`n`-th smallest edge
    and we add their union. Note that we may have :math:`P_n = P_{n-1}` if both extremities of the edge :math:`e_n`
    were in a same region of :math:`P_{n-1}`. Otherwise, :math:`P_n` is obtained by merging two regions of :math:`P_{n-1}`.

    The *canonical binary partition tree* is then the tree representing the merging steps in this sequence,
    it is thus binary. Each merging step, and thus each non leaf node of the tree, is furthermore associated to a
    specific edge of the graph, called *a building edge* that led to this merge. It can be shown that the set of all
    building edges associated to a canonical binary partition tree is a minimum spanning tree of the graph for the given
    edge ordering :math:`\\prec`.

    The map that associates every non leaf node of the canonical binary partition tree to its building edge is called
    the *mst_edge_map*. In practice this map is represented by an array of size :math:`tree.num\_vertices() - tree.num\_leaves()`
    and, for any internal node :math:`i` of the tree, :math:`mst\_edge\_map[i - tree.num\_leaves()]` is equal to the index
    of the building edge in :math:`G` associated to :math:`i`.

    The ordering :math:`\\prec` can be specified explicitly by providing the array of indices :attr:`sorted_edge_indices`
    that sort the edges, or implicitly by providing the array of edge weights :attr:`edge_weights`. In this case,
    :attr:`sorted_edge_indices` is set equal to ``hg.arg_sort(edge_weights, stable=True)``. If :attr:`edge_weights`
    is an array with more than 1 dimension, a lexicographic ordering is used.

    If requested, altitudes associated to the nodes of the canonical binary partition tree are computed as follows:

      - if :attr:`edge_weights` are provided, the altitude of a non-leaf node is equal to the edge weight of its
        building edge; and
      - otherwise, the altitude of a non-leaf node is equal to the rank of its building edge.

    The altitude of a leaf node is always equal to 0.

    :Example:

    .. figure:: /fig/canonical_binary_partition_tree_example.svg
        :alt: Example of a binary partition tree by altitude ordering
        :align: center

        Given an edge weighted graph :math:`G`, the binary partition tree by altitude ordering :math:`T` (in blue) is
        associated to a minimum spanning tree :math:`S` of :math:`G` (whose edges are thick and gray). Each leaf node of
        the tree corresponds to a vertex of :math:`G` while each non-leaf node :math:`n_i` of :math:`T` corresponds to a
        building edge of :math:`T` which belongs to the minimum spanning tree :math:`S`. The association between the non-leaf
        nodes and the minimum spanning tree edges, called *mst_edge_map*, is depicted by green arrows .


    The above figure corresponds to the following code (note that vertex indices start at 0 in the code):

        >>> g = hg.UndirectedGraph(5)
        >>> g.add_edges((0, 0, 1, 1, 1, 2, 3),
        >>>             (1, 2, 2, 3, 4, 4, 4))
        >>> edge_weights = np.asarray((4, 6, 3, 7, 11, 8, 5))
        >>> tree, altitudes = hg.bpt_canonical(g, edge_weights)
        >>> tree.parents()
        array([6, 5, 5, 7, 7, 6, 8, 8, 8])
        >>> altitudes
        array([0, 0, 0, 0, 0, 3, 4, 5, 7])
        >>> tree.mst_edge_map
        array([2, 0, 6, 3])
        >>> tree.mst.edge_list()
        (array([1, 0, 3, 1]), array([2, 1, 4, 3]))

    An object of type UnidrectedGraph is not necessary:

        >>> edge_weights = np.asarray((4, 6, 3, 7, 11, 8, 5))
        >>> sources = (0, 0, 1, 1, 1, 2, 3)
        >>> targets = (1, 2, 2, 3, 4, 4, 4)
        >>> tree, altitudes = hg.bpt_canonical((sources, targets, 5), edge_weights)
        >>> tree.parents()
        array([6, 5, 5, 7, 7, 6, 8, 8, 8])
        >>> altitudes
        array([0, 0, 0, 0, 0, 3, 4, 5, 7])
        >>> tree.mst_edge_map
        array([2, 0, 6, 3])


    :Complexity:

    The algorithm used is based on Kruskal's minimum spanning tree algorithm and is described in:

        Laurent Najman, Jean Cousty, Benjamin Perret.
        `Playing with Kruskal: Algorithms for Morphological Trees in Edge-Weighted Graphs
        <https://hal.archives-ouvertes.fr/file/index/docid/798621/filename/ismm2013-algo.pdf>`_.
        ISMM 2013: 135-146.

    If :attr:`sorted_edge_indices` is provided the algorithm runs in quasi linear :math:`\mathcal{O}(n \\alpha(n))`,
    with :math:`n` the number of elements in the graph and with :math`\\alpha` the inverse of the Ackermann function.
    Otherwise, the computation time is dominated by the sorting of the edge weights which is performed in linearithmic
    :math:`\mathcal{O}(n \log(n))` time.

    :param graph: input graph or triplet of two arrays and an integer (sources, targets, num_vertices)
           defining all the edges of the graph and its number of vertices.
    :param edge_weights: edge weights of the input graph (may be omitted if :attr:`sorted_edge_indices` is given).
    :param sorted_edge_indices: array of indices that sort the edges of the input graph by increasing weight (may be
           omitted if :attr:`edge_weights` is given).
    :param return_altitudes: if ``True`` an array representing the altitudes of the tree vertices is returned.
           (default: ``True``).
    :param compute_mst: if ``True`` and if the input is a graph object computes an explicit undirected graph
           representing the minimum spanning tree associated to the hierarchy, accessible through the
           :class:`~higra.CptBinaryHierarchy` Concept (e.g. with ``tree.mst``). (default: ``True``).
    :return: a tree (Concept :class:`~higra.CptBinaryHierarchy` if the input is a graph object),
             and, if :attr:`return_altitudes` is ``True``, its node altitudes
    """

    if edge_weights is None and sorted_edge_indices is None:
        raise ValueError(
            "edge_weights and sorted_edge_indices cannot be both equal to None."
        )

    if sorted_edge_indices is None:
        if edge_weights.ndim > 2:
            tmp_edge_weights = edge_weights.reshape(
                (edge_weights.shape[0], -1))
        else:
            tmp_edge_weights = edge_weights
        sorted_edge_indices = hg.arg_sort(tmp_edge_weights, stable=True)

    input_is_graph_object = False

    if hg.has_method(graph, "edge_list") and hg.has_method(
            graph, "num_vertices"):
        input_is_graph_object = True
        sources, targets = graph.edge_list()
        num_vertices = graph.num_vertices()
    else:
        try:
            sources, targets, num_vertices = graph
        except Exception as e:
            raise ValueError("Invalid graph input.") from e

    parents, mst_edge_map = hg.cpp._bpt_canonical(sources, targets,
                                                  sorted_edge_indices,
                                                  num_vertices)
    tree = hg.Tree(parents)

    if return_altitudes:
        if edge_weights is None:
            edge_weights = np.empty_like(sorted_edge_indices)
            edge_weights[sorted_edge_indices] = np.arange(
                sorted_edge_indices.size)

        if edge_weights.ndim == 1:
            altitudes = np.zeros((tree.num_vertices(), ),
                                 dtype=edge_weights.dtype)
            altitudes[num_vertices:] = edge_weights[mst_edge_map]
        else:
            shape = [tree.num_vertices()] + list(edge_weights.shape[1:])
            altitudes = np.zeros(shape, dtype=edge_weights.dtype)
            altitudes[num_vertices:, ...] = edge_weights[mst_edge_map, ...]

    if input_is_graph_object:
        # if the base graph is itself a mst, we take the base graph of this mst as the new base graph
        if hg.CptMinimumSpanningTree.validate(graph):
            leaf_graph = hg.CptMinimumSpanningTree.construct(
                graph)["base_graph"]
        else:
            leaf_graph = graph

        hg.CptHierarchy.link(tree, leaf_graph)

    if compute_mst and input_is_graph_object:
        mst = hg.subgraph(graph, mst_edge_map)
        hg.CptMinimumSpanningTree.link(mst, leaf_graph, mst_edge_map)
    else:
        mst = None

    hg.CptBinaryHierarchy.link(tree, mst_edge_map, mst)

    if not return_altitudes:
        return tree
    else:
        return tree, altitudes