
通过 ffmpeg 对pal8格式的 png 图片进行缩放时,原始图像的透明通道丢失了,被替换为黑色底。
原始图像:

ffmpeg 命令:
ffmpeg -i 0_1.png -filter_complex "scale=w=100:h=100" -y o.png结果:

首选看下 ffmpeg 对 AVPixelFormat 说明,从注释部分清楚可以看到,AV_PIX_FMT_PAL8 格式比较特殊,在 ffmpeg 的 frame 中的存储和其他格式不一样。AV_PIX_FMT_PAL8 时,它的透明通道是存放在AVFrame.data[1]中的。
include/libavutil/pixfmt.h
* @par
* When the pixel format is palettized RGB32 (AV_PIX_FMT_PAL8), the palettized
* image data is stored in AVFrame.data[0]. The palette is transported in
* AVFrame.data[1], is 1024 bytes long (256 4-byte entries) and is
* formatted the same as in AV_PIX_FMT_RGB32 described above (i.e., it is
* also endian-specific). Note also that the individual RGB32 palette
* components stored in AVFrame.data[1] should be in the range 0..255.
* This is important as many custom PAL8 video codecs that were designed
* to run on the IBM VGA graphics adapter use 6-bit palette components.继续跟进到 scale 滤镜,通过源文件发现,scale 滤镜在实现时,对 AV_PIX_FMT_PAL8 做了很多特殊逻辑。
libavfilter/vf_scale.c
A、AVPixelFormat 协商时,输出的 AVPixelFormat 对 AV_PIX_FMT_PAL8 做了特殊判断,取了 sws_isSupportedOutput 和 AV_PIX_FMT_PAL8 的并集,也就是强制指定支持了 AV_PIX_FMT_PAL8。
这里补充下,因为 scale 滤镜,底层是使用 ffmpeg 的 swscale 来实现的,所以 scale 在协商时需要通过 sws_isSupportedInput、sws_isSupportedOutput 分别来判断输入输出的 AVPixelFormat 支持情况。

B、帧处理时,遇到输出的 AVPixelFormat 为 AV_PIX_FMT_PAL8 时,也会做一些特殊处理。
一方面在送入 swscale 处理前,会强行指定 swscale 的输出 AVPixelFormat 为 AV_PIX_FMT_BGR8。

另一方面前面也提到,AV_PIX_FMT_PAL8 时 AVFrame.data[1]存放的是透明通道。通过代码发现,scale 滤镜直接将对AVFrame.data[1]进行指定,这源于 AV_PIX_FMT_PAL8 的特殊存储结构。

通过对 scale 滤镜分析,发现虽然其底层调用的是 swscale,为什么又要对 AV_PIX_FMT_PAL8 做这么多特殊逻辑呢?是不是 swscale 对 PAL8 有什么特殊限制呢?我们继续深入到 swscale 内部进行分析。我们先看下 sws_isSupportedInput、sws_isSupportedOutput 的实现。通过代码发现,这两个函数其实就是两个查表函数,依赖于一个 FormatEntry 表 format_entries。
libswscale/utils.c
typedef struct FormatEntry {
uint8_t is_supported_in :1;
uint8_t is_supported_out :1;
uint8_t is_supported_endianness :1;
} FormatEntry;
static const FormatEntry format_entries[AV_PIX_FMT_NB] = {
[AV_PIX_FMT_YUV420P] = { 1, 1 },
[AV_PIX_FMT_YUYV422] = { 1, 1 },
// ...
[AV_PIX_FMT_PAL8] = { 1, 0 },
// ...
};可以清晰看到,swscale 只支持 AV_PIX_FMT_PAL8 的输入,不支持 AV_PIX_FMT_PAL8 的输出。可以简单的类比为 swscale 支持 AV_PIX_FMT_PAL8 的解码,但是不支持 AV_PIX_FMT_PAL8 的编码。

现在整个问题的原因基本就清晰了。因为 scale 滤镜到 swscale 滤镜中间做了一次 AV_PIX_FMT_BGR8 转换,所以透明通道丢失了。
可能有疑问,上面说 scale 滤镜会直接指定 AVFrame.data[1](也就是透明通道),那岂不是透明通道还存在?答案是否定的,我们看代码发现,这个指定位于 sws_scale 调用前,而此时并没有透明通道信息,而且 swscale 的输出被强制指定为 AV_PIX_FMT_BGR8 ,而 AV_PIX_FMT_BGR8 没有数据在 AVFrame.data[1],所以 swscale 并没有处理AVFrame.data[1],最终输出的帧里面也不会有透明通道。
/*
+--------------+ +---------+
---------PAL8------> in | ---------PAL8---------> in |
| scale filter | | swscale |
<--PAL8 loss alpha-- out | <--BGR8 without alpha-- out |
+--------------+ +---------+
*/既然根本原因是 ffmpeg swscale 不支持 AV_PIX_FMT_PAL8 的输出,那我们支持上不就可以了? 这里并不简单,我们要支持的话,需要去修改 ffmepg 代码,实现 swscale 内部对 AV_PIX_FMT_PAL8 的处理,也需要调整链路上的 scale 滤镜实现。加上提交到社区合并,整个流程耗时会比较长。
这里我们采取了另一种牺牲效率的方式,即在帧送入 scale 滤镜前,将其 AVPixelFormat 转换为 AV_PIX_FMT_RGBA,swscale 完全支持 AV_PIX_FMT_RGBA。这样就解决了透明通道丢失的问题。
ffmpeg -i 0_1.png -pix_fmt rgba -filter_complex "scale=w=100:h=100" -y o.png原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。