GDI+命中测试的效率[r]

问题的提出:
摄像头的分辨率从30万一直到700万,成本不停地降低,性能不停地提高,在项目开发过程中碰到这样的问题,为了将提高精度,使用700万的摄像头来进行微距拍摄,因此带来最直接的问题是相同的颗粒需要处理的像素多了很多多多多。
在低分辨率图像中,像素少,整体的处理时间也不多。但是一旦使用高分辨率的图像,为便于用户全局观察,图像显示时缩小了,对用户来说并未感觉到选择区域的变化,可是需要处理的像素增加了,处理时间则呈平方倍数增加,就会明显地察觉到停顿。为此,我们需要找到原来的程序的速度瓶颈,进行优化。



问题的分析:
在不改变算法逻辑的前提下,我们对程序的各部分的时间耗费进行了分析和测试。最后发现程序中有两中功能的代码应该还可以改进,减少所需的时间。
一个是对图像像素值的读写,也就是通过坐标(x,y)对像素的寻址定位,一般的方法就是通过
p = y*width + x;
来换算像素值在线形内存地址中的位置,这样的做法简单且直观,容易理解,但是每访问一个就需要进行1次乘法和1次加法。如果单个像素使用多个字节存储(一般24位3个字节),则还需要额外的乘法和加法。因此我们希望可以改变地址计算的方法,按照像素的存储顺序进行扫描,避免不必要的乘法运算。

另外一方面,就是判断坐标为(x,y)的像素是否在选择的区域内。因为程序所使用的检测算法对杂质的影响非常敏感,已经定好的解决方案是让用户自己圈定一个区域进行检测,因此在对图像进行处理时就需要判断点是否在区域内,是的话则进行统计并处理,否则放弃。原来的程序是基于GDI的,使用CRgn类创建多边形区域,使用CRgn::PtInRegion(x,y)函数判断命中。测试发现,对矩形区域(不判断)和对一个多边形区域进行处理,在代码中就是将判断命中的if语句注销掉和不去掉一行的差别,处理时间相差甚多。
我们使用了GDI+中的Region对象及Region::IsVisible(x,y)函数替代,处理时间有所缩短,但是,因为处理的像素实在太多(选择图像的一半就有350万个像素),判断和不判断区域命中的时间差距仍然比较多。因此开始怀疑GDI+的Region::IsVisible(x,y)函数的判断算法的效率有问题,于是决定自己实现这个判断的功能。实现的基本思想就是用空间来换时间。



问题的解决方案:

首先是扫描线问题,这个比较简单,按行扫描则符合像素的存储顺序。图中兰色代表扫描的区域,红色是扫描的顺序也就是地址的递增操作,需要注意的是每行扫描结束后需要增加offx,以跳到下一行行首地址,值应该为 w - right ,一般right都为选择区域内的最右边像素x坐标的再往右一个像素的坐标。

命中测试问题用FastHitTest类来解决,初始化时创建一个选定区域的外接矩形大小的位图,并记录矩形的左上坐标,然后将位图先全部涂成指定的背景色,然后在选定的区域中涂上用指定的前景色。判定的时候,只要检查这个位图对应点是否前景色,是则在区域内,否则在区域外。在外接矩形外的点不需要判断了,直接否定。在FastHitTest,记录了三种区域,如图中白色区域为外接矩形外的区域,通过左上坐标和位图大小来确定;兰色为指定的背景色,为选择区域的外接矩形;红色为指定的前景色,为所选择的区域。空间耗费就为位图所占用的空间,单次区域内判断的时间耗费就仅为查询位图内指定坐标的点的颜色。


项目中实际使用过的代码,集合放到IPLab程序中了,是VC6下直接链接gdiplus.lib库,可以直接编译运行,在这里下载。程序运行后打开工程文件夹下的700万(3072*2304)像素的图片,显示默认缩放为原图的15%,然后应该按住鼠标选定一块区域,快捷按钮1-5的功能分别为:1对全图进行反色处理;2、3、4都对选定的局部区域进行反色处理,但是判断命中的方式分别为CRgn方式、Region方式和FastHitTest方式,处理结束后分别给出处理时间;5将选择区域(记录了鼠标的划过的路径),用DrawPath和Fill两种方式画出来以比较路径和区域的差异。

贴出关键代码的全文。

图像反色算法函数:

