Source code for openalea.phenomenal.segmentation.maize_analysis

# -*- python -*-
#
#       Copyright INRIA - CIRAD - INRA
#
#       Distributed under the Cecill-C License.
#       See accompanying file LICENSE.txt or copy at
#           http://www.cecill.info/licences/Licence_CeCILL-C_V1-en.html
#
# ==============================================================================
from __future__ import print_function, absolute_import

import math
import numpy
import scipy.integrate

from .plane_interception import (
    intercept_points_along_path_with_planes,
    max_distance_in_points,
)


# ==============================================================================


def unit_vector(vector):
    """Returns the unit vector of the vector."""
    return vector / numpy.linalg.norm(vector)


def angle_between(v1, v2):
    """Returns the angle in radians between vectors 'v1' and 'v2'::
    0 and between 2pi

        >>> angle_between((1, 0, 0), (0, 1, 0))
        1.5707963267948966
        >>> angle_between((1, 0, 0), (1, 0, 0))
        0.0
        >>> angle_between((1, 0, 0), (-1, 0, 0))
        3.141592653589793
    """
    v1_u = unit_vector(v1)
    v2_u = unit_vector(v2)
    return numpy.arccos(numpy.clip(numpy.dot(v1_u, v2_u), -1.0, 1.0))


def get_max_distance(node, nodes):
    max_distance = 0
    max_node = node

    for n in nodes:
        distance = abs(numpy.linalg.norm(numpy.array(node) - numpy.array(n)))
        if distance >= max_distance:
            max_distance = distance
            max_node = n

    return max_node, max_distance


def compute_width_organ(closest_nodes):
    width = []
    for nodes in closest_nodes:
        width.append(max_distance_in_points(nodes))

    return width


def compute_curvilinear_abscissa(polyline, length):
    v = 0.0
    curvilinear_abscissa = [0]
    for n1, n2 in zip(polyline, polyline[1:]):
        v += numpy.linalg.norm(numpy.array(n1) - numpy.array(n2))
        curvilinear_abscissa.append(v / float(length))

    return curvilinear_abscissa


def compute_length_organ(polyline):
    length = 0
    for n1, n2 in zip(polyline, polyline[1:]):
        length += numpy.linalg.norm(numpy.array(n1) - numpy.array(n2))

    return length


def compute_inclination_angle(polyline, step=1):
    if not len(polyline) > step:
        return None

    length = 0
    for (x0, y0, z0), (x1, y1, z1) in zip(polyline[::step], polyline[step::step]):
        length += numpy.linalg.norm(numpy.array((x1 - x0, y1 - y0, z1 - z0)))

    angles = []
    z_axis = numpy.array([0, 0, 1])

    for (x0, y0, z0), (x1, y1, z1) in zip(polyline[::step], polyline[step::step]):
        vector = numpy.array((x1 - x0, y1 - y0, z1 - z0))
        norm = numpy.linalg.norm(vector)
        angle = angle_between(z_axis, vector)
        angles.append(math.degrees(angle) * (norm / length))

    inclination_angle = sum(angles) / float(len(angles))

    if inclination_angle > 180.0:
        inclination_angle -= 360.0
    return inclination_angle


def compute_fitted_width(width, curvilinear_abscissa):
    x = numpy.array(curvilinear_abscissa)
    XX = numpy.vstack((x**2, x)).T
    p_all = numpy.linalg.lstsq(XX, width[::-1], rcond=None)[0]
    fitted_width = numpy.dot(p_all, XX.T)

    return fitted_width


def compute_vector_mean(polyline):
    x, y, z = polyline[0]

    vectors = []
    for i in range(1, len(polyline)):
        xx, yy, zz = polyline[i]

        v = (xx - x, yy - y, zz - z)
        vectors.append(v)

    vector_mean = numpy.array(vectors).mean(axis=0)

    return vector_mean


def compute_azimuth_angle(polyline):
    vector_mean = compute_vector_mean(polyline)
    x, y, _ = vector_mean
    azimuth_angle = math.degrees(math.atan2(y, x))

    return azimuth_angle, tuple(vector_mean)


