2012年3月24日土曜日

画像を平滑化する その2 - OpenCV for Android


前回に続いて平滑化です。

ボックスフィルタを用いた平滑化を行うにはImgproc.boxFilter()メソッドを利用します。


Imgproc.boxFilter(Mat src, Mat dst, int ddepth,
Size ksize, Point anchor, boolean normalize, int borderType)

Mat src         処理したい元画像のMat
Mat dst         変換後Mat
int ddepth        変換後のビット深度
Size ksize       カーネルサイズ
Point anchor      アンカー点
boolean normalize  正規化の有無
int borderType     ピクセル外挿手法


ddepthについては、変換後の画像のビット深度を指定します。
通常はsrc.type()を指定すれば良いようです。

ksizeについては、この値が大きくなるとぼかしが強くなります。

anchorについては、どちらかに寄せると画像が少しズレます。
new Size(-1, -1)で中心を指定すれば良いようです。

normalizeについては、trueを指定すると正規化されます。
正規化されていないボックスフィルタは、特徴検出などに利用されるようです。

また、Imgproc.blur()というメソッドもありますが、boxFilter()メソッドの第6引数にtrueを指定したものと同じ結果になります。



それでは、実際にやってみましょう。

元画像です。


正規化有です。
Imgproc.boxFilter(mat, dstMat, mat.type(),
  new Size(10, 10), new Point(-1, -1), true);


ksizeを変えてみます。
Imgproc.boxFilter(mat, dstMat, mat.type(),
  new Size(50, 5), new Point(-1, -1), true);


正規化無しです。
Imgproc.boxFilter(mat, dstMat, mat.type(),
  new Size(2, 1), new Point(-1, -1), false);


ksizeを変えてみます。
あまり大きな数字にすると画像が真っ白になります。
Imgproc.boxFilter(mat, dstMat, mat.type(),
  new Size(2, 2), new Point(-1, -1), false);


ちなみにddepthにおかしなtypeを指定すると、処理は可能ですが当然こうなります。
Imgproc.boxFilter(mat, dstMat, CvType.CV_16S,
  new Size(10, 10), new Point(-1, -1), true);


余談でしたね。

画像を平滑化する その1 - OpenCV for Android


おなじみの平滑化です。

はい。すいません。読めすらしません。
「へいかつか」と読みますが、いわゆるぼかし処理です。

いくつか種類があるようで、利用するフィルタ毎にメソッドが用意されています。

ガウシアンフィルタを用いた平滑化を行うにはImgproc.GaussianBlur()メソッドを利用します。

Imgproc.GaussianBlur(Mat src, Mat dst, Size ksize,
double sigma1, double sigma2, int borderType)

Mat src      処理したい元画像のMat
Mat dst      変換後Mat
Size ksize    ガウシアンカーネルサイズ
double sigma1  ガウシアンカーネルのx方向の標準偏差
double sigma2  ガウシアンカーネルのy方向の標準偏差
int borderType  ピクセル外挿手法


ksizeについては、ksize.widthとksize.heightがそれぞれ正の奇数である必要があります。
サイズを大きくすると、それだけぼかしが強くなります。
ゼロを指定した場合は、sigma1、sigma2から計算されます。

sigma1、sigma2については、それぞれx方向(横方向)、y方向(縦方向)の標準偏差です。
ksizeの対応する方向軸がゼロの場合のみ計算に利用されます。
片方がゼロであれば、もう片方と等しくなるよう設定され、両方がゼロであればksizeから計算されます。

borderTypeについては、画像を回転する その2 - OpenCV for Androidの第6引数と同じで、画像の外側をどう扱うかを指定します。
例によって省略可能です。



メディアンフィルタを用いた平滑化を行うにはImgproc.medianBlur()メソッドを利用します。

Imgproc.medianBlur(Mat src, Mat dst, int ksize)

Mat src     処理したい元画像のMat
Mat dst     変換後Mat
int ksize    アパーチャサイズ


ksizeについては、1より大きな奇数を指定します。
ネットで調べるとksizeによって、処理出来る画像の画像のビット深度が違うと書いてありますが、特にエラーは出ませんでした。



バイラテラルフィルタを用いた平滑化を行うにはImgproc.bilateralFilter()メソッドを利用します。
基本的に処理が重たい割りに、大きな変化はありません。

Imgproc.bilateralFilter(Mat src, Mat dst, int d,
double sigmaColor, double sigmaSpace, int borderType)

Mat src          処理したい元画像のMat
Mat dst          変換後Mat
int d            各ピクセル近傍領域の直径
double sigmaColor   色空間におけるフィルタシグマ
double sigmaSpace  座標空間におけるフィルタシグマ
int borderType      ピクセル外挿手法

srcについては、tyepがCV_8UC1かCV_8UC3である必要があります。
CvType.typeToString(mat.type())で確認し、必要に応じて変換して処理します。

dについては、この値が大きくなるとぼかしが強くなります。
ゼロ以下の場合は、sigmaSpaceから計算されます。

sigmaColorについては、この値が大きくなると、近くにある色的により遠くのピクセルが混ぜ合わせられます。

sigmaSpaceについては、この値が大きくなると、より遠くのピクセル同士が影響しあいます。
dがゼロ以下の場合のみ計算に利用されます。

borderTypeについては、同じく省略可能です。



それでは長くなりましたが、実際にやってみましょう。


元画像です。


ガウシアンフィルタです。
Imgproc.GaussianBlur(mat, dstMat, new Size(11, 11), 0, 0);


ksizeを変更してみます。
Imgproc.GaussianBlur(mat, dstMat, new Size(51, 3), 0, 0);


メディアンフィルタです。
Imgproc.medianBlur(mat, dstMat, 7);


ksizeを変更してみます。
Imgproc.medianBlur(mat, dstMat, 21);


バイラテラルフィルタです。
Imgproc.cvtColor(mat, mat, Imgproc.COLOR_RGBA2BGR);
Imgproc.bilateralFilter(mat, dstMat, 10, 50, 0);
Imgproc.cvtColor(dstMat, dstMat, Imgproc.COLOR_BGR2RGBA);


dを変更してみます。
かなり重たいです。
Imgproc.cvtColor(mat, mat, Imgproc.COLOR_RGBA2BGR);
Imgproc.bilateralFilter(mat, dstMat, 50, 50, 0);
Imgproc.cvtColor(dstMat, dstMat, Imgproc.COLOR_BGR2RGBA);




しかし、なぜGaussianBlur()だけ大文字から始まるんでしょう??
謎です。


2012年3月20日火曜日

画像の膨張・収縮処理を行う その3 - OpenCV for Android


膨張、収縮処理についてもう一点。

kernelことdilate()、erode()、morphologyEx()メソッドに渡す構造要素についてです。

はじめは以下のように、コンストラクタでサイズを指定してMatクラスのインスタンスを生成したのですが、どうやらこれでは間違っているようです。
Imgproc.dilate(mat, mat, new Mat(5, 10, CvType.CV_8UC1));




実行するたびに若干違う結果になったり、数ピクセル画像がズレたり、正しく画像を処理できません。

デフォルトコンストラクタでMatを生成した場合は特に問題は起きないので、サイズがゼロだと正しく3×3の構造要素を作成してくれるようです。

では構造要素を指定したい場合どうするかと言うと、Imgproc.getStructuringElement()メソッドを利用します。


Imgproc.getStructuringElement(int shape, Size ksize, Point anchor)

int shape   構造要素の形状
Size ksize  構造要素のサイズ
Point anchor 要素内のアンカー位置


shapeについては、Imgprocに用意された以下の定数から選択します。

MORPH_RECT   矩形構造要素
MORPH_ELLIPSE  楕円構造要素
MORPH_CROSS  十字形構造要素


anchorについては、十字形構造要素の場合にのみ意味を成します。


それでは実際にやってみましょう。

矩形構造要素です。
Imgproc.dilate(mat, mat,
  Imgproc.getStructuringElement(Imgproc.MORPH_RECT,
  new Size(2, 10));


楕円構造要素です。若干丸みがありますね。
Imgproc.dilate(mat, mat,
  Imgproc.getStructuringElement(Imgproc.MORPH_ELLIPSE,
  new Size(2, 10));


十字形構造要素です。まずはアンカー位置デフォルトから。
Imgproc.dilate(mat, mat,
  Imgproc.getStructuringElement(Imgproc.MORPH_CROSS,
  new Size(5, 10), new Point(-1, -1)));


アンカー位置を端によせてみます。
Imgproc.dilate(mat, mat,
  Imgproc.getStructuringElement(Imgproc.MORPH_CROSS,
  new Size(5, 10), new Point(4, 9)));


逆サイドへ。
Imgproc.dilate(mat, mat,
  Imgproc.getStructuringElement(Imgproc.MORPH_CROSS,
  new Size(5, 10), new Point(0, 0)));


色々な形状の構造要素を試してみくなりましたが、それはまた別の機会に。

画像の膨張・収縮処理を行う その2 - OpenCV for Android


前回実験した、膨張(Dilation)と収縮(Erosion)はそれ単体で利用されることは少なく、それぞれを組み合わせることで様々な画像処理を行います。
このあたりをまとめて「モルフォロジー演算」と呼び、ノイズ除去や平滑化などに利用されます。

OpenCVでは膨張、収縮を併せると7種類の演算が実装されていて、Imgproc.morphologyEx()メソッドを利用します。

Imgproc.morphologyEx(Mat src, Mat dst, int op, Mat kernel,
Point anchor, int iterations)

Mat src     処理したい元画像のMat
Mat dst     変換後Mat
int op      演算の種類
Mat kernel   使用する構造要素を表すMat
Point anchor  要素内のアンカー位置
int iterations  実行回数


op以外の引数は、Imgproc.dilate()や、Imgproc.erode()と同じです。

opについては、Imgprocに用意された以下の定数から選択します。

MORPH_ERODE   結果はerode()と同様
MORPH_DILATE    結果はdilate()と同様
MORPH_OPEN    オープニング
MORPH_CLOSE    クロージング
MORPH_GRADIENT  モルフォロジー勾配(エッジ検出)
MORPH_TOPHAT   トップハット
MORPH_BLACKHAT  ブラックハット

オープニングは収縮した結果に対して同じ回数だけ膨張する処理を指します。
クロージングは膨張した結果に対して同じ回数だけ収縮する処理を指します。
モルフォロジー勾配は膨張した結果から収縮した結果を差し引く処理を指します。
トップハットは元画像からオープニングした画像を差し引く処理を指します。
ブラックハットはクロージングした画像から元画像を差し引く処理を指します。

なお、余談ですが「Imgproc.MORPH_」から始まる定数は、上記以外にも「MORPH_RECT」「MORPH_CROSS」「MORPH_ELLIPSE」の3つあります。
これらは、Imgproc.getStructuringElement()メソッドで利用し、モルフォロジー演算の引数(kernel)となる、指定されたサイズと形状の構造要素を返します。


それでは実際にやってみましょう。

元画像は月にしてみます。
「Imgproc.THRESH_OTSU」を利用すると単なる円になってしまうので、閾値を指定して二値化します。
Imgproc.threshold(mat, mat, 220.0, 255.0,
  Imgproc.THRESH_BINARY_INV);



まずは収縮です。
Imgproc.morphologyEx(mat, mat, Imgproc.MORPH_ERODE,
  new Mat(), new Point(-1, -1), 3);




続いて膨張です。
Imgproc.morphologyEx(mat, mat, Imgproc.MORPH_DILATE,
  new Mat(), new Point(-1, -1), 3);



オープニング。(収縮 ⇒ 膨張)
Imgproc.morphologyEx(mat, mat, Imgproc.MORPH_OPEN,
  new Mat(), new Point(-1, -1), 3);



クロージング。(膨張 ⇒ 収縮)
Imgproc.morphologyEx(mat, mat, Imgproc.MORPH_CLOSE,
  new Mat(3, 3, CvType.CV_8UC1), new Point(-1, -1), 3);



モルフォロジー勾配。(膨張 マイナス 収縮)
Imgproc.morphologyEx(mat, mat, Imgproc.MORPH_GRADIENT,
  new Mat(), new Point(-1, -1), 3);



トップハット。(元画像 マイナス オープニング)
Imgproc.morphologyEx(mat, mat, Imgproc.MORPH_TOPHAT,
  new Mat(), new Point(-1, -1), 3);



ブラックハット。(クロージング マイナス 元画像)
Imgproc.morphologyEx(mat, mat, Imgproc.MORPH_BLACKHAT,
  new Mat(), new Point(-1, -1), 3);


ということで、モルフォロジー演算にはゴミ、ノイズの除去や、単純二値化では検出出来ないものを検出、抽出出来るようにしたりなど、様々な応用が考えられます。


2012年3月11日日曜日

画像の膨張・収縮処理を行う その1 - OpenCV for Android


今回も画像の膨張・収縮とは何ぞやから入りましょう。


主に2値化された画像に対して、対象の周辺に白い画素が1画素でもあれば白に置き換える処理を膨張、逆に周辺に黒い画素があれば黒に置き換える処理を収縮と言うそうです。
膨張と収縮を同じ回数実行することで、大きなパターンだけ残すような処理に良く利用します。


画像の膨張を行うには、Imgproc.dilate()メソッド、収縮にはImgproc.erode()メソッドを利用します。

Imgproc.dilate(Mat src, Mat dst, Mat kernel,
Point anchor, int iterations, int borderType, Scalar borderValue)

Imgproc.erode(Mat src, Mat dst, Mat kernel,
Point anchor, int iterations, int borderType, Scalar borderValue)


Mat src        処理したい元画像のMat
Mat dst        変換後Mat
Mat kernel      使用する構造要素を表すMat
Point anchor     要素内のアンカー位置
int iterations     実行回数
int borderType    ピクセル外挿手法
Scalar borderValue  定数境界モードで利用されるピクセル値


kernelについては、デフォルトが3×3で、1×1だと何もおこりません。
設定することで、膨張、収縮の度合いを縦横で差をつけることが出来ます。
独自の形状も使えるようです。
※Matのコンストラクタでサイズを指定するとうまくいかないようです。
 12/3/20修正。詳細はその3を参照。

anchorについては、デフォルトがkernelの中心を表す特別な値でPoint(-1、-1)です。
いくつか試してみましたが、結果に差は認められませんでした。

borderTypeとborderValueは画像を回転する その2 - OpenCV for Androidの第6引数以下と同じで、画像の外側をどう扱うかを指定します。

anchor以下は省略可能です。


それでは実際にやってみましょう。

元画像です。


すべてデフォルトで膨張してみます。
Imgproc.dilate(mat, mat, new Mat());


使用する構造要素を1×10に設定します。縦方向に膨張します。
getStructuringElement()メソッドについてはその3で説明します。
Imgproc.dilate(mat, mat,
  Imgproc.getStructuringElement(Imgproc.MORPH_RECT,
  new Size(1, 10)));



使用する構造要素を10×1に設定します。横方向に膨張します。
Imgproc.dilate(mat, mat,
  Imgproc.getStructuringElement(Imgproc.MORPH_RECT,
  new Size(10, 1)));



画像の外側を白として扱い、分かりやすいように縦方向のみ膨張します。
Imgproc.dilate(mat, mat,
  Imgproc.getStructuringElement(Imgproc.MORPH_RECT,
  new Size(1, 20)), new Point(-1, -1), 1,
  Imgproc.BORDER_CONSTANT, new Scalar(255, 255, 255));



デフォルトのままで実行回数を増やしてみます。
Imgproc.dilate(mat, mat, new Mat(),
  new Point(-1, -1), 3);




収縮もやってみます。
Imgproc.erode(mat, mat, new Mat(),
  new Point(-1, -1), 3);



ちなみにこれカラー画像やグレースケールに対して実行するとどうなるのでしょう?

グレースケール。



カラー。



水墨画や水彩画のようになりましたね。


2012年3月4日日曜日

画像ピラミッドを作成する - OpenCV for Android


画像ピラミッドって何?というところから入りますが、「解像度の異なる同一画像の集合」だそうです。
画像の拡大縮小表示や、処理の高速化のためにまず低解像度画像に対してざっくり処理し、徐々に精度を上げていく手法(coarse-to-fine)などに利用されます。

画像ピラミッドを作成するには、pyrDown()、pyrUp()メソッドを利用します。
C++ではbuildPyramid()という縮小のみ行うメソッドがありますが、JAVAでは見当たりませんでした。
裏ではpyrDown()を再帰で呼んでListに格納しているだけのようなので、必要であれば自分で実装すれば良いと思います。


Imgproc.pyrDown(Mat src, Mat dst, Size dstsize)

Mat src    処理したい元画像のMat
Mat dst    変換後Mat
Size dstsize 変換後サイズ

dstsizeに関しては制限が厳しいようで、何パターンか試しましたがsrcの1/2以外通りませんでした。
dstsizeを省略しても同じく1/2になるようなので、省略してしまえば良いようです。
さらに縮小する場合は、縮小後のMatに再度pyrDownを実行すれば縮小されます。


Imgproc.pyrUp(Mat src, Mat dst, Size dstsize)

Mat src    処理したい元画像のMat
Mat dst    変換後Mat
Size dstsize 変換後サイズ


ちなみに、どちらもmatは使いまわしがききます。


それでは実際にやってみましょう。

ダウンサンプリング1回。
Imgproc.pyrDown(mat, dstMat);


ダウンサンプリング2回。
Imgproc.pyrDown(mat, dstMat);
Imgproc.pyrDown(dstMat, dstMat);


アップサンプリング1回。
Imgproc.pyrUp(mat, dstMat);


アップサンプリング2回。
Imgproc.pyrUp(mat, dstMat);
Imgproc.pyrUp(dstMat, dstMat);


拡大縮小と何が違うのか良く分かりませんが、coarse-to-fineのために前処理として使うことになるのだと思われます。

OpenCVで画像を扱う際の注意点 - OpenCV for Android


序盤でハマった話を書いていなかったので、このあたりで書いておこうと思います。
Google先生に聞いても日本語のサイトは出てこないかも。

かなーりハマったのですが、OpenCV for Androidはどんな画像でもそのまますんなり処理してくれるわけではありません。
BitmapからMatへ変換してから何かの処理をするわけですが、いくつか画像を試すと読み込める画像と読み込めない画像が出てきます。

正確には、BitmapのフォーマットがARGB_8888以外の場合、Utils.bitmapToMat()の段階でサイズがゼロでチャンネルが1のMatを返すようです。
mat.empty()で調べるとtrueを返します。

当然何かの処理をしようとしても、実行エラーとなります。

そこで、Bitmapを変換する際は、以下のコードで変換を行っています。
Bitmap src = BitmapFactory.decodeResource(
  getResources(), R.drawable.resourceid);
Mat mat = Utils.bitmapToMat(src.copy(
  Config.ARGB_8888,true));

これだけの事ですが、解決するのにかなりの時間を費やしました。。

透視変換を行う - OpenCV for Android


透視変換を行うにはImgproc.getPerspectiveTransform()メソッドで変換元と変換先の4点を指定して変換行列を計算し、Imgproc.warpPerspective()メソッドで変換します。

Imgproc.getPerspectiveTransform(Mat src, Mat dst)

Mat src  変換元の四角形のMat
Mat dst  変換先の四角形のMat


Imgproc.warpPerspective(Mat src, Mat dst, Mat M, Size dsize, int flags, int borderMode, Scalar borderValue)

引数はImgproc.warpAffine()メソッドと同じなので省略します。


まずはgetPerspectiveTransform()の引数となるMatを作成します。
4つのPointを持つArrayListを作成し、Convertersクラスのメソッドvector_Point_to_Mat()でMatへ変換します。
この際、タイプをCV_32FかCV_64Fに指定する必要があります。
ここでかなりハマりました。

List<point> srcPoints = new ArrayList<point>(4);
srcPoints.add(new Point(150,150));
srcPoints.add(new Point(150,300));
srcPoints.add(new Point(350,300));
srcPoints.add(new Point(350,150));
Mat srcPointsMat = Converters.vector_Point_to_Mat(
  srcPoints, CvType.CV_32F);

List<point> dstPoints = new ArrayList<point>(4);
dstPoints.add(new Point(200,150));
dstPoints.add(new Point(200,300));
dstPoints.add(new Point(340,270));
dstPoints.add(new Point(340,180));
Mat dstPointsMat = Converters.vector_Point_to_Mat(
  dstPoints, CvType.CV_32F);


ついでにborderModeを指定してみましょう。
Imgproc.warpPerspective(mat, dstMat, affineTrans,
  dstMat.size(), Imgproc.INTER_LINEAR,
  Imgproc.BORDER_REFLECT);


それっぽくなりましたが分かりづらいので、Chromeアイコンでもやってみましょう。



うまくいってますね。

画像を回転する その3 - OpenCV for Android


前回は変換元と変換後の三点を指定し画像を回転しました。
今回は変換パラメータを指定してアフィン変換行列を計算してみます。

変換パラメータを指定するには、Imgproc.getRotationMatrix2D()メソッドを使用します。

Imgproc.getRotationMatrix2D(Point center, double angle, double scale)

Point center   回転の中心点
double angle   回転角度
double scale   拡大倍率

angleが正の値であれば反時計回りを意味します。
ちなみに単位は度(degree)で90なら単純に90度です。
角度の単位には度に始まり、ラジアンやらゴンやら歩兵ミルやら色々あります。
この手の話になるとラジアンが良く出てきますが、直感的にイメージ出来ないのであまり好きではないです。
「度」最高。


それでは実際にやってみましょう。

まずは元画像です。


30度回転してみます。
Point center = new Point(0.0, 0.0);
double angle = 30.0;
double scale = 1.0;
Mat affineTrans =
  Imgproc.getRotationMatrix2D(center, angle, scale);
Imgproc.warpAffine(mat, dstMat, affineTrans,
  dstMat.size());


パラメータを少し変更してみます。
Point center = new Point(100.0, 100.0);
double angle = 30.0;
double scale = 0.2;
Mat affineTrans =
  Imgproc.getRotationMatrix2D(center, angle, scale);
Imgproc.warpAffine(mat, dstMat, affineTrans,
  dstMat.size());


borderModeを指定してみます。
Point center = new Point(100.0, 100.0);
double angle = 30.0;
double scale = 0.2;
Mat affineTrans =
  Imgproc.getRotationMatrix2D(center, angle, scale);
Imgproc.warpAffine(mat, dstMat, affineTrans, dstMat.size(),
  Imgproc.INTER_LINEAR,Imgproc.BORDER_REFLECT);

またまたサイケな画像がw
でも今回は狙い通りです。