
简介面向遥感图像目标识别场景的YOLOv5完整项目包涵盖卫星图像目标检测的Python程序代码、训练好的模型权重、网络结构配置及配套说明文档适配高分项目与科研实训需求。项目代码经过验证运行稳定适合计算机视觉相关专业学生用于毕业设计、课程项目或竞赛初筛也适合初学者系统接触YOLO系列检测流程。压缩包共156个文件主要包含Python脚本、YAML网络配置、pt模型权重、PNG/JPG图像样本、运行日志与训练记录等类型整体约242.81MB附带Dockerfile和shell脚本便于快速复现环境与训练部署。目前已有42人学习下载。资源内提供可直接调用的权重文件与可视化推理结果对遥感影像执行目标识别演示时无需从零训练events.out.tfevents等日志可用于分析收敛趋势和评估模型表现说明文档则辅助理解数据组织、训练参数与调优方向方便在此基础上做类别扩展和精度优化。1. YOLOv5 遥感图像目标识别项目一个已经跑过训练的资源包值不值得拆拿遥感图像做目标识别最卡人的往往不是模型精度而是从环境、数据到能跑通的全过程。这套「YOLOv5 遥感图像目标识别」项目资源包是一个已经在 GPU 上跑出过训练日志的完整工程除了算法代码和说明文档目录里还有 TensorBoard 事件文件events.out.tfevents.1599910333.C-000015-GPU.31726.0和 Dockerfile。这意味着训练确实发生过日志不是空理论环境被固化过特别适合以「复现训练」为起点。适合三类人要快速出成果的毕业设计/课程设计学生想验证 YOLOv5 用于卫星/无人机影像目标检测流程的工程师以及从「跑通代码」进阶到「训练自己数据集」的初学者。我在下文的拆解会按文件日志→数据准备→训练参数→推理排查的顺序走命令都能直接粘进终端。2. 资源拆解从 tfevents 到 Dockerfile把「能跑」的证据先找出来2.1 先别急着训练两个事件文件已经说明训练发生过拿到任何项目资源我第一件事不是开 train.py而是先翻目录。这个包根目录下躺着两个events.out.tfevents.1599910333.C-000015-GPU.31726.0和events.out.tfevents.1599909779.C-000015-GPU.24092.0文件不懂的人会当垃圾删掉懂的人知道这是宝贝。它们是 TensorBoard 写入的事件日志文件名里1599910333是 Unix 时间戳换算下来是 2020 年 9 月C-000015-GPU是训练那台机器的节点名。两个文件名差 5 分钟左右通常对应一次训练的多个 worker 写入或两次相隔很近的运行。从工程角度能拿到三条信息项目确实在 GPU 机器上训练过、训练过程中写出来了可视化指标、日志内容含 loss/mAP 曲线不会出现“论文写着 0.9代码一跑报错”的尴尬。想验证这个日志里到底记录了什么用 Python 读一下是最直接的。常见做法是拿tensorboard的event_accumulator去解析不用起完整的 TensorBoard 服务from tensorboard.backend.event_processing import event_accumulator log_file events.out.tfevents.1599910333.C-000015-GPU.31726.0 ea event_accumulator.EventAccumulator( log_file, size_guidance{scalars: 10000}, # 防止日志太多导致截断 ) ea.Reload() print(日志里包含的 Tag, ea.Tags()) for metric in [metrics/mAP_0.5, metrics/mAP_0.5:0.95, val/box_loss]: try: events ea.Scalars(metric) print(f{metric} 记录数: {len(events)}最后值: {events[-1].value:.4f}) except KeyError: print(f{metric} 不存在可能是旧版本命名)这段代码的价值在于先确认包里这份日志到底记录到哪一步。如果最后 mAP 是正常数值说明训练闭环走通过如果只有 train/loss那可能训练中途断了。参数说明size_guidance要适当调大因为 YOLOv5 每个 epoch 会写十几类标量默认上限容易把曲线截短Scalars只接受完整 Tag 名不同 YOLOv5 版本的命名从val/box_loss改到metrics/mAP_0.5所以我加了try/except跑一遍就知道这份日志是哪个时期写的。看到结果后再决定后面复现时用哪种权重的命名方式不会带着错误预期。2.2 Dockerfile 是环境后悔药先 build 镜像再谈复现这个包带 Dockerfile这点比代码本身还重要。YOLOv5 对环境极其敏感PyTorch 版本、CUDA 版本、OpenCV 版本稍微差一点轻则报错重则损失函数都踩不对。多数 YOLOv5 项目的 Dockerfile 基础镜像是pytorch/pytorch加cuda、cudnn的组合再通过requirements.txt固化依赖。有Dockerfile在复现环境就是确定性的不用自己踩一遍“装 torch 装到崩溃”的坑。我拿到包后会第一时间执行docker build -t yolov5-remote .构建成功后再起容器把数据和输出目录挂进去docker run --gpus all -it --rm \ -v /data/remote_images:/workspace/data \ -v $(pwd)/runs:/workspace/runs \ yolov5-remote bash参数说明--gpus all是让容器里的 CUDA 程序能看到宿主机 GPU前提是宿主机装好了 NVIDIA Container Toolkit没装的话docker run --gpus all会直接报 “could not select device driver”。-v把宿主机的真实数据目录和训练输出目录映射到容器里这样容器内跑的训练结果能直接留在宿主机上不会容器一关全没了。这里要多说一句.dockerignore和.gitattributes。.dockerignore控制哪些文件不进 Docker 构建上下文一般会排除runs/、dataset/这类大目录避免docker build时把几个 GB 的旧日志和数据集一起打进镜像.gitattributes是给 Git 处理换行符和合并用的对复现影响不大但别手贱去改它改了换行符可能导致 shell 脚本在 Linux 容器里炸掉。我自己就遇到过一次在 Windows 上把脚本按 CRLF 保存放进容器跑直接\r报错后来都是靠 gitattributes 里的* textauto eollf兜底。环境问题提前在 Dockerfile 阶段解决掉后面每一步才敢说「可复现」。3. 从卫星影像到 YOLO 格式切图、标注与数据划分的三个关键动作3.1 遥感影像和自然图像不一样目标不是“小一点”那么简单遥感影像与自然图像的最大差别不只是目标尺寸小而是整幅图的尺寸过分大。高分卫星或者大疆无人机导出的一张正射影像动辄 8000×8000 像素甚至更大直接塞进 YOLOv5 训练显存瞬间爆掉小目标经过多次下采样后只剩几个像素特征图里根本找不到。所以行业里的常规做法是先把大影像切成 patch再在 patch 上做标注、训练、验证。切图时如果不做 overlap目标刚好卡在两张图的边界上会被一刀切成两半两个 patch 各保留半截残影模型很难学。我一般会写一个滑动窗口切图脚本同时完成“影像切成 patch”和“标签同步迁移”两件事。这里的关键是标签坐标换算原图上的目标中心点(cx, cy)要减掉 patch 的左上角坐标(x0, y0)再归一化到 patch 尺寸才能把 YOLO 格式的标签正确迁移过去import cv2 import os import numpy as np def crop_images_and_labels(img_path, label_path, out_img_dir, out_label_dir, patch_size1024, overlap_rate0.2): img cv2.imread(img_path) h, w img.shape[:2] stride int(patch_size * (1 - overlap_rate)) os.makedirs(out_img_dir, exist_okTrue) os.makedirs(out_label_dir, exist_okTrue) with open(label_path, r, encodingutf-8) as f: lines [l.strip().split() for l in f.readlines() if l.strip()] count 0 for y0 in range(0, h - patch_size 1, stride): for x0 in range(0, w - patch_size 1, stride): crop img[y0:y0 patch_size, x0:x0 patch_size] new_lines [] for line in lines: cls, cx, cy, bw, bh map(float, line) cx_abs, cy_abs cx * w, cy * h # 原图绝对像素坐标 bw_abs, bh_abs bw * w, bh * h # 目标中心是否落在当前 patch 内留一点边距 if x0 10 cx_abs x0 patch_size - 10 and \ y0 10 cy_abs y0 patch_size - 10: cx_new (cx_abs - x0) / patch_size # 迁移到 patch 并归一化 cy_new (cy_abs - y0) / patch_size bw_new bw_abs / patch_size bh_new bh_abs / patch_size new_lines.append(f{int(cls)} {cx_new:.5f} {cy_new:.5f} {bw_new:.5f} {bh_new:.5f}) # 只有真正包含目标的 patch 才保存避免一堆空白图 if new_lines: cv2.imwrite(os.path.join(out_img_dir, f{os.path.basename(img_path)[:-4]}_{count}.jpg), crop) with open(os.path.join(out_label_dir, f{os.path.basename(img_path)[:-4]}_{count}.txt), w) as f: f.write(\n.join(new_lines)) count 1逻辑说明是先算滑动步长stride patch_size * (1 - overlap_rate)overlap_rate 越大 patch 越多。10这个边距值是我刻意留的防止目标中心刚好处在 patch 边缘继承一个被裁掉一角的标签。注释里也写了只保存包含目标的 patch这一步能显著减少后续训练时的负样本数量val 的时候也不会被大量空白 patch 拖慢速度。参数没有搞太花哨patch_size1024在显存和精度之间比较平衡overlap_rate0.2能把边界目标覆盖住的成本压到最低。如果你的目标比汽车还小可以调成 1280 0.3但训练时间会肉眼可见变长。3.2 标注工具与目录结构约定大于配置路径错一步全盘崩切图完成后标注可以用 labelImg 或 roLabelImg。前者输出水平矩形框格式直接兼容 YOLOv5后者支持旋转框适合船体、飞机这类长条目标但需要额外把旋转框转成四角坐标或者外接水平框麻烦一点。对大多数起步项目我建议先用 labelImg 的水平框跑通后续有需要再上旋转框方案。标注完成后目录必须严格按 YOLOv5 的约定组织。常见的错误是把标签和图片混在一个目录里或者训练集验证集没分开。标准结构是data/ custom.yaml images/ train/ 000001.jpg val/ 000002.jpg labels/ train/ 000001.txt val/ 000002.txt图片和标签文件名要一一对应后缀不同但主名必须相同。我自己写过一个简单的划分脚本按 8:2 随机切分保证同一条船在同一张原图上的多个 patch 不会被分进训练和验证两个集合。如果原图本身就是一张大影像切成 10 个 patch这 10 个 patch 必须要互斥否则验证集数据泄露mAP 虚高答辩现场被问两句就露馅import os import random import shutil src_img_dir patched_images src_label_dir patched_labels train_img, train_label data/images/train, data/labels/train val_img, val_label data/images/val, data/labels/val os.makedirs(train_img, exist_okTrue) os.makedirs(train_label, exist_okTrue) os.makedirs(val_img, exist_okTrue) os.makedirs(val_label, exist_okTrue) files [f for f in os.listdir(src_img_dir) if f.endswith(.jpg)] random.seed(0) random.shuffle(files) split int(len(files) * 0.8) for img_name in files[:split]: stem os.path.splitext(img_name)[0] shutil.copy(os.path.join(src_img_dir, img_name), os.path.join(train_img, img_name)) shutil.copy(os.path.join(src_label_dir, stem .txt), os.path.join(train_label, stem .txt)) for img_name in files[split:]: stem os.path.splitext(img_name)[0] shutil.copy(os.path.join(src_img_dir, img_name), os.path.join(val_img, img_name)) shutil.copy(os.path.join(src_label_dir, stem .txt), os.path.join(val_label, stem .txt))说明两点。第一random.seed(0)必须固定否则每次运行划分结果都不一样后面几次实验的验证集会变来变去指标对比就失去意义了。第二拷贝时先查一下stem .txt是否存在有些边界 patch 被脚本过滤掉但图片漏存了就会造成训练时图片没标签、YOLOv5 打开后发现它把空文件也算负样本方向读起来很怪。这步检查我就吃过亏跑了一轮训练才发现 val 集里少了 9 个标签文件mAP 结果白算。4. 训练复现数据配置、超参数与 TensorBoard 日志解读4.1 写准 data.yaml比调模型结构更优先YOLOv5 的训练入口是train.py但它不认你的目录名只认data.yaml里写的路径。这个文件是整套训练配置的地基写错了后面所有参数都白调。我见过太多人一上来就把nc和names写错位nc表示类别数量names是类别名字列表顺序必须与标注文件里的 class id 一一对应。比如标注文件里 0 是船、1 是车、2 是飞机names就必须是[boat, car, plane]不能随手写[car, boat, plane]。常见的 data.yaml 长这样path: /workspace/data # 数据集根目录容器里用绝对路径 train: images/train # 相对 path 的训练集图片目录 val: images/val # 验证集 nc: 3 names: [boat, car, plane]然后启动训练python train.py \ --data /workspace/data/custom.yaml \ --weights yolov5s.pt \ --img 1280 \ --batch 16 \ --epochs 100 \ --hyp data/hyps/hyp.scratch-low.yaml \ --project runs/remote参数说明--weights yolov5s.pt是官方预训练权重遥感影像与 COCO 数据集差异较大但预训练权重里学到的纹理、边缘特征仍能加速收敛不建议从零训练除非数据量极大。--img 1280是遥感项目的重点小目标在 640 分辨率下往往只占几个像素放大到 1280 后特征明显得多。--hyp hyp.scratch-low.yaml是低数据增强策略遥感小样本场景下过强的马赛克增强会把目标裁得七零八落反而让模型学不到完整轮廓用官方推荐的 low 策略更稳。再说--project runs/remote这个参数能避免默认输出目录跑乱。每跑一次实验YOLOv5 会在runs/remote下生成 exp、exp2、exp3 这样的子目录日志、权重、PR 曲线全在里面。做多组对比实验时千万别把所有结果混在默认的runs/train/exp里后面复盘会很痛苦。4.2 训练时别只盯 lossTensorBoard 和 PR 曲线才是真相来源很多人看到 train/loss 下降就觉得稳了这在遥感目标识别里是玄学。loss 下降只代表模型在拟合训练集不代表验证集上检测准。真正的指标要看metrics/mAP_0.5和metrics/mAP_0.5:0.95前者是 IoU 阈值 0.5 时的平均精度后者对框的位置精度要求更苛刻遥感小目标通常后者被拉得很低。YOLOv5 训练过程中就会往runs/remote/exp/events.out.tfevents.*写日志。我习惯直接起 TensorBoard 去看曲线的趋势比训练结束后等控制台输出直观得多tensorboard --logdir runs/remote浏览器打开http://localhost:6006重点看三个标量train/loss、metrics/mAP_0.5、metrics/mAP_0.5:0.95。如果 loss 下降但 mAP 纹丝不动先别调参回去查标签。另一个有用的是plots目录下的PR_curve.pngYOLOv5 会自动生成 PR 曲线曲线越接近右上角越好。如果某一类的 AP 明显低说明该类别的样本量不够或者标注质量差需要回到第 3 章的数据环节补样本。这里还要提一下网络结构图。YOLOv5 的配置文件models/yolov5s.yaml就是网络结构的描述文件depth_multiple和width_multiple两个参数控制模型深度和宽度。遥感场景目标小、背景复杂从yolov5s换到yolov5m或yolov5l往往比调超参数更直接。代价是显存占用和推理速度成倍上升具体换哪一档要看你的 GPU 显存余量。5. 常见问题排查遥感目标检测四个翻车现场的现象、原因与解决5.1 推理入口和后处理参数conf-thres 和 iou-thres 先搞明白训练完的模型要用detect.py做推理也可以写脚本调用。后处理是 YOLOv5 里看起来像“黑匣子”的部分但其实就两个参数在起作用--conf-thres是置信度阈值低于这个值的框直接丢弃--iou-thres是 NMS 的 IoU 阈值两个框重叠超过这个比例就保留分数高的那个。遥感影像背景干净海面、农田这类区域误检少--conf-thres可以放到 0.15-0.25如果是城市高楼或港口密集区遮挡多、目标密集建议提到 0.35 以上否则会冒出一堆假正例。实际推理命令python detect.py \ --weights runs/remote/exp/weights/best.pt \ --img 1280 \ --conf 0.25 \ --iou 0.45 \ --source data/images/val \ --save-txt--save-txt会把每个检测框以 YOLO 格式写到runs/detect/exp*/labels/做结果统计时用得上。比如我想快速统计验证集上每张图检出了多少船直接读这些 txt 就行import glob for label_file in glob.glob(runs/detect/exp/labels/*.txt): with open(label_file, r, encodingutf-8) as f: boxes [l.strip().split() for l in f.readlines() if l.strip()] if boxes: print(f{label_file}: {len(boxes)} 个目标)这一步能快速发现两类问题一是某张图检测框数量异常多大概率是阈值太低产生大量误检二是所有图检测框数量都很少说明模型训练没收敛不是推理参数的问题。5.2 坑一训练 loss 在降验证集 mAP 却恒等于 0现象日志里 train/loss 从 8 降到 2数值很好看但metrics/mAP_0.5始终是 0PR 曲线完全出不来。原因最常见的是标签类别 id 从 1 开始而 YOLOv5 的类别索引从 0 开始。标注工具里把第一类标成 1nc3时类别编号变成了 1、2、3模型实际只能识别 2、3、4验证时永远对不上真实标签。还有一个次常见原因标签文件放错目录images/val对应的是labels/trainYOLOv5 找不到标签直接当负样本处理自然 mAP 为 0。解决写一行命令统计标签类别 id 的分布看有没有 0python -c import glob; idsset(); [ids.update(l.split()[0] for l in open(p)) for p in glob.glob(data/labels/**/*.txt, recursiveTrue)]; print(ids)输出如果包含{1, 2, 3}显然就是错误的正确应该是{0, 1, 2}。用这个脚本全量扫一遍把所有类别减 1 再存然后重新训练。改完之后跑一个 epoch 再看metrics/mAP_0.5应该能看到非零数值。5.3 坑二小船、车辆全部漏检大目标倒是一个不落现象验证集里大船能检出来岸边的小汽车几乎一个都检不到mAP_0.5不算太低但mAP_0.5:0.95很惨。原因遥感影像里小目标在 640 分辨率下只有 20-30 像素而 YOLOv5 默认的锚框尺寸和输入分辨率是为 COCO 数据集设计的COCO 里的小目标也比遥感目标大得多。下采样倍数固定小目标经过几层 stride 卷积后特征在特征图里连一个像素都占不满。解决优先把输入分辨率提到 1280这是成本最低、收益最快的手段。还不行就把训练用的锚框改小在data/hyp.scratch-low.yaml里调整anchors部分或者用 YOLOv5 自带的锚框聚类重新算。第三招是换大模型yolov5m或yolov5l特征提取能力更强小目标层的信息保留更好。这三个手段按顺序试不要一上来就换模型显存翻倍未必扛得住。5.4 坑三CUDA out of memory 训练中断Docker 容器被杀现象训练到一半控制台报RuntimeError: CUDA out of memory更隐蔽的是 Docker 容器里进程直接显示Killed没有 Python 报错。原因--batch和--img 1280的组合超出显卡显存。遥感项目通常用 1280 输入特征图占用的显存是 640 输入的四倍左右同样 batch size 下内存需求陡增。Killed则是 Docker 容器默认共享内存/dev/shm太小DataLoader 多进程读图时把共享内存打爆。解决先说调显存按顺序做batch 从 16 降到 8 或 4加上--amp开启混合精度训练再不行把输入降到 960。我一般不优先降 imgsz因为遥感小目标对分辨率太敏感。再说 Docker 的/dev/shm启动容器时加参数docker run --gpus all --shm-size 8g -it -v /data:/workspace/data yolov5-remote bash--shm-size 8g能直接解决Killed问题。后面训练中断这种“没头没脑”的现象十次里有八次是共享内存不够不是显卡坏了。5.5 坑四推理输出全空怎么回事事都像“模型废了”现象detect.py 能正常读图日志也不报错但输出目录里没有检测框或者一个 label 文件都没有。原因第一种是--conf-thres设太高低置信度目标被全滤了第二种是数据路径有问题比如图片文件名带中文或者路径含空格Docker 容器里的 OpenCV 在部分版本下读不出图第三种是源大图超过 6000×6000 时某些旧版 YOLOv5 的 letterbox 处理会出边界情况模型推理直接返回空结果。解决先把置信度阈值降到 0.1 试一次有输出说明是后处理参数问题再慢慢调回合适值。路径问题要治本项目根目录、数据集、输出目录全部改纯英文Docker 挂载后容器内统一用/workspace/data这类路径不要再往上叠加中文段。大图推理时先按第 3 章的切图脚本把影像切成 patch再进入检测流程一步到位避开大图的 letterbox 问题。6. 进阶验证大幅影像切片推理与 PR 曲线复盘6.1 对整幅遥感影像推理patch 推理加全局 NMS训练和验证都过了最后要面对的问题是一张完整的遥感影像怎么出检测结果直接整图喂进去会爆显存而且大图经过 letterbox 缩放到模型输入尺寸后小目标直接没了。我的做法是推理阶段同样切片但要把每个 patch 的检测框坐标还原到原图坐标系再做一个全局 NMS 合并防止目标在重叠区域被重复框出来。这段代码是这个项目里最值得收藏的部分import cv2 import numpy as np import torch from utils.augmentations import letterbox from utils.general import non_max_suppression def infer_large_image(model, img, img_size1280, patch1280, overlap0.15, conf0.25, iou0.45): h, w img.shape[:2] stride int(patch * (1 - overlap)) all_boxes [] for y0 in range(0, h, stride): for x0 in range(0, w, stride): crop img[y0:y0 patch, x0:x0 patch] im, ratio, (dw, dh) letterbox(crop, new_shapeimg_size, stridestride) im im[:, :, ::-1].transpose(2, 0, 1) # BGR 转 RGB 并转 CHW im np.ascontiguousarray(im) im torch.from_numpy(im).float().div(255).unsqueeze(0) dets model(im)[0] dets non_max_suppression(dets, conf, iou)[0] if dets is None: continue for det in dets.cpu().numpy(): x1, y1, x2, y2, score, cls det # letterbox 坐标还原到 crop 原始坐标 x1 (x1 - dw) / ratio y1 (y1 - dh) / ratio x2 (x2 - dw) / ratio y2 (y2 - dh) / ratio all_boxes.append([x0 x1, y0 y1, x0 x2, y0 y2, score, cls]) if not all_boxes: return [], [], [] keep cv2.dnn.NMSBoxes( [[b[0], b[1], b[2] - b[0], b[3] - b[1]] for b in all_boxes], [b[4] for b in all_boxes], score_thresholdconf, nms_thresholdiou, ) keep np.array(keep).flatten() final [all_boxes[i] for i in keep] return final[:, :4], final[:, 4], final[:, 5]逻辑说明每个 patch 推理前先调用letterbox得到缩放比例ratio和填充边距(dw, dh)推理出来的框坐标是在 letterbox 之后的图像坐标里必须反算回 patch 原坐标再叠加 patch 在原图的位置(x0, y0)才能得到全局坐标。最后cv2.dnn.NMSBoxes是跨 patch 的全局去重两个 patch 重叠区域检出的同一目标会被合并。参数解释里有两个坑non_max_suppression返回的 tenson 可能为空必须先判空再取索引cv2.dnn.NMSBoxes在不同 OpenCV 版本返回的数组 shape 不一样一定要用np.array(keep).flatten()统一直接用返回值索引列表会翻车。6.2 验证和复盘PR 曲线之外还要看图说话训练全流程走完后我会把runs/remote/exp/PR_curve.png和confusion_matrix.png调出来再配合原图检测结果一起复盘。PR 曲线看类别均衡性混淆矩阵看类别之间有没有互相串扰。遥感场景里最常见的串扰是船和车在一个 patch 里混检本质是训练时同一 patch 里不同类别的目标比例失衡这个靠调参解决不了只能回数据环节补样本或者做类别重加权。从那以后我每次跑遥感项目都会强制自己走一遍这个流程先读 tfevents 确认训练发生过再查标签类别 id然后切图、训练、看 PR 曲线最后用切片推理把检测框叠到原图上。看着简单每一步都救过我一次。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。