OpenCV——图像分块局部阈值二值化

在这里插入图片描述

OpenCV——图像分块局部阈值二值化由CSDN点云侠原创,爬虫自重。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫。

一、算法原理

1、算法概述

   针对目前局部阈值二值化结果存在目标虚假或断裂的缺陷,提出了一种基于图像分块的局部阈值二值化方法。首先,将图像分成若干子块并分析每个子块像素灰度变化情况; 接着,取一定大小的局部窗口在图像中移动,比较该局部窗口内与包含窗口自身且比窗口更大区域内的像素灰度变化情况,更大区域由窗口模板当前覆盖的所有子块组成,以此判断窗口内是否为灰度变化平坦( 或剧烈) 区域; 最后,根据不同的区域,给出具体的二值化方案。

2、参考文献

[1] 张洁玉. 基于图像分块的局部阈值二值化方法 [J]. 计算机应用, 2017, 37 (03): 827-831.

二、代码实现

ImageBinarization.h

#pragma once
#include <vector>  
#include <opencv2/opencv.hpp>

class ImageBinarization
{
private:
	// 参数
	cv::Mat m_src;                // 输入数据
	cv::Mat m_dst;                // 输出数据
	cv::Size m_blockSize;         // 分块图像的尺寸
	cv::Size m_moveWndSize;       // 移动窗口的尺寸
	int m_segRow = 0;             // 图像分块的行数
	int m_segCol = 0;             // 图像分块的列数
	double m_alpha = 0.5;         // 平坦区域与剧烈区域的区分阈值
	double m_beta = 0.5;          // 剧烈区域,像素中心点与阈值O的距离
	std::vector<std::tuple<cv::Rect, double, double>> m_imgProperty;
	// 内部函数
	double getMatMAD(const cv::Mat& wndImg); // 计算平均绝对偏差
	std::vector<int>getGridIndices(const int windCenterInSegRow, const int windCenterInSegCol);
	void imageSegment();// 图像分块
	void segBlockBin(); // 分块二值化
public:
	ImageBinarization() {}
	~ImageBinarization() {}

	void setInputImage(const cv::Mat& src);
	void setBlockSize(const cv::Size& blockSize);       // 设置分块图像的尺寸
	void setMoveWindSize(const cv::Size& moveWndSize);  // 设置移动窗口的尺寸
	void setAlphaValue(double alpha);                   // 设置区分阈值
	void setBetaValue(double beta);                     // 设置像素中心点与阈值的距离
	void blockBinResult(cv::Mat& dst);                  // 分块二值化结果输出
};


ImageBinarization.cpp


#include<cstdlib>

#include"ImageBinarization.h"

// 参数设置
// 输入图像
void ImageBinarization::setInputImage(const cv::Mat& src)
{
	m_src = src;
}
// 分块尺寸
void ImageBinarization::setBlockSize(const cv::Size& blockSize)
{
	m_blockSize = blockSize;
}
// 滑动窗口尺寸
void ImageBinarization::setMoveWindSize(const cv::Size& moveWndSize)
{
	m_moveWndSize = moveWndSize;
}
// 平滑与剧烈区域分割阈值
void ImageBinarization::setAlphaValue(double alpha)
{
	m_alpha = alpha;
}
// 剧烈区域距离阈值
void ImageBinarization::setBetaValue(double beta)
{
	m_beta = beta;
}

// 计算平均绝对偏差
double ImageBinarization::getMatMAD(const cv::Mat& wndImg)
{
	CV_Assert(wndImg.type() == CV_8UC1);
	// 计算均值
	cv::Scalar myMean = cv::mean(wndImg);
	double sum = 0.0;
	for (int y = 0; y < wndImg.rows; ++y)
	{
		for (int x = 0; x < wndImg.cols; ++x)
		{
			// 计算每一个像素灰度减去平均值的绝对值
			int diffAbs = cv::abs(wndImg.at<uchar>(y, x) - myMean[0]);
			sum += diffAbs; // 计算绝对值之和
		}
	}

	return sum / (wndImg.rows * wndImg.cols); // 平均绝对偏差
}
// 中心点八邻域格网号计算
std::vector<int> ImageBinarization::getGridIndices(const int windCenterInSegRow, const int windCenterInSegCol)
{
	// p8,p7,p6
	// p1,p0,p5
	// p2,p3,p4
	std::vector<int>nGridIndices;// 邻域格网索引号容器
	nGridIndices.resize(9);
	// 中心点p0所在格网
	int p0Row = windCenterInSegRow;
	int p0Col = windCenterInSegCol;
	int p0Grid = -1;
	if (p0Row < m_segRow && p0Col < m_segCol)
	{
		p0Grid = p0Row * m_segCol + p0Col;
	}
	// 中心点越界则计算结束
	else
	{
		std::cerr << "中心点越界" << std::endl;
		abort();
	}
	nGridIndices[0] = p0Grid;
	// p0左侧p1所在格网
	int p1Row = p0Row;
	int p1Col = p0Col - 1;
	int p1Grid = -1;
	if (p1Col >= 0)
	{
		p1Grid = p1Row * m_segCol + p1Col;
	}
	nGridIndices[1] = p1Grid;

	// p0左下方p2所在格网
	int p2Row = p0Row + 1;
	int p2Col = p0Col - 1;
	int p2Grid = -1;
	if (p2Row < m_segRow && p2Col >= 0)
	{
		p2Grid = p2Row * m_segCol + p2Col;
	}
	nGridIndices[2] = p2Grid;

	// p0正下方p3所在格网
	int p3Row = p0Row + 1;
	int p3Col = p0Col;
	int p3Grid = -1;
	if (p3Row < m_segRow)
	{
		p3Grid = p3Row * m_segCol + p3Col;
	}
	nGridIndices[3] = p3Grid;

	// p0右下方p4所在格网
	int p4Row = p0Row + 1;
	int p4Col = p0Col + 1;
	int p4Grid = -1;
	if (p4Row < m_segRow && p4Col < m_segCol)
	{
		p4Grid = p4Row * m_segCol + p4Col;
	}
	nGridIndices[4] = p4Grid;
	// p0右侧p5所在格网
	int p5Row = p0Row;
	int p5Col = p0Col + 1;
	int p5Grid = -1;
	if (p5Col < m_segCol)
	{
		p5Grid = p5Row * m_segCol + p5Col;
	}
	nGridIndices[5] = p5Grid;

	// p0右上方p6所在格网
	int p6Row = p0Row - 1;
	int p6Col = p0Col + 1;
	int p6Grid = -1;
	if (p6Row >= 0 && p6Col < m_segCol)
	{
		p6Grid = p6Row * m_segCol + p6Col;
	}
	nGridIndices[6] = p6Grid;

	// p0正上方p7所在格网
	int p7Row = p0Row - 1;
	int p7Col = p0Col;
	int p7Grid = -1;
	if (p7Row >= 0)
	{
		p7Grid = p7Row * m_segCol + p7Col;
	}
	nGridIndices[7] = p7Grid;

	// p0左上方p8所在格网
	int p8Row = p0Row - 1;
	int p8Col = p0Col - 1;
	int p8Grid = -1;
	if (p8Row >= 0 && p8Col >= 0)
	{
		p8Grid = p8Row * m_segCol + p8Col;
	}
	nGridIndices[8] = p8Grid;

	return nGridIndices;
}
// 图像分块
void ImageBinarization::imageSegment()
{
	// OpenCV异常检测
	CV_Assert((m_blockSize.width <= m_src.cols) && (m_blockSize.height <= m_src.rows));
	// 1、获取分块图像的尺寸
	const int blockHeight = m_blockSize.height; // 图像分块的高度
	const int blockWidth = m_blockSize.width;   // 图像分块的宽度
	// 2、根据分块尺寸计算分块的个数
	m_segRow = cvCeil(m_src.rows / static_cast<double>(blockHeight)); // 图像分块的行数
	m_segCol = cvCeil(m_src.cols / static_cast<double>(blockWidth));  // 图像分块的列数
	// 3、使用裁剪函数进行分块
	cv::Mat roiImg;
	for (int i = 0; i < m_segRow; ++i)
	{
		int rectHeight = 0;// 裁剪矩形框的高度
		int rectWidth = 0; // 裁剪矩形框的宽度
		// 如果剩余像素的高度小于分块的高度,则裁剪矩形框的高度为剩余像素的高度
		rectHeight = m_src.rows - i * blockHeight < blockHeight ? m_src.rows - i * blockHeight : blockHeight;

		for (int j = 0; j < m_segCol; ++j)
		{
			// 如果剩余像素的宽度小于分块的宽度,则裁剪矩形框的宽度为剩余像素的宽度
			rectWidth = m_src.cols - j * blockWidth < blockWidth ? m_src.cols - j * blockWidth : blockWidth;

			// 获取分块图像
			cv::Rect rect(j * blockWidth, i * blockHeight, rectWidth, rectHeight);
			m_src(rect).copyTo(roiImg);
			// 计算每个分块的平均绝对偏差
			double madValue = getMatMAD(roiImg);
			// 计算每个分块的otsu阈值
			cv::Mat imgOtsu;
			double otsuValue = cv::threshold(roiImg, imgOtsu, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);
			// 分块信息、平均绝对偏差和otsu阈值存储到vector容器
			m_imgProperty.push_back(std::make_tuple(rect, madValue, otsuValue));

		}
	}
}
// 分块二值化
void ImageBinarization::segBlockBin()
{
	CV_Assert(m_src.type() == CV_8UC1);
	CV_Assert((m_moveWndSize.width % 2 == 1) && (m_moveWndSize.height % 2 == 1));
	CV_Assert((m_moveWndSize.width <= m_src.cols) && (m_moveWndSize.height <= m_src.rows));
	// 1、计算每个窗口的阈值
	m_dst = cv::Mat::zeros(m_src.rows, m_src.cols, CV_8UC1);
	for (int y = m_moveWndSize.height / 2; y <= m_src.rows - m_moveWndSize.height / 2 - 1; ++y)
	{
		for (int x = m_moveWndSize.width / 2; x <= m_src.cols - m_moveWndSize.width / 2 - 1; ++x)
		{
			// 获取以(x,y)为中心的滑动窗口范围内的灰度值
			cv::Point topLeftPoint = cv::Point(x - m_moveWndSize.width / 2, y - m_moveWndSize.height / 2);
			cv::Rect moveWindRect = cv::Rect(topLeftPoint.x, topLeftPoint.y, m_moveWndSize.width, m_moveWndSize.height);
			cv::Mat moveWindMat = m_src(moveWindRect);

			double delta = getMatMAD(moveWindMat);     // 局部窗口像素灰度平均绝对偏差
			double windMean = cv::mean(moveWindMat)[0];// 局部窗口像素灰度平均值
			double T2 = 0.0;                           // 区分平坦区域和非平坦区域的阈值
			double windOtsu = 0.0;                     // 平坦区域局部窗口阈值
		   // -----------------------计算滑动窗口中心点所在的分块格网号---------------------
			const int windCenterInSegRow = cvFloor(y / m_blockSize.height); // 行号
			const int windCenterInSegCol = cvFloor(x / m_blockSize.width);  // 列号
		   // ----------------------------中心点八邻域格网号计算----------------------------
			std::vector<int>nGridIndices = getGridIndices(windCenterInSegRow, windCenterInSegCol);
			// -----------------------------根据子块计算阈值--------------------------------
			for (size_t gridIdx = 0; gridIdx < nGridIndices.size(); ++gridIdx)
			{
				cv::Rect Intersection; // 重叠区域 
				int n = nGridIndices[gridIdx];
				if (n != -1)
				{
					Intersection = moveWindRect & std::get<0>(m_imgProperty[n]);
				}

				if (Intersection.area() > 0)
				{
					double ki = 1.0 * Intersection.area() / moveWindRect.area();
					T2 += ki * std::get<1>(m_imgProperty[n]);
					windOtsu += ki * std::get<2>(m_imgProperty[n]);
				}
			}
			int value = m_src.at<uchar>(y, x);
			// 平坦区域
			if (delta < m_alpha * T2)
			{
				// 大于阈值的部分为黑色
				if (value > windOtsu)
				{
					m_dst.at<uchar>(y, x) = 0;
				}
				// 小于阈值的部分为白色
				else
				{
					m_dst.at<uchar>(y, x) = 255;
				}
			}
			// 剧烈区域
			else
			{
				// img(x,y)>=(1-B)*O
				if (value >= (1 + m_beta) * windOtsu)
				{
					m_dst.at<uchar>(y, x) = 0;
				}
				// img(x,y)<(1-B)*O
				else if (value < (1 - m_beta) * windOtsu)
				{
					m_dst.at<uchar>(y, x) = 255;
				}

				else
				{
					// img(x,y)< Mean + 0.5*delta
					if (value < windMean + 0.5 * delta)
					{
						m_dst.at<uchar>(y, x) = 255;
					}
					// img(x,y)>= Mean + 0.5*delta
					else
					{
						m_dst.at<uchar>(y, x) = 0;
					}
				}
			}
		}
	}
}
// 分块二值化结果输出
void ImageBinarization::blockBinResult(cv::Mat& dst)
{
	imageSegment();
	segBlockBin();
	// 结果输出
	dst = m_dst;
}

main.cpp

#include<string>
#include<iostream>
#include"ImageBinarization.h"

int main()
{
	cv::Mat img = cv::imread("CG.jpg");

	// 转灰度图
	cv::Mat gray;
	cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);

	cv::Size windSize(3, 3); // 分块窗口大小
	cv::Size moveWind(5, 5); // 移动窗口大小
	cv::Mat img_Thr_O;       // 二值化结果
	ImageBinarization ib;
	ib.setInputImage(gray);
	ib.setBlockSize(windSize);
	ib.setMoveWindSize(moveWind);
	ib.setAlphaValue(0.5);
	ib.setBetaValue(0.7);
	ib.blockBinResult(img_Thr_O);

	cv::imshow("origion_pic", img);
	cv::imshow("img_Thr_O", img_Thr_O);
	cv::waitKey(0);

}

三、结果展示

在这里插入图片描述

  • 11
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
OpenCV中,图像阈值二值化是一种基本的图像处理操作,可以将图像转换为二值图像OpenCV提供了两种常用的图像阈值二值化方法:全局阈值二值化和自适应阈值二值化。全局阈值二值化是指将整个图像分割为黑白两个部分,而自适应阈值二值化是指根据图像局部区域来确定阈值,从而得到更精确的二值化结果。 在OpenCV中,全局阈值二值化操作的C API如下: double cv::threshold(InputArray src, OutputArray dst, double thresh, double maxval, int type) 其中,src表示输入图像,dst表示输出的二值化图像,thresh表示设定的阈值,maxval表示阈值以上的像素值,type表示二值化的类型。 另外,OpenCV还提供了一些其他的阈值操作方法。自适应阈值二值化是其中之一,其API如下: void adaptiveThreshold(InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C) 其中,src表示输入图像,dst表示输出的二值化图像,maxValue表示阈值以上的像素值,adaptiveMethod表示自适应阈值的计算方法,thresholdType表示二值化的类型,blockSize表示局部阈值计算的邻域大小,C表示从计算得到的阈值中减去的常数。 总结起来,OpenCV提供了多种图像阈值二值化方法,可以根据具体需求选择合适的方法进行二值化操作。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [二值图像分析:OpenCV中的二值化阈值操作](https://blog.csdn.net/PecoHe/article/details/113876296)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [【OpenCv阈值操作(超详细)](https://blog.csdn.net/qq_49838656/article/details/119516784)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

点云侠

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

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

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

打赏作者

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

抵扣说明:

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

余额充值