【opencv實踐】帶你再學一遍直方圖

今天給大家總結下直方圖的知識,爭取一文幫你搞定直方圖。
本文篇幅有點長,給大家列個目錄,大家可以跳着看:

  1. 直方圖介紹
  2. 使用opencv自帶繪製直方圖的函數繪製直方圖
  3. 自己定義函數進行直方圖繪製
  4. 直方圖均衡化簡介
  5. 直方圖均衡化自定義函數的實現

1:直方圖介紹

直方圖到底可以幹什麼呢?我覺得最明顯的作用就是有利於你對這個圖像進行分析了,直方圖就像我們常用的統計圖,只不過直方圖統計的是圖片的一些特徵,例如像素值(這是最常用的了)。
因此我們在開始前,先列個統計圖的例子,來幫助大家理解,也有利於我解釋一些概念:
在這裏插入圖片描述
我們統計了一個有11個學生的班級的身高和體重情況,身高爲160cm的有5人,170cm的有4人,180cm的有2人。然後看體重,體重160斤的有3人,170斤的有5人,180斤的有3人。

嘿嘿,有點怪異是不是,奈何我用ppt導入統計圖實在不是很會,就這樣吧

舉完例子,就開始學習吧,我覺得搞懂直方圖真的很有必要,所以你要靜下心來好好看下面的內容啦。
我們常規的統計圖,往往需要x軸,y軸,組距,統計對象等等,直方圖也一樣,有三個術語:

  • dims:需要統計的特徵的數目。如上面例子裏有身高和體重兩個特徵。
  • bins:每個特徵空間子區段的數目,可以翻譯爲“直條”和“組距”。

統計一個班級的身高和體重,身高就是一個特徵區間,身高有160,170,180三個段位,那麼子區段數目就是三。

  • range:每個特徵空間的取值範圍。例如:range = [0,255]。

上例中身高的取值範圍就是[160,180]

2:使用opencv自帶繪製直方圖的函數繪製直方圖

opencv提供了計算直方圖的函數calcHist(),函數原型:

    calcHist(
        const Mat*   images,    //輸入數組
        int          nimages,   //輸入數組個數
        const int*   channels,  //通道索引
        InputArray   mask;      //Mat(),  //不使用醃膜
        OutputArray  hist,      //輸出的目標直方圖,一個二維數組
        int       dims,      //需要計算的直方圖的維度  例如:灰度,R,G,B,H,S,V等數據
        congst int*  histSize,   //存放每個維度的直方圖尺寸的數組
        const float**    ranges, //每一維數組的取值範圍數組
        bool          uniform=true,   
        bool          accumulate = false
      );

爲什麼直方圖要計算呢?其實這個函數執行的就是統計的功能,比如我們統計灰度圖(灰度值爲[0,255])的各個灰度值的像素點個數,我們不能自己數吧?這個函數就可以返回一個二維數組告訴我們。

下面我們用這個函數畫一幅直方圖(我將代碼拆開講,但大家直接順次複製就可以了):

#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
  //【1】讀取原圖並顯示
  Mat srcImage = imread("5.jpg", 0);
  imshow("原圖:", srcImage);
  if (!srcImage.data) {
    cout << "fail to load image" << endl;
    return 0;
  }

首先,上面的是開頭,將需要計算的圖片載入並顯示,原圖如下圖:
在這裏插入圖片描述
但我們載入時

Mat srcImage = imread(5.jpg”, 0);

也就是按灰度圖載入的,所以顯示出來爲:
在這裏插入圖片描述

//【2】定義變量
  MatND dstHist;   
  int dims = 1;  //特徵數目(直方圖維度)
  float hranges[] = { 0,255 }; //特徵空間的取值範圍
  const float *ranges[] = { hranges };
  int size = 256;  //存放每個維度的直方圖的尺寸的數組
  int channels = 0;  //通道數

