def attribute_gaussian_region_weights_model(tree, vertex_weights, leaf_graph=None): """ Estimates a gaussian model (mean, (co-)variance) for leaf weights inside a node. The result is composed of two arrays: - the first one contains the mean value inside each node, scalar if vertex weights are scalar and vectorial otherwise, - the second one contains the variance of the values inside each node, scalar if vertex weights are scalar and a (biased) covariance matrix otherwise. Vertex weights must be scalar or 1 dimensional. :param tree: input tree (Concept :class:`~higra.CptHierarchy`) :param vertex_weights: vertex weights of the leaf graph of the input tree :param leaf_graph: leaf graph of the input tree (deduced from :class:`~higra.CptHierarchy`) :return: two arrays mean and variance """ if leaf_graph is not None: vertex_weights = hg.linearize_vertex_weights(vertex_weights, leaf_graph) if vertex_weights.ndim > 2: raise ValueError( "Vertex weight can either be scalar or 1 dimensional.") if vertex_weights.dtype not in (np.float32, np.float64): vertex_weights = vertex_weights.astype(np.float64) area = hg.attribute_area(tree, leaf_graph=leaf_graph) mean = hg.accumulate_sequential(tree, vertex_weights, hg.Accumulators.sum, leaf_graph) if vertex_weights.ndim == 1: # general case below would work but this is simpler mean /= area mean2 = hg.accumulate_sequential(tree, vertex_weights * vertex_weights, hg.Accumulators.sum, leaf_graph) mean2 /= area variance = mean2 - mean * mean else: mean /= area[:, None] tmp = vertex_weights[:, :, None] * vertex_weights[:, None, :] mean2 = hg.accumulate_sequential(tree, tmp, hg.Accumulators.sum, leaf_graph) mean2 /= area[:, None, None] variance = mean2 - mean[:, :, None] * mean[:, None, :] return mean, variance
def attribute_mean_vertex_weights(tree, vertex_weights, area=None, leaf_graph=None): """ Mean vertex weights of the leaf graph vertices inside each node of the given tree. For any node :math:`n`, the mean vertex weights :math:`a(n)` of :math:`n` is .. math:: a(n) = \\frac{\sum_{x\in n} vertex\_weights(x)}{area(n)} :param tree: input tree (Concept :class:`~higra.CptHierarchy`) :param vertex_weights: vertex weights of the leaf graph of the input tree :param area: area of the tree nodes (provided by :func:`~higra.attribute_area`) :param leaf_graph: leaf graph of the input tree (deduced from :class:`~higra.CptHierarchy`) :return: a nd array """ if area is None: area = hg.attribute_area(tree) if leaf_graph is not None: vertex_weights = hg.linearize_vertex_weights(vertex_weights, leaf_graph) attribute = hg.accumulate_sequential( tree, vertex_weights.astype(np.float64), hg.Accumulators.sum) / area.reshape([-1] + [1] * (vertex_weights.ndim - 1)) return attribute
def dendrogram_purity_naif(tree, leaf_labels): from itertools import combinations tree.lowest_common_ancestor_preprocess() area = hg.attribute_area(tree) max_label = np.max(leaf_labels) label_histo = np.zeros((tree.num_leaves(), max_label + 1), dtype=np.int64) label_histo[np.arange(tree.num_leaves()), leaf_labels] = 1 label_histo = hg.accumulate_sequential(tree, label_histo, hg.Accumulators.sum) class_purity = label_histo / area[:, None] count = 0 total = 0 for label in set(leaf_labels): same = leaf_labels == label same_indices, = same.nonzero() if len(same_indices) < 2: continue pairs = list(combinations(same_indices, 2)) count += len(pairs) pairs = np.asarray(pairs, dtype=np.int64) lcas = tree.lowest_common_ancestor(pairs[:, 0], pairs[:, 1]) total += np.sum(class_purity[lcas, label]) return total / count
def labelisation_seeded_watershed(graph, edge_weights, vertex_seeds): """ Seeded watershed cut on an edge weighted graph. Seeds are defined as vertex weights: any flat zone of value strictly greater than 0 is considered as a seed. Note that if two different seeds are places in a minima of the edge weighted graph, and if the altitude of this minima is equal to the smallest representable value for the given `dtype` of the edge weights, then the algorithm won't be able to produce two different regions for these two seeds. :param graph: input graph :param edge_weights: Weights on the edges of the graph :param vertex_seeds: Seeds on the vertices of the graph :return: A labelisation of the graph vertices """ # edges inside a seed take the value of the seed and 0 otherwise edges_in_or_between_seeds = hg.weight_graph(graph, vertex_seeds, hg.WeightFunction.L0) edges_outside_seeds = hg.weight_graph(graph, vertex_seeds, hg.WeightFunction.min) edges_in_seed = np.logical_and(edges_outside_seeds > 0, 1 - edges_in_or_between_seeds) # set edges inside seeds at minimum level edge_weights = edge_weights.copy() edge_weights[edges_in_seed > 0] = hg.dtype_info(edge_weights.dtype).min tree, altitudes = hg.watershed_hierarchy_by_attribute( graph, edge_weights, lambda tree, _: hg.accumulate_sequential( tree, vertex_seeds, hg.Accumulators.max)) return hg.labelisation_hierarchy_supervertices(tree, altitudes)
def test_tree_accumulator(self): tree = TestTreeAccumulators.get_tree() input_array = np.asarray((1, 1, 1, 1, 1, 1, 1, 1)) res1 = hg.accumulate_parallel(tree, input_array, hg.Accumulators.sum) ref1 = np.asarray((0, 0, 0, 0, 0, 2, 3, 2)) self.assertTrue(np.allclose(ref1, res1)) leaf_data = np.asarray((1, 1, 1, 1, 1)) res2 = hg.accumulate_sequential(tree, leaf_data, hg.Accumulators.sum) ref2 = np.asarray((1, 1, 1, 1, 1, 2, 3, 5)) self.assertTrue(np.allclose(ref2, res2)) res3 = hg.accumulate_and_add_sequential(tree, input_array, leaf_data, hg.Accumulators.max) ref3 = np.asarray((1, 1, 1, 1, 1, 2, 2, 3)) self.assertTrue(np.allclose(ref3, res3)) input_array = np.asarray((1, 2, 1, 2, 1, 1, 4, 5)) res4 = hg.accumulate_and_max_sequential(tree, input_array, leaf_data, hg.Accumulators.sum) ref4 = np.asarray((1, 1, 1, 1, 1, 2, 4, 6)) self.assertTrue(np.allclose(ref4, res4)) input_array = np.asarray((1, 2, 1, 2, 1, 2, 3, 1)) res5 = hg.accumulate_and_multiply_sequential(tree, input_array, leaf_data, hg.Accumulators.sum) ref5 = np.asarray((1, 1, 1, 1, 1, 4, 9, 13)) self.assertTrue(np.allclose(ref5, res5)) input_array = np.asarray((1, 2, 1, 2, 1, 4, 2, 10)) res6 = hg.accumulate_and_min_sequential(tree, input_array, leaf_data, hg.Accumulators.sum) ref6 = np.asarray((1, 1, 1, 1, 1, 2, 2, 4)) self.assertTrue(np.allclose(ref6, res6))
def dendrogram_purity(tree, leaf_labels): """ Weighted average of the purity of each node of the tree with respect to a ground truth labelization of the tree leaves. Let :math:`T` be a tree with leaves :math:`V=\{1, \ldots, n\}`. Let :math:`C=\{C_1, \ldots, C_K\}` be a partition of :math:`V` into :math:`k` (label) sets. The purity of a subset :math:`X` of :math:`V` with respect to class :math:`C_\ell\in C` is the fraction of elements of :math:`X` that belongs to class :math:`C_\ell`: .. math:: pur(X, C_\ell) = \\frac{| X \cap C_\ell |}{| X |}. The purity of :math:`T` is the defined as: .. math:: pur(T) = \\frac{1}{Z}\sum_{k=1}^{K}\sum_{x,y\in C_k, x\\neq y} pur(lca_T(x,y), C_k) with :math:`Z=| \{\{x,y\} \subseteq V \mid x\\neq y, \exists k, \{x,y\}\subseteq C_k\} |`. :See: Heller, Katherine A., and Zoubin Ghahramani. "`Bayesian hierarchical clustering <https://www2.stat.duke.edu/~kheller/bhcnew.pdf>`_ ." Proc. ICML. ACM, 2005. :Complexity: The dendrogram purity is computed in :math:`\mathcal{O}(N\\times K \\times C^2)` with :math:`N` the number of nodes in the tree, :math:`K` the number of classes, and :math:`C` the maximal number of children of a node in the tree. :param tree: input tree :param leaf_labels: a 1d integral array of length `tree.num_leaves()` :return: a score between 0 and 1 (higher is better) """ if leaf_labels.ndim != 1 or leaf_labels.size != tree.num_leaves( ) or leaf_labels.dtype.kind != 'i': raise ValueError( "leaf_labels must be a 1d integral array of length `tree.num_leaves()`" ) num_l = tree.num_leaves() area = hg.attribute_area(tree) max_label = np.max(leaf_labels) num_labels = max_label + 1 label_histo_leaves = np.zeros((num_l, num_labels), dtype=np.float64) label_histo_leaves[np.arange(num_l), leaf_labels] = 1 label_histo = hg.accumulate_sequential(tree, label_histo_leaves, hg.Accumulators.sum) class_purity = label_histo / area[:, np.newaxis] weights = hg.attribute_children_pair_sum_product(tree, label_histo) total = np.sum(class_purity[num_l:, :] * weights[num_l:, :]) return total / np.sum(weights[num_l:])
def propagate_weights(tree, label_map, weight_map): # tree: input tree where leaves are superpixels # label_map: 2d array of integers of shape (M,N) # weight_map: 2d array of shape (M,N) # # return: propagated probabilities on each node of the tree regions = regionprops(label_map + 1, intensity_image=weight_map) # get area of all nodes area_leaves = np.array([p['area'] for p in regions]) areas = hg.accumulate_sequential(tree, area_leaves, hg.Accumulators.sum) # get sum of proba on each leaf means = np.array([p['mean_intensity'] for p in regions]) sums = means * area_leaves attribute = hg.accumulate_sequential( tree, sums, hg.Accumulators.sum) / areas.reshape([-1] + [1] * (sums.ndim - 1)) return attribute
def candidate_subtrees(tree, label_map, leaves_weights, clicked_coords=[]): # clicked_coords is a list of ij coordinates # returns list [(accumulated_weight, [l0, ...])] # where [l0, ...] are reachable leaves # leaves_weights will be propagated. # 1d array: It is taken as an area-normalized (average pooling) metric # 2d array: It will be average pooled according to label_map # # returns: # - weights of each node # - indices of parent nodes that are eligible (from which one can reach clicked) leaves = np.unique(label_map) regions = regionprops(label_map + 1) if (leaves_weights.ndim == 2): regions = regionprops(label_map + 1, intensity_image=leaves_weights) leaves_weights = np.array([p['mean_intensity'] for p in regions]) elif (leaves_weights.ndim == 1): pass else: raise TypeError('leaves_weights: Only 1-D and 2-D arrays supported.') area_leaves = np.array([p['area'] for p in regions]) areas = hg.accumulate_sequential(tree, area_leaves, hg.Accumulators.sum) sums = leaves_weights * area_leaves # each node is area-weighted sum of weights accum_weights = hg.accumulate_sequential( tree, sums, hg.Accumulators.sum) / areas.reshape([-1] + [1] * (sums.ndim - 1)) # get list [(a, [l0, ...])] where a is a candidate ancestor and [l0, ...] are its leaves parents = [] for r in clicked_coords: clicked_label = label_map[tuple(r)] parents.extend(tree.ancestors(clicked_label)[1:]) return accum_weights, np.array(parents)
def _get_associated_mst(tree, altitudes): """ Create a valid edge mst for the given tree (returns an edge weighted undirected graph) """ nb = tree.num_leaves() link_v = np.arange(nb) link_v = hg.accumulate_sequential(tree, link_v, hg.Accumulators.first) g = hg.UndirectedGraph(nb) edge_weights = np.zeros((nb - 1,), np.float32) for r in tree.leaves_to_root_iterator(include_leaves=False): g.add_edge(link_v[tree.child(0, r)], link_v[tree.child(1, r)]) edge_weights[r - nb] = altitudes[r] return g, edge_weights
def test_tree_accumulatorVec(self): tree = TestTreeAccumulators.get_tree() input_array = np.asarray(((1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7))) res1 = hg.accumulate_parallel(tree, input_array, hg.Accumulators.sum) ref1 = np.asarray(((0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (2, 1), (3, 9), (2, 11))) self.assertTrue(np.allclose(ref1, res1)) leaf_data = np.asarray(((1, 0), (1, 1), (1, 2), (1, 3), (1, 4))) res2 = hg.accumulate_sequential(tree, leaf_data, hg.Accumulators.sum) ref2 = np.asarray(((1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (2, 1), (3, 9), (5, 10))) self.assertTrue(np.allclose(ref2, res2)) res3 = hg.accumulate_and_add_sequential(tree, input_array, leaf_data, hg.Accumulators.sum) ref3 = np.asarray(((1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (3, 6), (4, 15), (8, 28))) self.assertTrue(np.allclose(ref3, res3))
def attribute_area(tree, vertex_area=None, leaf_graph=None): """ Area of each node the given tree. The area of a node is equal to the sum of the area of the leaves of the subtree rooted in the node. **Provider name**: "area" :param tree: input tree (Concept :class:`~higra.CptHierarchy`) :param vertex_area: area of the vertices of the leaf graph of the tree (provided by :func:`~higra.attribute_vertex_area` on `leaf_graph` ) :param leaf_graph: (deduced from :class:`~higra.CptHierarchy`) :return: a 1d array """ if vertex_area is None: vertex_area = np.ones((tree.num_leaves(),), dtype=np.float64) if leaf_graph is not None: vertex_area = hg.linearize_vertex_weights(vertex_area, leaf_graph) return hg.accumulate_sequential(tree, vertex_area, hg.Accumulators.sum)
def attribute_mean_weights(tree, vertex_weights, area, leaf_graph=None): """ Mean weight of the leaf graph vertices inside each node of the given tree. **Provider name**: "mean_weights" :param tree: input tree (Concept :class:`~higra.CptHierarchy`) :param vertex_weights: vertex weights of the leaf graph of the input tree :param area: area of the tree nodes (provided by :func:`~higra.attribute_area` on `tree`) :param leaf_graph: leaf graph of the input tree (deduced from :class:`~higra.CptHierarchy`) :return: a nd array """ if leaf_graph is not None: vertex_weights = hg.linearize_vertex_weights(vertex_weights, leaf_graph) attribute = hg.accumulate_sequential( tree, vertex_weights.astype(np.float64), hg.Accumulators.sum) / area.reshape((-1, 1)) return attribute
def attribute_tree_sampling_probability(tree, leaf_graph, leaf_graph_edge_weights, model='edge'): """ Given a tree :math:`T`, estimate the probability that a node :math:`n` of the tree represents the smallest cluster containing a pair of vertices :math:`\{a, b\}` of the graph :math:`G=(V, E)` with edge weights :math:`w`. This method is defined in [1]_. We define the probability :math:`P(\{a,b\})` of a pair of vertices :math:`\{a,b\}` as :math:`w(\{a,b\}) / Z` with :math:`Z=\sum_{e\in E}w(E)` if :math:`\{a,b\}` is an edge of :math:`G` and 0 otherwise. Then the probability :math:`P(a)` of a vertex :math:`b` is defined as :math:`\sum_{b\in V}P(\{a, b\})` Two sampling strategies are proposed for sampling pairs of vertices to compute the probability of a node of the tree: - *edge*: the probability of sampling the pair :math:`\{a, b\}` is given by :math:`P(\{a, b\})`; and - *null*: the probability of sampling the pair :math:`\{a, b\}` is given by the product of the probabilities of :math:`a` and :math:`b`: :math:`P(a)*P(b)`. Assuming that the edge weights on the leaf graph of a hierarchy represents similarities: .. epigraph:: *We expect these distributions to differ significantly if the tree indeed represents the hierarchical structure of the graph. Specifically, we expect [the edge distribution] to be mostly concentrated on deep nodes of the tree (far from the root), as two nodes* :math:`u`, :math:`v` *connected with high weight* :math:`w(\{u, v\})` *in the graph typically belong to a small cluster, representative of the clustering structure of the graph; on the contrary, we expect [the null distribution] to be concentrated over shallow nodes (close to the root) as two nodes* :math:`w(\{u, v\})` *sampled independently at random typically belong to large clusters, less representative of the clustering structure of the graph*. [1]_ .. [1] Charpentier, B. & Bonald, T. (2019). `"Tree Sampling Divergence: An Information-Theoretic Metric for \ Hierarchical Graph Clustering." <https://hal.telecom-paristech.fr/hal-02144394/document>`_ Proceedings of IJCAI. :Complexity: The tree sampling divergence runtime complexity depends of the sampling model: - *edge*: :math:`\mathcal{O}(N\log(N) + M)` with :math:`N` the number of nodes in the tree and :math:`M` the number of edges in the leaf graph. - *null*: :math:`\mathcal{O}(N\\times C^2)` with :math:`N` the number of nodes in the tree and :math:`C` the maximal number of children of a node in the tree. :see: The :func:`~higra.tree_sampling_divergence` is a non supervised hierarchical cost function defined as the Kullback-Leibler divergence between the edge sampling model and the independent (null) sampling model. :param tree: Input tree :param leaf_graph: Graph defined on the leaves of the input tree :param leaf_graph_edge_weights: Edge weights of the leaf graphs (similarities) :param model: defines the edge sampling strategy, either "edge" or "null" :return: a 1d array """ if model not in ("edge", "null"): raise ValueError("Parameter 'model' must be either 'edge' or 'null'.") if model == 'edge': lca_map = hg.attribute_lca_map(tree, leaf_graph=leaf_graph) leaf_graph_edge_weights = leaf_graph_edge_weights / np.sum(leaf_graph_edge_weights) return hg.accumulate_at(lca_map, leaf_graph_edge_weights, hg.Accumulators.sum) else: # model = 'null' leaf_graph_vertex_weights = hg.accumulate_graph_edges(leaf_graph, leaf_graph_edge_weights, hg.Accumulators.sum) leaf_graph_vertex_weights = leaf_graph_vertex_weights / np.sum(leaf_graph_edge_weights) tree_node_weights = hg.accumulate_sequential(tree, leaf_graph_vertex_weights, hg.Accumulators.sum) return hg.attribute_children_pair_sum_product(tree, tree_node_weights)
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
def attribute_moment_of_inertia(tree, leaf_graph): """ Moment of inertia (first Hu moment) of each node of the given tree. This function works only if :attr:`leaf_graph` is a 2D grid graph. The moment of inertia is a translation, scale and rotation invariant characterization of the shape of the nodes. Given a node :math:`X` of :attr:`tree`, the raw moments :math:`M_{ij}` are defined as: .. math:: M_{ij} = \sum_{x}\sum_{y} x^i y^j where :math:`(x,y)` are the coordinates of every vertex in :math:`X`. Then, the centroid :math:`\{\overline{x},\overline{y}\}` of :math:`X` is given by .. math:: \overline{x} = \\frac{M_{10}}{M_{00}} \\textrm{ and } \overline{y} = \\frac{M_{01}}{M_{00}} Some central moments of :math:`X` are then: - :math:`\mu_{00} = M_{00}` - :math:`\mu_{20} = M_{20} - \overline{x} \\times M_{10}` - :math:`\mu_{02} = M_{02} - \overline{y} \\times M_{01}` The moment of inertia :math:`I_1` of :math:`X` if finally defined as .. math:: I_1 = \eta_{20} + \eta_{02} where :math:`\eta_{ij}` are given by: :math:`\eta_{ij} = \\frac{\mu_{ij}}{\mu_{00}^{1+\\frac{i+j}{2}}}` :param tree: input tree (Concept :class:`~higra.CptHierarchy`) :param leaf_graph: graph on the leaves of the input tree (deduced from :class:`~higra.CptHierarchy` on `tree`) :return: a 1d array """ if (not hg.CptGridGraph.validate(leaf_graph)) or (len( hg.CptGridGraph.get_shape(leaf_graph)) != 2): raise ValueError("Parameter 'leaf_graph' must be a 2D grid graph.") coordinates = hg.attribute_vertex_coordinates(leaf_graph) coordinates = np.reshape( coordinates, (coordinates.shape[0] * coordinates.shape[1], coordinates.shape[2])) M_00_leaves = np.ones((tree.num_leaves())).astype(dtype=np.float64) x_leaves = coordinates[:, 0].astype(dtype=np.float64) y_leaves = coordinates[:, 1].astype(dtype=np.float64) M_10_leaves = x_leaves * M_00_leaves M_01_leaves = y_leaves * M_00_leaves M_20_leaves = (x_leaves**2) * M_00_leaves M_02_leaves = (y_leaves**2) * M_00_leaves M_00 = hg.accumulate_sequential(tree, M_00_leaves, hg.Accumulators.sum) M_01 = hg.accumulate_sequential(tree, M_01_leaves, hg.Accumulators.sum) M_10 = hg.accumulate_sequential(tree, M_10_leaves, hg.Accumulators.sum) M_02 = hg.accumulate_sequential(tree, M_02_leaves, hg.Accumulators.sum) M_20 = hg.accumulate_sequential(tree, M_20_leaves, hg.Accumulators.sum) _x = M_10 / M_00 _y = M_01 / M_00 miu_20 = M_20 - _x * M_10 miu_02 = M_02 - _y * M_01 I_1 = (miu_20 + miu_02) / (M_00**2) return I_1