ЗА КУЛИСАМИ ОБРАБОТКИ ИЗОБРАЖЕНИЙ (7 ИЗ 9)

Обработка изображений с использованием Python — реализация машинного обучения

В этой статье мы отправляемся в увлекательное путешествие в мир обработки изображений, уделяя особое внимание реализации методов машинного обучения для классификации листьев. Наша цель — продемонстрировать, как алгоритмы машинного обучения можно использовать для анализа коллекции фотографий листьев, обеспечивая точную классификацию и предоставляя ценную информацию о ботанической сфере.

Суть машинного обучения в обработке изображений:

Машинное обучение произвело революцию в области обработки изображений, позволив компьютерам изучать закономерности и делать прогнозы на основе визуальных данных. В контексте классификации листьев алгоритмы машинного обучения можно обучать на наборе данных помеченных изображений листьев, чтобы распознавать и классифицировать различные виды на основе их уникальных особенностей. Используя возможности машинного обучения, мы можем автоматизировать процесс идентификации листьев и внести свой вклад в ботанические исследования и усилия по сохранению.

Для начала давайте импортируем все необходимые библиотеки.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from math import isclose
from fractions import Fraction
from skimage import data, io, filters, util, color
from skimage.morphology import (disk, square, rectangle, skeletonize,
                                erosion, dilation, opening, closing,
                                binary_erosion, binary_dilation,
                                binary_opening, binary_closing)
from skimage.measure import label, regionprops
from skimage.io import imread, imshow
from skimage.color import rgb2gray
from tqdm.notebook import tqdm, trange

from imblearn.over_sampling import SMOTE


from sklearn.preprocessing import (StandardScaler, MinMaxScaler,
                                    OneHotEncoder, OrdinalEncoder)
from sklearn.compose import ColumnTransformer
from sklearn.model_selection import train_test_split, GridSearchCV, KFold
from sklearn.pipeline import make_pipeline, Pipeline
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import LinearSVC
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier

import cv2
import torch
from tqdm.notebook import tqdm, trange
from PIL import Image
from transformers import YolosFeatureExtractor, YolosForObjectDetection
from yoloface import face_analysis
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as T
from torch.utils.data import DataLoader
from torchvision import models, transforms, datasets
import matplotlib.pyplot as plt
from PIL import Image, ImageDraw, ImageFont
import time
import math
import shutil
import copy
from pathlib import Path
from torchsummary import summary
import warnings

warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.filterwarnings("ignore", category=UserWarning)

# Empty the GPU memory cache
torch.cuda.empty_cache()

Теперь давайте посмотрим образец изображения!

image_raw = io.imread('leaves/plantA_1.jpg')
fig, ax = plt.subplots()
ax.imshow(image_raw,cmap='gray');

gray_leaves = rgb2gray(image_raw[:,:,:3])
binary_leaves = util.invert(gray_leaves > 0.5)
plt.figure()
plt.imshow(binary_leaves, cmap='gray')
plt.show()

Сегментация

label_leaves = label(binary_leaves)
plt.figure()
plt.imshow(label_leaves);

raw_props = regionprops(label_leaves)[1:] # remove the background class
clean_props = [prop for prop in raw_props if prop.area > 10] 

Я буду использовать эти свойства regionprops, чтобы различать листья для модели ML.

  1. область
  2. периметр
  3. эксцентриситет
  4. прочность
  5. степень

Извлечение признаков объекта

def get_class(fpath):
    '''
    Extracts the class of the leaves from the filepath.
    '''
    return fpath.split('/')[1].split('.')[0].split('_')[0]

leaves_data = []

folder_path = 'leaves'

for filename in tqdm(os.listdir(folder_path)):
    file_path = os.path.join(folder_path, filename)
    
    if os.path.isfile(file_path):
        
        image_raw = io.imread(file_path)
        gray_leaves = rgb2gray(image_raw[:,:,:3])
        binary_leaves = util.invert(gray_leaves > 0.5)
        
        label_leaves = label(binary_leaves)
        
        raw_props = regionprops(label_leaves)[1:] 
        clean_props = [prop for prop in raw_props if prop.area > 10] 
        for prop in clean_props:
            
            leaves_data.append({'area': prop.area,
                                'perim': prop.perimeter,
                                'ecc': prop.eccentricity,
                                'solid': prop.solidity,
                                'extent': prop.extent,
                                'label': get_class(file_path)
                               })
            
df_leaves = pd.DataFrame(data=leaves_data)
display(df_leaves)

КНН

pipeline = Pipeline(steps=[('scl', StandardScaler()),
                                ('model', KNeighborsClassifier())])


param_grid = {'model__n_neighbors': list(range(5, 31, 5)),
             }

scoring = 'accuracy'
cv = 3

grid_search = GridSearchCV(estimator=pipeline,
                            param_grid=param_grid,
                            scoring=scoring,
                            cv=cv,
                            n_jobs=-1,
                            verbose=1,
                            return_train_score=True)

grid_search.fit(X_trainval, y_trainval)
# grid_search.fit(X_trainval_res, y_trainval_res)

val_acc = grid_search.best_score_
train_acc = grid_search.cv_results_[
    'mean_train_score'][grid_search.best_index_]
hold_acc = grid_search.score(X_hold, y_hold)

print(f'\nKNN Classifier\n\nTrain score: {train_acc:.3f}\nVal score: {val_acc:.3f}\n\nTest score: {hold_acc:.3f}')
Fitting 3 folds for each of 6 candidates, totalling 18 fits

KNN Classifier

Train score: 0.825
Val score: 0.803

Test score: 0.714

Логистическая регрессия

pipeline = Pipeline(steps=[('scl', StandardScaler()),
                                ('model', LogisticRegression())])


param_grid = {'model__C': [0.1, 1, 5, 10, 100, 1000],
              'model__penalty': ['l2'],
              'model__solver': ['liblinear'],
              'model__random_state': [69]
             }

scoring = 'accuracy'
cv = 3

grid_search = GridSearchCV(estimator=pipeline,
                            param_grid=param_grid,
                            scoring=scoring,
                            cv=cv,
                            n_jobs=-1,
                            verbose=1,
                            return_train_score=True)

grid_search.fit(X_trainval, y_trainval)
# grid_search.fit(X_trainval_res, y_trainval_res)

val_acc = grid_search.best_score_
train_acc = grid_search.cv_results_[
    'mean_train_score'][grid_search.best_index_]
hold_acc = grid_search.score(X_hold, y_hold)

print(f'\nLogistic Regression\n\nTrain score: '
      f'{train_acc:.3f}\nVal score: {val_acc:.3f}'
      f'\n\nTest score: {hold_acc:.3f}')
Fitting 3 folds for each of 6 candidates, totalling 18 fits

Logistic Regression

Train score: 0.865
Val score: 0.831

Test score: 0.714

GBM

pipeline = Pipeline(steps=[('scl', StandardScaler()),
                                ('model', GradientBoostingClassifier())])


param_grid = {'model__learning_rate': [0.001],
              'model__max_features': [3, 4, 5],
              'model__max_depth': [10, 20],
              'model__random_state': [69]
             }

scoring = 'accuracy'
cv = 3

grid_search = GridSearchCV(estimator=pipeline,
                            param_grid=param_grid,
                            scoring=scoring,
                            cv=cv,
                            n_jobs=-1,
                            verbose=1,
                            return_train_score=True)

grid_search.fit(X_trainval, y_trainval)
# grid_search.fit(X_trainval_res, y_trainval_res)

val_acc = grid_search.best_score_
train_acc = grid_search.cv_results_[
    'mean_train_score'][grid_search.best_index_]
hold_acc = grid_search.score(X_hold, y_hold)

print(f'\GBM\n\nTrain score: '
      f'{train_acc:.3f}\nVal score: {val_acc:.3f}'
      f'\n\nTest score: {hold_acc:.3f}')
Fitting 3 folds for each of 6 candidates, totalling 18 fits
GBM

Train score: 0.998
Val score: 0.798

Test score: 0.821

Глубокое обучение

Я также попытаюсь реализовать глубокое обучение для классификации листьев.

output_folder = 'cropped_leaves'

# Delete the 'cropped_leaves' folder if it exists
if os.path.exists(output_folder):
    shutil.rmtree(output_folder)

if not os.path.exists(output_folder):
    os.makedirs(output_folder)

class_count = {}
leaf_count = 0
classes = []

for filename in tqdm(os.listdir(folder_path)):
    file_path = os.path.join(folder_path, filename)
    
    if os.path.isfile(file_path):
        leaf_class = get_class(file_path)
        if leaf_class not in classes:
            classes.append(leaf_class)
        if leaf_class not in class_count:
            class_count[leaf_class] = 0
        
        image_raw = io.imread(file_path)
        gray_leaves = rgb2gray(image_raw[:,:,:3])
        binary_leaves = util.invert(gray_leaves > 0.5)
        
        label_leaves = label(binary_leaves)
        
        raw_props = regionprops(label_leaves)[1:] # remove the background class
        clean_props = [prop for prop in raw_props if prop.area > 10] # just the leaves, remove specks
        
        image = Image.open(file_path)
        
        for prop in clean_props:
            class_count[leaf_class] += 1
            leaf_count += 1
            
            cropped_image = image.crop((prop.bbox[1], # left
                                        prop.bbox[0], # top
                                        prop.bbox[3], # right
                                        prop.bbox[2]) # bottom
                                      )
            class_folder = os.path.join(output_folder, leaf_class)
            
            if not os.path.exists(class_folder):
                os.makedirs(class_folder)
            
            new_filename = f"leaf_{leaf_count}.jpg"
            new_file_path = os.path.join(class_folder, new_filename)
            cropped_image.save(new_file_path)

# Rename files to ensure monotonically labeled filenames

for leaf_class, count in class_count.items():
    class_folder = os.path.join(output_folder, leaf_class)
    for i, filename in enumerate(os.listdir(class_folder), start=1):
        file_path = os.path.join(class_folder, filename)
        new_filename = f"leaf_{i}.jpg"
        new_file_path = os.path.join(class_folder, new_filename)
        os.rename(file_path, new_file_path)
def create_dataset(src, dst, range_, class_):
    """Copy images of class class_ within range_ from src to dst.
    
    Parameters
    ----------
    src : str
        source directory
    dst : str
        destination directory
    range_ : tuple
        tuple of min and max image index to copy
    class_ : str
        image class ('plantA', 'plantB', ...)
    """
    if os.path.exists(dst):
        shutil.rmtree(dst)
    os.makedirs(dst)
    fnames = [f'leaf_{i}.jpg' for i in range(*range_)]
    for fname in fnames:
        src_file = os.path.join(src, fname)
        dst_file = os.path.join(dst, fname)
        shutil.copyfile(src_file, dst_file)
# looping through create_dataset for each class
class_counts = [len(os.listdir(os.path.join(output_folder, class_))) for class_ in classes]  # Number of pictures for each class

for class_, count in zip(classes, class_counts):
    src = output_folder

    # Define the custom ranges for each split based on the available count
    train_range = (1, int(0.7 * count)) # 70% for training
    validation_range = (int(0.7 * count) + 1, int(0.9 * count)) # 20% for validation
    test_range = (int(0.9 * count) + 1, count) # 10% for testing
    
    dst = f'cropped_leaves/train/{class_}'
    create_dataset(src+'/'+class_, dst, range_=train_range, class_=class_)

    dst = f'cropped_leaves/validation/{class_}'
    create_dataset(src+'/'+class_, dst, range_=validation_range, class_=class_)

    dst = f'cropped_leaves/test/{class_}'
    create_dataset(src+'/'+class_, dst, range_=test_range, class_=class_)
data_path = Path(output_folder)

data_path_list = list(data_path.glob("*/*.jpg"))

train_dir = data_path / "train"
val_dir = data_path / "validation"
test_dir = data_path / "test"
train_data = datasets.ImageFolder(root=train_dir,
                                  transform=transforms.Compose(
                                      [transforms.Resize((224, 224)),
                                       transforms.ToTensor()]))

all_images = torch.stack([img_t for img_t, _ in train_data], dim=3)
means = all_images.view(3, -1).mean(dim=1).numpy()
stds = all_images.view(3, -1).std(dim=1).numpy()
data_transforms = {
    'train': transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.ToTensor(),
        transforms.Normalize(mean=means, std=stds)
    ]),
    'validation': transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.ToTensor(),
        transforms.Normalize(mean=means, std=stds)
    ]),
    'test': transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.ToTensor(),
        transforms.Normalize(mean=means, std=stds)
    ])
}

data_dir = data_path

image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
                                          data_transforms[x])
                  for x in ['train', 'validation', 'test']}

dataloaders = {x: DataLoader(image_datasets[x], batch_size=1,
                             shuffle=True)
              for x in ['train', 'validation', 'test']}

dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'validation', 'test']}

class_names = image_datasets['train'].classes
class_names
['plantA', 'plantB', 'plantC', 'plantD', 'plantE']

Точная настройка VGG-19

# Set device
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# Load train, validation, and test datasets
train_data = image_datasets['train']
val_data = image_datasets['validation']
test_data = image_datasets['test']

# Define data loaders
train_loader = dataloaders['train']
valid_loader = dataloaders['validation']
test_loader = dataloaders['test']

# Load the pretrained VGG19 model
model = models.vgg19(pretrained=True)

# Freeze the parameters of the pretrained layers
for param in model.parameters():
    param.requires_grad = False

# Modify the last fully connected layer to match the number of classes
num_classes = len(class_names)
model.classifier[6] = nn.Linear(4096, num_classes)

# Move the model to the appropriate device
model = model.to(device)

# Define loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# Train the model
num_epochs = 30
best_valid_loss = float('inf')

for epoch in range(num_epochs):
    train_loss = 0.0
    valid_loss = 0.0
    
    # Training
    model.train()
    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)
        
        optimizer.zero_grad()
        
        outputs = model(images)
        loss = criterion(outputs, labels)
        
        loss.backward()
        optimizer.step()
        
        train_loss += loss.item() * images.size(0)
    
    # Validation
    model.eval()
    with torch.no_grad():
        for images, labels in valid_loader:
            images, labels = images.to(device), labels.to(device)
            
            outputs = model(images)
            loss = criterion(outputs, labels)
            
            valid_loss += loss.item() * images.size(0)
    
    train_loss = train_loss / len(train_loader.dataset)
    valid_loss = valid_loss / len(valid_loader.dataset)
    
    print(f"Epoch: {epoch+1}/{num_epochs}, Train Loss: {train_loss:.4f}, "
          f"Valid Loss: {valid_loss:.4f}")
    
    # Save the best model based on validation loss
    if valid_loss < best_valid_loss:
        best_valid_loss = valid_loss
        torch.save(model.state_dict(), 'best_leaves_model.pt')

# Test the model
model.load_state_dict(torch.load('best_leaves_model.pt'))
model.eval()
correct = 0
total = 0

with torch.no_grad():
    for images, labels in test_loader:
        images, labels = images.to(device), labels.to(device)
        
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)
        
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

accuracy = 100 * correct / total
print(f"Test Accuracy: {accuracy:.2f}%")
Epoch: 1/30, Train Loss: 0.4749, Valid Loss: 0.1140
Epoch: 2/30, Train Loss: 0.1253, Valid Loss: 0.1190
Epoch: 3/30, Train Loss: 0.1363, Valid Loss: 0.1185
Epoch: 4/30, Train Loss: 0.0705, Valid Loss: 0.0598
Epoch: 5/30, Train Loss: 0.0796, Valid Loss: 0.0407
Epoch: 6/30, Train Loss: 0.0894, Valid Loss: 0.0853
Epoch: 7/30, Train Loss: 0.0718, Valid Loss: 0.0495
Epoch: 8/30, Train Loss: 0.0468, Valid Loss: 0.0574
Epoch: 9/30, Train Loss: 0.0561, Valid Loss: 0.0269
Epoch: 10/30, Train Loss: 0.0543, Valid Loss: 0.0370
Epoch: 11/30, Train Loss: 0.0639, Valid Loss: 0.1421
Epoch: 12/30, Train Loss: 0.0657, Valid Loss: 0.0481
Epoch: 13/30, Train Loss: 0.0876, Valid Loss: 0.0563
Epoch: 14/30, Train Loss: 0.0704, Valid Loss: 0.0673
Epoch: 15/30, Train Loss: 0.0948, Valid Loss: 0.0335
Epoch: 16/30, Train Loss: 0.0389, Valid Loss: 0.0402
Epoch: 17/30, Train Loss: 0.0598, Valid Loss: 0.0882
Epoch: 18/30, Train Loss: 0.0962, Valid Loss: 0.0865
Epoch: 19/30, Train Loss: 0.0872, Valid Loss: 0.0295
Epoch: 20/30, Train Loss: 0.0600, Valid Loss: 0.0713
Epoch: 21/30, Train Loss: 0.0445, Valid Loss: 0.1138
Epoch: 22/30, Train Loss: 0.0997, Valid Loss: 0.2957
Epoch: 23/30, Train Loss: 0.0483, Valid Loss: 0.0287
Epoch: 24/30, Train Loss: 0.0539, Valid Loss: 0.0495
Epoch: 25/30, Train Loss: 0.0744, Valid Loss: 0.0827
Epoch: 26/30, Train Loss: 0.1011, Valid Loss: 0.1187
Epoch: 27/30, Train Loss: 0.0960, Valid Loss: 0.2000
Epoch: 28/30, Train Loss: 0.0857, Valid Loss: 0.1095
Epoch: 29/30, Train Loss: 0.0709, Valid Loss: 0.0857
Epoch: 30/30, Train Loss: 0.0584, Valid Loss: 0.1196
Test Accuracy: 96.00%
model.load_state_dict(torch.load('best_leaves_model.pt'))
model.eval()

label_map = {k: v for k, v in enumerate(image_datasets['train'].classes)}

fig, ax = plt.subplots(5, 5, figsize=(25, 25))
ax = ax.flatten()
plt.suptitle('Test set predictions vs ground truth', fontsize=24)
plt.subplots_adjust(wspace=0.1, hspace=0.3)

for idx, (images, labels) in enumerate(test_loader):
    images = images.to(device)
    labels = labels.to(device)
        
    outputs = model(images)
    _, predicted = torch.max(outputs.data, 1)

    # Map the predicted class index to the corresponding label
    predicted_labels = [label_map[p.item()] for p in predicted]

    # Convert the labels to a list of strings
    actual_labels = [label_map[l.item()] for l in labels]

    # Iterate over the images, actual labels, and predicted labels
    for image, actual_label, predicted_label in zip(images,
                                      actual_labels, predicted_labels):
        # Move the image to the CPU and convert it to a NumPy array
        image = image.cpu().numpy()
        image = np.transpose(image, (1, 2, 0))

        # Clip the image data to the valid range [0, 1]
        image = np.clip(image, 0, 1)

        # Display the image
        ax[idx].imshow(image)
        ax[idx].axis('off')

        # Add the actual and predicted labels as title
        if actual_label == predicted_label:
            ax[idx].set_title(f"Actual: {actual_label}\nPredicted: "
                              f"{predicted_label}\nCORRECT", fontsize=16)
        else:
            ax[idx].set_title(f"Actual: {actual_label}\nPredicted: "
                              f"{predicted_label}\nWRONG", fontsize=16)
        fig.show()

Значение и применение машинного обучения в классификации листьев

Применение машинного обучения в классификации листьев имеет важные последствия в различных областях. От ботанических исследований и идентификации видов до экологических исследований и обнаружения болезней растений методы классификации листьев на основе машинного обучения предлагают бесценную информацию и возможности автоматизации. Используя крупномасштабные наборы данных и мощные алгоритмы, мы можем улучшить наше понимание биоразнообразия растений и внести свой вклад в устойчивые экологические методы.

Заключительные мысли

Благодаря внедрению методов машинного обучения в обработку изображений листьев мы стали свидетелями преобразующих возможностей этого подхода. Обучая модели точной классификации изображений листьев, мы можем автоматизировать и оптимизировать процесс идентификации листьев, открывая возможности для ботанических исследований и усилий по сохранению. Сила машинного обучения заключается в его способности учиться на огромном количестве визуальных данных, что делает его бесценным инструментом в области классификации листьев и не только. Итак, давайте воспользуемся потенциалом машинного обучения в обработке изображений и отправимся в путешествие, чтобы раскрыть чудеса нашего ботанического мира с помощью автоматической классификации листьев.

Ссылки

Бенджур Эммануэль Л. Борха. 2023. Обработка изображений (MSDS2023). Азиатский институт менеджмента.

Понравилась ли вам эта статья?

Не стесняйтесь связаться со мной на LinkedIn. Давайте установим профессиональную сеть и оставайтесь на связи. Кроме того, вы можете изучить мой профиль GitHub, чтобы узнать о полной реализации этого проекта и узнать о других интересных проектах, над которыми я работал.