Skip to content

About

Deep learning pipeline for plant phenotyping images: YOLOv11 leaf segmentation, then leaf trait classification.

Topics

Resources

Stars

1 star

Watchers

2 watching

Forks

Latest commit

 

History

89 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PhenoSelect

A modular deep learning pipeline for automated high-throughput leaf segmentation and trait classification.

High-throughput plant phenotyping platforms take thousands of images, and outlining every leaf by hand does not scale. PhenoSelect finds each single leaf in a canopy image with a YOLOv11 instance segmentation model and cuts it out. A second model then sorts the cutouts by traits you define yourself, for example visibility or health. Every leaf ends up as one row in a CSV file, ready for further analysis such as leaf area or leaf-level NDVI.

The full story, with figures and results, is in the blog post: PhenoSelect: Training a Neural Network Because I Refuse to Click on 100,000 Leaves.

How it works

The pipeline has two stages.

  1. Segmentation. You outline leaves on a set of training images in LabelMe. PhenoSelect converts the labels to YOLO format, splits them into train, validation and test sets, and trains a YOLOv11 segmentation model. The trained model then finds the leaves in new images and saves a cutout and a mask for each one.
  2. Classification. You label a share of the leaf cutouts with the labeling tool in toolbox/. A ResNet18 classifier with one output head per label group learns these labels and then labels every cutout.

A label such as "not a plant" is a handy safety net. It lets the classifier catch pot rims, label stakes or soil that the segmenter mistook for leaves, so you can drop them from the results.

What you need

  • Python 3.10.
  • These Python packages (there is no requirements file yet):
    • pipeline: ultralytics, torch, torchvision, opencv-python, tifffile, numpy, pillow, pandas, pyyaml, tqdm
    • analysis scripts: also matplotlib, seaborn, scipy, scikit-learn, scikit-posthocs, statsmodels
    • labeling tool: tkinter
  • LabelMe to draw the leaf polygons for training.
  • A CUDA GPU is used for training when one is available. Without one, the scripts fall back to the CPU.
  • Internet access the first time you train, to download the YOLO11 weights.

The pretrained PhenoSelect models are too large for GitHub. To get them, please contact me (see Contact).

Folder layout

Run every script from the repository root. All paths are relative to it, and every script appends its log to master_log.txt.

Folder Contents
train_image_dump/ Training images (.tiff) with their LabelMe .json files
train_data_split/ Train, val and test sets made by 12_split_data_set.py
inference_in/ Images you want to segment (.tiff)
inference_out/ Everything the pipeline produces (cutouts, masks, CSV files, plots)
runs/ Training runs and trained models (created on first training)
models/ YOLO11 base weights and the pretrained PhenoSelect models (not in git)
all_images_all_spec/ Raw multispectral images for the analysis scripts
toolbox/ Helper tools, including the labeling tool
analysis_scripts/ Trait extraction, statistics and plots

Quick start with 99_run_pipeline.py

99_run_pipeline.py runs the numbered scripts in the right order. It has one function per use case:

Function What it runs
default_pipeline_segmenter() 11, 12, 14, then 22 with --epochs 100 --batch-size 4, then 23
default_pipeline_classifier() 52, then 53
data_preparation_only() 11, 12 and 14
continue_training_segmenter() 22 with --epochs 100 --batch-size 4 --continue-training
inference_segmenter_pretrained_model_own_data() 23 with --use-sample-model --sample-model-size x
classifier_pretrained_model_own_data() 53 with --use-sample-model
inference_classifier_pretrained_model_own_data() 53 with --use-sample-model and the sample annotations.csv

default_pipeline_classifier() runs 53 with its default model folder, which is not where 52 saves the model. See step 8 for the fix.

As shipped, the __main__ block calls every function one after the other. Comment out the ones you don't need, then run:

python 99_run_pipeline.py

Step by step

The numbered pipeline scripts (11 to 53) take --help, which lists all of their arguments with their defaults.

1. Prepare the images

The pipeline reads .tiff files. Inference opens each image as a three-channel picture, so multispectral data has to be packed into three channels first. Two toolbox scripts do that:

  • toolbox/01_replace_B_by_NIR1.py pairs an RGB image (_Source0_ in the file name) with a NIR image (_Source1_) taken within 6 seconds of it. It writes a TIFF with the channels red, green and NIR, named after the RGB file. File names must start with a YYYYMMDD_HHMMSS timestamp. Set the input and output folders at the bottom of the script.
  • toolbox/05_open_hsi_image.py and toolbox/06_hsi_to_rgb.py turn a hyperspectral cube into a pseudo-RGB image (see Toolbox).

2. Label leaves for training

Outline the leaves in LabelMe and put the images and their .json files into train_image_dump/.

3. Convert, split and check the labels

python 11_convert_lableme_to_yolo.py
python 12_split_data_set.py
python 13_verify_yolo_labes.py
python "14_generate,update_21_model_config.py"
  • 11_convert_lableme_to_yolo.py writes one YOLO .txt label file per image and a class list, classes_(read_only).txt. It deletes old .txt label files in the output folder first. Use --show-groups to preview the classes without changing anything.
  • 12_split_data_set.py copies image and label pairs into train_data_split/ (80 % train, 10 % val, 10 % test by default). The split is done separately for each combination of classes, so every split gets its share of each class. Options: --train-ratio, --val-ratio, --test-ratio, --image-ext (default .tiff) and --no-clean.
  • 13_verify_yolo_labes.py is optional. It shows each image with its label polygons. Use a/d or the arrow keys to browse and Esc to quit.
  • 14_generate,update_21_model_config.py writes 21_model_config.yaml for YOLO. Its default --classes-file path uses a Windows backslash. On Linux or macOS, pass it yourself: --classes-file "train_image_dump/classes_(read_only).txt".

4. Train the segmenter

python 22_train_segmenter.py --epochs 100 --batch-size 4
  • --model-size picks the YOLO11 size: n, s, m, l or x (default x). The base weights are downloaded to models/architecture_model/ on first use.
  • The default for --epochs is 10000, so set it yourself.
  • --imgsz (default 1024) and --device (default cuda) set the image size and device.
  • --continue-training starts from the newest best.pt in runs/.
  • --use-pretrained-model starts from the pretrained PhenoSelect model in models/pretrained_model_data/segmentation/ instead of the plain YOLO11 weights.
  • The augmentation settings have their own flags (--hsv_h, --degrees, --mosaic, --mixup, --copy-paste and more).

Training results go to runs/segment/. Careful: without --continue-training or --use-pretrained-model, the script deletes the whole runs/ folder before it starts. Copy any models you want to keep first.

5. Segment new images

Put the images into inference_in/ and run:

python 23_inference_segmenter.py

It uses the newest best.pt in runs/, or the model given with --model-path. With the pretrained models, use --use-sample-model --sample-model-size x, and add --use-sample-images to run on the example images that come with them.

You can limit the analysis to one plot per image. Put a LabelMe .json file with the same name as the image into inference_in/plot_border/ and draw a polygon or rectangle labelled plot. --leaf-inclusion-mode then decides which leaves count: touching_or_inside (default) or fully_inside. Images without a border file are used in full.

Output in inference_out/ (cleaned first unless you pass --no-clean):

Path Contents
numeric/results.csv One row per leaf
frames/ Each input image with all kept leaves drawn on it
cutouts_masked/ Each leaf on a black background
cutouts_leaf_only/ Each leaf's bounding box, background kept
cutouts_with_outline/ Same, with the leaf outline drawn in red
binary_masks/ A black and white mask per leaf

results.csv has these columns: image_name, image_title (the TIFF description tag), original_width, original_height, instance_number, bbox_center_x, bbox_center_y, bbox_width, bbox_height, confidence, class_id, class and polygon_coordinates (pixel coordinates as x,y;x,y;...).

6. Label the leaf cutouts

python toolbox/02_classification_labeling.py

The labeling tool opens in its own window. Click the gear button to set it up:

  • Image folder (default inference_out/cutouts_masked) and output CSV (default annotations.csv in that folder).
  • Labeling mode. Categorical gives one label per group for each image and writes one column per group. This is the format the classifier reads. Multi-Label lets you give one image several labels.
  • Label groups, their labels and a key for each label.

Keys while labeling: your label keys, Space to skip an image, Backspace to undo, r to jump to a random unlabeled image, Enter to confirm in Multi-Label mode and Esc to quit. Labels are saved as you go, and the settings are kept in toolbox/config.json.

The classifier treats some labels in a special way. Skipped images are left out. A label that starts with unknown_, or the label not_visible, means "can't tell" and does not count towards training. The default groups use this already: visibility (fully_visible, partly_visible, not_visible) and health (healthy, unhealthy, unknown_health).

7. Train the classifier

python 52_train_classifier.py

It reads inference_out/cutouts_masked/annotations.csv and trains a ResNet18 (ImageNet weights) with one output head per label group. Images are resized to 224 × 224 pixels and split 80/10/10. Defaults: --epochs 300, --batch-size 1024, --lr 0.001. --show-groups prints how labels map to numbers and stops.

The model with the best average validation accuracy is saved as runs/segment/all_species/best_multihead_classifier.pt, with the training history in results.csv next to it. The script also tries to train one extra model per segmentation class, using the class column of inference_out/numeric/results.csv. Those go to runs/segment/<class>/.

8. Classify all leaves

python 53_inference_classifier.py --global-model-dir runs/segment/all_species --per-species-model-dir runs/segment

By default this script looks for models in runs/classifier/, while step 7 saves them in runs/segment/, so point it there as shown. It rebuilds the label names from the annotation file, so keep that file in place (or pass --annotation-file). With the pretrained models, use --use-sample-model instead.

The script adds one column per label group and a classifier_model_used column to inference_out/numeric/results.csv. Use --output-csv-path to write somewhere else. It also saves up to 300 random cutouts with their predicted labels in inference_out/visualized_classifier_inference/ (--visualize-count to change the number, --no-visualize to turn it off).

Analysis scripts

The scripts in analysis_scripts/ turn results.csv into traits, statistics and plots. They were written for the showcase datasets, so their paths are set inside each script and some of them expect that experiment's naming. Most of them add columns to inference_out/numeric/results.csv in place. Run them from the repository root, roughly in this order:

Script What it does
31_canopy_coverage.py Adds polygon_area, the leaf area in pixels
32_create_5ch.py Stacks RGB (_Source0_), NIR1 (_Source1_) and NIR2 (_Source2_) images taken within 6 seconds into 5-channel TIFFs. Reads all_images_all_spec/, writes temp/combined_spectral_cutouts_5channel/
33_5ch_cutout.py Cuts every leaf out of the 5-channel images, background set to 0. Writes temp/final_masked_cutouts_5channel/
34_indecis.py Adds the mean NDVI, NDWI, EVI and NRGI of each leaf (NDVI_avg and so on), using NIR2 as the NIR band
35_add_variety_and_treatment.py Adds species, variety, treatment, plot_id and date, decoded from the image title and file name. The codes (Hosta, Convallaria, Astilbe, blueberry varieties, irrigation levels) belong to the showcase experiment, so edit the maps at the top for your own data
36_data_aggregation.py Writes per-image results to inference_out/numeric/image_level_results.csv and per-plant canopy spread (a convex hull around the leaf centres) to inference_out/numeric/plant_level_canopy_results.csv, with plots in inference_out/canopy_spread_visuals/. It keeps only species and treatment combinations measured on exactly four dates
36_track_leaves.py A first attempt at following single leaves across dates within each plot. Adds leaf_id and draws the tracks into inference_out/visual_tracking/
37_stat.py Fits a linear mixed-effects model per trait and species (treatment and time as fixed effects, plant as group) and saves plots to inference_out/statistics/
38_visulize_data.py Plots each leaf's centre coloured by its NDVI, one plot per image, in inference_out/spatial_plots/

36_track_leaves.py and 37_stat.py still contain absolute Windows paths. Change them before you run these two.

Toolbox

Script What it does
01_replace_B_by_NIR1.py Builds red, green, NIR TIFFs from RGB and NIR image pairs (see step 1)
02_classification_labeling.py The labeling tool (see step 6)
03_test_models.py Trains several YOLO11 sizes (YOLO_MODEL_SIZES_TO_TEST, default l and x) for 150 epochs and prints a table of their mAP scores. Halves the batch size when the GPU runs out of memory. Runs go to runs/segment/model_comparison_runs/, best weights to models/finetuned_comparison_results/. Add | tee -a results.log to keep the output
04_write_coulumn_form_results_to_annotation.py Copies the class column from results.csv into an annotation CSV. Paths are set at the top
05_open_hsi_image.py <file.raw or folder> [--save cube.npy] Reads an ENVI hyperspectral image (.raw plus .hdr) and saves the cube as .npy
06_hsi_to_rgb.py <file.raw, file.npy or folder> [--out path] [--show] Makes a pseudo-RGB TIFF from a hyperspectral cube. Bands up to 650 nm are weighted with the colour sensitivity curves of a Thorlabs Zelux 1.6 MP colour camera, so the result looks like a normal camera image. Needs the .hdr file next to the cube
07_reduce_channels.py <image_dir> <target_channels> Keeps only the first target_channels channels of every TIFF in a folder. Overwrites the files

results/model/model_size_comparison/plot_model_size_comparison.py plots the mask mAP50 and validation box loss of each model size from the 03_test_models.py runs into model_performance_comparison.pdf. Set RAW_DATA_PATH at the top first.

Pretrained models and sample data

Several scripts have flags for the pretrained models and sample data: --use-sample-model, --use-sample-images, --use-sample-data and --use-pretrained-model. They all expect the models/pretrained_model_data/ folder, which is not on GitHub. Contact me to get it.

Results

From the blog post:

  • On a multi-species RGB-NIR dataset from a gantry-based phenotyping platform, the YOLOv11x segmentation model reached a mAP50 of 0.949 on the validation set.
  • On hyperspectral images from a Specim IQ camera, turned into pseudo-RGB with 06_hsi_to_rgb.py, the model scored a mAP above 0.8 without any extra training. After fine-tuning on 30 more images it reached a mAP50 of 0.903.
  • On a seven-year-old desktop PC the whole chain took about one second per image, from loading the image to saving the classified leaves.

Known limits: in the hyperspectral tests, the model sometimes mixed up blueberry leaves and other species in mixed canopies. Tracking single leaves over time (36_track_leaves.py) is only a first attempt.

Contact

Questions, problems, or want the pretrained models? Get in touch through moritznelle.com/contact-me.

About

Deep learning pipeline for plant phenotyping images: YOLOv11 leaf segmentation, then leaf trait classification.

Topics

Resources

Stars

1 star

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages