たったの6ステップ!『漫画カメラ』風に写真を加工するiPhoneアプリの作り方
超大ヒットした『漫画カメラ』、ほんとに漫画っぽくなって、動作も軽快、シェアも簡単で楽しいですよね。
ただ、ちらほらと「同じこと考えてた」「そういうの作ってた」という声を聞くことがあります。実際に同様のコンセプトのアプリもたくさん出ています。
実は、カヤックでも、2年ほど前、Instagramが出てきた頃に"Comicgram"という企画が出たことがあって、ちょっとだけモックアプリをつくったことがありました。
そのときつくったモックアプリで自動で写真加工した結果がこちら
『漫画カメラ』ほど漫画っぽくないかもしれませんが、それっぽいといえばそれっぽいです。
実は、このモックアプリはOpenCVにもともと備わっている機能を組み合わせただけなので、結構サックリ実現できます。
以下でその6ステップの処理内容について紹介させていただきます。
ステップ1:領域分割
『画像ピラミッドを用いた画像の領域分割』というのをやります。領域分割とは、色情報をもとに画像内をセグメント化する処理です。次の画像がわかりやすいかと思います。
(OpenCVのページからお借りしました)
実装にあたっては細かいアルゴリズムを知っている必要はなく、OpenCV の cvPyrSegmentation 関数を呼べばOKです。
cvPyrSegmentation (cvsrc, cvdst, storage, &comp, level, threshold1, threshold2);
引数が何やら多いですが、モックアプリでは、level に 4、threshold1 に 255.0、threshold2 に 50.0 を渡しています。
CvMemStorage *storage = 0; CvSeq *comp = 0; storage = cvCreateMemStorage (0); IplImage *cvdst = cvCloneImage (cvsrc); cvdst = cvCloneImage (cvsrc); cvPyrSegmentation (cvsrc, cvdst, storage, &comp, 4, 255.0, 50.0); cvReleaseMemStorage (&storage);
領域分割だけをかけた状態では、こんな感じ。
ちなみに Photoshop でいう『ポスタリゼーション』を使って階調を減らすのと効果が似ていますが、あちらは画素単体の色値だけを見て階調を間引くだけなのに対し、領域分割は「領域」とついている通り、周囲の画素の色値との関係も見ているので、まとまりを伴って階調を減らすことになり、より漫画っぽい効果が得られます(その分処理は重いです)
ステップ2:白黒にする
グレースケール(いわゆる白黒)にします。
OpenCV の cvCvtColor 関数を呼ぶだけ。
cvCvtColor(cvsrc, cvgray, CV_BGR2GRAY);
こんな感じになります。
グレースケール化の前に領域分割を行うのは、情報量が多い方が(色のチャネルが多い方が)領域分割の精度が高い(と当時の僕が考えた)からです。
ステップ3:ハイライトをとばす
ハイライトとは色値が明るい部分、つまり限りなく白に近いグレー部分です。漫画っぽくするために、いっそのこと白く飛ばしてしまいます。
forループで画素ごとに色値を見ていって、しきい値以上であれば白(255)にします。
for(int y=0; y<cvgray->height; y++) { for(int x=0; x<cvgray->width; x++) { uchar *p = (uchar *)cvgray->imageData + y * cvgray->widthStep + x; *p = *p >= threshold ? 255 : *p; } }
モックでは threshold (しきい値)を 200 にしてます。
ステップ4:スクリーントーンをあてる
陰影を漫画っぽく表現するために、スクリーントーン風パターン画像を用意し、
これを色値が特定の範囲におさまる領域に当てはめて行きます。たとえば色値が151〜200の範囲の領域には薄めのトーンを当てはめる、といった感じです。
モックではスクリーントーンのパターン画像と入力画像を同じサイズに揃えたあと、次のようなOpenCVを用いた自作関数に渡して、パターンへの置き換え処理を行っています。
/* min から maxまでをパターン画像で埋める */ - (UIImage *)createPatternedImage:(UIImage *)src pattern:(UIImage *)pattern minThreshold:(int)minThreshold maxThreshold:(int)maxThreshold { // UIImageをIPlImageに変換 IplImage *cvsrc = [self CreateIplImageFromUIImage:src]; IplImage *cvdst = [self CreateIplImageFromUIImage:pattern]; // グレースケール IplImage *cvgray = cvCreateImage(cvGetSize(cvsrc), IPL_DEPTH_8U, 1); cvCvtColor(cvsrc, cvgray, CV_BGR2GRAY); // 2値化してマスクを作成する /* CV_THRESH_BINARY : 閾値を超えるピクセルは maxVal に,それ以外のピクセルは 0 になります. CV_THRESH_BINARY_INV : 閾値を超えるピクセルは 0 に,それ以外のピクセルは maxVal になります. */ // maxThresholdを超える領域は処理対象外 IplImage *cvmask = cvCloneImage (cvgray); cvThreshold (cvgray, cvmask, maxThreshold, 255, CV_THRESH_BINARY); // minThresholdより小さい領域は処理対象外 IplImage *cvmask2 = cvCloneImage (cvgray); cvThreshold (cvgray, cvmask2, minThreshold, 255, CV_THRESH_BINARY); cvNot(cvmask2, cvmask2); // 反転 cvReleaseImage(&cvgray); // 2つのマスクの論理和 cvAdd(cvmask, cvmask2, cvmask, NULL); cvCopy(cvsrc, cvdst, cvmask); cvReleaseImage(&cvsrc); cvReleaseImage(&cvmask); cvReleaseImage(&cvmask2); // 出力UIImageを生成 UIImage *dst = [self UIImageFromIplImage:cvdst]; cvReleaseImage(&cvdst); return dst; }
上記関数では何をやっているかというと、指定した色値の範囲に収まるピクセルだけを残したマスク画像を生成し、そのマスクに従ってスクリーントーンのパターンを合成する、ということをやっています。
ここまでの処理結果はこんな感じです。
モックでは4段階のスクリーントーン画像を使っています。トーン画像を変えればまたかなり画風が変わってくるので、このあたりでフィルタのバリエーションを増やすと楽しそうです。
ステップ5:輪郭線をつける
最後に輪郭線をつけるとグッと漫画っぽくなります。
『適応的閾値処理』というのを用いて、輪郭線を生成します。
cvAdaptiveThreshold(cvgray, cv1bit, 255, CV_ADAPTIVE_THRESH_GAUSSIAN_C, CV_THRESH_BINARY, blockSize, 10);
単なる二値化や、エッジ抽出フィルタでは、いい感じに輪郭線を生成できません。「適応的に」二値化するのがポイントです。適応的に、というのは近傍の画素を見ながら閾値処理を行うという意味ですが、上記のように cvAdaptiveThreshold を用いればOKです。
生成した輪郭を、元画像に合成するとこんな感じになります。
ステップ6:集中線画像を合成
最後に集中線の画像を合成します。
合成には cvAdd 関数を使います。
cvAdd(src1, src2, dst, NULL);
これでグッと漫画っぽくなります。
『漫画カメラ』のようにこれにセリフや擬音を当てはめて、たくさんの種類を用意するとバリエーションが豊かになります。
まとめ
漫画カメラ風写真加工を実現するための6ステップの画像処理方法を紹介しました。
漫画風に加工するためのアルゴリズムはこれ意外にも何通りも考えられると思います。漫画風にするポイントは、階調を減らしてスクリーントーンをあてはめる、輪郭をつけるというところなので、このアルゴリズムのように領域分割といった重い処理を用いない方法も可能かと思います。
OpenCV以外にもハードウェアアクセラレーションの効いた vImage を用いる方法や、 CoreImageを用いる方法 もあるので、AppStoreにリリースするレベルのものをつくる際にはぜひ検討してみてください。