This is a full workflow that shows methods for analyzing shape, color, and morphological features of plants imaged from a side-view. Similar methods should work for other tyles of plants that are imaged in a similar way.
import os
# Detect notebook environment
def detect_notebook_environment():
if 'COLAB_GPU' in os.environ:
print('Google Colaboratory detected.')
%pip install "altair>=5" ipympl plantcv
# Give access and mount your Google Drive (need a Google Account)
from google.colab import drive
drive.mount('/content/gdrive')
# Enable widget feature with matplotlib
from google.colab import output
output.enable_custom_widget_manager()
#View working directory, may need to change
%pwd
else:
print('Binder detected.')
environment = detect_notebook_environment()
# Set the notebook display method
%matplotlib widget
# Import libraries
from plantcv import plantcv as pcv
from plantcv.parallel import WorkflowInputs
The options class mimics the workflow command-line argument parser that is used for workflow parallelization. Using it while developing a workflow in Jupyter makes it easier to convert the workflow to a script later.
# Input/output options
args = WorkflowInputs(
images=["img/side_view_sorghum.png"],
names="image1",
result="side_view_morphology_analysis.json",
outdir=".",
writeimg=True,
debug="plot"
)
# Set debug to the global parameter
pcv.params.debug = args.debug
# Set plotting size (default = 100)
pcv.params.dpi = 100
# Increase text size and thickness to make labels clearer
# (size may need to be altered based on original image size)
pcv.params.text_size = 20
pcv.params.text_thickness = 20
pcv.params.line_thickness = 10
# Inputs:
# filename - Image file to be read in
# mode - How to read in the image; either 'native' (default), 'rgb', 'gray', or 'csv'
img, path, filename = pcv.readimage(filename=args.image1)
The visualization tool converts the color image into HSV and LAB colorspaces and displays the grayscale channels in a matrix so that they can be visualized simultaneously. The idea is to select a channel that maximizes the difference between the plant and the background pixels.
colorspaces = pcv.visualize.colorspaces(rgb_img=img, original_img=False)
Converts the input color image into the LAB colorspace and returns the B (blue-yellow) channel as a grayscale image.
b = pcv.rgb2gray_lab(rgb_img=img, channel="b")
A histogram can be used to visualize the distribution of values in an image. The histogram can aid in the selection of a threshold value.
For this image, the large peak between 120-130 are from the darker background pixels. The smaller peaks that represent plant material cannot be distinguished because there are so few total plant pixels, but we can use the upper bound of the background peaks to estimate a good thresholding point, somewhere between 127-135.
hist = pcv.visualize.histogram(img=b)
Use a threshold function (binary in this case) to segment the grayscale image into plant (white) and background (black) pixels. Using the histogram above, a threshold point between 120-125 will segment the plant and background peaks. Because the plant has darker pixels in this image, use object_type="dark" to do a traditional threshold.
b_thresh = pcv.threshold.binary(gray_img=b, threshold=134, object_type='light')
To eventually combine all of the objects into a singular object that identifies the plant, we need to identify a region of interest (ROI) which will either fully encapsulate or overlap with plant material. This way, if objects are identified due to "salt" noise or other background elements, they will be filtered out. In this case, a rectangular ROI that partially overlaps with the plant object can be used to filter out some of the excess noise around the plant.
roi1 = pcv.roi.rectangle(img=b_thresh, x=1000, y=1250, h=200, w=500)
Any objects that do not overlap with the ROI will be filtered out, leaving only objects that identify plant material.
kept_mask = pcv.roi.filter(mask=b_thresh, roi=roi1, roi_type='partial')
This is entirely optional, but it can help to look closer at the mask to ensure there is no "salt" noise or any other issues with the binary mask that aren't clear. To visualize the cropped mask, use plot_image.
# Chose to directly crop the images using base Python, but could also use pcv.crop()
cropped_mask = kept_mask[1100:1800, 700:1600]
cropped_img = img[1100:1800, 700:1600]
pcv.plot_image(cropped_mask)
Especially in images with thin leaves, there is a greater risk that when removing 'salt' background noise, some of the plant pixels will be degraded as well. This can be avoided by dilating the image, which increases white pixel area i times.
mask_dilated = pcv.dilate(gray_img=cropped_mask, ksize=3, i=1)
Thresholding mostly labeled plant pixels white but also labeled small regions of the background white. The fill function removes "salt" noise from the background by filtering white regions by size. After dilating, we can see a few specks of noise that would otherwise interfere with the morphology analysis functions.
mask_fill = pcv.fill(bin_img=mask_dilated, size=30)
mask_fill = pcv.fill_holes(bin_img=mask_fill)
Convert the mask into a 1-pixel wide skeleton, which can be used for mophology analyses.
skeleton = pcv.morphology.skeletonize(mask=mask_fill)
Generall, skeletonized images will have barbs that represent the width of plant material, which then need to be pruned off.
pruned_skel, seg_img, edge_objects = pcv.morphology.prune(skel_img=skeleton, size=50, mask=mask_fill)
The top left leaf still has a barb, so it's getting split into two segments. Let's prune again to remove that stubborn barb.
pruned_skel, seg_img, edge_objects = pcv.morphology.prune(skel_img=pruned_skel, size=50, mask=mask_fill)
Differentiate between the primary segment (stem) and secondart segments (leaves). Downstream steps can be performed on just one class of objects at a time, or all objects.
leaf_obj, stem_obj= pcv.morphology.segment_sort(skel_img=pruned_skel, objects=edge_objects, mask=mask_fill)
Fill in different segments (branches/leaves) of the plant in the binary mask, and store out area data for each segment.
filled_img = pcv.morphology.fill_segments(mask=mask_fill, objects=leaf_obj, label="default")
Find branch/junction points from a skeleton image.
branch_pts_mask = pcv.morphology.find_branch_pts(skel_img=pruned_skel, mask=cropped_mask, label="default")
/srv/conda/envs/notebook/lib/python3.10/site-packages/plantcv/plantcv/plot_image.py:29: RuntimeWarning: More than 20 figures have been opened. Figures created through the pyplot interface (`matplotlib.pyplot.figure`) are retained until explicitly closed and may consume too much memory. (To control this warning, see the rcParam `figure.max_open_warning`).
Find the tip points for branches.
tip_pts_mask = pcv.morphology.find_tips(skel_img=pruned_skel, mask=None, label="default")
# Downsize parameters to make annotations more readable
pcv.params.text_size = 2
pcv.params.text_thickness = 3
pcv.params.line_thickness = 10
Identify and label unique segments from skeletonized image.
segmented_img, labeled_img = pcv.morphology.segment_id(skel_img=pruned_skel,
objects=leaf_obj,
mask=cropped_mask)
Measure and plot the path length of each segment.
labeled_img = pcv.morphology.segment_path_length(segmented_img=segmented_img,
objects=leaf_obj, label="default")
labeled_img = pcv.morphology.segment_euclidean_length(segmented_img=segmented_img,
objects=leaf_obj, label="default")
Measure segment curvature using the ratio of geodesic distance to euclidean distance. Larger values indicate greater curvature.
labeled_img = pcv.morphology.segment_curvature(segmented_img=segmented_img,
objects=leaf_obj, label="default")
Calculate segment angle using a linear regression line fit to each segment, and finding the angle in degrees for each.
labeled_img = pcv.morphology.segment_angle(segmented_img=segmented_img,
objects=leaf_obj, label="default")
Find tangent angles of skeleton segments and calculate angle between the two angles for each segment.
labeled_img = pcv.morphology.segment_tangent_angle(segmented_img=segmented_img,
objects=leaf_obj, size=15, label="default")
Calculate angle of leaf insertion in segrees compared to stem angle.
labeled_img = pcv.morphology.segment_insertion_angle(skel_img=pruned_skel,
segmented_img=segmented_img,
leaf_objects=leaf_obj,
stem_objects=stem_obj,
size=20, label="default")
shape_img = pcv.analyze.size(img=cropped_img, labeled_mask=mask_fill, label="default")
The save results function will take the measurements stored when running any PlantCV analysis functions, format, and print an output text file for data analysis. The Outputs class in this example will store: 'segment_area', 'tips', 'branch_pts', 'segment_angle', 'segment_curvature', 'segment_eu_length', 'segment_insertion_angle', 'segment_path_length', and 'segment_tangent_angle' from the morphology functions.
Here, results are saved to a CSV file for easy viewing, but when running workflows in parallel, save results as "json"
# Inputs:
# filename = filename for saving results
# outformat = output file format: "json" (default) hierarchical format or "csv" tabular format
pcv.outputs.save_results(filename=args.result)