首页 > 技术文章 > MTP 写字机器

uestcman 2019-01-09 14:23 原文

目标

无意中看到下面视频,我打算也实现一个类似机器
视频视频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的灰度矩阵
功能:

  1. 灰度化图片
  2. 确定图片右下角与绘制起点的相对位置
  3. 根据设定要求放缩图片,使图片整体位于绘制范围内部
  4. 输出尺寸为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吧

推荐阅读