2017年10月28日に実施された「東北Tech道場 郡山道場 第7回」の作業内容をまとめます。
前回までの作業で、公式サンプルアプリのDoorbellをほどんど流用して、5秒おきに写真を撮る「CatFans」を作成しましたが、いくつか問題が出てきました。
- スマホでFirebaseに保存した画像と注釈を確認すると、画像は表示されるが注釈は「no annotations yet」になっている。
- 画像サイズが2Mバイトを超えるため、スマホで確認するときに通信量が多くなる。
今回はこれらの問題を解決することにしました。
#1. "no annotations yet" になる問題
##(1) HandlerThreadがうまく動作していない
元々Doorbellでは、LooperとHandlerを使って、Cloud Vision APIを呼んでいます。調べたところによると、Androidでは時間のかかる処理はHandlerThreadを使ってバッググラウンドで処理をするように作るようです。LooperとHandlerはそのための仕組みです。Doorbellでは、カメラから取得したイメージとタイムスパンは即時にFirebaseに書き込みに行くのですが、Cloud Vision APIで画像解析する部分は時間がかかるためだと思いますが、別スレッドで動くようになっています。ところが、ブレークポイントを設置してもこのスレッドに一向に止まらないのです。そのため、画像はFirebeseに保存するが、注釈は付かない、という状況になっています。
###(i) HandlerThreadを外してみる
試しに、スレッドは使わず、画像のFirebeseへのアップロードと同時にCloud Vision APIを呼ぶようにコメントアウトしてみました。けれども、Cloud Vision APIを呼んでいる
Map<String, Float> annotations = CloudVisionUtils.annotateImage(imageBytes);
の部分で戻ってこなくなってしまいました。と、思ったら戻ってきましたがものすごく時間がかかります。
これは、ファイルサイズが大きいためではないかと思い、ファイルサイズを小さくすることにしました。
###(ii) ファイルサイズを小さくする
カメラで撮った画像のサイズはおよそ2Mバイトもあるので、「Android: 画像周りの処理を楽にするUtilクラス。Bitmap生成、拡大縮小リサイズ、バイト配列<=>Bitmapなどなど」から「createScaleBitmap」メソッドを使わせていただき、320×240の大きさにしてからCloud Vision APIに渡すようにしました。すると、すぐに戻ってくるようになりました。また、画像サイズもおよそ25kバイトと飛躍的に少なくなりました。
###(iii) コメントアウトを戻す
そこで、(i)でコメントアウトしていた部分を戻してみました。すると、スレッドも正常に動くようになりました。結局、カメラの性能が良すぎるため、画像サイズが大きすぎて正常に処理できなかった、というのが "no annotations yet" になった理由のようです。
#2. 修正ソース
以下、今回修正した部分のソースです。なお、BitmapUtilsクラスは、「Android: 画像周りの処理を楽にするUtilクラス。Bitmap生成、拡大縮小リサイズ、バイト配列<=>Bitmapなどなど」を一部変更しています。
package xxxxxx.catfans;
/**
* https://qiita.com/ogawatachi/items/3000f0b8232e96bd762e
*/
import java.io.ByteArrayOutputStream;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
/**
* 画像変換クラス
*
*/
public class BitmapUtils {
/**
* バイト配列からBitpmap生成 表示サイズ合わせて拡大縮小します。
*
* @param bytes
* バイト配列の画像
* @param wPercent
* 表示幅拡大縮小率
* @param hPercent
* 表示高さ拡大縮小率
* @return 生成Bitmap
*/
public static Bitmap createScaleBitmap(byte[] bytes, float wPercent, float hPercent) {
Bitmap bm = byte2bmp(bytes);
Matrix matrix = new Matrix();
float xScale = wPercent * 0.8f;
float yScale = hPercent * 0.8f;
matrix.postScale(xScale, yScale);
return Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true);
}
/**
* バイト配列をbitmapに変換します。
*
* @param bytes
* @return
*/
public static Bitmap byte2bmp(byte[] bytes) {
Bitmap bmp = null;
if (bytes != null) {
bmp = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
}
return bmp;
}
/**
* bitmapをバイト配列に変換します。
*
* @param bitmap
* ビットマップ
* @param format
* 圧縮フォーマット
* @param compressVal
* 圧縮率
* @return
*/
public static byte[] bmp2byteArray(Bitmap bitmap, CompressFormat format, int compressVal) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(format, compressVal, baos);
return baos.toByteArray();
}
}
package xxxxxx.catfans;
import android.Manifest;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.media.Image;
import android.media.ImageReader;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Base64;
import android.util.Log;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ServerValue;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.TimerTask;
import java.util.Timer;
import static kitcut.catfans.BitmapUtils.bmp2byteArray;
import static kitcut.catfans.BitmapUtils.createScaleBitmap;
/**
* ラスベリーパイ3カメラの画像をタイマーでキャプチャし、
* FirebaseとGoogle Cloud Vision APIに投稿するCatFansアクティビティ
*/
public class CatFansActivity extends Activity {
private static final String TAG = CatFansActivity.class.getSimpleName();
private FirebaseDatabase mDatabase;
private CatFansCamera mCamera;
private Timer timer;
/**
* A {@link Handler} カメラタスクをバックグラウンドで実行するために必要
*/
private Handler mCameraHandler;
/**
* UIをブロックすべきでないCameraタスクを実行するための追加のスレッド
*/
private HandlerThread mCameraThread;
/**
* A {@link Handler} バックグラウンドでCloudタスクを実行
*/
private Handler mCloudHandler;
/**
* UIをブロックすべきでないCloudタスクを実行するための追加のスレッド。
*/
private HandlerThread mCloudThread;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "CatFans Activity created.");
// カメラにアクセスするための許可が必要
if (checkSelfPermission(Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
// 権限を自動付与する際に問題が発生した
Log.d(TAG, "No permission");
return;
}
mDatabase = FirebaseDatabase.getInstance();
// カメラおよびネットワーク操作用の新しいハンドラと関連するスレッドを作成
mCameraThread = new HandlerThread("CameraBackground");
mCameraThread.start();
mCameraHandler = new Handler(mCameraThread.getLooper());
mCloudThread = new HandlerThread("CloudThread");
mCloudThread.start();
mCloudHandler = new Handler(mCloudThread.getLooper());
mCamera = CatFansCamera.getInstance();
mCamera.initializeCamera(this, mCameraHandler, mOnImageAvailableListener);
//タイマーの作成と設定
timer = new Timer();
TimerTask timerTask = new CatFansTimerTask(this);
timer.scheduleAtFixedRate(timerTask, 0, 5000); //5秒ごとにタイマーを起動
}
//カメラで写真を撮る
public void takePicture() {
mCamera.takePicture();
Log.d(TAG, "Take a picture.");
}
@Override
protected void onDestroy() {
super.onDestroy();
mCamera.shutDown();
mCameraThread.quitSafely();
mCloudThread.quitSafely();
}
/**
* 新しいカメラ画像のリスナー
*/
private ImageReader.OnImageAvailableListener mOnImageAvailableListener =
new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireLatestImage();
// イメージバイトを取得する
ByteBuffer imageBuf = image.getPlanes()[0].getBuffer();
final byte[] imageBytes = new byte[imageBuf.remaining()];
imageBuf.get(imageBytes);
onPictureTaken(imageBytes);
image.close();
}
};
/**
* Cloud Visionで画像にコメントを付けてからFirebaseにアップロードする
*/
private void onPictureTaken(final byte[] imageBytes) {
if (imageBytes != null) {
final DatabaseReference log = mDatabase.getReference("logs").push();
//画像を画面サイズに合わせて縮小する
Bitmap b = createScaleBitmap(imageBytes, 1.0F, 1.0F);
byte[] temp = bmp2byteArray(b, Bitmap.CompressFormat.JPEG, 100);
final byte[] imageB = temp;
String imageStr = Base64.encodeToString(imageB, Base64.NO_WRAP | Base64.URL_SAFE);
// イメージをfirebaseにアップロードする
log.child("timestamp").setValue(ServerValue.TIMESTAMP);
log.child("image").setValue(imageStr);
mCloudHandler.post(new Runnable() {
@Override
public void run() {
Log.d(TAG, "sending image to cloud vision");
// Cloud Vision APIにアップロードして画像に注釈を付ける
try {
Map<String, Float> annotations = CloudVisionUtils.annotateImage(imageB);
Log.d(TAG, "cloud vision annotations:" + annotations);
if (annotations != null) {
log.child("annotations").setValue(annotations);
}
} catch (IOException e) {
Log.e(TAG, "Cloud Vison API error: ", e);
}
}
});
}
}
}
第8回に続きます。