《H.264/AVC視訊編解碼技術詳解》視訊教程已經在“CSDN學院”上線,視訊中詳述了H.264的背景、標準協議和實現,並通過一個實戰工程的形式對H.264的標準進行解析和實現,歡迎觀看!

“紙上得來終覺淺,絕知此事要躬行”,只有自己按照標準文件以程式碼的形式操作一遍,才能對視訊壓縮編碼標準的思想和方法有足夠深刻的理解和體會!

GitHub程式碼地址:點選這裡

本節視訊免費

1. 熵編碼概念

“熵”這一概念原本來自於化學和熱力學,用於度量能量退化的指標,即熵越高,物體或系統的做功能力越低。後來夏農將這一概念引入到資訊理論中,用於表示訊息的平均資訊量。信源的熵通常可以表示信源所發出資訊的不確定性,即越是隨機的、前後不相關的資訊,其熵越高。

在資訊理論中,夏農提出了信源編碼定理。該定理說明了夏農熵與信源符號概率之間的關係,說明資訊的熵為信源無損編碼後的平均碼字長度的下限。任何的無損編碼方法都不可能使編碼後的平均碼長小於夏農熵,只能使其儘量接近。

基於此,對信源進行熵編碼的基本思想,是使其前後的碼字之間儘量更加隨機,儘量減小前後的相關性,更加接近其信源的夏農熵。這樣在表示同樣的資訊量時所用的資料長度更短。

在實際使用中,常用的熵編碼主要有變長編碼和算術編碼等方法。其中變長編碼相對於算術編碼較為簡單,但平均壓縮比可能略低。常見的變長編碼方法有哈夫曼編碼和夏農-費諾編碼等。

2. 熵編碼的簡單實現——哈夫曼編碼

戴維·哈夫曼(David·A·Huffman)於1952年在麻省理工學院的羅伯特·費諾的指導下攻讀博士學位時,發明了一種基於有序頻率二叉樹的編碼方法,該方法的編碼效率超過了他的導師和資訊理論之父夏農的研究成果(夏農-費諾編碼),因此又稱作“最優編碼方法”。

哈夫曼編碼時變長編碼方法的一種,該方法完全依賴於碼字出現的概率來構造整體平均長度最短的編碼方法。進行哈夫曼編碼的關鍵步驟是建立符合哈夫曼編碼規則的二叉樹,該樹又稱作哈夫曼樹。

哈夫曼樹是一種特殊的二叉樹,其終端節點的個數與待編碼的碼元的個數等同,而且每個終端節點上都帶有各自的權值。每個終端節點的路徑長度乘以該節點的權值的總和稱為整個二叉樹的加權路徑長度。在滿足條件的各種二叉樹中,該路徑長度最短的二叉樹即為哈夫曼樹。

在使用哈夫曼編碼執行對碼元的實際編碼過程時,碼元的權值可設定為其概率值,那麼可以根據其權值來構建哈夫曼樹。我們假設使用哈夫曼編碼對以下概率的碼字進行編碼:

碼字 概率
A 0.1
B 0.1
C 0.15
D 0.2
E 0.2
F 0.25

根據概率表構建哈夫曼樹的過程如下圖所示:

最終我們可以得到如下圖所示的哈夫曼樹:

在哈夫曼樹構建完成後,便可以得到每一個碼元的哈夫曼編碼的碼字。具體方法是:從哈夫曼樹的根節點開始遍歷,直至每一個終端節點,當訪問某個節點的左子樹時賦予碼字0,訪問右子樹時賦予一個碼字1(反之亦可),直到遍歷到終端節點時這一路徑所代表的0和1的串便是該碼元的哈夫曼編碼碼字。

例如上圖的哈夫曼樹,根節點訪問左子樹ABCF,賦予碼字0;然後再訪問左子樹ABC,賦予碼字0,此時整個碼字為00,然後訪問右子樹得到終端節點C,賦予碼字1,此時便可以得到C的哈夫曼編碼碼字001。以此規律,整個六個元素的碼元集合的編碼碼錶為:

  • A: 0000
  • B: 0001
  • C: 001
  • D: 10
  • E: 11
  • F: 01

從這個碼錶中還可以看出另外一個規律:哈夫曼編碼的任意一個碼字,都不可能是其他碼字的字首。因此通過哈夫曼編碼的資訊可以緊密排列連續傳輸,而不用擔心解碼時的歧義性。

3. 哈夫曼樹的構建Demo

下面的程式段給出一個構建哈夫曼樹,並生成對應碼元的哈夫曼編碼的過程:

#include "stdafx.h"
#include <iostream>
#include <fstream>
#include <queue>
#include <vector>
#include <string>

using namespace std;

//每一個符號定義為一個結構體,包括字元和出現頻次
typedef struct
{
    unsigned char   character;
    unsigned int    frequency;
} CharNode;

static bool open_input_file(ifstream &input, const char *inputFileName)
{
    input.open(inputFileName);
    if (!input.is_open())
    {
        return false;
    }
    return true;
}

struct MinHeapNode
{
    char data;
    unsigned int freq;
    MinHeapNode *left, *right;
    MinHeapNode(char data, unsigned freq)
    {
        left = right = NULL;
        this->data = data;
        this->freq = freq;
    }
};
typedef struct MinHeapNode MinHeapNode;

struct compare
{
    bool operator()(MinHeapNode* l, MinHeapNode *r)
    {
        return (l->freq > r->freq);
    }
};

static void get_huffman_code(MinHeapNode *root, string code)
{
    if (!root)
    {
        return;
    }

    if (root->data != -1)
    {
        cout << root->data << " : " << code << endl;;
    }

    get_huffman_code(root->left, code + "0");
    get_huffman_code(root->right, code + "1");
}

int _tmain(int argc, _TCHAR* argv[])
{
    ifstream inputFile;
    if (!open_input_file(inputFile, "input.txt"))
    {
        cout << "Error: opening input file failed!" << endl;
        return -1;
    }

    char buf = inputFile.get();
    CharNode nodeArr[256] = { { 0, 0 } };
    while (inputFile.good())
    {
        cout << buf;
        nodeArr[buf].character = buf;
        nodeArr[buf].frequency++;
        buf = inputFile.get();
    }
    cout << endl;

    priority_queue<MinHeapNode*, vector<MinHeapNode*>, compare>  minHeap;
    for (int idx = 0; idx < 256; idx++)
    {
        if (nodeArr[idx].frequency > 0)
        {
            cout << "Node " << idx << ": [" << nodeArr[idx].character << ", " << nodeArr[idx].frequency << "]" << endl;
            minHeap.push(new MinHeapNode(nodeArr[idx].character, nodeArr[idx].frequency));
        }
    }

    MinHeapNode *leftNode = NULL, *rightNode = NULL, *topNode = NULL;
    while (minHeap.size() != 1)
    {
        leftNode = minHeap.top();
        minHeap.pop();

        rightNode = minHeap.top();
        minHeap.pop();

        topNode = new MinHeapNode(-1, leftNode->freq + rightNode->freq);
        topNode->left = leftNode;
        topNode->right = rightNode;
        minHeap.push(topNode);
    }

    get_huffman_code(topNode, "");

    inputFile.close();
    return 0;
}

程式的詳細解釋過程請到視訊中觀看。