Qt Quick 图像处理实例之美图秀秀(附源代码下载)

时间:2022-06-27 08:12:58

在《Qt Quick 之 QML 与 C++ 混合编程具体解释》一文中我们解说了 QML 与 C++ 混合编程的方方面面的内容,这次我们通过一个图像处理应用。再来看一下 QML 与 C++ 混合编程的威力,同一时候也为诸君揭开美图秀秀、魔拍之类的相片美化应用的底层原理。

项目的创建过程请參考《Qt Quick 之 Hello World 图文具体解释》,项目名称为 imageProcessor ,创建完毕后须要加入两个文件: imageProcessor.h 和 imageProcessor.cpp 。

本文是作者 Qt Quick 系列文章中的一篇,其他文章在这里:

实例效果

先看一下演示样例的实际运行效果。然后我们再来展开。

图 1 是在电脑上打开一个图片后的初始效果:

Qt Quick 图像处理实例之美图秀秀(附源代码下载)

图 1 初始效果

图 2 是应用柔化特效后的效果:

Qt Quick 图像处理实例之美图秀秀(附源代码下载)

图 2 柔化特效

图 3 是应用灰度特效后的截图:

Qt Quick 图像处理实例之美图秀秀(附源代码下载)

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZm9ydW9r/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="" />

图 3 灰度特效

图 4 是浮雕特效:

Qt Quick 图像处理实例之美图秀秀(附源代码下载)

图 4 浮雕特效

图 5 是黑白特效:

Qt Quick 图像处理实例之美图秀秀(附源代码下载)

图 5 黑白特效

图 6 是应用底片特效后的截图:

Qt Quick 图像处理实例之美图秀秀(附源代码下载)

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZm9ydW9r/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="" />

图 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 实例,然后应用算法,处理后的图片保存到指定位置。

底片

早些年的相机使用胶卷记录拍摄结果,洗照片比較麻烦,只是假设你拿究竟片。逆光去看,效果就非常特别。

底片算法事实上非常easy,取 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 来显示。

界面布局比較简陋。打开和退出两个button放在左上角,使用锚布局。关于锚布局。请參考《Qt Quick 布局介绍》或《Qt Quick 简单教程》。图像处理的 6 个button使用 Grid 定位器来管理。 2 行 3 列,放在界面左下角。

Grid 定位器的使用请參考《Qt Quick 布局介绍》。

关于图像处理button。以黑白特效做下说明。在 onClicked 信号处理器中。调用 processor 的 process() 方法。传入本地图片路径和特效算法。当特效运算异步完毕后,就会触发 finished 信号,进而 imageViewer 会更新……

好啦好啦,我们的图像处理实例就到此为止了。

秒懂?

实例项目及源代码下载:点这里点这里。须要一点积分啊亲。

回想一下吧:

本文写作过程中參考了下列文章,特此感谢:

  • winorlose2000 博客( http://vaero.blog.51cto.com/ )中关于图像处理算法的博文
  • ian 的个人博客( http://www.icodelogic.com/ )中关于图像处理算法的博文