基于光电球机低空无人机智能追踪系统

文章目录


项目介绍与效果展示

最近在做基于光电球机低空无人机智能追踪项目,项目功能主要分为两种模型:主动跟踪模式与被动跟踪模式。主动跟踪模式为:视觉拉流探测发现 → 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

相关推荐
小许同学记录成长11 小时前
QGIS二次开发技术文档
图像处理·qt·信息可视化·无人机
GrepowTattu1 天前
新品发布|Tattu TA-BC无人机电池检测器上线,多通道充电状态尽在掌握
无人机
xuanshang_yutou2 天前
无人机调试起飞遇到的问题及注意事项
无人机
延凡科技2 天前
多场景落地复盘:端边云架构无人机智能巡检系统设计与实践
大数据·数据结构·人工智能·科技·架构·无人机·能源
科技大视界2 天前
无人机智能巡检服务商技术哪家强?
无人机
鼎艺创新科技3 天前
技术解析|基于三维GIS电子沙盘的无人机空地协同防控管理方案
无人机·三维电子沙盘·智能安防·无人机空地协同·全域可视化
沈阳昊天环宇无人机小编辑5 天前
沈阳无人机飞手如何破局?掌握激光点云三维建模,抢占低空测绘新赛道
无人机
深蓝学院5 天前
北大团队FSD-VLN:用“慢系统”管语义、“快系统”管飞控,未知环境导航成功率翻倍!
无人机
2601_963282776 天前
龙江低空巡检协同调度!无人机 + 手持对讲一体化通信方案,适配河湖、林区、国土巡查
无人机