ASCII文本图形
前言
ASCII文本图形源头是19世纪后期出现的打字机文本图形。在20世纪60年代,计算机有了较弱的图形处理硬件,ASCII被用于表示图形。今天,ASCII文本图形继续作为因特网上的一种表现形式,你可以在网上找到各种创意的例子。
一、学习目标及步骤
这个项目用Python创建一个程序,从图像生成ASCII文本图形。该程序让你指定输出文本序列的宽度,并设置垂直比例因子。它也支持两种灰度值到ASCII字符的映射:稀疏的10级映射和更精细校正的70级映射。
要从图像生成ASCII文本图像,需要学习如何做到以下几点:
- 用Pillow将彩色图像转换成灰度图像,它是Python的图像库(PIL)的一个分支;
- 使用numpy计算灰度图像的平均亮度;
- 用一个字符串作为灰度值的快速查找表。
下面是程序生成ASCII文本图形的步骤:
1. 将输入的图形装成灰度;
2. 将图像分成M×N个小块;
3. 修正M(行数),以匹配图像和字体的横纵比;
4. 计算每一个小块图像的平均亮度,然后为每个小块查找合适的ASCII字符;
5. 汇集各行的ASCII字符串,将他们打印到文件,形成最终图像。
二、代码
1.引入库
代码如下(示例):
import argparse
import numpy as np
from PIL import Image
2.定义灰度等级和网格
先定义两种灰度等级作为全局值,分别是70级,10级,这两个值保存为字符串,从最黑暗变到最亮,用于将亮度值转换为ASCII字符。
# 70 levels of gray
gscale1 = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^.y' "
# 10 leves of gray
gscale2 = "@%#*+=-:. "
既然有了灰度梯度,就可以准备图像了。下面的代码打开图像,并分割成网格:
# 打开图像并转换为灰度
image = Image.open(filename).convert('L')
# 存储图像尺寸
W, H = image.size[0], image.size[1]
# 计算平铺宽度
w = W/cols
# 根据字体的纵横比和比例计算平铺高度
h = w/scale
# 计算要在最终网格中使用的行数
rows = int(H/h)
比例系数确定每个小块的大小,以匹配用于显示文字的字体的横纵比,这样最终图像不会失真。scale的值可以作为参数传入,或者设置位默认值0.43,在用Courier显示结果时,效果最好。
3.计算平均亮度
计算灰度图像中每一小块的平均亮度。函数getAverageL()完成。
def getAverageL(image):
# 以numpy数组的形式获取图像
im = np.array(image)
# 得到尺寸
w, h = im.shape
# 获得平均值
return np.average(im.reshape(w*h))
图像小块作为PIL Image对象传入,将 image转换成一个numpy数组,此时 im成为一个二维数组,包含每个像素的亮度。保存该图像的尺寸(宽度和高度)。numpy.average()计算该图像中的亮度平均值,做法是用numpy.reshape()先将维度为宽和高(w,h)的二维数组转换成扁平的一维,其长度是宽度乘以高度(w*h)。然后numpy.average()调用对这些数组值求和并计算平均值。
4.从图像生成ASCII内容
# ASCII图像是字符串的列表
aimg = []
# 生成平铺尺寸列表
for j in range(rows):
y1 = int(j * h)
y2 = int((j + 1) * h)
# 更正最后一个平铺方块
if j == rows - 1:
y2 = H
# 追加一个空字符串
aimg.append("")
for i in range(cols):
# 裁剪图像以适合平铺
x1 = int(i * w)
x2 = int((i + 1) * w)
# 更正最后一个平铺方块
if i == cols - 1:
x2 = W
# 裁剪图像以将其平铺提取到另一个图像对象中
img = image.crop((x1, y1, x2, y2))
# 获得平均亮度
avg = int(getAverageL(img))
# 在ascii字符中查找平均灰度值
if moreLevels:
gsval = gscale1[int((avg * 69) / 255)]
else:
gsval = gscale2[int((avg * 9) / 255)]
# 将ascii字符附加到字符串
aimg[j] += gsval
在程序的这一部分,ASCII图像先作为一个字符串列表保存并初始化。接下来,按计算好的图像小块行数迭代遍历,计算每个图像小块的起始和结束y坐标。虽然这些是浮点运算,但在传给图像裁剪方法之前,将它们截断为整数。接着,因为只有当图像的宽度是列数的整数倍时,图像分割成小块时,边缘的小块才有相同的大小,所以在最后一行校正小块的y坐标,将y坐标设置为图像的实际高度。这样做确保了图像顶部的边缘不被截断。然后为ASCII 图像添加一个空字符串,作为一种紧凑的方式来表示图像的当前行。接下来会填充这个字符串(将字符串作为字符的列表)。
计算每个小块的左、右x坐标,为最后一小块校正x坐标,原因和校正y坐标时一样。然后用 image.crop()提取图像小块,然后将该小块传入getAverageL()函数,取得小块的平均亮度。将平均亮度值从[0,255]缩小至[0,9](默认10级灰度梯度值的范围)。然后,用 gscale2(保存的梯度字符串)作为查找表,找到对应的ASCII 值。然后类似,不同之处在于,只有命令行标志设置为使用70级梯度时,才会用它。最后,在文本行中添加找到的ASCII 值 gsval,代码循环,直到处理完所有行。
5.类初始化定义
descStr = "This program converts an image into ASCII art."
# 添加预期参数
parser = argparse.ArgumentParser(description=descStr)
parser.add_argument('--file', dest='imgFile', required=False)
parser.add_argument('--scale', dest='scale', required=False)
parser.add_argument('--out', dest='outFile', required=False)
parser.add_argument('--cols', dest='cols', required=False)
parser.add_argument('--morelevels', dest='moreLevels', action='store_true')
指定图像文件输入的选项(唯一必须的参数),设置垂直比例因子,设置输出文件名,设置ASCII输出中的文本列数。在行,添加–morelevels选项,让用户选择更多层次的灰度梯度。
6.将ASCII文本图形字符串写入文本文件
# 打开一个新文件
f = open(outFile, 'w')
# 将列表中的每个字符串写入新文件
for row in aimg:
f.write(row + '\n')
# 关闭文件
f.close()
使用内置的 open()方法,打开一个新的文本文件用于写入。然后迭代遍历列表中的每个字符串,将它写入文件,关闭文件对象,释放系统资源。
三、完整代码
import argparse
from PIL import Image
import numpy as np
# 70 leves of gray
gscale1 = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^.y' "
# 10 leves of gray
gscale2 = "@%#*+=-:. "
def getAverageL(image):
# 以numpy数组的形式获取图像
im = np.array(image)
# 得到尺寸
w, h = im.shape
# 获得平均值
return np.average(im.reshape(w*h))
def covertImageToAscii(filename, cols, scale, moreLevels):
global gscale1, gscale2
# 打开图像并转换为灰度
image = Image.open(filename).convert('L')
# 存储图像尺寸
W, H = image.size[0], image.size[1]
# print("input image dims: %d * %d" % (W, H))
# 计算平铺宽度
w = W/cols
# 根据字体的纵横比和比例计算平铺高度
h = w/scale
# 计算要在最终网格中使用的行数
rows = int(H/h)
# print("col: %d, row: %d" % (cols, rows))
# print("tile dims: %d * %d" % (w, h))
if cols > W or rows > H:
# print("Image too small for specified cols!")
exit(0)
# ASCII图像是字符串的列表
aimg = []
# 生成平铺尺寸列表
for j in range(rows):
y1 = int(j * h)
y2 = int((j + 1) * h)
# 更正最后一个平铺方块
if j == rows - 1:
y2 = H
# 追加一个空字符串
aimg.append("")
for i in range(cols):
# 裁剪图像以适合平铺
x1 = int(i * w)
x2 = int((i + 1) * w)
# 更正最后一个平铺方块
if i == cols - 1:
x2 = W
# 裁剪图像以将其平铺提取到另一个图像对象中
img = image.crop((x1, y1, x2, y2))
# 获得平均亮度
avg = int(getAverageL(img))
# 在ascii字符中查找平均灰度值
if moreLevels:
gsval = gscale1[int((avg * 69) / 255)]
else:
gsval = gscale2[int((avg * 9) / 255)]
# 将ascii字符附加到字符串
aimg[j] += gsval
return aimg
def main(imgFile, scale=0.52, cols=65, outFile='out.txt'):
descStr = "This program converts an image into ASCII art."
# 添加预期参数
parser = argparse.ArgumentParser(description=descStr)
parser.add_argument('--file', dest='imgFile', required=False)
parser.add_argument('--scale', dest='scale', required=False)
parser.add_argument('--out', dest='outFile', required=False)
parser.add_argument('--cols', dest='cols', required=False)
parser.add_argument('--morelevels', dest='moreLevels', action='store_true')
args = parser.parse_args()
if args.outFile:
outFile = args.outFile
if args.scale:
scale = float(args.scale)
if args.cols:
cols = int(args.cols)
# print('generating ASCII art ...')
aimg = covertImageToAscii(imgFile, cols, scale, args.moreLevels)
# 打开一个新文件
f = open(outFile, 'w')
# 将列表中的每个字符串写入新文件
for row in aimg:
print(row)
f.write(row + '\n')
# 关闭文件
f.close()
# print("ASCII art written to %s" % outFile)
if __name__ == '__main__':
main('fxp.jpg')
四、运行结果
在这个项目中,我们学习了如何从任意的输入图像生成ASCII文本图形,还学习了计算平均亮度值,将图片转换成灰度,以及如何基于灰度值用字符替换一小块图像,对比原图和运行结果图,代码基本运行成功。(其实代码也是闲着没事给某个小姑娘写的,顺便学习一下python语法)
参考代码:https://github.com/electronut/pp/blob/master/ascii/ascii.py