一、 前言
该项目是我进BYD 以来的第二个义务性的项目,与前一个一样,技术难度都不高,但是看见自己写的代码能帮助到车间的同事,减少他们的工作量,我还是很自豪,正如以前看到的一句话:“代码也是有温度的”,与诸君共勉。
先上图看下待处理的图片样子,如下图: 台面后面可能要变,所以除了晶圆之外的东西先不要管。中间每块白色的小块就是我们检测的对象——芯片。个别芯片上面有黑点点,那是质检的同事做的标记,意为坏芯片。我们的任务就是识别出一块晶圆上的所有坏芯片的位置,并将坏芯片的位置信息写入相关的txt 文件。
该demo 经过两次优化,单帧图片运行时长的优化记录为 1.0+s -> 0.75s -> 0.33s 。检测效果上也较1.0 版提升许多。详细代码见:IGBT项目代码 。由于代码较多,正文中的代码只做简单展示,即只贴出对应操作的核心代码,具体还是下载我的源码来阅读调试。
二、 正文
由上图可看出,芯片很小,整张图片又很大,所以第一步我要将晶圆部分裁剪出来。这里我利用HSV 颜色筛选来做,通过定位黄色区域,裁剪出晶圆部分的图片。 先给出查看图片HSV值的博客 ,用链接里的脚本查看黄色区域的HSV值范围。
(一)颜色初筛、截取出 ROI
img = cv2.imread("../img/%d.jpg" % i)
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
lower_color = np.array([20, 105, 100])
upper_color = np.array([36, 255, 255])
mask_1 = cv2.inRange(img_hsv, lower_color, upper_color)
x1, y1, x2, y2 = get_roi(mask_1)
img_hsv_roi = img_hsv[y1:y2, x1:x2]
最后的效果如下图: 注意,这一步只是初筛,可以看出芯片之间的黄色缝没有很好地检测出来,但是没关系,我们目的是裁剪晶圆的ROI ,如果缝全被检测出来了,下面的ROI 反而不好找,具体你们可以自己试试。
def get_roi(mask, img=None):
_, contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
max_w = 0
max_h = 0
max_x = 0
max_y = 0
for c in contours:
x, y, w, h = cv2.boundingRect(c)
if ((w > max_w) and (h > max_h)):
max_w = w
max_h = h
max_x = x
max_y = y
return max_x, max_y, (max_x + max_w), (max_y + max_h)
经过上面函数的努力,我们得到: 上面这张图只是给你们看下我们这步的目的,其实我只对HSV 图做了裁剪,取出了对应的ROI 区域。下面进行二次颜色筛选。
(二)颜色二次筛选
img_hsv_roi = img_hsv[y1:y2, x1:x2]
lower_color_1 = np.array([18, 85, 30])
upper_color_1 = np.array([40, 255, 255])
mask_2 = cv2.inRange(img_hsv_roi, lower_color_1, upper_color_1)
mask_2 = cv2.medianBlur(np.uint8(mask_2), 5)
h, w = mask_2.shape
最后得到mask_2 的图片为下图,可以看见一些边边角角没处理好,不过没关系,它不是我们的重点,模糊就模糊吧。
(三) 去除晶圆轮廓
接下来我的做法是针对上图的圆形轮廓做个处理,免得其带来干扰,具体为
mask = np.full((img.shape), 255)
h, w = img.shape
radius = max(w, h) // 2
center = (radius, radius)
cv2.circle(mask, center, radius - 10, (0, 0, 0), -1)
img = img + mask
img[img > 200] = 255
img[img <= 0] = 0
效果大致如下图,计划把灰色圆的内部全部用0 填充,圆外部是255 ,这样与原图相加就能得到更干净的芯片mask 图。 最后结果为下图,可以看见,背景前景都干净许多了。
(四) 确定芯片群边界
对上图采用canny 轮廓提取,得到下图,注意,图中的红绿黄白线是我画的,下面会讲它们的作用,原图就是一个个小格子。看起来是不是很接近我们的目标了?接近是接近,但还不是,由于摄像头角度原因,图片有点歪,而且不止是角度歪,芯片群的边界线连起来也不会是个矩形。所以我们需要做透视变换,但做透视变换需要获得四个点的坐标,这就是本节的作用——确定边界,然后确定四个顶点坐标。
大致方法为,用上图中 平行于x 轴的红白二线(看起来不平行是因为我手画没画好),穿过芯片群的轮廓,会得到1、2、3、4 四个交点,其中1、2 连起来就是芯片群左边界,3、4 连起来是芯片群右边界。再画两个平行于y 轴的黄绿二线,同理得到芯片群的上边界、下边界。 再求出上面四条边界线延长线的交点,就可画出芯片群的bbox 框(该框不是矩形框)。 可以看见,边界检测效果还是可以的,即使有偏差,也不是很大。如果你问,上面红绿黄白四根线有可能穿过小格子之间的空隙中,那样不是不准了吗?这也确实是个问题,我的方法是用膨胀方法填满空隙,代码:
img_blur = cv2.medianBlur(np.uint8(img), 5)
canny = cv2.Canny(img_blur, 50, 50)
canny[h - 30:h, :] = 0
kerne_x = cv2.getStructuringElement(cv2.MORPH_RECT,(1, 15))
canny_x = cv2.dilate(canny, kerne_x, iterations = 2)
kerne_y = cv2.getStructuringElement(cv2.MORPH_RECT,(15, 1))
canny_y = cv2.dilate(canny, kerne_y, iterations = 2)
(五) 透视变换
边界知道了,就是做透视变换了,代码不贴了,具体自己去看我开源的代码,效果如下图:
(六) 得到每个芯片的bbox
对上图再做一次canny ,提取其轮廓,如下图 一开始,直接用一根直线穿过每个小格子,取直线上值不为0 的坐标,最后发现较多小格子的边并不是直线,使得最后效果不好。针对该问题,我的方法是,在宽、高方向上,用一个滤波进行过滤,语言不好形容,看代码吧。
h, w = mask_canny.shape
dst_x = mask_canny.copy()
dst_y = mask_canny
max_sum_1 = np.sum(np.full((h, ), 255))
for i in range(w):
temp_sum = np.sum((dst_x[:, i]))
ratio = temp_sum / max_sum_1
if (ratio > 0.2):
dst_x[:, i] = 255
else:
dst_x[:, i] = 0
max_sum_2 = np.sum(np.full((w, ), 255))
for j in range(h):
temp_sum = np.sum((dst_y[j, :]))
ratio = temp_sum / max_sum_2
if (ratio > 0.25):
dst_y[j, :] = 255
else:
dst_y[j, :] = 0
最后的效果如下图,理论上是呈现20 根竖线,图中每根竖线都是每块芯片的x 方向上的边界。之所以会呈现明暗不一的视觉效果,是因为个别边界处的线不止一根(即竖线总数大于20 根),好几根叠加才显得亮,多余的线对我们来说是噪声,所以要去掉它们。为此我采用的方法是聚类,直接聚为20 个类,类中心就是我们最后输出的每块芯片边界。y 方向上处理方法与之相同。 根据坐标,画出每块芯片的bbox,效果为下图,可以看出边界检测效果还是可以的。 剩下的芯片识别部分就不写了,个人以为没什么东西了,想必大伙都知道怎么搞,不班门弄斧了。如果觉得我讲的不清楚,可以去看看源码,想必会更加清楚。
三、 后记
仓促之下写成,如有错误,还望帮忙雅正,谢谢!
|