简介基于Python与Faster-RCNN的PCB元器件缺陷检测项目面向毕业设计、课程设计及项目开发场景尤其适合具备一定深度学习基础、希望掌握目标检测全流程的开发者。项目以VOC07格式数据为依托完整覆盖数据预处理、模型训练、预测与评估环节并包含开发文档与项目解析方便二次延展。资源包共79个文件以35个Python脚本和39个pyc编译文件为主体辅以README等Markdown说明文档及依赖清单整体仅214KB轻量易部署目录结构清晰便于按模块查阅。目前已有379人学习下载。除了可直接运行的检测源码还提供训练自有数据集的步骤、预训练权重使用方法以及评估脚本能够帮助读者快速跑通PCB元器件缺陷检测流程理解Faster-RCNN在工业质检场景中的落地思路。1. PCB元器件缺陷检测拿到faster-RCNN标题后第一步该做什么课程设计或毕业设计里拿到“基于pythonfaster-RCNN的PCB元器件缺陷检测”这个题目意味着你手头大概会有一套源码、一份开发文档和一个项目解析PPT。你的任务往往不只是把代码跑通而是把别人写好的工程变成能讲清楚、能改数据、能扛住答辩追问的东西。PCB板上元器件的缺陷比如缺件、偏移、桥连、立碑靠人工盯产线又慢又容易漏这个项目的价值就是搭一条检测管线代替人眼做初检。这篇文章不打算逐行复述源码而是讲清楚三件事为什么选faster-RCNN、数据怎么准备、哪几个坑最耽误时间。适合python刚入门、第一次碰目标检测的课设党也适合想快速评估这个方案值不值得投入的工程师。2. 选型分析faster-RCNN为什么比单阶段模型更适合PCB小缺陷2.1 两阶段设计对PCB小目标更友好faster-RCNN的检测流程是backbone提取特征RPN生成候选框ROI Pooling把候选框统一成固定尺寸最后的分类和回归头同时输出类别和框坐标。这个两阶段结构在PCB检测里很占便宜因为候选框先由RPN粗筛一遍再由分类头细判等于给“漏检”上了两道保险。而YOLO这类单阶段检测器直接在特征图上回归框和类别速度快但遇到密集排列的元件和小缺陷召回率往往要靠多尺度训练和更精细的anchor设计才能拉回来。PCB元器件检测有个特殊性板子大、元件小、密度高、缺陷更小。一片板子常见的尺寸是几千像素宽而一个电容或者一颗电阻在图像里可能只占几十个像素。这正好落在两阶段检测器的优势区间。如果你做的不是产线实时检测推理速度要求没那么苛刻faster-RCNN就是更稳妥的选择踩坑成本也更低。对比项faster-RCNN两阶段YOLO系列单阶段检测精度高小目标召回更好中高依赖多尺度设计推理速度慢几帧到十几帧快轻松数十帧以上原理复杂度RPN ROI Pooling环节多端到端单网络结构直观小目标/密集场景更适合需要额外tricks才能追平答辩和文档友好度原理经典、好扩展版本迭代快要追最新论文另外一点容易被忽略两阶段结构是个“白匣子”每一步都能拿出来讲道理。答辩时老师问“为什么用RPN而不是直接回归”你可以把候选框生成的逻辑讲清楚问“为什么不用YOLO”你可以用速度和精度的权衡来回答。相比之下用YOLO做课设很容易被问“你改进了什么”而用faster-RCNN本身就能讲出一套完整的系统设计。2.2 工程目录怎么摆拿到源码后先认路我见过的大部分课设源码目录结构都差不多。拿到任一“基于pythonfaster-RCNN的PCB元器件缺陷检测”工程包先别急着跑按下面这张图把它认一遍。理解了目录你才知道训练脚本读的是哪个文件夹、权重存在哪、配置在哪里改。pcb-defect-detection/ ├── configs/ # 训练和推理的参数配置 ├── data/ # 图像、标注、划分好的train/val列表 │ ├── annotations/ # 标注文件VOC XML或COCO json │ ├── images/ # 原始PCB图像 │ └── ImageSets/ # train.txt / val.txt ├── src/ 或 lib/ # 模型代码backbone、RPN、ROI Head ├── tools/ 或根目录 # train.py / test.py / demo.py ├── utils/ # 数据加载、可视化、评测工具 ├── weights/ # 训练好的.pth权重 └── docs/ # 开发文档、项目解析PPT这里每个文件夹都有它的职责。configs是命门类别数、学习率、anchor参数全在里面后面要反复改data目录决定了模型吃什么数据标注和图像必须严格对得上weights目录里通常放着一个预训练好的权重第一次跑通就用它做推理验证。拿到源码后的第一个动作不是运行train.py而是看两样东西requirements.txt和README。requirements里写了依赖库列表README里写了启动命令和数据集格式。如果开发文档没写清楚环境怎么装就以这两个文件为准。第二个动作是把weights里已有的权重加载起来用自带的sample图片跑一次推理确认整个链路是通的再考虑重新训练。这个顺序能帮你把“代码问题”和“数据问题”分开排查。2.3 环境安装python版本与CUDA别再互相打架环境问题劝退过很多人。faster-RCNN的课设源码大多是两三年甚至更早之前写的依赖的pytorch或tensorflow版本比较旧。直接pip install -r requirements.txt大概率会遇到版本冲突。我的习惯是用conda单独建环境把python版本固定再装对应cuda的pytorch最后装其他依赖。conda create -n pcb python3.9 -y conda activate pcb # 先装pytorch确认cuda可用再装其他依赖 conda install pytorch torchvision pytorch-cuda11.8 -c pytorch -c nvidia pip install -r requirements.txt这里有个顺序问题如果requirements.txt里已经有torch的版本号就以它为准前面的conda安装语句可以跳过或者只留python版本。先装pytorch再装其他依赖是为了避免pip在装依赖时顺手把torch升级成不兼容的版本。装完后用python -c import torch;print(torch.version)和print(torch.cuda.is_available())各验证一次前者看版本后者看GPU有没有接上。对python入门阶段的同学来说环境冲突看起来像玄学其实绝大多数是版本组合不匹配。我的原则是不要用最新版本去跑老源码除非你做好了改代码的准备优先复现作者标注过的版本组合。如果机器上确实没有GPUCPU也能训练但PCB原图往往很大速度会慢到让人怀疑人生。这种时候先把图片长边缩到1200像素以内用小数据集跑一遍流程至少能确认代码没问题。3. 数据到训练从labelImg标注到跑通第一个epoch3.1 标注工具与类别清单先定类别再动手数据准备的第一步不是打开标注工具而是定类别清单。PCB元器件缺陷检测的常见问题包括缺件、偏移、立碑、桥连、错件、虚焊每一项都是一个潜在类别。课设场景下有两种做法一种是细分成多类每类缺陷单独一个类别另一种是只设一个“defect”类别把所有缺陷框出来。我建议你在动手标注前先做一个决定如果源码自带了匹配的PCB缺陷数据集并且开发文档里写明了类别列表就沿用原来的类别。如果要用自己的板子图先按“缺件、偏移、立碑、桥连”这几类最常见的缺陷分或者干脆先只标一个类。类别少数据量要求低训练稳定性高类别多答辩时内容好看但每个类别至少要积累几百个有效框否则分类头根本学不出来。标注工具用labelImg最省事。它支持PascalVOC格式保存出来是XML文件每个XML对应一张图里面记录着所有目标的类别名和坐标框。注意打开图片后要把保存格式切成PascalVOC默认可能是YOLO格式。PCB原图尺寸大的时候标注界面里把图片缩放显示不影响坐标保存框选时软件记录的是原图坐标这一点不用担心。真正需要注意的是类的拼写错一个字母后面训练脚本就会告诉你“找不到这个类”。3.2 清洗与格式转换把脏标注挡在训练之前标注完先别急着训练。我一般会写一个脚本把标注目录扫一遍统计每个类别的数量、找出没有目标的XML、顺手把标注格式转换成模型需要的格式。这一步叫数据清洗它能拦下大量因为“手滑”产生的训练事故。import xml.etree.ElementTree as ET import os, csv annot_dir data/annotations output_csv data/annotations_stats.csv all_boxes [] class_counter {} empty_xmls [] for xml_name in os.listdir(annot_dir): if not xml_name.endswith(.xml): continue xml_path os.path.join(annot_dir, xml_name) tree ET.parse(xml_path) root tree.getroot() objects root.findall(object) if len(objects) 0: empty_xmls.append(xml_name) continue for obj in objects: cls_name obj.findtext(name).strip() bndbox obj.find(bndbox) x1 int(float(bndbox.findtext(xmin))) y1 int(float(bndbox.findtext(ymin))) x2 int(float(bndbox.findtext(xmax))) y2 int(float(bndbox.findtext(ymax))) class_counter[cls_name] class_counter.get(cls_name, 0) 1 all_boxes.append([xml_name, cls_name, x1, y1, x2, y2]) with open(output_csv, w, newline) as f: writer csv.writer(f) writer.writerow([image, class, x1, y1, x2, y2]) writer.writerows(all_boxes) print(类别统计:, class_counter) print(空标注XML数量:, len(empty_xmls), empty_xmls[:10])这段脚本做的事情很直接遍历XML目录解析出每个目标的类别和坐标写入CSV同时打印类别分布和空标注文件。其中class_counter能让你一眼看出数据平不平衡如果“缺件”有800个框而“立碑”只有20个后期模型大概率会偏向多数类empty_xmls是排雷重点某些图片文件正常但标注为空这种XML如果流进训练集会让模型接受“这张图没有目标”的信号浪费训练时间。坐标转换也是在这里完成的。很多faster-RCNN工程读的是VOC XML本身但有的工程会要求一个单独的txt或json文件每行存“类名 x1 y1 x2 y2”。上面脚本输出的CSV已经够你手工做转换了。格式转换这一步没有统一标准以你拿到的train.py里数据加载代码为准参考它的要求改输出即可。常见做法是先在清洗脚本里把XML转成统一的中间格式再在数据加载脚本里做最后的适配。3.3 数据集划分、增强与三个先调参数清洗完成之后是数据集划分。这里我吃过一次亏提醒你特别注意划分train/val的时候按“板子”分不要按“图片”分。如果同一个原始大图被切成多个patch切出来的子图又同时出现在train和val里评测时会虚高得离谱视觉上却依然漏检严重。正确做法是先把所有原始板子图编号按板子比例划分再从每块板子的patch里分配train/val。这点在第4章还会再展开。数据增强方面PCB检测适合温和的增强。水平翻转、小幅随机亮度、对比度和轻微旋转就够用你会发现它们不会破坏元件的外观和位置关系。不要用那种会把图像扭曲得很厉害的强增强比如大角度旋转或随机裁剪特别狠PCB板上的丝印、铜走线和焊盘方向性很强过度扭曲会让模型学到错误先验。第一次训练请先跑一个小数据集比如每类50个框、跑几个epoch确认loss下降、验证能出框再上全量数据。训练命令一般是这样的python train.py --config configs/pcb_faster_rcnn.yaml \ --batch_size 4 \ --lr 0.0001 \ --epochs 60 \ --workers 4这里面的三个参数是优先要调的。学习率lr先给1e-4ResNet50这类backbone用这个值起步比较稳如果loss不降或者降得太慢可以降到3e-5如果loss震荡得厉害也先降lr。batch_size按显存来能堆多大堆多大但不要为了塞进大batch把图像缩到面目全非。epochs先给60轮同时打开日志里的验证集mAP曲线或者自己打印每个epoch的val lossmAP连续10个epoch不再涨就可以停。参数建议初始值判断依据与调整方向学习率 lr1e-4loss震荡则降低到3e-5~5e-5收敛太慢可轻微上调batch_size4 起步显存不够就减小batch越小lr也要相应调低epochs60看val mAP曲线连续10轮不涨就提前停4. 训练与调参避坑四个让我返工两天的问题训练不顺时绝大多数情况不是模型代码有问题而是数据和参数在打架。下面四个问题是我自己返工过的案例按“现象 → 原因 → 解决”写希望能帮你少走两天弯路。4.1 loss变成NaN或纹丝不动现象是训练跑到某一步日志里打印的loss变成nan或者从第一个epoch开始loss就一直是0.05、0.08这种小数点后两位稳定不动的数值。nan最常见的原因是学习率过大梯度爆炸直接把权重冲成了无效数字。特别是用预训练weight继续训练时整体lr可以沿用1e-4但如果源码里默认给的是1e-2或者1e-3这种明显偏大的值先降下来再跑。第二个原因是标注数据里有非法坐标比如xmax小于xmin或者某个框的宽高是0。RPN在计算anchor匹配时会除到这些零值一除就nan。第三个原因是图像预处理顺序错了有的源码要求输入图像归一化到[0, 1]再减去均值如果你拿着[0, 255]的图直接喂进去某些层的输出会溢出。解决方法是按顺序排查先看lr降到1e-4再试再跑一遍第3章的清洗脚本过滤掉空XML和非法坐标最后检查数据加载代码里的normalize和to_tensor顺序。这个问题看起来玄学其实90%是lr和标注数据的问题。4.2 推理结果全空或没有目标现象很打击人训练跑完了验证集mAP看着还行但拿一张新的PCB图去推理输出结果是零个框或者只输出score特别低的框一张图一个目标都检不出来。我排查过三个原因。第一个是类别数设置错误。faster-RCNN的分类头在训练时占一个背景类如果源码的configs里num_classes设置成缺陷类别数而不是缺陷类别数1推理时模型会把所有候选框都当作背景自然不会输出任何目标。第二个是score阈值设得太高。很多源码默认阈值是0.7甚至0.9这是针对通用目标检测数据集的设置PCB小目标本身置信度就偏低0.7会把大量真目标滤掉。第三步才是调整anchor尺寸如果目标在图中占比很小而anchor用的是适合大目标的8/16/32像素基础尺寸建议把基础尺寸改成4/8/16或者增加更小尺度的anchor。这三个检查点要按顺序来。先确认num_classes是N1再把推理脚本里的score阈值降到0.3看看最后再动anchor。4.3 CUDA OOM一张图就把显存吃光了现象是训练脚本一把数据load进来就报CUDA out of memory或者跑了几个epoch之后突然OOM。PCB原始图像的尺寸通常很大一张几千乘几千像素的图直接喂给faster-RCNNRPN要对整张图的特征图生成候选框显存消耗瞬间爆表。解决思路有三个方向。第一是限制输入尺寸在数据加载时把图像长边缩放到一个固定值比如1333这是检测任务很常见的输入约定配合短边限制在800左右显存压力会小很多。第二是减小batch_size从8减到4再不行就减到2同时把lr同步调小因为batch变小后梯度噪声变大。第三是用梯度累积模拟更大的batchoptimizer.zero_grad() loss.backward() if (step 1) % accumulate_steps 0: optimizer.step() optimizer.zero_grad()这段代码的意思是每accumulate_steps个batch更新一次权重等效于把batch_size放大了accumulate_steps倍但显存占用只有原来单个batch的大小。注意手动zero_grad和step的组合如果前一步已经step了后一步又step就会出现梯度重复累加的问题我一般把accumulate_steps设为2或4。如果显存问题依然无解那就走最后一条路把原始大图切成patch来训练让模型每次只学图片的局部推理时也按patch预测再把结果合并回原图坐标。4.4 mAP虚高但可视化漏检最让人困惑的现象是日志里val mAP 0.9但把预测结果画到图上漏检的元件一眼就能看出来。这说明评测结果失真了模型实际能力并没有指标显示的那么好。我遇到过的第一个原因是数据划分泄漏也就是同一张板子切片同时进了train和val。PCB大图切patch时如果划分前没有先按板子分组随机打乱会让同一块板子的两个patch一个在训练集一个在验证集模型相当于直接背下了答案。第二个原因是评估时误开了数据增强如果验证流程里也做了随机翻转或亮度扰动输出的置信度本身就是波动的评测分数虚高。第三个原因是测试集和训练集太像比如同一批次PCB板的光照、角度和摆放位置几乎不变模型学习的其实是背景模式而不是缺陷特征。解决办法也很明确。划分时按板子编号分组保证同一块板的图只出现在一个集合里验证和推理时关闭所有随机增强最后每次训练完强制保存一批可视化结果图人工翻一遍。mAP只是参考真实能力要看画出来的框是否出现在该出现的位置。5. 验证与导出mAP之外把模型接到答辩现场5.1 导出TorchScript或ONNX脱离训练代码运行训练完成后我习惯把模型导出成TorchScript或ONNX这样推理时不需要再加载完整训练工程。答辩演示时你只需要一个权重文件和小段推理脚本不用现场跑训练代码也避开了“换个机器环境就崩”的尴尬。用TorchScript导出很简单import torch model torch.load(weights/pcb_faster_rcnn.pth, map_locationcpu) model.eval() dummy_input torch.zeros((1, 3, 1333, 800)) traced_model torch.jit.trace(model, dummy_input) traced_model.save(weights/pcb_faster_rcnn_traced.pt)代码里dummy_input的尺寸要和训练时的输入尺寸一致。trace模式会把这个尺寸当作固定输入如果训练脚本允许动态尺寸导出后推理时也尽量保持同一尺寸避免形状不匹配。导出前的model.eval()是必写的它把dropout和batch norm切到推理模式不写的话导出的模型行为会不稳定。CPU机器上跑这张图通常需要一两秒对演示来说完全够用。如果你的源码封装比较复杂torch.jit.trace可能会告警或断掉备选方案是转ONNXonnx.export的流程和trace类似但要注意ROI Pooling这类自定义层可能需要额外算子支持。5.2 把预测结果落盘用眼睛验收一遍每次训练完不管mAP多漂亮我都会写一小段脚本从val集里随机取几十张图把预测结果画出来保存到results目录然后人眼过一遍。这个习惯救过我多次指标可以骗人画出来的框不会。import cv2, torch from src.model import get_model model get_model(num_classes6) model.load_state_dict(torch.load(weights/best.pth)[model]) model.eval() img cv2.imread(data/images/board_001.jpg) img_tensor transform(img).unsqueeze(0) with torch.no_grad(): outputs model(img_tensor) boxes outputs[0][boxes].cpu().numpy() scores outputs[0][scores].cpu().numpy() labels outputs[0][labels].cpu().numpy() for box, score, label in zip(boxes, scores, labels): if score 0.3: continue x1, y1, x2, y2 [int(v) for v in box] cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, f{label}:{score:.2f}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(results/board_001_pred.jpg, img)这段脚本里的score阈值在这里同样设为0.3目的是在人工观察阶段尽可能多暴露候选框漏检比误检更能反映模型薄弱点。输出的图片里除了框和置信度建议把类别名也打印出来检查类别误判。如果画出来的多数框都压准了位置再谈上线或写文档如果连肉眼这关都过不了优先回头检查数据划分和增强策略而不是继续堆epochs。项目解析环节老师常问的问题不外乎“为什么选这个backbone”“阈值怎么确定”“模型在哪类缺陷上效果最差”。现在你可以从自己保存的可视化结果里直接找出几张典型图片把“score阈值设定依据”和“在桥连缺陷上的误检”讲成两个具体案例比背论文里的套话更有说服力。这也是我做课设以来最实在的一条经验先保存可视化再写文档文档里所有结论都从图里来。希望帮到你。本文还有配套的精品资源点击获取
