900字范文,内容丰富有趣,生活中的好帮手!
900字范文 > VS+openCV 用直方图统计像素(上)计算图像直方图 利用查找表修改图像外观

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

时间:2020-08-11 11:49:07

相关推荐

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

一、计算图像直方图

图像由各种数值的像素构成。例如在单通道灰度图像中,每个像素都有一个 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); }

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

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。