☰
基于YOLOv8的钢筋数量识别实战:标注、训练与切片计数
2026/9/28 15:59:59 网站建设 项目流程

简介:面向计算机视觉课程设计需求,该资源以YOLO v3为核心,提供从钢筋图像标注、模型训练到数量识别的完整Python实现。包内工程结构清晰,包括核心模型代码、数据预处理脚本、锚框聚类、推理演示及相应说明文档,可帮助初学者快速搭建检测流程并理解一阶段检测器原理。资源压缩包共72个文件,以Python源码、XML标注文件、类别文件与PyCharm配置、Jupyter分析笔记等组成,整体仅2.4MB,轻量易部署。已有671人学习浏览,适合需要完成物体计数课题的高校学生或想动手实践YOLO算法的开发者。整体方案在公开基准中达到较高准确率,且预留调参空间,便于在此基础上做优化实验。

1. 钢筋数量识别到底解决什么问题:一堆钢筋要数清楚

工地上验收钢筋,材料员蹲在钢筋堆旁拿表格一根根点,一车螺纹钢小几千根,点完一小时过去,中间被打断一次就得重来。这个场景催生了“钢筋数量识别”这个需求:对着钢筋端面拍一张照片,让基于Python的视觉模型把每一根钢筋圈出来,直接输出数字。它不关心这是几号螺纹钢,只回答一个问题——这一堆里到底有多少根。项目听起来不大,但牵扯到数据标注、模型选型、训练调参和计数后处理一整条链路。适合谁看?现场做材料验收的技术员、做工地智能化软件的团队、刚入门想拿真实场景练手的目标检测开发者。

2. 技术选型:为什么传统图像处理在钢筋计数上翻车,目标检测才是正解

很多人拿到这个需求第一反应是:钢筋端面不是一个个圆吗?用OpenCV找圆不就行了。实际做下来,经典图像处理在这个场景几乎是全面翻车,翻车点还特别典型。先把选型理由讲清楚,后面的路才走得稳。

2.1 阈值分割、霍夫圆检测、连通域分析为什么数不清钢筋

先说阈值分割。钢筋端面经过切割和锈蚀,颜色是灰褐色,而工地背景往往是泥土色、水泥色,光照不均匀时端面和背景的灰度范围大面积重叠。你设一个阈值,要么把背景也切进来,要么把暗部的钢筋丢掉,怎么调都有一批端面粘连在一起。现场拍摄还有反光、阴影、指纹和油污,阈值分割出来的图基本没法看。

霍夫圆检测更直接:它假设目标是标准圆。但钢筋端面在轧制和切割过程中多少会变形,边缘不是平滑圆弧;成捆钢筋互相挤压,端面被挡住一半甚至三分之二的情况很常见。霍夫变换对“完整圆弧”有强依赖,一遇到遮挡,圆的拟合就崩了。就算强行调低累加器阈值,又会把钢筋表面的纹理误检成大量小圆,数量直接爆表。

连通域分析是另一个常见思路:把图像二值化、提轮廓、数连通块。问题是钢筋端面互相接触、锈水粘连时,二值化后几个端面连成一个连通域,数出来就少了。反过来,端面上的月牙纹、切割毛刺又会被拆成多个连通域,数出来就多了。这一多一少,误差动辄百分之十几,在验收场景里根本没法用。

这些方法本质上都在用“颜色、形状、边缘”这种手工特征,而钢筋端面恰恰是纹理复杂、形状非标准、光照随意的工业对象。手工特征撑不住,就只能让模型自己学特征。目标检测模型直接输出每个端面的位置框,天然绕开了“分割-数连通块”的老路。

2.2 目标检测模型的选型比较:YOLOv8为什么是性价比之选

确定了走目标检测路线,剩下的问题是用哪个模型。Faster R-CNN精度不差,但推理速度慢、部署依赖重,工地现场往往就一台普通办公电脑,跑起来吃力。SSD在密集小目标上的表现一般,钢筋端面恰恰是典型的小目标密集场景。YOLO系列是目前工业落地最成熟的检测框架,而YOLOv8把训练和部署都收进ultralytics一个包,对单类目标检测项目来说,学习成本和踩坑成本都最低。

模型参数量量级精度定位最合适的场景
YOLOv8n约 3M轻量够用CPU推理、快速验证
YOLOv8s约 11M精度/速度均衡普通显卡训练,通用首选
YOLOv8m约 26M精度更稳密集小目标、端面特征细
YOLOv8l约 43M高精度但慢服务端离线批量处理
YOLOv8x约 68M最高精度对速度无要求的研究场景

