python 将视频转化为字符画之badapple

看了用PYTHON制作字符动画演示科技bilibili哔哩哔哩弹幕视频网后觉得挺好玩,复现了一下,整体思路很简单,将视频分解为图片,然后将图片逐一转换为字符画,然后利用浏览器进行逐帧播放。

浏览器播放效果
  • 首先安装FFmpeg,一款开源的视频软件,有丰富的视频处理功能。如何在Windows上安装FFmpeg程序

  • 然后使用window下的批处理batch对视频抓帧,在工作目录下新建run.bat

mkdir images
set /p input="input file:"
set /p rate="set frame rate(Hz value, fraction or abbreviation):"
set /p output="output file:"
ffmpeg -i %input% -r %rate% %output%

然后双击该bat运行,在第一句后输入被转化视频名称,在第二句后指定抓帧频率,为33.333(与后文代码中的播放间隔相对应),在第三句后输入images/%d.bmp,将所有转化图片放于工作目录下的images文件夹中。

抓帧

  • 测试单张转换。在这里,仅仅先将图片转换为灰阶,然后对每个像素点做判断,根据黑白分别转化为'@'' '(空格),最后输出文本到html中。
 import os
os.chdir(r'F:\badapple!!\images') # 转移到工作目录
from PIL import Image, ImageFont, ImageDraw
originalImage = Image.open('6382.bmp')# 图片尺寸为(480, 360)
grayImage = originalImage.resize((120, int(90/2))).convert('L')
# 将图片尺寸缩小,即减少像素点,并转换为灰阶
# int(90/2) 因为字符的高约是宽的两倍,由于之后是一个像素点替换为一个字符,所以提前将高缩小一倍
resulttext = ''
for row in range(grayImage.size[1]): # 先行后列
    for col in range(grayImage.size[0]):
        pixel = grayImage.getpixel((col, row))
        char = '@' if pixel < 127 else ' ' # 像素点值为0是黑色
        resulttext += char
    resulttext += '\n'
#print(resulttext)
head = '''
<html>
<head>
</head>
<style>
pre {font-size:14px; line-height:14px}
</style>
<body>
<pre>
'''
foot = '''
</pre>
</body>
</html>
'''
with open('1.html','w') as f:
    f.write(head)
    f.write(resulttext)
    f.write(foot)
单张转换效果
  • 由于上述对图像的转化只有黑白两个层次,太过简单,表现力不够丰富,所以我们将一系列字符按一定规则排序,然后匹配到相应的灰度上。我们先从网上下载一份simsun的字体文件来提供画图中的字体,然后利用PIL中的画图功能,先创建一个最大字符尺寸的矩形白板,然后在上面画上字符,然后计算它的平均像素(每个像素点*该点像素值/总像素点数),根据平均像素来排序。然后再做下单张测试。此外,由于文本输出到html文件,所以需要html.escape()函数进行转义。

补充:chr函数将数字转换为ACII码对应的字符
chr
### 然后对像素对字符的转换方式进行改进
font= ImageFont.truetype('../simsun.ttf', 14)
chars = list(chr(i) for i in range(32, 126))
sizeList = list(font.getsize(char) for char in chars)
import functools
maxSize = functools.reduce(lambda x,y:(max(x[0],y[0]), max(x[1],y[1])), sizeList)
#(8, 15)
tempCharImage = Image.new('L', maxSize, 'white')
tempCharDraw = ImageDraw.Draw(tempCharImage)
charDegreeDict = {}
for char in chars:
    tempCharDraw.rectangle([(0,0), maxSize], fill='white')#在(0,0)位置处以白色填充一个canvasSize的矩形。
    tempCharDraw.text((0,0), char, font=font)
    pixelColor = tempCharImage.getcolors()#返回当前图片上的所有色彩及其像素点数的列表,[(个数,色彩),(个数,色彩),...]
    grayDegree = sum(pixelnum*color for pixelnum,color in pixelColor)/(maxSize[0]*maxSize[1])
    charDegreeDict[char] = grayDegree
sortedCharDegreeList = sorted(charDegreeDict.items(), key=lambda d:d[1])
sortedCharDegreeList = list(i[0] for i in sortedCharDegreeList)
charsIndexMax = len(sortedCharDegreeList) -1 
# ['M', 'W', '@', 'N', 'H', '%', 'X', '$', 'B', 'K', 'R', '#', 'E', 'U', 'Q', 'D', '&', 'F', 'w', '8', 'k', '9', '6', 'h', 'm', 'A', 'P', 'p', 'd', '*', 'V', 'g', 'Y', '0', '5', 'b', 'q', 'G', 'O', 'n', 'x', 'f', 'S', 'J', 'T', 'Z', '3', 'y', 'u', 'I', 'C', 'L', '2', 'a', ']', '[', '1', '?', 'l', 's', 'e', 'r', '|', '}', '{', 't', 'z', 'o', 'v', '4', '+', '/', 'i', 'j', '=', 'c', '7', '(', ')', '!', '\\', '_', '>', '<', ':', '-', '"', "'", ',', ';', '.', '^', '`', ' ']
### 按灰度替换字符重新测试
import os
os.chdir(r'F:\badapple!!\images')
from PIL import Image, ImageFont, ImageDraw
originalImage = Image.open('6382.bmp')# 图片尺寸为(480, 360)
grayImage = originalImage.resize((120, int(90/2))).convert('L')
# 将图片尺寸缩小,即减少像素点,并转换为灰阶
# int(90/2) 因为字符的高是长的两倍,由于之后是一个像素点替换为一个字符,所以提前将高缩小一倍
resulttext = ''
for row in range(grayImage.size[1]):
    for col in range(grayImage.size[0]):
        pixel = grayImage.getpixel((col, row))
        char = sortedCharDegreeList[int(pixel/255*charsIndexMax)] # 像素点值为0是黑色,255为白色
        resulttext += char
    resulttext += '\n'
