13. 플러터 : 개와 고양이 사진 분류 (케라스) (4)

이번 프로젝트의 마지막인 안드로이드 부분만 남았다.


6번
파이썬으로 작업한 딥러닝 모델 파일을 추가한다.
MainActivity.java 파일이 포함된 java 프로젝트와 같은 위치에 assets 폴더를 생성하고
cats_and_dogs.tflite 모델 파일을 붙여넣는다.



7번
gradle 파일을 수정한다.
위의 그림에서 profile 폴더 바로 아래 있는 파일이다.
같은 이름이 두 개 있어서 헷갈리기 딱 좋다.


android {
...

// 추가한 부분
aaptOptions {
noCompress "tflite"
}
}


dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

// 추가한 부분
implementation 'org.tensorflow:tensorflow-lite:+'
}


8번
경로 수신 및 모델 연동 코드를 구현한다.
MainActivity.java 파일을 열고 아래 코드를 붙여넣는다.


package tflite.com.catdog.flutter_catdog;

import android.os.Bundle;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import android.app.Activity;
import android.content.res.AssetFileDescriptor;
import android.util.Log;

import java.io.FileInputStream;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import org.tensorflow.lite.Interpreter;

// 기본적으로 포함되어 있는 모듈
import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant;

// MethodChannel 관련 모듈 (추가해야 함)
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.MethodCall;

public class MainActivity extends FlutterActivity {
// 채널 문자열. 플러터에 정의한 것과 동일해야 한다.
private static final String mChannel = "catdog/predict";

@Override
protected void onCreate(Bundle savedInstanceState) {
// 빌트인 코드
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);

// 추가한 함수.
new MethodChannel(getFlutterView(), mChannel).setMethodCallHandler(
new MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, Result result) {
// 각각의 메소드를 switch로 분류. 아이폰에서는 else if 사용.
switch (call.method) {
case "predictImage":
// Map<String, String> args = (HashMap) call.arguments;
// String path = args.get("path");
String path = call.argument("path");

final Bitmap bitmapRaw = BitmapFactory.decodeFile(path);

int cx = 150, cy = 150;
Bitmap bitmap = Bitmap.createScaledBitmap(bitmapRaw, cx, cy, false);

int[] pixels = new int[cx * cy];
bitmap.getPixels(pixels, 0, cx, 0, 0, cx, cy);

// 두 가지 방법 모두 동작. 입력 텐서는 자료형을 내부적으로 재해석한다.
// float[][][][] input_img = getInputImage_1(pixels, cx, cy);
ByteBuffer input_img = getInputImage_2(pixels, cx, cy);
// float[] input_img = getInputImage_3(pixels, cx, cy);

Interpreter tf_lite = getTfliteInterpreter("cats_and_dogs.tflite");

float[][] pred = new float[1][1];
tf_lite.run(input_img, pred);

final String predText = String.format("%f", pred[0][0]);
Log.d("예측", predText);
result.success(predText);
break;
default:
result.notImplemented();
}
}
}
);
}

// 입력 이미지와 동일한 형태의 배열 사용
private float[][][][] getInputImage_1(int[] pixels, int cx, int cy) {
float[][][][] input_img = new float[1][cx][cy][3];

int k = 0;
for (int y = 0; y < cy; y++) {
for (int x = 0; x < cx; x++) {
int pixel = pixels[k++]; // ARGB : ff4e2a2a

input_img[0][y][x][0] = ((pixel >> 16) & 0xff) / (float) 255;
input_img[0][y][x][1] = ((pixel >> 8) & 0xff) / (float) 255;
input_img[0][y][x][2] = ((pixel >> 0) & 0xff) / (float) 255;
}
}

return input_img;
}

// 다루기 편한 1차원 배열 사용
private ByteBuffer getInputImage_2(int[] pixels, int cx, int cy) {
ByteBuffer input_img = ByteBuffer.allocateDirect(cx * cy * 3 * 4);
input_img.order(ByteOrder.nativeOrder());

for (int i = 0; i < cx * cy; i++) {
int pixel = pixels[i]; // ARGB : ff4e2a2a

input_img.putFloat(((pixel >> 16) & 0xff) / (float) 255);
input_img.putFloat(((pixel >> 8) & 0xff) / (float) 255);
input_img.putFloat(((pixel >> 0) & 0xff) / (float) 255);
}

return input_img;
}

// 될 것 같지만 되지 않는 코드
private float[] getInputImage_3(int[] pixels, int cx, int cy) {
float[] input_img = new float[cx * cy * 3 * 4];

int k = 0;
for (int i = 0; i < cx * cy; i++) {
int pixel = pixels[i]; // ARGB : ff4e2a2a

input_img[k++] = ((pixel >> 16) & 0xff) / (float) 255;
input_img[k++] = ((pixel >> 8) & 0xff) / (float) 255;
input_img[k++] = ((pixel >> 0) & 0xff) / (float) 255;
}

return input_img;
}

// 모델 파일 인터프리터를 생성하는 공통 함수
// loadModelFile 함수에 예외가 포함되어 있기 때문에 반드시 try, catch 블록이 필요하다.
private Interpreter getTfliteInterpreter(String modelPath) {
try {
return new Interpreter(loadModelFile(MainActivity.this, modelPath));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

// 모델을 읽어오는 함수로, 텐서플로 라이트 홈페이지에 있다.
// MappedByteBuffer 바이트 버퍼를 Interpreter 객체에 전달하면 모델 해석을 할 수 있다.
private MappedByteBuffer loadModelFile(Activity activity, String modelPath) throws IOException {
AssetFileDescriptor fileDescriptor = activity.getAssets().openFd(modelPath);
FileInputStream inputStream = new FileInputStream(fileDescriptor.getFileDescriptor());
FileChannel fileChannel = inputStream.getChannel();
long startOffset = fileDescriptor.getStartOffset();
long declaredLength = fileDescriptor.getDeclaredLength();
return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength);
}
}


코드를 교체하고 나서 문제가 발생했다면
오른쪽 상단에 있는 Setup SDK 메뉴를 선택해서 해결해야 한다.
아래와 같은 화면이 뜨고 적당한 버전을 선택해서 연결한다.



9번
개와 고양이 사진을 에뮬레이터로 찍기는 어렵다.
안드로이드 폰에 올려서 결과를 볼 수도 있겠지만
여기서는 에뮬레이터로 복사해서 가져다 쓰도록 한다.

[View - Tool Windows - Device File Explorer] 메뉴를 선택한다.
[sdcard - Download] 폴더에서 마우스 오른쪽 버튼을 눌러 업로드를 선택한다.
개와 고양이 폴더에서 각각 2개씩 추가했다.


  


4장의 사진을 업로드하고 나면 아래와 같이 확인할 수 있다.



이제 모든게 끝났다.
완성을 하지 못해서 계속 비공개로 유지했었다.
마지막으로 우재 폰을 사용해서 카메라 메뉴도 확인했다.
모니터에 있는 개와 고양이 사진을 얼마나 잘 분류하던지..
감동이었다.

일요일 12시 20분.
우재는 곤충 박사님이 부탁한 프로젝트를 하고 있고
서진이는 한동안 잊고 살았던 파이썬을 다시 공부하고 있다.
모두 흥미를 느끼고 있어 좋다.
일요일이 일요일이다.
다만 많이 시끄럽다. ^^