pdftkを用いてPDFのページ順を変更する

[備忘録]
[加藤和也・黒川信重・斎藤毅]数論1 Fermatの夢 を裁断,スキャン,加工してiPadで読んでいたのだが,どうもスキャンのときにページ順を間違えたらしく,PDFの73ページから130ページが逆順になっていた。ScanSnapでスキャンするときに,逆さまに置いたらしい。再度スキャンしてもよいのだが,pdftkで何とかなるらしい。

この場合,1-72ページをそのまま印刷し,73-130は逆順で印刷,そして131から後は再びそのまま印刷する,という感じでコマンドを入力すればよいらしい。具体的には次のようになる。

pdftk before.pdf  cat 1-72  130-73  131-end output after.pdf

ヤグロム著「初等的に解いた高等数学の問題」

ヤグロム著「初等的に解いた高等数学の問題」を裁断・スキャンした。古い本なのだが、何とホチキスで留めてある。このまま裁断すると刃がボロボロになってしまう。そこで、まずはホチキスを外す。これが案外と面倒。

ヤグロム 初等的に解いた高等数学の問題(1) 東京図書

ヤグロム 初等的に解いた高等数学の問題(1) 東京図書

しかるのちに、裁断機へ。

ヤグロム 初等的に解いた高等数学の問題(1) 東京図書

裁断したあとは、例によって、カラー、600dpiのjPEG画像でスキャンする。ここからあとはコンピューターでの作業。まずは、自作のプログラムで白黒画像に変換する。全体的に紙焼けがひどいので、周辺部の白塗り領域を多めに取り、黒を抑えて、パラメーターを high=245, low=10, gamma=1.0 あたりに設定。

mic02 -r 1  -i ./600dpi-jpeg3 -o ./group4tiff600/  --high 245 --low 10 --gamma 1.0 -t 200 --cleft 100 --cright 100 --ctop 100 --cbottom 100 --density 600

漢文の本とかだと、1200dpiにオーバーサンプリングして、1200dpiの白黒にするのだが、数学の本の場合、600のままでも十分なので、ファイルサイズのこともあって、600dpiのままで白黒に。出来上がった白黒2値のTIFFファイルをまとめて、複数ページTIFFにしてからPDFに変換する。

tiffcp ./group4tiff600/a0*.tiff bundle600.tiff
tiff2pdf -o bundle600.pdf bundle600.tiff

AcrobatでPDFを開き、別途スキャン・編集(GIMPにて)しておいた表紙画像を付けて、とりあえず完成。この段階でPDFのファイルサイズは、8.5MBになっている。ちなみに、600dpiのカラーJPEG(圧縮レベル3)でスキャンした段階では、174ページで148MBあった。

8.5MBでも十分に軽量であるのだが、Acrobat XでOptimize Scanned PDFというのを行い、白黒の圧縮法として JBIG2 Lossy というのを選ぶと更にサイズが小さくなる。このとき、一緒に傾き補正を行っておく方が良いかもしれない。JBIG2 Lossy の圧縮の結果、最終的にPDFのサイズは、4.4MBとなった。

ヴィノグラードフの整数論入門を裁断・スキャン

このところ,久しぶりに裁断・スキャンをしている。昨年の11月以来だろうか。
もっと良いやり方もあるのだろうが,いろいろな経験と実験の結果,白黒画像の書籍(数学の本は大体これだ)については,ほぼ固まってきた。
今日は,ヴィノグラードフ「整数論入門」(共立全書)の裁断・スキャンのメモ。

共立全書はハードカバー(ハードバウンド)なのに普通のハードカバーと違って,背表紙に直接接着剤でくっついているので,いささか面倒。完全に解体するのなら,カッターでガシガシ切れば良いのだが,カバーの外観を残しつつ解体しようとすると,デリケートな作業を強いられる。

ヴィノグラードル「整数論入門」(共立全書)の裁断1

ヴィノグラードル「整数論入門」(共立全書)の裁断2

ヴィノグラードル「整数論入門」(共立全書)の裁断3

ヴィノグラードル「整数論入門」(共立全書)の裁断4

