用Linux终端播放Bad Apple
2024-04-22 23:00:44

引子

Bad Apple常常指一部影绘PV:【東方】Bad Apple!! PV【影絵】。在各大网站,您可以看到很多在不同设备上播放这个PV的视频,包括但不限于单片机液晶屏、国际象棋棋盘等。

本文主要讲述如何借助libpngncursesffmpeg,编写C语言程序,在uxterm上播放该视频。

预处理

基本思想是:我们需要将视频流转换为一张张图片,然后通过计算图片每个像素的灰度,输出白色/黑色的色块,达到播放的效果。

首先,视频流转换为图片,可以使用ffmpeg工具,只需要在命令行执行:

1
ffmpeg -i VIDEO_NAME %04d.png

这个意思是,将VIDEO_NAME这个视频流转化为一张张图片,图片是png格式,名称是一个补零四位数整数,比如0001.png0887.png1145.png

为了查看图片的颜色属性,比如RGB、RGBA,可以使用file命令:

1
file 0001.png

我这里输出的是RGB。

图片的加载

单个图片的加载

png图片的加载需要用到libpng这个库,读取的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
png_bytepp ReadPNG(char* file_name, int* height, int* width) {
// Read file
FILE* fp = fopen(file_name, "rb");
if(fp == NULL) {
printf("Unable to read!\n");
exit(-1);
}
// Initialize
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
png_infop info_ptr = png_create_info_struct(png_ptr);
png_init_io(png_ptr, fp);
// Read Info of a picture
png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL);
png_bytepp row_pointers = png_get_rows(png_ptr, info_ptr);
// Get height and width of a png picture
*height = png_get_image_height(png_ptr, info_ptr);
*width = png_get_image_width(png_ptr, info_ptr);
png_destroy_read_struct(&png_ptr, NULL, NULL);
// Deinitalize
fclose(fp);
return row_pointers;
}

基本的思路是这样的,首先读取png文件,接着创建读取png的相关结构体,接着读取图片,将像素输入到一个指定结构体里,获取图片的长和宽。

多个图片的加载

首先,我们要获取图片的总数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int GetPNGNums(const char* path) {
DIR* directory = NULL;
int total_num = 0;
if ((directory = opendir(path)) == NULL) {
fprintf(stderr, "Can't open %s\n", path);
return EXIT_FAILURE;
}

struct dirent* entry = NULL;
while ((entry = readdir(directory)) != NULL) {
if (entry->d_type != DT_DIR) {
++total_num;
}
}
closedir(directory);
return total_num;
}

这里用到了dirent库,遍历文件夹,获取所有非文件夹的文件总数即可。

输出

首先,要初始化窗口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void InitWindow() {
window = initscr();
refresh();
getmaxyx(window, *&window_height, *&window_width);
start_color();
// Hide cursor
curs_set(0);
refresh();
init_pair(1, COLOR_WHITE, COLOR_BLACK);
init_pair(2, COLOR_WHITE, COLOR_WHITE);
init_pair(3, COLOR_BLACK, COLOR_BLACK);
wbkgd(window, COLOR_PAIR(1));
attron(A_BOLD);
}

首先初始化窗口,然后获取窗口的长和宽,这一步的目的是便于之后的图片缩放,接着初始化颜色色对,按照编号,前景色,背景色的顺序初始化即可,接着使用wbkgd设置窗口背景颜色。

接着,将每张图片的像素输出到窗口上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
   for(int h = 0; h < height; h+=HEIGHT_RATIO) {
curr_col = 0;
for(int w = 0; w < width; w+=WIDTH_RATIO) {
unsigned int r = row_pointers[h][w*3+0],
g = row_pointers[h][w*3+1],
b = row_pointers[h][w*3+2];
unsigned int gray = RGB2Gray(r, g, b);
if(gray >= THRESHOLD) {
attron(COLOR_PAIR(2));
mvaddch(curr_row, curr_col, ' ');
attroff(COLOR_PAIR(2));
} else {
attron(COLOR_PAIR(3));
mvaddch(curr_row, curr_col, ' ');
attroff(COLOR_PAIR(3));
}
curr_col++;
}
curr_row++;
}

这里,HEIGHT_RATIOWIDTH_RATIO分别代表图片长宽的缩放比例,比如HEIGHT_RATIO为2,就代表在图片长的遍历上,要跳过一个像素,这样图片的长就变为了原来的1/2。

对于灰度的计算,我采用了一个近似的方法:

1
2
3
unsigned int RGB2Gray(unsigned int R, unsigned int G, unsigned int B) {
return (R+G+B)/3;
}

接着设置一个灰度阈值THRESHOLD,根据灰度与阈值的关系决定输出黑块/白块即可。

在每次输出之后,需要应用usleep()睡眠一段时间,这个时间可以根据采样率等计算。

Prev
2024-04-22 23:00:44
Next