Компʼютерний зір · Блок 1 · Тема 01

Зображення очима компʼютера

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

Дошка оголошень про вживані телефони, з якою ти працював увесь курс машинного навчання, змінилась. Раніше кожне оголошення було рядком таблиці: модель, рік, стан, памʼять, ціна, вік акаунта. Тепер до оголошення прикріплюють фото. І питання, яке ще вчора не мало сенсу, раптом стало практичним: як згодувати моделі картинку?

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

01 / ПостановкаФото зʼявилось в оголошенні

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

Виходить синтетичне «фото товару» розміром 320 пікселів завширшки й 240 заввишки. Воно не претендує на реалізм, зате в нього є все, що нам потрібно: різкі краї, плавні градієнти, насичені кольори, темні й світлі ділянки. І, головне, воно однакове у кожного, хто відкриє цю сторінку або запустить зошит із практикою. Усі числа в лекції пораховані саме на ньому.

Що таке піксель. Слово походить від англійського picture element — «елемент зображення». Це найдрібніша клітинка, з яких складено картинку: маленький однорідний квадратик одного кольору. Жодних півклітинок не буває — компʼютер зберігає рівно стільки квадратиків, скільки їх є, і для кожного зберігає числа, що задають колір. Усе інше в цій темі — наслідки цього одного речення.

02 / Головна думкаКартинка — це таблиця чисел

Візьмемо з нашого фото крихітний шматочок: квадрат 8 на 8 пікселів у тому місці, де світла стільниця межує з темним корпусом. Це 64 клітинки. Щоб на кожну клітинку припадало рівно одне число, поки що заберемо колір і залишимо тільки яскравість: 0 — чорний, 255 — білий, усе між ними — відтінки сірого. Кольори повернемо в розділі 4.

Схема 1 · Той самий фрагмент двічі: як картинка й як числа

Ліворуч — усе фото, на ньому позначено квадратик 8 × 8. Посередині — цей квадратик, збільшений так, що видно окремі пікселі. Праворуч — ті самі 64 пікселі числами.

Це не два різні обʼєкти. Квадратик посередині й таблиця праворуч — одна й та сама річ, показана двома способами. Ліві чотири колонки чисел близькі до 185 — це світла стільниця. Праві чотири близькі до 70 — це темний корпус. Межа між ними в таблиці видно так само чітко, як на картинці.

Подивись на четверту й пʼяту колонки таблиці. У першому рядку там стоять 181 і 72. Між сусідніми пікселями — стрибок на 109 рівнів яскравості. Ось це і є «край предмета» мовою чисел: не лінія, не контур, а різниця між двома сусідніми числами. Коли в наступних темах ми шукатимемо межі обʼєктів, шукати ми будемо саме такі стрибки.

А тепер помітимо дрібницю, яку легко проґавити. У світлій частині числа не однакові: 188, 183, 187, 181. Стільниця ж намальована рівним градієнтом — звідки розкид у 7 рівнів? Це шум матриці: у справжній камері сенсор ніколи не дає двом сусіднім пікселям точно однакове значення, і в нашому синтетичному фото ми його теж додали. Через розділ ти побачиш, як цей шум псує стиснення.

03 / БудоваПіксель, роздільність, форма масиву

Тепер повернімо колір. У кольоровому зображенні один піксель — це не одне число, а три: скільки в ньому червоного, скільки зеленого, скільки синього. Кожне — від 0 до 255. Піксель (45, 203, 93) означає «мало червоного, багато зеленого, трохи синього», тобто яскраво-зелений. Піксель (0, 0, 0) — чорний, (255, 255, 255) — білий.

Далі — інтерактив, який варто покрутити довше за решту. Рухай рамку по фото (мишею, пальцем або повзунками) і дивись, як шматок картинки перетворюється на числа.

Інтерактив 1 · Піксель під лупою

Рамка 8 × 8 пікселів на фото ліворуч. Праворуч ті самі пікселі, збільшені у 40 разів, із числом усередині кожного. Перемикач обирає, яке саме з чотирьох чисел пікселя показувати.

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

Наше фото цілком — це numpy-масив, з яким ти знайомий ще з теми про NumPy. Його форма:

photo.shape → (240, 320, 3)

Три числа означають: 240 рядків, 320 стовпців, 3 канали. Читається як «висота, ширина, канали» — саме в такому порядку, і цей порядок регулярно збиває з пантелику, бо в побуті ми кажемо «фото 320 на 240», спершу ширину.

