简体   繁体   English

如何在2D中绘制/变形QImage?

[英]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 cygwintestQImage4Point.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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM