python视频字符化带你回到2018年的夏天

先看看原图和对比图吧:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
涉及到的第三方库有:

1`opencv``PIL``moviepy`,`theading 2 3

没有安装的请自行pip安装

main函数三个参数分别为(原始视频路径,字符视频名称,最终视频名称)

其中第二个参数字符视频名称为中间过程产品,导出的视频是没有声音的


然后说说具体思路吧:

简而言之,就是将视频逐帧拆分并操作,最后进行合并

拆分合并主要使用了opencv,但是这样做得到的是无声的,因此需要用moviepy将声音添加进去

逐帧的图像处理使用了PIL库

为了加速,使用了多线程

然后上源码:(注释都在里面了,有什么不懂的可以私信或在评论区里提)

1import cv2 2import os 3import threading 4from PIL import Image, ImageDraw, ImageFont 5from moviepy.editor import concatenate_videoclips, VideoFileClip 6 7chars = list("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. ") 8 9frames = r"..\pictures\\" 10frames_char = r"..\pitcures_char\\" 11 12width, height, fps, count = [None] * 4 13 14 15def get_video(pathv): 16 """ 17 导入视频 18 :param pathv:视频路径 19 :return: None 20 """ 21 global width, height, fps, count 22 vc = cv2.VideoCapture(pathv) # 导入视频 23 frame_No = 0 # 帧编号 24 ret = vc.isOpened() # 判断载入的视频是否可以打开 25 # 获取视频参数 26 fps = vc.get(cv2.CAP_PROP_FPS) 27 count = vc.get(cv2.CAP_PROP_FRAME_COUNT) 28 width = int(vc.get(cv2.CAP_PROP_FRAME_WIDTH)) 29 height = int(vc.get(cv2.CAP_PROP_FRAME_HEIGHT)) 30 while ret: 31 ret, frame = vc.read() 32 if ret: 33 cv2.imwrite(frames + str(frame_No) + '.jpg', frame) 34 frame_No += 1 35 cv2.waitKey(1) # 1ms延时 36 else: 37 break 38 vc.release() 39 40 41def get_char(r, g, b, alpha=256): 42 """ 43 根据每一帧的RGB获取每种颜色对应的字符 44 :param r: R 45 :param g: G 46 :param b: B 47 :param alpha: Alpha 48 :return: Integer 49 """ 50 if alpha == 0: 51 return ' ' 52 return chars[int(int(0.2126 * r + 0.7152 * g + 0.0722 * b) / ((256.0 + 1) / len(chars)))] 53 54 55def get_frame_char(begin=0, end=count, is_gray=True): 56 """ 57 将每一帧转化为字符 58 :param begin: 开始帧位置 59 :param end: 结束帧位置 60 :param is_gray: 是否为灰色,默认为True 61 :return: picture 62 """ 63 for frame_No in range(begin, end): 64 img = frames + str(frame_No) + ".jpg" 65 if os.path.exists(img): 66 im = Image.open(img).convert('RGB') # 将图片转化为RGB格式 67 new_width = int(im.width) 68 new_height = int(im.height) 69 # 获取设定的字体的尺寸,ImageFont默认的尺寸大小为6x11,其他字体会有所不同 70 # 此处使用的字体为truetype字体,大小为10px 71 font = ImageFont.truetype('consola.ttf', 10, encoding='unic') 72 font_x, font_y = font.getsize(' ') 73 # 确定单元的大小 74 unit_width = int(font_x) 75 unit_height = int(font_y) 76 # 确定长宽各有几个单元 77 w = int(new_width / unit_width) 78 h = int(new_height / unit_height) 79 # 将每个单元缩小为一个像素 80 im = im.resize((w, h), Image.NEAREST) 81 # txts和colors分别存储对应块的ASCII字符和RGB值 82 txts = [] 83 colors = [] 84 for i in range(h): # 遍历行 85 line = '' 86 lineColor = [] 87 for j in range(w): # 遍历列 88 pixel = im.getpixel((j, i)) 89 lineColor.append((pixel[0], pixel[1], pixel[2])) 90 line += get_char(pixel[0], pixel[1], pixel[2]) 91 txts.append(line) 92 colors.append(lineColor) 93 # 创建新画布 94 im_txt = Image.new("RGB", (new_width, new_height), (255, 255, 255)) 95 # 创建ImageDraw对象以写入ASCII 96 draw_handle = ImageDraw.Draw(im_txt) 97 for j in range(len(txts)): 98 for i in range(len(txts[j])): 99 if is_gray: # 灰色 100 draw_handle.text((i * unit_width, j * unit_height), txts[j][i], (50, 50, 50)) 101 else: # 原色 102 draw_handle.text((i * unit_width, j * unit_height), txts[j][i], colors[j][i]) 103 name = frames_char + str(frame_No) + '.jpg' 104 im_txt.save(name, 'JPEG') 105 106 107def severalThreadings(isgray=True): 108 """ 109 多线程处理灰度图像 110 :param isgray: 是否为灰色,默认为True 111 :return: pictures 112 """ 113 lines = 5 114 eachPice = int(count / lines) # 每个线程所需处理帧数 115 threads = [] 116 for line in range(lines): # 对于一片进行处理,然后计算扩展到 117 begin = line * eachPice 118 end = (line + 1) * eachPice 119 if line == lines - 1: # 如果是最后一个线程,则其工作为收尾工作,将图片进行到最后 120 end = int(count) 121 thread = threading.Thread(target=get_frame_char, args=(begin, end, isgray)) 122 threads.append(thread) 123 thread.setDaemon(True) 124 thread.start() 125 for item in threads: 126 item.join() 127 128 129def create_video(pathv): 130 # 输出视频参数设置,包含视频文件名、编码器(MJPG)、帧率、视频宽高(此处和字符图片大小一致) 131 video_writer = cv2.VideoWriter(pathv, cv2.VideoWriter_fourcc(*'MJPG'), fps, (width, height)) 132 for i in range(1, 1000): 133 filename = frames_char + str(i) + '.jpg' 134 if os.path.exists(filename): # 图片存在 135 img = cv2.imread(filename=filename) 136 cv2.waitKey(100) # 延时1ms 137 video_writer.write(img) # 将图片写入视频中 138 video_writer.release() 139 140 141def add_audio(initial_video, char_video, end_video): 142 """ 143 添加音频 144 :param initial_video: 原始视频路径 145 :param char_video: 字符视频名称 146 :param end_video: 最终视频名称 147 :return: video 148 """ 149 ini_video = VideoFileClip(initial_video) # 原始视频 150 audio = ini_video.audio # 提取音频 151 # audio.write_audiofile(initial_video+".mp3") # 保存音频 152 ch_video = VideoFileClip(char_video) # 字符视频 153 ch_video1 = ch_video.set_audio(audio) # 字符视频添加音频 154 ch_video2 = concatenate_videoclips([ch_video1]) 155 ch_video2.write_videofile(end_video) 156 # 也可以一行代码解决: 157 # concatenate_videoclips([VideoFileClip(char_video).set_audio(VideoFileClip(initial_video).audio)]).write_videofile() 158 159 160def main(pathv, charv, endv, isgray=True): 161 """ 162 主函数 163 :param pathv: 原始视频路径 164 :param charv: 字符视频名称 165 :param endv: 最终视频名称 166 :param isgray: 是否灰色,默认为True 167 :return: video 168 """ 169 if not os.path.exists(frames): 170 os.mkdir(frames) 171 if not os.path.exists(frames_char): 172 os.mkdir(frames_char) 173 get_video(pathv) 174 severalThreadings(isgray=isgray) 175 create_video(charv) # 生成字符视频 176 add_audio(pathv, charv, endv) # 字符视频添加音频 177 os.system("rd /s /q " + frames) 178 os.system("rd /s /q " + frames_char) 179 180 181if __name__ == '__main__': 182 main("2018.mp4", "silent_2018_gray.mp4", "char_2018_gray.mp4", isgray=True) 183 main("2018.mp4", "silent_2018_normal.mp4", "char_2018_normal.mp4", isgray=False) 184 185 186

接着上视频结果:


视频字符化

B站高清视频地址:https://www.bilibili.com/video/BV1rZ4y1x739


视频字符化保留原色

B站高清视频地址:https://www.bilibili.com/video/BV1DC4y1p78o


这是我自制的一个:

程序猿是怎样回忆2018年夏天的

B站高清视频地址:https://www.bilibili.com/video/BV1gK4y1k7vV


(说实话,我在B站上传这视频还审核不通过了一次呢…🤦)

还有就是,这个东西的只是一个雏形,后面我还会将他GUI(大概率用PyQt5),大家可以关注一下~

这篇文章的G站地址:https://github.com/sleepyyoung/VideoToChar

代码交流 2021