├── LICENSE ├── README.md ├── ch_01 ├── meet_sympy_numeric.py └── meet_sympy_symbolic.py ├── ch_03 └── ray_triangle_intersection.py ├── ch_04 ├── affine_transformation.py ├── linear_transformation.py ├── matrix_composition.py ├── matrix_inversion.py └── projective_transformation_in_steps.py ├── ch_05 ├── arc_and_segment.py ├── better_arc_and_segment.py └── biarc.py ├── ch_06 ├── approximate.py ├── both_approximate_and_interpolate.py └── interpolate.py ├── ch_07 ├── bezier_2.py ├── bezier_3.py ├── bezier_rational.py ├── nurbs_circle.py ├── parametric_polynomial.py ├── parametric_polynomial_tangents.py ├── sine_integral_odd.py ├── sine_ipol.py ├── sine_ipol_circle.py ├── sine_naive.py ├── sine_odd.py ├── sine_series.py └── sine_series_circle.py ├── ch_08 ├── bilinear_transformation.py ├── biquadratic_interpolation.py ├── deformed_mushrooms.py ├── getting_cubic_interpolation.py ├── getting_quadratic_interpolation.py ├── linear_cubic_interpolation.py ├── mushrooms.py └── ugly_mushroom.py ├── ch_09 ├── nd_box.py └── nd_cross.py ├── ch_10 ├── sdf_circle.py ├── sdf_disks_intersection.py ├── sdf_disks_offset.py ├── sdf_disks_subtraction.py ├── sdf_disks_union.py ├── sdf_gyroid_parametric.py ├── sdf_hollow.py ├── sdf_metaballs_quasi_inverse.py ├── sdf_multifocal_lemniscate.py ├── sdf_rectangle.py ├── sdf_simpler_rectangle.py └── sdf_triangle.py ├── ch_11 └── smooth_contour.py └── ch_12 ├── denoise.py └── image_vectorization.py /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # geometry-for-programmers-code 2 | The source code for the [Geometry for Programmers](https://www.manning.com/books/geometry-for-programmers) book. 3 | 4 | - On Manning: https://www.manning.com/books/geometry-for-programmers 5 | - On Good Reads: https://www.goodreads.com/book/show/61294688-geometry-for-programmers 6 | - On Amazon: https://www.amazon.com/Geometry-Programmers-Oleksandr-Kaleniuk/dp/1633439607 7 | 8 | ![The book looks like this](https://m.media-amazon.com/images/I/41IZGlCh+hL._SX397_BO1,204,203,200_.jpg) 9 | 10 | All the code is in Python. There are no specific dependencies on modern Python features or obscure libraries, so any generic 3.x Python will do. 11 | 12 | The code samples only use: 13 | 1. SymPy; 14 | 2. NumPy; 15 | 3. Matplotlib. 16 | 17 | In early chapters, the only working tool is SymPy. We will make it solve equations and write code for us. In later chapters, the code illustrates specific algorithms and as such relies on numeric computations and visualization. That's what we need NumPy and Matplotlib for. 18 | 19 | In general, I encourage you to treat these samples as samples and implement the algorithms in your language of preference. 20 | 21 | 22 | ## 1. Getting started 23 | In this chapter, the reader will learn about applied geometry and its relation to mathematics, engineering, and computer science. The chapter showcases some of its modern applications in computer-aided design and game development. There will also be a comforting list of knowledge and skills the reader is expected to have, of which the most exotic, namely computer algebra system SymPy, will be taught on spot with a brief but good enough to get started tutorial. 24 | 25 | ## 2. Terminology and jargon (no code) 26 | In this chapter, the reader will learn the basic language of applied geometry. In the end, they should be comfortable operating with terms like “near-degenerate triangle”, “non-manifold mesh”, or “continuous transformation”. 27 | 28 | ## 3. Geometry of linear systems 29 | This chapter will explain the linear systems to the reader as if they were systems of lines, planes, and hyperplanes. Which, of course, from the geometric point of view, they are. The chapter will show the conditions in which such systems have computable solutions. There will be practical guidance on choosing the best algorithm for the job, and for that, an iterative linear system solving algorithm, and a direct solving algorithm will be explained graphically. In the end, the reader will be able to evaluate a linear system and solve it programmatically using the best possible tool from any library or toolkit that provides one. 30 | 31 | ## 4. Projective geometric transformations 32 | From this chapter, the reader will learn how the most common geometric transformations such as rotation, scale, and translation, are all generalized and extended by the projective transformation. The reader will also learn the concept of homogeneous coordinates: how adding a single number to a vector allows us to combine all the possible projective transformations into a single operation which, in practice, makes understanding transformations a little harder, but programming – much simpler. 33 | 34 | ## 5. Geometry of calculus 35 | From this chapter, the reader will learn how calculus is connected to the geometric properties of curves and surfaces. And how to use these properties to program curves and surfaces in practice. This chapter doesn’t require the reader to be familiar with calculus, its basic concepts will be explained briefly but sufficiently to make the rest of the book comprehensible. 36 | 37 | ## 6. Polynomials 38 | In this chapter, the reader will learn how to use polynomials as some kind of digital clay – a building material for arbitrary functions with requested properties. There will also be an explanation of the polynomial modeling downsides along with the ways to mitigate these downsides. 39 | 40 | ## 7. Splines 41 | In this chapter, the reader will learn how to craft custom curves and surfaces with splines. The chapter will explain the general idea of splines, propose a few well-known approaches such as Bezier splines and NURBS, but also will give all the necessary toolset so the user could design their own polynomial splines with the properties they want. 42 | 43 | ## 8. Non-projective geometric transformations 44 | In this chapter, the reader will learn about signed distance functions, and why they are becoming more and more popular in modern computer-aided design. The chapter will explain what are the benefits of signed distance functions, what are the drawbacks, how to create one, and how to turn one into a contour or mesh. 45 | 46 | ## 9. Geometry of vector algebra 47 | In this chapter, the basics of vector algebra will be presented from a geometric perspective. The reader will learn how do vector products work, how do they generalize, and how to use their geometric properties to solve real-world problems. 48 | 49 | ## 10. Modeling shapes with signed distance functions 50 | In this chapter, the reader will learn about signed distance functions, and why they become more and more popular in the modern computer-aided design. What are the benefits, what are the drawbacks, how to create one, and how to turn one into a contour or mesh. 51 | 52 | ## 11. Modeling surfaces with boundary representations and triangle meshes 53 | From this chapter, the reader will learn more classical ways to model surfaces and bodies. Meshes are mostly used in game development and 3D printing, while boundary representations are mostly popular in computer-aided design. Some of the hybrid applications are emerging on the market right now, so it becomes important to know how these models compare to each other, and this chapter addresses this too. 54 | 55 | ## 12. Modeling bodies with images and voxels 56 | In this chapter, the reader will learn about modeling bodies with 3D images and voxels. 3D images came to the software industry from medical applications such as computer tomography, but they are now used more and more in mechanical engineering too. This chapter showcases a few image processing techniques and explains a simple vectorization algorithm that turns images into contours. 57 | -------------------------------------------------------------------------------- /ch_01/meet_sympy_numeric.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | # Va - Amsterdam train speed 4 | # Vp - Paris train speed 5 | Va, Vp = symbols('Va Vp') 6 | 7 | solution = solve([ 8 | Vp - Va * 2, 9 | Va * 1 + Vp * 1 - 450 10 | ], (Va, Vp)) 11 | 12 | print(solution) 13 | -------------------------------------------------------------------------------- /ch_01/meet_sympy_symbolic.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | # Va - Amsterdam train speed 4 | # Vp - Paris train speed 5 | # Vpx - a ratio Vp/Va (used to be 2) 6 | # D - the distance in between Paris and Amsterdam (450) 7 | # t - the time before the trains meet (1) 8 | Va, Vp, Vpx, D, t = symbols('Va Vp Vpx D t') 9 | 10 | solution = solve([ 11 | Vp - Va * Vpx, 12 | Va * t + Vp * t - D 13 | ], (Va, Vp)) 14 | 15 | print(solution) 16 | print(pycode(solution)) 17 | print(latex(solution)) 18 | -------------------------------------------------------------------------------- /ch_03/ray_triangle_intersection.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | # the point from which the ray starts 4 | Px, Py, Pz = symbols('Px Py Pz') 5 | 6 | # the ray’s direction vector 7 | dx, dy, dz = symbols('dx dy dz') 8 | 9 | # point A of the ABC triangle 10 | Ax, Ay, Az = symbols('Ax Ay Az') 11 | 12 | # vector A to B of the ABC triangle 13 | ABx, ABy, ABz = symbols('ABx ABy ABz') 14 | 15 | # vector A to C of the ABC triangle 16 | ACx, ACy, ACz = symbols('ACx ACy ACz') 17 | 18 | # parameters that specify the point on a ray and point on a triangle’s plane 19 | t, u, v = symbols('t u v') 20 | 21 | solution = solve([ 22 | Px +t*dx - (Ax + ABx*u + ACx*v), 23 | Py +t*dy - (Ay + ABy*u + ACy*v), 24 | Pz +t*dz - (Az + ABz*u + ACz*v) 25 | ], (t, u, v)) 26 | 27 | print(' the whole solution:') 28 | print (pycode(solution)) 29 | 30 | print('') 31 | print(' divisor with collected dx, dy, dz:') 32 | print(collect(ABx*ACy*dz - ABx*ACz*dy - ABy*ACx*dz + ABy*ACz*dx + ABz*ACx*dy - ABz*ACy*dx, (dx, dy, dz))) 33 | 34 | print('') 35 | print(' t with collected ACx, ACy, ACz:') 36 | print(collect(Ax*ABy*ACz - Ax*ABz*ACy - Ay*ABx*ACz + Ay*ABz*ACx + Az*ABx*ACy - Az*ABy*ACx - ABx*ACy*Pz + ABx*ACz*Py + ABy*ACx*Pz - ABy*ACz*Px - ABz*ACx*Py + ABz*ACy*Px, (ACx, ACy, ACz))) 37 | 38 | print('') 39 | print(' u with collected dx, dy, dz:') 40 | print(collect(-Ax*ACy*dz + Ax*ACz*dy + Ay*ACx*dz - Ay*ACz*dx - Az*ACx*dy + Az*ACy*dx - ACx*Py*dz + ACx*Pz*dy + ACy*Px*dz - ACy*Pz*dx - ACz*Px*dy + ACz*Py*dx, (dx, dy, dz))) 41 | 42 | print('') 43 | print(' v with collected dx, dy, dz:') 44 | print(collect(Ax*ABy*dz - Ax*ABz*dy - Ay*ABx*dz + Ay*ABz*dx + Az*ABx*dy - Az*ABy*dx + ABx*Py*dz - ABx*Pz*dy - ABy*Px*dz + ABy*Pz*dx + ABz*Px*dy - ABz*Py*dx, (dx, dy, dz))) 45 | -------------------------------------------------------------------------------- /ch_04/affine_transformation.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | # Affine transformation in 2D looks like this: 4 | # xt = a11*x + a12 *y + a13; 5 | # yt = a21*x + a22*y + a23, 6 | # we need 6 coefficients to define it. 7 | a11, a12, a13, a21, a22, a23 = symbols('a11 a12 a13 a21 a22 a23') 8 | 9 | # It is a 3-point transformation so we need to define 3 starting points. 10 | x1, y1, x2, y2, x3, y3 = symbols('x1 y1 x2 y2 x3 y3') 11 | 12 | # And, since we are going to infer the coefficients from the actual 13 | # transformation of 3 points, we need to define the 3 ending points as well. 14 | xt1, yt1, xt2, yt2, xt3, yt3 = symbols('xt1 yt1 xt2 yt2 xt3 yt3') 15 | 16 | solution = solve([ 17 | a11 * x1 + a12 * y1 + a13 - xt1, 18 | a21 * x1 + a22 * y1 + a23 - yt1, 19 | a11 * x2 + a12 * y2 + a13 - xt2, 20 | a21 * x2 + a22 * y2 + a23 - yt2, 21 | a11 * x3 + a12 * y3 + a13 - xt3, 22 | a21 * x3 + a22 * y3 + a23 - yt3 23 | ], (a11, a12, a13, a21, a22, a23)) 24 | 25 | print(solution) 26 | -------------------------------------------------------------------------------- /ch_04/linear_transformation.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | # four coefficients for the linear transformation 4 | a11, a12, a21, a22 = symbols('a11 a12 a21 a22') 5 | 6 | # two points before the transformation 7 | x1, y1, x2, y2 = symbols('x1 y1 x2 y2') 8 | 9 | # two points after the transforamtion 10 | xt1, yt1, xt2, yt2 = symbols('xt1 yt1 xt2 yt2') 11 | 12 | solution = solve([ 13 | a11 * x1 + a12 * y1 - xt1, 14 | a21 * x1 + a22 * y1 - yt1, 15 | a11 * x2 + a12 * y2 - xt2, 16 | a21 * x2 + a22 * y2 - yt2 17 | ], (a11, a12, a21, a22)) 18 | 19 | print(solution) 20 | -------------------------------------------------------------------------------- /ch_04/matrix_composition.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | # translation to 0, 0 4 | a1 = Matrix([[1, 0, -1], [0, 1, -1], [0, 0, 1]]) 5 | 6 | # rotation 7 | a2 = Matrix([[0, 1, 0], [-1, 0, 0], [0, 0, 1]]) 8 | 9 | # translation back 10 | a3 = Matrix([[1, 0, 1], [0, 1, 1], [0, 0, 1]]) 11 | 12 | # composition 13 | c=a3*a2*a1 14 | print(" composition:") 15 | print(c) 16 | 17 | p1 = Matrix([0.75, 0.5, 1]) 18 | p2 = Matrix([1.25, 0.5, 1]) 19 | p3 = Matrix([1.25, 1.5, 1]) 20 | p4 = Matrix([0.75, 1.5, 1]) 21 | print("") 22 | print(" points:") 23 | print(p1) 24 | print(p2) 25 | print(p3) 26 | print(p4) 27 | 28 | pt1 = c * p1 29 | pt2 = c * p2 30 | pt3 = c * p3 31 | pt4 = c * p4 32 | 33 | print("") 34 | print(" transformed points:") 35 | print(pt1) 36 | print(pt2) 37 | print(pt3) 38 | print(pt4) 39 | 40 | -------------------------------------------------------------------------------- /ch_04/matrix_inversion.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | a1 = Matrix([[1, 0, -1], [0, 1, -1], [0, 0, 1]]) 4 | a2 = Matrix([[0, 1, 0], [-1, 0, 0], [0, 0, 1]]) 5 | a3 = Matrix([[1, 0, 1], [0, 1, 1], [0, 0, 1]]) 6 | 7 | # composition 8 | c=a3*a2*a1 9 | print(" composition:") 10 | print(c) 11 | 12 | p1 = Matrix([0.75, 0.5, 1]) 13 | p2 = Matrix([1.25, 0.5, 1]) 14 | p3 = Matrix([1.25, 1.5, 1]) 15 | p4 = Matrix([0.75, 1.5, 1]) 16 | print("") 17 | print(" points:") 18 | print(p1) 19 | print(p2) 20 | print(p3) 21 | print(p4) 22 | 23 | pt1 = c * p1 24 | pt2 = c * p2 25 | pt3 = c * p3 26 | pt4 = c * p4 27 | 28 | print("") 29 | print(" transformed points:") 30 | print(pt1) 31 | print(pt2) 32 | print(pt3) 33 | print(pt4) 34 | 35 | 36 | # inversion 37 | 38 | def minor(M, i, j): 39 | M_copy = M[:, :] 40 | M_copy.col_del(j) 41 | M_copy.row_del(i) 42 | return M_copy 43 | 44 | def cofactor(M): 45 | C = M[:, :] 46 | for i in range(M.shape[0]): 47 | for j in range(M.shape[1]): 48 | determinant = minor(M, i, j).det() # this is the determinant in SymPy 49 | sign = 1 if i+j%2 == 1 else -1 50 | C[i, j] = sign*determinant 51 | return C 52 | 53 | def not_a_real_inverse(M): 54 | # dividing by the determinant is optional 55 | return cofactor(M).T # / cofactor(M).det() 56 | 57 | print("") 58 | print(" real inverse:") 59 | print(c.inv() * pt1) 60 | print(c.inv() * pt2) 61 | print(c.inv() * pt3) 62 | print(c.inv() * pt4) 63 | 64 | print("") 65 | print(" inverse transformation, not a real inversion:") 66 | print(not_a_real_inverse(c) * pt1) 67 | print(not_a_real_inverse(c) * pt2) 68 | print(not_a_real_inverse(c) * pt3) 69 | print(not_a_real_inverse(c) * pt4) 70 | -------------------------------------------------------------------------------- /ch_04/projective_transformation_in_steps.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | # transformation matrix coefficients 4 | a, b, c, d, e, f, g, h, i = symbols('a b c d e f g h i') 5 | 6 | # points before the transformation 7 | x1, y1, x2, y2, x3, y3, x4, y4 = symbols('x1 y1 x2 y2 x3 y3 x4 y4') 8 | 9 | # points after the transformation 10 | xt1, yt1, xt2, yt2, xt3, yt3, xt4, yt4 = symbols('xt1 yt1 xt2 yt2 xt3 yt3 xt4 yt4') 11 | 12 | solution_1 = solve([ 13 | xt1 * i - c, 14 | yt1 * i - f, 15 | i - 1 16 | ], (c, f, i)) 17 | print(" c, f, i:") 18 | print(solution_1) 19 | 20 | c = xt1 21 | f = yt1 22 | i = 1 23 | solution_2 = solve([ 24 | xt2 * g + xt2 * i - a - c, 25 | yt2 * g + yt2 * i - d - f, 26 | xt4 * h + xt4 * i - b - c, 27 | yt4 * h + yt4 * i - e - f, 28 | ], (a, b, d, e)) 29 | print("") 30 | print(" a, b, d, e:") 31 | print(solution_2) 32 | 33 | a = g*xt2 - xt1 + xt2 34 | d = g*yt2 - yt1 + yt2 35 | b = h*xt4 - xt1 + xt4 36 | e = h*yt4 - yt1 + yt4 37 | 38 | solution_3 = solve([ 39 | xt3 * g + xt3 * h + xt3 * i - a - b - c, 40 | yt3 * g + yt3 * h + yt3 * i - d - e - f, 41 | ], (g, h)) 42 | print("") 43 | print(" g, h:") 44 | print(solution_3) 45 | 46 | -------------------------------------------------------------------------------- /ch_05/arc_and_segment.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | # the input is a pair of points (x1, y1), (x2, y2), 4 | # and a pair of corresponding tangent vectors (dx1, dy1), (dx2, dy2) 5 | x1, y1, dx1, dy1 = symbols('x1 y1 dx1 dy1') 6 | x2, y2, dx2, dy2 = symbols('x2 y2 dx2 dy2') 7 | 8 | # the circle's radius 9 | r1 = symbols('r1') 10 | 11 | # (ix, iy) is the point where the arc meets the line 12 | ix, iy = symbols('ix iy') 13 | 14 | # the output is now only one arc's center (ax1, ay1), 15 | # and we already know its radius from the input. 16 | # The other piece of output is the parameter t2 that says where the line ends 17 | ax1, ay1, t2 = symbols('ax1 ay1 t2') 18 | 19 | solutions = solve( 20 | [ 21 | # if we rotate a tangent vectors 90 degrees, multiply it by "r1" and put it from the first point, 22 | # it will end up in the arc's centert 23 | x1 - r1*dy1 - ax1, 24 | y1 + r1*dx1 - ay1, 25 | # the intersection point lies on the tangent line positioned by parameter t2: 26 | # (ix, iy) = (x2, y2) + t2*(dx2, dy2) 27 | x2 + dx2 * t2 - ix, 28 | y2 + dy2 * t2 - iy, 29 | # the intersection point also belongs to the arc, so the vector from it to the arc center is 30 | # orthogonal to the second tangent vector 31 | (ix-ax1)*dx2 + (iy-ay1)*dy2 32 | ], (ax1, ay1, t2, ix, iy)) 33 | 34 | print (pycode(solutions)) 35 | 36 | -------------------------------------------------------------------------------- /ch_05/better_arc_and_segment.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | # the input is a pair of points (x1, y1), (x2, y2), 4 | # and a pair of corresponding tangent vectors (dx1, dy1), (dx2, dy2) 5 | x1, y1, dx1, dy1 = symbols('x1 y1 dx1 dy1') 6 | x2, y2, dx2, dy2 = symbols('x2 y2 dx2 dy2') 7 | 8 | # the circle's radius 9 | r1 = symbols('r1') 10 | 11 | # (ix, iy) is the point where the arc meets the line 12 | ix, iy = symbols('ix iy') 13 | 14 | # the output is now only one arc's center (ax1, ay1), 15 | # and we already know its radius from the input. 16 | # The other piece of output is the parameter t2 that says where the line ends 17 | ax1, ay1, t2 = symbols('ax1 ay1 t2') 18 | 19 | ax1 = x1 - r1*dy1 20 | ay1 = y1 + r1*dx1 21 | 22 | solution = solve( 23 | [ 24 | # the intersection point lies on the tangent line positioned by parameter t2: 25 | # (ix, iy) = (x2, y2) + t2*(dx2, dy2) 26 | x2 + dx2 * t2 - ix, 27 | y2 + dy2 * t2 - iy, 28 | # the intersection point also belongs to the arc, so the vector from it to the arc center is 29 | # orthogonal to the second tangent vector 30 | (ix-ax1)*dx2 + (iy-ay1)*dy2 31 | ], (t2, ix, iy)) 32 | 33 | print ("ax1 =", ax1) 34 | print ("ay1 =", ay1) 35 | print ("t2 =", pycode(solution[t2])) 36 | print ("ix =", x2 + dx2 * t2) 37 | print ("iy =", y2 + dy2 * t2) 38 | -------------------------------------------------------------------------------- /ch_05/biarc.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | # the input is a pair of points (x1, y1), (x2, y2), 4 | # and a pair of corresponding tangent vectors (dx1, dy1), (dx2, dy2) 5 | x1, y1, dx1, dy1 = symbols('x1 y1 dx1 dy1') 6 | x2, y2, dx2, dy2 = symbols('x2 y2 dx2 dy2') 7 | 8 | # the output is the arcs’ center points (ax1, ay1) and (ax2, ay2), 9 | ax1, ay1, ax2, ay2 = symbols('ax1 ay1 ax2 ay2') 10 | 11 | # and corresponding radii r1 and r2 12 | r1, r2 = symbols('r1 r2') 13 | 14 | solutions = solve( 15 | [ 16 | r2 - r1, #radii are equal by design 17 | #both radius vectors are orthogonal to tangent vectors 18 | x1 + r1*dy1 - ax1, 19 | y1 - r1*dx1 - ay1, 20 | x2 + r2*dy2 - ax2, 21 | y2 - r2*dx2 - ay2, 22 | # circles touch at some point, this means that the distance 23 | # between their centers is the same as the sum of their radii 24 | (ax1-ax2)**2 + (ay1-ay2)**2 - (r1+r2)**2 25 | ], (ax1, ay1, ax2, ay2, r1, r2)) 26 | 27 | for s in solutions: 28 | print () 29 | print ('ax1 = ' + pycode(s[0])) 30 | print ('ay1 = ' + pycode(s[1])) 31 | print ('ax2 = ' + pycode(s[2])) 32 | print ('ay2 = ' + pycode(s[3])) 33 | print ('r1 = ' + pycode(s[4])) 34 | print ('r2 = ' + pycode(s[5])) 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ch_06/approximate.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | xs = [1, 2, 3, 4, 5] 4 | ys = [8 + 20/60., 8 + 4/60., 7 + 56/60., 7 + 52/60., 8 + 0/60.] 5 | N = len(xs) 6 | M = 1 7 | 8 | # the least squares matrix 9 | LS = Matrix([[sum([xs[i]**(row + collumn) for i in range(N)]) for collumn in range(M+1)] for row in range(M+1)]) 10 | 11 | # the least squares column-vector 12 | B = Matrix([sum([ys[i]*(xs[i]**row) for i in range(N)]) for row in range(M+1)]) 13 | 14 | # This tunrs approximation into interpolation 15 | #a0, a1, a2, a3, a4 = symbols('a0 a1 a2 a3 a4') 16 | #Pol = Matrix([a0, a1, a2, a3, a4]) 17 | #solution = solve(LS*Pol - B, (a0, a1, a2, a3, a4)) 18 | 19 | 20 | # polynomial in its symbolic form 21 | a0, a1 = symbols('a0 a1') 22 | Pol = Matrix([a0, a1]) 23 | 24 | # solving the system for the polynomial’s coefficients 25 | solution = solve(LS*Pol - B, (a0, a1)) 26 | 27 | # storing the coefficients in a list for convenient reuse 28 | a = [value for key, value in solution.items()] 29 | 30 | 31 | # the plotting code 32 | import numpy as np 33 | import matplotlib.pyplot as plt 34 | 35 | import matplotlib 36 | matplotlib.interactive(True) 37 | matplotlib.use('WebAgg') 38 | Pxs = np.arange(0.5, 5.5, 0.01) 39 | Pys = sum([a[i]*Pxs**i for i in range(M+1)]) 40 | 41 | fig, ax = plt.subplots() 42 | ax.plot(Pxs, Pys) 43 | ax.set(xlabel='x', ylabel='y', title='Approximating polynomial') 44 | ax.grid() 45 | plt.scatter(xs, ys) 46 | # save a picture if you want 47 | #fig.savefig("approximation_4.png") 48 | plt.show() 49 | 50 | 51 | -------------------------------------------------------------------------------- /ch_06/both_approximate_and_interpolate.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | xs = [10, 11, 12, 13, 14] 4 | ys = [-42, -41, -38, -37, -37] 5 | N = len(xs) 6 | M = 2 7 | 8 | # the least squares matrix 9 | LS = Matrix([[sum([xs[k]**(i+j) for k in range(N)]) for j in range(M+1)] for i in range(M+1)]) 10 | 11 | # the least squares column-vector 12 | B = Matrix([sum([ys[k]*(xs[k]**i) for k in range(N)]) for i in range(M+1)]) 13 | 14 | last_point_reentrance = 0 15 | a = [0, 0, 0] 16 | 17 | # this calculates the polynomial from a list of coefficients 18 | def P(x, a): 19 | return sum([a[i]*x**i for i in range(len(a))]) 20 | 21 | # the algorithm tries adding the last point again and again until the precision expectations are met 22 | precision = 0.01 23 | while abs(P(xs[N-1], a) - ys[N-1]) > precision: 24 | LS_more = Matrix([[last_point_reentrance*xs[N-1]**(i+j) for j in range(M+1)] for i in range(M+1)]) 25 | B_more = Matrix([last_point_reentrance*ys[N-1]*xs[N-1]**i for i in range(M+1)]) 26 | 27 | a0, a1, a2 = symbols('a0 a1 a2') 28 | Pol = Matrix([a0, a1, a2]) 29 | 30 | solution = solve((LS+LS_more)*Pol - (B+B_more), (a0, a1, a2)) 31 | a = [value for key, value in solution.items()] 32 | # on each iteration, exactly one new last point is added. 33 | # But you can add more if you want the algorithm to converge faster 34 | last_point_reentrance += 1 35 | 36 | 37 | # the plotting code 38 | import numpy as np 39 | import matplotlib.pyplot as plt 40 | 41 | import matplotlib 42 | matplotlib.interactive(True) 43 | matplotlib.use('WebAgg') 44 | Pxs = np.arange(9.5, 15.5, 0.01) 45 | Pys = sum([a[i]*Pxs**i for i in range(M+1)]) 46 | 47 | fig, ax = plt.subplots() 48 | ax.plot(Pxs, Pys) 49 | ax.set(xlabel='x', ylabel='y', title='Approximating polynomial') 50 | ax.grid() 51 | plt.scatter(xs, ys) 52 | fig.savefig("approximation_t3.png") 53 | plt.show() 54 | 55 | 56 | -------------------------------------------------------------------------------- /ch_06/interpolate.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | xs = [1, 2, 3, 4, 5] 4 | ys = [4, 2, 3, 2, 4] 5 | N = len(xs) 6 | 7 | # the Vandermonde matrix 8 | Vandermonde = Matrix([[xs[i]**j for j in range(N)] for i in range(N)]) 9 | 10 | # the target function’s values 11 | Ys = Matrix(ys) 12 | 13 | # polynomial’s coefficients in symbolic form 14 | a0, a1, a2, a3, a4 = symbols('a0 a1 a2 a3 a4') 15 | Pol = Matrix([a0, a1, a2, a3, a4]) 16 | 17 | # solving the system for the polynomial’s coefficients 18 | solution = solve(Vandermonde*Pol - Ys, (a0, a1, a2, a3, a4)) 19 | 20 | # putting the coefficients into a list for convenient storage and evaluation 21 | a = [value for key, value in solution.items()] 22 | 23 | 24 | # the plotting code 25 | import numpy as np 26 | import matplotlib.pyplot as plt 27 | 28 | import matplotlib 29 | matplotlib.interactive(True) 30 | matplotlib.use('WebAgg') 31 | Pxs = np.arange(0.5, 5.5, 0.01) 32 | Pys = sum([a[i]*Pxs**i for i in range(N)]) 33 | 34 | fig, ax = plt.subplots() 35 | ax.plot(Pxs, Pys) 36 | ax.set(xlabel='x', ylabel='y', title='Interpolating polynomial') 37 | ax.grid() 38 | plt.scatter(xs, ys) 39 | fig.savefig("interpolating.png") 40 | plt.show() 41 | 42 | 43 | -------------------------------------------------------------------------------- /ch_07/bezier_2.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | import matplotlib 5 | matplotlib.interactive(True) 6 | matplotlib.use('WebAgg') 7 | 8 | # points 9 | xs = [0, 0.3, 1] 10 | ys = [0.2, 0.8, 0.5] 11 | 12 | # Bezier function 13 | def b2x(t, xs): 14 | return (1-t)*(1-t)*xs[0] + 2*(1-t)*t*xs[1] + t*t*xs[2] 15 | 16 | # no point in copypasting, ys and xs share the same basis 17 | def b2y(t, ys): 18 | return b2x(t, ys) 19 | 20 | ts = np.arange(0.0, 1.01, 0.01) 21 | Bxs = np.vectorize(lambda t : b2x(t, xs))(ts) 22 | Bys = np.vectorize(lambda t : b2y(t, ys))(ts) 23 | 24 | 25 | fig, ax = plt.subplots() 26 | ax.plot(Bxs, Bys) 27 | ax.set(xlabel='x', ylabel='y', title='Quadratic Bezier') 28 | ax.grid() 29 | plt.scatter(xs, ys) 30 | fig.savefig("bezier_2.png") 31 | plt.show() 32 | -------------------------------------------------------------------------------- /ch_07/bezier_3.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | import matplotlib 5 | matplotlib.interactive(True) 6 | matplotlib.use('WebAgg') 7 | 8 | # points 9 | xs = [0, 0.3, 0.7, 1] 10 | ys = [0.2, 0.8, 1., 0.5] 11 | 12 | # cubic Bezier formula 13 | def b3x(t, xs): 14 | return (1-t)*(1-t)*(1-t)*xs[0] + 3*(1-t)*(1-t)*t*xs[1] + 3*(1-t)*t*t*xs[2] + t*t*t*xs[3] 15 | 16 | def b3y(t, ys): 17 | return b3x(t, ys) 18 | 19 | ts = np.arange(0.0, 1.01, 0.01) 20 | Bxs = np.vectorize(lambda t : b3x(t, xs))(ts) 21 | Bys = np.vectorize(lambda t : b3y(t, ys))(ts) 22 | 23 | 24 | fig, ax = plt.subplots() 25 | ax.plot(Bxs, Bys) 26 | ax.set(xlabel='x', ylabel='y', title='Cubic Bezier') 27 | ax.grid() 28 | plt.scatter(xs, ys) 29 | fig.savefig("bezier_3.png") 30 | plt.show() 31 | -------------------------------------------------------------------------------- /ch_07/bezier_rational.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | import matplotlib 5 | matplotlib.interactive(True) 6 | matplotlib.use('WebAgg') 7 | 8 | # points and weights 9 | xs = [0, 0.3, 0.7, 1] 10 | ys = [0.2, 0.8, 1., 0.5] 11 | ws = [1., 0.5, 0.5, 1.] 12 | 13 | # rational Bezier function 14 | def b3x(t, xs): 15 | n = ((1-t)*(1-t)*(1-t)*xs[0]*ws[0] 16 | + 3*(1-t)*(1-t)*t*xs[1]*ws[1] 17 | + 3*(1-t)*t*t*xs[2]*ws[2] 18 | + t*t*t*xs[3]*ws[3]) 19 | d = ((1-t)*(1-t)*(1-t)*ws[0] 20 | + 3*(1-t)*(1-t)*t*ws[1] 21 | + 3*(1-t)*t*t*ws[2] 22 | + t*t*t*ws[3]) 23 | return n / d 24 | 25 | def b3y(t, ys): 26 | return b3x(t, ys) 27 | 28 | ts = np.arange(0.0, 1.01, 0.01) 29 | Bxs = np.vectorize(lambda t : b3x(t, xs))(ts) 30 | Bys = np.vectorize(lambda t : b3y(t, ys))(ts) 31 | 32 | 33 | fig, ax = plt.subplots() 34 | ax.plot(Bxs, Bys) 35 | ax.set(xlabel='x', ylabel='y', title=str(ws)) 36 | ax.grid() 37 | plt.scatter(xs, ys) 38 | fig.savefig("bezier_rational_3.png") 39 | plt.show() 40 | -------------------------------------------------------------------------------- /ch_07/nurbs_circle.py: -------------------------------------------------------------------------------- 1 | # circle in NURBS 2 | sr2i = 1.0 / (2**0.5) 3 | xs = [1.0, sr2i, 0.0, -sr2i, -1.0, -sr2i, 0.0, sr2i, 1.0] 4 | ys = [0.0, sr2i, 1.0, sr2i, 0.0, -sr2i, -1.0, -sr2i, 0.0] 5 | ws = [1.0, sr2i, 1.0, sr2i, 1.0, sr2i, 1.0, sr2i, 1.0] 6 | n = len(xs) 7 | knot = [0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 4]; 8 | degree = 2; 9 | 10 | 11 | def index_in_knots(t, nurbs_knot): 12 | for i in range(len(nurbs_knot) - 1): 13 | if t >= nurbs_knot[i] and t < nurbs_knot[i+1]: 14 | return i 15 | return -1 16 | 17 | def nurbs_in_t(t, control_xs, control_ys, control_ws, knot, p): 18 | # de Boor 19 | k = index_in_knots(t, knot) 20 | 21 | 22 | d_x = [] 23 | d_y = [] 24 | d_w = [] 25 | for j in range(p + 1): 26 | d_x += [control_xs[j + k - p]] 27 | d_y += [control_ys[j + k - p]] 28 | d_w += [control_ws[j + k - p]] 29 | 30 | for r in range(1, p+1): 31 | for j in reversed(range(r, p+1)): 32 | a = (t - knot[j+k-p]) / (knot[j+1+k-r] - knot[j+k-p]) 33 | d_x[j] = (1.0 - a) * d_x[j-1] + a * d_x[j] 34 | d_y[j] = (1.0 - a) * d_y[j-1] + a * d_y[j] 35 | d_w[j] = (1.0 - a) * d_w[j-1] + a * d_w[j] 36 | 37 | return [d_x[p] / d_w[p], d_y[p] / d_w[p]] 38 | 39 | 40 | def circle(t): 41 | global xs, ys, ws, knot, degree 42 | return nurbs_in_t(t, xs, ys, ws, knot, degree) 43 | 44 | 45 | # make the plot 46 | import numpy as np 47 | import matplotlib.pyplot as plt 48 | 49 | import matplotlib 50 | matplotlib.interactive(True) 51 | matplotlib.use('WebAgg') 52 | 53 | # polynomial curve 54 | ts = np.arange(0.00, 4.00, 0.01) 55 | Pxs = [] 56 | Pys = [] 57 | for t in ts: 58 | xy = circle(t) 59 | Pxs += [xy[0]] 60 | Pys += [xy[1]] 61 | 62 | fig, ax = plt.subplots() 63 | ax.plot(Pxs, Pys) 64 | ax.set(xlabel='x', ylabel='y', title='de Boor\'s circle') 65 | ax.grid() 66 | ax.set_aspect('equal', adjustable='box') 67 | plt.scatter([x/w for x,w in zip(xs,ws)], [y/w for y,w in zip(ys,ws)]) 68 | fig.savefig("nurbs_circle.png") 69 | plt.show() 70 | -------------------------------------------------------------------------------- /ch_07/parametric_polynomial.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | ax, bx, cx, dx = symbols('ax bx cx dx') 4 | ay, by, cy, dy = symbols('ay by cy dy') 5 | x1, x2, x3, x4 = symbols('x1 x2 x3 x4') 6 | y1, y2, y3, y4 = symbols('y1 y2 y3 y4') 7 | 8 | # as you can see, the systems are identical. 9 | # It’s just “x” becomes “y” in the latter. In cases such as this, 10 | # you can save yourself some time on symbolic computation, 11 | # just solve one and use its solution for every coordinate. 12 | x_system = [ 13 | dx - x1, 14 | (1/27)*ax + (1/9)*bx + (1/3)*cx + dx - x2, 15 | (8/27)*ax + (4/9)*bx + (2/3)*cx + dx - x3, 16 | ax + bx + cx + dx - x4 17 | ] 18 | 19 | y_system = [ 20 | dy - y1, 21 | (1/27)*ay + (1/9)*by + (1/3)*cy + dy - y2, 22 | (8/27)*ay + (4/9)*by + (2/3)*cy + dy - y3, 23 | ay + by + cy + dy - y4 24 | ] 25 | 26 | axs = solve(x_system, (ax, bx, cx, dx)) 27 | ays = solve(y_system, (ay, by, cy, dy)) 28 | 29 | print(" ax and ay coefficients:") 30 | print(axs) 31 | print(ays) 32 | 33 | 34 | # the curve 35 | def Px(t, xs): 36 | ax = -4.5*xs[0] + 13.5*xs[1] - 13.5*xs[2] + 4.5*xs[3] 37 | bx = 9.0*xs[0] - 22.5*xs[1] + 18.0*xs[2] - 4.5*xs[3] 38 | cx = -5.5*xs[0] + 9.0*xs[1] - 4.5*xs[2] + xs[3] 39 | dx = xs[0] 40 | return ax*t**3 + bx*t**2 + cx*t + dx 41 | 42 | def Py(t, ys): 43 | return Px(t, ys) # they share the same formula 44 | 45 | # testing points 46 | xs = [0.25, 0.75, 0.75, 0.25] 47 | ys = [0.25, 0.25, 0.75, 0.75] 48 | 49 | # make the plot 50 | import numpy as np 51 | import matplotlib.pyplot as plt 52 | 53 | import matplotlib 54 | matplotlib.interactive(True) 55 | matplotlib.use('WebAgg') 56 | 57 | # polynomial curve 58 | t = np.arange(-0.05, 1.05, 0.01) 59 | Pxs = Px(t, xs) 60 | Pys = Py(t, ys) 61 | 62 | fig, ax = plt.subplots() 63 | ax.plot(Pxs, Pys) 64 | ax.set(xlabel='x', ylabel='y', title='Parametric polynomial curve') 65 | ax.grid() 66 | plt.scatter(xs, ys) 67 | fig.savefig("parametric_polynomial.png") 68 | plt.show() 69 | -------------------------------------------------------------------------------- /ch_07/parametric_polynomial_tangents.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | ax, bx, cx, dx = symbols('ax bx cx dx') 4 | ay, by, cy, dy = symbols('ay by cy dy') 5 | x1, x2, dx1, dx2 = symbols('x1 x2 dx1 dx2') 6 | y1, y2, dy1, dy2 = symbols('y1 y2 dy1 dy2') 7 | 8 | x_system = [ 9 | dx - x1, 10 | cx - dx1, 11 | ax + bx + cx + dx - x2, 12 | 3*ax + 2*bx + cx - dx2 13 | ] 14 | 15 | y_system = [ 16 | dy - y1, 17 | cy - dy1, 18 | ay + by + cy + dy - y2, 19 | 3*ay + 2*by + cy - dy2 20 | ] 21 | 22 | axs = solve(x_system, (ax, bx, cx, dx)) 23 | ays = solve(y_system, (ay, by, cy, dy)) 24 | 25 | print(" ax and ay coefficients:") 26 | print(axs) 27 | print(ays) 28 | 29 | # the curve 30 | def Px(t, xs, dxs): 31 | ax = dxs[0] + dxs[1] + 2*xs[0] - 2*xs[1] 32 | bx = -2*dxs[0] - dxs[1] - 3*xs[0] + 3*xs[1] 33 | cx = dxs[0] 34 | dx = xs[0] 35 | return ax*t**3 + bx*t**2 + cx*t + dx 36 | 37 | def Py(t, ys, dyx): 38 | return Px(t, ys, dys) # they share the same formula 39 | 40 | # testing points and tangents 41 | xs = [0.25, 0.75] 42 | ys = [0.25, 0.75] 43 | dxs = [0.025, -0.125] 44 | dys = [0.8, -0.7] 45 | 46 | # make the plot 47 | import numpy as np 48 | import matplotlib.pyplot as plt 49 | 50 | import matplotlib 51 | matplotlib.interactive(True) 52 | matplotlib.use('WebAgg') 53 | 54 | # polynomial curve 55 | t = np.arange(-0.1, 1.1, 0.01) 56 | Pxs = Px(t, xs, dxs) 57 | Pys = Py(t, ys, dys) 58 | 59 | fig, ax = plt.subplots() 60 | ax.plot(Pxs, Pys) 61 | ax.set(xlabel='x', ylabel='y', title='Parametric polynomial curve with tangents') 62 | ax.grid() 63 | plt.scatter(xs, ys) 64 | plt.arrow(xs[0], ys[0], dxs[0], dys[0]) 65 | plt.arrow(xs[1], ys[1], dxs[1], dys[1]) 66 | fig.savefig("parametric_polynomial_tangents.png") 67 | plt.show() 68 | -------------------------------------------------------------------------------- /ch_07/sine_integral_odd.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | from math import pi 3 | 4 | x, a, b, c, d = symbols('x a b c d') 5 | 6 | sine = a*x**7 + b*x**5 + c*x**3 + d*x 7 | sine_d = diff(sine, x) 8 | sine_i = integrate(sine, x) 9 | 10 | the_system = [ 11 | sine.subs(x, pi / 2) - 1, 12 | sine_d.subs(x, 0) - 1, 13 | sine_d.subs(x, pi / 2), 14 | sine_i.subs(x, pi / 2) - sine_i.subs(x, 0) - 1 15 | ] 16 | 17 | res = solve(the_system, (a, b, c, d)) 18 | 19 | print(" polynomial coefficients:") 20 | for var, value in res.items(): 21 | print(var, value) 22 | 23 | 24 | # store coefficients in a list 25 | ais = [] 26 | for var, value in res.items(): 27 | ais = [value] + ais 28 | 29 | # make the plot 30 | import numpy as np 31 | import matplotlib.pyplot as plt 32 | 33 | import matplotlib 34 | matplotlib.interactive(True) 35 | matplotlib.use('WebAgg') 36 | 37 | # polynomial ax 38 | Pxs = np.arange(-1.6, 1.6, 0.01) 39 | Pys = sum([ais[i]*Pxs**(2*i+1) for i in range(len(ais))]) 40 | 41 | # real sine 42 | sine_ys = np.sin(Pxs) 43 | 44 | # print mean error 45 | print("") 46 | print(" mean error of the approximation:", np.sum(np.abs(sine_ys - Pys)) / len(Pxs)) 47 | 48 | fig, ax = plt.subplots() 49 | ax.plot(Pxs, sine_ys, color='grey') 50 | ax.plot(Pxs, Pys) 51 | ax.set(xlabel='x', ylabel='y', title='Sine') 52 | ax.grid() 53 | fig.savefig("sine_integral_odd.png") 54 | plt.show() 55 | -------------------------------------------------------------------------------- /ch_07/sine_ipol.py: -------------------------------------------------------------------------------- 1 | from math import pi 2 | from math import sin 3 | 4 | # in interpolation, we use Vandermonde matrix to build the equation 5 | from sympy import * 6 | 7 | # Let’s spread “x”s evenly over the range from -pi/2 to +pi/2 8 | xs = [(i / 4.) * pi - pi / 2. for i in range(5)] 9 | ys = [sin(xi) for xi in xs] 10 | N = len(xs) 11 | 12 | # The coefficients of the interpolating polynomial will then come from solving the Vandermonde equation 13 | Vandermonde = Matrix([[xs[i]**j for j in range(N)] for i in range(N)]) 14 | Ys = Matrix(ys) 15 | a0, a1, a2, a3, a4 = symbols('a0 a1 a2 a3 a4') 16 | Pol = Matrix([a0, a1, a2, a3, a4]) 17 | 18 | solution = solve(Vandermonde*Pol - Ys, (a0, a1, a2, a3, a4)) 19 | ais = [value for key, value in solution.items()] 20 | 21 | 22 | # make the plot 23 | import numpy as np 24 | import matplotlib.pyplot as plt 25 | 26 | import matplotlib 27 | matplotlib.interactive(True) 28 | matplotlib.use('WebAgg') 29 | 30 | # polynomial ax 31 | Pxs = np.arange(-1.6, 1.6, 0.01) 32 | Pys = sum([ais[i]*Pxs**i for i in range(len(ais))]) 33 | 34 | # real sine 35 | sine_ys = np.sin(Pxs) 36 | 37 | # print mean error 38 | print(" mean error of the approximation:", np.sum(np.abs(sine_ys - Pys)) / len(Pxs)) 39 | 40 | fig, ax = plt.subplots() 41 | ax.plot(Pxs, sine_ys, color='grey') 42 | ax.plot(Pxs, Pys) 43 | ax.set(xlabel='x', ylabel='y', title='Sine') 44 | ax.grid() 45 | plt.scatter(xs, ys) 46 | fig.savefig("sine_ipol.png") 47 | plt.show() 48 | -------------------------------------------------------------------------------- /ch_07/sine_ipol_circle.py: -------------------------------------------------------------------------------- 1 | from math import factorial 2 | from math import pi 3 | from math import sin 4 | 5 | # coefficients as a list 6 | from sympy import * 7 | 8 | xs = [(i / 4.) * pi - pi / 2. for i in range(5)] 9 | ys = [sin(xi) for xi in xs] 10 | N = len(xs) 11 | 12 | Vandermonde = Matrix([[xs[i]**j for j in range(N)] for i in range(N)]) 13 | Ys = Matrix(ys) 14 | a0, a1, a2, a3, a4 = symbols('a0 a1 a2 a3 a4') 15 | Pol = Matrix([a0, a1, a2, a3, a4]) 16 | 17 | solution = solve(Vandermonde*Pol - Ys, (a0, a1, a2, a3, a4)) 18 | ais = [value for key, value in solution.items()] 19 | 20 | print(" polynomial coefficients:") 21 | print(ais) 22 | 23 | def sine(x): 24 | global ais 25 | def sine_quarter(x): 26 | return sum([ais[i]*x**i for i in range(len(ais))]) 27 | if x < 0: 28 | return -sine(-x) 29 | if x <= pi / 2: 30 | return sine_quarter(x) 31 | if x <= pi: 32 | return sine_quarter(pi - x) 33 | else: 34 | return sine(x - 2*pi) 35 | 36 | def cosine(x): 37 | return sine(x + pi/2.) 38 | 39 | # make the plot 40 | import numpy as np 41 | import matplotlib.pyplot as plt 42 | 43 | import matplotlib 44 | matplotlib.interactive(True) 45 | matplotlib.use('WebAgg') 46 | 47 | # polynomial model 48 | Pts = np.arange(-pi, pi, pi/1000) 49 | Pxs = np.vectorize(sine)(Pts) 50 | Pys = -np.vectorize(cosine)(Pts) 51 | 52 | fig, ax = plt.subplots() 53 | ax.plot(Pxs, Pys) 54 | ax.set(xlabel='x', ylabel='y', title='A circle made with the sine model') 55 | ax.set_aspect('equal', adjustable='box') 56 | ax.grid() 57 | fig.savefig("sine_ipol_circle.png") 58 | plt.show() 59 | -------------------------------------------------------------------------------- /ch_07/sine_naive.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | from math import pi 3 | 4 | x, a, b, c, d, e, f = symbols('x a b c d e f') 5 | 6 | sine = a*x**5 + b*x**4 + c*x**3 + d*x**2 + e*x + f 7 | sine_d = diff(sine, x) 8 | 9 | the_system = [ 10 | sine.subs(x, 0), 11 | sine.subs(x, pi / 2) - 1, 12 | sine.subs(x, -pi / 2) + 1, 13 | sine_d.subs(x, 0) - 1, 14 | sine_d.subs(x, pi / 2), 15 | sine_d.subs(x, -pi / 2), 16 | ] 17 | 18 | res = solve(the_system, (a, b, c, d, e, f)) 19 | 20 | print(" polynomial coefficients:") 21 | for var, value in res.items(): 22 | print(var, value) 23 | 24 | 25 | # store coefficients in a list 26 | ais = [] 27 | for var, value in res.items(): 28 | ais = [value] + ais 29 | 30 | # make the plot 31 | import numpy as np 32 | import matplotlib.pyplot as plt 33 | 34 | import matplotlib 35 | matplotlib.interactive(True) 36 | matplotlib.use('WebAgg') 37 | 38 | # polynomial ax 39 | Pxs = np.arange(-1.6, 1.6, 0.01) 40 | Pys = sum([ais[i]*Pxs**i for i in range(len(ais))]) 41 | 42 | # real sine 43 | sine_ys = np.sin(Pxs) 44 | 45 | # print mean error 46 | print("") 47 | print(" mean error of the approximation:", np.sum(np.abs(sine_ys - Pys)) / len(Pxs)) 48 | 49 | fig, ax = plt.subplots() 50 | ax.plot(Pxs, sine_ys, color='grey') 51 | ax.plot(Pxs, Pys) 52 | ax.set(xlabel='x', ylabel='y', title='Sine') 53 | ax.grid() 54 | fig.savefig("sine_naive.png") 55 | plt.show() 56 | -------------------------------------------------------------------------------- /ch_07/sine_odd.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | from math import pi 3 | 4 | x, a, b, c = symbols('x a b c') 5 | 6 | sine = a*x**5 + b*x**3 + c*x 7 | sine_d = diff(sine, x) 8 | # just like SymPy can differentiate your expression with “diff”, 9 | # it can integrate your expression with “integrate” 10 | sine_i = integrate(sine, x) 11 | 12 | the_system = [ 13 | sine.subs(x, pi / 2) - 1, 14 | sine_d.subs(x, 0) - 1, 15 | sine_d.subs(x, pi / 2), 16 | ] 17 | 18 | res = solve(the_system, (a, b, c)) 19 | 20 | print(" polynomial coefficients:") 21 | for var, value in res.items(): 22 | print(var, value) 23 | 24 | 25 | # store coefficients in a list 26 | ais = [] 27 | for var, value in res.items(): 28 | ais = [value] + ais 29 | 30 | # make the plot 31 | import numpy as np 32 | import matplotlib.pyplot as plt 33 | 34 | import matplotlib 35 | matplotlib.interactive(True) 36 | matplotlib.use('WebAgg') 37 | 38 | # polynomial model 39 | Pxs = np.arange(-1.6, 1.6, 0.01) 40 | Pys = sum([ais[i]*Pxs**(2*i+1) for i in range(len(ais))]) 41 | 42 | # real sine 43 | sine_ys = np.sin(Pxs) 44 | 45 | # print mean error 46 | print("") 47 | print(" mean error of the approximation:", np.sum(np.abs(sine_ys - Pys)) / len(Pxs)) 48 | 49 | fig, ax = plt.subplots() 50 | ax.plot(Pxs, sine_ys, color='grey') 51 | ax.plot(Pxs, Pys) 52 | ax.set(xlabel='x', ylabel='y', title='Sine') 53 | ax.grid() 54 | fig.savefig("sine_odd.png") 55 | plt.show() 56 | -------------------------------------------------------------------------------- /ch_07/sine_series.py: -------------------------------------------------------------------------------- 1 | from math import factorial 2 | 3 | # in series, coefficients come as a list 4 | ais = [] 5 | for i in range(4): 6 | n = i*2+1 7 | ais += [0, (1 if i % 2 == 0 else -1) / factorial(n)] 8 | 9 | print(" polynomial coefficients:") 10 | print(ais) 11 | 12 | # make the plot 13 | import numpy as np 14 | import matplotlib.pyplot as plt 15 | 16 | import matplotlib 17 | matplotlib.interactive(True) 18 | matplotlib.use('WebAgg') 19 | 20 | # polynomial ax 21 | Pxs = np.arange(-1.6, 1.6, 0.01) 22 | Pys = sum([ais[i]*Pxs**i for i in range(len(ais))]) 23 | 24 | # real sine 25 | sine_ys = np.sin(Pxs) 26 | 27 | # print mean error 28 | print("") 29 | print(" mean error of the approximation:", np.sum(np.abs(sine_ys - Pys)) / len(Pxs)) 30 | 31 | fig, ax = plt.subplots() 32 | ax.plot(Pxs, sine_ys, color='grey') 33 | ax.plot(Pxs, Pys) 34 | ax.set(xlabel='x', ylabel='y', title='Sine') 35 | ax.grid() 36 | fig.savefig("sine_series.png") 37 | plt.show() 38 | -------------------------------------------------------------------------------- /ch_07/sine_series_circle.py: -------------------------------------------------------------------------------- 1 | from math import factorial 2 | from math import pi 3 | 4 | # in series, coefficients come as a list 5 | ais = [] 6 | for i in range(4): 7 | n = i*2+1 8 | ais += [0, (1 if i % 2 == 0 else -1) / factorial(n)] 9 | 10 | print(" polynomial coefficients:") 11 | print(ais) 12 | 13 | def sine(x): 14 | global ais 15 | def sine_quarter(x): 16 | return sum([ais[i]*x**i for i in range(len(ais))]) 17 | if x < 0: 18 | return -sine(-x) 19 | if x <= pi / 2: 20 | return sine_quarter(x) 21 | if x <= pi: 22 | return sine_quarter(pi - x) 23 | else: 24 | return sine(x - 2*pi) 25 | 26 | def cosine(x): 27 | return sine(x + pi/2.) 28 | 29 | # make the plot 30 | import numpy as np 31 | import matplotlib.pyplot as plt 32 | 33 | import matplotlib 34 | matplotlib.interactive(True) 35 | matplotlib.use('WebAgg') 36 | 37 | # polynomial ax 38 | Pts = np.arange(-pi, pi, pi/1000) 39 | Pxs = np.vectorize(sine)(Pts) 40 | Pys = -np.vectorize(cosine)(Pts) 41 | 42 | fig, ax = plt.subplots() 43 | ax.plot(Pxs, Pys) 44 | ax.set(xlabel='x', ylabel='y', title='Circle made with a series ax of sine') 45 | ax.set_aspect('equal', adjustable='box') 46 | ax.grid() 47 | fig.savefig("sine_series_circle.png") 48 | plt.show() 49 | -------------------------------------------------------------------------------- /ch_08/bilinear_transformation.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | # the symbols are more or less like in the book, 4 | # only the initial positions of points now lose the “i” index for brevity 5 | a11, a12, a13, a14 = symbols('a11 a12 a13 a14') 6 | a21, a22, a23, a24 = symbols('a21 a22 a23 a24') 7 | x1, x2, x3, x4 = symbols('x1 x2 x3 x4') 8 | y1, y2, y3, y4 = symbols('y1 y2 y3 y4') 9 | xt1, xt2, xt3, xt4 = symbols('xt1 xt2 xt3 xt4') 10 | yt1, yt2, yt3, yt4 = symbols('yt1 yt2 yt3 yt4') 11 | 12 | # the system consists of 4 pairs of “x transforms to xt” 13 | # and “y transforms to yt” equations 14 | system = [ 15 | a11*x1*y1 + a12*x1 + a13*y1 + a14 - xt1, 16 | a21*x1*y1 + a22*x1 + a23*y1 + a24 - yt1, 17 | a11*x2*y2 + a12*x2 + a13*y2 + a14 - xt2, 18 | a21*x2*y2 + a22*x2 + a23*y2 + a24 - yt2, 19 | a11*x3*y3 + a12*x3 + a13*y3 + a14 - xt3, 20 | a21*x3*y3 + a22*x3 + a23*y3 + a24 - yt3, 21 | a11*x4*y4 + a12*x4 + a13*y4 + a14 - xt4, 22 | a21*x4*y4 + a22*x4 + a23*y4 + a24 - yt4 23 | ] 24 | 25 | # this runs for 0.26 seconds (on the reference machine AKA my PC) 26 | all_as = solve(system, (a11, a12, a13, a14, a21, a22, a23, a24)) 27 | 28 | if False: # alternative take 29 | x_system = [ 30 | a11*x1*y1 + a12*x1 + a13*y1 + a14 - xt1, 31 | a11*x2*y2 + a12*x2 + a13*y2 + a14 - xt2, 32 | a11*x3*y3 + a12*x3 + a13*y3 + a14 - xt3, 33 | a11*x4*y4 + a12*x4 + a13*y4 + a14 - xt4 34 | ] 35 | 36 | y_system = [ 37 | a21*x1*y1 + a22*x1 + a23*y1 + a24 - yt1, 38 | a21*x2*y2 + a22*x2 + a23*y2 + a24 - yt2, 39 | a21*x3*y3 + a22*x3 + a23*y3 + a24 - yt3, 40 | a21*x4*y4 + a22*x4 + a23*y4 + a24 - yt4 41 | ] 42 | 43 | # this runs in 0.18 seconds 44 | axs = solve(x_system, (a11, a12, a13, a14)) 45 | ays = solve(y_system, (a21, a22, a23, a24)) 46 | 47 | 48 | # testing points 49 | xis = [0.0, 1.0, 1.0, 0.0] 50 | yis = [0.0, 0.0, 1.0, 1.0] 51 | xts = [0.25, 0.75, 1.25, 0.25] 52 | yts = [0.25, 0.25, 1.25, 0.75] 53 | 54 | # substitute the solution's argument with numeric points 55 | a11_ = all_as[a11].subs([(x1, xis[0]), (x2, xis[1]), (x3, xis[2]), (x4, xis[3]), 56 | (y1, yis[0]), (y2, yis[1]), (y3, yis[2]), (y4, yis[3]), 57 | (xt1, xts[0]), (xt2, xts[1]), (xt3, xts[2]), (xt4, xts[3]), 58 | (yt1, yts[0]), (yt2, yts[1]), (yt3, yts[2]), (yt4, yts[3])]) 59 | a12_ = all_as[a12].subs([(x1, xis[0]), (x2, xis[1]), (x3, xis[2]), (x4, xis[3]), 60 | (y1, yis[0]), (y2, yis[1]), (y3, yis[2]), (y4, yis[3]), 61 | (xt1, xts[0]), (xt2, xts[1]), (xt3, xts[2]), (xt4, xts[3]), 62 | (yt1, yts[0]), (yt2, yts[1]), (yt3, yts[2]), (yt4, yts[3])]) 63 | a13_ = all_as[a13].subs([(x1, xis[0]), (x2, xis[1]), (x3, xis[2]), (x4, xis[3]), 64 | (y1, yis[0]), (y2, yis[1]), (y3, yis[2]), (y4, yis[3]), 65 | (xt1, xts[0]), (xt2, xts[1]), (xt3, xts[2]), (xt4, xts[3]), 66 | (yt1, yts[0]), (yt2, yts[1]), (yt3, yts[2]), (yt4, yts[3])]) 67 | a14_ = all_as[a14].subs([(x1, xis[0]), (x2, xis[1]), (x3, xis[2]), (x4, xis[3]), 68 | (y1, yis[0]), (y2, yis[1]), (y3, yis[2]), (y4, yis[3]), 69 | (xt1, xts[0]), (xt2, xts[1]), (xt3, xts[2]), (xt4, xts[3]), 70 | (yt1, yts[0]), (yt2, yts[1]), (yt3, yts[2]), (yt4, yts[3])]) 71 | a21_ = all_as[a21].subs([(x1, xis[0]), (x2, xis[1]), (x3, xis[2]), (x4, xis[3]), 72 | (y1, yis[0]), (y2, yis[1]), (y3, yis[2]), (y4, yis[3]), 73 | (xt1, xts[0]), (xt2, xts[1]), (xt3, xts[2]), (xt4, xts[3]), 74 | (yt1, yts[0]), (yt2, yts[1]), (yt3, yts[2]), (yt4, yts[3])]) 75 | a22_ = all_as[a22].subs([(x1, xis[0]), (x2, xis[1]), (x3, xis[2]), (x4, xis[3]), 76 | (y1, yis[0]), (y2, yis[1]), (y3, yis[2]), (y4, yis[3]), 77 | (xt1, xts[0]), (xt2, xts[1]), (xt3, xts[2]), (xt4, xts[3]), 78 | (yt1, yts[0]), (yt2, yts[1]), (yt3, yts[2]), (yt4, yts[3])]) 79 | a23_ = all_as[a23].subs([(x1, xis[0]), (x2, xis[1]), (x3, xis[2]), (x4, xis[3]), 80 | (y1, yis[0]), (y2, yis[1]), (y3, yis[2]), (y4, yis[3]), 81 | (xt1, xts[0]), (xt2, xts[1]), (xt3, xts[2]), (xt4, xts[3]), 82 | (yt1, yts[0]), (yt2, yts[1]), (yt3, yts[2]), (yt4, yts[3])]) 83 | a24_ = all_as[a24].subs([(x1, xis[0]), (x2, xis[1]), (x3, xis[2]), (x4, xis[3]), 84 | (y1, yis[0]), (y2, yis[1]), (y3, yis[2]), (y4, yis[3]), 85 | (xt1, xts[0]), (xt2, xts[1]), (xt3, xts[2]), (xt4, xts[3]), 86 | (yt1, yts[0]), (yt2, yts[1]), (yt3, yts[2]), (yt4, yts[3])]) 87 | 88 | 89 | # make the plot 90 | import numpy as np 91 | import matplotlib.pyplot as plt 92 | 93 | import matplotlib 94 | matplotlib.interactive(True) 95 | matplotlib.use('WebAgg') 96 | 97 | # points from a standard square 98 | xs_before = [] 99 | ys_before = [] 100 | xs_after = [] 101 | ys_after = [] 102 | for i in range(10 + 1): 103 | for j in range(10 + 1): 104 | x = j / 10. 105 | y = i / 10. 106 | xs_before += [x] 107 | ys_before += [y] 108 | xs_after += [a11_*x*y + a12_*x + a13_*y + a14_] 109 | ys_after += [a21_*x*y + a22_*x + a23_*y + a24_] 110 | 111 | fig, ax = plt.subplots() 112 | ax.set(xlabel='x', ylabel='y', title='Bilinear transforamtion') 113 | ax.grid() 114 | plt.scatter(xs_before, ys_before) 115 | plt.scatter(xs_after, ys_after) 116 | fig.savefig("bilinear_transformation.png") 117 | plt.show() 118 | -------------------------------------------------------------------------------- /ch_08/biquadratic_interpolation.py: -------------------------------------------------------------------------------- 1 | # the polynomial 2 | def P2(z1, z2, z3, x): 3 | a = 2.0*z1 - 4.0*z2 + 2.0*z3 4 | b = -3.0*z1 + 4.0*z2 - z3 5 | c = z1 6 | return a*x*x + b*x + c 7 | 8 | # Z-values 9 | z11 = 0.3; z12 = 0.5; z13 = 0.9 10 | z21 = 0.4; z22 = 0.7; z23 = 1.1 11 | z31 = 0.8; z32 = 1.4; z33 = 1.5 12 | 13 | # three y-interpolants 14 | def z1(y): return P2(z11, z12, z13, y) 15 | def z2(y): return P2(z21, z22, z23, y) 16 | def z3(y): return P2(z31, z32, z33, y) 17 | 18 | # and a single x-interpolant that brings them up together 19 | def z(x, y): return P2(z1(y), z2(y), z3(y), x) 20 | 21 | 22 | # plotting 23 | from mpl_toolkits import mplot3d 24 | import numpy as np 25 | import matplotlib.pyplot as plt 26 | import matplotlib 27 | 28 | matplotlib.interactive(True) 29 | matplotlib.use('WebAgg') 30 | 31 | x = np.linspace(0, 1, 10) 32 | y = np.linspace(0, 1, 10) 33 | 34 | X, Y = np.meshgrid(x, y) 35 | Z = z(X, Y) 36 | 37 | fig = plt.figure() 38 | ax = plt.axes(projection='3d') 39 | ax.set_xlabel('x') 40 | ax.set_ylabel('y') 41 | ax.set_zlabel('z'); 42 | ax.plot_wireframe(X, Y, Z, color='#457fd6') 43 | ax.set_title(''); 44 | plt.show() 45 | -------------------------------------------------------------------------------- /ch_08/deformed_mushrooms.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | import numpy as np 3 | import math 4 | import random 5 | 6 | # generation 7 | zs = [0.0, 0.25, 0.5, 0.75, 1.0] 8 | rs = [0.2, 0.4, 0.2, 0.8, 0.0] 9 | N = len(zs) 10 | 11 | radius_variation = 0.5 12 | height_variation = 0.6 13 | deformation_variation = 0.7 14 | 15 | rs = [r+random.random()*radius_variation for r in rs[:-1]]+[rs[-1]] 16 | dz = random.random()*height_variation 17 | 18 | Vandermonde = Matrix([[zs[i]**j for j in range(N)] for i in range(N)]) 19 | Rs = Matrix(rs) 20 | a0, a1, a2, a3, a4 = symbols('a0 a1 a2 a3 a4') 21 | Pol = Matrix([a0, a1, a2, a3, a4]) 22 | 23 | solution = solve(Vandermonde*Pol - Rs, (a0, a1, a2, a3, a4)) 24 | a = [value for key, value in solution.items()] 25 | 26 | def P(x, a): 27 | return sum([a[i]*x**i for i in range(len(a))]) 28 | 29 | u = np.linspace(0, 1, 59) 30 | v = np.linspace(0, 1, 59) 31 | 32 | theta = u * 2 * np.pi 33 | 34 | r, _ = np.meshgrid(P(v, a), u) 35 | v, theta = np.meshgrid(v, theta) 36 | 37 | x = r * np.cos(theta) 38 | y = r * np.sin(theta) 39 | z = v * (1 + dz) 40 | 41 | 42 | # deformation 43 | dxs = [-1, 1, 1, -1, -1, 1, 1, -1] 44 | dys = [-1, -1, 1, 1, -1, -1, 1, 1] 45 | dzs = [0, 0, 0, 0, 1+dz, 1+dz, 1+dz, 1+dz] 46 | ddxs = [random.random()*deformation_variation - deformation_variation/2 for i in range(len(dxs))] 47 | ddys = [random.random()*deformation_variation - deformation_variation/2 for i in range(len(dys))] 48 | ddzs = [random.random()*deformation_variation - deformation_variation/2 for i in range(len(dzs))] 49 | 50 | def F(x, y, z, dds): # inverse distance interpolation 51 | global dxs, dys, dzs, n 52 | N = 0. # numerator 53 | D = 0. # denominator 54 | for i in range(len(dds)): 55 | # Euclidean distance 56 | di = ((x-dxs[i])**2 + (y-dys[i])**2 + (z-dzs[i])**2)**0.5 57 | if di == 0.: 58 | return dds[i] 59 | ki = 1/di # weight function 60 | N += ki * dds[i] 61 | D += ki 62 | return N / D 63 | 64 | # applying the deformation to all the vertices 65 | for i in range(len(x)): 66 | for j in range(len(x[0])): 67 | xij = x[i][j] 68 | yij = y[i][j] 69 | zij = z[i][j] 70 | ddx = F(xij, yij, zij, ddxs) 71 | ddy = F(xij, yij, zij, ddys) 72 | ddz = F(xij, yij, zij, ddzs) 73 | x[i][j] += ddx 74 | y[i][j] += ddy 75 | z[i][j] += ddz 76 | 77 | 78 | # plotting 79 | from mpl_toolkits import mplot3d 80 | import matplotlib.pyplot as plt 81 | import matplotlib 82 | 83 | matplotlib.interactive(True) 84 | matplotlib.use('WebAgg') 85 | 86 | from matplotlib.tri import Triangulation 87 | tri = Triangulation(np.ravel(z), np.ravel(theta)) 88 | 89 | fig = plt.figure() 90 | ax = plt.axes(projection='3d') 91 | ax.set_xlabel('x') 92 | ax.set_ylabel('y') 93 | ax.set_zlabel('z'); 94 | ax.plot_trisurf(np.ravel(x), np.ravel(y), np.ravel(z), triangles=tri.triangles, 95 | cmap='viridis', linewidths=0.2); 96 | ax.set_xlim(-1.2, 1.2); ax.set_ylim(-1.2, 1.2); ax.set_zlim(0, 1.2 + dz); 97 | ax.set_title(''); 98 | plt.show() 99 | 100 | -------------------------------------------------------------------------------- /ch_08/getting_cubic_interpolation.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | z1, z2, z3, z4, a, b, c, d = symbols('z1 z2 z3 z4 a b c d') 4 | 5 | print(solve([ 6 | d - z1, 7 | a * 1./27. + b * 1./9. + c * 1./3. + d - z2, 8 | a * 8./27. + b * 4./9. + c * 1./3. + d - z3, 9 | a + b + c + d - z4, 10 | ], (a, b, c, d))) 11 | -------------------------------------------------------------------------------- /ch_08/getting_quadratic_interpolation.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | z1, z2, z3, a, b, c = symbols('z1 z2 z3 a b c') 4 | 5 | print(solve([ 6 | c - z1, 7 | a * 0.25 + b * 0.5 + c - z2, 8 | a + b + c - z3, 9 | ], (a, b, c))) 10 | -------------------------------------------------------------------------------- /ch_08/linear_cubic_interpolation.py: -------------------------------------------------------------------------------- 1 | # see getting_cubic_interpolation.py 2 | def P3(z1, z2, z3, z4, x): 3 | a = 5.4*z1 - 2.7*z2 - 5.4*z3 + 2.7*z4 4 | b = -4.2*z1 - 0.9*z2 + 7.2*z3 - 2.1*z4 5 | c = -2.2*z1 + 3.6*z2 - 1.8*z3 + 0.4*z4 6 | d = z1 7 | return a*x*x*x + b*x*x + c*x + d 8 | 9 | # that's just linear interpolation 10 | def P1(z1, z2, y): 11 | return z1*(1-y) + z2*y 12 | 13 | z11 = 0.5; z12 = 0.6; 14 | z21 = 0.4; z22 = 0.5; 15 | z31 = 0.7; z32 = 0.9; 16 | z41 = 0.6; z42 = 0.7; 17 | 18 | # four linear y-interpolants 19 | def z1(y): return P1(z11, z12, y) 20 | def z2(y): return P1(z21, z22, y) 21 | def z3(y): return P1(z31, z32, y) 22 | def z4(y): return P1(z41, z42, y) 23 | 24 | # and a single cubic x-interpolant 25 | # that makes them into a two-variable function 26 | def z(x, y): return P3(z1(y), z2(y), z3(y), z4(y), x) 27 | 28 | 29 | # plotting 30 | from mpl_toolkits import mplot3d 31 | import numpy as np 32 | import matplotlib.pyplot as plt 33 | import matplotlib 34 | 35 | matplotlib.interactive(True) 36 | matplotlib.use('WebAgg') 37 | 38 | x = np.linspace(0, 1, 10) 39 | y = np.linspace(0, 1, 10) 40 | 41 | X, Y = np.meshgrid(x, y) 42 | Z = z(X, Y) 43 | 44 | fig = plt.figure() 45 | ax = plt.axes(projection='3d') 46 | ax.set_xlabel('x') 47 | ax.set_ylabel('y') 48 | ax.set_zlabel('z'); 49 | ax.plot_wireframe(X, Y, Z, color='#339428') 50 | ax.set_title(''); 51 | plt.show() 52 | -------------------------------------------------------------------------------- /ch_08/mushrooms.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | import numpy as np 3 | import math 4 | import random 5 | 6 | # initial shape in heights and radii 7 | zs = [0.0, 0.25, 0.5, 0.75, 1.0] 8 | rs = [0.2, 0.4, 0.2, 0.8, 0.0] 9 | N = len(zs) 10 | 11 | # variativity. rs is altered right here, dz is then added to z 12 | rs = [r+random.random()*0.5 for r in rs[:-1]]+[rs[-1]] 13 | dz = random.random()*0.6 14 | 15 | # interpolate the r(z) function to get a profile line 16 | Vandermonde = Matrix([[zs[i]**j for j in range(N)] for i in range(N)]) 17 | Rs = Matrix(rs) 18 | a0, a1, a2, a3, a4 = symbols('a0 a1 a2 a3 a4') 19 | Pol = Matrix([a0, a1, a2, a3, a4]) 20 | 21 | solution = solve(Vandermonde*Pol - Rs, (a0, a1, a2, a3, a4)) 22 | a = [value for key, value in solution.items()] 23 | 24 | def P(x, a): 25 | return sum([a[i]*x**i for i in range(len(a))]) 26 | 27 | # make a surface 28 | u = np.linspace(0, 1, 59) 29 | v = np.linspace(0, 1, 59) 30 | 31 | theta = u * 2 * np.pi 32 | 33 | r, _ = np.meshgrid(P(v, a), u) 34 | v, theta = np.meshgrid(v, theta) 35 | 36 | # the surface of revolution 37 | x = r * np.cos(theta) 38 | y = r * np.sin(theta) 39 | z = v * (1 + dz) 40 | 41 | 42 | # plotting 43 | from mpl_toolkits import mplot3d 44 | import matplotlib.pyplot as plt 45 | import matplotlib 46 | 47 | matplotlib.interactive(True) 48 | matplotlib.use('WebAgg') 49 | 50 | from matplotlib.tri import Triangulation 51 | tri = Triangulation(np.ravel(z), np.ravel(theta)) 52 | 53 | fig = plt.figure() 54 | ax = plt.axes(projection='3d') 55 | ax.set_xlabel('x') 56 | ax.set_ylabel('y') 57 | ax.set_zlabel('z'); 58 | ax.plot_trisurf(np.ravel(x), np.ravel(y), np.ravel(z), triangles=tri.triangles, 59 | cmap='viridis', linewidths=0.2); 60 | ax.set_xlim(-0.8, 0.8); ax.set_ylim(-0.8, 0.8); ax.set_zlim(0, 1 + dz); 61 | ax.set_title(''); 62 | plt.show() 63 | 64 | -------------------------------------------------------------------------------- /ch_08/ugly_mushroom.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | import numpy as np 3 | import math 4 | 5 | # initial shape in heights and radii 6 | zs = [0.0, 0.25, 0.5, 0.75, 1.0] 7 | rs = [0.2, 0.4, 0.2, 0.8, 0.0] 8 | N = len(zs) 9 | 10 | # interpolate the r(z) function to get a profile line 11 | Vandermonde = Matrix([[zs[i]**j for j in range(N)] for i in range(N)]) 12 | Rs = Matrix(rs) 13 | a0, a1, a2, a3, a4 = symbols('a0 a1 a2 a3 a4') 14 | Pol = Matrix([a0, a1, a2, a3, a4]) 15 | 16 | solution = solve(Vandermonde*Pol - Rs, (a0, a1, a2, a3, a4)) 17 | a = [value for key, value in solution.items()] 18 | 19 | def P(x, a): 20 | return sum([a[i]*x**i for i in range(len(a))]) 21 | 22 | # make a surface 23 | u = np.linspace(0, 1, 9) 24 | v = np.linspace(0, 1, 9) 25 | 26 | theta = u * 2 * np.pi 27 | 28 | r, _ = np.meshgrid(P(v, a), u) 29 | v, theta = np.meshgrid(v, theta) 30 | 31 | # the surface of revolution 32 | x = r * np.cos(theta) 33 | y = r * np.sin(theta) 34 | z = v 35 | 36 | 37 | # plotting 38 | from mpl_toolkits import mplot3d 39 | import matplotlib.pyplot as plt 40 | import matplotlib 41 | 42 | matplotlib.interactive(True) 43 | matplotlib.use('WebAgg') 44 | 45 | from matplotlib.tri import Triangulation 46 | tri = Triangulation(np.ravel(z), np.ravel(theta)) 47 | 48 | fig = plt.figure() 49 | ax = plt.axes(projection='3d') 50 | ax.set_xlabel('x') 51 | ax.set_ylabel('y') 52 | ax.set_zlabel('z'); 53 | ax.plot_trisurf(np.ravel(x), np.ravel(y), np.ravel(z), triangles=tri.triangles, 54 | cmap='viridis', linewidths=0.2); 55 | ax.set_xlim(-0.8, 0.8); ax.set_ylim(-0.8, 0.8); ax.set_zlim(0, 1); 56 | ax.set_title(''); 57 | plt.show() 58 | 59 | -------------------------------------------------------------------------------- /ch_09/nd_box.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | a1, a2, a3, a4 = symbols('a1 a2 a3 a4') 4 | b1, b2, b3, b4 = symbols('b1 b2 b3 b4') 5 | c1, c2, c3, c4 = symbols('c1 c2 c3 c4') 6 | d1, d2, d3, d4 = symbols('d1 d2 d3 d4') 7 | 8 | # 2D box product 9 | A = Matrix([[a1, a2], [b1, b2]]) 10 | print(A.det()) 11 | 12 | # 3D box AKA triple probuct (also see SymPy dot and cross methods, and a & (b ^ c) construction) 13 | A = Matrix([[a1, a2, a3], [b1, b2, b3], [c1, c2, c3]]) 14 | print(A.det()) 15 | 16 | #4D box product 17 | A = Matrix([[a1, a2, a3, a4], [b1, b2, b3, b4], [c1, c2, c3, c4], [d1, d2, d3, d4]]) 18 | print(A.det()) 19 | -------------------------------------------------------------------------------- /ch_09/nd_cross.py: -------------------------------------------------------------------------------- 1 | from sympy import * 2 | 3 | e1, e2, e3, e4 = symbols('e1 e2 e3 e4') 4 | a1, a2, a3, a4 = symbols('a1 a2 a3 a4') 5 | b1, b2, b3, b4 = symbols('b1 b2 b3 b4') 6 | c1, c2, c3, c4 = symbols('c1 c2 c3 c4') 7 | 8 | # 2D cross product 9 | A = Matrix([[e1, e2], [a1, a2]]) 10 | print(A.det()) 11 | 12 | # 3D cross product (also see SymPy cross method and ^ operator) 13 | A = Matrix([[e1, e2, e3], [a1, a2, a3], [b1, b2, b3]]) 14 | print(A.det()) 15 | 16 | # 4D cross product 17 | A = Matrix([[e1, e2, e3, e4], [a1, a2, a3, a4], [b1, b2, b3, b4], [c1, c2, c3, c4]]) 18 | print(A.det()) 19 | print(collect(A.det(), [e1, e2, e3, e4])) 20 | -------------------------------------------------------------------------------- /ch_10/sdf_circle.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib 3 | import numpy as np 4 | from matplotlib import ticker, cm 5 | from numpy import ma 6 | 7 | matplotlib.interactive(True) 8 | matplotlib.use('WebAgg') 9 | 10 | 11 | # linspace is just a fancy word for an array with evenly spaced values in it 12 | N = 100 13 | x = np.linspace(-1.4, 1.4, N) 14 | y = np.linspace(-1.4, 1.4, N) 15 | 16 | # and a mesh grid is a 2D array. We have already seen both in chapter 8 17 | X, Y = np.meshgrid(x, y) 18 | 19 | # the signed distance function that shapes a circle 20 | SDF = (X*X + Y*Y)**0.5 - 1. 21 | 22 | 23 | # plotting 24 | fig, ax = plt.subplots() 25 | ax.set_aspect('equal', adjustable='box') 26 | bg = ax.contourf(X, Y, SDF, cmap=cm.coolwarm, levels=[-1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1]) 27 | fg = ax.contour(X, Y, SDF, 28 | levels = [-1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1], 29 | colors = ['0.1', '0.1', '0.1', '0.1', '0.', '0.1', '0.1', '0.1', '0.1'], 30 | linewidths = [0.25, 0.25, 0.25, 0.25, 2, 0.25, 0.25, 0.25, 0.25]) 31 | cbar = fig.colorbar(bg) 32 | 33 | plt.show() 34 | -------------------------------------------------------------------------------- /ch_10/sdf_disks_intersection.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib 3 | import numpy as np 4 | from matplotlib import ticker, cm 5 | from numpy import ma 6 | 7 | matplotlib.interactive(True) 8 | matplotlib.use('WebAgg') 9 | 10 | N = 100 11 | x = np.linspace(-10, 10, N) 12 | y = np.linspace(-10, 10, N) 13 | 14 | X, Y = np.meshgrid(x, y) 15 | 16 | def circle(X, Y, r): 17 | return np.sqrt(X*X + Y*Y) - r 18 | 19 | # left circle 20 | SDF1 = circle(X+2, Y, 6) 21 | 22 | # right circle 23 | SDF2 = circle(X-2, Y, 6) 24 | 25 | # intersection 26 | SDF = np.maximum(SDF1, SDF2) 27 | 28 | 29 | # plotting 30 | fig, ax = plt.subplots() 31 | ax.set_aspect('equal', adjustable='box') 32 | bg = ax.contourf(X, Y, SDF, cmap=cm.coolwarm, levels = [-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10]) 33 | fg = ax.contour(X, Y, SDF, 34 | levels = [-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10], 35 | colors=['0.1', '0.1', '0.1', '0.1', '0.1', '0.2', '0.1', '0.1', '0.1', '0.1', '0.1'], 36 | linewidths=[0.25, 0.25, 0.25, 0.25, 0.25, 1.0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25]) 37 | fg1 = ax.contour(X, Y, SDF1, levels=[0], colors=['0'], linewidths=[0.5], linestyles=['dashed']) 38 | fg2 = ax.contour(X, Y, SDF2, levels=[0], colors=['0'], linewidths=[0.5], linestyles=['dashed']) 39 | cbar = fig.colorbar(bg) 40 | 41 | plt.show() 42 | -------------------------------------------------------------------------------- /ch_10/sdf_disks_offset.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib 3 | import numpy as np 4 | from matplotlib import ticker, cm 5 | from numpy import ma 6 | 7 | matplotlib.interactive(True) 8 | matplotlib.use('WebAgg') 9 | 10 | N = 100 11 | x = np.linspace(-10, 10, N) 12 | y = np.linspace(-10, 10, N) 13 | 14 | X, Y = np.meshgrid(x, y) 15 | 16 | def circle(X, Y, r): 17 | return np.sqrt(X*X + Y*Y) - r 18 | 19 | # left circle 20 | SDF1 = circle(X+2, Y, 6) 21 | 22 | # right circle 23 | SDF2 = circle(X-2, Y, 6) 24 | 25 | # offset. Please change the values from above to experiment 26 | dilation = 1 # make the shape larger 27 | erosion = 0 # make the shape smaller 28 | SDF = np.maximum(SDF1, -SDF2) - dilation + erosion 29 | 30 | 31 | # plotting 32 | fig, ax = plt.subplots() 33 | ax.set_aspect('equal', adjustable='box') 34 | bg = ax.contourf(X, Y, SDF, cmap=cm.coolwarm, levels = [-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10]) 35 | fg = ax.contour(X, Y, SDF, 36 | levels = [-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10], 37 | colors=['0.1', '0.1', '0.1', '0.1', '0.1', '0.2', '0.1', '0.1', '0.1', '0.1', '0.1'], 38 | linewidths=[0.25, 0.25, 0.25, 0.25, 0.25, 1.0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25]) 39 | fg1 = ax.contour(X, Y, SDF1, levels=[0], colors=['0'], linewidths=[0.5], linestyles=['dashed']) 40 | fg2 = ax.contour(X, Y, SDF2, levels=[0], colors=['0'], linewidths=[0.5], linestyles=['dashed']) 41 | cbar = fig.colorbar(bg) 42 | 43 | plt.show() 44 | -------------------------------------------------------------------------------- /ch_10/sdf_disks_subtraction.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib 3 | import numpy as np 4 | from matplotlib import ticker, cm 5 | from numpy import ma 6 | 7 | matplotlib.interactive(True) 8 | matplotlib.use('WebAgg') 9 | 10 | N = 1000 11 | x = np.linspace(-10, 10, N) 12 | y = np.linspace(-10, 10, N) 13 | 14 | X, Y = np.meshgrid(x, y) 15 | 16 | def circle(X, Y, r): 17 | return np.sqrt(X*X + Y*Y) - r 18 | 19 | # left circle 20 | SDF1 = circle(X+2, Y, 6) 21 | 22 | # right circle 23 | SDF2 = circle(X-1.99, Y, 6) 24 | 25 | # subtraction 26 | SDF = np.maximum(SDF1, -SDF2) 27 | 28 | 29 | # plotting 30 | fig, ax = plt.subplots() 31 | ax.set_aspect('equal', adjustable='box') 32 | bg = ax.contourf(X, Y, SDF, cmap=cm.coolwarm, levels = [-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10]) 33 | fg = ax.contour(X, Y, SDF, 34 | levels = [-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10], 35 | colors=['0.1', '0.1', '0.1', '0.1', '0.1', '0.2', '0.1', '0.1', '0.1', '0.1', '0.1'], 36 | linewidths=[0.25, 0.25, 0.25, 0.25, 0.25, 1.0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25]) 37 | fg1 = ax.contour(X, Y, SDF1, levels=[0], colors=['0'], linewidths=[0.5], linestyles=['dashed']) 38 | fg2 = ax.contour(X, Y, SDF2, levels=[0], colors=['0'], linewidths=[0.5], linestyles=['dashed']) 39 | cbar = fig.colorbar(bg) 40 | 41 | plt.show() 42 | -------------------------------------------------------------------------------- /ch_10/sdf_disks_union.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib 3 | import numpy as np 4 | from matplotlib import ticker, cm 5 | from numpy import ma 6 | 7 | matplotlib.interactive(True) 8 | matplotlib.use('WebAgg') 9 | 10 | N = 100 11 | x = np.linspace(-10, 10, N) 12 | y = np.linspace(-10, 10, N) 13 | 14 | X, Y = np.meshgrid(x, y) 15 | 16 | def circle(X, Y, r): 17 | return np.sqrt(X*X + Y*Y) - r 18 | 19 | # left circle 20 | SDF1 = circle(X+2, Y, 6) 21 | 22 | # right circle 23 | SDF2 = circle(X-2, Y, 6) 24 | 25 | # union 26 | SDF = np.minimum(SDF1, SDF2) 27 | 28 | 29 | # plotting 30 | fig, ax = plt.subplots() 31 | ax.set_aspect('equal', adjustable='box') 32 | bg = ax.contourf(X, Y, SDF, cmap=cm.coolwarm, levels = [-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10]) 33 | fg = ax.contour(X, Y, SDF, 34 | levels = [-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10], 35 | colors=['0.1', '0.1', '0.1', '0.1', '0.1', '0.2', '0.1', '0.1', '0.1', '0.1', '0.1'], 36 | linewidths=[0.25, 0.25, 0.25, 0.25, 0.25, 1.0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25]) 37 | fg1 = ax.contour(X, Y, SDF1, levels=[0], colors=['0'], linewidths=[0.5], linestyles=['dashed']) 38 | fg2 = ax.contour(X, Y, SDF2, levels=[0], colors=['0'], linewidths=[0.5], linestyles=['dashed']) 39 | cbar = fig.colorbar(bg) 40 | 41 | plt.show() 42 | -------------------------------------------------------------------------------- /ch_10/sdf_gyroid_parametric.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib 3 | import numpy as np 4 | from matplotlib import ticker, cm 5 | from numpy import ma 6 | 7 | matplotlib.interactive(True) 8 | matplotlib.use('WebAgg') 9 | 10 | N = 100 11 | x = np.linspace(-16, 16, N) 12 | y = np.linspace(-16, 16, N) 13 | 14 | # these are the meshgrids that cover 15 | # the part of the infinite function we want to display 16 | X, Y = np.meshgrid(x, y) 17 | 18 | # this is the z-coordinate of the x-y plane section 19 | z = 0.5 20 | 21 | # this is the quasi-offset term 22 | SDF = (np.sin(X)*np.sin(Y) + np.sin(Y)*np.cos(z) + np.sin(z)*np.cos(X) - Y / 32) 23 | 24 | 25 | # plotting 26 | fig, ax = plt.subplots() 27 | ax.set_aspect('equal', adjustable='box') 28 | bg = ax.contourf(X, Y, SDF, cmap=cm.coolwarm, levels = [-2, -1.5, -1.0, -0.5, 0, 0.5, 1.0, 1.5, 2]) 29 | fg = ax.contour(X, Y, SDF, 30 | levels = [-2, -1.5, -1.0, -0.5, 0, 0.5, 1.0, 1.5, 2], 31 | colors = ['0.1', '0.1', '0.1', '0.1', '0.', '0.1', '0.1', '0.1', '0.1'], 32 | linewidths = [0.25, 0.25, 0.25, 0.25, 1, 0.25, 0.25, 0.25, 0.25]) 33 | cbar = fig.colorbar(bg) 34 | 35 | plt.show() 36 | -------------------------------------------------------------------------------- /ch_10/sdf_hollow.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib 3 | import numpy as np 4 | from matplotlib import ticker, cm 5 | from numpy import ma 6 | 7 | matplotlib.interactive(True) 8 | matplotlib.use('WebAgg') 9 | 10 | N = 100 11 | x = np.linspace(-1.4, 1.4, N) 12 | y = np.linspace(-1.4, 1.4, N) 13 | 14 | X, Y = np.meshgrid(x, y) 15 | 16 | SDF = (X*X + Y*Y)**0.5 - 1. 17 | 18 | # hollow shell is the result of the erode-unsign-dilate sequence 19 | shell_width = 0.2 20 | SDF_eroded = SDF + shell_width / 2 21 | unsigned_DF = np.abs(SDF_eroded) 22 | SDF_hollowed = unsigned_DF - shell_width / 2 23 | 24 | 25 | # plotting 26 | fig, ax = plt.subplots() 27 | ax.set_aspect('equal', adjustable='box') 28 | bg = ax.contourf(X, Y, SDF_hollowed, cmap=cm.coolwarm, levels=[-1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1]) 29 | fg = ax.contour(X, Y, SDF_hollowed, 30 | levels = [-1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1], 31 | colors = ['0.1', '0.1', '0.1', '0.1', '0.', '0.1', '0.1', '0.1', '0.1'], 32 | linewidths = [0.25, 0.25, 0.25, 0.25, 1, 0.25, 0.25, 0.25, 0.25]) 33 | fg2 = ax.contour(X, Y, SDF, levels=[0], colors=['0'], linewidths=[0.5], linestyles=['dashed']) 34 | cbar = fig.colorbar(bg) 35 | 36 | plt.show() 37 | -------------------------------------------------------------------------------- /ch_10/sdf_metaballs_quasi_inverse.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib 3 | import numpy as np 4 | from matplotlib import ticker, cm 5 | from numpy import ma 6 | 7 | matplotlib.interactive(True) 8 | matplotlib.use('WebAgg') 9 | 10 | def distance_p2p(x1, y1, x2, y2): 11 | return ((x2-x1)**2 + (y2-y1)**2)**0.5 12 | 13 | # quasi-inverse function local to (0, 1] 14 | @np.vectorize 15 | def quasi_inverse(x): 16 | if x < 1: 17 | return (1 - 2*x + x*x)/x 18 | return 1e-15 # not to cause div by zero 19 | 20 | 21 | # quasi-inverse function with variative locality 22 | def quasi_inverse_with_locality(x, locality): 23 | return quasi_inverse(x / locality) 24 | 25 | 26 | N = 1000 27 | x = np.linspace(-16, 16, N) 28 | y = np.linspace(-16, 16, N) 29 | 30 | X, Y = np.meshgrid(x, y) 31 | 32 | balls = [(-6, -5), (9, -8), (0, 9)] 33 | offset = -2.5 34 | locality = 25 35 | 36 | # in this SDF, real inverse is replaced with quasi-inverse 37 | SDF = (quasi_inverse_with_locality( 38 | sum([quasi_inverse_with_locality(distance_p2p(X, Y, ball[0], ball[1]), locality) for ball in balls]), locality) 39 | / len(balls) 40 | + offset) 41 | 42 | 43 | # plotting 44 | fig, ax = plt.subplots() 45 | ax.set_aspect('equal', adjustable='box') 46 | bg = ax.contourf(X, Y, SDF, cmap=cm.coolwarm, levels = [-50, -40, -30, -20, -10, 0, 10, 20, 30, 40, 50]) 47 | fg = ax.contour(X, Y, SDF, 48 | levels = [-50, -40, -30, -20, -10, 0, 10, 20, 30, 40, 50], 49 | colors = ['0.1', '0.1', '0.1', '0.1', '0.1', '0.', '0.1', '0.1', '0.1', '0.1', '0.1'], 50 | linewidths = [0.25, 0.25, 0.25, 0.25, 0.25, 1, 0.25, 0.25, 0.25, 0.25, 0.25]) 51 | cbar = fig.colorbar(bg) 52 | 53 | plt.show() 54 | -------------------------------------------------------------------------------- /ch_10/sdf_multifocal_lemniscate.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib 3 | import numpy as np 4 | from matplotlib import ticker, cm 5 | from numpy import ma 6 | 7 | matplotlib.interactive(True) 8 | matplotlib.use('WebAgg') 9 | 10 | N = 1000 11 | x = np.linspace(-3, 3, N) 12 | y = np.linspace(-3, 3, N) 13 | 14 | X, Y = np.meshgrid(x, y) 15 | 16 | # a point-to-point distance function 17 | def distance_p2p(x1, y1, x2, y2): 18 | return ((x2-x1)**2 + (y2-y1)**2)**0.5 19 | 20 | # a product function. Like sum() but prod() 21 | def prod(list): 22 | if len(list) == 2: 23 | return list[0]*list[1] 24 | else: 25 | return prod([list[0]*list[1]] + list[2:]) 26 | 27 | # input data for the lemniscate 28 | foci = [(-0.6, -1.12), (-0.6, 1.12), (-1.12, -2.06), (-1.12, 2.06), (0.4, 0), (1.2, 0)] 29 | r = 2 30 | 31 | # it’s easier to govern the function with a quasi-offset 32 | # by exponentiating the offset into the power 33 | # proportional to the number of points 34 | c = r**len(foci) 35 | 36 | # a lemniscate is the isoline of the product of all distances to focii 37 | SDF = prod([distance_p2p(X, Y, focus[0], focus[1]) for focus in foci]) - c 38 | 39 | 40 | # plotting 41 | fig, ax = plt.subplots() 42 | ax.set_aspect('equal', adjustable='box') 43 | bg = ax.contourf(X, Y, SDF, cmap=cm.coolwarm, levels = [-8000, -6000, -4000, -2000, 0, 2000, 4000, 6000, 8000]) 44 | fg = ax.contour(X, Y, SDF, 45 | levels = [-8000, -6000, -4000, -2000, 0, 2000, 4000, 6000, 8000], 46 | colors = ['0.1', '0.1', '0.1', '0.1', '0.', '0.1', '0.1', '0.1', '0.1'], 47 | linewidths = [0.25, 0.25, 0.25, 0.25, 1, 0.25, 0.25, 0.25, 0.25]) 48 | cbar = fig.colorbar(bg) 49 | 50 | plt.show() 51 | -------------------------------------------------------------------------------- /ch_10/sdf_rectangle.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib 3 | import numpy as np 4 | from matplotlib import ticker, cm 5 | from numpy import ma 6 | 7 | matplotlib.interactive(True) 8 | matplotlib.use('WebAgg') 9 | 10 | N = 100 11 | x = np.linspace(-10, 10, N) 12 | y = np.linspace(-10, 10, N) 13 | 14 | X, Y = np.meshgrid(x, y) 15 | 16 | # dot product 17 | def dot2d(x1, y1, x2, y2): 18 | return x1*x2 + y1*y2 19 | 20 | # box product 21 | def box2d(x1, y1, x2, y2): 22 | return x1*y2 - x2*y1 23 | 24 | # point-to-point distance 25 | def distance_p2p(x1, y1, x2, y2): 26 | return dot2d(x2-x1, y2-y1, x2-x1, y2-y1)**0.5 27 | # this is the alternative take, feel free to swap 28 | #return ((x2-x1)**2 + (y2-y1)**2)**0.5 29 | 30 | # point-to-line signed distance 31 | def signed_distance_p2l(x, y, x1, y1, x2, y2): 32 | return box2d(x-x1, y-y1, x2-x1, y2-y1) / distance_p2p(x1, y1, x2, y2) 33 | 34 | # sign of a point-to-line distance 35 | def sign_of_distance_p2l(x, y, x1, y1, x2, y2): 36 | return 1 if signed_distance_p2l(x, y, x1, y1, x2, y2) >= 0 else -1 37 | 38 | # point-to-line Euclidean distance 39 | def distance_p2l(x, y, x1, y1, x2, y2): 40 | return abs(signed_distance_p2l(x, y, x1, y1, x2, y2)) 41 | 42 | # point-to-edge distance 43 | def distance_p2e(x, y, x1, y1, x2, y2): 44 | projection_normalized = (dot2d(x-x1, y-y1, x2-x1, y2-y1) 45 | / distance_p2p(x1, y1, x2, y2) ** 2) 46 | if projection_normalized < 0: 47 | return distance_p2p(x, y, x1, y1) 48 | elif projection_normalized > 1: 49 | return distance_p2p(x, y, x2, y2) 50 | else: 51 | return distance_p2l(x, y, x1, y1, x2, y2) 52 | 53 | # signed distance to rectangle 54 | @np.vectorize 55 | def rect(x, y, xmin, ymin, xmax, ymax): 56 | d1 = distance_p2e(x, y, xmin, ymax, xmin, ymin) 57 | d2 = distance_p2e(x, y, xmax, ymax, xmin, ymax) 58 | d3 = distance_p2e(x, y, xmax, ymin, xmax, ymax) 59 | d4 = distance_p2e(x, y, xmin, ymin, xmax, ymin) 60 | if d1 < d2 and d1 < d3 and d1 < d4: 61 | return d1*sign_of_distance_p2l(x, y, xmin, ymax, xmin, ymin) 62 | elif d2 < d3 and d2 < d4: 63 | return d2*sign_of_distance_p2l(x, y, xmax, ymax, xmin, ymax) 64 | elif d3 < d4: 65 | return d3*sign_of_distance_p2l(x, y, xmax, ymin, xmax, ymax) 66 | else: 67 | return d4*sign_of_distance_p2l(x, y, xmin, ymin, xmax, ymin) 68 | 69 | SDF = rect(X, Y, -6, -4, 6, 4) 70 | 71 | 72 | # plotting 73 | fig, ax = plt.subplots() 74 | ax.set_aspect('equal', adjustable='box') 75 | bg = ax.contourf(X, Y, SDF, cmap=cm.coolwarm, levels=[-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10]) 76 | fg = ax.contour(X, Y, SDF, 77 | levels = [-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10], 78 | colors = ['0.1', '0.1', '0.1', '0.1', '0.1', '0.2', '0.1', '0.1', '0.1', '0.1', '0.1'], 79 | linewidths = [0.25, 0.25, 0.25, 0.25, 0.25, 1.0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25]) 80 | cbar = fig.colorbar(bg) 81 | 82 | plt.show() 83 | -------------------------------------------------------------------------------- /ch_10/sdf_simpler_rectangle.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib 3 | import numpy as np 4 | from matplotlib import ticker, cm 5 | from numpy import ma 6 | 7 | matplotlib.interactive(True) 8 | matplotlib.use('WebAgg') 9 | 10 | N = 100 11 | x = np.linspace(-10, 10, N) 12 | y = np.linspace(-10, 10, N) 13 | 14 | X, Y = np.meshgrid(x, y) 15 | 16 | # quasi-SDF that still produces rectangular shape 17 | @np.vectorize 18 | def not_a_true_sdf_rect(x, y, xmin, ymin, xmax, ymax): 19 | return max([xmin-x, ymin-y, x-xmax, y-ymax]) 20 | 21 | # real rectangular SDF 22 | @np.vectorize 23 | def rect(x, y, xmin, ymin, xmax, ymax): 24 | center = [(xmax + xmin) / 2, (ymax + ymin) / 2] 25 | half_width = (xmax - xmin) / 2 26 | half_height = (ymax - ymin) / 2 27 | relative_x = x - center[0] 28 | relative_y = y - center[1] 29 | dx = max(0, abs(relative_x) - half_width) 30 | dy = max(0, abs(relative_y) - half_height) 31 | outside_d = (dx**2 + dy**2)**0.5 32 | inside_d = min(max(dx, dy), 0.0) 33 | return outside_d + inside_d 34 | 35 | # feel free to try any 36 | SDF = not_a_true_sdf_rect(X, Y, -6, -4, 6, 4) 37 | #SDF = rect(X, Y, -6, -4, 6, 4) 38 | 39 | 40 | # plotting 41 | fig, ax = plt.subplots() 42 | ax.set_aspect('equal', adjustable='box') 43 | bg = ax.contourf(X, Y, SDF, cmap=cm.coolwarm, levels=[-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10]) 44 | fg = ax.contour(X, Y, SDF, 45 | levels = [-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10], 46 | colors = ['0.1', '0.1', '0.1', '0.1', '0.1', '0.2', '0.1', '0.1', '0.1', '0.1', '0.1'], 47 | linewidths = [0.25, 0.25, 0.25, 0.25, 0.25, 1.0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25]) 48 | cbar = fig.colorbar(bg) 49 | 50 | plt.show() 51 | -------------------------------------------------------------------------------- /ch_10/sdf_triangle.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib 3 | import numpy as np 4 | from matplotlib import ticker, cm 5 | from numpy import ma 6 | 7 | matplotlib.interactive(True) 8 | matplotlib.use('WebAgg') 9 | 10 | N = 100 11 | x = np.linspace(-10, 10, N) 12 | y = np.linspace(-10, 10, N) 13 | 14 | X, Y = np.meshgrid(x, y) 15 | 16 | # dot product 17 | def dot2d(x1, y1, x2, y2): 18 | return x1*x2 + y1*y2 19 | 20 | # box product 21 | def box2d(x1, y1, x2, y2): 22 | return x1*y2 - x2*y1 23 | 24 | # point-to-point distance 25 | def distance_p2p(x1, y1, x2, y2): 26 | return dot2d(x2-x1, y2-y1, x2-x1, y2-y1)**0.5 27 | #return ((x2-x1)**2 + (y2-y1)**2)**0.5 28 | 29 | # point-to-line signed distance 30 | def signed_distance_p2l(x, y, x1, y1, x2, y2): 31 | return box2d(x-x1, y-y1, x2-x1, y2-y1) / distance_p2p(x1, y1, x2, y2) 32 | 33 | # sign of a point-to-line distance 34 | def sign_of_distance_p2l(x, y, x1, y1, x2, y2): 35 | return 1 if signed_distance_p2l(x, y, x1, y1, x2, y2) >= 0 else -1 36 | 37 | # point-to-line Euclidean distance 38 | def distance_p2l(x, y, x1, y1, x2, y2): 39 | return abs(signed_distance_p2l(x, y, x1, y1, x2, y2)) 40 | 41 | # point-to-edge distance 42 | def distance_p2e(x, y, x1, y1, x2, y2): 43 | projection_normalized = (dot2d(x-x1, y-y1, x2-x1, y2-y1) 44 | / distance_p2p(x1, y1, x2, y2) ** 2) 45 | if projection_normalized < 0: 46 | return distance_p2p(x, y, x1, y1) 47 | elif projection_normalized > 1: 48 | return distance_p2p(x, y, x2, y2) 49 | else: 50 | return distance_p2l(x, y, x1, y1, x2, y2) 51 | 52 | # signed distance to a triangle 53 | @np.vectorize 54 | def triangle(x, y, x1, y1, x2, y2, x3, y3): 55 | d1 = distance_p2e(x, y, x1, y1, x2, y2) 56 | d2 = distance_p2e(x, y, x2, y2, x3, y3) 57 | d3 = distance_p2e(x, y, x3, y3, x1, y1) 58 | sign = -1 if sum([sign_of_distance_p2l(x, y, x1, y1, x2, y2) 59 | , sign_of_distance_p2l(x, y, x2, y2, x3, y3) 60 | , sign_of_distance_p2l(x, y, x3, y3, x1, y1)]) == -3 else 1; 61 | return min([d1, d2, d3]) * sign 62 | 63 | SDF = triangle(X, Y, -6, -5, 6, -5, 0, 7) 64 | 65 | 66 | # plotting 67 | fig, ax = plt.subplots() 68 | ax.set_aspect('equal', adjustable='box') 69 | bg = ax.contourf(X, Y, SDF, cmap=cm.coolwarm, levels=[-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10]) 70 | fg = ax.contour(X, Y, SDF, 71 | levels = [-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10], 72 | colors = ['0.1', '0.1', '0.1', '0.1', '0.1', '0.2', '0.1', '0.1', '0.1', '0.1', '0.1'], 73 | linewidths = [0.25, 0.25, 0.25, 0.25, 0.25, 1.0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25]) 74 | cbar = fig.colorbar(bg) 75 | 76 | plt.show() 77 | -------------------------------------------------------------------------------- /ch_11/smooth_contour.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib 3 | import numpy as np 4 | from matplotlib import ticker, cm 5 | from matplotlib import collections as mc 6 | from numpy import ma 7 | 8 | matplotlib.interactive(True) 9 | matplotlib.use('WebAgg') 10 | 11 | N = 100 12 | x = np.linspace(0, 10, N) 13 | y = np.linspace(0, 10, N) 14 | 15 | X, Y = np.meshgrid(x, y) 16 | 17 | # dot product 18 | def dot2d(x1, y1, x2, y2): 19 | return x1*x2 + y1*y2 20 | 21 | # box product 22 | def box2d(x1, y1, x2, y2): 23 | return x1*y2 - x2*y1 24 | 25 | # point-to-point distance 26 | def distance_p2p(x1, y1, x2, y2): 27 | return dot2d(x2-x1, y2-y1, x2-x1, y2-y1)**0.5 28 | 29 | # point-to-line signed distance 30 | def signed_distance_p2l(x, y, x1, y1, x2, y2): 31 | return box2d(x-x1, y-y1, x2-x1, y2-y1) / distance_p2p(x1, y1, x2, y2) 32 | 33 | # sign of a point-to-line distance 34 | def sign_of_distance_p2l(x, y, x1, y1, x2, y2): 35 | return 1 if signed_distance_p2l(x, y, x1, y1, x2, y2) >= 0 else -1 36 | 37 | # point-to-line Euclidean distance 38 | def distance_p2l(x, y, x1, y1, x2, y2): 39 | return abs(signed_distance_p2l(x, y, x1, y1, x2, y2)) 40 | 41 | # point-to-edge distance 42 | def distance_p2e(x, y, x1, y1, x2, y2): 43 | projection_normalized = (dot2d(x-x1, y-y1, x2-x1, y2-y1) 44 | / distance_p2p(x1, y1, x2, y2) ** 2) 45 | if projection_normalized < 0: 46 | return distance_p2p(x, y, x1, y1) 47 | elif projection_normalized > 1: 48 | return distance_p2p(x, y, x2, y2) 49 | else: 50 | return distance_p2l(x, y, x1, y1, x2, y2) 51 | 52 | # distance to a segment 53 | @np.vectorize 54 | def line(x, y, x1, y1, x2, y2, w): 55 | return distance_p2e(x, y, x1, y1, x2, y2) - w 56 | 57 | # V shape 58 | def SDF(x, y): 59 | return np.minimum(line(x, y, 5, 2.5, 3, 6, 1.2), line(x, y, 5, 2.5, 7, 7.5, 1.2)) 60 | 61 | Z = SDF(X, Y) 62 | 63 | # background plotting 64 | fig, ax = plt.subplots() 65 | ax.set_aspect('equal', adjustable='box') 66 | bg = ax.contourf(X, Y, Z, cmap=cm.coolwarm, levels = [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5]) 67 | fg = ax.contour(X, Y, Z, 68 | levels = [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5], 69 | colors=['0.1', '0.1', '0.1', '0.1', '0.1', '0.2', '0.1', '0.1', '0.1', '0.1', '0.1'], 70 | linewidths=[0.25, 0.25, 0.25, 0.25, 0.25, 1.0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25]) 71 | cbar = fig.colorbar(bg) 72 | 73 | 74 | 75 | # Step 1: do the cell border 76 | border = [] 77 | for i in range(-10, 10): 78 | for j in range(-10, 10): 79 | # vertical line 80 | if SDF(j - 0.5, i + 0.5) * SDF(j + 0.5, i + 0.5) < 0: 81 | border += [[(j, i), (j, i+1)]] 82 | # horisontal line 83 | if SDF(j + 0.5, i - 0.5) * SDF(j + 0.5, i + 0.5) < 0: 84 | border += [[(j, i), (j+1, i)]] 85 | 86 | # Step 2: fit the contour 87 | epsilon = 1e-5 88 | 89 | # an approximation of the partial /dx derivative 90 | def dx(x, y): 91 | return (SDF(x+epsilon, y) - SDF(x, y)) / epsilon 92 | 93 | # an approximation of the partial /dy derivative 94 | def dy(x, y): 95 | return (SDF(x, y+epsilon) - SDF(x, y)) / epsilon 96 | 97 | contour = [] 98 | for segment in border: 99 | new_segment = [] 100 | for i in range(2): 101 | x, y = segment[i] 102 | pdx = dx(x, y) # x-part of the gradient vector 103 | pdy = dy(x, y) # y-part of the gradient vector 104 | # we’ll use the gradient’s length to normalize the fitting direction 105 | dxy_length = distance_p2p(pdx, pdy, 0, 0) 106 | # this is also the fitting vector’s new length 107 | sdf_value = SDF(x, y) 108 | new_p = (x - pdx / dxy_length * sdf_value, 109 | y - pdy / dxy_length * sdf_value) 110 | new_segment += [new_p] 111 | contour += [new_segment] 112 | 113 | 114 | # Step 3: smoothen the contour 115 | smoothened = [] 116 | for segment in contour: 117 | x0 = segment[0][0] 118 | y0 = segment[0][1] 119 | x1 = segment[1][0] 120 | y1 = segment[1][1] 121 | 122 | # tangents from gradient 123 | # 124 | # the gradient is orthogonal to the tangent direction, 125 | # so dx goes to the y-part and dy to the x-part of the tangent. 126 | dx0 = dy(x0, y0) 127 | dy0 = -dx(x0, y0) 128 | dx1 = dy(x1, y1) 129 | dy1 = -dx(x1, y1) 130 | 131 | # invert the tangents if they disagree with the segment direction 132 | # 133 | # there are two possible directions orthogonal to the gradient. 134 | # Check whether the first best choice agrees with the piece's direction. 135 | # Invert if it doesn’t. 136 | if dot2d(x1-x0, y1-y0, dx0, dy0) < 0: 137 | dx0 = -dx0 138 | dy0 = -dy0 139 | if dot2d(x1-x0, y1-y0, dx1, dy1) < 0: 140 | dx1 = -dx1 141 | dy1 = -dy1 142 | 143 | # compute the cubic coefficients 144 | a_x = dx0 + dx1 + 2*x0 - 2*x1 145 | b_x = -2*dx0 - dx1 - 3*x0 + 3*x1 146 | c_x = dx0 147 | d_x = x0 148 | a_y = dy0 + dy1 + 2*y0 - 2*y1 149 | b_y = -2*dy0 - dy1 - 3*y0 + 3*y1 150 | c_y = dy0 151 | d_y = y0 152 | 153 | # refine the contour 154 | # 155 | # this last step is not essential for the algorithm. 156 | # It’s just instead of drawing the smooth contour 157 | # as a set of cubic polynomials, we refine each into 158 | # a set of short flat segments. Now we can draw the border, 159 | # the line segment contour, and the smoothened result 160 | # all in the same way. 161 | for i in range(10): 162 | t0 = i / 10. 163 | t1 = t0 + 1. / 10. 164 | x0 = a_x*t0**3 + b_x*t0**2 + c_x*t0 + d_x 165 | y0 = a_y*t0**3 + b_y*t0**2 + c_y*t0 + d_y 166 | x1 = a_x*t1**3 + b_x*t1**2 + c_x*t1 + d_x 167 | y1 = a_y*t1**3 + b_y*t1**2 + c_y*t1 + d_y 168 | smoothened += [[(x0, y0), (x1, y1)]] 169 | 170 | 171 | # plot the result (uncomment the line you want to see intermediate results) 172 | #lc = mc.LineCollection(border) 173 | #lc = mc.LineCollection(contour) 174 | lc = mc.LineCollection(smoothened) 175 | 176 | lc.set_color("black") 177 | lc.set_linewidth(2) 178 | ax.add_collection(lc) 179 | 180 | plt.show() 181 | -------------------------------------------------------------------------------- /ch_12/denoise.py: -------------------------------------------------------------------------------- 1 | from math import inf 2 | import numpy as np 3 | 4 | image = [[0, 0, 0, 0, 0, 0, 0, 0], 5 | [0, 1, 1, 0, 0, 0, 0, 0], 6 | [0, 0, 0, 0, 0, 0, 0, 0], 7 | [0, 0, 0, 0, 0, 0, 0, 0], 8 | [0, 0, 0, 0, 0, 0, 1, 0], 9 | [1, 1, 0, 0, 0, 0, 1, 0], 10 | [1, 1, 1, 1, 1, 1, 1, 1], 11 | [1, 1, 1, 1, 1, 1, 1, 1]] 12 | 13 | 14 | def dilate(img, depth): 15 | # 1. initializing the distance map with infinite values 16 | distances = [[inf for _ in row] for row in img] 17 | 18 | # 2. doing the first travel: left to right, top to bottom 19 | for i in range(len(img)): 20 | for j in range(len(img[0])): 21 | if img[i][j] == 1: 22 | distances[i][j] = 0 23 | continue 24 | if j > 0 and img[i][j-1] == 1: 25 | distances[i][j] = 1 26 | if i > 0 and img[i-1][j] == 1: 27 | distances[i][j] = 1 28 | candidates = [distances[i][j]] 29 | if j > 0: 30 | candidates += [distances[i][j-1] + 1] 31 | if i > 0: 32 | candidates += [distances[i-1][j] + 1] 33 | distances[i][j] = min(candidates) 34 | 35 | # 3. doing the reverse travel: right to left, bottom to top 36 | for i in reversed(range(len(img))): 37 | for j in reversed(range(len(img[0]))): 38 | if img[i][j] == 1: 39 | distances[i][j] = 0 40 | continue 41 | if j < len(img[0]) - 1 and img[i][j+1] == 1: 42 | distances[i][j] = 1 43 | if i < len(img) - 1 and img[i+1][j] == 1: 44 | distances[i][j] = 1 45 | candidates = [distances[i][j]] 46 | if j > 0: 47 | candidates += [distances[i][j-1] + 1] 48 | if i > 0: 49 | candidates += [distances[i-1][j] + 1] 50 | if j < len(img[0]) - 1: 51 | candidates += [distances[i][j+1] + 1] 52 | if i < len(img) - 1: 53 | candidates += [distances[i+1][j] + 1] 54 | distances[i][j] = min(candidates) 55 | 56 | # marking all the voxels with a distance lower or equal to the depth value 57 | for i in range(len(img)): 58 | for j in range(len(img[0])): 59 | if distances[i][j] <= depth: 60 | img[i][j] = 1 61 | return img 62 | 63 | 64 | # a simple rule: every one becomes a zero, and every zero – a one 65 | def invert(img): 66 | for i in range(len(img)): 67 | for j in range(len(img[0])): 68 | img[i][j] = 1 - img[i][j] 69 | return img 70 | 71 | # erosion is then just an inverted dilation of an inverted image 72 | def erode(img, depth): 73 | return invert(dilate(invert(img), depth)) 74 | 75 | # denoise is now jut an erode-dilate workflow 76 | def denoise(img, noise_size): 77 | return dilate(erode(img, noise_size), noise_size) 78 | 79 | # a pretty-print 80 | def pprint(img): 81 | for row in img: 82 | print(row) 83 | 84 | print(" image before:") 85 | pprint(image) 86 | print("") 87 | print(" image after denoising:") 88 | pprint(denoise(image, 1)) 89 | -------------------------------------------------------------------------------- /ch_12/image_vectorization.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib 3 | import numpy as np 4 | from matplotlib import ticker, cm 5 | from matplotlib import collections as mc 6 | from numpy import ma 7 | 8 | matplotlib.interactive(True) 9 | matplotlib.use('WebAgg') 10 | 11 | from pylab import rcParams 12 | rcParams['figure.figsize'] = 8, 8 13 | 14 | # width 15 | w = 16 16 | 17 | # height 18 | h = 16 19 | 20 | # linspace parameters. Visualisation only, irrelevant for the algorithm 21 | N = 128 22 | x = np.linspace(0, w, N) 23 | y = np.linspace(0, h, N) 24 | 25 | # the image is a two-dimensional array of 1-channel 8-bit depth “grey values” 26 | img = [ 27 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 28 | [0, 77, 125,38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 29 | [0, 120,255,254,203,144,96, 3, 0, 0, 0, 0, 0, 0, 0, 0], 30 | [0, 34, 253,255,255,255,255,230,154,94, 8, 0, 0, 0, 0, 0], 31 | [0, 0, 196,255,255,255,255,255,255,252,241,139,83, 6, 0, 0], 32 | [0, 0, 149,255,255,255,255,255,255,255,255,255,250,213,80, 0], 33 | [0, 0, 98, 255,255,255,255,255,255,255,255,255,255,224,58, 0], 34 | [0, 0, 2, 224,255,255,255,255,255,255,255,242,152,4, 0, 0], 35 | [0, 0, 0, 145,255,255,255,255,255,255,255,154,1, 0, 0, 0], 36 | [0, 0, 0, 82, 251,255,255,255,255,255,255,253,156,1, 0, 0], 37 | [0, 0, 0, 6, 237,255,255,255,255,255,255,255,252,146,3, 0], 38 | [0, 0, 0, 0, 149,255,255,243,149,252,255,255,255,240,21, 0], 39 | [0, 0, 0, 0, 69, 249,255,152,1, 150,252,255,238,71, 0, 0], 40 | [0, 0, 0, 0, 0, 211,224,4, 0, 1, 137,240,86, 0, 0, 0], 41 | [0, 0, 0, 0, 0, 73, 57, 0, 0, 0, 2, 20, 0, 0, 0, 0], 42 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 43 | ] 44 | 45 | # with distance threshold, we govern where we want our isoline to pass. 46 | # The number 128 implies that we expect a curve roughly 47 | # in the middle between full light and full dark pixels 48 | distance_threshold = 128 49 | 50 | # with the accuracy threshold, we regulate how fast our fitting will exit. 51 | # It’s a lever to balance speed and accuracy 52 | accuracy_threshold = 2 53 | 54 | 55 | # a function of pixel values. It is not continuous 56 | @np.vectorize 57 | def img_as_f(x, y): 58 | if x > 0 and x < w and y >= 0 and y < h: 59 | return img[h-1-int(y)][int(x)] 60 | return 0 61 | 62 | # a smooth and continuous image function 63 | @np.vectorize 64 | def img_as_smooth_f(x, y): 65 | if x > 0 and x < w and y >= 0 and y < h: 66 | # this is a trick to shift the whole function 67 | # to (0.5, 0.5) vector. Without this shift, 68 | # our pixel's colors are defined in the pixels’ 69 | # top left corners. With this shift, the colors 70 | # are now defined in the pixel’s centers. 71 | centered_x = x - 0.5 72 | centered_y = y - 0.5 73 | 74 | # truncated coordinates 75 | ix = int(centered_x) 76 | iy = int(centered_y) 77 | 78 | # these are the in-pixel distance coefficients. 79 | # We take 4 of them – one per each pixel’s side 80 | x0 = centered_x - ix 81 | x1 = 1 - x0 82 | y0 = centered_y - iy 83 | y1 = 1 - y0 84 | 85 | # literally the corner cases. 86 | # With our (0.5, 0.5) shifts, the corners now coincide 87 | # with the pixel’s centers, so if we detect a corner case, 88 | # we just return the color value from the corresponding pixel 89 | if x0 == 0 and y0 == 0: 90 | return img_as_f(ix, iy) 91 | if x0 == 0 and y1 == 0: 92 | return img_as_f(ix, iy + 1) 93 | if x1 == 0 and y0 == 0: 94 | return img_as_f(ix + 1, iy) 95 | if x1 == 0 and y1 == 0: 96 | return img_as_f(ix + 1, iy + 1) 97 | 98 | # pixel’s sides. 99 | # Special cases of inverse distance interpolation 100 | # with just two interpolated values 101 | if x0 == 0: 102 | return ((img_as_f(ix, iy) / y0 + img_as_f(ix, iy + 1) / y1) 103 | / (1/y0 + 1/y1)) 104 | if x1 == 0: 105 | return ((img_as_f(ix+1, iy) / y0 + img_as_f(ix+1, iy + 1) / y1) 106 | / (1/y0 + 1/y1)) 107 | if y0 == 0: 108 | return ((img_as_f(ix, iy) / x0 + img_as_f(ix + 1, iy) / x1) 109 | / (1/x0 + 1/x1)) 110 | if y1 == 0: 111 | return ((img_as_f(ix, iy+1) / x0 + img_as_f(ix + 1, iy+1) / x1) 112 | / (1/x0 + 1/x1)) 113 | 114 | # finally, the general case of inverse distance interpolation; 115 | # all four corner values are being mixed together 116 | return ((img_as_f(ix, iy) / (x0*y0) + 117 | img_as_f(ix, iy+1) / (x0*y1) + 118 | img_as_f(ix+1, iy) / (x1*y0) + 119 | img_as_f(ix+1, iy+1) / (x1*y1)) 120 | / (1/(x0*y0) + 1/(x0*y1) + 1/(x1*y0) + 1/(x1*y1))) 121 | return 0 122 | 123 | 124 | def dot2d(x1, y1, x2, y2): 125 | return x1*x2 + y1*y2 126 | 127 | def distance_p2p(x1, y1, x2, y2): 128 | return dot2d(x2-x1, y2-y1, x2-x1, y2-y1)**0.5 129 | 130 | X, Y = np.meshgrid(x, y) 131 | 132 | 133 | def SDF(x, y): 134 | return img_as_smooth_f(x, y) - distance_threshold 135 | 136 | Z = SDF(X, Y) + distance_threshold 137 | 138 | fig, ax = plt.subplots() 139 | ax.set_aspect('equal', adjustable='box') 140 | bg = ax.contourf(X, Y, Z, cmap=cm.Blues_r, levels = [16 * i for i in range(-16, 16)]) 141 | cbar = fig.colorbar(bg) 142 | 143 | # Step 1: do the cell border 144 | 145 | # the border will be a list of line segments 146 | # where each line segment is a 2-piece list of 2D points 147 | border = [] 148 | for i in range(0, 16): 149 | for j in range(0, 16): 150 | # vertical line 151 | if SDF(j - 0.5, i + 0.5) * SDF(j + 0.5, i + 0.5) < 0: 152 | border += [[(j, i), (j, i+1)]] 153 | # horisontal line 154 | if SDF(j + 0.5, i - 0.5) * SDF(j + 0.5, i + 0.5) < 0: 155 | border += [[(j, i), (j+1, i)]] 156 | 157 | # Step 2: fit the contour 158 | 159 | # we approximate partial derivatives with differences. 160 | # An epsilon – is a difference step size. Normally, 161 | # the smaller the size, the better the approximation 162 | epsilon = 1e-5 163 | 164 | # partial derivatives 165 | def dx(x, y): 166 | return (SDF(x+epsilon, y) - SDF(x-epsilon, y)) / (2*epsilon) 167 | def dy(x, y): 168 | return (SDF(x, y+epsilon) - SDF(x, y-epsilon)) / (2*epsilon) 169 | 170 | contour = [] 171 | # we take the vertices from the initial border as a set of starting points 172 | for segment in border: 173 | new_segment = [] 174 | for i in range(2): 175 | x, y = segment[i] 176 | sdf_value = SDF(x, y) 177 | while abs(sdf_value) > accuracy_threshold: # the exit condition 178 | step_length = sdf_value / 256 179 | pdx = dx(x, y) 180 | pdy = dy(x, y) 181 | dxy_length = distance_p2p(pdx, pdy, 0, 0) 182 | # the iteration itself 183 | x = x - pdx / dxy_length * step_length 184 | y = y - pdy / dxy_length * step_length 185 | sdf_value = SDF(x, y) 186 | new_segment += [(x, y)] 187 | contour += [new_segment] 188 | 189 | 190 | # Step 3: smoothen the contour 191 | smoothened = [] 192 | for segment in contour: 193 | # initial values for the cubics’ ends 194 | # come from the segments’ vertices 195 | x0 = segment[0][0] 196 | y0 = segment[0][1] 197 | x1 = segment[1][0] 198 | y1 = segment[1][1] 199 | 200 | # initial tangent vectors come from partial derivatives 201 | dx0 = dy(x0, y0) 202 | dy0 = -dx(x0, y0) 203 | dx1 = dy(x1, y1) 204 | dy1 = -dx(x1, y1) 205 | 206 | # invert the tangents if the disagree with the segment direction 207 | if dot2d(x1-x0, y1-y0, dx0, dy0) < 0: 208 | dx0 = -dx0 209 | dy0 = -dy0 210 | if dot2d(x1-x0, y1-y0, dx1, dy1) < 0: 211 | dx1 = -dx1 212 | dy1 = -dy1 213 | 214 | # normalize the partial derivatives to get rid of the “loops” 215 | dxy_length0 = distance_p2p(dx0, dy0, 0, 0) 216 | dxy_length1 = distance_p2p(dx1, dy1, 0, 0) 217 | dx0 = dx0 / dxy_length0 218 | dy0 = dy0 / dxy_length0 219 | dx1 = dx1 / dxy_length1 220 | dy1 = dy1 / dxy_length1 221 | 222 | # compute the cubic coefficients 223 | # 224 | # We get the cubics’ coefficients by solving 225 | # two systems of equations. Here are the ready-made 226 | # solutions provided to us by SymPy (see chapter 11) 227 | a_x = dx0 + dx1 + 2*x0 - 2*x1 228 | b_x = -2*dx0 - dx1 - 3*x0 + 3*x1 229 | c_x = dx0 230 | d_x = x0 231 | a_y = dy0 + dy1 + 2*y0 - 2*y1 232 | b_y = -2*dy0 - dy1 - 3*y0 + 3*y1 233 | c_y = dy0 234 | d_y = y0 235 | 236 | # refine the contour 237 | # 238 | # This is just a technicality – we turn cubics 239 | # back into linear segments so we can draw them 240 | # in the same way we drew the initial and fitted contours 241 | for i in range(16): 242 | t0 = i / 16. 243 | t1 = t0 + 1. / 16. 244 | x0 = a_x*t0**3 + b_x*t0**2 + c_x*t0 + d_x 245 | y0 = a_y*t0**3 + b_y*t0**2 + c_y*t0 + d_y 246 | x1 = a_x*t1**3 + b_x*t1**2 + c_x*t1 + d_x 247 | y1 = a_y*t1**3 + b_y*t1**2 + c_y*t1 + d_y 248 | smoothened += [[(x0, y0), (x1, y1)]] 249 | 250 | # draw the result 251 | # 252 | # Feel free to uncomment any line you want. 253 | # All the contours are plotted in the similar way. 254 | 255 | #lc = mc.LineCollection(border) 256 | #lc = mc.LineCollection(contour) 257 | lc = mc.LineCollection(smoothened) 258 | 259 | lc.set_color("black") 260 | lc.set_linewidth(3) 261 | ax.add_collection(lc) 262 | 263 | plt.show() 264 | --------------------------------------------------------------------------------