然後就需要定義變量了,MatND爲多維,多通道的密集數組類型
dims爲特徵數目,此程序只計算該圖片的一個特徵,且圖片是一張灰度圖,由後面的int channals = 0我們可以看出,計算的是該圖片的通道0,也就是灰度的直方圖。
hranges[]爲特徵空間的取值範圍數組,爲0-255;有幾個特徵就需要定義幾個這樣的數組,然後將這些數組存到
const float *ranges[] = { hranges }中。

當我們需要統計的直方圖包含多個特徵空間時,這麼做的意義就很明顯了,不如我計算一幅彩色圖RGB三個通道的直方圖,就需要有三個hranges[],然後將這三個放到const
float *ranges[]中,並傳給直方圖計算函數calcHist()

size爲存放每個維度的直方圖的尺寸的數組。因爲我們只統計灰度,所以用一個int也可以。

    //【3】計算直方圖
  calcHist(&srcImage, 1, &channels, Mat(), dstHist, dims, &size, ranges);
  int scale = 1;
  cout << dstHist << endl;

然後我們計算直方圖,並將結果傳遞給了dstHist,我們可以輸出看一下我們計算出來的直方圖到底是啥?(下圖只是截取了一小段):
在這裏插入圖片描述
我們可以看到輸出的是一個n行(其實應該是256行,因爲我們的灰度值是0-255)1列的數組,每一行代表圖像中在該灰度的像素點個數。
但很明顯這樣的輸出是不直觀的,所以我們要將直方圖進行繪製(也就是可視化):

  Mat dstImage(size * scale, size, CV_8U, Scalar(0));
  //【4】獲取最大值和最小值
  double minValue = 0;
  double maxValue = 0;
  minMaxLoc(dstHist, &minValue, &maxValue, 0, 0);
//【5】繪製直方圖
  int hpt = saturate_cast<int>(0.9*size);
  for (int i = 0; i < 256; i++)
  {
    float binValue = dstHist.at<float>(i);
    int realValue = saturate_cast<int>(binValue*hpt / maxValue);
    rectangle(dstImage, Point(i*scale, size - 1), Point((i + 1)*scale - 1, size - realValue), Scalar(255));
  }
  imshow("一維直方圖", dstImage);
  waitKey(0);
  return 0;
}

首先我們定義了一個畫布dstImage,我們就在它上面畫直方圖。
我們用第五行的
minMaxLoc(dstHist, &minValue, &maxValue, 0, 0);
返回了數組dstHist中的最大值和最小值。

爲什麼需要最大值和最小值呢?回想下我們畫統計圖時,是不是需要先知道人數最多的那個和最少的那個,然後才知道如何分派紙的空間。

然後變開始繪製,先進行讀取數值,然後對數值進行歸一化,然後用畫矩形的函數將柱形圖畫出來。

rectangle(
    img,  //輸入圖像
    pt1,  //矩陣的一個定點
    pt2,  //矩陣對角線上另一個頂點
    color, //線條顏色(RGB)或亮度(灰度圖像)(grayscale image)
    thickness,  //組成矩形的線條的粗細程度。取負值時函數繪製填充了色彩的矩形
    line_type,  //線條的類型  
    shift  //座標點的小數點位數
    );

上面程序第8行爲

int hpt = saturate_cast<int>(0.9*size);

感覺0.9出現的很突然,這一句其實是可以調整直方圖繪製的大小的,看了下面截圖應該就明白了:
在這裏插入圖片描述
當:

int hpt = saturate_cast(0.5*size); 時:

在這裏插入圖片描述
這下應該很清楚明白了吧?
但到目前爲止我們僅會用了一個函數而已,如果你沒有耐心了,可以先退出並收藏,或者關注公衆號【行走的機械人】。

3:自己定義函數進行直方圖繪製

然後我們自己來實現一個函數來進行一維直方圖的繪製。
我們來統計這幅圖的灰度圖的灰度直方圖。
在這裏插入圖片描述
首先看主函數:

int main(void)
{
  Mat img = imread("4.jpg",0);  //讀取圖片
  if (img.empty())   //判斷圖片是否爲空
  {
    cout << "圖片爲空";
    return -1;
  }
  imshow("灰度圖", img); //展示灰度圖
  int img_num[256] = { 0 };  //定義一個存放統計數據的數組
  Mat histogram; //定義直方圖
  histogram = histogram_draw(img, img_num);
  imshow("直方圖", histogram);
  waitKey(0);
}

主函數就很簡單啦,其中我們用了我們自定義的畫直方圖的函數
histogram_draw( )。
然後我們看自定義函數:

//@img:需要計算的圖像
//@img_num[]:計算直方圖的特徵空間子區段的數目
Mat histogram_draw(Mat img, int *img_num)
{
  int r = 200; //定義高
  int w = 1000; //定義寬
  Mat histogram = Mat(r, w, CV_8UC3); //直方圖畫布
  int row = img.rows;  //圖片的高度
  int col = img.cols;  //圖片的寬度
  for (int i = 0; i < row; i++)
  {
    for (int j = 0; j < col; j++)
    {
      int num = img.at<uchar>(i, j); //讀取圖片像素位置(i,j)處的灰度值
      img_num[num]++;  //將對應灰度值的個數加一
    }
  }
  int all = row * col;
  for (int i = 0; i < 256; i++)  //對灰度值0-255循環處理
  {
    int hight = int(double(img_num[i])  / double(all)*r); //對灰度值i進行歸一化
    //opencv圖像的像素座標系原點在左上角
    Point ps(i * 4, r);   
    Point pe(i * 4, r - hight);
    line(histogram, pe, ps, Scalar(0, 0, 255));
  }
  return histogram;
}

上面函數實現思想:
遍歷整幅圖像的像素點,統計灰度值0-256的像素點個數並存到數組img_num[]中
遍歷這個img_num[]數組,對灰度值進行歸一化,計算出的高度爲各灰度值所佔的比值
用畫直線函數進行繪製
最後運行程序,所畫直方圖爲:
在這裏插入圖片描述
可以看到右下角紅色的爲直方圖的柱形。
因爲不明顯,所以我們將上面程序第23行歸一化後的高再乘100來擴大,就可以明瞭的看出各灰度值所佔的比例了。
在這裏插入圖片描述
好了!到此我們已經會畫直方圖了,如果你沒有耐心了,可以先退出收藏,或者關注【行走的機械人】不迷路哦。

4:直方圖均衡化簡介

下面我們來說說直方圖均衡化,這是圖像處理的一大利器哦。
在這裏插入圖片描述
我們可以看到上面圖片灰濛濛的能見度很低,有沒有方法給它處理一下,來使細節更明顯呢?當然有了,就是直方圖均衡化。
opencv給了一個內置函數equalizeHist來幫助我們完成直方圖均衡化,這是個無腦函數,有兩個輸入,一個是原圖像,另一個就是與原圖像同大小的輸出圖像。我們先看看用該函數均衡化後的結果:
在這裏插入圖片描述
可以看到,細節要多很多了。我們用上面的畫直方圖函數來看看均衡化後直方圖:
在這裏插入圖片描述
可以看到灰度值的分佈要更爲均勻了,這就使均衡化的圖像對比度更爲明顯。細節也就更爲凸顯了。

那直方圖均衡化的實現原理呢?我推薦大家看岡薩雷斯的《數字圖像處理》第三章,講的很細緻。本人能力有限,在這裏我只能給大家照本宣科的簡單介紹一下了,大家可以關注我公衆號【行走的機械人】回覆【電子書資源】,裏面有這本書的電子版(還要其他近10G的我搜集的各種電子書)。

在原圖直方圖中,灰度值大部分之中在一小段區域,而其他部分都是空白的,我們要做的就是將這一小段區域展開到整個灰度範圍內(如上圖)。
如何展開到整個區域呢?我們可以製作一個映射表,將原本集中在一起的像素值映射到整幅圖中。
那映射的依據呢?比如我們原來有個像素點的灰度爲240,我們憑什麼把它映射爲灰度120呢?靠一個數學公式:
在這裏插入圖片描述
r0是我們圖像某個像素點的灰度值,T(r0)就是映射函數,S0就是映射後的灰度值。上式中我們r0本來爲0,映射後爲1.33。
再看一個:
在這裏插入圖片描述
上式就是灰度爲r1的像素點,r1=1,經過映射後S1爲3.08。
這樣看來,我們的目的是不是就達到了?
在深入看一下T()這個映射函數,它映射的算法是計算對應灰度的概率乘灰度的累加,還乘了個7,乘7是因爲我們只有(7+1)個灰度值。
我們從整體上來看一下:
在這裏插入圖片描述
我們以一幅圖的七個像素點來看,像素點的灰度值分佈本來爲:
在這裏插入圖片描述
經過映射函數T()之後灰度值:
在這裏插入圖片描述
再看一下分佈:
在這裏插入圖片描述
是不是更均勻了呢?
如果你明白一些原理了,那就繼續看下面的代碼吧,如果沒有,那肯定是我講的水平有限,你只能再去看我上面推薦的《數字圖像處理》這本書了。

5:直方圖均衡化自定義函數的實現

我們要做的是希望實現上面的函數T(),然後將函數T映射出來的新的灰度值存到數組中,並將原圖像中的灰度值進行替換。
把代碼放下面了,我都詳細註釋了,我就不講了,挺簡單的,越說越亂不如大家自己看看。


#include <iostream>
#include <opencv2/opencv.hpp>
#include <string.h>
using namespace std;
using namespace cv;
//@img:輸入灰度圖
//@int *img_num:定義一個存放統計數據的數組
//@double* ratio:存放各個灰度所佔比例的數組
//@int* map_num:映射數組
Mat junheng(Mat img, int *img_num, double* ratio, int* map_num)
{
  Mat map = img;  
  double gailv = 0.0;
  int row = img.rows; //獲取原圖的高和寬
  int col = img.cols;
  int all = row * col; //計算總像素點數
  for (int i = 0; i < row; i++)   //統計灰度值個數
  {
    for (int j = 0; j < col; j++)
    {
      int num = img.at<uchar>(i, j); //讀取圖片像素位置(i,j)處的灰度值
      img_num[num]++;  //將對應灰度值的個數加一
    }
  }
  for (int i = 0; i < 256; i++)  //計算灰度值概率
  {
    ratio[i] = double(img_num[i]) / double(all); //將概率存到數組中
  }
  for (int i = 0; i < 256; i++)  //設置映射數組
  {
    gailv += ratio[i];  //累計概率
    map_num[i] = int(gailv * 255 + 0.5);  //加0.5起到四捨五入的作用
  }
  for (int i = 0; i < row; i++)   //進行灰度值的映射(替換)
  {
    for (int j = 0; j < col; j++)
    {
      int num = img.at<uchar>(i, j);
      map.at<uchar>(i, j) = map_num[num];
    }
  }
  return map;  //返回均衡完畢的圖像
}
int main(void)
{
  Mat img_copy,img = imread("4.jpg",32);  //讀取圖片
  img.copyTo(img_copy);
  if (img.empty())   //判斷圖片是否爲空
  {
    cout << "圖片爲空";
    return -1;
  }
  imshow("灰度圖", img); //展示灰度圖
  double ratio[256] = { 0 };  //存放各個灰度所佔比例的數組
  int map_num[256] = { 0 };   //映射數組
  int img_num[256] = { 0 };  //定義一個存放統計數據的數組
  img_copy=junheng(img, img_num,ratio, map_num);  //均衡化
  imshow("均衡化", img_copy);
  waitKey(0);
}

直方圖我們就到這裏啦,除了上面說的,直方圖還有很多其他的東西,比如直方圖匹配,直方圖規定化等等,因爲篇幅就不介紹了,還是推薦大家去看看《數字圖像處理》這本書。

有疑問或者有錯的地方,歡迎大家評論哦~

作者簡介

最後,歡迎大家關注我的微信公衆號【行走的機械人】,我會在上面更新視覺以及深度學習的知識哦,一起來學習吧!
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章