简介:基于Python-OpenCV开发的铭牌印刷缺陷视觉检测系统源码,面向机器视觉入门者、工业质检与自动化相关开发者,用于解决文字模糊、色彩不均、印刷错位、表面划痕等常见缺陷的自动识别问题。源码共22个文件,压缩包约36.28MB,以8个Python脚本为核心,覆盖图像增强、霍夫变换、轮廓检测、仿射变换、KNN分类等模块,并配有标准图、待测图以及jpeg/bmp/png多格式样本图片,方便直接运行对比;另附思路说明txt和项目说明MD,帮助理解检测流程与算法思路。资源目前已有104人学习下载。借助这套源码,读者可以快速搭建完整的缺陷检测流程,掌握基于OpenCV的预处理、特征提取与分类判别方法,并可扩展至其他印刷品或零部件外观质检任务。代码中还涉及缺陷信息的记录与结果可视化思路,兼具较高的学习与工程复用价值。
1. 铭牌印刷缺陷检测:用 OpenCV 搭一套能上线的视觉方案
做视觉检测的常见需求,铭牌上的型号、序列号、LOGO一旦缺笔画、脏污、划痕,到了客户手里就是批量退货。人工目检在强光下盯一天,漏检率并不低,而且人的标准容易漂移。用python-opencv写了一套传统视觉检测系统,不依赖深度学习硬件,一台普通工控机就能跑。它能识别缺墨、飞墨、脏污、划痕、套印偏移这几类常见的铭牌印刷缺陷,并且把缺陷图自动存盘。这份源码适合正在做产线视觉方案、或者想用OpenCV入门缺陷检测的工程师,整个算法链路是完整的,改改参数就能接到自己的工位上,不需要从零开始造轮子。
2. 系统设计与检测流水线:从相机到缺陷分类的完整流程
铭牌检测系统看起来简单,真正跑起来要兼顾稳定性和速度。这一章先说清楚系统建立在什么硬件基础上,然后给一条可执行的主流程。源码不是把所有逻辑堆在一个文件里,而是分成采集、定位、检测、输出四个模块,这样现场调试时能单独替换某一段。
2.1 硬件假设与图像输入约定
系统默认用USB相机或者GigE相机,分辨率按1280x720设计,光源用环形LED或同轴光。铭牌是金属材质,反光强,所以要尽量固定曝光和增益,不要开自动曝光,否则亮度一会高一会低,算法阈值会很纠结。源码里输入统一按BGR三通道处理,如果你拿到的是灰度图,也要先转成三通道再进流程。这样做的好处是,后面所有模块不需要关心相机型号,只要保证图像尺寸和通道一致。
2.2 检测流水线各模块的职责划分
整个流程可以分成五个环节:图像采集、预处理、定位校正、缺陷检测、结果输出。采集模块负责读帧和丢弃模糊帧;预处理负责去噪和光照归一化;定位校正负责找到铭牌四角并做透视变换;缺陷检测负责差影、连通域分析;结果输出负责画框、存盘、统计日志。每个环节的失败处理也不一样,比如定位不到铭牌时,宁可把原始图存下来人工复核,也不要继续往下走,否则会把背景全当成缺陷。
主循环框架代码如下:
cap = cv2.VideoCapture(0) while True: ok, frame = cap.read() if not ok: continue # 预处理:去噪、增强 gray = preprocess(frame) # 定位铭牌区域,得到透视变换矩阵 rect = locate_plate(gray) if rect is None: save_for_manual_check(frame) continue # 透视校正,得到标准正视图 plate = perspective_correct(frame, rect) # 缺陷检测:返回缺陷列表和标注图 defects, marked = detect_defects(plate) # 输出:显示、保存缺陷样本、统计 handle_result(frame, defects, marked)逻辑说明:cap用OpenCV的VideoCapture读取,如果读取失败直接跳过;preprocess拿到灰度图,locate_plate返回铭牌四个角点,找不到就人工复核;perspective_correct把铭牌抠成标准矩形;detect_defects返回缺陷轮廓和标注图;handle_result根据是否有缺陷决定保存方式。这套流程不涉及深度学习,每个函数都能单独跑单元测试。
参数说明:VideoCapture(0)是默认相机索引,多相机时改成0、1、2;preprocess的高斯核大小默认(5,5),这个值直接影响后面差分的噪声水平。如果图像分辨率更高,比如1920x1080,建议把预处理核相应放大。
2.3 模板与检测区域的标定方法
差影法需要一张标准模板图。源码里提供了一个calibrate.py,运行后会弹出画面,让你手动框选铭牌的四角和每个待检测的ROI,最后生成一个json标定文件。运行时locate_plate先做边缘检测找四边形,然后和json里的角点计算单应矩阵。如果现场工位固定,也可以直接跳过定位,用json里的固定裁剪框,但震动稍微大一点就会裁偏,所以我还是推荐保留定位步骤。
标定文件里几个关键字段如下:
| 字段 | 含义 | 格式/单位 |
|---|---|---|
| corners | 铭牌四角像素坐标 | 4x2 list |
| regions | 待检测ROI列表 | list of [x, y, w, h] |
| template_img | 模板图路径 | string |
| exposure | 建议曝光时间 | 微秒 |
字段说明:corners是手动标定的四角,locate_plate找到的角点要和它算单应矩阵,所以顺序必须一致;regions可以包含字符区、logo区、空白区,不同区域使用不同的检测参数,这一点后面会讲到;exposure只是记在文件里提示现场用,不会主动改相机参数。
3. 核心实现:预处理、字符分割与缺陷判据
这一章深入到代码层面。铭牌的印刷缺陷主要有缺墨、飞墨、脏污、划痕、套印偏移。把这些缺陷统一成"当前图和标准模板的差异",就可以用差影法检测。难点在于金属反光导致差异图里全是噪点,所以预处理和判据要下功夫。
3.1 图像预处理:光照矫正与双线性滤波
金属铭牌反光严重,同一字符在不同角度亮度差很大。如果直接对原图做差分,光照梯度会完全淹没印刷缺陷。源码里先转灰度,再做高斯滤波,然后用一个关键的步骤:用形态学闭运算估计背景亮度,再用原图除以背景,消除不均匀光照。
def preprocess(gray): blur = cv2.GaussianBlur(gray, (5,5), 0) # 用大核闭运算估计背景照度 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (31,31)) bg = cv2.morphologyEx(blur, cv2.MORPH_CLOSE, kernel) # 背景归一化:原图与背景之比放大到0-255 normalized = cv2.divide(blur, bg, scale=255) return normalized逻辑说明:闭运算是先膨胀后腐蚀,能在局部区域内取到最亮值,因为铭牌背景通常比笔画亮,所以闭运算结果就是背景照度的估计。cv2.divide做逐像素除法,把亮度归一化到0-255,这样图像上半边的亮区和下半边的暗区会拉到一个水平线上。这是传统预处理里对抗反光比较有效的一招。
参数说明:高斯核(5,5)去掉随机噪点,核太大会把笔画边缘磨平,导致小缺陷漏检。背景估计的椭圆核(31,31)是经验值,它必须大于单个字符的笔画宽度,比如笔画宽度是10像素,那这个核至少要到21;但又不能太大,否则会吞掉大面积的局部阴影。现场需要针对自己的铭牌做一组核尺寸的实验,可以用调试模式看归一化效果。
3.2 定位铭牌区域与透视校正
定位的目标是找到铭牌四角,然后透视校正成标准正视图。源码里用Canny边缘检测加轮廓筛选来定位。
def locate_plate(gray): edges = cv2.Canny(gray, 50, 150) contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours = sorted(contours, key=cv2.contourArea, reverse=True)[:10] for cnt in contours: peri = cv2.arcLength(cnt, True) approx = cv2.approxPolyDP(cnt, 0.02 * peri, True) if len(approx) == 4: return approx.reshape(4,2).astype(np.float32) return None逻辑说明:先用Canny得边缘,再抽外部轮廓;按面积排序取前10个,因为铭牌在画面中通常是面积最大的四边形。approxPolyDP把轮廓近似成多边形,如果拟合出来正好4个点,就认为是铭牌角点。得到角点后,还需要和模板角点算单应矩阵,这一步在perspective_correct里做。
参数说明:Canny阈值50和150是低阈值和高阈值,低阈值以下不算边缘,高阈值以上的算强边缘。如果铭牌周围有复杂的背景纹理,需要提高低阈值;如果铭牌本身边缘太浅,要降低。0.02是approxPolyDP的近似精度,值越大,多边形角点越少,但可能丢掉真实角点;值太小,边缘上的锯齿会被当成角点,所以0.02是一个平衡点。
3.3 缺陷检测:差分、连通域与阈值判据
拿到透视校正后的标准图,下一步就是和模板图差分。源码里detect_defects函数是核心,它输出缺陷轮廓列表和标注图。
def detect_defects(plate): # plate是透视校正后的灰度图 diff = cv2.absdiff(plate, template_plate) _, thresh = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3)) thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel) contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) defects = [] for cnt in contours: area = cv2.contourArea(cnt) if area >= min_area and area <= max_area: defects.append(cnt) # 标注结果 marked = plate.copy() cv2.drawContours(marked, defects, -1, (0,0,255), 2) return defects, marked逻辑说明:absdiff是逐像素取亮度差,阈值30把差异大于30的像素置为白色,其余为黑。白点里既有缺陷也有噪点,开运算用3x3矩形核去掉孤立噪点。然后对白色区域做连通域分析,计算每个轮廓面积,面积在[min_area, max_area]之间的才认为是真缺陷。面积小于min_area的是噪点,大于max_area的一般是大面积脏污或者反光带,需要单独处理。
参数说明:阈值30是相对值,它对光照归一化后的图像比较稳定;但如果换了一组铭牌,需要重跑离线统计。min_area和max_area是缺陷判据的核心参数,建议按ROI分组设置。比如字符区域的小划痕面积可能只有15像素,而logo区域的墨迹面积会到2000像素,一个全局面积范围很难同时覆盖,所以源码里允许每个ROI单独配参数。
3.4 输出结果与保存缺陷样本
检测结果不能只是屏幕上画个框,还要能回溯。源码里每个被判定为NG的工位,都会把原图、标注图、缺陷裁剪图按时间戳存下来,同时把缺陷数量、总面积追加到csv日志里。
def handle_result(frame, defects, marked): if defects: ts = time.strftime("%Y%m%d_%H%M%S") cv2.imwrite(f"logs/{ts}_marked.jpg", marked) for i, cnt in enumerate(defects): x,y,w,h = cv2.boundingRect(cnt) crop = marked[y:y+h, x:x+w] cv2.imwrite(f"logs/{ts}_defect_{i}.jpg", crop) with open("logs/defects.csv", "a") as f: f.write(f"{ts},{len(defects)},{sum(cv2.contourArea(c) for c in defects)}\n")逻辑说明:时间戳精确到秒,但同一秒可能有多个工件,所以更稳妥的做法是加一个自增序号,源码里用的是时间戳加雪花ID,这里简化为时间戳。crop保存的是标注后的彩色图,方便直接看缺陷形状。csv日志能统计每天的缺陷率,还能按时间追溯产线哪一批料出了问题。注意保存路径要提前建好,否则imwrite会失败。
4. 参数调优与性能优化:让检测速度从 500ms 降到 80ms
4.1 关键参数表与推荐起始值
源码默认给了一组在1280x720下能跑通的参数。我把它们整理成表,现场改参数时可以对照着来。
| 参数 | 推荐起始值 | 影响 | 调参方向 |
|---|---|---|---|
| GaussianBlur kernel | (5,5) | 去噪,太大会丢边缘 | 噪点多时稍加大 |
| 背景估计核 | (31,31) 椭圆 | 越大越忽略局部细节 | 反光强时加大 |
| Canny阈值 | 50/150 | 太低引入纹理,太高漏边 | 背景复杂时调高 |
| 差分阈值 | 30 | 太低误检,太高漏检 | 缺陷浅时降低 |
| min_area | 20 | 小于该面积的忽略 | 污点噪声多时调高 |
| max_area | 5000 | 大片脏污不判缺陷 | 灰度污染严重时调低 |
说明:这些值是起点,不要当金科玉律。我一般会把现场采集到的正常件和NG件各留50张,离线调好参数再上在线。特别是min_area,很多新手喜欢设得很小,结果把相机噪点都当缺陷,一天要停十次线。
4.2 加速技巧:ROI 裁剪、图像金字塔、多线程
先说结论:如果所有步骤都在全图上做,1280x720的图能跑到500ms已经很不错。但要满足工厂节拍,必须做三件事:ROI裁剪、图像金字塔定位、预计算模板背景。
ROI裁剪的意思是,铭牌位置固定后,我们只处理包含铭牌的那一小块区域,而不是全图。图像金字塔用于定位阶段:把图缩小一半,在小图上找角点,找到后把坐标放大两倍回到原图。预计算模板背景是指,模板图的背景照度、差分阈值都提前算好,在线不再重复计算。
if fast_mode: small = cv2.resize(gray, (0,0), fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA) rect = locate_plate(small) if rect is not None: rect = rect * 2 # 坐标放大回原图 plate = perspective_correct(gray, rect) else: plate = perspective_correct(gray, rect)逻辑说明:fast_mode开关在配置文件里,打开后先用半分辨率图定位,定位成功再把角点坐标乘以2。注意透视校正还是要用原分辨率图做,因为角点亚像素偏差放大后可能引入误差,所以校正之前可以再做一次亚像素优化。这样做以后,定位耗时基本可以忽略。
参数说明:缩小时用INTER_AREA插值,适合缩小场景;如果改成放大,要用INTER_CUBIC。坐标放大倍数必须和缩小比例严格对应,否则定位框会偏移。实际运行还可以把高分辨率图下采样到640x360定位,效果差不多,速度再快一截。
多线程方面,建议把采集放在单独线程,主线程只处理最新一帧。用queue.Queue(1),队列满就丢弃旧帧,保证检测模块拿到的永远是相机抓到的最新一帧,而不是积压的旧数据。
性能对比表:
| 方案 | 耗时(1280x720) | 适用场景 |
|---|---|---|
| 全图+实时背景估计 | 约500ms | 离线调试 |
| ROI+预计算背景 | 约150ms | 一般产线 |
| ROI+图像金字塔定位 | 约80ms | 高速产线 |
5. 避坑指南:铭牌反光、字符断裂、过检漏检的 5 条血泪经验
5.1 反光导致的误检
现象:检测时金属铭牌表面产生斜向亮斑,差分后出现一大块伪缺陷,面积经常超过max_area。原因:环形光源的入射角度不对,或者铭牌表面有轻微弧面,光线反射到相机里形成高光。单纯调高差分阈值会压掉反光,但也会把浅色字符的细节一起压掉。解决:先把光源换成同轴光或加偏振片,从物理层面消除反光。如果硬件不好动,把背景估计核从(31,31)加大到(63,63),让背景照度更平滑,反光残留会被当成背景吸收。注意这一步要重新验证正常件的误检率,不要只看NG件改善。
5.2 字符断裂与阈值选择的矛盾
现象:印刷字符本身笔画浅,差分后字符被拆成几段,每段面积都很小,低于min_area,缺陷就漏了。原因:min_area设得太大,或者开运算核太大把细笔画抹掉了。解决:不要用全局min_area,按ROI单独设置。字符区域的min_area可以设到5像素,脏污区域设到50像素。另外,对差分图先做一次闭运算,核用(5,5),能把断裂的笔画重新连成一条完整的轮廓,再去算面积。但闭运算也会把相隔很近的噪声连起来,所以只对字符ROI开这个操作,其他ROI保持开运算。
5.3 模板匹配对位置偏差过于敏感
现象:工位震动1-2个像素,差分图边缘就出现一圈伪差异,有时候整条边框都被判定为缺陷。原因:角点定位精度不够,或者透视校正后图像有细微偏移。OpenCV的findContours返回的是像素级坐标,震动会导致角点抖动。解决:先用cv2.cornerSubPix把角点优化到亚像素级。另外,差分前做一个微小的配准,用cv2.findTransformECC或者相位相关,能压到0.3像素误差。但配准算法对初始位置敏感,配准失败时要做一个相似度校验,如果匹配度太低,就跳过配准而直接用原始校正图,避免引入更大的误检。
5.4 相机抖动与模糊
现象:产线振动导致抓帧模糊,差分图上全是细碎噪点,误检率飙升。原因:曝光时间太长,或者相机支架刚性不够。模糊图没有清晰边缘,Canny会输出大量随机轮廓。解决:缩短曝光时间,比如从30ms提到5ms,同时把光源亮度调大;相机支架换成减震结构。软件层面,在采集模块里用Laplacian方差判断图像清晰度,方差低于阈值就丢弃这一帧,并记录一条"模糊帧"日志。阈值可以先用100,如果很多好帧被丢就降到50。
5.5 批量测试时的过拟合
现象:用自己收集的测试图验证,准确率99%,一上现场误检率立刻翻几倍。原因:测试图库太"干净",只包含一种光照、一种相机角度,没有覆盖现场反光、灰尘、不同的铭牌批次差异。解决:把现场跑一周的图全部存下来,包括正常件和NG件。然后离线把每个误检样本按原因分类:是反光、是震动、还是参数分类错误。对每一类单独调对应ROI的参数。永远要用之前保留的回归样本集复测,不要改完参数只瞄一眼测试集就上线。源码里的debug_mode会输出所有正常件的差分图,方便快速看出哪些区域总是出现伪差异。
6. 进阶用法:把传统视觉升级成混合检测,低代码接入产线
6.1 用OCR验证印刷内容
差影法能发现形状差异,但不知道印出来的内容到底对不对。厂家要求序列号不能串号时,常见做法是加OCR识别字符区域,然后把结果和配置的期望字符串比对。源码外挂了一个ocr_check.py,核心逻辑很直接:
import pytesseract def ocr_check(plate_roi, expected): text = pytesseract.image_to_string(plate_roi, config='--psm 7') confidence = 0.0 if text.strip().upper() == expected.upper(): confidence = 1.0 else: import difflib confidence = difflib.SequenceMatcher( None, text.strip().upper(), expected.upper()).ratio() return text, confidence说明:--psm 7告诉tesseract把ROI当成单行文本,适合序列号这类单行内容;如果铭牌区域是多行,需要按行切分后逐行调用。difflib的ratio返回0到1的相似度,可以用来做模糊判断。注意pytesseract只是Python封装,你还需要单独安装tesseract-ocr本体,并且把训练好的中文字库放进去,否则中文铭牌会识别成乱码。
6.2 接PLC信号:把检测结果变成电信号
产线上下一个工位通常需要知道当前工件是OK还是NG,源码里在plc_client.py里做了一个socket客户端,每秒把状态发出去:
import socket def send_result(sock, ok): sock.send(b'1' if ok else b'0')常见做法是PLC做TCP Server,工控机做Client,连接之后PLC每收到一个字节就翻一次气缸挡板。这个字节没有协议头,所以要在PLC侧做超时判断:如果连续2秒没收到心跳,就认为视觉系统掉线,强制停车。源码里每2秒发送一个0x7F心跳,PLC通过心跳状态判断链路健康,这一点非常关键,不然视觉程序崩了,产线还在放行NG件。
6.3 让误检样本回流:简单半监督闭环
系统上线之后,维护才是重头。我在几个项目里吃过亏,一开始调参调到满意就扔在那里,结果跑了半个月,误检率慢慢上来,又得从零开始查。后来养成了一个习惯:每过一段时间,把现场新增的误检样本按当天日期放进样本库,重新跑一次全量仿真,看哪些ROI的阈值需要调整。这个操作不花太多时间,但能避免系统慢慢退化。从那以后,我每次上线视觉系统都会强制走一遍"留一批图、跑一周、再调一次"的流程。希望帮到你。
本文还有配套的精品资源,点击获取