Фаза 04 · урок 01
Основы изображений: пиксели, каналы, цветовые пространства
Цель урока: Каждая статья, которую вы прочитаете, каждый скачанный набор предобученных весов и каждый API компьютерного зрения предполагают конкретное кодирование входа. Передайте изображение uint8 , когда модели нужен float32 , — она всё равно…
Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.
Содержание урока
- Цели обучения
- Проблема
- Концепция
- Полный конвейер предобработки одним взглядом
- Пиксель — это измерение, а не квадрат
- Почему три канала
- Два соглашения о раскладке: HWC и CHW
- Диапазоны байтов и dtype
- Цветовые пространства и зачем они нужны
- Соотношение сторон, изменение размера и интерполяция
- Соберите это
- Шаг 1: Загрузите изображение и изучите его форму
- Шаг 2: Разделите каналы и измените порядок раскладки
- Шаг 3: Преобразования в оттенки серого и HSV
- Шаг 4: Нормализуйте, стандартизируйте и выполните обратное преобразование
- Шаг 5: Измените размер с тремя методами интерполяции
- Примените это
- Подготовьте к поставке
- Упражнения
- Ключевые термины
- Дополнительное чтение
Изображение — это тензор измерений света. С этого единственного факта начинается каждая модель компьютерного зрения, которой вы когда-либо воспользуетесь.
Тип: Сборка Языки: Python Предварительные требования: Фаза 1, урок 12 (Операции с тензорами); фаза 3, урок 11 (Введение в PyTorch) Время: ~45 минут
Цели обучения
- Объяснять, как непрерывная сцена дискретизируется в пиксели и почему решения о выборке и квантовании задают предел для каждой последующей модели
- Читать, срезать и исследовать изображения как массивы NumPy, а также уверенно переключаться между раскладками HWC и CHW
- Преобразовывать RGB, оттенки серого, HSV и YCbCr и объяснять назначение каждого цветового пространства
- Применять предобработку на уровне пикселей (нормализация, стандартизация, изменение размера, порядок «каналы сначала») в точности так, как ожидает torchvision
Проблема
Каждая статья, которую вы прочитаете, каждый скачанный набор предобученных весов и каждый API компьютерного зрения предполагают конкретное кодирование входа. Передайте изображение uint8, когда модели нужен float32, — она всё равно запустится и молча выдаст мусор. Подайте BGR в сеть, обученную на RGB, — и точность упадёт на десять пунктов. Передайте модели вход с каналами в последней оси, когда она ждёт каналы в первой, — и первый свёрточный слой примет высоту за канал признаков. Ни одно из этих действий не вызовет ошибку. Они просто испортят метрики, а вы потратите неделю на поиски ошибки, которая скрывается в способе загрузки файла.
Свёртка несложна, когда вы знаете, по чему она скользит. Трудность в том, что «изображение» означает разные вещи для камеры, JPEG-декодера, PIL, OpenCV, torchvision и CUDA-ядра. У каждого стека собственный порядок осей, диапазон байтов и соглашение о каналах. Инженер компьютерного зрения, который не умеет держать это в голове, выпускает сломанные конвейеры.
Этот урок закладывает основу, на которой строится остальная фаза. К концу вы будете знать, что такое пиксель, почему на пиксель приходится три числа, а не одно, что на самом деле означает «нормализовать со статистиками ImageNet» и как переходить между двумя или тремя раскладками, которые предполагают все последующие уроки этой фазы.
Концепция
Полный конвейер предобработки одним взглядом
Каждая промышленная система компьютерного зрения — это одна и та же последовательность обратимых преобразований. Ошибитесь в одном шаге — и модель увидит не тот вход, на котором обучалась.
Два красный и синий блока — места, где возникают 80 % тихих сбоев: пропущенная стандартизация и неверная раскладка.
Пиксель — это измерение, а не квадрат
Сенсор камеры считает фотоны, попадающие на сетку крошечных детекторов. Каждый детектор интегрирует свет в течение доли секунды и выдаёт напряжение, пропорциональное числу попавших фотонов. Затем сенсор дискретизирует это напряжение до целого числа. Один детектор становится одним пикселем.
Continuous scene Sensor grid Digital image
(infinite detail) (H x W detectors) (H x W integers)
~~~~~ +--+--+--+--+--+ 210 198 180 155 120
~ ~ ~ | | | | | | 205 195 178 152 118
~ light ~ ----> +--+--+--+--+--+ ----> 200 190 175 150 115
~~~~~ | | | | | | 195 185 170 148 112
+--+--+--+--+--+ 188 180 165 145 108
На этом шаге происходят два выбора, и они задают предел для всего последующего:
- Пространственная выборка (spatial sampling) определяет число детекторов на градус сцены. Их недостаток делает края зубчатыми (алиасинг); избыток взрывает требования к хранилищу и вычислениям.
- Квантование интенсивности (intensity quantization) определяет, насколько мелко напряжение разбивается на интервалы. 8 бит дают 256 уровней и являются стандартом для отображения. 10, 12 или 16 бит дают более плавные градиенты и важны для медицинской визуализации, HDR и конвейеров с необработанными данными сенсора.
Пиксель — не цветной квадрат с площадью. Это одиночное измерение. При изменении размера или повороте вы повторно дискретизируете эту измерительную сетку.
Почему три канала
Один детектор считает фотоны во всём видимом спектре — это оттенки серого. Чтобы получить цвет, сенсор покрывает сетку мозаикой из красных, зелёных и синих фильтров. После демозаики каждая пространственная позиция имеет три целых числа: отклик ближайшего детектора с красным фильтром, зелёным фильтром и синим фильтром. Эти три целых числа образуют RGB-тройку пикселя.
One pixel in memory:
(R, G, B) = (210, 140, 30) <- reddish-orange
An H x W RGB image:
shape (H, W, 3) stored as H rows of W pixels of 3 values
each in [0, 255] for uint8
Три — не магическое число. Камеры глубины добавляют канал Z. Спутники добавляют инфракрасные и ультрафиолетовые полосы. Медицинские сканы часто имеют один канал (рентген, КТ) или много каналов (гиперспектральные изображения). Число каналов — это последняя ось; свёрточные слои учатся смешивать их.
Два соглашения о раскладке: HWC и CHW
Один и тот же тензор, два порядка осей. Каждая библиотека выбирает свой.
HWC (height, width, channels) CHW (channels, height, width)
W -> H ->
+-----+-----+-----+ +-----+-----+
H |R G B|R G B|R G B| C |R R R R R R|
| +-----+-----+-----+ | +-----+-----+
v |R G B|R G B|R G B| v |G G G G G G|
+-----+-----+-----+ +-----+-----+
|B B B B B B|
+-----+-----+
PIL, OpenCV, matplotlib, PyTorch, most deep learning
almost every image file on disk frameworks, cuDNN kernels
CHW существует, потому что ядра свёртки скользят по H и W. Сохранение оси каналов первой означает, что каждое ядро видит непрерывную двумерную плоскость для каждого канала, что удобно векторизуется. Форматы на диске используют HWC, потому что это соответствует тому, как строки развёртки выходят из сенсора.
Строка преобразования, которую вы наберёте тысячу раз:
img_chw = img_hwc.transpose(2, 0, 1) # NumPy
img_chw = img_hwc.permute(2, 0, 1) # PyTorch tensor
Визуализация раскладки памяти:
Диапазоны байтов и dtype
Доминируют три соглашения:
| Соглашение | dtype | Диапазон | Где встречается |
|---|---|---|---|
| Необработанное | uint8 |
[0, 255] | Файлы на диске, вывод PIL и OpenCV |
| Нормализованное | float32 |
[0.0, 1.0] | После img.astype('float32') / 255 |
| Стандартизованное | float32 |
примерно [-2, +2] | После вычитания среднего и деления на std |
Свёрточные сети обучались на стандартизованных входах. Статистики ImageNet mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] — это арифметическое среднее и стандартное отклонение трёх каналов по всему обучающему набору ImageNet, рассчитанные на нормализованных в [0, 1] пикселях. Подача необработанного uint8 в модель, ожидающую стандартизованный float, — самый частый тихий сбой в прикладном компьютерном зрении.
Цветовые пространства и зачем они нужны
RGB — это формат захвата, но не всегда наиболее полезное представление для модели.
RGB HSV YCbCr / YUV
R red H hue (angle 0-360) Y luminance (brightness)
G green S saturation (0-1) Cb chroma blue-yellow
B blue V value/brightness (0-1) Cr chroma red-green
Linear to Separates color from Separates brightness from
sensor output brightness. Useful for color. JPEG and most video
color thresholding, UI codecs compress the chroma
sliders, simple filters channels harder because the
human eye is less sensitive
to chroma detail than to Y.
Для большинства современных CNN вы подаёте RGB. Другие пространства встретятся, когда:
- HSV — классический код CV, сегментация по цвету, баланс белого.
- YCbCr — чтение внутреннего устройства JPEG, видеоконвейеры, модели сверхразрешения, работающие только с Y.
- Оттенки серого (grayscale) — OCR, модели документов и любой случай, где цвет — мешающая переменная, а не сигнал.
Оттенки серого из RGB получаются взвешенной суммой, а не средним, потому что человеческий глаз чувствительнее к зелёному, чем к красному или синему:
Y = 0.299 R + 0.587 G + 0.114 B (ITU-R BT.601, the classic weights)
Соотношение сторон, изменение размера и интерполяция
Каждая модель имеет фиксированный размер входа (224x224 для большинства классификаторов ImageNet, 384x384 или 512x512 для современных детекторов). Ваши изображения редко совпадают с ним. Важны три варианта изменения размера:
- Изменить короткую сторону, затем вырезать центр — стандартный рецепт ImageNet. Сохраняет соотношение сторон, но отбрасывает полосу пикселей по краям.
- Изменить размер и дополнить полями — сохраняет соотношение сторон и каждый пиксель, добавляет чёрные полосы. Стандарт для обнаружения объектов и OCR.
- Изменить размер прямо до целевого — растягивает изображение. Это дёшево и искажает геометрию, но подходит для многих задач классификации.
Метод интерполяции определяет, как вычисляются промежуточные пиксели, если новая сетка не совпадает со старой:
Nearest neighbour fastest, blocky, only choice for masks/labels
Bilinear fast, smooth, default for most image resizing
Bicubic slower, sharper on upscaling
Lanczos slowest, best quality, used for final display
Практическое правило: билинейная интерполяция для обучения, бикубическая или Lanczos для материалов, которые будут смотреть люди, и ближайший сосед для всего, что содержит целочисленные идентификаторы классов.
conv-output-size
Соберите это
Шаг 1: Загрузите изображение и изучите его форму
Используйте Pillow, чтобы загрузить любой JPEG или PNG, преобразуйте его в NumPy и выведите полученный результат. Для детерминированного примера, работающего офлайн, синтезируйте изображение.
import numpy as np
from PIL import Image
def synthetic_rgb(h=128, w=192, seed=0):
rng = np.random.default_rng(seed)
yy, xx = np.meshgrid(np.linspace(0, 1, h), np.linspace(0, 1, w), indexing="ij")
r = (np.sin(xx * 6) * 0.5 + 0.5) * 255
g = yy * 255
b = (1 - yy) * xx * 255
rgb = np.stack([r, g, b], axis=-1) + rng.normal(0, 6, (h, w, 3))
return np.clip(rgb, 0, 255).astype(np.uint8)
arr = synthetic_rgb()
# Or load from disk:
# arr = np.asarray(Image.open("your_image.jpg").convert("RGB"))
print(f"type: {type(arr).__name__}")
print(f"dtype: {arr.dtype}")
print(f"shape: {arr.shape} # (H, W, C)")
print(f"min: {arr.min()}")
print(f"max: {arr.max()}")
print(f"pixel at (0, 0): {arr[0, 0]}")
Ожидаемый вывод: shape: (H, W, 3), dtype: uint8, диапазон [0, 255]. Это каноническое представление на диске независимо от того, пришли ли байты из камеры, JPEG-декодера или синтетического генератора.
Шаг 2: Разделите каналы и измените порядок раскладки
Извлеките R, G и B по отдельности, затем преобразуйте HWC в CHW для PyTorch.
R = arr[:, :, 0]
G = arr[:, :, 1]
B = arr[:, :, 2]
print(f"R shape: {R.shape}, mean: {R.mean():.1f}")
print(f"G shape: {G.shape}, mean: {G.mean():.1f}")
print(f"B shape: {B.shape}, mean: {B.mean():.1f}")
arr_chw = arr.transpose(2, 0, 1)
print(f"\nHWC shape: {arr.shape}")
print(f"CHW shape: {arr_chw.shape}")
Три плоскости оттенков серого — по одной на канал. CHW лишь меняет порядок осей; при подходящей раскладке памяти строго говоря копирование данных не требуется.
Шаг 3: Преобразования в оттенки серого и HSV
Сначала взвешенная сумма для оттенков серого, затем ручное преобразование RGB в HSV.
def rgb_to_grayscale(rgb):
weights = np.array([0.299, 0.587, 0.114], dtype=np.float32)
return (rgb.astype(np.float32) @ weights).astype(np.uint8)
def rgb_to_hsv(rgb):
rgb_f = rgb.astype(np.float32) / 255.0
r, g, b = rgb_f[..., 0], rgb_f[..., 1], rgb_f[..., 2]
cmax = np.max(rgb_f, axis=-1)
cmin = np.min(rgb_f, axis=-1)
delta = cmax - cmin
h = np.zeros_like(cmax)
mask = delta > 0
rmax = mask & (cmax == r)
gmax = mask & (cmax == g)
bmax = mask & (cmax == b)
h[rmax] = ((g[rmax] - b[rmax]) / delta[rmax]) % 6
h[gmax] = ((b[gmax] - r[gmax]) / delta[gmax]) + 2
h[bmax] = ((r[bmax] - g[bmax]) / delta[bmax]) + 4
h = h * 60.0
s = np.where(cmax > 0, delta / cmax, 0)
v = cmax
return np.stack([h, s, v], axis=-1)
gray = rgb_to_grayscale(arr)
hsv = rgb_to_hsv(arr)
print(f"gray shape: {gray.shape}, range: [{gray.min()}, {gray.max()}]")
print(f"hsv shape: {hsv.shape}")
print(f"hue range: [{hsv[..., 0].min():.1f}, {hsv[..., 0].max():.1f}] degrees")
print(f"sat range: [{hsv[..., 1].min():.2f}, {hsv[..., 1].max():.2f}]")
print(f"val range: [{hsv[..., 2].min():.2f}, {hsv[..., 2].max():.2f}]")
Оттенок получается в градусах, а насыщенность и значение — в [0, 1]. Это совпадает с соглашением OpenCV hsv_full.
Шаг 4: Нормализуйте, стандартизируйте и выполните обратное преобразование
Перейдите от необработанных байтов к точному тензору, который ожидает предобученная модель ImageNet, а затем обратно.
mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
def preprocess_imagenet(rgb_uint8):
x = rgb_uint8.astype(np.float32) / 255.0
x = (x - mean) / std
x = x.transpose(2, 0, 1)
return x
def deprocess_imagenet(chw_float32):
x = chw_float32.transpose(1, 2, 0)
x = x * std + mean
x = np.clip(x * 255.0, 0, 255).astype(np.uint8)
return x
x = preprocess_imagenet(arr)
print(f"preprocessed shape: {x.shape} # (C, H, W)")
print(f"preprocessed dtype: {x.dtype}")
print(f"preprocessed mean per channel: {x.mean(axis=(1, 2)).round(3)}")
print(f"preprocessed std per channel: {x.std(axis=(1, 2)).round(3)}")
roundtrip = deprocess_imagenet(x)
max_diff = np.abs(roundtrip.astype(int) - arr.astype(int)).max()
print(f"roundtrip max pixel diff: {max_diff} # should be 0 or 1")
Среднее по каждому каналу должно быть близко к нулю, а std — к единице. Пара preprocess/deprocess в точности выполняет то же, что каждый вызов transforms.Normalize в torchvision, но «под капотом».
Шаг 5: Измените размер с тремя методами интерполяции
Сравните ближайшего соседа, билинейную и бикубическую интерполяцию при увеличении, чтобы разница была видна.
target = (arr.shape[0] * 3, arr.shape[1] * 3)
nearest = np.asarray(Image.fromarray(arr).resize(target[::-1], Image.NEAREST))
bilinear = np.asarray(Image.fromarray(arr).resize(target[::-1], Image.BILINEAR))
bicubic = np.asarray(Image.fromarray(arr).resize(target[::-1], Image.BICUBIC))
def local_roughness(x):
gy = np.diff(x.astype(float), axis=0)
gx = np.diff(x.astype(float), axis=1)
return float(np.abs(gy).mean() + np.abs(gx).mean())
for name, out in [("nearest", nearest), ("bilinear", bilinear), ("bicubic", bicubic)]:
print(f"{name:>8} shape={out.shape} roughness={local_roughness(out):6.2f}")
Ближайший сосед показывает наибольшую шероховатость, поскольку сохраняет жёсткие края. Билинейная интерполяция самая гладкая. Бикубическая находится между ними, сохраняя воспринимаемую резкость без ступенчатых артефактов.
Примените это
torchvision.transforms объединяет всё описанное выше в единый компонуемый конвейер. Код ниже в точности воспроизводит работу preprocess_imagenet и добавляет изменение размера и обрезку.
import torch
from torchvision import transforms
from PIL import Image
img = Image.fromarray(synthetic_rgb(256, 256))
pipeline = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
x = pipeline(img)
print(f"tensor type: {type(x).__name__}")
print(f"tensor dtype: {x.dtype}")
print(f"tensor shape: {tuple(x.shape)} # (C, H, W)")
print(f"per-channel mean: {x.mean(dim=(1, 2)).tolist()}")
print(f"per-channel std: {x.std(dim=(1, 2)).tolist()}")
batch = x.unsqueeze(0)
print(f"\nbatched shape: {tuple(batch.shape)} # (N, C, H, W) — ready for a model")
Четыре шага в строго таком порядке: Resize(256) масштабирует короткую сторону до 256; CenterCrop(224) берёт из центра фрагмент 224x224; ToTensor() делит на 255 и меняет HWC на CHW; Normalize вычитает среднее ImageNet и делит на std. Обратный порядок молча меняет то, что достигает модели.
Подготовьте к поставке
Этот урок создаёт:
outputs/prompt-vision-preprocessing-audit.md— промпт, превращающий любую карточку модели или набора данных в контрольный список точных инвариантов предобработки, которые должна соблюдать команда.outputs/skill-image-tensor-inspector.md— навык, который для любого тензора или массива формы изображения сообщает dtype, раскладку, диапазон и то, похож ли он на необработанный, нормализованный или стандартизованный.
Упражнения
- (Легко) Загрузите JPEG с OpenCV (
cv2.imread) и Pillow. Выведите обе формы и пиксель в(0, 0). Объясните разницу порядка каналов, затем напишите однострочное преобразование, делающее массив OpenCV идентичным массиву Pillow. - (Средне) Напишите
standardize(img, mean, std)и обратную ему функцию, которые вместе проходят тестroundtrip_max_diff <= 1на любом изображении uint8. Ваши функции должны одинаково работать для одиночного изображения HWC и пакета NCHW. - (Сложно) Возьмите трёхканальный тензор, стандартизованный по ImageNet, и пропустите его через свёртку 1x1, которая учится взвешенно смешивать RGB в один канал оттенков серого. Инициализируйте веса как
[0.299, 0.587, 0.114], заморозьте их и проверьте, что результат совпадает с ручнойrgb_to_grayscaleв пределах ошибки плавающей точки. Какие ещё классические преобразования цветовых пространств можно записать как свёртки 1x1?
Ключевые термины
| Термин | Как обычно говорят | Что это на самом деле означает |
|---|---|---|
| Пиксель | «Цветной квадрат» | Одно измерение интенсивности света в одной позиции сетки: три числа для цвета, одно для оттенков серого |
| Канал | «Цвет» | Одна из параллельных пространственных сеток, сложенных в тензор изображения; последняя ось в HWC, первая в CHW |
| HWC / CHW | «Форма» | Порядки осей тензора изображения; диск и PIL используют HWC, PyTorch и cuDNN — CHW |
| Нормализовать | «Масштабировать изображение» | Разделить на 255, чтобы пиксели оказались в [0, 1]; это необходимо, но недостаточно |
| Стандартизировать | «Центрировать относительно нуля» | Вычесть среднее и разделить на std для каждого канала, чтобы распределение входа совпало с тем, на котором обучалась модель |
| Преобразование в оттенки серого | «Усреднить каналы» | Взвешенная сумма с коэффициентами 0.299/0.587/0.114, соответствующая восприятию яркости человеком |
| Интерполяция | «Как изменение размера выбирает пиксели» | Правило, определяющее значения выхода, когда новая сетка не совпадает со старой: ближайший сосед для меток, билинейная для обучения, бикубическая для отображения |
| Соотношение сторон | «Ширина к высоте» | Отношение, отличающее «изменить размер и дополнить полями» от «изменить размер и растянуть» |
Дополнительное чтение
- Charles Poynton — A Guided Tour of Color Space — наиболее ясное техническое изложение причин существования стольких цветовых пространств и случаев, где каждое из них важно
- Документация PyTorch Vision Transforms — полный конвейер преобразований, который вы действительно будете компоновать в промышленной среде
- Как работает JPEG (Colt McAnlis) — наглядный обзор субдискретизации цветности, DCT и причин, по которым JPEG кодирует YCbCr, а не RGB
- Соглашения предобработки ImageNet (модели torchvision) — источник истины для
mean=[0.485, 0.456, 0.406]и объяснения, почему каждая модель в zoo ожидает эти значения
Источник: Image Fundamentals — Pixels, Channels, Color Spaces 03.13 — Отладка нейронных сетей · Фаза 4 — Компьютерное зрение · Полный каталог · 04.02 — Свёртки с нуля