У кінці попередньої теми ми домовились, що далі беремо в руки OpenCV — бібліотеку, якою всі ці зображення читають, перетворюють і показують: змінити розмір, вирізати шматок, перевести в градації сірого, намалювати ту саму рамку поверх фото. Час виконати обіцянку — але не довідником функцій: довідник є на сайті бібліотеки, і він кращий.
Тема буде про одну думку. Кожна операція над зображенням щось із нього викидає. Зменшив — викинув пікселі. Перевів у сірий — викинув колір. Повернув — викинув кути, що вилетіли за кадр. Зберіг у JPEG — викинув дрібні перепади. Приклав поріг — викинув усе, крім «світліше/темніше». Тому питання ніколи не звучить «яку функцію викликати». Воно звучить так: що саме ця функція викине і чи можу я собі це дозволити.
ІнструментНавіщо тут бібліотека
У темі 01 ми не використали жодної бібліотеки: канали, сірий, HSV, яскравість, гістограма виявились арифметикою над масивом NumPy. Тоді навіщо OpenCV? Є рівно три речі, яких чистий NumPy не робить.
Декодування форматів. Файл photo.jpg — не масив, а
стиснений потік байтів, із якого піксель без розпакування не дістати. Писати декодер JPEG
самотужки — проєкт на місяці.
Швидкість геометрії. Зменшити кадр 1920 × 1080 означає порахувати два мільйони вихідних пікселів. Цикл на чистому Python іде секунди, OpenCV робить те саме приблизно за мілісекунду — поміряємо в розділі 12. Різниця не в мові, а в тому, що всередині код на C++, векторизований під інструкції процесора.
Готові алгоритми. Пороги, межі, морфологія, контури, детектори ознак, оптичний потік — десятиліття роботи, які немає причин переписувати.
(x, y), хоча NumPy індексує навпаки; що прапорці — цілі числа; що помилка
часто повертається значенням, а не винятком. Усі три дивини ми зустрінемо в наступних двох
розділах.На межі між OpenCV і NumPy перетворення немає: cv2.resize приймає звичайний
масив і повертає звичайний масив, тож усе з
теми про NumPy тут працює без змін.
ФайлиЧитання й запис
imread повертає масив uint8 форми «висота × ширина × 3» із
порядком каналів BGR — пастка з
теми 01. imwrite обирає формат
за розширенням імені.
Пастка, на якій спотикаються всі
Що буде, якщо файлу немає? Досвід Python підказує виняток. OpenCV поводиться інакше:
Винятку немає — лише рядок зі словом WARN у stderr. Функція повертає
None, виконання йде далі, і програма падає значно пізніше й в іншому місці:
на img.shape з повідомленням AttributeError: 'NoneType' object has no
attribute 'shape'. Людина шукає помилку в обробці, хоча вона була в шляху за
двадцять рядків вище.
imread
перевіряй результат явно: if img is None: raise FileNotFoundError(path). Один
рядок перетворює мовчазну поломку на гучну — той самий підхід, що й у
темі про винятки: помилка має
падати якнайближче до причини.| прапорець читання | що повертає | коли брати |
|---|---|---|
| без прапорця | масив H × W × 3, BGR | звичайний випадок |
| cv2.IMREAD_GRAYSCALE | масив H × W, один канал | колір не потрібен |
| cv2.IMREAD_UNCHANGED | як у файлі, з альфа-каналом | PNG із прозорістю: H × W × 4 |
Перша ціна: скільки коштує запис
| формат | байтів на диску | пікселів змінилось | найбільша похибка |
|---|---|---|---|
| сирий масив у памʼяті | 230 400 | — | — |
| PNG | 128 388 | 0 | 0 |
| JPEG, якість 90 | 12 367 | 76 708 із 76 800 | 84 рівні |
PNG стискає без утрат — масив після читання побайтово той самий. JPEG менший у 18.6 раза, зате 99.9% пікселів мають інші значення: у середньому кожен зсунувся на 3 рівні, а один — на 84 з 255. Для фото в оголошенні це неважливо; для задачі, де модель шукає ледь помітну подряпину, — може бути вирішальним.
ПамʼятьЗріз — це вид, а не копія
Найдешевша операція — узяти прямокутний шматок, і синтаксис знайомий із
теми про NumPy:
patch = img[100:200, 50:150]. Вона не копіює жодного байта:
patch дивиться в ту саму памʼять, просто починає з іншого місця. NumPy зве це
видом (view).
Оригінал зіпсовано без жодного попередження. Це та сама механіка спільного володіння,
що й у списках Python (розділ «Копія,
якої немає»), тільки псується не список на десять чисел, а фотографія на 76 800
пікселів. Ліки — один виклик: img[40:120, 110:210].copy().
.copy() обовʼязковий. Збираєшся писати
у шматок — копіюй. Тільки читати (порахувати середнє, передати у функцію, яка повертає
новий масив) — не копіюй, це зайва памʼять. Найпідступніший випадок — функції малювання з
розділу 10: вони пишуть у масив на місці, і вид дає їм повний доступ до
оригіналу.Вид проти копії
Ліворуч — оригінальне фото, праворуч — вирізаний шматок екрана. Кнопкою малюємо по шматку. Перемикач вирішує, чи був зроблений .copy().
.copy() коштує 24 000 байтів — і псує
рівно нуль. Ціна безпеки відома наперед: висота × ширина × канали.КоординатиОбрізка й дві системи координат
Зрізом шматок уже вирізано. Але тут ховається найгустіше джерело помилок, і воно не про обрізку, а про порядок двох чисел.
NumPy індексує «рядок, стовпець» — img[y, x]: масив
зберігається рядками, тому перша вісь — вісь рядків, і shape у
темі 01 виявився
(висота, ширина, канали). Функції OpenCV беруть точку як
(x, y) — як у шкільній системі координат, тільки з віссю Y вниз.
Обидві угоди розумні; проблема в тому, що вони стоять поруч:
Фото має 240 рядків і 320 стовпців, тож точка «рядок 200, стовпець 300» існує.
Пишемо img[300, 200], переплутавши місцями:
Гучно, одразу, з точним поясненням.
Та сама помилка у функції малювання: cv2.circle(img, (200, 300), 9, ...).
Жодного повідомлення. Порахуймо, скільки пікселів вона змінила:
Точка (x=200, y=300) лежить нижче кадру, а OpenCV обрізає все, що виходить за межі. Для малювання це правильно — інакше лінія, що зачепила край, кидала б виняток. Для помилки в порядку координат це найгірше з можливого: рамка не намалювалась, і ніхто про це не сказав.
height, width = img.shape[:2]. Тоді (width, height) у виклику
resize уже не переплутаєш.Одна точка, дві адреси
Тягни маркер по фото або крути повзунки. Бірюзовий хрест — там, куди точку кладе NumPy. Рожевий — куди її покладе OpenCV, якщо переплутати числа місцями.
У OpenCV є й власна обрізка — cv2.getRectSubPix(img, (ширина, висота),
(x, y)). Вона бере центр шматка, а не кут, і вміє центр дробовий: тоді
результат інтерполюється між пікселями. На цілому центрі вона дає той самий масив, що й
зріз, — і це перевіряється одним np.array_equal: числу тут можна довіритись
саме тому, що його легко перевірити.
МасштабЗміна розміру
Модель чекає картинку фіксованого розміру, а фото приходять різні, тож ресайз — найчастіша операція в компʼютерному зорі.
Розмір передається як (ширина, висота) — знову порядок «ікс, ігрек»,
протилежний до shape, і плутанина тут коштує найдорожче: результат виглядає
майже правильно.
Інтерполяція — не косметика
Коли 400 × 400 стає 50 × 50, коефіцієнт зменшення дорівнює 8, отже, на кожен вихідний піксель припадає квадрат 8 × 8, тобто 64 вхідних. Спосіб, яким із них робиться одне число, зветься інтерполяцією.
Права колонка таблиці — це std, стандартне відхилення: міра того, наскільки сильно значення розкидані навколо свого середнього. Її поміряно на картинці з чистого шуму, де кожен піксель незалежний від сусідів, тож std прямо показує, скільки шуму пережило зменшення: чим менше число, тим більше шуму метод усереднив.
| метод | що робить | читає | std шуму |
|---|---|---|---|
| INTER_NEAREST | бере один піксель — найближчий до центра | 1 із 64 | 73.99 |
| INTER_CUBIC | зважує 16 сусідів гладкою кривою | 16 із 64 | 52.81 |
| INTER_LINEAR | усереднює 4 сусідні пікселі | 4 із 64 | 36.82 |
| INTER_AREA | усереднює всі, що потрапили в клітинку | 64 із 64 | 9.28 |
INTER_LINEAR вироджується в INTER_NEAREST:
точка вибірки лягає рівно в центр вхідного пікселя, ваги трьох сусідів обнуляються, і метод
читає один піксель замість чотирьох. Поміряно на cv2.resize: при коефіцієнті 2
std дорівнює 37.19, при 3 — 73.70, тобто рівно стільки, скільки дає
NEAREST. Інтерактив нижче відтворює це на коефіцієнтах 3, 5, 7 і 9. Слово
«цілому» тут важливе: коефіцієнт 400 ÷ 133 = 3.007 уже не цілий, точка вибірки не лягає
в центр пікселя, і виродження не настає.Різниця між першим
і останнім рядком — у вісім разів. INTER_NEAREST викинув 63 пікселі з 64 і
зберіг увесь шум того одного, що лишився; INTER_AREA усереднив усі 64, а
середнє 64 незалежних чисел коливається приблизно у √64 = 8 разів менше, ніж кожне з
них.
INTER_AREA. Збільшуєш —
INTER_LINEAR (швидко) або INTER_CUBIC (гладкіше).
INTER_NEAREST — тільки для масок із номерами класів, де усереднювати не можна в
принципі: середнє між класом 2 і класом 4 дало б клас 3, якого на цьому пікселі немає.
Замовчування OpenCV — INTER_LINEAR, і воно не найкраще для зменшення. Наслідок
видно на дрібницях: при NEAREST подряпина завширшки в піксель має 1 шанс із 64
вижити, при AREA вона лишить блідий, але наявний слід.Що саме викидає інтерполяція
Угорі — поле чистого шуму, унизу — фото товару. Обидва зменшуються однаково й показані назад у збільшенні, щоб було видно окремі пікселі результату.
NEAREST завжди читає рівно один вхідний
піксель, скільки б їх не було в клітинці. Стовпчик «std» показує наслідок. Права
колонка — теоретичне значення для чесного усереднення, std ÷ коефіцієнт;
AREA лягає на нього майже точно, і це не збіг.Пропорції: третій варіант, про який забувають
Модель хоче квадрат 224 × 224, фото має 320 × 240. Здається, варіантів два. Насправді їх три, і кожен щось віддає.
| спосіб | що втрачаємо | коли брати |
|---|---|---|
| розтягнути | геометрію: коло стає овалом | коли форма неважлива |
| обрізати, тоді зменшити | частину кадру назавжди | коли предмет посередині |
| вписати й доповнити полями | частину площі під порожні поля | замовчування детекції |
Третій спосіб зветься в коді letterbox: зменшуємо за меншим із двох коефіцієнтів, решту доповнюємо однотонними полями. Рамки предметів переносяться при цьому тривіально — множення на один коефіцієнт і зсув. При розтягуванні довелося б множити на два різні, а при обрізанні — ще й викидати рамки, які не влізли.
Три способи вписати фото у квадрат
Повзунки задають цільовий прямокутник. Три панелі показують той самий кадр, приведений до нього трьома способами; підписи рахують, скільки кожен віддав.
ГеометріяПовороти й афінні перетворення
Піксель, повернутий на 30°, майже ніколи не потрапляє рівно в клітинку сітки, тому кожен вихідний піксель доводиться збирати з кількох вхідних — знову інтерполяцією. В OpenCV це два кроки:
Матриця M — таблиця 2 × 3, шість чисел. Вона задає афінне
перетворення: правило, яке кожній точці ставить у відповідність нову. «Афінне»
означає, що прямі лишаються прямими, а паралельні — паралельними. Правило записується двома
рядками, і в них немає нічого, крім множення й додавання:
Тут x, y — координати до перетворення, x′,
y′ — після; перші два числа рядка відповідають за поворот і масштаб, третє — за
перенос. Порахуймо одну точку руками.
Для повороту на кут α навколо центра (cx, cy) без зміни масштабу OpenCV будує:
Для α = 30° косинус дорівнює 0.8660, синус — 0.5000; центр фото — (160, 120):
Центр має лишитись на місці. Підставимо x = 160, y = 120:
Збіглося — жодної магії, два множення й додавання на координату. А тепер лівий
верхній кут, точка (0, 0): x′ = −38.56, y′ = 96.08. Ікс
отримав відʼємне значення — після повороту ця точка опинилась ліворуч
від кадру й у кадр уже не потрапить.
warpAffine малює результат у полотно того розміру, який ти вказав третім
аргументом; усе, що виїхало за межі, зникає. Скільки саме — легко поміряти, пропустивши
через ту саму матрицю білий прямокутник розміру кадру.
| кут | пікселів лишилось | частка кадру | полотно, яке вмістило б усе |
|---|---|---|---|
| 5° | 73 589 | 95.8% | 339 × 266 |
| 15° | 68 604 | 89.3% | 371 × 314 |
| 30° | 63 928 | 83.2% | 397 × 367 |
| 45° | 61 674 | 80.3% | 395 × 395 |
| 90° | 57 600 | 75.0% | 240 × 320 |
Поворот на непомітні 5° уже викидає кожен двадцять пʼятий піксель, а на 30° зникає шоста частина кадру — і зникає з кутів. Якщо поворот робиться для аугментації, частина предметів так само тихо покидає кадр разом із їхньою розміткою. Полотно, яке вмістить усе, рахується з тих самих кутів:
Вертикальні риски — це модуль, значення без знаку: для розміру напрямок нахилу неважливий. Для 30° маємо 240·0.5 + 320·0.866 = 397.1 і 240·0.866 + 320·0.5 = 367.8; розмір полотна — ціле число пікселів, тож дробову частину відкидаємо й дістаємо 397 × 367 — ті самі числа, що в таблиці. Лишається зсунути картинку в центр нового полотна:
cv2.flip(img, 1) дзеркалить по горизонталі, 0 — по вертикалі,
-1 — обидва. Те саме робить зріз img[:, ::-1], і результати
збігаються побайтово — рівність двох масивів перевіряється одним
np.array_equal. Різниця в тому, що flip
віддає суцільний масив, а зріз — вид зі зворотним кроком, який деякі функції відмовляться
приймати.Куди дівається кадр при повороті
Повзунок крутить фото. Перемикач вирішує, чи лишається полотно тим самим, чи перераховується під повернений прямокутник.
МежіМежі: чим заповнити порожнечу
Порожні кути з попереднього інтерактиву — окремий випадок загальнішого питання. Щоразу, коли операція заглядає за край зображення, треба вирішити, що там лежить: край — це не «нічого», бо щоб порахувати значення, потрібне число.
Ті самі режими приймають майже всі функції, що працюють з околом пікселя. Візьмімо найменше можливе зображення — масив 3 × 3 зі значеннями 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 | дзеркалить, не повторюючи край |
Два спостереження, обидва неочевидні. Перше: у рамці в один піксель
REPLICATE і REFLECT дають однаковий рядок, бо дзеркало
REFLECT проходить по зовнішній межі крайнього пікселя й першим
відображенням стає він сам; різниця зʼявляється з другого пікселя рамки.
Друге: REFLECT_101 дає 4 3 4 5 4 — копію
другого рядка масиву, бо його дзеркало проходить по центру крайнього пікселя й
край не подвоюється. Саме цей режим стоїть за замовчуванням майже скрізь, і причина
практична: подвоєний край створює штучну «сходинку» — два однакові значення поспіль там, де
їх у зображенні не було, — а операції, які шукають перепади, приймають її за справжню
деталь.
Що саме підставляється за краєм
Масив 3 × 3 зі значеннями 0-8 у центрі, рамка завтовшки в два пікселі навколо. У кожній клітинці — число, яке туди потрапить.
REFLECT дає перший рядок
4 3 3 4 5 5 4 із подвоєними значеннями, REFLECT_101 —
8 7 6 7 8 7 6 без жодного подвоєння. Стовпчик «перепад на межі фото» рахує
середню різницю яскравостей через край на справжньому фрагменті знімка: у
CONSTANT вона найбільша, бо чорна рамка сама по собі є різким перепадом.КолірКольорові простори в роботі
Що таке канали, чим сірий відрізняється від середнього трьох і навіщо HSV — розібрано в темі 01. Тут — тільки те, як цим користуватись.
Імʼя коду читається дослівно: з чого, у що. COLOR_BGR2GRAY — та сама
зважена сума 0.299·R + 0.587·G + 0.114·B, що й у темі 01, і вона викидає дві
третини даних.
Маска за кольором
На екрані нашого телефона є зелена смужка індикатора. cv2.inRange бере
зображення й дві межі й повертає маску: масив тієї самої висоти й ширини,
де 255 стоїть там, де всі три канали вклались у межі. Це вже знайомий із
теми 02 формат відповіді, тільки отриманий
не моделлю, а трьома нерівностями.
Перше число трійки — відтінок, воно й відповідає за «зелений»; друге й третє задані широко: «будь-який достатньо насичений і не надто темний». Ось чому — присвітімо фото нерівномірно, ніби збоку падає лампа, і подивімось на піксель індикатора з двох боків:
| піксель індикатора | відтінок H | яскравість V |
|---|---|---|
| ближче до тіні | 69 | 178 |
| ближче до лампи | 69 | 226 |
Яскравість змінилась на 48 одиниць, відтінок — узагалі ні. Тому поріг за відтінком переживає зміну освітлення, а поріг за сирими каналами BGR — ні: там змінюються всі три числа одразу.
Далі маску найпростіше обвести: x, y, w, h = cv2.boundingRect(mask) дає
найменший прямокутник, що вміщує всі ненульові пікселі, — у нас це
117 170 87 17. Чотири числа — рівно та форма відповіді, яку в
темі 02 віддавав детектор.
ПорігПороги: від пікселів до маски
Найкоротший шлях від пікселів до маски — поріг: усе, що яскравіше за число t, — біле, решта — чорне.
Функція повертає дві речі: використаний поріг і маску. Перше здається зайвим, бо ми ж самі його передали, — але звідки взяти число 127? На конкретному знімку правильний поріг залежить від освітлення. Метод Оцу (Otsu) шукає його сам, і ідея зрозуміла без формул: поріг ділить пікселі на дві купи, і хороший поріг — той, після якого всередині кожної купи значення схожі між собою, а самі купи далеко одна від одної. Метод перебирає всі 256 можливих порогів по гістограмі, тому працює швидко — перебір іде не по пікселях, а по 256 стовпчиках.
Нуль тут — заглушка: його значення ігнорується, а справжній поріг повертається першим результатом.
Де глобальний поріг ламається
Продавець сфотографував наліпку з IMEI на коробці. Треба відокремити темні цифри від світлого паперу — перший крок будь-якого розпізнавання тексту. Наліпку знято при лампі збоку: лівий край майже в тіні, правий пересвічений; цифри всюди однакові, папір усюди однаковий, змінюється лише освітлення. Мірою візьмімо IoU — той самий перетин, поділений на обʼєднання, тільки рахований по пікселях маски.
| спосіб | поріг | IoU з цифрами | правильних пікселів |
|---|---|---|---|
| назвати все папером | — | 0.0% | 89.6% |
| глобальний, найкращий можливий | 42 | 67.0% | 96.2% |
| глобальний, Оцу | 158 | 24.5% | 68.1% |
| адаптивний, блок 31 | — | 97.7% | 99.8% |
| Оцу при рівному світлі | 138 | 99.9% | 100.0% |
Останній рядок: при рівному освітленні Оцу знаходить поріг 138 і відділяє цифри практично ідеально — метод чудовий, коли його припущення виконується. Третій рядок: варто додати лампу збоку, і той самий метод дає 24.5%, бо він чесно поділив картинку на темну й світлу половини, от тільки ця межа проходить по кадру, а не по цифрах.
Другий рядок найважливіший: навіть найкращий із усіх 256 глобальних порогів дає лише 67.0%. Причина фундаментальна: найтемніший піксель паперу має 31 рівень, а найсвітліший піксель цифри — 192. Діапазони перекриваються, тож одного числа, яке відділило б перше від другого, не існує. Перший рядок контрольний: цифри займають 10.4% наліпки, тож маска «усе папір» дає 89.6% правильних пікселів, не роблячи нічого. Ось чому в таблиці стоїть IoU — частка правильних пікселів на розріджених масках підлещує будь-який метод.
Адаптивний поріг
Розвʼязок випливає з діагнозу: якщо одного порога на весь кадр не існує, треба рахувати свій поріг для кожного пікселя — по його околу.
Для кожного пікселя береться квадрат 31 × 31 навколо нього, рахується середня яскравість і від неї віднімається 6; це число й стає порогом саме для цього пікселя. Результат — IoU 97.7% замість 24.5%: освітлення зникло з задачі, бо всередині квадрата воно однакове і для пікселя, і для його околу, а різниця між ними від лампи не залежить.
Три пороги на нерівно освітленій наліпці
Угорі — фото наліпки з IMEI, освітленої лампою збоку. Унизу — маска, яку дає обраний спосіб. Зелене — правильно знайдені цифри, рожеве — зайве, тьмяне — проґавлене.
РозміткаМалювання поверх
Тема 02 обіцяла рамку детектора. Чотири числа з розділу 8 у нас є — лишилось намалювати.
- усі точки —
(x, y), з усіма наслідками з розділу 4;rectangleбере два протилежні кути; - колір — трійка BGR:
(0, 0, 255)це червоний, не синій; - товщина
-1означає «залити фігуру», а не обвести; cv2.LINE_AAвмикає згладжування — без нього похила лінія виходить сходинками.
Але найважливіше не в аргументах. Подивись іще раз: результат нікуди не присвоюється. Усі чотири функції міняють масив на місці, і оригінальні значення тих пікселів утрачено назавжди. Тепер згадай розділ 3 і склади дві половинки:
vis = img.copy(), малюємо
по vis, показуємо vis, а img лишається чистим для
обчислень. Копія кадру 1080p коштує близько половини мілісекунди — незрівнянно дешевше,
ніж день пошуку відповіді на питання, чому метрики моделі впали після того, як хтось
увімкнув візуалізацію.ПоказЯк це показати
cv2.imshow відкриває власне вікно системи: показує миттєво,
у справжньому масштабі, уміє оновлюватись у циклі — це стандартний спосіб дивитись відео
покадрово. Вимагає обслуговування:
У зошиті Jupyter цей спосіб не працює — зошит виконується на сервері,
який може не мати ані екрана, ані віконної системи, тому виклик або нічого не покаже, або
підвісить ядро. У ноутбуці беремо matplotlib:
Перетворення тут не косметичне: matplotlib чекає RGB, OpenCV віддає BGR.
Забудеш cvtColor — телефон стане синім, а стільниця блакитною, бо перший і
третій канали помінялись місцями. Чому BGR узагалі існує — у
темі 01.
waitKey вікно
imshow зʼявиться й одразу зникне — саме ця функція віддає керування системі. А
одноканальне зображення plt.imshow(gray) без аргументів розфарбує
жовто-фіолетовим: це замовчувана палітра matplotlib, а не помилка в даних. Потрібне
cmap="gray" й обовʼязково vmin=0, vmax=255, інакше matplotlib
розтягне контраст під фактичні межі й темна картинка виглядатиме нормальною.ЦінаСкільки це коштує
У реальній задачі це не одне фото, а потік кадрів. Поміряймо на кадрі 1920 × 1080 × 3 в один потік, беручи мінімум із шістдесяти прогонів.
| операція | мілісекунд | частка кадру при 25 к/с |
|---|---|---|
| img.copy() | 0.53 | 1.3% |
| resize → 960 × 540, NEAREST | 0.71 | 1.8% |
| flip по горизонталі | 0.86 | 2.2% |
| cvtColor BGR → GRAY | 0.96 | 2.4% |
| resize → 960 × 540, AREA | 1.05 | 2.6% |
| resize → 960 × 540, LINEAR | 1.07 | 2.7% |
| cvtColor BGR → HSV | 5.74 | 14.3% |
| warpAffine, поворот на 12° | 14.30 | 35.8% |
Права колонка береться так: 25 кадрів за секунду дають на кожен кадр рівно
40 мілісекунд — і в цей бюджет має вміститись читання, обробка, модель і
показ. Копія плюс зменшення плюс сірий — 2.54 мс, тобто 6.4%, практично безкоштовно.
Додаємо HSV — уже 8.28 мс, пʼята частина. Додаємо warpAffine, щоб вирівняти
нахилений кадр, — 22.58 мс: більше половини бюджету пішло на підготовку, і на розпізнавання
лишилось 17 мілісекунд. Для більшості моделей цього замало.
Головне тут не конкретні числа — на іншій машині вони будуть інші, — а стійке співвідношення: HSV коштує вшестеро дорожче за сірий, а поворот — упʼятнадцятеро. Причина видна з того, що ми розбирали: сірий — одне множення-додавання на піксель; HSV — ділення й порівняння, тобто розгалуження на кожному пікселі; поворот — чотири читання з непослідовних адрес памʼяті на кожен вихідний піксель, а памʼять не любить, коли з неї читають не підряд.
МістокКуди далі
Обіцянку виконано: ми навчились читати файл і не спіткнутись об None,
вирізати шматок і не зіпсувати оригінал, зменшувати, не втрачаючи дрібниць, повертати,
знаючи ціну в пікселях, робити маску порогом і малювати рамку. Тепер подивись на весь
список операцій згори — він розпадається рівно на дві купки.
| що робить операція | приклади |
|---|---|
| переставляє пікселі, не міняючи значень | обрізка, flip, поворот, зміна розміру, рамка |
| міняє кожен піксель окремо, не дивлячись на сусідів | cvtColor, threshold, inRange, яскравість |
Жодна операція цієї теми не питала: а що там у сусіда? Значення кожного вихідного пікселя або переїхало з іншого місця, або порахувалось із самого себе. Єдиний виняток — інтерполяція при зменшенні; вважай це першим натяком.
Обмеження видно на конкретній задачі: знайти межу корпусу телефона порогом неможливо, бо піксель усередині корпусу й піксель просто над межею мають однаково нічим не примітну яскравість. Межа — не властивість пікселя, а властивість перепаду між сусідами, і щоб її побачити, операція зобовʼязана дивитись на окіл.
Саме така операція — тема наступного заняття. Тема 04 · Згортка вручну
розбирає перше перетворення, де значення пікселя залежить від сусідів: ми зберемо його з нуля
на NumPy, порівняємо з cv2.filter2D через np.allclose, зʼясуємо
неприємну правду про те, що ця функція насправді рахує, і доведемо справу до детектора меж.
Межові режими з розділу 7 знадобляться там уже в першому абзаці — бо в
пікселя на краю кадру частина сусідів просто відсутня.