python+乐高=打印机

接上文 梦幻联动——python+EV3

1.1 尝试

首先要实现打印机的txt打印功能,首先要把txt转化为打印黑(1)和打印白(0)组成的数组,然后逐行打印出来,而这个转换过程需要自创一种‘字体’来做一一对应

之后我先是尝试了点阵图

把每一个字母映射成6*3的点阵图,这样每一行字有6行点阵,再把每一行的文本转换为6行的字符,然后对于每一行字重复以上操作,得到多重多行多组的‘趣味’数据结构,我想你已经被我说晕了,我写的时候也很晕。那就让我们更直观地感受一下:

a1a2a3a1a2a3a1a2a3
b1b2b3b1b2b3b1b2b3
c1c2c3c1c2c3c1c2c3
d1d2d3d1d2d3d1d2d3
e1e2e3e1e2e3e1e2e3
f1f2f3f1f2f3f1f2f3
a1a2a3a4a5a6a7a8a9a10a11a12
b1b2b3b4b5b6b7b8b9b10b11b12
c1c2c3c4c5c6c7c8c9c10cc
d1d2d3d4d5d6d7d8d9d10d11d12
e1e2e3e4e5e6e7e8e9e10e11e12
f1f2f3f4f5f6f7f8f9f10f11f12
把前6排的矩阵转换为后6排

这样做的清晰度会很低,但由于我当时还没有想出更好的方法,便开始写代码:

def convert(i):#one digital to image
    word = list('1234567890qwertyuiopasdfghjklzxcvbnmQ
WERTYUIOPASDFGHJKLZXCVBNM')
    if i not in word:
        return [0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]
    dic = {'1':([0,0,0],[0,1,0],[1,1,0],[0,1,0],[0,1,0],[1,1,1]),
           '2':([0,0,0],[0,1,0],[1,0,1],[0,0,1],[0,1,0],[1,1,1]),
           '3':([0,0,0],[1,1,1],[0,0,1],[1,1,1],[0,0,1],[1,1,1]),
           '4':([0,0,0],[1,0,1],[1,0,1],[1,1,1],[0,0,1],[0,0,1]),
           '5':([0,0,0],[1,1,1],[1,0,0],[1,1,1],[0,0,1],[1,1,1]),
           '6':([0,0,0],[1,1,1],[1,0,0],[1,1,1],[1,0,1],[1,1,1]),
           '7':([0,0,0],[1,1,1],[0,0,1],[0,0,1],[0,0,1],[0,0,1]),
           '8':([0,0,0],[1,1,1],[1,0,1],[1,1,1],[1,0,1],[1,1,1]),
           '9':([0,0,0],[1,1,1],[1,0,1],[1,1,1],[0,0,1],[0,0,1]),
           '0':([0,0,0],[0,1,0],[1,0,1],[1,0,1],[1,0,1],[0,1,0]),
           'a':([0,0,0],[0,1,0],[1,1,0],[0,1,0],[0,1,0],[1,1,1]),
           'A':([0,0,0],[0,1,0],[1,1,0],[0,1,0],[0,1,0],[1,1,1]),
           'b':([0,0,0],[0,1,0],[1,1,0],[0,1,0],[0,1,0],[1,1,1]),
           'B':([0,0,0],[0,1,0],[1,1,0],[0,1,0],[0,1,0],[1,1,1]),
           'c':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'C':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'd':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'D':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'e':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'E':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'f':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'F':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'g':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'G':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'h':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'H':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'i':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'I':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'j':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'J':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'k':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'K':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'l':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'L':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'm':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'M':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'n':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'N':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'o':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'O':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'p':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'P':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'q':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'Q':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'r':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'R':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           's':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'S':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           't':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'T':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'u':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'U':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'v':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'V':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'w':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'W':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'x':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'X':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'y':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'Y':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'z':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           'Z':([0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]),
           }
    return dic[i]

def convertall(txt):#txt to image
    now = 0
    lines = [[]]
    line = [[]]*6
    
    for i in txt:
        if i == '\n':
            lines[now] = line
            now += 1
            lines.append([])
            line = [[]]*6
            continue

        get = convert(i)
        for i in range(0,6):
            line[i] = line[i]+get[i]+[0]

        lines[now] = line
    return lines

def printall(lines):#EV3's display is too small for this
    this = ''
    for line in lines:
        for i in line:
            this = ''
            for bw in i:
                if bw == 1:
                    this += '#'
                else:
                    this += ' '
            print(this)
            brick.display.text(this)
            time.sleep(1)

if __name__ == '__main__':
    #get file
    txt = open('name.txt')
    txt = txt.read()
    lines = convertall(txt)
    printall(lines)

B往后的几个字母就没写了,由于我没学过高数,不大懂矩阵的运算和转换,只好用一些极傻的方法,而后面的转换合并更是棘手,实话实说,我已经忘的差不多了。

2.1 Plan B

在做上面那个企划时我就开始想第二种方法。比起自创的字体我们更应该运用公式化、矢量图式的成熟字体,把txt转化为jpg格式的文字,再求出图片中的每一个像素的灰度值,灰度值大于某一个阈值的像素就会被选为黑色,反之为白色,再进行逐行打印

2.2 转换代码

其实整个项目最为有趣的是我用了两个之前从来没想到还可以这么用的

2.2.1 txttojpg.py #本段代码有乡村熊提供,略有修改

#导入pygame做字体转换(3.9不能用,在这里我废了好长时间
import os
import pygame
#ttj即为txttojpg
def ttj(txt = 'input.txt',size = 500):
    pygame.init()#初始化
    text = open(txt).read()#读文本
    font = pygame.font.Font(os.path.join("fonts","mao's.ttf"), size)#载入字体
    rtext = font.render(text,True ,[0, 0, 0], [255, 255, 255])#转换

    file = 'printthis.jpg'
    pygame.image.save(rtext, file)#保存
if __name__ == "__main__":
    ttj()

注释写的很明白了(夸我

2.2.1 jpgtoprint.py #也就是之前的字符画

from PIL import Image

 
# 将256灰度映射到70个字符上
def get_char(r,g,b,alpha = 256):
    if alpha == 0:
        return ' '
    gray = int(0.2126 * r + 0.7152 * g + 0.0722 * b)

    if gray <= 150:#由于打印时只有黑白,便没有字符画那么多的字符可用
        return '$'#黑
    else:
        return ' '#白
 
def jtp(IMG = 'printthis.jpg'):
    im = Image.open(IMG)
    im = im.resize((200,int(im.size[1]//(im.size[0]/100))))
    
    txt = ""
 
    print(im.size,im.size)
    for j in range(im.size[1]):
        for i in range(im.size[0]):
            a,b = i,j
            #print(get_char(*im.getpixel((a,b))))
            txt += get_char(*im.getpixel((a,b)))
        txt += '\n'
 
    print(txt)
    im.save("thumbnail", "JPEG")
 
    #字符画输出到文件
    with open("output.txt",'w') as f:
        f.write(txt)
if __name__ == "__main__":
    jtp()

具体原理就看字符画内篇文章

2.3 打印代码

#这部分有一点pybricks的内容,文档见下

#!/usr/bin/env pybricks-micropython

from pybricks import ev3brick as brick
from pybricks.ev3devices import (Motor, TouchSensor, ColorSensor,
                                 InfraredSensor, UltrasonicSensor, GyroSensor)
from pybricks.parameters import (Port, Stop, Direction, Button, Color,
                                 SoundFile, ImageFile, Align)
from pybricks.tools import print, wait, StopWatch
from pybricks.robotics import DriveBase

首先一股脑导入所有给定的模块,用起来方便

xmotor = Motor(Port.C)#x轴电机
ymotor = Motor(Port.B)#y轴电机
updownmotor = Motor (Port.D)#抬笔落笔电机
touch = TouchSensor (Port.S3)#触动传感器(初始化用

示例化电机对象,参数即为端口号

def initialization():
    while touch.pressed() == 0:
        xmotor.run(-300)
        
    xmotor.run_angle(300, 100)
    updownmotor.run_until_stalled(360)
    xmotor.reset_angle(0)

初始化,在触动传感器触发前转动x轴电机,并做电机角度传感器器归零,并将抬笔落笔电机初始化:

run_until_stalled()——给定参数的速度旋转直至堵转

def nextline():#会车
    xmotor.run_target(1000,0)#x轴电机走到零点(之前初始化时计数器归的零
    ymotor.run_angle(1000, 7)#y轴电机转到下一行

def printwhite(space):#白色
    xmotor.run_angle(1000, 7 * space)#x轴电机走空格数space个空格

def printblack(do):#黑色
    updownmotor.run_angle(300, -30)#落笔
    if do != 1:
        xmotor.run_angle(1000,7 * do)#x轴电机走黑格数do个格画黑线

    updownmotor.run_until_stalled(360)#抬笔

有了以上的函数,我们打印的所有所需要使用的东西都准备好了

content = open('output.txt')
content = content.read()
lines = content.split('\n')

把之前jpgtoprint.py输出的txt文件读取出来,并用换行进行分割

def printline(line):
    do = 0#打印黑色格数
    space = 0#打印白色格数
    line += ' '
    for i in line:
        if i == ' ':            
            space += 1
            if do != 0:
                printblack(do)
                do = 0
            continue

        if i == '$':            
            do += 1
            if space != 0:
                printwhite(space)
                space = 0
            continue

打印一行,逻辑如下:

def printpage():
    for i in lines:
        printline(i)
        nextline()

打印一页 = 打印多行

if __name__ == "__main__":
    initialization()
    printpage()

最后的最后:打印!

3.2 硬件

People who are really serious about software should make their own hardware.

Alan Kay

这个打印机是我翻出来的,后来想起好像好像是按照一个图纸做的

我稍微改了一下,一开始我用的是粗笔,后来改成了细笔,其精确度至少提高了五倍,还有一些因为没有足够零件的无奈改动,先发布吧,过一会我会发一个视频上来看成果

关于 “python+乐高=打印机” 的 5 个意见

  1. 小的时候也做过这个打印机,当时用的是圆珠笔矢量打印。效果极差。你的像素打印解决方法可以,但也可以加上矢量打印的方法。

发表评论

电子邮件地址不会被公开。 必填项已用*标注