简介:一套运行在树莓派硬件上的轻量级瓷砖缺陷检测原型系统源码,面向嵌入式视觉、工业质检方向的开发者和高校学生。项目通过CSI总线接入OV5647摄像头,按钮触发拍照后自动分析瓷砖表面是否存在缺陷,判定结果以红灯加蜂鸣器或白灯直观反馈;算法链路涵盖Canny边缘检测、霍夫变换线段提取、邻域线段关联、梯度欧氏距离计算及归一化阈值判别,思路清晰且便于二次开发。压缩包共15个文件、约263KB,其中3个Python脚本(main.py、Utils.py、GenReport.py)承担主流程、图像处理与报告生成,8张jpg包含正常/缺陷样本及硬件接线图,3个html模板用于检测结果可视化,另有README.md说明项目整体结构。当前已有180人下载学习,对于想快速搭建边缘端视觉检测演示、完成课设或入门树莓派图像识别的读者,是一份轻量且完整的实用参照。
1. 为什么选择用Python在树莓派上做轻量级瓷砖缺陷检测
很多做视觉项目的朋友一听到瓷砖缺陷检测,第一反应就是上深度学习:准备几千张标注图、租一台带GPU的服务器。如果目标只是“先跑通一个原型系统”,这个思路反而最费时间。瓷砖表面纹理稳定、缺陷与背景灰度差明显、相机和光源都可以人工控制,这种条件下传统图像处理的开发速度和检测效果都更划算。
这套原型系统用Python写检测脚本,用树莓派4B做边缘端运算,接一只ov5647摄像头模块成像,再用GPIO控制补光灯,就能把裂纹、针孔这类缺陷找出来并输出结果。适合自动化设备预研工程师,也适合在毕业设计里完整展示采集、处理、联动链路的同学。下面把算法选型、硬件接线、源码和调试顺序依次讲清,按这个路径走,不用在模型训练和标注数据上绕路。
2. 瓷砖缺陷检测的轻量级方案:算法选型与图像预处理参数
2.1 缺陷的成像特征决定检测算法
先看瓷砖上常见缺陷在相机里的样子。裂纹在图像里是一条暗色细线,长度远大于宽度;针孔是没有釉料的小洞,表现为圆形暗斑,直径只有几个到十几个像素;崩边在瓷砖边缘出现缺口,轮廓形状不规则。三类缺陷在灰度上都比正常釉面暗,这是传统阈值分割能成立的前提。
| 缺陷类型 | 成像特征 | 推荐检测算法 | 关键筛选参数 |
|---|---|---|---|
| 裂纹 | 暗色细线,长宽比大 | 顶帽变换 + 灰度阈值 | 长宽比 > 3,最小面积 50 |
| 针孔 | 圆形暗斑,面积小 | 灰度阈值 + 连通域分析 | 面积 50~500,圆形度 > 0.5 |
| 崩边 | 边缘缺口,形状不规则 | 轮廓检测 + ROI裁切 | 边缘邻域内面积突变 |
轻量级方案里不选神经网络的原因很直接:原型阶段最缺的是标注数据,而传统方案只需要调阈值和形态学参数。样本用来验证系统而不是用来训练模型。“轻量级”不是算法简陋,而是在分辨率和缺陷类型受限的前提下,用最少的计算资源解决主要问题。树莓派4B跑一帧1280x960的完整传统视觉流程,从采集到输出结果可以控制在100毫秒量级,这对原型系统来说已经够用。
补光角度对成像的影响在这一步就要想清楚。低角度光最容易照出裂纹,因为光从侧面进入凹槽会形成明显阴影;垂直打光时裂纹对比度显著下降,后面阈值怎么调都吃力。
2.2 顶帽变换增强裂纹与针孔的最小灰度处理
直接用原始灰度图做阈值分割的常见问题是:瓷砖表面本身有深浅不一的纹路和灰尘,固定阈值要么把纹路切进来,要么漏掉低对比度缺陷。顶帽变换(top-hat)先做背景抑制,思路是用形态学开运算估计背景亮度,再用原图减背景,把比背景暗的缺陷单独留下。
import cv2 import numpy as np def enhance_dark_defects(img_bgr): gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (5, 5), 0) kernel_size = 15 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size)) background = cv2.morphologyEx(gray, cv2.MORPH_OPEN, kernel) tophat = cv2.subtract(gray, background) _, thresh = cv2.threshold(tophat, 30, 255, cv2.THRESH_BINARY) return gray, tophat, thresh高斯模糊核用5x5,作用是压掉传感器的高频噪声,太小没效果,太大把细裂纹的边缘也磨平了。kernel_size取15是经验起点,它必须大于期望检出的最宽缺陷,否则开运算会把宽裂纹错当成背景。阈值30表示只保留比背景暗30个灰度级以上的像素;缺陷与釉面反差越大,这个值就能调得越高。实际调参时先降到5和8把小缺陷找出来,再逐步往上提,直到背景噪声消失,比直接套固定值可靠。
提示:同一批瓷砖的釉色深浅会有波动,阈值不要写死在代码里,从配置文件读入。第4章源码就是这么组织的。
2.3 形态学开运算与连通域面积过滤的边界条件
阈值分割之后的结果里通常有噪声:灰尘形成的孤立暗点,以及瓷砖纹理被切割后留下的碎屑。形态学开运算能去掉一部分孤立小点,但核如果太大,会把真正的针孔一起抹掉。3x3椭圆核是处理针孔场景的安全起点,开运算一次约能滤掉半径1个像素的孤立噪声。
open_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) cleaned = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, open_kernel) contours, _ = cv2.findContours(cleaned, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates = [] for c in contours: area = cv2.contourArea(c) if area < 50: continue candidates.append(c)min_area=50是按1280x960分辨率给的初值,语义是“面积小于50像素的连通域一律视为噪声”。这个数字和像素、物理尺寸的换算直接关联:摄像头离瓷砖1米时,50像素可能对应一个直径2mm的针孔;离瓷砖0.3米时同样50像素对应更小的孔洞。所以换安装位置后必须重新标定像素与毫米的比例,这是原型项目里最容易漏的一步。
2.4 长宽比与最小外接矩形区分裂纹和针孔
裂纹和针孔在阈值图里都是暗色连通域,区分它们最常用的特征是长宽比。用最小外接矩形包住连通域,长边是短边3倍以上时判为裂纹,否则按面积和圆形度判为针孔或颗粒。圆形度用轮廓面积和最小外接圆面积之比计算,越接近1越像圆。
def classify_contour(c): area = cv2.contourArea(c) rect = cv2.minAreaRect(c) w, h = rect[1] if min(w, h) < 1e-6: return "unknown", 0.0 long_side, short_side = max(w, h), min(w, h) ratio = long_side / short_side (x, y), radius = cv2.minEnclosingCircle(c) circularity = area / (np.pi * radius * radius) if radius > 0 else 0.0 if ratio > 3.0 and short_side < 5: return "crack", ratio if circularity > 0.5 and area < 500: return "pinhole", ratio return "chip", ratio长宽比阈值取3.0是因为裂纹在成像时往往弯曲,长宽比会因此略微下降,取2.5到3之间都在合理范围;取太高会把弯曲裂纹漏掉。short_side < 5是为了排除宽大的暗色块。圆形度高的连通域长宽比通常接近1,而细长裂纹的圆形度很低,这两个特征互相印证。实际项目里优先看长宽比,再看面积,圆形度只做补充判断。
3. 树莓派4B硬件接线:ov5647摄像头与GPIO补光灯的接入方式
3.1 ov5647摄像头模块与树莓派4B的CSI接口连接
树莓派官方摄像头模块的核心传感器是ov5647,通过排线接到板上的CSI接口。树莓派4B的CSI接口在HDMI接口旁边,是一个按压卡扣式排线座。接排线时把金属触点朝网口方向推到底,压紧卡扣,全程不要带电操作。
插好后确认排线两端都到位,新手最容易犯的错是插反方向,导致系统识别不到摄像头。直观检查方法是看排线上的印刷字方向,通常有字的一面朝接口外侧。方向不对时,运行libcamera-hello会直接报无法找到摄像头。
CSI摄像头和USB摄像头有本质区别:图像数据不经过CPU搬运,走专用接口,同样分辨率下CPU占用明显更低。原型系统用CSI摄像头还省下USB口,键盘鼠标和后续外设都能接上,调试时少很多麻烦。
3.2 启用摄像头:系统配置与拍照验证命令
树莓派官方系统默认不启用摄像头接口,刚刷完系统要先进入配置界面打开Camera选项。
sudo raspi-config # Interface Options -> Camera -> Enable,重启重启后先确认硬件链路:
libcamera-hello --list-cameras libcamera-still -o test.jpg --width 1280 --height 960 --rotation 0第一条命令应列出ov5647,第二条命令拍一张测试图。test.jpg能正常生成,说明摄像头硬件没问题。后面要确认OpenCV能否直接访问摄像头设备,检查/dev/video0是否存在;这个节点不存在时,传统做法是加载bcm2835-v4l2驱动模块并加入开机加载列表,重启后再查一遍。这一步做完,第4章的源码才能直接在树莓派上跑。
树莓派上安装OpenCV直接用系统源,提前装好可以省去源码编译的时间:
sudo apt update sudo apt install -y python3-opencv3.3 GPIO引脚分配与补光灯接线对照表
补光灯是成像质量的关键因素,树莓派4B的CSI接口没有闪光灯同步能力,光源控制只能走GPIO。原型里用一组白光LED照亮瓷砖,控制端接BCM编号17的GPIO引脚,外部用NPN三极管或MOS管做开关,避免让GPIO直接驱动大电流LED。
| 树莓派物理引脚 | BCM编号 | 接线对象 | 说明 |
|---|---|---|---|
| 序号11 | GPIO17 | 补光灯控制信号 | 高电平点亮,低电平熄灭 |
| 序号14 | GND | LED驱动板负极 | 必须与树莓派共地 |
| 序号4 | 5V电源 | LED灯串正极(经驱动) | 大功率灯珠勿从GPIO取电 |
对照表格画硬件接线图时,按引脚序号和BCM编号一一对应即可。三极管驱动常规接法是:控制信号接基极,基极与GPIO之间串1kΩ电阻,LED负极接集电极,LED正极接5V,发射极接地。
共地是最容易出错的一环。灯串如果使用独立12V适配器供电,适配器负极必须和树莓派GND连在一起,否则GPIO输出的高电平对驱动电路来说没有参考地,灯始终不亮。
3.4 采集与光源同步:GPIO点亮补光灯后再拍照
原型系统的工作方式不是常亮灯,而是拍照瞬间点亮。这样LED发热量小,也能让相机自动曝光时环境光尽量稳定。点亮之后要等一小段时间,约100到300毫秒,让LED电流稳定后再拍,否则同一块瓷砖拍两次的灰度值都会漂移。
import time import cv2 import RPi.GPIO as GPIO LED_PIN = 17 GPIO.setmode(GPIO.BCM) GPIO.setup(LED_PIN, GPIO.OUT) cap = cv2.VideoCapture(0, cv2.CAP_V4L2) GPIO.output(LED_PIN, GPIO.HIGH) time.sleep(0.2) ret, frame = cap.read() if not ret: print("采集失败,检查 /dev/video0") GPIO.cleanup() exit(1) GPIO.output(LED_PIN, GPIO.LOW) cap.release() GPIO.cleanup()cv2.VideoCapture的第二个参数cv2.CAP_V4L2是显式指定V4L2后端,在树莓派上比默认后端稳定。time.sleep(0.2)放在亮灯之后、拍照之前,这个顺序不能颠倒。RPi.GPIO是树莓派专用库,开发机上没有,先在电脑上调算法时可以用模拟接口占位,到树莓派上再接真实GPIO。
4. 在树莓派上跑通瓷砖缺陷检测的Python原型源码
4.1 原型系统的运行流程
运行流程拆成四步:初始化、采集、检测、输出。初始化阶段设置GPIO和摄像头;采集阶段开灯、等待稳定、读一帧;检测阶段做第2章的预处理和连通域分析;输出阶段把缺陷列表写成JSON并打印。
设计上要把“缺陷检测”和“硬件控制”分开。检测函数只接收一张BGR图像,返回缺陷列表;硬件部分只负责亮灯和拍照。这样在电脑上调试图像算法时不需要树莓派硬件,在树莓派上跑完整系统时也不用改动算法代码。
4.2 完整可运行的检测脚本
import json import time import cv2 import numpy as np import RPi.GPIO as GPIO LED_PIN = 17 MIN_AREA = 50 RATIO_THRESH = 3.0 def enhance_dark_defects(img_bgr): gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (5, 5), 0) kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15)) background = cv2.morphologyEx(gray, cv2.MORPH_OPEN, kernel) tophat = cv2.subtract(gray, background) _, thresh = cv2.threshold(tophat, 30, 255, cv2.THRESH_BINARY) return thresh def find_defects(thresh): open_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) cleaned = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, open_kernel) contours, _ = cv2.findContours(cleaned, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) defects = [] for c in contours: area = cv2.contourArea(c) if area < MIN_AREA: continue rect = cv2.minAreaRect(c) w, h = rect[1] if min(w, h) < 1e-6: continue long_side, short_side = max(w, h), min(w, h) ratio = long_side / short_side x, y = int(rect[0][0]), int(rect[0][1]) if ratio > RATIO_THRESH: dtype = "crack" elif area < 500: dtype = "pinhole" else: dtype = "chip" defects.append({ "type": dtype, "x": x, "y": y, "w": int(w), "h": int(h), "area": int(area), "ratio": round(ratio, 2) }) return defects def capture_and_detect(save_path=None): GPIO.setmode(GPIO.BCM) GPIO.setup(LED_PIN, GPIO.OUT) cap = cv2.VideoCapture(0, cv2.CAP_V4L2) GPIO.output(LED_PIN, GPIO.HIGH) time.sleep(0.2) ret, frame = cap.read() GPIO.output(LED_PIN, GPIO.LOW) cap.release() GPIO.cleanup() if not ret: raise RuntimeError("camera read failed") if save_path: cv2.imwrite(save_path, frame) thresh = enhance_dark_defects(frame) defects = find_defects(thresh) return { "width": frame.shape[1], "height": frame.shape[0], "defect_count": len(defects), "defects": defects } if __name__ == "__main__": result = capture_and_detect(save_path="/tmp/last_frame.jpg") print(json.dumps(result, indent=2, ensure_ascii=False))执行方式就是python3 tile_inspect.py。脚本先点亮补光灯,等待LED稳定后采集一帧,关闭灯光,再进入检测流程。find_defects内部先做形态学开运算,再进行轮廓提取和特征筛选。输出的JSON包含图像尺寸、缺陷数量和每个缺陷的类型、中心坐标、宽高、面积、长宽比,字段结构方便后续接串口显示或数据库。
4.3 关键函数与参数在工作中的设定依据
增强函数里的高斯核5x5和顶帽结构核15,是相对平衡的组合。15的结构核能滤掉宽度约8像素以下的背景纹理,同时保留更宽的裂纹。如果瓷砖表面纹理较粗,把15改成21;纹理细密则改成11。改完要在阈值图上观察背景噪声变化,不能只看最终输出。
find_defects里min_area=50对应连通域面积下限。设定依据来自中间结果,而不是拍脑袋。调试时先在所有轮廓上画出边界框和面积值,统计正常瓷砖上有哪些噪声轮廓,取一个能覆盖它们的上限作为min_area。现场灰尘多就调大,光线干净可以调小。
JSON输出里的x和y来自minAreaRect的中心点,经纬度式的定位方式在原型阶段足够用。后面的w和h是旋转矩形在局部坐标系下的尺寸,如果要做缺陷面积统计,直接用area字段更准确,不要拿w乘h,因为矩形有旋转角度。
4.4 把参数外置成配置文件,项目说明才能跟上代码
原型交付时要复制到另一台机器或者换一块瓷砖批次,参数经常要改。常见做法是把检测参数放到config.json,代码启动时读取,这样项目说明文档只描述配置文件的作用,不用维护散落的常量。
{ "width": 1280, "height": 960, "led_pin": 17, "gaussian_kernel": 5, "tophat_kernel": 15, "threshold": 30, "open_kernel": 3, "min_area": 50, "ratio_threshold": 3.0 }每个字段都对应代码里一个可调输入。width和height受摄像头模组限制,ov5647在树莓派驱动下最高可达2592x1944,原型检测建议用1280x960,处理耗时更短。led_pin对照第3章引脚表确认,不要与系统默认占用的GPIO冲突。threshold是最敏感的参数,换砖后优先调它。
5. 瓷砖缺陷检测原型上线前的三组调试场景
5.1 曝光与白平衡漂移让阈值失效,先锁死相机参数
树莓派摄像头默认开启自动曝光和自动白平衡,同一种瓷砖在不同时间会拍出不同的灰度分布。固定阈值对这种漂移非常敏感:上午能分出缺陷的阈值,下午可能把整块瓷砖都报成缺陷。先把自动参数关掉,锁定曝光时间和白平衡。
cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) # 切手动曝光模式 cap.set(cv2.CAP_PROP_EXPOSURE, 300) # 依环境亮度调整 cap.set(cv2.CAP_PROP_AUTO_WB, 0) # 关自动白平衡不同V4L2驱动的枚举值含义略有差异,核心目的是固定曝光。验证方法是同一块瓷砖连续拍5张,统计灰度均值,波动超过±5就要继续调。
5.2 从一张图开始调参,保存中间结果而不是盯着终端
调试最有效的手段是保存中间图像。把顶帽变换后的图、阈值图、形态学处理后的连通域标记图都写到/tmp目录,用看图软件逐张对比,能立刻看出是阈值太高造成漏检,还是形态学核太大把小针孔抹掉了。
建议用带参数的命令行入口跑系统:python3 tile_inspect.py --debug --save-dir /tmp/tile_debug。每改一次参数,重跑同一张测试图,对比三组中间结果的变化。这比在终端一遍遍打印数值高效得多。
5.3 用离线样本跑批,评估误检率和漏检率
原型进产线之前,准备两组图片:正常瓷砖20张,带缺陷瓷砖20张。离线跑一遍检测脚本,统计误检率和漏检率。误检率高通常问题在光照和背景,漏检率高通常问题在阈值和形态学参数。
| 指标 | 计算公式 | 常见瓶颈 |
|---|---|---|
| 误检率 | 误报缺陷数 / 正常样本数 | 光照不均匀、背景纹路过粗 |
| 漏检率 | 未检出缺陷样本数 / 缺陷样本数 | 阈值过高、结构核过大 |
最后这轮跑批放在树莓派上执行,和真实运行环境保持一致。不要只在开发机上验证,因为树莓派的图像处理库版本和摄像头驱动与开发机存在差异,结果可能有偏差。
本文还有配套的精品资源,点击获取