【OpenCV-Python】カメラからの画像取込を別スレッドにする

カメラからの画像取込を、なぜ、わざわざ別スレッドにしたいか?というと、よくある画像取込のプログラム

import cv2

# カメラを開く
cap = cv2.VideoCapture(0)

while True:
    # 画像をキャプチャする
    _, frame = cap.read()

    # 画像を表示する
    cv2.imshow("Image", frame)

    # キーを押すとループを終了する
    if cv2.waitKey(1) > 0:
        break

# カメラを閉じる
cap.release()
# すべてのウィンドウを閉じる
cv2.destroyAllWindows()

を実行し、画像が表示されたウィンドウのタイトルバーの部分をマウスでクリックすると、while文のループ内で、waitKey(1) の部分で処理が停止します。

 

処理が停止するということは、画像の表示(imshow())や画像の取込(read())も、つられて実行されなくなってしまいます。

 

画像の表示は、最悪、停止してもしょうがないとしても、画像の取込が停止するのは、画像の取込ながら画像処理する場合には、不都合です。

 

そこで、画像の取込部分を別スレッドにしたプログラムがこちら↓

import threading
import queue

import cv2

# カメラを開く
cap = cv2.VideoCapture(0)

# フレーム共用のキュー
q_frames = queue.Queue()

def frame_update():
    '''画像取込'''
    while True:
        # カメラ画像の取得
        _, frame = cap.read()
        # 取得した画像をキューに追加
        q_frames.put(frame)
        print(f"qsize:{q_frames.qsize()}")

# 画像取り込みスレッドの作成
thread = threading.Thread(target=frame_update, daemon=True)
# スレッドの開始
thread.start()

# 画像表示用スレッド
while True:
    frame = q_frames.get() # queueが空の場合、キューに何か入るまで、スレッドをブロックする。
    cv2.imshow("Image", frame)

    # キーを押すとループを終了する
    if cv2.waitKey(1) > 0:
        break

# カメラを閉じる
cap.release()
# すべてのウィンドウを閉じる
cv2.destroyAllWindows()

このようにすることで、画像を表示しているウィンドウのタイトルバーの部分をクリックしても、画像の取込(read())が停止する事は無くなります。

 

ただし、少々、厄介なのが、別スレッドからGUIの表示を行う事が出来ないので、queueを介して画像データをメインスレッド側で画像を表示するようにしています。

 

今回は、waitKeyの部分で処理が停止してしまうのを、スレッドで回避していますが、waitKeyの関数に限らず、処理が停止してしまう関数が他にもあるので、注意が必要です。

【OpenCV-Python】Canny(Canny edge detection)

Canny edge detectionの処理アルゴリズムついてはこちらのページ

Canny edge detectionの処理アルゴリズム

で紹介していますが、Canny edge detectionで画像のエッジ部分を抽出すると、明確なエッジがなくとも、風景画などの、自然な画像においてもエッジ部分を抽出してくれます。

処理アルゴリズムはやや複雑ですが、OpenCVではCanny()関数を使う事で、簡単にできてしまいます。

 

Canny()関数の構文

Canny( image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]] ) -> edges

引数

image 入力画像(8bitグレースケールのみ)
threshold1 エッジしきい値(小)
threshold2 エッジしきい値(大)
apertureSize Sobelフィルタ処理のカーネルのサイズ(3, 5, 7 のいずれか)
L2gradient X方向とY方向のSobleフィルタから、エッジの大きさを求める方法
Falseのとき:L1ノルム(X方向の絶対値とY方向の絶対値の和)(初期値)
Trueのとき:L2ノルム(X方向の2乗とY方向の2乗の和の平方根)


戻り値

edges Canny edge detectionの処理画像

サンプルプログラム

Canny edge detection処理のサンプルプログラムを示します。

import cv2

# グレースケールで画像を読み込む
src_img = cv2.imread("image.bmp", cv2.IMREAD_GRAYSCALE)
# Canny edge detection
canny_img = cv2.Canny(src_img, 100, 200)

# 入力画像とCanny edge detectionの処理画像の表示
cv2.imshow("Image", src_img)
cv2.imshow("Canny", canny_img)
cv2.waitKey()

処理結果

 

Cannyのしきい値の決め方

Cannyで処理するエッジのしきい値(threshold1, threshold2)はしきい値(小)としきい値(大)説明しましたが、これだけだと少々わかりづらいかと思いますが、このしきい値は、L2gradientの設定を初期状態で行うと、X方向のSobelフィルタとY方向のSobelフィルタ処理の結果をそれぞれの絶対値の和の画像に対して、非極大抑制を行った画像に対するヒステリシスしきい値の小さい方のしきい値と大きい方のしきい値になります。

と、説明すると余計に分からないw

しきい値の意味合い的には

threshold1 エッジの候補としたいエッジ強度のしきい値(エッジが多めに残る)
threshold2 必ずエッジとして残したい部分のエッジ強度のしきい値(エッジが少なめに残る)

Cannyで処理を行った画像は、threshold2で抽出したエッジと、threshold2のエッジとつながっているthreshold1のエッジを残すようになります。

threshold1、threshold2の値は255を超えた値も有効である事に注意してください。

 

現実的には、どの程度のしきい値が最適なのか?わかりづらいので、threshold1とthreshold2の値を同じ値で、しきい値(小)の値と、しきい値(大)の値のそれぞれのCanny処理後の画像を確認し、最適な値を見つけると良いかと思います。

 

処理例のプログラム

/import cv2

# グレースケールで画像を読み込む
src_img = cv2.imread("image.bmp", cv2.IMREAD_GRAYSCALE)

# しきい値(小)の画像
threshold1 = 100
canny_thre1 = cv2.Canny(src_img, threshold1, threshold1)
# しきい値(大)の画像
threshold2 = 500
canny_thre2 = cv2.Canny(src_img, threshold2, threshold2)

# Cannyフィルタの画像
canny_img = cv2.Canny(src_img, threshold1, threshold2)

# 入力画像とCanny edge detectionの処理画像の表示
cv2.imshow("Image", src_img)
cv2.imshow("threshold1", canny_thre1)
cv2.imshow("threshold2", canny_thre2)
cv2.imshow("Canny", canny_img)

cv2.waitKey()

処理結果

thireshold1(しきい値小)のエッジの中から、thireshold2(しきい値大)のエッジに繋がっている部分を残した画像がCanny処理後の画像になります。

 

参考ページ

Canny edge detectionの処理アルゴリズム

【OpenCV-Python】Sobel(エッジ検出)

【OpenCV-Python】Webカメラから画像をキャプチャして保存する

OpenCVでWebカメラ(USBカメラ)から画像をキャプチャして保存する方法を紹介します。

必要なもの

●Python

●OpenCV(pip install opencv-pythonでOpenCVをインストールします)

●Webカメラ(USBカメラ)

 

サンプルプログラム

OpenCVでカメラから画像を取得し、画像ファイルに保存するサンプルプログラムを以下に示します。

import cv2

# カメラを開く
cap = cv2.VideoCapture(0)

# 画像をキャプチャする
ret, frame = cap.read()

# 画像を保存する
cv2.imwrite("image.jpg", frame)

# カメラを閉じる
cap.release()

これで、Webカメラから画像を取得し、 image.jpg というファイル名で画像が保存されます。

 

次に各コードの説明をします。

 

VideoCapture()

cap = cv2.VideoCapture(0)

の部分で、カメラを開いています。

関数の引数で渡している  は、最初に見つけたカメラを開く事を意味しています。

ほとんどの場合、0 で大丈夫だと思いますが、例えば、PC内臓のカメラのあるノートPCに、別途、USBカメラを刺して使う場合には、この番号を 1 や 2 に調整してみてください。

 

read()

ret, frame = cap.read()

この部分で、現在、撮影している画像データを取得します。

画像の取得し失敗すると ret は Falseになります。

失敗する原因としては、VideoCaptureの部分で指定する番号を間違えたとか、他のプログラムでカメラを使用していた場合などがあります。

frameが撮影した画像データとなります。

 

その他のサンプルを以下に示します。

 

画像をリアルタイムに表示するサンプル

import cv2

# カメラを開く
cap = cv2.VideoCapture(0)

while True:
    # 画像をキャプチャする
    ret, frame = cap.read()

    # 画像を表示する
    cv2.imshow("Image", frame)

    # `q`キーを押すとループを終了する
    if cv2.waitKey(1) == ord('q'):
        break

# カメラを閉じる
cap.release()
# すべてのウィンドウを閉じる
cv2.destroyAllWindows()

このサンプルで q キーを押すまで画像を取得し、表示します。

 

キーを押すたびに連番でファイルに保存するサンプル

import cv2

# カメラを開く
cap = cv2.VideoCapture(0)

indexNo = 0
while True:
    # 画像をキャプチャする
    ret, frame = cap.read()

    # 画像を表示する
    cv2.imshow("Image", frame)

    # `q`キーを押すとループを終了する
    if cv2.waitKey(1) == ord('q'):
        break
    elif cv2.waitKey(1) == ord('c'):
        cv2.imwrite(f"image{indexNo:05d}.jpg", frame)
        indexNo = indexNo + 1
# カメラを閉じる
cap.release()
# すべてのウィンドウを閉じる
cv2.destroyAllWindows()

このサンプルでは、一つ前のサンプルに加えて、 c  キーを入力するたびに、連番のjpegファイルに画像を保存します。

 

read()での画像の取得タイミングについて

ここからが、個人的に興味のあった部分。

画像を取得する際に、read()関数をwhile文で繰り返し呼んでいますが、カメラのフレームレートよりも速くread()関数が呼ばれている可能性が高いです。

このとき、read()関数は、呼ばれるたびに、即、画像データを取得しているのか?それとも、1フーレムの撮影が終わるのを待ってから、画像を取得しているのか?気になった。。

 

そこで、こんなプログラムを実行してみました。

import cv2
import time

# カメラを開く
cap = cv2.VideoCapture(0)

# 撮影開始時間
start = time.perf_counter()
for i in range(300):
    # 画像をキャプチャする
    ret, frame = cap.read()

# 画像300枚を撮影するのにかかった時間
print(time.perf_counter() - start)
# カメラを閉じる
cap.release()
# すべてのウィンドウを閉じる
cv2.destroyAllWindows()

このサンプルプログラムでは、30fpsのカメラを使って、300枚の画像を撮影しています。

 

実際に、プログラムを実行すると、300枚の画像を撮影するのにかっかった時間は、10.4秒という結果になりました。

30fpsのカメラで300枚撮影しているので、計算上は300枚撮影するのに10秒かかる事になり、ほぼ、理論通りの結果となりました。

このことから、read()関数が呼ばれたら、即、画像データを取得しているのではなく、カメラが1フレーム分、撮影が完了するのを待ってから画像を取得しているものと、推測されます。

少なくとも、read()関数を繰り返し呼んでも、同じ画像を重複して取得する事は無さそうです。

【OpenCV-Python】Tkinter GUI Sample

OpenCVのPython版でもC#のようなウィンドウのGUIプログラムを作りたい!

ということで、GUIにTkinterを使って、簡単なサンプルプログラムを作成しました。

 

ここで公開しているコードは自由に変更して使って頂いてもらって構いません。
どこかで公開する場合は、参照元を書いて頂けると嬉しいです。
エラー処理は甘めなので、自己責任でお願いします。

 

機能的には画像ファイル(日本語ファイルを含む)を開いてtkinterのCanvas上に画像を表示します。

画像は、マウス操作で上下左右の移動と、マウスホイールで拡大縮小が可能になっています。

マウスのダブルクリックで画像全体を表示します。

 

OpenCVの処理そのものは二値化とガウシアンフィルタのみの至ってシンプルなものなので、いろいろと追加してお試しください。

 

ソースコードはこちら↓

 

GitHubにもソースコードは上げておきました。

https://github.com/ImagingSolution/OpenCVTkinterGUISample

 

関連記事

【Python】画像ビューア(ズーム(拡大/縮小)、移動表示)

【Python/tkinter】ウィジェットの配置(pack)

【Python/tkinter】ウィジェットの配置(grid)

【Python/tkinter】Label(ラベル)

【Python/tkinter】Button(ボタン)

【Python/tkinter】Entry(テキストボックス)

【Python/tkinter】Menu(メニュー)

【Python/tkinter】Frame(フレーム)

【Python/tkinter】Canvas(キャンバス)の作成

【OpenCV/Python】日本語の画像ファイル読込・保存

【Python】画像データ(NumPy,Pillow(PIL))の相互変換

【OpenCV-Python】アフィン変換(同次座標系を使用)

アフィン変換については、こちら↓のページ

アフィン変換(平行移動、拡大縮小、回転、スキュー行列)

で、紹介していますが、回転や拡大縮小、平行移動などは3行3列の行列を使った同次座標系を用いるのが便利ですよ!

と言っているのですが、OpenCVでは、2行3列の行列を使ったアフィン変換となります。

アフィン変換では、平行移動だけ、回転移動だけ、拡大縮小だけ、などということも少なく、平行移動、回転、拡大縮小などのアフィン変換を組み合わせて、変換行列は行列の積で求めます。

しかしOpenCVで扱うアフィン変換は2行3列の行列なので、行列の積が計算できないので、個人的には少し不便に感じます。
特に平行移動がからむと、ちょっと難しくなります。

そこで、前半では標準的にOpenCVで出来る事を説明し、後半では3行3列の行列を使ったアフィン変換の方法を紹介したいと思います。

 

30°回転と0.8倍の縮小を行った例

import cv2
import affine

# 画像を読み込む
img = cv2.imread("image.bmp", cv2.IMREAD_UNCHANGED) 

height, width, _ = img.shape

# 回転行列の取得
affineMatrix = cv2.getRotationMatrix2D((width/2, height/2), 30, 0.8)
print(affineMatrix)

# アフィン変換
img = cv2.warpAffine(img, affineMatrix, (width, height))

# 画像の表示
cv2.imshow("Image", img)

# キー入力待ち(ここで画像が表示される)
cv2.waitKey()

実行結果

アフィン変換前 アフィン変換後

 

OpenCVによるアフィン変換

OpenCVでは2行3列の行列を指定して、画像をアフィン変換するwarpAffine()関数と、

回転の中心座標、回転角度、倍率を指定してアフィン変換行列(2行3列)を取得するgetRotationMatrix2D()関数、

変換前の3点の座標と、対応する変換後の3点の座標を指定してアフィン変換行列(2行3列)を取得するgetAffineTransform() 関数が用意されています。

 

warpAffine()

画像のアフィン変換を行います。

warpAffine( src, M, dsize[, dst[, flags[, borderMode[, borderValue]]]] ) -> dst
引数 説明
src 変換前の画像データ
M 2行3列のアフィン変換行列
dsize 出力画像のサイズ (幅, 高さ)
dst borderMode = cv2.BORDER_TRANSPARENTのとき、背景画像を指定します。
ただし、dsizeとdstの画像サイズは同じにする必要があります。
flags 補間方法を指定します。
cv2.INTER_NEAREST
cv2.INTER_LINEAR
cv2.INTER_CUBIC
cv2.INTER_AREA
cv2.INTER_LANCZOS4
borderMode ボーダー(画像からはみ出した部分)の設定を行います。
cv2.BORDER_CONSTANT (初期値)
cv2.BORDER_REPLICATE
cv2.BORDER_REFLECT
cv2.BORDER_WRAP
cv2.BORDER_TRANSPARENT
borderValue borderModeにcv2.BORDER_CONSTANTを指定したときの画像のはみ出した部分の輝度値を指定します。 初期値:0
(戻り値)dst アフィン変換後の画像データ

 

補間モード(flag)について

cv2.INTER_NEAREST ニアレストネイバー
cv2.INTER_LINEAR バイリニア
cv2.INTER_CUBIC バイキュービック
cv2.INTER_AREA 縮小のときにモアレが発生しないモード
公式ドキュメントには拡大のときはINTER_NEARESTと同じと書いてありますが、実際にやってみると、INTER_LINEARと同じと思われます。
cv2.INTER_LANCZOS4 ランチョス

(元画像)目の部分を拡大したときの比較

INTER_NEAREST INTER_LINEAR INTER_CUBIC
INTER_AREA INTER_LANCZOS4

個人的な使い分けですが、画像処理結果を画像で見たいときはINTER_NEAREST

画像処理の画像データとして、拡大縮小したいときはINTER_LINEAR

写真のように見た目が大事なときはINTER_CUBIC

と、することが多いです。

(参考)

画素の補間(Nearest neighbor,Bilinear,Bicubic)の計算方法

 

borderMode(画像の外側の表示方法)について

cv2.BORDER_CONSTANT borderValueで指定した色で埋めます。(初期値は黒)
cv2.BORDER_REPLICATE 画像の一番外側の色で埋めます。
cv2.BORDER_REFLECT 画像を上下、左右方向にミラー反転して画像で繰り返し埋めます。
cv2.BORDER_WRAP 画像で繰り返し埋めます。
cv2.BORDER_TRANSPARENT dstに出力画像と同じ大きさの画像を指定すると、画像の外側は透過して、画像の上に画像を配置します。
BORDER_CONSTANT BORDER_REPLICATE BORDER_REFLECT
BORDER_WRAP BORDER_TRANSPARENT

 

getRotationMatrix2D()

基点(中心)周りに回転、拡大縮小を行うアフィン変換行列(2行3列)を取得します。

取得する行列は以下のようになります。

$$\begin{bmatrix}\alpha & \beta & (1-\alpha)\cdot center.x-\beta \cdot center.y \\-\beta & \alpha & \beta \cdot center.x + (1-\alpha )\cdot center.y \end{bmatrix}$$

ただし、

$$\begin{cases}\alpha=scale\cdot cos(angle)\\ \beta =scale\cdot sin(angle)\end{cases}$$

getRotationMatrix2D( center, angle, scale ) -> retval
引数 説明
center 回転、拡大縮小の基点(中心)となる(x,y)座標
angle 回転角度を度で指定します。 反時計周りが正
scale 拡大縮小する倍率を指定します。
(戻り値)retval (x,y)座標を基点に回転、拡大縮小したときの2行3列のアフィン変換行列を取得します。

 

getAffineTransform()

アフィン変換前の3点の座標と、それに対応したアフィン変換後の3点の座標を指定して、アフィン変換行列を求まます。

getPerspectiveTransform( src, dst) -> retval
引数 説明
src アフィン変換前の3点の(x,y)座標
dst アフィン変換前の点に対応したアフィン変換後の3点の(x,y)座標
(戻り値)retval 2行3列のアフィン変換行列

 

3行3列の行列を使ったアフィン変換

OpenCVでは、2行3列の行列を用いてアフィン変換を行うので、同次座標系の特徴でもある、平行移動も含めて行列の積でアフィン変換の行列を求めることができません。

そこで、回転、拡大縮小、平行移動の3行3列の行列は自作で作って、実際に画像をアフィン変換する部分はwarpAffine()関数のアフィン変換行列の部分に3行3列の行列をスライスして2行3列の行列として渡す方針でやってみたいと思います。

3行3列のアフィン変換行列を求める部分はファイル(affine.py)にまとめました。

# affine.py

import cv2
import numpy as np

def scaleMatrix(scale):
    '''拡大縮小用アフィン変換行列の取得(X方向とY方向同じ倍率)'''
    mat = identityMatrix() # 3x3の単位行列
    mat[0,0] = scale
    mat[1,1] = scale

    return mat

def scaleXYMatrix(sx, sy):
    '''拡大縮小用アフィン変換行列の取得(X方向とY方向の倍率をぞれぞれ指定)'''
    mat = identityMatrix() # 3x3の単位行列
    mat[0,0] = sx
    mat[1,1] = sy

    return mat

def translateMatrix(tx, ty):
    '''平行移動用アフィン変換行列の取得'''
    mat = identityMatrix() # 3x3の単位行列
    mat[0,2] = tx
    mat[1,2] = ty

    return mat

def rotateMatrix(deg):
    '''回転用アフィン変換行列の取得'''
    mat = identityMatrix() # 3x3の単位行列
    rad = np.deg2rad(deg) # 度をラジアンへ変換
    sin = np.sin(rad)
    cos = np.cos(rad)

    mat[0,0] = cos
    mat[0,1] = -sin
    mat[1,0] = sin
    mat[1,1] = cos

    return mat

def scaleAtMatrix(scale, cx, cy):
    '''点(cx, cy)を基点とした拡大縮小用アフィン変換行列の取得'''

    # 基点の座標を原点へ移動
    mat = translateMatrix(-cx, -cy)
    # 原点周りに拡大縮小
    mat = scaleMatrix(scale).dot(mat)
    # 元の位置へ戻す
    mat = translateMatrix(cx, cy).dot(mat)

    return mat

def rotateAtMatrix(deg, cx, cy):
    '''点(cx, cy)を基点とした回転用アフィン変換行列の取得'''

    # 基点の座標を原点へ移動
    mat = translateMatrix(-cx, -cy)
    # 原点周りに回転
    mat = rotateMatrix(deg).dot(mat)
    # 元の位置へ戻す
    mat = translateMatrix(cx, cy).dot(mat)

    return mat

def afiinePoint(mat, px, py):
    '''点(px, py)をアフィン変換行列(mat)で変換した後の点を取得'''

    srcPoint = np.array([px, py, 1])

    return mat.dot(srcPoint)[:2]

def inverse(mat):
    '''行列の逆行列を求める'''
    return np.linalg.inv(mat)

def identityMatrix():
    '''3x3の単位行列を取得'''
    return np.eye(3, dtype = np.float32) # 3x3の単位行列

使い方としては、上記のプログラムをaffine.pyというファイルに保存して、使う側のプログラムと同一フォルダに配置し、

import affine

のようにすれば、使えるようになります。

例えば、OpenCVのgetRotationMatrix2D()関数で行っていることは、

中心座標を原点へ平行移動

拡大縮小

回転(マイナス方向)

原点から中心へ平行移動

となっていて、この順番でアフィン変換の行列の積を行えば、行列が求まります。

getRotationMatrix2D()関数と同じアフィン変換行列を求めるサンプル

import cv2
import affine # affine.pyファイルを同一フォルダに置くこと

# 中心座標
cx = 100
cy = 5
# 回転角度
angle = 30
# 拡大縮小
scale = 0.8

# アフィン変換行列を求める
matAffine = affine.translateMatrix(-cx, -cy)                # 原点へ平行移動
matAffine = affine.scaleMatrix(scale).dot(matAffine)        # 拡大縮小
matAffine = affine.rotateMatrix(-angle).dot(matAffine)      # 回転
matAffine = affine.translateMatrix(cx, cy).dot(matAffine)   # 中心へ戻す
print(matAffine)
#[[ 0.6928203  0.4       28.717972 ]
# [-0.4        0.6928203 41.5359   ]
# [ 0.         0.         1.       ]]

matAffine_cv = cv2.getRotationMatrix2D((cx, cy), angle, scale)
print(matAffine_cv)
#[[ 0.69282032  0.4        28.7179677 ]
# [-0.4         0.69282032 41.53589838]]

あとは、ここで求めたアフィン変換の行列を2行3列の行列にスライスして、warpAffine()関数へ渡せば、画像のアフィン変換ができます。

img = cv2.warpAffine(img, affineMatrix[:2,], (width, height))

 

例題)3x3画素の画像を100倍に拡大する

画像を拡大するとき注意が必要なのが、画素の中心が、小数点以下が0になる座標(X.0, X.0)だという事に注意してください。詳しくは以下のページを参照ください。

画像の拡大

この事を知らずに、OpenCVのgetRotationMatrix2D()関数で3×3画素の市松模様の画像の拡大を行うと・・・

import numpy as np
import cv2

# 画像データ
img = np.array(
    [[0,   255,   0],
     [255,   0, 255],
     [0,   255,   0],
    ],
    dtype = np.uint8
    )

# OpenCVでアフィン変換行列を求める
matAffine = cv2.getRotationMatrix2D(center=(0,0), angle=0, scale=100)

# アフィン変換
img = cv2.warpAffine(img, matAffine, (300, 300), flags = cv2.INTER_NEAREST)

# 画像の表示
cv2.imshow("Image", img)

# キー入力待ち(ここで画像が表示される)
cv2.waitKey()

実行結果

上図のように左上の画素の中心に拡大されるため、ズレた画像になってしまいます。

この100倍の拡大を3行3列のアフィン変換を使って行うには、どのように考えるかというと、

(+0.5, +0.5)の平行移動(画像の角を原点に合わせる)

100倍の拡大

(-0.5, -0.5)の平行移動(左上の画素の中心を原点に合わせる)

 

というように変換を行います。

import numpy as np
import cv2
import affine # afiine.py のファイルが同一フォルダにあること

# 画像データ
img = np.array(
    [[0,   255,   0],
     [255,   0, 255],
     [0,   255,   0],
    ],
    dtype = np.uint8
    )

# OpenCVでアフィン変換行列を求める
matAffine = affine.translateMatrix(0.5, 0.5)            # 平行移動
matAffine = affine.scaleMatrix(100).dot(matAffine)      # 100倍の拡大
matAffine = affine.translateMatrix(-0.5, -0.5).dot(matAffine) # 原点の位置へ移動

# アフィン変換
img = cv2.warpAffine(img, matAffine[:2,], (300, 300), flags = cv2.INTER_NEAREST)

# 画像の表示
cv2.imshow("Image", img)

# キー入力待ち(ここで画像が表示される)
cv2.waitKey()

実行結果

このサンプルのソースコードはここ↓へ置きましたので、ご自由にお使いください。

OpenCVAffineSample.zip

 

関連記事

アフィン変換(平行移動、拡大縮小、回転、スキュー行列)

画像の拡大

画素の補間(Nearest neighbor,Bilinear,Bicubic)の計算方法

任意点周りの回転移動(アフィン変換)

【Python/NumPy】座標からアフィン変換行列を求める方法

【OpenCV-Python】輪郭の周囲長(arcLength)

findContours()関数などで取得した輪郭の座標から輪郭の長さを求めるにはarcLength()関数を用います。

構文

arcLength(curve, closed) ->retval
curve 輪郭を構成する輪郭のxy座標(x, y)の配列
closed 始点と終点が閉じた周囲長を求めるときはTrue
閉じていない周囲長を求めるときはFalse
(戻り値)retval 周囲長

 

サンプルプログラム

通常はfindContours()関数で取得した輪郭の座標を用いる場合が多いかと思いますが、今回は簡単のため、4点からなる周囲長を求めます。

この輪郭座標の閉じた周囲長(cloased = True)と、閉じていない周囲長(cloased = False)を求めます。

閉じた周囲長(cloased = True)

閉じていない周囲長(cloased = False)

import cv2
import numpy as np

# 輪郭の座標
contour = np.array(
    [
     [1, 2],
     [1, 4],
     [5, 4],
     [5, 2]
    ],
    dtype = np.int32
    )

# 閉じた周囲長
perimeter = cv2.arcLength(contour, True) 
print(perimeter) # 12.0

# 閉じていない周囲長
perimeter = cv2.arcLength(contour, False) 
print(perimeter) # 8.0

関連記事

【OpenCV-Python】findContoursによる輪郭検出

【OpenCV-Python】輪郭(contour)の矩形領域の取得

OpenCVのfindContours関数などで得られた点の座標から、点を囲う矩形領域(四角形の領域)を取得するにはboundingRect関数を用います。

さらに、傾きを考慮した矩形領域を取得するにはminAreaRect関数を用います。

まずは、両方の関数を用いたサンプルを示します。

import cv2
import numpy as np

# 8ビット1チャンネルのグレースケールとして画像を読み込む
img = cv2.imread("sample.bmp", cv2.IMREAD_GRAYSCALE) 

contours, hierarchy = cv2.findContours(
    img, 
    cv2.RETR_EXTERNAL,      # 一番外側の輪郭のみを取得する 
    cv2.CHAIN_APPROX_NONE   # 輪郭座標の省略なし
    ) 

# 画像表示用に入力画像をカラーデータに変換する
img_disp = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)

# 輪郭の点の描画
for i, contour in enumerate(contours):
    # 輪郭を描画
    cv2.drawContours(img_disp, contours, i, (255, 0, 0), 2)

    # 傾いていない外接する矩形領域
    x,y,w,h = cv2.boundingRect(contour)
    cv2.rectangle(img_disp,(x,y),(x+w-1,y+h-1),(0,255,0),2)

    # 傾いた外接する矩形領域
    rect = cv2.minAreaRect(contour)
    box = cv2.boxPoints(rect)
    box = np.intp(box)
    cv2.drawContours(img_disp,[box],0,(0,0,255), 2)

# 画像の表示
cv2.imshow("Image", img_disp)

# キー入力待ち(ここで画像が表示される)
cv2.waitKey()

実行結果

緑の線がboundingRect関数を用いて取得した領域、赤の線がminAreaRect関数を用いた領域となります。

 

矩形領域の取得(boundingRect関数)

boundingRect( array ) -> x, y, width, height
array (x,y)座標の配列 もしくは グレースケールの画像を指定します。
(戻り値) x 取得した矩形領域の左上のx座標を取得します。
(戻り値) y 取得した矩形領域の左上のy座標を取得します。
(戻り値) width 取得した矩形領域の幅を取得します。
(戻り値) height 取得した矩形領域の高さを取得します。

注意点

戻り値で取得した幅(width)および高さ(height)の値は、実際の幅、高さ(2点間の距離)の +1 の値を取得します。

例えば、下図のように、(1, 2), (5, 2), (5, 4), (1, 4)の点を囲う矩形領域を取得すると、

x = 1

y = 2

width = 5

height = 3

となります。

import cv2
import numpy as np

contour = np.array(
    [
     [1, 2],
     [5, 2],
     [5, 4],
     [1, 4]
    ],
    dtype = np.int32
    )

# 傾いていない外接する矩形領域
ret = cv2.boundingRect(contour)

print(ret) # (1, 2, 5, 3)

傾きを考慮した矩形領域の取得(minAreaRect関数)

minAreaRect( points) -> rect
points (x,y)座標の配列
(戻り値) rect 傾いた矩形領域の
中心のxy座標 (x, y)
矩形の幅、高さ(width, height)
矩形の回転角度
のタプルで取得します。

 

点(1, 1), (0, 2), (4, 4), (5, 3) を囲う傾きを考慮したときの最小となる矩形領域の取得のサンプルを以下に示します。

import cv2
import numpy as np

contour = np.array(
    [
     [1, 1],
     [0, 2],
     [4, 4],
     [5, 3]
    ],
    dtype = np.int32
    )

# 傾いた外接する矩形領域
rect = cv2.minAreaRect(contour) # rectは中心座標(x,y), (width, height), 回転角度
cnter, size, rot = rect

print(rect) # ((2.500000238418579, 2.5), (4.919349670410156, 1.3416407108306885), 26.56505012512207)

関連記事

【OpenCV-Python】findContoursによる輪郭検出

【OpenCV-Python】矩形抽出(矩形度)

【OpenCV-Python】findContoursによる輪郭検出

OpenCV(Python)で二値化された画像中の白の部分の外側の輪郭のデータを取得するにはfindContours()関数を用います。
黒の部分の輪郭は、白の部分の内側の輪郭という認識になります。

findContours()関数で取得できる情報は、輪郭を構成している点の座標群と輪郭の内側、外側の情報となります。

さらに、これらの情報を用いて、輪郭の長さや、輪郭の内側の面積、重心、輪郭部分の矩形領域などを取得できる関数も別途、用意されています。

処理例

(入力画像)

(処理結果)

※結果は画像データとしては得られず、上図の色付きの線で書いた部分の情報を取得します。

 

構文

findContours(image, mode, method[, contours[, hierarchy[, offset]]]) ->contours, hierarchy
image 輪郭抽出する画像データ
mode 輪郭構造の取得方法
method 輪郭座標の取得方法
(戻り値)contours 輪郭座標
(戻り値)hierarchy 輪郭の階層情報

 

image

輪郭抽出する画像データを指定します。

画像は8bitのグレースケールで、基本的に二値化された画像を指定します。

一部、32bit1チャンネルのデータ(ラベリングデータ)も指定可能です。

二値化されていない場合は、輝度値が0で無い部分に関しての輪郭情報を取得します。

mode

輪郭の階層情報の取得方法をRETR_EXTERNAL, RETR_LIST, RETR_CCOMP, RETR_TREE, RETR_FLOODFILLの中から指定します。

最初に見つける輪郭が白の外側の輪郭となります。

そのため、入力画像は黒色背景の画像を指定します。

白色背景の画像を指定した場合、最初の輪郭は画像全体となります。

modeで指定した輪郭情報は戻り値のhierarchyに格納されます。

●RETR_TREE

輪郭の階層情報をツリー形式で取得します。

この輪郭の階層情報をというのは、下図を用いて説明します。

まず、一番外側の輪郭(白の領域の外側の輪郭)にがあります。

の内側の輪郭にはがあり、の内側にはがあります。

さらに、

の内側にはがあり、の内側には

の内側には

の内側には

の輪郭があります。

この構造をツリーで表現すると、このようになります。

modeRETR_TREEを指定するとhierarchyの中にこの階層構造が格納されます。

 

●RETR_CCOMP

白の輪郭(外側の輪郭)と黒の輪郭(内側の輪郭)の情報だけを取得し、白の輪郭のさらに内側の輪郭かどうか?の情報は失われます。

この構造をツリーで表現すると、このようになります。

●RETR_LIST

白(外側)、黒(内側)の区別なく、すべての輪郭を同じ階層として取得します。

この構造をツリーで表現すると、このようになります。

●RETR_EXTERNAL

一番外側の白の輪郭だけを取得します。

この構造をツリーで表現すると、このようになります。

method

輪郭を構成する点の座標の取得方法をCHAIN_APPROX_NONE, CHAIN_APPROX_SIMPLE, CHAIN_APPROX_TC89_L1, CHAIN_APPROX_TC89_KCOSの中から指定します。

取得した座標は戻り値のcontoursに格納されます。

 

●CHAIN_APPROX_NONE

輪郭上のすべての座標を取得します。

●CHAIN_APPROX_SIMPLE

縦、横、斜め45°方向に完全に直線の部分の輪郭の点を省略します。

 

●CHAIN_APPROX_TC89_L1, CHAIN_APPROX_TC89_KCOS

輪郭の座標を直線で近似できる部分の輪郭の点を省略します。

 

(戻り値)contours

輪郭上の座標を取得します。

実際にどのような座標を取得するかは、methodの設定により異なります。

contoursのデータは以下のような配列となります。

contours[輪郭番号][点の番号][0][X座標, Y座標]

 

(戻り値)hierarchy

輪郭の階層情報を取得します。

取得する構造はmodeの設定により異なります。

contoursのデータは以下のような配列となります。

hierarchy[0][輪郭番号][次の輪郭番号, 前の輪郭番号, 最初子供(内側)の輪郭番号, 親(外側)の輪郭番号]

となります。

下図の例について説明します。

輪郭番号の親はで、最初の子供の番号はとなります。

の次の輪郭番号はで、の前の輪郭番号はとなります。

該当する輪郭番号が無い時は -1 になります。

上図の階層情報の例で言うと、

hierarchy[0][0] は [ 3, -1,  1, -1]
hierarchy[0][1] は [-1, -1,  2,  0]
hierarchy[0][2] は [-1, -1, -1,  1]
hierarchy[0][3] は [-1,  0,  4, -1]
hierarchy[0][4] は [-1, -1,  5,  3]
hierarchy[0][5] は [ 6, -1, -1,  4]
hierarchy[0][6] は [-1,  5,  7,  4]
hierarchy[0][7] は [-1, -1,  8,  6]
hierarchy[0][8] は [ 9, -1, -1,  7]
hierarchy[0][9] は [-1,  8, -1,  7]

となります。

 

輪郭座標(contours)の注意点

contoursで取得できる座標は、白の領域を構成する外側の座標、もしくは内側の座標となります。

例えば、下図のような図形の輪郭座標を取得するとき、

白の輪郭を取得するときは、一番、外側の画素の座標(下図の赤い点)がcontoursに格納されます。

しかし、白の内側の黒の輪郭座標は、黒の画素の座標にはなりません

黒の輪郭座標は、輪郭を構成している黒の画素の4近傍の白の画素の座標となります。
黒の輪郭座標は下図の青い点の座標となります。

輪郭の描画

findContours()関数で取得した輪郭(contours)はdrawContours()関数を用いて描画します。

drawContours(image, contours, contourIdx, color[, thickness[, lineType[, hierarchy[, maxLevel[, offset]]]]]) ->image
image 描画先の画像データ
contours 輪郭座標
contourIdx 描画する輪郭の番号
-1にすると、すべての輪郭を描画
color 描画の色を(b, g, r)のタプルで指定します
thickness 線幅
-1を指定すると塗りつぶしになります。
lineType 線の描画スタイルをFILLED, LINE_4, LINE_8, LINE_AAの中から指定します。
hierarchy 輪郭の階層情報を指定します。
maxLevel 輪郭をどの階層まで表示するかを指定します。
0のとき、最初の輪郭のみ
1以降は数値の階層の輪郭すべてが表示されます。
offset

 

サンプルプログラム

import cv2

# 8ビット1チャンネルのグレースケールとして画像を読み込む
img = cv2.imread("sample.bmp", cv2.IMREAD_GRAYSCALE) 

contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE ) 

# 画像表示用に入力画像をカラーデータに変換する
img_disp = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)

# 全ての輪郭を描画
cv2.drawContours(img_disp, contours, -1, (0, 0, 255), 2)

# 輪郭の点の描画
for contour in contours:
    for point in contour:
        cv2.circle(img_disp, point[0], 3, (0, 255, 0), -1)

cv2.imshow("Image", img_disp)

# キー入力待ち(ここで画像が表示される)
cv2.waitKey()

(実行結果)

 

関連記事

【OpenCV-Python】輪郭(contour)の面積(contourArea)

【OpenCV-Python】輪郭(contour)の矩形領域の取得

【OpenCV-Python】輪郭の周囲長(arcLength)

【参考文献】OpenCVではじめよう ディープラーニングによる画像認識

4月8日、OpneCVではじめよう ディープラーニングによる画像認識 という本のKindle版が先行して発売されました。(単行本は4月30日発売)

 

この本はOpenCV界隈では有名な二人(実名なので認識しづらいですが)がタッグを組んで書かれた本なので、買わない訳にはいきませんww

この本の対象者としては、PythonのOpenCVで画像処理をしてきて、ディープラーニングの処理もOpenCVで動かしてみたい!という人向けです。

 

