俯拍航拍森林火灾检测数据集:VOC/YOLO转换与训练避坑

发布时间:2026/10/25 14:35:47
俯拍航拍森林火灾检测数据集:VOC/YOLO转换与训练避坑

简介:面向森林火灾智能监测与目标检测训练需求,这份文档系统说明了一套俯拍航拍森林火灾数据集,适合遥感视觉、应急预警算法研究与工程实践人员参考。资源包内为1个docx文件,约5.06MB,集中梳理数据集的格式组成、类别设置、标注统计、标注工具与使用说明。数据集共6116张图像,采用Pascal VOC与YOLO双格式标注:VOC以jpg图像配xml记录位置类别,YOLO以txt保存坐标;类别为fire与smoke,标注框分别15380和12613个,总计27993个,均由labelImg按矩形框规则完成。两类目标在俯拍场景中尺度差异明显,文档附有图片预览与标注示例,便于核对标注细节、理解类别边界,并说明模型精度不作保证,提醒使用者独立评估。目前已有62人浏览学习,可作为森林火灾与烟雾检测任务的数据准备、标注核查、模型训练与验证及实验设计参考。

1. 俯拍视角下的火焰与烟雾:一份能直接开训的双格式数据集

去年帮一个做林区监测的团队看模型,他们自己拿无人机拍了两个月,标了不到八百张,fire 和 smoke 两类混在一起,训出来的模型见了云就报警。问题不在模型,在样本——俯拍航拍视角下的火点和烟雾,跟地面拍摄完全是两个分布:目标小、对比度低、背景纹理高度重复。这份俯拍航拍森林火灾检测数据集正好补的就是这一块:6116 张 jpg,配套 6116 个 VOC 的 xml 和 6116 个 YOLO 的 txt,两类标签 fire 与 smoke,总框数 27993(fire 15380、smoke 12613),labelImg 画的矩形框。它解决的是「没有航拍火情样本、从零标注成本太高」这个最现实的问题,适合做目标检测落地、需要快速拉起一个可训练基线的算法和工程同学。要注意的是,数据集本身不承诺任何模型精度,标注对不对、够不够用,得你自己跑脚本验。

2. 先摸清标注分布:6116 张图背后的三个数字

拿到一个检测数据集,很多人第一件事是解压、改 yaml、开训。我一般反过来:先花二十分钟把标注统计跑一遍,因为后面所有的参数选择——输入分辨率、anchor、增广强度、甚至要不要做切片推理——都由这几个数字决定。这份数据集的三个关键量是:每图平均框数 27993 / 6116 ≈ 4.58,fire 与 smoke 的框数比约 1.22:1,以及框的面积分布(这一项原文档没给,必须自己算)。平均 4.58 个框意味着绝大部分图里目标并不密集,单图目标数少、但单目标尺寸可能很小,这正是航拍检测的典型形态,也决定了后面不能用太激进的 mosaic。

2.1 VOC 与 YOLO 两套文件怎么对应

数据集是双格式并存的,理解它们的映射关系是后面所有脚本的前提。VOC 侧是三件套:JPEGImages/xxx.jpg、Annotations/xxx.xml,xml 里用<size>记录宽高、用若干<object>记录name和bndbox(xmin/ymin/xmax/ymax,绝对像素坐标,左上右下)。YOLO 侧是两件套:images/xxx.jpg、labels/xxx.txt,txt 每行五个字段class cx cy w h,全部归一化到 0~1,且 cx/cy 是框中心。两边靠同名不同后缀配对,stem 必须严格一致——这是第一个容易翻车的地方,后面避坑章会展开。

目录大致是这样:

dataset/ ├── JPEGImages/ # 6116 张 jpg ├── Annotations/ # 6116 个 xml(VOC) ├── images/ # 6116 张 jpg(YOLO 侧,通常是同一批图的拷贝或软链) └── labels/ # 6116 个 txt(YOLO)

提示:先确认images和JPEGImages是同一批图(文件名集合完全相等),有些发布包两边会有缺漏,直接拿 images 训练、拿 Annotations 做校验,就会对不上账。

类别顺序也要先钉死。这份数据集只有两个类,YOLO 侧约定0=fire、1=smoke,但这个约定必须用脚本从 txt 里反查确认,不能靠猜——一旦顺序反了,模型会把烟雾全判成火,mAP 看着还行,实际完全不可用。

