本周有机会接触了一点opnev, 在此做一下记录, 最终以框选出下图箱子为目的(图片箱子为相机实拍结果,曝光有点低,会有亿点点暗 ), 本文会拆解步骤并附上图片, 完整的源码在最后.PS:本文参考了好多大佬分享的理论知识, 在此先感谢大佬的分享~~
首先是梳理一下流程, 下图是本次图片处理的大概流程和部分效果图以及部分会用到的算子~~
1.图像读取
图像读取可以直接使用如下模块读取直接路径的图片:
img1 = cv::imread("C:/Users/Jiang/Desktop/data/1.png");
但如果预处理图片较多, 需要从文件夹循环读取处理的话可以用如下模块,只需更改文件夹地址以及文件命名格式就行, 这里我文件夹里的图片名字均为"1.png"这样的格式:
for (int ii = 1; ii < 11; ii++)
{
vector<cv::Mat> images;
string name = cv::format("C:/Users/Jiang/Desktop/data3/%d.png", ii);
cv::Mat img1 = cv::imread(name);
if (img1.empty())
{
printf("没找到图片");
return -1;
}
images.push_back(img1);
//如果是循环读取,后面的处理就放在这个位置,不要出花括号哦~
}
2.提取需要处理的roi区域
这里的示例是拍照空间区域中存在一个托盘, 托盘上方有预提取的箱子, 所以需要建立托盘的roi区域, 之后只单独对这个区域进行处理, 这样可以排除干扰项~
cv::resize(img1, img2, cv::Size(img1.cols / 7, img1.rows / 7), 0, 0, cv::INTER_NEAREST);//这里是因为图片太大了,所以等比例缩小了一下面积
//通过roi区域筛选出托盘位置
cv::Mat roi(img2, cv::Rect(190, 140, 280, 250));
3.灰度处理
这一步转成灰度图, 并进行阈值提取,方便之后提取
cv::cvtColor(roi, g_grayImage, cv::COLOR_BGR2GRAY);
cv::threshold(g_grayImage, img3, 12, 255, cv::THRESH_BINARY);
4.灰度处理
这里的腐蚀是为了将阈值分割之后的部分噪声去除, 膨胀是为了还原上一步被删除了的特征点,偷个懒用上面的图了哈~其实这一步就和直接用闭运算是一样的, 但是为了能看出过程的变换我就拆分开了.
//定义腐蚀和膨胀的结构化元素和迭代次数
element = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(2, 2));
int iteration = 2;
int iteration2 = 1;
//腐蚀
cv::morphologyEx(img3, img3, cv::MORPH_ERODE, element, cv::Point(-1, -1), iteration);
cv::imshow("1", roi);
cv::imshow("腐蚀后", img3);
//膨胀
cv::morphologyEx(img3, img3, cv::MORPH_DILATE, element, cv::Point(-1, -1), iteration2);
cv::imshow("膨胀后", img3);
5.提取连通区域并删除其中面积较小的区域
// 提取连通区域,并剔除小面积联通区域
std::vector<std::vector<cv::Point>> contours;
cv::findContours(img3, contours, cv::RETR_LIST, cv::CHAIN_APPROX_NONE);
contours.erase(std::remove_if(contours.begin(), contours.end(), [](const std::vector<cv::Point>& c)
{
return cv::contourArea(c) < 150;
}), contours.end());
// 显示图像
img3.setTo(0);
cv::drawContours(img3, contours, -1, cv::Scalar(255), cv::FILLED);
cv::imshow("删除了小面积区域", img3); //cv::waitKey(0);
6.筛选出面积较大的区域部分 并绘制矩形 框选出箱子轮廓
这一块的逻辑就是第一次筛选掉小面积区域后, 对剩下的区域再进行一次筛选,这次筛选面积较大的区域, 并对每一个面积大的区域进行单独绘制出轮廓, 并将这个轮廓点集用minAreaRect算子, 输入点集输出四个点的坐标, 并将这四个点用直线连接起来, 就可以得到结果了~
cv::Mat delet(img3.size(), img3.type(), cv::Scalar(0, 0, 0));;
cout << "剔除小区域后的轮廓个数:" << contours.size();
for (int tmp = 0; tmp < contours.size(); tmp++)
{
if (cv::contourArea(contours[tmp]) > 3000)
{
cout << "大轮廓的编号是" << tmp;
cout << "此时的面积为" << cv::contourArea(contours[tmp]);
cv::drawContours(delet, contours, tmp, cv::Scalar(255), cv::FILLED);
cv::Canny(delet, mid, 0, 18, 3);
cv::bitwise_not(mid, bitwise);
// 用Canny算子检测边缘 阈值1和阈值2两者中比较小的值用于边缘连接,较大的值用来控制边缘的初始段
Canny(bitwise, g_cannyMat_output, g_nThresh, g_nThresh * 2, 3);
// 寻找轮廓
findContours(g_cannyMat_output, g_vContours, g_vHierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE, cv::Point(0, 0));
// 绘出轮廓
cv::Mat drawing = cv::Mat::zeros(g_cannyMat_output.size(), CV_8UC3);
for (int i = 0; i < g_vContours.size(); i++)
{
cv::Scalar color = cv::Scalar(255, 182, 193);
drawContours(drawing, g_vContours, i, color, 2, 8, g_vHierarchy, 0, cv::Point());
}
cv::imshow("drawidrawingdrawingng", drawing);
vector<cv::Point> tempPoint; // 点集
// 将所有点集存储到tempPoint
for (int k = 0; k < g_vContours.size(); k++)
{
for (int m = 0; m < g_vContours[k].size(); m++)
{
tempPoint.push_back(g_vContours[k][m]);
}
}
//对给定的 2D 点集,寻找最小面积的包围矩形
cv::RotatedRect box = minAreaRect(cv::Mat(tempPoint));
cv::Point2f vertex[4];
box.points(vertex);
//绘制出最小面积的包围矩形
for (int i = 0; i < 4; i++)
{
cv::line(roi, vertex[i], vertex[(i + 1) % 4], cv::Scalar(100, 200, 211), 1, cv::LINE_AA);
}
cv::imshow("最终提取结果", roi);
cv::waitKey(0);
string Img_Name = "C:\\Users\\Jiang\\Desktop\\result\\" + to_string(k) + ".png";
cv::imwrite(Img_Name, roi);//这里是按照固定路劲保存结果图片,下标从0开始,如果重复了会覆盖
k++;
delet = cv::Scalar(0, 0, 0);//这里是将处理过的图片阈值赋为0,即为黑色,不然处理过的图片还会残留在这里
}
}
7.全部源码
#include <iostream>
#include<opencv2\opencv.hpp>
#include "testOpenCV346.h"
#include <memory>
#include <opencv2/highgui/highgui.hpp>
#include <vector>
#include <algorithm>
using namespace std;
cv::Mat g_srcImage,img1,img2,img3, img4, element, element2, result, result2,erzhi,mid,g_grayImage,kai, bitwise,morph;//这里有一些是定义了其他函数用的,这里没有删哦
int g_nThresh = 13;
cv::Mat g_cannyMat_output;
vector<vector<cv::Point>> g_vContours; //向量内每个元素保存了一组由连续的Point点构成的点的集合的向量,每一组Point点集就是一个轮廓。有多少轮廓,向量contours就有多少元素
vector<cv::Vec4i> g_vHierarchy; //“向量内每一个元素包含了4个int型变量”的向量分 别表示第i个轮廓的后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号。
int main(int argc, char** argv)
{
std::shared_ptr<testOpenCV346> pMyOpenCv = std::make_shared<testOpenCV346>();
//for (int ii = 1; ii < 11; ii++)
//{
// vector<cv::Mat> images;
// string name = cv::format("C:/Users/Jiang/Desktop/data3/%d.png", ii);
// cv::Mat img1 = cv::imread(name);
// if (img1.empty())
// {
// printf("没找到图片");
// return -1;
// }
// images.push_back(img1);
// 加载源图像,这里采用了固定路径,如果要循环,就用上面的for,记得下面的花括号打开就行
img1 = cv::imread("C:/Users/Jiang/Desktop/data/1.png");
cv::resize(img1, img2, cv::Size(img1.cols / 7, img1.rows / 7), 0, 0, cv::INTER_NEAREST);
//通过roi区域筛选出托盘位置
cv::Mat roi(img2, cv::Rect(190, 140, 280, 250));
cv::Mat roi1(img2, cv::Rect(190, 160, 261, 210));
//转成灰度并模糊化降噪
cv::cvtColor(roi, g_grayImage, cv::COLOR_BGR2GRAY);
cv::threshold(g_grayImage, img3, 12, 255, cv::THRESH_BINARY);
cv::imshow("阈值之后", img3);
//cv::waitKey(0);
//定义腐蚀和膨胀的结构化元素和迭代次数
element = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(2, 2));
element2 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
int iteration = 2;
int iteration2 = 1;
//腐蚀
cv::morphologyEx(img3, img3, cv::MORPH_ERODE, element, cv::Point(-1, -1), iteration);
cv::imshow("1", roi);
cv::imshow("腐蚀后", img3);
//cv::waitKey(0);
cv::morphologyEx(img3, img3, cv::MORPH_DILATE, element, cv::Point(-1, -1), iteration2);
cv::imshow("膨胀后", img3);
//cv::waitKey(0);
// 提取连通区域,并剔除小面积联通区域
std::vector<std::vector<cv::Point>> contours;
cv::findContours(img3, contours, cv::RETR_LIST, cv::CHAIN_APPROX_NONE);
contours.erase(
std::remove_if(contours.begin(), contours.end(), [](const std::vector<cv::Point>& c)
{
return cv::contourArea(c) < 150;
})
, contours.end());
// 显示图像
img3.setTo(0);
cv::drawContours(img3, contours, -1, cv::Scalar(255), cv::FILLED);
cv::imshow("删除了小面积区域", img3); //cv::waitKey(0);
//cv::waitKey(0);
cv::Mat delet(img3.size(), img3.type(), cv::Scalar(0, 0, 0));;
cout << "剔除小区域后的轮廓个数:" << contours.size();
for (int tmp = 0; tmp < contours.size(); tmp++)
{
if (cv::contourArea(contours[tmp]) > 3000)
{
cout << "大轮廓的编号是" << tmp;
cout << "此时的面积为" << cv::contourArea(contours[tmp]);
cv::drawContours(delet, contours, tmp, cv::Scalar(255), cv::FILLED);
cv::Canny(delet, mid, 0, 18, 3);
cv::bitwise_not(mid, bitwise);
// 用Canny算子检测边缘 阈值1和阈值2两者中比较小的值用于边缘连接,较大的值用来控制边缘的初始段
Canny(bitwise, g_cannyMat_output, g_nThresh, g_nThresh * 2, 3);
// 寻找轮廓
findContours(g_cannyMat_output, g_vContours, g_vHierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE, cv::Point(0, 0));
// 绘出轮廓
cv::Mat drawing = cv::Mat::zeros(g_cannyMat_output.size(), CV_8UC3);
for (int i = 0; i < g_vContours.size(); i++)
{
cv::Scalar color = cv::Scalar(255, 182, 193);
drawContours(drawing, g_vContours, i, color, 2, 8, g_vHierarchy, 0, cv::Point());
}
cv::imshow("drawidrawingdrawingng", drawing);
//cv::waitKey(0);
vector<cv::Point> tempPoint; // 点集
// 将所有点集存储到tempPoint
for (int k = 0; k < g_vContours.size(); k++)
{
for (int m = 0; m < g_vContours[k].size(); m++)
{
tempPoint.push_back(g_vContours[k][m]);
}
}
//对给定的 2D 点集,寻找最小面积的包围矩形
cv::RotatedRect box = minAreaRect(cv::Mat(tempPoint));
cv::Point2f vertex[4];
box.points(vertex);
//绘制出最小面积的包围矩形
for (int i = 0; i < 4; i++)
{
cv::line(roi, vertex[i], vertex[(i + 1) % 4], cv::Scalar(100, 200, 211), 1, cv::LINE_AA);
}
cv::imshow("最终提取结果", roi);
cv::waitKey(0);
string Img_Name = "C:\\Users\\Jiang\\Desktop\\result\\" + to_string(k) + ".png";
cv::imwrite(Img_Name, roi);//这里是按照固定路劲保存结果图片,下标从0开始,如果重复了会覆盖
k++;
delet = cv::Scalar(0, 0, 0);//这里是将处理过的图片阈值赋为0,即为黑色,不然处理过的图片还会残留在这里
}
}
//cv::waitKey(0);
//}
cout << "任务完成咯~`";
return(0);
}
至此, 本次分享的箱子提取过程完结, 图片处理其实很吃图片的像素分布情况 ( 是否有过亮过暗, 是否反光, 是否有噪声, 是否清晰等 ) , 比如本次图片就是属于较暗的情况, 在阈值分割的时候就很头疼, 用了自适应阈值和直方图阈值分割的方式, 但效果并不是很理想 ,最终还是手动给了阈值, 而且干扰项也蛮多的, 处理方式还是比较简单和基础, 最后再附一张轮廓检测的结果图 , 效果其实也还行 . 比如图片17 , 原图是三个箱子堆叠在一起 , 但是轮廓补全后还是把箱子轮廓框选出来了.文章来源:https://www.toymoban.com/news/detail-471540.html
这里单独提一下,在VS的"工具"→"拓展和跟新"里可以找到"Image Watch 2017"插件,可以用来查看图片的具体像素点,在阈值处理的时候会很有用文章来源地址https://www.toymoban.com/news/detail-471540.html
到了这里,关于OPENCV C++图像提取,图像处理,roi,阈值分割,连通区域筛选,边缘检测(以箱子边缘框选为例)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!