ヴィノグラードル「整数論入門」(共立全書)の裁断5

裁断が済めば,あとは簡単。少し時間はかかるけれど,600dpiのJPEG画像で読み込むことにしている。ホントは圧縮なしのBMPとか可逆圧縮のPNGが良いのだが,ScanSnapではそれが出来ないので,やむを得ずJPEGで。それを自作のプログラム(OpenCVとImageMagickを利用したC++プログラム)でモノクロに変換する。パラメーターは本の状態で変えるのだが,赤チャンネルのみ抽出してグレースケールにしたあと,High=245, Low=20, Gamma=0.8くらいで変換しThreshold=200あるいはOotsuの手法(自動)にて2値モノクロにし,画像深度1bitでCCITT Fax Group4の圧縮にてTIFF画像として保存。場合によっては,最初に2倍にオーバーサンプリングして1200dpiにする。今回はそれは止めて,600dpiのままでモノクロにした。

600dpiのモノクロTIFF画像をPDFにまとめて,別途スキャン&加工しておいた表紙をつけて,とりあえずは出来上がり。今回はさらにAcrobatのOptimize Scanned PDFを行って,傾き補正をしつつOCRをし,圧縮をCCITT Group4からJBIG2(Lossless)に変更して軽量化した。

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 もサポートされるはず。

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

複数のtiff画像をまとめてPDFにする

裁断・スキャンして加工したTIFF画像たちをまとめてPDFにする方法。Acrobatでも出来るが,かなり時間がかかる。また,Windowsの画像梱包(pic2pdf)でも出来るはずなのだが,上手く行かない場合があり,理由も分からない。Macで一番手っ取り早いのはPreviewで読み込んでPDFで保存するというものだが,これだとファイルサイズが増加してしまう。せっかく1bitのモノクロ2値にしてGroup4 FAXの圧縮でファイルサイズを小さく加工したのが台無しになってしまう。

ということで,適当なツールを探していたのだが,UNIX(LINUX)ツールのtiffcpとtiff2pdfを使えばよいことを知ったので,メモ。

たとえば,あるフォルダーにある image001.tiff, image002.tiff, という複数のTIFFファイルをまとめたいとする。まずは,tiffcpを使って,

tiffcp image*.tiff bundle.tiff

とすれば,マルチページTIFFファイル bundle.tiffが出来る。次に,

tiff2pdf -o output.pdf bundle.tiff

とすれば,それが output.pdf というPDFファイルに変換される。

ターミナルからやってみたのだが,あっという間に変換が終わってしまい,処理が速いのに驚く。ターミナルからコマンドラインで出来るというのも,スクリプトで一括処理させるのに都合がよい。

gsdjvuをビルドしてdjvudigitalを使う

[備忘録]Ubuntu上でgsdjvuを自前ビルド&インストールしたのでメモ。

使いたいのは djvudigital というプログラム。djvudigital hoge.pdf とすれば,hoge.djvu が出来上がるというもの。そのためには,DjvuLibre というものを入れる必要がある。

DjvuLibre自体はSynapticパッケージマネージャーから簡単にインストール出来るのだが,これだけではダメ。djvudigitalを使おうとすると,これこれが必要なので,別途インストールしてね,的なメッセージが出てしまう。どうやらライセンス(GPLとかCPLとか)の関係で,GSDjvuというものが同梱できないらしい。その辺の事情はGSDjvuのページに書いてある。そこでGSDjvuのページをざっと読んで,自前でのビルドを試みた。

まずはGSDjvuをダンロードする。現時点での最新版は gsdjvu-1.6.tar.gz で,これを解凍した gsdjvu-1.6 というフォルダーを適当な場所に移動させる。今回は,/home/foo/gsdjvu-1.6 とした。ちなみに foo の箇所は自分のアカウント名に置き換え。そして,その中に BUILD という名前のフォルダーを作る。/home/foo/gsdjvu-1.6/BUILD となる。他の名前でも良いと思うが,BUILDがビルド用スクリプトでのデフォルト名になっているので,あとで名前を入力する手間が省ける。

次に,READMEに従って ghostscriptとフォント関係をダウンロード。ghostscript-8.64.tar.bz2 と ghostscript-fonts-std-8.11.tar.gz を指定のサイトからダウンロードして,/home/foo/gsdjvu-1.6/BUILD/ に置く。これ以外にオプションで,解凍ツールとかJPEG, PNG関係のライブラリーがダウンロードリストに載っているが,システムにある場合は,そちらが最新版だろうから,それを使った方が良いだろうというような事が書いてある。実際,これらはすでにシステムに入れてあるので,今回はパスした。

では,ビルド開始。シェルから /home/foo/gsdjvu-1.6/build-gsdjvu なるスクリプトを起動させる。途中でいくつか質問されるが,YESとかYを選択すれば良い。警告(Warning)がたくさん出るので一瞬あせるのだが,無事にビルド出来たようだ。

最後に,インストール。Makefile とか無いので,手動にて移動ならびにシンボリック・リンクを張る。READMEにはrootでインストールする方法が書いてあるが,UbuntuとかMacってルートが居ないんじゃなかったんだっけ? ということで,sudo を付けて次のように。

# sudo cp -r /home/foo/gsdjvu-1.6/BUILD/INST/gsdjvu /usr/local/lib
# cd /usr/local/bin
# sudo ln -s ../lib/gsdjvu/gsdjvu gsdjvu

これで終了。試しに,大鏡の文庫本を変換したら,15MBあったのが,5MBくらいになった。

今回は,Ubuntu上でビルドしたが,Macでもやってみたいと思う。djvulibreはhomebrewで入れてあるので。

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

ScanSnapの白黒2値読み取りについて

文字主体の本はできれば白黒2値,それが読みにくいなら16階調グレースケールにすることを念頭において,白黒2値化について実験中なのだが,とりあえず,ScanSnap IX500 の白黒2値読み取りについてのメモ。

300dpi カラー読み取りの画像を,赤チャンネル抽出によるグレースケール化,レベル補正,白黒2値化したものと比較すると,直接ScanSnapから300dpi 白黒でスキャンした画像の方が,品質が高そうに思える。

これが,300dpi カラーで読み取ったままの画像。

300dpiカラー圧縮度1

全体的に紙焼けがあるものの,文字は読みやすく,ファイルサイズを気にしなければ,実用上は問題ない。これを1つ前の日記に書いたように,画像梱包,Ralpha, XnView を用いて白黒2値にしたものが,次の画像。

300dpiカラー圧縮度1から白黒2値に変換

紙焼けも除去されていて,まずまずだと思う。ところが,ScanSnapから直接300dpi 白黒で読み込んだ次の画像と較べてみると,明らかに後者の方が品質がよさそうなのである。

300dpi白黒

紙の左端の方に紙焼けの影響が残ってはいるが,それ以外は手動で白黒化したものよりも綺麗というのが,納得の行かないところ。そんなに素晴らしいアルゴリズムなのか?

ところが,画像のサイズを見ると,この白黒画像は縦横ともに300dpiカラーの2倍あるのだ!これはどういうことだろうか。想像だが,300dpiで読み取ったものを2倍に拡大して,それから白黒2値化しているのではないだろうか。そう言えば,スーパーファインについては 300dpi (白黒は600dpi相当) と不思議な事が書いてあった。600dpi相当というのは,そういうことなのかもしれない。

そこで,実験。300dpiカラーで読み取った画像に対して,2倍に拡大(アルゴリズムはLanczos)した後に,白黒2値化を手続きを施してみた。それが次の画像。

300dpiカラー圧縮度1を600dpiに拡大したのち白黒2値に変換

紙焼け除去のためのレベル補正とかの影響を除けば,ScanSnapの300dpi白黒とほぼ同じ品質だと思う。ファイルサイズも,CCITT FAX G4圧縮のTIFFファイル同士で,ほぼ同じであった。

というような訳で,断言は出来ないが,ScanSnapが生成する白黒画像は,おそらくは2倍にリサンプリングしたのちに2値化しているのだろうと思う。だから,白黒とカラーのどちらか一方を選ぶとすれば,フルカラーの方が良いかも。白黒2値化はあとでソフトウェア的に行えるのだから。まあ,それが面倒なら白黒でも悪くはないのだが。

スキャンしたPDFの後処理

以前から懸案ではあったのだが,Windowsマシンも買ったことだし,画像処理関係のツールをいろいろ集めて実験してみた。実は,Mac上でもImageMagickなどで実験していたのだが,Windowsだと専用のツールが豊富にあるようなので,それがWindowsマシン購入の理由の1つだったりするのだ。

自分でスキャンするものについては,スキャンの段階で白黒2値とか選べるが,Bookscanなどのスキャン代行業者に依頼したものは,そういうわけにもいかず,黄色く紙焼けした本など,そのまんまカラーでスキャンされているものが,けっこうある。それをきれいに脱色して,白黒2値あるいは16階調グレースケールあたりに変換するのが目的である。

いくつか試してみた結果,画像梱包,Ralpha Image Resizer, XnView の3つを使って,とりあえず目的を果たした。XnViewはMac版やLinux版もあるが,あとの2つはWindows専用のプログラムである。

まず,画像梱包(pic2pdf)を使って,PDFの画像を抽出する。ソフトの注意書きには,画像梱包でPDFにしたものに対してのみ抽出可能とあったが,スキャンした本のPDFでも大丈夫のようだった。要するに,複雑な構造のPDFもあるから,それは無理ということなのだろう。本をスキャンして作ったPDFは,たいていの場合,単にJPEGのデータにヘッダー(XMLとか)を付加しただけであるから,問題ないのだと思う。抽出のスピードも非常に速い。AcrobatでJPEG出力するよりもずっと速いのだ。しかもJPEGのヘッダーを外すだけなので,基本的に無劣化で画像が取り出せるらしい。

次は,Ralpha Image Resizer を使っての画像処理。紙焼けは,周辺部がとくにひどいので,文字がないことを確認してから,上下左右をトリミングする。そして,同じ分量だけ付け加える。こうすると,紙の周囲が真っ白になる。次に,2倍に拡大する。これで,600dpi相当にオーバーサンプリングしたことになる。最後に白黒2値化するので,解像度を上げておきたいから。続いて,トーンカーブを調節する。ここのところは,ImageMagickのlevel処理のように,high, low, gamma の3つで簡単に処理しても良いと思うのだが,まあ,似たようなもの。プレビュー出来るので,結果を見ながら調節できる。最後に,赤チャンネルのみを抽出してグレースケールにする。

このあと,白黒2値でCCITT FAX G4圧縮のTIFFにしたいのだが,Ralphaでは白黒2値というメニューがない。グレースケールで色数を2色にすれば,白黒2値と同じなのかもだが,それでもJPEGになってしまうのは嫌なので,Ralphaだけで済ますのはあきらめた。結局,Ralphaでは,グレースケールにしてPNGとして出力させて終了。

Ralphaで出力したPNGファイルをXnViewで一括変換する。一気に白黒2値とどれくらい違うが疑問だが,段階を踏むことにして,グレースケールの色数を4bit, 3bit, 2bitと下げていき,最後に1bitつまり白黒2値にする。白黒2値にする際には,閾値(しきいち,threshold)というパラメーターがあるはずなのだが。何故か見当たらない。ここが謎なのだが,ともかく白黒2値にする。出力はCCITT FAX G4圧縮のTIFFファイルとする。

最後に,再び画像梱包を使う。解像度を600dpiと指定して,PDFに固める。解像度の指定をしておくと,Acrobatで見るとき,100%でもそれほど巨大にならないし,それに,表紙と裏表紙(これはPDFから抽出したままで,2倍にしていない)を後からAcrobatで加えるとき,同じ大きさで表示されるので,ちょうど良い。

以上で作業は終了。これをバッチ処理できるスクリプトがあればよいのだが。書式は,
mypdf2pdf hoge.pdf -level high low gamma -color 1-2 143-144 -resize 2 hoge2.pdf
とかいった感じで。