File indexing completed on 2024-05-12 15:56:39
0001 /* This file is part of the KDE project 0002 SPDX-FileCopyrightText: 2001-2003 Rob Buis <buis@kde.org> 0003 SPDX-FileCopyrightText: 2007, 2009 Jan Hambrecht <jaham@gmx.net> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "KoCurveFit.h" 0009 #include <KoPathShape.h> 0010 #include <QVector> 0011 #include <math.h> 0012 0013 /// our equivalent to zero 0014 const qreal Zero = 10e-12; 0015 0016 /* 0017 An Algorithm for Automatically Fitting Digitized Curves 0018 by Philip J. Schneider 0019 from "Graphics Gems", Academic Press, 1990 0020 0021 http://tog.acm.org/resources/GraphicsGems/gems/FitCurves.c 0022 http://tog.acm.org/resources/GraphicsGems/gems/README 0023 */ 0024 0025 class FitVector { 0026 public: 0027 FitVector(const QPointF &p) 0028 : m_X(p.x()) 0029 , m_Y(p.y()) 0030 { 0031 } 0032 0033 FitVector() 0034 : m_X(0) 0035 , m_Y(0) 0036 { 0037 } 0038 0039 FitVector(const QPointF &a, const QPointF &b) 0040 : m_X(a.x() - b.x()) 0041 , m_Y(a.y() - b.y()) 0042 { 0043 } 0044 0045 void normalize() { 0046 qreal len = length(); 0047 if (qFuzzyCompare(len, qreal(0.0))) { 0048 return; 0049 } 0050 m_X /= len; m_Y /= len; 0051 } 0052 0053 void negate() { 0054 m_X = -m_X; 0055 m_Y = -m_Y; 0056 } 0057 0058 void scale(qreal s) { 0059 qreal len = length(); 0060 if (qFuzzyCompare(len, qreal(0.0))) { 0061 return; 0062 } 0063 m_X *= s / len; 0064 m_Y *= s / len; 0065 } 0066 0067 qreal dot(const FitVector &v) const { 0068 return ((m_X*v.m_X) + (m_Y*v.m_Y)); 0069 } 0070 0071 qreal length() const { 0072 return (qreal) sqrt(m_X*m_X + m_Y*m_Y); 0073 } 0074 0075 QPointF operator+(const QPointF &p) { 0076 QPointF b(p.x() + m_X, p.y() + m_Y); 0077 return b; 0078 } 0079 0080 public: 0081 qreal m_X, m_Y; 0082 }; 0083 0084 qreal distance(const QPointF &p1, const QPointF &p2) 0085 { 0086 qreal dx = (p1.x() - p2.x()); 0087 qreal dy = (p1.y() - p2.y()); 0088 return sqrt(dx*dx + dy*dy); 0089 } 0090 0091 0092 FitVector ComputeLeftTangent(const QList<QPointF> &points, int end) 0093 { 0094 FitVector tHat1(points.at(end + 1), points.at(end)); 0095 0096 tHat1.normalize(); 0097 0098 return tHat1; 0099 } 0100 0101 FitVector ComputeRightTangent(const QList<QPointF> &points, int end) 0102 { 0103 FitVector tHat1(points.at(end - 1), points.at(end)); 0104 0105 tHat1.normalize(); 0106 0107 return tHat1; 0108 } 0109 0110 /* 0111 * ChordLengthParameterize : 0112 * Assign parameter values to digitized points 0113 * using relative distances between points. 0114 */ 0115 static qreal *ChordLengthParameterize(const QList<QPointF> &points, int first, int last) 0116 { 0117 int i; 0118 qreal *u; /* Parameterization */ 0119 0120 u = new qreal[(last-first+1)]; 0121 0122 u[0] = 0.0; 0123 for (i = first + 1; i <= last; ++i) { 0124 u[i-first] = u[i-first-1] + 0125 distance(points.at(i), points.at(i - 1)); 0126 } 0127 0128 qreal denominator = u[last-first]; 0129 if (qFuzzyCompare(denominator, qreal(0.0))) { 0130 denominator = Zero; 0131 } 0132 0133 for (i = first + 1; i <= last; ++i) { 0134 u[i-first] = u[i-first] / denominator; 0135 } 0136 0137 return(u); 0138 } 0139 0140 static FitVector VectorAdd(FitVector a, FitVector b) 0141 { 0142 FitVector c; 0143 c.m_X = a.m_X + b.m_X; c.m_Y = a.m_Y + b.m_Y; 0144 return (c); 0145 } 0146 static FitVector VectorScale(FitVector v, qreal s) 0147 { 0148 FitVector result; 0149 result.m_X = v.m_X * s; result.m_Y = v.m_Y * s; 0150 return (result); 0151 } 0152 0153 static FitVector VectorSub(FitVector a, FitVector b) 0154 { 0155 FitVector c; 0156 c.m_X = a.m_X - b.m_X; c.m_Y = a.m_Y - b.m_Y; 0157 return (c); 0158 } 0159 0160 static FitVector ComputeCenterTangent(const QList<QPointF> &points, int center) 0161 { 0162 FitVector V1, V2, tHatCenter; 0163 0164 FitVector cpointb(points.at(center - 1)); 0165 FitVector cpoint(points.at(center)); 0166 FitVector cpointa(points.at(center + 1)); 0167 0168 V1 = VectorSub(cpointb, cpoint); 0169 V2 = VectorSub(cpoint, cpointa); 0170 tHatCenter.m_X = ((V1.m_X + V2.m_X) / 2.0); 0171 tHatCenter.m_Y = ((V1.m_Y + V2.m_Y) / 2.0); 0172 tHatCenter.normalize(); 0173 return tHatCenter; 0174 } 0175 0176 /* 0177 * B0, B1, B2, B3 : 0178 * Bezier multipliers 0179 */ 0180 static qreal B0(qreal u) 0181 { 0182 qreal tmp = 1.0 - u; 0183 return (tmp * tmp * tmp); 0184 } 0185 0186 0187 static qreal B1(qreal u) 0188 { 0189 qreal tmp = 1.0 - u; 0190 return (3 * u *(tmp * tmp)); 0191 } 0192 0193 static qreal B2(qreal u) 0194 { 0195 qreal tmp = 1.0 - u; 0196 return (3 * u * u * tmp); 0197 } 0198 0199 static qreal B3(qreal u) 0200 { 0201 return (u * u * u); 0202 } 0203 0204 /* 0205 * GenerateBezier : 0206 * Use least-squares method to find Bezier control points for region. 0207 * 0208 */ 0209 QPointF* GenerateBezier(const QList<QPointF> &points, int first, int last, qreal *uPrime, FitVector tHat1, FitVector tHat2) 0210 { 0211 int i; 0212 int nPts; /* Number of pts in sub-curve */ 0213 qreal C[2][2]; /* Matrix C */ 0214 qreal X[2]; /* Matrix X */ 0215 qreal det_C0_C1, /* Determinants of matrices */ 0216 det_C0_X, 0217 det_X_C1; 0218 qreal alpha_l, /* Alpha values, left and right */ 0219 alpha_r; 0220 FitVector tmp; /* Utility variable */ 0221 QPointF *curve; 0222 0223 curve = new QPointF[4]; 0224 nPts = last - first + 1; 0225 0226 /* Precomputed rhs for eqn */ 0227 // FitVector A[nPts][2] 0228 QVector< QVector<FitVector> > A(nPts, QVector<FitVector>(2)); 0229 0230 /* Compute the A's */ 0231 for (i = 0; i < nPts; ++i) { 0232 FitVector v1, v2; 0233 v1 = tHat1; 0234 v2 = tHat2; 0235 v1.scale(B1(uPrime[i])); 0236 v2.scale(B2(uPrime[i])); 0237 A[i][0] = v1; 0238 A[i][1] = v2; 0239 } 0240 0241 /* Create the C and X matrices */ 0242 C[0][0] = 0.0; 0243 C[0][1] = 0.0; 0244 C[1][0] = 0.0; 0245 C[1][1] = 0.0; 0246 X[0] = 0.0; 0247 X[1] = 0.0; 0248 0249 for (i = 0; i < nPts; ++i) { 0250 C[0][0] += (A[i][0]).dot(A[i][0]); 0251 C[0][1] += A[i][0].dot(A[i][1]); 0252 /* C[1][0] += V2Dot(&A[i][0], &A[i][1]);*/ 0253 C[1][0] = C[0][1]; 0254 C[1][1] += A[i][1].dot(A[i][1]); 0255 0256 FitVector vfirstp1(points.at(first + i)); 0257 FitVector vfirst(points.at(first)); 0258 FitVector vlast(points.at(last)); 0259 0260 tmp = VectorSub(vfirstp1, 0261 VectorAdd( 0262 VectorScale(vfirst, B0(uPrime[i])), 0263 VectorAdd( 0264 VectorScale(vfirst, B1(uPrime[i])), 0265 VectorAdd( 0266 VectorScale(vlast, B2(uPrime[i])), 0267 VectorScale(vlast, B3(uPrime[i])))))); 0268 0269 0270 X[0] += A[i][0].dot(tmp); 0271 X[1] += A[i][1].dot(tmp); 0272 } 0273 0274 /* Compute the determinants of C and X */ 0275 det_C0_C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1]; 0276 det_C0_X = C[0][0] * X[1] - C[0][1] * X[0]; 0277 det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1]; 0278 0279 /* Finally, derive alpha values */ 0280 if (qFuzzyCompare(det_C0_C1, qreal(0.0))) { 0281 det_C0_C1 = (C[0][0] * C[1][1]) * 10e-12; 0282 if (qFuzzyCompare(det_C0_C1, qreal(0.0))) { 0283 det_C0_C1 = Zero; 0284 } 0285 } 0286 alpha_l = det_X_C1 / det_C0_C1; 0287 alpha_r = det_C0_X / det_C0_C1; 0288 0289 0290 /* If alpha negative, use the Wu/Barsky heuristic (see text) */ 0291 /* (if alpha is 0, you get coincident control points that lead to 0292 * divide by zero in any subsequent NewtonRaphsonRootFind() call. */ 0293 if (alpha_l < 1.0e-6 || alpha_r < 1.0e-6) { 0294 qreal dist = distance(points.at(last), points.at(first)) / 3.0; 0295 0296 curve[0] = points.at(first); 0297 curve[3] = points.at(last); 0298 0299 tHat1.scale(dist); 0300 tHat2.scale(dist); 0301 0302 curve[1] = tHat1 + curve[0]; 0303 curve[2] = tHat2 + curve[3]; 0304 return curve; 0305 } 0306 0307 /* First and last control points of the Bezier curve are */ 0308 /* positioned exactly at the first and last data points */ 0309 /* Control points 1 and 2 are positioned an alpha distance out */ 0310 /* on the tangent vectors, left and right, respectively */ 0311 curve[0] = points.at(first); 0312 curve[3] = points.at(last); 0313 0314 tHat1.scale(alpha_l); 0315 tHat2.scale(alpha_r); 0316 0317 curve[1] = tHat1 + curve[0]; 0318 curve[2] = tHat2 + curve[3]; 0319 0320 return (curve); 0321 } 0322 0323 /* 0324 * Bezier : 0325 * Evaluate a Bezier curve at a particular parameter value 0326 * 0327 */ 0328 static QPointF BezierII(int degree, QPointF *V, qreal t) 0329 { 0330 int i, j; 0331 QPointF Q; /* Point on curve at parameter t */ 0332 QPointF *Vtemp; /* Local copy of control points */ 0333 0334 Vtemp = new QPointF[degree+1]; 0335 0336 for (i = 0; i <= degree; ++i) { 0337 Vtemp[i] = V[i]; 0338 } 0339 0340 /* Triangle computation */ 0341 for (i = 1; i <= degree; ++i) { 0342 for (j = 0; j <= degree - i; ++j) { 0343 Vtemp[j].setX((1.0 - t) * Vtemp[j].x() + t * Vtemp[j+1].x()); 0344 Vtemp[j].setY((1.0 - t) * Vtemp[j].y() + t * Vtemp[j+1].y()); 0345 } 0346 } 0347 0348 Q = Vtemp[0]; 0349 delete[] Vtemp; 0350 return Q; 0351 } 0352 0353 /* 0354 * ComputeMaxError : 0355 * Find the maximum squared distance of digitized points 0356 * to fitted curve. 0357 */ 0358 static qreal ComputeMaxError(const QList<QPointF> &points, int first, int last, QPointF *curve, qreal *u, int *splitPoint) 0359 { 0360 int i; 0361 qreal maxDist; /* Maximum error */ 0362 qreal dist; /* Current error */ 0363 QPointF P; /* Point on curve */ 0364 FitVector v; /* Vector from point to curve */ 0365 0366 *splitPoint = (last - first + 1) / 2; 0367 maxDist = 0.0; 0368 for (i = first + 1; i < last; ++i) { 0369 P = BezierII(3, curve, u[i-first]); 0370 v = VectorSub(P, points.at(i)); 0371 dist = v.length(); 0372 if (dist >= maxDist) { 0373 maxDist = dist; 0374 *splitPoint = i; 0375 } 0376 } 0377 return (maxDist); 0378 } 0379 0380 0381 /* 0382 * NewtonRaphsonRootFind : 0383 * Use Newton-Raphson iteration to find better root. 0384 */ 0385 static qreal NewtonRaphsonRootFind(QPointF *Q, QPointF P, qreal u) 0386 { 0387 qreal numerator, denominator; 0388 QPointF Q1[3], Q2[2]; /* Q' and Q'' */ 0389 QPointF Q_u, Q1_u, Q2_u; /*u evaluated at Q, Q', & Q'' */ 0390 qreal uPrime; /* Improved u */ 0391 int i; 0392 0393 /* Compute Q(u) */ 0394 Q_u = BezierII(3, Q, u); 0395 0396 /* Generate control vertices for Q' */ 0397 for (i = 0; i <= 2; ++i) { 0398 Q1[i].setX((Q[i+1].x() - Q[i].x()) * 3.0); 0399 Q1[i].setY((Q[i+1].y() - Q[i].y()) * 3.0); 0400 } 0401 0402 /* Generate control vertices for Q'' */ 0403 for (i = 0; i <= 1; ++i) { 0404 Q2[i].setX((Q1[i+1].x() - Q1[i].x()) * 2.0); 0405 Q2[i].setY((Q1[i+1].y() - Q1[i].y()) * 2.0); 0406 } 0407 0408 /* Compute Q'(u) and Q''(u) */ 0409 Q1_u = BezierII(2, Q1, u); 0410 Q2_u = BezierII(1, Q2, u); 0411 0412 /* Compute f(u)/f'(u) */ 0413 numerator = (Q_u.x() - P.x()) * (Q1_u.x()) + (Q_u.y() - P.y()) * (Q1_u.y()); 0414 denominator = (Q1_u.x()) * (Q1_u.x()) + (Q1_u.y()) * (Q1_u.y()) + 0415 (Q_u.x() - P.x()) * (Q2_u.x()) + (Q_u.y() - P.y()) * (Q2_u.y()); 0416 0417 if (qFuzzyCompare(denominator, qreal(0.0))) { 0418 denominator = Zero; 0419 } 0420 0421 /* u = u - f(u)/f'(u) */ 0422 uPrime = u - (numerator / denominator); 0423 return (uPrime); 0424 } 0425 0426 /* 0427 * Reparameterize: 0428 * Given set of points and their parameterization, try to find 0429 * a better parameterization. 0430 * 0431 */ 0432 static qreal *Reparameterize(const QList<QPointF> &points, int first, int last, qreal *u, QPointF *curve) 0433 { 0434 int nPts = last - first + 1; 0435 int i; 0436 qreal *uPrime; /* New parameter values */ 0437 0438 uPrime = new qreal[nPts]; 0439 for (i = first; i <= last; ++i) { 0440 uPrime[i-first] = NewtonRaphsonRootFind(curve, points.at(i), u[i-first]); 0441 } 0442 return (uPrime); 0443 } 0444 0445 QPointF *FitCubic(const QList<QPointF> &points, int first, int last, FitVector tHat1, FitVector tHat2, float error, int &width) 0446 { 0447 qreal *u; 0448 qreal *uPrime; 0449 qreal maxError; 0450 int splitPoint; 0451 int nPts; 0452 qreal iterationError; 0453 int maxIterations = 4; 0454 FitVector tHatCenter; 0455 QPointF *curve; 0456 int i; 0457 0458 width = 0; 0459 0460 0461 iterationError = error * error; 0462 nPts = last - first + 1; 0463 0464 if (nPts == 2) { 0465 qreal dist = distance(points.at(last), points.at(first)) / 3.0; 0466 0467 curve = new QPointF[4]; 0468 0469 curve[0] = points.at(first); 0470 curve[3] = points.at(last); 0471 0472 tHat1.scale(dist); 0473 tHat2.scale(dist); 0474 0475 curve[1] = tHat1 + curve[0]; 0476 curve[2] = tHat2 + curve[3]; 0477 0478 width = 4; 0479 return curve; 0480 } 0481 0482 /* Parameterize points, and attempt to fit curve */ 0483 u = ChordLengthParameterize(points, first, last); 0484 curve = GenerateBezier(points, first, last, u, tHat1, tHat2); 0485 0486 0487 /* Find max deviation of points to fitted curve */ 0488 maxError = ComputeMaxError(points, first, last, curve, u, &splitPoint); 0489 if (maxError < error) { 0490 delete[] u; 0491 width = 4; 0492 return curve; 0493 } 0494 0495 0496 /* If error not too large, try some reparameterization */ 0497 /* and iteration */ 0498 if (maxError < iterationError) { 0499 for (i = 0; i < maxIterations; ++i) { 0500 uPrime = Reparameterize(points, first, last, u, curve); 0501 delete[] curve; 0502 curve = GenerateBezier(points, first, last, uPrime, tHat1, tHat2); 0503 maxError = ComputeMaxError(points, first, last, 0504 curve, uPrime, &splitPoint); 0505 if (maxError < error) { 0506 delete[] u; 0507 delete[] uPrime; 0508 width = 4; 0509 return curve; 0510 } 0511 delete[] u; 0512 u = uPrime; 0513 } 0514 } 0515 0516 /* Fitting failed -- split at max error point and fit recursively */ 0517 delete[] u; 0518 delete[] curve; 0519 tHatCenter = ComputeCenterTangent(points, splitPoint); 0520 0521 int w1, w2; 0522 QPointF *cu1 = 0, *cu2 = 0; 0523 cu1 = FitCubic(points, first, splitPoint, tHat1, tHatCenter, error, w1); 0524 0525 tHatCenter.negate(); 0526 cu2 = FitCubic(points, splitPoint, last, tHatCenter, tHat2, error, w2); 0527 0528 QPointF *newcurve = new QPointF[w1+w2]; 0529 for (int i = 0; i < w1; ++i) { 0530 newcurve[i] = cu1[i]; 0531 } 0532 for (int i = 0; i < w2; ++i) { 0533 newcurve[i+w1] = cu2[i]; 0534 } 0535 0536 delete[] cu1; 0537 delete[] cu2; 0538 width = w1 + w2; 0539 return newcurve; 0540 } 0541 0542 0543 KoPathShape * bezierFit(const QList<QPointF> &points, float error) 0544 { 0545 FitVector tHat1, tHat2; 0546 0547 tHat1 = ComputeLeftTangent(points, 0); 0548 tHat2 = ComputeRightTangent(points, points.count() - 1); 0549 0550 int width = 0; 0551 QPointF *curve; 0552 curve = FitCubic(points, 0, points.count() - 1, tHat1, tHat2, error, width); 0553 0554 KoPathShape * path = new KoPathShape(); 0555 0556 if (width > 3) { 0557 path->moveTo(curve[0]); 0558 path->curveTo(curve[1], curve[2], curve[3]); 0559 for (int i = 4; i < width; i += 4) { 0560 path->curveTo(curve[i+1], curve[i+2], curve[i+3]); 0561 } 0562 } 0563 0564 delete[] curve; 0565 return path; 0566 } 0567