从MPEG-2传输流(H.264 - 附录B)字节流中提取原始I帧图像数据

3

背景

我正在尝试从带有H.264 annex B编解码器的MPEG-2传输流中提取每个I帧的原始图像数据。该视频在每隔2秒的间隔内包含I帧。据我所知,可以在类型为5(例如IDR图片的编码切片)的NALu起始码后找到I帧。这些NALu的字节有效负载包含构建完整帧所需的所有数据。虽然对于我来说是以H.264编码格式。

我想要构建一个方案,从输入的字节流中提取这些I帧,通过查找包含I帧的NALu,保存有效负载并将有效负载解码为某种普遍的原始图像格式,以访问像素数据等。

注意:如有可能,我想避免使用类似于ffmpeg的依赖文件系统的二进制文件,并且更重要的是如果可行的话!

概念验证

到目前为止,我已经使用Rust构建了一个概念验证,以查找I帧的字节偏移量和字节大小:

use std::fs::File;
use std::io::{prelude::*, BufReader};
extern crate image;

fn main() {
    let file = File::open("vodpart-0.ts").unwrap();
    let reader = BufReader::new(file);

    let mut idr_payload = Vec::<u8>::new();
    let mut total_idr_frame_count = 0;
    let mut is_idr_payload = false;
    let mut is_nalu_type_code = false;
    let mut start_code_vec = Vec::<u8>::new();

    for (pos, byte_result) in reader.bytes().enumerate() {
        let byte = byte_result.unwrap();
        if is_nalu_type_code {
            is_idr_payload = false;
            is_nalu_type_code = false;
            start_code_vec.clear();
            if byte == 101 {
                is_idr_payload = true;
                total_idr_frame_count += 1;
                println!("Found IDR picture at byte offset {}", pos);
            }
            continue;
        }
        if is_idr_payload {
            idr_payload.push(byte);
        }
        if byte == 0 {
            start_code_vec.push(byte);
            continue;
        }
        if byte == 1 && start_code_vec.len() >= 2 {
            if is_idr_payload {
                let payload = idr_payload.len() - start_code_vec.len() + 1;
                println!("Previous NALu payload is {} bytes long\n", payload);
                save_image(&idr_payload.as_slice(), total_idr_frame_count);
                idr_payload.clear();
            }
            is_nalu_type_code = true;
            continue;
        }
        start_code_vec.clear();
    }

    println!();
    println!("total i frame count: {}", total_idr_frame_count);

    println!();
    println!("done!");
}

fn save_image(buffer: &[u8], index: u16) {
    let image_name = format!("image-{}.jpg", index);
    image::save_buffer(image_name, buffer, 858, 480, image::ColorType::Rgb8).unwrap()
}

其结果看起来像这样:
Found IDR picture at byte offset 870
Previous NALu payload is 202929 bytes long

Found IDR picture at byte offset 1699826
Previous NALu payload is 185069 bytes long

Found IDR picture at byte offset 3268686
Previous NALu payload is 145218 bytes long

Found IDR picture at byte offset 4898270
Previous NALu payload is 106114 bytes long

Found IDR picture at byte offset 6482358
Previous NALu payload is 185638 bytes long


total i frame count: 5

done!

根据我的研究,使用H.264比特流查看器等工具,这是正确的。在那些字节偏移处,有确切地5个I帧!

问题是我不知道如何将H.264字节流有效载荷转换为原始图像RBG数据格式。一旦转换为jpg格式的图像就会变得模糊,占用大约10%的图像区域。

例如:

输出jpg图像

问题

  1. 是否需要进行解码步骤?
  2. 我是否正确处理,并且自己尝试这样做是可行的,还是应该依赖于另一个库?

任何帮助都将不胜感激!

1个回答

1

“是否需要执行解码步骤?”

是的。从头编写解码器非常复杂。描述它的文档(ISO 14496-10)长达750页。你应该使用一个库。ffmpeg的Libavcodec是你唯一的选择。(除非你只需要基准配置文件,在这种情况下,你可以使用android的开源解码器)

你可以编译一个自定义版本的libavcodec来排除你不需要的内容。


谢谢您的澄清!我看到了一个类似问题的答案,警告ISO文档的大小。后续问题:解码I-frame NALu负载后,图像数据将以什么格式表示?并且像libavcodec这样的库是否能够接受单个帧的字节负载? - Jarvis Prestidge
"像libavcodec这样的库能否接受单个帧的字节负载?" 这取决于格式,如果它是annex B格式,并且"帧"包括SPS/PPS和IDR,则可以。 - szatmary
解码I-frame NALu有效载荷后,图像数据以何种格式表示?这取决于其编码方式,但最常见的是YUV和4:2:0色度子采样平面。 - szatmary

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接