top of page
Search
Writer's pictureAbel Joshua Cruzada

Image Machine Learning Classification without Neural Networks


From my past blogs, I have discussed morphological operations, blob detection, and connected components. In this blog, we will apply these techniques to extract features in images without the use of Neural Networks.


For our application, the dataset is composed of 27 grayscale images of leaves for five different types of plants. Each image contains a different number of leaves with the same plant type. Our goal is to extract relevant features in classifying the type of plant each leaf belongs to in our image. We will then feed these features to various machine learning models to predict each leaves' plant type and evaluate its accuracy. For simplicity, we will name the five types of plants as plant A, B, C, D, and E. Sample leaves of each plant type are shown in the figure below.

Fig 1. Plant Samples


The general workflow of this application is enumerated below:

  1. Read the image

  2. Binarize the image using a threshold intensity value

  3. Apply morphological operations to clean the image

  4. Detect the connected components in the image

  5. Get the region properties of identified objects

  6. Filter unnecessary objects

  7. Calculate new features per object

  8. Feature selection

  9. Data Preprocessing

  10. Train machine learning model


First, let us import the necessary libraries for our application and define the common function we will use to clean our image.

import os
import re
import time
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from scipy.signal import convolve2d
from skimage.io import imread, imshow
from skimage.color import rgb2gray
from skimage.measure import label, regionprops, regionprops_table
from skimage.transform import rotate
from skimage.morphology import (erosion, dilation, closing, opening,
                                area_closing, area_opening)
from sklearn.preprocessing import MinMaxScaler, StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import LinearSVC
from sklearn.svm import SVC
from sklearn.naive_bayes import GaussianNB
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.ensemble import RandomForestClassifier
from glob import glob
# Constants
plant_prefix = 'Leaves/plant'

sel_h = np.array([[0, 0, 0],
                  [1, 1, 1],
                  [0, 0, 0]])

sel_v = np.array([[0, 1, 0],
                  [0, 1, 0],
                  [0, 1, 0]])

sel_c = np.array([[0, 1, 0],
                  [1, 1, 1],
                  [0, 1, 0]])

blur = (1 / 16.0) * np.array([[1., 2., 1.],
                              [2., 4., 2.],
                              [1., 2., 1.]])


def multi_dil(im, num, sel=None):
    for i in range(num):
        im = dilation(im, sel)
    return im


def multi_ero(im, num, sel=None):
    for i in range(num):
        im = erosion(im, sel)
    return im

# Region Properties to extract
properties = ['area', 'centroid', 'convex_area',
              'bbox', 'bbox_area', 'eccentricity',                 
              'equivalent_diameter',
              'extent', 'filled_area',
              'major_axis_length', 'minor_axis_length', 
              'mean_intensity',
              'perimeter', 'orientation', 'solidity']

Now that we have imported, defined, and assigned the necessary libraries, functions, and variables, we can now segment each leaf in the dataset. We binarize the images using a pre-defined threshold to remove unnecessary objects in our image. However, some leaves are close to each other and appear as one leaf. Morphological operations can be used to solve this issue and separate conjoined leaves. After isolating each leaves with each other, we use the label and regionprops_table function from the skimage library. The regionprops_table function extracts each connected components' properties in our image, such as area, centroid, mean intensity, etc. However, not all the objects detected are leaves. We used the convex_area of each object to filter out objects that did not meet specific standards to be classified as a leaf. We can also use the orientation property to derive new features not available in the regionprops_table function like the circularity and length of each leaf to increase the accuracy of the machine learning models. The code for segmenting and extracting features for plant a is shown in the figure below.

plant_a_files = glob(f'{plant_prefix}A*.jpg')

clean_plant_a = []  # List of two tuples (img, regionprops)
fig = plt.figure(figsize=(20, 20))
file_count = len(plant_a_files)
thres = 0.4
no_col = 6
no_row = int(np.ceil(file_count * 2 / no_col))

gs = gridspec.GridSpec(no_row, no_col)

for i, file in enumerate(plant_a_files):

    img = imread(file)
    fn = file.split('\\')[-1].split('.')[0]
    ax0 = fig.add_subplot(gs[i * 2])
    ax1 = fig.add_subplot(gs[i * 2 + 1])
    ax0.axis('off')
    ax1.axis('off')

    
    # Display Threshold Image
    ax0.imshow(img)
    ax0.set_title(fn)
    
    # Threshold image
    img = rgb2gray(img)
    img_bw = img < thres

    # Morph Image
    img_morph = area_closing(area_opening(img_bw, 200), 200)
    img_morph = multi_ero(multi_ero(img_morph, 22, sel_v), 2, sel_h)
    img_morph = multi_dil(img_morph, 20, sel_v)
    
    # Get region properties of image
    img_label = label(img_morph)
    df_regions = pd.DataFrame(regionprops_table(img_label, img,
                                                properties=properties))
    
    # Filter regions using area
    area_thres = df_regions.convex_area.mean()/3
    df_regions = df_regions.loc[df_regions.convex_area > area_thres]
    
    mask_equal_height = ((df_regions['bbox-2'] - df_regions['bbox-0'])
                         != img_label.shape[0])
    mask_equal_width = ((df_regions['bbox-3'] - df_regions['bbox-1'])
                         != img_label.shape[1])
    df_regions = df_regions.loc[mask_equal_height & mask_equal_width]

    # Compute for Derive features
    y0, x0 = df_regions['centroid-0'], df_regions['centroid-1']
    orientation = df_regions.orientation
    x1 = (x0 + np.cos(df_regions.orientation) * 0.5
          * df_regions.minor_axis_length)
    y1 = (y0 - np.sin(df_regions.orientation) * 0.5
          * df_regions.minor_axis_length)
    x2 = (x0 - np.sin(df_regions.orientation) * 0.5
          * df_regions.major_axis_length)
    y2 = (y0 - np.cos(df_regions.orientation) * 0.5
          * df_regions.major_axis_length)

    df_regions['major_length'] = np.sqrt((x2 - x0)**2 + (y2 - y0)**2)
    df_regions['minor_length'] = np.sqrt((x1 - x0)**2 + (y1 - y0)**2)
    df_regions['circularity'] = (4 * np.pi * df_regions.filled_area
                                 / (df_regions.perimeter ** 2))
    
    # Display segmented image
    ax1.imshow(img_label)
    ax1.set_title(f'{fn} segmented: {df_regions.shape[0]}')
    
    df_regions['target']='plant_a'
    clean_plant_a.append(df_regions)

Fig 3. Plant A segmentation


In segmenting other plant types, we can reuse the steps for segmenting plant A. Now that we have extracted the features from different plant types, feature selection is performed to efficiently train the machine learning model and avoid storing unnecessary data. In building our dataset, we will use the following features:

  1. area - Area of the detected object

  2. convex_area - Area of the detected convex region

  3. bbox_area - Number of bounding box pixels.

  4. eccentricity - Ratio of the focal distance over the major axis length.

  5. equivalent_diameter - Diameter of a circle with the same area as the region.

  6. extent - Ratio of pixels in the region to pixels in the total bounding box

  7. filled_area - Number of pixels of the region will all the holes filled in.

  8. perimeter - Length of the path that surrounds the detected object.

  9. mean_intensity - Average intensity in the detected region.

  10. solidity - Ratio of pixels in the region to pixels of the convex hull.

  11. major_length - Major length of the object.

  12. minor_length - Minor length of the object

  13. circularity - Degree of roundness of the detected object.

To know more about the region properties, check this link.

df_plant = pd.concat([*clean_plant_a, *clean_plant_b, *clean_plant_c,
                      *clean_plant_d, *clean_plant_e])
req_feat = ['area', 'convex_area', 'bbox_area', 'eccentricity',
            'equivalent_diameter', 'extent', 'filled_area',
            'perimeter', 'mean_intensity',
            'solidity', 'major_length', 'minor_length',
            'circularity', 'target']
df_plant = df_plant[req_feat]
df_plant

Fig 4. Extracted Features


Since the features we have extracted have different scales, the dataset is normalized using the Min-Max normalization method. To validate the model's robustness and generalizability, we have also split the dataset into two, training and test dataset.

X = df_plant.drop('target', axis=1)
y = df_plant.target

# Normalize the data
scaler = MinMaxScaler()
X_scaled = scaler.fit_transform(X)

# Split the data
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y,
                                                    test_size=0.25,
                                                    random_state=0)

Now that we have extracted and preprocessed our data, we are ready to train our models. Different machine models are used to evaluate which model performed best given the features we have extracted in the dataset. The top-predictors for each plant type are also monitored to determine which features are significant in classifying the plant type.

#Training     
startTime = time.time()
knn = KNeighborsClassifier()
knn.fit(X_train, y_train)
elapsed_time_knn = time.time() - startTime

