Contenido detallado
🗺️ El plan: cámara → detección → error → motores
El robot quiere dos cosas: mantener el objetivo en el centro de la imagen y a una distancia fija. La cámara no mide la distancia, pero el tamaño aparente del objeto sirve: cerca ocupa muchos píxeles y lejos ocupa pocos. Entonces, cada cuadro se convierte en dos números de error, y cada error controla un movimiento: el error horizontal indica girar, el error de tamaño indica avanzar o retroceder.
Cómo leer: las cuatro cajas moradas se ejecutan en el Pi, una vez por cuadro; la cian es el ESP32. El énfasis está en el error porque ahí es donde la imagen se convierte en un número. Si algo falla, averigua en qué caja: la detección no encuentra el objetivo, el error tiene el signo invertido o el motor no responde.
🌱 ¿Eres nuevo aquí?
Error normalizado es el error dividido por la mitad del ancho de la imagen, para que quede entre −1 (objetivo en el borde izquierdo) y +1 (borde derecho), con 0 en el centro. Así, las ganancias del control no cambian si cambias la resolución. Ganancia es el número por el que se multiplica el error para convertirlo en velocidad.
Por qué aprender
Seguir con la cámara es el mismo ciclo sentir-pensar-actuar del módulo 1.1, solo que ahora el sensor es una imagen y el «pensar» incluye visión artificial. Este esquema (detector → error → controlador → actuador) sirve para una cámara de seguridad que sigue a alguien, un dron que sigue un objetivo y un brazo que apunta.
Conceptos clave
🔧 Montaje del Pi + ESP32 en el chasis
Parte del rover de la Trilha 1 (chasis, motores, TB6612FNG y ESP32). Agrega: el Pi 5 con enfriador activo, la Camera Module 3 en la parte delantera y una fuente de alimentación separada para el Pi. El Pi 5 necesita 5 V con hasta 5 A (la fuente oficial es de 27 W); en el robot, usa un regulador buck de 5 V y 5 A conectado a un pack 2S (dos 18650) con BMS. Los motores usan su propia batería, y todas las GND se conectan entre sí.
Cómo leer: la cámara va centrada al frente, porque el código asume que el centro de la imagen es el frente del robot. El morado alimenta el Pi; el ámbar alimenta solo el puente H. Separar las dos fuentes evita que el pico de corriente del motor reinicie el Pi. La línea discontinua es el cable UART del módulo 3.5.
Paso a paso
Prueba cada parte en la mesa de trabajo
Pi con cámara (módulo 3.2), ESP32 con el receptor (3.5) y el link_esp32.py con las ruedas girando en el aire. Ensambla solo lo que ya haya funcionado por separado.
Fíjalo dejando espacio para que circule el aire
Pi sobre separadores (una segunda placa de acrílico o MDF ayuda), con el cooler despejado. La cámara va en una pieza firme, a la altura del objetivo y ligeramente inclinada hacia abajo.
Conecta la alimentación y las señales
Buck ajustado y medido con multímetro (5,1 V) antes de conectarlo al Pi. Tres cables UART con GND común. Interruptor de corte en la batería de los motores, al alcance de la mano.
Accede de forma inalámbrica
Pi conectado al Wi-Fi y SSH del módulo 3.1. Ejecuta los programas por SSH, con el robot sobre un soporte y las ruedas en el aire, la primera vez.
✓ Hazlo
- ✓Mide la salida del buck antes de conectar el Pi.
- ✓Sujeta el cable plano de la cámara para que no se doble ni se suelte.
- ✓Apaga el Pi con sudo poweroff antes de cortar la alimentación.
✗ No lo hagas
- ✗Alimenta el Pi desde los 5 V del ESP32 o de un módulo L298N.
- ✗Conecta la batería directamente al pin de 5 V del Pi sin regulador.
- ✗No cubras el cooler: el Pi 5 reduce la velocidad cuando se calienta y bajan los FPS.
Por qué aprender
La mitad de los «bugs» de robots con Pi se deben a la alimentación: reinicios aleatorios, tarjeta corrupta, cámara que desaparece. Montar con fuentes separadas, GND común y medir antes de encender elimina estos problemas antes de que abras el código.
Conceptos clave
🔵 Seguir un objeto de color (OpenCV)
Empieza por la versión más sencilla: una pelota de color intenso (azul, por ejemplo), detectada mediante la máscara HSV y el contorno más grande, exactamente como en el módulo 3.2. A 320×240, el Pi 5 procesa decenas de cuadros por segundo, lo que permite un control fluido. Aquí el control es solo proporcional (velocidad = ganancia × error); en el tema 5 se convierte en PID.
Cómo leer: la línea discontinua es el centro de la imagen, que coincide con el frente del robot. La flecha morada representa el error: apunta hacia donde el robot debe girar. Con un error positivo, la rueda izquierda gira más que la derecha y el robot gira a la derecha, llevando la pelota de vuelta al centro.
Objetivo: el robot gira hasta centrar la pelota y avanza hasta que esta tenga el tamaño deseado. Guarda el archivo en la misma carpeta que link_esp32.py y ejecútalo en el venv del curso (creado con --system-site-packages para detectar picamera2). Ajusta HSV_MIN/HSV_MAX a tu color.
#!/usr/bin/env python3
"""Robot que sigue un objeto de color (módulo 3.6). Requiere link_esp32.py (módulo 3.5) en la misma carpeta."""
import time
import cv2
import numpy as np
from picamera2 import Picamera2
from link_esp32 import Link
LARGURA, ALTURA = 320, 240
HSV_MIN = np.array([100, 120, 70]) # rango del azul; calibra con el módulo 3.2
HSV_MAX = np.array([130, 255, 255])
AREA_MIN = 300 # menor que esto es ruido
AREA_ALVO = 6000 # área (píxeles) a la distancia deseada: mide la tuya
KP_GIRO, KP_DIST, VEL_MAX = 180, 160, 180
def acha_objeto(quadro):
hsv = cv2.cvtColor(quadro, cv2.COLOR_BGR2HSV)
mascara = cv2.inRange(hsv, HSV_MIN, HSV_MAX)
mascara = cv2.morphologyEx(mascara, cv2.MORPH_OPEN, np.ones((5, 5), np.uint8))
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)
return m["m10"] / m["m00"], area # centro x y área
def comando(cx, area):
erro_x = (cx - LARGURA / 2) / (LARGURA / 2) # -1 izquierda .. +1 derecha
erro_d = max(-1.0, min(1.0, (AREA_ALVO - area) / AREA_ALVO)) # >0 lejos, <0 cerca
giro, avanco = KP_GIRO * erro_x, KP_DIST * erro_d
esq = max(-VEL_MAX, min(VEL_MAX, avanco + giro))
drt = max(-VEL_MAX, min(VEL_MAX, avanco - giro))
return esq, drt
def main():
cam = Picamera2()
cam.configure(cam.create_video_configuration(main={"size": (LARGURA, ALTURA), "format": "RGB888"}))
cam.start()
link = Link()
t0, quadros, esq, drt = time.monotonic(), 0, 0, 0
try:
while True:
quadro = cam.capture_array() # "RGB888" de picamera2 ya viene en el orden BGR de OpenCV
achado = acha_objeto(quadro)
esq, drt = comando(*achado) if achado else (0, 0) # perdió el objetivo: detenido
link.motores(esq, drt) # cada cuadro envía un comando: mantiene activo el ESP32
quadros += 1
if time.monotonic() - t0 >= 1.0:
print(f"{quadros} FPS | objetivo={'sí' if achado else 'no'} | izq={esq:.0f} der={drt:.0f}")
t0, quadros = time.monotonic(), 0
except KeyboardInterrupt:
pass
finally:
link.fechar() # envía "S"
cam.stop()
if __name__ == "__main__":
main()
Cómo verificar (primero con las ruedas en el aire)
- ✓La terminal muestra una línea por segundo con los FPS (espera algo por encima de 20) y «objetivo=sí» cuando la pelota está al frente.
- ✓Pelota a la derecha: la rueda izquierda gira más. Pelota demasiado cerca: ambas giran hacia atrás.
- ✗¿Gira hacia el lado equivocado? Cambia los cables de un motor o invierte el signo de giro. Si la cámara está montada al revés, también se invierte.
Por qué aprender
La versión por color es rápida, económica y fácil de depurar, así que es la mejor para ajustar la mecánica, las señales y las ganancias. Cuando funciona, cambiar el detector por una red neuronal consiste en cambiar una función.
Conceptos clave
🧍 Seguir a una persona o un objeto (YOLO26)
Para seguir a una persona, un color no es suficiente. Cambia el detector por el YOLO26n exportado a NCNN (módulo 3.3). En lugar del área, usa la altura del recuadro como medida de distancia: cambia menos cuando la persona abre los brazos. El detector es más lento que la máscara de color (unos pocos cuadros por segundo en CPU), por eso las ganancias aquí son menores. Con el AI HAT+ los FPS aumentan bastante.
🌱 ¿Eres nuevo aquí?
El YOLO26 preentrenado conoce las 80 clases del conjunto COCO; la clase 0 es "person", la 32 es "sports ball". Cada detección incluye una caja (x1, y1, x2, y2), a clase e a confianza (de 0 a 1). Ignora las detecciones con baja confianza para no perseguir fantasmas.
Objetivo: seguir a la persona más grande de la imagen. Primero, exporta el modelo con el tamaño que se usa aquí: yolo export model=yolo26n.pt format=ncnn imgsz=320 (crea la carpeta yolo26n_ncnn_model).
#!/usr/bin/env python3
"""Sigue a una persona con YOLO26 (NCNN) en el Pi 5 (módulo 3.6). Requiere link_esp32.py y el modelo exportado."""
import time
from picamera2 import Picamera2
from ultralytics import YOLO
from link_esp32 import Link
LARGURA, ALTURA = 640, 480
CLASSE = 0 # 0 = "person" en COCO; 32 = "sports ball"
CONF_MIN = 0.5
ALTURA_ALVO = 0.6 # altura del recuadro ÷ altura de la imagen a la distancia deseada
KP_GIRO, KP_DIST, VEL_MAX = 140, 150, 150
modelo = YOLO("yolo26n_ncnn_model")
def maior_caixa(resultado):
melhor = None
for caixa in resultado.boxes:
if int(caixa.cls) != CLASSE or float(caixa.conf) < CONF_MIN:
continue
x1, y1, x2, y2 = caixa.xyxy[0].tolist()
if melhor is None or (y2 - y1) > (melhor[3] - melhor[1]):
melhor = (x1, y1, x2, y2)
return melhor
def main():
cam = Picamera2()
cam.configure(cam.create_video_configuration(main={"size": (LARGURA, ALTURA), "format": "RGB888"}))
cam.start()
link = Link()
try:
while True:
t = time.monotonic()
quadro = cam.capture_array()
caixa = maior_caixa(modelo.predict(quadro, imgsz=320, verbose=False)[0])
esq = drt = 0
if caixa:
x1, y1, x2, y2 = caixa
erro_x = ((x1 + x2) / 2 - LARGURA / 2) / (LARGURA / 2)
erro_d = max(-1.0, min(1.0, (ALTURA_ALVO - (y2 - y1) / ALTURA) / ALTURA_ALVO))
giro, avanco = KP_GIRO * erro_x, KP_DIST * erro_d
esq = max(-VEL_MAX, min(VEL_MAX, avanco + giro))
drt = max(-VEL_MAX, min(VEL_MAX, avanco - giro))
link.motores(esq, drt)
print(f"{(time.monotonic() - t) * 1000:.0f} ms | pessoa={'sim' if caixa else 'nao'} | esq={esq:.0f} dir={drt:.0f}")
except KeyboardInterrupt:
pass
finally:
link.fechar()
cam.stop()
if __name__ == "__main__":
main()
Cómo verificar
- ✓Cada línea muestra el tiempo del ciclo. Si supera los 300 ms, el ESP32 se detiene entre un cuadro y otro: reduce imgsz o usa el AI HAT+.
- ✓Camina despacio frente al robot: gira para mantenerte en el centro y se detiene a unos dos metros.
- ✗¿"pessoa=nao" aunque estés frente a él? Hay poca luz o luz de frente desde la ventana; prueba el modelo por sí solo como en el módulo 3.3.
Por qué aprender
Cambiar el color por una red neuronal muestra el valor de la arquitectura en capas: el enlace, el ESP32 y la lógica de las ruedas siguen iguales. Al entrenar YOLO26 con tus imágenes (módulo 3.3), el mismo programa sigue tu objeto, la mascota de la escuela u otro robot.
Conceptos clave
🎛️ PID de giro y distancia
Solo con el término P, el robot suele pasarse del punto y oscilar de un lado a otro (el retraso de la cámara lo empeora). El D frena el giro cuando el error ya está disminuyendo; el I corrige la pequeña distancia que queda por la fricción. Son dos PID independientes, como en el módulo 2.3: uno para el giro y otro para la distancia, que se suman al enviar las órdenes a las ruedas.
Cómo leer: cada error tiene su propio controlador, y las flechas cruzadas muestran que ambas ruedas reciben los dos componentes. El giro entra con signo opuesto en cada rueda; el avance entra por igual. Ajusta un PID a la vez: primero el giro con el avance en cero y luego la distancia.
Objetivo: un PID reutilizable, con límite de salida y protección contra windup (la integral deja de crecer cuando la salida se satura).
"""PID simple con límite y anti-windup (módulo 3.6)."""
import time
class PID:
def __init__(self, kp, ki=0.0, kd=0.0, limite=255.0):
self.kp, self.ki, self.kd, self.limite = kp, ki, kd, limite
self.reset()
def reset(self):
self.integral, self.erro_ant, self.t_ant = 0.0, 0.0, None
def __call__(self, erro):
agora = time.monotonic()
dt = 0.0 if self.t_ant is None else agora - self.t_ant
deriv = (erro - self.erro_ant) / dt if dt > 0 else 0.0
self.integral += erro * dt
saida = self.kp * erro + self.ki * self.integral + self.kd * deriv
if abs(saida) > self.limite: # saturou: desfaz a soma (anti-windup)
self.integral -= erro * dt
saida = max(-self.limite, min(self.limite, saida))
self.erro_ant, self.t_ant = erro, agora
return saida
En el seguidor_cor.py, agrega el import y los dos objetos después de las constantes, y reemplaza la función comando completa por esta. En main(), cuando desaparezca el objetivo, llama a pid_giro.reset() e pid_dist.reset().
from pid import PID
pid_giro = PID(kp=180, ki=0.0, kd=20, limite=150) # empieza con kd=0 y súbelo poco a poco
pid_dist = PID(kp=160, ki=30, kd=0, limite=150)
def comando(cx, area):
erro_x = (cx - LARGURA / 2) / (LARGURA / 2)
erro_d = max(-1.0, min(1.0, (AREA_ALVO - area) / AREA_ALVO))
if abs(erro_x) < 0.05: # zona muerta: no vibrar cuando el objetivo está en el centro
erro_x = 0.0
giro, avanco = pid_giro(erro_x), pid_dist(erro_d)
esq = max(-VEL_MAX, min(VEL_MAX, avanco + giro))
drt = max(-VEL_MAX, min(VEL_MAX, avanco - giro))
return esq, drt
Cómo verificar: con el robot en el suelo, empuja la pelota hacia un lado. Solo con P, se pasa del punto y regresa; con D ajustado, llega al centro con una sobrepasada pequeña como máximo. Si empieza a vibrar, kd está demasiado alto.
Por qué aprender
El mismo PID del seguidor de línea de la Trilha 2, ahora con un sensor lento y ruidoso (la cámara). Aprender a ajustar con retraso es lo que distingue a un robot que persigue suavemente de uno que baila alrededor del objetivo.
Conceptos clave
✅ Pruebas, seguridad y mejoras
Haz pruebas por etapas, de la más segura a la más real: ruedas en el aire, suelo despejado con velocidad máxima baja (VEL_MAX = 100), y solo después la velocidad final. Anota en cada ronda qué cambió y cuál fue el resultado, como en el diario de bitácora de la Trilha 2. Las causas de los problemas más comunes son conocidas:
| Síntoma | Causa probable | Qué ajustar |
|---|---|---|
| Oscila alrededor del objetivo | KP_GIRO alto, retraso de la cámara | baja kp, sube kd |
| Se aleja del objetivo | la fricción vence al PWM bajo | sube ki de la distancia |
| Se detiene y avanza a tirones | ciclo de más de 300 ms | menor resolución, NCNN, AI HAT+ |
| Persigue la cortina azul | rango HSV demasiado amplio | reduce el rango HSV, sube AREA_MIN |
| La Pi se reinicia al arrancar | alimentación compartida | fuentes separadas (tema 2) |
✓ Seguridad
- ✓Mantén el interruptor de corte de los motores siempre al alcance.
- ✓Área de prueba libre de escaleras y cables; nadie debe estar acostado en el suelo.
- ✓Prueba el timeout: Ctrl+C y desconectar el cable deben detener el robot.
✗ Evita
- ✗Seguir a niños o animales a alta velocidad.
- ✗Probarlo sobre la mesa: un error de signo y el robot se cae.
- ✗Cambiar tres ganancias a la vez: no sabrás cuál ayudó.
Mejoras (desafíos)
En vez de detenerse, gira despacio hacia el lado donde vio el objetivo por última vez, durante un máximo de 5 segundos.
Un promedio móvil del centro x (módulo 1.4) reduce la vibración causada por el ruido en la detección.
Conecta el HC-SR04 al ESP32: por debajo de 20 cm ignora la orden de avanzar, sin importar lo que decida la Pi.
Dibuja el cuadro y el error en la imagen y transmítelos al navegador, como en el módulo 3.2.
💡 Siguiente paso: ROS 2
El robot de la Trilha 3 ya tiene el diseño de un robot con ROS 2: una computadora que piensa, un microcontrolador que actúa y mensajes entre ambos. En la Trilha 4, el protocolo casero se convierte en tópicos (/cmd_vel), el ESP32 usa micro-ROS y un LiDAR agrega el mapa y la navegación.
Por qué aprender
Un proyecto termina cuando funciona de manera repetible y segura, no la primera vez que sale bien. Documentar los síntomas, las causas y los ajustes permite presentar el robot, enseñar con él y seguir mejorándolo sin empezar de nuevo.
Conceptos clave
✅ Resumen del módulo y de la ruta
Para profundizar
- Ultralytics en Raspberry Pi — YOLO26, exportación NCNN y rendimiento
- Manual de picamera2 — ajustes de video y formatos
- OpenCV: espacios de color y HSV — rastrear objetos por color
- Raspberry Pi AI HAT+ — acelerar la detección
- Documentación de ROS 2 — para la Ruta 4
Siguiente: Ruta 4 · ROS 2 y navegación
4.1 · ROS 2 sin miedo: nodos, tópicos y mensajes. Tu robot de la Ruta 3 adquiere mapa, localización y navegación de una habitación a otra.