Python_tips-投影分割图像.

万壑松风知客来,摇扇抚琴待留声

1. 文起

纪念刚过的 1024。

图像投影和分割在图像处理中也是一种常见的处理方式,通过某种途径对一种图像中的物体进行区分,本文将记录一种十分简单却十分有效的投影、分割方法。

其实这个投影十分的容易理解,简单来说就是将图像中的像素点投影到图像的边缘。同时还可以按照某种条件对其进行分割,从而得到其中一部分的图像信息。分割的话就是根据物体间隙横纵切割。下面具体来说说。

2. 垂直方向

2.1. 垂直投影图像

以上是一张原图,其中包含了三个形状,每个形状之间都有一定的空隙,先来看看投影的步骤。

1
2
3
4
5
6
7
8
9
10
11
12
import numpy as np
import cv2
import copy

# 按单通道读取图像,获得矩阵
image = cv2.imread(r'./1.png', flags=0)
# 并将白色区域转换为黑色
image[image == 255] = 0

cv2.imshow('image', image)
cv2.waitKey()
cv2.destroyAllWindows()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 获得图像的高、宽
h,w = image.shape
# 拷贝图像
image_count = copy.deepcopy(image)
# 获得一个非黑色像素点计数矩阵
image_count[image_count != 0] = 1

# 获得每一列非0元素的个数,这就是需要投影的像素点数
projection_col = np.sum(image_count, axis=0)
# 生成一个全黑同样大小的矩阵
draw_image = np.zeros((h,w))
# 循环每一列
for i in range(w):
# 循环该列非0的长度
for j in range(projection_col[i]):
# 在全黑矩阵中,从上到下将赋值非0长度个白色元素
draw_image[j,i] = 255

cv2.imshow('image', draw_image)
cv2.waitKey()
cv2.destroyAllWindows()

上图就是垂直向上投影的结果,和想象中还是差不多的。

2.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
# 记录分割起点、末点
split_start = None
split_end = None
# 0的位置就是分割条件
where_id = np.where(projection_col == 0)[0]
# 按照0的位置条件开始分割
split_points = np.split(projection_col, where_id)
# 这里给划分列表头添加0,尾部添加一个图像的宽度值,这个是为了保证如果边缘还有图像最后的划分将不会报错(这需要对numpy的方法有一个较好的理解)
# 解决边界划分超长度问题
where_id = np.insert(where_id, 0, 0)
where_id = np.append(where_id, w) #如果数值分割则添加h

# 通过循环将所有的分割点都保留下来
split_image_points = []
for i,split_point in enumerate(split_points):
if len(split_point) > 1:
split_start = where_id[i]
split_end = where_id[i+1]
split_image_points.append((split_start, split_end))
print(split_image_points)
# [(20, 264), (281, 457), (475, 637)]

# 根据分割点垂直分割图像
for split_image_point in split_image_points:
temp_image = copy.deepcopy(image)
temp_image[:,:split_image_point[0]] = 0
temp_image[:,split_image_point[1]:] = 0
cv2.imshow('temp_image', temp_image)
cv2.waitKey()
cv2.destroyAllWindows()

以上三个图形就是通过物体之间间隙的条件进行划分的结果,可以明显看到十分准确。

3. 水平方向

3.1. 水平投影图像

既然有垂直投影,当然就会有水平投影。如果我们将上面的原图转换一下,那垂直投影就不再适用了。

1
2
3
4
5
6
7
8
9
# 读取原图
image = cv2.imread(r'./1.png', flags=1)
# 旋转矩阵,得到新图像
image_90 = np.rot90(image)
cv2.imwrite('./2.png', image_90)

cv2.imshow('image_90', image_90)
cv2.waitKey()
cv2.destroyAllWindows()

面对这样的图像,下意识就能想到不论是投影还是分割,都将从垂直转换为水平方向。其实水平处理和垂直处理相差无异,区别只是某些参数的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
image_90 = cv2.imread(r'./2.png', flags=0)
image_90[image_90 == 255] = 0
# 计数矩阵
image_count = copy.deepcopy(image_90)
image_count[image_count != 0] = 1
# 获得每一行非0元素的个数,这就是需要投影的像素点数
projection_ver = np.sum(image_count, axis=1)
h,w = image_count.shape
draw_image = np.zeros((h,w))
for i in range(h):
for j in range(projection_ver[i]):
draw_image[i,j] = 255

