返回首页

无人机野生动物巡查系统

2025 全国大学生电子设计竞赛 H 题 · 视觉识别 + 路径规划 + 自主飞行

K210 树莓派 Linux YOLO
无人机野生动物巡查系统
2025 全国大学生电子设计竞赛 H 题 · 野生动物巡查系统

赛题背景

2025 年全国大学生电子设计竞赛 H 题要求设计一套野生动物巡查系统,使用多旋翼自主飞行无人机巡查 450cm×350cm 区域(分成 63 个 50cm×50cm 方格),识别、统计区域内野生动物类型(象、虎、狼、猴、孔雀等)、所在位置及各种动物的数量。

核心要求

  • 巡查前在地面站设置3 个禁飞区,规划覆盖所有非禁飞区方格的航线
  • 无人机从红色起飞区起飞,在 120±10cm 高度按航线巡查,500 秒内完成
  • 发现动物时识别种类及数量,将方格代码、动物名称及数量发送到地面站
  • 发挥部分:用机载激光笔光斑照射动物体态轮廓

系统硬件组成

整套系统采用多传感器融合架构,涵盖飞控、视觉、导航、通信四大模块:

模块 型号 作用
飞控主芯片 TI TM4C123GH6PM ARM Cortex-M4F, 80MHz,姿态解算、PID 控制、任务调度
机载计算机 树莓派 4B ROS 建图 + SLAM 定位 + OpenCV 图像处理
视觉模块 K210 (MaixPy) YOLOv2 目标检测,5 类动物识别(象、虎、狼、猴、孔雀)
激光雷达 乐动 DTOF 雷达感应器 SLAM 建图与定位,提供全局位置信息
IMU ICM20689 三轴陀螺仪 + 三轴加速度计 (1000Hz 采样)
气压计 SPL06 气压高度测量
对地测距 TOFSense / VL53L1X 精确对地高度,保证 120cm 巡查高度
光流 LC306 水平速度估计,辅助位置闭环
地面站 触摸屏 + 蓝牙 禁飞区设置、动物统计显示、参数调试

我的工作板块

在整个项目中,我主要负责软件部分的开发,包括视觉端通信设计、模型训练部署、以及路径规划算法三大模块。

3 核心工作模块
70%+ YOLO 识别准确率
60 全覆盖路径规划

一、视觉端 K210 与飞控端字节流通信设计

设计了 K210 视觉模块与 TM4C123 飞控之间的二进制通信协议,实现可靠的双向数据传输。飞控发送触发帧通知 K210 开始检测,K210 完成检测后发送结果帧回传数据。

通信协议架构
K210 视觉端与飞控端通信协议架构
Python - K210 端数据打包 (K210main3.0.py)
def package_data(mode_val=MODE_CONST):
    """FF FC | (0xA0+mode) | len | payload | checksum"""
    data = [
        0xFF, 0xFC,                          # 帧头
        (0xA0 + (mode_val & 0xFF)) & 0xFF,  # 功能字
        0x00,                                 # 长度占位

        (target.x >> 8) & 0xFF, target.x & 0xFF,           # 目标X
        (target.y >> 8) & 0xFF, target.y & 0xFF,           # 目标Y
        (target.pixel >> 8) & 0xFF, target.pixel & 0xFF,   # 面积

        target.flag & 0xFF,                  # 检测标志
        target.state & 0xFF,                 # 保留

        (target.angle >> 8) & 0xFF, target.angle & 0xFF,           # 象
        (target.distance >> 8) & 0xFF, target.distance & 0xFF,     # 虎
        (target.apriltag_id >> 8) & 0xFF, target.apriltag_id & 0xFF, # 狼
        (target.img_width >> 8) & 0xFF, target.img_width & 0xFF,   # 猴
        (target.img_height >> 8) & 0xFF, target.img_height & 0xFF, # 孔雀

        target.fps & 0xFF,                   # 帧率
        target.camera_id & 0xFF,             # 摄像头ID
        0x00                                  # 校验占位
    ]
    data[3] = (len(data) - 5) & 0xFF        # 填充长度
    data[-1] = (sum(data[:-1]) & 0xFF)       # 填充校验和
    return data
通信字节流设计
视觉端与飞控端通信字节流设计图

二、视觉端 K210 模型训练与部署

针对 5 类野生动物(象、虎、狼、猴、孔雀)的检测需求,在勘智(Kendryte)平台训练 YOLOv2 模型,并部署到 K210 的 KPU 神经网络加速器。

模型训练流程

  • 数据采集:拍摄 5 类动物图片各 100 张,包含不同角度、光照、背景
  • 标注:在勘智平台用矩形框标注动物位置,标记类别
  • 训练:选择 YOLO 模型,输入尺寸 240×320,训练 500 epochs
  • 导出:下载 .kmodel 格式模型文件(约 571KB)
Python - K210 YOLO 模型配置与推理 (K210main3.0.py)
# YOLO 模型配置
labels = ["1", "2", "3", "4", "5"]
anchor = (0.91, 1.05, 1.58, 0.63, 1.29, 0.87, 1.67, 1.69, 2.77, 1.03)

kpu = KPU()
kpu.load_kmodel("/sd/det4.kmodel")
kpu.init_yolo2(
    anchor,
    anchor_num=int(len(anchor) / 2),
    img_w=320, img_h=240,
    net_w=320, net_h=240,
    layer_w=10, layer_h=8,
    threshold=0.4, nms_value=0.3,
    classes=len(labels),
)
Python - 单帧检测与投票决策
N_FRAMES = 20
MODE_CONST = 0x07

# 单帧检测
def detect_one_frame():
    clock.tick()
    img = sensor.snapshot()
    kpu.run_with_output(img)
    dets = kpu.regionlayer_yolo2()

    counts = [0, 0, 0, 0, 0]
    prob_sum = 0.0
    if dets:
        for d in dets:
            cls = int(d[4])       # 类别索引 0~4
            conf = float(d[5])    # 置信度
            if 0 <= cls < 5:
                counts[cls] += 1
                prob_sum += conf

    packs = [pack_status(c) for c in counts]
    flag = 1 if sum(counts) > 0 else 0
    return {'flag': flag, 'packs': packs, 'counts': counts, 'prob_sum': prob_sum}

# 主循环:触发 → 20帧检测 → 投票 → 发送
while True:
    gc.collect()
    rx_frame_ok = False
    uart_data_read()   # 检查是否收到飞控触发指令

    if rx_frame_ok and ((MODE_CONST >> BIT_TASK) & 0x01):
        results = []

        # 阶段1:连续采集 20 帧并检测
        for _ in range(N_FRAMES):
            results.append(detect_one_frame())
            time.sleep_ms(2)

        # 阶段2:投票决策
        if all(r['flag'] == 0 for r in results):
            target.flag = 0
        else:
            freq = {}
            for idx, r in enumerate(results):
                key = tuple(r['packs'])
                if key not in freq:
                    freq[key] = [(idx, r['prob_sum'])]
                else:
                    freq[key].append((idx, r['prob_sum']))

            best_key = max(freq.items(), 
                          key=lambda x: (len(x[1]), max(v for _, v in x[1])))[0]
            best_idx = max(freq[best_key], key=lambda t: t[1])[0]
            best = results[best_idx]

            target.flag = best['flag']
            target.angle, target.distance, target.apriltag_id, \
            target.img_width, target.img_height = best['packs']

        # 阶段3:打包发送
        tx = package_data(MODE_CONST)
        yb_uart.write(bytearray(tx))

三、飞控端遍历算法设计

设计了基于贪心最近邻策略的全覆盖路径规划算法,在 9×7 网格地图上遍历所有可通行格子,同时避开 3 个禁飞区,最终返回起点。算法核心为:每步用曼哈顿距离筛选最近的未访问目标点,再用 BFS 计算实际最短路径。

路径规划算法
贪心最近邻覆盖路径规划示意图(9×7 网格,含 3 个禁飞区)

算法思路

  • Step 1 — 收集目标点:遍历网格,排除禁飞区后得到所有可通行格子的坐标列表
  • Step 2 — 贪心选择:从当前位置出发,用曼哈顿距离 (|Δr|+|Δc|) 在所有未访问目标中选出最近点
  • Step 3 — BFS 寻路:用 BFS 在网格上计算当前位置到目标点的实际最短路径(绕开禁飞区),将方向序列写入路径数组
  • Step 4 — 回到起点:所有目标点访问完毕后,BFS 计算返回起始点 (6,8) 的最短路径
C - 贪心最近邻选择
// 计算曼哈顿距离(启发式估价)
static int manhattan_distance(int r1, int c1, int r2, int c2) {
    return abs(r1 - r2) + abs(c1 - c2);
}

// 贪心算法选择下一个最近的目标点
static int find_nearest_unvisited(const int m[MAP_HEIGHT][MAP_WIDTH],
                                   int cr, int cc,
                                   const int targets_r[], const int targets_c[],
                                   int visited[], int target_count,
                                   int *next_idx)
{
    int best_idx = -1;
    int best_dist = INT_MAX;

    for (int i = 0; i < target_count; i++) {
        if (visited[i]) continue;
        int dist = manhattan_distance(cr, cc, targets_r[i], targets_c[i]);
        if (dist < best_dist) {
            best_dist = dist;
            best_idx = i;
        }
    }

    *next_idx = best_idx;
    return best_idx >= 0;
}
C - BFS 最短路径计算
// BFS 最短路:从 (sr,sc) 到 (tr,tc),返回步数,路径写入 out_dir
static int bfs_shortest(const int m[MAP_HEIGHT][MAP_WIDTH],
                        int sr, int sc, int tr, int tc, char *out_dir)
{
    if (sr == tr && sc == tc) return 0;

    static int qx[MAP_HEIGHT * MAP_WIDTH];
    static int qy[MAP_HEIGHT * MAP_WIDTH];
    static int vis[MAP_HEIGHT][MAP_WIDTH];
    static char pre_dir[MAP_HEIGHT][MAP_WIDTH];

    memset(vis, 0, sizeof(vis));
    memset(pre_dir, 0, sizeof(pre_dir));

    int front = 0, rear = 0;
    qx[rear] = sr; qy[rear] = sc; rear++;
    vis[sr][sc] = 1;

    int found = 0;
    while (front < rear) {
        int r = qx[front], c = qy[front]; front++;
        for (int k = 0; k < 4; ++k) {
            int nr = r + DX[k], nc = c + DY[k];
            if (!passable(nr, nc, m) || vis[nr][nc]) continue;
            vis[nr][nc] = 1;
            pre_dir[nr][nc] = DIRC[k];
            qx[rear] = nr; qy[rear] = nc; rear++;
            if (nr == tr && nc == tc) { found = 1; break; }
        }
        if (found) break;
    }

    if (!found) return -1;

    // 回溯并反转得到正向路径
    char tmp[MAP_HEIGHT * MAP_WIDTH];
    int len = 0;
    int cr = tr, cc = tc;
    while (!(cr == sr && cc == sc)) {
        char d = pre_dir[cr][cc];
        tmp[len++] = d;
        if      (d == 'U') cr += 1;
        else if (d == 'D') cr -= 1;
        else if (d == 'L') cc += 1;
        else if (d == 'R') cc -= 1;
    }
    for (int i = 0; i < len; ++i) out_dir[i] = tmp[len - 1 - i];
    return len;
}
C - 主流程:贪心覆盖 + 返回起点
int build_cover_path_with_return_greedy(const int stop[3][2], int *out_steps, int max_steps)
{
    // 构建地图,标记禁飞区
    int map[MAP_HEIGHT][MAP_WIDTH];
    memset(map, 0, sizeof(map));
    for (int i = 0; i < 3; ++i)
        map[stop[i][0]][stop[i][1]] = 1;

    const int SR = MAP_HEIGHT - 1;
    const int SC = MAP_WIDTH - 1;

    // 收集所有可走的目标点
    int targets_r[MAP_HEIGHT * MAP_WIDTH];
    int targets_c[MAP_HEIGHT * MAP_WIDTH];
    int target_count = 0;
    for (int r = 0; r < MAP_HEIGHT; r++)
        for (int c = 0; c < MAP_WIDTH; c++)
            if (map[r][c] == 0) {
                targets_r[target_count] = r;
                targets_c[target_count] = c;
                target_count++;
            }

    int visited[MAP_HEIGHT * MAP_WIDTH];
    memset(visited, 0, sizeof(visited));
    visited[start_idx] = 1;
    int total_len = 0;
    int cr = SR, cc = SC;
    int visited_count = 1;

    // 贪心循环:每次选曼哈顿距离最近的未访问点,BFS 走过去
    while (visited_count < target_count) {
        int next_idx;
        if (!find_nearest_unvisited(map, cr, cc, targets_r, targets_c,
                                     visited, target_count, &next_idx))
            break;

        char seg[MAX_PATH];
        int len = bfs_shortest(map, cr, cc,
                               targets_r[next_idx], targets_c[next_idx], seg);
        if (len < 0) { visited[next_idx] = 1; visited_count++; continue; }

        for (int k = 0; k < len; k++)
            out_steps[total_len + k] = dirchar_to_code(seg[k]);
        total_len += len;
        cr = targets_r[next_idx];
        cc = targets_c[next_idx];
        visited[next_idx] = 1;
        visited_count++;
    }

    // BFS 返回起点 (6, 8)
    if (!(cr == SR && cc == SC)) {
        char seg[MAX_PATH];
        int len = bfs_shortest(map, cr, cc, SR, SC, seg);
        for (int k = 0; k < len; k++)
            out_steps[total_len + k] = dirchar_to_code(seg[k]);
        total_len += len;
    }

    return total_len;
}

飞行控制系统

飞控软件采用定时器中断驱动的多任务架构,控制系统采用经典的串级 PID 结构。

串级 PID 控制架构
串级 PID 控制架构:位置环 → 姿态角环 → 角速率环

成果展示

视觉端采集结果
视觉端 K210 采集结果实时回传地面站
竞赛获奖
2025 年全国大学生电子设计竞赛重庆赛区二等奖

落地成果

70%+ 动物识别准确率
60 全覆盖巡查
500s 时限内完成
省二 重庆赛区奖项

项目资源

  • 飞控固件: CarryPilot V6.0.2(基于 TI Tiva C Series TM4C123GH6PM)
  • 视觉方案: K210 (MaixPy + YOLO) + 20 帧投票策略
  • 机载计算: 树莓派 4B (ROS + SLAM + OpenCV)
  • 激光雷达: 乐动 DTOF 雷达感应器(SLAM 建图定位)
  • 路径算法: 贪心最近邻 + BFS 最短路径 + 返回起点
  • 地面站: 触摸屏 + 蓝牙通信
  • 开发环境: Keil MDK µVision 5 + MaixPy IDE