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 ¢er); 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 }