LoginSignup
0
0

More than 5 years have passed since last update.

AndroidThings & Raspberry Pi3 で猫用扇風機を作る(4)

Last updated at Posted at 2017-12-15

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などなど」を一部変更しています。

BitmapUtils.java
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();

    }
}
CatFansActivity.java
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回に続きます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0