2017年2月7日火曜日

二値画像の収縮処理(ErosionFilter)

 今回は二値画像の収縮処理を実装します。収縮処理とは、二値画像における処理方法のひとつで、各ピクセルについてそのピクセルと周囲のピクセルを調べ、一つでも黒色があるならば黒色に変換する処理です。そのため、処理の後は基本的に黒色が増えます。具体的な処理などは以下のHPを参照してください。
http://imagingsolution.blog107.fc2.com/blog-entry-101.html

プログラムのコーディングに移ります。 Tutorial1Activityクラスと同じ場所にErosionFilterクラスを作成します。その後、filterメソッドに以下のコードを入力します。
    public int[] filter(int[] imageArray, int width, int height,int n) {
        int[] oimgArray = new int[imageArray.length];
        for(int nth=0;nth < n;nth++){//①
            int[] blackArray=new int[imageArray.length];//②
            for(int i =0;i < imageArray.length;i++){
                blackArray[i]=imageArray[i]& 0xff;//③
            }
            for (int x = 1; x < width-1; x++) {
                for (int y = 1; y < height-1; y++) {
                    int black=blackArray[x + y * width]*blackArray[x + y * width+1]*blackArray[x + y * width-1]
                            *blackArray[x + (y-1) * width]*blackArray[x + (y+1) * width]
                            *blackArray[x + (y-1) * width+1]*blackArray[x + (y-1) * width-1]
                            *blackArray[x + (y+1) * width+1]*blackArray[x + (y+1) * width-1];//④
                    if(black > 0){//⑤
                        oimgArray[x + y * width] = 0xFFFFFFFF;
                    }else{
                        oimgArray[x + y * width] = 0xFF000000;
                    }
                }
            }
            for (int i = 0; i < imageArray.length; i++) {
                if (oimgArray[i]==0) {
                    oimgArray[i] = 0xFFFFFFFF;//⑥
                }
            }
            imageArray=oimgArray;//⑦
        }
        return oimgArray;
    }

 ①は収縮処理の繰り返し処理です。メソッドの第四引数の数だけ収縮処理を繰り返します。
 ②は 白黒の情報のみを持つ配列です。③によって入力用配列であるimageArrayから白(255)または黒(0)の情報を得て格納します。
 ④では、各ピクセルとその周囲のピクセルについて、白黒の値の論理積を求めます。このとき、白がいくつあっても黒が一つでもあれば④の答えは0となります。逆にすべてのピクセルが白の場合は答えは0より大きな値になります。
 ⑤では④の答えから0より大きい場合には出力用配列に白の情報を格納し、0であるならば黒の情報を格納します。
 ⑥では④で処理の行わなかった画像の一番外側について白の情報を格納します。
 ⑦では出力用配列を入力用配列にコピーし、繰り返し処理ができるようにしています。

 Tutorial1ActivityクラスにErosionFilterを記入する際には必ず二値処理の後に加えます。今回は以下のような順番で記載しました。
imageArray =new PTileFilter().filter(imageArray, imagewidth, imageheight,50);
imageArray =new ErosionFilter().filter(imageArray, imagewidth, imageheight, 2);
imageArray =new HistogramFilter().filter(imageArray, imagewidth, imageheight);

ErosionFilterの第四引数は収縮回数を示しており、回数だけ収縮処理が繰り返されます。この例では2回繰り返されます。
 以下に実機での実行結果を示します。繰り返し回数は2です。Pタイル法フィルターの実行結果に比べて黒が増えていることがわかります。ヒストグラムでも0(黒)のバーが長く、255(白)のバーが短くなっています。


Pタイル法フィルター(PTileFilter)

 今回は二値変換の方法の一つであるPタイル法について書きます。Pタイル法は画像の二値化したい領域が全画像の領域に占める割合を パーセント(%)で指定し二値化する手法です。Pタイル法については以下のHPをご覧ください。
http://imagingsolution.blog107.fc2.com/blog-entry-112.html