Порядок такий не з примхи. У NumPy перша вісь — це та, по якій масив нарізається першою: photo[0] дає перший рядок зображення. Це узгоджується з тим, як зображення зберігається в памʼяті: рядок за рядком, зліва направо, згори вниз — так само, як ти читаєш цю сторінку. Про осі масиву ми говорили в курсі машинного навчання; тут вони отримали конкретне фізичне значення.

Отже, звертання до одного числа виглядає так:

photo[176, 150, 1] → 203 # рядок 176, стовпець 150, канал G photo[176, 150] → [45 203 93] # усі три канали цього пікселя photo[:, :, 1] → масив 240 × 320 # уся зелена площина

Схема 2 · Форма масиву й ціна одного знімка

Ліворуч — три площини, складені одна на одну. Праворуч — той самий знімок, розтягнутий у рядок таблиці ознак.

Порахуй разом зі схемою: 240 × 320 × 3 = 230 400. Стільки чисел у нашому маленькому фото. Стандартний для нейромереж вхід 224 × 224 × 3 дає 150 528 чисел — це один обʼєкт, тобто один рядок таблиці завширшки 150 528 колонок.

Затримайся на цій цифрі. У курсі машинного навчання найширша таблиця, яку ми розбирали, мала десятки колонок. Тут одна фотографія — рядок зі 150 тисяч колонок. Повнозвʼязний шар, що приймає такий рядок і має хоча б тисячу нейронів, потребує 150 мільйонів вагових коефіцієнтів — тільки на перший шар. Це не «багато», це непрацездатно. Проблема поставлена; розвʼязок буде у другому блоці курсу, і зветься він згорткою.

04 / КаналиТри площини кольору

Три канали зручно уявляти не як «трійки чисел», а як три окремі чорно-білі знімки, накладені один на одного. Перший показує, де в кадрі багато червоного, другий — зеленого, третій — синього. Складені разом, вони дають кольорову картинку; узяті окремо, кожен виглядає як звичайне чорно-біле фото.

Інтерактив 2 · Канали окремо

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

що показати праворуч
піксель індикатора · R G B
ліворуч
праворуч
Прожени всі вісім. Зелена смужка індикатора яскрава в каналі G і майже чорна в каналі R — бо зеленого в ній 203, а червоного 45. Заставка екрана поводиться навпаки: угорі вона синя, тому канал B там світлий, а канал R темний.

05 / СірийСірий — не середнє

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

Перша ідея — узяти середнє. Для пікселя зеленого індикатора (45, 203, 93) це дає:

(45 + 203 + 93) ÷ 3 = 341 ÷ 3 = 113.67 → 114

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

Стандартна формула враховує чутливість ока. Ось вона, і зараз ми її повністю розберемо:

Y = 0.299 · R + 0.587 · G + 0.114 · B
крок 1 — що це за букви

R, G, B — три числа пікселя: червоний, зелений і синій, кожне від 0 до 255. Y — те одне число, яке ми хочемо отримати: яскравість того самого пікселя, теж від 0 до 255. Крапка «·» означає звичайне множення.

крок 2 — що означає формула словами

Візьми зелене значення й порахуй його майже повністю (59 відсотків). Червоне — приблизно на третину (30 відсотків). Синє — майже не рахуй (11 відсотків). Склади три результати. Це і є яскравість.

крок 3 — чому саме такі три числа

Вони не виведені з формули, а виміряні: людям показували кольорові поля й питали, яке здається світлішим. Ваги 0.299, 0.587 і 0.114 — усереднений результат таких вимірювань, закріплений ще в стандарті аналогового кольорового телебачення. Головне, що вони дають у сумі рівно 1: 0.299 + 0.587 + 0.114 = 1.000. Тому білий піксель (255, 255, 255) лишається білим, а чорний — чорним.

крок 4 — рахуємо на тому самому пікселі

Піксель індикатора (45, 203, 93):

0.299 × 45 = 13.455 0.587 × 203 = 119.161 0.114 × 93 = 10.602 сума = 143.218 → 143

Було 114 за середнім, стало 143 за вагами. Різниця — 29 рівнів яскравості з 255. Це не похибка округлення, це третина шляху від чорного до сірого.

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

Бібліотечна функція cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) робить рівно цей розрахунок, тільки в цілих числах замість дробових — заради швидкості. Через це її результат може відрізнятись від нашої формули на одиницю, приблизно у двох пікселях з десяти тисяч. У практиці ми це перевіримо assert-ом.