2.2 用一段脚本把框面积分位数打出来

下面这段脚本做四件事:遍历 xml 拿到每张图的宽高、统计每个类的框数、算每个框的面积占全图比例、检查有没有退化框(x2<=x1 或 y2<=y1 的脏标注)。跑完你就有了一张分布表,后面调分辨率全靠它。

import os, glob import xml.etree.ElementTree as ET from collections import Counter ANN_DIR = "Annotations" cls_cnt = Counter() # 每个类的框数 area_ratio = {"fire": [], "smoke": []} # 框面积占全图比例 per_img = [] # 每图框数 bad_boxes = [] # 退化框 sizes = Counter() # 图像宽高组合 for xml_path in glob.glob(os.path.join(ANN_DIR, "*.xml")): root = ET.parse(xml_path).getroot() sz = root.find("size") W, H = float(sz.find("width").text), float(sz.find("height").text) sizes[(int(W), int(H))] += 1 n = 0 for obj in root.iter("object"): name = obj.find("name").text.strip() bb = obj.find("bndbox") x1, y1 = float(bb.find("xmin").text), float(bb.find("ymin").text) x2, y2 = float(bb.find("xmax").text), float(bb.find("ymax").text) if x2 <= x1 or y2 <= y1: bad_boxes.append((os.path.basename(xml_path), name)) continue cls_cnt[name] += 1 area_ratio.setdefault(name, []).append((x2 - x1) * (y2 - y1) / (W * H)) n += 1 per_img.append(n) def pct(lst, ps=(10, 25, 50, 75, 90)): lst = sorted(lst) if not lst: return {} return {p: round(lst[int(len(lst) * p / 100)], 6) for p in ps} print("类别框数:", dict(cls_cnt)) print("图像尺寸分布 Top5:", sizes.most_common(5)) print("每图框数 均值/最大:", round(sum(per_img)/len(per_img), 2), max(per_img)) for k, v in area_ratio.items(): print(f"{k} 面积占比分位:", pct(v)) print("退化框数量:", len(bad_boxes), bad_boxes[:5])

逻辑说明:size节点给的是原图像素宽高,用它做归一化分母;area_ratio按类分开存,是因为 fire 和 smoke 的尺度差异通常很大,混在一起看会掩盖问题;per_img用来判断有没有「一张图几十个框」的极端样本,这种样本在 mosaic 里会主导梯度。

参数怎么改:pct里的分位数可以按需加 95、99,看长尾;如果sizes里出现多种分辨率(航拍数据常见),要记下来,因为后面统一 resize 时小图会被放大、放大倍率过高会让烟雾纹理糊成一团。

结果怎么读:如果 fire 的面积占比中位数明显小于 smoke,说明火点是小目标、烟雾是大目标,这两类不能用同一套 anchor 尺度去照顾,训练时要么提高输入分辨率保小目标,要么对烟雾单独放宽 NMS 阈值防止大面积框被抑制。如果 90 分位的面积占比接近 0.5 以上,说明存在几乎占满全图的大框,这类框在切片推理时会被切断,需要额外处理。

2.3 fire 与 smoke 的标注差异决定了调参方向

两个类的物理形态完全不同。火点边界相对清晰,labelImg 画框时人能看出一个明确的亮斑范围,框比较紧;烟雾是弥散的,从浓烟核心到边缘过渡几十上百像素,不同人标注的框大小能差一倍,框普遍偏松偏大。这个差异会直接反映在训练上:smoke 的定位损失收敛慢、mAP50-95 明显低于 mAP50,因为高 IoU 阈值下松框根本对不齐。

应对方式不是去改标注(6116 张重标不现实),而是在训练侧做两件事。第一,评价时把两个类分开看,别只看总体 mAP,smoke 的 mAP50-95 才是真实水位。第二,回归损失用 CIoU 或 EIoU 这类对尺度敏感的,别用纯 IoU 系列里对大框宽容度太高的变体;同时 NMS 的 iou 阈值不要压太低,0.5 左右比较稳妥,压到 0.3 会把相邻的烟雾碎片全吃掉。

另外,12613 个 smoke 框对应 6116 张图,意味着有不少图里 smoke 是多块分布的(一片主烟加几缕飘散),这些碎块在低分辨率下会连成一片或者直接消失。这一点在选输入尺寸时必须考虑进去,下一章会具体给数。

