Learning OpenCV Lecture 3 (Counting the Pixels with Histograms)
In this chapter, we will cover:
- Computing the image histogram
- Applying look-up tables to modify image appearance
- Equalizing the image histogram
- Backprojecting a histogram to detect specific image content
- Using the mean shift algorithm to find an object
- Retrieving similar images using histogram comparison
Computing the image histogram
Using the cv::calcHist function.
Caculate 1D Histogram
Histogram1D.h
#include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> class Histogram1D { private: int histSize[1]; // number of bins float hranges[2]; // min and max pixel value const float* ranges[1]; int channels[1]; // only 1 channel used here public: Histogram1D() { // Prepare arguments for 1D histogram histSize[0] = 256; hranges[0] = 0.0; hranges[1] = 255.0; ranges[0] = hranges; channels[0] = 0; // by default, we look at channel 0 } // Computes the 1D histogram cv::MatND getHistogram(const cv::Mat &image); // Computes the 1D histogram and returns an image of it. cv::MatND getHistogramImage(const cv::Mat &image); };
Histogram.cpp
#include "Histgram1D.h" // Computes the 1D histogram cv::MatND Histogram1D::getHistogram(const cv::Mat &image) { cv::MatND hist; // Compute histogram cv::calcHist(&image, 1, // histogram from 1 image only channels, // the channel used cv::Mat(), // no mask is used hist, // the resulting histogram 1, // it is a 1D histgram histSize, // number of bins ranges // pixel value range ); return hist; } cv::Mat Histogram1D::getHistogramImage(const cv::Mat &image) { // Compute histogram first cv::MatND hist = getHistogram(image); // Get min and max bin values double maxVal = 0; double minVal = 0; cv::minMaxLoc(hist, &minVal, &maxVal, 0, 0); // Image on which to display histogram cv::Mat histImg(histSize[0], histSize[0], CV_8U, cv::Scalar(255)); // Set highest point at 90% of nbins int hpt = static_cast <int >(0.9 * histSize[0]); // Draw a vertical line for each bin for ( int h = 0; h < histSize[0]; h++ ) { float binVal = hist.at<float>(h); int intensity = static_cast <int >(binVal * hpt / maxVal); // This function draws a line between 2 points cv::line(histImg, cv::Point(h, histSize[0]), cv::Point(h, histSize[0] - intensity), cv::Scalar::all(0)); } return histImg; }
main.cpp
#include <iostream> #include "Histgram1D.h" int main() { // Read input image cv::Mat image = cv::imread( "group.jpg", 0); // open in b&w // The histogram object Histogram1D h; // Compute the histogram cv::MatND histo = h.getHistogram(image); // Loop over each bin for (int i = 0; i < 256; i++) { std::cout << "Value " << i << " = " << histo.at<float >(i) << std::endl; } // Draw histogram image cv::Mat histoImage = h.getHistogramImage(image); cv::namedWindow( "histogram", CV_WINDOW_AUTOSIZE); cv::imshow( "histogram", histoImage); // threshold the image cv::Mat thresholded; cv::threshold(image, thresholded, 60, 255, cv::THRESH_BINARY); cv::namedWindow( "Binary image", CV_WINDOW_AUTOSIZE); cv::imshow( "Binary image", thresholded); cv::waitKey(0); return 0; }
the result as follows:
Caculate Color image histogram
ColoHistogram.h
#include <iostream> #include "Histgram1D.h" int main() { // Read input image cv::Mat image = cv::imread( "group.jpg", 0); // open in b&w // The histogram object Histogram1D h; // Compute the histogram cv::MatND histo = h.getHistogram(image); // Loop over each bin for (int i = 0; i < 256; i++) { std::cout << "Value " << i << " = " << histo.at<float >(i) << std::endl; } // Draw histogram image cv::Mat histoImage = h.getHistogramImage(image); cv::namedWindow( "histogram", CV_WINDOW_AUTOSIZE); cv::imshow( "histogram", histoImage); // threshold the image cv::Mat thresholded; cv::threshold(image, thresholded, 60, 255, cv::THRESH_BINARY); cv::namedWindow( "Binary image", CV_WINDOW_AUTOSIZE); cv::imshow( "Binary image", thresholded); cv::waitKey(0); return 0; }
ColorHistogram.cpp
#include "ColorHistogram.h" // Computes the 1D histogram cv::MatND ColorHistogram::getHistogram(const cv::Mat &image) { cv::MatND hist; // Compute histogram cv::calcHist(&image, 1, // histogram from 1 image only channels, // the channel used cv::Mat(), // no mask is used hist, // the resulting histogram 3, // it is a 3D histgram histSize, // number of bins ranges // pixel value range ); return hist; } cv::SparseMat ColorHistogram::getSpareHistogram(const cv::Mat &image) { // Compute histogram first cv::SparseMat hist(3, histSize, CV_32F); // Compute histogram cv::calcHist(&image, 1, // histogram from 1 image only channels, // the channel used cv::Mat(), // no mask is used hist, // the resulting histogram 3, // it is a 3D histgram histSize, // number of bins ranges // pixel value range ); return hist; }
Applying look-up tables to modify image appearance
A look-up table is a simple one-to-one (or many-to-one) function that defines how pixel values are transformed into new values. It is a 1D array with, in the case of regular gray-level images, 256 entries. Entry i of the table gives the new intensity value of the corresponding gray level, that is:
newIntensity= lookup[oldIntensity];
Function cv::LUT in OpenCV applies a look-up table to an image in order to produce a new image. We can add this function to our Histogram1D class:
cv::Mat Histogram1D::applyLookUp(const cv::Mat& image, // input image const cv::Mat& lookup) { // 1*256 uchar matrix // the output image cv::Mat result; // apply the lookup table cv::LUT(image, lookup, result); return result; } cv::Mat Histogram1D::strech(const cv::Mat &image, int minValue /* = 0 */) { // Compute histogram first cv::MatND hist = getHistogram(image); // find left extremity of the histogram int imin = 0; for ( ; imin < histSize[0]; imin ++) { std::cout << hist.at<float>(imin) << std::endl; if (hist.at<float >(imin) > minValue) { break; } } // find right extremity of the histogram int imax = histSize[0] - 1; for ( ; imax >= 0; imax --) { if (hist.at<float >(imax) > minValue) break; } // Create lookup table int dim(256); cv::Mat lookup(1, // 1 dimension &dim, // 256 entries CV_8U // uchar ); // Build lookup table for (int i = 0; i < 256; i++) { // stretch between imin and imax if (i < imin) lookup.at<uchar>(i) = 0; else if (i > imax) lookup.at<uchar>(i) = 255; //linear mapping else lookup.at<uchar>(i) = static_cast <uchar>(255.0 * (i - imin) / (imax - imin) + 0.5); } // Apply lookup table cv::Mat result; result = applyLookUp(image, lookup); return result; }
Using the function as follows:
cv::Mat streched = h.strech(image, 100); cv::namedWindow( "streched image", CV_WINDOW_AUTOSIZE); cv::imshow( "streched image", streched); cv::Mat strechedHistoImage = h.getHistogramImage(streched); cv::namedWindow( "strechedHistoImage", CV_WINDOW_AUTOSIZE); cv::imshow( "strechedHistoImage", strechedHistoImage);
results as follows:
Equalizing the image histogram
OpenCV offers an easy-to-use function that performs histogram equalization. It can be called as follows:
cv::Mat Histogram1D::equalize(const cv::Mat &image) { cv::Mat result; cv::equalizeHist(image, result); return result; }
result as follows:
Backprojecting a histogram to detect specific image content
ContentFinder.h
#include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> class ContentFinder { private: float hranges[2]; const float* ranges[3]; int channels[3]; float threshold; cv::MatND histogram; public: ContentFinder() : threshold(-1.0f) { ranges[0] = hranges; // all channels have same range ranges[1] = hranges; ranges[2] = hranges; } // Sets the threshold on histogram values [0, 1] void setThreshold(float t) { threshold = t; } // Gets the threshold float getThreshold() { return threshold; } // Sets the reference histogram void setHistogram(const cv::MatND &h) { histogram = h; cv::normalize(histogram, histogram, 1.0); } cv::Mat find(const cv::Mat &image, float minValue, float maxValue, int *channels, int dim); };
ContentFinder.cpp
#include "ContentFinder.h" cv::Mat ContentFinder::find(const cv::Mat &image, float minValue, float maxValue, int *channels, int dim) { cv::Mat result; hranges[0] = minValue; hranges[1] = maxValue; for (int i = 0; i < dim; i++) { this->channels[i] = channels[i]; } cv::calcBackProject(&image, 1, // input image channels, // list of channels used histogram, // the histogram we are using result, // the resulting backprojection ranges, // the range of values 255.0 // the scaling factor ); // Threshold back projection to obtain a binary image if (threshold > 0.0) cv::threshold(result, result, 255 * threshold, 255, cv::THRESH_BINARY); return result; }
Let's now use a BGR histogram on the color version of the image we used above. This time, we will try to detect the blue sky area. We will first load the color image, reduce the number of color using the color reduction function of Chapter 2, and define the region of interest: ColorHistogram hc;
ColorHistogram hc;// load color imagecv::Mat color = cv::imread( "waves.jpg");//reduce colorscolor = hc.colorReduce(color, 32);// blue sky areacv::Mat imageROI = color(cv::Rect(0, 0, 165, 75));
Next, you compute the histogram and use the findmethod to detect the sky portion
of the image:
cv::MatND hist = hc.getHistogram(imageROI);ContentFinder finder;finder.setHistogram(hist);finder.setThreshold(0.05f);//Get back-projection of color histogramcv::Mat result = finder.find(color);cv::namedWindow( "original image", CV_WINDOW_AUTOSIZE);cv::imshow( "original image", color);cv::namedWindow( "color back projection result", CV_WINDOW_AUTOSIZE);cv::imshow( "color back projection result", result);
The result of the detection on the color version of the image, of the previous section is seen
here:
Using the mean shift algorithm to find an object
colorhistogram.h
#if !defined COLHISTOGRAM #define COLHISTOGRAM #include <opencv2\core\core.hpp> #include <opencv2\imgproc\imgproc.hpp> class ColorHistogram { private: int histSize[3]; float hranges[2]; const float* ranges[3]; int channels[3]; public: ColorHistogram() { // Prepare arguments for a color histogram histSize[0]= histSize[1]= histSize[2]= 256; hranges[0]= 0.0; // BRG range hranges[1]= 255.0; ranges[0]= hranges; // all channels have the same range ranges[1]= hranges; ranges[2]= hranges; channels[0]= 0; // the three channels channels[1]= 1; channels[2]= 2; } // Computes the histogram. cv::MatND getHistogram(const cv::Mat &image) { cv::MatND hist; // BGR color histogram hranges[0]= 0.0; // BRG range hranges[1]= 255.0; channels[0]= 0; // the three channels channels[1]= 1; channels[2]= 2; // Compute histogram cv::calcHist(&image, 1, // histogram of 1 image only channels, // the channel used cv::Mat(), // no mask is used hist, // the resulting histogram 3, // it is a 3D histogram histSize, // number of bins ranges // pixel value range ); return hist; } // Computes the histogram. cv::SparseMat getSparseHistogram(const cv::Mat &image) { cv::SparseMat hist(3,histSize,CV_32F); // BGR color histogram hranges[0]= 0.0; // BRG range hranges[1]= 255.0; channels[0]= 0; // the three channels channels[1]= 1; channels[2]= 2; // Compute histogram cv::calcHist(&image, 1, // histogram of 1 image only channels, // the channel used cv::Mat(), // no mask is used hist, // the resulting histogram 3, // it is a 3D histogram histSize, // number of bins ranges // pixel value range ); return hist; } // Computes the 2D ab histogram. // BGR source image is converted to Lab cv::MatND getabHistogram(const cv::Mat &image) { cv::MatND hist; // Convert to Lab color space cv::Mat lab; cv::cvtColor(image, lab, CV_BGR2Lab); // Prepare arguments for a 2D color histogram hranges[0]= -128.0; hranges[1]= 127.0; channels[0]= 1; // the two channels used are ab channels[1]= 2; // Compute histogram cv::calcHist(&lab, 1, // histogram of 1 image only channels, // the channel used cv::Mat(), // no mask is used hist, // the resulting histogram 2, // it is a 2D histogram histSize, // number of bins ranges // pixel value range ); return hist; } // Computes the 1D Hue histogram with a mask. // BGR source image is converted to HSV cv::MatND getHueHistogram(const cv::Mat &image) { cv::MatND hist; // Convert to Lab color space cv::Mat hue; cv::cvtColor(image, hue, CV_BGR2HSV); // Prepare arguments for a 1D hue histogram hranges[0]= 0.0; hranges[1]= 180.0; channels[0]= 0; // the hue channel // Compute histogram cv::calcHist(&hue, 1, // histogram of 1 image only channels, // the channel used cv::Mat(), // no mask is used hist, // the resulting histogram 1, // it is a 1D histogram histSize, // number of bins ranges // pixel value range ); return hist; } cv::Mat colorReduce(const cv::Mat &image, int div=64) { int n= static_cast<int >(log(static_cast <double >(div))/log(2.0)); // mask used to round the pixel value uchar mask= 0xFF<<n; // e.g. for div=16, mask= 0xF0 cv::Mat_<cv::Vec3b>::const_iterator it= image.begin<cv::Vec3b>(); cv::Mat_<cv::Vec3b>::const_iterator itend= image.end<cv::Vec3b>(); // Set output image (always 1-channel) cv::Mat result(image.rows,image.cols,image.type()); cv::Mat_<cv::Vec3b>::iterator itr= result.begin<cv::Vec3b>(); for ( ; it!= itend; ++it, ++itr) { (*itr)[0]= ((*it)[0]&mask) + div/2; (*itr)[1]= ((*it)[1]&mask) + div/2; (*itr)[2]= ((*it)[2]&mask) + div/2; } return result; } // Computes the 1D Hue histogram with a mask. // BGR source image is converted to HSV // Pixels with low saturation are ignored cv::MatND getHueHistogram(const cv::Mat &image, int minSaturation=0) { cv::MatND hist; // Convert to HSV color space cv::Mat hsv; cv::cvtColor(image, hsv, CV_BGR2HSV); // Mask to be used (or not) cv::Mat mask; if (minSaturation>0) { // Spliting the 3 channels into 3 images std::vector<cv::Mat> v; cv::split(hsv,v); // Mask out the low saturated pixels cv::threshold(v[1],mask,minSaturation,255, cv::THRESH_BINARY); } // Prepare arguments for a 1D hue histogram hranges[0]= 0.0; hranges[1]= 180.0; channels[0]= 0; // the hue channel // Compute histogram cv::calcHist(&hsv, 1, // histogram of 1 image only channels, // the channel used mask, // binary mask hist, // the resulting histogram 1, // it is a 1D histogram histSize, // number of bins ranges // pixel value range ); return hist; } }; #endif
bojectFinder.h
#if !defined OFINDER #define OFINDER #include <opencv2\core\core.hpp> #include <opencv2\imgproc\imgproc.hpp> class ObjectFinder { private: float hranges[2]; const float* ranges[3]; int channels[3]; float threshold; cv::MatND histogram; cv::SparseMat shistogram; bool isSparse; public: ObjectFinder() : threshold(0.1f), isSparse(false) { ranges[0]= hranges; // all channels have the same range ranges[1]= hranges; ranges[2]= hranges; } // Sets the threshold on histogram values [0,1] void setThreshold(float t) { threshold= t; } // Gets the threshold float getThreshold() { return threshold; } // Sets the reference histogram void setHistogram(const cv::MatND& h) { isSparse= false; histogram= h; cv::normalize(histogram,histogram,1.0); } // Sets the reference histogram void setHistogram(const cv::SparseMat& h) { isSparse= true; shistogram= h; cv::normalize(shistogram,shistogram,1.0,cv::NORM_L2); } // Finds the pixels belonging to the histogram cv::Mat find(const cv::Mat& image) { cv::Mat result; hranges[0]= 0.0; // range [0,255] hranges[1]= 255.0; channels[0]= 0; // the three channels channels[1]= 1; channels[2]= 2; if (isSparse) { // call the right function based on histogram type cv::calcBackProject(&image, 1, // one image channels, // vector specifying what histogram dimensions belong to what image channels shistogram, // the histogram we are using result, // the resulting back projection image ranges, // the range of values, for each dimension 255.0 // the scaling factor is chosen such that a histogram value of 1 maps to 255 ); } else { cv::calcBackProject(&image, 1, // one image channels, // vector specifying what histogram dimensions belong to what image channels histogram, // the histogram we are using result, // the resulting back projection image ranges, // the range of values, for each dimension 255.0 // the scaling factor is chosen such that a histogram value of 1 maps to 255 ); } // Threshold back projection to obtain a binary image if (threshold>0.0) cv::threshold(result, result, 255*threshold, 255, cv::THRESH_BINARY); return result; } cv::Mat find(const cv::Mat& image, float minValue, float maxValue, int *channels, int dim) { cv::Mat result; hranges[0]= minValue; hranges[1]= maxValue; for (int i=0; i<dim; i++) this->channels[i]= channels[i]; if (isSparse) { // call the right function based on histogram type cv::calcBackProject(&image, 1, // we only use one image at a time channels, // vector specifying what histogram dimensions belong to what image channels shistogram, // the histogram we are using result, // the resulting back projection image ranges, // the range of values, for each dimension 255.0 // the scaling factor is chosen such that a histogram value of 1 maps to 255 ); } else { cv::calcBackProject(&image, 1, // we only use one image at a time channels, // vector specifying what histogram dimensions belong to what image channels histogram, // the histogram we are using result, // the resulting back projection image ranges, // the range of values, for each dimension 255.0 // the scaling factor is chosen such that a histogram value of 1 maps to 255 ); } // Threshold back projection to obtain a binary image if (threshold>0.0) cv::threshold(result, result, 255*threshold, 255, cv::THRESH_BINARY); return result; } }; #endif
finder.cpp
#include <iostream> #include <vector> using namespace std; #include <opencv2\core\core.hpp> #include <opencv2\highgui\highgui.hpp> #include <opencv2\imgproc\imgproc.hpp> #include <opencv2\video\tracking.hpp> #include "objectFinder.h" #include "colorhistogram.h" int main() { // Read reference image cv::Mat image= cv::imread("../baboon1.jpg" ); if (!image.data) return 0; // Define ROI cv::Mat imageROI= image(cv::Rect(110,260,35,40)); cv::rectangle(image, cv::Rect(110,260,35,40),cv::Scalar(0,0,255)); // Display image cv::namedWindow( "Image"); cv::imshow( "Image",image); // Get the Hue histogram int minSat=65; ColorHistogram hc; cv::MatND colorhist= hc.getHueHistogram(imageROI,minSat); ObjectFinder finder; finder.setHistogram(colorhist); finder.setThreshold(0.2f); // Convert to HSV space cv::Mat hsv; cv::cvtColor(image, hsv, CV_BGR2HSV); // Split the image vector<cv::Mat> v; cv::split(hsv,v); // Eliminate pixels with low saturation cv::threshold(v[1],v[1],minSat,255,cv::THRESH_BINARY); cv::namedWindow( "Saturation"); cv::imshow( "Saturation",v[1]); // Get back-projection of hue histogram int ch[1]={0}; cv::Mat result= finder.find(hsv,0.0f,180.0f,ch,1); cv::namedWindow( "Result Hue"); cv::imshow( "Result Hue",result); cv::bitwise_and(result,v[1],result); cv::namedWindow( "Result Hue and"); cv::imshow( "Result Hue and",result); // Second image image= cv::imread("../baboon3.jpg"); // Display image cv::namedWindow( "Image 2"); cv::imshow( "Image 2",image); // Convert to HSV space cv::cvtColor(image, hsv, CV_BGR2HSV); // Split the image cv::split(hsv,v); // Eliminate pixels with low saturation cv::threshold(v[1],v[1],minSat,255,cv::THRESH_BINARY); cv::namedWindow( "Saturation"); cv::imshow( "Saturation",v[1]); // Get back-projection of hue histogram result= finder.find(hsv,0.0f,180.0f,ch,1); cv::namedWindow( "Result Hue"); cv::imshow( "Result Hue",result); // Eliminate low stauration pixels cv::bitwise_and(result,v[1],result); cv::namedWindow( "Result Hue and"); cv::imshow( "Result Hue and",result); // Get back-projection of hue histogram finder.setThreshold(-1.0f); result= finder.find(hsv,0.0f,180.0f,ch,1); cv::bitwise_and(result,v[1],result); cv::namedWindow( "Result Hue and raw"); cv::imshow( "Result Hue and raw",result); cv::Rect rect(110,260,35,40); cv::rectangle(image, rect, cv::Scalar(0,0,255)); cv::TermCriteria criteria(cv::TermCriteria::MAX_ITER,10,0.01); cout << "meanshift= " << cv::meanShift(result,rect,criteria) << endl; cv::rectangle(image, rect, cv::Scalar(0,255,0)); // Display image cv::namedWindow( "Image 2 result"); cv::imshow( "Image 2 result",image); cv::waitKey(); return 0; }
results:
Retrieving similar images using histogram comparison
imageComparator.h
#if !defined ICOMPARATOR #define ICOMPARATOR #include <opencv2\core\core.hpp> #include <opencv2\imgproc\imgproc.hpp> #include "colorhistogram.h" class ImageComparator { private: cv::Mat reference; cv::Mat input; cv::MatND refH; cv::MatND inputH; ColorHistogram hist; int div; public: ImageComparator() : div(32) { } // Color reduction factor // The comparaison will be made on images with // color space reduced by this factor in each dimension void setColorReduction( int factor) { div= factor; } int getColorReduction() { return div; } void setReferenceImage(const cv::Mat& image) { reference= hist.colorReduce(image,div); refH= hist.getHistogram(reference); } double compare(const cv::Mat& image) { input= hist.colorReduce(image,div); inputH= hist.getHistogram(input); return cv::compareHist(refH,inputH,CV_COMP_INTERSECT); } }; #endif
retrieve.cpp
#include <iostream> using namespace std; #include <opencv2\core\core.hpp> #include <opencv2\highgui\highgui.hpp> #include "imageComparator.h" int main() { // Read reference image cv::Mat image= cv::imread("../waves.jpg" ); if (!image.data) return 0; // Display image cv::namedWindow( "Query Image"); cv::imshow( "Query Image",image); ImageComparator c; c.setReferenceImage(image); // Read an image and compare it with reference cv::Mat input= cv::imread("../dog.jpg" ); cout << "waves vs dog: " << c.compare(input) << endl; // Read an image and compare it with reference input= cv::imread("../marais.jpg"); cout << "waves vs marais: " << c.compare(input) << endl; // Read an image and compare it with reference input= cv::imread("../bear.jpg"); cout << "waves vs bear: " << c.compare(input) << endl; // Read an image and compare it with reference input= cv::imread("../beach.jpg"); cout << "waves vs beach: " << c.compare(input) << endl; // Read an image and compare it with reference input= cv::imread("../polar.jpg"); cout << "waves vs polar: " << c.compare(input) << endl; // Read an image and compare it with reference input= cv::imread("../moose.jpg"); cout << "waves vs moose: " << c.compare(input) << endl; // Read an image and compare it with reference input= cv::imread("../lake.jpg"); cout << "waves vs lake: " << c.compare(input) << endl; // Read an image and compare it with reference input= cv::imread("../fundy.jpg"); cout << "waves vs fundy: " << c.compare(input) << endl; cv::waitKey(); return 0; }
results: