OpenCVでベジェ曲線で遊ぶ　～異空間を操る～

# はじめに
葉っぱや唇のようになだらかな山の曲線を描きたいと思った。ガウス関数（正規分布のアレ）も悪くはないが、両端をまっすぐにしたい。そう考えて思いついたのがベジェ曲線だった。

# ベジェ曲線
## matplotlibでグラフを描く
ベジェ曲線そのものを勉強したいわけではないので、基本的な部分は先人のプログラムを流用させていただく。
　　[Pythonでベジエ曲線を描く（TadaoYamaokaの日記）](https://tadaoyamaoka.hatenablog.com/entry/2017/03/07/221018)

```python:bezier_matplotlib.py
#import scipy.misc as scm       # 元サイトのこのインポートは古い
from scipy.special import comb  # 現在はこのcombを使う
import numpy as np
import matplotlib.pyplot as plt

def bernstein(n, i, t):
    return comb(n, i) * t**i * (1 - t)**(n-i)  # 元サイトから修正　scm.comb -> comb

def bezier(n, t, q):
    p = np.zeros(2)
    for i in range(n + 1):
        p += bernstein(n, i, t) * q[i]
    return p

# 当然、制御点は改変している
q = np.array([[0, 0], [3, 0], [4, 1],[5, 0], [8, 0]], dtype=np.float)

list = []
for t in np.linspace(0, 1, 100):
    list.append(bezier(len(q)-1, t, q))  # 元サイトから改良　3を決め打ち -> 制御点の数に従う
P = np.array(list)

plt.plot(P.T[0], P.T[1])
plt.plot(q.T[0], q.T[1], '--o')
plt.show()
```

![bezier_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/b55fa08b-9a79-8b4e-fd2a-9e9357e9e7a1.png)
両端が水平の山形を作ることができた。線分の両端（0番目と4番目）が決まったら1番目と3番目の制御点を線分上で適当に決めてやれば、自由なのはただ一つ2番目だけとなる。

## OpenCVで自由に描く
GUIでxy平面上に自由にこの曲線を描けるようにする。
せっかく細かい計算をしているのにOpenCVにすると整数上の話になってしまうのが残念なところではあるが。
マウスイベントについては[こちら](https://qiita.com/mo256man/items/3a9e67cb5b9fe32839f0)を参照のこと。


```Python:bezier_opencv.py
from scipy.special import comb
import numpy as np
import cv2

def bernstein(n, i, t):
    return comb(n, i) * t**i * (1 - t)**(n-i)

def bezier(n, t, pts):
    p = np.zeros(2)
    for i in range(n + 1):
        p += bernstein(n, i, t) * pts[i]
    return p

def MouseEvent(event, x, y, flag, params):
    global pts, cnt, clicking
    # 左クリックの挙動
    if event == cv2.EVENT_LBUTTONDOWN:
        if cnt == 0:  # 最初の点を決める
            cnt += 1
            pts[0] = (x,y)

        elif cnt == 1 and (x,y) is not pts[0]:  # 線分を確定すると同時に制御点も決める
            cnt += 1
            pts[4] = (x,y)
            pts[1] = pts[0]*0.7 + pts[4]*0.3
            pts[2] = pts[0]*0.5 + pts[4]*0.5
            pts[3] = pts[0]*0.3 + pts[4]*0.7
            draw_bezier(pts)

        else:  # 線分が確定したらドラッグモード
            clicking = True

    # 左ボタンから手を離す
    if event == cv2.EVENT_LBUTTONUP:
        clicking = False

    # 左クリック（ドラッグ）中はその座標を中央の制御点とする
    if clicking:
        pts[2] = (x, y)
        draw_bezier(pts)
        
    # 右クリックでベジェ曲線をリセットする
    if event == cv2.EVENT_RBUTTONDOWN:
        pts[2] = pts[0]*0.5 + pts[1]*0.5
        draw_bezier(pts)
    
def draw_bezier(pts):
    img = img1.copy()
    curve1 = []
    for t in np.linspace(0, 1, 50):
        curve1.append(bezier(4, t, pts))
    curve = np.array(curve1, np.int32)
    cv2.polylines(img, [curve], False, (255,0,0), thickness = 5)
    cv2.imshow(winname, img)

winname = "kupa"
img1 = np.full((240,320,3), (0,128,128), np.uint8)
pts = np.zeros((5, 2), dtype=np.float32)
cnt = 0
clicking = False
cv2.imshow(winname, img1)
cv2.setMouseCallback(winname, MouseEvent)
cv2.waitKey(0)
cv2.destroyAllWindows()
```

![kupa_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/d6f90d45-b755-247b-8ff8-58abd5f6c549.gif)

# 葉っぱ曲線

## 幾何の問題

話は変わるが、高校数学で「直線に対して対称な点の座標を求める」という問題がよくあった。だが、そのほとんどは直線や点の座標が具体的に明記されており、汎用化された式を目にすることはなかった。

![taisyou.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/2c46478f-40c1-8b45-7f25-e3a9dbf2731d.png)

そこで頑張って解いてみた。
答えは、

```math
X = (2x_1-a) - 2(x_1-x_2)t \\
Y = (2y_1-b) - 2(y_1-y_2)t \\
ここで　t = \frac{(x_1-x_2)(x_1-a) + (y_1-y_2)(y_1-b)}{(x_1-x_2)^2+(y_1-y_2)^2}\\
```

だ。これから頑張って*t*を消すと複雑かつ美しい式が現れるが、数学の問題ではなくプログラムとして実装していく話なのでこの形で十分だろう。それにしてもここまで複雑な式になるとは思わなかった。道理で高校数学の問題としては出てこないわけだ。

## 葉っぱ曲線を実装する
ソースは略。あとで完成形を紹介します。
![kupa_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/8596dd2f-9e8e-d635-1c9c-074f43cd19cf.gif)


# 応用
閉曲線の内部を別画像にすればいろいろ面白いことができる。
葉っぱとか唇とか書いたが実は私が想定していたのは**八雲紫**だった。スキマ妖怪に限らずとも空間に生じた裂け目と考えれば、そこから地獄が顔をのぞかせていたり宇宙とつながっていたり巨大な目玉がこちらを睨みつけていたりと様々な妄想をカタチにすることができる。必要とあらば（？）「くぱぁ」も可能だ。

ここでは単一の画像で簡単に実装できるそこそこエモい例として、閉曲線の中は色を反転させるようにした。遊んでいるうちに**魔空空間**とか**ポドリアルスペース**とかいろいろ懐かしい言葉が思い浮かんできて涙が止まらなくなった。

## ソース

```Python:kupa.py
from scipy.special import comb
import numpy as np
import cv2

def bernstein(n, i, t):
    # bezier_opencv.pyと同

def bezier(n, t, pts):
    # bezier_opencv.pyと同

def MouseEvent(event, x, y, flag, params):
    # bezier_opencv.pyと同
    
def get_symmetry_points(pts):
    x1, y1 = pts[0]
    x2, y2 = pts[4]
    a, b = pts[2]
    t = ((x1-x2)*(x1-a)+(y1-y2)*(y1-b))/((x1-x2)**2+(y1-y2)**2)
    X = (2*x1-a)-2*(x1-x2)*t
    Y = (2*y1-b)-2*(y1-y2)*t
    q = pts.copy()
    q[2] = (X, Y)
    return q

def draw_inner_kupa(img1, img2):
    # 元画像に(0,0,0)という色があるとおかしくなる
    img1 = np.where(img1==(0,0,0), img2, img1)
    return img1

def draw_bezier(pts):
    # ptsと両端の線分で対象な点を持つ座標群を作る
    # ptsと対称だからといってqtsという変数名は我ながらどうかと思う    
    qts = get_symmetry_points(pts)

    # ptsおよびqtsを制御点とするベジェ曲線を作る
    curve1, curve2 = [], []
    for t in np.linspace(0, 1, 50):
        curve1.append(bezier(4, t, pts))
        curve2.append(bezier(4, t, qts))

    # curve2の順序を逆にし、curve1とcurve2を連結して閉曲線とする
    curve = np.concatenate((curve1, curve2[::-1]), axis =0)

    curve = np.array(curve, np.int32)
    img = img1.copy()
    cv2.fillPoly(img, [curve], (0,0,0))
    img = draw_inner_kupa(img, img2)
    cv2.imshow(winname, img)

winname = "kupa"
filename = "hoge.jpg"
img1 = cv2.imread(filename)
img2 = 255 - img1
pts = np.zeros((5, 2), dtype=np.float32)
cnt = 0
clicking = False
cv2.imshow(winname, img1)
cv2.setMouseCallback(winname, MouseEvent)
cv2.waitKey(0)
cv2.destroyAllWindows()
```

## 実行結果

元画像は[新宿の高層ビルの写真素材（ぱくたそ）](https://www.pakutaso.com/20160539148post-7979.html)。いろいろなサイトで使われてるな、この画像。
![kupa.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/ea664f1d-8025-91b7-adce-c6983cf91d3c.gif)

# 終わりに
「ギャバンやウィングマンを例に出してるけどそういう演出なら○○のほうが適切ですよ」といったマサカリ、お待ちしています。