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

How to paint / deform a QImage in 2D?

我有一个代码可以操纵 QImage 并将其渲染到 QWidget 上。操作包括典型的齐次变换以及裁剪、应用颜色传递函数等。

现在我必须将图像 变形 成一个四顶点多边形(四边形),不一定是矩形。需要说明的是,我说的不是裁剪图像,而是变形图像。

我知道如何使用 OpenGL(带纹理的四边形)执行此操作,但我想知道是否可以在不切换到 OpenGL 的情况下执行此操作。

Qt 是否有类似 2D 纹理的多边形或任何其他方式来变形 QImage,就像 PhotoShop 中的自由变换工具一样?

QPainter::transform() 确实是我在评论中推荐的解决方案。我对此并不完全确定,但 QPainter::transform() 甚至覆盖了分别使原始图像矩形变形的绘制图像。 (否则,我已将转换应用于 QImage 本身。)

然而,在制作一个小样本时,我意识到这只是故事的一半。

尽快完成了一个小型 Qt 示例应用程序,但我很难找到正确设置转换的方法。

顺便说一下,我不得不意识到 translate()scale()shear() 最多只适用于 3 点变形。 4 点变形也可能引入透视变形。因此,可能还需要 project()。感觉,我已经达到了我个人数学能力的极限,我 google 了解其他人是如何解决这个问题并发现

OpenCV getPerspectiveTransform()

Calculates a perspective transform from four pairs of the corresponding points.

听起来很有希望。查看 cv::getPerspectiveTransform() 的(不太长的)实现后,我意识到他们制作了一个线性方程并使用求解器产生相应的变换。

因此,我在 google 上进行了另一次搜索,发现了 Martin Thoma Solving linear equations with Gaussian elimination 的恕我直言 straight-forward 实现。 (我不知何故记得我一定在我的数学课上听说过高斯消元法,但那是几十年前的事了,从那以后我在日常工作中再也不需要这个了。)

所以,这就是我为求解器所做的(对 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

主要应用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();
}

我制作了一个项目文件在cygwin中编译 – testQImage4Point.pro:

SOURCES = testQImage4Point.cc

QT += widgets

可以使用以下命令构建和 运行:

$ 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

因为我必须调试代码,所以我也做了一个 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)

我用来制作 VS2017 解决方案。

这是我按预期运行后代码的运行方式:

我还没有做的事情:应用转换将 QImage 直接转换为另一个 QImage。我认为这是可能的。对于结果 QImage,必须在源中查找每个像素,将逆变换应用于坐标。因此,当然,转换后的坐标可能会超出范围。因此,必须处理这种情况(例如返回 pre-defined 边框颜色)。 (这是我在回答类似问题时所做的

,这是一天后偶然得到的。)