3. 从原始包到可训练:划分、转换与自检三步走

原始包是「能看」的状态,不是「能训」的状态。要做的事有三件:把 6116 张图切成 train/val(可选 test),把 xml 转成规范的 YOLO txt(如果发布包里的 txt 已经规范,这一步就退化成校验),最后写一份 data.yaml 并做一次 dry-run。这三步任何一步偷懒,后面训练出来的曲线都是假的。我见过最典型的情况是 val 集里混进了 train 的相邻帧,mAP 一路 0.9,换真实航拍视频直接崩到 0.3。

3.1 按文件名哈希划分,别用随机种子

用random.shuffle划分的问题是不可复现——换台机器、换个 Python 版本、哪怕只是中间插入一行代码,划分结果就变了,实验没法对比。更稳的做法是对文件名做哈希取模,天然可复现,而且天然打散。代码:

import os, glob, hashlib, shutil IMG_DIR = "JPEGImages" OUT = "split" VAL_RATIO = 0.15 # 验证集比例 TEST_RATIO = 0.05 # 测试集比例,不要就设 0 for sub in ("train", "val", "test"): os.makedirs(os.path.join(OUT, sub, "images"), exist_ok=True) os.makedirs(os.path.join(OUT, sub, "labels"), exist_ok=True) def bucket(stem: str) -> str: # 用 md5 前 8 位转整数,稳定且与文件顺序无关 h = int(hashlib.md5(stem.encode()).hexdigest()[:8], 16) % 10000 if h < VAL_RATIO * 10000: return "val" if h < (VAL_RATIO + TEST_RATIO) * 10000: return "test" return "train" stat = {"train": 0, "val": 0, "test": 0} for img in glob.glob(os.path.join(IMG_DIR, "*.jpg")): stem = os.path.splitext(os.path.basename(img))[0] split = bucket(stem) if split == "test" and TEST_RATIO == 0: split = "train" shutil.copy(img, os.path.join(OUT, split, "images", stem + ".jpg")) src_txt = os.path.join("labels", stem + ".txt") if os.path.exists(src_txt): shutil.copy(src_txt, os.path.join(OUT, split, "labels", stem + ".txt")) stat[split] += 1 print(stat)

逻辑说明:bucket把文件名映射到 0~9999,按比例切三段,同一文件名永远落同一桶。参数上,VAL_RATIO取 0.15 是我在六千量级数据上的常用起点,验证集九百多张,够跑出稳定的 mAP;TEST_RATIO设 0 时全部并入 train,适合只关心相对效果的场景。

一个必须提醒的点:如果这批图是从连续航拍视频里抽帧来的,相邻帧几乎一样,哈希划分挡不住这种泄漏。判断方法是看文件名里有没有连续编号——有的话先按编号分段(比如每 30 帧取 1 帧入 val),再对段做哈希。这一步多花十分钟,能省掉后面「线上效果和验证集差一倍」的排查。

3.2 VOC 转 YOLO:归一化、越界裁剪与精度

即使发布包带了 txt,我也建议自己跑一遍转换再和原 txt 对一遍,因为标注质量是这份数据里最需要验证的部分。转换的核心就三行公式,但边界处理是坑点。

import os, glob import xml.etree.ElementTree as ET CLASSES = ["fire", "smoke"] # 顺序必须和 data.yaml 完全一致 SAVE_DIR = "labels_converted" os.makedirs(SAVE_DIR, exist_ok=True) def clip(v, lo, hi): return max(lo, min(hi, v)) for xml_path in glob.glob("Annotations/*.xml"): root = ET.parse(xml_path).getroot() sz = root.find("size") W, H = float(sz.find("width").text), float(sz.find("height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text.strip() if name not in CLASSES: continue # 未登记类别直接丢弃,不猜 cid = CLASSES.index(name) bb = obj.find("bndbox") x1 = clip(float(bb.find("xmin").text), 0, W) y1 = clip(float(bb.find("ymin").text), 0, H) x2 = clip(float(bb.find("xmax").text), 0, W) y2 = clip(float(bb.find("ymax").text), 0, H) if x2 - x1 < 1 or y2 - y1 < 1: # 裁剪后退化,跳过 continue cx = (x1 + x2) / 2 / W cy = (y1 + y2) / 2 / H bw = (x2 - x1) / W bh = (y2 - y1) / H lines.append(f"{cid} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") stem = os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(SAVE_DIR, stem + ".txt"), "w") as f: f.write("\n".join(lines))

逻辑说明:clip是因为航拍标注里确实存在 xmax 略大于 width 的情况(画框时鼠标甩出去了),不裁的话归一化后坐标大于 1,训练时要么被框架静默丢弃、要么直接报错。1e-6精度保留六位是惯例,够用且不会让文件膨胀。框宽高裁剪后小于 1 像素的直接丢,这种框训练时只会贡献噪声。

参数上唯一要动的是CLASSES的顺序。想省事可以直接从已有 txt 里统计出现的 class id 集合,和 xml 的 name 集合对一遍,两边数量一致再继续。

3.3 data.yaml 与开训前的 dry-run

文件齐了不代表能训。写一份最小的 yaml,然后跑一次单 batch 的前向,看有没有「标签越界」「找不到图片」这类报错。

# data.yaml path: /abs/path/to/split train: train/images val: val/images names: 0: fire 1: smoke

dry-run 用一条命令就够,目的是让框架自己把标签读一遍:

yolo detect train data=data.yaml model=yolov8n.pt epochs=1 imgsz=640 batch=4 \ workers=0 plots=False val=False

workers=0是为了让报错直接打在终端上而不是被多进程吞掉;val=False跳过验证,只验证数据管道通不通;一个 epoch、batch=4 是最小开销。跑通了再放大参数,这一步别省。

4. 训练参数:分辨率、增广与指标口径

参数没有标准答案,但有明确的因果链。这份数据的三个特征——平均框数少、烟雾大而糊、火点小而亮——决定了参数调整的优先级:分辨率 > 增广 > 学习率。六千张图的量级,训一个中等大小的检测器在单卡上几个小时内能出结果,真正耗时间的是反复试错,所以参数要在开训前想清楚。

4.1 输入分辨率:640 是底线,不是默认

imgsz小目标保留情况显存与速度适用场景
640远距离小烟点可能只剩几像素最省,迭代快快速验证管道、粗调超参
960多数框能保留结构中等常规训练起点
1280小框纹理基本不丢显存翻倍以上,batch 要降最终出模型、远距离监测

判断依据还是第 2 章算出来的面积占比分位数。如果 fire 的面积占比 10 分位低于 0.001(在一张 1920×1080 的图上意味着不到 45×45 像素),640 输入下这个框只剩 15×15 左右,特征图上的响应很弱,recall 上不去。这种情况我会直接从 960 起步,训完再用 1280 微调若干 epoch,而不是一上来就 1280 硬扛显存。

还要注意一点:数据集里如果混有多种原始分辨率,统一 resize 到 imgsz 会让小图被放大,放大倍率超过 2 时烟雾纹理明显发虚,反而伤模型。这种情况用 letterbox 保持长宽比、灰边填充,比直接拉伸稳妥。

4.2 增广:别把烟雾的低对比度削没了

通用的强增广套件放到这个场景里,有几个是要打折的。mosaic 会把四张图拼一起,单图里本来就不多的目标被进一步缩小,小烟点容易在拼接边缘被切断,建议mosaic从默认的 1.0 降到 0.5 左右,并在最后 10~15 个 epoch 关掉(close_mosaic),让模型在真实分布上收尾。色彩方面,hsv_v适当加大是有益的——真实烟雾在逆光、阴天、黄昏下亮度差异极大,亮度扰动相当于免费的域增广;但hsv_s不要开太猛,烟雾和背景灰雾主要靠饱和度差异区分,饱和度过扰动会让模型学不到颜色线索。

明确不建议的:随机擦除(Random Erasing)和小目标裁剪类增广。烟雾本身就低对比度,擦掉一块很可能正好擦掉唯一的目标区域,模型学成「看到被擦除的块就预测有烟」。翻转可以用,但垂直翻转要谨慎——俯拍视角下地形纹理的上下方向性不强,垂直翻转问题不大,水平和垂直都开是合理的。

4.3 指标口径:漏检率比 mAP 更该盯

火情监测这个场景,误报的代价是人工复核一次,漏报的代价是一次火情没被及时发现。所以评价指标不能只看总体 mAP50。我一般要求同时看四个数:fire 的 recall、smoke 的 recall、两类各自的 mAP50-95、以及按框面积分层(小/中/大)的 recall。前两个直接对应业务,后两个定位问题——如果小框 recall 明显低于大框,就是分辨率不够;如果 smoke 的 mAP50 高但 mAP50-95 低,就是标注框松的问题,跟模型无关。

实操上,训练日志里的metrics/recall(B)是 batch 平均,受 batch 组成影响,不如训完在 val 集上单独跑一次yolo detect val拿混淆矩阵可靠。混淆矩阵里fire被判成背景的那一格,就是漏检数,直接除以 fire 总框数就是漏检率,这个数字比 mAP 更能说服人。

5. 避坑与排查:五个把我拖到凌晨的点

下面五条都是真踩过的,按「现象 → 原因 → 解决」写,遇到同类问题可以直接对号。

5.1 现象:转换后 txt 数量比 jpg 少了几十个

原因:那些图里确实没有目标(纯林地、纯天空),labelImg 存出来就是空文件,有些打包脚本会把 0 字节文件过滤掉,或者你以为它是损坏文件手动删了。 解决:YOLO 里空 txt 是合法的负样本,它告诉模型「这张图没有目标」,对降低误报很有价值,必须保留。先核对labels目录里到底有没有这些空文件,再决定是补空文件还是从训练集里剔除对应图片。补空文件用一行touch就行,但补之前确认图里真的没目标,别把漏标的图当负样本喂进去。

5.2 现象:训练报Label class X is invalid或框明显偏移

原因:xml 里 xmax 超出图像宽度,或者 xmin/xmax 写反。归一化后 cx 变成负数或大于 1。 解决:回第 3.2 节的转换脚本,clip到[0, W]/[0, H],并且加一条退化框判断(宽或高小于 1 像素直接跳过)。别指望框架帮你裁,不同框架对越界坐标的处理策略不一样,有的直接丢样本,你会莫名其妙少一批数据。

5.3 现象:val mAP 0.9,换真实航拍片段只有 0.4

原因:划分泄漏。同一段航线连续拍摄的帧被随机分到了 train 和 val,两边的图几乎一模一样。 解决:不要按单张图随机划分,按「拍摄段前缀」分组后再分组划分,或者干脆按文件名序号分段取样。判断有没有泄漏的土办法:从 val 里抽 10 张,肉眼在 train 里找有没有几乎同视角的图,找到一张就说明泄漏了。

5.4 现象:找不到 label,日志里全是 background

原因:文件名大小写或后缀不一致。图片是.JPG,标签是.jpg.txt或.txt前缀不匹配;也有xxx(1).jpg这种带括号的命名,在不同系统上处理不一致。 解决:写一个集合比对脚本,把 images 和 labels 的 stem 各自 set 一遍,求差集,差集非空就先修文件名,不要靠框架的容错去猜。命名统一成纯小写字母数字下划线最保险。

5.5 现象:模型把所有目标都预测成 fire

原因:data.yaml里names的顺序和 txt 里的 class id 对不上,或者类别名带了空格/大小写差异(Firevsfire)。 解决:从 txt 里统计实际出现的 id 集合,从 xml 里统计实际出现的 name 集合,两边同时打印出来对照,确认0对应哪个、1对应哪个。这一步一分钟,比训完两小时发现全错强。改完 yaml 记得清掉缓存文件,有些框架会把数据集信息缓存在labels.cache,不删的话新配置不生效。

6. 进阶:切片推理把大图的小烟点捞回来

训练分辨率定了 960 或 1280,不代表推理时也得整图缩放。真实航拍图动辄 4000×3000,整图缩到 1280 送进网络,远处那个几十像素的初起烟点基本就没了——而初起烟点恰恰是最有价值的。工程上的解法是滑窗切片:把大图按固定窗口切块、逐块推理、再把所有框映射回原图坐标做一次全局 NMS 合并。代价是推理次数按切片数量线性增长,所以切片大小和重叠率要算。

切片边长重叠推理耗时倍数(相对整图)收益
640128约 20~30 倍极小目标召回提升最明显,适合离线批处理
960192约 10~15 倍大多数场景的平衡点
1280256约 6 倍兼顾速度,边缘切断裂问题少

