アプリ開発#3「open cvを使ってみた」

アプリ開発

android app.です。
こちらはjavaで開発しました。
opencvにまつわる情報でkotlinのものが少ないため。

アプリ紹介

該当アプリはこちらになります。
Google Play で手に入れよう

紹介動画

苦労した点

今回は色々苦労があった。
①viewでタッチの機能を備えるため、画像をimage viewでなく、自作
②タッチで座標を拾う
③open cvを使う

①viewを自作

こちらのようにviewを自作した。詳細は中を見たほうが理解が進むであろう。

public class Myview extends View {
    private Context context;
    private Drawable drawable;
    private Rect lastRect;
    private Bitmap bmpview=null;
    private Integer flgview;
    private int[][] posi={{1,125,125},{2,325,125},{3,525,125},{4,725,125}};

    public Myview(Context context){
        super(context);
        this.context=context;
    }

    public Myview(Context context, AttributeSet attrs) {
        super(context,attrs);
        flgview=0;
    }

    //ここでメインアクティビティからbmpを受け取り、viewに描画の準備
    public void showCanvas(Bitmap bmp, Integer flgbase){
        bmpview=bmp;
        flgview=flgbase;
        invalidate();
    }

    @Override
    protected void onDraw(Canvas c) {
        super.onDraw(c);
        c.drawColor(Color.WHITE);
        int w=this.getWidth();
        int h=this.getHeight();
        Rect rect=new Rect(5,5,w-10,h-10);

        if(bmpview==null){
            Paint p=new Paint();
            p.setStyle(Paint.Style.STROKE);
            p.setColor(Color.DKGRAY);
            c.drawRect(rect,p);

            for (int i=0; i<10; i++){
                Paint p2=new Paint();
                p2.setStyle(Paint.Style.FILL);
                p2.setColor(Color.rgb(25*i,0,0));
                c.drawCircle(25*i+125, 25*i+125, 100, p2);
            }
        }
        else {
            //上で受け取ったbmpを使ってbmpを画面に描画
            c.drawBitmap(bmpview,0,0,new Paint());

            Paint p00=new Paint();
            p00.setStyle(Paint.Style.FILL);
            p00.setColor(Color.rgb(255,0,0));

            //メインアクティビティの操作で赤点を出す
            switch (flgview) {
                case 0:
                    break;
                case 1:
                        //タッチで移動
                        c.drawCircle(posi[0][1], posi[0][2], 10, p00);
                        c.drawCircle(posi[1][1], posi[1][2], 10, p00);
                        c.drawCircle(posi[2][1], posi[2][2], 10, p00);
                        c.drawCircle(posi[3][1], posi[3][2], 10, p00);
                    break;
                case 2:

                    break;
                case 3:

                    break;
                case 4:

                    break;
            }
        }
    }

    //ここで画面タッチ時の処理をする
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action=event.getAction();
        int x=(int)event.getX();
        int y=(int)event.getY();

        //四隅ドラッグ
        if(flgview==1) {
            int index = -1;
            int i;
            int hantei = 30;

            //タッチの処理の仕方がいくつかあるが、色々やった結果これに。他でやると赤点が重なった。。
            switch (action) {
                case MotionEvent.ACTION_MOVE:
                    for (i = 0; i < 4; i++) {
                        if (posi[i][1] - hantei < x && posi[i][1] + hantei > x &&
                                posi[i][2] - hantei < y && posi[i][2] + hantei > y) {
                            index = i + 1;
                        }
                    }
                    break;
            }
            switch (index) {
                case -1:
                    break;
                case 1:
                    posi[0][1] = x;
                    posi[0][2] = y;
                    break;
                case 2:
                    posi[1][1] = x;
                    posi[1][2] = y;
                    break;
                case 3:
                    posi[2][1] = x;
                    posi[2][2] = y;
                    break;
                case 4:
                    posi[3][1] = x;
                    posi[3][2] = y;
                    break;
                default:
            break;
            }
        }
        invalidate();
        return true;
    }
    //これはメインアクティビティで使う。
    public int[][] getposition(){
        return posi;
    }
}

②タッチの座標を拾い処理&③opencvで処理

以下のようにタッチした画像を拾って4角の座標をmyviewの関数から拾う。
そして4角の座標から射影変換を実施
こちらはmain actibityに記載。
4角の座標を拾って、opencvの処理に入れるのに苦労した。
射影変換では角の順番を正確に入れる必要があるため。

    private Bitmap shaei(Bitmap bmphiki){
        Mat mat=new Mat();
        Utils.bitmapToMat(bmphiki, mat); // OpenCVの行列へ
        int [][]posi_shaei=myview.getposition();
        int [][]posi_shaei1=new int[4][3];
        int w=bmphiki.getWidth();
        int h=bmphiki.getHeight();
        Bitmap bmpmodori=Bitmap.createBitmap(
                bmphiki.getWidth(), bmphiki.getHeight(), Bitmap.Config.ARGB_8888);
        int kyori;
        Integer[] irekae={-1,-1,-1,-1};
        int count=0;

        kyori=w*w+h*h;
        //左上
        for(int i=0; i<4; i++){
            if((posi_shaei[i][1]*posi_shaei[i][1]+posi_shaei[i][2]*posi_shaei[i][2])<kyori){
                irekae[0]=i;
                kyori=(posi_shaei[i][1]*posi_shaei[i][1]+posi_shaei[i][2]*posi_shaei[i][2]);
            }
        }

        kyori=w*w+h*h;
        //右上
        for(int i=0; i<4; i++){
            if(Arrays.asList(irekae).contains(i)){
                count=count+1;
            }
            else{
                if(((w-posi_shaei[i][1])*(w-posi_shaei[i][1])
                        +(posi_shaei[i][2])*(posi_shaei[i][2]))<kyori){
                    irekae[1]=i;
                    kyori=((w-posi_shaei[i][1])*(w-posi_shaei[i][1])
                            +(posi_shaei[i][2])*(posi_shaei[i][2]));
                }
            }
        }

        kyori=w*w+h*h;
        //右下
        for(int i=0; i<4; i++){
            if(Arrays.asList(irekae).contains(i)){
                count=count+1;
            }
            else{
                if(((posi_shaei[i][1]-w)*(posi_shaei[i][1]-w)
                        +(posi_shaei[i][2]-h)*(posi_shaei[i][2]-h))<kyori){
                    irekae[2]=i;
                    kyori=((posi_shaei[i][1]-w)*(posi_shaei[i][1]-w)
                            +(posi_shaei[i][2]-h)*(posi_shaei[i][2]-h));
                }
            }
        }

        kyori=w*w+h*h;
        //左下
        for(int i=0; i<4; i++){
            if(Arrays.asList(irekae).contains(i)){
                count=count+1;
            }
            else{
                if(((posi_shaei[i][1])*(posi_shaei[i][1])
                        +(posi_shaei[i][2]-h)*(posi_shaei[i][2]-h))<kyori){
                    irekae[3]=i;
                    kyori=((posi_shaei[i][1])*(posi_shaei[i][1])
                            +(posi_shaei[i][2]-h)*(posi_shaei[i][2]-h));
                }
            }
        }

        //射影変換処理
        List<Point> srcPoints_t=new ArrayList();

        for (int i=0;i<4;i++){
            posi_shaei1[i][0]=i;
            posi_shaei1[i][1]=posi_shaei[irekae[i]][1];
            posi_shaei1[i][2]=posi_shaei[irekae[i]][2];
            srcPoints_t.add(new Point(posi_shaei1[i][1],posi_shaei1[i][2]));
        }

        Mat srcPoints= Converters.vector_Point_to_Mat(srcPoints_t, CvType.CV_32F);

        List<Point> dstpoints_t=new ArrayList();
        dstpoints_t.add(new Point(0,0)); //0 左上
        dstpoints_t.add(new Point(w,0)); //1 右上
        dstpoints_t.add(new Point(w,h)); //2 右下
        dstpoints_t.add(new Point(0,h)); //3 左下

        Mat dstpoints=Converters.vector_Point_to_Mat(dstpoints_t, CvType.CV_32F);

        Mat perspectiveMmat= Imgproc.getPerspectiveTransform(srcPoints,dstpoints);
        Imgproc.warpPerspective(
                mat,mat,perspectiveMmat,mat.size(),Imgproc.INTER_LINEAR);

        Utils.matToBitmap(mat, bmpmodori);
        return bmpmodori;
    }

 せっかくなのでmain activityの全容

参考になればと。処理はコードから察してください。。
画像はアプリ固有フォルダに格納される。

public class MainActivity extends AppCompatActivity {

    private static final int RESULT_PICK_IMAGEFILE = 1000;
    private float pointX;
    private float pointY;
    private Myview myview;
    private Bitmap bmp;
    private int itemid_button=-1;
    private Button Button1;
    private Button Button2;
    private Button Button3;
    private int seekvalue=50;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //自作viewとレイアウトの紐づけ
        myview=this.findViewById(R.id.Myview01);

        //opencv おまじない
        OpenCVLoader.initDebug();

        //ボタン設定
        Button1=findViewById(R.id.button1);
        Button2=findViewById(R.id.button2);
        Button3=findViewById(R.id.button3);

        //写真読み込み
        Button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
                intent.addCategory(Intent.CATEGORY_OPENABLE);
                intent.setType("image/*");
                startActivityForResult(intent, RESULT_PICK_IMAGEFILE);
            }
        });

        Button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(itemid_button==R.id.no1){
                    bmp=shaei(bmp);
                    myview.showCanvas(bmp, 0);
                }
                if(itemid_button==R.id.no2){
                    LayoutInflater inflater=(LayoutInflater)MainActivity.this.
                            getSystemService(LAYOUT_INFLATER_SERVICE) ;
                    final View layout=inflater.inflate(
                            R.layout.dialog_contents_item,
                            (ViewGroup)findViewById(R.id.dialogcustom)
                    );

                    //seekbar設定 (view)layoutからfindviewする!!
                    SeekBar seek = (SeekBar) layout.findViewById(R.id.scale_value);
                    seek.setProgress(seekvalue);  //バーの値に応じた初期位置に
                    seek.setOnSeekBarChangeListener(new SeekBarChangeListener());

                    AlertDialog.Builder builder=new AlertDialog.Builder(
                            MainActivity.this);
                    builder.setTitle("settting threshold");
                    builder.setView(layout);

                    builder.setPositiveButton("OK", new DialogInterface.OnClickListener(){
                        public void onClick(DialogInterface dialog, int which){
                            Toast.makeText(MainActivity.this,String.valueOf(seekvalue),Toast.LENGTH_SHORT).show();
                            bmp=background(bmp, seekvalue);
                            myview.showCanvas(bmp, 0);
                        }
                    });

                    builder.setNegativeButton("cancel", new DialogInterface.OnClickListener(){
                        public  void onClick(DialogInterface dialog, int which){

                        }
                    });
                    Log.i("tag0","tag0");
                    builder.create().show();

                }
                if(itemid_button==R.id.no3){
                    bmp=openingshori(bmp);
                    myview.showCanvas(bmp, 0);
                }
            }
        });

        Button3.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if((itemid_button==R.id.no1)||
                        (itemid_button==R.id.no2)||
                        (itemid_button==R.id.no3)){
                    //画像の保存///////////////////////
                    Context context = getApplicationContext();
                    File path = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);

                    File fname=new File( path,
                            new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss",
                            java.util.Locale.JAPAN).format(new Date())
                            + ".jpg");

                    ByteArrayOutputStream baos=new ByteArrayOutputStream();
                    bmp.compress(Bitmap.CompressFormat.JPEG, 100, baos);
                    byte[] bmptojpg=baos.toByteArray();

                    try{
                        FileOutputStream fos=new FileOutputStream(fname, true);
                        fos.write(bmptojpg);
                        fos.close();
                        Toast.makeText(MainActivity.this,"saved",Toast.LENGTH_SHORT).show();
                    }catch (Exception e){
                        Toast.makeText(MainActivity.this,"error...",Toast.LENGTH_SHORT).show();
                    }
                }
            }
        });

    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
        if (requestCode == RESULT_PICK_IMAGEFILE && resultCode == RESULT_OK) {
            Uri uri = null;
            if (resultData != null) {
                uri = resultData.getData();

                try {
                    bmp = getBitmapFromUri(uri);
                    myview.showCanvas(bmp, 0);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private Bitmap getBitmapFromUri(Uri uri) throws IOException {
        ParcelFileDescriptor parcelFileDescriptor =
                getContentResolver().openFileDescriptor(uri, "r");
        FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
        Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
        parcelFileDescriptor.close();
        return image;
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater=getMenuInflater();
        inflater.inflate(R.menu.menu_options,menu);
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        if (bmp != null) {
            int itemId = item.getItemId();
            itemid_button=itemId;

            switch (itemId) {
                case R.id.no1:
                    Toast.makeText(this, "please put red dot to 4 edge.", Toast.LENGTH_SHORT).show();
                    myview.showCanvas(bmp, 1);
                    Button2.setText("finish!!");
                    Button3.setText("save!!");
                    break;
                case R.id.no2:
                    Toast.makeText(this, "clean background", Toast.LENGTH_SHORT).show();
                    myview.showCanvas(bmp, 2);
                    Button2.setText("clean");
                    Button3.setText("save!!");
                    break;
                case R.id.no3:
                    Toast.makeText(this, "thin line del.", Toast.LENGTH_SHORT).show();
                    Button2.setText("line del.");
                    Button3.setText("save!!");
                    break;
            }
        }
        else{
            Toast.makeText(this, "please set image", Toast.LENGTH_SHORT).show();
        }
        return super.onOptionsItemSelected(item);
    }

    private Bitmap shaei(Bitmap bmphiki){
        Mat mat=new Mat();
        Utils.bitmapToMat(bmphiki, mat); // OpenCVの行列へ
        int [][]posi_shaei=myview.getposition();
        int [][]posi_shaei1=new int[4][3];
        int w=bmphiki.getWidth();
        int h=bmphiki.getHeight();
        Bitmap bmpmodori=Bitmap.createBitmap(
                bmphiki.getWidth(), bmphiki.getHeight(), Bitmap.Config.ARGB_8888);
        int kyori;
        Integer[] irekae={-1,-1,-1,-1};
        int count=0;

        kyori=w*w+h*h;
        //左上
        for(int i=0; i<4; i++){
            if((posi_shaei[i][1]*posi_shaei[i][1]+posi_shaei[i][2]*posi_shaei[i][2])<kyori){
                irekae[0]=i;
                kyori=(posi_shaei[i][1]*posi_shaei[i][1]+posi_shaei[i][2]*posi_shaei[i][2]);
            }
        }

        kyori=w*w+h*h;
        //右上
        for(int i=0; i<4; i++){
            if(Arrays.asList(irekae).contains(i)){
                count=count+1;
            }
            else{
                if(((w-posi_shaei[i][1])*(w-posi_shaei[i][1])
                        +(posi_shaei[i][2])*(posi_shaei[i][2]))<kyori){
                    irekae[1]=i;
                    kyori=((w-posi_shaei[i][1])*(w-posi_shaei[i][1])
                            +(posi_shaei[i][2])*(posi_shaei[i][2]));
                }
            }
        }

        kyori=w*w+h*h;
        //右下
        for(int i=0; i<4; i++){
            if(Arrays.asList(irekae).contains(i)){
                count=count+1;
            }
            else{
                if(((posi_shaei[i][1]-w)*(posi_shaei[i][1]-w)
                        +(posi_shaei[i][2]-h)*(posi_shaei[i][2]-h))<kyori){
                    irekae[2]=i;
                    kyori=((posi_shaei[i][1]-w)*(posi_shaei[i][1]-w)
                            +(posi_shaei[i][2]-h)*(posi_shaei[i][2]-h));
                }
            }
        }

        kyori=w*w+h*h;
        //左下
        for(int i=0; i<4; i++){
            if(Arrays.asList(irekae).contains(i)){
                count=count+1;
            }
            else{
                if(((posi_shaei[i][1])*(posi_shaei[i][1])
                        +(posi_shaei[i][2]-h)*(posi_shaei[i][2]-h))<kyori){
                    irekae[3]=i;
                    kyori=((posi_shaei[i][1])*(posi_shaei[i][1])
                            +(posi_shaei[i][2]-h)*(posi_shaei[i][2]-h));
                }
            }
        }

        //射影変換処理
        List<Point> srcPoints_t=new ArrayList();

        for (int i=0;i<4;i++){
            posi_shaei1[i][0]=i;
            posi_shaei1[i][1]=posi_shaei[irekae[i]][1];
            posi_shaei1[i][2]=posi_shaei[irekae[i]][2];
            srcPoints_t.add(new Point(posi_shaei1[i][1],posi_shaei1[i][2]));
        }

        Mat srcPoints= Converters.vector_Point_to_Mat(srcPoints_t, CvType.CV_32F);

        List<Point> dstpoints_t=new ArrayList();
        dstpoints_t.add(new Point(0,0)); //0 左上
        dstpoints_t.add(new Point(w,0)); //1 右上
        dstpoints_t.add(new Point(w,h)); //2 右下
        dstpoints_t.add(new Point(0,h)); //3 左下

        Mat dstpoints=Converters.vector_Point_to_Mat(dstpoints_t, CvType.CV_32F);

        Mat perspectiveMmat= Imgproc.getPerspectiveTransform(srcPoints,dstpoints);
        Imgproc.warpPerspective(
                mat,mat,perspectiveMmat,mat.size(),Imgproc.INTER_LINEAR);

        Utils.matToBitmap(mat, bmpmodori);
        return bmpmodori;
    }

    private Bitmap background(Bitmap bmphiki, int seekvalue){
        Mat mat=new Mat();
        Mat mask=new Mat();
        Utils.bitmapToMat(bmphiki, mat); // OpenCVの行列へ

        //2値化
        Imgproc.cvtColor(mat, mat, COLOR_RGB2GRAY);  // まずグレースケールへ(明るさだけの形式)
        Imgproc.threshold(mat, mask, seekvalue, 255.0, Imgproc.THRESH_BINARY);
        //Core.bitwise_not(mask,mask);

        Log.i("tag2","tag2");

        //背景白との足し算
        Core.add(mat,mask,mat);

        Bitmap bmpmodori=Bitmap.createBitmap(
                bmphiki.getWidth(), bmphiki.getHeight(), Bitmap.Config.ARGB_8888);

        Utils.matToBitmap(mat, bmpmodori);

        return  bmpmodori;
    }

    private Bitmap openingshori(Bitmap bmphiki){
        Mat mat=new Mat();
        Utils.bitmapToMat(bmphiki, mat); // OpenCVの行列へ

        Bitmap bmpmodori=Bitmap.createBitmap(
                bmphiki.getWidth(), bmphiki.getHeight(), Bitmap.Config.ARGB_8888);

        //opening
        Imgproc.morphologyEx(mat, mat, Imgproc.MORPH_DILATE,
                new Mat(), new Point(-1, -1), 2);

        //closing
        //Imgproc.morphologyEx(mat, mat, Imgproc.MORPH_CLOSE,
        //        new Mat(3, 3, CvType.CV_8UC1), new Point(-1, -1), 2);

        Utils.matToBitmap(mat, bmpmodori);
        return  bmpmodori;
    }

    class SeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
        @Override
        public void onProgressChanged(SeekBar seekBar, int progress,
                                      boolean fromUser) {
            //つまみをドラッグしたとき
            seekvalue=seekBar.getProgress();
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
            //つまみに触れたとき
        }

        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            //つまみを離したとき

        }
    }
}

まとめ

他に色々苦労したのがあったが、それは他のページによくまとまったのがあったので、ここでは割愛。opencvの関数を使うにあたり以下の書物が参考になった。近々、アプリ開発のための参考書をまとめたい。

以下の書物でopencv3とopencvがあり、改定前後になっているが、前verでは射影変換がなかったりする。自分は先に3を買い、中古で安くなった初期本を買い、自炊したときにこの違いに気づいた。前verはラクマにて出品中ですので、興味あるかたはご購入ください。先着1名様。

改定前にないもの
・画面上に文字記載
・透視投影(射影変換)
・トリミング
・モザイク処理
・コーナー検出
・顔検出
・オブジェクト除去
・ダメージ補修
・テンプレートマッチング
・特異点検出

Javaで始めるOpenCV3プログラミング 注)ver違いあり