Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spline from fit_from_points has discontinuity #28

Open
nicolaschan opened this issue Feb 23, 2024 · 2 comments
Open

Spline from fit_from_points has discontinuity #28

nicolaschan opened this issue Feb 23, 2024 · 2 comments

Comments

@nicolaschan
Copy link

nicolaschan commented Feb 23, 2024

There are sometimes discontinuities in the bezier spline given from Curve::fit_from_points. I expect the spline to consist of connecting curves. Since the spline does connect most of the time, I think it must be a bug when it isn't connected.

I've attached an example with points that trigger this bug with flo_curves = "0.7.2".

mod test {
    use flo_curves::{bezier, BezierCurve, BezierCurveFactory, Coord2};

    #[test]
    fn test_demonstrate_discontinuity() {
        let points = vec![
            Coord2(72.0, 216.0),
            Coord2(73.0, 217.0),
            Coord2(74.0, 218.0),
            Coord2(75.0, 219.0),
            Coord2(74.0, 220.0),
            Coord2(75.0, 221.0),
            Coord2(75.0, 222.0),
            Coord2(75.0, 223.0),
            Coord2(76.0, 224.0),
            Coord2(77.0, 225.0),
            Coord2(77.0, 226.0),
            Coord2(77.0, 227.0),
            Coord2(78.0, 227.0),
            Coord2(79.0, 227.0),
            Coord2(80.0, 228.0),
            Coord2(79.0, 229.0),
            Coord2(80.0, 230.0),
            Coord2(81.0, 231.0),
            Coord2(82.0, 232.0),
            Coord2(82.0, 233.0),
            Coord2(82.0, 234.0),
            Coord2(83.0, 235.0),
            Coord2(83.0, 236.0),
            Coord2(83.0, 237.0),
            Coord2(84.0, 238.0),
            Coord2(85.0, 239.0),
            Coord2(84.0, 240.0),
            Coord2(83.0, 241.0),
            Coord2(82.0, 241.0),
            Coord2(81.0, 241.0),
            Coord2(80.0, 242.0),
            Coord2(80.0, 243.0),
            Coord2(80.0, 244.0),
            Coord2(81.0, 245.0),
            Coord2(80.0, 246.0),
            Coord2(81.0, 247.0),
            Coord2(82.0, 248.0),
            Coord2(83.0, 249.0),
            Coord2(84.0, 250.0),
            Coord2(84.0, 251.0),
            Coord2(84.0, 252.0),
            Coord2(83.0, 251.0),
            Coord2(82.0, 251.0),
            Coord2(81.0, 251.0),
            Coord2(80.0, 250.0),
            Coord2(81.0, 249.0),
            Coord2(80.0, 248.0),
            Coord2(79.0, 247.0),
            Coord2(78.0, 246.0),
            Coord2(78.0, 245.0),
            Coord2(78.0, 244.0),
            Coord2(77.0, 243.0),
            Coord2(76.0, 242.0),
            Coord2(76.0, 241.0),
            Coord2(76.0, 240.0),
            Coord2(75.0, 239.0),
            Coord2(75.0, 238.0),
            Coord2(75.0, 237.0),
            Coord2(74.0, 237.0),
            Coord2(73.0, 237.0),
            Coord2(72.0, 236.0),
            Coord2(73.0, 235.0),
            Coord2(72.0, 234.0),
            Coord2(71.0, 233.0),
            Coord2(71.0, 232.0),
            Coord2(71.0, 231.0),
            Coord2(70.0, 230.0),
            Coord2(69.0, 229.0),
            Coord2(68.0, 228.0),
            Coord2(67.0, 227.0),
            Coord2(66.0, 226.0),
            Coord2(65.0, 225.0),
            Coord2(65.0, 224.0),
            Coord2(65.0, 223.0),
            Coord2(64.0, 223.0),
            Coord2(63.0, 223.0),
            Coord2(64.0, 222.0),
            Coord2(65.0, 222.0),
            Coord2(66.0, 222.0),
            Coord2(65.0, 221.0),
            Coord2(66.0, 220.0),
            Coord2(66.0, 219.0),
            Coord2(66.0, 218.0),
            Coord2(65.0, 217.0),
            Coord2(65.0, 216.0),
            Coord2(65.0, 215.0),
            Coord2(64.0, 214.0),
            Coord2(64.0, 213.0),
            Coord2(64.0, 212.0),
            Coord2(64.0, 211.0),
            Coord2(64.0, 210.0),
            Coord2(63.0, 209.0),
            Coord2(62.0, 208.0),
            Coord2(62.0, 207.0),
            Coord2(62.0, 206.0),
            Coord2(61.0, 205.0),
            Coord2(62.0, 204.0),
            Coord2(61.0, 204.0),
            Coord2(60.0, 204.0),
            Coord2(60.0, 203.0),
            Coord2(60.0, 202.0),
            Coord2(59.0, 201.0),
        ];
        let spline = bezier::Curve::fit_from_points(&points, 1.0).unwrap();
        let mut last = None;
        for curve in spline {
            if let Some(last) = last {
                if curve.start_point() != last {
                    panic!(
                        "Discontinuity detected between ({:?}, {:?}) and ({:?}, {:?})",
                        last.0,
                        last.1,
                        curve.start_point().0,
                        curve.start_point().1
                    );
                }
            }
            last = Some(curve.end_point());
        }
    }
}

The output is:

---- example::test::test_demonstrate_discontinuity stdout ----
thread 'example::test::test_demonstrate_discontinuity' panicked at src/example.rs:115:21:
Discontinuity detected between (60.0, 203.0) and (60.0, 202.0)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
@Logicalshift
Copy link
Owner

The algorithm can detect corners, so it's not unexpected that sometimes it will produce two curves where the tangent at the end of one does not match the following curve. In this case, you've specified a maximum error for the fit of 1.0, but only provided coordinates that align on integer boundaries, so there are some corners - there probably aren't a sequence of 'smooth' curves that can match these points to that level of accuracy. That looks like this:

Error 1.0

You can decrease the accuracy of the fit to match the accuracy of your samples. For example, with a maximum error of 3.0, you'll get this:

Error 3.0

Another approach would be to filter the input to reduce the noise and then use a higher level of fit. For instance, applying a gaussian filter to the input of the fitting function and a maximum error of 0.5 would create a result like this:

Error 0.5, Gaussian filter radius 5

Averaging together neighboring points would also smooth out the results a bit, but a gaussian filter provides considerably more control over the result. I've put the code I used to generate these examples here: https://github.com/Logicalshift/flo_curves/blob/v0.8/demos/curve_fit/src/main.rs

The fitting function isn't really doing anything incorrect in the first case, though: it's meant to detect sudden changes in direction and interpret them as corners, and with a small error your data set has a lot of changes in direction.

@nicolaschan
Copy link
Author

Thanks for the explanation! I really like the Gaussian filter idea.

On my end I've been filling the discontinuity by connecting them with a line, but that might not make sense for the library to do internally. As you said, it indicates that my points need some preprocessing anyway.

P.S. Thanks for the library. It's the best bezier curve fitter I've found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants