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

目录

  • 一、算法原理
    • 1、算法概述
    • 2、参考文献
  • 二、代码实现
  • 三、结果展示

在这里插入图片描述

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);

}

三、结果展示

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/569168.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

消息队列 Kafka 入门篇(二) -- 安装启动与可视化工具

一、Windows 10 环境安装 1、下载与解压 首先&#xff0c;访问Apache Kafka的官方下载地址&#xff1a; https://kafka.apache.org/downloads 在本教程中&#xff0c;我们将使用kafka_2.13-2.8.1版本作为示例。下载完成后&#xff0c;解压到您的工作目录的合适位置&#xff…

目标检测——YOLOv6算法解读

论文&#xff1a;YOLOv6: A Single-Stage Object Detection Framework for Industrial Applications (2022.9.7) 作者&#xff1a;Chuyi Li, Lulu Li, Hongliang Jiang, Kaiheng Weng, Yifei Geng, Liang Li, Zaidan Ke, Qingyuan Li, Meng Cheng, Weiqiang Nie, Yiduo Li, Bo …

企业商业活动如何获得央级媒体的采访报道?

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 企业想要获得央级媒体的采访报道&#xff0c;确实需要精心策划和准备&#xff1a; 一、如何巧妙给媒体报选题 精准定位&#xff1a;首先要明确企业的核心价值、创新点或行业影响力&…

【C++】手撕list(list的模拟实现)

目录 01.节点 02.迭代器 迭代器运算符重载 03.list类 &#xff08;1&#xff09;构造与析构 &#xff08;2&#xff09;迭代器相关 &#xff08;3&#xff09;容量相关 &#xff08;4&#xff09;访问操作 &#xff08;5&#xff09;插入删除 我们在学习数据结构的时候…

StartAI智能绘图软件出现“缺少Python运行库”怎么办?

StartAI做为一款国产AI界的新秀&#xff0c;是一款贴合AIGC新手的智能绘图软件。新手安装遇见“缺少Python运行库”怎么办”&#xff1f;小编一招搞定~ 解决方法&#xff1a;手动下载【resource文件】&#xff0c;将文件添加到安装目录下。 点击链接进行手动下载噢~ 确保 Star…

图像处理之模板匹配(C++)

图像处理之模板匹配&#xff08;C&#xff09; 文章目录 图像处理之模板匹配&#xff08;C&#xff09;前言一、基于灰度的模板匹配1.原理2.代码实现3.结果展示 总结 前言 模板匹配的算法包括基于灰度的匹配、基于特征的匹配、基于组件的匹配、基于相关性的匹配以及局部变形匹…

Spring-IOC之组件扫描

版本 Spring Framework 6.0.9​ 1. 前言 通过自动扫描&#xff0c;Spring 会自动从扫描指定的包及其子包下的所有类&#xff0c;并根据类上的特定注解将该类装配到容器中&#xff0c;而无需在 XML 配置文件或 Java 配置类中逐一声明每一个 Bean。 支持的注解 Spring 支持一系…

Mysql索引详解(索引分类)

文章目录 概述索引对查询速度的影响索引的优缺点索引类型一级索引和二级索引的区别MySQL 回表联合索引&#xff08;最左前缀原则主键索引和唯一索引的区别BTree索引和Hash索引的区别 覆盖索引索引下推加索引能够提升查询效率原因MySQL 索引结构采用 B树原因索引失效的场景MySQL…

JAVASE基础语法(异常、常用类)

一、异常 1.1 什么是异常 异常就是指不正常。是指代码在运行过程中可能发生错误&#xff0c;导致程序无法正常运行。 package com.atguigu.exception;public class TestException {public static void main(String[] args) {int[] arr {1,2,3,4,5};System.out.println(&quo…

前端css中filter(滤镜)的使用

前端css中filter的使用 一、前言二、补充内容说明三、模糊&#xff08;一&#xff09;、模糊效果&#xff0c;源码1&#xff08;二&#xff09;、源码1运行效果1.视频演示2.截图演示 四、阴影&#xff08;一&#xff09;、阴影效果&#xff0c;源码2&#xff08;二&#xff09;…

Linux文件系统与日志

一、inode和block 文件数据包括元信息与实际数据&#xff0c;文件存储在硬盘上&#xff0c;硬盘最小存储单位是扇区&#xff0c;每个扇区存储512字节 1.block(块)&#xff1a;文件系统中用于存储文件实际数据的最小单位&#xff0c;由文件系统进行分配和管理&#xff0c;并通…

JavaSE内部类

内部类概述 1.内部类的基础 内部类的分类&#xff1a;实例化内部类&#xff0c;静态内部类&#xff0c;局部内部类和匿名内部类 public class OutClass {// 成员位置定义&#xff1a;未被static修饰 --->实例内部类public class InnerClass1{}// 成员位置定义&#xff1a;被…

01、创建型-单例模式--只有一个实例

文章目录 前言一、基本介绍1.1 什么是单例模式1.2 为什么要用单例模式1.3 应用场景1.4 单例优缺点 二、单例模式的实现方式2.1 饿汉式单例2.1.1 静态变量方式2.1.2 静态代码块 2.2 懒汉式单例2.2.1 懒汉式单例2.2.2 懒汉式优化①-线程安全2.2.2 懒汉式优化②-双重检查锁2.2.3 懒…

ROS1快速入门学习笔记 - 04创建工作环境与功能包

一、定义 工作空间(workspace)是一个存放工程开发相关文件的文件夹。 src:代码空间&#xff08;Source Space&#xff09;build: 编辑空间&#xff08;Build Space&#xff09;devel:开发空间&#xff08;Development Space&#xff09;install:安装空间&#xff08;Install …

深入理解Linux文件系统于日志分析

目录 一.Inode 和 block 概述 ​编辑 1.inode 的内容 &#xff08;1&#xff09;Inode 包含文件的元信息 &#xff08;2&#xff09;用 stat 命令可以查看某个文件的 inode 信息 &#xff08;3&#xff09; Linux系统文件三个主要的时间属性 &#xff08;4&#xff09;目…

CentOS 系统的优缺点

CentOS &#xff08;社区企业操作系统的缩写&#xff09;是一个基于红帽企业 Linux (RHEL)的免费开源发行版&#xff0c; 旨在为服务器和工作站提供稳定、可靠和安全的平台。 不应将其与CentOS Stream 混淆&#xff0c;后者是即将发布的 RHEL 版本的上游开发平台。 CentOS Li…

第67天:APP攻防-Frida反证书抓包移动安全系统资产提取评估扫描

思维导图 案例一&#xff1a;内在-资产提取-AppinfoScanne AppinfoScanner 一款适用于以 HW 行动/红队/渗透测试团队为场景的移动端(Android、iOS、WEB、H5、静态网站)信息收集扫描工具&#xff0c;可以帮助渗透测试工程师、攻击队成员、红队成员快速收集到移动端或者静态 WEB …

机器学习之sklearn基础教程

ChatGPT Scikit-learn (简称sklearn) 是一个非常受欢迎的Python机器学习库。它包含了从数据预处理到训练模型的各种工具。下面是一个关于如何使用sklearn进行机器学习的基础教程。 1. 安装和导入sklearn库 首先&#xff0c;你需要安装sklearn库&#xff08;如果你还没有安装的…

使用写入这类接口后,文件指针fp是否会偏移?

以fprintf为例&#xff1a; 在使用 fprintf 函数写入数据时&#xff0c;文件指针 fp 会自动进行偏移&#xff0c;以确保数据被写入到文件的正确位置。 每次调用 fprintf 函数都会将数据写入文件&#xff0c;并且文件指针会在写入完成后自动移动到写入的末尾&#xff0c;以便下…

56-FMC连接器电路设计

视频链接 FMC连接器电路设计01_哔哩哔哩_bilibili FMC连接器电路设计 1、FMC简介 1.1、FMC介绍 FMC&#xff08;FPGA Mezzanine Card&#xff09;是一个应用范围、适应环境范围和市场领域范围都很广的通用模块。FMC连接器连接了由FPGA提供的引脚和FMC子板的I/O接口。最新的…