简介:本资源为基于YOLOv8的航拍屋顶识别目标检测项目代码包,面向计算机视觉学习者、深度学习课程设计者及遥感影像分析方向的开发者,用于解决航拍视角下屋顶目标自动检测与定位问题。压缩包共467个文件,约23.41MB,以227个md文档、130个py源码、43个yaml与12个yml配置为主,另含pt权重、cpp推理示例、sh脚本、ipynb笔记及少量图片与dockerfile,覆盖训练、推理、部署与说明文档等环节。已有127人学习下载。项目按requirements.txt配置环境即可运行,目录中提供数据集说明、模型配置、推理代码与结果记录,便于读者快速复现航拍屋顶检测流程,理解YOLOv8在遥感场景下的数据组织、参数调整与效果验证思路,也可作为二次开发与课程实践的参考模板。
1. 航拍屋顶识别为什么值得单独拆一个 YOLOv8 项目
去年帮某公司做光伏选址评估,拿到一批无人机航拍图,要求把每栋建筑的屋顶轮廓和类型标出来。我第一反应是拿现成的 YOLOv8 通用权重直接跑,结果翻车翻得很彻底——屋顶在航拍视角下是俯视多边形,通用模型见过的“屋顶”大多是斜视或平视,特征分布完全对不上。后来老老实实从头训了一个航拍屋顶识别模型,才把 mAP 拉到能用的水平。
这个项目代码就是干这件事的:基于 YOLOv8 做航拍场景下的屋顶目标检测,输出屋顶的边界框和类别。它解决的核心问题是——通用检测模型在俯视小目标、密集排列、光照多变的情况下召回率极低,而这份代码把数据组织、训练配置、推理脚本整条链路都搭好了。适合谁用?做遥感图像分析、城市建筑普查、光伏潜力评估、违建监测的从业者,以及想拿一个完整 YOLOv8 实战项目练手的目标检测学习者。你不需要从零写数据加载器,但需要理解航拍数据的特殊性,否则调参就是玄学。
2. 拆开代码包:目录结构、数据格式与模型选型逻辑
2.1 目录骨架与各文件职责
拿到代码包先别急着跑train.py,花五分钟把目录看一遍能省后面两小时的排错。典型结构如下:
roof_detection/ ├── data/ │ ├── images/ │ │ ├── train/ # 训练集原图 │ │ ├── val/ # 验证集原图 │ │ └── test/ # 测试集原图 │ ├── labels/ │ │ ├── train/ # 训练集标注(YOLO txt 格式) │ │ ├── val/ │ │ └── test/ │ └── roof_dataset.yaml # 数据集配置文件 ├── models/ │ └── yolov8n.pt # 预训练权重(n/s/m/l/x 可选) ├── cfg/ │ └── train_config.yaml # 训练超参配置 ├── train.py # 训练入口 ├── predict.py # 单图/批量推理 ├── export.py # 模型导出(ONNX/TensorRT) └── utils/ ├── dataset_check.py # 数据完整性校验 └── vis_labels.py # 标注可视化关键点在于data/下 images 和 labels 必须严格一一对应,文件名相同、扩展名不同。我见过太多人 images 里是roof_001.jpg,labels 里写成roof_1.txt,训练时损失直接不降,查半天才发现是配对失败。
2.2 航拍屋顶的标注格式与类别设计
YOLO 格式的标注是每行一个目标:class_id x_center y_center width height,坐标全部归一化到 0~1。航拍屋顶场景有个容易忽略的点——屋顶边界往往不是矩形,但 YOLO 输出的是水平边界框,所以标注时要把整个屋顶的外接矩形框进去,不要只框屋脊或某个斜面。
类别设计上,常见做法是分flat_roof(平屋顶)、pitched_roof(坡屋顶)、unknown(遮挡或模糊)三类。类别不宜过多,航拍分辨率下细分到材质级别(瓦片/彩钢/混凝土)标注一致性很难保证,反而拉低整体精度。如果只做屋顶有无检测,单类别也够用。
roof_dataset.yaml的内容:
path: ./data train: images/train val: images/val test: images/test nc: 3 names: 0: flat_roof 1: pitched_roof 2: unknownnc是类别数,names的索引必须和标注文件里的 class_id 对应。改类别时这两个地方要同步改,漏一个就会报索引越界。
2.3 为什么选 YOLOv8 而不是 v5 或 RT-DETR
选型理由很实际:YOLOv8 的 anchor-free 头对小目标更友好,航拍屋顶在 640 分辨率下往往只占几十个像素,v5 的 anchor 机制需要重新聚类才能适配,v8 省了这一步。相比 RT-DETR,v8 的推理速度快一个量级,部署到边缘设备更现实。模型尺寸上,航拍数据量通常不大(几千到几万张),yolov8s是精度和速度的平衡点;如果数据量过万且 GPU 显存充足,可以上yolov8m。yolov8n适合快速验证流程,但小目标召回会明显偏低。
提示:预训练权重建议用 COCO 版本,不要用 ImageNet 分类权重,检测头的初始化对收敛速度影响很大。
3. 从零跑通训练:配置文件、命令行与关键参数
3.1 环境搭建与依赖安装
先确认 CUDA 版本和 PyTorch 匹配,这是最常见的翻车点。我一般用 conda 建独立环境:
conda create -n roof_det python=3.10 -y conda activate roof_det # 根据本机 CUDA 版本装 PyTorch,下面以 CUDA 11.8 为例 pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu118 # 安装 ultralytics 和辅助库 pip install ultralytics opencv-python pyyaml tqdm装完跑一句python -c "import torch; print(torch.cuda.is_available())",返回True才继续。返回False的话后面训练会静默用 CPU,速度慢到你以为卡死了。
3.2 数据校验:训练前必须跑的一步
utils/dataset_check.py做三件事:检查 images 和 labels 文件名配对、检查标注坐标是否越界、统计各类别样本数。核心逻辑:
import os from pathlib import Path from collections import Counter def check_dataset(root): for split in ['train', 'val', 'test']: img_dir = Path(root) / 'images' / split lbl_dir = Path(root) / 'labels' / split imgs = {p.stem for p in img_dir.glob('*.jpg')} lbls = {p.stem for p in lbl_dir.glob('*.txt')} # 配对检查 missing_lbl = imgs - lbls missing_img = lbls - imgs if missing_lbl: print(f"[{split}] 缺少标注: {list(missing_lbl)[:5]}") if missing_img: print(f"[{split}] 缺少图片: {list(missing_img)[:5]}") # 坐标越界与类别统计 cls_counter = Counter() for lbl in lbl_dir.glob('*.txt'): for line in lbl.read_text().strip().splitlines(): parts = line.split() cls_id = int(parts[0]) coords = [float(x) for x in parts[1:5]] if any(c < 0 or c > 1 for c in coords): print(f"[{split}] 坐标越界: {lbl.name}") cls_counter[cls_id] += 1 print(f"[{split}] 类别分布: {dict(cls_counter)}") check_dataset('./data')逻辑说明:imgs - lbls用集合差找缺失配对;坐标检查遍历每行标注,归一化值超出 0~1 就是标注工具导出时出了问题;类别分布用来判断是否严重不均衡,如果某一类占比超过 80%,训练时需要考虑加权或补充样本。参数上root指向 data 目录,split 列表按实际划分调整。
3.3 训练命令与超参配置
最简训练命令:
yolo detect train \ data=./data/roof_dataset.yaml \ model=./models/yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=30 \ project=./runs \ name=roof_v1逐个说关键参数:imgsz=640是航拍场景的底线,再低小屋顶就糊成一片;如果显存够,可以试imgsz=1024,小目标召回通常能涨 3~5 个点。batch=16是 8G 显存的安全值,爆显存就降到 8 并配合accumulate=2模拟大 batch。lr0=0.01是 SGD 的初始学习率,如果用 AdamW 要降到 0.001。patience=30表示 30 轮验证指标不涨就早停,航拍数据容易过拟合,这个值别设太大。
cfg/train_config.yaml里可以覆盖更多细节,比如数据增强:
# 航拍场景增强策略 hsv_h: 0.015 # 色调抖动,模拟不同光照 hsv_s: 0.7 # 饱和度抖动 hsv_v: 0.4 # 亮度抖动 degrees: 90.0 # 旋转角度,航拍视角旋转不变性重要 translate: 0.1 scale: 0.5 flipud: 0.5 # 上下翻转,俯视图无方向性 fliplr: 0.5 mosaic: 1.0 # 马赛克增强,提升小目标 mixup: 0.1航拍场景我一般把degrees开到 90,因为无人机朝向不固定;flipud和fliplr都开 0.5,俯视图没有上下左右的方向语义。mosaic保持 1.0 对小目标帮助明显,但最后 10 轮建议关掉,让模型在真实分布上收尾。
3.4 训练过程监控与中断恢复
训练启动后看runs/roof_v1/下的results.csv和weights/。重点盯三个指标:metrics/mAP50、metrics/mAP50-95、train/box_loss。正常情况 box_loss 在前 10 轮快速下降,mAP50 稳步上升。如果 box_loss 震荡不降,大概率是学习率太大或标注有问题。
中断后恢复:
yolo detect train resume model=./runs/roof_v1/weights/last.ptresume会从 last.pt 里读回优化器状态和 epoch 数,不要手动改 epochs 再从头训,那样学习率调度会乱。
4. 推理、评估与导出:模型落地的三个环节
4.1 单图与批量推理
predict.py的核心调用:
from ultralytics import YOLO model = YOLO('./runs/roof_v1/weights/best.pt') # 单图推理 results = model.predict( source='./data/images/test/roof_test_001.jpg', conf=0.25, # 置信度阈值 iou=0.45, # NMS IoU 阈值 imgsz=640, save=True, save_txt=True # 同时输出 YOLO 格式结果 ) # 批量推理整个目录 results = model.predict( source='./data/images/test/', conf=0.25, iou=0.45, stream=True # 流式处理,省内存 ) for r in results: boxes = r.boxes for box in boxes: cls_id = int(box.cls[0]) conf_score = float(box.conf[0]) xyxy = box.xyxy[0].tolist() print(f"类别:{cls_id} 置信度:{conf_score:.2f} 坐标:{xyxy}")参数说明:conf=0.25是航拍场景的常用起点,漏检多就降到 0.15,误检多就升到 0.4。iou=0.45控制 NMS 合并程度,密集屋顶排列时可以降到 0.35 避免相邻屋顶被合并。stream=True在批量处理时逐张加载,避免一次性把几千张图读进内存。
4.2 评估指标怎么看
验证集评估命令:
yolo detect val \ model=./runs/roof_v1/weights/best.pt \ data=./data/roof_dataset.yaml \ imgsz=640 \ batch=16输出里重点看每类的 P(精确率)、R(召回率)、mAP50。航拍屋顶识别里,pitched_roof的召回通常低于flat_roof,因为坡屋顶的纹理和阴影更复杂。如果某一类召回低于 0.5,先查该类样本量是否太少,再查标注是否把坡屋顶误标成了 unknown。
4.3 导出 ONNX 与 TensorRT
部署到生产环境一般导出 ONNX:
yolo export \ model=./runs/roof_v1/weights/best.pt \ format=onnx \ imgsz=640 \ half=True \ simplify=Truehalf=True导出 FP16,推理速度提升约 30%,精度损失通常小于 1 个点。simplify=True会做图优化,去掉冗余算子。导出后建议用onnxruntime跑一遍验证输出和 PyTorch 一致,再上 TensorRT 做进一步加速。
5. 避坑指南:航拍屋顶检测的五个血泪教训
5.1 现象:训练 loss 正常下降但 mAP 始终为 0
原因:标注文件的 class_id 从 1 开始编号,而 YOLO 要求从 0 开始。很多标注工具默认从 1 计数,导出时没改。
解决:跑一遍dataset_check.py看类别分布,如果最小 class_id 是 1 且没有 0,就是这个问题。批量把所有标注文件第一列减 1:
import glob for f in glob.glob('./data/labels/**/*.txt', recursive=True): lines = open(f).read().strip().splitlines() new_lines = [] for line in lines: parts = line.split() parts[0] = str(int(parts[0]) - 1) new_lines.append(' '.join(parts)) open(f, 'w').write('\n'.join(new_lines))5.2 现象:小屋顶大量漏检,大屋顶正常
原因:imgsz=640下小目标特征在 backbone 下采样后丢失,或者 mosaic 增强关闭太早。
解决:先把imgsz提到 1024 试一轮,如果显存不够就保持 640 但把mosaic开到 1.0 并延长到最后 20 轮才关闭。另外检查model是否用了yolov8n,n 版本对小目标本身就不友好,换yolov8s或yolov8m。
5.3 现象:验证集 mAP 很高但测试集一塌糊涂
原因:训练集和验证集来自同一批次航拍数据,光照、季节、区域分布一致,但测试集是另一批次,域偏移严重。
解决:划分数据集时按航拍批次或区域划分,不要随机打乱。如果已经训完,用测试集做一次val看差距,差距超过 15 个点就说明域偏移问题,需要补充测试集同分布的数据进训练。
5.4 现象:推理时相邻屋顶被合并成一个框
原因:NMS 的iou阈值太高,密集排列的屋顶边界框重叠度大,被误合并。
解决:把推理时的iou从 0.45 降到 0.3~0.35,同时检查标注时相邻屋顶的边界框是否留了间隙。如果屋顶本身紧挨着,考虑用agnostic_nms=False按类别分别做 NMS。
5.5 现象:导出 ONNX 后推理结果和 PyTorch 不一致
原因:导出时imgsz和推理时不一致,或者half=True在 CPU 上推理导致精度异常。
解决:导出和推理的imgsz必须一致;FP16 模型只在 GPU 上推理,CPU 推理用 FP32 导出。导出后用同一张图分别跑 PyTorch 和 ONNX,对比框坐标差异,超过 2 个像素就要查导出配置。
6. 进阶技巧:用 TTA 和分块推理把召回再拉一截
训练收尾后如果 mAP 还差一口气,别急着加数据,先试两个推理侧的技巧。
TTA(测试时增强):推理时对同一张图做翻转和缩放,把多组结果做 NMS 融合。YOLOv8 内置了augment=True参数:
results = model.predict( source='./data/images/test/', conf=0.2, iou=0.4, augment=True, # 开启 TTA imgsz=640 )开启后推理速度约慢 2~3 倍,但 mAP50 通常能涨 1~3 个点。适合对精度要求高、对延迟不敏感的场景。注意 TTA 下conf可以适当降低,因为融合会过滤掉部分误检。
分块推理:航拍图分辨率往往在 4000×3000 以上,直接缩到 640 会丢失大量小目标细节。做法是把原图切成有重叠的子块,每块单独推理,再把结果映射回原图坐标做全局 NMS。重叠率一般设 20%,避免边缘目标被切断。核心逻辑:
import cv2 import numpy as np def sliding_window_inference(model, img_path, tile_size=640, overlap=0.2): img = cv2.imread(img_path) h, w = img.shape[:2] stride = int(tile_size * (1 - overlap)) all_boxes = [] for y in range(0, h, stride): for x in range(0, w, stride): tile = img[y:y+tile_size, x:x+tile_size] if tile.shape[0] < tile_size or tile.shape[1] < tile_size: tile = cv2.copyMakeBorder(tile, 0, tile_size-tile.shape[0], 0, tile_size-tile.shape[1], cv2.BORDER_CONSTANT, value=(114,114,114)) results = model.predict(tile, conf=0.25, verbose=False) for box in results[0].boxes: xyxy = box.xyxy[0].tolist() # 映射回原图坐标 xyxy[0] += x; xyxy[1] += y xyxy[2] += x; xyxy[3] += y all_boxes.append((xyxy, float(box.conf[0]), int(box.cls[0]))) # 全局 NMS # ... 按类别做 NMS 后输出 return all_boxes参数上tile_size和训练时imgsz保持一致,overlap=0.2是经验值,目标密集时提到 0.3。这个方案显存占用低,但推理时间随图幅线性增长,适合离线批处理。
从那以后我每次训完航拍检测模型,都会先用 TTA 跑一遍验证集看上限,再决定要不要补数据重训——这个习惯帮我省了好几次无谓的标注返工。希望帮到你。
本文还有配套的精品资源,点击获取