Класичні ознаки

Тридцять років люди вигадували, яке саме число описує зображення. Виходило добре рівно доти, доки було точно відомо, що саме шукаємо.

У попередній темі ми зробили згортку руками й дійшли до Собеля й Кенні: з картинки виходить інша картинка, де світяться межі. Це вже не сира яскравість, але це досі зображення — тисячі чисел. А для більшості задач потрібно щось інше: коротке число, за яким два знімки можна порівняти. Це і зветься ознакою (feature), і вся ця тема — про те, як її вигадували руками.

Слово «вигадували» тут не образа. Придумані ознаки зробили те, що працює й досі: панорами в телефоні, стабілізація відео, навігація робота по камері. Але той самий підхід програв у розпізнаванні, і програв нищівно. Ця тема — про обидві половини історії, бо без другої не зрозуміти, навіщо знадобився наступний блок. Сцена та сама, що в темах 01 і 02: «фото товару з дошки оголошень», телефон на столі, на екрані сітка іконок, і ми його малюємо формулами.

ЗадачаЧому не можна порівняти пікселі

Здається, що порівняти два зображення просто: відніми одне від одного й подивись, чи близький результат до нуля. Спробуй — і воно розсиплеться на першому ж прикладі. Візьми знімок і зсунь його на один піксель праворуч. Око не побачить різниці взагалі. А порівняння піксель у піксель побачить: на кожній межі, де яскравість стрибала зі 60 на 230, тепер стоїть чуже значення, і різниця там 170 рівнів. Формально знімки «різні» — настільки ж різні, наскільки два зовсім різні фото.

Що таке піксель у піксель. Береш перше число першого зображення й перше число другого, віднімаєш, підносиш до квадрата; те саме для другої пари, третьої й так усі 20 480 разів — стільки пікселів у нашому знімку 128 × 160; додаєш і ділиш на кількість. Вийде одне число — середня квадратична різниця. Нуль означає «побайтово однакові».

Далі ще гірше. Ввімкнули лампу яскравіше — усі числа поїхали вгору, різниця величезна, предмет той самий. Повернули телефон на пʼять градусів — так само. Відійшли на крок далі — знову. Пряме порівняння реагує на все, крім того єдиного, що нас цікавить: чи це той самий предмет.

Звідси задача теми. Потрібне перетворення, яке з масиву на 20 тисяч чисел робить короткий вектор — скажімо, на 32 або на 128 чисел — так, щоб у нього не потрапила дрібниця, а потрапила суть. Цей короткий вектор і зветься ознакою, або дескриптором (descriptor), а перетворення — виділенням ознак (feature extraction).

МірилоТри вимоги до ознаки

Перш ніж вигадувати ознаки, домовимось, чим їх міряти. Вимог рівно три, і далі кожна ознака в темі складатиме іспит саме за цими трьома.

ВимогаЩо означаєЯк перевіряємо
СтійкістьСпотворили знімок — вектор майже не змінився. Спотворення: зсув, поворот, масштаб, освітлення, шум. Порахувати вектор до і після й поміряти, наскільки вони близькі
РозрізняльністьДва різні предмети дають різні вектори. Ознака, стійка до всього на світі, стійка й до зміни предмета — і тому нікчемна. Порахувати вектор для іншого предмета й переконатись, що він далеко
КомпактністьВектор коротший за саме зображення: місце, час порівняння й памʼять залежать від його довжини. Просто подивитись на довжину

Головне про ці три вимоги: вони тягнуть у різні боки. Найстійкіша ознака у світі — константа: вона однакова для будь-якого зображення, а отже, нічого не розрізняє. Найрозрізняльніша — саме зображення: унікальне, але не стійке ні до чого й не коротке. Уся історія придуманих ознак — це пошук точки між цими крайнощами.

Як міряти «близькість» двох векторів, домовимось теж один раз: перемнож їх поелементно, додай добутки й поділи на добуток довжин самих векторів. Виходить число від −1 до 1: 1 — вектори дивляться в один бік (збіг), 0 — нічого спільного. Це косинусна близькість (cosine similarity), і саме її показують табло в інтерактивах нижче. Мінус одиниця означала б «дивляться в протилежні боки», але в цій темі вона не трапиться: усі наші вектори — це підрахунки, тобто невідʼємні числа, і нижче нуля косинус між ними не спускається.

ГістограмаГістограма яскравості

Найпростіша ознака, яку взагалі можна вигадати: полічити, скільки в зображенні пікселів кожної яскравості. Отримаємо 256 чисел — по одному на кожен рівень від 0 до 255. Це гістограма, і ми вже бачили її в темі 01.

гістограма[v] = скільки пікселів мають яскравість рівно v

Три вимоги вона складає нерівномірно. Компактність чудова: 20 480 чисел стиснулись у 256, а якщо групувати по 8 рівнів у корзину (bin), то й у 32. Стійкість теж чудова — і саме тут пастка. Гістограма стійка до зсуву, повороту, дзеркального відбиття й навіть до повного перемішування пікселів, бо взагалі не знає, де саме кожен піксель лежав: вона рахує тільки, скільки їх якого відтінку.

Що саме викидає гістограма

Угорі — «фото» після перетворення. Унизу — його гістограма, накладена на гістограму оригіналу. Перемкни перетворення й подивись, чи стовпчики зрушили хоч на волосину.

перетворення
корзин збіглося—
з них усього256
косинус із оригіналом—
Переворот, зсув і навіть перемішування блоків дають побітовий збіг: 256 корзин із 256. Перемішування зруйнувало зображення до невпізнанності — гістограма й на це не зреагувала. І тільки притемнення, яке око майже пробачає, зсунуло всі стовпчики ліворуч і зламало ознаку: збіглося 27 корзин із 256, косинус впав до 0.374.

Це не спостереження, а наслідок означення, і доводиться він в один рядок. Гістограма відповідає рівно на одне питання: «скільки пікселів мають значення v». А переворот, дзеркальне відбиття, циклічний зсув і навіть повне перемішування — це перестановки: пікселі міняються місцями, але жоден не зникає, не додається й не міняє власного значення. Отже, скільки пікселів мали значення v до перестановки, стільки й після — для кожного з 256 значень одразу. Гістограма зобовʼязана збігтися, і не «майже», а побітово: це арифметика, а не щасливий збіг. Перевіряється воно одним np.array_equal(cv2.calcHist(a,…), cv2.calcHist(b,…)), і відповідь буде True для кожної з чотирьох операцій.

А от притемнення — не перестановка: воно міняє самі значення, а не місця пікселів. Тому саме воно — і тільки воно — гістограму й ламає. Ознака, яку не чіпає перемішування картинки в кашу, але валить поворот регулятора яскравості: неважко уявити гіршу пару властивостей для впізнавання предметів.

Отже, розрізняльність слабка, а ламається гістограма там, де людина ламатись не чекає — від зміни освітлення. Тому в реальних системах беруть гістограму не яскравості, а тону: переводять знімок у HSV і будують гістограму по каналах H і S, викидаючи канал V, який і є яскравістю. Порівнює їх cv2.compareHist, і на нашій сцені після множення яскравості на 0.6 виходить близькість 0.999 — світло таку гістограму майже не турбує. Але перевернутий згори вниз кадр дає 1.000: простору вона все одно не бачить, лише перестала боятись лампи.

І тут же чесна межа, бо вона є. Там, де треба дешево відкинути завідомо не те, гістограма й сьогодні найкращий вибір — і жодна мережа її звідти не витіснила. Причина в ціні: 32 числа проти 3780 у HOG і 512 байтів у SIFT, а порівняння двох гістограм — це 32 множення, тобто мільйони порівнянь за секунду на звичайному процесорі, без відеокарти й без моделі. Тому гістограму ставлять перед дорогою частиною: відсіяти кадри відео, у яких нічого не змінилось; знайти дублікати у великій теці знімків; звузити мільйон кандидатів до тисячі, яку вже можна віддати чомусь розумнішому. Правило просте: гістограма не годиться, щоб відповісти «що це», і чудово годиться, щоб відповісти «це точно не воно».

ПростірКуди дівається простір

Слабкість гістограми названа точно: вона викинула координати. Напрошується перше компромісне рішення, і воно справді так і робиться. Розріж зображення сіткою на клітинки і візьми гістограму окремо в кожній клітинці, а потім склей їх в один довгий вектор: тепер він знає не тільки «скільки темного», а й «темне було в лівому нижньому куті». Це просторова піраміда (spatial pyramid).

Ціна очевидна з арифметики. Візьмімо для рахунку грубішу гістограму — не 256 корзин, а 16, по 16 рівнів яскравості в кожній: саме її і рахує інтерактив нижче, бо з 256 корзинами на клітинку вектор роздувся б до нечитабельного. Було 16 корзин на весь знімок; стало 16 корзин на кожну з n × n клітинок, тобто при сітці 8 × 8 — 1024 числа, у 64 рази довше. І зʼявилась нова слабкість: якщо предмет зсунеться на пів клітинки, гістограми поїдуть у сусідні клітинки, і вектор зміниться.

Просторова піраміда

