目录
效果展示
关键代码
<?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>
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:gravity="center_horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:text="源图像"
android:textColor="@color/black"
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageView
android:src="@drawable/ic_img"
android:layout_width="match_parent"
android:layout_height="300dp" />
<TextView
android:text="进行二值化处理,并通过闭操作去除多余噪点"
android:textColor="@color/black"
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/img_binary"
android:layout_width="match_parent"
android:layout_height="300dp" />
<TextView
android:text="开操作,让数字联结到一起,方便查找轮廓"
android:textColor="@color/black"
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/img_coupling"
android:layout_width="match_parent"
android:layout_height="300dp" />
<TextView
android:text="查找出信息所在的轮廓"
android:textColor="@color/black"
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/img_contours"
android:layout_width="match_parent"
android:layout_height="300dp" />
<TextView
android:text="找出信息所在的外接矩形"
android:textColor="@color/black"
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/img_numrect"
android:layout_width="match_parent"
android:layout_height="300dp" />
<TextView
android:text="最后识别结果如下"
android:textColor="@color/black"
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/img_user"
android:layout_marginTop="20dp"
android:layout_width="match_parent"
android:layout_height="300dp" />
<TextView
android:id="@+id/tv_ocrinfo"
android:layout_marginBottom="20dp"
android:textColor="@color/black"
android:textSize="20sp"
android:layout_gravity="center_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</ScrollView>
</layout>
class MainActivity : AppCompatActivity() {
private lateinit var activityMainBinding: ActivityMainBinding
private var ocrDataFilePath = "" //数据识别的文件路径
private var ocrInfoText:StringBuilder = StringBuilder()
@SuppressLint("HandlerLeak")
private val handler = object : Handler() {
override fun handleMessage(msg: Message) {
when(msg.what){
0->{
val info:String = msg.obj as String
ocrInfoText.appendLine(info)
}
1->{
//识别成功
activityMainBinding.tvOcrinfo.text = ocrInfoText.toString()
}
}
super.handleMessage(msg)
}
}
private val mLoaderCallback: BaseLoaderCallback = object : BaseLoaderCallback(this) {
override fun onManagerConnected(status: Int) {
when (status) {
SUCCESS -> {
//导入源图像
val bitmap: Bitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_img)
val src = Mat()
Utils.bitmapToMat(bitmap, src) //将bitmap转换为Mat
val thresholdImage = Mat(src.size(),src.type()) //这个二值图像用于找出关键信息的图像
val thresholdImageOcr = Mat(src.size(),src.type()) //这个二值图像用于识别信息
val cannyImage = Mat(src.size(),src.type())
//将图像转换为灰度图像
Imgproc.cvtColor(src, thresholdImage, Imgproc.COLOR_RGBA2GRAY)
//将图像转换为边缘二值图像
Imgproc.threshold(thresholdImage,thresholdImageOcr,140.0,255.0, Imgproc.THRESH_BINARY)
Imgproc.threshold(thresholdImage,thresholdImage,100.0,255.0, Imgproc.THRESH_BINARY)
//闭操作去掉多余的杂点
var kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, Size(bitmap.width * 0.002, bitmap.width * 0.002)) //获取结构元素
Imgproc.morphologyEx(thresholdImage, thresholdImage, Imgproc.MORPH_CLOSE, kernel)
//显示当前阶段效果图像
val binaryBitmap = Bitmap.createBitmap(bitmap.width,bitmap.height,bitmap.config)
Utils.matToBitmap(thresholdImageOcr,binaryBitmap)
activityMainBinding.imgBinary.setImageBitmap(binaryBitmap)
//开操作让数字联结到一起方便查出数字的位置
kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, Size(bitmap.width * 0.05, bitmap.width * 0.036)) //获取结构元素
Imgproc.morphologyEx(thresholdImage, cannyImage, Imgproc.MORPH_OPEN, kernel)
//显示当前阶段效果图像
val couplingBitmap = Bitmap.createBitmap(bitmap.width,bitmap.height,bitmap.config)
Utils.matToBitmap(cannyImage,couplingBitmap)
activityMainBinding.imgCoupling.setImageBitmap(couplingBitmap)
//查找边缘
Imgproc.Canny(cannyImage, cannyImage, 100.0, 200.0,3)
//膨胀让边缘更明显
kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, Size(bitmap.width * 0.0036, bitmap.width * 0.0036)) //获取结构元素
Imgproc.dilate(cannyImage, cannyImage, kernel) //膨胀操作
//显示当前阶段效果图像
val contoursBitmap = Bitmap.createBitmap(bitmap.width,bitmap.height,bitmap.config)
Utils.matToBitmap(cannyImage,contoursBitmap)
activityMainBinding.imgContours.setImageBitmap(contoursBitmap)
val hierarchy = Mat()
val contours: ArrayList<MatOfPoint> = ArrayList()
//轮廓发现
Imgproc.findContours(
cannyImage,
contours,
hierarchy,
Imgproc.RETR_EXTERNAL,
Imgproc.CHAIN_APPROX_NONE
)
//找出信息所在的轮廓
val allRect = ArrayList<Rect>()
contours.forEach {
val rect = Imgproc.boundingRect(it)
//left为0的是不正确的
if(rect.x != 0 && rect.x + rect.width != src.width()){
//对区域进行小量的扩充,方便识别数据
rect.x = rect.x - 10
rect.y = rect.y - 10
rect.width = rect.width + 20
rect.height = rect.height + 20
allRect.add(rect)
}
}
//对包括头像的全部区域进行排序(头像排在最后)
allRect.sortByDescending { -(it.x + it.width) }
//展示头像区域
val userIconRect = allRect[allRect.size - 1]
val userBitmap = Bitmap.createBitmap(bitmap,userIconRect.x,userIconRect.y,userIconRect.width,userIconRect.height)
activityMainBinding.imgUser.setImageBitmap(userBitmap)
//剔除头像区域(方便后面的OCR识别)
val infoRect = ArrayList<Rect>(allRect.take(allRect.size - 1))
//对矩形轮廓进行排序(姓名往下依次排列)
val c1: Comparator<Rect> = Comparator { o1, o2 ->
if (abs(o1.y - o2.y) <= 10) {
//可容误差为10(因为有些识别的区域y轴存在小量偏移,比如性别和民族)
//当y轴在同一位置的时候,比较x轴
o1.x - o2.x
} else {
o1.y - o2.y
}
}
infoRect.sortWith(c1)
//画出信息所在的位置
val showInfoRectImg = Mat()
src.copyTo(showInfoRectImg)
infoRect.forEach {
Imgproc.rectangle(showInfoRectImg,it,Scalar(0.0, 255.0, 0.0, 255.0),(bitmap.width * 0.003).toInt(), 8)
}
Utils.matToBitmap(showInfoRectImg,bitmap)
activityMainBinding.imgNumrect.setImageBitmap(bitmap)
ocrInfo(binaryBitmap,infoRect)
//释放资源
thresholdImage.release()
thresholdImageOcr.release()
cannyImage.release()
src.release()
showInfoRectImg.release()
}
else -> {
super.onManagerConnected(status)
}
}
}
}
/**
* 根据信息所在的位置,识别信息
*/
private fun ocrInfo(dstBitmap: Bitmap, infoRect: ArrayList<Rect>) {
thread {
initOcrData()
if(!TextUtils.isEmpty(ocrDataFilePath)){
// 开始调用Tess函数对图像进行识别
val tessBaseAPI = TessBaseAPI()
tessBaseAPI.setDebug(true)
tessBaseAPI.init(ocrDataFilePath, "chi_sim")
// tessBaseAPI.setVariable(TessBaseAPI.VAR_CHAR_WHITELIST, "0123456789") // 识别白名单
tessBaseAPI.setVariable(TessBaseAPI.VAR_CHAR_BLACKLIST, "ABCDEFGHIJKLMNOPQRSTUVWYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+=-[]}{;:'\"\\|~`,./<>?…】丨") // 识别黑名单
tessBaseAPI.setPageSegMode(TessBaseAPI.PageSegMode.PSM_SINGLE_WORD)//设置识别模式
infoRect.forEach {
val ocrBitmap = Bitmap.createBitmap(dstBitmap,it.x,it.y,it.width,it.height)
//当识别的字为单个字符的时候,切换识别模式为单字符的,识别的比较准确(这里设置宽高比只要小于1.5就是单字符)
//用大的值除以小的值,这样才不至于产生小于0的值
val maxVal = max(ocrBitmap.width,ocrBitmap.height).toDouble()
val minVal = min(ocrBitmap.width,ocrBitmap.height).toDouble()
if(maxVal / minVal <= 1.5){
tessBaseAPI.setPageSegMode(TessBaseAPI.PageSegMode.PSM_SINGLE_WORD)//设置识别模式
}else{
tessBaseAPI.setPageSegMode(TessBaseAPI.PageSegMode.PSM_AUTO)//设置识别模式
}
tessBaseAPI.setImage(ocrBitmap)//设置需要识别图片的bitmap
val number = tessBaseAPI.utF8Text
val msg = Message()
msg.what = 0
msg.obj = number
handler.sendMessage(msg)
}
tessBaseAPI.end()
handler.sendEmptyMessage(1)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityMainBinding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
}
/**
* 加载数据识别的文件
*/
private fun initOcrData() {
val ocrDataStream = resources.openRawResource(R.raw.chi_sim)
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.let {
ocrDataFilePath = it.absolutePath
val ocrDataFile = File("${ocrDataFilePath}${File.separator}tessdata${File.separator}chi_sim.traineddata")
if(!ocrDataFile.exists()){
FileIOUtils.writeFileFromIS(ocrDataFile,ocrDataStream)
}
}
}
override fun onResume() {
super.onResume()
if (!OpenCVLoader.initDebug()) {
OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_0_0, this, mLoaderCallback)
} else {
mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS)
}
}
}
案例源码
https://gitee.com/itfitness/opencv-idcard-ocr