首页 > 代码库 > Qt Quick 图像处理实例之美图秀秀(附源码下载)
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 是在电脑上打开一个图片后的初始效果:
图 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); }
这样处理的效果不是特别明显,采用高斯模糊算法可以获取更好的效果。
灰度
把图像变灰,大概有这么三种方法:
- 最大值法,即 R = G = B = max(R , G , B),这种方法处理过的图片亮度偏高
- 平均值法,即 R = G = B = (R + G + B) / 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.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/ )中关于图像处理算法的博文