startTime = time.time()
LR2 = LogisticRegression(penalty='l2', max_iter=10000, n_jobs=-1)
LR2.fit(X_train, y_train)
elapsed_time_LR2 = time.time() - startTime

startTime = time.time()
LR1 = LogisticRegression(penalty='l1', max_iter=10000, solver='liblinear')
LR1.fit(X_train, y_train)
elapsed_time_LR1 = time.time() - startTime

startTime = time.time()
LSVC2 = LinearSVC(penalty='l1', dual=False, max_iter=10000)
LSVC2.fit(X_train, y_train)
elapsed_time_LSVC2 = time.time() - startTime

startTime = time.time()
LSVC1 = LinearSVC(penalty='l2', max_iter=10000)
LSVC1.fit(X_train, y_train)
elapsed_time_LSVC1 = time.time() - startTime

startTime = time.time()
NSVC = SVC(kernel='rbf')
NSVC.fit(X_train, y_train)
elapsed_time_NSVC = time.time() - startTime

startTime = time.time()
Bayes_Gaussian = GaussianNB()
Bayes_Gaussian.fit(X_train, y_train)
elapsed_time_Bayes_Gaussian = time.time() - startTime

startTime = time.time()
DT = DecisionTreeClassifier(max_depth=5)
DT.fit(X_train, y_train)
elapsed_time_DT = time.time() - startTime

startTime = time.time()
RF = RandomForestClassifier(max_depth=5, n_estimators=100)
RF.fit(X_train, y_train)
elapsed_time_RF = time.time() - startTime

startTime = time.time()
GB = GradientBoostingClassifier(max_depth=5, n_estimators=100)
GB.fit(X_train, y_train)
elapsed_time_GB = time.time() - startTime

#Prediction
startTime = time.time()
y_pred_knn = knn.predict(X_test)
test_time_knn = time.time() - startTime

startTime = time.time()
y_pred_LR2 = LR2.predict(X_test)
test_time_LR2 = time.time() - startTime

startTime = time.time()
y_pred_LR1 = LR1.predict(X_test)
test_time_LR1 = time.time() - startTime

startTime = time.time()
y_pred_LSVC2 = LSVC2.predict(X_test)
test_time_LSVC2 = time.time() - startTime

startTime = time.time()
y_pred_LSVC1 = LSVC1.predict(X_test)
test_time_LSVC1 = time.time() - startTime

startTime = time.time()
y_pred_NSVC = NSVC.predict(X_test)
test_time_NSVC = time.time() - startTime

startTime = time.time()
y_pred_Bayes_Gaussian= Bayes_Gaussian.predict(X_test)
test_time_Bayes_Gaussian = time.time() - startTime

startTime = time.time()
y_pred_DT= DT.predict(X_test)
test_time_DT = time.time() - startTime

startTime = time.time()
y_pred_RF= RF.predict(X_test)
test_time_RF = time.time() - startTime

startTime = time.time()
y_pred_GB= GB.predict(X_test)
test_time_GB = time.time() - startTime

cols = ['Model','Train Accuracy', 
        'Test Accuracy', 'Training Time', 'Testing Time', 
        "Plant A Top Predictor", 
        "Plant B Top Predictor", 
        "Plant C Top Predictor", 
        "Plant D Top Predictor", 
        "Plant E Top Predictor", 
        "Top Predictor Importance"]

df_result = pd.DataFrame(columns=cols)

df_result.loc[0] = ['kNN', knn.score(X_train, y_train), 
                     knn.score(X_test, y_test), 
                     elapsed_time_knn, 
                     test_time_knn,
                     "", "", "", "", "", ""]
df_result.loc[1] = ['Logistic Regression (L2)', LR2.score(X_train, y_train), 
                                      LR2.score(X_test, y_test), 
                                      elapsed_time_LR2,
                                      test_time_LR2,
                                      X.columns[np.argmax(LR2.coef_[0])], 
                                      X.columns[np.argmax(LR2.coef_[1])], 
                                      X.columns[np.argmax(LR2.coef_[2])], 
                                      X.columns[np.argmax(LR2.coef_[3])], 
                                      X.columns[np.argmax(LR2.coef_[4])], 
                                      np.max(LR2.coef_)/np.sum(LR2.coef_)]

