Tutorial on leaf recognition

This tutorial covers the basics of leaf recognition based on its shape and color statistics. The dataset is availabe at <http://flavia.sourceforge.net>.

Contents

Preprocessing

Before we start, let's add necessary toolboxes to the search path of MATLAB:

addpath d:/users/jang/matlab/toolbox/utility
addpath d:/users/jang/matlab/toolbox/machineLearning

All the above toolboxes can be downloaded from the author's toolbox page. Make sure you are using the latest toolboxes to work with this script.

For compatibility, here we list the platform and MATLAB version that we used to run this script:

fprintf('Platform: %s\n', computer);
fprintf('MATLAB version: %s\n', version);
fprintf('Date & time: %s\n', char(datetime));
scriptStartTime=tic;
Platform: PCWIN64
MATLAB version: 9.3.0.651671 (R2017b) Prerelease
Date & time: 18-Jan-2018 07:39:39

Dataset construction

First of all, we shall collect all the image data from the image directory. Note that

imDir='D:\users\jang\books\dcpr\appNote\leafId\leafSorted';
opt=mmDataCollect('defaultOpt');
opt.extName='jpg';
opt.maxClassNum=5;
imageData=mmDataCollect(imDir, opt, 1);
Collecting 299 files with extension "jpg" from "D:\users\jang\books\dcpr\appNote\leafId\leafSorted"...
Warning: Image is too big to fit on screen; displaying at 13% 
Warning: Image is too big to fit on screen; displaying at 13% 
Warning: Image is too big to fit on screen; displaying at 13% 
Warning: Image is too big to fit on screen; displaying at 13% 
Warning: Image is too big to fit on screen; displaying at 13% 

Feature extraction

For each image, we need to extract the corresponding features for classification. We shall use the function "leafFeaExtract" for feature extraction. We also need to put all the dataset into a format that is easier for further processing, including classifier construction and evaluation.

opt=dsCreateFromMm('defaultOpt');
if exist('ds.mat', 'file')
	fprintf('Loading ds.mat...\n');
	load ds.mat
else
	myTic=tic;
	opt=dsCreateFromMm('defaultOpt');
	opt.imFeaFcn=@leafFeaExtract;	% Function for feature extraction
	opt.imFeaOpt=feval(opt.imFeaFcn, 'defaultOpt');	% Feature options
	ds=dsCreateFromMm(imageData, opt);
	fprintf('Time for feature extraction over %d images = %g sec\n', length(imageData), toc(myTic));
	fprintf('Saving ds.mat...\n');
	save ds ds
end
Loading ds.mat...

Note that since feature extraction is a lengthy process, we have save the resulting variable "ds" into "ds.mat". If needed, you can simply load the file to restore the dataset variable "ds" and play around with it. But if you have changed the feature extraction function, be sure to delete ds.mat first to enforce the feature extraction.

Basically the extracted features are based on the regions separated by Otsu's method. We only consider the region with the maximum area, and compute its region properties and color statistics as features. You can type "leafFeaExtract" to have a self-demo of the function:

figure; leafFeaExtract;

Dataset visualization

Once we have all the necessary information stored in "ds", we can invoke many different functions in Machine Learning Toolbox for data visualization and classification.

For instance, we can display the size of each class:

figure;
[classSize, classLabel]=dsClassSize(ds, 1);
6 features
299 instances
5 classes

We can plot the distribution of each features within each class:

figure; dsBoxPlot(ds);

The box plots indicate the ranges of the features vary a lot. To verify this, we can simply plot the range of features of the dataset:

figure; dsRangePlot(ds);

Big range difference cause problems in distance-based classification. To avoid this, we can simply apply z-normalization to each feature:

ds2=ds;
ds2.input=inputNormalize(ds2.input);

We can now plot the feature vectors within each class:

figure; dsFeaVecPlot(ds); figEnlarge;

We can also do scatter plots on each pair of the original features:

figure; dsProjPlot2(ds); figEnlarge;

It is hard to see the above plots due to a large difference in the range of each features. We can try the same plot with normalized inputs:

figure; dsProjPlot2(ds2); figEnlarge;

We can also do the scatter plots in the 3D space:

figure; dsProjPlot3(ds2); figEnlarge;

In order to visualize the distribution of the dataset, we can project the original dataset into 2-D space. This can be achieved by LDA (linear discriminant analysis):

ds2d=lda(ds);
ds2d.input=ds2d.input(1:2, :);
figure; dsScatterPlot(ds2d); xlabel('Input 1'); ylabel('Input 2');
title('Features projected on the first 2 lda vectors');

Classification

We can try the most straightforward KNNC (k-nearest neighbor classifier):

rr=knncLoo(ds);
fprintf('rr=%g%% for ds\n', rr*100);
rr=77.2575% for ds

For normalized dataset, usually we can obtain a better accuracy:

[rr, computed]=knncLoo(ds2);
fprintf('rr=%g%% for ds2 of normalized inputs\n', rr*100);
rr=97.3244% for ds2 of normalized inputs

We can plot the confusion matrix:

confMat=confMatGet(ds2.output, computed);
opt=confMatPlot('defaultOpt');
opt.className=ds.outputName;
opt.mode='both';
figure; confMatPlot(confMat, opt);

We can perform sequential input selection to find the best features:

figure; tic; inputSelectSequential(ds2, inf, 'knnc'); toc
Construct 21 knnc models, each with up to 6 inputs selected from 6 candidates...

Selecting input 1:
Model 1/21: selected={1} => Recog. rate = 45.8%
Model 2/21: selected={2} => Recog. rate = 65.2%
Model 3/21: selected={3} => Recog. rate = 65.2%
Model 4/21: selected={4} => Recog. rate = 48.8%
Model 5/21: selected={5} => Recog. rate = 27.8%
Model 6/21: selected={6} => Recog. rate = 44.8%
Currently selected inputs: 2

Selecting input 2:
Model 7/21: selected={2, 1} => Recog. rate = 75.9%
Model 8/21: selected={2, 3} => Recog. rate = 65.2%
Model 9/21: selected={2, 4} => Recog. rate = 97.0%
Model 10/21: selected={2, 5} => Recog. rate = 72.6%
Model 11/21: selected={2, 6} => Recog. rate = 78.3%
Currently selected inputs: 2, 4

Selecting input 3:
Model 12/21: selected={2, 4, 1} => Recog. rate = 98.7%
Model 13/21: selected={2, 4, 3} => Recog. rate = 99.0%
Model 14/21: selected={2, 4, 5} => Recog. rate = 94.3%
Model 15/21: selected={2, 4, 6} => Recog. rate = 95.7%
Currently selected inputs: 2, 4, 3

Selecting input 4:
Model 16/21: selected={2, 4, 3, 1} => Recog. rate = 99.0%
Model 17/21: selected={2, 4, 3, 5} => Recog. rate = 97.7%
Model 18/21: selected={2, 4, 3, 6} => Recog. rate = 98.7%
Currently selected inputs: 2, 4, 3, 1

Selecting input 5:
Model 19/21: selected={2, 4, 3, 1, 5} => Recog. rate = 97.7%
Model 20/21: selected={2, 4, 3, 1, 6} => Recog. rate = 97.7%
Currently selected inputs: 2, 4, 3, 1, 5

Selecting input 6:
Model 21/21: selected={2, 4, 3, 1, 5, 6} => Recog. rate = 97.3%
Currently selected inputs: 2, 4, 3, 1, 5, 6

Overall maximal recognition rate = 99.0%.
Selected 3 inputs (out of 6): 2, 4, 3
Elapsed time is 0.316317 seconds.

Since the number of features is not too big, we can also exhaustive search to find the best features:

figure; tic; inputSelectExhaustive(ds2, inf, 'knnc'); toc
Construct 63 knnc models, each with up to 6 inputs selected from 6 candidates...
modelIndex 1/63: selected={1} => Recog. rate = 45.819398%
modelIndex 2/63: selected={2} => Recog. rate = 65.217391%
modelIndex 3/63: selected={3} => Recog. rate = 65.217391%
modelIndex 4/63: selected={4} => Recog. rate = 48.829431%
modelIndex 5/63: selected={5} => Recog. rate = 27.759197%
modelIndex 6/63: selected={6} => Recog. rate = 44.816054%
modelIndex 7/63: selected={1, 2} => Recog. rate = 75.919732%
modelIndex 8/63: selected={1, 3} => Recog. rate = 77.257525%
modelIndex 9/63: selected={1, 4} => Recog. rate = 88.628763%
modelIndex 10/63: selected={1, 5} => Recog. rate = 59.531773%
modelIndex 11/63: selected={1, 6} => Recog. rate = 73.913043%
modelIndex 12/63: selected={2, 3} => Recog. rate = 65.217391%
modelIndex 13/63: selected={2, 4} => Recog. rate = 96.989967%
modelIndex 14/63: selected={2, 5} => Recog. rate = 72.575251%
modelIndex 15/63: selected={2, 6} => Recog. rate = 78.260870%
modelIndex 16/63: selected={3, 4} => Recog. rate = 99.331104%
modelIndex 17/63: selected={3, 5} => Recog. rate = 72.240803%
modelIndex 18/63: selected={3, 6} => Recog. rate = 79.933110%
modelIndex 19/63: selected={4, 5} => Recog. rate = 63.210702%
modelIndex 20/63: selected={4, 6} => Recog. rate = 74.247492%
modelIndex 21/63: selected={5, 6} => Recog. rate = 59.531773%
modelIndex 22/63: selected={1, 2, 3} => Recog. rate = 77.591973%
modelIndex 23/63: selected={1, 2, 4} => Recog. rate = 98.662207%
modelIndex 24/63: selected={1, 2, 5} => Recog. rate = 80.267559%
modelIndex 25/63: selected={1, 2, 6} => Recog. rate = 85.284281%
modelIndex 26/63: selected={1, 3, 4} => Recog. rate = 98.662207%
modelIndex 27/63: selected={1, 3, 5} => Recog. rate = 78.595318%
modelIndex 28/63: selected={1, 3, 6} => Recog. rate = 85.953177%
modelIndex 29/63: selected={1, 4, 5} => Recog. rate = 88.963211%
modelIndex 30/63: selected={1, 4, 6} => Recog. rate = 93.645485%
modelIndex 31/63: selected={1, 5, 6} => Recog. rate = 79.598662%
modelIndex 32/63: selected={2, 3, 4} => Recog. rate = 98.996656%
modelIndex 33/63: selected={2, 3, 5} => Recog. rate = 72.575251%
modelIndex 34/63: selected={2, 3, 6} => Recog. rate = 79.264214%
modelIndex 35/63: selected={2, 4, 5} => Recog. rate = 94.314381%
modelIndex 36/63: selected={2, 4, 6} => Recog. rate = 95.652174%
modelIndex 37/63: selected={2, 5, 6} => Recog. rate = 80.602007%
modelIndex 38/63: selected={3, 4, 5} => Recog. rate = 97.658863%
modelIndex 39/63: selected={3, 4, 6} => Recog. rate = 98.996656%
modelIndex 40/63: selected={3, 5, 6} => Recog. rate = 81.270903%
modelIndex 41/63: selected={4, 5, 6} => Recog. rate = 79.598662%
modelIndex 42/63: selected={1, 2, 3, 4} => Recog. rate = 98.996656%
modelIndex 43/63: selected={1, 2, 3, 5} => Recog. rate = 80.602007%
modelIndex 44/63: selected={1, 2, 3, 6} => Recog. rate = 87.959866%
modelIndex 45/63: selected={1, 2, 4, 5} => Recog. rate = 97.658863%
modelIndex 46/63: selected={1, 2, 4, 6} => Recog. rate = 98.327759%
modelIndex 47/63: selected={1, 2, 5, 6} => Recog. rate = 85.618729%
modelIndex 48/63: selected={1, 3, 4, 5} => Recog. rate = 96.989967%
modelIndex 49/63: selected={1, 3, 4, 6} => Recog. rate = 96.989967%
modelIndex 50/63: selected={1, 3, 5, 6} => Recog. rate = 85.953177%
modelIndex 51/63: selected={1, 4, 5, 6} => Recog. rate = 95.652174%
modelIndex 52/63: selected={2, 3, 4, 5} => Recog. rate = 97.658863%
modelIndex 53/63: selected={2, 3, 4, 6} => Recog. rate = 98.662207%
modelIndex 54/63: selected={2, 3, 5, 6} => Recog. rate = 83.612040%
modelIndex 55/63: selected={2, 4, 5, 6} => Recog. rate = 93.645485%
modelIndex 56/63: selected={3, 4, 5, 6} => Recog. rate = 96.655518%
modelIndex 57/63: selected={1, 2, 3, 4, 5} => Recog. rate = 97.658863%
modelIndex 58/63: selected={1, 2, 3, 4, 6} => Recog. rate = 97.658863%
modelIndex 59/63: selected={1, 2, 3, 5, 6} => Recog. rate = 89.297659%
modelIndex 60/63: selected={1, 2, 4, 5, 6} => Recog. rate = 97.993311%
modelIndex 61/63: selected={1, 3, 4, 5, 6} => Recog. rate = 95.986622%
modelIndex 62/63: selected={2, 3, 4, 5, 6} => Recog. rate = 98.327759%
modelIndex 63/63: selected={1, 2, 3, 4, 5, 6} => Recog. rate = 97.324415%

Overall max recognition rate = 99.3%.
Selected 2 inputs (out of 6): 3, 4
Elapsed time is 0.554176 seconds.

It is obvious that the exhaustive search can find the best features, but at the cost of more computation.

We can even perform an exhaustive search on the classifiers and the way of input normalization:

opt=perfCv4classifier('defaultOpt');
opt.foldNum=10;
tic; [perfData, bestId]=perfCv4classifier(ds, opt, 1); toc
structDispInHtml(perfData, 'Performance of various classifiers via cross validation');
Warning in gmmTrain: max(range)/min(range)=10881.1>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=13356.6>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=12241.4>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=12103.1>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=10881.1>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=12103.1>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=10881.1>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=11780.9>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=10289.8>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=12103.1>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=10881.1>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=12103.1>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=10775.2>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=12103.1>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=10881.1>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=12103.1>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=10881.1>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=12103.1>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=12279.3>10000 ===> Perhaps you should normalize the data first.
Warning in gmmTrain: max(range)/min(range)=12084.8>10000 ===> Perhaps you should normalize the data first.
Elapsed time is 34.157843 seconds.

We can then display the confusion matrix of the best classifier:

confMat=confMatGet(ds.output, perfData(bestId).bestComputedClass);
opt=confMatPlot('defaultOpt');
opt.className=ds.outputName;
figure; confMatPlot(confMat, opt);

We can also list all the misclassified images in a table:

for i=1:length(imageData)
	imageData(i).classIdPredicted=perfData(bestId).bestComputedClass(i);
	imageData(i).classPredicted=ds.outputName{imageData(i).classIdPredicted};
end
listOpt=mmDataList('defaultOpt');
mmDataList(imageData, listOpt);

List of 1 misclassified cases

Index\FieldFileGT ==> PredictedHiturl
 1 3173.jpg Chinese Toon ==> Anhui Barberry false

Summary

This is a brief tutorial on leaf recognition based on its shape and color statistics. There are several directions for further improvement:

Appendix

List of functions, scripts, and datasets used in this script:

Overall elapsed time:

toc(scriptStartTime)
Elapsed time is 79.447077 seconds.

Jyh-Shing Roger Jang, created on

datetime
ans = 

  datetime

   18-Jan-2018 07:40:58

If you are interested in the original MATLAB code for this page, you can type "grabcode(URL)" under MATLAB, where URL is the web address of this page.