6.1. Por qué arrays

La clase Image es la herramienta adecuada para el trabajo con píxeles porque cada uno de sus métodos opera directamente sobre el búfer de píxeles nativo de la cámara en una sola llamada rápida. La mayor parte de lo que la aplicación hace con un fotograma – umbralización, detección de manchas (blobs), detección de AprilTag, filtros de bordes – ya reside ahí.

Lo que la biblioteca de imágenes no expone es el resto del trabajo numérico con el que se encuentra una aplicación de OpenMV:

  • búferes de sensores que no son píxeles – muestras de un ADC, ejes de una IMU (unidad de medición inercial), audio de micrófono,

  • números derivados de la imagen que ningún método integrado devuelve – una columna de histograma, una mezcla personalizada de dos fotogramas, una transformación por píxel que el catálogo no cubre,

  • álgebra lineal de pequeña escala – la matriz de calibración que rectifica la lente, la rotación que fusiona la IMU,

  • matemáticas de procesamiento de señales – el contenido en frecuencia de un búfer de vibración, el suavizado aplicado a la salida de un sensor, un vector de características que un clasificador quiere como entrada.

Todo esto requiere la misma forma: un búfer de números con una operación aplicada a cada elemento. Un bucle for de Python es la manera obvia de escribirlo:

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

El bucle funciona. También es lento. Python es un lenguaje interpretado, y cada iteración de un bucle de Python conlleva el coste de ejecutar el intérprete una vez: buscar samples, leer el elemento i, multiplicar, volver a escribir, avanzar el contador del bucle, comprobar la condición del bucle. En un búfer de mil muestras de sensor esos costes del intérprete suman decenas de milisegundos para lo que es, en el fondo, una operación rápida.

Esa sobrecarga muerde cada vez que un script accede a un búfer. Un fotograma QVGA en escala de grises tiene 76.800 píxeles; un acelerómetro a 100 Hz entrega cien muestras de tres ejes por segundo; un micrófono llena un búfer de 1024 muestras cada 64 ms. Un bucle for puramente en Python sobre cualquiera de ellos convierte un trabajo que debería tardar unos pocos microsegundos en uno que tarda decenas de milisegundos – y aproximadamente diez veces más en un búfer del tamaño de una imagen.

6.1.1. Las funciones de biblioteca son más rápidas que los bucles

La solución es expresar la operación como una sola llamada a función sobre todo el búfer, en lugar de un bucle de Python sobre sus elementos. numpy es exactamente eso: una biblioteca de matemáticas de arrays donde cada operación es una sola función ya optimizada que recorre el búfer una vez de principio a fin. np.multiply(samples, cal) multiplica cada elemento de samples por cal dentro de una sola llamada – la misma aritmética que hacía el bucle, sin el coste del intérprete por iteración. La misma multiplicación de 1000 elementos que tardaba decenas de milisegundos como bucle de Python tarda decenas de microsegundos como llamada de numpy.

Este es el trato que numpy ofrece de forma general: suma, media, seno, exponencial, multiplicación de matrices, primitivas de procesamiento de señales – cada una es una sola función de biblioteca que opera sobre todo un búfer de una vez. La contrapartida es que los datos tienen que residir en el tipo array de numpy y la operación tiene que expresarse contra ese array, no contra sus elementos uno a uno.

6.1.2. Por qué una lista no sirve

Una list de Python no puede sustituirla. Una lista puede contener cualquier mezcla de objetos – enteros, floats, cadenas, otras listas – y una función de biblioteca que la lee aún tiene que mirar cada ranura para averiguar qué hay en ella y extraer el valor antes de que ocurra cualquier operación aritmética. Esa sobrecarga por ranura es exactamente el coste que paga el bucle de Python. Las listas no encajan para las matemáticas rápidas de arrays.

6.1.3. Por qué un bytearray tampoco basta

Un bytearray es la forma adecuada – un búfer tipado, un byte por elemento, todo en un único bloque contiguo. Es lo que la mayoría de las API de periféricos orientadas a bytes devuelven. Lo que le falta son las matemáticas. bytearray * 2 repite el búfer en lugar de duplicar cada valor, y no hay un significado sensato para bytearray + bytearray elemento a elemento.

La estructura de datos que combina un búfer tipado con matemáticas elemento a elemento es el ndarray. Lo que hay dentro de la caja y cómo cada campo modela el comportamiento de la ruta rápida son los cimientos sobre los que descansa el resto de este capítulo.