文章目录
项目介绍与效果展示
最近在做基于光电球机低空无人机智能追踪项目,项目功能主要分为两种模型:主动跟踪模式与被动跟踪模式。主动跟踪模式为:视觉拉流探测发现 → AI 检测与锁定 → 伺服转台自动跟踪 → 保持无人机在画面中;被动模式为:接受Remote ID计算PTZ→ 伺服转台调节至相应的PTZ→ 保持无人机在画面中。
下面为主动跟踪效果:

下面为被动跟踪效果:

借此机会记录下实现过程。
一、主动跟踪模式
主动模式流程框图

云台部分控制代码
本项目采用的是海康的球机,所以本文记录球机控制方式为海康的SDK调用方式。
python
# ==================== PTZ Controller ====================
class PTZController:
"""Hikvision PTZ controller - Pulse Control Version"""
# PTZ Commands
TILT_UP = 21
TILT_DOWN = 22
PAN_LEFT = 23
PAN_RIGHT = 24
UP_LEFT = 25
UP_RIGHT = 26
DOWN_LEFT = 27
DOWN_RIGHT = 28
ZOOM_IN = 11
ZOOM_OUT = 12
def __init__(self, ip: str, port: int, username: str, password: str):
self.ip = ip
self.port = port
self.username = username
self.password = password
self.sdk = None
self.user_id = -1
self.connected = False
self.command_queue = queue.Queue(maxsize=1)
self.running = False
self.worker_thread = None
def connect(self) -> bool:
try:
self.sdk = ctypes.CDLL(r'./SDK/lib/win/HCNetSDK.dll')
self.sdk.NET_DVR_Init()
class LoginInfo(ctypes.Structure):
_fields_ = [
("sDeviceAddress", ctypes.c_char * 129),
("byUseTransport", ctypes.c_byte),
("wPort", ctypes.c_uint16),
("sUserName", ctypes.c_char * 64),
("sPassword", ctypes.c_char * 64),
("cbLoginResult", ctypes.c_void_p),
("pUser", ctypes.c_void_p),
("bUseAsynLogin", ctypes.c_uint32),
("byProxyType", ctypes.c_byte),
("byUseUTCTime", ctypes.c_byte),
("byLoginMode", ctypes.c_byte),
("byHttps", ctypes.c_byte),
("iProxyID", ctypes.c_uint32),
("byVerifyMode", ctypes.c_byte),
("byRes2", ctypes.c_byte * 119)
]
class DeviceInfo(ctypes.Structure):
_fields_ = [("byRes", ctypes.c_byte * 512)]
login_info = LoginInfo()
login_info.sDeviceAddress = self.ip.encode()
login_info.wPort = self.port
login_info.sUserName = self.username.encode()
login_info.sPassword = self.password.encode()
device_info = DeviceInfo()
self.user_id = self.sdk.NET_DVR_Login_V40(
ctypes.byref(login_info), ctypes.byref(device_info)
)
if self.user_id < 0:
print("PTZ登录失败")
return False
self.connected = True
print("PTZ连接成功")
return True
except Exception as e:
print("PTZ连接异常:", e)
return False
def start_worker(self, frame_width, frame_height):
self.center_x = frame_width // 2
self.center_y = frame_height // 2
# ⭐ 调优参数
self.dead_zone = 100
# PTZ执行时间
self.pulse_duration = 0.1
# PTZ执行频率
self.control_interval = 0.1
self.running = True
self.worker_thread = threading.Thread(target=self._worker_loop, daemon=True)
self.worker_thread.start()
def _ptz_pulse(self, command, speed):
"""核心:脉冲控制"""
if not self.connected:
return
self.sdk.NET_DVR_PTZControlWithSpeed_Other(
self.user_id, 1, command, 0, speed
)
time.sleep(self.pulse_duration)
self.sdk.NET_DVR_PTZControlWithSpeed_Other(
self.user_id, 1, command, 1, speed
)
def _worker_loop(self):
last_control_time = 0
while self.running:
try:
target = self.command_queue.get_nowait()
except queue.Empty:
time.sleep(0.01)
continue
if target is None:
continue
# 控制频率限制(防抖)
if time.time() - last_control_time < self.control_interval:
continue
last_control_time = time.time()
x, y, w, h = target
cx = x + w // 2
cy = y + h // 2
dx = cx - self.center_x
dy = cy - self.center_y
# 死区
if abs(dx) < self.dead_zone and abs(dy) < self.dead_zone:
continue
# ⭐ 动态速度(远快近慢)
offset = max(abs(dx), abs(dy))
speed = min(5, max(1, int(offset / 120)))
command = None
if dx < -self.dead_zone and dy < -self.dead_zone:
command = self.UP_LEFT
elif dx > self.dead_zone and dy < -self.dead_zone:
command = self.UP_RIGHT
elif dx < -self.dead_zone and dy > self.dead_zone:
command = self.DOWN_LEFT
elif dx > self.dead_zone and dy > self.dead_zone:
command = self.DOWN_RIGHT
elif dx < -self.dead_zone:
command = self.PAN_LEFT
elif dx > self.dead_zone:
command = self.PAN_RIGHT
elif dy < -self.dead_zone:
command = self.TILT_UP
elif dy > self.dead_zone:
command = self.TILT_DOWN
if command:
self._ptz_pulse(command, speed)
def update_target(self, bbox):
try:
if self.command_queue.full():
self.command_queue.get_nowait()
self.command_queue.put(bbox, block=False)
except:
pass
def stop_tracking(self):
try:
self.command_queue.put(None, block=False)
except:
pass
def disconnect(self):
self.running = False
if self.worker_thread:
self.worker_thread.join(timeout=1)
if self.sdk and self.user_id >= 0:
self.sdk.NET_DVR_Logout(self.user_id)
self.sdk.NET_DVR_Cleanup()
print("PTZ已断开")
技术细节
偏移量计算:
获取当前图像尺寸。
计算目标中心点坐标:
python
x, y, w, h = target
cx = x + w // 2
cy = y + h // 2
计算与图像中心的偏移量:
python
dx = cx - self.center_x
dy = cy - self.center_y
死区设置与转动速度:
python
# 死区
if abs(dx) < self.dead_zone and abs(dy) < self.dead_zone:
continue
# ⭐ 动态速度(远快近慢)
offset = max(abs(dx), abs(dy))
speed = min(5, max(1, int(offset / 120)))
在死区范围内则不控制云台转动,否则云台跟随目标运动方向进行转动。
二、被动跟踪模式
被动模式流程框图

