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 process_pot(self, pot_image): device = 0 # debug=None updated_pot_image = self.threshold_green(pot_image) # plt.imshow(updated_pot_image) # plt.show() device, a = pcv.rgb2gray_lab(updated_pot_image, 'a', device) device, img_binary = pcv.binary_threshold(a, 127, 255, 'dark', device, None) # plt.imshow(img_binary) # plt.show() mask = np.copy(img_binary) device, fill_image = pcv.fill(img_binary, mask, 50, device) device, dilated = pcv.dilate(fill_image, 1, 1, device) device, id_objects, obj_hierarchy = pcv.find_objects( updated_pot_image, updated_pot_image, device) device, roi1, roi_hierarchy = pcv.define_roi(updated_pot_image, 'rectangle', device, None, 'default', debug, False) device, roi_objects, hierarchy3, kept_mask, obj_area = pcv.roi_objects( updated_pot_image, 'partial', roi1, roi_hierarchy, id_objects, obj_hierarchy, device, debug) device, obj, mask = pcv.object_composition(updated_pot_image, roi_objects, hierarchy3, device, debug) device, shape_header, shape_data, shape_img = pcv.analyze_object( updated_pot_image, "Example1", obj, mask, device, debug, False) print(shape_data[1])
def main(path, imagename): args = {'names': 'names.txt', 'outdir': './output-images'} #Read image img1, path, filename = pcv.readimage(path + imagename, "native") #pcv.params.debug=args['debug'] #img1 = pcv.white_balance(img,roi=(400,800,200,200)) #img1 = cv2.resize(img1,(4000,2000)) shift1 = pcv.shift_img(img1, 10, 'top') img1 = shift1 a = pcv.rgb2gray_lab(img1, 'a') img_binary = pcv.threshold.binary(a, 120, 255, 'dark') fill_image = pcv.fill(img_binary, 10) dilated = pcv.dilate(fill_image, 1, 1) id_objects, obj_hierarchy = pcv.find_objects(img1, dilated) roi_contour, roi_hierarchy = pcv.roi.rectangle(4000, 2000, -2000, -4000, img1) #print(roi_contour) roi_objects, roi_obj_hierarchy, kept_mask, obj_area = pcv.roi_objects( img1, 'partial', roi_contour, roi_hierarchy, id_objects, obj_hierarchy) clusters_i, contours, hierarchies = pcv.cluster_contours( img1, roi_objects, roi_obj_hierarchy, 1, 4) ''' pcv.params.debug = "print"''' out = args['outdir'] names = args['names'] output_path = pcv.cluster_contour_splitimg(img1, clusters_i, contours, hierarchies, out, file=filename, filenames=names)
def get_plant_object(image, mask): ''' Use the mask information to filter out the plant object ''' id_objects, obj_heirachy = pcv.find_objects(image, mask) return id_objects, obj_heirachy
def segment_skeleton(skel_img, mask=None): """ Segment a skeleton image into pieces Inputs: skel_img = Skeletonized image mask = (Optional) binary mask for debugging. If provided, debug image will be overlaid on the mask. Returns: segmented_img = Segmented debugging image objects = list of contours hierarchy = contour hierarchy list :param skel_img: numpy.ndarray :param mask: numpy.ndarray :return segmented_img: numpy.ndarray :return segment_objects: list "return segment_hierarchies: numpy.ndarray """ # Store debug debug = params.debug params.debug = None # Find branch points bp = find_branch_pts(skel_img) bp = dilate(bp, 3, 1) # Subtract from the skeleton so that leaves are no longer connected segments = image_subtract(skel_img, bp) # Gather contours of leaves segment_objects, _ = find_objects(segments, segments) # Color each segment a different color rand_color = color_palette(len(segment_objects)) if mask is None: segmented_img = skel_img.copy() else: segmented_img = mask.copy() segmented_img = cv2.cvtColor(segmented_img, cv2.COLOR_GRAY2RGB) for i, cnt in enumerate(segment_objects): cv2.drawContours(segmented_img, segment_objects, i, rand_color[i], params.line_thickness, lineType=8) # Reset debug mode params.debug = debug # Auto-increment device params.device += 1 if params.debug == 'print': print_image(segmented_img, os.path.join(params.debug_outdir, str(params.device) + '_segmented.png')) elif params.debug == 'plot': plot_image(segmented_img) return segmented_img, segment_objects
def main(): # Get options args = options() debug = args.debug # Read image img, path, filename = pcv.readimage(args.image) # Pipeline step device = 0 device, corrected_img = pcv.white_balance(device, img, debug, (500, 1000, 500, 500)) img = corrected_img device, img_gray_sat = pcv.rgb2gray_lab(img, 'a', device, debug) device, img_binary = pcv.binary_threshold(img_gray_sat, 120, 255, 'dark', device, debug) mask = np.copy(img_binary) device, fill_image = pcv.fill(img_binary, mask, 300, device, debug) device, id_objects, obj_hierarchy = pcv.find_objects( img, fill_image, device, debug) device, roi, roi_hierarchy = pcv.define_roi(img, 'rectangle', device, None, 'default', debug, True, 1800, 1600, -1500, -500) device, roi_objects, roi_obj_hierarchy, kept_mask, obj_area = pcv.roi_objects( img, 'partial', roi, roi_hierarchy, id_objects, obj_hierarchy, device, debug) device, obj, mask = pcv.object_composition(img, roi_objects, roi_obj_hierarchy, device, debug) outfile = os.path.join(args.outdir, filename) device, color_header, color_data, color_img = pcv.analyze_color( img, img, mask, 256, device, debug, None, 'v', 'img', 300, outfile) device, shape_header, shape_data, shape_img = pcv.analyze_object( img, "img", obj, mask, device, debug, outfile) shapepath = outfile[:-4] + '_shapes.jpg' shapepic = cv2.imread(shapepath) plantsize = "The plant is " + str(np.sum(mask)) + " pixels large" cv2.putText(shapepic, plantsize, (500, 500), cv2.FONT_HERSHEY_SIMPLEX, 5, (0, 255, 0), 10) pcv.print_image(shapepic, outfile[:-4] + '-out_shapes.jpg')
def main(): #Menangkap gambar IP Cam dengan opencv ##Ngosek, koding e wis ono tapi carane ngonek ning gcloud rung reti wkwk #Mengambil gambar yang sudah didapatkan dari opencv untuk diproses di plantcv path = 'Image test\capture (1).jpg' gmbTumbuhanRaw, path, filename = pcv.readimage(path, mode='native') #benarkan gambar yang miring koreksiRot = pcv.rotate(gmbTumbuhanRaw, 2, True) gmbKoreksi = koreksiRot pcv.print_image(gmbKoreksi, 'Image test\Hasil\gambar_koreksi.jpg') #Mengatur white balance dari gambar #usahakan gambar rata (tanpa bayangan dari manapun!)! #GANTI nilai dari region of intrest (roi) berdasarkan ukuran gambar!! koreksiWhiteBal = pcv.white_balance(gmbTumbuhanRaw, roi=(2, 100, 1104, 1200)) pcv.print_image(koreksiWhiteBal, 'Image test\Hasil\koreksi_white_bal.jpg') #mengubah kontras gambar agar berbeda dengan warna background #tips: latar jangan sama hijaunya kontrasBG = pcv.rgb2gray_lab(koreksiWhiteBal, channel='a') pcv.print_image(kontrasBG, 'Image test\Hasil\koreksi_kontras.jpg') #binary threshol gambar #sesuaikan thresholdnya binthres = pcv.threshold.binary(gray_img=kontrasBG, threshold=115, max_value=255, object_type='dark') #hilangkan noise dengan fill noise resiksitik = pcv.fill(binthres, size=10) pcv.print_image(resiksitik, 'Image test\Hasil\\noiseFill.jpg') #haluskan dengan dilate dilasi = pcv.dilate(resiksitik, ksize=12, i=1) #ambil objek dan set besar roi id_objek, hirarki_objek = pcv.find_objects(gmbTumbuhanRaw, mask=dilasi) roi_contour, roi_hierarchy = pcv.roi.rectangle(img=gmbKoreksi, x=20, y=96, h=1100, w=680) #keluarkan gambar (untuk debug aja sih) roicontour = cv2.drawContours(gmbKoreksi, roi_contour, -1, (0, 0, 255), 3) pcv.print_image(roicontour, 'Image test\Hasil\\roicontour.jpg') """
def read_true_positive(true_positive_file): class args: #image = "C:\\Users\\RensD\\OneDrive\\studie\\Master\\The_big_project\\top_perspective\\0214_2018-03-07 08.55 - 26_true_positive.png" image = true_positive_file outdir = "C:\\Users\\RensD\\OneDrive\\studie\\Master\\The_big_project\\top_perspective\\output" debug = "None" result = "results.txt" # Get options pcv.params.debug = args.debug #set debug mode pcv.params.debug_outdir = args.outdir #set output directory # Read image (readimage mode defaults to native but if image is RGBA then specify mode='rgb') # Inputs: # filename - Image file to be read in # mode - Return mode of image; either 'native' (default), 'rgb', 'gray', or 'csv' img, path, filename = pcv.readimage(filename=args.image, mode='rgb') s = pcv.rgb2gray_hsv(rgb_img=img, channel='s') mask, masked_image = pcv.threshold.custom_range(rgb_img=s, lower_thresh=[10], upper_thresh=[255], channel='gray') masked = pcv.apply_mask(rgb_img=img, mask=mask, mask_color='white') #new_im = Image.fromarray(mask) #name = "positive_test.png" #Recognizing objects id_objects, obj_hierarchy = pcv.find_objects(masked, mask) roi1, roi_hierarchy = pcv.roi.rectangle(img=masked, x=0, y=0, h=960, w=1280) # Currently hardcoded with HiddenPrints(): 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=roi_type) obj, mask = pcv.object_composition(img=img, contours=roi_objects, hierarchy=hierarchy3) #new_im.save(name) return (mask)
def read_dot(self, imageread): img1 = imageread device, img1gray = pcv.rgb2gray(img1, 0) img1hsv = cv2.cvtColor(img1, cv2.COLOR_BGR2HSV) dev, img_binary = pcv.binary_threshold(img1gray, 1, 255, 'light', 0) device, id_objects, obj_hierarchy = pcv.find_objects( img1, img_binary, 0) device, roi, roi_hierarchy = pcv.define_roi(img1, shape="rectangle", device=device, roi_input="default", adjust=False, x_adj=600, y_adj=600, w_adj=1200, h_adj=1200) device, roi_objects, roi_obj_hierarchy, kept_mask, obj_area = pcv.roi_objects( img1, "partial", roi, roi_hierarchy, id_objects, obj_hierarchy, device) device, clusters_i, contours, obj_hierarchy = pcv.cluster_contours( device=device, img=img1, roi_objects=roi_objects, roi_obj_hierarchy=roi_obj_hierarchy, nrow=1, ncol=int(3)) dotQ = list() obj_hierarchy = obj_hierarchy[0] for clusters1 in clusters_i: for contourtocheck in clusters1: if not obj_hierarchy[contourtocheck][2] == -1: if not cv2.contourArea( contours[obj_hierarchy[contourtocheck][2]]) >= 20: obj_hierarchy[contourtocheck][2] = -1 counter = 0 for foundcontour in clusters_i: hierarchycontours = [obj_hierarchy[j][2] for j in foundcontour] if not all([bool(j == -1) for j in hierarchycontours]): dotQ.append(True) else: dotQ.append(False) return dotQ
def find_branch_pts(skel_img, mask=None): """ The branching algorithm was inspired by Jean-Patrick Pommier: https://gist.github.com/jeanpat/5712699 Inputs: skel_img = Skeletonized image mask = (Optional) binary mask for debugging. If provided, debug image will be overlaid on the mask. Returns: branch_pts_img = Image with just branch points, rest 0 :param skel_img: numpy.ndarray :return branch_pts_img: numpy.ndarray """ # Store debug debug = params.debug params.debug = None # In a kernel: 1 values line up with 255s, -1s line up with 0s, and 0s correspond to dont care # T like branch points t1 = np.array([[-1, 1, -1], [ 1, 1, 1], [-1, -1, -1]]) t2 = np.array([[ 1, -1, 1], [-1, 1, -1], [ 1, -1, -1]]) t3 = np.rot90(t1) t4 = np.rot90(t2) t5 = np.rot90(t3) t6 = np.rot90(t4) t7 = np.rot90(t5) t8 = np.rot90(t6) # Y like branch points y1 = np.array([[ 1, -1, 1], [ 0, 1, 0], [ 0, 1, 0]]) y2 = np.array([[-1, 1, -1], [ 1, 1, 0], [-1, 0, 1]]) y3 = np.rot90(y1) y4 = np.rot90(y2) y5 = np.rot90(y3) y6 = np.rot90(y4) y7 = np.rot90(y5) y8 = np.rot90(y6) kernels = [t1, t2, t3, t4, t5, t6, t7, t8, y1, y2, y3, y4, y5, y6, y7, y8] branch_pts_img = np.zeros(skel_img.shape[:2], dtype=int) # Store branch points for kernel in kernels: branch_pts_img = np.logical_or(cv2.morphologyEx(skel_img, op=cv2.MORPH_HITMISS, kernel=kernel, borderType=cv2.BORDER_CONSTANT, borderValue=0), branch_pts_img) # Switch type to uint8 rather than bool branch_pts_img = branch_pts_img.astype(np.uint8) * 255 # Make debugging image if mask is None: dilated_skel = dilate(skel_img, params.line_thickness, 1) branch_plot = cv2.cvtColor(dilated_skel, cv2.COLOR_GRAY2RGB) else: # Make debugging image on mask mask_copy = mask.copy() branch_plot = cv2.cvtColor(mask_copy, cv2.COLOR_GRAY2RGB) skel_obj, skel_hier = find_objects(skel_img, skel_img) cv2.drawContours(branch_plot, skel_obj, -1, (150, 150, 150), params.line_thickness, lineType=8, hierarchy=skel_hier) branch_objects, _ = find_objects(branch_pts_img, branch_pts_img) for i in branch_objects: x, y = i.ravel()[:2] cv2.circle(branch_plot, (x, y), params.line_thickness, (255, 0, 255), -1) # Reset debug mode params.debug = debug # Auto-increment device params.device += 1 if params.debug == 'print': print_image(branch_plot, os.path.join(params.debug_outdir, str(params.device) + '_skeleton_branches.png')) elif params.debug == 'plot': plot_image(branch_plot) return branch_pts_img
#Setting threshold continued b_cnt = pcv.threshold.binary(gray_img=b, threshold=135, max_value=255, object_type='light') # In[112]: #Join the blue and yellow binary images bs = pcv.logical_and(bin_img1=s_mblur, bin_img2=b_cnt) masked = pcv.apply_mask(img=img1, mask=bs, mask_color='white') #identify objects obj2 = id_objects, obj_hierarchy = pcv.find_objects(img=masked, mask=bs) #Define Range of Intrest # 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=img1, x=75, y=60, h=20, w=20) # In[113]: # Decide which objects to keep # Inputs:
def main(): # Create input arguments object args = options() # Set debug mode pcv.params.debug = args.debug # Open a single image img, imgpath, imgname = pcv.readimage(filename=args.image) # Visualize colorspaces all_cs = pcv.visualize.colorspaces(rgb_img=img) # Extract the Blue-Yellow ("b") channel from the LAB colorspace gray_img = pcv.rgb2gray_lab(rgb_img=img, channel="b") # Plot a histogram of pixel values for the Blue-Yellow ("b") channel. hist_plot = pcv.visualize.histogram(gray_img=gray_img) # Apply a binary threshold to the Blue-Yellow ("b") grayscale image. thresh_img = pcv.threshold.binary(gray_img=gray_img, threshold=140, max_value=255, object_type="light") # Apply a dilation with a 5x5 kernel and 3 iterations dil_img = pcv.dilate(gray_img=thresh_img, ksize=5, i=3) # Fill in small holes in the leaves closed_img = pcv.fill_holes(bin_img=dil_img) # Erode the plant pixels using a 5x5 kernel and 3 iterations er_img = pcv.erode(gray_img=closed_img, ksize=5, i=3) # Apply a Gaussian blur with a 5 x 5 kernel. blur_img = pcv.gaussian_blur(img=er_img, ksize=(5, 5)) # Set pixel values less than 255 to 0 blur_img[np.where(blur_img < 255)] = 0 # Fill/remove objects less than 300 pixels in area cleaned = pcv.fill(bin_img=blur_img, size=300) # Create a circular ROI roi, roi_str = pcv.roi.circle(img=img, x=1725, y=1155, r=400) # Identify objects in the binary image cnts, cnts_str = pcv.find_objects(img=img, mask=cleaned) # Filter objects by region of interest plant_cnt, plant_str, plant_mask, plant_area = pcv.roi_objects( img=img, roi_contour=roi, roi_hierarchy=roi_str, object_contour=cnts, obj_hierarchy=cnts_str) # Combine objects into one plant, mask = pcv.object_composition(img=img, contours=plant_cnt, hierarchy=plant_str) # Measure size and shape properties shape_img = pcv.analyze_object(img=img, obj=plant, mask=mask) if args.writeimg: pcv.print_image(img=shape_img, filename=os.path.join(args.outdir, "shapes_" + imgname)) # Analyze color properties color_img = pcv.analyze_color(rgb_img=img, mask=mask, hist_plot_type="hsv") if args.writeimg: pcv.print_image(img=color_img, filename=os.path.join(args.outdir, "histogram_" + imgname)) # Save the measurements to a file pcv.print_results(filename=args.result)
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_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
green = np.zeros_like(img, np.uint8) green[imask] = img[imask] ## save cv2.imwrite("upload/output_imgs/Crop.png", green) i = i + 1 filename = pcv.readimage(filename="upload/output_imgs/Crop.png") # In[20]: # 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) # In[21]: # Define the region of interest (ROI) # Inputs: # img - RGB or grayscale image to plot the ROI on # x - The x-coordiate 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=200, y=300, h=200,
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 image_avg(fundf): # dn't understand why import suddently needs to be inside function # import cv2 as cv2 # import numpy as np # import pandas as pd # import os # from matplotlib import pyplot as plt # from skimage import filters # from skimage import morphology # from skimage import segmentation # Predefine some variables global c, h, roi_c, roi_h, ilegend, mask_Fm, fn_Fm # Get the filename for minimum and maximum fluoresence fn_min = fundf.query('frame == "Fo" or frame == "Fp"').filename.values[0] fn_max = fundf.query('frame == "Fm" or frame == "Fmp"').filename.values[0] # Get the parameter name that links these 2 frames param_name = fundf['parameter'].iloc[0] # Create a new output filename that combines existing filename with parameter outfn = os.path.splitext(os.path.basename(fn_max))[0] outfn_split = outfn.split('-') # outfn_split[2] = datetime.strptime(fundf.jobdate.values[0],'%Y-%m-%d').strftime('%Y%m%d') outfn_split[2] = fundf.jobdate.dt.strftime('%Y%m%d').values[0] basefn = "-".join(outfn_split[0:-1]) outfn_split[-1] = param_name outfn = "-".join(outfn_split) print(outfn) # Make some directories based on sample id to keep output organized plantbarcode = outfn_split[0] fmaxdir = os.path.join(fluordir, plantbarcode) os.makedirs(fmaxdir, exist_ok=True) # If debug mode is 'print', create a specific debug dir for each pim file if pcv.params.debug == 'print': debug_outdir = os.path.join(debugdir, outfn) os.makedirs(debug_outdir, exist_ok=True) pcv.params.debug_outdir = debug_outdir # read images and create mask from max fluorescence # read image as is. only gray values in PSII images imgmin, _, _ = pcv.readimage(fn_min) img, _, _ = pcv.readimage(fn_max) fdark = np.zeros_like(img) out_flt = fdark.astype('float32') # <- needs to be float32 for imwrite if param_name == 'FvFm': # save max fluorescence filename fn_Fm = fn_max # create mask # #create black mask over lower half of image to threshold upper plant only # img_half, _, _, _ = pcv.rectangle_mask(img, p1=(0,321), p2=(480,640)) # # mask1 = pcv.threshold.otsu(img_half,255) # algaethresh = filters.threshold_otsu(image=img_half) # mask0 = pcv.threshold.binary(img_half, algaethresh, 255, 'light') # # create black mask over upper half of image to threshold lower plant only # img_half, _, _, _ = pcv.rectangle_mask(img, p1=(0, 0), p2=(480, 319), color='black') # # mask0 = pcv.threshold.otsu(img_half,255) # algaethresh = filters.threshold_otsu(image=img_half) # mask1 = pcv.threshold.binary(img_half, algaethresh, 255, 'light') # mask = pcv.logical_xor(mask0, mask1) # # mask = pcv.dilate(mask, 2, 1) # mask = pcv.fill(mask, 350) # mask = pcv.erode(mask, 2, 2) # mask = pcv.erode(mask, 2, 1) # mask = pcv.fill(mask, 100) # otsuT = filters.threshold_otsu(img) # # sigma=(k-1)/6. This is because the length for 99 percentile of gaussian pdf is 6sigma. # k = int(2 * np.ceil(3 * otsuT) + 1) # gb = pcv.gaussian_blur(img, ksize = (k,k), sigma_x = otsuT) # mask = img >= gb + 10 # pcv.plot_image(mask) # local_otsu = filters.rank.otsu(img, pcv.get_kernel((9,9), 'rectangle'))#morphology.disk(2)) # thresh_image = img >= local_otsu #_------> elevation_map = filters.sobel(img) # pcv.plot_image(elevation_map) thresh = filters.threshold_otsu(image=img) # thresh = 50 markers = np.zeros_like(img, dtype='uint8') markers[img > thresh + 8] = 2 markers[img <= thresh + 8] = 1 # pcv.plot_image(markers,cmap=plt.cm.nipy_spectral) mask = segmentation.watershed(elevation_map, markers) mask = mask.astype(np.uint8) # pcv.plot_image(mask) mask[mask == 1] = 0 mask[mask == 2] = 1 # pcv.plot_image(mask, cmap=plt.cm.nipy_spectral) # mask = pcv.erode(mask, 2, 1) mask = pcv.fill(mask, 100) # pcv.plot_image(mask, cmap=plt.cm.nipy_spectral) # <----------- roi_c, roi_h = pcv.roi.multi(img, coord=(250, 200), radius=70, spacing=(0, 220), ncols=1, nrows=2) if len(np.unique(mask)) == 1: c = [] YII = mask NPQ = mask newmask = mask else: # find objects and setup roi c, h = pcv.find_objects(img, mask) # setup individual roi plant masks newmask = np.zeros_like(mask) # compute fv/fm and save to file YII, hist_fvfm = pcv.photosynthesis.analyze_fvfm(fdark=fdark, fmin=imgmin, fmax=img, mask=mask, bins=128) # YII = np.divide(Fv, # img, # out=out_flt.copy(), # where=np.logical_and(mask > 0, img > 0)) # NPQ is 0 NPQ = np.zeros_like(YII) # cv2.imwrite(os.path.join(fmaxdir, outfn + '-fvfm.tif'), YII) # print Fm - will need this later # cv2.imwrite(os.path.join(fmaxdir, outfn + '-fmax.tif'), img) # NPQ will always be an array of 0s else: # compute YII and NPQ if parameter is other than FvFm newmask = mask_Fm # use cv2 to read image becase pcv.readimage will save as input_image.png overwriting img # newmask = cv2.imread(os.path.join(maskdir, basefn + '-FvFm-mask.png'),-1) if len(np.unique(newmask)) == 1: YII = np.zeros_like(newmask) NPQ = np.zeros_like(newmask) else: # compute YII YII, hist_yii = pcv.photosynthesis.analyze_fvfm(fdark, fmin=imgmin, fmax=img, mask=newmask, bins=128) # make sure to initialize with out=. using where= provides random values at False pixels. you will get a strange result. newmask comes from Fm instead of Fm' so they can be different #newmask<0, img>0 = FALSE: not part of plant but fluorescence detected. #newmask>0, img<=0 = FALSE: part of plant in Fm but no fluorescence detected <- this is likely the culprit because pcv.apply_mask doesn't always solve issue. # YII = np.divide(Fvp, # img, # out=out_flt.copy(), # where=np.logical_and(newmask > 0, img > 0)) # compute NPQ # Fm = cv2.imread(os.path.join(fmaxdir, basefn + '-FvFm-fmax.tif'), -1) Fm = cv2.imread(fn_Fm, -1) NPQ = np.divide(Fm, img, out=out_flt.copy(), where=np.logical_and(newmask > 0, img > 0)) NPQ = np.subtract(NPQ, 1, out=out_flt.copy(), where=np.logical_and(NPQ >= 1, newmask > 0)) # cv2.imwrite(os.path.join(fmaxdir, outfn + '-yii.tif'), YII) # cv2.imwrite(os.path.join(fmaxdir, outfn + '-npq.tif'), NPQ) # end if-else Fv/Fm # Make as many copies of incoming dataframe as there are ROIs so all results can be saved outdf = fundf.copy() for i in range(0, len(roi_c) - 1): outdf = outdf.append(fundf) outdf.frameid = outdf.frameid.astype('uint8') # Initialize lists to store variables for each ROI and iterate through each plant frame_avg = [] yii_avg = [] yii_std = [] npq_avg = [] npq_std = [] plantarea = [] ithroi = [] inbounds = [] if len(c) == 0: for i, rc in enumerate(roi_c): # each variable needs to be stored 2 x #roi frame_avg.append(np.nan) frame_avg.append(np.nan) yii_avg.append(np.nan) yii_avg.append(np.nan) yii_std.append(np.nan) yii_std.append(np.nan) npq_avg.append(np.nan) npq_avg.append(np.nan) npq_std.append(np.nan) npq_std.append(np.nan) inbounds.append(False) inbounds.append(False) plantarea.append(0) plantarea.append(0) # Store iteration Number even if there are no objects in image ithroi.append(int(i)) ithroi.append(int(i)) # append twice so each image has a value. else: i = 1 rc = roi_c[i] for i, rc in enumerate(roi_c): # Store iteration Number ithroi.append(int(i)) ithroi.append(int(i)) # append twice so each image has a value. # extract ith hierarchy rh = roi_h[i] # Filter objects based on being in the defined ROI roi_obj, hierarchy_obj, submask, obj_area = pcv.roi_objects( img, roi_contour=rc, roi_hierarchy=rh, object_contour=c, obj_hierarchy=h, roi_type='partial') if obj_area == 0: print('!!! No plant detected in ROI ', str(i)) frame_avg.append(np.nan) frame_avg.append(np.nan) yii_avg.append(np.nan) yii_avg.append(np.nan) yii_std.append(np.nan) yii_std.append(np.nan) npq_avg.append(np.nan) npq_avg.append(np.nan) npq_std.append(np.nan) npq_std.append(np.nan) inbounds.append(False) inbounds.append(False) plantarea.append(0) plantarea.append(0) else: # Combine multiple plant objects within an roi together plant_contour, plant_mask = pcv.object_composition( img=img, contours=roi_obj, hierarchy=hierarchy_obj) #combine plant masks after roi filter if param_name == 'FvFm': newmask = pcv.image_add(newmask, plant_mask) # Calc mean and std dev of fluoresence, YII, and NPQ and save to list frame_avg.append(cppc.utils.mean(imgmin, plant_mask)) frame_avg.append(cppc.utils.mean(img, plant_mask)) # need double because there are two images per loop yii_avg.append(cppc.utils.mean(YII, plant_mask)) yii_avg.append(cppc.utils.mean(YII, plant_mask)) yii_std.append(cppc.utils.std(YII, plant_mask)) yii_std.append(cppc.utils.std(YII, plant_mask)) npq_avg.append(cppc.utils.mean(NPQ, plant_mask)) npq_avg.append(cppc.utils.mean(NPQ, plant_mask)) npq_std.append(cppc.utils.std(NPQ, plant_mask)) npq_std.append(cppc.utils.std(NPQ, plant_mask)) plantarea.append(obj_area * cppc.pixelresolution**2) plantarea.append(obj_area * cppc.pixelresolution**2) # Check if plant is compeltely within the frame of the image inbounds.append(pcv.within_frame(plant_mask)) inbounds.append(pcv.within_frame(plant_mask)) # Output a pseudocolor of NPQ and YII for each induction period for each image imgdir = os.path.join(outdir, 'pseudocolor_images') outfn_roi = outfn + '-roi' + str(i) os.makedirs(imgdir, exist_ok=True) npq_img = pcv.visualize.pseudocolor(NPQ, obj=None, mask=plant_mask, cmap='inferno', axes=False, min_value=0, max_value=2.5, background='black', obj_padding=0) npq_img = cppc.viz.add_scalebar( npq_img, pixelresolution=cppc.pixelresolution, barwidth=10, barlabel='1 cm', barlocation='lower left') # If you change the output size and resolution you will need to adjust the timelapse video script npq_img.set_size_inches(6, 6, forward=False) npq_img.savefig( os.path.join(imgdir, outfn_roi + '-NPQ.png'), bbox_inches='tight', dpi=100) #100 is default for matplotlib/plantcv if ilegend == 1: #only need to print legend once npq_img.savefig(os.path.join(imgdir, 'npq_legend.pdf'), bbox_inches='tight') npq_img.clf() yii_img = pcv.visualize.pseudocolor( YII, obj=None, mask=plant_mask, cmap='gist_rainbow', #custom_colormaps.get_cmap( # 'imagingwin')# axes=False, min_value=0, max_value=1, background='black', obj_padding=0) yii_img = cppc.viz.add_scalebar( yii_img, pixelresolution=cppc.pixelresolution, barwidth=10, barlabel='1 cm', barlocation='lower left') yii_img.set_size_inches(6, 6, forward=False) yii_img.savefig(os.path.join(imgdir, outfn_roi + '-YII.png'), bbox_inches='tight', dpi=100) if ilegend == 1: #print legend once and increment ilegend to stop in future iterations yii_img.savefig(os.path.join(imgdir, 'yii_legend.pdf'), bbox_inches='tight') ilegend = ilegend + 1 yii_img.clf() # end try-except-else # end roi loop # end if there are objects from roi filter # save mask of all plants to file after roi filter if param_name == 'FvFm': mask_Fm = newmask.copy() # pcv.print_image(newmask, os.path.join(maskdir, outfn + '-mask.png')) # check YII values for uniqueness between all ROI. nonunique ROI suggests the plants grew into each other and can no longer be reliably separated in image processing. # a single value isn't always robust. I think because there are small independent objects that fall in one roi but not the other that change the object within the roi slightly. # also note, I originally designed this for trays of 2 pots. It will not detect if e.g. 2 out of 9 plants grow into each other rounded_avg = [round(n, 3) for n in yii_avg] rounded_std = [round(n, 3) for n in yii_std] if len(roi_c) > 1: isunique = not (rounded_avg.count(rounded_avg[0]) == len(yii_avg) and rounded_std.count(rounded_std[0]) == len(yii_std)) else: isunique = True # save all values to outgoing dataframe outdf['roi'] = ithroi outdf['frame_avg'] = frame_avg outdf['yii_avg'] = yii_avg outdf['npq_avg'] = npq_avg outdf['yii_std'] = yii_std outdf['npq_std'] = npq_std outdf['obj_in_frame'] = inbounds outdf['unique_roi'] = isunique return (outdf)
# In[32]: # Filling any small objects even though there were none visible in the last image. ab_fill = pcv.fill(bin_img=ab, size=200) # In[34]: # Appying the new mask for a wiped background. masked2 = pcv.apply_mask(img=masked, mask=ab_fill, mask_color='white') # In[35]: # Object is masked and filled. If wispy leaves were pictured, the spaces between the # leaves would've been filled as well. That does not apply here. id_objects, obj_hierarchy = pcv.find_objects(masked2, ab_fill) # In[46]: # Selecting a region of interest (roi). Defining (x,y) sets the location for # the TOP LEFT corner of the rectangle. roi1, roi_hierarchy = pcv.roi.rectangle(img=masked2, x=110, y=40, h=200, w=190) # In[47]: # This function is useful for separating plant from backgroud if there are many spaces between leaves. roi_objects, hierarchy3, kept_mask, obj_area = pcv.roi_objects( img=img, roi_contour=roi1, roi_hierarchy=roi_hierarchy, object_contour=id_objects,
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_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 check_cycles(skel_img): """ Check for cycles in a skeleton image Inputs: skel_img = Skeletonized image Returns: cycle_img = Image with cycles identified :param skel_img: numpy.ndarray :return cycle_img: numpy.ndarray """ # Store debug debug = params.debug params.debug = None # Create the mask needed for cv2.floodFill, must be larger than the image h, w = skel_img.shape[:2] mask = np.zeros((h + 2, w + 2), np.uint8) # Copy the skeleton since cv2.floodFill will draw on it skel_copy = skel_img.copy() cv2.floodFill(skel_copy, mask=mask, seedPoint=(0, 0), newVal=255) # Invert so the holes are white and background black just_cycles = cv2.bitwise_not(skel_copy) # Erode slightly so that cv2.findContours doesn't think diagonal pixels are separate contours just_cycles = erode(just_cycles, 2, 1) # Use pcv.find_objects to turn plots of holes into countable contours cycle_objects, cycle_hierarchies = find_objects(just_cycles, just_cycles) # Count the number of holes num_cycles = len(cycle_objects) # Make debugging image cycle_img = skel_img.copy() cycle_img = dilate(cycle_img, params.line_thickness, 1) cycle_img = cv2.cvtColor(cycle_img, cv2.COLOR_GRAY2RGB) if num_cycles > 0: rand_color = color_palette(num_cycles) for i, cnt in enumerate(cycle_objects): cv2.drawContours(cycle_img, cycle_objects, i, rand_color[i], params.line_thickness, lineType=8, hierarchy=cycle_hierarchies) # Store Cycle Data outputs.add_observation(variable='num_cycles', trait='number of cycles', method='plantcv.plantcv.morphology.check_cycles', scale='none', datatype=int, value=num_cycles, label='none') # Reset debug mode params.debug = debug # Auto-increment device params.device += 1 if params.debug == 'print': print_image( cycle_img, os.path.join(params.debug_outdir, str(params.device) + '_cycles.png')) elif params.debug == 'plot': plot_image(cycle_img) return cycle_img
def main(): # Get options args = options() # Read image img, path, filename = pcv.readimage(args.image) pcv.params.debug=args.debug #set debug mode # STEP 1: Check if this is a night image, for some of these dataset's images were captured # at night, even if nothing is visible. To make sure that images are not taken at # night we check that the image isn't mostly dark (0=black, 255=white). # if it is a night image it throws a fatal error and stops the workflow. if np.average(img) < 50: pcv.fatal_error("Night Image") else: pass # STEP 2: Normalize the white color so you can later # compare color between images. # Inputs: # img = image object, RGB colorspace # roi = region for white reference, if none uses the whole image, # otherwise (x position, y position, box width, box height) # white balance image based on white toughspot #img1 = pcv.white_balance(img=img,roi=(400,800,200,200)) img1 = pcv.white_balance(img=img, mode='hist', roi=None) # STEP 3: Rotate the image # Inputs: # img = image object, RGB color space # rotation_deg = Rotation angle in degrees, can be negative, positive values # will move counter-clockwise # crop = If True then image will be cropped to original image dimensions, if False # the image size will be adjusted to accommodate new image dimensions rotate_img = pcv.rotate(img=img1,rotation_deg=-1, crop=False) # STEP 4: Shift image. This step is important for clustering later on. # For this image it also allows you to push the green raspberry pi camera # out of the image. This step might not be necessary for all images. # The resulting image is the same size as the original. # Inputs: # img = image object # number = integer, number of pixels to move image # side = direction to move from "top", "bottom", "right","left" shift1 = pcv.shift_img(img=img1, number=300, side='top') img1 = shift1 # STEP 5: Convert image from RGB colorspace to LAB colorspace # Keep only the green-magenta channel (grayscale) # Inputs: # img = image object, RGB colorspace # channel = color subchannel ('l' = lightness, 'a' = green-magenta , 'b' = blue-yellow) #a = pcv.rgb2gray_lab(img=img1, channel='a') a = pcv.rgb2gray_lab(rgb_img=img1, channel='a') # STEP 6: Set a binary threshold on the saturation channel image # Inputs: # img = img object, grayscale # threshold = threshold value (0-255) # max_value = value to apply above threshold (usually 255 = white) # object_type = light or dark # - If object is light then standard thresholding is done # - If object is dark then inverse thresholding is done img_binary = pcv.threshold.binary(gray_img=a, threshold=120, max_value=255, object_type='dark') #img_binary = pcv.threshold.binary(gray_img=a, threshold=120, max_value=255, object_type'dark') # ^ # | # adjust this value # STEP 7: Fill in small objects (speckles) # Inputs: # bin_img = image object, binary. img will be returned after filling # size = minimum object area size in pixels (integer) fill_image = pcv.fill(bin_img=img_binary, size=10) # ^ # | # adjust this value # STEP 8: Dilate so that you don't lose leaves (just in case) # Inputs: # img = input image # ksize = kernel size # i = iterations, i.e. number of consecutive filtering passes #dilated = pcv.dilate(img=fill_image, ksize=1, i=1) dilated = pcv.dilate(gray_img=fill_image, ksize=2, i=1) # STEP 9: Find objects (contours: black-white boundaries) # Inputs: # img = image that the objects will be overlayed # mask = what is used for object detection id_objects, obj_hierarchy = pcv.find_objects(img=img1, mask=dilated) #id_objects, obj_hierarchy = pcv.find_objects(gray_img, mask) # STEP 10: Define region of interest (ROI) # Inputs: # img = img to overlay roi # x_adj = adjust center along x axis # y_adj = adjust center along y axis # h_adj = adjust height # w_adj = adjust width # roi_contour, roi_hierarchy = pcv.roi.rectangle(img1, 10, 500, -10, -100) # ^ ^ # |________________| # adjust these four values roi_contour, roi_hierarchy = pcv.roi.rectangle(img=img1, x=200, y=190, h=2000, w=3000) # STEP 11: Keep objects that overlap with the ROI # 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 "Identifying Objects" function # obj_hierarchy = hierarchy of objects, output from "Identifying Objects" function # roi_type = 'partial' (default, for partially inside), 'cutto', or 'largest' (keep only largest contour) roi_objects, roi_obj_hierarchy, kept_mask, obj_area = pcv.roi_objects(img=img1, roi_contour=roi_contour, roi_hierarchy=roi_hierarchy, object_contour=id_objects, obj_hierarchy=obj_hierarchy, roi_type='partial') # STEP 12: This function take a image with multiple contours and # clusters them based on user input of rows and columns # Inputs: # img = An RGB image # roi_objects = object contours in an image that are needed to be clustered. # roi_obj_hierarchy = object hierarchy # nrow = number of rows to cluster (this should be the approximate number of desired rows in the entire image even if there isn't a literal row of plants) # ncol = number of columns to cluster (this should be the approximate number of desired columns in the entire image even if there isn't a literal row of plants) # show_grid = if True then a grid gets displayed in debug mode (default show_grid=False) clusters_i, contours, hierarchies = pcv.cluster_contours(img=img1, roi_objects=roi_objects, roi_obj_hierarchy=roi_obj_hierarchy, nrow=2, ncol=3) # STEP 13: This function takes clustered contours and splits them into multiple images, # also does a check to make sure that the number of inputted filenames matches the number # of clustered contours. If no filenames are given then the objects are just numbered # Inputs: # img = ideally a masked RGB image. # grouped_contour_indexes = output of cluster_contours, indexes of clusters of contours # contours = contours to cluster, output of cluster_contours # hierarchy = object hierarchy # outdir = directory for output images # file = the name of the input image to use as a base name , output of filename from read_image function # filenames = input txt file with list of filenames in order from top to bottom left to right (likely list of genotypes) # Set global debug behavior to None (default), "print" (to file), or "plot" (Jupyter Notebooks or X11) pcv.params.debug = "print" out = args.outdir names = args.names output_path, imgs, masks = pcv.cluster_contour_splitimg(rgb_img=img1, grouped_contour_indexes=clusters_i, contours=contours, hierarchy=hierarchies, outdir=out, file=filename, filenames=names)
def report_size_marker_area(img, roi_contour, roi_hierarchy, marker='define', objcolor='dark', thresh_channel=None, thresh=None): """Detects a size marker in a specified region and reports its size and eccentricity Inputs: img = An RGB or grayscale image to plot the marker object on roi_contour = A region of interest contour (e.g. output from pcv.roi.rectangle or other methods) roi_hierarchy = A region of interest contour hierarchy (e.g. output from pcv.roi.rectangle or other methods) marker = 'define' or 'detect'. If define it means you set an area, if detect it means you want to detect within an area objcolor = Object color is 'dark' or 'light' (is the marker darker or lighter than the background) thresh_channel = 'h', 's', or 'v' for hue, saturation or value thresh = Binary threshold value (integer) Returns: analysis_images = List of output images :param img: numpy.ndarray :param roi_contour: list :param roi_hierarchy: numpy.ndarray :param marker: str :param objcolor: str :param thresh_channel: str :param thresh: int :return: analysis_images: list """ params.device += 1 # Make a copy of the reference image ref_img = np.copy(img) # If the reference image is grayscale convert it to color if len(np.shape(ref_img)) == 2: ref_img = cv2.cvtColor(ref_img, cv2.COLOR_GRAY2BGR) # Marker components # If the marker type is "defined" then the marker_mask and marker_contours are equal to the input ROI # Initialize a binary image roi_mask = np.zeros(np.shape(img)[:2], dtype=np.uint8) # Draw the filled ROI on the mask cv2.drawContours(roi_mask, roi_contour, -1, (255), -1) marker_mask = [] marker_contour = [] # If the marker type is "detect" then we will use the ROI to isolate marker contours from the input image if marker.upper() == 'DETECT': # We need to convert the input image into an one of the HSV channels and then threshold it if thresh_channel is not None and thresh is not None: # Mask the input image masked = apply_mask(rgb_img=ref_img, mask=roi_mask, mask_color="black") # Convert the masked image to hue, saturation, or value marker_hsv = rgb2gray_hsv(rgb_img=masked, channel=thresh_channel) # Threshold the HSV image marker_bin = binary_threshold(gray_img=marker_hsv, threshold=thresh, max_value=255, object_type=objcolor) # Identify contours in the masked image contours, hierarchy = find_objects(img=ref_img, mask=marker_bin) # Filter marker contours using the input ROI kept_contours, kept_hierarchy, kept_mask, obj_area = roi_objects(img=ref_img, object_contour=contours, obj_hierarchy=hierarchy, roi_contour=roi_contour, roi_hierarchy=roi_hierarchy, roi_type="partial") # If there are more than one contour detected, combine them into one # These become the marker contour and mask marker_contour, marker_mask = object_composition(img=ref_img, contours=kept_contours, hierarchy=kept_hierarchy) else: fatal_error('thresh_channel and thresh must be defined in detect mode') elif marker.upper() == "DEFINE": # Identify contours in the masked image contours, hierarchy = find_objects(img=ref_img, mask=roi_mask) # If there are more than one contour detected, combine them into one # These become the marker contour and mask marker_contour, marker_mask = object_composition(img=ref_img, contours=contours, hierarchy=hierarchy) else: fatal_error("marker must be either 'define' or 'detect' but {0} was provided.".format(marker)) # Calculate the moments of the defined marker region m = cv2.moments(marker_mask, binaryImage=True) # Calculate the marker area marker_area = m['m00'] # Fit a bounding ellipse to the marker center, axes, angle = cv2.fitEllipse(marker_contour) major_axis = np.argmax(axes) minor_axis = 1 - major_axis major_axis_length = axes[major_axis] minor_axis_length = axes[minor_axis] # Calculate the bounding ellipse eccentricity eccentricity = np.sqrt(1 - (axes[minor_axis] / axes[major_axis]) ** 2) # Make a list to store output images analysis_image = [] cv2.drawContours(ref_img, marker_contour, -1, (255, 0, 0), 5) # out_file = os.path.splitext(filename)[0] + '_sizemarker.jpg' # print_image(ref_img, out_file) analysis_image.append(ref_img) if params.debug is 'print': print_image(ref_img, os.path.join(params.debug_outdir, str(params.device) + '_marker_shape.png')) elif params.debug is 'plot': plot_image(ref_img) outputs.add_observation(variable='marker_area', trait='marker area', method='plantcv.plantcv.report_size_marker_area', scale='pixels', datatype=int, value=marker_area, label='pixels') outputs.add_observation(variable='marker_ellipse_major_axis', trait='marker ellipse major axis length', method='plantcv.plantcv.report_size_marker_area', scale='pixels', datatype=int, value=major_axis_length, label='pixels') outputs.add_observation(variable='marker_ellipse_minor_axis', trait='marker ellipse minor axis length', method='plantcv.plantcv.report_size_marker_area', scale='pixels', datatype=int, value=minor_axis_length, label='pixels') outputs.add_observation(variable='marker_ellipse_eccentricity', trait='marker ellipse eccentricity', method='plantcv.plantcv.report_size_marker_area', scale='none', datatype=float, value=eccentricity, label='none') # Store images outputs.images.append(analysis_image) return analysis_image
def main(): # Set variables args = options() pcv.params.debug = args.debug # Read and rotate image img, path, filename = pcv.readimage(filename=args.image) img = pcv.rotate(img, -90, False) # Create mask from LAB b channel l = pcv.rgb2gray_lab(rgb_img=img, channel='b') l_thresh = pcv.threshold.binary(gray_img=l, threshold=115, max_value=255, object_type='dark') l_mblur = pcv.median_blur(gray_img=l_thresh, ksize=5) # Apply mask to image masked = pcv.apply_mask(img=img, mask=l_mblur, mask_color='white') ab_fill = pcv.fill(bin_img=l_mblur, size=50) # Extract plant object from image id_objects, obj_hierarchy = pcv.find_objects(img=img, mask=ab_fill) roi1, roi_hierarchy = pcv.roi.rectangle(img=masked, x=150, y=270, h=100, w=100) 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') obj, mask = pcv.object_composition(img=img, contours=roi_objects, hierarchy=hierarchy3) ############### Analysis ################ # Analyze shape properties analysis_image = pcv.analyze_object(img=img, obj=obj, mask=mask) boundary_image2 = pcv.analyze_bound_horizontal(img=img, obj=obj, mask=mask, line_position=370) # Analyze colour properties color_histogram = pcv.analyze_color(rgb_img=img, mask=kept_mask, hist_plot_type='all') # Analyze shape independent of size 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 pcv.print_results(filename='{}'.format(args.result)) pcv.print_image(img=color_histogram, filename='{}_color_hist.jpg'.format(args.outdir)) pcv.print_image(img=kept_mask, filename='{}_mask.jpg'.format(args.outdir))
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_skeleton(skel_img, mask=None): """ Segment a skeleton image into pieces Inputs: skel_img = Skeletonized image mask = (Optional) binary mask for debugging. If provided, debug image will be overlaid on the mask. Returns: segmented_img = Segmented debugging image segment_objects = list of contours :param skel_img: numpy.ndarray :param mask: numpy.ndarray :return segmented_img: numpy.ndarray :return segment_objects: list """ # Store debug debug = params.debug params.debug = None # Find branch points bp = find_branch_pts(skel_img) bp = dilate(bp, 3, 1) # Subtract from the skeleton so that leaves are no longer connected segments = image_subtract(skel_img, bp) # Gather contours of leaves segment_objects, _ = find_objects(segments, segments) # Reset debug mode params.debug = debug # Color each segment a different color, do not used a previously saved color scale rand_color = color_palette(num=len(segment_objects), saved=False) if mask is None: segmented_img = skel_img.copy() else: segmented_img = mask.copy() segmented_img = cv2.cvtColor(segmented_img, cv2.COLOR_GRAY2RGB) for i, cnt in enumerate(segment_objects): cv2.drawContours(segmented_img, segment_objects, i, rand_color[i], params.line_thickness, lineType=8) # Auto-increment device params.device += 1 if params.debug == 'print': print_image( segmented_img, os.path.join(params.debug_outdir, str(params.device) + '_segmented.png')) elif params.debug == 'plot': plot_image(segmented_img) return segmented_img, segment_objects
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 report_size_marker_area(img, roi_contour, roi_hierarchy, marker='define', objcolor='dark', thresh_channel=None, thresh=None): """Detects a size marker in a specified region and reports its size and eccentricity Inputs: img = An RGB or grayscale image to plot the marker object on roi_contour = A region of interest contour (e.g. output from pcv.roi.rectangle or other methods) roi_hierarchy = A region of interest contour hierarchy (e.g. output from pcv.roi.rectangle or other methods) marker = 'define' or 'detect'. If define it means you set an area, if detect it means you want to detect within an area objcolor = Object color is 'dark' or 'light' (is the marker darker or lighter than the background) thresh_channel = 'h', 's', or 'v' for hue, saturation or value thresh = Binary threshold value (integer) Returns: analysis_images = List of output images :param img: numpy.ndarray :param roi_contour: list :param roi_hierarchy: numpy.ndarray :param marker: str :param objcolor: str :param thresh_channel: str :param thresh: int :return: analysis_images: list """ # Store debug debug = params.debug params.debug = None params.device += 1 # Make a copy of the reference image ref_img = np.copy(img) # If the reference image is grayscale convert it to color if len(np.shape(ref_img)) == 2: ref_img = cv2.cvtColor(ref_img, cv2.COLOR_GRAY2BGR) # Marker components # If the marker type is "defined" then the marker_mask and marker_contours are equal to the input ROI # Initialize a binary image roi_mask = np.zeros(np.shape(img)[:2], dtype=np.uint8) # Draw the filled ROI on the mask cv2.drawContours(roi_mask, roi_contour, -1, (255), -1) marker_mask = [] marker_contour = [] # If the marker type is "detect" then we will use the ROI to isolate marker contours from the input image if marker.upper() == 'DETECT': # We need to convert the input image into an one of the HSV channels and then threshold it if thresh_channel is not None and thresh is not None: # Mask the input image masked = apply_mask(rgb_img=ref_img, mask=roi_mask, mask_color="black") # Convert the masked image to hue, saturation, or value marker_hsv = rgb2gray_hsv(rgb_img=masked, channel=thresh_channel) # Threshold the HSV image marker_bin = binary_threshold(gray_img=marker_hsv, threshold=thresh, max_value=255, object_type=objcolor) # Identify contours in the masked image contours, hierarchy = find_objects(img=ref_img, mask=marker_bin) # Filter marker contours using the input ROI kept_contours, kept_hierarchy, kept_mask, obj_area = roi_objects( img=ref_img, object_contour=contours, obj_hierarchy=hierarchy, roi_contour=roi_contour, roi_hierarchy=roi_hierarchy, roi_type="partial") # If there are more than one contour detected, combine them into one # These become the marker contour and mask marker_contour, marker_mask = object_composition( img=ref_img, contours=kept_contours, hierarchy=kept_hierarchy) else: fatal_error( 'thresh_channel and thresh must be defined in detect mode') elif marker.upper() == "DEFINE": # Identify contours in the masked image contours, hierarchy = find_objects(img=ref_img, mask=roi_mask) # If there are more than one contour detected, combine them into one # These become the marker contour and mask marker_contour, marker_mask = object_composition(img=ref_img, contours=contours, hierarchy=hierarchy) else: fatal_error( "marker must be either 'define' or 'detect' but {0} was provided.". format(marker)) # Calculate the moments of the defined marker region m = cv2.moments(marker_mask, binaryImage=True) # Calculate the marker area marker_area = m['m00'] # Fit a bounding ellipse to the marker center, axes, angle = cv2.fitEllipse(marker_contour) major_axis = np.argmax(axes) minor_axis = 1 - major_axis major_axis_length = axes[major_axis] minor_axis_length = axes[minor_axis] # Calculate the bounding ellipse eccentricity eccentricity = np.sqrt(1 - (axes[minor_axis] / axes[major_axis])**2) cv2.drawContours(ref_img, marker_contour, -1, (255, 0, 0), 5) analysis_image = ref_img # Reset debug mode params.debug = debug if params.debug is 'print': print_image( ref_img, os.path.join(params.debug_outdir, str(params.device) + '_marker_shape.png')) elif params.debug is 'plot': plot_image(ref_img) outputs.add_observation(variable='marker_area', trait='marker area', method='plantcv.plantcv.report_size_marker_area', scale='pixels', datatype=int, value=marker_area, label='pixels') outputs.add_observation(variable='marker_ellipse_major_axis', trait='marker ellipse major axis length', method='plantcv.plantcv.report_size_marker_area', scale='pixels', datatype=int, value=major_axis_length, label='pixels') outputs.add_observation(variable='marker_ellipse_minor_axis', trait='marker ellipse minor axis length', method='plantcv.plantcv.report_size_marker_area', scale='pixels', datatype=int, value=minor_axis_length, label='pixels') outputs.add_observation(variable='marker_ellipse_eccentricity', trait='marker ellipse eccentricity', method='plantcv.plantcv.report_size_marker_area', scale='none', datatype=float, value=eccentricity, label='none') # Store images outputs.images.append(analysis_image) return analysis_image
def main(): # Get options args = options() # Set variables pcv.params.debug = args.debug # Replace the hard-coded debug with the debug flag img_file = args.image # Replace the hard-coded input image with image flag ############### Image read-in ################ # Read target image img, path, filename = pcv.readimage(filename = img_file, mode = "rgb") ############### Find scale and crop ################ # find colour card in the image to be analysed df, start, space = pcv.transform.find_color_card(rgb_img = img) if int(start[0]) < 2000: img = imutils.rotate_bound(img, -90) rotated = 1 df, start, space = pcv.transform.find_color_card(rgb_img = img) else: rotated = 0 #if img.shape[0] > 6000: # rotated = 1 #else: rotated = 0 img_mask = pcv.transform.create_color_card_mask(rgb_img = img, radius = 10, start_coord = start, spacing = space, ncols = 4, nrows = 6) # write the spacing of the colour card to file as size marker with open(r'size_marker.csv', 'a') as f: writer = csv.writer(f) writer.writerow([filename, space[0]]) # define a bounding rectangle around the colour card x_cc,y_cc,w_cc,h_cc = cv2.boundingRect(img_mask) x_cc = int(round(x_cc - 0.3 * w_cc)) y_cc = int(round(y_cc - 0.3 * h_cc)) h_cc = int(round(h_cc * 1.6)) w_cc = int(round(w_cc * 1.6)) # crop out colour card start_point = (x_cc, y_cc) end_point = (x_cc+w_cc, y_cc+h_cc) colour = (0, 0, 0) thickness = -1 crop_img = cv2.rectangle(img, start_point, end_point, colour, thickness) ############### Fine segmentation ################ # Threshold A and B channels of the LAB colourspace and the Hue channel of the HSV colourspace l_thresh, _ = pcv.threshold.custom_range(img=crop_img, lower_thresh=[70,0,0], upper_thresh=[255,255,255], channel='LAB') a_thresh, _ = pcv.threshold.custom_range(img=crop_img, lower_thresh=[0,0,0], upper_thresh=[255,145,255], channel='LAB') b_thresh, _ = pcv.threshold.custom_range(img=crop_img, lower_thresh=[0,0,123], upper_thresh=[255,255,255], channel='LAB') h_thresh_low, _ = pcv.threshold.custom_range(img=crop_img, lower_thresh=[0,0,0], upper_thresh=[130,255,255], channel='HSV') h_thresh_high, _ = pcv.threshold.custom_range(img=crop_img, lower_thresh=[150,0,0], upper_thresh=[255,255,255], channel='HSV') h_thresh = pcv.logical_or(h_thresh_low, h_thresh_high) # Join the thresholded images to keep only consensus pixels ab = pcv.logical_and(b_thresh, a_thresh) lab = pcv.logical_and(l_thresh, ab) labh = pcv.logical_and(lab, h_thresh) # Fill small objects labh_clean = pcv.fill(labh, 200) # Dilate to close broken borders #labh_dilated = pcv.dilate(labh_clean, 4, 1) labh_dilated = labh_clean # Apply mask (for VIS images, mask_color=white) masked = pcv.apply_mask(crop_img, labh_dilated, "white") # Identify objects contours, hierarchy = pcv.find_objects(crop_img, labh_dilated) # Define ROI if rotated == 1: roi_height = 3000 roi_lwr_bound = y_cc + (h_cc * 0.5) - roi_height roi_contour, roi_hierarchy= pcv.roi.rectangle(x=1000, y=roi_lwr_bound, h=roi_height, w=2000, img=crop_img) else: roi_height = 1500 roi_lwr_bound = y_cc + (h_cc * 0.5) - roi_height roi_contour, roi_hierarchy= pcv.roi.rectangle(x=2000, y=roi_lwr_bound, h=roi_height, w=2000, img=crop_img) # Decide which objects to keep filtered_contours, filtered_hierarchy, mask, area = pcv.roi_objects(img = crop_img, roi_type = 'partial', roi_contour = roi_contour, roi_hierarchy = roi_hierarchy, object_contour = contours, obj_hierarchy = hierarchy) # Combine kept objects obj, mask = pcv.object_composition(crop_img, filtered_contours, filtered_hierarchy) ############### Analysis ################ outfile=False if args.writeimg==True: outfile_black=args.outdir+"/"+filename+"_black" outfile_white=args.outdir+"/"+filename+"_white" outfile_analysed=args.outdir+"/"+filename+"_analysed" # analyse shape shape_img = pcv.analyze_object(crop_img, obj, mask) pcv.print_image(shape_img, outfile_analysed) # analyse colour colour_img = pcv.analyze_color(crop_img, mask, 'hsv') # keep the segmented plant for visualisation picture_mask = pcv.apply_mask(crop_img, mask, "black") pcv.print_image(picture_mask, outfile_black) picture_mask = pcv.apply_mask(crop_img, mask, "white") pcv.print_image(picture_mask, outfile_white) # print out results pcv.outputs.save_results(filename=args.result, outformat="json")
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)
def segmentation(imgW, imgNIR, shape): # VIS example from PlantCV with few modifications # Higher value = more strict selection s_threshold = 165 b_threshold = 200 # Read image img = imread(imgW) #img = cvtColor(img, COLOR_BGR2RGB) imgNIR = imread(imgNIR) #imgNIR = cvtColor(imgNIR, COLOR_BGR2RGB) #img, path, img_filename = pcv.readimage(filename=imgW, mode="native") #imgNIR, pathNIR, imgNIR_filename = pcv.readimage(filename=imgNIR, mode="native") # Convert RGB to HSV and extract the saturation channel s = pcv.rgb2gray_hsv(rgb_img=img, channel='s') # Threshold the saturation image s_thresh = pcv.threshold.binary(gray_img=s, threshold=s_threshold, max_value=255, object_type='light') # Median Blur s_mblur = pcv.median_blur(gray_img=s_thresh, ksize=5) s_cnt = pcv.median_blur(gray_img=s_thresh, ksize=5) # Convert RGB to LAB and extract the Blue channel b = pcv.rgb2gray_lab(rgb_img=img, channel='b') # Threshold the blue image ORIGINAL 160 b_thresh = pcv.threshold.binary(gray_img=b, threshold=b_threshold, max_value=255, object_type='light') b_cnt = pcv.threshold.binary(gray_img=b, threshold=b_threshold, max_value=255, object_type='light') # Join the thresholded saturation and blue-yellow images bs = pcv.logical_or(bin_img1=s_mblur, bin_img2=b_cnt) # Apply Mask (for VIS images, mask_color=white) masked = pcv.apply_mask(img=img, mask=bs, mask_color='white') # 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 # 115 # 135 # 128 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') # 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) # Fill small objects ab_fill = pcv.fill(bin_img=ab, size=200) # Apply mask (for VIS images, mask_color=white) masked2 = pcv.apply_mask(img=masked, mask=ab_fill, mask_color='white') # Identify objects id_objects, obj_hierarchy = pcv.find_objects(img=masked2, mask=ab_fill) # Define ROI height = shape[0] width = shape[1] roi1, roi_hierarchy= pcv.roi.rectangle(img=masked2, x=0, y=0, h=height, w=width) # Decide which 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) # Filling holes in the mask, works great for alive plants, not so good for dead plants filled_mask = pcv.fill_holes(mask) final = pcv.apply_mask(img=imgNIR, mask=mask, mask_color='white') pcv.print_image(final, "./segment/segment-temp.png")
def find_branch_pts(skel_img, mask=None, label="default"): """ The branching algorithm was inspired by Jean-Patrick Pommier: https://gist.github.com/jeanpat/5712699 Inputs: skel_img = Skeletonized image mask = (Optional) binary mask for debugging. If provided, debug image will be overlaid on the mask. label = optional label parameter, modifies the variable name of observations recorded Returns: branch_pts_img = Image with just branch points, rest 0 :param skel_img: numpy.ndarray :param mask: np.ndarray :param label: str :return branch_pts_img: numpy.ndarray """ # In a kernel: 1 values line up with 255s, -1s line up with 0s, and 0s correspond to don't care # T like branch points t1 = np.array([[-1, 1, -1], [1, 1, 1], [-1, -1, -1]]) t2 = np.array([[1, -1, 1], [-1, 1, -1], [1, -1, -1]]) t3 = np.rot90(t1) t4 = np.rot90(t2) t5 = np.rot90(t3) t6 = np.rot90(t4) t7 = np.rot90(t5) t8 = np.rot90(t6) # Y like branch points y1 = np.array([[1, -1, 1], [0, 1, 0], [0, 1, 0]]) y2 = np.array([[-1, 1, -1], [1, 1, 0], [-1, 0, 1]]) y3 = np.rot90(y1) y4 = np.rot90(y2) y5 = np.rot90(y3) y6 = np.rot90(y4) y7 = np.rot90(y5) y8 = np.rot90(y6) kernels = [t1, t2, t3, t4, t5, t6, t7, t8, y1, y2, y3, y4, y5, y6, y7, y8] branch_pts_img = np.zeros(skel_img.shape[:2], dtype=int) # Store branch points for kernel in kernels: branch_pts_img = np.logical_or( cv2.morphologyEx(skel_img, op=cv2.MORPH_HITMISS, kernel=kernel, borderType=cv2.BORDER_CONSTANT, borderValue=0), branch_pts_img) # Switch type to uint8 rather than bool branch_pts_img = branch_pts_img.astype(np.uint8) * 255 # Store debug debug = params.debug params.debug = None # Make debugging image if mask is None: dilated_skel = dilate(skel_img, params.line_thickness, 1) branch_plot = cv2.cvtColor(dilated_skel, cv2.COLOR_GRAY2RGB) else: # Make debugging image on mask mask_copy = mask.copy() branch_plot = cv2.cvtColor(mask_copy, cv2.COLOR_GRAY2RGB) skel_obj, skel_hier = find_objects(skel_img, skel_img) cv2.drawContours(branch_plot, skel_obj, -1, (150, 150, 150), params.line_thickness, lineType=8, hierarchy=skel_hier) branch_objects, _ = find_objects(branch_pts_img, branch_pts_img) # Initialize list of tip data points branch_list = [] branch_labels = [] for i, branch in enumerate(branch_objects): x, y = branch.ravel()[:2] coord = (int(x), int(y)) branch_list.append(coord) branch_labels.append(i) cv2.circle(branch_plot, (x, y), params.line_thickness, (255, 0, 255), -1) outputs.add_observation( sample=label, variable='branch_pts', trait='list of branch-point coordinates identified from a skeleton', method='plantcv.plantcv.morphology.find_branch_pts', scale='pixels', datatype=list, value=branch_list, label=branch_labels) # Reset debug mode params.debug = debug # Auto-increment device params.device += 1 if params.debug == 'print': print_image( branch_plot, os.path.join(params.debug_outdir, str(params.device) + '_branch_pts.png')) elif params.debug == 'plot': plot_image(branch_plot) return branch_pts_img
def main(): args = options() os.chdir(args.outdir) # Read RGB image img, path, filename = pcv.readimage(args.image, mode="native") # Get metadata from file name geno_name = filename.split("}{") geno_name = geno_name[5] geno_name = geno_name.split("_") geno_name = geno_name[1] day = filename.split("}{") day = day[7] day = day.split("_") day = day[1] day = day.split("}") day = day[0] plot = filename.split("}{") plot = plot[0] plot = plot.split("_") plot = plot[1] exp_name = filename.split("}{") exp_name = exp_name[1] exp_name = exp_name.split("_") exp_name = exp_name[1] treat_name = filename.split("}{") treat_name = treat_name[6] # Create masks using Naive Bayes Classifier and PDFs file masks = pcv.naive_bayes_classifier(img, args.pdfs) # The following code will identify the racks in the image, find the top edge, and choose a line along the edge to pick a y coordinate to trim any soil/pot pixels identified as plant material. # Convert RGB to HSV and extract the Value channel v = pcv.rgb2gray_hsv(img, 'v') # Threshold the Value image v_thresh = pcv.threshold.binary(v, 98, 255, 'light') # Dilate mask to fill holes dilate_racks = pcv.dilate(v_thresh, 2, 1) # Fill in small objects mask = np.copy(dilate_racks) fill_racks = pcv.fill(mask, 100000) #edge detection edges = cv2.Canny(fill_racks, 60, 180) #write all the straight lines from edge detection lines = cv2.HoughLinesP(edges, rho=1, theta=1 * np.pi / 180, threshold=150, minLineLength=50, maxLineGap=15) N = lines.shape[0] for i in range(N): x1 = lines[i][0][0] y1 = lines[i][0][1] x2 = lines[i][0][2] y2 = lines[i][0][3] cv2.line(img, (x1, y1), (x2, y2), (255, 0, 0), 2) # keep only horizontal lines N = lines.shape[0] tokeep = [] for i in range(N): want = (abs(lines[i][0][1] - lines[i][0][3])) <= 10 tokeep.append(want) lines = lines[tokeep] # keep only lines in lower half of image N = lines.shape[0] tokeep = [] for i in range(N): want = 3100 > lines[i][0][1] > 2300 tokeep.append(want) lines = lines[tokeep] # assign lines to positions around plants N = lines.shape[0] tokeep = [] left = [] mid = [] right = [] for i in range(N): leftones = lines[i][0][2] <= 2000 left.append(leftones) midones = 3000 > lines[i][0][2] > 2000 mid.append(midones) rightones = lines[i][0][0] >= 3300 right.append(rightones) right = lines[right] left = lines[left] mid = lines[mid] # choose y values for right left mid adding some pixels to go about the pot (subtract because of orientation of axis) y_left = left[0][0][3] - 50 y_mid = mid[0][0][3] - 50 y_right = right[0][0][3] - 50 # reload original image to write new lines on img, path, filename = pcv.readimage(args.image) # write horizontal lines on image cv2.line(img, (left[0][0][0], left[0][0][1]), (left[0][0][2], left[0][0][3]), (255, 255, 51), 2) cv2.line(img, (mid[0][0][0], mid[0][0][1]), (mid[0][0][2], mid[0][0][3]), (255, 255, 51), 2) cv2.line(img, (right[0][0][0], right[0][0][1]), (right[0][0][2], right[0][0][3]), (255, 255, 51), 2) # Add masks together added = masks["healthy"] + masks["necrosis"] + masks["stem"] # Dilate mask to fill holes dilate_img = pcv.dilate(added, 2, 1) # Fill in small objects mask = np.copy(dilate_img) fill_img = pcv.fill(mask, 400) ret, inverted = cv2.threshold(fill_img, 75, 255, cv2.THRESH_BINARY_INV) # Dilate mask to fill holes of plant dilate_inv = pcv.dilate(inverted, 2, 1) # Fill in small objects of plant mask2 = np.copy(dilate_inv) fill_plant = pcv.fill(mask2, 20) inverted_img = pcv.invert(fill_plant) # Identify objects id_objects, obj_hierarchy = pcv.find_objects(img, inverted_img) # Define ROIs roi_left, roi_hierarchy_left = pcv.roi.rectangle(280, 1280, 1275, 1200, img) roi_mid, roi_hierarchy_mid = pcv.roi.rectangle(1900, 1280, 1275, 1200, img) roi_right, roi_hierarchy_right = pcv.roi.rectangle(3600, 1280, 1275, 1200, img) # Decide which objects to keep roi_objects_left, roi_obj_hierarchy_left, kept_mask_left, obj_area_left = pcv.roi_objects( img, 'partial', roi_left, roi_hierarchy_left, id_objects, obj_hierarchy) roi_objects_mid, roi_obj_hierarchy_mid, kept_mask_mid, obj_area_mid = pcv.roi_objects( img, 'partial', roi_mid, roi_hierarchy_mid, id_objects, obj_hierarchy) roi_objects_right, roi_obj_hierarchy_right, kept_mask_right, obj_area_right = pcv.roi_objects( img, 'partial', roi_right, roi_hierarchy_right, id_objects, obj_hierarchy) # Combine objects obj_r, mask_r = pcv.object_composition(img, roi_objects_right, roi_obj_hierarchy_right) obj_m, mask_m = pcv.object_composition(img, roi_objects_mid, roi_obj_hierarchy_mid) obj_l, mask_l = pcv.object_composition(img, roi_objects_left, roi_obj_hierarchy_left) def analyze_bound_horizontal2(img, obj, mask, line_position, filename=False): ori_img = np.copy(img) # Draw line horizontal line through bottom of image, that is adjusted to user input height if len(np.shape(ori_img)) == 3: iy, ix, iz = np.shape(ori_img) else: iy, ix = np.shape(ori_img) size = (iy, ix) size1 = (iy, ix, 3) background = np.zeros(size, dtype=np.uint8) wback = np.zeros(size1, dtype=np.uint8) x_coor = int(ix) y_coor = int(iy) - int(line_position) rec_corner = int(iy - 2) rec_point1 = (1, rec_corner) rec_point2 = (x_coor - 2, y_coor - 2) cv2.rectangle(background, rec_point1, rec_point2, (255), 1) below_contour, below_hierarchy = cv2.findContours( background, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)[-2:] below = [] above = [] mask_nonzerox, mask_nonzeroy = np.nonzero(mask) obj_points = np.vstack((mask_nonzeroy, mask_nonzerox)) obj_points1 = np.transpose(obj_points) for i, c in enumerate(obj_points1): xy = tuple(c) pptest = cv2.pointPolygonTest(below_contour[0], xy, measureDist=False) if pptest == 1: below.append(xy) cv2.circle(ori_img, xy, 1, (0, 0, 255)) cv2.circle(wback, xy, 1, (0, 0, 0)) else: above.append(xy) cv2.circle(ori_img, xy, 1, (0, 255, 0)) cv2.circle(wback, xy, 1, (255, 255, 255)) return wback ori_img = np.copy(img) # Draw line horizontal line through bottom of image, that is adjusted to user input height if len(np.shape(ori_img)) == 3: iy, ix, iz = np.shape(ori_img) else: iy, ix = np.shape(ori_img) if obj_r is not None: wback_r = analyze_bound_horizontal2(img, obj_r, mask_r, iy - y_right) if obj_m is not None: wback_m = analyze_bound_horizontal2(img, obj_m, mask_m, iy - y_mid) if obj_l is not None: wback_l = analyze_bound_horizontal2(img, obj_l, mask_l, iy - y_left) threshold_light = pcv.threshold.binary(img, 1, 1, 'dark') if obj_r is not None: fgmask_r = pcv.background_subtraction(wback_r, threshold_light) if obj_m is not None: fgmask_m = pcv.background_subtraction(wback_m, threshold_light) if obj_l is not None: fgmask_l = pcv.background_subtraction(wback_l, threshold_light) if obj_l is not None: id_objects_left, obj_hierarchy_left = pcv.find_objects(img, fgmask_l) if obj_m is not None: id_objects_mid, obj_hierarchy_mid = pcv.find_objects(img, fgmask_m) if obj_r is not None: id_objects_right, obj_hierarchy_right = pcv.find_objects(img, fgmask_r) # Combine objects if obj_r is not None: obj_r2, mask_r2 = pcv.object_composition(img, id_objects_right, obj_hierarchy_right) if obj_m is not None: obj_m2, mask_m2 = pcv.object_composition(img, id_objects_mid, obj_hierarchy_mid) if obj_l is not None: obj_l2, mask_l2 = pcv.object_composition(img, id_objects_left, obj_hierarchy_left) # Shape measurements if obj_l is not None: shape_header_left, shape_data_left, shape_img_left = pcv.analyze_object( img, obj_l2, fgmask_l, geno_name + '_' + plot + '_' + 'A' + '_' + day + '_' + 'shape.jpg') if obj_r is not None: shape_header_right, shape_data_right, shape_img_right = pcv.analyze_object( img, obj_r2, fgmask_r, geno_name + '_' + plot + '_' + 'C' + '_' + day + '_' + 'shape.jpg') if obj_m is not None: shape_header_mid, shape_data_mid, shape_img_mid = pcv.analyze_object( img, obj_m2, fgmask_m, geno_name + '_' + plot + '_' + 'B' + '_' + day + '_' + 'shape.jpg') # Color data if obj_r is not None: color_header_right, color_data_right, norm_slice_right = pcv.analyze_color( img, fgmask_r, 256, None, 'v', 'img', geno_name + '_' + plot + '_' + 'C' + '_' + day + '_' + 'color.jpg') if obj_m is not None: color_header_mid, color_data_mid, norm_slice_mid = pcv.analyze_color( img, fgmask_m, 256, None, 'v', 'img', geno_name + '_' + plot + '_' + 'B' + '_' + day + '_' + 'color.jpg') if obj_l is not None: color_header_left, color_data_left, norm_slice_left = pcv.analyze_color( img, fgmask_l, 256, None, 'v', 'img', geno_name + '_' + plot + '_' + 'A' + '_' + day + '_' + 'color.jpg') new_header = [ 'experiment', 'day', 'genotype', 'treatment', 'plot', 'plant', 'percent.necrosis', 'area', 'hull-area', 'solidity', 'perimeter', 'width', 'height', 'longest_axis', 'center-of-mass-x', 'center-of-mass-y', 'hull_vertices', 'in_bounds', 'ellipse_center_x', 'ellipse_center_y', 'ellipse_major_axis', 'ellipse_minor_axis', 'ellipse_angle', 'ellipse_eccentricity', 'bin-number', 'bin-values', 'blue', 'green', 'red', 'lightness', 'green-magenta', 'blue-yellow', 'hue', 'saturation', 'value' ] table = [] table.append(new_header) added2 = masks["healthy"] + masks["stem"] # Object combine kept objects if obj_l is not None: masked_image_healthy_left = pcv.apply_mask(added2, fgmask_l, 'black') masked_image_necrosis_left = pcv.apply_mask(masks["necrosis"], fgmask_l, 'black') added_obj_left = masked_image_healthy_left + masked_image_necrosis_left sample = "A" # Calculations necrosis_left = np.sum(masked_image_necrosis_left) necrosis_percent_left = float(necrosis_left) / np.sum(added_obj_left) table.append([ exp_name, day, geno_name, treat_name, plot, sample, round(necrosis_percent_left, 5), shape_data_left[1], shape_data_left[2], shape_data_left[3], shape_data_left[4], shape_data_left[5], shape_data_left[6], shape_data_left[7], shape_data_left[8], shape_data_left[9], shape_data_left[10], shape_data_left[11], shape_data_left[12], shape_data_left[13], shape_data_left[14], shape_data_left[15], shape_data_left[16], shape_data_left[17], '"{}"'.format(color_data_left[1]), '"{}"'.format(color_data_left[2]), '"{}"'.format( color_data_left[3]), '"{}"'.format(color_data_left[4]), '"{}"'.format(color_data_left[5]), '"{}"'.format( color_data_left[6]), '"{}"'.format(color_data_left[7]), '"{}"'.format(color_data_left[8]), '"{}"'.format( color_data_left[9]), '"{}"'.format(color_data_left[10]), '"{}"'.format(color_data_left[11]) ]) # Object combine kept objects if obj_m is not None: masked_image_healthy_mid = pcv.apply_mask(added2, fgmask_m, 'black') masked_image_necrosis_mid = pcv.apply_mask(masks["necrosis"], fgmask_m, 'black') added_obj_mid = masked_image_healthy_mid + masked_image_necrosis_mid sample = "B" # Calculations necrosis_mid = np.sum(masked_image_necrosis_mid) necrosis_percent_mid = float(necrosis_mid) / np.sum(added_obj_mid) table.append([ exp_name, day, geno_name, treat_name, plot, sample, round(necrosis_percent_mid, 5), shape_data_mid[1], shape_data_mid[2], shape_data_mid[3], shape_data_mid[4], shape_data_mid[5], shape_data_mid[6], shape_data_mid[7], shape_data_mid[8], shape_data_mid[9], shape_data_mid[10], shape_data_mid[11], shape_data_mid[12], shape_data_mid[13], shape_data_mid[14], shape_data_mid[15], shape_data_mid[16], shape_data_mid[17], '"{}"'.format(color_data_mid[1]), '"{}"'.format(color_data_mid[2]), '"{}"'.format(color_data_mid[3]), '"{}"'.format(color_data_mid[4]), '"{}"'.format(color_data_mid[5]), '"{}"'.format(color_data_mid[6]), '"{}"'.format(color_data_mid[7]), '"{}"'.format(color_data_mid[8]), '"{}"'.format(color_data_mid[9]), '"{}"'.format( color_data_mid[10]), '"{}"'.format(color_data_mid[11]) ]) # Object combine kept objects if obj_r is not None: masked_image_healthy_right = pcv.apply_mask(added2, fgmask_r, 'black') masked_image_necrosis_right = pcv.apply_mask(masks["necrosis"], fgmask_r, 'black') added_obj_right = masked_image_healthy_right + masked_image_necrosis_right sample = "C" # Calculations necrosis_right = np.sum(masked_image_necrosis_right) necrosis_percent_right = float(necrosis_right) / np.sum( added_obj_right) table.append([ exp_name, day, geno_name, treat_name, plot, sample, round(necrosis_percent_right, 5), shape_data_right[1], shape_data_right[2], shape_data_right[3], shape_data_right[4], shape_data_right[5], shape_data_right[6], shape_data_right[7], shape_data_right[8], shape_data_right[9], shape_data_right[10], shape_data_right[11], shape_data_right[12], shape_data_right[13], shape_data_right[14], shape_data_right[15], shape_data_right[16], shape_data_right[17], '"{}"'.format(color_data_right[1]), '"{}"'.format(color_data_right[2]), '"{}"'.format( color_data_right[3]), '"{}"'.format(color_data_right[4]), '"{}"'.format(color_data_right[5]), '"{}"'.format( color_data_right[6]), '"{}"'.format(color_data_right[7]), '"{}"'.format(color_data_right[8]), '"{}"'.format( color_data_right[9]), '"{}"'.format(color_data_right[10]), '"{}"'.format(color_data_right[11]) ]) if obj_l is not None: merged2 = cv2.merge([ masked_image_healthy_left, np.zeros(np.shape(masks["healthy"]), dtype=np.uint8), masked_image_necrosis_left ]) #blue, green, red pcv.print_image( merged2, geno_name + '_' + plot + '_' + 'A' + '_' + day + '_' + 'merged.jpg') if obj_m is not None: merged3 = cv2.merge([ masked_image_healthy_mid, np.zeros(np.shape(masks["healthy"]), dtype=np.uint8), masked_image_necrosis_mid ]) #blue, green, red pcv.print_image( merged3, geno_name + '_' + plot + '_' + 'B' + '_' + day + '_' + 'merged.jpg') if obj_r is not None: merged4 = cv2.merge([ masked_image_healthy_right, np.zeros(np.shape(masks["healthy"]), dtype=np.uint8), masked_image_necrosis_right ]) #blue, green, red pcv.print_image( merged4, geno_name + '_' + plot + '_' + 'C' + '_' + day + '_' + 'merged.jpg') # Save area results to file (individual csv files for one image...) file_name = filename.split("}{") file_name = file_name[0] + "}{" + file_name[5] + "}{" + file_name[7] outfile = str(file_name[:-4]) + 'csv' with open(outfile, 'w') as f: for row in table: f.write(','.join(map(str, row)) + '\n') print(filename)
def check_cycles(skel_img): """ Check for cycles in a skeleton image Inputs: skel_img = Skeletonized image Returns: cycle_img = Image with cycles identified :param skel_img: numpy.ndarray :return cycle_img: numpy.ndarray """ # Store debug debug = params.debug params.debug = None # Create the mask needed for cv2.floodFill, must be larger than the image h, w = skel_img.shape[:2] mask = np.zeros((h + 2, w + 2), np.uint8) # Copy the skeleton since cv2.floodFill will draw on it skel_copy = skel_img.copy() cv2.floodFill(skel_copy, mask=mask, seedPoint=(0, 0), newVal=255) # Invert so the holes are white and background black just_cycles = cv2.bitwise_not(skel_copy) # Erode slightly so that cv2.findContours doesn't think diagonal pixels are separate contours just_cycles = erode(just_cycles, 2, 1) # Use pcv.find_objects to turn plots of holes into countable contours cycle_objects, cycle_hierarchies = find_objects(just_cycles, just_cycles) # Count the number of holes num_cycles = len(cycle_objects) # Make debugging image cycle_img = skel_img.copy() cycle_img = dilate(cycle_img, params.line_thickness, 1) cycle_img = cv2.cvtColor(cycle_img, cv2.COLOR_GRAY2RGB) rand_color = color_palette(num_cycles) for i, cnt in enumerate(cycle_objects): cv2.drawContours(cycle_img, cycle_objects, i, rand_color[i], params.line_thickness, lineType=8, hierarchy=cycle_hierarchies) # Store Cycle Data outputs.add_observation(variable='num_cycles', trait='number of cycles', method='plantcv.plantcv.morphology.check_cycles', scale='none', datatype=int, value=num_cycles, label='none') # Reset debug mode params.debug = debug # Auto-increment device params.device += 1 if params.debug == 'print': print_image(cycle_img, os.path.join(params.debug_outdir, str(params.device) + '_cycles.png')) elif params.debug == 'plot': plot_image(cycle_img) return cycle_img