标签打印缺陷检测算法及其实现

当前似乎还没有能够直接检测打印缺陷的算法,所以本算法还是主要基于传统视觉算法。

本项目将开源于https://github.com/h13-0/label_checker

1. 基本思路确定

现在有一个检测标签的打印缺陷的需求,具体缺陷类别如下所示:

  1. 污渍缺陷,如下图中的红框区域:
    污渍1.jpg
  2. 断墨、缺墨缺陷,如下图中的红框区域:
    断墨1.jpg

当然,上图最明显的问题是IMEI和SN码及其对应条码是不一样的,这部分暂时无法直接使用模板匹配进行校验,因此这些部分在第三章之前先不予讨论,在第三章中进行讨论。

1.1 算法基本思路的确定

查阅了有关资料,发现目前的各种打印品缺陷检测均是基于模板进行匹配的,本文也不例外,经过简单思考就可得到如下思路:

graph TD;
    A[1. 从标签纸中识别出标签所在区域] --> B[2. 对标签所在区域进行仿射变换,初步调整视角]
    B --> C[3. 从模板标签中通过灰度阈值获取打印样式]
    C --> D[4. 依靠待检标签和模板标签的打印样式对其进行匹配, 获取偏移量]
    D --> E[5. 计算并输出误差]

1.2 图像采集结构的确定

但是使用摄像头直接采集到的图像完全无法保证平整度和同一性。例如在直接使用摄像头获取到的图像中:
摄像头图像1.bmp
由于不够平整,标签两次放置所获得的图像完全无法进行重合和比对,左上角对其后,右下角的图像有相当大的误差:
摄像头图像同一性对比.png

因此可以考虑使用扫描仪结构对标签进行图像采样,即有如下方案:

  1. 直接使用扫描仪进行图像获取。
  2. 使用摄像头+扫描仪结构获取图像,结构示意图如下:
    扫描仪结构.png

当然也可以是类流水线的依次扫描结构。
由于本次项目时间紧急,因此直接使用了扫描仪进行实现。但是在软件实现中,更推荐使用摄像头构造一个类扫描仪结构进行实现,因为软件控制摄像头的复杂度和效果要比扫描仪要好得多。

但是无论是哪种结构,其像素密度PPI必须大于等于600,最好800,否则可能无法完成较小尺寸的缺陷检测

2. 算法实现

2.1 标签识别算法

标签识别算法可以直接使用HSV阈值实现。HSV颜色空间本质是对RGB颜色空间的一种线性变换,其将RGB三个维度转换为了Hue(色相)、Saturation(饱和度)、Value(明度)三个维度,相较于RGB,其更能避免亮度等外界因素的干扰,并能使用OpenCV从图像中获得在HSV阈值区域内的掩板。
使用HSV阈值获取标签所在区域相当简单,拖动滑块到合适的区域即可。
HSV阈值.gif)
(上述工具链接在https://github.com/h13-0/HSV-Range)
随后使用opencv的 findCounters 算法在简单的过滤目标大小后即可获得标签位置。

2.2 标签区域的仿射变换与打印样式获取

仿射变换本质没什么难度,只需要注意使用OpenCV的 boxPoints 获取 minAreaRect 的顶点顺序并不固定,需要手动矫正顺序即可。可以考虑使用如下代码进行顺序纠正:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def get_box_point(self, min_area_rect:tuple) -> list:
"""
@brief: 将min_area_rect转换为四个顶点, 横方向为长边并且输出顺序为: [左上 右上 左下 右下],
@return:
- 横方向为长边并且输出list为[左上 右上 左下 右下]
"""
# cv2.boxPoints得到的四个点一定是按照顺时针排序的
points = cv2.boxPoints(min_area_rect)
# 先计算前两个边的边长, 从而确定长边
side1 = math.sqrt(
math.pow(points[0][0] - points[1][0], 2) +
math.pow(points[0][1] - points[1][1], 2)
)
side2 = math.sqrt(
math.pow(points[1][0] - points[2][0], 2) +
math.pow(points[1][1] - points[2][1], 2)
)
result = []
if(side1 > side2):
# 第一边大于第二边, 则第(0-1)、(2-3)边为长边
## 判断0、3点高度
if(points[0][1] < points[3][1]):
# 此时0点为左上点
result = [points[0], points[1], points[3], points[2]]
else:
# 此时2点为左上点
result = [points[2], points[3], points[1], points[0]]
else:
# 第二边大于第一边, 则第(1-2)、(3-4)边为长边
## 判断0、1点高度
if(points[0][1] < points[1][1]):
# 此时3点为左上点
result = [points[3], points[0], points[2], points[1]]
else:
# 此时1点为左上点
result = [points[1], points[2], points[0], points[3]]
return result

在获得统一的顶点顺序后,即可计算仿射矩阵,对标签目标进行视角仿射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def wrap_min_aera_rect(self, src:np.ndarray, min_area_rect):
'''
@brief: 将 minAreaRect 所在区域仿射并裁切到长方形
@note: 该函数会自动将图像旋转到正确的方向
'''
# 提取w、h
w = min_area_rect[1][0]
h = min_area_rect[1][1]
if(w < h):
w = min_area_rect[1][1]
h = min_area_rect[1][0]
# 计算仿射前四个顶点位置
points = self.get_box_point(min_area_rect)
src_array = np.array(
[
# 左上、右上
points[0], points[1],
# 左下、右下
points[2], points[3],
],
dtype='float32'
)
dst_array = np.array(
[
[0, 0], [w - 1, 0],
[0, h - 1], [w - 1, h - 1]
],
dtype='float32'
)
matrix = cv2.getPerspectiveTransform(src_array, dst_array)
return cv2.warpPerspective(src, matrix, (int(w), int(h)))

打印样式可以直接使用阈值二值化即可。最终可获得如下图像:

打印样式.jpg

2.3 模板匹配

2.3.1 基本模板匹配算法

关于模板匹配,OpenCV内置的有一套对应的API的,即 matchTemplate
虽然最开始我也写了一套简单的迭代优化算法,但是无论是性能还是鲁棒性都无法与OpenCV内置的相比。
在大多数情况下,直接使用OpenCV内置的算法都可以表现的很好,如下图所示的原图与异或比较图:

图像名 图像
选定模板 2400PPI测试模板.jpg
待测样本 2400PPI测试待检2.jpg
误差图 效果良好的模板匹配.jpg

但是在部分情况下也存在匹配效果较差的情况,如下图所示:

图像名 图像
选定模板 2400PPI测试模板.jpg
待测样本 2400PPI测试待检3.jpg
误差图 较差匹配.jpg

2.3.2 误差分析

对上述待检图像进行分析:
2400PPI测试待检3.jpg
上述待检图像主要有如下几个问题:

  1. 标签疑似不平整
  2. 横向边距过大

但是通过手动在PS中比对发现其是可以对得上的,如下图所示:
手动比对.jpg

分析发现,该误差主要是由于打印出来的样式与模板样式在标签纸上有1.6度的旋转误差导致的。
而OpenCV内置的模板匹配算法无法有效的应对旋转造成的影响,即该匹配算法只能做x、y两个自由度的匹配,无法做旋转以及缩放的。

2.4 含有旋转角的迭代匹配

如果使用 matchTemplate 对分区进行模板匹配,则时间复杂度为 $O((m-n)^2)$ ,其中:

  • $m$ 为匹配图像的像素宽度
  • $n$ 为待检测图像的像素宽度
    而OpenCV的 matchTemplate 要求待检图像比原图要小,因此应当使用如下方法给待检图像制作Border
1
2
3
4
# 将模板向外拓展, 方便使用cv2.matchTemplate进行模板匹配
border_w = max_abs_x
border_h = max_abs_y
std_with_border = cv2.copyMakeBorder(std, border_h, border_h, border_w, border_w,cv2.BORDER_CONSTANT, value=(0))

为了保证程序执行效率,原先考虑的如下的执行逻辑经过验证后发现并无实际作用:

  1. 使用 matchTemplate 将模板x、y轴offset进行匹配,并记录本次迭代中x、y轴offset是否发生改变。
  2. 基于当前最优x、y轴offset进行旋转角匹配,并记录本次迭代中旋转角度是否发生改变。
  3. 若x、y轴offset和旋转角均未改变,则可判定为最优,退出迭代。
    但是通常在第一次进入步骤2时就无法继续优化。因此需要将1、2同时进行迭代,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
def fine_tune(self, test:np.ndarray, std:np.ndarray, max_abs_x:int, max_abs_y:int, max_abs_a:float, max_iterations, 
shielded_areas:list=None, angle_accu = 0.1, view_size:int = 2, show_process:bool = False
):
'''
@brief: 将待测图像(test)通过线性变换微调到模板(std)上。
@param:
- test: 待测图像
- std: 模板
- max_abs_x: x轴最大微调像素数(绝对值)
- max_abs_y: y轴最大微调像素数(绝对值)
- max_abs_a: 最大旋转角角度数(绝对值)
- max_iterations: 最大微调迭代数
- angle_accu: 旋转角调整精度, 当angle_accu<0时为跳过旋转角匹配
- view_size: 视野边距:
例如边距为为1时, 对应视野矩阵为3x3, 视野行向量长度为3
例如边距为为2时, 对应视野矩阵为5x5, 视野行向量长度为5
@return:
best param in [x, y, angle]
'''
iterations = 0
finish = False
# 初始值与现值
curr_x = 0
curr_y = 0
curr_a = 0
## [x, y, angle]
last_params = [0, 0, 0.]
last_loss = self.try_match(
test, std,
x=0,
y=0,
angle=0,
shielded_areas=shielded_areas
)
best_params = last_params
best_loss = last_loss
logging.info("iteration: %d, loss: %f" % (iterations, last_loss))
## 角度搜索步长
curr_angle_step = max_abs_a / float(view_size)
# 将模板向外拓展, 方便使用cv2.matchTemplate进行模板匹配
border_w = max_abs_x
border_h = max_abs_y
std_with_border = cv2.copyMakeBorder(std, border_h, border_h, border_w, border_w, cv2.BORDER_CONSTANT, value=(0))
# curr_loss为每代所得到的最小误差值
curr_loss = -1
while(iterations < max_iterations and finish == False):
iterations += 1
# 判定是否需要角度匹配
if(angle_accu <= 0):
## 跳过角度匹配, 直接进行单次模板匹配
### 使用opencv自带的模板匹配进行匹配
result = cv2.matchTemplate(std_with_border, test, cv2.TM_CCOEFF)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
top_left = max_loc
offset_x = border_w - top_left[0]
offset_y = border_h - top_left[1]
curr_x = offset_x
curr_y = offset_y
curr_loss = self.try_match(
test, std,
x=round(curr_x),
y=round(curr_y),
angle=curr_a,
shielded_areas=shielded_areas,
show_diff=show_process
)
finish = True
else:
## 计算损失矩阵大小
matrix_size = view_size * 2 + 1
## 旋转角度损失矩阵, 未计算点为-1
### angle_loss[0][i]: loss
### angle_loss[1][i]: offset_x
### angle_loss[2][i]: offset_y
angle_loss = np.full((3, matrix_size), -1)

## 1. 计算各旋转角下的xy_offset及loss
### min_loss为本次更新后, 损失向量中最小的loss值
min_loss = -1
### min_da为本次更新后, 损失向量中最小的loss值相对于中心的index偏移量
### 例如min_loss在中心偏左2个单位, 则min_da=-2
min_da = 0
for i in range(matrix_size):
if(angle_loss[0][i] < 0):
### 1.1 将待测图像旋转到指定角度
rotated = self.linear_trans_to(
img=test,
x=0,
y=0,
angle=curr_a + (i - view_size) * curr_angle_step,
border_color=[0]
)
### 1.2 执行模板匹配
result = cv2.matchTemplate(std_with_border, rotated, cv2.TM_CCOEFF)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
top_left = max_loc
offset_x = border_w - top_left[0]
offset_y = border_h - top_left[1]
angle_loss[0][i] = self.try_match(
test, std,
x=curr_x,
y=curr_y,
angle=curr_a + (i - view_size) * curr_angle_step,
shielded_areas=shielded_areas
)
angle_loss[1][i] = offset_x
angle_loss[2][i] = offset_y
### 1.3 更新最小loss所在参数
if((min_loss < 0) or (angle_loss[0][i] < min_loss)):
min_loss = angle_loss[0][i]
min_da = i - view_size
### 1.4 当中心也为最小值时则不移动
if(angle_loss[0][view_size] == min_loss):
min_da = 0
# 2. 输出计算结果
logging.debug("iteration: %d, angle_loss matrix: %s"%(iterations, angle_loss))
logging.debug("iteration: %d, min_da: %d"%(iterations, min_da))
logging.debug("iteration: %d, min_loss: %d"%(iterations, min_loss))
logging.debug("iteration: %d, curr_angle_step: %f"%(iterations, curr_angle_step))
# 3. 将curr_offset更新为loss最低的位置, 并更新loss矩阵(用-1填充矩阵)
curr_a = curr_a + min_da * curr_angle_step
curr_x = angle_loss[1][min_da + view_size]
curr_y = angle_loss[2][min_da + view_size]
if(min_da < 0):
# 中心向左移动abs(delta_x)个坐标点, 矩阵向右移动abs(delta_x)个坐标点
## 仍有效的矩阵remain_xy=xy_loss[:, : matrix_size + delta_x]
new_loss = np.full((3, matrix_size), -1)
new_loss[: , abs(min_da):] = angle_loss[:, : matrix_size + min_da]
angle_loss = new_loss
elif(min_da > 0):
# 中心向右移动abs(delta_x)个坐标点, 矩阵向左移动abs(delta_x)个坐标点
## 仍有效的矩阵remain_xy=xy_loss[:, delta_x: ]
new_loss = np.full((3, matrix_size), -1)
new_loss[: , : matrix_size - min_da] = angle_loss[:, min_da:]
angle_loss = new_loss
# 4. 检查运行结果
if(angle_loss[0].min() < 0):
## 4.1 如果损失矩阵仍在更新, 则表示在当前精度下未达到最优, 则继续在当前精度下运算
pass
else:
## 4.2 如果损失矩阵未更新(此时loss向量无负值), 则表示当前精度下无优化空间
if(curr_angle_step >= angle_accu):
### 4.2.1 如果此时精度未达标, 则提升精度并继续运算
curr_angle_step /= 2.0
#### 清空矩阵
new_loss = np.full((3, matrix_size), -1)
new_loss[0][view_size] = angle_loss[0][view_size]
new_loss[1][view_size] = angle_loss[1][view_size]
new_loss[2][view_size] = angle_loss[2][view_size]
angle_loss = new_loss
else:
### 4.2.2 如果此时精度达标, 则结束运算
finish = True

# 5. 同步本代最小误差结果
curr_loss = min_loss
if(curr_loss < best_loss):
best_loss = curr_loss
best_params = [curr_x, curr_y, curr_a]
logging.info("iteration: %d, loss: %f, x: %d, y: %d, a: %f" % (iterations, curr_loss, curr_x, curr_y, curr_a))
return best_params

其中,try_match 函数定义的loss为异或操作后,不匹配的像素数,代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def try_match(self, img1:np.ndarray, img2:np.ndarray, x:int, y:int, angle,shielded_areas:list=None, show_diff = False) -> int:
'''
@brief: 将待测图像以指定线性变换匹配到标准图像上, 用于二值图匹配
@param:
- img1: 待变换的图像
- img2: 欲匹配的图像
@return: 未能匹配的像素数量
'''
# 线性变换
trans = self.linear_trans_to(img1, x, y, angle, [img2.shape[1], img2.shape[0]], 0)
# 对选定区域进行屏蔽
if(isinstance(shielded_areas, list)):
for area in shielded_areas:
# 执行屏蔽
trans = cv2.rectangle(trans, (area[0], area[1]), (area[2], area[3]), 0, thickness=cv2.FILLED)
# 计算像素点误差
#diff = cv2.absdiff(trans, img2)
diff = cv2.bitwise_xor(trans, img2)
if(show_diff):
cv2.imshow("try_match:trans", trans)
cv2.imshow("try_match:diff", diff)
cv2.waitKey(1)
loss = cv2.countNonZero(diff)
return loss

2.5 区域匹配的引入

2.5.1 区域的划分

在经过总体的模板匹配后,基本上可以认定其各打印区域已经无旋转误差,所以在做区域匹配时只需要考虑xy偏移误差即可。
在分区时,需要:

  1. 将模板和待检进行逻辑与操作。
  2. 对逻辑与之后的图像进行膨胀操作,膨胀半径应等于容许线性误差数。
  3. 对膨胀后的逐个遍历闭合区域,并将该区域对应的模板样式和待检样式裁剪出来分区匹配调用 fine_tune 即可。
  4. 计算出偏移量后,将模板图像裁剪出来的区域做上线性偏移,拼成 “模板图像分区匹配到待检图像” 的打印样式图。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def match_template_to_target_partitioned(self, 
template_pattern:np.ndarray,
target_pattern:np.ndarray,
dilate_diameter:int,
shielded_areas:list=None
):
"""
@brief: 将模板分区匹配到目标样式上, 并返回分区匹配后的结果
@param:
- template_pattern: 模板样式, 要求二值图
- target_pattern: 待检图像样式, 要求二值图
- shielded_areas: 屏蔽区域列表
@return: 将模板分区匹配到待检图像后的结果
"""
# 1. 先屏蔽待检区域
if(isinstance(shielded_areas, list)):
for area in shielded_areas:
# 执行屏蔽
template_pattern = cv2.rectangle(template_pattern, (area[0], area[1]), (area[2], area[3]), 0, thickness=cv2.FILLED)
target_pattern = cv2.rectangle(target_pattern, (area[0], area[1]), (area[2], area[3]), 0, thickness=cv2.FILLED)
# 2. 做逻辑与操作, 寻找单个闭合区域
closed_pattern = cv2.bitwise_or(template_pattern, target_pattern)
# 3. 逐个遍历闭合区域, 并进行分区匹配
result = np.zeros((target_pattern.shape[0], target_pattern.shape[1]), np.uint8)
## 3.1 对图像进行膨胀操作, 避免分区过小
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (dilate_diameter * 2 + 1, dilate_diameter * 2 + 1))
closed_pattern_dilated = cv2.dilate(closed_pattern, kernel, 1)
## 3.1 寻找闭合区域时只检测外围轮廓
contours, hierarchy = cv2.findContours(closed_pattern_dilated, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE)
for c in contours:
x, y, w, h = cv2.boundingRect(c)
## 3.2 分区域进行fine_tune
template_partition = template_pattern[y : y + h, x : x + w]
target_partition = target_pattern[y : y + h, x : x + w]
logging.debug("shape: " + str(template_partition.shape))
offset = self.fine_tune(
test=template_partition,
std=target_partition,
max_abs_x=min(28, w),
max_abs_y=min(28, h),
max_abs_a=2.5,
max_iterations=40,
angle_accu=-1,
view_size=10,
)
## 3.3 将fine_tune结果拼回总图
result[y : y + h, x : x + w] = self.linear_trans_to(
img=template_partition,
x=offset[0],
y=offset[1],
angle=offset[2],
output_size=(w, h),
border_color=0
)
return result

关于为什么是 “模板图像分区匹配到待检图像” ,而不是 “待检图像分区匹配到模板图像” ,是由于需要计算出缺陷在待检图像上的坐标,因此需要反向匹配。
最终可以完成如下的检测代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def _match_label(self, 
template_pattern, target_img, target_rect, threshold:int, shielded_areas:list,
template_defects:list=[], linear_error:int = 3,
gen_high_pre_diff:bool = False
) -> LabelDetectResult:
"""
@brief: 将target_img图像中指定的target_rect所在的标签与template_pattern进行匹配
@param:
- template_pattern: [只读参数], 模板样式
- target_img: [只读参数], 待测图像, 可以包含多个标签
- target_rect: [只读参数], 目标标签所在minAreaRect
- threshold: [只读参数], 标签图像黑度阈值, 同方法 `_process_template` 中的同名参数
- template_defects: [只读参数], 标签模板检出缺陷, 用于进行匹配
- linear_error: [只读参数], 容许的线性偏移误差
@return:
- LabelDetectResult类型的匹配结果
@note:
本函数未来会做为并行运算的方法使用
"""
# 1. 将模板标签仿射回标准视角
target_wrapped = self._checker.wrap_min_aera_rect(target_img, target_rect)
# 2. 获取待检图像原始样式
pattern = self._checker.get_pattern(target_wrapped, threshold)
# 3. 监测相似度是否超标
loss = self._checker.try_match(
pattern, template_pattern,
x=0,
y=0,
angle=0,
shielded_areas=shielded_areas,
show_diff=False
)
# 4. 微调, TODO: 参数可调
x, y, angle = self._checker.fine_tune(
test=pattern, std=template_pattern,
max_abs_x=80, max_abs_y=80, max_abs_a=1,
max_iterations=40,
shielded_areas=shielded_areas,
angle_accu=0.03, view_size=3,
show_process=False
)
# 5. 获取微调后的样式
target_pattern = self._checker.linear_trans_to(
img=pattern, x=x, y=y, angle=angle, output_size=[template_pattern.shape[1], template_pattern.shape[0]], border_color=0
)
# 6. 将模板分区微调到微调后的待测样式
matched_template_pattern = self._checker.match_template_to_target_partitioned(
template_pattern=template_pattern.copy(),
target_pattern=target_pattern.copy(),
dilate_diameter=linear_error,
shielded_areas=shielded_areas,
)
# 7. 获得误差图像
## 7.1 计算全局误差
global_target_remain = self._checker.cut_with_tol(target_pattern, template_pattern, 0, shielded_areas)
global_template_remain = self._checker.cut_with_tol(template_pattern, target_pattern, 0, shielded_areas)
global_diff = cv2.bitwise_or(global_target_remain, global_template_remain)
## 7.2 计算区域匹配后的误差
target_remain = self._checker.cut_with_tol(matched_template_pattern, target_pattern, linear_error, shielded_areas)
template_remain = self._checker.cut_with_tol(target_pattern, matched_template_pattern, linear_error, shielded_areas)
sub_region_matched_diff = cv2.bitwise_or(target_remain, template_remain)
## 7.3 消除由于子区域匹配带来的误差
diff = cv2.bitwise_and(global_diff, sub_region_matched_diff)
high_pre_diff = None
if(gen_high_pre_diff):
target_remain = self._checker.cut_with_tol(matched_template_pattern, target_pattern, 0, shielded_areas)
template_remain = self._checker.cut_with_tol(target_pattern, matched_template_pattern, 0, shielded_areas)
high_pre_diff = cv2.bitwise_or(target_remain, template_remain)
high_pre_diff = cv2.bitwise_and(high_pre_diff, global_diff)
# 8. 计算线性变换后原图
target_transed = self._checker.linear_trans_to(
img=target_wrapped, x=x, y=y, angle=angle, output_size=[template_pattern.shape[1], template_pattern.shape[0]], border_color=[255, 255, 255]
)
# 9. 检测断墨缺陷
ink_defects = []
if(self._detector):
ink_defects = self._detector.detect(target_transed, template_defects=template_defects)
# 10. 填装运算结果
result = LabelDetectResult()
result.diff = diff
result.target_transed = target_transed
result.target_pattern = target_pattern
result.high_pre_diff = high_pre_diff
result.matched_template_pattern = matched_template_pattern
result.ink_defects = ink_defects
result.offset_x = x
result.offset_y = y
result.offset_angle = angle
return result

经过此步骤后,误差已被大大缩减,异或所得的高精误差图像如下图所示:
分区匹配后高精误差图.jpg

2.6 滤波

将匹配好的待检图像和模板图像分别膨胀后相减,在做逻辑与即可。
步骤如下:

  1. 将做好匹配的待检样式膨胀,膨胀半径为线性误差数(为运行时算法参数)。
  2. 计算得到模板图像的样式包含,而膨胀后的待检图像不包含的误差点,记作误差图1。
  3. 将做好匹配的模板样式膨胀,膨胀半径为线性误差数(为运行时算法参数)。
  4. 计算得到待检图像的样式包含,而膨胀后的模板图像不包含的误差点,记作误差图2。
  5. 将误差图1和误差图2做逻辑与操作,得到最终误差。
    代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def cut_with_tol(self, img1:np.ndarray, img2:np.ndarray, tolerance:int, shielded_areas:list=None):
'''
@brief: 膨胀相切算法, 用img1来切img2, 返回img2剩余部分, 使用时要交替互相相切最终得误差图
@note: 图像均为二值图
@param:
- img1
- img2
- tolerance: 像素误差值
'''
# 先进行膨胀相切(0腐蚀)
remain = None
if(tolerance > 0):
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (tolerance * 2 + 1, tolerance * 2 + 1))
img1_dilate = cv2.dilate(img1, kernel, 1)
img2_dilate = cv2.dilate(img2, kernel, 1)
xor = cv2.bitwise_xor(img1_dilate, img2_dilate)
remain = cv2.bitwise_and(xor, img2)
else:
xor = cv2.bitwise_xor(img1, img2)
remain = cv2.bitwise_and(xor, img2)
# 执行屏蔽
if(isinstance(shielded_areas, list)):
for area in shielded_areas:
remain = cv2.rectangle(remain, (area[0], area[1]), (area[2], area[3]), 0, thickness=cv2.FILLED)
return remain

而2.5中获得的误差就会被滤波为如下图像:
滤波后误差图.jpg

3. 联动

使用BarTender的打印接口即可扫描并制定打印机并发送打印任务。