tocubic.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. # Some code to convert arbitrary curves to high quality cubics.
  2. # Some conventions: points are (x, y) pairs. Cubic Bezier segments are
  3. # lists of four points.
  4. import sys
  5. from math import *
  6. import pcorn
  7. def pt_wsum(points, wts):
  8. x, y = 0, 0
  9. for i in range(len(points)):
  10. x += points[i][0] * wts[i]
  11. y += points[i][1] * wts[i]
  12. return x, y
  13. # Very basic spline primitives
  14. def bz_eval(bz, t):
  15. degree = len(bz) - 1
  16. mt = 1 - t
  17. if degree == 3:
  18. return pt_wsum(bz, [mt * mt * mt, 3 * mt * mt * t, 3 * mt * t * t, t * t * t])
  19. elif degree == 2:
  20. return pt_wsum(bz, [mt * mt, 2 * mt * t, t * t])
  21. elif degree == 1:
  22. return pt_wsum(bz, [mt, t])
  23. def bz_deriv(bz):
  24. degree = len(bz) - 1
  25. return [(degree * (bz[i + 1][0] - bz[i][0]), degree * (bz[i + 1][1] - bz[i][1])) for i in range(degree)]
  26. def bz_arclength(bz, n = 10):
  27. # We're just going to integrate |z'| over the parameter [0..1].
  28. # The integration algorithm here is eqn 4.1.14 from NRC2, and is
  29. # chosen for simplicity. Likely adaptive and/or higher-order
  30. # algorithms would be better, but this should be good enough.
  31. # Convergence should be quartic in n.
  32. wtarr = (3./8, 7./6, 23./24)
  33. dt = 1./n
  34. s = 0
  35. dbz = bz_deriv(bz)
  36. for i in range(0, n + 1):
  37. if i < 3:
  38. wt = wtarr[i]
  39. elif i > n - 3:
  40. wt = wtarr[n - i]
  41. else:
  42. wt = 1.
  43. dx, dy = bz_eval(dbz, i * dt)
  44. ds = hypot(dx, dy)
  45. s += wt * ds
  46. return s * dt
  47. # One step of 4th-order Runge-Kutta numerical integration - update y in place
  48. def rk4(y, dydx, x, h, derivs):
  49. hh = h * .5
  50. h6 = h * (1./6)
  51. xh = x + hh
  52. yt = []
  53. for i in range(len(y)):
  54. yt.append(y[i] + hh * dydx[i])
  55. dyt = derivs(xh, yt)
  56. for i in range(len(y)):
  57. yt[i] = y[i] + hh * dyt[i]
  58. dym = derivs(xh, yt)
  59. for i in range(len(y)):
  60. yt[i] = y[i] + h * dym[i]
  61. dym[i] += dyt[i]
  62. dyt = derivs(x + h, yt)
  63. for i in range(len(y)):
  64. y[i] += h6 * (dydx[i] + dyt[i] + 2 * dym[i])
  65. def bz_arclength_rk4(bz, n = 10):
  66. dbz = bz_deriv(bz)
  67. def arclength_deriv(x, ys):
  68. dx, dy = bz_eval(dbz, x)
  69. return [hypot(dx, dy)]
  70. dt = 1./n
  71. t = 0
  72. ys = [0]
  73. for i in range(n):
  74. dydx = arclength_deriv(t, ys)
  75. rk4(ys, dydx, t, dt, arclength_deriv)
  76. t += dt
  77. return ys[0]
  78. # z0 and z1 are start and end points, resp.
  79. # th0 and th1 are the initial and final tangents, measured in the
  80. # direction of the curve.
  81. # aab is a/(a + b), where a and b are the lengths of the bezier "arms"
  82. def fit_cubic_arclen(z0, z1, arclen, th0, th1, aab):
  83. chord = hypot(z1[0] - z0[0], z1[1] - z0[1])
  84. cth0, sth0 = cos(th0), sin(th0)
  85. cth1, sth1 = -cos(th1), -sin(th1)
  86. armlen = .66667 * chord
  87. darmlen = 1e-6 * armlen
  88. for i in range(10):
  89. a = armlen * aab
  90. b = armlen - a
  91. bz = [z0, (z0[0] + cth0 * a, z0[1] + sth0 * a),
  92. (z1[0] + cth1 * b, z1[1] + sth1 * b), z1]
  93. actual_s = bz_arclength_rk4(bz)
  94. if (abs(arclen - actual_s) < 1e-12):
  95. break
  96. a = (armlen + darmlen) * aab
  97. b = (armlen + darmlen) - a
  98. bz = [z0, (z0[0] + cth0 * a, z0[1] + sth0 * a),
  99. (z1[0] + cth1 * b, z1[1] + sth1 * b), z1]
  100. actual_s2 = bz_arclength_rk4(bz)
  101. ds = (actual_s2 - actual_s) / darmlen
  102. #print '% armlen = ', armlen
  103. if ds == 0:
  104. break
  105. armlen += (arclen - actual_s) / ds
  106. a = armlen * aab
  107. b = armlen - a
  108. bz = [z0, (z0[0] + cth0 * a, z0[1] + sth0 * a),
  109. (z1[0] + cth1 * b, z1[1] + sth1 * b), z1]
  110. return bz
  111. def mod_2pi(th):
  112. u = th / (2 * pi)
  113. return 2 * pi * (u - floor(u + 0.5))
  114. def measure_bz(bz, arclen, th_fn, n = 1000):
  115. dt = 1./n
  116. dbz = bz_deriv(bz)
  117. s = 0
  118. score = 0
  119. for i in range(n):
  120. dx, dy = bz_eval(dbz, (i + .5) * dt)
  121. ds = dt * hypot(dx, dy)
  122. s += ds
  123. score += ds * (mod_2pi(atan2(dy, dx) - th_fn(s)) ** 2)
  124. return score
  125. def measure_bz_rk4(bz, arclen, th_fn, n = 10):
  126. dbz = bz_deriv(bz)
  127. def measure_derivs(x, ys):
  128. dx, dy = bz_eval(dbz, x)
  129. ds = hypot(dx, dy)
  130. s = ys[0]
  131. dscore = ds * (mod_2pi(atan2(dy, dx) - th_fn(s)) ** 2)
  132. return [ds, dscore]
  133. dt = 1./n
  134. t = 0
  135. ys = [0, 0]
  136. for i in range(n):
  137. dydx = measure_derivs(t, ys)
  138. rk4(ys, dydx, t, dt, measure_derivs)
  139. t += dt
  140. return ys[1]
  141. # th_fn() is a function that takes an arclength from the start point, and
  142. # returns an angle - thus th_fn(0) and th_fn(arclen) are the initial and
  143. # final tangents.
  144. # z0, z1, and arclen are as fit_cubic_arclen
  145. def fit_cubic(z0, z1, arclen, th_fn, fast = 1):
  146. chord = hypot(z1[0] - z0[0], z1[1] - z0[1])
  147. if (arclen < 1.000001 * chord):
  148. return [z0, z1], 0
  149. th0 = th_fn(0)
  150. th1 = th_fn(arclen)
  151. imax = 4
  152. jmax = 10
  153. aabmin = 0
  154. aabmax = 1.
  155. if fast:
  156. imax = 1
  157. jmax = 0
  158. for i in range(imax):
  159. for j in range(jmax + 1):
  160. if jmax == 0:
  161. aab = 0.5 * (aabmin + aabmax)
  162. else:
  163. aab = aabmin + (aabmax - aabmin) * j / jmax
  164. bz = fit_cubic_arclen(z0, z1, arclen, th0, th1, aab)
  165. score = measure_bz_rk4(bz, arclen, th_fn)
  166. print '% aab =', aab, 'score =', score
  167. sys.stdout.flush()
  168. if j == 0 or score < best_score:
  169. best_score = score
  170. best_aab = aab
  171. best_bz = bz
  172. daab = .06 * (aabmax - aabmin)
  173. aabmin = max(0, best_aab - daab)
  174. aabmax = min(1, best_aab + daab)
  175. print '%--- best_aab =', best_aab
  176. return best_bz, best_score
  177. def plot_prolog():
  178. print '%!PS'
  179. print '/m { moveto } bind def'
  180. print '/l { lineto } bind def'
  181. print '/c { curveto } bind def'
  182. print '/z { closepath } bind def'
  183. def plot_bz(bz, z0, scale, do_moveto = True):
  184. x0, y0 = z0
  185. if do_moveto:
  186. print bz[0][0] * scale + x0, bz[0][1] * scale + y0, 'm'
  187. if len(bz) == 4:
  188. x1, y1 = bz[1][0] * scale + x0, bz[1][1] * scale + y0
  189. x2, y2 = bz[2][0] * scale + x0, bz[2][1] * scale + y0
  190. x3, y3 = bz[3][0] * scale + x0, bz[3][1] * scale + y0
  191. print x1, y1, x2, y2, x3, y3, 'c'
  192. elif len(bz) == 2:
  193. print bz[1][0] * scale + x0, bz[1][1] * scale + y0, 'l'
  194. def test_bz_arclength():
  195. bz = [(0, 0), (.5, 0), (1, 0.5), (1, 1)]
  196. ans = bz_arclength_rk4(bz, 2048)
  197. last = 1
  198. lastrk = 1
  199. for i in range(3, 11):
  200. n = 1 << i
  201. err = bz_arclength(bz, n) - ans
  202. err_rk = bz_arclength_rk4(bz, n) - ans
  203. print n, err, last / err, err_rk, lastrk / err_rk
  204. last = err
  205. lastrk = err_rk
  206. def test_fit_cubic_arclen():
  207. th = pi / 4
  208. arclen = th / sin(th)
  209. bz = fit_cubic_arclen((0, 0), (1, 0), arclen, th, th, .5)
  210. print '%', bz
  211. plot_bz(bz, (100, 400), 500)
  212. print 'stroke'
  213. print 'showpage'
  214. # -- cornu fitting
  215. import cornu
  216. def cornu_to_cubic(t0, t1):
  217. def th_fn(s):
  218. return (s + t0) ** 2
  219. y0, x0 = cornu.eval_cornu(t0)
  220. y1, x1 = cornu.eval_cornu(t1)
  221. bz, score = fit_cubic((x0, y0), (x1, y1), t1 - t0, th_fn, 0)
  222. return bz, score
  223. def test_draw_cornu():
  224. plot_prolog()
  225. thresh = 1e-6
  226. print '/ss 1.5 def'
  227. print '/circle { ss 0 moveto currentpoint exch ss sub exch ss 0 360 arc } bind def'
  228. s0 = 0
  229. imax = 200
  230. x0, y0, scale = 36, 100, 500
  231. bzs = []
  232. for i in range(1, imax):
  233. s = sqrt(i * .1)
  234. bz, score = cornu_to_cubic(s0, s)
  235. if score > (s - s0) * thresh or i == imax - 1:
  236. plot_bz(bz, (x0, y0), scale, s0 == 0)
  237. bzs.append(bz)
  238. s0 = s
  239. print 'stroke'
  240. for i in range(len(bzs)):
  241. bz = bzs[i]
  242. bx0, by0 = x0 + bz[0][0] * scale, y0 + bz[0][1] * scale
  243. bx1, by1 = x0 + bz[1][0] * scale, y0 + bz[1][1] * scale
  244. bx2, by2 = x0 + bz[2][0] * scale, y0 + bz[2][1] * scale
  245. bx3, by3 = x0 + bz[3][0] * scale, y0 + bz[3][1] * scale
  246. print 'gsave 0 0 1 setrgbcolor .5 setlinewidth'
  247. print bx0, by0, 'moveto', bx1, by1, 'lineto stroke'
  248. print bx2, by2, 'moveto', bx3, by3, 'lineto stroke'
  249. print 'grestore'
  250. print 'gsave', bx0, by0, 'translate circle fill grestore'
  251. print 'gsave', bx1, by1, 'translate .5 dup scale circle fill grestore'
  252. print 'gsave', bx2, by2, 'translate .5 dup scale circle fill grestore'
  253. print 'gsave', bx3, by3, 'translate circle fill grestore'
  254. # -- fitting of piecewise cornu curves
  255. def pcorn_segment_to_bzs_optim_inner(curve, s0, s1, thresh, nmax = None):
  256. result = []
  257. if s0 == s1: return [], 0
  258. while s0 < s1:
  259. def th_fn_inner(s):
  260. if s > s1: s = s1
  261. return curve.th(s0 + s, s == 0)
  262. z0 = curve.xy(s0)
  263. z1 = curve.xy(s1)
  264. bz, score = fit_cubic(z0, z1, s1 - s0, th_fn_inner, 0)
  265. if score < thresh or nmax != None and len(result) == nmax - 1:
  266. result.append(bz)
  267. break
  268. r = s1
  269. l = s0 + .001 * (s1 - s0)
  270. for i in range(10):
  271. smid = 0.5 * (l + r)
  272. zmid = curve.xy(smid)
  273. bz, score = fit_cubic(z0, zmid, smid - s0, th_fn_inner, 0)
  274. if score > thresh:
  275. r = smid
  276. else:
  277. l = smid
  278. print '% s0=', s0, 'smid=', smid, 'actual score =', score
  279. result.append(bz)
  280. s0 = smid
  281. print '% last actual score=', score
  282. return result, score
  283. def pcorn_segment_to_bzs_optim(curve, s0, s1, thresh, optim):
  284. result, score = pcorn_segment_to_bzs_optim_inner(curve, s0, s1, thresh)
  285. bresult, bscore = result, score
  286. if len(result) > 1 and optim > 2:
  287. nmax = len(result)
  288. gamma = 1./6
  289. l = score
  290. r = thresh
  291. for i in range(5):
  292. tmid = (0.5 * (l ** gamma + r ** gamma)) ** (1/gamma)
  293. result, score = pcorn_segment_to_bzs_optim_inner(curve, s0, s1, tmid, nmax)
  294. if score < tmid:
  295. l = max(l, score)
  296. r = tmid
  297. else:
  298. l = tmid
  299. r = min(r, score)
  300. if max(score, tmid) < bscore:
  301. bresult, bscore = result, max(score, tmid)
  302. return result
  303. def pcorn_segment_to_bzs(curve, s0, s1, optim = 0, thresh = 1e-3):
  304. if optim >= 2:
  305. return pcorn_segment_to_bzs_optim(curve, s0, s1, thresh, optim)
  306. z0 = curve.xy(s0)
  307. z1 = curve.xy(s1)
  308. fast = (optim == 0)
  309. def th_fn(s):
  310. return curve.th(s0 + s, s == 0)
  311. bz, score = fit_cubic(z0, z1, s1 - s0, th_fn, fast)
  312. if score < thresh:
  313. return [bz]
  314. else:
  315. smid = 0.5 * (s0 + s1)
  316. result = pcorn_segment_to_bzs(curve, s0, smid, optim, thresh)
  317. result.extend(pcorn_segment_to_bzs(curve, smid, s1, optim, thresh))
  318. return result
  319. def pcorn_curve_to_bzs(curve, optim = 3, thresh = 1e-3):
  320. result = []
  321. extrema = curve.find_extrema()
  322. extrema.extend(curve.find_breaks())
  323. extrema.sort()
  324. print '%', extrema
  325. for i in range(len(extrema)):
  326. s0 = extrema[i]
  327. if i == len(extrema) - 1:
  328. s1 = extrema[0] + curve.arclen
  329. else:
  330. s1 = extrema[i + 1]
  331. result.extend(pcorn_segment_to_bzs(curve, s0, s1, optim, thresh))
  332. return result
  333. import struct
  334. def fit_cubic_arclen_forplot(z0, z1, arclen, th0, th1, aab):
  335. chord = hypot(z1[0] - z0[0], z1[1] - z0[1])
  336. cth0, sth0 = cos(th0), sin(th0)
  337. cth1, sth1 = -cos(th1), -sin(th1)
  338. armlen = .66667 * chord
  339. darmlen = 1e-6 * armlen
  340. for i in range(10):
  341. a = armlen * aab
  342. b = armlen - a
  343. bz = [z0, (z0[0] + cth0 * a, z0[1] + sth0 * a),
  344. (z1[0] + cth1 * b, z1[1] + sth1 * b), z1]
  345. actual_s = bz_arclength_rk4(bz)
  346. if (abs(arclen - actual_s) < 1e-12):
  347. break
  348. a = (armlen + darmlen) * aab
  349. b = (armlen + darmlen) - a
  350. bz = [z0, (z0[0] + cth0 * a, z0[1] + sth0 * a),
  351. (z1[0] + cth1 * b, z1[1] + sth1 * b), z1]
  352. actual_s2 = bz_arclength_rk4(bz)
  353. ds = (actual_s2 - actual_s) / darmlen
  354. #print '% armlen = ', armlen
  355. armlen += (arclen - actual_s) / ds
  356. a = armlen * aab
  357. b = armlen - a
  358. bz = [z0, (z0[0] + cth0 * a, z0[1] + sth0 * a),
  359. (z1[0] + cth1 * b, z1[1] + sth1 * b), z1]
  360. return bz, a, b
  361. def plot_errors_2d(t0, t1, as_ppm):
  362. xs = 1024
  363. ys = 1024
  364. if as_ppm:
  365. print 'P6'
  366. print xs, ys
  367. print 255
  368. def th_fn(s):
  369. return (s + t0) ** 2
  370. y0, x0 = cornu.eval_cornu(t0)
  371. y1, x1 = cornu.eval_cornu(t1)
  372. z0 = (x0, y0)
  373. z1 = (x1, y1)
  374. chord = hypot(y1 - y0, x1 - x0)
  375. arclen = t1 - t0
  376. th0 = th_fn(0)
  377. th1 = th_fn(arclen)
  378. cth0, sth0 = cos(th0), sin(th0)
  379. cth1, sth1 = -cos(th1), -sin(th1)
  380. for y in range(ys):
  381. b = .8 * chord * (ys - y - 1) / ys
  382. for x in range(xs):
  383. a = .8 * chord * x / xs
  384. bz = [z0, (z0[0] + cth0 * a, z0[1] + sth0 * a),
  385. (z1[0] + cth1 * b, z1[1] + sth1 * b), z1]
  386. s_bz = bz_arclength(bz, 10)
  387. def th_fn_scaled(s):
  388. return (s * arclen / s_bz + t0) ** 2
  389. score = measure_bz_rk4(bz, arclen, th_fn_scaled, 10)
  390. if as_ppm:
  391. ls = -log(score)
  392. color_th = ls
  393. darkstep = 0
  394. if s_bz > arclen:
  395. g0 = 128 - darkstep
  396. else:
  397. g0 = 128 + darkstep
  398. sc = 127 - darkstep
  399. rr = g0 + sc * cos(color_th)
  400. gg = g0 + sc * cos(color_th + 2 * pi / 3)
  401. bb = g0 + sc * cos(color_th - 2 * pi / 3)
  402. sys.stdout.write(struct.pack('3B', rr, gg, bb))
  403. else:
  404. print a, b, score
  405. if not as_ppm:
  406. print
  407. def plot_arclen(t0, t1):
  408. def th_fn(s):
  409. return (s + t0) ** 2
  410. y0, x0 = cornu.eval_cornu(t0)
  411. y1, x1 = cornu.eval_cornu(t1)
  412. z0 = (x0, y0)
  413. z1 = (x1, y1)
  414. chord = hypot(y1 - y0, x1 - x0)
  415. arclen = t1 - t0
  416. th0 = th_fn(0)
  417. th1 = th_fn(arclen)
  418. for i in range(101):
  419. aab = i * .01
  420. bz, a, b = fit_cubic_arclen_forplot(z0, z1, arclen, th0, th1, aab)
  421. print a, b
  422. if __name__ == '__main__':
  423. #test_bz_arclength()
  424. test_draw_cornu()
  425. #run_one_cornu_seg()
  426. #plot_errors_2d(.5, 1.0, False)
  427. #plot_arclen(.5, 1.0)