0

3. Предварительная обработка данных

В главе 2 мы исследовали данные набора данных Face Mask Detection. В этой главе мы продолжим предварительную обработку данных.

Популярные наборы данных содержат десятки тысяч изображений. Но не все наборы данных такие. Итак, как нам обучить ограниченный набор данных?

Если набор данных закончился это не означает что нужно искать новые изображения. Это связано с тем, что дополнительные изображения в различных состояниях могут быть получены с помощью увеличения (расширения) данных.

рисунок 3-1 пример создания нескольких изображений из исходного

Рисунок 3-1 представляет собой теннисный мяч, который выглядит так же, как человеческий глаз. Но модель глубокого обучения видит все три теннисных мяча по-разному. Таким образом, мы можем извлекать несколько данных, изменяя одну фотографию.

В разделе 3.1 мы рассмотрим модуль torchvision.transforms используемый для расширения набора изображений. Этот модуль, официально предоставленный PyTorch, обеспечивает высокую скорость обработки данных. Также мы будем использовать библиотеку с открытым исходным кодом .albumentations от OpenCV

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

Поэтому в разделе 3.2 мы будем практиковать увеличение ограничивающей рамки с помощью albumentations. Наконец, в разделе 3.3 мы разделим данные на обучающие данные и тестовые данные.

3.1. Практика увеличения

Чтобы попрактиковаться в расширении, мы будем использовать код из раздела 2.1 для загрузки данных.

!git clone https://github.com/Pseudo-Lab/Tutorial-Book-Utils
!python Tutorial-Book-Utils/PL_data_loader.py --data FaceMaskDetection
!unzip -q Face Mask Detection.zip

Обновите до последней версии библиотеку albumentations.  Вы можете обновить определенный модуль с помощью команды pip install —upgrade.

pip install --upgrade albumentations

Чтобы визуализировать вывод albumentations, мы используем схематический код ограничивающей рамки из раздела 2.3.

import os
import glob
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import matplotlib.patches as patches
import torch as torch
from bs4 import BeautifulSoup
def generate_box(obj): xmin = float(obj.find('xmin').text) ymin = float(obj.find('ymin').text) xmax = float(obj.find('xmax').text) ymax = float(obj.find('ymax').text) return [xmin, ymin, xmax, ymax] def generate_label(obj): if obj.find('name').text == "with_mask": return 1 elif obj.find('name').text == "mask_weared_incorrect": return 2 return 0 def generate_target(file): with open(file) as f: data = f.read() soup = BeautifulSoup(data, "html.parser") objects = soup.find_all("object") num_objs = len(objects) boxes = [] labels = [] for i in objects: boxes.append(generate_box(i)) labels.append(generate_label(i)) boxes = torch.as_tensor(boxes, dtype=torch.float32) labels = torch.as_tensor(labels, dtype=torch.int64) target = {} target["boxes"] = boxes target["labels"] = labels return target def plot_image_from_output(img, annotation): img = img.permute(1,2,0) fig,ax = plt.subplots(1) ax.imshow(img) for idx in range(len(annotation["boxes"])): xmin, ymin, xmax, ymax = annotation["boxes"][idx] if annotation['labels'][idx] == 0 : rect = patches.Rectangle((xmin,ymin),(xmax-xmin),(ymax-ymin),linewidth=1,edgecolor='r',facecolor='none') elif annotation['labels'][idx] == 1 : rect = patches.Rectangle((xmin,ymin),(xmax-xmin),(ymax-ymin),linewidth=1,edgecolor='g',facecolor='none') else : rect = patches.Rectangle((xmin,ymin),(xmax-xmin),(ymax-ymin),linewidth=1,edgecolor='orange',facecolor='none') ax.add_patch(rect) plt.show()

Вы можете увидеть, что есть отличия от функций, которые мы писали в главе 2.  Так функция torch.as_tensor была добавлена ​​к функции generate_target, чтобы подготовить тензорную операцию к будущему обучению модели глубокого обучения.

