用 Java 做个“你画手机猜”的小游戏
本文适合有 Java 基础的人群

作者:DJL-Lanking
HelloGitHub 推出的《讲解开源项目》系列。有幸邀请到了亚马逊 + Apache 的工程师:Lanking( https://github.com/lanking520 ),为我们讲解 DJL —— 完全由 Java 构建的深度学习平台,本文为系列的第三篇。
一、前言
在 2018 年时,Google 推出了《猜画小歌》应用:玩家可以直接与AI进行你画我猜的游戏。通过画出一个房子或者一个猫,AI 会推断出各种物品被画出的概率。它的实现得益于深度学习模型在其中的应用,通过深度神经网络的归纳,曾经令人头疼的绘画识别也变得易如反掌。现如今,只要使用一个简单的图片分类模型,我们便可以轻松的实现绘画识别。试试看这个在线涂鸦小游戏吧:
在当时,大部分机器学习计算任务仍旧需要依托网络在云端进行。随着算力的不断增进,机器学习任务已经可以直接在边缘设备部署,包括各类运行安卓系统的智能手机。但是,由于安卓本身主要是用 Java ,部署基于 Python 的各类深度学习模型变成了一个难题。为了解决这个问题,AWS 开发并开源了 DeepJavaLibrary (DJL),一个为 Java 量身定制的深度学习框架。
在这个文章中,我们将尝试通过 PyTorch 预训练模型在在安卓平台构建一个涂鸦绘画的应用。由于总代码量会比较多,我们这次会挑重点把最关键的代码完成。你可以后续参考我们完整的项目进行构建。
涂鸦应用完整代码:https://github.com/aws-samples/djl-demo/tree/master/android
二、环境配置
为了兼容 DJL 需求的 Java 功能,这个项目需要 Android API 26 及以上的版本。你可以参考我们案例配置来节约一些时间,下面是这个项目需要的依赖项:
案例 gradle: https://github.com/aws-samples/djl-demo/blob/master/android/quickdraw_recognition/build.gradle
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'ai.djl:api:0.7.0'
implementation 'ai.djl.android:core:0.7.0'
runtimeOnly 'ai.djl.pytorch:pytorch-engine:0.7.0'
runtimeOnly 'ai.djl.android:pytorch-native:0.7.0'
}
我们将使用 DJL 提供的 API 以及 PyTorch 包。
三、构建应用
3.1 第一步:创建 Layout
我们可以先创建一个 View class 以及 layout(如下图)来构建安卓的前端显示界面。

