251 |
252 |
--------------------------------------------------------------------------------
/app/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | }
4 |
5 | android {
6 | compileSdk 31
7 |
8 | defaultConfig {
9 | applicationId "com.dynamsoft.documentscanner"
10 | minSdk 21
11 | targetSdk 31
12 | versionCode 1
13 | versionName "1.0"
14 |
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 | }
17 |
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | compileOptions {
25 | sourceCompatibility JavaVersion.VERSION_1_8
26 | targetCompatibility JavaVersion.VERSION_1_8
27 | }
28 | }
29 |
30 | dependencies {
31 |
32 | implementation 'com.jsibbold:zoomage:1.3.1'
33 |
34 | //Dynamsoft products
35 | implementation 'com.dynamsoft:dynamsoftcapturevisionrouter:2.0.21'
36 | implementation 'com.dynamsoft:dynamsoftdocumentnormalizer:2.0.20'
37 | implementation 'com.dynamsoft:dynamsoftcore:3.0.20'
38 | implementation 'com.dynamsoft:dynamsoftlicense:3.0.30'
39 | implementation 'com.dynamsoft:dynamsoftimageprocessing:2.0.21'
40 |
41 |
42 | //CameraX
43 | def camerax_version = '1.1.0-rc01'
44 | implementation "androidx.camera:camera-core:$camerax_version"
45 | implementation "androidx.camera:camera-camera2:$camerax_version"
46 | implementation "androidx.camera:camera-lifecycle:$camerax_version"
47 | implementation "androidx.camera:camera-view:$camerax_version"
48 |
49 | implementation 'androidx.appcompat:appcompat:1.4.1'
50 | implementation 'com.google.android.material:material:1.6.0'
51 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
52 | testImplementation 'junit:junit:4.13.2'
53 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
54 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
55 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/dynamsoft/documentscanner/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.dynamsoft.documentscanner;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.test.platform.app.InstrumentationRegistry;
6 | import androidx.test.ext.junit.runners.AndroidJUnit4;
7 |
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 | import static org.junit.Assert.*;
12 |
13 | /**
14 | * Instrumented test, which will execute on an Android device.
15 | *
16 | * @see Testing documentation
17 | */
18 | @RunWith(AndroidJUnit4.class)
19 | public class ExampleInstrumentedTest {
20 | @Test
21 | public void useAppContext() {
22 | // Context of the app under test.
23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
24 | assertEquals("com.dynamsoft.documentscanner", appContext.getPackageName());
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
10 |
11 |
18 |
21 |
24 |
27 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dynamsoft/documentscanner/BitmapUtils.java:
--------------------------------------------------------------------------------
1 | package com.dynamsoft.documentscanner;
2 |
3 | /*
4 | * Copyright 2020 Google LLC. All rights reserved.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 |
19 |
20 | import android.annotation.TargetApi;
21 | import android.graphics.Bitmap;
22 | import android.graphics.BitmapFactory;
23 | import android.graphics.ImageFormat;
24 | import android.graphics.Matrix;
25 | import android.graphics.Rect;
26 | import android.graphics.YuvImage;
27 | import android.media.Image;
28 | import android.media.Image.Plane;
29 | import android.os.Build.VERSION_CODES;
30 | import androidx.annotation.Nullable;
31 | import android.util.Log;
32 | import androidx.annotation.RequiresApi;
33 | import androidx.camera.core.ExperimentalGetImage;
34 | import androidx.camera.core.ImageProxy;
35 | import java.io.ByteArrayOutputStream;
36 | import java.nio.ByteBuffer;
37 |
38 | /** Utils functions for bitmap conversions. */
39 | public class BitmapUtils {
40 | private static final String TAG = "BitmapUtils";
41 |
42 | /** Converts NV21 format byte buffer to bitmap. */
43 | @Nullable
44 | public static Bitmap getBitmap(ByteBuffer data, FrameMetadata metadata) {
45 | data.rewind();
46 | byte[] imageInBuffer = new byte[data.limit()];
47 | data.get(imageInBuffer, 0, imageInBuffer.length);
48 | try {
49 | YuvImage image =
50 | new YuvImage(
51 | imageInBuffer, ImageFormat.NV21, metadata.getWidth(), metadata.getHeight(), null);
52 | ByteArrayOutputStream stream = new ByteArrayOutputStream();
53 | image.compressToJpeg(new Rect(0, 0, metadata.getWidth(), metadata.getHeight()), 80, stream);
54 |
55 | Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
56 |
57 | stream.close();
58 | return rotateBitmap(bmp, metadata.getRotation(), false, false);
59 | } catch (Exception e) {
60 | Log.e("VisionProcessorBase", "Error: " + e.getMessage());
61 | }
62 | return null;
63 | }
64 |
65 | /** Converts a YUV_420_888 image from CameraX API to a bitmap. */
66 | @RequiresApi(VERSION_CODES.LOLLIPOP)
67 | @Nullable
68 | @ExperimentalGetImage
69 | public static Bitmap getBitmap(ImageProxy image) {
70 | FrameMetadata frameMetadata =
71 | new FrameMetadata.Builder()
72 | .setWidth(image.getWidth())
73 | .setHeight(image.getHeight())
74 | .setRotation(image.getImageInfo().getRotationDegrees())
75 | .build();
76 |
77 | ByteBuffer nv21Buffer =
78 | yuv420ThreePlanesToNV21(image.getImage().getPlanes(), image.getWidth(), image.getHeight());
79 | return getBitmap(nv21Buffer, frameMetadata);
80 | }
81 |
82 | /** Rotates a bitmap if it is converted from a bytebuffer. */
83 | private static Bitmap rotateBitmap(
84 | Bitmap bitmap, int rotationDegrees, boolean flipX, boolean flipY) {
85 | Matrix matrix = new Matrix();
86 |
87 | // Rotate the image back to straight.
88 | matrix.postRotate(rotationDegrees);
89 |
90 | // Mirror the image along the X or Y axis.
91 | matrix.postScale(flipX ? -1.0f : 1.0f, flipY ? -1.0f : 1.0f);
92 | Bitmap rotatedBitmap =
93 | Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
94 |
95 | // Recycle the old bitmap if it has changed.
96 | if (rotatedBitmap != bitmap) {
97 | bitmap.recycle();
98 | }
99 | return rotatedBitmap;
100 | }
101 |
102 | /**
103 | * Converts YUV_420_888 to NV21 bytebuffer.
104 | *
105 | *
The NV21 format consists of a single byte array containing the Y, U and V values. For an
106 | * image of size S, the first S positions of the array contain all the Y values. The remaining
107 | * positions contain interleaved V and U values. U and V are subsampled by a factor of 2 in both
108 | * dimensions, so there are S/4 U values and S/4 V values. In summary, the NV21 array will contain
109 | * S Y values followed by S/4 VU values: YYYYYYYYYYYYYY(...)YVUVUVUVU(...)VU
110 | *
111 | *
YUV_420_888 is a generic format that can describe any YUV image where U and V are subsampled
112 | * by a factor of 2 in both dimensions. {@link Image#getPlanes} returns an array with the Y, U and
113 | * V planes. The Y plane is guaranteed not to be interleaved, so we can just copy its values into
114 | * the first part of the NV21 array. The U and V planes may already have the representation in the
115 | * NV21 format. This happens if the planes share the same buffer, the V buffer is one position
116 | * before the U buffer and the planes have a pixelStride of 2. If this is case, we can just copy
117 | * them to the NV21 array.
118 | */
119 | @RequiresApi(VERSION_CODES.KITKAT)
120 | private static ByteBuffer yuv420ThreePlanesToNV21(
121 | Plane[] yuv420888planes, int width, int height) {
122 | int imageSize = width * height;
123 | byte[] out = new byte[imageSize + 2 * (imageSize / 4)];
124 |
125 | if (areUVPlanesNV21(yuv420888planes, width, height)) {
126 | // Copy the Y values.
127 | yuv420888planes[0].getBuffer().get(out, 0, imageSize);
128 |
129 | ByteBuffer uBuffer = yuv420888planes[1].getBuffer();
130 | ByteBuffer vBuffer = yuv420888planes[2].getBuffer();
131 | // Get the first V value from the V buffer, since the U buffer does not contain it.
132 | vBuffer.get(out, imageSize, 1);
133 | // Copy the first U value and the remaining VU values from the U buffer.
134 | uBuffer.get(out, imageSize + 1, 2 * imageSize / 4 - 1);
135 | } else {
136 | // Fallback to copying the UV values one by one, which is slower but also works.
137 | // Unpack Y.
138 | unpackPlane(yuv420888planes[0], width, height, out, 0, 1);
139 | // Unpack U.
140 | unpackPlane(yuv420888planes[1], width, height, out, imageSize + 1, 2);
141 | // Unpack V.
142 | unpackPlane(yuv420888planes[2], width, height, out, imageSize, 2);
143 | }
144 |
145 | return ByteBuffer.wrap(out);
146 | }
147 |
148 | /** Checks if the UV plane buffers of a YUV_420_888 image are in the NV21 format. */
149 | @RequiresApi(VERSION_CODES.KITKAT)
150 | private static boolean areUVPlanesNV21(Plane[] planes, int width, int height) {
151 | int imageSize = width * height;
152 |
153 | ByteBuffer uBuffer = planes[1].getBuffer();
154 | ByteBuffer vBuffer = planes[2].getBuffer();
155 |
156 | // Backup buffer properties.
157 | int vBufferPosition = vBuffer.position();
158 | int uBufferLimit = uBuffer.limit();
159 |
160 | // Advance the V buffer by 1 byte, since the U buffer will not contain the first V value.
161 | vBuffer.position(vBufferPosition + 1);
162 | // Chop off the last byte of the U buffer, since the V buffer will not contain the last U value.
163 | uBuffer.limit(uBufferLimit - 1);
164 |
165 | // Check that the buffers are equal and have the expected number of elements.
166 | boolean areNV21 =
167 | (vBuffer.remaining() == (2 * imageSize / 4 - 2)) && (vBuffer.compareTo(uBuffer) == 0);
168 |
169 | // Restore buffers to their initial state.
170 | vBuffer.position(vBufferPosition);
171 | uBuffer.limit(uBufferLimit);
172 |
173 | return areNV21;
174 | }
175 |
176 | /**
177 | * Unpack an image plane into a byte array.
178 | *
179 | *
The input plane data will be copied in 'out', starting at 'offset' and every pixel will be
180 | * spaced by 'pixelStride'. Note that there is no row padding on the output.
181 | */
182 | @TargetApi(VERSION_CODES.KITKAT)
183 | private static void unpackPlane(
184 | Plane plane, int width, int height, byte[] out, int offset, int pixelStride) {
185 | ByteBuffer buffer = plane.getBuffer();
186 | buffer.rewind();
187 |
188 | // Compute the size of the current plane.
189 | // We assume that it has the aspect ratio as the original image.
190 | int numRow = (buffer.limit() + plane.getRowStride() - 1) / plane.getRowStride();
191 | if (numRow == 0) {
192 | return;
193 | }
194 | int scaleFactor = height / numRow;
195 | int numCol = width / scaleFactor;
196 |
197 | // Extract the data in the output buffer.
198 | int outputPos = offset;
199 | int rowStart = 0;
200 | for (int row = 0; row < numRow; row++) {
201 | int inputPos = rowStart;
202 | for (int col = 0; col < numCol; col++) {
203 | out[outputPos] = buffer.get(inputPos);
204 | outputPos += pixelStride;
205 | inputPos += plane.getPixelStride();
206 | }
207 | rowStart += plane.getRowStride();
208 | }
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dynamsoft/documentscanner/CameraActivity.java:
--------------------------------------------------------------------------------
1 | package com.dynamsoft.documentscanner;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.annotation.RequiresApi;
5 | import androidx.appcompat.app.AppCompatActivity;
6 | import androidx.camera.core.AspectRatio;
7 | import androidx.camera.core.Camera;
8 | import androidx.camera.core.CameraSelector;
9 | import androidx.camera.core.ImageAnalysis;
10 | import androidx.camera.core.ImageCapture;
11 | import androidx.camera.core.ImageCaptureException;
12 | import androidx.camera.core.ImageProxy;
13 | import androidx.camera.core.Preview;
14 | import androidx.camera.core.UseCaseGroup;
15 | import androidx.camera.lifecycle.ProcessCameraProvider;
16 | import androidx.camera.view.PreviewView;
17 | import androidx.core.content.ContextCompat;
18 | import androidx.lifecycle.LifecycleOwner;
19 |
20 | import android.annotation.SuppressLint;
21 | import android.content.Intent;
22 | import android.content.res.Configuration;
23 | import android.graphics.Bitmap;
24 | import android.graphics.Point;
25 | import android.os.Build;
26 | import android.os.Bundle;
27 | import android.util.Log;
28 | import android.util.Size;
29 | import android.view.Display;
30 | import android.view.Window;
31 | import android.view.WindowManager;
32 |
33 | import com.dynamsoft.core.basic_structures.CapturedResult;
34 | import com.dynamsoft.core.basic_structures.CapturedResultItem;
35 | import com.dynamsoft.cvr.CaptureVisionRouter;
36 | import com.dynamsoft.cvr.CaptureVisionRouterException;
37 | import com.dynamsoft.ddn.DetectedQuadResultItem;
38 | import com.dynamsoft.ddn.DocumentNormalizerException;
39 | import com.google.common.util.concurrent.ListenableFuture;
40 |
41 | import java.io.File;
42 | import java.nio.ByteBuffer;
43 | import java.util.ArrayList;
44 | import java.util.concurrent.ExecutionException;
45 | import java.util.concurrent.ExecutorService;
46 | import java.util.concurrent.Executors;
47 |
48 | public class CameraActivity extends AppCompatActivity {
49 | private PreviewView previewView;
50 | private OverlayView overlayView;
51 | private ListenableFuture cameraProviderFuture;
52 | private ExecutorService exec;
53 | private Camera camera;
54 | private CaptureVisionRouter cvr;
55 | private ImageCapture imageCapture;
56 | private Boolean taken = false;
57 | private ArrayList previousResults = new ArrayList();
58 | @Override
59 | protected void onCreate(Bundle savedInstanceState) {
60 | super.onCreate(savedInstanceState);
61 | setContentView(R.layout.activity_camera);
62 | getSupportActionBar().hide();
63 | getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
64 | WindowManager.LayoutParams.FLAG_FULLSCREEN);
65 |
66 | previewView = findViewById(R.id.previewView);
67 | overlayView = findViewById(R.id.overlayView);
68 | exec = Executors.newSingleThreadExecutor();
69 | cameraProviderFuture = ProcessCameraProvider.getInstance(this);
70 | cameraProviderFuture.addListener(new Runnable() {
71 | @Override
72 | public void run() {
73 | try {
74 | ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
75 | bindUseCases(cameraProvider);
76 | } catch (ExecutionException | InterruptedException e) {
77 | e.printStackTrace();
78 | }
79 | }
80 | }, ContextCompat.getMainExecutor(this));
81 | initCVR();
82 | }
83 |
84 | @SuppressLint("UnsafeExperimentalUsageError")
85 | private void bindUseCases(@NonNull ProcessCameraProvider cameraProvider) {
86 |
87 | int orientation = getApplicationContext().getResources().getConfiguration().orientation;
88 | Size resolution;
89 | if (orientation == Configuration.ORIENTATION_PORTRAIT) {
90 | resolution = new Size(720, 1280);
91 | }else{
92 | resolution = new Size(1280, 720);
93 | }
94 |
95 | Preview.Builder previewBuilder = new Preview.Builder();
96 | previewBuilder.setTargetAspectRatio(AspectRatio.RATIO_16_9);
97 | Preview preview = previewBuilder.build();
98 |
99 | ImageAnalysis.Builder imageAnalysisBuilder = new ImageAnalysis.Builder();
100 |
101 | imageAnalysisBuilder.setTargetResolution(resolution)
102 | .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST);
103 |
104 | ImageAnalysis imageAnalysis = imageAnalysisBuilder.build();
105 |
106 | imageAnalysis.setAnalyzer(exec, new ImageAnalysis.Analyzer() {
107 | @Override
108 | public void analyze(@NonNull ImageProxy image) {
109 | @SuppressLint("UnsafeOptInUsageError")
110 | Bitmap bitmap = BitmapUtils.getBitmap(image);
111 | overlayView.setSrcImageWidth(bitmap.getWidth());
112 | overlayView.setSrcImageHeight(bitmap.getHeight());
113 | try {
114 | CapturedResult capturedResult = cvr.capture(bitmap,"DetectDocumentBoundaries_Default");
115 | CapturedResultItem[] results = capturedResult.getItems();
116 | if (results != null) {
117 | if (results.length>0) {
118 | DetectedQuadResultItem result = (DetectedQuadResultItem) results[0];
119 | Log.d("DDN","confidence: "+result.getConfidenceAsDocumentBoundary());
120 | overlayView.setPoints(result.getLocation().points);
121 | if (result.getConfidenceAsDocumentBoundary() > 50) {
122 | if (taken == false) {
123 | if (previousResults.size() == 3) {
124 | if (steady() == true) {
125 | Log.d("DDN","take photo");
126 | takePhoto(result, bitmap.getWidth(), bitmap.getHeight());
127 | taken = true;
128 | }else{
129 | previousResults.remove(0);
130 | previousResults.add(result);
131 | }
132 | }else{
133 | previousResults.add(result);
134 | }
135 | }
136 | }
137 | }
138 | }
139 | } catch (Exception e) {
140 | e.printStackTrace();
141 | }
142 | image.close();
143 | }
144 | });
145 |
146 | CameraSelector cameraSelector = new CameraSelector.Builder()
147 | .requireLensFacing(CameraSelector.LENS_FACING_BACK).build();
148 | preview.setSurfaceProvider(previewView.getSurfaceProvider());
149 | imageCapture =
150 | new ImageCapture.Builder()
151 | .setTargetAspectRatio(AspectRatio.RATIO_16_9)
152 | .build();
153 | UseCaseGroup useCaseGroup = new UseCaseGroup.Builder()
154 | .addUseCase(preview)
155 | .addUseCase(imageAnalysis)
156 | .addUseCase(imageCapture)
157 | .build();
158 | camera = cameraProvider.bindToLifecycle((LifecycleOwner) this, cameraSelector, useCaseGroup);
159 | }
160 |
161 | private Boolean steady(){
162 | float iou1 = Utils.intersectionOverUnion(previousResults.get(0).getLocation().points,previousResults.get(1).getLocation().points);
163 | float iou2 = Utils.intersectionOverUnion(previousResults.get(1).getLocation().points,previousResults.get(2).getLocation().points);
164 | float iou3 = Utils.intersectionOverUnion(previousResults.get(0).getLocation().points,previousResults.get(2).getLocation().points);
165 | Log.d("DDN","iou1: "+iou1);
166 | Log.d("DDN","iou2: "+iou2);
167 | Log.d("DDN","iou3: "+iou3);
168 | if (iou1>0.9 && iou2>0.9 && iou3>0.9) {
169 | return true;
170 | }else{
171 | return false;
172 | }
173 | }
174 |
175 | private void takePhoto(DetectedQuadResultItem result,int bitmapWidth,int bitmapHeight){
176 | File dir = getExternalCacheDir();
177 | File file = new File(dir, "photo.jpg");
178 | ImageCapture.OutputFileOptions outputFileOptions =
179 | new ImageCapture.OutputFileOptions.Builder(file).build();
180 | imageCapture.takePicture(outputFileOptions, exec,
181 | new ImageCapture.OnImageSavedCallback() {
182 | @Override
183 | public void onImageSaved(ImageCapture.OutputFileResults outputFileResults) {
184 | Log.d("DDN","saved");
185 | Log.d("DDN",outputFileResults.getSavedUri().toString());
186 | Intent intent = new Intent(CameraActivity.this, CroppingActivity.class);
187 | intent.putExtra("imageUri",outputFileResults.getSavedUri().toString());
188 | intent.putExtra("points",result.getLocation().points);
189 | intent.putExtra("bitmapWidth",bitmapWidth);
190 | intent.putExtra("bitmapHeight",bitmapHeight);
191 | startActivity(intent);
192 | }
193 |
194 | @Override
195 | public void onError(@NonNull ImageCaptureException exception) {
196 |
197 | }
198 |
199 | }
200 | );
201 | }
202 |
203 | private void initCVR(){
204 | cvr = new CaptureVisionRouter(CameraActivity.this);
205 | try {
206 | cvr.initSettings("{\"CaptureVisionTemplates\": [{\"Name\": \"Default\"},{\"Name\": \"DetectDocumentBoundaries_Default\",\"ImageROIProcessingNameArray\": [\"roi-detect-document-boundaries\"]},{\"Name\": \"DetectAndNormalizeDocument_Default\",\"ImageROIProcessingNameArray\": [\"roi-detect-and-normalize-document\"]},{\"Name\": \"NormalizeDocument_Binary\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-binary\"]}, {\"Name\": \"NormalizeDocument_Gray\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-gray\"]}, {\"Name\": \"NormalizeDocument_Color\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-color\"]}],\"TargetROIDefOptions\": [{\"Name\": \"roi-detect-document-boundaries\",\"TaskSettingNameArray\": [\"task-detect-document-boundaries\"]},{\"Name\": \"roi-detect-and-normalize-document\",\"TaskSettingNameArray\": [\"task-detect-and-normalize-document\"]},{\"Name\": \"roi-normalize-document-binary\",\"TaskSettingNameArray\": [\"task-normalize-document-binary\"]}, {\"Name\": \"roi-normalize-document-gray\",\"TaskSettingNameArray\": [\"task-normalize-document-gray\"]}, {\"Name\": \"roi-normalize-document-color\",\"TaskSettingNameArray\": [\"task-normalize-document-color\"]}],\"DocumentNormalizerTaskSettingOptions\": [{\"Name\": \"task-detect-and-normalize-document\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-detect-and-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-detect-and-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-detect-and-normalize\"}]},{\"Name\": \"task-detect-document-boundaries\",\"TerminateSetting\": {\"Section\": \"ST_DOCUMENT_DETECTION\"},\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-detect\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-detect\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-detect\"}]},{\"Name\": \"task-normalize-document-binary\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\", \"ColourMode\": \"ICM_BINARY\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]}, {\"Name\": \"task-normalize-document-gray\", \"ColourMode\": \"ICM_GRAYSCALE\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]}, {\"Name\": \"task-normalize-document-color\", \"ColourMode\": \"ICM_COLOUR\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]}],\"ImageParameterOptions\": [{\"Name\": \"ip-detect-and-normalize\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7}},{\"Name\": \"ip-detect\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0,\"ThresholdCompensation\" : 7}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7},\"ScaleDownThreshold\" : 512},{\"Name\": \"ip-normalize\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7}}]}");
207 | } catch (CaptureVisionRouterException e) {
208 | e.printStackTrace();
209 | }
210 | }
211 |
212 | @Override
213 | protected void onResume(){
214 | previousResults.clear();
215 | taken = false;
216 | super.onResume();
217 | }
218 |
219 | @Override
220 | protected void onPause(){
221 | super.onPause();
222 | }
223 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dynamsoft/documentscanner/CroppingActivity.java:
--------------------------------------------------------------------------------
1 | package com.dynamsoft.documentscanner;
2 |
3 | import androidx.appcompat.app.AppCompatActivity;
4 |
5 | import android.content.Intent;
6 | import android.content.res.Resources;
7 | import android.graphics.Bitmap;
8 | import android.graphics.BitmapFactory;
9 | import android.graphics.Matrix;
10 | import android.graphics.Point;
11 | import android.media.ExifInterface;
12 | import android.net.Uri;
13 | import android.os.Bundle;
14 | import android.os.Parcelable;
15 | import android.provider.MediaStore;
16 | import android.util.DisplayMetrics;
17 | import android.util.Log;
18 | import android.util.TypedValue;
19 | import android.view.MotionEvent;
20 | import android.view.View;
21 | import android.view.ViewTreeObserver;
22 | import android.view.WindowManager;
23 | import android.widget.Button;
24 | import android.widget.ImageView;
25 |
26 | import java.io.FileOutputStream;
27 | import java.io.IOException;
28 | import java.io.InputStream;
29 |
30 | public class CroppingActivity extends AppCompatActivity {
31 | private Button okayButton;
32 | private Button reTakeButton;
33 | private Bitmap background;
34 | private ImageView imageView;
35 | private OverlayView overlayView;
36 | private ImageView corner1;
37 | private ImageView corner2;
38 | private ImageView corner3;
39 | private ImageView corner4;
40 | private ImageView[] corners = new ImageView[4];
41 | private int mLastX;
42 | private int mLastY;
43 | private Point[] points;
44 | private int screenWidth;
45 | private int screenHeight;
46 | private int bitmapWidth;
47 | private int bitmapHeight;
48 | private int cornerWidth = (int) dp2px(15);
49 | @Override
50 | protected void onCreate(Bundle savedInstanceState) {
51 | super.onCreate(savedInstanceState);
52 | setContentView(R.layout.activity_cropping);
53 | getSupportActionBar().hide();
54 | getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
55 | WindowManager.LayoutParams.FLAG_FULLSCREEN);
56 | imageView = findViewById(R.id.imageView);
57 | imageView.setScaleType(ImageView.ScaleType.FIT_XY);
58 | overlayView = findViewById(R.id.cropOverlayView);
59 | reTakeButton = findViewById(R.id.reTakeButton);
60 | reTakeButton.setOnClickListener(v -> {
61 | onBackPressed();
62 | });
63 | okayButton = findViewById(R.id.okayButton);
64 | okayButton.setOnClickListener(v -> {
65 | Intent intent = new Intent(this, ViewerActivity.class);
66 | intent.putExtra("imageUri",getIntent().getStringExtra("imageUri"));
67 | intent.putExtra("points",points);
68 | intent.putExtra("bitmapWidth",bitmapWidth);
69 | intent.putExtra("bitmapHeight",bitmapHeight);
70 | startActivity(intent);
71 | });
72 |
73 | corner1 = findViewById(R.id.corner1);
74 | corner2 = findViewById(R.id.corner2);
75 | corner3 = findViewById(R.id.corner3);
76 | corner4 = findViewById(R.id.corner4);
77 | corners[0] = corner1;
78 | corners[1] = corner2;
79 | corners[2] = corner3;
80 | corners[3] = corner4;
81 | imageView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
82 | @Override
83 | public void onGlobalLayout() {
84 | //updateOverlayViewLayout();
85 | }
86 | });
87 | DisplayMetrics metrics=new DisplayMetrics();
88 | getWindowManager().getDefaultDisplay().getMetrics(metrics);
89 | screenWidth=metrics.widthPixels;
90 | screenHeight=metrics.heightPixels;
91 | bitmapWidth = getIntent().getIntExtra("bitmapWidth",720);
92 | bitmapHeight = getIntent().getIntExtra("bitmapHeight",1280);
93 | loadPoints();
94 | loadImage();
95 | setEvents();
96 | }
97 |
98 | private void loadPoints(){
99 | Parcelable[] parcelables = getIntent().getParcelableArrayExtra("points");
100 | points = new Point[parcelables.length];
101 | for (int i = 0; i < parcelables.length; i++) {
102 | points[i] = (Point) parcelables[i];
103 | }
104 | }
105 |
106 | private void loadImage(){
107 | try {
108 | Uri uri = Uri.parse(getIntent().getStringExtra("imageUri"));
109 | InputStream inp = getContentResolver().openInputStream(uri);
110 | Bitmap bitmap = BitmapFactory.decodeStream(inp);
111 | bitmap = rotatedImageBasedOnExif(bitmap,uri.getPath());
112 | imageView.setImageBitmap(bitmap);
113 | background = bitmap;
114 | drawOverlay();
115 | updateCornersPosition();
116 | } catch (Exception e) {
117 | e.printStackTrace();
118 | }
119 | }
120 |
121 | private Bitmap rotatedImageBasedOnExif(Bitmap bitmap, String path) {
122 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
123 | ExifInterface exif = null;
124 | try {
125 | exif = new ExifInterface(path);
126 | } catch (IOException e) {
127 | return bitmap;
128 | }
129 | int rotate = 0;
130 | int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,
131 | ExifInterface.ORIENTATION_NORMAL);
132 | Log.d("DDN","orientation: "+orientation);
133 | switch (orientation) {
134 | case ExifInterface.ORIENTATION_ROTATE_270:
135 | rotate = 270;
136 | break;
137 | case ExifInterface.ORIENTATION_ROTATE_180:
138 | rotate = 180;
139 | break;
140 | case ExifInterface.ORIENTATION_ROTATE_90:
141 | rotate = 90;
142 | break;
143 | }
144 | Matrix matrix = new Matrix();
145 | matrix.postRotate(rotate);
146 | Bitmap rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
147 | bitmap.getHeight(), matrix, true);
148 | try (FileOutputStream out = new FileOutputStream(path)) {
149 | rotated.compress(Bitmap.CompressFormat.JPEG, 100, out);
150 | } catch (IOException e) {
151 | e.printStackTrace();
152 | }
153 | return rotated;
154 | }
155 | return bitmap;
156 | }
157 |
158 | private void updateCornersPosition(){
159 | for (int i = 0; i < 4; i++) {
160 | int offsetX = getOffsetX(i);
161 | int offsetY = getOffsetY(i);
162 | corners[i].setX(points[i].x*screenWidth/bitmapWidth+offsetX);
163 | corners[i].setY(points[i].y*screenHeight/bitmapHeight+offsetY);
164 | }
165 | }
166 |
167 | private int getOffsetX(int index) {
168 | if (index == 0) {
169 | return -cornerWidth;
170 | }else if (index == 1){
171 | return 0;
172 | }else if (index == 2){
173 | return 0;
174 | }else{
175 | return -cornerWidth;
176 | }
177 | }
178 |
179 | private int getOffsetY(int index) {
180 | if (index == 0) {
181 | return -cornerWidth;
182 | }else if (index == 1){
183 | return -cornerWidth;
184 | }else if (index == 2){
185 | return 0;
186 | }else{
187 | return 0;
188 | }
189 | }
190 |
191 | private void setEvents(){
192 | for (int i = 0; i < 4; i++) {
193 | corners[i].setOnTouchListener(new View.OnTouchListener() {
194 | @Override
195 | public boolean onTouch(View view, MotionEvent event) {
196 | Log.d("DDN",event.toString());
197 | int x = (int) event.getX();
198 | int y = (int) event.getY();
199 | switch (event.getAction()){
200 | case MotionEvent.ACTION_DOWN:
201 | mLastX = x;
202 | mLastY = y;
203 | break;
204 | case MotionEvent.ACTION_MOVE:
205 | view.setX(view.getX()+x);
206 | view.setY(view.getY()+y);
207 | updatePointsAndRedraw();
208 | break;
209 | default:
210 | break;
211 | }
212 | return true;
213 | }
214 | });
215 | }
216 | }
217 |
218 | private void updatePointsAndRedraw(){
219 | for (int i = 0; i < 4; i++) {
220 | int offsetX = getOffsetX(i);
221 | int offsetY = getOffsetY(i);
222 | points[i].x = (int) ((corners[i].getX()-offsetX)/screenWidth*bitmapWidth);
223 | points[i].y = (int) ((corners[i].getY()-offsetY)/screenHeight*bitmapHeight);
224 | }
225 | drawOverlay();
226 | }
227 |
228 | private void drawOverlay(){
229 | overlayView.setPointsAndImageGeometry(points,bitmapWidth,bitmapHeight);
230 | }
231 |
232 | private void updateOverlayViewLayout(){
233 | Bitmap bm = background;
234 | double ratioView = ((double) imageView.getWidth())/imageView.getHeight();
235 | double ratioImage = ((double) bm.getWidth())/bm.getHeight();
236 | double offsetX = (ratioImage*bm.getWidth()-bm.getHeight())/2;
237 | overlayView.setX((float) offsetX);
238 | }
239 |
240 | public float dp2px(float dp) {
241 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
242 | }
243 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dynamsoft/documentscanner/FrameMetadata.java:
--------------------------------------------------------------------------------
1 | package com.dynamsoft.documentscanner;
2 |
3 | /*
4 | * Copyright 2020 Google LLC. All rights reserved.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 |
19 | /** Describing a frame info. */
20 | public class FrameMetadata {
21 |
22 | private final int width;
23 | private final int height;
24 | private final int rotation;
25 |
26 | public int getWidth() {
27 | return width;
28 | }
29 |
30 | public int getHeight() {
31 | return height;
32 | }
33 |
34 | public int getRotation() {
35 | return rotation;
36 | }
37 |
38 | private FrameMetadata(int width, int height, int rotation) {
39 | this.width = width;
40 | this.height = height;
41 | this.rotation = rotation;
42 | }
43 |
44 | /** Builder of {@link FrameMetadata}. */
45 | public static class Builder {
46 |
47 | private int width;
48 | private int height;
49 | private int rotation;
50 |
51 | public Builder setWidth(int width) {
52 | this.width = width;
53 | return this;
54 | }
55 |
56 | public Builder setHeight(int height) {
57 | this.height = height;
58 | return this;
59 | }
60 |
61 | public Builder setRotation(int rotation) {
62 | this.rotation = rotation;
63 | return this;
64 | }
65 |
66 | public FrameMetadata build() {
67 | return new FrameMetadata(width, height, rotation);
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dynamsoft/documentscanner/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.dynamsoft.documentscanner;
2 |
3 | import androidx.appcompat.app.AppCompatActivity;
4 | import androidx.core.app.ActivityCompat;
5 | import androidx.core.content.ContextCompat;
6 |
7 | import android.Manifest;
8 | import android.content.Intent;
9 | import android.content.pm.PackageManager;
10 | import android.os.Bundle;
11 | import android.util.Log;
12 | import android.view.View;
13 | import android.widget.Button;
14 | import android.widget.Toast;
15 | import com.dynamsoft.license.LicenseManager;
16 |
17 | public class MainActivity extends AppCompatActivity {
18 | private Button startScanButton;
19 | private static final String[] CAMERA_PERMISSION = new String[]{Manifest.permission.CAMERA};
20 | private static final int CAMERA_REQUEST_CODE = 10;
21 | @Override
22 | protected void onCreate(Bundle savedInstanceState) {
23 | super.onCreate(savedInstanceState);
24 | setContentView(R.layout.activity_main);
25 | startScanButton = findViewById(R.id.startScanButton);
26 | startScanButton.setOnClickListener(v -> {
27 | if (hasCameraPermission()) {
28 | startScan();
29 | } else {
30 | requestPermission();
31 | }
32 | });
33 | initDynamsoftLicense();
34 | }
35 |
36 | private boolean hasCameraPermission() {
37 | return ContextCompat.checkSelfPermission(
38 | this,
39 | Manifest.permission.CAMERA
40 | ) == PackageManager.PERMISSION_GRANTED;
41 | }
42 |
43 | private void requestPermission() {
44 | ActivityCompat.requestPermissions(
45 | this,
46 | CAMERA_PERMISSION,
47 | CAMERA_REQUEST_CODE
48 | );
49 | }
50 |
51 | @Override
52 | public void onRequestPermissionsResult(int requestCode, String[] permissions,
53 | int[] grantResults) {
54 | super.onRequestPermissionsResult(requestCode, permissions, grantResults);
55 | switch (requestCode) {
56 | case CAMERA_REQUEST_CODE:
57 | if (grantResults.length > 0 &&
58 | grantResults[0] == PackageManager.PERMISSION_GRANTED) {
59 | startScan();
60 | } else {
61 | Toast.makeText(this, "Please grant camera permission", Toast.LENGTH_SHORT).show();
62 | }
63 | }
64 | }
65 |
66 | private void startScan(){
67 | Intent intent = new Intent(this, CameraActivity.class);
68 | startActivity(intent);
69 | }
70 |
71 | private void initDynamsoftLicense(){
72 | LicenseManager.initLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==", this, (isSuccess, error) -> {
73 | if (!isSuccess) {
74 | Log.e("DDN", "InitLicense Error: " + error);
75 | }else{
76 | Log.d("DDN","license valid");
77 | }
78 | });
79 | }
80 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dynamsoft/documentscanner/OverlayView.java:
--------------------------------------------------------------------------------
1 | package com.dynamsoft.documentscanner;
2 |
3 |
4 | import android.content.Context;
5 | import android.graphics.Canvas;
6 | import android.graphics.Color;
7 | import android.graphics.Paint;
8 | import android.graphics.PixelFormat;
9 | import android.graphics.Point;
10 | import android.graphics.PorterDuff;
11 | import android.util.AttributeSet;
12 | import android.util.Log;
13 | import android.view.SurfaceView;
14 | import android.view.SurfaceHolder;
15 |
16 |
17 | public class OverlayView extends SurfaceView implements SurfaceHolder.Callback {
18 |
19 | private int srcImageWidth;
20 | private int srcImageHeight;
21 | private SurfaceHolder surfaceHolder = null;
22 | private Point[] points = null;
23 | private Paint stroke = new Paint();
24 | public OverlayView(Context context, AttributeSet attrs) {
25 | super(context, attrs);
26 | setFocusable(true);
27 | Log.d("DDN","initialize overlay view");
28 | srcImageWidth = 0;
29 | srcImageHeight = 0;
30 | if(surfaceHolder == null) {
31 | // Get surfaceHolder object.
32 | surfaceHolder = getHolder();
33 | // Add this as surfaceHolder callback object.
34 | surfaceHolder.addCallback(this);
35 | }
36 | stroke.setColor(Color.GREEN);
37 | // Set the parent view background color. This can not set surfaceview background color.
38 | this.setBackgroundColor(Color.TRANSPARENT);
39 |
40 | // Set current surfaceview at top of the view tree.
41 | this.setZOrderOnTop(true);
42 |
43 | this.getHolder().setFormat(PixelFormat.TRANSLUCENT);
44 | }
45 |
46 | @Override
47 | public void surfaceCreated(SurfaceHolder surfaceHolder) {
48 | Log.d("DDN","surface created");
49 | if (points != null) {
50 | drawPolygon();
51 | }
52 | }
53 |
54 | @Override
55 | public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
56 |
57 | }
58 |
59 | @Override
60 | public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
61 |
62 | }
63 |
64 | public SurfaceHolder getSurfaceHolder(){
65 | return surfaceHolder;
66 | }
67 |
68 | public void setStroke(Paint paint){
69 | stroke = paint;
70 | }
71 | public Paint getStroke(){
72 | return stroke;
73 | }
74 |
75 | public Point[] getPoints(){
76 | return points;
77 | }
78 |
79 | public void setPoints(Point[] points){
80 | this.points = points;
81 | drawPolygon();
82 | }
83 |
84 | public void setPointsAndImageGeometry(Point[] points, int width,int height){
85 | this.srcImageWidth = width;
86 | this.srcImageHeight = height;
87 | this.points = points;
88 | drawPolygon();
89 | }
90 |
91 | public void setSrcImageWidth(int width) {
92 | Log.d("DDN","set image width: "+width);
93 | this.srcImageWidth = width;
94 | }
95 |
96 | public void setSrcImageHeight(int height) {
97 | Log.d("DDN","set image height: "+height);
98 | this.srcImageHeight = height;
99 | }
100 |
101 | public int getSrcImageWidth() {
102 | return srcImageWidth;
103 | }
104 |
105 | public int getSrcImageHeight() {
106 | return srcImageHeight;
107 | }
108 |
109 | public void drawPolygon()
110 | {
111 | Log.d("DDN","draw polygon");
112 | // Get and lock canvas object from surfaceHolder.
113 | Canvas canvas = surfaceHolder.lockCanvas();
114 | if (canvas == null) {
115 | Log.d("DDN","canvas is null");
116 | return;
117 | }
118 | Point[] pts;
119 | Log.d("DDN","srcImageHeight: "+srcImageHeight);
120 | if (srcImageWidth != 0 && srcImageHeight != 0) {
121 | Log.d("DDN","convert points");
122 | pts = convertPoints(canvas.getWidth(),canvas.getHeight());
123 | }else{
124 | pts = points;
125 | }
126 | // Clear canvas
127 | canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
128 | for (int index = 0; index <= pts.length - 1; index++) {
129 | if (index == pts.length - 1) {
130 | canvas.drawLine(pts[index].x,pts[index].y,pts[0].x,pts[0].y,stroke);
131 | }else{
132 | canvas.drawLine(pts[index].x,pts[index].y,pts[index+1].x,pts[index+1].y,stroke);
133 | }
134 | }
135 |
136 | // Unlock the canvas object and post the new draw.
137 | surfaceHolder.unlockCanvasAndPost(canvas);
138 | }
139 |
140 | public Point[] convertPoints(int canvasWidth, int canvasHeight){
141 | Point[] newPoints = new Point[points.length];
142 | double ratioX = ((double) canvasWidth)/srcImageWidth;
143 | double ratioY = ((double) canvasHeight)/srcImageHeight;
144 | for (int index = 0; index <= points.length - 1; index++) {
145 | Point p = new Point();
146 | p.x = (int) (ratioX * points[index].x);
147 | p.y = (int) (ratioY * points[index].y);
148 | newPoints[index] = p;
149 | }
150 | return newPoints;
151 | }
152 |
153 | }
154 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dynamsoft/documentscanner/Utils.java:
--------------------------------------------------------------------------------
1 | package com.dynamsoft.documentscanner;
2 |
3 | import android.graphics.Bitmap;
4 | import android.graphics.BitmapFactory;
5 | import android.graphics.Point;
6 | import android.graphics.Rect;
7 |
8 | import java.io.Closeable;
9 | import java.io.File;
10 | import java.io.RandomAccessFile;
11 |
12 | public class Utils {
13 |
14 | public static float intersectionOverUnion(Point[] pts1,Point[] pts2){
15 | Rect rect1 = getRectFromPoints(pts1);
16 | Rect rect2 = getRectFromPoints(pts2);
17 | return intersectionOverUnion(rect1,rect2);
18 | }
19 |
20 | public static float intersectionOverUnion(Rect rect1, Rect rect2){
21 | int leftColumnMax = Math.max(rect1.left, rect2.left);
22 | int rightColumnMin = Math.min(rect1.right,rect2.right);
23 | int upRowMax = Math.max(rect1.top, rect2.top);
24 | int downRowMin = Math.min(rect1.bottom,rect2.bottom);
25 |
26 | if (leftColumnMax>=rightColumnMin || downRowMin<=upRowMax){
27 | return 0;
28 | }
29 |
30 | int s1 = rect1.width()*rect1.height();
31 | int s2 = rect2.width()*rect2.height();
32 | float sCross = (downRowMin-upRowMax)*(rightColumnMin-leftColumnMax);
33 | return sCross/(s1+s2-sCross);
34 | }
35 |
36 | public static Rect getRectFromPoints(Point[] points){
37 | int left,top,right,bottom;
38 | left = points[0].x;
39 | top = points[0].y;
40 | right = 0;
41 | bottom = 0;
42 | for (Point point:points) {
43 | left = Math.min(point.x,left);
44 | top = Math.min(point.y,top);
45 | right = Math.max(point.x,right);
46 | bottom = Math.max(point.y,bottom);
47 | }
48 | return new Rect(left,top,right,bottom);
49 | }
50 |
51 | public static Bitmap bitmapFromFile(File file){
52 | byte[] b = readFile(file);
53 | Bitmap bitmap = BitmapFactory.decodeByteArray(b, 0, b.length);
54 | return bitmap;
55 | }
56 |
57 | public static byte[] readFile(File file) {
58 | RandomAccessFile rf = null;
59 | byte[] data = null;
60 | try {
61 | rf = new RandomAccessFile(file, "r");
62 | data = new byte[(int) rf.length()];
63 | rf.readFully(data);
64 | } catch (Exception exception) {
65 | exception.printStackTrace();
66 | } finally {
67 | closeQuietly(rf);
68 | }
69 | return data;
70 | }
71 |
72 | public static void closeQuietly(Closeable closeable) {
73 | try {
74 | if (closeable != null) {
75 | closeable.close();
76 | }
77 | } catch (Exception exception) {
78 | exception.printStackTrace();
79 | }
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dynamsoft/documentscanner/ViewerActivity.java:
--------------------------------------------------------------------------------
1 | package com.dynamsoft.documentscanner;
2 |
3 | import androidx.appcompat.app.AppCompatActivity;
4 | import androidx.core.app.ActivityCompat;
5 | import androidx.core.content.ContextCompat;
6 |
7 | import android.Manifest;
8 | import android.content.Intent;
9 | import android.content.pm.PackageManager;
10 | import android.content.res.Resources;
11 | import android.graphics.Bitmap;
12 | import android.graphics.Matrix;
13 | import android.graphics.Point;
14 | import android.net.Uri;
15 | import android.os.Bundle;
16 | import android.os.Environment;
17 | import android.os.Parcelable;
18 | import android.provider.MediaStore;
19 | import android.util.Log;
20 | import android.widget.Button;
21 | import android.widget.ImageView;
22 | import android.widget.RadioButton;
23 | import android.widget.RadioGroup;
24 | import android.widget.Toast;
25 |
26 | import com.dynamsoft.core.basic_structures.CapturedResult;
27 | import com.dynamsoft.core.basic_structures.Quadrilateral;
28 | import com.dynamsoft.cvr.CaptureVisionRouter;
29 | import com.dynamsoft.cvr.CaptureVisionRouterException;
30 | import com.dynamsoft.cvr.SimplifiedCaptureVisionSettings;
31 | import com.dynamsoft.ddn.NormalizedImageResultItem;
32 | import com.jsibbold.zoomage.ZoomageView;
33 |
34 | import java.io.ByteArrayOutputStream;
35 | import java.io.File;
36 | import java.io.FileNotFoundException;
37 | import java.io.FileOutputStream;
38 | import java.io.IOException;
39 | import java.io.InputStream;
40 | import java.nio.ByteBuffer;
41 |
42 | public class ViewerActivity extends AppCompatActivity {
43 |
44 | private ZoomageView normalizedImageView;
45 | private Point[] points;
46 | private Bitmap rawImage;
47 | private Bitmap normalized;
48 | private CaptureVisionRouter cvr;
49 | private int rotation = 0;
50 | private String templateName = "NormalizeDocument_Binary";
51 |
52 | private static final String[] WRITE_EXTERNAL_STORAGE_PERMISSION = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
53 | private static final int WRITE_EXTERNAL_STORAGE_REQUEST_CODE = 10;
54 |
55 |
56 | @Override
57 | protected void onCreate(Bundle savedInstanceState) {
58 | super.onCreate(savedInstanceState);
59 | setContentView(R.layout.activity_viewer);
60 |
61 | Button rotateButton = findViewById(R.id.rotateButton);
62 | Button saveImageButton = findViewById(R.id.saveImageButton);
63 |
64 | rotateButton.setOnClickListener(v -> {
65 | rotation = rotation + 90;
66 | if (rotation == 360) {
67 | rotation = 0;
68 | }
69 | normalizedImageView.setRotation(rotation);
70 | });
71 |
72 | saveImageButton.setOnClickListener(v -> {
73 | if (hasStoragePermission()) {
74 | saveImage(normalized);
75 | }else{
76 | requestPermission();
77 | }
78 | });
79 |
80 | RadioGroup filterRadioGroup = findViewById(R.id.filterRadioGroup);
81 | filterRadioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
82 | @Override
83 | public void onCheckedChanged(RadioGroup group, int checkedId) {
84 | if (checkedId == R.id.binaryRadioButton) {
85 | templateName = "NormalizeDocument_Binary";
86 | }else if (checkedId == R.id.grayscaleRadioButton) {
87 | templateName = "NormalizeDocument_Gray";
88 | }else{
89 | templateName = "NormalizeDocument_Color";
90 | }
91 | normalize();
92 | }
93 | });
94 |
95 | normalizedImageView = findViewById(R.id.normalizedImageView);
96 |
97 | cvr = new CaptureVisionRouter(ViewerActivity.this);
98 | try {
99 | cvr.initSettings("{\"CaptureVisionTemplates\": [{\"Name\": \"Default\"},{\"Name\": \"DetectDocumentBoundaries_Default\",\"ImageROIProcessingNameArray\": [\"roi-detect-document-boundaries\"]},{\"Name\": \"DetectAndNormalizeDocument_Default\",\"ImageROIProcessingNameArray\": [\"roi-detect-and-normalize-document\"]},{\"Name\": \"NormalizeDocument_Binary\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-binary\"]}, {\"Name\": \"NormalizeDocument_Gray\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-gray\"]}, {\"Name\": \"NormalizeDocument_Color\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-color\"]}],\"TargetROIDefOptions\": [{\"Name\": \"roi-detect-document-boundaries\",\"TaskSettingNameArray\": [\"task-detect-document-boundaries\"]},{\"Name\": \"roi-detect-and-normalize-document\",\"TaskSettingNameArray\": [\"task-detect-and-normalize-document\"]},{\"Name\": \"roi-normalize-document-binary\",\"TaskSettingNameArray\": [\"task-normalize-document-binary\"]}, {\"Name\": \"roi-normalize-document-gray\",\"TaskSettingNameArray\": [\"task-normalize-document-gray\"]}, {\"Name\": \"roi-normalize-document-color\",\"TaskSettingNameArray\": [\"task-normalize-document-color\"]}],\"DocumentNormalizerTaskSettingOptions\": [{\"Name\": \"task-detect-and-normalize-document\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-detect-and-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-detect-and-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-detect-and-normalize\"}]},{\"Name\": \"task-detect-document-boundaries\",\"TerminateSetting\": {\"Section\": \"ST_DOCUMENT_DETECTION\"},\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-detect\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-detect\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-detect\"}]},{\"Name\": \"task-normalize-document-binary\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\", \"ColourMode\": \"ICM_BINARY\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]}, {\"Name\": \"task-normalize-document-gray\", \"ColourMode\": \"ICM_GRAYSCALE\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]}, {\"Name\": \"task-normalize-document-color\", \"ColourMode\": \"ICM_COLOUR\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]}],\"ImageParameterOptions\": [{\"Name\": \"ip-detect-and-normalize\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7}},{\"Name\": \"ip-detect\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0,\"ThresholdCompensation\" : 7}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7},\"ScaleDownThreshold\" : 512},{\"Name\": \"ip-normalize\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7}}]}");
100 | } catch (CaptureVisionRouterException e) {
101 | e.printStackTrace();
102 | }
103 | loadImageAndPoints();
104 | normalize();
105 | }
106 |
107 | private void loadImageAndPoints(){
108 | Uri uri = Uri.parse(getIntent().getStringExtra("imageUri"));
109 | try {
110 | rawImage = MediaStore.Images.Media.getBitmap(getContentResolver(),uri);
111 | } catch (IOException e) {
112 | e.printStackTrace();
113 | return;
114 | }
115 | int bitmapWidth = getIntent().getIntExtra("bitmapWidth",720);
116 | int bitmapHeight = getIntent().getIntExtra("bitmapHeight",1280);
117 | Parcelable[] parcelables = getIntent().getParcelableArrayExtra("points");
118 | points = new Point[parcelables.length];
119 | for (int i = 0; i < parcelables.length; i++) {
120 | points[i] = (Point) parcelables[i];
121 | points[i].x = points[i].x*rawImage.getWidth()/bitmapWidth;
122 | points[i].y = points[i].y*rawImage.getHeight()/bitmapHeight;
123 | }
124 |
125 | }
126 |
127 | private void normalize(){
128 | try {
129 | Quadrilateral quad = new Quadrilateral();
130 | quad.points = points;
131 | SimplifiedCaptureVisionSettings settings = cvr.getSimplifiedSettings(templateName);
132 | settings.roi = quad;
133 | settings.roiMeasuredInPercentage = false;
134 | cvr.updateSettings(templateName,settings);
135 | CapturedResult capturedResult = cvr.capture(rawImage,templateName);
136 | NormalizedImageResultItem result = (NormalizedImageResultItem) capturedResult.getItems()[0];
137 | normalized = result.getImageData().toBitmap();
138 | normalizedImageView.setImageBitmap(normalized);
139 | } catch (Exception e) {
140 | e.printStackTrace();
141 | }
142 | }
143 |
144 |
145 | public void saveImage(Bitmap bmp) {
146 | File appDir = new File(this.getApplicationContext().getExternalFilesDir(""), "ddn");
147 | if (!appDir.exists()) {
148 | appDir.mkdir();
149 | }
150 | String fileName = System.currentTimeMillis() + ".jpg";
151 | File file = new File(appDir, fileName);
152 | try {
153 | FileOutputStream fos = new FileOutputStream(file);
154 | if (rotation != 0) {
155 | Matrix matrix = new Matrix();
156 | matrix.setRotate(rotation);
157 | Bitmap rotated = Bitmap.createBitmap(bmp,0,0,bmp.getWidth(),bmp.getHeight(),matrix,false);
158 | rotated.compress(Bitmap.CompressFormat.JPEG, 100, fos);
159 | }else{
160 | bmp.compress(Bitmap.CompressFormat.JPEG, 100, fos);
161 | }
162 | fos.flush();
163 | fos.close();
164 | Toast.makeText(ViewerActivity.this,"File saved to "+file.getAbsolutePath(),Toast.LENGTH_LONG).show();
165 | } catch (FileNotFoundException e) {
166 | e.printStackTrace();
167 | } catch (IOException e) {
168 | e.printStackTrace();
169 | }
170 | }
171 |
172 | private boolean hasStoragePermission() {
173 | return ContextCompat.checkSelfPermission(
174 | this,
175 | Manifest.permission.WRITE_EXTERNAL_STORAGE
176 | ) == PackageManager.PERMISSION_GRANTED;
177 | }
178 |
179 | private void requestPermission() {
180 | ActivityCompat.requestPermissions(
181 | this,
182 | WRITE_EXTERNAL_STORAGE_PERMISSION,
183 | WRITE_EXTERNAL_STORAGE_REQUEST_CODE
184 | );
185 | }
186 |
187 | @Override
188 | public void onRequestPermissionsResult(int requestCode, String[] permissions,
189 | int[] grantResults) {
190 | super.onRequestPermissionsResult(requestCode, permissions, grantResults);
191 | switch (requestCode) {
192 | case WRITE_EXTERNAL_STORAGE_REQUEST_CODE:
193 | if (grantResults.length > 0 &&
194 | grantResults[0] == PackageManager.PERMISSION_GRANTED) {
195 | saveImage(normalized);
196 | } else {
197 | Toast.makeText(this, "Please grant the permission to write external storage.", Toast.LENGTH_SHORT).show();
198 | }
199 | }
200 | }
201 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/corner.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_camera.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
16 |
17 |
18 |
19 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_cropping.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
20 |
21 |
31 |
32 |
39 |
40 |
47 |
48 |
55 |
56 |
63 |
64 |
72 |
73 |
74 |
75 |
83 |
84 |
90 |
91 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_viewer.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
26 |
27 |
35 |
36 |
42 |
43 |
49 |
50 |
51 |
52 |
61 |
62 |
68 |
69 |
74 |
75 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tony-xlh/Android-Document-Scanner/d409b432a307ae5a8848cd922a70c1389b70965c/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tony-xlh/Android-Document-Scanner/d409b432a307ae5a8848cd922a70c1389b70965c/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tony-xlh/Android-Document-Scanner/d409b432a307ae5a8848cd922a70c1389b70965c/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tony-xlh/Android-Document-Scanner/d409b432a307ae5a8848cd922a70c1389b70965c/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tony-xlh/Android-Document-Scanner/d409b432a307ae5a8848cd922a70c1389b70965c/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tony-xlh/Android-Document-Scanner/d409b432a307ae5a8848cd922a70c1389b70965c/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tony-xlh/Android-Document-Scanner/d409b432a307ae5a8848cd922a70c1389b70965c/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tony-xlh/Android-Document-Scanner/d409b432a307ae5a8848cd922a70c1389b70965c/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tony-xlh/Android-Document-Scanner/d409b432a307ae5a8848cd922a70c1389b70965c/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tony-xlh/Android-Document-Scanner/d409b432a307ae5a8848cd922a70c1389b70965c/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-land/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 48dp
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w1240dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 200dp
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w600dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 48dp
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 16dp
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Document Scanner
3 | ViewerActivity
4 |
5 | First Fragment
6 | Second Fragment
7 | Next
8 | Previous
9 |
10 | Hello first fragment
11 | Hello second fragment. Arg: %1$s
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/test/java/com/dynamsoft/documentscanner/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.dynamsoft.documentscanner;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | id 'com.android.application' version '7.1.3' apply false
4 | id 'com.android.library' version '7.1.3' apply false
5 | }
6 |
7 | task clean(type: Delete) {
8 | delete rootProject.buildDir
9 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Enables namespacing of each library's R class so that its R class includes only the
19 | # resources declared in the library itself and none from the library's dependencies,
20 | # thereby reducing the size of the R class for that library
21 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tony-xlh/Android-Document-Scanner/d409b432a307ae5a8848cd922a70c1389b70965c/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Aug 16 11:13:21 CST 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | ## Android Document Scanner
2 |
3 | Android Document Scanner using [Dynamsoft Document Normalizer](https://www.dynamsoft.com/document-normalizer/docs/introduction/).
4 |
5 | You can [apply for a trial license](https://www.dynamsoft.com/customer/license/trialLicense/?product=dcv&package=cross-platform) and update [this line](https://github.com/xulihang/Android-Document-Scanner/blob/78ba04916bb395ae82ddd586bd6ab0c74def39ab/app/src/main/java/com/dynamsoft/documentscanner/MainActivity.java#L75) to use Dynamsoft Document Normalizer.
6 |
7 |
8 | ### Document Scanning Process
9 |
10 | 1. Start the camera using CameraX and analyse the frames to detect the boundary of documents. When the IOU of three consecutive detected polygons are over 90%, take a photo.
11 | 2. After the photo is taken, the users are directed to a cropping activity. They can drag the corner points to adjust the detected polygons.
12 | 3. If the user confirms that the polygon is correct, the app then runs perspective correction and cropping to get a normalized document image. Users can rotate the image and set the color mode (binary, grayscale and color) of the image.
13 |
14 | A demo video of the whole process.
15 |
16 |
18 |
19 | ### Features
20 |
21 | 1. live detection of documents
22 | 2. auto scan
23 | 3. edit detected polygons of documents
24 | 4. support three color mode: binary, grayscale and color
25 |
26 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | maven {
14 | url "https://download2.dynamsoft.com/maven/aar"
15 | }
16 | }
17 | }
18 | rootProject.name = "Document Scanner"
19 | include ':app'
20 |
--------------------------------------------------------------------------------