Недавно я изучаю компьютерное зрение и наткнулся на функцию resize в OpenCV. Для изменения размера изображения необходим способ вычисления значений пикселей для нового изображения по сравнению с исходным. Пять таких методов интерполяции, предоставляемые OpenCV, - это INTER_NEAREST, INTER_LINEAR, INTER_AREA, INTER_CUBIC и INTER_LANCZOS4.

Среди этих пяти методов четыре довольно легко догадаться, как они выполняют интерполяцию. INTER_NEAREST использует интерполяцию ближайшего соседа; INTER_LINEAR билинейный; INTER_CUBIC - бикубическая функция; и INTER_LANCZOS4 - это синусоидальный метод. Однако INTER_AREA относительно загадочен, поскольку документ OpenCV описывает его следующим образом:

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

Что это значит под «отношением площади пикселей»? Возможно, я новичок, и это те общие знания в области компьютерного зрения, которые знает каждый инсайдер. Поэтому я погуглил термин, надеясь найти статьи в Википедии, сообщения в блогах или, может быть, статью. Оказывается, что в большинстве случаев, когда люди упоминают эти методы интерполяции, они просто перефразируют краткое объяснение выше или просто копируют его. И это включает ответы на таких сайтах, как StackOverflow.

В конце концов, я решил сам почитать исходный код. И оказывается, что метод «истинной площади» очень интуитивно понятен, и в зависимости от некоторых условий INTER_LINEAR может быть INTER_AREA, и наоборот.

Поэтому я написал эту статью, чтобы помочь другим, которым тоже может быть интересно, что INTER_AREA делает. И я объясню это дальше.

В зависимости от того, насколько мы хотим масштабировать исходное изображение, INTER_AREA может делать разные вещи.

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

inv_scale_x и inv_scale_y - это ширина выходного изображения по сравнению с исходной шириной изображения и высота выходного изображения по сравнению с исходной высотой изображения, соответственно. Однако scale_x и scale_y - ширина исходного изображения превышает ширину выходного изображения, а высота исходного изображения превышает высоту выходного изображения. Другими словами:

double scale_x = 1./inv_scale_x, scale_y = 1./inv_scale_y;

Также есть две целочисленные версии, iscale_x и iscale_y, которые равны satuarate_cast<int> из scale_x и scale_y соответственно.

Уменьшение изображений

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

Если это так, то логическая переменная is_area_fast будет установлена ​​в true. Следуя этому:

  1. Если используется метод интерполяции INTER_LINEAR_EXACT, и, мы хотим уменьшить исходное изображение до его половинного размера, т. Е. iscale_x == 2 && iscale_y == 2, и количество каналов изображения не 2, тогда функция resize фактически использует INTER_AREA.
  2. Если используется метод интерполяции INTER_LINEAR, и, мы хотим уменьшить исходное изображение до его половинного размера, то есть iscale_x == 2 && iscale_y == 2, тогда функция resize фактически использует INTER_AREA.

Метод «истинной площади» применяется только в тех случаях, когда мы не увеличиваем изображение. Таким образом, два вышеуказанных пункта также удовлетворяют. Эта «истинная область» работает следующим образом.

Сначала мы объясним случай, когда is_area_fast равно true. Переменная названа так, потому что будет вызвано семейство функций, использующих параллельные вычисления. Мы можем представить изображение RGB в виде куба. На рисунке 1 показана такая схематическая иллюстрация строки изображения. Каждый маленький кубик представляет собой пиксель. Цифры - это индексы.

Указатель на массив int xofs указывает на массив, который содержит начальные индексы пикселей в строке, которые будут усреднены на более позднем этапе. Например, предположим, что мы хотим уменьшить изображение до трети по ширине и высоте. Потом iscale_x = 3 и iscale_y = 3. Что содержит xofs? Это 0, 1, 2, 9, 10, 11 и т. Д. Обратите внимание на шаблон? Мы берем все индексы в направлении канала каждые iscale_x столбец. Далее будет видно, что пиксели с индексами 3, 4, 5, 6, 7, 8 будут объединены с пикселями 0, 1, 2 для вычисления средневзвешенного значения. Таким образом, xofs отмечает начальные границы всех таких блоков усреднения.

Затем мы вычисляем переменную area, которая равна iscale_x * iscale_y. В нашем примере area = 9.

Другой указатель на массив int ofs указывает на массив area числа индексов. Эти индексы представляют собой смещения в направлениях x и y, так что они образуют окно для каждого канала, а количество пикселей в этом окне равно area. В нашем примере ofs указывает на массив из 9 элементов. Предположим, что наше исходное изображение имеет ширину 9 пикселей. Тогда каждая строка изображения имеет 27 пикселей с учетом 3 каналов. Следовательно, первые три индекса ofs - это 0, 3, 6. А затем следующие три - 27, 30, 33. Затем, наконец, последние три - 54, 57, 60. Это проиллюстрировано на рисунке 2 (конечно, ширина изображения на рисунке не 9…).

