1 前言
在数控系统中,plt文件是标准的数控加工文件格式。一般可由signMast、文泰等工控软件生成plt加工文件。现在假设电脑或手机上没有工控软件,只有一些描述加工路径的图片,此时可以用opencv提取轮廓来生成加工路径,并将路径保存成plt文件。使用python版的opencv库可以快速搞定这个功能。
2 轮廓的提取
轮廓的提取
先用网上搜到的提取轮廓最简单的几步:
import cv2
import numpy
import os
def show_img(window,img):
cv2.namedWindow(window,0)
cv2.resizeWindow(window,480,640)
cv2.imshow(window,img)
img = cv2.imread("D:/iphone.png")
img = cv2.bitwise_not(img)
#转为灰度图
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#图像二值化
ret,thresh = cv2.threshold(gray,127,255,cv2.THRESH_BINARY| cv2.THRESH_OTSU)
#得到轮廓
contours,hierarchy = cv2.findContours(thresh,cv2.RETR_CCOMP,cv2.CHAIN_APPROX_TC89_L1)
draw_img_1 = numpy.ones(img.shape,dtype=numpy.uint8)
cv2.drawContours(draw_img_1,contours,-1,(0,0,255),1)
show_img('contours',draw_img_1)
cv2.waitKey(0)
原图片 iphone.png:
提取到的轮廓图 draw_img_1:
仔细看可以发现这里为每个图形对象找到了两个轮廓。由于图片中的线条有宽度,所以Opencv会为每个图形对象找到了2个轮廓,分别是线条外边构成的外轮廓和线条另外一边构成的内轮廓。现在的任务就是怎么去掉另外的重复轮廓。
去除重复轮廓
研究cv2.findContours函数:
contours,hierarchy = cv2.findContours(image,mode,method)
输入:
image:带有轮廓信息的图像;
mode:提取轮廓后,输出轮廓信息的组织形式,可以取以下值:
cv2.RETR_EXTERNAL:输出轮廓中只有外侧轮廓信息;
cv2.RETR_LIST:以列表形式输出轮廓信息,各轮廓之间无等级关系;
cv2.RETR_CCOMP:输出两层轮廓信息,即内外两个边界(下面将会说到contours的数据结构);
cv2.RETR_TREE:以树形结构输出轮廓信息。
method:指定轮廓的近似办法,有以下选项
cv2.CHAIN_APPROX_SIMPLE:指定轮廓的近似办法,有以下选项:
cv2.CHAIN_APPROX_NONE:存储轮廓所有点的信息,相邻两个轮廓点在图象上也是相邻的;
cv2.CHAIN_APPROX_SIMPLE:压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标;
cv2.CHAIN_APPROX_TC89_L1:使用teh-Chinl chain 近似算法保存轮廓信息。
输出:
python3里返回2个值:contours,hierarchy
contours:list结构,列表中每个元素代表一个边沿信息。每个元素是(x,1,2)的三维向量,x表示该条边沿里共有多少个像素点,第三维的那个“2”表示每个点的横、纵坐标;
hierarchy:返回类型是(x,4)的二维ndarray。x和contours里的x是一样的意思。如果输入选择cv2.RETR_TREE,则以树形结构组织输出,hierarchy的四列分别对应同级下一个轮廓编号、同级上一个轮廓编号、子轮廓编号、父轮廓编号,该值为负数表示没有对应项。
重点需要关注返回的hierarchy参数。它描述了轮廓的层次信息。当mode采用cv2.RETR_CCOMP参数后,findContours只会取找到两层轮廓。对于ihone.png图片,hierarchy仿真控制台打印结果是:
这里共有8个轮廓。轮廓1的父轮廓是轮廓0;轮廓3的父轮廓是轮廓2;轮廓5的父轮廓是轮廓4;轮廓7的父轮廓是轮廓0。轮廓0,2,4,6都没有父轮廓,意味着他们是最顶级轮廓。由此可以看出这里轮廓0,2,4,6是要提取的轮廓,轮廓1,3,5,7分别是多出来的重复内层轮廓。因此程序中是否可以采用hierarchy数组元素的第4个子元素hierarchy[i][3]为-1的条件来判断,hierarchy[i][3]为-1就是顶层轮廓,只要hierarchy[i][3]不为-1就是内层轮廓就可以去掉该轮廓了吗?但是还需要考虑另外一种情况,看这张abcd.png图片:
提取出来的hierarchy打印如下:
这里字母ABCD共提取了8条轮廓,字母A由2条轮廓,字母B有3条轮廓,字母C由1条轮廓,字母D由2条轮廓。若是采用刚才的判断条件,就只剩下4条轮廓了,这明显不对。在这里的内层轮廓也需要保留。这是因为iphone图片的内层轮廓是由线条宽度引入进来的,和外层轮廓相似度非常高,这张字母图片的内层轮廓是字母固有的内层轮廓,和外层轮廓的差别比较大。可以用轮廓长度的相似度来区分这俩种情况。下面代码当子轮廓和父轮廓轮廓长度比值小于0.8时就认为是不同轮廓。
points_arr = []
for i in range(len(hierarchy[0])-1,-1,-1):
item = hierarchy[0][i]
if item[3] == -1:
cv2.drawContours(draw_img_1,contours,i,(0,0,255),1)
points_arr.append(contours[i])
else:
father_id = item[3]
l1 = cv2.arcLength(contours[father_id],True)
l2 = cv2.arcLength(contours[i],True)
if l2/l1<0.8:
cv2.drawContours(draw_img_1,contours,i,(0,0,255),1)
points_arr.append(contours[i])
绘制加工轨迹
提取出所有轮廓以后,就可以生成加工路径了。每条轮廓都是由加工点构成的闭合路径。为了减小轮廓点数,所以在findContours函数中未采用cv2.CHAIN_APPROX_NONE参数,而是采用cv2.CHAIN_APPROX_TC89_L1近似算法来保留关键拐点。对于同一条轮廓,从第1个点进入以后走完该轮廓所有点,还需要回到最初点才构成闭合路径。当走完第1条轮廓路径以后,接着走第2条轮廓路径。
#绘制线条加工轨迹到draw_img_2中
draw_img_2 = numpy.zeros(img.shape,dtype=numpy.uint8)
for i in range(len(points_arr)):
points = points_arr[i]
old_point = points[0]
for point in points:
cv2.line(draw_img_2,(old_point[0][0],old_point[0][1]),(point[0][0],point[0][1]),(0,255,0),1)
old_point = point
cv2.line(draw_img_2,(old_point[0][0],old_point[0][1]),(points[0][0][0],points[0][0][1]),(0,255,0),1)
if i<len(points_arr)-1:
cv2.line(draw_img_2,(points_arr[i][0][0][0],points_arr[i][0][0][1]),(points_arr[i+1][0][0][0],points_arr[i+1][0][0][1]),(255,0,0),1)
show_img('animation',draw_img_2)
cv2.imwrite('contours2.png',draw_img_2)
生成plt文件
生成plt文件就是跟绘制加工轨迹相似的方法。就是注意下刀和台刀。走刀第每条路径第1个点时必然是抬刀,使用PU;在路径中走动是下刀,使用PD。走完所有路径以后,需要回到(0,max_y)点,这里的max_y是所有加工路径中纵坐标最大值。此外函数中设置了一个比值ratio,代表1像素对应多少毫米。将像素换成毫米。在plt文件中1毫米代表40。这是程序中又乘以了40的由来。
def writePoint(f,point,height,flag,ratio):
if(flag == 1):
f.write('PD%d,%d;' % (int((point[0][1])*ratio*40),int(point[0][0]*ratio*40)))
else:
f.write('PU%d,%d;' % (int((point[0][1])*ratio*40),int(point[0][0]*ratio*40)))
#ratio:,像素到毫米的换算比例,1像素=ratio毫米
def createPltFile(arr,height,ratio):
f = open('text.plt','w+')
f.write('IN;SP1;')
max_y = 0
writePoint(f,points_arr[0][0],height,0,ratio)
max_y = max(points_arr[0][0][0][1],max_y)
for i in range(len(points_arr)):
points = points_arr[i]
first_point = points[0]
for point in points:
writePoint(f,point,height,1,ratio)
max_y = max(point[0][1],max_y)
writePoint(f,first_point,height,1,ratio)
max_y = max(first_point[0][1],max_y)
if i<len(points_arr)-1:
writePoint(f,points_arr[i+1][0],height,0,ratio)
max_y = max(points_arr[i+1][0][0][1],max_y)
writePoint(f,[[0,max_y]],height,0,ratio)
f.write('PG;@@@@@@@@@@@@@@@@@@@;')
完整程序:
import cv2
import numpy
import os
def show_img(window,img):
cv2.namedWindow(window,0)
cv2.resizeWindow(window,480,640)
cv2.imshow(window,img)
def writePoint(f,point,height,flag,ratio):
if(flag == 1):
f.write('PD%d,%d;' % (int((point[0][1])*ratio*40),int(point[0][0]*ratio*40)))
else:
f.write('PU%d,%d;' % (int((point[0][1])*ratio*40),int(point[0][0]*ratio*40)))
#ratio:,像素到毫米的换算比例,1像素=ratio毫米
def createPltFile(arr,height,ratio):
f = open('text.plt','w+')
f.write('IN;SP1;')
max_y = 0
writePoint(f,points_arr[0][0],height,0,ratio)
max_y = max(points_arr[0][0][0][1],max_y)
for i in range(len(points_arr)):
points = points_arr[i]
first_point = points[0]
for point in points:
writePoint(f,point,height,1,ratio)
max_y = max(point[0][1],max_y)
writePoint(f,first_point,height,1,ratio)
max_y = max(first_point[0][1],max_y)
if i<len(points_arr)-1:
writePoint(f,points_arr[i+1][0],height,0,ratio)
max_y = max(points_arr[i+1][0][0][1],max_y)
writePoint(f,[[0,max_y]],height,0,ratio)
f.write('PG;@@@@@@@@@@@@@@@@@@@;')
img = cv2.imread("D:/iphone.png")
img = cv2.bitwise_not(img)
#转为灰度图
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
kernel = numpy.ones((3, 3), numpy.uint8)
img_dilate = cv2.dilate(gray, kernel,iterations = 3)
img_dilate = cv2.erode(gray, kernel,iterations = 3)
#gray = cv2.Canny(gray, 100, 300)
#图像二值化
ret,thresh = cv2.threshold(gray,127,255,cv2.THRESH_BINARY| cv2.THRESH_OTSU)
#得到轮廓
contours,hierarchy = cv2.findContours(thresh,cv2.RETR_CCOMP,cv2.CHAIN_APPROX_TC89_L1)
draw_img_1 = numpy.ones(img.shape,dtype=numpy.uint8)
index = 0
points_arr = []
for i in range(len(hierarchy[0])-1,-1,-1):
item = hierarchy[0][i]
if item[3] == -1:
cv2.drawContours(draw_img_1,contours,i,(0,0,255),3)
points_arr.append(contours[i])
else:
father_id = item[3]
l1 = cv2.arcLength(contours[father_id],True)
l2 = cv2.arcLength(contours[i],True)
if l2/l1<0.8:
cv2.drawContours(draw_img_1,contours,i,(0,0,255),3)
points_arr.append(contours[i])
#cv2.drawContours(draw_img_1,contours,-1,(0,0,255),1)
show_img('input',img)
show_img('gray',gray)
show_img('thresh',thresh)
show_img('contours',draw_img_1)
cv2.imwrite('contours.png',draw_img_1)
#绘制线条加工轨迹到draw_img_2中
draw_img_2 = numpy.zeros(img.shape,dtype=numpy.uint8)
for i in range(len(points_arr)):
points = points_arr[i]
old_point = points[0]
for point in points:
cv2.line(draw_img_2,(old_point[0][0],old_point[0][1]),(point[0][0],point[0][1]),(0,255,0),3)
old_point = point
cv2.line(draw_img_2,(old_point[0][0],old_point[0][1]),(points[0][0][0],points[0][0][1]),(0,255,0),3)
if i<len(points_arr)-1:
cv2.line(draw_img_2,(points_arr[i][0][0][0],points_arr[i][0][0][1]),(points_arr[i+1][0][0][0],points_arr[i+1][0][0][1]),(255,0,0),3)
show_img('animation',draw_img_2)
cv2.imwrite('contours2.png',draw_img_2)
createPltFile(points_arr,img.shape[0],1.0/10)
cv2.waitKey(0)
当读入iphone.png时执行结果:
当读入abcd.png时执行结果: