目标检测

概述

在完成图像分类的学习后,我们将目光转向更高级的计算机视觉任务——目标检测。图像分类为整张图片分配一个标签,而目标检测则进一步识别并定位图片中的多个目标。这一能力为边缘计算和物联网设备(如 Raspberry Pi)带来了更多应用场景和挑战。

目标检测融合了分类与定位两大任务。它不仅判断图片中有哪些对象,还能通过绘制边界框等方式,精确标注对象的位置。这种复杂性让目标检测成为理解视觉场景的强大工具,但也对模型和训练方法提出了更高要求。

在边缘 AI 场景下,受限于计算资源,实现高效的目标检测模型尤为关键。图像分类中遇到的模型体积、推理速度与精度的平衡问题,在目标检测中被进一步放大。但回报也更大,因为目标检测能实现更细致、丰富的视觉数据分析。

目标检测在边缘设备上的典型应用包括:

  • 安防监控系统
  • 自动驾驶与无人机
  • 工业质检
  • 野生动物监测
  • 增强现实应用

在本章实践中,我们将基于图像分类的知识,深入目标检测。重点介绍适合边缘设备的主流目标检测架构,如:

  • 单阶段检测器(如 MobileNet、EfficientDet)
  • FOMO(Faster Objects, More Objects)
  • YOLO(You Only Look Once)

想进一步了解目标检测模型,可参考教程 A Gentle Introduction to Object Recognition With Deep Learning

我们将分别体验以下目标检测模型:

  • TensorFlow Lite Runtime(现已更名为 LiteRT
  • Edge Impulse Linux Python SDK
  • Ultralitics
图 1: 目标检测流程图
图 1: 目标检测流程图

在本实验中,我们将系统学习目标检测的基本原理及其与图像分类的区别,并掌握如何基于自建数据集训练、微调、测试、优化和部署主流目标检测模型。

目标检测基础

目标检测是在图像分类基础上的重要扩展。理解目标检测,首先要明确它与图像分类的核心区别:

图像分类 vs. 目标检测

图像分类

  • 为整张图片分配一个标签
  • 回答“这张图片的主要内容是什么?”
  • 输出单一类别预测

目标检测

  • 识别并定位图片中的多个对象
  • 回答“图片中有哪些对象?它们分别在哪里?”
  • 输出多个预测结果,每个包含类别标签和边界框

举例说明:

图 2: 图像分类与目标检测对比示意图
图 2: 图像分类与目标检测对比示意图

如上图所示,图像分类只给出整体标签,而目标检测能识别多个对象及其具体位置。

目标检测的关键组成

目标检测系统通常包含两大核心模块:

  1. 目标定位:确定对象在图片中的位置,通常输出边界框(矩形区域)。
  2. 目标分类:对每个定位到的区域进行类别判别,类似于图像分类,但针对每个局部区域。

目标检测的挑战

相比图像分类,目标检测面临更多挑战:

  • 多目标:一张图片可能包含多个不同类别、大小、位置的对象
  • 尺度变化:对象在图片中可能有不同尺寸
  • 遮挡:对象可能部分被遮挡或重叠
  • 背景干扰:复杂背景下区分目标更难
  • 实时性:许多应用要求极快的推理速度,尤其在边缘设备上

目标检测方法

主流目标检测方法分为两类:

  1. 两阶段检测器:先生成候选区域,再对每个区域分类。典型如 R-CNN 系列(Fast R-CNN、Faster R-CNN)。
  2. 单阶段检测器:一次前向传播同时预测边界框和类别概率。典型如 YOLO、EfficientDet、SSD、FOMO。单阶段方法速度更快,更适合 Raspberry Pi 等边缘设备。

评估指标

目标检测常用的评估指标有:

  • IoU(交并比):衡量预测框与真实框的重叠程度
  • mAP(平均精度均值):综合各类别和 IoU 阈值下的精度与召回率
  • FPS(帧率):检测速度,边缘设备实时应用的关键指标

预训练目标检测模型概览

如前所述,目标检测模型可对图片或视频流中的已知对象进行识别,并输出其在图片中的位置。

可在线体验常见模型:Object Detection - MediaPipe Studio

Kaggle 上可找到适用于 Raspberry Pi 的主流 tflite 预训练模型,如 ssd_mobilenet_v1EfficientDet。这些模型基于 COCO 数据集(91 类,20 万张标注图片)训练。下载模型后上传至 Raspi 的 ./models 文件夹。

也可在 GitHub 获取模型及 COCO 标签文件。

本实验首先聚焦于 $300\times 300$ 的 SSD-Mobilenet V1 预训练模型,并与 $320\times 320$ 的 EfficientDet-lite0 进行对比,二者均基于 COCO 2017 数据集训练,已转换为 TensorFlow Lite 格式(SSD Mobilenet 4.2 MB,EfficientDet 4.6 MB)。

SSD-Mobilenet V2/V3 更适合迁移学习,但本节以公开的 V1 TFLite 模型为例进行演示。

图 3: 模型部署流程图
图 3: 模型部署流程图

配置 TFLite 环境

请确认已完成上一节“图像分类”实验的相关步骤:

  • 更新 Raspberry Pi
  • 安装所需库
  • (可选)创建虚拟环境
source ~/tflite/bin/activate
  • 安装 TensorFlow Lite Runtime
  • 在虚拟环境中安装其他 Python 库

创建工作目录

假设上节已创建 Documents/TFLITE 文件夹,现在为目标检测实验新建专用目录:

cd Documents/TFLITE/
mkdir OBJ_DETECT
cd OBJ_DETECT
mkdir images
mkdir models
cd models

推理与后处理

新建 notebook 按步骤实现目标检测:

导入所需库:

import time
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import tflite_runtime.interpreter as tflite

加载 TFLite 模型并分配张量:

model_path = "./models/ssd-mobilenet-v1-tflite-default-v1.tflite"
interpreter = tflite.Interpreter(model_path=model_path)
interpreter.allocate_tensors()

获取输入输出张量信息:

input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

输入信息 指明模型期望的图片格式。形状为 (1, 300, 300, 3),类型为 uint8,即输入为未归一化的 $300\times 300\times 3$ 彩色图片,单张输入(Batch=1)。

输出信息 包含类别(classes)、概率(scores)、边界框(boxes)及检测到的对象数量(num_detections)。模型最多可检测 10 个对象。

![推理结果示意图](https://assets.jimmysong.io/images/book/ml-systems/raspberry-pi/object-detection/inference result.webp)

如上例,模型以 76% 概率检测到类别 ID 为 16 的对象,边界框为 [0.028011084, 0.020121813, 0.9886069, 0.802299],分别对应 ymin、xmin、ymax、xmax。

y 轴自上而下,x 轴自左至右。已知图片尺寸,即可绘制目标的矩形框:

图 4: 边界框示意图
图 4: 边界框示意图

查找类别 ID 16 的含义,打开 coco_labels.txt,第 16 行为 cat,概率即为得分。

接下来上传包含多个对象的图片进行测试。

img_path = "./images/cat_dog.jpeg"
orig_img = Image.open(img_path)

# 显示图片
plt.figure(figsize=(8, 8))
plt.imshow(orig_img)
plt.title("原始图片")
plt.show()
图 5: 猫狗图片
图 5: 猫狗图片

根据输入信息,预处理图片:

img = orig_img.resize(
    (input_details[0]["shape"][1], input_details[0]["shape"][2])
)
input_data = np.expand_dims(img, axis=0)
input_data.shape, input_data.dtype

新输入数据形状为 (1, 300, 300, 3),类型为 uint8,与模型要求一致。

推理并计时:

start_time = time.time()
interpreter.set_tensor(input_details[0]["index"], input_data)
interpreter.invoke()
end_time = time.time()
inference_time = (
    end_time - start_time
) * 1000  # 毫秒
print("推理时间:{:.1f}ms".format(inference_time))

约 800ms,可获取 4 个输出:

boxes = interpreter.get_tensor(output_details[0]["index"])[0]
classes = interpreter.get_tensor(output_details[1]["index"])[0]
scores = interpreter.get_tensor(output_details[2]["index"])[0]
num_detections = int(
    interpreter.get_tensor(output_details[3]["index"])[0]
)

快速查看得分大于 0.5 的目标:

for i in range(num_detections):
    if scores[i] > 0.5:
        print(f"Object {i}:")
        print(f"  Bounding Box: {boxes[i]}")
        print(f"  Confidence: {scores[i]}")
        print(f"  Class: {classes[i]}")
图 6: 推理文本结果
图 6: 推理文本结果

可视化检测结果:

plt.figure(figsize=(12, 8))
plt.imshow(orig_img)
for i in range(num_detections):
    if scores[i] > 0.5:
        ymin, xmin, ymax, xmax = boxes[i]
        (left, right, top, bottom) = (
            xmin * orig_img.width,
            xmax * orig_img.width,
            ymin * orig_img.height,
            ymax * orig_img.height,
        )
        rect = plt.Rectangle(
            (left, top),
            right - left,
            bottom - top,
            fill=False,
            color="red",
            linewidth=2,
        )
        plt.gca().add_patch(rect)
        class_id = int(classes[i])
        class_name = labels[class_id]
        plt.text(
            left,
            top - 10,
            f"{class_name}: {scores[i]:.2f}",
            color="red",
            fontsize=12,
            backgroundcolor="white",
        )
图 7: 推理可视化结果
图 7: 推理可视化结果

EfficientDet 简介

EfficientDet 虽不属于 SSD(单次检测器)模型,但与 SSD 等架构有诸多相似之处,并在此基础上创新:

  1. EfficientDet:

    • 2019 年 Google 提出
    • 以 EfficientNet 为骨干网络
    • 引入 BiFPN(双向特征金字塔网络)
    • 采用复合缩放策略,整体高效扩展
  2. 与 SSD 的相似点:

    • 均为单阶段检测器,单次前向传播完成定位与分类
    • 均利用多尺度特征图检测不同尺寸目标
  3. 主要区别:

    • 骨干网络:SSD 常用 VGG/MobileNet,EfficientDet 用 EfficientNet
    • 特征融合:SSD 用简单金字塔,EfficientDet 用 BiFPN
    • 缩放方式:EfficientDet 复合缩放,整体更灵活
  4. 优势:

    • 精度与效率权衡优于 SSD 等主流模型
    • 支持多种规模,适配不同性能需求

EfficientDet 可视为单阶段检测架构的进化,输出结构与 SSD 类似(边界框与类别分数)。

可参考 notebook 进一步探索 EfficientDet。

目标检测项目实战

接下来,我们将从数据采集到训练与部署,完整实现一个目标检测项目。训练模型包括 SSD-MobileNet V2、FOMO 和 YOLO,均基于同一数据集。

项目目标

所有机器学习项目都需明确目标。假设我们在工厂需对 轮子 和特殊 盒子 进行分拣与计数。

图 8: 项目目标示意图
图 8: 项目目标示意图

即需实现多标签分类,每张图片可能有三类:

  • 背景(无对象)
  • 盒子
  • 轮子

原始数据采集

明确目标后,首要任务是采集数据集。可用手机、Raspi 或混合方式采集原始图片(无标签)。Raspberry Pi 可通过 Web 应用实时预览并采集 QVGA (320 x 240) 图片。

从 GitHub 获取 get_img_data.py 脚本并运行:

python3 get_img_data.py

访问 Web 界面:

  • 若在 Raspberry Pi 本地(有桌面环境):浏览器访问 http://localhost:5000
  • 局域网其他设备:浏览器访问 http://<raspberry_pi_ip>:5000(将 <raspberry_pi_ip> 替换为实际 IP),如 http://192.168.4.210:5000
图 9: Web 采集界面
图 9: Web 采集界面

该脚本基于 Raspberry Pi 摄像头,提供 Web 采集界面,便于机器学习项目采集图片数据。

在浏览器输入标签(如 box-wheel),点击 Start Capture 开始采集。

图 10: 采集图片界面
图 10: 采集图片界面

注意,采集图片暂不标注具体类别,后续再标注。

通过实时预览调整拍摄,点击 Capture Image 保存图片至当前标签文件夹(如 dataset/box-wheel)。

图 11: 图片采集示意
图 11: 图片采集示意

采集足够图片后,点击 Stop Capture。图片保存在 dataset/box-wheel 文件夹:

图 12: 数据集文件夹结构
图 12: 数据集文件夹结构

建议采集约 60 张图片,涵盖不同角度、背景和光照。可用 Filezilla 将数据集传至主机。

数据标注

目标检测项目的下一步是为图片打标签,即为每个对象绘制边界框。可用 LabelImgCVATRoboflowEdge Impulse Studio。本节以 Roboflow 为例。

选用 Roboflow(免费版)主要因其支持自动标注和多格式导出,便于在 Edge Impulse、Ultralitics 等平台训练。Edge Impulse 标注数据无法直接用于其他平台。

上传原始数据集至 Roboflow,新建项目(如“box-versus-wheel”)。

图 13: Roboflow 新建项目
图 13: Roboflow 新建项目

Roboflow 标注流程详见其官方教程,此处不再赘述。

标注

项目创建并上传图片后,使用“Auto-Label”工具自动标注。可上传仅含背景的图片,无需标注。

图 14: 自动标注界面
图 14: 自动标注界面

标注完成后,按训练、验证、测试划分数据集。

图 15: 数据集划分
图 15: 数据集划分

数据预处理

最后一步是预处理数据集,生成最终训练集。将所有图片调整为 $320\times 320$,并进行数据增强(如旋转、裁剪、亮度调整)。

图 16: 数据增强示意
图 16: 数据增强示意

最终得到 153 张图片。

图 17: 最终数据集
图 17: 最终数据集

导出数据集为 Edge Impulse、Ultralitics 等支持的格式(如 YOLOv8),下载压缩包。

图 18: 下载数据集
图 18: 下载数据集

数据集结构如下:

图 19: 数据集结构
图 19: 数据集结构

三大文件夹分别为 traintestvalid,每个包含 imageslabels 子文件夹。标签文件格式为 class_id bounding box coordinates,本例中 0 表示 box1 表示 wheel,编号按类别字母顺序排列。

data.yaml 文件包含类别名等信息,符合 YOLO 格式。

预处理后的数据集可在 Roboflow 获取。

在 Edge Impulse Studio 训练 SSD MobileNet 模型

登录 Edge Impulse Studio,新建项目。

可直接克隆本实验项目:Raspi - Object Detection

Dashboard 页,Project info 处选择 Bounding boxes (object detection) 作为标注方式。

上传标注数据

Data acquisition 页,UPLOAD DATA 区域上传本地数据集。

选择 train 文件夹(含 imageslabels),标签格式选 “YOLO TXT”,上传至 Training,点击 Upload data

图 20: 上传数据界面
图 20: 上传数据界面

同理上传 testvalid 数据。最终数据集共 153 张图片,训练/测试比例约 84%/16%。

标签文件中 01 分别对应 boxwheel

图 21: 数据集上传结果
图 21: 数据集上传结果

Impulse 设计

进入 Create impulse 步骤,首先选择部署目标设备。弹窗中选择 Raspberry 4(介于 Raspi-Zero 与 Raspi-5 之间)。

该选择仅影响延迟估算,不影响训练。

本阶段需定义:

  • 预处理:图片已在 Roboflow 预处理为 320x320,无需调整。若上传非正方形图片,将自动拉伸为正方形。
  • 模型设计:选择 “Object Detection”。
图 22: Impulse 设计界面
图 22: Impulse 设计界面

数据集预处理

Image 区域,选择 Color depthRGB,点击 Save parameters

图 23: 图片预处理界面
图 23: 图片预处理界面

自动进入 Generate features,所有样本预处理后共 480 个对象:207 个 box,273 个 wheel。

图 24: 特征生成结果
图 24: 特征生成结果

特征可视化显示样本分布良好。

模型设计、训练与测试

选择预训练模型:MobileNetV2 SSD FPN-Lite (320x320 only),支持最多检测 10 个对象,模型约 3.7 MB,输入为 $320\times 320$ RGB。

训练超参数:

  • 轮数(Epochs):25
  • 批量大小(Batch size):32
  • 学习率(Learning Rate):0.15

训练时 20% 数据用于验证。

图 25: 训练结果
图 25: 训练结果

最终模型 COCO mAP 精度 88.8%,测试集精度 83.3%。

部署模型

两种部署方式:

  • TFLite 模型:导出 .tflite,Raspi 可用 Python 推理
  • Linux (AARCH64):导出 Linux 可执行文件,支持 Python SDK

选择 TFLite 模型,在 Dashboard 页下载 Transfer learning model (int8 quantized):

图 26: 下载 TFLite 模型
图 26: 下载 TFLite 模型

将模型传至 Raspi ./models,采集或准备图片至 ./images

推理与后处理

推理流程同“预训练目标检测模型”章节。新建 notebook 实现检测。

导入库:

import time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from PIL import Image
import tflite_runtime.interpreter as tflite

定义模型路径与标签:

model_path = "./models/ei-raspi-object-detection-SSD-\
              MobileNetv2-320x0320-int8.lite"
labels = ["box", "wheel"]

模型输出类别 ID(0、1),按类别字母顺序排列。

加载模型、分配张量、获取输入输出信息:

# 加载 TFLite 模型
interpreter = tflite.Interpreter(model_path=model_path)
interpreter.allocate_tensors()

# 获取输入输出张量
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

注意输入类型为 int8,需归一化处理:

input_dtype = input_details[0]["dtype"]
input_dtype
numpy.int8

加载并显示图片:

# 加载图片
img_path = "./images/box_2_wheel_2.jpg"
orig_img = Image.open(img_path)

# 显示图片
plt.figure(figsize=(6, 6))
plt.imshow(orig_img)
plt.title("原始图片")
plt.show()
图 27: 原始图片
图 27: 原始图片

预处理图片:

scale, zero_point = input_details[0]["quantization"]
img = orig_img.resize(
    (input_details[0]["shape"][1], input_details[0]["shape"][2])
)
img_array = np.array(img, dtype=np.float32) / 255.0
img_array = (
    (img_array / scale + zero_point).clip(-128, 127).astype(np.int8)
)
input_data = np.expand_dims(img_array, axis=0)

检查输入数据:

input_data.shape, input_data.dtype
((1, 320, 320, 3), dtype('int8'))

推理并计时:

# Raspi-Zero 上推理
start_time = time.time()
interpreter.set_tensor(input_details[0]["index"], input_data)
interpreter.invoke()
end_time = time.time()
inference_time = (
    end_time - start_time
) * 1000  # 毫秒
print("推理时间:{:.1f}ms".format(inference_time))

Raspi-Zero 上约 600ms,Raspi-5 更快。

获取输出:

boxes = interpreter.get_tensor(output_details[1]["index"])[0]
classes = interpreter.get_tensor(output_details[3]["index"])[0]
scores = interpreter.get_tensor(output_details[0]["index"])[0]
num_detections = int(
    interpreter.get_tensor(output_details[2]["index"])[0]
)

输出检测结果:

for i in range(num_detections):
    if scores[i] > 0.5:
        print(f"Object {i}:")
        print(f"  Bounding Box: {boxes[i]}")
        print(f"  Confidence: {scores[i]}")
        print(f"  Class: {classes[i]}")
图 28: 推理文本结果
图 28: 推理文本结果

可视化检测结果(阈值 0.5):

threshold = 0.5
plt.figure(figsize=(6, 6))
plt.imshow(orig_img)
for i in range(num_detections):
    if scores[i] > threshold:
        ymin, xmin, ymax, xmax = boxes[i]
        (left, right, top, bottom) = (
            xmin * orig_img.width,
            xmax * orig_img.width,
            ymin * orig_img.height,
            ymax * orig_img.height,
        )
        rect = plt.Rectangle(
            (left, top),
            right - left,
            bottom - top,
            fill=False,
            color="red",
            linewidth=2,
        )
        plt.gca().add_patch(rect)
        class_id = int(classes[i])
        class_name = labels[class_id]
        plt.text(
            left,
            top - 10,
            f"{class_name}: {scores[i]:.2f}",
            color="red",
            fontsize=12,
            backgroundcolor="white",
        )
图 29: 推理可视化结果
图 29: 推理可视化结果

降低阈值至 0.3:

图 30: 低阈值多目标检测
图 30: 低阈值多目标检测

此时出现误检和多重检测(同一对象被多次检测)。

为提升检测效果,可实现 非极大值抑制(NMS),去除重叠框,仅保留置信度最高的检测。

定义 non_max_suppression() 函数:

def non_max_suppression(boxes, scores, threshold):
    # Convert to corner coordinates
    x1 = boxes[:, 0]
    y1 = boxes[:, 1]
    x2 = boxes[:, 2]
    y2 = boxes[:, 3]

    areas = (x2 - x1 + 1) * (y2 - y1 + 1)
    order = scores.argsort()[::-1]

    keep = []
    while order.size > 0:
        i = order[0]
        keep.append(i)
        xx1 = np.maximum(x1[i], x1[order[1:]])
        yy1 = np.maximum(y1[i], y1[order[1:]])
        xx2 = np.minimum(x2[i], x2[order[1:]])
        yy2 = np.minimum(y2[i], y2[order[1:]])

        w = np.maximum(0.0, xx2 - xx1 + 1)
        h = np.maximum(0.0, yy2 - yy1 + 1)
        inter = w * h
        ovr = inter / (areas[i] + areas[order[1:]] - inter)

        inds = np.where(ovr <= threshold)[0]
        order = order[inds + 1]

    return keep

函数说明略。

定义可视化函数,结合 IoU 阈值,仅显示 NMS 后目标:

def visualize_detections(
    image, boxes, classes, scores, labels, threshold, iou_threshold
):
    if isinstance(image, Image.Image):
        image_np = np.array(image)
    else:
        image_np = image
    height, width = image_np.shape[:2]
    # Convert normalized coordinates to pixel coordinates
    boxes_pixel = boxes * np.array([height, width, height, width])
    # Apply NMS
    keep = non_max_suppression(boxes_pixel, scores, iou_threshold)
    # Set the figure size to 12x8 inches
    fig, ax = plt.subplots(1, figsize=(12, 8))
    ax.imshow(image_np)
    for i in keep:
        if scores[i] > threshold:
            ymin, xmin, ymax, xmax = boxes[i]
            rect = patches.Rectangle(
                (xmin * width, ymin * height),
                (xmax - xmin) * width,
                (ymax - ymin) * height,
                linewidth=2,
                edgecolor="r",
                facecolor="none",
            )

            ax.add_patch(rect)
            class_name = labels[int(classes[i])]
            ax.text(
                xmin * width,
                ymin * height - 10,
                f"{class_name}: {scores[i]:.2f}",
                color="red",
                fontsize=12,
                backgroundcolor="white",
            )
    plt.show()

定义推理主函数:

def detect_objects(img_path, conf=0.5, iou=0.5):
    orig_img = Image.open(img_path)
    scale, zero_point = input_details[0]["quantization"]
    img = orig_img.resize(
        (input_details[0]["shape"][1], input_details[0]["shape"][2])
    )
    img_array = np.array(img, dtype=np.float32) / 255.0
    img_array = (
        (img_array / scale + zero_point)
        .clip(-128, 127)
        .astype(np.int8)
    )
    input_data = np.expand_dims(img_array, axis=0)

    # Raspi-Zero 上推理
    start_time = time.time()
    interpreter.set_tensor(input_details[0]["index"], input_data)
    interpreter.invoke()
    end_time = time.time()
    inference_time = (
        end_time - start_time
    ) * 1000  # 毫秒

    print("推理时间:{:.1f}ms".format(inference_time))

    # 提取输出
    boxes = interpreter.get_tensor(output_details[1]["index"])[0]
    classes = interpreter.get_tensor(output_details[3]["index"])[0]
    scores = interpreter.get_tensor(output_details[0]["index"])[0]
    num_detections = int(
        interpreter.get_tensor(output_details[2]["index"])[0]
    )

    visualize_detections(
        orig_img,
        boxes,
        classes,
        scores,
        labels,
        threshold=conf,
        iou_threshold=iou,
    )

调用示例:

img_path = "./images/box_2_wheel_2.jpg"
detect_objects(img_path, conf=0.3, iou=0.05)
图 31: NMS 后检测结果
图 31: NMS 后检测结果

在 Edge Impulse Studio 训练 FOMO 模型

虽然 SSD MobileNet 模型的推理效果不错,但在 Raspi-Zero 上延迟较高,推理时间在 0.5 到 1.3 秒之间,约等于或低于 1 帧每秒(FPS)。为提升速度,可以采用 FOMO(Faster Objects, More Objects)算法。

FOMO 是一种新型机器学习算法,可在实时场景下计数多个目标并定位其在图片中的位置,所需算力和内存仅为 MobileNet SSD 或 YOLO 的 $1/30$。其核心在于,传统模型通过边界框标注目标大小,而 FOMO 只关注目标的中心点坐标,忽略目标尺寸,从而极大提升效率。

FOMO 的工作原理

在典型的目标检测流程中,第一步是从输入图片中提取特征。FOMO 利用 MobileNetV2 完成特征提取,高效捕捉纹理、形状和边缘等关键信息。

图 32: FOMO 特征提取流程图
图 32: FOMO 特征提取流程图

特征提取后,FOMO 采用简化的中心点检测架构,将特征图划分为网格,每个单元格判断是否存在目标中心点。模型为每个单元格输出置信度,表示该区域存在目标的概率。

以 $96\times 96$ 输入为例,FOMO 将图片划分为 $12\times 12$ 网格($96/8=12$);$160\times 160$ 输入则为 $20\times 20$ 网格。FOMO 对每个像素块进行分类,判断是否包含 box 或 wheel,并确定概率最高的区域。若某块无目标,则归为 background。最终,FOMO 输出每个目标中心点的坐标(归一化到图片尺寸)。

图 33: FOMO 网格检测示意图
图 33: FOMO 网格检测示意图

速度与精度的权衡

  • 网格分辨率:FOMO 使用固定分辨率网格,每个单元格独立检测目标中心,牺牲部分定位精度以换取极高速度,非常适合边缘设备。
  • 多目标检测:各单元格独立,FOMO 可同时检测多目标。

Impulse 设计、训练与测试

回到 Edge Impulse Studio,在 Experiments 标签页新建 impulse,输入图片尺寸设为 $160\times 160$(MobileNetV2 推荐输入)。

图 34: Impulse 设计界面
图 34: Impulse 设计界面

Image 标签页生成特征后,进入 Object detection 标签。

选择预训练模型:FOMO (Faster Objects, More Objects) MobileNetV2 0.35

图 35: 模型选择界面
图 35: 模型选择界面

训练超参数如下:

  • 轮数(Epochs):30
  • 批量大小(Batch size):32
  • 学习率(Learning Rate):0.001

训练时 20% 数据用于验证。由于数据集在 Roboflow 标注阶段已增强,训练集(80%)无需再做数据增强。

最终模型 F1 分数达 93.3%,在 Raspi-4 上推理延迟仅 8 毫秒,比 SSD MobileNetV2 快约 $60$ 倍。

图 36: FOMO 训练结果
图 36: FOMO 训练结果

注意,FOMO 会自动为原有的 box(0)和 wheel(1)类别添加第三类 background

Model testing 标签页可见模型准确率为 94%。下图为测试样例:

图 37: FOMO 测试样例
图 37: FOMO 测试样例

目标检测任务中,准确率并非主要评估指标。FOMO 只输出中心点而非边界框,单纯用准确率衡量模型表现并不全面。

部署模型

如前所述,训练好的模型可导出为 TFLite 或 Linux (AARCH64) 格式。此处选择 Linux (AARCH64),即 Edge Impulse Linux 协议的可执行文件。

Edge Impulse for Linux 模型以 .eim 格式交付,可执行文件包含完整 impulse(信号处理、学习与异常检测模块),针对处理器或 GPU 优化(如 ARM NEON),并内置简易 IPC 层。

Deploy 标签页选择 Linux (AARCH64),选择 int8 模型并点击 Build

图 38: Linux 部署界面
图 38: Linux 部署界面

模型将自动下载至本地。

在 Raspi 上新建工作目录:

cd ~
cd Documents
mkdir EI_Linux
cd EI_Linux
mkdir models
mkdir images

重命名模型便于识别,如 raspi-object-detection-linux-aarch64-FOMO-int8.eim,并传至 ./models,采集或准备图片至 ./images

推理与后处理

推理采用 Linux Python SDK。该库支持在 Linux 设备上用 Python 运行模型与采集数据,开源地址:edgeimpulse/linux-sdk-python

创建虚拟环境:

python3 -m venv ~/eilinux
source ~/eilinux/bin/activate

安装依赖库:

sudo apt-get update
sudo apt-get install libatlas-base-dev libportaudio0 libportaudio2
sudo apt-get install libportaudiocpp0 portaudio19-dev

pip3 install edge_impulse_linux -i https://pypi.python.org/simple
pip3 install Pillow matplotlib pyaudio opencv-contrib-python

sudo apt-get install portaudio19-dev
pip3 install pyaudio
pip3 install opencv-contrib-python

赋予模型可执行权限:

chmod +x raspi-object-detection-linux-aarch64-FOMO-int8.eim

安装 Jupyter Notebook:

pip3 install jupyter

本地启动 notebook(Raspi-4/5 桌面环境):

jupyter notebook

或在电脑浏览器远程访问:

jupyter notebook --ip=192.168.4.210 --no-browser

新建 notebook,按步骤用 FOMO 模型检测图片中的 box 和 wheel。

导入库:

import sys, time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from PIL import Image
import cv2
from edge_impulse_linux.image import ImageImpulseRunner

定义模型路径与标签:

model_file = "raspi-object-detection-linux-aarch64-int8.eim"
model_path = "models/" + model_file  # Edge Impulse 训练模型
labels = ["box", "wheel"]

模型输出类别 ID(0、1),按字母顺序排列。

加载并初始化模型:

# 加载模型文件
runner = ImageImpulseRunner(model_path)

# 初始化模型
model_info = runner.init()

model_info 包含模型关键信息。与 TFLite 不同,EI Linux Python SDK 会自动准备模型推理。

加载并显示图片(需用 OpenCV 读取并转为 RGB):

# 加载图片
img_path = "./images/1_box_1_wheel.jpg"
orig_img = cv2.imread(img_path)
img_rgb = cv2.cvtColor(orig_img, cv2.COLOR_BGR2RGB)

# 显示图片
plt.imshow(img_rgb)
plt.title("原始图片")
plt.show()
图 39: 原始图片
图 39: 原始图片

runner 获取特征与预处理图片(cropped):

features, cropped = (
    runner.get_features_from_image_auto_studio_settings(img_rgb)
)

推理并计时:

res = runner.classify(features)

输出检测到的类别、中心点坐标及置信度:

print(
    "Found %d bounding boxes (%d ms.)"
    % (
        len(res["result"]["bounding_boxes"]),
        res["timing"]["dsp"] + res["timing"]["classification"],
    )
)
for bb in res["result"]["bounding_boxes"]:
    print(
        "\t%s (%.2f): x=%d y=%d w=%d h=%d"
        % (
            bb["label"],
            bb["value"],
            bb["x"],
            bb["y"],
            bb["width"],
            bb["height"],
        )
    )
Found 2 bounding boxes (29 ms.)
 1 (0.91): x=112 y=40 w=16 h=16
 0 (0.75): x=48 y=56 w=8 h=8

结果显示检测到两个目标:类别 0(box)和类别 1(wheel),均正确。

可视化检测结果(默认阈值 0.5):

print(
    "\tFound %d bounding boxes (latency: %d ms)"
    % (
        len(res["result"]["bounding_boxes"]),
        res["timing"]["dsp"] + res["timing"]["classification"],
    )
)
plt.figure(figsize=(5, 5))
plt.imshow(cropped)

# 遍历所有检测框
bboxes = res["result"]["bounding_boxes"]
for bbox in bboxes:

    # 获取检测框中心
    left = bbox["x"]
    top = bbox["y"]
    width = bbox["width"]
    height = bbox["height"]

    # 绘制中心圆
    circ = plt.Circle(
        (left + width // 2, top + height // 2),
        5,
        fill=False,
        color="red",
        linewidth=3,
    )
    plt.gca().add_patch(circ)
    class_id = int(bbox["label"])
    class_name = labels[class_id]
    plt.text(
        left,
        top - 10,
        f'{class_name}: {bbox["value"]:.2f}',
        color="red",
        fontsize=12,
        backgroundcolor="white",
    )
plt.show()
图 40: FOMO 检测结果可视化
图 40: FOMO 检测结果可视化

使用 Ultralytics 探索 YOLO 模型

本实验将带你体验 YOLOv8。 UltralyticsYOLOv8 是著名实时目标检测与图像分割模型 YOLO 的一个版本。YOLOv8 基于深度学习和计算机视觉的最新进展,兼具极高的速度与精度,结构简洁,适用于从边缘设备到云 API 的多种硬件平台,易于集成和扩展。

YOLO 模型简介

YOLO(You Only Look Once)是一种高效且广泛应用的目标检测算法,以其实时处理能力著称。与传统目标检测系统通过分类器或定位器多步检测不同,YOLO 将检测问题视为单一回归任务。该创新方法使 YOLO 能够在一次前向推理中同时预测多组边界框及其类别概率,大幅提升了检测速度。

主要特性

  1. 单网络架构

    • YOLO 采用单一神经网络处理整张图片,将图片划分为网格,每个网格单元直接预测边界框及类别概率。端到端训练提升了速度并简化了模型结构。
  2. 实时处理能力

    • YOLO 最大亮点之一是实时目标检测能力。根据不同版本和硬件,YOLO 可实现高帧率(FPS)处理,非常适合视频监控、自动驾驶、体育赛事分析等对速度和准确性要求极高的场景。
  3. 版本演进

    • YOLO 从 v1 发展到最新的 v10,每一代都在精度、速度和效率上持续优化。YOLOv8 引入了更优的网络结构、训练方法和硬件适配,性能更强。
    • 虽然 YOLOv10 是家族最新成员,论文表现优异,但截至 2024 年 5 月尚未完全集成进 Ultralytics 库。根据精度 - 召回曲线分析,YOLOv8 通常优于 YOLOv9,能更好地捕获真阳性并减少误报(详见 相关分析)。因此本实验以 YOLOv8n 为基础。
    图 41: YOLO 版本演进示意图
    图 41: YOLO 版本演进示意图
  1. 精度与效率兼备

    • 早期 YOLO 以速度换取部分精度,近年版本在两者间取得更好平衡。新模型不仅更快,还能检测小目标(如蜜蜂),在复杂数据集上表现优异。
  2. 广泛应用场景

    • YOLO 的通用性使其在交通监控、安防、农业等领域广泛应用,如车辆计数、威胁识别、农作物与牲畜监测等,几乎适用于所有需高效目标检测的场景。
  3. 活跃社区与持续开发

    • YOLO 拥有强大的开发者和研究者社区(YOLOv8 社区尤为活跃)。开源实现和丰富文档便于定制和集成。主流深度学习框架如 Darknet、TensorFlow、PyTorch 均支持 YOLO,极大拓展了其适用范围。
    • Ultralytics YOLOv8 不仅支持 检测(本实验重点),还支持 分割姿态估计(基于 COCO 数据集预训练),以及 分类(基于 ImageNet 预训练)。跟踪 模式适用于所有检测、分割和姿态模型。
    图 42: Ultralytics YOLO 支持的任务类型
    图 42: Ultralytics YOLO 支持的任务类型

安装步骤

在树莓派上,先退出当前环境,创建新的工作目录:

deactivate
cd ~
cd Documents/
mkdir YOLO
cd YOLO
mkdir models
mkdir images

为 Ultralytics YOLOv8 创建虚拟环境:

python3 -m venv ~/yolo
source ~/yolo/bin/activate

安装 Ultralytics 及其本地推理依赖:

  1. 更新软件包列表,安装 pip 并升级:

    sudo apt update
    sudo apt install python3-pip -y
    pip install -U pip
    
  2. 安装带可选依赖的 ultralytics 包:

    pip install ultralytics[export]
    
  3. 重启设备:

    sudo reboot
    

YOLO 测试

树莓派重启后,激活 yolo 环境,进入工作目录:

source ~/yolo/bin/activate
cd /Documents/YOLO

使用 YOLOv8n(家族中最小模型)在终端运行推理,自动下载测试图片:

yolo predict model='yolov8n' \
     source='https://ultralytics.com/images/bus.jpg'

YOLO 模型家族均基于 COCO 数据集预训练。

推理结果会在终端显示。以 bus.jpg 为例,检测到 4 个 person,1 个 bus 和 1 个 stop signal

图 43: YOLO 推理结果示例
图 43: YOLO 推理结果示例

同时会提示 Results saved to runs/detect/predict。在该目录下可找到保存的图片(bus.jpg),可通过 FTP 下载到桌面查看:

图 44: 推理后保存的图片
图 44: 推理后保存的图片

说明 Ultralytics YOLO 已在树莓派上正确安装。但在 Raspi-Zero 上,即使使用最小模型 YOLOv8n,单次推理延迟约 18 秒,速度较慢。

导出模型为 NCNN 格式

在算力有限的边缘设备(如 Raspi-Zero)上部署视觉模型时,延迟是常见问题。可通过导出为高性能格式优化推理速度。

Ultralytics 支持多种导出格式,其中 NCNN 是专为移动端优化的高性能神经网络推理框架,无第三方依赖,跨平台,推理速度优于 TFLite 等主流开源框架。

在树莓派等 ARM 架构设备上,NCNN 推理速度表现最佳。

模型转换及推理流程如下:

  1. 将 YOLOv8n PyTorch 模型导出为 NCNN 格式,生成 /yolov8n_ncnn_model

    yolo export model=yolov8n.pt format=ncnn
    
  2. 使用导出的模型进行推理(此时可直接用本地 bus.jpg 图片):

    yolo predict model='./yolov8n_ncnn_model' source='bus.jpg'
    

首次加载模型推理延迟较高(约 17 秒),后续推理可降至 2 秒左右。

用 Python 交互式探索 YOLO

可通过 Python 解释器逐步体验 YOLO 推理流程:

python3

导入 Ultralytics YOLO 库并加载模型:

from ultralytics import YOLO

model = YOLO("yolov8n_ncnn_model")

对图片进行推理(以 bus.jpg 为例):

img = "bus.jpg"
result = model.predict(img, save=True, imgsz=640, conf=0.5, iou=0.3)
图 45: Python 推理结果截图
图 45: Python 推理结果截图

结果与命令行推理基本一致,但使用 NCNN 模型时可能检测不到公交站牌,且延迟更低。

进一步分析 result 内容:

例如,result[0].boxes.data 返回主推理结果,为形状 (4, 6) 的张量,每行分别为 4 个边界框坐标、第 5 列为置信度、第 6 列为类别(如 0: person5: bus):

图 46: 推理结果张量示例
图 46: 推理结果张量示例

可分别获取推理时间并格式化输出:

inference_time = int(result[0].speed["inference"])
print(f"Inference Time: {inference_time} ms")

统计检测到的目标数量:

print(f"Number of objects: {len (result[0].boxes.cls)}")
图 47: 检测数量输出示例
图 47: 检测数量输出示例

通过 Python 可灵活定制输出,详见 Ultralytics YOLO 推理文档

建议将上述流程写入脚本,便于复用。以 nano 编辑器新建 yolov8_tests.py

nano yolov8_tests.py

输入如下代码:

from ultralytics import YOLO

# 加载 YOLOv8 模型
model = YOLO("yolov8n_ncnn_model")

# 推理
img = "bus.jpg"
result = model.predict(img, save=False, imgsz=640, conf=0.5, iou=0.3)

# 输出结果
inference_time = int(result[0].speed["inference"])
print(f"Inference Time: {inference_time} ms")
print(f"Number of objects: {len (result[0].boxes.cls)}")
图 48: Python 脚本编辑示例
图 48: Python 脚本编辑示例

保存并退出([CTRL+O] + [ENTER] + [CTRL+X])。

运行脚本:

python yolov8_tests.py

结果与命令行和交互式推理一致。

首次加载 YOLO 库和模型推理耗时较长,后续推理速度显著提升。单次推理可由数秒降至 1 秒以内。

用自定义数据集训练 YOLOv8

回到我们在 Roboflow 标注的“Box versus Wheel”数据集。下载数据集时,选择“Show download code”以便在训练 notebook 中直接引用。

图 49: Roboflow 下载代码窗口截图
图 49: Roboflow 下载代码窗口截图

训练建议基于 Ultralytics 官方 Colab 示例进行适配。以下为可直接使用的 Colab 链接:

Notebook 关键步骤

  1. 选择 GPU 运行环境(推荐 NVidia T4,免费)。

  2. 使用 PIP 安装 Ultralytics。

    图 50: Ultralytics 安装截图
    图 50: Ultralytics 安装截图
  1. 导入 YOLO 并上传数据集,粘贴 Roboflow 下载代码,数据集将挂载到 /content/datasets/

    图 51: 数据集上传截图
    图 51: 数据集上传截图
  2. 检查并修改 data.yaml,确保图片路径正确:

    names:
    
    - box
    - wheel
    nc: 2
    roboflow:
    license: CC BY 4.0
    project: box-versus-wheel-auto-dataset
    url: https://universe.roboflow.com/marcelo-rovai-riila/ \
        box-versus-wheel-auto-dataset/dataset/5
    version: 5
    workspace: marcelo-rovai-riila
    test: /content/datasets/Box-versus-Wheel-auto-dataset-5/ \
        test/images
    train: /content/datasets/Box-versus-Wheel-auto-dataset-5/ \
        train/images
    val: /content/datasets/Box-versus-Wheel-auto-dataset-5/ \
        valid/images
    
  3. 设置主要超参数,例如:

    MODEL = 'yolov8n.pt'
    IMG_SIZE = 640
    EPOCHS = 25 # 正式项目建议至少 100 轮
    
  4. 运行训练(命令行方式):

    !yolo task=detect mode=train model={MODEL} \
      data={dataset.location}/data.yaml \
      epochs={EPOCHS}
      imgsz={IMG_SIZE} plots=True
    
    图 52: 训练结果截图
    图 52: 训练结果截图
    图 53: 混淆矩阵截图
    图 53: 混淆矩阵截图
  5. 训练完成后,模型(best.pt)保存在 /runs/detect/train/weights/。用验证集评估模型:

    !yolo task=detect mode=val model={HOME}/runs/detect/train/\
        weights/best.pt data={dataset.location}/data.yaml
    

    验证结果与训练相近。

  6. 用测试集图片推理:

    !yolo task=detect mode=predict model={HOME}/runs/detect/train/\
        weights/best.pt conf=0.25 source={dataset.location}/test/\
        images save=True
    

    推理结果保存在 runs/detect/predict 文件夹。部分结果如下:

    图 54: 测试集推理结果
    图 54: 测试集推理结果
  7. 建议将训练、验证和测试结果导出到 Google Drive。先挂载云盘:

    from google.colab import drive
    drive.mount('/content/gdrive')
    

    然后将 /runs 文件夹内容复制到 Drive 指定目录:

    !scp -r /content/runs '/content/gdrive/MyDrive/\
         10_UNIFEI/Box_vs_Wheel_Project'
    

在树莓派上用训练模型推理

将训练好的模型 /runs/detect/train/weights/best.pt 下载到本地,通过 FileZilla FTP 上传至树莓派 models 文件夹(可重命名为 box_wheel_320_yolo.pt)。

同样用 FTP 上传部分测试集图片到 ./YOLO/images

回到 YOLO 目录,进入 Python 解释器:

cd ..
python

导入 YOLO 库并加载自定义模型:

from ultralytics import YOLO

model = YOLO("./models/box_wheel_320_yolo.pt")

指定图片并推理(本次保存推理结果图片以便外部查看):

img = "./images/1_box_1_wheel.jpg"
result = model.predict(img, save=True, imgsz=320, conf=0.5, iou=0.3)

可对多张图片重复操作。推理结果保存在 runs/detect/predict8

图 55: 树莓派推理结果截图
图 55: 树莓派推理结果截图

用 FileZilla FTP 将推理结果下载到桌面查看:

图 56: 推理结果桌面查看
图 56: 推理结果桌面查看

可以看到推理效果非常好!本模型基于 YOLOv8n(家族最小模型)训练。推理延迟约 1 秒,如需进一步优化可转换为 TFLite 或 NCNN 格式。

实时视频流目标检测

本实验涉及的所有模型均可通过摄像头实现实时目标检测。只需将摄像头采集的图片作为模型输入即可。在 Raspi-4/5 桌面环境下,可用 OpenCV 捕获帧并实时显示推理结果。

此外,也可基于 Flask 和 TensorFlow Lite 快速搭建实时目标检测 Web 应用。例如,基于图像分类应用脚本稍作修改,即可实现 基于 TensorFlow Lite 和 Flask 的实时目标检测 Web 应用

本应用适用于所有 TFLite 模型。请确保模型路径正确,例如:

model_path = "./models/ssd-mobilenet-v1-tflite-default-v1.tflite"

GitHub 下载 Python 脚本 object_detection_app.py

在终端运行:

python3 object_detection_app.py

访问 Web 界面:

  • 树莓派本机(有桌面环境):浏览器访问 http://localhost:5000
  • 局域网其他设备:浏览器访问 http://<raspberry_pi_ip>:5000(将 <raspberry_pi_ip> 替换为树莓派 IP),如 http://192.168.4.210:5000

外部桌面运行效果如下:

图 57: Web 应用运行截图
图 57: Web 应用运行截图

下面简要介绍应用关键模块:

  1. TensorFlow Lite (tflite_runtime)
    • 用于边缘设备高效推理,模型体积小、性能优,支持硬件加速和量化。
    • 关键函数:Interpreter 加载模型,get_input_details()get_output_details() 交互模型。
  2. Flask
    • 轻量级 Web 框架,适合快速开发和部署,资源占用低。
    • 关键组件:路由装饰器定义 API,Response 流式传输视频,render_template_string 动态 HTML。
  3. Picamera2
    • 树莓派摄像头接口库,性能优于旧版 Picamera。
    • 关键函数:create_preview_configuration() 配置相机,capture_file() 捕获帧。
  4. PIL (Python Imaging Library)
    • 图像处理库,用于调整大小、绘制边框、格式转换。
    • 关键类:Image 加载处理图片,ImageDraw 绘制形状和文本。
  5. NumPy
    • 高效数组运算,适合处理图像数据和模型输入输出。
    • 关键函数:array() 创建数组,expand_dims() 增加维度。
  6. Threading
    • 并发执行,保证实时性能。
    • 关键组件:Thread 创建线程,Lock 线程同步。
  7. io.BytesIO
    • 内存二进制流,高效处理图片数据,减少 I/O。
  8. time
    • 时间相关函数,sleep() 控制帧率和性能测量。
  9. jQuery (前端)
    • 简化 DOM 操作和 AJAX 请求,动态更新界面。
    • 关键函数:.get().post() AJAX,DOM 操作。

主要系统架构:

  1. 主线程:运行 Flask 服务器,处理 HTTP 请求和 Web 界面。
  2. 相机线程:持续采集摄像头帧。
  3. 检测线程:用 TFLite 模型处理帧,进行目标检测。
  4. 帧缓冲区:共享内存(加锁)存储最新帧和检测结果。

数据流简述:

  1. 相机采集帧 → 帧缓冲区
  2. 检测线程读取帧 → TFLite 推理 → 更新检测结果
  3. Flask 路由访问缓冲区,提供最新帧和结果
  4. Web 客户端 AJAX 获取更新,动态刷新界面

该架构使树莓派等资源有限设备也能高效实现实时目标检测。多线程与高效库(TFLite、PIL)保证实时处理,Flask 和 jQuery 提供友好交互。

如需测试其他模型(如 EfficientDet),只需更改模型路径:

model_path = "./models/lite-model_efficientdet_lite0_\
   detection_metadata_1.tflite"

若需支持 Edge Impulse Studio 训练的 SSD-MobileNetV2 模型,需根据输入细节调整代码,详见 notebook

总结

本实验系统展示了在树莓派等边缘设备上实现目标检测的完整流程,体现了在资源受限硬件上运行先进视觉任务的强大潜力。主要内容包括:

  1. 模型对比:分析了 SSD-MobileNet、EfficientDet、FOMO、YOLO 等多种目标检测模型在边缘设备上的性能与权衡。
  2. 训练与部署:以自定义“箱子与轮子”数据集(Roboflow 标注)为例,演示了 Edge Impulse Studio 与 Ultralytics 的训练与部署流程。
  3. 优化技巧:介绍了模型量化(TFLite int8)、格式转换(如 NCNN)等多种推理加速方法。
  4. 实时应用:以实时目标检测 Web 应用为例,展示了模型在实际交互系统中的集成方式。
  5. 性能考量:贯穿实验讨论了模型精度与推理速度的平衡,这对边缘 AI 应用至关重要。

边缘设备目标检测为精准农业、工业自动化、质量检测、智能家居、环境监测等领域带来无限可能。数据本地处理可降低延迟、提升隐私、适应弱网环境。

未来可进一步探索:

  • 多模型流水线实现更复杂任务
  • 树莓派硬件加速方案
  • 目标检测与多传感器融合,打造更完整的边缘 AI 系统
  • 构建边缘 - 云协同的智能解决方案

边缘目标检测让 AI 能力深入物理世界,打造智能、响应迅速的系统,开拓人机交互与环境感知新前沿。

参考资料

创建于 2025/10/22 更新于 2025/10/22 12493 字 阅读约 25 分钟

提交勘误/建议