在《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); }
把图像变灰,大概有这么三种方法:
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); }
"浮雕" 图象效果是指图像的前景前向凸出背景。
浮雕的算法相对复杂一些,用当前点的 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); }
黑白图片的处理算法比较简单:对一个像素的 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); }
早些年的相机使用胶卷记录拍摄结果,洗照片比较麻烦,不过如果你拿到底片,逆光去看,效果就很特别。
底片算法其实很简单,取 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))
好啦,看代码:
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 类时,我希望它能够在 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.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; } } }
算法函数放在一个全局的函数指针数组中, AlgorithmRunnable 则根据算法枚举值从数组中取出相应的函数来处理图像。
其它的代码一看即可明白,不再多说。
要想在 QML 中实用 ImageProcessor 类,需要导出一个 QML 类型。这个工作是在 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(); }
import an.qt.ImageProcessor 1.0
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; } }
你看到了,我像使用 QML 内建对象那样使用了 ImageProcessor 对象,为它的 finished 信号定义了 onFinished 信号处理器,在信号处理器中把应用图像特效后的中间文件传递给 imageViewer 来显示。
界面布局比较简陋,打开和退出两个按钮放在左上角,使用锚布局。关于锚布局,请参考《Qt Quick 布局介绍》或《Qt Quick 简单教程》。图像处理的 6 个按钮使用 Grid 定位器来管理, 2 行 3 列,放在界面左下角。 Grid 定位器的使用请参考《Qt Quick 布局介绍》。
关于图像处理按钮,以黑白特效做下说明,在 onClicked 信号处理器中,调用 processor 的 process() 方法,传入本地图片路径和特效算法。当特效运算异步完成后,就会触发 finished 信号,进而 imageViewer 会更新……
好啦好啦,我们的图像处理实例就到此为止了。秒懂?
实例项目及源代码下载:点这里点这里。需要一点积分啊亲。
回顾一下吧:
本文写作过程中参考了下列文章,特此感谢: