def segment_tangent_angle(segmented_img, objects, size): """ Find 'tangent' angles in degrees of skeleton segments. Use `size` pixels on either end of each segment to find a linear regression line, and calculate angle between the two lines drawn per segment. Inputs: segmented_img = Segmented image to plot slope lines and intersection angles on objects = List of contours size = Size of ends used to calculate "tangent" lines Returns: labeled_img = Segmented debugging image with angles labeled :param segmented_img: numpy.ndarray :param objects: list :param size: int :return labeled_img: numpy.ndarray """ # Store debug debug = params.debug params.debug = None labeled_img = segmented_img.copy() intersection_angles = [] label_coord_x = [] label_coord_y = [] rand_color = color_palette(len(objects)) for i, cnt in enumerate(objects): find_tangents = np.zeros(segmented_img.shape[:2], np.uint8) cv2.drawContours(find_tangents, objects, i, 255, 1, lineType=8) cv2.drawContours(labeled_img, objects, i, rand_color[i], params.line_thickness, lineType=8) pruned_segment = _iterative_prune(find_tangents, size) segment_ends = find_tangents - pruned_segment segment_end_obj, segment_end_hierarchy = find_objects(segment_ends, segment_ends) slopes = [] for j, obj in enumerate(segment_end_obj): # Find bounds for regression lines to get drawn rect = cv2.minAreaRect(cnt) pts = cv2.boxPoints(rect) df = pd.DataFrame(pts, columns=('x', 'y')) x_max = int(df['x'].max()) x_min = int(df['x'].min()) # Find line fit to each segment [vx, vy, x, y] = cv2.fitLine(obj, cv2.DIST_L2, 0, 0.01, 0.01) slope = -vy / vx left_list = int(((x - x_min) * slope) + y) right_list = int(((x - x_max) * slope) + y) slopes.append(slope) if slope > 1000000 or slope < -1000000: print("Slope of contour with ID#", i, "is", slope, "and cannot be plotted.") else: # Draw slope lines cv2.line(labeled_img, (x_max - 1, right_list), (x_min, left_list), rand_color[i], 1) if len(slopes) < 2: # If size*2>len(obj) then pruning will remove the segment completely, and # makes segment_end_objs contain just one contour. print("Size too large, contour with ID#", i, "got pruned away completely.") intersection_angles.append("NA") else: # Calculate intersection angles slope1 = slopes[0][0] slope2 = slopes[1][0] intersection_angle = _slope_to_intesect_angle(slope1, slope2) intersection_angles.append(intersection_angle) # Store coordinates for labels label_coord_x.append(objects[i][0][0][0]) label_coord_y.append(objects[i][0][0][1]) segment_ids = [] for i, cnt in enumerate(objects): # Label slope lines w = label_coord_x[i] h = label_coord_y[i] if type(intersection_angles[i]) is str: text = "{}".format(intersection_angles[i]) else: 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_tangent_angle', trait='segment tangent angle', method='plantcv.plantcv.morphology.segment_tangent_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_tangent_angles.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 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) _, 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) pruned_img = _iterative_prune(pruned_img, 1) # Reset debug mode params.debug = debug # 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) # Auto-increment device 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