动态规划在图像压缩中的应用
一、基本原理
图像压缩是一种降低图像文件大小的技术,目的是为了节省存储空间或加快传输速度。
图像表示
在计算机中,图像通常用一系列的像素点来表示,每个像素点有一个灰度值,范围通常是0到255,所以需要8位来表示一个像素点。
图像压缩的存储格式
图像压缩的目的是减少存储或传输图像所需的位数。这段内容描述的一种方法是将图像分割成若干个连续的像素段,每个段中的所有像素都用相同数量的位来表示。每个像素段还需要一些额外的信息来存储,比如用多少位来表示这个段中的像素,以及这个段有多少个像素等。
存储空间计算
对于每个像素段,需要存储的位数是这个段中像素数量乘以每个像素的位数,再加上一些额外的位数。整个图像所需的存储空间是所有像素段所需存储空间的总和。
动态规划求解
动态规划是一种解决问题的方法,它将问题分解成一系列相互关联的子问题,先解决子问题,然后用子问题的解来构造原问题的解。
在这个图像压缩问题中,定义
s
[
i
]
s[i]
s[i] 为前
i
i
i 个像素的最优分段所需的存储位数。最优分段意味着所需存储位数最少。这个问题满足最优子结构性质,即整个问题的最优解可以通过组合其子问题的最优解来获得。
具体来说,对于前 i i i 个像素的最优分段,可以通过考虑前 i − k i-k i−k 个像素的最优分段,再加上从第 i − k + 1 i-k+1 i−k+1 个像素到第 i i i 个像素的一个段来获得。通过遍历所有可能的 k k k 值,可以找到使得总存储位数最小的分段方式。
具体计算公式
s [ i ] = min k { s [ i − k ] + k ⋅ b max ( i − k + 1 , i ) + 11 } s[i] = \min_k \{ s[i-k] + k \cdot b_{\text{max}}(i-k+1, i) + 11 \} s[i]=kmin{s[i−k]+k⋅bmax(i−k+1,i)+11}
其中, b max ( i − k + 1 , i ) b_{\text{max}}(i-k+1, i) bmax(i−k+1,i)表示从第 i − k + 1 i-k+1 i−k+1 个像素到第 i i i个像素这个段中最大灰度值对应的位数,加上一些额外的位数来存储必要的信息。
通过这种方法,可以逐步计算出整个图像的最优分段,从而实现图像压缩。
二、主要实现代码(参考计算机算法设计与分析教材)
1、各个基本功能函数
-
int length(int i);
- 功能:该函数计算数字
i
在二进制表示下所需要的位数。 - 参数:一个整数
i
。 - 返回值:一个整数,表示
i
在二进制下所需要的位数。
- 功能:该函数计算数字
-
void Compress(int n, int p[], int s[], int l[], int b[]);
- 功能:该函数根据给定的图像灰度数组
p
,计算出图像的最优压缩方式。它会填充s
,l
, 和b
数组,其中s
表示每个像素位置的累计最小存储空间,l
表示每个像素位置的最优段长度,b
表示每个像素位置的最大位数。 - 参数:
n
:灰度数组p
的长度。p[]
:图像的灰度数组。s[]
:存储每个像素位置的累计最小存储空间。l[]
:存储每个像素位置的最优段长度。b[]
:存储每个像素位置的最大位数。
- 返回值:无。
- 功能:该函数根据给定的图像灰度数组
-
void Traceback(int n, int& i, int s[], int l[]);
- 功能:该函数通过回溯法找到图像的最优分段位置。
- 参数:
n
:需要回溯的最后一个像素位置。i
:引用参数,用于记录分段的数量。s[]
:存储每个像素位置的累计最小存储空间。l[]
:存储每个像素位置的最优段长度。
- 返回值:无。
-
void Output(int s[], int l[], int b[], int n);
- 功能:该函数输出图像的压缩结果,包括每个段的长度和所需存储位数。
- 参数:
s[]
:存储每个像素位置的累计最小存储空间。l[]
:存储每个像素位置的最优段长度。b[]
:存储每个像素位置的最大位数。n
:灰度数组的长度。
- 返回值:无。
2、具体代码实现
// 导入标准输入输出库
#include <iostream>
// 使用标准命名空间,这样我们就可以直接使用cout, cin等,而不需要写std::cout, std::cin。
using namespace std;
// 定义N为7,表示图像的灰度序列长度。
const int N = 7;
// 函数声明
int length(int i);
void Compress(int n,int p[],int s[],int l[],int b[]);
void Traceback(int n,int& i,int s[],int l[]);
void Output(int s[],int l[],int b[],int n);
int main()
{
// 定义图像的灰度数组p,下标从1开始计数。
int p[] = {0,10,12,15,255,1,2};
// 初始化存储空间数组s,分段长度数组l,每段所需存储位数数组b。
int s[N],l[N],b[N];
cout << "图像的灰度序列为:" << endl;
// 输出原始的灰度序列
for(int i=1; i<N; i++)
{
cout << p[i] << " ";
}
cout << endl;
// 调用压缩函数
Compress(N-1, p, s, l, b);
// 输出压缩后的结果
Output(s, l, b, N-1);
return 0;
}
void Compress(int n, int p[], int s[], int l[], int b[])
{
// 这里我们设置了两个常数:每段的最大像素数量为256,每段的头部信息需要11位来存储。
int Lmax = 256, header = 11;
// 我们设置s[0]为0,这表示开始时,没有任何像素,所以需要的存储空间为0。
s[0] = 0;
// 这个循环表示我们要查看每一个像素,从第一个开始。
for(int i=1; i<=n; i++)
{
// 对于每个像素,我们要计算它需要多少位来表示。这个数字存储在b数组中。
b[i] = length(p[i]);
// 为了方便计算,我们假设当前像素是最需要位数的像素。
int bmax = b[i];
// 初始假设:如果只有这一个像素作为一个段,那么所需的存储空间就是前面所有的加上这个像素的。
s[i] = s[i-1] + bmax;
// 并且我们先假设这个像素单独是一个段。
l[i] = 1;
// 然后我们要考虑把更多的像素加入这个段,看看是否能够更节省空间。
for(int j=2; j<=i && j<=Lmax; j++)
{
// 我们检查加入新像素后,是否有新的像素需要的位数更多。
if(bmax < b[i-j+1])
{
// 如果是,我们更新这个最大的位数。
bmax = b[i-j+1];
}
// 接下来我们要看,如果加入了j个像素作为一个段,是否能节省存储空间。
if(s[i] > s[i-j] + j*bmax)
{
// 如果能,我们就更新需要的存储空间和这个段的长度。
s[i] = s[i-j] + j*bmax;
l[i] = j;
}
}
// 最后,加上每个段开始时的那11位的头部信息。
s[i] += header;
}
}
// 计算数字i的二进制表示需要的位数
int length(int i)
{
// 这里,我们初始化k为1。因为任何非零数字至少需要1位来表示。
int k = 1;
// 我们将i除以2,因为我们想知道这个数字能够被多少次2整除。
// 这是因为每次除以2,我们其实就是在去掉这个数字二进制表示的最后一位。
i = i/2;
// 当i大于0时,我们继续执行循环体。
while(i > 0)
{
// 每次循环,我们都将k增加1,因为我们又去掉了i的一个二进制位。
k++;
// 继续除以2,为下一次循环做准备。
i = i/2;
}
// 最后,我们返回k,这是数字i需要的二进制位数。
return k;
}
// 回溯找到最优分段的具体位置
void Traceback(int n, int& i, int s[], int l[])
{
if(n == 0)
return;
Traceback(n-l[n], i, s, l);
// 存储分段位置
s[i++] = n-l[n];
}
void Output(int s[], int l[], int b[], int n)
{
// 输出最小的存储空间
cout << "图像压缩后的最小空间为:" << s[n] << endl;
int m = 0;
Traceback(n, m, s, l);
s[m] = n;
cout << "将原灰度序列分成" << m << "段序列段" << endl;
// 输出每段的长度和所需存储位数
for(int j=1; j<=m; j++)
{
l[j] = l[s[j]];
b[j] = b[s[j]];
}
for(int j=1; j<=m; j++)
{
cout << "段长度:" << l[j] << ",所需存储位数:" << b[j] << endl;
}
}