opencv——机器视觉检测和计数
引言
在机器视觉中,有时需要对产品进行检测和计数。其难点无非是对于产品的图像分割。
由于之前网购的维生素片,有时候忘了今天有没有吃过,就想对瓶子里的药片计数...在学习opencv以后,希望实现对于维生素片分割计数算法。本次实战在基于形态学的基础上又衍生出基于距离变换的分水岭算法,使其实现的效果更具普遍性。
基于形态学的维生素片检测和计数
整体思路:
- 读取图片
- 形态学处理(在二值化前进行适度形态学处理,效果俱佳)
- 二值化
- 提取轮廓(进行药片分割)
- 获取轮廓索引,并筛选所需要的轮廓
- 画出轮廓,显示计数
opencv实现:
- int main(int argc, char** argv)
- {
- Mat src, src_binary,dst,src_distance;
- src = imread("D:/opencv练习图片/维生素片机器视觉检测和计数.png");
- imshow("原图片", src);
- Mat kernel = getStructuringElement(MORPH_RECT, Size(16, 16), Point(-1, -1));
- morphologyEx(src, dst, MORPH_OPEN, kernel);
- imshow("形态学",dst);
- cvtColor(dst, dst, COLOR_RGB2GRAY);
- threshold(dst, src_binary, 100, 255, THRESH_OTSU);
- imshow("二值化", src_binary);
- vector<vector<Point>> contours;
- findContours(src_binary, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE, Point(0, 0));
- RNG rng(12345);
- double area;
- Point2i PL;
- for (size_t i = 0; i < contours.size(); i++)
- {
- area = contourArea(contours[i]);
- if (area < 500)continue;
- PL = contours[i].front();
- Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
- drawContours(src, contours, i, color, 2, 8);
- putText(src, to_string(i), PL, FONT_HERSHEY_COMPLEX, 1, color, 2);
- }
- imshow("计数结果", src);
- waitKey(0);
- return 0;
- }
效果展示:
由上图可以看的,原图在经过形态学处理后,可以去除很多细节,简化后续的药片分割操作。
但是在计数结果图上发现,索引17号药片并没有完全分割(实际上修改形态学的结构元素尺寸(改为20*20)也可以完全分离这两个药片)。
这不由得让我们思考,如果简单的形态学处理分割不了药片呢?
对于复杂的产品图片,我们可以使用基于距离变换的分水岭算法对其分割。
基于距离变换的分水岭算法检测和计数
OpenCV 采用了基于标记点的分水岭算法,在这种算法中我们要设置哪些山谷点会汇合,哪些不会。这是一种交互式的图像分割。我们要做的就是给我们已知的对象打上不同的标签(即添加注水点)。然后实施分水岭算法。每一次灌水,我们的标签就会被更新,当两个不同颜色的标签相遇时就构建堤坝,直到将所有山峰淹没,最后我们得到的边界对象(堤坝)的值为 -1。
对于如何打上标签(即添加注水点)有两种办法:
opencv中,对于一张二值化的图像,后续处理方式有两种。第一种方式就是利用findContours、drawContours等函数进行轮廓分析(opencv以对轮廓的处理为主)。第二种方式就是计算连通域进行区域分析。
第一种(基于轮廓):在二值化后,对图像寻找轮廓findContours,筛选出注水区域轮廓,然后通过drawContours对轮廓标记。
第二种(基于区域):在二值化后,先对寻找图像中的前景图(即注水点),再寻找到背景图(进行膨胀),最后找到未知区域(背景减去前景,得到边缘图),通过connectedComponents()获取标记点。
相关API:
- 分水岭函数watershed函数原型
- void watershed( InputArray image, InputOutputArray markers );
第一个输入参数 image,必须是CV_8UC3类型图像。
第二个输入/输出参数markers必须是32位单通道图像。和image尺寸一样。包含不同区域的轮廓,每个轮廓有一个自己唯一的编号。
在执行watershed函数后,算法会根据markers传入的轮廓作为种子,对图像上其他的像素点根据分水岭算法规则进行判断,并对每个像素点的区域归属进行划定,直到处理完图像上所有像素点。而区域与区域之间的分界处的值被置为“-1”,以做区分。
- 距离变换函数distanceTransform函数原型
距离变换运算用于计算二值化图像中的每一个非零点距自己最近的零点的距离,距离变换图像上越亮的点,代表了这一点距离零点的距离越远。
距离变换通常用于求解图像的骨骼和查找物体的质心(即获取距离变换的极大值)和计算非零像素到最近零像素点的最短距离。
- distanceTransform( InputArray src, OutputArray dst, int distanceType, int maskSize,
int dstType = CV_32F
);
第一个输入参数src,必须是CV_8UC1类型的二值图像(只有0或1)
第二个输出参数dst,表示的是计算距离的输出图像,输出类型是CV_32F/CV_8U的单通道图像,大小与输入图片相同。
第三个参数distanceType,表示的是选取距离的类型,可以设置为DIST_L1,DIST_L2,DIST_C
第四个参数maskSize,表示的是距离变换的掩膜模板,可以设置为3,5(常用3)
第四个参数dstType,表示输出类型,可选择CV_32F/CV_8U
注:若输出类型为CV_32F,想要显示距离变换后的骨架图像,需要对其归一化。(normalize)
先来看看第一种标记mark(基于轮廓)的方法:
(一)读入图像,形态学,二值化(消除噪声)
Mat src, src_binary, dst, src_distance;- src = imread("D:/opencv练习图片/维生素片机器视觉检测和计数.png");
- imshow("原图片", src);
- Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
- morphologyEx(src, dst, MORPH_OPEN, kernel);
- imshow("形态学", dst);
- cvtColor(dst, dst, COLOR_RGB2GRAY);
- threshold(dst, src_binary, 100, 255, THRESH_OTSU);
- imshow("二值化", src_binary);
(二)距离变换(归一化显示),再二值化
- distanceTransform(src_binary, src_distance, DIST_L2, 3, 5);
- normalize(src_distance, src_distance, 0, 1, NORM_MINMAX);
- imshow("距离变换", src_distance);
- threshold(src_distance, src_distance, 0.4,1, THRESH_BINARY);
- imshow("再二值化", src_distance);
经过距离变换后的二值化,可以清晰看到,药片以及完全分割开来。
(三)打上标签(添加注水点),基于轮廓
- //寻找标记点marsk的轮廓信息 也就是分水岭的水坝
- src_distance.convertTo(src_distance, CV_8UC1);
- vector<vector<Point>> contours;
- findContours(src_distance, contours, RETR_TREE, CHAIN_APPROX_SIMPLE);
- //创建maker
- Mat markers = Mat::zeros(src.size(), CV_32S);// //因为分水岭后的边缘存储是-1,所以必须使用有符号的CV_32S
- for (size_t t = 0; t < contours.size(); t++)
- {
- drawContours(markers, contours, static_cast<int>(t), Scalar(static_cast<int>(t) + 1), -1);//轮廓数字编号
- }
- circle(markers, Point(5, 5), 30, Scalar(255), -1);//关键代码(mark做一个小标记)
- int index1 = 0;
- //打印轮廓数据 有值的均为轮廓线
- for (int row = 0; row < markers.rows; row++)
- for (int col = 0; col < markers.cols; col++)
- {
- index1 = markers.at<int>(row, col);
- cout << index1 << ",";
- }
部分标签markers轮廓数据截图,可以看到0代表背景,轮廓线用正数索引标识。
(四)进行分水岭操作,并给分水岭后的区域随机上色,并打印出检测的药片个数。
- // 生成随机颜色
- vector<Vec3b> colors;
- for (size_t i = 0; i < contours.size(); i++) {
- int r = theRNG().uniform(0, 255);
- int g = theRNG().uniform(0, 255);
- int b = theRNG().uniform(0, 255);
- colors.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
- }
- // 颜色填充与最终显示
- Mat dst1 = Mat::zeros(markers.size(), CV_8UC3);
- int index = 0;
- for (int row = 0; row < markers.rows; row++) {
- for (int col = 0; col < markers.cols; col++) {
- index = markers.at<int>(row, col);
- if (index > 0 && index <= contours.size()) {
- dst1.at<Vec3b>(row, col) = colors[index - 1];
- }
- else {
- dst1.at<Vec3b>(row, col) = Vec3b(0, 0, 0);
- }
- }
- }
- imshow("结果显示", dst1);
- printf("药片检测个数: %d\n", contours.size());
再来看看第二种标记mark(基于区域)的方法:
(一)读入图像,形态学,二值化(消除噪声)
- Mat foreground, background, unkonwn;//创建前景,背景,未知区域
- Mat src, src_binary, dst, src_distance;
- src = imread("D:/opencv练习图片/维生素片机器视觉检测和计数.png");
- imshow("原图片", src);
- Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
- morphologyEx(src, dst, MORPH_OPEN, kernel);
- imshow("形态学", dst);
- cvtColor(dst, dst, COLOR_RGB2GRAY);
- threshold(dst, src_binary, 100, 255, THRESH_OTSU);
- imshow("二值化", src_binary);
- (二)对二值化图像进行膨胀操作,得到大部分是背景的图片
- //得到背景图片
- dilate(src_binary, background, kernel, Point(-1, -1), 3);
- imshow("背景图片", background);
(三)通过对二值图像距离变换得到前景图片(即注水点)
- //距离变换
- distanceTransform(src_binary, src_distance, DIST_L2, 3, 5);
- imshow("距离变换", src_distance);
- normalize(src_distance, src_distance, 0, 255, NORM_MINMAX);
- double my_minv = 0.0, my_maxv = 0.0;
- minMaxIdx(src_binary, &my_minv, &my_maxv);
- threshold(src_distance, foreground, 0.4 * my_maxv, 255, THRESH_BINARY);
- foreground.convertTo(foreground, CV_8U);
- imshow("前景图片", foreground);
(四)通过背景与前景的差值,得到未知区域(即边缘所在区域)
- //得到未知区域
- unkonwn = background - foreground;
- imshow("未知区域", unkonwn);
(五)得到这些区域以后,我们可以获取注水点的标签,通过connectedComponents实现(即获取markers标签)
- //创建标记点markers
- Mat markers = Mat(src.size(), CV_32S);
- int num = connectedComponents(foreground, markers, 8);
- cout << num << endl;
- markers = markers + 1;
- for (int i = 0; i < unkonwn.rows; i++)
- {
- for (int j = 0; j < unkonwn.cols; j++)
- {
- if (((int)unkonwn.at<uchar>(i, j)) == 255)
- {
- markers.at<signed int>(i, j) = 0;
- }
- }
- }
详细理解该步骤:
现在我们已经知道哪些是背景,哪些是药片(前景区域)。
因此我们可以创建一个标签(和原图大小,类型为CV_32S),通过connectedComponents函数对前景区域进行标记
该函数会对前景区域连通域分析,并将背景设定为0,其他区域从1开始正整数标记(这就是我们的种子,水漫时会从这里漫出),结果返回给markers。
但是对于分水岭算法,会将为0的区域认为是未知区域,因此要markers整体加一。
(六)进行分水岭操作,并显示边缘
- watershed(src, markers);
- for (int row = 0; row < markers.rows; row++)
- {
- for (int col = 0; col < markers.cols; col++)
- {
- if (markers.at< int>(row, col) == -1)
- {
- src.at<Vec3b>(row, col) = Vec3b(0, 0, 255);
- }
- }
- }
- imshow("结果", src);
由于分水岭算法会将找到的边缘在markers置为-1,因此我们对原图操作,将索引为-1的位置的像素值改为红色(即显示边缘)。
参考链接:OpenCV---分水岭算法 - 山上有风景 - 博客园 (cnblogs.com)
(8条消息) c++和opencv小知识:基于距离变换的分水岭算法(固定流程)_梦游城市的博客-CSDN博客
(8条消息) OpenCV分水岭算法图像分割_冰冰bing的博客-CSDN博客
opencv——机器视觉检测和计数的更多相关文章
- 基于OpenCV制作道路车辆计数应用程序
基于OpenCV制作道路车辆计数应用程序 发展前景 随着科学技术的进步和工业的发展,城市中交通量激增,原始的交通方式已不能满足要求:同时,由于工业发展为城市交通提供的各种交通工具越来越多,从而加速了城 ...
- opencv直线检测在c#、Android和ios下的实现方法
opencv直线检测在c#.Android和ios下的实现方法 本文为作者原创,未经允许,不得转载 :原文由作者发表在博客园:http://www.cnblogs.com/panxiaochun/p/ ...
- OPENCV条形码检测与识别
条形码是当前超市和部分工厂使用比较普遍的物品,产品标识技术,使用摄像头检测一张图片的条形码包含有两个步骤,第一是定位条形码的位置,定位之后剪切出条形码,并且识别出条形码对应的字符串,然后就可以调用网络 ...
- OpenCV矩形检测
OpenCV矩形检测 需求:提取图像中的矩形,图像存在污染现象,即矩形区域不是完全规则的矩形. 思路一:轮廓法 OpenCV里提取目标轮廓的函数是findContours,它的输入图像是一幅二值图像, ...
- keras系列︱人脸表情分类与识别:opencv人脸检测+Keras情绪分类(四)
引自:http://blog.csdn.net/sinat_26917383/article/details/72885715 人脸识别热门,表情识别更加.但是表情识别很难,因为人脸的微表情很多,本节 ...
- OpenCV绘制检测结果
OpenCV绘制检测结果 opencv rtcp timestamp 一.介绍 由于在验证阶段,使用FPGA时我们的算法检测速度很慢,没法直接在主流上进行绘图,否则的话,主流就要等待算法很久才能 ...
- OpenCV人形检测Hog
#include "iostream" #include "queue" using namespace std; #include "opencv2 ...
- opencv人脸检测分类器训练小结
这两天在初学目标检测的算法及步骤,其中人脸检测作为最经典的算法,于是进行了重点研究.该算法最重要的是建立人脸检测分类器,因此我用了一天的时间来学习分类器的训练.这方面的资料很多,但是能按照一个资料运行 ...
- opencv行人检测里遇到的setSVMDetector()问题
参考了博客http://blog.csdn.net/carson2005/article/details/7841443 后,自己动手后发现了一些问题,博客里提到的一些问题没有解决 ,是关于为什么图像 ...
随机推荐
- java四种字符串拼接方式
1.直接用"+"号 2.使用String的方法concat 3.使用StringBuilder的append 4.使用StringBuffer的append
- 第20 章 : GPU 管理和 Device Plugin 工作机制
GPU 管理和 Device Plugin 工作机制 本文将主要分享以下几个方面的内容: 需求来源 GPU 的容器化 Kubernetes 的 GPU 管理 工作原理 课后思考与实践 需求来源 201 ...
- html+css写出响应式侧边导航栏
html部分:先写用div画好六个导航的卡片,再利用css添加响应效果 <div class='card-holder'> <div class='card-wrapper'> ...
- VS2019 自定义项目模板
前言: 使用"宇宙最强IDE"开发项目时,都需要根据不同情况选择一个项目模板,来满足开发需求:如下 VS为我们提供了基础的项目模板,但现有项目模板未包含基础功能如:日志输出.审计日 ...
- [Fundamental of Power Electronics]-PART I-3.稳态等效电路建模,损耗和效率-3.4 如何获得模型的输入端口
3.4 如何获得模型的输入端口 Fig 3.16 Buck converter example 让我们尝试使用3.3.3节的步骤来推导图3.16所示的Buck变换器的模型.电感绕组电阻同样由串联电阻\ ...
- 如何从 dump 文件中提取出 C# 源代码?
一:背景 相信有很多朋友在遇到应用程序各种奇葩问题后,拿下来一个dump文件,辛辛苦苦分析了大半天,终于在某一个线程的调用栈上找到了一个可疑的方法,但 windbg 常常是以 汇编 的方式显示方法代码 ...
- 关于Green AI
上一篇文章提到了模型不环保这个话题.这篇文章就这个问题展开唠叨一下. 自从BERT, GPT此类的大型模型诞生以来,小作坊们除了把pre-trained的模型拿过来微调一下,就束手无策了,因为成本实在 ...
- 孙悟空的身外身法术使用了Java设计模式:原型模式
目录 定义 意图 主要解决问题 何时使用 优缺点 结构 简单形式的原型模式 登记形式的原型模式 两种形式比较 浅克隆和深克隆 孙悟空的身外身法术 浅克隆实现 深克隆实现 定义 原型模式属于对象的创建型 ...
- 《C++编程思想》部分章节学习笔记整理
简介 此笔记为<C++编程思想>中部分章节的学习笔记,主要是第15章--多态性和虚函数 的学习笔记,此外还有少量其他章节的内容. 目录 文档:<C++编程思想>
- Day14_76_反射与静态语句块
反射与静态语句块 * 获取class对象与静态语句块的关系 package com.shige.Reflect; import java.nio.channels.ClosedSelectorExce ...