【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

# OpenCVのgetRotationMatrix2D()関数と同等のアフィン変換行列を求める
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.       ]]

# OpenCVのgetRotationMatrix2D()関数を実行し、求めたアフィン変換行列が上記と一致している事を確認
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)

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

OpenCVでBmpやJpegなどの画像ファイルを開くには、imread関数を用います。

開いた画像データはimshow関数でウィンドウ付きで画像を表示します。

実際に画像が表示されるのは、waitKey関数が呼ばれたタイミングとなります。

以下に最もシンプルなサンプルを示します。

import cv2

# 画像ファイルの読み込み(カラー画像(3チャンネル)として読み込まれる)
img = cv2.imread("Mandrill.bmp")

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

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

(実行結果)

ここに示したサンプルでは、画像データは必ず8bit x 3ch のデータとなり、表示されたウィンドウはリサイズすることができません。

そのようにしないためには、各関数のオプションを設定します。

詳細は、各関数ごとに説明します。

imread()

bmp, jpeg, png, tiff などの画像ファイルを開きます。

cv2.imread( filename[, flags] ) -> retval
引数 説明
filename 画像のファイル名を指定します。
ただし、日本語は使えません。
flags ファイルを開く際に、カラーデータとして開くか?モノクロデータとして開くか?などを指定します。
指定しない場合、カラーデータ(BGRの3チャンネル)として画像ファイルを開きます。
cv2.IMREAD_UNCHANGED 画像ファイルのフォーマットのまま、画像ファイルを開きます。
cv2.IMREAD_GRAYSCALE   グレースケールに変換して画像ファイルを開きます。
cv2.IMREAD_COLOR          カラーデータ(BGRの3チャンネル)に変換して画像ファイルを開きます。
cv2.IMREAD_ANYDEPTH    画像ファイルのビット深度のまま画像ファイルを開きます。
cv2.IMREAD_ANYCOLOR    画像ファイルの色(チャンネル数)のまま画像ファイルを開きます。(参考)ImreadModes
戻り値 画像の輝度値がnumpyのndarray配列に格納されて返されます。
画像データは画像の左上から格納されています。
グレースケールの場合、[高さ, 幅]の二次元配列になります。
カラーの場合、[高さ, 幅, 色]の三次元配列になります。色の順番は、B,G,R,B,G,R・・・の順で格納されています。

画像ファイル名に日本語ファイルを使いたい場合は、NumPyやPillowを使って日本語ファイルを読み込む事も可能です。詳細は以下のページを参照ください。

日本語の画像ファイル読込・保存

また、imread()関数では、画像ファイルが見つからない場合など、画像ファイル読み込み時にエラーが発生しがちです。その場合の対応は以下のページを参照ください。

imreadで画像ファイルが読み込めないときの対応

 

(参考)

https://docs.opencv.org/4.5.5/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56

imshow()

画像をウィンドウ付きで表示します。
実際に画面に画像が表示されるのは、cv2.waitKey()関数が呼ばれたタイミングで表示されます。

cv2.imshow( winname, mat ) -> None
引数 説明
winname 表示するウィンドウのタイトルを指定します。
namedWindow()関数で作成したウィンドウと同じwinnameを指定することで、画像を表示するウィンドウを切り替えることもできます。
winnameで指定したウィンドウが無い場合、新しいウィンドウで画像を表示します。
日本語を指定すると文字化けします。
mat 表示する画像データを指定します。
戻り値 なし

 

(参考)

https://docs.opencv.org/4.5.5/d7/dfc/group__highgui.html#ga453d42fe4cb60e5723281a89973ee563

waitKey()

指定時間(mSec)分、キー入力を待ちます。

OpenCVで生成したウィンドウのキー入力イベント待ちとなります。

引数 説明
delay キー入力までの待ち時間をmSec単位で指定します。
値を省略、もしくは 0以下 を指定した場合、無限待ちとなります。
戻り値 入力したキーコードが戻されます。

(参考)

https://docs.opencv.org/4.5.5/d7/dfc/group__highgui.html#ga5628525ad33f52eab17feebcfba38bd7

namedWindow()

imshow()関数で表示するためのウィンドウを生成します。

cv2.namedWindow( winname[, flags] ) -> None
引数 説明
winname 表示するウィンドウのタイトルを指定します。
日本語を指定すると文字化けします。
flags ウィンドウのリサイズに関する設定を指定します。
cv2.WINDOW_NORMAL リサイズ可能なウィンドウを生成します。
表示されている画像はウィンドウサイズに合わせてリサイズされます。
cv2.WINDOW_AUTOSIZE 画像のサイズに合わせてウィンドウサイズを調整します。
ウィンドウのリサイズはできません。
他に cv2.WINDOW_FREERATIO, cv2.WINDOW_KEEPRATIO, cv2.WINDOW_GUI_NORMAL, cv2.WINDOW_GUI_EXPANDED の設定がありますが、少なくともWindows環境ではcv2.WINDOW_NORMALと同じ
(参考)WindowFlags
戻り値 なし

生成したウィンドウはdestroyWindow()関数で指定したウィンドウを削除するか、destroyAllWindows()関数で全てのウィンドウを削除するようにします。

(参考)

https://docs.opencv.org/4.5.5/d7/dfc/group__highgui.html#ga5afdf8410934fd099df85c75b2e0888b

destroyWindow()

ウィンドウタイトル名を指定してウィンドウを閉じます。

cv2.destroyWindow( winname ) -> None
引数 説明
winname 閉じるウィンドウのタイトルを指定します。
戻り値 なし

(参考)

https://docs.opencv.org/4.5.5/d7/dfc/group__highgui.html#ga851ccdd6961022d1d5b4c4f255dbab34

destroyAllWindows()

表示されているウィンドウ全てを閉じます。

cv2.destroyAllWindows() -> None

(参考)

https://docs.opencv.org/4.5.5/d7/dfc/group__highgui.html#ga6b7fc1c1a8960438156912027b38f481

各種設定を行ったサンプルプログラム

import cv2

# 画像ファイルの読み込み(ファイルフォーマットのまま読み込み)
img1 = cv2.imread("Mandrill.bmp", cv2.IMREAD_UNCHANGED)
img2 = cv2.imread("Text.bmp", cv2.IMREAD_UNCHANGED)

# リサイズ可能なウィンドウの作成
cv2.namedWindow("Image1", cv2.WINDOW_NORMAL)
cv2.namedWindow("Image2", cv2.WINDOW_NORMAL)

# 画像の表示
cv2.imshow("Image1", img1)
cv2.imshow("Image2", img2)

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

# すべて全てのウィンドウを削除
cv2.destroyAllWindows()

(実行結果)

【OpenCV/Python】ドキュメントの場所

OpenCVの公式ホームページは以下の場所になります。

Home

 

Pythonに関する情報は、まだ少ない気もしますが、各関数のマニュアルは下記のページから参照します。

https://docs.opencv.org/4.5.5/

 

各関数の引数や戻り値は、使用するOpenCVのバージョンによっても異なる場合があるので、参照する前にバージョンを使用するバージョンに合わせてください。

 

調べたい関数名が分かっている場合は、右上の検索ボックスに関数名を入力すると、関数候補が表示されるので、その関数をクリックします。

 

Pythonに関する情報は、ちょっと少ない気もしますが、引数の数や順番などは確認できます。

 

日本語のドキュメントが見たい場合には、最近の更新がありませんが、もともとあったOpenCV-Pythonのチュートリアルのページ(現在は閉鎖されています)を翻訳してくれたページがあるので、こちら↓が参考になります。

http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_tutorials.html

こちらのページは情報が古くなっているので、本家のページと合わせて参照すると良いかと思います。

【OpenCV/Python】imreadで画像ファイルが読み込めないときの対応

opencv-pythonのimreadで画像を開き、以下のようなフィルタ処理を行うプログラムを実行すると、エラーが表示される場合があります。

import cv2

# OpenCVで画像ファイルを開く
img = cv2.imread("Mandrill.bmp", cv2.IMREAD_UNCHANGED)

# ガウシアンフィルタ
dst = cv2.GaussianBlur(img, (3, 3), 0)

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

エラー内容

OpenCV(4.5.5) D:\a\opencv-python\opencv-python\opencv\modules\imgproc\src\smooth.dispatch.cpp:617: error: (-215:Assertion failed) !_src.empty() in function ‘cv::GaussianBlur’

 

OpenCVの場合、エラー内容の最後の方を見ると、だいたいエラーの原因がわかる場合が多いのですが、今回のエラーの内容の場合

  !_src.empty() in function ‘cv::GaussianBlur’

の部分が大事です。

直訳的に言うと、

  ’cv::GaussianBlur’関数の入力画像(src)は空(empty)ではいけません。

と言われています。

入力画像が空になる原因は imread()関数で画像ファイルが読み込めない場合がほとんどなのですが、読み込めない原因は、主に以下の2つです。

 

●画像ファイルがみつからない。(カレントディレクトリに画像ファイルが無い)

●ファイル名、ディレクトリ名に日本語が含まれている

 

画像ファイルがみつからない場合

imread()関数で、ファイル名の部分に相対パスでファイル名を指定すると、カレントディレクトリにあるファイルを探しに行くため、画像ファイルがカレントディレクトリに無いと、画像ファイルを開く事ができません。

【対策】

①ファイル名を相対パスではなく、絶対パス(フルパス)で指定する。

(例)

img = cv2.imread(r"C:\temp\Mandrill.bmp", cv2.IMREAD_UNCHANGED)

 

②カレントディレクトリを確認し、カレントディレクトリにファイルを置く。

カレントディレクトリは以下のようにすると、確認ができます。

import os

# カレントディレクトリの表示
print(os.getcwd())

ファイル名、ディレクトリ名に日本語が含まれている場合

OpenCVでは日本語を扱う事ができません。

そのため、画像ファイル名においても日本語が含まれるとimread関数で画像ファイルの読み込みに失敗します。

日本語ファイル名の画像を開くには、OpenCVではなく、PillowやNumPyを使って画像ファイルを開き、OpenCVの画像形式である ndarray へ変換する事ができます。

Pillowで画像ファイルを開いた方が、さまざまな画像ファイルフォーマットに対応しているので、汎用性が高いと思います。

(参考)

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

【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を使った方がいいかもしれません。

【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 は不向きなのでご注意下さい。

 

ちなみに買ったチョコビ

【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を試してみたい!という人向けにはいいかも?しれません。(私がその状態)