def watershed_segmentation(rgb_img, mask, distance=10): """Uses the watershed algorithm to detect boundary of objects. Needs a marker file which specifies area which is object (white), background (grey), unknown area (black). Inputs: rgb_img = image to perform watershed on needs to be 3D (i.e. np.shape = x,y,z not np.shape = x,y) mask = binary image, single channel, object in white and background black distance = min_distance of local maximum Returns: analysis_images = list of output images :param rgb_img: numpy.ndarray :param mask: numpy.ndarray :param distance: int :return analysis_images: list """ params.device += 1 # Store debug mode debug = params.debug params.debug = None dist_transform = cv2.distanceTransformWithLabels(mask, cv2.DIST_L2, maskSize=0)[0] localMax = peak_local_max(dist_transform, indices=False, min_distance=distance, labels=mask) markers = ndi.label(localMax, structure=np.ones((3, 3)))[0] dist_transform1 = -dist_transform labels = watershed(dist_transform1, markers, mask=mask) img1 = np.copy(rgb_img) for x in np.unique(labels): rand_color = color_palette(len(np.unique(labels))) img1[labels == x] = rand_color[x] img2 = apply_mask(img1, mask, 'black') joined = np.concatenate((img2, rgb_img), axis=1) estimated_object_count = len(np.unique(markers)) - 1 # Reset debug mode params.debug = debug if params.debug == 'print': print_image(dist_transform, os.path.join(params.debug_outdir, str(params.device) + '_watershed_dist_img.png')) print_image(joined, os.path.join(params.debug_outdir, str(params.device) + '_watershed_img.png')) elif params.debug == 'plot': plot_image(dist_transform, cmap='gray') plot_image(joined) outputs.add_observation(variable='estimated_object_count', trait='estimated object count', method='plantcv.plantcv.watershed', scale='none', datatype=int, value=estimated_object_count, label='none') # Store images outputs.images.append([dist_transform, joined]) return joined
def watershed_segmentation(rgb_img, mask, distance=10): """Uses the watershed algorithm to detect boundary of objects. Needs a marker file which specifies area which is object (white), background (grey), unknown area (black). Inputs: rgb_img = image to perform watershed on needs to be 3D (i.e. np.shape = x,y,z not np.shape = x,y) mask = binary image, single channel, object in white and background black distance = min_distance of local maximum Returns: analysis_images = list of output images :param rgb_img: numpy.ndarray :param mask: numpy.ndarray :param distance: int :return analysis_images: list """ params.device += 1 # # Will be depricating opencv version 2 # if cv2.__version__[0] == '2': # dist_transform = cv2.distanceTransform(mask, cv2.cv.CV_DIST_L2, maskSize=0) # else: dist_transform = cv2.distanceTransformWithLabels(mask, cv2.DIST_L2, maskSize=0)[0] localMax = peak_local_max(dist_transform, indices=False, min_distance=distance, labels=mask) markers = ndi.label(localMax, structure=np.ones((3, 3)))[0] dist_transform1 = -dist_transform labels = watershed(dist_transform1, markers, mask=mask) img1 = np.copy(rgb_img) for x in np.unique(labels): rand_color = color_palette(len(np.unique(labels))) img1[labels == x] = rand_color[x] img2 = apply_mask(img1, mask, 'black') joined = np.concatenate((img2, rgb_img), axis=1) estimated_object_count = len(np.unique(markers)) - 1 analysis_image = [] analysis_image.append(joined) if params.debug == 'print': print_image(dist_transform, os.path.join(params.debug_outdir, str(params.device) + '_watershed_dist_img.png')) print_image(joined, os.path.join(params.debug_outdir, str(params.device) + '_watershed_img.png')) elif params.debug == 'plot': plot_image(dist_transform, cmap='gray') plot_image(joined) outputs.add_observation(variable='estimated_object_count', trait='estimated object count', method='plantcv.plantcv.watershed', scale='none', datatype=int, value=estimated_object_count, label='none') # Store images outputs.images.append(analysis_image) return analysis_image
def segment_id(skel_img, objects, hierarchies, mask=None): """ Plot segment ID's 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: segmented_img = Segmented image labeled_img = Labeled image :param skel_img: numpy.ndarray :param objects: list :param hierarchies: numpy.ndarray :param mask: numpy.ndarray :return segmented_img: numpy.ndarray :return labeled_img: numpy.ndarray """ label_coord_x = [] label_coord_y = [] if mask is None: segmented_img = skel_img.copy() else: segmented_img = mask.copy() segmented_img = cv2.cvtColor(segmented_img, cv2.COLOR_GRAY2RGB) # Color each segment a different color rand_color = color_palette(len(objects)) # Plot all segment contours for i, cnt in enumerate(objects): cv2.drawContours(segmented_img, objects, i, rand_color[i], params.line_thickness, lineType=8, hierarchy=hierarchies) # Store coordinates for labels label_coord_x.append(objects[i][0][0][0]) label_coord_y.append(objects[i][0][0][1]) labeled_img = segmented_img.copy() for i, cnt in enumerate(objects): # Label slope lines w = label_coord_x[i] h = label_coord_y[i] text = "ID:{}".format(i) cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=.65, color=rand_color[i], thickness=2) # Auto-increment device params.device += 1 if params.debug == 'print': print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segmented_ids.png')) elif params.debug == 'plot': plot_image(labeled_img) return segmented_img, labeled_img
def segment_id(skel_img, objects, mask=None): """ Plot segment ID's 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: segmented_img = Segmented image labeled_img = Labeled image :param skel_img: numpy.ndarray :param objects: list :param mask: numpy.ndarray :return segmented_img: numpy.ndarray :return labeled_img: numpy.ndarray """ label_coord_x = [] label_coord_y = [] if mask is None: segmented_img = skel_img.copy() else: segmented_img = mask.copy() segmented_img = cv2.cvtColor(segmented_img, cv2.COLOR_GRAY2RGB) # Color each segment a different color rand_color = color_palette(len(objects)) # Plot all segment contours for i, cnt in enumerate(objects): cv2.drawContours(segmented_img, objects, i, rand_color[i], params.line_thickness, lineType=8) # Store coordinates for labels label_coord_x.append(objects[i][0][0][0]) label_coord_y.append(objects[i][0][0][1]) labeled_img = segmented_img.copy() for i, cnt in enumerate(objects): # Label slope lines w = label_coord_x[i] h = label_coord_y[i] text = "ID:{}".format(i) cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=params.text_size, color=rand_color[i], thickness=params.text_thickness) # Auto-increment device params.device += 1 if params.debug == 'print': print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segmented_ids.png')) elif params.debug == 'plot': plot_image(labeled_img) return segmented_img, 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 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 fill_segments(mask, objects): """Fills masked segments from contours. Inputs: mask = Binary image, single channel, object = 1 and background = 0 objects = List of contours Returns: filled_img = Filled mask :param mask: numpy.ndarray :param object: list :return filled_img: numpy.ndarray """ params.device += 1 h, w = mask.shape markers = np.zeros((h, w)) labels = np.arange(len(objects)) + 1 for i, l in enumerate(labels): cv2.drawContours(markers, objects, i, int(l), 5) # Fill as a watershed segmentation from contours as markers filled_mask = watershed(mask == 0, markers=markers, mask=mask != 0, compactness=0) # Count area in pixels of each segment ids, counts = np.unique(filled_mask, return_counts=True) outputs.add_observation(variable='segment_area', trait='segment area', method='plantcv.plantcv.morphology.fill_segments', scale='pixels', datatype=list, value=counts[1:].tolist(), label=(ids[1:] - 1).tolist()) rgb_vals = color_palette(num=len(labels)) filled_img = np.zeros((h, w, 3), dtype=np.uint8) for l in labels: for ch in range(3): filled_img[:, :, ch][filled_mask == l] = rgb_vals[l - 1][ch] if params.debug == 'print': print_image( filled_img, os.path.join(params.debug_outdir, str(params.device) + '_filled_img.png')) elif params.debug == 'plot': plot_image(filled_img) return filled_img
def main(): # Get options args = options() debug = args.debug # Read image img, path, filename = pcv.readimage(args.image) # Pipeline step device = 0 device, img1 = pcv.white_balance(device, img, debug, (100, 100, 1000, 1000)) img = img1 #seedmask, path1, filename1 = pcv.readimage(args.mask) #device, seedmask = pcv.rgb2gray(seedmask, device, debug) #device, inverted = pcv.invert(seedmask, device, debug) #device, masked_img = pcv.apply_mask(img, inverted, 'white', device, debug) device, img_gray_sat = pcv.rgb2gray_hsv(img1, 's', device, debug) device, img_binary = pcv.binary_threshold(img_gray_sat, 70, 255, 'light', device, debug) img_binary1 = np.copy(img_binary) device, fill_image = pcv.fill(img_binary1, img_binary, 300, device, debug) device, seed_objects, seed_hierarchy = pcv.find_objects( img, fill_image, device, debug) device, roi1, roi_hierarchy1 = pcv.define_roi(img, 'rectangle', device, None, 'default', debug, True, 1500, 1000, -1000, -500) device, roi_objects, roi_obj_hierarchy, kept_mask, obj_area = pcv.roi_objects( img, 'partial', roi1, roi_hierarchy1, seed_objects, seed_hierarchy, device, debug) img_copy = np.copy(img) for i in range(0, len(roi_objects)): rand_color = pcv.color_palette(1) cv2.drawContours(img_copy, roi_objects, i, rand_color[0], -1, lineType=8, hierarchy=roi_obj_hierarchy) pcv.print_image( img_copy, os.path.join(args.outdir, filename[:-4]) + "-seed-confetti.jpg") shape_header = [] # Store the table header table = [] # Store the PlantCV measurements for each seed in a table for i in range(0, len(roi_objects)): if roi_obj_hierarchy[0][i][ 3] == -1: # Only continue if the object is an outermost contour # Object combine kept objects # Inputs: # contours = object list # device = device number. Used to count steps in the pipeline # debug = None, print, or plot. Print = save to file, Plot = print to screen. device, obj, mask = pcv.object_composition( img, [roi_objects[i]], np.array([[roi_obj_hierarchy[0][i]]]), device, None) if obj is not None: # Measure the area and other shape properties of each seed # Inputs: # img = image object (most likely the original), color(RGB) # imgname = name of image # obj = single or grouped contour object # device = device number. Used to count steps in the pipeline # debug = None, print, or plot. Print = save to file, Plot = print to screen. # filename = False or image name. If defined print image device, shape_header, shape_data, shape_img = pcv.analyze_object( img, "img", obj, mask, device, None) if shape_data is not None: table.append(shape_data[1]) data_array = np.array(table) maxval = np.argmax(data_array) maxseed = np.copy(img) cv2.drawContours(maxseed, roi_objects, maxval, (0, 255, 0), 10) imgtext = "This image has " + str(len(data_array)) + " seeds" sizeseed = "The largest seed is in green and is " + str( data_array[maxval]) + " pixels" cv2.putText(maxseed, imgtext, (500, 300), cv2.FONT_HERSHEY_SIMPLEX, 5, (0, 0, 0), 10) cv2.putText(maxseed, sizeseed, (500, 600), cv2.FONT_HERSHEY_SIMPLEX, 5, (0, 0, 0), 10) pcv.print_image(maxseed, os.path.join(args.outdir, filename[:-4]) + "-maxseed.jpg")
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 watershed_segmentation(rgb_img, mask, distance=10): """Uses the watershed algorithm to detect boundary of objects. Needs a marker file which specifies area which is object (white), background (grey), unknown area (black). Inputs: rgb_img = image to perform watershed on needs to be 3D (i.e. np.shape = x,y,z not np.shape = x,y) mask = binary image, single channel, object in white and background black distance = min_distance of local maximum Returns: watershed_header = shape data table headers watershed_data = shape data table values analysis_images = list of output images :param rgb_img: numpy.ndarray :param mask: numpy.ndarray :param distance: int :return watershed_header: list :return watershed_data: list :return analysis_images: list """ params.device += 1 # # Will be depricating opencv version 2 # if cv2.__version__[0] == '2': # dist_transform = cv2.distanceTransform(mask, cv2.cv.CV_DIST_L2, maskSize=0) # else: dist_transform = cv2.distanceTransformWithLabels(mask, cv2.DIST_L2, maskSize=0)[0] localMax = peak_local_max(dist_transform, indices=False, min_distance=distance, labels=mask) markers = ndi.label(localMax, structure=np.ones((3, 3)))[0] dist_transform1 = -dist_transform labels = watershed(dist_transform1, markers, mask=mask) img1 = np.copy(rgb_img) for x in np.unique(labels): rand_color = color_palette(len(np.unique(labels))) img1[labels == x] = rand_color[x] img2 = apply_mask(img1, mask, 'black') joined = np.concatenate((img2, rgb_img), axis=1) estimated_object_count = len(np.unique(markers)) - 1 analysis_image = [] analysis_image.append(joined) watershed_header = ( 'HEADER_WATERSHED', 'estimated_object_count' ) watershed_data = ( 'WATERSHED_DATA', estimated_object_count ) if params.debug == 'print': print_image(dist_transform, os.path.join(params.debug_outdir, str(params.device) + '_watershed_dist_img.png')) print_image(joined, os.path.join(params.debug_outdir, str(params.device) + '_watershed_img.png')) elif params.debug == 'plot': plot_image(dist_transform, cmap='gray') plot_image(joined) # Store into global measurements if not 'watershed' in outputs.measurements: outputs.measurements['watershed'] = {} outputs.measurements['watershed']['estimated_object_count'] = estimated_object_count # Store images outputs.images.append(analysis_image) return watershed_header, watershed_data, analysis_image
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 cluster_contours(img, roi_objects, roi_obj_hierarchy, nrow=1, ncol=1, show_grid=False): """ This function take a image with multiple contours and clusters them based on user input of rows and columns Inputs: img = RGB or grayscale image data for plotting 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 the grid will get plot to show how plants are being clustered Returns: grouped_contour_indexes = contours grouped contours = All inputed contours :param img: numpy.ndarray :param roi_objects: list :param nrow: int :param ncol: int :param show_grid: bool :return grouped_contour_indexes: list :return contours: list :return roi_obj_hierarchy: list """ params.device += 1 if len(np.shape(img)) == 3: iy, ix, iz = np.shape(img) else: iy, ix, = np.shape(img) # get the break groups if nrow == 1: rbreaks = [0, iy] else: rstep = np.rint(iy / nrow) rstep1 = np.int(rstep) rbreaks = range(0, iy, rstep1) if ncol == 1: cbreaks = [0, ix] else: cstep = np.rint(ix / ncol) cstep1 = np.int(cstep) cbreaks = range(0, ix, cstep1) # categorize what bin the center of mass of each contour def digitize(a, step): # The way cbreaks and rbreaks are calculated, step will never be an integer # if isinstance(step, int): # i = step # else: i = len(step) for x in range(0, i): if x == 0: if a >= 0 and a < step[x + 1]: return x + 1 elif a >= step[x - 1] and a < step[x]: return x elif a > step[x - 1] and a > np.max(step): return i dtype = [('cx', int), ('cy', int), ('rowbin', int), ('colbin', int), ('index', int)] coord = [] for i in range(0, len(roi_objects)): m = cv2.moments(roi_objects[i]) if m['m00'] == 0: pass else: cx = int(m['m10'] / m['m00']) cy = int(m['m01'] / m['m00']) # colbin = np.digitize(cx, cbreaks) # rowbin = np.digitize(cy, rbreaks) colbin = digitize(cx, cbreaks) rowbin = digitize(cy, rbreaks) a = (cx, cy, colbin, rowbin, i) coord.append(a) coord1 = np.array(coord, dtype=dtype) coord2 = np.sort(coord1, order=('colbin', 'rowbin')) # get the list of unique coordinates and group the contours with the same bin coordinates groups = [] for i, y in enumerate(coord2): col = y[3] row = y[2] location = str(row) + ',' + str(col) groups.append(location) unigroup = np.unique(groups) coordgroups = [] for i, y in enumerate(unigroup): col = int(y[0]) row = int(y[2]) for a, b in enumerate(coord2): if b[2] == col and b[3] == row: grp = i contour = b[4] coordgroups.append((grp, contour)) else: pass coordlist = [[y[1] for y in coordgroups if y[0] == x] for x in range(0, (len(unigroup)))] contours = roi_objects grouped_contour_indexes = coordlist # Debug image is rainbow printed contours if params.debug is not None: if len(np.shape(img)) == 3: img_copy = np.copy(img) else: iy, ix = np.shape(img) img_copy = np.zeros((iy, ix, 3), dtype=np.uint8) rand_color = color_palette(len(coordlist)) for i, x in enumerate(coordlist): for a in x: if roi_obj_hierarchy[0][a][3] > -1: pass else: cv2.drawContours(img_copy, roi_objects, a, rand_color[i], -1, hierarchy=roi_obj_hierarchy) if show_grid: for y in rbreaks: cv2.line(img_copy, (0, y), (ix, y), (255, 0, 0), params.line_thickness) for x in cbreaks: cv2.line(img_copy, (x, 0), (x, iy), (255, 0, 0), params.line_thickness) if params.debug=='print': print_image(img_copy, os.path.join(params.debug_outdir, str(params.device) + '_clusters.png')) elif params.debug=='plot': plot_image(img_copy) return grouped_contour_indexes, contours, roi_obj_hierarchy
def watershed_segmentation(device, img, mask, distance=10, filename=False, debug=None): """Uses the watershed algorithm to detect boundary of objects. Needs a marker file which specifies area which is object (white), background (grey), unknown area (black). Inputs: device = device number. Used to count steps in the pipeline img = image to perform watershed on needs to be 3D (i.e. np.shape = x,y,z not np.shape = x,y) mask = binary image, single channel, object in white and background black distance = min_distance of local maximum filename = if user wants to output analysis images change filenames from false debug = None, print, or plot. Print = save to file, Plot = print to screen. Returns: device = device number watershed_header = shape data table headers watershed_data = shape data table values analysis_images = list of output images :param device: int :param img: numpy array :param mask: numpy array :param distance: int :param filename: str :param debug: str :return device: int :return watershed_header: list :return watershed_data: list :return analysis_images: list """ if cv2.__version__[0] == '2': dist_transform = cv2.distanceTransform(mask, cv2.cv.CV_DIST_L2, maskSize=0) else: dist_transform = cv2.distanceTransformWithLabels(mask, cv2.DIST_L2, maskSize=0)[0] localMax = peak_local_max(dist_transform, indices=False, min_distance=distance, labels=mask) markers = ndi.label(localMax, structure=np.ones((3, 3)))[0] dist_transform1 = -dist_transform labels = watershed(dist_transform1, markers, mask=mask) img1 = np.copy(img) for x in np.unique(labels): rand_color = color_palette(len(np.unique(labels))) img1[labels == x] = rand_color[x] device, img2 = apply_mask(img1, mask, 'black', device, debug=None) joined = np.concatenate((img2, img), axis=1) estimated_object_count = len(np.unique(markers)) - 1 analysis_images = [] if filename != False: out_file = str(filename[0:-4]) + '_watershed.jpg' print_image(joined, out_file) analysis_images.append(['IMAGE', 'watershed', out_file]) watershed_header = ('HEADER_WATERSHED', 'estimated_object_count') watershed_data = ('WATERSHED_DATA', estimated_object_count) if debug == 'print': print_image(dist_transform, str(device) + '_watershed_dist_img.png') print_image(joined, str(device) + '_watershed_img.png') elif debug == 'plot': plot_image(dist_transform, cmap='gray') plot_image(joined) return device, watershed_header, watershed_data, analysis_images
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 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 clustered_contours(img, grouped_contour_indices, roi_objects, roi_obj_hierarchy, nrow=1, ncol=1): """ This function takes the outputs from cluster_contours Inputs: img = RGB or grayscale image data for plotting grouped_contour_indices = Indices for grouping contours roi_objects = object contours in an image that are needed to be clustered. roi_obj_hierarchy = object hierarchy nrow = Optional, number of rows. If changed from default, grid gets plot. ncol = Optional, number of columns. If changed from default, grid gets plot. Returns: clustered_image = Labeled clusters image :param img: numpy.ndarray :param grouped_contour_indices: list :param roi_objects: list :param roi_obj_hierarchy: numpy.ndarray :param nrow: int :param ncol: int :return clustered_image: numpy.ndarray """ clustered_image = np.copy(img) iy, ix = np.shape(img)[:2] # Gray input images need to get converted to RGB for plotting colors if len(np.shape(img)) == 2: clustered_image = cv2.cvtColor(clustered_image, cv2.COLOR_GRAY2RGB) # Plot grid if nrow or ncol are changed from the default if nrow > 1 or ncol > 1: rbreaks = range(0, iy, int(np.rint(iy / nrow))) cbreaks = range(0, ix, int(np.rint(ix / ncol))) for y in rbreaks: cv2.line(clustered_image, (0, y), (ix, y), (255, 0, 0), params.line_thickness) for x in cbreaks: cv2.line(clustered_image, (x, 0), (x, iy), (255, 0, 0), params.line_thickness) rand_color = color_palette(len(grouped_contour_indices)) grouped_contours = [] for i, x in enumerate(grouped_contour_indices): for a in x: if roi_obj_hierarchy[0][a][3] > -1: pass else: cv2.drawContours(clustered_image, roi_objects, a, rand_color[i], -1, hierarchy=roi_obj_hierarchy) # Add contour to list to get grouped grouped_contours.append(roi_objects[a]) if len(grouped_contours) > 0: # Combine contours into a single contour grouped_contours = np.vstack(grouped_contours) # Plot the bounding circle around the contours that got grouped together center, radius = cv2.minEnclosingCircle(points=grouped_contours) cv2.circle(img=clustered_image, center=(int(center[0]), int(center[1])), radius=int(radius), color=rand_color[i], thickness=params.line_thickness, lineType=8) # Label the cluster ID cv2.putText(img=clustered_image, text=str(i), org=(int(center[0]), int(center[1])), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=params.text_size, color=(200, 200, 200), thickness=params.text_thickness) # Empty the grouped_contours list for the next group grouped_contours = [] params.device += 1 if params.debug == 'print': print_image( clustered_image, os.path.join(params.debug_outdir, str(params.device) + '_clusters.png')) elif params.debug == 'plot': plot_image(clustered_image) return clustered_image
def segment_combine(segment_list, objects, mask): """ Combine user specified segments together Inputs: segment_list = List of segments to get combined, or list of lists of segments to get combined objects = List of contours hierarchy = Contour hierarchy NumPy array mask = Binary mask for debugging image Returns: segmented_img = Segmented image objects = Updated list of contours hierarchy = Updated contour hierarchy NumPy array :param segment_list: list :param objects: list :param mask: numpy.ndarray :return labeled_img: numpy.ndarray :return objects: list """ label_coord_x = [] label_coord_y = [] all_objects = objects[:] # If user provides a single list of objects to combine if type(segment_list[0]) is int: num_contours = len(segment_list) count = 1 # Store the first object into the new object array new_objects = all_objects[segment_list[0]] # Remove the objects getting combined from the list of all objects all_objects.remove(objects[segment_list[0]]) while count < num_contours: # Combine objects into a single array new_objects = np.append(new_objects, objects[segment_list[count]], 0) # Remove the objects getting combined from the list of all objects all_objects.remove(objects[segment_list[count]]) count += 1 # Replace with the combined object all_objects.append(new_objects) # If user provides a list of lists of objects to combine elif type(segment_list[0]) is list: # For each list provided for lists in segment_list: num_contours = len(lists) count = 1 # Store the first object into the new object array new_objects = all_objects[lists[0]] # Remove the objects getting combined from the list of all objects all_objects.remove(objects[lists[0]]) while count < num_contours: # Combine objects into a single array new_objects = np.append(new_objects, objects[lists[count]], 0) # Remove the objects getting combined from the list of all objects all_objects.remove(objects[lists[count]]) count += 1 # Add combined contour to list of all contours all_objects.append(new_objects) else: fatal_error( "segment_list must be a list of object ID's or a list of lists of ID's!" ) labeled_img = mask.copy() labeled_img = cv2.cvtColor(labeled_img, cv2.COLOR_GRAY2RGB) # Color each segment a different color, use a previously saved scale if available rand_color = color_palette(num=len(all_objects), saved=True) # Plot all segment contours for i, cnt in enumerate(all_objects): cv2.drawContours(labeled_img, all_objects[i], -1, rand_color[i], params.line_thickness, lineType=8) # Store coordinates for labels label_coord_x.append(all_objects[i][0][0][0]) label_coord_y.append(all_objects[i][0][0][1]) # Label segments for i, cnt in enumerate(all_objects): w = label_coord_x[i] h = label_coord_y[i] text = "ID:{}".format(i) cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=params.text_size, color=rand_color[i], thickness=2) # Auto-increment device params.device += 1 if params.debug == 'print': print_image( labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_combined_segment_ids.png')) elif params.debug == 'plot': plot_image(labeled_img) return labeled_img, all_objects
def cluster_contours(img, roi_objects, roi_obj_hierarchy, nrow=1, ncol=1, show_grid=False): """ This function take a image with multiple contours and clusters them based on user input of rows and columns Inputs: img = RGB or grayscale image data for plotting 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 the grid will get plot to show how plants are being clustered Returns: grouped_contour_indexes = contours grouped contours = All inputed contours :param img: numpy.ndarray :param roi_objects: list :param nrow: int :param ncol: int :param show_grid: bool :return grouped_contour_indexes: list :return contours: list :return roi_obj_hierarchy: list """ params.device += 1 if len(np.shape(img)) == 3: iy, ix, iz = np.shape(img) else: iy, ix, = np.shape(img) # get the break groups if nrow == 1: rbreaks = [0, iy] else: rstep = np.rint(iy / nrow) rstep1 = np.int(rstep) rbreaks = range(0, iy, rstep1) if ncol == 1: cbreaks = [0, ix] else: cstep = np.rint(ix / ncol) cstep1 = np.int(cstep) cbreaks = range(0, ix, cstep1) # categorize what bin the center of mass of each contour def digitize(a, step): # The way cbreaks and rbreaks are calculated, step will never be an integer # if isinstance(step, int): # i = step # else: i = len(step) for x in range(0, i): if x == 0: if a >= 0 and a < step[x + 1]: return x + 1 elif a >= step[x - 1] and a < step[x]: return x elif a > step[x - 1] and a > np.max(step): return i dtype = [('cx', int), ('cy', int), ('rowbin', int), ('colbin', int), ('index', int)] coord = [] for i in range(0, len(roi_objects)): m = cv2.moments(roi_objects[i]) if m['m00'] == 0: pass else: cx = int(m['m10'] / m['m00']) cy = int(m['m01'] / m['m00']) # colbin = np.digitize(cx, cbreaks) # rowbin = np.digitize(cy, rbreaks) colbin = digitize(cx, cbreaks) rowbin = digitize(cy, rbreaks) a = (cx, cy, colbin, rowbin, i) coord.append(a) coord1 = np.array(coord, dtype=dtype) coord2 = np.sort(coord1, order=('colbin', 'rowbin')) # get the list of unique coordinates and group the contours with the same bin coordinates groups = [] for i, y in enumerate(coord2): col = y[3] row = y[2] location = str(row) + ',' + str(col) groups.append(location) unigroup = np.unique(groups) coordgroups = [] for i, y in enumerate(unigroup): col = int(y[0]) row = int(y[2]) for a, b in enumerate(coord2): if b[2] == col and b[3] == row: grp = i contour = b[4] coordgroups.append((grp, contour)) else: pass coordlist = [[y[1] for y in coordgroups if y[0] == x] for x in range(0, (len(unigroup)))] contours = roi_objects grouped_contour_indexes = coordlist # Debug image is rainbow printed contours if params.debug is not None: if len(np.shape(img)) == 3: img_copy = np.copy(img) else: iy, ix = np.shape(img) img_copy = np.zeros((iy, ix, 3), dtype=np.uint8) rand_color = color_palette(len(coordlist)) for i, x in enumerate(coordlist): for a in x: if roi_obj_hierarchy[0][a][3] > -1: pass else: cv2.drawContours(img_copy, roi_objects, a, rand_color[i], -1, hierarchy=roi_obj_hierarchy) if show_grid: for y in rbreaks: cv2.line(img_copy, (0, y), (ix, y), (255, 0, 0), params.line_thickness) for x in cbreaks: cv2.line(img_copy, (x, 0), (x, iy), (255, 0, 0), params.line_thickness) if params.debug == 'print': print_image( img_copy, os.path.join(params.debug_outdir, str(params.device) + '_clusters.png')) elif params.debug == 'plot': plot_image(img_copy) return grouped_contour_indexes, contours, roi_obj_hierarchy
def warp(img, refimg, pts, refpts, method='default'): """Warp an image to another perspective Inputs: img = grayscale or binary image data to be warped refimg = RGB or grayscale image data to be used as reference pts = 4 coordinates on img1 refpts = 4 coordinates on img2 method = method of finding the transformation. 'default', 'ransac', 'lmeds', 'rho' Returns: warped_img = warped image :param img: numpy.ndarray :param refimg: numpy.ndarray :param pts: list of tuples :param refpts: list of tuples :param method: str :return warped_img: numpy.ndarray """ params.device += 1 if len(pts) != 4 or len(refpts) != 4: fatal_error('Please provide 4 pairs of corresponding coordinates.') if len(img.shape) > 2: fatal_error('The input `img` should be grayscale or binary.') methods = { 'default': 0, 'ransac': cv2.RANSAC, 'lmeds': cv2.LMEDS, 'rho': cv2.RHO } shape_ref = refimg.shape rows_ref, cols_ref = shape_ref[0:2] # convert list of tuples to array for cv2 functions ptsarr = np.array(pts, dtype='float32') refptsarr = np.array(refpts, dtype='float32') # find tranformation matrix and warp mat, _ = cv2.findHomography(ptsarr, refptsarr, method=methods.get(method)) warped_img = cv2.warpPerspective(src=img, M=mat, dsize=(cols_ref, rows_ref)) # preserve binary if len(np.unique(img)) == 2: warped_img[warped_img > 0] = 255 if params.debug is not None: # scale marker_size and line_thickness for different resolutions rows_img = img.shape[0] if rows_img > rows_ref: res_ratio_i = int( np.ceil(rows_img / rows_ref)) # ratio never smaller than 1 with np.ceil res_ratio_r = 1 else: res_ratio_r = int(np.ceil(rows_ref / rows_img)) res_ratio_i = 1 # marker colors colors = color_palette(len(pts)) # temp rgb image for colored markers on img img2 = img.copy() img2 = cv2.merge((img2, img2, img2)) for i, pt in enumerate(pts): cv2.drawMarker(img2, pt, color=colors[i], markerType=cv2.MARKER_CROSS, markerSize=params.marker_size * res_ratio_i, thickness=params.line_thickness * res_ratio_i) # temp rgb image for colored markers on refimg refimg2 = refimg.copy() if len(shape_ref) == 2: refimg2 = cv2.merge((refimg2, refimg2, refimg2)) for i, pt in enumerate(refpts): cv2.drawMarker(refimg2, pt, color=colors[i], markerType=cv2.MARKER_CROSS, markerSize=params.marker_size * res_ratio_r, thickness=params.line_thickness * res_ratio_r) debug_mode = params.debug params.debug = None img_blend = overlay_two_imgs(warped_img, refimg) params.debug = debug_mode if params.debug == 'plot': plot_image(img2) plot_image(refimg2) plot_image(img_blend) if params.debug == 'print': print_image( img2, os.path.join(params.debug_outdir, str(params.device) + "_img-to-warp.png")) print_image( refimg2, os.path.join(params.debug_outdir, str(params.device) + "_img-ref.png")) print_image( img_blend, os.path.join(params.debug_outdir, str(params.device) + "_warp_overlay.png")) return warped_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) 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 segment_insertion_angle(skel_img, segmented_img, leaf_objects, leaf_hierarchies, stem_objects, size): """ Find leaf insertion angles in degrees of skeleton segments. Fit a linear regression line to the stem. Use `size` pixels on the portion of leaf next to the stem find a linear regression line, and calculate angle between the two lines per leaf object. Inputs: skel_img = Skeletonized image segmented_img = Segmented image to plot slope lines and intersection angles on leaf_objects = List of leaf segments leaf_hierarchies = Leaf contour hierarchy NumPy array stem_objects = List of stem segments size = Size of inner leaf used to calculate slope lines Returns: insertion_angle_header = Leaf insertion angle headers insertion_angle_data = Leaf insertion angle values labeled_img = Debugging image with angles labeled :param skel_img: numpy.ndarray :param segmented_img: numpy.ndarray :param leaf_objects: list :param leaf_hierarchies: numpy.ndarray :param stem_objects: list :param size: int :return insertion_angle_header: list :return insertion_angle_data: list :return labeled_img: numpy.ndarray """ # Store debug debug = params.debug params.debug = None rows, cols = segmented_img.shape[:2] labeled_img = segmented_img.copy() segment_slopes = [] insertion_segments = [] insertion_hierarchies = [] intersection_angles = [] label_coord_x = [] label_coord_y = [] # Create a list of tip tuples to use for sorting tips = find_tips(skel_img) tip_objects, tip_hierarchies = find_objects(tips, tips) tip_tuples = [] for i, cnt in enumerate(tip_objects): tip_tuples.append((cnt[0][0][0], cnt[0][0][1])) rand_color = color_palette(len(leaf_objects)) for i, cnt in enumerate(leaf_objects): # Draw leaf objects find_segment_tangents = np.zeros(segmented_img.shape[:2], np.uint8) cv2.drawContours(find_segment_tangents, leaf_objects, i, 255, 1, lineType=8, hierarchy=leaf_hierarchies) cv2.drawContours(labeled_img, leaf_objects, i, rand_color[i], params.line_thickness, lineType=8, hierarchy=leaf_hierarchies) # Prune back ends of leaves pruned_segment = prune(find_segment_tangents, size) # Segment ends are the portions pruned off segment_ends = find_segment_tangents - pruned_segment segment_end_obj, segment_end_hierarchy = find_objects( segment_ends, segment_ends) is_insertion_segment = [] if not len(segment_end_obj) == 2: print("Size too large, contour with ID#", i, "got pruned away completely.") else: # Determine if a segment is leaf end or leaf insertion segment for j, obj in enumerate(segment_end_obj): cnt_as_tuples = [] num_pixels = len(obj) count = 0 # Turn each contour into a list of tuples (can't search for list of coords, so reformat) while num_pixels > count: x_coord = obj[count][0][0] y_coord = obj[count][0][1] cnt_as_tuples.append((x_coord, y_coord)) count += 1 for tip_tups in tip_tuples: # If a tip is inside the list of contour tuples then it is a leaf end segment if tip_tups in cnt_as_tuples: is_insertion_segment.append(False) else: is_insertion_segment.append(True) # If none of the tips are within a segment_end then it's an insertion segment if all(is_insertion_segment): insertion_segments.append(segment_end_obj[j]) insertion_hierarchies.append(segment_end_hierarchy[0][j]) # Store coordinates for labels label_coord_x.append(leaf_objects[i][0][0][0]) label_coord_y.append(leaf_objects[i][0][0][1]) # Plot stem segments stem_img = np.zeros(segmented_img.shape[:2], np.uint8) cv2.drawContours(stem_img, stem_objects, -1, 255, 2, lineType=8) branch_pts = find_branch_pts(skel_img) stem_img = stem_img + branch_pts stem_img = closing(stem_img) combined_stem, combined_stem_hier = find_objects(stem_img, stem_img) # Make sure stem objects are a single contour while len(combined_stem) > 1: stem_img = dilate(stem_img, 2, 1) stem_img = closing(stem_img) combined_stem, combined_stem_hier = find_objects(stem_img, stem_img) # Find slope of the stem [vx, vy, x, y] = cv2.fitLine(combined_stem[0], cv2.DIST_L2, 0, 0.01, 0.01) stem_slope = -vy / vx stem_slope = stem_slope[0] lefty = int((-x * vy / vx) + y) righty = int(((cols - x) * vy / vx) + y) cv2.line(labeled_img, (cols - 1, righty), (0, lefty), (150, 150, 150), 3) for t, segment in enumerate(insertion_segments): # Find line fit to each segment [vx, vy, x, y] = cv2.fitLine(segment, cv2.DIST_L2, 0, 0.01, 0.01) slope = -vy / vx left_list = int((-x * vy / vx) + y) right_list = int(((cols - x) * vy / vx) + y) segment_slopes.append(slope[0]) # Draw slope lines if possible if slope > 1000000 or slope < -1000000: print("Slope of contour with ID#", t, "is", slope, "and cannot be plotted.") else: cv2.line(labeled_img, (cols - 1, right_list), (0, left_list), rand_color[t], 1) # Store intersection angles between insertion segment and stem line intersection_angle = _slope_to_intesect_angle(slope[0], stem_slope) # Function measures clockwise but we want the acute angle between stem and leaf insertion if intersection_angle > 90: intersection_angle = 180 - intersection_angle intersection_angles.append(intersection_angle) insertion_angle_header = ['HEADER_INSERTION_ANGLE'] insertion_angle_data = ['INSERTION_ANGLE_DATA'] for i, cnt in enumerate(insertion_segments): # Label slope lines w = label_coord_x[i] h = label_coord_y[i] text = "{:.2f}".format(intersection_angles[i]) cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=.55, color=(150, 150, 150), thickness=2) segment_label = "ID" + str(i) insertion_angle_header.append(segment_label) insertion_angle_data.extend(intersection_angles) if 'morphology_data' not in outputs.measurements: outputs.measurements['morphology_data'] = {} outputs.measurements['morphology_data'][ 'segment_insertion_angles'] = intersection_angles # Reset debug mode params.debug = debug # Auto-increment device params.device += 1 if params.debug == 'print': print_image( labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segment_insertion_angles.png')) elif params.debug == 'plot': plot_image(labeled_img) return insertion_angle_header, insertion_angle_data, labeled_img
def fill_segments(mask, objects, stem_objects=None, label="default"): """Fills masked segments from contours. Inputs: mask = Binary image, single channel, object = 1 and background = 0 objects = List of contours Returns: filled_img = Filled mask :param mask: numpy.ndarray :param objects: list :param stem_objects: numpy.ndarray :param label: str :return filled_img: numpy.ndarray """ h, w = mask.shape markers = np.zeros((h, w)) objects_unique = objects.copy() if stem_objects is not None: objects_unique.append(np.vstack(stem_objects)) labels = np.arange(len(objects_unique)) + 1 for i, l in enumerate(labels): cv2.drawContours(markers, objects_unique, i, int(l), 5) # Fill as a watershed segmentation from contours as markers filled_mask = watershed(mask == 0, markers=markers, mask=mask != 0, compactness=0) # Count area in pixels of each segment ids, counts = np.unique(filled_mask, return_counts=True) if stem_objects is None: outputs.add_observation( sample=label, variable='segment_area', trait='segment area', method='plantcv.plantcv.morphology.fill_segments', scale='pixels', datatype=list, value=counts[1:].tolist(), label=(ids[1:] - 1).tolist()) else: outputs.add_observation( sample=label, variable='leaf_area', trait='segment area', method='plantcv.plantcv.morphology.fill_segments', scale='pixels', datatype=list, value=counts[1:].tolist(), label=(ids[1:] - 1).tolist()) outputs.add_observation( sample=label, variable='stem_area', trait='segment area', method='plantcv.plantcv.morphology.fill_segments', scale='pixels', datatype=list, value=counts[1:].tolist(), label=(ids[1:] - 1).tolist()) rgb_vals = color_palette(num=len(labels), saved=False) filled_img = np.zeros((h, w, 3), dtype=np.uint8) for l in labels: for ch in range(3): filled_img[:, :, ch][filled_mask == l] = rgb_vals[l - 1][ch] _debug(visual=filled_img, filename=os.path.join(params.debug_outdir, str(params.device) + "_filled_img.png")) return filled_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
clust_img, clust_masks = pcv.spatial_clustering(mask=kept_mask, algorithm="DBSCAN", min_cluster_size=5, max_distance=None) # In[32]: # The pcv.cluster_contours function uses another PlantCV function # that returns a random list of RGB color values equally spaces # across a rainbow color spectrum. This function can be useful # when a color palette is needed # Inputs: # num - An integer greater than or equal to 1. If num=1 then # a random color is returned rand_colors = pcv.color_palette(num=5) print(rand_colors) # In[26]: # Set the sequence of colors from the color_scale created by the # color_palette function to "sequential" or "random" order. # Default = "sequential". pcv.params.color_sequence = 'random' cluster_img = pcv.visualize.clustered_contours( img=img1, grouped_contour_indices=clusters_i, roi_objects=contours, roi_obj_hierarchy=hierarchies) # In[27]:
def spatial_clustering(mask, algorithm="DBSCAN", min_cluster_size=5, max_distance=None): """Counts and segments portions of an image based on distance between two pixels. Masks showing all clusters, plus masks of individual clusters, are returned. Inputs: mask = Mask/binary image to segment into clusters. algorithm = Algorithm to use for segregating different clusters. Currently supporting OPTICS and DBSCAN. (Default="DBSCAN") min_cluster_size = The minimum size a section of a mask must be (in pixels) before it can be considered its own cluster. (Default=5) max_distance = The total distance between two pixels for them to be considered a part of the same cluster. For the DBSCAN algorithm, value must be between 0 and 1. For OPTICS, the value is in pixels and depends on the size of your picture. (Default=0) Returns: clust_img = Output image with each cluster draw with a unique color. clust_masks = List of binary masks, one per cluster. :param mask: numpy.ndarray :param algorithm: str :param min_cluster_size: int :param max_distance: float :return clust_img: numpy.ndarray :return clust_masks: list """ # Increment device counter params.device += 1 # Uppercase algorithm name al_upper = algorithm.upper() # Dictionary of default values per algorithm default_max_dist = {"DBSCAN": 0.2, "OPTICS": np.inf} # If the algorithm is not in the default_max_dist dictionary raise a NameError if al_upper not in default_max_dist: raise NameError("Please use only 'OPTICS' or 'DBSCAN' ") # If max_distance is not set, apply the default value if max_distance is None: max_distance = default_max_dist.get(al_upper) # Get all x, y coordinates of white pixels in the mask x, y = np.where(mask == 255) zipped = np.column_stack((x, y)) if "OPTICS" in al_upper: scaled = StandardScaler(with_mean=False, with_std=False).fit_transform(zipped) db = OPTICS(max_eps=max_distance, min_samples=min_cluster_size, n_jobs=-1).fit(scaled) elif "DBSCAN" in al_upper: scaled = StandardScaler().fit_transform(zipped) db = DBSCAN(eps=max_distance, min_samples=min_cluster_size, n_jobs=-1).fit(scaled) # Number of clusters n_clusters = len(set(db.labels_)) - (1 if -1 in db.labels_ else 0) # Create a color palette of n_clusters colors colors = color_palette(n_clusters + 1) # Initialize variables dict_of_colors = {} clust_masks = [] h, w = mask.shape # Colorized clusters image clust_img = np.zeros((h, w, 3), np.uint8) # Index the label color for each cluster for y in range(0, n_clusters): dict_of_colors[str(y)] = colors[y] clust_masks.append(np.zeros((h, w), np.uint8)) # Group -1 are points not assigned to a cluster dict_of_colors["-1"] = (255, 255, 255) # Loop over labels/clusters for z in range(0, len(db.labels_)): if not db.labels_[z] == -1: # Create a binary mask for each cluster clust_masks[db.labels_[z]][zipped[z][0], zipped[z][1]] = 255 # Add a cluster with a unique label color to the cluster image clust_img[zipped[z][0], zipped[z][1]] = (dict_of_colors[str(db.labels_[z])][2], dict_of_colors[str(db.labels_[z])][1], dict_of_colors[str(db.labels_[z])][0]) if params.debug == 'print': print_image(clust_img, os.path.join(params.debug_outdir, f"{params.device}_{al_upper}_clusters.png")) elif params.debug == 'plot': plot_image(clust_img) return clust_img, clust_masks
def segment_tangent_angle(segmented_img, objects, size): """ Find 'tangent' angles in degrees of skeleton segments. Use `size` pixels on either end of each segment to find a linear regression line, and calculate angle between the two lines drawn per segment. Inputs: segmented_img = Segmented image to plot slope lines and intersection angles on objects = List of contours size = Size of ends used to calculate "tangent" lines Returns: labeled_img = Segmented debugging image with angles labeled :param segmented_img: numpy.ndarray :param objects: list :param size: int :return labeled_img: numpy.ndarray """ # Store debug debug = params.debug params.debug = None labeled_img = segmented_img.copy() intersection_angles = [] label_coord_x = [] label_coord_y = [] rand_color = color_palette(len(objects)) for i, cnt in enumerate(objects): find_tangents = np.zeros(segmented_img.shape[:2], np.uint8) cv2.drawContours(find_tangents, objects, i, 255, 1, lineType=8) cv2.drawContours(labeled_img, objects, i, rand_color[i], params.line_thickness, lineType=8) pruned_segment = _iterative_prune(find_tangents, size) segment_ends = find_tangents - pruned_segment segment_end_obj, segment_end_hierarchy = find_objects(segment_ends, segment_ends) slopes = [] for j, obj in enumerate(segment_end_obj): # Find bounds for regression lines to get drawn rect = cv2.minAreaRect(cnt) pts = cv2.boxPoints(rect) df = pd.DataFrame(pts, columns=('x', 'y')) x_max = int(df['x'].max()) x_min = int(df['x'].min()) # Find line fit to each segment [vx, vy, x, y] = cv2.fitLine(obj, cv2.DIST_L2, 0, 0.01, 0.01) slope = -vy / vx left_list = int(((x - x_min) * slope) + y) right_list = int(((x - x_max) * slope) + y) slopes.append(slope) if slope > 1000000 or slope < -1000000: print("Slope of contour with ID#", i, "is", slope, "and cannot be plotted.") else: # Draw slope lines cv2.line(labeled_img, (x_max - 1, right_list), (x_min, left_list), rand_color[i], 1) if len(slopes) < 2: # If size*2>len(obj) then pruning will remove the segment completely, and # makes segment_end_objs contain just one contour. print("Size too large, contour with ID#", i, "got pruned away completely.") intersection_angles.append("NA") else: # Calculate intersection angles slope1 = slopes[0][0] slope2 = slopes[1][0] intersection_angle = _slope_to_intesect_angle(slope1, slope2) intersection_angles.append(intersection_angle) # Store coordinates for labels label_coord_x.append(objects[i][0][0][0]) label_coord_y.append(objects[i][0][0][1]) segment_ids = [] for i, cnt in enumerate(objects): # Label slope lines w = label_coord_x[i] h = label_coord_y[i] if type(intersection_angles[i]) is str: text = "{}".format(intersection_angles[i]) else: text = "{:.2f}".format(intersection_angles[i]) cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=params.text_size, color=(150, 150, 150), thickness=params.text_thickness) segment_label = "ID" + str(i) segment_ids.append(i) outputs.add_observation(variable='segment_tangent_angle', trait='segment tangent angle', method='plantcv.plantcv.morphology.segment_tangent_angle', scale='degrees', datatype=list, value=intersection_angles, label=segment_ids) # Reset debug mode params.debug = debug # Auto-increment device params.device += 1 if params.debug == 'print': print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segment_tangent_angles.png')) elif params.debug == 'plot': plot_image(labeled_img) return labeled_img
def segment_curvature(segmented_img, objects, label="default"): """ Calculate segment curvature as defined by the ratio between geodesic and euclidean distance. Measurement of two-dimensional tortuosity. Inputs: segmented_img = Segmented image to plot lengths on objects = List of contours label = optional label parameter, modifies the variable name of observations recorded Returns: labeled_img = Segmented debugging image with curvature labeled :param segmented_img: numpy.ndarray :param objects: list :param label: str :return labeled_img: numpy.ndarray """ label_coord_x = [] label_coord_y = [] labeled_img = segmented_img.copy() # Store debug debug = params.debug params.debug = None _ = segment_euclidean_length(segmented_img, objects, label="backend") _ = segment_path_length(segmented_img, objects, label="backend") eu_lengths = outputs.observations['backend']['segment_eu_length']['value'] path_lengths = outputs.observations['backend']['segment_path_length'][ 'value'] curvature_measure = [ float(x / y) for x, y in zip(path_lengths, eu_lengths) ] # Create a color scale, use a previously stored scale if available rand_color = color_palette(num=len(objects), saved=True) for i, cnt in enumerate(objects): # Store coordinates for labels label_coord_x.append(objects[i][0][0][0]) label_coord_y.append(objects[i][0][0][1]) # Draw segments one by one to group segment tips together finding_tips_img = np.zeros(segmented_img.shape[:2], np.uint8) cv2.drawContours(finding_tips_img, objects, i, (255, 255, 255), 1, lineType=8) segment_tips = find_tips(finding_tips_img) tip_objects, tip_hierarchies = find_objects(segment_tips, segment_tips) points = [] for t in tip_objects: # Gather pairs of coordinates x, y = t.ravel() coord = (x, y) points.append(coord) # Draw euclidean distance lines cv2.line(labeled_img, points[0], points[1], rand_color[i], 1) segment_ids = [] # Reset debug mode params.debug = debug for i, cnt in enumerate(objects): # Calculate geodesic distance text = "{:.3f}".format(curvature_measure[i]) w = label_coord_x[i] h = label_coord_y[i] cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=params.text_size, color=(150, 150, 150), thickness=params.text_thickness) # segment_label = "ID" + str(i) segment_ids.append(i) outputs.add_observation( sample=label, variable='segment_curvature', trait='segment curvature', method='plantcv.plantcv.morphology.segment_curvature', scale='none', datatype=list, value=curvature_measure, label=segment_ids) # Auto-increment device params.device += 1 if params.debug == 'print': print_image( labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segment_curvature.png')) elif params.debug == 'plot': plot_image(labeled_img) return labeled_img
def cluster_contours(device, img, roi_objects, nrow=1, ncol=1, debug=None): """ This function take a image with multiple contours and clusters them based on user input of rows and columns Inputs: img = An RGB image array roi_objects = object contours in an image that are needed to be clustered. 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) file = output of filename from read_image function filenames = input txt file with list of filenames in order from top to bottom left to right debug = print debugging images Returns: device = pipeline step counter grouped_contour_indexes = contours grouped contours = All inputed contours :param device: int :param img: ndarray :param roi_objects: list :param nrow: int :param ncol: int :param debug: str :return device: int :return grouped_contour_indexes: list :return contours: list """ device += 1 if len(np.shape(img)) == 3: iy, ix, iz = np.shape(img) else: iy, ix, = np.shape(img) # get the break groups if nrow == 1: rbreaks = [0, iy] else: rstep = np.rint(iy / nrow) rstep1 = np.int(rstep) rbreaks = range(0, iy, rstep1) if ncol == 1: cbreaks = [0, ix] else: cstep = np.rint(ix / ncol) cstep1 = np.int(cstep) cbreaks = range(0, ix, cstep1) # categorize what bin the center of mass of each contour def digitize(a, step): if isinstance(step, int) == True: i = step else: i = len(step) for x in range(0, i): if x == 0: if a >= 0 and a < step[x + 1]: return x + 1 elif a >= step[x - 1] and a < step[x]: return x elif a > step[x - 1] and a > np.max(step): return i dtype = [('cx', int), ('cy', int), ('rowbin', int), ('colbin', int), ('index', int)] coord = [] for i in range(0, len(roi_objects)): m = cv2.moments(roi_objects[i]) if m['m00'] == 0: pass else: cx = int(m['m10'] / m['m00']) cy = int(m['m01'] / m['m00']) # colbin = np.digitize(cx, cbreaks) # rowbin = np.digitize(cy, rbreaks) colbin = digitize(cx, cbreaks) rowbin = digitize(cy, rbreaks) a = (cx, cy, colbin, rowbin, i) coord.append(a) coord1 = np.array(coord, dtype=dtype) coord2 = np.sort(coord1, order=('colbin', 'rowbin')) # get the list of unique coordinates and group the contours with the same bin coordinates groups = [] for i, y in enumerate(coord2): col = y[3] row = y[2] location = str(row) + ',' + str(col) groups.append(location) unigroup = np.unique(groups) coordgroups = [] for i, y in enumerate(unigroup): col = int(y[0]) row = int(y[2]) for a, b in enumerate(coord2): if b[2] == col and b[3] == row: grp = i contour = b[4] coordgroups.append((grp, contour)) else: pass coordlist = [[y[1] for y in coordgroups if y[0] == x] for x in range(0, (len(unigroup)))] contours = roi_objects grouped_contour_indexes = coordlist # Debug image is rainbow printed contours if debug == 'print': if len(np.shape(img)) == 3: img_copy = np.copy(img) else: iy, ix = np.shape(img) img_copy = np.zeros((iy, ix, 3), dtype=np.uint8) rand_color = color_palette(len(coordlist)) for i, x in enumerate(coordlist): for a in x: cv2.drawContours(img_copy, roi_objects, a, rand_color[i], -1, lineType=8) print_image(img_copy, (str(device) + '_clusters.png')) elif debug == 'plot': if len(np.shape(img)) == 3: img_copy = np.copy(img) else: iy, ix = np.shape(img) img_copy = np.zeros((iy, ix, 3), dtype=np.uint8) rand_color = color_palette(len(coordlist)) for i, x in enumerate(coordlist): for a in x: cv2.drawContours(img_copy, roi_objects, a, rand_color[i], -1, lineType=8) plot_image(img_copy) return device, grouped_contour_indexes, contours
def segment_angle(segmented_img, objects): """ Calculate angle of segments (in degrees) by fitting a linear regression line to segments. Inputs: segmented_img = Segmented image to plot slope lines and angles on objects = List of contours Returns: labeled_img = Segmented debugging image with angles labeled :param segmented_img: numpy.ndarray :param objects: list :return labeled_img: numpy.ndarray """ label_coord_x = [] label_coord_y = [] segment_angles = [] labeled_img = segmented_img.copy() rand_color = color_palette(len(objects)) for i, cnt in enumerate(objects): # Find bounds for regression lines to get drawn rect = cv2.minAreaRect(cnt) pts = cv2.boxPoints(rect) df = pd.DataFrame(pts, columns=('x', 'y')) x_max = int(df['x'].max()) x_min = int(df['x'].min()) # Find line fit to each segment [vx, vy, x, y] = cv2.fitLine(objects[i], cv2.DIST_L2, 0, 0.01, 0.01) slope = -vy / vx left_list = int(((x - x_min) * slope) + y) right_list = int(((x - x_max) * slope) + y) if slope > 1000000 or slope < -1000000: print("Slope of contour with ID#", i, "is", slope, "and cannot be plotted.") else: # Draw slope lines cv2.line(labeled_img, (x_max - 1, right_list), (x_min, left_list), rand_color[i], 1) # Store coordinates for labels label_coord_x.append(objects[i][0][0][0]) label_coord_y.append(objects[i][0][0][1]) # Calculate degrees from slopes segment_angles.append(np.arctan(slope[0]) * 180 / np.pi) segment_ids = [] for i, cnt in enumerate(objects): # Label slope lines w = label_coord_x[i] h = label_coord_y[i] text = "{:.2f}".format(segment_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_angle', trait='segment angle', method='plantcv.plantcv.morphology.segment_angle', scale='degrees', datatype=list, value=segment_angles, 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) + '_segmented_angles.png')) elif params.debug == 'plot': plot_image(labeled_img) return labeled_img
def segment_curvature(segmented_img, objects, hierarchies): """ Calculate segment curvature as defined by the ratio between geodesic and euclidean distance. Measurement of two-dimensional tortuosity. Inputs: segmented_img = Segmented image to plot lengths on objects = List of contours hierarchy = Contour hierarchy NumPy array Returns: curvature_header = Segment curvature data header curvature_data = Segment curvature data values labeled_img = Segmented debugging image with curvature labeled :param segmented_img: numpy.ndarray :param objects: list :param hierarchy: numpy.ndarray :return labeled_img: numpy.ndarray :return curvature_header: list :return curvature_data: list """ # Store debug debug = params.debug params.debug = None label_coord_x = [] label_coord_y = [] _, eu_lengths, _ = segment_euclidean_length(segmented_img, objects, hierarchies) _, path_lengths, labeled_img = segment_path_length(segmented_img, objects) del eu_lengths[0] del path_lengths[0] curvature_measure = [x/y for x, y in zip(path_lengths, eu_lengths)] rand_color = color_palette(len(objects)) for i, cnt in enumerate(objects): # Store coordinates for labels label_coord_x.append(objects[i][0][0][0]) label_coord_y.append(objects[i][0][0][1]) # Draw segments one by one to group segment tips together finding_tips_img = np.zeros(segmented_img.shape[:2], np.uint8) cv2.drawContours(finding_tips_img, objects, i, (255, 255, 255), 1, lineType=8, hierarchy=hierarchies) segment_tips = find_tips(finding_tips_img) tip_objects, tip_hierarchies = find_objects(segment_tips, segment_tips) points = [] for t in tip_objects: # Gather pairs of coordinates x, y = t.ravel() coord = (x, y) points.append(coord) # Draw euclidean distance lines cv2.line(labeled_img, points[0], points[1], rand_color[i], 1) curvature_header = ['HEADER_CURVATURE'] curvature_data = ['CURVATURE_DATA'] for i, cnt in enumerate(objects): # Calculate geodesic distance text = "{:.3f}".format(curvature_measure[i]) w = label_coord_x[i] h = label_coord_y[i] cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=.4, color=(150, 150, 150), thickness=1) segment_label = "ID" + str(i) curvature_header.append(segment_label) curvature_data.append(curvature_measure[i]) outputs.measurements['morphology_data']['segment_curvature'] = curvature_measure # Reset debug mode params.debug = debug # Auto-increment device params.device += 1 if params.debug == 'print': print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segment_curvature.png')) elif params.debug == 'plot': plot_image(labeled_img) return curvature_header, curvature_data, labeled_img
def segment_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_angle(segmented_img, objects): """ Calculate angle of segments (in degrees) by fitting a linear regression line to segments. Inputs: segmented_img = Segmented image to plot slope lines and angles on objects = List of contours Returns: angle_header = Segment angle data header angle_data = Segment angle data values labeled_img = Segmented debugging image with angles labeled :param segmented_img: numpy.ndarray :param objects: list :return labeled_img: numpy.ndarray :return angle_header: list :return angle_data: list """ label_coord_x = [] label_coord_y = [] segment_angles = [] labeled_img = segmented_img.copy() rand_color = color_palette(len(objects)) for i, cnt in enumerate(objects): # Find bounds for regression lines to get drawn rect = cv2.minAreaRect(cnt) pts = cv2.boxPoints(rect) df = pd.DataFrame(pts, columns=('x', 'y')) x_max = int(df['x'].max()) x_min = int(df['x'].min()) # Find line fit to each segment [vx, vy, x, y] = cv2.fitLine(objects[i], cv2.DIST_L2, 0, 0.01, 0.01) slope = -vy / vx left_list = int(((x - x_min) * slope) + y) right_list = int(((x - x_max) * slope) + y) if slope > 1000000 or slope < -1000000: print("Slope of contour with ID#", i, "is", slope, "and cannot be plotted.") else: # Draw slope lines cv2.line(labeled_img, (x_max - 1, right_list), (x_min, left_list), rand_color[i], 1) # Store coordinates for labels label_coord_x.append(objects[i][0][0][0]) label_coord_y.append(objects[i][0][0][1]) # Calculate degrees from slopes segment_angles.append(np.arctan(slope[0]) * 180 / np.pi) angle_header = ['HEADER_ANGLE'] angle_data = ['ANGLE_DATA'] for i, cnt in enumerate(objects): # Label slope lines w = label_coord_x[i] h = label_coord_y[i] text = "{:.2f}".format(segment_angles[i]) cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=.55, color=(150, 150, 150), thickness=2) segment_label = "ID" + str(i) angle_header.append(segment_label) angle_data.extend(segment_angles) if 'morphology_data' not in outputs.measurements: outputs.measurements['morphology_data'] = {} outputs.measurements['morphology_data']['segment_angles'] = segment_angles # Auto-increment device params.device += 1 if params.debug == 'print': print_image( labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segmented_angles.png')) elif params.debug == 'plot': plot_image(labeled_img) return angle_header, angle_data, labeled_img
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_angle(segmented_img, objects): """ Calculate angle of segments (in degrees) by fitting a linear regression line to segments. Inputs: segmented_img = Segmented image to plot slope lines and angles on objects = List of contours Returns: labeled_img = Segmented debugging image with angles labeled :param segmented_img: numpy.ndarray :param objects: list :return labeled_img: numpy.ndarray """ label_coord_x = [] label_coord_y = [] segment_angles = [] labeled_img = segmented_img.copy() rand_color = color_palette(len(objects)) for i, cnt in enumerate(objects): # Find bounds for regression lines to get drawn rect = cv2.minAreaRect(cnt) pts = cv2.boxPoints(rect) df = pd.DataFrame(pts, columns=('x', 'y')) x_max = int(df['x'].max()) x_min = int(df['x'].min()) # Find line fit to each segment [vx, vy, x, y] = cv2.fitLine(objects[i], cv2.DIST_L2, 0, 0.01, 0.01) slope = -vy / vx left_list = int(((x - x_min) * slope) + y) right_list = int(((x - x_max) * slope) + y) if slope > 1000000 or slope < -1000000: print("Slope of contour with ID#", i, "is", slope, "and cannot be plotted.") else: # Draw slope lines cv2.line(labeled_img, (x_max - 1, right_list), (x_min, left_list), rand_color[i], 1) # Store coordinates for labels label_coord_x.append(objects[i][0][0][0]) label_coord_y.append(objects[i][0][0][1]) # Calculate degrees from slopes segment_angles.append(np.arctan(slope[0]) * 180 / np.pi) segment_ids = [] for i, cnt in enumerate(objects): # Label slope lines w = label_coord_x[i] h = label_coord_y[i] text = "{:.2f}".format(segment_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_angle', trait='segment angle', method='plantcv.plantcv.morphology.segment_angle', scale='degrees', datatype=list, value=segment_angles, 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) + '_segmented_angles.png')) elif params.debug == 'plot': plot_image(labeled_img) return labeled_img