はじめに
今回は適応的二値化とk-means法を用いた減色によってアニメ風の画像を作ってみました。
実行環境は次の通りです。
- OS:MacOS Catalina
- OpenCV:ver4.1.2
ソースコード
とりあえず使えればいいやって人はソースコードをコピペして環境に合わせてコンパイルをしてやれば動くはずです。
このコードでは入力画像が実行ファイルと同じフォルダにある「test.jpg」で実行すると、結果が「output.jpg」として出力されます。
また、「cv::Mat filter(cv::Mat img)」が画像を処理する関数で、引数に「cv::Mat」の画像を与えると「cv::Mat」で画像を返すので何かの途中に使う場合は関数をコピペして貰えば使えるはずです。
#include <iostream> #include <opencv2/opencv.hpp> //フィルター cv::Mat filter(cv::Mat img){ //エッジ画像の生成 cv::Mat gray_img, binary_img, edge_img; //グレースケール画像, 二値化処理画像, エッジ画像 cv::cvtColor(img, gray_img, cv::COLOR_BGR2GRAY); //入力画像のグレースケール化 cv::adaptiveThreshold(gray_img, binary_img, 255, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY_INV, 5, 5); //適応的二値化処理によるエッジの検出 cv::cvtColor(binary_img, edge_img, cv::COLOR_GRAY2BGR); //グレースケール画像のBGR化 //メモリの解放 gray_img.release(); binary_img.release(); //減色画像の生成 cv::Mat points; //一行配列画像 img.convertTo(points, CV_32FC3); //行列のデータ型を変換 points = points.reshape(3, img.rows*img.cols); //行数を1に変換 cv::Mat_<int> clusters(points.size(), CV_32SC1); //クラスタインデックス cv::Mat centers; //クラスタ中心値 cv::kmeans(points, 20, clusters, cv::TermCriteria(cv::TermCriteria::MAX_ITER + cv::TermCriteria::EPS, 10, 1.0), 1, cv::KMEANS_PP_CENTERS, centers); //k-meansを適用 cv::Mat discol_img(img.size(), img.type()); //減色画像 //始点,終点イテレータ cv::MatIterator_<cv::Vec3b> itd = discol_img.begin<cv::Vec3b>(); cv::MatIterator_<cv::Vec3b> itd_end = discol_img.end<cv::Vec3b>(); //減色を実行 for(int i = 0; itd != itd_end; ++itd, ++i) { cv::Vec3f &color = centers.at<cv::Vec3f>(clusters(i), 0); (*itd)[0] = cv::saturate_cast<uchar>(color[0]); (*itd)[1] = cv::saturate_cast<uchar>(color[1]); (*itd)[2] = cv::saturate_cast<uchar>(color[2]); } //メモリの解放 points.release(); clusters.release(); centers.release(); //エッジ画像と減色画像の合成 cv::Mat filter_img; //合成後画像 cv::subtract(discol_img, edge_img, filter_img); //差分を取る //メモリの解放 discol_img.release(); edge_img.release(); return filter_img; //返り値 } //メイン処理 int main(){ cv::Mat src_img; src_img = cv::imread("test.jpg", 1); //画像データの読み込み if (src_img.empty()) { //エラー処理 std::cout << "read_error" << std::endl; return 0; } cv::Mat out_img; //出力フレーム格納用 out_img = filter(src_img); //フィルター処理 cv::imwrite("output.jpg", out_img); //出力 //メモリの解放 src_img.release(); out_img.release(); return 0; }
OpenCVの関数について簡単な説明
過去のOpenCVについての記事で書いていないものについて簡単に説明していきます。
減色部分については色々な要素が絡み合っているので、また別の記事で説明する予定です。
cv::cvtColor
画像の色空間を変更する関数です。
次のようにして使います。
cv::cvtColor(入力画像,出力画像,変換コード);
変換コードについては一覧のようなものを見つけることができなかったので、今回利用しているのは次の通りです。
- cv::COLOR_BGR2GRAY:BGR>グレースケール
- cv::COLOR_GRAY2BGR:グレースケール>BGR
cv::adaptiveThreshold
適応的二値化処理を行う関数です。
次のようにして使います。
cv::adaptiveThreshold(入力画像,出力画像,最大値,適応的閾値処理のアルゴリズム,閾値処理の種類,計算に使う領域サイズ,定数);
適応的閾値処理のアルゴリズムについては「ADAPTIVE_THRESH_MEAN_C」もしくは「ADAPTIVE_THRESH_GAUSSIAN_C」から選択できます。(周囲の画素の平均から定数を引くか、加重平均から定数を引くかの違い)
閾値処理の種類は「THRESH_BINARY」もしくは「THRESH_BINARY_INV」から選択できます。説明は以前の記事を参照してください。
計算に使う領域サイズとは周囲の画素をどこまで使うかで、これが大きいとエッジが太くなります。また「3,5,…,2n-1」でなければいけません。
定数とは計算する際に平均から引く定数のことです。これが小さいと多くのエッジを検出します。
実行してみた
入力画像、及び出力画像は次の通りでした。
最後に
どちらかというとお土産屋さんにある絵葉書みたいな雰囲気になりました。
エッジと減色は簡単に思いつくのですが、ここからどうやって本当にアニメっぽい画像にしていくかって感じですかね。
まだまだ改善の余地があるので、もしかしたら次回があるかもしれないです。
コメント