Conteúdo detalhado
🗺️ O plano: câmera → detecção → erro → motores
O robô quer duas coisas: manter o alvo no centro da imagem e a uma distância fixa. A câmera não mede distância, mas o tamanho aparente do objeto serve: perto ele ocupa muitos pixels, longe ocupa poucos. Então cada quadro vira dois números de erro, e cada erro controla um movimento: o erro horizontal manda girar, o erro de tamanho manda avançar ou recuar.
Como ler: as quatro caixas roxas rodam no Pi, uma vez por quadro; a ciano é o ESP32. O destaque está no erro porque é ali que a imagem vira número. Se algo falha, descubra em qual caixa: a detecção não acha o alvo, o erro sai com sinal trocado ou o motor não obedece.
🌱 Novo aqui?
Erro normalizado é o erro dividido pela metade da largura da imagem, para ficar entre −1 (alvo na borda esquerda) e +1 (borda direita), com 0 no centro. Assim os ganhos do controle não mudam se você trocar a resolução. Ganho é o número que multiplica o erro para virar velocidade.
Por que aprender
Seguir com a câmera é o mesmo laço sentir-pensar-agir do módulo 1.1, só que o sensor agora é uma imagem e o "pensar" inclui visão computacional. Esse desenho (detector → erro → controlador → atuador) serve para câmera de segurança que acompanha, drone que segue e braço que mira.
Conceitos-chave
🔧 Montagem do Pi + ESP32 no chassi
Parta do rover da Trilha 1 (chassi, motores, TB6612FNG e ESP32). O que entra: o Pi 5 com cooler ativo, a Camera Module 3 na frente e uma alimentação separada para o Pi. O Pi 5 pede 5 V com até 5 A (a fonte oficial é de 27 W); no robô, use um regulador buck de 5 V e 5 A ligado a um pack 2S (duas 18650) com BMS. Os motores ficam na bateria deles, e todos os GND se encontram.
Como ler: a câmera vai centralizada na frente, porque o código assume que o centro da imagem é a frente do robô. O roxo alimenta o Pi; o âmbar alimenta só a ponte H. Separar as duas fontes impede que o pico de corrente do motor reinicie o Pi. O tracejado é o cabo UART do módulo 3.5.
Passo a passo
Teste cada parte na bancada
Pi com câmera (módulo 3.2), ESP32 com o receptor (3.5) e o link_esp32.py girando as rodas no ar. Só monte o que já funcionou solto.
Fixe com folga para o ar
Pi sobre espaçadores (uma segunda placa de acrílico ou MDF ajuda), cooler livre. A câmera numa peça firme, na altura do alvo e levemente inclinada para baixo.
Ligue energia e sinais
Buck ajustado e medido com multímetro (5,1 V) antes de ligar no Pi. Três fios de UART com GND comum. Chave de corte na bateria dos motores, ao alcance da mão.
Acesse sem fio
Pi no Wi-Fi e SSH do módulo 3.1. Rode os programas pelo SSH, com o robô sobre um apoio, rodas no ar, na primeira vez.
✓ Faça
- ✓Medir a saída do buck antes de conectar o Pi.
- ✓Prender o cabo flat da câmera para não dobrar nem soltar.
- ✓Desligar o Pi com sudo poweroff antes de cortar a energia.
✗ Não faça
- ✗Alimentar o Pi pelo 5 V do ESP32 ou de um módulo L298N.
- ✗Ligar a bateria direto no pino de 5 V do Pi sem regulador.
- ✗Tampar o cooler: o Pi 5 reduz a velocidade quando esquenta e o FPS cai.
Por que aprender
Metade dos "bugs" de robô com Pi são de energia: reinício aleatório, cartão corrompido, câmera que some. Montar com fontes separadas, GND comum e medição antes de ligar elimina esses fantasmas antes de você abrir o código.
Conceitos-chave
🔵 Seguir um objeto colorido (OpenCV)
Comece pela versão mais leve: uma bola de cor forte (azul, por exemplo), achada pela máscara HSV e pelo maior contorno, exatamente como no módulo 3.2. Em 320×240 o Pi 5 processa dezenas de quadros por segundo, o que deixa o controle suave. O controle aqui é só proporcional (velocidade = ganho × erro); no tópico 5 ele vira PID.
Como ler: a linha tracejada é o centro da imagem, que coincide com a frente do robô. A seta roxa é o erro: ela aponta para onde o robô precisa virar. Com erro positivo, a roda esquerda gira mais que a direita e o robô vira para a direita, trazendo a bola de volta ao centro.
Objetivo: o robô gira até centralizar a bola e anda até ela ficar do tamanho desejado. Salve na mesma pasta do link_esp32.py e rode no venv do curso (criado com --system-site-packages para enxergar o picamera2). Ajuste HSV_MIN/HSV_MAX para a sua cor.
#!/usr/bin/env python3
"""Robô que segue um objeto colorido (módulo 3.6). Requer link_esp32.py (módulo 3.5) na mesma pasta."""
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]) # faixa do azul; calibre com o módulo 3.2
HSV_MAX = np.array([130, 255, 255])
AREA_MIN = 300 # menor que isso é ruído
AREA_ALVO = 6000 # área (pixels) na distância desejada: meça a sua
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 e área
def comando(cx, area):
erro_x = (cx - LARGURA / 2) / (LARGURA / 2) # -1 esquerda .. +1 direita
erro_d = max(-1.0, min(1.0, (AREA_ALVO - area) / AREA_ALVO)) # >0 longe, <0 perto
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" do picamera2 já vem na ordem BGR do OpenCV
achado = acha_objeto(quadro)
esq, drt = comando(*achado) if achado else (0, 0) # perdeu o alvo: parado
link.motores(esq, drt) # todo quadro manda um comando: mantém o ESP32 vivo
quadros += 1
if time.monotonic() - t0 >= 1.0:
print(f"{quadros} FPS | alvo={'sim' if achado else 'nao'} | esq={esq:.0f} dir={drt:.0f}")
t0, quadros = time.monotonic(), 0
except KeyboardInterrupt:
pass
finally:
link.fechar() # manda "S"
cam.stop()
if __name__ == "__main__":
main()
Como verificar (rodas no ar primeiro)
- ✓O terminal mostra uma linha por segundo com o FPS (espere algo acima de 20) e "alvo=sim" com a bola na frente.
- ✓Bola à direita: a roda esquerda gira mais. Bola perto demais: as duas giram para trás.
- ✗Vira para o lado errado? Troque os fios de um motor ou inverta o sinal de giro. Câmera montada de cabeça para baixo também inverte.
Por que aprender
A versão por cor é rápida, barata e fácil de depurar, então é a melhor para acertar a mecânica, os sinais e os ganhos. Quando ela funciona, trocar o detector por uma rede neural é mudar uma função.
Conceitos-chave
🧍 Seguir uma pessoa ou objeto (YOLO26)
Para seguir uma pessoa, uma cor não basta. Troque o detector pelo YOLO26n exportado para NCNN (módulo 3.3). Em vez de área, use a altura da caixa como medida de distância: ela muda menos quando a pessoa abre os braços. O detector é mais lento que a máscara de cor (alguns quadros por segundo em CPU), por isso os ganhos aqui são menores. Com o AI HAT+ o FPS sobe bastante.
🌱 Novo aqui?
O YOLO26 pré-treinado conhece as 80 classes do conjunto COCO; a classe 0 é "person", a 32 é "sports ball". Cada detecção vem com uma caixa (x1, y1, x2, y2), a classe e a confiança (0 a 1). Ignore detecções com confiança baixa para não perseguir fantasmas.
Objetivo: seguir a maior pessoa da imagem. Antes, exporte o modelo no tamanho usado aqui: yolo export model=yolo26n.pt format=ncnn imgsz=320 (cria a pasta yolo26n_ncnn_model).
#!/usr/bin/env python3
"""Segue uma pessoa com YOLO26 (NCNN) no Pi 5 (módulo 3.6). Requer link_esp32.py e o 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" no COCO; 32 = "sports ball"
CONF_MIN = 0.5
ALTURA_ALVO = 0.6 # altura da caixa ÷ altura da imagem na distância desejada
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()
Como verificar
- ✓Cada linha mostra o tempo do ciclo. Se passar de 300 ms, o ESP32 para entre um quadro e outro: reduza imgsz ou use o AI HAT+.
- ✓Ande devagar na frente do robô: ele gira para manter você no centro e para a uns dois metros.
- ✗"pessoa=nao" com você na frente? Luz fraca ou contra a janela; teste o modelo sozinho como no módulo 3.3.
Por que aprender
Trocar a cor por uma rede neural mostra o valor da arquitetura em camadas: o link, o ESP32 e a lógica de rodas ficaram iguais. Treinando o YOLO26 com as suas imagens (módulo 3.3), o mesmo programa segue o seu objeto, o mascote da escola ou outro robô.
Conceitos-chave
🎛️ PID de giro e de distância
Só com o termo P, o robô costuma passar do ponto e balançar de um lado para o outro (o atraso da câmera piora isso). O D freia a virada quando o erro já está diminuindo; o I corrige a pequena distância que sobra por atrito. São dois PIDs independentes, como no módulo 2.3: um para o giro, outro para a distância, somados na hora de mandar as rodas.
Como ler: cada erro tem o seu controlador, e as setas cruzadas mostram que as duas rodas recebem as duas parcelas. O giro entra com sinal oposto em cada roda; o avanço entra igual. Ajuste um PID de cada vez: primeiro o giro com o avanço zerado, depois a distância.
Objetivo: um PID reutilizável, com limite de saída e proteção contra windup (a integral para de crescer quando a saída satura).
"""PID simples com limite e 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
No seguidor_cor.py, acrescente o import e os dois objetos depois das constantes e troque a função comando inteira por esta. No main(), quando o alvo some, chame pid_giro.reset() e pid_dist.reset().
from pid import PID
pid_giro = PID(kp=180, ki=0.0, kd=20, limite=150) # comece com kd=0 e suba aos poucos
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 morta: não tremer com o alvo no 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
Como verificar: com o robô no chão, empurre a bola para o lado. Só com P ele passa do ponto e volta; com o D ajustado, chega ao centro com no máximo uma pequena ultrapassagem. Se começar a tremer, o kd está alto demais.
Por que aprender
O mesmo PID do seguidor de linha da Trilha 2, agora com um sensor lento e ruidoso (a câmera). Aprender a sintonizar com atraso é o que separa um robô que persegue suavemente de um que dança em volta do alvo.
Conceitos-chave
✅ Testes, segurança e melhorias
Teste em camadas, do mais seguro para o mais real: rodas no ar, chão livre com velocidade máxima baixa (VEL_MAX = 100), e só depois a velocidade final. Anote em cada rodada o que mudou e o resultado, como no diário de bordo da Trilha 2. Os problemas mais comuns têm causa conhecida:
| Sintoma | Causa provável | O que mexer |
|---|---|---|
| Balança em volta do alvo | KP_GIRO alto, atraso da câmera | baixar kp, subir kd |
| Para longe do alvo | atrito vence o PWM baixo | subir ki da distância |
| Para e anda aos trancos | ciclo maior que 300 ms | menor resolução, NCNN, AI HAT+ |
| Persegue a cortina azul | faixa HSV larga demais | estreitar HSV, subir AREA_MIN |
| Pi reinicia ao arrancar | energia compartilhada | fontes separadas (tópico 2) |
✓ Segurança
- ✓Chave de corte dos motores sempre ao alcance.
- ✓Área de teste livre de escadas e cabos; ninguém deitado no chão.
- ✓Teste o timeout: Ctrl+C e cabo puxado param o robô.
✗ Evite
- ✗Seguir crianças ou animais em velocidade alta.
- ✗Testar na mesa: um erro de sinal e o robô cai.
- ✗Mudar três ganhos de uma vez: você não sabe qual ajudou.
Melhorias (desafios)
Em vez de parar, gire devagar para o lado onde o alvo foi visto por último, por no máximo 5 segundos.
Uma média móvel do centro x (módulo 1.4) reduz o tremor causado por ruído na detecção.
Ligue o HC-SR04 no ESP32: abaixo de 20 cm ele ignora o comando de avançar, decida o Pi o que decidir.
Desenhe a caixa e o erro no quadro e transmita para o navegador, como no módulo 3.2.
💡 Próximo degrau: ROS 2
O robô da Trilha 3 já tem o desenho de um robô com ROS 2: um computador que pensa, um microcontrolador que age e mensagens entre os dois. Na Trilha 4 o protocolo caseiro vira tópicos (/cmd_vel), o ESP32 fala micro-ROS, e um LiDAR acrescenta mapa e navegação.
Por que aprender
Um projeto termina quando funciona de forma repetível e segura, não na primeira vez que dá certo. Documentar sintomas, causas e ajustes é o que permite apresentar o robô, ensinar com ele e continuar evoluindo sem recomeçar.
Conceitos-chave
✅ Resumo do módulo e da trilha
Para ir além
- Ultralytics no Raspberry Pi — YOLO26, exportação NCNN e desempenho
- Manual do picamera2 — configurações de vídeo e formatos
- OpenCV: espaços de cor e HSV — rastrear objeto por cor
- Raspberry Pi AI HAT+ — acelerar a detecção
- Documentação do ROS 2 — para a Trilha 4
Próximo: Trilha 4 · ROS 2 e navegação
4.1 · ROS 2 sem medo: nós, tópicos e mensagens. O seu robô da Trilha 3 ganha mapa, localização e navegação de cômodo em cômodo.