作者:翟天保Steven
著作權宣告:著作權歸作者所有,商業轉載請聯系作者獲得授權,非商業轉載請注明出處
需求說明
在對影像進行處理時,經常會有這類需求:想通過閾值對影像進行二值化分割,以提取自己感興趣的區域,常見的閾值分割方法有常數分割、最大類間方差法、雙峰分割、三角法等等,不同的場景應用不同的閾值方法,
今天要講的方法,適合當影像的直方圖具有明顯單峰特征時使用,結合了三角法的原理而設計,相比較OpenCV自帶的三角法,好處是可以根據自身需求合理修改函式;如果用OpenCV庫的函式,只有一個介面,若不能達到較理想的應用效果,就束手無策了,
下面介紹具體實作流程,
具體流程
1)取影像的灰度圖,并遍歷統計0-255各個灰度值所出現的次數,
cv::Mat src = imread("test.jpg", 0);
cv::Mat hist = cv::Mat::zeros(1, 256, CV_32FC1);
for (int i = 0; i < src.rows; ++i)
{
for (int j = 0; j < src.cols; ++j)
{
hist.at<float>(0, src.at <uchar>(i, j))++;
}
}
2)去除0和255的直方圖資料,這一步就是OpenCV三角法所沒有的,很多人可能不理解為什么要這一步,在你對影像進行閾值化時如果提前進行了相關的運算,可能導致結果大于255的數值全部變為255,或者數值低于0的數值全部變為0,這就使得0和255的數值其實涵蓋了許多數值,呈累加態,很容易形成雙峰,這樣就很難找到我們真正想要的峰,例如0和255的數值都是10000左右,0略大一些,而我們的真峰是在250左右的灰度值,數值只有8000多,那么在后續閾值計算時就會因為峰的方向錯了而帶來毀滅性打擊,別覺得我說夸張了,只有自己去碰碰壁才能深刻領悟我說的,
hist.at<float>(0, 255) = 0;
hist.at<float>(0, 0) = 0;
3)確認峰值位置,maxidx是峰值對應的灰度值,max是峰值高度,也是灰度值對應資料的個數,
float max = 0;
int maxidx = 0;
for (int i = 0; i < 256; ++i)
{
if (hist.at<float>(0, i) > max)
{
max = hist.at<float>(0, i);
maxidx = i;
}
}
4)判斷峰值在左側還是右側,true為左側,false為右側,
bool lr = maxidx < 127;
5)當在左側時,連接峰值(maxidx,max)和(255,0)點,用兩點建立直線公式,如下圖所示公式, L的運算式可以轉換為Ax+By+C=0的形式,A是-max,B是maxidx-255,C是max*255,在結合距離公式可以計算出直方圖曲線上每個點到直線的距離,取距離最長的那個點作為閾值,

if (lr)
{
float A = float(-max);
float B = float(maxidx - 255);
float C = float(max * 255);
for (int i = maxidx + 1; i < 256; ++i)
{
float x0 = float(i);
float y0 = hist.at<float>(0, i);
float d = abs(A * x0 + B * y0 + C) / std::sqrt(A * A + B * B);
if (d > maxd)
{
maxd = d;
maxdidx = i;
}
}
}
6)右側同理,連接峰值(maxidx,max)和(0,0)點,公式ABC如代碼所示,
else {
float A = float(-max);
float B = float(maxidx);
float C = 0.0f;
for (int i = 0; i < maxidx; ++i)
{
float x0 = float(i);
float y0 = hist.at<float>(0, i);
float d = abs(A * x0 + B * y0 + C) / std::sqrt(A * A + B * B);
if (d > maxd)
{
maxd = d;
maxdidx = i;
}
}
}
7)二值化,完成,
result.setTo(255, src > maxdidx);
idx = maxdidx;
return result;
功能函式
// 單峰三角閾值法
cv::Mat Thresh_Unimodal(cv::Mat &src, int& idx)
{
cv::Mat result = cv::Mat::zeros(src.size(), CV_8UC1);
// 統計直方圖
cv::Mat hist = cv::Mat::zeros(1, 256, CV_32FC1);
for (int i = 0; i < src.rows; ++i)
{
for (int j = 0; j < src.cols; ++j)
{
hist.at<float>(0, src.at<uchar>(i, j))++;
}
}
hist.at<float>(0, 255) = 0;
hist.at<float>(0, 0) = 0;
// 搜索最大值位置
float max = 0;
int maxidx = 0;
for (int i = 0; i < 256; ++i)
{
if (hist.at<float>(0, i) > max)
{
max = hist.at<float>(0, i);
maxidx = i;
}
}
// 判斷最大點在哪一側,true為左側,false為右側
bool lr = maxidx < 127;
float maxd = 0;
int maxdidx = 0;
// 假設在左側
if (lr)
{
float A = float(-max);
float B = float(maxidx - 255);
float C = float(max * 255);
for (int i = maxidx + 1; i < 256; ++i)
{
float x0 = float(i);
float y0 = hist.at<float>(0, i);
float d = abs(A * x0 + B * y0 + C) / std::sqrt(A * A + B * B);
if (d > maxd)
{
maxd = d;
maxdidx = i;
}
}
}
// 假設在右側
else {
float A = float(-max);
float B = float(maxidx);
float C = 0.0f;
for (int i = 0; i < maxidx; ++i)
{
float x0 = float(i);
float y0 = hist.at<float>(0, i);
float d = abs(A * x0 + B * y0 + C) / std::sqrt(A * A + B * B);
if (d > maxd)
{
maxd = d;
maxdidx = i;
}
}
}
// 二值化
result.setTo(255, src > maxdidx);
idx = maxdidx;
return result;
}
C++測驗代碼
#include <iostream>
#include <time.h>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
cv::Mat DrawHistImg(cv::Mat &hist);
cv::Mat Thresh_Unimodal(cv::Mat &src, int& idx);
int main()
{
cv::Mat src = imread("test.jpg", 0);
// 繪制均衡化后直方圖
cv::Mat hrI = DrawHistImg(src);
// 單峰三角閾值法
int thresh;
cv::Mat result = Thresh_Unimodal(src, thresh);
cout << " thresh: " << thresh << endl;
imshow("original", src);
imshow("hist", hrI);
imshow("result", result);
waitKey(0);
return 0;
}
// 繪制簡易直方圖
cv::Mat DrawHistImg(cv::Mat &src)
{
cv::Mat hist = cv::Mat::zeros(1, 256, CV_32FC1);
for (int i = 0; i < src.rows; ++i)
{
for (int j = 0; j < src.cols; ++j)
{
hist.at<float>(0, src.at <uchar>(i, j))++;
}
}
cv::Mat histImage = cv::Mat::zeros(540, 1020, CV_8UC1);
const int bins = 255;
double maxValue;
cv::Point2i maxLoc;
cv::minMaxLoc(hist, 0, &maxValue, 0, &maxLoc);
int scale = 4;
int histHeight = 540;
for (int i = 0; i < bins; i++)
{
float binValue = (hist.at<float>(i));
int height = cvRound(binValue * histHeight / maxValue);
cv::rectangle(histImage, cv::Point(i * scale, histHeight),
cv::Point((i + 1) * scale - 1, histHeight - height), cv::Scalar(255), -1);
}
return histImage;
}
// 單峰三角閾值法
cv::Mat Thresh_Unimodal(cv::Mat &src, int& idx)
{
cv::Mat result = cv::Mat::zeros(src.size(), CV_8UC1);
// 統計直方圖
cv::Mat hist = cv::Mat::zeros(1, 256, CV_32FC1);
for (int i = 0; i < src.rows; ++i)
{
for (int j = 0; j < src.cols; ++j)
{
hist.at<float>(0, src.at<uchar>(i, j))++;
}
}
hist.at<float>(0, 255) = 0;
hist.at<float>(0, 0) = 0;
// 搜索最大值位置
float max = 0;
int maxidx = 0;
for (int i = 0; i < 256; ++i)
{
if (hist.at<float>(0, i) > max)
{
max = hist.at<float>(0, i);
maxidx = i;
}
}
// 判斷最大點在哪一側,true為左側,false為右側
bool lr = maxidx < 127;
float maxd = 0;
int maxdidx = 0;
// 假設在左側
if (lr)
{
float A = float(-max);
float B = float(maxidx - 255);
float C = float(max * 255);
for (int i = maxidx + 1; i < 256; ++i)
{
float x0 = float(i);
float y0 = hist.at<float>(0, i);
float d = abs(A * x0 + B * y0 + C) / std::sqrt(A * A + B * B);
if (d > maxd)
{
maxd = d;
maxdidx = i;
}
}
}
// 假設在右側
else {
float A = float(-max);
float B = float(maxidx);
float C = 0.0f;
for (int i = 0; i < maxidx; ++i)
{
float x0 = float(i);
float y0 = hist.at<float>(0, i);
float d = abs(A * x0 + B * y0 + C) / std::sqrt(A * A + B * B);
if (d > maxd)
{
maxd = d;
maxdidx = i;
}
}
}
// 二值化
result.setTo(255, src > maxdidx);
idx = maxdidx;
return result;
}
測驗效果
通過imagewatch插件可以觀察閾值203是不是在距離最遠的位置,答案是肯定的,
如果函式有什么可以改進完善的地方,非常歡迎大家指出,一同進步何樂而不為呢~
如果文章幫助到你了,可以點個贊讓我知道,我會很快樂~加油!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/377328.html
標籤:其他
上一篇:卡爾曼濾波器
