简介本资源为基于YOLOv8的航拍屋顶识别目标检测项目代码包面向深度学习入门与进阶开发者、计算机视觉方向学生及需要落地航拍场景检测任务的工程人员。包内共467个文件以227个md说明文档、130个py训练与推理脚本、43个yaml及12个yml配置文件为主另含pt权重、cpp推理源码、sh运行脚本、ipynb示例与少量图片和dockerfile部署文件压缩包约23.41MB按requirements.txt配置环境即可使用。项目围绕航拍图像中屋顶目标的检测展开涵盖数据集组织、模型训练、推理验证与部署配置等完整环节目录结构清晰便于按模块查阅与二次开发。已有127人学习下载适合希望快速复现航拍屋顶识别流程、理解YOLOv8工程结构并积累目标检测实战经验的读者参考。1. 航拍屋顶识别为什么总在“小目标”上翻车航拍屋顶识别这个方向最近问的人明显变多了。光伏选址、违建排查、城市屋顶资源普查、保险定损背后都指向同一件事从无人机或卫星影像里把屋顶一块块框出来再判断类型和边界。YOLOv8 目标检测是目前落地成本最低的一条路但真正跑过的人都知道通用权重直接拿来用效果往往惨不忍睹——屋顶密集、尺度差异大、边缘模糊、遮挡严重模型要么漏检要么把一整片屋顶框成一个巨框。这篇笔记就围绕“YOLOv8 航拍屋顶识别”这个具体任务把数据准备、模型选型、训练参数、推理调优和踩坑记录讲清楚。适合两类人一类是刚拿到一批航拍图、想快速跑通 baseline 的开发者另一类是已经训过一版但 mAP 卡在 0.5 上下、不知道怎么调的老手。我不会假装见过某个现成仓库的源码所有步骤都是这个任务里最常见、最稳的做法你照着改路径就能复现。2. 数据这一关屋顶标注和普通目标检测不是一回事2.1 航拍屋顶数据的三个特殊点普通 COCO 数据集里的目标大多是水平视角、尺度适中、边界清晰。航拍屋顶完全反过来。第一视角是俯视屋顶之间紧挨着一栋楼的屋顶和隔壁楼的屋顶可能只隔几个像素标注框稍微松一点就重叠。第二尺度跨度极大同一张 4K 图里小屋顶可能只有 20×20 像素大厂房屋顶能占满半个画面。第三屋顶形状不规则L 形、凹字形、带天井的都有矩形框天然框不准但目标检测只能用矩形框所以标注策略要提前定好。我一般会先做一件事把原始大图切块。直接拿 4000×3000 的原图训练显存吃不消而且小目标缩放到 640 后基本消失。常见做法是滑窗切 1024×1024重叠 200 像素保证边缘目标不被切断。切完再标注标注质量会高很多。import cv2 import os def slice_image(img_path, out_dir, tile1024, overlap200): img cv2.imread(img_path) h, w img.shape[:2] step tile - overlap idx 0 for y in range(0, h, step): for x in range(0, w, step): # 保证最后一块不越界 x2 min(x tile, w) y2 min(y tile, h) x1 max(0, x2 - tile) y1 max(0, y2 - tile) crop img[y1:y2, x1:x2] if crop.shape[0] tile or crop.shape[1] tile: continue name os.path.splitext(os.path.basename(img_path))[0] cv2.imwrite(f{out_dir}/{name}_{idx}.jpg, crop) idx 1 slice_image(raw/DJI_0001.jpg, sliced, tile1024, overlap200)这段代码的逻辑很直白按步长滑动窗口每块固定 1024×1024重叠区用来兜住跨块目标。参数上tile不要小于 640否则小屋顶信息损失太多overlap建议取 tile 的 15% 到 25%太小会切断目标太大则重复样本过多、训练变慢。切完的图再送标注工具标注时统一规则只框屋顶主体天井、女儿墙不算被画面边缘截断超过 30% 的目标直接标为忽略。2.2 标注格式转换与类别设计YOLOv8 用的是 YOLO 格式的 txt 标注每行class x_center y_center width height全部归一化到 0 到 1。如果你用 LabelImg 或类似工具标的是 VOC 的 xml需要转一道。类别设计上我建议第一版不要分太细先只分“屋顶”一类跑通之后再拆“平屋顶 / 坡屋顶 / 彩钢瓦 / 光伏板”这些子类。类别一多每类样本量骤降小类直接训不动。import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, out_txt, classes): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) lines [] for obj in root.iter(object): cls obj.find(name).text if cls not in classes: continue cid classes.index(cls) bbox obj.find(bndbox) x1 float(bbox.find(xmin).text) y1 float(bbox.find(ymin).text) x2 float(bbox.find(xmax).text) y2 float(bbox.find(ymax).text) # 归一化并转中心点格式 cx (x1 x2) / 2.0 / w cy (y1 y2) / 2.0 / h bw (x2 - x1) / w bh (y2 - y1) / h lines.append(f{cid} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(out_txt, w) as f: f.write(\n.join(lines)) voc_to_yolo(ann/roof_001.xml, labels/roof_001.txt, classes[roof])转换时最容易翻车的是坐标越界。有些标注框的 xmax 超过了图片宽度归一化后大于 1YOLOv8 训练时会直接报错或静默丢弃。稳妥做法是在转换后加一道校验把所有坐标 clamp 到 [0,1]并统计每张图的框数量框数为 0 的图要么补标要么剔除别留着当负样本——航拍图里几乎没有真正的“无屋顶”背景留着只会让模型学偏。2.3 数据集划分与配置文件划分比例上航拍数据我一般用 8:1:1但如果你的场景跨区域比如不同城市、不同季节拍的一定要按区域划分不能随机打散。随机打散会让同一栋楼的不同切块同时出现在训练集和验证集验证 mAP 虚高上线就露馅。配置文件按 YOLOv8 的 yaml 格式写路径用绝对路径最省事。# roof_dataset.yaml path: /data/roof train: images/train val: images/val test: images/test names: 0: roofpath是数据集根目录train/val/test是相对路径。names 里的类别索引必须和标注 txt 里的 class 编号严格对应错一位整个训练就废了。改完 yaml 先用几行脚本抽查一下随机抽 5 张训练图把标注框画出来看一眼确认框和屋顶对得上再开始训练。这一步花五分钟能省掉后面几小时的无效训练。3. 用 YOLOv8 跑通第一版屋顶检测模型3.1 模型选型n/s/m 怎么选YOLOv8 提供 n、s、m、l、x 五个尺度。航拍屋顶这个任务我的经验是如果你只是验证流程用 yolov8n 最快单卡几小时就能出结果如果要落地起步用 yolov8s 或 yolov8m。n 太小小屋顶召回率明显不够l 和 x 参数量大航拍数据集通常也就几千到几万张容易过拟合而且推理速度在边缘设备上扛不住。选型的判断标准不是“越大越好”而是看你的部署端。如果最终跑在 Jetson 这类边缘盒子上s 是甜点如果跑在服务器 GPU 上做批量离线推理m 甚至 l 都可以考虑。第一版我建议统一从 s 开始把流程跑通再根据验证集的小目标召回情况决定要不要升。3.2 训练命令与关键参数YOLOv8 的训练入口很简洁一条命令搞定但参数里藏着不少门道。yolo detect train \ modelyolov8s.pt \ dataroof_dataset.yaml \ epochs150 \ imgsz1024 \ batch8 \ lr00.01 \ lrf0.01 \ warmup_epochs3 \ cos_lrTrue \ mosaic1.0 \ scale0.5 \ degrees0.0 \ fliplr0.5 \ flipud0.5 \ patience30 \ projectruns/roof \ namev1逐条说。imgsz1024是关键航拍小目标多640 会把小屋顶压没1024 是精度和显存的平衡点显存不够就降到 896 或 768别硬撑。batch8配合 1024 输入在 16G 显存的卡上比较稳显存富余可以往上加。lr00.01是初始学习率配合cos_lrTrue余弦退火比阶梯下降更平滑。warmup_epochs3让模型前几轮慢慢升温避免一开始梯度炸掉。数据增强这块要特别注意。mosaic1.0默认开启对密集小目标有帮助但航拍图本身已经是俯视拼接mosaic 四合一后可能出现不自然的接缝如果验证发现模型对拼接边缘敏感可以降到 0.5。degrees0.0我强烈建议保持 0航拍图旋转会引入大量黑边而且屋顶的朝向本身是有语义的比如坡屋顶朝向和光照相关乱转反而有害。fliplr和flipud都开 0.5俯视图上下左右翻转是合理的。patience30是早停30 轮验证 mAP 不涨就停省时间。3.3 训练过程怎么看loss 和 mAP 的读法训练启动后终端会打印每轮的 box_loss、cls_loss、dfl_loss 和 mAP50、mAP50-95。新手最容易犯的错是只盯 mAP不看 loss。box_loss 持续下降说明框回归在学cls_loss 下降说明分类在学dfl_loss 是分布焦点损失和框的精细度相关。如果 box_loss 降但 mAP 不涨大概率是标注框质量差或者验证集分布和训练集不一致。另一个信号是 mAP50 和 mAP50-95 的差距。航拍屋顶任务里mAP50 到 0.85 以上不算难但 mAP50-95 往往只有 0.5 左右因为屋顶边界模糊IOU 阈值一高就框不准。如果你的 mAP50 高但 mAP50-95 很低说明模型“找到了”但“框不精”这时候可以考虑加更多边界清晰的样本或者换更大的输入尺寸。训练日志里runs/roof/v1/results.csv可以用 pandas 读出来画曲线比终端刷屏直观得多。import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/roof/v1/results.csv) df.columns df.columns.str.strip() plt.plot(df[epoch], df[metrics/mAP50(B)], labelmAP50) plt.plot(df[epoch], df[metrics/mAP50-95(B)], labelmAP50-95) plt.xlabel(epoch) plt.ylabel(mAP) plt.legend() plt.savefig(map_curve.png, dpi150)这段脚本把训练曲线画出来重点看两条线是否还在涨、有没有过拟合的拐点。如果训练 mAP 还在涨但验证 mAP 平了就是过拟合该早停或加增强如果两条都平了且很低那是欠拟合或数据问题加轮次没用得回头查标注。4. 推理、调参与效果验证的实操细节4.1 推理命令与置信度阈值训练完拿 best.pt 推理命令很简单但阈值设置直接决定你看到的结果。yolo detect predict \ modelruns/roof/v1/weights/best.pt \ sourcetest_images/ \ imgsz1024 \ conf0.25 \ iou0.5 \ saveTrue \ save_txtTrueconf0.25是置信度阈值低于它的框不输出。航拍屋顶密集阈值设高了漏检设低了误检一堆。我的做法是先设 0.25 看整体再针对漏检的图单独降到 0.15 看是不是被阈值卡掉了。iou0.5是 NMS 的 IOU 阈值屋顶紧挨着的时候这个值设太低会把相邻屋顶合并设太高又会保留重复框0.5 到 0.6 之间比较稳。save_txtTrue会把每个框的坐标和置信度存成 txt方便后续做统计或二次过滤。4.2 小目标漏检的三个调法如果验证发现小屋顶漏检严重按这个顺序调。第一提高推理输入尺寸从 1024 提到 1280 甚至 1536小目标像素变多召回立刻改善代价是速度下降。第二在训练时把imgsz也提上去让模型在训练阶段就见过大分辨率下的小目标比只在推理时放大更有效。第三检查数据里小目标的样本比例如果小屋顶只占 5%模型自然偏向大目标可以单独切一批小目标密集的图做过采样或者在损失里给小目标更高权重。还有一个容易被忽略的点NMS 之前先做类别内合并。航拍屋顶如果只分一类NMS 就是纯几何抑制如果分了多类不同类之间的框不会互相抑制可能出现一个屋顶被同时框成“平屋顶”和“光伏板”。这时候要么在推理后加逻辑规则要么在训练时把易混类别的样本标清楚。4.3 用验证集做定量评估光看图不够得有数字。YOLOv8 的 val 命令会输出每类的 P、R、mAP50、mAP50-95。yolo detect val \ modelruns/roof/v1/weights/best.pt \ dataroof_dataset.yaml \ imgsz1024 \ batch8 \ conf0.001 \ iou0.6注意conf0.001验证时要把阈值压到极低让所有预测都参与 mAP 计算这样得到的指标才反映模型真实能力而不是被阈值美化过的。iou0.6是评估时的匹配阈值和推理时的 NMS iou 不是一回事别搞混。看结果时重点看 recall屋顶识别漏检的代价通常比误检高recall 低于 0.8 就得回头调。5. 航拍屋顶识别避坑记录这五个坑我踩过5.1 坑一验证集 mAP 很高上线全是漏检现象是本地验证 mAP50 到 0.9换一批新区域的图推理漏检一大片。原因是数据集按随机划分同一栋楼的不同切块同时进了训练和验证模型其实在“背题”。解决方法是按拍摄区域或拍摄架次划分数据集同一区域的数据只出现在一个集合里。如果数据量够最好再留一个完全独立的测试区域训练全程不碰最后只测一次。5.2 坑二训练 loss 正常但 mAP 一直是 0现象是 box_loss 在降但 mAP 始终 0。九成是标注格式或类别配置错了。排查顺序先看 labels 目录下 txt 是不是空的再看 txt 里的 class 编号是不是从 0 开始且和 yaml 的 names 对得上最后看图片路径有没有写错导致加载的是空图。还有一个隐蔽情况图片是灰度图或带 alpha 通道YOLOv8 读取时通道数不对也会导致训练异常。统一转成三通道 RGB 再训。5.3 坑三显存爆了batch 降到 1 还是 OOM现象是 imgsz1024 时 batch8 直接 OOM降到 1 还是报错。原因往往不是 batch而是数据加载的 worker 太多或者缓存没清。先把workers降到 2 试试再检查是不是有其他进程占着显存。如果单张 1024 都 OOM那说明卡太小老老实实降到 640 训练或者用梯度累积模拟大 batch。别硬扛 1024训不动就是训不动。5.4 坑四屋顶框大量重叠NMS 后只剩一个现象是推理结果里一片屋顶只出一个框。原因是 NMS 的 iou 阈值设太低相邻屋顶的框 IOU 超过阈值被互相抑制。解决是把推理时的iou提到 0.6 甚至 0.7或者在训练时就让模型学会更紧的框。另一个办法是改用 soft-NMS但 YOLOv8 默认不带需要自己改后处理成本较高优先调阈值。5.5 坑五模型对光照和季节变化极敏感现象是晴天训练的模型阴天图召回骤降。航拍图的光照差异比地面视角大得多阴影、反光、积雪都会改变屋顶外观。解决不是硬训一个模型通吃而是做数据增强时加入亮度、对比度、色调抖动并且在采集阶段尽量覆盖不同天气。如果条件允许按季节或光照条件分模型比一个模型硬扛效果好得多。6. 把屋顶检测推到可用切片推理与结果后处理第一版模型跑通之后真正决定能不能用的是推理阶段怎么处理大图。前面训练时切了块推理时如果直接把 4000×3000 的原图丢进去YOLOv8 会缩放到 1024小屋顶又没了。正确做法是切片推理再合并也就是 SAHI 那套思路但不用装额外库自己写几十行就够。import cv2 import numpy as np from ultralytics import YOLO model YOLO(runs/roof/v1/weights/best.pt) def slice_infer(img_path, tile1024, overlap200, conf0.25): img cv2.imread(img_path) h, w img.shape[:2] step tile - overlap boxes [] for y in range(0, h, step): for x in range(0, w, step): x2 min(x tile, w) y2 min(y tile, h) x1 max(0, x2 - tile) y1 max(0, y2 - tile) crop img[y1:y2, x1:x2] res model.predict(crop, imgsztile, confconf, verboseFalse)[0] for b in res.boxes: bx1, by1, bx2, by2 b.xyxy[0].tolist() # 把局部坐标还原到原图 boxes.append([bx1 x1, by1 y1, bx2 x1, by2 y1, float(b.conf)]) return boxes def merge_boxes(boxes, iou_thr0.5): boxes sorted(boxes, keylambda b: b[4], reverseTrue) keep [] for b in boxes: discard False for k in keep: ix1 max(b[0], k[0]); iy1 max(b[1], k[1]) ix2 min(b[2], k[2]); iy2 min(b[3], k[3]) iw max(0, ix2 - ix1); ih max(0, iy2 - iy1) inter iw * ih union (b[2]-b[0])*(b[3]-b[1]) (k[2]-k[0])*(k[3]-k[1]) - inter if union 0 and inter / union iou_thr: discard True break if not discard: keep.append(b) return keep raw slice_infer(test/DJI_0099.jpg) final merge_boxes(raw, iou_thr0.5) print(f合并前 {len(raw)} 个框合并后 {len(final)} 个框)这段代码分两步。slice_infer负责滑窗推理并把局部坐标加回原图偏移merge_boxes做跨块去重因为重叠区会让同一个屋顶被相邻两块各检一次。参数上tile和训练时保持一致overlap也保持一致iou_thr控制去重力度0.5 比较通用。合并后如果还有明显重复把 iou_thr 降到 0.4如果相邻屋顶被误合并升到 0.6。后处理还有一步值得做按面积过滤。航拍图里偶尔会出现极小的误检框面积小于 15×15 像素的直接丢掉对召回几乎无影响但能明显降误检。另外如果下游要做屋顶面积统计记得把像素面积按 GSD地面采样距离换算成平方米这个换算系数来自无人机飞行高度和相机参数每批数据可能不同别用固定值。最后说个我自己的习惯每次训完一版我都会挑 20 张“最差”的验证图——漏检最多、误检最多的那些——单独存一个文件夹改完参数先在这 20 张上看变化。这比盯整体 mAP 敏感得多整体指标涨 0.5 个点可能只是波动但这 20 张里少漏两个屋顶才是真的在变好。模型调优这件事指标是参考眼睛才是裁判。希望帮到你。本文还有配套的精品资源点击获取