Featured image of post Python实现视频转gif字符动态图

Python实现视频转gif字符动态图

前言

最近在网上看到一个非常有好玩技术,可以把彩色照片转成字符图片,先看看效果。

动态效果图

原理

把图片中每个像素点转成灰度图(黑白电视的展现方式),然后根据每个像素的灰度值用不同的字符去替换。其中最大的问题是转字符后如何和原始图片尺寸保持一致,翻阅了很多资料才勉强看上去一致。

关键步骤

  1. 从视频中提取图片
  2. 图片转字符文本
  3. 字符文本转字符图片
  4. 字符图片合成gif动态图
#!/usr/bin/python3  
# coding: utf-8  
import os  
import imageio.v2 as imageio  
import cv2  
from PIL import Image, ImageDraw, ImageFont  
  
# ascii_char = list("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. ")  
ascii_char = list('MNHQ$OC?7>!:-;. ')  
  
  
def get_char(r, g, b, alpha=256):  
    if alpha == 0:  
        return ' '  
    # 这是一个优化,将浮点运算转成整型运算  
    gray = (2126 * r + 7152 * g + 722 * b) / 10000  
    # 为什么是1.0 因为alpha、gray是整型运算的时候按整型算如果小于1的话按0算  
    char_idx = int((gray / (alpha + 1.0)) * len(ascii_char))  
    return ascii_char[char_idx]  
  
  
def text2image(content, width, height, filename):  
    '''  
    字符转图片  
    :param content: 字符数组  
    :param width: 输出的图片宽度  
    :param height: 输出的图片高度  
    :param filename: 输出的图片名称  
    :return:    '''    image = Image.new("RGB", (width, height), (255, 255, 255))  
    canvas = ImageDraw.Draw(image)  
    font = ImageFont.truetype('JetBrainsMono-Thin.ttf')  
    fillcolor = "#000000"  
  
    x, y = 0, 0  
    font_w, font_h = 6, 12          # 设置单个字符长款  
    for i in range(len(content)):  
        if content[i] == '\n':  
            x = -font_w  
            y += font_h  
            continue  
        canvas.text((x, y), content[i], fill=fillcolor, font=font)  
        x += font_w  
    image.save(filename)  
  
  
def image2textimage(file_name="test.jpg", multiple=1.0, out_file_name='output.txt'):  
    '''  
    将图片转成字符图片  
    :param file_name:    :param multiple: 缩放比例  
    :param out_file_name:    :return:    '''    text = ''  
    im = Image.open(file_name)  
    width, height = int(im.size[0] * multiple), int(im.size[1] * multiple)  
    width_text, height_text = int(width / 6), int(height / 12)     # 原始图片长款等比例缩放  
    im = im.resize((width_text, height_text), Image.NEAREST)  
    for i in range(height_text):  
        for j in range(width_text):  
            text += get_char(*im.getpixel((j, i)))  
        text += '\n'  
    # with open(out_file_name, 'w') as f:  
    #     f.write(text)    text2image(text, width, height, file_name + '_txt.png')  
    return file_name + '_txt.png'  
  
def video2images(source_video, start_time, end_time, interval=1.0):  
    '''  
    将视频转保存为图片s  
    :param source_video: 视频文件  
    :param start_time: 开始时间/s  
    :param end_time: 结束时间/s  
    :param interval: 时间间隔/s  
    :return:    '''    # 创建零时目录  
    tmppath = os.path.join(os.getcwd(), 'tmp')  
    if not os.path.exists(tmppath):  
        os.mkdir(tmppath)  
  
    cap = cv2.VideoCapture(source_video)  
    if not cap.isOpened():  
        print("Error: Could not open video.")  
        return []  
    else:  
        fps = cap.get(5)  # 帧速率  
        frame_number = cap.get(7)  # 视频文件的帧数  
        duration = frame_number / fps  # 视频总帧数/帧速率 是时间/秒  
  
        print("总时长[%.2f]秒,开始时间[%.2f]秒结束时间[%.2f]秒" % (duration, start_time, end_time))  
        if start_time > duration or end_time > duration:  
            print("Error: 截取时间断异常.总时长[%d]秒,开始时间[%d]秒结束时间[%d]秒" % (duration, start_time, end_time))  
            return []  
  
        start_frame = int(fps * float(start_time))  
        end_frame = int(fps * float(end_time))  
        interval_frame = int(fps * float(interval))  
  
        print("总帧数[%d],帧率[%d],开始帧数[%d],结束帧数[%d], 间隔[%d]" % (frame_number, fps, start_frame, end_frame, interval_frame))  
        num = 0  
        images = []  
        while True:  
            success, frame = cap.read()  
            if success:  
                if int(start_frame) <= int(num) <= int(end_frame):  
                    if success and int(num) % int(interval_frame) == 0:  
                        image_name = os.path.join(tmppath, f"frame_{num}.png")  
                        cv2.imwrite(image_name, frame)  
                        images.append(image_name)  
            num += 1  
            if num > frame_number:  
                break  
        cap.release()  
        return images  
    return texts  
  
  
def image2gif(textimages, output, fps=2):  
    images=[]  
    for pic in textimages:  
        images.append(imageio.imread(pic))  
    imageio.mimsave(output, images, format='GIF', fps=fps, loop=0)  
  
def fontText(path=''):  
    '''  
    查看文本字符宽度和高度  
    :param path:    :return:    '''    font = ImageFont.truetype(path)  
    for t in ascii_char:  
        fbox = font.getbbox(t)  
        print(t, fbox[2]-fbox[0], fbox[3]-fbox[1])  
  
if __name__ == '__main__':  
    images = video2images('庆余年2.mov', start_time=3, end_time=21)  
    textImages = []  
    for im in images:  
        textImages.append(image2textimage(im))  
    image2gif(textImages, 'output.gif')

完成项目内容见附件:源码附件

参考

使用python中的imageio生成gif动态图
Python写实用小工具-实现图片转字符画
使用Python完成曾火爆全网的图片转符号图片、GIF图像转字符GIF动画操作