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 main(): # Get options args = options() # Set variables device = 0 pcv.params.debug = args.debug img_file = args.image # Read image img, path, filename = pcv.readimage(filename=img_file, mode='rgb') # Process saturation channel from HSV colour space s = pcv.rgb2gray_hsv(rgb_img=img, channel='s') lp_s = pcv.laplace_filter(s, 1, 1) shrp_s = pcv.image_subtract(s, lp_s) s_eq = pcv.hist_equalization(shrp_s) s_thresh = pcv.threshold.binary(gray_img=s_eq, threshold=215, max_value=255, object_type='light') s_mblur = pcv.median_blur(gray_img=s_thresh, ksize=5) # Process green-magenta channel from LAB colour space b = pcv.rgb2gray_lab(rgb_img=img, channel='a') b_lp = pcv.laplace_filter(b, 1, 1) b_shrp = pcv.image_subtract(b, b_lp) b_thresh = pcv.threshold.otsu(b_shrp, 255, object_type='dark') # Create and apply mask bs = pcv.logical_or(bin_img1=s_mblur, bin_img2=b_thresh) filled = pcv.fill_holes(bs) masked = pcv.apply_mask(img=img, mask=filled, mask_color='white') # Extract colour channels from masked image masked_a = pcv.rgb2gray_lab(rgb_img=masked, channel='a') masked_b = pcv.rgb2gray_lab(rgb_img=masked, channel='b') # Threshold the green-magenta and blue images maskeda_thresh = pcv.threshold.binary(gray_img=masked_a, threshold=115, max_value=255, object_type='dark') maskeda_thresh1 = pcv.threshold.binary(gray_img=masked_a, threshold=140, max_value=255, object_type='light') maskedb_thresh = pcv.threshold.binary(gray_img=masked_b, threshold=128, max_value=255, object_type='light') # Join the thresholded saturation and blue-yellow images (OR) ab1 = pcv.logical_or(bin_img1=maskeda_thresh, bin_img2=maskedb_thresh) ab = pcv.logical_or(bin_img1=maskeda_thresh1, bin_img2=ab1) # Produce and apply a mask opened_ab = pcv.opening(gray_img=ab) ab_fill = pcv.fill(bin_img=ab, size=200) closed_ab = pcv.closing(gray_img=ab_fill) masked2 = pcv.apply_mask(img=masked, mask=bs, mask_color='white') # Identify objects id_objects, obj_hierarchy = pcv.find_objects(img=masked2, mask=ab_fill) # Define region of interest (ROI) roi1, roi_hierarchy = pcv.roi.rectangle(img=masked2, x=250, y=100, h=200, w=200) # Decide what objects to keep roi_objects, hierarchy3, kept_mask, obj_area = pcv.roi_objects( img=img, roi_contour=roi1, roi_hierarchy=roi_hierarchy, object_contour=id_objects, obj_hierarchy=obj_hierarchy, roi_type='partial') # Object combine kept objects obj, mask = pcv.object_composition(img=img, contours=roi_objects, hierarchy=hierarchy3) ############### Analysis ################ outfile = False if args.writeimg == True: outfile = args.outdir + "/" + filename # Analyze the plant analysis_image = pcv.analyze_object(img=img, obj=obj, mask=mask) color_histogram = pcv.analyze_color(rgb_img=img, mask=kept_mask, hist_plot_type='all') top_x, bottom_x, center_v_x = pcv.x_axis_pseudolandmarks(img=img, obj=obj, mask=mask) top_y, bottom_y, center_v_y = pcv.y_axis_pseudolandmarks(img=img, obj=obj, mask=mask) # Print results of the analysis pcv.print_results(filename=args.result) pcv.output_mask(img, kept_mask, filename, outdir=args.outdir, mask_only=True)
def main(): # Initialize options args = options() # Set PlantCV debug mode to input debug method pcv.params.debug = args.debug # Use PlantCV to read in the input image. The function outputs an image as a NumPy array, the path to the file, # and the image filename img, path, filename = pcv.readimage(filename=args.image) # ## Segmentation # ### Saturation channel # Convert the RGB image to HSV colorspace and extract the saturation channel s = pcv.rgb2gray_hsv(rgb_img=img, channel='s') # Use a binary threshold to set an inflection value where all pixels in the grayscale saturation image below the # threshold get set to zero (pure black) and all pixels at or above the threshold get set to 255 (pure white) s_thresh = pcv.threshold.binary(gray_img=s, threshold=80, max_value=255, object_type='light') # ### Blue-yellow channel # Convert the RGB image to LAB colorspace and extract the blue-yellow channel b = pcv.rgb2gray_lab(rgb_img=img, channel='b') # Use a binary threshold to set an inflection value where all pixels in the grayscale blue-yellow image below the # threshold get set to zero (pure black) and all pixels at or above the threshold get set to 255 (pure white) b_thresh = pcv.threshold.binary(gray_img=b, threshold=134, max_value=255, object_type='light') # ### Green-magenta channel # Convert the RGB image to LAB colorspace and extract the green-magenta channel a = pcv.rgb2gray_lab(rgb_img=img, channel='a') # In the green-magenta image the plant pixels are darker than the background. Setting object_type="dark" will # invert the image first and then use a binary threshold to set an inflection value where all pixels in the # grayscale green-magenta image below the threshold get set to zero (pure black) and all pixels at or above the # threshold get set to 255 (pure white) a_thresh = pcv.threshold.binary(gray_img=a, threshold=122, max_value=255, object_type='dark') # Combine the binary images for the saturation and blue-yellow channels. The "or" operator returns a binary image # that is white when a pixel was white in either or both input images bs = pcv.logical_or(bin_img1=s_thresh, bin_img2=b_thresh) # Combine the binary images for the combined saturation and blue-yellow channels and the green-magenta channel. # The "or" operator returns a binary image that is white when a pixel was white in either or both input images bsa = pcv.logical_or(bin_img1=bs, bin_img2=a_thresh) # The combined binary image labels plant pixels well but the background still has pixels labeled as foreground. # Small white noise (salt) in the background can be removed by filtering white objects in the image by size and # setting a size threshold where smaller objects can be removed bsa_fill1 = pcv.fill(bin_img=bsa, size=15) # Fill small noise # Before more stringent size filtering is done we want to connect plant parts that may still be disconnected from # the main plant. Use a dilation to expand the boundary of white regions. Ksize is the size of a box scanned # across the image and i is the number of times a scan is done bsa_fill2 = pcv.dilate(gray_img=bsa_fill1, ksize=3, i=3) # Remove small objects by size again but use a higher threshold bsa_fill3 = pcv.fill(bin_img=bsa_fill2, size=250) # Use the binary image to identify objects or connected components. id_objects, obj_hierarchy = pcv.find_objects(img=img, mask=bsa_fill3) # Because the background still contains pixels labeled as foreground, the object list contains background. # Because these images were collected in an automated system the plant is always centered in the image at the # same position each time. Define a region of interest (ROI) to set the area where we expect to find plant # pixels. PlantCV can make simple ROI shapes like rectangles, circles, etc. but here we use a custom ROI to fit a # polygon around the plant area roi_custom, roi_hier_custom = pcv.roi.custom(img=img, vertices=[[1085, 1560], [1395, 1560], [1395, 1685], [1890, 1744], [1890, 25], [600, 25], [615, 1744], [1085, 1685]]) # Use the ROI to filter out objects found outside the ROI. When `roi_type = "cutto"` objects outside the ROI are # cropped out. The default `roi_type` is "partial" which allows objects to overlap the ROI and be retained roi_objects, hierarchy, kept_mask, obj_area = pcv.roi_objects(img=img, roi_contour=roi_custom, roi_hierarchy=roi_hier_custom, object_contour=id_objects, obj_hierarchy=obj_hierarchy, roi_type='cutto') # Filter remaining objects by size again to remove any remaining background objects filled_mask1 = pcv.fill(bin_img=kept_mask, size=350) # Use a closing operation to first dilate (expand) and then erode (shrink) the plant to fill in any additional # gaps in leaves or stems filled_mask2 = pcv.closing(gray_img=filled_mask1) # Remove holes or dark spot noise (pepper) in the plant binary image filled_mask3 = pcv.fill_holes(filled_mask2) # With the clean binary image identify the contour of the plant id_objects, obj_hierarchy = pcv.find_objects(img=img, mask=filled_mask3) # Because a plant or object of interest may be composed of multiple contours, it is required to combine all # remaining contours into a single contour before measurements can be done obj, mask = pcv.object_composition(img=img, contours=id_objects, hierarchy=obj_hierarchy) # ## Measurements PlantCV has several built-in measurement or analysis methods. Here, basic measurements of size # and shape are done. Additional typical modules would include plant height (`pcv.analyze_bound_horizontal`) and # color (`pcv.analyze_color`) shape_img = pcv.analyze_object(img=img, obj=obj, mask=mask) # Save the shape image if requested if args.writeimg: outfile = os.path.join(args.outdir, filename[:-4] + "_shapes.png") pcv.print_image(img=shape_img, filename=outfile) # ## Morphology workflow # Update a few PlantCV parameters for plotting purposes pcv.params.text_size = 1.5 pcv.params.text_thickness = 5 pcv.params.line_thickness = 15 # Convert the plant mask into a "skeletonized" image where each path along the stem and leaves are a single pixel # wide skel = pcv.morphology.skeletonize(mask=mask) # Sometimes wide parts of leaves or stems are skeletonized in the direction perpendicular to the main path. These # "barbs" or "spurs" can be removed by pruning the skeleton to remove small paths. Pruning will also separate the # individual path segments (leaves and stem parts) pruned, segmented_img, segment_objects = pcv.morphology.prune(skel_img=skel, size=30, mask=mask) pruned, segmented_img, segment_objects = pcv.morphology.prune(skel_img=pruned, size=3, mask=mask) # Leaf and stem segments above are separated but only into individual paths. We can sort the segments into stem # and leaf paths by identifying primary segments (stems; those that end in a branch point) and secondary segments # (leaves; those that begin at a branch point and end at a tip point) leaf_objects, other_objects = pcv.morphology.segment_sort(skel_img=pruned, objects=segment_objects, mask=mask) # Label the segment unique IDs segmented_img, labeled_id_img = pcv.morphology.segment_id(skel_img=pruned, objects=leaf_objects, mask=mask) # Measure leaf insertion angles. Measures the angle between a line fit through the stem paths and a line fit # through the first `size` points of each leaf path labeled_angle_img = pcv.morphology.segment_insertion_angle(skel_img=pruned, segmented_img=segmented_img, leaf_objects=leaf_objects, stem_objects=other_objects, size=22) # Save leaf angle image if requested if args.writeimg: outfile = os.path.join(args.outdir, filename[:-4] + "_leaf_insertion_angles.png") pcv.print_image(img=labeled_angle_img, filename=outfile) # ## Other potential morphological measurements There are many other functions that extract data from within the # morphology sub-package of PlantCV. For our purposes, we are most interested in the relative angle between each # leaf and the stem which we measure with `plantcv.morphology.segment_insertion_angle`. However, the following # cells show some of the other traits that we are able to measure from images that can be succesfully sorted into # primary and secondary segments. # Segment the plant binary mask using the leaf and stem segments. Allows for the measurement of individual leaf # areas # filled_img = pcv.morphology.fill_segments(mask=mask, objects=leaf_objects) # Measure the path length of each leaf (geodesic distance) # labeled_img2 = pcv.morphology.segment_path_length(segmented_img=segmented_img, objects=leaf_objects) # Measure the straight-line, branch point to tip distance (Euclidean) for each leaf # labeled_img3 = pcv.morphology.segment_euclidean_length(segmented_img=segmented_img, objects=leaf_objects) # Measure the curvature of each leaf (Values closer to 1 indicate that a segment is a straight line while larger # values indicate the segment has more curvature) # labeled_img4 = pcv.morphology.segment_curvature(segmented_img=segmented_img, objects=leaf_objects) # Measure absolute leaf angles (angle of linear regression line fit to each leaf object) Note: negative values # signify leaves to the left of the stem, positive values signify leaves to the right of the stem # labeled_img5 = pcv.morphology.segment_angle(segmented_img=segmented_img, objects=leaf_objects) # Measure leaf curvature in degrees # labeled_img6 = pcv.morphology.segment_tangent_angle(segmented_img=segmented_img, objects=leaf_objects, size=35) # Measure stem characteristics like stem angle and length # stem_img = pcv.morphology.analyze_stem(rgb_img=img, stem_objects=other_objects) # Remove unneeded observations (hack) _ = pcv.outputs.observations.pop("tips") _ = pcv.outputs.observations.pop("branch_pts") angles = pcv.outputs.observations["segment_insertion_angle"]["value"] remove_indices = [] for i, value in enumerate(angles): if value == "NA": remove_indices.append(i) remove_indices.sort(reverse=True) for i in remove_indices: _ = pcv.outputs.observations["segment_insertion_angle"]["value"].pop(i) # ## Save the results out to file for downsteam analysis pcv.print_results(filename=args.result)
# Inputs: # bin_img - Binary image data # size - Minimum object area size in pixels (must be an integer), and smaller objects will be filled ab_fill = pcv.fill(bin_img=ab, size=200) pcv.print_image(img=ab_fill, filename="upload/output_imgs/NoiseRe_img.jpg") # In[17]: # Closing filters out dark noise from an image. # Inputs: # gray_img - Grayscale or binary image data # kernel - Optional neighborhood, expressed as an array of 1's and 0's. If None (default), # uses cross-shaped structuring element. closed_ab = pcv.closing(gray_img=ab_fill) pcv.print_image(img=closed_ab, filename="upload/output_imgs/DarkNoise_img.jpg") # In[18]: # Apply mask (for VIS images, mask_color=white) rgb_img = masked masked2 = pcv.apply_mask(rgb_img, mask=ab_fill, mask_color='white') pcv.print_image(img=masked2, filename="upload/output_imgs/Masked_img.jpg") pcv.print_image(img=img, filename="upload/output_imgs/tag.jpg") # In[19]: import cv2 import numpy as np import glob
def main(): # Get options args = options() if args.debug: pcv.params.debug = args.debug # set debug mode if args.debugdir: pcv.params.debug_outdir = args.debugdir # set debug directory os.makedirs(args.debugdir, exist_ok=True) # pixel_resolution # mm # see pixel_resolution.xlsx for calibration curve for pixel to mm translation pixelresolution = 0.052 # The result file should exist if plantcv-workflow.py was run if os.path.exists(args.result): # Open the result file results = open(args.result, "r") # The result file would have image metadata in it from plantcv-workflow.py, read it into memory metadata = json.load(results) # Close the file results.close() # Delete the file, we will create new ones os.remove(args.result) plantbarcode = metadata['metadata']['plantbarcode']['value'] print(plantbarcode, metadata['metadata']['timestamp']['value'], sep=' - ') else: # If the file did not exist (for testing), initialize metadata as an empty string metadata = "{}" regpat = re.compile(args.regex) plantbarcode = re.search(regpat, args.image).groups()[0] # read images and create mask img, _, fn = pcv.readimage(args.image) imagename = os.path.splitext(fn)[0] # create mask # taf=filters.try_all_threshold(s_img) ## remove background s_img = pcv.rgb2gray_hsv(img, 's') min_s = filters.threshold_minimum(s_img) thresh_s = pcv.threshold.binary(s_img, min_s, 255, 'light') rm_bkgrd = pcv.fill_holes(thresh_s) ## low greenness thresh_s = pcv.threshold.binary(s_img, min_s + 15, 255, 'dark') # taf = filters.try_all_threshold(s_img) c = pcv.logical_xor(rm_bkgrd, thresh_s) cinv = pcv.invert(c) cinv_f = pcv.fill(cinv, 500) cinv_f_c = pcv.closing(cinv_f, np.ones((5, 5))) cinv_f_c_e = pcv.erode(cinv_f_c, 2, 1) ## high greenness a_img = pcv.rgb2gray_lab(img, channel='a') # taf = filters.try_all_threshold(a_img) t_a = filters.threshold_isodata(a_img) thresh_a = pcv.threshold.binary(a_img, t_a, 255, 'dark') thresh_a = pcv.closing(thresh_a, np.ones((5, 5))) thresh_a_f = pcv.fill(thresh_a, 500) ## combined mask lor = pcv.logical_or(cinv_f_c_e, thresh_a_f) close = pcv.closing(lor, np.ones((2, 2))) fill = pcv.fill(close, 800) erode = pcv.erode(fill, 3, 1) fill2 = pcv.fill(erode, 1200) # dilate = pcv.dilate(fill2,2,2) mask = fill2 final_mask = np.zeros_like(mask) # Compute greenness # split color channels b, g, r = cv2.split(img) # print green intensity # g_img = pcv.visualize.pseudocolor(g, cmap='Greens', background='white', min_value=0, max_value=255, mask=mask, axes=False) # convert color channels to int16 so we can add them (values will be greater than 255 which is max of current uint8 format) g = g.astype('uint16') r = r.astype('uint16') b = b.astype('uint16') denom = g + r + b # greenness index out_flt = np.zeros_like(denom, dtype='float32') # divide green by sum of channels to compute greenness index with values 0-1 gi = np.divide(g, denom, out=out_flt, where=np.logical_and(denom != 0, mask > 0)) # find objects c, h = pcv.find_objects(img, mask) rc, rh = pcv.roi.multi(img, coord=[(1300, 900), (1300, 2400)], radius=350) # Turn off debug temporarily, otherwise there will be a lot of plots pcv.params.debug = None # Loop over each region of interest i = 0 rc_i = rc[i] for i, rc_i in enumerate(rc): rh_i = rh[i] # Add ROI number to output. Before roi_objects so result has NA if no object. pcv.outputs.add_observation(variable='roi', trait='roi', method='roi', scale='int', datatype=int, value=i, label='#') roi_obj, hierarchy_obj, submask, obj_area = pcv.roi_objects( img, roi_contour=rc_i, roi_hierarchy=rh_i, object_contour=c, obj_hierarchy=h, roi_type='partial') if obj_area == 0: print('\t!!! No object found in ROI', str(i)) pcv.outputs.add_observation( variable='plantarea', trait='plant area in sq mm', method='observations.area*pixelresolution^2', scale=pixelresolution, datatype="<class 'float'>", value=0, label='sq mm') else: # Combine multiple objects # ple plant objects within an roi together plant_object, plant_mask = pcv.object_composition( img=img, contours=roi_obj, hierarchy=hierarchy_obj) final_mask = pcv.image_add(final_mask, plant_mask) # Save greenness for individual ROI grnindex = np.mean(gi[np.where(plant_mask > 0)]) pcv.outputs.add_observation( variable='greenness_index', trait='mean normalized greenness index', method='g/sum(b+g+r)', scale='[0,1]', datatype="<class 'float'>", value=float(grnindex), label='/1') # Analyze all colors hist = pcv.analyze_color(img, plant_mask, 'all') # Analyze the shape of the current plant shape_img = pcv.analyze_object(img, plant_object, plant_mask) plant_area = pcv.outputs.observations['area'][ 'value'] * pixelresolution**2 pcv.outputs.add_observation( variable='plantarea', trait='plant area in sq mm', method='observations.area*pixelresolution^2', scale=pixelresolution, datatype="<class 'float'>", value=plant_area, label='sq mm') # end if-else # At this point we have observations for one plant # We can write these out to a unique results file # Here I will name the results file with the ROI ID combined with the original result filename basename, ext = os.path.splitext(args.result) filename = basename + "-roi" + str(i) + ext # Save the existing metadata to the new file with open(filename, "w") as r: json.dump(metadata, r) pcv.print_results(filename=filename) # The results are saved, now clear out the observations so the next loop adds new ones for the next plant pcv.outputs.clear() if args.writeimg and obj_area != 0: imgdir = os.path.join(args.outdir, 'shape_images', plantbarcode) os.makedirs(imgdir, exist_ok=True) pcv.print_image( shape_img, os.path.join(imgdir, imagename + '-roi' + str(i) + '-shape.png')) imgdir = os.path.join(args.outdir, 'colorhist_images', plantbarcode) os.makedirs(imgdir, exist_ok=True) pcv.print_image( hist, os.path.join(imgdir, imagename + '-roi' + str(i) + '-colorhist.png')) # end roi loop if args.writeimg: # save grnness image of entire tray imgdir = os.path.join(args.outdir, 'pseudocolor_images', plantbarcode) os.makedirs(imgdir, exist_ok=True) gi_img = pcv.visualize.pseudocolor(gi, obj=None, mask=final_mask, cmap='viridis', axes=False, min_value=0.3, max_value=0.6, background='black', obj_padding=0) gi_img = add_scalebar(gi_img, pixelresolution=pixelresolution, barwidth=20, barlocation='lower left') gi_img.set_size_inches(6, 6, forward=False) gi_img.savefig(os.path.join(imgdir, imagename + '-greenness.png'), bbox_inches='tight') gi_img.clf()
def root(): uploaded_file = st.file_uploader("Choose an image...", type="jpg") if uploaded_file is not None: inp = Image.open(uploaded_file) inp.save('input.jpg') img, path, filename = pcv.readimage(filename='input.jpg') image = Image.open('input.jpg') st.image(image, caption='Original Image',use_column_width=True) # Convert RGB to HSV and extract the saturation channel # Inputs: # rgb_image - RGB image data # channel - Split by 'h' (hue), 's' (saturation), or 'v' (value) channel s = pcv.rgb2gray_hsv(rgb_img=img, channel='s') pcv.print_image(s, "plant/rgbtohsv.png") image = Image.open('plant/rgbtohsv.png') st.image(image, caption='RGB to HSV', use_column_width=True) s_thresh = pcv.threshold.binary(gray_img=s, threshold=85, max_value=255, object_type='light') pcv.print_image(s_thresh, "plant/binary_threshold.png") image = Image.open('plant/binary_threshold.png') st.image(image, caption='Binary Threshold',use_column_width=True) # Median Blur to clean noise # Inputs: # gray_img - Grayscale image data # ksize - Kernel size (integer or tuple), (ksize, ksize) box if integer input, # (n, m) box if tuple input s_mblur = pcv.median_blur(gray_img=s_thresh, ksize=5) pcv.print_image(s_mblur, "plant/Median_blur.png") image = Image.open('plant/Median_blur.png') st.image(image, caption='Median Blur',use_column_width=True) # An alternative to using median_blur is gaussian_blur, which applies # a gaussian blur filter to the image. Depending on the image, one # technique may be more effective than others. # Inputs: # img - RGB or grayscale image data # ksize - Tuple of kernel size # sigma_x - Standard deviation in X direction; if 0 (default), # calculated from kernel size # sigma_y - Standard deviation in Y direction; if sigmaY is # None (default), sigmaY is taken to equal sigmaX gaussian_img = pcv.gaussian_blur(img=s_thresh, ksize=(5, 5), sigma_x=0, sigma_y=None) # Convert RGB to LAB and extract the blue channel ('b') # Input: # rgb_img - RGB image data # channel- Split by 'l' (lightness), 'a' (green-magenta), or 'b' (blue-yellow) channel b = pcv.rgb2gray_lab(rgb_img=img, channel='b') b_thresh = pcv.threshold.binary(gray_img=b, threshold=160, max_value=255, object_type='light') # Join the threshold saturation and blue-yellow images with a logical or operation # Inputs: # bin_img1 - Binary image data to be compared to bin_img2 # bin_img2 - Binary image data to be compared to bin_img1 bs = pcv.logical_or(bin_img1=s_mblur, bin_img2=b_thresh) pcv.print_image(bs, "plant/threshold comparison.png") image = Image.open('plant/threshold comparison.png') st.image(image, caption='Threshold Comparision',use_column_width=True) # Appy Mask (for VIS images, mask_color='white') # Inputs: # img - RGB or grayscale image data # mask - Binary mask image data # mask_color - 'white' or 'black' masked = pcv.apply_mask(img=img, mask=bs, mask_color='white') pcv.print_image(masked, "plant/Apply_mask.png") image = Image.open('plant/Apply_mask.png') st.image(image, caption='Applied Mask',use_column_width=True) # Convert RGB to LAB and extract the Green-Magenta and Blue-Yellow channels masked_a = pcv.rgb2gray_lab(rgb_img=masked, channel='a') masked_b = pcv.rgb2gray_lab(rgb_img=masked, channel='b') # Threshold the green-magenta and blue images maskeda_thresh = pcv.threshold.binary(gray_img=masked_a, threshold=115, max_value=255, object_type='dark') maskeda_thresh1 = pcv.threshold.binary(gray_img=masked_a, threshold=135,max_value=255, object_type='light') maskedb_thresh = pcv.threshold.binary(gray_img=masked_b, threshold=128, max_value=255, object_type='light') pcv.print_image( maskeda_thresh, "plant/maskeda_thresh.png") pcv.print_image(maskeda_thresh1, "plant/maskeda_thresh1.png") pcv.print_image(maskedb_thresh, "plant/maskedb_thresh1.png") image = Image.open('plant/maskeda_thresh.png') st.image(image, caption='Threshold green-magneta and blue image',use_column_width=True) # Join the thresholded saturation and blue-yellow images (OR) ab1 = pcv.logical_or(bin_img1=maskeda_thresh, bin_img2=maskedb_thresh) ab = pcv.logical_or(bin_img1=maskeda_thresh1, bin_img2=ab1) # Opening filters out bright noise from an image. # Inputs: # gray_img - Grayscale or binary image data # kernel - Optional neighborhood, expressed as an array of 1's and 0's. If None (default), # uses cross-shaped structuring element. opened_ab = pcv.opening(gray_img=ab) # Depending on the situation it might be useful to use the # exclusive or (pcv.logical_xor) function. # Inputs: # bin_img1 - Binary image data to be compared to bin_img2 # bin_img2 - Binary image data to be compared to bin_img1 xor_img = pcv.logical_xor(bin_img1=maskeda_thresh, bin_img2=maskedb_thresh) # Fill small objects (reduce image noise) # Inputs: # bin_img - Binary image data # size - Minimum object area size in pixels (must be an integer), and smaller objects will be filled ab_fill = pcv.fill(bin_img=ab, size=200) # Closing filters out dark noise from an image. # Inputs: # gray_img - Grayscale or binary image data # kernel - Optional neighborhood, expressed as an array of 1's and 0's. If None (default), # uses cross-shaped structuring element. closed_ab = pcv.closing(gray_img=ab_fill) # Apply mask (for VIS images, mask_color=white) masked2 = pcv.apply_mask(img=masked, mask=ab_fill, mask_color='white') # Identify objects # Inputs: # img - RGB or grayscale image data for plotting # mask - Binary mask used for detecting contours id_objects, obj_hierarchy = pcv.find_objects(img=masked2, mask=ab_fill) # Define the region of interest (ROI) # Inputs: # img - RGB or grayscale image to plot the ROI on # x - The x-coordinate of the upper left corner of the rectangle # y - The y-coordinate of the upper left corner of the rectangle # h - The height of the rectangle # w - The width of the rectangle roi1, roi_hierarchy= pcv.roi.rectangle(img=masked2, x=50, y=50, h=100, w=100) # Decide which objects to keep # Inputs: # img = img to display kept objects # roi_contour = contour of roi, output from any ROI function # roi_hierarchy = contour of roi, output from any ROI function # object_contour = contours of objects, output from pcv.find_objects function # obj_hierarchy = hierarchy of objects, output from pcv.find_objects function # roi_type = 'partial' (default, for partially inside the ROI), 'cutto', or # 'largest' (keep only largest contour) roi_objects, hierarchy3, kept_mask, obj_area = pcv.roi_objects(img=img, roi_contour=roi1, roi_hierarchy=roi_hierarchy, object_contour=id_objects, obj_hierarchy=obj_hierarchy, roi_type='partial') # Object combine kept objects # Inputs: # img - RGB or grayscale image data for plotting # contours - Contour list # hierarchy - Contour hierarchy array obj, mask = pcv.object_composition(img=img, contours=roi_objects, hierarchy=hierarchy3) ############### Analysis ################ # Find shape properties, data gets stored to an Outputs class automatically # Inputs: # img - RGB or grayscale image data # obj- Single or grouped contour object # mask - Binary image mask to use as mask for moments analysis analysis_image = pcv.analyze_object(img=img, obj=obj, mask=mask) pcv.print_image(analysis_image, "plant/analysis_image.png") image = Image.open('plant/analysis_image.png') st.image(image, caption='Analysis_image',use_column_width=True) # Shape properties relative to user boundary line (optional) # Inputs: # img - RGB or grayscale image data # obj - Single or grouped contour object # mask - Binary mask of selected contours # line_position - Position of boundary line (a value of 0 would draw a line # through the bottom of the image) boundary_image2 = pcv.analyze_bound_horizontal(img=img, obj=obj, mask=mask, line_position=370) pcv.print_image(boundary_image2, "plant/boundary_image2.png") image = Image.open('plant/boundary_image2.png') st.image(image, caption='Boundary Image',use_column_width=True) # Determine color properties: Histograms, Color Slices and Pseudocolored Images, output color analyzed images (optional) # Inputs: # rgb_img - RGB image data # mask - Binary mask of selected contours # hist_plot_type - None (default), 'all', 'rgb', 'lab', or 'hsv' # This is the data to be printed to the SVG histogram file color_histogram = pcv.analyze_color(rgb_img=img, mask=kept_mask, hist_plot_type='all') # Print the histogram out to save it pcv.print_image(img=color_histogram, filename="plant/vis_tutorial_color_hist.jpg") image = Image.open('plant/vis_tutorial_color_hist.jpg') st.image(image, caption='Color Histogram',use_column_width=True) # Divide plant object into twenty equidistant bins and assign pseudolandmark points based upon their # actual (not scaled) position. Once this data is scaled this approach may provide some information # regarding shape independent of size. # Inputs: # img - RGB or grayscale image data # obj - Single or grouped contour object # mask - Binary mask of selected contours top_x, bottom_x, center_v_x = pcv.x_axis_pseudolandmarks(img=img, obj=obj, mask=mask) top_y, bottom_y, center_v_y = pcv.y_axis_pseudolandmarks(img=img, obj=obj, mask=mask) # The print_results function will take the measurements stored when running any (or all) of these functions, format, # and print an output text file for data analysis. The Outputs class stores data whenever any of the following functions # are ran: analyze_bound_horizontal, analyze_bound_vertical, analyze_color, analyze_nir_intensity, analyze_object, # fluor_fvfm, report_size_marker_area, watershed. If no functions have been run, it will print an empty text file pcv.print_results(filename='vis_tutorial_results.txt')
def segment_on_dt(a, img): #logAnd = plantcv.logical_and(a, img) #cv2.imshow('logical', logAnd) #cv2.waitKey(0) gauss = pcv.gaussian_blur(a, (5, 5), 0, 0) edges = pcv.canny_edge_detect(a, mask=None, sigma=2, low_thresh=15, high_thresh=40, thickness=2, mask_color=None, use_quantiles=False) cv2.imshow('canny', edges) cv2.waitKey(0) canny_dilate = cv2.dilate(edges, None, iterations=1) cv2.imshow("canny dilate", canny_dilate) cv2.waitKey(0) dilate = cv2.dilate(img, None, iterations=1) border = dilate - cv2.erode(dilate, None, iterations=2) cv2.imshow('border', border) cv2.waitKey(0) border = cv2.bitwise_or(border, canny_dilate, mask=dilate) cv2.imshow("border", border) cv2.waitKey(0) dt = cv2.distanceTransform(img, 1, 5) cv2.imshow('dist trans', dt) cv2.waitKey(0) dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8) cv2.imshow('minmax', dt) cv2.waitKey(0) _, dt = cv2.threshold(dt, 45, 255, cv2.THRESH_BINARY) cv2.imshow('thresh', dt) cv2.waitKey(0) seg = cv2.subtract(dt, border) cv2.imshow('thresh - border', seg) cv2.waitKey(0) seg1 = cv2.erode(seg, None, iterations=2) cv2.imshow('erode', seg1) cv2.waitKey(0) fill_image = pcv.closing(seg1) cv2.imshow('Closing', fill_image) cv2.waitKey(0) lbl, ncc = label(fill_image) lbl = lbl * (255 / (ncc + 1)) # Completing the markers now. lbl[border == 255] = 255 print(ncc) lbl = lbl.astype(numpy.int32) #cv2.watershed(a, lbl) lbl[lbl == -1] = 0 lbl = lbl.astype(numpy.uint8) return 255 - lbl
def main(): # Get options args = options() # plt.rcParams["font.family"] = "Arial" # All text is Arial # pixel_resolution cppc.pixelresolution = 0.052 #mm # see pixel_resolution.xlsx for calibration curve for pixel to mm translation pcv.params.text_size = 12 pcv.params.text_thickness = 12 if args.debug: pcv.params.debug = args.debug # set debug mode if args.debugdir: pcv.params.debug_outdir = args.debugdir # set debug directory os.makedirs(args.debugdir, exist_ok=True) args = cppc.roi.copy_metadata(args) # read images and create mask img, _, fn = pcv.readimage(args.image) args.imagename = os.path.splitext(fn)[0] # cps=pcv.visualize.colorspaces(img) # create mask if args.pdfs: #if not provided in run_workflows.sh then will be False print('naive bayes classification') # Classify each pixel as plant or background (background and system components) img_blur = pcv.gaussian_blur(img, (7, 7)) masks = pcv.naive_bayes_classifier(rgb_img=img_blur, pdf_file=args.pdfs) mask = masks['Plant'] # save masks colored_img = pcv.visualize.colorize_masks( masks=[masks['Plant'], masks['Background'], masks['Blue']], colors=['green', 'black', 'blue']) # Print out the colorized figure that got created imgdir = os.path.join(args.outdir, 'bayesmask_images') os.makedirs(imgdir, exist_ok=True) pcv.print_image( colored_img, os.path.join(imgdir, args.imagename + '-bayesmask.png')) else: print('\nthreshold masking') # tray mask # _, rm, _, _ = pcv.rectangle_mask(img, (425,350), (2100,3050),'white') # img_tray = pcv.apply_mask(img, rm, 'black') # dark green # imgt_h = pcv.rgb2gray_hsv(img,'h') mask1, img1 = pcv.threshold.custom_range(img, [15, 0, 0], [60, 255, 255], 'hsv') mask1 = pcv.fill(mask1, 200) mask1 = pcv.closing(mask1, pcv.get_kernel((5, 5), 'rectangle')) mask = mask1 # img1 = pcv.apply_mask(img, mask1, 'black') # # remove faint algae # img1_a = pcv.rgb2gray_lab(img1,'b') # # img1_b = pcv.rgb2gray_lab(img1,'b') # th = filters.threshold_otsu(img1_a) # algaemask = pcv.threshold.binary(img1_a,th,255,'light') # # bmask, _ = pcv.threshold.custom_range(img1,[0,0,100],[120,120,255], 'RGB') # img2 = pcv.apply_mask(img1,algaemask,'black') # mask = pcv.rgb2gray(img2) # mask[mask > 0] = 255 # pcv.plot_image(mask) # find objects based on threshold mask c, h = pcv.find_objects(img, mask) # setup roi based on pot locations rc, rh = pcv.roi.multi(img, coord=[(1250, 1000), (1250, 2300)], radius=300) # Turn off debug temporarily if activated, otherwise there will be a lot of plots pcv.params.debug = None # Loop over each region of interest # i=0 # rc_i = rc[i] # rh_i = rh[i] final_mask = cppc.roi.iterate_rois(img, c, h, rc, rh, args=args, masked=True, gi=True, shape=True, hist=True, hue=True)
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