引子
Bad Apple常常指一部影绘PV:【東方】Bad Apple!! PV【影絵】。在各大网站,您可以看到很多在不同设备上播放这个PV的视频,包括但不限于单片机液晶屏、国际象棋棋盘等。
本文主要讲述如何借助libpng
、ncurses
和ffmpeg
,编写C语言程序,在uxterm
上播放该视频。
预处理
基本思想是:我们需要将视频流转换为一张张图片,然后通过计算图片每个像素的灰度,输出白色/黑色的色块,达到播放的效果。
首先,视频流转换为图片,可以使用ffmpeg
工具,只需要在命令行执行:
1
| ffmpeg -i VIDEO_NAME %04d.png
|
这个意思是,将VIDEO_NAME这个视频流转化为一张张图片,图片是png格式,名称是一个补零四位数整数,比如0001.png
、0887.png
或1145.png
。
为了查看图片的颜色属性,比如RGB、RGBA,可以使用file
命令:
我这里输出的是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) { FILE* fp = fopen(file_name, "rb"); if(fp == NULL) { printf("Unable to read!\n"); exit(-1); } 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); png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL); png_bytepp row_pointers = png_get_rows(png_ptr, info_ptr); *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); 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(); 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_RATIO
和WIDTH_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()
睡眠一段时间,这个时间可以根据采样率等计算。