上一篇打通了读取字节流文件后,能够得到每一帧的字节数据了,这一篇就来解决为这些数据赋予实际意义。

彩色图像采用H.264编码 -> 那就需要按照H.264解码

视差图像采用LZ4压缩 -> 视差图保存的16位无符号整数,采用LZ4解压缩(开源)

背景知识

LZ4

LZ4 是一个非常快速的压缩算法,提供了实时压缩速度和非常快的解压缩速度,由 Yann Collet(FaceBook大佬) 开发。它属于无损压缩算法,意味着压缩后的数据可以完全恢复到其原始形态。LZ4 主要设计用于非常高速的场景(解压速度可以达到数GB每秒),例如实时数据传输、日志数据处理等。

H.264

H.264,也被称为MPEG-4 AVC(Advanced Video Coding),是一种广泛使用的视频压缩标准。它是由国际电信联盟(ITU-T)下的视频编码专家组(VCEG)以及运动图像专家组(MPEG)共同开发的。H.264标准的目标是提供高质量的视频传输,同时显著降低比特率,相对于以前的标准如MPEG-2和MPEG-4 Part 2,它在保持相同视觉质量的情况下,可以使文件大小减少到原来的一半以上。

H.264 和 Qt

Qt 使用 Qt Multimedia 模块来处理多媒体内容,包括视频和音频。这个模块支持多种格式,包括 H.264。使用 Qt Multimedia,开发者可以较容易地在应用中集成视频播放功能,而不需要直接处理底层的视频解码。

  • Qt Multimedia Widgets:可以使用 QMediaPlayerQVideoWidget 来播放 H.264 视频。Qt 处理多媒体流的解码和渲染,开发者只需要关注如何控制播放器和集成到UI中。
  • 自动解码:Qt 底层使用系统的编解码库(如 DirectShow on Windows, GStreamer on Linux)来解码视频流,包括 H.264。这意味着通常不需要手动解析 H.264 数据帧,除非需要非常定制化的处理。

FFmpeg

FFmpeg 是一个开源的多媒体框架,它提供了一套全面的库和工具,用于处理视频、音频和其他多媒体文件和流。这个框架支持转码、转流、播放和分析几乎所有类型的多媒体数据。FFmpeg 包括以下主要组件:

  • libavcodec: 提供广泛的编解码器支持,是处理视频和音频编解码的核心库。
  • libavformat: 处理多种音视频容器格式的输入和输出。
  • libavutil: 包含一些辅助的实用功能,如日志管理和错误处理。
  • libavfilter: 提供视频和音频流的转换和操作功能。
  • libswscale: 处理图像色彩和像素格式转换。
  • libswresample: 处理音频采样数据的转换。

项目中使用LZ4

首先安装 liblz4-dev

sudo apt update
sudo apt install liblz4-dev

有可能 CMake 仍然找不到 LZ4,这时候可以手动指定 LZ4 库和头文件的路径。首先需要确定 liblz4.so 和 LZ4 的头文件 lz4.h 的位置。这些文件通常位于 /usr/include/usr/lib 下,如果不确定可以通过 find /usr -name lz4.h命令查找

修改 CMakeLists.txt 文件,手动设置路径:

cmake_minimum_required(VERSION 3.10)
project(DataFrameParser)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# Find OpenCV
find_package(OpenCV REQUIRED)

# Include directories for OpenCV and manually for LZ4
include_directories(
    ${OpenCV_INCLUDE_DIRS}
    /usr/include
)

# Link directories for LZ4
link_directories(/usr/lib /usr/lib/x86_64-linux-gnu)  # Adjust this path according to your system

add_executable(DataFrameParser main.cpp)
target_link_libraries(DataFrameParser lz4 ${OpenCV_LIBS})

这里,这里使用了 link_directoriestarget_link_libraries 中直接指定 lz4,而不是 ${LZ4_LIBRARIES}。这是因为没有使用 find_package 来定义 LZ4_LIBRARIES 变量。

项目中使用FFmpeg

安装 FFmpeg 和相应的开发库

sudo apt update
sudo apt install ffmpeg
sudo apt install libavcodec-dev libavformat-dev libswscale-dev libavutil-dev

这些开发库包括用于处理视频编解码的 libavcodec,用于处理多媒体容器格式的 libavformat,以及用于视频缩放和格式转换的 libswscale 等。

在 CMake 项目中使用 FFmpeg,则需要在 CMakeLists.txt 文件中正确设置找到和链接这些库,举例如下:

cmake_minimum_required(VERSION 3.10)
project(DataFrameParser)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# Find OpenCV
find_package(OpenCV REQUIRED)

# Find FFmpeg components
find_package(PkgConfig REQUIRED)
pkg_check_modules(AVCODEC REQUIRED libavcodec)
pkg_check_modules(AVFORMAT REQUIRED libavformat)
pkg_check_modules(SWSCALE REQUIRED libswscale)
pkg_check_modules(AVUTIL REQUIRED libavutil)

include_directories(
    ${OpenCV_INCLUDE_DIRS}
    ${AVCODEC_INCLUDE_DIRS}
    ${AVFORMAT_INCLUDE_DIRS}
    ${SWSCALE_INCLUDE_DIRS}
    ${AVUTIL_INCLUDE_DIRS}
)

add_executable(DataFrameParser main.cpp)

target_link_libraries(DataFrameParser
    ${OpenCV_LIBS}
    ${AVCODEC_LIBRARIES}
    ${AVFORMAT_LIBRARIES}
    ${SWSCALE_LIBRARIES}
    ${AVUTIL_LIBRARIES}
)

难点分析

不使用QT,希望能够转换为OpenCV处理的图像,因此需要使用到FFmpeg

由于处理的字节流,那么就需要保证能够持续的解码。也就是需要初始化一次,然后持续运行,运行结束后释放资源。

使用 FFmpeg 解析 H.264 图像的过程和原理

  1. 初始化和配置解码器(这部分只需要初始化一次)

    • 查找并打开解码器:使用 avcodec_find_decoder 根据指定的编解码器ID(例如 AV_CODEC_ID_H264)来查找对应的解码器。然后使用 avcodec_alloc_context3avcodec_open2 来分配并初始化解码器上下文。
    • 设置解码上下文:配置解码器上下文,如设置时间基、延迟等。
  2. 解码过程

  • 读取和发送数据包:从源数据(如文件、网络流)中读取数据,并构建成 AVPacket。使用 av_read_frame 从多媒体文件读取数据帧或使用 av_init_packet 处理自定义的数据,然后使用 avcodec_send_packet 将数据包发送给解码器。
  • 接收和解码:使用 avcodec_receive_frame 接收解码器输出的帧。该函数从解码器中提取一个解码好的帧,如果没有足够的数据解码一个完整的帧,它会返回 EAGAIN
  • 图像转换:使用 libswscale(或 SwsContext)将解码后的帧(通常为YUV格式)转换为其他像素格式(如RGB),以便于显示或进一步处理。
  1. 资源清理
    • 释放资源:使用 av_frame_freeav_packet_freeavcodec_close 释放帧、数据包和关闭解码器。

代码实现

// 全局变量或类成员变量
AVCodecContext* context = nullptr;

bool initializeDecoder() {
    AVCodec* codec = avcodec_find_decoder(AV_CODEC_ID_H264);
    if (!codec) {
        std::cerr << "Codec not found.\n";
        return false;
    }

    context = avcodec_alloc_context3(codec);
    if (!context) {
        std::cerr << "Could not allocate video codec context.\n";
        return false;
    }

    if (avcodec_open2(context, codec, NULL) < 0) {
        std::cerr << "Could not open codec.\n";
        avcodec_free_context(&context);
        return false;
    }
    return true;
}

void cleanupDecoder() {
    if (context) {
        avcodec_close(context);
        avcodec_free_context(&context);
    }
}

bool processH264Frame(const std::vector<uint8_t>& data, cv::Mat& image) {
    AVPacket* pkt = av_packet_alloc();
    if (!pkt) {
        std::cerr << "Could not allocate AVPacket.\n";
        return false;
    }

    av_init_packet(pkt);
    // set data source
    pkt->data = const_cast<uint8_t*>(data.data());  // Unsafe cast, data must be non-const
    pkt->size = data.size();

    int ret = avcodec_send_packet(context, pkt);
    if (ret < 0) {
        std::cerr << "Error sending a packet for decoding.\n";
        av_packet_free(&pkt);
        return false;
    }

    AVFrame* frame = av_frame_alloc();
    ret = avcodec_receive_frame(context, frame);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
        av_frame_free(&frame);
        av_packet_free(&pkt);
        return false;
    } else if (ret < 0) {
        std::cerr << "Error during decoding.\n";
        av_frame_free(&frame);
        av_packet_free(&pkt);
        return false;
    }

    // Convert AVFrame to cv::Mat
    // yuvj420p
    std::cout << "Pixel Format: " << av_get_pix_fmt_name(static_cast<AVPixelFormat>(frame->format)) << std::endl;
    std::cout << "Pixel Format Enum: " << frame->format << std::endl;
    SwsContext* img_convert_ctx = sws_getContext(frame->width, frame->height,
                                                 static_cast<AVPixelFormat>(frame->format),
                                                 frame->width, frame->height, AV_PIX_FMT_BGR24,
                                                 SWS_BICUBIC, NULL, NULL, NULL);

    int linesize[1] = {3 * frame->width};  // RGB stride
    image = cv::Mat(frame->height, frame->width, CV_8UC3);
    sws_scale(img_convert_ctx, frame->data, frame->linesize, 0, frame->height, &image.data, linesize);
    sws_freeContext(img_convert_ctx);
    av_frame_free(&frame);
    av_packet_free(&pkt);

    return true;
}

代码分析

由于SPS(序列参数集)和PPS(图像参数集)等关键解码信息通常只在视频流的开始部分发送,因此只需要初始化解码器一次。(最开始的实现是每次解码帧之前都重新初始化解码器,这样的话,这些关键信息可能会丢失,导致后续帧无法正确解码,表现的现象是卡在了第一帧画面)。

以下是上述代码的逐行解释:

初始化解码器

// 找到并确认解码器
AVCodec* codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
    std::cerr << "Codec not found.\n";
    return false;
}
  • avcodec_find_decoder: 这个函数用于查找指定编解码器(这里是H.264),返回一个指向解码器的指针。
  • if (!codec): 如果没有找到解码器,输出错误信息并返回失败。
// 分配编解码器上下文
AVCodecContext* context = avcodec_alloc_context3(codec);
if (!context) {
    std::cerr << "Could not allocate video codec context.\n";
    return false;
}
  • avcodec_alloc_context3: 分配一个新的解码器上下文,关联给定的编解码器。
  • if (!context): 如果上下文分配失败,输出错误信息并返回失败。
// 打开解码器
if (avcodec_open2(context, codec, NULL) < 0) {
    std::cerr << "Could not open codec.\n";
    return false;
}
  • avcodec_open2: 初始化解码器上下文以准备使用,传入的 codec 是上文查找得到的编解码器。
  • if (avcodec_open2(...) < 0): 如果解码器打开失败,输出错误信息并返回失败。

处理单帧数据

// 分配数据包
AVPacket* pkt = av_packet_alloc();
if (!pkt) {
    std::cerr << "Could not allocate AVPacket.\n";
    return false;
}
  • av_packet_alloc: 分配一个新的数据包。数据包用于存储编码的数据(例如一个压缩的视频帧)。
  • if (!pkt): 如果数据包分配失败,输出错误信息并返回失败。
