早上在B站看到一个字符版的《改革春风吹满地》,然后想起前不久在实验楼刚做了将图片转字符与视频转动态字符的小demo,另外关于相应的模块也不是很熟,那么本篇就来总结一下这些东西。
argparse是python的一个命令行解析包,适合编写可读性非常好的程序,可能某种意义上比函数式更加清晰,下面我就以思维导图的方式对该包的两个方面进行说明。
关于argparse中的ArgumentParser与add_argument参数我的一些理解与总结就是上面那些,具体的案例分析我就不提了,本篇重点不再这里。如果感兴趣的可以看下面的官方介绍,我的顺序也基本上按其来的:
argparse — Parser for command-line options, arguments and sub-commands
如果觉得英文难理解,这里再推荐一篇我觉得写得比较好的中文版:
33 Python 详解命令解析 - argparse
然后我们就进入主题了。
字符画是一系列字符的组合,我们可以把字符看作是比较大块的像素,一个字符能表现一种颜色(为了简化可以这么理解),字符的种类越多,可以表现的颜色也越多,图片也会更有层次感。而我们也一般用256的灰度值来表示上述关系,这里抛开精确不讲,大致用一个简单的式子表示为:
g r a y = 0.2126 ∗ r + 0.7152 ∗ g + 0.0722 ∗ b gray = 0.2126 * r + 0.7152 * g + 0.0722 * b gray=0.2126∗r+0.7152∗g+0.0722∗b
以此,我们可以创建一个不重复的字符列表ascii_char,灰度值小(暗)的用列表开头的符号,灰度值大(亮)的用列表末尾的符号。这就相当于一种映射,将灰度的区间转换成字符的区间,用字符替代灰度值,那么设当前字符为x,公式为:
g r a y 256.0 = x a s c i i − c h a r ⇒ x = g r a y 256 ∗ a s c i i − c h a r \frac{gray}{256.0} = \frac{x}{ascii-char} \Rightarrow x = \frac{gray}{256}*ascii-char 256.0gray=ascii−charx⇒x=256gray∗ascii−char
如果是256个字符,与256的灰度值就是一一对应的关系,所得出来的结果也越精确,字符画也越清晰,但要想出256个不重复的字符,不容易。。。所以我们用70个字符来表示这个区间。
这里可能还会用到的一个模块就是关于pillow,这个也蛮重要,有时间的话下次我会写一个总结或者画一个思维导图,那么接下来根据代码来看看大致步骤,此处为第一版,为无argparse模块的版本:
from PIL import Image
ascii_char = list("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. ")
# 将256灰度值映射到70个字符上
def get_char(r,g,b,alpha = 256):
"""
:param r: red
:param g: green
:param b: blue
:param alpha: 图像的灰度值或者说像素值
:return: 返回经过公式处理后的70个字符值中对应的区间
"""
if alpha == 0: # 可能会出现0的区间,这部分如果不处理会产生报错
return " "
gray = 0.2126 * r + 0.7152 * g + 0.0722 * b # RGB三原色的计算公式
# print(gray,alpha) 发现gray会超过alpha边界,所以要加1.0,浮点数
x = int((gray / (alpha + 1.0)) * len(ascii_char)) # 映射转换
return ascii_char[x] # 返回我们上面取到的信道区间值
# 写入函数
def write_file(content):
with open("out_file_name.png","w") as f:
f.write(content)
# 主函数
def main(file_name="test.jpg",width=80,height=80):
"""
:param file_name: 指定输入图片路径
:param width: 宽度,默认80
:param height: 高度,默认80
:return: 无返回值
"""
text = ""
im = Image.open(file_name)
im = im.resize((width,height),Image.NEAREST) # resize对图片进行压缩,NEAREST为KNN插值尺寸
for i in range(height):
for j in range(width):
content = im.getpixel((j,i)) # 宽度代表横坐标,高度代表纵坐标,得到一个元组
text += get_char(*content) # 对上面的元组进行解包
text += "\n" # 行加上换行符
# print(text) # 在终端输出字符
write_file(text)
if __name__ == '__main__':
main(file_name="5.jpg")
我们以如下图片为例:
这里一定要注意边界的判定,因为任何一个字符有超出,都会引发报错。然后我们可以得到一个txt文件,然后你就会看到一个很丑的txt符号文件。。。这里可以向ascii_char中添加其它的字符,写得越多越清晰,我后来直接减成3个字符了,发现比70的好看一些。。。但对于上面这张色彩斑斓的图片来讲,转换成txt文件类型我们得出的结果还是有些难看,所以我们这里就不贴出来了。
到这里,我们基本了解了图片转字符的步骤与逻辑,那么接下来我们就可以通过argparse模块来实现命令行的运行,代码如下:
from PIL import Image
import argparse
#命令行输入参数处理
parser = argparse.ArgumentParser() # 创建解析器
parser.add_argument('file') # 输入文件
parser.add_argument('-o', '--output') #输出文件
parser.add_argument('--width', type = int, default = 80) #输出字符画宽
parser.add_argument('--height', type = int, default = 80) #输出字符画高
#获取参数
args = parser.parse_args()
IMG = args.file
WIDTH = args.width
HEIGHT = args.height
OUTPUT = args.output
ascii_char = list("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. ")
# 将256灰度映射到70个字符上
def get_char(r,g,b,alpha = 256):
if alpha == 0:
return ' '
length = len(ascii_char)
gray = int(0.2126 * r + 0.7152 * g + 0.0722 * b)
unit = (256.0 + 1)/length
return ascii_char[int(gray/unit)]
if __name__ == '__main__':
im = Image.open(IMG)
im = im.resize((WIDTH,HEIGHT), Image.NEAREST)
txt = ""
for i in range(HEIGHT):
for j in range(WIDTH):
txt += get_char(*im.getpixel((j,i)))
txt += '\n'
print(txt)
#字符画输出到文件
if OUTPUT:
with open(OUTPUT,'w') as f:
f.write(txt)
else:
with open("output.txt",'w') as f:
f.write(txt)
这个代码是实验楼的源码,这里相当于对上面那一个版本进行了封装,将函数替换成了命令行,总体来看简洁清晰了些,然后这里的运行结果也和上面的一样,那么因为切换到命令行了,怎么运行呢?如果看了我上面发的官方链接的案例,应该是会了,如果没看我也在这里提一下,命令为:
python 代码文件名.py 5.jpg
然后我在看官方链接的时候看到一个比较有趣的东西是,如果将输入文件的那一行改了,改成如下形式,那么代码还是可以直接运行而不用命令行。
parser.add_argument('--file', default='333.jpg') # 输入文件
那么到此为止,我们实现了图片转换成字符文件,但好像上面的代码都只能转换成txt文件,主要还是太简单,字符直接写进文件里,而不是说通过变量接收再给文件上色,然后保存成图片,这里我找到一个博主的文章,写得很棒,并且对字符图片研究颇深,下面给出代码和地址:
#-*- coding:utf-8 -*-
import os
from PIL import Image, ImageFont, ImageDraw
import argparse
#命令行输入参数处理
parser = argparse.ArgumentParser()
parser.add_argument('file')
parser.add_argument('-o','--output')
#获取参数
args = parser.parse_args()
File = args.file
OUTPUT = args.output
ascii_char = list("MNHQ$OC67)oa+>!:+. ")
#将像素转换为ascii码
def get_char(r,g,b,alpha = 256):
if alpha == 0:
return ''
length = len(ascii_char)
gray = int(0.2126 * r + 0.7152 * g + 0.0722 * b)
unit = (256.0+1)/length
return ascii_char[int(gray/unit)]
if __name__=='__main__':
im = Image.open(File)
WIDTH = int(im.width/6) #高度比例为原图的1/6较好,由于字体宽度
HEIGHT = int(im.height/15) #高度比例为原图的1/15较好,由于字体高度
im_txt = Image.new("RGB",(im.width,im.height),(255,255,255))
im = im.resize((WIDTH,HEIGHT),Image.NEAREST)
txt = ""
colors = []
for i in range(HEIGHT):
for j in range(WIDTH):
pixel = im.getpixel((j,i))
colors.append((pixel[0],pixel[1],pixel[2]))#记录像素颜色信息
if(len(pixel) == 4):
txt += get_char(pixel[0],pixel[1],pixel[2],pixel[3])
else:
txt += get_char(pixel[0],pixel[1],pixel[2])
txt += '\n'
colors.append((255,255,255))
dr = ImageDraw.Draw(im_txt)
font=ImageFont.load_default().font#获取字体
x=y=0
#获取字体的宽高
font_w,font_h=font.getsize(txt[1])
font_h *= 1.37 #调整后更佳
#ImageDraw为每个ascii码进行上色
for i in range(len(txt)):
if(txt[i]=='\n'):
x+=font_h
y=-font_w
dr.text([y,x],txt[i],colors[i])
y+=font_w
#输出
im_txt.save("output.png")
简单实现图片转彩色字符画
这里如果运用我们上面讲到的,将输入文件指定文件,然后ascii_char设置更大的区间,效果更好些。图片如下:
然后这个代码就是比较好的运用了pillow模块,代码整体还是比较好理解的,下篇我会总结一下pillow的详细使用,然后写一下视频转字符视频的方式,这里可以上一张图,同样也是用的上面的博主的代码。
Python将视频转换为全字符视频(含音频)
本篇有些地方写得有点匆忙,事后有时间会回过头来修正一下,主要是这几天是在美赛,虽然我没有参加,但还是有去陪别人玩下的,可打着打着还是很累的,然后就总结了一下之前玩的一个算小游戏吧,那么总结就是这么多,未完待续。