Commit 241ddd4e authored by Dmitry Kazakov's avatar Dmitry Kazakov
Browse files

Make transform mask return a change rect for the non-affine transoforms as well

We have to use really rough approximations for calculating the change
rect of cage, warp and liquify transformation. The main problem is that
there is no explicit f(x) solution for most of them (except for warp).
So we should be a bit tricky.

CCBUG:347755
parent 6cc9dea6
......@@ -52,7 +52,7 @@
#ifdef DEBUG_MERGER
#define DEBUG_NODE_ACTION(message, type, leaf, rect) \
dbgKrita << message << type << ":" << leaf->node()->name() << rect
qDebug() << message << type << ":" << leaf->node()->name() << rect
#else
#define DEBUG_NODE_ACTION(message, type, leaf, rect)
#endif
......
......@@ -297,7 +297,7 @@ protected:
virtual void registerChangeRect(KisProjectionLeafSP leaf, NodePosition position) {
// We do not work with masks here. It is KisLayer's job.
if(!leaf->isLayer()) return;
if(!leaf->visible()) return;
if(!(position & N_FILTHY) && !leaf->visible()) return;
QRect currentChangeRect = leaf->projectionPlane()->changeRect(m_resultChangeRect,
convertPositionToFilthy(position));
......
......@@ -27,6 +27,7 @@
#include "kis_selection.h"
#include "kis_painter.h"
#include "kis_image.h"
#include "krita_utils.h"
#include <qnumeric.h>
......@@ -281,6 +282,65 @@ struct KisCageTransformWorker::Private::MapIndexesOp {
QPolygonF m_srcCagePolygon;
};
QRect KisCageTransformWorker::approxChangeRect(const QRect &rc)
{
const int margin = 0.30;
QVector<QPointF> cageSamplePoints;
const int minStep = 3;
const int maxSamples = 200;
const int totalPixels = rc.width() * rc.height();
const int realStep = qMax(minStep, totalPixels / maxSamples);
const QPolygonF cagePolygon(m_d->origCage);
for (int i = 0; i < totalPixels; i += realStep) {
const int x = rc.x() + i % rc.width();
const int y = rc.y() + i / rc.width();
const QPointF pt(x, y);
if (cagePolygon.containsPoint(pt, Qt::OddEvenFill)) {
cageSamplePoints << pt;
}
}
if (cageSamplePoints.isEmpty()) {
return rc;
}
KisGreenCoordinatesMath cage;
cage.precalculateGreenCoordinates(m_d->origCage, cageSamplePoints);
cage.generateTransformedCageNormals(m_d->transfCage);
const int numValidPoints = cageSamplePoints.size();
QVector<QPointF> transformedPoints(numValidPoints);
int failedPoints = 0;
for (int i = 0; i < numValidPoints; i++) {
transformedPoints[i] = cage.transformedPoint(i, m_d->transfCage);
if (qIsNaN(transformedPoints[i].x()) ||
qIsNaN(transformedPoints[i].y())) {
transformedPoints[i] = cageSamplePoints[i];
failedPoints++;
}
}
QRect resultRect =
KritaUtils::approximateRectFromPoints(transformedPoints).toAlignedRect();
return KisAlgebra2D::blowRect(resultRect | rc, margin);
}
QRect KisCageTransformWorker::approxNeedRect(const QRect &rc, const QRect &fullBounds)
{
Q_UNUSED(rc);
return fullBounds;
}
void KisCageTransformWorker::run()
{
if (m_d->isGridEmpty()) return;
......
......@@ -45,6 +45,9 @@ public:
void setTransformedCage(const QVector<QPointF> &transformedCage);
void run();
QRect approxChangeRect(const QRect &rc);
QRect approxNeedRect(const QRect &rc, const QRect &fullBounds);
QImage runOnQImage(QPointF *newOffset);
private:
......
......@@ -20,6 +20,7 @@
#include "kis_grid_interpolation_tools.h"
#include "kis_dom_utils.h"
#include "krita_utils.h"
struct Q_DECL_HIDDEN KisLiquifyTransformWorker::Private
......@@ -428,6 +429,33 @@ void KisLiquifyTransformWorker::run(KisPaintDeviceSP device)
m_d->transformedPoints);
}
QRect KisLiquifyTransformWorker::approxChangeRect(const QRect &rc)
{
const int margin = 0.05;
/**
* Here we just return the full area occupied by the transformed grid.
* We sample grid points for not doing too much work.
*/
const int maxSamplePoints = 200;
const int minStep = 3;
const int step = qMax(minStep, m_d->transformedPoints.size() / maxSamplePoints);
QVector<QPoint> samplePoints;
for (int i = 0; i < m_d->transformedPoints.size(); i += step) {
samplePoints << m_d->transformedPoints[i].toPoint();
}
QRect resultRect = KritaUtils::approximateRectFromPoints(samplePoints);
return KisAlgebra2D::blowRect(resultRect | rc, margin);
}
QRect KisLiquifyTransformWorker::approxNeedRect(const QRect &rc, const QRect &fullBounds)
{
Q_UNUSED(rc);
return fullBounds;
}
#include <functional>
#include <QTransform>
......
......@@ -84,6 +84,9 @@ public:
void translate(const QPointF &offset);
QRect approxChangeRect(const QRect &rc);
QRect approxNeedRect(const QRect &rc, const QRect &fullBounds);
private:
struct Private;
const QScopedPointer<Private> m_d;
......
......@@ -292,27 +292,35 @@ QRect KisTransformMask::changeRect(const QRect &rect, PositionToFilthy pos) cons
* on the higher/lower level
*/
if (rect.isEmpty()) return rect;
if (!m_d->params->isAffine()) return rect;
QRect bounds;
QRect interestRect;
KisNodeSP parentNode = parent();
QRect changeRect = rect;
if (parentNode) {
bounds = parentNode->original()->defaultBounds()->bounds();
interestRect = parentNode->original()->extent();
} else {
bounds = QRect(0,0,777,777);
interestRect = QRect(0,0,888,888);
warnKrita << "WARNING: transform mask has no parent (change rect)."
<< "Cannot run safe transformations."
<< "Will limit bounds to" << ppVar(bounds);
}
if (m_d->params->isAffine()) {
QRect bounds;
QRect interestRect;
KisNodeSP parentNode = parent();
if (parentNode) {
bounds = parentNode->original()->defaultBounds()->bounds();
interestRect = parentNode->original()->extent();
} else {
bounds = QRect(0,0,777,777);
interestRect = QRect(0,0,888,888);
warnKrita << "WARNING: transform mask has no parent (change rect)."
<< "Cannot run safe transformations."
<< "Will limit bounds to" << ppVar(bounds);
}
const QRect limitingRect = KisAlgebra2D::blowRect(bounds, m_d->offBoundsReadArea);
const QRect limitingRect = KisAlgebra2D::blowRect(bounds, m_d->offBoundsReadArea);
KisSafeTransform transform(m_d->worker.forwardTransform(), limitingRect, interestRect);
changeRect = transform.mapRectForward(rect);
} else {
QRect interestRect;
interestRect = parent() ? parent()->original()->extent() : QRect();
KisSafeTransform transform(m_d->worker.forwardTransform(), limitingRect, interestRect);
QRect changeRect = transform.mapRectForward(rect);
changeRect = m_d->params->nonAffineChangeRect(rect);
}
return changeRect;
}
......@@ -343,10 +351,17 @@ QRect KisTransformMask::needRect(const QRect& rect, PositionToFilthy pos) const
<< "Will limit bounds to" << ppVar(bounds);
}
const QRect limitingRect = KisAlgebra2D::blowRect(bounds, m_d->offBoundsReadArea);
QRect needRect = rect;
if (m_d->params->isAffine()) {
const QRect limitingRect = KisAlgebra2D::blowRect(bounds, m_d->offBoundsReadArea);
KisSafeTransform transform(m_d->worker.forwardTransform(), limitingRect, interestRect);
needRect = transform.mapRectBackward(rect);
KisSafeTransform transform(m_d->worker.forwardTransform(), limitingRect, interestRect);
QRect needRect = transform.mapRectBackward(rect);
} else {
needRect = m_d->params->nonAffineNeedRect(rect, interestRect);
}
return needRect;
}
......
......@@ -136,6 +136,17 @@ void KisDumbTransformMaskParams::translate(const QPointF &offset)
m_d->transform *= QTransform::fromTranslate(offset.x(), offset.y());
}
QRect KisDumbTransformMaskParams::nonAffineChangeRect(const QRect &rc)
{
return rc;
}
QRect KisDumbTransformMaskParams::nonAffineNeedRect(const QRect &rc, const QRect &srcBounds)
{
Q_UNUSED(srcBounds);
return rc;
}
QTransform KisDumbTransformMaskParams::testingGetTransform() const
{
return m_d->transform;
......
......@@ -43,6 +43,9 @@ public:
virtual void toXML(QDomElement *e) const = 0;
virtual void translate(const QPointF &offset) = 0;
virtual QRect nonAffineChangeRect(const QRect &rc) = 0;
virtual QRect nonAffineNeedRect(const QRect &rc, const QRect &srcBounds) = 0;
};
......@@ -72,6 +75,9 @@ public:
QTransform testingGetTransform() const;
void testingSetTransform(const QTransform &t);
QRect nonAffineChangeRect(const QRect &rc);
QRect nonAffineNeedRect(const QRect &rc, const QRect &srcBounds);
private:
struct Private;
const QScopedPointer<Private> m_d;
......
......@@ -262,6 +262,24 @@ void KisWarpTransformWorker::run()
srcBounds, pixelPrecision);
}
#include "krita_utils.h"
QRect KisWarpTransformWorker::approxChangeRect(const QRect &rc)
{
const qreal margin = 0.05;
FunctionTransformOp functionOp(m_warpMathFunction, m_origPoint, m_transfPoint, m_alpha);
QRect resultRect = KritaUtils::approximateRectWithPointTransform(rc, functionOp);
return KisAlgebra2D::blowRect(resultRect, margin);
}
QRect KisWarpTransformWorker::approxNeedRect(const QRect &rc, const QRect &fullBounds)
{
Q_UNUSED(rc);
return fullBounds;
}
QImage KisWarpTransformWorker::transformQImage(WarpType warpType,
const QVector<QPointF> &origPoint,
const QVector<QPointF> &transfPoint,
......
......@@ -63,9 +63,12 @@ public:
// Prepare the transformation on dev
KisWarpTransformWorker(WarpType warpType, KisPaintDeviceSP dev, QVector<QPointF> origPoint, QVector<QPointF> transfPoint, qreal alpha, KoUpdater *progress);
~KisWarpTransformWorker();
// Perform the prepated transformation
// Perform the prepared transformation
void run();
QRect approxChangeRect(const QRect &rc);
QRect approxNeedRect(const QRect &rc, const QRect &fullBounds);
private:
struct FunctionTransformOp;
typedef QPointF (*WarpMathFunction)(QPointF, QVector<QPointF>, QVector<QPointF>, qreal);
......
......@@ -27,6 +27,11 @@
#include <QPen>
#include <QPainter>
#include <boost/accumulators/accumulators.hpp>
#include <boost/accumulators/statistics/stats.hpp>
#include <boost/accumulators/statistics/min.hpp>
#include <boost/accumulators/statistics/max.hpp>
#include "kis_algebra_2d.h"
#include <KoColorSpaceRegistry.h>
......@@ -81,6 +86,97 @@ namespace KritaUtils
return patches;
}
template <class Rect, class Point>
QVector<Point> sampleRectWithPoints(const Rect &rect)
{
QVector<Point> points;
Point m1 = 0.5 * (rect.topLeft() + rect.topRight());
Point m2 = 0.5 * (rect.bottomLeft() + rect.bottomRight());
points << rect.topLeft();
points << m1;
points << rect.topRight();
points << 0.5 * (rect.topLeft() + rect.bottomLeft());
points << 0.5 * (m1 + m2);
points << 0.5 * (rect.topRight() + rect.bottomRight());
points << rect.bottomLeft();
points << m2;
points << rect.bottomRight();
return points;
}
QVector<QPoint> sampleRectWithPoints(const QRect &rect)
{
return sampleRectWithPoints<QRect, QPoint>(rect);
}
QVector<QPointF> sampleRectWithPoints(const QRectF &rect)
{
return sampleRectWithPoints<QRectF, QPointF>(rect);
}
template <class Rect, class Point, bool alignPixels>
Rect approximateRectFromPointsImpl(const QVector<Point> &points)
{
using namespace boost::accumulators;
accumulator_set<qreal, stats<tag::min, tag::max > > accX;
accumulator_set<qreal, stats<tag::min, tag::max > > accY;
Q_FOREACH (const Point &pt, points) {
accX(pt.x());
accY(pt.y());
}
Rect resultRect;
if (alignPixels) {
resultRect.setCoords(std::floor(min(accX)), std::floor(min(accY)),
std::ceil(max(accX)), std::ceil(max(accY)));
} else {
resultRect.setCoords(min(accX), min(accY),
max(accX), max(accY));
}
return resultRect;
}
QRect approximateRectFromPoints(const QVector<QPoint> &points)
{
return approximateRectFromPointsImpl<QRect, QPoint, true>(points);
}
QRectF approximateRectFromPoints(const QVector<QPointF> &points)
{
return approximateRectFromPointsImpl<QRectF, QPointF, false>(points);
}
QRect approximateRectWithPointTransform(const QRect &rect, std::function<QPointF(QPointF)> func)
{
QVector<QPoint> points = KritaUtils::sampleRectWithPoints(rect);
using namespace boost::accumulators;
accumulator_set<qreal, stats<tag::min, tag::max > > accX;
accumulator_set<qreal, stats<tag::min, tag::max > > accY;
Q_FOREACH (const QPoint &pt, points) {
QPointF dstPt = func(pt);
accX(dstPt.x());
accY(dstPt.y());
}
QRect resultRect;
resultRect.setCoords(std::floor(min(accX)), std::floor(min(accY)),
std::ceil(max(accX)), std::ceil(max(accY)));
return resultRect;
}
bool checkInTriangle(const QRectF &rect,
const QPolygonF &triangle)
{
......
......@@ -41,6 +41,14 @@ namespace KritaUtils
QVector<QRect> KRITAIMAGE_EXPORT splitRectIntoPatches(const QRect &rc, const QSize &patchSize);
QVector<QRect> KRITAIMAGE_EXPORT splitRegionIntoPatches(const QRegion &region, const QSize &patchSize);
QVector<QPoint> KRITAIMAGE_EXPORT sampleRectWithPoints(const QRect &rect);
QVector<QPointF> KRITAIMAGE_EXPORT sampleRectWithPoints(const QRectF &rect);
QRect KRITAIMAGE_EXPORT approximateRectFromPoints(const QVector<QPoint> &points);
QRectF KRITAIMAGE_EXPORT approximateRectFromPoints(const QVector<QPointF> &points);
QRect KRITAIMAGE_EXPORT approximateRectWithPointTransform(const QRect &rect, std::function<QPointF(QPointF)> func);
QRegion KRITAIMAGE_EXPORT splitTriangles(const QPointF &center,
const QVector<QPointF> &points);
QRegion KRITAIMAGE_EXPORT splitPath(const QPainterPath &path);
......
......@@ -25,56 +25,67 @@
#include <KoProgressUpdater.h>
struct WarpTransforWorkerData {
void KisWarpTransformWorkerTest::test()
{
TestUtil::TestProgressBar bar;
KoProgressUpdater pu(&bar);
KoUpdaterPtr updater = pu.startSubtask();
WarpTransforWorkerData() {
TestUtil::TestProgressBar bar;
KoProgressUpdater pu(&bar);
updater = pu.startSubtask();
const KoColorSpace *cs = KoColorSpaceRegistry::instance()->rgb8();
// QImage image(TestUtil::fetchDataFileLazy("test_transform_quality.png"));
QImage image(TestUtil::fetchDataFileLazy("test_transform_quality_second.png"));
const KoColorSpace *cs = KoColorSpaceRegistry::instance()->rgb8();
// QImage image(TestUtil::fetchDataFileLazy("test_transform_quality.png"));
QImage image(TestUtil::fetchDataFileLazy("test_transform_quality_second.png"));
KisPaintDeviceSP dev = new KisPaintDevice(cs);
dev->convertFromQImage(image, 0);
dev = new KisPaintDevice(cs);
dev->convertFromQImage(image, 0);
QVector<QPointF> origPoints;
QVector<QPointF> transfPoints;
qreal alpha = 1.0;
alpha = 1.0;
QRectF bounds(dev->exactBounds());
bounds = dev->exactBounds();
origPoints << bounds.topLeft();
origPoints << bounds.topRight();
origPoints << bounds.bottomRight();
origPoints << bounds.bottomLeft();
origPoints << bounds.topLeft();
origPoints << bounds.topRight();
origPoints << bounds.bottomRight();
origPoints << bounds.bottomLeft();
origPoints << 0.5 * (bounds.bottomLeft() + bounds.bottomRight());
origPoints << 0.5 * (bounds.bottomLeft() + bounds.bottomRight()) + QPointF(-20, 0);
origPoints << 0.5 * (bounds.bottomLeft() + bounds.bottomRight());
origPoints << 0.5 * (bounds.bottomLeft() + bounds.bottomRight()) + QPointF(-20, 0);
transfPoints << bounds.topLeft();
transfPoints << bounds.bottomLeft() + 0.6 * (bounds.topRight() - bounds.bottomLeft());
transfPoints << bounds.topLeft() + 0.8 * (bounds.bottomRight() - bounds.topLeft());
transfPoints << bounds.bottomLeft() + QPointF(200, 0);
transfPoints << bounds.topLeft();
transfPoints << bounds.bottomLeft() + 0.6 * (bounds.topRight() - bounds.bottomLeft());
transfPoints << bounds.topLeft() + 0.8 * (bounds.bottomRight() - bounds.topLeft());
transfPoints << bounds.bottomLeft() + QPointF(200, 0);
transfPoints << 0.5 * (bounds.bottomLeft() + bounds.bottomRight()) + QPointF(40,20);
transfPoints << 0.5 * (bounds.bottomLeft() + bounds.bottomRight()) + QPointF(-20, 0) + QPointF(-40,20);
}
transfPoints << 0.5 * (bounds.bottomLeft() + bounds.bottomRight()) + QPointF(40,20);
transfPoints << 0.5 * (bounds.bottomLeft() + bounds.bottomRight()) + QPointF(-20, 0) + QPointF(-40,20);
KisPaintDeviceSP dev;
QVector<QPointF> origPoints;
QVector<QPointF> transfPoints;
qreal alpha;
KoUpdaterPtr updater;
QRectF bounds;
};
void KisWarpTransformWorkerTest::test()
{
WarpTransforWorkerData d;
KisWarpTransformWorker worker(KisWarpTransformWorker::RIGID_TRANSFORM,
dev,
origPoints,
transfPoints,
alpha,
updater);
d.dev,
d.origPoints,
d.transfPoints,
d.alpha,
d.updater);
QBENCHMARK_ONCE {
worker.run();
}
QImage result = dev->convertToQImage(0);
QImage result = d.dev->convertToQImage(0);
TestUtil::checkQImage(result, "warp_transform_test", "simple", "tr");
}
......@@ -323,6 +334,20 @@ void KisWarpTransformWorkerTest::testBackwardInterpolatorExtrapolation()
QCOMPARE(interp.map(QPointF(0,110)), QPointF(110, 100));
QCOMPARE(interp.map(QPointF(-10,110)), QPointF(110,110));
}
#include "krita_utils.h"
void KisWarpTransformWorkerTest::testNeedChangeRects()
{
WarpTransforWorkerData d;
KisWarpTransformWorker worker(KisWarpTransformWorker::RIGID_TRANSFORM,
d.dev,
d.origPoints,
d.transfPoints,
d.alpha,
d.updater);
QCOMPARE(KritaUtils::sampleRectWithPoints(d.bounds.toAlignedRect()).size(), 9);
QCOMPARE(worker.approxChangeRect(d.bounds.toAlignedRect()), QRect(-89,-89, 1072,1076));
}
QTEST_MAIN(KisWarpTransformWorkerTest)
......@@ -34,6 +34,8 @@ private Q_SLOTS:
void testBackwardInterpolatorRoundTrip();
void testGridSize();
void testBackwardInterpolatorExtrapolation();
void testNeedChangeRects();
};
#endif /* __KIS_WARP_TRANSFORM_WORKER_TEST_H */
......@@ -94,6 +94,16 @@ void KisTransformMaskAdapter::translate(const QPointF &offset)
m_d->args.translate(offset);
}
QRect KisTransformMaskAdapter::nonAffineChangeRect(const QRect &rc)
{
return KisTransformUtils::changeRect(m_d->args, rc);
}
QRect KisTransformMaskAdapter::nonAffineNeedRect(const QRect &rc, const QRect &srcBounds)
{
return KisTransformUtils::needRect(m_d->args, rc, srcBounds);
}
#include "kis_transform_mask_params_factory_registry.h"
struct ToolTransformParamsRegistrar {
......