06 / ПросториHSV: один поріг замість трьох

RGB — не єдиний спосіб описати колір. Він зручний залізу (у монітора буквально три різні лампочки в кожному пікселі), але незручний людині й незручний для задач на кшталт «знайди на фото все зелене».

Спробуй сформулювати «зелений» через RGB. Доведеться сказати щось на зразок «зеленого багато, червоного мало, синього мало» — три умови одночасно. І варто вимкнути світло, як усі три числа поїдуть униз, а межі доведеться підбирати заново.

HSV розкладає колір інакше — на три інші величини:

Той самий піксель індикатора (45, 203, 93) у HSV має вигляд (69, 198, 203): відтінок 69 — зелена ділянка кола, насиченість 198 з 255 — колір соковитий, яскравість 203 — світлий. Тепер «знайди все зелене» пишеться однією умовою: 45 ≤ H ≤ 75. І ось що з цією умовою відбувається, коли змінюється освітлення:

що зробили з фотоумова по Hтри умови по RGBтреба знайти
нічого143914391439
притемнили ×0.55143901439
висвітлили +6014395681439

У таблиці — кількість знайдених пікселів індикатора. Умова по відтінку не помітила зміни освітлення взагалі: 1439 з 1439 у всіх трьох випадках. Три пороги по RGB на притемненому фото не знайшли жодного пікселя, а на висвітленому — менш ніж половину. Це і є вся практична цінність HSV: він відділяє «який це колір» від «скільки на нього впало світла».

простірщо даєколи брати
RGBТри яскравості: червона, зелена, синя лампочки пікселя. За замовчуванням. Усе, що йде в нейромережу, і все, що показується на екрані.
GrayscaleОдне число — яскравість, утричі менше даних. Коли колір не несе інформації: пошук країв, текст, номери, штрихкоди.
HSVОкремо відтінок, насиченість і світлість. Виділення обʼєкта за кольором, робота при змінному освітленні.
LabЯскравість плюс дві осі кольору, підігнані під сприйняття ока. Коли треба порівнювати кольори «наскільки вони різні на вигляд».

07 / ПасткаBGR: пастка OpenCV

А тепер найпоширеніша помилка новачка в компʼютерному зорі — і, чесно кажучи, не тільки новачка.

OpenCV зберігає канали в порядку синій, зелений, червоний. Не RGB, а BGR. Історична причина проста й трохи безглузда: коли бібліотеку писали наприкінці 1990-х, саме такий порядок байтів використовували камери й формат BMP під Windows, і його взяли як є. Виправити зараз неможливо — зламається весь код, написаний за чверть століття.

Наслідок: cv2.imread повертає масив, у якому нульовий канал — синій. А matplotlib, PIL, більшість нейромереж і взагалі весь інший світ чекають на RGB. Якщо просто передати одне в інше, помилки не буде: масив правильної форми, правильного типу, усі числа на місці. Просто небо стане помаранчевим.

Схема 3 · Те саме фото, переплутані канали

Ліворуч — правильний порядок. Праворуч — той самий масив, показаний так, ніби перший канал червоний, хоча насправді він синій.

Ознака в природі: синє стало помаранчевим, помаранчеве — синім, а сірі й білі ділянки не змінились узагалі. Зелений канал стоїть посередині в обох порядках, тому зелене теж лишається собою. Якщо на твоєму знімку небо руде — річ майже напевно в цьому.
Правило, яке рятує. Одразу після cv2.imread(path) став cv2.cvtColor(img, cv2.COLOR_BGR2RGB) — і далі по всьому коду працюй у RGB. Зворотний перехід роби тільки в момент запису файлу через cv2.imwrite. Тоді порядок каналів перестає бути питанням, про яке треба памʼятати в кожному рядку.

08 / Тип данихБітова глибина й переповнення

Чому саме 0–255, а не, скажімо, 0–1000? Тому що на одне число відводять один байт — вісім двійкових розрядів. Вісім розрядів дають 28 = 256 різних комбінацій, тобто цілі числа від 0 до 255. Тип даних, що це описує, зветься uint8: «unsigned integer, 8 біт», ціле беззнакове восьмибітове.

256 градацій на канал — це 256 × 256 × 256 ≈ 16.7 мільйона кольорів. Для екрана й для ока цього більш ніж достатньо. Але у uint8 є властивість, на якій спотикаються всі:

250 + 10 = 4

Ні, це не одрук. У беззнаковому восьмибітовому числі 260 просто не існує. Коли результат не влазить, старший розряд відкидається, і лічильник перескакує через нуль — як пробіг у старому автомобілі, що після 999999 показує 000000. Формально: результат беруть за остачею від ділення на 256, тобто 260 − 256 = 4. Це зветься переповненням (overflow).

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

що написалищо вийде для 250 + 10чому
photo + 104арифметика uint8, обгортається через нуль
cv2.add(a, b)255OpenCV сам обрізає по межах типу
astype(int16)26016 біт зі знаком, місця вистачає
np.clip(…, 0, 255)255ми обрізаємо самі, вручну

Робочий рецепт такий. Якщо в тебе одна проста операція — бери cv2.add і cv2.subtract, вони самі стежать за межами. Якщо обчислення довше — перейди в int16 або float32, порахуй усе там, а наприкінці один раз обріж і поверни в uint8:

result = np.clip(photo.astype(np.int16) + 40, 0, 255).astype(np.uint8)

Окремо про float32. Нейромережі майже завжди хочуть числа не 0–255, а 0–1 у дробовому вигляді. Причина не в естетиці: під час навчання ваги оновлюються кроками, і якщо вхід має розмах у 255 одиниць, градієнти виходять велетенськими, а крок доводиться робити мікроскопічним. Тому перед подачею в модель роблять photo.astype(np.float32) / 255.0. Це та сама нормалізація ознак, про яку йшлося в курсі машинного навчання, — тільки ознак тут 150 тисяч.

09 / АрифметикаЯскравість і контраст — арифметика

Тепер приємне. «Зробити фото світлішим» — це не таємнича операція з редактора, а додавання числа до масиву. «Підняти контраст» — множення. Обидві дії ти вже сто разів робив у курсі машинного навчання, просто над таблицею, а не над картинкою:

новий = (старий − 128) × k + 128 + b

Розберемо словами. b — це зсув яскравості: додаємо однакове число до кожного пікселя, і все фото рівномірно світлішає (при b більше нуля) або темнішає. k — коефіцієнт контрасту: відстань кожного пікселя від середини діапазону множиться на k. При k = 1 нічого не змінюється; при k = 2 світле стає ще світлішим, темне — ще темнішим, картинка «жорсткішає»; при k = 0.5 все сповзає до сірого. Віднімання й додавання 128 потрібне тільки для того, щоб розтягувати відносно середини діапазону, а не відносно чорного.

Найважливіше: photo — масив 240 × 320 × 3, а b — одне число. Складати їх дозволяє broadcasting, той самий механізм NumPy, що розтягує одне число на весь масив. Жодного циклу по пікселях писати не треба — і не варто: цикл на 230 400 елементів у Python працює в сотні разів повільніше.

Інтерактив 3 · Яскравість, контраст і переповнення

Обидві панелі рахують ту саму формулу. Ліва зберігає результат у uint8 без жодного захисту, права обрізає значення по межах 0 і 255. Смужки під панелями — гістограми результату.

вилетіло вгору
вилетіло вниз
частка зіпсованих
середнє праворуч
Почни зі стандартного стану. Зсув +40 і контраст 1.00 — і на лівій панелі вже видно чорні плями там, де фото найсвітліше: ці пікселі перескочили через 255 і почали рахуватись від нуля. Права панель у тому самому місці просто біла. Постав контраст 2.40 — і ліва панель розсиплеться повністю.

10 / ДіагностикаГістограма знімка

Гістограма зображення — це стовпчикова діаграма, яка відповідає на одне питання: скільки в кадрі пікселів кожної яскравості. По горизонталі — значення від 0 до 255, по вертикалі — кількість пікселів із таким значенням. Уся інформація про композицію кадру втрачається, лишається тільки розподіл світла.

Здавалось би, дрібниця, але саме за гістограмою одним поглядом видно те, що на око можна проґавити.

Схема 4 · Три експозиції й три гістограми

Те саме фото, знімане ніби при різному світлі. Під кожним — гістограма його яскравості.

Читається так. Уся маса зліва — знімок недоекспонований: деталі в тінях є, але втиснуті у вузький діапазон 26…96 замість 0…255. Маса справа з високим стовпчиком рівно на 255 — знімок пересвічений, і цей стовпчик означає безповоротно втрачені пікселі. Посередині маса розподілена по більшій частині шкали, і жоден край не переповнений — так і має бути.

Ключове слово — безповоротно. Якщо піксель уперся в 255, у нього відібрали інформацію: усі значення, що мали бути 260, 280 і 340, стали одним і тим самим числом. Притемнити таке фото назад не вийде — вийде рівна сіра пляма замість деталей. Тому в нашому третьому інтерактиві права панель, хоч і не показує психоделічних артефактів, теж псує зображення — просто тихіше.

Для навчання моделей це має пряме значення. Якщо в датасеті багато пересвічених знімків, модель побачить у них однорідні білі плями й нічого з них не візьме. І навпаки, штучна зміна яскравості й контрасту — це один з найдешевших способів збільшити датасет, бо об’єкт на фото від цього не змінюється, а числа змінюються сильно.

11 / РозмірСкільки важить зображення

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

що церозмірчиселпамʼяті
наше фото320 × 240 × 3230 400225 КіБ
вхід нейромережі224 × 224 × 3150 528147 КіБ
він же у float32224 × 224 × 3150 528588 КіБ
кадр Full HD1920 × 1080 × 36 220 8005.93 МіБ
фото з телефона4032 × 3024 × 336 578 30434.9 МіБ

Третій рядок пояснює, чому про тип даних варто памʼятати. Той самий знімок у float32 важить учетверо більше, бо дробове число займає чотири байти замість одного. Датасет із 50 тисяч картинок 224 × 224 — це 7.5 гігабайта в uint8 і 30 гігабайтів у float32. Саме тому картинки тримають на диску стиснутими, а в float32 переводять невеликими порціями, уже під час навчання.

Що робить JPEG

Наше фото важить 225 КіБ. Збережене у JPEG з якістю 75, воно займає 5 893 байти — у 39 разів менше. Той самий файл у PNG — 125 КіБ, усього в 1.8 раза менше. Різниця в тому, що PNG стискає без втрат: із файлу відновлюється точнісінько той масив, який записували. JPEG стискає із втратами: відновлюється схоже, але не те саме.

якість JPEGрозмір файлустисненнясередня змінанайбільша
9521 618 Б10.7×2.893
9012 367 Б18.6×3.084
755 893 Б39.1×3.383
202 773 Б83.1×4.7101

«Середня зміна» — на скільки рівнів у середньому змінилось одне число після збереження й зчитування. «Найбільша» — найгірший піксель. Зверни увагу на два факти. Перший: навіть за якості 95 середня зміна не нульова — 2.8 рівня. Це JPEG прибрав той самий шум матриці, з якого ми починали: дрібні випадкові коливання він вважає несуттєвими й викидає першими.

Другий факт цікавіший. Найбільша похибка при якості 75 — 83 рівні, і сидить вона в пікселі (171, 201), тобто рівно на краю зеленої смужки індикатора. JPEG розкладає зображення на плавні хвилі й ріже найдрібніші з них; різкий перепад плавними хвилями описується погано, і навколо контрастної межі зʼявляється характерне «дзвеніння» — світлий і темний ореол. Це і є артефакти стиснення.

Чому це важить для моделей. По-перше, мережа вчиться на тому, що їй дали: якщо навчальні знімки збережені з якістю 95, а в бойових умовах приходять з якістю 40, статистика входу зміниться, і точність упаде. По-друге, артефакти живуть саме на різких краях — на тому, за що чіпляються перші шари згорткової мережі. По-третє, «пережати датасет ще раз» — операція незворотна: кожне перезбереження JPEG додає втрат до вже наявних.

12 / МістокКуди далі

Підсумуємо одним абзацом. Зображення — тривимірний масив чисел форми (висота, ширина, канали). Кожне число — від 0 до 255, тип uint8, і арифметика в ньому обгортається через нуль без жодного попередження. Три канали — це три чорно-білі знімки; сірий із них роблять зваженою сумою, а не середнім. OpenCV тримає канали задом наперед. Яскравість і контраст — додавання й множення над масивом. Гістограма показує, що з експозицією. І один знімок 224 × 224 — це 150 тисяч чисел, через які доведеться щось робити.

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

Далі в практиці. У practice.ipynb ти згенеруєш це саме фото з нуля, роздрукуєш фрагмент 8 × 8 числами, розкладеш знімок на канали, зробиш сірий двома способами й побачиш різницю майже в 30 рівнів, влаштуєш переповнення uint8 і вилікуєш його трьома різними способами, покрутиш яскравість із контрастом, побудуєш гістограму й поміряєш, скільки важить фото до й після JPEG.

Далі в темі

Теорію прочитано. Тепер закріпи її на практиці.