
简介这是一套面向计算机视觉初学者与工业质检方向开发者的主板质量检测系统源码基于Python与图像识别技术实现可用于学习缺陷检测、目标检测与关键点识别等典型任务的工程落地。资源包共41个文件以34个Python脚本为核心涵盖推理、预处理、后处理与可视化等模块另含2个ui界面文件、2个md说明文档、1个yml配置文件及备份与依赖清单整体约102KB结构紧凑便于快速阅读与二次开发。内容涉及检测、关键点、多目标跟踪等推理流程并集成SAHI、OpenCV、深度学习工具与可视化界面适合作为课程设计、毕业设计或质检项目原型参考。目前已有447人学习下载可帮助读者理解从模型推理到界面展示的完整链路掌握图像识别在主板检测场景中的实现思路与排错方法。1. 从一张主板照片到判定结果这套 Python 图像识别质检系统到底在做什么产线上刚下线的工控主板肉眼盯着看焊点、电容极性、排针歪斜一天两千片人眼根本扛不住。基于 Python 和图像识别的主板质量检测系统解决的就是这件事用工业相机拍下主板交给算法判断有没有缺件、错件、偏移、极性反、焊锡不良这几类典型缺陷。它适合两类人——一类是想把手工目检换成自动判定的产线工程师另一类是手里有 Python 基础、想找一个能跑通的图像识别落地项目练手的开发者。整套系统的骨架其实不复杂图像采集、预处理、特征或模板比对、缺陷判定、结果输出五步走完。真正拉开差距的地方在于你怎么让算法在光照波动、板子轻微位移、不同批次色差的情况下依然稳定地给出判定而不是今天准明天崩。这也是后面几章要重点拆的部分。2. 主板质检的图像识别方案选型模板匹配、特征工程还是深度学习2.1 三种主流路线在主板场景下的真实差异主板质检和一般物体识别不一样它的核心诉求是「比对」而不是「分类」。同一块板子良品和不良品的差异可能只有几个像素——一个 0402 封装的电阻偏了 0.3mm或者一个电容的极性标记转了一百八十度。这种场景下三条技术路线的表现差异很大。模板匹配是最容易上手的路线。你拿一张确认过的良品图当基准新板子拍完对齐后逐区域做像素级或归一化互相关比对差异超过阈值就报警。它的优点是零训练成本、可解释性强、误判了你能直接看到是哪个区域对不上。缺点是它对光照和位移极其敏感板子稍微转一点角度匹配分数就断崖式下跌。所以用这条路你必须先把图像对齐做到位。特征工程路线是在模板匹配基础上往前走一步。你不直接比像素而是提取每个元件区域的几何特征——面积、周长、最小外接矩形长宽比、Hu 矩、颜色直方图——然后对这些特征做判定。比如一个贴片电阻的正常面积范围是 800 到 1200 像素如果检测出来只有 300那大概率是缺件如果面积正常但长宽比从 2:1 变成了 1:1那可能是偏移或者立碑。这条路对光照的鲁棒性比纯模板匹配好但特征阈值需要针对每种元件单独调元件种类多了之后维护成本不低。深度学习路线是近两年越来越多人在主板质检上尝试的方向。常见做法是用目标检测模型比如 YOLO 系列先定位每个元件再用一个分类网络判断该元件是否正常或者直接用分割网络把焊点区域抠出来做像素级分类。它的优势是泛化能力强对光照和轻微形变不敏感缺点是标注数据成本高、训练需要 GPU、模型更新后需要重新验证。对于主板这种元件种类相对固定、缺陷模式可枚举的场景我的经验是如果元件种类在二十种以内、每天检测量不超过五千片模板匹配加特征工程的组合完全够用如果元件种类多、批次切换频繁、缺陷样本积累得快那深度学习的投入才划得来。2.2 用 OpenCV 搭一个最小可跑的模板比对质检流程下面这段代码是一个最小可跑的质检流程用 OpenCV 做图像对齐和模板比对。它假设你已经有一张良品基准图并且相机位置固定。import cv2 import numpy as np def align_images(good_img, test_img): 用 ORB 特征点做图像对齐返回对齐后的测试图 orb cv2.ORB_create(5000) kp1, des1 orb.detectAndCompute(good_img, None) kp2, des2 orb.detectAndCompute(test_img, None) # 用 BFMatcher 做暴力匹配交叉验证筛选 bf cv2.BFMatcher(cv2.NORM_HAMMING, crossCheckTrue) matches bf.match(des1, des2) matches sorted(matches, keylambda x: x.distance) # 取前 15% 的匹配点算单应矩阵 num_good max(10, int(len(matches) * 0.15)) src_pts np.float32([kp1[m.queryIdx].pt for m in matches[:num_good]]).reshape(-1, 1, 2) dst_pts np.float32([kp2[m.trainIdx].pt for m in matches[:num_good]]).reshape(-1, 1, 2) H, mask cv2.findHomography(dst_pts, src_pts, cv2.RANSAC, 5.0) h, w good_img.shape[:2] aligned cv2.warpPerspective(test_img, H, (w, h)) return aligned def detect_defects(good_img, aligned_img, diff_threshold30, area_threshold50): 逐像素比对返回缺陷区域列表 # 转灰度后做绝对差 gray_good cv2.cvtColor(good_img, cv2.COLOR_BGR2GRAY) gray_test cv2.cvtColor(aligned_img, cv2.COLOR_BGR2GRAY) # 高斯模糊降噪避免单像素噪声触发误报 gray_good cv2.GaussianBlur(gray_good, (5, 5), 0) gray_test cv2.GaussianBlur(gray_test, (5, 5), 0) diff cv2.absdiff(gray_good, gray_test) _, thresh cv2.threshold(diff, diff_threshold, 255, cv2.THRESH_BINARY) # 形态学开运算去掉小噪点 kernel np.ones((3, 3), np.uint8) thresh cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations1) contours, _ cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) defects [] for cnt in contours: area cv2.contourArea(cnt) if area area_threshold: x, y, w, h cv2.boundingRect(cnt) defects.append({bbox: (x, y, w, h), area: area}) return defects # 使用示例 good cv2.imread(golden_board.jpg) test cv2.imread(test_board.jpg) aligned align_images(good, test) defects detect_defects(good, aligned) print(f检测到 {len(defects)} 处疑似缺陷) for d in defects: print(f 位置: {d[bbox]}, 面积: {d[area]})这段代码的逻辑分两层。第一层是对齐用 ORB 提取特征点暴力匹配后取距离最小的前 15% 作为可靠匹配对再用 RANSAC 算单应矩阵把测试图 warp 到和良品图同一个坐标系下。这一步是整个流程的命门对齐不准后面全白搭。第二层是比对灰度化、高斯模糊降噪、绝对差、阈值二值化、形态学去噪、找轮廓、按面积过滤。diff_threshold控制像素差异的敏感度设太小会把光照波动当缺陷设太大又漏掉真实缺陷一般从 25 到 40 之间试。area_threshold控制最小缺陷面积低于这个值的轮廓直接丢弃用来过滤灰尘和噪点。2.3 参数怎么调三个必须动手试的阈值上面代码里有三个参数直接决定误报率和漏报率必须拿实际产线的图去试。diff_threshold是最关键的。我的做法是先拿二十张确认良品跑一遍记录每张的最大差异值取这些值的均值加两倍标准差作为初始阈值。然后拿十张确认不良品跑一遍看这个阈值能不能全部检出。如果漏检往下调如果良品误报往上调。这个过程通常要迭代三到四轮。area_threshold取决于你的最小缺陷尺寸要求。如果产线要求能检出 0.5mm 的偏移而你的相机分辨率是每毫米 20 像素那 0.5mm 对应大约 100 像素的面积阈值就设 80 到 100 之间留点余量。num_good匹配点数量影响对齐精度。取太少单应矩阵算不准取太多可能引入错误匹配。15% 是一个经验值但如果你的板子纹理特别少比如大面积空白 PCB可能需要降到 10% 甚至更低同时把 ORB 的特征点数量从 5000 提到 10000。3. 把质检流程工程化从单张测试到批量产线部署3.1 目录结构与批量处理脚本单张图跑通只是第一步产线上你需要批量处理、记录结果、标记缺陷位置。下面是一个可以直接用的批量处理脚本假设你的图片按批次放在data/目录下良品基准图放在golden/目录。import os import cv2 import csv import json from datetime import datetime def batch_inspect(golden_path, test_dir, output_dir, diff_threshold30, area_threshold50): 批量质检主函数 golden cv2.imread(golden_path) if golden is None: raise FileNotFoundError(f良品基准图读取失败: {golden_path}) os.makedirs(output_dir, exist_okTrue) results [] # 支持的图片格式 exts (.jpg, .jpeg, .png, .bmp) test_files [f for f in os.listdir(test_dir) if f.lower().endswith(exts)] for fname in sorted(test_files): fpath os.path.join(test_dir, fname) test_img cv2.imread(fpath) if test_img is None: results.append({file: fname, status: READ_ERROR, defects: 0}) continue try: aligned align_images(golden, test_img) defects detect_defects(golden, aligned, diff_threshold, area_threshold) # 在图上画出缺陷框并保存 vis aligned.copy() for d in defects: x, y, w, h d[bbox] cv2.rectangle(vis, (x, y), (xw, yh), (0, 0, 255), 2) cv2.imwrite(os.path.join(output_dir, fmarked_{fname}), vis) status NG if defects else OK results.append({ file: fname, status: status, defects: len(defects), detail: json.dumps(defects, ensure_asciiFalse) }) except Exception as e: results.append({file: fname, status: PROCESS_ERROR, defects: -1, detail: str(e)}) # 写 CSV 报告 report_path os.path.join(output_dir, freport_{datetime.now():%Y%m%d_%H%M%S}.csv) with open(report_path, w, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnames[file, status, defects, detail]) writer.writeheader() writer.writerows(results) ok_count sum(1 for r in results if r[status] OK) ng_count sum(1 for r in results if r[status] NG) print(f处理完成: 共 {len(results)} 张, OK {ok_count}, NG {ng_count}) return report_path # 调用 batch_inspect(golden/golden_board.jpg, data/batch_001/, output/batch_001/)这个脚本做了几件事遍历测试目录下所有图片、逐张对齐和比对、把缺陷框画在原图上保存、把结果写进带时间戳的 CSV。status字段有三个值——OK 表示无缺陷NG 表示检出缺陷PROCESS_ERROR 表示处理过程中出了异常比如图片损坏、对齐失败。CSV 里的detail字段存了每个缺陷的坐标和面积方便后续追溯。3.2 用配置文件管理多产品线参数产线不会只跑一种板子。不同型号的主板元件布局不同、光照条件不同、缺陷判定标准也不同。把参数硬编码在脚本里换线就得改代码这是工程化的大忌。正确做法是把每种板子的参数抽到配置文件里。import yaml # config.yaml 示例内容 # boards: # board_a: # golden: golden/board_a_golden.jpg # diff_threshold: 30 # area_threshold: 50 # roi: [100, 100, 800, 600] # 只检测这个区域 # board_b: # golden: golden/board_b_golden.jpg # diff_threshold: 35 # area_threshold: 80 # roi: null # 全图检测 def load_config(config_path): with open(config_path, r, encodingutf-8) as f: return yaml.safe_load(f) def inspect_with_config(board_name, test_dir, config_pathconfig.yaml): cfg load_config(config_path) board_cfg cfg[boards][board_name] golden cv2.imread(board_cfg[golden]) # 如果配置了 ROI只裁剪该区域做比对 if board_cfg.get(roi): x, y, w, h board_cfg[roi] golden golden[y:yh, x:xw] # 后续流程同上参数从 board_cfg 读取 # ...用 YAML 管配置的好处是换线时只改配置文件不动代码参数可以版本化管理出问题能回溯不同工程师维护不同板子的参数互不干扰。roi字段特别有用——主板上有些区域本来就有合理差异比如手焊的连接器、丝印批次不同把这些区域排除掉能大幅降低误报。3.3 结果可视化与人工复核界面全自动判定在产线上永远需要人工复核兜底。我的做法是做一个简单的复核界面把 NG 图片和对应的良品基准图并排显示缺陷区域用红框标出复核人员点「确认缺陷」或「误报」两个按钮结果写回数据库。这个界面用 PyQt 或 Tkinter 都能快速搭出来核心逻辑就是把marked_开头的图片加载出来加两个按钮绑定事件。import tkinter as tk from PIL import Image, ImageTk import os class ReviewApp: def __init__(self, output_dir): self.root tk.Tk() self.root.title(质检复核) self.output_dir output_dir self.files [f for f in os.listdir(output_dir) if f.startswith(marked_)] self.idx 0 self.label tk.Label(self.root) self.label.pack() btn_frame tk.Frame(self.root) btn_frame.pack() tk.Button(btn_frame, text确认缺陷, commandlambda: self.judge(True)).pack(sidetk.LEFT) tk.Button(btn_frame, text误报, commandlambda: self.judge(False)).pack(sidetk.LEFT) self.show_current() self.root.mainloop() def show_current(self): if self.idx len(self.files): self.label.config(text复核完成) return img Image.open(os.path.join(self.output_dir, self.files[self.idx])) img img.resize((800, 600)) self.photo ImageTk.PhotoImage(img) self.label.config(imageself.photo) def judge(self, is_defect): # 把复核结果写回 CSV 或数据库 print(f{self.files[self.idx]}: {确认 if is_defect else 误报}) self.idx 1 self.show_current() # ReviewApp(output/batch_001/)这个界面很粗糙但够用。关键是它把「算法判定」和「人工确认」串起来了复核结果还能反过来用来调阈值——如果某个区域连续被标为误报就把那个区域加进 ROI 排除列表。4. 主板质检避坑记录五个让误报率翻倍的常见问题4.1 光照变化导致整板误报现象同一块良品板上午拍的和下午拍的比对整板都是红色缺陷框。原因自然光或车间照明随时间变化相机自动曝光跟着变导致两张图整体亮度不同。解决固定光源和曝光参数相机用锁定曝光模式如果做不到在比对前加一步直方图均衡化或自适应亮度归一化把两张图的均值方差拉到同一水平。4.2 板子放置角度偏差导致对齐失败现象对齐后的图明显歪了缺陷框全在板子边缘。原因ORB 特征点匹配在板子旋转角度超过 15 度时容易匹配错误单应矩阵算出来是错的。解决在工装上加机械定位销把板子放置角度控制在 5 度以内如果做不到先用轮廓检测找到板子外框做粗对齐再用 ORB 做精对齐。4.3 元件批次色差触发误报现象换了新批次的电容颜色比上一批深一点整个电容区域被标为缺陷。原因模板比对是像素级的颜色差异直接反映为像素差。解决对颜色敏感的区域改用灰度比对或者只比对梯度特征或者在 ROI 配置里把这类元件区域排除单独用特征工程判定。4.4 阈值设太紧导致漏检微小缺陷现象产线反馈有偏移的电阻没被检出。原因area_threshold设太大偏移产生的差异区域面积小于阈值被过滤掉了。解决把area_threshold降到能覆盖最小缺陷的面积同时把diff_threshold适当调低用形态学操作控制噪点。这两个参数要联动调不能单独改一个。4.5 图片读取失败导致整批中断现象批量处理跑到一半报错退出后面的图全没处理。原因某张图片损坏或格式不支持cv2.imread返回 None后续操作抛异常。解决在循环里对每张图做 try-except读取失败就记录错误状态继续跑不要让单张图的异常中断整批。上面批量脚本里的PROCESS_ERROR状态就是干这个的。5. 让质检系统越跑越准用复核数据反哺阈值和 ROI系统上线只是开始真正决定它能不能长期用的是「越跑越准」的能力。我一般会做两件事。第一件是定期统计误报和漏报的分布。每周把复核结果拉出来按缺陷区域坐标做聚类看看误报集中在哪些位置。如果某个区域连续三周都有误报那基本可以确定是合理差异区域直接加进 ROI 排除列表。如果某个区域有漏报就单独把那个区域的diff_threshold调低做区域化阈值。第二件是积累缺陷样本为后续升级到深度学习做准备。每次复核确认的缺陷图按缺陷类型分类存好——缺件、偏移、极性反、焊锡不良各一个文件夹。积累到每类两百张以上就可以训练一个轻量分类网络来替代或补充模板比对。迁移到深度学习的路径是先用模板比对跑三个月积累标注数据再用这些数据训练模型最后把模型和模板比对做融合判定——两者都判 NG 才报 NG降低误报。# 按缺陷类型归档复核确认的样本 import shutil import os def archive_defect(img_path, defect_type, archive_rootdefect_samples): 把确认的缺陷图按类型归档 valid_types [missing, offset, polarity, solder] if defect_type not in valid_types: raise ValueError(f未知缺陷类型: {defect_type}) target_dir os.path.join(archive_root, defect_type) os.makedirs(target_dir, exist_okTrue) fname os.path.basename(img_path) target_path os.path.join(target_dir, fname) shutil.copy2(img_path, target_path) print(f已归档: {target_path}) # 在复核界面里确认缺陷时调用 # archive_defect(output/batch_001/marked_board_023.jpg, offset)这个归档脚本很简单但坚持做三个月你手里就有一套带标注的主板缺陷数据集。这套数据的价值远超过系统本身——它是你后续做模型迭代、做缺陷根因分析、做产线良率改进的基础。我自己的习惯是每周五下午花半小时过一遍这周的复核记录把误报区域更新到 ROI 配置里把确认的缺陷样本归档。这个习惯坚持了半年之后系统的误报率从最初的 15% 降到了 3% 以下产线工人从「每张都要看一眼」变成了「只看 NG 的」。质检系统不是一次调好就完事的它需要你持续喂数据、调参数、修边界。希望帮到你。本文还有配套的精品资源点击获取