云台部分控制代码
python
# -*- coding: utf-8 -*-
import json
import paho.mqtt.client as mqtt
from utils.calculate_relative_position import calculate_relative_position
from SDK.move_ptz_absolute import move_ptz_by_degree
import math
# MQTT配置
MQTT_BROKER = "192.168.1.130" # 修改为实际的MQTT服务器地址
MQTT_PORT = 1883
MQTT_USERNAME = None # 如果需要认证
MQTT_PASSWORD = None
MQTT_TOPIC = "edge/device/optoelect_sensor/test/track_cmd"
# 云台自身的位置(固定)
# LAT1 = 32.029722
# LON1 = 118.699722
# ALT1 = 2.2
LAT1 = 32.029758
LON1 = 118.699892
ALT1 = 2.2
# 基准方位
# P0 = 37.89203241
P0 = 34.08187643
# 全局变量,用于跟踪当前云台位置
current_pan_deg = 0
current_tilt_deg = 0
current_zoom = 1
def calculate_pitch(U, distH):
# if distH == 0:
# return 90.0 if U >= 0 else -90.0
# 返回弧度
rad = math.atan(U / distH)
# 如需角度:deg = math.degrees(rad)
deg = math.degrees(rad)
return deg
def on_connect(client, userdata, flags, rc):
"""MQTT连接成功回调"""
if rc == 0:
print("MQTT连接成功")
client.subscribe(MQTT_TOPIC)
print(f"已订阅主题: {MQTT_TOPIC}")
else:
print(f"连接失败,返回码: {rc}")
def on_message(client, userdata, msg):
"""MQTT消息接收回调"""
global current_pan_deg, current_tilt_deg, current_zoom
try:
# 解析JSON消息
payload = msg.payload.decode('utf-8')
data = json.loads(payload)
print(f"收到消息: {data}")
# 从消息中提取目标位置
lat2 = data.get("lat")
lon2 = data.get("lng") # 注意:消息中是lng而不是lon
alt2 = data.get("alt")
identifier = data.get("identifier", "unknown")
if lat2 is None or lon2 is None or alt2 is None:
print("错误: 消息中缺少位置信息")
return
print(f"目标位置: 纬度={lat2}, 经度={lon2}, 高度={alt2}")
print(f"云台位置: 纬度={LAT1}, 经度={LON1}, 高度={ALT1}")
# 计算相对位置
results, az, U, distH = calculate_relative_position(LAT1, LON1, ALT1, lat2, lon2, alt2)
# 计算俯仰角
deg = calculate_pitch(U, distH)
print("deg:", deg)
tilt = int(deg * 10)
tilt_str = str(tilt)
tilt_deg = int(tilt_str, 16)
print("tilt_deg", tilt_deg)
# 计算云台角度
wPanPos = az - P0
if wPanPos < 0:
wPanPos += 360
print(f"计算出的方位角: {az}°")
print(f"调整后云台角度: {wPanPos}°")
# 球机倒置安装
wPanPos = 360 - wPanPos
# 步骤1:乘以10,还原为SDK原始整数
wPanPos_int = int(wPanPos * 10) # 3000
# 还原hex字符串
hex_str = str(wPanPos_int)
# 用正确基数解析
hex_digits = int(hex_str, 16) if hex_str.isdigit() else 0
print(f"原始角度: {wPanPos}°")
print(f"乘以10后的整数: {wPanPos_int}")
print(f"16进制字符串: {hex_str}")
print(f"16进制数字部分: {hex_digits}")
# 控制云台转动
# 注意:这里只转动了水平方向,俯仰角度保持当前值
# 你可以根据消息中的elevation字段调整俯仰角度
# elevation = data.get("elevation", 0)
# 如果消息中有俯仰角度,更新tilt_deg
# tilt_deg = int(elevation * 10) if elevation != 0 else current_tilt_deg
# 如果消息中有距离信息,可以调整zoom
dist = data.get("dist", 0)
if dist > 0:
# 根据距离调整变焦倍数,这里需要根据实际情况调整
new_zoom = min(max(int(dist / 100), 1), 16) # 简单的距离-变焦映射
if new_zoom != current_zoom:
current_zoom = new_zoom
print(f"根据距离调整变焦: {current_zoom}")
success, response = move_ptz_by_degree(
ip="192.168.1.40",
port=8000,
username="admin",
password="admin2026",
channel=1,
pan_deg=hex_digits,
# tilt_deg=tilt_deg,
# zoom=current_zoom,
tilt_deg=tilt_deg,
zoom=16,
wait_time=2
)
if success:
# 更新当前云台位置
current_pan_deg = hex_digits
current_tilt_deg = tilt_deg
print(f"云台控制成功: 目标{identifier}")
else:
print(f"云台控制失败: {response}")
except json.JSONDecodeError as e:
print(f"JSON解析错误: {e}")
except Exception as e:
print(f"处理消息时出错: {e}")
以上代码就可以实现把球机的经纬高转成球机所对应的PT,Z值在代码中没有变化,可以通过距离写一个映射关系表获得对应的Z值。
总结
本文介绍了基于光电球机低空无人机智能追踪系统。系统结合人工智能视觉检测、PTZ云台控制、Remote ID定位解析以及MQTT通信技术,实现了无人机目标的主动发现、被动定位和自动跟踪。后续优化方向可以从主动+被动融合入手:Remote ID提供目标初始位置,AI视觉进行精准锁定,实现更稳定的无人机持续跟踪。
参考文档:
https://blog.csdn.net/zhanghui3239619/article/details/160966974