核心实现不复杂,用 numpy 做切块和 NMS,避免引额外依赖:

import numpy as np from ultralytics import YOLO def nms(boxes, scores, iou_thr=0.5): # boxes: (N,4) xyxy order = scores.argsort()[::-1] keep = [] while order.size > 0: i = order[0] keep.append(i) if order.size == 1: break xx1 = np.maximum(boxes[i, 0], boxes[order[1:], 0]) yy1 = np.maximum(boxes[i, 1], boxes[order[1:], 1]) xx2 = np.minimum(boxes[i, 2], boxes[order[1:], 2]) yy2 = np.minimum(boxes[i, 3], boxes[order[1:], 3]) inter = np.maximum(0, xx2 - xx1) * np.maximum(0, yy2 - yy1) area_i = (boxes[i, 2] - boxes[i, 0]) * (boxes[i, 3] - boxes[i, 1]) area_o = (boxes[order[1:], 2] - boxes[order[1:], 0]) * (boxes[order[1:], 3] - boxes[order[1:], 1]) iou = inter / (area_i + area_o - inter + 1e-9) order = order[1:][iou <= iou_thr] return keep def sliced_infer(model, img, slice_size=960, overlap=192, conf=0.25): h, w = img.shape[:2] stride = slice_size - overlap # 保证最后一块贴边,避免右下角漏检 ys = sorted(set(list(range(0, max(h - slice_size, 0) + 1, stride)) + [max(h - slice_size, 0)])) xs = sorted(set(list(range(0, max(w - slice_size, 0) + 1, stride)) + [max(w - slice_size, 0)])) all_boxes, all_scores, all_cls = [], [], [] for y in ys: for x in xs: patch = img[y:y + slice_size, x:x + slice_size] r = model.predict(patch, conf=conf, verbose=False)[0] if r.boxes is None or len(r.boxes) == 0: continue b = r.boxes.xyxy.cpu().numpy() b[:, [0, 2]] += x # 映射回原图坐标 b[:, [1, 3]] += y all_boxes.append(b) all_scores.append(r.boxes.conf.cpu().numpy()) all_cls.append(r.boxes.cls.cpu().numpy()) if not all_boxes: return np.zeros((0, 6)) boxes = np.concatenate(all_boxes) scores = np.concatenate(all_scores) clses = np.concatenate(all_cls) out = [] for c in np.unique(clses): # 分类别做 NMS,别把两类混在一起抑制 idx = np.where(clses == c)[0] keep = nms(boxes[idx], scores[idx], 0.5) for k in keep: out.append([*boxes[idx][k], scores[idx][k], c]) return np.array(out) model = YOLO("runs/detect/train/weights/best.pt") img = __import__("cv2").imread("your_large_aerial.jpg") res = sliced_infer(model, img, slice_size=960, overlap=192, conf=0.25) print("合并后目标数:", len(res))

逻辑说明:ys/xs里显式补了max(h - slice_size, 0),是为了让最右、最下那一块贴边,否则边角区域会有一块永远不被覆盖——航拍图里的边角恰恰常有目标。坐标加x/y的偏移必须在拼回原图后做,不能先做 NMS 再映射。按类别分别做 NMS 是硬性要求,fire 和 smoke 框空间上高度重叠,混着做会把烟雾框当成火的重复框抑制掉。

参数怎么调:overlap不小于slice_size的 20%,否则跨切片的完整目标会被两半切断、每半都不够置信度;conf在切片模式下可以适当放低到 0.2,因为切片后目标变大、置信度分布上移,低阈值多召回的框靠 NMS 收掉。合并阈值 0.5 是折中,如果想进一步压误报可以提到 0.6,但 smoke 的召回会掉。

验证方法很直接:挑二十张有代表性的原图,分别跑整图推理和切片推理,把两者的框画出来叠在一起看,重点看边角和远景区域——切片多出来的那些框,就是整图缩放丢掉的。如果多出来的框大部分是误报,说明conf放太低了,回调到 0.3 再看。

从那以后我每次拿到新的航拍检测数据集,都会先按第 2 章那段统计脚本跑一遍面积分位数,再决定输入分辨率,最后一定用切片推理在几张原图上做对比验证——顺序搞反了,就得靠训完几轮再回头怀疑数据来补课,成本高得多。希望帮到你。

本文还有配套的精品资源,点击获取