天天看點

Qt Quick 圖像處理執行個體之美圖秀秀(附源碼下載下傳)執行個體效果圖像處理算法源碼情景分析

    在《Qt Quick 之 QML 與 C++ 混合程式設計詳解》一文中我們講解了 QML 與 C++ 混合程式設計的方方面面的内容,這次我們通過一個圖像處理應用,再來看一下 QML 與 C++ 混合程式設計的威力,同時也為諸君揭開美圖秀秀、魔拍之類的相片美化應用的底層原理。

    項目的建立過程請參考《Qt Quick 之 Hello World 圖文詳解》,項目名稱為 imageProcessor ,建立完成後需要添加兩個檔案: imageProcessor.h 和 imageProcessor.cpp 。

    本文是作者 Qt Quick 系列文章中的一篇,其它文章在這裡:

  • Qt Quick 簡介
  • QML 語言基礎
  • Qt Quick 之 Hello World 圖文詳解
  • Qt Quick 簡單教程
  • Qt Quick 事件處理之信号與槽
  • Qt Quick事件處理之滑鼠、鍵盤、定時器
  • Qt Quick 事件處理之捏拉縮放與旋轉
  • Qt Quick 元件與對象動态建立詳解
  • Qt Quick 布局介紹
  • Qt Quick 之 QML 與 C++ 混合程式設計詳解

執行個體效果

    先看一下示例的實際運作效果,然後我們再來展開。

    圖 1 是在電腦上打開一個圖檔後的初始效果:

Qt Quick 圖像處理執行個體之美圖秀秀(附源碼下載下傳)執行個體效果圖像處理算法源碼情景分析

                  圖 1 初始效果

    圖 2 是應用柔化特效後的效果:

Qt Quick 圖像處理執行個體之美圖秀秀(附源碼下載下傳)執行個體效果圖像處理算法源碼情景分析

               圖 2 柔化特效

    圖 3 是應用灰階特效後的截圖:

Qt Quick 圖像處理執行個體之美圖秀秀(附源碼下載下傳)執行個體效果圖像處理算法源碼情景分析

                   圖 3 灰階特效

    圖 4 是浮雕特效:

Qt Quick 圖像處理執行個體之美圖秀秀(附源碼下載下傳)執行個體效果圖像處理算法源碼情景分析

             圖 4 浮雕特效

    圖 5 是黑白特效:

Qt Quick 圖像處理執行個體之美圖秀秀(附源碼下載下傳)執行個體效果圖像處理算法源碼情景分析

                圖 5 黑白特效

    圖 6 是應用底片特效後的截圖:

Qt Quick 圖像處理執行個體之美圖秀秀(附源碼下載下傳)執行個體效果圖像處理算法源碼情景分析

                  圖 6 底片特效

    如果你注意到我部落格的頭像……嗯,木錯,它就是我使用本文執行個體的底片特效做出來的。

    圖 7 是應用銳化特效後的截圖:

Qt Quick 圖像處理執行個體之美圖秀秀(附源碼下載下傳)執行個體效果圖像處理算法源碼情景分析

                  圖 7 銳化特效

    特效展示完畢,那麼它們是怎麼實作的呢?這就要說到圖像處理算法了。

圖像處理算法

    imageProcessor 執行個體提供了"柔化"、"灰階"、"浮雕"、"黑白"、"底片"、"銳化"六種圖像效果。算法的實作在 imageProcessor.h / imageProcessor.cpp 兩個檔案中,我們先簡介每種效果對應的算法,然後看代碼實作。

柔化

    柔化又稱模糊,圖像模糊算法有很多種,我們最常見的就是均值模糊,即取一定半徑内的像素值之平均值作為目前點的新的像素值。

    為了提高計算速度,我們取 3 為半徑,就是針對每一個像素,将周圍 8 個點加上自身的 RGB 值的平均值作為像素新的顔色值置。代碼如下:

static void _soften(QString sourceFile, QString destFile)
{
    QImage image(sourceFile);
    if(image.isNull())
    {
        qDebug() << "load " << sourceFile << " failed! ";
        return;
    }
    int width = image.width();
    int height = image.height();
    int r, g, b;
    QRgb color;
    int xLimit = width - 1;
    int yLimit = height - 1;
    for(int i = 1; i < xLimit; i++)
    {
        for(int j = 1; j < yLimit; j++)
        {
            r = 0;
            g = 0;
            b = 0;
            for(int m = 0; m < 9; m++)
            {
                int s = 0;
                int p = 0;
                switch(m)
                {
                case 0:
                    s = i - 1;
                    p = j - 1;
                    break;
                case 1:
                    s = i;
                    p = j - 1;
                    break;
                case 2:
                    s = i + 1;
                    p = j - 1;
                    break;
                case 3:
                    s = i + 1;
                    p = j;
                    break;
                case 4:
                    s = i + 1;
                    p = j + 1;
                    break;
                case 5:
                    s = i;
                    p = j + 1;
                    break;
                case 6:
                    s = i - 1;
                    p = j + 1;
                    break;
                case 7:
                    s = i - 1;
                    p = j;
                    break;
                case 8:
                    s = i;
                    p = j;
                }
                color = image.pixel(s, p);
                r += qRed(color);
                g += qGreen(color);
                b += qBlue(color);
            }

            r = (int) (r / 9.0);
            g = (int) (g / 9.0);
            b = (int) (b / 9.0);

            r = qMin(255, qMax(0, r));
            g = qMin(255, qMax(0, g));
            b = qMin(255, qMax(0, b));

            image.setPixel(i, j, qRgb(r, g, b));
        }
    }

    image.save(destFile);
}
           

    這樣處理的效果不是特别明顯,采用高斯模糊算法可以擷取更好的效果。

灰階

    把圖像變灰,大概有這麼三種方法:

  1. 最大值法,即 R = G = B = max(R , G , B),這種方法處理過的圖檔亮度偏高
  2. 平均值法,即 R = G = B = (R + G + B) / 3 ,這種方法處理過的圖檔比較柔和
  3. 權重平均值法,即 R = G = B = R*Wr + G*Wg + B*Wb ,因為人眼對不同顔色的敏感度不一樣,三種顔色權重也不一樣,一般來說綠色最高,紅色次之,藍色最低。這種方法最合理的取值,紅、綠、藍的權重依次是 0.299 、0.587 、 0.114 。為了避免浮點運算,可以用移位替代。

    Qt 架構有一個 qGray() 函數,采取權重平均值法計算灰階。 qGray() 将浮點運算轉為整型的乘法和除法,公式是  (r * 11 + g * 16 + b * 5)/32 ,沒有使用移位運算。

    我使用 qGray() 函數計算灰階,下面是代碼:

static void _gray(QString sourceFile, QString destFile)
{
    QImage image(sourceFile);
    if(image.isNull())
    {
        qDebug() << "load " << sourceFile << " failed! ";
        return;
    }
    qDebug() << "depth - " << image.depth();

    int width = image.width();
    int height = image.height();
    QRgb color;
    int gray;
    for(int i = 0; i < width; i++)
    {
        for(int j= 0; j < height; j++)
        {
            color = image.pixel(i, j);
            gray = qGray(color);
            image.setPixel(i, j, 
                   qRgba(gray, gray, gray, qAlpha(color)));
        }
    }

    image.save(destFile);
}
           

    qGray() 計算灰階時忽略了 Alpha 值,我在實作時保留原有的 Alpha 值。

浮雕

    "浮雕" 圖象效果是指圖像的前景前向凸出背景。

    浮雕的算法相對複雜一些,用目前點的 RGB 值減去相鄰點的 RGB 值并加上 128 作為新的 RGB 值。由于圖檔中相鄰點的顔色值是比較接近的,是以這樣的算法處理之後,隻有顔色的邊沿區域,也就是相鄰顔色差異較大的部分的結果才會比較明顯,而其他平滑區域則值都接近128左右,也就是灰色,這樣就具有了浮雕效果。

    看代碼:

static void _emboss(QString sourceFile, QString destFile)
{
    QImage image(sourceFile);
    if(image.isNull())
    {
        qDebug() << "load " << sourceFile << " failed! ";
        return;
    }
    int width = image.width();
    int height = image.height();
    QRgb color;
    QRgb preColor = 0;
    QRgb newColor;
    int gray, r, g, b, a;
    for(int i = 0; i < width; i++)
    {
        for(int j= 0; j < height; j++)
        {
            color = image.pixel(i, j);
            r = qRed(color) - qRed(preColor) + 128;
            g = qGreen(color) - qGreen(preColor) + 128;
            b = qBlue(color) - qBlue(preColor) + 128;
            a = qAlpha(color);
            gray = qGray(r, g, b);
            newColor = qRgba(gray, gray, gray, a);
            image.setPixel(i, j, newColor);
            preColor = newColor;
        }
    }
    image.save(destFile);
}
           

    在實作 _emboss() 函數時 ,為避免有些區域殘留“彩色”雜點或者條狀痕迹,我對新的 RGB 值又做了一次灰階處理。

黑白

    黑白圖檔的處理算法比較簡單:對一個像素的 R 、G 、B 求平均值,average = (R + G + B) / 3 ,如果 average 大于等于標明的門檻值則将該像素置為白色,小于門檻值就把像素置為黑色。

    示例中我選擇的門檻值是 128 ,也可以是其它值,根據效果調整即可。比如你媳婦兒高圓圓嫌給她拍的照片黑白處理後黑多白少,那可以把門檻值調低一些,取 80 ,效果肯定就變了。下面是代碼:

static void _binarize(QString sourceFile, QString destFile)
{
    QImage image(sourceFile);
    if(image.isNull())
    {
        qDebug() << "load " << sourceFile << " failed! ";
        return;
    }
    int width = image.width();
    int height = image.height();
    QRgb color;
    QRgb avg;
    QRgb black = qRgb(0, 0, 0);
    QRgb white = qRgb(255, 255, 255);
    for(int i = 0; i < width; i++)
    {
        for(int j= 0; j < height; j++)
        {
            color = image.pixel(i, j);
            avg = (qRed(color) + qGreen(color) + qBlue(color))/3;
            image.setPixel(i, j, avg >= 128 ? white : black);
        }
    }
    image.save(destFile);
}
           

    代碼的邏輯簡單,從檔案加載圖檔,生成一個 QImage 執行個體,然後應用算法,處理後的圖檔儲存到指定位置。

底片

    早些年的相機使用膠卷記錄拍攝結果,洗照片比較麻煩,不過如果你拿到底片,逆光去看,效果就很特别。

    底片算法其實很簡單,取 255 與像素的 R 、 G、 B 分量之差作為新的 R、 G、 B 值。實作代碼:

static void _negative(QString sourceFile, QString destFile)
{
    QImage image(sourceFile);
    if(image.isNull())
    {
        qDebug() << "load " << sourceFile << " failed! ";
        return;
    }
    int width = image.width();
    int height = image.height();
    QRgb color;
    QRgb negative;
    for(int i = 0; i < width; i++)
    {
        for(int j= 0; j < height; j++)
        {
            color = image.pixel(i, j);
            negative = qRgba(255 - qRed(color),
                             255 - qGreen(color),
                             255 - qBlue(color),
                             qAlpha(color));
            image.setPixel(i, j, negative);
        }
    }
    image.save(destFile);
}
           

銳化

    圖像銳化的主要目的是增強圖像邊緣,使模糊的圖像變得更加清晰,顔色變得鮮明突出,圖像的品質有所改善,産生更适合人眼觀察和識别的圖像。

    常見的銳化算法有微分法和高通濾波法。微分法又以梯度銳化和拉普拉斯銳化較為常用。本示例采用微分法中的梯度銳化,用差分近似微分,則圖像在點(i,j)處的梯度幅度計算公式如下:

G[f(i,j)] = abs(f(i,j) - f(i+1,j)) + abs(f(i,j) - f(i,j+1))
           

    為了更好的增強圖像邊緣,我們引入一個門檻值,隻有像素點的梯度值大于門檻值時才對該像素點進行銳化,将像素點的 R 、 G、 B 值設定為對應的梯度值與一個常數之和。常數值的選取應當參考圖像的具體特點。我們的示例為簡單起見,将常數設定為 100 ,梯度門檻值取 80 ,寫死在算法函數中,更好的做法是通過參數傳入,以便客戶程式可以調整這些變量來觀察效果。

    好啦,看代碼:

static void _sharpen(QString sourceFile, QString destFile)
{
    QImage image(sourceFile);
    if(image.isNull())
    {
        qDebug() << "load " << sourceFile << " failed! ";
        return;
    }
    int width = image.width();
    int height = image.height();
    int threshold = 80;
    QImage sharpen(width, height, QImage::Format_ARGB32);
    int r, g, b, gradientR, gradientG, gradientB;
    QRgb rgb00, rgb01, rgb10;
    for(int i = 0; i < width; i++)
    {
        for(int j= 0; j < height; j++)
        {
            if(image.valid(i, j) &&
                    image.valid(i+1, j) &&
                    image.valid(i, j+1))
            {
                rgb00 = image.pixel(i, j);
                rgb01 = image.pixel(i, j+1);
                rgb10 = image.pixel(i+1, j);
                r = qRed(rgb00);
                g = qGreen(rgb00);
                b = qBlue(rgb00);
                gradientR = abs(r - qRed(rgb01)) + abs(r - qRed(rgb10));
                gradientG = abs(g - qGreen(rgb01)) + 
                                               abs(g - qGreen(rgb10));
                gradientB = abs(b - qBlue(rgb01)) + 
                                               abs(b - qBlue(rgb10));

                if(gradientR > threshold)
                {
                    r = qMin(gradientR + 100, 255);
                }

                if(gradientG > threshold)
                {
                    g = qMin( gradientG + 100, 255);
                }

                if(gradientB > threshold)
                {
                    b = qMin( gradientB + 100, 255);
                }

                sharpen.setPixel(i, j, qRgb(r, g, b));
            }
        }
    }

    sharpen.save(destFile);
}
           

    示例用到的圖像處理算法和 Qt 代碼實作已經介紹完畢,您看得累嗎?累就對了,舒服是留給死人的。擦,睡着了,我

Qt Quick 圖像處理執行個體之美圖秀秀(附源碼下載下傳)執行個體效果圖像處理算法源碼情景分析

……

源碼情景分析

    上一節介紹了圖像特效算法,現在我們先看應用與管理這些特效的 C++ 類 ImageProcessor ,然後再來看 QML 代碼。

ImageProcessor

    在設計 ImageProcessor 類時,我希望它能夠在 QML 環境中使用,是以實用了信号、槽、 Q_ENUMS 、 Q_PROPERTY 等特性,感興趣的話請參考《Qt Quick 之 QML 與 C++ 混合程式設計詳解》進一步熟悉。

    先看 imageProcessor.h :

#ifndef IMAGEPROCESSOR_H
#define IMAGEPROCESSOR_H
#include <QObject>
#include <QString>

class ImageProcessorPrivate;
class ImageProcessor : public QObject
{
    Q_OBJECT
    Q_ENUMS(ImageAlgorithm)
    Q_PROPERTY(QString sourceFile READ sourceFile)
    Q_PROPERTY(ImageAlgorithm algorithm READ algorithm)

public:
    ImageProcessor(QObject *parent = 0);
    ~ImageProcessor();

    enum ImageAlgorithm{
        Gray = 0,
        Binarize,
        Negative,
        Emboss,
        Sharpen,
        Soften,
        AlgorithmCount
    };

    QString sourceFile() const;
    ImageAlgorithm algorithm() const;
    void setTempPath(QString tempPath);

signals:
    void finished(QString newFile);
    void progress(int value);

public slots:
    void process(QString file, ImageAlgorithm algorithm);
    void abort(QString file, ImageAlgorithm algorithm);
    void abortAll();

private:
    ImageProcessorPrivate *m_d;
};

#endif
           

    ImageProcessor 類的聲明比較簡單,它通過 finished() 信号通知關注者圖像處理完畢,提供 process() 方法供客戶程式調用,還有 setTempPath() 設定臨時目錄,也允許你取消待執行的任務……

    下面是實作檔案 imageProcessor.cpp :

#include "imageProcessor.h"
#include <QThreadPool>
#include <QList>
#include <QFile>
#include <QFileInfo>
#include <QRunnable>
#include <QEvent>
#include <QCoreApplication>
#include <QPointer>
#include <QUrl>
#include <QImage>
#include <QDebug>
#include <QDir>

typedef void (*AlgorithmFunction)(QString sourceFile, 
                                              QString destFile);

class AlgorithmRunnable;
class ExcutedEvent : public QEvent
{
public:
    ExcutedEvent(AlgorithmRunnable *r)
        : QEvent(evType()), m_runnable(r)
    {
    }
    AlgorithmRunnable *m_runnable;

    static QEvent::Type evType()
    {
        if(s_evType == QEvent::None)
        {
            s_evType = (QEvent::Type)registerEventType();
        }
        return s_evType;
    }

private:
    static QEvent::Type s_evType;
};
QEvent::Type ExcutedEvent::s_evType = QEvent::None;

static void _gray(QString sourceFile, QString destFile);
static void _binarize(QString sourceFile, QString destFile);
static void _negative(QString sourceFile, QString destFile);
static void _emboss(QString sourceFile, QString destFile);
static void _sharpen(QString sourceFile, QString destFile);
static void _soften(QString sourceFile, QString destFile);

static AlgorithmFunction g_functions[ImageProcessor::AlgorithmCount] = {
    _gray,
    _binarize,
    _negative,
    _emboss,
    _sharpen,
    _soften
};

class AlgorithmRunnable : public QRunnable
{
public:
    AlgorithmRunnable(
            QString sourceFile,
            QString destFile,
            ImageProcessor::ImageAlgorithm algorithm,
            QObject * observer)
        : m_observer(observer)
        , m_sourceFilePath(sourceFile)
        , m_destFilePath(destFile)
        , m_algorithm(algorithm)
    {
    }
    ~AlgorithmRunnable(){}


    void run()
    {
        g_functions[m_algorithm](m_sourceFilePath, m_destFilePath);
        QCoreApplication::postEvent(m_observer, 
                                               new ExcutedEvent(this));
    }

    QPointer<QObject> m_observer;
    QString m_sourceFilePath;
    QString m_destFilePath;
    ImageProcessor::ImageAlgorithm m_algorithm;
};

class ImageProcessorPrivate : public QObject
{
public:
    ImageProcessorPrivate(ImageProcessor *processor)
        : QObject(processor), m_processor(processor),
          m_tempPath(QDir::currentPath())
    {
        ExcutedEvent::evType();
    }
    ~ImageProcessorPrivate()
    {
    }

    bool event(QEvent * e)
    {
        if(e->type() == ExcutedEvent::evType())
        {
            ExcutedEvent *ee = (ExcutedEvent*)e;
            if(m_runnables.contains(ee->m_runnable))
            {
                m_notifiedAlgorithm = ee->m_runnable->m_algorithm;
                m_notifiedSourceFile = 
                              ee->m_runnable->m_sourceFilePath;
        emit m_processor->finished(ee->m_runnable->m_destFilePath);
                m_runnables.removeOne(ee->m_runnable);
            }
            delete ee->m_runnable;
            return true;
        }
        return QObject::event(e);
    }

    void process(QString sourceFile, ImageProcessor::ImageAlgorithm algorithm)
    {
        QFileInfo fi(sourceFile);
        QString destFile = QString("%1/%2_%3").arg(m_tempPath)
                .arg((int)algorithm).arg(fi.fileName());
        AlgorithmRunnable *r = new AlgorithmRunnable(sourceFile,
                                           destFile, algorithm, this);
        m_runnables.append(r);
        r->setAutoDelete(false);
        QThreadPool::globalInstance()->start(r);
    }

    ImageProcessor * m_processor;
    QList<AlgorithmRunnable*> m_runnables;
    QString m_notifiedSourceFile;
    ImageProcessor::ImageAlgorithm m_notifiedAlgorithm;
    QString m_tempPath;
};

ImageProcessor::ImageProcessor(QObject *parent)
    : QObject(parent)
    , m_d(new ImageProcessorPrivate(this))
{}
ImageProcessor::~ImageProcessor()
{
    delete m_d;
}

QString ImageProcessor::sourceFile() const
{
    return m_d->m_notifiedSourceFile;
}

ImageProcessor::ImageAlgorithm ImageProcessor::algorithm() const
{
    return m_d->m_notifiedAlgorithm;
}

void ImageProcessor::setTempPath(QString tempPath)
{
    m_d->m_tempPath = tempPath;
}

void ImageProcessor::process(QString file, ImageAlgorithm algorithm)
{
    m_d->process(file, algorithm);
}

void ImageProcessor::abort(QString file, ImageAlgorithm algorithm)
{
    int size = m_d->m_runnables.size();
    AlgorithmRunnable *r;
    for(int i = 0; i < size; i++)
    {
        r = m_d->m_runnables.at(i);
        if(r->m_sourceFilePath == file && r->m_algorithm == algorithm)
        {
            m_d->m_runnables.removeAt(i);
            break;
        }
    }
}
           

    為避免阻塞 UI 線程,我把圖像處理部分放到線程池内完成,根據 QThreadPool 的要求,從 QRunnable 繼承,實作了 AlgorithmRunnable ,當 run() 函數執行完時發送自定義的 ExecutedEvent 給 ImageProcessor ,而 ImageProcessor 就在處理事件時發出 finished() 信号。關于 QThreadPool 和自定義事件,請參考 Qt 幫助了解詳情。

    算法函數放在一個全局的函數指針數組中, AlgorithmRunnable 則根據算法枚舉值從數組中取出相應的函數來處理圖像。

    其它的代碼一看即可明白,不再多說。

    要想在 QML 中實用 ImageProcessor 類,需要導出一個 QML 類型。這個工作是在 main() 函數中完成的。

main() 函數

    main() 函數就在 main.cpp 中,下面是 main.cpp 的全部代碼:

#include <QApplication>
#include "qtquick2applicationviewer.h"
#include <QtQml>
#include "imageProcessor.h"
#include <QQuickItem>
#include <QDebug>

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    qmlRegisterType<ImageProcessor>("an.qt.ImageProcessor", 1, 0,"ImageProcessor");

    QtQuick2ApplicationViewer viewer;
    viewer.rootContext()->setContextProperty("imageProcessor", new ImageProcessor);

    viewer.setMainQmlFile(QStringLiteral("qml/imageProcessor/main.qml"));
    viewer.showExpanded();

    return app.exec();
}
           

    我使用 qmlRegisterType() 注冊了 ImageProcessor 類,包名是 an.qt.ImageProcessor ,版本是 1.0 ,是以你在稍後的 main.qml 文檔中可以看到下面的導入語句:

import an.qt.ImageProcessor 1.0
           

    上了賊船,就跟賊走,是時候看看 main.qml 了 。

main.qml

    main.qml 還是比較長的哈,有 194 行代碼:

import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Dialogs 1.1
import an.qt.ImageProcessor 1.0
import QtQuick.Controls.Styles 1.1

Rectangle {
    width: 640;
    height: 480;
    color: "#121212";

    BusyIndicator {
        id: busy;
        running: false;
        anchors.centerIn: parent;
        z: 2;
    }

    Label {
        id: stateLabel;
        visible: false;
        anchors.centerIn: parent;
    }

    Image {
        objectName: "imageViewer";
        id: imageViewer;
        asynchronous: true;
        anchors.fill: parent;
        fillMode: Image.PreserveAspectFit;
        onStatusChanged: {
            if (imageViewer.status === Image.Loading) {
                busy.running = true;
                stateLabel.visible = false;
            }
            else if(imageViewer.status === Image.Ready){
                busy.running = false;
            }
            else if(imageViewer.status === Image.Error){
                busy.running = false;
                stateLabel.visible = true;
                stateLabel.text = "ERROR";
            }
        }
    }

    ImageProcessor {
        id: processor;
        onFinished: {
            imageViewer.source = "file:///" +newFile;
        }
    }

    FileDialog {
        id: fileDialog;
        title: "Please choose a file";
        nameFilters: ["Image Files (*.jpg *.png *.gif)"];
        onAccepted: {
            console.log(fileDialog.fileUrl);
            imageViewer.source = fileDialog.fileUrl;
        }
    }

    Component{
        id: btnStyle;
        ButtonStyle {
            background: Rectangle {
                implicitWidth: 70
                implicitHeight: 25
                border.width: control.pressed ? 2 : 1
                border.color: (control.pressed || control.hovered) ? "#00A060" : "#888888"
                radius: 6
                gradient: Gradient {
                    GradientStop { position: 0 ; color: control.pressed ? "#cccccc" : "#e0e0e0" }
                    GradientStop { position: 1 ; color: control.pressed ? "#aaa" : "#ccc" }
                }
            }
        }
    }

    Button {
        id: openFile;
        text: "打開";
        anchors.left:  parent.left;
        anchors.leftMargin: 6;
        anchors.top: parent.top;
        anchors.topMargin: 6;
        onClicked: {
            fileDialog.visible = true;
        }
        style: btnStyle;
        z: 1;
    }

    Button {
        id: quit;
        text: "退出";
        anchors.left: openFile.right;
        anchors.leftMargin: 4;
        anchors.bottom: openFile.bottom;
        onClicked: {
            Qt.quit()
        }
        style: btnStyle;
        z: 1;
    }

    Rectangle {
        anchors.left: parent.left;
        anchors.top: parent.top;
        anchors.bottom: openFile.bottom;
        anchors.bottomMargin: -6;
        anchors.right: quit.right;
        anchors.rightMargin: -6;
        color: "#404040";
        opacity: 0.7;
    }
    Grid {
        id: op;
        anchors.left: parent.left;
        anchors.leftMargin: 4;
        anchors.bottom: parent.bottom;
        anchors.bottomMargin: 4;
        rows: 2;
        columns: 3;
        rowSpacing: 4;
        columnSpacing: 4;
        z: 1;

        Button {
            text: "柔化";
            style: btnStyle;
            onClicked: {
                busy.running = true;
                processor.process(fileDialog.fileUrl, ImageProcessor.Soften);
            }
        }

        Button {
            text: "灰階";
            style: btnStyle;
            onClicked: {
                busy.running = true;
                processor.process(fileDialog.fileUrl, ImageProcessor.Gray);
            }
        }

        Button {
            text: "浮雕";
            style: btnStyle;
            onClicked: {
                busy.running = true;
                processor.process(fileDialog.fileUrl, ImageProcessor.Emboss);
            }
        }
        Button {
            text: "黑白";
            style: btnStyle;
            onClicked: {
                busy.running = true;
                processor.process(fileDialog.fileUrl, ImageProcessor.Binarize);
            }
        }

        Button {
            text: "底片";
            style: btnStyle;
            onClicked: {
                busy.running = true;
                processor.process(fileDialog.fileUrl, ImageProcessor.Negative);
            }
        }

        Button {
            text: "銳化";
            style: btnStyle;
            onClicked: {
                busy.running = true;
                processor.process(fileDialog.fileUrl, ImageProcessor.Sharpen);
            }
        }
    }

    Rectangle {
        anchors.left: parent.left;
        anchors.top: op.top;
        anchors.topMargin: -4;
        anchors.bottom: parent.bottom;
        anchors.right: op.right;
        anchors.rightMargin: -4;
        color: "#404040";
        opacity: 0.7;
    }
}
           

    圖檔的顯示使用一個充滿視窗的 Image 對象,在 onStatusChanged 信号處理器中控制加載提示對象 BusyIndicator 是否顯示。我通過 Z 序來保證 busy 總是在 imageViewer 上面。

    你看到了,我像使用 QML 内建對象那樣使用了 ImageProcessor 對象,為它的 finished 信号定義了 onFinished 信号處理器,在信号處理器中把應用圖像特效後的中間檔案傳遞給 imageViewer 來顯示。

    界面布局比較簡陋,打開和退出兩個按鈕放在左上角,使用錨布局。關于錨布局,請參考《Qt Quick 布局介紹》或《Qt Quick 簡單教程》。圖像處理的 6 個按鈕使用 Grid 定位器來管理, 2 行 3 列,放在界面左下角。 Grid 定位器的使用請參考《Qt Quick 布局介紹》。

    關于圖像處理按鈕,以黑白特效做下說明,在 onClicked 信号處理器中,調用 processor 的 process() 方法,傳入本地圖檔路徑和特效算法。當特效運算異步完成後,就會觸發 finished 信号,進而 imageViewer 會更新……

    好啦好啦,我們的圖像處理執行個體就到此為止了。秒懂?

    執行個體項目及源代碼下載下傳:點這裡點這裡。需要一點積分啊親。

    回顧一下吧:

  • Qt Quick 簡介
  • QML 語言基礎
  • Qt Quick 之 Hello World 圖文詳解
  • Qt Quick 簡單教程
  • Qt Quick 事件處理之信号與槽
  • Qt Quick事件處理之滑鼠、鍵盤、定時器
  • Qt Quick 事件處理之捏拉縮放與旋轉
  • Qt Quick 元件與對象動态建立詳解
  • Qt Quick 布局介紹
  • Qt Quick 之 QML 與 C++ 混合程式設計詳解

    本文寫作過程中參考了下列文章,特此感謝:

  • winorlose2000 部落格( http://vaero.blog.51cto.com/ )中關于圖像處理算法的博文
  • ian 的個人部落格( http://www.icodelogic.com/ )中關于圖像處理算法的博文