Modelling for Leaf Disease Detection

PlantDoc Dataset, IceVision Framework and YOLOv5 model

Maria L Rodriguez
7 min readSep 2, 2021

What drives progress most is the need to solve a problem. And what more serious quantifiable problem is there than that of poor food supply? Humans and animals heavily rely on good plant crop production. And crop production is highly influenced by plant health. It is thus imperative that crops be monitored for signs of early disease development. However, present techniques require laboratory diagnosis which takes time and resources. To help improve plant disease detection, the PlantDoc dataset was created.

The original dataset contained 2,598 data images with 13 plant species and 17 classes of diseases. Data was provided as images in JPG, and annotations in both the VOC XML format and CSV format.

In a previous blog, we explored how to use the dataset using manual upload and creating a custom parser. In this blog, we will upload the data using the git clone approach, proceed with parsing and continue to modelling.

We will follow this Outline:

A. Set-up

B. Establish directories

C. Parsing

D. Transforms

E. Modelling

E.1. Faster R-CNN

E.2. YOLOv5

E.3. RetinaNet

E.4. EfficientDet

E.5. Final Model

F. Visualize Results

G. Saving the Model

Open your Notebook and let’s see code grow!

A. Set-up

!wget https://raw.githubusercontent.com/airctic/icevision/master/install_colab.sh
!bash install_colab.sh

Let the above installations finish before proceeding with the next.

from icevision.all import *

Retrieve the data directly from the Github source:

!git clone https://github.com/pratikkayal/PlantDoc-Object-Detection-Dataset.git

B. Establish directories

%pwd  # output: '/content'
!ls # output: PlantDoc-Object-Detection-Dataset (among others)

B.1. Specify a direct route to the data.

%cd PlantDoc-Object-Detection-Dataset/
!ls

The TRAIN file contains the images and individual annotations.

data_dir = Path('.')

B.2. Specify a route to the annotations.

The annotations are also available as an aggregated information in the train_labels.csv.

import pandas as pd
annot = pd.read_csv('train_labels.csv')
annot.rename(columns={'class':'label'}, inplace=True)
annot.sample(3)
annot.filename.nunique()                      # output: 2,345

B.3. Specify a route to the labels.

_CLASSES = annot['label'].unique().tolist()
len(_CLASSES) # ouput: 29
class_map = ClassMap(_CLASSES)

C. Parsing

template_record = ObjectDetectionRecord()class PlantDocParser(Parser):
def __init__(self, template_record, data_dir):
super().__init__(template_record=template_record)
self.data_dir = data_dir
self.df = annot
self.class_map = class_map
def __iter__(self) -> Any:
for o in self.df.itertuples():
yield o
def __len__(self) -> int:
return len(self.df)
def record_id(self, o) -> Hashable:
return o.filename
def parse_fields(self, o, record, is_new):
if is_new:
filepath = self.data_dir / 'TRAIN' / o.filename
record.set_filepath(filepath)
if filepath.exists():
image_size = get_img_size(filepath)
record.set_img_size(image_size) #
record.detection.set_class_map(self.class_map)
record.detection.add_bboxes(
[BBox.from_xyxy(o.xmin, o.ymin, o.xmax, o.ymax)])
record.detection.add_labels([o.label])

The custom parsing steps are discussed in Section E here.

parser = PlantDocParser(template_record, data_dir)train_records, valid_records = parser.parse()show_record(train_records[0], class_map=class_map, 
figsize = (10,10),font_size=20, label_color = '#ffff00')
train_records[0]
show_records(train_records_csv[33:36], ncols=3,
font_size=60, label_color = '#ff5000')

D. Transforms

presize = 512
image_size = 384
train_tfms = tfms.A.Adapter(
[*tfms.A.aug_tfms(size= image_size, presize= presize), tfms.A.Normalize()])
valid_tfms = tfms.A.Adapter(
[*tfms.A.resize_and_pad(size=image_size), tfms.A.Normalize()])
train_ds = Dataset(train_records_csv, train_tfms)
valid_ds = Dataset(valid_records_csv, valid_tfms)
samples = [train_ds[38] for _ in range(3)]
show_samples(samples, ncols=3, font_size=22, label_color= '#ffff00')

E. Modelling

import matplotlib.pyplot as plt
def plot_metrics(learn, title, x, y):
plt.plot(L(learn.recorder.values).itemgot())
plt.xlabel('epoch')
plt.ylabel('mAP (green), Loss (blue, orange)')
plt.title(title)
plt.text(x, y,
'Legend: mAP(green), train_loss(blue), valid_loss(orange');

For a recap on the following four models, refer to Section C here.

E.1. Faster R-CNN

metrics = [COCOMetric(metric_type=COCOMetricType.bbox)]model_type_frcnn = models.torchvision.faster_rcnn 
model_frcnn = model_type_frcnn.model(
num_classes=len(parser.class_map))
train_dl_frcnn = model_type_frcnn.train_dl(train_ds,
batch_size=16, num_workers=4, shuffle=True)
valid_dl_frcnn = model_type_frcnn.valid_dl(valid_ds,
batch_size=16, num_workers=4, shuffle=False)
learn_frcnn = model_type_frcnn.fastai.learner(
dls=[train_dl_frcnn, valid_dl_frcnn],
model=model_frcnn, metrics=metrics)
learn_frcnn.lr_find() #
learn_frcnn.fine_tune(10, 2e-4, freeze_epochs=1) #plot_metrics(learn_frcnn,
'Mean Average Precision and Losses for Faster_rcnn', 0,-0.2)

The Faster R-CNN model reached a mAP of 0.222 after 10 epochs at LR 2e-4. The mAP and losses trend were steady and has started to plateau. The runtime was 01:46 per epoch.

E.2. YOLOv5

model_type_yolo = models.ultralytics.yolov5 
backbone_yolo = model_type_yolo.backbones.small
model_yolo = model_type_yolo.model(
backbone = backbone_yolo(pretrained=True),
num_classes=len(parser.class_map), img_size = image_size)
train_dl_yolo = model_type_yolo.train_dl(train_ds,
batch_size=16, num_workers=4, shuffle=True)
valid_dl_yolo = model_type_yolo.valid_dl(valid_ds,
batch_size=16, num_workers=4, shuffle=False)

learn_yolo = model_type_yolo.fastai.learner(
dls=[train_dl_yolo, valid_dl_yolo], model=model_yolo, metrics=metrics)
learn_yolo.lr_find()#learn_yolo.fine_tune(10, 3e-3 , freeze_epochs=1) #plot_metrics(learn_yolo,
'Mean Average Precision and Losses for YOLOv5', 0,-0.23)

The YOLOv5 model reached a mAP of 0.229 after 10 epochs at LR 3e-3. The mAP and losses trend were steady and has started to plateau. The runtime was 00:51 per epoch.

E.3. RetinaNet

model_type_ret = models.mmdet.retinanet 
backbone_r50 = model_type_ret.backbones.resnet50_fpn_1x(pretrained=True)
model_ret = model_type_ret.model(backbone=backbone_r50(pretrained=True),
num_classes=len(parser.class_map))
train_dl_ret = model_type_ret.train_dl(
train_ds, batch_size=16, num_workers=4, shuffle=True)
valid_dl_ret = model_type_ret.valid_dl(
valid_ds, batch_size=16, num_workers=4, shuffle=False)
learn_ret = model_type_ret.fastai.learner(
dls=[train_dl_ret, valid_dl_ret], model=model_ret, metrics=metrics)
learn_ret.lr_find()#learn_ret.fine_tune(10, 7e-5 , freeze_epochs=1) #plot_metrics(learn_ret,
'Mean Average Precision and Losses for Retinanet/Resnet50',0, -0.22)

The RetinaNet model reached a mAP of 0.165 after 10 epochs at LR 7e-5. The mAP and losses trend were steady and has started to plateau. The runtime was 01:16 per epoch.

E.4. EfficientDet

model_type_eff = models.ross.efficientdet 
backbone_eff = model_type_eff.backbones.tf_lite0
model_eff = model_type_eff.model(backbone = backbone_eff(pretrained=True), num_classes=len(parser.class_map), img_size = image_size)train_dl_eff = model_type_eff.train_dl(
train_ds, batch_size=16, num_workers=4, shuffle=True)
valid_dl_eff = model_type_eff.valid_dl(
valid_ds, batch_size=16, num_workers=4, shuffle=False)

learn_eff = model_type_eff.fastai.learner(
dls=[train_dl_eff, valid_dl_eff], model=model_eff, metrics=metrics)
learn_eff.lr_find() #learn_eff.fine_tune(10, 5e-2, freeze_epochs=1) #plot_metrics(learn_eff,
'Mean Average Precision and Losses for EfficientDet',0,-0.9)

The EfficientDet model reached a mAP of 0.122 after 10 epochs at LR 5e-2. The map and train loss slowly improved. The valid loss showed an initial high loss, eventually matching that of the train loss. The runtime was 01:02 per epoch.

E.5. Final Model

* y-axes NOT to scale

The YOLOv5 was chosen for final modelling for this dataset because of the best mAP and loss trends, with the additional benefit of faster run time. Despite the plateauing, the epochs will be extended to 30 to improve the model’s precision and recall.

model_type = models.ultralytics.yolov5 
backbone = model_type.backbones.small
model = model_type.model(backbone = backbone(pretrained=True),
num_classes=len(parser.class_map),img_size = image_size)
train_dl = model_type.train_dl(
train_ds, batch_size=16, num_workers=4, shuffle=True)
valid_dl = model_type.valid_dl(
valid_ds, batch_size=16, num_workers=4, shuffle=False)

learn = model_type.fastai.learner(
dls=[train_dl, valid_dl], model=model, metrics=metrics)
# learn.lr_find()learn.fine_tune(30, 3e-3 , freeze_epochs=1) plot_metrics(learn,
'Mean Average Precision and Losses for YOLOv5', 0,-0.23)

The final YOLOv5 model reached a mAP of 0.286 after 30 epochs at LR 3e-3. The mAP and losses trend still show minute improvements with no overfit. However, for the purposes of important but non-critical leaf identity or disease detection, these levels will be considered as reasonable.

F. Visualize Results

The model is able to correctly identify and localize some leaf species and diseases. However, the images show that the classification results are suboptimal.

The authors of the paper report of mAP 38.9 using Faster R-CNN- Inception- Resnet model combination, using pretrained weights from COCO. The present mAP of 28.6 may be improved with further training or adapting the paper’s model.

G. Saving the Model

from google.colab import drive
drive.mount('/content/gdrive', force_remount=True)
root_dir = Path('/content/gdrive/My Drive/')
fname_model = 'plantdoc-yolov5.pth'# create a 'models' folder in GDrive prior to running the following line:
torch.save(model.state_dict(), root_dir/'models'/fname_model)

Summary:

A custom parser was created for a cloned PlantDoc dataset. A trial of four model approaches was done. Final modelling with YOLOv5 resulted in a mAP of 28.6 after a run of 30 epochs. Visualization showed that the results were still suboptimal. Further acquisition of data, longer training, or shifting to other model combinations might be tried to improve the model.

I hope you enjoyed learning! :)

Maria

LinkedIn: https://www.linkedin.com/in/rodriguez-maria/

Twitter: https://twitter.com/Maria_Rod_Data

Github repo used for this blog: IV_plantdoc_git_customparser_model

--

--