一、前言

为了挑战一下OpenCV的学习成果,最经一直在找各类项目进行实践。机缘巧合之下,得到了以下的需求:

要求从以下图片中找出所有的近似矩形的点并计数,重叠点需要拆分单独计数。

二、解题思路

1.图片作二值化处理

auto image = cv::imread("points.jpg");
cv::Mat border;
// 为了将图片边缘的点加入计算,将图片整体扩大十个单位
cv::copyMakeBorder(image, border, 10, 10, 10, 10, cv::BORDER_CONSTANT, cv::Scalar(255, 255, 255)); // 灰度化
cv::Mat gray;
cv::cvtColor(border, gray, cv::COLOR_BGR2GRAY);
//二值化
cv::Mat binary;
cv::threshold(gray, binary, 10, 255, cv::THRESH_BINARY);

得到以下结果:

2.腐蚀

可以看到二值化后多数点的形状残缺。会对轮廓查找造成影响,主要就是锯齿和空洞。为了解决这个问题,可以采用腐蚀方法,得到边缘较为光滑的点。

cv::Mat erosion;
cv::Mat kernel = cv::Mat(3, 3, CV_8UC1, cv::Scalar(1));
cv::erode(binary, erosion, kernel, cv::Point(-1, -1), 1) ; // 腐蚀

3.提取所有点的轮廓并计数

std::vector<cv::Mat>contours;
cv::findContours(erosion, contours, cv::RETR_TREE,cv::CHAIN_APPROX_SIMPLE) ;// 轮廓提取

用黄色笔迹绘制出所有的轮廓,如下图所示:

至此,基本能够得到点的数量了,前提是没有重叠点,直接获取contours的数量即可。当然重叠点的问题肯定要解决。接下来采用面积判断法解决该问题。

重叠部分探索

从图中我们可以看出,大部分独立点的面积是近似的,重叠点处的点数基本可以近似于重叠处的面积除以标准独立点的面积。此外,采用面积法也可以过滤一些面积较小的噪点。

1.标准独立点面积的获取

如何找到那个较为标准的点的面积呢?可以使用众数作为标准值。但由于点面积的离散性,不一定能够找到一个完美的众数。所以这里写了一个分组找中位数的方法来找这个标准值。

首先,计算出各个轮廓的面积,并过滤噪点,此处小于30的将视为噪点:

std::vector<double> listArea;
std::vector<cv::Mat> listContour;
std::vector<double> listLength;
auto count = contours.size();
for(int i = 1; i < count; i++)
{
auto contour = contours[i];
auto area = cv::contourArea(contour);
if (area < 30)
continue;
listArea.push_back(area);
listContour.push_back(contour);
auto arcLen = cv::arcLength(contour, true);
listLength.push_back(arcLen);
}

其次,将所有面积分组,分组个数等于总个数的平方根:

int groupByStep(const nc::NdArray<double> &nclt, QMap<double, QList<double>> &map)
{
auto minV = nc::min(nclt)[0];
auto maxV = nc::max(nclt)[0];
auto range = maxV - minV;//极差
auto count = nclt.size();
auto group = sqrt(count);//分组数量
int step = range / group;//分组步长 for(size_t i = 0; i < count; i++)
{
auto v = nclt[i];
auto key = (int)((v - minV) / step);
map[key].push_back(v);
}
return group;
}

接着,找到数量最大的组的索引:

int findMaxCountIdx(QMap<double, QList<double>> &map)
{
int maxIdx = 0;
auto maxCount = 0;
auto ks = map.keys();
auto count = ks.size();
for(int i = 0; i < count; i++)
{
auto k = ks[i];
auto vs = map.value(k);
if(maxCount < vs.size())
{
maxCount = vs.size();
maxIdx = i;
}
qDebug() << __FUNCTION__ << k << vs.size();
}
return maxIdx;
}

再者,以上一步的索引为起始点,逐步探求前后的索引组,直至索引组中点数大于总点数的一半:

/**
* @brief accumulate 累加获取选中分组中点的总个数
* @param idxs
* @param vss
* @return
*/
int accumulate(QList<int> &idxs, QList < QList<double >> &vss)
{
auto c = 0;
foreach (auto idx, idxs)
{
c += vss[idx].size();
}
return c;
}
/**
* @brief findMoreThanHalfIdxs 获取分组点数量和大于总点一半时的索引
* @param idxs
* @param count
* @param vss
*/
void findMoreThanHalfIdxs(QList<int> &idxs, int count, QList < QList<double >> &vss)
{
QList<int> idxsTmp = idxs;
auto l = idxs.first() - 1;
auto r = idxs.last() + 1;
auto count_2 = count / 2;
auto lb = false;
auto rb = false; if(l > -1)
{
idxsTmp.insert(0, l);
auto cnt = accumulate(idxsTmp, vss);
if(cnt > count_2)
{
lb = true;
}
}
if(r < count)
{
idxsTmp << r;
auto cnt = accumulate(idxsTmp, vss);
if(cnt > count_2)
{
rb = true;
}
}
if(lb && rb)
{
auto lc = vss[l].size();
auto rc = vss[r].size();
if(lc > rc)
{
idxs.insert(0, l);
}
else
{
idxs << r;
}
}
else if (lb)
{
idxs.insert(0, l);
}
else if (rb)
{
idxs << r;
}
else
{
idxs.insert(0, l);
idxs << r;
findMoreThanHalfIdxs(idxs, count, vss);
}
}