// 初始化数据包并设置数据
av_init_packet(pkt);
pkt->data = const_cast<uint8_t*>(data.data());  // Unsafe cast, data must be non-const
pkt->size = data.size();
  • av_init_packet: 初始化分配的数据包。
  • pkt->data, pkt->size: 设置数据包的数据指针和大小。data 是函数传入的H.264数据。
// 发送数据包到解码器
int ret = avcodec_send_packet(context, pkt);
if (ret < 0) {
    std::cerr << "Error sending a packet for decoding.\n";
    return false;
}
  • avcodec_send_packet: 将数据包发送到解码器,等待解码。
  • if (ret < 0): 如果发送失败,输出错误信息并返回失败。

接收解码后的帧并转换为cv::Mat

// 分配帧存储解码后的数据
AVFrame* frame = av_frame_alloc();
ret = avcodec_receive_frame(context, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
    av_frame_free(&frame);
    break;
} else if (ret < 0) {
    std::cerr << "Error during decoding.\n";
    return false;
}
  • av_frame_alloc: 分配一个帧用于存储解码后的数据。
  • avcodec_receive_frame: 从解码器中获取一个解码后的帧。
  • if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF): 检查是否全部数据已处理完毕或需要更多数据。
// 转换解码后的帧为cv::Mat格式
SwsContext* img_convert_ctx = sws_getContext(frame->width, frame->height, static_cast<AVPixelFormat>(frame->format), frame->width, frame->height, AV_PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);

int linesize[1] = {3 * frame->width};  // RGB stride
image = cv::Mat(frame->height, frame->width, CV_8UC3);
sws_scale(img_convert_ctx, frame->data, frame->linesize, 0, frame->height, &image.data, linesize);
sws_freeContext(img_convert_ctx);
  • sws_getContext: 创建一个上下文用于转换像素数据格式。
  • sws_scale: 转换像素数据格式从解码帧的格式到BGR24,这是OpenCV常用的格式。
  • cv::Mat: 创建一个OpenCV矩阵来存储图像数据。

这个流程需要在视频处理的生命周期中只初始化和清理一次编解码器,而帧处理可以多次调用,以提高性能和保持编解码器状态。这是一种高效处理视频帧的典型方法。

L4Z 解压缩

L4Z解压缩的过程相对直接,直接根据函数来实现即可。

代码实现

bool decompressLZ4Frame(const std::vector<uint8_t>& compressedData, cv::Mat& image, int width, int height) {
    std::vector<uint8_t> decompressedData(width * height * sizeof(uint16_t));
    int decompressedSize = LZ4_decompress_safe(reinterpret_cast<const char*>(compressedData.data()),
                                               reinterpret_cast<char*>(decompressedData.data()),
                                               compressedData.size(),
                                               decompressedData.size());
    if (decompressedSize < 0) {
        std::cerr << "Failed to decompress LZ4 data." << std::endl;
        return false;
    }

    image = cv::Mat(height, width, CV_16UC1, decompressedData.data());

    cv::Mat pseudoColorImage(height, width, CV_8UC3);
    for (int i = 0; i < height; i++) {
        for (int j = 0; j < width; j++) {
            uint16_t disparity = image.at<uint16_t>(i, j);
            if (disparity < 8192) {
                pseudoColorImage.at<cv::Vec3b>(i, j) = cv::Vec3b(pColorTable[disparity * 3 + 2],
                                                                  pColorTable[disparity * 3 + 1],
                                                                  pColorTable[disparity * 3]);
            } else {
                pseudoColorImage.at<cv::Vec3b>(i, j) = cv::Vec3b(0, 0, 0);
            }
        }
    }
    image = pseudoColorImage;

    return true;
}

这里对解压缩的视差图进行颜色映射的处理,是根据视差值直接映射得到