Fix floating point error in bezier bbox calculation

pull/219/head
Bouhek 2024-02-01 13:04:40 +01:00
parent fcb648b9bb
commit de1eb22b16
3 changed files with 55 additions and 4 deletions

View File

@ -10,7 +10,7 @@ from numpy import poly1d
# Internal dependencies
from .polytools import real, imag, polyroots, polyroots01
from .constants import FLOAT_EPSILON
# Evaluation ##################################################################
@ -171,7 +171,7 @@ def bezier_real_minmax(p):
if len(p) == 4: # cubic case
a = [p.real for p in p]
denom = a[0] - 3*a[1] + 3*a[2] - a[3]
if denom != 0:
if abs(denom) > FLOAT_EPSILON: # check that denom != 0 accounting for floating point error
delta = a[1]**2 - (a[0] + a[1])*a[2] + a[2]**2 + (a[0] - a[1])*a[3]
if delta >= 0: # otherwise no local extrema
sqdelta = sqrt(delta)

View File

@ -0,0 +1,3 @@
"""This submodule contains constants used throughout the project."""
FLOAT_EPSILON = 1e-12

View File

@ -1,8 +1,8 @@
from __future__ import division, absolute_import, print_function
import numpy as np
import unittest
from svgpathtools.bezier import bezier_point, bezier2polynomial, polynomial2bezier
from svgpathtools.path import bpoints2bezier
from svgpathtools.bezier import bezier_point, bezier2polynomial, polynomial2bezier, bezier_bounding_box, bezier_real_minmax
from svgpathtools.path import bpoints2bezier, CubicBezier
class HigherOrderBezier:
@ -54,5 +54,53 @@ class TestPolynomial2Bezier(unittest.TestCase):
self.assertAlmostEqual(b.point(t), p(t), msg=msg)
class TestBezierBoundingBox(unittest.TestCase):
def test_bezier_bounding_box(self):
# This bezier curve has denominator == 0 but due to floating point arithmetic error it is not exactly 0
zero_denominator_bezier_curve = CubicBezier(612.547 + 109.3261j, 579.967 - 19.4422j, 428.0344 - 19.4422j, 395.4374 + 109.3261j)
zero_denom_xmin, zero_denom_xmax, zero_denom_ymin, zero_denom_ymax = bezier_bounding_box(zero_denominator_bezier_curve)
self.assertAlmostEqual(zero_denom_xmin, 395.437400, 5)
self.assertAlmostEqual(zero_denom_xmax, 612.547, 5)
self.assertAlmostEqual(zero_denom_ymin, 12.7498749, 5)
self.assertAlmostEqual(zero_denom_ymax, 109.3261, 5)
# This bezier curve has global extrema at the start and end points
start_end_bbox_bezier_curve = CubicBezier(886.8238 + 354.8439j, 884.4765 + 340.5983j, 877.6258 + 330.0518j, 868.2909 + 323.2453j)
start_end_xmin, start_end_xmax, start_end_ymin, start_end_ymax = bezier_bounding_box(start_end_bbox_bezier_curve)
self.assertAlmostEqual(start_end_xmin, 868.2909, 5)
self.assertAlmostEqual(start_end_xmax, 886.8238, 5)
self.assertAlmostEqual(start_end_ymin, 323.2453, 5)
self.assertAlmostEqual(start_end_ymax, 354.8439, 5)
# This bezier curve is to cover some random case where at least one of the global extrema is not the start or end point
general_bezier_curve = CubicBezier(295.2282 + 402.0233j, 310.3734 + 355.5329j, 343.547 + 340.5983j, 390.122 + 355.7018j)
general_xmin, general_xmax, general_ymin, general_ymax = bezier_bounding_box(general_bezier_curve)
self.assertAlmostEqual(general_xmin, 295.2282, 5)
self.assertAlmostEqual(general_xmax, 390.121999999, 5)
self.assertAlmostEqual(general_ymin, 350.030030142, 5)
self.assertAlmostEqual(general_ymax, 402.0233, 5)
class TestBezierRealMinMax(unittest.TestCase):
def test_bezier_real_minmax(self):
# This bezier curve has denominator == 0 but due to floating point arithmetic error it is not exactly 0
zero_denominator_bezier_curve = [109.3261, -19.4422, -19.4422, 109.3261]
zero_denominator_minmax = bezier_real_minmax(zero_denominator_bezier_curve)
self.assertAlmostEqual(zero_denominator_minmax[0], 12.7498749, 5)
self.assertAlmostEqual(zero_denominator_minmax[1], 109.3261, 5)
# This bezier curve has global extrema at the start and end points
start_end_bbox_bezier_curve = [354.8439, 340.5983, 330.0518, 323.2453]
start_end_bbox_minmax = bezier_real_minmax(start_end_bbox_bezier_curve)
self.assertAlmostEqual(start_end_bbox_minmax[0], 323.2453, 5)
self.assertAlmostEqual(start_end_bbox_minmax[1], 354.8439, 5)
# This bezier curve is to cover some random case where at least one of the global extrema is not the start or end point
general_bezier_curve = [402.0233, 355.5329, 340.5983, 355.7018]
general_minmax = bezier_real_minmax(general_bezier_curve)
self.assertAlmostEqual(general_minmax[0], 350.030030142, 5)
self.assertAlmostEqual(general_minmax[1], 402.0233, 5)
if __name__ == '__main__':
unittest.main()