diff --git a/docs/img/documentation_images/segment_ends/segment_end_pts.png b/docs/img/documentation_images/segment_ends/segment_end_pts.png new file mode 100644 index 000000000..89b4493c8 Binary files /dev/null and b/docs/img/documentation_images/segment_ends/segment_end_pts.png differ diff --git a/docs/img/documentation_images/segment_ends/setaria_mask.png b/docs/img/documentation_images/segment_ends/setaria_mask.png new file mode 100644 index 000000000..3e3e53cb2 Binary files /dev/null and b/docs/img/documentation_images/segment_ends/setaria_mask.png differ diff --git a/docs/segment_ends.md b/docs/segment_ends.md new file mode 100644 index 000000000..c71a1b75e --- /dev/null +++ b/docs/segment_ends.md @@ -0,0 +1,51 @@ +## Identify Segment Ends + +Find segment tip and inner branch-point coordinates, and sort them by the y-coordinates of the branch points + +**plantcv.morphology.segment_ends**(*skel_img, leaf_objects, mask=None, label=None*) + +**returns** Re-ordered leaf segments + +- **Parameters:** + - skel_img - Skeleton image (output from [plantcv.morphology.skeletonize](skeletonize.md)) + - leaf_objects - Secondary segment objects (output from [plantcv.morphology.segment_sort](segment_sort.md)). + - mask - Binary mask for plotting. If provided, the debugging image will be overlaid on the mask (optional). + - label - Optional label parameter, modifies the variable name of observations recorded. (default = `pcv.params.sample_label`) +- **Context:** + - Aims to sort leaf objects by biological age. This tends to work somewhat consistently for grass species that have leav + +**Reference Images** + +![Screenshot](img/documentation_images/segment_ends/setaria_mask.png) + +```python + +from plantcv import plantcv as pcv + +# Set global debug behavior to None (default), "print" (to file), +# or "plot" (Jupyter Notebooks or X11) +pcv.params.debug = "plot" + +# Adjust point thickness with the global line_thickness parameter (default = 5) +pcv.params.line_thickness = 3 + +sorted_obs = pcv.morphology.segment_ends(skel_img=skeleton, + leaf_objects=leaf_objs, + mask=plant_mask, + label="leaves") + +segmented_img, leaves_labeled = pcv.morphology.segment_id(skel_img=skeleton, + objects=leaf_objs, + mask=plant_mask +# Without ID re-assignment +segmented_img, leaves_labeled = pcv.morphology.segment_id(skel_img=skeleton, + objects=sorted_obs, + mask=plant_mask) + +``` + +*Segment end points Debug* + +![Screenshot](img/documentation_images/segment_ends/segment_end_pts.png) + +**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/main/plantcv/plantcv/morphology/segment_ends.py) diff --git a/docs/updating.md b/docs/updating.md index 25566975f..25e0ee404 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -685,6 +685,11 @@ pages for more details on the input and output variable types. * post v3.11: labeled_img = **plantcv.morphology.segment_curvature**(*segmented_img, objects, label="default"*) * post v4.0: labeled_img = **plantcv.morphology.segment_curvature**(*segmented_img, objects, label=None*) +#### plantcv.morphology.segment_ends + +* pre v4.6: NA +* post v4.6: **plantcv.morphology.segment_ends**(*skel_img, leaf_objects, mask=None, label=None*) + #### plantcv.morphology.segment_euclidean_length * pre v3.3: NA diff --git a/mkdocs.yml b/mkdocs.yml index a0e83e2d7..d625c7455 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -118,6 +118,7 @@ nav: - 'Segment Angles': segment_angle.md - 'Combine Segments': segment_combine.md - 'Segment Curvature': segment_curvature.md + - 'Segment Ends': segment_ends.md - 'Segment Euclidean Length': segment_euclidean_length.md - 'Segment ID': segment_id.md - 'Segment Insertion Angle': segment_insertion_angle.md diff --git a/plantcv/plantcv/_helpers.py b/plantcv/plantcv/_helpers.py index 9b10d7ba0..0899edc23 100644 --- a/plantcv/plantcv/_helpers.py +++ b/plantcv/plantcv/_helpers.py @@ -1,11 +1,176 @@ import cv2 import numpy as np +from plantcv.plantcv.dilate import dilate from plantcv.plantcv.logical_and import logical_and +from plantcv.plantcv.image_subtract import image_subtract from plantcv.plantcv import fatal_error, warn from plantcv.plantcv import params import pandas as pd +def _find_segment_ends(skel_img, leaf_objects, plotting_img, size): + """Find both segment ends and sort into tips or inner branchpoints. + + Inputs: + skel_img = Skeletonized image + leaf_objects = List of leaf segments + plotting_img = Mask for debugging, might be a copy of the Skeletonized image + size = Size of inner segment ends (in pixels) + + :param skel_img: numpy.ndarray + :param leaf_objects: list + :param plotting_img: numpy.ndarray + """ + labeled_img = cv2.cvtColor(plotting_img, cv2.COLOR_GRAY2RGB) + tips, _, _ = _find_tips(skel_img) + # Initialize list of tip data points + labels = [] + tip_list = [] + inner_list = [] + + # Find segment end coordinates + for i in range(len(leaf_objects)): + labels.append(i) + # Draw leaf objects + find_segment_tangents = np.zeros(labeled_img.shape[:2], np.uint8) + cv2.drawContours(find_segment_tangents, leaf_objects, i, 255, 1, lineType=8) + cv2.drawContours(labeled_img, leaf_objects, i, (150, 150, 150), params.line_thickness, lineType=8) # segments debug + # Prune back ends of leaves + pruned_segment = _iterative_prune(find_segment_tangents, size) + # Segment ends are the portions pruned off + ends = find_segment_tangents - pruned_segment + segment_end_obj, _ = _cv2_findcontours(bin_img=ends) + # Determine if a segment is segment tip or branch point + for j, obj in enumerate(segment_end_obj): + segment_plot = np.zeros(skel_img.shape[:2], np.uint8) + cv2.drawContours(segment_plot, obj, -1, 255, 1, lineType=8) + segment_plot = dilate(segment_plot, 3, 1) + overlap_img = logical_and(segment_plot, tips) + x, y = segment_end_obj[j].ravel()[:2] + coord = (int(x), int(y)) + # If none of the tips are within a segment_end then it's an insertion segment + if np.sum(overlap_img) == 0: + inner_list.append(coord) + cv2.circle(labeled_img, coord, params.line_thickness, (50, 0, 255), -1) # Red auricles + else: + tip_list.append(coord) + cv2.circle(labeled_img, coord, params.line_thickness, (0, 255, 0), -1) # green tips + + return labeled_img, tip_list, inner_list, labels + + +def _iterative_prune(skel_img, size): + """Iteratively remove endpoints (tips) from a skeletonized image. + The pruning algorithm was inspired by Jean-Patrick Pommier: https://gist.github.com/jeanpat/5712699 + "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 + + # Iteratively remove endpoints (tips) from a skeleton + for _ 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 = _cv2_findcontours(bin_img=pruned_img) + pruned_obj, pruned_hierarchy = _cv2_findcontours(bin_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 + + +def _find_tips(skel_img, mask=None): + """Helper function to find tips in skeletonized image. + The endpoints algorithm was inspired by Jean-Patrick Pommier: https://gist.github.com/jeanpat/5712699 + + Inputs: + skel_img = Skeletonized image + mask = (Optional) binary mask for debugging. If provided, debug image will be overlaid on the mask. + label = Optional label parameter, modifies the variable name of + observations recorded (default = pcv.params.sample_label). + Returns: + tip_img = Image with just tips, rest 0 + + :param skel_img: numpy.ndarray + :param mask: numpy.ndarray + :param label: str + :return tip_img: numpy.ndarray + """ + # In a kernel: 1 values line up with 255s, -1s line up with 0s, and 0s correspond to dont care + endpoint1 = np.array([[-1, -1, -1], + [-1, 1, -1], + [0, 1, 0]]) + endpoint2 = np.array([[-1, -1, -1], + [-1, 1, 0], + [-1, 0, 1]]) + + endpoint3 = np.rot90(endpoint1) + endpoint4 = np.rot90(endpoint2) + endpoint5 = np.rot90(endpoint3) + endpoint6 = np.rot90(endpoint4) + endpoint7 = np.rot90(endpoint5) + endpoint8 = np.rot90(endpoint6) + + endpoints = [endpoint1, endpoint2, endpoint3, endpoint4, endpoint5, endpoint6, endpoint7, endpoint8] + tip_img = np.zeros(skel_img.shape[:2], dtype=int) + for endpoint in endpoints: + tip_img = np.logical_or(cv2.morphologyEx(skel_img, op=cv2.MORPH_HITMISS, kernel=endpoint, + borderType=cv2.BORDER_CONSTANT, borderValue=0), tip_img) + tip_img = tip_img.astype(np.uint8) * 255 + # Store debug + debug = params.debug + params.debug = None + tip_objects, _ = _cv2_findcontours(bin_img=tip_img) + + if mask is None: + # Make debugging image + dilated_skel = dilate(skel_img, params.line_thickness, 1) + tip_plot = cv2.cvtColor(dilated_skel, cv2.COLOR_GRAY2RGB) + + else: + # Make debugging image on mask + mask_copy = mask.copy() + tip_plot = cv2.cvtColor(mask_copy, cv2.COLOR_GRAY2RGB) + skel_obj, skel_hier = _cv2_findcontours(bin_img=skel_img) + cv2.drawContours(tip_plot, skel_obj, -1, (150, 150, 150), params.line_thickness, + lineType=8, hierarchy=skel_hier) + + # Initialize list of tip data points + tip_list = [] + tip_labels = [] + for i, tip in enumerate(tip_objects): + x, y = tip.ravel()[:2] + coord = (int(x), int(y)) + tip_list.append(coord) + tip_labels.append(i) + cv2.circle(tip_plot, (x, y), params.line_thickness, (0, 255, 0), -1) + + # Reset debug mode + params.debug = debug + + return tip_img, tip_list, tip_labels + + def _hough_circle(gray_img, mindist, candec, accthresh, minradius, maxradius, maxfound=None): """ Hough Circle Detection diff --git a/plantcv/plantcv/morphology/__init__.py b/plantcv/plantcv/morphology/__init__.py index 858112df5..dc44af6c8 100644 --- a/plantcv/plantcv/morphology/__init__.py +++ b/plantcv/plantcv/morphology/__init__.py @@ -1,6 +1,5 @@ from plantcv.plantcv.morphology.find_branch_pts import find_branch_pts from plantcv.plantcv.morphology.find_tips import find_tips -from plantcv.plantcv.morphology._iterative_prune import _iterative_prune from plantcv.plantcv.morphology.segment_skeleton import segment_skeleton from plantcv.plantcv.morphology.segment_sort import segment_sort from plantcv.plantcv.morphology.prune import prune @@ -16,8 +15,10 @@ from plantcv.plantcv.morphology.segment_combine import segment_combine from plantcv.plantcv.morphology.analyze_stem import analyze_stem from plantcv.plantcv.morphology.fill_segments import fill_segments +from plantcv.plantcv.morphology.segment_ends import segment_ends -__all__ = ["find_branch_pts", "find_tips", "prune", "skeletonize", "check_cycles", "segment_skeleton", "segment_angle", +__all__ = ["find_branch_pts", "find_tips", "prune", "skeletonize", "check_cycles", + "segment_skeleton", "segment_angle", "segment_path_length", "segment_euclidean_length", "segment_curvature", "segment_sort", "segment_id", - "segment_tangent_angle", "segment_insertion_angle", "segment_combine", "_iterative_prune", "analyze_stem", - "fill_segments"] + "segment_tangent_angle", "segment_insertion_angle", "segment_combine", "analyze_stem", + "fill_segments", "segment_ends"] diff --git a/plantcv/plantcv/morphology/_iterative_prune.py b/plantcv/plantcv/morphology/_iterative_prune.py deleted file mode 100644 index 4816bfdb1..000000000 --- a/plantcv/plantcv/morphology/_iterative_prune.py +++ /dev/null @@ -1,46 +0,0 @@ -import cv2 -import numpy as np -from plantcv.plantcv import params -from plantcv.plantcv import image_subtract -from plantcv.plantcv.morphology import find_tips -from plantcv.plantcv._helpers import _cv2_findcontours - - -def _iterative_prune(skel_img, size): - """Iteratively remove endpoints (tips) from a skeletonized image. - The pruning algorithm was inspired by Jean-Patrick Pommier: https://gist.github.com/jeanpat/5712699 - "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 - - # Iteratively remove endpoints (tips) from a skeleton - for _ 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 = _cv2_findcontours(bin_img=pruned_img) - pruned_obj, pruned_hierarchy = _cv2_findcontours(bin_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 diff --git a/plantcv/plantcv/morphology/find_tips.py b/plantcv/plantcv/morphology/find_tips.py index 4d40c5c19..4831c8772 100644 --- a/plantcv/plantcv/morphology/find_tips.py +++ b/plantcv/plantcv/morphology/find_tips.py @@ -1,12 +1,9 @@ """Find tips from skeleton image.""" import os -import cv2 -import numpy as np from plantcv.plantcv import params -from plantcv.plantcv import dilate from plantcv.plantcv import outputs from plantcv.plantcv._debug import _debug -from plantcv.plantcv._helpers import _cv2_findcontours +from plantcv.plantcv._helpers import _find_tips def find_tips(skel_img, mask=None, label=None): @@ -26,65 +23,16 @@ def find_tips(skel_img, mask=None, label=None): :param label: str :return tip_img: numpy.ndarray """ + tip_img, tip_list, tip_labels = _find_tips(skel_img=skel_img, mask=mask) + + _debug(visual=tip_img, filename=os.path.join(params.debug_outdir, f"{params.device}_skeleton_tips.png")) + # Set lable to params.sample_label if None if label is None: label = params.sample_label - - # In a kernel: 1 values line up with 255s, -1s line up with 0s, and 0s correspond to dont care - endpoint1 = np.array([[-1, -1, -1], - [-1, 1, -1], - [0, 1, 0]]) - endpoint2 = np.array([[-1, -1, -1], - [-1, 1, 0], - [-1, 0, 1]]) - - endpoint3 = np.rot90(endpoint1) - endpoint4 = np.rot90(endpoint2) - endpoint5 = np.rot90(endpoint3) - endpoint6 = np.rot90(endpoint4) - endpoint7 = np.rot90(endpoint5) - endpoint8 = np.rot90(endpoint6) - - endpoints = [endpoint1, endpoint2, endpoint3, endpoint4, endpoint5, endpoint6, endpoint7, endpoint8] - tip_img = np.zeros(skel_img.shape[:2], dtype=int) - for endpoint in endpoints: - tip_img = np.logical_or(cv2.morphologyEx(skel_img, op=cv2.MORPH_HITMISS, kernel=endpoint, - borderType=cv2.BORDER_CONSTANT, borderValue=0), tip_img) - tip_img = tip_img.astype(np.uint8) * 255 - # Store debug - debug = params.debug - params.debug = None - tip_objects, _ = _cv2_findcontours(bin_img=tip_img) - - if mask is None: - # Make debugging image - dilated_skel = dilate(skel_img, params.line_thickness, 1) - tip_plot = cv2.cvtColor(dilated_skel, cv2.COLOR_GRAY2RGB) - - else: - # Make debugging image on mask - mask_copy = mask.copy() - tip_plot = cv2.cvtColor(mask_copy, cv2.COLOR_GRAY2RGB) - skel_obj, skel_hier = _cv2_findcontours(bin_img=skel_img) - cv2.drawContours(tip_plot, skel_obj, -1, (150, 150, 150), params.line_thickness, - lineType=8, hierarchy=skel_hier) - - # Initialize list of tip data points - tip_list = [] - tip_labels = [] - for i, tip in enumerate(tip_objects): - x, y = tip.ravel()[:2] - coord = (int(x), int(y)) - tip_list.append(coord) - tip_labels.append(i) - cv2.circle(tip_plot, (x, y), params.line_thickness, (0, 255, 0), -1) - + # Save coordinates to Outputs outputs.add_observation(sample=label, variable='tips', trait='list of tip coordinates identified from a skeleton', method='plantcv.plantcv.morphology.find_tips', scale='pixels', datatype=list, value=tip_list, label=tip_labels) - # Reset debug mode - params.debug = debug - _debug(visual=tip_plot, filename=os.path.join(params.debug_outdir, f"{params.device}_skeleton_tips.png")) - return tip_img diff --git a/plantcv/plantcv/morphology/prune.py b/plantcv/plantcv/morphology/prune.py index e71b16dfa..4edbf9ac0 100644 --- a/plantcv/plantcv/morphology/prune.py +++ b/plantcv/plantcv/morphology/prune.py @@ -5,11 +5,9 @@ import numpy as np from plantcv.plantcv import params from plantcv.plantcv import image_subtract -from plantcv.plantcv.morphology import segment_sort -from plantcv.plantcv.morphology import segment_skeleton -from plantcv.plantcv.morphology import _iterative_prune +from plantcv.plantcv.morphology import segment_sort, segment_skeleton from plantcv.plantcv._debug import _debug -from plantcv.plantcv._helpers import _cv2_findcontours +from plantcv.plantcv._helpers import _cv2_findcontours, _iterative_prune def prune(skel_img, size=0, mask=None): diff --git a/plantcv/plantcv/morphology/segment_curvature.py b/plantcv/plantcv/morphology/segment_curvature.py index 8e7b3a413..94ee8c85b 100644 --- a/plantcv/plantcv/morphology/segment_curvature.py +++ b/plantcv/plantcv/morphology/segment_curvature.py @@ -5,11 +5,10 @@ from plantcv.plantcv import params from plantcv.plantcv import outputs from plantcv.plantcv import color_palette -from plantcv.plantcv.morphology import find_tips from plantcv.plantcv.morphology import segment_path_length from plantcv.plantcv.morphology import segment_euclidean_length from plantcv.plantcv._debug import _debug -from plantcv.plantcv._helpers import _cv2_findcontours +from plantcv.plantcv._helpers import _cv2_findcontours, _find_tips def segment_curvature(segmented_img, objects, label=None): @@ -58,7 +57,7 @@ def segment_curvature(segmented_img, objects, label=None): # 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) + segment_tips, _, _ = _find_tips(finding_tips_img) tip_objects, _ = _cv2_findcontours(bin_img=segment_tips) points = [] diff --git a/plantcv/plantcv/morphology/segment_ends.py b/plantcv/plantcv/morphology/segment_ends.py new file mode 100644 index 000000000..e60790cba --- /dev/null +++ b/plantcv/plantcv/morphology/segment_ends.py @@ -0,0 +1,63 @@ +# Find both segment end coordinates +import os +import numpy as np +from plantcv.plantcv import params, outputs +from plantcv.plantcv._debug import _debug +from plantcv.plantcv._helpers import _find_segment_ends + + +def segment_ends(skel_img, leaf_objects, mask=None, label=None): + """Find tips and segment branch points. + + Inputs: + skel_img = Skeletonized image + leaf_objects = List of leaf segments + mask = (Optional) binary mask for debugging. If provided, debug image will be overlaid on the mask. + label = Optional label parameter, modifies the variable name of + observations recorded (default = pcv.params.sample_label). + + Returns: + sorted_obs = Reordered segments based on segment branch point y-coordinates + + :param segmented_img: numpy.ndarray + :param leaf_objects: list + :param mask: numpy.ndarray + :param label: str + :return sorted_ids: list + """ + # Store debug + debug = params.debug + params.debug = None + + if mask is None: + labeled_img = skel_img.copy() + else: + labeled_img = mask.copy() + # Find and sort segment ends, and create debug image + labeled_img, tip_list, inner_list, labels = _find_segment_ends( + skel_img=skel_img, leaf_objects=leaf_objects, plotting_img=labeled_img, size=1) + # Set lable to params.sample_label if None + if label is None: + label = params.sample_label + # Save coordinates to Outputs + outputs.add_observation(sample=label, variable='segment_tips', + trait='list of tip coordinates identified from segments', + method='plantcv.plantcv.morphology.segment_ends', scale='None', datatype=list, + value=tip_list, label=labels) + outputs.add_observation(sample=label, variable='segment_branch_points', + trait='list of branch point coordinates identified from segments', + method='plantcv.plantcv.morphology.segment_ends', scale='None', datatype=list, + value=inner_list, label=labels) + # Reset debug mode + params.debug = debug + _debug(visual=labeled_img, filename=os.path.join(params.debug_outdir, f"{params.device}_segment_ends.png")) + + # Determine optimal segment order by y-coordinate order + d = {} + for i, coord in enumerate(inner_list): + d[i] = coord[1] # y-coord is the key and index the value + values = list(d.values()) + sorted_key_index = np.argsort(values) + sorted_objs = [leaf_objects[i] for i in sorted_key_index[::-1]] + + return sorted_objs diff --git a/plantcv/plantcv/morphology/segment_euclidean_length.py b/plantcv/plantcv/morphology/segment_euclidean_length.py index dea6d99e5..e2422ff44 100644 --- a/plantcv/plantcv/morphology/segment_euclidean_length.py +++ b/plantcv/plantcv/morphology/segment_euclidean_length.py @@ -7,8 +7,7 @@ from plantcv.plantcv import fatal_error from plantcv.plantcv import color_palette from plantcv.plantcv._debug import _debug -from plantcv.plantcv._helpers import _cv2_findcontours -from plantcv.plantcv.morphology import find_tips +from plantcv.plantcv._helpers import _cv2_findcontours, _find_tips from scipy.spatial.distance import euclidean @@ -52,7 +51,7 @@ def segment_euclidean_length(segmented_img, objects, label=None): # 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) + segment_tips, _, _ = _find_tips(finding_tips_img) tip_objects, _ = _cv2_findcontours(bin_img=segment_tips) points = [] if not len(tip_objects) == 2: diff --git a/plantcv/plantcv/morphology/segment_id.py b/plantcv/plantcv/morphology/segment_id.py index bf24db742..d7c159959 100644 --- a/plantcv/plantcv/morphology/segment_id.py +++ b/plantcv/plantcv/morphology/segment_id.py @@ -7,13 +7,14 @@ from plantcv.plantcv._debug import _debug -def segment_id(skel_img, objects, mask=None): +def segment_id(skel_img, objects, mask=None, optimal_assignment=None): """Plot segment IDs. 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. + optimal_assignment = functions similar to the "label" parameter where it replaces the unique labels Returns: segmented_img = Segmented image @@ -40,20 +41,30 @@ def segment_id(skel_img, objects, mask=None): # Plot all segment contours for i, cnt in enumerate(objects): + if optimal_assignment is not None: + color_index = optimal_assignment[i] + else: + color_index = i cv2.drawContours(segmented_img, cnt, -1, rand_color[i], params.line_thickness, lineType=8) # Store coordinates for labels - label_coord_x.append(objects[i][0][0][0]) - label_coord_y.append(objects[i][0][0][1]) + label_coord_x.append(objects[color_index][0][0][0]) + label_coord_y.append(objects[color_index][0][0][1]) labeled_img = segmented_img.copy() for i, cnt in enumerate(objects): - # Label slope lines - w = label_coord_x[i] - h = label_coord_y[i] - text = f"ID:{i}" + if optimal_assignment is not None: + # relabel IDs + text = f"{optimal_assignment[i]}" + color_index = optimal_assignment[i] + else: + text = f"{i}" + color_index = i + # Label segments + w = label_coord_x[color_index] + h = label_coord_y[color_index] cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX, - fontScale=params.text_size, color=rand_color[i], thickness=params.text_thickness) + fontScale=params.text_size, color=rand_color[color_index], thickness=params.text_thickness) _debug(visual=labeled_img, filename=os.path.join(params.debug_outdir, f"{params.device}_segmented_ids.png")) diff --git a/plantcv/plantcv/morphology/segment_insertion_angle.py b/plantcv/plantcv/morphology/segment_insertion_angle.py index c0cc6e44a..9e18f589a 100644 --- a/plantcv/plantcv/morphology/segment_insertion_angle.py +++ b/plantcv/plantcv/morphology/segment_insertion_angle.py @@ -9,11 +9,9 @@ from plantcv.plantcv import logical_and from plantcv.plantcv import fatal_error from plantcv.plantcv import color_palette -from plantcv.plantcv.morphology import _iterative_prune -from plantcv.plantcv.morphology import find_tips from plantcv.plantcv.morphology.segment_tangent_angle import _slope_to_intesect_angle from plantcv.plantcv._debug import _debug -from plantcv.plantcv._helpers import _cv2_findcontours +from plantcv.plantcv._helpers import _cv2_findcontours, _find_tips, _iterative_prune def segment_insertion_angle(skel_img, segmented_img, leaf_objects, stem_objects, size, label=None): @@ -62,12 +60,7 @@ def segment_insertion_angle(skel_img, segmented_img, leaf_objects, stem_objects, pruned_away = [] # Create a list of tip tuples to use for sorting - tips = find_tips(skel_img) - tips = dilate(tips, 3, 1) - tip_objects, _ = _cv2_findcontours(bin_img=tips) - tip_tuples = [] - for i, cnt in enumerate(tip_objects): - tip_tuples.append((cnt[0][0][0], cnt[0][0][1])) + tips, _, _ = _find_tips(skel_img) for i, cnt in enumerate(leaf_objects): # Draw leaf objects diff --git a/plantcv/plantcv/morphology/segment_sort.py b/plantcv/plantcv/morphology/segment_sort.py index c86c4bcde..e797871b9 100644 --- a/plantcv/plantcv/morphology/segment_sort.py +++ b/plantcv/plantcv/morphology/segment_sort.py @@ -5,9 +5,8 @@ import numpy as np from plantcv.plantcv import dilate from plantcv.plantcv import params -from plantcv.plantcv import outputs from plantcv.plantcv import logical_and -from plantcv.plantcv.morphology import find_tips +from plantcv.plantcv._helpers import _find_tips from plantcv.plantcv._debug import _debug @@ -36,9 +35,6 @@ def segment_sort(skel_img, objects, mask=None, first_stem=True): debug = params.debug params.debug = None - # Store label - label = params.sample_label - secondary_objects = [] primary_objects = [] @@ -47,7 +43,7 @@ def segment_sort(skel_img, objects, mask=None, first_stem=True): else: labeled_img = mask.copy() - tips_img = find_tips(skel_img) + tips_img, _, _ = _find_tips(skel_img) tips_img = dilate(tips_img, 3, 1) # Loop through segment contours @@ -59,10 +55,6 @@ def segment_sort(skel_img, objects, mask=None, first_stem=True): # 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) - # Remove the first "tip" since it corresponds to stem not leaf. This helps - # leaf number to match the number of "tips" - outputs.observations[label]["tips"]["value"] = outputs.observations[label]["tips"]["value"][1:] - outputs.observations[label]["tips"]["label"] = outputs.observations[label]["tips"]["label"][:-1] # Sort segments else: diff --git a/plantcv/plantcv/morphology/segment_tangent_angle.py b/plantcv/plantcv/morphology/segment_tangent_angle.py index 6a5d86938..a281a929d 100644 --- a/plantcv/plantcv/morphology/segment_tangent_angle.py +++ b/plantcv/plantcv/morphology/segment_tangent_angle.py @@ -6,9 +6,8 @@ from plantcv.plantcv import params from plantcv.plantcv import outputs from plantcv.plantcv import color_palette -from plantcv.plantcv.morphology import _iterative_prune from plantcv.plantcv._debug import _debug -from plantcv.plantcv._helpers import _cv2_findcontours +from plantcv.plantcv._helpers import _cv2_findcontours, _iterative_prune def _slope_to_intesect_angle(m1, m2): diff --git a/tests/plantcv/morphology/test_prune.py b/tests/plantcv/morphology/test_prune.py index ab09fdc65..cc2317e76 100644 --- a/tests/plantcv/morphology/test_prune.py +++ b/tests/plantcv/morphology/test_prune.py @@ -1,6 +1,6 @@ import cv2 import numpy as np -from plantcv.plantcv.morphology import prune, _iterative_prune +from plantcv.plantcv.morphology import prune def test_prune(morphology_test_data): @@ -22,10 +22,3 @@ def test_prune_size0(morphology_test_data): skeleton = cv2.imread(morphology_test_data.skel_img, -1) pruned_img, _, _ = prune(skel_img=skeleton, size=0) assert np.sum(pruned_img) == np.sum(skeleton) - - -def test_iterative_prune(morphology_test_data): - """Test for PlantCV.""" - skeleton = cv2.imread(morphology_test_data.skel_img, -1) - pruned_img = _iterative_prune(skel_img=skeleton, size=3) - assert np.sum(pruned_img) < np.sum(skeleton) diff --git a/tests/plantcv/morphology/test_segment_ends.py b/tests/plantcv/morphology/test_segment_ends.py new file mode 100644 index 000000000..e3a05e84c --- /dev/null +++ b/tests/plantcv/morphology/test_segment_ends.py @@ -0,0 +1,23 @@ +import cv2 +from plantcv.plantcv import outputs +from plantcv.plantcv.morphology import segment_ends + + +def test_segment_ends(morphology_test_data): + """Test for PlantCV.""" + # Clear previous outputs + outputs.clear() + leaf_obj = morphology_test_data.load_segments(morphology_test_data.segments_file, "leaves") + skeleton = cv2.imread(morphology_test_data.skel_img, -1) + segment_ends(skel_img=skeleton, leaf_objects=leaf_obj, mask=skeleton) + assert len(outputs.observations['default']['segment_branch_points']['value']) == 4 + + +def test_segment_ends_no_mask(morphology_test_data): + """Test for PlantCV.""" + # Clear previous outputs + outputs.clear() + leaf_obj = morphology_test_data.load_segments(morphology_test_data.segments_file, "leaves") + skeleton = cv2.imread(morphology_test_data.skel_img, -1) + segment_ends(skel_img=skeleton, leaf_objects=leaf_obj, mask=None) + assert len(outputs.observations['default']['segment_branch_points']['value']) == 4 \ No newline at end of file diff --git a/tests/plantcv/morphology/test_segment_id.py b/tests/plantcv/morphology/test_segment_id.py index bed778194..256f47dc2 100644 --- a/tests/plantcv/morphology/test_segment_id.py +++ b/tests/plantcv/morphology/test_segment_id.py @@ -6,7 +6,7 @@ def test_segment_id(morphology_test_data): """Test for PlantCV.""" skel = cv2.imread(morphology_test_data.skel_img, -1) leaf_obj = morphology_test_data.load_segments(morphology_test_data.segments_file, "leaves") - _, labeled_img = segment_id(skel_img=skel, objects=leaf_obj, mask=skel) + _, labeled_img = segment_id(skel_img=skel, objects=leaf_obj, mask=skel, optimal_assignment=[0,1,2,3]) assert skel.shape == labeled_img.shape[:2]