目标

无意中看到下面视频,我打算也实现一个类似机器

视频视频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吧

MTP 写字机器的更多相关文章

  1. 魅蓝note2在ubuntu14.04下mtp模式无法自动mount的解决方法

    是因为新机型没在列表里的原因. 处理方法如下: As far as I know, MTP works fine in Trusty. You can try this: Uncomment #use ...

  2. Teaching Machines to Understand Us 让机器理解我们 之二 深度学习的历史

    Deep history 深度学习的历史 The roots of deep learning reach back further than LeCun’s time at Bell Labs. H ...

  3. 2019年MTP管理技能提升培训笔记

    2019年MTP管理技能提升培训笔记 管理专题培训–MTP管理技能提升培训 高水准的问题分析解决 何为高水准 高 多层探寻,高度分析,即需要有深度 水 团队讨论,水平思考,即需要有广度 准 预防应变, ...

  4. Linux A机器免密码SSH登录B机器

    一.问题 如上,A机器经常需远程操作B机器,传输文件到B机器,每次输入帐号密码过于繁琐,下文通过ssh公钥能解免密码操作问题. 二.解决 1.方案 SSH认证采用公钥与私钥认证方式. 2.步骤 1) ...

  5. Java_jvisualvm使用JMX连接远程机器(实践)

    https://my.oschina.net/heroShane/blog/196227 一.启动普通的jar程序 1.执行foo.jar启动命令 java -Dcom.sun.management. ...

  6. 使用ARP欺骗, 截取局域网中任意一台机器的网页请求,破解用户名密码等信息

    ARP欺骗的作用 当你在网吧玩,发现有人玩LOL大吵大闹, 用ARP欺骗把他踢下线吧 当你在咖啡厅看上某一个看书的妹纸,又不好意思开口要微信号, 用arp欺骗,不知不觉获取到她的微信号和聊天记录,吓一 ...

  7. [bigdata] 使用Redis队列来实现与机器无关的Job提交与执行 (python实现)

    用例场景: 定时从远程多台机器上下载文件存入HDFS中.一开始采用shell 一对一的方式实现,但对于由于网络或者其他原因造成下载失败的任务无法进行重试,且如果某台agent机器down机,将导致它对 ...

  8. XP机器上WCF采用X509证书加密时IIS读取证书的授权

    XP机器上WCF采用X509证书加密时IIS读取证书的授权 XP下的授权命令为:winhttpcertcfg -g -c LOCAL_MACHINE\My -s 证书名称 -a "ASPNE ...

  9. Linux下不同机器之间拷贝文件

    在Linux系统下,不同机器上实现文件拷贝 一.将本地文件拷贝到远程机器: scp /home/administrator/news.txt root@192.168.6.129:/etc/squid ...

随机推荐

  1. Linux基础命令---查找用户信息finger

    finger finger指令用来查找.显示指定用户的信息.查询远程主机信息是,可以用user@localhost来指定用户. 此命令的适用范围:RedHat.RHEL.Ubuntu.CentOS.S ...

  2. golang学习笔记5 用bee工具创建项目 bee工具简介

    golang学习笔记5 用bee工具创建项目 bee工具简介 Bee 工具的使用 - beego: 简约 & 强大并存的 Go 应用框架https://beego.me/docs/instal ...

  3. highchart应用示例2-上:圆角柱状图,下:多指标曲线图

    1.ajax调用接口获取数据 function getCityData() { var date1 = $('#datetimepicker1').val(); var date2 = $('#dat ...

  4. Step5:SQL Server 跨网段(跨机房)FTP复制

    一.本文所涉及的内容(Contents) 本文所涉及的内容(Contents) 背景(Contexts) 搭建过程(Process) 注意事项(Attention) 参考文献(References) ...

  5. flask框架----flask入门

    一.Flask介绍(轻量级的框架,非常快速的就能把程序搭建起来) Flask是一个基于Python开发并且依赖jinja2模板和Werkzeug WSGI服务的一个微型框架,对于Werkzeug本质是 ...

  6. Django后端项目---- Rest Framework(2)

    一.认证(补充的一个点) 认证请求头 #!/usr/bin/env python # -*- coding:utf-8 -*- from rest_framework.views import API ...

  7. 一位前辈的博客,收获颇丰,包括Android、Java、linux、前端、大数据、网络安全等等

    https://www.cnblogs.com/lr393993507/   魔流剑

  8. ZYNQ学习之路1. Linux最小系统构建

    https://blog.csdn.net/u010580016/article/details/80430138?utm_source=blogxgwz1 开发环境:window10, vivado ...

  9. Golang并发编程进程通信channel了解及简单使用

    概念及作用 channel是一个数据类型,用于实现同步,用于两个协程之间交换数据.goroutine奉行通过通信来共享内存,而不是共享内存来通信.引用类型channel是CSP模式的具体实现,用于多个 ...

  10. Spring Maven 包的依赖

    <properties> <spring.version>4.3.11.RELEASE</spring.version> </properties> & ...