PR5071/03 — Robot UR3e amb Visió OAK-D Pro-W
Objectius
Al finalitzar aquesta pràctica hauràs de ser capaç de:
- Configurar la càmera OAK-D Pro-W amb el SDK DepthAI dins d'un contenidor Docker
- Executar detecció d'objectes YOLOv8 directament al xip de la càmera (inferència on-device)
- Obtenir les coordenades 3D (X, Y, Z) d'un objecte detectat mitjançant la profunditat estèreo
- Realitzar una calibració mà-ull simplificada (transformació càmera → base del robot)
- Connectar-se al robot UR3e mitjançant la biblioteca Python
ur_rtde(o al simulador URSim) - Enviar una ordre de moviment al robot cap a la posició 3D calculada
- Personalitzar totes les sortides (fitxers, contenidors, JSON) amb el teu nom i cognom
Prerequisits
| Requisit | Detall |
|---|---|
| Temps estimat | 6 hores (3 sessions de 2h) |
| RAM mínima | 8 GB (16 GB recomanat amb URSim actiu) |
| Python | 3.11 o superior |
| Docker | Docker Desktop (Linux containers) |
| Hardware real | UR3e + OAK-D Pro-W (via USB 3.0) |
| Mode simulació | URSim e-Series + webcam o fitxer de vídeo |
Sense hardware real
Si no disposes del robot UR3e físic ni de la càmera OAK-D Pro-W, pots completar la pràctica en mode simulació:
- Robot: URSim e-Series (contenidor Docker oficial)
- Càmera: webcam USB convencional o fitxer de vídeo
.mp4com a entrada simulada
Introducció
La manipulació automatitzada de peces és una de les operacions més comunes a la indústria manufacturera moderna. En entorns d'automatització flexible del 2025, els robots col·laboratius (cobots) han substituït en moltes línies de producció els robots industrials tradicionals, permetent treballar de forma segura al costat dels operaris humans sense necessitat de gàbies de protecció. La tasca de pick-and-place —agafar un objecte d'una posició i deixar-lo en una altra— requereix que el robot "sàpiga" on és l'objecte, i aquí és on entra la visió per computador.
La càmera Luxonis OAK-D Pro-W és un dispositiu de visió estèreo amb inferència neuronal integrada. Disposa de dos sensors de profunditat sincronitzats i un processador neural dedicat (Intel Myriad X) que permet executar models de xarxa neuronal directament al dispositiu, sense necessitat de GPU al PC amfitrió. El SDK DepthAI exposa una API de pipeline en Python que permet combinar fluxos de color, profunditat i detecció d'objectes en temps real. El resultat és que per a cada objecte detectat, podem obtenir directament les seves coordenades tridimensionals en l'espai de la càmera, expressades en mil·límetres.
El robot Universal Robots UR3e és un braç col·laboratiu de 6 eixos amb una càrrega útil de 3 kg i un radi d'acció de 500 mm. Es programa mitjançant el seu propi llenguatge URScript, però exposa una interfície de comunicació en temps real anomenada RTDE (Real-Time Data Exchange) que permet controlar-lo des d'un programa extern via xarxa TCP/IP. La biblioteca Python ur_rtde encapsula aquest protocol i ofereix funcions d'alt nivell per llegir l'estat del robot i enviar-li ordres de moviment. En mode col·laboratiu, el UR3e incorpora sensors de força a totes les articulacions que aturen el moviment si detecten una col·lisió inesperada.
La combinació de visió 3D i robòtica és un dels pilars de la Indústria 4.0. Quan la càmera és capaç de localitzar un objecte en l'espai tridimensional i el robot és capaç de moure's a aquella posició, tenim la base d'un sistema autònom de manipulació. Per fer-ho possible cal resoldre el problema de la calibració mà-ull: determinar la relació geomètrica entre el sistema de coordenades de la càmera i el sistema de coordenades de la base del robot. Aquesta pràctica aborda una versió simplificada d'aquest problema i integra tots els components en un pipeline funcional.
Part 1 — Configuració de l'entorn
Estructura del projecte
practica_ur3e_oakd/
├── docker-compose.yml
├── oak-detection/
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── deteccio_objectes_nom_cognom.py
│ └── robot_vision_nom_cognom.py
└── resultats/
└── resultat_nom_cognom.json
docker-compose.yml
services:
# Simulador oficial del robot UR3e (e-Series)
ursim:
image: universalrobots/ursim_e-series
container_name: ursim-nom-cognom
stdin_open: true
tty: true
ports:
- "5900:5900" # VNC per visualitzar la interfície gràfica del simulador
- "29999:29999" # Dashboard Server
- "30001:30001" # Primary Interface
- "30002:30002" # Secondary Interface
- "30003:30003" # Real-Time Interface (RTDE)
- "30004:30004" # RTDE (alternatiu)
environment:
- ROBOT_MODEL=UR3e
networks:
- robot_net
restart: unless-stopped
# Servei de detecció d'objectes amb OAK-D i control del robot
oak-detection:
build:
context: ./oak-detection
dockerfile: Dockerfile
container_name: oak-nom-cognom
depends_on:
- ursim
environment:
- ROBOT_IP=ursim # Nom del servei Docker (resolució DNS interna)
- ROBOT_PORT=30004
- ALUMNE=nom-cognom # Canvia per el teu nom i cognom
- DISPLAY=${DISPLAY:-:0} # Necessari per a finestres OpenCV (opcional)
volumes:
- ./resultats:/app/resultats
- ./oak-detection:/app
# Passthrough del dispositiu USB per a la càmera OAK-D real
# Comenta aquestes línies si uses mode simulació
devices:
- /dev/bus/usb:/dev/bus/usb
privileged: true
networks:
- robot_net
restart: unless-stopped
networks:
robot_net:
driver: bridge
oak-detection/Dockerfile
FROM python:3.11-slim
WORKDIR /app
# Dependències del sistema per a DepthAI i OpenCV
RUN apt-get update && apt-get install -y \
libusb-1.0-0-dev \
udev \
libgl1 \
libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/*
# Regles udev per a la càmera OAK-D (permisos USB)
RUN echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="03e7", MODE="0666"' \
> /etc/udev/rules.d/80-movidius.rules
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "robot_vision_nom_cognom.py"]
oak-detection/requirements.txt
Posada en marxa
# Construir i iniciar tots els serveis
docker compose up --build -d
# Verificar que els contenidors estan en execució
docker compose ps
# Veure els logs del servei de detecció
docker compose logs -f oak-detection
# Aturar tot
docker compose down
Verificació del simulador
Un cop ursim-nom-cognom estigui en marxa, obre un client VNC (com TigerVNC) i connecta't a localhost:5900. Hauràs de veure la interfície gràfica del Polyscope (el sistema operatiu del UR3e). Activa el robot des de la pantalla d'inicialització i posa'l en mode Remote Control des de la barra superior.
Part 2 — Detecció d'objectes amb OAK-D i YOLOv8
Concepte del pipeline DepthAI
La OAK-D Pro-W processa tot internament: els dos sensors estèreo calculen el mapa de profunditat, el sensor RGB captura el color, i el xip neuronal executa el model YOLOv8. Tot plegat surt per USB com a fluxos sincronitzats de dades.
Sensor RGB ──────────────────────────────────────────┐
▼
Sensor estèreo esquerra ──► Processador estèreo ──► Fusió ──► Detecció amb profunditat (x, y, z mm)
Sensor estèreo dret ──────► ▲
│
Xip Intel Myriad X ──► YOLOv8n (inferència on-device)
Script de detecció: deteccio_objectes_nom_cognom.py
#!/usr/bin/env python3
"""
Detecció d'objectes en 3D amb OAK-D Pro-W i YOLOv8
Pràctica PR5071/03 — Mòdul 5071 Models d'IA
Autor: Nom Cognom
"""
import depthai as dai
import numpy as np
import cv2
import json
import time
from pathlib import Path
# ── Configuració general ──────────────────────────────────────────────────────
ALUMNE = "nom-cognom" # Canvia per el teu nom i cognom (sense espais)
CONFIANCA_MINIMA = 0.5 # Llindar mínim de confiança per acceptar una detecció
OBJECTE_DIANA = "bottle" # Classe COCO a detectar (pots canviar-ho per "cup", "scissors", etc.)
AMPLADA_IMG = 416 # Resolució d'entrada del model (píxels)
ALCADA_IMG = 416
def crear_pipeline() -> dai.Pipeline:
"""
Crea i configura el pipeline DepthAI per a:
- Captura de vídeo RGB
- Càlcul de profunditat estèreo
- Detecció d'objectes amb YOLOv8n (o MobileNetSSD com a alternativa)
Retorna: objecte dai.Pipeline configurat
"""
pipeline = dai.Pipeline()
# ── Nodes de càmera ──────────────────────────────────────────────────────
# Càmera de color (RGB central)
cam_rgb = pipeline.create(dai.node.ColorCamera)
cam_rgb.setPreviewSize(AMPLADA_IMG, ALCADA_IMG)
cam_rgb.setResolution(dai.ColorCameraProperties.SensorResolution.THE_1080_P)
cam_rgb.setInterleaved(False)
cam_rgb.setColorOrder(dai.ColorCameraProperties.ColorOrder.BGR)
cam_rgb.setFps(30)
# Càmeres monocromàtiques esquerda i dreta per a la profunditat
cam_esq = pipeline.create(dai.node.MonoCamera)
cam_esq.setResolution(dai.MonoCameraProperties.SensorResolution.THE_400_P)
cam_esq.setBoardSocket(dai.CameraBoardSocket.CAM_B) # Càmera esquerra
cam_dre = pipeline.create(dai.node.MonoCamera)
cam_dre.setResolution(dai.MonoCameraProperties.SensorResolution.THE_400_P)
cam_dre.setBoardSocket(dai.CameraBoardSocket.CAM_C) # Càmera dreta
# ── Node de profunditat estèreo ───────────────────────────────────────────
profunditat = pipeline.create(dai.node.StereoDepth)
profunditat.setDefaultProfilePreset(dai.node.StereoDepth.PresetMode.HIGH_DENSITY)
profunditat.setLeftRightCheck(True) # Millora la qualitat de la profunditat
profunditat.setSubpixel(True) # Subpíxel per a major precisió
profunditat.setDepthAlign(dai.CameraBoardSocket.CAM_A) # Alinea amb RGB
# Connectar càmeres mono al node de profunditat
cam_esq.out.link(profunditat.left)
cam_dre.out.link(profunditat.right)
# ── Node de detecció d'objectes (YOLOv8n) ────────────────────────────────
# Intentem carregar YOLOv8n; si no existeix, usem MobileNetSSD com a fallback
detector = pipeline.create(dai.node.YoloDetectionNetwork)
detector.setConfidenceThreshold(CONFIANCA_MINIMA)
detector.setNumClasses(80) # COCO: 80 classes
detector.setCoordinateSize(4)
detector.setIouThreshold(0.5)
detector.setNumInferenceThreads(2)
detector.input.setBlocking(False)
# Intentar carregar model YOLOv8n en format .blob (optimitzat per Myriad X)
blob_path = Path("yolov8n_coco_416x416_6shave.blob")
if blob_path.exists():
detector.setBlobPath(str(blob_path))
print(f"[INFO] Carregant model YOLOv8n des de {blob_path}")
else:
# Fallback: MobileNetSSD (inclòs amb DepthAI)
print("[AVÍS] No s'ha trobat yolov8n.blob. Usant MobileNetSSD com a alternativa.")
detector_mn = pipeline.create(dai.node.MobileNetSpatialDetectionNetwork)
detector_mn.setBlobPath(
dai.OpenVINO.getBlobLatestVersion("face-detection-retail-0004")
)
detector_mn.setConfidenceThreshold(CONFIANCA_MINIMA)
detector_mn.setBoundingBoxScaleFactor(0.5)
detector_mn.setDepthLowerThreshold(100) # mínim 10 cm
detector_mn.setDepthUpperThreshold(5000) # màxim 500 cm
cam_rgb.preview.link(detector_mn.input)
profunditat.depth.link(detector_mn.inputDepth)
# Sortides al host
sortida_deteccions = pipeline.create(dai.node.XLinkOut)
sortida_deteccions.setStreamName("deteccions")
detector_mn.out.link(sortida_deteccions.input)
sortida_preview = pipeline.create(dai.node.XLinkOut)
sortida_preview.setStreamName("preview")
cam_rgb.preview.link(sortida_preview.input)
return pipeline # Retorna amb fallback MobileNetSSD
# ── Node de profunditat espacial per a YOLOv8 ────────────────────────────
detector_espacial = pipeline.create(dai.node.YoloSpatialDetectionNetwork)
detector_espacial.setBlobPath(str(blob_path))
detector_espacial.setConfidenceThreshold(CONFIANCA_MINIMA)
detector_espacial.setNumClasses(80)
detector_espacial.setCoordinateSize(4)
detector_espacial.setIouThreshold(0.5)
detector_espacial.setBoundingBoxScaleFactor(0.5)
detector_espacial.setDepthLowerThreshold(100) # mínim 10 cm de distància
detector_espacial.setDepthUpperThreshold(5000) # màxim 500 cm
# Connectar entrades al detector espacial
cam_rgb.preview.link(detector_espacial.input)
profunditat.depth.link(detector_espacial.inputDepth)
# ── Sortides al host via XLink ────────────────────────────────────────────
sortida_deteccions = pipeline.create(dai.node.XLinkOut)
sortida_deteccions.setStreamName("deteccions")
detector_espacial.out.link(sortida_deteccions.input)
sortida_profunditat = pipeline.create(dai.node.XLinkOut)
sortida_profunditat.setStreamName("profunditat")
detector_espacial.passthroughDepth.link(sortida_profunditat.input)
sortida_preview = pipeline.create(dai.node.XLinkOut)
sortida_preview.setStreamName("preview")
detector_espacial.passthrough.link(sortida_preview.input)
return pipeline
# Etiquetes de classes COCO (80 classes)
ETIQUETES_COCO = [
"person", "bicycle", "car", "motorbike", "aeroplane", "bus", "train",
"truck", "boat", "traffic light", "fire hydrant", "stop sign",
"parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow",
"elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag",
"tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite",
"baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket",
"bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana",
"apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza",
"donut", "cake", "chair", "sofa", "pottedplant", "bed", "diningtable",
"toilet", "tvmonitor", "laptop", "mouse", "remote", "keyboard",
"cell phone", "microwave", "oven", "toaster", "sink", "refrigerator",
"book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush"
]
def dibuixar_deteccio(frame: np.ndarray, deteccio, etiquetes: list) -> None:
"""
Dibuixa el quadre de detecció i la informació 3D sobre el frame.
frame: imatge BGR de NumPy
deteccio: objecte de detecció DepthAI amb coordenades espacials
etiquetes: llista de noms de classe
"""
h, w = frame.shape[:2]
# Coordenades del quadre delimitador (normalitzades 0–1 → píxels)
x1 = int(deteccio.xmin * w)
y1 = int(deteccio.ymin * h)
x2 = int(deteccio.xmax * w)
y2 = int(deteccio.ymax * h)
# Coordenades 3D en mil·límetres (sistema de referència de la càmera)
x_mm = deteccio.spatialCoordinates.x
y_mm = deteccio.spatialCoordinates.y
z_mm = deteccio.spatialCoordinates.z
# Etiqueta de classe
idx_classe = deteccio.label
nom_classe = etiquetes[idx_classe] if idx_classe < len(etiquetes) else f"classe_{idx_classe}"
confianca = deteccio.confidence
# Color del quadre: verd per a l'objecte diana, blau per als altres
color = (0, 255, 0) if nom_classe == OBJECTE_DIANA else (255, 100, 0)
# Dibuixar quadre delimitador
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
# Text amb classe i confiança
text_classe = f"{nom_classe}: {confianca:.2f}"
cv2.putText(frame, text_classe, (x1, y1 - 25),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)
# Text amb coordenades 3D
text_posicio = f"X:{x_mm:.0f} Y:{y_mm:.0f} Z:{z_mm:.0f} mm"
cv2.putText(frame, text_posicio, (x1, y1 - 8),
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0, 200, 255), 1)
# Imprimir a consola
print(f" [{nom_classe}] conf={confianca:.3f} | "
f"X={x_mm:.1f}mm Y={y_mm:.1f}mm Z={z_mm:.1f}mm")
def executar_deteccio() -> dict | None:
"""
Executa el bucle de detecció en temps real.
Retorna un diccionari amb la millor detecció de l'objecte diana, o None.
"""
pipeline = crear_pipeline()
millor_deteccio = None
print(f"\n[PR5071/03] Iniciant detecció — Alumne: {ALUMNE}")
print(f"[INFO] Cercant objecte: '{OBJECTE_DIANA}'")
print("[INFO] Prem 'q' per sortir, 's' per guardar la detecció actual\n")
with dai.Device(pipeline) as dispositiu:
# Obtenir les cues de sortida
cua_deteccions = dispositiu.getOutputQueue(
name="deteccions", maxSize=4, blocking=False
)
cua_preview = dispositiu.getOutputQueue(
name="preview", maxSize=4, blocking=False
)
while True:
# Obtenir frames
frame_dades = cua_preview.tryGet()
deteccions_dades = cua_deteccions.tryGet()
if frame_dades is not None:
frame = frame_dades.getCvFrame()
# Afegir capçalera amb nom de l'alumne
cv2.putText(frame, f"PR5071/03 — {ALUMNE}", (10, 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1)
if deteccions_dades is not None:
deteccions = deteccions_dades.detections
for det in deteccions:
idx = det.label
nom = ETIQUETES_COCO[idx] if idx < len(ETIQUETES_COCO) else "?"
dibuixar_deteccio(frame, det, ETIQUETES_COCO)
# Guardar la millor detecció de l'objecte diana
if nom == OBJECTE_DIANA:
if (millor_deteccio is None or
det.confidence > millor_deteccio.confidence):
millor_deteccio = det
# Mostrar el frame
cv2.imshow(f"OAK-D Detecció — {ALUMNE}", frame)
# Control de tecles
tecla = cv2.waitKey(1) & 0xFF
if tecla == ord('q'):
break
elif tecla == ord('s') and millor_deteccio is not None:
print(f"\n[OK] Detecció guardada: {OBJECTE_DIANA}")
break
cv2.destroyAllWindows()
if millor_deteccio is not None:
return {
"classe": ETIQUETES_COCO[millor_deteccio.label],
"confianca": float(millor_deteccio.confidence),
"x_cam_mm": float(millor_deteccio.spatialCoordinates.x),
"y_cam_mm": float(millor_deteccio.spatialCoordinates.y),
"z_cam_mm": float(millor_deteccio.spatialCoordinates.z),
}
return None
if __name__ == "__main__":
resultat = executar_deteccio()
if resultat:
print(f"\n[RESULTAT] Objecte detectat: {resultat}")
else:
print(f"\n[AVÍS] No s'ha detectat cap '{OBJECTE_DIANA}'")
Executar amb passthrough USB (hardware real)
# Executar directament amb accés al dispositiu USB de la càmera
docker run --rm -it \
--device=/dev/bus/usb \
--privileged \
-v $(pwd)/resultats:/app/resultats \
oak-nom-cognom \
python deteccio_objectes_nom_cognom.py
Part 3 — Calibració mà-ull simplificada
Concepte de calibració mà-ull
La calibració mà-ull (hand-eye calibration) determina la relació geomètrica entre el sistema de coordenades de la càmera i el sistema de coordenades del robot. Existeixen dues configuracions:
| Configuració | Descripció | Quan s'usa |
|---|---|---|
| Eye-in-hand | Càmera muntada al canell del robot | Quan la càmera es mou amb el robot |
| Eye-to-hand | Càmera fixa, independent del robot | Quan la càmera observa l'espai de treball des de fora |
En aquesta pràctica usem la configuració eye-to-hand: la càmera OAK-D està fixada en un suport extern i observa la taula de treball on opera el robot.
Mètode simplificat: 3 punts de calibració
En lloc d'usar el mètode complet de Tsai-Lenz (que requereix moure el robot a múltiples posicions), usarem 3 punts de calibració coneguts: posicions de l'espai de treball de les quals coneixem tant les coordenades en el sistema de la càmera com les coordenades en el sistema base del robot.
#!/usr/bin/env python3
"""
Calibració mà-ull simplificada — eye-to-hand
Transformació del sistema de coordenades de la càmera al sistema base del robot UR3e
Pràctica PR5071/03 — Mòdul 5071 Models d'IA
Autor: Nom Cognom
"""
import numpy as np
# ── Punts de calibració ───────────────────────────────────────────────────────
# Cada punt és una posició coneguda de l'espai de treball.
# Per obtenir-los: col·loca una marca (p.ex. una boleta de color) a cada posició,
# llegeix les coordenades de la càmera (x_cam, y_cam, z_cam) en mm,
# i mesura o llegeix la posició real en coordenades de la base del robot (x_rob, y_rob, z_rob) en metres.
# Coordenades en el sistema de la càmera (mm)
PUNTS_CAMERA_MM = np.array([
[-150.0, 80.0, 520.0], # Punt de calibració 1 (esquerra, prop)
[ 10.0, 80.0, 580.0], # Punt de calibració 2 (centre, mig)
[ 160.0, 80.0, 490.0], # Punt de calibració 3 (dreta, prop)
], dtype=np.float64)
# Coordenades en el sistema base del robot (metres, convenció UR)
PUNTS_ROBOT_M = np.array([
[0.250, -0.180, 0.050], # Punt de calibració 1 → base robot
[0.350, 0.000, 0.050], # Punt de calibració 2 → base robot
[0.250, 0.200, 0.050], # Punt de calibració 3 → base robot
], dtype=np.float64)
def calcular_transformacio_cam_robot(
punts_cam: np.ndarray,
punts_robot: np.ndarray
) -> np.ndarray:
"""
Calcula la matriu de transformació homogènia 4x4 que transforma
coordenades del sistema de la càmera al sistema base del robot.
Mètode: mínims quadrats via pseudo-inversa (SVD).
punts_cam: array (N, 3) en mm
punts_robot: array (N, 3) en metres
Retorna: matriu 4x4 de transformació homogènia
"""
# Convertir les coordenades de la càmera de mm a metres
punts_cam_m = punts_cam / 1000.0
# Calcular els centroides de cada conjunt de punts
centroide_cam = punts_cam_m.mean(axis=0)
centroide_robot = punts_robot.mean(axis=0)
# Centrar els punts al voltant dels seus centroides
cam_centrat = punts_cam_m - centroide_cam
robot_centrat = punts_robot - centroide_robot
# Descomposició SVD per calcular la matriu de rotació òptima
H = cam_centrat.T @ robot_centrat
U, S, Vt = np.linalg.svd(H)
R = Vt.T @ U.T
# Correcció si la matriu de rotació té reflexió (det = -1)
if np.linalg.det(R) < 0:
Vt[-1, :] *= -1
R = Vt.T @ U.T
# Calcular la translació
t = centroide_robot - R @ centroide_cam
# Construir la matriu homogènia 4x4
T = np.eye(4)
T[:3, :3] = R
T[:3, 3] = t
return T
def transformar_coordenades(
pos_cam_mm: np.ndarray,
T_cam2robot: np.ndarray
) -> np.ndarray:
"""
Transforma una posició del sistema de la càmera al sistema base del robot.
pos_cam_mm: array (3,) amb coordenades [x, y, z] en mm
T_cam2robot: matriu de transformació 4x4
Retorna: array (3,) amb coordenades [x, y, z] en metres (sistema robot)
"""
# Convertir a metres i a coordenades homogènies
pos_cam_m = pos_cam_mm / 1000.0
pos_hom = np.array([pos_cam_m[0], pos_cam_m[1], pos_cam_m[2], 1.0])
# Aplicar la transformació
pos_robot_hom = T_cam2robot @ pos_hom
# Retornar les coordenades cartesianes (sense el component homogeni)
return pos_robot_hom[:3]
def validar_calibracio(
T_cam2robot: np.ndarray,
punts_cam: np.ndarray,
punts_robot: np.ndarray
) -> float:
"""
Calcula l'error de reprojeccció (RMS) de la calibració en mm.
Retorna: error RMS en mm
"""
errors = []
for p_cam, p_robot_real in zip(punts_cam, punts_robot):
p_robot_calc = transformar_coordenades(p_cam, T_cam2robot)
error_m = np.linalg.norm(p_robot_calc - p_robot_real)
errors.append(error_m * 1000.0) # Convertir a mm
rms = float(np.sqrt(np.mean(np.array(errors) ** 2)))
return rms
# ── Execució principal ────────────────────────────────────────────────────────
if __name__ == "__main__":
print("[PR5071/03] Calibració mà-ull simplificada (eye-to-hand)")
print("=" * 60)
# Calcular la matriu de transformació
T_cam2robot = calcular_transformacio_cam_robot(
PUNTS_CAMERA_MM, PUNTS_ROBOT_M
)
print("\nMatriu de transformació càmera → robot (T_cam2robot):")
print(np.array2string(T_cam2robot, precision=6, suppress_small=True))
# Validar la calibració
error_rms = validar_calibracio(T_cam2robot, PUNTS_CAMERA_MM, PUNTS_ROBOT_M)
print(f"\nError RMS de calibració: {error_rms:.2f} mm")
if error_rms < 5.0:
print("[OK] Calibració acceptable (error < 5 mm)")
else:
print("[AVÍS] Error de calibració alt. Reviseu els punts de referència.")
# Exemple de transformació d'una posició detectada
posicio_camera = np.array([25.0, 90.0, 550.0]) # mm (sistema càmera)
posicio_robot = transformar_coordenades(posicio_camera, T_cam2robot)
print(f"\nExemple de transformació:")
print(f" Posició càmera: X={posicio_camera[0]:.1f}mm "
f"Y={posicio_camera[1]:.1f}mm Z={posicio_camera[2]:.1f}mm")
print(f" Posició robot: X={posicio_robot[0]:.4f}m "
f"Y={posicio_robot[1]:.4f}m Z={posicio_robot[2]:.4f}m")
Com obtenir els punts de calibració
- Imprimeix o col·loca 3 marques de color a posicions conegudes de la taula de treball.
- Per cada marca: llegeix les coordenades (x, y, z) en mm que reporta el pipeline DepthAI.
- Porta manualment el robot (mode freedrive o teach pendant) fins a tocar cada marca.
- Llegeix la posició cartesiana del TCP del robot (via Polyscope o
ur_rtde). - Omple els arrays
PUNTS_CAMERA_MMiPUNTS_ROBOT_Mamb els valors mesurats.
Part 4 — Connexió i control del UR3e
Prerequisits del robot
Abans de connectar-se via RTDE, assegura't que:
- El robot (o URSim) està en marxa i inicialitzat
- El robot està en mode Remote Control (barra superior del Polyscope → icona de l'ordinador)
- El programa "External Control" o un programa buit està carregat i en mode play
- La IP del robot és accessible (
ping 192.168.56.101per a URSim)
Script de control: connexió i moviment
#!/usr/bin/env python3
"""
Control del robot UR3e via ur_rtde
Pràctica PR5071/03 — Mòdul 5071 Models d'IA
Autor: Nom Cognom
"""
import rtde_control
import rtde_receive
import rtde_io
import numpy as np
import time
# ── Configuració de connexió ──────────────────────────────────────────────────
IP_ROBOT = "192.168.56.101" # IP del robot (o contenidor URSim)
VELOCITAT = 0.1 # m/s — velocitat baixa per seguretat
ACCELERACIO = 0.05 # m/s² — acceleració suau
# Posició d'inici segura (posició "home") — [x, y, z, rx, ry, rz] en metres i radians
# rx, ry, rz: vector de rotació (eix-angle, format UR)
POSICIO_HOME = [0.300, 0.000, 0.300, np.pi, 0.0, 0.0]
# Altura de seguretat per als moviments de translació (metres)
ALCADA_SEGURA = 0.250
def connectar_robot(ip: str) -> tuple:
"""
Estableix connexió RTDE amb el robot UR3e.
Retorna: (rtde_control, rtde_receive) o llança excepció si falla.
"""
print(f"[INFO] Connectant al robot: {ip} ...")
try:
# Interfície de control (enviar ordres)
control = rtde_control.RTDEControlInterface(ip)
# Interfície de recepció (llegir estat)
recepcio = rtde_receive.RTDEReceiveInterface(ip)
print("[OK] Connexió RTDE establerta correctament")
print(f" Versió del controlador: {recepcio.getRobotMode()}")
return control, recepcio
except Exception as e:
print(f"[ERROR] No s'ha pogut connectar al robot: {e}")
print("[CONSELL] Verifica que el robot és en Remote Control i que la IP és correcta.")
raise
def llegir_estat_robot(recepcio: rtde_receive.RTDEReceiveInterface) -> dict:
"""
Llegeix l'estat actual del robot (posició joints i TCP).
Retorna: diccionari amb l'estat
"""
# Posició de les 6 articulacions en radians
angles_joints = recepcio.getActualQ()
# Posició cartesiana del TCP [x, y, z, rx, ry, rz]
posicio_tcp = recepcio.getActualTCPPose()
# Velocitat actual del TCP
velocitat_tcp = recepcio.getActualTCPSpeed()
# Mode del robot (0=Disconnected, 7=Running, etc.)
mode_robot = recepcio.getRobotMode()
estat = {
"angles_joints_rad": angles_joints,
"posicio_tcp_m": posicio_tcp,
"velocitat_tcp": velocitat_tcp,
"mode": mode_robot,
}
print(f"\n[ESTAT ROBOT]")
print(f" Mode: {mode_robot}")
print(f" TCP: X={posicio_tcp[0]:.4f}m Y={posicio_tcp[1]:.4f}m Z={posicio_tcp[2]:.4f}m")
print(f" Joints (º): " +
" ".join(f"J{i+1}={np.degrees(q):.1f}" for i, q in enumerate(angles_joints)))
return estat
def moure_a_posicio(
control: rtde_control.RTDEControlInterface,
recepcio: rtde_receive.RTDEReceiveInterface,
posicio_objectiu: list,
velocitat: float = VELOCITAT,
acceleracio: float = ACCELERACIO
) -> bool:
"""
Mou el robot fins a la posició cartesiana especificada (moveL).
posicio_objectiu: [x, y, z, rx, ry, rz] en metres i radians
Retorna: True si el moviment s'ha completat, False si hi ha hagut error.
"""
# Verificació de seguretat: la posició Z ha de ser >= 0 (sobre la taula)
if posicio_objectiu[2] < 0.02:
print(f"[SEGURETAT] La posició Z={posicio_objectiu[2]:.3f}m és massa baixa. Moviment cancel·lat.")
return False
# Verificació de l'espai de treball del UR3e (radi màxim ~500mm des de la base)
dist_horitzontal = np.sqrt(posicio_objectiu[0]**2 + posicio_objectiu[1]**2)
if dist_horitzontal > 0.48:
print(f"[SEGURETAT] Posició fora del radi de treball ({dist_horitzontal*1000:.0f}mm > 480mm).")
return False
print(f"\n[MOVIMENT] Movent a: "
f"X={posicio_objectiu[0]:.4f}m "
f"Y={posicio_objectiu[1]:.4f}m "
f"Z={posicio_objectiu[2]:.4f}m")
print(f" Velocitat: {velocitat} m/s | Acceleració: {acceleracio} m/s²")
try:
# moveL: moviment lineal en l'espai cartesià
control.moveL(posicio_objectiu, velocitat, acceleracio)
# Llegir i mostrar la posició final
time.sleep(0.2)
posicio_final = recepcio.getActualTCPPose()
print(f"[OK] Posició assolida: "
f"X={posicio_final[0]:.4f}m "
f"Y={posicio_final[1]:.4f}m "
f"Z={posicio_final[2]:.4f}m")
return True
except Exception as e:
print(f"[ERROR] El moviment ha fallat: {e}")
return False
def anar_a_home(
control: rtde_control.RTDEControlInterface,
recepcio: rtde_receive.RTDEReceiveInterface
) -> bool:
"""Porta el robot a la posició d'inici segura (home)."""
print("\n[HOME] Movent a posició d'inici...")
return moure_a_posicio(control, recepcio, POSICIO_HOME)
def apropar_i_baixar(
control: rtde_control.RTDEControlInterface,
recepcio: rtde_receive.RTDEReceiveInterface,
posicio_objectiu: list,
alcada_aproximacio: float = 0.10
) -> bool:
"""
Moviment de seguretat en dues fases:
1. Moure't per sobre de l'objecte (posició d'aproximació)
2. Baixar fins a l'objectiu
posicio_objectiu: [x, y, z, rx, ry, rz]
alcada_aproximacio: distància per sobre de l'objectiu per a la fase d'aproximació (m)
"""
# Fase 1: posició d'aproximació (per sobre de l'objectiu)
posicio_aprox = posicio_objectiu.copy()
posicio_aprox[2] = posicio_objectiu[2] + alcada_aproximacio
print(f"\n[APROXIMACIÓ] Fase 1: moure's per sobre de l'objectiu")
ok1 = moure_a_posicio(control, recepcio, posicio_aprox)
if not ok1:
return False
# Fase 2: baixar fins a l'objectiu (velocitat reduïda)
print(f"[APROXIMACIÓ] Fase 2: baixar fins a l'objectiu")
ok2 = moure_a_posicio(control, recepcio, posicio_objectiu,
velocitat=0.05, acceleracio=0.02)
return ok2
# ── Exemple d'ús complet ──────────────────────────────────────────────────────
if __name__ == "__main__":
print("[PR5071/03] Control del UR3e via RTDE")
print("=" * 60)
# Connectar al robot
control, recepcio = connectar_robot(IP_ROBOT)
try:
# Llegir estat inicial
llegir_estat_robot(recepcio)
# Moure a posició home
anar_a_home(control, recepcio)
time.sleep(1.0)
# Exemple: moure a una posició de prova per damunt de la taula
posicio_prova = [0.350, 0.100, 0.150, np.pi, 0.0, 0.0]
print("\n[PROVA] Movent a posició de prova...")
apropar_i_baixar(control, recepcio, posicio_prova)
time.sleep(1.0)
# Tornar a home
anar_a_home(control, recepcio)
finally:
# Alliberar la connexió RTDE correctament
control.stopScript()
print("\n[INFO] Connexió RTDE tancada.")
Part 5 — Integració completa
Pipeline complet: visió → transformació → robot
flowchart TD
A([Inici del sistema]) --> B[Inicialitzar càmera OAK-D<br/>Pipeline DepthAI + YOLOv8]
B --> C[Connectar al robot UR3e<br/>via RTDE]
C --> D[Moure robot a posició HOME]
D --> E[Capturar frame de la càmera]
E --> F{Objecte diana<br/>detectat?}
F -- No --> E
F -- Sí --> G[Obtenir coordenades 3D<br/>X, Y, Z en mm — sistema càmera]
G --> H{Confiança ><br/>llindar mínim?}
H -- No --> E
H -- Sí --> I[Aplicar transformació<br/>T_cam2robot → coordenades robot]
I --> J[Verificació de seguretat<br/>Z > 0.02m, dist < 480mm]
J -- Fora de rang --> K[Registrar avís<br/>i continuar capturant]
K --> E
J -- OK --> L[Moure robot per sobre<br/>de l'objectiu — aproximació]
L --> M[Baixar fins a<br/>la posició objectiu]
M --> N[Guardar resultat JSON<br/>resultat_nom_cognom.json]
N --> O[Tornar a HOME]
O --> P([Fi])
Script d'integració: robot_vision_nom_cognom.py
#!/usr/bin/env python3
"""
Script d'integració completa: Visió OAK-D + Control UR3e
Pràctica PR5071/03 — Mòdul 5071 Models d'IA
Autor: Nom Cognom
Data: 2025
"""
import depthai as dai
import numpy as np
import cv2
import json
import time
import os
from pathlib import Path
from datetime import datetime
# Importar la lògica de control del robot (del script de la Part 4)
import rtde_control
import rtde_receive
# ── Paràmetres globals ────────────────────────────────────────────────────────
ALUMNE = os.environ.get("ALUMNE", "nom-cognom") # Llegit de la variable d'entorn Docker
IP_ROBOT = os.environ.get("ROBOT_IP", "192.168.56.101")
OBJECTE_DIANA = "bottle" # Classe COCO a detectar
CONFIANCA_MINIMA = 0.60 # Llindar de confiança (60%)
VELOCITAT_ROBOT = 0.08 # m/s — velocitat segura per a l'entorn de laboratori
ACCELERACIO_ROBOT = 0.04 # m/s²
# Orientació del TCP durant els moviments (apunta cap avall, vertical)
ORIENTACIO_TCP = [np.pi, 0.0, 0.0]
# Directori de sortida per als resultats
DIR_RESULTATS = Path("/app/resultats")
DIR_RESULTATS.mkdir(parents=True, exist_ok=True)
# Matriu de transformació càmera → robot (calculada a la Part 3)
# Substitueix amb la teva matriu calibrada!
T_CAM2ROBOT = np.array([
[ 0.9998, -0.0105, 0.0174, 0.3512],
[ 0.0108, 0.9998, -0.0152, -0.0023],
[-0.0172, 0.0154, 0.9997, 0.0481],
[ 0.0000, 0.0000, 0.0000, 1.0000],
], dtype=np.float64)
# ── Etiquetes COCO ────────────────────────────────────────────────────────────
ETIQUETES_COCO = [
"person", "bicycle", "car", "motorbike", "aeroplane", "bus", "train",
"truck", "boat", "traffic light", "fire hydrant", "stop sign",
"parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow",
"elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag",
"tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite",
"baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket",
"bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana",
"apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza",
"donut", "cake", "chair", "sofa", "pottedplant", "bed", "diningtable",
"toilet", "tvmonitor", "laptop", "mouse", "remote", "keyboard",
"cell phone", "microwave", "oven", "toaster", "sink", "refrigerator",
"book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush"
]
# ── Funcions auxiliars ────────────────────────────────────────────────────────
def transformar_cam_a_robot(pos_cam_mm: np.ndarray) -> np.ndarray:
"""Aplica la transformació T_CAM2ROBOT i retorna la posició en metres."""
pos_m = pos_cam_mm / 1000.0
pos_hom = np.array([pos_m[0], pos_m[1], pos_m[2], 1.0])
pos_robot = T_CAM2ROBOT @ pos_hom
return pos_robot[:3]
def verificar_posicio_segura(pos: np.ndarray) -> bool:
"""
Comprova que la posició objectiu és dins dels límits de seguretat del UR3e.
Retorna True si és segura, False si cal rebutjar el moviment.
"""
# Altura mínima per sobre de la taula
if pos[2] < 0.02:
print(f"[SEGURETAT] Z={pos[2]:.3f}m — massa proper a la taula")
return False
# Radi màxim horitzontal del UR3e (500mm de rang, usem 480mm per marge)
radi = np.sqrt(pos[0]**2 + pos[1]**2)
if radi > 0.48:
print(f"[SEGURETAT] Radi={radi*1000:.0f}mm — fora del rang de treball")
return False
return True
def crear_pipeline_deteccio() -> dai.Pipeline:
"""
Crea el pipeline DepthAI per a detecció espacial YOLOv8.
Versió simplificada per a la integració.
"""
pipeline = dai.Pipeline()
cam_rgb = pipeline.create(dai.node.ColorCamera)
cam_rgb.setPreviewSize(416, 416)
cam_rgb.setResolution(dai.ColorCameraProperties.SensorResolution.THE_1080_P)
cam_rgb.setInterleaved(False)
cam_rgb.setColorOrder(dai.ColorCameraProperties.ColorOrder.BGR)
cam_esq = pipeline.create(dai.node.MonoCamera)
cam_esq.setResolution(dai.MonoCameraProperties.SensorResolution.THE_400_P)
cam_esq.setBoardSocket(dai.CameraBoardSocket.CAM_B)
cam_dre = pipeline.create(dai.node.MonoCamera)
cam_dre.setResolution(dai.MonoCameraProperties.SensorResolution.THE_400_P)
cam_dre.setBoardSocket(dai.CameraBoardSocket.CAM_C)
profunditat = pipeline.create(dai.node.StereoDepth)
profunditat.setDefaultProfilePreset(dai.node.StereoDepth.PresetMode.HIGH_DENSITY)
profunditat.setLeftRightCheck(True)
profunditat.setDepthAlign(dai.CameraBoardSocket.CAM_A)
cam_esq.out.link(profunditat.left)
cam_dre.out.link(profunditat.right)
detector = pipeline.create(dai.node.YoloSpatialDetectionNetwork)
detector.setBlobPath("yolov8n_coco_416x416_6shave.blob")
detector.setConfidenceThreshold(CONFIANCA_MINIMA)
detector.setNumClasses(80)
detector.setCoordinateSize(4)
detector.setIouThreshold(0.5)
detector.setBoundingBoxScaleFactor(0.5)
detector.setDepthLowerThreshold(100)
detector.setDepthUpperThreshold(5000)
cam_rgb.preview.link(detector.input)
profunditat.depth.link(detector.inputDepth)
sortida_det = pipeline.create(dai.node.XLinkOut)
sortida_det.setStreamName("deteccions")
detector.out.link(sortida_det.input)
sortida_prev = pipeline.create(dai.node.XLinkOut)
sortida_prev.setStreamName("preview")
detector.passthrough.link(sortida_prev.input)
return pipeline
def detectar_objecte_diana(
dispositiu: dai.Device,
max_intents: int = 60
) -> dict | None:
"""
Cerca l'objecte diana en els frames de la càmera.
Espera fins a max_intents frames abans de rendir-se.
Retorna: diccionari amb la detecció o None.
"""
cua_det = dispositiu.getOutputQueue("deteccions", maxSize=4, blocking=False)
cua_prev = dispositiu.getOutputQueue("preview", maxSize=4, blocking=False)
millor = None
intents = 0
print(f"[DETECCIÓ] Cercant '{OBJECTE_DIANA}' (màx {max_intents} frames)...")
while intents < max_intents:
frame_dades = cua_prev.tryGet()
det_dades = cua_det.tryGet()
if det_dades is not None:
for det in det_dades.detections:
nom = ETIQUETES_COCO[det.label] if det.label < len(ETIQUETES_COCO) else "?"
if nom == OBJECTE_DIANA and det.confidence >= CONFIANCA_MINIMA:
if millor is None or det.confidence > millor["confianca"]:
millor = {
"classe": nom,
"confianca": float(det.confidence),
"x_cam_mm": float(det.spatialCoordinates.x),
"y_cam_mm": float(det.spatialCoordinates.y),
"z_cam_mm": float(det.spatialCoordinates.z),
}
print(f" [OK] '{nom}' detectat amb confiança {det.confidence:.3f} "
f"— Z={det.spatialCoordinates.z:.0f}mm")
if frame_dades is not None:
frame = frame_dades.getCvFrame()
cv2.putText(frame, f"Cercant: {OBJECTE_DIANA} — {ALUMNE}",
(10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 1)
cv2.imshow("PR5071/03 — Detecció", frame)
if cv2.waitKey(1) == ord('q'):
break
intents += 1
cv2.destroyAllWindows()
return millor
def guardar_resultat(
deteccio: dict,
pos_cam: np.ndarray,
pos_robot: np.ndarray,
posicio_tcp_final: list
) -> Path:
"""
Guarda el resultat de la pràctica en un fitxer JSON amb el nom de l'alumne.
Retorna: ruta al fitxer guardat.
"""
nom_fitxer = DIR_RESULTATS / f"resultat_{ALUMNE}.json"
resultat = {
"metadades": {
"alumne": ALUMNE,
"practica": "PR5071/03",
"modul": "5071",
"timestamp": datetime.now().isoformat(),
},
"deteccio": {
"classe": deteccio["classe"],
"confianca": deteccio["confianca"],
"coordenades_camera_mm": {
"x": deteccio["x_cam_mm"],
"y": deteccio["y_cam_mm"],
"z": deteccio["z_cam_mm"],
},
},
"transformacio": {
"coordenades_robot_m": {
"x": float(pos_robot[0]),
"y": float(pos_robot[1]),
"z": float(pos_robot[2]),
},
"matriu_T_cam2robot": T_CAM2ROBOT.tolist(),
},
"robot": {
"ip": IP_ROBOT,
"posicio_tcp_final": posicio_tcp_final,
"velocitat_ms": VELOCITAT_ROBOT,
"acceleracio_ms2": ACCELERACIO_ROBOT,
},
}
with open(nom_fitxer, "w", encoding="utf-8") as f:
json.dump(resultat, f, ensure_ascii=False, indent=2)
print(f"\n[RESULTAT] Fitxer guardat: {nom_fitxer}")
return nom_fitxer
# ── Pipeline principal ────────────────────────────────────────────────────────
def executar_pipeline_complet() -> None:
"""
Funció principal que orquestra tot el pipeline:
1. Inicialitza la càmera OAK-D
2. Connecta al robot UR3e
3. Porta el robot a HOME
4. Detecta l'objecte diana
5. Transforma coordenades càmera → robot
6. Mou el robot cap a l'objectiu
7. Guarda el resultat JSON
8. Torna a HOME
"""
print(f"\n{'='*60}")
print(f" PR5071/03 — Integració Visió + Robòtica")
print(f" Alumne: {ALUMNE}")
print(f" Robot: {IP_ROBOT}")
print(f" Objecte diana: '{OBJECTE_DIANA}'")
print(f"{'='*60}\n")
# ── Pas 1: Connectar al robot ─────────────────────────────────────────────
print("[PAS 1] Connectant al robot...")
try:
robot_ctrl = rtde_control.RTDEControlInterface(IP_ROBOT)
robot_recv = rtde_receive.RTDEReceiveInterface(IP_ROBOT)
print("[OK] Robot connectat")
except Exception as e:
print(f"[ERROR CRÍTIC] No es pot connectar al robot: {e}")
print(" → Verifica que URSim (o el robot) estigui en Remote Control")
return
try:
# ── Pas 2: Moure a HOME ───────────────────────────────────────────────
print("\n[PAS 2] Movent a posició HOME...")
posicio_home = POSICIO_HOME = [0.300, 0.000, 0.300] + ORIENTACIO_TCP
robot_ctrl.moveL(posicio_home, VELOCITAT_ROBOT, ACCELERACIO_ROBOT)
time.sleep(0.5)
print("[OK] Robot a HOME")
# ── Pas 3: Inicialitzar càmera i detectar ─────────────────────────────
print("\n[PAS 3] Inicialitzant càmera OAK-D i detectant objecte...")
pipeline = crear_pipeline_deteccio()
with dai.Device(pipeline) as dispositiu:
deteccio = detectar_objecte_diana(dispositiu, max_intents=90)
if deteccio is None:
print(f"\n[AVÍS] No s'ha detectat '{OBJECTE_DIANA}'. Aturant el pipeline.")
return
print(f"\n[OK] Objecte detectat: {deteccio['classe']} "
f"(confiança: {deteccio['confianca']:.3f})")
# ── Pas 4: Transformar coordenades ────────────────────────────────────
print("\n[PAS 4] Transformant coordenades càmera → robot...")
pos_cam = np.array([
deteccio["x_cam_mm"],
deteccio["y_cam_mm"],
deteccio["z_cam_mm"],
])
pos_robot = transformar_cam_a_robot(pos_cam)
print(f" Càmera: X={pos_cam[0]:.1f}mm Y={pos_cam[1]:.1f}mm Z={pos_cam[2]:.1f}mm")
print(f" Robot: X={pos_robot[0]:.4f}m Y={pos_robot[1]:.4f}m Z={pos_robot[2]:.4f}m")
# ── Pas 5: Verificació de seguretat i moviment ────────────────────────
print("\n[PAS 5] Verificació de seguretat i moviment cap a l'objectiu...")
# Afegir una petita alçada per no xocar amb l'objecte directament
pos_objectiu = [
pos_robot[0],
pos_robot[1],
pos_robot[2] + 0.05, # 5 cm per sobre de l'objecte
] + ORIENTACIO_TCP
if not verificar_posicio_segura(np.array(pos_objectiu[:3])):
print("[AVÍS] Posició fora de rang. Moviment cancel·lat.")
return
# Moviment en dues fases: aproximació + descens
pos_aprox = pos_objectiu.copy()
pos_aprox[2] += 0.10 # 10 cm per sobre per a la fase d'aproximació
print(" → Fase d'aproximació...")
robot_ctrl.moveL(pos_aprox, VELOCITAT_ROBOT, ACCELERACIO_ROBOT)
time.sleep(0.3)
print(" → Descens fins a l'objectiu...")
robot_ctrl.moveL(pos_objectiu, VELOCITAT_ROBOT * 0.5, ACCELERACIO_ROBOT * 0.5)
time.sleep(0.5)
# Llegir la posició TCP final real
posicio_tcp_final = list(robot_recv.getActualTCPPose())
print(f"[OK] Posició TCP final: {[f'{v:.4f}' for v in posicio_tcp_final]}")
# ── Pas 6: Guardar resultat ───────────────────────────────────────────
print("\n[PAS 6] Guardant resultat...")
fitxer = guardar_resultat(deteccio, pos_cam, pos_robot, posicio_tcp_final)
print(f"[OK] Resultat guardat a: {fitxer}")
# ── Pas 7: Tornar a HOME ──────────────────────────────────────────────
print("\n[PAS 7] Tornant a posició HOME...")
time.sleep(0.5)
robot_ctrl.moveL(posicio_home, VELOCITAT_ROBOT, ACCELERACIO_ROBOT)
print("[OK] Robot a HOME. Pipeline completat!")
finally:
# Tancar la connexió RTDE de forma segura
robot_ctrl.stopScript()
print("\n[INFO] Connexió RTDE tancada correctament.")
if __name__ == "__main__":
executar_pipeline_complet()
Estructura del fitxer de resultat
El fitxer resultat_nom-cognom.json tindrà aquesta estructura:
{
"metadades": {
"alumne": "nom-cognom",
"practica": "PR5071/03",
"modul": "5071",
"timestamp": "2025-03-15T10:42:37.123456"
},
"deteccio": {
"classe": "bottle",
"confianca": 0.847,
"coordenades_camera_mm": { "x": 25.3, "y": 91.7, "z": 548.2 }
},
"transformacio": {
"coordenades_robot_m": { "x": 0.3721, "y": 0.0891, "z": 0.0523 },
"matriu_T_cam2robot": [[...], [...], [...], [...]]
},
"robot": {
"ip": "192.168.56.101",
"posicio_tcp_final": [0.3721, 0.0891, 0.1023, 3.1416, 0.0, 0.0],
"velocitat_ms": 0.08,
"acceleracio_ms2": 0.04
}
}
Preguntes de reflexió
Preguntes de reflexió (inclou les respostes a la memòria)
1. Fonts d'error en la calibració mà-ull
Enumera almenys tres fonts d'error que poden afectar la precisió de la matriu de transformació T_cam2robot en el mètode simplificat de 3 punts. Com podries reduir-ne l'impacte? Considera tant errors de mesura com errors sistemàtics del mètode.
2. Seguretat: robot industrial vs. cobot
Explica les diferències principals de seguretat entre un robot industrial tradicional (com l'ABB IRB 6700) i un cobot com el UR3e. Per quines raons el UR3e és adequat per treballar sense gàbia de protecció? Quins riscos residuals existeixen igualment?
3. Profunditat estèreo vs. càmera única
Per quina raó s'usa una càmera estèreo (com la OAK-D) en lloc d'una càmera monocular per obtenir coordenades 3D en aplicacions de robòtica? Quins avantatges ofereix el sistema estèreo integrat de la OAK-D respecte a l'ús de dues càmeres separades calibrades manualment?
4. Llindar de confiança de la detecció
Descriu el comportament del sistema si el llindar de confiança CONFIANCA_MINIMA es fixa molt baix (0.1) o molt alt (0.95). Quin compromís cal fer? Com implementaries una lògica que s'adapti automàticament al nivell de llum ambient del laboratori?
5. Extensió per a pick-and-place amb pinça
El pipeline actual porta el robot fins a l'objecte però no el recull. Descriu els passos addicionals necessaris per implementar una seqüència completa de pick-and-place amb una pinça (per exemple una Robotiq 2F-85). Quines accions calen abans d'agafar l'objecte? I per deixar-lo en una posició de destí?
Ampliació (AP5071/03)
Si has completat la pràctica i vols aprofundir, et proposem les extensions següents:
-
Control de pinça Robotiq: Integra una pinça Robotiq 2F-85 via el protocol RTDE o el paquet
robotiq_gripper. Implementa la seqüència completa agafar → transportar → deixar. -
Seqüència completa pick-and-place: Defineix dues zones de l'espai de treball (origen i destí). El robot ha de detectar objectes a la zona d'origen, agafar-los un per un i deixar-los a la zona de destí. Comptabilitza quants objectes s'han processats i guarda-ho al JSON.
-
Calibració amb marcadors ArUco: Substitueix el mètode de 3 punts per una calibració basada en marcadors ArUco (OpenCV
arucomodule). Col·loca un tauler de marcadors a l'espai de treball i usa la seva posició detectada per calcular la transformació. Compara el resultat (error RMS) amb el mètode simplificat. -
Zona de seguretat dinàmica: Implementa una verificació que comprovi en temps real si cap persona s'apropa a l'espai de treball (detecta "person" amb YOLOv8) i aturi el robot si es detecta una presència humana a menys de 800mm del robot.
Consulta la Rúbrica PR5071/03