[英]How to paint / deform a QImage in 2D?
I have a code that manipulates and renders a QImage
onto a QWidget
. 我有一个代码来操纵
QImage
并将其渲染到QWidget
。 Manipulation includes typical homogeneous transformations as well as clipping, applying a color-transfer function, etc. 操作包括典型的均匀变换以及裁剪,应用颜色传递函数等。
Now I have to deform the image onto a four-vertices polygon (a quad), not necessarily a rectangle. 现在我必须将图像变形为四顶点多边形(四边形),不一定是矩形。 To be clear, I'm not talking about clipping the image, but deforming it.
要清楚,我不是在谈论剪裁图像,而是将其变形。
I know how to do it using OpenGL (a textured quad), but I wonder if it is possible to do it without switching to OpenGL. 我知道如何使用OpenGL(纹理四边形)来做到这一点,但我想知道是否可以在不切换到OpenGL的情况下完成它。
Does Qt have any 2D-like textured polygon or any other way to deform a QImage
, like the free-transformation tool in PhotoShop? Qt是否有任何类似2D的纹理多边形或任何其他方式来变形
QImage
,就像PhotoShop中的自由变换工具一样?
QPainter::transform()
is indeed the solution as I already recommended in my comment. QPainter::transform()
确实是我在评论中已经推荐的解决方案。 I was not fully sure about this but QPainter::transform()
even covers drawn images deforming the original image rectangle respectively. 我对此并不完全确定,但
QPainter::transform()
甚至覆盖了分别使原始图像矩形变形的绘制图像。 (Otherwise, I had applied the transformation to the QImage
itself.) (否则,我已将转换应用于
QImage
本身。)
However, while making a small sample I realized that's only half of the story. 然而,在制作一个小样本时,我意识到这只是故事的一半。
A small Qt sample application was done ASAP but I struggled to find a way to setup the transformation properly. 一个小的Qt示例应用程序尽快完成,但我很难找到一种正确设置转换的方法。
By the way I had to realize that translate()
, scale()
, and shear()
are at best good for a 3 point deformation. 顺便说一句,我必须意识到
translate()
, scale()
和shear()
最好是3点变形。 A 4 point deformation may introduce perspective distortion as well. 4点变形也可能引入透视畸变。 Hence,
project()
may be needed also. 因此,也可能需要
project()
。 Feeling, that I had reached the border of my personal math capabilities I googled how other guys have solved that and found 感觉,我已经达到了我个人数学能力的边界,我用谷歌搜索了其他人是如何解决这个问题并找到的
OpenCV
getPerspectiveTransform()
OpenCV
getPerspectiveTransform()
Calculates a perspective transform from four pairs of the corresponding points.
从四对对应点计算透视变换。
That sounded promising. 听起来很有希望。 Having a look into the (not so long) implementation of
cv::getPerspectiveTransform()
, I realized that they made a linear equation and used a solver to yield the respective transformation. 看一下
cv::getPerspectiveTransform()
的(不那么长)实现,我意识到他们制作了一个线性方程并使用求解器来产生相应的变换。
So, I did another search on google and found an IMHO straight-forward implementation by Martin Thoma Solving linear equations with Gaussian elimination . 所以,我在谷歌上做了另一个搜索,发现了马丁托马的IMHO直接实现解决了高斯消元的线性方程 。 (I somehow remember that I must have heard about Gaussian elimination in my math lessons but that's decades ago, and I never have needed this in daily business since then.)
(我不记得我记得在数学课上我一定听说过高斯消除,但那是几十年前的事了,从那以后我从来没有在日常业务中需要这个。)
So, this is what I did for a solver (applying minor stylistic changes to the original code of Martin Thoma) – solveLin.h
: 所以,这就是我为求解器所做的事情(对Martin Thoma的原始代码进行了微小的风格修改) -
solveLin.h
:
#ifndef SOLVE_LIN_H
#define SOLVE_LIN_H
#include <cassert>
#include <cmath>
#include <vector>
template <typename VALUE>
class MatrixT {
public:
typedef VALUE Value;
private:
std::vector<Value> _values;
size_t _nCols;
public:
explicit MatrixT(
size_t nRows, size_t nCols, const Value &value = Value()):
_values(nRows * nCols, value),
_nCols(nCols)
{ }
explicit MatrixT(
size_t nRows, size_t nCols,
std::initializer_list<Value> values):
_values(/*assert(values.size() == nRows * nCols),*/ values),
_nCols(nCols)
{ }
~MatrixT() = default;
MatrixT(const MatrixT&) = default;
MatrixT& operator=(const MatrixT&) = default;
size_t cols() const { return _nCols; }
size_t rows() const { return _values.size() / _nCols; }
const Value* operator[](size_t row) const
{
assert(row < rows());
return &_values[row * _nCols];
}
Value* operator[](const size_t row)
{
return (Value*)((const MatrixT&)*this)[row];
}
};
/** strongly inspired by (not to say: shamelessly copied from)
* Martin Thoma "Solving linear equations with Gaussian elimination"
* https://martin-thoma.com/solving-linear-equations-with-gaussian-elimination/
*/
template <typename VALUE>
std::vector<VALUE> gauss(MatrixT<VALUE> mat)
{
typedef VALUE Value;
const size_t n = mat.rows();
assert(mat.cols() == n + 1);
for (size_t i = 0; i < n; ++i) {
// search for max. value in this column
Value maxI = std::abs(mat[i][i]);
size_t iMax = i;
for (size_t k = i + 1; k < n; ++k) {
const Value maxK = std::abs(mat[k][i]);
if (maxI < maxK) maxI = maxK, iMax = k;
}
// swap max. row with current row
std::swap_ranges(
mat[i] + i, mat[i] + n + 1,
mat[iMax] + i);
// make all rows below this one 0 in current column
for (size_t k = i + 1; k < n; ++k) {
const Value c = mat[k][i] / mat[i][i];
for (size_t j = i; j < n + 1; ++j) {
if (i == j) mat[k][j] = (Value)0;
else mat[k][j] -= c * mat[i][j];
}
}
}
// solve equation Ax=b for an upper triangular matrix A
std::vector<Value> x(n);
for (size_t i = n; i--;) {
x[i] = mat[i][n] / mat[i][i];
for (size_t k = i; k--;) {
mat[k][n] -= mat[k][i] * x[i];
}
}
// done
return x;
}
#endif // SOLVE_LIN_H
The main application testQImage4Point.cc
: 主要应用
testQImage4Point.cc
:
#include <QtWidgets>
#include "solveLin.h"
/* strongly inspired by (not to say: shamelessly copied from)
* cv::Mat cv::getPerspectiveTransform(
* const Point2f src[], const Point2f dst[], int solveMethod)
*/
QTransform xform4Point(
const QPoint quad0[4], const QPoint quad1[4])
{
qDebug() << "quad0:" << quad0[0] << quad0[1] << quad0[2] << quad0[3];
qDebug() << "quad1:" << quad1[0] << quad1[1] << quad1[2] << quad1[3];
typedef MatrixT<double> Matrix;
Matrix mat(8, 9, 0.0);
for (size_t i = 0; i < 4; ++i) {
mat[i][0] = mat[i + 4][3] = quad0[i].x();
mat[i][1] = mat[i + 4][4] = quad0[i].y();
mat[i][2] = mat[i + 4][5] = 1.0;
mat[i][6] = -quad0[i].x() * quad1[i].x();
mat[i][7] = -quad0[i].y() * quad1[i].x();
mat[i + 4][6] = -quad0[i].x() * quad1[i].y();
mat[i + 4][7] = -quad0[i].y() * quad1[i].y();
mat[i][8] = quad1[i].x();
mat[i + 4][8] = quad1[i].y();
}
std::vector<double> result = gauss(mat);
return QTransform(
result[0], result[3], result[6],
result[1], result[4], result[7],
result[2], result[5], 1.0);
}
class Canvas: public QWidget {
private:
QImage _qImg;
QTransform _qXform;
QPoint _quadOrig[4];
QPoint _quadXform[4];
int _editMode;
bool _viewXform;
QSize _gripSize;
QPoint *_quadEdit; // pointer to currently edited quad
int _grip; // current grip (in mouse drag)
public:
Canvas();
virtual ~Canvas() = default;
Canvas(const Canvas&) = delete;
Canvas& operator=(const Canvas&) = delete;
public:
const QImage& image() const { return _qImg; }
void setImage(const QImage &qImg);
int editMode() const { return _editMode; }
void setEditMode(int editMode);
bool viewXform() const { return _viewXform; }
void setViewXform(bool enable);
protected:
virtual void paintEvent(QPaintEvent *pQEvent) override;
virtual void mousePressEvent(QMouseEvent *pQEvent) override;
virtual void mouseMoveEvent(QMouseEvent *pQEvent) override;
private:
int pickGrip(const QPoint &pos) const;
void drawQuad(QPainter &qPainter, const QPoint quad[4], bool grips);
void drawGrid(QPainter &qPainter);
};
Canvas::Canvas():
QWidget(),
_quadOrig{
QPoint(0.25 * width(), 0.25 * height()),
QPoint(0.75 * width(), 0.25 * height()),
QPoint(0.75 * width(), 0.75 * height()),
QPoint(0.25 * width(), 0.75 * height())
},
_quadXform{
_quadOrig[0], _quadOrig[1], _quadOrig[2], _quadOrig[3]
},
_editMode(0),
_viewXform(true),
_gripSize(7, 7),
_quadEdit(_quadOrig),
_grip(-1)
{ }
void Canvas::setImage(const QImage &qImg)
{
_qImg = qImg;
_quadOrig[0] = QPoint(0.25 * _qImg.width(), 0.25 * _qImg.height());
_quadOrig[1] = QPoint(0.75 * _qImg.width(), 0.25 * _qImg.height());
_quadOrig[2] = QPoint(0.75 * _qImg.width(), 0.75 * _qImg.height());
_quadOrig[3] = QPoint(0.25 * _qImg.width(), 0.75 * _qImg.height());
std::copy(_quadOrig, _quadOrig + 4, _quadXform);
update();
}
void Canvas::setEditMode(int editMode)
{
_editMode = editMode;
_quadEdit
= _editMode == 0 ? _quadOrig
: _editMode == 1 ? _quadXform
: nullptr;
update();
}
void Canvas::setViewXform(bool enable)
{
_viewXform = enable;
update();
}
void Canvas::paintEvent(QPaintEvent *pQEvent)
{
QWidget::paintEvent(pQEvent);
QPainter qPainter(this);
const QTransform qXformOld = qPainter.transform();
if (_viewXform) qPainter.setTransform(_qXform);
qPainter.drawImage(0, 0, _qImg);
qPainter.setPen(Qt::white);
drawGrid(qPainter);
qPainter.setPen(Qt::black);
qPainter.setPen(Qt::DotLine);
drawGrid(qPainter);
qPainter.setPen(Qt::SolidLine);
qPainter.setTransform(qXformOld);
for (int i = 1; i <= 2; ++i) {
switch ((_editMode + i) % 2) {
case 0: // draw orig. quad
qPainter.setPen(Qt::red);
drawQuad(qPainter, _quadOrig, _editMode == 0);
break;
case 1:
// draw transformed quad
qPainter.setPen(Qt::green);
drawQuad(qPainter, _quadXform, _editMode == 1);
break;
}
}
}
void Canvas::mousePressEvent(QMouseEvent *pQEvent)
{
if (pQEvent->button() == Qt::LeftButton) {
_grip = pickGrip(pQEvent->pos());
qDebug() << "hit:" << _grip;
}
}
void Canvas::mouseMoveEvent(QMouseEvent *pQEvent)
{
if ((pQEvent->buttons() & Qt::LeftButton) && _grip >= 0) {
_quadEdit[_grip] = pQEvent->pos();
_qXform = xform4Point(_quadOrig, _quadXform);
qDebug() << "transform:" << _qXform;
update();
}
}
int Canvas::pickGrip(const QPoint &pos) const
{
if (!_quadEdit) return -1;
const QPoint gripOffs(_gripSize.width() / 2, _gripSize.height() / 2);
for (int i = 4; i--;) {
const QRect rect(_quadEdit[i] - gripOffs, _gripSize);
if (rect.contains(pos)) return i;
}
return -1;
}
void Canvas::drawQuad(QPainter &qPainter, const QPoint quad[4], bool grips)
{
qPainter.drawPolygon(quad, 4);
if (grips) {
const QPoint gripOffs(_gripSize.width() / 2, _gripSize.height() / 2);
for (int i = 0; i < 4; ++i) {
qPainter.drawRect(QRect(quad[i] - gripOffs, _gripSize));
}
}
}
void Canvas::drawGrid(QPainter &qPainter)
{
const int w = _qImg.width() - 1, h = _qImg.height() - 1;
const int n = 5;
for (int i = 0; i <= n; ++i) {
const int x = i * w / n, y = i * h / n;
qPainter.drawLine(x, 0, x, h);
qPainter.drawLine(0, y, w, y);
}
}
int main(int argc, char **argv)
{
qDebug() << "Qt Version:" << QT_VERSION_STR;
QApplication app(argc, argv);
// init GUI
QMainWindow winMain;
winMain.setWindowTitle("4 Point Transform");
Canvas canvas;
winMain.setCentralWidget(&canvas);
QToolBar qToolBar;
QActionGroup qTglGrpEdit(&qToolBar);
QAction qTglFrom("Edit From", &qTglGrpEdit);
qTglFrom.setCheckable(true);
if (canvas.editMode() == 0) qTglFrom.setChecked(true);
qToolBar.addAction(&qTglFrom);
QAction qTglTo("Edit To", &qTglGrpEdit);
qTglTo.setCheckable(true);
if (canvas.editMode() == 1) qTglTo.setChecked(true);
qToolBar.addAction(&qTglTo);
qToolBar.addSeparator();
QActionGroup qTglGrpView(&qToolBar);
QAction qTglOrig("View Original", &qTglGrpView);
qTglOrig.setCheckable(true);
if (!canvas.viewXform()) qTglOrig.setChecked(true);
qToolBar.addAction(&qTglOrig);
QAction qTglXform("View Deformed", &qTglGrpView);
qTglXform.setCheckable(true);
if (canvas.viewXform()) qTglXform.setChecked(true);
qToolBar.addAction(&qTglXform);
winMain.addToolBar(&qToolBar);
winMain.show();
// init image
const int dx = winMain.width() - canvas.width();
const int dy = winMain.height() - canvas.height();
canvas.setImage(QImage("window-cats.jpg"));
winMain.resize(canvas.image().width() + dx, canvas.image().height() + dy);
// install signal handlers
QObject::connect(&qTglFrom, &QAction::triggered,
[&](bool checked) { if (checked) canvas.setEditMode(0); });
QObject::connect(&qTglTo, &QAction::triggered,
[&](bool checked) { if (checked) canvas.setEditMode(1); });
QObject::connect(&qTglOrig, &QAction::triggered,
[&](bool checked) { if (checked) canvas.setViewXform(false); });
QObject::connect(&qTglXform, &QAction::triggered,
[&](bool checked) { if (checked) canvas.setViewXform(true); });
// runtime loop
return app.exec();
}
I made a project file to compile in cygwin – testQImage4Point.pro
: 我制作了一个项目文件,在cygwin中编译 -
testQImage4Point.pro
:
SOURCES = testQImage4Point.cc
QT += widgets
which can be built and run with the following commands: 可以使用以下命令构建和运行:
$ qmake-qt5 testQImage4Point.pro
$ make && ./testQImage4Point
g++ -c -fno-keep-inline-dllexport -D_GNU_SOURCE -pipe -O2 -Wall -W -D_REENTRANT -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -I. -isystem /usr/include/qt5 -isystem /usr/include/qt5/QtWidgets -isystem /usr/include/qt5/QtGui -isystem /usr/include/qt5/QtCore -I. -I/usr/lib/qt5/mkspecs/cygwin-g++ -o testQImage4Point.o testQImage4Point.cc
g++ -o testQImage4Point.exe testQImage4Point.o -lQt5Widgets -lQt5Gui -lQt5Core -lGL -lpthread
Qt Version: 5.9.4
As I had to debug the code, I made a CMakeLists.txt
as well: 因为我必须调试代码,所以我也制作了一个
CMakeLists.txt
:
project(QImage4Point)
cmake_minimum_required(VERSION 3.10.0)
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(Qt5Widgets CONFIG REQUIRED)
include_directories(
"${CMAKE_SOURCE_DIR}")
add_executable(testQImage4Point
testQImage4Point.cc
solveLin.h)
target_link_libraries(testQImage4Point
Qt5::Widgets)
# define QT_NO_KEYWORDS to prevent confusion between of Qt signal-slots and
# other signal-slot APIs
target_compile_definitions(testQImage4Point PUBLIC QT_NO_KEYWORDS)
which I used to make a VS2017 solution. 我曾经制作过VS2017解决方案。
This is how the code looked in action after I got it working as expected: 这是我按预期工作后代码的行动方式:
What I did not yet: applying the transformation to transform a QImage
directly into another QImage
. 我还没有:应用转换将
QImage
直接转换为另一个QImage
。 I think it's possible. 我认为这是可能的。 For the resulting
QImage
, every pixel has to be looked up in the source applying the inverse transformation to the coordinates. 对于得到的
QImage
,必须在源中查找每个像素,将逆变换应用于坐标。 Thereby, of course, transformed coordinates might be out of range. 因此,当然,变换的坐标可能超出范围。 Hence, this case has to be handled (eg returning a pre-defined border color).
因此,必须处理这种情况(例如,返回预定义的边框颜色)。 (This is what I did in the answer to a similar question
(这就是我在回答类似问题时所做的
SO: Rotate an image in C++ without using OpenCV functions SO:在不使用OpenCV函数的情况下用C ++旋转图像
which incidentally came one day later.) 顺便提到一天后。)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.