OpenCV

自炊PDF用のプログラムを修復した

正確には、プログラムを修復したのではなく、ビルドするための環境を修復したのだが。

最初にプログラムを書いたのは、2013年頃で、みようみまねで、OpenCVとC++を勉強しながら、何とか動作するプログラムを作成した。その後、2015年頃かな? HomebrewとかXCodeをアップデートした影響だと思うが、コンパイルできなくなってしまった。あれこれ調べて、OpenCVのバージョンを3から2に下げ、ImageMagickもヴァージョン7から6に下げることで、なんとかコンパイルできるようになった。

以上は、すべて2011版のMacBook Airでの話。2020年にMacBook Pro (最後のインテルMacですよ) に変わって、いまでも使ってるのだが、久しぶりに自作のプログラムを起動しようとしたら、ライブラリーが見つからないとかの実行時エラーで動かなかった。なんとか、シンボリックリンクを張り、とりあえず動作するようにはなったが、これでは、プログラムの修正や改良もできない。やれやれ。

一番良いのは、最新の環境にして、一からプログラムを書き換えることだと思うが、もうね、いろんな事を忘れているし、新しいことを覚えるには歳もとりすぎている。ううむ、困る・・・。

ということで、とりあえず、今の環境でプログラムのビルドを可能にしたい。いろいろやったところ、前回と同様に、OpenCVとImageMagickのバージョンを下げることで、成功した。

実は、OpenCVの方は、古いバージョン2を既にhomebrewで入れてある。ただし、リンクは張ってない状態で、Cellarに入っていて、/usr/local/opt/opencv@2 から参照できるようにもなっている。そこで、コンパイルするときに、それを読み込むように Makefile に書く。

INC_OPENCV2_PATH = /usr/local/opt/opencv@2/include
LIB_OPENCV2_PATH = /usr/local/opt/opencv@2/lib

そして、これをコンパイルするときに、-I$(INC_OPENCV2_PATH) -L$(LIB_OPENCV2_PATH) として読み込む。

OpenCVは以上でOKなのだが、ImageMagickで困った。何を読み込むのか複雑なため、コンパイルのときに、Magick++-config なるプログラムを実行させて、必要なライブラリなどを取り込むのが普通のようだった。そのため、imagemagick@6 以下にバージョン6を収録するだけでは不十分で、リンクを張る必要がある。

brew link imagemagick@6 --force


で良いと思ったのだが、エラーが出た。曰く、

Linking /usr/local/Cellar/imagemagick@6/6.9.12-42...
Error: Could not symlink bin/Magick++-config
Target /usr/local/bin/Magick++-config
is a symlink belonging to imagemagick. You can unlink it:
brew unlink imagemagick

To force the link and overwrite all conflicting files:
brew link --overwrite imagemagick@6

そこで、

brew unlink imagemagick
brew link --overwrite imagemagick@6


とした。
必要ならパスを通せ、というメッセージが出たが、ワタシの場合、それはしなくても大丈夫だった。convert –version などとしてバージョンを調べると、ちゃんと6になっている。これで、ImageMagickのバージョン6にダウングレード出来た。

あとは、以前のMakefileのままでmakeして、警告はいっぱい出るものの、エラーはなくて、なんとかコンパイルに成功した。

次の目標は、OpenCV3とImagemagick7で動作するように、改良することだが、何時になるか。いや、この際、C++ではなく、Pythonに乗り換えるという手もある。Python上にも、OpenCVや他の画像ライブラリがあるらしいのである。

Mavericksにしたら

Mavericksにしたから,というより,Xcode, Homebrew などの開発環境が変わってしまったから,という方が正確なのだが,ともかく,自作のプログラムが動作しなくなった。ちなみに,Homebrewで入れたOpenCVなどを利用してC++で書いた自家用の画像処理プログラム。自炊した書籍を600dpiのカラーJPEGから1200dpiのモノクロTIFFに変換するのに用いている。

