自炊代行業者さん(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);
}
}
}