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
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
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
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
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
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
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
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
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