此文章为数组图像处理课程设计的笔记。项目我放在了我的GitHub上。这个系统要求用纯VC++实现以下功能:
Open BMP file
打开一个BMP文件,并在窗口中显示出来。Save to new BMP file
将当前视图保存为一个新的BMP文件(先弹出一个对话框,输入一个BMP文件名)。-
Display file header
按如下的格式显示文件头信息:
Get pixel value
取某个位置像素的颜色值,并显示出来。Set pixel value
设置某个位置像素的颜色值,并显示出来。
功能(4)和(5)所需的参数从对话框中获取。前面5个功能对灰度图像和彩色图像都适用,后面的功能仅要求针对灰度图像。
- Image interpolation
图像缩放:x和y方向的缩放因子、插值算法选择(最邻近和双线性),从对话框中获取。需要将图像缩放的结果显示出来。 - Median filtering
实现3x3的中值滤波,并将结果显示出来。 - Gaussian smoothing
从对话框中获取高斯函数的均方差,对图像做高斯平滑,并将结果显示出来。
功能(9) ~ (13)是选做题。
- Histogram equalization
直方图均衡化,并将结果显示出来。 - Sharpening by gradient
实现基于梯度的图像锐化,所需参数从对话框中获取,将锐化结果显示出来。 - Bilateral filtering
实现双边滤波,参数sigma_d和sigma_R从对话框中获取,并将结果显示出来。 - Add impulse noise
在图像中加入脉冲噪声,噪声密度和类型从对话框中获取,将噪声图像显示出来。 - Canny edge detection
实现Canny算子边缘检测,并将结果显示出来。
由于Qt把HWND和HDC这种平台相关的数据类型全都封装好了,没有提供直接的操作接口,我决定用Qt自己的绘图设备比如QImage实现这些功能。虽然用的数据类型是封装好的,但是图片读取、显示和处理算法都在像素级别处理。主要是MFC真的苦手啊。
以下分功能进行记录。
0 主界面和数据结构设计
本图像处理系统需要一个用户友好的可操作界面。我们可以用Qt自带的设计功能来实现这个界面。新建一个MainWindow工程,Qt会自动给你生成mainwindow.h
,mainwindow.cpp
和mainwindow.ui
。我在新建工程时,为了和以后可能新建的mianwindow窗口区分,将这个mainwindow类改名成了psmainwindow类。我们先打开mainwindow.ui
,进行界面的设计。
设计过程中,为了能够进行良好的布局、且能够让所有控件随窗口大小改变而改变,我们需要用到Qt的布局管理器,具体参见这篇文章:
我做出的界面是这个样子的:
其中,每个菜单项下拉内容如下:
About菜单没有下拉项。
界面设计结构如下:
接下来需要进行数据结构的设计。为了打开一张BMP图片,我们首先要知道BMP图片是如何被存储的,之后才能够对其设计数据结构,读出图像放入内存。
为了学习BMP图像的存储,有两个资料可供参考。一个是B站上的一个数字图像处理课:
数字图像处理-Digital Image Processing (DIP)
还有一篇博客:
图像识别_2010暑期实训有感【二】
BMP图像文件的数据结构如下:
//bmpfile.h
#ifndef BMPFILE_H
#define BMPFILE_H
#include <QtGlobal>
#include <QDebug>
typedef unsigned char BYTE;
typedef unsigned short WORD;//2byte
typedef unsigned int DWORD;//4byte
typedef qint32 LONG;//long 32bit
//位图文件头定义;
//其中不包含文件类型信息(由于结构体的内存结构决定,
//要是加了的话将不能正确读取文件信息)
typedef struct tagBITMAPFILEHEADER{
WORD bfType;//文件类型,必须是0x424D,即字符“BM”
DWORD bfSize;//文件大小
WORD bfReserved1;//保留字
WORD bfReserved2;//保留字
DWORD bfOffBits;//从文件头到实际位图数据的偏移字节数
}BITMAPFILEHEADER;
typedef struct tagBITMAPINFOHEADER{
DWORD biSize;//信息头大小
LONG biWidth;//图像宽度
LONG biHeight;//图像高度
WORD biPlanes;//位平面数,必须为1
WORD biBitCount;//每像素位数:1,4,8,24
DWORD biCompression; //压缩类型
DWORD biSizeImage; //压缩图像大小字节数
LONG biXPelsPerMeter; //水平分辨率
LONG biYPelsPerMeter; //垂直分辨率
DWORD biClrUsed; //位图实际用到的色彩数
DWORD biClrImportant; //本位图中重要的色彩数
}BITMAPINFOHEADER; //位图信息头定义
typedef struct tagRGBQUAD{//24位时没有这个
BYTE rgbBlue; //该颜色的蓝色分量
BYTE rgbGreen; //该颜色的绿色分量
BYTE rgbRed; //该颜色的红色分量
BYTE rgbReserved; //保留值
}RGBQUAD;//调色板定义
//像素信息:24位
typedef struct tagIMAGEDATA
{
BYTE blue;//当1,4,8时,用blue存储信息
BYTE green;
BYTE red;
}IMAGEDATA;
#endif // BMPFILE_H
为了方便对这个数据结构进行操作,我定义了BMPIMG
类,用于存储整张图像:
#ifndef BMPIMG_H
#define BMPIMG_H
#include <QtCore/QString>
#include <QFile>
#include <QMessageBox>
#include <QDataStream>
#include <QImage>
#include <bmpfile.h>
class BMPIMG
{
private:
BITMAPFILEHEADER fileHeader;
BITMAPINFOHEADER infoHeader;
RGBQUAD *rgbQuad;
IMAGEDATA *imgData;
};
#endif // BMPIMG_H
1 Open BMP file
功能说明:打开一个BMP文件,并在窗口中显示出来。
为了实现这个功能,首先需要能够获得图片的路径地址,这里我们需要使用到Qt的文件对话框类,具体使用说明参见这个文章:
在Action Editor中,右键单击actionOpen_BMP_file
动作,点击“转到槽”,即可跳转到槽函数,也就是Open BMP File项被点击之后会运行的函数。
在槽函数中添加添加获取图片地址的逻辑:
void PSMainWindow::on_actionOpen_BMP_file_triggered()
{
QString path = QFileDialog::getOpenFileName(this, tr("Open Image"), ".", tr("Image Files(*.bmp)"));
if(path.length() == 0) {
QMessageBox::information(this, tr("Path"), tr("You didn't select any files."));
}
}
之后我们需要通过图片路径读取图片。我将这个逻辑放在了BMPIMG
类中,通过构造函数实现读取一张图片、并把图片信息保存在BMPIMG
对象中,然后才能显示。
1.1 读取图片
定义bool getImage(QString filename)
函数。
用QFile
打开路径文件,并用QDataStream
进行数据读取。参考文章:
这部分代码如下
bool BMPIMG::getImage(QString filename)
{
qDebug()<<"open file: " + filename;
//open file
QFile file(filename);
if(!file.open(QIODevice::ReadOnly)){//open file failed
QMessageBox::warning(0, "Waring", "open file " + filename + " failed!", QMessageBox::Yes);
return false;
}
QDataStream dataStream(&file);
}
之后开始读取文件内容。参考文章如下:
首先读取开头2个字节,即fileHeader.bfType
,判断是不是“BM”,即16进制的424D,从而判断选中的是否是BMP图像。如果是则进行后续操作,不是则弹出警告对话框,并返回false
。
bool BMPIMG::getImage(QString filename)
{
//前面代码省略
//read file header
dataStream>>fileHeader.bfType;
if(fileHeader.bfType != 0x424D){//bfType != "BM"
QMessageBox::warning(0, "Waring", "file " + filename + " is not a BMP image!", QMessageBox::Yes);
return false;
}
}
之后开始读取图片的其余头数据,即fileHeader
和infoHeader
。要注意的是,BMP文件是小端存储,即低位在前,高位在后,比如读取数据3A 00 00 00
,实际上是00 00 00 3A
,也就是3A
;而Qt的QDataStream
类则是默认的大端读取,所以我们要先设置一下大小端,再进行数据的读取。参考文章:
bool BMPIMG::getImage(QString filename)
{
//前略
dataStream.setByteOrder(QDataStream::LittleEndian);
dataStream>>fileHeader.bfSize;
dataStream>>fileHeader.bfReserved1;
dataStream>>fileHeader.bfReserved2;
dataStream>>fileHeader.bfOffBits;
//read info header
qDebug()<<"reading info header...";
dataStream>>infoHeader.biSize;
dataStream>>infoHeader.biWidth;
dataStream>>infoHeader.biHeight;
dataStream>>infoHeader.biPlanes;
dataStream>>infoHeader.biBitCount;
dataStream>>infoHeader.biCompression; //压缩类型
dataStream>>infoHeader.biSizeImage; //压缩图像大小字节数
dataStream>>infoHeader.biXPelsPerMeter; //水平分辨率
dataStream>>infoHeader.biYPelsPerMeter; //垂直分辨率
dataStream>>infoHeader.biClrUsed; //位图实际用到的色彩数
}
这里有一个坑:256色bmp位图,有时biClrUsed
会被存储为0。所以我在函数中添加了一个判断:如果是8位存储的图像且biClrUsed = 0
,则将biClrUsed
赋值为256。
bool BMPIMG::getImage(QString filename)
{
//前略
if(infoHeader.biClrUsed == 0 && infoHeader.biBitCount == 8)
infoHeader.biClrUsed = 256;
dataStream>>infoHeader.biClrImportant; //本位图中重要的色彩数
}
接下来读取调色盘数据。需要注意的是,如果图片是24位存储的话,则没有调色盘数据,而直接在image data中存储RGB值,所以需要做一下判断。
bool BMPIMG::getImage(QString filename)
{
//前略
if(infoHeader.biBitCount != 24){
//read rgbquad
qDebug()<<"reading rgbquad...";
rgbQuad = (RGBQUAD*)malloc(sizeof(RGBQUAD) * infoHeader.biClrUsed);
for(unsigned int nCounti=0; nCounti<infoHeader.biClrUsed; nCounti++){
dataStream>>(*(rgbQuad + nCounti)).rgbBlue;
dataStream>>(*(rgbQuad + nCounti)).rgbGreen;
dataStream>>(*(rgbQuad + nCounti)).rgbRed;
dataStream>>(*(rgbQuad + nCounti)).rgbReserved;
}
}
}
同样的,在读取image data的时候也要对存储深度、即biBitCount
做一下判断。如果是24位真彩色图像,则RGB三个变量都需要用到;如果是8位的话只需要存储到rgbBlue
里就可以。
这里需要注意的是,需要把位数对齐!!!!bmp位图的数据在存储时,会把每一行的数据对齐到4的倍数,比如一个宽为7的图像,每行会存储8个IMAGEDATA
数据,最后一个全都是0。
bool BMPIMG::getImage(QString filename)
{
//前略
//read image data
imgData = (IMAGEDATA*)malloc(sizeof(IMAGEDATA) * infoHeader.biWidth * infoHeader.biHeight);
int cnt = 0;
int align = (4 - (int)infoHeader.biWidth % 4) % 4;
IMAGEDATA temp;
switch (infoHeader.biBitCount) {
case 8:
for(int i = 0; i < infoHeader.biHeight; i++){
for(int j = 0; j < infoHeader.biWidth; j++){
dataStream>>(*(imgData + cnt)).blue;
cnt++;
}
if(align!=0){
for(int k=0; k<align; k++){
dataStream >> temp.blue;
}
}
}
break;
case 24:
for(int i = 0; i < infoHeader.biHeight; i++){
for(int j = 0; j < infoHeader.biWidth; j++){
dataStream>>(*(imgData + cnt)).blue;
dataStream>>(*(imgData + cnt)).green;
dataStream>>(*(imgData + cnt)).red;
cnt++;
}
if(align!=0){
for(int k=0; k<align; k++){
dataStream >> temp.blue;
dataStream >> temp.green;
dataStream >> temp.red;
}
}
}
break;
}
file.close();
return true;
}
至此文件读取完毕。
1.2 显示图片
在显示图片之前首先了解一下坐标系的问题。根据上面提到的文章图像识别_2010暑期实训有感【二】:
一般来说,.bmp文件的数据从下到上,从左到右的。也就是说,从文件中最先读到的是图象最下面一行的左边第一个象素,然后是左边第二个象素……接下来是倒数第二行左边第一个象素,左边第二个象素……依次类推 ,最后得到的是最上面一行的最右一个象素。
而QImage
的坐标系是反着的,左上角才是原点,所以在输出(setPixel)的时候,需要从大数开始输出。基本流程就是,按顺序读取每一个像素的rgb值, 然后设置这个位置的像素为这个颜色。
要显示图片,要先将BMPIMG
对象里存储的IMAGEDATA
信息输出为QImage
。由于我不希望调用QImage自带的打开图片接口,而想在像素级别进行操作,所以我需要QImage像素级操作的接口。参考:
我们需要用到void QImage::setPixelColor(int x, int y, const [QColor](qcolor.html) &color)
这个API。
在BMPIMG
类中添加QImage toQImage()
函数:
QImage BMPIMG::toQImage()
{
int cnt = 0;
QImage outputImg = QImage(infoHeader.biWidth, infoHeader.biHeight, QImage::Format_ARGB32);
QPoint pos;
QColor color;
BYTE rgb;
if(infoHeader.biBitCount == 24){
for(int i=infoHeader.biHeight-1; i>=0; i--){
for(int j=0; j<infoHeader.biWidth; j++){
pos = QPoint(j,i);
color = QColor((imgData + cnt)->red, (imgData + cnt)->green, (imgData + cnt)->blue);
outputImg.setPixelColor(pos, color);
cnt++;
}
}
}
else{
for(int i=infoHeader.biHeight-1; i>=0; i--){
for(int j=0; j<infoHeader.biWidth; j++){
pos = QPoint(j,i);
rgb = (imgData + cnt)->blue;
color = QColor((rgbQuad + rgb)->rgbRed, (rgbQuad + rgb)->rgbGreen, (rgbQuad + rgb)->rgbBlue);
outputImg.setPixelColor(pos, color);
cnt++;
}
}
}
return outputImg;
}
这样一来,要显示图片所需的功能就都有了。接下来我们在Open BMP file项的槽函数中继续添加读取文件、显示图像的逻辑,也就是调用我们刚写好的BMPIMG
类的函数。
void PSMainWindow::on_actionOpen_BMP_file_triggered()
{
//前略
BMPIMG image(path);
setImg(image);
qDebug()<<"here";
QImage qImage = image.toQImage();
QGraphicsScene *scene = new QGraphicsScene();
scene->addPixmap(QPixmap::fromImage(qImage));
qDebug()<<qImage;
ui->graphicsView->setScene(scene);
ui->graphicsView->show();
qDebug()<<"here";
return;
}
这样一来,点击Open BMP file项之后就可以看到图片了。
2 Save to new BMP file
功能要求:将当前视图保存为一个新的BMP文件(先弹出一个对话框,输入一个BMP文件名)。
和打开文件一样,为了获取保存的路径,我们依然需要用到文件对话框,只不过用法和之前不太一样。参考 Qt入门-打开和保存文件对话框 。除此之外,在保存之前,我们需要判断是否有已经打开的图片,不然对空对象进行操作会报错。对于判断是否打开了图片,我是通过fileHeader.bfType
这一项来判断的。如果打开的文件是bmp文件,那么这个变量的值应该是0x424D,我修改了BMPIMG
类的构造函数,使得一个BMPIMG
对象刚被构造出来时,fileHeader.bfType
会被赋值为0:
BMPIMG::BMPIMG()
{
fileHeader.bfType = 0;
}
同时添加BMPIMG
判空函数bool BMPIMG::isEmpty()
,若对象为空则返回true
,否则返回false
:
bool BMPIMG::isEmpty()
{
if(fileHeader.bfType == 0x424D){
return false;
}
else{
return true;
}
}
接下来编写保存图像的逻辑。保存图像实际上就是打开图像的逆操作,怎么读出来的就怎么写回去,同样要注意对齐的问题,用0将每行不足4的倍数的位置补齐。这里还有一点要注意的是,对齐时是以BYTE
为单位,而非以IMAGEDATA
为单位。当补齐24位图像时,不需要补几个IMAGEDATA
,只需要补几个BYTE
。
bool BMPIMG::saveImage(QString path){
QFile newImg(path);
if(!newImg.open(QIODevice::WriteOnly)){
QMessageBox::warning(0, "Waring", "save file " + path + " failed!", QMessageBox::Yes);
return false;
}
QDataStream dataStream(&newImg);
dataStream<<fileHeader.bfType;
dataStream.setByteOrder(QDataStream::LittleEndian);
dataStream<<fileHeader.bfSize;
dataStream<<fileHeader.bfReserved1;
dataStream<<fileHeader.bfReserved2;
dataStream<<fileHeader.bfOffBits;
//write info header
dataStream<<infoHeader.biSize;
dataStream<<infoHeader.biWidth;
dataStream<<infoHeader.biHeight;
dataStream<<infoHeader.biPlanes;
dataStream<<infoHeader.biBitCount;
dataStream<<infoHeader.biCompression; //压缩类型
dataStream<<infoHeader.biSizeImage; //压缩图像大小字节数
dataStream<<infoHeader.biXPelsPerMeter; //水平分辨率
dataStream<<infoHeader.biYPelsPerMeter; //垂直分辨率
dataStream<<infoHeader.biClrUsed; //位图实际用到的色彩数
dataStream<<infoHeader.biClrImportant; //本位图中重要的色彩数
if(infoHeader.biBitCount != 24){
//write rgbquad
for(unsigned int nCounti=0; nCounti<infoHeader.biClrUsed; nCounti++){
dataStream<<(*(rgbQuad + nCounti)).rgbBlue;
dataStream<<(*(rgbQuad + nCounti)).rgbGreen;
dataStream<<(*(rgbQuad + nCounti)).rgbRed;
dataStream<<(*(rgbQuad + nCounti)).rgbReserved;
}
}
//write image data
int cnt = 0;
int alignByte = (4 - (int)infoHeader.biWidth % 4) % 4;
IMAGEDATA align;
align.blue = 0;
align.green = 0;
align.red = 0;
switch (infoHeader.biBitCount) {
case 8:
for(int i = 0; i < infoHeader.biHeight; i++){
for(int j = 0; j < infoHeader.biWidth; j++){
dataStream<<(imgData+cnt)->blue;
cnt++;
}
if(alignByte != 0){
for(int k = 0; k < alignByte; k++){
dataStream<<align.blue;
}
}
}
break;
case 24:
for(int i = 0; i < infoHeader.biHeight; i++){
for(int j = 0; j < infoHeader.biWidth; j++){
dataStream<<(imgData+cnt)->blue;
dataStream<<(imgData+cnt)->green;
dataStream<<(imgData+cnt)->red;
cnt++;
}
if(alignByte != 0){
for(int k = 0; k < alignByte; k++){
dataStream<<align.blue;
}
}
}
break;
}
newImg.close();
return true;
}
现在我们有了所有的模块,可以将他们拼在一起了。右键添加Save to new BMP file的槽函数,先图片判空,再地址判空,最后写图片:
void PSMainWindow::on_actionSave_to_new_BMP_file_triggered()
{
if(image.isEmpty()){
QMessageBox::information(this, tr("warning"), tr("Please open an image first."));
return;
}
QString path = QFileDialog::getSaveFileName(this, tr("Save Image"), " ", tr("Image Files(*.bmp)"));
if(!path.isNull()){
image.saveImage(path);
}
else{
QMessageBox::information(this, tr("Path"), tr("You didn't input a file name."));
}
}
保存功能完成。
3 Display file header
首先设计显示文件头信息的窗口headerInfoDialog
。在普通的Dialog中加入一个List Widget控件和一个按钮,用于显示文件头信息。
在headerInfoDialog
类中添加显示信息的接口:
void HeaderInfoDialog::setInfo(BITMAPFILEHEADER fileHeader, BITMAPINFOHEADER infoHeader)
按要求的格式,将说明和值拼接成字符串,调用List Widget控件的setItem函数即可添加信息。每次展示信息时,先将控件内容清空,再依次添加信息。有一些需要用到数学计算的信息,计算函数可参考Qt下常用的数值计算(绝对值qAbs,最大qMax,最小qMin,开根号Sqrt,N次方是pow,断言宏Q_ASSERT和Q_ASSERT_X )。
void HeaderInfoDialog::setInfo(BITMAPFILEHEADER fileHeader, BITMAPINFOHEADER infoHeader)
{
ui->listWidget->clear();
QString line;
line = "btType (file type) = " + QString::number(fileHeader.bfType);
ui->listWidget->addItem(line);
line = "bfSize (file length) = " + QString::number(fileHeader.bfSize);
ui->listWidget->addItem(line);
line = "bfOffBits (offset of bit map data in bytes) = " + QString::number(fileHeader.bfOffBits);
ui->listWidget->addItem(line);
line = "biSize (header structure length shoud be 40 or 0x28) = " + QString::number(infoHeader.biSize);
ui->listWidget->addItem(line);
line = "biWidth (image width) = " + QString::number(infoHeader.biWidth);
ui->listWidget->addItem(line);
line = "biHeight (image height) = " + QString::number(infoHeader.biHeight);
ui->listWidget->addItem(line);
line = "biPlanes (must be eaual to 1) = " + QString::number(infoHeader.biPlanes);
ui->listWidget->addItem(line);
line = "biBitCount (color/pixel bits) = " + QString::number(infoHeader.biBitCount);
ui->listWidget->addItem(line);
line = "biCompression (compressed?) = " + QString::number(infoHeader.biCompression);
ui->listWidget->addItem(line);
line = "biSizeImage (length of bit map data in bytes must be the times of 4) = " + QString::number(infoHeader.biSizeImage);
ui->listWidget->addItem(line);
line = "biXPelsPerMeter (horizontal resolution of target device in pixels/metre) = " + QString::number(infoHeader.biXPelsPerMeter);
ui->listWidget->addItem(line);
line = "biYpelsPerMeter (vertical resolution of target device in pixels/metre) = " + QString::number(infoHeader.biYPelsPerMeter);
ui->listWidget->addItem(line);
line = "biColorUsed (number of colors used in bitmap, 0 = 2**biBitCount) = " + QString::number(infoHeader.biClrUsed);
ui->listWidget->addItem(line);
line = "biColorImportant (number of important colors, 0 = all colors are impretant) = " + QString::number(infoHeader.biClrImportant);
ui->listWidget->addItem(line);
line = "";
ui->listWidget->addItem(line);
line = "The following is additional information:";
ui->listWidget->addItem(line);
line = "Bytes per row in bitmap (nBytesPerRow) = " + QString::number(infoHeader.biWidth * (infoHeader.biBitCount/8));
ui->listWidget->addItem(line);
line = "Total bytes of bitmap (nImageSizeInByte) = " + QString::number(fileHeader.bfSize - fileHeader.bfOffBits);
ui->listWidget->addItem(line);
line = "Actual pixels per row in bitmap (nPixelsPerRpe)= " + QString::number(infoHeader.biWidth);
ui->listWidget->addItem(line);
line = "Total rows of bitmap (nTotalRows) = " + QString::number(infoHeader.biHeight);
ui->listWidget->addItem(line);
line = "Total colors (2**biBitCount)(nTotalColors) = " + QString::number(pow(2, infoHeader.biBitCount));
ui->listWidget->addItem(line);
line = "Used colors (biColorUsed)(nUsedolors) = " + QString::number(infoHeader.biClrUsed);
ui->listWidget->addItem(line);
}
为了将信息传到头信息展示窗口中,我们还需要获取图片的fileHeader
和infoHeader
这两个私有成员。在BMPIMG
类中添加相应接口:
BITMAPFILEHEADER BMPIMG::getFileHeader()
{
return fileHeader;
}
BITMAPINFOHEADER BMPIMG::getInfoHeader()
{
return infoHeader;
}
接下来编写Display file header槽函数中的逻辑。先定义一个新的信息展示窗口,获取文件头信息和信息头信息,设置展示内容,最后调用show()函数运行窗口即可。
这里需要注意的是,如果直接定义窗口、调用show()函数,窗口运行之后会马上自动退出,解决方法见:关于窗口闪退的解决。我采取了楼主所说的第一种方法:
将dlg作为类的成员,而不是在函数内部。
在psmainwindow.h中添加展示窗口的定义:
//psmainwindow.h
#ifndef PSMAINWINDOW_H
#define PSMAINWINDOW_H
//头文件略
namespace Ui {
class PSMainWindow;
}
class PSMainWindow : public QMainWindow
{
Q_OBJECT
//前略
public:
explicit PSMainWindow(QWidget *parent = nullptr);
~PSMainWindow();
private:
HeaderInfoDialog headerInfoDialog;
};
#endif // PSMAINWINDOW_H
右键添加槽函数:
void PSMainWindow::on_actionDisplay_file_header_triggered()
{
if(image.isEmpty()){
QMessageBox::information(this, tr("warning"), tr("You didn't open any image, please open an image first."));
return;
}
BITMAPFILEHEADER fileHeader = image.getFileHeader();
BITMAPINFOHEADER infoHeader = image.getInfoHeader();
headerInfoDialog.setInfo(fileHeader, infoHeader);
headerInfoDialog.show();
}
4 Get pixel value
功能要求:取某个位置像素的颜色值,并显示出来。
先实现获取颜色值的逻辑。
首先要实现定位读取IMAGEDATA
。在BMPIMG
类中添加:
IMAGEDATA BMPIMG::getPixelData(int x, int y)
{
IMAGEDATA pix = *(imgData + y * infoHeader.biWidth + x);
return pix;
}
直接返回特定位置的IMAGEDATA
信息。接下来判断biBitCount
,分情况解析颜色信息:
QColor BMPIMG::getPixel(int x, int y)
{
IMAGEDATA pix = getPixelData(x, y);
QColor color;
switch(infoHeader.biBitCount){
case 8:
color.setRed((rgbQuad + pix.blue)->rgbRed);
color.setBlue((rgbQuad + pix.blue)->rgbBlue);
color.setGreen((rgbQuad + pix.blue)->rgbGreen);
break;
case 24:
color.setRed(pix.red);
color.setBlue(pix.blue);
color.setGreen(pix.green);
break;
}
return color;
}
设计获取坐标的position
窗口。新建一个Dialog
,在对话框中添加两组标签和Spin Box(整型数输入框),和一个OK按钮。
此处我们需要将子窗口position
中获取的值传回父窗口psmainwindow
,传值方法可以参考Qt窗体之间相互传值的三种方式。这里我采用了信号和槽的方法。
编辑position
类的头文件position.h,定义信号函数:
#ifndef POSITION_H
#define POSITION_H
#include <QDialog>
namespace Ui {
class position;
}
class position : public QDialog
{
Q_OBJECT
//前略
signals:
void send_position(QPoint p);
};
#endif // POSITION_H
定义好之信号后只需emit send_position(p);
即可发射信号。
编辑psmainwindow
类,定义对应的槽函数用于接收信号,并定义QPoint p
成员用于存储接收到的坐标:
#ifndef PSMAINWINDOW_H
#define PSMAINWINDOW_H
//头文件略
namespace Ui {
class PSMainWindow;
}
class PSMainWindow : public QMainWindow
{
Q_OBJECT
//前略
private slots:
void receivePosition(QPoint p);
private:
QPoint p;
};
#endif // PSMAINWINDOW_H
在position
的设计界面右键点击OK按钮添加槽函数,获取坐标并发射信号:
void position::on_pushButton_clicked()
{
int x = ui->xInput->value();
int y = ui->yInput->value();
QPoint p(x,y);
emit send_position(p);
this->close();
}
编辑psmainwindow.cpp,完成槽函数void receivePosition(QPoint p)
:
void PSMainWindow::receivePosition(QPoint p)
{
this->p = p;
}
接下来设计展示颜色的窗口colorDisplay
。用Label
来展示颜色值,再添加一个小块的GraphicsView
来直观地显示颜色:
定义接口void colorDisplay::setInfo(QColor color)
来设置展示的颜色:
void colorDisplay::setInfo(QColor color){
ui->Red->setText(QString::number(color.red()));
ui->Green->setText(QString::number(color.green()));
ui->Blue->setText(QString::number(color.blue()));
QImage *colorBlock = new QImage(40, 40, QImage::Format_A2BGR30_Premultiplied);
colorBlock->fill(color);
QGraphicsScene *scene = new QGraphicsScene();
scene->addPixmap(QPixmap::fromImage(*colorBlock));
ui->graphicsView->setScene(scene);
}
之后我们就可以写Get pixel value本体了。在设计界面添加Get pixel value的槽函数:
void PSMainWindow::on_actionGet_pixel_value_triggered()
{
if(image.isEmpty()){
QMessageBox::information(this, tr("warning"), tr("Please open an image first."));
return;
}
position *dlg = new position(this);
connect(dlg, SIGNAL(send_position(QPoint)), this, SLOT(receivePosition(QPoint)));
dlg->setMax(image.getInfoHeader().biWidth, image.getInfoHeader().biHeight);
dlg->exec();
QColor pix = image.getPixel(p.x(), p.y());
colorDisplay *displayDialog = new colorDisplay();
displayDialog->setInfo(pix);
displayDialog->show();
}
这里有一点需要注意的是,Dialog::shwo()
函数是展示子窗口之后,父窗口还会继续运行;而Dialog::exec()
函数是等待子窗口运行结束(即关闭)之后,父窗口才会继续运行。我们需要获取到坐标之后才能读取像素值,所以获取坐标的窗口position *dlg
要用exec()
运行,而父窗口不依赖展示窗口colorDisplay *displayDialog
,直接调用show()
就可以。
5 Set pixel value
功能要求:设置某个位置像素的颜色值,并显示出来。
在BMPIMG
类中添加void setPixel(int r, int b, int g, int x, int y)
函数:
void BMPIMG::setPixel(int r, int b, int g, int x, int y)
{
if(infoHeader.biBitCount == 8){
BYTE color = getColor(r,b,g);
(imgData + y * infoHeader.biWidth + x)->blue = color;
}
else{
(imgData + y * infoHeader.biWidth + x)->blue = b;
(imgData + y * infoHeader.biWidth + x)->green = g;
(imgData + y * infoHeader.biWidth + x)->red = r;
}
return;
}
这里有一个败笔:我之前写的getPixelData(int x, int y)
函数返回值不是指针,所以当需要修改像素颜色值时,这个函数无法被复用。