使用Java 實(shí)現(xiàn)一個(gè)“你畫手機(jī)猜”的小游戲
本文適合有 Java 基礎(chǔ)的人群
作者:DJL-Lanking
HelloGitHub 推出的《講解開源項(xiàng)目》系列。有幸邀請到了亞馬遜 + Apache 的工程師:Lanking( https://github.com/lanking520 ),為我們講解 DJL —— 完全由 Java 構(gòu)建的深度學(xué)習(xí)平臺,本文為系列的第三篇。
一、前言
在 2018 年時(shí),Google 推出了《猜畫小歌》應(yīng)用:玩家可以直接與AI進(jìn)行你畫我猜的游戲。通過畫出一個(gè)房子或者一個(gè)貓,AI 會推斷出各種物品被畫出的概率。它的實(shí)現(xiàn)得益于深度學(xué)習(xí)模型在其中的應(yīng)用,通過深度神經(jīng)網(wǎng)絡(luò)的歸納,曾經(jīng)令人頭疼的繪畫識別也變得易如反掌?,F(xiàn)如今,只要使用一個(gè)簡單的圖片分類模型,我們便可以輕松的實(shí)現(xiàn)繪畫識別。試試看這個(gè)在線涂鴉小游戲吧:
在線涂鴉小游戲:https://djl.ai/website/demo.html
在當(dāng)時(shí),大部分機(jī)器學(xué)習(xí)計(jì)算任務(wù)仍舊需要依托網(wǎng)絡(luò)在云端進(jìn)行。隨著算力的不斷增進(jìn),機(jī)器學(xué)習(xí)任務(wù)已經(jīng)可以直接在邊緣設(shè)備部署,包括各類運(yùn)行安卓系統(tǒng)的智能手機(jī)。但是,由于安卓本身主要是用 Java ,部署基于 Python 的各類深度學(xué)習(xí)模型變成了一個(gè)難題。為了解決這個(gè)問題,AWS 開發(fā)并開源了 DeepJavaLibrary (DJL),一個(gè)為 Java 量身定制的深度學(xué)習(xí)框架。
在這個(gè)文章中,我們將嘗試通過 PyTorch 預(yù)訓(xùn)練模型在在安卓平臺構(gòu)建一個(gè)涂鴉繪畫的應(yīng)用。由于總代碼量會比較多,我們這次會挑重點(diǎn)把最關(guān)鍵的代碼完成。你可以后續(xù)參考我們完整的項(xiàng)目進(jìn)行構(gòu)建。
涂鴉應(yīng)用完整代碼:https://github.com/aws-samples/djl-demo/tree/master/android
二、環(huán)境配置
為了兼容 DJL 需求的 Java 功能,這個(gè)項(xiàng)目需要 Android API 26 及以上的版本。你可以參考我們案例配置來節(jié)約一些時(shí)間,下面是這個(gè)項(xiàng)目需要的依賴項(xiàng):
案例 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 包。
三、構(gòu)建應(yīng)用
3.1 第一步:創(chuàng)建 Layout
我們可以先創(chuàng)建一個(gè) View class 以及 layout(如下圖)來構(gòu)建安卓的前端顯示界面。
如上圖所示,你可以在主界面創(chuàng)建兩個(gè) View
目標(biāo)。PaintView
是用來讓用戶畫畫的,在右下角 ImageView
是用來展示用于深度學(xué)習(xí)推理的圖像。同時(shí)我們預(yù)留一個(gè)按鈕來進(jìn)行畫板的清空操作。
3.2 第二步: 應(yīng)對繪畫動作
在安卓設(shè)備上,你可以自定義安卓的觸摸事件響應(yīng)來應(yīng)對用戶的各種觸控操作。在我們的情況下,我們需要定義下面三種時(shí)間響應(yīng):
- touchStart:感應(yīng)觸碰時(shí)觸發(fā)
- touchMove:當(dāng)用戶在屏幕上移動手指時(shí)觸發(fā)
- touchUp:當(dāng)用戶抬起手指時(shí)觸發(fā)
與此同時(shí),我們用 paths 來存儲用戶在畫板所繪制的路徑?,F(xiàn)在我們看一下實(shí)現(xiàn)代碼。
3.2.1 重寫 OnTouchEvent
和 OnDraw
方法
現(xiàn)在我們重寫 onTouchEvent
來應(yīng)對各種響應(yīng):
@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; }
如上面代碼所示,你可以添加一個(gè) runInference
方法在 MotionEvent.ACTION_UP
事件響應(yīng)上。這個(gè)方法是用來在用戶繪制完后對結(jié)果進(jìn)行推理。在之后的幾步中,我們會講解它的具體實(shí)現(xiàn)。
我們同樣需要重寫 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(); }
真正的圖像會保存在一個(gè) Bitmap
上。
3.2.2 操作開始(touchStart)
當(dāng)用戶觸碰行為開始時(shí),下面的代碼會建立一個(gè)新的路徑同時(shí)記錄路徑中每一個(gè)點(diǎn)在屏幕上的坐標(biāo)。
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)
在手指移動中,我們會持續(xù)記錄坐標(biāo)點(diǎn)然后將它們構(gòu)成一個(gè) quadratic bezier. 通過一定的誤差閥值來動態(tài)優(yōu)化用戶的繪畫動作。只有差別超出誤差范圍內(nèi)的動作才會被記錄下來。
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 操作結(jié)束(touchUp)
當(dāng)觸控操作結(jié)束后,下面的代碼會繪制一個(gè)路徑同時(shí)計(jì)算最小長方形目標(biāo)框。
private void touchUp() { path.lineTo(this.x, this.y); maxBound.add(new Path(path)); }
3.3 第三步:開始推理
為了在安卓設(shè)備上進(jìn)行推理任務(wù),我們需要完成下面幾個(gè)任務(wù):
- 從 URL 讀取模型
- 構(gòu)建前處理和后處理過程
- 從 PaintView 進(jìn)行推理任務(wù)
為了完成以下目標(biāo),我們嘗試構(gòu)建一個(gè) DoodleModel
class。在這一步,我們將介紹一些完成這些任務(wù)的關(guān)鍵步驟。
3.3.1 讀取模型
DJL 內(nèi)建了一套模型管理系統(tǒng)。開發(fā)者可以自定義儲存模型的文件夾。
File dir = getFilesDir(); System.setProperty("DJL_CACHE_DIR", dir.getAbsolutePath());
通過更改 DJL_CACHE_DIR
屬性,模型會被存入相應(yīng)路徑下。
下一步可以通過定義 Criteria 從指定 URL 處下載模型。下載的 zip 文件內(nèi)包含:
doodle_mobilenet.pt
:PyTorch 模型synset.txt
:包含分類任務(wù)中所有類別的名稱
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);
上述代碼同時(shí)定義了 translator,它會被用來做圖片的前處理和后處理。
最后,如下述代碼創(chuàng)建一個(gè) Model
并用它創(chuàng)建一個(gè) 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; }
更多關(guān)于模型加載的信息,請參閱如何加載模型。
DJL 模型加載文檔:http://docs.djl.ai/docs/load_model.html
3.3.2 用 Translator 定義前處理和后處理
在 DJL 中,我們定義了 Translator 接口進(jìn)行前處理和后處理。在 DoodleModel 中我們定義了 ImageClassificationTranslator 來實(shí)現(xiàn) Translator:
ImageClassificationTranslator.builder() .addTransform(new ToTensor()) .optFlag(Image.Flag.GRAYSCALE) .optApplySoftmax(true).build());
下面我們詳細(xì)闡述 translator 所定義的前處理和后處理如何被用在模型的推理步驟中。當(dāng)你創(chuàng)建 translator 時(shí),內(nèi)部程序會自動加載 synset.txt
文件得到做分類任務(wù)時(shí)所有類別的名稱。當(dāng)模型的 predict()
方法被調(diào)用時(shí),內(nèi)部程序會先執(zhí)行所對應(yīng)的 translator 的前處理步驟,而后執(zhí)行實(shí)際推理步驟,最后執(zhí)行 translator 的后處理步驟。對于前處理,我們會將 Image 轉(zhuǎn)化 NDArray,用于作為模型推理過程的輸入。對于后處理,我們對推理輸出的結(jié)果(NDArray)進(jìn)行 softmax 操作。最終返回結(jié)果為 Classifications 的一個(gè)實(shí)例。
自定義 Translator 案例:http://docs.djl.ai/jupyter/pytorch/load_your_own_pytorch_bert.html
3.3.3 用 PaintView 進(jìn)行推理任務(wù)
最后,我們來實(shí)現(xiàn)之前定義好的 runInference 方法。
public void runInference() { // 拷貝圖像 Bitmap bmp = Bitmap.createBitmap(bitmap); // 縮放圖像 bmp = Bitmap.createScaledBitmap(bmp, 64, 64, true); // 執(zhí)行推理任務(wù) 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(); }
這將會創(chuàng)建一個(gè) Toast 彈出頁面用于展示結(jié)果,示例如下:
恭喜你!我們完成了一個(gè)涂鴉識別小程序!
3.4 可選優(yōu)化:輸入裁剪
為了得到更高的模型推理準(zhǔn)確度,你可以通過截取圖像來去除無意義的邊框部分。
上面右側(cè)的圖片會比左邊的圖片有更好的推理結(jié)果,因?yàn)樗目瞻走吙蚋?。你可以通過 Bound 類來尋找圖片的有效邊界,即能把圖中所有白色像素點(diǎn)覆蓋的最小矩形。在得到 x 軸最左坐標(biāo),y 軸最上坐標(biāo),以及矩形高度和寬度后,就可以用這些信息截取出我們想要的圖形(如右圖所示)實(shí)現(xiàn)代碼如下:
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);
恭喜你!現(xiàn)在你就掌握了全部教程內(nèi)容!期待看到你創(chuàng)建的第一個(gè) DoodleDraw 安卓游戲!
最后,可以在GitHub找到本教程的完整案例代碼。
涂鴉應(yīng)用完整代碼:https://github.com/aws-samples/djl-demo/tree/master/android
關(guān)于 DJL
Deep Java Library (DJL) 是一個(gè)基于 Java 的深度學(xué)習(xí)框架,同時(shí)支持訓(xùn)練以及推理。 DJL 博取眾長,構(gòu)建在多個(gè)深度學(xué)習(xí)框架之上 (TenserFlow、PyTorch、MXNet 等) 也同時(shí)具備多個(gè)框架的優(yōu)良特性。你可以輕松使用 DJL 來進(jìn)行訓(xùn)練然后部署你的模型。
它同時(shí)擁有著強(qiáng)大的模型庫支持:只需一行便可以輕松讀取各種預(yù)訓(xùn)練的模型?,F(xiàn)在 DJL 的模型庫同時(shí)支持高達(dá) 70 個(gè)來自 GluonCV、 HuggingFace、TorchHub 以及 Keras 的模型。
項(xiàng)目地址:https://github.com/awslabs/djl/
到此這篇關(guān)于使用Java 實(shí)現(xiàn)一個(gè)“你畫手機(jī)猜”的小游戲的文章就介紹到這了,更多相關(guān)Java實(shí)現(xiàn)你畫手機(jī)猜小游戲內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中線程狀態(tài)+線程安全問題+synchronized的用法詳解
這篇文章主要介紹了Java中線程狀態(tài)+線程安全問題+synchronized的用法詳解,本文結(jié)合示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-04-04利用idea快速搭建一個(gè)spring-cloud(圖文)
本文主要介紹了idea快速搭建一個(gè)spring-cloud,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07使用feign調(diào)用接口時(shí)調(diào)不到get方法的問題及解決
這篇文章主要介紹了使用feign調(diào)用接口時(shí)調(diào)不到get方法的問題及解決,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03mybatis實(shí)現(xiàn)批量插入并返回主鍵(xml和注解兩種方法)
這篇文章主要介紹了mybatis實(shí)現(xiàn)批量插入并返回主鍵(xml和注解兩種方法),具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12ConcurrentHashMap線程安全及實(shí)現(xiàn)原理實(shí)例解析
這篇文章主要介紹了ConcurrentHashMap線程安全及實(shí)現(xiàn)原理實(shí)例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11java開發(fā)只要tomcat設(shè)計(jì)模式用的好下班就能早
這篇文章主要為大家介紹了java開發(fā)只要tomcat設(shè)計(jì)模式的示例詳解,<BR>只要設(shè)計(jì)模式用的好下班就能早,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02