图形图像处理-之-误差扩散 下篇 - 更快的速度或更好的效果
[email protected] 2010.01.05
(2010.01.06 补充误差扩散算法并行化的一些探讨 )
tag: 误差扩散,真彩色到高彩色转换,色阶,减色,半色调
摘要: 在图像的颜色转换过程中,由于颜色值域的不同,转换过程中可能会产生误差;
误差扩散算法通过将误差传递到周围像素而减轻其造成的视觉误差。
上篇:简单实现; 中篇:简单的速度优化; 下篇: 更快的速度或更好的效果.
(测试源代码下载: https://github.com/sisong/demoForHssBlog )
正文:
代码使用C++,编译器:VC2005
测试平台:(CPU:i7-920(3.44G); 内存:DDR3 1333(三通道); 编译器:VC2005)
(请先参看文章的上篇和中篇)
A:更快的速度
前面的文章用使用的误差传递系数为:
* 2
1 1 0 /4
为速度和质量做了折中;而在某些应用场合下,可能希望更快的实时进行误差扩散;
这时可以考虑这样的误差扩散系数:
* 1
0 1 0 /2
甚至于:
* 1 /1
这时误差只用传递到右边,实现起来就简单多了:
//扩散模板 // * 1 /1 void CvsPic32To16_ErrorDiffuse_Line_fast(UInt16 * pDst, const Color32 * pSrc, long width){ TErrorColor HErr; HErr.dR = 0 ; HErr.dG = 0 ; HErr.dB = 0 ; for ( long x = 0 ;x < width; ++ x) { long cB = (pSrc[x].b + HErr.dB); long cG = (pSrc[x].g + HErr.dG); long cR = (pSrc[x].r + HErr.dR); long rB = BestRGB16_555Color_Table[cB]; long rG = BestRGB16_555Color_Table[cG]; long rR = BestRGB16_555Color_Table[cR]; pDst[x] = rB | (rG << 5 ) | (rR << 10 ); HErr.dB = (cB - getC8Color(rB)) ; HErr.dG = (cG - getC8Color(rG)) ; HErr.dR = (cR - getC8Color(rR)) ; } } void CvsPic32To16_ErrorDiffuse_fast( const TPicRegion_RGB16_555 & dst, const TPixels32Ref & src){ UInt16 * pDst = (UInt16 * )dst.pdata; const Color32 * pSrc = src.pdata; const long width = src.width; for ( long y = 0 ;y < src.height; ++ y){ CvsPic32To16_ErrorDiffuse_Line_fast(pDst,pSrc,width); (UInt8 *& )pDst += dst.byte_width; (UInt8 *& )pSrc += src.byte_width; } }
速度测试:
//////////////////////////////////////////////////////////////
//CvsPic32To16_ErrorDiffuse_fast 422.83 FPS
//////////////////////////////////////////////////////////////
效果(比前面的差一些了):
简单改写成MMX实现:
const UInt64 csMMX_erdf_mul_w = 0x41CE41CE41CE41CE; //0x41CE=16846=(255*(1<<11)/((1<<5)-1)); const UInt64 csMMX_erdf_mask_w = 0x0000000000F8F8F8; //0xF8=((1<<5)-1)<<3; inline void CvsPic32To16_ErrorDiffuse_Line_MMX(UInt16* pDst,const Color32* pSrc,long width){ asm{ //push esi //push edi //push ebx mov ecx,width mov edi,pDst mov esi,pSrc lea edi,[edi+ecx*2] //2==sizeof(UInt16) lea esi,[esi+ecx*4] //4==sizeof(Color32) neg ecx pxor mm6,mm6 //HErr=0000000... pxor mm7,mm7 //mm7 =0000000... movq mm3,csMMX_erdf_mul_w movq mm4,csMMX_erdf_mask_w loop_begin: movd mm0,[esi+ecx*4] punpcklbw mm0,mm7 paddw mm0,mm6 //ok: cB,cG,cR movq mm6,mm0 packuswb mm0,mm7 pand mm0,mm4 psrlq mm0,3 //>>3 movd eax,mm0 //00000000 000RRRRR 000GGGGG 000BBBBB punpcklbw mm0,mm7 movzx ebx,ah movzx edx,al psllw mm0,5 shr eax,16 shl ebx,5 pmulhw mm0,mm3 shl eax,10 or edx,ebx psubw mm6,mm0 or eax,edx mov word ptr[edi+ecx*2],ax //pDst[x]= rB|(rG<<5)|(rR<<10); inc ecx jnz loop_begin emms //pop ebx //pop edi //pop esi } } void CvsPic32To16_ErrorDiffuse_MMX( const TPicRegion_RGB16_555 & dst, const TPixels32Ref & src){ UInt16 * pDst = (UInt16 * )dst.pdata; const Color32 * pSrc = src.pdata; const long width = src.width; if (width<=0) return; for ( long y = 0 ;y < src.height; ++ y){ CvsPic32To16_ErrorDiffuse_Line_MMX(pDst,pSrc,width); (UInt8 *& )pDst += dst.byte_width; (UInt8 *& )pSrc += src.byte_width; } }
速度测试:
//////////////////////////////////////////////////////////////
//CvsPic32To16_ErrorDiffuse_MMX 662.98 FPS
//////////////////////////////////////////////////////////////
效果:
B:更好的效果
为了优化颜色转化效果,一般用半色调技术来优化输出;
这些技术中误差扩散算法一直是效果最好的技术之一;
我们来实现经典的Floyd-Steinberg误差扩散系数,
//Floyd-Steinberg // * 7 //3 5 1 /16 void CvsPic32To16_ErrorDiffuse_Line_fs(UInt16 * pDst, const Color32 * pSrc, long width,TErrorColor * PHLineErr0,TErrorColor * PHLineErr1){ TErrorColor HErr; HErr.dR = 0 ; HErr.dG = 0 ; HErr.dB = 0 ; PHLineErr1[ - 1 ].dB = 0 ; PHLineErr1[ - 1 ].dG = 0 ; PHLineErr1[ - 1 ].dR = 0 ; PHLineErr1[ 0 ].dB = 0 ; PHLineErr1[ 0 ].dG = 0 ; PHLineErr1[ 0 ].dR = 0 ; for ( long x = 0 ;x < width; ++ x) { long cB = (pSrc[x].b + ((HErr.dB + PHLineErr0[x].dB)>>4) ); long cG = (pSrc[x].g + ((HErr.dG + PHLineErr0[x].dG)>>4) ); long cR = (pSrc[x].r + ((HErr.dR + PHLineErr0[x].dR)>>4) ); long rB = BestRGB16_555Color_Table[cB]; long rG = BestRGB16_555Color_Table[cG]; long rR = BestRGB16_555Color_Table[cR]; pDst[x] = rB | (rG << 5 ) | (rR << 10 ); PHLineErr1[x + 1].dB = (cB - getC8Color(rB)); PHLineErr1[x + 1].dG = (cG - getC8Color(rG)); PHLineErr1[x + 1].dR = (cR - getC8Color(rR)); PHLineErr1[x - 1].dB += (PHLineErr1[x + 1].dB*3); PHLineErr1[x - 1].dG += (PHLineErr1[x + 1].dG*3); PHLineErr1[x - 1].dR += (PHLineErr1[x + 1].dR*3); PHLineErr1[x ].dB += (PHLineErr1[x + 1].dB*5); PHLineErr1[x ].dG += (PHLineErr1[x + 1].dG*5); PHLineErr1[x ].dR += (PHLineErr1[x + 1].dR*5); HErr.dB = (PHLineErr1[x + 1].dB*7); HErr.dG = (PHLineErr1[x + 1].dG*7); HErr.dR = (PHLineErr1[x + 1].dR*7); } } void CvsPic32To16_ErrorDiffuse_fs( const TPicRegion_RGB16_555 & dst, const TPixels32Ref & src){ UInt16 * pDst = (UInt16 * )dst.pdata; const Color32 * pSrc = src.pdata; const long width = src.width; TErrorColor * _HLineErr = new TErrorColor[(width + 2)*2 ]; for ( long x = 0 ;x < (width + 2)*2 ; ++ x){ _HLineErr[x].dR = 0 ; _HLineErr[x].dG = 0 ; _HLineErr[x].dB = 0 ; } TErrorColor * HLineErr0 =& _HLineErr[ 1 ]; TErrorColor * HLineErr1 =& _HLineErr[ 1 + (width + 2) ]; for ( long y = 0 ;y < src.height; ++ y){ CvsPic32To16_ErrorDiffuse_Line_fs(pDst,pSrc,width,HLineErr0,HLineErr1); std::swap(HLineErr0,HLineErr1); (UInt8 *& )pDst += dst.byte_width; (UInt8 *& )pSrc += src.byte_width; } delete[]_HLineErr; }
速度测试:
//////////////////////////////////////////////////////////////
//CvsPic32To16_ErrorDiffuse_fs 198.81 FPS
//////////////////////////////////////////////////////////////
看看效果:
放大对比这几幅图,注意看看细节;
误差扩散算法还原效果很好,但有一个缺点,就是容易产生局部细纹;
误差扩散形成的一些颗粒组成了某些可见的小纹理(因为固定的扩散模板系数);
要克服这个问题,可以考虑一些改进方案或其组合:
a.随机调整阈值,从而在计算最接近的颜色的时候有一定的随机性;
b.建立一个较大的扩展系数表,根据当前的误差值选择定不同的扩展模板;
c.根据已经处理的颜色的误差情况,动态调整模板系数;比如当前点上面的点误差较大,那么就减小向下面的扩展系数值 (或者把按误差大小逆序);
或者据此做一定的阈值调整;
d.可以考虑一个后处理局部纹理的迭代过程: 先做一遍误差处理,然后对处理完的满足一定的条件的点做一些值反转,从当前值(比如亮度增大了)变成对应的阈值(亮度减小),(可以同时处理两个相邻值,它们一个变亮,一个变暗),反转条件是是否更有利于还原原图像,评价模型可以选择高斯值/梯度等手段(或其他评价模型);
e.有时如果目标颜色色域和当前颜色色域范围差别大,可以考虑先进行一个色域的线性映射,以牺牲颜色还原来维持较好的整体对比度;
f.某些时候如果目标颜色表很小时(比如彩色转成黑白2色),可以先对图片做一些锐化/边缘增强和局部对比度增强,这样得到的效果更好;
g.某些时候,扩散结果,亮度还原/梯度还原/对比度还原等可能比颜色还原更重要;
一个较复杂算法的处理效果图,请对比这些效果图的整体和放大后的细节:
(用我写的一个简单工具生成的,输出用了其中的555颜色输出模式,这里可以下载该程序: http://bbs.meizu.com/thread-1440271-1-1.html
该工具用于预处理图片,优化其在不支持真彩色显示的设备上的显示效果; )
另一个例子,将图片转换到一个较小的固定颜色表:
源图片:
优化的误差扩散,颜色还原优先:
考虑视觉模型误差扩散 亮度还原优先 (效果好了很多):
把梯度还原作为最优先: (很有艺术感的图片:),并注意其光滑性! 没有见过其他人实现,应该属于原创)
C: 并行误差扩散算法的探讨
误差扩散一般的算法都是一行一行从上到下依次处理;算法本身是不太适合并行的(除了 * 1 /1 模板支持行之间无影响的并行);
有些建议说每个处理核心延迟一下,接着上一个处理核心启动处理下一行,就能满足并行要求了;但实际情况是,CPU并不能按时按量准时完成(多任务操作系统);下一行的核心很可能超过上一行的处理进度,从而处理失败;如果为了保证处理进度,而增加一些锁,也不是很好,代码复杂,而且随着核心越来越多,还可能反而变慢! (而且GPU具有比CPU多得多的的并行核心)
a.如果对质量要求不高,可以考虑将图片分成很多块交给多个核心并行处理;处理完以后可以考虑对边界做一点特殊的修正处理;
b.按固定的区块大小将图片分成多个区,这样的话区块间是可以完全并行的;对区块内的各个点设置一个固定的处理顺序和不同的扩散系数方向,
利用处理顺序和系数方向,可以设计出几个批次,每个批次内点的处理顺序无关; 这样就能提够足够的并行性了;
c.思考中的一个迭代算法,主要用在有很多个核心的系统上(比如GPU上),每个点启动一个核心(最大的并行粒度), 算法将周围点的误差值*系数(可能随迭代变动)+当前值作为输入值,根据某个视觉模型生成最佳的目标值(下次迭代中的当前值)和新的误差值(输入值=目标值+误差值); 几次迭代以后或视觉模型差值小于某阈值以后停止迭代; (该算法没有试验过)