图象的半影调和抖动技术

来源:互联网 发布:ace editor python 编辑:程序博客网 时间:2024/04/29 10:33

在介绍本章内容之前,先提出一个问题?普通的黑白针式打印机能打出灰度图来吗?如果说能,从针式打印机的打印原理来分析,似乎是不可能的。因为针打是靠撞针击打色带在纸上形成黑点的,不可能打出灰色的点来;如果说不能,可是我们的确见过用针式打印机打印出来的灰色图象。到底是怎么回事呢?

你再仔细看看那些打印出来的所谓的灰色图象,最好用放大镜看。你会发现,原来这些灰色图象都是由一些黑点组成的,黑点多一些,图象就暗一些;黑点少一些,图案就亮一些。下面这几张图就很能说明这一点。

4.1用黑白两种颜色打印出灰度效果

4.1中最左边的是原图,是一幅真正的灰度图,另外三张图都是黑白二值图。容易看出,最左的那幅和原图最接近。

由二值图象显示出灰度效果的方法,就是我们今天要讲的半影调(halftone)技术,它的一个主要用途就是在只有二值输出的打印机上打印图象。我们介绍两种方法:图案法和抖动法。

4.1 图案法

图案法(patterning)是指灰度可以用一定比例的黑白点组成的区域表示,从而达到整体图象的灰度感。黑白点的位置选择称为图案化。

在具体介绍图案法之前,先介绍一下分辨率的概念。计算机显示器,打印机,扫描仪等设备的一个重要指标就是分辨率,单位是dpi(dot per inch),即每英寸点数,点数越多,分辨率就越高,图象就越清晰。让我们来计算一下,计算机显示器的分辨率有多高。设显示器为15英寸(指对角线长度),最多显示1280×1024个点。因为宽高比为43,所以宽有12英寸,高有9英寸,则该显示器的水平分辨率为106dpi,垂直分辨率为113.8dpi。一般的激光打印机的分辨率有300dpi×300dpi600dpi×600dpi720dpi×720dpi。所以打出来的图象要比计算机显示出来的清晰的多。扫描仪的分辨率要高一些,数码相机的分辨率更高。

言归正传,前面讲了,图案化使用图案来表示象素的灰度,那么我们来做一道计算题。假设有一幅240×180×8bit的灰度图,当用分辨率为300dpi×300dpi的激光打印机将其打印到12.8×9.6英寸的纸上时,每个象素的图案有多大?

这道题很简单,这张纸最多可以打(300×12.8)×(300×9.6)=3840×2880个点,所以每个象素可以用(3840/240)×(2880/180)=16×16个点大小的图案来表示,即一个象素256个点。如果这16×16的方块中一个黑点也没有,就可以表示灰度256;有一个黑点,就表示灰度255;依次类推,当都是黑点时,表示灰度0。这样,16×16的方块可以表示257级灰度,比要求的8bit256级灰度还多了一个。所以上面的那幅图的灰度级别完全能够打印出来。

这里有一个图案构成的问题,即黑点打在哪里?比如说,只有一个黑点时,我们可以打在正中央,也可以打16×16的左上角。图案可以是规则的,也可以是不规则的。一般情况下,有规则的图案比随即图案能够避免点的丛集,但有时会导致图象中有明显的线条。

如图4.1中,2×2的图案可以表示5级灰度,当图象中有一片灰度为的1的区域时,如图4.2所示,有明显的水平和垂直线条。

4.2                 2×2的图案

4.3                 规则图案导致线条

如果想存储256级灰度的图案,就需要256×16×16的二值点阵,占用的空间还是相当可观的。有一个更好的办法是:只存储一个整数矩阵,称为标准图案,其中的每个值从0255。图象的实际灰度和阵列中的每个值比较,当该值大于等于灰度时,对应点打一黑点。下面举一个25级灰度的例子加以说明。

4.4    标准图案举例

