[C/C++]閾値処理を用いて画像内の物体の重心を求めるプログラム[コード付]

C言語

どうも〜シュモクザメです。
いつもはアルゴリズムや行列等の計算っぽいことの記事ばかり書いていたのですが、今回は初めて「画像処理」の記事について書いていきます。
一発目はタイトルにもある通り、画像から物体の重心を認識するプログラムです。

いつもはC言語の記事を書いていましたが、画像処理の記事ではC++で書いていきます。
と言っても、画像の読み書き等の際にopencvを利用しているのでそのためにC++を利用しているようなものです。
なので、画像の読み書き以外の部分はC言語で書いているので普段C++を利用していない人でも大丈夫です!

前提的なもの

前提として僕はXcodeでコードを書いて、実行しています。
opencvはhomebrewからダウンロードして、xcodeからパスをつないだり必要なファイルなどをブチ込んだり、、、とバンバンに使える状態から始めています。
Xcodeにopencvを導入する方法はgoogkeで調べればいくらでもあるので、まだ導入できてない人はまずそこをお願いします。(気が向いたらこのサイトでもopencvの導入の記事を書くかも)

重心とは

いや、まず重心ってなんやんねん

物体の重心をGoogleで調べてみると、、

重心は、力学において、空間的広がりをもって質量が分布するような系において、その質量に対して他の物体から働く万有引力の合力の作用点であると定義される点のことである。(wikipediaより引用)

おk、わからん

そんな説明ほど難しいものではないです。
まあ物体のド真ん中ってイメージで良いですね。
以下は重心で画像検索した結果です。

結果にもある通り、三角形の重心は有名ですよね。
それの三角形に限らず、いろんな図形に対して重心を見つけようっていうのが今回の目的です。

画像の場合は1画素をその物体の面積の単位として、それから物体のど真ん中が重心ってことです。
必ずしも物体中に重心が入るってわけではないのが注意ですね。

コードの実装

画像の読み書き

#define WIDTH 640
#define HEIGHT 480

void Read_Color( const char *name, unsigned char img[HEIGHT][WIDTH][3] )
{
    cv::Mat readImage( HEIGHT, WIDTH, CV_8UC3);
    IMAGE = cv::imread(name, 1);

    for( int w=0; w<WIDTH; w++ )
        for( int h=0; h<HEIGHT; h++ )
            for( int c=0; c<3; c++ )
                img[h][w][c] = IMAGE.data[(w+h*WIDTH)*3+c];
}

void Write_Color( const char *name, unsigned char img[HEIGHT][WIDTH][3] )
{
    cv::Mat IMAGE( HEIGHT, WIDTH, CV_8UC3 );

    for( int w=0; w<WIDTH; w++ )
        for( int h=0; h<HEIGHT; h++ )
            for( int c=0; c<3; c++ )
                IMAGE.data[(w+h*WIDTH)*3+c] = img[h][w][c];
    
    cv::imwrite( name , IMAGE);
}

まずは準備として画像の読み書きですね。

何をやっているかというとそれぞれ関数名の通り画像の読み書きを行なっています。
仮引数のnameが扱うファイル名で、img[HEIGHT][WIDTH][3]が扱う画像が入っている配列ですね。
HEIGHTとWIDTH はマクロで定義していて、それぞれ画像の横と縦の画素数です。
つまり640×480の画像を扱うってことです。

簡単に説明しますが、カラー画像には一つの画素につきRGB(赤緑青)の3の値のパラメータを持ちます。
それぞれの値は0から255まで取り得て、0ならその色の要素なし、255ならその色の要素が最大ってことです。例えばR255、G0、B0ならば赤色。R255、G0、B255なら紫って感じです。

それぞれの関数ではその情報を配列に入れています。
つまりimg[HEIGHT][WIDTH][3]の[HEIGHT][WIDTH]は各要素の座標的なイメージで、[3]はRGBのパラメータが格納されているイメージです。

まあ詳しい内容に関してはまた今度別の記事で書こうと思います。
とりあえず今は便利な画像読み書き関数と思っときましょう!

閾値処理

この記事では目的が物体の重心を求めることなので、ここは簡単な画像のみを扱うってことにします。(それで許して)
簡単な画像は以下のようなものです。

この黒色のよくわからないものが物体ってことです。
そして画像から物体の重心を求めると説明しましたが、

いやどうやって画像から物体を認識すんねん

まあそうなりますよね。
画像に物体があったとしても、もちろん背景なども写り込んでいるわけで、それらを区別しなければなりません。
ただ今回は上で示した通り、物体も背景も全然違う色合いでわかりやすいのでそれを利用します。
どういうことかというと、求めたい物体の色合いや濃さを持つ画素をあぶり出すってことです。
の際に使うのが閾値という概念です!

閾値、、(なんて読むんやろ、、)

しきいち、です!
まああんま聞かない言葉ですよね。 
意味は境目となる値です
まあ言葉の意味をつらつらと説明していてもキリがないので早速コードにいきましょう。

#define t 100

void th_f(unsigned char org[HEIGHT][WIDTH][3], unsigned char f[HEIGHT][WIDTH]){
    for(int w=0; w<WIDTH; w++){
        for(int h=0; h<HEIGHT; h++){
            int sum=0;
            for(int c=0; c<3; c++)
                sum += org[h][w][c];
            if(sum/3 < t)
                f[h][w] = 1;
            else
                f[h][w] = 0;
        }
    }
}

まず仮引数のorgは扱う画像が入った配列です。そんでfはどの位置の画素に物体があるかを記録する配列です。
関数の中ではすぐに二重forループを行います。これは変数を見てわかる通り、画像のすべての画素を走査するよ!!ってことです。

そしてその中の処理です。つまりある画素に注目している時ですね。

       int sum=0;
            for(int c=0; c<3; c++)
                sum += org[h][w][c];
            if(sum/3 < t)
                f[h][w] = 1;
            else
                f[h][w] = 0;

最初の方では何をやっているかっていうと、sumを定義してそこにすべてのRGBのパラメータの値の和を代入しています。ここはfor文を用いて注目画素のパラメータの部分を動かして実現してますね。

そんでif文。ここでは先ほどのsumを3で割った値がマクロ定義したtより小さいかを調べています。
まずsumを3で割るということですが、これは結局注目画素の平均的な色の暗さ(明るさ)を調べているってことになりますね。
そんでtですが、これがいわゆる閾値です。
先ほど述べた通り閾値とは境目となる値。
つまりこのif文の式は、注目画素の平均的な暗さが、閾値として設定した値より暗いかを調べている。ってことです。

おお、やっと閾値の意味がわかってきた

if文の中ですが、閾値より濃ければfの対応する要素を1に、
else文(つまり閾値以下の濃さの場合)ではfの対応する要素を0にします。

閾値より濃い部分が物体なので、つまりfには物体のある位置には1、そうでない位置には0が格納されていることになりますね。

これでこの関数の役目は終わりです!!

重心位置の算出

ここからは重心の位置を具体的に計算で求めていきます。
上で作成したfを使っていきます。まずは計算式です。

ここのx,yの値っていうのはそのまま画素の場所で良いです。
つまり配列でいうところの添字のことですね。
またfは上の関数で作ったとおり、物体のある位置では1、そうでない位置では0となる関数です。

この計算式のイメージはモーメントを重さで割って残るものが重心って感じですかね?(間違っていたら恥ずかしい、、)

まあとにかくこれで求められるのでコードを書きましょう。

void CoG(unsigned char f[HEIGHT][WIDTH], double *x_g, double *y_g){
    double x_sum=0;
    double y_sum=0;
    double sum=0;
    for(int h=0; h<HEIGHT; h++){
        for(int w=0; w<WIDTH; w++){
            sum += f[h][w];
            x_sum += w*f[h][w];
            y_sum += h*f[h][w];
        }
    }
    
    *x_g = x_sum/sum;
    *y_g = y_sum/sum;
}

仮引数のfは先ほど作ったfです。
double型のポインタx_gとy_gが求める重心のx座標とy座標を入れる変数です。

関数内で最初に定義しているx_sum、y_sumは上の式の分母、sumは分子の値を計算して格納する変数です。

そして二重forループ文にいきます。ここでも先ほどと同様に画像の全画素を走査します。
中ではそれぞれの分母分子を計算していきます。その際にfを使います。

そしてforループを抜けた後は分母を分子で割ったものを重心を格納する変数に代入して終わりです。

main関数

まずコードです。

int main(int argc, const char * argv[]) {
    unsigned char img[HEIGHT][WIDTH][3]={0};
    Read_Color("aaa.png", img);
    
    unsigned char f[HEIGHT][WIDTH] = {0};
    th_f(img, f);
    
    double x,y;
    CoG(f, &x, &y);
    
    printf("x_g:%f  y_g:%f",x,y);
    
    for(int i=-2; i<5; i++){
        for(int j=-2; j<5; j++){
            img[(int)y+i][(int)x+j][0] = 0;
            img[(int)y+i][(int)x+j][1] = 0;
            img[(int)y+i][(int)x+j][2] = 255;
        }
    }
    
    Write_Color("bbb.png", img);
    return 0;
}

前半の流れは、
配列img画像読み込み
その画像から関数th_fでfを作成
f関を用いて関数CoGで重心を計算
printfで重心を表示

って感じです。

その後のfor文では、
配列imgにおける重心の位置とその近傍8画素を赤色にしています。
for文の変数を見れば近傍8画素を扱っているのがわかると思います。
また説明するのが遅れましたが、C++ではGBRの順に入っているので[2]を255、[0][1]を0として赤色にしています。

そして最後にその変更した配列を画像として書き出して終了です!

実行した結果

実行してみましょう。
先ほど紹介した画像を用いてみます。

これをこのプログラムに処理させると、、

こんな感じです。
赤点が小さいですが、ちょうど重心っぽい位置ですね。おk。
ちなみにfから認識した物体のみを画像出力してみると、、

完璧ですね。

まとめ

以上で今回の記事を終わります。いやー疲れた。
今回は明るい背景に対して、暗い物体という条件なのだったので、閾値より小さいかで判断しました。
もちろん逆の場合は、大きいかで判断したりと臨機応変に変えていくことに注意してください。
こんな感じで画像処理の記事をどんどん書いていこうと思うのでよろしくお願いします。

ではまたー

全体のコード

#include <iostream>
#include <opencv4/opencv2/opencv.hpp>

#define WIDTH 640
#define HEIGHT 480

#define t 100

void Read_Color( const char *name, unsigned char img[HEIGHT][WIDTH][3] )
{
    cv::Mat readImage( HEIGHT, WIDTH, CV_8UC3);
    IMAGE = cv::imread(name, 1);

    for( int w=0; w<WIDTH; w++ )
        for( int h=0; h<HEIGHT; h++ )
            for( int c=0; c<3; c++ )
                img[h][w][c] = IMAGE.data[(w+h*WIDTH)*3+c];
}

void th_f(unsigned char org[HEIGHT][WIDTH][3], unsigned char f[HEIGHT][WIDTH]){
    for(int w=0; w<WIDTH; w++){
        for(int h=0; h<HEIGHT; h++){
            int sum=0;
            for(int c=0; c<3; c++)
                sum += org[h][w][c];
            if(sum/3 < t)
                f[h][w] = 1;
            else
                f[h][w] = 0;
        }
    }
}

void Write_Color( const char *name, unsigned char img[HEIGHT][WIDTH][3] )
{
    cv::Mat IMAGE( HEIGHT, WIDTH, CV_8UC3 );

    for( int w=0; w<WIDTH; w++ )
        for( int h=0; h<HEIGHT; h++ )
            for( int c=0; c<3; c++ )
                IMAGE.data[(w+h*WIDTH)*3+c] = img[h][w][c];
    
    cv::imwrite( name , IMAGE);
}

void CoG(unsigned char f[HEIGHT][WIDTH], double *x_g, double *y_g){
    double x_sum=0;
    double y_sum=0;
    double sum=0;
    for(int h=0; h<HEIGHT; h++){
        for(int w=0; w<WIDTH; w++){
            sum += f[h][w];
            x_sum += w*f[h][w];
            y_sum += h*f[h][w];
        }
    }
    
    *x_g = x_sum/sum;
    *y_g = y_sum/sum;
}

int main(int argc, const char * argv[]) {
    unsigned char img[HEIGHT][WIDTH][3]={0};
    Read_Color("aaa.png", img);
    
    unsigned char f[HEIGHT][WIDTH] = {0};
    th_f(img, f);
    
    double x,y;
    CoG(f, &x, &y);
    
    printf("x_g:%f  y_g:%f",x,y);
    
    for(int i=-2; i<5; i++){
        for(int j=-2; j<5; j++){
            img[(int)y+i][(int)x+j][0] = 255;
            img[(int)y+i][(int)x+j][1] = 0;
            img[(int)y+i][(int)x+j][2] = 255;
        }
    }
    
    Write_Color("bbb.png", img);
    return 0;
}


コメント

タイトルとURLをコピーしました