之前在b站上看到有人用C写了个脚本把妹抖龙op转换成字符画的形式输出了,感觉比较好玩在下就用python也写了一遍(主要是因为python比较简单好用)。这里就这里就不介绍字符画了,因为能搜到这个的肯定知道自己在干什么 = =。
首先梳理转换思路:转换图片也就是转换视频,因为视频就是有连续的图片组成的。而为了简单起见,我们这里先把彩色图片先转换成黑白图片(将RGB三通道转换成一个),然后读取图片的每个像素点,根据像素点的灰度(grayscale)选择相应的字符(比如@#对应高灰度点,而.,对应低灰度点),将所有当前图片或帧的像素转换成字符后输出就是我们看到的字符画了。
这里我们可以看到这其实是一个很简单的事,最重要无非就是要根据灰度选择字符。这一点已经有好事者统计每个字符所占的像素帮大家做了(参考)。
完整版(灰度从高到低排列,注意最后的空白字符):$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,"^\
'. 而在实际应用中,发现**精简版**的打印效果更好(其实可以自己随便挑一些):
@%#*+=-:. `
代码实现:
为了简单起见,这里只讲如何将视屏直接打印到到终端(输出为黑白,不保留颜色),至于输出为其他文件形式具体可以参考这里
1.读取图片或视频
这里我们用Pillow或者opencv包进行图片读取和处理,后者可以用于视频操作,但是由于opencv读取视频的格式只支持avi,所以进行视频处理的时候我们需要另一个包:scikit-video;而如果是转换图片,那么写出可以用matplotlib包。
import skvideo.io
import cv2
def read_image(image_path, method="opencv"):
if self.method == "pillow":
img = PI.open(image_path)
grey_image = img.convert("L")
elif self.method == "opencv":
img = cv2.imread(image_path)
grey_image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
return grey_image
def read_video(video_path):
videogen = skvideo.io.vreader(video_path)
for frame in videogen:
grey_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
yield grey_frame # 由于视频包含的图片很多,为了防止一次处理的内存过大,这里用generator逐帧返回图片
- 将读取的图片各像素点转换成相应的字符
STANDARD_CHAR_LIST = list("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^'\`. ")
ABBREVIATED_CHAR_LIST = list("@%#*+=-:. ")
def img2ascii(grey_image, char_list=ABBREVIATED_CHAR_LIST, scale=1.0, method='pillow'):
if method == 'pillow':
scaled_img_width = int(grey_image.size[0] * scale) # 按比例放大或缩小图片
scaled_img_height = int(grey_image.size[1] * scale)
scaled_grey_img = grey_image.resize((scaled_img_width, scaled_img_height))
elif method == 'opencv':
scaled_grey_img = cv2.resize(grey_image, None, fx=scale, fy=scale)
scaled_img_width = len(scaled_grey_img[0])
scaled_img_height = len(scaled_grey_img)
char_list_length = len(char_list)
ascii_img = [[None for i in range(scaled_img_width)] for j in range(scaled_img_height)]
ascii_color = [x[:] for x in ascii_img]
for i in range(scaled_img_height):
for j in range(scaled_img_width):
if self.method == "pillow":
# brightness: the larger, the brighter, and later position in given char list
brightness = scaled_grey_img.getpixel((j, i))
elif self.method == "opencv":
brightness = scaled_grey_img[i, j]
ascii_img[i][j] = char_list[brightness * char_list_length / 255]
return ascii_img
3.打印图片/视频
打印图片和视频稍微有些不同,因为打印视频是希望能与原视频播放同步,因此实时读取转换打印的时间是无法在0.0278s(默认24帧/1s)内完成的,需要先将转换结果保存到数组然后再依次打印。
a)图片
output_str = ""
for line in ascii_img:
output_str += "".join(line) + "\n"
os.system(self.clear_console)
sys.stdout.write(output_str)
b)视频
total_video = []
video_temp_file = video_path + ".temp"
if not os.path.exists(video_temp_file):
with open(video_temp_file, "wb") as of:
first_frame = True
for grey_frame in read_video(video_path):
ascii_img = self.img2ascii(grey_image=grey_frame, char_list=char_list, scale=scale)
output_str = ""
for line in ascii_img:
output_str += "".join(line) + "\n"
total_video.append(output_str)
if first_frame:
of.write("{}\n".format(len(ascii_img))) # 再文件开头保存打印每一帧所需的行数
first_frame = False
of.write(output_str)
of.close()
else:
row = 1
frame_str = ""
with open(video_temp_file, "rb") as fi:
frame_row = int(fi.readline().strip())
for line in fi:
frame_str += line
if row % frame_row == 0:
total_video.append(frame_str)
frame_str = ""
row += 1
fi.close()
for frame in total_video:
sys.stdout.write(frame)
sys.stdout.flush() # 清空屏幕
sys.stdout.write("\x1b[0;0H") # 重新定位打印光标于最开始的为止(0,0)
time.sleep(0.015) # 设置与下一帧的打印时间间隔
注意:这里要选择将中间字符保存到文件,然后再读取打印,这是因为在打印的过程中读取和打印都需要时间,所以如果设置sleep时间为0.0278,那么将会出现播放时间延迟不同步,因此需要调整sleep的时间。为了避免每次调试重新转化需要大量时间,这里就进行了保存。
总结:转换图片到字符画并不是什么难的事情,不过在下发现转换以后的字符画跟原图相比就跟打了码一样,那么不免想到不妨用DL技术训练一个模型用于还原万恶的马赛克,岂不美哉2333
参考
http://paulbourke.net/dataformats/asciiart/
http://pillow.readthedocs.io/en/3.0.x/handbook/tutorial.html
opencv官方文档(随便找找,这边忘了链接就不给了)