写真の濃淡を、紙から鉛筆を離さずに引ける1 本の閉じた線に変えます。濃いところで線が密になるので、離れて見ると元の絵の階調が戻ります。巡回路は閉じているので、そのまま複素フーリエ級数に載り、回る円の連鎖として描き直せます。出口は既存の G-code op なので、ペンプロッタで実際に引けます。自分の写真で走らせる口も付いています::
py -3.11 examples/poc_one_stroke_epicycles.py --image my_photo.jpg \
--points 12000 --out out/mine
★「絵が似ている」を人の目に任せません。 各段が何を保って何を捨てたかを数で返します(北斎「神奈川沖浪裏」202×300、点 9,000 での実測):
インク率 ≈ 線長 × ペン幅 / 面積。解いた 0.903 px でインク率は目標の 0.891 倍 —— 不足の 11 % が線の重なりの量そのものです(合併は和より小さいので、実測は必ず予言以下になります)。| ★★円の本数は描く前に決められます: 次数 K で打ち切った誤差はパーセバルにより「 | k | > K の係数の二乗和」に厳密に等しいので、K=16 で 94.6 %、K=64 で 99.2 %、K=600 で 99.97 % と先に言えます(予言と実測の差は機械精度)。 |
Turn the tone of a picture into a single closed line a pen could draw without lifting: points are placed by darkness (weighted Lloyd), ordered into a closed tour, resampled at equal arc length, and rewritten as a chain of rotating circles via the complex Fourier series. Nothing is judged by eye. The stipple is checked against a ramp (correlation 0.9946) with a flat image as the control — and the exponent is measured too, because a centroidal Voronoi tessellation puts density at sqrt(rho), not rho (Gersho): 0.61 measured, 0.77 with the weight squared. The tour is checked against the minimum spanning tree, which no closed tour can beat (1.146, against 41.4 for coordinate order); the tone is measured (0.984, against +0.013 for random points); the pen width that reproduces the mean tone is solved in closed form and falls 11 % short, which is the stroke overlap; the resampling is checked against Nyquist (the error falls as 1/N: 6.5, 3.3, 1.6 % of the stroke length); and Parseval turns the number of circles into a prediction made before the drawing exists.
向く: ペンプロッタ・レーザー・刺繍のように線でしか描けない機械への変換。形状記述(閉じた輪郭を少ない係数で表す)。様式化の効きを数で比べたいとき。
向かない: 点描は画素数 × 点数で効くので大きな画像は遅い(実測: 202×300・9,000 点で点描 101 秒)。巡回路は最近傍 + 2-opt で、最適解ではありません(下界との比で質を言うのはそのため)。色は扱いません。★線画には向きません —— 重心ボロノイは必ず画面全体を埋めるので、暗い画素が数 % しかない墨の線画では点の大半が白地に配られます。線画は細線化して線そのものを辿るべきで、それは別の道具です。
import fullseye as fs
img = fs.read_image("photo.png")
pts = fs.ledger.stipple_points_from_image(img, 9000, gamma=1.6) # 濃いところに点
tour = fs.ledger.stroke_tour_closed(pts) # 1 本の閉じた線
err = fs.ledger.stroke_tone_error(img, tour) # 何を保ったかを数で
print(err["corr"], err["length_px"])
stipple_points_from_image / stipple_energy / stroke_tour_closed / mst_length / stroke_resample_closed / stroke_tone_error / contour_fourier_complex / contour_epicycle_chain / contour_fourier_truncation_energypoc_one_stroke_epicyclesisPublicDomain: true(CC0)で公開している版(object 45434、画像 DP130155)を輝度化して 202×300 に縮めたもの。