引言
近来琐事繁多,疏于整理手头的工作,实在是有违当初定下的目标。深知已错失种一棵树的最好时机,那么当下必须有所行动了。悟以往之不谏,知来者之可追。
本篇主要介绍太阳能面板外观瑕疵的图像识别方法,希望能对类似场景的需求实现有所启示或借鉴。
问题描述
太阳能面板外观瑕疵的检测是生产过程中必不可少的一步,目前是与EL检测同步,现场质检员在处理EL图像的同时对外观图像的瑕疵进行甄别并做进一步处理。与EL环节不同的是外观图像是普通相机成像而非红外,且瑕疵类别较为复杂、细小。但就异物来说可能存在锡渣、纸片、玻璃渣、甚至头发丝等多种情况,且大小、分布均是不可预知的。另外还有间距异常、型材脏污、划伤、结晶、错位等。细分起来约有接近二十个小类。下面贴两张异常样本感受下:
P1-间距异常-A7A8位置
P2-异物-玻璃渣C10
最终目的希望找出瑕疵的大概位置。由于不可抗因素导致瑕疵样本极其稀缺,想收集大量有效样本更是难上加难,深度学习所要求的样本量是极难满足的。因此这里采用传统图像处理方法尝试解决一部分问题。
下面以P1图的间距问题为例,讨论处理过程,识别异物的思路也基本类似。
思路
首先为了后期处理的方便可以先对原图进行切边处理,如下图所示,切出红线外的部分。实际上间距、异物等瑕疵大概率发生在电池片区域内,因此可以首先挖出电池片区域,再进行后续处理。这样做的好处:一是无需考虑灰白玻璃背板条带的影响易于选取更加合适的处理参数;二是易于对电池片进行切割分块处理,从而易于识别电池片上的极小瑕疵。
ROI区域
总体目标是希望通过各种处理、运算,凸显瑕疵特征,过滤干扰因素。从灰度图开始,经过二值化、腐蚀、膨胀等一系列形态学运算应该就能得到想要的效果,最后可以通过对水平或竖直方向的像素做投影,找出瑕疵特征进行判别和位置确定。
需要注意的是应该根据实际情况,选择最合适的阈值使其具有更广泛的处理能力。某些参数对某张图像处理效果可能是非常好的,对其他的图像处理效果就差很多,而有些参数对图像处理效果达不到完美但能达标且对多个图像均适用,因此最终确定参数时需要折中考虑。
对于间距问题的识别不做切边也是可以做的,影响不大,对于异物最好是切成电池片处理。
过程
- 读取图像-切边预处理
该部分的思路是通过开闭运算形成界限清晰的不同区域,从而提取电池片所在的ROI。代码如下:
import cv2
#import utils
import numpy as np
from imutils import contours
from skimage import measure ,exposure
from PIL import Image,ImageEnhance
from collections import Counter
def _black_edges(image):
height, width = image.shape[:2]#读取尺寸
size = (int(width * 0.25), int(height * 0.25))#缩小至1/4大小
shrink = cv2.resize(image, size, interpolation=cv2.INTER_AREA)
gray = cv2.cvtColor(shrink, cv2.COLOR_BGR2GRAY)#灰度图
ret2, image_binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)#产生二值化阈值
ret, binary = cv2.threshold(gray, ret2 * 0.85, 255, cv2.THRESH_BINARY)#二值化
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (8, 8))#定义形态学运算的卷积核形状,可根据实际调整
iOpen = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)#开运算
iClose = cv2.morphologyEx(iOpen, cv2.MORPH_CLOSE, kernel)#闭运算-结果见下图P3
height,width = iClose.shape
# 下面就是根据闭运算结果寻找电池片区域的上下左右分割点,依据行列中0的占比进行判断,注意需要设定起始条件incol、flag避免错误的触发阈值条件
xx = [];yy=[];incol = 'nan';flag = True
for x in range(width):
if flag == True and Counter(iClose[:,x])[0]/height<=0.1:
incol = 1
flag = False
elif incol==1 and Counter(iClose[:,x])[0]/height > 0.50:
xx.append(x)
incol = 0
elif incol==0 and Counter(iClose[:,x])[0]/height<0.1:
xx.append(x)
incol = 'nan'
break
for y in range(height):
if flag == False and Counter(iClose[y])[0]/width<=0.1:
incol = 2
flag = True
elif incol==2 and Counter(iClose[y])[0]/width > 0.50:
yy.append(y)
incol = 3
elif incol==3 and Counter(iClose[y])[0]/width<=0.1:
yy.append(y)
break
return image[yy[0]*4:yy[1]*4,xx[0]*4:xx[1]*4]
image=cv2.imread("A10190700300743.jpg")
ims = _black_edges(image)#切边后的图像如下图
P3-对图像P1的闭操作结果
P4-对图像P1的切边后结果
- 电池片切分(间距识别无需切分-仅针对异物识别)
去除白边后再切分就比较好办了,根据行列的电池片数量,等分切割,实际计算电池片尺寸时向整数近似,切出的效果可能略有偏差但不会太大。代码如下:
ims = _black_edges(image)
#cv2.imwrite('ims.jpg',ims)
height,width,t = ims.shape
w=12;h=6
item_width = int(width / w)
item_height = int(height /h)
for j in range(0,w):
for i in range(0,h):
imbox = ims[i*item_height:(i+1)*item_height,j*item_width:(j+1)*item_width]
cv2.imwrite('cut_' + str(i+1)+'_'+str(j+1) + '_result.jpg',imbox)
切出的效果如下:
切分后的电池片(cut_6_4_result.jpg)
对异物瑕疵的识别,这里仅针对大概率出现在电池片区域内的,对于可能落在电池片边界的异物可能会随着电池片切分而被裁切,大的话也还可以识别,太小可能就难了。切分后的电池片需要逐个输入写好的检测步骤。过程与检测间距是类似的。
- 二值化
对切边后的图像灰度化后做二值化处理:
image= ims
#image=cv2.resize(ims,(1500,800))#尺寸调整
gray=cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
cv_show('gray',gray)
ref_num =cv2.threshold(gray,65,255,cv2.THRESH_BINARY_INV)[1]#需选择合适的阈值,多试几次
cv_show('ref2',ref_num)
cv2.imwrite('b.jpg',ref_num)
P5-对P4的二值图
- 腐蚀操作:
kernel2 = cv2.getStructuringElement(cv2.MORPH_RECT, (5,1))
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2,2))
kernel3 = cv2.getStructuringElement(cv2.MORPH_RECT, (2,5))
#横向腐蚀
erode = cv2.erode(ref_num, kernel2,iterations=3)
cv2.imwrite('erode.jpg',erode)
P5-对P4的腐蚀操作
腐蚀的目的是突出纵向线条,当然这不是必须的步骤,但处理后能使得像素投影图的区分特征更加明显,更易于设计过滤规则。包括下面的梯度运算等也非必须。这里直接给出处理效果:
#横向梯度
tidu = cv2.morphologyEx(erode, cv2.MORPH_GRADIENT, kernel2,iterations=3)
cv2.imwrite('tidu.jpg',tidu)
#纵向腐蚀去除噪点
erode2 = cv2.erode(tidu,kernel,iterations=3)
cv2.imwrite('erode2.jpg',erode2)
#反二值化
f2=cv2.threshold(erode2,85,255,cv2.THRESH_BINARY_INV)[1]
cv2.imwrite('f2.jpg',f2)
#闭运算
gradX=cv2.morphologyEx(f2,cv2.MORPH_CLOSE,kernel3,iterations=3)
cv2.imwrite('gradX.jpg',gradX)
P6-gradX.jpg
- 像素投影分析(统计0和255的个数)
纵向的间距问题做垂直投影即可,分析横向的像素规律需要做水平投影。以下分别给出绘制垂直、水平像素投影代码:
#垂直投影
def czty(binary):
height, width = binary.shape[:2]
v = [0]*width
a = 0
for x in range(0, width):
for y in range(0, height):
if binary[y,x] == 0:
a = a + 1
else :
continue
v[x] = a
a = 0
emptyImage = np.zeros((height, width, 3), np.uint8) #为便于观察效果这里生成图像,实际操作中可将规则判断与投影写在同一个函数
for x in range(0,width):
for y in range(0, v[x]):
b = (255,255,255)
emptyImage[y,x] = b
cv2.imwrite('czty.jpg',emptyImage)
#水平投影
def spty(binary):
height, width = binary.shape[:2]
a = 0;z = [0]*height
emptyImage = np.zeros((height, width, 3), np.uint8)
for y in range(0, height):
for x in range(0, width):
if binary[y,x] == 0:
a = a + 1
else :
continue
z[y] = a
a = 0
for y in range(0,height):
for x in range(0, z[y]):
b = (255,255,255)
emptyImage[y,x] = b
cv2.imwrite('spty.jpg', emptyImage)
P7-对P5的垂直像素投影
至此,基于P7的结果结合特定的判出规则即可识别间距问题。规则比较简单,可以先找到白线的入口与出口,判断每个白线长度是否符合判出标准,比如小于纵向长度的5/6。
小结
本篇主要探讨了太阳能面板的外观图像瑕疵中纵向间距问题的识别方法,实际上由于两块电池片距离太近导致两者之间的汇流条被覆盖,在图像上就表现为纵向亮线的缺失。异物的识别也是类似的,对图像进行一系列的形态学处理,使之突出瑕疵特征,最后做像素投影判断瑕疵位置。
需要指出的是传统算法的弊端在于泛化能力太差,参数的鲁棒性不足;原始图像的质量和形态学处理的方式方法决定了参数的适应性。对图像的处理方式虽因人而异但大方向应该是一致的,但实际中原始图像的质量情况往往是比较复杂,受制于多种因素。如果你有足够的样本量还是首选深度学习方案。
网友评论