4.4中,左边为标准图案,右边为灰度为15的图案,共有10个黑点,15个白点。其实道理很简单,灰度为0时全是黑点,灰度每增加1,减少一个黑点。要注意的是,5×5的图案可以表示26种灰度,当灰度是25才是全白点,而不是灰度为24时。

下面介绍一种设计标准图案的算法,是由Limb1969年提出的。

先以一个2×2的矩阵开始:设M1=    ,通过递归关系有Mn+1=,其中MnUn均为2n×2n的方阵,Un的所有元素都是1。根据这个算法,可以得到M2=    ,为16级灰度的标准图案。

M3(8×8)比较特殊,称为Bayer抖动表。M4是一个16×16的矩阵。

根据上面的算法,如果利用M3一个象素要用8×8的图案表示,则一幅N×N的图将变成8N×8N大小。如果利用M4,就更不得了,变成16N×16N了。能不能在保持原图大小的情况下利用图案化技术呢?一种很自然的想法是:如果用M2阵,则将原图中每8×8个点中取一点,即重新采样,然后再应用图案化技术,就能够保持原图大小。实际上,这种方法并不可行。首先,你不知道这8×8个点中找哪一点比较合适,另外,8×8的间隔实在太大了,生成的图象和原图肯定相差很大,就象图4.1最右边的那幅图一样。

我们可以采用这样的做法:假设原图是256级灰度,利用Bayer抖动表,做如下处理

if (g[y][x]>>2) > bayer[y&7][x&7] then打一白点 else 打一黑点

其中,x,y代表原图的象素坐标,g[y][x]代表该点灰度。首先将灰度右移两位,变成64级,然后将xy做模8运算,找到Bayer表中的对应点,两者做比较,根据上面给出的判据做处理。

我们可以看到,模8运算使得原图分成了一个个8×8的小块,每个小块和8×8Bayer表相对应。小块中的每个点都参与了比较,这样就避免了上面提到的选点和块划分过大的问题。模8运算实质上是引入了随机成分,这就是我们下面要讲到的抖动技术。

4.5就是利用了这个算法,使用M3(Bayer抖动表)阵得到的;图6是使用M4阵得到的,可见两者的差别并不是很大,所以一般用Bayer表就可以了。

4.5    利用M3抖动生成的图

4.6    利用M4抖动生成的图

下面是算法的源程序,是针对Bayer表的。因为它是个常用的表,我们不再利用Limb公式,而是直接给出。针对M4阵的算法是类似的,不同的地方在于,要用Limb公式得到M4阵,灰度也不用右移2位。要注意的是,为了处理的方便,我们的结果图仍采用256级灰度图,不过只用到了0255两种灰度。

BYTE BayerPattern[8][8]={  0,32,8,40,2,34,10,42,

48,16,56,24,50,18,58,26,

                                                          12,44,4,36,14,46,6,38,

                                                          60,28,52,20,62,30,54,22,

                                                          3,35,11,43,1,33,9,41,

                                                          51,19,59,27,49,17,57,25,

                                                          15,47,7,39,13,45,5,37,

                                                          63,31,55,23,61,29,53,21};

BOOL LimbPatternM3(HWND hWnd)

{

DWORD                                       OffBits,BufSize

LPBITMAPINFOHEADER   lpImgData;

LPSTR                            lpPtr;

HLOCAL                           hTempImgData;

LPBITMAPINFOHEADER   lpTempImgData;

LPSTR                            lpTempPtr;

HDC                              hDc;

HFILE                             hf;

LONG                             x,y;

unsigned char                       num;

OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);

BufSize=OffBits+bi.biHeight*LineBytes;//要开的缓冲区大小

if((hTempImgData=LocalAlloc(LHND,BufSize))==NULL)

{

MessageBox(hWnd,"Error alloc memory!","Error     Message",MB_OK|

MB_ICONEXCLAMATION);

    return FALSE;

}

lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);      

lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);

//拷贝头信息和位图数据

memcpy(lpTempImgData,lpImgData,BufSize);

