Krita Source Code Documentation
Loading...
Searching...
No Matches
kis_liquify_transform_worker.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2014 Dmitry Kazakov <dimula73@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
8
9#include <KoColorSpace.h>
11#include "kis_dom_utils.h"
12#include "krita_utils.h"
13
14
16{
17 Private(const QRect &_srcBounds,
18 KoUpdater *_progress,
19 int _pixelPrecision)
20 : srcBounds(_srcBounds),
21 progress(_progress),
22 pixelPrecision(_pixelPrecision)
23 {
24 }
25
26 QRect srcBounds;
27
30
33 QSize gridSize;
34
36
37 struct MapIndexesOp;
38
39 template <class ProcessOp>
41 const QPointF &base,
42 qreal sigma);
43
44 template <class ProcessOp>
46 const QPointF &base,
47 qreal sigma,
48 qreal flow);
49
50 template <class ProcessOp>
51 void processTransformedPixels(ProcessOp op,
52 const QPointF &base,
53 qreal sigma,
54 bool useWashMode,
55 qreal flow);
56};
57
59 KoUpdater *progress,
60 int pixelPrecision)
61 : m_d(new Private(srcBounds, progress, pixelPrecision))
62{
64
65 // TODO: implement 'progress' stuff
66 m_d->preparePoints();
67}
68
73
77
79{
80 bool result =
81 m_d->srcBounds == other.m_d->srcBounds &&
82 m_d->pixelPrecision == other.m_d->pixelPrecision &&
83 m_d->gridSize == other.m_d->gridSize &&
84 m_d->originalPoints.size() == other.m_d->originalPoints.size() &&
85 m_d->transformedPoints.size() == other.m_d->transformedPoints.size();
86
87 if (!result) return false;
88
89 const qreal eps = 1e-6;
90
91 result =
92 KisAlgebra2D::fuzzyPointCompare(m_d->originalPoints, other.m_d->originalPoints, eps) &&
93 KisAlgebra2D::fuzzyPointCompare(m_d->transformedPoints, other.m_d->transformedPoints, eps);
94
95 return result;
96}
97
99{
100 const qreal eps = 1e-6;
101 return KisAlgebra2D::fuzzyPointCompare(m_d->originalPoints, m_d->transformedPoints, eps);
102}
103
105{
106 return GridIterationTools::pointToIndex(cellPt, m_d->gridSize);
107}
108
110{
111 return m_d->gridSize;
112}
113
115{
116 return m_d->originalPoints;
117}
118
120{
121 return m_d->transformedPoints;
122}
123
125{
126 AllPointsFetcherOp(QRectF srcRect) : m_srcRect(srcRect) {}
127
128 inline void processPoint(int col, int row,
129 int prevCol, int prevRow,
130 int colIndex, int rowIndex) {
131
132 Q_UNUSED(prevCol);
133 Q_UNUSED(prevRow);
134 Q_UNUSED(colIndex);
135 Q_UNUSED(rowIndex);
136
137 QPointF pt(col, row);
138 m_points << pt;
139 }
140
141 inline void nextLine() {
142 }
143
145 QRectF m_srcRect;
146};
147
148void KisLiquifyTransformWorker::Private::preparePoints()
149{
150 gridSize =
151 GridIterationTools::calcGridSize(srcBounds, pixelPrecision);
152
153 AllPointsFetcherOp pointsOp(srcBounds);
154 GridIterationTools::processGrid(pointsOp, srcBounds, pixelPrecision);
155
156 const int numPoints = pointsOp.m_points.size();
157
158 KIS_ASSERT_RECOVER_RETURN(numPoints == gridSize.width() * gridSize.height());
159
160 originalPoints = pointsOp.m_points;
161 transformedPoints = pointsOp.m_points;
162}
163
164void KisLiquifyTransformWorker::translate(const QPointF &offset)
165{
166 QVector<QPointF>::iterator it = m_d->transformedPoints.begin();
167 QVector<QPointF>::iterator end = m_d->transformedPoints.end();
168
169 QVector<QPointF>::iterator refIt = m_d->originalPoints.begin();
170 KIS_ASSERT_RECOVER_RETURN(m_d->originalPoints.size() ==
171 m_d->transformedPoints.size());
172
173 for (; it != end; ++it, ++refIt) {
174 *it += offset;
175 *refIt += offset;
176 }
177}
178
180{
181 QVector<QPointF>::iterator it = m_d->transformedPoints.begin();
182 QVector<QPointF>::iterator end = m_d->transformedPoints.end();
183
184 for (; it != end; ++it) {
185 *it += offset;
186 }
187}
188
190 qreal amount,
191 qreal sigma)
192{
193 const qreal maxDistCoeff = 3.0;
194 const qreal maxDist = maxDistCoeff * sigma;
195 QRectF clipRect(base.x() - maxDist, base.y() - maxDist,
196 2 * maxDist, 2 * maxDist);
197
198 QVector<QPointF>::iterator it = m_d->transformedPoints.begin();
199 QVector<QPointF>::iterator end = m_d->transformedPoints.end();
200
201 QVector<QPointF>::iterator refIt = m_d->originalPoints.begin();
202 KIS_ASSERT_RECOVER_RETURN(m_d->originalPoints.size() ==
203 m_d->transformedPoints.size());
204
205 for (; it != end; ++it, ++refIt) {
206 if (!clipRect.contains(*it)) continue;
207
208 QPointF diff = *it - base;
209 qreal dist = KisAlgebra2D::norm(diff);
210 if (dist > maxDist) continue;
211
212 qreal lambda = exp(-0.5 * pow2(dist / sigma));
213 lambda *= amount;
214 *it = *refIt * lambda + *it * (1.0 - lambda);
215 }
216}
217
218template <class ProcessOp>
219void KisLiquifyTransformWorker::Private::
220processTransformedPixelsBuildUp(ProcessOp op,
221 const QPointF &base,
222 qreal sigma)
223{
224 const qreal maxDist = ProcessOp::maxDistCoeff * sigma;
225 QRectF clipRect(base.x() - maxDist, base.y() - maxDist,
226 2 * maxDist, 2 * maxDist);
227
228 QVector<QPointF>::iterator it = transformedPoints.begin();
229 QVector<QPointF>::iterator end = transformedPoints.end();
230
231 for (; it != end; ++it) {
232 if (!clipRect.contains(*it)) continue;
233
234 QPointF diff = *it - base;
235 qreal dist = KisAlgebra2D::norm(diff);
236 if (dist > maxDist) continue;
237
238 const qreal lambda = exp(-0.5 * pow2(dist / sigma));
239 *it = op(*it, base, diff, lambda);
240 }
241}
242
243template <class ProcessOp>
244void KisLiquifyTransformWorker::Private::
245processTransformedPixelsWash(ProcessOp op,
246 const QPointF &base,
247 qreal sigma,
248 qreal flow)
249{
250 const qreal maxDist = ProcessOp::maxDistCoeff * sigma;
251 QRectF clipRect(base.x() - maxDist, base.y() - maxDist,
252 2 * maxDist, 2 * maxDist);
253
254 QVector<QPointF>::iterator it = transformedPoints.begin();
255 QVector<QPointF>::iterator end = transformedPoints.end();
256
257 QVector<QPointF>::iterator refIt = originalPoints.begin();
258 KIS_ASSERT_RECOVER_RETURN(originalPoints.size() ==
259 transformedPoints.size());
260
261 for (; it != end; ++it, ++refIt) {
262 if (!clipRect.contains(*it)) continue;
263
264 QPointF diff = *refIt - base;
265 qreal dist = KisAlgebra2D::norm(diff);
266 if (dist > maxDist) continue;
267
268 const qreal lambda = exp(-0.5 * pow2(dist / sigma));
269 QPointF dstPt = op(*refIt, base, diff, lambda);
270
271 if (kisDistance(dstPt, *refIt) > kisDistance(*it, *refIt)) {
272 *it = (1.0 - flow) * (*it) + flow * dstPt;
273 }
274 }
275}
276
277template <class ProcessOp>
278void KisLiquifyTransformWorker::Private::
279processTransformedPixels(ProcessOp op,
280 const QPointF &base,
281 qreal sigma,
282 bool useWashMode,
283 qreal flow)
284{
285 if (useWashMode) {
286 processTransformedPixelsWash(op, base, sigma, flow);
287 } else {
288 processTransformedPixelsBuildUp(op, base, sigma);
289 }
290}
291
293{
294 TranslateOp(const QPointF &offset) : m_offset(offset) {}
295
296 QPointF operator() (const QPointF &pt,
297 const QPointF &base,
298 const QPointF &diff,
299 qreal lambda)
300 {
301 Q_UNUSED(base);
302 Q_UNUSED(diff);
303 return pt + lambda * m_offset;
304 }
305
306 static const qreal maxDistCoeff;
307
308 QPointF m_offset;
309};
310
311const qreal TranslateOp::maxDistCoeff = 3.0;
312
314{
315 ScaleOp(qreal scale) : m_scale(scale) {}
316
317 QPointF operator() (const QPointF &pt,
318 const QPointF &base,
319 const QPointF &diff,
320 qreal lambda)
321 {
322 Q_UNUSED(pt);
323 Q_UNUSED(diff);
324 return base + (1.0 + m_scale * lambda) * diff;
325 }
326
327 static const qreal maxDistCoeff;
328
329 qreal m_scale;
330};
331
332const qreal ScaleOp::maxDistCoeff = 3.0;
333
335{
336 RotateOp(qreal angle) : m_angle(angle) {}
337
338 QPointF operator() (const QPointF &pt,
339 const QPointF &base,
340 const QPointF &diff,
341 qreal lambda)
342 {
343 Q_UNUSED(pt);
344
345 const qreal angle = m_angle * lambda;
346 const qreal sinA = std::sin(angle);
347 const qreal cosA = std::cos(angle);
348
349 qreal x = cosA * diff.x() + sinA * diff.y();
350 qreal y = -sinA * diff.x() + cosA * diff.y();
351
352 return base + QPointF(x, y);
353 }
354
355 static const qreal maxDistCoeff;
356
357 qreal m_angle;
358};
359
360const qreal RotateOp::maxDistCoeff = 3.0;
361
363 const QPointF &offset,
364 qreal sigma,
365 bool useWashMode,
366 qreal flow)
367{
368 TranslateOp op(offset);
369 m_d->processTransformedPixels(op, base, sigma, useWashMode, flow);
370}
371
373 qreal scale,
374 qreal sigma,
375 bool useWashMode,
376 qreal flow)
377{
378 ScaleOp op(scale);
379 m_d->processTransformedPixels(op, base, sigma, useWashMode, flow);
380}
381
383 qreal angle,
384 qreal sigma,
385 bool useWashMode,
386 qreal flow)
387{
388 RotateOp op(angle);
389 m_d->processTransformedPixels(op, base, sigma, useWashMode, flow);
390}
391
393{
394 KIS_SAFE_ASSERT_RECOVER_RETURN(*srcDevice->colorSpace() == *dstDevice->colorSpace());
395
396 dstDevice->clear();
397
398 using namespace GridIterationTools;
399
400 PaintDevicePolygonOp polygonOp(srcDevice, dstDevice);
401 RegularGridIndexesOp indexesOp(m_d->gridSize);
402 iterateThroughGrid<AlwaysCompletePolygonPolicy>(polygonOp, indexesOp,
403 m_d->gridSize,
404 m_d->originalPoints,
405 m_d->transformedPoints);
406}
407
409{
410 const qreal margin = 0.05;
411
416 const int maxSamplePoints = 200;
417 const int minStep = 3;
418 const int step = qMax(minStep, m_d->transformedPoints.size() / maxSamplePoints);
419 Q_UNUSED(step);
420
421 QVector<QPoint> samplePoints;
422 for (auto it = m_d->transformedPoints.constBegin(); it != m_d->transformedPoints.constEnd(); ++it) {
423 samplePoints << it->toPoint();
424 }
425
426 QRect resultRect = KisAlgebra2D::approximateRectFromPoints(samplePoints);
427 return KisAlgebra2D::blowRect(resultRect | rc, margin);
428}
429
430QRect KisLiquifyTransformWorker::approxNeedRect(const QRect &rc, const QRect &fullBounds)
431{
432 Q_UNUSED(rc);
433 return fullBounds;
434}
435
437{
438 KIS_SAFE_ASSERT_RECOVER_RETURN(t.type() <= QTransform::TxScale);
439
440 m_d->srcBounds = t.mapRect(m_d->srcBounds);
441
442 for (auto it = m_d->originalPoints.begin(); it != m_d->originalPoints.end(); ++it) {
443 *it = t.map(*it);
444 }
445 for (auto it = m_d->transformedPoints.begin(); it != m_d->transformedPoints.end(); ++it) {
446 *it = t.map(*it);
447 }
448}
449
450#include <functional>
451#include <QTransform>
452
453using PointMapFunction = std::function<QPointF (const QPointF&)>;
454
455
456PointMapFunction bindPointMapTransform(const QTransform &transform) {
457 using namespace std::placeholders;
458
459 typedef QPointF (QTransform::*MapFuncType)(const QPointF&) const;
460 return std::bind(static_cast<MapFuncType>(&QTransform::map), &transform, _1);
461}
462
463QImage KisLiquifyTransformWorker::runOnQImage(const QImage &srcImage,
464 const QPointF &srcImageOffset,
465 const QTransform &imageToThumbTransform,
466 QPointF *newOffset)
467{
468 KIS_ASSERT_RECOVER(m_d->originalPoints.size() == m_d->transformedPoints.size()) {
469 return QImage();
470 }
471
472 KIS_ASSERT_RECOVER(!srcImage.isNull()) {
473 return QImage();
474 }
475
476 KIS_ASSERT_RECOVER(srcImage.format() == QImage::Format_ARGB32) {
477 return QImage();
478 }
479
480 QVector<QPointF> originalPointsLocal(m_d->originalPoints);
481 QVector<QPointF> transformedPointsLocal(m_d->transformedPoints);
482
483 PointMapFunction mapFunc = bindPointMapTransform(imageToThumbTransform);
484
485 std::transform(originalPointsLocal.begin(), originalPointsLocal.end(),
486 originalPointsLocal.begin(), mapFunc);
487
488 std::transform(transformedPointsLocal.begin(), transformedPointsLocal.end(),
489 transformedPointsLocal.begin(), mapFunc);
490
491 QRectF dstBounds;
492 Q_FOREACH (const QPointF &pt, transformedPointsLocal) {
493 KisAlgebra2D::accumulateBounds(pt, &dstBounds);
494 }
495
496 const QRectF srcBounds(srcImageOffset, srcImage.size());
497 dstBounds |= srcBounds;
498
499 QPointF dstQImageOffset = dstBounds.topLeft();
500 *newOffset = dstQImageOffset;
501
502 QRect dstBoundsI = dstBounds.toAlignedRect();
503
504 QImage dstImage(dstBoundsI.size(), srcImage.format());
505 dstImage.fill(0);
506
507 GridIterationTools::QImagePolygonOp polygonOp(srcImage, dstImage, srcImageOffset, dstQImageOffset);
511 m_d->gridSize,
512 originalPointsLocal,
513 transformedPointsLocal);
514 return dstImage;
515}
516
517void KisLiquifyTransformWorker::toXML(QDomElement *e) const
518{
519 QDomDocument doc = e->ownerDocument();
520 QDomElement liqEl = doc.createElement("liquify_points");
521 e->appendChild(liqEl);
522
523 KisDomUtils::saveValue(&liqEl, "srcBounds", m_d->srcBounds);
524 KisDomUtils::saveValue(&liqEl, "originalPoints", m_d->originalPoints);
525 KisDomUtils::saveValue(&liqEl, "transformedPoints", m_d->transformedPoints);
526 KisDomUtils::saveValue(&liqEl, "pixelPrecision", m_d->pixelPrecision);
527 KisDomUtils::saveValue(&liqEl, "gridSize", m_d->gridSize);
528}
529
531{
532 QDomElement liquifyEl;
533
534 QRect srcBounds;
537 int pixelPrecision;
538 QSize gridSize;
539
540 bool result = false;
541
542
543 result =
544 KisDomUtils::findOnlyElement(e, "liquify_points", &liquifyEl) &&
545
546 KisDomUtils::loadValue(liquifyEl, "srcBounds", &srcBounds) &&
547 KisDomUtils::loadValue(liquifyEl, "originalPoints", &originalPoints) &&
548 KisDomUtils::loadValue(liquifyEl, "transformedPoints", &transformedPoints) &&
549 KisDomUtils::loadValue(liquifyEl, "pixelPrecision", &pixelPrecision) &&
550 KisDomUtils::loadValue(liquifyEl, "gridSize", &gridSize);
551
552 if (!result) {
553 warnKrita << "WARNING: Failed to load liquify worker from XML";
554 return new KisLiquifyTransformWorker(QRect(0,0,1024, 1024), 0, 8);
555 }
556
559
560 const int numPoints = originalPoints.size();
561
562 if (numPoints != transformedPoints.size() ||
563 numPoints != worker->m_d->originalPoints.size() ||
564 gridSize != worker->m_d->gridSize) {
565 warnKrita << "WARNING: Inconsistent number of points!";
566 warnKrita << ppVar(originalPoints.size());
569 warnKrita << ppVar(worker->m_d->originalPoints.size());
570 warnKrita << ppVar(worker->m_d->transformedPoints.size());
571 warnKrita << ppVar(worker->m_d->gridSize);
572
573 return worker;
574 }
575
576 for (int i = 0; i < numPoints; i++) {
577 worker->m_d->originalPoints[i] = originalPoints[i];
578 worker->m_d->transformedPoints[i] = transformedPoints[i];
579 }
580
581
582 return worker;
583}
virtual void clear()
const KoColorSpace * colorSpace() const
#define KIS_ASSERT_RECOVER(cond)
Definition kis_assert.h:55
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define KIS_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:75
const qreal eps
#define warnKrita
Definition kis_debug.h:87
#define ppVar(var)
Definition kis_debug.h:155
qreal kisDistance(const QPointF &pt1, const QPointF &pt2)
Definition kis_global.h:190
T pow2(const T &x)
Definition kis_global.h:166
std::function< QPointF(const QPointF &)> PointMapFunction
PointMapFunction bindPointMapTransform(const QTransform &transform)
void iterateThroughGrid(PolygonOp &polygonOp, IndexesOp &indexesOp, const QSize &gridSize, const QVector< QPointF > &originalPoints, const QVector< QPointF > &transformedPoints)
int pointToIndex(const QPoint &cellPt, const QSize &gridSize)
void processGrid(ProcessCell &cellOp, const QRect &srcBounds, const int pixelPrecision)
QSize calcGridSize(const QRect &srcBounds, const int pixelPrecision)
Rect blowRect(const Rect &rect, qreal coeff)
QRect approximateRectFromPoints(const QVector< QPoint > &points)
void accumulateBounds(const Point &pt, Rect *bounds)
qreal norm(const T &a)
bool fuzzyPointCompare(const QPointF &p1, const QPointF &p2)
void saveValue(QDomElement *parent, const QString &tag, const QSize &size)
bool findOnlyElement(const QDomElement &parent, const QString &tag, QDomElement *el, QStringList *errorMessages)
bool loadValue(const QDomElement &e, float *v)
void processPoint(int col, int row, int prevCol, int prevRow, int colIndex, int rowIndex)
void rotatePoints(const QPointF &base, qreal angle, qreal sigma, bool useWashMode, qreal flow)
Private(const QRect &_srcBounds, KoUpdater *_progress, int _pixelPrecision)
KisLiquifyTransformWorker(const QRect &srcBounds, KoUpdater *progress, int pixelPrecision=8)
void translate(const QPointF &offset)
void undoPoints(const QPointF &base, qreal amount, qreal sigma)
void translateDstSpace(const QPointF &offset)
QImage runOnQImage(const QImage &srcImage, const QPointF &srcImageOffset, const QTransform &imageToThumbTransform, QPointF *newOffset)
void transformSrcAndDst(const QTransform &t)
void scalePoints(const QPointF &base, qreal scale, qreal sigma, bool useWashMode, qreal flow)
void processTransformedPixelsWash(ProcessOp op, const QPointF &base, qreal sigma, qreal flow)
static KisLiquifyTransformWorker * fromXML(const QDomElement &e)
const QScopedPointer< Private > m_d
void processTransformedPixelsBuildUp(ProcessOp op, const QPointF &base, qreal sigma)
void processTransformedPixels(ProcessOp op, const QPointF &base, qreal sigma, bool useWashMode, qreal flow)
bool operator==(const KisLiquifyTransformWorker &other) const
void translatePoints(const QPointF &base, const QPointF &offset, qreal sigma, bool useWashMode, qreal flow)
QRect approxNeedRect(const QRect &rc, const QRect &fullBounds)
void run(KisPaintDeviceSP srcDevice, KisPaintDeviceSP dstDevice)
QPointF operator()(const QPointF &pt, const QPointF &base, const QPointF &diff, qreal lambda)
static const qreal maxDistCoeff
static const qreal maxDistCoeff
QPointF operator()(const QPointF &pt, const QPointF &base, const QPointF &diff, qreal lambda)
TranslateOp(const QPointF &offset)
QPointF operator()(const QPointF &pt, const QPointF &base, const QPointF &diff, qreal lambda)
static const qreal maxDistCoeff