上篇我们大致了解了如何运用OpenCV在Android上进行图片基本特征的提取
Android OpenCV应用篇四:图片特征检测
本篇我们运用一些图片特征提取方法来做一个OCR目标纸片扫描修正的小工具。
参考:《深入OpenCV Android应用开发》
OCR目标纸片扫描修正效果
左边是我们相机实际拍摄图片,右边为处理过后的效果。OK,今天我们就来运用之前的知识来实现这样一个功能。
前言
开始之前,我们先来思考一下我们的大致有哪些步骤:
- 纸面识别
- 轮廓检测
- 角点检测
- 角点归位
- 透视变换
纸面识别
开始之前,为了提高效率,我们将图片进行缩放处理,并进行一次高斯模糊减少噪声:
val scalFactor = calcScaleFactor(srcOrig.rows(), srcOrig.cols())
val src = Mat()
Imgproc.resize(
srcOrig,
src,
Size(srcOrig.cols() / scalFactor.toDouble(), srcOrig.rows() / scalFactor.toDouble())
)
Imgproc.GaussianBlur(src, src, Size(5.0, 5.0), 1.0)
fun calcScaleFactor(rows: Int, cols: Int): Int {
var ideaRows = 0
var ideaCols = 0
if (rows < cols) {
ideaRows = 240
ideaCols = 320
} else {
ideaCols = 240
ideaRows = 320
}
val value = Math.min(rows / ideaRows, cols / ideaCols)
return if (value < 0) {
1
} else {
value
}
}
接下来,我们使用K-均值聚类算法对图像进行处理。(K-均值聚类算法),其效果将图片的背景与纸面有更加清晰的区别。
首先执行包含两个聚类中心的K均值聚类
val samples = Mat(src.rows() * src.cols(), 3, CvType.CV_32F)
for (y in 0 until src.rows()) {
for (x in 0 until src.cols()) {
for (z in 0 until 3)
samples.put(x + y * src.cols(), z, src.get(y, x)[z])
}
}
然后执行K-均值算法
val clusterCount = 2
val lables = Mat()
val attempts = 5
val centers = Mat()
Log.i("kmeans", "--------start--------")
Core.kmeans(
samples,
clusterCount,
lables,
TermCriteria(TermCriteria.MAX_ITER or TermCriteria.EPS, 10000, 0.0001),
attempts,
Core.KMEANS_PP_CENTERS,
centers
)
我们得到了两个聚类中心,并且原始图像中每个像素都有了标签,然后我们利用这两个聚类来检测哪一个是纸面。
找出两个中心的颜色与白色之间的欧式距离(欧式距离),较近的我们认为是纸面。
val center0 = calcWhiteDist(centers.get(0, 0)[0], centers.get(0, 1)[0], centers.get(0, 2)[0])
val center1 = calcWhiteDist(centers.get(1, 0)[0], centers.get(1, 1)[0], centers.get(1, 2)[0])
Log.i("calcWhiteDist", "--------end--------")
val paperCluter = if (center0 < center1) {
0
} else {
1
}
/**
* 计算距离
*/
fun calcWhiteDist(r: Double, g: Double, a: Double): Double {
return Math.sqrt(Math.pow(255 - r, 2.0) + Math.pow(255 - g, 2.0) + Math.pow(255 - a, 2.0))
}
进行图像分割,将前景显示为白色,将背景显示为黑色
val srcRes = Mat(src.size(), src.type())
val srcGray = Mat()
for (y in 0 until src.rows()) {
for (x in 0 until src.cols()) {
val clusterIdx = lables.get(x + y * src.cols(), 0)[0].toInt()
if (clusterIdx == paperCluter) {
srcRes.put(y, x, 0.0, 0.0, 0.0, 255.0)
} else {
srcRes.put(y, x, 255.0, 255.0, 255.0, 255.0)
}
}
}
处理后我们会得到的图像:
对比差别可以看出,我们将灰色的背景全部设置成来纯黑色,将纸面设置成来纯白色。
至此,我们已经可以从图像中识别出背景与纸面
轮廓检测
接下来我们进行轮廓检测。
Log.i("Canny", "--------start--------")
Imgproc.cvtColor(srcRes, srcGray, Imgproc.COLOR_BGR2GRAY)
Imgproc.Canny(srcGray, srcGray, 50.0, 150.0)
Log.i("Canny", "--------end--------")
Log.i("findContours", "--------start--------")
val contours = ArrayList<MatOfPoint>()
val hierarchy = Mat()
Imgproc.findContours(srcGray, contours, hierarchy, Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE)
Log.i("contours", "${contours.size}")
Log.i("findContours", "--------end--------")
Log.i("contourArea", "--------start--------")
var index = 0
var maxim = Imgproc.contourArea(contours[0])
for (contourIdx in 0 until contours.size) {
val temp = Imgproc.contourArea(contours[contourIdx])
if (maxim < temp) {
maxim = temp
index = contourIdx
}
}
Log.i("contourArea", "--------end--------")
val drawing = Mat.zeros(srcRes.size(), CvType.CV_8UC1)
Imgproc.drawContours(drawing, contours, index, Scalar(255.0), 1)
我们进行轮廓检测的处理方式:
- 灰度图像
- Canny边缘检测
- 轮廓检测
- 将轮廓绘制在一张新的图像上
我们不出意料地得到了我们目标区域的轮廓图像:
角点检测
为了能够准确的找到我们轮廓的四个角的顶点,我们这里不直接采用OpenCV提供的角点检测的算法,我们的做法如下:
- 霍夫直线检测
- 计算每两条直线的交点
Log.i("HoughLinesP", "--------start--------")
val lines = Mat()
Imgproc.HoughLinesP(drawing, lines, 1.0, Math.PI / 180, 70, 30.0, 10.0)
println("" + lines.rows() + "---------" + lines.cols())
var corners = ArrayList<Point>()
for (i in 0 until lines.rows()) {
for (j in i + 1 until lines.rows()) {
val line1 = lines.get(i, 0)
val line2 = lines.get(j, 0)
val p = findIntersection(line1, line2)
if (p.x > 0 && p.x < drawing.width() && p.y > 0 && p.y < drawing.height()) {
corners.add(p)
}
}
}
Log.i("HoughLinesP", "--------end--------")
if (corners.size < 4) {
Log.i("------------", "不能完美检测到角点")
return null
}
/**
* 计算两条直线之间的交点
*/
fun findIntersection(line1: DoubleArray, line2: DoubleArray): Point {
Log.i("findIntersection", "--------start--------")
val startX1 = line1[0]
val startY1 = line1[1]
val endX1 = line1[2]
val endY1 = line1[3]
val startX2 = line2[0]
val startY2 = line2[1]
val endX2 = line2[2]
val endY2 = line2[3]
val denominator = (startX1 - endX1) * (startY2 - endY2) - (startY1 - endY1) * (startX2 - endX2)
if (denominator != 0.0) {
val pt = Point()
pt.x =
((startX1 * endY1 - startY1 * endX1) * (startX2 - endX2) - (startX1 - endX1) * (startX2 * endY2 - startY2 * endX2)) / denominator
pt.y =
((startX1 * endY1 - startY1 * endX1) * (startY2 - endY2) - (startY1 - endY1) * (startX2 * endY2 - startY2 * endX2)) / denominator
return pt
}
return Point(-1.0, -1.0)
}
如果最终得到的角点个数小于4个,说明我们没有从图片中成功提取到目标区域。
我们将得到到角点绘制到图片上看一下效果:
corners.forEach {
Imgproc.circle(drawing, it, 5, Scalar(255.0, 255.0, 0.0, 255.0), 10)
}
if(1==1){
return returnBitmap(drawing)
}
角点归位
上面我们成功拿到了四个顶点的角点,但是我们还不确定每个角点的位置,接下来我们就来确定一下每个角点在图像中的位置:
Log.i("setCorners", "--------start--------")
corners = setCorners(corners, scalFactor)
Log.i("setCorners", "--------end--------")
/**
* 确定四个顶点的位置
*/
fun setCorners(corners: ArrayList<Point>, scalFactor: Int): ArrayList<Point> {
var topLeft = Point()
var topRight = Point()
var bottomLeft = Point()
var bottomRight = Point()
var centerX = 0.0
var centerY = 0.0
for (i in 0 until corners.size) {
centerX += corners[i].x / corners.size
centerY += corners[i].y / corners.size
}
for (i in 0 until corners.size) {
val point = corners[i]
if (point.y < centerY && point.x > centerX) {
topRight.x = point.x * scalFactor
topRight.y = point.y * scalFactor
} else if (point.y < centerY && point.x < centerX) {
topLeft.x = point.x * scalFactor
topLeft.y = point.y * scalFactor
} else if (point.y > centerY && point.x < centerX) {
bottomLeft.x = point.x * scalFactor
bottomLeft.y = point.y * scalFactor
} else if (point.y > centerY && point.x > centerX) {
bottomRight.x = point.x * scalFactor
bottomRight.y = point.y * scalFactor
}
}
corners.clear()
corners.add(topLeft)
corners.add(topRight)
corners.add(bottomRight)
corners.add(bottomLeft)
return corners
}
这里由于每个角得到的角点不止一个,我们取每个角其中的一个即可。并且我们将前面计算的缩放因子计算进去,得到角点真正的图像中的位置。
接下来我们进行目标区域尺寸的确定
val top =
Math.sqrt(Math.pow(corners[0].x - corners[1].x, 2.0) + Math.pow(corners[0].y - corners[1].y, 2.0))
val right =
Math.sqrt(Math.pow(corners[1].x - corners[2].x, 2.0) + Math.pow(corners[1].y - corners[2].y, 2.0))
val bottom =
Math.sqrt(Math.pow(corners[3].x - corners[3].x, 2.0) + Math.pow(corners[3].y - corners[2].y, 2.0))
val left =
Math.sqrt(Math.pow(corners[3].x - corners[1].x, 2.0) + Math.pow(corners[3].y - corners[1].y, 2.0))
val quad = Mat.zeros(Size(Math.max(top, bottom), Math.max(left, right)), CvType.CV_8UC3)
透视变换
有了目前图像,以及图像的尺寸,下面我们进行最后一步的处理,透视变换 使得整个纸面占据整个图像。
val resultPoints = ArrayList<Point>()
resultPoints.add(Point(0.0, 0.0))
resultPoints.add(Point(quad.cols().toDouble(), 0.0))
resultPoints.add(Point(quad.cols().toDouble(), quad.rows().toDouble()))
resultPoints.add(Point(0.0, quad.rows().toDouble()))
val cornerPts = Converters.vector_Point2f_to_Mat(corners)
val resultPts = Converters.vector_Point2f_to_Mat(resultPoints)
Log.i("getPerspectiveTransform", "--------start--------")
val transformation = Imgproc.getPerspectiveTransform(cornerPts, resultPts)
Imgproc.warpPerspective(srcOrig, quad, transformation, quad.size())
这里需要注意点的顺序。执行完这一步,我们的工作就基本完成了,接下来我们我们处理得到的图像展示出来看一下:
下面为完整处理代码
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.os.AsyncTask
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.ImageView
import com.hankang.opencv.R
import com.hankang.opencv.base.BasePicturePickActivity
import org.opencv.android.Utils
import org.opencv.core.*
import org.opencv.imgproc.Imgproc
import org.opencv.utils.Converters
class OcrActivity : BasePicturePickActivity() {
lateinit var imageView: ImageView
lateinit var srcOrig: Mat
lateinit var imageShow: ImageView
init {
System.loadLibrary("opencv_java3")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.actiivty_ocr_layout)
imageView = findViewById(R.id.image_view)
imageShow = findViewById(R.id.image2)
findViewById<Button>(R.id.button1).setOnClickListener {
imageBitmap?.apply {
val scalFactor = calcScaleFactor(srcOrig.rows(), srcOrig.cols())
val src = Mat()
Imgproc.resize(
srcOrig,
src,
Size(srcOrig.cols() / scalFactor.toDouble(), srcOrig.rows() / scalFactor.toDouble())
)
Imgproc.GaussianBlur(src, src, Size(5.0, 5.0), 1.0)
getPage(src, scalFactor)
}
}
}
@SuppressLint("StaticFieldLeak")
fun getPage(src: Mat, scalFactor: Int) {
object : AsyncTask<Any, Any, Bitmap?>() {
override fun doInBackground(vararg params: Any): Bitmap? {
Log.i("samples", "--------start--------")
val samples = Mat(src.rows() * src.cols(), 3, CvType.CV_32F)
for (y in 0 until src.rows()) {
for (x in 0 until src.cols()) {
for (z in 0 until 3)
samples.put(x + y * src.cols(), z, src.get(y, x)[z])
}
}
Log.i("samples", "--------end--------")
val clusterCount = 2
val lables = Mat()
val attempts = 5
val centers = Mat()
Log.i("kmeans", "--------start--------")
Core.kmeans(
samples,
clusterCount,
lables,
TermCriteria(TermCriteria.MAX_ITER or TermCriteria.EPS, 10000, 0.0001),
attempts,
Core.KMEANS_PP_CENTERS,
centers
)
Log.i("kmeans", "--------end--------")
Log.i("calcWhiteDist", "--------start--------")
val center0 = calcWhiteDist(centers.get(0, 0)[0], centers.get(0, 1)[0], centers.get(0, 2)[0])
val center1 = calcWhiteDist(centers.get(1, 0)[0], centers.get(1, 1)[0], centers.get(1, 2)[0])
Log.i("calcWhiteDist", "--------end--------")
val paperCluter = if (center0 < center1) {
0
} else {
1
}
val srcRes = Mat(src.size(), src.type())
val srcGray = Mat()
for (y in 0 until src.rows()) {
for (x in 0 until src.cols()) {
val clusterIdx = lables.get(x + y * src.cols(), 0)[0].toInt()
if (clusterIdx != paperCluter) {
srcRes.put(y, x, 0.0, 0.0, 0.0, 255.0)
} else {
srcRes.put(y, x, 255.0, 255.0, 255.0, 255.0)
}
}
}
Log.i("Canny", "--------start--------")
Imgproc.cvtColor(srcRes, srcGray, Imgproc.COLOR_BGR2GRAY)
Imgproc.Canny(srcGray, srcGray, 50.0, 150.0)
Log.i("Canny", "--------end--------")
Log.i("findContours", "--------start--------")
val contours = ArrayList<MatOfPoint>()
val hierarchy = Mat()
Imgproc.findContours(srcGray, contours, hierarchy, Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE)
Log.i("contours", "${contours.size}")
Log.i("findContours", "--------end--------")
Log.i("contourArea", "--------start--------")
var index = 0
var maxim = Imgproc.contourArea(contours[0])
for (contourIdx in 0 until contours.size) {
val temp = Imgproc.contourArea(contours[contourIdx])
if (maxim < temp) {
maxim = temp
index = contourIdx
}
}
Log.i("contourArea", "--------end--------")
val drawing = Mat.zeros(srcRes.size(), CvType.CV_8UC1)
Imgproc.drawContours(drawing, contours, index, Scalar(255.0), 1)
Log.i("HoughLinesP", "--------start--------")
val lines = Mat()
Imgproc.HoughLinesP(drawing, lines, 1.0, Math.PI / 180, 70, 30.0, 10.0)
println("" + lines.rows() + "---------" + lines.cols())
var corners = ArrayList<Point>()
for (i in 0 until lines.rows()) {
for (j in i + 1 until lines.rows()) {
val line1 = lines.get(i, 0)
val line2 = lines.get(j, 0)
val p = findIntersection(line1, line2)
if (p.x > 0 && p.x < drawing.width() && p.y > 0 && p.y < drawing.height()) {
corners.add(p)
}
}
}
Log.i("HoughLinesP", "--------end--------")
if (corners.size < 4) {
Log.i("------------", "不能完美检测到角点")
return null
}
Log.i("setCorners", "--------start--------")
corners = setCorners(corners, scalFactor)
Log.i("setCorners", "--------end--------")
val top =
Math.sqrt(Math.pow(corners[0].x - corners[1].x, 2.0) + Math.pow(corners[0].y - corners[1].y, 2.0))
val right =
Math.sqrt(Math.pow(corners[1].x - corners[2].x, 2.0) + Math.pow(corners[1].y - corners[2].y, 2.0))
val bottom =
Math.sqrt(Math.pow(corners[3].x - corners[3].x, 2.0) + Math.pow(corners[3].y - corners[2].y, 2.0))
val left =
Math.sqrt(Math.pow(corners[3].x - corners[1].x, 2.0) + Math.pow(corners[3].y - corners[1].y, 2.0))
val quad = Mat.zeros(Size(Math.max(top, bottom), Math.max(left, right)), CvType.CV_8UC3)
val resultPoints = ArrayList<Point>()
resultPoints.add(Point(0.0, 0.0))
resultPoints.add(Point(quad.cols().toDouble(), 0.0))
resultPoints.add(Point(quad.cols().toDouble(), quad.rows().toDouble()))
resultPoints.add(Point(0.0, quad.rows().toDouble()))
val cornerPts = Converters.vector_Point2f_to_Mat(corners)
val resultPts = Converters.vector_Point2f_to_Mat(resultPoints)
Log.i("getPerspectiveTransform", "--------start--------")
val transformation = Imgproc.getPerspectiveTransform(cornerPts, resultPts)
Imgproc.warpPerspective(srcOrig, quad, transformation, quad.size())
Imgproc.cvtColor(quad, quad, Imgproc.COLOR_BGR2RGBA)
Log.i("getPerspectiveTransform", "--------end--------")
return returnBitmap(quad)
}
override fun onPostExecute(result: Bitmap?) {
super.onPostExecute(result)
imageShow.setImageBitmap(result)
}
}.execute()
}
fun returnBitmap(src: Mat): Bitmap {
val bitmap = Bitmap.createBitmap(src.cols(), src.rows(), Bitmap.Config.ARGB_8888)
Utils.matToBitmap(src, bitmap)
return bitmap
}
/**
* 确定四个顶点的位置
*/
fun setCorners(corners: ArrayList<Point>, scalFactor: Int): ArrayList<Point> {
var topLeft = Point()
var topRight = Point()
var bottomLeft = Point()
var bottomRight = Point()
var centerX = 0.0
var centerY = 0.0
for (i in 0 until corners.size) {
centerX += corners[i].x / corners.size
centerY += corners[i].y / corners.size
}
for (i in 0 until corners.size) {
val point = corners[i]
if (point.y < centerY && point.x > centerX) {
topRight.x = point.x * scalFactor
topRight.y = point.y * scalFactor
} else if (point.y < centerY && point.x < centerX) {
topLeft.x = point.x * scalFactor
topLeft.y = point.y * scalFactor
} else if (point.y > centerY && point.x < centerX) {
bottomLeft.x = point.x * scalFactor
bottomLeft.y = point.y * scalFactor
} else if (point.y > centerY && point.x > centerX) {
bottomRight.x = point.x * scalFactor
bottomRight.y = point.y * scalFactor
}
}
corners.clear()
corners.add(topLeft)
corners.add(topRight)
corners.add(bottomRight)
corners.add(bottomLeft)
return corners
}
fun exists(contours: ArrayList<Point>, pt: Point): Boolean {
for (i in 0 until contours.size) {
if (Math.sqrt(Math.pow(contours[i].x - pt.x, 2.0)) + Math.pow(contours[i].y - pt.y, 2.0) < 10) {
return true
}
}
return false
}
fun findIntersection(line1: DoubleArray, line2: DoubleArray): Point {
Log.i("findIntersection", "--------start--------")
val startX1 = line1[0]
val startY1 = line1[1]
val endX1 = line1[2]
val endY1 = line1[3]
val startX2 = line2[0]
val startY2 = line2[1]
val endX2 = line2[2]
val endY2 = line2[3]
val denominator = (startX1 - endX1) * (startY2 - endY2) - (startY1 - endY1) * (startX2 - endX2)
if (denominator != 0.0) {
val pt = Point()
pt.x =
((startX1 * endY1 - startY1 * endX1) * (startX2 - endX2) - (startX1 - endX1) * (startX2 * endY2 - startY2 * endX2)) / denominator
pt.y =
((startX1 * endY1 - startY1 * endX1) * (startY2 - endY2) - (startY1 - endY1) * (startX2 * endY2 - startY2 * endX2)) / denominator
return pt
}
return Point(-1.0, -1.0)
}
/**
* 计算距离
*/
fun calcWhiteDist(r: Double, g: Double, a: Double): Double {
return Math.sqrt(Math.pow(255 - r, 2.0) + Math.pow(255 - g, 2.0) + Math.pow(255 - a, 2.0))
}
override fun onImageLoadSuccess() {
imageView.setImageBitmap(imageBitmap)
imageBitmap?.apply {
srcOrig = Mat(height, width, CvType.CV_8UC4)
Utils.bitmapToMat(imageBitmap, srcOrig)
}
}
fun calcScaleFactor(rows: Int, cols: Int): Int {
var ideaRows = 0
var ideaCols = 0
if (rows < cols) {
ideaRows = 240
ideaCols = 320
} else {
ideaCols = 240
ideaRows = 320
}
val value = Math.min(rows / ideaRows, cols / ideaCols)
return if (value < 0) {
1
} else {
value
}
}
}