当前似乎还没有能够直接检测打印缺陷的算法,所以本算法还是主要基于传统视觉算法。
本项目将开源于https://github.com/h13-0/label_checker
1. 基本思路确定
现在有一个检测标签的打印缺陷的需求,具体缺陷类别如下所示:
- 污渍缺陷,如下图中的红框区域:
- 断墨、缺墨缺陷,如下图中的红框区域:
当然,上图最明显的问题是IMEI和SN码及其对应条码是不一样的,这部分暂时无法直接使用模板匹配进行校验,因此这些部分在第三章之前先不予讨论,在第三章中进行讨论。
1.1 算法基本思路的确定
查阅了有关资料,发现目前的各种打印品缺陷检测均是基于模板进行匹配的,本文也不例外,经过简单思考就可得到如下思路:
graph TD; A[1. 从标签纸中识别出标签所在区域] --> B[2. 对标签所在区域进行仿射变换,初步调整视角] B --> C[3. 从模板标签中通过灰度阈值获取打印样式] C --> D[4. 依靠待检标签和模板标签的打印样式对其进行匹配, 获取偏移量] D --> E[5. 计算并输出误差]
1.2 图像采集结构的确定
但是使用摄像头直接采集到的图像完全无法保证平整度和同一性。例如在直接使用摄像头获取到的图像中:
由于不够平整,标签两次放置所获得的图像完全无法进行重合和比对,左上角对其后,右下角的图像有相当大的误差:
因此可以考虑使用扫描仪结构对标签进行图像采样,即有如下方案:
- 直接使用扫描仪进行图像获取。
- 使用摄像头+扫描仪结构获取图像,结构示意图如下:
当然也可以是类流水线的依次扫描结构。
由于本次项目时间紧急,因此直接使用了扫描仪进行实现。但是在软件实现中,更推荐使用摄像头构造一个类扫描仪结构进行实现,因为软件控制摄像头的复杂度和效果要比扫描仪要好得多。
但是无论是哪种结构,其像素密度PPI必须大于等于600,最好800,否则可能无法完成较小尺寸的缺陷检测。
2. 算法实现
2.1 标签识别算法
标签识别算法可以直接使用HSV阈值实现。HSV颜色空间本质是对RGB颜色空间的一种线性变换,其将RGB三个维度转换为了Hue(色相)、Saturation(饱和度)、Value(明度)三个维度,相较于RGB,其更能避免亮度等外界因素的干扰,并能使用OpenCV从图像中获得在HSV阈值区域内的掩板。
使用HSV阈值获取标签所在区域相当简单,拖动滑块到合适的区域即可。
)
(上述工具链接在https://github.com/h13-0/HSV-Range)
随后使用opencv的 findCounters
算法在简单的过滤目标大小后即可获得标签位置。
2.2 标签区域的仿射变换与打印样式获取
仿射变换本质没什么难度,只需要注意使用OpenCV的 boxPoints
获取 minAreaRect
的顶点顺序并不固定,需要手动矫正顺序即可。可以考虑使用如下代码进行顺序纠正:
1 | def get_box_point(self, min_area_rect:tuple) -> list: |
在获得统一的顶点顺序后,即可计算仿射矩阵,对标签目标进行视角仿射。
1 | def wrap_min_aera_rect(self, src:np.ndarray, min_area_rect): |
打印样式可以直接使用阈值二值化即可。最终可获得如下图像:
2.3 模板匹配
2.3.1 基本模板匹配算法
关于模板匹配,OpenCV内置的有一套对应的API的,即 matchTemplate
。
虽然最开始我也写了一套简单的迭代优化算法,但是无论是性能还是鲁棒性都无法与OpenCV内置的相比。
在大多数情况下,直接使用OpenCV内置的算法都可以表现的很好,如下图所示的原图与异或比较图:
图像名 | 图像 |
---|---|
选定模板 | |
待测样本 | |
误差图 |
但是在部分情况下也存在匹配效果较差的情况,如下图所示:
图像名 | 图像 |
---|---|
选定模板 | |
待测样本 | |
误差图 |
2.3.2 误差分析
对上述待检图像进行分析:
上述待检图像主要有如下几个问题:
- 标签疑似不平整
- 横向边距过大
但是通过手动在PS中比对发现其是可以对得上的,如下图所示:
分析发现,该误差主要是由于打印出来的样式与模板样式在标签纸上有1.6度的旋转误差导致的。
而OpenCV内置的模板匹配算法无法有效的应对旋转造成的影响,即该匹配算法只能做x、y两个自由度的匹配,无法做旋转以及缩放的。
2.4 含有旋转角的迭代匹配
如果使用 matchTemplate
对分区进行模板匹配,则时间复杂度为 $O((m-n)^2)$ ,其中:
- $m$ 为匹配图像的像素宽度
- $n$ 为待检测图像的像素宽度
而OpenCV的matchTemplate
要求待检图像比原图要小,因此应当使用如下方法给待检图像制作Border
1 | # 将模板向外拓展, 方便使用cv2.matchTemplate进行模板匹配 |
为了保证程序执行效率,原先考虑的如下的执行逻辑经过验证后发现并无实际作用:
- 使用
matchTemplate
将模板x、y轴offset进行匹配,并记录本次迭代中x、y轴offset是否发生改变。 - 基于当前最优x、y轴offset进行旋转角匹配,并记录本次迭代中旋转角度是否发生改变。
- 若x、y轴offset和旋转角均未改变,则可判定为最优,退出迭代。
但是通常在第一次进入步骤2时就无法继续优化。因此需要将1、2同时进行迭代,代码如下:
1 | def fine_tune(self, test:np.ndarray, std:np.ndarray, max_abs_x:int, max_abs_y:int, max_abs_a:float, max_iterations, |
其中,try_match
函数定义的loss为异或操作后,不匹配的像素数,代码实现如下:
1 | def try_match(self, img1:np.ndarray, img2:np.ndarray, x:int, y:int, angle,shielded_areas:list=None, show_diff = False) -> int: |
2.5 区域匹配的引入
2.5.1 区域的划分
在经过总体的模板匹配后,基本上可以认定其各打印区域已经无旋转误差,所以在做区域匹配时只需要考虑xy偏移误差即可。
在分区时,需要:
- 将模板和待检进行逻辑与操作。
- 对逻辑与之后的图像进行膨胀操作,膨胀半径应等于容许线性误差数。
- 对膨胀后的逐个遍历闭合区域,并将该区域对应的模板样式和待检样式裁剪出来分区匹配调用
fine_tune
即可。 - 计算出偏移量后,将模板图像裁剪出来的区域做上线性偏移,拼成 “模板图像分区匹配到待检图像” 的打印样式图。
1 | def match_template_to_target_partitioned(self, |
关于为什么是 “模板图像分区匹配到待检图像” ,而不是 “待检图像分区匹配到模板图像” ,是由于需要计算出缺陷在待检图像上的坐标,因此需要反向匹配。
最终可以完成如下的检测代码:
1 | def _match_label(self, |
经过此步骤后,误差已被大大缩减,异或所得的高精误差图像如下图所示:
2.6 滤波
将匹配好的待检图像和模板图像分别膨胀后相减,在做逻辑与即可。
步骤如下:
- 将做好匹配的待检样式膨胀,膨胀半径为线性误差数(为运行时算法参数)。
- 计算得到模板图像的样式包含,而膨胀后的待检图像不包含的误差点,记作误差图1。
- 将做好匹配的模板样式膨胀,膨胀半径为线性误差数(为运行时算法参数)。
- 计算得到待检图像的样式包含,而膨胀后的模板图像不包含的误差点,记作误差图2。
- 将误差图1和误差图2做逻辑与操作,得到最终误差。
代码如下:
1 | def cut_with_tol(self, img1:np.ndarray, img2:np.ndarray, tolerance:int, shielded_areas:list=None): |
而2.5中获得的误差就会被滤波为如下图像:
3. 联动
使用BarTender的打印接口即可扫描并制定打印机并发送打印任务。