LoginSignup
5
4

More than 3 years have passed since last update.

OpenCVとPILで図形を描写する

Last updated at Posted at 2020-04-26

はじめに

カンニングペーパーを作れば、その過程で理解が進むというもの。これは自分のための記事だ。
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の値が変わってしまうのだ。そうなってほしくない場合は元画像を別オブジェクトとしてコピーしておく必要がある。こちらの記事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

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

多角形を描く 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警察に逮捕されるんでしたっけ。

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

多角形を塗りつぶす cv2.fillPoly(img, pts, color)

凹を持つ多角形も正確に塗りつぶす。Wikipediaには凹四角形という項目があるが、昔読んだ子供向けの百科事典では「矢の根形」という名前だった。

凸多角形を塗りつぶす 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」など下に飛び出ているアレだ。

文字描写のサンプル

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

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()を使っている。

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

終わりに

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

次回予告

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

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4