for(y=0;y<bi.biHeight;y++){

      //lpPtr为指向原图位图数据的指针

      lpPtr=(char     *)lpImgData+(BufSize-LineBytes-y*LineBytes);

      //lpTempPtr为指向新图位图数据的指针

      lpTempPtr=(char     *)lpTempImgData+(BufSize-LineBytes-y*LineBytes);

      for(x=0;x<bi.biWidth;x++){

                  num=(unsigned char)*lpPtr++;

                  if ( (num>>2) > BayerPattern[y&7][x&7]) //右移两位后做比较

                         *(lpTempPtr++)=(unsigned char)255;     //打白点

                  else *(lpTempPtr++)=(unsigned char)0; //打黑点

      }

}

if(hBitmap!=NULL)

    DeleteObject(hBitmap);

hDc=GetDC(hWnd);   

//形成新的位图

hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,

(LONG)CBM_INIT,

(LPSTR)lpTempImgData+

sizeof(BITMAPINFOHEADER)+

NumColors*sizeof(RGBQUAD),

(LPBITMAPINFO)lpTempImgData,

DIB_RGB_COLORS);

hf=_lcreat("c:\\limbm3.bmp",0);

_lwrite(hf,(LPSTR)&bf,sizeof(BITMAPFILEHEADER));

_lwrite(hf,(LPSTR)lpTempImgData,BufSize);

_lclose(hf);

//释放内存和资源

ReleaseDC(hWnd,hDc);

LocalUnlock(hTempImgData);

LocalFree(hTempImgData);

GlobalUnlock(hImgData);

return TRUE;

}

4.2 抖动法

让我们考虑更坏的情况:即使使用了图案化技术,仍然得不到要求的灰度级别。举例说明:假设有一幅600×450×8bit的灰度图,当用分辨率为300dpi×300dpi的激光打印机将其打印到8×6英寸的纸上时,每个象素可以用(2400/600)×(1800/450)=4×4个点大小的图案来表示,最多能表示17级灰度,无法满足256级灰度的要求。可有两种解决方案:(1)减小图象尺寸,由600×450变为150×113(2)降低图象灰度级,由256级变成16级。这两种方案都不理想。这时,我们就可以采用“抖动法”(dithering)的技术来解决这个问题。其实刚才给出的算法就是一种抖动算法,称为规则抖动(regular     dithering)。规则抖动的优点是算法简单;缺点是图案化有时很明显,这是因为取模运算虽然引入了随机成分,但还是有规律的。另外,点之间进行比较时,只要比标准图案上点的值大就打白点,这种做法并不理想,因为,如果当标准图案点的灰度值本身就很小,而图象中点的灰度只比它大一点儿时,图象中的点更接近黑色,而不是白色。一种更好的方法是将这个误差传播到邻近的象素。

下面介绍的Floyd-Steinberg算法就采用了这种方案。

假设灰度级别的范围从b(black)w(white),中间值t(b+w)/2,对应256级灰度,b=0,w=255,t=127.5。设原图中象素的灰度为g,误差值为e,则新图中对应象素的值用如下的方法得到:

if g > t then

打白点

e=g-w

else

打黑点

e=g-b

3/8 × e加到右边的象素

3/8 × e加到下边的象素

1/4 × e加到右下方的象素

算法的意思很明白:以256级灰度为例,假设一个点的灰度为130,在灰度图中应该是一个灰点。由于一般图象中灰度是连续变化的,相邻象素的灰度值很可能与本象素非常接近,所以该点及周围应该是一片灰色区域。在新图中,130大于128,所以打了白点,但130离真正的白点255还差的比较远,误差e=130-255=-125比较大。,将3/8×(-125)加到相邻象素后,使得相邻象素的值接近0而打黑点。下一次,e又变成正的,使得相邻象素的相邻象素打白点,这样一白一黑一白,表现出来刚好就是灰色。如果不传递误差,就是一片白色了。再举个例子,如果一个点的灰度为250,在灰度图中应该是一个白点,该点及周围应该是一片白色区域。在新图中,虽然e=-5也是负的,但其值很小,对相邻象素的影响不大,所以还是能够打出一片白色区域来。这样就验证了算法的正确性。其它的情况你可以自己推敲。图4.7是利用Floyd-Steinberg算法抖动生成的图。

