def hist_equalization(img, device, debug=None): """Histogram equalization is a method to normalize the distribution of intensity values. If the image has low contrast it will make it easier to threshold. Inputs: img = input image device = device number. Used to count steps in the pipeline debug = None, print, or plot. Print = save to file, Plot = print to screen. Returns: device = device number img_eh = normalized image :param img: numpy array :param device: int :param debug: str :return device: int :return img_eh: numpy array """ if len(np.shape(img)) == 3: fatal_error("Input image must be gray") img_eh = cv2.equalizeHist(img) device += 1 if debug == 'print': print_image(img_eh, str(device) + '_hist_equal_img.png') elif debug == 'plot': plot_image(img_eh, cmap='gray') return device, img_eh
def closing(gray_img, kernel=None): """Wrapper for scikit-image closing functions. Opening can remove small dark spots (i.e. pepper). Inputs: gray_img = input image (grayscale or binary) kernel = optional neighborhood, expressed as an array of 1s and 0s. If None, use cross-shaped structuring element. :param gray_img: ndarray :param kernel = ndarray :return filtered_img: ndarray """ params.device += 1 # Make sure the image is binary/grayscale if len(np.shape(gray_img)) != 2: fatal_error("Input image must be grayscale or binary") # If image is binary use the faster method if len(np.unique(gray_img)) == 2: bool_img = morphology.binary_closing(image=gray_img, selem=kernel) filtered_img = np.copy(bool_img.astype(np.uint8) * 255) # Otherwise use method appropriate for grayscale images else: filtered_img = morphology.closing(gray_img, kernel) if params.debug == 'print': print_image( filtered_img, os.path.join(params.debug_outdir, str(params.device) + '_opening' + '.png')) elif params.debug == 'plot': plot_image(filtered_img, cmap='gray') return filtered_img
def erode(gray_img, kernel, i): """Perform morphological 'erosion' filtering. Keeps pixel in center of the kernel if conditions set in kernel are true, otherwise removes pixel. Inputs: gray_img = Grayscale (usually binary) image data kernel = Kernel size (int). A k x k kernel will be built. Must be greater than 1 to have an effect. i = interations, i.e. number of consecutive filtering passes Returns: er_img = eroded image :param gray_img: numpy.ndarray :param kernel: int :param i: int :return er_img: numpy.ndarray """ kernel1 = int(kernel) kernel2 = np.ones((kernel1, kernel1), np.uint8) er_img = cv2.erode(src=gray_img, kernel=kernel2, iterations=i) params.device += 1 if params.debug == 'print': print_image( er_img, os.path.join( params.debug_outdir, str(params.device) + '_er_image_' + 'itr_' + str(i) + '.png')) elif params.debug == 'plot': plot_image(er_img, cmap='gray') return er_img
def distance_transform(bin_img, distance_type, mask_size): """Creates an image where for each object pixel, a number is assigned that corresponds to the distance to the nearest background pixel. Inputs: img = Binary image data distance_type = Type of distance. It can be CV_DIST_L1, CV_DIST_L2 , or CV_DIST_C which are 1, 2 and 3, respectively. mask_size = Size of the distance transform mask. It can be 3, 5, or CV_DIST_MASK_PRECISE (the latter option is only supported by the first function). In case of the CV_DIST_L1 or CV_DIST_C distance type, the parameter is forced to 3 because a 3 by 3 mask gives the same result as 5 by 5 or any larger aperture. Returns: norm_image = grayscale distance-transformed image normalized between [0, 1] :param bin_img: numpy.ndarray :param distance_type: int :param mask_size: int :return norm_image: numpy.ndarray """ params.device += 1 dist = cv2.distanceTransform(src=bin_img, distanceType=distance_type, maskSize=mask_size) norm_image = cv2.normalize(src=dist, dst=dist, alpha=0, beta=1, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_32F) if params.debug == 'print': print_image(norm_image, os.path.join(params.debug, str(params.device) + '_distance_transform.png')) elif params.debug == 'plot': plot_image(norm_image, cmap='gray') return norm_image
def resize(img, resize_x, resize_y): """Resize image. Inputs: img = RGB or grayscale image data to resize resize_x = scaling factor resize_y = scaling factor Returns: reimg = resized image :param img: numpy.ndarray :param resize_x: int :param resize_y: int :return reimg: numpy.ndarray """ params.device += 1 if resize_x <= 0 and resize_y <= 0: fatal_error("Resize values both cannot be 0 or negative values!") reimg = cv2.resize(img, (0, 0), fx=resize_x, fy=resize_y) if params.debug == 'print': print_image(reimg, os.path.join(params.debug_outdir, str(params.device) + "_resize1.png")) elif params.debug == 'plot': plot_image(reimg) return reimg
def skeletonize(mask): """Reduces binary objects to 1 pixel wide representations (skeleton) Inputs: mask = Binary image data Returns: skeleton = skeleton image :param mask: numpy.ndarray :return skeleton: numpy.ndarray """ # Convert mask to boolean image, rather than 0 and 255 for skimage to use it skeleton = skmorph.skeletonize(mask.astype(bool)) skeleton = skeleton.astype(np.uint8) * 255 # Auto-increment device params.device += 1 if params.debug == 'print': print_image( skeleton, os.path.join(params.debug_outdir, str(params.device) + '_skeleton.png')) elif params.debug == 'plot': plot_image(skeleton, cmap='gray') return skeleton
def background_subtraction(background_image, foreground_image): """Creates a binary image from a background subtraction of the foreground using cv2.BackgroundSubtractorMOG(). The binary image returned is a mask that should contain mostly foreground pixels. The background image should be the same background as the foreground image except not containing the object of interest. Images must be of the same size and type. If not, larger image will be taken and downsampled to smaller image size. If they are of different types, an error will occur. Inputs: background_image = img object, RGB or binary/grayscale/single-channel foreground_image = img object, RGB or binary/grayscale/single-channel Returns: fgmask = background subtracted foreground image (mask) :param background_image: numpy.ndarray :param foreground_image: numpy.ndarray :return fgmask: numpy.ndarray """ params.device += 1 # Copying images to make sure not alter originals bg_img = np.copy(background_image) fg_img = np.copy(foreground_image) # Checking if images need to be resized or error raised if bg_img.shape != fg_img.shape: # If both images are not 3 channel or single channel then raise error. if len(bg_img.shape) != len(fg_img.shape): fatal_error( "Images must both be single-channel/grayscale/binary or RGB") # Forcibly resizing largest image to smallest image print("WARNING: Images are not of same size.\nResizing") if bg_img.shape > fg_img.shape: width, height = fg_img.shape[1], fg_img.shape[0] bg_img = cv2.resize(bg_img, (width, height), interpolation=cv2.INTER_AREA) else: width, height = bg_img.shape[1], bg_img.shape[0] fg_img = cv2.resize(fg_img, (width, height), interpolation=cv2.INTER_AREA) bgsub = cv2.createBackgroundSubtractorMOG2() # Applying the background image to the background subtractor first. # Anything added after is subtracted from the previous iterations. _ = bgsub.apply(bg_img) # Applying the foreground image to the background subtractor (therefore removing the background) fgmask = bgsub.apply(fg_img) # Debug options if params.debug == "print": print_image( fgmask, os.path.join(params.debug_outdir, str(params.device) + "_background_subtraction.png")) elif params.debug == "plot": plot_image(fgmask, cmap="gray") return fgmask
def flip(img, direction): """Flip image. Inputs: img = RGB or grayscale image data direction = "horizontal" or "vertical" Returns: vh_img = flipped image :param img: numpy.ndarray :param direction: str :return vh_img: numpy.ndarray """ params.device += 1 if direction.upper() == "VERTICAL": vh_img = cv2.flip(img, 1) elif direction.upper() == "HORIZONTAL": vh_img = cv2.flip(img, 0) else: fatal_error(str(direction) + " is not a valid direction, must be horizontal or vertical") if params.debug == 'print': print_image(vh_img, os.path.join(params.debug_outdir, str(params.device) + "_flipped.png")) elif params.debug == 'plot': if len(np.shape(vh_img)) == 3: plot_image(vh_img) else: plot_image(vh_img, cmap='gray') return vh_img
def readimage(filename, debug=None): """Read image from file. Inputs: filename = name of image file debug = None, print, or plot. Print = save to file, Plot = print to screen. Returns: img = image object as numpy array path = path to image file img_name = name of image file :param filename: str :param debug: str :return img: numpy array :return path: str :return img_name: str """ img = cv2.imread(filename) if img is None: fatal_error("Failed to open " + filename) # Split path from filename path, img_name = os.path.split(filename) if debug == "print": print_image(img, "input_image.png") elif debug == "plot": plot_image(img) return img, path, img_name
def average_all_img(directory, outdir): allfiles = os.listdir(directory) path = str(directory) allpaths = [] for files in allfiles: p = path + str(files) allpaths.append(p) img, path, filename = pcv.readimage(allpaths[0]) n = len(allpaths) if len(np.shape(img)) == 3: ix, iy, iz = np.shape(img) arr = np.zeros((ix, iy, iz), np.float) else: ix, iy = np.shape(img) arr = np.zeros((ix, iy), np.float) # Build up average pixel intensities, casting each image as an array of floats for i, paths in enumerate(allpaths): img, path, filename = pcv.readimage(allpaths[i]) imarr = np.array(img, dtype=np.float) arr = arr + imarr / n # Round values in array and cast as 8-bit integer arr = np.array(np.round(arr), dtype=np.uint8) pcv.print_image(arr, (str(outdir) + "average_" + str(allfiles[0])))
def opening(gray_img, kernel=None): """Wrapper for scikit-image opening functions. Opening can remove small bright spots (i.e. salt). Inputs: gray_img = input image (grayscale or binary) kernel = optional neighborhood, expressed as an array of 1s and 0s. If None, use cross-shaped structuring element. :param gray_img: ndarray :param kernel = ndarray :return filtered_img: ndarray """ params.device += 1 # Make sure the image is binary/grayscale if len(np.shape(gray_img)) != 2: fatal_error("Input image must be grayscale or binary") # If image is binary use the faster method if len(np.unique(gray_img)) == 2: bool_img = morphology.binary_opening(gray_img, kernel) filtered_img = np.copy(bool_img.astype(np.uint8) * 255) # Otherwise use method appropriate for grayscale images else: filtered_img = morphology.opening(gray_img, kernel) if params.debug == 'print': print_image(filtered_img, os.path.join(params.debug_outdir, str(params.device) + '_opening' + '.png')) elif params.debug == 'plot': plot_image(filtered_img, cmap='gray') return filtered_img
def scharr_filter(gray_img, dx, dy, scale): """This is a filtering method used to identify and highlight gradient edges/features using the 1st derivative. Typically used to identify gradients along the x-axis (dx = 1, dy = 0) and y-axis (dx = 0, dy = 1) independently. Performance is quite similar to Sobel filter. Used to detect edges / changes in pixel intensity. ddepth = -1 specifies that the dimensions of output image will be the same as the input image. Inputs: gray_img = Grayscale image data dx = derivative of x to analyze (1-3) dy = derivative of x to analyze (1-3) scale = scaling factor applied (multiplied) to computed Scharr values (scale = 1 is unscaled) Returns: sr_img = Scharr filtered image :param gray_img: numpy.ndarray :param dx: int :param dy: int :param scale: int :return sr_img: numpy.ndarray """ sr_img = cv2.Scharr(src=gray_img, ddepth=-1, dx=dx, dy=dy, scale=scale) params.device += 1 if params.debug == 'print': name = os.path.join(params.debug_outdir, str(params.device)) name += '_sr_img_dx' + str(dx) + '_dy' + str(dy) + '_scale' + str(scale) + '.png' print_image(sr_img, name) elif params.debug == 'plot': plot_image(sr_img, cmap='gray') return sr_img
def erode(img, kernel, i, device, debug=None): """Perform morphological 'erosion' filtering. Keeps pixel in center of the kernel if conditions set in kernel are true, otherwise removes pixel. Inputs: img = input image kernel = filtering window, you'll need to make your own using as such: kernal = np.zeros((x,y), dtype=np.uint8), then fill the kernal with appropriate values i = interations, i.e. number of consecutive filtering passes device = device number. Used to count steps in the pipeline debug = None, print, or plot. Print = save to file, Plot = print to screen. Returns: device = device number er_img = eroded image :param img: numpy array :param kernel: numpy array :param i: int :param device: int :param debug: str :return device: int :return er_img: numpy array """ kernel1 = int(kernel) kernel2 = np.ones((kernel1, kernel1), np.uint8) er_img = cv2.erode(src=img, kernel=kernel2, iterations=i) device += 1 if debug == 'print': print_image(er_img, str(device) + '_er_image_' + 'itr_' + str(i) + '.png') elif debug == 'plot': plot_image(er_img, cmap='gray') return device, er_img
def find_objects(img, mask): """Find all objects and color them blue. Inputs: img = RGB or grayscale image data for plotting mask = Binary mask used for contour detection Returns: objects = list of contours hierarchy = contour hierarchy list :param img: numpy.ndarray :param mask: numpy.ndarray :return objects: list :return hierarchy: numpy.ndarray """ params.device += 1 mask1 = np.copy(mask) ori_img = np.copy(img) # If the reference image is grayscale convert it to color if len(np.shape(ori_img)) == 2: ori_img = cv2.cvtColor(ori_img, cv2.COLOR_GRAY2BGR) objects, hierarchy = cv2.findContours(mask1, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)[-2:] for i, cnt in enumerate(objects): cv2.drawContours(ori_img, objects, i, (255, 102, 255), -1, lineType=8, hierarchy=hierarchy) if params.debug == 'print': print_image(ori_img, os.path.join(params.debug_outdir, str(params.device) + '_id_objects.png')) elif params.debug == 'plot': plot_image(ori_img) return objects, hierarchy
def gaussian_blur(img, ksize, sigma_x=0, sigma_y=None): """Applies a Gaussian blur filter. Inputs: # img = RGB or grayscale image data # ksize = Tuple of kernel dimensions, e.g. (5, 5) # sigmax = standard deviation in X direction; if 0, calculated from kernel size # sigmay = standard deviation in Y direction; if sigmaY is None, sigmaY is taken to equal sigmaX Returns: img_gblur = blurred image :param img: numpy.ndarray :param ksize: tuple :param sigmax: int :param sigmay: str or int :return img_gblur: numpy.ndarray """ img_gblur = cv2.GaussianBlur(img, ksize, sigma_x, sigma_y) params.device += 1 if params.debug == 'print': print_image(img_gblur, os.path.join(params.debug_outdir, str(params.device) + '_gaussian_blur.png')) elif params.debug == 'plot': if len(np.shape(img_gblur)) == 3: plot_image(img_gblur) else: plot_image(img_gblur, cmap='gray') return img_gblur
def average_all_img(directory, outdir): allfiles = os.listdir(directory) path = str(directory) allpaths = [] for files in allfiles: p = path + str(files) allpaths.append(p) img, path, filename = pcv.readimage(allpaths[0]) n = len(allpaths) if len(np.shape(img)) == 3: ix, iy, iz = np.shape(img) arr = np.zeros((ix, iy, iz), np.float) else: ix, iy = np.shape(img) arr = np.zeros((ix, iy), np.float) # Build up average pixel intensities, casting each image as an array of floats for i, paths in enumerate(allpaths): img, path, filename = pcv.readimage(allpaths[i]) imarr = np.array(img, dtype=np.float) arr = arr + imarr / n # Round values in array and cast as 8-bit integer arr = np.array(np.round(arr), dtype=np.uint8) pcv.print_image(arr, (str(outdir)+"average_"+str(allfiles[0])))
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 gaussian_blur(img, ksize, sigma_x=0, sigma_y=None): """Applies a Gaussian blur filter. Inputs: # img = RGB or grayscale image data # ksize = Tuple of kernel dimensions, e.g. (5, 5) # sigmax = standard deviation in X direction; if 0, calculated from kernel size # sigmay = standard deviation in Y direction; if sigmaY is None, sigmaY is taken to equal sigmaX Returns: img_gblur = blurred image :param img: numpy.ndarray :param ksize: tuple :param sigma_x: int :param sigma_y: str or int :return img_gblur: numpy.ndarray """ img_gblur = cv2.GaussianBlur(img, ksize, sigma_x, sigma_y) params.device += 1 if params.debug == 'print': print_image( img_gblur, os.path.join(params.debug_outdir, str(params.device) + '_gaussian_blur.png')) elif params.debug == 'plot': if len(np.shape(img_gblur)) == 3: plot_image(img_gblur) else: plot_image(img_gblur, cmap='gray') return img_gblur
def image_add(img1, img2, device, debug=None): """This is a function used to add images. The numpy addition function '+' is used. This is a modulo operation rather than the cv2.add fxn which is a saturation operation. ddepth = -1 specifies that the dimensions of output image will be the same as the input image. Inputs: img1 = input image img2 = second input image device = device number. Used to count steps in the pipeline debug = None, print, or plot. Print = save to file, Plot = print to screen. Returns: device = device number added_img = summed images :param img1: numpy array :param img2: numpy array :param device: int :param debug: str :return device: int :return added_img: numpy array """ added_img = img1 + img2 device += 1 if debug == 'print': print_image(added_img, str(device) + '_added' + '.png') elif debug == 'plot': plot_image(added_img, cmap='gray') return device, added_img
def scharr_filter(img, dx, dy, scale): """This is a filtering method used to identify and highlight gradient edges/features using the 1st derivative. Typically used to identify gradients along the x-axis (dx = 1, dy = 0) and y-axis (dx = 0, dy = 1) independently. Performance is quite similar to Sobel filter. Used to detect edges / changes in pixel intensity. ddepth = -1 specifies that the dimensions of output image will be the same as the input image. Inputs: gray_img = Grayscale image data dx = derivative of x to analyze (1-3) dy = derivative of x to analyze (1-3) scale = scaling factor applied (multiplied) to computed Scharr values (scale = 1 is unscaled) Returns: sr_img = Scharr filtered image :param img: numpy.ndarray :param dx: int :param dy: int :param scale: int :return sr_img: numpy.ndarray """ sr_img = cv2.Scharr(src=img, ddepth=-1, dx=dx, dy=dy, scale=scale) params.device += 1 if params.debug == 'print': name = os.path.join(params.debug_outdir, str(params.device)) name += '_sr_img_dx' + str(dx) + '_dy' + str(dy) + '_scale' + str(scale) + '.png' print_image(sr_img, name) elif params.debug == 'plot': plot_image(sr_img, cmap='gray') return sr_img
def sobel_filter(gray_img, dx, dy, ksize): """This is a filtering method used to identify and highlight gradient edges/features using the 1st derivative. Typically used to identify gradients along the x-axis (dx = 1, dy = 0) and y-axis (dx = 0, dy = 1) independently. Performance is quite similar to Scharr filter. Used to detect edges / changes in pixel intensity. ddepth = -1 specifies that the dimensions of output image will be the same as the input image. Inputs: gray_img = Grayscale image data dx = derivative of x to analyze dy = derivative of x to analyze ksize = specifies the size of the kernel (must be an odd integer: 1,3,5, ... , 31) Returns: sb_img = Sobel filtered image :param gray_img: numpy.ndarray :param dx: int :param dy: int :param ksize: int :param scale: int :return sb_img: numpy.ndarray """ params.device += 1 sb_img = cv2.Sobel(src=gray_img, ddepth=-1, dx=dx, dy=dy, ksize=ksize) if params.debug == 'print': name = os.path.join(params.debug_outdir, str(params.device) + '_sb_img_dx' + str(dx) + '_dy' + str(dy) + '_kernel' + str(ksize) + '.png') print_image(sb_img, name) elif params.debug == 'plot': plot_image(sb_img, cmap='gray') return sb_img
def image_subtract(gray_img1, gray_img2): """This is a function used to subtract values of one gray-scale image array from another gray-scale image array. The resulting gray-scale image array has a minimum element value of zero. That is all negative values resulting from the subtraction are forced to zero. Inputs: gray_img1 = Grayscale image data from which gray_img2 will be subtracted gray_img2 = Grayscale image data which will be subtracted from gray_img1 Returns: new_img = subtracted image :param gray_img1: numpy.ndarray :param gray_img2: numpy.ndarray :return new_img: numpy.ndarray """ params.device += 1 # increment device # check inputs for gray-scale if len(np.shape(gray_img1)) != 2 or len(np.shape(gray_img2)) != 2: fatal_error("Input image is not gray-scale") new_img = gray_img1.astype(np.float64) - gray_img2.astype(np.float64) # subtract values new_img[np.where(new_img < 0)] = 0 # force negative array values to zero new_img = new_img.astype(np.uint8) # typecast image to 8-bit image # print-plot handling if params.debug == 'print': print_image(new_img, os.path.join(params.debug_outdir, str(params.device) + "_subtraction.png")) elif params.debug == 'plot': plot_image(new_img, cmap='gray') return new_img # return
def skeletonize(mask): """Reduces binary objects to 1 pixel wide representations (skeleton) Inputs: mask = Binary image data Returns: skeleton = skeleton image :param mask: numpy.ndarray :return skeleton: numpy.ndarray """ # Store debug debug = params.debug params.debug = None # Convert mask to boolean image, rather than 0 and 255 for skimage to use it skeleton = skmorph.skeletonize(mask.astype(bool)) skeleton = skeleton.astype(np.uint8) * 255 # Reset debug mode params.debug = debug # Auto-increment device params.device += 1 if params.debug == 'print': print_image(skeleton, os.path.join(params.debug_outdir, str(params.device) + '_skeleton.png')) elif params.debug == 'plot': plot_image(skeleton, cmap='gray') return skeleton
def hist_equalization(gray_img): """Histogram equalization is a method to normalize the distribution of intensity values. If the image has low contrast it will make it easier to threshold. Inputs: gray_img = Grayscale image data Returns: img_eh = normalized image :param gray_img: numpy.ndarray :return img_eh: numpy.ndarray """ if len(np.shape(gray_img)) == 3: fatal_error("Input image must be gray") img_eh = cv2.equalizeHist(gray_img) params.device += 1 if params.debug == 'print': print_image(img_eh, os.path.join(params.debug_outdir, str(params.device) + '_hist_equal_img.png')) elif params.debug == 'plot': plot_image(img_eh, cmap='gray') return img_eh
def median_blur(gray_img, ksize): """Applies a median blur filter (applies median value to central pixel within a kernel size). Inputs: gray_img = Grayscale image data ksize = kernel size => integer or tuple, ksize x ksize box if integer, (n, m) size box if tuple Returns: img_mblur = blurred image :param gray_img: numpy.ndarray :param ksize: int or tuple :return img_mblur: numpy.ndarray """ # Make sure ksize is valid if type(ksize) is not int and type(ksize) is not tuple: fatal_error("Invalid ksize, must be integer or tuple") img_mblur = median_filter(gray_img, size=ksize) params.device += 1 if params.debug == 'print': print_image( img_mblur, os.path.join( params.debug_outdir, str(params.device) + '_median_blur' + str(ksize) + '.png')) elif params.debug == 'plot': plot_image(img_mblur, cmap='gray') return img_mblur
def logical_and(img1, img2, device, debug=None): """Join two images using the bitwise AND operator. Inputs: img1 = image object1, grayscale img2 = image object2, grayscale device = device number. Used to count steps in the pipeline debug = None, print, or plot. Print = save to file, Plot = print to screen. Returns: device = device number merged = joined image :param img1: numpy array :param img2: numpy array :param device: int :param debug: str :return device: int :return merged: numpy array """ device += 1 merged = cv2.bitwise_and(img1, img2) if debug == 'print': print_image(merged, (str(device) + '_and_joined.png')) elif debug == 'plot': plot_image(merged, cmap='gray') return device, merged
def invert(img, device, debug=None): """Inverts grayscale images. Inputs: img = image object, grayscale device = device number. Used to count steps in the pipeline debug = None, print, or plot. Print = save to file, Plot = print to screen. Returns: device = device number img_inv = inverted image :param img: numpy array :param device: int :param debug: str :return device: int :return img_inv: numpy array """ device += 1 img_inv = cv2.bitwise_not(img) if debug == 'print': print_image(img_inv, (str(device) + '_invert.png')) elif debug == 'plot': plot_image(img_inv, cmap='gray') return device, img_inv
def sobel_filter(gray_img, dx, dy, ksize): """This is a filtering method used to identify and highlight gradient edges/features using the 1st derivative. Typically used to identify gradients along the x-axis (dx = 1, dy = 0) and y-axis (dx = 0, dy = 1) independently. Performance is quite similar to Scharr filter. Used to detect edges / changes in pixel intensity. ddepth = -1 specifies that the dimensions of output image will be the same as the input image. Inputs: gray_img = Grayscale image data dx = derivative of x to analyze dy = derivative of x to analyze ksize = specifies the size of the kernel (must be an odd integer: 1,3,5, ... , 31) Returns: sb_img = Sobel filtered image :param gray_img: numpy.ndarray :param dx: int :param dy: int :param ksize: int :return sb_img: numpy.ndarray """ params.device += 1 sb_img = cv2.Sobel(src=gray_img, ddepth=-1, dx=dx, dy=dy, ksize=ksize) if params.debug == 'print': fname = str(params.device) + '_sb_img_dx' + str(dx) + '_dy' + str(dy) + '_kernel' + str(ksize) + '.png' name = os.path.join(params.debug_outdir, fname) print_image(sb_img, name) elif params.debug == 'plot': plot_image(sb_img, cmap='gray') return sb_img
def median_blur(img, ksize, device, debug=None): """Applies a median blur filter (applies median value to central pixel within a kernel size ksize x ksize). Inputs: # img = img object # ksize = kernel size => ksize x ksize box # device = device number. Used to count steps in the pipeline # debug = None, print, or plot. Print = save to file, Plot = print to screen. Returns: device = device number img_mblur = blurred image :param img: numpy array :param ksize: int :param device: int :param debug: str :return device: int :return img_mblur: numpy array """ img_mblur = cv2.medianBlur(img, ksize) device += 1 if debug == 'print': print_image(img_mblur, (str(device) + '_median_blur' + str(ksize) + '.png')) elif debug == 'plot': plot_image(img_mblur, cmap='gray') return device, img_mblur
def image_add(gray_img1, gray_img2): """This is a function used to add images. The numpy addition function '+' is used. This is a modulo operation rather than the cv2.add fxn which is a saturation operation. ddepth = -1 specifies that the dimensions of output image will be the same as the input image. Inputs: gray_img1 = Grayscale image data to be added to image 2 gray_img2 = Grayscale image data to be added to image 1 Returns: added_img = summed images :param gray_img1: numpy.ndarray :param gray_img2: numpy.ndarray :return added_img: numpy.ndarray """ added_img = gray_img1 + gray_img2 params.device += 1 if params.debug == 'print': print_image( added_img, os.path.join(params.debug_outdir, str(params.device) + '_added' + '.png')) elif params.debug == 'plot': plot_image(added_img, cmap='gray') return added_img
def roi2mask(img, contour): """Create a binary mask from an ROI contour Inputs: img = RGB or grayscale image data contour = An ROI set of points (contour) Returns: mask = Binary mask :param roi_contour: numpy.ndarray :param contour: list :return mask: numpy.ndarray """ params.device += 1 # create a blank image of same size shape_info = np.shape(img) bnk = np.zeros((shape_info[0], shape_info[1]), dtype=np.uint8) mask = cv2.drawContours(bnk, contour, 0, 255, -1) if params.debug == 'print': print_image(mask, os.path.join(params.debug_outdir, str(params.device) + '_roi_mask.png')) elif params.debug == 'plot': plot_image(mask, cmap="gray") return mask
def median_blur(gray_img, ksize): """Applies a median blur filter (applies median value to central pixel within a kernel size). Inputs: gray_img = Grayscale image data ksize = kernel size => integer or tuple, ksize x ksize box if integer, (n, m) size box if tuple Returns: img_mblur = blurred image :param gray_img: numpy.ndarray :param ksize: int or tuple :return img_mblur: numpy.ndarray """ # Make sure ksize is valid if type(ksize) is not int and type(ksize) is not tuple: fatal_error("Invalid ksize, must be integer or tuple") img_mblur = median_filter(gray_img, size=ksize) params.device += 1 if params.debug == 'print': print_image(img_mblur, os.path.join(params.debug_outdir, str(params.device) + '_median_blur' + str(ksize) + '.png')) elif params.debug == 'plot': plot_image(img_mblur, cmap='gray') return img_mblur
def resize(img, resize_x, resize_y): """Resize image. Inputs: img = RGB or grayscale image data to resize resize_x = scaling factor resize_y = scaling factor Returns: reimg = resized image :param img: numpy.ndarray :param resize_x: int :param resize_y: int :return reimg: numpy.ndarray """ params.device += 1 if resize_x <= 0 and resize_y <= 0: fatal_error("Resize values both cannot be 0 or negative values!") reimg = cv2.resize(img, (0, 0), fx=resize_x, fy=resize_y) if params.debug == 'print': print_image( reimg, os.path.join(params.debug_outdir, str(params.device) + "_resize1.png")) elif params.debug == 'plot': plot_image(reimg) return reimg
def image_add(gray_img1, gray_img2): """This is a function used to add images. The numpy addition function '+' is used. This is a modulo operation rather than the cv2.add fxn which is a saturation operation. ddepth = -1 specifies that the dimensions of output image will be the same as the input image. Inputs: gray_img1 = Grayscale image data to be added to image 2 gray_img2 = Grayscale image data to be added to image 1 Returns: added_img = summed images :param gray_img1: numpy.ndarray :param gray_img2: numpy.ndarray :return added_img: numpy.ndarray """ added_img = gray_img1 + gray_img2 params.device += 1 if params.debug == 'print': print_image(added_img, os.path.join(params.debug_outdir, str(params.device) + '_added' + '.png')) elif params.debug == 'plot': plot_image(added_img, cmap='gray') return added_img
def rescale(gray_img, min_value=0, max_value=255): """Rescale image. Inputs: gray_img = Grayscale image data min_value = (optional) new minimum value for range of interest. default = 0 max_value = (optional) new maximum value for range of interest. default = 255 Returns: rescaled_img = rescaled image :param gray_img: numpy.ndarray :param min_value: int :param max_value: int :return c: numpy.ndarray """ if len(np.shape(gray_img)) != 2: fatal_error("Image is not grayscale") rescaled_img = np.interp(gray_img, (gray_img.min(), gray_img.max()), (min_value, max_value)) rescaled_img = (rescaled_img).astype('uint8') # Autoincrement the device counter params.device += 1 if params.debug == 'print': print_image( rescaled_img, os.path.join(params.debug_outdir, str(params.device) + "_rescaled.png")) elif params.debug == 'plot': plot_image(rescaled_img, cmap='gray') return rescaled_img
def laplace_filter(gray_img, ksize, scale): """This is a filtering method used to identify and highlight fine edges based on the 2nd derivative. A very sensetive method to highlight edges but will also amplify background noise. ddepth = -1 specifies that the dimensions of output image will be the same as the input image. Inputs: gray_img = Grayscale image data ksize = apertures size used to calculate 2nd derivative filter, specifies the size of the kernel (must be an odd integer: 1,3,5...) scale = scaling factor applied (multiplied) to computed Laplacian values (scale = 1 is unscaled) Returns: lp_filtered = laplacian filtered image :param gray_img: numpy.ndarray :param kernel: int :param scale: int :return lp_filtered: numpy.ndarray """ lp_filtered = cv2.Laplacian(src=gray_img, ddepth=-1, ksize=ksize, scale=scale) params.device += 1 if params.debug == 'print': print_image(lp_filtered, os.path.join(params.debug_outdir, str(params.device) + '_lp_out_k' + str(ksize) + '_scale' + str(scale) + '.png')) elif params.debug == 'plot': plot_image(lp_filtered, cmap='gray') return lp_filtered
def image_subtract(img1, img2, device, debug=None): """This is a function used to subtract one image from another image (img1 - img2). The numpy subtraction function '-' is used. This is a modulo operation rather than the cv2.subtract fxn which is a saturation operation. ddepth = -1 specifies that the dimensions of output image will be the same as the input image. Inputs: img1 = input image img2 = input image used to subtract from img1 device = device number. Used to count steps in the pipeline debug = None, print, or plot. Print = save to file, Plot = print to screen. Returns: device = device number subed_img = subtracted image :param img1: numpy array :param img2: numpy array :param device: int :param debug: str :return device: int :return subed_img: numpy array """ subed_img = img1 - img2 device += 1 if debug == 'print': print_image(subed_img, str(device) + '_subtracted' + '.png') elif debug == 'plot': plot_image(subed_img, cmap='gray') return device, subed_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: 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 stdev_filter(img, ksize, borders='nearest'): """Creates a binary image from a grayscale image using skimage texture calculation for thresholding. This function is quite slow. Inputs: gray_img = Grayscale image data ksize = Kernel size for texture measure calculation borders = How the array borders are handled, either 'reflect', 'constant', 'nearest', 'mirror', or 'wrap' Returns: output = Standard deviation values image :param gray_img: numpy.ndarray :param ksize: int :param borders: str :return output: numpy.ndarray """ # Make an array the same size as the original image output = np.zeros(img.shape, dtype=img.dtype) # Apply the texture function over the whole image generic_filter(img, np.std, size=ksize, output=output, mode=borders) if params.debug == "print": # If debug is print, save the image to a file print_image(output, os.path.join(params.debug_outdir, str(params.device) + "_variance.png")) elif params.debug == "plot": # If debug is plot, print to the plotting device plot_image(output) return output
def object_composition(img, contours, hierarchy): """Groups objects into a single object, usually done after object filtering. Inputs: img = RGB or grayscale image data for plotting contours = Contour list hierarchy = Contour hierarchy NumPy array Returns: group = grouped contours list mask = image mask :param img: numpy.ndarray :param contours: list :param hierarchy: numpy.ndarray :return group: list :return mask: numpy.ndarray """ params.device += 1 ori_img = np.copy(img) # If the reference image is grayscale convert it to color if len(np.shape(ori_img)) == 2: ori_img = cv2.cvtColor(ori_img, cv2.COLOR_GRAY2BGR) stack = np.zeros((len(contours), 1)) r, g, b = cv2.split(ori_img) mask = np.zeros(g.shape, dtype=np.uint8) for c, cnt in enumerate(contours): # if hierarchy[0][c][3] == -1: if hierarchy[0][c][2] == -1 and hierarchy[0][c][3] > -1: stack[c] = 0 else: stack[c] = 1 ids = np.where(stack == 1)[0] if len(ids) > 0: group = np.vstack(contours[i] for i in ids) cv2.drawContours(mask, contours, -1, 255, -1, hierarchy=hierarchy) if params.debug is not None: cv2.drawContours(ori_img, group, -1, (255, 0, 0), 4) for cnt in contours: cv2.drawContours(ori_img, cnt, -1, (255, 0, 0), 4) if params.debug == 'print': print_image( ori_img, os.path.join(params.debug_outdir, str(params.device) + '_objcomp.png')) print_image( ori_img, os.path.join(params.debug_outdir, str(params.device) + '_objcomp_mask.png')) elif params.debug == 'plot': plot_image(ori_img) return group, mask else: print("Warning: Invalid contour.") return None, None
def erode(gray_img, ksize, i): """Perform morphological 'erosion' filtering. Keeps pixel in center of the kernel if conditions set in kernel are true, otherwise removes pixel. Inputs: gray_img = Grayscale (usually binary) image data ksize = Kernel size (int). A ksize x ksize kernel will be built. Must be greater than 1 to have an effect. i = interations, i.e. number of consecutive filtering passes Returns: er_img = eroded image :param gray_img: numpy.ndarray :param ksize: int :param i: int :return er_img: numpy.ndarray """ if ksize <= 1: raise ValueError('ksize needs to be greater than 1 for the function to have an effect') kernel1 = int(ksize) kernel2 = np.ones((kernel1, kernel1), np.uint8) er_img = cv2.erode(src=gray_img, kernel=kernel2, iterations=i) params.device += 1 if params.debug == 'print': print_image(er_img, os.path.join(params.debug_outdir, str(params.device) + '_er_image' + str(ksize) + '_itr_' + str(i) + '.png')) elif params.debug == 'plot': plot_image(er_img, cmap='gray') return er_img
def dilate(gray_img, ksize, i): """Performs morphological 'dilation' filtering. Adds pixel to center of kernel if conditions set in kernel are true. Inputs: gray_img = Grayscale (usually binary) image data ksize = Kernel size (int). A k x k kernel will be built. Must be greater than 1 to have an effect. i = iterations, i.e. number of consecutive filtering passes Returns: dil_img = dilated image :param gray_img: numpy.ndarray :param ksize: int :param i: int :return dil_img: numpy.ndarray """ if ksize <= 1: raise ValueError('ksize needs to be greater than 1 for the function to have an effect') kernel1 = int(ksize) kernel2 = np.ones((kernel1, kernel1), np.uint8) dil_img = cv2.dilate(src=gray_img, kernel=kernel2, iterations=i) params.device += 1 if params.debug == 'print': print_image(dil_img, os.path.join(params.debug_outdir, str(params.device) + '_dil_image' + str(ksize) + '_itr' + str(i) + '.png')) elif params.debug == 'plot': plot_image(dil_img, cmap='gray') return dil_img
def laplace_filter(gray_img, ksize, scale): """This is a filtering method used to identify and highlight fine edges based on the 2nd derivative. A very sensetive method to highlight edges but will also amplify background noise. ddepth = -1 specifies that the dimensions of output image will be the same as the input image. Inputs: gray_img = Grayscale image data ksize = apertures size used to calculate 2nd derivative filter, specifies the size of the kernel (must be an odd integer: 1,3,5...) scale = scaling factor applied (multiplied) to computed Laplacian values (scale = 1 is unscaled) Returns: lp_filtered = laplacian filtered image :param gray_img: numpy.ndarray :param kernel: int :param scale: int :return lp_filtered: numpy.ndarray """ lp_filtered = cv2.Laplacian(src=gray_img, ddepth=-1, ksize=ksize, scale=scale) params.device += 1 if params.debug == 'print': print_image(lp_filtered, os.path.join(params.debug_outdir, str(params.device) + '_lp_out_k' + str(ksize) + '_scale' + str(scale) + '.png')) elif params.debug == 'plot': plot_image(lp_filtered, cmap='gray') return lp_filtered
def logical_xor(bin_img1, bin_img2): """Join two images using the bitwise XOR operator. Inputs: bin_img1 = Binary image data to be compared to bin_img2 bin_img2 = Binary image data to be compared to bin_img1 Returns: merged = joined binary image :param bin_img1: numpy.ndarray :param bin_img2: numpy.ndarray :return merged: numpy.ndarray """ params.device += 1 merged = cv2.bitwise_xor(bin_img1, bin_img2) if params.debug == 'print': print_image( merged, os.path.join(params.debug_outdir, str(params.device) + '_xor_joined.png')) elif params.debug == 'plot': plot_image(merged, cmap='gray') return merged
def rotate(img, rotation_deg, crop): """Rotate image, sometimes it is necessary to rotate image, especially when clustering for multiple plants is needed. Inputs: img = RGB or grayscale image data rotation_deg = rotation angle in degrees, can be a negative number, positive values move counter clockwise. crop = either true or false, if true, dimensions of rotated image will be same as original image. Returns: rotated_img = rotated image :param img: numpy.ndarray :param rotation_deg: double :param crop: bool :return rotated_img: numpy.ndarray """ if len(np.shape(img)) == 3: iy, ix, iz = np.shape(img) else: iy, ix = np.shape(img) m = cv2.getRotationMatrix2D((ix / 2, iy / 2), rotation_deg, 1) cos = np.abs(m[0, 0]) sin = np.abs(m[0, 1]) if not crop: # compute the new bounding dimensions of the image nw = int((iy * sin) + (ix * cos)) nh = int((iy * cos) + (ix * sin)) # adjust the rotation matrix to take into account translation m[0, 2] += (nw / 2) - (ix / 2) m[1, 2] += (nh / 2) - (iy / 2) rotated_img = cv2.warpAffine(img, m, (nw, nh)) else: rotated_img = cv2.warpAffine(img, m, (ix, iy)) params.device += 1 if params.debug == 'print': print_image( rotated_img, os.path.join( params.debug_outdir, str(params.device) + '_' + str(rotation_deg) + '_rotated_img.png')) elif params.debug == 'plot': if len(np.shape(img)) == 3: plot_image(rotated_img) else: plot_image(rotated_img, cmap='gray') return rotated_img
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 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 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 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 colorize_masks(masks, colors): """Plot masks with different colors Inputs: masks = list of masks to colorize colors = list of colors (either keys from the color_dict or a list of custom tuples) :param masks: list :param colors: list :return colored_img: ndarray """ # Users must enter the exact same number of colors as classes they'd like to color num_classes = len(masks) num_colors = len(colors) if not num_classes == num_colors: fatal_error("The number of colors provided doesn't match the number of class masks provided.") # Check to make sure user provided at least one mask and color if len(colors) == 0 or len(masks) == 0: fatal_error("At least one class mask and color must be provided.") # Dictionary of colors and the BGR values, based on some of the colors listed here: # https://en.wikipedia.org/wiki/X11_color_names color_dict = {'white': (255,255,255), 'black': (0,0,0), 'aqua': (0,255,255), 'blue': (255,0,0), 'blue violet': (228,44,138), 'brown': (41,41,168), 'chartreuse': (0,255,128), 'dark blue': (140,0,0), 'gray': (169,169,169), 'yellow': (0, 255, 255), 'turquoise': (210,210,64), 'red': (0, 0, 255), 'purple': (241,33,161), 'orange red': (0,69, 255), 'orange': (0,166,255), 'lime': (0, 255, 0), 'lime green': (52,205,52), 'fuchsia': (255,0,255), 'crimson': (61,20,220), 'beige': (197,220,246), 'chocolate': (31,105,210), 'coral': (79,128,255), 'dark green': (0,100,0), 'dark orange': (0,140,255), 'green yellow': (46,255,174), 'light blue': (230,218,174), 'tomato': (72,100,255), 'slate gray': (143,128,113), 'gold': (0,215,255), 'goldenrod': (33,166,218), 'light green': (143,238,143), 'sea green': (77,141,46), 'dark red': (0,0,141), 'pink': (204,192,255), 'dark yellow': (0,205,255), 'green': (0,255,0)} ix, iy = np.shape(masks[0]) colored_img = np.zeros((ix,iy,3), dtype=np.uint8) # Assign pixels to the selected color for i in range(0, len(masks)): mask = np.copy(masks[i]) mask = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR) if isinstance(colors[i], tuple): mask[masks[i] > 0] = colors[i] elif isinstance(colors[i], str): mask[masks[i] > 0] = color_dict[colors[i]] else: fatal_error("All elements of the 'colors' list must be either str or tuple") colored_img = colored_img + mask if params.debug == 'print': print_image(colored_img, os.path.join(params.debug_outdir, str(params.device) + '_classes_plot.png')) elif params.debug == 'plot': plot_image(colored_img) return colored_img
def object_composition(img, contours, hierarchy): """Groups objects into a single object, usually done after object filtering. Inputs: img = RGB or grayscale image data for plotting contours = Contour list hierarchy = Contour hierarchy NumPy array Returns: group = grouped contours list mask = image mask :param img: numpy.ndarray :param contours: list :param hierarchy: numpy.ndarray :return group: list :return mask: numpy.ndarray """ params.device += 1 ori_img = np.copy(img) # If the reference image is grayscale convert it to color if len(np.shape(ori_img)) == 2: ori_img = cv2.cvtColor(ori_img, cv2.COLOR_GRAY2BGR) stack = np.zeros((len(contours), 1)) r, g, b = cv2.split(ori_img) mask = np.zeros(g.shape, dtype=np.uint8) for c, cnt in enumerate(contours): # if hierarchy[0][c][3] == -1: if hierarchy[0][c][2] == -1 and hierarchy[0][c][3] > -1: stack[c] = 0 else: stack[c] = 1 ids = np.where(stack == 1)[0] if len(ids) > 0: group = np.vstack(contours[i] for i in ids) cv2.drawContours(mask, contours, -1, 255, -1, hierarchy=hierarchy) if params.debug is not None: cv2.drawContours(ori_img, group, -1, (255, 0, 0), params.line_thickness) for cnt in contours: cv2.drawContours(ori_img, cnt, -1, (255, 0, 0), params.line_thickness) if params.debug == 'print': print_image(ori_img, os.path.join(params.debug_outdir, str(params.device) + '_objcomp.png')) print_image(ori_img, os.path.join(params.debug_outdir, str(params.device) + '_objcomp_mask.png')) elif params.debug == 'plot': plot_image(ori_img) return group, mask else: print("Warning: Invalid contour.") return None, None
def _call_adaptive_threshold(gray_img, max_value, adaptive_method, threshold_method, method_name): # Threshold the image bin_img = cv2.adaptiveThreshold(gray_img, max_value, adaptive_method, threshold_method, 11, 2) # Print or plot the binary image if debug is on if params.debug == 'print': print_image(bin_img, os.path.join(params.debug_outdir, str(params.device) + method_name + '.png')) elif params.debug == 'plot': plot_image(bin_img, cmap='gray') return bin_img
def rotate(img, rotation_deg, crop): """Rotate image, sometimes it is necessary to rotate image, especially when clustering for multiple plants is needed. Inputs: img = RGB or grayscale image data rotation_deg = rotation angle in degrees, can be a negative number, positive values move counter clockwise. crop = either true or false, if true, dimensions of rotated image will be same as original image. Returns: rotated_img = rotated image :param img: numpy.ndarray :param rotation_deg: double :param crop: bool :return rotated_img: numpy.ndarray """ params.device += 1 if len(np.shape(img)) == 3: iy, ix, iz = np.shape(img) else: iy, ix = np.shape(img) m = cv2.getRotationMatrix2D((ix / 2, iy / 2), rotation_deg, 1) cos = np.abs(m[0, 0]) sin = np.abs(m[0, 1]) if not crop: # compute the new bounding dimensions of the image nw = int((iy * sin) + (ix * cos)) nh = int((iy * cos) + (ix * sin)) # adjust the rotation matrix to take into account translation m[0, 2] += (nw / 2) - (ix / 2) m[1, 2] += (nh / 2) - (iy / 2) rotated_img = cv2.warpAffine(img, m, (nw, nh)) else: rotated_img = cv2.warpAffine(img, m, (ix, iy)) if params.debug == 'print': print_image(rotated_img, os.path.join(params.debug_outdir, str(params.device) + str(rotation_deg) + '_rotated_img.png')) elif params.debug == 'plot': if len(np.shape(img)) == 3: plot_image(rotated_img) else: plot_image(rotated_img, cmap='gray') return rotated_img
def segment_path_length(segmented_img, objects): """ Use segments to calculate geodesic distance 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 """ label_coord_x = [] label_coord_y = [] segment_lengths = [] labeled_img = segmented_img.copy() for i, cnt in enumerate(objects): # Calculate geodesic distance, divide by two since cv2 seems to be taking the perimeter of the contour segment_lengths.append(cv2.arcLength(objects[i], False) / 2) # Store coordinates for labels label_coord_x.append(objects[i][0][0][0]) label_coord_y.append(objects[i][0][0][1]) segment_ids = [] # Put labels of length for c, value in enumerate(segment_lengths): text = "{:.2f}".format(value) w = label_coord_x[c] h = label_coord_y[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_path_length', trait='segment path length', method='plantcv.plantcv.morphology.segment_path_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_path_lengths.png')) elif params.debug == 'plot': plot_image(labeled_img) return labeled_img
def readimage(filename, mode="native"): """Read image from file. Inputs: filename = name of image file mode = mode of imread ("native", "rgb", "rgba", "gray") Returns: img = image object as numpy array path = path to image file img_name = name of image file :param filename: str :param mode: str :return img: numpy.ndarray :return path: str :return img_name: str """ if mode.upper() == "GRAY" or mode.upper() == "GREY": img = cv2.imread(filename, 0) elif mode.upper() == "RGB": img = cv2.imread(filename) elif mode.upper() == "RGBA": img = cv2.imread(filename, -1) else: img = cv2.imread(filename, -1) # Default to drop alpha channel if user doesn't specify 'rgba' if len(np.shape(img))==3 and np.shape(img)[2] == 4 and mode.upper() == "NATIVE": img = cv2.imread(filename) if img is None: fatal_error("Failed to open " + filename) # Split path from filename path, img_name = os.path.split(filename) if params.debug == "print": print_image(img, os.path.join(params.debug_outdir, "input_image.png")) elif params.debug == "plot": plot_image(img) return img, path, img_name
def _draw_roi(img, roi_contour): """Draw an ROI :param img: numpy.ndarray :param roi_contour: list """ # 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) # Draw the contour on the reference image cv2.drawContours(ref_img, roi_contour, -1, (255, 0, 0), params.line_thickness) if params.debug == "print": # If debug is print, save the image to a file print_image(ref_img, os.path.join(params.debug_outdir, str(params.device) + "_roi.png")) elif params.debug == "plot": # If debug is plot, print to the plotting device plot_image(ref_img)
def rgb2gray(rgb_img): """Convert image from RGB colorspace to Gray. Inputs: rgb_img = RGB image data Returns: gray = grayscale image :param rgb_img: numpy.ndarray :return gray: numpy.ndarray """ gray = cv2.cvtColor(rgb_img, cv2.COLOR_BGR2GRAY) params.device += 1 if params.debug == 'print': print_image(gray, os.path.join(params.debug_outdir, str(params.device) + '_gray.png')) elif params.debug == 'plot': plot_image(gray, cmap='gray') return gray
def logical_xor(bin_img1, bin_img2): """Join two images using the bitwise XOR operator. Inputs: bin_img1 = Binary image data to be compared to bin_img2 bin_img2 = Binary image data to be compared to bin_img1 Returns: merged = joined binary image :param bin_img1: numpy.ndarray :param bin_img2: numpy.ndarray :return merged: numpy.ndarray """ params.device += 1 merged = cv2.bitwise_xor(bin_img1, bin_img2) if params.debug == 'print': print_image(merged, os.path.join(params.debug_outdir, str(params.device) + '_xor_joined.png')) elif params.debug == 'plot': plot_image(merged, cmap='gray') return merged
def rgb2gray_lab(rgb_img, channel): """Convert image from RGB colorspace to LAB colorspace. Returns the specified subchannel as a gray image. Inputs: rgb_img = RGB image data channel = color subchannel (l = lightness, a = green-magenta, b = blue-yellow) Returns: l | a | b = grayscale image from one LAB color channel :param rgb_img: numpy.ndarray :param channel: str :return channel: numpy.ndarray """ # Auto-increment the device counter params.device += 1 # The allowable channel inputs are l, a or b names = {"l": "lightness", "a": "green-magenta", "b": "blue-yellow"} channel = channel.lower() if channel not in names: fatal_error("Channel " + str(channel) + " is not l, a or b!") # Convert the input BGR image to LAB colorspace lab = cv2.cvtColor(rgb_img, cv2.COLOR_BGR2LAB) # Split LAB channels l, a, b = cv2.split(lab) # Create a channel dictionaries for lookups by a channel name index channels = {"l": l, "a": a, "b": b} if params.debug == "print": print_image(channels[channel], os.path.join(params.debug_outdir, str(params.device) + "_lab_" + names[channel] + ".png")) elif params.debug == "plot": plot_image(channels[channel], cmap="gray") return channels[channel]
def fill(bin_img, size): """Identifies objects and fills objects that are less than size. Inputs: bin_img = Binary image data size = minimum object area size in pixels (integer) Returns: filtered_img = image with objects filled :param bin_img: numpy.ndarray :param size: int :return filtered_img: numpy.ndarray """ params.device += 1 # Make sure the image is binary if len(np.shape(bin_img)) != 2 or len(np.unique(bin_img)) != 2: fatal_error("Image is not binary") # Cast binary image to boolean bool_img = bin_img.astype(bool) # Find and fill contours bool_img = remove_small_objects(bool_img, size) # Cast boolean image to binary and make a copy of the binary image for returning filtered_img = np.copy(bool_img.astype(np.uint8) * 255) if params.debug == 'print': print_image(filtered_img, os.path.join(params.debug_outdir, str(params.device) + '_fill' + str(size) + '.png')) elif params.debug == 'plot': plot_image(filtered_img, cmap='gray') return filtered_img