OpenCVでマウスイベントを取得する　～GUIな集中線ツールを作る～

# はじめに
漫画のように集中線の加工がされている画像を見かけた。
ググるとスマホアプリやWebアプリが公開されており特段珍しいものではなかったのだが、私もこれを作ってみたくなった。

# 集中線を描く関数

最初はアレもコレも定数だったが、自然に見えるようあちこちに乱数を付与していった。
また、画像が指示されていないときに単色画像を用意したり注目範囲の指定がないときでも自動でそれっぽく集中線を描くようにした。このようにいろいろ肉付けしていくのは楽しいものだ。
ただし勉強不足につき半透明は実装できていません。

```Python:ソース1
import sys
import numpy as np
import cv2
import random
import math

def speed_line(img, center=False, radius=False, color=(255,255,255)):
    random.seed()
    h, w = img.shape[:2]

    # 中心と半径が未設定の場合、自動で設定
    xc = w//2 if center == False else center[0]
    yc = h//2 if center == False else center[1]
    rx = w//8 if center == False else radius[0]
    ry = h//8 if center == False else radius[1]

    r2 = w+h                                    # 外径
    max_num = 128                               # 線の数
    num = int(max_num*random.uniform(0.9,1.3))  # 線の数バラツキ
    a = 2 * math.pi / num                       # 角度
    b0 = math.pi / max_num                      # 線（三角形）の先端角度
    p = 0.7                                     # 線（三角形）が出現する確率

    for i in range(num):
        if random.random() < p:
            b = b0*random.uniform(0.1,1)        # 線（三角形）の先端角度バラツキ
            r = random.uniform(0.9,1.2)         # 内径のバラツキ倍率
            x1 = xc + int(r*rx*math.cos(i*a))
            y1 = yc + int(r*ry*math.sin(i*a))
            x2 = xc + int(r2*math.cos(i*a))
            y2 = yc + int(r2*math.sin(i*a))
            x3 = xc + int(r2*math.cos(i*a+b))
            y3 = yc + int(r2*math.sin(i*a+b))
            pts = np.array(((x1,y1), (x2,y2), (x3,y3)))
            cv2.fillConvexPoly(img, pts, color)
    return img

def main():
    args = sys.argv
    if len(args) == 1:
        img_origin = np.full((240,320,3), (120,120,120), np.uint8)
    else:
        img_origin = cv2.imread(args[1])
    cv2.imshow("speed line", speed_line(img_origin.copy()))
    cv2.waitKey(0)
    cv2.destroyAllWindows()     

if __name__ == "__main__":
    main()
```

結果はこんな感じ。
![speedline1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/e67c4d61-0e72-a855-a188-49f13c514701.png)

# GUI的なことをする
OpenCVには**cv2.setMouseCallback()**という関数があり、マウスイベントを管理することができる。
チュートリアルとしては[ここ](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_gui/py_mouse_handling/py_mouse_handling.html)、またQiitaの先人の記事としては以下の記事がわかりやすい。

　　[OpenCVを使ってマウスイベント（手動）でテニスコート領域を選択できるようにする](https://qiita.com/otakoma/items/04e525ac74b7191dffe6)

これらを参考に、任意の場所に任意の大きさの楕円を描くプログラムを書いてみる。

メインルーチンからコールバック関数（このあたり正しい呼称は不明）を呼び出すプログラムはウェブ上に多数あるが、`main()`という関数からコールバック関数を呼び出そうとしたらグローバル変数の扱いに難儀して何とも小汚いソースになってしまった。もっとエレガントな方法があったら教えて下さい。

```Python:ソース2
import sys
import numpy as np
import cv2
import random

def draw_ellipse(event, x, y, flags, param):
    global cnt, xc, yc, rx, ry
    color = (random.randint(0,255), random.randint(0,255), random.randint(0,255))
    if event == cv2.EVENT_MOUSEMOVE:
        img_tmp = img.copy()
        if cnt == 0:
            h, w = img.shape[:2]
            cv2.line(img_tmp, (x,0), (x,h), color)
            cv2.line(img_tmp, (0,y), (w,y), color)
            cv2.imshow(winname, img_tmp)
        elif cnt == 1:
            rx, ry = abs(x-xc), abs(y-yc)
            cv2.line(img_tmp, (xc,yc-ry), (xc,yc+ry), color)
            cv2.line(img_tmp, (xc-rx,yc), (xc+rx,yc), color)
            cv2.ellipse(img_tmp, (xc,yc), (rx, ry), 0, 0, 360, color)
            cv2.imshow(winname, img_tmp)

    if event == cv2.EVENT_LBUTTONDOWN:
        cnt = cnt + 1
        if cnt == 1:
            xc, yc = x, y
        elif cnt == 2:
            rx, ry = abs(x-xc), abs(y-yc)
            cv2.ellipse(img, (xc,yc), (rx, ry), 0, 0, 360, (0,0,255), 3)
        cv2.imshow(winname, img)

def main():
    global img, img_tmp, cnt,winname, xc, yc, rx, ry

    args = sys.argv
    if len(args) == 1:
        img_origin = np.full((240,320,3), (120,120,120), np.uint8)
    else:
        img_origin = cv2.imread(args[1])

    img = img_origin.copy()

    cnt = 0
    winname = "GUI tool"
    cv2.namedWindow(winname)
    cv2.setMouseCallback(winname, draw_ellipse)
    cv2.imshow(winname, img)

    while cnt<2:
        key = cv2.waitKey(1) & 0xFF

    cv2.waitKey(0)
    cv2.destroyAllWindows()

if __name__ == "__main__":
    main()
```

結果はこんな感じ。
アニメーションPNGはPythonプログラムで作ったのではなく、他のフリーソフトを使った。

アニメーションPNGは編集画面ではうまく表示されるのだが公開された記事ではうまく表示されないのでGIFに変更。

![speedline2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/a0a24a0a-5479-c6d0-a4d6-57d73587d592.gif)



# 両者を合体する
合体するだけでも大変だった。関数の戻り値などを使いこなしたかったのだが、これまたグローバル変数頼みとなってしまった。
多重ループから抜ける処理というのもはじめて書いのだが…変数名が気に入らないな。

```Python:ソース3
import sys
import numpy as np
import cv2
import random
import math

def speed_line(img, center=False, radius=False, color=(255,255,255)):
    # ソース1にあるやつ

def draw_ellipse(event, x, y, flags, param):
    # ソース2にあるやつ

def main():
    global img, img_tmp, cnt,winname, xc, yc, rx, ry

    args = sys.argv
    if len(args) == 1:
        img_origin = np.full((240,320,3), (120,120,120), np.uint8)
    else:
        img_origin = cv2.imread(args[1])

    img = img_origin.copy()
    img_tmp = img.copy()

    cnt = 0
    winname = "speed line"
    cv2.namedWindow(winname)
    cv2.setMouseCallback(winname, draw_ellipse)
    cv2.imshow(winname, img_tmp)

    # 左クリックを2回するまでループ
    while cnt<2:
        cv2.waitKey(1)
        
    # グローバル変数によりいつの間にか得ていた楕円情報を使って集中線画像を複数回描く
    frames = 4
    imgs = []
    for i in range(frames):
        imgs.append(speed_line(img_origin.copy(), center=(xc,yc), radius=(rx,ry)))

    # ピカピカアニメーション
    isBreak = False
    while True:
        for i in range(frames):
            cv2.imshow(winname, imgs[i])
            if cv2.waitKey(1) & 0xFF == ord("q"):
                isBreak = True
        if isBreak:
            break

    cv2.destroyAllWindows()

if __name__ == "__main__":
    main()
```

結果はこんな感じ。モデルが古いって？ ナウなヤングにバカウケなフィギュアなどは持っていないので仕方がない。
こちらもアニメGIFに変更。
![speedline3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/62f05eb2-e87e-dc40-beb2-2b4b96a56ac1.gif)



# 終わりに
[顔検出](https://qiita.com/mo256man/items/c570c645153cae122196)と組み合わせればもっと面白いことができる。と思いついたが、すでにそれも動画になっていた。それも8年前にだ。

　　[ガンダムAGEの「強いられているんだ！（集中線）」を顔認識でやってみた](https://www.nicovideo.jp/watch/sm16690027)

たとえ周回遅れであっても、自分自身の経験を積むために成果物を発表するのを怠るわけにはいかない。