2026.6.3小更新

This commit is contained in:
2026-06-03 16:36:02 +08:00
parent 5294cd3859
commit 1cc194185b
3 changed files with 445 additions and 125 deletions
+296
View File
@@ -0,0 +1,296 @@
import os
import cv2
import numpy as np
import time
import threading
import queue
import socket
import json
import sys
from ais_bench.infer.interface import InferSession
# ================== 导入基础控制与状态机模块 =====================
from drone_controller.base_control import DroneController
from drone_controller.pid_counter import PID
from drone_controller.mission_manager import MissionManager, MissionState, FailsafeTriggered
# ===================================================================
class VideoStreaming:
""" 📡 视频流采集与后台图传模块 """
def __init__(self, video_cfg, net_cfg):
self.video_source = video_cfg.get("video_source", 0)
self.jpeg_quality = video_cfg.get("jpeg_quality", 75)
self.gs_ip = net_cfg.get("ground_station_ip", "127.0.0.1")
self.udp_port = net_cfg.get("udp_port", 9999)
self.cap = cv2.VideoCapture(self.video_source)
self.tx_queue = queue.Queue(maxsize=2)
self.is_running = True
if not self.cap.isOpened():
raise RuntimeError(f"❌ 无法打开视频源: {self.video_source}")
self.sender_thread = threading.Thread(target=self._udp_stream_sender_worker, daemon=True)
self.sender_thread.start()
def read_frame(self):
return self.cap.read()
def push_frame(self, frame):
if not self.tx_queue.full():
self.tx_queue.put(frame.copy())
def _udp_stream_sender_worker(self):
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
dest_addr = (self.gs_ip, self.udp_port)
print(f"📡 [图传后台] UDP 推流服务已启动,目标地面站: {self.gs_ip}:{self.udp_port}")
while self.is_running:
try:
frame = self.tx_queue.get(timeout=1.0)
ret, jpeg_buf = cv2.imencode('.jpg', frame, [int(cv2.IMWRITE_JPEG_QUALITY), self.jpeg_quality])
if not ret:
continue
data = jpeg_buf.tobytes()
packet_size = 60000
total_size = len(data)
for i in range(0, total_size, packet_size):
udp_socket.sendto(data[i:i + packet_size], dest_addr)
except queue.Empty:
continue
except Exception as e:
print(f"⚠️ [图传异常] 发送失败: {e}")
udp_socket.close()
def release(self):
self.is_running = False
if self.cap.isOpened():
self.cap.release()
class YOLO26UAVInfer:
""" 🧠 昇腾 NPU YOLOv8 极致推理模块 """
def __init__(self, model_cfg):
self.model_path = model_cfg.get("model_path", "./om/yolo26n-balloon.om")
self.conf_threshold = model_cfg.get("conf_threshold", 0.25)
device_id = model_cfg.get("device_id", 0)
print(f"🧠 [NPU 初始化] 正在载入昇腾 OM 模型: {self.model_path} ...")
self.session = InferSession(device_id, self.model_path)
def preprocess(self, img, img_size=640):
h, w = img.shape[:2]
r = min(img_size / h, img_size / w)
unpad_h, unpad_w = int(round(h * r)), int(round(w * r))
dw, dh = img_size - unpad_w, img_size - unpad_h
dw /= 2
dh /= 2
if (w, h) != (unpad_w, unpad_h):
img_resized = cv2.resize(img, (unpad_w, unpad_h), interpolation=cv2.INTER_LINEAR)
else:
img_resized = img
top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
img_padded = cv2.copyMakeBorder(img_resized, top, bottom, left, right, cv2.BORDER_CONSTANT,
value=(114, 114, 114))
img_in = img_padded.transpose((2, 0, 1))[::-1] # BGR to RGB
img_in = np.ascontiguousarray(img_in).astype(np.float32) / 255.0
return img_in, r, (dw, dh)
def postprocess(self, outputs, ratio, dwdh, orig_shape):
boxout = outputs[0][0]
box_num = outputs[1][0]
detections = []
for i in range(box_num):
box = boxout[i]
score = box[4]
cls_id = int(box[5])
if score < self.conf_threshold:
continue
x1, y1, x2, y2 = box[0], box[1], box[2], box[3]
x1 -= dwdh[0]
x2 -= dwdh[0]
y1 -= dwdh[1]
y2 -= dwdh[1]
x1, x2 = x1 / ratio, x2 / ratio
y1, y2 = y1 / ratio, y2 / ratio
x1 = max(0, min(x1, orig_shape[1]))
y1 = max(0, min(y1, orig_shape[0]))
x2 = max(0, min(x2, orig_shape[1]))
y2 = max(0, min(y2, orig_shape[0]))
detections.append({
"box": [int(x1), int(y1), int(x2), int(y2)],
"score": float(score),
"class": cls_id
})
return detections
def draw_boxes(self, frame, detections):
for det in detections:
x1, y1, x2, y2 = det["box"]
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.putText(frame, f"Target: {det['score']:.2f}", (x1, y1 - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
return frame
class UAVControlLoop:
""" ✈️ 融合型无人机控制闭环中心(深度适配状态机) """
def __init__(self, flight_cfg, pid_y_cfg, pid_z_cfg):
self.uav = DroneController(
connection_string=flight_cfg.get("connection_string", "/dev/ttyUSB0"),
baud=flight_cfg.get("baud_rate", 57600)
)
self.pid_y = PID(**pid_y_cfg)
self.pid_z = PID(**pid_z_cfg)
def start_uav(self):
print("⚡ [底层通信] 正在建立与飞控的 MAVLink 链路...")
if self.uav.connect():
print("✅ [底层通信] 飞控监听守护线程就绪。")
return True
print("❌ [底层通信] 串口独占失败或未连上。")
return False
def process_control(self, detections, orig_shape, frame, mission_state):
"""
根据当前状态机状态,决定是否发送闭环 PID 控制速度
"""
h, w = orig_shape
center_x_target, center_y_target = w / 2.0, h / 2.0
# 绘制画面正中央的十字瞄准准心
cv2.drawMarker(frame, (int(center_x_target), int(center_y_target)), (0, 0, 255), cv2.MARKER_CROSS, 20, 2)
# 判定是否包含有效检测目标
if len(detections) > 0:
best_det = max(detections, key=lambda x: x["score"])
x1, y1, x2, y2 = best_det["box"]
# 计算目标的像素中心点
obj_x = (x1 + x2) / 2.0
obj_y = (y1 + y2) / 2.0
# 绘制目标质点追踪轨迹
cv2.circle(frame, (int(obj_x), int(obj_y)), 5, (255, 0, 0), -1)
cv2.line(frame, (int(center_x_target), int(center_y_target)), (int(obj_x), int(obj_y)), (255, 255, 0), 2)
# 归一化百分比误差计算值
err_x = (obj_x - center_x_target) / center_x_target
err_y = (obj_y - center_y_target) / center_y_target
# 核心控制逻辑决策:只有当状态机真正进入 VISUAL_TRACKING 状态,香橙派才拥有物理速度控制权!
if mission_state == MissionState.VISUAL_TRACKING:
vy_cmd = self.pid_y.update(err_x)
vz_cmd = self.pid_z.update(err_y)
# 下发机体系速度:前房保持0(悬停对准),vy左右平移,vz上下爬升
self.uav.send_body_velocity(vx=0.0, vy=vy_cmd, vz=vz_cmd, yaw_rate=0.0)
else:
# 目标丢失兜底保护:如果在追踪状态下目标跟丢了,立即刹车原地悬停,并清除积分项
if mission_state == MissionState.VISUAL_TRACKING:
self.uav.send_body_velocity(0, 0, 0, 0)
self.pid_y.reset()
self.pid_z.reset()
def load_config(config_path="./config.json"):
with open(config_path, "r", encoding="utf-8") as f:
return json.load(f)
if __name__ == "__main__":
# 模拟外部定义的自动化航点列表(NED坐标系:北, 东, 地。Z 轴向上为负,所以高度 1.5 米设为 -1.5)
test_waypoints = [
{'x': 1.0, 'y': 0.0, 'z': -1.5, 'yaw': 0},
{'x': 2.0, 'y': 1.5, 'z': -1.5, 'yaw': 45},
{'x': 0.0, 'y': 2.0, 'z': -1.5, 'yaw': 90}
]
cfg = load_config()
# 实例化子系统
streamer = VideoStreaming(cfg["video"], cfg["network"])
detector = YOLO26UAVInfer(cfg["model"])
controller = UAVControlLoop(cfg["flight_control"], cfg["pid_y"], cfg["pid_z"])
# 启动无人机串口连接
if not controller.start_uav():
sys.exit("❌ 飞控连接失败,无法继续业务")
# 实例化复合状态机业务管理器,配置不发现目标时巡点悬停检索时间为 8 秒,飞完直接原地降落不返航
mission = MissionManager(
drone=controller.uav,
waypoints=test_waypoints,
target_altitude=1.5,
arrival_radius=0.3,
hold_duration=8.0,
return_to_home=False
)
print("🚀 NPU 精准推理 [航点巡航与视觉追踪融合版] 主业务循环全面启动...")
try:
while True:
ret, frame = streamer.read_frame()
if not ret:
print("❌ 未能读取到摄像头数据,退出中...")
break
orig_shape = frame.shape[:2]
# 1. 图像预处理与硬件 NPU 高速推理
input_tensor, ratio, dwdh = detector.preprocess(frame)
outputs = detector.session.infer([input_tensor])
# 2. 坐标反求与后处理
detections = detector.postprocess(outputs, ratio, dwdh, orig_shape)
# 判断当前帧是否捕捉到了目标
target_detected = len(detections) > 0
# 3. 驱动高层任务管理器状态机(传入视觉反馈)
try:
mission.update(target_detected=target_detected)
except FailsafeTriggered:
# 触发人肉拦截,跳出死循环,优雅关闭程序
break
# 4. 驱动底层视觉闭环速度计算(只有状态机进入 VISUAL_TRACKING 时才会真正发送速度指令)
controller.process_control(detections, orig_shape, frame, mission.state)
# 5. 地面站视频帧渲染绘制
frame = detector.draw_boxes(frame, detections)
# 在图传画面上实时叠加当前系统状态机状态
status_text = f"State: {mission.state.name} | WP: {mission.current_wp_index + 1}/{len(test_waypoints)}"
cv2.putText(frame, status_text, (20, 40), cv2.FONT_HERSHEY_COMPLEX, 0.6, (0, 255, 255), 2)
# 推送视频到后台队列传输至 PC
streamer.push_frame(frame)
# 限制主循环高频速度(30Hz 左右),防止高频过度占用 CPU
time.sleep(0.033)
except KeyboardInterrupt:
print("\n👋 接收到终端键盘中断指令,正在安全退出...")
finally:
# 优雅释放
streamer.release()
controller.uav.close()
print("🏁 系统资源已安全回收,程序退出。")