File indexing completed on 2024-05-12 15:58:26
0001 /* 0002 * SPDX-FileCopyrightText: 2014 Dmitry Kazakov <dimula73@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "kis_liquify_transform_worker.h" 0008 0009 #include <KoColorSpace.h> 0010 #include "kis_grid_interpolation_tools.h" 0011 #include "kis_dom_utils.h" 0012 #include "krita_utils.h" 0013 0014 0015 struct Q_DECL_HIDDEN KisLiquifyTransformWorker::Private 0016 { 0017 Private(const QRect &_srcBounds, 0018 KoUpdater *_progress, 0019 int _pixelPrecision) 0020 : srcBounds(_srcBounds), 0021 progress(_progress), 0022 pixelPrecision(_pixelPrecision) 0023 { 0024 } 0025 0026 QRect srcBounds; 0027 0028 QVector<QPointF> originalPoints; 0029 QVector<QPointF> transformedPoints; 0030 0031 KoUpdater *progress; 0032 int pixelPrecision; 0033 QSize gridSize; 0034 0035 void preparePoints(); 0036 0037 struct MapIndexesOp; 0038 0039 template <class ProcessOp> 0040 void processTransformedPixelsBuildUp(ProcessOp op, 0041 const QPointF &base, 0042 qreal sigma); 0043 0044 template <class ProcessOp> 0045 void processTransformedPixelsWash(ProcessOp op, 0046 const QPointF &base, 0047 qreal sigma, 0048 qreal flow); 0049 0050 template <class ProcessOp> 0051 void processTransformedPixels(ProcessOp op, 0052 const QPointF &base, 0053 qreal sigma, 0054 bool useWashMode, 0055 qreal flow); 0056 }; 0057 0058 KisLiquifyTransformWorker::KisLiquifyTransformWorker(const QRect &srcBounds, 0059 KoUpdater *progress, 0060 int pixelPrecision) 0061 : m_d(new Private(srcBounds, progress, pixelPrecision)) 0062 { 0063 KIS_ASSERT_RECOVER_RETURN(!srcBounds.isEmpty()); 0064 0065 // TODO: implement 'progress' stuff 0066 m_d->preparePoints(); 0067 } 0068 0069 KisLiquifyTransformWorker::KisLiquifyTransformWorker(const KisLiquifyTransformWorker &rhs) 0070 : m_d(new Private(*rhs.m_d.data())) 0071 { 0072 } 0073 0074 KisLiquifyTransformWorker::~KisLiquifyTransformWorker() 0075 { 0076 } 0077 0078 bool KisLiquifyTransformWorker::operator==(const KisLiquifyTransformWorker &other) const 0079 { 0080 bool result = 0081 m_d->srcBounds == other.m_d->srcBounds && 0082 m_d->pixelPrecision == other.m_d->pixelPrecision && 0083 m_d->gridSize == other.m_d->gridSize && 0084 m_d->originalPoints.size() == other.m_d->originalPoints.size() && 0085 m_d->transformedPoints.size() == other.m_d->transformedPoints.size(); 0086 0087 if (!result) return false; 0088 0089 const qreal eps = 1e-6; 0090 0091 result = 0092 KisAlgebra2D::fuzzyPointCompare(m_d->originalPoints, other.m_d->originalPoints, eps) && 0093 KisAlgebra2D::fuzzyPointCompare(m_d->transformedPoints, other.m_d->transformedPoints, eps); 0094 0095 return result; 0096 } 0097 0098 bool KisLiquifyTransformWorker::isIdentity() const 0099 { 0100 const qreal eps = 1e-6; 0101 return KisAlgebra2D::fuzzyPointCompare(m_d->originalPoints, m_d->transformedPoints, eps); 0102 } 0103 0104 int KisLiquifyTransformWorker::pointToIndex(const QPoint &cellPt) 0105 { 0106 return GridIterationTools::pointToIndex(cellPt, m_d->gridSize); 0107 } 0108 0109 QSize KisLiquifyTransformWorker::gridSize() const 0110 { 0111 return m_d->gridSize; 0112 } 0113 0114 const QVector<QPointF>& KisLiquifyTransformWorker::originalPoints() const 0115 { 0116 return m_d->originalPoints; 0117 } 0118 0119 QVector<QPointF>& KisLiquifyTransformWorker::transformedPoints() 0120 { 0121 return m_d->transformedPoints; 0122 } 0123 0124 struct AllPointsFetcherOp 0125 { 0126 AllPointsFetcherOp(QRectF srcRect) : m_srcRect(srcRect) {} 0127 0128 inline void processPoint(int col, int row, 0129 int prevCol, int prevRow, 0130 int colIndex, int rowIndex) { 0131 0132 Q_UNUSED(prevCol); 0133 Q_UNUSED(prevRow); 0134 Q_UNUSED(colIndex); 0135 Q_UNUSED(rowIndex); 0136 0137 QPointF pt(col, row); 0138 m_points << pt; 0139 } 0140 0141 inline void nextLine() { 0142 } 0143 0144 QVector<QPointF> m_points; 0145 QRectF m_srcRect; 0146 }; 0147 0148 void KisLiquifyTransformWorker::Private::preparePoints() 0149 { 0150 gridSize = 0151 GridIterationTools::calcGridSize(srcBounds, pixelPrecision); 0152 0153 AllPointsFetcherOp pointsOp(srcBounds); 0154 GridIterationTools::processGrid(pointsOp, srcBounds, pixelPrecision); 0155 0156 const int numPoints = pointsOp.m_points.size(); 0157 0158 KIS_ASSERT_RECOVER_RETURN(numPoints == gridSize.width() * gridSize.height()); 0159 0160 originalPoints = pointsOp.m_points; 0161 transformedPoints = pointsOp.m_points; 0162 } 0163 0164 void KisLiquifyTransformWorker::translate(const QPointF &offset) 0165 { 0166 QVector<QPointF>::iterator it = m_d->transformedPoints.begin(); 0167 QVector<QPointF>::iterator end = m_d->transformedPoints.end(); 0168 0169 QVector<QPointF>::iterator refIt = m_d->originalPoints.begin(); 0170 KIS_ASSERT_RECOVER_RETURN(m_d->originalPoints.size() == 0171 m_d->transformedPoints.size()); 0172 0173 for (; it != end; ++it, ++refIt) { 0174 *it += offset; 0175 *refIt += offset; 0176 } 0177 } 0178 0179 void KisLiquifyTransformWorker::translateDstSpace(const QPointF &offset) 0180 { 0181 QVector<QPointF>::iterator it = m_d->transformedPoints.begin(); 0182 QVector<QPointF>::iterator end = m_d->transformedPoints.end(); 0183 0184 for (; it != end; ++it) { 0185 *it += offset; 0186 } 0187 } 0188 0189 void KisLiquifyTransformWorker::undoPoints(const QPointF &base, 0190 qreal amount, 0191 qreal sigma) 0192 { 0193 const qreal maxDistCoeff = 3.0; 0194 const qreal maxDist = maxDistCoeff * sigma; 0195 QRectF clipRect(base.x() - maxDist, base.y() - maxDist, 0196 2 * maxDist, 2 * maxDist); 0197 0198 QVector<QPointF>::iterator it = m_d->transformedPoints.begin(); 0199 QVector<QPointF>::iterator end = m_d->transformedPoints.end(); 0200 0201 QVector<QPointF>::iterator refIt = m_d->originalPoints.begin(); 0202 KIS_ASSERT_RECOVER_RETURN(m_d->originalPoints.size() == 0203 m_d->transformedPoints.size()); 0204 0205 for (; it != end; ++it, ++refIt) { 0206 if (!clipRect.contains(*it)) continue; 0207 0208 QPointF diff = *it - base; 0209 qreal dist = KisAlgebra2D::norm(diff); 0210 if (dist > maxDist) continue; 0211 0212 qreal lambda = exp(-0.5 * pow2(dist / sigma)); 0213 lambda *= amount; 0214 *it = *refIt * lambda + *it * (1.0 - lambda); 0215 } 0216 } 0217 0218 template <class ProcessOp> 0219 void KisLiquifyTransformWorker::Private:: 0220 processTransformedPixelsBuildUp(ProcessOp op, 0221 const QPointF &base, 0222 qreal sigma) 0223 { 0224 const qreal maxDist = ProcessOp::maxDistCoeff * sigma; 0225 QRectF clipRect(base.x() - maxDist, base.y() - maxDist, 0226 2 * maxDist, 2 * maxDist); 0227 0228 QVector<QPointF>::iterator it = transformedPoints.begin(); 0229 QVector<QPointF>::iterator end = transformedPoints.end(); 0230 0231 for (; it != end; ++it) { 0232 if (!clipRect.contains(*it)) continue; 0233 0234 QPointF diff = *it - base; 0235 qreal dist = KisAlgebra2D::norm(diff); 0236 if (dist > maxDist) continue; 0237 0238 const qreal lambda = exp(-0.5 * pow2(dist / sigma)); 0239 *it = op(*it, base, diff, lambda); 0240 } 0241 } 0242 0243 template <class ProcessOp> 0244 void KisLiquifyTransformWorker::Private:: 0245 processTransformedPixelsWash(ProcessOp op, 0246 const QPointF &base, 0247 qreal sigma, 0248 qreal flow) 0249 { 0250 const qreal maxDist = ProcessOp::maxDistCoeff * sigma; 0251 QRectF clipRect(base.x() - maxDist, base.y() - maxDist, 0252 2 * maxDist, 2 * maxDist); 0253 0254 QVector<QPointF>::iterator it = transformedPoints.begin(); 0255 QVector<QPointF>::iterator end = transformedPoints.end(); 0256 0257 QVector<QPointF>::iterator refIt = originalPoints.begin(); 0258 KIS_ASSERT_RECOVER_RETURN(originalPoints.size() == 0259 transformedPoints.size()); 0260 0261 for (; it != end; ++it, ++refIt) { 0262 if (!clipRect.contains(*it)) continue; 0263 0264 QPointF diff = *refIt - base; 0265 qreal dist = KisAlgebra2D::norm(diff); 0266 if (dist > maxDist) continue; 0267 0268 const qreal lambda = exp(-0.5 * pow2(dist / sigma)); 0269 QPointF dstPt = op(*refIt, base, diff, lambda); 0270 0271 if (kisDistance(dstPt, *refIt) > kisDistance(*it, *refIt)) { 0272 *it = (1.0 - flow) * (*it) + flow * dstPt; 0273 } 0274 } 0275 } 0276 0277 template <class ProcessOp> 0278 void KisLiquifyTransformWorker::Private:: 0279 processTransformedPixels(ProcessOp op, 0280 const QPointF &base, 0281 qreal sigma, 0282 bool useWashMode, 0283 qreal flow) 0284 { 0285 if (useWashMode) { 0286 processTransformedPixelsWash(op, base, sigma, flow); 0287 } else { 0288 processTransformedPixelsBuildUp(op, base, sigma); 0289 } 0290 } 0291 0292 struct TranslateOp 0293 { 0294 TranslateOp(const QPointF &offset) : m_offset(offset) {} 0295 0296 QPointF operator() (const QPointF &pt, 0297 const QPointF &base, 0298 const QPointF &diff, 0299 qreal lambda) 0300 { 0301 Q_UNUSED(base); 0302 Q_UNUSED(diff); 0303 return pt + lambda * m_offset; 0304 } 0305 0306 static const qreal maxDistCoeff; 0307 0308 QPointF m_offset; 0309 }; 0310 0311 const qreal TranslateOp::maxDistCoeff = 3.0; 0312 0313 struct ScaleOp 0314 { 0315 ScaleOp(qreal scale) : m_scale(scale) {} 0316 0317 QPointF operator() (const QPointF &pt, 0318 const QPointF &base, 0319 const QPointF &diff, 0320 qreal lambda) 0321 { 0322 Q_UNUSED(pt); 0323 Q_UNUSED(diff); 0324 return base + (1.0 + m_scale * lambda) * diff; 0325 } 0326 0327 static const qreal maxDistCoeff; 0328 0329 qreal m_scale; 0330 }; 0331 0332 const qreal ScaleOp::maxDistCoeff = 3.0; 0333 0334 struct RotateOp 0335 { 0336 RotateOp(qreal angle) : m_angle(angle) {} 0337 0338 QPointF operator() (const QPointF &pt, 0339 const QPointF &base, 0340 const QPointF &diff, 0341 qreal lambda) 0342 { 0343 Q_UNUSED(pt); 0344 0345 const qreal angle = m_angle * lambda; 0346 const qreal sinA = std::sin(angle); 0347 const qreal cosA = std::cos(angle); 0348 0349 qreal x = cosA * diff.x() + sinA * diff.y(); 0350 qreal y = -sinA * diff.x() + cosA * diff.y(); 0351 0352 return base + QPointF(x, y); 0353 } 0354 0355 static const qreal maxDistCoeff; 0356 0357 qreal m_angle; 0358 }; 0359 0360 const qreal RotateOp::maxDistCoeff = 3.0; 0361 0362 void KisLiquifyTransformWorker::translatePoints(const QPointF &base, 0363 const QPointF &offset, 0364 qreal sigma, 0365 bool useWashMode, 0366 qreal flow) 0367 { 0368 TranslateOp op(offset); 0369 m_d->processTransformedPixels(op, base, sigma, useWashMode, flow); 0370 } 0371 0372 void KisLiquifyTransformWorker::scalePoints(const QPointF &base, 0373 qreal scale, 0374 qreal sigma, 0375 bool useWashMode, 0376 qreal flow) 0377 { 0378 ScaleOp op(scale); 0379 m_d->processTransformedPixels(op, base, sigma, useWashMode, flow); 0380 } 0381 0382 void KisLiquifyTransformWorker::rotatePoints(const QPointF &base, 0383 qreal angle, 0384 qreal sigma, 0385 bool useWashMode, 0386 qreal flow) 0387 { 0388 RotateOp op(angle); 0389 m_d->processTransformedPixels(op, base, sigma, useWashMode, flow); 0390 } 0391 0392 void KisLiquifyTransformWorker::run(KisPaintDeviceSP srcDevice, KisPaintDeviceSP dstDevice) 0393 { 0394 KIS_SAFE_ASSERT_RECOVER_RETURN(*srcDevice->colorSpace() == *dstDevice->colorSpace()); 0395 0396 dstDevice->clear(); 0397 0398 using namespace GridIterationTools; 0399 0400 PaintDevicePolygonOp polygonOp(srcDevice, dstDevice); 0401 RegularGridIndexesOp indexesOp(m_d->gridSize); 0402 iterateThroughGrid<AlwaysCompletePolygonPolicy>(polygonOp, indexesOp, 0403 m_d->gridSize, 0404 m_d->originalPoints, 0405 m_d->transformedPoints); 0406 } 0407 0408 QRect KisLiquifyTransformWorker::approxChangeRect(const QRect &rc) 0409 { 0410 const qreal margin = 0.05; 0411 0412 /** 0413 * Here we just return the full area occupied by the transformed grid. 0414 * We sample grid points for not doing too much work. 0415 */ 0416 const int maxSamplePoints = 200; 0417 const int minStep = 3; 0418 const int step = qMax(minStep, m_d->transformedPoints.size() / maxSamplePoints); 0419 Q_UNUSED(step); 0420 0421 QVector<QPoint> samplePoints; 0422 for (auto it = m_d->transformedPoints.constBegin(); it != m_d->transformedPoints.constEnd(); ++it) { 0423 samplePoints << it->toPoint(); 0424 } 0425 0426 QRect resultRect = KisAlgebra2D::approximateRectFromPoints(samplePoints); 0427 return KisAlgebra2D::blowRect(resultRect | rc, margin); 0428 } 0429 0430 QRect KisLiquifyTransformWorker::approxNeedRect(const QRect &rc, const QRect &fullBounds) 0431 { 0432 Q_UNUSED(rc); 0433 return fullBounds; 0434 } 0435 0436 void KisLiquifyTransformWorker::transformSrcAndDst(const QTransform &t) 0437 { 0438 KIS_SAFE_ASSERT_RECOVER_RETURN(t.type() <= QTransform::TxScale); 0439 0440 m_d->srcBounds = t.mapRect(m_d->srcBounds); 0441 0442 for (auto it = m_d->originalPoints.begin(); it != m_d->originalPoints.end(); ++it) { 0443 *it = t.map(*it); 0444 } 0445 for (auto it = m_d->transformedPoints.begin(); it != m_d->transformedPoints.end(); ++it) { 0446 *it = t.map(*it); 0447 } 0448 } 0449 0450 #include <functional> 0451 #include <QTransform> 0452 0453 using PointMapFunction = std::function<QPointF (const QPointF&)>; 0454 0455 0456 PointMapFunction bindPointMapTransform(const QTransform &transform) { 0457 using namespace std::placeholders; 0458 0459 typedef QPointF (QTransform::*MapFuncType)(const QPointF&) const; 0460 return std::bind(static_cast<MapFuncType>(&QTransform::map), &transform, _1); 0461 } 0462 0463 QImage KisLiquifyTransformWorker::runOnQImage(const QImage &srcImage, 0464 const QPointF &srcImageOffset, 0465 const QTransform &imageToThumbTransform, 0466 QPointF *newOffset) 0467 { 0468 KIS_ASSERT_RECOVER(m_d->originalPoints.size() == m_d->transformedPoints.size()) { 0469 return QImage(); 0470 } 0471 0472 KIS_ASSERT_RECOVER(!srcImage.isNull()) { 0473 return QImage(); 0474 } 0475 0476 KIS_ASSERT_RECOVER(srcImage.format() == QImage::Format_ARGB32) { 0477 return QImage(); 0478 } 0479 0480 QVector<QPointF> originalPointsLocal(m_d->originalPoints); 0481 QVector<QPointF> transformedPointsLocal(m_d->transformedPoints); 0482 0483 PointMapFunction mapFunc = bindPointMapTransform(imageToThumbTransform); 0484 0485 std::transform(originalPointsLocal.begin(), originalPointsLocal.end(), 0486 originalPointsLocal.begin(), mapFunc); 0487 0488 std::transform(transformedPointsLocal.begin(), transformedPointsLocal.end(), 0489 transformedPointsLocal.begin(), mapFunc); 0490 0491 QRectF dstBounds; 0492 Q_FOREACH (const QPointF &pt, transformedPointsLocal) { 0493 KisAlgebra2D::accumulateBounds(pt, &dstBounds); 0494 } 0495 0496 const QRectF srcBounds(srcImageOffset, srcImage.size()); 0497 dstBounds |= srcBounds; 0498 0499 QPointF dstQImageOffset = dstBounds.topLeft(); 0500 *newOffset = dstQImageOffset; 0501 0502 QRect dstBoundsI = dstBounds.toAlignedRect(); 0503 0504 QImage dstImage(dstBoundsI.size(), srcImage.format()); 0505 dstImage.fill(0); 0506 0507 GridIterationTools::QImagePolygonOp polygonOp(srcImage, dstImage, srcImageOffset, dstQImageOffset); 0508 GridIterationTools::RegularGridIndexesOp indexesOp(m_d->gridSize); 0509 GridIterationTools::iterateThroughGrid 0510 <GridIterationTools::AlwaysCompletePolygonPolicy>(polygonOp, indexesOp, 0511 m_d->gridSize, 0512 originalPointsLocal, 0513 transformedPointsLocal); 0514 return dstImage; 0515 } 0516 0517 void KisLiquifyTransformWorker::toXML(QDomElement *e) const 0518 { 0519 QDomDocument doc = e->ownerDocument(); 0520 QDomElement liqEl = doc.createElement("liquify_points"); 0521 e->appendChild(liqEl); 0522 0523 KisDomUtils::saveValue(&liqEl, "srcBounds", m_d->srcBounds); 0524 KisDomUtils::saveValue(&liqEl, "originalPoints", m_d->originalPoints); 0525 KisDomUtils::saveValue(&liqEl, "transformedPoints", m_d->transformedPoints); 0526 KisDomUtils::saveValue(&liqEl, "pixelPrecision", m_d->pixelPrecision); 0527 KisDomUtils::saveValue(&liqEl, "gridSize", m_d->gridSize); 0528 } 0529 0530 KisLiquifyTransformWorker* KisLiquifyTransformWorker::fromXML(const QDomElement &e) 0531 { 0532 QDomElement liquifyEl; 0533 0534 QRect srcBounds; 0535 QVector<QPointF> originalPoints; 0536 QVector<QPointF> transformedPoints; 0537 int pixelPrecision; 0538 QSize gridSize; 0539 0540 bool result = false; 0541 0542 0543 result = 0544 KisDomUtils::findOnlyElement(e, "liquify_points", &liquifyEl) && 0545 0546 KisDomUtils::loadValue(liquifyEl, "srcBounds", &srcBounds) && 0547 KisDomUtils::loadValue(liquifyEl, "originalPoints", &originalPoints) && 0548 KisDomUtils::loadValue(liquifyEl, "transformedPoints", &transformedPoints) && 0549 KisDomUtils::loadValue(liquifyEl, "pixelPrecision", &pixelPrecision) && 0550 KisDomUtils::loadValue(liquifyEl, "gridSize", &gridSize); 0551 0552 if (!result) { 0553 warnKrita << "WARNING: Failed to load liquify worker from XML"; 0554 return new KisLiquifyTransformWorker(QRect(0,0,1024, 1024), 0, 8); 0555 } 0556 0557 KisLiquifyTransformWorker *worker = 0558 new KisLiquifyTransformWorker(srcBounds, 0, pixelPrecision); 0559 0560 const int numPoints = originalPoints.size(); 0561 0562 if (numPoints != transformedPoints.size() || 0563 numPoints != worker->m_d->originalPoints.size() || 0564 gridSize != worker->m_d->gridSize) { 0565 warnKrita << "WARNING: Inconsistent number of points!"; 0566 warnKrita << ppVar(originalPoints.size()); 0567 warnKrita << ppVar(transformedPoints.size()); 0568 warnKrita << ppVar(gridSize); 0569 warnKrita << ppVar(worker->m_d->originalPoints.size()); 0570 warnKrita << ppVar(worker->m_d->transformedPoints.size()); 0571 warnKrita << ppVar(worker->m_d->gridSize); 0572 0573 return worker; 0574 } 0575 0576 for (int i = 0; i < numPoints; i++) { 0577 worker->m_d->originalPoints[i] = originalPoints[i]; 0578 worker->m_d->transformedPoints[i] = transformedPoints[i]; 0579 } 0580 0581 0582 return worker; 0583 }