Повзунок ділить кадр сіткою. Ліворуч — клітинки на «фото», праворуч — довжина вектора й дві близькості: до зсунутого кадру (стійкість) і до перевернутого (розрізняльність).

довжина вектора16
клітинок1
зсув на 6 пікселів—
перевернутий кадр—
Прожени повзунок від 1 до 8. Довжина вектора росте з 16 до 1024; близькість до перевернутого кадру падає з 1.000 до приблизно 0.5 — ознака нарешті бачить різницю. Але й близькість до зсунутого кадру сповзає з 1.000 приблизно до 0.87: за розрізняльність заплачено стійкістю.

Запамʼятай форму цього компромісу: щоб ознака почала розрізняти, у неї довелося повернути частину просторової інформації, і рівно настільки вона стала крихкішою. Далі ми повторимо цей хід ще двічі — у HOG і в SIFT.

ГрадієнтиГрадієнти замість яскравості

Гістограму ламає освітлення. Виправлення підказує попередня тема: рахувати не яскравість, а градієнт — на скільки яскравість змінюється від пікселя до сусіда. Собель із теми 04 дає gx (зміна вправо) і gy (зміна вниз), а з них — модуль і напрямок.

|g| = √( gx2 + gy2 )   ·   напрямок = atan2( gy, gx )

Словами: модуль — наскільки різко змінюється яскравість у цій точці, тобто «наскільки тут межа». Напрямок — куди саме вона змінюється найшвидше, тобто під яким кутом ця межа лежить. Символ atan2 — це функція, яка з двох катетів прямокутного трикутника повертає кут; вона є в кожній мові й у NumPy зветься np.arctan2.

Розберемо запис по знаках. gx — наскільки яскравість зростає, якщо ступити на піксель управо; gy — те саме вниз. Обидва можуть бути відʼємні: це означає «спадає». Дві риски |g| — це довжина, а корінь із суми квадратів тут не з примхи: gx і gy — два катети прямокутного трикутника, а модуль градієнта — його гіпотенуза, тобто це теорема Піфагора й нічого понад неї. Верхня двійка означає «помножити число саме на себе».

Порахуймо руками, щоб побачити механізм. Хай у якійсь точці gx = 3, а gy = 4. Тоді |g| = √(3² + 4²) = √(9 + 16) = √25 = 5, а напрямок = atan2(4, 3) = 53.1°. Тепер пригасімо лампу вдвічі — обидва числа діляться навпіл: gx = 1.5, gy = 2. Модуль стає √(2.25 + 4) = √6.25 = 2.5, тобто теж удвічі менший. А напрямок — atan2(2, 1.5) = ті самі 53.1°. Ось уся ідея на пʼяти числах: модуль поїхав, напрямок не зрушив.

Тепер найважливіше — чому це рятує від освітлення. Світло на знімку змінюється двома типовими способами.

Крок 1 — лампа світить рівномірніше

До кожного пікселя додалося одне й те саме число: було 100 і 160, стало 140 і 200. Різниця сусідів була 60 і лишилась 60 — доданок скоротився при відніманні. Градієнт не змінився взагалі, ні модуль, ні напрямок.

(b + 40) − (a + 40) = b − a
Крок 2 — лампу пригасили вдвічі

Кожен піксель помножився на 0.5: було 100 і 160, стало 50 і 80. Різниця була 60, стала 30 — модуль градієнта помножився на ті самі 0.5. Але gx і gy помножились на 0.5 обидва, а кут залежить лише від їхнього відношення. Відношення не змінилось — напрямок лишився той самий до останнього знака.

atan2( 0.5·gy, 0.5·gx ) = atan2( gy, gx )
Крок 3 — де це ламається

Один-єдиний виняток, і він практичний: якщо множник більший за одиницю, світлі пікселі впираються в стелю 255 і далі не ростуть. Межа, яка була різкою, стає плоскою, і напрямок у цих місцях таки пливе. Заміряно на сцені цієї теми (128 × 160 пікселів): поки не вигоряє жоден піксель, відхилення напрямку — 0.000°, тобто нуль до останнього знака. При множнику 1.8 у стелю впираються 10 999 пікселів із 20 480 — понад половина кадру, — і напрямок відхиляється в середньому вже на 3.38°. Ті самі числа покаже табло інтерактиву нижче, якщо довести повзунок до 1.80.

Яскравість поїхала, напрямок ні

Повзунок множить усі пікселі на коефіцієнт. Стрілки показують напрямок градієнта в девʼяти точках; праворуч — три числа до й після.

середня яскравість—
середній модуль—
відхилення напрямку—
пікселів у стелі—
Веди повзунок ліворуч від одиниці. Яскравість і модуль градієнта падають разом, а відхилення напрямку тримається на 0.00° — рівно нуль, а не «майже нуль». Праворуч від одиниці пікселі починають упиратись у 255, і саме звідти напрямок починає пливти.

Ось звідки правило, на якому стоїть уся класична робота із зображеннями: напрямок градієнта — найдешевша ознака, стійка до освітлення. Модуль теж корисний, але його треба нормувати; напрямок — ні. Лишається зібрати з напрямків щось компактне.

HOGHOG по кроках

Найвідоміша придумана ознака в історії зору зветься HOG — Histogram of Oriented Gradients, «гістограма орієнтованих градієнтів». Вона зроблена рівно з того, що ми щойно отримали, і будується в чотири кроки.

Крок 1 — комірки

Розрізаємо вікно сіткою на квадратні комірки (cell) 8 × 8 пікселів. Це те саме компромісне рішення, що в просторовій піраміді: усередині комірки координати втрачаються, між комірками — зберігаються.

Крок 2 — гістограма напрямків

У кожній комірці заводимо 9 корзин: 0–20°, 20–40°, …, 160–180°. Чому 180, а не 360? Бо межа, темна зліва й світла справа, і та сама межа навпаки — це одна й та сама межа; напрямок беремо без знака. Кожен із 64 пікселів комірки кидає свій голос у ту корзину, куди потрапив його напрямок.

Крок 3 — зважування модулем

Голос не одиничний: піксель голосує своїм модулем градієнта. Так різка межа важить багато, а шумовий тремт на рівному фоні — майже нічого. Це рятує ознаку від шуму без жодного порога.

Крок 4 — нормування по блоках

Беремо квадрат 2 × 2 сусідні комірки — це блок (block), разом 4 × 9 = 36 чисел. Ділимо всі 36 на довжину цього вектора з 36 чисел, тобто на корінь із суми їхніх квадратів. Тепер у блоці важать не абсолютні величини, а співвідношення напрямків. Блоки беруть із перекриттям: кожна комірка потрапляє в кілька блоків і щоразу нормується по-різному.

vнорм = v ÷ √( v12 + v22 + … + v362 )

Тепер порахуймо, якої довжини вийде вектор — це чотири ділення, і кожне варто зробити самому, бо саме тут видно, звідки береться славнозвісне число 3780. Класичне вікно детектора пішоходів — 64 × 128 пікселів.

  1. Комірок. Ділимо на квадрати 8 × 8: по ширині 64 ÷ 8 = 8 комірок, по висоті 128 ÷ 8 = 16. Разом 8 × 16 = 128 комірок.
  2. Блоків. Блок — квадрат 2 × 2 сусідні комірки, і блоки зсуваються щоразу на одну комірку, тобто перекриваються. Лівий верхній кут блоку може стояти в будь-якій комірці, крім останньої в рядку й останньої в стовпці — інакше блок вилізе за край. Отже (8 − 1) × (16 − 1) = 7 × 15 = 105 блоків.
  3. Чисел у блоці. 4 комірки × 9 корзин напрямку = 36.
  4. Разом. 105 × 36 = 3780 чисел.

Порівняй із входом: у вікні 64 × 128 = 8192 пікселі, а на виході 3780 чисел. Стиснення всього вдвічі — HOG не про компактність. Він про те, що ці 3780 чисел не залежать від освітлення й описують форму, а вісім тисяч сирих пікселів не описують нічого, крім самих себе.

HOG по кроках

Тисни «наступний крок» і дивись, як із сирої яскравості виростає вектор ознаки. На третьому кроці кожна комірка малюється «зірочкою»: довжина променя — сила напрямку.

крок
чисел на цьому кроці—
проти сирого вікна—
Чотири кроки — і 10 240 чисел вікна перетворились на вектор, у якому лишились тільки напрямки меж. Вікно тут 80 × 128, тому чисел на виході 135 × 36 = 4860. Для класичного вікна пішоходів 64 × 128 той самий рахунок дає 7 × 15 = 105 блоків по 36 — разом 3780: це число не заміряне, а виведене з розмірів вікна, і будь-яка реалізація зобовʼязана видати саме його.

Тепер найцікавіше в темі, і це не теорія, а факт із командного рядка. Класична демонстрація HOG — детектор пішоходів, який лежить у кожному підручнику двома рядками: створити cv2.HOGDescriptor, підвантажити готові ваги. В OpenCV 5 класу HOGDescriptor більше немає — не «немає файлу з вагами», а немає самого класу. Разом із ним викинуто CascadeClassifier, тобто й виявлення облич каскадами Хаара, другу безсмертну демонстрацію підручників.

