
简介这是一套基于YOLOv8的战斗机类型识别检测系统完整工程面向深度学习、计算机视觉与目标检测方向的开发者和进阶学习者。项目以Python源码为主体配合yolov8n.onnx推理模型与PyQt5打造的GUI界面开箱即可完成战斗机图像的检测与类型识别也适合作为课程设计或毕业设计的参考框架。压缩包共264个文件约19.25MB其中240张jpg为测试图片6个xml为标注文件6张png为训练过程中的mAP、P、R评估曲线另有3个py源码文件及onnx模型文件目录结构清晰便于按模块学习和二次开发。目前已有259人学习下载。借助该资源读者可直观了解YOLOv8的训练评估流程与推理部署方式参照评估曲线分析模型性能并通过替换数据集和调整参数进一步拓展识别类别对掌握深度学习目标检测完整链路具有较高实践参考价值。1. 解压后别急着跑这套YOLOv8战斗机识别系统到底解决了什么问题如果你拿到一个写着“基于yolov8的战斗机类型识别检测系统python源码onnx模型评估指标曲线精美GUI界面.zip”的压缩包先别急着双击运行。这里面的东西不是孤立的代码文件而是一条完整的链路python源码负责从训练到推理的完整逻辑onnx模型让系统不用安装几个G的PyTorch就能在普通电脑上运行评估指标曲线用来说服你模型真的能用GUI界面则把命令行操作变成了非程序员也能点开的桌面工具。这套系统解决的核心问题只有一个给定一张含飞机的图片或一段视频自动框出每个飞机目标并标注它的类型。适合手里有一批飞机图片、想在本地跑通一个可演示的目标检测系统的开发者也适合想学YOLOv8完整落地流程的人。2. 拆开源码包看结构推理、训练、评估三块骨架以及不装PyTorch跑通ONNX2.1 为什么选YOLOv8做机型识别anchor-free、C2f与细粒度任务的取舍先想清楚一个问题识别战斗机类型本质上是什么任务它不是图像分类而是“检测分类”的联合任务。你得先把飞机从背景里找出来再对框内的机型做区分。YOLOv8在这里是合理的选型理由有三点。第一YOLOv8是anchor-free的检测器。战斗机有大有小重型轰炸机和轻型战斗机的长宽比差异很大anchor-based方案需要针对数据分布调anchor尺寸而anchor-free直接在特征点上预测目标中心与边界省掉这一层人工调参。对一个小样本的机型识别项目来说少一个玄学变量就少一个坑。第二YOLOv8的C2f模块在保持推理速度的前提下增强了梯度回传对细粒度特征更友好。型号识别靠的是机翼布局、尾翼形状、进气道细节这些纹理级差异特征提取能力直接决定上限。第三YOLOv8的解耦检测头把分类和回归分成两个分支避免两个任务互相干扰训练收敛更稳。但这里有个边界要说清楚如果只是做机型识别YOLOv8不是唯一选择YOLOv5、RT-DETR都能干。选定YOLOv8更多是因为它的生态完整训练、验证、导出ONNX一条命令走完这对从业者来说比“理论指标高0.2个点”更值钱。另外机型识别是细粒度任务建议训练时输入分辨率不低于640×640否则锯齿和轰炸机这种外轮廓相似的目标真的会认错。2.2 源码包里常见的四块内容与“先读哪个文件”这类压缩包的结构大同小异通常包含四块python源码、权重文件、评估结果、GUI入口。我一般拿到包第一件事不是打开GUI而是先看目录结构找到推理主脚本把“模型加载→预处理→推理→画框”这条主链路先跑通。典型结构大致如下目录/文件职责备注train.py / train部分训练入口读data.yaml与预训练权重重训时才需要碰detect.py / predict部分推理入口加载pt或onnx做单张/视频识别先读这个gui/main_window.pyPyQt5桌面界面内部调用推理逻辑二次开发重点weights/best.ptPyTorch训练产物精度最高依赖torchweights/best.onnx导出后的部署模型不依赖torch用onnxruntime跑runs/detect/val/评估曲线与指标PR曲线、mAP、混淆矩阵都在这里requirements.txt依赖清单先看它再建环境先读顺序建议requirements.txt → detect.py → gui/main_window.py。很多新手上来就双击GUI报了一堆缺少模块的错其实是环境没建对。常见做法是新建一个Python 3.8-3.10的虚拟环境按requirements装依赖。这里特别提示一个认知差异onnx模型和onnxruntime是两回事。onnx是一种模型文件格式描述网络结构和权重onnxruntime是微软的推理引擎负责把这个格式跑起来。你要部署onnx模型只需要装onnxruntime不需要装PyTorch。很多教程让你在Ubuntu 20.04上搭CPU版YOLOv8环境其实如果只做推理一个onnxruntime加opencv就够了。2.3 最小可用的ONNX推理代码letterbox、归一化、后处理与NMS不管zip包里源码怎么组织onnx推理的核心逻辑始终是三步预处理、模型推理、后处理。下面这段代码可以直接复制出来替换成你自己的模型路径和图片路径先验证模型能不能出结果。假设模型是四类fighter、bomber、transport、helicopter输入尺寸640×640。import cv2 import numpy as np import onnxruntime as ort CLASSES [fighter, bomber, transport, helicopter] def letterbox(img, new_shape(640, 640), color(114, 114, 114)): # 保持宽高比缩放再补边到目标尺寸与训练时预处理保持一致 shape img.shape[:2] # 原图 H, W r min(new_shape[0] / shape[0], new_shape[1] / shape[1]) new_unpad (int(round(shape[1] * r)), int(round(shape[0] * r))) # W, H img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) dw (new_shape[1] - new_unpad[0]) / 2 dh (new_shape[0] - new_unpad[1]) / 2 left, right int(round(dw - 0.1)), int(round(dw 0.1)) top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, valuecolor) return img, r, (left, top) def postprocess(out, conf_thres0.25, iou_thres0.45): # out: 输出可能是 [1, 4num_classes, 8400] 或 [1, 8400, 4num_classes] preds out[0] if preds.shape[0] 84: # 训练维度在前需要转置 preds preds.transpose(1, 0) boxes, scores, cls_ids [], [], [] for pred in preds: score pred[4:].max() if score conf_thres: continue cls_id int(pred[4:].argmax()) x, y, w, h pred[:4] # 注意YOLOv8导出时默认输出的是中心点坐标 xywh不是 x1y1x2y2 x1, y1, x2, y2 x - w / 2, y - h / 2, x w / 2, y h / 2 boxes.append([x1, y1, x2, y2]) scores.append(float(score)) cls_ids.append(cls_id) if len(boxes) 0: return [], [], [] keep cv2.dnn.NMSBoxes(boxes, scores, score_thresholdconf_thres, nms_thresholdiou_thres) keep np.array(keep).flatten() return [boxes[i] for i in keep], [scores[i] for i in keep], [cls_ids[i] for i in keep] session ort.InferenceSession(best.onnx, providers[CPUExecutionProvider]) input_name session.get_inputs()[0].name img0 cv2.imread(test.jpg) if img0 is None: raise FileNotFoundError(test.jpg 不存在请换成你的测试图) img, ratio, (left, top) letterbox(img0) blob img[:, :, ::-1].transpose(2, 0, 1) # BGR - RGB, HWC - CHW blob np.ascontiguousarray(blob, dtypenp.float32) / 255.0 blob blob[None] # 增加 batch 维度变成 [1, 3, 640, 640] out session.run(None, {input_name: blob}) boxes, scores, cls_ids postprocess(out[0]) for box, score, cls_id in zip(boxes, scores, cls_ids): x1, y1, x2, y2 box # 还原到原图坐标去掉 letterbox 补边再除以缩放比例 x1, x2 (x1 - left) / ratio, (x2 - left) / ratio y1, y2 (y1 - top) / ratio, (y2 - top) / ratio cv2.rectangle(img0, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) label f{CLASSES[cls_id]} {score:.2f} cv2.putText(img0, label, (int(x1), int(y1) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(result.jpg, img0) print(检测完成结果已保存到 result.jpg)代码逻辑是标准的三段式letterbox函数保持宽高比缩放并补边到640这一步必须和训练时的预处理完全一致否则框的位置会整体偏移这是后处理阶段最常见的问题来源。预处理里把BGR转成RGB、HWC转成CHW、除以255归一化这三件套少一个模型输出立刻乱掉。后处理按置信度过滤候选框再用NMS去掉重叠框。这里有一个容易被忽略的细节很多YOLOv8导出ONNX时输出的是中心点xywh坐标而不是角点x1y1x2y2如果你把w和h当成了x2和y2框会整体错位。推理时几个关键参数conf_thres0.25是置信度阈值低于它的候选框直接丢弃实战中如果漏检多可以降到0.1如果误检多可以调到0.5iou_thres0.45是NMS的交并比阈值。输出8400这个数字也不用觉得神秘它来自YOLOv8三个检测头80×8040×4020×20三个尺度加起来正好是8400个候选位置。2.4 onnxruntime与PyTorch推理结果对比掉点多少算正常跑通onnx推理后有一个动作必须做用同一张测试图分别跑best.pt和best.onnx对比输出结果。常见做法是写一个脚本把两种方式检测出的框和置信度打印出来计算差异。正常情况下由于BN层折叠和算子融合onnx的mAP会略低于PyTorch但差距应该在0.5%以内视觉上几乎看不出差别。如果发现onnx的输出明显变差不要怀疑模型坏了。优先检查两个地方一是导出时是否开了simplify简化模型有些算子被融合后精度反而略降属正常现象二是输入图的做法是否一致PyTorch推理时ultralytics内部会对图像做letterbox而你手工实现的letterbox如果边界取整方式不同框就会偏移。我一般会让预处理函数返回ratio和pad偏移量后处理严格按这两个值还原基本不会出问题。3. 训练自己的战斗机数据集labelme转YOLO、参数含义与评估指标曲线怎么读3.1 数据准备labelme标注转YOLO格式的脚本与目录规范拿到别人的zip包只是第一步真正想让系统认自己的目标就得重新训练。战斗机类型识别的数据标注社区里最常用的工具是labelme因为可以画多边形对飞机这种轮廓不规则的目标比矩形框更准确。但YOLOv8训练需要的是txt格式的标签每行一个目标类别id 归一化的中心点x、y 归一化的框宽高。所以需要一个转换脚本把labelme的json转成YOLO的txt。import json import glob import os def labelme2yolo(json_path, out_dir, classes): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w data[imageWidth] img_h data[imageHeight] name os.path.splitext(os.path.basename(json_path))[0] lines [] for shape in data[shapes]: label shape[label] if label not in classes: continue # 跳过没在类别列表里的标注 cls_id classes.index(label) pts shape[points] xs [p[0] for p in pts] ys [p[1] for p in pts] x1, y1 min(xs), min(ys) x2, y2 max(xs), max(ys) cx ((x1 x2) / 2) / img_w cy ((y1 y2) / 2) / img_h bw (x2 - x1) / img_w bh (y2 - y1) / img_h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) if lines: with open(os.path.join(out_dir, name .txt), w) as f: f.write(\n.join(lines)) classes [fighter, bomber, transport, helicopter] os.makedirs(labels/train, exist_okTrue) for j in glob.glob(labelme_json/*.json): labelme2yolo(j, labels/train, classes)这段脚本做了几件关键的事把多边形的外接矩形当成检测框对飞机这类刚体目标够用坐标全部除以图片宽高归一化这样不同分辨率图片可以混合训练最后按YOLO的格式写txt文件一行一个目标。如果你想要更精准的贴合轮廓YOLOv8也支持seg模式训练但对型号识别来说矩形框里已经包含足够的纹理信息检测框模式性价比更高。目录结构必须严格遵循YOLO约定数据集根目录下放images和labels两个大目录各自再分train和val子集。注意图片和标签的文件名必须一一对应jpg对应txt少了任何一个都会在训练时报错。3.2 训练命令逐项拆解epochs、imgsz、batch与yolov8必调参数数据准备好了接下来是训练。YOLOv8用命令行训练先看一条最小命令再逐项拆解参数。假设你的数据配置文件叫fighter.yaml。yolo detect train \ datafighter.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ optimizerAdamW \ lr00.001 \ patience20 \ projectruns \ namefighterfighter.yaml的内容也很简单path: ./dataset train: images/train val: images/val names: 0: fighter 1: bomber 2: transport 3: helicopter参数逐个说清楚。model这里填yolov8n.ptn是nano版本模型最小、速度最快显存紧张或CPU训练时可以用它起步如果显存够换成yolov8s.pt精度会更好。epochs100是训练轮数小数据集50轮就能看到收敛趋势官方默认值也是100。imgsz640是输入分辨率机型识别吃细节如果显存允许我一般会用到960代价是训练时间大约翻倍。batch16是每批图片数它和显存强相关。batch太大爆显存会直接OOM太小了BN层不稳定导致震荡。一个经验值是8G显存跑yolov8n用16跑yolov8s用8。optimizerAdamW是细粒度分类和小数据集的首选收敛稳SGD需要更大的epochs才有效果。lr00.001是初始学习率用预训练权重做迁移学习时0.001比默认的0.01更可靠。patience20是早停机制连续20轮mAP不涨就自动停作用是省时间、防过拟合。训练启动后屏幕上会实时刷新每个epoch的loss、precision、recall和mAP。如果看到box_loss和cls_loss从头到尾一条直线说明学习率太高或者数据没配对先停下来检查data.yaml里的路径和标签格式。还有一个小建议训练时别把amp自动混合精度关掉它能让训练速度快30%-40%对精度影响极小。3.3 训练产物与评估指标曲线PR_curve、F1_curve、results.png怎么看训练结束后runs/fighter/目录下会有一堆文件。新手最容易忽视的是这些评估图而这恰恰是判断模型能不能用的关键。这个zip包里既然带了“评估指标曲线”就应该知道每张图怎么读。第一张是results.png里面通常有7条曲线box_loss边框回归损失、cls_loss分类损失、dfl_loss分布焦点损失、precision、recall、mAP50、mAP50-95。看这张图的顺序是先看loss是否下降并收敛再看mAP50是否到了0.9以上最后看precision和recall是否平衡。如果precision高、recall低说明模型“宁缺毋滥”漏检多反过来说明误检多。第二张是PR_curve.png横轴recall纵轴precision曲线和坐标轴围成的面积就是mAP。曲线越靠近右上角越好。如果曲线呈“L”形说明模型在低召回率时精度高实际部署时需要调低conf阈值才能多找回一些目标。第三张是F1_curve.png用来看最优置信度阈值。F1是precision和recall的调和平均曲线最高点对应的阈值就是理论上最优的conf_thres。常见做法是先看F1曲线找到最优阈值再把这个值写进推理脚本而不是无脑用0.25。第四张是confusion_matrix.png混淆矩阵。如果bomber和transport互相认错说明这两个类别的外形特征过于接近需要增加这两类的训练样本量或者考虑用更高的输入分辨率。这张图能直接告诉你数据瓶颈在哪比盯着mAP数字更有用。3.4 从best.pt导出best.onnxpytorch转onnx的官方命令与动态尺寸训练完模型以pt格式存储但部署时我们要的是onnx。YOLOv8提供了官方导出命令不需要手写torch.onnx.export。yolo export modelruns/fighter/weights/best.pt formatonnx opset12 dynamicTrue simplifyTrue这里几个参数有讲究。opset12是ONNX算子集的版本太老有些算子不支持太新则旧版本onnxruntime跑不了12是兼容性最稳的选择。dynamicTrue表示输入尺寸动态可变导出后可以喂任意分辨率的图但换来的是推理速度略降和部分设备兼容性问题如果你确定部署时只用固定640×640分辨率建议dynamicFalse速度更快转RKNN或NCNN这类边缘端格式时也更顺利。simplifyTrue会调用onnx-simplifier做图优化能去掉一些冗余算子。导出完成后建议做一个精度回测用验证集分别跑pt和onnx对比mAP50。如果onnx精度掉了超过1个点先检查导出日志里有没有警告再考虑重新导出。掉点0.5%以内都算正常这是BN折叠和算子重排带来的固有精度损失。4. 避坑从pt导出ONNX到GUI部署五个必然遇到的翻车现场4.1 现象onnx推理全乱框位置漂移、置信度全部异常现象是模型能跑但画出来的框歪七扭八置信度要么接近1要么全是0和pt推理结果完全对不上。原因是预处理和后处理与训练时不一致。最常见的两个错误一是推理时用cv2.resize直接拉伸到640×640而训练时用的是letterbox保持宽高比补边导致目标被拉变形二是后处理把中心点xywh当成x1y1x2y2用了框当然全是错的。解决方法是严格对齐ultralytics的预处理逻辑letterbox里的resize比例r和补边偏移量(left, top)必须传回后处理还原坐标时先减偏移再除以比例。调试时可以写一段对比逻辑用pt模型和onnx模型处理同一张图打印每个框的坐标和置信度差值在几个像素内才算正常。4.2 现象一个框都检测不到pt能出框但onnx输出全空现象是PyTorch推理正常导出onnx后一切结果为空没有任何检测框输出。原因是两种可能性一种是用simplifyTrue过度简化后某些算子被错误合并另一种是模型输出结构与你的后处理预期不匹配。YOLOv8官方导出默认不带NMS输出是全部候选框8400×8580类时需要自己做NMS但有些版本的源码会导出带NMS的onnx此时输出直接就是最终框如果你还是按原始输出做后处理自然什么都拿不到。解决方法是先打印onnx输出shape确认结构然后用dynamicFalse固定输入尺寸重新导出如果还是空的去掉simplify再试一次。这类玄学问题最快排查路径就是二分法改一个变量跑一次直到找到是哪个环节吞掉了结果。4.3 现象GUI里视频很卡拖动窗口明显掉帧现象是GUI界面能打开但播放视频检测时画面一卡一卡拖动窗口也卡顿本来GPU推理只要几十毫秒一帧界面却像幻灯片。原因是把推理直接放在了主线程里执行。PyQt的界面绘制和事件循环都在主线程推理一旦阻塞主线程界面就停止响应用户操作。另一个常见原因是每帧都用QImage从GBR转换并拷贝频繁分配内存拖慢了整体帧率。解决方法是把推理放进QThread用信号槽把结果传回主线程同时做帧采样不用每帧都推理视频场景下每3帧推理一次画面流畅度和检测实时性之间完全够用。这部分代码在第5章展开先把架构思路记住界面线程绝不做耗时操作。4.4 现象mAP曲线很高但拿真实图片测却啥也识别不出来现象是评估曲线很好看mAP50已经0.9以上但拿一张没参与训练的实拍图或网络图测试模型一个目标都检测不到。原因通常有三类一是训练集和测试集分布差异太大比如训练集全是卫星视角的俯视图测试图却是一张地面仰拍的飞机照片二是类别不平衡某个类别在数据集中占80%以上模型只学会了这类三是推理时confidence阈值设置过高真实场景目标遮挡、模糊置信度天然偏低。解决方法先看混淆矩阵和每类的mAP找出短板类别回训练集给该类补数据推理阈值按F1曲线选择而不是拍脑袋用0.5打通一条快速验证路径比如用手机随便拍几张飞机图丢进推理脚本看看输出这一步能少走很多弯路。训练时保留mosaic和mixup增强也能提升模型在真实场景下的泛化能力。4.5 现象CPU机器上onnx推理太慢一帧要一秒钟现象是模型部署到只有CPU的旧电脑上推理一张640×640的图耗时800ms甚至更久完全没法用。原因是默认onnxruntime会话只用单线程模型本身又是s或m这样的大模型float32精度计算在CPU上本来就吃力。解决方法是设置intra_op_num_threads多线程推理同时换小模型或者做INT8量化。动态量化最简单一个命令就能压掉模型体积。from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic(best.onnx, best_int8.onnx, weight_typeQuantType.QInt8)量化后体积约减半CPU推理速度能提升20%-40%。如果想更快需要走静态量化那要用校准集统计激活值范围精度损失比动态量化更可控但实现复杂度也更高。还有个方向是剪枝和蒸馏不过对这个小项目来说性价比不高。如果后续要往RK3588这类边缘设备上部署ONNX转RKNN是不错的路径同样要先确认onnx模型没有动态维度转换时会更顺利。5. 把GUI与ONNX完整串起来QThread异步推理、曲线面板联动与最终验证5.1 用QThread把推理搬出主线程界面不卡才是“精美”的前提GUI里的“精美”不只是配色好看最重要的是交互不卡顿。用PyQt5做界面时推理逻辑必须放到QThread里。from PyQt5.QtCore import QThread, pyqtSignal import queue class DetectWorker(QThread): result_ready pyqtSignal(object) def __init__(self, session): super().__init__() self.session session self.frame_queue queue.Queue(maxsize2) self.running True def run(self): while self.running: frame self.frame_queue.get() boxes, scores, cls_ids inference(self.session, frame) self.result_ready.emit((frame, boxes, scores, cls_ids))关键设计在frame_queue.maxsize2队列只缓存两帧。视频输入时如果推理速度跟不上采集速度生产者的新帧会被丢弃这比无限堆积内存要健康得多也保证了界面永远显示的是最新检测结果。线程内循环跑推理通过信号把结果传回主线程主线程只负责画框和刷新画面。这样拖动窗口、调整阈值滑块都不会卡。界面布局上左侧放视频显示区域右侧放检测结果列表和参数面板。把conf阈值做成滑块绑定后处理函数的参数用户拖动滑块立刻生效这个交互比改代码重启方便十倍。评估曲线图可以用QLabel加载PNG显示PR曲线、F1曲线各放一页部署后随时能对着曲线调整阈值。5.2 最终验证清单与打包成exe传给同事整套系统做完最后一步是验证和打包。验证顺序我习惯这样做先拿训练集里的一张图测确认模型没有坏再拿一张从未参与训练的同类图片测确认泛化能力最后用一段视频测确认帧率可以接受。如果验证集中有拥挤场景和遮挡场景也要专门测一下这两个场景最能暴露模型短板。打包用PyInstaller注意在spec文件里把onnx模型文件加进去同时确认onnxruntime的dll被正确收集否则拷到别人电脑上会闪退。我自己的习惯是每改一次预处理或后处理就用同一张测试图把pt和onnx的框打印出来对比绝不靠肉眼看效果下结论。这套工作流踩过太多次“玄学bug”之后才沉淀下来希望帮到你。本文还有配套的精品资源点击获取