Help us understand the problem. What is going on with this article?

THETAプラグインで画像処理【FastCV画像処理編】

こんにちは、リコーの@roohii_3です。
本記事は「THETAプラグインで画像処理【FastCV導入編】」の続きです。


※【11/28(水) もくもく会@銀座】 11/28(水)にRICOH THETAプラグイン開発のもくもく会を企画しています。 ご興味ある方はぜひご参加ください。

→ご参加頂いた皆様ありがとうございました!

はじめに

2018年11月現在のRICOH THETAの最新機種、RICOH THETA Vでは「プラグイン」機能を使ってTHETAをカスタマイズできます。
さらに、「RICOH THETAプラグインパートナープログラム」に登録するとプラグインを作ることもできます。
パートナープログラムについては、記事の一番最後をご覧ください。

本記事では前回に引き続き、THETAプラグインでFastCVを動かす方法をまとめます。
前回は FastCVの導入まで でしたが、今回はいよいよ 画像処理をする手順 をご紹介したいと思います。

また、本記事のソースコードは下記で公開されているので参考にしてみてください。

ricohapi/theta-plugin-fastcv-sample

先にお見せすると、このプラグインを使うと下のような画像が撮れます。
普通に静止画を撮影するようにシャッターを押せば、THETA内部で自動的に画像処理してくれます。
canny_sample_thum.png

事前準備

AndroidStudioプロジェクト

本記事のプラグインではカメラの制御も必要になりますが、その部分は本記事の範疇を超えているので、サンプルコードを流用することにします。
下記リポジトリにて公開されている、CameraAPI12 を使ったサンプルコードをベースにします。

ricohapi/theta-plugin-camera-api-sample

ダウンロードしたものをAndroidStudioで読み込み、プラグインの名称やアプリケーションID等を変更してから使います。
各名称の変更方法は前回記事「プロジェクトファイルの準備 > 2.名称変更」項を参考にしてください。

※メタデータ(Exif、XMP等)について

CameraAPIを使う場合は、撮影データにExifやXMPなどのメタデータが付与されません。
WebAPI経由で撮影した場合はメタデータが付くようになっているため、メタデータが必要な場合はWebAPIベースでの作成を検討してみてください。
ricohapi/theta-automatic-face-blur-pluginExifクラス が参考になると思います。

パーミッションの設定

このプラグインを使用するには、 カメラ、マイク、ストレージパーミッション許可 が必要です。
パーミッションに許可がなければ、プラグインを起動してもすぐに落ちてしまいます。

なお、プラグインストア経由でインストールしたプラグインについては、マニフェストの記載に従い自動的にパーミッションに許可が入るようになっています。
今回のように開発時には手動で設定を行ってください。

開発時には、Vysorを利用して事前にパーミッション設定をしておくと楽です。
Vysorについては下記記事が参考になります。

THETAプラグイン開発におけるVysorの使い方【THETAプラグイン開発】

パーミッションの設定方法

以下の手順で、パーミッション設定を行います。

  • AndroidStudioで、用意したプロジェクトを開く
  • Vysorを立ち上げておく
  • AndroidStudioから "▶" または "Build > Run" を選択し、THETA上でプラグインを実行
    (パーミッション設定をしていないため、実行したプラグインはすぐ落ちてしまうはず)
  • Vysorにて、ホーム画面からアプリ一覧を表示
  • Settings アプリを開き、"(Plugin名) > Permissions" を選択
  • 各種項目をONにする
    permissions.png

処理の方法

今回作るプラグインでは、サンプルコード静止画撮影 の部分を改造します。
静止画撮影では、大まかには以下のような処理を行っています。

  1. シャッターボタンが押されたことを認識
  2. 撮影パラメータ(露出やホワイトバランス、画像形式など)の設定
  3. カメラ制御による撮影
  4. 撮影画像データの取得
  5. 画像データの保存

本プラグインでは「4. 撮影画像データの取得」と「5. 画像データの保存」の間で画像処理を行います。
本項では、画像処理前後に必要な処理についてまとめます。

撮影画像データの取得方法

カメラにまつわる処理はCameraFragment.javaで実装されており、撮影画像のデータはCamera.PictureCallbackonPictureTaken メソッドから取得できます。
第一引数のbyte[] dataに、JPEG形式で画像データが入っています。
このdataを画像処理します。

CameraFragment.java
private Camera.PictureCallback onJpegPictureCallback = new Camera.PictureCallback() {
    @Override
    public void onPictureTaken(byte[] data, Camera camera) {

        // ...

        byte[] dst = mImageProcessor.process(data);

        // ...
    }
};

上の例では、ImageProcessorクラスのprocessメソッドに撮影画像データdataを渡すと、画像処理されたデータdstが返ってくることを想定しています。

画像データの取り扱い方

画像処理の前処理

上で触れたように、onPictureTaken で取得できる画像は JPEG形式 になっています。
一方で、FastCVではJPEG形式を扱うことを想定していません3
そのため、FastCVに入力する画像は RGBA8888 などのピクセルフォーマットに変換しなければなりません。

RGBA8888形式について

RGBA8888のデータの中身は下図のようになっています。
画素単位でRed, Green, Blue, Alpha(不透明度)の値が順に並び、各チャンネルには0~255(8bit)の値が入ります。
JPEGは画像圧縮形式であることに対し、このようにRGBA8888では全画素の情報を持ちます。
※ R,G,B,Aの並び方は、システムやライブラリによって異なります。それぞれのドキュメントを参照し、並び順を確認してください。

rgba8888.png

上でも述べましたが、FastCVへ入力するための前処理としては、JPEG から RGBA8888 への変換を行います。
RGBA8888Bitmapクラス として扱えます。
さらに注意すべきところとして、NativeコードへはBitmapクラスをそのまま渡せないので、Bitmapクラスから byte配列 を取り出して渡します。

撮影画像データを受け取ってからFastCV(Nativeコード)に渡すまで

前処理を図にすると以下のような流れです。

before-proc.png

画像処理の後処理

FastCV画像処理からは、RGBA8888形式の画像データがbyte配列で出力されます。
画像処理の後処理では、画像処理後のデータを JPEGPNG などに変換(画像圧縮)し、保存するための形式にします。
画像データを圧縮する際には、Bitmapクラスのメソッドを使います。そのために、画像処理後のデータをbyte配列からBitmap型に変換します。

FastCV(Nativeコード)から画像処理後データを受け取ってから保存するまで

後処理を図にすると以下のような流れです。

after-proc.png

各フォーマットの相互変換の方法

Android SDKの BitmapBitmapFactory を使えば、上に述べた画像の形式や型の相互変換ができます。
これらの相互変換については、下記記事などを参考にしてください。
AndroidでのBitmap/JPEG/byte配列の相互変換

FastCVでの画像処理

画像処理の流れは、以下のような簡単なものです。

  1. RGB888化
  2. グレースケール化
  3. Cannyフィルタ
  4. 色反転
  5. RGBA8888化

基本的にFastCV APIを用います。
しかしAPIリファレンスによると RGBA8888 → グレースケール のAPIはなく、代わりに RGB888 → グレースケール はあったので、「1. RGB888化」の処理を加えています。
また、グレースケール → RGBA8888 もないようだったので、「5. RGBA8888化」は自作しました。

関数

1.~4.については、それぞれ以下のAPIを使います。

  1. fcvColorRGBA8888ToRGB888u8()
  2. fcvColorRGB888ToGrayu8()
  3. fcvFilterCanny3x3u8()
  4. fcvBitwiseNotu8()

FastCVでの画像データの扱い

FastCVの関数の引数として画像データを使うものに関しては、アライメント指定がされているものもありました。
たとえばfcvColorRGB888ToGrayu8()では、入力画像srcと出力画像dstに128bitでアライメントされた配列を指定されています。

alignment.png

そういった場合、fcvMemAlloc()を使うと、メモリの動的確保と同時にアライメント指定もできるようです。
解放時にはfcvMemFree()を使います。

void* dst = fcvMemAlloc( width*height, 16);     // <- 128bitアライメントでメモリを確保

fcvColorRGBA8888ToRGB888u8((uint8_t*)src, width, height, 0, (uint8_t*)dst, 0);

fcvMemFree(dst);    // <- 解放

実装

本項で実装例を紹介します。
以下では 画像取得~画像処理~保存 の部分を抜粋しており、実際はFastCVの 初期化・終了処理 も実装する必要があります。
FastCVを扱う部分はNativeコード(C/C++)で実装します。
詳しくは前回記事をご参照ください。
また、下記実装例ではエラー処理が不十分なので流用される際はご注意ください。

Java側

画像取得~FastCV(Nativeコード)に渡すまで と、Nativeから画像処理後データを取得~保存まで を実装します。

ここでは、Java ↔ Nativeコードのやりとりを担うImageProcessorクラスを定義しました。
CameraFragmentonPictureTakenで撮影画像データをImageProcessorに渡し、諸々処理するようにしています。
ImageProcessorからはJPEG圧縮された画像データが返るようにしており、そのまま保存しています。

CameraFragment.java
/**
 * CameraFragment
 */
public class CameraFragment extends Fragment {

    private Camera.PictureCallback onJpegPictureCallback = new Camera.PictureCallback() {
        @Override
        public void onPictureTaken(byte[] data, Camera camera) {

            // ...

            byte[] dst = mImageProcessor.process(data);     // <- 画像データを渡すと画像処理結果が返る

            if (dst != null) {
                // 画像保存
                String fileName = String.format("fastcv_%s.jpg", getDateTime());
                String fileUrl = DCIM + "/" + fileName;
                try (FileOutputStream fileOutputStream = new FileOutputStream(fileUrl)) {
                    fileOutputStream.write(dst);            // <- 画像保存
                    registerDatabase(fileName, fileUrl);    // <- Databaseへ登録
                    Log.d("CameraFragment", "save: " + fileUrl);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            // ...

        }
    };

    // ...

    private void registerDatabase(String fileName, String filePath){
        ContentValues values = new ContentValues();
        ContentResolver contentResolver = this.getContext().getContentResolver();
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
        values.put(MediaStore.Images.Media.TITLE, fileName);
        values.put("_data", filePath);
        contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
    }   
}

ImageProcessorprocessメソッドでは、画像データの取り扱い方で紹介した前処理とNativeコードへの値受け渡し、後処理を行っています。

ImageProcessor.java
public class ImageProcessor {

    // ...

    public byte[] process(byte[] data) {
        Log.d(TAG, "ImageProcess");

        /**
        * 前処理
        * JPEG形式からRGBA8888形式に変換する
        */
        // BitmapFactory.decodeByteArray()で、byte配列→Bitmap型の変換と同時にJPEG形式→RGBA8888形式への変換が行われる
        Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length);
        // byte配列を取り出すために、ByteBufferにBitmapの中身をコピー
        ByteBuffer byteBuffer = ByteBuffer.allocate(bmp.getByteCount());
        bmp.copyPixelsToBuffer(byteBuffer);

        int width = bmp.getWidth();
        int height = bmp.getHeight();


        /**
        * FastCV(nativeコード)で画像処理
        */
        // byteBuffer.array()でbyte配列を取り出し、nativeコードへ渡す
        byte[] dstBmpArr = cannyFilter(byteBuffer.array(), width, height);
        if (dstBmpArr == null) {
            Log.e(TAG, "Failed to cannyFilter(). dstBmpArr is null.");
            return null;
        }


        /**
        * 後処理
        * 画像処理後データをJPEG形式に圧縮する
        */
        // byte配列をBitmap型変数の中身にコピー
        Bitmap dstBmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        dstBmp.copyPixelsFromBuffer(ByteBuffer.wrap(dstBmpArr));

        // JPEGに圧縮
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        dstBmp.compress(Bitmap.CompressFormat.JPEG,100, baos);
        byte[] dst = baos.toByteArray();

        return dst;
    }

    // Native Functions
    private native byte[] cannyFilter(byte[] data, int width, int height);

    // ...

}

画像データの処理にメモリを多く使うため、AndroidManifest.xmlでLargeHeapを有効化しておきます。

AndroidManifest.xml
    ...

    <application
        ...
        android:largeHeap="true">
        ...
    </application>
    ...

FastCV(Nativeコード)

Java側から渡ってきた画像をFastCVで画像処理し、処理後データをJavaに戻す処理をNativeコード(C/C++)に実装します。
また、グレースケールからRGBA8888に変換する関数も実装します。

FastCVSample.cpp
// ...

void colorGrayToRGBA8888(const uint8_t* src, unsigned int width, unsigned int height, uint8_t* dst){

    if (src == NULL) {
        DPRINTF("colorGrayToRGBA8888() src is NULL.");
        return;
    }
    if (dst == NULL) {
        DPRINTF("colorGrayToRGBA8888() dst is NULL.");
        return;
    }

    // グレースケールからRGBA8888へ変換
    // R,G,Bに同じ値をコピーする
    for (unsigned int i = 0; i < width*height; i++) {
        dst[4*i]   = src[i];
        dst[4*i+1] = src[i];
        dst[4*i+2] = src[i];
        dst[4*i+3] = 0xFF;     // アルファチャンネル
    }
}

// ...

JNIEXPORT jbyteArray
JNICALL Java_com_theta360_fastcvsample_ImageProcessor_cannyFilter
        (
                JNIEnv* env,
                jobject obj,
                jbyteArray img,
                jint w,
                jint h
        )
{
    DPRINTF("cannyFilter()");

    /**
     * convert input data to jbyte object
     */
    jbyte* jimgData = NULL;
    jboolean isCopy = 0;
    jimgData = env->GetByteArrayElements( img, &isCopy);
    if (jimgData == NULL) {
        DPRINTF("jimgData is NULL");
        return NULL;
    }

    /**
     * process
     */
    // RGBA8888 -> RGB888
    void* rgb888 = fcvMemAlloc( w*h*4, 16);
    fcvColorRGBA8888ToRGB888u8((uint8_t*) jimgData, w, h, 0, (uint8_t*)rgb888, 0);

    // グレースケール化
    void* gray = fcvMemAlloc( w*h, 16);
    fcvColorRGB888ToGrayu8((uint8_t*)rgb888, w, h, 0, (uint8_t*)gray, 0);

    // Cannyフィルタ適用
    void* canny = fcvMemAlloc(w*h, 16);
    // Cannyフィルタの閾値(第5, 6引数)は適当な値
    fcvFilterCanny3x3u8((uint8_t*)gray,w,h,(uint8_t*)canny, 10, 30);

    // 色反転
    void* bitwise_not = fcvMemAlloc( w*h, 16);
    fcvBitwiseNotu8((uint8_t*)canny, w, h, 0, (uint8_t*)bitwise_not, 0);

    // グレースケール -> 3ch + アルファch
    void* dst_rgba8888 = fcvMemAlloc( w*h*4, 16);
    colorGrayToRGBA8888((uint8_t*)bitwise_not, w, h, (uint8_t*)dst_rgba8888);

    /**
     * copy to destination jbyte object
     */
    jbyteArray dst = env->NewByteArray(w*h*4);
    if (dst == NULL){
        DPRINTF("dst is NULL");
        // release
        fcvMemFree(dst_rgba8888);
        fcvMemFree(bitwise_not);
        fcvMemFree(canny);
        fcvMemFree(gray);
        fcvMemFree(rgb888);
        env->ReleaseByteArrayElements(img, jimgData, 0);
        return NULL;
    }
    env->SetByteArrayRegion(dst,0,w*h*4,(jbyte*)dst_rgba8888);
    DPRINTF("copy");

    // release
    fcvMemFree(dst_rgba8888);
    fcvMemFree(bitwise_not);
    fcvMemFree(canny);
    fcvMemFree(gray);
    fcvMemFree(rgb888);
    env->ReleaseByteArrayElements(img,jimgData,0);

    DPRINTF("processImage end");
    return dst;
}

// ...

おわりに

FastCVがらみのデータの持ち方や関数の使い方など、慣れるのに時間は掛かりそうです…。
今回は簡単に、FastCVの既存フィルタを掛けてみるものでしたが、工夫をしたら絵画調などもできそうですね。
また、動画撮影中やモニタリング時に画像処理・認識処理ができないか試してみたいところです。

THETAプラグイン + Computer Visionに興味がある方も、ぜひやってみてください!

RICOH THETAプラグインパートナープログラムについて

RICOH THETAプラグインをご存じない方はこちらをご覧ください。
RICOH THETAプラグイン開発者コミュニティの以下の記事もぜひご覧ください。

興味を持たれた方はtwitterのフォローとTHETAプラグイン開発コミュニティ(slack)への参加もよろしくおねがいします。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away