Python, OpenCVで図形描画(線、長方形、円、矢印、文字など)
OpenCVには線分や四角(長方形、正方形)、円、楕円、円弧、矢印、マーカー、文字など、様々な図形を描画する関数が用意されている。物体を検出した位置を図示したりする際などに便利。
次の画像のように図形を描画できる。サンプルコードは最後に示す。

はじめに図形描画関数における引数について説明したあと、個別の関数について説明、最後に全体のサンプルコードとその出力結果を示す。
- OpenCVでの図形描画の共通の引数
- 線分を描画:
cv2.line() - 矢印を描画:
cv2.arrowedLine() - 長方形を描画:
cv2.rectangle() - 円を描画:
cv2.circle() - 楕円を描画:
cv2.ellipse() - 円弧を描画:
cv2.ellipse() - マーカーを描画:
cv2.drawMarker() - 折れ線、多角形を描画:
cv2.polylines(),cv2.fillPoly(),fillConvexPoly() - 文字列(テキスト)を描画:
cv2.putText() - サンプルコードと出力結果
公式のドキュメント(英語)は以下。
今回の例で用いているOpenCVのバージョンは3.3.0。2.x系だと異なる部分があるかもしれない。
なお、Pillowでも図形を描画することができる。
OpenCVでの図形描画の共通の引数
OpenCVの様々な図形描画関数にはいくつかの共通の引数がある。
コードの例とその出力結果は最後に示す。
画像: img
図形を描画する画像。NumPyの配列ndarray。このオブジェクト自体が変更される。
座標: pt1, pt2 / centerなど
線の両端の座標や円の中心などを(x, y)で指定する。単位はピクセル、原点は左上。
色: color
カラー画像でBGRの場合は(Blue, Green, Red)で指定する。OpenCVのimread()で画像ファイルを読み込んだ場合はBGRの並びになっている。
線の太さ、内部の塗りつぶし: thickness
線の太さは引数thicknessで指定する。単位はピクセル。
長方形や円の場合、-1などの負の値を指定すると内部が塗りつぶされる。
線の色と塗りつぶし色を変えたい場合は、塗りつぶし図形を描画して同じ位置にさらに枠線を描画する。一度に指定することは出来ない。
線の種類: lineType
線を描画するアルゴリズムの種類を以下の定数から指定する。
- 4連結:
cv2.LINE_4 - 8連結:
cv2.LINE_8(デフォルト) - アンチエイリアス:
cv2.LINE_AA
なお、太い線の端や太い線の長方形の角は、丸く描画される。
座標の小数部分のビット数: shift
座標の小数部分のビット数を整数で指定する。デフォルトは0。
shiftを指定すると、座標が(x*2^(-shift), y*2^(-shift))として計算される。
例えば、座標を(100, 101)でshift=1とすると、実際に描画に用いられる座標は(50, 50.5)となり、サブピクセル精度で座標を指定することができる。
アンチエイリアス前提なので、lineType=cv2.LINE_AAとする必要がある。
例
np.full()を使って全面が128のグレー画像を生成し、その上に図形を描画する。
もちろん、cv2.imread()で既存の画像ファイルを読み込めば、画像の上に図形を描画できる。
例として、長方形cv2.rectangle()とcv2.line()を使って、様々なthicknessやlineType、shiftの値で描画する。
shiftの違いは拡大しないと分からないかもしれない。
import cv2
import numpy as np
print(cv2.__version__)
# 3.3.0
img = np.full((210, 425, 3), 128, dtype=np.uint8)
cv2.rectangle(img, (50, 10), (125, 60), (255, 0, 0))
cv2.rectangle(img, (50, 80), (125, 130), (0, 255, 0), thickness=-1)
cv2.rectangle(img, (50, 150), (125, 200), (0, 0, 255), thickness=-1)
cv2.rectangle(img, (50, 150), (125, 200), (255, 255, 0))
cv2.rectangle(img, (175, 10), (250, 60), (255, 255, 255), thickness=8, lineType=cv2.LINE_4)
cv2.line(img, (175, 10), (250, 60), (0, 0, 0), thickness=1, lineType=cv2.LINE_4)
cv2.rectangle(img, (175, 80), (250, 130), (255, 255, 255), thickness=8, lineType=cv2.LINE_8)
cv2.line(img, (175, 80), (250, 130), (0, 0, 0), thickness=1, lineType=cv2.LINE_8)
cv2.rectangle(img, (175, 150), (250, 200), (255, 255, 255), thickness=8, lineType=cv2.LINE_AA)
cv2.line(img, (175, 150), (250, 200), (0, 0, 0), thickness=1, lineType=cv2.LINE_AA)
cv2.rectangle(img, (600, 20), (750, 120), (0, 0, 0), lineType=cv2.LINE_AA, shift=1)
cv2.rectangle(img, (601, 160), (751, 260), (0, 0, 0), lineType=cv2.LINE_AA, shift=1)
cv2.rectangle(img, (602, 300), (752, 400), (0, 0, 0), lineType=cv2.LINE_AA, shift=1)
cv2.imwrite('data/dst/opencv_draw_argument.png', img)

戻り値
cv2.rectangle()やcv2.line()のような図形描画の関数は、戻り値としてNumPyの配列ndarrayを返す。
入力画像img自体に図形が描画され変更されるが、それと同じオブジェクトが返される。
Jupyter Notebookのセルの末尾で図形描画関数を実行するとndarrayの中身が表示されるので邪魔くさい。なぜこのような実装になっているかはよく分からない。
img_rect = cv2.rectangle(img, (10, 10), (110, 60), (255, 0, 0))
print(img is img_rect)
# True
線分を描画: cv2.line()
線分を描画する関数はcv2.line()。
cv2.line(img, pt1, pt2, color, thickness=1, lineType=cv2.LINE_8, shift=0)
cv2.line(img, (50, 10), (125, 60), (255, 0, 0))
cv2.line(img, (50, 60), (125, 10), (0, 255, 255), thickness=4, lineType=cv2.LINE_AA)
矢印を描画: cv2.arrowedLine()
矢印を描画する関数はcv2.arrowedLine()。
cv2.arrowedLine(img, pt1, pt2, color, thickness=1, lineType=cv2.LINE_8, shift=0, tipLength=0.1)
引数tipLengthは矢の先の部分の長さ。全体の長さに対する比で表し、デフォルトは0.1。
cv2.arrowedLine(img, (50, 80), (125, 130), (0, 255, 0), thickness=4)
cv2.arrowedLine(img, (50, 130), (125, 80), (255, 0, 255), tipLength=0.3)
長方形を描画: cv2.rectangle()
長方形を描画する関数はcv2.rectangle()。
cv2.rectangle(img, pt1, pt2, color, thickness=1, lineType=cv2.LINE_8, shift=0)
cv2.rectangle(img, (50, 150), (125, 200), (255, 255, 0))
円を描画: cv2.circle()
円を描画する関数はcv2.circle()。
cv2.circle(img, center, radius, color, thickness=1, lineType=cv2.LINE_8, shift=0)
中心の座標centerと半径radiusを指定する。
cv2.circle(img, (190, 35), 15, (255, 255, 255), thickness=-1)
cv2.circle(img, (240, 35), 20, (0, 0, 0), thickness=3, lineType=cv2.LINE_AA)
楕円を描画: cv2.ellipse()
楕円を描画する関数はcv2.ellipse()。
cv2.ellipse(img, box, color, thickness=1, lineType=cv2.LINE_8)
引数boxは(center, axes, angle)で表し、centerは(x, y)、axesは(横方向直径, 縦方向直径)、回転角度angleはx軸方向を0度として時計回りに度で指定する。
cv2.ellipse(img, ((190, 105), (20, 50), 0), (255, 255, 255))
cv2.ellipse(img, ((240, 105), (20, 50), 30), (0, 0, 0), thickness=-1)
円弧を描画: cv2.ellipse()
同じくcv2.ellipse()で円弧を描画することもできる。
cv2.ellipse(img, center, axes, angle, startAngle, endAngle, color, thickness=1, lineType=cv2.LINE_8, shift=0)
引数が異なり、axesは(横方向半径, 縦方向半径)。
startAngleは円弧の開始角度、endAngleは終了角度で、どちらも回転角度angleが基準。
cv2.ellipse(img, (190, 175), (10, 25), 0, 0, 270, (255, 255, 255))
cv2.ellipse(img, (240, 175), (10, 25), 30, 0, 270, (0, 0, 0), thickness=-1)
マーカーを描画: cv2.drawMarker()
マーカーを描画する関数はcv2.drawMarker()。
ポイントを図示したい場合は、cv2.rectangle()で小さい四角を描くよりもマーカーのほうが便利。
cv2.drawMarker(img, position, color, markerType=cv2.MARKER_CROSS, markerSize=20, thickness=1, line_type=cv2.LINE_8)
引数markerTypeで指定するマーカーの種類は以下のドキュメントを参照。十字や四角、三角など。
cv2.drawMarker(img, (300, 20), (255, 0, 0))
cv2.drawMarker(img, (337, 20), (0, 255, 0), markerType=cv2.MARKER_TILTED_CROSS, markerSize=15)
cv2.drawMarker(img, (375, 20), (0, 0, 255), markerType=cv2.MARKER_STAR, markerSize=10)
cv2.drawMarker(img, (300, 50), (0, 255, 255), markerType=cv2.MARKER_DIAMOND)
cv2.drawMarker(img, (337, 50), (255, 0, 255), markerType=cv2.MARKER_SQUARE, markerSize=15)
cv2.drawMarker(img, (375, 50), (255, 255, 0), markerType=cv2.MARKER_TRIANGLE_UP, markerSize=10)
折れ線、多角形を描画: cv2.polylines(), cv2.fillPoly(), fillConvexPoly()
折れ線や多角形を描画する関数はcv2.polylines()。
cv2.polylines(img, pts, isClosed, color, thickness=1, lineType=cv2.LINE_8, shift=0)
引数isClosedで開始点と終了点を結ぶかどうかを指定する。
引数ptsには各点の座標を示すNumPy配列ndarray(二次元配列)のリストや配列を指定する。複数の折れ線を一度に描画できる。折れ線が一つの場合も[]で囲むなどしてリストにする必要があるので注意。三次元のndarrayにしてもOK。
pts = np.array(((300, 80), (300, 130), (335, 130)))
cv2.polylines(img, [pts], True, (255, 255, 255), thickness=2)
cv2.polylines()でisClosed=True, thickness=-1としてもエラーになり、内部を塗りつぶす事はできない。
内部を塗りつぶした多角形を描画したい場合は、cv2.fillPoly()またはfillConvexPoly()を使う。
cv2.fillPoly(img, pts, color, lineType=cv2.LINE_8, shift=0)
pts = np.array(((335, 80), (375, 80), (375, 130)))
cv2.fillPoly(img, [pts], (0, 0, 0))
cv2.fillPoly()はcv2.polylines()と同様に引数ptsに各点の座標を示すNumPy配列ndarray(二次元配列)のリストや配列を指定する。複数の多角形を一度に描画できる。
fillConvexPoly()は一つの多角形しか描画できないが、引数ptsに各点の座標を示すNumPy配列ndarray(二次元配列)をそのまま指定できる。上のサンプルコードはfillConvexPoly()を使うと以下のように書ける。
# cv2.fillPoly(img, [pts], (0, 0, 0))
cv2.fillConvexPoly(img, pts, (0, 0, 0))
公式ドキュメントによるとfillConvexPoly()のほうがcv2.fillPoly()よりも高速。多角形一つだけを描画するならfillConvexPoly()のほうがよい。
文字列(テキスト)を描画: cv2.putText()
文字列(テキスト)を描画する関数はcv2.putText()。
cv.putText(img, text, org, fontFace, fontScale, color, thickness=1, lineType=cv2.LINE_8)
引数textは描画する文字列、orgは文字列の左下座標、fontFaceはフォントの種類、fontScaleは縮尺(1.0が標準)。
fontFaceに指定できるフォントは以下のドキュメントを参照。Hershey Fontsというシンプルなフォントが使える。日本語は使えない。
cv2.putText(img, 'nkmk', (300, 170), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 255), thickness=2)
cv2.putText(img, 'nkmk', (300, 195), cv2.FONT_HERSHEY_COMPLEX, 0.8, (0, 0, 0), lineType=cv2.LINE_AA)
Pillowだと日本語のフォントの指定が可能。
サンプルコードと出力結果
img = np.full((210, 425, 3), 128, dtype=np.uint8)
cv2.line(img, (50, 10), (125, 60), (255, 0, 0))
cv2.line(img, (50, 60), (125, 10), (0, 255, 255), thickness=4, lineType=cv2.LINE_AA)
cv2.arrowedLine(img, (50, 80), (125, 130), (0, 255, 0), thickness=4)
cv2.arrowedLine(img, (50, 130), (125, 80), (255, 0, 255), tipLength=0.3)
cv2.rectangle(img, (50, 150), (125, 200), (255, 255, 0))
cv2.circle(img, (190, 35), 15, (255, 255, 255), thickness=-1)
cv2.circle(img, (240, 35), 20, (0, 0, 0), thickness=3, lineType=cv2.LINE_AA)
cv2.ellipse(img, ((190, 105), (20, 50), 0), (255, 255, 255))
cv2.ellipse(img, ((240, 105), (20, 50), 30), (0, 0, 0), thickness=-1)
cv2.ellipse(img, (190, 175), (10, 25), 0, 0, 270, (255, 255, 255))
cv2.ellipse(img, (240, 175), (10, 25), 30, 0, 270, (0, 0, 0), thickness=-1)
cv2.drawMarker(img, (300, 20), (255, 0, 0))
cv2.drawMarker(img, (337, 20), (0, 255, 0), markerType=cv2.MARKER_TILTED_CROSS, markerSize=15)
cv2.drawMarker(img, (375, 20), (0, 0, 255), markerType=cv2.MARKER_STAR, markerSize=10)
cv2.drawMarker(img, (300, 50), (0, 255, 255), markerType=cv2.MARKER_DIAMOND)
cv2.drawMarker(img, (337, 50), (255, 0, 255), markerType=cv2.MARKER_SQUARE, markerSize=15)
cv2.drawMarker(img, (375, 50), (255, 255, 0), markerType=cv2.MARKER_TRIANGLE_UP, markerSize=10)
pts = np.array(((300, 80), (300, 130), (335, 130)))
cv2.polylines(img, [pts], True, (255, 255, 255), thickness=2)
pts = np.array(((335, 80), (375, 80), (375, 130)))
cv2.fillPoly(img, [pts], (0, 0, 0))
cv2.putText(img, 'nkmk', (300, 170), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 255), thickness=2)
cv2.putText(img, 'nkmk', (300, 195), cv2.FONT_HERSHEY_COMPLEX, 0.8, (0, 0, 0), lineType=cv2.LINE_AA)
cv2.imwrite('data/dst/opencv_draw_etc.png', img)
