Edited at

AndroidでTensorFlowを使ってアプリ作りたいけど何すりゃいいの(前編)

More than 1 year has passed since last update.

タイトルと同じことを心でつぶやいている人は結構いると思います。

当記事では、「どんな仕組みで動くのか」ではなく、「どうすれば動くのか」に焦点を当てています。

飛行機はどんな理屈で飛ぶのか、ではなく、飛行機はどう操作すれば飛ぶのか、という感じです。


要するに何すりゃいいの

端的に言えば次の通り:


  • 学習済みモデルを用意

  • プロジェクトにTensorFlowのライブラリを追加


  • TensorFlowInferenceInterface をnewして学習済みモデルを読み込ませる

  • 画像とかを配列化してTensorFlowInferenceInterfaceに与える

  • 結果の配列をTensorFlowInferenceInterfaceから受け取る

機械学習のややこしいところは学習済みモデルの用意のところに集約されているので、出来合いの学習済みモデルを使うだけなら実は簡単。

手っ取り早く核心を掴みたいのであればいろいろすっ飛ばしてコーディング例を参照してください。


前提

当記事では画像上の物体識別モデル"DeepLab"をAndroid上で利用する手順を軸に、Android上でのTensolFlowの利用法を説明しています。

着手時点での記事作成者のステータスは次の通り:


  • 機械学習についての知識とスキルは白紙

  • Androidアプリの作成は難なくこなせる

  • プログラム言語はおおむねどれも似たようなもので、未知の言語でもまあ何とかなるだろうと思っている

…こんな感じのひとは街でよく見かける程度に多いのではなかろうか。


動機


車の写真のナンバープレートをぼかしたい

旅をしながら車の写真をアップすることがよくあるんだけど、毎度毎度ナンバーをぼかしてからアップするのがめんどくさい。

既存アプリもあるにはあるけどしっくりこない。

自分で作れんかなあ。ディープラーニングとかそういうのでパパっとナンバーを認識してぼかすとかできそうじゃん?

でもそもそも何をどうすりゃいいのかさっぱりわからん…。


DeepLabを使えばできそう

機械学習とか画像認識とかでググるとみんなすごく難しいことを言ってるので腰が引ける。

違うんだ、そんな込み入った話が聞きたいんじゃないんだ、使い方が知りたいんだ…。

Googleが提供してるDeepLabってのがなんかすごく都合よさそう。

写真に写ってるものを認識して、その領域を画素単位で教えてくれる。

これでナンバープレートの領域を得て、そこをぼかせば目標達成じゃん?

DeepLabはTensorFlowっていう機械学習のプラットフォーム?上で動くらしい。

TensorFlowはAndroid上でも動くらしい。

だがしかし、そんなことできるのか?Androidの端末上で?自分の現有スキルで?


要点

いろいろ調べて得られた要点は次の通り:


  • AndroidアプリにTensorFlowを導入するのは割と簡単

  • DeepLabの学習済みモデルが公開されている


    • 一般的な物の識別は試せる

    • ただし車のナンバーというニッチなモデルはない



  • 自分で学習済みモデルを生成するのはいろいろ大変そう


目標

とりあえずなんか動かせそうな気がしてきたので動作を実証するための目標を立てた:


  • Android端末上で写真を選択、物体を認識してその部分を塗りつぶしたマスク画像を得るアプリを作る


    • とりあえずナンバープレートじゃなくてなんでもいいから認識してみる

    • Android端末上でできるということを実証したい



  • 物体の認識にはTensorFlow + DeepLab を利用する


    • 自分で認識の仕組みを一朝一夕に作れるとは考えにくい。人類の英知は利用させてもらう



ナンバープレートだけを認識する学習済みモデルを用意するには自分で学習を実行する必要があるようだ。

たぶんそれだけで一つの記事になるボリュームになりそうなので、この記事ではここまでにしておく。


Android用TensorFlow

Android上で動くTensorFlowは二種類ある。

https://www.tensorflow.org/mobile/ に違いが書いてある。要約すると次の通り:


  • TensorFlow Lite :TensorFlow Mobileを進歩させたもの。軽量で速いが、すべてのモデルが動作するわけではない

  • TensorFlow Mobile :TensorFlowのフルセットをAndroid用に持ってきたもの。

記事作成時点で導入が簡単なのはTensorFlow Mobileの方。

機能の取捨選択ができるほどの知識もないので、TensorFlow Mobileを利用することにする。


TensorFlowについての初心者視点での疑問点と、初心者視点での回答

AndroidアプリからのTensorFlowを用いた機械学習の利用について着手時点での疑問点と、ある程度動かせた時点での答えを並べていく。

多くの人がおなじような疑問を前に足踏みしているのではなかろうか。

総括すると、学習済みモデルを利用するだけなら予想以上に簡単

初心者の疑問に初心者が答える形になっているのでナメた問答になっているかもしれないがご容赦願いたい。


何に対して何を与えたら何が得られるのか

TensorFlowに対して配列を与えたら配列が得られる。

Tensor(テンソル)っていうのは多次元配列とおおむね同義で、TensorFlowはテンソルを処理するライブラリ。

画像も画素の配列であり画素もRGB各値の配列なわけで、画像データはテンソルと言える。

TensorFlowで扱うには都合がいいデータというわけだ。

これを何かに渡せばそれを元に何かの配列をこしらえて返してくれる。

DeepLabを利用する場合については次のように言える:


TensorFlow に対して 画像の画素配列 を与えたら 全画素に対応した物体既定のIDの配列 が得られる


ここにDeepLabの出る幕はない。DeepLabを利用して作成された学習済みモデルがTensorFlowでの演算に利用されるという形の関連性となっている。


機械学習の知識が必要なのか

学習済みモデルを利用するだけであれば必要ない。

自分でロジックを組み立てた独自のモデルを作成する場合には深い知識が必要になる。

ロジックは既存のものを利用して、独自の推論対象を実現する場合には浅い知識でなんとかなる。

例えば、DeepLabを利用し車のナンバープレートを抽出するモデルを用意する場合は、自力で学習用データセットを用意してモデルを学習させればよい。はず。


自分が実装しなければならないのはどこまでの範囲か

学習済みモデルを利用するだけであれば次の通り:


  • 学習済みモデルのファイルを読み込む

  • 与えるデータをTensorFlowの解釈できるフォーマットに整える

  • TensorFlowにデータを渡す

  • TensorFlowから応答をもらう

学習のアルゴリズム等をこちらで考える必要はない。

引数を与えて戻り値を受け取る、関数のような利用法になる。


PythonとかLinuxとか使えなければダメなのか

学習済みモデルを利用するだけであれば必要ない。

が、あちこちでこれらが出てくることになるのでかかわりを避けることは難しい。

覚悟はしておいたほうがよい。

自力で学習済みモデルを生成するのであれば必須のスキルになると思われる。


クラウドの利用が必要なのか

学習済みモデルを利用するだけであれば必要ない。

大規模な演算が発生する仕様でなければローカルの端末で演算能力が足りる場合がほとんどかと思われる。

当記事でも端末上でローカルに実行するサンプルコードを記載している。

開発環境としてクラウドに用意されたサービスを利用する方が楽な場面はちょくちょく発生する。

本稿でも、学習済みモデルを得る際に利用している。


高スペックPCが必要なのか

学習済みモデルを利用するだけであれば必要ない。

普通にアプリを組むの場合と同等の環境で問題ない。

実際に、当記事にかかわる開発環境は7インチのAtomのノートPC(GPC Pocket)のみでまかなっている。

自分で学習済みモデルを生成する場合は、データの規模によっては必要になるかもしれない。

というかだんだん欲しくなる。ほんとに。


高スペックの端末が必要なのか

特に必要ない。

Android4.2の古い端末でもそこそこ普通に動いている。


ライブラリの導入が大変ではないのか

簡単になった。

AndroidStudioでTensorFlow Mobile利用のアプリを作る場合、アプリモジュールのbuild.gradleに次の1行を追加するだけでよい:


implementation "org.tensorflow:tensorflow-android:+"



学習済みモデルは端末に収まるボリュームなのか

大規模なモデルでなければ収まる。

サイズは数MBから数百MBまで様々なので、大規模なものはクラウド上で処理するなどの方法が必要。

156MBのモデルを端末上で読ませたところ、読み込み処理中にOutOfMemoryErrorの例外発生で動作しなかった。


処理速度はどうなのか

我慢できる程度。

処理するモデルとデータ量による。

当記事での画像認識の場合、1画像を処理するのに30秒弱を要している。


DeepLabを利用するにあたっての予備知識

コーディング例を提示する前にDeepLabについてあらためて説明しておく。


概略

DeepLabは画像内の物体を画素単位で識別するDeepLearingのモデル。TensorFlow上で利用できる。

学習済みモデルを利用するだけなら、DeepLabの内部動作に関する予備知識は必要ない。

DeepLabを利用して作成された学習済みモデルをTensorFlowで読み込むだけでよい。

演算はTensorFlow単体が行い、DeepLabはモデルの作成時点でのみ必要となる。

TensorFlowとやり取りするデータの形式はDeepLabの仕様に基づいて規定されているので、その仕様だけは把握しておく必要がある。


学習済みモデル

各種学習済みモデルのサンプルが提供されている。

詳細は後述。


アプリから与えるデータ

画素数*3要素のByte配列。

RGBRGBRGB...の順に並ぶ1次元配列とする。

TensorFlowに配列を渡す際に縦横の寸法も同時に渡す。


アプリが受け取るデータ

画素数分のint配列。

ただし、Bitmapの画素値ではなく、識別した物体のIDが画素位置毎に格納されている。

IDと物体の対応は学習用データセット毎に定義されている。


PASCAL VOC(2012) + MS-COCO データセットのIDと物体対応例:

0=背景, 1=飛行機, 2=自転車, 3=鳥, 4=ボート, 5=ボトル, 6=バス, 7=車, 8=猫、…



学習済みモデル

サンプルとして提供されている学習済みモデルは、そのままではAndroid上で利用できない。

解説すると長くなるので別記事にまとめた。

Android端末向けDeepLab用モデルのエクスポート を参照されたい。


コーディング例

ミニマルな実装例。

AndroidStudioでのビルドを前提としている。


動作

与えられた画像から車が映っている領域を抽出して表示する。

起動時に表示された画像をタッチすることで抽出処理が実行される。

以降、画像タッチで元画像とマスクの表示が切り替わる。


ビルド

AndroidStudioで空のアクティビティの新規プロジェクトを作成し、次のように改変する:


  • MainActivity.javaを上書きでペースト

  • TFModel.javaを新規作成してペースト

  • appモジュールのbuild.gradleの dependencies に以下の1行を追加:


implementation "org.tensorflow:tensorflow-android:+"



実行

実行にあたっては/<内部共有ストレージ>/deeplab/ 内に次のファイルを置くこと:


  • frozen_inference_graph.pb :学習済みモデルファイル


    • Android用に凍結されたもの


    • 学習済みモデルの項からダウンロードできる:mobilenetv2_coco_xxxを利用の事



  • image.jpg :車が映った画像ファイル

外部ストレージの読み取り権限が必要。

宣言はライブラリについてくるのでマニフェストに追記する必要はないが、実行時にパーミッションを確認する機構は実装していない。

インストール後にアプリの設定で手動で許可するか、targetSDKを22以下にすることでインストール時に許可する必要がある。


注意

エラー処理無配慮でメインスレッドでの長所要時間処理も恐れずぶっこんであることに注意。

わかりやすさ優先で記述されている。


コード


MainActivity.java

import android.app.Activity;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;

//"<内部共有ストレージ>/deeplab/"に以下の2ファイルを置いて実行する
// ・"frozen_inference_graph.pb" :モデルファイル
// ・"image.jpg" :車が写った画像
public class MainActivity extends Activity {
private static final String IMAGEFILE = "/sdcard/deeplab/image.jpg";

//TensorFlowの実行
// ここだけがキモ。これ以外は表示とかの雑務
//=========================================================================
private TFModel mModel=new TFModel();
private Bitmap mSource; //元画像
private Bitmap mResult; //得られたマスク画像

private void runTensorFlow() {
if(!mModel.isInitialized()) return;

//元画像を渡してマスクを得る
// ここが核心
mResult=mModel.run(mSource);
}

//以下雑務
//=========================================================================
private static final int MAX_SRCWIDTH = 1920;
private static final int MAX_SRCHEIGHT = 1920;
private ImageView mIv;
private int mImageIndex=-1;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

//画面の生成
// 画面いっぱいにImageViewを配置
mIv=new ImageView(this);
mIv.setImageResource(R.drawable.ic_launcher_foreground);
setContentView(mIv);

//ImageViewをタップしたら処理を行う
mIv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(mImageIndex==-1) runTensorFlow();
switchImage();
}
});

//TensorFlowの初期化
// モデルを読み込む
if(!mModel.initialize()) {
Toast.makeText(this,"TensorFlow initialization failed.",Toast.LENGTH_SHORT).show();
return;
}

//元画像を開く
loadImage(IMAGEFILE);
}

//画像をタップしたときに表示を切り替える
private void switchImage() {
mImageIndex=(mImageIndex+1)%2; //0~1をループ
mIv.setImageBitmap((mImageIndex==0)?mResult:mSource);
}

//Uriで指定された画像を読み込む
// 大きすぎる場合は端末によっては表示できないので縮小する
private void loadImage(String path) {
//画像取得
mSource=BitmapFactory.decodeFile(path);

//縮小
Bitmap shrinked=createShrinedkBitmap(mSource);
if(shrinked!=null) {mSource.recycle();mSource=shrinked;}

//表示
mIv.setImageBitmap(mSource);
}

//表示に適したサイズに縮小したBitmapを生成する
private Bitmap createShrinedkBitmap(Bitmap src) {
int wOrg=src.getWidth();
int hOrg=src.getHeight();
int w=wOrg,h=hOrg;
if(w>MAX_SRCWIDTH) {h=(int) (((float)MAX_SRCWIDTH/w)*h);w=MAX_SRCWIDTH;}
if(h>MAX_SRCHEIGHT) {w=(int) (((float)MAX_SRCHEIGHT/h)*w);h=MAX_SRCHEIGHT;}
if(w==wOrg && h==hOrg) return null;

return Bitmap.createScaledBitmap(src,w,h,true);
}
}



TFModel.java

import android.graphics.Bitmap;

import android.graphics.Color;

import org.tensorflow.contrib.android.TensorFlowInferenceInterface;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class TFModel {
private final static String MODELFILE = "/sdcard/deeplab/frozen_inference_graph.pb";
private final static String INPUTNAME = "ImageTensor";
private final static String OUTPUTNAME = "SemanticPredictions";
private static final int MAXWIDTH = 513;
private static final int MAXHEIGHT = 513;

private TensorFlowInferenceInterface mTFInterface=null;

public boolean isInitialized() {return mTFInterface!=null;}

//初期化
public boolean initialize() {
//モデルファイルを開く
File file=new File(MODELFILE);
FileInputStream is=null;

try {
is=new FileInputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}

if(is==null) return false;

//TensorFlowにモデルファイルを読み込む
mTFInterface=new TensorFlowInferenceInterface(is);

//モデルファイルを閉じる
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}

return true;
}

//実行
public Bitmap run(Bitmap src) {
if(mTFInterface==null || src==null) return null;

int wOrg=src.getWidth();
int hOrg=src.getHeight();
int w=wOrg,h=hOrg;

//処理可能なサイズに縮小する
Bitmap shrinked=null;
if(w>MAXWIDTH || h>MAXHEIGHT) {
if(w>MAXWIDTH) {h= (int) (((float)MAXWIDTH/w)*h);w=MAXWIDTH;}
if(h>MAXHEIGHT) {w= (int) (((float)MAXHEIGHT/h)*w);h=MAXHEIGHT;}
shrinked=Bitmap.createScaledBitmap(src,w,h,true);
if(shrinked==null) return null;
src=shrinked;
}

//BitmapをTensorFlowに与えるデータ形式に変換する
// …今回のモデルではRGBのbyte配列
int srcInt[]=new int[w*h];
src.getPixels(srcInt,0,w,0,0,w,h);
byte srcByte[]=new byte[w*h*3];
for(int i=0,c=srcInt.length;i<c;i++) {
int col=srcInt[i];
srcByte[i*3+0]=(byte)((col&0x00FF0000)>>16); //R
srcByte[i*3+1]=(byte)((col&0x0000FF00)>>8); //G
srcByte[i*3+2]=(byte)((col&0x000000FF)); //B
}

//TensorFlow実行
// inputName,outputNameにはモデルで規定された名前を与える
int resultInt[]=new int[w*h]; //戻り値:推定された物体のIDの配列。0なら背景
mTFInterface.feed(INPUTNAME,srcByte,1,h,w,3);
mTFInterface.run(new String[]{OUTPUTNAME});//,true);
mTFInterface.fetch(OUTPUTNAME,resultInt);

//結果をビットマップに変換
Bitmap result=Bitmap.createBitmap(w,h, Bitmap.Config.ARGB_8888);
for(int y=0;y<h;y++) {
for(int x=0;x<w;x++) {
int id=resultInt[y*w+x];
// result.setPixel(x,y,id==0? Color.TRANSPARENT:0x80FF0000); //0x80FF0000:=半透明の赤
result.setPixel(x,y,id!=7? Color.TRANSPARENT:0x80FF0000); //0x80FF0000:=半透明の赤,7=車のID
}
}

if(w!=wOrg || h!=hOrg) result=Bitmap.createScaledBitmap(result,wOrg,hOrg,true);
if(shrinked!=null) shrinked.recycle();

return result;
}
}



課題

コーディング例では長くなるので割愛したが、Googleフォトなどから写真をピックアップして車の領域を抽出する動作は実装できた。

先に掲げた目標である、


  • Android端末上で写真を選択、物体を認識してその部分を塗りつぶしたマスク画像を得るアプリを作る

  • 物体の認識にはTensorFlow + DeepLab を利用する

は果たせたわけだが、最終的な目標である「車のナンバーをぼかしたい」を達成するにはそのための学習済みデータを用意する必要がある。

次のような課題が浮き彫りになってきた:


  • どんなデータを用意すればよいのか

  • どれくらいの量のデータを用意すればよいのか

  • どうやって学習させればよいのか


つづく

上にあげた課題はおおむね解決したので、区切りの良いところで当記事を公開します。

続きはいずれまた。

続きはこちら:AndroidでTensorFlowを使ってアプリ作りたいけど何すりゃいいの(後編・DeepLab用独自データの作成と学習)

学習データ作成作業で一生分くらい車のナンバー隠蔽処理をこなすことになろうとは…




おすすめの資料

良記事だと思います。第5章あたりまでがんばって読めば当記事の範囲はカバーできます。