Python で自由曲線を描く 【HTML & SVG】

Python

Python で自由曲線を描画するスクリプトについてまとめておきます。ベジェ曲線を繋げてベクター画像で描きます。
具体的には、テキストファイルで複数の点の座標を指定しておくと、それらの点を通る滑らかな曲線を生成し、ベクター画像(HTML 形式、および、SVG 形式)で出力します。線画でのお絵描きが可能です。

以下の環境で動作確認をしています。
環境: Windows パソコン、Python 3.X (+ Anaconda)

背景 ~ 直線や曲線を自由に使って、お絵描きをしたい!

これまで Python を使って、グラフや図形、表を生成するプログラムをまとめてきています。
どのデバイスでも表示ができるよう、HTML 形式など汎用フォーマットでの出力が可能です。
主なものを以下の関連リンクに挙げておきますが、電気回路の回路図のように、多少、凝った図形であっても、描画が可能です。

これらの図形は、比較的単純な、直線や円、文字を組み合わせたものでした。しかしながら、任意の曲線については扱っていませんでした。
そこで、フリーハンドで描くような曲線についても、作り方を Python のスクリプトでまとめ、公開しておきます。

具体的には、テキストファイルに半角スペース区切りで XY の座標を記載しておくと、その点を滑らかにつないだ曲線を生成し、出力します。

自由曲線といえば、ベジェ曲線やスプライン曲線などがよく知られています。どれを使ってもよいのですが、Chromium や Microsoft Edge などの最近のブラウザでは、SVG形式がサポートされています。そこで、SVG で使用できる3次のベジェ曲線 Bezier Curve を使うことにします。
ベクター画像ですので、拡大しても図形の崩れが少なく、ロゴなどの図形の作成が可能になります。
また、別途、点の並びを数値データで作っておけば、サインカーブ、コサインカーブ、双曲線など、任意の自作の曲線を描くことも可能です。

設定方法

① 以下を参考に、パソコンにフォルダを作成してください。
例: “c:\user\svg_drawing1”
② ①のフォルダの中に、”svg_drawing.py” というファイル名でテキストファイルを作成してください。
このテキストファイルに、下記のサンプルスクリプトをコピー&ペーストして保存してください。
③ さらに、①のフォルダの中に “svg1” というフォルダを作ってください。
例: “c:\user\svg_drawing1\svg1
このフォルダに、座標データを記載したテキストファイル(描画データ)を入れておくと、描画が可能です。
具体的には、同フォルダ内に図形を記載した HTML を出力します。

使い方 ~ ベジェ曲線で描いてみる!

④ ③のフォルダ “svg1” 内に、座標データを記入したテキストファイルを作成してください。
最初は、下記のサンプルを使ってください。座標データは、XY 座標を半角スペース区切りで記載します。
⑤ Python のスクリプト(②)を実行してください。
→ フォルダ “svg1″(③)内のテキストごとに、HTML ファイルが生成されます。

※ 下記のサンプルをテキストファイルで保存して、スクリプトを実行してみると、要領がわかるかと思います。

うまく動いたら

描画方法の詳細

うまく動いたら、テキストファイル(描画データ)を追加して、図形を描画してみてください。

描画方法の詳細は以下の通りです。
・ 曲線が通る点 (x1, y1)、(x2, y2)、… 、(xn, yn) を、半角スペース区切り1行で記載してください。
例えば、(x1, y1) = (100, 100)、(x2, y2) = (200, 200)、(x3, y3) = (300, 100) の3点を通る曲線を描画したい場合は、以下のように記載します。
例: 100 100 200 200 300 100
曲線の具体例として、下記のサンプル “draw01.txt” (波線)を参照してください。
・ 座標系の定義は、モニター座標と同じです。すなわち、ブラウザの画面左上を原点 (0, 0) とし、画面右方向を +X、画面下方向を +Y とします。
・ テキストファイルの1行が1本の曲線(直線)に対応します。2本目、3本目、…の曲線を描画する場合は、テキストファイルの2行目、3行目、…に、上と同じ要領で、座標を記載します。
・ 直線を描画する場合は、1行に2点の座標のみを指定してください。
例: 100 100 200 100
→ 2点(100, 100) 、(200, 100) を通る直線(線分)を描画します。
直線の描画例として、”draw04.txt” の星マークなどを参照してみてください。
・ 楕円のように、閉じた曲線を描画する場合は、最初の点の座標と、最後の点の座標が完全に一致するように記載してください。滑らかに閉じた曲線を描画します。
始点と終点が一致しない場合は、両点を端点とする曲線(直線を含む)となります。
具体例は、draw02.txt、draw04.txt を参照してください。最初の2つの数値と、最後の2つの数値がまったく同一となるよう記載することで、閉じた曲線を描画します。

