こんにちは、リコーの@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内部で自動的に画像処理してくれます。
事前準備
AndroidStudioプロジェクト
本記事のプラグインではカメラの制御も必要になりますが、その部分は本記事の範疇を超えているので、サンプルコードを流用することにします。
下記リポジトリにて公開されている、CameraAPI12 を使ったサンプルコードをベースにします。
ricohapi/theta-plugin-camera-api-sample
ダウンロードしたものをAndroidStudioで読み込み、プラグインの名称やアプリケーションID等を変更してから使います。
各名称の変更方法は前回記事「プロジェクトファイルの準備 > 2.名称変更」項を参考にしてください。
※メタデータ(Exif、XMP等)について
CameraAPIを使う場合は、撮影データにExifやXMPなどのメタデータが付与されません。
WebAPI経由で撮影した場合はメタデータが付くようになっているため、メタデータが必要な場合はWebAPIベースでの作成を検討してみてください。
ricohapi/theta-automatic-face-blur-plugin の Exifクラス が参考になると思います。
パーミッションの設定
このプラグインを使用するには、 カメラ、マイク、ストレージ の パーミッション許可 が必要です。
パーミッションに許可がなければ、プラグインを起動してもすぐに落ちてしまいます。
なお、プラグインストア経由でインストールしたプラグインについては、マニフェストの記載に従い自動的にパーミッションに許可が入るようになっています。
今回のように開発時には手動で設定を行ってください。
開発時には、Vysorを利用して事前にパーミッション設定をしておくと楽です。
Vysorについては下記記事が参考になります。
THETAプラグイン開発におけるVysorの使い方【THETAプラグイン開発】
パーミッションの設定方法
以下の手順で、パーミッション設定を行います。
- AndroidStudioで、用意したプロジェクトを開く
- Vysorを立ち上げておく
- AndroidStudioから "▶" または "Build > Run" を選択し、THETA上でプラグインを実行
(パーミッション設定をしていないため、実行したプラグインはすぐ落ちてしまうはず) - Vysorにて、ホーム画面からアプリ一覧を表示
- Settings アプリを開き、"(Plugin名) > Permissions" を選択
- 各種項目をONにする
処理の方法
今回作るプラグインでは、サンプルコードの 静止画撮影 の部分を改造します。
静止画撮影では、大まかには以下のような処理を行っています。
- シャッターボタンが押されたことを認識
- 撮影パラメータ(露出やホワイトバランス、画像形式など)の設定
- カメラ制御による撮影
- 撮影画像データの取得
- 画像データの保存
本プラグインでは「4. 撮影画像データの取得」と「5. 画像データの保存」の間で画像処理を行います。
本項では、画像処理前後に必要な処理についてまとめます。
撮影画像データの取得方法
カメラにまつわる処理はCameraFragment.java
で実装されており、撮影画像のデータはCamera.PictureCallback
の onPictureTaken
メソッドから取得できます。
第一引数のbyte[] data
に、JPEG形式で画像データが入っています。
このdata
を画像処理します。
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の並び方は、システムやライブラリによって異なります。それぞれのドキュメントを参照し、並び順を確認してください。
上でも述べましたが、FastCVへ入力するための前処理としては、JPEG から RGBA8888 への変換を行います。
RGBA8888 は Bitmapクラス として扱えます。
さらに注意すべきところとして、NativeコードへはBitmapクラスをそのまま渡せないので、Bitmapクラスから byte配列 を取り出して渡します。
撮影画像データを受け取ってからFastCV(Nativeコード)に渡すまで
前処理を図にすると以下のような流れです。
画像処理の後処理
FastCV画像処理からは、RGBA8888形式の画像データがbyte配列で出力されます。
画像処理の後処理では、画像処理後のデータを JPEG や PNG などに変換(画像圧縮)し、保存するための形式にします。
画像データを圧縮する際には、Bitmapクラスのメソッドを使います。そのために、画像処理後のデータをbyte配列からBitmap型に変換します。
FastCV(Nativeコード)から画像処理後データを受け取ってから保存するまで
後処理を図にすると以下のような流れです。
各フォーマットの相互変換の方法
Android SDKの Bitmap や BitmapFactory を使えば、上に述べた画像の形式や型の相互変換ができます。
これらの相互変換については、下記記事などを参考にしてください。
AndroidでのBitmap/JPEG/byte配列の相互変換
FastCVでの画像処理
画像処理の流れは、以下のような簡単なものです。
- RGB888化
- グレースケール化
- Cannyフィルタ
- 色反転
- RGBA8888化
基本的にFastCV APIを用います。
しかしAPIリファレンスによると RGBA8888 → グレースケール のAPIはなく、代わりに RGB888 → グレースケール はあったので、「1. RGB888化」の処理を加えています。
また、グレースケール → RGBA8888 もないようだったので、「5. RGBA8888化」は自作しました。
関数
1.~4.については、それぞれ以下のAPIを使います。
fcvColorRGBA8888ToRGB888u8()
fcvColorRGB888ToGrayu8()
fcvFilterCanny3x3u8()
fcvBitwiseNotu8()
FastCVでの画像データの扱い
FastCVの関数の引数として画像データを使うものに関しては、アライメント指定がされているものもありました。
たとえばfcvColorRGB888ToGrayu8()
では、入力画像src
と出力画像dst
に128bitでアライメントされた配列を指定されています。
そういった場合、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
クラスを定義しました。
CameraFragment
のonPictureTaken
で撮影画像データをImageProcessor
に渡し、諸々処理するようにしています。
ImageProcessor
からはJPEG圧縮された画像データが返るようにしており、そのまま保存しています。
/**
* 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);
}
}
ImageProcessor
のprocess
メソッドでは、画像データの取り扱い方で紹介した前処理とNativeコードへの値受け渡し、後処理を行っています。
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を有効化しておきます。
...
<application
...
android:largeHeap="true">
...
</application>
...
FastCV(Nativeコード)
Java側から渡ってきた画像をFastCVで画像処理し、処理後データをJavaに戻す処理をNativeコード(C/C++)に実装します。
また、グレースケールからRGBA8888に変換する関数も実装します。
// ...
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)への参加もよろしくおねがいします。