草庐IT

超越OpenCV速度的MorphologyEx函数实现(特别是对于二值图,速度是CV的4倍左右)。

只(挚)爱图像处理 2023-03-28 原文

       最近研究了一下opencv的 MorphologyEx这个函数的替代功能, 他主要的特点是支持任意形状的腐蚀膨胀,对于灰度图,速度基本和CV的一致,但是 CV没有针对二值图做特殊处理,因此,这个函数对二值图的速度和灰度是一样的,但是这个函数,如果使用的话,估计大部分还是针对二值图像,因此,我对二值图做了特别优化,速度可以做到是CV这个函数的4倍左右。

       MorphologyEx的主要功能是对灰度图进行相关形态学的处理,比如腐蚀、膨胀、开闭等计算,其代码可以在github上找到:https://github.com/opencv/opencv/blob/master/modules/imgproc/src/morph.dispatch.cpp#L1160

  opencv的这个代码,1000多行,从头看到尾,就没有看到几句和算法本身有关的内容,仔细看下里面有下面的代码:

      

       他不是调用Opencl就是使用IPP库,还是自己去想算法的优化吧。

       其实这个算法的优化我在很多年前就一直在考虑,只是一直么有动手,主要是无思路。最近在研究模板匹配的时候,因为有需求,做了下带蒙版功能的NCC匹配,对于这个类似的算法也就有了想法。

       在正常情况下,我们的核是矩形的或者是圆形的,对于矩形核,在SSE图像算法优化系列七:基于SSE实现的极速的矩形核腐蚀和膨胀(最大值和最小值)算法 一文中已经提出了优化算法,对于圆形半径,在【短道速滑八】圆形半径的图像最大值和最小值算法的实现及其实时优化(非二值图) 一文也提出了解决方案,两种方案都非常的高效和快速。

      如果是任意形状的核,考虑到其无固定的规律,上述常规的优化手段都无法完成,如何弄呢。

      我对这个算法想过很久,那么最近我得到的结论是肯定不能整体做优化,我想到的就是把蒙版区域按水平方向或者垂直方向分割成一条一条或者一列一列的小块,每个小块单独执行类似的算法,那么比如一个9*9的蒙版,如果其中的连续的小块有20个,那最多也就是标准矩形算法的20倍耗时(实际是不需要的,以为有很多公共计算),而矩形算法的速度是非常非常高效的。 

      实践了下,这个做法是有效的,而且也是相对来说高效的,但是后面想了下,为什么要分割成一条一条的呢,毕竟有很多条条的宽度或者高度是一样的,可以把他们作为一个整体合并成一个Rectangle,对一个Rectanle进行处理,和标准的矩形核的算法也不是一样的吗。  

  如下所示,如果按照列方向一次一个列,则有31个列,但是如果是将相同高的列合并,则只有19个,数量减少了近一半。

                       

           原图                    列方向的分块矩形                行方向的分块矩形

  实践表明,这种处理后,整体能有效的提高计算速度。

       至于是选择列方向的分块矩形还是行方向的,则和算法本身的优化有一定的关系,比如在本例中,由于SIMD的特性,我们在计算腐蚀或者膨胀的时候,利用的有关的G值和H值在垂直方向计算时可方便的使用SIMD指令进行比较,因此,选择列方向的分块则更为有利。

  那么对于二值图像的腐蚀和膨胀,我们在超越halcon速度的二值图像的腐蚀和膨胀,实现目前最快的半径相关类算法(附核心源码) 一文中有提高一种更为特别的优化手段。那么这个手段但让也可以用到本例中来。而在Opencv中,MorphologyEx函数是没有对这个做特殊处理的。

  我们做下简单的速度比较:

  对一副 500万像素的图进行 31*31的 椭圆蒙版进行处理,本例耗时约为95ms, CV耗时约为 250ms。

       但是奇怪的是,如果在CV中把蒙版的尺寸设置为偶数,比如30*30,其执行速度会快很多,比如同样上述图,CV的耗时只有78毫秒了,和我这里速度差不多,目前还不知道这个问题是怎么引起的。

  相关测试代码如下:

    IplConvKernel *kernel0 = cvCreateStructuringElementEx(31, 31, 15, 15, CV_SHAPE_ELLIPSE);

    for (int i = 0; i < 100; i++)
        cvMorphologyEx(Src, Dest, NULL, kernel0, CV_MOP_DILATE, 1);

    cvReleaseStructuringElement(&kernel0);

    QueryPerformanceCounter(&t2);
    printf("Use Time:%f\n", (t2.QuadPart - t1.QuadPart)*1.0 / tc.QuadPart * 1000);

    IplConvKernel *kernel1 = cvCreateStructuringElementEx(30, 30, 15, 15, CV_SHAPE_ELLIPSE);
    QueryPerformanceCounter(&t1);

    for (int i = 0; i < 100; i++)
        cvMorphologyEx(Src, Dest, NULL, kernel1, CV_MOP_DILATE, 1);

    cvReleaseStructuringElement(&kernel1);
    QueryPerformanceCounter(&t2);
    printf("Use Time:%f\n", (t2.QuadPart - t1.QuadPart)*1.0 / tc.QuadPart * 1000);

    IplConvKernel *kernel2 = cvCreateStructuringElementEx(15, 15, 7, 7, CV_SHAPE_ELLIPSE);
    QueryPerformanceCounter(&t1);

    for (int i = 0; i < 100; i++)
        cvMorphologyEx(Src, Dest, NULL, kernel2, CV_MOP_DILATE, 1);

    cvReleaseStructuringElement(&kernel2);
    QueryPerformanceCounter(&t2);
    printf("Use Time:%f\n", (t2.QuadPart - t1.QuadPart)*1.0 / tc.QuadPart * 1000);

    IplConvKernel *kernel3 = cvCreateStructuringElementEx(14, 14, 7, 7, CV_SHAPE_ELLIPSE);
    QueryPerformanceCounter(&t1);

    for (int i = 0; i < 100; i++)
        cvMorphologyEx(Src, Dest, NULL, kernel3, CV_MOP_DILATE, 1);
    cvReleaseStructuringElement(&kernel3);

    QueryPerformanceCounter(&t2);
    printf("Use Time:%f\n", (t2.QuadPart - t1.QuadPart)*1.0 / tc.QuadPart * 1000);

     CV的耗时统计如下(100次循环计算的耗时)。

 

       如果是同样一份大小的二值图像,在本例只需22ms,CV的耗时则还是和上面的一样。

       这里也不得不说一句,Intel的IPP的优化功能真的也还是不错。

       使用halcon的也做了类似的测试,halcon里对于规则的图像有一些特别的函数,比如 gray_dilation_rect, gray_dilation_shape,他的这些算子和我的标准优化的版本速度差不多。而对于其他的自定义形状,则要使用read_gray_se读取一个固定格式的文件。当然对于我们上面使用的椭圆, halcon已经有个一个定义好的函数gen_disc_se。

      对于椭圆,在Halcon中我用下述代码测试:

read_image (Image, 'd:/1.bmp')
gen_disc_se (SE, 'byte', 31, 31, 0)
for J := 0 to 100 - 1 by 1
    gray_dilation(Image, SE,  ImageDilation)
endfor

       得到的速度结果非常吓人,循环100次也只要1600ms,那意味着每次只要16ms,比我这里要快5倍多,真是牛逼,然后看了下gray_dilation的说明文档,在Parallelization一项里有这个说明:Automatically parallelized on internal data level.即在在内部数据级别自动并行。同时观察上述代码运行时的CPU运行情况,CPU的使用率高达80%左右,我个人分析他内部是做了多核并行的。这样的话我还可以接受的。我的机器是4核单位,如果我的速度除以4,嗨嗨。

     说到这,我正好也抽空研究了下read_gray_se这个函数,如果我要在Halcon里实现其他非规则形状的腐蚀,只能通过这个函数,这个函数的需要从文件里读取一些列数据,而这个文件我在百度搜索,基本没看到有详细的说明,Halcon帮助文档里到时有说明,不过有点晦涩,我这里正好解释下,也当做给自己做个笔记,不然时间长了,我自己也不记得了。

    他对文件(文本文件)的要求如下:

    第一行指明类型,可以是一下三个字符中的一种:   'byte', 'uint2' or 'real'     注意不要带引号,对于图像数据,一般用byte

    第二行,是指 structuring element 的尺寸,宽度 +  空格 + 高度

    第三行,这个比较重要,他的意思我们可以这样理解, 就是按照单行方向考虑,你需要计算腐蚀和膨胀的 连续区域的总数量。

         接下来的每一行数据, 都必然是3个数字,每个数字之间用空格隔开, 第一个数据是指这个行所在的行号(以0为下标起点),第二个数据只区域的起点,  第三个数据只区域的 终点。

         这些行的行数必须和第三行的数字对应,而且不能超过高度和宽度的范围。

    接下来的数据就是Halcon独有的了,我的和CV的都不具有这个功能,他还能指定structuring element 每个位置对应的偏移量值,就在对应位置的元素值加上这个偏移量值作为计算腐蚀和膨胀的依据,可正可负。这个确实比较强大,但是测试表明,如果有这些值,函数的计算速度可能会急剧下降,比如前面的测试代码中gen_disc_se (SE, 'byte', 31, 31, 0), 如果更改为gen_disc_se (SE, 'byte', 31, 31, 1),100次的速度会立即增加到8秒多。

       比如前面对应的31*31的椭圆区域的SE就可以用下述字符描述:

byte
31 31
31
0 12 18
1 9 21
2 7 23
3 6 24
4 5 25
5 4 26
6 3 27
7 2 28
8 2 28
9 1 29
10 1 29
11 1 29
12 0 30
13 0 30
14 0 30
15 0 30
16 0 30
17 0 30
18 0 30
19 1 29 
20 1 29
21 1 29
22 2 28
23 2 28
24 3 27
25 4 26
26 5 25
27 6 24
28 7 23
29 9 21 
30 12 18

       不过,由这个结构,也可以窥探到,Halcon内部的Region结构可能用的是这种单行的RLE编码,而不是基于Rectangle的。

       上面的例子可能不是很好,因为他正好是一行只有一个结构,其实一行是可以是有多个,比如下面的数据:

byte
5 5
8
0  1 4
1  0 0
1  3 3
2  2 3 
2  4 4 
3  0 4
4  0 1
4  4 4

        本例相关测试结果可参考: https://files.cnblogs.com/files/Imageshop/MaskFilter.rar?t=1652089081

    

 

    如果想时刻关注本人的最新文章,也可关注公众号:

                             

 

有关超越OpenCV速度的MorphologyEx函数实现(特别是对于二值图,速度是CV的4倍左右)。的更多相关文章

  1. ruby - 在没有 sass 引擎的情况下使用 sass 颜色函数 - 2

    我想在一个没有Sass引擎的类中使用Sass颜色函数。我已经在项目中使用了sassgem,所以我认为搭载会像以下一样简单:classRectangleincludeSass::Script::FunctionsdefcolorSass::Script::Color.new([0x82,0x39,0x06])enddefrender#hamlengineexecutedwithcontextofself#sothatwithintemlateicouldcall#%stop{offset:'0%',stop:{color:lighten(color)}}endend更新:参见上面的#re

  2. ruby-on-rails - 在 ruby​​ 中使用 gsub 函数替换单词 - 2

    我正在尝试用ruby​​中的gsub函数替换字符串中的某些单词,但有时效果很好,在某些情况下会出现此错误?这种格式有什么问题吗NoMethodError(undefinedmethod`gsub!'fornil:NilClass):模型.rbclassTest"replacethisID1",WAY=>"replacethisID2andID3",DELTA=>"replacethisID4"}end另一个模型.rbclassCheck 最佳答案 啊,我找到了!gsub!是一个非常奇怪的方法。首先,它替换了字符串,所以它实际上修改了

  3. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  4. ruby - 在 Ruby 中有条件地定义函数 - 2

    我有一些代码在几个不同的位置之一运行:作为具有调试输出的命令行工具,作为不接受任何输出的更大程序的一部分,以及在Rails环境中。有时我需要根据代码的位置对代码进行细微的更改,我意识到以下样式似乎可行:print"Testingnestedfunctionsdefined\n"CLI=trueifCLIdeftest_printprint"CommandLineVersion\n"endelsedeftest_printprint"ReleaseVersion\n"endendtest_print()这导致:TestingnestedfunctionsdefinedCommandLin

  5. ruby - 在 Ruby 中按名称传递函数 - 2

    如何在Ruby中按名称传递函数?(我使用Ruby才几个小时,所以我还在想办法。)nums=[1,2,3,4]#Thisworks,butismoreverbosethanI'dlikenums.eachdo|i|putsiend#InJS,Icouldjustdosomethinglike:#nums.forEach(console.log)#InF#,itwouldbesomethinglike:#List.iternums(printf"%A")#InRuby,IwishIcoulddosomethinglike:nums.eachputs在Ruby中能不能做到类似的简洁?我可以只

  6. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  7. Vscode+Cmake配置并运行opencv环境(Windows和Ubuntu大同小异) - 2

    之前在培训新生的时候,windows环境下配置opencv环境一直教的都是网上主流的vsstudio配置属性表,但是这个似乎对新生来说难度略高(虽然个人觉得完全是他们自己的问题),加之暑假之后对cmake实在是爱不释手,且这样配置确实十分简单(其实都不需要配置),故斗胆妄言vscode下配置CV之法。其实极为简单,图比较多所以很长。如果你看此文还配不好,你应该思考一下是不是自己的问题。闲话少说,直接开始。0.CMkae简介有的人到大二了都不知道cmake是什么,我不说是谁。CMake是一个开源免费并且跨平台的构建工具,可以用简单的语句来描述所有平台的编译过程。它能够根据当前所在平台输出对应的m

  8. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  9. C51单片机——实现用独立按键控制LED亮灭(调用函数篇) - 2

    说在前面这部分我本来是合为一篇来写的,因为目的是一样的,都是通过独立按键来控制LED闪灭本质上是起到开关的作用,即调用函数和中断函数。但是写一篇太累了,我还是决定分为两篇写,这篇是调用函数篇。在本篇中你主要看到这些东西!!!1.调用函数的方法(主要讲语法和格式)2.独立按键如何控制LED亮灭3.程序中的一些细节(软件消抖等)1.调用函数的方法思路还是比较清晰地,就是通过按下按键来控制LED闪灭,即每按下一次,LED取反一次。重要的是,把按键与LED联系在一起。我打算用K1来作为开关,看了一下开发板原理图,K1连接的是单片机的P31口,当按下K1时,P31是与GND相连的,也就是说,当我按下去时

  10. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

随机推荐