NumPy y datos: la imagen es un array
Antes de empezar: prepara el entorno. En este módulo y en todo el curso usaremos un entorno virtual (venv) para que las librerías del proyecto no se mezclen con las del sistema.
# 1. Crea el entorno virtual en la carpeta del proyecto python3 -m venv .venv # 2. Actívalo source .venv/bin/activate # macOS / Linux .venv\Scripts\activate # Windows (PowerShell) # 3. Instala las dependencias de TODO el curso pip install numpy opencv-python pandas matplotlibCuando veas
(.venv)al inicio de tu terminal, el entorno está activo. Para salir:deactivate.
¿Qué es NumPy?
NumPy (Numerical Python) es la librería que añade a Python un tipo de dato nuevo: el ndarray (arreglo N-dimensional). Es una caja de números, todos del mismo tipo, guardados de forma compacta en memoria. Casi todo el ecosistema de IA —Pandas, OpenCV, scikit-learn, PyTorch— guarda sus datos como arrays de NumPy por debajo.
¿Cómo funciona?
A diferencia de una lista de Python (que puede mezclar tipos y guarda punteros dispersos), un ndarray guarda elementos del mismo tipo (dtype) en memoria contigua. Eso permite la vectorización: aplicar una operación a millones de números de una sola vez, sin escribir bucles for. Por dentro NumPy ejecuta ese cálculo en C, así que es muchísimo más rápido.
import numpy as np
lista = [1, 2, 3, 4]
arr = np.array(lista)
print(arr) # [1 2 3 4]
print(arr.dtype) # int64
print(arr.shape) # (4,) -> 4 elementos en 1 dimensión
# Vectorización: sumamos 10 a TODOS los elementos sin bucle
print(arr + 10) # [11 12 13 14]
print(arr * 2) # [2 4 6 8]
¿Para qué sirve? (y qué pieza del OMR construye)
En nuestro proyecto final —el calificador de exámenes con la cámara (OMR)— la hoja de respuestas escaneada es un array de NumPy. Una imagen en escala de grises es una matriz 2D donde cada número es el brillo de un píxel (0 = negro, 255 = blanco). Aquí construimos la pieza de base: aprender a tratar una imagen como números para luego recortar regiones (cada burbuja), medir cuánto están rellenas y comparar con la plantilla.
Una imagen es una matriz de píxeles
Vamos a fabricar una "mini hoja" a mano para entender la estructura, sin necesidad todavía de OpenCV.
import numpy as np
# Imagen 5x5 en escala de grises (uint8: enteros de 0 a 255)
# 255 = blanco (papel), 0 = negro (marca de lápiz)
img = np.full((5, 5), 255, dtype=np.uint8)
print(img.shape) # (5, 5) -> 5 filas, 5 columnas
print(img.dtype) # uint8
# "Rellenamos" una burbuja: ponemos a 0 un bloque central
img[1:4, 1:4] = 0
print(img)
El orden es (filas, columnas) = (alto, ancho). NumPy indexa primero por fila (eje Y) y luego por columna (eje X). Es al revés de las coordenadas (x, y) a las que estamos acostumbrados.
Recortar (slicing) y medir una región
El OMR se reduce a recortar el rectángulo de cada burbuja y preguntar: ¿cuántos píxeles oscuros tiene? Eso es contar y promediar, y NumPy lo hace en una línea.
import numpy as np
img = np.full((5, 5), 255, dtype=np.uint8)
img[1:4, 1:4] = 0 # burbuja "rellena"
# Recortamos la región central (la burbuja)
burbuja = img[1:4, 1:4] # array 3x3 de ceros
# Máscara booleana: True donde el píxel es oscuro (< 128)
oscuros = burbuja < 128
print(oscuros.sum()) # 9 píxeles oscuros
print(oscuros.mean()) # 1.0 -> 100% relleno
# Comparemos con una burbuja vacía
vacia = img[0:1, 0:1] # esquina blanca
print((vacia < 128).mean()) # 0.0 -> 0% relleno
Esa proporción de píxeles oscuros (mean() sobre la máscara) será el criterio para decidir si una burbuja está marcada. Lo guardaremos para todas las opciones (A, B, C, D) y la más oscura gana.
Cuidado con el dtype. Si tu array es uint8 y haces 200 + 100, NumPy da la vuelta (overflow) y devuelve 44, no 300. Cuando sumes o restes imágenes, convierte primero con .astype(np.int16) o np.int32.
Tech English: array = arreglo/matriz; shape = forma (dimensiones); slicing = rebanado/recorte; boolean mask = máscara booleana; vectorization = vectorización.
reshape y ejes: pensar en bloques
La hoja real tendrá una rejilla de burbujas. Conviene saber reorganizar y resumir por ejes.
import numpy as np
datos = np.arange(12) # [0 1 2 ... 11]
rejilla = datos.reshape(3, 4) # 3 filas, 4 columnas
print(rejilla)
print(rejilla.sum(axis=0)) # suma por columnas -> [12 15 18 21]
print(rejilla.sum(axis=1)) # suma por filas -> [ 6 22 38]
axis=0 recorre las filas (colapsa el eje vertical → resultado por columna). axis=1 recorre las columnas (resultado por fila). En el OMR sumaremos por filas para puntuar cada pregunta.
Ejercicios
- Crea una imagen
10x10blanca (uint8). "Rellena" dos burbujas en posiciones distintas poniéndolas a 0. Recorta cada una y calcula su porcentaje de píxeles oscuros con una máscara booleana. Imprime cuál está más rellena. - Genera
np.arange(24), conviértela en una rejilla4x6conreshape, y calcula la media de cada fila (axis=1). Explica en un comentario qué representaría cada fila si fuera una pregunta del examen.