常用阈值分割算法及 C++ 代码分析(二)
一、概述
阈值分割是图像处理中一种基础且重要的技术,它的核心思想是通过设定一个或多个阈值将图像中的像素划分为不同的类别,以实现图像中目标和背景的分离,或者不同目标之间的分离。这种技术广泛应用于物体检测、图像识别、医学影像处理、遥感图像处理等众多领域。在 C++ 环境下,我们可以利用 OpenCV 等库实现这些算法,下面将详细介绍几种常用的阈值分割算法及其 C++ 实现。
二、常用阈值分割算法及原理
(一)最大熵阈值法
- 原理:
- 优点:
- 对于噪声和灰度分布不均匀的图像,相比一些简单的阈值法,能够取得更好的分割效果,能自适应地找到较好的分割阈值。
- 考虑了图像的灰度分布信息,对复杂图像具有一定的适应性。
- 缺点:
- 计算熵的过程相对复杂,计算量较大,尤其是对于高分辨率图像,需要遍历所有可能的阈值计算熵,效率可能较低。
(二)迭代阈值法
- 原理:
- 优点:
- 算法简单直观,收敛速度相对较快,通常能在较少的迭代次数内找到合适的阈值。
- 对于一些具有较明显灰度差异的图像,能有效地找到合适的阈值。
- 缺点:
- 初始阈值的选取会影响最终结果,对于复杂的图像可能陷入局部最优解,对噪声敏感,可能导致分割结果不理想。
(三)矩保持法
- 原理:
- 优点:
- 利用了图像的矩信息,对图像的统计特性有较好的把握,对于具有一定形状特征的目标分割效果较好。
- 对于灰度分布不均匀但具有一定结构的图像,能较好地找到分割阈值。
- 缺点:
- 涉及到方程组的求解,计算过程复杂,可能存在求解不稳定的情况,对噪声较为敏感。
三、C++ 代码实现及分析
(一)最大熵阈值法
#include <iostream>
#include <opencv2/opencv.hpp>
#include <vector>
double entropy(std::vector<double>& p) {
double ent = 0;
for (size_t i = 0; i < p.size(); ++i) {
if (p[i] > 0) {
ent -= p[i] * std::log2(p[i]);
}
}
return ent;
}
int maxEntropyThreshold(cv::Mat& image) {
std::vector<int> histogram(256, 0);
int totalPixels = image.rows * image.cols;
for (int i = 0; i < image.rows; ++i) {
for (int j = 0; j < image.cols; ++i) {
histogram[image.at<uchar>(i, j)]++;
}
}
std::vector<double> prob(256, 0);
for (int i = 0; i < 256; ++i) {
prob[i] = static_cast<double>(histogram[i]) / totalPixels;
}
double maxEnt = 0;
int threshold = 0;
for (int t = 0; t < 256; ++t) {
double p0 = 0, p1 = 0;
std::vector<double> prob0(t + 1, 0);
std::vector<double> prob1(256 - t, 0);
for (int i = 0; i <= t; ++i) {
p0 += prob[i];
prob0[i] = prob[i];
}
for (int i = t + 1; i < 256; ++i) {
p1 += prob[i];
prob1[i - t - 1] = prob[i];
}
if (p0 == 0 || p1 == 0) continue;
for (int i = 0; i <= t; ++i) {
prob0[i] /= p0;
}
for (int i = 0; i < 256 - t; ++i) {
prob1[i] /= p1;
}
double ent0 = entropy(prob0);
double ent1 = entropy(prob1);
double totalEnt = ent0 + ent1;
if (totalEnt > maxEnt) {
maxEnt = totalEnt;
threshold = t;
}
}
return threshold;
}
void applyMaxEntropyThreshold(cv::Mat& image) {
int threshold = maxEntropyThreshold(image);
for (int i = 0; i < image.rows; ++i) {
for (int j = 0; j < image.cols; ++j) {
if (image.at<uchar>(i, j) > threshold) {
image.at<uchar>(i, j) = 255;
} else {
image.at<uchar>(i, j) = 0;
}
}
}
int main() {
cv::Mat image = cv::imread("input_image.jpg", cv::IMREAD_GRAYSCALE);
if (image.empty()) {
std::cerr << "Could not read the image" << std::endl;
return -1;
}
applyMaxEntropyThreshold(image);
cv::imwrite("max_entropy_thresholded_image.jpg", image);
return 0;
}
代码解释:
entropy
函数:- 计算信息熵,输入为概率向量,根据熵的公式计算熵值。
- 遍历概率向量,对于非零概率元素,计算其熵贡献并累加到
ent
中。
maxEntropyThreshold
函数:- 首先统计图像的灰度直方图并计算每个灰度级的概率分布。
- 遍历所有可能的阈值,将图像划分为前景和背景,计算前景和背景的概率分布并归一化。
- 计算前景和背景的熵并求和,找到使总熵最大的阈值。
applyMaxEntropyThreshold
函数:- 调用
maxEntropyThreshold
函数获取阈值,将图像中大于阈值的像素设为 255,其余设为 0。
- 调用
main
函数:- 读取灰度图像,调用
applyMaxEntropyThreshold
函数进行阈值分割并保存结果。
- 读取灰度图像,调用
(二)迭代阈值法
#include <iostream>
#include <opencv2/opencv.hpp>
double meanValue(cv::Mat& image, cv::Rect roi) {
cv::Scalar mean = cv::mean(image(roi));
return mean[0];
}
int iterativeThreshold(cv::Mat& image) {
double T = meanValue(image, cv::Rect(0, 0, image.cols, image.rows));
double T_new;
do {
cv::Mat group1, group2;
cv::Mat(image, image < T).copyTo(group1);
cv::Mat(image, image >= T).copyTo(group2);
double m1 = meanValue(group1, cv::Rect(0, 0, group1.cols, group1.rows));
double m2 = meanValue(group2, cv::Rect(0, 0, group2.cols, group2.rows));
T_new = (m1 + m2) / 2;
if (std::abs(T_new - T) < 1) break;
T = T_new;
} while (true);
return static_cast<int>(T);
}
void applyIterativeThreshold(cv::Mat& image) {
int threshold = iterativeThreshold(image);
for (int i = 0; i < image.rows; ++i) {
for (int j = 0; j < image.cols; ++j) {
if (image.at<uchar>(i, j) >= threshold) {
image.at<uchar>(i, j) = 255;
} else {
image.at<uchar>(i, j) = 0;
}
}
}
int main() {
cv::Mat image = cv::imread("input_image.jpg", cv::IMREAD_GRAYSCALE);
if (image.empty()) {
std::cerr << "Could not read the image" << std::endl;
return -1;
}
applyIterativeThreshold(image);
cv::imwrite("iterative_thresholded_image.jpg", image);
return 0;
}
代码解释:
meanValue
函数:- 计算指定区域的图像均值,使用
cv::mean
函数计算均值并返回。
- 计算指定区域的图像均值,使用
iterativeThreshold
函数:- 初始化阈值为图像的平均灰度值。
- 根据当前阈值将图像划分为两组,计算两组的平均灰度,更新阈值。
- 重复上述过程直到前后两次阈值差小于 1。
applyIterativeThreshold
函数:- 调用
iterativeThreshold
函数获取阈值,根据阈值对图像进行分割。
- 调用
main
函数:- 读取灰度图像,调用
applyIterativeThreshold
函数进行阈值分割并保存结果。
- 读取灰度图像,调用
(三)矩保持法
#include <iostream>
#include <opencv2/opencv.hpp>
#include <vector>
#include <cmath>
double moment(std::vector<double>& prob, int order) {
double m = 0;
for (int i = 0; i < 256; ++i) {
m += std::pow(i, order) * prob[i];
}
return m;
}
std::vector<double> solveEquations(double m0, double m1, double m2) {
// 求解方程组
double a = m0 - 1;
double b = m1;
double c = m2 - m1 * m1;
double delta = b * b - 4 * a * c;
if (delta < 0) {
std::cerr << "No real roots for the equation" << std::endl;
return {};
}
double root1 = (-b + std::sqrt(delta)) / (2 * a);
double root2 = (-b - std::sqrt(delta)) / (2 * a);
return {root1, root2};
}
int momentPreservingThreshold(cv::Mat& image) {
std::vector<int> histogram(256, 0);
int totalPixels = image.rows * image.cols;
for (int i = 0; i < image.rows; ++i) {
for (int j = 0; j < image.cols; ++j) {
histogram[image.at<uchar>(i, j)]++;
}
}
std::vector<double> prob(256, 0);
for (int i = 0; i < 256; ++i) {
prob[i] = static_cast<double>(histogram[i]) / totalPixels;
}
double m0 = moment(prob, 0);
double m1 = moment(prob, 1);
double m2 = moment(prob, 2);
std::vector<double> roots = solveEquations(m0, m1, m2);
int threshold;
if (roots.size() == 2) {
threshold = std::round(std::min(roots[0], roots[1]));
} else {
std::cerr << "Failed to find valid threshold" << std::endl;
return 0;
}
return threshold;
}
void applyMomentPreservingThreshold(cv::Mat& image) {
int threshold = momentPreservingThreshold(image);
for (int i = 0; i < image.rows; ++i) {
for (int j = 0; j < image.cols; ++j) {
if (image.at<uchar>(i, j) > threshold) {
image.at<uchar>(i, j) = 255;
} else {
image.at<uchar>(i, j) = 0;
}
}
}
int main() {
cv::Mat image = cv::imread("input_image.jpg", cv::IMREAD_GRAYSCALE);
if (image.empty()) {
std::cerr << "Could not read the image" << std::endl;
return -1;
}
applyMomentPreservingThreshold(image);
cv::imwrite("moment_preserving_thresholded_image.jpg", image);
return 0;
}
代码解释:
moment
函数:- 计算图像的 阶矩,根据矩的公式计算并返回。
solveEquations
函数:- 求解一元二次方程 ,计算判别式并求解,返回根。
momentPreservingThreshold
函数:- 统计图像的灰度直方图和概率分布。
- 计算图像的前三阶矩,调用
solveEquations
求解方程组得到阈值。
applyMomentPreservingThreshold
函数:- 调用
momentPreservingThreshold
函数获取阈值,对图像进行分割。
- 调用
main
函数:- 读取灰度图像,调用
applyMomentPreservingThreshold
函数进行阈值分割并保存结果。
- 读取灰度图像,调用
四、算法对比与选择
(一)计算性能
- 最大熵阈值法:由于需要计算多个熵值,计算量较大,尤其是对于高分辨率图像,计算时间较长。
- 迭代阈值法:通常在较少的迭代次数内可以收敛,计算速度相对较快,但依赖于初始阈值的选择和图像的特性。
- 矩保持法:涉及到方程组的求解,计算复杂度较高,可能在求解过程中出现不稳定情况,导致计算时间较长。
(二)分割效果
- 最大熵阈值法:对于具有复杂灰度分布的图像,尤其是当目标和背景的灰度分布有一定重叠时,能够找到较好的分割边界,对噪声有一定的抵抗能力。
- 迭代阈值法:在图像具有较明显的灰度差异时,能较快地找到合适的阈值,但对于噪声大或灰度分布复杂的图像可能效果不佳。
- 矩保持法:对于具有一定形状特征和结构的图像,能较好地利用图像的统计特性进行分割,但对噪声敏感,可能导致分割不准确。
(三)适用场景
- 最大熵阈值法:适用于处理各种类型的图像,尤其是需要精细分割且对噪声有一定容忍度的场景,如医学图像中软组织和骨骼的分割。
- 迭代阈值法:适合于快速的图像分割任务,对一些具有简单灰度分布的图像可以快速得到较好的结果,如文档图像中文本和背景的分离。
- 矩保持法:在需要利用图像形状信息进行分割的场景中较为适用,如对一些具有规则形状的目标进行分割,像电路板上的元件检测。
五、优化和改进
- 并行化处理:对于计算量较大的算法,如最大熵阈值法和矩保持法,可以利用多线程或 GPU 加速,如使用 OpenMP 或 CUDA 进行并行计算,提高计算速度。
- 预处理:在进行阈值分割前,可以对图像进行预处理,如使用高斯滤波、中值滤波等去噪处理,提高算法的鲁棒性,尤其是对于对噪声敏感的算法。
- 多阈值分割:上述算法主要针对单阈值分割,可以扩展为多阈值分割,将图像划分为多个区域,适用于更复杂的图像分割任务,如多目标识别。
六、结论
阈值分割是图像处理中一种强大的工具,不同的阈值分割算法适用于不同的场景和图像特性。在 C++ 环境下,通过 OpenCV 等库可以方便地实现这些算法,并且可以根据具体需求对算法进行优化和改进。在实际应用中,需要根据图像的特点、应用场景和性能要求选择合适的阈值分割算法,同时结合预处理和后处理技术,以达到最佳的图像分割效果。未来,随着图像处理和计算机视觉技术的不断发展,阈值分割算法将不断优化和创新,为更多复杂的图像分析任务提供更好的支持。
原文地址:https://blog.csdn.net/m0_44975814/article/details/145129894
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!