4.7    利用Floyd-Steinberg算法抖动生成的图

下面我们给出Floyd-Steinberg算法的源代码。有一点要说明,我们原来介绍的程序都是先开一个char类型的缓冲区,用来存储新图数据,但在这个算法中,因为e有可能是负数,为了防止得到的值超出char能表示的范围,我们使用了一个int类型的缓冲区存储新值。另外,当按从左到右,从上到下的顺序处理象素时,处理过的象素以后不会再用到了,所以用这个int类型的缓冲区存储新值是可行的。全部象素处理完后,再将这些值拷贝到char类型的缓冲区去。

BOOL Steinberg(HWND hWnd)

{

DWORD                                       OffBits,BufSize,IntBufSize;

LPBITMAPINFOHEADER lpImgData;

HLOCAL                                       hTempImgData;

LPBITMAPINFOHEADER lpTempImgData;

LPSTR                     lpPtr;

LPSTR                     lpTempPtr;

HDC                       hDc;

HFILE                               hf;

LONG                               x,y;

unsigned char                          num;

float                                 e,f;

HLOCAL                                  hIntBuf;

int                                   *lpIntBuf,*lpIntPtr;

int                                   tempnum;

//OffBitsBITMAPINFOHEADER结构长度加调色板的大小

OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);

BufSize=OffBits+bi.biHeight*LineBytes;//要开的缓冲区的大小

if((hTempImgData=LocalAlloc(LHND,BufSize))==NULL)

{

MessageBox(hWnd,"Error alloc memory!","Error Message",MB_OK|

MB_ICONEXCLAMATION);

return FALSE;

}

IntBufSize=(DWORD)bi.biHeight*LineBytes*sizeof(int); if((hIntBuf=LocalAlloc(LHND,IntBufSize))==NULL)     //int类型的缓冲区

{

MessageBox(hWnd,"Error alloc memory!","Error     Message",MB_OK|

MB_ICONEXCLAMATION);

LocalFree(hTempImgData);

return FALSE;

}

lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);

lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);

lpIntBuf=(int *)LocalLock(hIntBuf);

//拷贝头信息

memcpy(lpTempImgData,lpImgData,OffBits);

//将图象数据拷贝到int类型的缓冲区中

for(y=0;y<bi.biHeight;y++){

lpPtr=(char *)lpImgData+(BufSize-LineBytes-y*LineBytes);

lpIntPtr=(int *)lpIntBuf+(bi.biHeight-1-y)*LineBytes;

for(x=0;x<bi.biWidth;x++)

                  *(lpIntPtr++)=(unsigned char)*(lpPtr++);

}

for(y=0;y<bi.biHeight;y++){

for(x=0;x<bi.biWidth;x++){

lpIntPtr=(int *)lpIntBuf+(bi.biHeight-1-y)*LineBytes+x;

                  num=(unsigned char)*lpIntPtr;

                  if ( num > 128 ){ //128是中值

                         *lpIntPtr=255; //打白点

                         e=(float)(num-255.0); //计算误差

                  }

       else{

                         *lpIntPtr=0; //打黑点

                         e=(float)num; //计算误差

                  }

                  if(x<bi.biWidth-1){ //注意判断边界

                         f=(float)*(lpIntPtr+1);

                         f+=(float)( (3.0/8.0) * e);

                  *(lpIntPtr+1)=(int)f; //向左传播

}

       if(y<bi.biHeight-1){     //注意判断边界

                         f=(float)*(lpIntPtr-LineBytes);

                         f+=(float)( (3.0/8.0) * e);

                         *(lpIntPtr-LineBytes)=(int)f; //向下传播

                         f=(float)*(lpIntPtr-LineBytes+1);

                         f+=(float)( (1.0/4.0) * e);

                         *(lpIntPtr-LineBytes+1)=(int)f; //向右下传播

                  }

      }

}

