MTP 写字机器
目标
无意中看到下面视频,我打算也实现一个类似机器
视频、视频2、视频3
来源于油管Creativity Buzz的创意,顺便了解到有家AxiDraw公司在生产这种机器,淘宝上也有售卖。
想法
观看视频可以发现,这个机器的原理类似于数控机床,笔尖根据预先设定好的程序、方案运动,绘制出图案。关键是如何根据图案或字符得到三个步进电机的运动控制信号。为了快速实现一个简易系统,先不考虑如何根据字符得到三个步进电机的控制信号,只考虑如何根据图案得到三个步进电机的控制信号。而且此机器不能中途换笔,故画笔只有一种颜色。
硬件
因为涉及图像处理,我选择树莓派作为控制板。上位机+stm32也是一种可行的好方案。
机械结构
参考视频的机械结构,购买了以下配件,自行淘宝3D打印机配件
丝杆、光杆套装各两套
A4988步进电机驱动板2块点我看使用方法
42步进电机2个
效果图2019.01.20
我对木工不太熟,这构造和预期有点出入。
软件规划
由以上分析知系统的输入为图片,输出为三个步进电机的运动控制信号。采用模块化分析方法,应构造以下模块
为便于分析,长度单位都为mm。设机器笔尖最大绘制范围为xy的矩阵,矩阵原点在机器极限位置左边距离n,上边距离m处。称矩阵原点为绘制起点。设笔尖宽度为q,则矩阵中有x/qy/q个像素点。
图像处理模块
输入:图片
输出:尺寸为x/q*y/q的灰度矩阵
功能:
- 灰度化图片
- 确定图片右下角与绘制起点的相对位置
- 根据设定要求放缩图片,使图片整体位于绘制范围内部
- 输出尺寸为x/q*y/q的灰度矩阵
构造控制信号模块
方案一
输入:尺寸为x/q*y/q的灰度矩阵
输出:描述笔尖运动的矩阵
功能:绘制图案时,机器将从绘制起点(0,0)开始绘制图案,先向y轴运动绘制x=0处的图案,然后向快速到(1,0)处向y轴运动绘制x=1处的图案。(注意回程误差)这样就把二维图像变成了一维图像。对于每一行,值大于0的数字连成几条线段,记录线段的起点、终点。构造一个如下所示的矩阵,每一行代表一条线段,左右分别是起点终点。
#作废
[
[(l1,l2),(l3,l4)],
[(l5,l6),(l7,l8)],
...
]
考虑到以后扩展的需要,决定记录画线类型、画线所需信息。目前只实现画直线
#n*5
[
[1 23 43 23 46] #从(23,43)画直线到(23,46)
]
方案二
输入:尺寸为x/q*y/q的矩阵
输出:NC程序代码
暂略
仿真模块
为了检查以上步骤软件是否写正确,简单编写了一个仿真模块
输入:描述笔尖运动的矩阵
输出:绘制的图案
效果如下,右边是原图,左边是根据描述笔尖运动的矩阵绘制的图案,所以从原理上来说这个机器是可能实现的。因为步长设置比较大,看起来失真有点严重
电机控制模块
输入:描述笔尖运动的矩阵
输出:三个控制步进电机的脉冲信号
功能:读取一组数据,运动到起点位置,下笔,运动到终点,提笔。
读取下一组数据,循环往复直至读完。
最后笔尖回到绘制起点。
代码
最新版本代码托管在Github
测试代码
#coding:utf-8
from IMPlib import IMP
from SGNlib import SGN
from SIMlib import SIM
#from CTRlib import CTR
import cv2
import numpy as np
#图像处理后得到的灰度图
outcome=IMP().getResult()
sgn=SGN()
sgn.setImgPixels(outcome)
outcome1=sgn.getResult() #得到的控制代码
#np.savetxt("c.txt",outcome1, fmt="%d", delimiter=",")
np.save("code.npy",outcome1)
print (outcome1.shape)
#仿真
sim=SIM()
sim.setImgPixels(outcome)
sim.setCode(outcome1)
outcome2=sim.analyse()
outcome=255-outcome
outcome2=255-outcome2
#ctr=CTR()
cv2.namedWindow("Image")
cv2.namedWindow("Image2")
cv2.imshow("Image", outcome)
cv2.imshow("Image2", outcome2)
cv2.waitKey (0)
cv2.destroyAllWindows()
图像处理
#coding:utf-8
import cv2
import numpy as np
# pip install opencv-python
class IMP:
'''
image process
输入:图片、背景图片大小、缩放比例、相对位置
输出:尺寸为x/q*y/q的灰度矩阵
功能:
灰度化图片
确定图片右下角与绘制起点的相对位置
根据设定要求放缩图片,使图片整体位于绘制范围内部
输出尺寸为x/q*y/q的灰度矩阵
'''
backgroundSize=np.asarray([210,150])#[297,210]
penLineSize=0.5
imgPath="MTP//girl3.jpg"
scaling=0.3
position=np.asarray([1,1])
def __init__(self):
## 创建空白背景图片
self.backgroundPixels=self.backgroundSize/self.penLineSize
self.backgroundPixels = np.zeros(self.backgroundPixels.astype(int).tolist(), np.uint8)
self.backgroundPixelsX,self.backgroundPixelsY=self.backgroundPixels.shape
## 读取目标图片
targetImg= cv2.GaussianBlur(cv2.imread(self.imgPath,0),(5,5),1.5)#高斯滤波
targetImgHeight,targetImgWidth=targetImg.shape
self.targetImg=255-cv2.resize(targetImg,(int(targetImgWidth*self.scaling),int(targetImgHeight*self.scaling))) #缩放
self.targetImgPixelsX,self.targetImgPixelsY=self.targetImg.shape
## 将目标图片放置到黑色空白背景图片上,并指定相对位置
'''
cv2.namedWindow("Image")
cv2.imshow("Image", outcome)
cv2.waitKey (0)
cv2.destroyAllWindows()
'''
def replace(self):
#输出
outcome = np.zeros(self.backgroundPixels.shape, np.uint8)
#确定背景、图片右下角的相对位置(像素)
positionPixelX,positionPixelY=(self.position/self.penLineSize).astype(int).tolist()
#循环变量,x和y确定背景中的一个像素点,indexX和indexY确定图片中的一个像素点
x,y=(self.backgroundPixelsX-positionPixelX+1,self.backgroundPixelsY-positionPixelY+1)
indexX,indexY=(self.targetImgPixelsX-1,self.targetImgPixelsY-1)
#用图片中的像素点替换掉背景中的像素点,从而将图片放入背景中,生成新图像outcome
while (indexX>=0 and x>=0) :
while (indexY>=0 and y>=0):
outcome[x,y]=self.targetImg[indexX,indexY]
indexY=indexY-1
y=y-1
indexX=indexX-1
x=x-1
#遍历完x方向后,y要回到原来的位置,从x+1开始
y=self.backgroundPixelsY-positionPixelY+1
indexY=self.targetImgPixelsY-1
self.outcome=outcome
def getResult(self):
self.replace()
return self.outcome
信号产生
#coding:utf-8
import cv2
import numpy as np
class SGN:
'''
输入:尺寸为x/q*y/q的灰度矩阵
输出:描述笔尖运动的矩阵
功能:绘制图案时,机器将从绘制起点(0,0)开始绘制图案,先向y轴运动绘制x=0处的图案,然后向快速到(1,0)处向y轴运动绘制x=1处的图案。
'''
#targetImgHeight,targetImgWidth=targetImg.shape
#np.savetxt("a.txt", sgn.getResult(), fmt="%d", delimiter=",")
step=50
informationContent=5 #描述输出矩阵的列数
threshold=0
def __init__(self):
#起始、终止信息
begin=np.zeros([1,self.informationContent], dtype = int)
begin[0,0]=100
self.stop=np.zeros([1,self.informationContent], dtype = int)
self.begin=begin
def setImgPixels(self,imgPixel):
self.imgPixel=imgPixel.astype(int)
self.imgPixelHeight,self.imgPixelWidth=self.imgPixel.shape
self.begin[0,1]=self.imgPixelWidth
self.begin[0,2]=self.imgPixelHeight
def run(self):
indexX=self.imgPixelHeight-1
indexY=self.imgPixelWidth-1
times=int(255/self.step)
imgCopy=self.imgPixel
while times>0:
recordeFlag=1
while (indexX>=0):
while(indexY>=0):
if imgCopy[indexX,indexY]>self.threshold:
if (recordeFlag==1)and(indexY>0):
temp=np.zeros([1,self.informationContent], dtype = int)
temp[0,0]=1
temp[0,1]=indexX
temp[0,2]=indexY
temp[0,3]=indexX
temp[0,4]=indexY
recordeFlag=0
elif indexY==0 and recordeFlag==1:
recordeFlag=1
elif indexY==0:
self.begin=np.r_[self.begin,temp]
recordeFlag=1
elif recordeFlag==0:
temp[0,3]=indexX
temp[0,4]=indexY
else:
if recordeFlag==0:
self.begin=np.r_[self.begin,temp]
recordeFlag=1
indexY=indexY-1
indexY=self.imgPixelWidth-1
indexX=indexX-1
indexX=self.imgPixelHeight-1
times=times-1
imgCopy=imgCopy-self.step
pass
def getResult(self):
self.run()
self.outcome=np.r_[self.begin,self.stop]
return self.outcome
电机控制
#coding: utf8
import RPi.GPIO as GPIO
import time
import sys
class StepMotor:
parameter=8*1.8/(16*360) #丝杆导程*全步进角/(细分系数*360度) 单位mm/次 意义每次步进脉冲平台移动距离
position=-5
reset=False
def __init__(self,stepPin,dirPin,minPosition=0,maxPosition=200):
self.stepPin=stepPin
self.dirPin=dirPin
self.minPosition=minPosition
self.maxPosition=maxPosition
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BOARD)
GPIO.setup(self.stepPin,GPIO.OUT)
GPIO.setup(self.dirPin,GPIO.OUT)
self.setDirF()
self.goToOriginalPoint()
def setDirF(self):
GPIO.output(self.dirPin,1)
def setDirB(self):
GPIO.output(self.dirPin,0)
def move(self):
GPIO.output(self.stepPin,0)
time.sleep(self.speed)
GPIO.output(self.stepPin,1)
time.sleep(self.speed)
def run(self,speed=0.0001,distance=0):
#speed=0.00001快 0.0001慢
self.speed=speed
times=int(distance/self.parameter) #这是由螺杆导程、步进电机步进角决定的
while(times>0):
self.move()
times=times-1
def getPermission(self,direction,distance):
'''
检查电机的位置,避免超程
'''
if direction == 'F' :
nextPosition=self.position+distance
elif direction == 'B':
nextPosition=self.position-distance
if (self.minPosition<=nextPosition) and (self.maxPosition>=nextPosition):
self.position=nextPosition
return True
else:
print("超程警告,已取消操作")
return False
def goF(self,speed=0.0001,distance=0):
if(self.getPermission('F',distance)):
self.setDirF()
self.run(speed,distance)
def goB(self,speed=0.0001,distance=0):
if(self.getPermission('B',distance)):
self.setDirB()
self.run(speed,distance)
def goToPosition(self,position,speed=0.0001):
distance=position-self.position
if distance>=0 :
self.goF(speed,distance)
else:
distance=-distance
self.goB(speed,distance)
def goToOriginalPoint(self,speed=0.0001):
if (not self.reset):
if self.position<=0:
self.setDirF()
self.run(speed,-self.position)
self.reset=True
else:
pass
else:
self.goToPosition(0,0.0001)
class Steer:
contrlPeriod=0.020
pulseWidth=0.000
def __init__(self,contrlPin):
self.contrlPin=contrlPin
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BOARD)
GPIO.setup(self.contrlPin,GPIO.OUT)
def run(self,angle):
self.pulseWidth=(angle+45.0)/90.0*0.001
i=0
while(i<25):
i=i+1
GPIO.output(self.contrlPin,1)
time.sleep(self.pulseWidth)
GPIO.output(self.contrlPin,0)
time.sleep(self.contrlPeriod-self.pulseWidth)
class CTR:
penLineSize=0.5
def __init__(self):
self.steer=Steer(29)
self.penUp()
self.XMotor=StepMotor(35,37,0,150)
self.YMotor=StepMotor(31,33,0,210)
def setCode(self,code):
self.code=code
print(self.code.shape)
def goToPosition(self,x,y,speed=0.0001):
self.YMotor.goToPosition(y,speed)
self.XMotor.goToPosition(x,speed)
def penDown(self):
self.steer.run(180)
def penUp(self):
self.steer.run(100)
def drawLine(self,line,start,end):
startPosition=(self.imgWidth-start)*self.penLineSize
endPosition=(self.imgWidth-end)*self.penLineSize
linePosition=(self.imgHeight-line)*self.penLineSize
self.goToPosition(startPosition,linePosition,0.00001)
#下笔
self.penDown()
self.goToPosition(endPosition,linePosition)
#抬笔
self.penUp()
def run(self):
codeIndex=0
codeId=100
while codeId>0:
currentCode=self.code[codeIndex] #读取第一条代码
codeId=currentCode[0] #下一条代码类型
if codeId==100:
self.imgWidth=currentCode[1]
self.imgHeight=currentCode[2]
if codeId==1: #如果是画直线
line=currentCode[1] # X方向第几行
start=currentCode[2] #Y方向开始的地方
end=currentCode[4] #Y方向结束的地方
self.drawLine(line,start,end)
codeIndex=codeIndex+1 #下一条代码索引
print(codeIndex)
print("结束")
self.XMotor.goToOriginalPoint()
self.YMotor.goToOriginalPoint()
仿真
#coding:utf-8
import cv2
import numpy as np
class SIM:
def __init__(self):
pass
def setImgPixels(self,imgPixel):
self.imgPixelHeight,self.imgPixelWidth=imgPixel.shape
self.imgPixel= np.zeros([self.imgPixelHeight,self.imgPixelWidth], np.uint8)
def setCode(self,code):
self.code=code
def analyse(self):
currentCode=self.code[0]
codeIndex=0
codeId=100
imgCopy=self.imgPixel
while codeId>0:
codeIndex=codeIndex+1
currentCode=self.code[codeIndex]
codeId=currentCode[0]
if codeId>0:
line=currentCode[1]
start=currentCode[2]
end=currentCode[4]
imgtemp=imgCopy[line,...]
imgtemp[end:start+1]=imgtemp[end:start+1]+50
imgCopy[line,...]=imgtemp
return imgCopy
实际效果
有时间放个视频
发现的问题
不是很精密,笔会抖
软件功能太少,有待升级
命名
就叫MTP吧