问题描述: 图片在计算机存储的是图片中的像素序列{p1,p2,p3......pn},也就是像素的灰度值(pi)。灰度值的范围是0~255。因此需要8位二进制表示一个像素。能否用更少的位数表示灰度值呢?即是图像压缩的问题。
问题分析:
可要求像素序列进行分段,使得最后所需要的位数最少(储存空间)。引入两个固定位数的值来表示:
1. 3位数表示当前段的每位像素的位数(000----表示像素位数为1);
2. 8位数表示当前段的像素个数。因为最多存储0~255个数字,一个灰度值最多2^3位(00000000----表示1个)
算法设计:
例子:{6, 5, 7,5, 245, 180, 28,28,19, 22, 25,20}一组灰度值序列。按默认需要12*8=96位
分为3段:第一段:最大为7,可用3位表示......
位数结果为:11*3(固定的11位数)+4*3+2*8+6*5=91<96,优化了!!
代码设计:
如果最优结果把像素序列分为m段(0<=m<=n)
DP法就需要一个数组记录每步最优值:S[i]记录{p1,p2,......,pi}的最优处理方式得到的最优解(存储位数);
假设每段由L[i]个像素点(0<=L[i]<=255):L[1]+L[2]+......+L[m]=n(所有段的像素点数量相加等于总的像素数量)--------8位表示
每段每个像素的存储位数肯定是一样的(否则这些像素不可能分配在同一段中),用b[i]来表示-----------3位表示
每段中有l[i]个像素点,每个像素点的灰度值用b[i]位存储
每段: l[i]*b[i](位存储每段的像素点的灰度值)+11 (每段至少需要11位)
递推公式:
s[i]表示前i个像素点{p1,p2,...,pi}所需要的总的存储位数
S[i]=min{ S[i-k]+k*bmax(i-k+1,i)+1},1<=k<=min{i,256}
#include<iostream>
using namespace std;
//求对数操作: 十进制0~1---1位表示;2~3---2位表示;4~7--3位表示;8~15--4位表示
int length(int i){
int k=1;
while(i>=2){
i=i/2;
k++;
}
return k;
}
/*
p[]:像素点的灰度序列,下标从1开始(p1,p2,....)
s[i]:前i个像素点的最优分段所需的储存位数,s[0]=0
*/
void compress(int n,int p[],int s[],int l[],int b[]){
int Lmax=256,header=11;
//限制每段最多有256(2^8)个像素点;每段至少需要储存的固定11位
s[0]=0;
for(int i=0;i<=n;i++)
{
int bmax;
b[i]=length(p[i]);
bmax=b[i];
s[i]=s[i-1]+bmax;
l[i]=1;//先初始化为1,若找到更好的分段再替换
//k代表从i开始往前数k个分成一块,从i开始往前数1,2,……分成一块,并求出其最小值
for(int k=2;k<=i && k<=Lmax;k++)
{
if(bmax<b[i-k+1]) bmax=b[i-k+1];
if(s[i]>=s[i-k]+k*bmax)
{
s[i]=s[i-k]+k*bmax;
l[i]=k;//记录划分的位置
}
}
s[i]+=header;
}
}
void Traceback(int n,int& i,int s[],int l[]){
if(n==0) return ;
Traceback(n-l[n],i,s,l); //最后一段有l[n]个像素,依次向前类推。
s[i++]=n-l[n];//重新为数组s赋值,存储分段位置,最终m为共分了多少段
}
void Output(int s[],int l[],int b[],int n){
//输出s[n]存储位数后,s[]被重新赋值,用来存储分段位置。
cout<<"图像压缩后的最小空间为:"<<s[n]<<endl;
int m=0;
Traceback(n,m,s,l);
s[m]=n; //m为总段数,设s[m]=n,分割位置为第n个像素。
cout<<"将原灰度序列分为"<<m<<"段序列"<<endl;
for(int i=1;i<=m;i++)
{
l[i]=l[s[i]];
b[i]=b[s[i]];
}
for(int i=1;i<=m;i++)
{
cout<<"段长度:"<<l[i]<<",所需存储位数:"<<b[i]<<endl;
}
}
int main()
{
int p[]={6, 5, 7,5, 245, 180, 28,28,19, 22, 25,20};
int N=13;//数组长度,因为下标从1开始
int s[N],l[N],b[N];
cout<<"图像序列灰度值为:"<<endl;
for(int i=1;i<N;i++)
printf("%d ",p[i]);
cout<<endl;
compress(N-1,p,s,l,b);
Output(s,l,b,N-1);
return 0;
}
上述的Output、Traceback函数运用递归进行,比较难理解。
下面是老师讲解比较容易理解的方法
int Maxb(int b[],int i,int j )//找该段最大的位数
{
int bmax=0;
bmax=b[i];
for(int k=i+1;k<=j;k++)
{
if(bmax<b[k])
bmax=b[k];
}
return bmax;
}
void Output(int s[],int l[],int b[],int n)
{
cout<<"图像压缩后的最小空间为:"<<s[n]<<endl;
int m = 0,t;
t=n;//序列长度
while(t)
{
m++;
t=t-l[t];
}//m:记录共分为几段
cout<<"将原灰度序列分成"<<m <<"段序列段"<<endl;
int *length=new int[m+1];//存储每段的长度值
int *bmax=new int[m+1];//存储每段的灰度值占用的位数
t=n;
for(int j=m; j>=1; j--)
{
length[j] = l[t];//第j段的灰度值长度
bmax[j]= Maxb(b,(t-l[t]+1),t);//修改后才是正确的!!!
t=t-l[t];
}
for(int j=1; j<=m; j++)
{
cout<<"段长度:"<<length[j]<<",所需存储位数:"<<bmax[j]<<endl;
}
}
完整代码展示
//动态规划 图像压缩问题
#include <iostream>
#include<stdlib.h>
using namespace std;
const int N = 7;
int length(int i);
void Compress(int n,int p[],int s[],int l[],int b[]);
void Tracebace(int n,int& i,int s[],int l[]);
void Output(int s[],int l[],int b[],int n);//非教材版本的输出
int main()
{
// int p[] = {0,10,12,15,255,2,1};//图像灰度数组 下标从1开始计数
// int p[] = {0,10,12,15,255,1,2};//图像灰度数组 下标从1开始计数
// int p[] = {0,230,222,1,255,121,2};//图像灰度数组 下标从1开始计数
// int p[] = {0,116,2,1,81,88,0};//图像灰度数组 下标从1开始计数
int p[] = {0,230,222,15,255,1,2};//图像灰度数组 下标从1开始计数
int s[N],l[N],b[N];
/*
s[]
l[]
b[]
*/
cout<<"图像的灰度序列为:"<<endl;
for(int i=1;i<N;i++)
{
cout<<p[i]<<" ";
}
cout<<endl;
Compress(N-1,p,s,l,b);
//************************************************
cout<<"b[]:"<<endl;
for(int i=1;i<=N-1;i++)
{
cout<<b[i]<<" ";
}
cout<<endl;
for(int i=1;i<=N-1;i++)
{
cout<<"s["<<i<<"]="<<s[i]<<" "<<"l["<<i<<"]="<<l[i]<<endl;
}
//************************************************
Output(s,l,b,N-1);
system("pause");
return 0;
}
void Compress(int n,int p[],int s[],int l[],int b[])
{//s[i]:1<=i<=n,是像素序列{p1,...,pi}的最优分段所需存储位数。
// l[i]:1<=i<=n,是像素序列{p1,...,pi}的最优分段的最后一段的长度。
//b[i]:一段每个像素的表示
//递归公式:s[i]=min(1<=k<=min(i,256)) (s[i-k]+k*bmax(i-k+1,i)+11.(k:表示像素序列{p1,...,pi}的最优分段的最后一段的长度)
int Lmax = 256,header = 11,bmax;//*************************
s[0] = 0; //初始值
for(int i=1; i<=n; i++)//按照s[1] -> s[2] ->....s[n]的顺序求解,s[n]即最终的最优解。
{
b[i] = length(p[i]);//计算像素点p需要的存储位数
bmax = b[i];
s[i] = s[i-1] + bmax;//当k=1的情况下的值
l[i] = 1;
for(int k=2; k<=i && k<=Lmax;k++) //求k取2--->min(i,256)时是否有更小的值,有更小的则更新s[i]。
{
if(bmax<b[i-k+1])
{
bmax = b[i-k+1];
}
if(s[i]>s[i-k]+k*bmax)
{
s[i] = s[i-k] + k*bmax;
l[i] = k;
}
}
s[i] += header;
}
}
int length(int i)
{//返回灰度值i需要的存储位数 。(例如:灰度值是0--1则返回1,灰度值是2--3则返回2,灰度值是4--7则返回3,... 灰度值是128--255则返回8)
int k=1;
i = i/2;
while(i>0)
{
k++;
i=i/2;
}
return k;
}
//********************增加这个函数后才是正确的!**********************************
int Maxb(int b[],int i,int j )//找该段最大的位数
{
int bmax=0;
bmax=b[i];
for(int k=i+1;k<=j;k++)
{
if(bmax<b[k])
bmax=b[k];
}
return bmax;
}
void Output(int s[],int l[],int b[],int n)
{
cout<<"图像压缩后的最小空间为:"<<s[n]<<endl;
int m = 0,t;
t=n;//序列长度
while(t)
{
m++;
t=t-l[t];
}//m:记录共分为几段
cout<<"将原灰度序列分成"<<m <<"段序列段"<<endl;
int *length=new int[m+1];//存储每段的长度值
int *bmax=new int[m+1];//存储每段的灰度值占用的位数
t=n;
for(int j=m; j>=1; j--)
{
length[j] = l[t];//第j段的灰度值长度
bmax[j]= Maxb(b,(t-l[t]+1),t);//修改后才是正确的!!!
t=t-l[t];
}
for(int j=1; j<=m; j++)
{
cout<<"段长度:"<<length[j]<<",所需存储位数:"<<bmax[j]<<endl;
}
}