#print(resulttext)
head = '''
<html>
<head>
</head>
<style>
pre {font-family:simsun;font-size:14px; line-height:14px}
</style>
<body>
<pre>
'''
foot = '''
</pre>
</body>
</html>
'''
with open('1.html','w') as f:
    f.write(head)
    import html
    f.write(html.escape(resulttext))
    f.write(foot)
多层次字符替换
  • 然后就是对所有图片进行转换了,利用glob找出工作目录images文件夹下的所有bmp图片,依次处理。所有字符图片存于html文件中的<pre></prev>标签中,一个标签对应一个图,然后在js代码中每隔30秒进行下一张图的显示和前一张的隐藏,这样就实现了播放。

补充:glob的用法

import glob  
#获取指定目录下的所有图片  
print glob.glob(r"E:\Picture\*\*.jpg")  
#获取上级目录的所有.py文件  
print glob.glob(r'../*.py') #相对路径  
### ffmpeg -i "Touhou - Bad Apple!!  PV.webm" -f mp3 -vn apple.mp3 可以输出音频,暂时没有用到
workdir = r'F:\badapple!!\images'
### 按灰度替换的字符列表
sortedCharDegreeList = ['M', 'W', '@', 'N', 'H', '%', 'X', '$', 'B', 'K', 'R', '#', 'E', 'U', 'Q', 'D', '&', 'F', 'w', '8', 'k', '9', '6', 'h', 'm', 'A', 'P', 'p', 'd', '*', 'V', 'g', 'Y', '0', '5', 'b', 'q', 'G', 'O', 'n', 'x', 'f', 'S', 'J', 'T', 'Z', '3', 'y', 'u', 'I', 'C', 'L', '2', 'a', ']', '[', '1', '?', 'l', 's', 'e', 'r', '|', '}', '{', 't', 'z', 'o', 'v', '4', '+', '/', 'i', 'j', '=', 'c', '7', '(', ')', '!', '\\', '_', '>', '<', ':', '-', '"', "'", ',', ';', '.', '^', '`', ' ']
charsIndexMax = len(sortedCharDegreeList) -1 
import os, glob, html
os.chdir(workdir)
from PIL import Image, ImageFont, ImageDraw
result = []
imgs = glob.glob('*.bmp')
imgs = sorted(imgs, key=lambda x: int(x.split('.')[0])) # 这里对图片路径进行了处理,取后缀前的数字值进行排序
#
for img in imgs:
    originalImage = Image.open(img)# 图片尺寸为(480, 360)
    grayImage = originalImage.resize((120, int(90/2))).convert('L')
    # 将图片尺寸缩小,即减少像素点,并转换为灰阶
    # int(90/2) 因为字符的高是长的两倍,由于之后是一个像素点替换为一个字符,所以提前将高缩小一倍
    resulttext = ''
    for row in range(grayImage.size[1]):
        for col in range(grayImage.size[0]):
            pixel = grayImage.getpixel((col, row))
            char = sortedCharDegreeList[int(pixel/255*charsIndexMax)] # 像素点值为0是黑色,255为白色
            resulttext += char
        resulttext += '\n'
    result.append(resulttext)
    print(img,'is done!')
#
head = '''
<html>
<head>
</head>
<style>
pre {display:none;font-family:simsun;font-size:14px; line-height:14px}
</style>
<script>
window.onload = function(){
    var pres = document.getElementsByTagName('pre');
    var i = 0;
    var play = function(){
        if(i > 0){
            pres[i-1].style.display = 'none';
        }
        pres[i].style.display = 'inline-block';
        i++;
        if(i == pres.length){
            clearInterval(run)
        }
    }
    run = setInterval(play, 30)
}
</script>
<body>
'''
foot = '''
<video width="480" height="360" controls="controls" autoplay="autoplay">
  <source src="../Touhou - Bad Apple!!  PV.webm" type="video/webm" />
</video>
</body>
</html>
'''
with open('2.html','w') as f:
    f.write(head)
    for resulttext in result:
        f.write("<pre>")
        f.write(html.escape(resulttext))
        f.write("</pre>")
    f.write(foot)
最终效果
  • 最后你可以优化一下字符的替换方式,去掉某些显示效果不好的字符。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容