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
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)))
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)))
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
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
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