【C#エラー】System.BadImageFormatException 間違ったフォーマットのプログラムを読み込もうとしました。

ライブラリ(DLL)を使ったアプリケーションを作成しようとすると、

 

アプリケーションはブレークモードになっています

System.BadImageFormatException: ‘間違ったフォーマットのプログラムを読み込もうとしました。

 

 

と表示される場合があります。

このBadImageFormatという部分がDLLが32bitもしくは64bitで作られたものを、C#アプリケーションから異なるbit(32bit)で作成し、DLLを参照した場合にこのエラーが表示されます。

最近のVisual Studioでは、C#のアプリケーションはデフォルトで32bitで作成されるため、とくにC言語で作られた64bitのDLLを使用している場合に、このエラーが発生しやすいです。

 

回避方法は、Visual Studioでプロジェクト名を右クリックし、プロパティを選択します。

表示されたプロパティ画面において、左側のビルドを選択し、右側に表示されている 32ビットを選ぶ のチェックを外します。

構成の部分でDebugReleaseの設定があるので、両方のチェックを外してください。

 

この状態で、再度ビルドを行うと、間違ったフォーマットのプログラムを読み込もうとしました。というエラーメッセージは表示されなくなります。

 

この32ビットを選ぶ の設定はVisual Studioのバージョンによって異なります。

少なくともVisual Studio 2019以降では、32ビットを選ぶが初期設定となっているので、最近のVisual StudioでDLLを使ったアプリを作成する場合は注意が必要です。

【C#エラー】ファイルForm.resxを処理できませんでした。インターネットまたは制限付きゾーン内にあるか、ファイルにWebマークがあるためです。

メールに添付したプログラムをVisual Studioで開き、ビルドをすると以下のようなメッセージが表示されました。

 

ファイルForm.resxを処理できませんでした。インターネットまたは制限付きゾーン内にあるか、ファイルにWebマークがあるためです。これらのファイルを処理するにはWebマークを削除してください。

 

Visual StudioでForm.resxファイルのプロパティを探してもWebマークらしきものがありません。

 

そこで、エクスプローラーでエラーの出ているファイル(ここではForm1.resx)を右クリックし、プロパティを表示し、

 

このファイルは他のコンピューターから取得したものです。このコンピューターを保護するため、このファイルへのアクセスはブロックされる可能性があります。

 

の右側にある 許可する にチェックを入れ、 適用 をクリックします。

 

これで、再度Visual Studioでビルドするとエラーが無くなります。

【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】resize(画像の拡大縮小)

OpenCVで画像のリサイズを行うには、resize関数を用います。

resize関数では、リサイズ後の画像の大きさ(幅と高さ)を指定する方法と、リサイズのスケールを指定する方法があります。

構文

resize( src, dsize[, dst[, fx[, fy[, interpolation]]]] ) -> dst
src リサイズを行う画像データを指定します。
dsize リサイズ後の画像サイズ(幅, 高さ)を指定します。
倍率(fx, fy)を指定するときは、Noneを指定します。
fx 横方向の倍率を指定します。
fy 縦方向の倍率を指定します。
interpolation 補間モードを以下の中から指定します。
cv2.INTER_NEAREST
cv2.INTER_LINEAR
cv2.INTER_CUBIC
cv2.INTER_AREA
cv2.INTER_LANCZOS4

 

出力サイズを指定してリサイズする方法

リサイズ後のサイズを(幅, 高さ)を指定してリサイズを行います。

サンプルプログラム

import cv2

# 画像の読込
src = cv2.imread("image.bmp", cv2.IMREAD_UNCHANGED);

# 出力サイズ(幅、高さ)を指定してリサイズ
dst = cv2.resize(src, (400, 200))

# 画像の表示
cv2.imshow("Src Image", src)
cv2.imshow("Resize Image", dst)
cv2.waitKey()

実行結果

倍率を指定してリサイズする方法

倍率はfx, fyを指定するのですが、出力後のサイズ(dsize)は省略できないので、dsizeにNoneを指定します。

指定方法は

dst = cv2.resize(src, None, None, 0.8, 1.2)

や、変数名を指定して

dst = cv2.resize(src, None, fx = 0.8, fy = 1.2)

のようにします。

サンプルプログラム

import cv2

# 画像の読込
src = cv2.imread("image.bmp", cv2.IMREAD_UNCHANGED);

# スケール(横方向、縦方向)を指定してリサイズ
dst = cv2.resize(src, None, None, 0.8, 1.2)

# 画像の表示
cv2.imshow("Src Image", src)
cv2.imshow("Resize Image", dst)
cv2.waitKey()

実行結果

補間方法の指定

リサイズ時の補間方法については

cv2.INTER_NEAREST
cv2.INTER_LINEAR
cv2.INTER_CUBIC
cv2.INTER_AREA
cv2.INTER_LANCZOS4

の中から指定します。

指定方法は

dst = cv2.resize(src, None, None, 0.8, 1.2, cv2.INTER_NEAREST)

や、変数名を指定して

dst = cv2.resize(src, None, fx = 0.8, fy = 1.2, interpolation = cv2.INTER_NEAREST)

のようにします。

 

補間モードについてはこちら↓を参照ください。

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

 

補間そのものについてはこちら↓を参照ください。

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

 

参照ページ

https://docs.opencv.org/4.6.0/da/d54/group__imgproc__transform.html#ga47a974309e9102f5f08231edc7e7529d

【OpenCV-Python】JPEG画像の品質を指定して保存する

OpenCVで画像をファイルに保存するには imwrite 関数を用いますが、この関数でjpegファイルの品質を指定して保存することが可能です。

imwrite関数の書式は

imwrite(filename, img[, params]) ->retval
引数 説明
filename ファイル名
img 画像データ
params オプション設定(省略可)

 

となりますが、paramsの部分に jpegファイルの品質の値(0~100)を指定することが可能です。

品質を指定して保存するサンプルは以下の通りです。

import cv2

# 画像を開く
img = cv2.imread("Mandrill.bmp", cv2.IMREAD_UNCHANGED)

# JPEGで品質(75)を指定して画像を保存する
cv2.imwrite("Mandrill_q75.jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 75])

品質の値による画像の違いは、Pillowのときの例を参照ください↓

【Python/Pillow(PIL)】JPEG画像の品質を指定して保存する

 

Pillowでは品質の初期値が75であったのに対して、OpenCVでは95になります。

OpenCVでは画像処理用として画像ファイルに保存されることが多いので、品質の値が大きめな初期値になっているのだと思いますが、品質95だと、jpeg画像特有のモスキートノイズのようなノイズは見た目上、ほぼ無いものの、輝度値レベルで見ると、1程度の計算誤差レベル?の誤差はあります。

画像処理用として画像ファイルに保存するには、基本的にはビットマップファイル(*.bmp)に保存することをおススメします。

参考ページ

https://docs.opencv.org/4.6.0/d4/da8/group__imgcodecs.html#gabbc7ef1aa2edfaa87772f1202d67e0ce

https://docs.opencv.org/4.6.0/d8/d6a/group__imgcodecs__flags.html#ga292d81be8d76901bff7988d18d2b42ac

【OpenCV-Python】ヒストグラムの取得、表示

ここでは、ヒストグラムの取得方法と、取得したヒストグラムをmatplotlibで表示する方法を紹介したいと思います。

ヒストグラムの取得方法

OpenCVでヒストグラムを取得するには calcHist()関数を用います。

calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]]) ->hist
引数 説明
images 画像の配列を指定します。
cv2.split()関数でB, G, Rのカラープレーンに分離した3枚の画像なども指定できます。
channels ヒストグラムを取得する色のチャンネル(B, G, Rのどれか?)を指定します。
グレースケールの場合は [0]
B の場合は [0]
G の場合は [1]
R の場合は [2]
を指定します。
mask ヒストグラムを取得する領域のマスク画像を指定します。
画像全体のヒストグラムを取得する場合は None を指定します。
histSize ヒストグラムのビンの数を指定します。通常は[256]となります。
ranges ヒストグラムを取得する輝度値の範囲を指定します。 通常は[0, 256]です。
(戻り値)hist 指定したチャンネルのヒストグラムを取得します。

 

サンプルプログラム

import cv2

# 画像の読込
img = cv2.imread("Parrots.bmp",cv2.IMREAD_UNCHANGED)

if len(img.shape) == 3:
    # カラーのとき
    channels = 3
else:
    # モノクロのとき
    channels = 1

histogram = []
for ch in range(channels):
    # チャンネル(B, G, R)ごとのヒストグラム
    hist_ch = cv2.calcHist([img],[ch],None,[64],[0,256])
    histogram.append(hist_ch[:,0])# 次元を削除して追加

print("==== B ====")
print(histogram[0])
print("==== G ====")
print(histogram[1])
print("==== R ====")
print(histogram[2])

※今回は、ビンの数を64にしました。

 

実行結果

 

matplotlibでヒストグラムの表示

ヒストグラムの表示については、Pillowを使って、ヒストグラムの取得、表示する方法を行いました。

【Python/Pillow(PIL)】画像のヒストグラム取得、表示

Pillowでは色の順番がR, G, B の順ですが、 OpenCVは B, G, R の順なので、注意してください。

せっかくなので、ヒストグラムを取得する部分と、ヒストグラムを表示する部分は関数にしてみました。

 

import cv2
import matplotlib.pyplot as plt # ヒストグラム表示用

def get_histogram(img):
    '''ヒストグラムの取得'''
    if len(img.shape) == 3:
        # カラーのとき
        channels = 3
    else:
        # モノクロのとき
        channels = 1

    histogram = []
    for ch in range(channels):
        # チャンネル(B, G, R)ごとのヒストグラム
        hist_ch = cv2.calcHist([img],[ch],None,[256],[0,256])
        histogram.append(hist_ch[:,0])

    # チャンネルごとのヒストグラムを返す
    return histogram

def draw_histogram(hist):
    '''ヒストグラムをmatplotlibで表示'''
    # チャンネル数
    ch = len(hist)

    # グラフの表示色
    if (ch == 1):
        colors = ["black"]
        label = ["Gray"]
    else:
        colors = ["blue", "green", "red"]
        label = ["B", "G", "R"]

    # ヒストグラムをmatplotlibで表示
    x = range(256)
    for col in range(ch):
        y = hist[col]
        plt.plot(x, y, color = colors[col], label = label[col])

    # 凡例の表示
    plt.legend(loc=2)

    plt.show()

######################################################################

# 画像の読込
img = cv2.imread("Parrots.bmp",cv2.IMREAD_UNCHANGED)

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

# ヒストグラムの取得
hist = get_histogram(img)

# ヒストグラムの描画
draw_histogram(hist)

実行結果

関連記事

【Python/Pillow(PIL)】画像のヒストグラム取得、表示

【OpenCV/Python】画像ファイルの読み込み、表示

【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】ラベリング(connectedComponents)

ラベリングとは、二値化された画像において、画素がつながっている領域に対して同じ番号(ラベル)を与える処理のことを言います。

このつながっている領域のことをブロブ(blob)といいますが、このブロブに対して、面積や幅、高さなどを求め、キズや打痕、汚れなどの欠陥検査を行うための前処理として、ラベリング処理が用いられることが多いです。

↓ラベリング

詳細はこちら↓を参照ください。

ラベリング

 

OpenCVでラベリング処理を行うには、connectedComponents()関数を用います。

関連して、connectedComponentsWithStats()connectedComponentsWithStatsWithAlgorithm()という関数もあります。

 

connectedComponents()関数は、二値化画像に対して、ラベリング画像のみを出力します。

 

connectedComponentsWithStats()関数は、ラベリング画像に追加して、さらにブロブの領域(位置、幅、高さ)と面積、重心の情報を出力します。

 

connectedComponentsWithStatsWithAlgorithm()関数は、さらに、ラベリング処理時の4連結、8連結の指定や出力するラベリング画像のデータ型の指定、ラベリング処理アルゴリズムの指定が可能になっています。

処理アルゴリズムについては、詳細は分からなかったのですが、こちら↓を参照くだだい。

https://docs.opencv.org/4.6.0/d3/dc0/group__imgproc__shape.html#ga5ed7784614678adccb699c70fb841075

 

connectedComponents()関数

connectedComponents( image[, labels[, connectivity[, ltype]]] ) -> retval, labels
引数 説明
image ラベリング処理を行う入力画像
8bit1chである必要があります。
labels ラベリング結果を格納する画像データ
戻り値で戻すこともできるので、指定しないか、Noneを指定でも大丈夫です。
connectivity 8連結の場合は 8、4連結の場合は 4 を指定します。
ltype 出力されるラベリング画像の型を指定します。
cv2.CV_16U もしくは cv2.CV_32S(初期値)
(戻り値)retval ラベルの個数が返されます。
ただし、背景も1つとしてカウントされます。
(戻り値)labels 出力されるラベリング画像

connectedComponentsWithStats()関数

connectedComponentsWithStats( image[, labels[, stats[, centroids[, connectivity[, ltype]]]]] ) -> retval, labels, stats, centroids
引数 説明
image ラベリング処理を行う入力画像
8bit1chである必要があります。
labels ラベリング結果を格納する画像データ
戻り値で戻すこともできるので、指定しないか、Noneを指定でも大丈夫です。
connectivity 8連結の場合は 8、4連結の場合は 4 を指定します。
ltype 出力されるラベリング画像の型を指定します。
cv2.CV_16U もしくは cv2.CV_32S(初期値)
(戻り値)retval ラベルの個数が返されます。
ただし、背景も1つとしてカウントされます。
(戻り値)labels 出力されるラベリング画像
(戻り値)stats 各ラベルごとの領域の情報が格納されます。
[領域の左上のx座標, 領域の左上のy座標, 領域の幅, 領域の高さ, 面積]
面積は領域の画素数です。
(戻り値)centroids 各ラベルごとの重心の情報が格納されます。

※labels, stats, centroidsの最初のデータは背景の情報となります。

 

connectedComponentsWithStats()関数

connectedComponentsWithStatsWithAlgorithm( image, connectivity, ltype, ccltype[, labels[, stats[, centroids]]] ) -> retval, labels, stats, centroids
引数 説明
image ラベリング処理を行う入力画像
8bit1chである必要があります。
labels ラベリング結果を格納する画像データ
戻り値で戻すこともできるので、指定しないか、Noneを指定でも大丈夫です。
connectivity 8連結の場合は 8、4連結の場合は 4 を指定します。
ltype 出力されるラベリング画像の型を指定します。
cv2.CV_16U もしくは cv2.CV_32S(初期値)
ccltype ラベリング処理アルゴリズムを指定します。
cv2.CCL_DEFAULT, cv2.CCL_WU, cv2.CCL_GRANA, cv2.CCL_BOLELLI, cv2.CCL_SAUF, cv2.CCL_BBDT, cv2.CCL_SPAGHETTI
(参考)
https://docs.opencv.org/4.6.0/d3/dc0/group__imgproc__shape.html#ga5ed7784614678adccb699c70fb841075
(戻り値)retval ラベルの個数が返されます。
ただし、背景も1つとしてカウントされます。
(戻り値)labels 出力されるラベリング画像
(戻り値)stats 各ラベルごとの領域の情報が格納されます。
[領域の左上のx座標, 領域の左上のy座標, 領域の幅, 領域の高さ, 面積]
面積は領域の画素数です。
(戻り値)centroids 各ラベルごとの重心の情報が格納されます。

※labels, stats, centroidsの最初のデータは背景の情報となります。

connectedComponentsWithStats()関数は、おそらく下記のようにするのと同じです。(アルゴリズムの指定がちょっと怪しい。。)

retval, labels, stats, centroids = cv2.connectedComponentsWithStatsWithAlgorithm(img, 8, cv2.CV_32S, cv2.CCL_DEFAULT)

 

ラベリング処理例

最初にも触れましたが、ラベリングは画像中のキズや打痕、汚れなどの検出に使われる場合が多いです。

下図のように画像中にキズ、打痕、ホコリのようなものがあるときに、黒い部分の面積が大きい部分を検出する例のサンプルプログラムを作成しました。

import cv2
import numpy as np

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

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

# 二値化(二値化して白黒反転)
retval, img = cv2.threshold(img, 200, 255, cv2.THRESH_BINARY_INV)

# クロージング処理(細切れ状態を防ぐため)
kernel = np.ones((3, 3), np.uint8)
img = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel, iterations=15)

# ラベリング
retval, labels, stats, centroids = cv2.connectedComponentsWithStats(img)

# 結果表示
for i in range(1, retval):
    x, y, width, height, area = stats[i] # x座標, y座標, 幅, 高さ, 面積

    if area > 10: # 面積が10画素以上の部分
        cv2.rectangle(img_disp,(x,y),(x+width,y+height),(0,0,255),2)
        cv2.putText(img_disp, f"[{i}]:{area}", (x, y-10), cv2.FONT_HERSHEY_PLAIN, 1, (255, 0, 0), 1, cv2.LINE_AA)

cv2.imshow("image", img_disp)
cv2.waitKey()

実行結果

 

connectedComponentsWithStatsとfindContoursの違い

ブロブの幅や高さ、面積などを解析することをブロブ解析と言いますが、connectedComponentsWithStats()関数で取得できる情報は、ブロブの領域(傾いていない矩形領域)、面積、重心だけですが、輪郭処理ベースのfindContours()関数では、ブロブの傾いた領域や外接円、楕円近似、直線近似、周囲長、凸法など出来る事が多くあるので、必要に応じて使い分けるといいと思います。

ただし、connectedComponentsWithStats()関数はブロブの画素に関する情報を取得しますが、findContours()関数は、あくまでの輪郭の情報を取得するので注意してください。

例えば、下図のような画像のとき

connectedComponentsWithStats()関数では面積(画素数)は8となりますが、findContours()関数では面積(輪郭の内側の面積、上図の赤線の四角の面積)は4となり、さらに輪郭の内側に白では無い画素があっても考慮されません。

 

関連記事

【OpenCV-Python】ラベリング(connectedComponents)

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

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

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

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

【OpenCV-Python】円近似(疑似逆行列を用いた方法)

OpenCVには座標を楕円で近似する関数(fitEllipse)はあるものの、円で近似するfitCircle()のような関数はありません。

そこで、最小二乗法的に座標を円で近似するfitCircle()関数を作ってみました。

円の最小二乗法については、以前、書きました。

一般式による最小二乗法(円の最小二乗法)

この記事で行っている最小二乗法はベタに式を2乗して、未知数で偏微分する必要があるので、計算が少々面倒です。

そこで、今回は、疑似逆行列を使って円近似を行いたいと思います。

 

疑似逆行列を用いた円近似

円上の座標を(x, y)、円の中心座標を(a, b)、半径を r  とすると、円の公式は

$$(x-a)^{2}+(y-b)^{2}=r^{2}$$

この式を展開すると

$$x^{2}-2ax+a^{2}+y^{2}-2by+b^{2}=r^{2}$$

となります。

ここで、

$$\begin{cases}A = -2a\\B = -2b\\C = a^{2}+b^{2}-r^{2}\end{cases}$$

と置き、式を整理すると、

$$Ax+By+C=-x^{2}-y^{2}$$

となります。

近似に用いる点の座標を \((x_{1},y_{1}), (x_{2},y_{2}), (x_{3},y_{3})・・・\)とすると

$$\begin{cases}Ax_{1}+By_{1}+C=-x_{1}^{2}-y_{1}^{2}\\Ax_{2}+By_{2}+C=-x_{2}^{2}-y_{2}^{2}\\Ax_{3}+By_{3}+C=-x_{3}^{2}-y_{3}^{2}\\      :\end{cases}$$

のように、近似する座標の点数(円近似の場合は3点以上必要)ぶんだけの式が成り立ちます。

この連立方程式を行列で表すと、

$$\left(\begin{array}{c}x_{1}&y_{1}&1\\ x_{2}&y_{2}&1\\x_{3}&y_{3}&1\\ &:\end{array}\right)\left(\begin{array}{c}A\\ B\\C\end{array}\right)=\left(\begin{array}{c}-x_{1}^{2}-y_{1}^{2}\\ -x_{2}^{2}-y_{2}^{2}\\ -x_{3}^{2}-y_{3}^{2}\\ :\end{array}\right)$$

となるので、あとは疑似逆行列を使えば、A, B, C が求まるので、A, B, Cの値から円の中心座標の(a, b)、半径の r  が求まります。

$$\begin{cases}a=-\frac{A}{2}\\b=-\frac{B}{2}\\r=\sqrt{a^{2}+b^{2}-C}\end{cases}$$

疑似逆行列を求める部分についはnumpylinalg.pinvという関数があるので、これを用います。

 

サンプルプログラム

上記の疑似逆行列を用いた円近似の処理をまとめました。

円近似の fitCircle()関数を作成しています。

import cv2
import numpy as np

def fitCircle(contour):
    '''
    座標の円近似(最小二乗円)
    '''

    matA = []
    matB = []

    for point in contour:
        x = point[0,0]
        y = point[0,1]
        matA.append([x,y,1])
        matB.append([-x*x -y*y])

    # listからndarrayへ変換
    matA = np.array(matA, np.float32)
    matB = np.array(matB, np.float32)

    # 疑似逆行列を求める
    matA_pinv = np.linalg.pinv(matA)

    # 行列の解
    matX = matA_pinv.dot(matB)

    # 中心座標
    a = -matX[0,0]/2
    b = -matX[1,0]/2
    # 半径
    r = np.sqrt(a*a+b*b-matX[2,0])

    # 中心座標, 半径
    return (a, b), r  

##########################################################################

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

# 白黒反転して二値化
ret, img = cv2.threshold(img, 128, 255, cv2.THRESH_BINARY_INV)

# 一番外側の輪郭のみを取得
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE ) 

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

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

# 輪郭の点の描画
for contour in contours:
    # 円近似
    center, r = fitCircle(contour)
    # 近似円を描画
    cv2.circle(img_disp, np.intp(center), int(r), (255, 0, 0), 2)

cv2.imshow("Image", img_disp)

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

実行結果

 

関連記事

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

一般式による最小二乗法(円の最小二乗法)

疑似逆行列(一般逆行列)の計算と使用方法

【OpenCV-Python】円形度

円形度とは、図形の面積と周囲長の関係から、円らしさの値を求めます。

円形度の詳細は以下のページを参照ください。

円形度

 

OpenCV的には、図形の面積はcontourArea()関数で、周囲長はarcLength()関数で求める事ができるので、これを使って円形度を求めます。

 

今回は、下図のように手書きで書いた図形の中から円らしい部分を見つけたいと思います。

 

サンプルプログラム

contourArea()関数と、周囲長はarcLength()関数を使って円らしい部分を抽出するサンプルプログラムを作成しました。
円形度を求める部分は関数(circularity)にしてあります。

import cv2
import numpy as np

def circularity(contour):
    '''
    円形度を求める

    Parameters
    ----------
    contour : ndarray
        輪郭の(x,y)座標の配列

    Returns
    -------
        円形度

    '''
    # 面積
    area = cv2.contourArea(contour)
    # 周囲長
    length = cv2.arcLength(contour, True)

    # 円形度を返す
    return 4*np.pi*area/length/length

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

# 白黒反転して二値化
ret, img = cv2.threshold(img, 128, 255, cv2.THRESH_BINARY_INV)

# 一番外側の輪郭のみを取得
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, 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:
    # 円形度の計算
    val = circularity(contour)
    # 輪郭の矩形領域
    x,y,w,h = cv2.boundingRect(contour)
    # 円形度の描画
    cv2.putText(img_disp, f"{val:.3f}", (x, y-10), cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 0), 1, cv2.LINE_AA)
    # 円らしい領域(円形度が0.85以上)を囲う
    if val > 0.85:
        cv2.rectangle(img_disp,(x-5,y-5),(x+w+5,y+h+5),(255,0,0),2) # 少し外側を囲う

cv2.imshow("Image", img_disp)

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

実行結果

 

実行結果を見ると、だいたい円っぽい部分は円形度を使うと抽出することが出来ますが、真円度的な円の正確さを求めるのには、円形度は不向きな感じがします。

 

関連記事

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

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

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

【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】輪郭(contour)の面積(contourArea)

OpenCVで二値化された領域の輪郭座標は、findContours関数を使えば取得することができます。

このときの戻り値である輪郭情報(contours)をcontourArea関数へ渡し面積を求めます。

ただし、求まる面積は、あくまでも輪郭線の内側の面積(画素数ではない)となります。
(下図の例では、面積は 35 となります。)

さらに、白の領域の内側に黒の領域があっても、考慮されません。

構文

contourArea(contour[, oriented]) ->retval
contour 輪郭座標
oriented 輪郭の向きを考慮するか(True)、しないか(False)を指定します。
初期値)False
Trueのとき、輪郭座標の向きが時計周りのときは正、反時計周りのときは負となります。
Falseのとき、向きに関係なく、常に正となります。
下図参照
(戻り値)retval 輪郭の内側の面積

サンプルプログラム

import cv2
import numpy as np

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

# 輪郭情報の取得
contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE) 

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

# 輪郭ごとの処理
for i, contour in enumerate(contours):
    # 輪郭の面積を求める
    area = cv2.contourArea(contour, True)
    print(f"面積[{i}]: {area}")

    # 輪郭座標
    for point in contour:
        print(point[0])

cv2.namedWindow("Image", cv2.WINDOW_NORMAL)
cv2.imshow("Image", img_disp)

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

実行結果

このサンプルでは、画像データはnumpyの配列で疑似的に作りましたが、画像にすると下図の用な画像に対して輪郭処理をしています。

この画像では、白の外側の輪郭の面積は 35 となります。

白の内側の輪郭の面積は 13 となります。

内側の輪郭の座標は、黒の画素の上下左右方向(4近傍)に隣接している白の画素の座標となります。

 

関連記事

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

【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)

【Python/tkinter】枠線(relief)の種類一覧

tkinterのウィジェットのButtonやFrame、Labelなどで設定できる枠線(relief)の種類ですが、

  • RAISED
  • GROOVE
  • SUNKEN
  • RIDGE
  • FLAT
  • SOLID

の6種類設定出来るのは、調べれば簡単に出てきますが、どれがどれ?と、いつもなってしまうので、一覧を示します。

サンプルコード

import tkinter as tk

root = tk.Tk()
root.geometry("300x320") 

# ボタンの作成
button1 = tk.Button(
    root, text="tk.RAISED", relief=tk.RAISED, bd=5, width=20)
button2 = tk.Button(
    root, text="tk.GROOVE", relief=tk.GROOVE, bd=5, width=20)
button3 = tk.Button(
    root, text="tk.SUNKEN", relief=tk.SUNKEN, bd=5, width=20)
button4 = tk.Button(
    root, text="tk.RIDGE", relief=tk.RIDGE, bd=5, width=20)
button5 = tk.Button(
    root, text="tk.FLAT", relief=tk.FLAT, bd=5, width=20)
button6 = tk.Button(
    root, text="tk.SOLID", relief=tk.SOLID, bd=5, width=20)

# 配置
button1.pack(pady=10)
button2.pack(pady=10)
button3.pack(pady=10)
button4.pack(pady=10)
button5.pack(pady=10)
button6.pack(pady=10)

root.mainloop()

【Python/tkinter】ウィンドウ(Frame)のxボタンがクリックされたときの終了確認

TkinterのFrameで作成したウィンドウのxボタンをクリックしたときに、終了確認を表示して、プログラムを閉じる/閉じないを選択する方法です。

 

まず、何もしないウィンドウを表示するだけのベースとなるプログラムから手を加えていきます。

 

import tkinter as tk

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

        self.master.geometry("300x300")       # ウィンドウサイズ(幅x高さ)

if __name__ == "__main__":
    root = tk.Tk()

    app = Application(master = root)
    app.mainloop()

(実行結果)

ここに、以下の一文を追加し、xボタンが押されたときに呼び出される関数を指定します。

関数名は何でも構いません。

self.master.protocol("WM_DELETE_WINDOW", self.delete_window)

全、コードはこちら

import tkinter as tk

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

        self.master.geometry("300x300")       # ウィンドウサイズ(幅x高さ)

        # ウィンドウの x ボタンが押された時に呼ばれるメソッドを設定
        self.master.protocol("WM_DELETE_WINDOW", self.delete_window)

    def delete_window(self):
        print("ウィンドウのxボタンが押された")
        pass

if __name__ == "__main__":
    root = tk.Tk()

    app = Application(master = root)
    app.mainloop()

このようにすると、delete_window()メソッドの部分で、printしているだけで特に何もしていないので、xボタンが押されても、ウィンドウが閉じられないプログラムとなります。

xボタンが押されたときにプログラムの終了確認を行うには、このメソッド内で行います。

終了確認を行うプログラムはこちら↓

import tkinter as tk
from tkinter import messagebox

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

        self.master.geometry("300x300")       # ウィンドウサイズ(幅x高さ)

        # ウィンドウの x ボタンが押された時に呼ばれるメソッドを設定
        self.master.protocol("WM_DELETE_WINDOW", self.delete_window)

    def delete_window(self):
        print("ウィンドウのxボタンが押された")

        # 終了確認のメッセージ表示
        ret = messagebox.askyesno(
            title = "終了確認",
            message = "プログラムを終了しますか?")

        if ret == True:
            # 「はい」がクリックされたとき
            self.master.destroy()

if __name__ == "__main__":
    root = tk.Tk()

    app = Application(master = root)
    app.mainloop()

(実行結果)

delete_window()メソッド内で何もしなければ、ウィンドウは閉じられません。

プログラムを終了するには、明示的にdestroy()メソッドを呼び出して、プログラムを終了させる必要があります。