В этом окне мы суммируем значения пикселей в нем, а затем делим на area. Результат - значение пикселя выходного изображения. Другими словами, алгоритм просто вычисляет среднее значение пикселей в штучной упаковке. Это усреднение выполняется для каждого индекса в xofs, заканчивая каждый канал.

Довольно интуитивно понятно и просто, не правда ли?

Это для шкал с целочисленным значением. Для нецелочисленного сжатия идея все та же, но вес будет меняться в зависимости от того, сколько пикселей исходного изображения включено в окно (обратите внимание, что на этот раз размер окна имеет десятичные значения). Для ряда пикселей это можно увидеть на рисунке 3, где разные цвета представляют, какие пиксели включаются при каждом перемещении окна. То же самое относится к направлению y.

Вес каждого пикселя - это доля, включенная умножить на 1/area. Например, если в окне содержится 0,3 пикселя, то его вес составляет 0,3 / площадь.

Мы можем проверить это на простом примере:

import numpy as np
import cv2
# Create a numpy array, pretending it is our image
img = np.array([[3, 106, 107, 40, 148, 112, 254, 151],
                [62, 173, 91, 93, 33, 111, 139, 25],
                [99, 137, 80, 231, 101, 204, 74, 219],
                [240, 173, 85, 14, 40, 230, 160, 152],
                [230, 200, 177, 149, 173, 239, 103, 74],
                [19, 50, 209, 82, 241, 103, 3, 87],
                [252, 191, 55, 154, 171, 107, 6, 123],
                [7, 101, 168, 85, 115, 103, 32, 11]],
                dtype=np.uint8)
# Resize the width and height in half
resized = cv2.resize(img, (img.shape[1]/2, img.shape[0]/2),
                     interpolation=cv2.INTER_AREA)
print(resized)
# Result:
# [[ 86  83 101 142]
# [162 103 144 151]
# [125 154 189  67]
# [138 116 124  43]]

Увеличение изображений

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

Для билинейного метода, предполагая, что мы рассматриваем одномерное изображение для простоты, значение интерполированного пикселя представляет собой средневзвешенное значение двух исходных соседних пикселей. Веса зависят от расстояний до двух пикселей. Близость означает больший вес. И значения веса изменяются линейно с расстоянием. Но прежде чем мы продолжим, я хотел бы поговорить о том, как на самом деле рассчитываются коэффициенты (веса) в OpenCV.

Для такого новичка, как я, я бы подумал, что масштабирование строки типа [0, 1] до 4 столбцов будет [0, 0.3, 0.6, 1]. Однако это не поведение resize OpenCV. И MATLAB imresize тоже.

Код для вычисления коэффициентов в направлении x для билинейного метода в OpenCV для каждого индекса dx пикселя выходного изображения равен

fx = (float)((dx+0.5)*scale_x - 0.5);    // (1)
sx = cvFloor(fx);                        // (2)
fx -= sx;                                // (3)

Напомним, что scale_x - это соотношение между шириной входного изображения и шириной выходного изображения. В (1) fx - это индекс пикселя «с плавающей запятой» входного изображения в направлении x, тогда как в (3) fx - это коэффициент интерполяции для правого пикселя в Направление x (т. е. коэффициенты интерполяции - это пара 1-fx и fx). Итак, откуда взялось уравнение в (1)?

Изобразить одномерное изображение можно следующим образом, показанным на рисунке 4.

На рисунке 4 мы показываем системы координат для одномерного изображения. Мы предполагаем, что пиксели расположены в середине прямоугольников пикселей, а границы имеют свои собственные координаты. Обратите внимание, что в MATLAB индекс начинается с 1.

Теперь то, что делает (1), - это сопоставлять координаты выходного изображения с исходным входным изображением. Поскольку это линейная интерполяция, для получения линейной функции нам нужны два уравнения. Первое уравнение исходит из требования, чтобы левые границы входного и выходного изображений имели одинаковые координаты. Это означает, что -0,5 в системе координат выходного изображения должно быть сопоставлено с -0,5 в системе координат входного изображения. Во-вторых, расстояние inv_scale_x в системе координат выходного изображения должно быть равно 1 в системе координат входного изображения. Решая линейную функцию, мы получаем выражение (1). Код MATLAB для этого отображения записывается как

u = x/scale + 0.5 * (1 - 1/scale);

