简介:面向深度学习课程设计、毕业设计与期末大作业场景,这份基于YoloV8的无人机检测项目包,提供了从数据预处理、模型训练到界面集成的完整实现。压缩包共18个文件,其中8个py文件为核心脚本,主导数据格式转换、数据集融合、模型训练、Qt界面及追踪器等模块,同时附带YAML配置、类别文本、结果图表与说明文档,整体仅283KB,结构清晰,便于直接运行调试。已有27人学习。项目针对无人机实时检测的实际需求,给出了一套含数据清洗、格式转换、调参训练与工程部署的完整方案,并包含界面展示与交互技巧;数据集配置和类别定义也已整理成独立文件,可帮助学习者深入理解YoloV8在目标检测任务中的落地路径,是一份面向课设、毕设的高价值参考资料。
1. 基于yolov8的无人机检测,先别急着训练:这个压缩包里到底装了什么
我拿到过不少类似「基于yolov8的xx检测设计.zip」的项目包,解压之后通常是数据集截图、一个train.py、一个best.pt,外加一份写得不太全的README。如果你是带着「我要复现并且用起来」的目的来的,我建议先别急着跑训练,先把这个项目的边界想清楚:无人机检测属于典型的小目标检测场景,yolov8虽然内置了多种尺度,但直接拿默认参数训练,十有八九在真实视频里框不准、漏检多。这篇文章会从环境配置、数据集处理、训练参数、损失曲线判断、模型导出到部署一套能落地的链路。适合刚接触yolov8的新手,也适合已经跑通训练、正在为部署效果头疼的工程师。核心只有一句话:让无人机检测从“能跑”变成“能用”,中间差的是对数据和部署细节的掌控。
2. 环境配置与数据集:让yolov8在你的机器上先跑起来
2.1 环境配置:torch、ultralytics与CUDA版本匹配
很多人拿到zip第一步是pip install ultralytics,然后直接报错或训练时莫名其妙OOM。其实yolov8的环境坑主要在torch和CUDA的版本匹配上。以我常用的GTX 1660 Ti为例,这张卡显存6GB,算力不算高,装CUDA 11.8配PyTorch 2.0.1是比较稳的组合;如果你硬上CUDA 12.x加新版本torch,虽然也能跑,但驱动、cuDNN和numpy版本经常打架。
我的做法是先建独立conda环境,再按显卡驱动版本选择torch。检查驱动支持的最高CUDA版本:
nvidia-smi看右上角CUDA Version,这个值只表示驱动支持的上限,不决定你需要安装的CUDA toolkit版本。之后用pip安装对应版本torch,例如CUDA 11.8环境:
pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics这里--index-url指定pytorch官方源,避免从默认pip源拉到CPU版;ultralytics最新版对torch版本有一定要求,如果后续训练时报AttributeError: module 'torch' has no attribute 'compile',大概率是torch版本过低,把torch升到2.0以上即可。另外我习惯在环境里再装numpy<2.0,因为某些旧版opencv和numpy 2.x不兼容,会报_ARRAY_API not found。
环境配好后,先别训,用官方权重跑一次推理验证GPU在干活:
yolo predict model=yolov8n.pt source='drone_test.mp4' device=0输出里看到device: cuda,并且视频画出了检测框,就说明环境正常。这里device=0指定第一张GPU卡;只有CPU就改device=cpu,但速度会慢一个数量级。对6GB显存来说,跑yolov8n用640输入分辨率batch=8是安全的;如果你用GTX 1660 Ti跑yolov8s,batch就得降到4,否则训练时直接OOM。
2.2 无人机数据集:公开数据集与自己标注的转换
无人机检测的数据集和普通目标检测不一样:无人机在画面里通常只有几十个像素,需要高分辨率图像或密集小目标的标注。常见的有VisDrone、UAVDT,这些公开数据集的标注格式大多是VOC(XML)或COCO(JSON),而yolov8默认要YOLO格式(txt每行:类别 cx cy w h)。拿到zip里的数据集,第一步往往是做格式转换。
我最常用的转换逻辑是:把XML解析出来,归一化坐标,写到对应图片同名的txt文件里。下面这段脚本可以处理VOC格式:
import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_dir, out_dir, classes): for xml_file in Path(xml_dir).glob('*.xml'): tree = ET.parse(xml_file) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) txt_path = Path(out_dir) / (xml_file.stem + '.txt') with open(txt_path, 'w') as f: for obj in root.iter('object'): cls_name = obj.find('name').text if cls_name not in classes: continue cls_id = classes.index(cls_name) box = obj.find('bndbox') x1, y1, x2, y2 = [int(box.find(t).text) for t in ('xmin','ymin','xmax','ymax')] x_center = (x1 + x2) / 2 / img_w y_center = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h f.write(f'{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n') classes = ['drone'] voc_to_yolo('annotations', 'labels', classes)这个脚本有个容易被忽略的点:yolov8训练时要求txt文件与图片文件同名,并且目录结构一般是images/train和labels/train分开。如果你把转换后的txt直接放在图片目录下,训练时会有警告。另外,边框坐标全部归一化到0~1,不保留原像素值,这是yolo格式的特点。
注意:某些开源标注导出的XML里,边框是浮点数,且没有归一化。转换前先打印几条数据检查坐标范围,避免出现大于1的坐标导致loss变成NaN。
对于没有标注数据的场景,你得自己标。我一般用LabelImg或X-AnyLabeling,标注框尽量贴近无人机边缘,不要包含螺旋桨的旋转轨迹,否则模型会学到一坨模糊的轮廓。标注完导出VOC格式,再用上面脚本转成yolov8需要的txt。
2.3 用预训练权重跑一次推理:验证整个链路
数据集准备好之后,先别急着从头训。一个稳妥的做法是先用官方yolov8n.pt在无人机视频上跑一下,看看预训练模型在自然图像上对无人机的反应。你会发现它对小目标几乎不检——这很正常,因为COCO里无人机样本太少。但我们用它的目的不是要检测结果,而是要确认从视频解码、图像resize、模型推理到画框显示这一条链路是否通畅。
我自己会写一个推理脚本,把检测结果叠加后保存成视频,方便逐帧查看:
from ultralytics import YOLO import cv2 model = YOLO('yolov8n.pt') cap = cv2.VideoCapture('drone_test.mp4') out = cv2.VideoWriter('result.mp4', cv2.VideoWriter_fourcc(*'mp4v'), 30, (1920, 1080)) while cap.isOpened(): ret, frame = cap.read() if not ret: break results = model.predict(frame, conf=0.35, iou=0.5, imgsz=1280) annotated = results[0].plot() out.write(annotated) cap.release() out.release()这里的imgsz=1280值得注意:无人机检测的输入分辨率直接决定小目标的特征保留程度,默认640在低分辨率视频里很容易漏检,用1280会好很多,但显存占用会翻倍。conf=0.35是置信度阈值,调低会召回更多候选框但误检也会增加;iou=0.5是NMS阈值,用于去除重叠框。如果这一步发现推理速度太慢,考虑换用yolov8s或从视频里抽帧检测,而不是每帧都跑。
跑通这一步后,你已经具备继续训练的基础。接下来要做的就是切好数据集,进入真正的训练环节。
3. 训练参数与损失曲线:让无人机检测模型真正收敛
3.1 数据yaml:类别、路径与验证集划分
yolov8训练第一步是写一个data.yaml。很多人从网上下载的项目zip里就带一个写好的yaml,但里面路径是别人电脑上的绝对路径,直接训练必报Dataset not found。我的做法是把这个yaml改成相对路径,基于项目根目录定位。
# drone.yaml path: ./datasets/drone train: images/train val: images/val test: images/test nc: 1 names: 0: dronepath是数据集根目录,train和val是相对于根目录的图片路径,注意不要写成/home/user/...这种绝对路径。nc是类别数,这里只有无人机一类;如果zip里包含多个类别,需要一一列出。这里有个容易被忽略的坑:yolov8会自动去根目录下找labels目录,它要求每张图片对应一个同名txt文件,txt里的类别id必须从0开始连续。
数据切分上,我一般用脚本按比例随机分配,而不是复制图片。下面是按7:2:1切分训练、验证、测试的简单脚本:
import random from pathlib import Path import shutil img_dir = Path('datasets/drone/images_all') train_dir = Path('datasets/drone/images/train') val_dir = Path('datasets/drone/images/val') test_dir = Path('datasets/drone/images/test') for d in (train_dir, val_dir, test_dir): d.mkdir(parents=True, exist_ok=True) imgs = list(img_dir.glob('*.jpg')) random.shuffle(imgs) n = len(imgs) for i, img in enumerate(imgs): label = Path(str(img).replace('images_all', 'labels_all')).with_suffix('.txt') if i < int(n * 0.7): shutil.copy(img, train_dir / img.name) shutil.copy(label, train_dir / label.name) elif i < int(n * 0.9): shutil.copy(img, val_dir / img.name) shutil.copy(label, val_dir / label.name) else: shutil.copy(img, test_dir / img.name) shutil.copy(label, test_dir / label.name)这个脚本的逻辑是:先把所有图片随机打乱,再按比例复制到对应目录,同时复制同名label。复制而不是移动的好处是原始标注不会被破坏,后悔药还是得留一颗。注意如果你的标注文件扩展名不是txt,或者label目录结构不一致,需要先调整路径映射。
3.2 关键训练参数:epochs、batch、imgsz、patience的设置逻辑
训练命令看起来简单,但参数值直接决定你显卡够不够用、模型能不能收敛。以GTX 1660 Ti 6GB显存为例,跑yolov8n勉强能上batch=16、imgsz=640;如果imgsz=1280,batch只能降到4,否则直接OOM。我常用的训练命令:
yolo train data=drone.yaml model=yolov8n.pt epochs=100 batch=8 imgsz=640 patience=15 device=0每一个参数都是血泪经验换来的。epochs=100对无人机这种单类小目标数据集来说足够,再多容易过拟合;batch=8在6GB显存上配合imgsz=640是安全值,如果你显存紧张,可以尝试batch=4配合accumulate=2,等效batch还是8;patience=15表示验证集mAP连续15个epoch不上升就提前停止,防止半夜训练白跑。device=0指定单卡。
这里有个很多人忽略的参数cache,设置cache=True会把数据集缓存到内存里,能显著减少每个epoch的磁盘IO,但6GB内存可能不够,我一般用cache=disk,把缓存写到磁盘上,速度介于纯内存和纯磁盘之间。还有一个关键点:model=yolov8n.pt表示从预训练权重开始训练。对于无人机检测,我不建议从yolov8n.yaml随机初始化训练,那样收敛慢且mAP低。使用COCO预训练权重做迁移学习,哪怕COCO里没有无人机,底层纹理特征仍然是有效的。
3.3 损失函数曲线:怎么判断欠拟合、过拟合和训练翻车
训练跑起来后,很多人只看results.png里的最后一个图,其实应该全程关注。yolov8训练日志会输出box_loss、cls_loss、dfl_loss,同时在runs/detect/train目录下生成results.csv。我一般用下面这段脚本画损失曲线,比yolo自带的图更灵活:
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('runs/detect/train/results.csv') df['epoch'] = range(len(df)) plt.figure(figsize=(10, 4)) plt.plot(df['epoch'], df['train/box_loss'], label='train_box_loss') plt.plot(df['epoch'], df['val/box_loss'], label='val_box_loss') plt.xlabel('epoch') plt.ylabel('loss') plt.legend() plt.title('drone yolov8 loss curve') plt.savefig('loss_curve.png')看曲线时,我关注的三个信号:第一,train和val的loss都持续下降,没有剧烈震荡,说明正常收敛;第二,train loss下降但val loss不降反升,说明过拟合,需要增加数据增强或提前停止;第三,val loss在前几个epoch就变成NaN或直接爆掉,大概率是学习率太高或数据里有异常的边界框坐标(比如坐标大于1或宽高为负)。遇到第三种情况,先检查数据集,而不是调参数。
有一个「玄学」经验:yolov8的loss曲线并不是理想的平滑下降,而是阶梯状下降,这是因为学习率调度器在特定epoch衰减。如果曲线长时间不降,可以把lr0从默认的0.01调到0.001再试,但不要低于0.0005,否则收敛慢到你想砸电脑。
除了损失曲线,我还会看热力图来辅助判断模型是否学到了无人机特征。无人机在图上时,热力图应集中在无人机区域;如果热力图集中在背景或整张图均匀分布,说明模型学到了背景纹理而非目标本身。实现方式可以借助pytorch的hook导出特征图:
import torch from ultralytics import YOLO model = YOLO('runs/detect/drone/weights/best.pt').model activation = {} def hook_fn(name): def fn(module, input, output): activation[name] = output.detach() return fn model.model[-2].register_forward_hook(hook_fn('last_conv')) # 推理一张真实图片 img_tensor = torch.rand(1, 3, 640, 640) # 实际应替换为预处理后的图像 model.eval() with torch.no_grad(): model(img_tensor) heatmap = activation['last_conv'].mean(dim=1).squeeze(0)这段代码通过注册前向hook,把最后一个卷积层的输出抓出来,然后对通道维度取均值,得到粗略的响应图。这里的model.model[-2]是ultralytics内部模型结构的倒数第二层,不同版本可能位置不同,你可以在导出网络结构图时确认具体层名。热力图只作辅助,别把它当成唯一标准。
4. 从权重到部署:onnx导出与rk3588上的yolov8落地
4.1 导出网络结构图:看清模型长什么样
训练出best.pt后,很多初学者以为完事了。但如果你要做部署,必须知道模型结构。yolov8的模型结构可以通过netron或torchinfo查看。我一般先导出onnx,再用netron打开看网络结构,顺带检查有没有不支持导出的算子。
导出onnx很简单:
yolo export model=best.pt format=onnx opset=12 imgsz=640命令参数:format=onnx导出为onnx格式,opset=12是onnx算子集版本,rk3588的rknn-toolkit2对opset 12支持比较稳定,太高可能遇到不兼容的算子;imgsz=640指定导出模型的输入尺寸,这会固化输入分辨率,部署时如果输入不同尺寸,需要先resize到640。
导出后可以用netron打开onnx文件,查看每个节点的输入输出形状。无人机检测模型通常包含一个backbone、一个neck和一个head输出。如果你想输出网络结构图而不打开GUI,可以用python脚本打印每一层名字:
import onnx model = onnx.load('best.onnx') for node in model.graph.node: print(node.op_type, node.name)这段代码会列出所有节点,虽然不直观,但能帮你确认模型是否包含动态维度或不支持的算子。如果出现Resize、ScatterND这类算子,在rk3588转换时需要特别处理。我一般还会用onnx.checker.check_model(model)校验一遍,报错的话先修onnx导出参数,再进入下一步。
4.2 模型压缩与导出:pt到onnx到engine的完整链路
如果目标平台不是rk3588,而是NVIDIA Jetson或桌面GPU,导出的onnx可以继续转TensorRT engine。这里有个经验:TensorRT对固定输入尺寸的模型优化最好,所以导出onnx时就不要用dynamic模式。转换命令:
trtexec --onnx=best.onnx --saveEngine=best.engine --fp16--fp16开启半精度推理,在GTX 1660 Ti上速度能提升约40%,但精度略有下降。如果检测框抖动厉害,可以改回FP32。注意trtexec是TensorRT自带的工具,需要安装TensorRT才可用。对于无人机这种小目标检测,我建议保留FP32,因为FP16对小于8像素的目标特征损失明显。
如果你要部署到Jetson或者边缘盒子,还要注意batch size。默认导出onnx的batch为1,如果要用trtexec测性能,加--shapes=input:1x3x640x640固定动态轴。TensorRT第一次推理会做引擎优化,耗时较长,所以实际部署时要把引擎加载和推理分开,加载一次,重复推理。
4.3 rk3588部署:rknn转换与npu推理的注意事项
rk3588是瑞芯微的8核处理器,内置6 TOPS NPU,很多无人机边缘检测盒子都用它。yolov8部署到rk3588的常规路径是:pt → onnx → rknn。rknn-toolkit2是瑞芯微提供的转换工具,运行在PC端(x86 Linux),转换命令如下:
rknn_convert.py --model best.onnx --output best.rknn --target rk3588 --quantize fp16参数说明:--target rk3588指定目标平台;--quantize fp16表示用fp16量化。rknn-toolkit2还支持int8量化,int8在rk3588上推理最快,但小目标精度掉得厉害。我建议先跑fp16,确认检测效果后再考虑int8。注意转换时需要指定onnx的输入尺寸,如果你导出的onnx是640x640,推理时也要把图像resize到640,不能临时改。
实际部署时,rk3588推理yolov8n在fp16下大约能跑到30~40ms一帧,如果达不到这个数,检查是否真的用了NPU。很多人调用了CPU推理,速度差了十倍。rknn的Python API里,初始化时需要明确设置NPU核心数:
from rknn.api import RKNN rknn = RKNN() rknn.load_rknn('best.rknn') rknn.init_runtime(target='rk3588', npu_core_id=0)npu_core_id=0指定使用第一个NPU核心;rk3588有三个NPU核心,可以创建三个线程分别推理三个视频流,但多核共享内存带宽,实际加速比不是线性的。这个init_runtime是rknn官方API的标准用法,实际项目里我会把前后处理(letterbox、NMS)放在CPU上并行,避免阻塞NPU推理。
在rk3588上跑推理,我习惯把整个流程写成如下骨架:
import cv2 import numpy as np from rknn.api import RKNN rknn = RKNN() rknn.load_rknn('best.rknn') rknn.init_runtime(target='rk3588', npu_core_id=0) img = cv2.imread('drone.jpg') img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (640, 640)) img = img.astype(np.float32) / 255.0 img = np.expand_dims(img, axis=0).transpose(0, 3, 1, 2) outputs = rknn.inference(inputs=[img]) print(outputs[0].shape)这个骨架的主要参数:输入需要是NCHW格式,归一化到0~1,和yolov8在ultralytics内部做的预处理保持一致。如果你在PC上测试正常、在板子上输出全零,先检查输入图像格式和归一化方法是否一致。另外,如果你接的是摄像头视频流,输入图像不要用cv2.imread,而是从解码器拿到帧后直接做cvtColor和resize。图像格式转换在CPU上很费时间,对帧率要求高的话可以改用RGA硬件加速,但那是另一个大坑,小项目先用cv2跑通再说。
5. 无人机检测的5个常见坑:现象、原因与解决
5.1 无人机尺寸太小,模型根本学不到特征
现象:训练时loss下降很快,但验证集mAP@0.5只有0.1左右,检测框要么乱飞要么漏检。 原因:数据集中无人机平均像素尺寸太小,比如在1080p画面里只有20×20像素,模型的下采样倍数最低到32倍,特征图上一个点对应的原图区域是32×32,目标的小特征在深层特征图里几乎消失。 解决:要么把训练图像分辨率提高,训练和推理都用imgsz=1280;要么在数据yaml里增加增强参数,比如使用scale=0.5让模型看到更大相对尺寸的无人机;最直接的办法是裁剪数据集中包含无人机的小块区域放大后加入训练。我一般会把原始图像按4:1比例混合原图和裁剪放大图,这样模型见过两种尺度的目标,泛化更好。
5.2 训练时loss正常但mAP很低:输入尺度和检测头的匹配问题
现象:train/box_loss降到0.05以下,val/box_loss也不高,但mAP就是上不去,精确率和召回率都低。 原因:yolov8是anchor-free模型,没有anchor超参数设置,但它的检测头有三个尺度。如果无人机目标尺寸分布超出了最浅检测头能覆盖的范围,尤其是极小目标,默认的head输出步长(8、16、32)里最小的8倍步长在640输入下对应80×80特征图,每个格点负责8×8像素区域。如果目标只有6像素,特征图上的响应非常弱。 解决:把输入分辨率提高到1280,让目标在特征图上占据更多像素;或者使用yolo的tune功能搜索最佳imgsz;更进阶的做法是增加一个检测头,但这需要改模型结构。对你从zip里拿到的项目,先不要改结构,优先升分辨率。如果升分辨率后显存不够,把batch降到2甚至1,配合梯度累积。
5.3 显存溢出:batch与图像尺寸的取舍
现象:训练到某个epoch直接CUDA out of memory,之前好好的,后面突然爆掉。 原因:最常见的是你用了imgsz=1280和较大的batch,而前几个epoch因为数据加载或缓存策略不同,内存峰值后移。如果跑到一半爆显存,往往是验证集里有一张特别大的图片没有resize,导致预处理分配了额外内存。或者你开了plots=True,可视化图像叠加缓冲占用了显存。 解决:先用最小配置跑一遍确认能跑通:batch=2 imgsz=640,再逐步上调。用batch=8时设置cache=disk减少内存峰值。如果确定是图片尺寸异常,在数据预处理时强制cv2.resize到统一尺寸,而不是依赖yolo内部自适应。还有一个技巧:在训练命令里加workers=0,有时候dataloader多进程在Windows上会重复加载数据导致显存峰值。
5.4 推理速度远低于预期:API调用与设备端部署的区别
现象:在PC上用model.predict()每帧要200ms,但文档说yolov8n能在GTX 1660 Ti上跑到50ms。检测速度为什么差这么多? 原因:model.predict()包含完整的预处理、推理、NMS后处理以及结果对象构建,每次调用还会重新初始化一些组件,内存分配开销大。另外,默认device=0可能没有真正使用TensorRT,只是PyTorch的CUDA推理,速度自然慢。 解决:部署时用导出的onnx或engine推理,并配合批处理或视频流多线程。一个简单的优化是预热模型:第一次推理前用空张量跑一次,后续速度才稳定。在PC上做性能测试时,用time测量连续100次推理的平均时间,而不是单次。如果你发现CPU占用高,检查是否误把device设成了cpu。还要记得关闭梯度计算——推理时torch.no_grad()能省下不少显存和计算。
5.5 数据集里背景太干净:模型在真实场景翻车
现象:在训练集和验证集上mAP很高,但放到真实飞行视频里,误检率骤增,把屋顶、飞鸟、树叶都框成无人机。 原因:公开数据集或自己拍的素材往往背景单一,比如天空占比大。模型学到了“天空中的小黑点”这个特征,而不是无人机本身的结构特征。真实场景里有大量相似的纹理,导致误检。 解决:训练时需要做数据增强——随机裁剪背景、加入随机噪声、水平翻转、亮度抖动;推理时提高conf阈值到0.5;更有效的办法是收集真实飞行中的负样本(没有无人机的视频帧),加进训练集。yolov8没有直接支持背景类别,但你可以加一个bird或其他干扰类,让模型学会区分。注意标注时不要把远景中的无人机漏标,否则它会变成隐形的负样本,把模型的置信度拉低。
6. 进阶:提升无人机小目标检测率的三个实用技巧
6.1 切片推理:把大图切成小块分别检测
对小目标,最直接有效的技巧是切片推理。把1920×1080的帧切成4个960×540的小块,分别用640输入检测,再合并结果。这样小目标在输入中所占像素比例翻倍,检测率提升明显。代价是计算量增加4倍,但可以用多线程并行处理。我在边缘盒子上常用4路并行,每路接一个NPU核心,帧率反而能保持稳定。
6.2 数据增强:把背景多样性补上去
在训练命令里开启增强。例如hsv_h=0.02增加色调扰动,degrees=30让模型适应无人机旋转,translate=0.2模拟目标偏移。我常用下面命令:
yolo train data=drone.yaml model=yolov8n.pt epochs=100 imgsz=1280 augment=True增强不是越多越好,对无人机这种小目标,scale和degrees最管用,flipud慎重开,因为真实飞行中无人机很少上下颠倒。
6.3 类别设计:参考安全帽检测项目的拆类思路
很多安全帽检测项目会把类别拆为“戴帽”和“不戴帽”,而不是单一“安全帽”,这样模型必须学习局部细节,准确率更高。无人机检测同理,你可以把类别拆成“四旋翼”“固定翼”,或者“近景无人机”“远景无人机”。远景和近景目标的特征差异很大,拆开后每个类内更紧凑,模型收敛更容易。如果你从zip里拿到的项目只有单一drone类,试着按目标像素面积分两个类,往往能带来mAP提升。
我在做第一个无人机项目时,只盯着loss下降,忽视了小目标和背景干扰,最后在实地测试中翻车。后来总结出三条经验:先提升输入分辨率,再想办法让类别内更紧凑,最后用切片推理兜底。希望帮到你。
本文还有配套的精品资源,点击获取