使用DPCM進行圖像壓縮的C++實現方法

使用DPCM進行圖像壓縮的C++實現方法

我們知道,對於圖像或視頻的每一幀而言,相鄰的像素之間有着較強的相關性——除了在邊緣、輪廓等位置,相鄰像素的像素值相差並不大,也就是說,圖像或視頻的一幀中存在着很大的空間冗餘。而DPCM(Differential Pulse Code Modulation,差分脈衝編碼調製)便是一種簡單而高效的去冗餘算法。

基本原理

DPCM

對於去除引言中提到的空間冗餘,一種很自然的想法便是,使用前一個像素的像素值作爲當前像素值的預測,然後只存儲並傳輸當前像素值和預測值的差值。

以8 bit量化的圖像爲例,差值的範圍在[255,255][-255, 255],完全表示需要9 bit,但並不是在所有圖像中都存在由純黑突變到純白的情況,因而如果實際的動態範圍只有[63,63][-63, 63],那麼就可以只用7 bit表示,從而實現初步的壓縮。

但除此之外,我們可以更多地依靠將差值量化,以進行更進一步的壓縮。量化的原理實際上非常簡單:例如對於一組取值爲[0,255][0,255]的數據,若要進行4 bit量化,我們只需要將它們分別除以284=162^{8-4}=16並向下取整即可,這樣數據範圍變成了[0,15][0,15],也就完成了量化。對於差值信號d[255,255]d\in[-255, 255],若同樣要進行4 bit量化,我們就要先將雙極性數據轉化爲單極性,再重複前面的操作即可:
d^=d+255294 \hat d = \left\lfloor \dfrac{d+255} {2^{9-4}} \right\rfloor
我們再將量化後的預測誤差與預測值疊加,就得到了該像素的重建(reconstruction)值,同時作爲下一個像素的預測值。

至此,我們就完成了DPCM編碼。實際上,在編碼器中已經包含了解碼器,因爲解碼同樣只需要將預測值和量化預測誤差相加。

其對應的框圖如下:


DPCM編解碼器原理框圖

PSNR

爲了便於我們對實驗結果進行定量分析,在這裏再簡單介紹一下PSNR(Peak Signal to Noise Ratio,峯值信號噪聲比)。

首先計算均方誤差 MSE(Mean Square Error):
MSE=i=0Mj=0N[f(i,j)f(i,j)]2MN {\rm MSE} = \frac{\sum_{i = 0}^M \sum_{j=0}^N \left[f(i,j)-f'(i,j)\right]^{2}}{MN}

其中M,NM,N爲圖像的寬和高,f(i,j),f(i,j)f(i,j),f’(i,j)爲原圖像和重建圖像在(i,j)(i,j)的像素值。PSNR(單位:dB)以如下方式計算:
PSNR=10lg(2bits1)2MSE {\rm PSNR} = 10\lg \dfrac {\left(2^{\rm bits}-1\right)^2}{\rm MSE}

其中bits{\rm bits}爲原圖像地量化比特數,即8。

一般來說:

  • PSNR40dB\text {PSNR} \ge 40 \text {dB}時,圖像質量非常好,接近於原圖像;
  • 30dBPSNR<40dB30 \text {dB} \le \text {PSNR} < 40 \text {dB}時,圖像有可察覺的失真,但質量仍可接受;
  • 20dBPSNR<30dB20 \text {dB} \le \text {PSNR} < 30 \text {dB}時,圖像質量較差;
  • PSNR<20dB\text {PSNR} < 20 \text {dB}時,圖像質量已經無法接受。

Huffman編碼

Huffman是一種無失真信源編碼算法,編碼後可以得到即時的最佳碼。在這裏我們直接調用現有的Huffman編碼程序,將原圖像及量化後的預測誤差圖像進行壓縮編碼。

核心代碼

前面沒有詳細說明對於每一行的第一個像素的預測值取值的問題,對於該問題有兩種處理方法:

  • 每行第一個像素的預測值均取128;
  • 每一行第一個像素的預測誤差直接取0,並將該像素值賦給重建圖像對應像素。

這裏我們選擇以一種方法,並根據前面的原理可以寫出如下的代碼:

int w = 256;
int h = 256;

int PixelOverflow(int value, int thLower, int thUpper) {
	if (value < thLower) {
		return thLower;
	} else if (value > thUpper) {
		return thUpper;
	} else {
		return unsigned char(value);
	}
}

void DpcmEncoding(unsigned char* yBuff, unsigned char* qPredErrBuff, unsigned char* reconBuff, int qBits) {
	int prediction;
	int predErr;	// Prediction error
	int invPredErr;	// Inverse quantised value of quantised prediction error

	for (int i = 0; i < h; i++) {
		prediction = 128;	// The prediction of the first pixel of each row set to be 128
		predErr = yBuff[i * w] - prediction;	// predErr with the domain of [-128, 128] (8-bit)
		int temp = (predErr + 128) / pow(2, 8 - qBits);	// qBits-bit quantisation
														// (predErr + 128) with the domain of [0, 256]
		qPredErrBuff[i * w] = PixelOverflow(temp, 0, pow(2, qBits) - 1);
		invPredErr = qPredErrBuff[i * w] * pow(2, 8 - qBits) - 128;	// Inverse quantisation
		reconBuff[i * w] = PixelOverflow(invPredErr + prediction, 0, 255);	// Reconstruction level

		for (int j = 1; j < w; j++) {	// Strat from the second pixel of each row
			prediction = reconBuff[i * w + j - 1];	// The previous pixel value set as prediction
			predErr = yBuff[i * w + j] - prediction;	// predErr with the domain of [-255, 255] (9-bit)
			int temp = (predErr + 255) / pow(2, 9 - qBits);	// qBits-bit quantisation
															// (predErr + 255) with the domain of [0, 510]; [0, 2^(qBits) - 1] after division
			qPredErrBuff[i * w + j] = PixelOverflow(temp, 0, (pow(2, qBits) - 1));	// (predErr + 255) with the domain of [0, 255]
			invPredErr = qPredErrBuff[i * w + j] * pow(2, 9 - qBits) - 255;
			reconBuff[i * w + j] = PixelOverflow(invPredErr + prediction, 0, 255);	// Reconstruction level
		}
	}
}

這裏需要說明的是,由於量化後得到的是一組索引值,因而需要通過反量化恢復正確的灰度值範圍。

實驗結果分析

量化預測誤差圖像、重建圖像

以8 bit Lena灰度圖作爲測試圖像,進行DPCM + 8 bit量化時的原圖、預測誤差圖像和重建圖像如下:


原圖、預測誤差圖像、重建圖像(8 bit量化)

下面再對比一下7 bit—1 bit量化的效果:


7 bit—1 bit量化的重建圖像

在5 bit量化時,不仔細分辨不會看出較大差異;4 bit時,已經出現可察覺的失真;3 bit及以下出現較大的失真,圖像質量難以接受。

由於量化預測誤差範圍爲[0,2bits]\left[0,2^{{\rm bits}}\right],因而隨着量化比特數降低,預測誤差圖像中的人物邊緣將越來越不明顯,因此這裏就不再展示。

在這裏我們也可以發現一點,例如對於量化誤差進行1 bit量化,並不代表重建圖像只有0和255兩個亮度電平,這也是相比於直接對原圖像進行量化的優勢之一。

定量分析

計算PMF和熵

首先可以計算原圖和預測誤差的概率密度函數以及對應的信源熵。具體方法可參考求RGB圖像各分量的概率分佈和熵

可以看到,原圖像中存在較大的相關性,進行了DPCM編碼後,相關性得到了較好的去除,從而實現了壓縮。

計算PSNR

我們可以通過計算重建圖像的PSNR來定量計算圖像的壓縮質量,相應的代碼如下:

void PrintPSNR(unsigned char* oriBuffer, unsigned char* recBuffer, int qBits, const char* psnrFileName) {
	double mse;
	double sum = 0;
	double temp;
	double psnr;

	for (int i = 0; i < w * h; i++) {
		temp = pow((oriBuffer[i] - recBuffer[i]), 2);
		sum += temp;
	}
	mse = sum / (w * h);
	psnr = 10 * log10(255 * 255 / mse);

	/* Output the stats into a csv file */
	FILE* outFilePtr;
	if (fopen_s(&outFilePtr, psnrFileName, "ab") == 0) {
		cout << "Successfully opened \"" << psnrFileName << "\".\n";
	} else {
		cout << "WARNING!! Failed to open \"" << psnrFileName << "\".\n";
		exit(-1);
	}
	fprintf(outFilePtr, "%d,%lf\n", qBits, psnr);

	fclose(outFilePtr);
}

計算結果如下:

可以看出,從客觀評價(定量)角度對圖像質量的分析與前面從主觀評價角度對圖像質量的分析基本吻合。

計算壓縮比

最後我們可以將經過了Huffman編碼的原圖像,與進行了DPCM和Huffman兩種編碼的圖像(量化後的預測誤差圖像)進行對比,並計算壓縮比,結果如圖。

完整代碼

最後附上完整代碼,供大家參考,如有問題歡迎評論區交流、指正。

declarations.h

#pragma once

/* Global variables */
extern int w;	// Width of image
extern int h;	// Height of image

/* Functions */
void DpcmEncoding(unsigned char* yBuff, unsigned char* qPredErrBuff, unsigned char* reconBuff, int qBits);
void PrintPMF_Entropy(unsigned char* buffer, int qBits, const char* pmfFileName, const char* entrFileName);
void PrintPSNR(unsigned char* oriBuffer, unsigned char* recBuffer, int qBits, const char* psnrFileName);

DPCM.cpp

#include <iostream>
#include "declarations.h"

int w = 256;
int h = 256;

int PixelOverflow(int value, int thLower, int thUpper) {
	if (value < thLower) {
		return thLower;
	} else if (value > thUpper) {
		return thUpper;
	} else {
		return unsigned char(value);
	}
}
void DpcmEncoding(unsigned char* yBuff, unsigned char* qPredErrBuff, unsigned char* reconBuff, int qBits) {
	int prediction;
	int predErr;	// Prediction error
	int invPredErr;	// Inverse quantised value of quantised prediction error

	for (int i = 0; i < h; i++) {
		prediction = 128;	// The prediction of the first pixel of each row set to be 128
		predErr = yBuff[i * w] - prediction;	// predErr with the domain of [-128, 128] (8-bit)
		int temp = (predErr + 128) / pow(2, 8 - qBits);	// qBits-bit quantisation
														// (predErr + 128) with the domain of [0, 256]
		qPredErrBuff[i * w] = PixelOverflow(temp, 0, pow(2, qBits) - 1);
		invPredErr = qPredErrBuff[i * w] * pow(2, 8 - qBits) - 128;	// Inverse quantisation
		reconBuff[i * w] = PixelOverflow(invPredErr + prediction, 0, 255);	// Reconstruction level

		for (int j = 1; j < w; j++) {	// Strat from the second pixel of each row
			prediction = reconBuff[i * w + j - 1];	// The previous pixel value set as prediction
			predErr = yBuff[i * w + j] - prediction;	// predErr with the domain of [-255, 255] (9-bit)
			int temp = (predErr + 255) / pow(2, 9 - qBits);	// qBits-bit quantisation
															// (predErr + 255) with the domain of [0, 510]; [0, 2^(qBits) - 1] after division
			qPredErrBuff[i * w + j] = PixelOverflow(temp, 0, (pow(2, qBits) - 1));	// (predErr + 255) with the domain of [0, 255]
			invPredErr = qPredErrBuff[i * w + j] * pow(2, 9 - qBits) - 255;
			reconBuff[i * w + j] = PixelOverflow(invPredErr + prediction, 0, 255);	// Reconstruction level
		}
	}
}

Stats.cpp

#include <iostream>
#include "declarations.h"
using namespace std;

void PrintPMF_Entropy(unsigned char* buffer, int qBits, const char* pmfFileName, const char* entrFileName) {
	int count[256] = { 0 };	// Counter
	double freq[256] = { 0 };	// Frequency
	double entropy = 0;

	/* Compute the frequency of each greyscale */
	for (int i = 0; i < w * h; i++) {
		int index = (int)buffer[i];
		count[index]++;
	}

	/* Compute the PMF & entropy */
	for (int i = 0; i < 256; i++) {
		freq[i] = (double)count[i] / (w * h);
		if (freq[i] != 0) {
			entropy += (-freq[i]) * log(freq[i]) / log(2);
		}
	}

	/* Output the stats into a csv file */
	FILE* pmfFilePtr;
	FILE* entrFilePtr;
	if (fopen_s(&pmfFilePtr, pmfFileName, "wb") == 0) {
		cout << "Successfully opened \"" << pmfFileName << "\".\n";
	} else {
		cout << "WARNING!! Failed to open \"" << pmfFileName << "\".\n";
		exit(-1);
	}
	if (fopen_s(&entrFilePtr, entrFileName, "ab") == 0) {
		cout << "Successfully opened \"" << entrFileName << "\".\n";
	} else {
		cout << "WARNING!! Failed to open \"" << entrFileName << "\".\n";
		exit(-1);
	}


	fprintf(pmfFilePtr, "Symbol,Frequency\n");
	for (int i = 0; i < 256; i++) {
		fprintf(pmfFilePtr, "%-3d,%-8.2e\n", i, freq[i]);	// 將數據輸出到文件中(csv文件以“,”作爲分隔符)
	}
	fprintf(entrFilePtr, "%d,%.4lf\n", qBits, entropy);

	fclose(pmfFilePtr);
	fclose(entrFilePtr);
}

void PrintPSNR(unsigned char* oriBuffer, unsigned char* recBuffer, int qBits, const char* psnrFileName) {
	double mse;
	double sum = 0;
	double temp;
	double psnr;

	for (int i = 0; i < w * h; i++) {
		temp = pow((oriBuffer[i] - recBuffer[i]), 2);
		sum += temp;
	}
	mse = sum / (w * h);
	psnr = 10 * log10(255 * 255 / mse);

	/* Output the stats into a csv file */
	FILE* outFilePtr;
	if (fopen_s(&outFilePtr, psnrFileName, "ab") == 0) {
		cout << "Successfully opened \"" << psnrFileName << "\".\n";
	} else {
		cout << "WARNING!! Failed to open \"" << psnrFileName << "\".\n";
		exit(-1);
	}
	fprintf(outFilePtr, "%d,%lf\n", qBits, psnr);

	fclose(outFilePtr);
}

main.cpp

#include <iostream>
#include <string>
#include <sstream>
#include "declarations.h"
using namespace std;

int main(int argc, char* argv[]) {
    int qBits = 1;
    const char* orFileName = "Lena.yuv";
    const char* qpeFileName = "Lena_QPE (1 bit).yuv";   // Name of quantised prediction error file
    const char* recFileName = "Lena_reconstruction (1 bit).yuv";    // Name of reconstruction level file
    FILE* oriFilePtr;
    FILE* qpeFilePtr;
	FILE* recFilePtr;

	/* Open the files */
    if (fopen_s(&oriFilePtr, orFileName, "rb") == 0) {
        cout << "Successfully opened \"" << orFileName << "\".\n";
    } else {
        cout << "WARNING!! Failed to open \"" << orFileName << "\".\n";
        exit(-1);
    }
    if (fopen_s(&qpeFilePtr, qpeFileName, "wb") == 0) {
        cout << "Successfully opened \"" << qpeFileName << "\".\n";
    } else {
        cout << "WARNING!! Failed to open \"" << qpeFileName << "\".\n";
        exit(-1);
    }
    if (fopen_s(&recFilePtr, recFileName, "wb") == 0) {
        cout << "Successfully opened \"" << recFileName << "\".\n";
    } else {
        cout << "WARNING!! Failed to open \"" << recFileName << "\".\n";
        exit(-1);
    }

    /* Space allocation */
    unsigned char* oriYBuff = new unsigned char[w * h];
    unsigned char* qpeYBuff = new unsigned char[w * h];
    unsigned char* recYbuff = new unsigned char[w * h];
    unsigned char* uBuff = new unsigned char[w * h / 4];
    unsigned char* vBuff = new unsigned char[w * h / 4];

    /* Read greyscale data */
    fread(oriYBuff, sizeof(unsigned char), w * h, oriFilePtr);

    /* DPCM */
    DpcmEncoding(oriYBuff, qpeYBuff, recYbuff, qBits);
    memset(uBuff, 128, w * h / 4);
    memset(vBuff, 128, w * h / 4);
    fwrite(qpeYBuff, sizeof(unsigned char), w * h, qpeFilePtr);
    fwrite(uBuff, sizeof(unsigned char), w * h / 4, qpeFilePtr);    // Greyscale image
    fwrite(vBuff, sizeof(unsigned char), w * h / 4, qpeFilePtr);
    fwrite(recYbuff, sizeof(unsigned char), w * h, recFilePtr);
    fwrite(uBuff, sizeof(unsigned char), w * h / 4, recFilePtr);    // Greyscale image
    fwrite(vBuff, sizeof(unsigned char), w * h / 4, recFilePtr);

    /* Write stats into csv files */
    PrintPMF_Entropy(oriYBuff, qBits, "Lena-PMF (1 bit).csv", "Lena-entropy.csv");
    PrintPMF_Entropy(qpeYBuff, qBits, "Lena_QPE-PMF (1 bit).csv", "Lena_QPE-entropy.csv");
    PrintPSNR(oriYBuff, recYbuff, qBits, "Lena_reconstruction-PSNR.csv");

    fclose(oriFilePtr);
    fclose(qpeFilePtr);
    fclose(recFilePtr);
    delete[]oriYBuff;
    delete[]qpeYBuff;
    delete[]recYbuff;
    delete[]uBuff;
    delete[]vBuff;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章