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 _debug(visual=bin_img, filename=os.path.join(params.debug_outdir, str(params.device) + method_name + '.png')) return bin_img
def overlay_two_imgs(img1, img2, alpha=0.5): """Overlay two images with a given alpha value. Inputs: img1 - RGB or grayscale image data img2 - RGB or grayscale image data alpha - Desired opacity of 1st image, range: (0,1), default value=0.5 Returns: out_img - Blended RGB image :param img1: numpy.ndarray :param img2: numpy.ndarray :param alpha: float :return: out_img: numpy.ndarray """ # Validate alpha if alpha > 1 or alpha < 0: fatal_error("The value of alpha should be in the range of (0,1)!") # Validate image sizes are the same size_img1 = img1.shape[0:2] size_img2 = img2.shape[0:2] if size_img1 != size_img2: fatal_error(f"The height/width of img1 ({size_img1}) needs to match img2 ({size_img2}).") # Convert the datatype of the image such that img1 = _preprocess_img_dtype(img1) img2 = _preprocess_img_dtype(img2) # Copy the input images img1_ = np.copy(img1) img2_ = np.copy(img2) # If the images are grayscale convert to BGR if len(img1_.shape) == 2: img1_ = cv2.cvtColor(img1_, cv2.COLOR_GRAY2BGR) if len(img2_.shape) == 2: img2_ = cv2.cvtColor(img2_, cv2.COLOR_GRAY2BGR) # initialize the output image out_img = np.zeros(size_img1 + (3,), dtype=np.uint8) # blending out_img[:, :, :] = (alpha * img1_[:, :, :]) + ((1 - alpha) * img2_[:, :, :]) _debug(visual=out_img, filename=os.path.join(params.debug_outdir, str(params.device) + '_overlay.png')) return out_img
def rgb2gray_cmyk(rgb_img, channel): """Convert image from RGB colorspace to CMYK colorspace. Returns the specified subchannel as a gray image. Inputs: rgb_img = RGB image data channel = color subchannel (c = cyan, m = magenta, y = yellow, k=black) Returns: c | m | y | k = grayscale image from one CMYK color channel :param rgb_img: numpy.ndarray :param channel: str :return channel: numpy.ndarray """ # The allowable channel inputs are c, m , y or k names = {"c": "cyan", "m": "magenta", "y": "yellow", "k": "black"} channel = channel.lower() if channel not in names: fatal_error("Channel " + str(channel) + " is not c, m, y or k!") # Create float bgr = rgb_img.astype(float) / 255. # K channel k = 1 - np.max(bgr, axis=2) # C Channel c = (1 - bgr[..., 2] - k) / (1 - k) # M Channel m = (1 - bgr[..., 1] - k) / (1 - k) # Y Channel y = (1 - bgr[..., 0] - k) / (1 - k) # Convert the input BGR image to LAB colorspace cmyk = (np.dstack((c, m, y, k)) * 255).astype(np.uint8) # Split CMYK channels y, m, c, k = cv2.split(cmyk) # Create a channel dictionaries for lookups by a channel name index channels = {"c": c, "m": m, "y": y, "k": k} # Save or display the grayscale image _debug(visual=channels[channel], filename=os.path.join( params.debug_outdir, str(params.device) + "_cmyk_" + names[channel] + ".png")) return channels[channel]
def readimage(filename, mode="native"): """Read image from file. Inputs: filename = name of image file mode = mode of imread ("native", "rgb", "rgba", "gray", "csv", "envi") 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) elif mode.upper() == "CSV": inputarray = pd.read_csv(filename, sep=',', header=None) img = inputarray.values elif mode.upper() == "ENVI": array_data = read_data(filename) return array_data 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) # Debugging visualization _debug(visual=img, filename=os.path.join(params.debug_outdir, "input_image.png")) return img, path, img_name
def mask_bad(float_img, bad_type='native'): """ Create a mask with desired "bad" pixels of the input floaat image marked. Inputs: float_img = image represented by an nd-array (data type: float). Most probably, it is the result of some calculation based on the original image. So the datatype is float, and it is possible to have some "bad" values, i.e. nan and/or inf bad_type = definition of "bad" type, can be 'nan', 'inf' or 'native' Returns: mask = A mask indicating the locations of "bad" pixels :param float_img: numpy.ndarray :param bad_type: str :return mask: numpy.ndarray """ size_img = np.shape(float_img) if len(size_img) != 2: fatal_error('Input image is not a single channel image!') mask = np.zeros(size_img, dtype='uint8') idx_nan, idy_nan = np.where(np.isnan(float_img) == 1) idx_inf, idy_inf = np.where(np.isinf(float_img) == 1) # neither nan nor inf exists in the image, print out a message and the mask would just be all zero if len(idx_nan) == 0 and len(idx_inf) == 0: mask = mask print('Neither nan nor inf appears in the current image.') # at least one of the "bad" exists # desired bad to mark is "native" elif bad_type.lower() == 'native': # mask[np.isnan(gray_img)] = 255 # mask[np.isinf(gray_img)] = 255 mask[idx_nan, idy_nan] = 255 mask[idx_inf, idy_inf] = 255 elif bad_type.lower() == 'nan' and len(idx_nan) >= 1: mask[idx_nan, idy_nan] = 255 elif bad_type.lower() == 'inf' and len(idx_inf) >= 1: mask[idx_inf, idy_inf] = 255 # "bad" exists but not the user desired bad type, return the all-zero mask else: mask = mask print('{} does not appear in the current image.'.format( bad_type.lower())) _debug(visual=mask, filename=os.path.join(params.debug_outdir, str(params.device) + "_bad_mask.png")) return mask
def _call_threshold(gray_img, threshold, max_value, threshold_method, method_name): # Threshold the image ret, bin_img = cv2.threshold(gray_img, threshold, max_value, threshold_method) if bin_img.dtype != 'uint16': bin_img = np.uint8(bin_img) # Print or plot the binary image if debug is on _debug(visual=bin_img, filename=os.path.join( params.debug_outdir, str(params.device) + method_name + str(threshold) + '.png')) return bin_img
def _package_index(hsi, raw_index, method): """Private function to package raw index array as a Spectral_data object. Inputs: hsi = hyperspectral data (Spectral_data object) raw_index = raw index array method = index method (e.g. NDVI) Returns: index = index image as a Spectral_data object. :params hsi: __main__.Spectral_data :params raw_index: np.array :params method: str :params index: __main__.Spectral_data """ params.device += 1 # Store debug mode debug = params.debug params.debug = None # Resulting array is float 32 from varying natural ranges, transform into uint8 for plotting all_positive = np.add(raw_index, 2 * np.ones(np.shape(raw_index))) scaled = rescale(all_positive) # Find array min and max values obs_max_pixel = float(np.nanmax(raw_index)) obs_min_pixel = float(np.nanmin(raw_index)) index = Spectral_data(array_data=raw_index, max_wavelength=0, min_wavelength=0, max_value=obs_max_pixel, min_value=obs_min_pixel, d_type=np.uint8, wavelength_dict={}, samples=hsi.samples, lines=hsi.lines, interleave=hsi.interleave, wavelength_units=hsi.wavelength_units, array_type="index_" + method.lower(), pseudo_rgb=scaled, filename=hsi.filename, default_bands=None) # Restore debug mode params.debug = debug _debug(visual=index.pseudo_rgb, filename=os.path.join(params.debug_outdir, str(params.device) + method + "_index.png")) return index
def saturation(rgb_img, threshold=255, channel="any"): """Return a mask filtering out saturated pixels. Inputs: rgb_img = RGB image threshold = value for threshold, above which is considered saturated channel = how many channels must be saturated for the pixel to be masked out ("any", "all") Returns: masked_img = A binary image with the saturated regions blacked out. :param rgb_img: np.ndarray :param threshold: int :param channel: str :return masked_img: np.ndarray """ # Mask red, green, and blue saturation separately b, g, r = cv2.split(rgb_img) b_saturated = cv2.inRange(b, threshold, 255) g_saturated = cv2.inRange(g, threshold, 255) r_saturated = cv2.inRange(r, threshold, 255) # Combine channel masks if channel.lower() == "any": # Consider a pixel saturated if any channel is saturated saturated = cv2.bitwise_or(b_saturated, g_saturated) saturated = cv2.bitwise_or(saturated, r_saturated) elif channel.lower() == "all": # Consider a pixel saturated only if all channels are saturated saturated = cv2.bitwise_and(b_saturated, g_saturated) saturated = cv2.bitwise_and(saturated, r_saturated) else: fatal_error( str(channel) + " is not a valid option. Channel must be either 'any', or 'all'.") # Invert "saturated" before returning, so saturated = black bin_img = cv2.bitwise_not(saturated) _debug(visual=bin_img, filename=os.path.join(params.debug_outdir, str(params.device), '_saturation_threshold.png')) return bin_img
def analyze_thermal_values(thermal_array, mask, histplot=None, label="default"): """This extracts the thermal values of each pixel writes the values out to a file. It can also print out a histogram plot of pixel intensity and a pseudocolor image of the plant. Inputs: array = numpy array of thermal values mask = Binary mask made from selected contours histplot = if True plots histogram of intensity values label = optional label parameter, modifies the variable name of observations recorded Returns: analysis_image = output image :param thermal_array: numpy.ndarray :param mask: numpy.ndarray :param histplot: bool :param label: str :return analysis_image: ggplot """ if histplot is not None: deprecation_warning( "'histplot' will be deprecated in a future version of PlantCV. " "This function creates a histogram by default.") # Store debug mode debug = params.debug # apply plant shaped mask to image and calculate statistics based on the masked image masked_thermal = thermal_array[np.where(mask > 0)] maxtemp = np.amax(masked_thermal) mintemp = np.amin(masked_thermal) avgtemp = np.average(masked_thermal) mediantemp = np.median(masked_thermal) # call the histogram function params.debug = None hist_fig, hist_data = histogram(thermal_array, mask=mask, hist_data=True) bin_labels, hist_percent = hist_data['pixel intensity'].tolist( ), hist_data['proportion of pixels (%)'].tolist() # Store data into outputs class outputs.add_observation(sample=label, variable='max_temp', trait='maximum temperature', method='plantcv.plantcv.analyze_thermal_values', scale='degrees', datatype=float, value=maxtemp, label='degrees') outputs.add_observation(sample=label, variable='min_temp', trait='minimum temperature', method='plantcv.plantcv.analyze_thermal_values', scale='degrees', datatype=float, value=mintemp, label='degrees') outputs.add_observation(sample=label, variable='mean_temp', trait='mean temperature', method='plantcv.plantcv.analyze_thermal_values', scale='degrees', datatype=float, value=avgtemp, label='degrees') outputs.add_observation(sample=label, variable='median_temp', trait='median temperature', method='plantcv.plantcv.analyze_thermal_values', scale='degrees', datatype=float, value=mediantemp, label='degrees') outputs.add_observation(sample=label, variable='thermal_frequencies', trait='thermal frequencies', method='plantcv.plantcv.analyze_thermal_values', scale='frequency', datatype=list, value=hist_percent, label=bin_labels) # Restore user debug setting params.debug = debug # change column names of "hist_data" hist_fig = hist_fig + labs(x="Temperature C", y="Proportion of pixels (%)") # Print or plot histogram _debug(visual=hist_fig, filename=os.path.join(params.debug_outdir, str(params.device) + "_therm_histogram.png")) analysis_image = hist_fig # Store images outputs.images.append(analysis_image) return analysis_image
def analyze_spectral(array, mask, histplot=None, label="default"): """This extracts the hyperspectral reflectance values of each pixel writes the values out to a file. It can also print out a histogram plot of pixel intensity and a pseudocolor image of the plant. Inputs: array = Hyperspectral data instance mask = Binary mask made from selected contours histplot = (to be deprecated) if True plots histogram of reflectance intensity values label = optional label parameter, modifies the variable name of observations recorded Returns: analysis_img = output image :param array: __main__.Spectral_data :param mask: numpy array :param histplot: bool :param label: str :return analysis_img: ggplot """ if histplot is not None: deprecation_warning("'histplot' will be deprecated in a future version of PlantCV. " "Instead of a histogram this function plots the mean of spectra in the masked area.") array_data = array.array_data # List of wavelengths recorded created from parsing the header file will be string, make list of floats wavelength_data = array_data[np.where(mask > 0)] # Calculate mean reflectance across wavelengths wavelength_freq = wavelength_data.mean(axis=0) max_per_band = wavelength_data.max(axis=0) min_per_band = wavelength_data.min(axis=0) std_per_band = wavelength_data.std(axis=0) # Identify smallest and largest wavelengths available to scale the x-axis min_wavelength = array.min_wavelength max_wavelength = array.max_wavelength # Create lists with wavelengths in float format rather than as strings # and make a list of the frequencies since they are in an array new_wavelengths = [] new_freq = [] new_std_per_band = [] new_max_per_band = [] new_min_per_band = [] for i, wavelength in enumerate(array.wavelength_dict): new_wavelengths.append(wavelength) new_freq.append((wavelength_freq[i]).astype(float)) new_std_per_band.append(std_per_band[i].astype(float)) new_max_per_band.append(max_per_band[i].astype(float)) new_min_per_band.append(min_per_band[i].astype(float)) # Calculate reflectance statistics avg_reflectance = np.average(wavelength_data) std_reflectance = np.std(wavelength_data) median_reflectance = np.median(wavelength_data) wavelength_labels = [] for i in array.wavelength_dict.keys(): wavelength_labels.append(i) # Store data into outputs class outputs.add_observation(sample=label, variable='global_mean_reflectance', trait='global mean reflectance', method='plantcv.plantcv.hyperspectral.analyze_spectral', scale='reflectance', datatype=float, value=float(avg_reflectance), label='reflectance') outputs.add_observation(sample=label, variable='global_median_reflectance', trait='global median reflectance', method='plantcv.plantcv.hyperspectral.analyze_spectral', scale='reflectance', datatype=float, value=float(median_reflectance), label='reflectance') outputs.add_observation(sample=label, variable='global_spectral_std', trait='pixel-wise standard deviation per band', method='plantcv.plantcv.hyperspectral.analyze_spectral', scale='None', datatype=float, value=float(std_reflectance), label='reflectance') outputs.add_observation(sample=label, variable='global_spectral_std', trait='pixel-wise standard deviation ', method='plantcv.plantcv.hyperspectral.analyze_spectral', scale='None', datatype=float, value=float(std_reflectance), label='reflectance') outputs.add_observation(sample=label, variable='max_reflectance', trait='maximum reflectance per band', method='plantcv.plantcv.hyperspectral.analyze_spectral', scale='reflectance', datatype=list, value=new_max_per_band, label=wavelength_labels) outputs.add_observation(sample=label, variable='min_reflectance', trait='minimum reflectance per band', method='plantcv.plantcv.hyperspectral.analyze_spectral', scale='reflectance', datatype=list, value=new_min_per_band, label=wavelength_labels) outputs.add_observation(sample=label, variable='spectral_std', trait='pixel-wise standard deviation per band', method='plantcv.plantcv.hyperspectral.analyze_spectral', scale='None', datatype=list, value=new_std_per_band, label=wavelength_labels) outputs.add_observation(sample=label, variable='spectral_frequencies', trait='spectral frequencies', method='plantcv.plantcv.hyperspectral.analyze_spectral', scale='frequency', datatype=list, value=new_freq, label=wavelength_labels) dataset = pd.DataFrame({'Wavelength (' + array.wavelength_units + ')': new_wavelengths, 'Reflectance': wavelength_freq}) mean_spectra = (ggplot(data=dataset, mapping=aes(x='Wavelength (' + array.wavelength_units + ')', y='Reflectance')) + geom_line(color='purple') + scale_x_continuous(breaks=list(range(int(np.floor(min_wavelength)), int(np.ceil(max_wavelength)), 50))) ) analysis_img = mean_spectra _debug(visual=mean_spectra, filename=os.path.join(params.debug_outdir, str(params.device) + "_mean_spectra.png")) return analysis_img
def analyze_color(rgb_img, mask, hist_plot_type=None, colorspaces="all", label="default"): """Analyze the color properties of an image object Inputs: rgb_img = RGB image data mask = Binary mask made from selected contours hist_plot_type = None, 'all', 'rgb','lab' or 'hsv' (to be deprecated) colorspaces = 'all', 'rgb', 'lab', or 'hsv' label = optional label parameter, modifies the variable name of observations recorded Returns: analysis_image = histogram output :param rgb_img: numpy.ndarray :param mask: numpy.ndarray :param colorspaces: str :param hist_plot_type: str :param label: str :return analysis_images: list """ # Save user debug setting debug = params.debug if hist_plot_type is not None: deprecation_warning( "'hist_plot_type' will be deprecated in a future version of PlantCV. " "Please use 'colorspaces' instead.") colorspaces = hist_plot_type if len(np.shape(rgb_img)) < 3: fatal_error("rgb_img must be an RGB image") # Mask the input image masked = cv2.bitwise_and(rgb_img, rgb_img, mask=mask) # Extract the blue, green, and red channels b, g, r = cv2.split(masked) # Convert the BGR image to LAB lab = cv2.cvtColor(masked, cv2.COLOR_BGR2LAB) # Extract the lightness, green-magenta, and blue-yellow channels l, m, y = cv2.split(lab) # Convert the BGR image to HSV hsv = cv2.cvtColor(masked, cv2.COLOR_BGR2HSV) # Extract the hue, saturation, and value channels h, s, v = cv2.split(hsv) # Color channel dictionary channels = { "b": b, "g": g, "r": r, "l": l, "m": m, "y": y, "h": h, "s": s, "v": v } # Histogram plot types hist_types = { "all": ("b", "g", "r", "l", "m", "y", "h", "s", "v"), "rgb": ("b", "g", "r"), "lab": ("l", "m", "y"), "hsv": ("h", "s", "v") } if colorspaces.lower() not in hist_types: fatal_error( f"Colorspace '{colorspaces}' is not supported, must be be one of the following: " f"{', '.join(map(str, hist_types.keys()))}") # Calculate histogram params.debug = None histograms = { "b": { "label": "blue", "graph_color": "blue", "hist": histogram(channels["b"], mask, 256, 0, 255, hist_data=True)[1]['proportion of pixels (%)'].tolist() }, "g": { "label": "green", "graph_color": "forestgreen", "hist": histogram(channels["g"], mask, 256, 0, 255, hist_data=True)[1]['proportion of pixels (%)'].tolist() }, "r": { "label": "red", "graph_color": "red", "hist": histogram(channels["r"], mask, 256, 0, 255, hist_data=True)[1]['proportion of pixels (%)'].tolist() }, "l": { "label": "lightness", "graph_color": "dimgray", "hist": histogram(channels["l"], mask, 256, 0, 255, hist_data=True)[1]['proportion of pixels (%)'].tolist() }, "m": { "label": "green-magenta", "graph_color": "magenta", "hist": histogram(channels["m"], mask, 256, 0, 255, hist_data=True)[1]['proportion of pixels (%)'].tolist() }, "y": { "label": "blue-yellow", "graph_color": "yellow", "hist": histogram(channels["y"], mask, 256, 0, 255, hist_data=True)[1]['proportion of pixels (%)'].tolist() }, "h": { "label": "hue", "graph_color": "blueviolet", "hist": histogram(channels["h"], mask, 256, 0, 255, hist_data=True)[1]['proportion of pixels (%)'].tolist() }, "s": { "label": "saturation", "graph_color": "cyan", "hist": histogram(channels["s"], mask, 256, 0, 255, hist_data=True)[1]['proportion of pixels (%)'].tolist() }, "v": { "label": "value", "graph_color": "orange", "hist": histogram(channels["v"], mask, 256, 0, 255, hist_data=True)[1]['proportion of pixels (%)'].tolist() } } # Restore user debug setting params.debug = debug # Create list of bin labels for 8-bit data binval = np.arange(0, 256) # Create a dataframe of bin labels and histogram data dataset = pd.DataFrame({ 'bins': binval, 'blue': histograms["b"]["hist"], 'green': histograms["g"]["hist"], 'red': histograms["r"]["hist"], 'lightness': histograms["l"]["hist"], 'green-magenta': histograms["m"]["hist"], 'blue-yellow': histograms["y"]["hist"], 'hue': histograms["h"]["hist"], 'saturation': histograms["s"]["hist"], 'value': histograms["v"]["hist"] }) # Make the histogram figure using plotnine if colorspaces.upper() == 'RGB': df_rgb = pd.melt(dataset, id_vars=['bins'], value_vars=['blue', 'green', 'red'], var_name='color Channel', value_name='proportion of pixels (%)') hist_fig = (ggplot( df_rgb, aes(x='bins', y='proportion of pixels (%)', color='color Channel')) + geom_line() + scale_x_continuous(breaks=list(range(0, 256, 25))) + scale_color_manual(['blue', 'green', 'red'])) elif colorspaces.upper() == 'LAB': df_lab = pd.melt( dataset, id_vars=['bins'], value_vars=['lightness', 'green-magenta', 'blue-yellow'], var_name='color Channel', value_name='proportion of pixels (%)') hist_fig = (ggplot( df_lab, aes(x='bins', y='proportion of pixels (%)', color='color Channel')) + geom_line() + scale_x_continuous(breaks=list(range(0, 256, 25))) + scale_color_manual(['yellow', 'magenta', 'dimgray'])) elif colorspaces.upper() == 'HSV': df_hsv = pd.melt(dataset, id_vars=['bins'], value_vars=['hue', 'saturation', 'value'], var_name='color Channel', value_name='proportion of pixels (%)') hist_fig = (ggplot( df_hsv, aes(x='bins', y='proportion of pixels (%)', color='color Channel')) + geom_line() + scale_x_continuous(breaks=list(range(0, 256, 25))) + scale_color_manual(['blueviolet', 'cyan', 'orange'])) elif colorspaces.upper() == 'ALL': s = pd.Series([ 'blue', 'green', 'red', 'lightness', 'green-magenta', 'blue-yellow', 'hue', 'saturation', 'value' ], dtype="category") color_channels = [ 'blue', 'yellow', 'green', 'magenta', 'blueviolet', 'dimgray', 'red', 'cyan', 'orange' ] df_all = pd.melt(dataset, id_vars=['bins'], value_vars=s, var_name='color Channel', value_name='proportion of pixels (%)') hist_fig = (ggplot( df_all, aes(x='bins', y='proportion of pixels (%)', color='color Channel')) + geom_line() + scale_x_continuous(breaks=list(range(0, 256, 25))) + scale_color_manual(color_channels)) hist_fig = hist_fig + labs(x="Pixel intensity", y="Proportion of pixels (%)") # Hue values of zero are red but are also the value for pixels where hue is undefined. The hue value of a pixel will # be undef. when the color values are saturated. Therefore, hue values of 0 are excluded from the calculations below # Calculate the median hue value (median is rescaled from the encoded 0-179 range to the 0-359 degree range) hue_median = np.median(h[np.where(h > 0)]) * 2 # Calculate the circular mean and standard deviation of the encoded hue values # The mean and standard-deviation are rescaled from the encoded 0-179 range to the 0-359 degree range hue_circular_mean = stats.circmean(h[np.where(h > 0)], high=179, low=0) * 2 hue_circular_std = stats.circstd(h[np.where(h > 0)], high=179, low=0) * 2 # Plot or print the histogram analysis_image = hist_fig _debug(visual=hist_fig, filename=os.path.join( params.debug_outdir, str(params.device) + '_analyze_color_hist.png')) # Store into global measurements # RGB signal values are in an unsigned 8-bit scale of 0-255 rgb_values = [i for i in range(0, 256)] # Hue values are in a 0-359 degree scale, every 2 degrees at the midpoint of the interval hue_values = [i * 2 + 1 for i in range(0, 180)] # Percentage values on a 0-100 scale (lightness, saturation, and value) percent_values = [round((i / 255) * 100, 2) for i in range(0, 256)] # Diverging values on a -128 to 127 scale (green-magenta and blue-yellow) diverging_values = [i for i in range(-128, 128)] if colorspaces.upper() == 'RGB' or colorspaces.upper() == 'ALL': outputs.add_observation(sample=label, variable='blue_frequencies', trait='blue frequencies', method='plantcv.plantcv.analyze_color', scale='frequency', datatype=list, value=histograms["b"]["hist"], label=rgb_values) outputs.add_observation(sample=label, variable='green_frequencies', trait='green frequencies', method='plantcv.plantcv.analyze_color', scale='frequency', datatype=list, value=histograms["g"]["hist"], label=rgb_values) outputs.add_observation(sample=label, variable='red_frequencies', trait='red frequencies', method='plantcv.plantcv.analyze_color', scale='frequency', datatype=list, value=histograms["r"]["hist"], label=rgb_values) if colorspaces.upper() == 'LAB' or colorspaces.upper() == 'ALL': outputs.add_observation(sample=label, variable='lightness_frequencies', trait='lightness frequencies', method='plantcv.plantcv.analyze_color', scale='frequency', datatype=list, value=histograms["l"]["hist"], label=percent_values) outputs.add_observation(sample=label, variable='green-magenta_frequencies', trait='green-magenta frequencies', method='plantcv.plantcv.analyze_color', scale='frequency', datatype=list, value=histograms["m"]["hist"], label=diverging_values) outputs.add_observation(sample=label, variable='blue-yellow_frequencies', trait='blue-yellow frequencies', method='plantcv.plantcv.analyze_color', scale='frequency', datatype=list, value=histograms["y"]["hist"], label=diverging_values) if colorspaces.upper() == 'HSV' or colorspaces.upper() == 'ALL': outputs.add_observation(sample=label, variable='hue_frequencies', trait='hue frequencies', method='plantcv.plantcv.analyze_color', scale='frequency', datatype=list, value=histograms["h"]["hist"][0:180], label=hue_values) outputs.add_observation(sample=label, variable='saturation_frequencies', trait='saturation frequencies', method='plantcv.plantcv.analyze_color', scale='frequency', datatype=list, value=histograms["s"]["hist"], label=percent_values) outputs.add_observation(sample=label, variable='value_frequencies', trait='value frequencies', method='plantcv.plantcv.analyze_color', scale='frequency', datatype=list, value=histograms["v"]["hist"], label=percent_values) # Always save hue stats outputs.add_observation(sample=label, variable='hue_circular_mean', trait='hue circular mean', method='plantcv.plantcv.analyze_color', scale='degrees', datatype=float, value=hue_circular_mean, label='degrees') outputs.add_observation(sample=label, variable='hue_circular_std', trait='hue circular standard deviation', method='plantcv.plantcv.analyze_color', scale='degrees', datatype=float, value=hue_circular_std, label='degrees') outputs.add_observation(sample=label, variable='hue_median', trait='hue median', method='plantcv.plantcv.analyze_color', scale='degrees', datatype=float, value=hue_median, label='degrees') # Store images outputs.images.append(analysis_image) return analysis_image
def analyze_index(index_array, mask, bins=100, min_bin=0, max_bin=1, histplot=None, label="default"): """This extracts the hyperspectral index statistics and writes the values as observations out to the Outputs class. Inputs: index_array = Instance of the Spectral_data class, usually the output from pcv.hyperspectral.extract_index mask = Binary mask made from selected contours histplot = if True plots histogram of intensity values bins = optional, number of classes to divide spectrum into min_bin = optional, minimum bin value ("auto" or user input minimum value) max_bin = optional, maximum bin value ("auto" or user input maximum value) label = optional label parameter, modifies the variable name of observations recorded :param index_array: __main__.Spectral_data :param mask: numpy array :param histplot: bool :param bins: int :param max_bin: float, str :param min_bin: float, str :param label: str :return analysis_image: ggplot, None """ if histplot is not None: deprecation_warning( "'histplot' will be deprecated in a future version of PlantCV. " "This function creates a histogram by default.") debug = params.debug params.debug = None if len(np.shape(mask)) > 2 or len(np.unique(mask)) > 2: fatal_error("Mask should be a binary image of 0 and nonzero values.") if len(np.shape(index_array.array_data)) > 2: fatal_error("index_array data should be a grayscale image.") # Mask data and collect statistics about pixels within the masked image masked_array = index_array.array_data[np.where(mask > 0)] masked_array = masked_array[np.isfinite(masked_array)] index_mean = np.nanmean(masked_array) index_median = np.nanmedian(masked_array) index_std = np.nanstd(masked_array) # Set starting point and max bin values maxval = max_bin b = min_bin # Calculate observed min and max pixel values of the masked array observed_max = np.nanmax(masked_array) observed_min = np.nanmin(masked_array) # Auto calculate max_bin if set if type(max_bin) is str and (max_bin.upper() == "AUTO"): maxval = float( round(observed_max, 8) ) # Auto bins will detect maxval to use for calculating labels/bins if type(min_bin) is str and (min_bin.upper() == "AUTO"): b = float(round(observed_min, 8)) # If bin_min is auto then overwrite starting value # Print a warning if observed min/max outside user defined range if observed_max > maxval or observed_min < b: print( "WARNING!!! The observed range of pixel values in your masked index provided is [" + str(observed_min) + ", " + str(observed_max) + "] but the user defined range of bins for pixel frequencies is [" + str(b) + ", " + str(maxval) + "]. Adjust min_bin and max_bin in order to avoid cutting off data being collected." ) # Calculate histogram hist_fig, hist_data = histogram(index_array.array_data, mask=mask, bins=bins, lower_bound=b, upper_bound=maxval, hist_data=True) bin_labels, hist_percent = hist_data['pixel intensity'].tolist( ), hist_data['proportion of pixels (%)'].tolist() # Restore user debug setting params.debug = debug hist_fig = hist_fig + labs(x='Index Reflectance', y='Proportion of pixels (%)') # Print or plot histogram _debug(visual=hist_fig, filename=os.path.join( params.debug_outdir, str(params.device) + index_array.array_type + "_hist.png")) analysis_image = hist_fig outputs.add_observation( sample=label, variable='mean_' + index_array.array_type, trait='Average ' + index_array.array_type + ' reflectance', method='plantcv.plantcv.hyperspectral.analyze_index', scale='reflectance', datatype=float, value=float(index_mean), label='none') outputs.add_observation( sample=label, variable='med_' + index_array.array_type, trait='Median ' + index_array.array_type + ' reflectance', method='plantcv.plantcv.hyperspectral.analyze_index', scale='reflectance', datatype=float, value=float(index_median), label='none') outputs.add_observation( sample=label, variable='std_' + index_array.array_type, trait='Standard deviation ' + index_array.array_type + ' reflectance', method='plantcv.plantcv.hyperspectral.analyze_index', scale='reflectance', datatype=float, value=float(index_std), label='none') outputs.add_observation(sample=label, variable='index_frequencies_' + index_array.array_type, trait='index frequencies', method='plantcv.plantcv.analyze_index', scale='frequency', datatype=list, value=hist_percent, label=bin_labels) # Print or plot the masked image _debug(visual=masked_array, filename=os.path.join( params.debug_outdir, str(params.device) + index_array.array_type + ".png")) # Store images outputs.images.append(analysis_image) return analysis_image
def analyze_object(img, obj, mask, label="default"): """Outputs numeric properties for an input object (contour or grouped contours). Inputs: img = RGB or grayscale image data for plotting obj = single or grouped contour object mask = Binary image to use as mask label = optional label parameter, modifies the variable name of observations recorded Returns: analysis_images = list of output images :param img: numpy.ndarray :param obj: list :param mask: numpy.ndarray :param label: str :return analysis_images: list """ # Valid objects can only be analyzed if they have >= 5 vertices if len(obj) < 5: return None ori_img = np.copy(img) # Convert grayscale images to color if len(np.shape(ori_img)) == 2: ori_img = cv2.cvtColor(ori_img, cv2.COLOR_GRAY2BGR) if len(np.shape(img)) == 3: ix, iy, iz = np.shape(img) else: ix, iy = np.shape(img) size = ix, iy, 3 size1 = ix, iy background = np.zeros(size, dtype=np.uint8) background1 = np.zeros(size1, dtype=np.uint8) background2 = np.zeros(size1, dtype=np.uint8) # Check is object is touching image boundaries (QC) in_bounds = within_frame(mask=mask, label=label) # Convex Hull hull = cv2.convexHull(obj) hull_vertices = len(hull) # Moments # m = cv2.moments(obj) m = cv2.moments(mask, binaryImage=True) # Properties # Area area = m['m00'] if area: # Convex Hull area hull_area = cv2.contourArea(hull) # Solidity solidity = 1 if int(hull_area) != 0: solidity = area / hull_area # Perimeter perimeter = cv2.arcLength(obj, closed=True) # x and y position (bottom left?) and extent x (width) and extent y (height) x, y, width, height = cv2.boundingRect(obj) # Centroid (center of mass x, center of mass y) cmx, cmy = (float(m['m10'] / m['m00']), float(m['m01'] / m['m00'])) # Ellipse center, axes, angle = cv2.fitEllipse(obj) major_axis = np.argmax(axes) minor_axis = 1 - major_axis major_axis_length = float(axes[major_axis]) minor_axis_length = float(axes[minor_axis]) eccentricity = float( np.sqrt(1 - (axes[minor_axis] / axes[major_axis])**2)) # Longest Axis: line through center of mass and point on the convex hull that is furthest away cv2.circle(background, (int(cmx), int(cmy)), 4, (255, 255, 255), -1) center_p = cv2.cvtColor(background, cv2.COLOR_BGR2GRAY) ret, centerp_binary = cv2.threshold(center_p, 0, 255, cv2.THRESH_BINARY) centerpoint, cpoint_h = cv2.findContours(centerp_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)[-2:] dist = [] vhull = np.vstack(hull) for i, c in enumerate(vhull): xy = tuple(c) pptest = cv2.pointPolygonTest(centerpoint[0], xy, measureDist=True) dist.append(pptest) abs_dist = np.absolute(dist) max_i = np.argmax(abs_dist) caliper_max_x, caliper_max_y = list(tuple(vhull[max_i])) caliper_mid_x, caliper_mid_y = [int(cmx), int(cmy)] xdiff = float(caliper_max_x - caliper_mid_x) ydiff = float(caliper_max_y - caliper_mid_y) # Set default values slope = 1 if xdiff != 0: slope = (float(ydiff / xdiff)) b_line = caliper_mid_y - (slope * caliper_mid_x) if slope != 0: xintercept = int(-b_line / slope) xintercept1 = int((ix - b_line) / slope) if 0 <= xintercept <= iy and 0 <= xintercept1 <= iy: cv2.line(background1, (xintercept1, ix), (xintercept, 0), (255), params.line_thickness) elif xintercept < 0 or xintercept > iy or xintercept1 < 0 or xintercept1 > iy: yintercept = int(b_line) yintercept1 = int((slope * iy) + b_line) cv2.line(background1, (0, yintercept), (iy, yintercept1), (255), 5) else: cv2.line(background1, (iy, caliper_mid_y), (0, caliper_mid_y), (255), params.line_thickness) ret1, line_binary = cv2.threshold(background1, 0, 255, cv2.THRESH_BINARY) cv2.drawContours(background2, [hull], -1, (255), -1) ret2, hullp_binary = cv2.threshold(background2, 0, 255, cv2.THRESH_BINARY) caliper = cv2.multiply(line_binary, hullp_binary) caliper_y, caliper_x = np.array(caliper.nonzero()) caliper_matrix = np.vstack((caliper_x, caliper_y)) caliper_transpose = np.transpose(caliper_matrix) caliper_length = len(caliper_transpose) caliper_transpose1 = np.lexsort((caliper_y, caliper_x)) caliper_transpose2 = [(caliper_x[i], caliper_y[i]) for i in caliper_transpose1] caliper_transpose = np.array(caliper_transpose2) analysis_images = [] # Draw properties if area: cv2.drawContours(ori_img, obj, -1, (255, 0, 0), params.line_thickness) cv2.drawContours(ori_img, [hull], -1, (255, 0, 255), params.line_thickness) cv2.line(ori_img, (x, y), (x + width, y), (255, 0, 255), params.line_thickness) cv2.line(ori_img, (int(cmx), y), (int(cmx), y + height), (255, 0, 255), params.line_thickness) cv2.line(ori_img, (tuple(caliper_transpose[caliper_length - 1])), (tuple(caliper_transpose[0])), (255, 0, 255), params.line_thickness) cv2.circle(ori_img, (int(cmx), int(cmy)), 10, (255, 0, 255), params.line_thickness) analysis_images.append(ori_img) analysis_images.append(mask) else: pass outputs.add_observation(sample=label, variable='area', trait='area', method='plantcv.plantcv.analyze_object', scale='pixels', datatype=int, value=area, label='pixels') outputs.add_observation(sample=label, variable='convex_hull_area', trait='convex hull area', method='plantcv.plantcv.analyze_object', scale='pixels', datatype=int, value=hull_area, label='pixels') outputs.add_observation(sample=label, variable='solidity', trait='solidity', method='plantcv.plantcv.analyze_object', scale='none', datatype=float, value=solidity, label='none') outputs.add_observation(sample=label, variable='perimeter', trait='perimeter', method='plantcv.plantcv.analyze_object', scale='pixels', datatype=int, value=perimeter, label='pixels') outputs.add_observation(sample=label, variable='width', trait='width', method='plantcv.plantcv.analyze_object', scale='pixels', datatype=int, value=width, label='pixels') outputs.add_observation(sample=label, variable='height', trait='height', method='plantcv.plantcv.analyze_object', scale='pixels', datatype=int, value=height, label='pixels') outputs.add_observation(sample=label, variable='longest_path', trait='longest path', method='plantcv.plantcv.analyze_object', scale='pixels', datatype=int, value=caliper_length, label='pixels') outputs.add_observation(sample=label, variable='center_of_mass', trait='center of mass', method='plantcv.plantcv.analyze_object', scale='none', datatype=tuple, value=(cmx, cmy), label='none') outputs.add_observation(sample=label, variable='convex_hull_vertices', trait='convex hull vertices', method='plantcv.plantcv.analyze_object', scale='none', datatype=int, value=hull_vertices, label='none') outputs.add_observation(sample=label, variable='object_in_frame', trait='object in frame', method='plantcv.plantcv.analyze_object', scale='none', datatype=bool, value=in_bounds, label='none') outputs.add_observation(sample=label, variable='ellipse_center', trait='ellipse center', method='plantcv.plantcv.analyze_object', scale='none', datatype=tuple, value=(center[0], center[1]), label='none') outputs.add_observation(sample=label, variable='ellipse_major_axis', trait='ellipse major axis length', method='plantcv.plantcv.analyze_object', scale='pixels', datatype=int, value=major_axis_length, label='pixels') outputs.add_observation(sample=label, variable='ellipse_minor_axis', trait='ellipse minor axis length', method='plantcv.plantcv.analyze_object', scale='pixels', datatype=int, value=minor_axis_length, label='pixels') outputs.add_observation(sample=label, variable='ellipse_angle', trait='ellipse major axis angle', method='plantcv.plantcv.analyze_object', scale='degrees', datatype=float, value=float(angle), label='degrees') outputs.add_observation(sample=label, variable='ellipse_eccentricity', trait='ellipse eccentricity', method='plantcv.plantcv.analyze_object', scale='none', datatype=float, value=float(eccentricity), label='none') # Debugging output params.device += 1 cv2.drawContours(ori_img, obj, -1, (255, 0, 0), params.line_thickness) cv2.drawContours(ori_img, [hull], -1, (255, 0, 255), params.line_thickness) cv2.line(ori_img, (x, y), (x + width, y), (255, 0, 255), params.line_thickness) cv2.line(ori_img, (int(cmx), y), (int(cmx), y + height), (255, 0, 255), params.line_thickness) cv2.circle(ori_img, (int(cmx), int(cmy)), 10, (255, 0, 255), params.line_thickness) cv2.line(ori_img, (tuple(caliper_transpose[caliper_length - 1])), (tuple(caliper_transpose[0])), (255, 0, 255), params.line_thickness) _debug(visual=ori_img, filename=os.path.join(params.debug_outdir, str(params.device) + '_shapes.png')) # Store images outputs.images.append(analysis_images) return ori_img
def cluster_contour_splitimg(img, grouped_contour_indexes, contours, hierarchy, outdir=None, file=None, filenames=None): """ This function takes clustered contours and splits them into multiple images, also does a check to make sure that the number of inputted filenames matches the number of clustered contours. Inputs: img = image data grouped_contour_indexes = output of cluster_contours, indexes of clusters of contours contours = contours to cluster, output of cluster_contours hierarchy = hierarchy of contours, output of find_objects outdir = out directory for output images file = the name of the input image to use as a plantcv name, output of filename from read_image function filenames = input txt file with list of filenames in order from top to bottom left to right (likely list of genotypes) Returns: output_path = array of paths to output images :param img: numpy.ndarray :param grouped_contour_indexes: list :param contours: list :param hierarchy: numpy.ndarray :param outdir: str :param file: str :param filenames: str :return output_path: str """ params.device += 1 sys.stderr.write( 'This function has been updated to include object hierarchy so object holes can be included\n') i = datetime.now() timenow = i.strftime('%m-%d-%Y_%H:%M:%S') if file is None: filebase = timenow else: filebase = os.path.splitext(file)[0] if filenames is None: namelist = [] for x in range(0, len(grouped_contour_indexes)): namelist.append(x) else: with open(filenames, 'r') as n: namelist = n.read().splitlines() n.close() # make sure the number of objects matches the namelist, and if not, remove the smallest grouped countor # removing contours is not ideal but the lists don't match there is a warning to check output if len(namelist) == len(grouped_contour_indexes): corrected_contour_indexes = grouped_contour_indexes elif len(namelist) < len(grouped_contour_indexes): print("Warning number of names is less than number of grouped contours, attempting to fix, to double check " "output") diff = len(grouped_contour_indexes) - len(namelist) size = [] for i, x in enumerate(grouped_contour_indexes): totallen = [] for a in x: g = i la = len(contours[a]) totallen.append(la) sumlen = np.sum(totallen) size.append((sumlen, g, i)) dtype = [('len', int), ('group', list), ('index', int)] lencontour = np.array(size, dtype=dtype) lencontour = np.sort(lencontour, order='len') rm_contour = lencontour[diff:] rm_contour = np.sort(rm_contour, order='group') corrected_contour_indexes = [] for x in rm_contour: index = x[2] corrected_contour_indexes.append(grouped_contour_indexes[index]) elif len(namelist) > len(grouped_contour_indexes): print("Warning number of names is more than number of grouped contours, double check output") diff = len(namelist) - len(grouped_contour_indexes) namelist = namelist[0:-diff] corrected_contour_indexes = grouped_contour_indexes # create filenames group_names = [] group_names1 = [] for i, x in enumerate(namelist): plantname = str(filebase) + '_' + str(x) + '_p' + str(i) + '.png' maskname = str(filebase) + '_' + str(x) + '_p' + str(i) + '_mask.png' group_names.append(plantname) group_names1.append(maskname) # split image output_path = [] output_imgs = [] output_masks = [] for y, x in enumerate(corrected_contour_indexes): if outdir is not None: savename = os.path.join(str(outdir), group_names[y]) savename1 = os.path.join(str(outdir), group_names1[y]) else: savename = os.path.join(".", group_names[y]) savename1 = os.path.join(".", group_names1[y]) iy, ix = np.shape(img)[:2] mask = np.zeros((iy, ix, 3), dtype=np.uint8) masked_img = np.copy(img) for a in x: if hierarchy[0][a][3] > -1: cv2.drawContours(mask, contours, a, (0, 0, 0), -1, lineType=8, hierarchy=hierarchy) else: cv2.drawContours(mask, contours, a, (255, 255, 255), -1, lineType=8, hierarchy=hierarchy) mask_binary = mask[:, :, 0] if np.sum(mask_binary) == 0: pass else: retval, mask_binary = cv2.threshold(mask_binary, 254, 255, cv2.THRESH_BINARY) masked1 = apply_mask(masked_img, mask_binary, 'white') output_imgs.append(masked1) output_masks.append(mask_binary) if outdir is not None: print_image(masked1, savename) print_image(mask_binary, savename1) output_path.append(savename) _debug(visual=masked1, filename=os.path.join(params.debug_outdir, str(params.device) + '_clusters.png')) _debug(visual=mask_binary, filename=os.path.join(params.debug_outdir, str(params.device) + '_clusters_mask.png')) return output_path, output_imgs, output_masks
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 histogram(img, mask=None, bins=100, lower_bound=None, upper_bound=None, title=None, hist_data=False): """Plot histograms of each input image channel Inputs: img = an RGB or grayscale image to analyze mask = binary mask, calculate histogram from masked area only (default=None) bins = divide the data into n evenly spaced bins (default=100) lower_bound = the lower bound of the bins (x-axis min value) (default=None) upper_bound = the upper bound of the bins (x-axis max value) (default=None) title = a custom title for the plot (default=None) hist_data = return the frequency distribution data if True (default=False) Returns: fig_hist = histogram figure hist_df = dataframe with histogram data, with columns "pixel intensity" and "proportion of pixels (%)" :param img: numpy.ndarray :param mask: numpy.ndarray :param bins: int :param lower_bound: int :param upper_bound: int :param title: str :param hist_data: bool :return fig_hist: plotnine.ggplot.ggplot :return hist_df: pandas.core.frame.DataFrame """ if not isinstance(img, np.ndarray): fatal_error("Only image of type numpy.ndarray is supported input!") if len(img.shape) < 2: fatal_error("Input image should be at least a 2d array!") if mask is not None: masked = img[np.where(mask > 0)] img_min, img_max = np.nanmin(masked), np.nanmax(masked) else: img_min, img_max = np.nanmin(img), np.nanmax(img) # for lower / upper bound, if given, use the given value, otherwise, use the min / max of the image lower_bound = lower_bound if lower_bound is not None else img_min upper_bound = upper_bound if upper_bound is not None else img_max if len(img.shape) > 2: if img.shape[2] == 3: b_names = ['blue', 'green', 'red'] else: b_names = [str(i) for i in range(img.shape[2])] if len(img.shape) == 2: bin_labels, hist_percent, hist_ = _hist_gray(img, bins=bins, lower_bound=lower_bound, upper_bound=upper_bound, mask=mask) hist_df = pd.DataFrame({ 'pixel intensity': bin_labels, 'proportion of pixels (%)': hist_percent, 'hist_count': hist_, 'color channel': ['0' for _ in range(len(hist_percent))] }) else: # Assumption: RGB image # Initialize dataframe column arrays px_int = np.array([]) prop = np.array([]) hist_count = np.array([]) channel = [] for (b, b_name) in enumerate(b_names): bin_labels, hist_percent, hist_ = _hist_gray( img[:, :, b], bins=bins, lower_bound=lower_bound, upper_bound=upper_bound, mask=mask) # Append histogram data for each channel px_int = np.append(px_int, bin_labels) prop = np.append(prop, hist_percent) hist_count = np.append(hist_count, hist_) channel = channel + [b_name for _ in range(len(hist_percent))] # Create dataframe hist_df = pd.DataFrame({ 'pixel intensity': px_int, 'proportion of pixels (%)': prop, 'hist_count': hist_count, 'color channel': channel }) fig_hist = (ggplot(data=hist_df, mapping=aes(x='pixel intensity', y='proportion of pixels (%)', color='color channel')) + geom_line()) if title is not None: fig_hist = fig_hist + labels.ggtitle(title) if len(img.shape) > 2 and img.shape[2] == 3: fig_hist = fig_hist + scale_color_manual(['blue', 'green', 'red']) # Plot or print the histogram _debug(visual=fig_hist, filename=os.path.join(params.debug_outdir, str(params.device) + '_hist.png')) if hist_data is True: return fig_hist, hist_df return fig_hist
def analyze_nir_intensity(gray_img, mask, bins=256, histplot=None, label="default"): """This function calculates the intensity of each pixel associated with the plant and writes the values out to a file. It can also print out a histogram plot of pixel intensity and a pseudocolor image of the plant. Inputs: gray_img = 8- or 16-bit grayscale image data mask = Binary mask made from selected contours bins = number of classes to divide spectrum into histplot = if True plots histogram of intensity values label = optional label parameter, modifies the variable name of observations recorded Returns: analysis_images = NIR histogram image :param gray_img: numpy array :param mask: numpy array :param bins: int :param histplot: bool :param label: str :return analysis_images: plotnine ggplot """ # Save user debug setting debug = params.debug if histplot is not None: deprecation_warning("'histplot' will be deprecated in a future version of PlantCV. " "This function creates a histogram by default.") # calculate histogram if gray_img.dtype == 'uint16': maxval = 65536 else: maxval = 256 masked_array = gray_img[np.where(mask > 0)] masked_nir_mean = np.average(masked_array) masked_nir_median = np.median(masked_array) masked_nir_std = np.std(masked_array) # Make a pseudo-RGB image rgbimg = cv2.cvtColor(gray_img, cv2.COLOR_GRAY2BGR) # Calculate histogram params.debug = None fig_hist, hist_data = histogram(gray_img, mask=mask, bins=bins, lower_bound=0, upper_bound=maxval, title=None, hist_data=True) bin_labels, hist_nir = hist_data["pixel intensity"].tolist(), hist_data['hist_count'].tolist() masked1 = cv2.bitwise_and(rgbimg, rgbimg, mask=mask) # Restore user debug setting params.debug = debug # Print or plot masked image _debug(visual=masked1, filename=os.path.join(params.debug_outdir, str(params.device) + "_masked_nir_plant.png")) fig_hist = fig_hist + labs(x="Grayscale pixel intensity (0-{})".format(maxval), y="Proportion of pixels (%)") # Print or plot histogram _debug(visual=fig_hist, filename=os.path.join(params.debug_outdir, str(params.device) + "_nir_hist.png")) analysis_image = fig_hist outputs.add_observation(sample=label, variable='nir_frequencies', trait='near-infrared frequencies', method='plantcv.plantcv.analyze_nir_intensity', scale='frequency', datatype=list, value=hist_nir, label=bin_labels) outputs.add_observation(sample=label, variable='nir_mean', trait='near-infrared mean', method='plantcv.plantcv.analyze_nir_intensity', scale='none', datatype=float, value=masked_nir_mean, label='none') outputs.add_observation(sample=label, variable='nir_median', trait='near-infrared median', method='plantcv.plantcv.analyze_nir_intensity', scale='none', datatype=float, value=masked_nir_median, label='none') outputs.add_observation(sample=label, variable='nir_stdev', trait='near-infrared standard deviation', method='plantcv.plantcv.analyze_nir_intensity', scale='none', datatype=float, value=masked_nir_std, label='none') # Store images outputs.images.append(analysis_image) return analysis_image
def custom_range(img, lower_thresh, upper_thresh, channel='gray'): """Creates a thresholded image and mask from an RGB image and threshold values. Inputs: img = RGB or grayscale image data lower_thresh = List of lower threshold values (0-255) upper_thresh = List of upper threshold values (0-255) channel = Color-space channels of interest (RGB, HSV, LAB, or gray) Returns: mask = Mask, binary image masked_img = Masked image, keeping the part of image of interest :param img: numpy.ndarray :param lower_thresh: list :param upper_thresh: list :param channel: str :return mask: numpy.ndarray :return masked_img: numpy.ndarray """ if channel.upper() == 'HSV': # Check threshold inputs if not (len(lower_thresh) == 3 and len(upper_thresh) == 3): fatal_error( "If using the HSV colorspace, 3 thresholds are needed for both lower_thresh and " + "upper_thresh. If thresholding isn't needed for a channel, set lower_thresh=0 and " + "upper_thresh=255") # Convert the RGB image to HSV colorspace hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # Separate channels hue = hsv_img[:, :, 0] sat = hsv_img[:, :, 1] value = hsv_img[:, :, 2] # Make a mask for each channel h_mask = cv2.inRange(hue, lower_thresh[0], upper_thresh[0]) s_mask = cv2.inRange(sat, lower_thresh[1], upper_thresh[1]) v_mask = cv2.inRange(value, lower_thresh[2], upper_thresh[2]) # Apply the masks to the image result = cv2.bitwise_and(img, img, mask=h_mask) result = cv2.bitwise_and(result, result, mask=s_mask) masked_img = cv2.bitwise_and(result, result, mask=v_mask) # Combine masks mask = cv2.bitwise_and(s_mask, h_mask) mask = cv2.bitwise_and(mask, v_mask) elif channel.upper() == 'RGB': # Check threshold inputs if not (len(lower_thresh) == 3 and len(upper_thresh) == 3): fatal_error( "If using the RGB colorspace, 3 thresholds are needed for both lower_thresh and " + "upper_thresh. If thresholding isn't needed for a channel, set lower_thresh=0 and " + "upper_thresh=255") # Separate channels (pcv.readimage reads RGB images in as BGR) blue = img[:, :, 0] green = img[:, :, 1] red = img[:, :, 2] # Make a mask for each channel b_mask = cv2.inRange(blue, lower_thresh[2], upper_thresh[2]) g_mask = cv2.inRange(green, lower_thresh[1], upper_thresh[1]) r_mask = cv2.inRange(red, lower_thresh[0], upper_thresh[0]) # Apply the masks to the image result = cv2.bitwise_and(img, img, mask=b_mask) result = cv2.bitwise_and(result, result, mask=g_mask) masked_img = cv2.bitwise_and(result, result, mask=r_mask) # Combine masks mask = cv2.bitwise_and(b_mask, g_mask) mask = cv2.bitwise_and(mask, r_mask) elif channel.upper() == 'LAB': # Check threshold inputs if not (len(lower_thresh) == 3 and len(upper_thresh) == 3): fatal_error( "If using the LAB colorspace, 3 thresholds are needed for both lower_thresh and " + "upper_thresh. If thresholding isn't needed for a channel, set lower_thresh=0 and " + "upper_thresh=255") # Convert the RGB image to LAB colorspace lab_img = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) # Separate channels (pcv.readimage reads RGB images in as BGR) lightness = lab_img[:, :, 0] green_magenta = lab_img[:, :, 1] blue_yellow = lab_img[:, :, 2] # Make a mask for each channel l_mask = cv2.inRange(lightness, lower_thresh[0], upper_thresh[0]) gm_mask = cv2.inRange(green_magenta, lower_thresh[1], upper_thresh[1]) by_mask = cv2.inRange(blue_yellow, lower_thresh[2], upper_thresh[2]) # Apply the masks to the image result = cv2.bitwise_and(img, img, mask=l_mask) result = cv2.bitwise_and(result, result, mask=gm_mask) masked_img = cv2.bitwise_and(result, result, mask=by_mask) # Combine masks mask = cv2.bitwise_and(l_mask, gm_mask) mask = cv2.bitwise_and(mask, by_mask) elif channel.upper() == 'GRAY' or channel.upper() == 'GREY': # Check threshold input if not (len(lower_thresh) == 1 and len(upper_thresh) == 1): fatal_error( "If useing a grayscale colorspace, 1 threshold is needed for both the " + "lower_thresh and upper_thresh.") if len(np.shape(img)) == 3: # Convert RGB image to grayscale colorspace gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) else: gray_img = img # Make a mask mask = cv2.inRange(gray_img, lower_thresh[0], upper_thresh[0]) # Apply the masks to the image masked_img = cv2.bitwise_and(img, img, mask=mask) else: fatal_error( str(channel) + " is not a valid colorspace. Channel must be either 'RGB', 'HSV', or 'gray'." ) # Auto-increment the device counter # Print or plot the binary image if debug is on _debug(visual=masked_img, filename=os.path.join( params.debug_outdir, str(params.device) + channel + 'custom_thresh.png')) _debug(visual=mask, filename=os.path.join( params.debug_outdir, str(params.device) + channel + 'custom_thresh_mask.png')) return mask, masked_img
def texture(gray_img, ksize, threshold, offset=3, texture_method='dissimilarity', borders='nearest', max_value=255): """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 threshold = Threshold value (0-255) offset = Distance offsets texture_method = Feature of a grey level co-occurrence matrix, either 'contrast', 'dissimilarity', 'homogeneity', 'ASM', 'energy', or 'correlation'.For equations of different features see scikit-image. borders = How the array borders are handled, either 'reflect', 'constant', 'nearest', 'mirror', or 'wrap' max_value = Value to apply above threshold (usually 255 = white) Returns: bin_img = Thresholded, binary image :param gray_img: numpy.ndarray :param ksize: int :param threshold: int :param offset: int :param texture_method: str :param borders: str :param max_value: int :return bin_img: numpy.ndarray """ # Function that calculates the texture of a kernel def calc_texture(inputs): inputs = np.reshape(a=inputs, newshape=[ksize, ksize]) inputs = inputs.astype(np.uint8) # Greycomatrix takes image, distance offset, angles (in radians), symmetric, and normed # http://scikit-image.org/docs/dev/api/skimage.feature.html#skimage.feature.greycomatrix glcm = greycomatrix(inputs, [offset], [0], 256, symmetric=True, normed=True) diss = greycoprops(glcm, texture_method)[0, 0] return diss # Make an array the same size as the original image output = np.zeros(gray_img.shape, dtype=gray_img.dtype) # Apply the texture function over the whole image generic_filter(gray_img, calc_texture, size=ksize, output=output, mode=borders) # Threshold so higher texture measurements stand out bin_img = binary(gray_img=output, threshold=threshold, max_value=max_value, object_type='light') _debug(visual=bin_img, filename=os.path.join(params.debug_outdir, str(params.device) + "_texture_mask.png")) return bin_img