Android 车牌号识别 HyperLPR3 如何使用CameraX

前言

说到车牌号识别,国内用的比较广泛的就是 HyperLPR3 了,可以识别绝大部分的通用号牌。
它有 Android开发 SDK
https://github.com/szad670401/HyperLPR
你可以下载APK来测试,不过大家用它肯定不只是直接用它的demo apk, 而是要集成到自己的应用中。


笔者下下来源代码,因为原有代码版本太低的原因,放到最新的Android开发环境中是编译不通的。

顺便吐槽一下,貌似目前绝大部分的网上整体项目源代码在最新Android环境中是编译不过的,没办法,安卓的开发环境迭代变化太快,很多项目文件的语法结构都变动很大。
只能建立新环境后手工把主要代码 copy 过来后调整。网上说也可以进行版本迁移,但是迁移过程会出现很多问题,等你花时间解决迁移过程中各种奇奇怪怪的问题。还不如直接把部分代码copy到新环境来得快。
不像服务端有Spring全家桶,稳定很多年了,设计上不需要动太多脑筋,本身安卓的开发就比服务端更麻烦一些,需要更强的设计思维,加上IDE框架经常剧烈变动,导致各种文献资料过时,
这一点难住了很多初学者。好多人好不容易在网上搜了点资料,结果放到自己机器上一跑,不是这里出问题就是那里出问题,半天捣鼓不出结果来。这也是目前安卓的开发人员越来越少的原因。

经过仔细调整后,HyperLPR3 Demo 可以在最新的环境中运行了。
能跑是能跑了,也能集成到自己的应用里面。
但是有一点不爽的是这个Demo 是,里面的实时摄像头CameraPreviews这个文件,发现他使用的是Camera1

里面一大堆Deprecated的类,满屏的删除线,虽然也能正常用,用户也不知道,但CameraX在Android5.0就推出了,至今10年了,现在还用Camera1,如果你跟我一样是个完美主义者,这有点不可忍受。

为了以后的扩展,还是把它改为用CameraX好。

CameraX主要流程框架

虽然CameraX 复杂一些,但是能更灵活能应付更多的场景。我们可以把复杂的部分封装到公共代码里去,保持使用起来简单就行了。像demo代码一样一长串的并不好。

其实就是要在实时的视频流里截取一个图片识别,只不过这个识别过程是连续的。

按照这样的要求我们就开始改造实时识别部分。

先画一个基本的界面:camera_plate.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">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        tools:context=".CameraMainActivity">


        <androidx.camera.view.PreviewView
            android:id="@+id/previewView"
            android:layout_width="match_parent"
            android:layout_height="400dp"
            android:layout_gravity="center"
            android:layout_marginStart="10dp"
            android:layout_marginTop="10dp"
            android:layout_marginEnd="10dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />


        <TextView
            android:id="@+id/lblResult"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_marginStart="10dp"
            android:layout_marginTop="30dp"
            android:gravity="center_vertical"
            android:text="@string/recognition_result"
            android:textSize="16sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintBottom_toTopOf="@+id/line_result">

        </TextView>


        <LinearLayout
            android:id="@+id/line_result"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="10dp"
            android:layout_marginEnd="10dp"
            android:layout_marginBottom="20dp"
            android:autofillHints=""
            android:gravity="top"
            android:inputType="textMultiLine"
            android:orientation="horizontal"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintBottom_toTopOf="@+id/line_button">

            <TextView
                android:layout_width="60dp"
                android:layout_height="wrap_content"
                android:labelFor="@+id/plateNo"
                android:text="@string/plate_no"
                android:textSize="16sp" />

            <EditText
                android:id="@+id/plateNo"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="5dp"
                android:layout_weight="1"
                android:autofillHints=""
                android:inputType="textCapCharacters"
                android:text=""
                android:textSize="16sp" />
        </LinearLayout>


        <LinearLayout
            android:id="@+id/line_button"
            style="?android:attr/buttonBarStyle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:layout_marginBottom="20dp"
            android:gravity="center_horizontal"
            android:orientation="horizontal"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent">


            <com.google.android.material.button.MaterialButton
                android:id="@+id/btnOk"
                style="?android:attr/button"
                android:layout_width="90dp"
                android:layout_height="@dimen/button_height"
                android:gravity="center"
                android:insetTop="0dp"
                android:insetBottom="0dp"
                android:text="@string/ok"
                android:textColor="@color/white" />

            <com.google.android.material.button.MaterialButton
                android:id="@+id/btnBack"
                style="?android:attr/button"
                android:layout_width="90dp"
                android:layout_height="@dimen/button_height"
                android:layout_marginStart="20dp"
                android:gravity="center"
                android:insetTop="0dp"
                android:insetBottom="0dp"
                android:text="@string/back"
                android:textColor="@color/white" />
        </LinearLayout>

        <ProgressBar
            android:id="@+id/progressBar"
            style="?android:attr/progressBarStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="invisible"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

然后编写主要框架部分,我们在基类BaseActivity中封装主要流程框架


import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Rational;
import android.widget.Toast;

import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.Preview;
import androidx.camera.core.UseCaseGroup;
import androidx.camera.core.ViewPort;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import com.bob.app.R;
import com.google.common.util.concurrent.ListenableFuture;

import java.util.concurrent.ExecutionException;

public abstract class BaseActivity extends AppCompatActivity {
    private static final int REQUEST_CODE_PERMISSIONS = 10;
    private static final String[] REQUIRED_PERMISSIONS = new String[]{"android.permission.CAMERA"};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    protected void checkCameraPermission() {
        if (allPermissionsGranted()) {
            startCamera();
        } else {
            ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
        }
    }

    private boolean allPermissionsGranted() {
        for (String permission : REQUIRED_PERMISSIONS) {
            if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
                return false;
            }
        }
        return true;
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                startCamera();
            } else {
                Toast.makeText(this, "请允许相机权限", Toast.LENGTH_SHORT).show();
                finish();
            }
        }
    }

    protected void startCamera() {
        ListenableFuture<ProcessCameraProvider> listenableFuture = ProcessCameraProvider.getInstance(this);
        listenableFuture.addListener(() -> {
            try {
                ProcessCameraProvider cameraProvider = listenableFuture.get();
                bindPreview(cameraProvider);
            } catch (ExecutionException | InterruptedException e) {
                // Handle any errors
            }
        }, ContextCompat.getMainExecutor(this));
    }

    private void bindPreview(ProcessCameraProvider cameraProvider) {
        CameraSelector cs = new CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build();
        PreviewView pv = findViewById(R.id.previewView);
        int rotation = pv.getDisplay().getRotation();
        Preview preview = new Preview.Builder().setTargetRotation(rotation).build();
        preview.setSurfaceProvider(pv.getSurfaceProvider());
        ImageAnalysis ia = new ImageAnalysis.Builder().setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).setTargetRotation(rotation).build();
        ia.setAnalyzer(ContextCompat.getMainExecutor(this), this::processImage);//处理单帧图像
        Rational aspectRatio = new Rational(pv.getWidth(), pv.getHeight());
        ViewPort viewPort = new ViewPort.Builder(aspectRatio, rotation).build();
        UseCaseGroup useCaseGroup = new UseCaseGroup.Builder().addUseCase(preview).addUseCase(ia).setViewPort(viewPort).build();
        cameraProvider.unbindAll();
        cameraProvider.bindToLifecycle(this, cs, useCaseGroup);
    }

    protected void processImage(ImageProxy imageProxy) {
        //子类覆盖此方法处理图像。
    }

    protected void openGallery(ActivityResultLauncher<Intent> galleryLauncher) {//打开相册
        Intent intent = new Intent(Intent.ACTION_PICK);
        intent.setType("image/*");
        galleryLauncher.launch(intent);
    }
}

说明:

按照惯例先检查相机权限,这个是常规性动作不细说,检查通过后就开始 startCamera了

在bindPreview方法里, 我们构建分析器,然后这个分析器执行分析单帧图像的动作。

因为是框架,我们这里只留一个虚拟方法待子类覆盖。

其它代码都是常规辅助操作。

具体识别动作

然后我们看在具体的子类中如何使用它。


import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;

import androidx.camera.core.ImageProxy;

import com.bob.app.common.BaseActivity;
import com.bob.app.databinding.CameraPlateBinding;
import com.hyperai.hyperlpr3.HyperLPR3;
import com.hyperai.hyperlpr3.bean.Plate;

public class CameraPlateActivity extends BaseActivity {
    private CameraPlateBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = CameraPlateBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        checkCameraPermission();//check permission
        binding.btnBack.setOnClickListener(v -> finish());
        binding.btnOk.setOnClickListener(v -> {
            String plateNo = binding.plateNo.getText().toString();
            if (!TextUtils.isEmpty(plateNo)) {
                Intent intent = new Intent();
                intent.putExtra("plateNo", plateNo);
                setResult(RESULT_OK, intent);
                finish();
            }
        });
    }

    @Override
    protected void processImage(ImageProxy imageProxy) {
        Bitmap bitmap = Util.imageProxyToBitmap(imageProxy);
        if (bitmap != null) {
            Plate[] plates = HyperLPR3.getInstance().plateRecognition(
                    bitmap, HyperLPR3.CAMERA_ROTATION_270, HyperLPR3.STREAM_BGRA);
            for (Plate plate : plates) {
                Log.i("PLATE RECOGNITION: ", plate.toString());
                String plateNo = "[" + HyperLPR3.PLATE_TYPE_MAPS[plate.getType()] + "]" + plate.getCode();
                binding.plateNo.setText(plateNo);
            }
        }
        imageProxy.close();
    }
}

说明,我们只要在子类中调用checkPermission即可。

获得权限后会自动打开摄像头,子类中只需要覆盖处理图像的方法processImage即可

其中 imageProxyToBitmap 是从视频流获取一个Bitmap图像的工具方法,内容如下。看不懂没关系直接用就行了。

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.YuvImage;

import androidx.camera.core.ImageProxy;

import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;

public class Util {

    public static Bitmap imageProxyToBitmap(ImageProxy imageProxy) {
        ImageProxy.PlaneProxy[] planes = imageProxy.getPlanes();
        if (planes.length >= 3) {
            ByteBuffer yBuffer = planes[0].getBuffer();
            ByteBuffer uBuffer = planes[1].getBuffer();
            ByteBuffer vBuffer = planes[2].getBuffer();

            int ySize = yBuffer.remaining();
            int uSize = uBuffer.remaining();
            int vSize = vBuffer.remaining();

            byte[] nv21 = new byte[ySize + uSize + vSize];
            yBuffer.get(nv21, 0, ySize);
            vBuffer.get(nv21, ySize, vSize);
            uBuffer.get(nv21, ySize + vSize, uSize);

            YuvImage yuvImage = new YuvImage(nv21, ImageFormat.NV21, imageProxy.getWidth(), imageProxy.getHeight(), null);
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            yuvImage.compressToJpeg(new android.graphics.Rect(0, 0, yuvImage.getWidth(), yuvImage.getHeight()), 100, out);
            byte[] jpegBytes = out.toByteArray();
            return BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.length);
        } else {
            return null;
        }
    }
}

下面的内容就是 HyperLPR3 的具体识别方法了。我们这里不涉及具体的算法。只是封装使用而已。可以看到经过我们封装处理后,自己的具体类的代码很简练。只要实现这个processImage方法即可,车牌识别只是其中一种应用,你可以用这个方法干任何事情。

跑起来的结果是这样:

注意使用前要先引入CameraX的库。

总结

HyperLPR3 的实时识别使用的Camera1,改造成CameraX的方法: 检查权限,然后打开Camera, 然后绑定Preview后构建一个图像分析器ImageAnalysis, 在图像分析器里调用具体的单帧图片处理方法。流程可以封装在公共代码里,具体的执行代码只需实现图片处理方法即可。

完整源代码

完整的项目代码在这里。可以直接在最新的IDE环境打开,支持 API35

https://download.csdn.net/download/usabcd2/89974180

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值