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>
10#include <Eigen/Dense>
11#include <cmath>
13#include "kis_dom_utils.h"
14#include "krita_utils.h"
15#include "KisSpatialContainer.h"
16
17
19{
20 Private(const QRect &_srcBounds,
21 KoUpdater *_progress,
22 int _pixelPrecision)
23 : srcBounds(_srcBounds)
24 , originalPointsContainer(_srcBounds)
25 , transformedPointsContainer(_srcBounds)
26 , progress(_progress)
27 , pixelPrecision(_pixelPrecision)
28 {
29 }
30
31 QRect srcBounds;
32
35
38
40
43 QSize gridSize;
44
46
47 struct MapIndexesOp;
48
49 template <class ProcessOp>
51 const QPointF &base,
52 qreal sigma);
53
54 template <class ProcessOp>
56 const QPointF &base,
57 qreal sigma,
58 qreal flow);
59
60 template <class ProcessOp>
61 void processTransformedPixels(ProcessOp op,
62 const QPointF &base,
63 qreal sigma,
64 bool useWashMode,
65 qreal flow);
66};
67
69 KoUpdater *progress,
70 int pixelPrecision)
71 : m_d(new Private(srcBounds, progress, pixelPrecision))
72{
74
75 // TODO: implement 'progress' stuff
76 m_d->preparePoints();
77}
78
83
87
89{
90 bool result =
91 m_d->srcBounds == other.m_d->srcBounds &&
92 m_d->pixelPrecision == other.m_d->pixelPrecision &&
93 m_d->gridSize == other.m_d->gridSize &&
94 m_d->originalPoints.size() == other.m_d->originalPoints.size() &&
95 m_d->transformedPoints.size() == other.m_d->transformedPoints.size();
96
97 if (!result) return false;
98
99 const qreal eps = 1e-6;
100
101 result =
102 KisAlgebra2D::fuzzyPointCompare(m_d->originalPoints, other.m_d->originalPoints, eps) &&
103 KisAlgebra2D::fuzzyPointCompare(m_d->transformedPoints, other.m_d->transformedPoints, eps);
104
105 return result;
106}
107
109{
110 const qreal eps = 1e-6;
111 return KisAlgebra2D::fuzzyPointCompare(m_d->originalPoints, m_d->transformedPoints, eps);
112}
113
115{
116 return GridIterationTools::pointToIndex(cellPt, m_d->gridSize);
117}
118
120{
121 return m_d->gridSize;
122}
123
125{
126 return m_d->originalPoints;
127}
128
130{
131 return m_d->transformedPoints;
132}
133
135{
136 AllPointsFetcherOp(QRectF srcRect) : m_srcRect(srcRect) {}
137
138 inline void processPoint(int col, int row,
139 int prevCol, int prevRow,
140 int colIndex, int rowIndex) {
141
142 Q_UNUSED(prevCol);
143 Q_UNUSED(prevRow);
144 Q_UNUSED(colIndex);
145 Q_UNUSED(rowIndex);
146
147 QPointF pt(col, row);
148 m_points << pt;
149 }
150
151 inline void nextLine() {
152 }
153
155 QRectF m_srcRect;
156};
157
158void KisLiquifyTransformWorker::Private::preparePoints()
159{
160 gridSize =
161 GridIterationTools::calcGridSize(srcBounds, pixelPrecision);
162
163 AllPointsFetcherOp pointsOp(srcBounds);
164 GridIterationTools::processGrid(pointsOp, srcBounds, pixelPrecision);
165
166 const int numPoints = pointsOp.m_points.size();
167
168 KIS_ASSERT_RECOVER_RETURN(numPoints == gridSize.width() * gridSize.height());
169
170 originalPoints = pointsOp.m_points;
171 transformedPoints = pointsOp.m_points;
172
173 originalPointsContainer.initializeWithGridPoints(srcBounds, pixelPrecision);
174 transformedPointsContainer.initializeWithGridPoints(srcBounds, pixelPrecision);
175
176}
177
178void KisLiquifyTransformWorker::translate(const QPointF &offset)
179{
180 KIS_ASSERT_RECOVER_RETURN(m_d->originalPoints.size() ==
181 m_d->transformedPoints.size());
182
183 // TODO: make it within Spatial Container, either a hidden offset, or just offsetting all points at once
184 // and benchmark
185 for (int i = 0; i < m_d->transformedPoints.count(); i++) {
186 m_d->originalPointsContainer.movePoint(i, m_d->originalPoints[i], m_d->originalPoints[i] + offset);
187 m_d->transformedPointsContainer.movePoint(i, m_d->transformedPoints[i], m_d->transformedPoints[i] + offset);
188
189 m_d->originalPoints[i] += offset;
190 m_d->transformedPoints[i] += offset;
191 }
192
193 m_d->accumulatedBrushStrokes.translate(offset);
194}
195
197{
198 // TODO: make it within Spatial Container, either a hidden offset, or just offsetting all points at once
199 // and benchmark
200 for (int i = 0; i < m_d->transformedPoints.count(); i++) {
201 m_d->transformedPointsContainer.movePoint(i, m_d->transformedPoints[i], m_d->transformedPoints[i] + offset);
202 m_d->transformedPoints[i] += offset;
203 }
204}
205
207 qreal amount,
208 qreal sigma)
209{
210 const qreal maxDistCoeff = 3.0;
211 const qreal maxDist = maxDistCoeff * sigma;
212
213 KIS_ASSERT_RECOVER_RETURN(m_d->originalPoints.size() ==
214 m_d->transformedPoints.size());
215
216 QVector<int> indexes;
217 m_d->transformedPointsContainer.findAllInRange(indexes, base, maxDist);
218 for (int i = 0; i < indexes.count(); i++) {
219
220 QPointF diff = m_d->transformedPoints[indexes[i]] - base;
221 qreal dist = KisAlgebra2D::norm(diff);
222 qreal lambda = exp(-0.5 * pow2(dist / sigma));
223 lambda *= amount;
224
225 QPointF oldPosition = m_d->transformedPoints[indexes[i]];
226 m_d->transformedPoints[indexes[i]] = m_d->originalPoints[indexes[i]] * lambda + m_d->transformedPoints[indexes[i]] * (1.0 - lambda);
227
228 m_d->transformedPointsContainer.movePoint(indexes[i], oldPosition, m_d->transformedPoints[indexes[i]]);
229 }
230}
231
232namespace {
233
234struct PointUpdate
235{
236 int index = -1;
237 QPointF oldPosition;
238 QPointF newPosition;
239};
240
241struct RestoreShapeSample
242{
243 int index = -1;
244 qreal weight = 0.0;
245 qreal lambda = 0.0;
246};
247
248Eigen::Vector2d toEigenPoint(const QPointF &pt)
249{
250 return Eigen::Vector2d(pt.x(), pt.y());
251}
252
253QPointF fromEigenPoint(const Eigen::Vector2d &pt)
254{
255 return QPointF(pt.x(), pt.y());
256}
257
258bool isFinite(const Eigen::Matrix2d &m)
259{
260 return std::isfinite(m(0, 0)) &&
261 std::isfinite(m(0, 1)) &&
262 std::isfinite(m(1, 0)) &&
263 std::isfinite(m(1, 1));
264}
265
266}
267
268// A restore-shape dab determines its samples and Gaussian falloff from the
269// points' positions at the beginning of the dab. The centroid correction keeps
270// this fixed weighted sample set centered; subsequent dabs query the moved
271// points and calculate their falloff again.
273 qreal amount,
274 qreal sigma,
275 bool preserveRotation,
276 bool preserveScale,
277 bool preserveStretch)
278{
279 const qreal maxDistCoeff = 3.0;
280 const qreal maxDist = maxDistCoeff * sigma;
281
282 KIS_ASSERT_RECOVER_RETURN(m_d->originalPoints.size() ==
283 m_d->transformedPoints.size());
284
285 QRectF clipRect(base.x() - maxDist, base.y() - maxDist,
286 2 * maxDist, 2 * maxDist);
287 m_d->accumulatedBrushStrokes |= kisGrowRect(clipRect, m_d->pixelPrecision);
288
289 QVector<int> indexes;
290 m_d->transformedPointsContainer.findAllInRange(indexes, base, maxDist);
291
293 samples.reserve(indexes.count());
294
295 qreal weightSum = 0.0;
296 Eigen::Vector2d originalCentroid(0.0, 0.0);
297 Eigen::Vector2d transformedCentroid(0.0, 0.0);
298
299 for (int i = 0; i < indexes.count(); i++) {
300 const int index = indexes[i];
301
302 const QPointF diff = m_d->transformedPoints[index] - base;
303 const qreal dist = KisAlgebra2D::norm(diff);
304 if (dist > maxDist) continue;
305
306 const qreal weight = exp(-0.5 * pow2(dist / sigma));
307 const qreal lambda = qBound<qreal>(0.0, weight * amount, 1.0);
308 if (lambda <= 0.0) continue;
309
310 samples << RestoreShapeSample{index, weight, lambda};
311
312 originalCentroid += weight * toEigenPoint(m_d->originalPoints[index]);
313 transformedCentroid += weight * toEigenPoint(m_d->transformedPoints[index]);
314 weightSum += weight;
315 }
316
317 if (samples.count() < 3 || weightSum <= 1e-12) return;
318
319 originalCentroid /= weightSum;
320 transformedCentroid /= weightSum;
321
322 Eigen::Matrix2d originalMoment = Eigen::Matrix2d::Zero();
323 Eigen::Matrix2d transformedOriginalMoment = Eigen::Matrix2d::Zero();
324
325 for (int i = 0; i < samples.count(); i++) {
326 const RestoreShapeSample &sample = samples[i];
327
328 const Eigen::Vector2d originalOffset =
329 toEigenPoint(m_d->originalPoints[sample.index]) - originalCentroid;
330 const Eigen::Vector2d transformedOffset =
331 toEigenPoint(m_d->transformedPoints[sample.index]) - transformedCentroid;
332
333 originalMoment += sample.weight * originalOffset * originalOffset.transpose();
334 transformedOriginalMoment += sample.weight * transformedOffset * originalOffset.transpose();
335 }
336
337 if (qAbs(originalMoment.determinant()) < 1e-12) return;
338
339 const Eigen::Matrix2d affine =
340 transformedOriginalMoment * originalMoment.inverse();
341
342 if (!isFinite(affine)) return;
343
344 Eigen::JacobiSVD<Eigen::Matrix2d> svd(affine, Eigen::ComputeFullU | Eigen::ComputeFullV);
345 Eigen::Matrix2d rotation = svd.matrixU() * svd.matrixV().transpose();
346
347 if (rotation.determinant() < 0.0) {
348 Eigen::Matrix2d u = svd.matrixU();
349 u.col(1) *= -1.0;
350 rotation = u * svd.matrixV().transpose();
351 }
352
353 if (!isFinite(rotation)) return;
354
355 const Eigen::Vector2d singularValues = svd.singularValues();
356 const qreal uniformScale =
357 preserveScale ? qMax<qreal>(1e-6, 0.5 * (singularValues.x() + singularValues.y())) : 1.0;
358
359 Eigen::Matrix2d targetLinear = Eigen::Matrix2d::Identity();
360 if (preserveRotation) {
361 targetLinear = rotation;
362 }
363 targetLinear *= uniformScale;
364
365 if (preserveStretch) {
366 const Eigen::Matrix2d stretchInRotationSpace = rotation.transpose() * affine;
367 qreal stretchX = qMax<qreal>(1e-6, qAbs(stretchInRotationSpace(0, 0)));
368 qreal stretchY = qMax<qreal>(1e-6, qAbs(stretchInRotationSpace(1, 1)));
369 const qreal stretchNormalization =
370 preserveScale ? uniformScale : qMax<qreal>(1e-6, std::sqrt(stretchX * stretchY));
371
372 Eigen::Matrix2d stretch = Eigen::Matrix2d::Identity();
373 stretch(0, 0) = stretchX / stretchNormalization;
374 stretch(1, 1) = stretchY / stretchNormalization;
375 targetLinear *= stretch;
376 }
377
378 QVector<PointUpdate> updates;
379 updates.reserve(samples.count());
380
381 Eigen::Vector2d updatedCentroid(0.0, 0.0);
382 qreal updatedWeightSum = 0.0;
383
384 for (int i = 0; i < samples.count(); i++) {
385 const RestoreShapeSample &sample = samples[i];
386 const Eigen::Vector2d oldPosition = toEigenPoint(m_d->transformedPoints[sample.index]);
387 const Eigen::Vector2d originalOffset =
388 toEigenPoint(m_d->originalPoints[sample.index]) - originalCentroid;
389 const Eigen::Vector2d targetPosition =
390 transformedCentroid + targetLinear * originalOffset;
391 const Eigen::Vector2d newPosition =
392 oldPosition * (1.0 - sample.lambda) + targetPosition * sample.lambda;
393
394 updates << PointUpdate{sample.index,
395 m_d->transformedPoints[sample.index],
396 fromEigenPoint(newPosition)};
397
398 updatedCentroid += sample.weight * newPosition;
399 updatedWeightSum += sample.weight;
400 }
401
402 if (updatedWeightSum <= 1e-12) return;
403 updatedCentroid /= updatedWeightSum;
404
405 const QPointF centroidCorrection = fromEigenPoint(transformedCentroid - updatedCentroid);
406
407 for (int i = 0; i < updates.count(); i++) {
408 PointUpdate &update = updates[i];
409 update.newPosition += centroidCorrection;
410 m_d->transformedPoints[update.index] = update.newPosition;
411 m_d->transformedPointsContainer.movePoint(update.index,
412 update.oldPosition,
413 update.newPosition);
414 }
415}
416
417template <class ProcessOp>
418void KisLiquifyTransformWorker::Private::
419processTransformedPixelsBuildUp(ProcessOp op,
420 const QPointF &base,
421 qreal sigma)
422{
423 const qreal maxDist = ProcessOp::maxDistCoeff * sigma;
424 QRectF clipRect(base.x() - maxDist, base.y() - maxDist,
425 2 * maxDist, 2 * maxDist);
426
427 accumulatedBrushStrokes |= kisGrowRect(clipRect, pixelPrecision);
428
429 QVector<int> indexes;
430 transformedPointsContainer.findAllInRange(indexes, base, maxDist);
431
432 for (int i = 0; i < indexes.count(); i++) {
433
434 QPointF diff = transformedPoints[indexes[i]] - base;
435 qreal dist = KisAlgebra2D::norm(diff);
436 if (dist > maxDist) continue;
437
438 const qreal lambda = exp(-0.5 * pow2(dist / sigma));
439 QPointF oldPosition = transformedPoints[indexes[i]];
440 transformedPoints[indexes[i]] = op(transformedPoints[indexes[i]], base, diff, lambda);
441
442
443 transformedPointsContainer.movePoint(indexes[i], oldPosition, transformedPoints[indexes[i]]);
444
445 }
446}
447
448template <class ProcessOp>
449void KisLiquifyTransformWorker::Private::
450processTransformedPixelsWash(ProcessOp op,
451 const QPointF &base,
452 qreal sigma,
453 qreal flow)
454{
455 const qreal maxDist = ProcessOp::maxDistCoeff * sigma;
456 QRectF clipRect(base.x() - maxDist, base.y() - maxDist,
457 2 * maxDist, 2 * maxDist);
458
459 accumulatedBrushStrokes |= kisGrowRect(clipRect, pixelPrecision);
460
461 KIS_ASSERT_RECOVER_RETURN(originalPoints.size() ==
462 transformedPoints.size());
463
464 // TODO: remove the originalPointsContainer entirely, and use GridIterationTools to figure out indexes instead
465 // and add unit tests for it
466
467 QVector<int> indexes;
468 originalPointsContainer.findAllInRange(indexes, base, maxDist);
469 for (int i = 0; i < indexes.count(); i++) {
470
471 QPointF diff = originalPoints[indexes[i]] - base;
472 qreal dist = KisAlgebra2D::norm(diff);
473
474 const qreal lambda = exp(-0.5 * pow2(dist / sigma));
475 QPointF dstPt = op(originalPoints[indexes[i]], base, diff, lambda);
476
477 if (kisDistance(dstPt, originalPoints[indexes[i]]) > kisDistance(transformedPoints[indexes[i]], originalPoints[indexes[i]])) {
478 QPointF oldPosition = transformedPoints[indexes[i]];
479 transformedPoints[indexes[i]] = (1.0 - flow) * transformedPoints[indexes[i]] + flow * dstPt;
480
481 transformedPointsContainer.movePoint(indexes[i], oldPosition, transformedPoints[indexes[i]]);
482 }
483 }
484}
485
486template <class ProcessOp>
487void KisLiquifyTransformWorker::Private::
488processTransformedPixels(ProcessOp op,
489 const QPointF &base,
490 qreal sigma,
491 bool useWashMode,
492 qreal flow)
493{
494 if (useWashMode) {
495 processTransformedPixelsWash(op, base, sigma, flow);
496 } else {
497 processTransformedPixelsBuildUp(op, base, sigma);
498 }
499}
500
502{
503 TranslateOp(const QPointF &offset) : m_offset(offset) {}
504
505 QPointF operator() (const QPointF &pt,
506 const QPointF &base,
507 const QPointF &diff,
508 qreal lambda)
509 {
510 Q_UNUSED(base);
511 Q_UNUSED(diff);
512 return pt + lambda * m_offset;
513 }
514
515 static const qreal maxDistCoeff;
516
517 QPointF m_offset;
518};
519
520const qreal TranslateOp::maxDistCoeff = 3.0;
521
523{
524 ScaleOp(qreal scale) : m_scale(scale) {}
525
526 QPointF operator() (const QPointF &pt,
527 const QPointF &base,
528 const QPointF &diff,
529 qreal lambda)
530 {
531 Q_UNUSED(pt);
532 Q_UNUSED(diff);
533 return base + (1.0 + m_scale * lambda) * diff;
534 }
535
536 static const qreal maxDistCoeff;
537
538 qreal m_scale;
539};
540
541const qreal ScaleOp::maxDistCoeff = 3.0;
542
544{
545 RotateOp(qreal angle) : m_angle(angle) {}
546
547 QPointF operator() (const QPointF &pt,
548 const QPointF &base,
549 const QPointF &diff,
550 qreal lambda)
551 {
552 Q_UNUSED(pt);
553
554 const qreal angle = m_angle * lambda;
555 const qreal sinA = std::sin(angle);
556 const qreal cosA = std::cos(angle);
557
558 qreal x = cosA * diff.x() + sinA * diff.y();
559 qreal y = -sinA * diff.x() + cosA * diff.y();
560
561 return base + QPointF(x, y);
562 }
563
564 static const qreal maxDistCoeff;
565
566 qreal m_angle;
567};
568
569const qreal RotateOp::maxDistCoeff = 3.0;
570
572 const QPointF &offset,
573 qreal sigma,
574 bool useWashMode,
575 qreal flow)
576{
577 TranslateOp op(offset);
578 m_d->processTransformedPixels(op, base, sigma, useWashMode, flow);
579}
580
582 qreal scale,
583 qreal sigma,
584 bool useWashMode,
585 qreal flow)
586{
587 ScaleOp op(scale);
588 m_d->processTransformedPixels(op, base, sigma, useWashMode, flow);
589}
590
592 qreal angle,
593 qreal sigma,
594 bool useWashMode,
595 qreal flow)
596{
597 RotateOp op(angle);
598 m_d->processTransformedPixels(op, base, sigma, useWashMode, flow);
599}
600
602{
603 KIS_SAFE_ASSERT_RECOVER_RETURN(*srcDevice->colorSpace() == *dstDevice->colorSpace());
604
605 dstDevice->clear();
606
607 using namespace GridIterationTools;
608 QRect correctSubGrid = calculateCorrectSubGrid(m_d->srcBounds, m_d->pixelPrecision, m_d->accumulatedBrushStrokes, m_d->gridSize);
609
610 PaintDevicePolygonOp polygonOp(srcDevice, dstDevice);
611 RegularGridIndexesOp indexesOp(m_d->gridSize);
612
613 bool canMergeRects = GridIterationTools::canProcessRectsInRandomOrder(indexesOp, m_d->transformedPoints, correctSubGrid);
614 polygonOp.setCanMergeRects(canMergeRects);
615
616#ifdef DEBUG_PAINTING_POLYGONS
617 polygonOp.setDebugColor(Qt::red);
618#endif
619
620 iterateThroughGrid<AlwaysCompletePolygonPolicy>(polygonOp, indexesOp,
621 m_d->gridSize,
622 m_d->originalPoints,
623 m_d->transformedPoints,
624 correctSubGrid);
625 QList<QRectF> areasToCopy = cutOutSubgridFromBounds(correctSubGrid, m_d->srcBounds, m_d->gridSize, m_d->originalPoints);
626#ifdef DEBUG_PAINTING_POLYGONS
627 QList<QColor> colors = {Qt::blue, Qt::green, Qt::yellow, Qt::black};
628#endif
629 for (int i = 0; i < areasToCopy.length(); i++) {
630#ifdef DEBUG_PAINTING_POLYGONS
631 polygonOp.setDebugColor(colors[i]);
632#endif
633 polygonOp.fastCopyArea(areasToCopy[i].toRect(), false);
634 }
635}
636
638{
639 const qreal margin = 0.05;
640 QRect resultRect = m_d->transformedPointsContainer.exactBounds().toRect();
641 return KisAlgebra2D::blowRect(resultRect | rc, margin);
642}
643
644QRect KisLiquifyTransformWorker::approxNeedRect(const QRect &rc, const QRect &fullBounds)
645{
646 Q_UNUSED(rc);
647 return fullBounds;
648}
649
651{
652 return m_d->accumulatedBrushStrokes;
653}
654
656{
657 KIS_SAFE_ASSERT_RECOVER_RETURN(t.type() <= QTransform::TxScale);
658
659 m_d->srcBounds = t.mapRect(m_d->srcBounds);
660
661 // TODO: do it within Spatial Container
662 for (int i = 0; i < m_d->transformedPoints.count(); i++) {
663 m_d->originalPointsContainer.movePoint(i, m_d->originalPoints[i], t.map(m_d->originalPoints[i]));
664 m_d->transformedPointsContainer.movePoint(i, m_d->transformedPoints[i], t.map(m_d->transformedPoints[i]));
665
666 m_d->originalPoints[i] = t.map(m_d->originalPoints[i]);
667 m_d->transformedPoints[i] = t.map(m_d->transformedPoints[i]);
668 }
669 m_d->accumulatedBrushStrokes = t.map(m_d->accumulatedBrushStrokes).boundingRect();
670 if (t == QTransform::fromScale(t.m11(), t.m22()) && t.m11() == t.m22()) {
671 m_d->pixelPrecision *= t.m11();
672 KIS_SAFE_ASSERT_RECOVER(m_d->pixelPrecision > 0) { m_d->pixelPrecision = 1; }
673 KIS_SAFE_ASSERT_RECOVER(QList<int>({1, 2, 4, 8, 16}).contains(m_d->pixelPrecision) || m_d->pixelPrecision%16 == 0) { m_d->pixelPrecision = 1; }
674 // should check if pixelPrecision is a power of 2, but that's more complicated
675 }
676}
677
678#include <functional>
679#include <QTransform>
680
681using PointMapFunction = std::function<QPointF (const QPointF&)>;
682
683
684PointMapFunction bindPointMapTransform(const QTransform &transform) {
685 using namespace std::placeholders;
686
687 typedef QPointF (QTransform::*MapFuncType)(const QPointF&) const;
688 return std::bind(static_cast<MapFuncType>(&QTransform::map), &transform, _1);
689}
690
691QImage KisLiquifyTransformWorker::runOnQImage(const QImage &srcImage,
692 const QPointF &srcImageOffset,
693 const QTransform &imageToThumbTransform,
694 QPointF *newOffset)
695{
696 KIS_ASSERT_RECOVER(m_d->originalPoints.size() == m_d->transformedPoints.size()) {
697 return QImage();
698 }
699
700 KIS_ASSERT_RECOVER(!srcImage.isNull()) {
701 return QImage();
702 }
703
704 KIS_ASSERT_RECOVER(srcImage.format() == QImage::Format_ARGB32) {
705 return QImage();
706 }
707
708 QVector<QPointF> originalPointsLocal(m_d->originalPoints);
709 QVector<QPointF> transformedPointsLocal(m_d->transformedPoints);
710
711 PointMapFunction mapFunc = bindPointMapTransform(imageToThumbTransform);
712
713 std::transform(originalPointsLocal.begin(), originalPointsLocal.end(),
714 originalPointsLocal.begin(), mapFunc);
715
716 std::transform(transformedPointsLocal.begin(), transformedPointsLocal.end(),
717 transformedPointsLocal.begin(), mapFunc);
718
719 QRectF dstBounds;
720 Q_FOREACH (const QPointF &pt, transformedPointsLocal) {
721 KisAlgebra2D::accumulateBounds(pt, &dstBounds);
722 }
723
724 const QRectF srcBounds(srcImageOffset, srcImage.size());
725 dstBounds |= srcBounds;
726
727 QPointF dstQImageOffset = dstBounds.topLeft();
728 *newOffset = dstQImageOffset;
729
730 QRect dstBoundsI = dstBounds.toAlignedRect();
731
732 QImage dstImage(dstBoundsI.size(), srcImage.format());
733 dstImage.fill(0);
734
735 GridIterationTools::QImagePolygonOp polygonOp(srcImage, dstImage, srcImageOffset, dstQImageOffset);
737
738
739 QRect correctSubGrid = GridIterationTools::calculateCorrectSubGrid(m_d->srcBounds, m_d->pixelPrecision, m_d->accumulatedBrushStrokes, m_d->gridSize);
740 bool canMergeRects = GridIterationTools::canProcessRectsInRandomOrder(indexesOp, m_d->transformedPoints, correctSubGrid);
741 polygonOp.setCanMergeRects(canMergeRects);
742
743
744 GridIterationTools::iterateThroughGrid<GridIterationTools::AlwaysCompletePolygonPolicy>(polygonOp, indexesOp,
745 m_d->gridSize,
746 originalPointsLocal,
747 transformedPointsLocal,
748 correctSubGrid);
749
750
751 QList<QRectF> areasToCopy = GridIterationTools::cutOutSubgridFromBounds(correctSubGrid, m_d->srcBounds, m_d->gridSize, m_d->originalPoints);
752 polygonOp.setCanMergeRects(false);
753 const qreal eps = 0.001;
754 for (int i = 0; i < areasToCopy.length(); i++) {
755 QPolygonF transformed = imageToThumbTransform.map(QPolygonF(areasToCopy[i]));
757 polygonOp.fastCopyArea(transformed.boundingRect().toRect());
758 } else {
759 polygonOp.operator()(transformed, transformed);
760 }
761 }
762 return dstImage;
763}
764
765
766void KisLiquifyTransformWorker::toXML(QDomElement *e) const
767{
768 QDomDocument doc = e->ownerDocument();
769 QDomElement liqEl = doc.createElement("liquify_points");
770 e->appendChild(liqEl);
771
772 KisDomUtils::saveValue(&liqEl, "srcBounds", m_d->srcBounds);
773 KisDomUtils::saveValue(&liqEl, "originalPoints", m_d->originalPoints);
774 KisDomUtils::saveValue(&liqEl, "transformedPoints", m_d->transformedPoints);
775 KisDomUtils::saveValue(&liqEl, "pixelPrecision", m_d->pixelPrecision);
776 KisDomUtils::saveValue(&liqEl, "gridSize", m_d->gridSize);
777}
778
780{
781 QDomElement liquifyEl;
782
783 QRect srcBounds;
786 int pixelPrecision;
787 QSize gridSize;
788
789 bool result = false;
790
791
792 result =
793 KisDomUtils::findOnlyElement(e, "liquify_points", &liquifyEl) &&
794
795 KisDomUtils::loadValue(liquifyEl, "srcBounds", &srcBounds) &&
796 KisDomUtils::loadValue(liquifyEl, "originalPoints", &originalPoints) &&
797 KisDomUtils::loadValue(liquifyEl, "transformedPoints", &transformedPoints) &&
798 KisDomUtils::loadValue(liquifyEl, "pixelPrecision", &pixelPrecision) &&
799 KisDomUtils::loadValue(liquifyEl, "gridSize", &gridSize);
800
801 if (!result) {
802 warnKrita << "WARNING: Failed to load liquify worker from XML";
803 return new KisLiquifyTransformWorker(QRect(0,0,1024, 1024), 0, 8);
804 }
805
808
809 const int numPoints = originalPoints.size();
810
811 if (numPoints != transformedPoints.size() ||
812 numPoints != worker->m_d->originalPoints.size() ||
813 gridSize != worker->m_d->gridSize) {
814 warnKrita << "WARNING: Inconsistent number of points!";
815 warnKrita << ppVar(originalPoints.size());
818 warnKrita << ppVar(worker->m_d->originalPoints.size());
819 warnKrita << ppVar(worker->m_d->transformedPoints.size());
820 warnKrita << ppVar(worker->m_d->gridSize);
821
822 return worker;
823 }
824
825 QRectF changedRect = QRectF();
826
827 for (int i = 0; i < numPoints; i++) {
828 worker->m_d->originalPoints[i] = originalPoints[i];
829 worker->m_d->transformedPoints[i] = transformedPoints[i];
833 }
834 }
835 changedRect = kisGrowRect(changedRect, pixelPrecision);
836
837 worker->m_d->transformedPointsContainer.initializeWith(worker->m_d->transformedPoints);
838 worker->m_d->originalPointsContainer.initializeWith(worker->m_d->originalPoints);
839
840 worker->m_d->accumulatedBrushStrokes = changedRect;
841
842
843 return worker;
844}
qreal u
virtual void clear()
const KoColorSpace * colorSpace() const
#define KIS_ASSERT_RECOVER(cond)
Definition kis_assert.h:55
#define KIS_SAFE_ASSERT_RECOVER(cond)
Definition kis_assert.h:126
#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:91
#define ppVar(var)
Definition kis_debug.h:159
qreal kisDistance(const QPointF &pt1, const QPointF &pt2)
Definition kis_global.h:190
T kisGrowRect(const T &rect, U offset)
Definition kis_global.h:186
T pow2(const T &x)
Definition kis_global.h:166
std::function< QPointF(const QPointF &)> PointMapFunction
PointMapFunction bindPointMapTransform(const QTransform &transform)
bool canProcessRectsInRandomOrder(IndexesOp &indexesOp, const QVector< QPointF > &transformedPoints, QSize grid)
QList< QRectF > cutOutSubgridFromBounds(QRect subGrid, QRect srcBounds, const QSize &gridSize, const QVector< QPointF > &originalPoints)
QRect calculateCorrectSubGrid(QRect originalBoundsForGrid, int pixelPrecision, QRectF currentBounds, QSize gridSize)
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)
bool isPolygonPixelAlignedRect(const Polygon &poly, Difference tolerance)
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)
void restoreShapePoints(const QPointF &base, qreal amount, qreal sigma, bool preserveRotation, bool preserveScale, bool preserveStretch)
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