【Outlook】メールや会議のボタン(ナビゲーション)の表示位置の変更

Outlookのプログラムを使っていると、メールや会議の表示を切り替えるボタン(ナビゲーション)の位置が突然、ウィンドウの左側(縦の表示)に移動していて、これを元の下(横の表示)の位置に戻したい。

 

 

 

 

 

 

 

 

 

 

 

 

 

この表示位置変更方法は、左にあったボタンを下に移動する例で説明したいと思います。

 

Outlookのメニューの ファイル → オプション より表示されたウィンドウの 詳細設定 より、

□ Outlookでアプリを表示する

のチェックを外し、ウィンドウ右下のOKボタンをクリックします。

すると、再起動が必要です というウィンドウが表示されるので、OKボタンをクリックし、Outlookを一旦終了させ、再度、Outlookを起動すると、ナビゲーションのボタンの表示位置が変更されます。

 

ボタンを左に表示したい場合は、Outlookでアプリを表示する にチェックを入れて、Outlookを再起動します。

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

以前、輪郭の内側の面積と周囲長の関係から円形度なる値を求め、円らしき領域を抽出する方法を行いました。

【OpenCV-Python】円形度

 

この考え方を応用して、今度は、矩形度(長方形らしさ)なる値を求めて、矩形らしき領域を抽出する方法を考えてみたいと思います。

矩形(長方形)は、名刺や本、コピー用紙のように、割と良く目にする図形なので、応用範囲は広いかと思います。ただし、今回紹介する方法は、四角形の被写体を斜めから撮影して、台形のようになってしまう画像には対応できません。

 

今回は、OpenCVを用いて、輪郭の内側面積contourArea()関数で求め、輪郭を囲う最小の矩形領域の面積minAreaRect()関数で求めて、この2つの面積の比で、矩形度なる値を求めます。

ただし、矩形度という言葉や計算方法が教科書的に存在するのかは不明です。(私が勝手に言ってます。)

この2つの面積が一致していれば完全に矩形(長方形)となります。

矩形度(小) 矩形度(中) 矩形度(大)

 

サンプルプログラム

矩形度を求め、下図の中から矩形らしい部分を抽出するサンプルプログラムは以下の通りです。

(サンプルプログラム)

import cv2
import numpy as np

def rectangularity(contour):
    '''
    矩形度を求める

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

    Returns
    -------
        矩形度

    '''
    # 面積
    area = cv2.contourArea(contour)
    # 傾いた外接する矩形領域
    _, (width, height), _ = cv2.minAreaRect(contour)

    # 矩形度を返す
    return area / width / height

# 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:
    # 傾いた外接する矩形領域の描画
    rect = cv2.minAreaRect(contour)
    box = cv2.boxPoints(rect)
    box = np.intp(box)
    cv2.drawContours(img_disp,[box],0,(0,255,255), 1)
    # 矩形度の計算
    val = rectangularity(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-10,y-10),(x+w+10,y+h+10),(255,0,0),2) # 少し外側を囲う

cv2.imshow("Image", img_disp)

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

実行結果

 

関連記事

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

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

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

【OpenCV-Python】円形度

SIMD(AVX,AVX2)の使用方法

AVX, AVX2の関数を使うには、至って簡単で、ヘッダファイル(immintrin.h)をインクルードするのみで、使えるようになります。

#include <immintrin.h>

AVX, AVX2で処理を行うデータは、通常、PCのメインメモリに格納されているかと思いますが、このデータをレジスタと呼ばれる高速で処理を行える領域にデータを読み込み、処理を行います。

このレジスタの領域でAVX, AVX2の処理を行い、画像処理などのように大きいメモリのデータを処理する場合は、このレジスタ上で行われた処理結果を、再度、PCのメインメモリへ書き込む必要があります。

 

メモリからレジスタへ読込

AVX,AVX2処理

レジスタからメモリへ書き込み

 

AVX命令の多くは、128bit(16byte)で処理を行うので、メインからのデータの読み書きは16バイトごとに行います。

同様にAVX2命令の多くは、256bit(32byte)で処理を行うので、32バイトごとに行います。

 

この簡単なサンプルを以下に示します。

アライメントされていないメモリの処理

#include <stdio.h>
#include <immintrin.h>  // AVX, AVX2を使うには、これをインクルードする

int main() {

    int len = 64;

    // メモリの確保(確保されたメモリはアライメントされていない)
    unsigned char* src1 = (unsigned char*)malloc(len * sizeof(unsigned char));
    unsigned char* src2 = (unsigned char*)malloc(len * sizeof(unsigned char));
    unsigned char* dst = (unsigned char*)malloc(len * sizeof(unsigned char));

    // 評価用データの代入
    for (int i = 0; i < len; i++) {
        src1[i] = i;
        src2[i] = i * 2;
    }

    // AVX処理
    for (int i = 0; i < len; i += 16) { // 16バイトごとに行う
        // メモリからレジスタへデータの読込
        __m128i a = _mm_loadu_si128((__m128i*)(src1 + i));
        __m128i b = _mm_loadu_si128((__m128i*)(src2 + i));

        // AVX処理(a と b を足す)
        __m128i c = _mm_add_epi8(a, b);

        // レジスタからメモリへ書き込み
        _mm_storeu_si128((__m128i*)(dst + i), c);
    }

    // 結果の表示
    for (int i = 0; i < len; i++) {
        printf("%d, %d, %d\n", src1[i], src2[i], dst[i]);
    }
    // 処理結果    
    //0, 0, 0
    //1, 2, 3
    //2, 4, 6
    //3, 6, 9
    //4, 8, 12
    //5, 10, 15
    //6, 12, 18
    //7, 14, 21
    // 以下、省略

    // メモリの解放
    free(src1);
    free(src2);
    free(dst);
}

上記のサンプルでは、malloc関数でメモリを確保し、AVXの_mm_loadu_si128関数で、メモリからレジスタへデータを読み込みしています。

今回は_mm_add_epi8関数で、8bitごとのデータの足し算を行い、処理結果を_mm_storeu_si128関数でレジスタからメモリへ戻しています。

malloc関数で確保したメモリは、アドレスがアライメントされているとは限らず、アライメントされていないメモリからレジスタへ読込を行うには、関数名に u の付いた _mm_loadu_si128 などを用います。

同様に、レジスタからメモリへの書き込みは _mm_storeu_si128 などを用います。

 

AVX, AVX2の処理では、メモリのアライメントを行うと、より高速に読込/書き込みを行うことができ、アライメントされたメモリを確保するには Windowsの場合、_aligned_malloc関数を用います。

 

アライメントされたメモリの処理

#include <stdio.h>
#include <immintrin.h>  // AVX, AVX2を使うには、これをインクルードする

int main() {

    int len = 64;

    // メモリの確保(確保されたメモリはアライメントされている)
    unsigned char* src1 = (unsigned char*)_aligned_malloc(len * sizeof(unsigned char), 32);
    unsigned char* src2 = (unsigned char*)_aligned_malloc(len * sizeof(unsigned char), 32);
    unsigned char* dst = (unsigned char*)_aligned_malloc(len * sizeof(unsigned char), 32);

    // 評価用データの代入
    for (int i = 0; i < len; i++) {
        src1[i] = i;
        src2[i] = i * 2;
    }

    // AVX処理
    for (int i = 0; i < len; i += 16) { // 16バイトごとに行う
        // メモリからレジスタへデータの読込
        __m128i a = _mm_load_si128((__m128i*)(src1 + i));
        __m128i b = _mm_load_si128((__m128i*)(src2 + i));

        // AVX処理(a と b を足す)
        __m128i c = _mm_add_epi8(a, b);

        // レジスタからメモリへ書き込み
        _mm_store_si128((__m128i*)(dst + i), c);
    }

    // 結果の表示
    for (int i = 0; i < len; i++) {
        printf("%d, %d, %d\n", src1[i], src2[i], dst[i]);
    }
    // 処理結果    
    //0, 0, 0
    //1, 2, 3
    //2, 4, 6
    //3, 6, 9
    //4, 8, 12
    //5, 10, 15
    //6, 12, 18
    //7, 14, 21
    // 以下、省略

    // 解放
    _aligned_free(src1);
    _aligned_free(src2);
    _aligned_free(dst);
}

アライメントされたメモリの確保をしているのは

unsigned char* src1 = (unsigned char*)_aligned_malloc(len * sizeof(unsigned char), 32);

の部分で、_aligned_malloc関数の第二引数に32を指定すると、メモリのアドレスが32バイト境界にアライメントされたメモリを確保する事ができます。

AVXの関数(128bit処理)を使用するには16(32でも可)、AVX2の関数(256bit処理)では32を指定する必要があります。

_aligned_mallocで確保したメモリは_aligned_freeで解放する必要があります。

 

注意点

アライメントされていないメモリに対して_mm_load_si128関数や_mm_store_si128関数を使うと、プログラムが落ちます。

ただし、malloc関数などを使ってメモリを確保したとき、アライメントがたまたま合っているアドレスで確保される事も多いので、アライメントの対応を間違っていても、プログラムとしては動く場合もあります。

そのため、たまにプログラムが落ちる場合には、まずは、このアライメント周りを疑った方が良いかと思います。

【Word】見出しなどのスタイル書式を別のファイルからコピーする方法

Word文章で、見出しなどのスタイルを別のファイルからコピーする方法です。

↓ コピーしたい

 

スタイルを別のファイルからコピーする方法

スタイルを別のファイルからコピーするには、まず、スタイルの部分の右下にある矢印をクリックします。

 

表示されたスタイルのウィンドウの下にあるスタイルの管理のボタンをクリックします。

表示されたスタイルの管理のウィンドウの左下に表示されているインポート/エクスポートのボタンをクリックします。

 

構成内容変更のウィンドウにスタイルのコピー先とコピー元のファイルが表示されているので、一旦、コピー元のファイルを閉じるボタンをクリックします。

 

すると、コピー元の欄が空白になりますが、ボタンの名前がファイルを開くに変わっているので、このボタンをクリックします。

 

ここで、スタイルをコピーしてきたいコピー元のファイルを選択します。

初期状態では、ファイルの種類がすべてのWordテンプレートになっているので、ここをWord文章を選択し、スタイルをコピーしたいコピー元のファイルを選択し、開くボタンをクリックします。

 

すると、構成内容変更のウィンドウで表示されているコピー先とコピー元のファイルが想定とは逆に表示されてしまいます。

 

ここで、右側のファイルをコピー元にするには、フッターやヘッダー、見出し1、見出し2など、コピーをしたい項目を選択すると、選択した方がコピー元になるので、この状態で、コピーボタンをクリックします。

 

すると、既存のスタイル”標準”を上書きしますか?と確認のメッセージが表示されるので、選択したスタイルの項目(見出し1、見出し2など)をすべてコピーしたい場合は、すべて上書きボタンをクリックします。

 

これで、構成内容変更ウィンドウに表示されている、コピー先とコピー元のファイルのスタイルの項目がコピーされます。

 

この状態で、閉じるボタンをクリックすると、Word文章のスタイルの部分が更新されます。

 

これで、スタイルの書式を別のファイルからコピーして持ってくるのは完了です。

【Word】表のセルの上下方向の中央寄せ

Wordの表で、文字や図を上下方向に中央寄せに配置するには、デフォルトでメニューが表示されていないため、少々わかりづらくなっています。

              ↓ 上下方向の中央寄せ

上下方向の中央寄せの操作方法

まず、上下方向に中央寄せにしたいセルをクリックし、セルを選択します。

 

すると、Wordの上の方に表示されているメニューに レイアウト の文字が追加され、この下に表示されている 3 x 3 個のアイコンが、上下左右方向の配置の設定になります。

上下方向の中央寄せを行うには、上下方法の中央のアイコンをクリックします。

アイコンが表示されていない場合は、 配置 をクリックすると表示されます。

 

この方法を駆使すると、上下、左右方向に自由に配置することが可能になります。

【OpenCV-Python】デモザイキング(Bayer変換)

デモザイキング(Demosaicing)とは、一般的なカラーカメラでは、センサの各画素に下図のようなBayerパターンと呼ばれる配置で、フィルタが配置されています。

この状態で画像を撮影すると、画像そのものはモノクロ画像の市松模様のようなノイズがのったような画像が撮影されます。

この画像からカラー画像へ変換することをデモザイキング(Demosaicing) や、Bayer変換 と言います。

OpenCVでデモザイキングを行うにはcvtColor()関数の codeの部分にBayerパターンの配置の4種類、デモザイキング処理アルゴリズムの3種類の組み合わせの定数が用意されています。

 

●Demosaicing using bilinear interpolation

cv2.COLOR_BayerBG2BGR, cv2.COLOR_BayerGB2BGR, cv2.COLOR_BayerRG2BGR, cv2.COLOR_BayerGR2BGR

 

Demosaicing using VNG(Variable Number of Gradients).

cv2.COLOR_BayerBG2BGR_VNG, cv2.COLOR_BayerGB2BGR_VNG, cv2.COLOR_BayerRG2BGR_VNG, cv2.COLOR_BayerGR2BGR_VNG

 

●EA(Edge-Aware) Demosaicing.

cv2.COLOR_BayerBG2BGR_EA, cv2.COLOR_BayerGB2BGR_EA, cv2.COLOR_BayerRG2BGR_EA, cv2.COLOR_BayerGR2BGR_EA

 

(参考)

https://docs.opencv.org/3.4/d8/d01/group__imgproc__color__conversions.html#ga57261f12fccf872a2b2d66daf29d5bd0

 

コード例

import cv2

# 画像ファイルの読み込み
src_img = cv2.imread("Mandrill_RAW.bmp", cv2.IMREAD_UNCHANGED)

# デモザイキング(COLOR_BayerRG2BGR, COLOR_BayerRG2BGR_VNG, COLOR_BayerRG2BGR_EAなど)
color_img = cv2.cvtColor(src_img, cv2.COLOR_BayerRG2BGR)

# 画像の表示
cv2.imshow("Src Image", src_img)
cv2.imshow("Demosaicing Image", color_img)

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

実行結果

 

バイリニア補間で行うデモザイキングは昔からあったので、知っていましたが、

Variable Number of Gradients や Edge-Aware

というのがあることを最近知って、???となったのですが、詳しい処理アルゴリズムについては分かりませんでしたが、処理結果の比較を行いました。

 

RAWデータ

Bilinearの処理結果

一部拡大

エッジの部分(鼻の赤い部分の輪郭など)がギザギザしていたり、擬色(本来無いはずの色)が確認できます。

 

VNG(Variable Number of Gradients)の処理結果

一部拡大

Bilinearと比べて、エッジ部分のギザギザが抑えられ、擬色も低減されています。

 

EA(Edge-Aware)の処理結果

一部拡大

鼻の脇の水色部分の黒い線の模様を見ると、VNGと比べて線が滑らかになっているように見えます。

ヒゲの部分のように、細かいパターンが潰れがちです。

 

処理時間の比較

2048×2048画素のRAWデータを私のPC(i7-7700 2.8GHz)で試してみたところ、1000回処理行った時の平均処理時間は以下の通りでした。

処理アルゴリズム 処理時間(mSec)
Bilinear 3.2
Variable Number of Gradients 45
Edge-Aware 47

 

Bilinear以外の処理は、そこそこキレイな画像になりますが、カメラで撮影した画像をリアルタイムで処理しようとすると、Bilinear一択になりそうです。

参考リンク

【Kinect SDK】RawBayerデータとは?

カラーカメラはモノクロカメラを兼ねない

https://docs.opencv.org/3.4/d8/d01/group__imgproc__color__conversions.html

【OpenCV-Python】cvtColor(色変換)

【OpenCV-Python】cvtColorでBayer変換するときの定数

【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

ハイパースペクトルカメラとは?その原理は?

ハイパースペクトルカメラとは?

ハイパースペクトルカメラとは、1回の撮影で多くの波長(例えば100波長分など)ごとの画像を撮影できるカメラとなります。

多くの波長の画像を取得できると何がいいか?というと、撮影する被写体によって、特定の波長で暗くみえたりするのですが、この特定波長というのが、材料や状態(水分を多く含むなど)によって異なるため、材料の分類やキズの検出が可能になります。

具体的には食品の中に含まれる異物の検出や、農作物の打痕、リサイクル材料の分類など、一般的なカメラで撮影した画像だけでは困難な検査も可能になります。

ただ、実際には複数の分類が必要でない場合には、検査したい被写体がどのような波長特性を持つのか?を解析的に撮影し、実際の検査では、特徴的な波長だけで撮影するようにバンドパスフィルタを使って、一般的なカメラで撮影する場合もあります。

 

ハイパースペクトルカメラで撮影した画像

下の写真は、ポリスチレン、ポリプロピレン、アクリル、生米、ナイロンを一般的なカラーカメラで撮影した画像になりますが、この画像を通常の画像処理で分類しようとすると、ほぼ白いか、透明なので、これを分類するのは、意外と難しかったりもします。

 

この被写体を実際にハイパースペクトルカメラで撮影し、各波長ごとの画像を並べた物が以下の動画になります。

ここでの注目ポイントとしては波長を大きくしていくと、特定の波長で暗くなり、さらに波長を大きくすると、元の明るさに戻ったりしています。

この特定の波長というのが、材質により異なっています。

上の動画で、各材料ごとの輝度変化を横軸に波長、縦軸に輝度値をプロットした物が以下のグラフになります。

このグラフを見ると、例えばポリスチレンの1150nm付近が輝度値が落ち込んでいるのは、他の材料と比べて特徴的です。

他にもポリプロピレンでは1540nm付近で明るくなっています。

さらに、各波長のピンポイントで見るのではなく、波長による輝度変化全体を捉えると、グラフの線の形状が材質により異なるので、材質の分類が可能になります。

 

実際の撮影風景と、分類の処理を行った様子がこちら↓

 

ハイパースペクトルカメラの原理、仕組み

一般的なカメラでも、エリアセンサ、ラインセンサがあるように、ハイパースペクトルカメラでも、エリアセンサのように撮影するタイプや、ラインセンサのように被写体を動かして撮影する必要のあるラインスキャン方式があり、さらに、光を分光する回折格子にも、反射型や透過型があるのですが、ここでは、ラインスキャン方式で、透過の回折格子を使うハイパースペクトルカメラの仕組みを紹介します。

まず最初に一般的なラインセンサカメラでは、どのように撮影しているかというと、被写体の像をレンズを介してラインセンサ上に結像させます。

この状態で、被写体を移動しながら連続的に撮影することで被写体全体の画像を取得しています。

では、ハイパースペクトルカメラでは、どのように撮影しているか?というと、上図のラインセンサの位置にエアスリット(細長い穴が開いたもの)を配置し、スリットを通り抜けた光をレンズで平行光にします。

その平行光を回折格子(グレーティング)に斜めに入射するように配置すると、回折格子から出た光は波長ごとに異なる角度で回折します。

この回折した光をレンズを介してエリアセンサ上に結像させます。

すると、エリアセンサ上では、縦方向に同じ位置の波長の異なる光の成分が結像した状態になります。

この状態で、通常のラインセンサと同じように、被写体を移動しながら撮影し、横1ラインごと(波長の異なるラインごと)に画像を並び替えると、1回のスキャンで複数波長で撮影した画像を同時に得ることができます。

実際の撮影に関して

ハイパースペクトルカメラを用いると、被写体の材質や状態によって、特定波長において特徴的な変化をする場合があると言いましたが、実際に撮影したい被写体において、そのような変化をするものか?というのは、実際に撮影してみないと分からない部分が多くあります。

具体的に撮影ワークを検討している方は、私の会社の営業までお問い合わせください。

下記、ページより電話か、製品に関してのお問い合わせフォームよりお願いします。

経験豊富な営業が対応してくれると思います。

被写体の撮影評価を行うことも可能です。

https://www.avaldata.co.jp/contact

 

関連製品

ハイパースペクトルカメラ AHS-003VIR (450nm~1700nm)

ハイパースペクトルカメラ AHS-U20MIR (1300nm~2150nm)

【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による輪郭検出

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

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