什么是直方图?
-
直方图是收集的数据计数,组织成一组预定义的bin
-
当我们说数据时,我们并没有将其限制为强度值(正如我们在前面的教程直方图均衡中看到的)。收集的数据可以是您认为对描述图像有用的任何特征。
-
让我们看一个例子。想象一个矩阵包含图像的信息(即范围内的强度0 - 255):
-
如果我们想以有组织的方式统计这些数据会发生什么?由于我们知道这种情况下的信息值范围是 256 个值,我们可以将我们的范围划分为子部分(称为bins),例如:
[0,255]=[0,15]∪[16,31]∪…∪[240,255]range=bin1∪bin2∪…∪binn=15
我们可以计算落在每个范围内的像素数我_n一世. 将其应用于上面的示例,我们得到下面的图像(轴 x 表示 bin,轴 y 表示每个 bin 中的像素数)。
-
这只是直方图如何工作以及为什么有用的一个简单示例。直方图不仅可以记录颜色强度,还可以记录我们想要测量的任何图像特征(即梯度、方向等)。
-
让我们识别直方图的某些部分:
- dims:您要收集数据的参数数量。在我们的示例中,dims = 1因为我们只计算每个像素的强度值(在灰度图像中)。
- bins:它是每个dim中的**细分数。**在我们的示例中,bin = 16
- range:要测量的值的限制。在这种情况下:范围 = [0,255]
-
如果你想计算两个特征怎么办?在这种情况下,您生成的直方图将是一个 3D 图(其中 x 和 y 将是binx 和 biny是的对于每个特征,z 将是每个组合的计数((binx,biny)). 这同样适用于更多功能(当然它会变得更棘手)。
OpenCV 为您提供什么
出于简单的目的,OpenCV 实现了函数cv::calcHist,它计算一组数组(通常是图像或图像平面)的直方图。它可以操作多达 32 个维度。我们将在下面的代码中看到它!
代码 C++爪哇Python
-
这个程序有什么作用?
- 加载图像
- 使用函数cv::split将图像拆分为 R、G 和 B 平面
- 通过调用函数cv::calcHist计算每个 1 通道平面的直方图
- 在窗口中绘制三个直方图
-
可下载代码:点击这里
-
代码一览:
-
import java.util.ArrayList; import java.util.List; import org.opencv.core.Core; import org.opencv.core.CvType; import org.opencv.core.Mat; import org.opencv.core.MatOfFloat; import org.opencv.core.MatOfInt; import org.opencv.core.Point; import org.opencv.core.Scalar; import org.opencv.highgui.HighGui; import org.opencv.imgcodecs.Imgcodecs; import org.opencv.imgproc.Imgproc; class CalcHist { public void run(String[] args) { String filename = args.length > 0 ? args[0] : "../data/lena.jpg"; Mat src = Imgcodecs.imread(filename); if (src.empty()) { System.err.println("Cannot read image: " + filename); System.exit(0); } List<Mat> bgrPlanes = new ArrayList<>(); Core.split(src, bgrPlanes); int histSize = 256; float[] range = {0, 256}; //the upper boundary is exclusive MatOfFloat histRange = new MatOfFloat(range); boolean accumulate = false; Mat bHist = new Mat(), gHist = new Mat(), rHist = new Mat(); Imgproc.calcHist(bgrPlanes, new MatOfInt(0), new Mat(), bHist, new MatOfInt(histSize), histRange, accumulate); Imgproc.calcHist(bgrPlanes, new MatOfInt(1), new Mat(), gHist, new MatOfInt(histSize), histRange, accumulate); Imgproc.calcHist(bgrPlanes, new MatOfInt(2), new Mat(), rHist, new MatOfInt(histSize), histRange, accumulate); int histW = 512, histH = 400; int binW = (int) Math.round((double) histW / histSize); Mat histImage = new Mat( histH, histW, CvType.CV_8UC3, new Scalar( 0,0,0) ); Core.normalize(bHist, bHist, 0, histImage.rows(), Core.NORM_MINMAX); Core.normalize(gHist, gHist, 0, histImage.rows(), Core.NORM_MINMAX); Core.normalize(rHist, rHist, 0, histImage.rows(), Core.NORM_MINMAX); float[] bHistData = new float[(int) (bHist.total() * bHist.channels())]; bHist.get(0, 0, bHistData); float[] gHistData = new float[(int) (gHist.total() * gHist.channels())]; gHist.get(0, 0, gHistData); float[] rHistData = new float[(int) (rHist.total() * rHist.channels())]; rHist.get(0, 0, rHistData); for( int i = 1; i < histSize; i++ ) { Imgproc.line(histImage, new Point(binW * (i - 1), histH - Math.round(bHistData[i - 1])), new Point(binW * (i), histH - Math.round(bHistData[i])), new Scalar(255, 0, 0), 2); Imgproc.line(histImage, new Point(binW * (i - 1), histH - Math.round(gHistData[i - 1])), new Point(binW * (i), histH - Math.round(gHistData[i])), new Scalar(0, 255, 0), 2); Imgproc.line(histImage, new Point(binW * (i - 1), histH - Math.round(rHistData[i - 1])), new Point(binW * (i), histH - Math.round(rHistData[i])), new Scalar(0, 0, 255), 2); } HighGui.imshow( "Source image", src ); HighGui.imshow( "calcHist Demo", histImage ); HighGui.waitKey(0); System.exit(0); } } public class CalcHistDemo { public static void main(String[] args) { // Load the native OpenCV library System.loadLibrary(Core.NATIVE_LIBRARY_NAME); new CalcHist().run(args); } }
解释Java
- 加载源图像
String filename = args.length > 0 ? args[0] : "../data/lena.jpg";
Mat src = Imgcodecs.imread(filename);
if (src.empty()) {
System.err.println("Cannot read image: " + filename);
System.exit(0);
}
- 在其三个 R、G 和 B 平面中分离源图像。为此,我们使用 OpenCV 函数cv::split:
List<Mat> bgrPlanes = new ArrayList<>();
Core.split(src, bgrPlanes);
我们的输入是要分割的图像(这种情况下是三个通道),输出是 Mat 的向量)
- 现在我们准备开始为每个平面配置直方图。由于我们正在使用 B、G 和 R 平面,我们知道我们的值将在区间内变化[ 0 , 255 ]
- 确定 bin 的数量(5、10…):
int histSize = 256;
- 设置值的范围(如我们所说,介于 0 和 255 之间)
float[] range = {0, 256}; //上边界是独占的
MatOfFloat histRange = new MatOfFloat(range);
- 我们希望我们的 bin 具有相同的大小(统一)并在开始时清除直方图,因此:
boolean accumulate = false;
- 我们继续使用 OpenCV 函数cv::calcHist计算直方图:
Mat bHist = new Mat(), gHist = new Mat(), rHist = new Mat();
Imgproc.calcHist(bgrPlanes, new MatOfInt(0), new Mat(), bHist, new MatOfInt(histSize), histRange, accumulate);
Imgproc.calcHist(bgrPlanes, new MatOfInt(1), new Mat(), gHist, new MatOfInt(histSize), histRange, accumulate);
Imgproc.calcHist(bgrPlanes, new MatOfInt(2), new Mat(), rHist, new MatOfInt(histSize), histRange, accumulate);
-
其中参数是(
C++ 代码
):
- **&bgr_planes[0]😗*源数组
- 1:源数组的数量(在这种情况下,我们使用 1。我们也可以在此处输入数组列表)
- 0 :要测量的通道(*暗淡)。*在这种情况下,它只是强度(每个阵列都是单通道),所以我们只写 0。
- Mat():要在源数组上使用的掩码(零表示要忽略的像素)。如果未定义,则不使用
- b_hist:将存储直方图的 Mat 对象
- 1:直方图维数。
- **histSize:**每个使用的维度的 bin 数量
- **histRange:**每个维度要测量的值的范围
- uniform和accumulate:bin 大小相同,直方图在开始时被清除。
-
创建图像以显示直方图:
int histW = 512, histH = 400;
int binW = (int) Math.round((double) histW / histSize);
Mat histImage = new Mat( histH, histW, CvType.CV_8UC3, new Scalar( 0,0,0) );
请注意,在绘制之前,我们首先对直方图进行cv::normalize,使其值落在输入参数指示的范围内:
Core.normalize(bHist, bHist, 0, histImage.rows(), Core.NORM_MINMAX);
Core.normalize(gHist, gHist, 0, histImage.rows(), Core.NORM_MINMAX);
Core.normalize(rHist, rHist, 0, histImage.rows(), Core.NORM_MINMAX);
-
此函数接收这些参数(
C++ 代码
):
- **b_hist:**输入数组
- **b_hist:**输出归一化数组(可以相同)
- 0和histImage.rows:对于这个例子,它们是标准化r_hist值的下限和上限
- **NORM_MINMAX:**表示归一化类型的参数(如上所述,它调整之前设置的两个限制之间的值)
- **-1:**表示输出的归一化数组将与输入的类型相同
- **Mat():**可选掩码
-
观察以访问 bin(在这种情况下,在此 1D 直方图中):
float[] bHistData = new float[(int) (bHist.total() * bHist.channels())];
bHist.get(0, 0, bHistData);
float[] gHistData = new float[(int) (gHist.total() * gHist.channels())];
gHist.get(0, 0, gHistData);
float[] rHistData = new float[(int) (rHist.total() * rHist.channels())];
rHist.get(0, 0, rHistData);
for( int i = 1; i < histSize; i++ ) {
Imgproc.line(histImage, new Point(binW * (i - 1), histH - Math.round(bHistData[i - 1])),
new Point(binW * (i), histH - Math.round(bHistData[i])), new Scalar(255, 0, 0), 2);
Imgproc.line(histImage, new Point(binW * (i - 1), histH - Math.round(gHistData[i - 1])),
new Point(binW * (i), histH - Math.round(gHistData[i])), new Scalar(0, 255, 0), 2);
Imgproc.line(histImage, new Point(binW * (i - 1), histH - Math.round(rHistData[i - 1])),
new Point(binW * (i), histH - Math.round(rHistData[i])), new Scalar(0, 0, 255), 2);
}
我们使用表达式(C++ 代码):
b_hist.at<float>(i)
在哪里一世表示维度。如果它是一个二维直方图,我们会使用类似的东西:
b_hist.at<float>( i, j )
- 最后我们显示我们的直方图并等待用户退出:
HighGui.imshow( "Source image", src );
HighGui.imshow( "calcHist Demo", histImage );
HighGui.waitKey(0);
Result
结果
- 使用如下图所示的图像作为输入参数:
-
生成以下直方图:
官网地址:
https://docs.opencv.org/3.4/d8/dbc/tutorial_histogram_calculation.html