7.7. Нормализация

ml.Model.predict() принимает список входов, потому что у некоторых сетей более одного входного тензора, но список не позволяет встроенно передавать аргументы для отдельных входов – нет места для именованного аргумента вроде «обрезать этот вход до (x, y, w, h), но оставить остальные входы как есть». ml.preprocessing.Normalization – это обёртка, заполняющая этот пробел. Экземпляр Normalization содержит параметры для одного входа; скрипт передаёт обёрнутый вход в список predict всякий раз, когда ему нужно что-то отличное от значений по умолчанию.

Самая частая причина к ней обратиться – обрезать определённую область захваченного кадра для сети вместо всего изображения.

7.7.1. Параметры

Normalization(scale=(0.0, 1.0),
              mean=(0.0, 0.0, 0.0),
              stdev=(1.0, 1.0, 1.0),
              roi=None)
  • roi – прямоугольник (x, y, w, h) в исходном кадре, который обрезается перед масштабированием. По умолчанию – весь кадр. В большинстве случаев Normalization задаёт только этот параметр.

  • scale – диапазон (min, max), который входные тензоры с плавающей точкой ожидают после нормализации. Диапазон пикселей 0..255 линейно отображается в этот диапазон. Распространённые значения – (0.0, 1.0) для сетей, обученных с ReLU, и (-1.0, 1.0) для симметрично нормализованных сетей.

  • mean – среднее по каждому каналу (R, G, B), вычитаемое из изображения после масштабирования. Соответствует статистике каналов, на которой обучалась сеть – (0.485, 0.456, 0.406) для сетей на основе ImageNet – канонический пример. Сети в оттенках серого приводят среднее к значению яркости по стандартной формуле 0.299*R + 0.587*G + 0.114*B.

  • stdev – стандартное отклонение по каждому каналу (R, G, B), на которое изображение делится после вычитания среднего, опять же в соответствии со статистикой обучения сети. Для сетей в оттенках серого приводится к яркости тем же способом.

7.7.2. Когда параметры важны

scale, mean и stdev игнорируются, когда input_dtype сети равен int8 или uint8. Для сетей с целочисленным входом байты обрезанного изображения записываются в тензор напрямую, а собственные input_scale и input_zero_point сети выполняют преобразование из целого числа в действительное. Эти три параметра важны только тогда, когда сеть ожидает вход с плавающей точкой.

roi читается в любом случае – он определяет, какая часть исходного кадра достигает сети, независимо от входного dtype.

7.7.3. ROI и масштабирование

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

Имеет ли значение растяжение, зависит от сети. Модели обнаружения лиц и ориентиров вроде семейства MediaPipe (BlazeFace, FaceLandmarks, HandLandmarks, MoveNet) обучались на квадратных обрезках и быстро деградируют, когда соотношение сторон входа неверно; для них приложению нужно подавать квадратный ROI – либо захватывая в квадратном размере кадра через window(), либо обрезая с помощью параметра roi=. Детекторы объектов семейства YOLO обычно обучаются с аугментацией, включающей случайные растяжения, и принимают неквадратные ROI без значительной потери точности; передача всего захваченного кадра напрямую обычно подходит.

Когда входные размеры сети точно совпадают с ROI, масштабирование сводится к копированию, что является самым дешёвым случаем.

7.7.4. Переопределение значения по умолчанию

predict() автоматически оборачивает каждый вход image.Image с помощью Normalization() – с параметрами по умолчанию, описанными выше. Большинство моделей, поставляемых с камерой, обучались на диапазонах пикселей, которые значения по умолчанию уже покрывают, поэтому распространённый случай – передать изображение напрямую:

result = model.predict([img])

Чтобы использовать пользовательский ROI – самое распространённое переопределение – создайте Normalization с заданным ROI и привяжите к нему изображение:

from ml.preprocessing import Normalization

norm = Normalization(roi=(80, 60, 160, 120))
result = model.predict([norm(img)])

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

norm = Normalization(scale=(0.0, 1.0),
                     mean=(0.485, 0.456, 0.406),
                     stdev=(0.229, 0.224, 0.225))

result = model.predict([norm(img)])

Вызов экземпляра Normalization на изображении возвращает новый привязанный экземпляр, из которого движок заполняет тензор. Привязанный экземпляр – это то, что predict принимает вместо сырого изображения, и поскольку это объект для отдельного входа, многовходовая сеть может смешивать изображения с разными ROI в одном списке predict.

Для сетей, ожидающих входы, которые приложение уже сформировало в виде тензора – буфер от периферийного устройства, ndarray, вычисленный другим конвейером, нечисловые данные не изображения – полностью пропустите Normalization и передайте ndarray или вызываемый объект, который его формирует. predict() передаёт их движку без обёртывания.