まず,そのままターミナルから起動させようとすると,
dyld: Library not loaded: /usr/local/lib/libopencv_core.2.4.dylib というエラー。
よくよく考えると,単にライブラーを探せないという話なので,慌てずにシンボリックリンクを貼り直せが良かったのかもしれない。しかし,開発環境一式をアップデートすることしか頭になかった。結局,何時かはアップデートすることになるので,悪いわけではなかったのだが。

homebrewでbrew doctorとすると,あれこれ古くなっているらしい。そこで,開発環境をアップデートすることにした。MacApp StoreからXcodeの最新版をDLしてインストール。Homebrewもアップデート。そして,OpenCVをhomebrewで入れ直すことに。すると,numpyがないのでインストール出来ないと。そこで,numpyを入れて,brew reinstall opencv として再インストール。

さて,自家用プログラムをビルドしてみる。pkg-configがないよ,とエラー。入れる。Imagemagickがないよ。ん?そんなバカな。あと,boost関係も何か変。

ということで,pkg-config, imagimagick, boost を再インストール。
これで,とりあえずビルドは成功なのだが,実行させるとランタイムエラーが発生。
dyld: Library not loaded: /usr/local/lib/libjpeg.8.dylib

思いつく解決策は2つ。その1。コンパイル時にライブラリーの場所を指定しておく。多分デフォルトと違うのだろう。その2。シンボリックリンクを貼る。どっちにするか,悩ましい。
/usr/local/Cellar/ を探すとあったので,とりあえずリンクを張る。

ln -s /usr/local/Cellar/jpeg/8d/lib/libjpeg.8.dylib /usr/local/lib/

さあ,これで大丈夫かなと思ったら,別のランタイムエラー。libc++abi.dylib: Magick: no encode delegate for this image format `TIFF'

brew reinstall libtiff として,libtiffを再インストールし,brew reinstall imagemagick –with-lib-tiff として,imagemagickもオプション付きで再インストール。これで TIFF もサポートされるはず。

これで何とか動作するようになった。ああ疲れた。

OpenCVで自炊本をモノクロ2値に変換してみた

自炊代行業者さん(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 &lt;1 と正規化したとき,f(x)={(x-low)/(high-low)}^{1/gamma} としてみた。
//---------------------------------------------------------------

// グローバル変数
uchar g_LUT&#91;256&#93;; // 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&#91;i&#93; = 0; }
    for (i=high; i&lt;256; i++) { g_LUT&#91;i&#93; = 255; }
    for (i=low; i<high; i++) 
    {
      x = (double) i;
      // 16階調にする場合 16x16=256
      //g_LUT&#91;i&#93; = cvCeil( ((x-a)/(b-a)) * 15 ) * 16;
      g_LUT&#91;i&#93; = 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&#91;"cleft"&#93;.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 < < "&#91;input, output, high, low&#93; required" << endl;
    cerr << opts << endl;
    return 1;
  }

  int high = argmap&#91;"high"&#93;.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&#91;"output"&#93;.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);
    }    
  }
}

ガンマ補正の式がわからない

OpenCVを使って,自炊した本の画像の変換は,まずまず上手く行っているのだが,ガンマ補正のところだけが分からない。しかたないので,とりあえずは gamma=1 つまり直線補間しているのだが,どこが間違っているのだろうか。

//—————————————————————
// cv_Level
// レベル補正・ガンマ補正
// (ImageMagickの convert -level white_point black_point gamma と同じにしたいのだが)
// src = 入力画像
// dst = 出力画像
// high = ホワイトポイント (0..255)
// low = ブラックポイント (0..255)
// gamma = ガンマ補正値
// 注意:暫定版。ガンマ補正の式はこれで正しいのか不明。
// 0high)
{
LUT[i] = 255;
}
else
{
x = (double) i /255.0;
LUT[i] = cvCeil( ((pow(x,p)-a)/(b-a)) * 255.0 );
//LUT[i] = (int) ( pow( ( ((double)i – (double)low) / ((double)high – (double)low) ), 1.0 / gamma ) * 255.0 );
//LUT[i] = (int)(pow((double)i / 255.0, 1.0 / gamma) * 255.0);
}
}
*/
for (i=0; i