Kotlin 、RxJava 以及传统的机器学习在手机质检上的应用

本文涉及的产品
阿里云百炼推荐规格 ADB PostgreSQL,4核16GB 100GB 1个月
通用文字识别,通用文字识别 200次/月
小语种识别,小语种识别 200次/月
简介: Kotlin 、RxJava 以及传统的机器学习在手机质检上的应用

一. 业务背景



隐私清除是手机质检的重要一环,我们回收的手机在经过自动化质检完成后,会对手机进行隐私清除。


在进行隐私清除之前,需要确保手机退出云服务的帐号。例如 iPhone 手机需要退出 iCloud ,华为、小米等手机都要退出对应的云服务。否则会造成隐私数据的泄漏的风险,也会让后续购买此手机的用户无法享受到云服务的功能。


因此,帐号检测是一项很重要的功能。本节以 Android 手机的帐号检测是否退出为例,主要是针对华为、小米等有比较明显的特征的手机,通过图像预处理、OCR 进行识别。

我们的隐私清除工具是一个桌面端程序,运行在 Ubuntu 系统上。


image.png

隐私清除1.jpg


对于 Android 手机,桌面工具通过 adb 命令将隐私清除 App 安装到手机上,然后二者通过 WebSocket 进行通信,做手机的隐私清除。

image.png

隐私清除2.jpg


二. 设计思路



在做帐号检测这个功能之前,我尝试过很多办法来判断帐号是否退出,例如找相关的 adb 命令,或者对应厂商的 API,都没有很好的效果。经过不断摸索后,采用如下的方式:


  • 使用 adb 命令修改手机的休眠时间,确保手机一段时间内不会熄屏。
  • 使用 adb 命令跳转到系统设置页面(不同的手机使用的命令略有不同)
  • 使用 adb 命令对当前页面进行截图
  • 使用 adb 命令将图片传输到桌面端的机器
  • 通过程序对原图进行裁剪,保留原先的40%
  • 对裁剪的图片进行图像二值化处理(不同的手机采用不同的二值化算法)
  • 调用 OCR 进行特征字符串的识别。
  • 比对字符串相似度,最终确定帐号是否退出。


image.png

帐号检测.png


这种方式在华为、小米手机上取得很好的效果。


三. 代码实现以及踩过的坑



核心代码


核心的代码使用 RxJava 将上述所有过程串联起来,每一个过程是一个 map 操作,下面展示检测华为手机的帐号是否退出:

object HuaweiDetect : IDetect {
    val logger: Logger = LoggerFactory.getLogger(this.javaClass)
    val list by lazy {
        arrayOf("华为帐号、云空间、应用市场等"
            ,"华为帐号、付款与账单、云空间等"
            ,"华为帐号、云空间"
            ,"华为帐号、付款与账单")
    }
    override fun detectUserAccount(serialNumber:String,detail:String): Observable<Boolean> {
        val file = File(detectAccountPath)
        if (!file.exists()) {
            file.mkdir()
        }
        val timeoutCmd = CommandBuilder.buildSudoCommand("aihuishou","$adbLocation -s $serialNumber shell settings put system screen_off_timeout 600000")
        CommandExecutor.executeSync(timeoutCmd,appender = object : Appender {
            override fun appendErrText(text: String) {
                println(text)
            }
            override fun appendStdText(text: String) {
                println(text)
            }
        }).getExecutionResult()
        val cmd = CommandBuilder.buildSudoCommand("aihuishou","$adbLocation -s $serialNumber shell am start -S com.android.settings/.HWSettings")
        val fileName = "${serialNumber}-${detail}.png"
        return CommandExecutor.execute(cmd)
                .asObservable()
                .delay(2, TimeUnit.SECONDS)
                .map {
                    val screencapCmd = CommandBuilder.buildSudoCommand("aihuishou","$adbLocation -s $serialNumber shell screencap -p /sdcard/$fileName")
                    CommandExecutor.executeSync(screencapCmd, appender = object : Appender {
                        override fun appendErrText(text: String) {
                            println(text)
                        }
                        override fun appendStdText(text: String) {
                            println(text)
                        }
                    }).getExecutionResult()
                }
                .map {
                    val pullCmd = CommandBuilder.buildSudoCommand("aihuishou","$adbLocation -s $serialNumber pull /sdcard/$fileName ${detectAccountPath}/${fileName}")
                    CommandExecutor.executeSync(pullCmd, appender = object : Appender {
                        override fun appendErrText(text: String) {
                            println(text)
                        }
                        override fun appendStdText(text: String) {
                            println(text)
                        }
                    }).getExecutionResult()
                    fileName
                }
                .map {
                    val input = File("$detectAccountPath/$it")
                    val image = ImageIO.read(input)
                    val width = image.width
                    val height = image.height
                    return@map imageCutByRectangle(image, 0, 0, width, (height * 0.4).toInt())
                }
                .map { // 二值化
                    binaryForHuawei(it)
                }
                .map{
                    val ocrValue = newTesseract().doOCR(it)
                    logger.info("ocrValue = $ocrValue")
                    ocrValue
                }
                .map { ocrValue->
                    if (ocrValue.contains("华为帐号、云空间、应用市场等")
                            || ocrValue.contains("华为帐号、付款与账单、云空间等")
                            || ocrValue.contains("华为帐号、云空间")
                            || ocrValue.contains("华为帐号、付款与账单")) {
                        return@map true
                    } else {
                        val array = ocrValue.split("\n".toRegex())
                        array.map {
                            it.replace("\\s+".toRegex(),"")
                        }.toList().forEach{ s->
                            for (item in list) {
                                val d = levenshtein(s,item) // 字符串相似度比较
                                if (d>=0.7) {
                                    return@map true
                                }
                            }
                        }
                        return@map false
                    }
                }
    }
}


其中,imageCutByRectangle() 用于裁剪图片

/**
 * 矩形裁剪,设定起始位置,裁剪宽度,裁剪长度
 * 裁剪范围需小于等于图像范围
 * @param image
 * @param xCoordinate
 * @param yCoordinate
 * @param xLength
 * @param yLength
 * @return
 */
fun imageCutByRectangle(
    image: BufferedImage,
    xCoordinate: Int,
    yCoordinate: Int,
    xLength: Int,
    yLength: Int
): BufferedImage {
    //判断x、y方向是否超过图像最大范围
    var xLength = xLength
    var yLength = yLength
    if (xCoordinate + xLength >= image.width) {
        xLength = image.width - xCoordinate
    }
    if (yCoordinate + yLength >= image.height) {
        yLength = image.height - yCoordinate
    }
    val resultImage = BufferedImage(xLength, yLength, image.type)
    for (x in 0 until xLength) {
        for (y in 0 until yLength) {
            val rgb = image.getRGB(x + xCoordinate, y + yCoordinate)
            resultImage.setRGB(x, y, rgb)
        }
    }
    return resultImage
}


binaryForHuawei() 用于图像二值化。


图像二值化( Image Binarization)就是将图像上的像素点的灰度值设置为0或255,也就是将整个图像呈现出明显的黑白效果的过程。

在数字图像处理中,二值图像占有非常重要的地位,图像的二值化使图像中数据量大为减少,从而能凸显出目标的轮廓。

fun binaryForHuawei(bi: BufferedImage):BufferedImage = binary(bi)
/**
 * 图像二值化操作
 * @param bi
 * @param thresh 二值化的阀值
 * @return
 */
fun binary(bi: BufferedImage,thresh:Int = 225):BufferedImage {
    // 获取当前图片的高,宽,ARGB
    val h = bi.height
    val w = bi.width
    val rgb = bi.getRGB(0, 0)
    val arr = Array(w) { IntArray(h) }
    // 获取图片每一像素点的灰度值
    for (i in 0 until w) {
        for (j in 0 until h) {
            // getRGB()返回默认的RGB颜色模型(十进制)
            arr[i][j] = getImageRgb(bi.getRGB(i, j)) //该点的灰度值
        }
    }
    val bufferedImage = BufferedImage(w, h, BufferedImage.TYPE_BYTE_BINARY) //  构造一个类型为预定义图像类型之一的 BufferedImage,TYPE_BYTE_BINARY(表示一个不透明的以字节打包的 1、2 或 4 位图像。)
    for (i in 0 until w) {
        for (j in 0 until h) {
            if (getGray(arr, i, j, w, h) > thresh) {
                val white = Color(255, 255, 255).rgb
                bufferedImage.setRGB(i, j, white)
            } else {
                val black = Color(0, 0, 0).rgb
                bufferedImage.setRGB(i, j, black)
            }
        }
    }
    return bufferedImage
}
private fun getImageRgb(i: Int): Int {
    val argb = Integer.toHexString(i) // 将十进制的颜色值转为十六进制
    // argb分别代表透明,红,绿,蓝 分别占16进制2位
    val r = argb.substring(2, 4).toInt(16) //后面参数为使用进制
    val g = argb.substring(4, 6).toInt(16)
    val b = argb.substring(6, 8).toInt(16)
    return ((r + g + b) / 3)
}
//自己加周围8个灰度值再除以9,算出其相对灰度值
private fun getGray(gray: Array<IntArray>, x: Int, y: Int, w: Int, h: Int): Int {
    val rs = (gray[x][y]
            + (if (x == 0) 255 else gray[x - 1][y])
            + (if (x == 0 || y == 0) 255 else gray[x - 1][y - 1])
            + (if (x == 0 || y == h - 1) 255 else gray[x - 1][y + 1])
            + (if (y == 0) 255 else gray[x][y - 1])
            + (if (y == h - 1) 255 else gray[x][y + 1])
            + (if (x == w - 1) 255 else gray[x + 1][y])
            + (if (x == w - 1 || y == 0) 255 else gray[x + 1][y - 1])
            + if (x == w - 1 || y == h - 1) 255 else gray[x + 1][y + 1])
    return rs / 9
}


对于不同的手机,在处理二值化时需要使用不同的阀值,甚者采用不同的二值化算法。


下图分别展示了使用 adb 命令截系统设置页面的图,以及裁剪并经过二值化处理后的图片。


image.png

HUAWEI-ELE-AL00.png


image.png

HUAWEI-ELE-AL00-debug.png


newTesseract().doOCR(it) 是使用 Tesseract 来对二值化后的图片调用 OCR 算法进行文字内容的识别。

fun newTesseract():Tesseract = Tesseract().apply {
    val path = SystemConfig.TESS_DATA
    this.setDatapath(path)
    this.setLanguage("eng+chi_sim")
    this.setOcrEngineMode(0)
}


这里我们采用英文和中文的模型,目前只能识别中英文的内容。


对于识别出的内容可能会跟我们预期的有误差,最后采用 Levenshtein 作为字符串相似度的比较。达到一定的值,我们会认为符合预期。


Levenshtein 距离,又称编辑距离,指的是两个字符串之间,由一个转换成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。


踩过的坑


  • Tesseract 在多线程情况下无法使用。后来又使用了对象池,但是仍然无法使用。只能每次实例一个新的 Tesseract 对象,因此不得不对 JVM 进行调优。
  • 对于不同品牌的手机,图像的二值化需要分别处理。
  • 同一个品牌的手机,不同型号可能需要采用不同的策略。


四.后续的规划



虽然上述的实现已经满足了大部分的需求,但是只能处理中英文,并且算法模型需要部署在桌面端。我们已经开始着手深度学习的算法实现 OCR 的功能。


在下一阶段的工作中,将算法和模型都部署在云端。一方面减轻桌面端的压力,另一方面能够支持多种语言并提高文字识别率。

