Example #1
0
def watershed_segmentation(rgb_img, mask, distance=10):
    """Uses the watershed algorithm to detect boundary of objects. Needs a marker file which specifies area which is
       object (white), background (grey), unknown area (black).

    Inputs:
    rgb_img             = image to perform watershed on needs to be 3D (i.e. np.shape = x,y,z not np.shape = x,y)
    mask                = binary image, single channel, object in white and background black
    distance            = min_distance of local maximum

    Returns:
    analysis_images     = list of output images

    :param rgb_img: numpy.ndarray
    :param mask: numpy.ndarray
    :param distance: int
    :return analysis_images: list
    """
    params.device += 1

    # Store debug mode
    debug = params.debug
    params.debug = None

    dist_transform = cv2.distanceTransformWithLabels(mask, cv2.DIST_L2, maskSize=0)[0]

    localMax = peak_local_max(dist_transform, indices=False, min_distance=distance, labels=mask)

    markers = ndi.label(localMax, structure=np.ones((3, 3)))[0]
    dist_transform1 = -dist_transform
    labels = watershed(dist_transform1, markers, mask=mask)

    img1 = np.copy(rgb_img)

    for x in np.unique(labels):
        rand_color = color_palette(len(np.unique(labels)))
        img1[labels == x] = rand_color[x]

    img2 = apply_mask(img1, mask, 'black')

    joined = np.concatenate((img2, rgb_img), axis=1)

    estimated_object_count = len(np.unique(markers)) - 1

    # Reset debug mode
    params.debug = debug
    if params.debug == 'print':
        print_image(dist_transform, os.path.join(params.debug_outdir, str(params.device) + '_watershed_dist_img.png'))
        print_image(joined, os.path.join(params.debug_outdir, str(params.device) + '_watershed_img.png'))
    elif params.debug == 'plot':
        plot_image(dist_transform, cmap='gray')
        plot_image(joined)

    outputs.add_observation(variable='estimated_object_count', trait='estimated object count',
                            method='plantcv.plantcv.watershed', scale='none', datatype=int,
                            value=estimated_object_count, label='none')

    # Store images
    outputs.images.append([dist_transform, joined])

    return joined
Example #2
0
def watershed_segmentation(rgb_img, mask, distance=10):
    """Uses the watershed algorithm to detect boundary of objects. Needs a marker file which specifies area which is
       object (white), background (grey), unknown area (black).

    Inputs:
    rgb_img             = image to perform watershed on needs to be 3D (i.e. np.shape = x,y,z not np.shape = x,y)
    mask                = binary image, single channel, object in white and background black
    distance            = min_distance of local maximum

    Returns:
    analysis_images     = list of output images

    :param rgb_img: numpy.ndarray
    :param mask: numpy.ndarray
    :param distance: int
    :return analysis_images: list
    """
    params.device += 1
    # # Will be depricating opencv version 2
    # if cv2.__version__[0] == '2':
    #     dist_transform = cv2.distanceTransform(mask, cv2.cv.CV_DIST_L2, maskSize=0)
    # else:
    dist_transform = cv2.distanceTransformWithLabels(mask, cv2.DIST_L2, maskSize=0)[0]

    localMax = peak_local_max(dist_transform, indices=False, min_distance=distance, labels=mask)

    markers = ndi.label(localMax, structure=np.ones((3, 3)))[0]
    dist_transform1 = -dist_transform
    labels = watershed(dist_transform1, markers, mask=mask)

    img1 = np.copy(rgb_img)

    for x in np.unique(labels):
        rand_color = color_palette(len(np.unique(labels)))
        img1[labels == x] = rand_color[x]

    img2 = apply_mask(img1, mask, 'black')

    joined = np.concatenate((img2, rgb_img), axis=1)

    estimated_object_count = len(np.unique(markers)) - 1

    analysis_image = []
    analysis_image.append(joined)

    if params.debug == 'print':
        print_image(dist_transform, os.path.join(params.debug_outdir, str(params.device) + '_watershed_dist_img.png'))
        print_image(joined, os.path.join(params.debug_outdir, str(params.device) + '_watershed_img.png'))
    elif params.debug == 'plot':
        plot_image(dist_transform, cmap='gray')
        plot_image(joined)

    outputs.add_observation(variable='estimated_object_count', trait='estimated object count',
                            method='plantcv.plantcv.watershed', scale='none', datatype=int,
                            value=estimated_object_count, label='none')

    # Store images
    outputs.images.append(analysis_image)

    return analysis_image
Example #3
0
def segment_id(skel_img, objects, hierarchies, mask=None):
    """ Plot segment ID's

            Inputs:
            skel_img      = Skeletonized image
            objects       = List of contours
            hierarchy     = Contour hierarchy NumPy array
            mask          = (Optional) binary mask for debugging. If provided, debug image will be overlaid on the mask.

            Returns:
            segmented_img = Segmented image
            labeled_img   = Labeled image

            :param skel_img: numpy.ndarray
            :param objects: list
            :param hierarchies: numpy.ndarray
            :param mask: numpy.ndarray
            :return segmented_img: numpy.ndarray
            :return labeled_img: numpy.ndarray
            """
    label_coord_x = []
    label_coord_y = []

    if mask is None:
        segmented_img = skel_img.copy()
    else:
        segmented_img = mask.copy()

    segmented_img = cv2.cvtColor(segmented_img, cv2.COLOR_GRAY2RGB)

    # Color each segment a different color
    rand_color = color_palette(len(objects))

    # Plot all segment contours
    for i, cnt in enumerate(objects):
        cv2.drawContours(segmented_img, objects, i, rand_color[i], params.line_thickness, lineType=8,
                         hierarchy=hierarchies)
        # Store coordinates for labels
        label_coord_x.append(objects[i][0][0][0])
        label_coord_y.append(objects[i][0][0][1])

    labeled_img = segmented_img.copy()

    for i, cnt in enumerate(objects):
        # Label slope lines
        w = label_coord_x[i]
        h = label_coord_y[i]
        text = "ID:{}".format(i)
        cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=.65, color=rand_color[i], thickness=2)
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segmented_ids.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return segmented_img, labeled_img
Example #4
0
def segment_id(skel_img, objects, mask=None):
    """ Plot segment ID's

            Inputs:
            skel_img      = Skeletonized image
            objects       = List of contours
            mask          = (Optional) binary mask for debugging. If provided, debug image will be overlaid on the mask.

            Returns:
            segmented_img = Segmented image
            labeled_img   = Labeled image

            :param skel_img: numpy.ndarray
            :param objects: list
            :param mask: numpy.ndarray
            :return segmented_img: numpy.ndarray
            :return labeled_img: numpy.ndarray
            """
    label_coord_x = []
    label_coord_y = []

    if mask is None:
        segmented_img = skel_img.copy()
    else:
        segmented_img = mask.copy()

    segmented_img = cv2.cvtColor(segmented_img, cv2.COLOR_GRAY2RGB)

    # Color each segment a different color
    rand_color = color_palette(len(objects))

    # Plot all segment contours
    for i, cnt in enumerate(objects):
        cv2.drawContours(segmented_img, objects, i, rand_color[i], params.line_thickness, lineType=8)
        # Store coordinates for labels
        label_coord_x.append(objects[i][0][0][0])
        label_coord_y.append(objects[i][0][0][1])

    labeled_img = segmented_img.copy()

    for i, cnt in enumerate(objects):
        # Label slope lines
        w = label_coord_x[i]
        h = label_coord_y[i]
        text = "ID:{}".format(i)
        cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=params.text_size, color=rand_color[i], thickness=params.text_thickness)
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segmented_ids.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return segmented_img, labeled_img
def segment_skeleton(skel_img, mask=None):
    """ Segment a skeleton image into pieces

        Inputs:
        skel_img      = Skeletonized image
        mask          = (Optional) binary mask for debugging. If provided, debug image will be overlaid on the mask.

        Returns:
        segmented_img = Segmented debugging image
        objects       = list of contours
        hierarchy     = contour hierarchy list

        :param skel_img: numpy.ndarray
        :param mask: numpy.ndarray
        :return segmented_img: numpy.ndarray
        :return segment_objects: list
        "return segment_hierarchies: numpy.ndarray
        """

    # Store debug
    debug = params.debug
    params.debug = None

    # Find branch points
    bp = find_branch_pts(skel_img)
    bp = dilate(bp, 3, 1)

    # Subtract from the skeleton so that leaves are no longer connected
    segments = image_subtract(skel_img, bp)

    # Gather contours of leaves
    segment_objects, _ = find_objects(segments, segments)

    # Color each segment a different color
    rand_color = color_palette(len(segment_objects))

    if mask is None:
        segmented_img = skel_img.copy()
    else:
        segmented_img = mask.copy()

    segmented_img = cv2.cvtColor(segmented_img, cv2.COLOR_GRAY2RGB)
    for i, cnt in enumerate(segment_objects):
        cv2.drawContours(segmented_img, segment_objects, i, rand_color[i], params.line_thickness, lineType=8)

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(segmented_img, os.path.join(params.debug_outdir, str(params.device) + '_segmented.png'))
    elif params.debug == 'plot':
        plot_image(segmented_img)

    return segmented_img, segment_objects
def fill_segments(mask, objects):
    """Fills masked segments from contours.
    Inputs:
    mask         = Binary image, single channel, object = 1 and background = 0
    objects      = List of contours
    Returns:
    filled_img   = Filled mask
    :param mask: numpy.ndarray
    :param object: list
    :return filled_img: numpy.ndarray
    """

    params.device += 1

    h, w = mask.shape
    markers = np.zeros((h, w))

    labels = np.arange(len(objects)) + 1
    for i, l in enumerate(labels):
        cv2.drawContours(markers, objects, i, int(l), 5)

    # Fill as a watershed segmentation from contours as markers
    filled_mask = watershed(mask == 0,
                            markers=markers,
                            mask=mask != 0,
                            compactness=0)

    # Count area in pixels of each segment
    ids, counts = np.unique(filled_mask, return_counts=True)
    outputs.add_observation(variable='segment_area',
                            trait='segment area',
                            method='plantcv.plantcv.morphology.fill_segments',
                            scale='pixels',
                            datatype=list,
                            value=counts[1:].tolist(),
                            label=(ids[1:] - 1).tolist())

    rgb_vals = color_palette(num=len(labels))
    filled_img = np.zeros((h, w, 3), dtype=np.uint8)
    for l in labels:
        for ch in range(3):
            filled_img[:, :, ch][filled_mask == l] = rgb_vals[l - 1][ch]

    if params.debug == 'print':
        print_image(
            filled_img,
            os.path.join(params.debug_outdir,
                         str(params.device) + '_filled_img.png'))
    elif params.debug == 'plot':
        plot_image(filled_img)

    return filled_img
