OpenCV二值图像处理——阈值,连通区域分析(C++)

阈值

阈值又叫临界值,是指一个效应能够产生的最低值或最高值

       对于图像的直方图存在明显边界的图像,我们可以很容易找到这个阈值,但是如果图像直方图分界不明显,那么这个阈值的寻找将变得十分困难。因此我们存在全局阈值与局部阈值两种

全局阈值

全局阈值就是在整幅图像中我们只有一个阈值来对图像进行二值化,但是其存在其局限性,例如图像中存在高斯噪声的情况下,我们无法找到一个很好的阈值将图像的边界分开:

 

另外如果图像的边界是在局部对比下出现的,即不同位置阈值不同,那么全局阈值的效果也非常不好

阈值分割

1.二进制阈值化   

像素值大于阈值,=255
像素值小于阈值,=0

在这里插入图片描述

2.反二进制阈值化

像素值小于阈值,=255
像素值大于阈值,=0

在这里插入图片描述

3.截断阈值化

像素值大于阈值,=阈值
像素值小于阈值,保持不变

在这里插入图片描述

4.反阈值化为0

像素值小于阈值,=0
像素值大于阈值,保持不变

在这里插入图片描述

5.阈值化为0

像素大于等于阈值,保持不变
像素小于阈值,=0

在这里插入图片描述

 下面是一个示例:

#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;

Mat src, graySrc, dst;
const char* inWin = "input window";
const char* outWin = "output window";
int threshold_value = 127;
int threshold_max = 255;
int type_value = 2;
int type_max = 4;
void ThresholdDemo(int, void*);

int main()
{
        src = imread("tahiti.jpg");
    if (!src.data)
    {
        printf("cannot load image!");
        return -1;
    }

    namedWindow(inWin, WINDOW_AUTOSIZE);
    imshow(inWin, src);
    namedWindow(outWin, WINDOW_AUTOSIZE);
    //添加调整thresholdValue值得滑动条
    createTrackbar("ThresholdValue", outWin, &threshold_value, threshold_max, ThresholdDemo);
    ThresholdDemo(0, 0);
    //添加调节不同阈值处理类型的滑动条
    createTrackbar("ThresholdType", outWin, &type_value, type_max, ThresholdDemo);
    waitKey(0);
    return 0;
}

void ThresholdDemo(int, void*) {
   
    //转换为灰度值
    cvtColor(src, graySrc, COLOR_RGB2GRAY);
    //二值化阈值
    //threshold(graySrc,dst, threshold_value, threshold_max,THRESH_BINARY);
    //调节4种不同阈值处理方法,再调节阈值处理
    threshold(graySrc, dst, threshold_value, threshold_max, type_value);
    //自动计算阈值
    //threshold(graySrc, dst, 0,255, THRESH_OTSU| type_value);
    //三角根据直方图计算阈值
    //threshold(graySrc, dst, 0, 255,THRESH_TRIANGLE| type_value);

    imshow(outWin, dst);
}

 

 

 

连通区域分析

       连通区域一般是指图像中具有相同像素值且位置相邻的前景像素点组成的图像区域。连通区域分析是指将图像中的各个连通区域找出并标记。
       连通区域分析是一种在CVPR和图像分析处理的众多应用领域中较为常用和基本的方法。例如:OCR识别中字符分割提取(车牌识别、文本识别、字幕识别等)、视觉跟踪中的运动前景目标分割与提取(行人入侵检测、遗留物体检测、基于视觉的车辆检测与跟踪等)、医学图像处理(感兴趣目标区域提取)、等等。也就是说,在需要将前景目标提取出来以便后续进行处理的应用场景中都能够用到连通区域分析方法,通常连通区域分析处理的对象是一张二值化后的图像。

连通区域分析的算法

       从连通区域的定义可以知道,一个连通区域是由具有相同像素值的相邻像素组成像素集合,因此,我们就可以通过这两个条件在图像中寻找连通区域,对于找到的每个连通区域,我们赋予其一个唯一的标识(Label),以区别其他连通区域。

连通区域分析有基本的算法,也有其改进算法,本文介绍其中的两种常见算法:

1)Two-Pass法;2)Seed-Filling种子填充法;
 

1)Two-Pass(两遍扫描法)