void CIPLabDoc::IPFuncInvInRegion_F(GraphicsPath* pWorkingRegion)
{
    
if (!pPicture || !pWorkingRegion) {
        
return;
    }

    
    BitmapData bitmapData;
    Rect imageRect(
00, pPicture->GetWidth(), pPicture->GetHeight());//900,900);
    Rect rect;
    
    pWorkingRegion
->GetBounds(&rect);
    rect.Intersect(imageRect);
    
    BYTE inv[
256*3];
    
int i;
    
for(i=0;i<256;i++)
    
{
        inv[i
+256*0]=255-i;    //b
        inv[i+256*1]=255-i;    //g
        inv[i+256*2]=255-i;    //r
    }

    
    Status status 
= pPicture->LockBits(
        
&rect,
        ImageLockModeRead 
| ImageLockModeWrite,
        PixelFormat24bppRGB,
        
&bitmapData);
    
    
if (status != Ok ) {
        
return;
    }

    unsigned 
char* pixels = (unsigned char*)bitmapData.Scan0;
    
int x,y;
    
int offx = bitmapData.Stride - bitmapData.Width*3;
    BYTE
* pHistogramProjection = inv;
    
    FastHitTest fHitTest(pWorkingRegion);
    
    
for(y=rect.GetTop();y<rect.GetBottom();y++)
    
{
        
for(x=rect.GetLeft();x<rect.GetRight();x++)
        
{
            
if (fHitTest.IsVisible(x,y)) {
                
                
//b
                *pixels = pHistogramProjection[*pixels];
                pixels
++;
                pHistogramProjection 
+= 256;
                
                
//g
                *pixels = pHistogramProjection[*pixels];
                pixels
++;
                pHistogramProjection 
+= 256;
                
                
//r
                *pixels = pHistogramProjection[*pixels];
                pixels
++;
                pHistogramProjection 
-= 256*2;
            }

            
else
            
{
                pixels 
+= 3;
            }

        }

        pixels 
+= offx;
    }

    pPicture
->UnlockBits(&bitmapData);
    
    UpdateAllViews(NULL);
}


FastHitTest的构造函数:

void FastHitTest::init(const GraphicsPath* pPath,BOOL includeEdge)
{
    pPath
->GetBounds(&m_Bounds);

    m_RegionBuffer 
= new Bitmap(m_Bounds.Width,m_Bounds.Height,PixelFormat32bppRGB);

    Graphics g(m_RegionBuffer);

    SolidBrush backgroundBrush(m_BackgroundColor);
    g.FillRectangle(
&backgroundBrush,0,0,m_Bounds.Width,m_Bounds.Height);

    SolidBrush foregroundBrush(m_ForegroundColor);
    Pen foregroundPen(
&foregroundBrush);

    GraphicsPath 
*path = pPath->Clone();
    Matrix mat;

    mat.Translate((
float)(-1.0 * m_Bounds.GetLeft()), (float)(-1 * m_Bounds.GetTop()));
    path
->Transform(&mat);

    g.FillPath(
&foregroundBrush,path);

    
//confirm required
    if (includeEdge) {
        g.DrawPath(
&foregroundPen,path);
    }


    delete path;
}

 

判断命中的函数:

BOOL FastHitTest::IsVisible(int x, int y)
{
    
if!m_RegionBuffer )
    
{
        
return FALSE;
    }


    
if ( x < m_Bounds.GetLeft() || x >= m_Bounds.GetRight() 
        
|| y < m_Bounds.GetTop() || y >= m_Bounds.GetBottom() )
    
{
        
return FALSE;
    }


    UINT 
*= (UINT*)m_Data.Scan0 + m_Data.Stride * (y-m_Bounds.GetTop() ) / 4 + (x - m_Bounds.GetLeft());

    
if ( *== m_ForegroundColor.GetValue() ) {
        
return TRUE;
    }


    
return FALSE;
}




总结:

当图特别大的时候,处理区域特别大的时候,空间换时间的方式可以将处理时间下降到可以容忍的程度。空间耗费嘛,及时释放的话还是能够忍受的。

如果需要进行多次区域命中判断,整体代价(时间、空间及复杂度等因素)太高时,而且算法跟区域无关时,可以选择对全图进行处理(不判断命中),然后再对结果进行剪切,这样就只需要进行一次命中判断。

 

除此之外,自己构造的类可以解决一些边缘的问题。无论在GDI或者GDI+中,Rect的边缘(Draw出来的)和内部区域(Fill出来的)都有1个像素的差别,对于多边形区域就更难发现其中的对应关系,如果边缘跟踪出来的颗粒利用区域命中的方法来计算面积,就会和预期的有所差距,特别在颗粒呈细长条状时特别明显。程序中的HitTest按钮将这个差别画出来了,不是很容易观察到,可以剪屏后到画笔中用大尺寸查看。因此FastHitTest的构造参数中有个BOOL变量,来决定是否包括边缘。

原来还希望加入边缘宽度,并以边缘框架的形式来测试命中,这样可以在判断单次点击是否点击命中这个路径时,决定敏感区域的宽窄。但是这个问题并不需要单独出一个类花这么多的空间来完成,直接使用GDI+提供的方法已经足够了。因此也就没有进一步的扩展。

 

问题还很多,有兴趣或者有相关经验的朋友,不妨一起讨论。兴趣最重要,但是要肯花时间,真诚,不劳而获是可耻的。:-|

路漫漫其修远兮 吾将上下而求索

posted @ 2006-06-29 14:29 .Live 阅读(1983) 评论(11)  编辑 收藏 网摘 所属分类: 图形图像

  回复  引用  查看    
#1楼 2006-06-29 16:43 | 盛国军      
你好,看了你的文章,很感兴趣。不知你是否在北京?
  回复  引用  查看    
#2楼 [楼主]2006-06-29 19:15 | .Live      
我嘛在人间天堂,但是这个blog的作者有三分之一在北京,还有三分之一在上海。:)
[r]
  回复  引用  查看    
#3楼 [楼主]2006-06-29 19:21 | .Live      
那三分之一马上就到上海啦,我2号出差到上海
[Teaks]
  回复  引用  查看    
#4楼 2006-06-30 09:11 | 沐枫      
不明白作者是什么意图。

我猜测一下,请指教是否正确。
[[
  作者是想对指定的区域的图象,进行某种处理,并且不影响区域以外的图象。为了加速处理,需要对图象进行锁定,并用指针对图形数据进行计算;因此,需要确定正在处理的象素是否在区域内。
  为了断定象素是否在区域内,有两种可选择:GDI方法和GDI+方法。
]]

我不明白,本文的重点是在于区域判断,还是图象处理。

对于文中的程序,感觉似乎是对选定区域的图象作反色处理。(存在的其它可能是对选定区域的图象的颜色作查表替换,或其它类型的颜色变换)。对于这样的功能,GDI+已经提供了ColorMatrix以及ColorMap功能,虽然性能不好,但是其可扩展性极好,编写的程序也简单,并且自动对区域进行剪裁。作者可以参考一下。

另外,GDI+可以运行在Win32系统下,完全可以替代GDI,只要发布的时候附带上gdiplus.dll即可,所以,可以完全丢弃GDI的代码。

如果是区域判断,果然是用空间换时间。看程序,是创建一个选定区域包围盒大小的位图,将其清空,然后在其中用指定的颜色,将选定区域填充。最终判定的时候,只要检查这个位图对应点是否是指定的颜色,就知道是否在该区域内。


--

希望作者能用示意图,而不是代码来作解释,这样更容易明白些,也可以节省许多文字和代码的说明。
而示例代码,就不是长篇列出,冗长的地方可以用伪代码,这样即清晰,又能明确意图。

BTW:

SolidBrush backgroundBrush(m_BackgroundColor);
g.FillRectangle(&backgroundBrush,0,0,m_Bounds.Width,m_Bounds.Height);


可以简单的改为:

g.Clear(m_BackgroundColor);
  回复  引用  查看    
#5楼 [楼主]2006-06-30 09:27 | .Live      
原上海的三分之一 就要到江苏泰州出差了[xgluxv]
  回复  引用  查看    
#6楼 2006-06-30 09:51 | 沐枫      
关于某个点是否在区域内,目前我是看不到M$是如何实现的。相应的,可以找到wine的gdi源代码和mono的gdiplus(novell公司的libgdiplus)源代码实现。

两者区别很大。

wine的PtInRegion中,遍历Region中的所有矩形,以判定指定点是否包含在其中的一个矩形中。--想当然,经常调用PtInRegion时间代价太高。

libgdiplus中的IsVisible就不一样了。它检查Region是否已生成区域Bitmap,如果没有就生成一个,如果有,直接检查该区域Bitmap的对应点是否有上色。--这个好象和楼主的想法是一样的啊。

因此来说,空间换时间,libgdiplus也是采用的。因此希望楼主能够测试一下,用Gdi+的IsVisible和自已实现的IsVisible,这两个到底哪个效率高。
假如gdi+的实现和libgdiplus一样或更好,那么应该与楼主不相上下,如果gdi+采用wine的gdi方式来实现的话,那应该是楼主的方法更好。

假如gdi+的方法与libgdiplus一样,那么楼主就可以不需要自已实现了,毕竟用现成的代码更好不是吗?


更何况,我认为,libgdiplus占的空间要比楼主的小32倍。因为libgdiplus是用二值图象的。

参考:

1. libgdiplus1.1.15源代码
http://go-mono.com/sources/libgdiplus/libgdiplus-1.1.15.tar.gz
(region.c region-bitmap.c)
2. wine的gdi源代码
http://source.winehq.org/source/dlls/gdi/region.c

  回复  引用  查看    
#7楼 [楼主]2006-06-30 10:06 | .Live      
@沐枫

“如果是区域判断,果然是用空间换时间。看程序,是创建一个选定区域包围盒大小的位图,将其清空,然后在其中用指定的颜色,将选定区域填充。最终判定的时候,只要检查这个位图对应点是否是指定的颜色,就知道是否在该区域内。 ”

对的,就是这个原理!!!你讲的很清楚,我没表达好。

“对于文中的程序,感觉似乎是对选定区域的图象作反色处理。(存在的其它可能是对选定区域的图象的颜色作查表替换,或其它类型的颜色变换)。对于这样的功能,GDI+已经提供了ColorMatrix以及ColorMap功能,虽然性能不好,但是其可扩展性极好,编写的程序也简单,并且自动对区域进行剪裁。作者可以参考一下。 ”

反色只是个应用示例,实质是对RGB直方图的映射处理,是“对选定区域的图象的颜色作查表替换”。ColorMatrix以及ColorMap还不是清楚用法和效果,要向你请教了。

这篇文章的主要问题是性能,对大面积不规则图像的处理时间,所以应该是区域判断的时间问题。写这个是因为处理时间这个问题困扰了我们很久,我想应该也会有很多程序员会碰到类似的问题,所以把代码整理出来。当然,这是我们的方法,是一种参考方案,并非唯一的答案。

非常感谢你的回复,确实这篇说明写得很不好。应该再修改一下。
[r]
  回复  引用  查看    
#8楼 [楼主]2006-06-30 10:16 | .Live      
@沐枫

嗯,GDI+的Region::IsVisible测试比较过了,确实慢点,使用应该不是libgdiplus的方法,所以才有了这个文章啊。

至于32位位图嘛,因为当时使用GDI+提供的类里没有找到合适的对二值图像或者4bits、8bits的画刷等操作,所以也就直接使用最简单方法了,甚至没有变成24位的。同时也是考虑到独立成一个类了,后面如果有好的方法可以独立改进,不会影响程序的整体。

你提供的libgdiplus和wine应该好好看一下。
[r]
  回复  引用  查看    
#9楼 2006-06-30 10:42 | 沐枫      
PixelFormat 可以使用Format1bppIndexed,甚至可以参考libgdiplus的代码构建一个。
如果可以甚至可以将libgdiplus构建一个gdiplus.dll。:)

  回复  引用  查看    
#10楼 [楼主]2006-06-30 11:17 | .Live      
@沐枫
你的意思是使用使用libgdiplus构建的gdiplus.dll来替代.net提供的gdiplus.dll了?这个需要花点+点+点+..时间去尝试。也要考虑这样的整体效果如何,需要和现在的程序兼容,并且需要整体的效率要高于原来的dll。

GDI+提供了ColorMatrix以及ColorMap倒是比较实用的,应该研究一下,我们已经应用过一些ColorMatrix来进行亮度和对比度等的调节。但是问题还是处理时间,GDI+的Graphics的DrawImage方法允许带一个ImageAttributes*参数来进行图像的调整,但是在Image*对象超过1024*768(超过屏幕大小,这是我们的估计值)时,绘制速度就非常的慢。ImageAttributes*为NULL的时候就都非常快。

当然,这个是工具使用的问题了,不完全是算法的问题,再过几个周末会有点时间,把之前的一些工作再整理一下,也会发到这个blog上。

如果有什么好的相关资料,链接资源也希望大家能共享。
[r]
  回复  引用  查看    
#11楼 2006-07-05 09:56 | 沐枫      
不错,赞一个。
修改过的文字看得清楚多了。




标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2006-10-25 13:20 编辑过
Google站内搜索

China-pub 计算机图书网上专卖店!6.5万品种 2-8折!
近千种 9-95 新二手计算图书火热销售中!
开发者征途系统新作:《设计模式——基于C#的工程化实现及扩展》

相关文章:

相关链接: