拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python+Faster-RCNN实现PCB元器件缺陷检测:从数据标注到模型调参全流程

Python+Faster-RCNN实现PCB元器件缺陷检测:从数据标注到模型调参全流程

简介:这是一套面向高校学生与开发者的PCB元器件缺陷检测实战项目,基于Python与Faster-RCNN实现,配套完整源码、开发文档与项目解析,适合用作毕业设计、课程设计或工程练手。项目围绕VOC07+12数据集展开,涵盖训练、预测与评估三条主线:既可训练标准数据集,也能迁移到自建数据集;既支持预训练权重推理,也支持自训练权重验证,并给出mAP评估流程。压缩包共79个文件,以35个py源码与39个pyc编译文件为主,另含md说明、txt依赖清单及license等,整体约214KB,目录按数据预处理、网络结构、训练与预测脚本分层组织,便于按模块阅读与二次开发。资源已通过严格测试,读者可据此掌握RPN、锚框聚类、数据增强与模型评估等关键环节,并在此基础上替换主干网络或扩展缺陷类别。目前已有381人学习下载。

1. 从一张贴片板说起:Python + Faster-RCNN 做 PCB 元器件缺陷检测到底在解决什么

手里拿到一块刚过炉的 PCB,贴片电阻歪了、电容偏了、某个 QFN 少了一角,甚至整颗料根本没贴上——这类问题在批量产线上靠人眼盯,盯两小时就开始漏。基于 Python + Faster-RCNN 的 PCB 元器件缺陷检测,本质就是把「找元件 + 判缺陷」这两件事交给一个两阶段目标检测网络:先用区域建议网络(RPN)在整板上框出可能是元器件的位置,再对每个候选框做分类和边框回归,输出「是什么元件、在哪、有没有缺陷」。它适合谁?做毕业设计、课程设计、项目开发的学生和刚转机器视觉的工程师,尤其是手头只有普通工业相机、没有深度学习产线经验、想用 Python 快速跑通一套可复现方案的人。这套方案不追求产线级节拍,追求的是你能从零把数据、训练、推理、评估这条链路走通,并且知道每一步的参数为什么这么设。源码和开发文档的价值也在这里——它把踩过的坑固化下来,让你少走弯路。

2. 数据与标注:PCB 缺陷检测的第一道生死线

2.1 为什么 PCB 数据集比想象中难搞

PCB 元器件缺陷检测的难点不在网络,在数据。工业现场拿到的原始图往往是大分辨率整板图,一块 300mm×200mm 的板子用 500 万像素相机拍,单张图里可能有几百个元件,小目标密集、类别极不均衡——正常元件成千上万,真正的缺陷样本可能只有几十个。如果直接把整板图丢给 Faster-RCNN,RPN 生成的 anchor 会淹没在背景里,正负样本比例能到 1:1000 以上,训练直接崩。

常见做法是先把整板图按固定步长裁成 512×512 或 800×800 的小图,裁的时候保留重叠区域(overlap 一般取 100~200 像素),避免元件被切断。裁完之后再筛:只保留包含至少一个标注框的图块,纯背景块直接丢掉。这一步能把无效样本砍掉一大半。

标注格式建议直接用 VOC 的 XML 或 COCO 的 JSON。VOC 更直观,适合手写解析脚本;COCO 生态工具多,pycocotools 评估方便。类别命名要克制,别一上来分十几类。我一般先分两大类:component(正常元件)和defect(缺陷元件),等模型稳了再细分缺件、偏移、极性反、破损这些子类。类别太细、样本又少,Faster-RCNN 的分类头根本学不动。

提示:标注框要贴着元件本体画,不要框到焊盘外的丝印。丝印文字会干扰 RPN,让网络把丝印当成元件。

2.2 用 Python 把 VOC 标注转成训练可用的格式

Faster-RCNN 在 PyTorch 下最省事的入口是 torchvision 自带的fasterrcnn_resnet50_fpn,它默认吃 COCO 格式的 target。所以拿到 VOC XML 后,第一步是转成 COCO 风格的字典。下面这段脚本把 VOC 的 XML 读成{image_id, boxes, labels}结构,供自定义 Dataset 使用。

import os import xml.etree.ElementTree as ET import torch from PIL import Image # 类别映射:0 是背景,Faster-RCNN 内部保留,标注从 1 开始 CLASS_MAP = {"component": 1, "defect": 2} def parse_voc_xml(xml_path): tree = ET.parse(xml_path) root = tree.getroot() boxes, labels = [], [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in CLASS_MAP: continue bbox = obj.find("bndbox") # VOC 坐标是 1-based,转成 0-based 的 xyxy x1 = float(bbox.find("xmin").text) - 1 y1 = float(bbox.find("ymin").text) - 1 x2 = float(bbox.find("xmax").text) - 1 y2 = float(bbox.find("ymax").text) - 1 # 过滤掉宽高小于 4 像素的脏框 if x2 - x1 < 4 or y2 - y1 < 4: continue boxes.append([x1, y1, x2, y2]) labels.append(CLASS_MAP[name]) if not boxes: return None return { "boxes": torch.tensor(boxes, dtype=torch.float32), "labels": torch.tensor(labels, dtype=torch.int64), }

逻辑说明:CLASS_MAP把类别名映射成整数,背景 0 由网络内部处理,所以标注从 1 开始。VOC 的坐标是 1-based,减 1 转成 0-based,否则框会整体偏移一个像素,小目标上这个偏差很致命。过滤宽高小于 4 像素的框,是因为这种框在 RPN 里几乎不可能匹配到正样本,留着只会增加噪声。

参数说明:CLASS_MAP按你的实际类别改;宽高阈值 4 是经验值,如果你的元件最小尺寸在 10 像素以上,可以提到 6~8。返回None表示这张图没有有效标注,Dataset 里要跳过。

2.3 自定义 Dataset 与数据增强的取舍

torchvision 的检测模型要求 Dataset 返回(image_tensor, target),image 是[C,H,W]的 float tensor,target 是上面那种字典。PCB 图像增强要小心:水平翻转通常安全,垂直翻转对极性元件可能改变语义(比如二极管方向),旋转超过 15 度会让矩形元件框失真。我一般只用水平翻转 + 轻微亮度抖动,颜色抖动幅度控制在 0.1 以内,因为 PCB 的焊点颜色是重要特征,抖太狠反而掉点。

import torchvision.transforms as T from torch.utils.data import Dataset class PCBDataset(Dataset): def __init__(self, img_dir, xml_dir, transforms=None): self.img_dir = img_dir self.xml_dir = xml_dir self.transforms = transforms self.ids = [f[:-4] for f in os.listdir(xml_dir) if f.endswith(".xml")] def __getitem__(self, idx): img_id = self.ids[idx] img = Image.open(os.path.join(self.img_dir, img_id + ".jpg")).convert("RGB") target = parse_voc_xml(os.path.join(self.xml_dir, img_id + ".xml")) if target is None: # 没有有效标注就换下一张,避免训练时报空 target return self.__getitem__((idx + 1) % len(self.ids)) if self.transforms: img = self.transforms(img) return img, target def __len__(self): return len(self.ids)

逻辑说明:__getitem__里遇到空标注递归取下一张,这是处理脏数据的实用写法,比在外面预筛更省事。transforms只做ToTensor和归一化,翻转类增强建议在 Dataset 内部按概率手动做,因为翻转后 boxes 也要同步变换,用 torchvision 的 transform 不好处理。

参数说明:归一化用 ImageNet 的均值方差[0.485,0.456,0.406]和[0.229,0.224,0.225],因为主干是 ResNet50 预训练权重,输入分布对齐能加快收敛。

3. 模型搭建与训练:Faster-RCNN 在 PCB 上的参数怎么调

3.1 主干与 anchor 的选型理由

torchvision 的fasterrcnn_resnet50_fpn默认主干是 ResNet50 + FPN,对 PCB 这种小目标密集场景够用。如果你显存紧张(比如只有 6G),可以换fasterrcnn_mobilenet_v3_large_fpn,速度更快但小目标召回会掉几个点。PCB 元件普遍偏小,FPN 的多尺度特征很关键,所以别用没有 FPN 的老版本。

anchor 是 Faster-RCNN 的命门。默认 anchor 尺寸是(32,64,128,256,512),对 PCB 上几十像素的元件来说偏大。我一般改成(16,32,64,128,256),长宽比保留(0.5,1.0,2.0),因为元件大多是矩形,极端长宽比很少。改 anchor 要在构建模型时传anchor_generator参数,或者直接改源码里的AnchorGenerator。

import torchvision from torchvision.models.detection.faster_rcnn import FastRCNNPredictor from torchvision.models.detection.anchor_utils import AnchorGenerator def build_model(num_classes=3): # 自定义 anchor:更小的尺寸适配 PCB 小元件 anchor_gen = AnchorGenerator( sizes=((16,), (32,), (64,), (128,), (256,)), aspect_ratios=((0.5, 1.0, 2.0),) * 5 ) model = torchvision.models.detection.fasterrcnn_resnet50_fpn( weights="DEFAULT", anchor_generator=anchor_gen, min_size=800, max_size=1333 ) # 替换分类头,num_classes 包含背景 in_features = model.roi_heads.box_predictor.cls_score.in_features model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes) return model

逻辑说明:sizes每个尺度只给一个值,配合 5 个 FPN 层级,正好一一对应。aspect_ratios用元组重复 5 次,表示每个层级都用同样的长宽比。min_size=800是输入短边缩放目标,PCB 小图块如果本身是 512,会被放大到 800,小目标特征更明显,但显存占用上升。

参数说明:num_classes一定要包含背景,两类缺陷就是 3。weights="DEFAULT"加载 COCO 预训练权重,这是收敛快的关键,别从随机初始化开始训。

3.2 训练循环与学习率策略

Faster-RCNN 的训练和普通分类不一样,模型在 train 模式下直接返回 loss 字典,不需要你手动算 loss。优化器用 SGD,momentum 0.9,weight_decay 0.0005,初始学习率 0.005(batch size 2 时)。如果显存够、batch 能到 4,学习率可以提到 0.01。学习率调度用StepLR,每 3 个 epoch 乘 0.1,或者用MultiStepLR在 8、11 epoch 降。

import torch from torch.utils.data import DataLoader import torchvision.transforms as T def collate_fn(batch): return tuple(zip(*batch)) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = build_model(num_classes=3).to(device) transform = T.Compose([T.ToTensor(), T.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])]) dataset = PCBDataset("images", "annotations", transforms=transform) loader = DataLoader(dataset, batch_size=2, shuffle=True, collate_fn=collate_fn, num_workers=2) params = [p for p in model.parameters() if p.requires_grad] optimizer = torch.optim.SGD(params, lr=0.005, momentum=0.9, weight_decay=0.0005) lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.1) for epoch in range(12): model.train() for images, targets in loader: images = [img.to(device) for img in images] targets = [{k: v.to(device) for k, v in t.items()} for t in targets] loss_dict = model(images, targets) losses = sum(loss for loss in loss_dict.values()) optimizer.zero_grad() losses.backward() optimizer.step() lr_scheduler.step() print(f"epoch {epoch}, loss {losses.item():.4f}")

逻辑说明:collate_fn必须自定义,因为每张图的 target 框数量不同,默认的 collate 会报错。loss_dict里通常有loss_classifier、loss_box_reg、loss_objectness、loss_rpn_box_reg四项,直接求和反传。训练时不要做验证,Faster-RCNN 在 train 模式下不返回预测结果,验证要切model.eval()。

参数说明:batch_size=2是 8G 显存的稳妥值,显存大可以往上加。num_workers在 Windows 上设 0 更稳,Linux 上设 2~4。12 个 epoch 是中小数据集的常见量级,数据少于 500 张可以减到 8。

3.3 推理与后处理:把输出变成能看的框

推理时切model.eval(),输入同样要归一化。模型返回的是 list,每个元素对应一张图,包含boxes、labels、scores。默认 score 阈值 0.05,实际用的时候要卡到 0.5 以上,再用 NMS(模型内部已经做了,但可以再补一次)去掉重叠框。

model.eval() with torch.no_grad(): img = Image.open("test.jpg").convert("RGB") tensor = transform(img).unsqueeze(0).to(device) outputs = model(tensor)[0] # 按分数过滤 keep = outputs["scores"] > 0.5 boxes = outputs["boxes"][keep].cpu().numpy() labels = outputs["labels"][keep].cpu().numpy() scores = outputs["scores"][keep].cpu().numpy() for box, label, score in zip(boxes, labels, scores): print(f"label={label}, score={score:.3f}, box={box.astype(int)}")

逻辑说明:unsqueeze(0)把单张图变成 batch=1。outputs是字典,直接按 score 过滤。label 1 是正常元件,2 是缺陷,实际部署时只关心 label 2 的框。

参数说明:score 阈值 0.5 是起点,漏检多就降到 0.3,误检多就提到 0.7。PCB 缺陷检测通常宁可误检不可漏检,所以阈值偏低更安全。

4. 避坑与排查:PCB 缺陷检测训练中最容易翻车的 5 个点

4.1 现象:loss 一直不降,卡在 2.0 以上

原因:最常见的是标注坐标没转成 0-based,或者类别映射漏了背景导致 label 从 0 开始。Faster-RCNN 内部把 0 当背景,如果你的标注也用 0,分类头永远学不对。另一个原因是 anchor 尺寸和实际目标差太远,RPN 匹配不到正样本。

解决:先打印几个 target 的 boxes 和 labels,确认坐标范围和类别从 1 开始。再把 anchor 尺寸改成和实际框分布匹配的值,用kmeans聚一下训练集框的宽高,取 5 个聚类中心当 anchor size。

4.2 现象:验证时 mAP 很高,实际推理全是误检

原因:验证集和训练集来自同一批图,分布太像,模型过拟合。PCB 缺陷检测里,不同批次板子的光照、丝印颜色、焊点反光差异很大,训练集覆盖不到就会翻车。

解决:按板子批次划分训练/验证,别随机切。验证集至少留 2~3 个不同批次的板子。如果条件允许,加一些不同光照下拍的图做增强。

4.3 现象:小元件(0402 封装)几乎全漏检

原因:输入图缩放后小元件只剩几个像素,FPN 最底层特征也抓不住。默认min_size=800对整板图来说缩放比例太小。

解决:把整板图裁成更小的图块(比如 400×400),让元件在图块里占比更大。或者把min_size提到 1200,但显存要够。另一个办法是在 FPN 里加一层更高分辨率的特征,这个改动大,新手先别碰。

4.4 现象:训练到一半 loss 突然变 NaN

原因:学习率太大,或者某张图的 boxes 坐标超出图像边界,导致 ROI Align 采样越界。PCB 标注时手抖把框画到图外很常见。

解决:在 Dataset 里加一步 clamp,把 boxes 限制在[0, W, 0, H]范围内。学习率从 0.005 降到 0.001 试试。如果还 NaN,检查有没有宽高为 0 的框混进去。

4.5 现象:推理速度太慢,单张图要好几秒

原因:min_size=800加上 ResNet50 主干,在没 GPU 的机器上就是慢。另外如果没设torch.no_grad(),会白白算梯度。

解决:推理一定包torch.no_grad()。速度要求高就换 MobileNet 主干,或者把min_size降到 600。批量推理时把多张图拼成一个 batch,比一张张跑快很多。

5. 从能跑到好用:评估指标与一个提点的小技巧

模型训完只是开始,怎么判断它到底行不行,得看指标。目标检测通用的是 mAP(mean Average Precision),IoU 阈值 0.5 下的 mAP@0.5 是入门线,PCB 缺陷检测我一般还会单独看缺陷类(label 2)的召回率,因为正常元件检测得再好,缺陷漏了等于白做。用 pycocotools 算指标最省事,把预测结果转成 COCO 格式的 json 再调COCOeval。

指标含义PCB 场景参考值
mAP@0.5IoU=0.5 时各类 AP 均值正常元件 >0.9,缺陷 >0.6
Recall@0.5缺陷类召回率优先保证 >0.85
FPS单卡推理帧率产线参考 >10,离线 >2
单图耗时端到端推理时间含预处理 <500ms 算合格

一个提点的小技巧:在 RPN 阶段把正样本 IoU 阈值从默认 0.7 降到 0.5。PCB 元件密集,默认阈值太严,很多真实元件框匹配不上正样本,RPN 召回上不去。改这个参数在RPN的fg_iou_thresh里,降到 0.5 后缺陷召回通常能涨 3~5 个点,代价是误检略增,配合 score 阈值调一下就能压回来。

# 修改 RPN 正样本 IoU 阈值(需在 build_model 后手动改) model.rpn.fg_iou_thresh = 0.5 # 默认 0.7 model.rpn.bg_iou_thresh = 0.3 # 默认 0.3,保持

逻辑说明:fg_iou_thresh是判定正样本的 IoU 下限,降低它意味着更多候选框被当成正样本,RPN 学到的目标更全。bg_iou_thresh是负样本上限,一般不动,动了容易让正负样本失衡。

参数说明:0.5 是我在几个 PCB 数据集上试出来的平衡点,再低(比如 0.4)误检会明显增多。改完要重新训,不能只改推理。

这套方案我从数据清洗一路踩到 anchor 调参,最深的教训是:PCB 缺陷检测的瓶颈从来不在网络结构,而在你有没有把标注质量当回事。我见过太多人拿着脏标注训半个月,最后怪 Faster-RCNN 不行。先把每一张图的框画准、类别分对,再谈调参,顺序反了就是白费电。希望帮到你。

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

返回列表