6.1. Зачем нужны массивы

Класс Image является правильным инструментом для работы с пикселями, потому что каждый его метод работает напрямую с нативным буфером пикселей камеры в одном быстром вызове. Большая часть того, что приложение делает с кадром – пороговая обработка, поиск блобов, обнаружение AprilTag, фильтры границ – уже находится там.

Что библиотека изображений не предоставляет, так это остальную числовую работу, с которой сталкивается приложение OpenMV:

  • буферы датчиков, которые не являются пикселями – отсчёты ADC, оси с IMU (инерциального измерительного модуля), аудио с микрофона,

  • производные числа из изображения, которые не возвращает ни один встроенный метод – столбец гистограммы, пользовательское смешение двух кадров, попиксельное преобразование, которое каталог не охватывает,

  • немного линейной алгебры – калибровочная матрица, исправляющая объектив, поворот, объединяющий IMU,

  • математика обработки сигналов – частотное содержание буфера вибрации, сглаживание, применяемое к выходу датчика, вектор признаков, который классификатор хочет получить на вход.

Всё это требует одной и той же формы: буфер чисел с одной операцией, применённой к каждому элементу. Цикл for в Python – очевидный способ написать это:

for i in range(len(samples)):
    samples[i] = samples[i] * cal

Цикл работает. Он также медленный. Python – интерпретируемый язык, и каждая итерация цикла 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 предлагает повсеместно: sum, mean, sin, exp, умножение матриц, примитивы обработки сигналов – каждый из них является одной функцией библиотеки, которая работает со всем буфером сразу. Компромисс в том, что данные должны жить в типе массива numpy и операция должна быть выражена для этого массива, а не для его элементов по одному за раз.

6.1.2. Почему список не подойдёт

Python list не может его заменить. Список может хранить любую смесь объектов – целые числа, числа с плавающей точкой, строки, другие списки – и функция библиотеки, читающая его, всё равно должна посмотреть в каждый слот, чтобы узнать, что в нём, и извлечь значение, прежде чем произойдёт какая-либо арифметика. Эти накладные расходы на каждый слот – ровно та стоимость, которую платит цикл Python. Списки плохо подходят для быстрой математики массивов.

6.1.3. Почему bytearray тоже недостаточно

bytearray имеет правильную форму – один типизированный буфер, один байт на элемент, всё в одном непрерывном блоке. Это то, что возвращает большинство байт-ориентированных API периферийных устройств. Чего ему не хватает, так это математики. bytearray * 2 повторяет буфер, а не удваивает каждое значение, и нет разумного смысла для bytearray + bytearray поэлементно.

Структура данных, которая сочетает типизированный буфер с поэлементной математикой, – это ndarray. Что находится внутри коробки и как каждое поле формирует поведение быстрого пути – это основы, на которых покоится остальная часть этой главы.