在这篇博客中,我们对目前程序中一个隐藏很深的BUG进行处理,这个BUG导致程序目前有一部分逻辑出现错误(虽然没有表现出来)。
一、触发BUG
1、准备触发样本
为了复现这个隐藏的BUG,需要实现准备两张测试样本,一张是彩色图(三通道图),一张是灰度图(单通道图):
临时读入这两个图像,验证其属性:
注意此时程序能够正常读取这两个图片,不会崩溃。
2、修改代码,触发BUG
解析来我们修改“图片文件夹”按钮对应事件响应函数中图像读取的代码,这里由于这是为了复现BUG,只修改一个模式下的读取函数即可,假设我们修改“图片文件”模式下的读取函数:
我们这里就是修改了cvLoadImage()的在读取图片时的标志位,“-1”代表原始读取方式,即在读取时不会改变图像的任何属性(如通道数,位数等),这种更改看上去是很合理的,因为我们应该保证加载到的图像就是图像本身,在加载图像的过程中对图像进行预处理本身是不合理的,因为我们会编写专门的图像预处理代码。
此时,在“图片文件”模式下读取彩色图像进行性别识别,一切正常。然后我们在“图片文件”模式下读取灰度图(单通道图),程序崩溃了:
接下来开始追踪定位这个BUG。
二、定位第一个BUG
出现这个错误之后,首先判断是否在图像加载的过程中出了问题。在cvLoadImage()函数处添加一个断点,F5调试运行:
程序运行到断点处后,按下F10,运行断点对应的程序语句,发现图片加载正常:
继续按F10,运行下面的人脸检测函数(detect_and_draw),程序崩溃,因此可以确定BUG出在detect_and_draw函数中。
detect_and_draw函数中,最复杂的操作莫过于是人脸检测函数cvHaarDetectObjects了,因此这里出现BUG的可能性也非常大,在这里设置一个断点,F5运行,程序崩溃,说明BUG在这条语句之前。
此时我们有理由怀疑是直方图均衡函数cvEqualizeHist()出了问题,在这个语句前面打一个断点,F5运行,程序依然崩溃,说明前面还有错误。
此时detect_and_draw()函数还剩三行代码需要检测,不妨将断点设在函数开始的位置,F5运行,程序正常进入函数体:
按F10单步运行,意外发现程序准备执行cvCvtColor()语句,这条语句是对图像进行灰度化处理,但很显然我们选择的图片本身就是灰度图,也就是单通道图,对灰度图进行灰度化,程序自然崩溃,第一个BUG算是找到了:
至于这个BUG出现的原因,想必大家此时都已经看出来了,由于我之前粗心,将if的判断条件写成了赋值语句if (img->nChannels = 3),导致这个条件永远为真,以至于不管是否为彩色图,都会执行if内的语句,程序崩溃,气人的是当输入图像本身就是彩色图时,这个BUG并不会显现,隐藏很深。
三、定位第二个BUG
修改if条件为“if (img->nChannels == 3)”,运行程序,选择灰度图像进行性别识别,程序再次崩溃。
通过之前的方法我们很快确定BUG依然出在detect_and_draw函数中,同样我们先检查其中的cvHaarDetectObjects()函数,经过“设置断点—>F5调试—>F10单步运行”的调试方式,很快确定这个函数能够正常运行:
接下来我们有理由推测是性别识别函数GenderRecognition()出现问题,在这里打上断点,F5运行,程序崩溃,说明问题在前面。
此时我们能够确定BUG出在cvHaarDetectObjects()函数和GenderRecognition()函数之间,这部分包含两段代码,一段代码是用来统计人脸检测结果中面积最大的矩形标号,另一段代码是用来绘制矩形检测结果。将断点设在“if(objects->total > 0)”处,F5调试运行程序,程序能够正常命中断点,说明前面的代码都没问题。
F10单步运行断点之后的代码,在这里又发现一处if (img->nChannels = 3)的错误,赶紧将其改正。
再次F5运行程序,命中断点,F10单步运行,发现程序居然顺利通过GenderRecognition()函数,似乎程序BUG已经消失,继续F10单步运行,程序在“ cvReleaseImage(&gray);”语句处发生错误,很明显句代码有问题。
四、终极BUG的发现
首先来解决“cvReleaseImage(&gray);”这句代码的BUG。这是IplImage结构体所特有的释放内存的语句,IplImage类型的变量在生命周期结束之后需要通过这句代码来显示的释放掉内存,而OpenCv2.x中的Mat类型则不需要这部操作(封装了智能指针),所以从这点来讲Mat类还是要优于IplImage结构体的。如果这句代码出现问题,很大一部分原因就是待释放的对象不存在(或者说是已经释放过),我们在这行代码处设置一个断点,F5运行程序,果然,变量gray已经被清空:
那我们是在什么时候不经意间把这个IplImage变量释放掉的呢?最值得怀疑的就是上面的那句“cvReleaseImage(&faceImage);”代码,很可能是这句代码在释放faceImage变量时连同gray变量也一起释放掉了,我们将断点放在这行代码上,验证我们的猜想,从下图中可以看到,在执行这句代码之前,faceImage和gray两个变量对应的图像均不为空:
程序运行到这里细心的朋友应该能够发现问题了,那就是faceImage所存储的图片居然和img变量所对应的图片是一样的!这显然是不合道理的,因为理论上faceImage中保存的应该是分割出来的人脸图像,然后把这个图像送入GenderRecognition(faceImage)函数中进行性别识别,可现在的情况却是faceImage保存了原始图片,并且是经过直方图均衡化的原始图片,也就意味着此时进行性别识别的是全部图像而非人脸部分,这种情况下的性别识别毫无道理可言,这就是所谓的终极BUG。
五、修改
首先,我们分析这个BUG出现的原因,理论情况下是通过检测到的人脸矩形,在原图(img)的基础上进行人脸区域分割,将分割得到的人脸赋值给faceImage变量,很明显这部分代码现在出了问题:
为什么会出问题呢?仔细分析这段代码,不难看出,如果当前图片是彩色图片,则“img->nChannels == 3”条件为真,执行cvCvtColor()函数,且我们已经为img图像变量指定了ROI区域,因此在灰度化操作中会只对ROI区域的图像进行操作,因此这步灰度化操作也间接的完成了ROI区域(也就是人脸区域)的分割;而目前的图片是灰度图片,“img->nChannels == 3”条件为假,执行“faceImage = img;”语句,问题是这种直接用等号连接的赋值语句属于“弱拷贝”,也就是只拷贝指针指向地址,但两个变量仍共享一段内存区域,通俗的将就是“faceImage ”和“img”这两个变量本质上都代表了同一个内存中的图像,这也就解释了为什么我们在释放完faceImage之后,连同img、gray这两个变量也一起释放掉了,因为它们三个之间都是通过这种“弱拷贝”的关系联系在了一起,都代表着同一图像。
更严重的问题就是在进行“弱拷贝”的过程中,ROI区域是无效的,因此在这种情况下人脸分割失败。
很明显,消除这种BUG的最有效的方法就是将“弱拷贝”替换为深度拷贝,即两个变量代表两个图像,指向两个不同的内存地址,这样就不会因为位置的连带关系而引发莫名的BUG。IplImage对应的深度拷贝函数有两个:void cvCopy( const CvArr* src, CvArr* dst, const CvArr* mask=NULL )和IplImage* cvCloneImage( const IplImage* image )。其中cvCopy在拷贝过程中只拷贝设置的ROI区域,正好符合我们的需求,因此在这里采用这个函数来替换之前的“浅拷贝”:
以及:
此时再次运行程序,发现faceImage中正常保存的分割后的人脸图像:
至此,这个重大BUG修改完成。
六、总结
在这篇博文中讲述了一个发现BUG、定位BUG、分析BUG、解决BUG的完整过程,希望对大家有所帮助。在下一篇博客中,开始引入摄像头。