☰
钢筋计数数据集解析:VOC标注转YOLO格式与训练实践
2026/9/24 23:16:26 网站建设 项目流程

简介:面向钢筋计数算法开发与计算机视觉目标检测实践,这份资源提供VOC格式的训练集标注文件,适用于钢筋盘点、工地物料统计等场景,帮助研究者省去从零采集和标注图像的繁琐过程。压缩包内共568个XML文件,每个XML对应一张训练图片的标注信息,包含目标类别名称、边界框坐标等关键字段,可直接用于YOLO、Faster R-CNN等主流检测框架的模型训练;包体整体仅1.07MB,RAR格式,传输与解压都非常便捷。目前已有659人学习浏览,适合具备一定深度学习基础的开发者、高校学生以及相关工程技术人员使用。通过参考博客链接可预先查看原始图片样本,确认图像质量与标注风格;若与同系列未标注测试集搭配使用,还能完成模型训练的闭环验证,评估泛化性能。数据集规模适中,既便于快速上手实验,也能为钢筋计数算法的迭代优化提供可靠标注基础。

1. 钢筋计数数据集:569 张 VOC 标注文件到底能干什么

做工地钢筋盘点的人应该都有体会,一捆钢筋拉到堆场,验收时要一根根点数,动辄上千根,人工数又慢又容易错,两个人数的结果对不上还要返工。这正是人工智能视觉计数最典型的落地场景——把相机架在堆场或传送带上方,用目标检测模型把每根钢筋的端面或侧面框出来,数一下框的数量就完成了盘点。这个数据集就是为这类钢筋计数算法开发准备的:训练集标注图片 569 张、测试集未标注图片 85 张,本次拿到的是训练集对应的那一摞 VOC 格式 XML 标注文件。适合正在做钢筋计数算法、毕业设计,或者熟悉 YOLO 系检测流程但缺少一个贴近工程场景的数据集来练手的人。资源发布时附带了一篇博客用于预览图片质量,看图画质符合预期再动手下载,效率会高不少。

2. 拆开 XML 看标注:VOC 格式里到底存了什么

2.1 一条钢筋是怎么被标成一堆坐标的

VOC 格式是目标检测领域最通用的标注格式之一,PASCAL VOC 数据集就是用它发布的。它的核心是以 XML 文件为单位,每张图片对应一个同名 XML,图片里每个目标用一个<object>节点描述。钢筋计数数据集里的 XML 文件名是一串哈希值,比如33DDB09455AB4E1CA72B21ADFBBC30A2.xml,这类命名在公开数据集里很常见,好处是全局唯一、不怕重名,坏处是脱离图片后完全不知道内容是什么。

打开一个 XML 能看到这样的核心结构:<filename>存图片名,<size>存图片宽高和通道数,<object>节点里<name>是类别名,<bndbox>里四个坐标xmin、ymin、xmax、ymax是标注框的左上角和右下角像素坐标。钢筋计数场景里通常只有一个类别,比如rebar或steel,所有框的name相同,靠坐标位置区分每一根钢筋。

这里有一个值得注意的点:钢筋是细长圆柱体,端面视角下是一堆圆形或椭圆形,侧面视角下是长条矩形,两种视角下标注框的宽高比差异很大。拿到数据集后第一步应该先统计所有标注框的宽高分布,确认这个数据集用的是端面视角还是侧面视角,这直接影响后续模型的选择和 anchor 配置。

2.2 用脚本批量解析所有标注文件

XML 是文本文件,直接用 Python 的xml.etree.ElementTree就能解析,不需要装额外依赖。我一般会把解析封装成一个函数,输入 XML 路径,输出图片信息和所有目标框列表,这样后续做格式转换、质量校验、可视化都能复用同一套代码。

import xml.etree.ElementTree as ET import glob def parse_voc_xml(xml_path): tree = ET.parse(xml_path) root = tree.getroot() # 图片基本信息 filename = root.findtext('filename') size = root.find('size') width = int(size.findtext('width')) height = int(size.findtext('height')) depth = int(size.findtext('depth')) # 所有目标框 boxes = [] for obj in root.iter('object'): name = obj.findtext('name') bndbox = obj.find('bndbox') xmin = int(float(bndbox.findtext('xmin'))) ymin = int(float(bndbox.findtext('ymin'))) xmax = int(float(bndbox.findtext('xmax'))) ymax = int(float(bndbox.findtext('ymax'))) boxes.append({ 'name': name, 'xmin': xmin, 'ymin': ymin, 'xmax': xmax, 'ymax': ymax }) return { 'filename': filename, 'width': width, 'height': height, 'depth': depth, 'boxes': boxes } # 批量解析所有 XML xml_files = sorted(glob.glob('./annotations/*.xml')) for xml_file in xml_files[:3]: # 先看前三张 info = parse_voc_xml(xml_file) print(f"{info['filename']} {info['width']}x{info['height']} targets: {len(info['boxes'])}") print(f" 第一个框: {info['boxes'][0]}")

代码逻辑很直接:parse_voc_xml函数用findtext提取文本节点,坐标统一转成int,因为像素坐标不可能是小数。遍历root.iter('object')能拿到所有目标,即使 XML 里嵌套层级比标准结构复杂也不容易漏。批量解析时先打印前三张确认格式没问题,再做全量处理。

参数说明:findtext('width')如果节点不存在会返回None,直接int()会报错,所以严谨的做法是先判空,但这个数据集是标准 VOC 导出,可以直接信任结构。glob.glob('./annotations/*.xml')的路径要替换成你实际存放 XML 的目录。

2.3 检查标注质量:重复框、漏标和越界是重灾区

拿到别人的标注文件,最忌讳的事情是直接拿去训练。先花十分钟做质量校验,能省下后面排错的几个小时。钢筋这种密集小目标场景里,最容易出现三类问题:重复框、坐标越界、类别名不统一。

重复框的表现是同一根钢筋被标注了两次,两个框的重合度很高,训练时模型会对同一个目标产生两个高置信度预测,NMS 后结果不稳定。坐标越界是标注时手滑把框拖出了图片边界,xmax大于图片宽度,训练时 YOLO 系模型算损失会出问题。类别名不统一更隐蔽,有的文件里写rebar,有的写steel,模型会当成两个类去学,计数时还得后处理合并。

import glob issues = [] for xml_file in glob.glob('./annotations/*.xml'): info = parse_voc_xml(xml_file) class_names = set() for box in info['boxes']: class_names.add(box['name']) # 检查坐标越界 if box['xmin'] < 0 or box['ymin'] < 0: issues.append(f"{info['filename']}: 负坐标 {box}") if box['xmax'] > info['width'] or box['ymax'] > info['height']: issues.append(f"{info['filename']}: 越界 {box}") # 检查宽高为 0 的退化框 if box['xmax'] <= box['xmin'] or box['ymax'] <= box['ymin']: issues.append(f"{info['filename']}: 退化框 {box}") # 检查类别名是否统一 if len(class_names) > 1: issues.append(f"{info['filename']}: 多个类别 {class_names}") # 检查重复框(IoU > 0.9 视为重复) boxes = info['boxes'] for i in range(len(boxes)): for j in range(i + 1, len(boxes)): a, b = boxes[i], boxes[j] inter_w = max(0, min(a['xmax'], b['xmax']) - max(a['xmin'], b['xmin'])) inter_h = max(0, min(a['ymax'], b['ymax']) - max(a['ymin'], b['ymin'])) inter_area = inter_w * inter_h a_area = (a['xmax'] - a['xmin']) * (a['ymax'] - a['ymin']) b_area = (b['xmax'] - b['xmin']) * (b['ymax'] - b['ymin']) union_area = a_area + b_area - inter_area if union_area > 0 and inter_area / union_area > 0.9: issues.append(f"{info['filename']}: 疑似重复框 {a} 和 {b}") print(f"发现问题 {len(issues)} 条") for issue in issues[:20]: print(issue)

这个脚本的 IoU 计算简化了一点,但用来发现重复框足够了。跑完之后把有问题的那几张 XML 单独拎出来,用可视化脚本把框画到图片上人工确认,该删的删、该改的改。这类数据集是人工标注的,出现瑕疵是常态,不是数据有问题,而是你需要在训练前替标注员做一次质检。血泪经验:这一步省掉的时间,后面都会在 loss 不收敛和目标漏检上加倍还回来。

3. 把 VOC 转成训练能吃的格式:YOLO txt 与 COCO json 转换全流程

3.1 为什么必须先转格式:YOLO 吃 txt、MMDetection 吃 json

VOC XML 是一种"给人看"的标注格式,结构清晰、可读性强,但主流深度学习框架训练时不会直接读 XML。YOLO 系(YOLOv5、YOLOv8、YOLO11)要求每张图片对应一个同名 txt 文件,每行一个目标,格式是class_id x_center y_center width height,四个坐标全部归一化到 0~1 区间。MMDetection 系则要求把所有标注汇总到一个 COCO 格式的 json 文件里,包含images、annotations、categories三个字段。

转换的核心是坐标系的换算。VOC 里存的是像素绝对坐标xmin/ymin/xmax/ymax,YOLO 里要的是归一化后的中心点和宽高,换算公式是:

x_center = ((xmin + xmax) / 2) / image_width y_center = ((ymin + ymax) / 2) / image_height width = (xmax - xmin) / image_width height = (ymax - ymin) / image_height

分母是图片宽高而不是标注框的最大值,这一点最容易写错。归一化后所有值都在 0~1 之间,与图片分辨率解耦,模型输入尺寸变了标注也不用改。

3.2 写转换脚本:从 object 节点到归一化中心点

用前面写好的parse_voc_xml函数,转换脚本本身非常短。我一般会同时生成训练集和验证集两个目录,按 8:2 比例随机划分,并用固定随机种子保证每次划分结果一致,方便复现实验。

import glob import os import random import shutil def voc_to_yolo(xml_files, output_dir, train_ratio=0.8, seed=42): random.seed(seed) random.shuffle(xml_files) train_files = xml_files[:int(len(xml_files) * train_ratio)] val_files = xml_files[int(len(xml_files) * train_ratio):] for split, files in [('train', train_files), ('val', val_files)]: label_dir = os.path.join(output_dir, 'labels', split) os.makedirs(label_dir, exist_ok=True) for xml_file in files: info = parse_voc_xml(xml_file) label_path = os.path.join(label_dir, os.path.splitext(os.path.basename(xml_file))[0] + '.txt') with open(label_path, 'w') as f: for box in info['boxes']: # 注意:所有类都归为 0,因为钢筋数据集只有一个类别 x_center = ((box['xmin'] + box['xmax']) / 2) / info['width'] y_center = ((box['ymin'] + box['ymax']) / 2) / info['height'] w = (box['xmax'] - box['xmin']) / info['width'] h = (box['ymax'] - box['ymin']) / info['height'] # 裁剪到 [0, 1] 区间,防止标注越界导致训练崩溃 x_center = max(0, min(1, x_center)) y_center = max(0, min(1, y_center)) w = max(0, min(1, w)) h = max(0, min(1, h)) f.write(f"0 {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n") print(f"训练集 {len(train_files)} 张,验证集 {len(val_files)} 张") return train_files, val_files xml_files = sorted(glob.glob('./annotations/*.xml')) train_files, val_files = voc_to_yoto(xml_files, './yolo_dataset')

这里多做了一个加保护的操作:坐标裁剪到 [0, 1] 区间。前面质检发现了越界框,但为了不让一个坏标注毁掉整个训练流程,转换时顺手 clip 一下。训练时 YOLO 对越界的归一化坐标处理方式是直接丢弃该目标,如果一张图里坏标注比例高,等于白白丢了训练样本。所以更推荐的做法是转换前去重修正,转换时只做兜底。

参数说明:train_ratio=0.8是常见的训练验证划分比例,569 张图分出约 455 张训练、114 张验证。钢筋计数场景里图片之间差异不大(都是堆场、都是钢筋),8:2 够用。如果你的场景变化大,建议提到 9:1 保证验证集多样性。seed=42固定随机种子,这个数字没有魔法含义,只是为了实验可复现。.6f格式化成 6 位小数,归一化坐标 6 位小数对应的像素误差在 640 分辨率下小于 0.001 像素,足够用了。

3.3 验证转换结果:画框可视化是唯一的可信方式

转换完别急着训练,先可视化验证。写 txt 坐标写错是常有的事——比如宽高写成 xmax/ymax、忘记归一化、框画到了图片外面。肉眼看到标注框准确贴住钢筋端面,才算转换成功。

import cv2 import numpy as np def visualize_yolo(image_path, label_path, class_names=['rebar']): img = cv2.imread(image_path) h, w = img.shape[:2] with open(label_path) as f: lines = f.readlines() for line in lines: parts = line.strip().split() cls_id = int(parts[0]) x_center = float(parts[1]) * w y_center = float(parts[2]) * h box_w = float(parts[3]) * w box_h = float(parts[4]) * h x1 = int(x_center - box_w / 2) y1 = int(y_center - box_h / 2) x2 = int(x_center + box_w / 2) y2 = int(y_center + box_h / 2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[cls_id], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2) cv2.imshow('check', img) cv2.waitKey(0) cv2.destroyAllWindows() # 随机抽两张验证集图片检查 for xml_file in val_files[:2]: basename = os.path.splitext(os.path.basename(xml_file))[0] image_path = f'./images/{basename}.jpg' # 根据你的图片实际扩展名调整 label_path = f'./yolo_dataset/labels/val/{basename}.txt' visualize_yolo(image_path, label_path)

这段代码的关键在于从归一化坐标还原回像素坐标时,必须乘以图片当前的实际宽高,而不是模型输入尺寸。如果你后面用 640×640 输入训练,但原图是 2000×1500,可视化时乘 640 就会出现框偏小的问题。我习惯直接读原图尺寸来可视化,这样能同时确认图片本身是否完好。

提示:如果发现某张图的标注框明显偏离钢筋位置,优先怀疑图片和 XML 文件名不匹配,而不是坐标换算错误。两个文件来自不同来源时这种情况经常发生。

4. 选模型与训练落点:从 YOLOv8 到密度图回归的取舍

4.1 钢筋计数有两条路线:检测求和还是密度图积分

钢筋计数本质上是一个"数数"问题,但实现路线有两条。第一条是把计数当目标检测做,检测出每根钢筋的框,然后对框的数量求和。这条路线简单直接,模型输出的框还能用于定位和质量分析,工程上最常用。第二条是把计数当密度估计做,用 CSRNet 这类模型输出密度图,对整个图的密度积分得到总数。密度图路线在极度密集、遮挡严重的场景下表现更好,但代价是需要逐像素标注或高斯核生成密度图,且无法给出每根钢筋的位置。

这个数据集是 VOC 检测框标注,天然适配第一条路线,直接用 YOLO 系模型训练即可。如果后续想尝试密度图路线,可以用检测框的中心点做高斯核扩散生成密度图,这个后面第 6 章会展开。

对比维度检测求和(YOLO 系)密度图积分(CSRNet 系)
标注要求目标框,本数据集已具备点标注或检测框转换
输出信息位置 + 数量仅数量(可后处理取位置)
密集遮挡场景漏检较多更鲁棒
工程部署成熟,TensorRT/OpenVINO 都支持相对小众
训练成本单卡几小时单卡几小时,调参更敏感

4.2 用 YOLOv8 在自己的数据集上训练

确认是单类别钢筋检测后,训练配置非常简单。先准备一个 data.yaml 文件,声明训练集和验证集路径、类别数和类别名,然后一条命令启动训练。

# data.yaml 内容 # path: /path/to/yolo_dataset # train: images/train # val: images/val # nc: 1 # names: ['rebar']

训练命令:

yolo detect train \ data=rebar.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ patience=30 \ optimizer=AdamW \ lr0=0.001 \ project=./runs \ name=rebar_count

几个参数值得展开。model=yolov8s.pt是加载 COCO 预训练权重做迁移学习,钢筋虽然不在 COCO 的 80 个类别里,但预训练模型已经学会了通用纹理和边缘特征,迁移后收敛速度远快于从零训练。imgsz=640是输入分辨率,如果原图里钢筋密集且每根很小,建议提高到 1024 甚至 1280,小目标检测对分辨率非常敏感,但显存占用会随之翻倍。batch=16在单张 24G 显存卡上配imgsz=640刚好,显存小就降到 8,同时把workers=8的数据加载线程配上去防止 GPU 空转。

patience=30是早停参数,验证集性能 30 个 epoch 不提升就自动停止。钢筋数据集场景单一,通常 50~80 个 epoch 就能收敛,设 150 的 epoch 上限是为了给复杂场景留余地,早停会在合适时机截断训练。lr0=0.001配合 AdamW 是迁移学习场景下比较稳的起点,SGD 的话建议 0.01。

4.3 评估指标:MAE 和 MSE 才是计数的核心指标

分类任务看 mAP,但计数任务的核心指标是平均绝对误差和均方根误差。mAP 衡量的是"框得准不准",而计数关心的是"数得对不对"。一根钢筋的框和标注框重合度只有 0.6,mAP 会扣分,但对总数没有任何影响。所以训练完看 mAP 的同时,必须单独算 MAE 和 MSE。

import numpy as np def compute_count_metrics(predictions, ground_truths): """ predictions: list[int], 每张图预测的钢筋数量 ground_truths: list[int], 每张图真实的钢筋数量 """ pred = np.array(predictions, dtype=np.float32) gt = np.array(ground_truths, dtype=np.float32) mae = np.mean(np.abs(pred - gt)) mse = np.sqrt(np.mean((pred - gt) ** 2)) return mae, mse # 示例:3 张验证图的预测和真实值 preds = [56, 132, 88] gts = [60, 128, 90] mae, mse = compute_count_metrics(preds, gts) print(f"MAE: {mae:.2f} 根, RMSE: {mse:.2f} 根")

代码逻辑不复杂,但有一个细节值得注意:评估时要把单张图的预测数累加起来算误差,而不是把所有图的目标框混在一起算。因为每张图的钢筋根数不同,逐图误差才能反映模型在不同密度下的表现。如果总根数算误差,一张 500 根的大图和一张 20 根的小图权重相同,50 根的绝对误差在 500 根那张图上不明显,但在 20 根那张图上就是灾难。

对这个数据集来说,判断模型能不能用的标准很简单:验证集 MAE 稳定在个位数,说明模型对常规堆场场景已经可用。如果 MAE 在 20 以上,优先检查是不是测试集和训练集分布差异太大(比如拍摄角度不同),或者在推理侧漏掉了大批量小目标,二选一,基本跑不掉。

5. 避坑实录:训练钢筋计数模型最容易翻车的五个地方

5.1 图片和 XML 对不上,一训练就报错

现象:数据加载时报FileNotFoundError,提示找不到某张图片,或者训练过程中 loss 突然变成nan。

原因:这个数据集的 XML 是哈希命名,图片文件名如果被批量重命名过,两边的名字就对不上了。如果训练脚本按base_name + .jpg去拼接图片路径,一旦实际扩展名是.png或.jpeg,就会直接扑空。更隐秘的是图片存在但内容张冠李戴——XML 里的filename指向 A 图片,实际你在 images 目录里放的是 B 图片,训练时模型看到的是"这个框配那个图",loss 直接飘掉。

解决:写一个自动化匹配脚本,遍历所有 XML 的<filename>,检查对应图片是否存在、尺寸是否与<size>一致。尺寸都对得上才继续。如果发现不一致,优先以 XML 的<filename>为准重新整理图片目录,不要手工一个个改。

5.2 坐标越界导致 loss 变为 NaN

现象:训练跑到第 20 个 epoch 左右,box_loss 突然变成nan,之后的 loss 全部失效,模型输出全是垃圾框。

原因:前面质检发现越界框时如果不处理,YOLO 在计算损失时会对归一化坐标做边界判断。越界目标的x_center可能算出 1.2、width可能算出 -0.1,这些值进入损失函数后经过 log 运算直接产生nan。更麻烦的是,nan会通过梯度传播污染整个 batch 的参数更新,等发现时模型已经废了。

解决:严格做两遍清理。第一遍在格式转换时 clip 坐标到 [0, 1] 区间,这是兜底。第二遍在训练前用脚本统计所有训练标注,把xmax <= xmin或ymax <= ymin的退化框直接删除,而不是修正——因为退化框往往意味着标注员画了个零面积框,修正出来的位置大概率也是错的。

5.3 密集区目标重叠,NMS 把该数的钢筋全滤掉了

现象:验证集 MAE 在 15 根左右,排查发现漏检全部集中在钢筋堆叠最密的区域,每捆钢筋中间被检成"一整块"。

原因:密集场景下相邻钢筋的检测框重叠度极高,IoU 可能到 0.7 以上。YOLO 默认的 NMS 阈值(iou=0.45)会把重叠框当作重复检测直接抑制掉,结果每两个相邻钢筋只保留一个,计数少一半。

解决:把推理时的 NMS IoU 阈值调高到0.7,同时把置信度阈值从默认的0.25降到0.15。这两个参数一个放宽"允许保留的重叠度",一个放宽"允许保留的置信度",配合使用能把挨在一起的钢筋框都留下来。代价是误检会增多,但计数任务里误检可以通过后处理过滤——比如框面积明显小于正常钢筋的剔除——漏检则很难补救。

5.4 光照变化:同一个堆场上午下午效果两个样

现象:模型在白天拍摄的验证图上 MAE 只有 4 根,但傍晚或阴天拍摄的图 MAE 直接飙到 30 根以上。

原因:钢筋表面是高反光金属,阳光直射时端面是高亮白色,阴天时是灰暗色,模型学到的其实是"特定光照下的钢筋外观"而不是"钢筋"本身。数据集的 569 张训练图如果都是同一时段拍的,这种分布偏移会非常明显。

解决:训练时在数据增强里打开亮度、对比度、HSV 扰动。YOLOv8 的默认增强包含hsv_h=0.015, hsv_s=0.7, hsv_v=0.4,如果默认增强不够,把hsv_v调到0.6并对图像做scale=0.3的随机缩放增强。这个场景下,增强的多样性比模型容量更重要。

5.5 拿测试集当验证集用,指标好看但上线就废

现象:验证集 MAE 在 2 根以内,模型完美收敛,但部署到现场新的照片上效果稀碎。

原因:我见过太多人把数据集里的测试集图片直接用来自测,美其名曰"验证泛化性"。但这份数据集的测试集是未标注的 85 张图,你拿它当验证集就得自己标一遍——一旦标了再拿去调参,测试集就被污染了,指标全部虚高。真正的验证集应该从带标注的训练集里划分,测试集只有最终定稿后才能碰一次。

解决:严格按照 训练集 455 张 / 验证集 114 张 / 测试集留作最终评估 的三段划分。验证集用来选超参和早停,测试集只在所有实验做完后跑一次,记录最终 MAE 就封存。从那以后我拿到任何数据集,第一件事就是把测试集物理隔离到单独目录,防止手滑把它拖进训练流程。

6. 把计数精度再往上提一截:滑窗推理与密度图交叉验证

检测模型训好后,提升计数的最终落点通常在推理侧而不是模型侧。钢筋原图很多是几千万像素的高清大图,直接缩放到模型输入尺寸会导致一堆小目标消失。常见做法是滑窗推理:把原图切成多个 640×640 的块,每块独立推理,最后把结果拼回原图坐标。切块时要有 20%~30% 的重叠,重叠区域的重复框用全局 NMS 压掉。

def sliding_window_detect(img, model, window_size=640, stride=512): h, w = img.shape[:2] detections = [] for y in range(0, h, stride): for x in range(0, w, stride): x_end = min(x + window_size, w) y_end = min(y + window_size, h) crop = img[y:y_end, x:x_end] results = model(crop, conf=0.15, iou=0.7) for box in results[0].boxes: x1, y1, x2, y2 = map(int, box.xyxy[0]) # 坐标偏移回原图 detections.append([x1 + x, y1 + y, x2 + x, y2 + y, float(box.conf[0])]) # 全局 NMS 去重重叠区域 detections = nms_by_list(detections, iou_threshold=0.5) return detections

stride=512表示滑窗步长小于窗口尺寸,重叠 128 像素。这个重叠量能保证钢筋被切断的概率降到最低——如果钢筋刚好在切割线上被切成两半,两个窗口各检出半根,全局 NMS 会把两个半截框合并成一个完整框。逻辑不复杂,但推理速度会变成原来的窗口数倍,实际部署时需要权衡实时性。

另一个值得试的技巧是密度图交叉验证:用检测框中心点生成高斯密度图,训练一个轻量密度回归分支,推理时把检测求和和密度积分两个结果对比,差异超过 10% 就对该区域重新推理。钢筋计数这种场景,密度图不需要很精确,用一个简单特征提取器加回归头就能对检测结果形成校验。两套方法互相纠偏,比单模型硬扛可靠得多。

最终落地时我通常把三个环节串成一条链:滑窗检测得到候选框 → 密度图校验总数量 → 异常区域二次检测。整套流程在验证集上能把 MAE 从单模型推理的 8~10 根压到 3~4 根。从那以后我每次拿到新的计数数据集,都会强制自己把推理链路先搭好再回头调模型——多数时候,精度瓶颈不在模型参数,而在推理策略和后处理。这个习惯帮我省过不少冤枉时间,希望也能帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询