Files
opi-yolo/drone_controller/optical_flow_controller.py
2026-07-15 09:36:58 +08:00

482 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
光流法障碍检测 - 网格点追踪版(复位点不参与统计)
特性:
1. 全图均匀网格点,覆盖无盲区
2. 点漂移后硬复位(当前帧和上一帧均置为初始位置)
3. 复位点在该帧不参与任何统计(散度、平均光流等)
4. 分区(左/中/右)风险统计,仅基于有效运动点
5. 与 PID 和 MAVLink 无缝集成
"""
import cv2
import numpy as np
import time
from collections import deque
from typing import Optional, Dict, Any
# ==================== PID 部分 =====================
from dataclasses import dataclass
@dataclass
class PIDConfig:
"""PID 参数配置"""
kp: float
ki: float
kd: float
max_out: float
min_out: float
dt_default: float = 0.033
alpha: float = 0.2 # 微分滤波系数 (0 < alpha <= 1)
class ImprovedPID:
def __init__(self, config: PIDConfig):
# 参数校验
if config.max_out <= config.min_out:
raise ValueError("max_out must be greater than min_out")
if not (0 < config.alpha <= 1):
raise ValueError("alpha must be in (0, 1]")
self.cfg = config
self.last_err: float = 0.0
self.last_fb: float = 0.0
self.integral: float = 0.0
self.last_d: float = 0.0
self.last_time: float = time.perf_counter()
self.first_run: bool = True # 标记首次调用
def update(self, error: float, feedback: Optional[float] = None, dt: Optional[float] = None) -> float:
current_time = time.perf_counter()
# --- 首次调用处理:初始化状态,避免微分冲击 ---
if self.first_run:
self.first_run = False
self.last_err = error
if feedback is not None:
self.last_fb = feedback
self.last_time = current_time
# 首次仅返回比例项,微分和积分暂不参与(或也可返回0,视应用而定)
# 通常直接返回 P 项能让系统平滑起步
return self.cfg.kp * error
# 1. 计算或使用传入的 dt
if dt is None:
dt = current_time - self.last_time
if dt <= 0:
dt = self.cfg.dt_default
# 2. 比例项 (P)
p_out = self.cfg.kp * error
# 3. 微分项 (D) —— 微分先行 + 低通滤波
if feedback is not None:
# 对反馈微分,设定值变化不引入冲击
d_raw = (self.last_fb - feedback) / dt
self.last_fb = feedback
else:
d_raw = (error - self.last_err) / dt
self.last_d = self.cfg.alpha * d_raw + (1.0 - self.cfg.alpha) * self.last_d
d_out = self.cfg.kd * self.last_d
# 4. 积分项 (I) —— 先计算潜在值
potential_integral = self.integral + error * dt
i_out = self.cfg.ki * potential_integral
# 5. 总输出与抗积分饱和(基于积分方向判断)
output = p_out + i_out + d_out
if output > self.cfg.max_out:
output = self.cfg.max_out
# 如果积分项仍在推高输出(i_out > 0),则冻结积分
if i_out > 0:
pass # 不更新 self.integral
else:
self.integral = potential_integral
elif output < self.cfg.min_out:
output = self.cfg.min_out
# 如果积分项仍在拉低输出(i_out < 0),则冻结积分
if i_out < 0:
pass
else:
self.integral = potential_integral
else:
# 未饱和,正常更新积分
self.integral = potential_integral
# 6. 更新状态变量
self.last_err = error
self.last_time = current_time
return output
def reset(self) -> None:
"""重置所有状态,恢复到首次运行前的状态"""
self.last_err = 0.0
self.last_fb = 0.0
self.integral = 0.0
self.last_d = 0.0
self.last_time = time.perf_counter()
self.first_run = True
# =================================================
class OpticalFlowObstacleDetector:
"""网格点光流检测器,复位点不参与统计"""
def __init__(self, grid_step: int = 25, reset_threshold: float = 30.0):
"""
Args:
grid_step: 网格点间距(像素),值越小点越多
reset_threshold: 点偏离初始位置超过此值时复位(像素)
"""
self.grid_step = grid_step
self.reset_threshold = reset_threshold
# 光流跟踪参数
self.lk_params = dict(
winSize=(21, 21),
maxLevel=3,
criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 30, 0.01)
)
# 状态变量
self.old_gray = None
self.p0 = None # 当前点坐标 (N,1,2)
self.grid_initial = None # 初始网格坐标 (N,2)
self.frame_count = 0
# 分区统计历史(用于兜底)
self.last_zone_stats = None
self.flow_history = deque(maxlen=10)
def _generate_grid(self, img_w: int, img_h: int) -> np.ndarray:
"""生成网格点,返回 (N,2) 坐标数组"""
x = np.arange(self.grid_step // 2, img_w, self.grid_step, dtype=np.float32)
y = np.arange(self.grid_step // 2, img_h, self.grid_step, dtype=np.float32)
xv, yv = np.meshgrid(x, y)
points = np.stack((xv.ravel(), yv.ravel()), axis=1)
return points
def process_frame(self, frame):
"""
处理单帧,返回 (is_collision, flow_data)
更新 self.last_zone_stats(仅包含有效点)
"""
self.frame_count += 1
if frame is None:
return False, None
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
h, w = gray.shape
# --- 1. 初始化第一帧 ---
if self.old_gray is None:
self.old_gray = gray
grid_pts = self._generate_grid(w, h)
self.grid_initial = grid_pts.copy()
self.p0 = grid_pts.reshape(-1, 1, 2).astype(np.float32)
self.last_zone_stats = None
return False, None
# --- 2. 光流跟踪 ---
p1, st, err = cv2.calcOpticalFlowPyrLK(
self.old_gray, gray, self.p0, None, **self.lk_params
)
if p1 is None:
p1 = self.p0.copy()
st = np.ones(len(self.p0), dtype=np.uint8)
# 将跟踪结果展开为 (N,2)
p1_flat = p1.reshape(-1, 2)
p0_flat = self.p0.reshape(-1, 2)
st_flat = st.flatten()
# 初始化新的点数组(默认保持旧位置)
new_points = p0_flat.copy()
old_points = p0_flat.copy() # 上一帧位置,用于计算光流
# 成功跟踪的点,先更新位置
success_indices = np.where(st_flat == 1)[0]
if len(success_indices) > 0:
new_points[success_indices] = p1_flat[success_indices]
old_points[success_indices] = p0_flat[success_indices]
# --- 3. 复位逻辑与有效点掩码生成 ---
# 复位掩码:True 表示该点需要复位(跟踪失败或漂移过大)
reset_mask = np.zeros(len(new_points), dtype=bool)
# 3.1 跟踪失败的点直接复位
failed_indices = np.where(st_flat == 0)[0]
reset_mask[failed_indices] = True
# 3.2 检查成功点是否漂移过大
for idx in success_indices:
if np.linalg.norm(new_points[idx] - self.grid_initial[idx]) > self.reset_threshold:
reset_mask[idx] = True
# 执行复位(将当前和上一帧位置均设为初始位置)
if np.any(reset_mask):
new_points[reset_mask] = self.grid_initial[reset_mask]
old_points[reset_mask] = self.grid_initial[reset_mask]
# --- 4. 生成有效点掩码(未复位且跟踪成功) ---
# 有效点:跟踪成功且未复位
valid_mask = np.ones(len(new_points), dtype=bool)
valid_mask[reset_mask] = False
# --- 5. 更新状态 ---
self.p0 = new_points.reshape(-1, 1, 2).astype(np.float32)
self.old_gray = gray
# 如果有效点太少,不进行统计(但保留状态)
if np.sum(valid_mask) < 5:
# 保留上一帧统计值,或返回空
return False, None
# --- 6. 计算光流向量(仅针对有效点) ---
valid_old = old_points[valid_mask]
valid_new = new_points[valid_mask]
flow_vectors = valid_new - valid_old # (M,2)
mean_mag = np.mean(np.linalg.norm(flow_vectors, axis=1))
# --- 7. 分区统计(仅使用有效点) ---
zone_stats = self._compute_zone_stats(valid_old, flow_vectors, w, h)
self.last_zone_stats = zone_stats
# 整体散度(用于碰撞警告)
divergence = self._compute_divergence(valid_old, flow_vectors, w, h)
# 碰撞判断(保留阈值)
is_collision = False
if divergence > 0.8 and mean_mag > 1.0:
is_collision = True
elif mean_mag > 3.0 and len(valid_old) > 50:
if self._check_roi_expansion(valid_old, flow_vectors):
is_collision = True
# 历史记录
self.flow_history.append({
'mean_flow': np.mean(flow_vectors, axis=0),
'mean_mag': mean_mag,
'divergence': divergence,
'points': np.sum(valid_mask)
})
return is_collision, {
'points': np.sum(valid_mask),
'mean_mag': mean_mag,
'divergence': divergence,
}
# ---------- 辅助方法(与之前一致,但传入的点已过滤) ----------
def _compute_divergence(self, old_points, flow_vectors, img_w, img_h):
if len(old_points) < 5:
return 0.0
center = np.array([img_w / 2, img_h / 2])
dir_to_center = center - old_points
dir_to_center /= (np.linalg.norm(dir_to_center, axis=1, keepdims=True) + 1e-6)
radial_flow = np.sum(flow_vectors * dir_to_center, axis=1)
return np.mean(radial_flow)
def _compute_zone_stats(self, old_pts, flow_vecs, img_w, img_h):
left_bound = img_w / 3
right_bound = 2 * img_w / 3
zones = {'left': [], 'center': [], 'right': []}
for pt, vec in zip(old_pts, flow_vecs):
x = pt[0]
if x < left_bound:
zones['left'].append((pt, vec))
elif x < right_bound:
zones['center'].append((pt, vec))
else:
zones['right'].append((pt, vec))
stats = {}
for name, pts_vecs in zones.items():
if len(pts_vecs) < 3: # 阈值降低,因为点可能较少
stats[name] = {'mean_vec': np.zeros(2), 'divergence': 0.0, 'count': 0, 'risk': 0.0}
continue
old_pts_zone = np.array([p for p, v in pts_vecs])
flow_zone = np.array([v for p, v in pts_vecs])
div = self._compute_divergence(old_pts_zone, flow_zone, img_w, img_h)
stats[name] = {
'mean_vec': np.mean(flow_zone, axis=0),
'divergence': div,
'count': len(pts_vecs),
'risk': div if div > 0 else 0.0
}
return stats
def _check_roi_expansion(self, old_points, flow_vectors):
h, w = 480, 640
roi_center = np.array([w / 2, h / 2])
roi_mask = (np.abs(old_points[:, 0] - w / 2) < w / 4) & (np.abs(old_points[:, 1] - h / 2) < h / 4)
roi_points = old_points[roi_mask]
roi_flow = flow_vectors[roi_mask]
if len(roi_points) < 5:
return False
dir_vec = roi_points - roi_center
dir_unit = dir_vec / (np.linalg.norm(dir_vec, axis=1, keepdims=True) + 1e-6)
radial = np.sum(roi_flow * dir_unit, axis=1)
return np.mean(radial) > 0.5
def get_zone_decision(self, zone_stats=None, threshold_div=0.8):
"""根据分区风险生成控制建议(返回 yaw_rate, speed_scale)"""
if zone_stats is None:
zone_stats = self.last_zone_stats
if zone_stats is None:
return 0.0, 1.0
left = zone_stats['left']['risk']
center = zone_stats['center']['risk']
right = zone_stats['right']['risk']
max_r = max(left, center, right)
if max_r < 0.3:
return 0.0, 1.0
if center >= left and center >= right and center > threshold_div:
if max(left, right) > threshold_div * 0.6:
return 0.0, 0.0
else:
return 0.0, 0.3
if left > right + 0.2:
yaw = min(0.8, 0.3 + 0.5 * (left - right))
return yaw, 0.7
elif right > left + 0.2:
yaw = -min(0.8, 0.3 + 0.5 * (right - left))
return yaw, 0.7
else:
return 0.0, 0.5
# ==================== 控制器(与之前相同) ====================
class ObstacleAvoidanceController:
"""避障控制器,集成网格点光流检测器、PID 和 MAVLink 发送"""
def __init__(self,
camera_id: int = 0,
frame_width: int = 640,
frame_height: int = 480,
cruise_speed: float = 2.0,
grid_step: int = 25,
reset_threshold: float = 30.0,
yaw_pid_cfg: Optional[PIDConfig] = None,
speed_pid_cfg: Optional[PIDConfig] = None):
self.cap = cv2.VideoCapture(camera_id)
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, frame_width)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, frame_height)
self.cap.set(cv2.CAP_PROP_FPS, 30)
if not self.cap.isOpened():
raise RuntimeError("无法打开摄像头")
self.detector = OpticalFlowObstacleDetector(grid_step=grid_step, reset_threshold=reset_threshold)
self.cruise_speed = cruise_speed
if yaw_pid_cfg is None:
yaw_pid_cfg = PIDConfig(kp=1.2, ki=0.1, kd=0.05, max_out=0.8, min_out=-0.8)
if speed_pid_cfg is None:
speed_pid_cfg = PIDConfig(kp=0.8, ki=0.05, kd=0.0, max_out=1.0, min_out=0.0)
self.yaw_pid = ImprovedPID(yaw_pid_cfg)
self.speed_pid = ImprovedPID(speed_pid_cfg)
self.current_yaw_rate = 0.0
self.current_speed = 0.0
self.running = False
def update_feedback(self, yaw_rate: float, speed: float):
self.current_yaw_rate = yaw_rate
self.current_speed = speed
def run_iteration(self, mav_proxy, enable_pid: bool = True) -> Dict[str, Any]:
ret, frame = self.cap.read()
if not ret:
return {'success': False, 'error': '摄像头读取失败'}
is_collision, flow_data = self.detector.process_frame(frame)
yaw_setpoint, speed_scale = self.detector.get_zone_decision()
speed_setpoint = speed_scale * self.cruise_speed
if enable_pid:
yaw_error = yaw_setpoint - self.current_yaw_rate
speed_error = speed_setpoint - self.current_speed
yaw_output = self.yaw_pid.update(yaw_error, feedback=self.current_yaw_rate)
speed_output = self.speed_pid.update(speed_error, feedback=self.current_speed)
else:
yaw_output = yaw_setpoint
speed_output = speed_setpoint
# 调用您的 MAVLink 代理的 send_setpoint 方法
success, ack = mav_proxy.send_setpoint(
control_mode="VELOCITY",
vx=speed_output,
vy=0.0,
vz=0.0,
yaw_rate=yaw_output
)
return {
'success': success,
'ack': ack,
'is_collision': is_collision,
'yaw_setpoint': yaw_setpoint,
'speed_setpoint': speed_setpoint,
'yaw_output': yaw_output,
'speed_output': speed_output,
'points': flow_data['points'] if flow_data else 0,
}
def run_forever(self, mav_proxy, enable_pid: bool = True):
self.running = True
print("🔄 网格点光流避障(复位点不统计)已启动,按 'q' 退出")
while self.running:
result = self.run_iteration(mav_proxy, enable_pid)
if not result.get('success', False):
print("⚠️ 迭代失败:", result.get('error', ''))
continue
if result['is_collision']:
print("🚨 碰撞警告!")
# 显示画面(方便调试)
ret, frame = self.cap.read()
if ret:
cv2.imshow('Grid Optical Flow', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
self.stop()
break
def stop(self):
self.running = False
self.cap.release()
cv2.destroyAllWindows()
# ==================== 测试入口 ====================
if __name__ == "__main__":
# 模拟 MAVLink 代理(实际应替换为真实对象)
class MockMavProxy:
def send_setpoint(self, control_mode="VELOCITY", **kwargs):
print(f"📤 发送指令: {control_mode} {kwargs}")
return True, {"result": "ok"}
mav = MockMavProxy()
controller = ObstacleAvoidanceController(
camera_id=0,
grid_step=25,
reset_threshold=30.0,
cruise_speed=2.0
)
controller.update_feedback(0.0, 0.0) # 模拟初始反馈
try:
controller.run_forever(mav, enable_pid=True)
except KeyboardInterrupt:
controller.stop()
print("🛑 已退出")