Перевірено імпортом, не з памʼяті. У cv2 версії 5.0 hasattr(cv2, "HOGDescriptor") дає False. Так само зникли AKAZE_create і BRISK_create. Лишилися SIFT_create і ORB_create — на них і побудована решта теми.

Бібліотека не викидає класи з примхи — вона викидає те, чим перестали користуватися. І це найкоротша відповідь на питання теми: придуманий детектор пішоходів зник із головної бібліотеки зору, бо його замінили, а SIFT лишився, бо його не замінили. Пояснювати HOG від цього не менш потрібно: це головна придумана ознака в історії, і без неї не зрозуміти, що саме навчилися робити мережі.

КутиКутові точки

HOG описує ціле вікно одним вектором. Але часто потрібне інше: не «що на знімку», а «де саме на другому знімку та сама точка, що й на першому». Панорама, стабілізація, навігація робота — це задачі про точки, а не про картинку цілком. Які точки годяться? Уяви, що дивишся на знімок крізь маленьке віконце й можеш посунути його на кілька пікселів у будь-який бік. Три випадки:

Це і є ідея Гарріса (Harris corner detector): цікава та точка, у якої навіть найменша зміна по всіх напрямках велика. Не найбільша — саме найменша. У межі найбільша зміна теж велика, тому дивитись треба на найгірший напрямок.

Кут проти межі проти плями

Обери місце й покрути напрямок зсуву віконця. Пелюстка праворуч — наскільки змінюється вміст віконця в кожному напрямку; вирішує найкоротший промінь.

де стоїть віконце
зміна в цей бік—
найгірший напрямок—
Дивись на праву цифру, а не на ліву. У плями найгірший напрямок дає 12.0, у межі — 17.5 (уздовж лінії нічого не змінюється, хоча впоперек зміна доходить до 9629), у кута — 5054. Між плямою й кутом різниця в чотириста разів, і саме її Гарріс перетворює на одне число відгуку.

Перебирати всі напрямки не треба. Гарріс показав, що зміну вмісту вікна при малому зсуві можна виразити через три суми добутків градієнтів у вікні — gx², gy² і gxgy. Позначмо їх коротко: A — сума квадратів горизонтальних градієнтів у вікні, C — вертикальних, B — сума їхніх добутків. Словами: A каже, наскільки вміст вікна змінюється при зсуві вбік, C — при зсуві вгору-вниз, а B — наскільки ці дві зміни повʼязані між собою. Відгук рахують так:

R = (A·C − B2) − k · (A + C)2

Що тут за що відповідає. Добуток A·C великий лише тоді, коли вікно змінюється в обох напрямках одразу — у межі один із двох множників малий, і добуток гине. Відніманням B² прибирають випадок, коли обидві зміни насправді одна й та сама, тільки під кутом. Другий доданок — k · (A + C)² — це штраф за «просто багато градієнта»: k тут маленька стала, зазвичай від 0.04 до 0.06, і її єдина робота — заганяти відгук межі в мінус, щоб межу не сплутати з кутом.

Порахуймо всі три випадки на круглих числах. Пляма: градієнтів немає, A = C = B = 0, значить R = 0. Межа, вертикальна: яскравість міняється тільки вбік, тож A = 1000, C = 0, B = 0, і R = (1000·0 − 0) − 0.04·1000² = −40 000, глибокий мінус. Кут: міняється в обидва боки, A = C = 1000, B = 0, і R = (1000·1000 − 0) − 0.04·2000² = 1 000 000 − 160 000 = +840 000. Три випадки — три різні знаки й порядки, і тепер кут відбирається одним порівнянням «R більше за поріг». В OpenCV це cv2.cornerHarris, а на практиці частіше беруть обгортку cv2.goodFeaturesToTrack: вона рахує відгук, відкидає слабкі точки й розганяє решту, щоб вони не тіснились. На нашій сцені вона знаходить 158 точок.

SIFTSIFT: масштаб і поворот

Кут ми знайшли. Але щоб зіставити його з кутом на іншому знімку, потрібен ще дескриптор — короткий вектор, який описує околицю точки. І тут проблема, якої не було в HOG: на другому знімку той самий кут може бути повернутий і вдвічі більший. SIFT (Scale-Invariant Feature Transform) розвʼязує це трьома ідеями, і кожна з них потім повторилась у мережах.

Ідея 1 — масштабний простір

Розмиваємо знімок гаусовим ядром дедалі сильніше й складаємо стосик картинок від різкої до дуже мутної. Це масштабний простір (scale space): нижні шари бачать дрібні деталі, верхні — тільки великі плями. Точку шукають по всьому стосику одразу й запамʼятовують, на якому шарі вона знайшлась — це і є її розмір. Коли на другому знімку предмет удвічі більший, та сама точка знайдеться на шарі вдвічі мутнішому, і дескриптор порахують з околиці вдвічі більшої. Розмір скоротився.

Ідея 2 — власна орієнтація

Навколо точки будують гістограму напрямків градієнта (усі 360°, тепер зі знаком) і беруть напрямок найвищого стовпчика. Це «північ» цієї точки. Далі всі координати й усі напрямки в дескрипторі відлічують від цієї півночі. Повернули знімок на 30° — північ повернулась на ті самі 30°, і дескриптор не змінився.

Ідея 3 — сітка гістограм

Околицю точки, уже нормовану за розміром і поворотом, ріжуть на 4 × 4 підкомірки й у кожній будують гістограму з 8 напрямків. Разом 128 чисел. Вектор нормують на довжину, обрізають усе більше за 0.2 (щоб один блиск не переважив усе) і нормують ще раз.

4 × 4 підкомірки × 8 напрямків = 128 чисел, тип float32

Придивись до третьої ідеї: це той самий хід, що в просторовій піраміді й у HOG — сітка клітинок, у кожній гістограма напрямків. Різниця в тому, що сітка тепер не прибита до кадру, а привʼязана до точки: до її розміру й до її півночі.

ORBORB: швидко й бінарно

SIFT точний, але дорогий: 128 чисел типу float32 — це 512 байтів на точку, а порівняння двох дескрипторів — 128 множень. Для телефона, який зшиває панораму в реальному часі, це багато. Тому придумали ORB (Oriented FAST and Rotated BRIEF).

Замість гістограм ORB робить 256 порівнянь пар пікселів: узяти два наперед визначені пікселі околиці й записати один біт — лівий яскравіший за правий чи ні. 256 бітів — це 32 байти типу uint8. Відстань між двома такими дескрипторами рахують підрахунком розбіжних бітів — це відстань Гемінга (Hamming distance).

А тепер найважливіше про неї, бо саме тут і криється вся вигода ORB. Порівняти два дескриптори SIFT — це 128 віднімань, 128 множень і 128 додавань дробових чисел. Порівняти два дескриптори ORB — це дві дії над цілими числами, повторені чотири рази. Спершу XOR («виключне або»): побітова операція, яка ставить одиницю там, де біти різні, і нуль там, де однакові — тобто одним рухом розмічає всі розбіжності. Лишається порахувати одиниці, і це вміє окрема інструкція процесора popcount (population count): вона за один такт повертає, скільки одиничних бітів у 64-бітному числі. Дескриптор із 256 бітів — це рівно чотири таких числа, тож уся відстань між двома точками коштує чотири XOR і чотири popcount. Ніяких множень і ніякої дробової арифметики.

Що це дає на масштабі. Зіставити 400 точок першого знімка з 400 точками другого — це 160 000 порівнянь дескрипторів. У SIFT це 160 000 × 128 ≈ 20 мільйонів дробових множень, у ORB — 160 000 × 4 ≈ 640 тисяч цілочислових операцій. Ось чому телефон зшиває панораму на льоту, а не після.

Числа нижче заміряні на синтетичній сцені. Час залежить від машини — важливе тут співвідношення, а не абсолютна величина:

Дескрипторточокбайтів на точкутипчас, мсусього байтів
SIFT69512float3235.935 328
ORB41132uint82.113 152

ORB знайшов у шість разів більше точок, витратив на кожну в шістнадцять разів менше памʼяті й упорався більш ніж на порядок швидше. Платить він точністю кожної окремої пари — але коли точок ушестеро більше, це часто вигідний обмін. Правило вибору просте: телефон або кадр відео — ORB; максимальна надійність зіставлення без тиску часу — SIFT.

Жодна ознака не виграє всюди

Перемкни спотворення й ознаку. Число — косинусна близькість між ознакою оригіналу й ознакою спотвореного кадру; 1.000 означає повний збіг.

спотворення
ознака
близькість—
довжина вектора—
Прожени всі вісімнадцять комбінацій — і побачиш точну дзеркальність. Гістограма переживає поворот (0.998) і гине від яскравості (0.363). HOG навпаки: яскравість 1.000, поворот 0.237. Ознака з нормованим кадром — єдина, що не провалюється ніде: її найгірша клітинка 0.751, тоді як у двох інших є по клітинці нижче 0.4. Платить вона тим, що й на легких спотвореннях тримається на 0.87, а не на одиниці.

ЗіставленняЗіставлення: тут класика виграє

Дійшли до задачі, заради якої SIFT і ORB живі досі: два знімки того самого предмета з різних боків, треба знайти пари «ця точка на першому — та сама, що ця на другому».

Перший підхід очевидний і поганий: для кожної точки першого знімка знайти найближчий дескриптор на другому. Погано тому, що у кожної точки завжди є найближчий сусід, навіть якщо на другому знімку цієї точки немає — вона затулена або вийшла за кадр. Такий пошук повертає повний список пар, і половина з них вигадана.

Рятує тест співвідношення Лоу (Lowe's ratio test), дивовижно простий. Знайди не одного, а двох найближчих сусідів на другому знімку й прийми пару, тільки якщо перший помітно ближчий за другого:

відстань до першого < поріг × відстань до другого

Логіка: якщо точка справді впізнана, її дескриптор близький до одного-єдиного кандидата й далекий від решти. Якщо ж два кандидати однаково близькі, точка невиразна — так буває в повторюваних візерунках і в шумі, — і краще її викинути, ніж загадувати. Класичний поріг Лоу — 0.75.

Тест співвідношення Лоу

Той самий предмет, знятий під іншим кутом і при слабшому світлі. Повзунок — поріг. Зелені лінії — пари, що справді відповідають одна одній; рожеві — хибні.

пар прийнято—
з них правильних—
частка правильних—
Веди повзунок від 0.50 до 1.00. При 0.50 пар мало, зате всі до єдиної правильні. При 1.00 тест вимкнено — пар шістдесят, і більш ніж половина вигадана. Компроміс «повнота проти чистоти» тут видно голими очима.

Після Лоу лишається другий фільтр, геометричний. Якщо на обох знімках один жорсткий предмет, усі правильні пари мусять узгоджуватись однією геометричною відповідністю. Алгоритм RANSAC шукає її навмання: бере чотири випадкові пари, будує по них перетворення, дивиться, скільки решти пар у нього вкладається, і повторює сотні разів, лишаючи найкращий варіант. Пари, що вклались, звуться внутрішніми (inliers), решта викидається.

Заміряно на синтетичній сцені, поворот 27° і масштаб 0.75:

Етаппарправильнихчастка
усі найближчі сусіди695072 %
після тесту Лоу, поріг 0.75514996 %
після RANSAC4949100 %

Сто відсотків. Не «майже» — усі 49 пар, які пройшли RANSAC, виявились правильними, і обидві відкинуті ним пари були хибними. Це той випадок, де придумана ознака не програла й не збирається програвати, і причину варто назвати прямо: у задачі «знайди ту саму точку на двох кадрах» є геометрична відповідь. Ту саму точку визначає будова поверхні, а не статистика класу. Тому SIFT і сьогодні стоїть у зшивачах панорам і у фотограмметрії, а ORB — у SLAM, тобто в системах, де робот чи дрон будує карту приміщення просто по камері й мусить робити це на кожному кадрі. Одна з найвідоміших таких систем так і зветься — ORB-SLAM, і вибір саме ORB у ній не випадковість: там дескриптор рахують тридцять разів на секунду на бортовому процесорі, і кожен зайвий float коштує кадру. Тут придумана ознака не «поки що тримається» — вона найкращий інструмент, і навчена її не витіснила.

МежаДе вона ламається

А тепер друга половина історії. Змінімо задачу з «той самий предмет» на «той самий клас предметів»: другий знімок — теж телефон на столі, але інша модель, світлий чохол, темна тема, інша сітка іконок. Людина скаже «телефон» за чверть секунди. Ознаки міряють інше.

ПорівнюємоSIFT, парHOG, близькість
той самий телефон, поворот 27° і масштаб 0.7551—
той самий телефон, поворот 20°—0.396
інша модель телефона, та сама поза30.475

Одразу застереження, щоб числа не сплутались: цей замір зроблено на окремій сцені — такій, де є дві різні моделі телефона, а не одна, як в інтерактивах вище. Тому абсолютні рівні тут свої (в інтерактиві 6 HOG на повороті 20° дає 0.237, а не 0.396), і важить не рівень, а порядок: яке з двох порівнянь виявилось ближчим. Прочитай ці числа уважно, бо вони і є вирок.

SIFT: 51 пара проти трьох. Той самий апарат, повернутий і зменшений, дає 51 узгоджену пару; інший апарат того ж класу — три, тобто випадковість. SIFT робить свою роботу бездоганно, просто робота в нього інша: він упізнає екземпляр, а не клас. Для панорами це саме те, що треба; для питання «чи є на фото телефон» — не годиться взагалі.

HOG: 0.475 проти 0.396. Ще гірше. Інша модель телефона схожа на нашу більше (0.475), ніж наш власний телефон, повернутий на двадцять градусів (0.396). Тобто жоден поріг не розділить ці два випадки: постав його між ними — і власний повернутий телефон опиниться в чужих. Придумана ознака помірялась не з тим, з чим ми хотіли.

Це не поодинока невдача, а закономірність, яку видно ще в інтерактиві 6: гістограма переживає поворот (0.998) і гине від світла (0.363), HOG навпаки — світло 1.000, поворот 0.237. Кожна придумана ознака стійка рівно до того, що її автор вирішив викинути — і сліпа рівно до того ж. Автор гістограми викинув координати, автор HOG — абсолютну яскравість; обидва вгадали для своєї задачі й не вгадали для чужої.

Назвімо прямо, чого саме придуманій ознаці бракує, бо «мережі кращі» — це не відповідь. Бракує їй переліку того, чим предмети одного класу дозволено відрізнятись. Автор мусить скласти цей перелік наперед, до першого знімка, і зашити в код: «поворот неважливий, яскравість неважлива, координати неважливі». Для задачі «той самий телефон під іншим кутом» такий перелік складається — там усього кілька пунктів, і всі геометричні. Для задачі «будь-який телефон» він не складається взагалі: туди довелось би вписати інші іконки, інший корпус, інше співвідношення сторін, чохол, тріщину на склі, темну тему, палець на екрані — і це список без кінця, який ще й різний для телефонів, облич і дорожніх знаків. Людина такий список не напише. А от прочитати його з тисячі розмічених знімків, де всі ці телефони вже позначені телефонами, — задача, яку ми вміємо ставити. Ось у чому справжня різниця, і вона не про якість, а про те, звідки береться рішення, що саме викидати.

Звідси й практика тих років. Ознака під пішоходів не годиться під дорожні знаки, під знаки — під обличчя, під обличчя — під літери. Щоразу окрема інженерна робота на місяці: придумати, чим саме цей клас відрізняється від усього іншого, запрограмувати вручну, поміряти, переробити. І кожна така ознака лишалась крихкою до того, чого автор не передбачив: незвичного ракурсу, тіні, затулення, іншого фону.

Ще один рядок із тієї самої бібліотеки. В OpenCV 5 замість викинутих класів зʼявились ALIKED і DISK — детектори ознак, які не придумані, а навчені на даних. Запустити їх однією командою, як SIFT_create(), не вийде: ALIKED.create() вимагає обовʼязковим аргументом шлях до файлу з вагами моделі, а самого файлу в пакеті немає — його викачують окремо. І це не незручність, а сама суть різниці: придуманій ознаці файл із числами не потрібен, бо вона вся вміщається в код, який можна прочитати; навченій він потрібен обовʼязково, бо все, що вона знає, здобуте з даних і лежить у тих числах. Прочитай цей факт двічі: у тій самій бібліотеці, яка викинула HOG і каскади Хаара, місце придуманих детекторів зайняли навчені.

ПідсумокКінець блоку 1

Блок 1 закінчено. Тема 01 показала, що зображення — це масив чисел. Тема 02 розклала задачі зору за формою відповіді. Тема 03 дала OpenCV як інструмент. Тема 04 зробила згортку руками й довела її до Собеля й Кенні. Ця тема взяла результат згортки й побудувала з нього ознаку — коротке число, за яким знімки можна порівнювати.

Що лишається з тобою назавжди:

І тепер питання, з якого починається наступний блок. Цілу тему ми вручну вирішували, що саме викинути з зображення й що лишити: викинути координати, лишити відтінки; викинути яскравість, лишити напрямки; викинути абсолютні числа в блоці, лишити їхні співвідношення. Кожне таке рішення — здогад інженера, перевірений потім числом.

Але ж у нас є числа наперед: тисячі розмічених знімків, де відомо, що на них зображено. І в курсі машинного навчання ми вже вміємо робити з даних те, що інакше довелося б угадувати руками. Питання звучить так: а чи не можна замість того, щоб вигадувати перетворення й перевіряти його на даних, просто вивчити саме перетворення з даних?

Можна. Ядро згортки з теми 04 — це набір чисел; замість Собеля туди можна поставити параметри й навчити їх градієнтним спуском, як будь-яку іншу вагу. Мережа сама вирішить, що викинути й що лишити, — і зробить це не для «зображень узагалі», а для тієї задачі, яку ти їй дав. Саме звідси починається блок 2, і починається він із питання, чому для зображень не годиться звичайна повнозвʼязна мережа.