def canonize_hierarchy(tree, altitudes, return_node_map=False): """ Removes consecutive tree nodes with equal altitudes. The new tree is composed of the inner nodes :math:`n` of the input tree such that :math:`altitudes[n] \\neq altitudes[tree.parent(n)]` or :math:`n = tree.root(n)`. For example, applying this function to the result of :func:`~higra.bpt_canonical` on an edge weighted graph is the same as computing the :func:`~higra.quasi_flat_zone_hierarchy` of the same edge weighted graph. If :attr:`return_node_map` is ``True``, an extra array that maps any vertex index :math:`i` of the new tree, to the index of the corresponding vertex in the original tree is returned. :param tree: input tree :param altitudes: altitudes of the vertices of the tree :param return_node_map: if ``True``, also return the node map. :return: a tree (Concept :class:`~higra.CptHierarchy` if input tree already satisfied this concept) its node altitudes, and, if requested, its node map. """ tree, node_map = hg.simplify_tree(tree, altitudes == altitudes[tree.parents()]) new_altitudes = altitudes[node_map] if return_node_map: return tree, new_altitudes, node_map else: return tree, new_altitudes
def num_parents(bpt_tree, altitudes): # construct quasi flat zone hierarchy from input bpt tree, node_map = hg.simplify_tree( bpt_tree, altitudes == altitudes[bpt_tree.parents()]) # determine inner nodes of the min tree, i.e. nodes of qfz having at least one node that is not a leaf num_children = tree.num_children(np.arange(tree.num_vertices())) num_children_leaf = np.zeros((tree.num_vertices(), ), dtype=np.int64) np.add.at(num_children_leaf, tree.parents()[:tree.num_leaves()], 1) inner_nodes = num_children != num_children_leaf # go back into bpt space inner_nodes_bpt = np.zeros((bpt_tree.num_vertices(), ), dtype=np.int64) inner_nodes_bpt[node_map] = inner_nodes inner_nodes = inner_nodes_bpt # count number of min tree inner nodes in the subtree rooted in the given node res = hg.accumulate_and_add_sequential(bpt_tree, inner_nodes, inner_nodes[:tree.num_leaves()], hg.Accumulators.sum) # add 1 to avoid having a zero measure in a minima res[bpt_tree.num_leaves():] = res[bpt_tree.num_leaves():] + 1 return res
def constrained_connectivity_hierarchy_strong_connection(graph, edge_weights): """ Strongly constrained connectivity hierarchy based on the given edge weighted graph. Let :math:`X` be a set of vertices, the range of :math:`X` is the maximal weight of the edges linking two vertices inside :math:`X`. Let :math:`\\alpha` be a positive real number, a set of vertices :math:`X` is :math:`\\alpha`-connected, if for any two vertices :math:`i` and :math:`j` in :math:`X`, there exists a path from :math:`i` to :math:`j` in :math:`X` composed of edges of weights lower than or equal to :math:`\\alpha`. Let :math:`\\alpha` be a positive real numbers, the :math:`\\alpha`-strongly connected components of the graph are the maximal :math:`\\alpha'`-connected sets of vertices with a range lower than or equal to :math:`\\alpha` with :math:`\\alpha'\leq\\alpha`. Finally, the strongly constrained connectivity hierarchy is defined as the hierarchy composed of all the :math:`\\alpha`- strongly connected components for all positive :math:`\\alpha`. The definition used follows the one given in: P. Soille, "Constrained connectivity for hierarchical image partitioning and simplification," in IEEE Transactions on Pattern Analysis and Machine Intelligence, vol. 30, no. 7, pp. 1132-1145, July 2008. doi: 10.1109/TPAMI.2007.70817 The algorithm runs in time :math:`\mathcal{O}(n\log(n))` and proceeds by filtering a quasi-flat zone hierarchy (see :func:`~higra.quasi_flat_zones_hierarchy`) :param graph: input graph :param edge_weights: edge_weights: edge weights of the input graph :return: a tree (Concept :class:`~higra.CptHierarchy`) and its node altitudes """ tree, altitudes = hg.quasi_flat_zone_hierarchy(graph, edge_weights) altitude_parents = altitudes[tree.parents()] # max edge weights inside each region lca_map = hg.attribute_lca_map(tree) max_edge_weights = np.zeros((tree.num_vertices(),), dtype=edge_weights.dtype) np.maximum.at(max_edge_weights, lca_map, edge_weights) max_edge_weights = hg.accumulate_and_max_sequential(tree, max_edge_weights, max_edge_weights[:tree.num_leaves()], hg.Accumulators.max) # parent node can't be deleted altitude_parents[tree.root()] = max(altitudes[tree.root()], max_edge_weights[tree.root()]) # nodes whith a range greater than the altitudes of their parent have to be deleted violated_constraints = max_edge_weights >= altitude_parents # the altitude of nodes with a range greater than their altitude but lower than the one of their parent must be changed reparable_node_indices = np.nonzero( np.logical_and(max_edge_weights > altitudes, max_edge_weights < altitude_parents)) altitudes[reparable_node_indices] = max_edge_weights[reparable_node_indices] # final result construction tree, node_map = hg.simplify_tree(tree, violated_constraints) altitudes = altitudes[node_map] hg.CptHierarchy.link(tree, graph) return tree, altitudes
def test_simplify_tree_with_leaves3(self): t = hg.Tree((7, 7, 8, 8, 8, 9, 9, 11, 10, 10, 11, 11)) criterion = np.zeros(t.num_vertices(), dtype=np.bool) criterion[:9] = True new_tree, node_map = hg.simplify_tree(t, criterion, process_leaves=True) ref_tree = hg.Tree((1, 2, 2)) self.assertTrue(hg.test_tree_isomorphism(new_tree, ref_tree)) self.assertFalse(np.max(criterion[node_map]))
def test_simplify_tree_with_leaves(self): t = hg.Tree((7, 7, 8, 8, 8, 9, 9, 11, 10, 10, 11, 11)) criterion = np.asarray((False, False, False, True, True, True, True, False, True, False, True, False), dtype=np.bool) new_tree, node_map = hg.simplify_tree(t, criterion, process_leaves=True) ref_tree = hg.Tree((4, 4, 5, 5, 5, 5)) self.assertTrue(hg.test_tree_isomorphism(new_tree, ref_tree)) self.assertFalse(np.max(criterion[node_map]))
def test_simplify_tree_propagate_category(self): g = hg.get_4_adjacency_implicit_graph((1, 6)) vertex_values = np.asarray((1, 5, 4, 3, 3, 6), dtype=np.int32) tree, altitudes = hg.component_tree_max_tree(g, vertex_values) condition = np.asarray((False, False, False, False, False, False, False, True, False, True, False), np.bool) new_tree, node_map = hg.simplify_tree(tree, condition) self.assertTrue( np.all(new_tree.parents() == (8, 7, 7, 8, 8, 6, 8, 8, 8))) self.assertTrue(np.all(node_map == (0, 1, 2, 3, 4, 5, 6, 8, 10))) self.assertTrue(new_tree.category() == hg.TreeCategory.ComponentTree) rec = hg.reconstruct_leaf_data(new_tree, altitudes[node_map]) self.assertTrue(np.all(rec == (1, 4, 4, 1, 1, 6)))
def canonize_hierarchy(tree, altitudes): """ Removes consecutive tree nodes with equal altitudes. The new tree is composed of the inner nodes :math:`n` of the input tree such that :math:`altitudes[n] \\neq altitudes[tree.parent(n)]` or :math:`n = tree.root(n)`. For example, applying this function to the result of :func:`~higra.bpt_canonical` on an edge weighted graph is the same as computing the :func:`~higra.quasi_flat_zone_hierarchy` of the same edge weighted graph. :param tree: input tree :param altitudes: altitudes of the vertices of the tree :return: a tree (Concept :class:`~higra.CptHierarchy` if input tree already satisfied this concept) and its node altitudes """ ctree, node_map = hg.simplify_tree(tree, altitudes == altitudes[tree.parents()]) return ctree, altitudes[node_map]
def test_simplifyTreeWithLeaves(self): t = hg.Tree((8, 8, 9, 7, 7, 11, 11, 9, 10, 10, 12, 12, 12)) criterion = np.asarray((False, True, True, False, False, False, False, False, True, True, False, False), dtype=np.bool) new_tree, node_map = hg.simplify_tree(t, criterion, process_leaves=True) self.assertTrue(new_tree.num_vertices() == 9) refp = np.asarray((6, 5, 5, 7, 7, 6, 8, 8, 8)) self.assertTrue(np.all(refp == new_tree.parents())) refnm = np.asarray((0, 3, 4, 5, 6, 7, 10, 11, 12)) self.assertTrue(np.all(refnm == node_map))
def test_simplify_tree(self): t = TestHierarchyCore.getTree() altitudes = np.asarray((0, 0, 0, 0, 0, 1, 2, 2)) criterion = np.equal(altitudes, altitudes[t.parents()]) new_tree, node_map = hg.simplify_tree(t, criterion) # for reference new_altitudes = altitudes[node_map] self.assertTrue(new_tree.num_vertices() == 7) refp = np.asarray((5, 5, 6, 6, 6, 6, 6)) self.assertTrue(np.all(refp == new_tree.parents())) refnm = np.asarray((0, 1, 2, 3, 4, 5, 7)) self.assertTrue(np.all(refnm == node_map))
def constrained_connectivity_hierarchy_alpha_omega(graph, vertex_weights): """ Alpha-omega constrained connectivity hierarchy based on the given vertex weighted graph. For :math:`(i,j)` be an edge of the graph, we define :math:`w(i,j)=|w(i) - w(j)|`, the weight of this edge. Let :math:`X` be a set of vertices, the range of :math:`X` is the maximal absolute difference between the weights of any two vertices in :math:`X`: :math:`R(X) = \max\{|w(i) - w(j)|, (i,j)\in X^2\}` Let :math:`\\alpha` be a positive real number, a set of vertices :math:`X` is :math:`\\alpha`-connected, if for any two vertices :math:`i` and :math:`j` in :math:`X`, there exists a path from :math:`i` to :math:`j` in :math:`X` composed of edges of weights lower than or equal to :math:`\\alpha`. Let :math:`\\alpha` and :math:`\omega` be a two positive real numbers, the :math:`\\alpha-\omega`-connected components of the graph are the maximal :math:`\\alpha'`-connected sets of vertices with a range lower than or equal to :math:`\omega`, with :math:`\\alpha'\leq\\alpha`. Finally, the alpha-omega constrained connectivity hierarchy is defined as the hierarchy composed of all the :math:`k-k`-connected components for all positive :math:`k`. The definition used follows the one given in: P. Soille, "Constrained connectivity for hierarchical image partitioning and simplification," in IEEE Transactions on Pattern Analysis and Machine Intelligence, vol. 30, no. 7, pp. 1132-1145, July 2008. doi: 10.1109/TPAMI.2007.70817 The algorithm runs in time :math:`\mathcal{O}(n\log(n))` and proceeds by filtering a quasi-flat zone hierarchy (see :func:`~higra.quasi_flat_zones_hierarchy`) :param graph: input graph :param vertex_weights: edge_weights: edge weights of the input graph :return: a tree (Concept :class:`~higra.CptHierarchy`) and its node altitudes """ vertex_weights = hg.linearize_vertex_weights(vertex_weights, graph) if vertex_weights.ndim != 1: raise ValueError("constrainted_connectivity_hierarchy_alpha_omega only works for scalar vertex weights.") # QFZ on the L1 distance weighted graph edge_weights = hg.weight_graph(graph, vertex_weights, hg.WeightFunction.L1) tree, altitudes = hg.quasi_flat_zone_hierarchy(graph, edge_weights) altitude_parents = altitudes[tree.parents()] # vertex value range inside each region min_value = hg.accumulate_sequential(tree, vertex_weights, hg.Accumulators.min) max_value = hg.accumulate_sequential(tree, vertex_weights, hg.Accumulators.max) value_range = max_value - min_value # parent node can't be deleted altitude_parents[tree.root()] = max(altitudes[tree.root()], value_range[tree.root()]) # nodes whith a range greater than the altitudes of their parent have to be deleted violated_constraints = value_range >= altitude_parents # the altitude of nodes with a range greater than their altitude but lower than the one of their parent must be changed reparable_node_indices = np.nonzero(np.logical_and(value_range > altitudes, value_range < altitude_parents)) altitudes[reparable_node_indices] = value_range[reparable_node_indices] # final result construction tree, node_map = hg.simplify_tree(tree, violated_constraints) altitudes = altitudes[node_map] hg.CptHierarchy.link(tree, graph) return tree, altitudes
def component_tree_multivariate_tree_of_shapes_image2d(image, padding='mean', original_size=True, immersion=True): """ Multivariate tree of shapes for a 2d multi-band image. This tree is defined as a fusion of the marginal trees of shapes. The method is described in: E. Carlinet. `A Tree of shapes for multivariate images <https://pastel.archives-ouvertes.fr/tel-01280131/file/TH2015PESC1118.pdf>`_. PhD Thesis, Université Paris-Est, 2015. The input :attr:`image` must be a 3d array of shape :math:`(height, width, channel)`. Note that the constructed hierarchy doesn't have natural altitudes associated to its node: as a node is generally a fusion of several marginal nodes, we can't associate a single canonical value from the original image to this node. The parameters :attr:`padding`, :attr:`original_size`, and :attr:`immersion` are forwarded to the function :func:`~higra.component_tree_tree_of_shapes_image2d`: please look at this function documentation for more details. :Complexity: The worst case runtime complexity of this method is :math:`\mathcal{O}(N^2D^2)` with :math:`N` the number of pixels and :math:`D` the number of bands. If the image pixel values are quantized on :math:`K<<N` different values (eg. with a color image in :math:`[0..255]^D`), then the worst case runtime complexity can be tightened to :math:`\mathcal{O}(NKD^2)`. :See: This function relies on :func:`~higra.tree_fusion_depth_map` to compute the fusion of the marinal trees. :param image: input *color* 2d image :param padding: possible values are `'none'`, `'zero'`, and `'mean'` (default = `'mean'`) :param original_size: remove all nodes corresponding to interpolated/padded pixels (default = `True`) :param immersion: performs a plain map continuous immersion fo the original image (default = `True`) :return: a tree (Concept :class:`~higra.CptHierarchy`) """ assert len( image.shape ) == 3, "This multivariate tree of shapes implementation only supports multichannel 2d images." ndim = image.shape[2] trees = tuple( hg.component_tree_tree_of_shapes_image2d( image[:, :, k], padding, original_size=False, immersion=immersion)[0] for k in range(ndim)) g = hg.CptHierarchy.get_leaf_graph(trees[0]) shape = hg.CptGridGraph.get_shape(g) depth_map = hg.tree_fusion_depth_map(trees) tree, altitudes = hg.component_tree_tree_of_shapes_image2d( np.reshape(depth_map, shape), padding="none", original_size=False, immersion=False) if original_size and (immersion or padding != "none"): deleted_vertices = np.ones((tree.num_leaves(), ), dtype=np.bool) deleted = np.reshape(deleted_vertices, shape) if immersion: if padding != "none": deleted[2:-2:2, 2:-2:2] = False else: deleted[0::2, 0::2] = False else: if padding != "none": deleted[1:-1, 1::-1] = False all_deleted = hg.accumulate_sequential(tree, deleted_vertices, hg.Accumulators.min) shape = (image.shape[0], image.shape[1]) else: all_deleted = np.zeros((tree.num_vertices(), ), dtype=np.bool) holes = altitudes < altitudes[tree.parents()] all_deleted = np.logical_or(all_deleted, holes) tree, _ = hg.simplify_tree(tree, all_deleted, process_leaves=True) g = hg.get_4_adjacency_graph(shape) hg.CptHierarchy.link(tree, g) return tree