越来越多的开发人员选择基于开源的Qt框架与OpenCV来实现界面和算法,其原因不单单是无版权问题,更多是两个社区的发展蓬勃,可用来学习的资料与例程特别丰富。以下是关于利用Qt构建GUI并使用OpenCV中的split/calcHist/normalize函数进行直方图计算。
软件版本:Qt-5.12.0/OpenCV-4.5.3
平台:Windows10/11–64
cv::split(const Mat& src, Mat *mvBegin)
cv::split(InputArray m, OutputArrayOfArrays mv);
参数解释:
src/m:要进行分离的图像矩阵;
mvBegin:Mat数组的首地址;
mv:vector对象;
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是二维矩阵。
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:掩码,选择感兴趣区域,选定后只能对该区域进行操作;
函数作用归一化数据,该函数分为范围归一化与数据值归一化,经常应用在将数值限定在一个范围,以便使用同一套阈值参数的情况。
如上图创建Histgram的功能按钮QPushButton, 直方图展示在Histgram的窗口中。
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 (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(i-1))),
cv::Point(bin_w*(i), hist_h - cvRound(r_hist.at(i))),
cv::Scalar(0, 0, 255), 2, 8, 0);
cv::line(tmpMat, cv::Point(bin_w*(i-1), hist_h - cvRound(g_hist.at(i-1))),
cv::Point(bin_w*(i), hist_h - cvRound(g_hist.at(i))),
cv::Scalar(0, 0, 255), 2, 8, 0);
cv::line(tmpMat, cv::Point(bin_w*(i-1), hist_h - cvRound(b_hist.at(i-1))),
cv::Point(bin_w*(i), hist_h - cvRound(b_hist.at(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函数进行图像缩放的介绍。
参考:
链接: 往期/前期https://blog.csdn.net/richard_yuu/article/details/127859793
链接: 往期/前期https://blog.csdn.net/richard_yuu/article/details/124140746
其中疑问或错误,欢迎联系交流,微信:electrical_program