6.1. Навіщо масиви¶
Клас Image — правильний інструмент для роботи з пікселями, оскільки кожен його метод безпосередньо працює з нативним піксельним буфером камери в одному швидкому виклику. Більшість того, що застосунок робить з кадром — порогова обробка, пошук плям, виявлення AprilTag, фільтри меж — вже там є.
Чого бібліотека зображень не надає — це решта числової роботи, з якою стикається застосунок OpenMV:
буфери датчиків, що не є пікселями — зразки ADC, осі з IMU (інерційний вимірювальний блок), аудіо мікрофона,
похідні числа із зображення, що не повертає жоден вбудований метод — стовпець гістограми, власне змішування двох кадрів, поелементне перетворення, якого немає в каталозі,
невелика лінійна алгебра — матриця калібрування для корекції лінзи, поворот, що об’єднує показники IMU,
математика обробки сигналів — частотний вміст буфера вібрацій, згладжування виходу датчика, вектор ознак, що потрібний класифікатору на вході.
Усе це потребує однієї форми: буфера чисел з однією операцією, застосованою до кожного елемента. Python for цикл — очевидний спосіб написати це:
for i in range(len(samples)):
samples[i] = samples[i] * cal
Цикл працює. Але він повільний. Python — інтерпретована мова, і кожна ітерація циклу несе вартість одного запуску інтерпретатора: пошук samples, читання елемента i, множення, запис назад, збільшення лічильника, перевірка умови циклу. На буфері з тисячі зразків датчика ці витрати інтерпретатора складаються в десятки мілісекунд для того, що є фундаментально швидкою операцією.
Ці накладні витрати дають про себе знати щоразу, коли скрипт звертається до буфера. Кадр QVGA у відтінках сірого — 76 800 пікселів; акселерометр на 100 Гц видає сотню тривісних зразків на секунду; мікрофон заповнює буфер із 1024 зразків кожні 64 мс. Чистий Python for цикл над будь-яким з них перетворює задачу, що мала б зайняти кілька мікросекунд, на таку, що займає десятки мілісекунд — і приблизно в десять разів довше для буфера розміром з зображення.
6.1.1. Функції бібліотеки швидші за цикли¶
Рішення — виразити операцію як один виклик функції над усім буфером, а не Python цикл по його елементах. numpy — це саме те: бібліотека масивної математики, де кожна операція є вже оптимізованою функцією, яка проходить буфер один раз від початку до кінця. np.multiply(samples, cal) множить кожен елемент samples на cal в одному виклику — та сама арифметика, що виконував цикл, але без вартості інтерпретатора на кожній ітерації. Те саме множення 1000 елементів, що займало десятки мілісекунд у Python циклі, займає десятки мікросекунд як виклик numpy.
Така угода, що пропонує numpy повсюди: сума, середнє, sin, exp, матричне множення, примітиви обробки сигналів — кожна є єдиною функцією бібліотеки, що працює над цілим буфером одразу. Умова — дані повинні зберігатися в типі масиву numpy, а операція має бути виражена над цим масивом, а не поелементно.
6.1.2. Чому список не підходить¶
Python list не може замінити. Список може містити будь-який набір об’єктів — цілі числа, числа з плаваючою крапкою, рядки, інші списки — і функція бібліотеки, що його читає, все одно повинна заглядати в кожен слот, щоб з’ясувати, що там є, і витягти значення перед будь-якою арифметикою. Ці витрати на слот — саме та вартість, яку платить Python цикл. Списки — неправильний вибір для швидкої масивної математики.
6.1.3. Чому bytearray теж недостатньо¶
bytearray має правильну форму — один типізований буфер, один байт на елемент, все в одному суцільному блоці. Саме це повертає більшість байт-орієнтованих API периферійних пристроїв. Чого йому бракує — це математики. bytearray * 2 повторює буфер, а не подвоює кожне значення, і немає розумного значення для bytearray + bytearray поелементно.
Структура даних, що поєднує типізований буфер з поелементною математикою — це ndarray. Що знаходиться всередині та як кожне поле формує поведінку швидкого шляху — це основи, на яких будується решта цього розділу.