【无标题】

OpenCV-python, 使用K-means聚类分割得到想要的矩形物体

一 、聚类算法

OpenCV 自带K-means聚类算法,该算法是一种无监督学习算法,用于对数据进行聚类,将相似的数据归为一类。K-means算法的主要思想是:给定一个数据集和K个初始的聚类中心,将所有的数据点分配到距离它最近的聚类中心,然后重新计算每个聚类的中心,不断迭代,直到聚类中心不再发生变化或者达到预先设定的最大迭代次数为止。

具体实现时,K-means算法首先随机选择K个初始聚类中心,然后迭代执行以下步骤:首先将所有的数据点分配到距离它最近的聚类中心,形成K个聚类,然后重新计算每个聚类的中心,得到新的K个聚类中心。接着再将所有数据点重新分配到距离它最近的新聚类中心,直到聚类中心不再发生变化或达到最大迭代次数为止。

K-means算法的优点是简单易用,计算效率高,对于大规模数据集也可以有效处理。缺点是需要事先确定聚类的数量K,而且初始的聚类中心对聚类结果影响较大,有可能收敛到局部最优解。

在图像处理任务中,可以使用K-means对彩色图的每一个像素点的色彩数值进行种类划分后,依据每个像素点的数据对该图进行图像分割。

在OpenCV中的示例

1) 在BGR通道的聚类结果

在网上的示例中,一般直接将OpenCV读取的彩色图中的BGR通道中像素数据拉直(即只保留B、G、R的数值,不保存像素点位置),然后再进行聚类。如下代码所示:

import cv2
import numpy as np

"""
    读取图像并进行4倍降采样,缩小运算量
"""
img_dir = ".\\PCB.jpg"
img_org = cv2.imread(img_dir)
time_start = time.time()
img_down_x2 = cv2.pyrDown(img_org)
img_down_x4 = cv2.pyrDown(img_down_x2)

"""
    将图像的B、G、R像素值展平,抹除像素值的位置信息,转换到x,y,z三维空间
"""
Z = img_down_x4.reshape((-1, 3))

"""
    对三维空间中的像素值进行聚类
"""
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
K = 2	# 因为只要划分前景(物体)和背景,所以将聚类中心数设为2
ret, label, center = cv2.kmeans(Z, K, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)

"""
    得到并显示聚类结果
"""
center = np.uint8(center)
res = center[label.flatten()]
res2 = res.reshape(img_down_x4.shape)
cv2.imshow("BGR Cluster Result", res2)
cv2.waitKey(0)

读取的图像如下:
原图

输出的图像如下:
BGR通道的聚类结果
但是从上图红框部分可以看出,前景(PCB电路板)和浅色背景(桌面)之间的分离并不彻底,部分背景区域被错误划成了前景。这是由于在BGR图中,PCB电路板的部分阴影和电路板的绿色像素值较为相似导致的。这一问题可以通过先将图片转到HSV色域,再对HSV色域上的像素点进行聚类来缓解。

2) 在HSV通道的聚类结果

import cv2
import numpy as np

"""
    读取图像并进行4倍降采样,缩小运算量
"""
img_dir = ".\\PCB.jpg"
img_org = cv2.imread(img_dir)
img_down_x2 = cv2.pyrDown(img_org)
img_down_x4 = cv2.pyrDown(img_down_x2)

"""
    将BGR图像先转到HSV空间,再展平
"""
img_down_x4 = cv2.cvtColor(img_down_x4, cv2.COLOR_BGR2HSV)
Z = img_down_x4.reshape((-1, 3))
# convert to np.float32
Z = np.float32(Z)

"""
    对HSV空间中的像素值进行聚类
"""
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
K = 2
ret, label, center = cv2.kmeans(Z, K, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)

"""
    得到并显示聚类结果
"""
# Now convert back into uint8, and make original image
center = np.uint8(center)
res = center[label.flatten()]
res2 = res.reshape(img_down_x4.shape)
res2 = cv2.cvtColor(res2, cv2.COLOR_HSV2BGR)
cv2.imshow("HSV Cluster Result", res2)
cv2.waitKey(0)

