Всем привет! Решила я вернуться на Хабр с новым мини-проектом. Сегодня попробуем детектить дорожные знаки используя YOLOv8. Что ж, приступим!

1: Работа с Google Colab

Первое что мы делаем это открываем Google colab и создаем New Notebook. После того как всё создалось, то сверху ищем <среду выполнения>. Дальше такие действия: Сменить среду выполнения -> T4 GPU -> Сохранить. (По незнанию мне пришлось запускать проект 2 раза, потому что у меня стояла среда выполнения CPU, поэтому сделаем сразу все важные моменты в самом начале).

P.s. меня забанили пока я писала статью, поэтому у вас есть 2 варианта (я не работала ещё ни разу с Google Colab, прошу не кидаться тапками):
1: переписать код и переключиться на GPU уже после написанного.
2: использовать готовый блокнот и переключиться на GPU, когда уже будете готовы запускать код.
Иначе получите это: Невозможно подключиться к ускорителю (GPU) из-за лимитов на использование в Colab.

Так-с, перейдём сразу к делу, проверим всё ли у нас правильно выбралось:

!nvcc --version

nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2022 NVIDIA Corporation
Built on Wed_Sep_21_10:33:58_PDT_2022
Cuda compilation tools, release 11.8, V11.8.89
Build cuda_11.8.r11.8/compiler.31833905_0

!nvidia-smi

Sat Aug 12 08:27:04 2023
NVIDIA-SMI 525.105.17 Driver Version: 525.105.17 CUDA Version: 12.0
...

Если получим такие выводы, то всё отлично. Можно продолжать работать :)

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

!rm -rf ./sample_data

2: Установка утилит

2.1: Клонируем репозиторий

!git clone https://github.com/ultralytics/JSON2YOLO

Есть несколько важных деталей, которые нужно сделать в репозитории:
1: В файле general_json2yolo.py меняем код в 274 строке на h, w, f = img['height'], img['width'], img['file_name'].split('/')[1] .

2: В general_json2yolo.py и labelbox_json2yolo.py, нужно добавить перед каждым utils точку, чтобы было так from .utils import ...

Если у вас возникли вопросы почему так надо сделать, то объясню:
1: Метод .split('/') разделяет строку img['file_name'] по символу / и создает список подстрок. Затем [1] выбирает второй элемент этого списка, то есть вторую подстроку после разделителя /. Строка img['file_name'] содержит путь к файлу с одним или несколькими слешами /, и нам нужно получить имя файла без пути.

2: Я долго не понимала, что я делаю не так и почему у меня не импортится файл, который лежит в той же папке с файлами, которые мы используем. Пришлось лезть в документацию импортов и смотреть какие варианты вообще есть. На своем железе такой ошибки не было, но в Google Colab она почему-то вылезла. Если у вас есть какие-то объяснения этой проблемы, то напишите в комментариях, буду благодарна :)

Так-с, идём дальше!

2.2: Переходим к установке библиотек

%pip install -r /content/JSON2YOLO/requirements.txt
%pip install kaggle ipywidgets widgetsnbextension

Если всё successfully installed, то переходим к самому датасету!

3: Работа с датасетом

3.1: Скачиваем датасет

import os

Дальше нам потребуется Kaggle. Если нет аккаунта -> создаём его, а если есть то переходим в Settings -> API -> Create New Token. Автоматически скачается json файл, там уже будут данные.

os.environ['KAGGLE_USERNAME'] = "сюда вписываем из json username"
os.environ['KAGGLE_KEY'] = "а сюда key"
!kaggle datasets download -d watchman/rtsd-dataset

Немного расскажу о самом датасете, пока он скачивается. Набор данных RTSD содержит кадры, предоставленные компанией "Геоцентр Консалтинг". Изображения получены с широкоформатного видеорегистратора, который снимает с частотой 5 кадров в секунду. Разрешения изображений от 1280×720 до 1920×1080. Фотографии сделаны в разное время года, в разное время суток и при различных погодных условиях . В наборе используется 155 знаков дорожного движения в формате разметки - COCO.

пример данных из датасета
пример данных из датасета

3.2: Распаковываем архив с данными

import zipfile
archive = zipfile.ZipFile('rtsd-dataset.zip', 'r')
archive.extractall('.')

3.3: Ну и чтобы он нам не мешался, то можем его удалить

os.remove('rtsd-dataset.zip')

4: Преобразование датасета в YOLO-формат

4.1: Импортим библиотеки

import pandas as pd
from tqdm.notebook import tqdm
from shutil import copyfile, move
import sys
import json
from ipywidgets import FloatProgress

4.2: Переходим к конвертации

Мы будем использовать готовый скрипт от Ultralytics, который скачали в самом начале. Это папка JSON2YOLO. Перейдём к конвертации СOCO-формата в YOLO-формат. Советую делать всё постепенно, если вы используете Google Colab. У меня возникали ошибки при выполнении, если я использовала код целиком.

from JSON2YOLO.general_json2yolo import convert_coco_json
sys.path.append('./JSON2YOLO')

test_path = 'test_annotation'
train_path = 'train_annotation'

os.makedirs(train_path, exist_ok=True)
os.makedirs(test_path, exist_ok=True)
move('train_anno.json', os.path.join(train_path, 'train_anno.json'))
move('val_anno.json', os.path.join(test_path, 'val_anno.json'))
for folder in ['labels', 'images']:
    for path in [test_path, train_path]:
        os.makedirs(os.path.join(path, folder), exist_ok=True)
convert_coco_json(train_path)
for file in tqdm(os.listdir(os.path.join('new_dir/labels/train_anno'))):
    move(os.path.join('new_dir/labels/train_anno', file), os.path.join(train_path, 'labels', file))

convert_coco_json('./test_annotation/')
for file in tqdm(os.listdir(os.path.join('new_dir/labels/val_anno'))):
    move(os.path.join('new_dir/labels/val_anno', file), os.path.join(test_path, 'labels', file))

Отлично, разметка у нас есть, не хватает лишь изображений, которые ей соответствуют. Добавим их :)

test_labels = os.listdir(os.path.join(test_path, 'labels'))
train_labels = os.listdir(os.path.join(train_path, 'labels'))

test_labels = set(map(lambda x: x.split('.')[0], test_labels))
train_labels = set(map(lambda x: x.split('.')[0], train_labels))
images = 'rtsd-frames/rtsd-frames'
for file in os.listdir(images):
    name = file.split('.')[0]
    if name in train_labels:
        move(os.path.join(images, file), os.path.join(train_path,'images', file))
    if name in test_labels:
        move(os.path.join(images, file), os.path.join(test_path,'images', file))

5: Работа с YAML файлом

Создадим файл "trafic_signs.yaml" с описанием путей и классов, используемых в датасете. Это обязательное требование для YOLOv8.

5.1: Установим библиотеку для записи данных yaml в файл.

%pip install PyYAML==5.1

5.2: Импортим библитеку

import yaml

5.3: Опишем пути и классы для обучения

data = [{
    'train': '/content/train_annotation/images',
    'val': '/content/test_annotation/images',
    'nc': 155,
    'names': ['2_1', '1_23', '1_17', '3_24', '8_2_1', '5_20', '5_19_1', '5_16', '3_25',
              '6_16', '7_15', '2_2', '2_4', '8_13_1', '4_2_1', '1_20_3', '1_25', '3_4', '8_3_2',
              '3_4_1', '4_1_6', '4_2_3', '4_1_1', '1_33', '5_15_5', '3_27', '1_15', '4_1_2_1',
              '6_3_1', '8_1_1', '6_7', '5_15_3', '7_3', '1_19', '6_4', '8_1_4', '8_8', '1_16',
              '1_11_1', '6_6', '5_15_1', '7_2', '5_15_2', '7_12', '3_18', '5_6', '5_5', '7_4',
              '4_1_2', '8_2_2', '7_11', '1_22', '1_27', '2_3_2', '5_15_2_2', '1_8', '3_13',
              '2_3', '8_3_3', '2_3_3', '7_7', '1_11', '8_13', '1_12_2', '1_20', '1_12', '3_32',
              '2_5', '3_1', '4_8_2', '3_20', '3_2', '2_3_6', '5_22', '5_18', '2_3_5', '7_5',
              '8_4_1', '3_14', '1_2', '1_20_2', '4_1_4', '7_6', '8_1_3', '8_3_1', '4_3', '4_1_5',
              '8_2_3', '8_2_4', '1_31', '3_10', '4_2_2', '7_1', '3_28', '4_1_3', '5_4', '5_3',
              '6_8_2', '3_31', '6_2', '1_21', '3_21', '1_13', '1_14', '2_3_4', '4_8_3', '6_15_2',
              '2_6', '3_18_2', '4_1_2_2', '1_7', '3_19', '1_18', '2_7', '8_5_4', '5_15_7', '5_14',
              '5_21', '1_1', '6_15_1', '8_6_4', '8_15', '4_5', '3_11', '8_18', '8_4_4', '3_30',
              '5_7_1', '5_7_2', '1_5', '3_29', '6_15_3', '5_12', '3_16', '1_30', '5_11', '1_6',
              '8_6_2', '6_8_3', '3_12', '3_33', '8_4_3', '5_8', '8_14', '8_17', '3_6', '1_26',
              '8_5_2', '6_8_1', '5_17', '1_10', '8_16', '7_18', '7_14', '8_23']
}]
def write_yaml_to_file(py_obj, filename) :
    with open(f'{filename}.yaml', 'w+',) as f:
        yaml.dump_all(py_obj, f, sort_keys=False)
write_yaml_to_file(data, 'trafic_signs')

6: Доп. установка библиотек

6.1: Устанавливаем библиотеки

%pip install ultralytics

7: Обучение

Переходим непосредственно к обучению модели, наконец-то :)

7.1: Импортим библиотеки

from ultralytics import YOLO
import torch
import gc

Если спросите зачем импортить torch и gc, то дам такой ответ. Они нужны для очистки графической памяти, если вам нужно будет это сделать, то пропишите:

gc.collect()
torch.cuda.empty_cache()

7.2: Начинаем обучать модель

model = YOLO('yolov8s.pt')

results = model.train(
   data='/content/trafic_signs.yaml',
   imgsz=1280,
   epochs=5,
   batch=6,
   device=0,
   name='YOLOv8s'
)

P.s. если хотите, то можете поколдовать над размером батчей, у меня стоит 5 для обучения на colab, т.к. там всего 15гб GPU_mem. Ну или можете взять и другую модель от YOLOv8, а можно и больше эпох сделать. Тут уже всё на ваш выбор :)

8: Итоги обучения

Самое время посмотреть на результаты после обучение, давайте потестируем модельку :)

model = YOLO("путь к лучшему весу -> best.pt")
pre = model.predict(
    source="путь к фото / видео, где хотите предсказать", 
    show=True,                                                  
    imgsz=1280,                                                 
    hide_labels=True,                                           
    save=True,                                                  
    name="название папки для результатов",                                                 
    conf=0.1,                                                   
    )
тест 1
тест 1
тест 2
тест 2
тест 3
тест 3

Как итог могу сказать, что получилось у меня обучить модельку всего 4 эпохи (вышло практически 6 часов), на 5 эпохе Google colab решил, что я превысила время использования ресурсов и остановил обучение не дав скачать веса и удалив все файлы. Так что совет на будущее, если будете обучать на colab, то сохраняйте веса каждую эпоху, как это делала я, просто потом дообучите модельку и всё. Думаю, что я так и сделаю в ближайшее время, чтобы сравнить результаты.

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

Я только начинаю изучать CV и ML, поэтому, если у вас есть рекомендации по материалам/проектам, то порекомендуйте что-нибудь, буду благодарна :)

Ну и оставлю ссылочки: GitHubTelegram

Комментарии (8)


  1. SmallDonkey
    13.08.2023 09:15
    +1

    Для лучшего результата можно настроить аугментацию в файле ultralytics/ultralytics/cfg/default.yaml


    1. kath_ml Автор
      13.08.2023 09:15

      Хорошо, попробую сегодня тогда. Спасибо за рекомендацию :)


  1. UndefinedRef
    13.08.2023 09:15
    +1

    Хотелось бы конечно увидеть хоть какие-то метрики по итогу и сравнения с классическими подходомами к детекции знаков (например, поиск по цвету, выделение контуров, сглаживание) и потом классификации (тут уже целый зоопарк, взять какую нибудь SoTA для легких устройств).
    Пока это выглядит так, как будто танком пытаются заводить блоху.
    Есть масса детекторов знаков (не обязательно дорожных), которые работают на базе встраиваемых устройств или вообще внутри ip камер. И там вашу yolo8s конечно будет трудно раскатать, а судя по всему, ещё и результат будет не намного лучше, хотя тут уже как посмотреть (а в случае, с нейронными сетями, ещё и как обучить)


    1. kath_ml Автор
      13.08.2023 09:15

      Метрики я не делала, поэтому их пока нет в статье. Либо в скором времени дополню статью, либо уже пообучаю модельку чуть подольше и тогда уже можно будет что-то сравнивать, но уже в новой статье.

      Я не собираюсь встраивать модельку в какие-либо устройства, это тестовый проект, чтобы попробовать свои силы, ну и узнать что-то новое :)


  1. dimnsk
    13.08.2023 09:15
    +2

    epochs=5 name='YOLOv8s' imgsz=1280


    5 эпох очень мало ставьте 200 эпох
    и модель не 's' а 'l' и будет счастье вам
    также размер можно смело уменьшить до 960, а то и 640


    1. kath_ml Автор
      13.08.2023 09:15

      Соглашусь, что 5 эпох маловато, обычно на обучение ставят больше, но это уже вопрос времени и ресурсов. Спасибо за рекомендации, попробую, посмотрю что получится по итогу :)


  1. ret77876
    13.08.2023 09:15
    +1

    что я превысила время использования ресурсов и остановил обучение не дав скачать веса и удалив все файлы

    Если обучаете на бесплатном коллабе, то есть следующий способ не потерять веса:

    Монтируем гугл диск, привязанный к аккаунту на котором запускается коллаб. Для этого можно нажать на соответствующую кнопку в файловом менеджере сессии, либо простым кодом:

    from google.colab import drive
    drive.mount('/content/drive')

    И у вас подключится гугл диск с read-write доступом. Абсолютный путь: /content/drive/MyDrive

    Теперь при обучении используйте аргумент save_dir, в который указываете путь к любой папке на коллабе.

    P.S. У этого решения есть небольшой минус, а именно сохранение в файловую систему текущей сессии быстрее сохранения с коллаба на гугл диск. Но в принципе это не критично, так как файлы вы будете сохранять сравнительно небольшие.


    1. kath_ml Автор
      13.08.2023 09:15

      Я пробовала использовать files.download(). В первый раз когда обучала, то в самом конце поставила веса на скачивание, как только обучение завершится. По итогу обучение остановилось, файлы удалились и всё. Скажем так, это был фейл.

      Во второй раз я всё-таки прикрутила гугл диск, даже подумала, что можно туда всё подгружать и ничего не удалится (как раз ваш вариант), но скорость загрузки меня немного разочаровала. По этой причине решила не использовать, да и обучать на колабе дольше, чем на своём железе, так что это так, получение навыков :(