PTENES
MÓDULO 3.6 · PROYECTO

🎯 Proyecto: robot que sigue un objeto

Todo lo de la trilha en un solo robot: la cámara del Pi 5 encuentra el objetivo (una pelota de color con OpenCV o una persona con YOLO26), el programa transforma la posición en error, o PID transforma el error en la velocidad de cada rueda, y el ESP32 del módulo 3.5 lo ejecuta con parada segura.

6
Temas
Avanzado
Nivel
Proyecto
Tipo
Rover + Pi 5
Hardware
0%
0 de 0 temas leídos · Sección 1 de 6

Contenido detallado

1

🗺️ 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ámaracuadro deteccióncentro, tamaño errorgiro, distancia PIDizq., der. ESP32motores Raspberry Pi 5UART el robot se mueve, el objetivo cambia de lugar en la imagen y todo vuelve a empezar

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

Error de giro
centro del objetivo − centro
Error de distancia
tamaño objetivo − actual
Normalizar
error entre −1 y 1
Diferencial
gira con ruedas a distintas velocidades
2

🔧 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ámara Pi 5+ cooler ESP32 TB6612FNG batería 2S+ buck 5 V bat. motores frente ↑ ruedas UARTlínea discontinua

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

1

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.

2

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.

3

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.

4

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

Buck 5 V / 5 A
alimentación del Pi
Fuentes separadas
el motor no reinicia el Pi
Cámara centrada
centro = frente
Interruptor de corte
parada física
3

🔵 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.

centro x = 160 cx = 232 error de giro (232 − 160) ÷ 160 = +0,45 objetivo a la derecha → girar a la derecha ruedas izq = avance + giro der = avance − giro área pequeña → lejos → avanza

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.

seguidor_cor.py
#!/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

Máscara HSV
separa el color
Momentos
centro del contorno
Control P
ganancia × error
AREA_ALVO
distancia deseada
4

🧍 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).

seguidor_yolo.py
#!/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

Clase COCO
0 = person
Confianza
umbral de 0,5
Altura del cuadro
medida de distancia
Tiempo del ciclo
menos de 300 ms
5

🎛️ 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.

error de giro error de dist. PID giroP + D PID dist.P + I izq = avance + giro der = avance − giro suma

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.py
"""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().

seguidor_cor.py (fragmento que cambia)
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

Dos PID
giro y distancia
Anti-windup
la integral no se dispara
Zona muerta
ignora errores pequeños
reset()
al perder el objetivo
6

✅ 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íntomaCausa probableQué ajustar
Oscila alrededor del objetivoKP_GIRO alto, retraso de la cámarabaja kp, sube kd
Se aleja del objetivola fricción vence al PWM bajosube ki de la distancia
Se detiene y avanza a tironesciclo de más de 300 msmenor resolución, NCNN, AI HAT+
Persigue la cortina azulrango HSV demasiado amplioreduce el rango HSV, sube AREA_MIN
La Pi se reinicia al arrancaralimentación compartidafuentes 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)

🔄 Buscar cuando pierde el objetivo

En vez de detenerse, gira despacio hacia el lado donde vio el objetivo por última vez, durante un máximo de 5 segundos.

📉 Filtrar la posición

Un promedio móvil del centro x (módulo 1.4) reduce la vibración causada por el ruido en la detección.

🛑 Freno por proximidad

Conecta el HC-SR04 al ESP32: por debajo de 20 cm ignora la orden de avanzar, sin importar lo que decida la Pi.

📺 Ver lo que ve el robot

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

Pruebas por etapas
aire, suelo lento, velocidad final
Un ajuste a la vez
y anotar
Bloqueo local
el ESP32 tiene prioridad sobre la Pi
/cmd_vel
lo mismo, con ROS 2

✅ Resumen del módulo y de la ruta

✓
Ciclo cámara → detección → error → PID → ESP32, que se ejecuta en cada cuadro.
✓
Montaje con fuentes separadas: buck 5 V/5 A para la Pi, batería propia para los motores, GND común, interruptor de corte.
✓
Dos detectores, el mismo control: color con OpenCV para ajustar la mecánica, YOLO26 en NCNN para personas.
✓
Dos PID: giro (P+D) y distancia (P+I), con anti-windup y zona muerta.
✓
Ruta 3 completa: Pi 5 y Linux, cámara, YOLO26, TinyML en ESP32-S3, enlace Pi↔ESP32 y un robot que sigue.

Para profundizar

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.