OpenCVとPILで図形を描写する

# はじめに
カンニングペーパーを作れば、その過程で理解が進むというもの。これは自分のための記事だ。
OpenCVに特化した記事を書くつもりだったのだが、理由があってPILについても書かざるを得なくなってしまった。
なお、一部の引数は省略している。

# OpenCV
## ベースとなる単色画像を作成する　numpy.full(shape, fill_value, dtype)
OpenCVの画像データはnumpy配列なのでnp.full()で単色画像を作ることができる。
黒地の場合はもう少し簡単な記述ができるが個別に覚えるほどではないので略。

 - *shape*　配列の形状。RGB画像を作るならば`(height, width, 3)`。
 - *fill_value*　埋める値。RGB画像を作るならば`(b, g, r)`。
 - *dtype*　データ型。画像を作るならば`dtype = numpy.uint8`の指定が必須。

## 図形描写する
以下の関数は指定した画像を加工する。どういうことかというと、
`img = cv2.line(img, ...)`
と戻り値を自分自身に代入しなおさなくても
`cv2.line(img, ...)`
と書くだけで`img`の値が変わってしまうのだ。そうなってほしくない場合は元画像を別オブジェクトとしてコピーしておく必要がある。[こちらの記事](https://qiita.com/mo256man/items/c570c645153cae122196)で`img_rect = img_origin.copy()`としたのはこのためだ。

### 線を引く　cv2.line(img, pt1, pt2, color, thickness) 
 - *img*　画像。
 - *pt1*, *pt2*　2点の座標を`(x, y)`で指定する。整数。画像外でも可。
 - *color*　線の色。
 - *thickness*　線の太さ。省略可能でデフォ値は1。

### 四角形を描く　cv2.rectangle(img, pt1, pt2, color, thickness)
 - *img*　画像。
 - *pt1*, *pt2*　対角線の座標。`(x, y)`で指定する。整数。画像外でも可。
 - *color*　線の色。
 - *thickness*　線の太さ。省略可能でデフォ値は1。-1を指定すると塗りつぶす。

### 円を描く　cv2.circle(img, center, radius, color, thickness)
円弧を描くにはcv2.ellipse()を使う。

 - *img*　画像。
 - *center*　中心の座標。`(x, y)`で指定する。整数。
 - *radius*　半径。整数。
 - *color*　線の色。
 - *thickness*　線の太さ。省略可能でデフォ値は1。-1を指定すると塗りつぶす。

#### 楕円もしくは円弧を描く　cv2.ellipse(img, center, axes, angle, startAngle, endAngle, color, thickness)

 - *img*　画像。
 - *center*　中心の座標。`(x, y)`で指定する。整数。
 - *axes*　長径と短径を`(a, b)`であらわす。どちらが縦でどちらが横かは*angle*によって決まる。
 - *angle*　楕円の回転角度。単位は度。 `angle = 0` で a > b なら横長の楕円というわけだ。
 - *startAngle*　円弧の開始角度。単位は度。基準は水平ではなく*angle*の値。
 - *endAngle*　円弧の終了角度。閉じた円もしくは普通に楕円を描くときは0と360を指定する必要がある。面倒だな。
 - *thickness*　線の太さ。省略可能でデフォ値は1。-1を指定すると塗りつぶす。

角度は小数でも可。また、x軸からy軸に向けて回転する方向が正となる。数学のxy座標なら反時計回りだが、コンピューターの座標系ではy軸は下向きなので反時計回りが正。
つまり、こういうこと。
![ellipse.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/c3e1b3a8-e9ea-db3b-f318-56f0e03651ac.png)

まあ、ここまで書いてもどうせ使わないんですけどね。


### 多角形を描く　cv2.polylines(img, pts, isClosed, color, thickness)
 - *img*　画像。
 - *pts*　多角形の**配列**。numpy配列をリスト化したもの。個々の多角形が`np.array([(x1,y1), (x2,y2), ...])`で、それをブラケットで括ってリストにする。一つの多角形を描写するときでも要素数が1のリストとする。
 - *isClosed*　最初の点と最後の点を結んで閉じた多角形とするときは`True`、折れ線にするときは`False`を指定する。
 - *color*　線の色。
 - *thickness*　線の太さ。省略可能でデフォ値は1。-1を指定して塗りつぶすことはできない。

*pts*の書き方がややこしいが、輪郭検出で遊ぶためにはこいつを使いこなす必要があるので頑張って覚えよう。
以下のサンプルプログラムでは同一の図形を複数回描写するために自作関数を織り込んでいる。
揃えるためにスペースを使うとPEP警察に逮捕されるんでしたっけ。

```Python:polylines_test.py
import numpy as np
import cv2

### 座標群をオフセットする関数
# [(x1,y1), (x2,y2)] と (a,b) から [(x1+a,y1+b), (x2+a, y2+b)] を作る
def position_offset(pos, offset):
    np_newpos = np.array(pos) + np.array(offset)
    list_newpos = list(map(tuple, np_newpos.tolist()))
    return list_newpos


### ベースとなる単色画像を作る
imgCV = np.full((180,200,3), (128,128,128), dtype = np.uint8)

### 同じ折れ線を3個作る
pos = [(10,10), (80,60), (80,10), (60,30)]
offset1 = (  0,  0)
offset2 = (100, 40)
offset3 = (100,100)
pts1 = np.array(position_offset(pos, offset1))
pts2 = np.array(position_offset(pos, offset2))
pts3 = np.array(position_offset(pos, offset3))

### 多角形を描く
# 一つの多角形を描写
cv2.polylines(imgCV, [pts1]      , True, (255,0,0), 3)

# 複数の多角形を一度に描写
cv2.polylines(imgCV, [pts2, pts3], True, (0,0,255), 3)

cv2.imshow("polylines", imgCV)
cv2.waitKey(0)
cv2.destroyAllWindows()
```

結果はこう。
![CV_test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/a977d8b8-426b-583c-a163-717460db71f7.png)



#### 多角形を塗りつぶす　cv2.fillPoly(img, pts, color) 
凹を持つ多角形も正確に塗りつぶす。Wikipediaには[凹四角形](https://ja.wikipedia.org/wiki/%E5%87%B9%E5%9B%9B%E8%A7%92%E5%BD%A2)という項目があるが、昔読んだ子供向けの百科事典では「矢の根形」という名前だった。

#### 凸多角形を塗りつぶす　cv2.fillConvexPoly(img, points, color)
凸多角形であることがわかっていればこちらを使ったほうが速い。
凹多角形でこいつを使うと、エラーにはならないが塗り残しが発生する。

 - *points*　座標群。ptsとは違い、numpy配列で一つの多角形のみ指定可能。なんでこいつだけいろいろ微妙に違ってるんだ。

## 文字を描く　cv2.putText(img, text, org, fontFace, fontScale, color, thickness) 
 - *img*　画像。
 - *text*　テキスト。**日本語不可**。改行コードで改行させることはできない。
 - *org*　テキストの**左下の**座標を`(x, y)`で指定する。
 - *fontFace*　フォント。何でもありな訳ではない。詳細は後述。
 - *fontScale*　フォントのサイズ。面倒なことに必須。「1」の大きさはフォントにより異なる。
 - *color*　テキストの色。
 - *thickness*　線の太さ。省略可能でデフォ値は1。

### cv2.putText()で使えるフォント
以下の8種類が使える。HERSHEYとはチョコレートのことではなく、ハーシー博士が開発したベクターフォントのこと。

 - cv2.FONT_HERSHEY_SIMPLEX
 - cv2.FONT_HERSHEY_PLAIN
 - cv2.FONT_HERSHEY_DUPLEX
 - cv2.FONT_HERSHEY_COMPLEX
 - cv2.FONT_HERSHEY_TRIPLEX
 - cv2.FONT_HERSHEY_COMPLEX_SMALL
 - cv2.FONT_HERSHEY_SCRIPT_SIMPLEX
 - cv2.FONT_HERSHEY_SCRIPT_COMPLEX

これにcv2.FONT_ITALIC を組み合わせる（組み込み定数なので足し算する）ことで斜体にすることができる。

#### テキストの幅と高さを求める　cv2.getTextSize(text, fontFace, fontScale, thickness)
テキストを図形の中央に配置したいときなどにこの関数を使う。
出力は二つある。`(width, height)`で表されるテキストのサイズと、文字列の最下点から見たベースラインのy座標。後者は「y」や「p」など下に飛び出ているアレだ。


## 文字描写のサンプル

```Python:putText_test.py
import numpy as np
import cv2

fonts=[cv2.FONT_HERSHEY_SIMPLEX,
       cv2.FONT_HERSHEY_PLAIN,
       cv2.FONT_HERSHEY_DUPLEX,
       cv2.FONT_HERSHEY_COMPLEX,
       cv2.FONT_HERSHEY_TRIPLEX,
       cv2.FONT_HERSHEY_COMPLEX_SMALL,
       cv2.FONT_HERSHEY_SCRIPT_SIMPLEX,
       cv2.FONT_HERSHEY_SCRIPT_COMPLEX]

# ベースとなる単色画像を作る
img = np.full((360,600,3), (255,255,255), dtype = np.uint8)

for i, font in enumerate(fonts):
    for j in range(2):
        for k in range(2):
            x, y = 200*(j+1)*k+10, i*40+50
            fnt = font if j == 0 else font + cv2.FONT_ITALIC
            text = "sample_{}".format(fnt)
            cv2.putText(img,
                        text = text,
                        org = (x, y),
                        fontFace = fnt,
                        fontScale = 1,
                        color = (0, 0, 255*j),
                        thickness=1)

            # ベースラインは未使用
            (w, h), _ = cv2.getTextSize(text = text,
                                        fontFace = fnt,
                                        fontScale = 1,
                                        thickness = 1)
            cv2.rectangle(img, (x, y), (x+w, y-h), (255, 0, 0))
            cv2.circle(img, (x, y), 3, (255, 0, 0))

cv2.imshow("hershey fonts",img)
cv2.waitKey(0)
cv2.destroyAllWindows()
```

中央の列が通常のフォント、右の列がイタリックなフォント。左はその二つを合成したもの。イタリックの効果が見られないフォントがいくつかあるな。

![cv_fonts.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/a7a9bf0d-7e8d-ae10-08b9-a06660c8f887.png)



# PIL
## ベースとなる単色画像を作成する　Image.new(mode, size, color)

 - *mode*　モード。`"RGB"`、`"RGBA"`、`"L"`（8ビットグレースケール）など。
 - *size*　 サイズ。`(width, height)`を指定する。
 - *color*　色。`mode = "RGB"`ならば`(r, g, b)`で指定する。必須ではなく、デフォ値は黒。

## 図形描写する
画像に図形を描写するには、ImageそもものではなくImageDraw.Drawオブジェクトを作成し、それに対して描写をおこなう。

### 線（折れ線）を引く　ImageDraw.line(xy, fill, width, joint)

 - *xy*　座標群。3点以上も可。リストで`[(x1, y1), (x2, y2), ...]`もしくは`[x1, y1, x2, y2, ...]`という書き方をする。
 - *fill*　線の色。カラー時のデフォ値は白。グレースケール時のデフォ値は黒。
 - *width*　線の幅。整数。
 - *joint*　デフォ値は`None`。`"curve"`を指定すると、折れ線の連結部が丸くなる。


たぶんバグだが、`joint = "curve"`を使う際は座標群の記載は`xy = [(x1, y1), (x2, y2), ...]`でなくてはならない。`xy = [x1, y1, x2, y2, ...]`だとエラーになってしまう。また、*fill*を設定せずにデフォの色で折れ線を描くと、curveの処理が中途半端になってしまう。



### 四角形を描く　ImageDraw.rectangle(xy, fill, outline, width)

 - *xy*　2点を指定する。リストで`[(x0, y0), (x1, y1)]`もしくは`[x0, y0, x1, y1]`という書き方をする。
 - *fill*　塗りつぶしの色。デフォ値はNone（塗りつぶしなし）。
 - *outline*　線の色。
 - *width*　線の幅。

### 円（楕円）を描く　ImageDraw.ellipse(xy, fill, outline, width)

 - *xy*　**中心ではなく囲む2点を指定する。**リストで`[(x0, y0), (x1, y1)]`もしくは`[x0, y0, x1, y1]`という書き方をする。四角形に内接する円（楕円）というイメージ。このときx0 <= x1 かつ y0 <= y1 である必要がある。
 - *fill*　塗りつぶしの色。
 - *outline*　線の色。
 - *width*　線の幅。

なんでそんな仕様になってんだと不思議に思ったが、よく考えたらお絵かきソフトでもこのような仕様のものが少なくなかった。

### 多角形を描く　ImageDraw.polygon(xy, fill, outline)
lineとrectangleがわかれば使いこなせる。現時点では線の幅は指定できず、1で固定。

 - *xy*　座標群。最初の点と最後の点は自動で結ばれる。
 - *fill*　塗りつぶしの色。
 - *outline*　線の色。

### その他
PIL.ImageDraw.ImageDraw.point(*xy*, *fill*)　で（複数の）点を、
ImageDraw.arc(*xy*, *start*, *end*, *fill*, *width*)　で円弧を、
ImageDraw.chord(*xy*, *start*, *end*, *fill*, *outline*, *width*)　で円弧＋弦を、
ImageDraw.pieslice(*xy*, *start*, *end*, *fill*, *outline*, *width*)　で扇形（円弧＋半径）を描写することができる。
角度の単位は度でOpenCVのcv2.ellipse()と同じく3時方向から時計方向に増えていく。

## 文字を描く
PILの文字を描くメソッドはPC内にあるフォントを使うので日本語も使える。

### 文字を描く　ImageDraw.text(xy, text, fill, font)

 - *xy*　テキストの**左上**の座標を`(x,y)`で指定する。
 - *text*　テキスト。`\n`を使っての改行もOK。
 - *fill*　テキストの色。
 - *font*　フォント。詳細は後述。省略可能で、そのときは日本語不可の非常に小さなフォントになる。
 - 複数行描写時の改行量などの設定もあるが、略。

### 文字のフォントを指定する　ImageFont.truetype(font, size)

 - *font*　トゥルータイプフォントのファイル名。Windowsなら C:\Windows\Fonts 、ラズパイなら /usr/share/fonts 、と適切な場所を自動で探してくれる。
 - *size*　フォントのサイズ。デフォ値は10ポイント。
 - エンコード（デフォ値はUnicode）などの設定もあるが、略。

#### テキストの幅と高さを求める　ImageDraw.textsize(text, font)

cv2.getTextSize()とは違い、出力は `(width, height)` のみ。

## PILのサンプル
えらく盛りだくさんになってしまった。
こちらでも自作関数position_offset()を使っている。

```Python:PIL_test.py
import numpy as np
from PIL import Image, ImageDraw, ImageFont

### 座標群をオフセットする関数
# [(x1,y1), (x2,y2)] と (a,b) から [(x1+a,y1+b), (x2+a, y2+b)] を作る
def position_offset(pos, offset):
    np_newpos = np.array(pos) + np.array(offset)
    list_newpos = list(map(tuple, np_newpos.tolist()))
    return list_newpos


### ベースとなる単色画像を作る
imgPIL = Image.new("RGB", (640,480), (128,128,128))

### Drawオブジェクトを作る
draw = ImageDraw.Draw(imgPIL)

### 折れ線
pos = [(10,10), (80,60), (80,10), (60,30)]

### line
for i in range(6):
    # y=0 : joint = None とした場合の折れ線　ついでに fill を省略している
    draw.line(xy = position_offset(pos,(100*i,0)),
              width = i*5,
              joint = None)

    # y=100 : joint = "curve" とした場合の折れ線
    draw.line(xy = position_offset(pos,(100*i,100)),
              fill = (0,255,255),
              width = i*5,
              joint = "curve")

    # y=200の右端 : fill を指定せず joint = "curve" としたときの挙動
    # forループの中に入れる必要はないが、比較しやすくするためにここに置いた。
    draw.line(xy = position_offset(pos,(100*5,200)),
              width = 25,
              joint = "curve")
    

### polygon　lineと同じ座標群を使っても閉じた多角形になる
# fillを指定
draw.polygon(xy = position_offset(pos,(0,200)),
             fill = (255,0,0))

# outlineを指定
draw.polygon(xy = position_offset(pos,(100,200)),
             outline = (0,255,0))

# fillとoutlineを指定
draw.polygon(xy = position_offset(pos,(200,200)),
             fill = (255,0,0),
             outline = (0,255,0))


### rectangleとellipse
pos=[(10,0), (200,70)] # 2点の順番を入れ替えてみよう
for i in range(3):
    draw.rectangle(xy = position_offset(pos,(200*i,300)),
                   fill = None,
                   outline = (0,0,255),
                   width = i*5) # width は 0 でも 1 になる

    draw.ellipse(xy = position_offset(pos,(200*i,300)),
                 fill = (255,255,0),
                 outline = (0,255,0),
                 width = i*5)

### arcとchordとpieslice
start, end = 45, 300
# arc 円弧　outlineは略
draw.arc(xy = position_offset(pos,(0,400)),
         start = start,
         end = end,
         width = 4)

# chord 弦
draw.chord(xy = position_offset(pos,(200,400)),
           start = start,
           end = end,
           fill = (255,0,0),
           outline = (0,255,0))

# pieslice 扇型
draw.pieslice(xy = position_offset(pos,(400,400)),
              start = start,
              end = end,
              fill = (255,0,0),
              outline = (0,255,0))


### 文字を描く
# 最小限の設定で文字を描く
draw.text(xy = (520,190),
          text = "bug? -> ")

# 指定したフォントで文字を描く
# フォントはDF麗雅宋とした
font = ImageFont.truetype(font="Dflgs9.TTC",
                          size=30)
draw.text(xy = (440,250),
          text = "バグ？ →",
          fill = (0,0,0),          
          font = font)

### 画像を表示する
imgPIL.show()
```

結果はこう。
![PIL_test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/bbb79de2-0879-8063-4025-8df29754c531.png)


# 終わりに
自作関数 position_offset() を作るにあたり、teratailのお世話になりました。ありがとうございます。
https://teratail.com/questions/234513


# 次回予告
OpenCVで日本語を表示させたい。そのためには、その部分だけPILを使えばよい。
そういう記事はウェブ上に多数あるものの、どうもそれらでは不満だ。先人をディスるつもりはないが、もっと実用的な関数を作りたい。