从零开始做一个简单的数字图像处理系统

此文章为数组图像处理课程设计的笔记。项目我放在了我的GitHub上。这个系统要求用纯VC++实现以下功能:

  1. Open BMP file
    打开一个BMP文件,并在窗口中显示出来。

  2. Save to new BMP file
    将当前视图保存为一个新的BMP文件(先弹出一个对话框,输入一个BMP文件名)。

  3. Display file header
    按如下的格式显示文件头信息:


    头信息格式
  4. Get pixel value
    取某个位置像素的颜色值,并显示出来。

  5. Set pixel value
    设置某个位置像素的颜色值,并显示出来。

功能(4)和(5)所需的参数从对话框中获取。前面5个功能对灰度图像和彩色图像都适用,后面的功能仅要求针对灰度图像。

  1. Image interpolation
    图像缩放:x和y方向的缩放因子、插值算法选择(最邻近和双线性),从对话框中获取。需要将图像缩放的结果显示出来。
  2. Median filtering
    实现3x3的中值滤波,并将结果显示出来。
  3. Gaussian smoothing
    从对话框中获取高斯函数的均方差,对图像做高斯平滑,并将结果显示出来。

功能(9) ~ (13)是选做题。

  1. Histogram equalization
    直方图均衡化,并将结果显示出来。
  2. Sharpening by gradient
    实现基于梯度的图像锐化,所需参数从对话框中获取,将锐化结果显示出来。
  3. Bilateral filtering
    实现双边滤波,参数sigma_d和sigma_R从对话框中获取,并将结果显示出来。
  4. Add impulse noise
    在图像中加入脉冲噪声,噪声密度和类型从对话框中获取,将噪声图像显示出来。
  5. Canny edge detection
    实现Canny算子边缘检测,并将结果显示出来。

由于Qt把HWND和HDC这种平台相关的数据类型全都封装好了,没有提供直接的操作接口,我决定用Qt自己的绘图设备比如QImage实现这些功能。虽然用的数据类型是封装好的,但是图片读取、显示和处理算法都在像素级别处理。主要是MFC真的苦手啊。

以下分功能进行记录。


0 主界面和数据结构设计

本图像处理系统需要一个用户友好的可操作界面。我们可以用Qt自带的设计功能来实现这个界面。新建一个MainWindow工程,Qt会自动给你生成mainwindow.hmainwindow.cppmainwindow.ui。我在新建工程时,为了和以后可能新建的mianwindow窗口区分,将这个mainwindow类改名成了psmainwindow类。我们先打开mainwindow.ui,进行界面的设计。
设计过程中,为了能够进行良好的布局、且能够让所有控件随窗口大小改变而改变,我们需要用到Qt的布局管理器,具体参见这篇文章:

我做出的界面是这个样子的:


主界面

其中,每个菜单项下拉内容如下:


File菜单
ImageProcessing菜单

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;
    }
}

之后开始读取图片的其余头数据,即fileHeaderinfoHeader。要注意的是,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项之后就可以看到图片了。


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);
}

为了将信息传到头信息展示窗口中,我们还需要获取图片的fileHeaderinfoHeader这两个私有成员。在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窗口

此处我们需要将子窗口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来直观地显示颜色:

colorDisplay

定义接口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)函数返回值不是指针,所以当需要修改像素颜色值时,这个函数无法被复用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容

  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,331评论 0 17
  • 教程一:视频截图(Tutorial 01: Making Screencaps) 首先我们需要了解视频文件的一些基...
    90后的思维阅读 4,634评论 0 3
  • 1 实验目的 目前计算机视觉技术已经比较成熟,相关的开源项目与算法很多,可以将这些开源算法进行整合,进而做成一个小...
    YOUNG_FAN阅读 6,648评论 0 50
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,068评论 1 32
  • 我要到一个 春天有雨 冬天有雪 秋天有红叶的地方 在那里 读书 写字 和恋爱 说自己想说的话 做自己想做的事 ...
    Alice王志荣阅读 204评论 0 0