Join the social network of Tech Nerds, increase skill rank, get work, manage projects...
 
  • Scan document like CS Scanner using OpenCV and NDK

    • 0
    • 0
    • 0
    • 0
    • 1
    • 0
    • 0
    • 0
    • 3.29k
    Comment on it

    In my last blog I have written about how to use Jhansi Karee's   Android Scanning library.

    This blog is about how you can use/modify library code according to your requirement. 

    I am assuming that you are already familiar with OpenCv and NDK. if not then study OpenCv and Android NDK first.

    Lets start to implement:-

    1. Create New Project in android Studio. 

    2. Create new Class PolygonView.

    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Paint;
    import android.graphics.PointF;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.FrameLayout;
    import android.widget.ImageView;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    public class PolygonView extends FrameLayout {
    
        protected Context context;
        private Paint paint;
        private ImageView pointer1;
        private ImageView pointer2;
        private ImageView pointer3;
        private ImageView pointer4;
        private ImageView midPointer13;
        private ImageView midPointer12;
        private ImageView midPointer34;
        private ImageView midPointer24;
        private PolygonView polygonView;
    
        public PolygonView(Context context) {
            super(context);
            this.context = context;
            init();
        }
    
        public PolygonView(Context context, AttributeSet attrs) {
            super(context, attrs);
            this.context = context;
            init();
        }
    
        public PolygonView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            this.context = context;
            init();
        }
    
        private void init() {
            polygonView = this;
            pointer1 = getImageView(0, 0);
            pointer2 = getImageView(getWidth(), 0);
            pointer3 = getImageView(0, getHeight());
            pointer4 = getImageView(getWidth(), getHeight());
            midPointer13 = getImageView(0, getHeight() / 2);
            midPointer13.setOnTouchListener(new MidPointTouchListenerImpl(pointer1, pointer3));
    
            midPointer12 = getImageView(0, getWidth() / 2);
            midPointer12.setOnTouchListener(new MidPointTouchListenerImpl(pointer1, pointer2));
    
            midPointer34 = getImageView(0, getHeight() / 2);
            midPointer34.setOnTouchListener(new MidPointTouchListenerImpl(pointer3, pointer4));
    
            midPointer24 = getImageView(0, getHeight() / 2);
            midPointer24.setOnTouchListener(new MidPointTouchListenerImpl(pointer2, pointer4));
    
            addView(pointer1);
            addView(pointer2);
            addView(midPointer13);
            addView(midPointer12);
            addView(midPointer34);
            addView(midPointer24);
            addView(pointer3);
            addView(pointer4);
            initPaint();
        }
    
        @Override
        protected void attachViewToParent(View child, int index, ViewGroup.LayoutParams params) {
            super.attachViewToParent(child, index, params);
        }
    
        private void initPaint() {
            paint = new Paint();
            paint.setColor(getResources().getColor(R.color.blue));
            paint.setStrokeWidth(2);
            paint.setAntiAlias(true);
        }
    
        public Map<Integer, PointF> getPoints() {
    
            List<PointF> points = new ArrayList<PointF>();
            points.add(new PointF(pointer1.getX(), pointer1.getY()));
            points.add(new PointF(pointer2.getX(), pointer2.getY()));
            points.add(new PointF(pointer3.getX(), pointer3.getY()));
            points.add(new PointF(pointer4.getX(), pointer4.getY()));
    
            return getOrderedPoints(points);
        }
    
        public Map<Integer, PointF> getOrderedPoints(List<PointF> points) {
    
            PointF centerPoint = new PointF();
            int size = points.size();
            for (PointF pointF : points) {
                centerPoint.x += pointF.x / size;
                centerPoint.y += pointF.y / size;
            }
            Map<Integer, PointF> orderedPoints = new HashMap<>();
            for (PointF pointF : points) {
                int index = -1;
                if (pointF.x < centerPoint.x && pointF.y < centerPoint.y) {
                    index = 0;
                } else if (pointF.x > centerPoint.x && pointF.y < centerPoint.y) {
                    index = 1;
                } else if (pointF.x < centerPoint.x && pointF.y > centerPoint.y) {
                    index = 2;
                } else if (pointF.x > centerPoint.x && pointF.y > centerPoint.y) {
                    index = 3;
                }
                orderedPoints.put(index, pointF);
            }
            return orderedPoints;
        }
    
        public void setPoints(Map<Integer, PointF> pointFMap) {
            if (pointFMap.size() == 4) {
                setPointsCoordinates(pointFMap);
            }
        }
    
    
        /**
         * set the circles to given point's xy.
         * @param pointFMap
         */
        private void setPointsCoordinates(Map<Integer, PointF> pointFMap) {
            pointer1.setX(pointFMap.get(0).x);
            pointer1.setY(pointFMap.get(0).y);
    
            pointer2.setX(pointFMap.get(1).x);
            pointer2.setY(pointFMap.get(1).y);
    
            pointer3.setX(pointFMap.get(2).x);
            pointer3.setY(pointFMap.get(2).y);
    
            pointer4.setX(pointFMap.get(3).x);
            pointer4.setY(pointFMap.get(3).y);
        }
    
        @Override
        protected void dispatchDraw(Canvas canvas) {
            super.dispatchDraw(canvas);
            canvas.drawLine(pointer1.getX() + (pointer1.getWidth() / 2), pointer1.getY() + (pointer1.getHeight() / 2), pointer3.getX() + (pointer3.getWidth() / 2), pointer3.getY() + (pointer3.getHeight() / 2), paint);
            canvas.drawLine(pointer1.getX() + (pointer1.getWidth() / 2), pointer1.getY() + (pointer1.getHeight() / 2), pointer2.getX() + (pointer2.getWidth() / 2), pointer2.getY() + (pointer2.getHeight() / 2), paint);
            canvas.drawLine(pointer2.getX() + (pointer2.getWidth() / 2), pointer2.getY() + (pointer2.getHeight() / 2), pointer4.getX() + (pointer4.getWidth() / 2), pointer4.getY() + (pointer4.getHeight() / 2), paint);
            canvas.drawLine(pointer3.getX() + (pointer3.getWidth() / 2), pointer3.getY() + (pointer3.getHeight() / 2), pointer4.getX() + (pointer4.getWidth() / 2), pointer4.getY() + (pointer4.getHeight() / 2), paint);
            midPointer13.setX(pointer3.getX() - ((pointer3.getX() - pointer1.getX()) / 2));
            midPointer13.setY(pointer3.getY() - ((pointer3.getY() - pointer1.getY()) / 2));
            midPointer24.setX(pointer4.getX() - ((pointer4.getX() - pointer2.getX()) / 2));
            midPointer24.setY(pointer4.getY() - ((pointer4.getY() - pointer2.getY()) / 2));
            midPointer34.setX(pointer4.getX() - ((pointer4.getX() - pointer3.getX()) / 2));
            midPointer34.setY(pointer4.getY() - ((pointer4.getY() - pointer3.getY()) / 2));
            midPointer12.setX(pointer2.getX() - ((pointer2.getX() - pointer1.getX()) / 2));
            midPointer12.setY(pointer2.getY() - ((pointer2.getY() - pointer1.getY()) / 2));
        }
    
        private ImageView getImageView(int x, int y) {
            ImageView imageView = new ImageView(context);
            LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            imageView.setLayoutParams(layoutParams);
            imageView.setImageResource(R.drawable.circle);
            imageView.setX(x);
            imageView.setY(y);
            imageView.setOnTouchListener(new TouchListenerImpl());
            return imageView;
        }
    
        private class MidPointTouchListenerImpl implements OnTouchListener {
    
            PointF DownPT = new PointF(); // Record Mouse Position When Pressed Down
            PointF StartPT = new PointF(); // Record Start Position of 'img'
    
            private ImageView mainPointer1;
            private ImageView mainPointer2;
    
            public MidPointTouchListenerImpl(ImageView mainPointer1, ImageView mainPointer2) {
                this.mainPointer1 = mainPointer1;
                this.mainPointer2 = mainPointer2;
            }
    
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int eid = event.getAction();
                switch (eid) {
                    case MotionEvent.ACTION_MOVE:
                        PointF mv = new PointF(event.getX() - DownPT.x, event.getY() - DownPT.y);
    
                        if (Math.abs(mainPointer1.getX() - mainPointer2.getX()) > Math.abs(mainPointer1.getY() - mainPointer2.getY())) {
                            if (((mainPointer2.getY() + mv.y + v.getHeight() < polygonView.getHeight()) && (mainPointer2.getY() + mv.y > 0))) {
                                v.setX((int) (StartPT.y + mv.y));
                                StartPT = new PointF(v.getX(), v.getY());
                                mainPointer2.setY((int) (mainPointer2.getY() + mv.y));
                            }
                            if (((mainPointer1.getY() + mv.y + v.getHeight() < polygonView.getHeight()) && (mainPointer1.getY() + mv.y > 0))) {
                                v.setX((int) (StartPT.y + mv.y));
                                StartPT = new PointF(v.getX(), v.getY());
                                mainPointer1.setY((int) (mainPointer1.getY() + mv.y));
                            }
                        } else {
                            if ((mainPointer2.getX() + mv.x + v.getWidth() < polygonView.getWidth()) && (mainPointer2.getX() + mv.x > 0)) {
                                v.setX((int) (StartPT.x + mv.x));
                                StartPT = new PointF(v.getX(), v.getY());
                                mainPointer2.setX((int) (mainPointer2.getX() + mv.x));
                            }
                            if ((mainPointer1.getX() + mv.x + v.getWidth() < polygonView.getWidth()) && (mainPointer1.getX() + mv.x > 0)) {
                                v.setX((int) (StartPT.x + mv.x));
                                StartPT = new PointF(v.getX(), v.getY());
                                mainPointer1.setX((int) (mainPointer1.getX() + mv.x));
                            }
                        }
    
                        break;
                    case MotionEvent.ACTION_DOWN:
                        DownPT.x = event.getX();
                        DownPT.y = event.getY();
                        StartPT = new PointF(v.getX(), v.getY());
                        break;
                    case MotionEvent.ACTION_UP:
                        int color = 0;
                        if (isValidShape(getPoints())) {
                            color = getResources().getColor(R.color.blue);
                        } else {
                            color = getResources().getColor(R.color.orange);
                        }
                        paint.setColor(color);
                        break;
                    default:
                        break;
                }
                polygonView.invalidate();
                return true;
            }
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            return super.onTouchEvent(event);
        }
    
        public boolean isValidShape(Map<Integer, PointF> pointFMap) {
            return pointFMap.size() == 4;
        }
    
        private class  TouchListenerImpl implements OnTouchListener {
    
            PointF DownPT = new PointF(); // Record Mouse Position When Pressed Down
            PointF StartPT = new PointF(); // Record Start Position of 'img'
    
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int eid = event.getAction();
                switch (eid) {
                    case MotionEvent.ACTION_MOVE:
                        PointF mv = new PointF(event.getX() - DownPT.x, event.getY() - DownPT.y);
                        if (((StartPT.x + mv.x + v.getWidth()) < polygonView.getWidth() && (StartPT.y + mv.y + v.getHeight() < polygonView.getHeight())) && ((StartPT.x + mv.x) > 0 && StartPT.y + mv.y > 0)) {
                            v.setX((int) (StartPT.x + mv.x));
                            v.setY((int) (StartPT.y + mv.y));
                            StartPT = new PointF(v.getX(), v.getY());
                        }
                        break;
                    case MotionEvent.ACTION_DOWN:
                        DownPT.x = event.getX();
                        DownPT.y = event.getY();
                        StartPT = new PointF(v.getX(), v.getY());
                        break;
                    case MotionEvent.ACTION_UP:
                        int color = 0;
                        if (isValidShape(getPoints())) {
                            color = getResources().getColor(R.color.blue);
                        } else {
                            color = getResources().getColor(R.color.orange);
                        }
                        paint.setColor(color);
                        break;
                    default:
                        break;
                }
                polygonView.invalidate();
                return true;
            }
    
        }

    3. Create xml drawable circle.xml.

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="oval">
    
        <solid android:color="@color/polygonViewCircleBackground" />
    
        <size
            android:width="@dimen/polygonViewCircleWidth"
            android:height="@dimen/polygonViewCircleWidth" />
    
        <stroke
            android:width="@dimen/polygonViewStrokeWidth"
            android:color="@color/polygonViewCircleStrokeColor"></stroke>
    </shape>

    4. Create libs and jni folder under. app->src -> main.

    5. Copy files from Jhansi Karee's  library-> src->main-> jni and paste to your Jni folder.

    6. Open Android.mk file from jni folder and change the location of OpenCV.mk and scan.cpp file location with your OpenCV.mk and scan.cpp file location.

    7.Open scan.cpp file and replace Java_com_scanlibrary with Java_(Your package name replace '.' with '_').

    8. Open build.gradle(Module app) and add below line of code in defaultConfig.

          ndk
                   {
                     moduleName "Scanner"             
      }

    9. and below code after defaultConfig tag.

     sourceSets.main
                {
                    jni.srcDirs = []
                    jniLibs.srcDir 'src/main/libs'
                }

    10. Create your activity layout like below:-

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        tools:context="com.ammy.camscannerdemo.ScanActivity">
    
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="@dimen/activity_horizontal_margin"
            android:layout_above="@+id/button"
            android:layout_alignParentTop="true">
    
            <FrameLayout
                android:id="@+id/fl_image"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_gravity="center"
                android:layout_margin="10dp">
    
                <ImageView
                    android:id="@+id/imageView"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:adjustViewBounds="true" />
    
            </FrameLayout>
    
            <packagename.PolygonView
                android:id="@+id/polygonView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_gravity="center" />
        </FrameLayout>
    
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true"
            android:text="Scan Image" />
    </RelativeLayout>
    

    11. In your activity onCreate() initialize Views.

     mImageFrame = (FrameLayout) findViewById(R.id.fl_image);
            imageView = (ImageView) findViewById(R.id.imageView);
            mPolygonView = (PolygonView) findViewById(R.id.polygonView);
    

     

    12. Add this code too in onCreate().

    mImageFrame.post(new Runnable() {
                        @Override
                        public void run() {
                            if (bitmap != null) {
                                setBitmap(bitmap);
                            }
                        }
                    });
                } catch (IOException e) {
                    e.printStackTrace();
                }

    * bitmap is object of your bitmap image.

     

    13. Copy below methods to your activity.

     private void setBitmap(Bitmap original) {
            Bitmap scaledBitmap = scaledBitmap(original, mImageFrame.getWidth(), mImageFrame.getHeight());
            imageView.setImageBitmap(scaledBitmap);
            Bitmap tempBitmap = ((BitmapDrawable) imageView.getDrawable()).getBitmap();
            Map<Integer, PointF> pointFs = getEdgePoints(tempBitmap);
            mPolygonView.setPoints(pointFs);
            mPolygonView.setVisibility(View.VISIBLE);
            int padding = (int) getResources().getDimension(R.dimen.scanPadding);
            FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(tempBitmap.getWidth() + 2 * padding, tempBitmap.getHeight() + 2 * padding);
            layoutParams.gravity = Gravity.CENTER;
            mPolygonView.setLayoutParams(layoutParams);
        }
    
    
        private Bitmap scaledBitmap(Bitmap bitmap, int width, int height) {
            Matrix m = new Matrix();
            m.setRectToRect(new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()), new RectF(0, 0, width, height), Matrix.ScaleToFit.CENTER);
            return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m, true);
        }
    
    
        private Map<Integer, PointF> getEdgePoints(Bitmap tempBitmap) {
            List<PointF> pointFs = getContourEdgePoints(tempBitmap);
            Map<Integer, PointF> orderedPoints = orderedValidEdgePoints(tempBitmap, pointFs);
            return orderedPoints;
        }
    
        private List<PointF> getContourEdgePoints(Bitmap tempBitmap) {
            float[] points = getPoints(tempBitmap);
            float x1 = points[0];
            float x2 = points[1];
            float x3 = points[2];
            float x4 = points[3];
    
            float y1 = points[4];
            float y2 = points[5];
            float y3 = points[6];
            float y4 = points[7];
    
            List<PointF> pointFs = new ArrayList<>();
            pointFs.add(new PointF(x1, y1));
            pointFs.add(new PointF(x2, y2));
            pointFs.add(new PointF(x3, y3));
            pointFs.add(new PointF(x4, y4));
            return pointFs;
        }
    
        static {
            System.loadLibrary("opencv_java");
            System.loadLibrary("Scanner");
        }
    
    
        public native Bitmap getScannedBitmap(Bitmap bitmap, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4);
    
        public native Bitmap getGrayBitmap(Bitmap bitmap);
    
        public native Bitmap getMagicColorBitmap(Bitmap bitmap);
    
        public native Bitmap getBWBitmap(Bitmap bitmap);
    
        public native float[] getPoints(Bitmap bitmap);
    
    
        private Map<Integer, PointF> orderedValidEdgePoints(Bitmap tempBitmap, List<PointF> pointFs) {
            Map<Integer, PointF> orderedPoints = mPolygonView.getOrderedPoints(pointFs);
            if (!mPolygonView.isValidShape(orderedPoints)) {
                orderedPoints = getOutlinePoints(tempBitmap);
            }
            return orderedPoints;
        }
    
        private Map<Integer, PointF> getOutlinePoints(Bitmap tempBitmap) {
            Map<Integer, PointF> outlinePoints = new HashMap<>();
            outlinePoints.put(0, new PointF(0, 0));
            outlinePoints.put(1, new PointF(tempBitmap.getWidth(), 0));
            outlinePoints.put(2, new PointF(0, tempBitmap.getHeight()));
            outlinePoints.put(3, new PointF(tempBitmap.getWidth(), tempBitmap.getHeight()));
            return outlinePoints;
        }
    
        private void showErrorDialog() {
            Toast.makeText(this, "Invalid shape", Toast.LENGTH_LONG).show();
        }
    
        private boolean isScanPointsValid(Map<Integer, PointF> points) {
            return points.size() == 4;
        }
    
    
        private class ScanAsyncTask extends AsyncTask<Void, Void, Bitmap> {
    
            private Map<Integer, PointF> points;
    
            public ScanAsyncTask(Map<Integer, PointF> points) {
                this.points = points;
            }
    
            @Override
            protected void onPreExecute() {
                super.onPreExecute();
    
            }
    
            @Override
            protected Bitmap doInBackground(Void... params) {
                return getScannedBitmap(bitmap, points);
            }
    
            @Override
            protected void onPostExecute(Bitmap bitmap) {
                super.onPostExecute(bitmap);
    
                Uri uri = Utils.getUri(ScanActivity.this, bitmap);
                bitmap.recycle();
                Intent intent = new Intent(ScanActivity.this, ResultActivity.class);
                intent.putExtra(Constants.BITMAP_PATH, uri);
                startActivity(intent);
    //            scanner.onScanFinish(uri);
            }
        }
    
        private Bitmap getScannedBitmap(Bitmap original, Map<Integer, PointF> points) {
            int width = original.getWidth();
            int height = original.getHeight();
            float xRatio = (float) original.getWidth() / imageView.getWidth();
            float yRatio = (float) original.getHeight() / imageView.getHeight();
    
            float x1 = (points.get(0).x) * xRatio;
            float x2 = (points.get(1).x) * xRatio;
            float x3 = (points.get(2).x) * xRatio;
            float x4 = (points.get(3).x) * xRatio;
            float y1 = (points.get(0).y) * yRatio;
            float y2 = (points.get(1).y) * yRatio;
            float y3 = (points.get(2).y) * yRatio;
            float y4 = (points.get(3).y) * yRatio;
            Log.d("", "POints(" + x1 + "," + y1 + ")(" + x2 + "," + y2 + ")(" + x3 + "," + y3 + ")(" + x4 + "," + y4 + ")");
            Bitmap _bitmap = getScannedBitmap(original, x1, y1, x2, y2, x3, y3, x4, y4);
            return _bitmap;
        }

    14. Build project. Building project creates "libopencv_java.so" and "libScanner.so" files in libs folder.

    if there is problem creating so files then follow this solution.

    15.  From Scan button write below code.

      Map<Integer, PointF> points = mPolygonView.getPoints();
                    if (isScanPointsValid(points)) {
                        new ScanAsyncTask(points).execute();
                    } else {
                        showErrorDialog();
                    }

     

    Now Call this activity where you want to Scan documents. or modify according to your need.

     

    Happy Coding :D

 1 Comment(s)

  • Hi!

    I only be able to use your solution, when I add to scan.cpp the following codes:

    extern "C" {
        JNIEXPORT jobject JNICALL Java_[MY PACKAGE]_ScanActivity_getScannedBitmap([METHOD PARAMS]);
        JNIEXPORT jobject JNICALL Java_[MY PACKAGE]_ScanActivity_getMagicColorBitmap([METHOD PARAMS]);
        JNIEXPORT jobject JNICALL Java_[MY PACKAGE]_ScanActivity_getBWBitmap([METHOD PARAMS]);
        JNIEXPORT jobject JNICALL Java_[MY PACKAGE]_ScanActivity_getGrayBitmap([METHOD PARAMS]);
        JNIEXPORT jfloatArray JNICALL Java_[MY PACKAGE]_ScanActivity_getPoints([METHOD PARAMS]);
    };

    I hope this helps anothers with the same needs.
Sign In
                           OR                           
                           OR                           
Register

Sign up using

                           OR                           
Forgot Password
Fill out the form below and instructions to reset your password will be emailed to you:
Reset Password
Fill out the form below and reset your password: