Windows下控制台播放Badapple,opencv的使用,以及代码优化方案

Windows下控制台播放Badapple

环境准备

  • VS2022编译环境
  • Opencv(对图像进行灰度处理)的配置 可以看我写的这篇文章vs下opencv的配置
  • 素材(Badpple的视频文件) 可以私信我
  • FFmpeg(对视频文件进行处理) 让视频文件的声音分离出来生成mp3文件

在这里插入图片描述

ffmpeg -i test.mp4 -map 0:v:0 -c copy test_video.mp4 -map 0:a:0 -c copy test_audio.mp3

关于ffmpeg的音视频分离就到这里,不做赘述。

编写流程

该程序首先使用VideoCapture打开一个视频文件,并获取视频的特征信息,如帧数、帧率和帧大小。然后,它根据设定的抽样大小将每一帧转换为字符画,并保存到内存中。

接下来,程序使用Windows API设置控制台窗口的大小和光标位置,并隐藏光标显示。然后,它从内存中读取字符画,并在控制台中连续输出,实现视频的字符化播放效果。

最后,该程序使用mciSendString函数播放背景音乐,并通过无限循环不断播放字符化视频,直到用户手动停止程序

main()

int main(void)
{  
	Init();
	readData();
	init2();
	play();
	return 0;
}

Init()

Init() 函数主要用于设置控制台窗口的大小、隐藏光标,并调整控制台窗口的缓冲区,为后续字符化视频的播放做准备。

int Init()
{

	SetConsoleTitle(L"坏苹果-M");//设置控制台窗口的标题为 "坏苹果-M"。
	sprintf_s(cmd, sizeof(cmd), "mode con cols=%d lines=%d", width, height);//控制台窗口的大小设置为 width 列和 height 行,并将命令保存在 cmd 字符数组中
	system(cmd);//行上一步生成的命令,从而改变控制台窗口的大小
	//隐藏光标
	HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);//使用Windows API函数 GetStdHandle() 获取标准输出流的句柄,并将其保存在变量 hOut 中
	CONSOLE_CURSOR_INFO info{ 1,0 };//将其成员 dwSize 设置为 1,使光标变得不可见。
	SetConsoleCursorInfo(hOut, &info);//将 hOut 和 info 作为参数,从而隐藏控制台窗口中的光标。

	COORD co = { width,height};//将其成员 X 设置为 width,将其成员 Y 设置为 height。
	SetConsoleCursorInfo(hOut, &info);
	SetConsoleScreenBufferSize(hOut,co);		//置控制台屏幕缓冲区的大小为 co
	SMALL_RECT rc = { 0, 0, width - 1, height - 1 };	//重要点:设置缓冲,消除闪烁
	SetConsoleWindowInfo(hOut, TRUE, &rc);							//将 hOut、TRUE 和 rc 作为参数,以设置控制台窗口的大小和位置。

	if (ret == false)
	{
		printf("视频文件打开失败!\n");
		exit(-1);
	}

	if (data == NULL)
	{
		printf("内存不足!\n");
		return 1;
	}

}

这里是对代码中的 Init() 函数进行分析:

  1. 首先,该函数使用 SetConsoleTitle() 设置控制台窗口的标题为 “坏苹果-M”。
  2. 然后,通过 sprintf_s() 函数将控制台窗口的大小设置为 width 列和 height 行,并将命令保存在 cmd 字符数组中。
  3. 使用 system() 函数执行上一步生成的命令,从而改变控制台窗口的大小。
  4. 接下来,使用Windows API函数 GetStdHandle() 获取标准输出流的句柄,并将其保存在变量 hOut 中。
  5. 创建一个 CONSOLE_CURSOR_INFO 结构体变量 info,并将其成员 dwSize 设置为 1,使光标变得不可见。
  6. 调用 SetConsoleCursorInfo() 函数,将 hOutinfo 作为参数,从而隐藏控制台窗口中的光标。
  7. 创建一个 COORD 结构体变量 co,将其成员 X 设置为 width,将其成员 Y 设置为 height
  8. 再次调用 SetConsoleCursorInfo() 函数,将 hOutinfo 作为参数,从而设置控制台屏幕缓冲区的大小为 co
  9. 创建一个 SMALL_RECT 结构体变量 rc,将其成员 Left 设置为 0,将其成员 Top 设置为 0,将其成员 Right 设置为 width - 1,将其成员 Bottom 设置为 height - 1
  10. 最后,调用 SetConsoleWindowInfo() 函数,将 hOutTRUErc 作为参数,以设置控制台窗口的大小和位置。

