美文网首页
从零开始做一个简单的数字图像处理系统

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

作者: 共醉明月Nessa | 来源:发表于2019-02-09 09:29 被阅读0次

    此文章为数组图像处理课程设计的笔记。项目我放在了我的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)函数返回值不是指针,所以当需要修改像素颜色值时,这个函数无法被复用。

    相关文章

      网友评论

          本文标题:从零开始做一个简单的数字图像处理系统

          本文链接:https://www.haomeiwen.com/subject/gnnoaftx.html