Кроме того, функция plot_image, представленная в главе 2, считывает изображение из пути к файлу. С другой стороны, функция plot_image_from_output визуализирует преобразованное изображение с помощью torch.tensor. В PyTorch изображения выражаются как [каналы, высота, ширина], тогда как в matplotlib изображения выражаются как [высота, ширина, каналы]. Поэтому, используя функцию перестановки, которая изменяет порядок каналов, она изменяет порядок каналов, ожидаемый matplotlib.

3.1.1. Torchvision Transforms

Чтобы попрактиковаться с torchvision.transforms, мы сначала определим класс TorchvisionDataset. Класс TorchvisionDataset загружает изображение с помощью метода __getitem__, а затем приступает к дополнению данных. Увеличение выполняется в соответствии с правилом увеличения, хранящимся в параметре преобразования. Чтобы измерить время, используйте функцию time для измерения времени и, наконец, верните image, label и total_time.

from PIL import Image
import cv2
import numpy as np
import time
import torch
import torchvision
from torch.utils.data import Dataset
from torchvision import transforms
import albumentations
import albumentations.pytorch
from matplotlib import pyplot as plt
import os
import random

class TorchvisionMaskDataset(Dataset):
    def __init__(self, path, transform=None):
        self.path = path
        self.imgs = list(sorted(os.listdir(self.path)))
        self.transform = transform
        
    def __len__(self):
        return len(self.imgs)

    def __getitem__(self, idx):
        file_image = self.imgs[idx]
        file_label = self.imgs[idx][:-3] + 'xml'
        img_path = os.path.join(self.path, file_image)
        
        if 'test' in self.path:
            label_path = os.path.join("test_annotations/", file_label)
        else:
            label_path = os.path.join("annotations/", file_label)

        img = Image.open(img_path).convert("RGB")
        
        target = generate_target(label_path)
        
        start_t = time.time()
        if self.transform:
            img = self.transform(img)

        total_time = (time.time() - start_t)

        return img, target, total_time

Давайте попрактикуемся с образом augmentation, используя функцию, предоставляемую файлом. Сделав размер изображения (300, 300), мы обрежем его до размера 224. Затем мы случайным образом изменим яркость изображения (brightness), контрастность (contrast), насыщенность (saturation), оттенок (hue). Наконец, после применения инверсии изображения мы приступим к преобразованию в tensor.

torchvision_transform = transforms.Compose([
    transforms.Resize((300, 300)), 
    transforms.RandomCrop(224),
    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.2),
    transforms.RandomHorizontalFlip(p = 1),
    transforms.ToTensor(),
])

torchvision_dataset = TorchvisionMaskDataset(
    path = 'images/',
    transform = torchvision_transform
)

Вы можете изменить размер изображения с помощью функции Resize, предоставляемой transforms, и обрезать изображение с помощью функции RandomCrop. Функция ColorJitter изменяет яркость, контрастность, насыщенность, оттенок и т. д. случайным образом, а RandomHorizontalFlip выполняет переворот влево-вправо с определенной вероятностью p. Выполните приведенный ниже код, чтобы сравнить изображения до и после изменения.

only_totensor = transforms.Compose([transforms.ToTensor()])

torchvision_dataset_no_transform = TorchvisionMaskDataset(
    path = 'images/',
    transform = only_totensor
)

img, annot, transform_time = torchvision_dataset_no_transform[0]
print('transforms до применения')
plot_image_from_output(img, annot) 
Изображение до преобразования

Изображение до преобразования

img, annot, transform_time = torchvision_dataset[0]

print('transforms после применения')
plot_image_from_output(img, annot)
Изображение после преобразования

Изображение после преобразования

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

В задаче классификации изображений значение метки фиксируется даже при изменении изображения, но в задаче обнаружения объектов значение метки также должно меняться по мере изменения изображения. В разделе 3.2 мы увидим, как решить эти проблемы. Прежде всего, в разделе 3.1 мы продолжим сравнивать модули torchvision и albumentations. Я рассчитаю время, необходимое для преобразования изображения в torchvision_dataset, и измерю время, необходимое для его повторения 100 раз, используя приведенный ниже код.

total_time = 0
for i in range(100):
  sample, _, transform_time = torchvision_dataset[0]
  total_time += transform_time

print("torchvision time: {} ms".format(total_time*10))
torchvision time: 10.959246158599854 ms

Видно, что для выполнения 100 преобразований изображений потребовалось от 10 до 12 мс. В следующем разделе мы рассмотрим скорость augmentation модуля albumentations.

3.1.2. Модуль Albumentations

Ранее в разделе 3.1.1 мы измерили скорость преобразования torchvision.transforms. В этом разделе мы рассмотрим еще один модуль !augmentations модуля !albumentations. Как и в случае с torchvision, давайте сначала определим наш класс набора данных. AlmentationDataset имеет структуру, аналогичную TorchVisionDataset. Я использую модуль cv2 для чтения изображения и преобразования его в RGB. И после преобразования изображения возвращается результат.

class AlbumentationsDataset(Dataset):
    def __init__(self, path, transform=None):
        self.path = path
        self.imgs = list(sorted(os.listdir(self.path)))
        self.transform = transform
        
    def __len__(self):
        return len(self.imgs)

    def __getitem__(self, idx):
        file_image = self.imgs[idx]
        file_label = self.imgs[idx][:-3] + 'xml'
        img_path = os.path.join(self.path, file_image)

        if 'test' in self.path:
            label_path = os.path.join("test_annotations/", file_label)
        else:
            label_path = os.path.join("annotations/", file_label)
        
        # Read an image with OpenCV
        image = cv2.imread(img_path)
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

        target = generate_target(label_path)

        start_t = time.time()
        if self.transform:
            augmented = self.transform(image=image)
            total_time = (time.time() - start_t)
            image = augmented['image']
        
            
        return image, target, total_time

Для сравнения скорости с torchvision.transform будем использовать те же функции Resize, RandomCrop, ColorJitter и HorizontalFlip. Затем давайте сравним изображения до и после изменения.

# Same transform with torchvision_transform
albumentations_transform = albumentations.Compose([
    albumentations.Resize(300, 300), 
    albumentations.RandomCrop(224, 224),
    albumentations.ColorJitter(p=1), 
    albumentations.HorizontalFlip(p=1), 
    albumentations.pytorch.transforms.ToTensor()
])
img, annot, transform_time = torchvision_dataset_no_transform[0]
plot_image_from_output(img, annot)
Изображение до преобразования

Изображение до преобразования

albumentation_dataset = AlbumentationsDataset(
    path = 'images/',
    transform = albumentations_transform
)

img, annot, transform_time = albumentation_dataset[0]
plot_image_from_output(img, annot)
Изображение после преобразования

Изображение после преобразования

Как и в случае с torchvision.transforms, изображение было преобразовано, но ограничивающая рамка осталась неизменной. Чтобы измерить скорость, мы будем измерять время после применения 100 раз метода albumentation .

total_time = 0
for i in range(100):
    sample, _, transform_time = albumentation_dataset[0]
    total_time += transform_time

print("albumentations time/sample: {} ms".format(total_time*10))

albumentations time/sample: 2.41302490234375 ms

Видно, что для выполнения 100 преобразований изображений потребовалось от 2,0 до 2,5 мс. Вы можете видеть, что это примерно в 4 раза быстрее по сравнению с torchvision.transforms.

3.1.3. Комбинации Augmentation на основе вероятности¶

!Albmentations не только быстрее, чем torchvision.transforms, но и предлагает новые функции. В этом разделе мы рассмотрим функцию OneOf, предоставляемую !Albumentations. Эта функция получает функции !augmentation в списке на основе заданного значения вероятности. В дополнение к значению вероятности самого значения списка вместе рассматривается значение вероятности соответствующей функции, чтобы определить, выполнять ли ее. Каждая из приведенных ниже функций OneOf имеет вероятность выбора 1. 3 функциям !albumentation внутри каждой функции также присваивается значение вероятности 1, поэтому можно видеть, что одна из 3 функций выбирается и выполняется с вероятностью практически 1/3. Различные augmentation возможны путем корректировки значения вероятности таким образом.

albumentations_transform_oneof = albumentations.Compose([
    albumentations.Resize(300, 300), 
    albumentations.RandomCrop(224, 224),
    albumentations.OneOf([
                          albumentations.HorizontalFlip(p=1),
                          albumentations.RandomRotate90(p=1),
                          albumentations.VerticalFlip(p=1)            
    ], p=1),
    albumentations.OneOf([
                          albumentations.MotionBlur(p=1),
                          albumentations.OpticalDistortion(p=1),
                          albumentations.GaussNoise(p=1)                 
    ], p=1),
    albumentations.pytorch.ToTensor()
])

Ниже показан результат десятикратного применения к изображению albumentations_transform_oneof.

albumentation_dataset_oneof = AlbumentationsDataset(
    path = 'images/',
    transform = albumentations_transform_oneof
)

num_samples = 10
fig, ax = plt.subplots(1, num_samples, figsize=(25, 5))
for i in range(num_samples):
  ax[i].imshow(transforms.ToPILImage()(albumentation_dataset_oneof[0][0]))
  ax[i].axis('off')

3.2. Ограничивающая рамка в Augmentation

При выполнении augmentation изображения, используемого для построения модели обнаружения объектов, важно не только преобразование изображения, но и преобразование ограничивающей рамки. Как мы видели в разделе 3.1, если ограничительная рамка не трансформируется вместе с изображением, обучение модели не будет выполняться должным образом, потому что ограничительная рамка определяет неправильное место положение. Расширение ограничивающей рамки возможно с помощью параметра bbox_params в функции Compose, предоставляемой Albumentations.

Во-первых, мы создадим новый класс набора данных, используя приведенный ниже код. Часть transform класса AlmentationsDataset, указанная в разделе 3.1.2, была изменена. Поскольку  transform выполняется не только для изображений, но и для ограничивающих рамок, мы исправили необходимые входные и выходные значения.

class BboxAugmentationDataset(Dataset):
    def __init__(self, path, transform=None):
        self.path = path
        self.imgs = list(sorted(os.listdir(self.path)))
        self.transform = transform
        
    def __len__(self):
        return len(self.imgs)

    def __getitem__(self, idx):
        file_image = self.imgs[idx]
        file_label = self.imgs[idx][:-3] + 'xml'
        img_path = os.path.join(self.path, file_image)

        if 'test' in self.path:
            label_path = os.path.join("test_annotations/", file_label)
        else:
            label_path = os.path.join("annotations/", file_label)
        
        image = cv2.imread(img_path)
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

        target = generate_target(label_path)

        if self.transform:
            transformed = self.transform(image = image, bboxes = target['boxes'], labels = target['labels'])
            image = transformed['image']
            target = {'boxes':transformed['bboxes'], 'labels':transformed['labels']}
        
            
        return image, target

Далее давайте определим преобразование с помощью функции albumentations.Compose. Сначала мы сделаем переворот влево-вправо, а затем повернемся в диапазоне от -90 до 90 градусов. Введите объект albumentations.BboxParams в параметр bbox_params, чтобы также преобразовать ограничивающую рамку. Набор данных Face Mask Detection имеет обозначение ограничивающей рамки как xmin, ymin, xmax, ymax, что совпадает с обозначением pascal_voc. Поэтому введите pascal_voc в параметр формата. Кроме того, когда выполняется transform, введите labels в  label_field, чтобы сохранить значение класса для каждого объекта в параметре labels.

bbox_transform = albumentations.Compose(
    [albumentations.HorizontalFlip(p=1),
     albumentations.Rotate(p=1),
     albumentations.pytorch.transforms.ToTensor()],
    bbox_params=albumentations.BboxParams(format='pascal_voc', label_fields=['labels']),
)

Теперь давайте активируем класс BboxAugmentationDataset, чтобы проверить вывод augmentation.

bbox_transform_dataset = BboxAugmentationDataset(
    path = 'images/',
    transform = bbox_transform
)

img, annot = bbox_transform_dataset[0]
plot_image_from_output(img, annot)
Изображение после преобразования

Изображение после преобразования с преобразованием рамки

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

3.3. Разделение данных

Для построения модели ИИ необходимы обучающие данные и тестовые данные. Обучающие данные используются для обучения модели, а тестовые данные используются для оценки модели. Тестовые данные не должны пересекаться с обучающими данными. Давайте разделим данные, полученные в разделе 3.1, на обучающие данные и тестовые данные. Во-первых, давайте проверим общее количество данных с помощью кода ниже.

print(len(os.listdir('annotations')))
print(len(os.listdir('images')))
853
853

Вы можете видеть, что всего 853 изображения. В общем случае отношение обучающих данных к тестовым данным принимается равным 7:3. На этот раз общее количество наборов данных невелико, поэтому возьмем соотношение 8:2. Чтобы использовать 170 из 853 данных в качестве тестовых данных, мы переместим данные в отдельную папку. Сначала создайте папку для тестовых данных с помощью команды mkdir.

!mkdir test_images
!mkdir test_annotations

Если вы запустите приведенный выше код, вы увидите, что папки test_images и папки test_annotations созданы. Теперь мы переместим по 170 файлов из папки изображений и папки аннотаций во вновь созданную папку. Мы будем использовать функцию выборки в модуле random для случайного извлечения числа и использования его в качестве значения индекса.

import random
random.seed(1234)
idx = random.sample(range(853), 170)
print(len(idx))
print(idx[:10])
170
[796, 451, 119, 7, 92, 826, 596, 35, 687, 709]
import numpy as np
import shutil

for img in np.array(sorted(os.listdir('images')))[idx]:
    shutil.move('images/'+img, 'test_images/'+img)

for annot in np.array(sorted(os.listdir('annotations')))[idx]:
    shutil.move('annotations/'+annot, 'test_annotations/'+annot)

Как и в приведенном выше коде, вы можете использовать пакет Shutil для перемещения 170 изображений и 170 файлов координат в папку test_images и папку test_annotations соответственно. Давайте проверим количество файлов в каждой папке.

print(len(os.listdir('annotations')))
print(len(os.listdir('images')))
print(len(os.listdir('test_annotations')))
print(len(os.listdir('test_images')))
683
683
170
170

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

from tqdm import tqdm
import pandas as pd
from collections import Counter

def get_num_objects_for_each_class(dataset):

    total_labels = []

    for img, annot in tqdm(dataset, position = 0, leave = True):
        total_labels += [int(i) for i in annot['labels']]

    return Counter(total_labels)


train_data =  BboxAugmentationDataset(
    path = 'images/'
)

test_data =  BboxAugmentationDataset(
    path = 'test_images/'
)

train_objects = get_num_objects_for_each_class(train_data)
test_objects = get_num_objects_for_each_class(test_data)

print('\n train объектов в данных', train_objects)
print('\n test объектов в данных', test_objects)
100%|██████████| 683/683 [00:13<00:00, 51.22it/s]
100%|██████████| 170/170 [00:03<00:00, 52.67it/s]

get_num_objects_for_each_classtotal_labels— это функция, которая сохраняет значения меток всех ограничивающих рамок в наборе данных a затем Counter использует класс для подсчета и возврата количества каждой метки. В тренировочных данных 532 класса 0, 2691 класса 1 и 97 класса 2, а в тестовых данных — 185 классов 0, 541 класса 1 и 26 класса 2. Вы можете видеть, что данные правильно разделены, видя, что отношения 0,1,2 одинаковы для каждого набора данных.

До сих пор мы рассматривали, как изменять изображения, используемое для построения модели обнаружения объектов, с помощью модуля Almentations, и мы рассмотрели, как разделить наши данные на обучающие данные и тестовые данные. В главе 4 мы обучим RetinaNet, одноэтапную модель, для построения модели обнаружения ношения маски.

 

Platon

Добавить комментарий