df_result.loc[2] = ['Logistic Regression (L1)', LR1.score(X_train, y_train), 
                                      LR1.score(X_test, y_test), 
                                      elapsed_time_LR1,
                                      test_time_LR1,
                                      X.columns[np.argmax(LR1.coef_[0])], 
                                      X.columns[np.argmax(LR1.coef_[1])], 
                                      X.columns[np.argmax(LR1.coef_[2])], 
                                      X.columns[np.argmax(LR1.coef_[3])], 
                                      X.columns[np.argmax(LR1.coef_[4])], 
                                      np.max(LR1.coef_)/np.sum(LR1.coef_)]

df_result.loc[3] = ['Linear SVC (L2)', LSVC2.score(X_train, y_train),  
                          LSVC2.score(X_test, y_test), 
                          elapsed_time_LSVC2,
                          test_time_LSVC2,
                          X.columns[np.argmax(LSVC2.coef_[0])], 
                          X.columns[np.argmax(LSVC2.coef_[1])], 
                          X.columns[np.argmax(LSVC2.coef_[2])], 
                          X.columns[np.argmax(LSVC2.coef_[3])], 
                          X.columns[np.argmax(LSVC2.coef_[4])], 
                          np.max(LSVC2.coef_)/np.sum(LSVC2.coef_)]

df_result.loc[4] = ['Linear SVC (L1)', LSVC1.score(X_train, y_train),  
                          LSVC1.score(X_test, y_test), 
                          elapsed_time_LSVC1,
                          test_time_LSVC1,
                          X.columns[np.argmax(LSVC1.coef_[0])], 
                          X.columns[np.argmax(LSVC1.coef_[1])], 
                          X.columns[np.argmax(LSVC1.coef_[2])], 
                          X.columns[np.argmax(LSVC1.coef_[3])], 
                          X.columns[np.argmax(LSVC1.coef_[4])], 
                          np.max(LSVC1.coef_)/np.sum(LSVC1.coef_)]


df_result.loc[5] = ['Non-linear SVC', NSVC.score(X_train, y_train),  
                          NSVC.score(X_test, y_test), 
                          elapsed_time_NSVC,
                          test_time_NSVC,
                          "", "", "", "", "", ""]

df_result.loc[6] = ['Naive Bayes Gaussian', Bayes_Gaussian.score(X_train, y_train),  
                                      Bayes_Gaussian.score(X_test, y_test), 
                                      elapsed_time_Bayes_Gaussian,
                                      test_time_Bayes_Gaussian, 
                                      "", "", "", "", "", ""]

df_result.loc[7] = ['Decision Tree', DT.score(X_train, y_train),  
                                          DT.score(X_test, y_test),
                                          elapsed_time_DT, 
                                          test_time_DT,
                          X.columns[np.argmax(DT.feature_importances_)], 
                          X.columns[np.argmax(DT.feature_importances_)], 
                          X.columns[np.argmax(DT.feature_importances_)], 
                          X.columns[np.argmax(DT.feature_importances_)], 
                          X.columns[np.argmax(DT.feature_importances_)], 
                          np.max(DT.feature_importances_)]
df_result.loc[8] = ['Random Forest Classifier', RF.score(X_train, y_train),  
                                          RF.score(X_test, y_test), 
                                          elapsed_time_RF,
                                          test_time_RF,
                             X.columns[np.argmax(RF.feature_importances_)],
                             X.columns[np.argmax(RF.feature_importances_)],
                             X.columns[np.argmax(RF.feature_importances_)],
                             X.columns[np.argmax(RF.feature_importances_)],
                             X.columns[np.argmax(RF.feature_importances_)],
                             np.max(RF.feature_importances_)]
df_result.loc[9] = ['Gradient Boosting Classifier', GB.score(X_train, y_train),  
                                              GB.score(X_test, y_test), 
                                              elapsed_time_GB, 
                                              test_time_GB,
                          X.columns[np.argmax(GB.feature_importances_)],
                          X.columns[np.argmax(GB.feature_importances_)],
                          X.columns[np.argmax(GB.feature_importances_)],
                          X.columns[np.argmax(GB.feature_importances_)],
                          X.columns[np.argmax(GB.feature_importances_)],
                          np.max(GB.feature_importances_)]
df_result

Fig 5. Accuracy and Top predictors of each model.



In summary, training the plant classifier using the extracted features using traditional image processing techniques in the image, we achieved a validation accuracy of 96.9% using the Gradient Boosting classifier. Using each segmented leaf's regional properties, we can extract the rich features like area, convex_area, bbox_area, eccentricity, equivalent_diameter, extent, filled_area, mean_intensity, perimeter, and solidity.

65 views0 comments

Recent Posts

See All

Comments


Post: Blog2_Post
bottom of page