在《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
#include
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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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 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 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
#include "qtquick2applicationviewer.h"
#include
#include "imageProcessor.h"
#include
#include
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
qmlRegisterType("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 会更新……
好啦好啦,我们的图像处理实例就到此为止了。秒懂?
实例项目及源代码下载:点这里点这里。需要一点积分啊亲。
回顾一下吧:
本文写作过程中参考了下列文章,特此感谢: