1. 程式人生 > >Qt Quick 影象處理例項之美圖秀秀 附原始碼下載

Qt Quick 影象處理例項之美圖秀秀 附原始碼下載

               

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

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

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

例項效果

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

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

                  圖 1 初始效果

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

  

               圖 2 柔化特效

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

                   圖 3 灰度特效

    圖 4 是浮雕特效:

             圖 4 浮雕特效

    圖 5 是黑白特效:

                圖 5 黑白特效

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

                  圖 6 底片特效

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

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

                  圖 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 程式碼實現已經介紹完畢,您看得累嗎?累就對了,舒服是留給死人的。擦,睡著了,我敲打……

原始碼情景分析

    上一節介紹了影象特效演算法,現在我們先看應用與管理這些特效的 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.2import QtQuick.Controls 1.1import QtQuick.Dialogs 1.1import an.qt.ImageProcessor 1.0import QtQuick.Controls.Styles 1.1Rectangle {    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 會更新……

    好啦好啦,我們的影象處理例項就到此為止了。秒懂?

    例項專案及原始碼下載:點這裡點這裡。需要一點積分啊親。

    回顧一下吧:

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

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