Contenido detallado
🖼️ Cómo ve una imagen la computadora
Para el programa, una foto no es una escena: es una tabla de números. Cada cuadrito, el píxel, guarda tres valores de 0 a 255, uno para cada color básico. Una imagen de 640 × 480 es una tabla con 480 filas, 640 columnas y 3 capas. En Python se convierte en un array de NumPy con formato (480, 640, 3). Encontrar "la pelota roja" es, en el fondo, buscar una región de la tabla donde los números tienen cierto patrón.
🌱 ¿Eres nuevo aquí?
Un canal es una de las capas de color de la imagen. OpenCV guarda los colores en el orden BGR (azul, verde, rojo), a diferencia del RGB que ves en los editores. NumPy NumPy es la biblioteca de Python para trabajar con tablas de números; OpenCV OpenCV es la biblioteca de visión computacional que trabaja sobre estas tablas. La coordenada (0, 0) es la esquina superior izquierda: x crece hacia la derecha e y crece hacia abajo.
Cómo leer: a la izquierda, un fragmento de imagen ampliado hasta que se ven los cuadritos. El píxel resaltado en morado es «rojo» porque el número del canal R es alto y los otros son bajos. Observa el orden B, G, R: es el de OpenCV. Las flechas cian muestran que y cuenta de arriba abajo.
Un solo canal, de 0 (negro) a 255 (blanco). Tres veces menos datos; ideal para bordes y formas.
Tres canales, como los entrega la cámara. Bueno para mostrar y guardar, pero malo para separar colores cuando cambia la luz.
Matiz, saturación y brillo. Separa «qué color» de «cuánta luz»: el formato adecuado para encontrar objetos de color (tema 3).
Por qué aprender
Cuando la máscara no detecta el objeto o el centro aparece en el lugar equivocado, casi siempre el problema está aquí: colores intercambiados (RGB tratado como BGR), eje y invertido o una imagen demasiado grande para que la procese el Pi a tiempo.
Conceptos clave
📷 Camera Module 3 con picamera2
A Camera Module 3 se conecta al conector de cámara del Pi 5 con un cable flat (cinta). En la terminal, las herramientas rpicam-apps (rpicam-hello, rpicam-still) prueban la cámara en segundos. En tu código Python, quien entrega las imágenes es picamera2, que ya viene con Raspberry Pi OS. Los tutoriales antiguos usan herramientas de línea de comandos que ya no existen en el sistema actual; si el comando no empieza con rpicam-, desconfía.
🌱 ¿Eres nuevo aquí?
El conector del Pi 5 es más pequeño (22 vías) que el de las cámaras (15 vías): usa el cable específico para Pi 5, que tiene un extremo de cada tamaño. El lado con los contactos metálicos debe seguir la orientación indicada en el conector; abre el seguro con la uña, insértalo recto y ciérralo. Hazlo con el Pi apagado.
Cómo leer: de izquierda a derecha, la luz se convierte en números. El ISP (procesador de imagen) corrige el color y la exposición antes de que recibas el cuadro; por eso la primera imagen justo después de encender la cámara puede salir oscura, y el código espera un segundo antes de capturarla.
Objetivo: confirmar que se detectó la cámara, tomar una foto desde la terminal y luego capturar un cuadro en Python. Instala OpenCV del sistema una vez (el venv del módulo 3.1 lo puede ver).
rpicam-hello --list-cameras # debe mostrar la cámara (imx708 = Camera Module 3)
rpicam-still -o ~/robo/teste.jpg # toma una foto desde la terminal
sudo apt install -y python3-opencv # OpenCV del sistema, visible desde el venv
source ~/robo-venv/bin/activate
# Captura un cuadro de la cámara y lo guarda como JPEG
import time, os
import cv2
from picamera2 import Picamera2
picam2 = Picamera2()
config = picam2.create_preview_configuration(main={"size": (640, 480), "format": "RGB888"})
picam2.configure(config)
picam2.start()
time.sleep(1) # deja que se ajuste la exposición automática
quadro = picam2.capture_array() # array NumPy en el orden BGR
print("formato:", quadro.shape, quadro.dtype)
print("píxel del centro (B, G, R):", quadro[240, 320])
cv2.imwrite(os.path.expanduser("~/robo/quadro.jpg"), quadro)
picam2.stop()
print("guardado en ~/robo/quadro.jpg")
Cómo verificar
- ✓La terminal muestra formato: (480, 640, 3) uint8 y tres números del píxel central.
- ✓Copia la foto a tu computadora (scp <seu_usuario>@robo.local:~/robo/quadro.jpg .) y ábrela: los colores deben verse naturales.
- ✗«no cameras available»? Apaga el Pi, revisa el cable (orientación y seguro) en ambos extremos y vuelve a encenderlo.
Por qué aprender
Este bloque de configuración (tamaño, formato, start, capture_array) es la puerta de entrada a todo lo que viene después: máscara de color, YOLO26 y el proyecto de la ruta. Probar primero con rpicam-hello permite separar un problema de hardware de uno de código.
Conceptos clave
🎨 OpenCV: colores, HSV y máscaras
Una pelota roja a la sombra y la misma pelota al sol tienen valores BGR muy diferentes. En el espacio HSV la diferencia se concentra casi toda en V (brillo), y H (matiz, «qué color») sigue siendo parecido. Por eso el procedimiento habitual es: convertir el cuadro a HSV, elegir un rango de H, S y V y crear una máscara, una imagen en blanco y negro en la que solo los píxeles dentro del rango quedan en blanco.
🌱 ¿Eres nuevo aquí?
Matiz (H) es el color en el círculo cromático; en OpenCV va de 0 a 179. Saturación (S) indica qué tan vivo es el color (0 = gris). Brillo (V) indica cuánta luz hay (0 = negro). Una máscara es un filtro: blanco donde se cumple la condición, negro en el resto. El rojo está en los dos extremos del círculo (cerca de 0 y de 179), por eso se necesitan dos rangos sumados.
Cómo leer: el rango es el círculo cromático abierto en línea recta, en la escala 0–179 de OpenCV. Los marcos morados muestran por qué cuesta trabajar con el rojo: empieza y termina la escala. El marco cian muestra un color «más sencillo», que cabe en una sola ventana.
Objetivo: generar la máscara de un objeto rojo y guardar el cuadro y la máscara uno al lado del otro para que ajustes el rango mirando el resultado.
# Máscara de color en HSV (rojo = dos rangos)
import time, os
import cv2, numpy as np
from picamera2 import Picamera2
picam2 = Picamera2()
picam2.configure(picam2.create_preview_configuration(main={"size": (640, 480), "format": "RGB888"}))
picam2.start(); time.sleep(1)
quadro = picam2.capture_array()
picam2.stop()
hsv = cv2.cvtColor(quadro, cv2.COLOR_BGR2HSV)
baixo1, alto1 = np.array([0, 120, 70]), np.array([10, 255, 255]) # rojo, principio de la escala
baixo2, alto2 = np.array([170, 120, 70]), np.array([179, 255, 255]) # rojo, final de la escala
mascara = cv2.inRange(hsv, baixo1, alto1) | cv2.inRange(hsv, baixo2, alto2)
# Limpieza: elimina puntos sueltos y cierra huecos
nucleo = np.ones((5, 5), np.uint8)
mascara = cv2.morphologyEx(mascara, cv2.MORPH_OPEN, nucleo)
mascara = cv2.morphologyEx(mascara, cv2.MORPH_CLOSE, nucleo)
brancos = cv2.countNonZero(mascara)
print(f"píxeles en la máscara: {brancos} ({100 * brancos / mascara.size:.1f}% de la imagen)")
lado_a_lado = np.hstack([quadro, cv2.cvtColor(mascara, cv2.COLOR_GRAY2BGR)])
cv2.imwrite(os.path.expanduser("~/robo/mascara.jpg"), lado_a_lado)
print("guardado en ~/robo/mascara.jpg")
Cómo verificar
- ✓Con un objeto rojo enfrente, la mitad derecha de mascara.jpg muestra una mancha blanca con su forma.
- ✓Sin el objeto, el porcentaje baja hasta cerca del 0%.
- ✗¿Aparecen piel y madera? Sube el mínimo de S (120 → 150). ¿El objeto desaparece en la sombra? Baja el mínimo de V.
✓ Ayuda a que funcione el color
- ✓Objeto de color vivo y mate (pelota, cartulina, cinta).
- ✓Fondo de un color muy distinto al del objeto.
- ✓Ajusta el rango en el lugar donde va a funcionar el robot.
✗ Dificulta
- ✗Objeto brillante que refleja la lámpara (se vuelve blanco).
- ✗Luz amarillenta o intermitente, que cambia el matiz.
- ✗Calibra de día y prueba de noche.
Por qué aprender
La máscara de color es la forma más ligera de visión: funciona a decenas de cuadros por segundo en la Pi, sin IA, y es suficiente para que un robot siga una pelota o una franja de color. Es la primera versión del proyecto 3.6, antes de incorporar YOLO26.
Conceptos clave
🎯 Contornos y centro del objeto
La máscara indica «aquí hay rojo», pero el robot necesita dos números: dónde está el objeto y qué tan cerca. OpenCV encuentra los contornos (los bordes de cada mancha blanca); te quedas con el más grande y calculas su centro y su área. El centro se convierte en el error horizontal (qué tan a la izquierda o a la derecha del centro de la imagen está el objeto), y el área indica la distancia: cuanto mayor sea, más cerca está.
Cómo leer: el rectángulo es la máscara; la línea discontinua es el centro de la imagen. De las dos manchas, solo cuenta la más grande (la pequeña es ruido). La flecha cian indica el error: es positivo cuando el objeto está a la derecha, negativo cuando está a la izquierda y cero cuando está centrado. Ese es el número que el PID del proyecto 3.6 llevará a cero.
Objetivo: ejecutar en un bucle continuo, imprimir el centro, el error y el área del objeto rojo más grande, e indicar hacia qué lado debería girar el robot.
# Encuentra el objeto rojo más grande y calcula el centro, el error y el área
import time
import cv2, numpy as np
from picamera2 import Picamera2
LARGURA, ALTURA = 640, 480
AREA_MIN = 800 # menor que esto es ruido
FAIXAS = [((0, 120, 70), (10, 255, 255)), ((170, 120, 70), (179, 255, 255))]
nucleo = np.ones((5, 5), np.uint8)
def achar_objeto(quadro):
hsv = cv2.cvtColor(quadro, cv2.COLOR_BGR2HSV)
mascara = np.zeros(hsv.shape[:2], np.uint8)
for baixo, alto in FAIXAS: # combina los rangos (OR)
mascara |= cv2.inRange(hsv, np.array(baixo), np.array(alto))
mascara = cv2.morphologyEx(mascara, cv2.MORPH_OPEN, nucleo)
contornos, _ = cv2.findContours(mascara, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contornos:
return None
maior = max(contornos, key=cv2.contourArea)
area = cv2.contourArea(maior)
if area < AREA_MIN:
return None
m = cv2.moments(maior)
cx, cy = int(m["m10"] / m["m00"]), int(m["m01"] / m["m00"])
return cx, cy, area
picam2 = Picamera2()
picam2.configure(picam2.create_preview_configuration(main={"size": (LARGURA, ALTURA), "format": "RGB888"}))
picam2.start(); time.sleep(1)
print("Muestra un objeto rojo. Ctrl+C para salir.")
try:
while True:
achado = achar_objeto(picam2.capture_array())
if achado is None:
print("ningún objeto → detenerse y buscar")
else:
cx, cy, area = achado
erro = cx - LARGURA // 2
lado = "centro" if abs(erro) < 40 else ("direita" if erro > 0 else "esquerda")
print(f"cx={cx:3d} cy={cy:3d} error={erro:+4d} área={area:7.0f} → {lado}")
time.sleep(0.1)
except KeyboardInterrupt:
picam2.stop()
Cómo verificar
- ✓Al mover el objeto hacia la derecha de la cámara, el error se vuelve positivo y aparece «→ derecha».
- ✓Al acercar el objeto, el área aumenta bastante; al alejarlo, disminuye.
- ✗¿Derecha e izquierda están invertidas? La cámara está al revés o en modo espejo: invierte el signo del error o gira la cámara.
💡 El área no es una distancia en metros
El área sirve para comparar («más cerca», «demasiado lejos»), no para medir. Anota el área con el objeto a 30 cm y a 1 m: esos dos números serán los límites de «llegó» y «acércate» en el proyecto.
Por qué aprender
El centro y el área son el puente entre la visión y el movimiento. YOLO26, en el siguiente módulo, entrega un cuadro en lugar de un contorno, pero calcularás exactamente el mismo error y el mismo «tamaño» a partir de él.
Conceptos clave
⏱️ Medir los FPS y reducir la resolución
Un robot que ve a 3 cuadros por segundo reacciona tarde: si avanza a medio metro por segundo, recorre más de 15 cm entre una imagen y otra. El número que importa es el FPS del bucle completo (capturar + procesar + decidir), no el de la cámara. La medida más efectiva es la resolución: la mitad del ancho y de la altura equivale a un cuarto de los píxeles, y casi todo el procesamiento se reduce en la misma proporción.
🌱 ¿Eres nuevo aquí?
FPS (cuadros por segundo) cuenta cuántas vueltas completas da tu bucle en un segundo. Latencia es el retraso entre lo que ocurre frente a la cámara y la reacción del robot. Ambos van de la mano: pocos FPS casi siempre significan una reacción tardía.
Cómo leer: cada barra representa el trabajo de un cuadro. Pasar de Full HD a 640 × 480 reduce el trabajo casi siete veces. Para seguir una pelota de color, basta con 320 × 240; para YOLO26, 640 × 480 es el tamaño natural.
Objetivo: medir los FPS reales del bucle «capturar + HSV + máscara» en tres resoluciones y elegir la tuya con datos.
# Mide los FPS del bucle completo en tres resoluciones (5 s cada una)
import time
import cv2
from picamera2 import Picamera2
picam2 = Picamera2()
for largura, altura in [(1280, 720), (640, 480), (320, 240)]:
cfg = picam2.create_video_configuration(main={"size": (largura, altura), "format": "RGB888"})
picam2.configure(cfg)
picam2.start(); time.sleep(1)
n, t0 = 0, time.perf_counter()
while time.perf_counter() - t0 < 5:
quadro = picam2.capture_array()
hsv = cv2.cvtColor(quadro, cv2.COLOR_BGR2HSV) # trabajo típico
mascara = cv2.inRange(hsv, (0, 120, 70), (10, 255, 255))
n += 1
fps = n / (time.perf_counter() - t0)
print(f"{largura}x{altura}: {fps:5.1f} FPS ({1000 / fps:5.1f} ms por vuelta)")
picam2.stop()
Cómo verificar
- ✓Aparecen tres líneas, y los FPS aumentan a medida que baja la resolución (el límite es la tasa de la propia cámara).
- ✓Anota los números: te servirán de referencia para compararlos con YOLO26 en el 3.3.
- ✗¿Los FPS disminuyen con el tiempo? Ejecuta saude.py del 3.1: probablemente se deba al calor.
✓ Aumenta los FPS
- ✓Pedirle a la cámara la resolución final (sin cambiar el tamaño después).
- ✓Procesar solo la franja de la imagen que te interesa (por ejemplo, la mitad inferior).
- ✓Imprimir menos: print cada cuadro también cuesta.
✗ Reduce los FPS
- ✗Guardar un JPEG en la tarjeta en cada cuadro.
- ✗Abrir el escritorio con una ventana de video mientras funciona el robot.
- ✗Una Pi sin disipador: empieza rápido y se vuelve lenta.
Por qué aprender
Sin medir, toda optimización es una suposición. Con los FPS a la vista, decides con datos entre «color rápido» e «IA más lenta» y sabes cuánto puede avanzar el robot entre dos decisiones sin perder el objeto.
Conceptos clave
🌐 Transmitir el video al navegador
La Pi del robot no tiene pantalla, y las ventanas de video por SSH no funcionan. La solución es que la Pi se convierta en un pequeño servidor web: comprime cada cuadro en JPEG y envía la secuencia por la red en el formato MJPEGque cualquier navegador muestra como video. Así ves, en el celular o en la laptop, exactamente lo que ve el robot, ya con las marcas de tu código.
🌱 ¿Eres nuevo aquí?
MJPEG es «JPEG en movimiento»: una foto detrás de otra, en una conexión que permanece abierta. Es simple y tiene poco retraso, pero consume más red que un video realmente comprimido. La puerto (aquí, 8000) es el número que diferencia los servicios dentro de la misma computadora; por eso la dirección termina en :8000.
Cómo leer: dentro del Pi (línea discontinua), dos tareas se ejecutan a la vez: una solo captura y prepara el JPEG más reciente; la otra solo lo entrega. Separarlas evita que un navegador lento retrase la visión del robot.
Objetivo: ver el video del robot en el navegador, con el centro del objeto rojo marcado. Usa solo la biblioteca estándar de Python, sin instalar nada.
# Video de la cámara en el navegador (MJPEG) con el centro del objeto marcado
import threading, time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import cv2, numpy as np
from picamera2 import Picamera2
picam2 = Picamera2()
picam2.configure(picam2.create_video_configuration(main={"size": (640, 480), "format": "RGB888"}))
picam2.start()
ultimo_jpeg, trava = None, threading.Lock()
def capturar():
global ultimo_jpeg
while True:
quadro = picam2.capture_array()
hsv = cv2.cvtColor(quadro, cv2.COLOR_BGR2HSV)
mascara = cv2.inRange(hsv, (0, 120, 70), (10, 255, 255)) | cv2.inRange(hsv, (170, 120, 70), (179, 255, 255))
contornos, _ = cv2.findContours(mascara, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contornos:
maior = max(contornos, key=cv2.contourArea)
m = cv2.moments(maior)
if m["m00"] > 800:
cx, cy = int(m["m10"] / m["m00"]), int(m["m01"] / m["m00"])
cv2.drawContours(quadro, [maior], -1, (255, 0, 255), 2)
cv2.circle(quadro, (cx, cy), 6, (255, 255, 0), -1)
cv2.line(quadro, (320, 0), (320, 480), (200, 200, 200), 1) # centro de la imagen
ok, buf = cv2.imencode(".jpg", quadro, [cv2.IMWRITE_JPEG_QUALITY, 70])
if ok:
with trava:
ultimo_jpeg = buf.tobytes()
PAGINA = b'<html><body style="margin:0;background:#111"><img src="/video" style="width:100%"></body></html>'
class Servidor(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/":
self.send_response(200); self.send_header("Content-Type", "text/html"); self.end_headers()
self.wfile.write(PAGINA)
elif self.path == "/video":
self.send_response(200)
self.send_header("Content-Type", "multipart/x-mixed-replace; boundary=QUADRO")
self.end_headers()
try:
while True:
with trava:
jpg = ultimo_jpeg
if jpg:
self.wfile.write(b"--QUADRO\r\nContent-Type: image/jpeg\r\n\r\n" + jpg + b"\r\n")
time.sleep(0.05) # hasta ~20 cuadros por segundo
except (BrokenPipeError, ConnectionResetError):
pass # el navegador cerró la pestaña
else:
self.send_error(404)
def log_message(self, *args):
pass # silencia el registro de cada solicitud
threading.Thread(target=capturar, daemon=True).start()
print("Abre http://robo.local:8000 en el navegador. Ctrl+C para salir.")
ThreadingHTTPServer(("0.0.0.0", 8000), Servidor).serve_forever()
Cómo verificar
- ✓En el celular (con el mismo Wi-Fi), http://robo.local:8000 muestra el video con una línea vertical en el centro.
- ✓Un objeto rojo queda marcado con un contorno magenta y un punto cian en el centro.
- ✗¿El celular no abre robo.local? Usa la IP del Pi (hostname -I) en lugar del nombre.
💡 Solo en la red local
Este servidor no tiene contraseña: cualquier persona en la misma red puede ver la cámara. Úsalo en casa o en el laboratorio, nunca expuesto a Internet. Si estás en la red de una escuela, consulta con el profesor antes de activarlo.
Por qué aprender
Ver lo que ve el robot es la herramienta de depuración más poderosa de la ruta. Cuando gire hacia el lado equivocado en el proyecto 3.6, mirarás el video y sabrás en segundos si el problema está en la visión (marcó el objeto equivocado) o en el control (lo marcó bien, pero giró mal).
Conceptos clave
✅ Resumen del módulo
Para profundizar
- Software de cámara de Raspberry Pi — rpicam-apps y picamera2
- Cámaras de Raspberry Pi — modelos, cables e instalación
- OpenCV: espacios de color — HSV e inRange paso a paso
- OpenCV: contornos — momentos, área y cajas
- Documentación de OpenCV — referencia completa
Siguiente módulo
3.3 · Detectar objetos con YOLO26: de «encuentra el rojo» a «encuentra a la persona, el gato o la botella», ejecutándose en el Pi 5.