アプリ開発 メモ (写真 を 漫画スケッチ・版画風に!)
2015年01月05日
アプリ開発 メモ (写真 を 漫画スケッチ・版画風に!)
写真 を 漫画スケッチ・版画風に!
/////
WEB ダイレクト
画像加工 (WEB)
画像加工 (WEB)
/////
http://japan.kusocartoon.com/photo-to-cartoon.php
/////
/////
http://www.photo-kako.com/comic.cgi
/////
/////
写真が「版画」風
http://www.bannerkoubou.com/photoeditor/woodcut
/////
/////
アプリ
http://tokyo.supersoftware.co.jp/mangacamera/
iPhone Android
オタクカメラ - 写真を漫画風
http://www.medias-joshibu.net/2013/04/04/10904/index.html
重要
当サイトはベータ版で、まだ不完全なところが残っています。インターネット上で公開したくない写真は、アップロードしないで下さい。
当サイトでは、無料アカウントへのサインアップ を推奨しています。無料アカウントを作成すると、すべての写真をパーソナルギャラリーで管理できます。
/////
App 作成 メモ
OpenCVで写真を漫画風に加工しよう 〜実装編〜
/////
OpenCVで写真を漫画風に加工してみよう 〜手法編〜
http://dev.classmethod.jp/smartphone/opencv-manga-1/最近iOS界隈では漫画風に画像を加工するカメラアプリが人気を博しております。写真に何らの加工を施すには、画像処理技術が必要です。画像処理と聞くと敷居が高そうに思われる方も多いかと思いますが、実はそんなに難しくありません。難しくないと言っても、じゃあ作ってみろよと言われると意外とわからないこともありますよね。実際に漫画風に加工するとなるとどのように実装するのか。。。
というわけで今回から2回に分けて写真を漫画風に加工するための画像処理について、手法と実装の2回に分けて解説していこうと思います。今回は手法の解説だけになるので、すぐにでも手を動かしたい方はこの記事をすっ飛ばして次の記事を読んでください。
使用する画像処理技術のあたりをつけよう
まずは加工後の写真をイメージして、使用する画像処理技術のあたりをつけていきましょう。
画像処理に関するアルゴリズムは既に一般化されているものがたくさんあります。今回のように目標(漫画風にする)がはっきりしている場合は、独自にどうこうするのではなく、まずは既にあるものから使えそうなものを探してみましょう。画像処理に関する情報はWEB、書籍ともに非常に充実しております。ここでは私が実際に読んだものを紹介しますが、とりあえず今の時点ですべて目を通す必要はありません。こんなものがあるんだぐらいに見といてください。
http://homepage2.nifty.com/tsugu/sotuken/ronbun/sec3-2.html
画像処理におけるアルゴリズム 画像処理におけるアルゴリズムの種類や内容を非常に簡単に解説しています。とても参考になりました。
https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html
Core Image Filter Reference(iOS Developer Library) Mac OS X/iOSアプリで標準で使用できるCoreImage.frameworkを用いた画像処理について解説されています。ここで紹介されている画像処理をすべてiOSアプリ開発で使用できるわけではありませんが、Objecive-Cに慣れている方であれば、こういう切り口から画像処理の世界に入るのもありではないでしょうか。
http://opencv.jp/sample/filter_and_color_conversion.html
opencv.jp - OpenCV: フィルタと色変換(Filters and Color Conversion)サンプルコード - OpenCVを使用した画像処理をサンプルコードと一緒に解説しています。OpenCVはインテルが開発したオープンソースのC/C++、Java、Python用ライブラリで、iOS以外にもMac OS Xはもちろん、Linux、Windows、Androidなど様々なプラットフォームで動作します。iOSに関してはむしろ最近標準でサポート(armv7sでは動きませんが)されたばかりです。
画像処理に関してまだ駆け出しのひよっこの癖に生意気なことを書かせて頂きましたが本題に戻りましょう。私は写真を漫画風に加工する上で、以下の2つのフィルタ処理が近そうだなぁと考えました。
エッジ検出(輪郭検出)
Wikipediaによると、エッジ検出とは「画像処理やコンピュータビジョンの用語で、特徴検出や特徴抽出の一種であり、デジタル画像の画像の明るさが鋭敏に(より形式的に言えば不連続に)変化している箇所を特定するアルゴリズムを指す」とのことです(参考:エッジ検出 - Wikipedia)。わかりやすく言うと、画像中にあるモノの境界線を検出する処理です。実際にエッジ検出処理をかけた画像を見てみましょう。
エッジ検出の実行例
これは輪郭を線でなぞったような効果を出すのに使えそうですね。
2値化
上で紹介した画像処理におけるアルゴリズムによると、「指定画像を白と黒の2階調の画像に変換する処理」とあります。具体的には、RGB各要素の平均値がしきい値より高ければ白、低ければ黒と言った具合に分けて、写真を真っ白と真っ黒の2色だけの画像を生成します。実際に2値化を施した画像を見てみましょう。
2値化の実行例
これも写真をモノクロにするために使えそうです。
この2つのアルゴリズムを使用すれば、写真を漫画風に加工したような効果を演出することができそうです。
画像処理工程の計画をたてよう
使えそうなフィルタは出揃いましたが、単体でみるとまだまだ完成にはほど遠いです。これらのフィルタを利用して、どうすれば完成に近づくか模索しながらどういう処理が必要か計画をたてていきましょう。
私は漫画風の写真を構成する要素として以下の3つに分けて考えました。
輪郭
白黒の部分
スクリーントーンの部分
この3つの要素を合成させれば写真を漫画風に加工できそうです。それでは実際にどういう工程でこれらの要素を生成するか計画をたてましょう。
1.輪郭
撮影した写真から黒いペンで輪郭をなぞったような画像です。これは先ほどあたりをつけたエッジ検出が使えそうですが、エッジ検出処理を施した画像を見ると加工してやる必要がありそうです。そこで私は以下のような工程を考えました。
エッジ検出する
エッジ検出した画像の色を反転する
白い部分を透過する
2.白黒の部分
次に白黒の部分です。これは2値化が使えそうです。しかし、2値化してしまうと白黒だけになってしまい、淡い色の部分、すなわちスクリーントーン部分が表現できません。 ここは2値化のアルゴリズムを利用して、白と黒の2値化ではなく白・灰色・黒の3値化することで対応しましょう。工程としては以下の通りです。
画像を3値化する
灰色の部分を透過する
灰色部分を透過するのは、次に紹介するスクリーントーンの部分を表示するためです。
3.スクリーントーンの部分
最後のスクリーントーンの部分。こいつは非常に簡単で以下の画像を輪郭画像と白黒部分の画像の最背面に配置するだけです。
今回の目標
これで漫画風の写真が生成できそうですね。画像に施す処理の工程がはっきりしました。次回は実際に手を動かして画像を加工してみましょう。
/////
http://opencv.jp/sample/filter_and_color_conversion.html
作成者: 怡土順一, 最終変更者: 怡土順一, 最終変更リビジョン: 342, 最終変更日時: 2007-10-14 23:39:34 +0900 (日, 14 10月 2007)
■ フィルタ
フィルタという単語は,非常に幅広い意味を持つ. OpenCVのリファレンス マニュアルでは, 主に二次元マトリックス(フィルタ,カーネルなどと呼ばれる)と, 画像の処理対象領域の画素値との畳み込み演算を行うことによって実現される, いわゆる空間フィルタ処理についての関数をフィルタ処理としている. 画像勾配を求める処理(微分フィルタ)もフィルタ処理の一部ではあるが,そ れは別途解説されているので,そちらを参照すること.
サンプル
平滑化 cvSmooth
ブラー,ガウシアン,メディアン,バイラテラル,の各フィルタによる平滑化
サンプルコード
#include <cv.h>
#include <highgui.h>
int
main (int argc, char **argv)
{
int i;
IplImage *src_img = 0, *dst_img[4];
// (1)画像を読み込む
if (argc >= 2)
src_img = cvLoadImage (argv[1], CV_LOAD_IMAGE_ANYDEPTH | CV_LOAD_IMAGE_ANYCOLOR);
if (src_img == 0)
exit (-1);
for (i = 0; i < 4; i++)
dst_img[i] = cvCloneImage (src_img);
// (2)手法を指定して画像を平滑化
cvSmooth (src_img, dst_img[0], CV_BLUR, 5, 0, 0, 0);
cvSmooth (src_img, dst_img[1], CV_GAUSSIAN, 11, 0, 0, 0);
cvSmooth (src_img, dst_img[2], CV_MEDIAN, 5, 0, 0, 0);
cvSmooth (src_img, dst_img[3], CV_BILATERAL, 80, 80, 0, 0);
// (3)処理された画像を実際に表示
cvNamedWindow ("Blur", CV_WINDOW_AUTOSIZE);
cvShowImage ("Blur", dst_img[0]);
cvNamedWindow ("Gaussian", CV_WINDOW_AUTOSIZE);
cvShowImage ("Gaussian", dst_img[1]);
cvNamedWindow ("Median", CV_WINDOW_AUTOSIZE);
cvShowImage ("Median", dst_img[2]);
cvNamedWindow ("Bilateral", CV_WINDOW_AUTOSIZE);
cvShowImage ("Bilateral", dst_img[3]);
cvWaitKey (0);
cvDestroyWindow ("Blur");
cvDestroyWindow ("Gaussian");
cvDestroyWindow ("Median");
cvDestroyWindow ("Bilateral");
cvReleaseImage (&src_img);
for (i = 0; i < 4; i++) {
cvReleaseImage (&dst_img[i]);
}
return 0;
}
// (1)画像を読み込む
コマンド引数で指定されたファイル名の画像(入力画像)をオープンし,関数 cvLoadImage()で読み込む.2番目の引数にCV_LOAD_IMAGE_ANYDEPTH | CV_LOAD_IMAGE_ANYCOLORを指定することで,元画像のデプス,チャンネルを変 更せずに読み込む.
// (2)手法を指定して画像を平滑化
関数cvSmooth()の3番目の引数で, CV_BLUR(ブラーフィルタ),CV_GAUSSIAN(ガウシアンフィルタ),CV_MEDIAN(メディアンフィルタ),CV_BILATERAL(バイラテラルフィルタ) の各手法を指定して平滑化を行う. 4番目以降の引数の意味は,手法毎に異なる.詳しくは,リファレンス マニュアルを参照すること.
// (3)処理された画像を実際に表示
各手法で平滑化された画像を実際に表示し,何かキーが押されるまで待つ.
実行結果例
下段の画像は,処理画像の一部を拡大したものである. ただし,手法毎にパラメータを変えてあるので,単純に比較できる画像ではない.
入力画像 Blur Gaussian Median Bilateral
ユーザ定義フィルタ cvFilter2D
ユーザが定義したカーネルによるフィルタリング
サンプルコード
#include <cv.h>
#include <highgui.h>
#include <stdio.h>
int
main (int argc, char **argv)
{
IplImage *src_img = 0, *dst_img;
float data[] = { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
};
CvMat kernel = cvMat (1, 21, CV_32F, data);
// (1)画像の読み込み
if (argc >= 2)
src_img = cvLoadImage (argv[1], CV_LOAD_IMAGE_ANYDEPTH | CV_LOAD_IMAGE_ANYCOLOR);
if (src_img == 0)
exit (-1);
dst_img = cvCreateImage (cvGetSize (src_img), src_img->depth, src_img->nChannels);
// (2)カーネルの正規化と,フィルタ処理
cvNormalize (&kernel, &kernel, 1.0, 0, CV_L1);
cvFilter2D (src_img, dst_img, &kernel, cvPoint (0, 0));
// (3)処理画像の表示
cvNamedWindow ("Filter2D", CV_WINDOW_AUTOSIZE);
cvShowImage ("Filter2D", dst_img);
cvWaitKey (0);
cvDestroyWindow ("Filter2D");
cvReleaseImage (&src_img);
cvReleaseImage (&dst_img);
return 0;
}
// (1)画像の読み込み
コマンド引数で指定されたファイル名の画像(入力画像)をオープンし,関数 cvLoadImage()で読み込む.2番目の引数にCV_LOAD_IMAGE_ANYDEPTH | CV_LOAD_IMAGE_ANYCOLORを指定することで,元画像のデプス,チャンネルを変 更せずに読み込む.
// (2)カーネルの正規化と,フィルタ処理
浮動小数点型配列dataの値を各要素とする,1×21;の行列をカーネルとするフィルタ処理を行う. 関数cvNormalize()によって,カーネルを正規化する. つまり,ここでは,絶対値のノルムが"1.0"になるように,配列の値を正規化している. そして,そのカーネルを利用(関数cvFilter2D()の3番目の引数に指定)して,フィルタ処理を行う. 関数cvFilter2D()の4番目の引数は,フィルタ対象ピクセルのカーネル内での相対位置を表しており, デフォルトはcvPoint(-1,-1)でありカーネル中心を指すが, 今回は,cvPoint(0,0)を指定することで,カーネルの左端を指している.
// (3)処理画像の表示
処理された画像を実際に表示し,何かキーが押されるまで待つ.
実行結果例
■ 境界線
画像の境界をどのように扱うかは,多くの画像処理においてしばしば問題になる. 例えば,フィルタ処理において,フィルタが部分的に画像の外にはみだしてし まった場合,どのような画素値を仮定して利用するかによって,処理結果が異なる. OpenCVでは,画像をコピーし、その周りに境界線をつける処理を行う関数が用 意されており,IPL_BORDER_CONSTANT,およびIPL_BORDER_REPLICATをサポートしている. OpenCVで利用される関数は,IPL_BORDER_REPLICAT,つまり複製境界モードを 利用することが多々あるが,ユーザがそのような処理を望んでいない場合には, あらかじめ境界を定数値で埋めてから処理を行い,処理後の画像をクリッピングする事で 境界の扱いをコントロールできる.
サンプル
境界線の作成 cvCopyMakeBorder
画像のコピーと境界の作成
サンプルコード
#include <cxcore.h>
#include <cv.h>
#include <highgui.h>
int
main (int argc, char **argv)
{
int i, offset = 30;
IplImage *src_img = 0, *dst_img[2];
// (1)画像の読み込み
if (argc >= 2)
src_img = cvLoadImage (argv[1], CV_LOAD_IMAGE_ANYDEPTH | CV_LOAD_IMAGE_ANYCOLOR);
if (src_img == 0)
exit (-1);
for (i = 0; i < 2; i++)
dst_img[i] =
cvCreateImage (cvSize (src_img->width + offset * 2, src_img->height + offset * 2), src_img->depth,
src_img->nChannels);
// (2)境界線の作成
cvCopyMakeBorder (src_img, dst_img[0], cvPoint (offset, offset), IPL_BORDER_REPLICATE);
cvCopyMakeBorder (src_img, dst_img[1], cvPoint (offset, offset), IPL_BORDER_CONSTANT, CV_RGB (255, 0, 0));
// (3)画像の表示
cvNamedWindow ("Border_replicate", CV_WINDOW_AUTOSIZE);
cvShowImage ("Border_replicate", dst_img[0]);
cvNamedWindow ("Border_constant", CV_WINDOW_AUTOSIZE);
cvShowImage ("Border_constant", dst_img[1]);
cvWaitKey (0);
cvDestroyWindow ("Border_replicate");
cvDestroyWindow ("Border_constant");
cvReleaseImage (&src_img);
for (i = 0; i < 2; i++) {
cvReleaseImage (&dst_img[i]);
}
return 0;
}
// (1)画像の読み込み
コマンド引数で指定されたファイル名の画像(入力画像)をオープンし,関数 cvLoadImage()で読み込む.2番目の引数にCV_LOAD_IMAGE_ANYDEPTH | CV_LOAD_IMAGE_ANYCOLORを指定することで,元画像のデプス,チャンネルを変 更せずに読み込む. また,境界を作成するために,入力画像よりも大きな出力領域を確保する.
// (2)境界線の作成
関数cvCopyMakeBorder()によって,入力画像を出力画像にコピーし,さらに境界を作成する. 3番目の引数は,出力画像上にコピーされる入力画像の左上座標を表しており, 画像がコピーされない部分(今回は,上下左右の端から offsetピクセル分)が境界領域となる. 4番目の引数は,境界の種類を表しており,IPL_BORDER_REPLICATEが指定されると, 画像の上/下の端と左/右の端(画像領域の一番外側の値)を用いて境界線を生成する. また,4番目の引数に,IPL_BORDER_CONSTANT が指定されると, 境界はこの関数の最後(5番目)のパラメータとして渡された定数(今回は赤色)で埋められる.
// (3)画像の表示
処理された画像を実際に表示し,何かキーが押されるまで待つ.
実行結果例
■ 閾値処理
画像にある閾値を設けて,画素値がその値よりも大きいか小さいかによって処理を変えたいという事はよくある. 例えば,単純な背景差分では,現画像と背景画像との画素値の差の絶対値がある閾値以下になるか否かで,背景か前景かを判断する. OpenCVでは,このような閾値処理を行うための関数,cvThreshold,および cvAdaptiveThreshold を用意してある.
サンプル
画像の二値化 cvThreshold, cvAdaptiveThreshold
cvThreshold, cvAdaptiveThresholdを利用して,画像の二値化を行う
サンプルコード
#include <cv.h>
#include <highgui.h>
int
main (int argc, char **argv)
{
IplImage *src_img = 0, *dst_img;
IplImage *src_img_gray = 0;
IplImage *tmp_img1, *tmp_img2, *tmp_img3;
// (1)画像を読み込む
if (argc >= 2)
src_img = cvLoadImage (argv[1], CV_LOAD_IMAGE_COLOR);
if (src_img == 0)
return -1;
tmp_img1 = cvCreateImage (cvGetSize (src_img), IPL_DEPTH_8U, 1);
tmp_img2 = cvCreateImage (cvGetSize (src_img), IPL_DEPTH_8U, 1);
tmp_img3 = cvCreateImage (cvGetSize (src_img), IPL_DEPTH_8U, 1);
src_img_gray = cvCreateImage (cvGetSize (src_img), IPL_DEPTH_8U, 1);
cvCvtColor (src_img, src_img_gray, CV_BGR2GRAY);
dst_img = cvCloneImage (src_img);
// (2)ガウシアンフィルタで平滑化を行う
cvSmooth (src_img_gray, src_img_gray, CV_GAUSSIAN, 5);
// (3)二値化:cvThreshold
cvThreshold (src_img_gray, tmp_img1, 90, 255, CV_THRESH_BINARY);
// (4)二値化:cvAdaptiveThreshold
cvAdaptiveThreshold (src_img_gray, tmp_img2, 255, CV_ADAPTIVE_THRESH_MEAN_C, CV_THRESH_BINARY, 11, 10);
// (5)二つの二値化画像の論理積
cvAnd (tmp_img1, tmp_img2, tmp_img3);
cvCvtColor (tmp_img3, dst_img, CV_GRAY2BGR);
// (6)元画像と二値画像の論理積
cvSmooth (src_img, src_img, CV_GAUSSIAN, 11);
cvAnd (dst_img, src_img, dst_img);
// (7)画像を表示する
cvNamedWindow ("Threshold", CV_WINDOW_AUTOSIZE);
cvShowImage ("Threshold", tmp_img1);
cvNamedWindow ("AdaptiveThreshold", CV_WINDOW_AUTOSIZE);
cvShowImage ("AdaptiveThreshold", tmp_img2);
cvNamedWindow ("Image", CV_WINDOW_AUTOSIZE);
cvShowImage ("Image", dst_img);
cvWaitKey (0);
cvDestroyWindow ("Threshold");
cvDestroyWindow ("AdaptiveThreshold");
cvDestroyWindow ("Image");
cvReleaseImage (&src_img);
cvReleaseImage (&dst_img);
cvReleaseImage (&src_img_gray);
cvReleaseImage (&tmp_img1);
cvReleaseImage (&tmp_img2);
cvReleaseImage (&tmp_img3);
return 0;
}
// (1)画像を読み込む
コマンド引数で指定されたファイル名の画像(入力画像)をオープンし,関数cvLoadImage()で読み込む. また,二値化を行うために入力画像をグレースケールに変換する.
// (2)ガウシアンフィルタで平滑化を行う
二値化を行う前に,ガウシアンフィルタで平滑化を行う.平滑化を行うことによって, 画像のノイズを減少させ,安定した(ノイズの少ない)二値化画像を得ることができる.
// (3)二値化:cvThreshold
関数cvThreshold()によって,画像の二値化処理を行う. この関数の最後の引数が,閾値処理の種類を表している. CV_THREASH_BINARYが指定されている場合は,
src(x,y)>thresholdの場合は、dst(x,y) = max_value
それ以外は、 0
となるように処理を行う.ここで,threshold(閾値)は3番目の引数で, max_value(最大値)は4番目の引数で,それぞれ指定されている. その他の手法を指定した場合の処理については,リファレンス マニュアルを参照のこと.
// (4)二値化:cvAdaptiveThreshold
関数cvAdaptiveThreshold()によって,画像の二値化処理を行う. 関数cvThreshold()では,すべてのピクセルにたいして同じ閾値をもって処理を行ったが, 関数cvAdaptiveThreshold()の場合は,ピクセル毎に使用する閾値が異なる. この関数の5番目の引数が,関数cvThresholdと同様に閾値処理の種類を表している. CV_THRESH_BINARYが指定されている場合は,
src(x,y)>T(x,y)の場合は、dst(x,y) = max_value
それ以外は、 0
となるように処理を行う.ここで,max_value(最大値)は3番目の引数で指定されている. また,T(x,y)は,各ピクセル毎に計算された閾値であり,4番目の引数で,そ の閾値の算出方法が指定される. CV_ADAPTIVE_THRESH_MEAN_C が指定された場合は,注目ピクセルの block_size×block_size 隣接領域の平均から,param1 を引いた値となり, block_sizeは6番目の引数で,param1は7番目の引数で,それぞれ指定される.
// (5)二つの二値化画像の論理積
二つの関数によって二値化された画像の論理積を計算する. つまり,二種類の二値化画像を合成し,3チャンネル画像へと変換する.
// (6)元画像と二値画像の論理積
(平滑化した)入力画像と二値化画像の要素毎の論理積を計算する. これにより,入力画像上に二値化画像が合成された画像を得る.
// (7)画像を表示する
それぞれの二値化画像,および入力画像と重ね合わせた画像を実際に表示し, 何かキーが押されるまで待つ.
実行結果例
入力画像 Threshold AdaptiveThreshold 合成画像
画像の二値化(大津の手法) cvThreshold
大津の手法を利用して閾値を決定し,画像の二値化を行う
サンプルコード
#include <cv.h>
#include <highgui.h>
int
main (int argc, char **argv)
{
IplImage *src_img = 0, *dst_img;
if (argc >= 2)
src_img = cvLoadImage (argv[1], CV_LOAD_IMAGE_GRAYSCALE);
if (src_img == 0)
return -1;
dst_img = cvCreateImage (cvGetSize (src_img), IPL_DEPTH_8U, 1);
cvSmooth (src_img, src_img, CV_GAUSSIAN, 5);
// (1)二値化(大津の手法を利用)
cvThreshold (src_img, dst_img, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);
cvNamedWindow ("Threshold", CV_WINDOW_AUTOSIZE);
cvShowImage ("Threshold", dst_img);
cvWaitKey (0);
cvDestroyWindow ("Threshold");
cvReleaseImage (&src_img);
cvReleaseImage (&dst_img);
return 0;
}
// (1)二値化(大津の手法を利用)
前述の二値化と異なるのは,最後の引数(threshold type)に,CV_THRESHOLD_OTSU が加わっている点である. OpencvCV-1.0.0時点でのリファレンス マニュアルには記述がない(CVS版にはある)が, 関数cvThreshold()は,大津の手法と呼ばれる閾値決定手法を実装している. ここで利用される大津の手法とは,ある値の集合を二つクラスに分類する場合の,適切な閾値を決定する手法である. 二つのクラス内の分散とクラス間の分散を考え,これらの比が最小に(つまり,クラス内分散はできるだけ小さく,クラス間分散はできるだけ大きく) なるような閾値を求める. よって,3番目の引数(threshold)に与える値は利用されないので,適当な値を指定して良い.
詳しくは,以下の文献を参考にすること.
大津, "判別および最小2乗基準に基づく自動しきい値選定法", 電子通信学会論文誌, Vol.J63-D, No.4, pp.349-356, 1980.
N. Otsu, "A threshold selection method from gray level histograms",IEEE Trans. Systems, Man and Cybernetics, 1979, Vol.9, pp.62-66
/////http://dev.classmethod.jp/smartphone/opencv-manga-2/
前回のおさらい
前回は漫画カメラで撮影した写真から、以下のように画像処理の工程を計画しました。
輪郭
エッジ検出する
エッジ検出した画像の色を反転する
白い部分を透過する
白黒の部分
画像を三値化する
灰色の部分を透過する
スクリーントーンの部分
スクリーントーン画像を輪郭画像と白黒部分の画像の最背面に配置する
今回は実際に手を動かして、写真を漫画風に加工するサンプルアプリを作成してみましょう。尚、今回は以下のような開発環境で実装を行います。
Mac OS X 10.8.2
Xcode 4.5.1
iOS SDK 6.0
OpenCVを使えるようにする
いきなりですが、今回の画像処理を行うにあたってOpenCVを使用します。OpenCVは画像処理に関してとても実績のあるライブラリです。OpenCVはインテルが開発したオープンソースのC/C++、Java、Python用ライブラリで、iOS以外にもMac OS Xはもちろん、Linux、Windows、Androidなど様々なプラットフォームで動作します。iOSに関してはむしろ最近標準でサポートされたばかりです。
今回OpenCVを使う理由はエッジ検出を使用するからです。このエッジ検出は他のアルゴリズムと比べると比較的簡単なので、自前で実装してもそんなに苦ではないのですが、そうは言ってもそれなりの行数を記述する必要があります。OpenCVではエッジ検出処理を行う関数が用意されています。また、前回少しだけ紹介したOS X/iOS標準のフレームワークであるCoreImage.frameworkでもエッジ検出がありますが、iOSアプリでは動作しないようになっています(対応していればCoreImage.frameworkを使用したいところですが)。というわけで、今回はOpenCVにあやかっちゃいましょう。どうしても自前で実装したいという方は画像のグレースケール/ネガティブ/エッジ検出処理で、C言語でのエッジ検出処理の実装方法をわかりやすく解説しているのでそちらを参照してください。
OpenCVのダウンロード
SourceForgeからOpenCVのフレームワークをダウンロードします。ダウンロードするバージョンは2.4.3です。OpenCV - Browse /opencv-ios/2.4.3 at SourceForge.netを開き、opencv2.framework.zipをクリックします。ダウンロードしたら、ZIPファイルを解凍しておいてください。
opencv2.frameworkのダウンロード
Xcodeプロジェクトの作成
Xcodeプロジェクトを作成します。テンプレートはSingle View Applicationを選択します。プロジェクトの情報に以下のように入力して任意の場所に保存してください。尚、今回はサンプルですので、保存するときにSource ControlのCreate local git repository for this projectのチェックは外しておいてください。
項目 設定値
Product Name MangaFilterTest
Organization Name 任意
Company Identifier 任意
Class Prefix なし
Devices iPhone
Use Storyboards チェックする
Use Automatic Reference Counting チェックする
Include Unit Tests チェックしない
opencv2.frameworkの導入
作成したXcodeプロジェクトにopencv2.frameworkを追加
Xcodeプロジェクトを作成したら、ファイルツリービューのFrameworksを右クリックしてAdd Files to "MangaFilterTest"...をクリックします。ファイル選択画面が表示されるので、先ほどダウンロード・解凍して先ほどダウンロード・解凍したopencv2.frameworkを選択し、Copy items into destination group's folder (if needed)にチェック、Create groups for any added foldersを選択し、Addボタンをクリックしてプロジェクトに追加します。
opencv2.frameworkのインポート
これで、XcodeプロジェクトでOpenCVを使用する準備が整いました。
注意:OpenCV 2.4.2を使用する場合
OpenCV 2.4.2はiOS6(armv7s)に対応していないため、使用する際は注意が必要です。 どうしてもOpenCV 2.4.2を使用したい場合は、以下のサイトで導入方法をわかりやすく解説していますので、ぜひ参考にしてください。 iOSでOpenCVを使えるようにしてみる - FuturesVision
必要なファイルを作成しよう
早速実装作業に入っていきましょう。漫画風のフィルタ処理を実現するため、以下のような工程で画像処理を行います。
今回の目標
上記のように、今回は元画像から輪郭画像と白黒部分の画像をそれぞれ生成し最後に合成するようにします。これらの処理を実現するために、以下のようなクラスを作成します。
クラス 親クラス ファイル 説明
FilterBase NSObject FilterBase.h
FilterBase.mm フィルタクラスの抽象クラスです。CGImageとOpenCV画像データ間の変換処理を実装します。
LineFilter FilterBase LineFilter.h
LineFilter.mm 輪郭を黒いペンでなぞったような画像を生成します。
MonochromeFilter FilterBase MonochromeFilter.h
MonochromeFilter.mm 白黒部分の画像を生成します。
上記ファイルを作成しましょう。ファイルを作成したら、実装ファイルの拡張子を.mから.mmに変更します。実装ファイルの拡張子を.mmにすると、ファイル中にObjective-CとC++のソースコードを混在させることができます。
FilterBaseクラス
FilterBaseクラスはフィルタクラスの抽象クラスです。CGImageからOpenCV画像データに変換したり、OpenCV画像データからCGImageに変換したりするメソッドを定義しておきます。また、このクラスを継承したサブクラスにてフィルタ処理をかけるときに使用するメソッド- doFilter:も定義しておきます。
FilterBase.h
#import <Foundation/Foundation.h>
#import <opencv2/opencv.hpp>
@interface FilterBase : NSObject
/*!
@method
@abstract フィルタ処理を実行するメソッド
@discussion 引数imageにフィルタ処理したCGImageを返す。
@param image CGImageRef フィルタ処理をかけるCGImage
@return CGImageRef フィルタ処理したCGImage
*/
- (CGImageRef)doFilter:(CGImageRef)image;
/*!
@method
@abstract CGImageをOpenCV画像データに変換するメソッド
@param image CGImageRef CGImage
@return IplImage OpenCV画像データ
*/
- (IplImage *)newIplImageFromCGImage:(CGImageRef)image;
/*!
@method
@abstract OpenCV画像データをCGImageに変換するメソッド
@param image IplImage OpenCV画像データ
@return CGImageRef CGImage
*/
- (CGImageRef)newCGImageFromIplImage:(IplImage *)image;
@end
FilterBase.mm
#import "FilterBase.h"
@implementation FilterBase
- (CGImageRef)doFilter:(CGImageRef)image
{
CGImageRetain(image);
// 必要なら呼び出し側でCGImageReleaseによりメモリを解放すること
return image;
}
- (IplImage *)newIplImageFromCGImage:(CGImageRef)image
{
CGContextRef context;
CGColorSpaceRef colorSpace;
IplImage *iplImageTemp, *iplImage;
// RGB色空間を作成
colorSpace = CGColorSpaceCreateDeviceRGB();
// 一時的なIplImageを作成
iplImageTemp = cvCreateImage(cvSize(CGImageGetWidth(image), CGImageGetHeight(image)), IPL_DEPTH_8U, 4);
// CGBitmapContextをIplImageのビットマップデータのポインタから作成
context = CGBitmapContextCreate(iplImageTemp->imageData,
iplImageTemp->width,
iplImageTemp->height,
iplImageTemp->depth,
iplImageTemp->widthStep,
colorSpace,
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrderDefault);
// CGImageをCGBitmapContextに描画
CGContextDrawImage(context, CGRectMake(0.0f, 0.0f, CGImageGetWidth(image), CGImageGetHeight(image)), image);
// ビットマップコンテキストと色空間を解放
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
// 最終的なIplImageを作成
iplImage = cvCreateImage(cvGetSize(iplImageTemp), IPL_DEPTH_8U, 3);
cvCvtColor(iplImageTemp, iplImage, CV_RGBA2RGB);
// 一時的なIplImageを解放
cvReleaseImage(&iplImageTemp);
// 必要なら呼び出し側でcvReleaseImageよりメモリを解放すること
return iplImage;
}
- (CGImageRef)newCGImageFromIplImage:(IplImage *)image
{
CGColorSpaceRef colorSpace;
NSData *data;
CGDataProviderRef provider;
CGImageRef cgImage;
// RGB色空間
colorSpace = CGColorSpaceCreateDeviceRGB();
// IplImageのビットマップデータのポインタアドレスからNSDataを作成
data = [NSData dataWithBytes:image->imageData length:image->imageSize];
provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
// CGImageを作成
cgImage = CGImageCreate(image->width,
image->height,
image->depth,
image->depth * image->nChannels,
image->widthStep,
colorSpace,
kCGImageAlphaNone | kCGBitmapByteOrderDefault,
provider,
NULL,
false,
kCGRenderingIntentDefault);
CGColorSpaceRelease(colorSpace);
CGDataProviderRelease(provider);
// 必要なら呼び出し側でCGImageReleaseによりメモリを解放すること
return cgImage;
}
@end
LineFilterクラス
元画像の輪郭を抽出し、黒いペンでなぞったような画像を生成します。この画像を生成するには前回紹介したエッジ検出を使用します。まずは実際のソースコードを見てみましょう。
LineFilter.h
#import "FilterBase.h"
@interface LineFilter : FilterBase
@end
LineFilter.mm
#import "LineFilter.h"
@implementation LineFilter
- (CGImageRef)doFilter:(CGImageRef)image
{
// 1.CGImageからIplImageを作成
IplImage *srcImage = [self newIplImageFromCGImage:image];
IplImage *grayscaleImage = cvCreateImage(cvGetSize(srcImage), IPL_DEPTH_8U, 1);
IplImage *edgeImage = cvCreateImage(cvGetSize(srcImage), IPL_DEPTH_8U, 1);
IplImage *dstImage = cvCreateImage(cvGetSize(srcImage), IPL_DEPTH_8U, 3);
// 2.グレースケール画像に変換
cvCvtColor(srcImage, grayscaleImage, CV_BGR2GRAY);
// 3.グレースケール画像を平滑化
cvSmooth(grayscaleImage, grayscaleImage, CV_GAUSSIAN, 3, 0, 0);
// 4.エッジ検出画像を作成
cvCanny(grayscaleImage, edgeImage, 20, 120);
// 5.エッジ検出画像色を反転
cvNot(edgeImage, edgeImage);
// 6.CGImage用にBGRに変換
cvCvtColor(edgeImage, dstImage, CV_GRAY2BGR);
// 7.IplImageからCGImageを作成
CGImageRef effectedImage = [self newCGImageFromIplImage:dstImage];
cvReleaseImage(&srcImage);
cvReleaseImage(&grayscaleImage);
cvReleaseImage(&edgeImage);
cvReleaseImage(&dstImage);
// 8.白色の部分を透過する
const float colorMasking[6] = {255, 255, 255, 255, 255, 255};
effectedImage = CGImageCreateWithMaskingColors(effectedImage, colorMasking);
return effectedImage;
}
@end
1.CGImageからIplImageを作成
FilterBaseで定義した- newIplImageFromCGImage:メソッドを使用してCGImageをIplImage(OpenCVで扱う画像データ)に変換します。
2.グレースケール画像に変換
エッジ検出をする場合、カラー画像ではノイズが多くうまく検出できない場合があるため、グレースケール画像や二値化した画像を使用することが多いです。今回はグレースケール画像を使用します。元画像からグレースケール画像を生成するには、void cvCvtColor(const CvArr* src, CvArr* dst, int code)関数を使用します。この関数は主に画像の色空間を変換するために使用します。第1引数srcに対して第3引数codeで指定した色空間に変換したものを第2引数dstに生成します。この段階での画像は以下のようになります。
グレースケールを適用した画像
3.グレースケール画像を平滑化
エッジ検出を行う前にガウシアンフィルタで平滑化を行います。平滑化を行うことによって、画像のノイズを減少させ、安定したエッジ検出を行うことができます。 void cvSmooth(const CvArr* src, CvArr* dst, int smoothtype=CV_GAUSSIAN, int param1=3, int param2=0, double param3=0, double param4=0)関数を使用します。この段階での画像は以下のようになります。
グレースケールに平滑化を適用した画像
4.エッジ検出画像を生成
生成したグレースケール画像を使用してエッジ検出処理を施します。エッジ検出処理はvoid cvCanny(const CvArr* image, CvArr* edges, double threshold1, double threshold2, int aperture_size=3)関数を使用します。cvCanny関数ではCanny法というアルゴリズムを使用してエッジ検出を行います。第3引数threshold1と第4引数threshold2は、小さいほうがエッジ同士を接続するために用いられ,大きいほうが強いエッジの初期検出に用いられます。threshold1、threshold2を変更することで、検出の度合を変更できます。今回はそれぞれ20、120を指定していますが、生成したい画像にあわせて設定しましょう。 この段階での画像は以下のようになります。
エッジ検出を適用した画像
5.エッジ検出画像色を反転
エッジ検出しただけでは今回生成したい画像とは白黒逆になっているので、色を反転させましょう。色を反転するには、void cvNot(const CvArr* src, CvArr* dst)関数を使用します。この段階での画像は以下のようになります。
エッジ検出画像の色を反転した画像
6.CGImage用にBGRに変換
ここまでで生成した画像をCGImage用にBGRに変換します。これを忘れてしまうと大変なことになってしまいます。この処理は先ほど紹介したcvCvtColor関数を使用します。
7.IplImageからCGImageを作成
FilterBaseで定義した- newCGImageFromIplImage:メソッドを使用してIplImageをCGImageに変換します。
.白色の部分を透過する
今回必要なのは、あくまで輪郭の黒い線なので、生成した画像の白い部分を透過します。指定した色を透過するにはCGImageRef CGImageCreateWithMaskingColors(CGImageRef image, const CGFloat components[])関数を使用します。この関数は第1引数imageで指定した画像に対して第2引数componentsで指定した範囲内の色でマスキングします。この段階での画像は以下のようになります。
輪郭画像完成
これで輪郭画像の完成です。
MonochromeFilterクラス
白黒でべた塗りしたような画像を生成します。しかし今回作成したい画像は、白黒だけではなく灰色の部分にスクリーントーンを表示しなければなりません。これは前回紹介した2値化のアルゴリズムを少し改良して、白・黒の2色ではなく白・黒・灰色の3値化を実現しましょう。
MonochromeFilter.h
#import "FilterBase.h"
@interface MonochromeFilter : FilterBase
@end
MonochromeFilter.mm
#import "MonochromeFilter.h"
@implementation MonochromeFilter
- (CGImageRef)doFilter:(CGImageRef)image
{
// 1.CGImageからIplImageを作成
IplImage *srcImage = [self newIplImageFromCGImage:image];
IplImage *grayScaleImage = cvCreateImage(cvGetSize(srcImage), IPL_DEPTH_8U, 1);
IplImage *dstImage = cvCreateImage(cvGetSize(srcImage), IPL_DEPTH_8U, 3);
// 2.グレースケール画像に変換
cvCvtColor(srcImage, grayScaleImage, CV_BGR2GRAY);
// 3.グレースケール画像を1画素ずつ走査して3値化する
for(int y = 0; y < grayScaleImage->height; y++) {
for(int x = 0; x < grayScaleImage->width; x++) {
int a = grayScaleImage->widthStep * y + x;
uchar p = grayScaleImage->imageData[a];
if (p < 70) {
// 70より小さい場合、黒
grayScaleImage->imageData[a] = 0;
} else if (70 <= p && p < 120) {
// 70以上、120未満の場合、灰色
grayScaleImage->imageData[a] = 100;
} else {
// 120以上の場合、白
grayScaleImage->imageData[a] = 255;
}
}
}
// 4.CGImage用にBGRに変換
cvCvtColor(grayScaleImage, dstImage, CV_GRAY2BGR);
// 5.IplImageからCGImageを作成
CGImageRef effectedImage = [self newCGImageFromIplImage:dstImage];
cvReleaseImage(&srcImage);
cvReleaseImage(&grayScaleImage);
cvReleaseImage(&dstImage);
// 6.灰色の部分を透過する
const float colorMasking[6] = {100, 100, 100, 100, 100, 100};
effectedImage = CGImageCreateWithMaskingColors(effectedImage, colorMasking);
return effectedImage;
}
@end
1.CGImageからIplImageを作成
LineFilterと同じです。FilterBaseで定義した- newIplImageFromCGImage:メソッドを使用してCGImageをIplImage(OpenCVで扱う画像データ)に変換します。
2.グレースケール画像に変換
エッジ検出と同様、画像に2値化処理を施す場合、グレースケール画像を使用することが多いです。なのでここでもグレースケール画像を使用します。この段階での画像は以下のようになります。
グレースケールを適用した画像
3.グレースケール画像を1画素ずつ走査して3値化する
グレースケール画像を1画素ずつ走査して3値化処理を施します。画像サイズはIplImage構造体に定義されているメンバ変数widthとheightから取得できます。画像データのバイト数はメンバ変数widthStepで取得できます。これらの値を使用して画像データへのポインタであるimageDataより1画素ずつ値を変更します。
通常、画像に対して2値化を行う場合、任画素の構成要素RGBの平均値を算出し任意のしきい値より大きければ白、小さければ黒にします。今回は3値化なのでしきい値を2つ用意します。これにより、白・黒・灰色の3色だけで構成された画像を生成することができます。この段階での画像は以下のようになります。尚、灰色はとりあえずR:100、G:100、B:100(#646464)としておきます。
3値化した画像
4.CGImage用にBGRに変換
ここまでで生成した画像をCGImage用にBGRに変換します。これを忘れてしまうと大変なことになってしまいます。この処理は先ほど紹介したcvCvtColor関数を使用します。
5.IplImageからCGImageを作成
FilterBaseで定義した- newCGImageFromIplImage:メソッドを使用してIplImageをCGImageに変換します。
6.白色の部分を透過する
今回は灰色の部分にスクリーントーンを表示するので、生成した画像の灰色の部分を透過します。指定した色を透過するにはCGImageRef CGImageCreateWithMaskingColors(CGImageRef image, const CGFloat components[])関数を使用します。この段階での画像は以下のようになります。
白黒部分の画像完成
スクリーントーン
スクリーントーンの部分は、UIImageViewの背景に以下の画像をタイル状に並べましょう。(スクリーントーン画像のダウンロード)この画像をscreentone.pngとして保存し、Xcodeプロジェクトに追加してください。尚、この画像はStripe Generator - ajax diagonal stripes background designerで生成しました。
スクリーントーンサンプル
合成してみよう
さぁこれでようやく準備が整いました。以下のサンプル画像をsample.jpgとして保存し、Xcodeプロジェクトに追加してください。
サンプル画像
保存できたら、Xcodeプロジェクトに予め作成されているViewController.mをViewController.mmに変更して、以下のように記述しましょう。
ViewController.mm
#import "ViewController.h"
// 作成したフィルタを読み込む
#import "LineFilter.h"
#import "MonochromeFilter.h"
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// 1.漫画風に加工する画像を読み込む
UIImage *image = [UIImage imageNamed:@"sample.jpg"];
// 2.白黒部分の画像を生成する
MonochromeFilter *monochromeFilter = [[MonochromeFilter alloc] init];
CGImageRef monocrhomeCGImage = [monochromeFilter doFilter:image.CGImage];
UIImage *monocrhomeImage = [UIImage imageWithCGImage:monocrhomeCGImage];
CGImageRelease(monocrhomeCGImage);
// 3.輪郭画像を生成する
LineFilter *lineFilter = [[LineFilter alloc] init];
CGImageRef lineCGImage = [lineFilter doFilter:image.CGImage];
UIImage *lineImage = [UIImage imageWithCGImage:lineCGImage];
CGImageRelease(lineCGImage);
UIImage *margedImage;
CGRect imageRect = CGRectMake(0, 0, image.size.width, image.size.height);
// 4.画像を合成する
// 4-1.オフスクリーン描画のためのグラフィックスコンテキストを用意
UIGraphicsBeginImageContext(imageRect.size);
// 4-2.白黒部分の画像をコンテキストに描画
[monocrhomeImage drawInRect:imageRect];
// 4-3.輪郭画像をコンテキストに描画
[lineImage drawInRect:imageRect];
// 4-4.合成画像をコンテキストから取得
margedImage = UIGraphicsGetImageFromCurrentImageContext();
// 4-5.オフスクリーン描画を終了
UIGraphicsEndImageContext();
// 表示用のUIImageViewを生成
UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
imageView.contentMode = UIViewContentModeScaleAspectFit;
// 合成画像を設定
imageView.image = margedImage;
// 5.スクリーントーン画像をUIImageViewの背景パータンとして設定
imageView.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"screentone.png"]];
// UIImageViewを配置
[self.view addSubview:imageView];
}
@end
1.漫画風に加工する画像を読み込む
加工対象の画像のCGImageを取得するために、UIImageインスタンスを生成します。
2.白黒部分の画像を生成する
先ほど作成したMonochromeFilterクラスを使用して、白黒部分の画像を生成します。
3.輪郭画像を生成する
先ほど作成したLineFilterクラスを使用して、白黒部分の画像を生成します。
4.画像を合成する
生成した画像を合成します。画像を合成するには、UIGraphicsBeginImageContext(CGSize size)関数でオフスクリーン描画のためのグラフィックスコンテキストを用意して(4-1)、生成した白黒部分の画像と輪郭画像を描画し(4-2、4-3)、できた合成画像をコンテキストから取得します(4-4)。最後にオフスクリーン描画を終了します(4-5)。
5.スクリーントーン画像をUIImageViewの背景パータンとして設定
最後にスクリーントーン画像をUIImageViewの背景パータンとして設定します。
完成!!
動作を確認してみましょう。
動作確認
まとめ
以上、2回に渡り写真を漫画風に加工するための画像処理をお送りしました。この記事ではOpenCVを使用していますが、自前で実装したりCoreImage.frameworkを利用したりするのもありでしょう。この記事をきっかけに面白いorかっちょいいカメラアプリを作って頂ければ幸いです。
/////
/////
Android用開発メモ OpenCV for Android入門 ? カメラ編
/////
/////
/////
Android用開発メモ OpenCV for Android入門 ? カメラ編
/////
/////
Android用開発メモ OpenCV for Android入門 ? カメラ編
ここ最近はAndroidアプリ開発の勉強をしています。今回はOpenCVでデバイスのカメラを利用した動画像処理を試してみました。
技術Wikiの方にもAndroid関連のメモを残しています。
* Android - Tech Note
http://rest-term.com/technote/index.php/Android
* OpenCV for Android - Tech Note
http://rest-term.com/technote/index.php/OpenCV%20for%20Android
Portions of this page are modifications based on work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License.
* OpenCV4Android SDK 2.4.4 (今回はJava APIのみ使用)
* 検証デバイス: Galaxy S2 LTE(Android 2.3.6), AQUOS PHONE ZETA SH-02E(Android 4.0.4)
* 開発機: MacBook Air Mid 2012
基本的にAndroid 2.2(API Level 8)でも動作するように確認しながら進めていきたいと思います。
環境構築とサンプルアプリのビルドまでの手順は、公式サイトにスクリーンショット付きで丁寧に説明してくれています。Windows 7用のガイドですが、ほぼEclipse上での手順説明なのでMacでもパスを読み替える程度で簡単にできました。僕の場合はSDKを ~/Library/OpenCV2.4.4 以下にインストールしておきました。
* OpenCV4Android SDK ? OpenCV 2.4.4 documentation
http://docs.opencv.org/doc/tutorials/introduction/android_binary_package/O4A_SDK.html
OpenCV Managerについて
OpenCV4Android SDK 2.4.2からライブラリ自体はOpenCV Managerというアプリで管理する構成に変更されました。これまでのようにアプリのバイナリにライブラリをコピーするのは無駄だからというのが主な理由とのことです。デバイスのアーキテクチャをOpenCV Managerが判別して適切なライブラリをインストールしてくれます。
using OpenCV Manager
using OpenCV Manager, feel the difference
確かにサイズは大きく減っていますね。ただ、アプリのユーザーにOpenCVを意識させることになるので(OpenCV Managerを別途インストールしてもらう必要がある)その辺りは微妙な気はしますけど。実際にAndroid端末にOpenCV ManagerをGoogle Playからインストール、実行してみました。ライブラリのインストールが成功すると以下のような画像が見られます。
opencv_manager_aquosphone
カメラアクセスの許可
OpenCVの使い方の前に、けっこう忘れがちなカメラアクセスの許可設定を行っておきます。AndroidManifest.xml ファイルを以下のように編集します。
XHTML
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
以下、OpenCVでデバイスのカメラを利用した動画像処理を行う際の基本フローを整理していきます。
1. レイアウトの定義
OpenCVではカメラを扱う2つのクラス JavaCameraView と NativeCameraView が提供されています(両者の違いは後述)。これのどちらかをビューとしてXMLレイアウトファイル(res/layout/*.xml)に配置します。
XHTML
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:opencv="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true"
tools:context=".MainActivity" >
<org.opencv.android.JavaCameraView
android:id="@+id/camera_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:visibility="visible"
opencv:camera_id="any"
opencv:show_fps="true" />
</RelativeLayout>
ここでは JavaCameraView を配置しています。利用できる属性は camera_id (any/front/back) と show_fps (true/false) です。camera_id には利用するカメラ(フロント/バックカメラ)を指定するのですが、any を指定しておけば自動的にカメラを判別してくれます。show_fps を true にすると画面左上にFPSと画角が表示されます。デバッグ時は有効にしておくと良いです。
2. OpenCVライブラリの読み込みと初期化
OpenCV Managerを使う場合、Activityの中でOpenCVライブラリの読み込みを行う OpenCVLoader.initAsync メソッドを呼び出す必要があります。このメソッドは非同期で実行されるので、読み込み完了後にUIスレッドで呼ばれるコールバックメソッドも併せて実装しておきます。
Java
public class MainActivity extends Activity implements CvCameraViewListener {
// カメラビューのインスタンス
// CameraBridgeViewBase は JavaCameraView/NativeCameraView のスーパークラス
private CameraBridgeViewBase mCameraView;
// ライブラリ初期化完了後に呼ばれるコールバック (onManagerConnected)
// public abstract class BaseLoaderCallback implements LoaderCallbackInterface
private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
@Override
public void onManagerConnected(int status) {
switch (status) {
// 読み込みが成功したらカメラプレビューを開始
case LoaderCallbackInterface.SUCCESS:
mCameraView.enableView();
break;
default:
super.onManagerConnected(status);
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// カメラビューのインスタンスを変数にバインド
mCameraView = (CameraBridgeViewBase) findViewById(R.id.camera_view);
// リスナーの設定 (後述)
mCameraView.setCvCameraViewListener(this);
}
@Override
protected void onResume() {
super.onResume();
// 非同期でライブラリの読み込み/初期化を行う
// static boolean initAsync(String Version, Context AppContext, LoaderCallbackInterface Callback)
OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_2_4_4, this, mLoaderCallback);
}
JavaCameraViewとNativeCameraViewの違い
JavaCameraView と NativeCameraView は両方とも CameraBridgeViewBase のサブクラスです。
opencv4android_cameraview
JavaCameraView は android.hardware.Camera を利用した実装、NativeCameraView はOpenCVの VideoCapture を利用した実装という違いがあります。安定性という観点ではAndroid SDKの機能を利用した JavaCameraView の方が安定しているかと思われますが、そんなに気にしなくて大丈夫でしょう。
CvCameraViewListenerインタフェースの実装
CvCameraViewListener または CvCameraViewListener2 インタフェースの以下の3つのメソッドを実装します。
* onCameraViewStarted
* onCameraViewStopped
* onCameraFrame
3つめの onCameraFrame メソッドに任意の画像処理を実装することになります。また、CvCameraViewListener と CvCameraViewListener2 の違いは onCameraFrame の引数のみで、onCameraViewStarted および onCameraViewStopped は共通です。
Java
@Override
public void onCameraViewStarted(int width, int height) {
// カメラプレビュー開始時に呼ばれる
}
@Override
public void onCameraViewStopped() {
// カメラプレビュー終了時に呼ばれる
}
// CvCameraViewListener の場合
@Override
public Mat onCameraFrame(Mat inputFrame) {
// フレームをキャプチャする毎(30fpsなら毎秒30回)に呼ばれる
}
// CvCameraViewListener2 の場合
@Override
public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
}
CvCameraViewFrame インタフェースは以下のようになっています。CvCameraViewListener2.onCameraFrame ではこのインタフェースを実装した JavaCameraFrame/NativeCameraFrame のインスタンスが渡されます。
Java
public interface CvCameraViewFrame {
// 4チャンネルRGBAカラーのMatインスタンスを返す
public Mat rgba();
// 1チャンネルグレースケールのMatインスタンスを返す
public Mat gray();
};
キャプチャしたフレーム画像をそのままカメラプレビューに表示させるには、CvCameraViewListener.onCameraFrame の場合は引数の inputFrame をそのまま return し、CvCameraViewListener2.onCameraFrame の場合は inputFrame.rgba() を return します。
OpenCVでカメラを利用した動画像処理を行う際の基本フローは以上になります。ここまでのまとめとして、カメラから取得したフレーム画像を線画風に変換する処理のサンプルコードを載せておきます。
Java
// package, import文は省略
public class MainActivity extends Activity implements CvCameraViewListener2 {
private CameraBridgeViewBase mCameraView;
private Mat mOutputFrame;
private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
@Override
public void onManagerConnected(int status) {
switch (status) {
case LoaderCallbackInterface.SUCCESS:
mCameraView.enableView();
break;
default:
super.onManagerConnected(status);
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mCameraView = (CameraBridgeViewBase)findViewById(R.id.camera_view);
mCameraView.setCvCameraViewListener(this);
}
@Override
public void onPause() {
if (mCameraView != null) {
mCameraView.disableView();
}
super.onPause();
}
@Override
protected void onResume() {
super.onResume();
OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_2_4_4, this, mLoaderCallback);
}
@Override
public void onDestroy() {
super.onDestroy();
if (mCameraView != null) {
mCameraView.disableView();
}
}
@Override
public void onCameraViewStarted(int width, int height) {
// Mat(int rows, int cols, int type)
// rows(行): height, cols(列): width
mOutputFrame = new Mat(height, width, CvType.CV_8UC1);
}
@Override
public void onCameraViewStopped() {
mOutputFrame.release();
}
@Override
public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
// Cannyフィルタをかける
Imgproc.Canny(inputFrame.gray(), mOutputFrame, 80, 100);
// ビット反転
Core.bitwise_not(mOutputFrame, mOutputFrame);
return mOutputFrame;
}
}
opencv4android_linedraw
opencv4android line drawing
Galaxy S2 LTEでだいたい15fpsほどで動作しました。スマートフォンでもこの速度が出せるのには驚きました。
今回はAndroidでOpenCVを使ったアプリ開発を試してみましたが、特に難しいところもなくスムーズに進めることができました。OpenCVがAndroidをサポートした当初はバグだらけで大変だったらしいですが、現在はとても使いやすくなっているので昔挫折してしまった人はリベンジしてみてください。これからも引き続きいろいろ試していきたいです。
* 参考
opencv.org - Home > PLATFORMS > ANDROID
Android | Blog はじめました
////// 写真を漫画風に加工できるアプリ3つを比較してみた (iPhone用)
私は漫画やイラストを描くうえで、背景がものすごく苦手です。
苦手っていうか全く描けない。
こんな本を買いましたが、読んで自分で描いてみてもさっぱり解らない…という頭の弱さが余計浮き彫りになる結果となりました。
iPhoneでは写真を漫画風に加工するアプリが多数あり、コミスタ等のデジタル環境が整うまでは鉛筆で描いた絵をアプリで漫画風に加工し、イラレで四コマに配置してセリフを入れる…という色々と突っ込みどころ満載の描き方をしていました。
Ctrl+zの使えない世界ではもう生きていけない体になっております。
そんな経緯から、写真を漫画風に加工して背景にすればよくね???と思って、三つの使いやすそうなアプリを使用して比較してみました!
もっと素敵なアプリをご存知の方は教えていただけるとうれしいです!!
★今回使用したiPhoneアプリ
今回使用したのは以下の3つです。
選んだ基準は、ひとつだけ。
「強制的に文字が入らないもの」です。
ちなみにiPhone5、iOS7.0.4で使用しております。
線画風カメラ
darjjeelling
写真/ビデオ
無料
MANGAkit-漫画風写真加工アプリ
Seesaa Inc.
写真/ビデオ
無料
漫画コミックカメラ-無料で写真をマンガ化。撮影した写真やカメラロールの画像をフィルターで線画加工、漫画風に画像加工
KeyLife, Inc.
写真/ビデオ
無料
線画風カメラはほんとの線画にしかならないので、カラー+線画で、その後iPhoneのカメラについている編集機能でグレースケール化しております!
↓線画だとこんな感じ
★実際使ってみた比較
その1 旅館の廊下
ロケーションは旅館の廊下です。
線画風カメラは写真にしか見えないですね…
MANGAkit、コミックカメラはともに漫画っぽく加工はできるのですが、MANGAkitはリアルっぽさがどうしてもでてしまい、お色気ムードを作りたくても違和感が出てしまいます。
線画風カメラは文字は入力できず、写真を線画にするだけのシンプルなアプリです。
MANGAkitは一番よく使うアプリですが、写真サイズが正方形のためトリミング必須になってしまいます。ただ背景の加工や文字、アイテム、効果は多数選ぶことができて、どちらかといえばギャグタッチの漫画に加工するのに向いています。
コミックカメラは背景の選択範囲が9種類から選択でき、選択した背景には68種類ものトーンを選び加工することが可能です。
効果的な文字も自分で入力することも雛形や素材から選ぶことが可能で、シリアスな場面や不思議な雰囲気を出すこともできます。
平和な東北の寂れた温泉宿が突如爆発するドラマチックな風景に…!
その2 神社の鳥居
撮影したのは年越しな真冬の真夜中なのですが、コミックカメラを使うと真夏の昼間のようにも見せることができるのは便利だと思いました。
その3 ビルのある街並
線画風カメラ、写真にしかならねえ…!
線を太めにしてデフォルメすると多少絵っぽくはなるのだとは思いますが…
MANGAkitでは青空も木の葉アイテムを散らすことと効果線や文字で暗い雰囲気を出せるし、コミックカメラでは平和なホリデイな風景も爆破。
★まとめ
漫画っぽく簡単に加工できるのはコミックカメラとMANGAkit。
MANGAkitはギャグっぽくなってしまうので汎用性があるのはコミックカメラ。
リアルさを追求するなら線画風カメラで加工したものをPhotoshop等でハーフトーン加工する等もう一手間必要な感じ。
コミックカメラは背景加工の選択範囲が自動のため、思い通りに選択できない事あり写真の撮り方にちょっと工夫が必要かも。
ちなみにMANGAkitを使用して描いた漫画はこちらです
引き寄せの法則四コマ★ろあちゃん - あゆお | ブクログのパブー
★最後に
これらのアプリは趣味で漫画を描く範囲では楽しめると思いますが、コミックカメラは非商用目的に限り使用することができるそうなので、出版社の賞等に応募する際は結局ライセンスを購入してあるPhotoshopやIllustratorでの加工の方になってしまいます。
というわけで今年は背景をちゃんと自分で描けるようになることを目標にしたいと思います…!
rikezyo00sumaho at 07:07|Permalink│Comments(0)