最后,从索引组中找到中位数,即是我们要找的标准值:

/**
* @brief majority 舍弃极值后再求中位数
* @param lt
* @return
*/
double majority(std::vector<double> &lt)
{
if(!lt.size())return 0;
auto count = lt.size();
std::vector<double> sortLt = lt;
std::sort(sortLt.begin(), sortLt.end(), std::less<double>());//排序
auto nclt = nc::NdArray<double>(sortLt);
QMap<double, QList<double>>map;//分组容器
groupByStep(lt, map);
auto idx = findMaxCountIdx(map);//个数最多的组的索引
auto vss = map.values();
QList<int> idxs{idx};
findMoreThanHalfIdxs(idxs, count, vss);//以idx为起点,搜寻数量过半的索引
QList<double> idxVs;//数量过半的值
foreach (auto idx, idxs)
{
idxVs << vss[idx];
}
std::sort(idxVs.begin(), idxVs.end(), std::less<double>());//排序
std::vector<double> idxVsStd;
toStd(idxVs, idxVsStd);
nclt = nc::NdArray<double>(idxVsStd);
auto ret = nc::median(nclt)[0];//中位数
return ret;
}

2.重叠处点个数的获取

在上一步,我们获取了标准面积auto stdArea = majority(listArea);。接下来只用作简单的除法运算就可以得到点的个数:

 auto countAlone = 0;
auto countLinked = 0;
count = listArea.size();
for(size_t i = 1; i < count; i++)
{
auto area = listArea[i];
auto contour = listContour[i];
auto c = getRectCount(area, stdArea, contour);
auto m = cv::moments(contour);
auto cX = int(m.m10 / m.m00);
auto cY = int(m.m01 / m.m00);
cv::putText(border, QString::number(c).toStdString(), cv::Point(cX, cY),
cv::FONT_HERSHEY_COMPLEX, 0.5, cv::Scalar(0, 100, 255), 1);
auto rect = cv::minAreaRect(contour);
if (c == 1)
countAlone += c;
else
countLinked += c;
cv::drawContours(border, listContour, i, cv::Scalar(0, 255, 255), 1);
// 获取最小外接矩形的4个顶点坐标(ps: cv2.boxPoints(rect) for OpenCV 3.x)
cv::Mat box;
cv::boxPoints(rect, box);
cv::Mat boxInt;
box.convertTo(boxInt, CV_32S);
cv::drawContours(border, std::vector<cv::Mat> {boxInt}, 0, cv::Scalar(255, 0, 0), 1);
}
cv::imshow("contoursImg", border);
cv::waitKey();

在以上的步骤中,我们得筛选出独立点,将他们排除在重叠点的个数计算之外。否则,独立点的个数大概率将被计算为:0.6、1.5、1.2...这样的值,会对最终结果产生极大的影响。独立点采用以下方法判断:

bool isOneRectPoint(double &area, cv::Mat &contour)
{
auto rect = minAreaRect(contour);//最小外接矩形
auto areaRect = rect.size.height * rect.size.width;//矩形面积
auto scale = rect.size.height / rect.size.width;//矩形长宽比。假设矩形是正方形,该值接近1
scale = std::abs(scale - 1);//为了直观判断,此处减1。假设矩形是正方形,那么该值接近0
auto scaleArea = area / areaRect;//假设矩形是独立矩形,该值接近1
scaleArea = std::abs(scaleArea - 1);//为了直观判断,此处减1。假设矩形是独立矩形,该值接近0
return scale < 0.1 && scaleArea < 0.2;//两个判断都接近0,说明该矩形是个独立矩形
}

至于点个数,采用以下方法简单计算即可:

int getRectCount(double &area, double &stdArea, cv::Mat &contour)
{
auto isRect = isOneRectPoint(area, contour);
if (isRect)
return 1;
auto c = std::max(1.0, round(area / stdArea));
return c;
}

三、总结

以上是个人对该问题的一些思考与实践。运用到了一些图像处理、几何、统计等相关的知识,此处既作为记录,也更加希望该方法对您有所帮助。困难点在于重叠处点个数的统计,本文采用面积法计算个数,是一种较为简单的方法。当然,如果有更加巧妙的方法,欢迎探讨交流。

OpenCV计数应用 c++(QT)的更多相关文章

  1. Opencv + opencv_contrib + Tesseract 之Qt开发环境搭建

    1.软件包准备 opencv源码包地址:                官网  github opencv_contrib源码包地址:   github Tesseract源码包地址:        ...

  2. 人脸识别中的检测(在Opencv中加入了QT)

    #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> #include & ...

  3. qt中使用opencv处理图片 QImage 和 IplImage 相互之间转换问题

    在用opencv处理图片显示在qt label上的时候遇到不是问题 1. qt上要用qimage形式才干显示 IplImage转成 Qimage 彩色图像转换 IplImage  *fram; QIm ...

  4. QT + OpenCV + MinGW 在windows下配置开发环境

           由于研究项目需要,最近开始接触C++界面设计,关于“QT + OpenCV + MinGW在windows下配置开发环境”着实让人头疼,单次配置时间相当长,也十分不容易,本人第一次配置成 ...

  5. OpenCV+Qt+CMake安装+十种踩坑

    平台:win10 x64+opencv-3.4.1 + qt-x86-5.9.0 + cmake3.13.4 x64 OpenCV+Qt+CMake安装,及目前安装完后打包:mingw32-make时 ...

  6. [QT_OPENCV] qt下opencv配置以及首个opencv工程

    使用环境 : window版本 : win7 x64 QT : 5.8 32bit MinGW530 OpenCv : 3.2 opencv在qt下的环境配置: 在百度上百度了许多关于opencv环境 ...

  7. [OpenCV Qt教程] 在Qt图形界面中显示OpenCV图像的OpenGL Widget (第一部分)

    本文译自:http://www.robot-home.it/blog/en/software/tutorial-opencv-qt-opengl-widget-per-visualizzare-imm ...

  8. QT+OpenCV+OpenGL安装

    Ubuntu 10.04.3 LTS ("fresh" install) OpenCV 2.3.1 Qt SDK version 1.2.0 for Linux/X11 32-bi ...

  9. VS2015+OpenCV+Qt

    VS2015+OpenCV+Qt 01.OpenCV 下载 进入官网链接: https://opencv.org,下载所需要的版本: 下载完成后直接双击,选择解压路径,解压到响应的文件夹中: 若之后需 ...

  10. OpenCV 之 编译和配置

    “工欲善其事,必先利其器”,下面介绍在 Win7 32位系统下,用 cmake 编译 OpenCV 的过程. 1  开发环境 1.1  Win7 Windows 7 家庭普通版,Service Pac ...

随机推荐

  1. [转帖]用实力诠释细节!“Java性能调优六大工具”之JDK命令行工具

    https://www.zhihu.com/people/javajia-gou-ji-zhu-44/posts   JDK命令行工具 在JDK的开发包中,除了大家熟知的java.exe和javac. ...

  2. [转帖]写给想了解"集成电路"的朋友

    https://zhuanlan.zhihu.com/p/602627000 寒假和朋友小聚,每当就专业问题展开谈话,很容易形成"一边热火朝天,一边大脑宕机"的局面.俗话说隔行如隔 ...

  3. 《PalWorld/幻兽帕鲁》旧电脑linux搭建服务器

    关键词: PalWorld, Linux, Natapp, 内网穿透, 幻兽帕鲁 注 意 文 章 时 效 性 最近幻兽帕鲁爆火,steam一上线好友列表一串正在游玩哈哈哈,自己也是蹭一波热度,顺便试用 ...

  4. SP5464 CT - Counting triangles 题解

    题目翻译 题意 有一个网格,左上角是 \((0,0)\),右上角是 \((x,y)\).求这个网格中一共有多少个等腰直角三角形. 输入 第一行给定一个 \(c\),表示有 \(c\) 组数据. 后面 ...

  5. DES加密和base64加密

    DES简介:参考知乎 https://www.zhihu.com/question/36767829 和博客https://www.cnblogs.com/idreamo/p/9333753.html ...

  6. ASP.NET Core必备知识之Autofac

    使用Autofac替换掉微软的DI 本文的项目为ASP.NET Core3.1,传统三层架构 在这就不过多介绍Autofac,直接上代码 Autofac官网:https://autofac.org/ ...

  7. 关于 vant 移动端的 rem 适配方案

    一.使用 lib-flexible 动态设置 rem 基准值 (html 标签的字体大小) (1) 安装依赖: npm i amfe-flexible -D (2) 在main.js 中引入 impo ...

  8. CH32V208蓝牙从机sleep模式下功耗测试

    本测试基于CH32V208W的开发板:蓝牙从机模式:使用程序BLE_UART 在进行功耗测试的时候尽量去除额外耗电器件,将开发板上的VDD于VIO相连接,测功耗时直接给VDD供电. 将会对500ms, ...

  9. 【实用小技巧】RSA非对称加解密及XML&PEM格式互换方案

    ​ 最近因考虑接口安全问题,有实现给WEB API实现统一的参数鉴权功能,以防止请求参数被篡改或重复执行,参数鉴权方法基本与常见的鉴权思路相同,采用(timestamp+sign),而我为了防止tim ...

  10. ABC306 A - F

    ABC306 A - F 代码不提供 A 题意:吧字符串的每个字符连续输出两遍,记得不要快读,不要忘记输入 $ n $ 纪念 Qinzh A 题 WA 掉 B 题意:给定长度为 $ 64 $ 的数组 ...