Esempio n. 1
0
def prune(skel_img, size):
    """
    The pruning algorithm was inspired by Jean-Patrick Pommier: https://gist.github.com/jeanpat/5712699
    Iteratively remove endpoints (tips) from a skeletonized image. "Prunes" barbs off a skeleton.

    Inputs:
    skel_img    = Skeletonized image
    size        = Size to get pruned off each branch

    Returns:
    pruned_img  = Pruned image

    :param skel_img: numpy.ndarray
    :param size: int
    :return pruned_img: numpy.ndarray

    """
    # Store debug
    debug = params.debug
    params.debug = None

    pruned_img = skel_img.copy()

    # Check to see if the skeleton has multiple objects
    objects, _ = find_objects(pruned_img, pruned_img)
    if not len(objects) == 1:
        print("Warning: Multiple objects detected! Pruning will further separate the difference pieces.")

    # Iteratively remove endpoints (tips) from a skeleton
    for i in range(0, size):
        endpoints = find_tips(pruned_img)
        pruned_img = image_subtract(pruned_img, endpoints)

    # Make debugging image
    pruned_plot = np.zeros(skel_img.shape[:2], np.uint8)
    pruned_plot = cv2.cvtColor(pruned_plot, cv2.COLOR_GRAY2RGB)
    skel_obj, skel_hierarchy = find_objects(skel_img, skel_img)
    pruned_obj, pruned_hierarchy = find_objects(pruned_img, pruned_img)
    cv2.drawContours(pruned_plot, skel_obj, -1, (0, 0, 255), params.line_thickness,
                     lineType=8, hierarchy=skel_hierarchy)
    cv2.drawContours(pruned_plot, pruned_obj, -1, (255, 255, 255), params.line_thickness,
                     lineType=8, hierarchy=pruned_hierarchy)

    # Reset debug mode
    params.debug = debug

    params.device += 1

    if params.debug == 'print':
        print_image(pruned_img, os.path.join(params.debug_outdir, str(params.device) + '_pruned.png'))
        print_image(pruned_plot, os.path.join(params.debug_outdir, str(params.device) + '_pruned_debug.png'))

    elif params.debug == 'plot':
        plot_image(pruned_img, cmap='gray')
        plot_image(pruned_plot)

    return pruned_img
def _iterative_prune(skel_img, size):
    """
    The pruning algorithm was inspired by Jean-Patrick Pommier: https://gist.github.com/jeanpat/5712699
    Iteratively remove endpoints (tips) from a skeletonized image. "Prunes" barbs off a skeleton.
    Inputs:
    skel_img    = Skeletonized image
    size        = Size to get pruned off each branch
    Returns:
    pruned_img  = Pruned image
    :param skel_img: numpy.ndarray
    :param size: int
    :return pruned_img: numpy.ndarray
    """
    pruned_img = skel_img.copy()
    # Store debug
    debug = params.debug
    params.debug = None

    # Check to see if the skeleton has multiple objects
    objects, _ = find_objects(pruned_img, pruned_img)

    # Iteratively remove endpoints (tips) from a skeleton
    for i in range(0, size):
        endpoints = find_tips(pruned_img)
        pruned_img = image_subtract(pruned_img, endpoints)

    # Make debugging image
    pruned_plot = np.zeros(skel_img.shape[:2], np.uint8)
    pruned_plot = cv2.cvtColor(pruned_plot, cv2.COLOR_GRAY2RGB)
    skel_obj, skel_hierarchy = find_objects(skel_img, skel_img)
    pruned_obj, pruned_hierarchy = find_objects(pruned_img, pruned_img)

    # Reset debug mode
    params.debug = debug

    cv2.drawContours(pruned_plot,
                     skel_obj,
                     -1, (0, 0, 255),
                     params.line_thickness,
                     lineType=8,
                     hierarchy=skel_hierarchy)
    cv2.drawContours(pruned_plot,
                     pruned_obj,
                     -1, (255, 255, 255),
                     params.line_thickness,
                     lineType=8,
                     hierarchy=pruned_hierarchy)

    return pruned_img
Esempio n. 3
0
def segment_sort(skel_img, objects, mask=None, first_stem=True):
    """ Calculate segment curvature as defined by the ratio between geodesic and euclidean distance

        Inputs:
        skel_img          = Skeletonized image
        objects           = List of contours
        mask              = (Optional) binary mask for debugging. If provided, debug image will be overlaid on the mask.
        first_stem        = (Optional) if True, then the first (bottom) segment always gets classified as stem

        Returns:
        labeled_img       = Segmented debugging image with lengths labeled
        secondary_objects = List of secondary segments (leaf)
        primary_objects   = List of primary objects (stem)

        :param skel_img: numpy.ndarray
        :param objects: list
        :param mask: numpy.ndarray
        :param first_stem: bool
        :return secondary_objects: list
        :return other_objects: list
        """
    # Store debug
    debug = params.debug
    params.debug = None

    secondary_objects = []
    primary_objects = []

    if mask is None:
        labeled_img = np.zeros(skel_img.shape[:2], np.uint8)
    else:
        labeled_img = mask.copy()

    tips_img = find_tips(skel_img)
    tips_img = dilate(tips_img, 3, 1)

    # Loop through segment contours
    for i, cnt in enumerate(objects):
        segment_plot = np.zeros(skel_img.shape[:2], np.uint8)
        cv2.drawContours(segment_plot, objects, i, 255, 1, lineType=8)
        is_leaf = False
        overlap_img = logical_and(segment_plot, tips_img)

        # The first contour is the base, and while it contains a tip, it isn't a leaf
        if i == 0 and first_stem:
            primary_objects.append(cnt)

        # Sort segments
        else:

            if np.sum(overlap_img) > 0:
                secondary_objects.append(cnt)
                is_leaf = True
            else:
                primary_objects.append(cnt)

    # Plot segments where green segments are leaf objects and fuschia are other objects
    labeled_img = cv2.cvtColor(labeled_img, cv2.COLOR_GRAY2RGB)
    for i, cnt in enumerate(primary_objects):
        cv2.drawContours(labeled_img, primary_objects, i, (255, 0, 255), params.line_thickness, lineType=8)
    for i, cnt in enumerate(secondary_objects):
        cv2.drawContours(labeled_img, secondary_objects, i, (0, 255, 0), params.line_thickness, lineType=8)

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_sorted_segments.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return secondary_objects, primary_objects
Esempio n. 4
0
def segment_euclidean_length(segmented_img, objects, label="default"):
    """ Use segmented skeleton image to gather euclidean length measurements per segment

        Inputs:
        segmented_img = Segmented image to plot lengths on
        objects       = List of contours
        label         = optional label parameter, modifies the variable name of observations recorded

        Returns:
        labeled_img      = Segmented debugging image with lengths labeled

        :param segmented_img: numpy.ndarray
        :param objects: list
        :param label: str
        :return labeled_img: numpy.ndarray

        """
    x_list = []
    y_list = []
    segment_lengths = []
    # Create a color scale, use a previously stored scale if available
    rand_color = color_palette(num=len(objects), saved=True)

    labeled_img = segmented_img.copy()
    # Store debug
    debug = params.debug
    params.debug = None

    for i, cnt in enumerate(objects):
        # Store coordinates for labels
        x_list.append(objects[i][0][0][0])
        y_list.append(objects[i][0][0][1])

        # Draw segments one by one to group segment tips together
        finding_tips_img = np.zeros(segmented_img.shape[:2], np.uint8)
        cv2.drawContours(finding_tips_img,
                         objects,
                         i, (255, 255, 255),
                         1,
                         lineType=8)
        segment_tips = find_tips(finding_tips_img)
        tip_objects, tip_hierarchies = find_objects(segment_tips, segment_tips)
        points = []
        if not len(tip_objects) == 2:
            fatal_error("Too many tips found per segment, try pruning again")
        for t in tip_objects:
            # Gather pairs of coordinates
            x, y = t.ravel()
            coord = (x, y)
            points.append(coord)

        # Draw euclidean distance lines
        cv2.line(labeled_img, points[0], points[1], rand_color[i], 1)

        # Calculate euclidean distance between tips of each contour
        segment_lengths.append(float(euclidean(points[0], points[1])))

    segment_ids = []
    # Reset debug mode
    params.debug = debug

    # Put labels of length
    for c, value in enumerate(segment_lengths):
        text = "{:.2f}".format(value)
        w = x_list[c]
        h = y_list[c]
        cv2.putText(img=labeled_img,
                    text=text,
                    org=(w, h),
                    fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=params.text_size,
                    color=(150, 150, 150),
                    thickness=params.text_thickness)
        # segment_label = "ID" + str(c)
        segment_ids.append(c)

    outputs.add_observation(
        sample=label,
        variable='segment_eu_length',
        trait='segment euclidean length',
        method='plantcv.plantcv.morphology.segment_euclidean_length',
        scale='pixels',
        datatype=list,
        value=segment_lengths,
        label=segment_ids)

    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(
            labeled_img,
            os.path.join(params.debug_outdir,
                         str(params.device) + '_segment_eu_lengths.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return labeled_img
def segment_insertion_angle(skel_img, segmented_img, leaf_objects, stem_objects, size):
    """ Find leaf insertion angles in degrees of skeleton segments. Fit a linear regression line to the stem.
        Use `size` pixels on  the portion of leaf next to the stem find a linear regression line,
        and calculate angle between the two lines per leaf object.

        Inputs:
        skel_img         = Skeletonized image
        segmented_img    = Segmented image to plot slope lines and intersection angles on
        leaf_objects     = List of leaf segments
        stem_objects     = List of stem segments
        size             = Size of inner leaf used to calculate slope lines

        Returns:
        labeled_img      = Debugging image with angles labeled

        :param skel_img: numpy.ndarray
        :param segmented_img: numpy.ndarray
        :param leaf_objects: list
        :param stem_objects: list
        :param size: int
        :return labeled_img: numpy.ndarray
        """

    # Store debug
    debug = params.debug
    params.debug = None

    rows,cols = segmented_img.shape[:2]
    labeled_img = segmented_img.copy()
    segment_slopes = []
    insertion_segments = []
    insertion_hierarchies = []
    intersection_angles = []
    label_coord_x = []
    label_coord_y = []
    valid_segment = []

    # Create a list of tip tuples to use for sorting
    tips = find_tips(skel_img)
    tips = dilate(tips, 3, 1)
    tip_objects, tip_hierarchies = find_objects(tips, tips)
    tip_tuples = []
    for i, cnt in enumerate(tip_objects):
        tip_tuples.append((cnt[0][0][0], cnt[0][0][1]))

    rand_color = color_palette(len(leaf_objects))

    for i, cnt in enumerate(leaf_objects):
        # Draw leaf objects
        find_segment_tangents = np.zeros(segmented_img.shape[:2], np.uint8)
        cv2.drawContours(find_segment_tangents, leaf_objects, i, 255, 1, lineType=8)

        # Prune back ends of leaves
        pruned_segment = _iterative_prune(find_segment_tangents, size)

        # Segment ends are the portions pruned off
        segment_ends = find_segment_tangents - pruned_segment
        segment_end_obj, segment_end_hierarchy = find_objects(segment_ends, segment_ends)
        is_insertion_segment = []

        if not len(segment_end_obj) == 2:
            print("Size too large, contour with ID#", i, "got pruned away completely.")
        else:
            # The contour can have insertion angle calculated
            valid_segment.append(cnt)

            # Determine if a segment is leaf end or leaf insertion segment
            for j, obj in enumerate(segment_end_obj):

                segment_plot = np.zeros(segmented_img.shape[:2], np.uint8)
                cv2.drawContours(segment_plot, obj, -1, 255, 1, lineType=8)
                overlap_img = logical_and(segment_plot, tips)

                # If none of the tips are within a segment_end then it's an insertion segment
                if np.sum(overlap_img) == 0:
                    insertion_segments.append(segment_end_obj[j])
                    insertion_hierarchies.append(segment_end_hierarchy[0][j])

            # Store coordinates for labels
            label_coord_x.append(leaf_objects[i][0][0][0])
            label_coord_y.append(leaf_objects[i][0][0][1])

    rand_color = color_palette(len(valid_segment))

    for i, cnt in enumerate(valid_segment):
        cv2.drawContours(labeled_img, valid_segment, i, rand_color[i], params.line_thickness, lineType=8)

    # Plot stem segments
    stem_img = np.zeros(segmented_img.shape[:2], np.uint8)
    cv2.drawContours(stem_img, stem_objects, -1, 255, 2, lineType=8)
    branch_pts = find_branch_pts(skel_img)
    stem_img = stem_img + branch_pts
    stem_img = closing(stem_img)
    combined_stem, combined_stem_hier = find_objects(stem_img, stem_img)

    # Make sure stem objects are a single contour
    loop_count=0
    while len(combined_stem) > 1 and loop_count<50:
        loop_count += 1
        stem_img = dilate(stem_img, 2, 1)
        stem_img = closing(stem_img)
        combined_stem, combined_stem_hier = find_objects(stem_img, stem_img)

    if loop_count == 50:
        fatal_error('Unable to combine stem objects.')

    # Find slope of the stem
    [vx, vy, x, y] = cv2.fitLine(combined_stem[0], cv2.DIST_L2, 0, 0.01, 0.01)
    stem_slope = -vy / vx
    stem_slope = stem_slope[0]
    lefty = int((-x * vy / vx) + y)
    righty = int(((cols - x) * vy / vx) + y)
    cv2.line(labeled_img, (cols - 1, righty), (0, lefty), (150, 150, 150), 3)

    rand_color = color_palette(len(insertion_segments))

    for t, segment in enumerate(insertion_segments):
        # Find line fit to each segment
        [vx, vy, x, y] = cv2.fitLine(segment, cv2.DIST_L2, 0, 0.01, 0.01)
        slope = -vy / vx
        left_list = int((-x * vy / vx) + y)
        right_list = int(((cols - x) * vy / vx) + y)
        segment_slopes.append(slope[0])

        # Draw slope lines if possible
        if slope > 1000000 or slope < -1000000:
            print("Slope of contour with ID#", t, "is", slope, "and cannot be plotted.")
        else:
            cv2.line(labeled_img, (cols - 1, right_list), (0, left_list), rand_color[t], 1)

        # Store intersection angles between insertion segment and stem line
        intersection_angle = _slope_to_intesect_angle(slope[0], stem_slope)
        # Function measures clockwise but we want the acute angle between stem and leaf insertion
        if intersection_angle > 90:
            intersection_angle = 180 - intersection_angle
        intersection_angles.append(intersection_angle)

    segment_ids = []

    for i, cnt in enumerate(insertion_segments):
        # Label slope lines
        w = label_coord_x[i]
        h = label_coord_y[i]
        text = "{:.2f}".format(intersection_angles[i])
        cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=params.text_size, color=(150, 150, 150), thickness=params.text_thickness)
        segment_label = "ID" + str(i)
        segment_ids.append(i)

    outputs.add_observation(variable='segment_insertion_angle', trait='segment insertion angle',
                            method='plantcv.plantcv.morphology.segment_insertion_angle', scale='degrees', datatype=list,
                            value=intersection_angles, label=segment_ids)

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(labeled_img,
                    os.path.join(params.debug_outdir, str(params.device) + '_segment_insertion_angles.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return labeled_img
Esempio n. 6
0
def segment_curvature(segmented_img, objects, label="default"):
    """ Calculate segment curvature as defined by the ratio between geodesic and euclidean distance.
        Measurement of two-dimensional tortuosity.

        Inputs:
        segmented_img     = Segmented image to plot lengths on
        objects           = List of contours
        label          = optional label parameter, modifies the variable name of observations recorded

        Returns:
        labeled_img        = Segmented debugging image with curvature labeled


        :param segmented_img: numpy.ndarray
        :param objects: list
        :param label: str
        :return labeled_img: numpy.ndarray

        """

    label_coord_x = []
    label_coord_y = []
    labeled_img = segmented_img.copy()

    # Store debug
    debug = params.debug
    params.debug = None

    _ = segment_euclidean_length(segmented_img, objects, label="backend")
    _ = segment_path_length(segmented_img, objects, label="backend")
    eu_lengths = outputs.observations['backend']['segment_eu_length']['value']
    path_lengths = outputs.observations['backend']['segment_path_length'][
        'value']
    curvature_measure = [
        float(x / y) for x, y in zip(path_lengths, eu_lengths)
    ]
    # Create a color scale, use a previously stored scale if available
    rand_color = color_palette(num=len(objects), saved=True)

    for i, cnt in enumerate(objects):
        # Store coordinates for labels
        label_coord_x.append(objects[i][0][0][0])
        label_coord_y.append(objects[i][0][0][1])

        # Draw segments one by one to group segment tips together
        finding_tips_img = np.zeros(segmented_img.shape[:2], np.uint8)
        cv2.drawContours(finding_tips_img,
                         objects,
                         i, (255, 255, 255),
                         1,
                         lineType=8)
        segment_tips = find_tips(finding_tips_img)
        tip_objects, tip_hierarchies = find_objects(segment_tips, segment_tips)
        points = []

        for t in tip_objects:
            # Gather pairs of coordinates
            x, y = t.ravel()
            coord = (x, y)
            points.append(coord)

        # Draw euclidean distance lines
        cv2.line(labeled_img, points[0], points[1], rand_color[i], 1)

    segment_ids = []
    # Reset debug mode
    params.debug = debug

    for i, cnt in enumerate(objects):
        # Calculate geodesic distance
        text = "{:.3f}".format(curvature_measure[i])
        w = label_coord_x[i]
        h = label_coord_y[i]
        cv2.putText(img=labeled_img,
                    text=text,
                    org=(w, h),
                    fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=params.text_size,
                    color=(150, 150, 150),
                    thickness=params.text_thickness)
        # segment_label = "ID" + str(i)
        segment_ids.append(i)

    outputs.add_observation(
        sample=label,
        variable='segment_curvature',
        trait='segment curvature',
        method='plantcv.plantcv.morphology.segment_curvature',
        scale='none',
        datatype=list,
        value=curvature_measure,
        label=segment_ids)

    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(
            labeled_img,
            os.path.join(params.debug_outdir,
                         str(params.device) + '_segment_curvature.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return labeled_img
def segment_euclidean_length(segmented_img, objects, hierarchies):
    """ Use segmented skeleton image to gather euclidean length measurements per segment

        Inputs:
        segmented_img = Segmented image to plot lengths on
        objects       = List of contours
        hierarchy     = Contour hierarchy NumPy array

        Returns:
        eu_length_header = Segment euclidean length data header
        eu_length_data   = Segment euclidean length data values
        labeled_img      = Segmented debugging image with lengths labeled

        :param segmented_img: numpy.ndarray
        :param objects: list
        :param hierarchy: numpy.ndarray
        :return labeled_img: numpy.ndarray
        :return eu_length_header: list
        :return eu_length_data: list

        """
    # Store debug
    debug = params.debug
    params.debug = None

    x_list = []
    y_list = []
    segment_lengths = []
    rand_color = color_palette(len(objects))


    labeled_img = segmented_img.copy()

    for i, cnt in enumerate(objects):
        # Store coordinates for labels
        x_list.append(objects[i][0][0][0])
        y_list.append(objects[i][0][0][1])

        # Draw segments one by one to group segment tips together
        finding_tips_img = np.zeros(segmented_img.shape[:2], np.uint8)
        cv2.drawContours(finding_tips_img, objects, i, (255, 255, 255), 1, lineType=8,
                         hierarchy=hierarchies)
        segment_tips = find_tips(finding_tips_img)
        tip_objects, tip_hierarchies = find_objects(segment_tips, segment_tips)
        points = []
        if not len(tip_objects) == 2:
            fatal_error("Too many tips found per segment, try pruning again")
        for t in tip_objects:
            # Gather pairs of coordinates
            x, y = t.ravel()
            coord = (x, y)
            points.append(coord)

        # Draw euclidean distance lines
        cv2.line(labeled_img, points[0], points[1], rand_color[i], 1)

        # Calculate euclidean distance between tips of each contour
        segment_lengths.append(euclidean(points[0], points[1]))

    eu_length_header = ['HEADER_EU_LENGTH']
    eu_length_data = ['EU_LENGTH_DATA']
    # Put labels of length
    for c, value in enumerate(segment_lengths):
        text = "{:.2f}".format(value)
        w = x_list[c]
        h = y_list[c]
        cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=.4,
                    color=(150, 150, 150), thickness=1)
        segment_label = "ID" + str(c)
        eu_length_header.append(segment_label)
        eu_length_data.append(segment_lengths[c])

    if 'morphology_data' not in outputs.measurements:
        outputs.measurements['morphology_data'] = {}
    outputs.measurements['morphology_data']['segment_eu_lengths'] = segment_lengths

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segment_eu_lengths.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return eu_length_header, eu_length_data, labeled_img
Esempio n. 8
0
def segment_sort(skel_img, objects, hierarchies, mask=None):
    """ Calculate segment curvature as defined by the ratio between geodesic and euclidean distance

        Inputs:
        skel_img          = Skeletonized image
        objects           = List of contours
        hierarchy         = Contour hierarchy NumPy array
        mask              = (Optional) binary mask for debugging. If provided, debug image will be overlaid on the mask.

        Returns:
        labeled_img       = Segmented debugging image with lengths labeled
        leaf_objects      = List of leaf segments
        leaf_hierarchies  = Contour hierarchy NumPy array
        other_objects     = List of other objects (stem)
        other_hierarchies = Contour hierarchy NumPy array

        :param skel_img: numpy.ndarray
        :param objects: list
        :param hierarchy: numpy.ndarray
        :param labeled_img: numpy.ndarray
        :param mask: numpy.ndarray
        :return leaf_objects: list
        :return leaf_hierarchies: numpy.ndarray
        :return other_objects: list
        :return other_hierarchies: numpy.ndarray
        """
    # Store debug
    debug = params.debug
    params.debug = None

    leaf_objects = []
    leaf_hierarchies = []
    other_objects = []
    other_hierarchies = []

    if mask is None:
        labeled_img = np.zeros(skel_img.shape[:2], np.uint8)
    else:
        labeled_img = mask.copy()

    tips = find_tips(skel_img)
    tip_objects, tip_hierarchies = find_objects(tips, tips)


    # Create a list of tip tuples
    tip_tuples = []
    for i, cnt in enumerate(tip_objects):
        tip_tuples.append((cnt[0][0][0], cnt[0][0][1]))

    # Loop through segment contours
    for i, cnt in enumerate(objects):
        is_leaf = False
        cnt_as_tuples = []
        num_pixels = len(cnt)
        count = 0

        # Turn each contour into a list of tuples (can't search for list of coords, so reformat)
        while num_pixels > count:
            x_coord = cnt[count][0][0]
            y_coord = cnt[count][0][1]
            cnt_as_tuples.append((x_coord, y_coord))
            count += 1

        # The first contour is the base, and while it contains a tip, it isn't a leaf
        if i == 0:
            other_objects.append(cnt)
            other_hierarchies.append(hierarchies[0][i])

        # Sort segments
        else:
            for tip_tups in tip_tuples:
                # If a tip is inside the list of contour tuples then it is a leaf segment
                if tip_tups in cnt_as_tuples:
                    leaf_objects.append(cnt)
                    leaf_hierarchies.append(hierarchies[0][i])
                    is_leaf = True

        # If none of the tip tuples are inside the contour, then it isn't a leaf segment
        if is_leaf == False:
            other_objects.append(cnt)
            other_hierarchies.append(hierarchies[0][i])

    # Format list of hierarchies so that cv2 can use them
    leaf_hierarchies = np.array([leaf_hierarchies])
    other_hierarchies = np.array([other_hierarchies])

    # Plot segments where green segments are leaf objects and fuschia are other objects
    labeled_img = cv2.cvtColor(labeled_img, cv2.COLOR_GRAY2RGB)
    for i, cnt in enumerate(other_objects):
        cv2.drawContours(labeled_img, other_objects, i, (255, 0, 255), params.line_thickness,
                         lineType=8, hierarchy=other_hierarchies)
    for i, cnt in enumerate(leaf_objects):
        cv2.drawContours(labeled_img, leaf_objects, i, (0, 255, 0), params.line_thickness,
                         lineType=8, hierarchy=leaf_hierarchies)

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_sorted_segments.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return leaf_objects, leaf_hierarchies, other_objects, other_hierarchies
def segment_curvature(segmented_img, objects):
    """ Calculate segment curvature as defined by the ratio between geodesic and euclidean distance.
        Measurement of two-dimensional tortuosity.

        Inputs:
        segmented_img     = Segmented image to plot lengths on
        objects           = List of contours

        Returns:
        labeled_img        = Segmented debugging image with curvature labeled


        :param segmented_img: numpy.ndarray
        :param objects: list
        :return labeled_img: numpy.ndarray

        """
    # Store debug
    debug = params.debug
    params.debug = None

    label_coord_x = []
    label_coord_y = []

    _ = segment_euclidean_length(segmented_img, objects)
    labeled_img = segment_path_length(segmented_img, objects)
    eu_lengths = outputs.observations['segment_eu_length']['value']
    path_lengths = outputs.observations['segment_path_length']['value']
    curvature_measure = [x/y for x, y in zip(path_lengths, eu_lengths)]
    rand_color = color_palette(len(objects))

    for i, cnt in enumerate(objects):
        # Store coordinates for labels
        label_coord_x.append(objects[i][0][0][0])
        label_coord_y.append(objects[i][0][0][1])

        # Draw segments one by one to group segment tips together
        finding_tips_img = np.zeros(segmented_img.shape[:2], np.uint8)
        cv2.drawContours(finding_tips_img, objects, i, (255, 255, 255), 1, lineType=8)
        segment_tips = find_tips(finding_tips_img)
        tip_objects, tip_hierarchies = find_objects(segment_tips, segment_tips)
        points = []

        for t in tip_objects:
            # Gather pairs of coordinates
            x, y = t.ravel()
            coord = (x, y)
            points.append(coord)

        # Draw euclidean distance lines
        cv2.line(labeled_img, points[0], points[1], rand_color[i], 1)

    segment_ids = []
    for i, cnt in enumerate(objects):
        # Calculate geodesic distance
        text = "{:.3f}".format(curvature_measure[i])
        w = label_coord_x[i]
        h = label_coord_y[i]
        cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=params.text_size, color=(150, 150, 150), thickness=params.text_thickness)
        segment_label = "ID" + str(i)
        segment_ids.append(i)

    outputs.add_observation(variable='segment_curvature', trait='segment curvature',
                            method='plantcv.plantcv.morphology.segment_curvature', scale='none', datatype=list,
                            value=curvature_measure, label=segment_ids)

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segment_curvature.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return labeled_img
Esempio n. 10
0
def segment_curvature(segmented_img, objects, hierarchies):
    """ Calculate segment curvature as defined by the ratio between geodesic and euclidean distance.
        Measurement of two-dimensional tortuosity.

        Inputs:
        segmented_img     = Segmented image to plot lengths on
        objects           = List of contours
        hierarchy         = Contour hierarchy NumPy array

        Returns:
        curvature_header   = Segment curvature data header
        curvature_data     = Segment curvature data values
        labeled_img        = Segmented debugging image with curvature labeled


        :param segmented_img: numpy.ndarray
        :param objects: list
        :param hierarchy: numpy.ndarray
        :return labeled_img: numpy.ndarray
        :return curvature_header: list
        :return curvature_data: list

        """
    # Store debug
    debug = params.debug
    params.debug = None

    label_coord_x = []
    label_coord_y = []

    _, eu_lengths, _ = segment_euclidean_length(segmented_img, objects, hierarchies)
    _, path_lengths, labeled_img = segment_path_length(segmented_img, objects)
    del eu_lengths[0]
    del path_lengths[0]
    curvature_measure = [x/y for x, y in zip(path_lengths, eu_lengths)]
    rand_color = color_palette(len(objects))

    for i, cnt in enumerate(objects):
        # Store coordinates for labels
        label_coord_x.append(objects[i][0][0][0])
        label_coord_y.append(objects[i][0][0][1])

        # Draw segments one by one to group segment tips together
        finding_tips_img = np.zeros(segmented_img.shape[:2], np.uint8)
        cv2.drawContours(finding_tips_img, objects, i, (255, 255, 255), 1, lineType=8,
                         hierarchy=hierarchies)
        segment_tips = find_tips(finding_tips_img)
        tip_objects, tip_hierarchies = find_objects(segment_tips, segment_tips)
        points = []

        for t in tip_objects:
            # Gather pairs of coordinates
            x, y = t.ravel()
            coord = (x, y)
            points.append(coord)

        # Draw euclidean distance lines
        cv2.line(labeled_img, points[0], points[1], rand_color[i], 1)

    curvature_header = ['HEADER_CURVATURE']
    curvature_data = ['CURVATURE_DATA']
    for i, cnt in enumerate(objects):
        # Calculate geodesic distance
        text = "{:.3f}".format(curvature_measure[i])
        w = label_coord_x[i]
        h = label_coord_y[i]
        cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=.4,
                    color=(150, 150, 150), thickness=1)
        segment_label = "ID" + str(i)
        curvature_header.append(segment_label)
        curvature_data.append(curvature_measure[i])

    outputs.measurements['morphology_data']['segment_curvature'] = curvature_measure

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segment_curvature.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return curvature_header, curvature_data, labeled_img
def segment_euclidean_length(segmented_img, objects):
    """ Use segmented skeleton image to gather euclidean length measurements per segment

        Inputs:
        segmented_img = Segmented image to plot lengths on
        objects       = List of contours

        Returns:
        labeled_img      = Segmented debugging image with lengths labeled

        :param segmented_img: numpy.ndarray
        :param objects: list
        :return labeled_img: numpy.ndarray

        """
    # Store debug
    debug = params.debug
    params.debug = None

    x_list = []
    y_list = []
    segment_lengths = []
    rand_color = color_palette(len(objects))


    labeled_img = segmented_img.copy()

    for i, cnt in enumerate(objects):
        # Store coordinates for labels
        x_list.append(objects[i][0][0][0])
        y_list.append(objects[i][0][0][1])

        # Draw segments one by one to group segment tips together
        finding_tips_img = np.zeros(segmented_img.shape[:2], np.uint8)
        cv2.drawContours(finding_tips_img, objects, i, (255, 255, 255), 1, lineType=8)
        segment_tips = find_tips(finding_tips_img)
        tip_objects, tip_hierarchies = find_objects(segment_tips, segment_tips)
        points = []
        if not len(tip_objects) == 2:
            fatal_error("Too many tips found per segment, try pruning again")
        for t in tip_objects:
            # Gather pairs of coordinates
            x, y = t.ravel()
            coord = (x, y)
            points.append(coord)

        # Draw euclidean distance lines
        cv2.line(labeled_img, points[0], points[1], rand_color[i], 1)

        # Calculate euclidean distance between tips of each contour
        segment_lengths.append(euclidean(points[0], points[1]))

    segment_ids = []
    # Put labels of length
    for c, value in enumerate(segment_lengths):
        text = "{:.2f}".format(value)
        w = x_list[c]
        h = y_list[c]
        cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=params.text_size, color=(150, 150, 150), thickness=params.text_thickness)
        segment_label = "ID" + str(c)
        segment_ids.append(c)

    outputs.add_observation(variable='segment_eu_length', trait='segment euclidean length',
                            method='plantcv.plantcv.morphology.segment_euclidean_length', scale='pixels', datatype=list,
                            value=segment_lengths, label=segment_ids)

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segment_eu_lengths.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return labeled_img
Esempio n. 12
0
def segment_sort(skel_img, objects, mask=None):
    """ Calculate segment curvature as defined by the ratio between geodesic and euclidean distance

        Inputs:
        skel_img          = Skeletonized image
        objects           = List of contours
        mask              = (Optional) binary mask for debugging. If provided, debug image will be overlaid on the mask.

        Returns:
        labeled_img       = Segmented debugging image with lengths labeled
        secondary_objects = List of secondary segments (leaf)
        primary_objects   = List of primary objects (stem)

        :param skel_img: numpy.ndarray
        :param objects: list
        :param labeled_img: numpy.ndarray
        :param mask: numpy.ndarray
        :return secondary_objects: list
        :return other_objects: list
        """
    # Store debug
    debug = params.debug
    params.debug = None

    secondary_objects = []
    primary_objects = []

    if mask is None:
        labeled_img = np.zeros(skel_img.shape[:2], np.uint8)
    else:
        labeled_img = mask.copy()

    tips = find_tips(skel_img)
    tip_objects, tip_hierarchies = find_objects(tips, tips)


    # Create a list of tip tuples
    tip_tuples = []
    for i, cnt in enumerate(tip_objects):
        tip_tuples.append((cnt[0][0][0], cnt[0][0][1]))

    # Loop through segment contours
    for i, cnt in enumerate(objects):
        is_leaf = False
        cnt_as_tuples = []
        num_pixels = len(cnt)
        count = 0

        # Turn each contour into a list of tuples (can't search for list of coords, so reformat)
        while num_pixels > count:
            x_coord = cnt[count][0][0]
            y_coord = cnt[count][0][1]
            cnt_as_tuples.append((x_coord, y_coord))
            count += 1

        # The first contour is the base, and while it contains a tip, it isn't a leaf
        if i == 0:
            primary_objects.append(cnt)

        # Sort segments
        else:
            for tip_tups in tip_tuples:
                # If a tip is inside the list of contour tuples then it is a leaf segment
                if tip_tups in cnt_as_tuples:
                    secondary_objects.append(cnt)
                    is_leaf = True

            # If none of the tip tuples are inside the contour, then it isn't a leaf segment
            if is_leaf == False:
                primary_objects.append(cnt)

    # Plot segments where green segments are leaf objects and fuschia are other objects
    labeled_img = cv2.cvtColor(labeled_img, cv2.COLOR_GRAY2RGB)
    for i, cnt in enumerate(primary_objects):
        cv2.drawContours(labeled_img, primary_objects, i, (255, 0, 255), params.line_thickness, lineType=8)
    for i, cnt in enumerate(secondary_objects):
        cv2.drawContours(labeled_img, secondary_objects, i, (0, 255, 0), params.line_thickness, lineType=8)

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_sorted_segments.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return secondary_objects, primary_objects
Esempio n. 13
0
def prune(skel_img, size=0, mask=None):
    """
    The pruning algorithm proposed by https://github.com/karnoldbio
    Segments a skeleton into discrete pieces, prunes off all segments less than or
    equal to user specified size. Returns the remaining objects as a list and the
    pruned skeleton.

    Inputs:
    skel_img    = Skeletonized image
    size        = Size to get pruned off each branch
    mask        = (Optional) binary mask for debugging. If provided, debug image will be overlaid on the mask.

    Returns:
    pruned_img      = Pruned image
    segmented_img   = Segmented debugging image
    segment_objects = List of contours

    :param skel_img: numpy.ndarray
    :param size: int
    :param mask: numpy.ndarray
    :return pruned_img: numpy.ndarray
    :return segmented_img: numpy.ndarray
    :return segment_objects: list

    """
    # Store debug
    debug = params.debug
    params.debug = None

    pruned_img = skel_img.copy()

    # Check to see if the skeleton has multiple objects
    skel_objects, _ = find_objects(skel_img, skel_img)
    if not len(skel_objects) == 1:
        print(
            "Warning: Multiple objects detected! Pruning will further separate the difference pieces."
        )

    _, objects = segment_skeleton(skel_img)
    kept_segments = []
    removed_segments = []

    if size > 0:
        # If size>0 then check for segments that are smaller than size pixels long

        # Sort through segments since we don't want to remove primary segments
        secondary_objects, primary_objects = segment_sort(skel_img, objects)

        # Keep segments longer than specified size
        for i in range(0, len(secondary_objects)):
            if len(secondary_objects[i]) > size:
                kept_segments.append(secondary_objects[i])
            else:
                removed_segments.append(secondary_objects[i])

        # Draw the contours that got removed
        removed_barbs = np.zeros(skel_img.shape[:2], np.uint8)
        cv2.drawContours(removed_barbs,
                         removed_segments,
                         -1,
                         255,
                         1,
                         lineType=8)
        # Subtract all short segments from the skeleton image
        pruned_img = image_subtract(pruned_img, removed_barbs)
        # Prune off one more pixel to account for the gaps while using segment_skeleton
        endpoints = find_tips(pruned_img)
        pruned_img = image_subtract(pruned_img, endpoints)

    # Make debugging image
    if mask is None:
        pruned_plot = np.zeros(skel_img.shape[:2], np.uint8)
    else:
        pruned_plot = mask.copy()
    pruned_plot = cv2.cvtColor(pruned_plot, cv2.COLOR_GRAY2RGB)
    pruned_obj, pruned_hierarchy = find_objects(pruned_img, pruned_img)
    cv2.drawContours(pruned_plot,
                     removed_segments,
                     -1, (0, 0, 255),
                     params.line_thickness,
                     lineType=8)
    cv2.drawContours(pruned_plot,
                     pruned_obj,
                     -1, (150, 150, 150),
                     params.line_thickness,
                     lineType=8)

    # Reset debug mode
    params.debug = debug

    params.device += 1

    if params.debug == 'print':
        print_image(
            pruned_img,
            os.path.join(params.debug_outdir,
                         str(params.device) + '_pruned.png'))
        print_image(
            pruned_plot,
            os.path.join(params.debug_outdir,
                         str(params.device) + '_pruned_debug.png'))

    elif params.debug == 'plot':
        plot_image(pruned_img, cmap='gray')
        plot_image(pruned_plot)

    # Segment the pruned skeleton
    segmented_img, segment_objects = segment_skeleton(pruned_img, mask)

    return pruned_img, segmented_img, segment_objects
Esempio n. 14
0
def segment_insertion_angle(skel_img, segmented_img, leaf_objects,
                            leaf_hierarchies, stem_objects, size):
    """ Find leaf insertion angles in degrees of skeleton segments. Fit a linear regression line to the stem.
        Use `size` pixels on  the portion of leaf next to the stem find a linear regression line,
        and calculate angle between the two lines per leaf object.

        Inputs:
        skel_img         = Skeletonized image
        segmented_img    = Segmented image to plot slope lines and intersection angles on
        leaf_objects     = List of leaf segments
        leaf_hierarchies = Leaf contour hierarchy NumPy array
        stem_objects     = List of stem segments
        size             = Size of inner leaf used to calculate slope lines

        Returns:
        insertion_angle_header = Leaf insertion angle headers
        insertion_angle_data   = Leaf insertion angle values
        labeled_img            = Debugging image with angles labeled

        :param skel_img: numpy.ndarray
        :param segmented_img: numpy.ndarray
        :param leaf_objects: list
        :param leaf_hierarchies: numpy.ndarray
        :param stem_objects: list
        :param size: int
        :return insertion_angle_header: list
        :return insertion_angle_data: list
        :return labeled_img: numpy.ndarray
        """

    # Store debug
    debug = params.debug
    params.debug = None

    rows, cols = segmented_img.shape[:2]
    labeled_img = segmented_img.copy()
    segment_slopes = []
    insertion_segments = []
    insertion_hierarchies = []
    intersection_angles = []
    label_coord_x = []
    label_coord_y = []

    # Create a list of tip tuples to use for sorting
    tips = find_tips(skel_img)
    tip_objects, tip_hierarchies = find_objects(tips, tips)
    tip_tuples = []
    for i, cnt in enumerate(tip_objects):
        tip_tuples.append((cnt[0][0][0], cnt[0][0][1]))

    rand_color = color_palette(len(leaf_objects))

    for i, cnt in enumerate(leaf_objects):
        # Draw leaf objects
        find_segment_tangents = np.zeros(segmented_img.shape[:2], np.uint8)
        cv2.drawContours(find_segment_tangents,
                         leaf_objects,
                         i,
                         255,
                         1,
                         lineType=8,
                         hierarchy=leaf_hierarchies)
        cv2.drawContours(labeled_img,
                         leaf_objects,
                         i,
                         rand_color[i],
                         params.line_thickness,
                         lineType=8,
                         hierarchy=leaf_hierarchies)

        # Prune back ends of leaves
        pruned_segment = prune(find_segment_tangents, size)

        # Segment ends are the portions pruned off
        segment_ends = find_segment_tangents - pruned_segment
        segment_end_obj, segment_end_hierarchy = find_objects(
            segment_ends, segment_ends)
        is_insertion_segment = []

        if not len(segment_end_obj) == 2:
            print("Size too large, contour with ID#", i,
                  "got pruned away completely.")
        else:
            # Determine if a segment is leaf end or leaf insertion segment
            for j, obj in enumerate(segment_end_obj):

                cnt_as_tuples = []
                num_pixels = len(obj)
                count = 0

                # Turn each contour into a list of tuples (can't search for list of coords, so reformat)
                while num_pixels > count:
                    x_coord = obj[count][0][0]
                    y_coord = obj[count][0][1]
                    cnt_as_tuples.append((x_coord, y_coord))
                    count += 1

                for tip_tups in tip_tuples:
                    # If a tip is inside the list of contour tuples then it is a leaf end segment
                    if tip_tups in cnt_as_tuples:
                        is_insertion_segment.append(False)
                    else:
                        is_insertion_segment.append(True)

                # If none of the tips are within a segment_end then it's an insertion segment
                if all(is_insertion_segment):
                    insertion_segments.append(segment_end_obj[j])
                    insertion_hierarchies.append(segment_end_hierarchy[0][j])

        # Store coordinates for labels
        label_coord_x.append(leaf_objects[i][0][0][0])
        label_coord_y.append(leaf_objects[i][0][0][1])

    # Plot stem segments
    stem_img = np.zeros(segmented_img.shape[:2], np.uint8)
    cv2.drawContours(stem_img, stem_objects, -1, 255, 2, lineType=8)
    branch_pts = find_branch_pts(skel_img)
    stem_img = stem_img + branch_pts
    stem_img = closing(stem_img)
    combined_stem, combined_stem_hier = find_objects(stem_img, stem_img)

    # Make sure stem objects are a single contour
    while len(combined_stem) > 1:
        stem_img = dilate(stem_img, 2, 1)
        stem_img = closing(stem_img)
        combined_stem, combined_stem_hier = find_objects(stem_img, stem_img)

    # Find slope of the stem
    [vx, vy, x, y] = cv2.fitLine(combined_stem[0], cv2.DIST_L2, 0, 0.01, 0.01)
    stem_slope = -vy / vx
    stem_slope = stem_slope[0]
    lefty = int((-x * vy / vx) + y)
    righty = int(((cols - x) * vy / vx) + y)
    cv2.line(labeled_img, (cols - 1, righty), (0, lefty), (150, 150, 150), 3)

    for t, segment in enumerate(insertion_segments):
        # Find line fit to each segment
        [vx, vy, x, y] = cv2.fitLine(segment, cv2.DIST_L2, 0, 0.01, 0.01)
        slope = -vy / vx
        left_list = int((-x * vy / vx) + y)
        right_list = int(((cols - x) * vy / vx) + y)
        segment_slopes.append(slope[0])

        # Draw slope lines if possible
        if slope > 1000000 or slope < -1000000:
            print("Slope of contour with ID#", t, "is", slope,
                  "and cannot be plotted.")
        else:
            cv2.line(labeled_img, (cols - 1, right_list), (0, left_list),
                     rand_color[t], 1)

        # Store intersection angles between insertion segment and stem line
        intersection_angle = _slope_to_intesect_angle(slope[0], stem_slope)
        # Function measures clockwise but we want the acute angle between stem and leaf insertion
        if intersection_angle > 90:
            intersection_angle = 180 - intersection_angle
        intersection_angles.append(intersection_angle)

    insertion_angle_header = ['HEADER_INSERTION_ANGLE']
    insertion_angle_data = ['INSERTION_ANGLE_DATA']

    for i, cnt in enumerate(insertion_segments):
        # Label slope lines
        w = label_coord_x[i]
        h = label_coord_y[i]
        text = "{:.2f}".format(intersection_angles[i])
        cv2.putText(img=labeled_img,
                    text=text,
                    org=(w, h),
                    fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=.55,
                    color=(150, 150, 150),
                    thickness=2)
        segment_label = "ID" + str(i)
        insertion_angle_header.append(segment_label)
    insertion_angle_data.extend(intersection_angles)

    if 'morphology_data' not in outputs.measurements:
        outputs.measurements['morphology_data'] = {}
    outputs.measurements['morphology_data'][
        'segment_insertion_angles'] = intersection_angles

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(
            labeled_img,
            os.path.join(params.debug_outdir,
                         str(params.device) + '_segment_insertion_angles.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return insertion_angle_header, insertion_angle_data, labeled_img