fullseye

写真を 1 本の線にする(点描 → 巡回路 → 回る円)

できること

写真の濃淡を、紙から鉛筆を離さずに引ける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 での実測):

What it does

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 で、最適解ではありません(下界との比で質を言うのはそのため)。色は扱いません。★線画には向きません —— 重心ボロノイは必ず画面全体を埋めるので、暗い画素が数 % しかない墨の線画では点の大半が白地に配られます。線画は細線化して線そのものを辿るべきで、それは別の道具です。

最初の 1 本

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"])

裏づけ