我一般从n或s起步,先用小模型把数据链路跑通,再换m提精度。钢筋端面识别有个容易踩的误区:以为模型越大越好。实际上一张端面照片里可能有几十上百根钢筋,输入图片被缩放到640×640后,每根钢筋只有十几个像素,换大模型也救不回来。提升密集团目标效果最直接的手段是提高输入分辨率或者做切片推理,这个放到后面细讲。选型上记住一句话:先跑通小模型,再拿分辨率换精度,最后才轮到换大模型。

3. 数据准备:拍好钢筋端面照片,把标注转成YOLO格式

数据准备决定了模型上限。钢筋识别只有一个类别,看起来简单,但现场拍摄环境杂乱,数据怎么拍、怎么标,直接决定模型会不会过拟合到某个工地。

3.1 采集与标注规范:让模型知道“钢筋端面长什么样”

采集照片时相机要尽量正对钢筋端面,不要斜着拍。斜拍会让圆形端面变成椭圆,模型学到的形状特征就会偏移。拍摄距离保持稳定,让端面在画面里占合适比例,一般单根端面直径占画面宽度的三十分之一到五十分之一比较合适。要覆盖不同堆叠状态:散放的、捆扎整齐的、横放竖放混在一起的、端面有锈蚀和切割毛刺的,都要拍。光照也要拉开差异,晴天直射、阴天散射、人工补光各来一批,避免模型只认识某一个工地的光线条件。

标注用的工具常见是LabelImg,在Windows上直接pip安装就能用。标注格式选YOLO或者PascalVOC都行,如果后面要转格式,建议一开始就统一成YOLO格式,省一道转换工序。标注规范只有一条硬性要求:端面轮廓清晰可见的才标,被完全遮挡到看不出圆形结构的不用标。半遮挡但能明显看出是钢筋端面的,正常标,这部分样本对模型抗遮挡能力帮助很大。

起步阶段300到500张标注图就能跑出一个能用的雏形,做到800张以上精度会明显稳定下来。这个数量是我做类似小目标检测项目时的常见经验,单类目标不需要一上来就追求几千张,先把bad case收集机制跑起来,后面针对性补数据。

3.2 把VOC标注转成YOLO格式:批量转换脚本与四个边界坑

如果手头数据是VOC格式的XML,需要转成YOLO格式的txt才能喂给YOLOv8。转换逻辑不复杂:读XML里的bndbox坐标,换算成归一化的中心点坐标和宽高,一行一个框写入txt。下面这个脚本可以直接用:

import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, img_w, img_h, class_index=0): tree = ET.parse(xml_path) root = tree.getroot() boxes = [] for obj in root.findall('object'): # 过滤掉标注为difficult的样本,这类框往往是半截钢筋或严重遮挡 difficult = obj.find('difficult') if difficult is not None and difficult.text == '1': continue name = obj.find('name').text if name is None: continue bndbox = obj.find('bndbox') x1 = float(bndbox.find('xmin').text) y1 = float(bndbox.find('ymin').text) x2 = float(bndbox.find('xmax').text) y2 = float(bndbox.find('ymax').text) # 换算成YOLO要求的归一化坐标 cx = ((x1 + x2) / 2) / img_w cy = ((y1 + y2) / 2) / img_h bw = (x2 - x1) / img_w bh = (y2 - y1) / img_h boxes.append(f"{class_index} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") return boxes

这个脚本的核心是归一化换算:YOLO格式要的是中心点坐标和框宽高分别除以图片宽高,得到0到1之间的小数。调用时img_w和img_h直接取XML里size节点的宽高,不要重新读图,速度快且不容易出错。class_index只有钢筋一个类别,固定写0就行。批量转换时建议加一层try/except,把解析失败的XML文件名打印出来,后面统一修。

转换过程中有四个边界坑,都是实际踩过的。第一个坑:归一化分母必须用图片宽高,有人图省事直接用标注坐标的最大值当分母,转换出来的框全偏。第二个坑:如果图片先做了resize再标注,txt里必须用resize后的宽高,用原图尺寸会导致框全部偏移。第三个坑:VOC格式里difficult=1的框在转换时要过滤掉,这类框通常是半截钢筋或严重遮挡,留着只会给模型喂噪声。第四个坑:某些标注工具会生成没有bndbox节点的空XML,批量转换时直接让脚本抛异常,所以循环里一定要包异常处理。

4. 模型训练与调参:用YOLOv8跑通钢筋端面检测

数据准备好之后进入训练环节。这个项目类别少,YOLOv8的训练流程相对固定,但有几个参数是钢筋场景特有的,调不好精度上不去。

4.1 训练集目录组织与YAML配置:先让代码找得到数据

YOLOv8对数据目录有约定要求,按下面结构整理最省事:

dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── rebar.yaml

