5.4. Читання та запис пікселів¶
Більшість операцій над зображенням приховують посіпіксельну роботу в середині одного виклику методу, де цикли, що обробляють кожен піксель, виконуються з нативною швидкістю. Проте трапляються випадки, коли код застосунку потребує безпосереднього звернення до конкретного пікселя: щоб прочитати значення в певній позиції, записати нове значення, отримати зразок у одній точці для кроку калібрування або налагодити значення у відомому місці. Модуль image надає такий рівень доступу через дві форми адресування, кожна з яких відповідає різному способу мислення про розташування пікселя.
5.4.1. Адресування за координатами¶
Найбільш природна форма — та, для якої Coordinates вже розробила словник: позначити піксель через декартові (x, y). get_pixel() приймає (x, y) і повертає значення в цій позиції; set_pixel() приймає ті самі (x, y) разом зі значенням і записує його.
Що саме ці виклики повертають або приймають, залежить від формату зображення. Зображення у відтінках сірого, бінарні та Bayer-зображення містять одне значення на піксель — яскравість для відтінків сірого, 0 або 1 для бінарного, зразок одного каналу кольору для Bayer, — тому get_pixel() повертає одне ціле число. RGB565 містить три канали кольору, упаковані в 16 бітів, і get_pixel розпаковує їх у кортеж (r, g, b) за замовчуванням, відображаючи кожен канал у діапазон 0 – 255.
Поведінку за замовчуванням можна змінити з обох боків. Передача rgbtuple=False до get_pixel для зображення RGB565 повертає необроблене 16-бітне упаковане слово — таку саму форму, яку повертає лінійний індекс, і ефективну форму, коли застосунок збирається одразу записати це упаковане значення назад. Передача rgbtuple=True для одноканального зображення робить протилежне: збережене значення перетворюється на кортеж RGB888 перед поверненням, а Bayer-зображення проходять крок дебаєрингу на місці. Цей аргумент дозволяє викликаючому коду запитувати пікселі в однаковому кольоровому просторі незалежно від того, як базове зображення їх зберігає.
Стиснені зображення — JPEG та PNG — не підтримуються get_pixel або set_pixel. Їхні байти не представляють пікселі у відомих позиціях, і методи викидають помилку, а не повертають значення, яке нічого б не означало.
На практиці шаблони виглядають так:
v = img.get_pixel(40, 30) # grayscale: int 0..255
img.set_pixel(40, 30, 255) # write white
r, g, b = img.get_pixel(40, 30) # RGB565: defaults to (r, g, b) tuple
img.set_pixel(40, 30, (255, 0, 0)) # write red
Якщо запитані (x, y) виходять за межі зображення, get_pixel повертає None, а set_pixel нічого не робить. Це навмисно прощаючий підхід: багато алгоритмів проходять близько до країв зображення і ненадовго виходять за межі, а тихий no-op менш руйнівний, ніж виняток кожного разу.
5.4.2. Адресування за лінійним індексом¶
Інша форма — це адресування пікселів за їхньою позицією в базовому буфері. Нагадаємо розміщення буфера: пікселі зберігаються рядок за рядком, спочатку всі пікселі верхнього рядка, потім всі пікселі наступного рядка і так далі до нижнього. Таке розміщення означає, що кожен піксель має єдиний цілочисельний індекс, що відраховується від 0 у верхньому лівому куті та збільшується вздовж кожного рядка по черзі. Піксель у координаті (x, y) має лінійний індекс y * width + x.
Пікселі адресуються як за декартовими (x, y), так і за лінійним індексом, що обходить буфер рядок за рядком, зліва направо.¶
Модуль image надає цей індекс через звичайну нотацію підписки Python: img[i] читає піксель за лінійним індексом i, img[i] = value записує один. Те, що повертає форма з індексом — це необроблене збережене значення для формату, а не розпакований кортеж, який get_pixel() повертає за замовчуванням. Ця відмінність важлива, оскільки формат, обраний раніше, визначає, як виглядає необроблене значення:
Пікселі у відтінках сірого та Bayer повертаються як 8-бітні цілі числа.
Пікселі RGB565 та YUV422 повертаються як 16-бітні цілі числа — упаковане слово.
Бінарні пікселі повертаються як
0або1.Пікселі JPEG та PNG повертаються як 8-бітні цілі числа, по одному байту за раз зі стиснутого потоку. Ці значення непрозорі — вони є частинами стиснутого кодування, а не пікселями в будь-якому звичайному сенсі.
Форма з індексом підходить для коду, який вже думає в термінах зміщень буфера: цикл, що обходить кожен піксель один раз, алгоритм, якому потрібно переходити на рядок за раз, або код, що перетворює між розміщеннями буферів. Код, який думає в термінах координат x та y, краще обслуговується get_pixel і set_pixel; обидві форми адресують ті самі пікселі через різні ментальні моделі.
Image також є ітерованим. for v in img: обходить буфер у тому самому рядково-мажорному порядку, повертаючи необроблені значення по одному пікселю за раз, а len(img) — це кількість пікселів для нестиснутих форматів або кількість байтів для стиснутих потоків.
5.4.3. Чому посіпіксельний Python є повільним шляхом¶
Практична замітка, яку варто чесно зазначити. Обхід зображення по одному пікселю за раз з Python — повільний. Зображення у відтінках сірого розміром 320 × 240 містить 76 800 пікселів; виклик get_pixel() для кожного з них у циклі for виконує мільйони інструкцій байткоду MicroPython для виконання роботи, яку еквівалентний нативний метод міг би завершити за кілька сотень мікросекунд. Це не малий коефіцієнт. Це різниця між скриптом, що обробляє кадри в реальному часі, і тим, що повзе значно нижче кадрової частоти камери.
Майже кожен метод на поверхні Image існує тому, що є швидша нативна версія поширеного посіпіксельного шаблону. Цикл, що складає два зображення разом, стає єдиним нативним викликом. Цикл, що згладжує кожен піксель шляхом усереднення з сусідами, стає іншим. Цикл, що класифікує кожен піксель за порогом, стає третім. Завдання застосунку, в більшості випадків, полягає в тому, щоб розпізнати, який метод цілого зображення відповідає роботі, яку б виконав цикл, і використати його замість написання циклу вручну.
Читання та запис на рівні пікселя все ще є правильним інструментом, коли більше нічого не підходить — виправлення конкретного виміру назад у буфер, отримання зразка однієї позиції для кроку калібрування, налагодження значення у відомому місці. Суть у тому, що це повільний шлях, який використовується, коли методи цілого зображення не мають потрібної форми для застосунку, а не як стандартний спосіб роботи з пікселями.