分享
 
 
 

Crazybit开发手记(一):设计之数据结构和算法的分离

王朝vc·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

Crazybit开发手记(一):设计之数据结构和算法的分离

原创:Crazybit

主页:www.crazy-bit.com

开发手记记录了一些我在开发PhoXo(一个小巧的32bit图像处理软件)中的心得体会,它们全部源于实际应用,是我在探索OO(object orient)的路上获得的战利品,希望对大家有帮助。

相信每一个在windows下编过程序的人都或多或少地用过位图,大多数人是从网上下载一些成熟完善的DIB类库来使用(例如CxImage、CDIB),少数人有一套自己封装好的DIB类库,方便以后的扩充和使用。(近几年GDI+异军突起,在某些处理方面,如:缩放、旋转、渐变填充等它提供无与伦比的速度和质量,但,如果你想做一个完善的图像处理程序,直接使用它会给架构设计带来困难,你可以用adapter模式封装它后再使用...扯远了)。

这时候,如果你需要一些图像处理操作你会怎么办呢?很多没有OO经验的C++程序员(例如一年前的我)可能会这样做:在类中直接添加方法。

//================================================================

int FClamp0255 (int nValue) {return max (0, min (0xFF, nValue));} // 饱和到0--255

class FCObjImage

{

public :

Invert () ;

AdjustRGB (int R, int G, int B) ;

} ;

//================================================================

void FCObjImage::Invert ()

{

if ((GetHandle() == NULL) || (ColorBits() < 24))

return ;

int nSpan = ColorBits() / 8 ; // 每象素字节数3, 4

for (int y=0 ; y < Height() ; y++)

{

BYTE * pPixel = GetBits (y) ;

for (int x=0 ; x < Width() ; x++, pPixel += nSpan)

{

pPixel[0] = ~pPixel[0] ;

pPixel[1] = ~pPixel[1] ;

pPixel[2] = ~pPixel[2] ;

}

}

}

//================================================================

void FCObjImage::AdjustRGB (int R, int G, int B)

{

if ((GetHandle() == NULL) || (ColorBits() < 24))

return ;

int nSpan = ColorBits() / 8 ; // 每象素字节数3, 4

for (int y=0 ; y < Height() ; y++)

{

BYTE * pPixel = GetBits (y) ;

for (int x=0 ; x < Width() ; x++, pPixel += nSpan)

{

pPixel[0] = FClamp0255 (pPixel[0] + B) ;

pPixel[1] = FClamp0255 (pPixel[1] + G) ;

pPixel[2] = FClamp0255 (pPixel[2] + R) ;

}

}

}

//================================================================

这里举了两个例子(分别实现反色,调节RGB值功能),现实中会有大量的此类操作:亮度、对比度、饱和度......现在回想一下,你添加这些方法的步骤是什么,Ooooooooo,RCP(我同事的发明,全称:rapid copy paste^-^),第一步一定是从上面复制一块代码下来,然后改掉其中的接口和处理部分。虽然这里的示范代码很短小,不会连同bug一起复制,但,定时炸弹却又多了一个。有天,你的boss告诉你:我不能忍受长时间的等待,请给我加个进度条.....。你也许会加个全局变量,也许会给每个函数加个参数,但不变的是:你必须修改所有这些处理函数的代码,内心的咒骂并不会使你少改其中的任何一个。而此时,bug已经在旁边伺机而动了...然而苦日子远没熬到头,一个月后,你心血来潮的老板会让你在其中加上区域处理的功能,再一个月后......

回头重新看看代码?没错,除了红色的代码外,其他地方一摸一样,那能不能把这些算法分离抽出来呢?可能我们马上会想到标准库中qsort和windows中常用的回调方法。好,让我们实作一下:

//================================================================

void Pixel_Invert (BYTE * pPixel)

{

pPixel[0] = ~pPixel[0] ;

pPixel[1] = ~pPixel[1] ;

pPixel[2] = ~pPixel[2] ;

}

//================================================================

void FCObjImage::PixelProcess (void(__cdecl*PixelProc)(BYTE * pPixel))

{

if ((GetHandle() == NULL) || (ColorBits() < 24))

return ;

int nSpan = ColorBits() / 8 ; // 每象素字节数3, 4

for (int y=0 ; y < Height() ; y++)

{

BYTE * pPixel = GetBits (y) ;

for (int x=0 ; x < Width() ; x++, pPixel += nSpan)

{

PixelProc (pPixel) ;

}

}

}

//================================================================

void FCObjImage::Invert ()

{

PixelProcess (Pixel_Invert) ;

}

//================================================================

嗯,看样子不错,算法被剥离到一个单一函数中,我们似乎已经解决问题了。处理Invert它完成的非常好,但处理AdjustRGB时遇到了麻烦,RGB那三个调节参数怎么传进去呢?我们的接口参数只有一个,通过添加全局变量/成员变量?这是一个办法,但随着类方法的增加,程序的可读性和维护性会急剧的下降,反而倒不如改之前的效果好。

那么如何实现高度的抽象和良好的接口呢?我们现场请来OO(object orient),请它来讲一下它的实现。设计如下派生关系:

//================================================================

class FCSinglePixelProcessBase

{

public :

virtual void ProcessPixel (int x, int y, BYTE * pPixel) PURE ;

} ;

//================================================================

class FCPixelInvert : public FCSinglePixelProcessBase

{

public :

virtual void ProcessPixel (int x, int y, BYTE * pPixel) ;

} ;

void FCPixelInvert::ProcessPixel (int x, int y, BYTE * pPixel)

{

pPixel[0] = ~pPixel[0] ; pPixel[1] = ~pPixel[1] ; pPixel[2] = ~pPixel[2] ;

}

//================================================================

class FCPixelAdjustRGB : public FCSinglePixelProcessBase

{

public :

FCPixelAdjustRGB (int DeltaR, int DeltaG, int DeltaB) ;

virtual void ProcessPixel (int x, int y, BYTE * pPixel) ;

protected :

int m_iDeltaR, m_iDeltaG, m_iDeltaB ;

} ;

void FCPixelAdjustRGB::ProcessPixel (int x, int y, BYTE * pPixel)

{

pPixel[0] = FClamp0255 (pPixel[0] + m_iDeltaB) ;

pPixel[1] = FClamp0255 (pPixel[1] + m_iDeltaG) ;

pPixel[2] = FClamp0255 (pPixel[2] + m_iDeltaR) ;

}

//================================================================

然后我们修改image类如下:

//================================================================

#include "PixelProcessor.h"

class FCObjImage

{

public :

void PixelHandler (FCSinglePixelProcessBase & PixelProcessor, FCObjProgress * progress = NULL) ;

} ;

//================================================================

void FCObjImage::PixelHandler (FCSinglePixelProcessBase & PixelProcessor, FCObjProgress * progress)

{

if (GetHandle() == NULL)

return ;

int nSpan = ColorBits() / 8 ; // 每象素字节数3, 4

for (int y=0 ; y < Height() ; y++)

{

BYTE * pPixel = GetBits (y) ;

for (int x=0 ; x < Width() ; x++, pPixel += nSpan)

{

PixelProcessor.ProcessPixel (x, y, pPixel) ;

}

if (progress != NULL)

progress->SetProgress (y * 100 / Height()) ;

}

}

//================================================================

void FCObjImage::Invert (FCObjProgress * progress)

{

PixelHandler (FCPixelInvert(), progress) ;

}

void FCObjImage::AdjustRGB (int R, int G, int B, FCObjProgress * progress)

{

PixelHandler (FCPixelAdjustRGB (R,G,B), progress) ;

}

//================================================================

(以上只是一个基本框架,你可以很轻易的把区域处理的参数添加进去-通过构造时传递一个RECT参数。)

对象真的是一个很奇妙的东西,它可以对外提供一个简单的接口,而自身又可以封装上很多附加信息。

好,现在让我们来检验一下刚才的成果:添加一个给图像奇数行置黑,给偶数行置白的操作。

//================================================================

class FCPixelTest : public FCSinglePixelProcessBase

{

public :

virtual void ProcessPixel (int x, int y, BYTE * pPixel) ;

} ;

void FCPixelTest::ProcessPixel (int x, int y, BYTE * pPixel)

{

if (y % 2)

pPixel[0]=pPixel[1]=pPixel[2] = 0 ; // 奇数行

else

pPixel[0]=pPixel[1]=pPixel[2] = 0xFF ; // 偶数行

}

然后进行如下调用:

PixelHandler (FCPixelTest(), progress) ;

//================================================================

多么的和谐美妙,设计算法的人员只需写出自己的算法,而不用去考虑怎么让它支持进度条和区域这些问题。感觉这就象一把设计优良的AK,你可以不断的往里添加子弹(对象)^-^

至此,我们应该已经大功告成了。

还有问题吗?

等等,别忙,有些地方不太对,我添加这个算法后,怎么编译这么久啊。

问题就出在那个不起眼的:

#include "PixelProcessor.h"

image是图像处理的最底层对象,工程中的所有文件都直接或间接地包含它,因此,任何对image.h本身及它所包含的.h的修改都会引起几乎整个工程的build,这当然是无法忍受的,解决的办法是使用“前置声明”,因为在PixelHandler接口中我们只需要它的引用(也即是说:我(接口)并不需要知道传给我的类的内部结构,给我一个32(64)的内存地址就OK了)。

因此我们把

#include "PixelProcessor.h"

替换成:

class FCSinglePixelProcessBase ; // external class 前置声明

然后在.cpp文件中再包含PixelProcessor.h,这样,对PixelProcessor.h的改变仅仅会导致.cpp文件的重新编译,大大节约了编译时间。

总结:

1)可能的话,在编程中永远也别去想“拷贝代码”这个字眼。毕竟,OO就是为了抽象和代码重用才诞生的。

2)除非必要,否则类的成员变量和函数的参数尽量用指针或引用代替,这样做可以在.h中尽可能地少包含其他.h文件,而用前置声明来替代,以此来减少编译时间和以后可能会产生的交叉包含。

3)最后说一下效率问题:有些朋友可能会说每个像素都调用虚函数会影响性能,这的确,但实际的损失远没有想象的大。我实测了一下:对1024*768的图片进行反片处理,速度只有5%左右的损失,进行复杂处理(亮度/对比度/gamma)时损失可完全忽略,毕竟多出来的那部分代码只是进出栈和查表,而不是浮点除这样耗时的指令。

2004/2/24Crazybit

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有