예제 #1
0
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
예제 #2
0
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
예제 #3
0
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]
예제 #4
0
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
예제 #5
0
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
예제 #6
0
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
예제 #7
0
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
예제 #8
0
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
예제 #9
0
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
예제 #10
0
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
예제 #11
0
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
예제 #12
0
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
예제 #13
0
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
예제 #14
0
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
예제 #15
0
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
예제 #16
0
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
예제 #17
0
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
예제 #18
0
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
예제 #19
0
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