聚类输出的图片如下:
HSV色彩空间的图片聚类结果
从上图可以看出,虽然在红色矩形框区域中的前景和背景的像素划分仍有误判,但是比直接在BGR通道图片上进行聚类的结果好了不少。可以通过形态学操作得到更干净的前景和背景。

二、形态学操作

在前一步的聚类过程中,通过对在HSV通道中的图像进行聚类,完成的初步的前景、背景划分,但是在得到的图像中仍有部分区域(如浅色的元器件、丝印、还有部分阴影区域等)被错误的划分了。对于这种零散的,不相连的块状区域,可以使用开、闭操作相结合的方式,完成对它们的滤除。

1)闭运算

"""
    对聚类结果进行闭运算
"""
kernel1 = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
seg_close = cv2.morphologyEx(res2, cv2.MORPH_CLOSE, kernel1, iterations=3)
cv2.imshow("cluster_close", seg_close)
cv2.waitKey(0)

闭运算
可以看到,经过闭运算之后,刚才在HSV聚类中阴影部分的孤立像素被消除了。但原本那些元器件部分的区域也有更多被划为了背景,这个可以通过开运算来改善。

2)开运算

"""
 对闭运算结果进行开运算
"""
kernel2 = cv2.getStructuringElement(cv2.MORPH_RECT, (13, 13))
seg_open = cv2.morphologyEx(seg_close, cv2.MORPH_OPEN, kernel2, iterations=7)  # 弥合孔隙
cv2.imshow("cluster_close_open", seg_open)
cv2.waitKey(0)

开运算
在经过开运算之后,已经得到了较为干净的前景区域了。接下来要对开运算的结果进行处理,得到该区域的最小外接矩形。

三、寻找最小外接矩形

在OpenCV-python中,可以使用函数cv2.findContours()来搜寻一个二值化图像的边框,在得到物体的边框后,然后再通过cv2.minAreaRect()搜索边框中的最小外接矩形。废话不多说,直接上代码。

"""
  搜寻带方向角的最小外界矩形
"""
_, img_binary = cv2.threshold(seg_open, thresh=100, maxval=255, type=cv2.THRESH_BINARY_INV)
img_gray = cv2.cvtColor(img_binary, code=cv2.COLOR_BGR2GRAY)
cv2.imshow("cluster_close_open_binary.jpg", img_gray)
img, contours, hierarchy = cv2.findContours(img_gray, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

for c in contours:
    # 最小外接矩形框,有方向角
    rect = cv2.minAreaRect(c)
    box = cv2.boxPoints(rect)
    box = np.int0(box)
    cv2.drawContours(img_down_x4, [box], 0, (255, 255, 255), 2)

result = cv2.cvtColor(img_down_x4, cv2.COLOR_HSV2BGR)
cv2.imshow("cluster_close_open_binary_rectangle.jpg", result)
cv2.waitKey(0)

在这段代码中,首先对图像进行了二值化处理,然后将二值化之后的图转为了单通道灰度图(cv2.fingContours()只能寻找二值化灰度图的外轮廓),再使用cv2.minAreaRect()函数搜寻轮廓中的最小旋转外接矩形,结果如下图所示。
最小外接矩形
效果还不错,能够较为准确的找到PCB板的边框(在PCB板没有发生畸变的情况下,比如PCB板是从正上方俯视拍摄的就可以使用这个方法寻找PCB板的最小外接矩形;但如果PCB板是从侧面拍摄的,就需要用到透视变换了,这是另一套处理流程。)

四、使用仿射变换摆正图像,然后裁剪得到PCB板的主体

1)仿射变换

仿射变换的变换矩阵分为4种,使用中学知识就能推导出来了,大家感兴趣可以自己试着推推。分别为:

a、平移:

平移变换可以通过将图像沿着 x x x轴和 y y y轴移动 d x d_x dx d y d_y dy来实现。其变换矩阵为:
[ 1 0 d x 0 1 d y 0 0 1 ] \begin{bmatrix} 1 & 0 & d_x\\ 0 & 1 & d_y\\ 0 & 0 & 1 \end{bmatrix} 100010dxdy1

b、旋转:

旋转变换可以通过将图像绕着原点逆时针旋转 θ \theta θ度来实现。其变换矩阵为:
[ c o s ( θ ) − s i n ( θ ) 0 s i n ( θ ) c o s ( θ ) 0 0 0 1 ] \begin{bmatrix} cos(\theta) & -sin(\theta ) & 0\\ sin(\theta ) & cos(\theta) & 0\\ 0 & 0 & 1 \end{bmatrix} cos(θ)sin(θ)0sin(θ)cos(θ)0001

c、缩放:

缩放变换可以通过将图像沿着 x x x轴和 y y y轴缩放 s x s_x sx s y s_y sy倍来实现。其变换矩阵为:
[ s x 0 0 0 s y 0 0 0 1 ] \begin{bmatrix} s_x & 0 & 0\\ 0 & s_y & 0\\ 0 & 0 & 1 \end{bmatrix} sx000sy0001

d、剪切:

剪切变换可以通过将图像在 x x x轴上剪切 h x h_x hx,在 y y y轴上剪切 h y h_y hy来实现。其变换矩阵为:
[ 1 h x 0 h y 1 0 0 0 1 ] \begin{bmatrix} 1 & h_x & 0\\ h_y & 1 & 0\\ 0 & 0 & 1 \end{bmatrix} 1hy0hx10001
由于PCB板的图像是从正上方拍摄的,透视畸变较小,因此无需对图像进行透视变换。只需要使用仿射变换中的旋转变换就能将图像摆正位置,在OpenCV-python可以通过函数cv2.getRotationMatrix2D()获取图像的旋转矩阵,再使用函数cv2.warpAffine()对图像进行仿射变换,如下所示:

"""
    旋转图像,摆正位置
"""
rect_center = rect[0]	# 获取矩形的中心坐标
rect_wh = rect[1]	# 获取矩形的长宽
rect_angle = rect[2]	# 获取矩形的旋转角

M_rotate = cv2.getRotationMatrix2D(center=(int(rect_center[0]*2), int(rect_center[1]*2)),
                                   angle=90+rect_angle, scale=1)
img_rotate = cv2.warpAffine(img_down_x2, M=M_rotate, dsize=(img_down_x2.shape[1], img_down_x2.shape[0]))
cv2.imshow("cluster_close_open_binary_rectangle_rotate", img_rotate)
cv2.waitKey(0)

具体实现效果如下图所示:
旋转后的图像

2) 图像裁剪

在OpenCV-python读取的图像中,图像的type为array,因此可以使用对array切片的方式完成图像的裁剪。由于在rect中存储了旋转矩形框的中心坐标、矩形长宽的信息,因此只要按照这些信息对图像进行裁剪就可以了。如下代码所示。

"""
    裁剪图像,获取ROI
"""
x_index_start = int(rect_center[0] - rect_wh[1] / 2)
x_index_end = int(rect_center[0] + rect_wh[1] / 2)
y_index_start = int(rect_center[1] - rect_wh[0] / 2)
y_index_end = int(rect_center[1] + rect_wh[0] / 2)
img_cutted = img_rotate[y_index_start*2:y_index_end*2, x_index_start*2:x_index_end*2]

cv2.imwrite(".\\cluster_close_open_binary_rectangle_rotate_cut.jpg", img_cutted)
cv2.imshow("cluster_close_open_binary_rectangle_rotate_cut", img_cutted)
cv2.waitKey(0)
cv2.destroyAllWindows()

经过这一系列处理后,最后得到的PCB区域如下图所示:
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值