def compute_insertion_angle(polyline, stem_vector_mean):
    x, y, z = polyline[0]

    vectors = []
    for i in range(1, len(polyline) // 4 + 1):
        xx, yy, zz = polyline[i]
        vectors.append((xx - x, yy - y, zz - z))

    insertion_vector = numpy.array(vectors).mean(axis=0)
    insertion_angle = angle_between(insertion_vector, stem_vector_mean)
    insertion_angle = math.degrees(insertion_angle)

    if insertion_angle > 180.0:
        insertion_angle -= 360.0

    return insertion_angle, tuple(insertion_vector)


# ==============================================================================
# ==============================================================================
# ==============================================================================


def voxel_base_height(vo, polyline, min_distance=30):
    """
    Search voxels that have a distance < min_distance to the polyline, and return the height of the lowest one.
    This can be used to determine the insertion height of a maize mature leaf (with vo = mature leaf organ,
    polyline = stem polyline), based on voxel data (since it's often more accurate than polyline data)
    """

    # all voxels
    vxs = numpy.array(list(vo.voxels_position()))

    # only voxels with distance to polyline the lowest point < min_distance
    vxs2 = []
    for x, y, z in vxs:
        # TODO : approx ?
        # TODO : param min_distance depending on leaf length ?
        x_stem, y_stem, _ = polyline[numpy.argmin(numpy.abs(polyline[:, 2] - z))]
        if numpy.sqrt((x_stem - x) ** 2 + (y_stem - y) ** 2) < min_distance:
            vxs2.append([x, y, z])
    vxs2 = numpy.array(vxs2)

    if vxs2.size == 0:
        height = vo.info["pm_position_base"]
    else:
        height = vxs2[numpy.argsort(vxs2[:, 2])][0]

    return height


def organ_analysis(organ, polyline, closest_nodes, stem_vector_mean=None):
    if len(polyline) <= 1:
        return None

    organ.info["pm_position_tip"] = tuple(polyline[-1])
    organ.info["pm_position_base"] = tuple(polyline[0])
    organ.info["pm_z_tip"] = polyline[-1][2]
    organ.info["pm_z_base"] = polyline[0][2]

    length = compute_length_organ(polyline)
    organ.info["pm_length"] = length
    normalized_curvilinear_abscissa = compute_curvilinear_abscissa(polyline, length)
    curvilinear_abscissa = compute_curvilinear_abscissa(polyline, 1)
    # Compute width
    width = compute_width_organ(closest_nodes)

    organ.info["pm_width_max"] = max(width)
    organ.info["pm_width_mean"] = sum(width) / float(len(width))
    organ.info["pm_surface"] = scipy.integrate.simpson(y=width, x=curvilinear_abscissa)

    fitted_width = compute_fitted_width(width, normalized_curvilinear_abscissa)
    organ.info["pm_fitted_width_max"] = max(fitted_width)
    organ.info["pm_fitted_width_mean"] = sum(fitted_width) / float(len(fitted_width))
    organ.info["pm_fitted_surface"] = scipy.integrate.simpson(
        y=fitted_width, x=curvilinear_abscissa
    )
    # Compute azimuth
    azimuth_angle, vector_mean = compute_azimuth_angle(polyline)
    organ.info["pm_vector_mean"] = vector_mean
    organ.info["pm_azimuth_angle"] = azimuth_angle

    inclination_angle = compute_inclination_angle(polyline)
    organ.info["pm_inclination_angle"] = inclination_angle

    if stem_vector_mean is not None and len(polyline) >= 4:
        insertion_angle, vector = compute_insertion_angle(polyline, stem_vector_mean)

        organ.info["pm_insertion_angle_vector"] = vector
        organ.info["pm_insertion_angle"] = insertion_angle

    return organ


def maize_stem_analysis(vo, voxels_size, distance_plane=0.75):
    voxels_position = vo.voxels_position()
    polyline = vo.get_longest_segment().polyline

    if len(polyline) <= 1:
        return vo

    # ==========================================================================
    # Compute height of the leaf

    closest_nodes, _ = intercept_points_along_path_with_planes(
        numpy.array(list(voxels_position)),
        polyline,
        distance_from_plane=distance_plane * voxels_size,
        without_connection=True,
        voxels_size=voxels_size,
    )

    # ==========================================================================

    return organ_analysis(vo, polyline, closest_nodes)


def maize_mature_leaf_analysis(vo, voxels_size, stem_vector_mean, distance_plane=0.75):
    voxels_position = vo.voxels_position()
    polyline = vo.get_longest_segment().polyline

    if len(polyline) <= 3:
        return None

    vo.info["pm_full_length"] = compute_length_organ(polyline)

    # ==========================================================================
    # Compute height of the leaf

    closest_nodes, _ = intercept_points_along_path_with_planes(
        numpy.array(list(voxels_position)),
        polyline,
        distance_from_plane=distance_plane * voxels_size,
        voxels_size=voxels_size,
    )

    # ==========================================================================
    # Compute extremity
    index_position_base = vo.get_real_index_position_base()

    # ==========================================================================

    real_polyline = polyline[index_position_base:]
    real_closest_nodes = closest_nodes[index_position_base:]

    vo = organ_analysis(vo, real_polyline, real_closest_nodes, stem_vector_mean)

    return vo


def maize_growing_leaf_analysis_real_length(maize_segmented, vo):
    voxels = maize_segmented.get_voxels_position(except_organs=[vo])
    longest_polyline = vo.get_longest_segment().polyline
    voxels = set(voxels).intersection(longest_polyline)
    z = numpy.max(numpy.array(list(voxels))[:, 2])
    return z


def maize_growing_leaf_analysis(
    vo, voxels_size, stem_vector_mean, voxels, distance_plane=0.75
):
    voxels_position = vo.voxels_position()
    polyline = vo.get_longest_segment().polyline
    vo.info["pm_full_length"] = compute_length_organ(polyline)

    if len(polyline) <= 1:
        return vo

    real_longest_polyline = vo.real_longest_polyline()
    vo.info["pm_length_with_speudo_stem"] = compute_length_organ(real_longest_polyline)

    # ==========================================================================
    # Compute height of the leaf
    closest_nodes, _ = intercept_points_along_path_with_planes(
        numpy.array(list(voxels_position)),
        polyline,
        distance_from_plane=distance_plane * voxels_size,
        voxels_size=voxels_size,
    )

    # ==========================================================================
    # Compute extremity
    voxels = list(set(polyline).intersection(set(voxels)))

    index_position_base = 0
    for i, node in enumerate(polyline):
        if node in voxels:
            index_position_base = i

    real_polyline = polyline[index_position_base:]
    real_closest_nodes = closest_nodes[index_position_base:]

    vo = organ_analysis(vo, real_polyline, real_closest_nodes, stem_vector_mean)

    if vo is not None:
        vo.info["pm_length_speudo_stem"] = (
            vo.info["pm_length_with_speudo_stem"] - vo.info["pm_length"]
        )

    return vo


[docs] def maize_analysis(maize_segmented): """Update info field of the VoxelSegmentation object with the analysis result computed. Each organ is a specific algorithm to extract information. Parameters ---------- maize_segmented : VoxelSegmentation Returns ------- maize_segmented: VoxelSegmentation """ voxels_size = maize_segmented.voxels_size for vo in maize_segmented.voxel_organs: vo.info = { "pm_label": vo.label, "pm_sub_label": vo.sub_label, "pm_voxels_volume": ( len(vo.voxels_position()) * maize_segmented.voxels_size**3 ), } # ========================================================================== vo_stem = maize_segmented.get_stem() vo_stem = maize_stem_analysis(vo_stem, voxels_size) # ========================================================================== mature_leafs = [] stem_polyline = numpy.array( list(maize_segmented.get_stem().get_highest_polyline().polyline) ) for vo_mature_leaf in maize_segmented.get_mature_leafs(): vo_mature_leaf = maize_mature_leaf_analysis( vo_mature_leaf, voxels_size, vo_stem.info["pm_vector_mean"] ) if vo_mature_leaf is None: continue if "pm_z_base" in vo_mature_leaf.info: vo_mature_leaf.info["pm_z_base_voxel"] = voxel_base_height( vo_mature_leaf, stem_polyline )[2] mature_leafs.append((vo_mature_leaf, vo_mature_leaf.info["pm_z_base"])) mature_leafs.sort(key=lambda x: x[1]) # ========================================================================== growing_leafs = [] for vo_growing_leaf in maize_segmented.get_growing_leafs(): z = maize_growing_leaf_analysis_real_length(maize_segmented, vo_growing_leaf) growing_leafs.append((vo_growing_leaf, z)) growing_leafs.sort(key=lambda x: x[1]) voxels = vo_stem.voxels_position() for vo, _ in growing_leafs: # TODO : bug here when two leaf are connected by the tips, the length is directly 0 vo = maize_growing_leaf_analysis( vo, voxels_size, vo_stem.info["pm_vector_mean"], voxels ) if vo is None: continue voxels = voxels.union(vo.voxels_position()) # ========================================================================== for leaf_number, (vo, _) in enumerate(mature_leafs + growing_leafs): vo.info["pm_leaf_number"] = leaf_number + 1 maize_segmented.update_plant_info() return maize_segmented