之前用Matlab做图像较多,最近准备学习用opencv和python进行图像处理,并就网上的实际案例进行了练手,其中的一篇扫描件切边案例是:opencv之案例实战-扫描件切边。仅当作学习,不喜勿喷!
本文链接:基于OpenCV4.x + Python3.7的文件扫描件切边实践
测试图像两种:(1)边缘整齐+倾斜;(2)边缘添加干扰+倾斜。具体图像如下所示。
1.具体思路
(1)边缘检测后进行孔洞填充,得到初始文件区域(包含其他连通域);
(2)取最大的连通域作为文件区域,此时其他连通域已被过滤去除;
(3)霍夫直线检测,以kmeans方法挑选出4类直线;
(4)4条筛选出的直线求交点,并以此对原图做仿射变换,得到校正后的图像;
(5)用上面的4个交点求仿射变换后的4个新点坐标,并以此切边获得最终结果图像
2.实现过程
2.1 边缘检测+孔洞填充
采用canny边缘检测,该算子以及用法在此不用详述;孔洞填充,opencv和python好像还没有可以直接用的函数(可能我还没发现,可以补充),孔洞填充函数如下,其中用到了cv.floodFill自带函数。(fillimg()是借鉴别人的,链接不知道了,下次碰见附上链接):
import cv2 as cv
import numpy as np
def fillimg(img):
img1 = img.copy()
h,w = img.shape[:2]
mask = np.zeros((h+2,w+2),np.uint8)
cv.floodFill(img1,mask,(0,0),255)
img2 = cv.bitwise_not(img1) #取反
out = img|img2 # 或操作
return out
该过程实现如下:
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
edge = cv.Canny(gray,th,2*th) #canny边缘检测
ele = cv.getStructuringElement(cv.MORPH_RECT,(3,3))
bw = cv.morphologyEx(edge,cv.MORPH_DILATE,ele) #形态学处理:膨胀
bw = fillimg(bw) #孔洞填充
结果见下:
左边:边缘不整齐; 右边:边缘整齐
2.2 最大连通域获取
最大连通域,首先查找轮廓,然后遍历轮廓面积,得到最大面积区域的联通域进行填充,便是文件区域,其中用到的函数:cv.findContours,cv.contourArea() ,以及cv.fillConvexPoly()。
def maxAreaContour_ROI(bw):
out = np.zeros_like(bw,np.uint8)
contour,her = cv.findContours(bw,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE) #找轮廓
area=[]
for i in range(len(contour)):
area.append(cv.contourArea(contour[i])) #找最大面积轮廓
max_ind = np.argmax(area)
angl =( cv.minAreaRect(contour[max_ind])[2]) #z最小矩形分析得到偏传角度,后续有用处的
cv.fillConvexPoly(out,contour[max_ind],255) #区域内填充白色
return out,angl
处理后的结果如下:
左边:边缘不整齐; 右边:边缘整齐
2.3 霍夫直线检测求交点
霍夫直线求出的直线有很多条,我们需要进行分类,通过kmeans分成4类点。其中用到的函数有:cv.HoughLinesP(),cv.kmeans() ,
注意kmeans输入数据是N行2列。
实现过程如下:
def getLines(bw,N,th,lineLength,angle):
h,w = bw.shape[:2]
edge = cv.Canny(bw,th,2*th)
lins = cv.HoughLinesP(edge,1,np.pi/180,10,minLineLength =lineLength,maxLineGap=1)
lin =lins[:,0,:] #注意数据的顺序
criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 10, 1.0)
#drawLine(img,lin)
flags = cv.KMEANS_RANDOM_CENTERS
temp = (lin[:,:2] - lin[:,2:-1]).astype(np.float32) #将4维转为2维,作为后续输入
compactness,labels,centers = cv.kmeans(temp,4,None,criteria,10,flags) # Kmeans聚类
points =[]
for j in range(N):
points.append(np.mean(lin[np.where(labels==j)[0],:],axis = 0))
#每类直线求均值,4行4列
points = np.array(points)
center = ((points[:,:2] + points[:,2:-1])/2).astype(np.float32) # 直线中点,4行2列
points_up=[] #上
points_down=[] #下
points_left=[] #左
points_right=[] #右
for i in range(N):
t =points[i]
d = (np.arctan2((t[3]-t[1]),(t[2]-t[0]))/np.pi*180)
d_angle = abs(d-angle)
# 通过最小矩形求得的转转角度做参考,偏传20度内的是上下边缘,偏传70度内的是左右边缘
# 并看中点横纵坐标与图像行列关系,具体确定上下左右边缘点(此方法可能不可靠,目前表现OK)
if d_angle <20 and center[i][1]>h/2 :
points_up.append([t[0],t[1]])
points_up.append([t[2],t[3]])
elif d_angle <20 and center[i][1] 70 and center[i][0]
2.4 拟合直线求交点,求仿射变换矩阵
上面已经得到了四类边缘点,此时只需要拟合直线再求交点;以四个交点的中心坐标和一条上或下边缘的角度作为仿射变换参数矩阵的中心点和旋转角度。其中,用到的函数有:cv.fitLine(),cv.getRotationMatrix2D()。
实现过程如下:
def getCrossPoint(points1,points2): #求直线交点
points1 = np.array(points1)
points2 = np.array(points2)
out1 = cv.fitLine(points1,cv.DIST_L2,0,0.1,0.1) #拟合直线
out2 = cv.fitLine(points2,cv.DIST_L2,0,0.1,0.1)
k1 = out1[1]/out1[0] # 通过直线(k,b)求直线交点,注意k=0,以及k为无穷时候情况
k2 = out2[1]/out2[0] # 本案例中未对k值进行讨论,注意,注意,注意
x3 =((out2[3] - k2*out2[2])-(out1[3] - k1*out1[2]))/(k1-k2)
y3 = k1*(x3-out1[2])+out1[3]
return x3,y3
u_lx,u_ly = getCrossPoint(u,l) #左上点
u_rx,u_ry = getCrossPoint(u,r) #右上点
d_lx,d_ly = getCrossPoint(d,l) #左下点
d_rx,d_ry = getCrossPoint(d,r) #右下点
p4 =[[u_lx,u_ly],[u_rx,u_ry],[d_lx,d_ly],[d_rx,d_ry ]] # 格式转换
center0 = np.mean(p4,axis=0) # 旋转中心
angle0 = np.arctan2((u_ly-u_ry),(u_lx-u_rx))/np.pi*180-180 # 旋转角度
rot = cv.getRotationMatrix2D((center0[0],center0[1]),angle0,1.0) # 变换矩阵
实现结果如下:
左边:边缘不整齐; 右边:边缘整齐
2.5 四个交点仿射变换
只需要将上面的四个交点按照仿射变换矩阵进行变换,会得到4个新的图像坐标,然后在仿射变换后的图像中裁剪出文件区域即可。这里我将4个点坐标用cv.warpAffine() 变换,企图得到新的坐标,可是这样做的结果坐标是错的,可能是我还没有弄清楚。这里自己定义了变换函数:Rote()。
该变换矩阵是:
def Rote(points,rot):
out=[]
for i in range(len(points)):
x = rot[0][0]*points[i][0]+rot[0][1]*points[i][1]+rot[0][2] # xc =a*x+b*y+c
y = rot[1][0]*points[i][0]+rot[1][1]*points[i][1]+rot[1][2]
out.append([x,y])
return out
Rote函数变换结果如下:
(1)边缘不整齐图片从倾斜状态下的4点(A)经过仿射变换后的新4点(B):
对原图进行仿射变换的结果如下:
outpoint = np.array(Rote(np.array(p4)[:,:,0],rot)).astype(np.int) #Rote函数变换
dst = cv.warpAffine(img,rot,img.shape[:2],borderValue=(255,255,255)) # 仿射变换
2.6 切边
直接根据B矩阵的4点坐标进行切边,形如img[y0:y1,x0:x1]操作,实现如下:
min_x = np.min(outpoint[:,0]) # 最大最小横纵轴,矩形框范围
min_y = np.min(outpoint[:,1])
max_x = np.max(outpoint[:,0])
max_y = np.max(outpoint[:,1])
OUT = dst[min_y:max_y,min_x:max_x] #最终结果
实现结果如下:
3 结论
终于写完了,从实现到编辑为文档,好累了!我在边缘上加了一些橙色形成非整齐的边缘,以此与整齐的边缘图形对比,其结果显示采用特定的方法可以很好的做到非整齐边缘文件的检测,但还是存在一些边缘误差(上面B矩阵中的X轴坐标109和112,注意结果图中非整齐边缘的蓝色边不是切边后的边缘,实际切边是x=min(109,112))。若有不正确或者好注意的地方,希望进行交流。完整的程序代码可以自行编写,基本上重要的都已经说到。