Pythonの文法やディープラーニング処理のアルゴリズム的な部分は書かれていません。

逆にディープラーニング処理を行うために必要になるOpenCV処理の説明などは割と細かく書かれています。

おそらく実際に手を動かして確認したであろう内容も含まれているので、その辺はOpenCVをやった事のある人でも参考になると思います。

全体的には、OpenCVに関する内容が半分、残りがディープラーニングに関する内容になっています。

 

サンプルプログラムもソースコードはgithubから、学習済みモデルは出版社のページからダウンロードできるので、比較的簡単にお試しができると思います。

本に書かれているサンプルを使って、こんなプログラムを簡単に動かす事ができました。

ただ、ファイルの読み込み時にフルパス設定になっていたので、パスの中に日本語が入っていてファイルが読み込めないというエラーに少しはまりました。→パスを相対パスにして動作させました。

 

私はKindle版を購入しましたが、参考文献などのリンクは、Kindleからリンクを貼ってあるので便利でした。

Kindle版をPCで見るときは、フォントのサイズやページの幅を調整すると見やすくなります。

 

目次

1章 OpenCVとは
1.1 OpenCVの概要
1.2 主要モジュール
1.3 OSSライセンス
1.4 サポート言語
1.5 サポートプラットフォーム
1.6 高速化
1.7 開発の歴史
1.8 ブランチ管理
1.9 各種サイト

2章 OpenCVインストール
2.1 OpenCVのインストール
2.2 pip
2.3 Miniconda
2.4 Docker
2.5 ソースコード

3章 coreモジュール
3.1 基本処理
3.2 ピクセルごとの操作
3.3 統計情報の取得
3.4 画像の分割、結合
3.5 ユーティリティ

4章 imgprocモジュール
4.1 画像の前処理と後処理
4.2 色の変換
4.3 しきい値処理(2値化)
4.4 画像の幾何変換

5章 imgcodecs、Videoioモジュール
5.1 画像の読み込み
5.2 画像の書き込み

6章 ディープラーニング
6.1 ディープラーニングの概要
6.2 精度評価
6.3 画像処理におけるディープラーニング

7章 dnnモジュール基礎
7.1 dnnモジュールの概要
7.2 顔検出
7.3 オブジェクト検出
7.4 クラス分類
7.5 セグメンテーション
7.6 テキスト検出とテキスト認識
7.7 キーポイント検出

8章 dnnモジュール応用
8.1 dnnモジュールの対応レイヤ
8.2 カスタムレイヤ対応
8.3 学習済みモデルの診断ツール(model-diagnostics)
8.4 外部DNNフレームワーク連携
8.5 学習済みモデルの利用

【OpenCV/Python】日本語の画像ファイル読込・保存

OpenCVで画像ファイルを開くとき、ファイル名やパスに日本語が含まれていると、画像ファイルを開いてくれません。

試しに以下のようなコードを実行すると、エラーが起き実行できません。

import cv2

# OpenCVで画像ファイルを開く(ファイル名が日本語)
img = cv2.imread("画像ファイル.bmp", cv2.IMREAD_UNCHANGED)

cv2.imshow("Image", img)
cv2.waitKey(0)

エラー情報

Message=OpenCV(4.5.3) C:\Users\runneradmin\AppData\Local\Temp\pip-req-build-sn_xpupm\opencv\modules\imgproc\src\color.cpp:182: 
error: (-215:Assertion failed) !_src.empty() in function 'cv::cvtColor'

エラーそのものはimread関数で画像ファイルを開くのに失敗し、戻り値の画像データ(img)が空(None)になっているため、エラーが発生しています。

日本語の画像ファイルを開くためには、PillowもしくはNumPyで画像ファイルを開いてOpenCVの画像データであるNumPyのndarray形式に変換すれば、OpenCVで日本語の画像ファイルを扱えるようになります。

Pillowで画像ファイルを開き、OpenCV(NumPyのndarray)に変換する

import cv2
import numpy as np
from PIL import Image

# Pillowで画像ファイルを開く
pil_img = Image.open("画像ファイル.bmp")
# PillowからNumPyへ変換
img = np.array(pil_img)
# カラー画像のときは、RGBからBGRへ変換する
if img.ndim == 3:
    img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)

cv2.imshow("Image", img)
cv2.waitKey(0)

Pillowで画像ファイルを開くと、カラー画像の場合、RGBの順でデータが格納されるので、cvtColorを用いて、OpenCVの形式(BGR)へ変換します。

PillowからNumPyの画像データへ変換する方法は下記ページを参照ください。

【Python】画像データ(NumPy,Pillow(PIL))の相互変換

NumPyで画像ファイルを開き、OpenCV(NumPyのndarray)に変換する

import cv2
import numpy as np
from PIL import Image

import time

start = time.perf_counter()

# NumPyで画像ファイルを開く
buf = np.fromfile("画像ファイル.bmp", np.uint8)
img = cv2.imdecode(buf, cv2.IMREAD_UNCHANGED)

print((time.perf_counter() - start) * 1000)#, "msec")

cv2.imshow("Image", img)
cv2.waitKey(0)

NumPyのfromfileで画像ファイルをバイナリで開き、ファイルの中身をメモリ(buf)に格納します。

OpenCVのimdecodeでメモリ上の画像データをOpenCVの画像データ(NumPyのndarray)に変換します。

OpenCVからPillowへ変換し画像ファイルに保存する

import cv2
from PIL import Image

# OpenCVで画像ファイルを開く(ファイル名に日本語が無い場合)
img = cv2.imread("image_file.bmp", cv2.IMREAD_UNCHANGED)

# カラー画像のときは、BGRからRGBへ変換する
if img.ndim == 3:
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# NumPyからPillowへ変換
pil_image = Image.fromarray(img)
# Pillowで画像ファイルへ保存
pil_image.save("画像ファイル_pillow.bmp")

Pillowで画像ファイルを開いたときの逆の事をすると、日本語のファイル名でも画像ファイルに保存することができます。

OpenCVの画像データ(ndarray)を画像形式に変換しファイルに保存する

import cv2
import numpy as np

# OpenCVで画像ファイルを開く(ファイル名に日本語が無い場合)
img = cv2.imread("image_file.bmp", cv2.IMREAD_UNCHANGED)

# 画像データを画像ファイル形式のメモリ変換する
ret, buf = cv2.imencode(".bmp", img)
# NumPyで画像ファイルへ保存
with open("画像ファイル_numpy.bmp", mode='w+b') as f:
    buf.tofile(f)

OpenCVの画像データ(NumPyのndarray)をimencodeで画像ファイル形式にメモリ上で変換し、NumPyのtofileで画像ファイルに書き込みます。

まとめ

日本語の画像ファイルの読込・保存をするのにPillowおよびNumPyを介して処理を行いましたが、それぞれの処理時間を比較しました。

処理時間は8192×8192画素のカラー画像を5回処理したときの平均時間です。

方法 平均処理時間(msec)
Pillowで画像ファイルを開く 513
NumPyで画像ファイルを開く 210
Pillowで画像ファイルへ保存 337
NumPyで画像ファイルへ保存 1106

これを見ると、日本語ファイル名の画像を開くときは、NumPy、保存するときはPillowを使った方が速い結果になりました。

NumPyで保存するときが極端に遅かったので、関数ごとの処理時間を見てみたところ、imencodeに約100msec、tofileに約1000msecという時間でした。

画像の読込と保存でPillowとNumPyを使い分けるのは、少々面倒ですが、処理時間もそれなりに違うので、画像の読込はNumPy、保存はPillowを使った方がいいかもしれません。

【Python/tkinter】OpenCVのカメラ動画をCanvasに表示する

USBカメラなどで取得した画像(動画)をOpenCVの cv2.imshow() で表示するには比較的簡単に表示する事ができますが、tkinterを使ってWindow付でCanvasに表示したい場合には、少しコツが必要になります。

そこで、tkinterのCanvasに動画を表示する方法を紹介します。

OpenCVでUSBカメラの動画を表示

USBカメラの動画をOpenCVのの cv2.imshow() で表示するプログラムは、以下のようにすれば表示されます。

import cv2

# カメラをオープンする
capture = cv2.VideoCapture(0)

# カメラがオープン出来たか?
camera_opened = capture.isOpened()

while camera_opened:

    # フレーム画像の取得
    ret, frame = capture.read()
    
    # 画像の表示
    cv2.imshow("Image", frame)

    if cv2.waitKey(1) != -1:
        # キー入力で終了
        break

capture.release()
cv2.destroyAllWindows()

tkinterのCanvasに動画を表示

tkinterのCanvasに動画を表示する場合は、OpenCVの動画表示プログラムの cv2.imshow() の部分を書き換えて表示しようとしても動画が表示されません。

これは、while文中でCanvasへ画像を表示しようとすると、画像の更新スレッドがブロックされた状態となってしまい、画像を更新するには、少しの時間、スレッドを空ける必要があります。

このスレッドを空ける処理には after() 関数を使います。

after() 関数では、指定した時間を待ってから、指定した関数を実行することができます。

処理の意味合い的には sleep() 関数とも似ていますが、sleep() 関数では、指定時間分、スレッドをブロックしてしまいますが、 after() 関数ではスレッドがブロックされません。

それらを考慮し、tkinterのCanvas上にUSBカメラの動画を表示したプログラムが以下になります。

 

以下のサンプルプログラムでは、Camvas上をマウスの左ボタンをクリックすると、画像の取得が開始/停止するようになっています。

import tkinter as tk
from tkinter import filedialog
from PIL import Image, ImageTk, ImageOps  # 画像データ用

import cv2

class Application(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)
        self.pack()

        self.master.title("OpenCVの動画表示")       # ウィンドウタイトル
        self.master.geometry("400x300")     # ウィンドウサイズ(幅x高さ)
        
        # Canvasの作成
        self.canvas = tk.Canvas(self.master)
        # Canvasにマウスイベント(左ボタンクリック)の追加
        self.canvas.bind('<Button-1>', self.canvas_click)
        # Canvasを配置
        self.canvas.pack(expand = True, fill = tk.BOTH)

        # カメラをオープンする
        self.capture = cv2.VideoCapture(0)

        self.disp_id = None

    def canvas_click(self, event):
        '''Canvasのマウスクリックイベント'''

        if self.disp_id is None:
            # 動画を表示
            self.disp_image()
        else:
            # 動画を停止
            self.after_cancel(self.disp_id)
            self.disp_id = None

    def disp_image(self):
        '''画像をCanvasに表示する'''

        # フレーム画像の取得
        ret, frame = self.capture.read()
    
        # BGR→RGB変換
        cv_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        # NumPyのndarrayからPillowのImageへ変換
        pil_image = Image.fromarray(cv_image)

        # キャンバスのサイズを取得
        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()

        # 画像のアスペクト比(縦横比)を崩さずに指定したサイズ(キャンバスのサイズ)全体に画像をリサイズする
        pil_image = ImageOps.pad(pil_image, (canvas_width, canvas_height))

        # PIL.ImageからPhotoImageへ変換する
        self.photo_image = ImageTk.PhotoImage(image=pil_image)

        # 画像の描画
        self.canvas.delete("all")
        self.canvas.create_image(
                canvas_width / 2,       # 画像表示位置(Canvasの中心)
                canvas_height / 2,                   
                image=self.photo_image  # 表示画像データ
                )

        # disp_image()を10msec後に実行する
        self.disp_id = self.after(10, self.disp_image)

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master = root)
    app.mainloop()

実行結果

ポイント

  • 動画を表示するときは、while文ではなく、after()関数で繰り返し処理を行う。
  • OpenCVの画像は、カラー画像の場合、BGRのデータの並びをRGBへ並び替えてからOpenCVの画像データ(NumPyのndarray)をPillowのImageへImage.fromarray()関数で変換する。

関連記事

【OpenCV-Python】Tkinter GUI Sample

【Python/tkinter】Canvasに画像を表示する

【Python】画像データ(NumPy,Pillow(PIL))の相互変換

【Python/NumPy】カラー画像データをRGBからBGRへ変換

【OpenCV/Python】adaptiveThresholdの処理アルゴリズム

自動でしきい値を決めて二値化してくれる画像処理と言えば、大津の二値化ですが、OpenCVにはadaptiveThreshold(適応的しきい値処理)という良さげな処理があります。

この adaptiveThreshold は画像全体に影や照明のムラがある場合に、効果を発揮します。

 

以下に大津の二値化とadaptiveThreshold の処理例を示します。

 

使用したプログラム

import cv2

img = cv2.imread("image.jpg")
# カラー→モノクロ変換
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 元画像の表示
cv2.imshow("src image", img)

# 大津の二値化
_, dst1 = cv2.threshold(
    img, 0, 255, cv2.THRESH_OTSU)
cv2.imshow("THRESH_OTSU", dst1)

# 適応的しきい値処理
dst2 = cv2.adaptiveThreshold(
    img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
    cv2.THRESH_BINARY, 51, 20)
cv2.imshow("adaptiveThreshold", dst2)

cv2.waitKey(0)

元画像

大津の二値化

adaptiveThreshold

 

 

元画像の左側に影のある例を示しています。

今回の画像は、文字の部分を黒く、それ以外の部分を白く二値化することを想定しているのですが、大津の二値化では、自動でしきい値は決めてくれるものの、画像全体に輝度値のムラがある場合は、うまく二値化してくれません。それに比べて adaptiveThreshold ではある程度狙った通りに二値化されています。

 

Pythonですが、この関数定義は以下のようになっています。

adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C)
src 入力画像
maxValue 二値化後の輝度値
adaptiveMethod 適応的しきい値処理で使用するアルゴリズム
cv2.ADAPTIVE_THRESH_MEAN_C
もしくは
cv2.ADAPTIVE_THRESH_GAUSSIAN_C
thresholdType 二値化の種類
cv2.THRESH_BINARY
もしくは
cv2.THRESH_BINARY_INV
blockSize しきい値計算のための近傍サイズ
C 平均あるいは加重平均から引かれる値
戻り値 処理後画像

(参考)

http://opencv.jp/opencv-2svn/c/imgproc_miscellaneous_image_transformations.html

 

だいたい上記のような説明されている場合が多いのですが、よく分からないですよね?!

ただ、やっている事自体は意外と簡単です。

実際にOpenCV内部で行われている処理と異なると思いますが、処理の意味合い的には以下の通りになります。

 

1.adaptiveMethodの設定に従って、平均化(blur)もしくはガウシアンフィルタ(GaussianBlur)で入力画像をぼかします。この時のカーネルのサイズが blockSize x blockSize となります。

 

2.元画像とぼかした画像の差分処理を行います。

 

3.差分画像を指定したしきい値( C ) で二値化し、白黒反転します。

 

すると、adaptiveThreshold で処理した二値化画像が取得できます。
重要なのは、処理の途中に平均化 もしくは ガウシアンフィルタで二値化したい部分をぼかしている部分です。そのため、二値化したい部分の大きさ(今回の例では文字の線幅)に対して十分大きな blockSize を指定する必要があります。

blockSizeを変えながら処理をすると、

blockSize = 5 のとき

blockSize = 21 のとき

blockSize = 51 のとき

 

このようにblockSizeが小さいと、文字の輪郭が二値化され、blockSizeを大きくすると、太い文字も文字全体が二値化されます。

処理の目的的にはトップハットボトムハットに似ています。

(参考)

膨張・収縮・オープニング・クロージング

 

実際の用途的には、画像にムラがあるときに、小さなゴミやキズなどの検出に用いられます。
逆に大きな領域を二値化する場合には adaptiveThreshold は不向きなのでご注意下さい。

 

ちなみに買ったチョコビ

【参考書籍】画像処理・機械学習プログラミング OpenCV 3対応

これまでもマイナビのOpenCVの本は購入してきたので、今回も買ってみました。

 

画像処理・機械学習プログラミング OpenCV 3対応

 

まず、第一印象は、薄い、小っちゃい(一番、右のやつ)

 

フォントも小さくなっているので、内容量はこれまでと同じぐらいか??

 

内容的には、インストール方法には割とページを割いてあって、画素値の操作方法も書いてあるところまではいいのですが、そこから先は具体例になってしまうので、OpenCV初心者には難しく感じるかもしれません。

当然ながらIplImageとかは登場しません。

 

以前はOpenCV.jpを見れば関数のマニュアルについては、日本語で情報を得られましたが、今は更新が止まっているので、OpenCV3対応の日本語マニュアルってあるのかな??

 

関数のマニュアルを見たい場合は英語になりますが、このへん↓が参考になると思います。

http://docs.opencv.org/master/index.html

 

右上の検索ボックスから、関数を一個一個検索しながら参照すると良いかと思います。

 

それでも日本語で最新の情報を知りたい!という場合には、ここ↓がおススメです。

http://www.buildinsider.net/small/opencv

 

ところで、今回の著者の中に井村先生の名前が入っていたけど、どこの記事を担当したんだろうか??

井村先生といえば、ラベリングクラスでお世話になった人も多いかと思いますが、OpenCV3からはラベリング処理(connectedComponents、connectedComponentsWithStats)は入っているし。。

 

目次

1章 導入編
2章 スタートアップ編
3章 ケーススタディ 編
画素値の直接操作
カメラキャリブレーションとステレオ視
画像のJPEG圧縮とPSNRの計算
簡単な笑顔度の算出
CAPTCHA画像の生成
Kinect v2とOpenCVの連携
ミニチュア風画像処理
ORB特徴を使ったイメージモザイキング
機械学習と画像処理
顔の子供化
動画ファイルをカメラ入力のように扱う

 

【Python】OpenCVをAnacondaでインストール(Windows編)

PythonでOpenCVのインストール方法を検索すると、macOSやLinuxの情報が多く、Windowsのインストール方法が何だか少ない。。

まだ、よく分かっていないのですが、とりあえずAnacondaを使ってOpenCVのインストールができたので、その方法を記しておきます。

 

前提条件として、私のPCの環境は

●Windows10(64bit)

●Anacondaがインストールされている

です。

 

まず、公式のOpenCVのページ

http://docs.opencv.org/3.2.0/d5/de5/tutorial_py_setup_in_windows.html

 

で確認すると、動作環境は

●Python2.7.X

●Numpy

●Matplotlib

という事らしい。

さらにNumpyが公式には64bit対応されていないので、32bit用のOpenCVを使ってます的なことが書いてある。

 

以上のことから、AnacondaでPythonをインストールするときはPython3.6用のパッケージでインストールしてしまったので、Python2.7用の環境を作ります。

 

Python2.7の環境構築方法は、下記ページにまとめました。

https://imagingsolution.net/program/python/anaconda/python-multi-version-environment/

 

次にAnaconda CloudというページからOpenCVのパッケージを探します。

 

https://anaconda.org/

 

このページの検索窓からOpenCVを検索します。

すると様々なOpenCVのパッケージが表示されるのですが、Platformsにwin-64が書いてある、一番上の menpo/opencv3 の部分をクリックします。

 

 

すると、インストールコマンドも書いてあるので、これでいけるのかな?と思ったら、そうも行かないので、まず、Filesの部分をクリックします。

 

 

すると、各種OS用なパッケージが表示されますが、このままだと見づらいので、Nameの部分をクリックします。

 

 

Windows用のファイルは6つ表示されていますが、Uploadedの部分にmenpoと書いてあるファイル

 

win-32/opencv3-3.1.0-py27_0.tar.bz2

 

と書いてあるファイルが使えるっぽい

 

 

ファイル名から察するに

Windowsの32bit、OpenCV Ver3.1.0、Python2.7用のファイル

 

ということがわかります。

そこで、今度はAnaconda Navigatorを起動し、Python2.7用の環境から Open Terminalをクリックします。

 

 

ターミナルを起動したらAnaconda Cloudのページに書いてあった、インストールコマンドのバージョンの部分を少し変えて

 

conda install -c menpo opencv3=3.1.0

 

と入力します。

 

 

すると

Proceed([y]/n)?

 

と聞かれるので y  を入力して、この様な画面になれば、インストール成功です。

(最後、少し止まってますが、我慢。。)

 

 

本当にOpenCVがインストールされてのか?を確認するため、Python2.7からJupyter Notebookを起動し、

 

 

OpenCVのバージョン確認用のコード↓

import cv2
print cv2.__version__

 

を入力し、実行して

 

バージョンが表示されればインストール成功です。

 

本当は64bit用のOpenCV3.2でPython3.X系でインストールしたいところなのですが、まだ、力量不足。。。

ただ、Anacondaでインストールするこの方法は、簡単なので、とりあえずPythonでOpenCVを試してみたい!という人向けにはいいかも?しれません。(私がその状態)

【OpenCV】疑似カラー(カラーマップ)

OpenCVを使ってモノクロ画像に疑似的に色をつけるにはcv::applyColorMap関数が用意されています。

 

【簡単なソースコード】

#include "stdafx.h"
#include <opencv2/opencv.hpp>;

using namespace cv;

int main()
{
 // 画像読込
 Mat img0 = imread("Mandrill.BMP");
 
 Mat cm_img0;
 // 疑似カラー(カラーマップ)変換
 applyColorMap(img0, cm_img0, COLORMAP_JET);

 // 画像表示
 imshow("cm_img0", cm_img0);

 // キー入力待ち
 waitKey(0);

 // 画像保存
 imwrite("Mandrill_COLORMAP_JET.BMP", cm_img0);

 return 0;
}

(参考)

http://docs.opencv.org/3.2.0/d3/d50/group__imgproc__colormap.html

 

【処理結果】

 

applyColorMap関数の書式は、下記の通り

 

void cv::applyColorMap (
InputArray src,
OutputArray dst,
int colormap
)

 

colormapの設定の違いにより、以下のように色変換されます。

 

オリジナル画像

 

COLORMAP_AUTUMN

 

COLORMAP_BONE

 

   
COLORMAP_JET

 

COLORMAP_WINTER

 

   
COLORMAP_RAINBOW

 

COLORMAP_OCEAN

 

   
COLORMAP_SUMMER

 

COLORMAP_SPRING

 

   
COLORMAP_COOL

 

COLORMAP_HSV

 

   
COLORMAP_PINK

 

COLORMAP_HOT

 

 
COLORMAP_PARULA

 

 

個人的に使えそうなのは、COLORMAP_JETとCOLORMAP_HOTぐらいかな?

 

OpenCVへ戻る

【OpenCvSharp】サンプルプログラムの公開

少し前に、とある記事向けに書いたOpenCvSharpのサンプルプログラム。

少しバージョンが古くなっていますが、眠らせておくのも、もったいないので公開しておきます。

 

 

ダウンロードはこちら↓

OpenCvSharp サンプルプログラム

注)zipファイルを解凍してから使って下さい。

zipのプレビューからslnファイルを実行すると、プロジェクトファイルの読込に失敗します。

 

このサンプルだけで、

 

平滑化(Blur、GaussianBlur、Median)

二値化(普通の二値化、大津の二値化)

輪郭処理(ソーベルフィルタ、Cannyエッジ)

モフォロジー(膨張、収縮、オープニング、クロージング)

ヒストグラム表示

 

ができます。

OpenCVでなかなかカラーのヒストグラムまでのサンプルプログラムを書いてあるのは、あまりないので、そこそこ良いサンプルプログラムだと思っていたのですが、なんでボツったんだろう??