BadApple个人试制,大神勿喷。

        一直看网上各种大神做的BadApple代码版视频,所以手痒想自己做一版出来,然而无奈本人编程菜鸟,只能参考别的大神的代码做下自己的修改。对于这里引用到的代码都会贴上链接,并原作者表达深深的敬意,权当是一个小项目自己练手。

        附上项目链接,有需要的可以参考下载:点击打开链接(https://github.com/clauyiye/BadApple)。

        整体思路总结起来主要分为四个步骤:1)原版视频的抽帧,生成每一帧的图像;2)图像转制为字符文档;3)文件的压缩与解压;4)插入音频,调整同步率,生成exe文件。

        系统及工具:Win10 64位系统,Visual Studio2015,集成OpenCV2.0。

        1、视频抽帧生成BMP图像。

        当然素材要准备好.AVI格式的“BadApple”视频文件,以及.WAV格式的音频文件。这一步的处理中有采用第三方软件抽取视频每帧图像的,我这里因为有使用OpenCV2.0图像处理库,因此参考这个帖子《opencv2 用imwrite 抽取并保存视频图像帧(http://www.cnblogs.com/miaojinmin799/p/6845462.html),做了处理。代码如下:

#include "stdafx.h"
#include <opencv2\opencv.hpp> 
#include <opencv2/video/video.hpp>
#include "highgui.h"
#include <iostream>
using namespace cv;
using namespace std;

int main()
{
	VideoCapture capture("badapple.avi");
	if (!capture.isOpened()) {
		cout << "fail to open!" << endl;
	}
	long totalFrameNumber = capture.get(CV_CAP_PROP_FRAME_COUNT);//获取视频总帧数
	cout << "整个视频共" << totalFrameNumber << "帧" << endl;
	double rate = capture.get(CV_CAP_PROP_FPS);//获取帧率
	cout << "帧率为:" << rate << endl;

	stringstream ss;
	int num=0;
	string str;
	char imagename[100];

	while (capture.read(frame))//读入视频
	{
		ss << num;
		ss >> str;
		sprintf_s(imagename, "%s%d%s","./image/badapple_", ++num,".bmp");//指定保存路径与存储名称,存储格式为.BMP
		
		imwrite(imagename, frame);//保存图像
	}
	capture.release();
    return 0;
}

        从而生成出了5000多幅BMP图像,有点庞大... 继续第二步。


        2、图像转制为字符文档。

        1)图像的转制。

        后面的代码就主要参考这个帖子了(先膜拜下):写一个自己的C++控制台字符画版BadApple!!(http://www.cnblogs.com/CodeMIRACLE/p/5508236.html)。将上一步制作好的图像文件夹放入这一步的项目文件夹下,闲话少叙,上代码。

#include "stdafx.h"
#include <cstdio>
#include <cstring>
#include <stdint.h>
#include <windows.h>
int32_t width, height;
RGBQUAD *pixels;
bool OpenBitmap(char const *filename)//位图文件打开函数
{
	FILE *file = fopen(filename, "rb");
	if (file)
	{
		width = 0;
		height = 0;
		BITMAPFILEHEADER bf;
		BITMAPINFOHEADER bi;
		fread(&bf, sizeof(bf), 1, file);
		fread(&bi, sizeof(bi), 1, file);
		if (bi.biBitCount != 24)
			return false;
		if (bi.biCompression != BI_RGB)
			return false;
		width = bi.biWidth;
		height = bi.biHeight;
		pixels = new RGBQUAD[width*height];
		uint32_t rowSize = (bi.biBitCount * width + 31) / 32 * 4;
		uint8_t *line = new uint8_t[rowSize];
		for (int y = 0; y < height; y++)
		{
			fread(line, rowSize, 1, file);
			for (int x = 0; x < width; x++)
			{
				uint8_t *color = line + x * 3;
				RGBQUAD *pixel = &pixels[(height - y - 1) * width + x];
				pixel->rgbBlue = color[0];
				pixel->rgbGreen = color[1];
				pixel->rgbRed = color[2];
			}
		}
		delete[] line;
		fclose(file);
		return true;
	}
	return false;
}
RGBQUAD GetColor(int x, int y, int w, int h)//自定义获取像素颜色函数
{
	int r = 0, g = 0, b = 0;
	for (int i = 0; i < w; i++)
	{
		if (i + x >= width) continue;
		for (int j = 0; j < h; j++)
		{
			if (j + y >= height) continue;
			RGBQUAD const& color = pixels[(y + j) * width + (x + i)];
			r += color.rgbRed;
			g += color.rgbGreen;
			b += color.rgbBlue;
		}
	}
	return RGBQUAD{(BYTE) (r / (w * h)), (BYTE)(g / (w * h)),(BYTE)(b / (w * h)) };
}
char ColorToCharacter(RGBQUAD const& color)//颜色转制字符函数
{
	int brightness = (color.rgbRed + color.rgbGreen + color.rgbBlue) / 3;
	static char const *characters = "8O&*dboc:_. ";
	int len = strlen(characters);
	int span = 0xFF / len;
	int cidx = brightness / span;
	if (cidx == len)
		cidx--;
	return characters[cidx];
}
void OutputAscii(const char* filename, int w, int h)//ASCII码输出函数
{
	FILE *file = fopen(filename, "a+");
	int x = width / w;
	int y = height / h;
	for (int i = 0; i < height; i += y)
	{
		for (int j = 0; j < width; j += x)
		{
			RGBQUAD color = GetColor(j, i, x, y);
			fprintf(file, "%c", ColorToCharacter(color));
			//printf("%c", ColorToCharacter(color));
		}
		fprintf(file, "\n");
		//printf("\n");
	}
	delete[] pixels;
	fclose(file);
}
int main()
{
	char filename[1024];
	printf("转换中,请稍后~\n");
	for (int i = 1; i <= 5262; i++)
	{
		sprintf(filename, "image/badapple_%d.bmp", i);
		if (OpenBitmap(filename))
			OutputAscii("badapple.txt", width / 6, height / 12);
	}
	printf("转换完成!");
}
        2)文件动态预览。

        上一步生成了一个大约5.6M左右的“badapple.txt”文档,可以对其进行下动态预览。由于本人C语言实在初学,里面很多代码都是边看边学,看注释就看出来水平不高了。

#include "stdafx.h"
#include <cstdio>
#include <windows.h>
struct fps_limit {

	int previous_time;
	int tpf_limit;
	int tpf;
	fps_limit(int fps = 60) : previous_time(GetTickCount()), tpf(0) {
		limit_fps(fps);
	}
	void reset() {
		previous_time = GetTickCount(), //GetTickCount()返回从系统启动到当前所经过的毫秒数
			tpf = 0;
		tpf_limit = 60;
	}
	void limit_fps(int fps) {
		tpf_limit = (int)(1000.0f / (float)fps);
	}
	void delay() {
		tpf = GetTickCount() - previous_time; //得到当前过程经过的毫秒数

		if (tpf < tpf_limit)
			Sleep(tpf_limit - tpf - 1); //程序挂起一段时间,单位为毫秒

		previous_time = GetTickCount();
	}
};
int main()
{
	FILE* fp = fopen("badapple.txt", "r");
	char buf[2048];
	fps_limit fps(25);//画面刷新帧率,正常24针/秒,但是应该是转换时出现的误差,因此调整后多少有些不太同步,25为最佳值
	while (!feof(fp))
	{
		HANDLE hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE); //GetStdHandle()用于从一个特定的标准设备
																 //(标准输入、标准输出或标准错误)中取得一个句柄
																 //STD_OUTPUT_HANDLE是个宏,代表标准输出,可以看作显示器
		COORD pos;//定义光标起始点
		pos.X = 0;
		pos.Y = 0;
		SetConsoleCursorPosition(hConsoleOutput, pos);//设置控制台光标坐标,参数就是设备句柄,坐标
		for (int i = 0; i<20; i++) //这里调整的是整个画面的刷新频率,小于20太慢,大于20太快,试出来的( ╯□╰ )
		{
			fgets(buf, 2048, fp);
			printf("%s", buf);
		}
		fps.delay();
	}
	return 0;
}

        3、文件的压制与解压。

        1)文件的压制。

        首先上代码:

#include "stdafx.h"
#include <stdlib.h>
#include <string.h>
#include <cstdio>
#include "zlib.h"

int main()
{
	FILE* fp = fopen("badapple.txt", "r");
	Bytef* buf = NULL;
	Bytef* src = NULL;
	fseek(fp, 0, SEEK_END);
	uLong flen = ftell(fp);
	fseek(fp, 0, SEEK_SET);
	if ((src = (Bytef*)malloc(sizeof(Bytef) * flen)) == NULL)
	{
		printf("no enough memory!\n");
		return -1;
	}
	fread(src, flen, sizeof(char), fp);
	uLongf blen = compressBound(flen);
	if ((buf = (Bytef*)malloc(sizeof(Bytef) * blen)) == NULL)
	{
		printf("no enough memory!\n");
		return -1;
	}
	if (compress(buf, &blen, src, flen) != Z_OK)
	{
		printf("compress failed!\n");
		return -1;
	}
	fclose(fp);
	free(src);
	fp = fopen("badapple.dat", "wb"); //wb 只写打开或新建一个二进制文件;只允许写数据
	fwrite(&flen, 1, sizeof(int), fp);
	fwrite(buf, blen, sizeof(char), fp);
	fclose(fp);
	free(buf);
	return 0;
}

        里面需要注意的是使用了"zlib.h"的库,这个需要另外下载配置,下载链接贴出:http://zlib.net/。库的配置方法与其他无异,可以参考 Visual Studio下C++第三方库的配置方法总结(http://qiusuoge.com/12233.html)尝试几次应该问题不大。还要注意到的是其中几个压缩函数(compress()、compressBound()等) 的参数类型大概由于版本的原因,都做了相应的修改。运行成功后生成"badapple.dat"文件。
        2)文件的解压缩。
        这一步可以作为最后一步exe文件生成的预调试,其本身可不作为整个项目的必备步骤。同样先上代码,内容也比较简短,不做过多注释。
#include "stdafx.h"
#include <cstdio>
#include <iostream>
#include <cstdlib>
#include "zlib.h"
using namespace std;
int main()
{
	FILE* fp = fopen("badapple.dat", "rb");
	uLongf slen;
	fread(&slen, 1, sizeof(int), fp);
	Bytef* buf = NULL;
	Bytef* dst = NULL;
	fseek(fp, 0, SEEK_END);
	uLongf flen = ftell(fp);
	fseek(fp, 4, SEEK_SET);
	flen -= 4;
	uLongf blen = compressBound(slen);
	if ((dst = (Bytef*)malloc(sizeof(Bytef) * slen)) == NULL)
	{
		printf("no enough memory!\n");
		return -1;
	}
	if ((buf = (Bytef*)malloc(sizeof(Bytef) * blen)) == NULL)
	{
		printf("no enough memory!\n");
		return -1;
	}
	fread(buf, flen, sizeof(char), fp);
	if (uncompress(dst, &slen, buf, blen) != Z_OK)
	{
		printf("uncompress failed!\n");
		return -1;
	} 
	free(buf);
	fclose(fp);
	for (int i = 0; i<slen; i++)
		printf("%c", dst[i]);
}
        4、exe文件的生成。
        这一步由于是参考别人的代码进行修改,因此走了一些弯路,这里也记录下吧。
        原文中使用的方法是将数据文件"badapple.dat"与音频文件"BadApple.wav"作为资源文件,制作rc文件并转换为res文件加入到项目中。因此首先要做文件转换,先下载了windres.exe执行程序(http://www.zhaodll.co/W/20130707/235684.html),放入C:\Windows\SysWOW64\C:\Windows\System32文件夹。随后创建source.rc文件,里面加入命令行:
0 TYPEDATA "badapple.dat"
1 TYPEWAV "BadApple.wav"
        然而在项目文件目录下打开命令窗口,执行"windres.exe -i source.rc -o source.res" 随后又提示“ 'gcc'不是内部或外部命令,也不是可运行的程序或批处理文件 ”。网上搜索后是需要安装MinGW,一个将GCC编译器和GNU Binutils移植到Win32平台下的程序,(下载链接https://sourceforge.net/projects/mingw/files/)。这一步完成后重新执行windres命令,依然出现了问题。提示:windres.exe: can't open file `TYPEWAV': No such file or directory。这一关实在是过不去了,尝试网上搜索到的各种方法,都没能解决。同时也尝试重新向VS项目中加入资源的形式加入两个文件,都出现了问题。
        因此我思考能不能跳过这一步直接获取两个文件,毕竟其中的代码也不过是将数据文件的解压缩。因此我参考第3步1)的代码重新做了修改,可以获取"badapple.dat"文件了!当然刷新的帧率还需要做调整,可以参考注释。
        而此时音频文件依然不能获取到,并提示错误“ error LNK2001: 无法解析的外部符号 __imp__PlaySoundW ”,继续网上找解决方案,直到找到这两篇提问中的回答。
            (1) error LNK2019: 无法解析的外部符号 __imp__PlaySoundW@12,该符号在函数 "long __stdcall WndProc(str [问题点数:40分,结帖人QQadw] (http://bbs.csdn.net/topics/370042779) 里面提到:
    使用PlaySound函数时需要在#include<windows.h>后面加上(注意:不能加在前面):
  #include <mmsystem.h>
  #pragma comment(lib, "WINMM.LIB") 

        这样就解决了PlaySound()函数的调用问题。

      (2)在使用PlaySound()播放WAV声音文件没声 [问题点数:100分,结帖人asdjy123](http://bbs.csdn.net/topics/390547623?ticket=ST-104735-4AkGFsn2blcV0xeQHr6Y-passport.csdn.net)里面提到:

        可以使用PlaySound(TEXT("C:\\windows\\Media\\Windows 关机.wav"),NULL,SND_FILENAME | | SND_ASYNC);做测试。

    将其中的文件路径进行替换,再次尝试,终于有声音了。生成exe文件,完成。下面贴上代码:

// badapple_exe.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <cstdio>
#include <windows.h>
#include <mmsystem.h>
#pragma comment(lib, "WINMM.LIB")
#include "zlib.h"
Bytef* dst = NULL;

struct fps_limit {//刷新率设置函数

	int previous_time;
	int tpf_limit;
	int tpf;
	fps_limit(int fps = 60) : previous_time(GetTickCount()), tpf(0) {
		limit_fps(fps);
	}
	void reset() {
		previous_time = GetTickCount(),
			tpf = 0;
		tpf_limit = 60;
	}
	void limit_fps(int fps) {
		tpf_limit = (int)(1000.0f / (float)fps);
	}
	void delay() {
		tpf = GetTickCount() - previous_time;

		if (tpf < tpf_limit)
			Sleep(tpf_limit - tpf - 1);

		previous_time = GetTickCount();
	}
};
void Uncompressdata()//文件解压缩函数
{
	FILE* fp = fopen("badapple.dat", "rb");
	uLongf slen;
	fread(&slen, 1, sizeof(int), fp);
	Bytef* buf = NULL;
	fseek(fp, 0, SEEK_END);
	uLongf flen = ftell(fp);
	fseek(fp, 4, SEEK_SET);
	flen -= 4;
	uLongf blen = compressBound(slen);
	if ((dst = (Bytef*)malloc(sizeof(Bytef) * slen)) == NULL)
	{
		printf("no enough memory!\n");
		exit(1);
	}
	if ((buf = (Bytef*)malloc(sizeof(Bytef) * blen)) == NULL)
	{
		printf("no enough memory!\n");
		exit(1);
	}
	fread(buf, flen, sizeof(char), fp);
	if (uncompress(dst, &slen, buf, blen) != Z_OK)
	{
		printf("uncompress failed!\n");
		exit(1);
	}
	dst[slen] = '\0';
	free(buf);
}
int main()
{
	printf("Loading......");
	Uncompressdata();
	system("PAUSE");//程序暂停并清屏,按任意键继续
	system("CLS");

	PlaySound(TEXT("C:\\opencv\\works\\others\\badapple_exe\\badapple_exe\\BadApple.wav"), NULL, SND_FILENAME || SND_ASYNC);//修改后的调用音频代码,换成自己的音频文件地址
	fps_limit fps(24);//画面刷新帧数
	int i = 0, j = 0;
	char buf[3000];
//	Sleep(1000);//这里可以调整data文件的读取延时时间以配合音频播放
	while (1)
	{
		HANDLE hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
		COORD pos;
		pos.X = 0;
		pos.Y = 0;
		SetConsoleCursorPosition(hConsoleOutput, pos);
		while (j<22 * 50) //确定每页刷新出来的字符总数,这里取1100时画面稳定
		{
			if (dst[i] == '\0')//检测到空字符时停止跳出循环
				return 0;
			buf[j++] = dst[i++];
		}
		j = 0;
		printf("%s", buf);
		fps.delay();
	}
}

    贴出效果图:


     以上就是整个程序的修改过程,虽然有现成的代码可以参考,依然在调试过程中出现了许多问题要自己查证解决,算是对自己的一次锻炼吧。 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值