images和labels要严格同名对应,比如images/train/IMG_001.jpg对应labels/train/IMG_001.txt。训练集和验证集建议按9:1或8:2划分,划分时用随机种子,不要手动挑,避免把某一类堆叠状态全分到验证集里。

YAML配置文件这样写:

path: D:/datasets/rebar_yolo train: images/train val: images/val names: 0: rebar

path写数据集根目录的绝对路径,train和val写相对于path的子目录路径。这里有一个常见错误:有人把train写成完整的绝对路径,导致换机器路径不对直接报错。names只有一个类别,索引从0开始,这个索引必须和txt标签文件里的类别编号一致,否则训练和推理对不上。

4.2 训练命令与参数:哪些参数直接影响钢筋端面检测

用ultralytics的Python接口训练,脚本很短。先把环境装上:pip install ultralytics,然后记得先装好对应版本的PyTorch。如果电脑还没配好Python环境,先把vscode和Python解释器弄好再继续,这个坑不多但很耽误时间。

from ultralytics import YOLO model = YOLO("yolov8n.pt") model.train( data="rebar.yaml", epochs=120, imgsz=1024, batch=8, patience=20, device=0, lr0=0.01, hsv_v=0.4, close_mosaic=10, )

参数里最关键的是imgsz。钢筋端面是小目标,imgsz=640会让每根钢筋只有十几个像素,模型很难学。我一般直接上1024,端面像素能到二十到三十个,精度提升非常明显。代价是显存占用翻倍,8GB显存跑1024加batch=8比较紧张,显存不够就把batch降到4,或者先用640跑通再渐进调大。

epochs设120配合patience=20做早停,一般60到80轮就能收敛,早停能省时间。lr0用默认的0.01就行,这个值在YOLOv8上很稳定。hsv_v=0.4是我针对工地光照不确定性加的颜色增强,模拟顺光、逆光、阴影下的色偏,让模型别死记训练工地的光线。close_mosaic=10表示最后10轮关闭马赛克增强,这个参数是ultralytics的常见设置,让模型在训练末期用更真实的数据做收敛,对最终精度有正面作用。

训练完看两个东西:runs/detect/train/weights/best.pt是最优权重,后面推理都用它;val目录下的results.csv里能看到每轮的loss和mAP曲线。如果train loss不断下降、val loss也同步下降,说明模型在正常学习。如果val loss在某个epoch后反弹,大概率是过拟合了,钢筋类别少、数据量不大的时候很容易出现,应对方法不是加正则,而是补数据或增强数据多样性。

5. 高频踩坑与排查清单:漏检、重复计数、现场翻车的原因和解决

目标检测项目最大的问题往往不是模型训不出来,而是训出来的模型到现场不好用。下面几条都是钢筋数量识别里最容易翻车的情况,按现象、原因、解决三个步骤写清楚。

5.1 同一根钢筋被识别成两个重复框,数量偏多

现象:模型对同一根钢筋输出了两个高度重叠的框,直接数框的话数量比实际多。

原因:模型训练时对同一目标输出了多个候选框,NMS(非极大值抑制)没能把它们合并。YOLOv8默认的NMS IoU阈值是0.7,阈值偏松时,两个IoU刚好低于阈值的重叠框会被同时保留。

解决:把推理阶段的conf阈值和NMS的iou阈值分开调。用模型自带的predict(iou=0.5, conf=0.25),或者在后处理里自己再做一次NMS。实测把iou调到0.5,重复框数量会明显下降,代价是极少数的紧邻钢筋会合并成一个框,需要结合下一节的计数后处理来权衡。

5.2 测试集mAP很高,新工地现场照片却漏检严重

现象:模型在验证集上mAP达到0.9以上,拿到另一个工地拍的照片,漏检率一下子飙到百分之二三十。

原因:训练数据和现场数据存在分布差异。这不是玄学,是典型的过拟合——模型可能记住了训练照片里的背景、光照、拍摄角度,而不是真正学会“识别钢筋端面”。钢筋场景里最常见的差异是拍摄距离、相机型号、堆叠方式和光线色温。

解决:采集数据时就故意混入不同工地、不同手机、不同时段的照片,训练时加大hsv增强力度。更有效的做法是“现场先试拍,模型跑一遍,把bad case手动挑出来补标注”,跑两三轮迭代后现场精度会有一个明显回升。

5.3 端面与背景对比度低时,输出一堆碎框

现象:阴天或逆光条件下拍的端面照片,背景灰暗,模型把钢筋表面的锈斑、月牙纹、切割纹误检成钢筋端面,输出大量面积很小的碎框。

原因:钢筋端面本身纹理复杂,低对比度时模型会把局部纹理当作目标特征。尤其当训练数据里纹理清晰的端面占多数,模型容易学成“有环形纹理就算钢筋”。

解决:后处理里加一个面积过滤,把宽度或高度小于正常端面尺寸一定比例的框直接丢弃。这个比例根据拍摄距离标定,比如端面直径在1024分辨率下通常大于40像素,那就过滤掉宽高小于25像素的框。这个办法简单有效,比调低confidence阈值可靠得多。

5.4 训练到一半loss变成nan

现象:训练跑到十几个epoch,loss突然变成nan,之后指标全部乱掉。

原因:最常见的诱因是学习率设置过高或数据里出现空标签文件。钢筋项目类别少,学习率一般不会爆,空标签反而更容易出问题——labels目录里如果有一个空的txt文件,模型读到的目标是空tensor,数值计算直接发散。

解决:训练前写个脚本扫描一遍labels目录,把大小小于3字节的txt文件列出来。把它们对应的图片也移出训练集,重新组织目录再跑。如果是学习率的问题,把lr0从0.01降到0.005就能压住。

6. 进阶技巧:大图切片推理与计数后处理,把模型输出变成可靠数字

模型训练好了,最后一公里是把“检测框”变成“钢筋数量”。这一步做不好,前面所有工作都会在验收环节翻车。尤其当一张照片里有几百根钢筋时,直接整图推理会有两个大坑:一是一张图塞不下,缩放后端面变成像素点;二是密集场景下漏检和重复框并存,简单数框根本不准。解决办法是切片推理加全局后处理。

6.1 为什么大图必须切片:输入缩放对小目标的伤害

假设一张照片宽度为4000像素,端面直径约50像素。整图缩放到1024后,端面直径只剩约13像素,检测器能提取的特征少得可怜。把图切成512×512的小块,端面直径能保留到原来的比例,检测难度直接下降一个量级。这就是切片推理的核心价值:不让缩放吃掉小目标的特征。

6.2 切片推理后处理脚本:坐标偏移、全局NMS与最终计数

下面这段脚本实现了简化版的切片推理:把大图切成固定尺寸的小块,每块做预测,预测框坐标偏移回原图坐标系,最后统一做一次NMS。

import cv2 import numpy as np from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") def slice_predict(img_path, slice_size=640, overlap=0.2): img = cv2.imread(img_path) h, w = img.shape[:2] stride = int(slice_size * (1 - overlap)) all_boxes = [] for y in range(0, h, stride): for x in range(0, w, stride): y_end = min(y + slice_size, h) x_end = min(x + slice_size, w) patch = img[y:y_end, x:x_end] results = model.predict(patch, imgsz=640, conf=0.25, iou=0.5) for box in results[0].boxes.data.cpu().numpy(): x1, y1, x2, y2, score, cls = box[:6] # 坐标偏移回原图坐标系 all_boxes.append([x1 + x, y1 + y, x2 + x, y2 + y, score]) # 全局NMS,合并切片边缘的重复框 boxes = np.array(all_boxes) keep = nms(boxes, iou_threshold=0.5) return boxes[keep] def nms(boxes, iou_threshold=0.5): x1, y1, x2, y2, scores = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3], boxes[:, 4] areas = (x2 - x1) * (y2 - y1) order = scores.argsort()[::-1] keep = [] while order.size > 0: i = order[0] keep.append(i) xx1 = np.maximum(x1[i], x1[order[1:]]) yy1 = np.maximum(y1[i], y1[order[1:]]) xx2 = np.minimum(x2[i], x2[order[1:]]) yy2 = np.minimum(y2[i], y2[order[1:]]) inter = np.maximum(0, xx2 - xx1) * np.maximum(0, yy2 - yy1) iou = inter / (areas[i] + areas[order[1:]] - inter + 1e-6) order = order[np.where(iou <= iou_threshold)[0] + 1] return keep

切片尺寸和overlap的选择直接影响效果。slice_size取640较稳,端面直径在50像素左右时不会切碎目标;overlap取0.2是为了避免目标刚好落在切片边缘被截断。全局NMS是我处理钢筋计数最依赖的一步,切片边缘的重复框会被合并,同一根钢筋不会数两次。最终数量就是len(keep)。

切片推理的代价是耗时成倍增加,一张4000×3000的图切30来块,GPU推理约2到3秒,CPU上就要十几秒,但对验收场景完全可接受。拍一张照片换一个准确数字,怎么算都比人工数一遍合算。我第一次跑通这个流程时,就是没做全局NMS,数出来比实际多了三成,后来统一后处理才把数字稳定下来。这套方案从数据准备到落地推理都不复杂,真正需要花时间的是把现场bad case一轮轮收敛干净——这也是整个项目最花功夫的地方。希望帮到你。

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

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

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

立即咨询