出力フォーマットについて ~ HTML、SVG 形式、PDF 形式での出力

・ スクリプトでは、デフォルトで HTML 形式で出力するようにしてあります。
SVG 形式で出力したい場合は、スクリプトの末尾のほうでコメントアウトしている “# ” の部分を外してください。
・ HTML や SVG 形式で出力している理由は特になく、汎用的だからという程度の理由です。
HTML や SVG で出力すれば、Windows、Linux、MacOS、スマートフォンの多くのブラウザが対応しています。出力結果をデフォルトのまま参照できるデバイスが多いといえます。また、SVG ファイルの読み込みが可能な描画ソフトでも読み込んで、応用が可能です。
・ また、jpg や png ではなく、ベクター画像を扱っていますので、解像度という点でベストといえると思います。
・ Microsoft Edge などのブラウザの機能を使って、PDF 形式での印刷を実行することで、生成した HTML ファイルを PDF ファイルに変換することができます。

参考:ベジェ曲線とは

定義のまとめ

ベジェ曲線の定義をまとめておきます。N次のベジェ曲線の定義は以下のとおりです。
$$B(t) = \sum_{i=0}^{N}{_{n} C_{i}} (1-t)^{N-i} t^{i} P_{i}$$

B(t)はベジェ曲線として描画される点(曲線)です。Piは制御点です。ともに、XY平面上の2次元のベクトルです。
t は媒介変数で、0から1までの値をとります。
\( {_{n}C_{i}}\) は二項係数で、n個の要素からi個を選ぶときの場合の数です。

Microsoft Edge などの最近のブラウザでは、3次のベジェ曲線が定義されています。
そこで、上の定義で N=3 とすると、つぎの式が得られます。
$$B(t) = (1-t)^{3} P_{0}+3(1-t)^{2} t P_{1}+3(1-t) t^{2} P_{2}+t^{3} P_{3}$$
媒介変数 t を t = 0, 1 とすると、ベジェ曲線上の点 B(t) は、点 P0、P3 に一致します。
一方、中間の2つの点 P1、P2 は、かならずしも、ベジェ曲線 B(t) 上にあるとは限りません。

ベジェ曲線を描く考え方、他

・ 上の定義に基づき、ベジェ曲線を描くものとします。
まず、端点の P0、P3 は、ユーザーが指定した連続する2点の座標とするのが自然です。するとつぎに、P1、P2 の2点をどう決めるかが問題となります。
下記のスクリプトは、ユーザが指定した点(端点)以外は、ベジェ曲線が滑らかにつながるよう Python のプログラム側で求めてしまえ、というアイディアに基づいて作成しています。
イラストレーターや描画ツールなどの書籍を見ると、ベジェ曲線を扱うにあたり制御点の設定などに習熟を必要としているものがあります。ソフトウェアで計算してしまえば習熟などは不要となり、ベジェ曲線をもっと簡単に扱えるようになる、ということです。
・ スクリプトでは、3次のベジェ曲線を複数つなげて使っています。端点となる P0、P3 は、ユーザーがテキストファイルで指定した点です。中間の2つの制御点 P1, P2 は、各端点 P0、P3 において、隣り合うベジェ曲線の一階微分が一致するよう Python のスクリプト側で座標を求めています。詳細は省略します。
・ スクリプトでは、ベジェ曲線の数値データの生成のところまで完了しています。したがって、出力部分を書き換えることで、jpg や png 形式での出力であっても、matplotlib での活用あっても、自由にアレンジできると思います。
・ ブラウザの SVG の仕様を確認すると、3次のベジェ曲線までが定義されています。3次のベジェ曲線は実質的に、3次の多項式となっています。すると、サインカーブや対数グラフは、仕様上、ベジェ曲線では直接描画できません。
しかしながら、サンプル(”draw05.txt”、サインカーブ)で示したように、曲線が経由する複数の点を指定することで、実質的に、サインカーブ、コサインカーブなどの正弦曲線、双曲線、対数曲線、指数関数など、任意の曲線を、滑らかな近似曲線で描画することが可能です。近似/描画の精度を上げたい場合は、点の数を細かく増やしていけばよいことになります。

まとめ

Python で自由曲線を描画するスクリプトについてまとめました。
ベクター画像での出力ですので、拡大しても画像の崩れがなく、任意の描画が可能となりました。

ネット検索をしたところ、ベジェ曲線などの自由曲線を使って任意の図形を描画する方法について解説されたサイトが見つかりませんでした。そこで、ポイントを Python のスクリプトの形でまとめました。

たかだか、100行少々のプログラミングをするだけで、線画を使ったデザインなどが可能になります。応用範囲が格段に広がります。
他にも、今回のベクター画像などを使って、任意の図形を描画するスクリプトを下記の関連リンクなどでまとめています。もし、興味があるようでしたら、参照してみてください。

関連リンク
・ ベクター形式でお絵描きをしてみる 【JavaScript】
・ ブラウザで動くお絵描きアプリ 【JavaScript】
・ Python で回路図を描画する 【HTML & SVG】
・ HTML の折れ線グラフを出力する 【Python】
・ 
フラクタル図形を描画してみる 【Python & tkinter】

描画サンプル

サンプル1:波 (draw01.txt) ~ 滑らかな曲線

100 100 200 200 300 100 400 200 500 100 600 200 

サンプル2:角が丸い四角 (draw02.txt)

100 100 300 100 300 200 100 200 100 100 

★ 長方形となるように4点を指定しています。4点の指定後、始点と完全に一致する5点目を加えています。始点と終点の座標 (100, 100) を完全に一致させることで、閉じた曲線を描画しています。

サンプル3:ふつうの星1 (draw03.txt)

200 100 258.778526529899 280.901698492516 
258.778526529899 280.901698492516 104.894346548868 169.098306168867 
104.894346548868 169.098306168867 295.105651795117 169.098301072174 
295.105651795117 169.098301072174 141.221477805606 280.901701642445 
141.221477805606 280.901701642445 200 100 

★ 上記の5行が、5本の直線に対応しています。なお、各点の数値は Excel で求めました。

サンプル4:角が丸い星2 (draw04.txt)

200 100 258.778526529899 280.901698492516 104.894346548868 169.098306168867 295.105651795117 169.098301072174 141.221477805606 280.901701642445 200 100 

★ 直線で描いた星マーク(サンプル3)の座標を1行にまとめることで、すべての点を通る1本の曲線で描画しています。

サンプル5:サインカーブ (draw05.txt)

180 265.797986226966 190 282.635182526505 200 300 210 317.364817473495 220 334.202013773034 230 349.999999226498 240 364.278760056384 250 376.604443355041 260 386.602539485281 270 393.969261365805 280 398.480774887631 290 400 300 398.480775818208 310 393.969263198683 320 386.60254216477 330 376.604446799726 340 364.2787641616 350 350.00000386751 360 334.202018808828 370 317.364822751059 380 300.000005358979 390 282.635187804069 400 265.797991262759 410 250.000005414514 420 235.721244048833 430 223.395560089645 440 213.397463194209 450 206.030740467075 460 201.519226042946 470 200 480 201.519223251215 490 206.030734968438 500 213.39745515574 510 223.395549755589 520 235.721231733184 530 249.999991491478 540 265.797976155379 550 282.635171971376 560 299.999989282041 570 317.364806918366 580 334.202003701447 590 349.999989944473 600 364.278751845951 610 376.604436465669 620 386.602534126301 630 393.969257700046 640 398.480773026476 650 399.999999999999 660 398.480777679361 670 393.96926686444 680 386.602547523749 690 376.604453689096 700 364.278772372032 710 350.000013149534 720 334.202028880414 730 317.364833306188 740 300.000016076938 750 282.635198359198 760 265.798001334346 770 250.000014696539 780 235.721252259266 790 223.395566979017 800 213.39746855319 810 206.030744132834 820 201.519227904102 830 200.000000000002 840 201.519221390063 850 206.030731302682 860 213.397449796762 870 223.395542866219 880 235.721223522752 890 249.999982209454 900 265.797966083793 910 282.635161416248 920 299.999978564083 930 317.364796363237 940 334.20199362986 
180 300 940 300 
200 180 200 420 

