压板识别项目
零、相关说明:
首先进行一下相关说明。在“jsxyhelu.cnblogs.com/项目实战派”栏目里面出现的需求、图片和其他资源,都是我在浏览威客网站、论坛等网站的时候通过正规渠道获得的真实需求。个人觉得比较感兴趣,但是由于时间或者工作的冲突自己没有去接这些项目。但是由于这些需求都很有实现价值,所以过了一段时间,仍然拿出来练一练手,并且实现了核心模块。希望能够给浏览者一些启发。如果你认为这些图片和资源放在这里不合适,请及时联系我(1755311380@qq.com),我会及时处理的。此外,我会将核心代码和技术细节尽可能将清楚,我认为这样才是最有价值的方式。如果需要原始代码,也可以和我联系。
一、原始需求/图片:
1.图片为保护压板正常方式投入,要求图像识别能正确判断,并将保护压板投退状态显示出来。
2.保护压板漏投(误投),要求图像识别能发现报警,并显示出是哪些保护压板漏投。
3.保护压板有投退异常现象,要求图像识别能发现报警,并显示出是哪些保护压板异常。
4.通过保护盘二维码(或条形码),要能识别出来保护盘柜名称
(正常情况下的图片)
(存在问题的图片1)
(存在问题的图片2)
二、初步分析:
其实需求主要是两个方面,一个是识别出压板的情况,二个是识别条码。
2.1识别压板:
常见的图像初步处理(换色彩域->OSTU->投影分析),对于初始的三幅图像,都能够得到比较干净的结果。但是这种情况过于理想了,在实
际项目中,需要考虑到由于其他原因引起的噪音,可能需要采用一定的方法去除噪音。此外,第5和第9个开关,采用的是和背景颜色比
较接近的压板,在实际项目中容易混乱,但是这个结果是原始需求直接关注的结果,可能要采用其他方法来强化 。
(预处理结果)
(投影运算结果)
////第一个部分,获得各个压板区域的位置////
//经过ostu运算,得到质量比较好的图像
threshold(matSplit[1],ostu,100,255,THRESH_OTSU);
dstclone = ostu.clone();
// 做竖直的投影。这里由于更关心截断的情况,而不关心具体升降,所以是画出截断线而不是画波形
for (int i=0;i<ostu.cols;i++)
{
Mat data = ostu.col(i);
int itmp = countNonZero(data);
vectorV.push_back(itmp);
}
//上波形为VUpper,下波形为VDown
for (int i=1;i<vectorV.size();i++)
{
if (vectorV[i-1] == 0 && vectorV[i]>0)
{
VUpper.push_back(i);
}
if (vectorV[i-1]>0 && vectorV[i] == 0)
{
VDown.push_back(i);
}
}
//计算结果
for (int i=0;i<VUpper.size();i++)
{
Mat roitmp = ostu(Rect(VUpper[i],0,VDown[i]-VUpper[i],ostu.rows));
dilate(roitmp,roitmp,Mat());//对ostu的结果适当膨胀
int uppertimes = 0;
for (int j=0;j<ostu.rows;j++)
{
Mat data = roitmp.row(j);
int itmp = countNonZero(data);
vectorH.push_back(itmp);
}
for (int j=0;j<vectorH.size()-1;j++)
{
if (vectorH[j]>0 && vectorH[j+1] == 0)
{
HDower.push_back(j);
}
if (vectorH[j] == 0 && vectorH[j+1]>0)
{
HUpper.push_back(j);
}
}
if (HUpper.size() <= 1)
{
result[i] = 1;
//printf("结果为连在一起的
");
}
else
{
int iresult = 0;
for (int j=0;j<HDower.size()-1;j++)
{
//得出之间空白的区域
int iwidth = HUpper[j+1] - HDower[j];
if (iwidth > 10)
{
iresult = iresult +1;
}
}
if (iresult > 0 )
{
result[i] = 0;
// printf("结果为断开的
");
}
else
{
result[i] = 1;
//printf("结果为连在一起的
");
}
iresult = 0;
}
vectorH.clear();
HUpper.clear();
HDower.clear();
uppertimes = 0;
}
2.2条码识别:
一般条码识别都依赖于现有的库(包括二维码,可以参考以前写过的一篇‘单向图像信息传输系统’和'ZXing一维二维编码解码'),图像处理方面需要做的主要是把条码区域抠出来。由于条码它本身在设计的时候就是在竖直方向具备了冗余性,所以可以根据这个特性,采用形态学的方法将需要的区域采集出来。
(canny处理并且经过形态学强化)
(最终取出来的结果)
(条码解码)
////第二个部分,获得条码区域////
Mat sobel;
Mat canny;
Mat canny_output;
int imax = 0;
int imaxcontour = -1;
std::vector<std::vector<cv::Point>>contours;
Mat cannyClone= Mat::zeros(Size(gray.cols,gray.rows),gray.type());
Canny(gray,canny,100,255);
Mat element = getStructuringElement(MORPH_ELLIPSE,Size(7,3));
morphologyEx(canny,canny,CV_MOP_DILATE,element);
morphologyEx(canny,canny,CV_MOP_ERODE ,element);
findContours(canny,contours,CV_RETR_TREE,CV_CHAIN_APPROX_NONE);
for (int i=0;i<contours.size();i++)
{
int itmp = contourArea(contours[i]);
if (imaxcontour < itmp )
{
imax = i;
imaxcontour = itmp;
}
}
//找到轮廓的处理
Rect boundRect;//最小外接矩形
drawContours(cannyClone,contours,imax,Scalar(255),-1);
boundRect = boundingRect(Mat(contours[imax]));
Mat srcRoi = src(boundRect);
imwrite("barcode.jpg",srcRoi);
三、难点攻关:
3.1第5和第9压板的特殊情况。
由于第5和第9压板的颜色和背景颜色非常接近,所以采用特殊的方法来进行处理。通过观察,结合常理。压板打开之后,必然带来的结果就
是下垂并且将下面的字符牌遮挡。那么可以通过反过来判断字符牌是否被遮盖来判断压板是否被打了下来。
(字符遮挡)
//将第5和第9条单独取出来
Mat roi05 = src(Rect(6*VUpper[4],0,6*(VDown[4]-VUpper[4]),src.rows));
Mat roi09 = src(Rect(6*VUpper[8],0,6*(VDown[8]-VUpper[8]),src.rows));
cvtColor(roi05,roi05,CV_BGR2GRAY);
cvtColor(roi09,roi09,CV_BGR2GRAY);
threshold(roi05,roi05,100,255,THRESH_OTSU);
threshold(roi09,roi09,100,255,THRESH_OTSU);
threshold(roi05,roi05,0,255,THRESH_BINARY_INV);
threshold(roi09,roi09,0,255,THRESH_BINARY_INV);
std::vector<std::vector<cv::Point>>contours2;
findContours(roi05,contours2,CV_RETR_LIST,CV_CHAIN_APPROX_SIMPLE);
int imax2 = 0;
int imaxcontour2 = -1;
for (int i=0;i<contours2.size();i++)
{
int itmp = contourArea(contours2[i]);
if (imaxcontour2 < itmp )
{
imax2 = i;
imaxcontour2 = itmp;
}
}
Rect boundRect2;//最小外接矩形
boundRect2 = boundingRect(Mat(contours2[imax2]));
if (boundRect2.y+boundRect2.height >750)
{
//printf("第5个为打开的
");
result[4] = 0;
}
else
{
//printf("第5个为关闭的
");
result[4] = 1;
}
contours2.clear();
findContours(roi09,contours2,CV_RETR_LIST,CV_CHAIN_APPROX_SIMPLE);
imax2 = 0;
imaxcontour2 = -1;
for (int i=0;i<contours2.size();i++)
{
int itmp = contourArea(contours2[i]);
if (imaxcontour2 < itmp )
{
imax2 = i;
imaxcontour2 = itmp;
}
}
boundRect2 = boundingRect(Mat(contours2[imax2]));
if (boundRect2.y+boundRect2.height >750)
{
//printf("第9个为打开的
");
result[8] = 0;
}
else
{
//printf("第9个为关闭的
");
result[8] = 1;
}
3.2倾斜情况下,条码识别错误
受到摄像头分辨率的限制,使得图像中条形码在倾斜的时候,受到插值算法的影响,边缘变得模糊。虽然尝试了许多方法,但是都无法达到
能够让zxing识别的程度。这个问题不知道哪位有更好的方法,如果可以的话,希望能够告之。
四、系统集成:
由于目前还没有很好地将zxing集成到mfc的环境中来。由于我对“csharp通过dll方式调用console程序”比较熟悉,所以这里尝试采用的是"csharp通过程序调用console的形式"。也就是主要图像处理的部分还是写的console程序,并且运算出相应的结果和图片,而后在csharp的程序中合并得到最后的结果。过程中发现这种方法的问题还是比较多的,包括参数的传递、程序重复运行时的控制等,应该说不是一种很成熟的方法,在以后面对类似的问题的时候,最好是能够直接将代码集成到mfc中,否则就要采用“csharp通过dll方式调用console程序”的方式。
最后的结果如下,并且可以多次测试都没有问题:
五、设计小结:
工作完成了,那么除了对代码进行重构并且提取出可以被重复使用的函数外,对于思路的小结也非常重要。在本例中:
5.1 提出了具有创造性的一个想法:采取分析下面的字符牌是否被遮挡的方式来判断开关闭合情况。逆向思维取得了稳定的结果;
5.2 对于色彩空间转换、对于投影的灵活运用构成了识别的主体。
不足的地方
5.3 对于一维/二维码识别没有构建稳定的库或解决方案,现在使用的zxing可以解决一部分问题,但是不过不了解原理,遇到不能解决的问题就无法继续优化;目前提取条码的方法应该被提出出来。
5.4 验证了"csharp通过程序调用console的形式"是不合算的。