Инструменты и окружение CV-инженера
Содержание курса
Чтение изображений
Начнём с самого начала — с чтения файла с диска. Читать умеют обе библиотеки, которые стоят в окружении: OpenCV, на которой в CV держится почти вся работа с кадром, и Pillow, обычная питоновская библиотека картинок. Прочитаем одну фотографию обеими и посмотрим, что получилось:
from pathlib import Path
import cv2
import numpy as np
from PIL import Image
path = Path("banana.jpg")
frame = cv2.imread(str(path))
print(f"тип {type(frame).__module__}.{type(frame).__name__}")
print(f"форма {frame.shape} (высота, ширина, каналы)")
print(f"тип чисел {frame.dtype}, значения от {frame.min()} до {frame.max()}")
print(f"на диске {path.stat().st_size / 1024:.0f} КБ → "
f"в памяти {frame.nbytes / 1024:.0f} КБ")
y, x = frame.shape[0] // 2, frame.shape[1] // 2
pil = np.asarray(Image.open(path))
print()
print(f"пиксель ({x}, {y}) по версии cv2.imread {frame[y, x]}")
print(f"пиксель ({x}, {y}) по версии PIL.Image {pil[y, x]}")
print("те же три числа в обратном порядке: OpenCV читает в BGR, все остальные — в RGB")
Вот что он напечатал на кадре 348×348:
Смотреть тут стоит на три вещи. Форма — (348, 348, 3): сначала высота, потом ширина, потом каналы. Порядок «высота, ширина» непривычен и путается с координатами точек, где первым идёт x; отсюда половина ошибок при работе с рамками. Тип чисел — uint8, целые от 0 до 255: столько занимает яркость одного канала. И размер: файл на диске весит 22 КБ, а развёрнутый массив — 355 КБ, в шестнадцать раз больше. JPEG хранит картинку сжатой, а считает модель по несжатой, поэтому оперативная память кончается быстрее, чем подсказывает папка с данными.
Ловушка с порядком каналов
Две строки про центральный пиксель — это то, на чём спотыкаются все. Один и тот же пиксель OpenCV отдал как [24 108 144], а Pillow — как [144 108 24]. Числа те же, порядок обратный: OpenCV читает каналы в порядке BGR (синий, зелёный, красный), а Pillow, matplotlib, torchvision и вообще все остальные — в RGB. Историческая причина простая: так было принято в камерах и кодеках, когда библиотеку начинали писать, и менять это уже поздно.
Ошибка не даёт ни исключения, ни предупреждения — код работает, просто результат другой. Слева массив из cv2.imread показан как есть, справа он же после перестановки каналов:
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
На бананах это заметно сразу. На снимке цеха или на серых документах — нет: картинка выглядит правдоподобно, а модель, обученная на RGB, тихо теряет несколько процентов точности. Отсюда рабочее правило: переводить кадр в RGB сразу после чтения, а обратно в BGR — только перед cv2.imwrite и cv2.imshow, потому что они тоже ждут BGR.
Сохранить и посмотреть
Эти два вызова стоит разобрать сразу — ими заканчивается почти любой кусок кода в курсе.
cv2.imwrite("out.png", frame)
cv2.imshow("banana", frame)
cv2.waitKey(0) # ждать нажатия клавиши; waitKey(1) — миллисекунду и дальше
cv2.destroyAllWindows()
imwrite кладёт массив обратно в файл, а формат выбирает по расширению имени. .png хранит пиксели точно и весит много, .jpg жмёт с потерями и весит в разы меньше. Для отладочного кадра, на который просто смотрят, хватает JPEG; картинку, по которой потом что-то меряют или сравнивают, лучше писать в PNG, чтобы не измерять заодно артефакты сжатия.
imshow показывает кадр в окне, но само по себе окно не рисуется: рисует его waitKey, который заодно отдаёт управление системе и ждёт нажатия клавиши. waitKey(0) ждёт бесконечно — так рассматривают один кадр. waitKey(1) ждёт миллисекунду и идёт дальше — так проигрывают последовательность кадров. Возвращает он код нажатой клавиши: 27 — это Esc, обычная кнопка выхода из такого просмотра. destroyAllWindows закрывает окна в конце.
Ради этого окна мы и переставляли opencv последней командой при сборке окружения. Если вместо неё в cv2 окажется сборка opencv-python-headless, которую тянет за собой FiftyOne, imshow упадёт с The function is not implemented: это та же библиотека, собранная без оконной части. Так что если увидите такую ошибку — вопрос не к коду, а к тому, какая сборка встала последней.
Окно удобно, пока вы сидите за своей машиной. На сервере, в контейнере и в блокноте вроде Colab его нет, и результат там смотрят файлом: записали imwrite и открыли чем удобно. По той же причине в шагах этого курса картинки чаще сохраняются, чем показываются, — так пример работает у всех одинаково.
Дальше этот массив идёт в аугментации, в модель и в отрисовку — и в каждом из трёх мест его ждёт своя библиотека со своими требованиями к форме и типу чисел.
Кадр — набор Grocery Store (Marcus Klasson), MIT; каналы переставлены и наложены подписи.
