4

Is there a way to draw a smooth line through a set of points in QT? The number and position of the points is set during run time.

Currently, I draw a QPainterPath which contains lineTo's going from point to point, creating a path. I do use render-hints antialiasing but the path is still jagged.

I've seen QSplineSeries which seems to give this kind of curved path but it is not available in Qt4.8, which is the QT Version I'm using.

Another option that is being suggested often is using Bezier Curves but those use one start and end point and two control point, so I would need to calculate it for every segment (every lineTo) and somehow calculate those control points which I don't have at the moment.

Damir Porobic
  • 681
  • 1
  • 8
  • 21

3 Answers3

7

In the end I've implemented some kind of workaround which basically takes two connected lines, removes the connection point between them and replaces it with a curve. As I have a lot of small lines where such a change would not be visible I remove all lines that are very short and reconnect the open ends. The function was mostly provided by Bojan Kverh, check out his tutorial: https://www.toptal.com/c-plus-plus/rounded-corners-bezier-curves-qpainter

And here the functions:

namespace
{
    float distance(const QPointF& pt1, const QPointF& pt2)
    {
        float hd = (pt1.x() - pt2.x()) * (pt1.x() - pt2.x());
        float vd = (pt1.y() - pt2.y()) * (pt1.y() - pt2.y());
        return std::sqrt(hd + vd);
    }

    QPointF getLineStart(const QPointF& pt1, const QPointF& pt2)
    {
        QPointF pt;
        float rat = 10.0 / distance(pt1, pt2);
        if (rat > 0.5) {
            rat = 0.5;
        }
        pt.setX((1.0 - rat) * pt1.x() + rat * pt2.x());
        pt.setY((1.0 - rat) * pt1.y() + rat * pt2.y());
        return pt;
    }

    QPointF getLineEnd(const QPointF& pt1, const QPointF& pt2)
    {
        QPointF pt;
        float rat = 10.0 / distance(pt1, pt2);
        if (rat > 0.5) {
            rat = 0.5;
        }
        pt.setX(rat * pt1.x() + (1.0 - rat)*pt2.x());
        pt.setY(rat * pt1.y() + (1.0 - rat)*pt2.y());
        return pt;
    }

}

void PainterPath::smoothOut(const float& factor)
{
    QList<QPointF> points;
    QPointF p;
    for (int i = 0; i < mPath->elementCount() - 1; i++) {
        p = QPointF(mPath->elementAt(i).x, mPath->elementAt(i).y);

        // Except for first and last points, check what the distance between two
        // points is and if its less then min, don't add them to the list.
        if (points.count() > 1 && (i < mPath->elementCount() - 2) && (distance(points.last(), p) < factor)) {
            continue;
        }
        points.append(p);
    }

    // Don't proceed if we only have 3 or less points.
    if (points.count() < 3) {
        return;
    }

    QPointF pt1;
    QPointF pt2;
    QPainterPath* path = new QPainterPath();
    for (int i = 0; i < points.count() - 1; i++) {
        pt1 = getLineStart(points[i], points[i + 1]);
        if (i == 0) {
            path->moveTo(pt1);
        } else {
            path->quadTo(points[i], pt1);
        }
        pt2 = getLineEnd(points[i], points[i + 1]);
        path->lineTo(pt2);
    }

    delete mPath;
    mPath = path;
    prepareGeometryChange();
}
Damir Porobic
  • 681
  • 1
  • 8
  • 21
1

I don't think that there is an out-of-the-box solution in Qt 4.8 (as you have noticed QSplineSeries is a Qt 5.x feature). Also QSplineSeries is part of QtCharts module which is a commercial one (like QtDataVisualization) so unless you have a commercial license or your project is GPL you can't use it.

You have to do it manually that is go through the math required for it and implement it yourself (or find a nice implementation (not necessary to be even in C++ let alone Qt-compatible)).

Since you have mentioned Bezier curves I would suggest giving the composite Bezier curve a shot. I remember implementing that thing for a project I worked on. It required some...work. :D This article might help you get started.

Bezier curves are in fact B-splines (if I remember correctly). Especially if you can settle with a certain lack of smoothness you can generate composite Bezier curves pretty fast. To to their robustness and popularity I'm 100% sure that you can find a decent implementation online. Probably not Qt-friendly but if written properly you should be able to adapt the code in no time.

This looks quite promising (it's in ActionScript but meh). Or you can given the QPainterPath::cubicTo() a shot which can create Bezier curves for you given you can also provide the two control points required for the calculation of the curve.

rbaleksandar
  • 8,713
  • 7
  • 76
  • 161
  • Yeah, I was afraid that there is no simple solution to that, I'll have a second look at the Bezier Curves and B-Splines. – Damir Porobic Nov 23 '16 at 12:35
  • Sorry for the bad news. :D I've updated my answer with a very useful link (uses ActionScript for the implementation). – rbaleksandar Nov 23 '16 at 12:45
  • Thanks, it looks interesting though messy, I'll give it a try :D – Damir Porobic Nov 23 '16 at 13:31
  • Nothing's perfect. :P – rbaleksandar Nov 23 '16 at 13:57
  • If you use Bezier curve, the curve will not go through points. – m. c. Nov 23 '16 at 18:34
  • The points can be used to calculate the points required for the curve and then approximate all that to fit the curve to the points. It will however not be that accurate, that I agree with. – rbaleksandar Nov 24 '16 at 02:44
  • It doesn't have to be 100% accurate, though it should be close, some compromise between precision and smoothness. But thanks for pointing this out. – Damir Porobic Nov 24 '16 at 10:17
  • Btw if the number of points you have is relatively large you can simply connect these with straight lines since at a given high density the human eye will not be able to make the difference (unless zooming in enough to see the choppiness) and it will appear as it's all smooth and nice. This will also make everything much simpler and you will not be required to do complicated math stuff. – rbaleksandar Nov 24 '16 at 10:30
1

Pretty much everyone uses cubic interpolation for this task, and your choice is a Bezier Curve or a Catmull-Rom spline. If you must hit every point, then you need to keep the "handles" or the line between the control points of the Beziers straight. You then fit using least squares, which as you have found out is a bit involved.

Catmull Rom splines have the advantage that they only need two extra control points (start and end, simply mirror points to create them). As long as the points are reasonably smooth, the line will be well-behaved. It's unlikely that QT graphics will draw CatMull Rom splines directly, so convert to Beziers, that's a standard published method, you can go from Catmull Rom to Bezier quite easily, though not the reverse - not every Bezier can be represented by a Catmull Rom with only a few points.

You can use other interpolation methods, eq quintic, if cubics won't give you the curve you desire.

Malcolm McLean
  • 6,258
  • 1
  • 17
  • 18