自炊代行業者さん(Bookscan)に依頼した本のPDFがたくさんあるのだが,読みやすく美白化して白黒2値に変換したい。それを実現するためのプログラム。まだ試作段階だが,とりあえず動作している。以前,GIMP用のスクリプトやImageMagick用のPerlスクリプトで行っていたことを,OpenCVでやってみた。以前のバージョンに比べて,明らかにスピードが速い。OpenCVはもちろんC++でプログラム書いたことないので,無事にコンパイル出来たときは,ちょっと嬉しかったな。
参考にしたのは,次のページ。参考というよりは,ほとんどそのまま頂いている。有難きは先達なり。
橋本商会 scansnapで自炊した本をkindleで読めるように補正する(2)
このページに書かれているプログラムをひな形として,自分の目的に合うように処理内容をあちこち変えた。実は,boost::filesystemのバージョンが上記ページが書かれた頃から変わっていたため,そのままではコンパイル出来なかった。また,OpenCVのバージョンも上がっていて,ライブラリーの名前などが以前とは変わっているようなので,それに応じてMakefileも少し修正を必要とした。ちなみに,OpenCVもboostもHomebrewでインストールした。バージョンは brew info によれば,それぞれ opencv: stable 2.4.5, boost: stable 1.54.0 (bottled) となっている。
やっていることは,次の通り。
- 赤チャンネルだけを抽出することでグレースケールに変換
- 上下左右の余白を白で塗りつぶす
- Lanczos法により2倍に拡大
- ルックアップテーブルを作成することにより,レベル補正,ガンマ補正
- 2値化
- PNGで保存
レベル補正,ガンマ補正の式が多分正しくない。gamma=1のときは直線補間だから正しいと思うが,それ以外のときの式がわからない。ImageMagickの level high low gamma と同じにしたいのだが。これは今後の課題。
PNGで保存しているが,可能であれば,CCITT G4圧縮のTIFFで保存したい。これも今後の課題。
#include "cv.h" #include "highgui.h" #include <boost /program_options.hpp> #include </boost><boost /filesystem/operations.hpp> #include </boost><boost /filesystem/path.hpp> #include </boost><boost /filesystem/fstream.hpp> #include <iostream> using namespace boost; using namespace std; namespace fs = boost::filesystem; //--------------------------------------------------------------- // my_Level // レベル補正・ガンマ補正 // (ImageMagickの convert -level white_point black_point gamma と同じにしたいのだが) // src = 入力画像 // dst = 出力画像 // high = ホワイトポイント (0..255) // low = ブラックポイント (0..255) // gamma = ガンマ補正値 // 注意:暫定版。ガンマ補正の式はこれで正しいのか不明。 // 0<x <1 と正規化したとき,f(x)={(x-low)/(high-low)}^{1/gamma} としてみた。 //--------------------------------------------------------------- // グローバル変数 uchar g_LUT[256]; // Lookup table用の配列 CvMat g_lut_mat; // それをopencvの行列とみなしたもの void make_lut(int high, int low, double gamma){ int i; double x; const double p = 1.0 / gamma ; const double a = (double) low; const double b = (double) high; //補正用のルックアップテーブルの作成 for (i=0; i<low; i++) { g_LUT[i] = 0; } for (i=high; i<256; i++) { g_LUT[i] = 255; } for (i=low; i<high; i++) { x = (double) i; // 16階調にする場合 16x16=256 //g_LUT[i] = cvCeil( ((x-a)/(b-a)) * 15 ) * 16; g_LUT[i] = cvCeil( 255.0 * pow((x-a)/(b-a), p) ); } //CvMatへ変換 g_lut_mat = cvMat(1, 256, CV_8UC1, g_LUT); } void my_Level(IplImage* src, IplImage* dst) { //ルックアップテーブル変換 cvLUT(src, dst, &g_lut_mat); } IplImage *adjust_image(IplImage *img, program_options::variables_map argmap){ int cleft = argmap["cleft"].as<int>(); int cright = argmap["cright"].as<int>(); int ctop = argmap["ctop"].as</int><int>(); int cbottom = argmap["cbottom"].as</int><int>(); int threshold = argmap["threshold"].as</int><int>(); const int w = img->width; const int h = img->height; const int w1 = 2*w; const int h1 = 2*h; const int x1 = cleft; const int x2 = w-cright; const int y1 = ctop; const int y2 = h-cbottom; IplImage *img_gray; //赤チャンネルを抽出してグレースケールの画像を得る //チャンネル毎に分割して,赤チャンネル以外は捨てる // IplImageはB,G,Rの順に格納されている // アルファチャンネルは用いないので,5番目の引数はNULLにしておく // cvSplit(img,channelB,channelG,channelR,NULL); img_gray = cvCreateImage(cvSize(w,h),IPL_DEPTH_8U,1); //cvCvtColor(img, img_gray, CV_BGR2GRAY); cvSplit(img,NULL,NULL,img_gray,NULL); //周囲を白で塗る cvRectangle(img_gray, cvPoint (0,0), cvPoint (x1,h), cvScalar (255), CV_FILLED, 8, 0); cvRectangle(img_gray, cvPoint (x2,0), cvPoint (w,h), cvScalar (255), CV_FILLED, 8, 0); cvRectangle(img_gray, cvPoint (0,0), cvPoint (w,y1), cvScalar (255), CV_FILLED, 8, 0); cvRectangle(img_gray, cvPoint (0,y2), cvPoint (w,h), cvScalar (255), CV_FILLED, 8, 0); //2倍に拡大 IplImage *img_resize = cvCreateImage(cvSize(w1,h1), IPL_DEPTH_8U, 1); cvResize(img_gray, img_resize, CV_INTER_LANCZOS4); //レベル補正 IplImage *img_level = cvCreateImage(cvSize(w1,h1), IPL_DEPTH_8U, 1); my_Level(img_resize, img_level); if (threshold > 0) { // 2値化する場合 IplImage *img_bin = cvCreateImage(cvSize(w1,h1), IPL_DEPTH_8U, 1); // ガウシアンフィルタで平滑化を行う // cvSmooth (img_level, img_level, CV_GAUSSIAN, 5); // 2値化 cvThreshold(img_level, img_bin, threshold, 255, CV_THRESH_BINARY); //cvThreshold(img_level, img_bin, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU); // 大津の手法 // メモリー開放 cvReleaseImage(&img_gray); cvReleaseImage(&img_resize); cvReleaseImage(&img_level); // 2値化した画像へのポインターを返す return img_bin; } else { // グレースケールで出力する場合 // メモリー開放 cvReleaseImage(&img_gray); cvReleaseImage(&img_resize); // レベル補正したグレースケール画像へのポインターを返す return img_level; } } int main(int argc, char* argv[]) { program_options::options_description opts("options"); opts.add_options() ("help", "ヘルプを表示") ("high", program_options::value</int><int>()->default_value(255), "level high") ("low", program_options::value</int><int>()->default_value(0), "level low") ("gamma", program_options::value<double>()->default_value(1.0), "level gamma") ("threshold,t", program_options::value<int>()->default_value(-1), "binarize threshold") ("input,i", program_options::value<string>(), "input directory name") ("output,o", program_options::value</string><string>(), "output directory name") ("cleft", program_options::value<int>()->default_value(0), "crop left (pixel)") ("cright", program_options::value</int><int>()->default_value(0), "crop right (pixel)") ("ctop", program_options::value</int><int>()->default_value(0), "crop top (pixel)") ("cbottom", program_options::value</int><int>()->default_value(0), "crop bottom (pixel)"); program_options::variables_map argmap; program_options::store(parse_command_line(argc, argv, opts), argmap); program_options::notify(argmap); if (argmap.count("help") || !argmap.count("input") || !argmap.count("output") || !argmap.count("high") || !argmap.count("low")) { cerr < < "[input, output, high, low] required" << endl; cerr << opts << endl; return 1; } int high = argmap["high"].as<int>(); int low = argmap["low"].as</int><int>(); double gamma = argmap["gamma"].as<double>(); // ルックアップテーブルを作成する。ルックアップテーブルはグローバル変数 g_lut_mat である。 make_lut(high,low,gamma); string in_dir = argmap["input"].as<string>(); fs::path path = complete(fs::path(in_dir)); fs::directory_iterator end; for (fs::directory_iterator i(path); i!=end; i++){ string img_fullname = in_dir + i->path().filename().string(); cout < < img_fullname << endl; IplImage *img, *img_result; img = cvLoadImage(img_fullname.c_str()); if(!img){ cerr << "image file load error" << endl; } else{ img_result = adjust_image(img, argmap); // string out_filename = argmap["output"].as<string>() + "/" + i->path().filename().string(); string out_filename = argmap["output"].as</string><string>() + "/" + i->path().stem().string() + ".png"; cvSaveImage(out_filename.c_str(), img_result); cvReleaseImage(&img); cvReleaseImage(&img_result); } } }