Android OpenCV应用篇五:图片扫描器


上篇我们大致了解了如何运用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)
                }

可以看出,我们成功找到了纸面到四个顶点。至此,我们已经完成了80%的工作,成功的在图片中找到,并提取出目标区域的有效信息。下面两步,我们将进行相关计算与变换,将目标区域拿出并摆正。

角点归位

上面我们成功拿到了四个顶点的角点,但是我们还不确定每个角点的位置,接下来我们就来确定一下每个角点在图像中的位置:

                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
        }
    }

}



OK,以上就是本篇分享的主要内容了

如果对你有帮助但话请点赞关注

未完待续。。。




发布了5 篇原创文章 · 获赞 8 · 访问量 816

猜你喜欢

转载自blog.csdn.net/qq_20158897/article/details/100581579