diff --git a/README.md b/README.md index 32ed4aa..997a453 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# Acuant Android SDK v11.5.0 -**January 2022** +# Acuant Android SDK v11.5.1 +**February 2022** See [https://github.com/Acuant/AndroidSDKV11/releases](https://github.com/Acuant/AndroidSDKV11/releases) for release notes. @@ -43,25 +43,25 @@ Before 11.5.0, the SDK was not compiled with AndroidX. The SDK could still be us The SDK includes the following modules: -**Acuant Common Library (AcuantCommon) :** +**Acuant Common Library (AcuantCommon):** - Contains shared internal models and supporting classes. -**Acuant Camera Library (AcuantCamera) :** +**Acuant Camera Library (AcuantCamera):** - Implemented using CameraX API and uses Google ML Kit for barcode reading. ML Kit model is packaged in the SDK (no outbound call to download model from Google Play services). - Encompasses three different versions of the camera for reading document and barcodes, reading MRZ zones, and a backup camera for reading only barcodes. - Uses AcuantImagePreparation for document detection and cropping. -**Acuant Image Preparation Library (AcuantImagePreparation) :** +**Acuant Image Preparation Library (AcuantImagePreparation):** - Contains all image processing including document detection, cropping, and metrics calculation. -**Acuant Document Processing Library (AcuantDocumentProcessing) :** +**Acuant Document Processing Library (AcuantDocumentProcessing):** - Contains all the methods to upload and process document images. -**Acuant Face Match Library (AcuantFaceMatch) :** +**Acuant Face Match Library (AcuantFaceMatch):** - Contains a method to match two face images. @@ -116,15 +116,15 @@ The SDK includes the following modules: - Add the following dependencies - implementation 'com.acuant:acuantcommon:11.5.0' - implementation 'com.acuant:acuantcamera:11.5.0' - implementation 'com.acuant:acuantimagepreparation:11.5.0' - implementation 'com.acuant:acuantdocumentprocessing:11.5.0' - implementation 'com.acuant:acuantechipreader:11.5.0' - implementation 'com.acuant:acuantipliveness:11.5.0' - implementation 'com.acuant:acuantfacematch:11.5.0' - implementation 'com.acuant:acuantfacecapture:11.5.0' - implementation 'com.acuant:acuantpassiveliveness:11.5.0' + implementation 'com.acuant:acuantcommon:11.5.1' + implementation 'com.acuant:acuantcamera:11.5.1' + implementation 'com.acuant:acuantimagepreparation:11.5.1' + implementation 'com.acuant:acuantdocumentprocessing:11.5.1' + implementation 'com.acuant:acuantechipreader:11.5.1' + implementation 'com.acuant:acuantipliveness:11.5.1' + implementation 'com.acuant:acuantfacematch:11.5.1' + implementation 'com.acuant:acuantfacecapture:11.5.1' + implementation 'com.acuant:acuantpassiveliveness:11.5.1' 1. Create an xml file with the following tags (If you plan to use bearer tokens to initialize, then username and password can be left blank): @@ -435,9 +435,15 @@ After you capture a document image and completed crop, it can be processed using fun createInstance(options: IdInstanceOptions, listener: CreateIdInstanceListener) - interface CreateIdInstanceListener : AcuantListener { - fun instanceCreated(instance: AcuantIdDocumentInstance) - } + class IdInstanceOptions ( + val authenticationSensitivity: AuthenticationSensitivity, + val tamperSensitivity: TamperSensitivity, + val countryCode: String? + ) + + interface CreateIdInstanceListener : AcuantListener { + fun instanceCreated(instance: AcuantIdDocumentInstance) + } 1. All further methods will be called on the instanced returned thorough the instanceCreated callback. You can run multiple instances simultaneously. Each instances tracks its own state independently. These are the available methods and relevant objects/interfaces: @@ -769,6 +775,20 @@ Must include EchipInitializer() in initialization (See **Initializing the SDK**) fun setColorBracketHold(value: Int) : DocumentCameraOptionsBuilder fun setColorBracketCloser(value: Int) : DocumentCameraOptionsBuilder fun setColorBracketCapturing(value: Int) : DocumentCameraOptionsBuilder + /** + * [ZoomType.Generic] keeps the camera zoomed out to enable you to use nearly all available + * capture space. This is the default setting. Use this setting to capture large + * documents (ID3) and to use old devices with low-resolution cameras. + * + * [ZoomType.IdOnly] zooms the camera by approximately 25%, pushing part of the capture + * space off the sides of the screen. Generally, IDs are smaller than passports and, on most + * devices, the capture space is sufficient for a 600 dpi capture of an ID. The + * [ZoomType.IdOnly] experience is more intuitive for users because [ZoomType.Generic] makes + * the the ID appear too far away for capture. Using [ZoomType.IdOnly] to capture large + * documents (ID3) usually results in a lower resolution capture that can cause + * classification/authentication errors. + */ + fun setZoomType(value: ZoomType) : DocumentCameraOptionsBuilder fun build() : AcuantCameraOptions } @@ -849,7 +869,7 @@ Acuant does not provide obfuscation tools. See the Android developer documentati ------------------------------------- -**Copyright 2021 Acuant Inc. All rights reserved.** +**Copyright 2022 Acuant Inc. All rights reserved.** This document contains proprietary and confidential information and creative works owned by Acuant and its respective licensors, if any. Any use, copying, publication, distribution, display, modification, or transmission of such technology, in whole or in part, in any form or by any means, without the prior express written permission of Acuant is strictly prohibited. Except where expressly provided by Acuant in writing, possession of this information shall not be construed to confer any license or rights under any Acuant intellectual property rights, whether by estoppel, implication, or otherwise. diff --git a/acuantcamera/build.gradle b/acuantcamera/build.gradle index fb49ccb..bcc1ec9 100644 --- a/acuantcamera/build.gradle +++ b/acuantcamera/build.gradle @@ -31,30 +31,29 @@ android { viewBinding true } } - dependencies { // Kotlin lang implementation 'androidx.core:core-ktx:1.7.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' // App compat and UI things - implementation 'androidx.appcompat:appcompat:1.4.0' + implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.2' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' //noinspection GradleDependency implementation 'androidx.window:window:1.0.0-alpha09' //newer versions changed the api, not updating yet // CameraX library - def camerax_version = '1.1.0-alpha12' + def camerax_version = '1.1.0-beta01' implementation "androidx.camera:camera-core:$camerax_version" implementation "androidx.camera:camera-camera2:$camerax_version" implementation "androidx.camera:camera-lifecycle:$camerax_version" - implementation "androidx.camera:camera-view:1.0.0-alpha32" + implementation "androidx.camera:camera-view:$camerax_version" //acuant specific stuff implementation 'androidx.exifinterface:exifinterface:1.3.3' - implementation 'com.google.mlkit:barcode-scanning:17.0.1' + implementation 'com.google.mlkit:barcode-scanning:17.0.2' implementation 'com.rmtheis:tess-two:9.1.0' - implementation 'com.acuant:acuantcommon:11.5.0' - implementation 'com.acuant:acuantimagepreparation:11.5.0' -} \ No newline at end of file + implementation 'com.acuant:acuantcommon:11.5.1' + implementation 'com.acuant:acuantimagepreparation:11.5.1' +} diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantBaseCameraFragment.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantBaseCameraFragment.kt index 66cf3d2..8e8e296 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantBaseCameraFragment.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantBaseCameraFragment.kt @@ -4,6 +4,7 @@ import android.Manifest import android.annotation.SuppressLint import android.app.AlertDialog import android.content.pm.PackageManager +import android.graphics.Point import android.os.Bundle import android.util.Log import android.view.* @@ -11,35 +12,38 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.camera.core.* import androidx.camera.core.impl.utils.Exif import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.exifinterface.media.ExifInterface import androidx.fragment.app.Fragment import androidx.window.WindowManager import com.acuant.acuantcamera.R +import com.acuant.acuantcamera.databinding.FragmentCameraBinding import com.acuant.acuantcamera.interfaces.IAcuantSavedImage import com.acuant.acuantcamera.interfaces.ICameraActivityFinish -import com.acuant.acuantcamera.databinding.FragmentCameraBinding import com.acuant.acuantcamera.overlay.BaseRectangleView import com.acuant.acuantcommon.model.AcuantError import com.acuant.acuantcommon.model.ErrorCodes import com.acuant.acuantcommon.model.ErrorDescriptions import com.acuant.acuantimagepreparation.AcuantImagePreparation +import com.google.common.util.concurrent.ListenableFuture import org.json.JSONObject +import java.io.* import java.util.concurrent.ExecutorService import java.util.concurrent.Executors -import java.io.* abstract class AcuantBaseCameraFragment: Fragment() { private var displayId: Int = -1 private var lensFacing: Int = CameraSelector.LENS_FACING_BACK - private var imageCapture: ImageCapture? = null private var camera: Camera? = null private var cameraProvider: ProcessCameraProvider? = null private var orientationEventListener: OrientationEventListener? = null private var preview: Preview? = null + private var failedToFocus: Boolean = false private lateinit var windowManager: WindowManager + protected var imageCapture: ImageCapture? = null protected var capturing: Boolean = false protected var fragmentCameraBinding: FragmentCameraBinding? = null protected var imageAnalyzer: ImageAnalysis? = null //set up by implementations @@ -69,10 +73,11 @@ abstract class AcuantBaseCameraFragment: Fragment() { override fun onDestroyView() { fragmentCameraBinding = null - super.onDestroyView() - // Shut down our background executor + imageAnalyzer?.clearAnalyzer() cameraExecutor.shutdown() + cameraProvider?.unbindAll() + super.onDestroyView() } override fun onCreateView( @@ -143,6 +148,9 @@ abstract class AcuantBaseCameraFragment: Fragment() { val binding = fragmentCameraBinding if (binding != null) { + if (acuantOptions.zoomType == AcuantCameraOptions.ZoomType.IdOnly) { + (binding.viewFinder.layoutParams as ConstraintLayout.LayoutParams?)?.dimensionRatio = "" + } // Keep track of the display in which this view is attached displayId = binding.viewFinder.display.displayId @@ -236,78 +244,128 @@ abstract class AcuantBaseCameraFragment: Fragment() { .setTargetRotation(rotation) .build() - buildImageAnalyzer(screenAspectRatio, rotation) + val trueScreenRatio = fragmentCameraBinding!!.viewFinder.height / fragmentCameraBinding!!.viewFinder.width.toFloat() + + buildImageAnalyzer(screenAspectRatio, trueScreenRatio, rotation) // Must unbind the use-cases before rebinding them cameraProvider.unbindAll() try { - // A variable number of use-cases can be passed here - - // camera provides access to CameraControl & CameraInfo - camera = if (imageAnalyzer == null) { - cameraProvider.bindToLifecycle( - this, cameraSelector, preview, imageCapture) - } else { - cameraProvider.bindToLifecycle( - this, cameraSelector, preview, imageCapture, imageAnalyzer) + val useCaseGroupBuilder = UseCaseGroup.Builder().addUseCase(preview!!).addUseCase(imageCapture!!) + + if (imageAnalyzer != null) { + useCaseGroupBuilder.addUseCase(imageAnalyzer!!) } + camera = cameraProvider.bindToLifecycle(this, cameraSelector, useCaseGroupBuilder.build()) + // Attach the viewfinder's surface provider to preview use case preview?.setSurfaceProvider(fragmentCameraBinding!!.viewFinder.surfaceProvider) observeCameraState(camera?.cameraInfo!!) - //todo camera.cameracontrol startFocusAndMetering to keep focus on the middle of the image and avoid focusing on reflections/background? } catch (exc: Exception) { Log.e(TAG, "Use case binding failed", exc) } } - abstract fun buildImageAnalyzer(screenAspectRatio: Int, rotation: Int) + abstract fun buildImageAnalyzer(screenAspectRatio: Int, trueScreenRatio: Float, rotation: Int) - fun captureImage (listener: IAcuantSavedImage, captureType: String? = null) { + abstract fun resetWorkflow() + + fun captureImage (listener: IAcuantSavedImage, middle: Point? = null, captureType: String? = null) { if (!capturing) { - imageCapture?.let { imageCapture -> - capturing = true + capturing = true + if (!failedToFocus) { + val width = fragmentCameraBinding?.root?.width?.toFloat() + val height = fragmentCameraBinding?.root?.height?.toFloat() + val action: FocusMeteringAction = + if (middle == null || width == null || height == null || middle.x == 0) { + val factory = SurfaceOrientedMeteringPointFactory(1f, 1f) + val point: MeteringPoint = factory.createPoint(0.5f, 0.5f) + FocusMeteringAction.Builder(point).build() + } else { + val factory = SurfaceOrientedMeteringPointFactory(width, height) + val point: MeteringPoint = + factory.createPoint(middle.x.toFloat(), middle.y.toFloat()) + FocusMeteringAction.Builder(point).build() + } + val future: ListenableFuture? = + camera?.cameraControl?.startFocusAndMetering(action) + if (future != null) { + future.addListener({ + try { + val result = future.get() + if (result.isFocusSuccessful) { + performCapture(listener, captureType) + } else { + failedToFocus = true + capturing = false + resetWorkflow() + } + } catch (e: Exception) { + failedToFocus = true + capturing = false + resetWorkflow() + } + }, ContextCompat.getMainExecutor(requireContext())) + } else { + listener.onError( + AcuantError( + ErrorCodes.ERROR_UnexpectedError, + ErrorDescriptions.ERROR_DESC_UnexpectedError, + "ListenableFuture was null, which likely means camera was null. This should not happen." + ) + ) + } + } else { + performCapture(listener, captureType) + } + } + } - // Create output file to hold the image (will automatically add numbers to create a uuid) - val photoFile = - File.createTempFile("AcuantCameraImage", ".jpg", requireActivity().cacheDir) + private fun performCapture(listener: IAcuantSavedImage, captureType: String?) { - // Create output options object which contains file + metadata - val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() + imageCapture?.let { imageCapture -> - // Setup image capture listener which is triggered after photo has been taken - imageCapture.takePicture( - outputOptions, - cameraExecutor, - object : ImageCapture.OnImageSavedCallback { - @SuppressLint("RestrictedApi") - override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { + // Create output file to hold the image (will automatically add numbers to create a uuid) + val photoFile = + File.createTempFile("AcuantCameraImage", ".jpg", requireActivity().cacheDir) - val savedUri = outputFileResults.savedUri?.path ?: photoFile.absolutePath + // Create output options object which contains file + metadata + val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() - val exif = Exif.createFromFileString(savedUri) - val rotation = exif.rotation + // Setup image capture listener which is triggered after photo has been taken + imageCapture.takePicture( + outputOptions, + cameraExecutor, + object : ImageCapture.OnImageSavedCallback { + @SuppressLint("RestrictedApi") + override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { - if (captureType != null) { - addExif(File(savedUri), captureType, rotation) - } else { - addExif(File(savedUri), "NOT SPECIFIED (implementer used deprecated constructor that lacks this data)", rotation) - } + val savedUri = outputFileResults.savedUri?.path ?: photoFile.absolutePath + + val exif = Exif.createFromFileString(savedUri) + val rotation = exif.rotation - listener.onSaved(savedUri) + if (captureType != null) { + addExif(File(savedUri), captureType, rotation) + } else { + addExif(File(savedUri), "NOT SPECIFIED (implementer used deprecated constructor that lacks this data)", rotation) } - override fun onError(exception: ImageCaptureException) { - listener.onError( - AcuantError( - ErrorCodes.ERROR_SavingImage, - ErrorDescriptions.ERROR_DESC_SavingImage, - exception.toString() - ) + listener.onSaved(savedUri) + } + + override fun onError(exception: ImageCaptureException) { + listener.onError( + AcuantError( + ErrorCodes.ERROR_SavingImage, + ErrorDescriptions.ERROR_DESC_SavingImage, + exception.toString() ) - } - }) - } + ) + } + }) } } @@ -369,6 +427,24 @@ abstract class AcuantBaseCameraFragment: Fragment() { } } + //extension credit to: https://stackoverflow.com/questions/1967039/onclicklistener-x-y-location-of-event + @SuppressLint("ClickableViewAccessibility") + protected fun View.setOnClickListenerWithPoint(action: (Point) -> Unit) { + val coordinates = Point() + val screenPosition = IntArray(2) + setOnTouchListener { v, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + v.getLocationOnScreen(screenPosition) + coordinates.set(event.x.toInt() + screenPosition[0], event.y.toInt() + screenPosition[1]) + } +// v.performClick() + false + } + setOnClickListener { + action.invoke(coordinates) + } + } + companion object { private const val TAG = "Acuant Camera" internal const val INTERNAL_OPTIONS = "options_internal" diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantCameraActivity.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantCameraActivity.kt index c4622cc..2dcdebf 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantCameraActivity.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantCameraActivity.kt @@ -13,8 +13,6 @@ import com.acuant.acuantcamera.constant.* import com.acuant.acuantcamera.databinding.ActivityCameraBinding import com.acuant.acuantcamera.helper.MrzResult import com.acuant.acuantcommon.model.AcuantError -import com.google.android.gms.common.ConnectionResult -import com.google.android.gms.common.GoogleApiAvailability class AcuantCameraActivity: AppCompatActivity(), ICameraActivityFinish { @@ -35,11 +33,6 @@ class AcuantCameraActivity: AppCompatActivity(), ICameraActivityFinish { unserializedOptions as AcuantCameraOptions } - //if the user wants to use google's barcode reader check if google apis are available - if (options.useGMS) { - options.useGMS = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS - } - //start the camera if this si the first time the activity is created (camera already exists otherwise) if (savedInstanceState == null) { val cameraFragment: AcuantBaseCameraFragment = when (options.cameraMode) { diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantCameraOptions.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantCameraOptions.kt index 0cedd3e..5252522 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantCameraOptions.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/AcuantCameraOptions.kt @@ -28,13 +28,15 @@ internal constructor( internal val colorBracketCloser : Int = Color.RED, internal val colorBracketHold : Int = Color.YELLOW, internal val colorBracketCapturing : Int = Color.GREEN, - internal var useGMS: Boolean = true, internal val cardRatio : Float = 0.65f, - internal val cameraMode: CameraMode = CameraMode.Document + internal val cameraMode: CameraMode = CameraMode.Document, + internal val zoomType: ZoomType = ZoomType.Generic ) : Serializable { enum class CameraMode {Document, Mrz, BarcodeOnly} + enum class ZoomType {Generic, IdOnly} + companion object { const val DEFAULT_TIMEOUT_BARCODE = 20000 const val DEFAULT_DELAY_BARCODE = 800 @@ -56,7 +58,7 @@ internal constructor( private var colorBracketCloser : Int = Color.RED private var colorBracketHold : Int = Color.YELLOW private var colorBracketCapturing : Int = Color.GREEN - private var useGms: Boolean = true + private var zoomType: ZoomType = ZoomType.Generic private val cardRatio : Float = 0.65f fun setTimeInMsPerDigit(value: Int) : DocumentCameraOptionsBuilder { @@ -129,8 +131,26 @@ internal constructor( return this } + @Deprecated("No longer reliant on GMS, option is ignored", ReplaceWith("")) fun setUseGms(value: Boolean) : DocumentCameraOptionsBuilder { - useGms = value + return this + } + + /** + * [ZoomType.Generic] keeps the camera zoomed out to enable you to use nearly all available + * capture space. This is the default setting. Use this setting to capture large + * documents (ID3) and to use old devices with low-resolution cameras. + * + * [ZoomType.IdOnly] zooms the camera by approximately 25%, pushing part of the capture + * space off the sides of the screen. Generally, IDs are smaller than passports and, on most + * devices, the capture space is sufficient for a 600 dpi capture of an ID. The + * [ZoomType.IdOnly] experience is more intuitive for users because [ZoomType.Generic] makes + * the the ID appear too far away for capture. Using [ZoomType.IdOnly] to capture large + * documents (ID3) usually results in a lower resolution capture that can cause + * classification/authentication errors. + */ + fun setZoomType(value: ZoomType) : DocumentCameraOptionsBuilder { + zoomType = value return this } @@ -138,7 +158,7 @@ internal constructor( @Suppress("DEPRECATION") return AcuantCameraOptions(timeInMsPerDigit, digitsToShow, allowBox, autoCapture, bracketLengthInHorizontal, bracketLengthInVertical, defaultBracketMarginWidth, defaultBracketMarginHeight, colorHold, - colorCapturing, colorBracketAlign, colorBracketCloser, colorBracketHold, colorBracketCapturing, useGms, cardRatio, cameraMode = CameraMode.Document) + colorCapturing, colorBracketAlign, colorBracketCloser, colorBracketHold, colorBracketCapturing, cardRatio, zoomType = zoomType, cameraMode = CameraMode.Document) } } @@ -203,7 +223,6 @@ internal constructor( private var colorBracketCloser : Int = Color.RED private var colorBracketHold : Int = Color.YELLOW private var colorBracketCapturing : Int = Color.GREEN - private var useGMS: Boolean = true private val cardRatio : Float = 0.65f private var isMrzMode: Boolean = true @@ -246,7 +265,7 @@ internal constructor( @Suppress("DEPRECATION") return AcuantCameraOptions(timeInMsPerDigit, digitsToShow, allowBox, autoCapture, bracketLengthInHorizontal, bracketLengthInVertical, defaultBracketMarginWidth, defaultBracketMarginHeight, colorHold, - colorCapturing, colorBracketAlign, colorBracketCloser, colorBracketHold, colorBracketCapturing, useGMS, cardRatio, cameraMode = CameraMode.Mrz) + colorCapturing, colorBracketAlign, colorBracketCloser, colorBracketHold, colorBracketCapturing, cardRatio, cameraMode = CameraMode.Mrz) } } } \ No newline at end of file diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/barcode/AcuantBarcodeCameraFragment.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/barcode/AcuantBarcodeCameraFragment.kt index bc6b39c..3142d58 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/barcode/AcuantBarcodeCameraFragment.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/barcode/AcuantBarcodeCameraFragment.kt @@ -143,9 +143,9 @@ class AcuantBarcodeCameraFragment: AcuantBaseCameraFragment() { } } - override fun buildImageAnalyzer(screenAspectRatio: Int, rotation: Int) { - val frameAnalyzer = DocumentFrameAnalyzer { _, _, barcode, _ -> - onBarcodeDetection(barcode) + override fun buildImageAnalyzer(screenAspectRatio: Int, trueScreenRatio: Float, rotation: Int) { + val frameAnalyzer = DocumentFrameAnalyzer (trueScreenRatio) { result, _ -> + onBarcodeDetection(result.barcode) } frameAnalyzer.disableDocumentDetection() imageAnalyzer = ImageAnalysis.Builder() @@ -158,6 +158,10 @@ class AcuantBarcodeCameraFragment: AcuantBaseCameraFragment() { } } + override fun resetWorkflow() { + //This camera does not have such a workflow to reset + } + companion object { const val INTERVAL = 1000.toLong() diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/document/AcuantDocCameraFragment.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/document/AcuantDocCameraFragment.kt index d33b845..97d63b9 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/document/AcuantDocCameraFragment.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/document/AcuantDocCameraFragment.kt @@ -13,6 +13,8 @@ import androidx.camera.core.ImageAnalysis import com.acuant.acuantcamera.R import com.acuant.acuantcamera.camera.AcuantBaseCameraFragment import com.acuant.acuantcamera.camera.AcuantCameraOptions +import com.acuant.acuantcamera.constant.MINIMUM_DPI +import com.acuant.acuantcamera.constant.TARGET_DPI import com.acuant.acuantcamera.interfaces.IAcuantSavedImage import com.acuant.acuantcamera.databinding.DocumentFragmentUiBinding import com.acuant.acuantcamera.detector.DocumentFrameAnalyzer @@ -36,12 +38,15 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { private var defaultTextDrawable: Drawable? = null private var holdTextDrawable: Drawable? = null private var tapToCapture = false - private var currentDigit: Int = 2 + private var currentDigit: Int = 0 private var lastTime: Long = System.currentTimeMillis() private var greenTransparent: Int = 0 private var firstThreeTimings: Array = arrayOf(-1, -1, -1) private var hasFinishedTest = false private var oldPoints: Array? = null + private var initialDpi: Int = 0 + private var hasFoundCorrectBounds = false + private var instancesOfCorrectDpi = 0 private lateinit var frameAnalyzer: DocumentFrameAnalyzer private fun drawBorder(points: Array?) { @@ -54,14 +59,14 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { } } - private fun onDocumentDetection(points: Array?, docState: DocumentState, cropDuration: Long) { + private fun onDocumentDetection(points: Array?, ratio: Float?, analyzerDPI: Int, docState: DocumentState, cropDuration: Long) { if (!capturing && !tapToCapture) { activity?.runOnUiThread { if (!hasFinishedTest) { rectangleView?.setViewFromState(DocumentCameraState.Align) setTextFromState(DocumentCameraState.Align) - resetTimer() + resetWorkflow() for (i in firstThreeTimings.indices) { if (firstThreeTimings[i] == (-1).toLong()) { @@ -80,6 +85,7 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { if (hasFinishedTest && !tapToCapture) { var detectedPoints = points + var realDpi = 0 val camContainer = fragmentCameraBinding?.root val analyzerSize = imageAnalyzer?.resolutionInfo?.resolution @@ -87,47 +93,47 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { val state = if (detectedPoints != null && detectedPoints.size == 4) { detectedPoints = PointsUtils.fixPoints(PointsUtils.scalePoints(detectedPoints, camContainer, analyzerSize, previewSize, rectangleView)) - if (previewSize != null) { - val mult = 0.02f - val view = Rect((previewSize.left * (1 + mult)).toInt(), (previewSize.top * (1 + mult)).toInt(), (previewSize.right * (1 - mult)).toInt(), (previewSize.bottom * (1 - mult)).toInt()) - var isContained = true - detectedPoints.forEach { - if (!view.contains(it.y, it.x)) { - isContained = false - } - } - if (isContained) { - docState - } else { - DocumentState.NoDocument - } - } else { - docState - } + realDpi = PointsUtils.scaleDpi(analyzerDPI, analyzerSize, imageCapture?.resolutionInfo?.resolution) + if (previewSize != null) { + val mult = 0.02f + val view = Rect((previewSize.left * (1 + mult)).toInt(), (previewSize.top * (1 + mult)).toInt(), (previewSize.right * (1 - mult)).toInt(), (previewSize.bottom * (1 - mult)).toInt()) + var isContained = true + detectedPoints.forEach { + if (!view.contains(it.y, it.x)) { + isContained = false + } + } + if (isContained) { + docState + } else { + DocumentState.NoDocument + } + } else { + docState + } } else { docState } - when (state) { DocumentState.NoDocument -> { rectangleView?.setViewFromState(DocumentCameraState.Align) setTextFromState(DocumentCameraState.Align) - resetTimer() + resetWorkflow() } DocumentState.TooClose -> { rectangleView?.setViewFromState(DocumentCameraState.MoveBack) setTextFromState(DocumentCameraState.MoveBack) - resetTimer() + resetWorkflow() } DocumentState.TooFar -> { rectangleView?.setViewFromState(DocumentCameraState.MoveCloser) setTextFromState(DocumentCameraState.MoveCloser) - resetTimer() + resetWorkflow() } else -> { // good document - if (System.currentTimeMillis() - lastTime > (acuantOptions.digitsToShow - currentDigit + 2) * acuantOptions.timeInMsPerDigit) - --currentDigit + if (System.currentTimeMillis() - lastTime > (currentDigit + 1) * acuantOptions.timeInMsPerDigit) + ++currentDigit var dist = 0 if (oldPoints != null && oldPoints!!.size == 4 && detectedPoints != null && detectedPoints.size == 4) { @@ -144,13 +150,14 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { dist > TOO_MUCH_MOVEMENT -> { rectangleView?.setViewFromState(DocumentCameraState.HoldSteady) setTextFromState(DocumentCameraState.HoldSteady) - resetTimer() + resetWorkflow() } System.currentTimeMillis() - lastTime < acuantOptions.digitsToShow * acuantOptions.timeInMsPerDigit -> { rectangleView?.setViewFromState(DocumentCameraState.CountingDown) setTextFromState(DocumentCameraState.CountingDown) } else -> { + val middle = PointsUtils.findMiddleForCamera(points, fragmentCameraBinding?.root?.width, fragmentCameraBinding?.root?.height) captureImage(object : IAcuantSavedImage { override fun onSaved(uri: String) { cameraActivityListener.onCameraDone(uri, latestBarcode) @@ -160,7 +167,7 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { cameraActivityListener.onError(error) } - }, "AUTO") + }, middle, captureType = "AUTO") rectangleView?.setViewFromState(DocumentCameraState.Capturing) setTextFromState(DocumentCameraState.Capturing) } @@ -168,6 +175,45 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { } } + //this adjusts the too close/too far bounds based on dpi + if (!hasFoundCorrectBounds && initialDpi > MINIMUM_DPI && ratio != null) { + if (initialDpi >= TARGET_DPI) { + if (realDpi < TARGET_DPI) { + if (instancesOfCorrectDpi == 0) { + frameAnalyzer.setNewMinDist(ratio - 0.01f) //we used to be too close and have zoomed out enough + } + ++instancesOfCorrectDpi + if (instancesOfCorrectDpi >= 3) { + hasFoundCorrectBounds = true + } + } else { + instancesOfCorrectDpi = 0 + frameAnalyzer.setNewMaxDist(ratio - 0.3f) //we used to be too close and are still too close + } + } else { + if (realDpi >= TARGET_DPI) { + if (instancesOfCorrectDpi == 0) { + frameAnalyzer.setNewMinDist(ratio - 0.01f) //we used to be too far and have zoomed in enough + } + ++instancesOfCorrectDpi + if (instancesOfCorrectDpi >= 3) { + hasFoundCorrectBounds = true + } + } else { + instancesOfCorrectDpi = 0 + frameAnalyzer.setNewMinDist(ratio + 0.03f) //we used to be too far and are still too far + } + } + } + + if (initialDpi == 0 && realDpi > MINIMUM_DPI) { + initialDpi = realDpi + } else if (realDpi < MINIMUM_DPI) { //if real dpi is negligible (document left screen) reset the detected bounds state + initialDpi = 0 + hasFoundCorrectBounds = false + instancesOfCorrectDpi = 0 + } + oldPoints = detectedPoints drawBorder(detectedPoints) } @@ -210,8 +256,8 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { context?.resources?.getDimension(R.dimen.cam_doc_font_big) ?: 48f textView.text = resources.getQuantityString( R.plurals.acuant_camera_timer, - currentDigit, - currentDigit + acuantOptions.digitsToShow - currentDigit, + acuantOptions.digitsToShow - currentDigit ) textView.setTextColor(Color.RED) } @@ -230,11 +276,7 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { context?.resources?.getDimension(R.dimen.cam_info_width)?.toInt() ?: 300 textView.textSize = context?.resources?.getDimension(R.dimen.cam_doc_font_big) ?: 48f - textView.text = resources.getQuantityString( - R.plurals.acuant_camera_timer, - currentDigit, - currentDigit - ) + textView.text = getString(R.string.acuant_camera_capturing) textView.setTextColor(Color.RED) } else -> {//align @@ -254,9 +296,14 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { textView?.rotation = rotation.toFloat() } - private fun resetTimer() { + override fun resetWorkflow() { lastTime = System.currentTimeMillis() - currentDigit = acuantOptions.digitsToShow + currentDigit = 0 + if (tapToCapture) { + setTextFromState(DocumentCameraState.Align) + textView?.text = getString(R.string.acuant_camera_align_and_tap) + textView?.layoutParams?.width = context?.resources?.getDimension(R.dimen.cam_info_width)?.toInt() ?: 300 + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -278,7 +325,7 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { textView = cameraUiContainerBinding?.documentText setOptions(rectangleView) - currentDigit = acuantOptions.digitsToShow + currentDigit = 0 capturingTextDrawable = AppCompatResources.getDrawable(requireContext(), R.drawable.camera_text_config_capturing) defaultTextDrawable = AppCompatResources.getDrawable(requireContext(), R.drawable.camera_text_config_default) @@ -292,8 +339,8 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { setTextFromState(DocumentCameraState.Align) textView?.text = getString(R.string.acuant_camera_align_and_tap) textView?.layoutParams?.width = context?.resources?.getDimension(R.dimen.cam_info_width)?.toInt() ?: 300 - fragmentCameraBinding?.root?.setOnClickListener{ - activity?.runOnUiThread{ + fragmentCameraBinding?.root?.setOnClickListenerWithPoint { point -> + activity?.runOnUiThread { textView?.setBackgroundColor(greenTransparent) textView?.text = getString(R.string.acuant_camera_capturing) captureImage(object : IAcuantSavedImage { @@ -304,7 +351,7 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { override fun onError(error: AcuantError) { cameraActivityListener.onError(error) } - }, "TAP") + }, point, captureType = "TAP") } } } @@ -315,10 +362,10 @@ class AcuantDocCameraFragment: AcuantBaseCameraFragment() { } } - override fun buildImageAnalyzer(screenAspectRatio: Int, rotation: Int) { - frameAnalyzer = DocumentFrameAnalyzer { points, state, barcode, detectTime -> - onBarcodeDetection(barcode) - onDocumentDetection(points, state, detectTime) + override fun buildImageAnalyzer(screenAspectRatio: Int, trueScreenRatio: Float, rotation: Int) { + frameAnalyzer = DocumentFrameAnalyzer (trueScreenRatio) { result, detectTime -> + onBarcodeDetection(result.barcode) + onDocumentDetection(result.points, result.currentDistRatio, result.analyzerDpi, result.state, detectTime) } if (!acuantOptions.autoCapture) { setTapToCapture() diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/mrz/AcuantMrzCameraFragment.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/mrz/AcuantMrzCameraFragment.kt index a962b2e..8ee4d92 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/mrz/AcuantMrzCameraFragment.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/camera/mrz/AcuantMrzCameraFragment.kt @@ -92,17 +92,17 @@ class AcuantMrzCameraFragment: AcuantBaseCameraFragment() { } if (dist > TOO_MUCH_MOVEMENT) { - resetCapture() + resetWorkflow() } when { state == MrzState.NoMrz || !PointsUtils.correctDirection(points, previewSize) -> { - resetCapture() + resetWorkflow() setTextFromState(MrzCameraState.Align) rectangleView?.setViewFromState(MrzCameraState.Align) } state == MrzState.TooFar -> { - resetCapture() + resetWorkflow() setTextFromState(MrzCameraState.MoveCloser) rectangleView?.setViewFromState(MrzCameraState.MoveCloser) @@ -145,7 +145,7 @@ class AcuantMrzCameraFragment: AcuantBaseCameraFragment() { } } - private fun resetCapture() { + override fun resetWorkflow() { tries = 0 } @@ -201,8 +201,8 @@ class AcuantMrzCameraFragment: AcuantBaseCameraFragment() { imageView?.rotation = rotation.toFloat() } - override fun buildImageAnalyzer(screenAspectRatio: Int, rotation: Int) { - val frameAnalyzer = MrzFrameAnalyzer (WeakReference(requireContext())) { points, result, state -> + override fun buildImageAnalyzer(screenAspectRatio: Int, trueScreenRatio: Float, rotation: Int) { + val frameAnalyzer = MrzFrameAnalyzer (WeakReference(requireContext()), trueScreenRatio) { points, result, state -> onMrzDetection(points, result, state) } imageAnalyzer = ImageAnalysis.Builder() diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/constant/Constants.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/constant/Constants.kt index 7bd9d66..c3934f1 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/constant/Constants.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/constant/Constants.kt @@ -8,4 +8,5 @@ const val ACUANT_EXTRA_CAMERA_OPTIONS = "cameraOptions" const val ACUANT_EXTRA_MRZ_RESULT = "mrzResult" const val ACUANT_EXTRA_ERROR = "error" const val RESULT_ERROR = -99 -const val MINIMUM_DPI = 20 // a random low number to filter out small detections due to noise but to not filter any documents even too small ones \ No newline at end of file +const val MINIMUM_DPI = 100 // a random low number to filter out small detections due to noise but to not filter any documents even too small ones +const val TARGET_DPI = 600 \ No newline at end of file diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/detector/DocumentFrameAnalyzer.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/detector/DocumentFrameAnalyzer.kt index ddf0312..9e61705 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/detector/DocumentFrameAnalyzer.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/detector/DocumentFrameAnalyzer.kt @@ -4,7 +4,6 @@ import android.graphics.* import android.util.Size import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageProxy -import com.acuant.acuantcamera.constant.MINIMUM_DPI import com.acuant.acuantimagepreparation.helper.ImageUtils import com.acuant.acuantcamera.helper.PointsUtils import com.acuant.acuantimagepreparation.AcuantImagePreparation @@ -15,19 +14,58 @@ import com.google.mlkit.vision.barcode.common.Barcode import com.google.mlkit.vision.common.InputImage import kotlin.concurrent.thread -typealias DocumentFrameListener = (points: Array?, state: DocumentState, barcode: String?, detectTime: Long) -> Unit +typealias DocumentFrameListener = (result: DocumentFrameResult, detectTime: Long) -> Unit -enum class DocumentState { NoDocument, TooFar, TooClose, GoodDocument } - -class DocumentFrameAnalyzer internal constructor(private val listener: DocumentFrameListener) : ImageAnalysis.Analyzer { +class DocumentFrameAnalyzer internal constructor(private val trueScreenRatio: Float, private val listener: DocumentFrameListener) : ImageAnalysis.Analyzer { private var runningThreads = 0 private var disableDocDetect = false + private var minDist = DEFAULT_MIN_DIST + private var maxDist = DEFAULT_MAX_DIST + private var minDistBound = DEFAULT_MIN_DIST_BOUND + private var maxDistBound = DEFAULT_MAX_DIST_BOUND fun disableDocumentDetection() { disableDocDetect = true } + //we eventually want to set the min and max to be within the focusable distance of the camera. + // However as of alpha12 there does not seem to be a stable way of accessing these values + @Suppress("unused") + fun setNewMaxDistBound(maxBound: Float) { + maxDistBound = maxBound + capBounds() + } + + @Suppress("unused") + fun setNewMinDistBound(minBound: Float) { + minDistBound = minBound + capBounds() + } + + fun setNewMinDist(minDist: Float) { + this.maxDist = minDist + ALLOWED_RATIO_VARIANCE + this.minDist = minDist + capBounds() + } + + fun setNewMaxDist(maxDist: Float) { + this.maxDist = maxDist + this.minDist = maxDist - ALLOWED_RATIO_VARIANCE + capBounds() + } + + private fun capBounds() { + if (minDist < minDistBound) { + minDist = minDistBound + maxDist = minDistBound + ALLOWED_RATIO_VARIANCE + } + if (maxDist > maxDistBound) { + minDist = maxDistBound - ALLOWED_RATIO_VARIANCE + maxDist = maxDistBound + } + } + //this is experimental annotation is required due to the weird ownership of the internal image // essentially it is unsafe to close the image wrapped by the ImageProxy and by adding this // annotation we acknowledge that @@ -42,6 +80,9 @@ class DocumentFrameAnalyzer internal constructor(private val listener: DocumentF var state: DocumentState = DocumentState.NoDocument var points: Array? = null var barcode: String? = null + var documentType: DocumentType = DocumentType.Other + var dpi = 0 + var currentDistRatio: Float? = null val mediaImage = image.image //don't close this one runningThreads = 0 if (mediaImage != null) { @@ -52,47 +93,63 @@ class DocumentFrameAnalyzer internal constructor(private val listener: DocumentF //document detection thread { - val origSize = Size(bitmap.width, bitmap.height) + val detectAspectRatio = bitmap.width / bitmap.height.toFloat() + val origSize = if (detectAspectRatio < trueScreenRatio) { + Size(bitmap.width, (bitmap.width / trueScreenRatio).toInt()) + } else { + Size((bitmap.height / trueScreenRatio).toInt(), bitmap.height) + } val detectData = DetectData(bitmap) val detectResult = AcuantImagePreparation.detect(detectData) points = detectResult.points + if (points != null) { + documentType = if (detectResult.isPassport) DocumentType.Passport else DocumentType.Id + dpi = detectResult.dpi + currentDistRatio = PointsUtils.getLargeRatio(points!!, origSize) + } state = when { - points == null || detectResult.dpi < MINIMUM_DPI || !detectResult.isCorrectAspectRatio -> DocumentState.NoDocument - PointsUtils.isTooClose(points, origSize, MAX_DIST) -> DocumentState.TooClose - !PointsUtils.isCloseEnough(points, origSize, MIN_DIST) -> DocumentState.TooFar + points == null || !detectResult.isCorrectAspectRatio -> { + documentType = DocumentType.Other + DocumentState.NoDocument + } + !PointsUtils.isNotTooClose(points, origSize, maxDist) -> DocumentState.TooClose + !PointsUtils.isCloseEnough(points, origSize, minDist) -> DocumentState.TooFar else -> DocumentState.GoodDocument } - finishThread(points, state, barcode, startTime) + finishThread(points, currentDistRatio, documentType, dpi, state, barcode, startTime) } } //barcode detection - val barcodeInput = - InputImage.fromMediaImage(mediaImage, image.imageInfo.rotationDegrees) + val barcodeInput = InputImage.fromMediaImage(mediaImage, image.imageInfo.rotationDegrees) barcodeScanner.process(barcodeInput).addOnSuccessListener { barcodes -> if (barcodes.isNotEmpty()) { barcode = barcodes[0].rawValue } }.addOnCompleteListener { //finally - finishThread(points, state, barcode, startTime) + finishThread(points, currentDistRatio, documentType, dpi, state, barcode, startTime) image.close() } } else { - finishThread(points, state, barcode, startTime) + finishThread(points, currentDistRatio, documentType, dpi, state, barcode, startTime) image.close() } } - private fun finishThread(points: Array?, state: DocumentState, barcode: String?, startTime: Long) { + private fun finishThread(points: Array?, currentDistRatio: Float?, documentType: DocumentType, dpi: Int, state: DocumentState, barcode: String?, startTime: Long) { --runningThreads if (runningThreads <= 0) { - listener(points, state, barcode, System.currentTimeMillis() - startTime) + val result = DocumentFrameResult(points, currentDistRatio, documentType, dpi, state, barcode) + listener(result, System.currentTimeMillis() - startTime) } } companion object { - private const val MIN_DIST = 0.75f - private const val MAX_DIST = 0.9f + private const val ALLOWED_RATIO_VARIANCE = 0.09f + private const val DEFAULT_MIN_DIST = 0.71f + private const val DEFAULT_MAX_DIST = DEFAULT_MIN_DIST + ALLOWED_RATIO_VARIANCE + private const val DEFAULT_MIN_DIST_BOUND = 0.51f + private const val DEFAULT_MAX_DIST_BOUND = 0.91f private val barcodeScanner = BarcodeScanning.getClient( BarcodeScannerOptions.Builder() diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/detector/DocumentFrameResult.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/detector/DocumentFrameResult.kt new file mode 100644 index 0000000..b6d8b93 --- /dev/null +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/detector/DocumentFrameResult.kt @@ -0,0 +1,14 @@ +package com.acuant.acuantcamera.detector + +import android.graphics.Point + +enum class DocumentType {Id, Passport, Other} + +enum class DocumentState { NoDocument, TooFar, TooClose, GoodDocument } + +class DocumentFrameResult (val points: Array?, + val currentDistRatio: Float?, + val documentType: DocumentType, + val analyzerDpi: Int, + val state: DocumentState, + val barcode: String?) \ No newline at end of file diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/detector/MrzFrameAnalyzer.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/detector/MrzFrameAnalyzer.kt index fd961ae..77b8943 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/detector/MrzFrameAnalyzer.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/detector/MrzFrameAnalyzer.kt @@ -19,7 +19,7 @@ typealias MrzFrameListener = (points: Array?, result: MrzResult?, state: enum class MrzState { NoMrz, TooFar, GoodMrz } -class MrzFrameAnalyzer internal constructor(contextWeak: WeakReference, private val listener: MrzFrameListener) : ImageAnalysis.Analyzer { +class MrzFrameAnalyzer internal constructor(contextWeak: WeakReference, private val trueScreenRatio: Float, private val listener: MrzFrameListener) : ImageAnalysis.Analyzer { private var running = false private var tryFlip = false private val textRecognizer = TessBaseAPI() @@ -58,7 +58,12 @@ class MrzFrameAnalyzer internal constructor(contextWeak: WeakReference, //document detection thread { - val origSize = Size(bitmap.width, bitmap.height) + val detectAspectRatio = bitmap.width / bitmap.height.toFloat() + val origSize = if (detectAspectRatio < trueScreenRatio) { + Size(bitmap.width, (bitmap.width / trueScreenRatio).toInt()) + } else { + Size((bitmap.height / trueScreenRatio).toInt(), bitmap.height) + } val detectData = DetectData(bitmap) val detectResult = AcuantImagePreparation.detectMrz(detectData) points = detectResult.points diff --git a/acuantcamera/src/main/java/com/acuant/acuantcamera/helper/PointsUtils.kt b/acuantcamera/src/main/java/com/acuant/acuantcamera/helper/PointsUtils.kt index 3992ec7..7fd7cda 100644 --- a/acuantcamera/src/main/java/com/acuant/acuantcamera/helper/PointsUtils.kt +++ b/acuantcamera/src/main/java/com/acuant/acuantcamera/helper/PointsUtils.kt @@ -1,14 +1,21 @@ package com.acuant.acuantcamera.helper import android.graphics.Point -import android.util.Log import android.util.Size import android.view.ViewGroup +import com.acuant.acuantcamera.constant.TARGET_DPI import com.acuant.acuantcamera.overlay.BaseRectangleView import kotlin.math.* object PointsUtils { + internal fun findMiddleForCamera(points: Array?, width: Int?, height: Int?): Point { + //the coordinate system in the camera is inverted hence the subtraction from width and height + if (points == null || points.size != 4 || width == null || height == null) + return Point() + return Point(width - intArrayOf(points[0].y, points[1].y, points[2].y, points[3].y).average().toInt(), height - intArrayOf(points[0].x, points[1].x, points[2].x, points[3].x).average().toInt()) + } + internal fun isAligned(points: Array?) : Boolean { if (points == null || points.size != 4) return false @@ -17,40 +24,37 @@ object PointsUtils { return abs(val1 - val2) < 15 } - internal fun isTooClose(points: Array?, screenSize: Size, maxDist: Float): Boolean { + internal fun isNotTooClose(points: Array?, screenSize: Size, maxDist: Float): Boolean { if (points != null) { - val shortSide = min( - distance(points[0], points[1]), - distance(points[0], points[3]) - ) - val largeSide = max( - distance(points[0], points[1]), - distance(points[0], points[3]) - ) - val screenShortSide = min(screenSize.width, screenSize.height).toFloat() - val screenLargeSide = max(screenSize.width, screenSize.height).toFloat() - - if (shortSide > maxDist * screenShortSide || largeSide > maxDist * screenLargeSide) { - return true - } + val ratio = getLargeRatio(points, screenSize) + return ratio < maxDist } return false } internal fun isCloseEnough(points: Array?, screenSize: Size, minDist: Float): Boolean { if (points != null) { - val shortSide = min(distance(points[0], points[1]), distance(points[0], points[3])) - val largeSide = max(distance(points[0], points[1]), distance(points[0], points[3])) - val screenShortSide = min(screenSize.width, screenSize.height).toFloat() - val screenLargeSide = max(screenSize.width, screenSize.height).toFloat() - - if (shortSide > minDist * screenShortSide || largeSide > minDist * screenLargeSide) { - return true - } + val ratio = getLargeRatio(points, screenSize) + return ratio > minDist } return false } + internal fun scaleDpi(dpi: Int, analyzerSize: Size?, captureSize: Size?): Int { + if (analyzerSize == null || captureSize == null) + return dpi + return min((dpi * max(captureSize.width, captureSize.height).toFloat() / max(analyzerSize.width, analyzerSize.height)).toInt(), TARGET_DPI + 1) + } + + internal fun getLargeRatio(points: Array, screenSize: Size): Float { + val shortSide = min(distance(points[0], points[1]), distance(points[0], points[3])) + val largeSide = max(distance(points[0], points[1]), distance(points[0], points[3])) + val screenShortSide = min(screenSize.width, screenSize.height).toFloat() + val screenLargeSide = max(screenSize.width, screenSize.height).toFloat() + + return max(shortSide / screenShortSide, largeSide / screenLargeSide) + } + internal fun distance(pointA: Point, pointB: Point): Float { return sqrt( (pointA.x - pointB.x).toFloat().pow(2) + (pointA.y - pointB.y).toFloat().pow(2)) } @@ -87,15 +91,17 @@ object PointsUtils { internal fun scalePoints(points: Array, camContainer: ViewGroup?, analyzerSize: Size?, previewSize: ViewGroup?, rectangleView: BaseRectangleView?) : Array { if (camContainer != null && previewSize != null && analyzerSize != null) { - val offset = ((camContainer.height - previewSize.height) / 2 ) - val scaledPointY: Float = previewSize.height.toFloat() / analyzerSize.width.toFloat() - val scaledPointX: Float = previewSize.width.toFloat() / analyzerSize.height.toFloat() + val scale: Float = previewSize.height.toFloat() / analyzerSize.width.toFloat() + val yOffset: Int = ((previewSize.width.toFloat() - analyzerSize.height * scale) / 2).toInt() + val xOffset: Int = ((camContainer.height - previewSize.height) / 2) + rectangleView?.setWidth(camContainer.width.toFloat()) points.onEach { - it.x = (it.x * scaledPointX).toInt() - it.y = (it.y * scaledPointY).toInt() - it.x += offset + it.x = (it.x * scale).toInt() + it.y = (it.y * scale).toInt() + it.x += xOffset + it.y += yOffset } } diff --git a/acuantcamera/src/main/res/layout/fragment_camera.xml b/acuantcamera/src/main/res/layout/fragment_camera.xml index 0d8e4c2..d362059 100644 --- a/acuantcamera/src/main/res/layout/fragment_camera.xml +++ b/acuantcamera/src/main/res/layout/fragment_camera.xml @@ -9,11 +9,12 @@ + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintDimensionRatio="11:16" /> diff --git a/acuantfacecapture/build.gradle b/acuantfacecapture/build.gradle index af269d8..17acc7a 100644 --- a/acuantfacecapture/build.gradle +++ b/acuantfacecapture/build.gradle @@ -27,30 +27,29 @@ android { viewBinding true } } - dependencies { // Kotlin lang implementation 'androidx.core:core-ktx:1.7.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' // App compat and UI things - implementation 'androidx.appcompat:appcompat:1.4.0' + implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.2' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' //noinspection GradleDependency implementation 'androidx.window:window:1.0.0-alpha09' //newer versions changed the api, not updating yet // CameraX library - def camerax_version = '1.1.0-alpha12' + def camerax_version = '1.1.0-beta01' implementation "androidx.camera:camera-core:$camerax_version" implementation "androidx.camera:camera-camera2:$camerax_version" implementation "androidx.camera:camera-lifecycle:$camerax_version" - implementation "androidx.camera:camera-view:1.0.0-alpha32" + implementation "androidx.camera:camera-view:$camerax_version" //acuant specific stuff - implementation 'com.google.mlkit:face-detection:16.1.3' - implementation 'com.acuant:acuantcommon:11.5.0' - implementation 'com.acuant:acuantimagepreparation:11.5.0' + implementation 'com.google.mlkit:face-detection:16.1.4' + implementation 'com.acuant:acuantcommon:11.5.1' + implementation 'com.acuant:acuantimagepreparation:11.5.1' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' diff --git a/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/camera/AcuantBaseFaceCameraFragment.kt b/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/camera/AcuantBaseFaceCameraFragment.kt index f27932d..a53ff2a 100644 --- a/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/camera/AcuantBaseFaceCameraFragment.kt +++ b/acuantfacecapture/src/main/java/com/acuant/acuantfacecapture/camera/AcuantBaseFaceCameraFragment.kt @@ -74,10 +74,11 @@ abstract class AcuantBaseFaceCameraFragment: Fragment() { override fun onDestroyView() { fragmentCameraBinding = null - super.onDestroyView() - // Shut down our background executor + imageAnalyzer?.clearAnalyzer() cameraExecutor.shutdown() + cameraProvider?.unbindAll() + super.onDestroyView() } override fun onCreateView( diff --git a/app/build.gradle b/app/build.gradle index a189a61..12e8889 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,7 +4,6 @@ apply plugin: 'kotlin-android' repositories { maven { url 'https://raw.githubusercontent.com/iProov/android/master/maven/' } } - dependencies { implementation "androidx.core:core-ktx:1.7.0" implementation 'androidx.appcompat:appcompat:1.4.0' @@ -20,19 +19,18 @@ dependencies { //If you do not plan to modify the camera code you can use these two regular maven dependencies // in place of the project ones. -// implementation 'com.acuant:acuantfacecapture:11.5.0' -// implementation 'com.acuant:acuantcamera:11.5.0' +// implementation 'com.acuant:acuantfacecapture:11.5.1' +// implementation 'com.acuant:acuantcamera:11.5.1' implementation project(path: ':acuantcamera') implementation project(path: ':acuantfacecapture') - implementation 'com.acuant:acuantcommon:11.5.0' - implementation 'com.acuant:acuantimagepreparation:11.5.0' - implementation 'com.acuant:acuantdocumentprocessing:11.5.0' - implementation 'com.acuant:acuantechipreader:11.5.0' - implementation 'com.acuant:acuantfacematch:11.5.0' - implementation 'com.acuant:acuantipliveness:11.5.0' - implementation 'com.acuant:acuantpassiveliveness:11.5.0' + implementation 'com.acuant:acuantcommon:11.5.1' + implementation 'com.acuant:acuantimagepreparation:11.5.1' + implementation 'com.acuant:acuantdocumentprocessing:11.5.1' + implementation 'com.acuant:acuantechipreader:11.5.1' + implementation 'com.acuant:acuantfacematch:11.5.1' + implementation 'com.acuant:acuantipliveness:11.5.1' + implementation 'com.acuant:acuantpassiveliveness:11.5.1' } - android { compileSdkVersion 31 buildToolsVersion '31' @@ -86,4 +84,4 @@ android { lintOptions { abortOnError false } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/acuant/sampleapp/MainActivity.kt b/app/src/main/java/com/acuant/sampleapp/MainActivity.kt index 89bdc99..e575bc5 100644 --- a/app/src/main/java/com/acuant/sampleapp/MainActivity.kt +++ b/app/src/main/java/com/acuant/sampleapp/MainActivity.kt @@ -24,6 +24,7 @@ import com.acuant.acuantcamera.camera.AcuantCameraOptions import com.acuant.acuantcamera.constant.* import com.acuant.acuantcamera.helper.MrzResult import com.acuant.acuantcamera.initializer.MrzCameraInitializer +import com.acuant.acuantcommon.background.AcuantAsync import com.acuant.acuantcommon.exception.AcuantException import com.acuant.acuantcommon.initializer.AcuantInitializer import com.acuant.acuantcommon.initializer.IAcuantPackageCallback @@ -52,7 +53,6 @@ import com.acuant.acuantipliveness.AcuantIPLiveness import com.acuant.acuantipliveness.IPLivenessListener import com.acuant.acuantipliveness.facialcapture.model.FacialCaptureResult import com.acuant.acuantipliveness.facialcapture.model.FacialSetupResult -import com.acuant.acuantipliveness.facialcapture.service.FacialCaptureCredentialListener import com.acuant.acuantipliveness.facialcapture.service.FacialCaptureListener import com.acuant.acuantipliveness.facialcapture.service.FacialSetupListener import com.acuant.acuantpassiveliveness.AcuantPassiveLiveness @@ -70,14 +70,9 @@ import java.util.* import kotlin.collections.HashMap import kotlin.concurrent.thread - class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { private var progressDialog: LinearLayout? = null private var progressText: TextView? = null - private var capturedFrontImage: AcuantImage? = null - private var capturedBackImage: AcuantImage? = null - private var capturedSelfieImage: Bitmap? = null - private var capturedFaceImage: Bitmap? = null private var capturedBarcodeString: String? = null private var frontCaptured: Boolean = false private var isHealthCard: Boolean = false @@ -94,20 +89,48 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { private var livenessSelected = 0 private var isKeyless = false private var processingFacialLiveness = false - private val useTokenInit = true - private var recentImage: AcuantImage? = null private var usingPassive = true + private var recentImage: AcuantImage? = null + private val useTokenInit = true + private val backgroundTasks = mutableListOf() private lateinit var livenessSpinner : Spinner private lateinit var livenessArrayAdapter: ArrayAdapter + private var capturedFrontImage: AcuantImage? = null + set (value) { + if (capturedFrontImage != null) { + capturedFrontImage?.destroy() + } + field = value + } + private var capturedBackImage: AcuantImage? = null + set (value) { + if (capturedBackImage != null) { + capturedBackImage?.destroy() + } + field = value + } + private var capturedSelfieImage: Bitmap? = null + set (value) { + if (capturedSelfieImage != null) { + capturedSelfieImage?.recycle() + } + field = value + } + private var capturedDocumentFaceImage: Bitmap? = null + set (value) { + if (capturedDocumentFaceImage != null) { + capturedDocumentFaceImage?.recycle() + } + field = value + } + fun cleanUpTransaction() { - capturedFrontImage?.destroy() - capturedBackImage?.destroy() facialResultString = null capturedFrontImage = null capturedBackImage = null capturedSelfieImage = null - capturedFaceImage = null + capturedDocumentFaceImage = null facialLivelinessResultString = null capturedBarcodeString = null isHealthCard = false @@ -118,6 +141,22 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { documentHealthInstance = null } + override fun onDestroy() { + stopBackgroundTasks() + super.onDestroy() + } + + //Ideally you should manually cancel all background tasks when your application gets + // destroyed/stopped. This is neater/good practice, but likely won't have an impact if + // you don't do this. + private fun stopBackgroundTasks() { + backgroundTasks.forEach { + it.cancel() + } + backgroundTasks.clear() + setProgress(false) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) @@ -151,8 +190,8 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { }) } - private fun initializeAcuantSdk(callback:IAcuantPackageCallback){ - try{ + private fun initializeAcuantSdk(callback: IAcuantPackageCallback) { + try { // Or, if required to initialize without a config file , then can be done the following way /*Credential.init("**username**", "**password**", @@ -195,7 +234,18 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener { findViewById