CSP-202212-3-JPEG 解码
关键点:Z字扫描
在JPEG压缩中,Z字形扫描是一种将8x8块的数据按照Z字形(或之字形)顺序重新排列的过程。这样做的目的是为了将相似的数据(尤其是零值)放置在一起,从而提高后续压缩步骤的效率。
1.方法一:任意大小矩阵的Z字扫描
zScan
函数实现了Z字形扫描的过程。它按照特定的顺序遍历8x8块,这个顺序开始于左上角,然后沿着块的对角线向上移动,达到边界后转向并沿着另一条对角线向下移动,依此类推。在每一步中,该函数检查是否还有待填充的系数(来自数组 m
),如果有,则将当前系数填入对应的位置;如果没有更多的系数,则在当前位置填充零。该过程的关键在于管理遍历的方向和处理边界情况——例如,当扫描达到矩阵的上边界或右边界时,需要正确改变方向。
void zScan(vector<vector<int>>& M, vector<int>& m) {
int size = 8; // 矩阵大小为8x8
vector<int> result;
int i = 0, j = 0, mi = 0;
bool up = true; // 用于标识当前移动方向,初始为向上
for (int k = 0; k < size * size; ++k) {
if (mi < m.size())
{
M[i][j] = m[mi];
mi++;
}
else M[i][j] = 0;
if (up) { // 如果当前方向是向上
if (j == size - 1) { // 如果触及右边界
++i; // 向下移动
up = false; // 改变方向
}
else if (i == 0) { // 如果触及上边界
++j; // 向右移动
up = false; // 改变方向
}
else { // 否则,继续向上和向右移动
--i;
++j;
}
}
else { // 如果当前方向是向下
if (i == size - 1) { // 如果触及底边界
++j; // 向右移动
up = true; // 改变方向
}
else if (j == 0) { // 如果触及左边界
++i; // 向下移动
up = true; // 改变方向
}
else { // 否则,继续向下和向左移动
++i;
--j;
}
}
}
}
2.方法二:固定大小矩阵下标驱动的Z字形扫描
另一种实现Z字形扫描的方法是使用一个预定义的索引矩阵(如你所示的 idx
矩阵)。这个矩阵直接定义了从Z字形位置到矩阵线性位置的映射。在这种方法中,你不需要通过逐步遍历来确定每个元素的Z字形顺序,而是直接使用索引矩阵来查找每个元素在Z字形顺序中的位置。
int idx[8][8] = {
{0, 1, 5, 6, 14, 15, 27, 28},
{2, 4, 7, 13, 16, 26, 29, 42},
{3, 8, 12, 17, 25, 30, 41, 43},
{9, 11, 18, 24, 31, 40, 44, 53},
{10, 19, 23, 32, 39, 45, 52, 54},
{20, 22, 33, 38, 46, 51, 55, 60},
{21, 34, 37, 47, 50, 56, 59, 61},
{35, 36, 48, 49, 57, 58, 62, 63}
};
void zScanByIndex(vector<vector<int>>& M, vector<int>& m) {
for (int i = 0; i < 8; i++) { // 填充M矩阵,使用idx矩阵中的值作为m向量的索引
for (int j = 0; j < 8; j++) {
if (idx[i][j] < m.size()) { // 检查idx[i][j]是否在m向量的大小范围内
M[i][j] = m[idx[i][j]];
} else {
M[i][j] = 0; // 如果索引超出了m的大小,就将M的当前元素设置为0
}
}
}
}
解题思路
这段代码回答了JPEG压缩的关键步骤:Z字形扫描、量化和离散余弦变换。这个过程有助于理解JPEG压缩如何减少图像数据的大小,同时保留了相对较高的图像质量。代码中每一步都对应JPEG压缩的一部分。
1.初始化和数据输入
代码开始时,创建了几个二维数组来存储量化矩阵 Q
、初始块 M
、变换后的块 M1
,以及其他变量 n
和 T
:
vector<vector<int>>Q(8, vector<int>(8))
: 8x8的量化矩阵。vector<vector<int>>M(8, vector<int>(8))
: 存储初始的8x8块,或是在Z字扫描后存储系数。vector<vector<double>>M1(8, vector<double>(8))
: 存储离散余弦变换(DCT)后的数据。
main
函数中读取这些值,其中 Q
是量化矩阵,n
是Z字形扫描输入系数的数量,T
是用户选择的压缩步骤标识。
2.Z字形扫描
zScan
函数将一维数组 m
(这是按Z字形顺序排列的系数)填入8x8块 M
中。这模仿了JPEG编码过程中将量化后的DCT系数按Z字形排列的过程,这一步骤有助于后续的编码效率,因为它通常会将许多零值放置在块的末尾。
3.量化
multiply
函数通过将8x8的DCT系数块 M
与量化矩阵 Q
逐元素相乘,来实现量化过程。在实际JPEG压缩中,这会降低图像的质量,但同时大大减少所需的数据量。
4.离散余弦变换(DCT)
discreteCos
函数计算了DCT的逆过程,将量化后的系数转换回像素值。这里使用的是二维DCT公式,通常用于JPEG的解码过程。函数计算了8x8块中每个位置的值,根据DCT的公式,使用cosine函数根据频率变化的系数。
5.最终阶段和显示结果
在main
函数的最后部分,根据用户选择的处理阶段(通过变量 T
控制),程序可以仅执行Z字形扫描、执行扫描和量化或执行全部步骤(包括DCT)。最终,display
函数用于输出处理后的8x8块,显示压缩过程中的中间或最终结果。
完整代码
#include <iostream>
#include <vector>
#include <cmath>
#include <algorithm>
using namespace std;
vector<vector<int>>Q(8, vector<int>(8));
vector<vector<int>>M(8, vector<int>(8));
vector<vector<double>>M1(8, vector<double>(8));
int n, T;
// Z字扫描如果大小是固定的,可以直接存下标矩阵
void zScan(vector<vector<int>>& M, vector<int>& m) {
int size = 8; // 矩阵大小为8x8
vector<int> result;
int i = 0, j = 0, mi = 0;
bool up = true; // 用于标识当前移动方向,初始为向上
for (int k = 0; k < size * size; ++k) {
if (mi < m.size())
{
M[i][j] = m[mi];
mi++;
}
else M[i][j] = 0;
if (up) { // 如果当前方向是向上
if (j == size - 1) { // 如果触及右边界
++i; // 向下移动
up = false; // 改变方向
}
else if (i == 0) { // 如果触及上边界
++j; // 向右移动
up = false; // 改变方向
}
else { // 否则,继续向上和向右移动
--i;
++j;
}
}
else { // 如果当前方向是向下
if (i == size - 1) { // 如果触及底边界
++j; // 向右移动
up = true; // 改变方向
}
else if (j == 0) { // 如果触及左边界
++i; // 向下移动
up = true; // 改变方向
}
else { // 否则,继续向下和向左移动
++i;
--j;
}
}
}
}
void multiply(vector<vector<int>>& M, vector<vector<int>>& Q) {
for (size_t i = 0; i < 8; i++)
{
for (size_t j = 0; j < 8; j++)
{
M[i][j] *= Q[i][j];
}
}
}
double pai = acos(-1);
double alpha(int u) {
if (u == 0)return sqrt(0.5);
return 1;
}
double discreteCos(vector<vector<int>>& M, int i, int j) {
double MM = 0;
for (size_t u = 0; u < 8.0; u++) {
for (size_t v = 0; v < 8.0; v++) {
MM += alpha(u) * alpha(v) * M[u][v] * cos((pai / 8.0) * (i + 0.5) * u) * cos((pai / 8.0) * (j + 0.5) * v);
}
}
MM = MM / 4;
return MM;
}
void display(vector<vector<int>>& M) {
for (auto& it : M) {
for (auto& jt : it) {
cout << jt << " ";
}
cout << endl;
}
}
int main() {
for (auto& it : Q) {
for (auto& jt : it) {
cin >> jt;
}
}
cin >> n >> T;
vector<int>m(n);
for (size_t i = 0; i < n; i++)
{
cin >> m[i];
}
zScan(M, m);
if (T == 0)
{
display(M);
return 0;
}
multiply(M, Q);
if (T == 1)
{
display(M);
return 0;
}
for (size_t i = 0; i < M.size(); i++)
{
for (size_t j = 0; j < M[i].size(); j++)
{
M1[i][j] = discreteCos(M, i, j);
}
}
for (size_t i = 0; i < M.size(); i++)
{
for (size_t j = 0; j < M[i].size(); j++)
{
M[i][j] = round(M1[i][j] + 128);
if (M[i][j] > 255) M[i][j] = 255;
if (M[i][j] < 0) M[i][j] = 0;
}
}
display(M);
return 0;
}