前言
越来越多的开发人员选择基于开源的Qt框架与OpenCV来实现界面和算法,其原因不单单是无版权问题,更多是两个社区的发展蓬勃,可用来学习的资料与例程特别丰富。以下是关于利用Qt构建GUI并使用OpenCV中的split/calcHist/normalize函数进行直方图计算。
软件版本:Qt-5.12.0/OpenCV-4.5.3
平台:Windows10/11–64
一、函数介绍
1 split
cv::split(const Mat& src, Mat *mvBegin)
cv::split(InputArray m, OutputArrayOfArrays mv);
参数解释:
src/m:要进行分离的图像矩阵;
mvBegin:Mat数组的首地址;
mv:vector对象;
2 calcHist
cv::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 )
cv::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 )
参数解释:
images:源图像数组,它们有同样的位深CV_8U或 CV_32F ,同样的尺寸;图像阵列中的每一个图像都可以有任意多个通道;
nimages:源图像的数目;
channels:维度通道序列,第一幅图像的通道标号从0~image[0].channels( )-1。Image[0]表示图像数组中的第一幅图像,channels()表示该图像的通道数量。同理,图像阵列中的第二幅图像,通道标号从image[0].channerls( )开始,到image[1].channels( )-1为止;第三、四幅图像的通道标号顺序依此类推;也就是说图像阵列中的所有图像的通道根据图像排列顺序,排成一个通道队列;
mask: 可选择的mask。如果该矩阵不空的话,它必须是一个8-bit的矩阵,与images[i]同尺寸。在图像中,只有被mask覆盖的区域的像素才参与直方图统计。如果这个参数想用默认值,输入Mat()就可以了;
hist:输出直方图, 它是一个稠密或稀疏矩阵,具有dims个维度;
dims :直方图的维度,一定是正值, CV_MAX_DIMS(当前OpenCV版本是32个);
histSize:数组,即histSize[i]表示第i个维度上bin的个数;这里的维度可以理解为通道;
ranges: 当uniform=true时,ranges是多个二元数组组成的数组;当uniform=false时,ranges是多元数组组成的数组。当在每个维度(或通道)上每个直方条等宽时,即uniform=true时,灰度值的有效统计范围的下界用L0表示,上界用UhistSize[i]-1表示,角标中的i表示第i个维度(或通道),上下界值可以表示为hrange[i]={ L0, UhistSize[i]-1}, 在统计时, L0和UhistSize[i]-1不在统计范围内。而ranges={ hrange[0], hrange[1], …… , hrange[dims]}。ranges的元素个数由参数dims决定。其中,L0表示在该通道上第0个直方条(bin)的下边界,UhistSize[i]-1表示最后一个直方条histSize[i]-1的上边界。在该维度上直方条的个数为histSize[i],如hrange[0]={ L0, UhistSize[0]},hrange[1]={ L1, UhistSize[1]}, hrange[2]={ L2, UhistSize[2]}, …… , hrange[dims]={ L0, UhistSize[0]}。当uniform=false时,ranges中的每个元素ranges[i]都是一个多元数组,元素个数为histSize[i]+1,它们分别是:L0 , U0=L1, U1= L2, …… ,UhistSize[i]-2 , LhistSize[i]-1 , UhistSize[i]-1 。所以,ranges[i]={ L0 , L1, L2, …… , LhistSize[i]-1 ,UhistSize[i]-1};
uniform:标识,用于说明直方条bin是否是均匀等宽的;
accumulate: 累积标识。如果该项设置,当直方图重新分配时,直方图在开始清零。这个特征可以使你通过几幅图像,累积计算一个简单的直方图,或者及时更新直方图;
函数calcHist可以计算一幅或多幅图像的直方图。在元组中增量一个直方图的时候,就是从输入图像组中的原位置提取一幅图像,并计算出它的直方图,并添加到元组中。当参数dims>1时,输出矩阵Hist是二维矩阵。
3 normalize
cv::normalize(InputArry src, InputOutputArray dst, double alpha=1, double beta=0, int norm_type=NORM_L2, int dtype=-1, InputArray mark=noArry())
参数解释:
src:输入数组;
**dst **:输出数组,数组的大小和原数组一致;
alpha:1-用来规范值,2-规范范围,并且是下限;
beta:只用来规范范围并且是上限;
**norm_type **: 归一化选择的数学公式类型;
**dtype **:当为负,输出的宽高通道数都等于输入,当为正,输出只在深度与输入不同,不同的地方由dtype决定;
mark:掩码,选择感兴趣区域,选定后只能对该区域进行操作;
函数作用归一化数据,该函数分为范围归一化与数据值归一化,经常应用在将数值限定在一个范围,以便使用同一套阈值参数的情况。
二、演示
1、GUI
如上图创建Histgram的功能按钮QPushButton, 直方图展示在Histgram的窗口中。
2、实现代码
histBtn的clicked()槽函数实现如下:
void MainWindow::on_histBtn_clicked() { std::size_t numView = ui->tabWidget->currentIndex() % 4; if (dispMat[numView]->empty()) { outputInfo(2, tr("Please make sure the Mat exist!")); } std::vector<cv::Mat> rgb_planes; if (dispMat[numView]->channels() == 3) { cv::split(*dispMat[numView], rgb_planes); // split函数,分离通道; } else { rgb_planes.push_back(*dispMat[numView]); rgb_planes.push_back(*dispMat[numView]); rgb_planes.push_back(*dispMat[numView]); } outputInfo(1, "Channels: " + QString::number(dispMat[numView]->channels())); int histSize = 255; float range[] = {0, 255}; const float* histRange = {range}; bool uniform = true; bool accumulate = false; cv::Mat r_hist, g_hist, b_hist; cv::calcHist(&rgb_planes[0], 1, nullptr, cv::Mat(), r_hist, 1, \ // 直方图计算 &histSize, &histRange, uniform, accumulate); cv::calcHist(&rgb_planes[1], 1, nullptr, cv::Mat(), g_hist, 1,\ &histSize, &histRange, uniform, accumulate); cv::calcHist(&rgb_planes[0], 1, nullptr, cv::Mat(), b_hist, 1,\ &histSize, &histRange, uniform, accumulate); int hist_w = 512; int hist_h = 800; cv::Mat tmpMat(hist_w, hist_h, CV_8UC3, cv::Scalar(0, 0, 0)); // 准备画布 /* cv::normalize(r_hist, r_hist, 0, tmpMat.rows, cv::NORM_MINMAX, \ // 归一化 -1, cv::Mat()); cv::normalize(g_hist, g_hist, 0, tmpMat.rows, cv::NORM_MINMAX, \ -1, cv::Mat()); cv::normalize(b_hist, b_hist, 0, tmpMat.rows, cv::NORM_MINMAX, \ -1, cv::Mat()); int bin_w = cvRound(static_cast<double> (hist_w/histSize)); for (int i = 1; i < histSize; i++) { cv::line(tmpMat, cv::Point(bin_w*(i-1), hist_h - cvRound(r_hist.at<float>(i-1))), cv::Point(bin_w*(i), hist_h - cvRound(r_hist.at<float>(i))), cv::Scalar(0, 0, 255), 2, 8, 0); cv::line(tmpMat, cv::Point(bin_w*(i-1), hist_h - cvRound(g_hist.at<float>(i-1))), cv::Point(bin_w*(i), hist_h - cvRound(g_hist.at<float>(i))), cv::Scalar(0, 0, 255), 2, 8, 0); cv::line(tmpMat, cv::Point(bin_w*(i-1), hist_h - cvRound(b_hist.at<float>(i-1))), cv::Point(bin_w*(i), hist_h - cvRound(b_hist.at<float>(i))), cv::Scalar(0, 0, 255), 2, 8, 0); } */ double rMax, gMax, bMax; cv::minMaxLoc(r_hist, nullptr, &rMax, nullptr, nullptr); cv::minMaxLoc(g_hist, nullptr, &gMax, nullptr, nullptr); cv::minMaxLoc(b_hist, nullptr, &bMax, nullptr, nullptr); double rBin_w = static_cast<double>(tmpMat.cols / histSize); double rBin_h = static_cast<double>(tmpMat.rows / rMax); double gBin_w = static_cast<double>(tmpMat.cols / histSize); double gBin_h = static_cast<double>(tmpMat.rows / gMax); double bBin_w = static_cast<double>(tmpMat.cols / histSize); double bBin_h = static_cast<double>(tmpMat.rows / bMax); for (int i = 1; i < histSize; i++) { cv::Point rP_0 = cv::Point(static_cast<int>(i*rBin_w), tmpMat.rows); int rVal = static_cast<int>(r_hist.at<float>(i)); cv::Point rP_1 = cv::Point(static_cast<int>((i + 1)*rBin_w), \ static_cast<int>(tmpMat.rows - rVal*rBin_h)); cv::rectangle(tmpMat, rP_0, rP_1, cv::Scalar(0, 0, 255), 2, 8, 0); cv::Point gP_0 = cv::Point(static_cast<int>(i*gBin_w), tmpMat.rows); int gVal = static_cast<int>(g_hist.at<float>(i)); cv::Point gP_1 = cv::Point(static_cast<int>((i + 1)*gBin_w), \ static_cast<int>(tmpMat.rows - gVal*gBin_h)); cv::rectangle(tmpMat, gP_0, gP_1, cv::Scalar(0, 255, 0), 2, 8, 0); cv::Point bP_0 = cv::Point(static_cast<int>(i*bBin_w), tmpMat.rows); int bVal = static_cast<int>(b_hist.at<float>(i)); cv::Point bP_1 = cv::Point(static_cast<int>((i + 1)*bBin_w), \ static_cast<int>(tmpMat.rows - bVal*bBin_h)); cv::rectangle(tmpMat, bP_0, bP_1, cv::Scalar(255, 0, 0), 2, 8, 0); } char string[12]; int mark = 0; for (int i = 1; mark < static_cast<int>(rMax); i++) { mark = static_cast<int>(i * rMax / 20); itoa(mark, string, 10); cv::putText(tmpMat, string, cv::Point(0, static_cast<int>(tmpMat.rows - \ mark * rBin_h)), 1, 1, \ cv::Scalar(0, 255, 255)); } mark = 0; for (int i = 1; mark < 256; i++) { mark = i * 20; itoa(mark, string, 10); cv::putText(tmpMat, string, cv::Point(mark * (tmpMat.cols / 256), tmpMat.rows), 1, 1, cv::Scalar(0, 255, 255)); } *dispMat[3] = tmpMat.clone(); cvtMatPixmap(dispMat, dispPixmap, 3); outputInfo(1, tr("Histogram Action done.")); double minVal, maxVal; cv::Point minLoc, maxLoc; if (dispMat[numView]->channels() == 3) { cv::cvtColor(*dispMat[numView], tmpMat, cv::COLOR_RGB2GRAY); } else { tmpMat = *dispMat[numView]; } cv::minMaxLoc(tmpMat, &minVal, &maxVal, &minLoc, &maxLoc); QString minMaxLocVal = "minVal: " + QString::number(minVal) + \ " maxVal: " + QString::number(maxVal); std::stringstream tmpStream; std::streambuf* coutBuf = std::cout.rdbuf(); std::cout.rdbuf(tmpStream.rdbuf()); std::cout << " minLoc: " << minLoc << " maxLoc: " << maxLoc << std::endl; std::string minMaxLocString(tmpStream.str()); std::cout.rdbuf(coutBuf); QString infoMinMaxLoc = minMaxLocVal + QString::fromStdString(minMaxLocString); outputInfo(1, infoMinMaxLoc); }
总结
以上是关于利用Qt进行GUI构建并使用OpenCV中的split/calcHist/normalized函数进行图像缩放的介绍。