Цифровое изображение и основы работы с OpenCV и NumPy
Цифровое изображение и основы работы с OpenCV и NumPy
В веб-разработке изображение часто воспринимается как готовый статический ресурс — файл в теге <img> или одномерный массив байтов ImageData.data в HTML5 Canvas. Однако в компьютерном зрении изображение — это не файл и не текстура. Это строгая математическая структура: двумерная или трёхмерная числовая матрица, в которой каждый элемент описывает интенсивность светового излучения, зафиксированного матрицей камеры.
Если вы хотите научить алгоритм находить лица, распознавать дорожные знаки или сегментировать опухоли на МРТ, первым шагом станет умение работать с этой матрицей на низком уровне.
Как компьютер видит изображение
В физическом мире свет непрерывен. Чтобы перевести его в цифровой вид, сенсор камеры выполняет два последовательных шага: дискретизацию (разбиение непрерывного пространства на сетку пикселей) и квантование (округление непрерывной яркости света в дискретный набор целых чисел).
В подавляющем большинстве задач компьютерного зрения яркость кодируется 8 битами на канал. Это означает, что одно число может принимать различных градаций: от (абсолютная темнота, чёрный цвет) до (максимальная яркость, белый цвет).
Структура данных зависит от типа изображения:
- Полутоновое (Grayscale): двумерная матрица размера , где (Height) — количество строк (высота в пикселях), а (Width) — количество столбцов (ширина в пикселях). Каждый элемент — число от 0 до 255.
- Цветное (Color): трёхмерный массив (тензор) размера , где (Channels) — количество цветовых каналов. Для стандартного цветного кадра .
Ловушка систем координат: OpenCV против фронтенда
Для разработчика с опытом в CSS, SVG или Canvas привычна декартова система координат экрана: точка задаётся парой , где — смещение по горизонтали вправо, а — смещение по вертикали вниз.
В библиотеках научных вычислений (NumPy, PyTorch) и компьютерного зрения (OpenCV) изображение хранится как стандартная матрица. В матричной нотации первый индекс всегда указывает на строку (row), а второй — на столбец (column).
Строка матрицы — это вертикальная координата , а столбец — горизонтальная координата .
В математике и NumPy координаты пикселя записываются как
img[y, x], а неimg[x, y]. Сначала указывается вертикаль (высота), затем — горизонталь (ширина).
Если в коде перепутать эти индексы, программа либо аварийно завершится с ошибкой IndexError, либо незаметно транспонирует изображение, повернув его на 90 градусов с зеркальным отражением.
Попробуйте изменить значения каналов отдельного пикселя в интерактивном инспекторе ниже, чтобы увидеть, как числовая матрица формирует цветное изображение.
Установка окружения и чтение первого кадра
Для практической работы в машинном зрении на Python стандартом де-факто являются библиотеки numpy и opencv-python.
Установить их можно командой терминала:
pip install opencv-python numpy
Загрузим изображение с диска и изучим его форму:
import cv2
# Чтение изображения
img = cv2.imread('photo.jpg')
# Проверка типа и размерности
print(type(img)) # <class 'numpy.ndarray'>
print(img.shape) # Например, (720, 1280, 3) -> (H, W, C)
print(img.dtype) # uint8 (unsigned 8-bit integer)
Метод cv2.imread() возвращает обычный массив NumPy (ndarray). Это ключевой концепт: для OpenCV изображение не является специфическим закрытым объектом — это открытый массив чисел, к которому применимы любые матричные операции.
Загадка BGR: почему лица становятся синими
Если вы попытаетесь отобразить загруженное через OpenCV изображение в популярной библиотеке matplotlib или передать его напрямую в веб-интерфейс, картинка приобретёт странный неестественный оттенок: лица людей станут синими, а небо — оранжевым.
Причина в историческом соглашении:
- Общепринятый цифровой стандарт — RGB (Red, Green, Blue). В таком порядке каналы хранятся в веб-форматах (PNG, JPEG, CSS
rgb()). - OpenCV создавался в начале 2000-х годов, когда производители камер и разработчики формата Windows Bitmap (BMP) оптимизировали память под порядок BGR (Blue, Green, Red).
OpenCV по умолчанию читает и сохраняет файлы в порядке BGR:
- Канал 0: Синий (Blue)
- Канал 1: Зелёный (Green)
- Канал 2: Красный (Red)
Чтобы привести изображение к стандартному формату RGB, используется функция преобразования цветовых пространств:
# Конвертация из BGR в RGB
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# Конвертация в полутоновое изображение (Grayscale)
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
print(img_gray.shape) # (720, 1280) — третий канал исчез
При конвертации в Grayscale яркость каждого пикселя вычисляется не как простое среднее арифметическое трёх каналов, а с учётом физиологии человеческого глаза, который наиболее чувствителен к зелёному спектру и слабее — к синему:
Здесь:
- — результирующая яркость пикселя от 0 до 255.
- — значения соответствующих каналов исходного пикселя.
- Коэффициенты , и отражают относительный вклад длин волн в восприятие яркости человеком.
Пример: если пиксель чисто синий (), его яркость в градациях серого составит всего (тёмно-серый), а чисто зелёный () даст (светло-серый).
Базовые манипуляции через срезы NumPy
Поскольку изображение — это массив NumPy, любые трансформации областей интереса (Region of Interest, ROI) выполняются через стандартный синтаксис срезов (slicing) без необходимости писать медленные циклы for.
Кадрирование (Crop)
Чтобы вырезать фрагмент изображения, достаточно указать диапазон строк по и диапазон столбцов по :
# Вырезаем прямоугольник: от y=100 до y=300, от x=250 до x=600
roi = img[100:300, 250:600]
Разделение и объединение каналов
Работать с отдельными каналами можно напрямую через индексы третьего измерения:
# Извлечение только синего канала (BGR)
blue_channel = img[:, :, 0]
# Обнуление красного канала (индекс 2)
img_no_red = img.copy()
img_no_red[:, :, 2] = 0
Арифметика над изображениями и опасность переполнения uint8
Один из важнейших аспектов при переходе от высокоуровневого веба к низкоуровневой обработке матриц — контроль типов данных. Массивы изображений хранятся с типом np.uint8 (беззнаковое 8-битное целое).
Диапазон допустимых значений ограничен от 0 до 255. Если в результате сложения получается число больше 255 или меньше 0, происходит циклическое переполнение:
import numpy as np
# Прямая арифметика NumPy (по модулю 256)
a = np.uint8([250])
b = np.uint8([20])
print(a + b) # Выведет [14], так как (250 + 20) % 256 = 14
В визуальном контексте это катастрофа: вместо того чтобы стать ярче, пересвеченный белый пиксель превращается в почти чёрный.
Для безопасной работы с яркостью и контрастом применяют два подхода:
| Подход | Как работает | Пример кода |
|---|---|---|
| Насыщающая арифметика OpenCV | Значения автоматически ограничиваются диапазоном : всё, что выше 255, становится 255; всё, что ниже 0, становится 0 | cv2.add(img, 50) |
| Каст во float32 с последующим clip | Массив переводится в числа с плавающей точкой, вычисляются формулы, затем значения обрезаются и возвращаются в uint8 |
np.clip(img.astype(np.float32) * 1.5, 0, 255).astype(np.uint8) |
Формула линейной коррекции контраста и яркости имеет вид:
Здесь:
- — исходное значение пикселя.
- — коэффициент контрастности ( повышает контраст, снижает).
- — смещение яркости ( делает светлее, темнее).
- — итоговое значение пикселя.
Пример: при и пиксель с исходной яркостью получит новое значение: .
Изменение размера и геометрические трансформации
В задачах глубокого обучения входные слои нейронных сетей требуют фиксированного размера тензора (например, или пикселей). Изменение размера (resizing) — обязательная стадия предобработки.
При изменении размера сетка пикселей пересчитывается с помощью интерполяции:
# Изменение размера до ширины 640 и высоты 480
# ВНИМАНИЕ: OpenCV в аргументе dsize ожидает (width, height), то есть (x, y)!
resized = cv2.resize(img, (640, 480), interpolation=cv2.INTER_LINEAR)
Основные методы интерполяции в OpenCV:
cv2.INTER_NEAREST— интерполяция по ближайшему соседу. Самая быстрая, но создаёт ступенчатые артефакты (пикселизацию). Применяется для масок сегментации, где нельзя создавать промежуточные классы.cv2.INTER_LINEAR— билинейная интерполяция. Базовый стандарт для увеличения и уменьшения изображений общего назначения.cv2.INTER_AREA— интерполяция с передискретизацией на основе площади пикселей. Даёт наилучший результат без муара при уменьшении разрешения.cv2.INTER_CUBIC— бикубическая интерполяция по окрестности. Даёт более плавные границы при увеличении, но требует больше вычислительных ресурсов.
Сквозной пайплайн предобработки
Соберём все изученные концепции в единый скрипт, который выполняет типовую задачу подготовки входных данных: загрузку, кадрирование центральной зоны, цветовую коррекцию, изменение разрешения и сохранение результата.
import cv2
import numpy as np
def preprocess_for_model(image_path: str, target_size=(224, 224)) -> np.ndarray:
# 1. Загрузка исходного кадра (BGR, uint8)
img_bgr = cv2.imread(image_path)
if img_bgr is None:
raise FileNotFoundError(f"Файл {image_path} не найден")
# 2. Определение размеров (H, W)
h, w = img_bgr.shape[:2]
# 3. Центральное кадрирование (Central Square Crop)
min_dim = min(h, w)
start_y = (h - min_dim) // 2
start_x = (w - min_dim) // 2
cropped = img_bgr[start_y:start_y + min_dim, start_x:start_x + min_dim]
# 4. Приведение к RGB
img_rgb = cv2.cvtColor(cropped, cv2.COLOR_BGR2RGB)
# 5. Масштабирование до целевого разрешения модели
resized = cv2.resize(img_rgb, target_size, interpolation=cv2.INTER_AREA)
# 6. Нормализация значений в диапазон [0.0, 1.0] (float32)
normalized = resized.astype(np.float32) / 255.0
return normalized
# Пример вызова
processed_tensor = preprocess_for_model('photo.jpg')
print("Форма тензора:", processed_tensor.shape) # (224, 224, 3)
print("Диапазон значений:", processed_tensor.min(), "-", processed_tensor.max()) # 0.0 - 1.0
Теперь изображение готово: оно представлено в корректном цветовом пространстве, строго заданного размера и нормализовано в диапазон вещественных чисел, понятных алгоритмам машинного обучения.