//int类型的缓冲区拷贝到char类型的缓冲区

for(y=0;y<bi.biHeight;y++){

lpTempPtr=(char *)lpTempImgData+(BufSize-LineBytes-y*LineBytes);

lpIntPtr=(int *)lpIntBuf+(bi.biHeight-1-y)*LineBytes;

for(x=0;x<bi.biWidth;x++){

                  tempnum=*(lpIntPtr++);

                  if(tempnum>255) tempnum=255;

                  else if (tempnum<0) tempnum=0;

                  *(lpTempPtr++)=(unsigned char)tempnum;

      }

}

if(hBitmap!=NULL)

DeleteObject(hBitmap);

hDc=GetDC(hWnd);

//产生新的位图

hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,

(LONG)CBM_INIT,

(LPSTR)lpTempImgData+

sizeof(BITMAPINFOHEADER)+

NumColors*sizeof(RGBQUAD),

(LPBITMAPINFO)lpTempImgData,

DIB_RGB_COLORS);

hf=_lcreat("c:\\steinberg.bmp",0);

_lwrite(hf,(LPSTR)&bf,sizeof(BITMAPFILEHEADER));

_lwrite(hf,(LPSTR)lpTempImgData,BufSize);

_lclose(hf);

//释放内存和资源

ReleaseDC(hWnd,hDc);

GlobalUnlock(hImgData);

LocalUnlock(hTempImgData);

LocalFree(hTempImgData);

LocalUnlock(hIntBuf);

LocalFree(hIntBuf);

return TRUE;

}

要注意的是,误差传播有时会引起流水效应,即误差不断向下,向右累加传播。解决的办法是:奇数行从左到右传播,偶数行从右到左传播。

4.3 bmp文件转换为txt文件

在讲图案化技术时,我突然想到了一个非常有趣的应用,那就是bmp2txt。如果你喜欢上BBS(电子公告牌系统),你可能想做一个花哨的签名档。瞧,这是我好朋友Casper的签名档(见图4.8),胖乎乎的,是不是特别可爱?

4.8     Casper的签名档

你仔细观察一下,就会发现,这是一幅全部由字符组成的图,因为在BBS中只能出现文本的东西。那么,这幅图是怎么做出来的呢?难道是自己一个字符一个字符拼出来的。当然不是了,有一种叫bmp2txt的应用程序(2的发音和“to”一样,所以如此命名),能把位图文件转换成和图案很相似的字符文本。是不是觉得很神奇?其实原理很简单,用到了和图案化技术类似的思想:首先将位图分成同样大小的小块,求出每一块灰度的平均值,然后和每个字符的灰度做比较,找出最接近的那个字符,来代表这一小块图象。那么,怎么确定字符的灰度呢?做下面的实验就明白了。

打开记事本(notepad),输入字符“1”,选定该字符,使其反色。按Alt+PrintScreen键拷贝窗口屏幕。打开画笔(paintbrush),粘贴;然后把图放到最大(×8),打开“查看”→“缩放”→“显示网格”菜单,如图4.9所示:

4.9    字符“1”的灰度

数数字符“1”用了几个点?是22个。我想你已经明白了,字符的灰度和它所占的黑色点数有关,点越少,灰度值越大,空格字符的灰度最大,为全白,因为它一个黑点也没有;而字符“W”的灰度值就比较低了。每个字符的面积是8×16(宽×高),所以一个字符的灰度值可以用如下的公式计算(1-所占的黑点数/(8×16))×255。下面是可显示的字符,及对应的灰度,共有95个。这可是我辛辛苦苦整理出来的呦!

static char ch[95]={

' ',

                                 '`','1','2','3','4','5','6','7','8','9','0','-','=','\\',

                                 'q','w','e','r','t','y','u','i','o','p','[',']',

                                 'a','s','d','f','g','h','j','k','l',';','\'',

                                 'z','x','c','v','b','n','m',',','.','/',

                                 '~','!','@','#','$','%','^','&','*','(',')','_','+','|',

                                 'Q','W','E','R','T','Y','U','I','O','P','{','}',

                                 'A','S','D','F','G','H','J','K','L',':','"',

                                 'Z','X','C','V','B','N','M','<','>','?'

                                 };

static int  gr[95]= {

                                  0,

                                  7,22,28,31,31,27,32,22,38,32,40, 6,12,20,38,32,26,20,24,40,

                         29,24,28,38,32,32,26,22,34,24,44,33,32,32,24,16, 6,22,26,22,

                                  26,34,29,35,10, 6,20,14,22,47,42,34,40,10,35,21,22,22,16,14,

                                  26,40,39,29,38,22,28,36,22,36,30,22,22,36,26,36,25,34,38,24,

                                  36,22,12,12,26,30,30,34,39,42,41,18,18,22

                                  };

下面的这段程序实现了bmp2txt的功能,结果存到文件bmp2txt.txt中。

BOOL Bmp2Txt(HWND hWnd)

{

DWORD                                       OffBits,BufSize;

LPBITMAPINFOHEADER   lpImgData;

LPSTR                               lpPtr;

HFILE                                    hf;

int                                                   i, j, k,h,tint,grayindex;

char                            tchar;

int                               TransHeight, TransWidth;

//先用起泡排序,将灰度值按从小到大排列,同时调整对应的字符位置

for(i=0;i<94;i++)

for(j=i+1;j<95;j++){

if(gr[i]>gr[j]){

                         tchar=ch[i],tint=gr[i];

                   ch[i]=ch[j],gr[i]=gr[j];

                         ch[j]=tchar,gr[j]=tint;

                  }

      }

//OffBitsBITMAPINFOHEADER结构长度加调色板的大小

OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);

BufSize=OffBits+bi.biHeight*LineBytes;//要开的缓冲区的大小

lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);

TransWidth = bi.biWidth/8; //每行字符的个数

TransHeight = bi.biHeight/16;  //共有多少行字符

hf=_lcreat("c:\\bmp2txt.txt",0);

for(i=0;i<TransHeight;i++){

      for(j=0;j<TransWidth;j++){

                  grayindex=0;

                  for(k=0;k<16;k++)

                         for(h=0;h<8;h++){ //求出8*16小块中各象素灰度之和

                                lpPtr=(char*)lpImgData+

BufSize-LineBytes-(i*16+k)*LineBytes+

j*8+h;

                                grayindex+=(unsigned char)*lpPtr;

                         }

                  grayindex/=16*8; //除以整个面积

                  grayindex=gr[94]*grayindex/255;

                  k=0;

                  while(gr[k+1]<grayindex)

                         k++;  //寻找灰度最接近的字符

                  _lwrite(hf,(char *)&ch[k],sizeof(char));   //将该字符写入文件中

      }

      tchar=(char)13;

      _lwrite(hf,(char     *)&tchar,sizeof(char));

      tchar=(char)10;

      _lwrite(hf,(char     *)&tchar,sizeof(char));  //每行加一个回车换行符

}

_lclose(hf);

GlobalUnlock(hImgData);

return TRUE;

}

上面的程序中,只考虑了8×16小块的平均灰度,而没有考虑小块内部象素的灰度分布。更精确的方法是将图象8×16小块和字符8×16小块每两个对应点之间相减,做平方误差计算,找出有最小平方误差的那个字符,来代表这一小块图象。显然,计算量要比刚才的大得多。这里我们就不给出程序了,有兴趣的读者可以自己实现。

其实利用图案化技术,还可以实现更有趣的应用,如图4.10,你仔细看看,贝多芬的头像是由许多个音乐符号组成的!

4.10  贝多芬的头像

0 0
原创粉丝点击