123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- """
- Takes 2 shapes, source and target, and morphs source on target
- this is obtained by fitting 18 parameters of a non linear,
- quadratic, transformation defined in transform()
- The fitting minimizes the distance to the target surface
- using algorithms available in the scipy.optimize package.
- """
- from vedo import dataurl, vector, mag2, mag
- from vedo import Plotter, Sphere, Point, Text3D, Arrows, Mesh
- import scipy.optimize as opt
- print(__doc__)
- class Morpher:
- def __init__(self):
- self.source = None
- self.target = None
- self.bound = 0.1
- self.method = "SLSQP" # 'SLSQP', 'L-BFGS-B', 'TNC' ...
- self.tolerance = 0.0001
- self.subsample = 200 # pick only subsample pts
- self.allow_scaling = False
- self.params = []
- self.msource = None
- self.s_size = ([0, 0, 0], 1) # ave position and ave size
- self.fitResult = None
- self.chi2 = 1.0e10
- self.plt = None
- # -------------------------------------------------------- fit function
- def transform(self, p):
- a1, a2, a3, a4, a5, a6, b1, b2, b3, b4, b5, b6, c1, c2, c3, c4, c5, c6, s = self.params
- pos, sz = self.s_size[0], self.s_size[1]
- x, y, z = (p - pos) / sz * s # bring to origin, norm and scale
- xx, yy, zz, xy, yz, xz = x * x, y * y, z * z, x * y, y * z, x * z
- xp = x + 2 * a1 * xy + a4 * xx + 2 * a2 * yz + a5 * yy + 2 * a3 * xz + a6 * zz
- yp = +2 * b1 * xy + b4 * xx + y + 2 * b2 * yz + b5 * yy + 2 * b3 * xz + b6 * zz
- zp = +2 * c1 * xy + c4 * xx + 2 * c2 * yz + c5 * yy + z + 2 * c3 * xz + c6 * zz
- p2 = vector(xp, yp, zp)
- p2 = (p2 * sz) + pos # take back to original size and position
- return p2
- def _func(self, pars):
- self.params = pars
- #calculate chi2
- d2sum, n = 0.0, self.source.npoints
- srcpts = self.source.vertices
- rng = range(0, n, int(n / self.subsample))
- for i in rng:
- p1 = srcpts[i]
- p2 = self.transform(p1)
- tp = self.target.closest_point(p2)
- d2sum += mag2(p2 - tp)
- d2sum /= len(rng)
- if d2sum < self.chi2:
- if d2sum < self.chi2 * 0.99:
- print("Emin ->", d2sum)
- self.chi2 = d2sum
- return d2sum
- # ------------------------------------------------------- Fit
- def morph(self):
- def avesize(pts): # helper fnc
- s, amean = 0, vector(0, 0, 0)
- for p in pts:
- amean = amean + p
- amean /= len(pts)
- for p in pts:
- s += mag(p - amean)
- return amean, s / len(pts)
- print("\n..minimizing with " + self.method)
- self.msource = self.source.clone()
- self.s_size = avesize(self.source.vertices)
- bnds = [(-self.bound, self.bound)] * 18
- x0 = [0.0] * 18 # initial guess
- x0 += [1.0] # the optional scale
- if self.allow_scaling:
- bnds += [(1.0 - self.bound, 1.0 + self.bound)]
- else:
- bnds += [(1.0, 1.0)] # fix scale to 1
- res = opt.minimize(self._func, x0,
- bounds=bnds, method=self.method, tol=self.tolerance)
- # recalc for all pts:
- self.subsample = self.source.npoints
- self._func(res["x"])
- print("\nFinal fit score", res["fun"])
- self.fitResult = res
- # ------------------------------------------------------- Visualization
- def draw_shapes(self):
- newpts = []
- for p in self.msource.vertices:
- newp = self.transform(p)
- newpts.append(newp)
- self.msource.vertices = newpts
- arrs = []
- pos, sz = self.s_size[0], self.s_size[1]
- sphere0 = Sphere(pos, r=sz, res=10, quads=True).wireframe().c("gray")
- for p in sphere0.vertices:
- newp = self.transform(p)
- arrs.append([p, newp])
- hair = Arrows(arrs, s=0.3, c='jet').add_scalarbar()
- zero = Point(pos).c("black")
- x1, x2, y1, y2, z1, z2 = self.target.bounds()
- tpos = [x1, y2, z1]
- text1 = Text3D("source vs target", tpos, s=sz/10).color("dg")
- text2 = Text3D("morphed vs target", tpos, s=sz/10).color("db")
- text3 = Text3D("deformation", tpos, s=sz/10).color("dr")
- self.plt = Plotter(shape=[1, 3], axes=1)
- self.plt.at(2).show(sphere0, zero, text3, hair)
- self.plt.at(1).show(self.msource, self.target, text2)
- self.plt.at(0).show(self.source, self.target, text1, zoom=1.2)
- self.plt.interactive().close()
- #################################
- if __name__ == "__main__":
- mr = Morpher()
- mr.source = Mesh(dataurl+"270.vtk").color("g",0.4)
- mr.target = Mesh(dataurl+"290.vtk").color("b",0.3)
- mr.target.wireframe()
- mr.allow_scaling = True
- mr.bound = 0.4 # limits the parameter value
- mr.morph()
- print("Result of parameter fit:\n", mr.params)
- # now mr.msource contains the modified/morphed source.
- mr.draw_shapes()
|