プログラムのコーディングを行います。  Tutorial1Activityクラスと同じ場所にPTileFilterクラスを作成します。その後、filterメソッドに以下のコードを入力します。
    public int[] filter(int[] imageArray,int width, int height, int threshould) {
        int tone[]=new int[256];
        int length=width*height;
        int border_length=length*threshould/100;//①
        int y_val = 0;
        int rgb = 0;
        int red = 0, green = 0, blue = 0;
        int[] oimgArray = new int[imageArray.length];
        for (int j = 0; j < imageArray.length; j++) {
            rgb = imageArray[j];
            red = (rgb >> 16) & 0xff;
            green = (rgb >> 8) & 0xff;
            blue = (rgb) & 0xff;
            y_val = ( 2 * red + 4 * green + blue ) / 7;//②
            oimgArray[j]=y_val;
            tone[y_val]++;//③
        }
        int total=0;
        int border=-1;
        for(int i =0;i < 256;i++){
            total+=tone[i];//④
            if(total>border_length){//⑤
                border=i;
                break;
            }
        }
        for (int j = 0; j < imageArray.length; j++) {
            if(oimgArray[j]>border){//⑥
                oimgArray[j] = 0xFFFFFFFF;
            }else{
                oimgArray[j] = 0xFF000000;
            }
        }
        return oimgArray;
    }

 ①は全ピクセル数について、閾値の割合に対応するピクセル数を取得しています。閾値が50ならば、全ピクセル数の50%に当たる数を取得します。
 ②はBinaryFilterによる二値化と同様に輝度を計算しています。
 ③は各輝度(0~255)について、出現回数を計算しています。
 ④は各輝度の出現回数を、0から順番に積み上げていきます。そして、⑤で①のピクセル数以上になったときに、その輝度を境界線として記録します。
 ⑥では 各ピクセルの輝度について境界線と比較し、高いならば白に、低いならば黒に色を変換します。

最後に、 Tutorial1Activityクラスのint imageArray[]=ba2ia(ba);の下に以下の文を記入します。
imageArray =new PTileFilter().filter(imageArray, imagewidth, imageheight,50);

この時、第四引数が閾値(%)を示します。範囲を外れてもエラーは発生しませんが、基本的に閾値の範囲は0~100までとなります。なお、BinaryFilterについてはコメントアウトしておいてください。
 以下に実機での実行結果を示します。 閾値は50%です。BinaryFilterと比較して、白の割合が増えています。また、ヒストグラムを見るとわかりますが、BinaryFilterでは異なっていた0のバーと255のバーの長さが、Pタイル法ではほぼ同じになっています。

2017年2月6日月曜日

閾値二値化(BinaryFilter)

 今回は二値化処理のなかで、指定閾値で二値化を行うBinaryFilterを実装します。BinaryFilterでは、あるピクセルについて輝度を計算し、その輝度が指定値以上ならばそのピクセルを白に、指定値以下ならそのピクセルを黒に変換する処理です。輝度の計算方法は複数ありますが、今回は計算速度を考慮した輝度計算を行います。なお、RGB値を輝度に変換し、それを新たなRGB値にする処理はグレースケール処理となります。
 まずは、 前回同様Tutorial1Activityクラスと同じ場所に、BinaryFilterクラスを作成します。その後以下のfilterメソッドを記述します。

    public int[] filter(int[] imageArray,int width, int height,int threshould) {
        int y_val = 0;
        int rgb = 0;
        int red = 0, green = 0, blue = 0;
        int[] oimgArray = new int[imageArray.length];//①
        for (int j = 0; j < imageArray.length; j++) {
            rgb = imageArray[j];
            red = (rgb >> 16) & 0xff;
            green = (rgb >> 8) & 0xff;
            blue = (rgb) & 0xff;
            y_val = ( 2 * red + 4 * green + blue ) / 7;//②
           if (y_val > threshould) {//③
                oimgArray[j] = 0xFFFFFFFF;
            } else {
                oimgArray[j] = 0xFF000000;
            }
        }
        return oimgArray;
    }


 ①では、出力用に画像のint配列と同じ長さの空の配列を作成します。
②では輝度を計算します。輝度の計算ではもっと厳密な計算式がありますが、②の計算式では小数点計算を行わなくてすむため、計算速度が速いです。なお、輝度やグレイスケール化については以下のHPをご覧ください。
http://daredemopc.blog51.fc2.com/blog-entry-877.html
 ③では得られた輝度と閾値を比較し、閾値より高い場合にはピクセルを白(255,255,255)に、閾値以下ならばピクセルを黒(0,0,0)にします。

 最後に、Tutorial1Activityクラスのint imageArray[]=ba2ia(ba);の下に以下の文を記入します。

      imageArray =new BinaryFilter().filter(imageArray, imagewidth, imageheight,120);

このとき、第4引数が閾値を示します。範囲を外れてもエラーは発生しませんが、基本的に閾値の範囲は0~254までとなります。
 以下に実機での実行結果を示します。閾値は120です。ヒストグラムからわかるように全てのピクセルが白または黒になっています。

Histogramフィルターの作成

今回からは画像処理の具体的な方法と実装について書いていきたいと思います。今回実装するのは画像のヒストグラムを表示するフィルターです。ヒストグラムは、各カラーの明るさのレベル別にピクセル数をグラフ化し、画像内のピクセル分布を示したものです。具体的には以下のURLをご覧ください。
https://helpx.adobe.com/jp/photoshop/using/viewing-histograms-pixel-values.html
Photoshop上では下の図のようにヒストグラムが表示されます。


 ヒストグラムを表示することで、画像処理によって色の分布がどのように変化するかを分析することができます。そのため、画像処理として最初に導入したいと思います。
 
 まず、Tutorial1Activityクラスと同じ場所に、HistogramFilterクラスを作成します。下の図のように選択してから、右クリックでNew→Java Classを選択します。
その後、クラスの内容を記入する画面では、下の図のようにHistogramFilterと記入し、OKを押します。
これでTutorial1Activityクラスと同じ場所に、HistogramFilterクラスが作成されました。
 次に、HistogramFilterクラスの内容を記入していきます。第一にfilterメソッドを作成します。比較的長いですが、これはred、green、blueそれぞれを分けて処理しているためです。各色で行っている処理は同じです。
filter
   public int[] filter(int[] imageArray, int width, int height) {
        int tone[][]=ToneCalculator(imageArray);//①
        int rate[][]=new int[3][256];
        int[] oimgArray =imageArray.clone();//②
        int red=0;
        int green=0;
        int blue=0;
        int maxr=0;
        int maxg=0;
        int maxb=0;
        //red--------------------------------------
        for(int i =0;i < 256;i++){
            if(maxr < tone[0][i]){
                maxr=tone[0][i];//③
            }
        }
        for(int i =0;i < 256;i++){
            rate[0][i]=(tone[0][i]*50)/maxr;//④
        }
        for(int i =0;i < 50;i++){
            oimgArray[(height-2-i -102)*width]=255*16777216;//⑤
            for(int n =0;n < 256;n++){
                oimgArray[(height-2-i-102)*width+n+1]=255*16777216+255*65536+255*256+255;//⑥
            }

        }
        for(int n =0;n < 256;n++){
            red=rate[0][n];
            if(red > 0){
                for(int rn=1;rn < red;rn++){
                    oimgArray[(height-1-rn-102)*width+n+1]=255*16777216+255*65536;//⑦
                }
            }
        }
        for(int n =0;n < 257;n++){
            oimgArray[(height-1-102)*width+n]=255*16777216;//⑧
        }
        //green--------------------------------------
        for(int i =0;i < 256;i++){
            if(maxg < tone[1][i]){
                maxg=tone[1][i];
            }
        }
        for(int i =0;i < 256;i++){
            rate[1][i]=(tone[1][i]*50)/maxg;
        }
        for(int i =0;i < 50;i++){
            oimgArray[(height-2-i-51)*width]=255*16777216;
            for(int n =0;n < 256;n++){
                oimgArray[(height-2-i-51)*width+n+1]=255*16777216+255*65536+255*256+255;
            }

        }
        for(int n =0;n < 256;n++){
            green=rate[1][n];
            if(green > 0){
                for(int gn=1;gn < green;gn++){
                    oimgArray[(height-1-gn-51)*width+n+1]=255*16777216+255*256;
                }
            }
        }
        for(int n =0;n < 257;n++){
            oimgArray[(height-1-51)*width+n]=255*16777216;
        }
        //blue--------------------------------------
        for(int i =0;i < 256;i++){
            if(maxb < tone[2][i]){
                maxb=tone[2][i];
            }
        }
        for(int i =0;i < 256;i++){
            rate[2][i]=(tone[2][i]*50)/maxb;
        }
        for(int i =0;i < 50;i++){
            oimgArray[(height-2-i)*width]=255*16777216;
            for(int n =0;n < 256;n++){
                oimgArray[(height-2-i)*width+n+1]=255*16777216+255*65536+255*256+255;
            }

        }
        for(int n =0;n < 256;n++){
            blue=rate[2][n];
            if(blue > 0){
                for(int bn=0;bn < blue;bn++){
                    oimgArray[(height-1-bn)*width+n+1]=255*16777216+255;
                }
            }
        }
        for(int n =0;n < 257;n++){
            oimgArray[(height-1)*width+n]=255*16777216;
        }
        return oimgArray;
    }

①では、3色256諧調分の配列を作り、画像のRGB各色の値について配列の対応する場所を増やしていく処理を行います。下にToneCalculatorメソッドを記載します。
    public int[][] ToneCalculator(int[] imageArray){
        int tone[][]=new int[3][256];
        int length = imageArray.length;
        int rgb;
        int red,green,blue;
        for(int i =0;i < length;i++){
            rgb = imageArray[i];
            red = (rgb >> 16) & 0xff;
            green = (rgb >> 8) & 0xff;
            blue = (rgb) & 0xff;
            tone[0][red]+=1;
            tone[1][green]+=1;
            tone[2][blue]+=1;
        }
        return tone;
    }

 ②では出力用にint配列をコピーしています。以降では赤色についてのみ述べます。
 ③では最も多く出現した色の値を求めています。この値がヒストグラム上での上限値となります。
 ④ではそれぞれの色の値(0~255)について、出現した色の値を最頻色の値で割ることで、最頻色の値に対する割合を出しています。さらに、50をかけることで0~50の51段階に変更しています。
 ⑤は画像上にヒストグラムを作る際の、左端の黒いバーを描写しています。
 ⑥は同様にヒストグラムの白い背景を描写しています。
 ⑦ではヒストグラムの描写を行っています。赤では下限位置から④でえられた各色の出現割合(0~50)の分だけ赤いピクセル(255,0,0)を描写しています。
 ⑧では下限を示す黒い横棒を描写しています。
 以上の操作を行うことにより、画像にヒストグラムを追加することができます。最後に、Tutorial1Activityクラスのint imageArray[]=ba2ia(ba);とba=ia2ba(imageArray);の間(前回の④と⑤の間)に以下の文を加えます。
 
imageArray=new HistogramFilter().filter(imageArray, imagewidth, imageheight);

今後、同じような文を同じ位置に加えていきますが、この文(HistogramFilter)は最後に記入することをお勧めします。画像処理後のヒストグラムを取得するためです。
 下に以上の編集を行った結果得られる画像を示します。画像の左下にヒストグラムが表示されます。この図ではRGB各色で白飛び(値が255)や黒つぶれ(値が0)が発生していることがわかります。

以上でヒストグラムの追加の説明を終わります。図のようにヒストグラムの画像に占める割合が大きいので、必要に応じて50段階ではなく30段階にすることや、横幅を256ではなく128にするなどの調整を行ってください。

Tutorial1Activityで画像処理を行うための編集2

前回に引き続き、Tutorial1Activityを編集し、細かい画像処理が可能な形に変更します。
今回は前回編集したonCameraFrameメソッドの中の、2つのImgproc.resizeおよびif文の間に以下の文を加えます。

        int imagewidth=mat.cols();//①
        int imageheight=mat.rows();//①
        byte[] ba=mat2ba(mat);//②
        mat = null;

        ba=ba4toba3(ba);//③
        int imageArray[]=ba2ia(ba);//④
        
        //ここで画像処理
        
        ba=ia2ba(imageArray);//⑤
        mat=ba2mat(imagewidth,imageheight,ba);//⑥
        ba=null;

 ①はそれぞれ、matから画像の幅と高さを取得しています。この値は実機によって異なる値です。また、リサイズしている場合にはリサイズ後の値が入ります。
 ②はmatをbyte配列に変換するメソッドです。詳しいメソッド内容は後述します。この時点でのbyte配列は、一つの画素・ピクセルを4個のbyteで表しており、配列上ではred、green、blue、alphaの順に並んでいます。
 ③ではこのバイト配列を変換し、一つのピクセルを3個のbyteで表すように変換します。ここではbyte配列からalphaを削除します。
 ④ではbyte配列をint配列に変換します。このint配列は一つの画素を一つのintで表しており、255*16777216を固定のalpha値、65536の倍数をred、256の倍数をgreen、1の倍数をblueで表しています。このint配列が、このブログで行う画像処理の基本的な処理対象となります。
 ⑤では画像処理した後のint配列を一度byte配列に戻します。この時点でのbyte配列は③と同じく一つのピクセルを3個のbyteで表しています。
 ⑥ではbyte配列をmatに変換しています。この際、必ず画像の幅と高さが必要になります。
 以上が今回の編集部分です。次に、各メソッドの内容を記載します。

mat2ba

    public static byte[] mat2ba(Mat mat){
        int dataSize=mat.cols()*mat.rows()*(int)mat.elemSize();
        byte[] data = new  byte[dataSize];
        mat.get(0, 0,data);
        mat=null;
        return data;
    }

ba4toba3
    public byte[] ba4toba3(byte ba[]){
        int length=ba.length;
        int slash=length/4;
        byte result[]=new byte[slash*3];
        for(int i =0;i < slash;i++){
            result[i*3]=ba[i*4];//r
            result[i*3+1]=ba[i*4+1];//g
            result[i*3+2]=ba[i*4+2];//b

        }
        ba=null;
        return result;
    }

ba2ia
    public static int[] ba2ia(byte ba[]){
        int length = ba.length/3;
        int result[]=new int[length];
        int r,g,b;
        for(int i =0;i < length;i++){
            r=ba[i*3]& 0xFF;
            g=ba[i*3+1]& 0xFF;
            b=ba[i*3+2]& 0xFF;
            result[i]=rgb2int(r, g, b);
        }
        ba=null;
        return result;
    }

ba2iaで使用しているrgb2int (r、g、bの値をint型に変換する)
    public static int rgb2int(int r,int g,int b){
        return 255*16777216+r*65536+g*256+b;
    }

ia2ba
    public static byte[] ia2ba(int ia[]){
        int length=ia.length;
        int red,green,blue;
        byte result[]=new byte[length*3];
        int rgb=0;
        for(int i =0;i < length;i++){
            rgb = ia[i];
            red = (rgb >> 16) & 0xff;
            green = (rgb >> 8) & 0xff;
            blue = (rgb) & 0xff;
            result[i*3]=(byte)red;
            result[i*3+1]=(byte)green;
            result[i*3+2]=(byte)blue;
        }
        ia=null;
        return result;
    }


ba2mat (CvTypeでorg.opencv.core.CVTypeのインポートが必要)
    public Mat ba2mat(int width,int height,byte pixels[]){
        Mat mat = new Mat(height,width, CvType.CV_8UC3);
        mat.put(0,0,pixels);
        return mat;
    }


以上が今回必要なメソッドです。実機で実行すると、これまでよりフレームレートの落ちたプレビューがみられるでしょうが、それ以外に変化はありません。

2017年2月5日日曜日

Tutorial1Activityで画像処理を行うための編集1

Tutorial1Activityを編集し、細かい画像処理が可能な形に変更します。

画面左のProjectタブからAndroid タブを選択し、OpenCVTutorial1CameraPreview→java→org.opencv.tutorial1→Tutorial1Activityを開きます。


このとき、前回でOpenCV Managerを使用しない設定にしている人は、 onCreateメソッドの先頭に以下の文を加えます。

System.loadLibrary("opencv_java3");

次に、97行目付近のonCameraFrameメソッドを編集します。
編集前は以下のようになっています。

public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
        return inputFrame.rgba();
}

このメソッドはプレビューで表示される画像を返すメソッドで、inputFrameを受け取り、それをMat形式に変換したデータをreturnしています。
よって、inputFrameで得られたデータを画像処理し、最終的にMat形式に変換してあげることで、画像処理したプレビューが見られるようになります。
今回は、onCameraFrameメソッドを以下のように編集します。

    public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
        Mat mat=inputFrame.rgba();//①
        inputFrame=null;

        double resize=0.5;//②
        if(resize!=1){
            Imgproc.resize(mat, mat, new Size(),resize,resize, Imgproc.INTER_AREA);//③
        }


        if(resize!=1){
            Imgproc.resize(mat, mat, new Size(),1/resize, 1/resize, Imgproc.INTER_CUBIC);//④
        }
        return mat;//⑤
    }

①ではinputFrameをmat形式に変換します。②では画像処理するにあたり、リサイズして処理するか、そしてリサイズする倍率を指定しています。これは、プレビュー画像が大きいときに、そのままのサイズで画像処理を行うと非常に時間がかかってしまうことがあるためです。そのため、一度二分の一のサイズなどにリサイズしてから画像処理し、またリサイズしなおして表示するという流れが必要になる場合があります。このリサイズ倍率は実機でのプレビューサイズに応じて変える必要があります。目安として横幅が1920や1440の場合は0.5を、720や640の場合は1でリサイズしないほうがよいでしょう。なお、倍率の小数点以下が0.33など1を割り切れない値の場合、エラーが発生する恐れがあります。
③では縮小リサイズ処理を、④では拡大リサイズ処理を行っています。このときImgprocとSizeでクラスをインポートする必要があります。その際はImgprocやSizeにカーソルを当ててからAlt+Enterを押し、Import Classを選びましょう。また、Sizeでクラスをインポートするときは複数の選択肢が出ますが、org.opencv.coreを選びましょう。
 ⑤ではリサイズ処理したmatを返します。このmatが画像として実機のディスプレイに表示されます。

この時点で実機で実行し、エラーが発生しないことを確認しましょう。
下に実行結果の図を載せますが、気持ち若干のぼやけが生じます。




2017年2月4日土曜日

Android StudioにOpenCVをインストール

Android StudioにOpenCVをインストールします。
以下のサイトからOpenCV for Androidをダウンロードします。
http://opencv.org/

ダウンロードしたファイルを任意の場所に展開します。(Cドライブ直下等)
その後、Android Studio上で任意のプロジェクト(初期設定のプロジェクトで可)を開いた状態で、File→New→Import Moduleを選択し、以下のフォルダを選択します。(Cドライブ直下の場合)

C:\OpenCV-android-sdk\sdk\java

Module NameがopenCVLibrary320等となっていればOKです。
Nextを押し、設定を変えずにFinishを押します。

次に、OpenCV for Androidのサンプルプログラムをインポートします。
File→New→Import Projectを選択し、以下のフォルダを選択します。(Cドライブ直下の場合)

C:\OpenCV-android-sdk\samples\tutorial-1-camerapreview

必要に応じて、Import Destination Directoryを変更し、Nextを押し、次の画面でFinishを押します。

次に、build.gradleの設定をします。
以下の図のように、Project画面に切り替え→tutorial-1-camerapreview→openCVLibrary320→build.gradleの中で、 各SdkVersionを21以上に設定します。



また、tutorial-1-camerapreview→openCVTutorial1CameraPreview→build.gradleについても同様の設定を行います。

右上にSync Nowが表示されている場合クリックします。
そして、下のほうのMessages Gradle Syncの中に発生しているエラーの、Install missing platform(s) and sync projectをクリックします。

インストールが実行され、Finishを押します。
Syncが実行され、しばらくすると処理が終わります。

次に、実機にOpenCV Manager をインストールします。
図のようにPlayストアにてOpenCV Managerを検索し、インストールします。


OpenCV Managerをインストールしたら、実機とパソコンをつなぎ、上のメニューのRun→Run 'openCVTutorial1CameraPreview'を押し、図のように実機を選択した状態でOKを押します。





 起動すると実機の画面上にカメラのプレビューが映ります。
下の図はそのスクリーンショットです 。



以上で、OpenCVのインストールとサンプルプログラムの実行を終わります。
なお、OpenCV Managerをインストールしたくない場合や、できない環境の場合には以下のサイトを参考にしてください。
http://qiita.com/denjin-m/items/8b2f30b98ef4529b8f1f
Cppに関することは行わなくても大丈夫です。