123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102 |
- """Take 2 clouds of points, source and target, and morph
- the plane using thin plate splines as a model.
- The fitting minimizes the distance to a subset of the target cloud"""
- from vedo import printc, Points, Grid, Arrows, Lines, Plotter
- import scipy.optimize as opt
- import numpy as np
- np.random.seed(2)
- class Morpher(Plotter):
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self.source = None
- self.morphed_source = None
- self.target = None
- self.bound = 1
- self.sigma = 1 # stiffness of the mesh
- self.method = "SLSQP" # 'SLSQP', 'L-BFGS-B', 'TNC' ...
- self.fitTolerance = 1e-6
- self.fitResult = None
- self.chi2 = 1.0e30
- self.npts = None
- self.ptsource = []
- self.pttarget = []
- def _func(self, pars):
- shift = np.array(np.split(pars, 2)).T # recreate the shift vectors
- z = np.zeros((self.npts, 1))
- shift = np.append(shift, z, axis=1) # make them 3d
- self.morphed_source = self.source.clone().warp(
- self.ptsource, self.ptsource + shift, sigma=self.sigma, mode="2d"
- )
- d = self.morphed_source.vertices - self.target.vertices
- chi2 = np.sum(np.multiply(d, d)) # /len(d)
- if chi2 < self.chi2:
- printc("new minimum ->", chi2)
- self.chi2 = chi2
- return chi2
- # ------------------------------------------------------- Fit
- def morph(self):
- print("\n..minimizing with " + self.method)
- self.morphed_source = self.source.clone()
- self.ptsource = self.source.vertices[: self.npts] # pick the first npts points
- self.pttarget = self.target.vertices[: self.npts]
- delta = self.pttarget - self.ptsource
- x0 = delta[:, (0, 1)].T.ravel() # initial guess, a flat list of x and y shifts
- bnds = [(-self.bound, self.bound)] * (2 * self.npts)
- res = opt.minimize(
- self._func, x0, bounds=bnds, method=self.method, tol=self.fitTolerance
- )
- self.fitResult = res
- # recalculate the last step:
- self._func(res["x"])
- # ------------------------------------------------------- Visualization
- def draw_shapes(self):
- sb = self.source.bounds()
- x1, x2, y1, y2, _, _ = sb
- maxb = max(x2 - x1, y2 - y1)
- grid0 = Grid(self.source.center_of_mass(), s=[maxb, maxb], res=[40, 40])
- T = self.morphed_source.transform
- grid1 = grid0.clone().apply_transform(T) # warp the grid
- arrows = Arrows(self.ptsource, self.pttarget, alpha=0.5, s=3).c("k")
- lines = Lines(self.source, self.target).c("db")
- mlines = Lines(self.morphed_source, self.target).c("db")
- self.at(0).show(grid0, self.source, self.target, lines, arrows, __doc__)
- self.at(1).show(
- grid1,
- self.morphed_source,
- self.target,
- mlines,
- f"morphed source (green) vs target (red)\nNDF = {2*self.npts}",
- )
- #################################
- if __name__ == "__main__":
- # make up a source random cloud
- pts_s = np.random.randn(25, 2)
- pts_t = pts_s + np.sin(2 * pts_s) / 5 # and distort it
- mr = Morpher(N=2)
- mr.source = Points(pts_s).color("g",0.5).ps(20)
- mr.target = Points(pts_t).color("r",1.0).ps(10)
- mr.bound = 2 # limits the x and y shift
- # allow move only a subset of points (implicitly sets the NDF of the fit)
- mr.npts = 6
- mr.sigma = 1.0 # stiffness of the mesh (1=max stiffness)
- mr.morph()
- # now mr.msource contains the modified/morphed source.
- mr.draw_shapes()
- mr.interactive().close()
|