如上图所示,你可以在主界面创建两个 View 目标。PaintView 是用来让用户画画的,在右下角 ImageView 是用来展示用于深度学习推理的图像。同时我们预留一个按钮来进行画板的清空操作。
3.2 第二步: 应对绘画动作
在安卓设备上,你可以自定义安卓的触摸事件响应来应对用户的各种触控操作。在我们的情况下,我们需要定义下面三种时间响应:
- touchStart:感应触碰时触发
- touchMove:当用户在屏幕上移动手指时触发
- touchUp:当用户抬起手指时触发
与此同时,我们用 paths 来存储用户在画板所绘制的路径。现在我们看一下实现代码。
3.2.1 重写 OnTouchEvent 和 OnDraw 方法
现在我们重写 onTouchEvent 来应对各种响应:
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN :
touchStart(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE :
touchMove(x, y);
invalidate();
break;
case MotionEvent.ACTION_UP :
touchUp();
runInference();
invalidate();
break;
}
return true;
}
如上面代码所示,你可以添加一个 runInference 方法在 MotionEvent.ACTION_UP 事件响应上。这个方法是用来在用户绘制完后对结果进行推理。在之后的几步中,我们会讲解它的具体实现。
我们同样需要重写 onDraw 方法来展示用户绘制的图像:
@Override
protected void onDraw(Canvas canvas) {
canvas.save();
this.canvas.drawColor(DEFAULT_BG_COLOR);
for (Path path : paths) {
paint.setColor(DEFAULT_PAINT_COLOR);
paint.setStrokeWidth(BRUSH_SIZE);
this.canvas.drawPath(path, paint);
}
canvas.drawBitmap(bitmap, 0, 0, bitmapPaint);
canvas.restore();
}
真正的图像会保存在一个 Bitmap 上。
3.2.2 操作开始(touchStart)
当用户触碰行为开始时,下面的代码会建立一个新的路径同时记录路径中每一个点在屏幕上的坐标。
private void touchStart(float x, float y) {
path = new Path();
paths.add(path);
path.reset();
path.moveTo(x, y);
this.x = x;
this.y = y;
}
3.2.3 手指移动(touchMove)
在手指移动中,我们会持续记录坐标点然后将它们构成一个 quadratic bezier. 通过一定的误差阀值来动态优化用户的绘画动作。只有差别超出误差范围内的动作才会被记录下来。
quadratic bezier 文档: https://developer.android.com/reference/android/graphics/Path
private void touchMove(float x, float y) {
if (x < 0 || x > getWidth() || y < 0 || y > getHeight()) {
return;
}
float dx = Math.abs(x - this.x);
float dy = Math.abs(y - this.y);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
path.quadTo(this.x, this.y, (x + this.x) / 2, (y + this.y) / 2);
this.x = x;
this.y = y;
}
}
3.2.4 操作结束(touchUp)
当触控操作结束后,下面的代码会绘制一个路径同时计算最小长方形目标框。
private void touchUp() {
path.lineTo(this.x, this.y);
maxBound.add(new Path(path));
}
3.3 第三步:开始推理
为了在安卓设备上进行推理任务,我们需要完成下面几个任务:
- 从 URL 读取模型
- 构建前处理和后处理过程
- 从 PaintView 进行推理任务
为了完成以下目标,我们尝试构建一个 DoodleModel class。在这一步,我们将介绍一些完成这些任务的关键步骤。
3.3.1 读取模型
DJL 内建了一套模型管理系统。开发者可以自定义储存模型的文件夹。
File dir = getFilesDir();
System.setProperty("DJL_CACHE_DIR", dir.getAbsolutePath());
通过更改 DJL_CACHE_DIR 属性,模型会被存入相应路径下。
下一步可以通过定义 Criteria 从指定 URL 处下载模型。下载的 zip 文件内包含:
doodle_mobilenet.pt:PyTorch 模型synset.txt:包含分类任务中所有类别的名称
Criteria<Image, Classifications> criteria =
Criteria.builder()
.setTypes(Image.class, Classifications.class)
.optModelUrls("https://djl-ai.s3.amazonaws.com/resources/demo/pytorch/doodle_mobilenet.zip")
.optTranslator(translator)
.build();
return ModelZoo.loadModel(criteria);
上述代码同时定义了 translator,它会被用来做图片的前处理和后处理。
最后,如下述代码创建一个 Model 并用它创建一个 Predictor:
@Override
protected Boolean doInBackground(Void... params) {
try {
model = DoodleModel.loadModel();
predictor = model.newPredictor();
return true;
} catch (IOException | ModelException e) {
Log.e("DoodleDraw", null, e);
}
return false;
}
更多关于模型加载的信息,请参阅如何加载模型。
DJL 模型加载文档:http://docs.djl.ai/docs/load_model.html
3.3.2 用 Translator 定义前处理和后处理
在 DJL 中,我们定义了 Translator 接口进行前处理和后处理。在 DoodleModel 中我们定义了 ImageClassificationTranslator 来实现 Translator:
ImageClassificationTranslator.builder()
.addTransform(new ToTensor())
.optFlag(Image.Flag.GRAYSCALE)
.optApplySoftmax(true).build());
下面我们详细阐述 translator 所定义的前处理和后处理如何被用在模型的推理步骤中。当你创建 translator 时,内部程序会自动加载 synset.txt 文件得到做分类任务时所有类别的名称。当模型的 predict() 方法被调用时,内部程序会先执行所对应的 translator 的前处理步骤,而后执行实际推理步骤,最后执行 translator 的后处理步骤。对于前处理,我们会将 Image 转化 NDArray,用于作为模型推理过程的输入。对于后处理,我们对推理输出的结果(NDArray)进行 softmax 操作。最终返回结果为 Classifications 的一个实例。
自定义 Translator 案例:http://docs.djl.ai/jupyter/pytorch/load_your_own_pytorch_bert.html
3.3.3 用 PaintView 进行推理任务
最后,我们来实现之前定义好的 runInference 方法。
public void runInference() {
// 拷贝图像
Bitmap bmp = Bitmap.createBitmap(bitmap);
// 缩放图像
bmp = Bitmap.createScaledBitmap(bmp, 64, 64, true);
// 执行推理任务
Classifications classifications = model.predict(bmp);
// 展示输入的图像
Bitmap present = Bitmap.createScaledBitmap(bmp, imageView.getWidth(), imageView.getHeight(), true);
imageView.setImageBitmap(present);
// 展示输出的图像
if (messageToast != null) {
messageToast.cancel();
}
messageToast = Toast.makeText(getContext(), classifications.toString(), Toast.LENGTH_SHORT);
messageToast.show();
}
这将会创建一个 Toast 弹出页面用于展示结果,示例如下:

恭喜你!我们完成了一个涂鸦识别小程序!
3.4 可选优化:输入裁剪
为了得到更高的模型推理准确度,你可以通过截取图像来去除无意义的边框部分。

上面右侧的图片会比左边的图片有更好的推理结果,因为它所包含的空白边框更少。你可以通过 Bound 类来寻找图片的有效边界,即能把图中所有白色像素点覆盖的最小矩形。在得到 x 轴最左坐标,y 轴最上坐标,以及矩形高度和宽度后,就可以用这些信息截取出我们想要的图形(如右图所示)实现代码如下:
RectF bound = maxBound.getBound();
int x = (int) bound.left;
int y = (int) bound.top;
int width = (int) Math.ceil(bound.width());
int height = (int) Math.ceil(bound.height());
// 截取部分图像
Bitmap bmp = Bitmap.createBitmap(bitmap, x, y, width, height);
恭喜你!现在你就掌握了全部教程内容!期待看到你创建的第一个 DoodleDraw 安卓游戏!
最后,可以在GitHub找到本教程的完整案例代码。
涂鸦应用完整代码:https://github.com/aws-samples/djl-demo/tree/master/android
关于 DJL

Deep Java Library (DJL) 是一个基于 Java 的深度学习框架,同时支持训练以及推理。 DJL 博取众长,构建在多个深度学习框架之上 (TenserFlow、PyTorch、MXNet 等) 也同时具备多个框架的优良特性。你可以轻松使用 DJL 来进行训练然后部署你的模型。
它同时拥有着强大的模型库支持:只需一行便可以轻松读取各种预训练的模型。现在 DJL 的模型库同时支持高达 70 个来自 GluonCV、 HuggingFace、TorchHub 以及 Keras 的模型。

关注 HelloGitHub 公众号
用 Java 做个“你画手机猜”的小游戏的更多相关文章
- 常用Java API之Ramdom--用代码模拟猜数小游戏
常用Java API之Ramdom Ramdom类用来生成随机数字.使用起来也是三个步骤: 1.导包 import java.util.Random; 2.创建 Random r = new Rand ...
- Java(控制台输出)实现猜数字小游戏
import java.util.Scanner; import java.util.Random; public class GuestNum{ public static void main(St ...
- Java基础知识强化之IO流笔记70:Properties练习之 如何让猜数字小游戏只能玩5次的案例
1. 使用Properties完成猜数字小游戏只能玩5次的案例: 2. 代码实现: (1)猜数字游戏GuessNumber: package cn.itcast_08; import java.uti ...
- java猜数字小游戏
/* * * 猜数字小游戏 * * 先由系统生成一个2-100之间的随机数字, * * 然后捕获用户从控制台中输入的数字是否与系统生成的随机数字相同, * * 如果相同则统计用户所猜的次数,并给出相应 ...
- Java数组重修,猜数小游戏改进和打印正三角形
数组重修,猜数小游戏 要求:从键盘输入一个数,判断数组是否包含此数,运用随机数 我们可能会这样写 import java.util.Random; import java.util.Scanner; ...
- day06-java-(方法,猜字符小游戏)
day05-java-(方法,猜字符小游戏) 1.方法: 1)用于封装一段特定的逻辑功能 2)方法应尽可能的独立,只干一件事 3)方法可以被反复的调用多次 4)避免代码重复,有利于代码的维护, ...
- 关于切片/截取(slice)和random模块的使用(实例:猜单词小游戏)
切片和random的使用在源码中都有注释(可以直接下载):https://github.com/NoobZeng/GuessWords 1. README.MD 基于Python的猜单词游戏 猜单词小 ...
- Demo_2:Qt实现猜字小游戏
1 环境 系统:windows 10 代码编写运行环境:Qt Creator 4.4.1 (community) Github: 2 简介 参考视频:https://www.bilibili.co ...
- 简单的C语言猜数字小游戏
猜数字小游戏可谓是C语言最为基础的一个知识点了,我们可以在此基础上进行延伸,实现随机数的猜测,然后是加入再来一局的模式,等等.这里是抛砖引玉,希望你能做出你的经典之作. #include <st ...
随机推荐
- 计算机网络-网络层(6)ICMP协议
互联网控制报文协议(ICMP,Internet Control Message Protocol),被主机和路由器用来彼此沟通网络层的信息 ICMP报文是承载在IP分组中的,即lCMP报文是作为IP有 ...
- Qt启动子进程,子进程关闭时通知主进程,实现主进程对子进程的管理
自己开发了一个股票智能分析软件,功能很强大,需要的点击下面的链接获取: https://www.cnblogs.com/bclshuai/p/11380657.html 1.需求描述 Qt主进程启动 ...
- 【从零开始撸一个App】PKCE
一个成功的App背后肯定有一堆后端服务提供支撑,认证授权服务(Authentication and Authorization Service,以下称AAS)就是其中之一,它是约束App.保障资源安全 ...
- python3中异常处理方法 try-except-else-finally
个人感觉逻辑上类似if else条件判断 但这是对语法错误以及抛出异常的处理 异常处理 try/except 异常捕捉可以使用 try/except 语句. 以下例子中,让用户输入一个合法的整数,但是 ...
- 一个神奇的jq插件----zTree
最近在公司做项目中用到了一个树(ztree)的插件,使用起来非常顺手,便写下这篇博客,用来记录一下,以便后续使用 首先先放上ztree官方的地址:http://www.treejs.cn/v3/mai ...
- MD5加密,java String 转变成MD5 String 详细代码,工具类Android开发必备
/** * MD5加码.32位 * @param inStr * @return */ public static String MD5(String inStr) { MessageDigest m ...
- Kubernetes-14:一文详解Pod、Node调度规则(亲和性、污点、容忍、固定节点)
Kubernetes Pod调度说明 简介 Scheduler 是 Kubernetes 的调度器,主要任务是把定义的Pod分配到集群的节点上,听起来非常简单,但要考虑需要方面的问题: 公平:如何保证 ...
- 深入了解Netty【五】线程模型
引言 不同的线程模型对程序的性能有很大的影响,Netty是建立在Reactor模型的基础上,要搞清Netty的线程模型,需要了解一目前常见线程模型的一些概念. 具体是进程还是线程,是和平台或者编程语言 ...
- [V&N公开赛] CheckIn
V&N战队考核+招新,赵师傅出的Web题,做着感觉有点顶,趁热打铁写write up记录一下考察的知识点 这道题说是CheckIn其实还是有一定难度的(也可能是我太菜了),进入题目直接给出了f ...
- 详解 Python 的二元算术运算,为什么说减法只是语法糖?
原题 | Unravelling binary arithmetic operations in Python 作者 | Brett Cannon 译者 | 豌豆花下猫("Python猫&q ...