cv2.imshow('image', draw_image)
cv2.waitKey()
cv2.destroyAllWindows()

将原来的图像水平向左投影后的结果如上图。

3.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
split_start = None
split_end = None
where_id = np.where(projection_ver == 0)[0]
split_points = np.split(projection_ver, where_id)
# 这里给划分列表头添加0,尾部添加一个图像的高度值,这个是为了保证如果边缘还有图像最后的划分将不会报错(这需要对numpy的方法有一个较好的理解)
# 解决边界划分超长度问题
where_id = np.insert(where_id, 0, 0)
where_id = np.append(where_id, h) #如果数值分割则添加w

split_image_points = []
for i,split_point in enumerate(split_points):
if len(split_point) > 1:
split_start = where_id[i]
split_end = where_id[i+1]
split_image_points.append((split_start, split_end))
print(split_image_points)
# [(15, 177), (195, 371), (388, 632)]

for split_image_point in split_image_points:
temp_image = copy.deepcopy(image_90)
temp_image[:split_image_point[0],:] = 0
temp_image[split_image_point[1]:,:] = 0
cv2.imshow('temp_image', temp_image)
cv2.waitKey()
cv2.destroyAllWindows()

到此完成了垂直和水平方向上投影、分割的功能。

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import numpy as np
import cv2
import copy

# 投影函数
def projection(img,direction='col'):
image_count = copy.deepcopy(img)
image_count[image_count == 255] = 0
image_count[image_count != 0] = 1
h,w = image_count.shape
draw_image = np.zeros((h,w))
if direction == 'col':
projection_count = np.sum(image_count, axis=0)
for i in range(w):
for j in range(projection_count[i]):
draw_image[j,i] = 255
else:
projection_count = np.sum(image_count, axis=1)
for i in range(h):
for j in range(projection_count[i]):
draw_image[i,j] = 255

cv2.imshow('image', draw_image)
cv2.waitKey()
cv2.destroyAllWindows()
return projection_count

# 分割函数
def split(img,projection_count,direction='col'):
split_start = None
split_end = None
h,w = img.shape
where_id = np.where(projection_count == 0)[0]
if direction == 'col':
where_id = np.insert(where_id, 0, 0)
# 这里给划分列表添加一个图像的宽度值,这个是为了保证如果边缘还有图像最后的划分将不会报错(这需要对numpy的方法有一个较好的理解)
where_id = np.append(where_id, w)
else:
where_id = np.insert(where_id, 0, 0)
# 这里给划分列表添加一个图像的高度值,这个是为了保证如果边缘还有图像最后的划分将不会报错(这需要对numpy的方法有一个较好的理解)
where_id = np.append(where_id, h)
split_points = np.split(projection_count, where_id)

split_image_points = []
for i,split_point in enumerate(split_points):
if len(split_point) > 1:
split_start = where_id[i]
split_end = where_id[i+1]
split_image_points.append((split_start, split_end))

for split_image_point in split_image_points:
temp_image = copy.deepcopy(img)
temp_image[temp_image == 255] = 0
# 覆盖像素的位置需要注意修改
if direction == 'col':
temp_image[:,:split_image_point[0]] = 0
temp_image[:,split_image_point[1]:] = 0
else:
temp_image[:split_image_point[0],:] = 0
temp_image[split_image_point[1]:,:] = 0
cv2.imshow('temp_image', temp_image)
cv2.waitKey()
cv2.destroyAllWindows()

if __name__=="__main__":
image = cv2.imread(r'./1.png', flags=0)
projection_count = projection(image, direction='col')
split(image, projection_count, direction='col')

image_90 = cv2.imread(r'./2.png', flags=0)
projection_count = projection(image_90, direction='row')
split(image_90, projection_count, direction='row')

这是简单的函数封装,如果有时间可以封装成类,并且解决其中的很多问题。

如果使用该封装,你需要注意 projection、split 两个函数传入的 direction 函数必须相同,否则 where_id 就会对应错误。

5. 文末

对于图像处理,以上的投影和分割只是很小的一部分。不过在实际的使用中,其实可以在以上的步骤中添加更多的方法合理组合使用以应对不同场景。