サンプル6:モニター (draw06.txt)

44 17 344 9 
344 9 386 188 
386 188 64 235 
64 235 44 17 
49 20 342 13 
342 13 378 169 
378 169 66 212 
66 212 49 20 
68 217 381 173 
214 214 257 292 
257 292 262 297 266 300 272 298 283 295 285 290 283 284 
283 284 238 210 
150 223 114 250 
114 250 113 256 115 259 125 255 125 252 
125 252 167 221 
262 206 279 216 
279 216 282 222 278 224 270 224 
270 224 250 208 
263 295 274 289 283 289 
237 247 257 283 
267 213 277 219 
41 20 61 234 
41 20 44 17 
61 234 64 235 

サンプル7:車 (draw07.txt)

13 311 97 273 114 270 120 268 126 266 194 243 199 242 293 243 336 246 367 252 428 262 
428 262 435 270 
435 270 435 289 
435 289 422 301 387 320 
392 317 394 315 400 303 395 293 386 284 376 280 365 279 306 303 305 308 308 317 314 330 
327 330 131 330 
143 330 141 312 130 294 109 283 95 283 79 289 68 300 64 315 66 330 
73 330 23 330 
23 330 22 322 
22 322 13 311
118 269 120 273 156 309
156 309 230 309 
230 309 249 285 259 266 260 242 
128 268 198 245 256 244 
256 244 254 263 
254 263 203 268 128 268 
267 269 295 267 324 263 358 260 382 261 412 265 428 268 
132.020216579462 296.037963897979 133.949547929605 300.371311892829 135.4834629872 306.095958470268 136 312 131.444863880509 328.999999737009 119.000000525981 341.444863424995 102.000000911026 346 85.0000010519628 341.444864336022 72.5551370305179 329.000001314953 68.0000000000001 312.000001822053 68.5165360636112 306.095963853384 70.0504502008583 300.371317029338 73.8127213217946 292.987443077871 
81 312.000001125386 83.8134658640518 301.500001137048 91.4999987005166 293.813467270784 101.999998311922 291 112.499998375646 293.813465582705 120.18653244787 301.49999821321 123 312 120.186533573255 322.499999837565 112.500000324871 330.186533291909 102.000000562693 333 91.5000006497417 330.186533854602 83.8134669894375 322.500000812177 81 312.000001125386 
369.36901819282 279.185533978756 371.999997370093 280.555134752952 384.444862058456 292.999997107102 389 310 384.444863880509 326.999999737009 372.000000525981 339.444863424995 355.000000911026 344 338.000001051963 339.444864336022 325.555137030518 327.000001314953 321 310.000001822053 325.555135208465 293.000001840935 327.148829245876 290.498402946819 328.954487502497 288.145222976603 
354.999998311922 289 365.499998375646 291.813465582705 373.18653244787 299.49999821321 376 310 373.186533573255 320.499999837565 365.500000324871 328.186533291909 355.000000562693 331 344.500000649742 328.186533854602 336.813466989437 320.500000812177 334 310.000001125386 336.813465864052 299.500001137048 344.499998700517 291.813467270784 354.999998311922 289 

サンプルスクリプト svg_drawing.py

import os 
import glob 

def read1( file1 ): 
    with open( file1, 'r', encoding='utf-8' ) as f1: 
        str1 = f1.read()
    return str1 

def write1( file1, str1 ): 
    with open( file1, 'w', encoding='utf-8' ) as f1: 
        f1.write( str1 ) 
    return 0 

def get_a1( file1 ):
    a1 = read1( file1 ).strip().replace( "\n\n", "\n" ).split( "\n" ) 
    for i1 in range( len( a1 ) ): 
        a1[i1] = a1[i1].split( " " ) 
    return a1 

def get_coordinates1( a1 ): 
    a2 = [] 
    for i1 in range( int( len( a1 )/2 ) ): 
        a2.append( [ float( a1[2*i1] ), float( a1[2*i1+1] ) ] )  
    return a2 

def generate_html1( str1 ): 
    str2 = ''' 
<html lang="ja">
<body>
{str3} 
</body> 
</html> 
'''.format( str3=str1 ) 
    return str2 

def generate_svg1( str1 ): 
    str2 = ''' 
<svg version="1.1" id="page1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 1536" width="1536" height="1024" style="background:#ffffff" > 
{str3} 
</svg> 
'''.format( str3=str1 ) 
    return str2  

def svg_bezier1( a1 ): 
    fill1 = "#000000"
    fill2 = "transparent" 
    str2 = "M " 
    str2 = str2 + str( a1[0][0] ) + " " + str( a1[0][1] ) + " C " 
    str2 = str2 + str( a1[1][0] ) + " " + str( a1[1][1] ) + ", " 
    str2 = str2 + str( a1[2][0] ) + " " + str( a1[2][1] ) + ", " 
    str2 = str2 + str( a1[3][0] ) + " " + str( a1[3][1] ) 
    str3 = '  <path d="' + str2 + '" stroke="' + fill1 + '" fill="' + fill2 + '" /> ' + "\n" 
    return str3     

def split_points1( v1, v4 ): 
    v2 = [ ( 2.0*float( v1[0] ) + 1.0*float( v4[0] ) )/3.0, ( 2.0*float( v1[1] ) + 1.0*float( v4[1] ) )/3.0 ] 
    v3 = [ ( 1.0*float( v1[0] ) + 2.0*float( v4[0] ) )/3.0, ( 1.0*float( v1[1] ) + 2.0*float( v4[1] ) )/3.0 ] 
    return v2, v3 

def get_unit1( v1 ): 
    len1 = ( v1[0]*v1[0] + v1[1]*v1[1] ) ** 0.5 
    return [ v1[0]/len1, v1[1]/len1 ], len1 

def smoothing_points1( a1, flag1 ): 
    a4 = [] 
    if flag1 > 0: 
        a3 = a1[ len(a1)-1 ] 
        i1_max = len( a1 ) 
    else: 
        a3 = a1[0] 
        i1_max = len( a1 ) -1 
    for i1 in range( i1_max ): 
        a2 = a3 
        if flag1 > 0: 
            a3 = a1[i1] 
        else: 
            a3 = a1[i1+1] 
        v1, len1 = get_unit1( [ a2[3][0] - a2[2][0], a2[3][1] - a2[2][1] ] ) 
        v2, len2 = get_unit1( [ a3[1][0] - a3[0][0], a3[1][1] - a3[0][1] ] ) 
        v3, len3 = get_unit1( [ v1[0]+v2[0], v1[1]+v2[1] ] ) 
        a2[2][0] = a2[3][0] - len1 * v3[0] 
        a2[2][1] = a2[3][1] - len1 * v3[1] 
        a3[1][0] = a3[0][0] + len2 * v3[0] 
        a3[1][1] = a3[0][1] + len2 * v3[1] 
        if flag1 > 0: 
            a4.append( a3 ) 
        else: 
            a4.append( a2 ) 
    a4.append( a3 ) 
    return a4 

def get_bezier_points1( a1 ): 
    if a1[0] == a1[ len( a1 )-1 ]: 
        flag1 = 1 
    else: 
        flag1 = -1 
    a2 = [] 
    for i1 in range( len( a1 )-1 ): 
        v1 = a1[i1  ] 
        v4 = a1[i1+1] 
        v2, v3 = split_points1( v1, v4 ) 
        a2.append( [v1, v2, v3, v4] ) 
    a3 = smoothing_points1( a2, flag1 ) 
    return a3 

def svg_beziers1( a1 ): 
    a2 = get_bezier_points1( a1 ) 
    str1 = "" 
    for i1 in range( len( a2 ) ):  
        str1 = str1 + svg_bezier1( a2[i1] ) 
    return str1 

path1 = os.path.dirname(__file__) + "/" 
a1 = glob.glob( path1 + "svg1/*.txt" ) 

for i1 in range( len( a1 ) ): 
    file1 = a1[i1] 
    print( file1 ) 
    a2 = get_a1( file1 ) 
    str1 = "" 
    for i2 in range( len( a2 ) ): 
        a3 = get_coordinates1( a2[i2] ) 
        str1 = str1 + svg_beziers1( a3 ) 
    str2 = generate_svg1( str1 ) 
#   file2 = file1.replace( ".txt", ".svg" ) 
#   write1( file2, str2 ) 
    str3 = generate_html1( str2 ) 
    file2 = file1.replace( ".txt", ".html" ) 
    write1( file2, str3 ) 

タイトルとURLをコピーしました