def main():
    # Get options
    args = options()

    debug = args.debug

    # Read image
    img, path, filename = pcv.readimage(args.image)

    # Pipeline step
    device = 0

    device, img1 = pcv.white_balance(device, img, debug,
                                     (100, 100, 1000, 1000))
    img = img1

    #seedmask, path1, filename1 = pcv.readimage(args.mask)
    #device, seedmask = pcv.rgb2gray(seedmask, device, debug)
    #device, inverted = pcv.invert(seedmask, device, debug)
    #device, masked_img = pcv.apply_mask(img, inverted, 'white', device, debug)

    device, img_gray_sat = pcv.rgb2gray_hsv(img1, 's', device, debug)

    device, img_binary = pcv.binary_threshold(img_gray_sat, 70, 255, 'light',
                                              device, debug)

    img_binary1 = np.copy(img_binary)
    device, fill_image = pcv.fill(img_binary1, img_binary, 300, device, debug)

    device, seed_objects, seed_hierarchy = pcv.find_objects(
        img, fill_image, device, debug)

    device, roi1, roi_hierarchy1 = pcv.define_roi(img, 'rectangle', device,
                                                  None, 'default', debug, True,
                                                  1500, 1000, -1000, -500)

    device, roi_objects, roi_obj_hierarchy, kept_mask, obj_area = pcv.roi_objects(
        img, 'partial', roi1, roi_hierarchy1, seed_objects, seed_hierarchy,
        device, debug)

    img_copy = np.copy(img)
    for i in range(0, len(roi_objects)):
        rand_color = pcv.color_palette(1)
        cv2.drawContours(img_copy,
                         roi_objects,
                         i,
                         rand_color[0],
                         -1,
                         lineType=8,
                         hierarchy=roi_obj_hierarchy)

    pcv.print_image(
        img_copy,
        os.path.join(args.outdir, filename[:-4]) + "-seed-confetti.jpg")

    shape_header = []  # Store the table header
    table = []  # Store the PlantCV measurements for each seed in a table
    for i in range(0, len(roi_objects)):
        if roi_obj_hierarchy[0][i][
                3] == -1:  # Only continue if the object is an outermost contour

            # Object combine kept objects
            # Inputs:
            #    contours = object list
            #    device   = device number. Used to count steps in the pipeline
            #    debug    = None, print, or plot. Print = save to file, Plot = print to screen.
            device, obj, mask = pcv.object_composition(
                img, [roi_objects[i]], np.array([[roi_obj_hierarchy[0][i]]]),
                device, None)
            if obj is not None:
                # Measure the area and other shape properties of each seed
                # Inputs:
                #    img             = image object (most likely the original), color(RGB)
                #    imgname         = name of image
                #    obj             = single or grouped contour object
                #    device          = device number. Used to count steps in the pipeline
                #    debug           = None, print, or plot. Print = save to file, Plot = print to screen.
                #    filename        = False or image name. If defined print image
                device, shape_header, shape_data, shape_img = pcv.analyze_object(
                    img, "img", obj, mask, device, None)

                if shape_data is not None:
                    table.append(shape_data[1])

    data_array = np.array(table)
    maxval = np.argmax(data_array)
    maxseed = np.copy(img)
    cv2.drawContours(maxseed, roi_objects, maxval, (0, 255, 0), 10)

    imgtext = "This image has " + str(len(data_array)) + " seeds"
    sizeseed = "The largest seed is in green and is " + str(
        data_array[maxval]) + " pixels"
    cv2.putText(maxseed, imgtext, (500, 300), cv2.FONT_HERSHEY_SIMPLEX, 5,
                (0, 0, 0), 10)
    cv2.putText(maxseed, sizeseed, (500, 600), cv2.FONT_HERSHEY_SIMPLEX, 5,
                (0, 0, 0), 10)
    pcv.print_image(maxseed,
                    os.path.join(args.outdir, filename[:-4]) + "-maxseed.jpg")
def segment_euclidean_length(segmented_img, objects):
    """ Use segmented skeleton image to gather euclidean length measurements per segment

        Inputs:
        segmented_img = Segmented image to plot lengths on
        objects       = List of contours

        Returns:
        labeled_img      = Segmented debugging image with lengths labeled

        :param segmented_img: numpy.ndarray
        :param objects: list
        :return labeled_img: numpy.ndarray

        """
    # Store debug
    debug = params.debug
    params.debug = None

    x_list = []
    y_list = []
    segment_lengths = []
    rand_color = color_palette(len(objects))


    labeled_img = segmented_img.copy()

    for i, cnt in enumerate(objects):
        # Store coordinates for labels
        x_list.append(objects[i][0][0][0])
        y_list.append(objects[i][0][0][1])

        # Draw segments one by one to group segment tips together
        finding_tips_img = np.zeros(segmented_img.shape[:2], np.uint8)
        cv2.drawContours(finding_tips_img, objects, i, (255, 255, 255), 1, lineType=8)
        segment_tips = find_tips(finding_tips_img)
        tip_objects, tip_hierarchies = find_objects(segment_tips, segment_tips)
        points = []
        if not len(tip_objects) == 2:
            fatal_error("Too many tips found per segment, try pruning again")
        for t in tip_objects:
            # Gather pairs of coordinates
            x, y = t.ravel()
            coord = (x, y)
            points.append(coord)

        # Draw euclidean distance lines
        cv2.line(labeled_img, points[0], points[1], rand_color[i], 1)

        # Calculate euclidean distance between tips of each contour
        segment_lengths.append(euclidean(points[0], points[1]))

    segment_ids = []
    # Put labels of length
    for c, value in enumerate(segment_lengths):
        text = "{:.2f}".format(value)
        w = x_list[c]
        h = y_list[c]
        cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=params.text_size, color=(150, 150, 150), thickness=params.text_thickness)
        segment_label = "ID" + str(c)
        segment_ids.append(c)

    outputs.add_observation(variable='segment_eu_length', trait='segment euclidean length',
                            method='plantcv.plantcv.morphology.segment_euclidean_length', scale='pixels', datatype=list,
                            value=segment_lengths, label=segment_ids)

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segment_eu_lengths.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return labeled_img
Example #9
0
def watershed_segmentation(rgb_img, mask, distance=10):
    """Uses the watershed algorithm to detect boundary of objects. Needs a marker file which specifies area which is
       object (white), background (grey), unknown area (black).

    Inputs:
    rgb_img             = image to perform watershed on needs to be 3D (i.e. np.shape = x,y,z not np.shape = x,y)
    mask                = binary image, single channel, object in white and background black
    distance            = min_distance of local maximum

    Returns:
    watershed_header    = shape data table headers
    watershed_data      = shape data table values
    analysis_images     = list of output images

    :param rgb_img: numpy.ndarray
    :param mask: numpy.ndarray
    :param distance: int
    :return watershed_header: list
    :return watershed_data: list
    :return analysis_images: list
    """
    params.device += 1
    # # Will be depricating opencv version 2
    # if cv2.__version__[0] == '2':
    #     dist_transform = cv2.distanceTransform(mask, cv2.cv.CV_DIST_L2, maskSize=0)
    # else:
    dist_transform = cv2.distanceTransformWithLabels(mask, cv2.DIST_L2, maskSize=0)[0]

    localMax = peak_local_max(dist_transform, indices=False, min_distance=distance, labels=mask)

    markers = ndi.label(localMax, structure=np.ones((3, 3)))[0]
    dist_transform1 = -dist_transform
    labels = watershed(dist_transform1, markers, mask=mask)

    img1 = np.copy(rgb_img)

    for x in np.unique(labels):
        rand_color = color_palette(len(np.unique(labels)))
        img1[labels == x] = rand_color[x]

    img2 = apply_mask(img1, mask, 'black')

    joined = np.concatenate((img2, rgb_img), axis=1)

    estimated_object_count = len(np.unique(markers)) - 1

    analysis_image = []
    analysis_image.append(joined)

    watershed_header = (
        'HEADER_WATERSHED',
        'estimated_object_count'
    )

    watershed_data = (
        'WATERSHED_DATA',
        estimated_object_count
    )

    if params.debug == 'print':
        print_image(dist_transform, os.path.join(params.debug_outdir, str(params.device) + '_watershed_dist_img.png'))
        print_image(joined, os.path.join(params.debug_outdir, str(params.device) + '_watershed_img.png'))
    elif params.debug == 'plot':
        plot_image(dist_transform, cmap='gray')
        plot_image(joined)

    # Store into global measurements
    if not 'watershed' in outputs.measurements:
        outputs.measurements['watershed'] = {}
    outputs.measurements['watershed']['estimated_object_count'] = estimated_object_count

    # Store images
    outputs.images.append(analysis_image)

    return watershed_header, watershed_data, analysis_image
def segment_curvature(segmented_img, objects):
    """ Calculate segment curvature as defined by the ratio between geodesic and euclidean distance.
        Measurement of two-dimensional tortuosity.

        Inputs:
        segmented_img     = Segmented image to plot lengths on
        objects           = List of contours

        Returns:
        labeled_img        = Segmented debugging image with curvature labeled


        :param segmented_img: numpy.ndarray
        :param objects: list
        :return labeled_img: numpy.ndarray

        """
    # Store debug
    debug = params.debug
    params.debug = None

    label_coord_x = []
    label_coord_y = []

    _ = segment_euclidean_length(segmented_img, objects)
    labeled_img = segment_path_length(segmented_img, objects)
    eu_lengths = outputs.observations['segment_eu_length']['value']
    path_lengths = outputs.observations['segment_path_length']['value']
    curvature_measure = [x/y for x, y in zip(path_lengths, eu_lengths)]
    rand_color = color_palette(len(objects))

    for i, cnt in enumerate(objects):
        # Store coordinates for labels
        label_coord_x.append(objects[i][0][0][0])
        label_coord_y.append(objects[i][0][0][1])

        # Draw segments one by one to group segment tips together
        finding_tips_img = np.zeros(segmented_img.shape[:2], np.uint8)
        cv2.drawContours(finding_tips_img, objects, i, (255, 255, 255), 1, lineType=8)
        segment_tips = find_tips(finding_tips_img)
        tip_objects, tip_hierarchies = find_objects(segment_tips, segment_tips)
        points = []

        for t in tip_objects:
            # Gather pairs of coordinates
            x, y = t.ravel()
            coord = (x, y)
            points.append(coord)

        # Draw euclidean distance lines
        cv2.line(labeled_img, points[0], points[1], rand_color[i], 1)

    segment_ids = []
    for i, cnt in enumerate(objects):
        # Calculate geodesic distance
        text = "{:.3f}".format(curvature_measure[i])
        w = label_coord_x[i]
        h = label_coord_y[i]
        cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=params.text_size, color=(150, 150, 150), thickness=params.text_thickness)
        segment_label = "ID" + str(i)
        segment_ids.append(i)

    outputs.add_observation(variable='segment_curvature', trait='segment curvature',
                            method='plantcv.plantcv.morphology.segment_curvature', scale='none', datatype=list,
                            value=curvature_measure, label=segment_ids)

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segment_curvature.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return labeled_img
def cluster_contours(img, roi_objects, roi_obj_hierarchy, nrow=1, ncol=1, show_grid=False):

    """
    This function take a image with multiple contours and clusters them based on user input of rows and columns

    Inputs:
    img                     = RGB or grayscale image data for plotting
    roi_objects             = object contours in an image that are needed to be clustered.
    roi_obj_hierarchy       = object hierarchy
    nrow                    = number of rows to cluster (this should be the approximate  number of desired rows
                              in the entire image (even if there isn't a literal row of plants)
    ncol                    = number of columns to cluster (this should be the approximate number of desired columns
                              in the entire image (even if there isn't a literal row of plants)
    show_grid               = if True then the grid will get plot to show how plants are being clustered

    Returns:
    grouped_contour_indexes = contours grouped
    contours                = All inputed contours

    :param img: numpy.ndarray
    :param roi_objects: list
    :param nrow: int
    :param ncol: int
    :param show_grid: bool
    :return grouped_contour_indexes: list
    :return contours: list
    :return roi_obj_hierarchy: list
    """

    params.device += 1

    if len(np.shape(img)) == 3:
        iy, ix, iz = np.shape(img)
    else:
        iy, ix, = np.shape(img)

    # get the break groups

    if nrow == 1:
        rbreaks = [0, iy]
    else:
        rstep = np.rint(iy / nrow)
        rstep1 = np.int(rstep)
        rbreaks = range(0, iy, rstep1)
    if ncol == 1:
        cbreaks = [0, ix]
    else:
        cstep = np.rint(ix / ncol)
        cstep1 = np.int(cstep)
        cbreaks = range(0, ix, cstep1)

    # categorize what bin the center of mass of each contour

    def digitize(a, step):
        # The way cbreaks and rbreaks are calculated, step will never be an integer
        # if isinstance(step, int):
        #     i = step
        # else:
        i = len(step)
        for x in range(0, i):
            if x == 0:
                if a >= 0 and a < step[x + 1]:
                    return x + 1
            elif a >= step[x - 1] and a < step[x]:
                return x
            elif a > step[x - 1] and a > np.max(step):
                return i

    dtype = [('cx', int), ('cy', int), ('rowbin', int), ('colbin', int), ('index', int)]
    coord = []
    for i in range(0, len(roi_objects)):
        m = cv2.moments(roi_objects[i])
        if m['m00'] == 0:
            pass
        else:
            cx = int(m['m10'] / m['m00'])
            cy = int(m['m01'] / m['m00'])
            # colbin = np.digitize(cx, cbreaks)
            # rowbin = np.digitize(cy, rbreaks)
            colbin = digitize(cx, cbreaks)
            rowbin = digitize(cy, rbreaks)
            a = (cx, cy, colbin, rowbin, i)
            coord.append(a)
    coord1 = np.array(coord, dtype=dtype)
    coord2 = np.sort(coord1, order=('colbin', 'rowbin'))

    # get the list of unique coordinates and group the contours with the same bin coordinates

    groups = []
    for i, y in enumerate(coord2):
        col = y[3]
        row = y[2]
        location = str(row) + ',' + str(col)
        groups.append(location)

    unigroup = np.unique(groups)
    coordgroups = []

    for i, y in enumerate(unigroup):
        col = int(y[0])
        row = int(y[2])
        for a, b in enumerate(coord2):
            if b[2] == col and b[3] == row:
                grp = i
                contour = b[4]
                coordgroups.append((grp, contour))
            else:
                pass

    coordlist = [[y[1] for y in coordgroups if y[0] == x] for x in range(0, (len(unigroup)))]

    contours = roi_objects
    grouped_contour_indexes = coordlist

    # Debug image is rainbow printed contours

    if params.debug is not None:
        if len(np.shape(img)) == 3:
            img_copy = np.copy(img)
        else:
            iy, ix = np.shape(img)
            img_copy = np.zeros((iy, ix, 3), dtype=np.uint8)

        rand_color = color_palette(len(coordlist))
        for i, x in enumerate(coordlist):
            for a in x:
                if roi_obj_hierarchy[0][a][3] > -1:
                    pass
                else:
                    cv2.drawContours(img_copy, roi_objects, a, rand_color[i], -1, hierarchy=roi_obj_hierarchy)
        if show_grid:
            for y in rbreaks:
                cv2.line(img_copy, (0, y), (ix, y), (255, 0, 0), params.line_thickness)
            for x in cbreaks:
                cv2.line(img_copy, (x, 0), (x, iy), (255, 0, 0), params.line_thickness)
        if params.debug=='print':
            print_image(img_copy, os.path.join(params.debug_outdir, str(params.device) + '_clusters.png'))
        elif params.debug=='plot':
            plot_image(img_copy)

    return grouped_contour_indexes, contours, roi_obj_hierarchy
Example #12
0
def watershed_segmentation(device,
                           img,
                           mask,
                           distance=10,
                           filename=False,
                           debug=None):
    """Uses the watershed algorithm to detect boundary of objects. Needs a marker file which specifies area which is
       object (white), background (grey), unknown area (black).

    Inputs:
    device              = device number. Used to count steps in the pipeline
    img                 = image to perform watershed on needs to be 3D (i.e. np.shape = x,y,z not np.shape = x,y)
    mask                = binary image, single channel, object in white and background black
    distance            = min_distance of local maximum
    filename            = if user wants to output analysis images change filenames from false
    debug               = None, print, or plot. Print = save to file, Plot = print to screen.

    Returns:
    device              = device number
    watershed_header    = shape data table headers
    watershed_data      = shape data table values
    analysis_images     = list of output images

    :param device: int
    :param img: numpy array
    :param mask: numpy array
    :param distance: int
    :param filename: str
    :param debug: str
    :return device: int
    :return watershed_header: list
    :return watershed_data: list
    :return analysis_images: list
    """

    if cv2.__version__[0] == '2':
        dist_transform = cv2.distanceTransform(mask,
                                               cv2.cv.CV_DIST_L2,
                                               maskSize=0)
    else:
        dist_transform = cv2.distanceTransformWithLabels(mask,
                                                         cv2.DIST_L2,
                                                         maskSize=0)[0]

    localMax = peak_local_max(dist_transform,
                              indices=False,
                              min_distance=distance,
                              labels=mask)

    markers = ndi.label(localMax, structure=np.ones((3, 3)))[0]
    dist_transform1 = -dist_transform
    labels = watershed(dist_transform1, markers, mask=mask)

    img1 = np.copy(img)

    for x in np.unique(labels):
        rand_color = color_palette(len(np.unique(labels)))
        img1[labels == x] = rand_color[x]

    device, img2 = apply_mask(img1, mask, 'black', device, debug=None)

    joined = np.concatenate((img2, img), axis=1)

    estimated_object_count = len(np.unique(markers)) - 1

    analysis_images = []
    if filename != False:
        out_file = str(filename[0:-4]) + '_watershed.jpg'
        print_image(joined, out_file)
        analysis_images.append(['IMAGE', 'watershed', out_file])

    watershed_header = ('HEADER_WATERSHED', 'estimated_object_count')

    watershed_data = ('WATERSHED_DATA', estimated_object_count)

    if debug == 'print':
        print_image(dist_transform, str(device) + '_watershed_dist_img.png')
        print_image(joined, str(device) + '_watershed_img.png')
    elif debug == 'plot':
        plot_image(dist_transform, cmap='gray')
        plot_image(joined)

    return device, watershed_header, watershed_data, analysis_images
