warp3.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. """Take 2 clouds of points, source and target, and morph
  2. the plane using thin plate splines as a model.
  3. The fitting minimizes the distance to a subset of the target cloud"""
  4. from vedo import printc, Points, Grid, Arrows, Lines, Plotter
  5. import scipy.optimize as opt
  6. import numpy as np
  7. np.random.seed(2)
  8. class Morpher(Plotter):
  9. def __init__(self, **kwargs):
  10. super().__init__(**kwargs)
  11. self.source = None
  12. self.morphed_source = None
  13. self.target = None
  14. self.bound = 1
  15. self.sigma = 1 # stiffness of the mesh
  16. self.method = "SLSQP" # 'SLSQP', 'L-BFGS-B', 'TNC' ...
  17. self.fitTolerance = 1e-6
  18. self.fitResult = None
  19. self.chi2 = 1.0e30
  20. self.npts = None
  21. self.ptsource = []
  22. self.pttarget = []
  23. def _func(self, pars):
  24. shift = np.array(np.split(pars, 2)).T # recreate the shift vectors
  25. z = np.zeros((self.npts, 1))
  26. shift = np.append(shift, z, axis=1) # make them 3d
  27. self.morphed_source = self.source.clone().warp(
  28. self.ptsource, self.ptsource + shift, sigma=self.sigma, mode="2d"
  29. )
  30. d = self.morphed_source.vertices - self.target.vertices
  31. chi2 = np.sum(np.multiply(d, d)) # /len(d)
  32. if chi2 < self.chi2:
  33. printc("new minimum ->", chi2)
  34. self.chi2 = chi2
  35. return chi2
  36. # ------------------------------------------------------- Fit
  37. def morph(self):
  38. print("\n..minimizing with " + self.method)
  39. self.morphed_source = self.source.clone()
  40. self.ptsource = self.source.vertices[: self.npts] # pick the first npts points
  41. self.pttarget = self.target.vertices[: self.npts]
  42. delta = self.pttarget - self.ptsource
  43. x0 = delta[:, (0, 1)].T.ravel() # initial guess, a flat list of x and y shifts
  44. bnds = [(-self.bound, self.bound)] * (2 * self.npts)
  45. res = opt.minimize(
  46. self._func, x0, bounds=bnds, method=self.method, tol=self.fitTolerance
  47. )
  48. self.fitResult = res
  49. # recalculate the last step:
  50. self._func(res["x"])
  51. # ------------------------------------------------------- Visualization
  52. def draw_shapes(self):
  53. sb = self.source.bounds()
  54. x1, x2, y1, y2, _, _ = sb
  55. maxb = max(x2 - x1, y2 - y1)
  56. grid0 = Grid(self.source.center_of_mass(), s=[maxb, maxb], res=[40, 40])
  57. T = self.morphed_source.transform
  58. grid1 = grid0.clone().apply_transform(T) # warp the grid
  59. arrows = Arrows(self.ptsource, self.pttarget, alpha=0.5, s=3).c("k")
  60. lines = Lines(self.source, self.target).c("db")
  61. mlines = Lines(self.morphed_source, self.target).c("db")
  62. self.at(0).show(grid0, self.source, self.target, lines, arrows, __doc__)
  63. self.at(1).show(
  64. grid1,
  65. self.morphed_source,
  66. self.target,
  67. mlines,
  68. f"morphed source (green) vs target (red)\nNDF = {2*self.npts}",
  69. )
  70. #################################
  71. if __name__ == "__main__":
  72. # make up a source random cloud
  73. pts_s = np.random.randn(25, 2)
  74. pts_t = pts_s + np.sin(2 * pts_s) / 5 # and distort it
  75. mr = Morpher(N=2)
  76. mr.source = Points(pts_s).color("g",0.5).ps(20)
  77. mr.target = Points(pts_t).color("r",1.0).ps(10)
  78. mr.bound = 2 # limits the x and y shift
  79. # allow move only a subset of points (implicitly sets the NDF of the fit)
  80. mr.npts = 6
  81. mr.sigma = 1.0 # stiffness of the mesh (1=max stiffness)
  82. mr.morph()
  83. # now mr.msource contains the modified/morphed source.
  84. mr.draw_shapes()
  85. mr.interactive().close()