总结起来,Init() 函数主要用于设置控制台窗口的大小、隐藏光标,并调整控制台窗口的缓冲区,为后续字符化视频的播放做准备

代码优化

int Init()
{
    SetConsoleTitle(L"坏苹果-M");
    sprintf_s(cmd, sizeof(cmd), "mode con cols=%d lines=%d", width, height);
    system(cmd);

    HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
    CONSOLE_CURSOR_INFO info{ 1,0 };
    SetConsoleCursorInfo(hOut, &info);

    COORD co = { width,height };
    SetConsoleScreenBufferSize(hOut, co);
    SMALL_RECT rc = { 0, 0, width - 1, height - 1 };
    SetConsoleWindowInfo(hOut, TRUE, &rc);

    // 错误处理改为异常抛出
    if (ret == false)
    {
        throw std::runtime_error("视频文件打开失败!");
    }

    if (data == nullptr)
    {
        throw std::bad_alloc();
    }

    

   
}

视频信息的处理

VideoCapture video;	
Mat frameImg, grayImg;
bool ret = video.open("./apple.mp4");

// 读取视频特征信息
int framecount = video.get(CV_CAP_PROP_FRAME_COUNT); //视频文件的总帧数
int fps = video.get(CV_CAP_PROP_FPS); //帧率
int cols = video.get(CV_CAP_PROP_FRAME_WIDTH); //视频流中的帧宽
int rows = video.get(CV_CAP_PROP_FRAME_HEIGHT);//视频流中的帧高

// 定义抽样大小
int hSize = 10; // 每 10 行,每 5 列,转换为 1 个字符
int wSize = 5;

// 定义字符集合 
char charImgs[] = " .,-'`:!1&@#$";

int height = rows / hSize;
int width = cols / wSize;
char cmd[128];

int countpersent = 1;

// 待优化为指针数组 
int frameSize = height * (width + 1) + 1; //每行的末尾有一个回车符
char* data = (char*)malloc(sizeof(char) * framecount * frameSize);

readdata()

void readData()
{
	sprintf_s(cmd, sizeof(cmd), "mode con cols=%d lines=%d", 22, 4);//
	system(cmd);//设置控制台大小
	 
		for (int n = 0; n < framecount; n++)
		{
				char* p = data + n * frameSize; video.read(frameImg); // 转换图片的色彩,这里转换为灰度效果
				cvtColor(frameImg, grayImg, COLOR_BGR2GRAY);//opencv 函数灰度图片
				string s = "";
				int k = 0;
				for (int row = 0; row < rows - hSize; row = row + hSize)
				{
						for (int col = 0; col < cols - wSize; col = col + wSize)
							 
						{
							int value = grayImg.at<uchar>(row, col);
							p[k++] = charImgs[int(value / 20)];	
						}
							p[k++] = '\n';
						
				}
							p[k++] = 0;
							 system("cls");
            printf("正在读取:%d / %d", n + 1, framecount);  					 						 						 													 				 						
		}	 	 
	mciSendString(L"play apple.mp3 repeat", 0, 0, 0);
			 
}

  1. 使用 sprintf_s() 函数将命令字符串 cmd 格式化为设置控制台窗口大小和行数的命令。
  2. 使用 system() 函数执行上述生成的命令,从而改变控制台窗口的大小和行数。
  3. 在每一帧循环中,首先声明一个指针 p 并将其初始化为指向当前帧的字符画数据的位置。
  4. 使用 video.read(frameImg) 从视频读取一帧图像,并将其转换为灰度图像。
  5. 在双重循环中,将灰度图像中的像素值转换为字符,并将字符保存到指针 p 指向的位置。每个字符的值通过除以20来映射到 charImgs 数组中。
  6. 在内层循环结束后,添加换行符 \n 到字符画中,以分隔每一行的字符。
  7. 在外层循环结束后,添加空字符 \0 到字符画末尾,以标识字符画的结尾。
  8. 使用 system("cls") 清空控制台屏幕。
  9. 使用 printf() 打印正在读取的帧数和总帧数。
  10. 最后,使用 mciSendString() 函数播放背景音乐 “apple.mp3” 并设置循环播放。

代码优化

void readData()
{
	sprintf_s(cmd, sizeof(cmd), "mode con cols=%d lines=%d", 22, 4);
	system(cmd);

	// 预分配内存
	char** data = new char*[framecount];
	for (int n = 0; n < framecount; n++)
	{
		data[n] = new char[frameSize];
	}

#pragma omp parallel for
	for (int n = 0; n < framecount; n++)
	{
		char* p = data[n];

		Mat frameImg;
		video.read(frameImg);
		cvtColor(frameImg, grayImg, COLOR_BGR2GRAY);

		string s = "";
		int k = 0;
		for (int row = 0; row < rows - hSize; row = row + hSize)
		{
			for (int col = 0; col < cols - wSize; col = col + wSize)
			{
				int value = grayImg.at<uchar>(row, col);
				p[k++] = charImgs[int(value / 20)];
			}
			p[k++] = '\n';
		}
		p[k++] = 0;

#pragma omp critical
	{
		system("cls");
		printf("正在读取:%d / %d", n + 1, framecount);
	}

	}

	mciSendString(L"play apple.mp3 repeat", 0, 0, 0);

	// 释放内存
	for (int n = 0; n < framecount; n++) {
		delete[] data[n];
	}
	delete[] data;
}

这里使用了OpenMP来并行处理每一帧的转换过程,以提高性能。使用#pragma omp parallel for将循环并行化,从而使每个线程独立处理一帧的转换操作。

另外,内存分配和释放的操作被提到循环外部,通过预分配内存避免了在每一帧循环中的重复动态内存分配和释放操作。最后,在关键的控制台输出部分使用了临界区(#pragma omp critical)来保证多个线程之间的输出不会互相干扰。

这些优化措施可以显著提高程序的运行速度。然而,请注意在实际应用中进行测试和评估,以确保其在特定环境下的有效性。

Init2()

void init2()
{
	sprintf_s(cmd, sizeof(cmd), "mode con cols=%d lines=%d", width, height);
	system(cmd);
}

paly()

函数优化

void play()
{
    for (int i = 0; i < framecount; i++)
    {
        char* p = data + i * frameSize;
        SetConsoleCursorPosition(h, pos); // 跳转控制台光标位置
        
        // 使用 WriteConsoleOutputCharacterA() 直接写入控制台屏幕缓冲区
        DWORD written;
        WriteConsoleOutputCharacterA(h, p, frameSize , pos, &written);
        
        Sleep(1000 / fps);
    }
}

在优化后的代码中:

使用 WriteConsoleOutputCharacterA() 函数直接将帧数据 p 写入控制台屏幕缓冲区,而不是使用 printf() 函数。这样可以提高输出的效率。

删除了外部的 while 循环,因为循环内的逻辑已经包含了需要重复播放的功能。每次迭代都会播放一帧,并通过 Sleep() 函数等待 1000 / fps 毫秒,以模拟视频的帧率。

请注意,在使用该函数之前,请确保已经定义和初始化了相关变量(如 framecountdataframeSizehpos),并且已经设置了合适的 fps 值。

指针数组优化

	// 待优化为指针数组 
	int frameSize = height * (width + 1) + 1; //每行的末尾有一个回车符
	char* data = (char*)malloc(sizeof(char) * framecount * frameSize);

要将上述代码优化为使用指针数组,您需要进行以下修改:

  1. 修改 data 的声明:将 char* data 改为 char** data,用于存储指向字符画的指针。
  2. 动态分配内存给 data:替换 char* data = (char*)malloc(sizeof(char) * framecount * frameSize);char** data = new char*[framecount],这将创建一个指针数组,每个指针指向一帧的字符画。
  3. 在循环中为每帧分配内存并保存字符画:在 for (int n = 0; n < framecount; n++) 循环开始之前添加 data[n] = new char[frameSize]; 来为每一帧分配内存。
  4. 将字符画保存到对应的指针位置:在 char* p = data + n * frameSize; 处将 p 更改为 char* p = data[n];
  5. 在程序结束前释放内存:在 main() 函数的最后使用循环来释放为每帧字符画分配的内存,例如:
for (int n = 0; n < framecount; n++) {
    delete[] data[n];
}
delete[] data;

通过上述修改,您将能够将字符画以指针数组的形式保存,并正确释放内存。请记得适时进行错误处理和异常处理。

需要代码以及素材或者我的opencv可以私信我

猜你喜欢

转载自blog.csdn.net/m0_72703340/article/details/131653899