Дошка оголошень про вживані телефони, з якою ти працював увесь курс машинного навчання, змінилась. Раніше кожне оголошення було рядком таблиці: модель, рік, стан, памʼять, ціна, вік акаунта. Тепер до оголошення прикріплюють фото. І питання, яке ще вчора не мало сенсу, раптом стало практичним: як згодувати моделі картинку?
Відповідь коротка й трохи розчаровує: ніяк особливо. Картинка вже є числами — просто дуже багатьма. Уся ця тема присвячена тому, щоб ти побачив ці числа на власні очі, навчився в них тикати пальцем і перестав вважати зображення чимось принципово іншим, ніж таблиця ознак.
01 / ПостановкаФото зʼявилось в оголошенні
Щоб нічого не завантажувати з мережі й щоб у нас із тобою на екрані були однакові числа, ми не братимемо готове фото. Ми його намалюємо — прямо з формул. Кілька десятків рядків арифметики над масивом: світлий фон стільниці, темний корпус телефона зі скругленими кутами, екран із кольоровою заставкою, зелена смужка індикатора, відблиск на склі, мʼяка тінь під корпусом і легкий шум матриці.
Виходить синтетичне «фото товару» розміром 320 пікселів завширшки й 240 заввишки. Воно не претендує на реалізм, зате в нього є все, що нам потрібно: різкі краї, плавні градієнти, насичені кольори, темні й світлі ділянки. І, головне, воно однакове у кожного, хто відкриє цю сторінку або запустить зошит із практикою. Усі числа в лекції пораховані саме на ньому.
02 / Головна думкаКартинка — це таблиця чисел
Візьмемо з нашого фото крихітний шматочок: квадрат 8 на 8 пікселів у тому місці, де світла стільниця межує з темним корпусом. Це 64 клітинки. Щоб на кожну клітинку припадало рівно одне число, поки що заберемо колір і залишимо тільки яскравість: 0 — чорний, 255 — білий, усе між ними — відтінки сірого. Кольори повернемо в розділі 4.
Схема 1 · Той самий фрагмент двічі: як картинка й як числа
Ліворуч — усе фото, на ньому позначено квадратик 8 × 8. Посередині — цей квадратик, збільшений так, що видно окремі пікселі. Праворуч — ті самі 64 пікселі числами.
Подивись на четверту й пʼяту колонки таблиці. У першому рядку там стоять 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. Його форма:
Три числа означають: 240 рядків, 320 стовпців, 3 канали. Читається як «висота, ширина, канали» — саме в такому порядку, і цей порядок регулярно збиває з пантелику, бо в побуті ми кажемо «фото 320 на 240», спершу ширину.
Порядок такий не з примхи. У NumPy перша вісь — це та, по якій масив нарізається
першою: photo[0] дає перший рядок зображення. Це узгоджується з тим,
як зображення зберігається в памʼяті: рядок за рядком, зліва направо, згори вниз — так
само, як ти читаєш цю сторінку. Про
осі масиву ми говорили в курсі
машинного навчання; тут вони отримали конкретне фізичне значення.
Отже, звертання до одного числа виглядає так:
Схема 2 · Форма масиву й ціна одного знімка
Ліворуч — три площини, складені одна на одну. Праворуч — той самий знімок, розтягнутий у рядок таблиці ознак.
Затримайся на цій цифрі. У курсі машинного навчання найширша таблиця, яку ми розбирали, мала десятки колонок. Тут одна фотографія — рядок зі 150 тисяч колонок. Повнозвʼязний шар, що приймає такий рядок і має хоча б тисячу нейронів, потребує 150 мільйонів вагових коефіцієнтів — тільки на перший шар. Це не «багато», це непрацездатно. Проблема поставлена; розвʼязок буде у другому блоці курсу, і зветься він згорткою.
04 / КаналиТри площини кольору
Три канали зручно уявляти не як «трійки чисел», а як три окремі чорно-білі знімки, накладені один на одного. Перший показує, де в кадрі багато червоного, другий — зеленого, третій — синього. Складені разом, вони дають кольорову картинку; узяті окремо, кожен виглядає як звичайне чорно-біле фото.
Інтерактив 2 · Канали окремо
Ліворуч — опора для порівняння, праворуч — обрана площина. Кружечок на обох панелях позначає той самий піксель зеленого індикатора, його числа — у табло.
05 / СірийСірий — не середнє
Дуже часто зображення переводять у відтінки сірого: одне число замість трьох, утричі менше даних, а для багатьох задач цього досить. Питання в тому, як саме звести три числа до одного.
Перша ідея — узяти середнє. Для пікселя зеленого індикатора (45, 203, 93) це дає:
Ідея проста, і вона неправильна. Річ у тім, що людське око реагує на три кольори по-різному. Сітківка ока має три типи колбочок, і найбільше їх — тих, що чутливі до зеленої частини спектра. Тому зелений предмет ми сприймаємо помітно яскравішим за синій, навіть коли з погляду фізики енергії в них однаково. Середнє арифметичне цього не знає: воно вважає, що всі три кольори однаково впливають на відчуття яскравості.
Стандартна формула враховує чутливість ока. Ось вона, і зараз ми її повністю розберемо:
R, G, B — три числа пікселя: червоний, зелений і синій, кожне від 0 до 255. Y — те одне число, яке ми хочемо отримати: яскравість того самого пікселя, теж від 0 до 255. Крапка «·» означає звичайне множення.
Візьми зелене значення й порахуй його майже повністю (59 відсотків). Червоне — приблизно на третину (30 відсотків). Синє — майже не рахуй (11 відсотків). Склади три результати. Це і є яскравість.
Вони не виведені з формули, а виміряні: людям показували кольорові поля й питали, яке здається світлішим. Ваги 0.299, 0.587 і 0.114 — усереднений результат таких вимірювань, закріплений ще в стандарті аналогового кольорового телебачення. Головне, що вони дають у сумі рівно 1: 0.299 + 0.587 + 0.114 = 1.000. Тому білий піксель (255, 255, 255) лишається білим, а чорний — чорним.
Піксель індикатора (45, 203, 93):
Було 114 за середнім, стало 143 за вагами. Різниця — 29 рівнів яскравості з 255. Це не похибка округлення, це третина шляху від чорного до сірого.
Перевір це очима: у другому інтерактиві вище перемкнись на режим «сірий: 2 способи». Ліва панель — просте середнє, права — правильні ваги. Зелена смужка на правій панелі помітно світліша, синя частина заставки — помітно темніша. Режим «їхня різниця» показує те саме як карту: там, де два способи розходяться найсильніше, карта світиться.
Бібліотечна функція cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) робить рівно цей
розрахунок, тільки в цілих числах замість дробових — заради швидкості. Через це її
результат може відрізнятись від нашої формули на одиницю, приблизно у двох пікселях
з десяти тисяч. У практиці ми це перевіримо assert-ом.
06 / ПросториHSV: один поріг замість трьох
RGB — не єдиний спосіб описати колір. Він зручний залізу (у монітора буквально три різні лампочки в кожному пікселі), але незручний людині й незручний для задач на кшталт «знайди на фото все зелене».
Спробуй сформулювати «зелений» через RGB. Доведеться сказати щось на зразок «зеленого багато, червоного мало, синього мало» — три умови одночасно. І варто вимкнути світло, як усі три числа поїдуть униз, а межі доведеться підбирати заново.
HSV розкладає колір інакше — на три інші величини:
- H (hue, відтінок) — «який це колір»: місце на кольоровому колі. В OpenCV кодується числом від 0 до 179, де 0 — червоний, 60 — зелений, 120 — синій.
- S (saturation, насиченість) — «наскільки колір соковитий»: 0 — сірий без кольору, 255 — гранично чистий колір.
- V (value, значення) — «наскільки він світлий»: просто найбільше з трьох чисел RGB.
Той самий піксель індикатора (45, 203, 93) у HSV має вигляд (69, 198, 203):
відтінок 69 — зелена ділянка кола, насиченість 198 з 255 — колір соковитий, яскравість
203 — світлий. Тепер «знайди все зелене» пишеться однією умовою: 45 ≤ H ≤ 75.
І ось що з цією умовою відбувається, коли змінюється освітлення:
| що зробили з фото | умова по H | три умови по RGB | треба знайти |
|---|---|---|---|
| нічого | 1439 | 1439 | 1439 |
| притемнили ×0.55 | 1439 | 0 | 1439 |
| висвітлили +60 | 1439 | 568 | 1439 |
У таблиці — кількість знайдених пікселів індикатора. Умова по відтінку не помітила зміни освітлення взагалі: 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 є властивість, на якій
спотикаються всі:
Ні, це не одрук. У беззнаковому восьмибітовому числі 260 просто не існує. Коли результат не влазить, старший розряд відкидається, і лічильник перескакує через нуль — як пробіг у старому автомобілі, що після 999999 показує 000000. Формально: результат беруть за остачею від ділення на 256, тобто 260 − 256 = 4. Це зветься переповненням (overflow).
І найпідступніше тут — що ніякої помилки не буде. Код відпрацює, масив лишиться масивом, програма нічого не скаже. Ти просто отримаєш зображення, де найсвітліші ділянки стали чорними.
| що написали | що вийде для 250 + 10 | чому |
|---|---|---|
| photo + 10 | 4 | арифметика uint8, обгортається через нуль |
| cv2.add(a, b) | 255 | OpenCV сам обрізає по межах типу |
| astype(int16) | 260 | 16 біт зі знаком, місця вистачає |
| np.clip(…, 0, 255) | 255 | ми обрізаємо самі, вручну |
Робочий рецепт такий. Якщо в тебе одна проста операція — бери
cv2.add і cv2.subtract, вони самі стежать за межами. Якщо
обчислення довше — перейди в int16 або float32, порахуй усе там,
а наприкінці один раз обріж і поверни в uint8:
Окремо про float32. Нейромережі майже завжди хочуть числа не 0–255,
а 0–1 у дробовому вигляді. Причина не в естетиці: під час навчання ваги оновлюються
кроками, і якщо вхід має розмах у 255 одиниць, градієнти виходять велетенськими, а крок
доводиться робити мікроскопічним. Тому перед подачею в модель роблять
photo.astype(np.float32) / 255.0. Це та сама
нормалізація ознак, про яку
йшлося в курсі машинного навчання, — тільки ознак тут 150 тисяч.
09 / АрифметикаЯскравість і контраст — арифметика
Тепер приємне. «Зробити фото світлішим» — це не таємнича операція з редактора, а додавання числа до масиву. «Підняти контраст» — множення. Обидві дії ти вже сто разів робив у курсі машинного навчання, просто над таблицею, а не над картинкою:
Розберемо словами. 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. Смужки під панелями — гістограми результату.
10 / ДіагностикаГістограма знімка
Гістограма зображення — це стовпчикова діаграма, яка відповідає на одне питання: скільки в кадрі пікселів кожної яскравості. По горизонталі — значення від 0 до 255, по вертикалі — кількість пікселів із таким значенням. Уся інформація про композицію кадру втрачається, лишається тільки розподіл світла.
Здавалось би, дрібниця, але саме за гістограмою одним поглядом видно те, що на око можна проґавити.
Схема 4 · Три експозиції й три гістограми
Те саме фото, знімане ніби при різному світлі. Під кожним — гістограма його яскравості.
Ключове слово — безповоротно. Якщо піксель уперся в 255, у нього відібрали інформацію: усі значення, що мали бути 260, 280 і 340, стали одним і тим самим числом. Притемнити таке фото назад не вийде — вийде рівна сіра пляма замість деталей. Тому в нашому третьому інтерактиві права панель, хоч і не показує психоделічних артефактів, теж псує зображення — просто тихіше.
Для навчання моделей це має пряме значення. Якщо в датасеті багато пересвічених знімків, модель побачить у них однорідні білі плями й нічого з них не візьме. І навпаки, штучна зміна яскравості й контрасту — це один з найдешевших способів збільшити датасет, бо об’єкт на фото від цього не змінюється, а числа змінюються сильно.
11 / РозмірСкільки важить зображення
Порахувати вагу нестиснутого зображення можна в голові: перемнож висоту, ширину
й кількість каналів — це кількість чисел, а кожне число в uint8 займає
рівно один байт.
| що це | розмір | чисел | памʼяті |
|---|---|---|---|
| наше фото | 320 × 240 × 3 | 230 400 | 225 КіБ |
| вхід нейромережі | 224 × 224 × 3 | 150 528 | 147 КіБ |
| він же у float32 | 224 × 224 × 3 | 150 528 | 588 КіБ |
| кадр Full HD | 1920 × 1080 × 3 | 6 220 800 | 5.93 МіБ |
| фото з телефона | 4032 × 3024 × 3 | 36 578 304 | 34.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 | розмір файлу | стиснення | середня зміна | найбільша |
|---|---|---|---|---|
| 95 | 21 618 Б | 10.7× | 2.8 | 93 |
| 90 | 12 367 Б | 18.6× | 3.0 | 84 |
| 75 | 5 893 Б | 39.1× | 3.3 | 83 |
| 20 | 2 773 Б | 83.1× | 4.7 | 101 |
«Середня зміна» — на скільки рівнів у середньому змінилось одне число після збереження й зчитування. «Найбільша» — найгірший піксель. Зверни увагу на два факти. Перший: навіть за якості 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.Далі в темі
Теорію прочитано. Тепер закріпи її на практиці.