def segment_insertion_angle(skel_img, segmented_img, leaf_objects, stem_objects, size):
    """ Find leaf insertion angles in degrees of skeleton segments. Fit a linear regression line to the stem.
        Use `size` pixels on  the portion of leaf next to the stem find a linear regression line,
        and calculate angle between the two lines per leaf object.

        Inputs:
        skel_img         = Skeletonized image
        segmented_img    = Segmented image to plot slope lines and intersection angles on
        leaf_objects     = List of leaf segments
        stem_objects     = List of stem segments
        size             = Size of inner leaf used to calculate slope lines

        Returns:
        labeled_img      = Debugging image with angles labeled

        :param skel_img: numpy.ndarray
        :param segmented_img: numpy.ndarray
        :param leaf_objects: list
        :param stem_objects: list
        :param size: int
        :return labeled_img: numpy.ndarray
        """

    # Store debug
    debug = params.debug
    params.debug = None

    rows,cols = segmented_img.shape[:2]
    labeled_img = segmented_img.copy()
    segment_slopes = []
    insertion_segments = []
    insertion_hierarchies = []
    intersection_angles = []
    label_coord_x = []
    label_coord_y = []
    valid_segment = []

    # Create a list of tip tuples to use for sorting
    tips = find_tips(skel_img)
    tips = dilate(tips, 3, 1)
    tip_objects, tip_hierarchies = find_objects(tips, tips)
    tip_tuples = []
    for i, cnt in enumerate(tip_objects):
        tip_tuples.append((cnt[0][0][0], cnt[0][0][1]))

    rand_color = color_palette(len(leaf_objects))

    for i, cnt in enumerate(leaf_objects):
        # Draw leaf objects
        find_segment_tangents = np.zeros(segmented_img.shape[:2], np.uint8)
        cv2.drawContours(find_segment_tangents, leaf_objects, i, 255, 1, lineType=8)

        # Prune back ends of leaves
        pruned_segment = _iterative_prune(find_segment_tangents, size)

        # Segment ends are the portions pruned off
        segment_ends = find_segment_tangents - pruned_segment
        segment_end_obj, segment_end_hierarchy = find_objects(segment_ends, segment_ends)
        is_insertion_segment = []

        if not len(segment_end_obj) == 2:
            print("Size too large, contour with ID#", i, "got pruned away completely.")
        else:
            # The contour can have insertion angle calculated
            valid_segment.append(cnt)

            # Determine if a segment is leaf end or leaf insertion segment
            for j, obj in enumerate(segment_end_obj):

                segment_plot = np.zeros(segmented_img.shape[:2], np.uint8)
                cv2.drawContours(segment_plot, obj, -1, 255, 1, lineType=8)
                overlap_img = logical_and(segment_plot, tips)

                # If none of the tips are within a segment_end then it's an insertion segment
                if np.sum(overlap_img) == 0:
                    insertion_segments.append(segment_end_obj[j])
                    insertion_hierarchies.append(segment_end_hierarchy[0][j])

            # Store coordinates for labels
            label_coord_x.append(leaf_objects[i][0][0][0])
            label_coord_y.append(leaf_objects[i][0][0][1])

    rand_color = color_palette(len(valid_segment))

    for i, cnt in enumerate(valid_segment):
        cv2.drawContours(labeled_img, valid_segment, i, rand_color[i], params.line_thickness, lineType=8)

    # Plot stem segments
    stem_img = np.zeros(segmented_img.shape[:2], np.uint8)
    cv2.drawContours(stem_img, stem_objects, -1, 255, 2, lineType=8)
    branch_pts = find_branch_pts(skel_img)
    stem_img = stem_img + branch_pts
    stem_img = closing(stem_img)
    combined_stem, combined_stem_hier = find_objects(stem_img, stem_img)

    # Make sure stem objects are a single contour
    loop_count=0
    while len(combined_stem) > 1 and loop_count<50:
        loop_count += 1
        stem_img = dilate(stem_img, 2, 1)
        stem_img = closing(stem_img)
        combined_stem, combined_stem_hier = find_objects(stem_img, stem_img)

    if loop_count == 50:
        fatal_error('Unable to combine stem objects.')

    # Find slope of the stem
    [vx, vy, x, y] = cv2.fitLine(combined_stem[0], cv2.DIST_L2, 0, 0.01, 0.01)
    stem_slope = -vy / vx
    stem_slope = stem_slope[0]
    lefty = int((-x * vy / vx) + y)
    righty = int(((cols - x) * vy / vx) + y)
    cv2.line(labeled_img, (cols - 1, righty), (0, lefty), (150, 150, 150), 3)

    rand_color = color_palette(len(insertion_segments))

    for t, segment in enumerate(insertion_segments):
        # Find line fit to each segment
        [vx, vy, x, y] = cv2.fitLine(segment, cv2.DIST_L2, 0, 0.01, 0.01)
        slope = -vy / vx
        left_list = int((-x * vy / vx) + y)
        right_list = int(((cols - x) * vy / vx) + y)
        segment_slopes.append(slope[0])

        # Draw slope lines if possible
        if slope > 1000000 or slope < -1000000:
            print("Slope of contour with ID#", t, "is", slope, "and cannot be plotted.")
        else:
            cv2.line(labeled_img, (cols - 1, right_list), (0, left_list), rand_color[t], 1)

        # Store intersection angles between insertion segment and stem line
        intersection_angle = _slope_to_intesect_angle(slope[0], stem_slope)
        # Function measures clockwise but we want the acute angle between stem and leaf insertion
        if intersection_angle > 90:
            intersection_angle = 180 - intersection_angle
        intersection_angles.append(intersection_angle)

    segment_ids = []

    for i, cnt in enumerate(insertion_segments):
        # Label slope lines
        w = label_coord_x[i]
        h = label_coord_y[i]
        text = "{:.2f}".format(intersection_angles[i])
        cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=params.text_size, color=(150, 150, 150), thickness=params.text_thickness)
        segment_label = "ID" + str(i)
        segment_ids.append(i)

    outputs.add_observation(variable='segment_insertion_angle', trait='segment insertion angle',
                            method='plantcv.plantcv.morphology.segment_insertion_angle', scale='degrees', datatype=list,
                            value=intersection_angles, label=segment_ids)

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(labeled_img,
                    os.path.join(params.debug_outdir, str(params.device) + '_segment_insertion_angles.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return labeled_img
Example #14
0
def check_cycles(skel_img):
    """ Check for cycles in a skeleton image
    Inputs:
    skel_img     = Skeletonized image

    Returns:
    cycle_img    = Image with cycles identified

    :param skel_img: numpy.ndarray
    :return cycle_img: numpy.ndarray
    """

    # Store debug
    debug = params.debug
    params.debug = None

    # Create the mask needed for cv2.floodFill, must be larger than the image
    h, w = skel_img.shape[:2]
    mask = np.zeros((h + 2, w + 2), np.uint8)

    # Copy the skeleton since cv2.floodFill will draw on it
    skel_copy = skel_img.copy()
    cv2.floodFill(skel_copy, mask=mask, seedPoint=(0, 0), newVal=255)

    # Invert so the holes are white and background black
    just_cycles = cv2.bitwise_not(skel_copy)

    # Erode slightly so that cv2.findContours doesn't think diagonal pixels are separate contours
    just_cycles = erode(just_cycles, 2, 1)

    # Use pcv.find_objects to turn plots of holes into countable contours
    cycle_objects, cycle_hierarchies = find_objects(just_cycles, just_cycles)

    # Count the number of holes
    num_cycles = len(cycle_objects)

    # Make debugging image
    cycle_img = skel_img.copy()
    cycle_img = dilate(cycle_img, params.line_thickness, 1)
    cycle_img = cv2.cvtColor(cycle_img, cv2.COLOR_GRAY2RGB)
    if num_cycles > 0:
        rand_color = color_palette(num_cycles)
        for i, cnt in enumerate(cycle_objects):
            cv2.drawContours(cycle_img,
                             cycle_objects,
                             i,
                             rand_color[i],
                             params.line_thickness,
                             lineType=8,
                             hierarchy=cycle_hierarchies)

    # Store Cycle Data
    outputs.add_observation(variable='num_cycles',
                            trait='number of cycles',
                            method='plantcv.plantcv.morphology.check_cycles',
                            scale='none',
                            datatype=int,
                            value=num_cycles,
                            label='none')

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(
            cycle_img,
            os.path.join(params.debug_outdir,
                         str(params.device) + '_cycles.png'))
    elif params.debug == 'plot':
        plot_image(cycle_img)

    return cycle_img
Example #15
0
def clustered_contours(img,
                       grouped_contour_indices,
                       roi_objects,
                       roi_obj_hierarchy,
                       nrow=1,
                       ncol=1):
    """
    This function takes the outputs from cluster_contours

    Inputs:
    img                     = RGB or grayscale image data for plotting
    grouped_contour_indices = Indices for grouping contours
    roi_objects             = object contours in an image that are needed to be clustered.
    roi_obj_hierarchy       = object hierarchy
    nrow                    = Optional, number of rows. If changed from default, grid gets plot.
    ncol                    = Optional, number of columns. If changed from default, grid gets plot.

    Returns:
    clustered_image         = Labeled clusters image

    :param img: numpy.ndarray
    :param grouped_contour_indices: list
    :param roi_objects: list
    :param roi_obj_hierarchy: numpy.ndarray
    :param nrow: int
    :param ncol: int

    :return clustered_image: numpy.ndarray
    """

    clustered_image = np.copy(img)
    iy, ix = np.shape(img)[:2]

    # Gray input images need to get converted to RGB for plotting colors
    if len(np.shape(img)) == 2:
        clustered_image = cv2.cvtColor(clustered_image, cv2.COLOR_GRAY2RGB)

    # Plot grid if nrow or ncol are changed from the default
    if nrow > 1 or ncol > 1:
        rbreaks = range(0, iy, int(np.rint(iy / nrow)))
        cbreaks = range(0, ix, int(np.rint(ix / ncol)))
        for y in rbreaks:
            cv2.line(clustered_image, (0, y), (ix, y), (255, 0, 0),
                     params.line_thickness)
        for x in cbreaks:
            cv2.line(clustered_image, (x, 0), (x, iy), (255, 0, 0),
                     params.line_thickness)

    rand_color = color_palette(len(grouped_contour_indices))
    grouped_contours = []
    for i, x in enumerate(grouped_contour_indices):
        for a in x:
            if roi_obj_hierarchy[0][a][3] > -1:
                pass
            else:
                cv2.drawContours(clustered_image,
                                 roi_objects,
                                 a,
                                 rand_color[i],
                                 -1,
                                 hierarchy=roi_obj_hierarchy)
                # Add contour to list to get grouped
                grouped_contours.append(roi_objects[a])
        if len(grouped_contours) > 0:
            # Combine contours into a single contour
            grouped_contours = np.vstack(grouped_contours)
            # Plot the bounding circle around the contours that got grouped together
            center, radius = cv2.minEnclosingCircle(points=grouped_contours)
            cv2.circle(img=clustered_image,
                       center=(int(center[0]), int(center[1])),
                       radius=int(radius),
                       color=rand_color[i],
                       thickness=params.line_thickness,
                       lineType=8)
            # Label the cluster ID
            cv2.putText(img=clustered_image,
                        text=str(i),
                        org=(int(center[0]), int(center[1])),
                        fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                        fontScale=params.text_size,
                        color=(200, 200, 200),
                        thickness=params.text_thickness)
        # Empty the grouped_contours list for the next group
        grouped_contours = []

    params.device += 1

    if params.debug == 'print':
        print_image(
            clustered_image,
            os.path.join(params.debug_outdir,
                         str(params.device) + '_clusters.png'))
    elif params.debug == 'plot':
        plot_image(clustered_image)

    return clustered_image
Example #16
0
def segment_combine(segment_list, objects, mask):
    """ Combine user specified segments together

            Inputs:
            segment_list  = List of segments to get combined, or list of lists of segments to get combined
            objects       = List of contours
            hierarchy     = Contour hierarchy NumPy array
            mask          = Binary mask for debugging image

            Returns:
            segmented_img = Segmented image
            objects       = Updated list of contours
            hierarchy     = Updated contour hierarchy NumPy array

            :param segment_list: list
            :param objects: list
            :param mask: numpy.ndarray
            :return labeled_img: numpy.ndarray
            :return objects: list
            """
    label_coord_x = []
    label_coord_y = []
    all_objects = objects[:]

    # If user provides a single list of objects to combine
    if type(segment_list[0]) is int:
        num_contours = len(segment_list)
        count = 1

        # Store the first object into the new object array
        new_objects = all_objects[segment_list[0]]
        # Remove the objects getting combined from the list of all objects
        all_objects.remove(objects[segment_list[0]])

        while count < num_contours:
            # Combine objects into a single array
            new_objects = np.append(new_objects, objects[segment_list[count]],
                                    0)
            # Remove the objects getting combined from the list of all objects
            all_objects.remove(objects[segment_list[count]])
            count += 1
        # Replace with the combined object
        all_objects.append(new_objects)

    # If user provides a list of lists of objects to combine
    elif type(segment_list[0]) is list:
        # For each list provided
        for lists in segment_list:
            num_contours = len(lists)
            count = 1
            # Store the first object into the new object array
            new_objects = all_objects[lists[0]]
            # Remove the objects getting combined from the list of all objects
            all_objects.remove(objects[lists[0]])

            while count < num_contours:
                # Combine objects into a single array
                new_objects = np.append(new_objects, objects[lists[count]], 0)
                # Remove the objects getting combined from the list of all objects
                all_objects.remove(objects[lists[count]])
                count += 1
            # Add combined contour to list of all contours
            all_objects.append(new_objects)
    else:
        fatal_error(
            "segment_list must be a list of object ID's or a list of lists of ID's!"
        )

    labeled_img = mask.copy()
    labeled_img = cv2.cvtColor(labeled_img, cv2.COLOR_GRAY2RGB)

    # Color each segment a different color, use a previously saved scale if available
    rand_color = color_palette(num=len(all_objects), saved=True)

    # Plot all segment contours
    for i, cnt in enumerate(all_objects):
        cv2.drawContours(labeled_img,
                         all_objects[i],
                         -1,
                         rand_color[i],
                         params.line_thickness,
                         lineType=8)
        # Store coordinates for labels
        label_coord_x.append(all_objects[i][0][0][0])
        label_coord_y.append(all_objects[i][0][0][1])

    # Label segments
    for i, cnt in enumerate(all_objects):
        w = label_coord_x[i]
        h = label_coord_y[i]
        text = "ID:{}".format(i)
        cv2.putText(img=labeled_img,
                    text=text,
                    org=(w, h),
                    fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=params.text_size,
                    color=rand_color[i],
                    thickness=2)

    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(
            labeled_img,
            os.path.join(params.debug_outdir,
                         str(params.device) + '_combined_segment_ids.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return labeled_img, all_objects
Example #17
0
def cluster_contours(img,
                     roi_objects,
                     roi_obj_hierarchy,
                     nrow=1,
                     ncol=1,
                     show_grid=False):
    """
    This function take a image with multiple contours and clusters them based on user input of rows and columns

    Inputs:
    img                     = RGB or grayscale image data for plotting
    roi_objects             = object contours in an image that are needed to be clustered.
    roi_obj_hierarchy       = object hierarchy
    nrow                    = number of rows to cluster (this should be the approximate  number of desired rows
                              in the entire image (even if there isn't a literal row of plants)
    ncol                    = number of columns to cluster (this should be the approximate number of desired columns
                              in the entire image (even if there isn't a literal row of plants)
    show_grid               = if True then the grid will get plot to show how plants are being clustered

    Returns:
    grouped_contour_indexes = contours grouped
    contours                = All inputed contours

    :param img: numpy.ndarray
    :param roi_objects: list
    :param nrow: int
    :param ncol: int
    :param show_grid: bool
    :return grouped_contour_indexes: list
    :return contours: list
    :return roi_obj_hierarchy: list
    """

    params.device += 1

    if len(np.shape(img)) == 3:
        iy, ix, iz = np.shape(img)
    else:
        iy, ix, = np.shape(img)

    # get the break groups

    if nrow == 1:
        rbreaks = [0, iy]
    else:
        rstep = np.rint(iy / nrow)
        rstep1 = np.int(rstep)
        rbreaks = range(0, iy, rstep1)
    if ncol == 1:
        cbreaks = [0, ix]
    else:
        cstep = np.rint(ix / ncol)
        cstep1 = np.int(cstep)
        cbreaks = range(0, ix, cstep1)

    # categorize what bin the center of mass of each contour

    def digitize(a, step):
        # The way cbreaks and rbreaks are calculated, step will never be an integer
        # if isinstance(step, int):
        #     i = step
        # else:
        i = len(step)
        for x in range(0, i):
            if x == 0:
                if a >= 0 and a < step[x + 1]:
                    return x + 1
            elif a >= step[x - 1] and a < step[x]:
                return x
            elif a > step[x - 1] and a > np.max(step):
                return i

    dtype = [('cx', int), ('cy', int), ('rowbin', int), ('colbin', int),
             ('index', int)]
    coord = []
    for i in range(0, len(roi_objects)):
        m = cv2.moments(roi_objects[i])
        if m['m00'] == 0:
            pass
        else:
            cx = int(m['m10'] / m['m00'])
            cy = int(m['m01'] / m['m00'])
            # colbin = np.digitize(cx, cbreaks)
            # rowbin = np.digitize(cy, rbreaks)
            colbin = digitize(cx, cbreaks)
            rowbin = digitize(cy, rbreaks)
            a = (cx, cy, colbin, rowbin, i)
            coord.append(a)
    coord1 = np.array(coord, dtype=dtype)
    coord2 = np.sort(coord1, order=('colbin', 'rowbin'))

    # get the list of unique coordinates and group the contours with the same bin coordinates

    groups = []
    for i, y in enumerate(coord2):
        col = y[3]
        row = y[2]
        location = str(row) + ',' + str(col)
        groups.append(location)

    unigroup = np.unique(groups)
    coordgroups = []

    for i, y in enumerate(unigroup):
        col = int(y[0])
        row = int(y[2])
        for a, b in enumerate(coord2):
            if b[2] == col and b[3] == row:
                grp = i
                contour = b[4]
                coordgroups.append((grp, contour))
            else:
                pass

    coordlist = [[y[1] for y in coordgroups if y[0] == x]
                 for x in range(0, (len(unigroup)))]

    contours = roi_objects
    grouped_contour_indexes = coordlist

    # Debug image is rainbow printed contours

    if params.debug is not None:
        if len(np.shape(img)) == 3:
            img_copy = np.copy(img)
        else:
            iy, ix = np.shape(img)
            img_copy = np.zeros((iy, ix, 3), dtype=np.uint8)

        rand_color = color_palette(len(coordlist))
        for i, x in enumerate(coordlist):
            for a in x:
                if roi_obj_hierarchy[0][a][3] > -1:
                    pass
                else:
                    cv2.drawContours(img_copy,
                                     roi_objects,
                                     a,
                                     rand_color[i],
                                     -1,
                                     hierarchy=roi_obj_hierarchy)
        if show_grid:
            for y in rbreaks:
                cv2.line(img_copy, (0, y), (ix, y), (255, 0, 0),
                         params.line_thickness)
            for x in cbreaks:
                cv2.line(img_copy, (x, 0), (x, iy), (255, 0, 0),
                         params.line_thickness)
        if params.debug == 'print':
            print_image(
                img_copy,
                os.path.join(params.debug_outdir,
                             str(params.device) + '_clusters.png'))
        elif params.debug == 'plot':
            plot_image(img_copy)

    return grouped_contour_indexes, contours, roi_obj_hierarchy
Example #18
0
def warp(img, refimg, pts, refpts, method='default'):
    """Warp an image to another perspective

    Inputs:
    img = grayscale or binary image data to be warped
    refimg = RGB or grayscale image data to be used as reference
    pts = 4 coordinates on img1
    refpts = 4 coordinates on img2
    method = method of finding the transformation. 'default', 'ransac', 'lmeds', 'rho'
    Returns:
    warped_img = warped image

    :param img: numpy.ndarray
    :param refimg: numpy.ndarray
    :param pts: list of tuples
    :param refpts: list of tuples
    :param method: str
    :return warped_img: numpy.ndarray
    """

    params.device += 1

    if len(pts) != 4 or len(refpts) != 4:
        fatal_error('Please provide 4 pairs of corresponding coordinates.')
    if len(img.shape) > 2:
        fatal_error('The input `img` should be grayscale or binary.')

    methods = {
        'default': 0,
        'ransac': cv2.RANSAC,
        'lmeds': cv2.LMEDS,
        'rho': cv2.RHO
    }

    shape_ref = refimg.shape
    rows_ref, cols_ref = shape_ref[0:2]

    # convert list of tuples to array for cv2 functions
    ptsarr = np.array(pts, dtype='float32')
    refptsarr = np.array(refpts, dtype='float32')

    # find tranformation matrix and warp
    mat, _ = cv2.findHomography(ptsarr, refptsarr, method=methods.get(method))
    warped_img = cv2.warpPerspective(src=img,
                                     M=mat,
                                     dsize=(cols_ref, rows_ref))

    # preserve binary
    if len(np.unique(img)) == 2:
        warped_img[warped_img > 0] = 255

    if params.debug is not None:
        # scale marker_size and line_thickness for different resolutions
        rows_img = img.shape[0]
        if rows_img > rows_ref:
            res_ratio_i = int(
                np.ceil(rows_img /
                        rows_ref))  # ratio never smaller than 1 with np.ceil
            res_ratio_r = 1
        else:
            res_ratio_r = int(np.ceil(rows_ref / rows_img))
            res_ratio_i = 1
        # marker colors
        colors = color_palette(len(pts))

        # temp rgb image for colored markers on img
        img2 = img.copy()
        img2 = cv2.merge((img2, img2, img2))
        for i, pt in enumerate(pts):
            cv2.drawMarker(img2,
                           pt,
                           color=colors[i],
                           markerType=cv2.MARKER_CROSS,
                           markerSize=params.marker_size * res_ratio_i,
                           thickness=params.line_thickness * res_ratio_i)

        # temp rgb image for colored markers on refimg
        refimg2 = refimg.copy()
        if len(shape_ref) == 2:
            refimg2 = cv2.merge((refimg2, refimg2, refimg2))
        for i, pt in enumerate(refpts):
            cv2.drawMarker(refimg2,
                           pt,
                           color=colors[i],
                           markerType=cv2.MARKER_CROSS,
                           markerSize=params.marker_size * res_ratio_r,
                           thickness=params.line_thickness * res_ratio_r)

        debug_mode = params.debug
        params.debug = None
        img_blend = overlay_two_imgs(warped_img, refimg)
        params.debug = debug_mode

        if params.debug == 'plot':
            plot_image(img2)
            plot_image(refimg2)
            plot_image(img_blend)
        if params.debug == 'print':
            print_image(
                img2,
                os.path.join(params.debug_outdir,
                             str(params.device) + "_img-to-warp.png"))
            print_image(
                refimg2,
                os.path.join(params.debug_outdir,
                             str(params.device) + "_img-ref.png"))
            print_image(
                img_blend,
                os.path.join(params.debug_outdir,
                             str(params.device) + "_warp_overlay.png"))

    return warped_img
Example #19
0
def check_cycles(skel_img):
    """ Check for cycles in a skeleton image
    Inputs:
    skel_img     = Skeletonized image

    Returns:
    cycle_img    = Image with cycles identified

    :param skel_img: numpy.ndarray
    :return cycle_img: numpy.ndarray
    """

    # Store debug
    debug = params.debug
    params.debug = None

    # Create the mask needed for cv2.floodFill, must be larger than the image
    h, w = skel_img.shape[:2]
    mask = np.zeros((h + 2, w + 2), np.uint8)

    # Copy the skeleton since cv2.floodFill will draw on it
    skel_copy = skel_img.copy()
    cv2.floodFill(skel_copy, mask=mask, seedPoint=(0, 0), newVal=255)

    # Invert so the holes are white and background black
    just_cycles = cv2.bitwise_not(skel_copy)

    # Erode slightly so that cv2.findContours doesn't think diagonal pixels are separate contours
    just_cycles = erode(just_cycles, 2, 1)

    # Use pcv.find_objects to turn plots of holes into countable contours
    cycle_objects, cycle_hierarchies = find_objects(just_cycles, just_cycles)

    # Count the number of holes
    num_cycles = len(cycle_objects)

    # Make debugging image
    cycle_img = skel_img.copy()
    cycle_img = dilate(cycle_img, params.line_thickness, 1)
    cycle_img = cv2.cvtColor(cycle_img, cv2.COLOR_GRAY2RGB)
    rand_color = color_palette(num_cycles)
    for i, cnt in enumerate(cycle_objects):
        cv2.drawContours(cycle_img, cycle_objects, i, rand_color[i], params.line_thickness, lineType=8,
                         hierarchy=cycle_hierarchies)

    # Store Cycle Data
    outputs.add_observation(variable='num_cycles', trait='number of cycles',
                            method='plantcv.plantcv.morphology.check_cycles', scale='none', datatype=int,
                            value=num_cycles, label='none')

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(cycle_img, os.path.join(params.debug_outdir, str(params.device) + '_cycles.png'))
    elif params.debug == 'plot':
        plot_image(cycle_img)

    return cycle_img
Example #20
0
def segment_insertion_angle(skel_img, segmented_img, leaf_objects,
                            leaf_hierarchies, stem_objects, size):
    """ Find leaf insertion angles in degrees of skeleton segments. Fit a linear regression line to the stem.
        Use `size` pixels on  the portion of leaf next to the stem find a linear regression line,
        and calculate angle between the two lines per leaf object.

        Inputs:
        skel_img         = Skeletonized image
        segmented_img    = Segmented image to plot slope lines and intersection angles on
        leaf_objects     = List of leaf segments
        leaf_hierarchies = Leaf contour hierarchy NumPy array
        stem_objects     = List of stem segments
        size             = Size of inner leaf used to calculate slope lines

        Returns:
        insertion_angle_header = Leaf insertion angle headers
        insertion_angle_data   = Leaf insertion angle values
        labeled_img            = Debugging image with angles labeled

        :param skel_img: numpy.ndarray
        :param segmented_img: numpy.ndarray
        :param leaf_objects: list
        :param leaf_hierarchies: numpy.ndarray
        :param stem_objects: list
        :param size: int
        :return insertion_angle_header: list
        :return insertion_angle_data: list
        :return labeled_img: numpy.ndarray
        """

    # Store debug
    debug = params.debug
    params.debug = None

    rows, cols = segmented_img.shape[:2]
    labeled_img = segmented_img.copy()
    segment_slopes = []
    insertion_segments = []
    insertion_hierarchies = []
    intersection_angles = []
    label_coord_x = []
    label_coord_y = []

    # Create a list of tip tuples to use for sorting
    tips = find_tips(skel_img)
    tip_objects, tip_hierarchies = find_objects(tips, tips)
    tip_tuples = []
    for i, cnt in enumerate(tip_objects):
        tip_tuples.append((cnt[0][0][0], cnt[0][0][1]))

    rand_color = color_palette(len(leaf_objects))

    for i, cnt in enumerate(leaf_objects):
        # Draw leaf objects
        find_segment_tangents = np.zeros(segmented_img.shape[:2], np.uint8)
        cv2.drawContours(find_segment_tangents,
                         leaf_objects,
                         i,
                         255,
                         1,
                         lineType=8,
                         hierarchy=leaf_hierarchies)
        cv2.drawContours(labeled_img,
                         leaf_objects,
                         i,
                         rand_color[i],
                         params.line_thickness,
                         lineType=8,
                         hierarchy=leaf_hierarchies)

        # Prune back ends of leaves
        pruned_segment = prune(find_segment_tangents, size)

        # Segment ends are the portions pruned off
        segment_ends = find_segment_tangents - pruned_segment
        segment_end_obj, segment_end_hierarchy = find_objects(
            segment_ends, segment_ends)
        is_insertion_segment = []

        if not len(segment_end_obj) == 2:
            print("Size too large, contour with ID#", i,
                  "got pruned away completely.")
        else:
            # Determine if a segment is leaf end or leaf insertion segment
            for j, obj in enumerate(segment_end_obj):

                cnt_as_tuples = []
                num_pixels = len(obj)
                count = 0

                # Turn each contour into a list of tuples (can't search for list of coords, so reformat)
                while num_pixels > count:
                    x_coord = obj[count][0][0]
                    y_coord = obj[count][0][1]
                    cnt_as_tuples.append((x_coord, y_coord))
                    count += 1

                for tip_tups in tip_tuples:
                    # If a tip is inside the list of contour tuples then it is a leaf end segment
                    if tip_tups in cnt_as_tuples:
                        is_insertion_segment.append(False)
                    else:
                        is_insertion_segment.append(True)

                # If none of the tips are within a segment_end then it's an insertion segment
                if all(is_insertion_segment):
                    insertion_segments.append(segment_end_obj[j])
                    insertion_hierarchies.append(segment_end_hierarchy[0][j])

        # Store coordinates for labels
        label_coord_x.append(leaf_objects[i][0][0][0])
        label_coord_y.append(leaf_objects[i][0][0][1])

    # Plot stem segments
    stem_img = np.zeros(segmented_img.shape[:2], np.uint8)
    cv2.drawContours(stem_img, stem_objects, -1, 255, 2, lineType=8)
    branch_pts = find_branch_pts(skel_img)
    stem_img = stem_img + branch_pts
    stem_img = closing(stem_img)
    combined_stem, combined_stem_hier = find_objects(stem_img, stem_img)

    # Make sure stem objects are a single contour
    while len(combined_stem) > 1:
        stem_img = dilate(stem_img, 2, 1)
        stem_img = closing(stem_img)
        combined_stem, combined_stem_hier = find_objects(stem_img, stem_img)

    # Find slope of the stem
    [vx, vy, x, y] = cv2.fitLine(combined_stem[0], cv2.DIST_L2, 0, 0.01, 0.01)
    stem_slope = -vy / vx
    stem_slope = stem_slope[0]
    lefty = int((-x * vy / vx) + y)
    righty = int(((cols - x) * vy / vx) + y)
    cv2.line(labeled_img, (cols - 1, righty), (0, lefty), (150, 150, 150), 3)

    for t, segment in enumerate(insertion_segments):
        # Find line fit to each segment
        [vx, vy, x, y] = cv2.fitLine(segment, cv2.DIST_L2, 0, 0.01, 0.01)
        slope = -vy / vx
        left_list = int((-x * vy / vx) + y)
        right_list = int(((cols - x) * vy / vx) + y)
        segment_slopes.append(slope[0])

        # Draw slope lines if possible
        if slope > 1000000 or slope < -1000000:
            print("Slope of contour with ID#", t, "is", slope,
                  "and cannot be plotted.")
        else:
            cv2.line(labeled_img, (cols - 1, right_list), (0, left_list),
                     rand_color[t], 1)

        # Store intersection angles between insertion segment and stem line
        intersection_angle = _slope_to_intesect_angle(slope[0], stem_slope)
        # Function measures clockwise but we want the acute angle between stem and leaf insertion
        if intersection_angle > 90:
            intersection_angle = 180 - intersection_angle
        intersection_angles.append(intersection_angle)

    insertion_angle_header = ['HEADER_INSERTION_ANGLE']
    insertion_angle_data = ['INSERTION_ANGLE_DATA']

    for i, cnt in enumerate(insertion_segments):
        # Label slope lines
        w = label_coord_x[i]
        h = label_coord_y[i]
        text = "{:.2f}".format(intersection_angles[i])
        cv2.putText(img=labeled_img,
                    text=text,
                    org=(w, h),
                    fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=.55,
                    color=(150, 150, 150),
                    thickness=2)
        segment_label = "ID" + str(i)
        insertion_angle_header.append(segment_label)
    insertion_angle_data.extend(intersection_angles)

    if 'morphology_data' not in outputs.measurements:
        outputs.measurements['morphology_data'] = {}
    outputs.measurements['morphology_data'][
        'segment_insertion_angles'] = intersection_angles

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(
            labeled_img,
            os.path.join(params.debug_outdir,
                         str(params.device) + '_segment_insertion_angles.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return insertion_angle_header, insertion_angle_data, labeled_img
Example #21
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
def segment_euclidean_length(segmented_img, objects, hierarchies):
    """ Use segmented skeleton image to gather euclidean length measurements per segment

        Inputs:
        segmented_img = Segmented image to plot lengths on
        objects       = List of contours
        hierarchy     = Contour hierarchy NumPy array

        Returns:
        eu_length_header = Segment euclidean length data header
        eu_length_data   = Segment euclidean length data values
        labeled_img      = Segmented debugging image with lengths labeled

        :param segmented_img: numpy.ndarray
        :param objects: list
        :param hierarchy: numpy.ndarray
        :return labeled_img: numpy.ndarray
        :return eu_length_header: list
        :return eu_length_data: list

        """
    # Store debug
    debug = params.debug
    params.debug = None

    x_list = []
    y_list = []
    segment_lengths = []
    rand_color = color_palette(len(objects))


    labeled_img = segmented_img.copy()

    for i, cnt in enumerate(objects):
        # Store coordinates for labels
        x_list.append(objects[i][0][0][0])
        y_list.append(objects[i][0][0][1])

        # Draw segments one by one to group segment tips together
        finding_tips_img = np.zeros(segmented_img.shape[:2], np.uint8)
        cv2.drawContours(finding_tips_img, objects, i, (255, 255, 255), 1, lineType=8,
                         hierarchy=hierarchies)
        segment_tips = find_tips(finding_tips_img)
        tip_objects, tip_hierarchies = find_objects(segment_tips, segment_tips)
        points = []
        if not len(tip_objects) == 2:
            fatal_error("Too many tips found per segment, try pruning again")
        for t in tip_objects:
            # Gather pairs of coordinates
            x, y = t.ravel()
            coord = (x, y)
            points.append(coord)

        # Draw euclidean distance lines
        cv2.line(labeled_img, points[0], points[1], rand_color[i], 1)

        # Calculate euclidean distance between tips of each contour
        segment_lengths.append(euclidean(points[0], points[1]))

    eu_length_header = ['HEADER_EU_LENGTH']
    eu_length_data = ['EU_LENGTH_DATA']
    # Put labels of length
    for c, value in enumerate(segment_lengths):
        text = "{:.2f}".format(value)
        w = x_list[c]
        h = y_list[c]
        cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=.4,
                    color=(150, 150, 150), thickness=1)
        segment_label = "ID" + str(c)
        eu_length_header.append(segment_label)
        eu_length_data.append(segment_lengths[c])

    if 'morphology_data' not in outputs.measurements:
        outputs.measurements['morphology_data'] = {}
    outputs.measurements['morphology_data']['segment_eu_lengths'] = segment_lengths

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segment_eu_lengths.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return eu_length_header, eu_length_data, labeled_img
clust_img, clust_masks = pcv.spatial_clustering(mask=kept_mask,
                                                algorithm="DBSCAN",
                                                min_cluster_size=5,
                                                max_distance=None)

# In[32]:

# The pcv.cluster_contours function uses another PlantCV function
# that returns a random list of RGB color values equally spaces
# across a rainbow color spectrum. This function can be useful
# when a color palette is needed

# Inputs:
#   num - An integer greater than or equal to 1. If num=1 then
#         a random color is returned
rand_colors = pcv.color_palette(num=5)
print(rand_colors)

# In[26]:

# Set the sequence of colors from the color_scale created by the
# color_palette function to "sequential" or "random" order.
# Default = "sequential".
pcv.params.color_sequence = 'random'
cluster_img = pcv.visualize.clustered_contours(
    img=img1,
    grouped_contour_indices=clusters_i,
    roi_objects=contours,
    roi_obj_hierarchy=hierarchies)

# In[27]:
Example #24
0
def spatial_clustering(mask, algorithm="DBSCAN", min_cluster_size=5, max_distance=None):
    """Counts and segments portions of an image based on distance between two pixels.
    Masks showing all clusters, plus masks of individual clusters, are returned.

    Inputs:
    mask             = Mask/binary image to segment into clusters.
    algorithm        = Algorithm to use for segregating different clusters.
                       Currently supporting OPTICS and DBSCAN. (Default="DBSCAN")
    min_cluster_size = The minimum size a section of a mask must be (in pixels)
                       before it can be considered its own cluster. (Default=5)
    max_distance     = The total distance between two pixels for them to be considered a part
                       of the same cluster.  For the DBSCAN algorithm, value must be between
                       0 and 1.  For OPTICS, the value is in pixels and depends on the size
                       of your picture.  (Default=0)

    Returns:
    clust_img        = Output image with each cluster draw with a unique color.
    clust_masks      = List of binary masks, one per cluster.

    :param mask: numpy.ndarray
    :param algorithm: str
    :param min_cluster_size: int
    :param max_distance: float
    :return clust_img: numpy.ndarray
    :return clust_masks: list
    """

    # Increment device counter
    params.device += 1

    # Uppercase algorithm name
    al_upper = algorithm.upper()

    # Dictionary of default values per algorithm
    default_max_dist = {"DBSCAN": 0.2, "OPTICS": np.inf}

    # If the algorithm is not in the default_max_dist dictionary raise a NameError
    if al_upper not in default_max_dist:
        raise NameError("Please use only 'OPTICS' or 'DBSCAN' ")

    # If max_distance is not set, apply the default value
    if max_distance is None:
        max_distance = default_max_dist.get(al_upper)

    # Get all x, y coordinates of white pixels in the mask
    x, y = np.where(mask == 255)
    zipped = np.column_stack((x, y))

    if "OPTICS" in al_upper:
        scaled = StandardScaler(with_mean=False, with_std=False).fit_transform(zipped)
        db = OPTICS(max_eps=max_distance, min_samples=min_cluster_size, n_jobs=-1).fit(scaled)
    elif "DBSCAN" in al_upper:
        scaled = StandardScaler().fit_transform(zipped)
        db = DBSCAN(eps=max_distance, min_samples=min_cluster_size, n_jobs=-1).fit(scaled)

    # Number of clusters
    n_clusters = len(set(db.labels_)) - (1 if -1 in db.labels_ else 0)
    # Create a color palette of n_clusters colors
    colors = color_palette(n_clusters + 1)
    # Initialize variables
    dict_of_colors = {}
    clust_masks = []
    h, w = mask.shape
    # Colorized clusters image
    clust_img = np.zeros((h, w, 3), np.uint8)
    # Index the label color for each cluster
    for y in range(0, n_clusters):
        dict_of_colors[str(y)] = colors[y]
        clust_masks.append(np.zeros((h, w), np.uint8))

    # Group -1 are points not assigned to a cluster
    dict_of_colors["-1"] = (255, 255, 255)

    # Loop over labels/clusters
    for z in range(0, len(db.labels_)):
        if not db.labels_[z] == -1:
            # Create a binary mask for each cluster
            clust_masks[db.labels_[z]][zipped[z][0], zipped[z][1]] = 255

        # Add a cluster with a unique label color to the cluster image
        clust_img[zipped[z][0], zipped[z][1]] = (dict_of_colors[str(db.labels_[z])][2],
                                                 dict_of_colors[str(db.labels_[z])][1],
                                                 dict_of_colors[str(db.labels_[z])][0])

    if params.debug == 'print':
        print_image(clust_img, os.path.join(params.debug_outdir, f"{params.device}_{al_upper}_clusters.png"))

    elif params.debug == 'plot':
        plot_image(clust_img)

    return clust_img, clust_masks
Example #25
0
def segment_tangent_angle(segmented_img, objects, size):
    """ Find 'tangent' angles in degrees of skeleton segments. Use `size` pixels on either end of
        each segment to find a linear regression line, and calculate angle between the two lines
        drawn per segment.

        Inputs:
        segmented_img  = Segmented image to plot slope lines and intersection angles on
        objects   = List of contours
        size      = Size of ends used to calculate "tangent" lines

        Returns:
        labeled_img    = Segmented debugging image with angles labeled

        :param segmented_img: numpy.ndarray
        :param objects: list
        :param size: int
        :return labeled_img: numpy.ndarray
        """
    # Store debug
    debug = params.debug
    params.debug = None

    labeled_img = segmented_img.copy()
    intersection_angles = []
    label_coord_x = []
    label_coord_y = []

    rand_color = color_palette(len(objects))

    for i, cnt in enumerate(objects):
        find_tangents = np.zeros(segmented_img.shape[:2], np.uint8)
        cv2.drawContours(find_tangents, objects, i, 255, 1, lineType=8)
        cv2.drawContours(labeled_img, objects, i, rand_color[i], params.line_thickness, lineType=8)
        pruned_segment = _iterative_prune(find_tangents, size)
        segment_ends = find_tangents - pruned_segment
        segment_end_obj, segment_end_hierarchy = find_objects(segment_ends, segment_ends)
        slopes = []
        for j, obj in enumerate(segment_end_obj):
            # Find bounds for regression lines to get drawn
            rect = cv2.minAreaRect(cnt)
            pts = cv2.boxPoints(rect)
            df = pd.DataFrame(pts, columns=('x', 'y'))
            x_max = int(df['x'].max())
            x_min = int(df['x'].min())

            # Find line fit to each segment
            [vx, vy, x, y] = cv2.fitLine(obj, cv2.DIST_L2, 0, 0.01, 0.01)
            slope = -vy / vx
            left_list = int(((x - x_min) * slope) + y)
            right_list = int(((x - x_max) * slope) + y)
            slopes.append(slope)

            if slope > 1000000 or slope < -1000000:
                print("Slope of contour with ID#", i, "is", slope, "and cannot be plotted.")
            else:
                # Draw slope lines
                cv2.line(labeled_img, (x_max - 1, right_list), (x_min, left_list), rand_color[i], 1)

        if len(slopes) < 2:
            # If size*2>len(obj) then pruning will remove the segment completely, and
            # makes segment_end_objs contain just one contour.
            print("Size too large, contour with ID#", i, "got pruned away completely.")
            intersection_angles.append("NA")
        else:
            # Calculate intersection angles
            slope1 = slopes[0][0]
            slope2 = slopes[1][0]
            intersection_angle = _slope_to_intesect_angle(slope1, slope2)
            intersection_angles.append(intersection_angle)

        # Store coordinates for labels
        label_coord_x.append(objects[i][0][0][0])
        label_coord_y.append(objects[i][0][0][1])

    segment_ids = []
    for i, cnt in enumerate(objects):
        # Label slope lines
        w = label_coord_x[i]
        h = label_coord_y[i]
        if type(intersection_angles[i]) is str:
            text = "{}".format(intersection_angles[i])
        else:
            text = "{:.2f}".format(intersection_angles[i])
        cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=params.text_size, color=(150, 150, 150), thickness=params.text_thickness)
        segment_label = "ID" + str(i)
        segment_ids.append(i)

    outputs.add_observation(variable='segment_tangent_angle', trait='segment tangent angle',
                            method='plantcv.plantcv.morphology.segment_tangent_angle', scale='degrees', datatype=list,
                            value=intersection_angles, label=segment_ids)

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segment_tangent_angles.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return labeled_img
Example #26
0
def segment_curvature(segmented_img, objects, label="default"):
    """ Calculate segment curvature as defined by the ratio between geodesic and euclidean distance.
        Measurement of two-dimensional tortuosity.

        Inputs:
        segmented_img     = Segmented image to plot lengths on
        objects           = List of contours
        label          = optional label parameter, modifies the variable name of observations recorded

        Returns:
        labeled_img        = Segmented debugging image with curvature labeled


        :param segmented_img: numpy.ndarray
        :param objects: list
        :param label: str
        :return labeled_img: numpy.ndarray

        """

    label_coord_x = []
    label_coord_y = []
    labeled_img = segmented_img.copy()

    # Store debug
    debug = params.debug
    params.debug = None

    _ = segment_euclidean_length(segmented_img, objects, label="backend")
    _ = segment_path_length(segmented_img, objects, label="backend")
    eu_lengths = outputs.observations['backend']['segment_eu_length']['value']
    path_lengths = outputs.observations['backend']['segment_path_length'][
        'value']
    curvature_measure = [
        float(x / y) for x, y in zip(path_lengths, eu_lengths)
    ]
    # Create a color scale, use a previously stored scale if available
    rand_color = color_palette(num=len(objects), saved=True)

    for i, cnt in enumerate(objects):
        # Store coordinates for labels
        label_coord_x.append(objects[i][0][0][0])
        label_coord_y.append(objects[i][0][0][1])

        # Draw segments one by one to group segment tips together
        finding_tips_img = np.zeros(segmented_img.shape[:2], np.uint8)
        cv2.drawContours(finding_tips_img,
                         objects,
                         i, (255, 255, 255),
                         1,
                         lineType=8)
        segment_tips = find_tips(finding_tips_img)
        tip_objects, tip_hierarchies = find_objects(segment_tips, segment_tips)
        points = []

        for t in tip_objects:
            # Gather pairs of coordinates
            x, y = t.ravel()
            coord = (x, y)
            points.append(coord)

        # Draw euclidean distance lines
        cv2.line(labeled_img, points[0], points[1], rand_color[i], 1)

    segment_ids = []
    # Reset debug mode
    params.debug = debug

    for i, cnt in enumerate(objects):
        # Calculate geodesic distance
        text = "{:.3f}".format(curvature_measure[i])
        w = label_coord_x[i]
        h = label_coord_y[i]
        cv2.putText(img=labeled_img,
                    text=text,
                    org=(w, h),
                    fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=params.text_size,
                    color=(150, 150, 150),
                    thickness=params.text_thickness)
        # segment_label = "ID" + str(i)
        segment_ids.append(i)

    outputs.add_observation(
        sample=label,
        variable='segment_curvature',
        trait='segment curvature',
        method='plantcv.plantcv.morphology.segment_curvature',
        scale='none',
        datatype=list,
        value=curvature_measure,
        label=segment_ids)

    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(
            labeled_img,
            os.path.join(params.debug_outdir,
                         str(params.device) + '_segment_curvature.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return labeled_img
Example #27
0
def cluster_contours(device, img, roi_objects, nrow=1, ncol=1, debug=None):
    """
    This function take a image with multiple contours and clusters them based on user input of rows and columns

    Inputs:
    img                     = An RGB image array
    roi_objects             = object contours in an image that are needed to be clustered.
    nrow                    = number of rows to cluster (this should be the approximate  number of desired rows
                              in the entire image (even if there isn't a literal row of plants)
    ncol                    = number of columns to cluster (this should be the approximate number of desired columns
                              in the entire image (even if there isn't a literal row of plants)
    file                    = output of filename from read_image function
    filenames               = input txt file with list of filenames in order from top to bottom left to right
    debug                   = print debugging images

    Returns:
    device                  = pipeline step counter
    grouped_contour_indexes = contours grouped
    contours                = All inputed contours

    :param device: int
    :param img: ndarray
    :param roi_objects: list
    :param nrow: int
    :param ncol: int
    :param debug: str
    :return device: int
    :return grouped_contour_indexes: list
    :return contours: list
    """

    device += 1

    if len(np.shape(img)) == 3:
        iy, ix, iz = np.shape(img)
    else:
        iy, ix, = np.shape(img)

    # get the break groups

    if nrow == 1:
        rbreaks = [0, iy]
    else:
        rstep = np.rint(iy / nrow)
        rstep1 = np.int(rstep)
        rbreaks = range(0, iy, rstep1)
    if ncol == 1:
        cbreaks = [0, ix]
    else:
        cstep = np.rint(ix / ncol)
        cstep1 = np.int(cstep)
        cbreaks = range(0, ix, cstep1)

    # categorize what bin the center of mass of each contour

    def digitize(a, step):
        if isinstance(step, int) == True:
            i = step
        else:
            i = len(step)
        for x in range(0, i):
            if x == 0:
                if a >= 0 and a < step[x + 1]:
                    return x + 1
            elif a >= step[x - 1] and a < step[x]:
                return x
            elif a > step[x - 1] and a > np.max(step):
                return i

    dtype = [('cx', int), ('cy', int), ('rowbin', int), ('colbin', int),
             ('index', int)]
    coord = []
    for i in range(0, len(roi_objects)):
        m = cv2.moments(roi_objects[i])
        if m['m00'] == 0:
            pass
        else:
            cx = int(m['m10'] / m['m00'])
            cy = int(m['m01'] / m['m00'])
            # colbin = np.digitize(cx, cbreaks)
            # rowbin = np.digitize(cy, rbreaks)
            colbin = digitize(cx, cbreaks)
            rowbin = digitize(cy, rbreaks)
            a = (cx, cy, colbin, rowbin, i)
            coord.append(a)
    coord1 = np.array(coord, dtype=dtype)
    coord2 = np.sort(coord1, order=('colbin', 'rowbin'))

    # get the list of unique coordinates and group the contours with the same bin coordinates

    groups = []
    for i, y in enumerate(coord2):
        col = y[3]
        row = y[2]
        location = str(row) + ',' + str(col)
        groups.append(location)

    unigroup = np.unique(groups)
    coordgroups = []

    for i, y in enumerate(unigroup):
        col = int(y[0])
        row = int(y[2])
        for a, b in enumerate(coord2):
            if b[2] == col and b[3] == row:
                grp = i
                contour = b[4]
                coordgroups.append((grp, contour))
            else:
                pass

    coordlist = [[y[1] for y in coordgroups if y[0] == x]
                 for x in range(0, (len(unigroup)))]

    contours = roi_objects
    grouped_contour_indexes = coordlist

    # Debug image is rainbow printed contours

    if debug == 'print':
        if len(np.shape(img)) == 3:
            img_copy = np.copy(img)
        else:
            iy, ix = np.shape(img)
            img_copy = np.zeros((iy, ix, 3), dtype=np.uint8)

        rand_color = color_palette(len(coordlist))
        for i, x in enumerate(coordlist):
            for a in x:
                cv2.drawContours(img_copy,
                                 roi_objects,
                                 a,
                                 rand_color[i],
                                 -1,
                                 lineType=8)
        print_image(img_copy, (str(device) + '_clusters.png'))

    elif debug == 'plot':
        if len(np.shape(img)) == 3:
            img_copy = np.copy(img)
        else:
            iy, ix = np.shape(img)
            img_copy = np.zeros((iy, ix, 3), dtype=np.uint8)

        rand_color = color_palette(len(coordlist))
        for i, x in enumerate(coordlist):
            for a in x:
                cv2.drawContours(img_copy,
                                 roi_objects,
                                 a,
                                 rand_color[i],
                                 -1,
                                 lineType=8)
        plot_image(img_copy)

    return device, grouped_contour_indexes, contours
Example #28
0
def segment_angle(segmented_img, objects):
    """ Calculate angle of segments (in degrees) by fitting a linear regression line to segments.

        Inputs:
        segmented_img  = Segmented image to plot slope lines and angles on
        objects        = List of contours

        Returns:
        labeled_img    = Segmented debugging image with angles labeled


        :param segmented_img: numpy.ndarray
        :param objects: list
        :return labeled_img: numpy.ndarray
        """

    label_coord_x = []
    label_coord_y = []
    segment_angles = []

    labeled_img = segmented_img.copy()

    rand_color = color_palette(len(objects))

    for i, cnt in enumerate(objects):
        # Find bounds for regression lines to get drawn
        rect = cv2.minAreaRect(cnt)
        pts = cv2.boxPoints(rect)
        df = pd.DataFrame(pts, columns=('x', 'y'))
        x_max = int(df['x'].max())
        x_min = int(df['x'].min())

        # Find line fit to each segment
        [vx, vy, x, y] = cv2.fitLine(objects[i], cv2.DIST_L2, 0, 0.01, 0.01)
        slope = -vy / vx
        left_list = int(((x - x_min) * slope) + y)
        right_list = int(((x - x_max) * slope) + y)

        if slope > 1000000 or slope < -1000000:
            print("Slope of contour with ID#", i, "is", slope, "and cannot be plotted.")
        else:
            # Draw slope lines
            cv2.line(labeled_img, (x_max - 1, right_list), (x_min, left_list), rand_color[i], 1)

        # Store coordinates for labels
        label_coord_x.append(objects[i][0][0][0])
        label_coord_y.append(objects[i][0][0][1])

        # Calculate degrees from slopes
        segment_angles.append(np.arctan(slope[0]) * 180 / np.pi)

    segment_ids = []
    for i, cnt in enumerate(objects):
        # Label slope lines
        w = label_coord_x[i]
        h = label_coord_y[i]
        text = "{:.2f}".format(segment_angles[i])
        cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=params.text_size, color=(150, 150, 150), thickness=params.text_thickness)
        # segment_label = "ID" + str(i)
        segment_ids.append(i)

    outputs.add_observation(variable='segment_angle', trait='segment angle',
                            method='plantcv.plantcv.morphology.segment_angle', scale='degrees', datatype=list,
                            value=segment_angles, label=segment_ids)

    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segmented_angles.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return labeled_img
Example #29
0
def segment_curvature(segmented_img, objects, hierarchies):
    """ Calculate segment curvature as defined by the ratio between geodesic and euclidean distance.
        Measurement of two-dimensional tortuosity.

        Inputs:
        segmented_img     = Segmented image to plot lengths on
        objects           = List of contours
        hierarchy         = Contour hierarchy NumPy array

        Returns:
        curvature_header   = Segment curvature data header
        curvature_data     = Segment curvature data values
        labeled_img        = Segmented debugging image with curvature labeled


        :param segmented_img: numpy.ndarray
        :param objects: list
        :param hierarchy: numpy.ndarray
        :return labeled_img: numpy.ndarray
        :return curvature_header: list
        :return curvature_data: list

        """
    # Store debug
    debug = params.debug
    params.debug = None

    label_coord_x = []
    label_coord_y = []

    _, eu_lengths, _ = segment_euclidean_length(segmented_img, objects, hierarchies)
    _, path_lengths, labeled_img = segment_path_length(segmented_img, objects)
    del eu_lengths[0]
    del path_lengths[0]
    curvature_measure = [x/y for x, y in zip(path_lengths, eu_lengths)]
    rand_color = color_palette(len(objects))

    for i, cnt in enumerate(objects):
        # Store coordinates for labels
        label_coord_x.append(objects[i][0][0][0])
        label_coord_y.append(objects[i][0][0][1])

        # Draw segments one by one to group segment tips together
        finding_tips_img = np.zeros(segmented_img.shape[:2], np.uint8)
        cv2.drawContours(finding_tips_img, objects, i, (255, 255, 255), 1, lineType=8,
                         hierarchy=hierarchies)
        segment_tips = find_tips(finding_tips_img)
        tip_objects, tip_hierarchies = find_objects(segment_tips, segment_tips)
        points = []

        for t in tip_objects:
            # Gather pairs of coordinates
            x, y = t.ravel()
            coord = (x, y)
            points.append(coord)

        # Draw euclidean distance lines
        cv2.line(labeled_img, points[0], points[1], rand_color[i], 1)

    curvature_header = ['HEADER_CURVATURE']
    curvature_data = ['CURVATURE_DATA']
    for i, cnt in enumerate(objects):
        # Calculate geodesic distance
        text = "{:.3f}".format(curvature_measure[i])
        w = label_coord_x[i]
        h = label_coord_y[i]
        cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=.4,
                    color=(150, 150, 150), thickness=1)
        segment_label = "ID" + str(i)
        curvature_header.append(segment_label)
        curvature_data.append(curvature_measure[i])

    outputs.measurements['morphology_data']['segment_curvature'] = curvature_measure

    # Reset debug mode
    params.debug = debug
    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segment_curvature.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return curvature_header, curvature_data, labeled_img
Example #30
0
def segment_skeleton(skel_img, mask=None):
    """ Segment a skeleton image into pieces

        Inputs:
        skel_img         = Skeletonized image
        mask             = (Optional) binary mask for debugging. If provided, debug image will be overlaid on the mask.

        Returns:
        segmented_img       = Segmented debugging image
        segment_objects     = list of contours

        :param skel_img: numpy.ndarray
        :param mask: numpy.ndarray
        :return segmented_img: numpy.ndarray
        :return segment_objects: list
        """

    # Store debug
    debug = params.debug
    params.debug = None

    # Find branch points
    bp = find_branch_pts(skel_img)
    bp = dilate(bp, 3, 1)

    # Subtract from the skeleton so that leaves are no longer connected
    segments = image_subtract(skel_img, bp)

    # Gather contours of leaves
    segment_objects, _ = find_objects(segments, segments)

    # Reset debug mode
    params.debug = debug

    # Color each segment a different color, do not used a previously saved color scale
    rand_color = color_palette(num=len(segment_objects), saved=False)

    if mask is None:
        segmented_img = skel_img.copy()
    else:
        segmented_img = mask.copy()

    segmented_img = cv2.cvtColor(segmented_img, cv2.COLOR_GRAY2RGB)
    for i, cnt in enumerate(segment_objects):
        cv2.drawContours(segmented_img,
                         segment_objects,
                         i,
                         rand_color[i],
                         params.line_thickness,
                         lineType=8)

    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(
            segmented_img,
            os.path.join(params.debug_outdir,
                         str(params.device) + '_segmented.png'))
    elif params.debug == 'plot':
        plot_image(segmented_img)

    return segmented_img, segment_objects
Example #31
0
def segment_angle(segmented_img, objects):
    """ Calculate angle of segments (in degrees) by fitting a linear regression line to segments.

        Inputs:
        segmented_img  = Segmented image to plot slope lines and angles on
        objects        = List of contours

        Returns:
        angle_header   = Segment angle data header
        angle_data     = Segment angle data values
        labeled_img    = Segmented debugging image with angles labeled


        :param segmented_img: numpy.ndarray
        :param objects: list
        :return labeled_img: numpy.ndarray
        :return angle_header: list
        :return angle_data: list
        """

    label_coord_x = []
    label_coord_y = []
    segment_angles = []

    labeled_img = segmented_img.copy()

    rand_color = color_palette(len(objects))

    for i, cnt in enumerate(objects):
        # Find bounds for regression lines to get drawn
        rect = cv2.minAreaRect(cnt)
        pts = cv2.boxPoints(rect)
        df = pd.DataFrame(pts, columns=('x', 'y'))
        x_max = int(df['x'].max())
        x_min = int(df['x'].min())

        # Find line fit to each segment
        [vx, vy, x, y] = cv2.fitLine(objects[i], cv2.DIST_L2, 0, 0.01, 0.01)
        slope = -vy / vx
        left_list = int(((x - x_min) * slope) + y)
        right_list = int(((x - x_max) * slope) + y)

        if slope > 1000000 or slope < -1000000:
            print("Slope of contour with ID#", i, "is", slope,
                  "and cannot be plotted.")
        else:
            # Draw slope lines
            cv2.line(labeled_img, (x_max - 1, right_list), (x_min, left_list),
                     rand_color[i], 1)

        # Store coordinates for labels
        label_coord_x.append(objects[i][0][0][0])
        label_coord_y.append(objects[i][0][0][1])

        # Calculate degrees from slopes
        segment_angles.append(np.arctan(slope[0]) * 180 / np.pi)

    angle_header = ['HEADER_ANGLE']
    angle_data = ['ANGLE_DATA']
    for i, cnt in enumerate(objects):
        # Label slope lines
        w = label_coord_x[i]
        h = label_coord_y[i]
        text = "{:.2f}".format(segment_angles[i])
        cv2.putText(img=labeled_img,
                    text=text,
                    org=(w, h),
                    fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=.55,
                    color=(150, 150, 150),
                    thickness=2)
        segment_label = "ID" + str(i)
        angle_header.append(segment_label)
    angle_data.extend(segment_angles)

    if 'morphology_data' not in outputs.measurements:
        outputs.measurements['morphology_data'] = {}
    outputs.measurements['morphology_data']['segment_angles'] = segment_angles

    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(
            labeled_img,
            os.path.join(params.debug_outdir,
                         str(params.device) + '_segmented_angles.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return angle_header, angle_data, labeled_img
Example #32
0
def segment_euclidean_length(segmented_img, objects, label="default"):
    """ Use segmented skeleton image to gather euclidean length measurements per segment

        Inputs:
        segmented_img = Segmented image to plot lengths on
        objects       = List of contours
        label         = optional label parameter, modifies the variable name of observations recorded

        Returns:
        labeled_img      = Segmented debugging image with lengths labeled

        :param segmented_img: numpy.ndarray
        :param objects: list
        :param label: str
        :return labeled_img: numpy.ndarray

        """
    x_list = []
    y_list = []
    segment_lengths = []
    # Create a color scale, use a previously stored scale if available
    rand_color = color_palette(num=len(objects), saved=True)

    labeled_img = segmented_img.copy()
    # Store debug
    debug = params.debug
    params.debug = None

    for i, cnt in enumerate(objects):
        # Store coordinates for labels
        x_list.append(objects[i][0][0][0])
        y_list.append(objects[i][0][0][1])

        # Draw segments one by one to group segment tips together
        finding_tips_img = np.zeros(segmented_img.shape[:2], np.uint8)
        cv2.drawContours(finding_tips_img,
                         objects,
                         i, (255, 255, 255),
                         1,
                         lineType=8)
        segment_tips = find_tips(finding_tips_img)
        tip_objects, tip_hierarchies = find_objects(segment_tips, segment_tips)
        points = []
        if not len(tip_objects) == 2:
            fatal_error("Too many tips found per segment, try pruning again")
        for t in tip_objects:
            # Gather pairs of coordinates
            x, y = t.ravel()
            coord = (x, y)
            points.append(coord)

        # Draw euclidean distance lines
        cv2.line(labeled_img, points[0], points[1], rand_color[i], 1)

        # Calculate euclidean distance between tips of each contour
        segment_lengths.append(float(euclidean(points[0], points[1])))

    segment_ids = []
    # Reset debug mode
    params.debug = debug

    # Put labels of length
    for c, value in enumerate(segment_lengths):
        text = "{:.2f}".format(value)
        w = x_list[c]
        h = y_list[c]
        cv2.putText(img=labeled_img,
                    text=text,
                    org=(w, h),
                    fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=params.text_size,
                    color=(150, 150, 150),
                    thickness=params.text_thickness)
        # segment_label = "ID" + str(c)
        segment_ids.append(c)

    outputs.add_observation(
        sample=label,
        variable='segment_eu_length',
        trait='segment euclidean length',
        method='plantcv.plantcv.morphology.segment_euclidean_length',
        scale='pixels',
        datatype=list,
        value=segment_lengths,
        label=segment_ids)

    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(
            labeled_img,
            os.path.join(params.debug_outdir,
                         str(params.device) + '_segment_eu_lengths.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return labeled_img
Example #33
0
def segment_angle(segmented_img, objects):
    """ Calculate angle of segments (in degrees) by fitting a linear regression line to segments.

        Inputs:
        segmented_img  = Segmented image to plot slope lines and angles on
        objects        = List of contours

        Returns:
        labeled_img    = Segmented debugging image with angles labeled


        :param segmented_img: numpy.ndarray
        :param objects: list
        :return labeled_img: numpy.ndarray
        """

    label_coord_x = []
    label_coord_y = []
    segment_angles = []

    labeled_img = segmented_img.copy()

    rand_color = color_palette(len(objects))

    for i, cnt in enumerate(objects):
        # Find bounds for regression lines to get drawn
        rect = cv2.minAreaRect(cnt)
        pts = cv2.boxPoints(rect)
        df = pd.DataFrame(pts, columns=('x', 'y'))
        x_max = int(df['x'].max())
        x_min = int(df['x'].min())

        # Find line fit to each segment
        [vx, vy, x, y] = cv2.fitLine(objects[i], cv2.DIST_L2, 0, 0.01, 0.01)
        slope = -vy / vx
        left_list = int(((x - x_min) * slope) + y)
        right_list = int(((x - x_max) * slope) + y)

        if slope > 1000000 or slope < -1000000:
            print("Slope of contour with ID#", i, "is", slope, "and cannot be plotted.")
        else:
            # Draw slope lines
            cv2.line(labeled_img, (x_max - 1, right_list), (x_min, left_list), rand_color[i], 1)

        # Store coordinates for labels
        label_coord_x.append(objects[i][0][0][0])
        label_coord_y.append(objects[i][0][0][1])

        # Calculate degrees from slopes
        segment_angles.append(np.arctan(slope[0]) * 180 / np.pi)

    segment_ids = []
    for i, cnt in enumerate(objects):
        # Label slope lines
        w = label_coord_x[i]
        h = label_coord_y[i]
        text = "{:.2f}".format(segment_angles[i])
        cv2.putText(img=labeled_img, text=text, org=(w, h), fontFace=cv2.FONT_HERSHEY_SIMPLEX,
                    fontScale=params.text_size, color=(150, 150, 150), thickness=params.text_thickness)
        segment_label = "ID" + str(i)
        segment_ids.append(i)

    outputs.add_observation(variable='segment_angle', trait='segment angle',
                            method='plantcv.plantcv.morphology.segment_angle', scale='degrees', datatype=list,
                            value=segment_angles, label=segment_ids)

    # Auto-increment device
    params.device += 1

    if params.debug == 'print':
        print_image(labeled_img, os.path.join(params.debug_outdir, str(params.device) + '_segmented_angles.png'))
    elif params.debug == 'plot':
        plot_image(labeled_img)

    return labeled_img