• 基于C++的OpenCV项目实战——零部件的自动光学检测


    基于C++的OpenCV项目实战——零部件的自动光学检测

    一、背景

    首先任务背景是AOI(自动光学检测)

    最重要的目的在于:将前景和物体进行分割与分类;

    场景示意图:

    在这里插入图片描述

    需要注意,在螺母的传送带上,需要有前光和背光,给物体打光才能够拍摄清晰的图像;


    二、基础知识

    首先分为以下几步:

    1、噪声抑制(预处理)

    2、背景移除(分割)

    3、二值化

    4、连通域、轮廓查找算法


    • 降噪算法:

    ​ 先使用中值滤波对椒盐噪声进行过滤,再使用高斯滤波对物体边缘进行模糊;

    • 背景移除

      首先有两种方案可以实现背景移除,也就是减法和除法;

    在这里插入图片描述

    • 连通图检测计数

      首先连通域类型分为4路连通和8路连通:

    在这里插入图片描述

    使用连通图检测算法,可以将不连通的每个物体都用不同颜色划分出来;


    三、代码实现

    1、实现多窗口展示

    如果想要多张图像展示在一个窗口中,也就是实现拼接图片的操作,使用Python代码实现起来可能比较便捷,C++代码需要定义一个类,并且实际编写也比较繁琐;

    class Display {
    private:
    	int cols, rows, width, height;
    	String title;
    	vector win_names;
    	vector images;
    	Mat canvas;
    public:
    	Display(String t, int c, int r, int flags) :title(t), cols(c), rows(r) {
    		height = 1080;
    		width = 1920;
    		namedWindow(title, flags);
    		canvas = Mat(height, width, CV_8UC3);
    		imshow(title, canvas);
    	}
    
    	int add_window(String win_name, Mat image, bool flag = true) {
    		win_names.push_back(win_name);
    		images.push_back(image);
    		if (flag) {
    			draw();
    		}
    		return win_names.size();
    	}
    	
        // 实现删除窗口
    	int delete_window(String win_name) {
    		int index = 0;
    		for (const auto& it : win_names) {
    			if (it == win_name) break;
    			index++;
    		}
    		win_names.erase(win_names.begin() + index);
    		images.erase(images.begin() + index);
    
    		return win_names.size();
    	}
    
    	void draw() {
    		canvas.setTo(Scalar(20, 20, 20));
    		int single_width = width / cols;
    		int single_height = height / rows;
    		int max_win = win_names.size() > cols * rows ? cols * rows : win_names.size();
    		
    		int i = 0;
    		auto iw = win_names.begin();
    		for (auto it = images.begin(); it != images.end()&&i multi_window;
    
    int main(int argc, char** argv)
    {
    
    	// 实现多窗口
    	String total_path = "imgpath";
    	String background_path = "imgpath";
    
    	Mat abc = imread(total_path, 0);
    
    	multi_window = make_shared("Review for all", 3, 2, WINDOW_NORMAL);
    	multi_window->add_window("ABC", abc);
    	multi_window->add_window("ABCC", abc);
        multi_window->delete_window("ABC");		// 也支持删除窗口
    	multi_window->draw();
    
    	waitKey(0);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    在这里插入图片描述


    2、降噪处理

    采用中值滤波+高斯滤波结合的降噪方法:

    Mat get_background(const Mat& bg){
        Mat img;
        medianBlur(bg,img,3);
        GaussianBlur(bg,img,Size(3,3),0);
        return img;
    }
    
    Mat smoothen_img(const Mat& noise_img){
        Mat img;
        medianBlur(noise_img,img,5);
        GaussianBlur(img,img,Size(3,3),0);
        return img;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    3、背景去除

    分为两种方式,一种为减法,一种为除法;

    Mat remove_background_divide(Mat image, Mat background) {
    	Mat tmp;
    	Mat fg, bg;
    	image.convertTo(fg, CV_32F);
    	background.convertTo(bg, CV_32F);
    	tmp = 1 - (fg / bg);
    	tmp.convertTo(tmp, CV_8U, 255);
    	return tmp;
    }
    
    Mat remove_background_minus(Mat image, Mat background) {
    	return background - image;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    从结果图上看,使用除法的方式能更好的保留白色部分的信息,因此选用除法的方式;


    4、连通图实现

    对二值化后的图像进行连通域划分,并且用随机颜色绘制到Mask图上;

    void connection_check(Mat image) {
    	Mat labels;
    	int num = connectedComponents(image, labels);
    	if (num <= 1) {
    		cout << "No stuff detect!!" << endl;
    		return;
    	}
    	else
    	{
    		cout << num << " objects detected!!" << endl;
    	}
    
    	Mat display = Mat::zeros(image.rows, image.cols, CV_8UC3);
    	for (int i = 1; i < num; i++) {
    		Mat mask = (labels == i);
    		display.setTo(random_color_generator(seed), mask);
    	}
    	multi_window->add_window("Segment", display);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里插入图片描述


    5、计算连通域面积

    OpenCV中也自带了对面积区域的计算:

    void connection_heavy_check(Mat image) {
    	Mat labels, stats, centroids;
    	int num =connectedComponentsWithStats(image, labels, stats, centroids);
    	if (num <= 1) {
    		cout << "No stuff detect!!" << endl;
    		return;
    	}
    	else
    	{
    		cout << num << " objects detected!!" << endl;
    	}
    	Mat display = Mat::zeros(image.rows, image.cols, CV_8UC3);
    	for (int i = 1; i < num; i++) {
    		// 得到连通域的质心点
    		Point2i pt(centroids.at(i, 0), centroids.at(i, 1));
    		// 打印标签和连通域坐标和面积
    		cout << "Stuff #" << i << ", Position: " << pt << " ,Area: " << stats.at(i, CC_STAT_AREA) << endl;
    		Mat mask = (labels == i);
    		display.setTo(random_color_generator(seed), mask);
    		stringstream ss;
    		ss << stats.at(i, CC_STAT_AREA);
    		putText(display, ss.str(), pt, FONT_HERSHEY_SIMPLEX, 0.5, Scalar(255, 255, 255), 2);
    	}
    	multi_window->add_window("Segment more", display);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    在这里插入图片描述


    6、轮廓检测

    实现对物体轮廓的检测;

    void get_contour(Mat image) {
    	vector> contours;
    	findContours(image, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
    	Mat display = Mat::zeros(image.rows, image.cols, CV_8UC3);
    	if (contours.size() == 0) {
    		cout << "No contour detect!!" << endl;
    		return;
    	}
    	else
    	{
    		cout << contours.size() << " contour detected!!" << endl;
    	}
    	for (int i = 0; i < contours.size(); i++) {
    		drawContours(display, contours, i, random_color_generator(seed), 2);
    	}
    	multi_window->add_window("CONTOURS", display);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述

    从上结果图可知,检测到的轮廓并不包含内部轮廓,如果想检测所有轮廓应该将findContours函数中的类型参数改为RETR_LIST即可;

    四、总结

    本次项目中涉及的技术点如下:

    • 多窗口展示
    • 背景去除
    • 连通图的实现
    • 轮廓边缘检测

    并且在实际的C++代码中,还涉及了智能指针等高阶知识;

    工业质检项目作为视觉领域较为成熟的落地项目,其大部分都是基于深度学习的方式实现了,但如果能掌握一些OpenCV的方法,也可以在项目中起到优化效果的作用;

  • 相关阅读:
    每次重启完IDEA,application.properties文件里的中文变成?
    从零开始学数据结构系列之第四章《 最小生成树概念》
    【LeetCode】最长连续序列 [M](数据结构设计)
    关于python的内存管理机制和python变量占用内存及时释放的问题
    Flink 流程处理和批处理开发
    【滤波器】最小均方(LMS)自适应滤波器
    TCP/IP网络编程(9) 进程间通信
    HTTP响应状态码详解(HTTP3)
    网址打包微信小程序源码 wap转微信小程序 网站转小程序源码 网址转小程序开发
    将算力普惠到底 阿里云开启金秋上云季:数百款爆品享专属特惠价阿里云上新金秋云创季:上百款云产品享特惠 全能爆品仅99元/年
  • 原文地址:https://blog.csdn.net/weixin_40620310/article/details/126848699