一、计算图像直方图

图像由各种数值的像素构成。例如在单通道灰度图像中,每个像素都有一个 0(黑色)~255(白色)的整数。对于每个灰度,都有不同数量的像素分布在图像内,具体取决于图片内容。

直方图是一个简单的表格,表示一幅图像(有时是一组图像)中具有某个值的像素的数量。因此,灰度图像的直方图有 256 个项目,也叫箱子(bin)。0 号箱子提供值为 0 的像素的数量,1 号箱子提供值为 1 的像素的数量,以此类推。很明显,如果把直方图的所有箱子进行累加,得到的结果就是像素的总数。你也可以把直方图归一化,即所有箱子的累加和等于 1。这时,每个箱子的数值表示对应的像素数量占总数的百分比。

【实现】

#include<opencv2/core.hpp>
#include<opencv2/highgui.hpp>
#include<opencv2/imgproc.hpp>
#include<iostream>
using namespace std;
using namespace cv;//创建灰度图像的直方图
class Histogram1D {
private:int histSize[1];//直方图中箱子的数量float hranges[2];//值范围const float* ranges[1];//值范围的指针int channels[1];//要检查的通道数量
public:Histogram1D() {//准备一维直方图的默认参数histSize[0] = 256;//256个箱子hranges[0] = 0.0;//从0开始(含)hranges[1] = 256.0;//到256(不含)ranges[0] = hranges;channels[0] = 0;//先关注通道0}cv::Mat getHistogram(const cv::Mat& image);
};//计算一维直方图
cv::Mat Histogram1D::getHistogram(const cv::Mat& image) {cv::Mat hist;//用calcHist函数计算一维直方图cv::calcHist(&image, 1,   //仅为一幅图像的直方图channels,   //使用的通道cv::Mat(),   //不使用掩码hist,        //作为结果的直方图1,            //这是一维的直方图histSize, //箱子数量ranges        //像素值的范围);return hist;
}int main()
{//读取输入的图像cv::Mat image = cv::imread("girl.jpg", 0);//以黑白方式打开//直方图对象Histogram1D h;//计算直方图cv::Mat histo = h.getHistogram(image);//循环遍历每个箱子for (int i = 0; i < 256; i++)cout << "Value" << i << "="<< histo.at<float>(i) << endl;
}

显然,只看这一系列数值很难得到任何有意义的信息。因此比较实用的做法是以函数的方式 显示直方图,例如用柱状图。

#include<opencv2/core.hpp>
#include<opencv2/highgui.hpp>
#include<opencv2/imgproc.hpp>
#include<iostream>
using namespace std;
using namespace cv;//创建灰度图像的直方图
class Histogram1D {
private:int histSize[1];//直方图中箱子的数量float hranges[2];//值范围const float* ranges[1];//值范围的指针int channels[1];//要检查的通道数量
public:Histogram1D() {//准备一维直方图的默认参数histSize[0] = 256;//256个箱子hranges[0] = 0.0;//从0开始(含)hranges[1] = 256.0;//到256(不含)ranges[0] = hranges;channels[0] = 0;//先关注通道0}cv::Mat getHistogram(const cv::Mat& image);cv::Mat getHistogramImage(const cv::Mat& image, int zoom = 1);static cv::Mat getImageOfHistogram(const cv::Mat& hist, int zoom);
};//计算一维直方图
cv::Mat Histogram1D::getHistogram(const cv::Mat& image) {cv::Mat hist;//用calcHist函数计算一维直方图cv::calcHist(&image, 1,   //仅为一幅图像的直方图channels,   //使用的通道cv::Mat(),   //不使用掩码hist,        //作为结果的直方图1,            //这是一维的直方图histSize, //箱子数量ranges        //像素值的范围);return hist;
}//创建一个表示直方图的图像(静态方法)
cv::Mat Histogram1D::getImageOfHistogram(const cv::Mat& hist, int zoom) {//取得箱子值的最大值和最小值double maxVal = 0;double minVal = 0;cv::minMaxLoc(hist, &minVal, &maxVal, 0, 0);//取得直方图的大小int histSize = hist.rows;//用于显示直方图的方形图像cv::Mat histImg(histSize * zoom, histSize * zoom,CV_8U, cv::Scalar(255));//设置最高点为90%(即图像高度)的箱子个数int hpt = static_cast<int>(0.9 * histSize);//为每个箱子画垂直线for (int h = 0; h < histSize; h++) {float binVal = hist.at<float>(h);if (binVal > 0) {int intensity = static_cast<int>(binVal * hpt / maxVal);cv::line(histImg, cv::Point(h * zoom, histSize * zoom),cv::Point(h * zoom, (histSize - intensity) * zoom),cv::Scalar(0), zoom);}}return histImg;
}//计算一维直方图,并返回它的图像
cv::Mat Histogram1D::getHistogramImage(const cv::Mat& image, int zoom) {zoom = 1;//先计算直方图cv::Mat hist = getHistogram(image);//创建图像return getImageOfHistogram(hist, zoom);
}int main()
{//读取输入的图像cv::Mat image = cv::imread("girl.jpg", 0);//以黑白方式打开//直方图对象Histogram1D h;cv::namedWindow("Histogram");cv::imshow("Histogram", h.getHistogramImage(image));cv::waitKey(0);
}

从图形化的直方图可以看出,在中等灰度值处有一个大的尖峰,并且比中等值更黑的像素有很多。巧的是,这两部分像素分别对应了图像的背景和前景。要验证这点,可以在这两部分的汇合处进行阈值化处理。OpenCV 中的 cv::threshold 函数可以实现这个功能。上一章介绍过,它是一个很实用的函数。我们取直方图中在升高为尖峰之前的最小值的位置(灰度值为 70), 对其进行阈值化处理,得到二值图像。

int main()
{//读取输入的图像cv::Mat image = cv::imread("girl.jpg", 0);//以黑白方式打开cv::Mat thresholded;                    //输出二值图像cv::threshold(image, thresholded, 70,   //阈值255,                        //对超过阈值的像素赋值cv::THRESH_BINARY);     //阈值化类型cv::namedWindow("Binary Image");cv::imshow("Binary Image", thresholded);cv::waitKey(0);
}

【实现原理】

为了适应各种场景,cv::calcHist 函数带有很多参数。

void calcHist(const Mat*images, // 源图像 int nimages, // 源图像的个数(通常为 1)const int*channels, // 列出通道InputArray mask, // 输入掩码(需处理的像素)OutputArray hist, // 输出直方图int dims, // 直方图的维度(通道数量)const int*histSize, // 每个维度位数const float**ranges, // 每个维度的范围bool uniform=true, // true 表示箱子间距相同bool accumulate=false) // 是否在多次调用时进行累加

大多数情况下,直方图是单个的单通道或三通道图像,但也可以在这个函数中指定一个分布在多幅图像(即多个 cv::Mat)上的多通道图像。这也是把输入图像数组作为函数第一个参数的原因。第六个参数 dims 指明了直方图的维数,例如 1 表示一维直方图。在分析多通道图像时,可以只把它的部分通道用于计算直方图,将需要处理的通道放在维数确定的数组 channel 中。在这个类的实现中只有一个通道,默认为 0。直方图用每个维度上的箱子数量(即整数数组histSize)以及每个维度(由 ranges 数组提供,数组中每个元素又是一个二元素数组)上的最小值(含)和最大值(不含)来描述。你也可以定义一个不均匀的直方图(倒数第二个参数应 设为false),这时需要指定每个箱子的限值。

和很多 OpenCV 函数一样,可以使用掩码表示计算时用到的像素(所有掩码值为 0 的像素都不使用)。此外还可以指定两个布尔值类型的附加参数,第一个表示是否采用均匀的直方图(默认为 true),第二个表示是否允许累加多个直方图计算的结果。如果第二个参数为 true,那么图像中的像素数量会累加到输入直方图的当前值中。在计算一组图像的直方图时,就可以使用这个参数。

得到的直方图存储在 cv::Mat 的实例中。事实上,cv::Mat 类可用于操作通用的 N 维矩阵。第 2 章讲过,cv::Mat 类定义了适用于一维、二维和三维矩阵的 at 方法。正因如此,我们才可以在 getHistogramImage 方法中用下面的代码访问一维直方图的每个箱子:

float binVal = hist.at(h);

注意,直方图中的值存储为 float 值。

【扩展阅读】

我们可以用同一个 cv::calcHist 函数计算多通道图像的直方图。例如,若想计算彩色 BGR 图像的直方图,可以这样定义这样一个类:

class ColorHistogram { private: int histSize[3]; // 每个维度的大小float hranges[2]; // 值的范围(三个维度用同一个值)const float* ranges[3]; // 每个维度的范围int channels[3]; // 需要处理的通道public: ColorHistogram() { // 准备用于彩色图像的默认参数// 每个维度的大小和范围是相等的histSize[0]= histSize[1]= histSize[2]= 256; hranges[0]= 0.0; // BGR 范围为 0~256 hranges[1]= 256.0; ranges[0]= hranges; // 这个类中ranges[1]= hranges; // 所有通道的范围都相等ranges[2]= hranges; channels[0]= 0; // 三个通道:B channels[1]= 1; // G channels[2]= 2; // R } 

这里的直方图将会是三维的,因此需要为每个维度指定一个范围。本例中的 BGR 图像的三个通道范围都是[0,255]。准备好参数后,就可以用下面的方法计算颜色直方图了:

// 计算直方图
cv::Mat getHistogram(const cv::Mat &image) { cv::Mat hist; // 计算直方图cv::calcHist(&image, 1, // 单幅图像的直方图channels, // 用到的通道cv::Mat(), // 不使用掩码hist, // 得到的直方图3, // 这是一个三维直方图histSize, // 箱子数量ranges // 像素值的范围); return hist;
} 

上述方法返回一个三维的 cv::Mat 实例。如果选用含有 256 个箱子的直方图,这个矩阵就有(256)^3 个元素,表示超过 1600 万个项目。在很多应用程序中,最好在计算直方图时减少箱子的数量。也可以使用数据结构 cv::SparseMat 表示大型稀疏矩阵(即非零元素非常稀少的矩阵),这样不会消耗过多的内存。cv::calcHist 函数具有返回这种矩阵的版本,因此只需要简单地修改一下前面的方法,即可使用 cv::SparseMatrix:

// 计算直方图
cv::SparseMat getSparseHistogram(const cv::Mat &image) { cv::SparseMat hist(3, // 维数histSize, // 每个维度的大小CV_32F); // 计算直方图cv::calcHist(&image, 1, // 单幅图像的直方图channels, // 用到的通道cv::Mat(), // 不使用掩码hist, // 得到的直方图3, // 这是三维直方图histSize, // 箱子数量ranges // 像素值的范围); return hist;
}

这是一个三维直方图,画起来比较困难。我们也可以通过显示独立的 R、G 和 B 通道的直方图来说明图像中颜色的分布情况。

【遇到的问题】

扩展阅读里的代码不完整,其他的地方我不会修改,所以运行不起来。

二、利用查找表修改图像外观

图像直方图提供了利用现有像素强度值进行场景渲染的方法。通过分析图像中像素值的分布 情况,你可以利用这个信息来修改图像,甚至提高图像质量。本节将解释如何用一个简单的映射函数(称为查找表)来修改图像的像素值。我们即将看到,查找表通常根据直方分布图生成。

【实现】

查找表是个一对一(或多对一)的函数,定义了如何把像素值转换成新的值。它是一个一维数组,对于规则的灰度图像,它包含 256 个项目。利用查找表的项目 i,可得到对应灰度级的新强度值。

 newIntensity = lookup[oldIntensity];

OpenCV 中的 cv::LUT 函数在图像上应用查找表生成一个新的图像。查找表通常根据直方图生成,以下是完整代码。

#include<opencv2/core.hpp>
#include<opencv2/highgui.hpp>
#include<opencv2/imgproc.hpp>
#include<iostream>
using namespace std;
using namespace cv;class Histogram1D {
public:static cv::Mat applyLookUp(const cv::Mat& image, const cv::Mat& lookup);
};cv::Mat Histogram1D::applyLookUp(const cv::Mat& image,     //输入图像const cv::Mat& lookup) {//uchar类型的1x256数组//输出图像cv::Mat result;//应用查找表cv::LUT(image, lookup, result);return result;
}int main()
{//读取输入的图像cv::Mat image = cv::imread("girl.jpg");//创建一个图像翻转的查找表cv::Mat lut(1, 256, CV_8U);//256x1矩阵for (int i = 0; i < 256; i++) {//0变成255,1变成254,以此类推lut.at<uchar>(i) = 255 - i;}                              //直方图对象Histogram1D h;cv::namedWindow("Negative image");cv::imshow("Negative image", h.applyLookUp(image, lut));cv::waitKey(0);
}

注意:这里我把上一部分的直方图代码删掉了,只存留了这次的查找表代码。其实也不一定非要定义一个类,直接用函数也可以实现。

【实现原理】

在图像上应用查找表后得到一个新图像,新图像的像素强度值被修改为查找表中规定的值。例如上述代码对像素强度进行了简单的反转,即强度 0 变成 255、1 变成 254、最后 255 变成0。对图像应用这种查找表后,会生成原始图像的反向图像。

【扩展阅读】

对于需要更换全部像素强度值的程序,都可以使用查找表。但是这个转换过程必须是针对整幅图像的。也就是说,一个强度值对应的全部像素都必须使用同一种转换方法。

1. 伸展直方图以提高图像对比度

定义一个修改原始图像直方图的查找表可以提高图像的对比度。例如,如果图中根本没有大于 200 的像素值。我们可以通过伸展直方图来生成一个对比度更高的图像。为此要使用一个百分比阈值,表示伸展后图像的最小强度值(0)和最大强度值(255) 像素的百分比。

我们必须在强度值中找到最小值(imin)和最大值(imax),使得所要求的最小的像素数量高于阈值指定的百分比。以下是完整代码:

#include<opencv2/core.hpp>
#include<opencv2/highgui.hpp>
#include<opencv2/imgproc.hpp>
#include<iostream>
using namespace std;
using namespace cv;//创建灰度图像的直方图
class Histogram1D {
private:int histSize[1];//直方图中箱子的数量float hranges[2];//值范围const float* ranges[1];//值范围的指针int channels[1];//要检查的通道数量
public:Histogram1D() {//准备一维直方图的默认参数histSize[0] = 256;//256个箱子hranges[0] = 0.0;//从0开始(含)hranges[1] = 256.0;//到256(不含)ranges[0] = hranges;channels[0] = 0;//先关注通道0}cv::Mat getHistogram(const cv::Mat& image);cv::Mat getHistogramImage(const cv::Mat& image, int zoom = 1);static cv::Mat getImageOfHistogram(const cv::Mat& hist, int zoom);cv::Mat stretch(const Mat& image, int minValue = 0);static cv::Mat applyLookUp(const cv::Mat& image, const cv::Mat& lookup);
};//计算一维直方图
cv::Mat Histogram1D::getHistogram(const cv::Mat& image) {cv::Mat hist;//用calcHist函数计算一维直方图cv::calcHist(&image, 1,   //仅为一幅图像的直方图channels,   //使用的通道cv::Mat(),   //不使用掩码hist,        //作为结果的直方图1,            //这是一维的直方图histSize, //箱子数量ranges        //像素值的范围);return hist;
}//创建一个表示直方图的图像(静态方法)
cv::Mat Histogram1D::getImageOfHistogram(const cv::Mat& hist, int zoom) {//取得箱子值的最大值和最小值double maxVal = 0;double minVal = 0;cv::minMaxLoc(hist, &minVal, &maxVal, 0, 0);//取得直方图的大小int histSize = hist.rows;//用于显示直方图的方形图像cv::Mat histImg(histSize * zoom, histSize * zoom,CV_8U, cv::Scalar(255));//设置最高点为90%(即图像高度)的箱子个数int hpt = static_cast<int>(0.9 * histSize);//为每个箱子画垂直线for (int h = 0; h < histSize; h++) {float binVal = hist.at<float>(h);if (binVal > 0) {int intensity = static_cast<int>(binVal * hpt / maxVal);cv::line(histImg, cv::Point(h * zoom, histSize * zoom),cv::Point(h * zoom, (histSize - intensity) * zoom),cv::Scalar(0), zoom);}}return histImg;
}//计算一维直方图,并返回它的图像
cv::Mat Histogram1D::getHistogramImage(const cv::Mat& image, int zoom) {zoom = 1;//先计算直方图cv::Mat hist = getHistogram(image);//创建图像return getImageOfHistogram(hist, zoom);
}cv::Mat Histogram1D::applyLookUp(const cv::Mat& image,  //输入图像const cv::Mat& lookup) {//uchar类型的1x256数组
//输出图像cv::Mat result;//应用查找表cv::LUT(image, lookup, result);return result;
}//伸展直方图
cv::Mat Histogram1D::stretch(const Mat& image, int minValue) {cv::Mat hist = getHistogram(image);//找到直方图的左极限int imin = 0;for (; imin < 256; imin++) {//小于或等于imin的像素数量必须>minValueif ((hist.at<float>(imin)) > minValue)break;}//找到直方图的右极限int imax = 255;for (; imax >= 0; imax--) {//大于或等于imax的像素必须>minValueif ((hist.at<float>(imax)) > minValue)break;}//minValue代表的是次数、个数,像素值最小(0左右的)以及像素值最大(255左右的)//这些极端的值都比较少,找到比较少的个数对应的像素值坐标(横坐标)Mat lookup(1, 256, CV_8U);  //LUT查找表的像素重映射的规则for (int i = 0; i < 256; i++)      //根据像素值大小划分{if (i < imin)                 //像素值(横坐标)imin左边的都置为0;极小的置0lookup.at<uchar>(i) = 0;else if (i > imax)          //像素值(横坐标)右边的都置255;极大的置255lookup.at<uchar>(i) = 255;elselookup.at<uchar>(i) = cvRound(255.0 * (i - imin) / (imax - imin)); //[min,max]重新分配 cvRound为取整,中间的重新映射}Mat result;result = applyLookUp(image, lookup);return result;     //返回处理好的增强的对比度 图片//这里需要分析下形参传进来的minValue。如果minValue过大,两边的0,255就会多;如果minValue过小,两边的0,255就会少
}   int main()
{//读取输入的图像cv::Mat image = cv::imread("bluesky.jpg");//直方图对象Histogram1D h;cv::Mat streteched = h.stretch(image, 200);cv::namedWindow("Streched Image");cv::imshow("Streched Image", streteched);cv::namedWindow("Original Image");cv::imshow("Original Image", image);cv::waitKey(0);
}

2. 在彩色图像上应用查找表

之前我们定义了一个减色函数,通过修改图像中的 BGR 值减少可能的颜色数量。当时的实现方法是循环遍历图像中的像素,并对每个像素应用减色函数。实际上,更高效的做法是预先计算好所有的减色值,然后用查找表修改每个像素。利用本节的方法,这很容易实现。下面是新的减色函数。

void colorReduce(cv::Mat &image, int div=64) { // 创建一维查找表cv::Mat lookup(1,256,CV_8U); // 定义减色查找表的值for (int i=0; i<256; i++)lookup.at<uchar>(i)= i/div*div + div/2; // 对每个通道应用查找表cv::LUT(image,lookup,image);
}

这种减色方案之所以能起作用,是因为在多通道图像上应用一维查找表时,同一个查找表会独立地应用在所有通道上。如果查找表超过一个维度,那么它和所用图像的通道数必须相同。

VS+openCV 用直方图统计像素(上)计算图像直方图、利用查找表修改图像外观相关推荐

  1. OpenCV 【十二】OpenCV如何扫描图像、利用查找表和计时

    目录 OpenCV如何扫描图像.利用查找表和计时 1.函数计算时间测试case 2. Mat图像的存储机理 3. 像素遍历的3--4种方式 4. 实例 OpenCV如何扫描图像.利用查找表和计时 如何 ...

  2. OpenCV之core 模块. 核心功能(1)Mat - 基本图像容器 OpenCV如何扫描图像、利用查找表和计时 矩阵的掩码操作 使用OpenCV对两幅图像求和(求混合(blending))

    Mat - 基本图像容器 目的 从真实世界中获取数字图像有很多方法,比如数码相机.扫描仪.CT或者磁共振成像.无论哪种方法,我们(人类)看到的是图像,而让数字设备来"看"的时候,则 ...

  3. git统计历史上某一段时间代码的修改量

    git统计历史上某一段时间代码的修改量 有两个方法,一个是git log的since - until,另外一个是git log after before,例如: git log --after=&qu ...

  4. OpenCV遍历图像性能比较、利用查找表

    原文:http://blog.csdn.net/chenjiazhou12/article/details/21052849 对于如何扫描图像的方法实在是太多了,在浏览一些资料的时候也找到了一些好的方 ...

  5. sin查找表 matlab,利用Xilinx中的ROM构造查找表来计算sin和cos的方法探讨

    1.使用matlab制作.coe文件 查找表的构造 构造256点的正余弦表 exp(-j*2*pi*(0:255)/256),分别得到 cos和sin的查找表 matlab代码: 求sin fid = ...

  6. opencv 直方图_OpenCV之图像直方图反向投影

    python代码: import cv2 as cv import numpy as np from matplotlib import pyplot as pltdef back_projectio ...

  7. OpenCV学习笔记(十六):直方图均衡化:equalizeHist()

    OpenCV学习笔记(十六):直方图均匀化:equalizeHist() 参考博客: 直方图均衡化的数学原理 直方图匹配的数学原理 直方图均衡化广泛应用于图像增强中: 直方图均衡化处理的"中 ...

  8. python利用opencv自带的颜色查找表(LUT)进行色彩风格变换

    1 LUT颜色查找表介绍 Look Up Table(LUT)查找表 1.颜色查找表就是一种像素值映射的表,如下是一个对比度改变图像的查找表,从图中可以知道: 原图中像素值为40的像素,经过查找表映射 ...

  9. skimage中的图像直方图均衡化

     skimage用于图像的直方图均衡化的函数有两个,分别是equalize_hist和equalize_adapthist函数,本文详细介绍了这两个函数. equalize_hist函数 语法: ​ ...

最新文章

  1. 基于Linux的集群系统(一)
  2. 如何搭建自己的 pip 本地 cache
  3. 深度探讨验证码发展史,账户中心安全科普文
  4. 【Java】 获取当前项目所有的线程
  5. 代码也浪漫:用Python放一场烟花秀!
  6. Sublime Text3 总结笔记
  7. 除了PS,原来这个也可以轻松实现图像处理!
  8. 使用Spring Boot和Spring Security验证JWT
  9. Android 简介:Android SDK 和开发框架简介
  10. R 包的手动打包流程
  11. 《计算机科学概论(第12版)》—第1章1.3节海量存储器
  12. 搭建云免流服务器教程,搭建云免流服务器教程
  13. Low-Code is Low—— 低代码的使用
  14. java关于int极限值的测试
  15. 存在的hive插入数据_往hive表中插入数据以及导出数据
  16. 这篇文章来自我的微信朋友圈,并不特别好玩,但可以给创业者补点财务知识
  17. Infor CloudSuite Industrial (SyteLine) 工序外协基本流程
  18. matlab结束外循环,求单源最短路径的BellmanFord算法的matlab实现及其优化
  19. linux获取时间戳精确到毫秒,微妙
  20. 基于HTML5的捕鱼达人游戏网页版

热门文章

  1. 从零开始学习Linux运维,成为IT领域翘楚(三)
  2. C语言求最大公约数及最小公倍数
  3. 好心情平台送给抑郁症患者20个正能量句子
  4. 如何让百度快照更新不断
  5. ISO认证需要多长时间,ISO9001认证流程
  6. 增益与放大倍数到底时说明关系
  7. 自动驾驶领域用到的一些数据集
  8. 权限维持——获取登陆账号及安装后门程序
  9. AEC行业那些开源的软件在这里
  10. linux8000端口一般是什么端口,Linux下有什么地方用到了8000端口