java质检功能_Kotlin 、RxJava 以及传统的机器学习在手机质检上的应用

bf65f731bb29

pexels-анна-хазова-4547763.jpg

一. 业务背景

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

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

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

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

bf65f731bb29

隐私清除1.jpg

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

bf65f731bb29

隐私清除2.jpg

二. 设计思路

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

使用 adb 命令修改手机的休眠时间,确保手机一段时间内不会熄屏。

使用 adb 命令跳转到系统设置页面(不同的手机使用的命令略有不同)

使用 adb 命令对当前页面进行截图

使用 adb 命令将图片传输到桌面端的机器

通过程序对原图进行裁剪,保留原先的40%

对裁剪的图片进行图像二值化处理(不同的手机采用不同的二值化算法)

调用 OCR 进行特征字符串的识别。

比对字符串相似度,最终确定帐号是否退出。

bf65f731bb29

帐号检测.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 {

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, 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 命令截系统设置页面的图,以及裁剪并经过二值化处理后的图片。

bf65f731bb29

HUAWEI-ELE-AL00.png

bf65f731bb29

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 的功能。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值