在图像像素操作一节中,介绍了如何访问像素值、使用指针和迭代器遍历图像以及遍历图像和邻域操作。接下来,我们介绍如何用(C语言版和C++语言版的)OpenCV来计算一维直方图计算,然后,给合python开发工具和NumPy计算和绘制直方图。
在数字图像处理中,灰度直方图是一种最简单、最有用的工具之一,它概括了一幅图像的灰度级内容。一个图像是由不同颜色值的像值组成。像素值在图像中的分布情况是这幅图像的一个重要特征。OpenCV里面提供了不少有关直方图处理的函数。其中最基本的是计算直方图的函数calcHist( )。
基础知识
首先来看看OpenCV1.1中函数calcHist()如下:
/* Calculates array histogram */ CVAPI(void) cvCalcArrHist( CvArr** arr, CvHistogram* hist, int accumulate CV_DEFAULT(0), const CvArr* mask CV_DEFAULT(NULL) ); CV_INLINE void cvCalcHist( IplImage** image, CvHistogram* hist, int accumulate CV_DEFAULT(0), const CvArr* mask CV_DEFAULT(NULL) )
- 其中函数中参数:
- image 输入图像s (虽然也可以使用 CvMat** ).
- hist 直方图指针
- accumulate 累计标识。如果设置,则直方图在开始时不被清零。这个特征保证可以为多个图像计算一个单独的直方图,或者在线更新直方图。
- mask 操作 mask, 确定输入图像的哪个象素被计数
函数 cvCalcHist 计算一张或多张单通道图像的直方图(若要计算多通道,可像以下例子那样用多个单通道图来表示)。 用来增加直方块的数组元素可从相应输入图像的同样位置提取。
接下来看看OpenCV2.xx中imgproc.hpp头文件有关于计算直方图的3个重载函数calcHist()中最重要的一个,如下所示:
//! computes the joint dense histogram for a set of images. CV_EXPORTS void calcHist( const Mat* images, int nimages, const int* channels, InputArray mask, OutputArray hist, int dims, const int* histSize,const float** ranges, bool uniform=true, bool accumulate=false );
其中函数中参数:
- images 表示需要用来计算直方图的源图像序列,因此可以允许有多张大小一样,数据类型相同的图像被用来统计其直方图特征。
- nimages 表示的就是使用多少张图像序列中的图像用于计算直方图。
- channels的出现主要是考虑到输入的每一张图像有可能是多通道的,比如说RGB图就是3通道的,那么从统计意义上来讲,一张RGB图其实就是3张单通道的图像,而计算直方图时其本质也是针对单张图像进行的。这里虽然我们输入的图像序列images中有很多图片,但是并不是每一张图片的每一个通道都会被用来计算。所以参数3的功能是指定哪些通道的图像被用来计算(后面的解释都假设图像序列中图像是3通道的,那么有的图像可能有多个通道都被用来计算,有的图像可能连一个通道都没有被采用),这时参数3里面保存的是通道的序号,那么图像序列images中的第一张图片的通道序号(假设图像时3通道的)为0,1,2;images中第二张图片的图像序列接着上一次的,为3,4,5,;依次类推即可。
- mask是mask掩膜操作,即指定每张图片的哪些像素被用于计算直方图,这个掩膜矩阵不能够针对特定图像设定特定的掩膜,因此在这里是一视同仁对待的。
- hist是保存计算的直方图结果的矩阵,有可能是多维矩阵。
- dims是需要计算的直方图的维数。
- histSize是所需计算直方图的每一维的大小,即每一维bin的个数。
- ranges是所需计算直方图的每一维的范围,如果参数9的uniform为true,这此时的参数8的大小为2,里面的元素值表示的是每一维的上下限这两个数字;如果参数9的uniform为false,则此时的参数8的大小为bin的个数,即参数7的值,参数8里面的元素值需要人为的指定,即每一维的坐标值不一定是均匀的,需要人为指定。
- uniform 如果为true的话,则说明所需计算的直方图的每一维按照它的范围和尺寸大小均匀取值;如果为false的话,说明直方图的每一维不是均匀分布取值的,参考参数8的解释。
- accumulate, 如果为false,则表示直方图输出矩阵hist在使用该函数的时候被清0了,如果为true,则表示hist在使用calcHist()函数时没有被清0,计算的结果会累加到前一次保存的值中。
使用该函数的时候需要注意,如果在默认参数的情况下uniform = true,则此时的ranges大小必须是histSize大小的两倍,并且channels的大小必须等于dims维数。从上面可以理解,channels里的值已经指定了使用哪些单通道的图像来计算目标直方图,因此当channels的尺寸确定,则对应的直方图的维数也就确定了,所以我们不能使用多张图像来计算一个一维的直方图。
另一个重载函数的形式如下:
//! computes the joint sparse histogram for a set of images. CV_EXPORTS void calcHist( const Mat* images, int nimages, const int* channels, InputArray mask, SparseMat& hist, int dims, const int* histSize, const float** ranges, bool uniform=true, bool accumulate=false ); CV_EXPORTS_W void calcHist( InputArrayOfArrays images, const vector<int>& channels, InputArray mask, OutputArray hist, const vector<int>& histSize, const vector<float>& ranges, bool accumulate=false );
虽然从名字上看第一个参数是一个图像序列,但是我们并不能通过该函数来计算这些图像序列的一个一维的直方图。这个函数中并不像前面的函数那样需要指定一个参数表明有多少图像参与计算,因为在images中已经体现有了。另外这个函数也不需要像上面的函数一样指定直方图的维数,因为使用这个重载函数就表示默认为直方图的维数和channels的尺寸一样。最后本重载函数中的uniform在函数内部设定了为true,表面直方图中每一维都必须是均匀分布的。总之,上面2个函数是计算多个图像的直方图,直方图可以是多维的,该维数等于最终用于计算直方图的单通道的图像的个数。
在使用OpenCV内部的判断条件时应该使用CV_Assert( )函数,而不是CV_ASSERT()。通过实验测试发现,虽然经过calcHist()函数计算过后的直方图保存在hist中,这里hist是一个Mat类型,并且如果计算的是一维的直方图的话,则hist是一个列向量。
现在,我们来使用不同版的OPENCV直方图计算。
C版OPENCV直方图计算
#include "stdafx.h" #include <cv.h> #include <cxcore.h> #include <highgui.h> IplImage* DrawHistogram(CvHistogram* hist,float scaleX =2,float scaleY =2){ float histMax =0; cvGetMinMaxHistValue(hist,0,&histMax,0); IplImage*imghist =cvCreateImage(cvSize(256*scaleX,64*scaleY),8,1); cvZero(imghist); for( int i=0; i<255; i++){ float histValue =cvQueryHistValue_1D(hist,i); float nextValue =cvQueryHistValue_1D(hist,i+1); CvPoint pt1 = cvPoint( i*scaleX,64*scaleY); CvPoint pt2 = cvPoint((i+1)*scaleX,64*scaleY); CvPoint pt3 = cvPoint((i+1)*scaleX,64*scaleY-(nextValue/histMax)*64*scaleY); CvPoint pt4 = cvPoint(i*scaleX,64*scaleY-(histValue/histMax)*64*scaleY); int numPts =5; CvPoint pts[5]; pts[0] = pt1; pts[1] = pt2; pts[2] = pt3; pts[3] = pt4; pts[4] = pt1; cvFillConvexPoly(imghist,pts,numPts,cvScalar(255,250,0,0)); } return imghist; } int _tmain(int argc, _TCHAR* argv[]) { IplImage*src = cvLoadImage("iris.tif"); cvNamedWindow("Sr"); cvShowImage("Sr",src); int bins =1; int dims =1; int size = 256/bins; float range[] ={0,255}; float* ranges[] ={range}; CvHistogram * hist = cvCreateHist(dims,&size,CV_HIST_ARRAY,ranges,1); cvClearHist(hist); IplImage *imgRed = cvCreateImage(cvGetSize(src),8,1); IplImage *imgGreen = cvCreateImage(cvGetSize(src),8,1); IplImage *imgBlue = cvCreateImage(cvGetSize(src),8,1); cvSplit(src,imgBlue,imgGreen,imgRed,NULL); cvCalcHist(&imgBlue,hist,0,0); IplImage*histBlue = DrawHistogram(hist); cvClearHist(hist); cvNamedWindow("Blue"); cvShowImage("Blue",histBlue); cvCalcHist(&imgGreen,hist,0,0); IplImage* histGreen = DrawHistogram(hist); cvClearHist(hist); cvNamedWindow("Green"); cvShowImage("Green",histGreen); cvCalcHist(&imgRed,hist,0,0); IplImage* histRed = DrawHistogram(hist); cvClearHist(hist); cvNamedWindow("Red"); cvShowImage("Red",histRed); cvWaitKey(0); cvDestroyAllWindows(); return 0; }
输出结果如下图所示
C plus plus版OPENCV直方图计算
#include "stdafx.h" #include <opencv2/opencv.hpp> using namespace std; using namespace cv; int _tmain(int argc, _TCHAR* argv[]) { Mat src = imread("iris.tif"); //Load image Mat dst; if(!src.data) exit(0); // Separate the image in 3 places ( B, G and R ) vector<Mat> bgr_planes; split( src, bgr_planes ); int histSize = 256; // Establish the number of bins float range[] = {0, 256}; /// Set the ranges ( for B,G,R) ) const float* histRange = {range}; bool uniform = true; bool accumulate = false; Mat b_hist, g_hist, r_hist; // Compute the histograms: calcHist(&bgr_planes[0],1,0,Mat(),b_hist,1,&histSize,&histRange,uniform,accumulate); calcHist(&bgr_planes[1],1,0,Mat(),g_hist,1,&histSize,&histRange,uniform,accumulate); calcHist(&bgr_planes[2],1,0,Mat(),r_hist,1,&histSize,&histRange,uniform,accumulate); // Draw the histograms for B, G and R int hist_w = 600; int hist_h = 400; int bin_w = cvRound( (double) hist_w/histSize ); Mat histImage(hist_h,hist_w,CV_8UC3,Scalar(0,0,0)); // Normalize the result to [ 0, histImage.rows ] normalize(b_hist, b_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() ); normalize(g_hist, g_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() ); normalize(r_hist, r_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() ); // Draw for each channel for( int i = 1; i < histSize; i++ ){ line(histImage,Point(bin_w*(i-1),hist_h-cvRound(b_hist.at<float>(i-1))), Point(bin_w*(i), hist_h - cvRound(b_hist.at<float>(i))), Scalar(255,0,0),2,8,0); line(histImage,Point(bin_w*(i-1),hist_h-cvRound(g_hist.at<float>(i-1))), Point(bin_w*(i),hist_h-cvRound(g_hist.at<float>(i))), Scalar(0, 255, 0),2,8,0); line(histImage,Point(bin_w*(i-1),hist_h-cvRound(r_hist.at<float>(i-1))), Point(bin_w*(i),hist_h-cvRound(r_hist.at<float>(i))), Scalar(0,0,255),2,8,0); } namedWindow("calcHist", CV_WINDOW_AUTOSIZE ); imshow("calcHist", histImage ); waitKey(0); destroyAllWindows(); return 0; }
输出结果如下:
在这里,输出图像整体反应了iris.tif 的图像直方图,但局部细节还待于优化。
Python版OpenCV计算直方图
下面来看下彩色图像的直方图处理。首先读取并分离各通道和接着计算每个通道的直方图,这里将其封装成一个函数calcAndDrawHist:
import cv2 import numpy as np def calcAndDrawHist(image, color): hist= cv2.calcHist([image], [0], None, [256], [0.0,255.0]) minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(hist) histImg = np.zeros([256,256,3], np.uint8) hpt = int(0.9* 256); for h in range(256): intensity = int(hist[h]*hpt/maxVal) cv2.line(histImg,(h,256), (h,256-intensity), color) return histImg; if __name__ == '__main__': img = cv2.imread("..//iris.tif") b, g, r = cv2.split(img) histImgB = calcAndDrawHist(b, [255, 0, 0]) histImgG = calcAndDrawHist(g, [0, 255, 0]) histImgR = calcAndDrawHist(r, [0, 0, 255]) cv2.imshow("histBlue", histImgB) cv2.imshow("histGreen", histImgG) cv2.imshow("histRed", histImgR) cv2.imshow("Src", img) cv2.waitKey(0) cv2.destroyAllWindows()
这样就能得到三个通道的直方图了,如下:
更进一步
参考abid rahman的做法,无需分离通道,用折线来描绘直方图的边界可在一副图中同时绘制三个通道的直方图。方法如下:
import cv2 import numpy as np img = cv2.imread('..//iris.tif') h = np.zeros((256,256,3)) bins = np.arange(256).reshape(256,1) color = [ (255,0,0),(0,255,0),(0,0,255) ] for ch, col in enumerate(color): originHist = cv2.calcHist([img],[ch],None,[256],[0,256]) cv2.normalize(originHist, originHist,0,255*0.9,cv2.NORM_MINMAX) hist=np.int32(np.around(originHist)) pts = np.column_stack((bins,hist)) cv2.polylines(h,[pts],False,col) h=np.flipud(h) cv2.imshow('colorhist',h) cv2.waitKey(0)
结果如下图所示:
说明:
这里的for循环是对三个通道遍历一次,每次绘制相应通道的直方图的折线。for循环的第一行是计算对应通道的直方图,经过上面的介绍,应该很容易就能明白。
这里所不同的是没有手动的计算直方图的最大值再乘以一个系数,而是直接调用了OpenCV的归一化函数。该函数将直方图的范围限定在0-255×0.9之间,与之前的一样。
语句hist= np.int32(np.around(originHist))先将生成的原始直方图中的每个元素四舍六入五凑偶取整(cv2.calcHist函数得到的是float32类型的数组),接着将整数部分转成np.int32类型。即61.123先转成61.0,再转成61。注意,这里必须使用np.int32(...)进行转换,numpy的转换函数可以对数组中的每个元素都进行转换,而Python的int(...)只能转换一个元素,如果使用int(...),将导致only length-1 arrays can be converted to Python scalars错误。
语句pts = np.column_stack((bins,hist))是将直方图中每个bin的值转成相应的坐标。比如hist[0] =3,...,hist[126] = 178,...,hist[255] = 5;而bins的值为[[0],[1],[2]...,[255]]。使用np.column_stack将其组合成[0, 3]、[126, 178]、[255, 5]这样的坐标作为元素组成的数组。
最后使用cv2.polylines函数根据这些点绘制出折线,第三个False参数指出这个折线不需要闭合。第四个参数指定了折线的颜色。
当所有完成后,别忘了用h = np.flipud(h)反转绘制好的直方图,因为绘制时,[0,0]在图像的左上角。
NumPy版的直方图计算
在查阅abid rahman的资料时,发现他用NumPy的直方图计算函数np.histogram也实现了相同的效果。如下:
import cv2 import numpy as np img = cv2.imread('../iris.tif') h = np.zeros((300,256,3)) bins = np.arange(257) bin = bins[0:-1] color = [ (255,0,0),(0,255,0),(0,0,255) ] for ch,col in enumerate(color): item = img[:,:,ch] N,bins = np.histogram(item,bins) v=N.max() N = np.int32(np.around((N*255)/v)) N=N.reshape(256,1) pts = np.column_stack((bin,N)) cv2.polylines(h,[pts],False,col) h=np.flipud(h) cv2.imshow('img',h) cv2.waitKey(0)
输出结果如下:
效果图和上面的一个相同。
未完待续。。。如有错误,请多多指正。谢谢!
参考文献
[1] Robert Lagnaiere "OpenCV 2 Computer Vision Application Programming Cookbook".
[2] Daniel Lelis Baggio,Shervin Emami,"Mastering OpenCV with Practical Computer Vision Projects"
[3] Joseph Howse "OpenCV Computer Vision with Python".
[4]Gary Bradski, Adrian Kaehler "Learing OpenCV Computer Vision with the OpenCV Library(the First Edition)"
[5] Utkarsh, "Drawing Histograms in OpneCV"
=======================================================