直方圖與匹配
- 一、直方圖簡介
- 二、直方圖統計
- 三、直方圖比較
- 四、直方圖均衡化
- 五、自適應的直方圖均衡化
- 六、直方圖反向投影
- 七、模板匹配
一、直方圖簡介
圖像直方圖(Histogram)是一種頻率分布圖,它描述了不同強度值在圖像中出現的頻率。圖像直方圖可以統計任何圖像特征,如灰度、飽和度、梯度等。
彩色圖像的亮度直方圖就是其灰度圖的直方圖。亮度直方圖考慮了所有顏色通道,但有時也需要對單種顏色通道進行觀察分析。計算單種顏色通道直方圖時,每種顏色通道都作為一個獨立的灰度圖像,分別計算其直方圖。各種顏色通道的直方圖有時近似的,有時則相差甚遠,特別是當圖像偏于某一色系時。
在討論直方圖時經常涉及以下3個概念:
- Dims(維數):需要統計的特征的維數,一般情況下,圖像直方圖統計的特征只有一種,即灰度,此時的維數等于1。
- Bins(組距):每個特征空間子區段的數目
- Range(范圍):需要統計的特征的取值范圍。通常情況下,圖像直方圖的灰度范圍為[0, 255]
下面用一個示例說明直方圖的畫法。假設有一副8*8的圖像,其灰度數據如下,為了簡化起見,將灰度區段數(Bins)設為16,編號為b0~b15,具體如下:
[0, 255] = [0, 15] U [16, 31] U...U [240, 255];
其中b0==[0,15],b1=[16,31],...,b15=[240,255]
//灰度值的取值范圍為[0, 255]
灰度值數據
38 | 130 | 167 | 191 | 215 | 180 | 33 | 18 |
---|---|---|---|---|---|---|---|
154 | 165 | 39 | 10 | 66 | 2 | 185 | 24 |
243 | 252 | 62 | 213 | 94 | 54 | 68 | 3 |
139 | 2 | 204 | 111 | 1 | 189 | 204 | 83 |
119 | 188 | 60 | 241 | 154 | 196 | 244 | 169 |
44 | 100 | 17 | 204 | 28 | 185 | 166 | 196 |
163 | 100 | 17 | 204 | 28 | 185 | 166 | 196 |
226 | 4 | 220 | 55 | 87 | 28 | 166 | 146 |
第一步,將灰度值轉換為Bin的編號,方法是將灰度值除以16,然后舍棄小數部分,如38除以16等于2,130除以16等于8等,轉換后的數據如下:
2 | 8 | 10 | 11 | 13 | 11 | 2 | 1 |
---|---|---|---|---|---|---|---|
9 | 10 | 2 | 0 | 4 | 0 | 11 | 1 |
15 | 15 | 3 | 13 | 5 | 3 | 4 | 0 |
8 | 0 | 12 | 6 | 0 | 11 | 12 | 5 |
7 | 11 | 3 | 15 | 9 | 12 | 15 | 10 |
2 | 1 | 14 | 6 | 2 | 12 | 11 | 12 |
10 | 6 | 1 | 12 | 1 | 11 | 10 | 12 |
14 | 0 | 13 | 3 | 5 | 1 | 10 | 9 |
第二步,統計16個灰度范圍(Bin的編號)的個數,如下表
Bin | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 合計 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
個數 | 6 | 6 | 5 | 4 | 2 | 3 | 3 | 1 | 2 | 3 | 6 | 7 | 7 | 3 | 2 | 4 | 64 |
第三步,根據上表,畫出直方圖如下,x軸代表一個灰度范圍,y軸代表個數:
由于直方圖只統計數量而不考慮像素在圖像中的位置,因而具有平移性、旋轉和縮放不變性。也正是因為這個原因,兩幅截然不同不同的圖像的直方圖可能是一樣的。
通過對直方圖的分析,可以發現有關亮度(曝光)和對比度的問題,并可以了解一張圖像是否有效利用了整個強度范圍。直方圖的均值和中值可用來描述圖像的亮度,其中中值比均值更具穩健性:直方圖的標準差(或方差)則可以用來描述圖像的對比度。
由于直方圖中數字越大亮度也越大,因此直方圖中的柱形明顯擊中于中間和右側,左側靠近0(黑色)的位置則非常稀疏,這說明這張圖像整體偏亮。直方圖的峰值都集中在左側的圖像往往曝光不足,而擊中在右側的圖像則往往曝光過度。對比度的高低在直方圖上也是一目了然的。如果圖像的大部分像素集中在直方圖的某個范圍,則說明其對比度較低,如果像素擴展至直方圖整個范圍,則對比度較高。
二、直方圖統計
在OpenCV中繪制直方圖需要先進行直方圖統計,然后用繪圖函數把直方圖繪制出來。
//圖像直方圖的數據統計
void Imgproc.calcHist(List<Mat> images, MatOfint channels, Mat mask, Mat hist, MatOfInt histSize, MatOfFloat ranges)
- image:輸入圖像
- channels:需要統計直方圖的第幾通道:如輸入圖像是灰度圖,則它的值是0,如是彩色圖像,則用0、1、2代表B、G、R三個通道
- mask:掩膜,如是整幅圖像的直方圖,則無須定義
- hist:直方圖計算結束
- histSize:直方圖被分成多少個取件,即bin的個數
- ranges:像素取值范圍,通常為0-255
上述函數只負責統計數據,如果想要看到直方圖,則還需要用繪圖函數把直方圖畫出來,但是這里有一個小問題。直方圖統計出來的是灰度值范圍的個數,有的值可能很大,有的則可能很小,要在一張圖像中把它們畫出來需要先統計出這些值的最大值,然后根據比例畫出來。這樣相當繁瑣,而用OpenCV中的normalize()函數進行歸一化就可以解決這個問題。
//對矩陣進行歸一化
void Core.normalize(Mat src, Mat dst, double alpha, double beta, int norm_type)
- src:輸入圖像
- dst:輸出矩陣,與src具有同樣的尺寸
- alpha:歸一化后的上限值
- beta:歸一化后的上限值
- norm_type:歸一化類型,常用參數如下:
- Core.NORM_INF:無窮范數,向量最大值
- Core.NORM_L1:L1范數,絕對值之和
- Core.NORM_L2:L2范數,平方和之平方根
- Core.NORM_L2SQR:L2范數,平方和
- Core.NORM_MINMAX:偏移歸一化
public class CalcHist {static {OpenCV.loadLocally(); // 自動下載并加載本地庫}public static void main(String[] args) {//讀取圖像灰度圖并顯示Mat src = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/key.jpg", Imgcodecs.IMREAD_GRAYSCALE);HighGui.imshow("src", src);HighGui.waitKey(0);//參數準備List<Mat> mat = new ArrayList<>();mat.add(src);float[] range = {0, 256};//直方圖統計值范圍Mat hist = new Mat();MatOfFloat histRange = new MatOfFloat(range);//直方圖數據統計并歸一化Imgproc.calcHist(mat, new MatOfInt(0), new Mat(), hist, new MatOfInt(256), histRange);//直方圖尺寸int width = 512;int height = 400;Core.normalize(hist, hist, 0, height, Core.NORM_MINMAX);//將直方圖數據轉存到數組中以便后續使用float[] histData = new float[(int)(hist.total() * hist.channels())];hist.get(0, 0, histData);//繪制直方圖Scalar black = new Scalar(0, 0, 0);Scalar white = new Scalar(255, 255, 255);Mat histImage = new Mat(height, width, CvType.CV_8UC3, black);int binWid = (int)Math.round((double)width / 256);//bin的寬度for (int i = 0; i < 256; i++) {Imgproc.line(histImage, new Point(i * binWid, height), new Point(i * binWid, height - Math.round(histData[i])), white, binWid);}//在屏幕上顯示繪制的直方圖HighGui.imshow("calcHist", histImage);HighGui.waitKey(0);System.exit(0);}
}
原圖:
直方圖:
三、直方圖比較
由于直方圖反映了圖像的灰度值的分布特性,因而通過直方圖的比較可以在一定程度上了解兩幅圖像的相似程度。當然,由于兩幅截然不同的圖像的直方圖可能是完全一樣的,這種比較只能作為參考。
//比較兩幅直方圖。此函數適用于一維、二維、三維密集直方圖,但可能不適用于高維稀疏直方圖
double Imgproc.compareHist(Mat h1, Mat h2, int method)
- h1:第一個直方圖
- h2:第二個直方圖
- method:比較方法,如下:
public class CompareHist {static {OpenCV.loadLocally(); // 自動下載并加載本地庫}public static void main(String[] args) {//讀取圖像灰度圖并顯示Mat src = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/pond.png", Imgcodecs.IMREAD_GRAYSCALE);HighGui.imshow("src", src);HighGui.waitKey(0);//對圖像進行中值濾波并顯示Mat src2 = new Mat();Imgproc.medianBlur(src, src2, 5);HighGui.imshow("Median Blur", src2);HighGui.waitKey(0);//直方圖參數設置float[] range = {0, 256};MatOfFloat histRange = new MatOfFloat(range);//圖像1的直方圖數據統計并歸一化Mat hist = new Mat();List<Mat> matList = new LinkedList<>();matList.add(src);Imgproc.calcHist(matList, new MatOfInt(0), new Mat(), hist, new MatOfInt(256), histRange);Core.normalize(hist, hist, 0, 400, Core.NORM_MINMAX);//圖像2的直方圖數據統計并歸一化Mat hist2 = new Mat();List<Mat> matList2 = new LinkedList<>();matList2.add(src2);Imgproc.calcHist(matList2, new MatOfInt(0), new Mat(), hist2, new MatOfInt(256), histRange);Core.normalize(hist2, hist2, 0, 400, Core.NORM_MINMAX);double s = Imgproc.compareHist(hist, hist2, Imgproc.HISTCMP_CORREL);System.out.println("相似度:" + s);System.exit(0);}
}
相似度:0.9987133134938458
程序中用于比較兩幅圖像中一幅是未處理的原圖像。另一幅是經過中值濾波后的圖像。經過比較兩者相似度約為0.9987。由于比較方法用的是相關性比較,完全一致時相似度為1,此結果顯示兩者相似度非常高。
當然,如前所述,直方圖只統計數量而不考慮像素在圖像中的位置,因而兩幅截然不同的圖像的直方圖可能是一樣的。直方圖的比較結果完全匹配也并不能說明兩幅圖像是一樣的,但如果兩幅圖像完全一樣,則它們的直方圖必然是完全匹配的。
四、直方圖均衡化
在曝光不足或曝光過度時,直方圖往往集中在一個區域,而解決問題的方法就是直方圖均衡化。所謂直方圖均衡化,就是盡可能地讓一張圖像的像素占據全部可能的灰度級并且分布均勻,從而具有較高的對比度。直方圖均衡化的原理圖如下:
//對圖像進行直方圖均衡化
void Imgproc.equalizeHist(Mat src, Mat dst)
- src:輸入圖像,必須是8位單通道圖像
- dst:輸出圖像,和src具有相同的尺寸和數據類型
public class EqualizeHist {static {OpenCV.loadLocally(); // 自動下載并加載本地庫}public static void main(String[] args) {//讀取圖像灰度圖并顯示Mat src = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/grotto.jpg", Imgcodecs.IMREAD_GRAYSCALE);HighGui.imshow("src", src);HighGui.waitKey(0);//直方圖均衡化并在屏幕上顯示結果Mat dst = new Mat();Imgproc.equalizeHist(src, dst);HighGui.imshow("dst", dst);HighGui.waitKey(0);System.exit(0);}
}
原圖:
直方圖均衡化:
五、自適應的直方圖均衡化
直方圖均衡對于背景和前景都太亮或太暗的圖像很有效,但是在很多情況下其效果并不理想。直方圖均衡化主要存在以下兩個問題:
- 某些區域由于對比度增強過大而成為噪點
- 某些區域調整后變得更暗或更亮,從而丟失細節
針對上述兩個問題,先后有人提出了對比度限制直方圖均衡算法(CLHE算法)和自適應直方圖均衡算法(AHE算法)。
CLHE算法在HE算法的基礎上加入了對比度限制,算法中設置了一個直方圖分布的閾值,將超過該閾值的部分均勻地分散到其他Bins中,其原理如下:
AHE算法則將圖像分成很多小塊,對每個小塊進行直方圖均衡化,然后將這些小塊拼接起來,但是這樣又產生了新的問題,由于對每個小塊進行均衡化時的參數不同,小塊之間會產生一些邊界。
限制對比度自適應直方圖均衡化(CLAHE算法)綜合了這兩個算法的優點,并通過雙線性差值的方法對小塊進行縫合以消除邊界問題。嚴格的說,自適應的直方圖均衡化算法是指AHE算法,而不是CLAHE算法。不過,為了簡化起見,目前在提起 自適應的直方圖均衡化算法 時所指的基本是CLAHE算法。
為了實現這個算法,OpenCV中專門設置了CLAHE類。CLAHE算法的實現一般需要如下兩步:
1. 創建一個CLAHE類:
CLAHE clahe = Imgproc.createCLAHE();
2. 調用CLAHE.apply()函數進行自適應的直方圖均衡化:
clahe.apply(src, dst);
public class Clahe {static {OpenCV.loadLocally(); // 自動下載并加載本地庫}public static void main(String[] args) {//讀取圖像灰度圖并顯示Mat src = Imgcodecs.imread("F:/IDEAworkspace/opencv/src/main/java/demo2/grotto.jpg", Imgcodecs.IMREAD_GRAYSCALE);HighGui.imshow("src", src);HighGui.waitKey(0);//直方圖均衡化并顯示結果Mat dst = new Mat();Imgproc.equalizeHist(src, dst);HighGui.imshow("dst", dst);HighGui.waitKey(0);//自適應直方圖均衡化并顯示CLAHE clahe = Imgproc.createCLAHE();clahe.apply(src, dst);HighGui.imshow("CLAHE", dst);HighGui.waitKey(0);System.exit(0);}
}
原圖:
直方圖均衡化:
自適應直方圖均衡化:
總體來看,直方圖均衡化后的圖像黑白對比仍然強烈,佛像背后有一塊黑色陰影,細節根本看不到,另外佛像下方底座部分仍然曝光過度。自適應的直方圖均衡化后的圖像總體對比度降低、因而細節展現更多。得益于分塊均衡化的算法,佛像背后的陰影部分沒有那么黑了,因而可以看到一些細節;另外佛像下方曝光過度問題也大有改善,但是左上角出現了明顯的塊狀,這是原圖中沒有的,這就是CLAHE算法在分塊均衡化后縫合效果不理想的表現,也可以說是這個算法的一個副作用。
六、直方圖反向投影
直方圖反向投影是指先計算某一特征的直方圖模型,然后使用該模型去尋找圖像中是否存在該特征。反向投影可用于檢測輸入圖像在給定圖像中最匹配的區域,因而常用于目標追蹤的MeanShift算法配合使用。
//對圖像直方圖進行反向投影
void Imgproc.calcBackProject(List<Mat> images, MatOfInt channels, Mat hist, Mat dst, MatOfFloat ranges, double scale)
- images:輸入的圖像集,所有圖像應具有相同的尺寸和數據類型,但通道數可以不同,圖像深度應為CV_8U、CV_16U或CV_32F
- channels:需要統計的通道索引
- hist:輸入的直方圖
- dst:輸出的反向投影圖像
- ranges:直方圖中bin的取值范圍
- scale:輸出的反向投影的縮放因子
public class BackProject {static {OpenCV.loadLocally(); // 自動下載并加載本地庫}public static void main(String[] args) {//讀取圖像轉換為HSV色彩空間并顯示Mat src = Imgcodecs.imread("F:/IDEAworkspace/opencv/src/main/java/demo/leaf.png");Mat hsv = new Mat();Imgproc.cvtColor(src, hsv, Imgproc.COLOR_BGR2HSV);HighGui.imshow("leaf", hsv);HighGui.waitKey(0);//將圖像的hue(色調)通道提取至hueList中Mat hue = new Mat(hsv.size(), hsv.depth());List<Mat> hsvList = new LinkedList<>();List<Mat> hueList = new LinkedList<>();hsvList.add(hsv);hueList.add(hue);Core.mixChannels(hsvList, hueList, new MatOfInt(0, 0));//直方圖參數設置int bins = 25;int histSize = Math.max(bins, 2);float[] hueRange = {0, 180};//直方圖數據統計并歸一化Mat hist = new Mat();Imgproc.calcHist(hueList, new MatOfInt(0), new Mat(), hist, new MatOfInt(histSize), new MatOfFloat(hueRange));Core.normalize(hist, hist, 0, 255, Core.NORM_MINMAX);//計算反向投影并顯示Mat backproj = new Mat();Imgproc.calcBackProject(hueList, new MatOfInt(0), hist, backproj, new MatOfFloat(hueRange), 1);HighGui.imshow("calcHist", backproj);HighGui.waitKey(0);System.exit(0);}
}
原圖HSV:
反向投影:
七、模板匹配
直方圖反向投影可用于檢測輸入圖像在給定圖像中最匹配的區域,但是由于直方圖的局限性,直方圖反向投影得到的匹配結果只能作為參考,如果需要精確匹配,則還需要用到模板匹配。
模板匹配是指在一張圖像中尋找與另一幅模板圖像最佳匹配(相似)區域。所謂模板,就是用來對比的圖像。模板匹配的具體方法是:在待匹配圖像中選擇與模板相同尺寸的滑動窗口,然后不斷地移動滑動窗口,計算其與圖像中相應區域的匹配度,最終匹配度最高的區域即為匹配結果。
//在圖像中尋找與模板匹配的區域
void Imgproc.matchTemplate(Mat image, Mat temp1, Mat result, int method)
- image:待匹配圖像,要求是8位或32位浮點圖像
- temp1:模板圖像,其數據類型與待匹配圖像相同,并且尺寸不能大于待匹配圖像
- result:輸出的匹配圖,必須是32位浮點圖像。如果待匹配圖像的尺寸為WH,模板圖像尺寸為wh,則輸出圖像尺寸為(W-w+1)*(H-h+1)
- method:匹配方法,可選參數如下,相應計算公式如下表
- Imgproc.TM_SQDIFF:平方差匹配法。完全匹配時計算值為0,匹配度越低數值越大
- Imgproc.TM_SQDIFF_NORMED:歸一化平方差匹配法。將平方差匹配法歸一化到0~1
- Imgproc.TM_CCORR:相關匹配法。0為最差匹配,數值越大匹配效果越好
- Imgproc.TM_CCORR_NORMED:歸一化相關匹配法。將相關匹配法歸一化到0~1
- Imgproc.TM_CCOEFF:系數匹配法。數值越大匹配度越高,數值越小匹配度越低
- Imgproc.TM_CCOEFF_NORMED:歸一化系數匹配法。將系數匹配法歸一化到-1~1,1表示完全匹配,-1表示完全不匹配
由于matchTemplate()函數只是計算各個區域的匹配度,要得到最佳匹配還需要用minMaxLoc()函數來定位:
//尋找矩陣中的最大值和最小值及在矩陣中的位置
MinMaxLocResult Core.minMaxLoc(Mat src)
- src:輸入矩陣,必須是單通道
public class MatchTemplate {static {OpenCV.loadLocally(); // 自動下載并加載本地庫}public static void main(String[] args) {//讀取圖像并顯示Mat src = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/fish.png");HighGui.imshow("src", src);HighGui.waitKey(0);//讀取模板圖像并顯示Mat template = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/leaf.png");HighGui.imshow("template", template);HighGui.waitKey(0);//進行模板匹配:結果在result中Mat result = new Mat();Imgproc.matchTemplate(src, template, result, Imgproc.TM_CCOEFF);//取出最大值的位置(TM_CCOEFF模式用最大值)Core.MinMaxLocResult mmr = Core.minMaxLoc(result);Point pt = mmr.maxLoc;//用矩形畫出匹配位置并在屏幕上顯示Scalar red = new Scalar(0, 0, 255);Imgproc.rectangle(src, pt, new Point(pt.x + template.cols(), pt.y + template.rows()), red, 3);HighGui.imshow("match", src);HighGui.waitKey(0);System.exit(0);}
}
原圖:
模板:
匹配結構:
上述程序只能找到最佳匹配,而有時匹配的圖像不止一個,此時就要從matchTemplate()函數的結構中找出所有符合條件的圖像。下面用一個完整的程序說明如何將模板匹配運用于多目標匹配:
public class MatchTemplate2 {static {OpenCV.loadLocally(); // 自動下載并加載本地庫}public static void main(String[] args) {//讀取圖像并顯示Mat src = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo2/rose.png");HighGui.imshow("src", src);HighGui.waitKey(0);//讀取模板圖像并顯示Mat template = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo2/my.png");HighGui.imshow("template", template);HighGui.waitKey(0);//進行模板匹配,匹配值范圍為-1~1Mat result = new Mat();Imgproc.matchTemplate(src, template, result, Imgproc.TM_CCOEFF_NORMED);System.out.println(result);//參數準備float[] p = new float[3];Scalar red = new Scalar(0, 0, 255);int temprow = template.rows();int tempcol = template.cols();//搜索匹配值>0.8的像素for (int i = 0; i < result.rows(); i++) {for (int j = 0; j < result.cols(); j++) {//獲取像素的匹配值result.get(i, j, p);//匹配值>0.8的像素用矩形畫出if (p[0] > 0.8) {Imgproc.rectangle(src, new Point(j, i), new Point(j + tempcol, i + temprow), red, 3);}}}//顯示匹配結果HighGui.imshow("match", src);HighGui.waitKey(0);System.exit(0);}
}
原圖:
模板:
匹配結果: