Bad apple for CSharp
前言:记得10年的时候我还在上学,有一天逛csdn看到了字符版的badapple,感觉这东西好NB啊,然后就下载了一份,最近整理博客就把他整理博客,原作者是谁真心不知道,这是在果壳看到的.
Bad Apple最初是作为日本同人游戏社团“上海爱莉丝幻乐团”所制作的“东方Project”系列游戏背景音乐出现的。后来在日本著名视频站点NicoVideo上有人萌发了为此系列制作MADMovie的想法,最后有了这段3D制作的影绘风格视频:
这段视频由不断变换的影绘风格游戏人物组成,完全黑白,视觉震撼力十足。此视频在网络上引发了一场宏大的运动,各种技术宅使用了各种方法来演奏这段三分多钟长的视频。
看完本文后,你将能做出如下效果的视频:
1 所需工具
● Visual Studio 2010
● KMPlayer 3.0
2 思路原理
● 我们想要做的就是在Windows控制台(也就是命令提示符,俗称“DOS窗口”)下播放Bad Apple视频,视频的基本原理就是利用人眼的视觉暂留效果,快速在控制台输出字符-清屏-输出字符-清屏,达到“动画”的效果。
而输出的字符需要从原始视频获取,将原始视频压缩成一个较低的分辨率,再获取到每一帧上的所有像素点,转换成对应的字符串。
3 确定思路
● 首先要选定使用的语言,这个根据大家的爱好,随便C/C++、Java、Python等等绝大部分编程语言都可以实现,这里选用的是C#。
● 关于素材的来源,这个是难度最大的步骤。原始1080P视频素材在网上很容易下到,但要处理成我们需要的字符串形式。也就是在视频黑色像素的位置输出一个字符,白色像素的位置输出为空格,每一帧都依据像素排列位置输出成这种形式,如图:
● 获取字符图像大概有两个思路:
1. 根据视频格式(我下到的是H264编码方式的),读取二进制文件并进行转码;
2. 使用第三方工具或者类库,将视频的每一帧转换为图片格式,再将图片转换为所需字符串形式。
● 第一种方式需要研究视频编码的问题,而且比较复杂,因此我们选择取巧的方式。KMPlayer是个很好的视频播放软件,它的截图功能非常强大,可以将视频每一帧图像都保存为截图,完全能够满足我们的需要。
4 捕获图像
● 使用KMPlayer打开原始视频,点击右键选择“捕获”->“高级捕获”(如果没有捕获项,请勾选“设置”->“高级菜单”项),就可以了。
● 需要考虑的是分辨率与帧数的问题,Windows7下1366×768分辨率默认命令提示符窗口最大一行显示80个字符,最大44列,因此我们截取的视频分辨率应该小于等于这个值。当然还可以在后面过程中在调整分辨率,但要设计插值计算的问题,因此还是交给KMPlayer比较轻松一些。
● 另外命令提示符每个字符的上下间距与左右间距是不同的,上下间距要高上许多,如果按照视频原始宽高比截图的话最后输出的画面会有上下拉伸现象,因此截图的时候就要故意把视频截的“胖”一些。
● 最后综合考虑,决定一秒截25帧图像,图像分辨率为80×30。
● 这时还可以顺便将音频也用KMPlayer截取下来,以便保证视频与音频的同步(如果再如此折腾下还没有对此歌产生审美疲劳的话还可以丢进MP3里听)。音频比较简单,从头到尾录制,保存为mp3或者其他常见格式就可以,只要你使用的语言有可用的将它播放出来的库就可以。
● 截取的图片一共有3553张,保存在一个文件夹中。
● 其实这个时候就有一个简单的方法将这些静态图片播放出来了,只要使用Picasa附带的图片查看器打开第一帧图片,鼠标点中下一张,如何,动起来了吧?其他能够点种下一张不放就可以连续切换图片的查看器也能实现同样的效果。
5 编码
● 下面就可以开始编码了,思路很容易,依次读取每一张图片,遍历图片上每一个像素点,如果是黑色的话输出一个字符(这里用#字符,因为#块头比较大并且用的是C#),白色的话输出一个空格,将字符串保存在一个文本文件里。
● 仔细观察保存的图片时会发现一个问题,Bad Apple视频里并不是绝对的黑色与白色,还有少量的灰色作为渐变色彩,因此判断黑白的时候还是根据RGB值,只要其中一项大于200便当作黑色处理就好了。
● 第一次保存的时候发现数据量比较大,未压缩大概有将近10M大小,有些难以接受。因为保存的文本大部分字符都是重复的,因此压缩空间应该非常大,于是使用.net自带的Gzip压缩了一下,果然文件大小降至517KB了。
● 此时还可以用另外一种方法播放这段“文本视频”,只要用记事本打开数据文件,调整记事本窗口的宽度与高度(宽度大于一行80个字符,高度刚好每行30个字符),然后按下PageDown按钮,看到效果了吧~!
6 输出
● 剩下的工作就比较简单,只要读取这个文件,并在控制台不停的输出,每次输出30行(因为横向分辨率为30),然后清屏,继续输出。只要设定好每次输出时间的间隔,与截屏的间隔相符的话,动画就可以顺利的在控制台播放出来了。
● 只要仔细调节截屏的帧数、分辨率,控制台的宽度与高度、输出时间的间隔,就一定可以顺利播放出来,当然这个过程比较繁琐。
● 最后给程序添加上背景音乐,这里使用第三方的NAudio库来播放我们一开始截取的音频文件,只要你之前调节的参数没有问题,音频应该和动画达到神同步的境界的。让我们随着Bad Apple的旋律哼唱着“多少红颜为傻逼 多少傻逼不珍惜”来运行我们的程序吧!
7 源码预览
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 //获取in目录下所有文件 未作异常处理 6 string[] path = Directory.GetFileSystemEntries("in"); 7 8 //保存为Gzip压缩的文本流 9 FileStream fileStream = new FileStream("badapple.dat", FileMode.Create, FileAccess.Write); 10 GZipStream compressionStream = new GZipStream(fileStream, CompressionMode.Compress); 11 StreamWriter sw = new StreamWriter(compressionStream); 12 13 //遍历每个像素点 14 for (int x=0;x<path.Length;x++) 15 { 16 //Console.WriteLine(path[x]); 17 Bitmap bitmap = new Bitmap(path[x]); 18 19 for (int i = 0; i < bitmap.Height; i++) 20 { 21 for (int j = 0; j < bitmap.Width; j++) 22 { 23 Color color = bitmap.GetPixel(j, i); 24 if (color.R > 200) 25 { 26 //Console.Write("1"); 27 sw.Write(" "); 28 } 29 else 30 { 31 //Console.Write("0"); 32 sw.Write("#"); 33 } 34 } 35 //Console.WriteLine(); 36 sw.WriteLine(); 37 Console.Clear(); 38 Console.WriteLine("{0} of {1} completed! ",x,path.Length); 39 } 40 bitmap.Dispose(); 41 } 42 sw.Dispose(); 43 } 44 45 }
1 class Program 2 { 3 private static StreamReader reader; 4 static void Main(string[] args) 5 { 6 //打开压缩文本流 7 FileStream fileStream = new FileStream("badapple.dat", FileMode.Open, FileAccess.Read); 8 GZipStream compressionStream = new GZipStream(fileStream, CompressionMode.Decompress); 9 reader = new StreamReader(compressionStream); 10 11 //初始化控制台参数 12 Console.Title = "BadAppleSharp 0.1"; 13 Console.WindowWidth = 80; 14 Console.WindowHeight = 31; 15 16 //调用播放器播放背景音乐 17 WaveStream mp3Reader = new Mp3FileReader(@"BadApple.mp3"); 18 WaveStream pcmStreamer = WaveFormatConversionStream.CreatePcmStream(mp3Reader); 19 WaveStream blockAlignedStream = new BlockAlignReductionStream(pcmStreamer); 20 WaveOut waveOut = new WaveOut(WaveCallbackInfo.FunctionCallback()); 21 waveOut.Init(blockAlignedStream); 22 waveOut.Play(); 23 24 //初始化定时器 25 Timer timer = new Timer(55); 26 timer.Elapsed+=new ElapsedEventHandler(Render); 27 timer.Enabled = true; 28 29 Console.ReadKey(); 30 waveOut.Dispose(); 31 blockAlignedStream.Dispose(); 32 pcmStreamer.Dispose(); 33 mp3Reader.Dispose(); 34 } 35 36 //渲染一帧 37 public static void Render(Object sender,ElapsedEventArgs e) 38 { 39 { 40 Console.Clear(); 41 StringBuilder data=new StringBuilder(2500); 42 for (int i = 0; i < 30; i++) 43 { 44 data.Append(reader.ReadLine()); 45 } 46 Console.Write(data.ToString()); 47 } 48 49 } 50 }
8 源码下载
程序及源代码下载 (需要本机安装.net2.0或以上运行环境)
转载自果壳艾斯昆