5.17. Каталог стандартних ядер¶
Класична обробка зображень накопичила чималий каталог шаблонів ваг ядер, які зустрічаються знову і знову – детектори меж, загострювачі, тиснення, згладжувачі, розмиття руху – і всі вони виконуються через morph(). Кожен є коротким, кожен виконує одну річ, і більшість легко читаються, як тільки зрозуміла основна логіка ваг.
Ядра нижче є всі розміром 3 на 3, якщо не зазначено інше, тому всі вони використовують size=1 у виклику. Структура ваг кожного ядра описана поруч з ним, оскільки читання ваг і будує інтуїцію щодо того, чому одне ядро створює тиснення, а інше загострює.
5.17.1. Тотожне ядро¶
Найпростіше можливе ядро – це тотожне – одиниця в центрі, нулі скрізь:
identity = [0, 0, 0,
0, 1, 0,
0, 0, 0]
img.morph(1, identity)
Кожен вихідний піксель отримує своє значення з центру околиці, яким є вхідний піксель на тій самій позиції. Зображення проходить незміненим. Тотожне ядро не має практичного використання як фільтр, але є корисною базовою точкою для розуміння будь-якого іншого ядра: будь-яке нетотожне ядро є тотожним плюс деякі зміни.
Ядро, центральна вага якого велика з невеликими від’ємними вагами навколо нього, віднімає оточення від центру. Ядро з нульовою центральною вагою ігнорує сам піксель і реагує лише на відмінності між сусідами. Читання ядра таким чином – що центральна вага робить з пікселем, що оточуючі ваги додають або відбирають – є найшвидшим способом передбачити його ефект.
5.17.2. Виявлення меж¶
Ядра виявлення меж сильно реагують на позиції, де яскравість швидко змінюється в певному напрямку, і видають майже нульовий вихід там, де яскравість рівномірна. Це сімейство, ваги якого в сумі дають нуль: плоска ділянка (кожен піксель має однакове значення) видає нульовий вихід, тому що кожна позитивна вага точно компенсується від’ємною вагою рівної величини.
Sobel-x є канонічним прикладом. Він виявляє вертикальні межі (переходи яскравості ліворуч/праворуч):
sobel_x = [-1, 0, 1,
-2, 0, 2,
-1, 0, 1]
img.morph(1, sobel_x, mul=0.25, add=128)
Відповідний Sobel-y є таким самим шаблоном, повернутим на 90 градусів; він виявляє горизонтальні межі (переходи яскравості вгору/вниз):
sobel_y = [-1, -2, -1,
0, 0, 0,
1, 2, 1]
Середній рядок Sobel-x має ваги -2 та 2, а не -1 та 1. Додаткова вага на центральному рядку надає ядру невелике вбудоване згладжування в напрямку вздовж межі, що робить його більш стійким до шуму, ніж простіший оператор Prewitt, який відкидає ці додаткові величини:
prewitt_x = [-1, 0, 1,
-1, 0, 1,
-1, 0, 1]
prewitt_y = [-1, -1, -1,
0, 0, 0,
1, 1, 1]
Prewitt зважує кожен рядок рівномірно, тому його відгук дещо різкіший, ніж у Sobel, ціною більшої чутливості до шуму одного пікселя (вартість виконання ядра однакова – згортка виконує ту саму роботу незалежно від ваг). На чистому зображенні з чіткими межами це цілком прийнятна заміна Sobel.
Scharr йде в іншому напрямку. Його ваги більші і налаштовані для точного виявлення напрямку межі під дрібнішими кутами:
scharr_x = [-3, 0, 3,
-10, 0, 10,
-3, 0, 3]
img.morph(1, scharr_x, mul=0.0625, add=128)
Дільник mul=0.0625 (1/16) повертає вихід назад у діапазон 0 – 255 після більшої суми добутків. Scharr є правильним вибором, коли застосунку потрібна найбільш геометрично достовірна відповідь градієнта і він готовий платити дещо більшим обчисленням за це.
5.17.3. Лапласіан¶
Ядро Лапласіана реагує на межі в будь-якому напрямку одночасно. Там де кожен Sobel виявляє зміни яскравості вздовж однієї осі, симетричний шаблон ваг Лапласіана реагує однаково незалежно від того, в якому напрямку йде межа:
laplacian_4 = [ 0, -1, 0,
-1, 4, -1,
0, -1, 0]
img.morph(1, laplacian_4, add=128)
Структура: центральна вага 4, чотири горизонтальні/вертикальні сусіди з вагою -1, чотири діагоналі з нульовою вагою. Сума ваг ядра дорівнює нулю, тому плоскі ділянки видають нульовий вихід. Там де яскравість змінюється, центральне значення відрізняється від середнього чотирьох кардинальних сусідів, і вихід є розміром цієї різниці.
Варіант із 8 зв’язками включає діагональних сусідів:
laplacian_8 = [-1, -1, -1,
-1, 8, -1,
-1, -1, -1]
Кожне ядро виявляє дещо різні речі. Версія з 4 зв’язками видає чистіший вихід на горизонтальних і вертикальних межах; версія з 8 зв’язками є більш ізотропною – вона однаково добре реагує в кожному напрямку – але видає дещо більш зашумлений вихід. Ядро з 8 зв’язками також відоме під назвою outline (контур), за його використання для візуалізації меж.
5.17.5. Тиснення¶
Ядро тиснення (emboss) видає ефект підсвічування збоку, знайомий у класичних редакторах зображень. Вихід виглядає так, ніби зображення було витиснуто у рельєф і потім підсвічено з одного кута:
emboss = [-2, -1, 0,
-1, 1, 1,
0, 1, 2]
img.morph(1, emboss, add=128)
Хитрість полягає в асиметрії по діагоналі. Лівий верхній кут має найбільш від’ємну вагу, правий нижній – найбільш позитивну вагу, і діагональ від кута до кута іде від від’ємного через одиницю до позитивного. В кожному пікселі ядро по суті обчислює «яскравість праворуч-знизу мінус яскравість ліворуч-згори,» що є позитивним там де зображення стає яскравішим у цьому напрямку і від’ємним там де воно стає темнішим. Додавання 128 повертає підписаний вихід до середньо-сірого, щоб ефект був видимим.
Повертання асиметрії по іншій діагоналі створює тиснення з протилежного напрямку:
emboss_alt = [ 0, 1, 2,
-1, 1, 1,
-2, -1, 0]
img.morph(1, emboss_alt, add=128)
Два напрямки тиснення корисні в поєднанні – віднімання одного від іншого, або запуск кожного на тому самому зображенні та порівняння відгуків – коли застосунку потрібно виявити орієнтацію.
5.17.6. Згладжування¶
Ядра згладжування – це сімейство, ваги якого в сумі дають одиницю (і всі невід’ємні). Плоска ділянка через таке ядро видає ту саму плоску яскравість, тому що ядро усереднює значення пікселів разом, а не посилює їх відмінності.
Найпростішим є box blur (розмиття прямокутником), яке саме і обчислює mean():
box_blur = [1, 1, 1,
1, 1, 1,
1, 1, 1]
img.morph(1, box_blur)
Сума ваг ядра дорівнює 9, тому автоматичний поділ на суму ваг перетворює суму добутків на справжнє середнє по дев’яти пікселях околиці. На практиці mean() є кращим способом запустити це ядро – воно видає той самий вихід швидше, через шлях, оптимізований для обчислення середнього і нічого іншого, тоді як morph запускає загальну машинерію згортки. Box blur є в каталозі тому, що це правильна відправна точка для розуміння кожного іншого ядра згладжування.
Наближення 3 на 3 для ваг Gaussian надає центру та кардинальним сусідам більшу вагу, ніж кутам:
gaussian = [1, 2, 1,
2, 4, 2,
1, 2, 1]
img.morph(1, gaussian)
Ваги є рядком трикутника Паскаля 1, 2, 1, взятим зовнішнім добутком на себе. Центральна вага 4 є найбільшою, тому що центральний піксель найбільше впливає на свій власний вихід; кути мають значення 1, тому що вони найдальші від центру. Сума ваг ядра дорівнює 16, і автоматичний поділ на суму ваг виконує нормалізацію – аргумент mul не потрібен. Форма 3 на 3 є грубим наближенням справжнього Gaussian і невідрізненна від gaussian() при size=1; форма morph здебільшого корисна, коли застосунок хоче поєднати згладжування з іншою операцією в тому самому проході.
5.17.7. Розмиття руху¶
Ядро розмиття руху усереднює пікселі в одному напрямку, залишаючи перпендикулярний напрямок незмазаним. Найпростіший випадок – горизонтальний:
motion_h = [0, 0, 0,
1, 1, 1,
0, 0, 0]
img.morph(1, motion_h)
Середній рядок усереднює три пікселі вздовж горизонтальної осі; верхній та нижній рядки є нульовими. Сума ваг ядра дорівнює 3, тому автоматичний поділ на суму ваг видає справжнє середнє трьох пікселів без будь-якого mul. Вихід – горизонтально розмазана копія вхідного – ефект, який фіксує камера, коли суб’єкт рухається горизонтально під час витримки. Вертикальне розмиття руху є таким самим шаблоном, повернутим:
motion_v = [0, 1, 0,
0, 1, 0,
0, 1, 0]
Діагональне розмиття руху використовує головну діагональ:
motion_diag = [1, 0, 0,
0, 1, 0,
0, 0, 1]
img.morph(1, motion_diag)
Ядра розмиття руху корисні як ефект (навмисне розмиття кадру з візуальною метою), так і як тестовий шаблон для алгоритмів, яким потрібно бути стійкими до артефактів руху (запустіть алгоритм на вхідному зображенні з розмиттям руху і перевірте, що він все одно видає правильну відповідь).
5.17.8. Читання ядер з першого погляду¶
Кілька правил великого пальця полегшують читання нових ядер з першого погляду:
Сума один з невід’ємними вагами ⇒ згладжування (зберігає середню яскравість).
Сума нуль з позитивними та від’ємними вагами ⇒ відгук на межу (нуль на плоских ділянках).
Сума один з великим позитивним центром і невеликими від’ємними оточеннями ⇒ загострення (тотожне плюс відгук на межу).
Асиметричне по діагоналі із сумою один ⇒ тиснення (підсвічує один бік кожного переходу яскравості).
Сконцентроване вздовж однієї осі із сумою один ⇒ спрямоване розмиття.
Перше з цих правил, якому відповідає ядро, зазвичай є правильною здогадкою щодо того, що воно робить. Більшість корисних ядер розпізнаються лише за схемою розташування їх ваг.
Коли жодне зі стандартних ядер не робить того, що хоче застосунок, наступним кроком є ручне налаштування одного. Поєднання наведених вище правил і елементів керування mul / add охоплює майже кожен лінійний прохід, який будь-коли потребував класичний конвеєр технічного зору; звідти це питання спроб ваг, перегляду виходу та ітерацій.