相关实践学习
阿里云百炼xAnalyticDB PostgreSQL构建AIGC应用
通过该实验体验在阿里云百炼中构建企业专属知识库构建及应用全流程。同时体验使用ADB-PG向量检索引擎提供专属安全存储,保障企业数据隐私安全。
AnalyticDB PostgreSQL 企业智能数据中台:一站式管理数据服务资产
企业在数据仓库之上可构建丰富的数据服务用以支持数据应用及业务场景;ADB PG推出全新企业智能数据平台,用以帮助用户一站式的管理企业数据服务资产,包括创建, 管理,探索, 监控等; 助力企业在现有平台之上快速构建起数据服务资产体系
相关文章
|
24天前
|
机器学习/深度学习 数据采集 算法
深入了解机器学习:从入门到应用
【10月更文挑战第6天】深入了解机器学习:从入门到应用
|
1天前
|
机器学习/深度学习 人工智能 自然语言处理
思通数科AI平台在尽职调查中的技术解析与应用
思通数科AI多模态能力平台结合OCR、NLP和深度学习技术,为IPO尽职调查、融资等重要交易环节提供智能化解决方案。平台自动识别、提取并分类海量文档,实现高效数据核验与合规性检查,显著提升审查速度和精准度,同时保障敏感信息管理和数据安全。
27 11
|
1天前
|
机器学习/深度学习 数据采集 运维
智能化运维:机器学习在故障预测和自动化响应中的应用
智能化运维:机器学习在故障预测和自动化响应中的应用
15 4
|
2天前
|
机器学习/深度学习 TensorFlow API
机器学习实战:TensorFlow在图像识别中的应用探索
【10月更文挑战第28天】随着深度学习技术的发展,图像识别取得了显著进步。TensorFlow作为Google开源的机器学习框架,凭借其强大的功能和灵活的API,在图像识别任务中广泛应用。本文通过实战案例,探讨TensorFlow在图像识别中的优势与挑战,展示如何使用TensorFlow构建和训练卷积神经网络(CNN),并评估模型的性能。尽管面临学习曲线和资源消耗等挑战,TensorFlow仍展现出广阔的应用前景。
17 5
|
2天前
|
存储 Kotlin
正则表达式在Kotlin中的应用:提取图片链接
正则表达式在Kotlin中的应用:提取图片链接
|
21天前
|
机器学习/深度学习 数据采集 数据挖掘
特征工程在营销组合建模中的应用:基于因果推断的机器学习方法优化渠道效应估计
因果推断方法为特征工程提供了一个更深层次的框架,使我们能够区分真正的因果关系和简单的统计相关性。这种方法在需要理解干预效果的领域尤为重要,如经济学、医学和市场营销。
47 1
特征工程在营销组合建模中的应用:基于因果推断的机器学习方法优化渠道效应估计
|
4天前
|
Web App开发 定位技术 iOS开发
Playwright 是一个强大的工具,用于在各种浏览器上测试应用,并模拟真实设备如手机和平板。通过配置 `playwright.devices`,可以轻松模拟不同设备的用户代理、屏幕尺寸、视口等特性。此外,Playwright 还支持模拟地理位置、区域设置、时区、权限(如通知)和配色方案,使测试更加全面和真实。例如,可以在配置文件中设置全局的区域设置和时区,然后在特定测试中进行覆盖。同时,还可以动态更改地理位置和媒体类型,以适应不同的测试需求。
Playwright 是一个强大的工具,用于在各种浏览器上测试应用,并模拟真实设备如手机和平板。通过配置 `playwright.devices`,可以轻松模拟不同设备的用户代理、屏幕尺寸、视口等特性。此外,Playwright 还支持模拟地理位置、区域设置、时区、权限(如通知)和配色方案,使测试更加全面和真实。例如,可以在配置文件中设置全局的区域设置和时区,然后在特定测试中进行覆盖。同时,还可以动态更改地理位置和媒体类型,以适应不同的测试需求。
10 1
|
25天前
|
机器学习/深度学习 自然语言处理 JavaScript
信息论、机器学习的核心概念:熵、KL散度、JS散度和Renyi散度的深度解析及应用
在信息论、机器学习和统计学领域中,KL散度(Kullback-Leibler散度)是量化概率分布差异的关键概念。本文深入探讨了KL散度及其相关概念,包括Jensen-Shannon散度和Renyi散度。KL散度用于衡量两个概率分布之间的差异,而Jensen-Shannon散度则提供了一种对称的度量方式。Renyi散度通过可调参数α,提供了更灵活的散度度量。这些概念不仅在理论研究中至关重要,在实际应用中也广泛用于数据压缩、变分自编码器、强化学习等领域。通过分析电子商务中的数据漂移实例,展示了这些散度指标在捕捉数据分布变化方面的独特优势,为企业提供了数据驱动的决策支持。
47 2
信息论、机器学习的核心概念:熵、KL散度、JS散度和Renyi散度的深度解析及应用
|
26天前
|
机器学习/深度学习 数据采集 自然语言处理
【机器学习】大模型驱动下的医疗诊断应用
摘要: 随着科技的不断发展,机器学习在医疗领域的应用日益广泛。特别是在大模型的驱动下,机器学习为医疗诊断带来了革命性的变化。本文详细探讨了机器学习在医疗诊断中的应用,包括疾病预测、图像识别、基因分析等方面,并结合实际案例进行分析。同时,还展示了部分相关的代码示例,以更好地理解其工作原理。
34 3
【机器学习】大模型驱动下的医疗诊断应用
|
9天前
|
机器学习/深度学习 数据采集 人工智能
R语言是一种强大的编程语言,广泛应用于统计分析、数据可视化、机器学习等领域
R语言是一种广泛应用于统计分析、数据可视化及机器学习的强大编程语言。本文为初学者提供了一份使用R语言进行机器学习的入门指南,涵盖R语言简介、安装配置、基本操作、常用机器学习库介绍及实例演示,帮助读者快速掌握R语言在机器学习领域的应用。
33 3