Image segmentation is a widely used term in image processing and computer vision world. It is the process of partitioning a digital image into multiple segments. The expected output from an image that we applied image segmentation on is labeling of each pixel into subgroups that we defined. By applying image segmentation, we get a more meaningful image, we get an image that each pixel of which is represented by categories such as human body, sky, plant, food etc.
Image segmentation can be used in many different scenarios. It can be used in photography apps to change the background or apply some effect only on plants or human body etc. It can also be used to determine cancerous cells in a microscope image or get land usage information in a satellite image or to determine the amount of herbicides that needed to be sprayed in a field according to crop density.
Huawei ML Kit’s Image Segmentation service segments same elements (such as human body, plant, and sky) from an image. The elements supported include human body, sky, plant, food, cat, dog, flower, water, sand, building, mountain, and others. By the way, Huawei ML Kit works on all Android phones with ARM architecture and as it is a device-side capability it is free.
ML Kit Image Segmentation allows developers two types of segmentation: human body and multiclass segmentation. We can apply image segmentation on static images and video streams if we select human body type. But we can only apply segmentation for static images in multiclass segmentation.
In multiclass image segmentation the return value of the process is the coordinate array of each element. For instance, if an image consists of human body, sky, plant, and cat, the return value is the coordinate array of the four elements. After this point our app can apply different effects on elements. For example, we can shange the blue sky with a red one.
The return values of human body segmentation include the coordinate array of the human body, human body image with a transparent background, and gray-scale image with a white human body and black background. Our app can further process the elements based on the return values, such as replacing the video background and cutting out the human body from an image.
Today, we are going to build Background Eraser app in Kotlin, on Android Studio. We are going to use human body segmentation function of ML Kit. In this example we are going to learn how to integrate HMS ML Kit into our project first and then we will see how to apply segmentation on images simply.
In the application we will have three imageViews. One for background image, one for selected image (supposed to contain human body) and one for processed image. We will distract human body/ bodies in the selected image and change its background with our selected background. It will be really simple, just to see how to apply this function in an application. I won’t bother you much with details. After we develop this simple application, you can go ahead and add various capabilities to your app.
Let’s start building our demo application step by step from scratch!
1. Firstly, let’s create our project on Android Studio. I named my project as Background Eraser. It’s totally up to you. We can create a project by selecting Empty Activity option and then follow the steps described in this page to create and sign our project in App Gallery Connect.
2. Secondly, In HUAWEI Developer AppGallery Connect, go to Develop > Manage APIs. Make sure ML Kit is activated.
3. Now we have integrated Huawei Mobile Services (HMS) into our project. Now let’s follow the documentation on developer.huawei.com and find the packages to add to our project. In the website click Developer / HMS Core/ AI / ML Kit. Here you will find introductory information to services, references, SDKs to download and others. Under ML Kit tab follow Android / Getting Started / Integrating HMS Core SDK / Adding Build Dependencies / Integrating the Image Segmentation SDK. We can follow the guide here to add image segmentation capability to our project. As we are not going to use multiclass segmentation in this project we only add base SDK and human body segmentation package. We have also one meta-data tag to be added into our AndroidManifest.xml file. After the integration your app-level build.gradle file will look like this.
Code:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin:'com.huawei.agconnect'
android {
compileSdkVersion 30
buildToolsVersion "30.0.1"
defaultConfig {
applicationId "com.demo.backgrounderaser"
minSdkVersion 23
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
//AGC Core
implementation'com.huawei.agconnect:agconnect-core:1.3.1.300'
//Image Segmentation Base SDK
implementation 'com.huawei.hms:ml-computer-vision-segmentation:2.0.2.300'
//Image Segmentation Human Body Model
implementation 'com.huawei.hms:ml-computer-vision-image-segmentation-body-model:2.0.2.300'
}
And your project-level build.gradle file will look like this.
Code:
buildscript {
ext.kotlin_version = "1.4.0"
repositories {
google()
jcenter()
maven {url 'http://developer.huawei.com/repo/'}
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.huawei.agconnect:agcp:1.3.1.300'
}
}
allprojects {
repositories {
google()
jcenter()
maven {url 'http://developer.huawei.com/repo/'}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Don’t forget to add the following meta-data tags in your AndroidManifest.xml. This is for automatic update of the machine learning model.
Code:
<manifest … >
<application
… >
<meta-data
android:name="com.huawei.hms.ml.DEPENDENCY"
android:value= "imgseg"/>
</application>
</manifest>
4. We can select applying human body segmentation on static images or video streams. In this project we are going to see how to do this on static images. To apply segmentation on images firstly we need to create our analyzer. setExact() method is to determine if we apply fine segmentation or not. I chose true here. As analyzer type we chose BODY_SEG here. The second option being IMAGE_SEG for multiclass segmentation. setScene() method is for determining result type. Here we chose FOREGROUND_ONLY, meaning a human body image with a transparent background and an original image for segmentation will be returned. Here is how to create it.
Code:
private lateinit var mAnalyzer: MLImageSegmentationAnalyzer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
createAnalyzer()
}
private fun createAnalyzer(): MLImageSegmentationAnalyzer {
val analyzerSetting = MLImageSegmentationSetting.Factory()
.setExact(true)
.setAnalyzerType(MLImageSegmentationSetting.BODY_SEG)
.setScene(MLImageSegmentationScene.FOREGROUND_ONLY)
.create()
return MLAnalyzerFactory.getInstance().getImageSegmentationAnalyzer(analyzerSetting).also {
mAnalyzer = it
}
}
5. Create a simple layout. It should contain three imageView and two butons. One imageView for background image, one imageView for image with human body to apply segmentation, one imageView for displaying processed image. One button for selecting background image and the other button for selecting image to be processed. Here is my example, you can reach the same result with a different design, too.
Code:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guidelineTopHorizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintGuide_percent="0.3"
android:orientation="horizontal" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guidelineBottomHorizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintGuide_percent="0.9"
android:orientation="horizontal" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guidelineCenterVertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintGuide_percent="0.5"
android:orientation="vertical" />
<ImageView
android:id="@+id/ivBackgroundFill"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="@dimen/margin_m"
android:contentDescription="@string/image_to_fill_background"
app:layout_constraintBottom_toBottomOf="@id/guidelineTopHorizontal"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="@id/guidelineCenterVertical"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/ivSelectedBitmap"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="@dimen/margin_m"
android:contentDescription="@string/image_to_fill_background"
app:layout_constraintBottom_toBottomOf="@id/guidelineTopHorizontal"
app:layout_constraintStart_toStartOf="@id/guidelineCenterVertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/ivProcessedBitmap"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="@dimen/margin_m"
android:contentDescription="@string/image_to_fill_background"
app:layout_constraintBottom_toBottomOf="@id/guidelineBottomHorizontal"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/guidelineTopHorizontal" />
<Button
android:id="@+id/buttonSelectBackground"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/select_background"
android:contentDescription="@string/image_to_fill_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="@id/guidelineCenterVertical"
app:layout_constraintTop_toTopOf="@id/guidelineBottomHorizontal" />
<Button
android:id="@+id/buttonPickImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pick_image"
android:contentDescription="@string/image_to_fill_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/guidelineCenterVertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/guidelineBottomHorizontal" />
</androidx.constraintlayout.widget.ConstraintLayout>
6. In order to get our background image and source image we can use intents. OnClickListeners of our buttons will help us get images via intents from device storage as shown below. In onActivityResult we will appoint bitmaps we get to corresponding variables: mBackgroundFill and mSelectedBitmap. Here WRITE_EXTERNAL_STORAGE permission is requested to automatically update Huawei ML Kit’s image segmentation model.
Code:
class MainActivity : AppCompatActivity() {
companion object {
private const val IMAGE_REQUEST_CODE = 58
private const val BACKGROUND_REQUEST_CODE = 32
}
private lateinit var mAnalyzer: MLImageSegmentationAnalyzer
private var mBackgroundFill: Bitmap? = null
private var mSelectedBitmap: Bitmap? = null
private var mProcessedBitmap: Bitmap? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if(ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)
init()
else
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 0)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 0 && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)
init()
}
private fun init() {
initView()
createAnalyzer()
}
private fun initView() {
buttonPickImage.setOnClickListener { getImage(IMAGE_REQUEST_CODE) }
buttonSelectBackground.setOnClickListener { getImage(BACKGROUND_REQUEST_CODE) }
}
private fun getImage(requestCode: Int) {
Intent(Intent.ACTION_GET_CONTENT).also {
it.type = "image/*"
startActivityForResult(it, requestCode)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
data?.data?.also {
when(requestCode) {
IMAGE_REQUEST_CODE -> {
mSelectedBitmap = MediaStore.Images.Media.getBitmap(contentResolver, it)
if (mSelectedBitmap != null) {
ivSelectedBitmap.setImageBitmap(mSelectedBitmap)
}
}
BACKGROUND_REQUEST_CODE -> {
mBackgroundFill = MediaStore.Images.Media.getBitmap(contentResolver, it)
if (mBackgroundFill != null) {
ivBackgroundFill.setImageBitmap(mBackgroundFill)
}
}
}
}
}
}
}
Code:
7. Now let’s create our method for analyzing images. It is very simple. It takes a bitmap as parameter, creates an MLFrame object out of it and asynchronously analyzes this MLFrame. It has OnSuccess and OnFailure callbacks. We will try to add the selected background if the analyze result is successful. Please remember that we chose our analyzer’s result type as FOREGROUND_ONLY. So, expect it to return us the original image together with human body image with transparent background.
Code:
private fun analyse(bitmap: Bitmap) {
val mlFrame = MLFrame.fromBitmap(bitmap)
mAnalyzer.asyncAnalyseFrame(mlFrame)
.addOnSuccessListener {
addSelectedBackground(it)
}
.addOnFailureListener {
Log.e(TAG, "analyse -> asyncAnalyseFrame: ", it)
}
}
8. To add human body part onto our background image, we should make sure we have a background image first, then we should obtain a mutable bitmap out of our background to work upon, then we should resize this mutable bitmap according to our selected bitmap to make our image more realistic, then we should create our canvas from our mutable bitmap, then we draw human body part on our canvas and lastly we can use our processed image. Here is our addSelectedBackground() method.
Code:
private fun addSelectedBackground(mlImageSegmentation: MLImageSegmentation) {
if (mBackgroundFill == null) {
Toast.makeText(applicationContext, "Please select a background image!", Toast.LENGTH_SHORT).show()
} else {
var mutableBitmap = if (mBackgroundFill!!.isMutable) {
mBackgroundFill
} else {
mBackgroundFill!!.copy(Bitmap.Config.ARGB_8888, true)
}
if (mutableBitmap != null) {
/*
* If background image size is different than our selected image,
* we change our background image's size according to selected image.
*/
if (mutableBitmap.width != mlImageSegmentation.original.width ||
mutableBitmap.height != mlImageSegmentation.original.height) {
mutableBitmap = Bitmap.createScaledBitmap(
mutableBitmap,
mlImageSegmentation.original.width,
mlImageSegmentation.original.height,
false)
}
val canvas = mutableBitmap?.let { Canvas(it) }
canvas?.drawBitmap(mlImageSegmentation.foreground, 0F, 0F, null)
mProcessedBitmap = mutableBitmap
ivProcessedBitmap.setImageBitmap(mProcessedBitmap)
}
}
}
9. Here is the whole of MainActivity. You can find this project on Github, too.
Code:
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "BE_MainActivity"
private const val IMAGE_REQUEST_CODE = 58
private const val BACKGROUND_REQUEST_CODE = 32
}
private lateinit var mAnalyzer: MLImageSegmentationAnalyzer
private var mBackgroundFill: Bitmap? = null
private var mSelectedBitmap: Bitmap? = null
private var mProcessedBitmap: Bitmap? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if(ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)
init()
else
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 0)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 0 && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)
init()
}
private fun init() {
initView()
createAnalyzer()
}
private fun initView() {
buttonPickImage.setOnClickListener { getImage(IMAGE_REQUEST_CODE) }
buttonSelectBackground.setOnClickListener { getImage(BACKGROUND_REQUEST_CODE) }
}
private fun createAnalyzer(): MLImageSegmentationAnalyzer {
val analyzerSetting = MLImageSegmentationSetting.Factory()
.setExact(true)
.setAnalyzerType(MLImageSegmentationSetting.BODY_SEG)
.setScene(MLImageSegmentationScene.FOREGROUND_ONLY)
.create()
return MLAnalyzerFactory.getInstance().getImageSegmentationAnalyzer(analyzerSetting).also {
mAnalyzer = it
}
}
private fun analyse(bitmap: Bitmap) {
val mlFrame = MLFrame.fromBitmap(bitmap)
mAnalyzer.asyncAnalyseFrame(mlFrame)
.addOnSuccessListener {
addSelectedBackground(it)
}
.addOnFailureListener {
Log.e(TAG, "analyse -> asyncAnalyseFrame: ", it)
}
}
private fun addSelectedBackground(mlImageSegmentation: MLImageSegmentation) {
if (mBackgroundFill == null) {
Toast.makeText(applicationContext, "Please select a background image!", Toast.LENGTH_SHORT).show()
} else {
var mutableBitmap = if (mBackgroundFill!!.isMutable) {
mBackgroundFill
} else {
mBackgroundFill!!.copy(Bitmap.Config.ARGB_8888, true)
}
if (mutableBitmap != null) {
/*
* If background image size is different than our selected image,
* we change our background image's size according to selected image.
*/
if (mutableBitmap.width != mlImageSegmentation.original.width ||
mutableBitmap.height != mlImageSegmentation.original.height) {
mutableBitmap = Bitmap.createScaledBitmap(
mutableBitmap,
mlImageSegmentation.original.width,
mlImageSegmentation.original.height,
false)
}
val canvas = mutableBitmap?.let { Canvas(it) }
canvas?.drawBitmap(mlImageSegmentation.foreground, 0F, 0F, null)
mProcessedBitmap = mutableBitmap
ivProcessedBitmap.setImageBitmap(mProcessedBitmap)
}
}
}
private fun getImage(requestCode: Int) {
Intent(Intent.ACTION_GET_CONTENT).also {
it.type = "image/*"
startActivityForResult(it, requestCode)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
data?.data?.also {
when(requestCode) {
IMAGE_REQUEST_CODE -> {
mSelectedBitmap = MediaStore.Images.Media.getBitmap(contentResolver, it)
if (mSelectedBitmap != null) {
ivSelectedBitmap.setImageBitmap(mSelectedBitmap)
analyse(mSelectedBitmap!!)
}
}
BACKGROUND_REQUEST_CODE -> {
mBackgroundFill = MediaStore.Images.Media.getBitmap(contentResolver, it)
if (mBackgroundFill != null) {
ivBackgroundFill.setImageBitmap(mBackgroundFill)
mSelectedBitmap?.let { analyse(it) }
}
}
}
}
}
}
}
10. Well done! We have finished all the steps and created our project.
11. We have created a simple Background Eraser app. We can get human body related pixels out of our image and apply different backgrounds to it. This project is to show you the basics. You can go further, create much better projects and come up with various ideas to apply image segmentation to. I hope you enjoyed this article and created your project easily. If you have any questions please ask.
Related
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Hi all,
In the era of powerful mobile devices we store thousands of photos, have video calls, shop, manage our bank accounts and perform many other tasks in the palm of our hands. We can also manage to take photos and videos or have video calls with our cameras integrated on our mobile phones. But, it would be inefficient to only use these cameras we carry by ourselves all day to take raw photos and videos. They can perform more, much more.
Face detection services are used by many applications in different industries. It is mainly used for security and entertainment purposes. For example, it can be used by a taxi app to identify its taxi’s customer, it can be used by a smart home app to identify guests’ faces and announce to the host who the person ringing the bell at the door is, it can be used to draw moustache on faces in an entertainment app or it can be used to detect if a drivers eyes are open or not and warn our driver if his/her eyes closed.
In contrary to how many different areas face detection can be used and how important tasks this service performs, it is really easy to implement a face detection app with the help of Huawei ML Kit. As it’s a device side capability that works on all Android devices with ARM architecture, it is completely free, faster and more secure than other services. The face detection service can detect the shapes and features of your user’s face, including their facial expression, age, gender, and wearing.
With face detection service you can detect up to 855 face contour points to locate face coordinates including face contour, eyebrows, eyes, nose, mouth, and ears, and identify the pitch, yaw, and roll angles of a face. You can detect seven facial features including the possibility of opening the left eye, possibility of opening the right eye, possibility of wearing glasses, gender possibility, possibility of wearing a hat, possibility of wearing a beard, and age. In addition to there, you can also detect facial expressions, namely, smiling, neutral, anger, disgust, fear, sadness, and surprise.
Let’s start to build our demo application step by step from scratch!
1.Firstly, let’s create our project on Android Studio. We can create a project selecting Empty Activity option and then follow the steps described in this post to create and sign our project in App Gallery Connect. You can follow this guide.
2. Secondly, In HUAWEI Developer AppGallery Connect, go to Develop > Manage APIs. Make sure ML Kit is activated.
3. Now we have integrated Huawei Mobile Services (HMS) into our project. Now let’s follow the documentation on developer.huawei.com and find the packages to add to our project. In the website click Developer > HMS Core > AI > ML Kit. Here you will find introductory information to services, references, SDKs to download and others. Under ML Kit tab follow Android > Getting Started > Integrating HMS Core SDK > Adding Build Dependencies > Integrating the Face Detection SDK. We can follow the guide here to add face detection capability to our project. To later go round and learn more about this service I added all of the three packages shown here. You can only choose the base SDK or select packages according to your needs. After the integration your app-level build.gradle file will look like this.
Code:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.huawei.agconnect'
android {
compileSdkVersion 29
buildToolsVersion "30.0.1"
defaultConfig {
applicationId "com.demo.faceapp"
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
// Import the base SDK.
implementation 'com.huawei.hms:ml-computer-vision-face:2.0.1.300'
// Import the contour and key point detection model package.
implementation 'com.huawei.hms:ml-computer-vision-face-shape-point-model:2.0.1.300'
// Import the facial expression detection model package.
implementation 'com.huawei.hms:ml-computer-vision-face-emotion-model:2.0.1.300'
// Import the facial feature detection model package.
implementation 'com.huawei.hms:ml-computer-vision-face-feature-model:2.0.1.300'
}
And your project-level build.gradle file will look like this.
Code:
buildscript {
ext.kotlin_version = "1.3.72"
repositories {
google()
jcenter()
maven { url 'https://developer.huawei.com/repo/' }
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.huawei.agconnect:agcp:1.3.1.300'
}
}
allprojects {
repositories {
google()
jcenter()
maven { url 'https://developer.huawei.com/repo/' }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Don’t forget to add the following meta-data tags in your AndroidManifest.xml. This is for automatic update of the machine learning model.
Code:
<?xml version="1.0" encoding="utf-8"?>
<manifest ...
<application ...
</application>
<meta-data
android:name="com.huawei.hms.ml.DEPENDENCY"
android:value= "face"/>
</manifest>
4. Now we can select detecting faces on a static image or on a camera stream. Let’s choose detecting faces on a camera stream for this example. Firstly, let’s create our analyzer. Its type is MLFaceAnalyzer. It is responsible for analyzing the faces detected. Here is the sample implementation. We can also use our MLFaceAnalyzer with default settings to make it simple.
Code:
private lateinit var mAnalyzer: MLFaceAnalyzer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mAnalyzer = createAnalyzer()
}
private fun createAnalyzer(): MLFaceAnalyzer {
val settings = MLFaceAnalyzerSetting.Factory()
.allowTracing()
.setFeatureType(MLFaceAnalyzerSetting.TYPE_FEATURES)
.setShapeType(MLFaceAnalyzerSetting.TYPE_SHAPES)
.setMinFaceProportion(.5F)
.setKeyPointType(MLFaceAnalyzerSetting.TYPE_KEYPOINTS)
.create()
return MLAnalyzerFactory.getInstance().getFaceAnalyzer(settings)
}
5. Create a simple layout. Use two surfaceViews, one above the other, for camera frames and for our overlay. Because, later we will draw some shapes on our overlay view. Here is a sample layout.
Code:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<SurfaceView
android:id="@+id/surfaceViewCamera"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<SurfaceView
android:id="@+id/surfaceViewOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
6. We should prepare our views. We need two surfaceHolders in our application. We are going to make surfaceHolderOverlay transparent, because we want to see our camera frames. Later we are going to add a callback to our surfaceHolderCamera to know when it’s created, changed and destroyed. Let’s create them.
Code:
private lateinit var mAnalyzer: MLFaceAnalyzer
private var surfaceHolderCamera: SurfaceHolder? = null
private var surfaceHolderOverlay: SurfaceHolder? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mAnalyzer = createAnalyzer()
prepareViews()
}
private fun prepareViews() {
surfaceHolderCamera = surfaceViewCamera.holder
surfaceHolderOverlay = surfaceViewOverlay.holder
surfaceHolderOverlay?.setFormat(PixelFormat.TRANSPARENT)
surfaceHolderCamera?.addCallback(surfaceHolderCallback)
}
private val surfaceHolderCallback = object : SurfaceHolder.Callback {
override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder?) {
}
override fun surfaceCreated(holder: SurfaceHolder?) {
}
}
7. Now we can create our LensEngine. It is a magic class that handles camera frames for us. You can set different settings to your LensEngine. Here is how you can create it simply. As you can see in the example, the order of width and height passed to LensEngine changes according to orientation. We can create our LensEngine inside surfaceChanged method of surfaceHolderCallback and release it inside surfaceDestroyed. Here is an example of creating and running LensEngine. LensEngine needs a surfaceHolder or a surfaceTexture to run on.
Code:
private lateinit var mAnalyzer: MLFaceAnalyzer
private lateinit var mLensEngine: LensEngine
private var surfaceHolderCamera: SurfaceHolder? = null
private var surfaceHolderOverlay: SurfaceHolder? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mAnalyzer = createAnalyzer()
prepareViews()
}
private fun prepareViews() {
surfaceHolderCamera = surfaceViewCamera.holder
surfaceHolderOverlay = surfaceViewOverlay.holder
surfaceHolderOverlay?.setFormat(PixelFormat.TRANSPARENT)
surfaceHolderCamera?.addCallback(surfaceHolderCallback)
}
private val surfaceHolderCallback = object : SurfaceHolder.Callback {
override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
mLensEngine = createLensEngine(width, height)
mLensEngine.run(holder)
}
override fun surfaceDestroyed(holder: SurfaceHolder?) {
mLensEngine.release()
}
override fun surfaceCreated(holder: SurfaceHolder?) {
}
}
private fun createLensEngine(width: Int, height: Int): LensEngine {
val lensEngineCreator = LensEngine.Creator(this, mAnalyzer)
.applyFps(20F)
.setLensType(LensEngine.FRONT_LENS)
.enableAutomaticFocus(true)
return if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
lensEngineCreator.let {
it.applyDisplayDimension(height, width)
it.create()
}
} else {
lensEngineCreator.let {
it.applyDisplayDimension(width, height)
it.create()
}
}
}
8. We also need somewhere to receive detected results and interact with them. For this purpose we create our FaceAnalyzerTransactor class, you can name it as you wish. It should implement MLAnalyzer.MLTransactor<MLFace> interface. We are going to set an overlay which is of type SurfaceHolder, get our canvas from this overlay and draw some shapes on the canvas. We have the required data about the detected face inside transactResult method. Here is the sample implementation of the whole of our FaceAnalyzerTransactor class.
Code:
class FaceAnalyzerTransactor : MLAnalyzer.MLTransactor<MLFace> {
private var mOverlay: SurfaceHolder? = null
fun setOverlay(surfaceHolder: SurfaceHolder) {
mOverlay = surfaceHolder
}
override fun transactResult(result: MLAnalyzer.Result<MLFace>?) {
draw(result?.analyseList)
}
private fun draw(faces: SparseArray<MLFace>?) {
val canvas = mOverlay?.lockCanvas()
if (canvas != null && faces != null) {
//Clear the canvas
canvas.drawColor(0, PorterDuff.Mode.CLEAR)
for (face in faces.valueIterator()) {
//Draw all 855 points of the face. If Front Lens is selected, change x points side.
for (point in face.allPoints) {
val x = mOverlay?.surfaceFrame?.right?.minus(point.x)
if (x != null) {
Paint().also {
it.color = Color.YELLOW
it.style = Paint.Style.FILL
it.strokeWidth = 16F
canvas.drawPoint(x, point.y, it)
}
}
}
//Prepare a string to show if the user smiles or not and draw a text on the canvas.
val smilingString = if (face.emotions.smilingProbability > 0.5) "SMILING" else "NOT SMILING"
Paint().also {
it.color = Color.RED
it.textSize = 60F
it.textAlign = Paint.Align.CENTER
canvas.drawText(smilingString, face.border.exactCenterX(), face.border.exactCenterY(), it)
}
}
mOverlay?.unlockCanvasAndPost(canvas)
}
}
override fun destroy() {
}
}
9. Create a FaceAnalyzerTransactor instance in MainActivity and use it as shown below.
Code:
private lateinit var mAnalyzer: MLFaceAnalyzer
private lateinit var mLensEngine: LensEngine
private lateinit var mFaceAnalyzerTransactor: FaceAnalyzerTransactor
private var surfaceHolderCamera: SurfaceHolder? = null
private var surfaceHolderOverlay: SurfaceHolder? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
init()
}
private fun init() {
mAnalyzer = createAnalyzer()
mFaceAnalyzerTransactor = FaceAnalyzerTransactor()
mAnalyzer.setTransactor(mFaceAnalyzerTransactor)
prepareViews()
}
Also, don’t forget to set the overlay of our transactor. We can do this inside surfaceChanged method like this.
Code:
private val surfaceHolderCallback = object : SurfaceHolder.Callback {
override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
mLensEngine = createLensEngine(width, height)
surfaceHolderOverlay?.let { mFaceAnalyzerTransactor.setOverlay(it) }
mLensEngine.run(holder)
}
override fun surfaceDestroyed(holder: SurfaceHolder?) {
mLensEngine.release()
}
override fun surfaceCreated(holder: SurfaceHolder?) {
}
}
10. We are almost done! Don’t forget to ask for permissions from our users. We need CAMERA and WRITE_EXTERNAL_STORAGE permissions. WRITE_EXTERNAL_STORAGE permission is for automatically updating the machine learning model. Add these permissions to your AndroidManifest.xml and ask from user to grant them at runtime. Here is a simple example.
Code:
private val requiredPermissions = arrayOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (hasPermissions(requiredPermissions))
init()
else
ActivityCompat.requestPermissions(this, requiredPermissions, 0)
}
private fun hasPermissions(permissions: Array<String>) = permissions.all {
ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 0 && grantResults.isNotEmpty() && hasPermissions(requiredPermissions))
init()
}
11. Well done! We have finished all the steps and created our project. Now we can test it. Here are some examples.
12. We have created a simple FaceApp which detects faces, face features and emotions. You can produce countless number of types of face detection apps, it is up to your imagination. ML Kit empowers your apps with the power of AI. If you have any questions, please ask through the link below. You can also find this project on Github.
Related Links
Thanks to Oğuzhan Demirci for this article.
Original post: https://medium.com/huawei-developers/build-a-face-detection-app-with-huawei-ml-kit-32caec06484
Nice and useful article
Does face detection depend on specific hardware devices?
Most android applications download it's content from the cloud (commonly a REST API) getting ready to parse and display that information with lists and menus in order to display dynamic content or provide a personalized experience. There are some third party libraries designed to consume a REST API (like Retrofit) or to download media content (like Glide and Picasso). This time, let me introduce you the new Huawei Network Kit.
What is nework kit?
Network kit is the new Huawei's System SDK designed to simplify the communications with web services by providing 2 main connection modes:
Rest Client
HTTP Client
Network kit supports QUIC connections automatically, that means if the Web service supports QUIC or migrates to QUIC, your app will keep working without require any change. In addition, this kit is pretty similar to the well known Retrofit, so, if you have previous experience with Retrofit, you will be able to integrate Network Kit withount complications.
Previously, we made a News client by using the HQUIC kit. In this article we are going to develop a news client application by using the new Huawei Network Kit.
Previous requirements
A developer account in newsapi.org
Android Studio 4.0 or later and the kotlin plugin
Setting up the project
Network kit doesn't require to setup a project in AGC, but you still need to add the Huawei Maven repositories to your project-level build.gradle:
Java:
buildscript {
ext.kotlin_version = "1.4.31"
repositories {
...
maven {url 'https://developer.huawei.com/repo/'}
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.2"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
...
maven {url 'https://developer.huawei.com/repo/'}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Go to the official documents and look for the Network kit latest version under version change history. Once you have found the latest version available, add it to yout app-level build.gradle as follows
Java:
implementation 'com.huawei.hms:network-embedded:5.0.1.301'
We will use Moshi to parse the response from the web service, let's add the related dependencies and the kapt plugin to proccess the annotations.
Java:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
android{
...
}
dependencies {
...
implementation 'com.huawei.hms:network-embedded:5.0.1.301'
implementation 'com.squareup.moshi:moshi:1.11.0'
implementation "com.squareup.moshi:moshi-kotlin:1.11.0"
kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.11.0'
...
}
To display the news in a list, we must add RecyclerView and CardView to our project and must enable the DataBinding library to make our job easier.
Java:
android {
...
//Enabling DataBinding and ViewBinding
buildFeatures{
viewBinding true
dataBinding true
}
...
}
dependencies {
...
//MVVM dependencies
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0"
//DataBinding dependency
kapt "com.android.databinding:compiler:3.1.4"
//Layout dependencies
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.cardview:cardview:1.0.0"
...
}
We are ready to start the project.
Building the request
First of all, Network kit must be initialized, let's create an Application class to do this job
NetworkApplication.kt
Java:
class NetworkApplication: Application() {
companion object {
const val TAG="Network Application"
}
override fun onCreate() {
super.onCreate()
initNetworkKit()
}
private fun initNetworkKit() {
// Initialize the object only once, upon the first call.
NetworkKit.init(this ,object : NetworkKit.Callback() {
override fun onResult(result: Boolean) {
if (result) {
Log.i(TAG, "Networkkit init success")
} else {
Log.i(TAG, "Networkkit init failed")
}
}
})
}
}
To make sure this code will be excecuted upon each startup, we must specify this class inside the application element in our AndroidManifest.xml. Let's add the required permissions too.
XML:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:name=".NetworkApplication"
android:requestLegacyExternalStorage="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.NetworkKitDemo"
android:usesCleartextTraffic="true">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
Now, we must create the data models wich Moshi will use to parse the response
NewsResponse.kt
Java:
@JsonClass(generateAdapter = true)
data class NewsResponse(
@Json(name = "status") val status: String?,
@Json(name = "totalResults") val totalResults: Int?,
@Json(name = "articles") val articles: List<Article>
)
@JsonClass(generateAdapter = true)
data class Article(
@Json(name = "source") val source: Source?,
@Json(name = "author") val author: String?,
@Json(name = "title") val title: String?,
@Json(name = "description") val description: String?,
@Json(name = "url") val url: String?,
@Json(name = "urlToImage") val urlToImage: String?,
@Json(name = "publishedAt") val publishedAt: String?,
@Json(name = "content") val content: String?
)
@JsonClass(generateAdapter = true)
data class Source(
@Json(name = "id") val id: String?,
@Json(name = "name") val name: String?
)
Network kit porvides 2 operation modes, we will use the REST Client to get the Top headlines in the user's country and the HTTP Client mode to download the picture of each Article. We will create a singleton class called NetworkKitHelper.
Let's take a look to the REST Client mode:
NetworkKitHelper.kt
Java:
object NetworkKitHelper {
const val TAG: String = "HTTPClient"
//Your API key from newsapi.org
val apiKey = Keys.readApiKey()
fun createNewsClient(): NewsService {
val restClient = RestClient.Builder()
.httpClient(HttpClient.Builder().build())
.baseUrl("https://newsapi.org/v2/")//Specify the API base URL, this is useful if you will consume multiple paths of the same API
.build()
return restClient.create(NewsService::class.java)
}
//Declare a Request API
interface NewsService {
//Use the GET annotation to specify the path
@GET("top-headlines/")
fun getTopHeadlines(/* use the Query annotation to specify a query parameter in the request*/
@Query("apiKey") apiKey: String? = "",
@Query("country") country: String
): Submit<String?>?
}
fun loadTopHeadlines(sampleService: NewsService, listener: NewsClientListener?,country:String=Locale.getDefault().country) {
sampleService.getTopHeadlines(apiKey,country)?.enqueue(object : Callback<String?>() {
@Throws(IOException::class)
override fun onResponse(submit: Submit<String?>?, response: Response<String?>) {
// Obtain the response. This method will be called if the request is successful.
val body = response.body
body?.let {
try {
val moshi = Moshi.Builder().build()
val adapter = moshi.adapter(NewsResponse::class.java)
val news = adapter.fromJson(it)
news?.let { myNews ->
listener?.onNewsDownloaded(myNews.articles)
}
} catch (e: JSONException) {
Log.e("excepion", e.toString())
}
}
}
override fun onFailure(submit: Submit<String?>?, exception: Throwable?) {
// Obtain the response. This method will be called if the request fails.
Log.e("LoadTopHeadlines", "response onFailure = " + exception?.message)
}
})
}
interface NewsClientListener {
fun onNewsDownloaded(news: List<Article>)
}
}
Put special attention to the loadTopHeadlines function. As you can see, there aren't coroutines or threads defined, we are using the enqueue API instead. By this way Network Kit will handle the request in asynchronous mode for us.
If the API call is successfull, we will use Moshi to parse the response into data objects. By other way, we will be notified about the error in the onFailure callback. Once the response has been parsed, NetworkKitHelper will repor the news to the specified NewsClientListener.
Let's add the code to download the preview pics:
NetworkKitHelper.kt (Adding)
Java:
object NetworkKitHelper {
private val httpClient: HttpClient = createClient()
private fun createClient(): HttpClient {
return HttpClient.Builder()
.callTimeout(1000)
.connectTimeout(10000)
.build()
}
fun createRequest(url: String): Request {
return httpClient.newRequest()
.url(url)
.method("GET")
.build()
}
fun httpClientEnqueue(request: Request, listener: HttpClientListener? = null) {
httpClient.newSubmit(request).enqueue(object : Callback<ResponseBody?>() {
@Throws(IOException::class)
override fun onResponse(
submit: Submit<ResponseBody?>?,
response: Response<ResponseBody?>
) {
// Process the response if the request is successful.
Log.i(TAG, "response code:" + response.code)
response.body?.let {
listener?.onSuccess(it.bytes())
}
}
override fun onFailure(submit: Submit<ResponseBody?>?, throwable: Throwable?) {
// Process the exception if the request fails.
Log.w(TAG, "response onFailure = ${throwable?.message}")
}
})
}
interface HttpClientListener {
fun onSuccess(body: ByteArray)
}
}
As well as with the REST Client mode, we are able to enqueue HTTP Requests and define a callback for each one. In this case, we are receiving a byte array which will be used to create and display a bitmap.
Here we will face a complication. If we try to store the bitmap in the same data class as the Article, Moshi will cause a reflection error at compilation time. To solve this, we will define a new class to store the article and be responsible to load the bitmap, by doing so, we will be able to load the news as soon as we get them and then using the observer pattern, the bitmap will be added to the view as soon as it's ready.
ArticleModel.kt
Java:
class ArticleModel(val article: Article) : NetworkKitHelper.HttpClientListener {
private val _bitmap= MutableLiveData<Bitmap?>().apply{postValue(null)}
val bitmap: LiveData<Bitmap?> =_bitmap
init {
loadBitmap()
}
fun loadBitmap() {
article.urlToImage?.let{
val request=NetworkKitHelper.createRequest(it)
NetworkKitHelper.httpClientEnqueue(request, this)
}
}
override fun onSuccess(body: ByteArray) {
val bitmap= BitmapFactory.decodeByteArray(body, 0, body.size)
val resizedBitmap = Bitmap.createScaledBitmap(bitmap, 1000, 600, true)
_bitmap.postValue(resizedBitmap)
}
}
As soon as any instance of ArticleModel is created, it will enqueue an HTTP request async for the preview pic. If the call is successfull, we will receive a ByteArray in the onSuccess callback to create our bitmap from it and let the observer know the bitmap is ready to be displayed.
Sending the request
Let's create a ViewModel which will be responsible to invoke the API and store the data. Here we will use the observer pattern to let the observer know the Articles are ready to be displayed.
MainViewModel.kt
Java:
class MainViewModel : ViewModel(), NetworkKitHelper.NewsClientListener {
private val _articles = MutableLiveData<ArrayList<ArticleModel>>().apply { value = ArrayList() }
val articles: LiveData<ArrayList<ArticleModel>> = _articles
fun loadTopHeadlines(){
articles.value?.let{
if(it.isEmpty()) getTopHeadlines()
else return
}
}
private fun getTopHeadlines() {
NetworkKitHelper.loadTopHeadlines(NetworkKitHelper.createNewsClient(),this)
}
override fun onNewsDownloaded(news: List<Article>) {
val list=ArrayList<ArticleModel>()
for (article: Article in news) {
list.add(ArticleModel(article))
}
_articles.postValue(list)
}
}
To avoid downloading the news again when the user rotates the screen, we are defining the loadTopHeadlines function. It will only make the request if the list of articles is empty.
Displaying the Articles
We will use DataBinding to quicly display our news in a RecyclerView on the MainActivity, let's take a look to the main layout
activity_main.xml
XML:
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
>
<data class="MainBinding"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_height="match_parent"
android:layout_width="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Now we must define the card wich will be rendered for each article
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
article_card.xml
XML:
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data class="ArticleBinding">
<variable
name="item"
type="com.hms.demo.networkkitdemo.ArticleModel" />
</data>
<androidx.cardview.widget.CardView
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="5dp"
android:layout_marginVertical="5dp"
card_view:cardCornerRadius="15dp"
card_view:cardElevation="20dp"
android:foreground="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/pic"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:contentDescription="@string/desc"
card_view:layout_constraintEnd_toEndOf="parent"
card_view:layout_constraintHorizontal_bias="1.0"
card_view:layout_constraintStart_toStartOf="parent"
card_view:layout_constraintTop_toBottomOf="@+id/articleTitle"
card_view:shapeAppearanceOverlay="@style/roundedImageView"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/articleTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.article.title}"
android:textAlignment="viewStart"
android:textSize="24sp"
android:textStyle="bold"
card_view:layout_constraintEnd_toEndOf="parent"
card_view:layout_constraintHorizontal_bias="0.498"
card_view:layout_constraintStart_toStartOf="parent"
card_view:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/desc"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="@{item.article.description}"
android:textSize="20sp"
card_view:layout_constraintEnd_toEndOf="parent"
card_view:layout_constraintStart_toStartOf="parent"
card_view:layout_constraintTop_toBottomOf="@+id/pic" />
<TextView
android:id="@+id/source"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="@{item.article.source.name}"
card_view:layout_constraintBottom_toBottomOf="parent"
card_view:layout_constraintStart_toStartOf="parent"
card_view:layout_constraintTop_toBottomOf="@+id/desc"
card_view:layout_constraintVertical_bias="0.09" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</layout>
The ArticleBinding class will be responsible to fill the view with the values in it's ArticleModel instance for us. That's the magic of DataBinding.
As you may know, to display elements in a RecyclerView we need an Adapter, so let's define it
NewsAdapter.kt
Java:
class NewsAdapter: RecyclerView.Adapter<NewsAdapter.NewsViewHolder>() {
var articles:List<ArticleModel> =ArrayList()
class NewsViewHolder(private val binding:ArticleBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(item:ArticleModel){
binding.item=item
item.bitmap.observe(binding.root.context as LifecycleOwner){
it?.let{
binding.pic.setImageBitmap(it)
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder {
val inflater=LayoutInflater.from(parent.context)
val binding=ArticleBinding.inflate(inflater,parent,false)
return NewsViewHolder(binding)
}
override fun onBindViewHolder(holder: NewsViewHolder, position: Int) {
holder.bind(articles[position])
}
override fun getItemCount(): Int {
return articles.size
}
}
Put special attention to the bind function of the NewsViewHolder class, from here we are telling to the ArticleBinding instance what is the information we want to display in the view. Also, we are using the observer pattern to update the ImageView once the article's preview pic has been downloaded.
Finally, is time to join everything through the MainActivity
MainActivity.kt
Java:
class MainActivity : AppCompatActivity() {
companion object {
const val TAG="Main"
}
private lateinit var binding:MainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding= MainBinding.inflate(layoutInflater)
binding.lifecycleOwner = this
setContentView(binding.root)
val viewModel:MainViewModel=ViewModelProvider(this).get(MainViewModel::class.java)
val adapter=NewsAdapter()
viewModel.articles.observe(this){
adapter.articles=it
adapter.notifyDataSetChanged()
}
binding.recycler.adapter=adapter
viewModel.loadTopHeadlines()
}
}
Final result
Tips & Tricks
If your app will consume a REST API with Kotlin, is better to use Moshi instead of gson because Moshi can understand the kotlin's not-nullable types.
If you will use API keys to authenticate your client with the server, is better to use the NDK to hide your KEY and prevent it from being obtained by using reverse engineering. Let's use the Rahul Sharma's hidding method. (Make sure to download the Android NDK from the SDK Manager)
1. Swithch to the Project view and create a jni directory under the main directory.
2. Under the jni directory add the next 3 files:
Android.mk
Code:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := keys
LOCAL_SRC_FILES := keys.c
include $(BUILD_SHARED_LIBRARY)
Application.mk
Code:
APP_ABI := all
Keys.c (Put here your API key)
Code:
#include <jni.h>
JNIEXPORT jstring JNICALL
Java_com_hms_demo_networkkitdemo_Keys_getApiKey(JNIEnv *env, jclass instance) {
return (*env)->NewStringUTF(env, "PUT_HERE_YOUR_API_KEY");
}
3. Switch back to the Android View and create a Keys kotlin object
Keys.kt
Java:
object Keys {
init {
System.loadLibrary("keys")
}
private external fun getApiKey(): String?
public fun readApiKey(): String? { //use this method for String
return getApiKey()
}
}
4. Tell gradle you will use NDK by adding the next code inside android
build.gradle (app-level)
Java:
plugins {
...
}
android {
...
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
}
dependencies {
...
}
Finally, modify the NetworkKitHelper object to read the API key from the native library.
NetworkKitHelper.kt (Modifying)
Code:
object NetworkKitHelper {
val apiKey = Keys.readApiKey()
}
Conclusion
By using Network kit your app will be ready to perform requests over QUIC or HTTP/2 without writting extra code. The REST Client mode and it's annotations are helpful to to quickly consume a REST API without taking care about Threads or Coroutines. And finally, the HTTP Client mode is useful to download preview images or any other stuff which is not a JSON.
References
Read In Forum
Network Kit official Docs
Hiding Secret/Api key from reverse engineering in Android using NDK
Moshi
Hi, i have one question if we use network kit then we no need to use any third-party like volley, Retrofit?
Is it faster and easy to use than Retrofit library?
Introduction
In this article, we will learn how to integrate Huawei General Text Recognition using Huawei HiAI. We will build the Book reader application.
About application:
Usually user get bored to read book. This application helps them to listen book reading instead of manual book reading. So all they need to do is just capture photo of book and whenever user is travelling or whenever user want to read the book on their free time. Just user need to select image from galley and listen like music.
Huawei general text recognition works on OCR technology.
First let us understand about OCR.
What is optical character recognition (OCR)?
Optical Character Recognition (OCR) technology is a business solution for automating data extraction from printed or written text from a scanned document or image file and then converting the text into a machine-readable form to be used for data processing like editing or searching.
Now let us understand about General Text Recognition (GTR).
At the core of the GTR is Optical Character Recognition (OCR) technology, which extracts text in screenshots and photos taken by the phone camera. For photos taken by the camera, this API can correct for tilts, camera angles, reflections, and messy backgrounds up to a certain degree. It can also be used for document and streetscape photography, as well as a wide range of usage scenarios, and it features strong anti-interference capability. This API works on device side processing and service connection.
Features
For photos: Provides text area detection and text recognition for Chinese, English, Japanese, Korean, Russian, Italian, Spanish, Portuguese, German, and French texts in multiple printing fonts. A wide range of scenarios are supported, and a high recognition accuracy can be achieved even under the influence of complex lighting condition, background, or more.
For screenshots: Optimizes text extraction algorithms based on the characteristics of screenshots captured on mobile phones. Currently, this function is available in the Chinese mainland supporting Chinese and English texts.
OCR features
Lightweight: This API greatly reduces the computing time and ROM space the algorithm model takes up, making your app more lightweight.
Customized hierarchical result return: You can choose to return the coordinates of text blocks, text lines, and text characters in the screenshot based on app requirements.
How to integrate General Text Recognition
1. Configure the application on the AGC.
2. Apply for HiAI Engine Library
3. Client application development process.
Configure application on the AGC
Follow the steps
Step 1: We need to register as a developer account in AppGallery Connect. If you are already a developer ignore this step.
Step 2: Create an app by referring to Creating a Project and Creating an App in the Project
Step 3: Set the data storage location based on the current location.
Step 4: Generating a Signing Certificate Fingerprint.
Step 5: Configuring the Signing Certificate Fingerprint.
Step 6: Download your agconnect-services.json file, paste it into the app root directory.
Apply for HiAI Engine Library
What is Huawei HiAI?
HiAI is Huawei’s AI computing platform. HUAWEI HiAI is a mobile terminal–oriented artificial intelligence (AI) computing platform that constructs three layers of ecology: service capability openness, application capability openness, and chip capability openness. The three-layer open platform that integrates terminals, chips, and the cloud brings more extraordinary experience for users and developers.
How to apply for HiAI Engine?
Follow the steps
Step 1: Navigate to this URL, choose App Service > Development and click HUAWEI HiAI.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Step 2: Click Apply for HUAWEI HiAI kit.
Step 3: Enter required information like Product name and Package name, click Next button.
Step 4: Verify the application details and click Submit button.
Step 5: Click the Download SDK button to open the SDK list.
Step 6: Unzip downloaded SDK and add into your android project under libs folder.
Step 7: Add jar files dependences into app build.gradle file.
Code:
implementation fileTree(include: ['*.aar', '*.jar'], dir: 'libs')
implementation 'com.google.code.gson:gson:2.8.6'
repositories {
flatDir {
dirs 'libs'
}
}
Client application development process
Follow the steps
Step 1: Create an Android application in the Android studio (Any IDE which is your favorite).
Step 2: Add the App level Gradle dependencies. Choose inside project Android > app > build.gradle.
Code:
apply plugin: 'com.android.application'
apply plugin: 'com.huawei.agconnect'
Root level gradle dependencies.
Code:
maven { url 'https://developer.huawei.com/repo/' }
classpath 'com.huawei.agconnect:agcp:1.4.1.300'
Step 3: Add permission in AndroidManifest.xml
XML:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_INTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
Step 4: Build application.
Initialize all view.
Java:
private void initializeView() {
mPlayAudio = findViewById(R.id.playAudio);
mTxtViewResult = findViewById(R.id.result);
mImageView = findViewById(R.id.imgViewPicture);
}
Request the runtime permission
Java:
private void requestPermissions() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
int permission1 = ActivityCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE);
int permission2 = ActivityCompat.checkSelfPermission(this,
Manifest.permission.CAMERA);
if (permission1 != PackageManager.PERMISSION_GRANTED || permission2 != PackageManager
.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA}, 0x0010);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length <= 0
|| grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
}
}
Initialize vision base
Java:
private void initVision() {
VisionBase.init(this, new ConnectionCallback() {
@override
public void onServiceConnect() {
Log.e(TAG, " onServiceConnect");
}
@override
public void onServiceDisconnect() {
Log.e(TAG, " onServiceDisconnect");
}
});
}
Initialize text to speech
Java:
private void initializeTextToSpeech() {
textToSpeech = new TextToSpeech(getApplicationContext(), new TextToSpeech.OnInitListener() {
@override
public void onInit(int status) {
if (status != TextToSpeech.ERROR) {
textToSpeech.setLanguage(Locale.UK);
}
}
});
}
Copy code
Create TextDetector instance.
Java:
mTextDetector = new TextDetector(this);
Define Vision image.
Java:
VisionImage image = VisionImage.fromBitmap(mBitmap);
Create instance of Text class.
Java:
final Text result = new Text();
Create and set VisionTextConfiguration
Java:
VisionTextConfiguration config = new VisionTextConfiguration.Builder()
.setAppType(VisionTextConfiguration.APP_NORMAL)
.setProcessMode(VisionTextConfiguration.MODE_IN)
.setDetectType(TextDetectType.TYPE_TEXT_DETECT_FOCUS_SHOOT)
.setLanguage(TextConfiguration.AUTO).build();
//Set vision configuration
mTextDetector.setVisionConfiguration(config);
Call detect method to get the result
Java:
int result_code = mTextDetector.detect(image, result, new VisionCallback<Text>() {
@override
public void onResult(Text text) {
dismissDialog();
Message message = Message.obtain();
message.what = TYPE_SHOW_RESULT;
message.obj = text;
mHandler.sendMessage(message);
}
@override
public void onError(int i) {
Log.d(TAG, "Callback: onError " + i);
mHandler.sendEmptyMessage(TYPE_TEXT_ERROR);
}
@override
public void onProcessing(float v) {
Log.d(TAG, "Callback: onProcessing:" + v);
}
});
Copy code
Create Handler
Java:
private final Handler mHandler = new Handler() {
[USER=439709]@override[/USER]
public void handleMessage(Message msg) {
super.handleMessage(msg);
int status = msg.what;
Log.d(TAG, "handleMessage status = " + status);
switch (status) {
case TYPE_CHOOSE_PHOTO: {
if (mBitmap == null) {
Log.e(TAG, "bitmap is null");
return;
}
mImageView.setImageBitmap(mBitmap);
mTxtViewResult.setText("");
showDialog();
detectTex();
break;
}
case TYPE_SHOW_RESULT: {
Text result = (Text) msg.obj;
if (dialog != null && dialog.isShowing()) {
dialog.dismiss();
}
if (result == null) {
mTxtViewResult.setText("Failed to detect text lines, result is null.");
break;
}
String textValue = result.getValue();
Log.d(TAG, "text value: " + textValue);
StringBuffer textResult = new StringBuffer();
List<TextLine> textLines = result.getBlocks().get(0).getTextLines();
for (TextLine line : textLines) {
textResult.append(line.getValue() + " ");
}
Log.d(TAG, "OCR Detection succeeded.");
mTxtViewResult.setText(textResult.toString());
textToSpeechString = textResult.toString();
break;
}
case TYPE_TEXT_ERROR: {
mTxtViewResult.setText("Failed to detect text lines, result is null.");
}
default:
break;
}
}
};
Complete code as follows
Java:
import android.Manifest;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.provider.MediaStore;
import android.speech.tts.TextToSpeech;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.huawei.hiai.vision.common.ConnectionCallback;
import com.huawei.hiai.vision.common.VisionBase;
import com.huawei.hiai.vision.common.VisionCallback;
import com.huawei.hiai.vision.common.VisionImage;
import com.huawei.hiai.vision.text.TextDetector;
import com.huawei.hiai.vision.visionkit.text.Text;
import com.huawei.hiai.vision.visionkit.text.TextDetectType;
import com.huawei.hiai.vision.visionkit.text.TextLine;
import com.huawei.hiai.vision.visionkit.text.config.TextConfiguration;
import com.huawei.hiai.vision.visionkit.text.config.VisionTextConfiguration;
import java.util.List;
import java.util.Locale;
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
private static final int REQUEST_CHOOSE_PHOTO_CODE = 2;
private Bitmap mBitmap;
private ImageView mPlayAudio;
private ImageView mImageView;
private TextView mTxtViewResult;
protected ProgressDialog dialog;
private TextDetector mTextDetector;
Text imageText = null;
TextToSpeech textToSpeech;
String textToSpeechString = "";
private static final int TYPE_CHOOSE_PHOTO = 1;
private static final int TYPE_SHOW_RESULT = 2;
private static final int TYPE_TEXT_ERROR = 3;
[USER=439709]@override[/USER]
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initializeView();
requestPermissions();
initVision();
initializeTextToSpeech();
}
private void initializeView() {
mPlayAudio = findViewById(R.id.playAudio);
mTxtViewResult = findViewById(R.id.result);
mImageView = findViewById(R.id.imgViewPicture);
}
private void initVision() {
VisionBase.init(this, new ConnectionCallback() {
[USER=439709]@override[/USER]
public void onServiceConnect() {
Log.e(TAG, " onServiceConnect");
}
[USER=439709]@override[/USER]
public void onServiceDisconnect() {
Log.e(TAG, " onServiceDisconnect");
}
});
}
private void initializeTextToSpeech() {
textToSpeech = new TextToSpeech(getApplicationContext(), new TextToSpeech.OnInitListener() {
[USER=439709]@override[/USER]
public void onInit(int status) {
if (status != TextToSpeech.ERROR) {
textToSpeech.setLanguage(Locale.UK);
}
}
});
}
public void onChildClick(View view) {
switch (view.getId()) {
case R.id.btnSelect: {
Log.d(TAG, "Select an image");
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
startActivityForResult(intent, REQUEST_CHOOSE_PHOTO_CODE);
break;
}
case R.id.playAudio: {
if (textToSpeechString != null && !textToSpeechString.isEmpty())
textToSpeech.speak(textToSpeechString, TextToSpeech.QUEUE_FLUSH, null);
break;
}
}
}
private void detectTex() {
/* create a TextDetector instance firstly */
mTextDetector = new TextDetector(this);
/*Define VisionImage and transfer the Bitmap image to be detected*/
VisionImage image = VisionImage.fromBitmap(mBitmap);
/*Define the Text class.*/
final Text result = new Text();
/*Use VisionTextConfiguration to select the type of the image to be called. */
VisionTextConfiguration config = new VisionTextConfiguration.Builder()
.setAppType(VisionTextConfiguration.APP_NORMAL)
.setProcessMode(VisionTextConfiguration.MODE_IN)
.setDetectType(TextDetectType.TYPE_TEXT_DETECT_FOCUS_SHOOT)
.setLanguage(TextConfiguration.AUTO).build();
//Set vision configuration
mTextDetector.setVisionConfiguration(config);
/*Call the detect method of TextDetector to obtain the result*/
int result_code = mTextDetector.detect(image, result, new VisionCallback<Text>() {
[USER=439709]@override[/USER]
public void onResult(Text text) {
dismissDialog();
Message message = Message.obtain();
message.what = TYPE_SHOW_RESULT;
message.obj = text;
mHandler.sendMessage(message);
}
[USER=439709]@override[/USER]
public void onError(int i) {
Log.d(TAG, "Callback: onError " + i);
mHandler.sendEmptyMessage(TYPE_TEXT_ERROR);
}
[USER=439709]@override[/USER]
public void onProcessing(float v) {
Log.d(TAG, "Callback: onProcessing:" + v);
}
});
}
private void showDialog() {
if (dialog == null) {
dialog = new ProgressDialog(MainActivity.this);
dialog.setTitle("Detecting text...");
dialog.setMessage("Please wait...");
dialog.setIndeterminate(true);
dialog.setCancelable(false);
}
dialog.show();
}
private final Handler mHandler = new Handler() {
[USER=439709]@override[/USER]
public void handleMessage(Message msg) {
super.handleMessage(msg);
int status = msg.what;
Log.d(TAG, "handleMessage status = " + status);
switch (status) {
case TYPE_CHOOSE_PHOTO: {
if (mBitmap == null) {
Log.e(TAG, "bitmap is null");
return;
}
mImageView.setImageBitmap(mBitmap);
mTxtViewResult.setText("");
showDialog();
detectTex();
break;
}
case TYPE_SHOW_RESULT: {
Text result = (Text) msg.obj;
if (dialog != null && dialog.isShowing()) {
dialog.dismiss();
}
if (result == null) {
mTxtViewResult.setText("Failed to detect text lines, result is null.");
break;
}
String textValue = result.getValue();
Log.d(TAG, "text value: " + textValue);
StringBuffer textResult = new StringBuffer();
List<TextLine> textLines = result.getBlocks().get(0).getTextLines();
for (TextLine line : textLines) {
textResult.append(line.getValue() + " ");
}
Log.d(TAG, "OCR Detection succeeded.");
mTxtViewResult.setText(textResult.toString());
textToSpeechString = textResult.toString();
break;
}
case TYPE_TEXT_ERROR: {
mTxtViewResult.setText("Failed to detect text lines, result is null.");
}
default:
break;
}
}
};
[USER=439709]@override[/USER]
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CHOOSE_PHOTO_CODE && resultCode == Activity.RESULT_OK) {
if (data == null) {
return;
}
Uri selectedImage = data.getData();
getBitmap(selectedImage);
}
}
private void requestPermissions() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
int permission1 = ActivityCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE);
int permission2 = ActivityCompat.checkSelfPermission(this,
Manifest.permission.CAMERA);
if (permission1 != PackageManager.PERMISSION_GRANTED || permission2 != PackageManager
.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA}, 0x0010);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void getBitmap(Uri imageUri) {
String[] pathColumn = {MediaStore.Images.Media.DATA};
Cursor cursor = getContentResolver().query(imageUri, pathColumn, null, null, null);
if (cursor == null) return;
cursor.moveToFirst();
int columnIndex = cursor.getColumnIndex(pathColumn[0]);
/* get image path */
String picturePath = cursor.getString(columnIndex);
cursor.close();
mBitmap = BitmapFactory.decodeFile(picturePath);
if (mBitmap == null) {
return;
}
//You can set image here
//mImageView.setImageBitmap(mBitmap);
// You can pass it handler as well
mHandler.sendEmptyMessage(TYPE_CHOOSE_PHOTO);
mTxtViewResult.setText("");
mPlayAudio.setEnabled(true);
}
private void dismissDialog() {
if (dialog != null && dialog.isShowing()) {
dialog.dismiss();
}
}
[USER=439709]@override[/USER]
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length <= 0
|| grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
}
}
[USER=439709]@override[/USER]
protected void onDestroy() {
super.onDestroy();
/* release ocr instance and free the npu resources*/
if (mTextDetector != null) {
mTextDetector.release();
}
dismissDialog();
if (mBitmap != null) {
mBitmap.recycle();
}
}
}
activity_main.xml
XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true"
androidrientation="vertical"
android:background="@android:color/darker_gray">
<android.support.v7.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#ff0000"
android:elevation="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
androidrientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="Book Reader"
android:layout_gravity="center"
android:gravity="center|start"
android:layout_weight="1"
android:textColor="@android:color/white"
android:textStyle="bold"
android:textSize="20sp"/>
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_baseline_play_circle_outline_24"
android:layout_gravity="center|end"
android:layout_marginEnd="10dp"
android:id="@+id/playAudio"
androidadding="5dp"/>
</LinearLayout>
</android.support.v7.widget.Toolbar>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
androidrientation="vertical"
android:background="@android:color/darker_gray"
>
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="5dp"
app:cardElevation="10dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginTop="20dp"
android:layout_gravity="center">
<ImageView
android:id="@+id/imgViewPicture"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_margin="8dp"
android:layout_gravity="center_horizontal"
android:scaleType="fitXY" />
</android.support.v7.widget.CardView>
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="5dp"
app:cardElevation="10dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginTop="10dp"
android:layout_gravity="center"
android:layout_marginBottom="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
androidrientation="vertical"
>
<TextView
android:layout_margin="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:text="Text on the image"
android:textStyle="normal"
/>
<TextView
android:id="@+id/result"
android:layout_margin="5dp"
android:layout_marginBottom="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textColor="#ff0000"/>
</LinearLayout>
</android.support.v7.widget.CardView>
<Button
android:id="@+id/btnSelect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
androidnClick="onChildClick"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
android:text="[USER=936943]@string[/USER]/select_picture"
android:background="@drawable/round_button_bg"
android:textColor="@android:color/white"
android:textAllCaps="false"/>
</LinearLayout>
</ScrollView>
</LinearLayout>
Result
Tips and Tricks
Maximum width and height: 1440 px and 15210 px (If the image is larger than this, you will receive error code 200).
Photos recommended size for optimal recognition accuracy.
Resolution > 720P
Aspect ratio < 2:1
If you are taking Video from a camera or gallery make sure your app has camera and storage permission.
Add the downloaded huawei-hiai-vision-ove-10.0.4.307.aar, huawei-hiai-pdk-1.0.0.aar file to libs folder.
Check dependencies added properly
Latest HMS Core APK is required.
Min SDK is 21. Otherwise you will get Manifest merge issue.
Conclusion
In this article, we have learnt the following concepts.
What is OCR?
Learnt about general text recognition.
Feature of GTR
Features of OCR
How to integrate General Text Recognition using Huawei HiAI
How to Apply Huawei HiAI
How to build the application
Reference
General Text Recognition
Apply for Huawei HiAI
Happy coding
how many languages can it be detected?
Introduction
If are you new to this application, please follow my previous articles
Pygmy collection application Part 1 (Account kit)
Intermediate: Pygmy Collection Application Part 2 (Ads Kit)
Intermediate: Pygmy Collection Application Part 3 (Crash service)
Intermediate: Pygmy Collection Application Part 4 (Analytics Kit Custom Events)
Intermediate: Pygmy Collection Application Part 5 (Safety Detect)
Intermediate: Pygmy Collection Application Part 6 (Room database)
Click to expand...
Click to collapse
In this article, we will learn how to integrate Huawei Document skew correction using Huawei HiAI in Pygmy collection finance application.
In pygmy collection application for customers KYC update need to happen, so agents will update the KYC, in that case document should be proper, so we will integrate the document skew correction for the image angle adjustment.
Commonly user Struggles a lot while uploading or filling any form due to document issue. This application helps them to take picture from the camera or from the gallery, it automatically detects document from the image.
Document skew correction is used to improve the document photography process by automatically identifying the document in an image. This actually returns the position of the document in original image.
Document skew correction also adjusts the shooting angle of the document based on the position information of the document in original image. This function has excellent performance in scenarios where photos of old photos, paper letters, and drawings are taken for electronic storage.
Features
Document detection: Recognizes documents in images and returns the location information of the documents in the original images.
Document correction: Corrects the document shooting angle based on the document location information in the original images, where areas to be corrected can be customized.
How to integrate Document Skew Correction
1. Configure the application on the AGC.
2. Apply for HiAI Engine Library.
3. Client application development process.
Configure application on the AGC
Follow the steps.
Step 1: We need to register as a developer account in AppGallery Connect. If you are already a developer ignore this step.
Step 2: Create an app by referring to Creating a Project and Creating an App in the Project
Step 3: Set the data storage location based on the current location.
Step 4: Generating a Signing Certificate Fingerprint.
Step 5: Configuring the Signing Certificate Fingerprint.
Step 6: Download your agconnect-services.json file, paste it into the app root directory.
Apply for HiAI Engine Library
What is Huawei HiAI?
HiAI is Huawei’s AI computing platform. HUAWEI HiAI is a mobile terminal–oriented artificial intelligence (AI) computing platform that constructs three layers of ecology: service capability openness, application capability openness, and chip capability openness. The three-layer open platform that integrates terminals, chips, and the cloud brings more extraordinary experience for users and developers.
How to apply for HiAI Engine?
Follow the steps.
Step 1: Navigate to this URL, choose App Service > Development and click HUAWEI HiAI.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Step 2: Click Apply for HUAWEI HiAI kit.
Step 3: Enter required information like Product name and Package name, click Next button.
Step 4: Verify the application details and click Submit button.
Step 5: Click the Download SDK button to open the SDK list.
Step 6: Unzip downloaded SDK and add into your android project under libs folder.
Step 7: Add jar files dependences into app build.gradle file.
Code:
implementation fileTree(include: ['*.aar', '*.jar'], dir: 'libs')
implementation 'com.google.code.gson:gson:2.8.6'
repositories {
flatDir {
dirs 'libs'
}
}
Client application development process
Follow the steps.
Step 1: Create an Android application in the Android studio (Any IDE which is your favorite).
Step 2: Add the App level Gradle dependencies. Choose inside project Android > app > build.gradle.
Code:
apply plugin: 'com.android.application'
apply plugin: 'com.huawei.agconnect'
Root level gradle dependencies.
Code:
maven { url 'https://developer.huawei.com/repo/' }
classpath 'com.huawei.agconnect:agcp:1.4.1.300'
Step 3: Add permission in AndroidManifest.xml
XML:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_INTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
Step 4: Build application.
Select image dialog
Java:
private void selectImage() {
final CharSequence[] items = {"Take Photo", "Choose from Library",
"Cancel"};
AlertDialog.Builder builder = new AlertDialog.Builder(KycUpdateActivity.this);
builder.setTitle("Add Photo!");
builder.setItems(items, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int item) {
boolean result = PygmyUtils.checkPermission(KycUpdateActivity.this);
if (items[item].equals("Take Photo")) {
/*userChoosenTask = "Take Photo";
if (result)
cameraIntent();*/
operate_type = TAKE_PHOTO;
requestPermission(Manifest.permission.CAMERA);
} else if (items[item].equals("Choose from Library")) {
/* userChoosenTask = "Choose from Library";
if (result)
galleryIntent();*/
operate_type = SELECT_ALBUM;
requestPermission(Manifest.permission.READ_EXTERNAL_STORAGE);
} else if (items[item].equals("Cancel")) {
dialog.dismiss();
}
}
});
builder.show();
}
Open Document Skew correction activity
Java:
private void startSuperResolutionActivity() {
Intent intent = new Intent(KycUpdateActivity.this, DocumentSkewCorrectionActivity.class);
intent.putExtra("operate_type", operate_type);
startActivityForResult(intent, DOCUMENT_SKEW_CORRECTION_REQUEST);
}
DocumentSkewCorrectonActivity.java
Java:
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ContentValues;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.Point;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.huawei.hmf.tasks.OnFailureListener;
import com.huawei.hmf.tasks.OnSuccessListener;
import com.huawei.hmf.tasks.Task;
import com.huawei.hms.mlsdk.common.MLFrame;
import com.huawei.hms.mlsdk.dsc.MLDocumentSkewCorrectionAnalyzer;
import com.huawei.hms.mlsdk.dsc.MLDocumentSkewCorrectionAnalyzerFactory;
import com.huawei.hms.mlsdk.dsc.MLDocumentSkewCorrectionAnalyzerSetting;
import com.huawei.hms.mlsdk.dsc.MLDocumentSkewCorrectionCoordinateInput;
import com.huawei.hms.mlsdk.dsc.MLDocumentSkewCorrectionResult;
import com.huawei.hms.mlsdk.dsc.MLDocumentSkewDetectResult;
import com.shea.pygmycollection.R;
import com.shea.pygmycollection.customview.DocumentCorrectImageView;
import com.shea.pygmycollection.utils.FileUtils;
import com.shea.pygmycollection.utils.UserDataUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class DocumentSkewCorrectionActivity extends AppCompatActivity implements View.OnClickListener {
private static final String TAG = "SuperResolutionActivity";
private static final int REQUEST_SELECT_IMAGE = 1000;
private static final int REQUEST_TAKE_PHOTO = 1;
private ImageView desImageView;
private ImageButton adjustImgButton;
private Bitmap srcBitmap;
private Bitmap getCompressesBitmap;
private Uri imageUri;
private MLDocumentSkewCorrectionAnalyzer analyzer;
private Bitmap corrected;
private ImageView back;
private Task<MLDocumentSkewCorrectionResult> correctionTask;
private DocumentCorrectImageView documetScanView;
private Point[] _points;
private RelativeLayout layout_image;
private MLFrame frame;
TextView selectTv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_document_skew_corretion);
//setStatusBarColor(this, R.color.black);
analyzer = createAnalyzer();
adjustImgButton = findViewById(R.id.adjust);
layout_image = findViewById(R.id.layout_image);
desImageView = findViewById(R.id.des_image);
documetScanView = findViewById(R.id.iv_documetscan);
back = findViewById(R.id.back);
selectTv = findViewById(R.id.selectTv);
adjustImgButton.setOnClickListener(this);
findViewById(R.id.back).setOnClickListener(this);
selectTv.setOnClickListener(this);
findViewById(R.id.rl_chooseImg).setOnClickListener(this);
back.setOnClickListener(this);
int operate_type = getIntent().getIntExtra("operate_type", 101);
if (operate_type == 101) {
takePhoto();
} else if (operate_type == 102) {
selectLocalImage();
}
}
private String[] chooseTitles;
@Override
public void onClick(View v) {
if (v.getId() == R.id.adjust) {
List<Point> points = new ArrayList<>();
Point[] cropPoints = documetScanView.getCropPoints();
if (cropPoints != null) {
points.add(cropPoints[0]);
points.add(cropPoints[1]);
points.add(cropPoints[2]);
points.add(cropPoints[3]);
}
MLDocumentSkewCorrectionCoordinateInput coordinateData = new MLDocumentSkewCorrectionCoordinateInput(points);
getDetectdetectResult(coordinateData, frame);
} else if (v.getId() == R.id.rl_chooseImg) {
chooseTitles = new String[]{getResources().getString(R.string.take_photo), getResources().getString(R.string.select_from_album)};
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setItems(chooseTitles, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int position) {
if (position == 0) {
takePhoto();
} else {
selectLocalImage();
}
}
});
builder.create().show();
} else if (v.getId() == R.id.selectTv) {
if (corrected == null) {
Toast.makeText(this, "Document Skew correction is not yet success", Toast.LENGTH_SHORT).show();
return;
} else {
ProgressDialog pd = new ProgressDialog(this);
pd.setMessage("Please wait...");
pd.show();
//UserDataUtils.saveBitMap(this, corrected);
Intent intent = new Intent();
intent.putExtra("status", "success");
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (pd != null && pd.isShowing()) {
pd.dismiss();
}
setResult(Activity.RESULT_OK, intent);
finish();
}
}, 3000);
}
} else if (v.getId() == R.id.back) {
finish();
}
}
private MLDocumentSkewCorrectionAnalyzer createAnalyzer() {
MLDocumentSkewCorrectionAnalyzerSetting setting = new MLDocumentSkewCorrectionAnalyzerSetting
.Factory()
.create();
return MLDocumentSkewCorrectionAnalyzerFactory.getInstance().getDocumentSkewCorrectionAnalyzer(setting);
}
private void takePhoto() {
layout_image.setVisibility(View.GONE);
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(this.getPackageManager()) != null) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.TITLE, "New Picture");
values.put(MediaStore.Images.Media.DESCRIPTION, "From Camera");
this.imageUri = this.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, this.imageUri);
this.startActivityForResult(takePictureIntent, DocumentSkewCorrectionActivity.this.REQUEST_TAKE_PHOTO);
}
}
private void selectLocalImage() {
layout_image.setVisibility(View.GONE);
Intent intent = new Intent(Intent.ACTION_PICK, null);
intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
startActivityForResult(intent, REQUEST_SELECT_IMAGE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_SELECT_IMAGE && resultCode == Activity.RESULT_OK) {
imageUri = data.getData();
try {
if (imageUri != null) {
srcBitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), imageUri);
String realPathFromURI = getRealPathFromURI(imageUri);
int i = readPictureDegree(realPathFromURI);
Bitmap spBitmap = rotaingImageView(i, srcBitmap);
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
getCompressesBitmap = Bitmap.createBitmap(spBitmap, 0, 0, spBitmap.getWidth(),
spBitmap.getHeight(), matrix, true);
reloadAndDetectImage();
}
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
} else if (requestCode == REQUEST_TAKE_PHOTO && resultCode == Activity.RESULT_OK) {
try {
if (imageUri != null) {
srcBitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), imageUri);
String realPathFromURI = getRealPathFromURI(imageUri);
int i = readPictureDegree(realPathFromURI);
Bitmap spBitmap = rotaingImageView(i, srcBitmap);
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
getCompressesBitmap = Bitmap.createBitmap(spBitmap, 0, 0, spBitmap.getWidth(),
srcBitmap.getHeight(), matrix, true);
reloadAndDetectImage();
}
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
} else if (resultCode == REQUEST_SELECT_IMAGE && resultCode == Activity.RESULT_CANCELED) {
finish();
}
}
private void reloadAndDetectImage() {
if (imageUri == null) {
return;
}
frame = MLFrame.fromBitmap(getCompressesBitmap);
Task<MLDocumentSkewDetectResult> task = analyzer.asyncDocumentSkewDetect(frame);
task.addOnSuccessListener(new OnSuccessListener<MLDocumentSkewDetectResult>() {
public void onSuccess(MLDocumentSkewDetectResult result) {
if (result.getResultCode() != 0) {
corrected = null;
Toast.makeText(DocumentSkewCorrectionActivity.this, "The picture does not meet the requirements.", Toast.LENGTH_SHORT).show();
} else {
// Recognition success.
Point leftTop = result.getLeftTopPosition();
Point rightTop = result.getRightTopPosition();
Point leftBottom = result.getLeftBottomPosition();
Point rightBottom = result.getRightBottomPosition();
_points = new Point[4];
_points[0] = leftTop;
_points[1] = rightTop;
_points[2] = rightBottom;
_points[3] = leftBottom;
layout_image.setVisibility(View.GONE);
documetScanView.setImageBitmap(getCompressesBitmap);
documetScanView.setPoints(_points);
}
}
}).addOnFailureListener(new OnFailureListener() {
public void onFailure(Exception e) {
Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
private void getDetectdetectResult(MLDocumentSkewCorrectionCoordinateInput coordinateData, MLFrame frame) {
try {
correctionTask = analyzer.asyncDocumentSkewCorrect(frame, coordinateData);
} catch (Exception e) {
Log.e(TAG, "The image does not meet the detection requirements.");
}
try {
correctionTask.addOnSuccessListener(new OnSuccessListener<MLDocumentSkewCorrectionResult>() {
@Override
public void onSuccess(MLDocumentSkewCorrectionResult refineResult) {
// The check is successful.
if (refineResult != null && refineResult.getResultCode() == 0) {
corrected = refineResult.getCorrected();
layout_image.setVisibility(View.VISIBLE);
desImageView.setImageBitmap(corrected);
UserDataUtils.saveBitMap(DocumentSkewCorrectionActivity.this, corrected);
} else {
Toast.makeText(DocumentSkewCorrectionActivity.this, "The check fails.", Toast.LENGTH_SHORT).show();
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
Toast.makeText(DocumentSkewCorrectionActivity.this, "The check fails.", Toast.LENGTH_SHORT).show();
}
});
} catch (Exception e) {
Log.e(TAG, "Please set an image.");
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (srcBitmap != null) {
srcBitmap.recycle();
}
if (getCompressesBitmap != null) {
getCompressesBitmap.recycle();
}
if (corrected != null) {
corrected.recycle();
}
if (analyzer != null) {
try {
analyzer.stop();
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
}
}
public static int readPictureDegree(String path) {
int degree = 0;
try {
ExifInterface exifInterface = new ExifInterface(path);
int orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
}
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
return degree;
}
private String getRealPathFromURI(Uri contentURI) {
String result;
result = FileUtils.getFilePathByUri(this, contentURI);
return result;
}
public static Bitmap rotaingImageView(int angle, Bitmap bitmap) {
Matrix matrix = new Matrix();
matrix.postRotate(angle);
Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0,
bitmap.getWidth(), bitmap.getHeight(), matrix, true);
return resizedBitmap;
}
}
activity_document_skew_correction.xml
XML:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000"
tools:ignore="MissingDefaultResource">
<LinearLayout
android:id="@+id/linear_views"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_alignParentBottom="true"
android:layout_marginBottom="10dp"
android:orientation="horizontal">
<RelativeLayout
android:id="@+id/rl_chooseImg"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_centerInParent="true"
android:src="@drawable/add_picture"
app:tint="@color/colorPrimary" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_adjust"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageButton
android:id="@+id/adjust"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_centerInParent="true"
android:background="@drawable/ic_baseline_adjust_24" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_help"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_centerInParent="true"
android:src="@drawable/back"
android:visibility="invisible" />
</RelativeLayout>
</LinearLayout>
<RelativeLayout
android:id="@+id/rl_navigation"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/colorPrimary">
<ImageButton
android:id="@+id/back"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_centerVertical="true"
android:layout_marginLeft="20dp"
android:layout_marginTop="@dimen/icon_back_margin"
android:background="@drawable/back" />
<TextView
android:id="@+id/selectTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="10dp"
android:fontFamily="@font/montserrat_bold"
android:padding="@dimen/hiad_10_dp"
android:text="Select Doc"
android:textColor="@color/white" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerHorizontal="true"
android:layout_marginTop="99dp"
android:layout_marginBottom="100dp">
<com.shea.pygmycollection.customview.DocumentCorrectImageView
android:id="@+id/iv_documetscan"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:padding="20dp"
app:LineColor="@color/colorPrimary" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/layout_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerHorizontal="true"
android:layout_marginTop="99dp"
android:layout_marginBottom="100dp"
android:background="#000000"
android:visibility="gone">
<ImageView
android:id="@+id/des_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:padding="20dp" />
</RelativeLayout>
</RelativeLayout>
Result
Tips and Tricks
Recommended image width and height: 1080 px and 2560 px.
Multi-thread invoking is currently not supported.
The document detection and correction API can only be called by 64-bit apps.
If you are taking Video from a camera or gallery make sure your app has camera and storage permission.
Add the downloaded huawei-hiai-vision-ove-10.0.4.307.aar, huawei-hiai-pdk-1.0.0.aar file to libs folder.
Check dependencies added properly.
Latest HMS Core APK is required.
Min SDK is 21. Otherwise you will get Manifest merge issue.
Conclusion
In this article, we have built an application where that detects the document in the image, and correct the document and it gives a result. We have learnt the following concepts.
1. What is Document skew correction?
2. Feature of Document skew correction.
3. How to integrate Document Skew correction using Huawei HiAI?
4. How to Apply Huawei HiAI?
5. How to build the application?
Reference
Document skew correction
Apply for Huawei HiAI
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Introduction
In this article, we can learn how to correct the document position using Huawei ML Kit of Document Skew Correction feature. This service automatically identifies the location of a document in an image and adjusts the shooting angle to the angle facing the document, even if the document is tilted. This service is majorly used in daily life. For example, if you have captured any document, bank card, driving license etc. from the phone camera with an unfair position, then this feature will adjust the document angle and provides the perfect position.
So, I will provide a series of articles on this Patient Tracking App, in upcoming articles I will integrate other Huawei Kits.
If you are new to this application, follow my previous articles.
https://forums.developer.huawei.com/forumPortal/en/topic/0201902220661040078
https://forums.developer.huawei.com/forumPortal/en/topic/0201908355251870119
https://forums.developer.huawei.com/forumPortal/en/topic/0202914346246890032
Precautions
Ensure that the camera faces document, document occupies most of the image, and the boundaries of the document are in viewfinder.
The best shooting angle is within 30 degrees. If the shooting angle is more than 30 degrees, the document boundaries must be clear enough to ensure better effects.
Requirements
1. Any operating system (MacOS, Linux and Windows).
2. Must have a Huawei phone with HMS 4.0.0.300 or later.
3. Must have a laptop or desktop with Android Studio, Jdk 1.8, SDK platform 26 and Gradle 4.6 and above installed.
4. Minimum API Level 21 is required.
5. Required EMUI 9.0.0 and later version devices.
How to integrate HMS Dependencies
1. First register as Huawei developer and complete identity verification in Huawei developers website, refer to register a Huawei ID.
2. Create a project in android studio, refer Creating an Android Studio Project.
3. Generate a SHA-256 certificate fingerprint.
4. To generate SHA-256 certificate fingerprint. On right-upper corner of android project click Gradle, choose Project Name > Tasks > android, and then click signingReport, as follows.
Note: Project Name depends on the user created name.
5. Create an App in AppGallery Connect.
6. Download the agconnect-services.json file from App information, copy and paste in android Project under app directory, as follows.
7. Enter SHA-256 certificate fingerprint and click Save button, as follows.
Note: Above steps from Step 1 to 7 is common for all Huawei Kits.
8. Click Manage APIs tab and enable ML Kit.
9. Add the below maven URL in build.gradle(Project) file under the repositories of buildscript, dependencies and allprojects, refer Add Configuration.
Java:
maven { url 'http://developer.huawei.com/repo/' }
classpath 'com.huawei.agconnect:agcp:1.4.1.300'
10. Add the below plugin and dependencies in build.gradle(Module) file.
Java:
apply plugin: 'com.huawei.agconnect'
// Huawei AGC
implementation 'com.huawei.agconnect:agconnect-core:1.5.0.300'
// Import the base SDK.
implementation 'com.huawei.hms:ml-computer-vision-documentskew:2.1.0.300'
// Import the document detection/correction model package.
implementation 'com.huawei.hms:ml-computer-vision-documentskew-model:2.1.0.300'
11. Now Sync the gradle.
12. Add the required permission to the AndroidManifest.xml file.
Java:
<uses-permission android:name="android.permission.CAMERA " />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
Let us move to development
I have created a project on Android studio with empty activity let us start coding.
In the DocumentCaptureActivity.kt we can find the business logic.
Java:
class DocumentCaptureActivity : AppCompatActivity(), View.OnClickListener {
private val TAG: String = DocumentCaptureActivity::class.java.getSimpleName()
private var analyzer: MLDocumentSkewCorrectionAnalyzer? = null
private var mImageView: ImageView? = null
private var bitmap: Bitmap? = null
private var input: MLDocumentSkewCorrectionCoordinateInput? = null
private var mlFrame: MLFrame? = null
var imageUri: Uri? = null
var FlagCameraClickDone = false
var fabc: ImageView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_document_capture)
findViewById<View>(R.id.btn_click).setOnClickListener(this)
mImageView = findViewById(R.id.image_result)
// Create the setting.
val setting = MLDocumentSkewCorrectionAnalyzerSetting.Factory()
.create()
// Get the analyzer.
analyzer = MLDocumentSkewCorrectionAnalyzerFactory.getInstance()
.getDocumentSkewCorrectionAnalyzer(setting)
fabc = findViewById(R.id.fab)
fabc!!.setOnClickListener(View.OnClickListener {
FlagCameraClickDone = false
val gallery = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
startActivityForResult(gallery, 1)
})
}
override fun onClick(v: View?) {
this.analyzer()
}
private fun analyzer() {
// Call document skew detect interface to get coordinate data
val detectTask = analyzer!!.asyncDocumentSkewDetect(mlFrame)
detectTask.addOnSuccessListener { detectResult ->
if (detectResult != null) {
val resultCode = detectResult.getResultCode()
// Detect success.
if (resultCode == MLDocumentSkewCorrectionConstant.SUCCESS) {
val leftTop = detectResult.leftTopPosition
val rightTop = detectResult.rightTopPosition
val leftBottom = detectResult.leftBottomPosition
val rightBottom = detectResult.rightBottomPosition
val coordinates: MutableList<Point> = ArrayList()
coordinates.add(leftTop)
coordinates.add(rightTop)
coordinates.add(rightBottom)
coordinates.add(leftBottom)
[email protected](MLDocumentSkewCorrectionCoordinateInput(coordinates))
[email protected]()}
else if (resultCode == MLDocumentSkewCorrectionConstant.IMAGE_DATA_ERROR) {
// Parameters error.
Log.e(TAG, "Parameters error!")
[email protected]() }
else if (resultCode == MLDocumentSkewCorrectionConstant.DETECT_FAILD) {
// Detect failure.
Log.e(TAG, "Detect failed!")
[email protected]()
}
} else {
// Detect exception.
Log.e(TAG, "Detect exception!")
[email protected]()
}
}.addOnFailureListener { e -> // Processing logic for detect failure.
Log.e(TAG, e.message + "")
[email protected]()
}
}
// Show result
private fun displaySuccess(refineResult: MLDocumentSkewCorrectionResult) {
if (bitmap == null) {
this.displayFailure()
return
}
// Draw the portrait with a transparent background.
val corrected = refineResult.getCorrected()
if (corrected != null) {
mImageView!!.setImageBitmap(corrected)
} else {
this.displayFailure()
}
}
private fun displayFailure() {
Toast.makeText(this.applicationContext, "Fail", Toast.LENGTH_LONG).show()
}
private fun setDetectData(input: MLDocumentSkewCorrectionCoordinateInput) {
this.input = input
}
// Refine image
private fun refineImg() {
// Call refine image interface
val correctionTask = analyzer!!.asyncDocumentSkewCorrect(mlFrame, input)
correctionTask.addOnSuccessListener { refineResult ->
if (refineResult != null) {
val resultCode = refineResult.getResultCode()
if (resultCode == MLDocumentSkewCorrectionConstant.SUCCESS) {
this.displaySuccess(refineResult)
} else if (resultCode == MLDocumentSkewCorrectionConstant.IMAGE_DATA_ERROR) {
// Parameters error.
Log.e(TAG, "Parameters error!")
[email protected]()
} else if (resultCode == MLDocumentSkewCorrectionConstant.CORRECTION_FAILD) {
// Correct failure.
Log.e(TAG, "Correct failed!")
[email protected]()
}
} else {
// Correct exception.
Log.e(TAG, "Correct exception!")
[email protected]()
}
}.addOnFailureListener { // Processing logic for refine failure.
[email protected]()
}
}
override fun onDestroy() {
super.onDestroy()
if (analyzer != null) {
try {
analyzer!!.stop()
} catch (e: IOException) {
Log.e(TAG, "Stop failed: " + e.message)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK && requestCode == 1) {
imageUri = data!!.data
try {
bitmap = MediaStore.Images.Media.getBitmap(this.contentResolver, imageUri)
// Create a MLFrame by using the bitmap.
mlFrame = MLFrame.Creator().setBitmap(bitmap).create()
} catch (e: IOException) {
e.printStackTrace()
}
// BitmapFactory.decodeResource(getResources(), R.drawable.new1);
FlagCameraClickDone = true
findViewById<View>(R.id.btn_click).visibility = View.VISIBLE
mImageView!!.setImageURI(imageUri)
}
}
}
In the activity_document_capture.xml we can create the UI screen.
Java:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".mlkit.DocumentCaptureActivity">
<ImageView
android:id="@+id/image_result"
android:layout_width="400dp"
android:layout_height="520dp"
android:paddingLeft="5dp"
android:paddingTop="5dp"
android:src="@drawable/slip"
android:paddingStart="5dp"
android:paddingBottom="5dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="4"
android:layout_alignParentBottom="true"
android:gravity="center_horizontal" >
<ImageView
android:id="@+id/cam"
android:layout_width="0dp"
android:layout_height="41dp"
android:layout_margin="4dp"
android:layout_weight="1"
android:text="sample"
app:srcCompat="@android:drawable/ic_menu_gallery" />
<Button
android:id="@+id/btn_click"
android:layout_width="10dp"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:textSize="19sp"
android:layout_weight="2"
android:textAllCaps="false"
android:text="Capture" />
<ImageView
android:id="@+id/fab"
android:layout_width="0dp"
android:layout_height="42dp"
android:layout_margin="4dp"
android:layout_weight="1"
android:text="sample"
app:srcCompat="@android:drawable/ic_menu_camera" />
</LinearLayout>
</RelativeLayout>
Demo
Tips and Tricks
1. Make sure you are already registered as Huawei developer.
2. Set minSDK version to 21 or later, otherwise you will get AndriodManifest merge issue.
3. Make sure you have added the agconnect-services.json file to app folder.
4. Make sure you have added SHA-256 fingerprint without fail.
5. Make sure all the dependencies are added properly.
Conclusion
In this article, we have learnt to correct the document position using Document Skew Correction feature by Huawei ML Kit. This service automatically identifies the location of a document in an image and adjust the shooting angle to angle facing the document, even if the document is tilted.
I hope you have read this article. If you found it is helpful, please provide likes and comments.
Reference
ML Kit - Document Skew Correction
ML Kit – Training Video