Commit 4195c38e authored by Dmitry Kazakov's avatar Dmitry Kazakov

Fixed dynamical updates of the Transform Mask

This patch implements numerous fixes and refactoings:

1) Implemented KisSafeTransform. It works like a usual QTransform but
   it takes maths into account. That is the transform and its reverse
   are *not* defined on the entire R^2 plane. Instead the valid input
   area is limited by the horizon line and nothing can be transformed
   above it. KisSafeTransform takes that into account and clips the
   desired rect/polygon to fit the valid area.

2) KisTransformMask::need/changeRect now uses safe trasnforms.

3) KisAsyncMerger recalculates the area of the clone's source to fetch
   correct data. To fix concurrency, this extra area is taken into account
   in KisCloneLayer::accessRect();

4) Implemented detailed unittests for dynamicat transform masks.

5) Added ability to store reference images of the unittest in a
   separate folder (outside repository).

   It consists of 3 major parts:

   1) checkQImageExternal() is expected to be used to fetch data from
      external folders only.

   2) KRITA_UNITTESTS_DATA_DIR environment variable is used to search for
      additional data sources

   3) KRITA_WRITE_UNITTESTS=1 together with KRITA_UNITTESTS_DATA_DIR set
      to a path will write the output of the unittest as a reference to
      an external folder.

6) The testing images are stored in:
	svn+ssh://svn@svn.kde.org/home/kde/trunk/tests/kritatests


CCMAIL:kimageshop@kde.org
parent f52d53be
......@@ -136,6 +136,7 @@ set(kritaimage_LIB_SRCS
kis_transform_mask_params_interface.cpp
kis_recalculate_transform_mask_job.cpp
kis_transform_mask_params_factory_registry.cpp
kis_safe_transform.cpp
kis_gradient_painter.cc
kis_iterator_ng.cpp
kis_async_merger.cpp
......
......@@ -124,4 +124,12 @@ QPainterPath smallArrow()
return p;
}
QRect blowRect(const QRect &rect, qreal coeff)
{
int w = rect.width() * coeff;
int h = rect.height() * coeff;
return rect.adjusted(-w, -h, w, h);
}
}
......@@ -81,10 +81,17 @@ Point normalize(const Point &a)
*/
template <typename T>
T signPZ(T x) {
const T zeroValue(0);
return x >= T(0) ? T(1) : T(-1);
}
/**
* Usual sign() function with zero returning zero
*/
template <typename T>
T signZZ(T x) {
return x == T(0) ? T(0) : x > T(0) ? T(1) : T(-1);
}
template <class T>
T leftUnitNormal(const T &a)
{
......@@ -209,6 +216,12 @@ inline Point clampPoint(Point pt, const Rect &bounds)
QPainterPath KRITAIMAGE_EXPORT smallArrow();
/**
* Multiply width and height of \p rect by \p coeff keeping the
* center of the rectangle pinned
*/
QRect KRITAIMAGE_EXPORT blowRect(const QRect &rect, qreal coeff);
}
#endif /* __KIS_ALGEBRA_2D_H */
......@@ -150,6 +150,18 @@ public:
QRegion prepareRegion(srcRect);
prepareRegion -= m_cropRect;
QStack<QRect> applyRects;
bool rectVariesFlag;
/**
* If a clone has complicated masks, we should prepare additional
* source area to ensure the rect is prepared.
*/
QRect needRectOnSource = layer->needRectOnSourceForMasks(srcRect);
if (!needRectOnSource.isEmpty()) {
prepareRegion += needRectOnSource;
}
foreach(const QRect &rect, prepareRegion.rects()) {
walker.collectRects(srcLayer, rect);
merger.startMerge(walker, false);
......
......@@ -34,6 +34,9 @@
#include "kis_clone_info.h"
#include "kis_paint_layer.h"
#include <QStack>
#include <kis_effect_mask.h>
struct KisCloneLayer::Private
{
......@@ -157,6 +160,28 @@ void KisCloneLayer::notifyParentVisibilityChanged(bool value)
KisLayer::notifyParentVisibilityChanged(value);
}
QRect KisCloneLayer::needRectOnSourceForMasks(const QRect &rc) const
{
QStack<QRect> applyRects_unused;
bool rectVariesFlag;
QList<KisEffectMaskSP> effectMasks = this->effectMasks();
if (effectMasks.isEmpty()) return QRect();
QRect needRect = this->masksNeedRect(effectMasks,
rc,
applyRects_unused,
rectVariesFlag);
if (needRect.isEmpty() ||
(!rectVariesFlag && needRect == rc)) {
return QRect();
}
return needRect;
}
qint32 KisCloneLayer::x() const
{
return m_d->x;
......@@ -196,8 +221,17 @@ QRect KisCloneLayer::accessRect(const QRect &rect, PositionToFilthy pos) const
{
QRect resultRect = rect;
if(pos & (N_FILTHY_PROJECTION | N_FILTHY) && (m_d->x || m_d->y)) {
resultRect |= rect.translated(-m_d->x, -m_d->y);
if(pos & (N_FILTHY_PROJECTION | N_FILTHY)) {
if (m_d->x || m_d->y) {
resultRect |= rect.translated(-m_d->x, -m_d->y);
}
/**
* KisUpdateOriginalVisitor will try to recalculate some area
* on the clone's source, so this extra rectangle should also
* be taken into account
*/
resultRect |= needRectOnSourceForMasks(rect);
}
return resultRect;
......
......@@ -116,6 +116,8 @@ public:
*/
void setDirtyOriginal(const QRect &rect);
QRect needRectOnSourceForMasks(const QRect &rc) const;
protected:
void notifyParentVisibilityChanged(bool value);
......
......@@ -156,7 +156,7 @@ void KisPerspectiveTransformWorker::runPartialDst(KisPaintDeviceSP srcDev,
return;
}
QRectF srcClipRect = srcDev->defaultBounds()->bounds();
QRectF srcClipRect = srcDev->exactBounds();
KisProgressUpdateHelper progressHelper(m_progressUpdater, 100, dstRect.height());
......
/*
* Copyright (c) 2014 Dmitry Kazakov <dimula73@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "kis_safe_transform.h"
#include <QTransform>
#include <QLineF>
#include <QPolygonF>
#include "kis_debug.h"
#include "kis_algebra_2d.h"
struct KisSafeTransform::Private
{
QRect bounds;
QTransform forwardTransform;
QTransform backwardTransform;
QPolygonF srcClipPolygon;
QPolygonF dstClipPolygon;
bool getHorizon(const QTransform &t, QLineF *horizon) {
static const qreal eps = 1e-10;
QPointF vanishingX(t.m11() / t.m13(), t.m12() / t.m13());
QPointF vanishingY(t.m21() / t.m23(), t.m22() / t.m23());
if (qAbs(t.m13()) < eps && qAbs(t.m23()) < eps) {
*horizon = QLineF();
return false;
} else if (qAbs(t.m23()) < eps) {
QPointF diff = t.map(QPointF(0.0, 10.0)) - t.map(QPointF());
vanishingY = vanishingX + diff;
} else if (qAbs(t.m13()) < eps) {
QPointF diff = t.map(QPointF(10.0, 0.0)) - t.map(QPointF());
vanishingX = vanishingY + diff;
}
*horizon = QLineF(vanishingX, vanishingY);
return true;
}
qreal getCrossSign(const QLineF &horizon, const QRectF &rc) {
if (rc.isEmpty()) return 1.0;
QPointF diff = horizon.p2() - horizon.p1();
return KisAlgebra2D::signPZ(KisAlgebra2D::crossProduct(diff, rc.center() - horizon.p1()));
}
QPolygonF getCroppedPolygon(const QLineF &baseHorizon, const QRect &rc, const qreal crossCoeff) {
if (rc.isEmpty()) return QPolygonF();
QRectF boundsRect(rc);
QPolygonF polygon(boundsRect);
QPolygonF result;
// calculate new (offset) horizon to avoid infinity
const qreal offsetLength = 10.0;
const QPointF horizonOffset = offsetLength * crossCoeff *
KisAlgebra2D::rightUnitNormal(baseHorizon.p2() - baseHorizon.p1());
const QLineF horizon = baseHorizon.translated(horizonOffset);
// base vectors to calculate the side of the horizon
const QPointF &basePoint = horizon.p1();
const QPointF horizonVec = horizon.p2() - basePoint;
// iteration
QPointF prevPoint = polygon[polygon.size() - 1];
qreal prevCross = crossCoeff * KisAlgebra2D::crossProduct(horizonVec, prevPoint - basePoint);
for (int i = 0; i < polygon.size(); i++) {
const QPointF &pt = polygon[i];
qreal cross = crossCoeff * KisAlgebra2D::crossProduct(horizonVec, pt - basePoint);
if ((cross >= 0 && prevCross >= 0) || (cross == 0 && prevCross < 0)) {
result << pt;
} else if (cross * prevCross < 0) {
QPointF intersection;
QLineF edge(prevPoint, pt);
QLineF::IntersectType intersectionType =
horizon.intersect(edge, &intersection);
KIS_ASSERT_RECOVER_NOOP(intersectionType != QLineF::NoIntersection);
result << intersection;
if (cross > 0) {
result << pt;
}
}
prevPoint = pt;
prevCross = cross;
}
if (!result.isClosed()) {
result << result.first();
}
return result;
}
};
KisSafeTransform::KisSafeTransform(const QTransform &transform,
const QRect &bounds,
const QRect &srcInterestRect)
: m_d(new Private)
{
m_d->bounds = bounds;
m_d->forwardTransform = transform;
m_d->backwardTransform = transform.inverted();
m_d->srcClipPolygon = QPolygonF(QRectF(m_d->bounds));
m_d->dstClipPolygon = QPolygonF(QRectF(m_d->bounds));
qreal crossCoeff = 1.0;
QLineF srcHorizon;
if (m_d->getHorizon(m_d->backwardTransform, &srcHorizon)) {
crossCoeff = m_d->getCrossSign(srcHorizon, srcInterestRect);
m_d->srcClipPolygon = m_d->getCroppedPolygon(srcHorizon, m_d->bounds, crossCoeff);
}
QLineF dstHorizon;
if (m_d->getHorizon(m_d->forwardTransform, &dstHorizon)) {
crossCoeff = m_d->getCrossSign(dstHorizon, mapRectForward(srcInterestRect));
m_d->dstClipPolygon = m_d->getCroppedPolygon(dstHorizon, m_d->bounds, crossCoeff);
}
}
KisSafeTransform::~KisSafeTransform()
{
}
QPolygonF KisSafeTransform::srcClipPolygon() const
{
return m_d->srcClipPolygon;
}
QPolygonF KisSafeTransform::dstClipPolygon() const
{
return m_d->dstClipPolygon;
}
QPolygonF KisSafeTransform::mapForward(const QPolygonF &p)
{
QPolygonF poly = m_d->srcClipPolygon.intersected(p);
return m_d->forwardTransform.map(poly).intersected(QRectF(m_d->bounds));
}
QPolygonF KisSafeTransform::mapBackward(const QPolygonF &p)
{
QPolygonF poly = m_d->dstClipPolygon.intersected(p);
return m_d->backwardTransform.map(poly).intersected(QRectF(m_d->bounds));
}
QRectF KisSafeTransform::mapRectForward(const QRectF &rc)
{
return mapForward(rc).boundingRect();
}
QRectF KisSafeTransform::mapRectBackward(const QRectF &rc)
{
return mapBackward(rc).boundingRect();
}
QRect KisSafeTransform::mapRectForward(const QRect &rc)
{
return mapRectForward(QRectF(rc)).toAlignedRect();
}
QRect KisSafeTransform::mapRectBackward(const QRect &rc)
{
return mapRectBackward(QRectF(rc)).toAlignedRect();
}
/*
* Copyright (c) 2014 Dmitry Kazakov <dimula73@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef __KIS_SAFE_TRANSFORM_H
#define __KIS_SAFE_TRANSFORM_H
#include <QScopedPointer>
#include "krita_export.h"
class QTransform;
class QRect;
class QRectF;
class QPolygonF;
class KRITAIMAGE_EXPORT KisSafeTransform
{
public:
KisSafeTransform(const QTransform &transform,
const QRect &bounds,
const QRect &srcInterestRect);
~KisSafeTransform();
QPolygonF srcClipPolygon() const;
QPolygonF dstClipPolygon() const;
QPolygonF mapForward(const QPolygonF &p);
QPolygonF mapBackward(const QPolygonF &p);
QRectF mapRectForward(const QRectF &rc);
QRectF mapRectBackward(const QRectF &rc);
QRect mapRectForward(const QRect &rc);
QRect mapRectBackward(const QRect &rc);
private:
struct Private;
const QScopedPointer<Private> m_d;
};
#endif /* __KIS_SAFE_TRANSFORM_H */
......@@ -39,6 +39,10 @@
#include "kis_transform_mask_params_interface.h"
#include "kis_recalculate_transform_mask_job.h"
#include "kis_signal_compressor.h"
#include "kis_algebra_2d.h"
#include "kis_safe_transform.h"
#define UPDATE_DELAY 3000 /*ms */
......@@ -165,7 +169,7 @@ void KisTransformMask::recaclulateStaticImage()
* into account all the change rects of all the masks. Usually,
* this work is done by the walkers.
*/
QRect requestedRect = parentLayer->changeRect(parentLayer->exactBounds());
QRect requestedRect = parentLayer->changeRect(parentLayer->original()->exactBounds());
parentLayer->updateProjection(requestedRect, N_FILTHY_PROJECTION);
m_d->recalculatingStaticImage = false;
......@@ -215,14 +219,6 @@ void KisTransformMask::accept(KisProcessingVisitor &visitor, KisUndoAdapter *und
return visitor.visit(this, undoAdapter);
}
QRect calculateLimitingRect(const QRect &bounds, qreal coeff)
{
int w = bounds.width() * coeff;
int h = bounds.height() * coeff;
return bounds.adjusted(-w, -h, w, h);
}
QRect KisTransformMask::changeRect(const QRect &rect, PositionToFilthy pos) const
{
Q_UNUSED(pos);
......@@ -234,33 +230,26 @@ QRect KisTransformMask::changeRect(const QRect &rect, PositionToFilthy pos) cons
if (rect.isEmpty()) return rect;
if (!m_d->params->isAffine()) return rect;
QRect changeRect = m_d->worker.forwardTransform()
.mapRect(QRectF(rect)).toAlignedRect();
KisNodeSP parentNode;
KisPaintDeviceSP parentOriginal;
if ((parentNode = parent()) &&
(parentOriginal = parentNode->original())) {
const QRect bounds = parentOriginal->defaultBounds()->bounds();
const QRect limitingRect = calculateLimitingRect(bounds, 2);
QRect bounds;
QRect interestRect;
KisNodeSP parentNode = parent();
changeRect &= limitingRect;
QRect backwardRect = limitingRect & m_d->worker.backwardTransform().mapRect(rect);
QRegion backwardRegion(backwardRect);
backwardRegion -= bounds;
backwardRegion = m_d->worker.forwardTransform().map(backwardRegion);
// FIXME: d-oh... please fix me and use region instead :(
changeRect |= backwardRegion.boundingRect();
if (parentNode) {
bounds = parentNode->original()->defaultBounds()->bounds();
interestRect = parentNode->original()->extent();
} else {
qWarning() << "WARNING: a transform mask has no parent, don't know how to limit it";
const QRect limitingRect(-1000, -1000, 10000, 10000);
changeRect &= limitingRect;
bounds = QRect(0,0,777,777);
interestRect = QRect(0,0,888,888);
qWarning() << "WARNING: transform mask has no parent (change rect)."
<< "Cannot run safe transformations."
<< "Will limit bounds to" << ppVar(bounds);
}
const QRect limitingRect = KisAlgebra2D::blowRect(bounds, 0.5);
KisSafeTransform transform(m_d->worker.forwardTransform(), limitingRect, interestRect);
QRect changeRect = transform.mapRectForward(rect);
return changeRect;
}
......@@ -275,17 +264,26 @@ QRect KisTransformMask::needRect(const QRect& rect, PositionToFilthy pos) const
if (rect.isEmpty()) return rect;
if (!m_d->params->isAffine()) return rect;
QRect needRect = kisGrowRect(m_d->worker.backwardTransform().mapRect(rect), 2);
QRect bounds;
QRect interestRect;
KisNodeSP parentNode = parent();
KisNodeSP parentNode;
if ((parentNode = parent())) {
needRect &= parentNode->extent();
} else if (needRect.width() > 1e6 || needRect.height() > 1e6) {
qWarning() << "WARNING: transform mask returns infinite need rect! Dropping..." << needRect;
needRect = 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);
qWarning() << "WARNING: transform mask has no parent (need rect)."
<< "Cannot run safe transformations."
<< "Will limit bounds to" << ppVar(bounds);
}
const QRect limitingRect = KisAlgebra2D::blowRect(bounds, 0.5);
KisSafeTransform transform(m_d->worker.forwardTransform(), limitingRect, interestRect);
QRect needRect = transform.mapRectBackward(rect);
return needRect;
}
......
......@@ -117,6 +117,11 @@ QTransform KisDumbTransformMaskParams::testingGetTransform() const
return m_d->transform;
}
void KisDumbTransformMaskParams::testingSetTransform(const QTransform &t)
{
m_d->transform = t;
}
#include "kis_transform_mask_params_factory_registry.h"
struct DumbParamsRegistrar {
......
......@@ -64,6 +64,7 @@ public:
// for tesing purposes only
QTransform testingGetTransform() const;
void testingSetTransform(const QTransform &t);
private:
struct Private;
......
......@@ -25,7 +25,11 @@ class KisTransformMaskTest : public QObject
{
Q_OBJECT
private slots:
void test();
void testSafeTransform();
void testMaskOnPaintLayer();
void testMaskOnCloneLayer();
void testSafeTransformUnity();
void testSafeTransformSingleVanishingPoint();
};
#endif /* __KIS_TRANSFORM_MASK_TEST_H */
......@@ -63,24 +63,51 @@ inline KisNodeSP findNode(KisNodeSP root, const QString &name) {
return 0;
}
inline QString fetchDataFileLazy(const QString relativeFileName)
#include <QProcessEnvironment>
inline QString fetchExternalDataFileName(const QString relativeFileName)
{
static QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
static QString unittestsDataDirPath = "KRITA_UNITTESTS_DATA_DIR";
QString path;
if (!env.contains(unittestsDataDirPath)) {
qWarning() << "Environment variable" << unittestsDataDirPath << "is not set";
return QString();
} else {
path = env.value(unittestsDataDirPath, "");
}
QString filename =
QString(FILES_DATA_DIR) +
path +
QDir::separator() +
relativeFileName;
if (QFileInfo(filename).exists()) {
return filename;
}
return filename;
}
filename =
QString(FILES_DEFAULT_DATA_DIR) +
QDir::separator() +
relativeFileName;
inline QString fetchDataFileLazy(const QString relativeFileName, bool externalTest = false)
{
if (externalTest) {
return fetchExternalDataFileName(relativeFileName);
} else {
QString filename =
QString(FILES_DATA_DIR) +
QDir::separator() +
relativeFileName;
if (QFileInfo(filename).exists()) {
return filename;
}
filename =
QString(FILES_DEFAULT_DATA_DIR) +
QDir::separator() +
relativeFileName;
if (QFileInfo(filename).exists()) {
return filename;
if (QFileInfo(filename).exists()) {
return filename;
}
}
return QString();
......@@ -135,7 +162,7 @@ private:
};
inline bool compareQImages(QPoint & pt, const QImage & image1, const QImage & image2, int fuzzy = 0, int fuzzyAlpha = 0)
inline bool compareQImages(QPoint & pt, const QImage & image1, const QImage & image2, int fuzzy = 0, int fuzzyAlpha = 0, int maxNumFailingPixels = 0)
{
// QTime t;
// t.start();
......@@ -153,6 +180,8 @@ inline bool compareQImages(QPoint & pt, const QImage & image1, const QImage & im
return false;
}
int numFailingPixels = 0;
for (int y = 0; y < h1; ++y) {
const QRgb * const firstLine = reinterpret_cast<const QRgb *>(image2.scanLine(y));
const QRgb * const secondLine = reinterpret_cast<const QRgb *>(image1.scanLine(y));
......@@ -170,11 +199,19 @@ inline bool compareQImages(QPoint & pt, const QImage & image1, const QImage & im
if (!bothTransparent && (!same || !sameAlpha)) {
pt.setX(x);
pt.setY(y);
numFailingPixels++;
qDebug() << " Different at" << pt
<< "source" << qRed(a) << qGreen(a) << qBlue(a) << qAlpha(a)
<< "dest" << qRed(b) << qGreen(b) << qBlue(b) << qAlpha(b)
<< "fuzzy" << fuzzy;
return false;
<< "fuzzy" << fuzzy
<< "fuzzyAlpha" << fuzzyAlpha
<< "(" << numFailingPixels << "of" << maxNumFailingPixels << "allowed )";
if (numFailingPixels > maxNumFailingPixels) {
return false;
}
}
}
}
......@@ -257,44 +294,99 @@ inline bool comparePaintDevicesClever(const KisPaintDeviceSP dev1, const KisPain
#ifdef FILES_OUTPUT_DIR
inline bool checkQImage(const QImage &image, const QString &testName,
const QString &prefix, const QString &name,
int fuzzy = 0)
inline bool checkQImageImpl(bool externalTest,
const QImage &image, const