前言
说到车牌号识别,国内用的比较广泛的就是 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


被折叠的 条评论
为什么被折叠?



