【AVD】杀鸡用牛刀,FFmpeg API 加载存储图片,比 ImageMagic 和 stb_image 快多了

最近工作中遇到一个需求。现有代码中的图形库使用 ImageMagic 加载图片并做简单处理,但是在移植到 iOS 平台的过程中遇到了些问题。于是找到我,看能否用 FFmpeg 实现图片的从文件中读取加载、存储到文件中、以及缩放、裁剪等简单处理,并对比与 ImageMagic 相关功能的效率
于是就边学边做,用 FFmpeg API 接口实现了一个 MyImage 类,提供 Load(string filename), Load(uint8_t* buffer, int w, int h, AVPixelFormat fmt) , Write(string filename), Write(uint8_t* buffer, int bufsize, enum AVPixelFormat format), width(), height(), format(), Resize(int to_w, to_h), Crop(int x, int y, int w, int h) 等接口。

编解码是大头

但很容易理解。关键就是把一张图片看作是只有一帧的视频

读取

所以,读取图片文件的整个代码流程如下:

bool Load(string filename) {
	AVFormatContext* fmt_ctx = nullptr;
	AVCodecContext* codec_ctx = nullptr;
	AVCodec* codec = nullptr;
	avformat_open_input(&fmt_ctx, filename.c_str(), nullptr, nullptr);
	// 代码删除了所有判断 api 调用失败的情况
	int stid = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, false);
	codec_ctx = avcodec_alloc_context3(codec);
	avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[stid]->codecpar);
	avcodec_open2(codec_ctx, codec, nullptr);
	AVFrame* frame = av_frame_alloc();
	frame->format = codec_ctx->pix_fmt;
	frame->width = codec_ctx->width;
	frame->height = codec_ctx->height;
	av_frame_get_buffer(frame, 0);
	AVPacket pkt;
	av_init_packet(&pkt);
	int ret = 0;
	while (av_read_frame(fmt_ctx, &pkt) >= 0) {
		if (pkt.stream_index == stid) {
			avcodec_send_packet(codec_ctx, &pkt);
			while ((ret = avcodec_receive_frame(codec_ctx, frame)) != 0) {
				if (ret == AVERROR_EOF) break;
				avcodec_send_packet(codec_ctx, nullptr);
			}
			av_packet_unref(&pkt);
			break;
		} else {
			av_packet_unref(&pkt);
		}
	}
	av_packet_unref(&pkt); // 注意:缺少这句将造成内存泄漏
	return frame->width * frame->height > 0;
}

以上,就算是成功地将一张 .jpg 或者 .png 或者其他扩展名的图片,读取到内存中了。只是暂时它存储在 frame->data 里,我们没有拷贝到自己申请的内存中。缺少倒数第三行会造成内存泄漏,参见我的文章FFmpeg av_packet_unref() 不严谨导致的一次内存泄漏,每次 6MB。当然,正常编码中 AVFrame* frame 是一个类成员变量,而非局部变量。此处为了方便,简化了。

存储

同样的,存储的代码流程如下:

bool Save(string filename) {
	if (!frame || frame->data[0]) { // 假设 frame 是个类成员变量
		cout << "nothing to save to file." << endl;
		return false;
	}
	AVFormatContext* ofmt_ctx = nullptr;
	AVCodecContext* ocodec_ctx = nullptr;
	AVCodec* ocodec = nullptr;
	int ret = avformat_alloc_output_context2(&ofmt_ctx, nullptr, nullptr, filename.c_str());
	if (!ofmt_ctx) {
		cout << "could not deduce output format from file extension, using image2. << endl;
		avformat_alloc_output_context2(&ofmt_ctx, nullptr, "image2", filename.c_str());
	}
	ocodec = avcodec_find_encoder(ofmt_ctx->oformat->video_codec);
	AVStream* st = avformat_new_stream(ofmt_ctx, nullptr);
	st->id = ofmt_ctx->nb_streams - 1;
	ocodec_ctx = avcodec_alloc_context3(ocodec);
	ocodec_context_->codec_id = oformat_context_->oformat->video_codec;
	ocodec_context_->codec_type = AVMEDIA_TYPE_VIDEO;
	ocodec_context_->width = frame_->width;
	ocodec_context_->height = frame_->height;
	st->time_base = av_make_q(1, 25);
	ocodec_context_->time_base = st->time_base;
	ocodec_context_->sample_aspect_ratio = av_make_q(1, 1);
	ocodec_context_->profile = FF_PROFILE_MJPEG_HUFFMAN_PROGRESSIVE_DCT;
	ocodec_context_->pix_fmt = AV_PIX_FMT_YUVJ420P; 
	avcodec_open2(ocodec_ctx, ocodec, nullptr);
	avcodec_parameters_from_context(st->codecpar, ocodec_ctx);
	avio_open(&ofmt_ctx->pb, filename.c_str(), AVIO_FLAG_WRITE);
	avformat_write_header(ofmt_ctx, nullptr);
	avcodec_send_frame(ocodec_ctx, frame);
	AVPacket pkt = {0};
	avcodec_receive_packet(ocodec_ctx, &pkt);
	av_packet_rescale_ts(&pkt, ocodec_ctx->time_base, st->time_base);
	pkt.stream_index = st->index;
	av_write_frame(ofmt_ctx, &pkt);
	av_packet_unref(&pkt);
	av_write_trailer(ofmt_ctx);
	avio_closep(&ofmt_ctx->pb);
	return true;
}

同样地,过程中省略了一些必要的 ffmpeg api 执行错误判断。

缩放

缩放听简单的,就是读取出来的图片在 frame 中,再利用 swscale 相关接口进行一次缩放或者图片颜色格式转换即可,与视频缩放一样。暂不赘述。

裁剪

由于 FFmpeg 的裁剪功能是用滤镜实现的。感觉颇为麻烦,于是,自己参考其 crop 滤镜实现了一种基于 RGBA 颜色格式的裁剪方法。具体代码如下:

bool Crop(int x, int y, int w, int h) { // 以 (x,y) 为起始,裁剪宽 w 高 h 的图片
	// 如果不是 RGBA 颜色格式,需先转换成 RGBA 颜色格式,此处省略
	// 转换成 RGBA 后的图片帧缓存在 frame 中
	croped_frame->width = w;
	croped_frame->height = h;
	croped_frame->format = frame->format;
	av_image_alloc(croped_frame->data, croped_frame->linesize, croped_frame->width, croped_frame->height, (AVPixelFormat)croped_frame->format, 1);
	uint8_t * p = nullptr;
	for (int i = 0; i < 8 && frame->linesize[i] > 0; i++) {
		// p = frame->data[i];
		// p += y * frame->linesize[i];
		// p += (frame->linesize[i] / frame->width) * x;
		// croped_frame->linesize[i] = (frame->linesize[i] / frame->width) * w;
		// av_image_copy_plane(croped_frame->data[i], croped_frame->linesize[i], p, frame->linesize[i], croped_frame->linesize[i], croped_frame->height);
		frame->data[i] += y * frame->linesize[i];
		frame->data[i] += (frame->linesize[i] / frame->width) * x;
		croped_frame->linesize[i] = (frame->linesize[i] / frame->width) * w;
		av_image_copy_plane(croped_frame->data[i], croped_frame->linesize[i], frame->data[i], frame->linesize[i], croped_frame->linesize[i], croped_frame->height);
	}
	av_frame_free(&frame);
	frame = croped_frame;
	croped_frame = nullptr;
	return true;
}

现在看起来,这个 crop 方法,可能存在一定的内存泄漏啊。因为 data[i]+= 操作移走了,av_frame_free 时估计会释放不完所有内存。因此,这里应该用一个临时变量 uint8_t * p 来代替其进行操作。由于是后期发现的 bug,因此代码中用 注释 进行标记。
for循环里,i < 8 是因为目前 FFmpeg 结构体中的 AVFrame->data[8] 是个长度为 8 的指针数组。当然,也得保证 frame->linesize[i] > 0 才行,其实这一条件,在这里,决定了,它只会执行一次,因为 RGBA 颜色格式的 frame->linesize[1] = 0
假设我们有一张位图,当前指针指向位图的 (0,0) 位置,第一句,frame->data[i] += y * frame->linesize[i]; 是将指针纵向向下移动 y 行。这时,指针来到了 (0,y - 1)位置
第二句 frame->data[i] += (frame->linesize[i] / frame->width) * x; 这里面,() 中的表达式计算了一个像素的字节数,然后乘上 x,就是讲指针向右移动 x 列,目前,指针来到了 (x - 1,y - 1) 位置。然后调用 ffmpeg 的接口 av_image_copy_plane 即可。

测试

在一台 linux 机器上,使用了一张 5.76MB 的 jpg 图片进行了简单的测试。
加载 100 次用时 15 秒,而使用 ImageMagic 则用时 29 秒。
使用 FFmpeg 的 swscale 对图片进行缩放,从 5k5k 到 1k1k,反复 100 次,用时 8 秒。使用 ImageMagic 反复缩放 10 次即用时 18 秒。
实现了一个 crop 方案,crop 50 次,用时 1 秒,而 ImageMagic 用时 2 秒。

结语

虽然 FFmpeg 对图片进行加载、处理,比 ImageMagic 和 stb_image 快不少。但是,FFmpeg 毕竟比较复杂,而且相关依赖更多。具体取舍还得三思。
我们是刚好工程中有音视频模块,本就用到 FFmpeg,所以,既杀牛又杀鸡的,就用一把刀也无所谓了。而且我们需要反复加载多张图片,如果只加载一次图片(例如之前实现的安全围栏的纹理素材),则 hpp 的 stb_image 是最好最简单的选择了。只需一个头文件,和一个接口,即可实现。


版权声明:本文为u014248312原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。