File indexing completed on 2024-05-12 15:58:37

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_polygonal_gradient_shape_strategy.h"
0008 
0009 #include "kis_debug.h"
0010 
0011 #include "kis_algebra_2d.h"
0012 
0013 #include <config-gsl.h>
0014 
0015 #ifdef HAVE_GSL
0016 #include <gsl/gsl_multimin.h>
0017 #endif /* HAVE_GSL */
0018 
0019 #include <QtCore/qmath.h>
0020 #include <limits>
0021 
0022 #include <boost/math/distributions/normal.hpp>
0023 
0024 #include <QPainterPath>
0025 #include "krita_utils.h"
0026 
0027 
0028 
0029 namespace Private {
0030 
0031     QPointF centerFromPath(const QPainterPath &selectionPath) {
0032         QPointF center;
0033         int numPoints = 0;
0034 
0035         for (int i = 0; i < selectionPath.elementCount(); i++) {
0036             QPainterPath::Element element = selectionPath.elementAt(i);
0037             if (element.type == QPainterPath::CurveToDataElement) continue;
0038 
0039             center += element;
0040             numPoints++;
0041         }
0042         if (numPoints == 0) { // can only happen if the path is empty
0043             return center;
0044         }
0045         center /= numPoints;
0046 
0047         return center;
0048     }
0049 
0050     qreal getDisnormedGradientValue(const QPointF &pt, const QPainterPath &selectionPath, qreal exponent)
0051     {
0052         // FIXME: exponent = 2.0
0053         //        We explicitly use pow2() and sqrt() functions here
0054         //        for efficiency reasons.
0055         KIS_ASSERT_RECOVER_NOOP(qFuzzyCompare(exponent, 2.0));
0056         const qreal minHiLevel = std::pow(0.5, 1.0 / exponent);
0057         qreal ptWeightNode = 0.0;
0058 
0059         for (int i = 0; i < selectionPath.elementCount(); i++) {
0060             if (selectionPath.elementAt(i).isMoveTo()) continue;
0061 
0062             const int prevI = i > 0 ? i - 1 : selectionPath.elementCount() - 1;
0063             const QPointF edgeP1 = selectionPath.elementAt(prevI);
0064             const QPointF edgeP2 = selectionPath.elementAt(i);
0065 
0066             const QPointF edgeVec = edgeP1 - edgeP2;
0067             const QPointF q1 = pt - edgeP1;
0068             const QPointF q2 = pt - edgeP2;
0069 
0070             const qreal proj1 = KisAlgebra2D::dotProduct(edgeVec, q1);
0071             const qreal proj2 = KisAlgebra2D::dotProduct(edgeVec, q2);
0072 
0073             qreal hi = 1.0;
0074 
0075             // pt's projection lays outside the edge itself,
0076             // when the projections has the same sign
0077 
0078             if (proj1 * proj2 >= 0) {
0079                 QPointF nearestPointVec =
0080                     qAbs(proj1) < qAbs(proj2) ? q1 : q2;
0081 
0082                 hi = KisAlgebra2D::norm(nearestPointVec);
0083             } else {
0084                 QLineF line(edgeP1, edgeP2);
0085                 hi = kisDistanceToLine(pt, line);
0086             }
0087 
0088             hi = qMax(minHiLevel, hi);
0089 
0090             // disabled for efficiency reasons
0091             // ptWeightNode += 1.0 / std::pow(hi, exponent);
0092 
0093             ptWeightNode += 1.0 / pow2(hi);
0094         }
0095 
0096         // disabled for efficiency reasons
0097         // return 1.0 / std::pow(ptWeightNode, 1.0 / exponent);
0098 
0099         return 1.0 / std::sqrt(ptWeightNode);
0100     }
0101 
0102     qreal initialExtremumValue(bool searchForMax) {
0103         return searchForMax ?
0104             std::numeric_limits<qreal>::min() :
0105             std::numeric_limits<qreal>::max();
0106     }
0107 
0108     bool findBestStartingPoint(int numSamples, const QPainterPath &path,
0109                                qreal exponent, bool searchForMax,
0110                                qreal initialExtremumValue,
0111                                QPointF *result) {
0112 
0113         const qreal minStepThreshold = 0.3;
0114         const int numCandidatesThreshold = 4;
0115 
0116         KIS_ASSERT_RECOVER(numSamples >= 4) {
0117             *result = centerFromPath(path);
0118             return true;
0119         }
0120 
0121         int totalSamples = numSamples;
0122         int effectiveSamples = numSamples & 0x1 ?
0123             (numSamples - 1) / 2 + 1 : numSamples;
0124 
0125         QRectF rect = path.boundingRect();
0126 
0127         qreal xOffset = rect.width() / (totalSamples + 1);
0128         qreal xStep = effectiveSamples == totalSamples ? xOffset : 2 * xOffset;
0129 
0130         qreal yOffset = rect.height() / (totalSamples + 1);
0131         qreal yStep = effectiveSamples == totalSamples ? yOffset : 2 * yOffset;
0132 
0133         if (xStep < minStepThreshold || yStep < minStepThreshold) {
0134             return false;
0135         }
0136 
0137         int numFound = 0;
0138         int numCandidates = 0;
0139         QPointF extremumPoint;
0140         qreal extremumValue = initialExtremumValue;
0141 
0142         const qreal eps = 1e-3;
0143         int sanityNumRows = 0;
0144         for (qreal y = rect.y() + yOffset; y < rect.bottom() - eps; y += yStep) {
0145             int sanityNumColumns = 0;
0146 
0147             sanityNumRows++;
0148             for (qreal x = rect.x() + xOffset; x < rect.right() - eps; x += xStep) {
0149                 sanityNumColumns++;
0150 
0151                 const QPointF pt(x, y);
0152                 if (!path.contains(pt)) continue;
0153 
0154                 qreal value = getDisnormedGradientValue(pt, path, exponent);
0155                 bool isExtremum = searchForMax ? value > extremumValue : value < extremumValue;
0156 
0157                 numCandidates++;
0158 
0159                 if (isExtremum) {
0160                     numFound++;
0161                     extremumPoint = pt;
0162                     extremumValue = value;
0163                 }
0164             }
0165 
0166             KIS_ASSERT_RECOVER_NOOP(sanityNumColumns == effectiveSamples);
0167         }
0168         KIS_ASSERT_RECOVER_NOOP(sanityNumRows == effectiveSamples);
0169 
0170         bool success = numFound && numCandidates >= numCandidatesThreshold;
0171 
0172         if (success) {
0173             *result = extremumPoint;
0174         } else {
0175             int newNumSamples = 2 * numSamples + 1;
0176             success = findBestStartingPoint(newNumSamples, path,
0177                                             exponent, searchForMax,
0178                                             extremumValue,
0179                                             result);
0180 
0181             if (!success && numFound > 0) {
0182                 *result = extremumPoint;
0183                 success = true;
0184             }
0185         }
0186 
0187         return success;
0188     }
0189 
0190 #ifdef HAVE_GSL
0191 
0192     struct GradientDescentParams {
0193         QPainterPath selectionPath;
0194         qreal exponent;
0195         bool searchForMax;
0196     };
0197 
0198     double errorFunc (const gsl_vector * x, void *paramsPtr)
0199     {
0200         double vX = gsl_vector_get(x, 0);
0201         double vY = gsl_vector_get(x, 1);
0202 
0203         const GradientDescentParams *params =
0204             static_cast<const GradientDescentParams*>(paramsPtr);
0205 
0206         qreal weight = getDisnormedGradientValue(QPointF(vX, vY),
0207                                                  params->selectionPath,
0208                                                  params->exponent);
0209 
0210         return params->searchForMax ? 1.0 / weight : weight;
0211     }
0212 
0213     qreal calculateMaxWeight(const QPainterPath &selectionPath,
0214                              qreal exponent,
0215                              bool searchForMax)
0216     {
0217         const gsl_multimin_fminimizer_type *T =
0218             gsl_multimin_fminimizer_nmsimplex2;
0219         gsl_multimin_fminimizer *s = 0;
0220         gsl_vector *ss, *x;
0221         gsl_multimin_function minex_func;
0222 
0223         size_t iter = 0;
0224         int status;
0225         double size;
0226 
0227         QPointF center;
0228         bool centerExists =
0229             findBestStartingPoint(4, selectionPath,
0230                                   exponent, searchForMax,
0231                                   Private::initialExtremumValue(searchForMax),
0232                                   &center);
0233 
0234         if (!centerExists || !selectionPath.contains(center)) {
0235 
0236             // if the path is too small just return default values
0237             if (selectionPath.boundingRect().width() >= 2.0 &&
0238                 selectionPath.boundingRect().height() >= 2.0) {
0239 
0240                 KIS_ASSERT_RECOVER_NOOP(selectionPath.contains(center));
0241             }
0242 
0243             return searchForMax ? 1.0 : 0.0;
0244         }
0245 
0246         /* Starting point */
0247         x = gsl_vector_alloc (2);
0248         gsl_vector_set (x, 0, center.x());
0249         gsl_vector_set (x, 1, center.y());
0250 
0251         /* Set initial step sizes to 10 px */
0252         ss = gsl_vector_alloc (2);
0253         gsl_vector_set (ss, 0, 10);
0254         gsl_vector_set (ss, 1, 10);
0255 
0256         GradientDescentParams p;
0257 
0258         p.selectionPath = selectionPath;
0259         p.exponent = exponent;
0260         p.searchForMax = searchForMax;
0261 
0262         /* Initialize method and iterate */
0263         minex_func.n = 2;
0264         minex_func.f = errorFunc;
0265         minex_func.params = (void*)&p;
0266 
0267         s = gsl_multimin_fminimizer_alloc (T, 2);
0268         gsl_multimin_fminimizer_set (s, &minex_func, x, ss);
0269 
0270         qreal result = searchForMax ?
0271             getDisnormedGradientValue(center, selectionPath, exponent) : 0.0;
0272 
0273         do
0274         {
0275             iter++;
0276             status = gsl_multimin_fminimizer_iterate(s);
0277 
0278             if (status)
0279                 break;
0280 
0281             size = gsl_multimin_fminimizer_size (s);
0282             status = gsl_multimin_test_size (size, 1e-6);
0283 
0284             if (status == GSL_SUCCESS)
0285             {
0286                 // dbgKrita << "*******Converged to minimum";
0287                 // dbgKrita << gsl_vector_get (s->x, 0)
0288                 //          << gsl_vector_get (s->x, 1)
0289                 //          << "|" << s->fval << size;
0290 
0291                 result = searchForMax ? 1.0 / s->fval : s->fval;
0292             }
0293         }
0294         while (status == GSL_CONTINUE && iter < 10000);
0295 
0296         gsl_vector_free(x);
0297         gsl_vector_free(ss);
0298         gsl_multimin_fminimizer_free (s);
0299 
0300         return result;
0301     }
0302 
0303 #else /* HAVE_GSL */
0304 
0305     qreal calculateMaxWeight(const QPainterPath &selectionPath,
0306                              qreal exponent,
0307                              bool searchForMax)
0308     {
0309         QPointF center = centerFromPath(selectionPath);
0310         return searchForMax ?
0311             getDisnormedGradientValue(center, selectionPath, exponent) : 0.0;
0312     }
0313 
0314 #endif /* HAVE_GSL */
0315 
0316 }
0317 
0318 QPainterPath simplifyPath(const QPainterPath &path,
0319                           qreal sizePortion,
0320                           qreal minLinearSize,
0321                           int minNumSamples)
0322 {
0323     QPainterPath finalPath;
0324 
0325     QList<QPolygonF> polygons = path.toSubpathPolygons();
0326     Q_FOREACH (const QPolygonF poly, polygons) {
0327         QPainterPath p;
0328         p.addPolygon(poly);
0329 
0330         const qreal length = p.length();
0331         const qreal lengthStep =
0332             KritaUtils::maxDimensionPortion(poly.boundingRect(),
0333                                             sizePortion, minLinearSize);
0334 
0335         int numSamples = qMax(qCeil(length / lengthStep), minNumSamples);
0336 
0337         if (numSamples > poly.size()) {
0338             finalPath.addPolygon(poly);
0339             finalPath.closeSubpath();
0340             continue;
0341         }
0342 
0343         const qreal portionStep = 1.0 / numSamples;
0344 
0345         QPolygonF newPoly;
0346         for (qreal t = 0.0; t < 1.0; t += portionStep) {
0347             newPoly << p.pointAtPercent(t);
0348         }
0349 
0350         finalPath.addPolygon(newPoly);
0351         finalPath.closeSubpath();
0352     }
0353 
0354     return finalPath;
0355 }
0356 
0357 KisPolygonalGradientShapeStrategy::KisPolygonalGradientShapeStrategy(const QPainterPath &selectionPath,
0358                                                                      qreal exponent)
0359     : m_exponent(exponent)
0360 {
0361     m_selectionPath = simplifyPath(selectionPath, 0.01, 3.0, 100);
0362 
0363     m_maxWeight = Private::calculateMaxWeight(m_selectionPath, m_exponent, true);
0364     m_minWeight = Private::calculateMaxWeight(m_selectionPath, m_exponent, false);
0365 
0366     m_scaleCoeff = 1.0 / (m_maxWeight - m_minWeight);
0367 }
0368 
0369 KisPolygonalGradientShapeStrategy::~KisPolygonalGradientShapeStrategy()
0370 {
0371 }
0372 
0373 double KisPolygonalGradientShapeStrategy::valueAt(double x, double y) const
0374 {
0375     QPointF pt(x, y);
0376     qreal value = 0.0;
0377 
0378     if (m_selectionPath.contains(pt)) {
0379         value = Private::getDisnormedGradientValue(pt, m_selectionPath, m_exponent);
0380         value =  (value - m_minWeight) * m_scaleCoeff;
0381     }
0382 
0383     return value;
0384 }
0385 
0386 QPointF KisPolygonalGradientShapeStrategy::testingCalculatePathCenter(int numSamples, const QPainterPath &path, qreal exponent, bool searchForMax)
0387 {
0388     QPointF result;
0389 
0390     qreal extremumValue = Private::initialExtremumValue(searchForMax);
0391     bool success = Private::findBestStartingPoint(numSamples, path,
0392                                                   exponent, searchForMax,
0393                                                   extremumValue,
0394                                                   &result);
0395 
0396     if (!success) {
0397         dbgKrita << "WARNING: Couldn't calculate findBestStartingPoint for:";
0398         dbgKrita << ppVar(numSamples);
0399         dbgKrita << ppVar(exponent);
0400         dbgKrita << ppVar(searchForMax);
0401         dbgKrita << ppVar(path);
0402 
0403     }
0404 
0405     return result;
0406 }