Усе, що ми робили із зображенням досі, мало спільну властивість, яку легко не помітити. Обрізання, поворот і зміна розміру переставляли пікселі, не змінюючи значень. Яскравість, контраст і переведення в сірий міняли кожен піксель окремо. Жодна операція ані разу не спитала, що лежить поруч.
А майже все цікаве в зображенні живе саме в «поруч». Межа предмета — це не властивість одного пікселя, а перепад між сусідніми. Розмитість — це те, наскільки схожі сусіди. Текстура, кут, подряпина на екрані телефона — відношення між пікселями, а не значення в одному з них.
Операція, яка вміє питати «а що навколо?», зветься згорткою (convolution). Саме з неї зроблені всі сучасні моделі зору — і саме тому ми пройдемо її повільно й руками: порахуємо олівцем, напишемо подвійним циклом, звіримо з бібліотекою й дійдемо до детектора меж Кенні, розібравши його на чотири окремі операції.
СусідиПіксель дивиться на сусідів
У попередній темі ми навчилися читати, масштабувати, обрізати й розфарбовувати зображення засобами OpenCV. Усі ті операції поділялися на дві родини, і жодна не виходила за межі одного пікселя.
| родина | що робить | приклади |
|---|---|---|
| Переставити | число лишається тим самим, змінюється лише його адреса | обрізання, поворот, дзеркало, зміна розміру |
| Перерахувати | кожен піксель окремо проходить через формулу | яскравість, контраст, поріг, сірий, HSV |
| Подивитись навколо | нове значення залежить від сусідів | розмиття, різкість, пошук меж |
Третій рядок — це вся ця тема. Як «подивитись навколо» й отримати одне число? Найпростіша чесна відповідь: узяти невеликий квадрат сусідів, дати кожному вагу і скласти. Ваги вирішують усе.
Однакові додатні ваги дають середнє по околиці — розмиття. Велика додатна вага в центрі проти маленьких відʼємних у сусідів — центр «перекричить» околицю, і перепади стануть крутішими, тобто різкість. Відʼємні ваги ліворуч і додатні праворуч дають різницю «правий бік мінус лівий» — вертикальну межу. Три різні ефекти, а механіка одна: маленька табличка чисел, помножена на квадрат сусідів. Табличка зветься ядром (kernel), інколи — фільтром.
МеханікаЯдро й операція по кроках
Ядро — маленький масив чисел, майже завжди непарного розміру: 3 × 3, 5 × 5, 9 × 9. Непарність потрібна, щоб у ядра був однозначний центр — саме він накладається на піксель, значення якого ми рахуємо.
Операція складається з чотирьох дій, які повторюються для кожного пікселя:
- поставити ядро центром на черговий піксель;
- перемножити кожне число ядра з тим пікселем, під яким воно опинилось;
- додати всі добутки — вийде одне число;
- записати його у вихідне зображення, у позицію, де стояв центр.
Одне застереження, бо це джерело плутанини: сума кладеться в нове зображення, а не в старе. Інакше наступне вікно читало б уже перероблені числа, і результат залежав би від порядку обходу.
Велика грецька літера Σ («сигма») означає «додай усе, що йде далі,
перебравши індекс». Сигм тут дві: перша перебирає рядки ядра (i), друга —
стовпці (j), разом девʼять добутків. r і c —
номер рядка (row) і стовпця (column) пікселя, який ми рахуємо; віднята одиниця зсуває
вікно так, щоб центр ядра припав рівно на [r, c]. Формула потрібна лише
щоб ти впізнав цей запис у документації.
Крихітне зображення 5 × 5: ліві три колонки темні (12…20), праві дві світлі (90…96). Між третьою й четвертою колонками — різка межа. Ядро — розмиття 3 × 3, де всі девʼять коефіцієнтів дорівнюють одній девʼятій, тобто приблизно 0.1111.
Ставимо ядро в лівий верхній кут. Під ним квадрат 12, 14, 15 / 13, 15, 16 / 14, 16, 18. Множимо кожне число на 0.1111 і додаємо:
Коли ваги однакові, є коротший шлях: скласти девʼять чисел (133) і поділити на девʼять.
Вікно рухається на колонку праворуч, і в суму вже потрапили три світлі числа: 367 ÷ 9 = 40.78. Одне світле вторгнення підняло значення майже втричі.
Вікно має де стояти лише в девʼяти положеннях: центр не може вилізти за край. Тому з масиву 5 × 5 вийшов результат 3 × 3:
Ліва колонка — чиста темна ділянка, права — чиста світла, середня показує межу, розмазану по трьох колонках. Розмиття не стерло межу — воно її розтягнуло.
Тепер прожени те саме на очах. Кнопка «крок» пересуває вікно на один піксель: підсвічуються девʼять множень, унизу росте сума, і клітинка результату заповнюється тим самим числом, яке ми щойно рахували руками.
Ядро по кроках
Вікно 3 × 3 ковзає по зображенню 5 × 5. Ліворуч — вхід із підсвіченим вікном, посередині — ядро й девʼять добутків, праворуч — результат, який заповнюється на очах.
ПасткаКореляція проти згортки
А тепер головна несподіванка теми, і стосується вона не математики, а того, що насправді роблять бібліотеки. Те, що ми щойно порахували, у математиці зветься не згорткою, а кореляцією (cross-correlation). Справжня згортка відрізняється однією дією: перед роботою вона перевертає ядро на 180° — відбиває його й по горизонталі, і по вертикалі.
Знаки при i та j у другому рядку відʼємні: індекс зображення
йде назустріч індексу ядра. Це те саме, що повернути ядро на пів оберта
й далі рахувати звичайну кореляцію.
Питання на мільйон: що робить cv2.filter2D — функція, якою в OpenCV
рахують «згортку»? Відповідь перевірено запуском: вона рахує кореляцію.
Звірка зі scipy.ndimage, де обидві операції є окремими функціями:
Третій рядок — рецепт: щоб отримати в OpenCV справжню згортку, ядро треба перевернути
самому (np.flip), а вже потім віддати у filter2D.
np.allclose з самим ядром
і з np.flip(ядро) окремо. Симетричне ядро для цієї перевірки не годиться
взагалі.Звідси й уся плутанина: найпоширеніші ядра симетричні. Розмиття, гаус, лапласіан після повороту на 180° збігаються самі із собою, тому кореляція й згортка дають на них побайтово однаковий результат. Різниця зʼявляється рівно тоді, коли ядро несиметричне. На нашому фрагменті фото таке ядро дало розбіжність до 38.7 рівня яскравості у 53.7 % пікселів — це не похибка округлення, а дзеркальний результат.
Кореляція проти згортки
Ліворуч — ядро й відгук на одиничний імпульс числами. Праворуч — те саме фото, оброблене двома способами. Перемкни ядро на симетричне — і різниця зникне.
Чому ж тоді всі говорять «згортковий шар»? Бо для навченого ядра різниці немає: коли коефіцієнти не пишуть, а підбирають навчанням, мережа однаково знайде матрицю, яка дає потрібний результат, — хай навіть у дзеркальному вигляді. Назва прижилась історично, і міняти її пізно. Але поки ядро пишеш ти, різниця цілком реальна.
СумаНормування: сума 1 або 0
У будь-якого ядра з підручника сума коефіцієнтів дорівнює або одиниці, або нулю. Це не традиція, а прямий наслідок арифметики.
Уяви рівну ділянку, де всі пікселі мають яскравість 100. Сума добутків дорівнює 100, помноженому на суму коефіцієнтів. Якщо та сума одинична, на виході теж буде 100 — рівне лишилось рівним. Якщо двійка, вийде 200: картинка стане вдвічі яскравішою без жодної причини.
Сума 1 — ядро перерозподіляє. Загальна яскравість зберігається, міняється лише те, як вона розкладена між сусідами: розмиття, гаус, різкість, тиснення. Перевірка на нашому фото: середня яскравість входу 162.56, після розмиття — теж 162.56, після ядра різкості — знову 162.56.
Сума 0 — ядро шукає різницю. На рівній ділянці результат нульовий, хай яка там яскравість: хоч 10, хоч 200. Ненульове значення зʼявляється тільки там, де сусіди відрізняються. Це ядра-детектори: Собель, лапласіан, будь-який пошук межі. Середнє після Собеля на нашому фото — 0.21, тобто практично нуль.
А що буває, якщо забути? Найпоширеніша помилка — узяти бокс-фільтр із девʼятьма одиницями й не поділити на девʼять. Сума стає девʼяткою, середня яскравість стрибає зі 162.6 до 1463, 100 % чисел вилітає за 255 — і після обрізання від фото лишається біла пляма з рівно одним унікальним значенням.
Конструктор ядра
Девʼять коефіцієнтів, які можна вписати руками. Праворуч — результат, унизу — скільки значень вилетіло за межі 0…255.
КраїКраї зображення
Повернімося до кроку, який у розділі 2 ми обійшли мовчки. Коли вікно 3 × 3 стоїть на кутовому пікселі, пʼять із девʼяти клітинок висять за межами зображення — читати там нічого. Для ядра 9 × 9 зіпсованою стає вже рамка завширшки в чотири пікселі по периметру.
- Обрізати результат (режим
valid): рахуємо тільки там, де вікно помістилось цілком. Чесно, але зображення меншає — з 240 × 320 після ядра 9 × 9 лишиться 232 × 312. - Добудувати кадр навколо зображення й порахувати все. Розмір зберігається, але значення на краю ми частково вигадали.
- Добудувати й потім обрізати — компроміс конвеєрів обробки: зіпсовану рамку відкидаємо наприкінці, коли всі фільтри відпрацювали.
Другий варіант стоїть за замовчуванням майже скрізь, тому питання «чим саме заповнити кадр» практичне. OpenCV дає чотири відповіді; подивимось на них на масиві з чисел від 0 до 8, до якого дописано рамку в один піксель:
| режим межі | верхній рядок після добудови | що робить |
|---|---|---|
BORDER_CONSTANT | 0 0 0 0 0 | заповнює сталою, звичайно нулем |
BORDER_REPLICATE | 0 0 1 2 2 | розмножує крайній піксель |
BORDER_REFLECT | 0 0 1 2 2 | дзеркалить разом із крайнім |
BORDER_REFLECT_101 | 4 3 4 5 4 | дзеркалить від крайнього, не дублюючи |
Різниця між двома останніми — в одному пікселі. REFLECT відбиває так,
що крайній піксель зустрічається двічі поспіль: у верхньому рядку видно «0 0».
REFLECT_101 відбиває від крайнього, не повторюючи його, і тому
у верхньому рядку опиняються значення з другого рядка оригіналу. Саме він стоїть
за замовчуванням у більшості функцій OpenCV: не створює на краю штучної сходинки
з двох однакових пікселів.
Тепер найважливіше — чому це має значення саме для згортки. Тема 03 уводила режими межі як технічну подробицю. Тут вони подробицею бути перестають, бо режим межі створює структуру, якої в зображенні не було, і наступний фільтр її чесно знайде. Ось кутовий піксель нашого фото після гаусового розмиття 9 × 9:
| режим межі | кут (0, 0) | центр (120, 160) |
|---|---|---|
| було до фільтрації | 199.00 | — |
BORDER_CONSTANT | 78.85 | 119.03 |
BORDER_REPLICATE | 200.95 | 119.03 |
BORDER_REFLECT | 201.60 | 119.03 |
BORDER_REFLECT_101 | 202.10 | 119.03 |
Центр однаковий у всіх чотирьох — до нього кадр не дотягується. А кут відрізняється
утричі: BORDER_CONSTANT домішує чорноту, якої в кадрі
не було, і світлий кут стільниці темніє з 199 до 79. По периметру зʼявляється темна
рамка, і детектор меж потім чесно знайде вздовж краю різку межу, якої не існує.
Що саме підставляється на краю
Ліворуч — лівий верхній кут зображення числами: сірі клітинки справжні, кольорові — добудований кадр. Праворуч — той самий кут після розмиття.
REFLECT_101 — того,
що стоїть за замовчуванням. У трьох режимах воно мале, у CONSTANT —
величезне, і темна рамка по краю видно неозброєним оком.РозмиттяРозмиття: бокс проти гауса
Найпростіше розмиття — те, яке ми рахували руками: усі коефіцієнти однакові. Воно
зветься бокс-фільтром (box filter), в OpenCV це cv2.blur.
Кожен піксель стає середнім по квадрату сусідів.
Вада бокса геометрична. Квадрат 9 × 9 дає однакову вагу сусідові збоку, який стоїть за чотири пікселі, і сусідові в кутку, який стоїть за 4 × √2 ≈ 5.7 пікселя — на 41 % далі. Розмиття перестає бути однаковим у всі боки: по діагоналях воно розмазує сильніше, і яскрава точка перетворюється не на кружечок, а на квадрат.
Це міряється числом. Візьмімо квадрат ядра й уписане в нього коло; порахуймо, яка частка ваги лежить поза цим колом, тобто в кутах:
| sigma | розмір ядра | вага в кутах: гаус | вага в кутах: бокс |
|---|---|---|---|
| 1.0 | 7 × 7 | 1.06 % | 40.82 % |
| 1.5 | 9 × 9 | 2.73 % | 39.51 % |
| 2.0 | 13 × 13 | 0.93 % | 33.14 % |
| 3.0 | 19 × 19 | 0.86 % | 29.92 % |
У бокса в кутах лишається близько третини всієї ваги, у гауса — один-три відсотки. Ось звідки беруться «квадратні» артефакти.
Гаусове ядро будують інакше: вага сусіда спадає з відстанню за формулою дзвону — найбільша в центрі, на краю ядра майже нульова.
Тут d — відстань від центра ядра до клітинки в пікселях, а
σ («сигма») — єдиний параметр, що задає ширину дзвону. Літера
e — стала приблизно 2.718; підносити її до відʼємного степеня означає
«швидко спадати». Чим більша σ, тим повільніше спадає вага
і тим ширше розмиття.
Розмір ядра й σ плутати не можна: σ вирішує, як сильно розмиває, розмір — де ядро обривають. На відстані трьох σ вага вже мізерна, тому звичайне правило — розмір близько 6σ + 1, округлений до непарного. Ядро 3 × 3 із σ = 5 виродиться в бокс, бо всі коефіцієнти вийдуть майже однакові; ядро 31 × 31 із σ = 0.5 складатиметься переважно з нулів, а часу зʼїсть у сто разів більше.
Тепер поверни око на таблицю вище — там один рядок зумисне порушує це правило. Для σ = 1.5 правило дало б 6·1.5 + 1 = 10, тобто ядро 11 × 11, а в таблиці стоїть 9 × 9: це наскрізне ядро цієї теми, і воно обрізане раніше, ніж радить правило. Наслідок видно в тій самій таблиці: вага в кутах гауса там 2.73 % проти 0.86-1.06 % у сусідніх рядках, бо дзвін обірвано ще тоді, коли в нього лишалась помітна вага. Розширивши це саме ядро до 11 × 11, дістанемо 0.32 % — рядок вертається до решти. Ось що на ділі означає «де ядро обривають».
Бокс проти гауса
Ліворуч — фото після розмиття. Посередині — відгук на одну яскраву точку в збільшенні: саме тут видно квадрат проти кружечка. Праворуч — переріз ядра.
ЕкономіяРоздільність: 81 проти 18
Гаусове ядро має властивість, заради якої його й люблять інженери. Двовимірне ядро 9 × 9 можна не застосовувати цілком: досить пройти зображення двічі одновимірним ядром з девʼяти чисел — спершу по рядках, потім по стовпцях. Результат буде той самий.
Причина в тому, що двовимірний дзвін розкладається на добуток двох одновимірних: вага клітинки залежить окремо від зсуву по горизонталі й окремо від зсуву по вертикалі. Ядро, для якого так можна, називають роздільним (separable).
Вигода на числах. Двовимірне ядро 9 × 9 вимагає 81 множення на піксель. Два одновимірні проходи — по девʼять кожен, разом 18. Це в 4.5 раза менше роботи, і виграш росте з розміром: для 21 × 21 буде 441 проти 42, тобто вже вдесятеро.
cv2.filter2D із двовимірним ядром проти cv2.sepFilter2D
із двома одновимірними: np.allclose істинний при допуску
10−3, найбільша різниця — 2 × 10−13.
Це похибка чисел із рухомою комою, а не розбіжність алгоритмів.
Двісті проходів двовимірним ядром — 0.156 с, двома одновимірними — 0.021 с. Реальне прискорення 7.3 раза, навіть більше за теоретичні 4.5: одновимірний прохід іще й приязніший до кешу процесора.
Ідею варто запамʼятати надовго: коли операція розкладається на послідовність дешевших із тим самим результатом — це майже завжди перемога. Блок 2 застосує ту саму думку до згорткових мереж.
[1, 2, 1] упоперек і [−1, 0, 1] уздовж). А довільна
матриця з девʼятьма випадковими числами — ні. Перевіряють це рангом матриці:
роздільні саме ті ядра, у яких ранг дорівнює одиниці.РізкістьРізкість — це мінус розмите
«Додати різкості» звучить як щось протилежне розмиттю, тож перше припущення новачка — що має бути якесь «зворотне розмиття». Насправді навпаки: різкість роблять за допомогою розмиття.
Думка в одному реченні: розмиття знищує дрібні деталі, отже, різниця «оригінал мінус розмите» — це і є ті деталі у чистому вигляді. Додай їх назад — і вони стануть удвічі виразнішими.
Тут сила — звичайний множник: 0 нічого не міняє, 1 подвоює деталі.
Прийом зветься unsharp mask, «маска нерізкості», і назва спантеличує:
різкість роблять через нерізку копію. Пішла вона з доцифрової фотографії, де для цього
справді брали розмиту негативну плівку.
Другий спосіб — одне ядро замість трьох дій:
Сума коефіцієнтів дорівнює одиниці — яскравість зберігається. Центр із вагою 5 «перекрикує» чотирьох сусідів із вагою −1. На рівній ділянці результат дорівнює самому пікселю: 5 × 100 − 4 × 100 = 100. А на перепаді центр отримує підсилення, і перепад стає крутішим. Це та сама формула, згорнута в табличку, тільки з жорстко зашитим найпростішим розмиттям — тоді як unsharp mask дозволяє взяти гаус із будь-якою σ.
Числа з нашого фото: стандартне відхилення яскравості (груба міра контрасту) становило 43.62 в оригіналі, 47.81 після ядра різкості й 45.36 після unsharp mask із σ = 1.5 і силою 1. Обидва способи підняли контраст, ядро — агресивніше.
ГрадієнтГрадієнт і Собель
Дійшли до найважливішого застосування згортки в класичному зорі — пошуку меж. Спершу домовмось, що таке межа числом.
Межа — це місце, де яскравість швидко змінюється, а «швидко» міряють різницею
сусідів: праворуч 90, ліворуч 10 — різниця 80, це багато; 101 проти 100 — це рівна
ділянка. Найпростіше ядро для цього — [−1, 0, 1]: віднімає лівого сусіда
від правого.
Але таке ядро занадто довірливе: дивиться на один рядок, і випадковий шумовий піксель дасть фальшивий сплеск. Ядро Собеля лікує це однією зміною: дивиться на три рядки, а середньому дає подвійну вагу.
Ліве ядро рахує, наскільки правий бік світліший за лівий, і реагує на вертикальні межі. Праве робить те саме по вертикалі й реагує на горизонтальні. Подвійна вага середнього рядка — вбудоване згладжування впоперек напрямку: шумовий піксель у крайньому рядку вплине вдвічі слабше.
Вікно, у якому ліві дві колонки мають яскравість 10, а права — 90. Множимо на ядро Собеля по x; середня колонка ядра — нулі, тож лишається права колонка мінус ліва:
Коротше: сума ваг з одного боку дорівнює чотирьом, тому результат — це просто 4 × (90 − 10). Висота перепаду, помножена на чотири.
Два ядра дають два числа на піксель, gx і gy, а разом
вони утворюють вектор — градієнт, у якого є довжина й напрямок.
Модуль — наскільки різкий перепад, тобто «сила межі».
Напрямок — у який бік яскравість росте найшвидше; він завжди
перпендикулярний до самої межі. arctan — функція, яка з відношення
двох катетів дає кут; руками її не рахують.
На нашому фото модуль градієнта доходить до 586.8, а медіана по кадру — 10.8. На рівній ділянці стільниці, де є лише шум матриці, модуль близько 17.5; на межі корпусу — 420.2. Різниця більш ніж у двадцять разів, і саме на ній тримаються всі порогові детектори меж.
cv2.Sobel із ksize=3 — це буквально filter2D
з наведеним вище ядром: різниця на всьому зображенні дорівнює рівно нулю, без жодного
допуску. Усередині немає нічого, крім девʼяти множень.Собель: чотири погляди на те саме фото
Перемикай канал і дивись на два збільшені фрагменти: вертикальну межу корпусу й горизонтальну. Одне ядро бачить лише одну з них.
gx друге число падає майже до нуля, на gy — перше.
Модуль бачить обидві, бо складає внески двох ядер.КенніКенні: чотири кроки, а не один виклик
Модуль градієнта — уже непоганий детектор меж, але в нього дві біди. Межа виходить завширшки в кілька пікселів, бо градієнт великий не тільки на переході, а й поруч. І доводиться обирати поріг, а будь-який вибір поганий: високий рве межу на пунктир, низький тягне шум.
Детектор Кенні (Canny edge detector) лікує обидві біди. Його майже
завжди подають одним викликом cv2.Canny(img, 100, 200), і тому він виглядає
чорною скринькою. Насправді всередині чотири операції, і кожну ми вже вміємо.
Похідна підсилює шум: на рівній ділянці сусіди й так стрибають, а Собель цей стрибок помножить на чотири. Тому спершу гаусове розмиття — шум на нашому фото впав із 2.32 до 1.08, тобто в 2.1 раза.
Тут деталь, про яку мовчать підручники: cv2.Canny цього
кроку не робить. Вона починає одразу з градієнта, а згладжувати має ти сам.
Без цього детектор знайде 1020 пікселів межі замість 743 — зайві триста це шум,
старанно підсилений Собелем.
Той самий Собель: модуль і напрямок у кожному пікселі. Після згладжування максимальний модуль — 299.9, а пікселів із модулем понад 100 набирається 2772. Це занадто товсті межі: контур телефона має бути завширшки в один піксель.
Головна ідея Кенні. Межа — це гребінь: уздовж напрямку градієнта модуль росте, досягає максимуму точно на переході й падає. Лишаємо піксель, лише якщо він не менший за двох сусідів уздовж градієнта, тобто впоперек самої межі. Напрямок округлюємо до одного з чотирьох: горизонталь, вертикаль, дві діагоналі. З 2772 пікселів лишається 1034.
Замість одного порога беремо два. Піксель вище верхнього — межа беззаперечно. Піксель між нижнім і верхнім — межа лише якщо він сполучений ланцюжком із беззаперечним. Решта викидається. На порогах 100 і 200 наше фото дало 353 беззаперечні пікселі й 696 кандидатів, з яких сполученістю врятувалося 436. Разом 789 пікселів межі.
Слово гістерезис прийшло з фізики й означає «поведінка залежить від попереднього стану». Тут буквально так: доля слабкого пікселя залежить не від нього самого, а від того, чи дотягнувся до нього ланцюжок від сильного. Саме це й не дає межі розсипатись на пунктир.
arctan2(gy, gx), вказує вправо-вниз,
і сусіди для нього — (r−1, c−1) та (r+1, c+1), а не навпаки.
Переплутавши дві діагоналі місцями, дістанеш код, який виглядає правильним і не падає.Що саме ламається. Притлумлення немаксимумів порівнює піксель із двома сусідами упоперек межі — там, де модуль швидко спадає, тож справжній гребінь виграє порівняння. Переставивши діагоналі, ти порівнюєш його з сусідами уздовж межі, а вздовж гребеня модуль майже сталий: піксель то виграє, то програє через шум у другому знаку. Тому діагональна межа не звужується, а розсипається — частина її пікселів гине, а натомість спалахують сусідні, зсунуті вбік.
Замір на сцені, де всі межі діагональні, — сірий квадрат, повернений на 45°,
плюс слабкий шум. Правильна реалізація відтворює 98.1 % пікселів
cv2.Canny і додає лише 4 зайві; реалізація з переставленими діагоналями
відтворює 37.7 % і додає 535 зайвих. Перетин до обʼєднання падає з
97.5 % до 20.5 %. Помилку в знаку чи в порядку координат тут не видно
очима — її видно тільки числом.
Про збіг. Власна реалізація всіх чотирьох кроків дає 789 пікселів межі проти 743
у cv2.Canny, причому всі 743 бібліотечні входять у наші. Частка спільних
до обʼєднання — 94.2 %. Точного збігу тут не буває: OpenCV округлює
напрямок своїм способом і по-своєму розвʼязує нічиї. Ці шість відсотків — не помилка,
а чесна ціна за те, що реалізацій одного алгоритму буває багато.
Кенні по кроках
Чотири стадії перемикачем і два пороги повзунками. Праворуч — розподіл модуля градієнта після притлумлення; вертикальні лінії показують, де саме стоять пороги.
На цьому легко обпектися, готуючи демонстрацію. Ось поміряна таблиця: чистий чорно-білий квадрат проти нашого фото з шумом.
| пороги | чистий квадрат | фото з шумом |
|---|---|---|
| 20 / 60 | 276 | 3342 |
| 50 / 150 | 276 | 1212 |
| 100 / 200 | 276 | 1111 |
| 150 / 250 | 276 | 1012 |
На квадраті число не змінюється жодного разу: усі його межі однаково нескінченно різкі, і жоден поріг нижче 1082 їх не зачіпає. На фото кількість падає більш ніж утричі. Мораль: якщо пороги «нічого не роблять», проблема не в порогах, а в картинці.
МежаЧому одного ядра замало
Ми навчилися рахувати одну відповідь на кожен піксель. Тепер чесно назвімо, чого ця відповідь не вміє — бо саме звідси росте решта курсу.
Перша сліпота — напрямок. Ядро gx шукає вертикальні межі
й на горизонтальних дає майже нуль:
| межа | середнє |gx| | середнє |gy| |
|---|---|---|
| вертикальна (бічна грань) | 179.3 | 6.5 |
| горизонтальна (верхня грань) | 5.9 | 200.4 |
Одне ядро бачить «свою» межу в 30 разів сильніше за чужу. Лікується це просто — двома ядрами й модулем. Але для меж під кутом 30° уже доводиться або покладатися на комбінацію двох, або заводити ядра для проміжних напрямків.
Друга сліпота — масштаб, і про неї згадують рідше. Ядро 3 × 3 дивиться на три пікселі й міряє перепад саме на такій відстані. Ось відгук того самого Собеля на перепади однакової висоти (з 10 до 90), розмазані на різну ширину:
| перепад розмазано на | максимум |gx| |
|---|---|
| 1 піксель | 320.0 |
| 5 пікселів | 128.0 |
| 10 пікселів | 64.0 |
| 40 пікселів | 16.0 |
Висота перепаду однакова в усіх чотирьох рядках, а відгук різниться в двадцять разів — залежно лише від того, на скількох пікселях перепад відбувається. Одне ядро міряє один масштаб, і поріг, підібраний під різкі межі, пропустить мʼякі.
Третя сліпота — зміст. Ядро не знає, що таке телефон. Воно однаково голосно кричить на межі корпусу, на краю тіні, на подряпині й на випадковому відблиску. Відрізнити важливу межу від неважливої одним ядром неможливо в принципі.
Звідси висновок, до якого йшла вся тема: ознак треба багато й різних — різного напрямку, різного масштабу, — а потім із них треба щось зібрати. Історично відповідей дві. Класична: людина вигадує набір ядер і правило, як звести їхні відгуки в опис зображення. Сучасна: коефіцієнти не вигадують, а навчають на даних.
Друга відповідь — це вже згорткові мережі, і вони належать блоку 2. Тут важливо інше:
механіка в обох випадках та сама. Мережа не робить із зображенням нічого,
чого не робить filter2D; вона лише перебирає коефіцієнти замість тебе,
спираючись на
градієнтний спуск
й зворотне поширення,
які ти вже знаєш.
Дві властивості, заради яких блок 2 узяв саме згортку
Механіка та сама — але саме з неї випливають дві властивості, які й зробили згортку основою моделей зору. Обидві видно на числах, обидві мають наслідок.
Перша: згортка лінійна. Це означає рівно дві речі. Якщо помножити зображення на число, відгук помножиться на те саме число. Якщо скласти два зображення, відгук дорівнює сумі двох відгуків. Порахуймо руками одновимірним ядром [−1, 0, 1], яке віднімає лівого сусіда від правого:
Наслідок, і він неприємний. Два згорткові проходи поспіль складаються в один: пройди зображення розмиттям 3 × 3, а потім різкістю 3 × 3 — і той самий результат дасть одне ядро 5 × 5, складене з цих двох (перевірено: різниця рівно нуль). Отже, шар за шаром чистих згорток не додає моделі жодної нової здатності — усе одно вийде одна велика згортка. Саме тому в блоці 2 після кожного згорткового шару стоїть нелінійність: без неї глибина мережі не має сенсу.
Друга: згортка зсувоінваріантна (точніше — зсувоеквіваріантна: зсунеш вхід — рівно так само зсунеться вихід). Причина проста: коефіцієнти ядра не залежать від того, у якій точці кадру стоїть вікно. Зсунь фото на пʼять пікселів униз — відгук зсунеться на ті самі пʼять пікселів, а значення не зміняться жодне.
Наслідок, і він приємний. Девʼять чисел ядра обслуговують усе зображення, скільки б у ньому не було пікселів. Детектор вертикальної межі, знайдений у кутку, працює й посередині — його не треба вчити вдруге для кожного місця. Саме звідси в блоці 2 візьмуться спільні ваги (weight sharing): згортковий шар має на порядки менше параметрів за повнозвʼязний шар тієї самої ширини, і предмет, який на новому фото зсунувся вбік, лишається впізнаваним. Саме з цієї думки починається тема 06 · Чому повнозвʼязна мережа не годиться.
ДаліКуди далі
Підсумуймо. Згортка — маленька табличка чисел, яку прикладають до кожного пікселя
разом із сусідами: помножили поелементно, склали, записали в новий масив. Сума
коефіцієнтів вирішує, зберігати яскравість чи шукати різницю; режим межі — що робити
з краєм. Гаус розмиває однаково в усі боки й економить у 4.5 раза завдяки роздільності.
Різкість — це оригінал плюс те, що зникає при розмитті. Собель дає модуль і напрямок
градієнта, а Кенні складається з чотирьох кроків, кожен з яких ти тепер можеш написати
сам. І найважливіше: cv2.filter2D рахує кореляцію, а не згортку — для
симетричного ядра це нічого не змінює, а для твого власного несиметричного змінює все.
Ми навчилися рахувати одну відповідь на кожен піксель: наскільки тут різко, у який бік, наскільки розмито. Але щоб порівняти два зображення чи знайти той самий телефон на іншому фото, потрібна відповідь не на піксель, а на все зображення. Саме цим займається тема 05 · Класичні ознаки: вона бере відгуки фільтрів і збирає з них компактний опис — гістограму напрямків градієнта, набір характерних точок, дескриптор околиці. І показує, де такий підхід ламається, а це найкоротший шлях до розуміння, навіщо потрібен блок 2.