两遍扫描法,正如其名,指的就是通过扫描两遍图像,就可以将图像中存在的所有连通区域找出并标记。思路:第一遍扫描时赋予每个像素位置一个label,扫描过程中同一个连通区域内的像素集合中可能会被赋予一个或多个不同label,因此需要将这些属于同一个连通区域但具有不同值的label合并,也就是记录它们之间的相等关系;第二遍扫描就是将具有相等关系的equal_labels所标记的像素归为一个连通区域并赋予一个相同的label(通常这个label是equal_labels中的最小值)。


下面给出Two-Pass算法的简单步骤:

(1)第一次扫描:

访问当前像素B(x,y),如果B(x,y) == 1:

a、如果B(x,y)的领域中像素值都为0,则赋予B(x,y)一个新的label:

label += 1, B(x,y) = label;

b、如果B(x,y)的领域中有像素值 > 1的像素Neighbors

1)将Neighbors中的最小值赋予给B(x,y):

B(x,y) = min{Neighbors}

2)记录Neighbors中各个值(label)之间的相等关系,即这些值(label)同属同一个连通区域

 labelSet[i] = { label_m, .., label_n },labelSet[i]中的所有label都属于同一个连通区域(注:这里可以有多种实现方式,只要能够记录这些具有相等关系的label之间的关系即可)

(2)第二次扫描:

访问当前像素B(x,y),如果B(x,y) > 1:

a、找到与label = B(x,y)同属相等关系的一个最小label值,赋予给B(x,y);
完成扫描后,图像中具有相同label值的像素就组成了同一个连通区域。


下面这张图动态地演示了Two-pass算法:

 

下面是一个示例:(借鉴模仿,不是本人写的)

#include <iostream>
#include <string>
#include <list>
#include <vector>
#include <map>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
 
void icvprCcaByTwoPass(const cv::Mat& _binImg, cv::Mat& _lableImg)
{
	// connected component analysis (4-component)
	// use two-pass algorithm
	// 1. first pass: label each foreground pixel with a label
	// 2. second pass: visit each labeled pixel and merge neighbor labels
	// 
	// foreground pixel: _binImg(x,y) = 1
	// background pixel: _binImg(x,y) = 0
 
 
	if (_binImg.empty() ||
		_binImg.type() != CV_8UC1)
	{
		return ;
	}
 
	// 1. first pass
 
	_lableImg.release() ;
	_binImg.convertTo(_lableImg, CV_32SC1) ;
 
	int label = 1 ;  // start by 2
	std::vector<int> labelSet ;
	labelSet.push_back(0) ;   // background: 0
	labelSet.push_back(1) ;   // foreground: 1
 
	int rows = _binImg.rows - 1 ;
	int cols = _binImg.cols - 1 ;
	for (int i = 1; i < rows; i++)
	{
		int* data_preRow = _lableImg.ptr<int>(i-1) ;
		int* data_curRow = _lableImg.ptr<int>(i) ;
		for (int j = 1; j < cols; j++)
		{
			if (data_curRow[j] == 1)
			{
				std::vector<int> neighborLabels ;
				neighborLabels.reserve(2) ;
				int leftPixel = data_curRow[j-1] ;
				int upPixel = data_preRow[j] ;
				if ( leftPixel > 1)
				{
					neighborLabels.push_back(leftPixel) ;
				}
				if (upPixel > 1)
				{
					neighborLabels.push_back(upPixel) ;
				}
 
				if (neighborLabels.empty())
				{
					labelSet.push_back(++label) ;  // assign to a new label
					data_curRow[j] = label ;
					labelSet[label] = label ;
				}
				else
				{
					std::sort(neighborLabels.begin(), neighborLabels.end()) ;
					int smallestLabel = neighborLabels[0] ;  
					data_curRow[j] = smallestLabel ;
 
					// save equivalence
					for (size_t k = 1; k < neighborLabels.size(); k++)
					{
						int tempLabel = neighborLabels[k] ;
						int& oldSmallestLabel = labelSet[tempLabel] ;
						if (oldSmallestLabel > smallestLabel)
						{							
							labelSet[oldSmallestLabel] = smallestLabel ;
							oldSmallestLabel = smallestLabel ;
						}						
						else if (oldSmallestLabel < smallestLabel)
						{
							labelSet[smallestLabel] = oldSmallestLabel ;
						}
					}
				}				
			}
		}
	}
 
	// update equivalent labels
	// assigned with the smallest label in each equivalent label set
	for (size_t i = 2; i < labelSet.size(); i++)
	{
		int curLabel = labelSet[i] ;
		int preLabel = labelSet[curLabel] ;
		while (preLabel != curLabel)
		{
			curLabel = preLabel ;
			preLabel = labelSet[preLabel] ;
		}
		labelSet[i] = curLabel ;
	}
 
 
	// 2. second pass
	for (int i = 0; i < rows; i++)
	{
		int* data = _lableImg.ptr<int>(i) ;
		for (int j = 0; j < cols; j++)
		{
			int& pixelLabel = data[j] ;
			pixelLabel = labelSet[pixelLabel] ;	
		}
	}
}

2)Seed Filling(种子填充法)

       种子填充方法来源于计算机图形学,常用于对某个图形进行填充。思路:选取一个前景像素点作为种子,然后根据连通区域的两个基本条件(像素值相同、位置相邻)将与种子相邻的前景像素合并到同一个像素集合中,最后得到的该像素集合则为一个连通区域。


下面给出基于种子填充法的连通区域分析方法:

(1)扫描图像,直到当前像素点B(x,y) == 1:

a、将B(x,y)作为种子(像素位置),并赋予其一个label,然后将该种子相邻的所有前景像素都压入栈中;

b、弹出栈顶像素,赋予其相同的label,然后再将与该栈顶像素相邻的所有前景像素都压入栈中;

c、重复b步骤,直到栈为空;

此时,便找到了图像B中的一个连通区域,该区域内的像素值被标记为label;

(2)重复第(1)步,直到扫描结束;

扫描结束后,就可以得到图像B中所有的连通区域;


下面这张图动态地演示了Seed-Filling算法:

 

下面是一个示例:

#include <iostream>
#include <string>
#include <list>
#include <vector>
#include <map>
#include <stack>
 
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
 
 
void icvprCcaBySeedFill(const cv::Mat& _binImg, cv::Mat& _lableImg)
{
	// connected component analysis (4-component)
	// use seed filling algorithm
	// 1. begin with a foreground pixel and push its foreground neighbors into a stack;
	// 2. pop the top pixel on the stack and label it with the same label until the stack is empty
	// 
	// foreground pixel: _binImg(x,y) = 1
	// background pixel: _binImg(x,y) = 0
 
 
	if (_binImg.empty() ||
		_binImg.type() != CV_8UC1)
	{
		return ;
	}
 
	_lableImg.release() ;
	_binImg.convertTo(_lableImg, CV_32SC1) ;
 
	int label = 1 ;  // start by 2
 
	int rows = _binImg.rows - 1 ;
	int cols = _binImg.cols - 1 ;
	for (int i = 1; i < rows-1; i++)
	{
		int* data= _lableImg.ptr<int>(i) ;
		for (int j = 1; j < cols-1; j++)
		{
			if (data[j] == 1)
			{
				std::stack<std::pair<int,int>> neighborPixels ;   
				neighborPixels.push(std::pair<int,int>(i,j)) ;     // pixel position: <i,j>
				++label ;  // begin with a new label
				while (!neighborPixels.empty())
				{
					// get the top pixel on the stack and label it with the same label
					std::pair<int,int> curPixel = neighborPixels.top() ;
					int curX = curPixel.first ;
					int curY = curPixel.second ;
					_lableImg.at<int>(curX, curY) = label ;
 
					// pop the top pixel
					neighborPixels.pop() ;
 
					// push the 4-neighbors (foreground pixels)
					if (_lableImg.at<int>(curX, curY-1) == 1)
					{// left pixel
						neighborPixels.push(std::pair<int,int>(curX, curY-1)) ;
					}
					if (_lableImg.at<int>(curX, curY+1) == 1)
					{// right pixel
						neighborPixels.push(std::pair<int,int>(curX, curY+1)) ;
					}
					if (_lableImg.at<int>(curX-1, curY) == 1)
					{// up pixel
						neighborPixels.push(std::pair<int,int>(curX-1, curY)) ;
					}
					if (_lableImg.at<int>(curX+1, curY) == 1)
					{// down pixel
						neighborPixels.push(std::pair<int,int>(curX+1, curY)) ;
					}
				}		
			}
		}
	}
}

  • 5
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DDsoup

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值