5.16. Користувацькі ядра згортки¶
Фільтри за сусідством, розглянуті досі, мали вбудовану статистику, яку фільтр застосовував до вікна в кожній позиції: середнє значення, зважене за Гауссом середнє, медіана. morph() — це єдиний фільтр, що дозволяє застосунку самому задавати статистику у вигляді ядра: невеликої матриці ваг, яка описує, як фільтр повинен об’єднувати сусідні пікселі в одне вихідне значення.
Механізм базується на класичній операції згортки. У кожній вихідній позиції кожен сусідній піксель множиться на відповідну вагу ядра, добутки підсумовуються, результат за потреби масштабується та зміщується, і значення записується у вихідний піксель. Різні ядра дають різні результати для однакового вхідного сигналу. Ядро з рівними додатними вагами відтворює фільтр mean(); ядро у формі дзвоника відтворює gaussian(). Інші патерни дають відгуки на межах, рельєфне тиснення, градієнти, підвищення різкості, розмиття руху та великий каталог інших ефектів — усе те, що класичний аналіз зображень будь-коли намагався зробити за один лінійний прохід.
5.16.1. Метод morph¶
Сигнатура схожа на інші фільтри за сусідством, але має один додатковий аргумент:
img.morph(size, kernel, mul=1.0, add=0.0)
size — це радіус, що діє так само, як і скрізь, тому ядро повинно мати рівно (2 * size + 1) рядків та (2 * size + 1) стовпців. Саме ядро — це плаский список Python із відповідною кількістю чисел у порядку рядок за рядком: перші (2 * size + 1) елементів — верхній рядок, наступні (2 * size + 1) — другий рядок, і так далі до нижнього рядка. mul масштабує суму добутків перед записом у вихідний піксель, а add додає константу. Значення за замовчуванням mul=1.0 та add=0.0 залишають результат згортки незмінним.
Варто явно зазначити одну деталь: метод автоматично ділить суму добутків на суму елементів ядра перед записом результату. Це автоматичне ділення означає, що усереднювальне ядро, елементи якого у сумі дають дев’ять — наприклад, прямокутне розмиття 3×3 — виводиться в масштабі одна дев’ята без жодних зусиль, а ядро наближення Гауса, сума якого дорівнює шістнадцяти, виводиться в масштабі одна шістнадцята, і жодне з них не потребує обчислення ділення з боку застосунку. Застосунок встановлює mul лише тоді, коли потрібне додаткове масштабування понад автонормалізацію — або, що більш поширено, коли сума елементів ядра дорівнює нулю (ядро відгуку на межах) і автоділення призвело б до ділення на нуль. У такому випадку фреймворк вважає суму рівною одиниці, і mul стає єдиним регулятором для утримання нормованої суми добутків у діапазоні.
Пара threshold=True / offset=N з розділу адаптивного порогу також працює в morph(), тому той самий фреймворк користувацьких ядер може створювати бінарний поріг, граничне значення якого обчислюється за користувацькою статистикою.
5.16.2. Розміщення ядра¶
Ядро 3×3 (size=1) — це плаский список із дев’яти чисел, розташованих зліва направо, зверху донизу. Конвенція легко читається, якщо список розбити на три рядки Python:
sobel_x = [-1, 0, 1,
-2, 0, 2,
-1, 0, 1]
Це оператор градієнта Sobel-x — перше стандартне ядро, яке знадобиться будь-якому застосунку, і корисне для покрокового розгляду. Патерн прямолінійний: від’ємні ваги у лівому стовпці, додатні ваги у правому стовпці, нулі в центральному стовпці. Вагові коефіцієнти рядків -1, -2, -1 (або 1, 2, 1 праворуч) вищі в центрі, ніж у кутах, що надає центральному рядку більше впливу на результат, ніж кутовим рядкам.
Коли ядро проходить через вертикальну межу — стовпець пікселів, що переходить від темного зліва до яскравого справа — від’ємні ваги захоплюють темний бік, а додатні ваги захоплюють яскравий бік. Сума добутків є великим додатним числом, яке фільтр записує як яскравий вихідний піксель. Горизонтальна ділянка рівномірної яскравості дає нуль, оскільки кожній додатній вазі відповідає від’ємна вага тієї самої величини на піксель з тим самим значенням.
Запуск ядра:
img.morph(1, sobel_x, mul=0.25)
Сума елементів ядра Собеля дорівнює нулю — кожна від’ємна вага з лівого боку збалансована рівною додатною вагою з правого — тому автоділення нічого не ділить, і mul є єдиним масштабним коефіцієнтом суми добутків. mul=0.25 утримує відгук у діапазоні: найбільша абсолютна сума, яку Sobel-x може отримати з патча 3×3, становить приблизно 4 * 255 = 1020 (вісім яскравих пікселів з вагою до 2), і ділення на чотири зводить екстремальні випадки до 255, де формат чисто обрізає їх.
Відповідне ядро Sobel-y виявляє горизонтальні межі шляхом повороту того самого патерну ваг на 90 градусів:
sobel_y = [-1, -2, -1,
0, 0, 0,
1, 2, 1]
Застосунки, які хочуть виявляти будь-яку межу незалежно від напрямку, зазвичай запускають обидва Собелі та об’єднують відгуки.
5.16.3. Зміщення виводу¶
add — це інша половина масштабування. Відгук ядра з нульовою сумою є знаковим — позитивний з одного боку межі, від’ємний з іншого — і від’ємна половина зрізається до нуля при записі в беззнаковий піксель. add=128 зміщує відгук до середньо-сірого, тому від’ємні відгуки зберігаються як значення нижче 128, а додатні — вище: відгук на межах або рельєфне тиснення стають видимими в обох напрямках ціною зменшення діапазону вдвічі в кожному.
Яку комбінацію mul та add очікує ядро — це частина його конструкції; каталог стандартних ядер містить правильні налаштування для кожного поширеного ядра.
5.16.4. Більші ядра¶
Все на цій сторінці описано для ядер 3×3 (size=1), оскільки саме цей розмір використовується у стандартному каталозі і оскільки розміщення рядок за рядком легко записати вручну при такому розмірі. Втім, механізм не обмежує ядро розміром 3×3. size=2 запускає ядро 5×5 із двадцятьма п’ятьма елементами у пласкому списку; size=3 запускає ядро 7×7 із сорока дев’ятьма; і так далі, до будь-якого радіуса, за який застосунок готовий платити. Фреймворк підтримує як плаский список, так і вкладені рядки для будь-якого непарного розміру.
Причина звертатися до більшого ядра та сама, що й до більшого сусідства в будь-якому з вбудованих фільтрів: більше усереднення, ширше виявлення ознак, менша чутливість до шуму окремих пікселів. Вартість зростає як квадрат радіуса — ядро 5×5 виконує приблизно в 2,8 рази більше роботи на піксель порівняно з 3×3, а ядро 7×7 — приблизно в 5,4 рази — і цей множник безпосередньо впливає на частоту кадрів.
Практичний підхід — залишатися на size=1 для стандартного каталогу і переходити до більших розмірів лише тоді, коли алгоритм потребує більшого сусідства. Детектори меж рідко виграють від розміру понад 3×3; згладжувальні фільтри іноді виграють; правильний розмір залежить від масштабу ознак, які застосунок намагається підсилити або придушити.
5.16.5. Коли звертатися до morph¶
Для звичайного згладжування mean(), gaussian() та bilateral() є швидшими та чистішими. Для виявлення меж laplacian() та find_edges() спроектовані спеціально. Підстава для прямого звернення до morph() виникає тоді, коли застосунку потрібна конкретна згортка, яку вбудовані фільтри не надають — напрямковий Собель, користувацький шаблон меж, ядро, налаштоване на конкретну текстуру, яку решта конвеєра шукатиме, або будь-яке з стандартного каталогу корисних ядер, що класична обробка зображень накопичила за десятиліття. Повна гнучкість довільних ядер доступна; ціна полягає в тому, що застосунок сам відповідає за вибір значень ядра, що дають потрібний результат.