本节介绍FFmpeg对JPEG图像的处理办法,首先描述为什么引入JPEG格式,以及JPEG格式有哪些种类;然后叙述如何把视频文件中的某帧画面保存为JPEG图像文件;最后阐述色彩空间YUV与YUVJ之间的区别,以及如何通过图像转换器把YUV格式转换为YUVJ格式,从而把视频帧保存为真实色彩的JPEG图像文件。
JPEG是一种图像格式编码标准,也是一种图像数据压缩标准,它的全称为Joint Photographic Experts Group,意思是联合图像专家组。JPEG标准于1992年由ISO(International Organization for Standardization,国际标准化组织)首次发布,它是长期以来广泛使用的计算机图像格式。采用JPEG格式的图像文件,其扩展名通常为.jpg或者.jpeg。
前面的4.1.1节提到,BT601、BT709、BT2020三个色度标准都包含数字电视版和全范围版两个版本,其中数字电视版的色度范围是[16,235],全范围版的色度范围是[0,255],全范围版属于JPEG格式的色彩空间,即YUV格式的JPEG变种,简称YUVJ,末尾的J代表JPEG。在FFmpeg框架中,视频帧的YUV图像默认采用数字电视版的色度标准,其像素格式为AV_PIX_FMT_YUV420P;而JPEG图像对应全范围版的色度标准,其像素格式为AV_PIX_FMT_YUVJ420P,也就是在YUV与420P之间多了个J。
JPEG在压缩图像时采用了差分预测编码调制(Differential Predictive Coding Modulation,DPCM)、离散余弦变换(Discrete Cosine Transform,DCT)以及熵编码(Entropy Coding)的联合编码算法,以去除冗余的图像数据。JPEG属于有损压缩格式,它的压缩比率会影响图像的保真程度,如果采用过高的压缩比率,JPEG图片经过解压后的图像质量将明显降低。不过由于JPEG标准发布较早,且压缩效率尚可,因此几乎所有的设备都支持JPEG图片,使得JPEG格式在互联网上大行其道。
根据图像的展示过程,可将JPEG格式分为标准JPEG、渐进JPEG、JPEG2000三种类型,分别说明如下。
(1)标准JPEG:该类型的JPEG图片根据加载进度从上到下、从左往右依序显示图像,直到所有图像数据加载完毕,才能看到整幅图片的全貌。标准JPEG的显示过程如图4-18和图4-19所示。
图4-18 标准JPEG正在加载
图4-19 标准JPEG即将加载完毕
(2)渐进JPEG:该类型的JPEG图片根据加载进度从模糊到清晰逐步显示图像,也就是先呈现出图像的粗略外观,再逐渐呈现出越来越清晰的图像。渐进JPEG的呈现过程如图4-20和图4-21所示。
图4-20 渐进JPEG正在加载
图4-21 渐进JPEG加载完毕
(3)JPEG2000:这是新一代的影像压缩标准,压缩品质更高,压缩性能比标准JPEG提高20%。JPEG2000图片的扩展名为.jp2,对应的MIME类型是image/jp2(传统JPEG的MIME类型是image/jpeg)。虽然JPEG2000的压缩效率得到提高,但是该标准的编解码算法被大量注册专利,因此极大地限制了它的应用场景。
因为FFmpeg自带对JPEG图片编解码的mjpeg库,所以通过MJPEG编码器即可将数据帧压缩为JPEG图片。把视频画面转存为JPEG图片的过程主要有两个步骤:获取并打开MJPEG编码器实例,以及把数据帧重新编码并写入JPEG文件,分别说明如下。
与视频的编码器实例类似,MJPEG编码器实例的打开步骤也分为以下4步。
(1)调用avcodec_find_encoder函数获取代号为AV_CODEC_ID_MJPEG的MJPEG编码器。
(2)调用avcodec_alloc_context3函数分配MJPEG编码器对应的编码器实例。
(3)给MJPEG编码器实例的各字段赋值,包括pix_fmt(像素格式)、width(视频宽度)、height(视频高度)、time_base(时间基)等字段。由于JPEG的颜色空间为YUVJ,因此pix_fmt字段要填AV_PIX_FMT_YUVJ420P。
(4)调用avcodec_open2函数打开MJPEG编码器的实例。
根据以上步骤描述的MJPEG编码器打开过程,编写获取并打开MJPEG编码器实例的FFmpeg代码如下(完整代码见chapter04/savejpg.c):
// 查找MJPEG编码器 AVCodec *jpg_codec = (AVCodec*) avcodec_find_encoder(AV_CODEC_ID_MJPEG); if (!jpg_codec) { av_log(NULL, AV_LOG_ERROR, "jpg_codec not found\n"); return -1; } // 获取编解码器上下文信息 AVCodecContext *jpg_encode_ctx = avcodec_alloc_context3(jpg_codec); if (!jpg_encode_ctx) { av_log(NULL, AV_LOG_ERROR, "jpg_encode_ctx is null\n"); return -1; } // JPG的像素格式是YUVJ。MJPEG编码器支持YUVJ420P/YUVJ422P/YUVJ444P等格式 jpg_encode_ctx->pix_fmt = AV_PIX_FMT_YUVJ420P; // 像素格式 jpg_encode_ctx->width = frame->width; // 视频宽度 jpg_encode_ctx->height = frame->height; // 视频高度 jpg_encode_ctx->time_base = (AVRational){1, 25}; // 时间基 ret = avcodec_open2(jpg_encode_ctx, jpg_codec, NULL); // 打开编码器的实例 if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Can't open jpg_encode_ctx.\n"); return -1; }
对数据帧重新编码的过程主要分为以下三步。
(1)调用avcodec_send_frame函数把原始的数据帧发给MJPEG编码器实例。
(2)调用avcodec_receive_packet函数从MJPEG编码器实例获取压缩后的数据包。
(3)调用av_write_frame函数往JPG文件写入压缩后的数据包。
注意上述的重新编码过程无须重复调用,只要执行一遍即可,因为JPEG格式属于静止的图像,仅包含一幅图片。下面是把视频帧保存为JPEG图片的FFmpeg代码片段。
int packet_index = -1; // 数据包的索引序号 // 对视频帧解码。save_index表示要把第几个视频帧保存为图片 int decode_video(AVPacket *packet, AVFrame *frame, int save_index) { // 把未解压的数据包发给解码器实例 int ret = avcodec_send_packet(video_decode_ctx, packet); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "send packet occur error %d.\n", ret); return ret; } while (1) { // 从解码器实例获取还原后的数据帧 ret = avcodec_receive_frame(video_decode_ctx, frame); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { break; } else if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "decode frame occur error %d.\n", ret); break; } packet_index++; if (packet_index < save_index) { // 还没找到对应序号的帧 return AVERROR(EAGAIN); } save_jpg_file(frame, save_index); // 把视频帧保存为JPEG图片 break; } return ret; }
接着执行下面的编译命令:
gcc savejpg.c -o savejpg -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib -lavformat -lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm
编译完成后,执行以下命令启动测试程序(默认保存首帧视频画面,即save_index为0)。
./savejpg ../fuzhous.mp4
程序运行完毕,发现控制台输出以下日志信息,说明完成了从视频帧到JPEG图片的转换操作。
Success open input_file ../fuzhous.mp4. format = 0, width = 480, height = 270 target image file is output_000.jpg Success save 0_index frame as jpg file.
最后打开看图软件可以正常浏览output_000.jpg,如图4-22所示,表示上述代码初步实现了把视频画面转存为JPEG图片的功能。
图4-22 把视频画面转存为JPEG图片
运行下面的ffmpeg命令也可以把指定的视频帧保存为JPG图片。命令中的-vframes 1表示输出一帧。
ffmpeg -i ../fuzhous.mp4 -ss 00:00:10 -vframes 1 ff_capture.jpg
4.2.2节虽然把视频画面转存为JPEG图片,使用看图软件也能浏览该图片,但是好像图片的色彩比原视频淡了一些,而且不止首帧视频画面,其他视频帧转存为JPEG图片也都变淡了。这是什么缘故呢?原来JPEG采用的是YUVJ颜色空间,与视频采用的YUV颜色空间略有不同。YUVJ的色彩范围为0~255,其中0表示黑色,255表示白色;而YUV的色彩范围为16~235,其中16表示黑色,235表示白色。可见YUVJ的色彩范围大于YUV的色彩范围,且YUV的色彩范围仅是YUVJ色彩范围的子集。这意味着YUV的色彩能够放入YUVJ空间,因为[16,235]可被[0,255]所容纳;然而YUVJ色彩不能放入YUV空间,因为[0,255]超出了[16,235]的表达范围,像[0,15]和[236,255]这两个区间均位于[16,235]之外。
不过即使YUV的色彩能够放入YUVJ空间,并不意味着YUV的色彩精度没有损失,实际上由于YUV的色彩空间缺少[0,15]和[236,255]这两个区间,导致YUV色彩在YUVJ看起来不够浓。也就是说,白的不够白,黑的也不够黑,整幅图像的色调就变淡了,没有原来的浓度高了。这便是4.2.2节转存后的JPEG图片颜色变淡的原因。
要想解决JPEG转存后颜色变淡的问题,就得引入FFmpeg提供的图像转换器SwsContext,由图像转换器完成色彩空间的转换操作。SwsContext位于swscale库,它主要用来转换图像数据的像素格式、画面宽度和画面高度等,与之相关的函数说明如下:
· sws_getContext:分配图像转换器的实例。还需要在输入参数中分别指定来源和目标的画面宽度、画面高度、像素格式。
· sws_scale:图像转换器开始处理图像数据。该函数同时实现以下三项功能:图像色彩空间转换、画面分辨率缩放、前后图像滤波处理。
· sws_freeContext:释放图像转换器的实例。
引入图像转换器之后,把视频帧转存为JPEG图片的过程需要注意以下两点:通过图像转换器把数据帧转换为YUVJ420P格式,以及把第一步转换后的数据帧重新编码并写入JPEG文件,分别说明如下。
图像转换器处理数据帧向YUVJ颜色空间的转换操作主要包含以下5个步骤:
01 调用sws_getContext函数分配图像转换器的实例,并分别指定来源和目标的宽度、高度、像素格式,这里的像素格式采用AV_PIX_FMT_YUVJ420P。
02 调用av_frame_alloc函数分配一个YUVJ数据帧,并设置YUVJ帧的像素格式、视频宽度和视频高度。
03 调用av_image_alloc函数分配缓冲区空间,用于存放转换后的图像数据。
04 调用sws_scale函数,命令转换器开始处理图像数据,把YUV图像转为YUVJ图像。
05 转换结束,调用sws_freeContext函数释放图像转换器的实例。
根据以上步骤描述的YUV空间到YUVJ空间的转换过程,编写FFmpeg的颜色空间转换代码(完整代码见chapter04/savejpg_sws.c)。
enum AVPixelFormat target_format = AV_PIX_FMT_YUVJ420P; // JPG的像素格式是YUVJ420P // 分配图像转换器的实例,并分别指定来源和目标的宽度、高度、像素格式 struct SwsContext *swsContext = sws_getContext( frame->width, frame->height, AV_PIX_FMT_YUV420P, frame->width, frame->height, target_format, SWS_FAST_BILINEAR, NULL, NULL, NULL); if (swsContext == NULL) { av_log(NULL, AV_LOG_ERROR, "swsContext is null\n"); return -1; } AVFrame *yuvj_frame = av_frame_alloc(); // 分配一个YUVJ数据帧 yuvj_frame->format = target_format; // 像素格式 yuvj_frame->width = frame->width; // 视频宽度 yuvj_frame->height = frame->height; // 视频高度 // 分配缓冲区空间,用于存放转换后的图像数据 av_image_alloc(yuvj_frame->data, yuvj_frame->linesize, frame->width, frame->height, target_format, 1); // 转换器开始处理图像数据,把YUV图像转为YUVJ图像 sws_scale(swsContext, (const uint8_t* const*) frame->data, frame->linesize, 0, frame->height, yuvj_frame->data, yuvj_frame->linesize); sws_freeContext(swsContext); // 释放图像转换器的实例
对数据帧重新编码的过程主要分为以下列3步。
(1)调用avcodec_send_frame函数把前一步转换后的YUVJ数据帧发给MJPEG编码器实例。
(2)调用avcodec_receive_packet函数从MJPEG编码器实例获取压缩后的数据包。
(3)调用av_write_frame函数往JPG文件写入压缩后的数据包。
根据以上步骤描述的数据帧重新编码过程,编写FFmpeg的JPEG图片转存代码。
int packet_index = -1; // 数据包的索引序号 // 对视频帧解码。save_index表示要把第几个视频帧保存为图片 int decode_video(AVPacket *packet, AVFrame *frame, int save_index) { // 把未解压的数据包发给解码器实例 int ret = avcodec_send_packet(video_decode_ctx, packet); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "send packet occur error %d.\n", ret); return ret; } while (1) { // 从解码器实例获取还原后的数据帧 ret = avcodec_receive_frame(video_decode_ctx, frame); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { break; } else if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "decode frame occur error %d.\n", ret); break; } packet_index++; if (packet_index < save_index) { // 还没找到对应序号的帧 return AVERROR(EAGAIN); } save_jpg_file(frame, save_index); // 把视频帧保存为JPEG图片 break; } return ret; }
接着执行下面的编译命令:
gcc savejpg_sws.c -o savejpg_sws -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib -lavformat -lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm
编译完成后,执行以下命令启动测试程序(默认保存首帧视频画面,即save_index为0)。
./savejpg_sws ../fuzhous.mp4
程序运行完毕,发现控制台输出以下日志信息,说明完成了从视频帧到JPEG图片的转换操作。
Success open input_file ../fuzhous.mp4. format = 0, width = 480, height = 270 target image file is output_000.jpg Success save 0_index frame as jpg file.
最后打开看图软件可以正常浏览output_000.jpg,并且该图片与视频的首帧画面拥有相同的色彩,如图4-23所示,表明上述代码正确实现了把视频画面转存为JPEG图片的功能。
图4-23 像素格式转换后的JPEG图片