ascii 字符画

https://www.asciiart.eu/image-to-ascii

原理:

先将图像转为灰度图像,灰度图像通常使用 8 位表示,每个像素可以有 256 个不同的灰度值,范围从 0 到 255。其中:

  • 0 表示黑色
  • 255 表示白色
  • 0 到 255 之间的值表示不同的灰度级别(如 128 为中灰色)。

将这些像素映射为对应字符,字符集长度越大,表示的层次越丰富(也要根据字符的稀疏对应,比如'表示的颜色比$浅)

ASCII_CHARS = ['`', '^', '"', ',', ':', ';', 'I', 'l', '!', 'i', '~', '+', '_', '-', '?', ']', '[', '}', '{', '1', ')', '(', '|', '\\', '/', '*', 'j', 't', 'f', 'r', 'x', 'n', 'v', 'c', 'z', 'X', 'Y', 'U', 'J', 'C', 'L', 'Q', '0', 'O', 'Z', 'm', 'w', 'q', 'p', 'd', 'b', 'k', 'h', ')', 'W', 'M', 'B', '8', '$', '%', '&', '@', '#']

当字符集有限的情况下,需要按比例划分灰度值,将一个范围的灰度映射为对应的字符

chars = ASCII_CHARS[pixel * len(ASCII_CHARS) // 256]

现有原图:

can

转换为 ascii 字符后:

ascii can

emm…,好像不是特别美观

完整代码
from PIL import Image

# 定义要使用的字符集,字符集长度越大,表示层次越丰富
ASCII_CHARS = ['`', '^', '"', ',', ':', ';', 'I', 'l', '!', 'i', '~', '+', '_', '-', '?', ']', '[', '}', '{', '1', ')', '(', '|', '\\', '/', '*', 'j', 't', 'f', 'r', 'x', 'n', 'v', 'c', 'z', 'X', 'Y', 'U', 'J', 'C', 'L', 'Q', '0', 'O', 'Z', 'm', 'w', 'q', 'p', 'd', 'b', 'k', 'h', ')', 'W', 'M', 'B', '8', '$', '%', '&', '@', '#']
print(ASCII_CHARS)
print(len(ASCII_CHARS))

# 调整图像大小
def resize_image(image, new_width=100):
    width, height = image.size
    ratio = height / width
    # 0.55 矫正宽高比
    new_height = int(ratio * new_width * 0.55)
    return image.resize((new_width, new_height))

# 将图像转为灰度
def grayify(image):
    return image.convert("L")

# 将像素值映射到ASCII字符
def pixels_to_ascii(image):
    pixels = image.getdata()
    ascii_str = "".join([ASCII_CHARS[pixel * len(ASCII_CHARS) // 256] for pixel in pixels])
    return ascii_str

# 主函数,执行图像到ASCII的转换
def image_to_ascii(image_path, new_width=100):
    # 打开图像并处理
    try:
        image = Image.open(image_path)
    except Exception as e:
        print(f"无法打开图像文件: {e}")
        return

    # 调整图像大小和灰度
    image = resize_image(image, new_width)
    image = grayify(image)

    # 转换为ASCII字符
    ascii_str = pixels_to_ascii(image)

    # 将ASCII字符串按宽度分行
    img_width = image.width
    ascii_str_len = len(ascii_str)
    ascii_img = "\n".join([ascii_str[i:i + img_width] for i in range(0, ascii_str_len, img_width)])

    # 输出或保存ASCII图像
    print(ascii_img)

    # 可以选择将ASCII字符输出到文件中
    with open("ascii_image.txt", "w") as f:
        f.write(ascii_img)

# 调整宽度参数来控制ASCII图像的分辨率
image_to_ascii("can.png", 600)

视频 ascii

对视频的处理就是对每一帧进行处理

reader = imageio.get_reader('bad_apple.mp4')
for frame in reader:
    # 将帧转换为灰度 ASCII 并显示
    image = Image.fromarray(frame)
    ascii_img = image_to_ascii(image, new_width=80)

为了符合原始视频帧率,需要额外计算延时(粗略计算)

fps = reader.get_meta_data()['fps']
frame_delay = 1.0 / fps  # 每帧显示的时间间隔(秒)

for frame in reader:
    start = time.time()

    end = time.time()
    # 等待适当的时间来同步帧
    time.sleep(frame_delay - (end - start))

由于使用curses控制,需要额外注意宽度:

ascii_img = image_to_ascii(image, new_width=200)
完整代码
import curses
import imageio
import time
from PIL import Image

# 定义要使用的字符集
ASCII_CHARS = ['`', '^', '"', ',', ':', ';', 'I', 'l', '!', 'i', '~', '+', '_', '-', '?', ']', '[', '}', '{', '1', ')', '(', '|', '\\', '/', '*', 'j', 't', 'f', 'r', 'x', 'n', 'v', 'c', 'z', 'X', 'Y', 'U', 'J', 'C', 'L', 'Q', '0', 'O', 'Z', 'm', 'w', 'q', 'p', 'd', 'b', 'k', 'h', ')', 'W', 'M', 'B', '8', '$', '%', '&', '@', '#']

# 调整图像大小
def resize_image(image, new_width=100):
    width, height = image.size
    ratio = height / width
    new_height = int(ratio * new_width * 0.55)
    return image.resize((new_width, new_height))

# 将图像转为灰度
def grayify(image):
    return image.convert("L")

# 将像素值映射到ASCII字符
def pixels_to_ascii(image):
    pixels = image.getdata()
    ascii_str = "".join([ASCII_CHARS[pixel * len(ASCII_CHARS) // 256] for pixel in pixels])
    return ascii_str

# 主函数,执行图像到ASCII的转换
def image_to_ascii(image, new_width=100):
    image = resize_image(image, new_width)
    image = grayify(image)
    ascii_str = pixels_to_ascii(image)
    img_width = image.width
    ascii_str_len = len(ascii_str)
    ascii_img = "\n".join([ascii_str[i:i + img_width] for i in range(0, ascii_str_len, img_width)])
    return ascii_img

def main(stdscr):
    curses.curs_set(0)  # 隐藏光标
    stdscr.nodelay(True)  # 使得 getch() 非阻塞
    stdscr.clear()

    # 打开视频文件
    reader = imageio.get_reader('bad_apple.mp4')
    fps = reader.get_meta_data()['fps']
    frame_delay = 1.0 / fps  # 每帧显示的时间间隔(秒)

    for frame in reader:
        start = time.time()

        # 将帧转换为灰度 ASCII 并显示
        image = Image.fromarray(frame)
        ascii_img = image_to_ascii(image, new_width=80)
        # 绘制 ASCII 图像
        stdscr.clear()
        for y, line in enumerate(ascii_img.split("\n")):
            stdscr.addstr(y, 0, line)
        stdscr.refresh()

        end = time.time()
        # 等待适当的时间来同步帧
        time.sleep(frame_delay - (end - start))

        # 按 'q' 键退出
        if stdscr.getch() == ord('q'):
            break

# 运行 curses 主程序
if __name__ == "__main__":
    try:
        curses.wrapper(main)
    except Exception as e:
        print(f"发生错误: {e}")

output


comment: