agrobot_base/Python/OAK/spatial_depth_tracker.py

316 lines
9.5 KiB
Python

import cv2
import depthai as dai
import numpy as np
import time
import math
# =========================
# CONFIGURAÇÕES
# =========================
MIN_DEPTH_MM = 300
MAX_DEPTH_MM = 3500
DANGER_DEPTH_MM = 1600
MIN_AREA_PX = 250
MAX_LOST_FRAMES = 10
TRACK_MAX_DIST = 80
ROI_TOP = 0.25
ROI_BOTTOM = 0.95
ROI_LEFT = 0.15
ROI_RIGHT = 0.85
# Quanto o pixel precisa ser "mais perto" que o chão esperado
# para ser considerado saliência/obstáculo.
GROUND_DELTA_MM = 220
# Confirmação temporal simples
MIN_TRACK_AGE_FOR_DANGER = 2
class SimpleBlobTracker:
def __init__(self):
self.next_id = 1
self.tracks = {}
def update(self, detections):
updated = []
used_tracks = set()
for det in detections:
best_id = None
best_dist = 999999
for tid, tr in self.tracks.items():
if tid in used_tracks:
continue
dx = det["cx"] - tr["cx"]
dy = det["cy"] - tr["cy"]
dz = (det["z_mm"] - tr["z_mm"]) / 30.0
dist = math.sqrt(dx * dx + dy * dy + dz * dz)
if dist < best_dist:
best_dist = dist
best_id = tid
if best_id is not None and best_dist < TRACK_MAX_DIST:
tid = best_id
used_tracks.add(tid)
self.tracks[tid].update(det)
self.tracks[tid]["lost"] = 0
self.tracks[tid]["age"] += 1
else:
tid = self.next_id
self.next_id += 1
self.tracks[tid] = dict(det)
self.tracks[tid]["lost"] = 0
self.tracks[tid]["age"] = 1
out = dict(self.tracks[tid])
out["id"] = tid
updated.append(out)
for tid in list(self.tracks.keys()):
if tid not in used_tracks and all(d.get("id") != tid for d in updated):
self.tracks[tid]["lost"] += 1
if self.tracks[tid]["lost"] > MAX_LOST_FRAMES:
del self.tracks[tid]
return updated
def build_ground_model_by_row(roi_depth):
"""
Estima a profundidade esperada do chão em cada linha da ROI.
Usa percentil alto, porque o chão costuma ser a superfície mais distante
dentro da linha quando há obstáculos mais próximos.
"""
rh, rw = roi_depth.shape
ground = np.zeros(rh, dtype=np.float32)
for y in range(rh):
row = roi_depth[y, :]
valid = row[(row > MIN_DEPTH_MM) & (row < MAX_DEPTH_MM)]
if len(valid) < 20:
ground[y] = np.nan
else:
ground[y] = np.percentile(valid, 75)
# Interpola linhas inválidas
idx = np.arange(rh)
good = np.isfinite(ground)
if np.count_nonzero(good) < 5:
return None
ground = np.interp(idx, idx[good], ground[good])
# Suaviza o perfil do chão
ground = cv2.GaussianBlur(ground.reshape(-1, 1), (1, 31), 0).reshape(-1)
return ground
# =========================
# PIPELINE OAK-D LITE
# =========================
pipeline = dai.Pipeline()
cam_rgb = pipeline.create(dai.node.ColorCamera)
cam_rgb.setPreviewSize(640, 400)
cam_rgb.setResolution(dai.ColorCameraProperties.SensorResolution.THE_1080_P)
cam_rgb.setInterleaved(False)
cam_rgb.setColorOrder(dai.ColorCameraProperties.ColorOrder.BGR)
cam_rgb.setFps(30)
mono_left = pipeline.create(dai.node.MonoCamera)
mono_right = pipeline.create(dai.node.MonoCamera)
mono_left.setResolution(dai.MonoCameraProperties.SensorResolution.THE_400_P)
mono_right.setResolution(dai.MonoCameraProperties.SensorResolution.THE_400_P)
mono_left.setBoardSocket(dai.CameraBoardSocket.CAM_B)
mono_right.setBoardSocket(dai.CameraBoardSocket.CAM_C)
stereo = pipeline.create(dai.node.StereoDepth)
stereo.setDefaultProfilePreset(dai.node.StereoDepth.PresetMode.DEFAULT)
stereo.setDepthAlign(dai.CameraBoardSocket.CAM_A)
stereo.setSubpixel(True)
stereo.setLeftRightCheck(True)
mono_left.out.link(stereo.left)
mono_right.out.link(stereo.right)
xout_rgb = pipeline.create(dai.node.XLinkOut)
xout_depth = pipeline.create(dai.node.XLinkOut)
xout_rgb.setStreamName("rgb")
xout_depth.setStreamName("depth")
cam_rgb.preview.link(xout_rgb.input)
stereo.depth.link(xout_depth.input)
tracker = SimpleBlobTracker()
with dai.Device(pipeline) as device:
q_rgb = device.getOutputQueue("rgb", maxSize=1, blocking=True)
q_depth = device.getOutputQueue("depth", maxSize=1, blocking=True)
last = time.time()
fps = 0
while True:
frame = q_rgb.get().getCvFrame()
depth = q_depth.get().getFrame()
h, w = frame.shape[:2]
if depth.shape[:2] != (h, w):
depth = cv2.resize(depth, (w, h), interpolation=cv2.INTER_NEAREST)
now = time.time()
fps = 0.9 * fps + 0.1 * (1 / max(now - last, 1e-6))
last = now
x1_roi = int(w * ROI_LEFT)
x2_roi = int(w * ROI_RIGHT)
y1_roi = int(h * ROI_TOP)
y2_roi = int(h * ROI_BOTTOM)
roi_depth = depth[y1_roi:y2_roi, x1_roi:x2_roi]
rh, rw = roi_depth.shape
ground = build_ground_model_by_row(roi_depth)
if ground is None:
obstacle_mask = np.zeros_like(roi_depth, dtype=np.uint8)
ground_vis = np.zeros_like(roi_depth, dtype=np.uint8)
detections = []
else:
ground_2d = np.repeat(ground[:, None], rw, axis=1)
valid_mask = (
(roi_depth > MIN_DEPTH_MM) &
(roi_depth < MAX_DEPTH_MM)
)
# Obstáculo = pixel válido significativamente mais perto que o chão esperado
diff = ground_2d - roi_depth
obstacle_mask = np.zeros_like(roi_depth, dtype=np.uint8)
obstacle_mask[(valid_mask) & (diff > GROUND_DELTA_MM)] = 255
kernel = np.ones((5, 5), np.uint8)
obstacle_mask = cv2.morphologyEx(obstacle_mask, cv2.MORPH_OPEN, kernel)
obstacle_mask = cv2.morphologyEx(obstacle_mask, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(
obstacle_mask,
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE
)
detections = []
for cnt in contours:
area = cv2.contourArea(cnt)
if area < MIN_AREA_PX:
continue
x, y, bw, bh = cv2.boundingRect(cnt)
gx = x + x1_roi
gy = y + y1_roi
gcx = gx + bw // 2
gcy = gy + bh // 2
blob_depth = roi_depth[y:y + bh, x:x + bw]
valid = blob_depth[
(blob_depth > MIN_DEPTH_MM) &
(blob_depth < MAX_DEPTH_MM)
]
if len(valid) < 50:
continue
z_mm = float(np.median(valid))
saliencia_mm = float(np.median(diff[y:y + bh, x:x + bw][obstacle_mask[y:y + bh, x:x + bw] > 0]))
detections.append({
"bbox": (gx, gy, gx + bw, gy + bh),
"cx": gcx,
"cy": gcy,
"z_mm": z_mm,
"area": area,
"saliencia_mm": saliencia_mm
})
ground_vis = np.clip(diff, 0, 800)
ground_vis = (ground_vis / 800.0 * 255).astype(np.uint8)
ground_vis = cv2.applyColorMap(ground_vis, cv2.COLORMAP_JET)
tracked = tracker.update(detections)
perigo = False
cv2.rectangle(frame, (x1_roi, y1_roi), (x2_roi, y2_roi), (255, 255, 0), 1)
for obj in tracked:
x1, y1, x2, y2 = obj["bbox"]
z_m = obj["z_mm"] / 1000.0
center_percent = obj["cx"] / w * 100.0
sal = obj.get("saliencia_mm", 0)
danger = (
obj["z_mm"] < DANGER_DEPTH_MM and
obj["age"] >= MIN_TRACK_AGE_FOR_DANGER
)
if danger:
perigo = True
color = (0, 0, 255) if danger else (0, 255, 0)
texto = (
f"ID {obj['id']} | Z={z_m:.2f}m | "
f"X={center_percent:.0f}% | sal={sal:.0f}mm | age={obj['age']}"
)
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
cv2.circle(frame, (obj["cx"], obj["cy"]), 4, (0, 255, 255), -1)
cv2.putText(frame, texto, (x1, max(20, y1 - 8)),
cv2.FONT_HERSHEY_SIMPLEX, 0.42, color, 1)
cv2.putText(frame, f"FPS: {fps:.1f}", (10, 25),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
cv2.putText(frame, f"Obstaculos: {len(tracked)} | Ground delta: {GROUND_DELTA_MM}mm",
(10, 52), cv2.FONT_HERSHEY_SIMPLEX, 0.55,
(255, 255, 255), 1)
if perigo:
cv2.putText(frame, "PERIGO: saliencia no caminho - PARAR", (10, 82),
cv2.FONT_HERSHEY_SIMPLEX, 0.75, (0, 0, 255), 2)
depth_vis = depth.copy()
depth_vis[depth_vis == 0] = MAX_DEPTH_MM
depth_vis = np.clip(depth_vis, MIN_DEPTH_MM, MAX_DEPTH_MM)
depth_vis = ((MAX_DEPTH_MM - depth_vis) / (MAX_DEPTH_MM - MIN_DEPTH_MM) * 255).astype(np.uint8)
depth_vis = cv2.applyColorMap(depth_vis, cv2.COLORMAP_JET)
cv2.imshow("OAK-D Lite - Ground Obstacle Tracker", frame)
cv2.imshow("Depth view", depth_vis)
cv2.imshow("Obstacle mask", obstacle_mask)
cv2.imshow("Ground diff / saliencia", ground_vis)
key = cv2.waitKey(1)
if key == ord("q") or key == 27:
break
cv2.destroyAllWindows()