который можно найти в images.internal.resize.contributions. Обратите внимание, что в коде MATLAB x - координата выходного изображения, u - координата входного изображения и scale - отношение ширины выходного изображения к ширине входного изображения, то же самое, что inv_scale_x в OpenCV. Итак, [0, 1] фактически превратился бы в [0, 0.25, 0.75, 1]. На рисунке 5 показаны коэффициенты для левого пикселя для 100 dx с, когда ширина выходного изображения в два раза больше входного изображения.

Однако INTER_AREA использует другую стратегию расчета весов. И поведение также немного отличается в зависимости от того, является ли коэффициент масштабирования целым или нет.

Если коэффициент масштабирования является целым числом

Код для вычисления коэффициента интерполяции (веса) в направлении x для каждой позиции пикселя выходного изображения dx:

sx = cvFloor(dx*scale_x);
fx = (float)((dx+1) - (sx+1)*inv_scale_x);            
fx = fx <= 0 ? 0.f : fx - cvFloor(fx);

а также

cbuf[0] = 1.f - fx;            
cbuf[1] = fx;

Расчет в направлении y такой же. Здесь cbuf содержит фактические коэффициенты, где первый применяется к левому пикселю, а второй - к правому.

Поскольку мы предполагаем, что inv_scale_x является целым числом, то fx по сути эквивалентно

int inv_scale_x_integer = saturate_cast<int>(inv_scale_x);
fx = dx % inv_scale_x_integer + 1 - inv_scale_x_integer;

Мы видим, что fx не может быть больше 1. На самом деле максимально возможное значение равно 0. Плюс это утверждение:

fx = fx <= 0 ? 0.f : fx - cvFloor(fx);

Это означает, что когда inv_scale_x является целым числом, fx всегда равно 0, а коэффициенты всегда равны 1 и 0. Это также означает, что интерполированное значение является копией левого пикселя. Мы можем убедиться в этом с помощью простого теста:

img = np.array([[ 86  83 101 142]
                [162 103 144 151]
                [125 154 189  67]
                [138 116 124  43]], dtype=np.uint8)
enlarged = cv2.resize(img, (8, 8), interpolation=cv2.INTER_AREA)
print(enlarged)
# Result:
#[[ 86  86  83  83 101 101 142 142]
# [ 86  86  83  83 101 101 142 142]
# [162 162 103 103 144 144 151 151]
# [162 162 103 103 144 144 151 151]
# [125 125 154 154 189 189  67  67]
# [125 125 154 154 189 189  67  67]
# [138 138 116 116 124 124  43  43]
# [138 138 116 116 124 124  43  43]]

Если коэффициент масштабирования не является целым числом

Если inv_scale_x или inv_scale_y не является целым числом, тогда коэффициенты интерполяции больше не равны 1 и 0. Это все еще похоже на целочисленный случай. Просто по модулю выполняется действительное число. Хотя это не совсем точно определено, мы все же можем понять смысл. На рисунке 6 показаны коэффициенты для левого пикселя, рассчитанные в 100 dx положениях, где inv_scale_x равно 5,6:

Соблюдайте закономерность. Наш масштабный коэффициент равен 5,6. Изображение, которое мы назначаем каждой позиции, и для каждой позиции максимальное количество, которое мы можем присвоить, равно 1. Затем с самого начала мы берем 5 единиц, а затем остается только 5,6–5 = 0,6, поэтому следующий коэффициент равен 0,6. После этого нам нужно разделить еще 5,6. В прошлый раз у нас осталось 0,4. Мы берем эту сумму из нового 5.6, как если бы предыдущее 0.6 было равно 1. Теперь мы продолжаем присваивать значения так, чтобы в каждой позиции мы присвоили полную сумму, которая равна 1. Затем, назначив еще 5 единиц, мы имеем только 0,2 осталось. Итак, следующий - 0,2.

Резюме

Подводить итоги,

  • Когда выходное изображение не больше входного как по ширине, так и по высоте:

- Масштаб ввода / вывода по ширине и высоте является целым числом:

  1. Если ширина и высота уменьшены вдвое, а количество каналов не равно 2, тогда INTER_LINEAR_EXACT будет INTER_AREA;
  2. Если ширина и высота уменьшены вдвое, тогда INTER_LINEAR будет INTER_AREA;

INTER_AREA - это коробочная / оконная передискретизация.

  • Когда выходное изображение больше входного изображения по ширине или / или высоте:

- Масштаб ввода / вывода по ширине и высоте является целым числом:

INTER_AREA - билинейная интерполяция с коэффициентами (1, 0).

- Иначе:

INTER_AREA - это билинейная интерполяция с немного более сложными значениями коэффициентов.