图像直方图包含丰富的图像细节信息,反映了图像像素点的概率分布情况,它统计了每一个强度值具有的像素个数。灰度级范围是[0,L-1]的数字图像的直方图是离散函数h(rk)=nk,其中是rk第k级灰度值,nk是图像中灰度为rk的像素个数。在实践中,经常用乘积MN表示的图像像素总数除它的每个分量来归一化直方图,M、N是图像的行列数。因此归一化后的直方图由p(rk)=nk/MN给出。直方图的横坐标表示灰度级,纵坐标表示图像中该灰度级出现的次数(频率)。
一般来说,在暗图像中,直方图的分量集中在灰度级较低的一侧。亮图像的直方图分量集中在灰度级值较高的一侧。低对比度的图像具有较窄的直方图,且集中于灰度级的中部。高对比度的图像中直方图的分量覆盖了很宽的灰度级范围。
图1 亮图像及其灰度直方图
图2 暗图像及其灰度直方图
图3 高对比度图像及其灰度直方图
图4 低对比度图像及其灰度直方图
opencv中提供了calchist函数用于计算图像的直方图。其声明如下:
void calcHist(const Mat* arrays, int narrays, const int* channels, InputArray mask, OutputArray hist, int dims, const int* histSize, const float** ranges, bool uniform=true, bool accumulate=false );
int main()
{
Mat src, dst;
src = imread("1.jpg");
if (!src.data)
{
return -1;
}
/// 通道分离
vector bgr_planes;
split(src, bgr_planes);
int histSize = 256;
/// 设置范围
float range[] = { 0, 256 };
const float* histRange = { range };
bool uniform = true; bool accumulate = false;
Mat b_hist, g_hist, r_hist;
/// 计算直方图:
calcHist(&bgr_planes[0], 1, 0, Mat(), b_hist, 1, &histSize, &histRange, uniform, accumulate);
calcHist(&bgr_planes[1], 1, 0, Mat(), g_hist, 1, &histSize, &histRange, uniform, accumulate);
calcHist(&bgr_planes[2], 1, 0, Mat(), r_hist, 1, &histSize, &histRange, uniform, accumulate);
// 创建画布
int hist_w = 512; int hist_h = 400;
int bin_w = cvRound((double)hist_w / histSize);
Mat histImage(hist_h, hist_w, CV_8UC3, Scalar(0, 0, 0));
/// 归一化到 [ 0, histImage.rows ]
normalize(b_hist, b_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat());
normalize(g_hist, g_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat());
normalize(r_hist, r_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat());
/// 画直方图
for (int i = 1; i < histSize; i++)
{
line(histImage, Point(bin_w*(i - 1), hist_h - cvRound(b_hist.at(i - 1))),
Point(bin_w*(i), hist_h - cvRound(b_hist.at(i))),
Scalar(255, 0, 0), 2, 8, 0);
line(histImage, Point(bin_w*(i - 1), hist_h - cvRound(g_hist.at(i - 1))),
Point(bin_w*(i), hist_h - cvRound(g_hist.at(i))),
Scalar(0, 255, 0), 2, 8, 0);
line(histImage, Point(bin_w*(i - 1), hist_h - cvRound(r_hist.at(i - 1))),
Point(bin_w*(i), hist_h - cvRound(r_hist.at(i))),
Scalar(0, 0, 255), 2, 8, 0);
}
namedWindow("calcHist Demo", CV_WINDOW_AUTOSIZE);
imshow("calcHist Demo", histImage);
waitKey(0);
return 0;
}
以下是使用QChart进行直方图显示的代码。对彩色图像分通道处理,当鼠标移动到指定位置时,还可以显示横纵坐标。
使用QLineSerial绘制折线,使用QScatterseries进行离散点的绘制,QScatterseries的hovered()信号中捕捉点的坐标。本代码使用QT6.2.2实现,没有使用QChart中已被弃用的函数。要注意的是,多个数据在同一坐标系中显示,后添加的数据必须设置attachAxis()进行坐标系的绑定,否则数据显示将会发生错误。
#include "histdialog.h"
#include
#include
#include
#include
#include
int rangeY = 0;
HistDialog::HistDialog(cv::Mat &mat, QWidget *parent) :
QDialog(parent), maxVal(0)
{
setWindowTitle("灰度直方图");
sourceImg = mat;
RcheckBox = new QCheckBox("R");
GcheckBox = new QCheckBox("G");
BcheckBox = new QCheckBox("B");
imageHist(sourceImg.channels(), 256);
seriesR = new QLineSeries();
seriesG = new QLineSeries();
seriesB = new QLineSeries();
chart = new QChart();
chart->legend()->hide();
ScatterseriesR1 = new QScatterSeries();
ScatterseriesR1->setMarkerShape(QScatterSeries::MarkerShapeCircle);//圆形的点
ScatterseriesR1->setBorderColor(QColor(Qt::red)); //离散点边框颜色
ScatterseriesR1->setBrush(QBrush(QColor(Qt::red)));//离散点背景色
ScatterseriesR1->setMarkerSize(2); //离散点大小
ScatterseriesG1 = new QScatterSeries();
ScatterseriesG1->setMarkerShape(QScatterSeries::MarkerShapeCircle);//圆形的点
ScatterseriesG1->setBorderColor(QColor(Qt::green)); //离散点边框颜色
ScatterseriesG1->setBrush(QBrush(QColor(Qt::green)));//离散点背景色
ScatterseriesG1->setMarkerSize(2); //离散点大小
ScatterseriesB1 = new QScatterSeries();
ScatterseriesB1->setMarkerShape(QScatterSeries::MarkerShapeCircle);//圆形的点
ScatterseriesB1->setBorderColor(QColor(Qt::blue)); //离散点边框颜色
ScatterseriesB1->setBrush(QBrush(QColor(Qt::blue)));//离散点背景色
ScatterseriesB1->setMarkerSize(2); //离散点大小
//设置横纵坐标
QValueAxis *axisX = new QValueAxis;
axisX->setRange(0, 255);
axisX->setTickType(QValueAxis::TickType::TicksDynamic);
axisX->setTickInterval(20);
axisX->setLabelFormat("%d");
QValueAxis *axisY = new QValueAxis;
axisY->setRange(0, maxVal);
axisY->setTickType(QValueAxis::TickType::TicksDynamic);
axisY->setTickInterval(400);
axisY->setLabelFormat("%d");
if(sourceImg.channels() == 1)
{
for(int i = 0; i < 256; i++)
{
seriesR->append(i, RChannelhist.at(i));
ScatterseriesR1->append(i, RChannelhist.at(i));
}
seriesR->setColor(Qt::red);
chart->addSeries(seriesR);
chart->addSeries(ScatterseriesR1);
chart->addAxis(axisX, Qt::AlignBottom);
chart->addAxis(axisY, Qt::AlignLeft);
}
else if(sourceImg.channels() == 3)
{
for(int i = 0; i < 256; i++)
{
seriesR->append(i, RChannelhist.at(i));
ScatterseriesR1->append(i, RChannelhist.at(i));
seriesG->append(i, GChannelhist.at(i));
ScatterseriesG1->append(i, GChannelhist.at(i));
seriesB->append(i, BChannelhist.at(i));
ScatterseriesB1->append(i, BChannelhist.at(i));
}
seriesR->setColor(Qt::red);
seriesG->setColor(Qt::green);
seriesB->setColor(Qt::blue);
chart->addSeries(seriesR);
chart->addSeries(ScatterseriesR1);
chart->addAxis(axisX, Qt::AlignBottom);
chart->addAxis(axisY, Qt::AlignLeft);
chart->addSeries(seriesG);
chart->addSeries(ScatterseriesG1);
seriesG->attachAxis(axisX);//各个通道使用同一坐标系显示
seriesG->attachAxis(axisY);
ScatterseriesG1->attachAxis(axisX);
ScatterseriesG1->attachAxis(axisY);
chart->addSeries(seriesB);
chart->addSeries(ScatterseriesB1);
seriesB->attachAxis(axisX);
seriesB->attachAxis(axisY);
ScatterseriesB1->attachAxis(axisX);
ScatterseriesB1->attachAxis(axisY);
}
area = new QChartView(chart);
area->setRenderHint(QPainter::Antialiasing);
area->setMouseTracking(true);
area->setMinimumSize(600, 500);
m_valueLabel = new QLabel(area);
m_valueLabel->setFixedSize(80, 30);
m_valueLabel->setAutoFillBackground(true);
m_valueLabel->setStyleSheet(QString("QLabel{color:#000000; font-family:\"Microsoft Yahei\"; font-size:12px;"
" background-color:rgba(21, 100, 255, 51); border-radius:4px; text-align:center;}"));
m_valueLabel->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
m_valueLabel->hide();
mainLayout = new QGridLayout(this);
mainLayout->addWidget(area, 0, 0, 1, 3);
mainLayout->addWidget(RcheckBox, 1, 0, 1, 1, Qt::AlignCenter);
mainLayout->addWidget(GcheckBox, 1, 1, 1, 1, Qt::AlignCenter);
mainLayout->addWidget(BcheckBox, 1, 2, 1, 1, Qt::AlignCenter);
connect (RcheckBox, SIGNAL(stateChanged(int)), this, SLOT(imhist(int)));
connect (GcheckBox, SIGNAL(stateChanged(int)), this, SLOT(imhist(int)));
connect (BcheckBox, SIGNAL(stateChanged(int)), this, SLOT(imhist(int)));
setAttribute(Qt::WA_DeleteOnClose);
connect(ScatterseriesR1, SIGNAL(hovered(const QPointF &, bool)), this, SLOT(dataShow(const QPointF&, bool)));
connect(ScatterseriesG1, SIGNAL(hovered(const QPointF &, bool)), this, SLOT(dataShow(const QPointF&, bool)));
connect(ScatterseriesB1, SIGNAL(hovered(const QPointF &, bool)), this, SLOT(dataShow(const QPointF&, bool)));
}
HistDialog::~HistDialog()
{
}
void HistDialog::imageHist(int chn, int N)
{
//设定直方图参数
const int channels[1] = {0};
const int histSize[1] = {N};
float pranges[2] = {0.0, 255.0};
const float* ranges[1] = {pranges};
double maxVal_R = 0.0;
double maxVal_G = 0.0;
double maxVal_B = 0.0;
if(chn == 1)
{
RcheckBox->setChecked(true);
GcheckBox->setEnabled(false);
BcheckBox->setEnabled(false);
cv::calcHist(&sourceImg, 1, channels, cv::Mat(), RChannelhist, 1, histSize, ranges);
cv::minMaxLoc(RChannelhist, 0, &maxVal, 0, 0);
}
else if(chn == 3)
{
//对于彩色图像,分离各个通道进行单独计算
std::vector rgb_planes;
cv::split(sourceImg, rgb_planes);
cv::calcHist(&rgb_planes[0], 1, channels, cv::Mat(), RChannelhist, 1, histSize, ranges);
cv::calcHist(&rgb_planes[1], 1, channels, cv::Mat(), GChannelhist, 1, histSize, ranges);
cv::calcHist(&rgb_planes[2], 1, channels, cv::Mat(), BChannelhist, 1, histSize, ranges);
cv::minMaxLoc(RChannelhist, 0, &maxVal_R, 0, 0);
cv::minMaxLoc(GChannelhist, 0, &maxVal_G, 0, 0);
cv::minMaxLoc(BChannelhist, 0, &maxVal_B, 0, 0);
maxVal = std::max(maxVal_R, std::max(maxVal_G, maxVal_B));
RcheckBox->setChecked(true);
GcheckBox->setChecked(true);
BcheckBox->setChecked(true);
}
//判断最大值所处的范围
//获取最大值的位数
QString numStr = QString::number(int(maxVal));
int numsize = numStr.size();
//获取前两位数
QString firstTwoStr = numStr.left(2);
int firstTwoNum = firstTwoStr.toInt();
QString firstStr = numStr.left(1);
int firstNum = firstStr.toInt();
QString secondStr = firstTwoStr.right(1);
int secondNum = secondStr.toInt();
if(secondNum < 5)
{
secondNum = 5;
firstTwoNum = firstNum * 10 + 5;
}
else
{
firstTwoNum = (firstNum + 1) * 10;
}
rangeY = firstTwoNum * pow(10, numsize - 2);
}
void HistDialog::imhist(int i)
{
if(RcheckBox->isChecked())
{
seriesR->setVisible(true);
ScatterseriesR1->setVisible(true);
}
else
{
seriesR->setVisible(false);
ScatterseriesR1->setVisible(false);
}
if(GcheckBox->isChecked())
{
seriesG->setVisible(true);
ScatterseriesG1->setVisible(true);
}
else
{
seriesG->setVisible(false);
ScatterseriesG1->setVisible(false);
}
if(BcheckBox->isChecked())
{
seriesB->setVisible(true);
ScatterseriesB1->setVisible(true);
}
else
{
seriesB->setVisible(false);
ScatterseriesB1->setVisible(false);
}
}
void HistDialog::dataShow(const QPointF& point, bool state)
{
qDebug() << point;
if (state)
{
m_valueLabel->setText(QString::asprintf("%1.0f , %1.0f", point.x(), point.y()));
QPoint curPos = mapFromGlobal(QCursor::pos());
m_valueLabel->move(curPos.x() - m_valueLabel->width(), curPos.y() - 10);
m_valueLabel->show();//显示
}
else
{
m_valueLabel->hide();//隐藏
}
}
#ifndef HISTDIALOG_H
#define HISTDIALOG_H
#include
#include
#include
#include
#include
#include
#include
#include "common.h"
class HistDialog : public QDialog
{
Q_OBJECT
public:
explicit HistDialog(cv::Mat& mat, QWidget *parent = 0);
~HistDialog();
void imageHist(int chn, int N);
private:
QChartView* area;
QLineSeries* seriesR;
QLineSeries* seriesG;
QLineSeries* seriesB;
QScatterSeries *ScatterseriesR1;
QScatterSeries *ScatterseriesG1;
QScatterSeries *ScatterseriesB1;
QChart* chart;
QCheckBox *RcheckBox;
QCheckBox *GcheckBox;
QCheckBox *BcheckBox;
QLabel* m_valueLabel;
QGridLayout *mainLayout;
cv::Mat sourceImg;
cv::MatND RChannelhist;
cv::MatND GChannelhist;
cv::MatND BChannelhist;
double maxVal;
public slots:
void imhist(int i);
void dataShow(const QPointF & point, bool state);
};
#endif // HISTDIALOG_H