虚苦心観察ブログ

ブログ管理者である虚苦心が私利私欲に基づいて書いているブログです。主にガジェットのレビューだったり、画像処理のことだったりを記事にしています。

sklearnのChange Logまとめ

バージョンアップでモジュールのパスなり引数の名前が変わってしまっていて、本を使って勉強をしているときに困ることが多いのでメモ。

  • パス
old path new path changed
sklearn.cross_validation sklearn.model_selection 0.18.0
sklearn.grid_search sklearn.model_selection 0.18.0
sklearn.learning_curve sklearn.model_selection 0.18.0
  • 引数 | function | old argument | new argument | changed | | :-- | :-- | :-- |

随時追加していこうかと思います。 載っていないものがあればコメントいただければ追記します。

Visual Studio 2015 で LLVM/Clang 3.6.2 をコンパイルする

LLVM/Clangはコンパイルのハードルが高いと思っていたのですが、そこまで大変ではないらしいことを知りました。

KMC Staff Blog:LLVM/Clang 3.6.2をVisualStudio 2013 Expressでビルド

上記の記事よりVisual Studioのバージョンが新しいのですが、コンパイルを試みました。 そしたらなぜかコンパイルエラーが。

Severity Code Description Project File Line Error C2664 'void llvm::BitstreamWriter::EmitRecordWithBlob<uint64_t>(unsigned int,llvm::SmallVectorImpl<uint64_t> &,llvm::StringRef)': cannot convert argument 3 from 'clang::serialization::DeclOffset *' to 'llvm::StringRef' clangSerialization  C:\Users\kyokushin\ProgramingLibraries\llvm-3.6.2.src\tools\clang\lib\Serialization\ASTWriter.cpp   2815

Visual Studioのバグでdataというテンプレート関数が別のインクルードファイルに含まれているテンプレート関数を呼び出してしまっているらしい。
エラー文からいくつかキーワードを引っ張ってググってみるとそれっぽいバグ報告を見つけた。

Bug 25650 – Cannot compile clang 3.7.0 with msvc 14 (2015)

このバグ報告によれば新しいClangでは直っているようです。なのでClang3.8.1のソースをダウンロードしてどのような変更があったのかを直接みました。 Clang3.8.1ではdataというテンプレート関数をbytesという名前に変更していることがわかり、同様の変更を加えてみました。

変更するファイルは一つ。
clang/lib/Serialization/ASTWriter.cpp

ファイル先頭にあるテンプレート関数dataを引数などはそのままで関数名のみをbytesに変更します。 同じファイル内でテンプレート関数dataを呼び出しているところをbytesに変更します。 一括置換で簡単に変更することができますが、dataというメソッド名も存在するので注意。 先頭にスペースを入れた" data("を検索文字列にすると対象となるテンプレート関数だけが置換できます。

変更後コンパイルしてみましたが、エラーなくコンパイルが終了しました。
こういうことがあると本当にVisual Studioはクソだなーって感じますね。

へっぽこ知識で作る画像処理プログラム 「自炊本のページ抜けチェック」 3.5日目

はじめに

2.5日目につづき今回もPython版を作成したので解説していこうと思います。 ただし、アルゴリズムについてはC++版を参考にしてもらうことにして、ここではC++版とPython版での違いについて紹介していきます。

C++Pythonでのピクセルアクセス方法の違い

C++ではOpenCVのクラスcv::Matに画像データを保持しますが、Pythonにはcv::Matは存在しません。
では何を使うのかというと、Python数値計算ライブラリであるnumpyのndarrayを利用します。

numpyのデータ構造の一つであるndarrayはC++のcv::Matに比べてかなり柔軟な書き方ができます。
例えば今回の場合だと、文字のない範囲を塗りつぶす、という処理を行っているわけですが、C++版とPython版を比べてみましょう。

C++

//文字のない範囲を3チャンネルの原画像に書き込む
    for (size_t i = 0; i < verticalRanges.size(); i++){
        Range& r = verticalRanges[i];
        //文字のない範囲を3チャンネルの原画像から切り出す
        cv::Rect rect(0, r.start, verticalRangeDst.cols, r.end - r.start + 1);
        cv::Mat roi(verticalRangeDst, rect);
        //切り出した画像を1色で塗りつぶす
        roi = cv::Scalar(0, 0, 255);
    }

Python

    for r in verticalRanges:
        verticalRangeDst[r.start:r.end, :, :] = (0, 0, 255)

C++版はちょっと冗長な書き方をしていますが、、、どうでしょう。Python版はものすごくシンプルで、なんと矩形の塗りつぶしだけなら1行で書けてしまいます!
ndarrayでの各ピクセルへのアクセス方法はPythonの配列と同等の方法になります。
そして":"はPythonのスライスと呼ばれる書き方です。 スライスを使うことでアクセスしたい配列の範囲を指定することができます。 スライスで指定した範囲に対して上記のように代入することができるため、C++に比べて非常に柔軟な書き方が可能です。

別の例として1チャンネルの画像から3チャンネルの画像を作る方法。cv::mergeを使わずに実装することができます。

color = numpy.zeros((gray.shape[0], gray.shape[1], 3), dtype=gray.dtype)
color[:,:,0] = gray
color[:,:,1] = gray
color[:,:,2] = gray

ただPython版のOpenCVにはcv2.merge関数が用意されていてそっちの方が簡単です。あくまで参考。

Pythonではrangeという組み込み関数が存在するので注意

Python版を作るうえで今回注意した点が一つあります。 それはrangeという名前の関数や変数を作らないことです。 なぜかというと、Pythonにはrangeという組み込み関数が存在し、誤って上書きしてしまうとrange関数を使っている場所で予期しない動作をする可能性があります。 今回の場合はrangeという変数名の代わりに頭文字のrをつかって対処しました。 PythonC++に比べて柔軟な書き方が出来て便利な反面、こういった落とし穴もあるので注意が必要です。

出来上がったプログラム

今回もタグを用意しています。

github.com

今回はここまで。

へっぽこ知識で作る画像処理プログラム 「自炊本のページ抜けチェック」 3日目

はじめに

前回の2日目では小説の画像に二値化を行い、積分画像を生成しました。
生成したところ、なんだか文字のある領域とない領域が判別出来そうだ、というところまでわかりました。

今回は前回生成した積分画像から文字のある領域とない領域を実際に判別してみようと思います。

文字のない領域の見つけ方

2日目の復習

2日目でもある程度説明しましたが、作成した積分画像からは以下のことがわかります f:id:ysuzuki116:20160605232954p:plain:w250

積分画像の下端(青)の横方向の変化を調べ、値が変化していない領域を調べれば以下のようになり、
f:id:ysuzuki116:20160608213645p:plain:w200
積分画像の右端(赤)の縦方向の変化を調べ、値が変化していない領域を調べれば以下のようになるはずです。
f:id:ysuzuki116:20160608213719p:plain:w200

さらにこの2つの調べた結果を合わせると、
f:id:ysuzuki116:20160608214131p:plain
となって文字のある領域と文字のない領域を判別できるはず!

なんとなく出来そうなのでプログラムを書いてみましょう。 書かなきゃわからないですし、2日目の結果からそれっぽい結果も得られていますし、何とかなるでしょう。

はじめてのピクセル

2日目ではOpenCVに用意されている関数を組み合わせてプログラムを書いていました。 しかし今回は画像を構成しているピクセルを直接触ります。

ポインタを扱うため、アクセスする範囲に注意が必要です。

プログラム自体はとても簡単で、難しいアルゴリズムは使わず、配列の1個前の要素の値と現在の要素の値を比較するだけです。
1個前の要素の値と現在の要素の値を比較し、値が同じなら白(255)、値が異なれば黒(0)とします。 値が同じということは変化のない領域、つまり文字のない領域のはずです。逆に値が異なるということは変化がある、つまり文字のある領域となるはずです。

まずは横方向だけ調べてみる

早速ですがプログラムです。ピクセルにアクセスするプログラムは長く読みにくくなりがちなので関数を定義しています。

//横方向をしらべる
//値が同じ連続領域を255、それ以外を0とするcv::Matを返す。
cv::Mat findSameValueHorizontal(const cv::Mat& src)
{
    //積分画像が欲しい
    CV_Assert(src.type() == CV_32SC1);

    //結果は白黒でいいから8bitの1チャンネル。しかも1行だけ
    cv::Mat dst(1, src.cols, CV_8UC1);
    //積分画像はintなのでint型のポインタを取得。下端なので位置はsrc.rows - 1
    const int* srcLine = src.ptr<int>(src.rows - 1);
    //結果は白黒の8bitなのでそのまま受け取っている。1行しかないので位置は0
    unsigned char* dstLine = dst.ptr(0);

    //とりあえず結果画像の一番左だけは右隣と比較する
    dstLine[0] = (srcLine[0] == srcLine[1]) * 255;
    for (int i = 1; i < src.cols; i++){
        //左隣と比較
        //比較結果(0または1)なので255を掛けて見えるように
        dstLine[i] = (srcLine[i] == srcLine[i-1]) * 255;
    }

    return dst;
}

ポインタの理解が浅いと読むのが大変かもしれませんが、難しい処理はないと思います。
ポインタを取得するメソッドptrは引数に与えた行の先頭を指すポインタを返します。 難しく書いてしまっていますが、要は積分画像の下端の1行分の配列と結果画像の1行分の配列を取ってきている感じです。

そして取ってきたのは配列なのでfor文で値を端から端まで比較し、ひとつ前の値と同じだったら白(255)、違かったら黒(0)に結果画像のピクセルをするだけです。
結果画像は以下になります。わかりやすいように枠をつけています。

f:id:ysuzuki116:20160610233509p:plain

バグではありません。結果画像のdstは1行の画像なのでこうなってしまいます。さすがに分かりにくいですね。 修正したものが以下になります。パワポで引き伸ばしました。

f:id:ysuzuki116:20160610233525p:plain

処理している原画像はというとこれでした。

f:id:ysuzuki116:20160610230658j:plain

非常にそれっぽい!(?)成功してそうです。プログラムを修正して原画像に重ね合わせて表示してみましょう。

先ほど載せたプログラムは画像を作るのではなく、文字のない領域を返すように変更しました。

//横方向をしらべる
//文字のない範囲をvectorで返す
void findSameValueHorizontal(const cv::Mat& src, std::vector<Range>& ranges)
{
    //積分画像が欲しい
    CV_Assert(src.type() == CV_32SC1);

    //値が入っているかもしれないので空にする
    ranges.clear();

    //積分画像はintなのでint型のポインタを取得。下端なので位置はsrc.rows - 1
    const int* srcLine = src.ptr<int>(src.rows - 1);

    Range range;
    for (int i = 1; i < src.cols; i++){
        //左隣と同じ値
        bool sameValue = srcLine[i] == srcLine[i - 1];
        //左隣と同じ値 かつ 範囲のstartが初期値(-1)のとき
        if (sameValue && range.start < 0){
            //文字のない範囲の始まり
            range.start = i - 1;
        }
        //左隣と違う値 かつ 範囲のstartが代入済み
        else if (!sameValue && range.start >= 0){
            
            //文字のない範囲の終わり
            range.end = i - 1;
            //結果として保存
            ranges.push_back(range);
            //文字のない範囲を初期値に戻す
            range.start = -1;
            range.end = -1;
        }
    }
    //最後の範囲が画像の右端まである場合はfor文を抜けてから結果を保存する
    //文字のない範囲のstartは代入済み かつ 範囲のendは初期値のとき
    if (range.start >= 0 && range.end < 0){
        range.end = src.cols - 1;
        ranges.push_back(range);
    }
}

さらに文字のない範囲を使って原画像に色を塗ります。

 //文字のない範囲を受け取る変数
    vector<Range> horizontalRanges;
    findSameValueHorizontal(integral, horizontalRanges);

    //文字のない範囲を書き込む画像
    cv::Mat horizontalRangeDst;
    //1チャンネルの原画像から3チャンネルの画像を作る
    cv::Mat srcArray[] = {image, image, image};
    cv::merge(srcArray, 3, horizontalRangeDst);

    //文字のない範囲を3チャンネルの原画像に書き込む
    for (size_t i = 0; i < horizontalRanges.size(); i++){
        Range& r = horizontalRanges[i];
        //文字のない範囲を3チャンネルの原画像から切り出す
        cv::Rect rect(r.start, 0, r.end - r.start + 1, horizontalRangeDst.rows);
        cv::Mat roi(horizontalRangeDst, rect);
        //切り出した画像を1色で塗りつぶす
        roi = cv::Scalar(240, 176, 0);
    }

結果は。

f:id:ysuzuki116:20160611001819p:plain

出来てるみたいですね!

縦方向も調べてみる

同様に縦方向も調べてみます。

 //文字のない範囲を受け取る変数
    vector<Range> verticalRanges;
    findSameValueVertival(integral, verticalRanges);

    //文字のない範囲を書き込む画像
    cv::Mat verticalRangeDst;
    //1チャンネルの原画像から3チャンネルの画像を作る
    cv::merge(srcArray, 3, verticalRangeDst);

    //文字のない範囲を3チャンネルの原画像に書き込む
    for (size_t i = 0; i < verticalRanges.size(); i++){
        Range& r = verticalRanges[i];
        //文字のない範囲を3チャンネルの原画像から切り出す
        cv::Rect rect(0, r.start, verticalRangeDst.cols, r.end - r.start + 1);
        cv::Mat roi(verticalRangeDst, rect);
        //切り出した画像を1色で塗りつぶす
        roi = cv::Scalar(0, 0, 255);
    }

結果はこちら。

f:id:ysuzuki116:20160615205102p:plain

縦方向も大丈夫そう!

縦横の結果を重ねる

せっかくなので縦方向の結果と横方向の結果を重ね合わせてみましょう。 横方向の結果画像に縦方向の結果を上書きしてしまいましょう。

//横方向の結果と縦方向の結果を合わせる
    for (size_t i = 0; i < verticalRanges.size(); i++){
        Range& r = verticalRanges[i];
        //文字のない範囲を3チャンネルの原画像から切り出す
        cv::Rect rect(0, r.start, horizontalRangeDst.cols, r.end - r.start + 1);
        cv::Mat roi(horizontalRangeDst, rect);
        //切り出した画像を1色で塗りつぶす
        roi = cv::Scalar(0, 0, 255);
    }

結果はこちら。

f:id:ysuzuki116:20160615205803p:plain

色合いが悪くて目が痛くなりそうですが、うまくいってるみたいです。 よしよし。

3日目まとめ

今回は実際に文字のない領域を検出するところまでプログラムを作成することができました。 これが出来ないとページの抜けをチェックするためのページ番号の抜き出しができません。だからこれができることは重要です。 出来なかったら処理方法を見直さなければなりませんからね。

今回の成果物はこちら

github.com

予告

次回は実際にページ番号を切り抜くところまで作ってみようと思います。

今回までのプログラムをそのまま使って作れそうですが、そうもいかない気がしています。 まずはリファクタリングを行い、そのあとに本題のページ番号の切り抜きを行おうと思います。

更新2週間に一回になりそう。。

へっぽこ知識で作る画像処理プログラム 「自炊本のページ抜けチェック」 2.5日目

はじめに

1日目で宣言したとおりPythonでもプログラムを作成しました。 ただC++と同時にプログラムを載せて説明するとややこしいと思ったので*.5日目というかたちで書いてみました。 なお、プログラムはC++と同じリポジトリで公開し、タグもPython版のものを作ります。
今回はPython版の初回なのでC++版とことなる点について説明するために別の記事としましたが、説明が必要なさそうな場合はC++版と一緒にPython版を公開するだけになると思います。

C++版とPython版での大きな違い

C++版とPython版での大きな違いはOpenCVでの画像データの取り扱いです。
C++版ではOpenCV独自のデータ構造であるcv::Matを使って画像データを表現していましたが、 Python版ではcv::Matは使わず、Python数値計算ライブラリであるnumpyの行列ndarrayに画像データを保持します。
そのため、cv::Matに用意されていたメソッドは使えず、numpyに用意されている方法で計算する必要があります。
ただ、cv::Matに用意されているメソッドだけですので、OpenCVに用意されていた関数やその他のクラスなどはその引数などが変更になりますが使うことができます。

例えばC++版で二値化を行っていたcv::threshold関数を比較してみましょう。

C++版のthreshold関数

double threshold(InputArray src, OutputArray dst, double thresh, double maxval, int type)

Python版のthreshold関数

cv2.threshold(src, thresh, maxval, type[, dst]) → retval, dst

大きな違いは処理結果を引数から得るか戻り値から得るかを選べる点です。 どちらを使うかは基本的に好みですが、引数から結果を得る方法をうまく使うことで省メモリなプログラムを書くことができます。
また戻り値はタプルとなっており、retvalには処理が正常に終了したかをboolで返し、dstには処理結果の画像が返ります。

今回は戻り値を使ってプログラムを作成します。理由はシンプルに書けるからです。

Python版にはないメソッドとしてはcv::Matに用意されていたconvertToメソッドがあります。
PythonでconvertToを実現するにはnumpyのndarrayで実装されている四則演算と型変換のメソッドastypeを利用します。

integralVisible = (integral * 255 / max).astype(numpy.uint8)

プログラム自体はとても簡単で0~maxまでの値をとっていた積分画像integralを0~255の値をとるように、maxで割って最大値を1にした後、255を掛けて最大値を255にしているだけです。
式的には一つの値に対して計算しているように見えますが、実際には画像全体い対してこの計算を一気に行っています。

出来上がったプログラム

C++版と同じリポジトリで公開していますが、タグだけ別に用意しています。

GitHub - kyokushin/EBookChecker at python_day_02.5

今回はPython版の説明なのでここまで。

へっぽこ知識で作る画像処理プログラム 「自炊本のページ抜けチェック」 2日目

はじめに

1日目では何を作るか、どういった方針で作るか、ということをまとめて書いたので早速作り始めていこうかと思います。

さっそく考える

まずは余白を検出しよう、と考えた

作りたいのは電子書籍チェックプログラム、チェックするうえで必要なのはページ番号です。
ページ番号は1日目で紹介した 本の各部名称 によるとノンブルと呼ぶらしいです。フランス語らしいのでここではページ番号と呼びます。

電子書籍チェックプログラムではページ番号を切り出し、それをページごとに並べることでページ抜けをチェックしやすくすることを目標としています。なのでページ番号を切り抜く必要があります。

そのページ番号は本を構成するものの中で最も外側にあります。もっとも外側ということは余白に隣接しているということになります。
ですから余白を検出できればページ番号を切り出すのも簡単なのでは?と考えました。

f:id:ysuzuki116:20160601205858p:plain:w300

ですが、ページ番号の場所を正確に特定するには余白だけではダメだということに気が付きました。
ページ番号と本文とのスペースと余白に接していない側のページ番号の端を検出しないといけません。

f:id:ysuzuki116:20160601210943j:plain

結果からいうとこの方法だと無駄に難しくしてしまうのでダメだな、と思いました。
直感でしかないのですが、小手先の技術だけでやろうとしていたので知恵を絞らないと後々辛くなりそうだな、と思ったのです。

余白じゃなくて文字のない領域を検出してしまえばいいんだ

私が作ろうとしている電子書籍チェックプログラムはページ番号さえ切り出せれば簡単なのではと思っていたのですが、 ページ番号を切り出すことだけを考えていたせいで余計に難しく考えてしまっていました。

どうすれば簡単になるかというと、大きく考え、ページに存在する文字のない領域を検出するのです。

f:id:ysuzuki116:20160601213210p:plain:w200 f:id:ysuzuki116:20160601213352p:plain:w200

文字のない領域はどこに文字がいっぱいあって、どこにないのかがわかればよいわけですが、そんな都合のよい方法がありました。
画像処理の基礎の一つである積分画像を使います。

積分画像は顔検出で使われるHaar-like特徴の抽出で使われていたと思います。

積分と書かれると難しく思われるかもしれませんが、デジタルの世界での積分はただの足し算です。
積分画像が何であるか図を使って説明するとたったこれだけ。 f:id:ysuzuki116:20160601221318p:plain

積分画像の何がうれしいのかというと、一度積分画像を作っておくと特定の領域の画素値の合計が 4つの値の足し引きだけで計算できるのです。

f:id:ysuzuki116:20160606224642p:plain

ただ今はこの計算を行わず、積分画像の値をそのまま使います。
さらに使うのは積分画像の右端1列と下端1行の値だけです。 この2箇所は文字のない領域を見つけるのにとても重要な場所で、この2か所を調べるだけで文字のない領域を調べることができるのです!

積分画像の右端1列には画素が属する行までの画素値の総和が入っており、同様に下端1行には画素が属する列までの画素値の総和が入っています。
ゆえにこの2つをそれぞれを調べると画像の縦方向と横方向それぞれの画素値の変化を知ることができます。

f:id:ysuzuki116:20160605232954p:plain:w200

今回は文字のない領域を調べたいわけですが、文字のない領域ではその変化が0になるはずです。 ということは積分画像の右端1列と下端1行を調べ、変化のない領域を見つければ良さそうではありませんか!
f:id:ysuzuki116:20160605235312p:plain:w300

さっそくプログラムを書いてみる

こまごまとした準備

小説をスキャンした画像を用意するのですが。。。

ということでプログラムを書いてみましょう。しかし、画像がないと画像処理できません。しかし処理対象が小説というものなのでそれをこういった場に掲載するというのはそれはそれで問題がありそうです。なので画像については各々探してもらうということで・・・

ここで処理結果を出すときは小説の画像を一部切り抜いたものを利用しようと思います。

環境設定

C++OpenCVとCMakeを使います

1日目にも書きましたが、プログラミング言語にはC++、画像処理ライブラリにはOpenCV、ビルドツールにはCMake、ビルドにはVisual Studioを使います。
OpenCVは言わずと知れた画像処理ライブラリです。バージョンが3系になってから積極的な開発が行われており、強力なライブラリとして成長しています。
CMakeはOSやコンパイラなどの開発環境に依存しないビルドツールです。CMakeでビルドの設定を書くとWindowsであればVisual Studio向けのソリューションファイルを、UnixLinuxであればMakefileを、Macであれば何かを生成してくれ、とても便利です。
これは特にVisual Studioを使っている人にお勧めのものです。Visual Studioでは設定の変更が基本GUIで、同じ設定のソリューションファイルを作ろうとするとGUIでポチポチしなければならず辛い思いをします。
しかしCMakeはテキストファイルですのでそのままコピーし、内容を少し書き換えるだけで同じ設定のものを作ることが可能です。ライブラリのリンクに必要な設定などもVisual StudioGUIで設定するのに比べたらめちゃくちゃ楽です!なのでおススメ。

Pythonでも書くよ、と言いましたが1日分のC++での成果物をまるっとPythonに書き換えたものを一緒に載せようと思います。 C++と書き方が異なり、分かり難そうなところはコメントで解説を入れようかと思います。

OpenCV

OpenCVはこちらで配布されています。

OpenCV download | SourceForge.net

OSにあったopencv-3.1.0.exeやzipをダウンロードしてください。
これを展開して、build/bindディレクトリを環境変数PATHに、buildディレクトリを環境変数CMAKE_PREFIX_PATHに追加してください。
これでOpenCVの環境設定は終了です。

CMake

CMakeはこちらで配布されています。

Download | CMake

OSにあったものをダウンロードしインストールしてください。OSによってはパッケージマネージャーからインストール可能かもしれません。

ソースコードはGitで管理、Githubで共有

ソースコードの管理にはGitを利用します。また作成したGitリポジトリGithubにアップします。URLはこちら。

github.com

私はGitを一人でしか使ったことがないのでコミットがかなり適当(雑)です。。。

画像処理開始

まずは二値化

白と黒でしか構成されていない本でもスキャナーから生成した画像は正確に白と黒ではありません。白っぽい色と黒っぽい色で構成されています。
カラー画像データは基本的に各ピクセルはRGBの3チャンネルで構成され、RGBそれぞれは0~255の値をとります。これは符号なし8bitのunsigned charに相当します。
またグレースケール画像のデータは白の1チャンネルで構成され、同じく0~255の値を取ります。0は黒、255は白になります。

正確に白と黒ではない、というのは白=255、黒=0という値をとらず、0~255の値をとる、ということです。
本来文字のないところでも白(255)にならず、また文字のある所でも黒(0)にならない、ゆえに文字のない領域が正確に求められません。

このままだと都合が悪いので、画像を白(255)と黒(0)の二つの値だけをとるように加工します。この処理は二値化と呼ばれます。名前のままですね。
二値化は画像処理では基礎中の基礎の処理。もちろんOpenCVにも用意されています。
Miscellaneous Image Transformations — OpenCV 2.4.13.0 documentation

double threshold(InputArray src, OutputArray dst, double thresh, double maxval, int type)

第1引数は処理対象の画像、第2引数は二値化の結果です。 第3引数が二値化の閾値、第4引数が割り当てる値、第5引数が二値化時の値の割り当て方の設定です。 今回は第5引数にTHRESH_BINARY_INVを利用し、閾値以下の画素が255、閾値より大きい画素が0になるようにします。見た目的には白黒を反転させたような画像になります。つまり、文字が白に、何もないところが黒になります。 cv::thresholdの第1引数に与えているimageはグレースケール画像で型はunsigned charの1チャンネル(CV_8UC1)です。

cv::Mat binary;
int binaryMax = 255;
int binaryThreshold = 128;
cv::threshold(image, binary, binaryThreshold, binaryMax, cv::THRESH_BINARY_INV);

f:id:ysuzuki116:20160606224853p:plain

なぜ白黒を反転させたのかというと、二値化の後に積分画像を生成し、文字のない部分を調べるときに文字のない領域で積分画像の画素値が変化しないようにするためです。文字のないところが黒(0)になっていれば積分画像では変化がないわけなので都合がいいのです。

積分画像を作る

積分画像の説明は先に説明したので省きます。積分画像も二値化と同様に画像処理の基礎技術なのでOpenCVに実装されています。
Miscellaneous Image Transformations — OpenCV 2.4.13.0 documentation

void integral(InputArray src, OutputArray sum, int sdepth=-1 )

第1引数は原画像、第2引数は処理結果です。第3引数は処理結果の型を指定します。

先ほどのプログラムを少し修正してから積分画像を生成します。二値化時の最大値(binaryMax)を1に変更します。 これは積分画像の生成時に白だったのか黒だったのかがわかればよいので255という値を必要としないからです。

cv::Mat binary;
int binaryMax = 1;//二値化時の最大値は1に。積分するときに白だったところか黒だったところかがわかればいい。
int binaryThreshold = 128;
cv::threshold(image, binary, binaryThreshold, binaryMax, cv::THRESH_BINARY_INV);

cv::Mat integral;
cv::integral(binary, integral);

f:id:ysuzuki116:20160606222526p:plain

お、なんとなくグラデーションになって良さげです。ただし、この画像はプログラムとしては間違っていないのですが、見た目が間違っています。どういうことかというと、ほとんどが白になっていますが、実際には白(255)より大きな値をとっているピクセルもあり、値としてはもっと違いがあるはずなのです。
ではなぜほとんど白になっているかというと一定より大きい値は白(255)として表示する、というOpenCVの仕様です。一定という値はcv::Matの型がunsigned charかintかによって異なります。また画像を保存するメソッドcv::imwriteか画像を表示するメソッドcv::imshowかでも異なるのでよくドキュメントを読みましょう。
Reading and Writing Images and Video — OpenCV 2.4.13.0 documentation
User Interface — OpenCV 2.4.13.0 documentation

このままでも問題は無いのですが、積分画像が実際にどんな値をとっているのか視覚的にわかるよう変換することで、積分画像の中身を確認しましょう。

変換にはcv::Matのメソッドを利用します。
Basic Structures — OpenCV 2.4.13.0 documentation

void Mat::convertTo(OutputArray m, int rtype, double alpha, double beta)

第1引数が処理結果、第2引数は処理結果の型、第3、4引数はそれぞれ値のスケールとシフトする量となります。 このメソッドは白(255)より大きな値を視覚的にわかるようにするために、値の大小関係を崩さずに0~255の値をとるようにするために使います。
またこのメソッドを使うには最大値が必要です。これもよく使うものなのでOpenCVに用意されています。
Operations on Arrays — OpenCV 2.4.13.0 documentation

void minMaxLoc(InputArray src, double* minVal, double* maxVal, Point* minLoc, Point* maxLoc, InputArray mask)

第1引数は原画像、第2引数は最小値を格納する変数のポインタ。第3引数は第2引数と同様に最大値を格納する変数のポインタ。第4、第5引数は最大最小値の値の場所を格納する変数のポインタ、第6引数は第1引数に与える原画像と同じサイズ(同じ行数、列数)の行列で0以外の値の座標に対応する原画像のピクセルを利用します。これはマスク画像と呼ばれます。
今回は最大値を知りたいので第3引数まで使います。最小値は0とわかっていますが、本当に0になっているのか確認するために使いましょう。

cv::Mat integralVisible;
double max;
double min;
cv::minMaxLoc(integral, &min, &max);//最大値だけほしいので第3引数まで。最小値は0のはずだが本当に0か確認するために使う
CV_Assert(min == 0.0);//本当に最小値が0になっているか確認

integral.convertTo(integralVisible, CV_8UC1, 255.0 / max); //betaは使わない。0-255の値をとるようにalphaを与える。

CV_AssertはOpenCVで定義されているマクロで、引数に与えた条件文が成立していない場合、プログラムが終了します。Assertは日本語で「断言する」なので、引数に与えた条件が成り立つとプログラムで断言しているわけです。

f:id:ysuzuki116:20160606222537p:plain

先ほどの積分画像よりも黒の部分が多くなりました。この画像だと先ほどは白かった部分にも実は変化があることがわかります。
画像処理はとにかくデータが多いので値を一つ一つ確認するには無理があります。なので、ちょっと面倒ではありますが、今回のように変換して視覚的にわかりやすくすることで確認しやすくすることはとても重要です。

処理内容から脱線するのですが、この画像の下の方の色が白→グレーを繰り返しているように見えると思いますが、実際には左から右まで黒(0)→白(255)に増加しながら変化しています。詳しくはマッハバンドで検索してみてください。

さて、もう少しこの画像を詳しく見てみましょう。色の変化が帯状に分かれていることがわかるかと思います。

f:id:ysuzuki116:20160606222554p:plain

縦線を引いてみました。位置はともかくとして文字の並びに沿って変化してる感じがしますね。この後の処理を工夫すれば文字のある領域とない領域がわかりそうです!
やった。なんとか目的のものは作れそう。

2日目まとめ

今回は文字のある領域(もしくはない領域)を判別する処理について考え、実際に画像処理をするところまで書きました。
画像処理はOpenCVの関数を使ったとても簡単なものですが、二値化と積分画像を使うことで文字のある領域(もしくはない領域)が見つけられそうだな、というところまで作成しました。

プログラムで主に役割を果たしているのは二値化と積分画像を行う2つの関数だけですが、組み合わせ次第で簡単に画像について調べることができました。 実際にプログラムを書いて動かすまで、考えていた処理で思っていた通りのものが得られるのかわからないのでこうやって”それっぽい”画像が得られたことは大きな一歩です。

成果物はこちら

GitHub - kyokushin/EBookChecker at c++_day_02

予告

次回は作成した積分画像から実際に文字のある領域と文字のない領域を判別してみます。
さて、毎週更新できるかな。。。

へっぽこ知識で作る画像処理プログラム 「自炊本のページ抜けチェック」 1日目

はじめに

ちょっとやりたいことがあるのですが、ただ単にプログラム書いて公開、というのも味気ないし、最後まで作りきらない気がしたので日記のように覚え書きをしながらプログラムを書いていこうと思い、「へっぽこ知識で作る画像処理プログラム」というタイトルで週1程度で更新していこうかと思います。

最終目標!

”はじめに”で書いたちょっとやりたいこと、というのが最終目標です。
そのちょっとやりたいこと、というのは画像処理の基礎知識で自炊した電子書籍のチェックプログラムを作ることです。

なぜ電子書籍チェックプログラム?

部屋の本を自炊したはいいけど・・・

自炊というのは1冊の紙の本をスキャナーで取り込んで1冊の電子書籍にすることです。
私は東京で一人暮らしなのですが、6畳しかない部屋で紙の本というのは思いのほか場所を取ります。
そこで「自炊」です。自炊をすることで紙の本は電子化され場所をとらなくなります!

ただし自炊というのは一つリスクがあります。
それは自炊した本のすべてのページが正しくスキャンされるとは限らない、ということです。

自炊した後の本は捨ててしまうので、捨てた後に自炊した本を読んでいたらページが抜けていた!なんてことがあるともう一度買いなおす、なんていう残念なことになりかねません。

自炊に適したスキャナーにドキュメントスキャナーという機器がありますが、これも完璧ではなくミスはあります。
数冊であれば目視で確認すればよいですが、数十冊となると話は別です。

これが今回電子書籍のチェックプログラムを作ろうと思ったきっかけになります。

もう一つの理由

それからこのプログラムを作ろうと思ってからしばらく、ない知識を絞って考えたところ画像処理の基礎知識だけで作れそうだなという結論に至ったことがもう一つの理由です。

さっそく作るプログラムについて考える

まずは対象とする本の種類・・・小説

万能なプログラムを作るのはめちゃくちゃ難しいので頭の良い人達に任せるとして、処理対象とする本の種類を一つにして簡単なプログラムを書くことにします。

そこで対象とする本は文字だけの本、特に小説を対象にしようと思います。もちろんラノベも含みます。
最終的には技術書も対象としたいのですが、とりあえずは小説に限定します。

小説にした理由は基本的にスキャンしたページの色が2色しかなく単純であること、ページを構成するものの場所が決まっていること、が主な理由です。

ページを構成するものというのはページ番号や本文などのことです。私は専門外で本の各部位の正式名を知りません。 ちょっと調べたら丁寧に解説されているページがあったのでメモとして載せさせていただきます。

本の各部名称

チェックプログラムで何をチェックする?

チェックするのは自炊した本のページ抜けの有無です。

で す が、「抜けあるよ!」「抜けないよ!」とプログラムを実行するだけで教えてくれるプログラムを作るのはほぼ不可能です。
なぜかというとページ番号と実際のページ数が対応していない場合がほとんどなので、これなら抜けがない、という「正解」の条件を決めることができないからです。

もし実行するだけで抜けの有無を教えてくれるプログラムを作ろうとすると、書かれている内容を理解できるプログラムを作らなければならないと思うのでとても大変です。頭の良い人達に任せましょう。

では作ろうとしているプログラムで何をするのかというと、「ページ番号の切り抜き」です。
ページ番号を切り抜いて一覧にして、”ページ抜けのチェックをしやすく”することを目的とします。

そんなわけで「チェックプログラム」と書きましたが、実際には「チェックのサポートプログラム」になります。
せっかくここまで読んでくれたのに思ってたものと違った人、ごめんなさい。。。

開発環境や使用言語、ライブラリ

今のところ想定しているものを並べてみます。

メインはWindowsで開発します。つまりVisual Studioを使います。使うバージョンはおそらく2015のComunityEdition。
ただし、後述しますがCMakeを使うので2013などOpenCVが対応しているバージョンであれば問題ないようにしたいと思います。
LinuxWindowsで開発したC++コンパイルの確認で使う予定です。C++Visual Studioだけで開発しているとひどい目に合うので・・・

  • 使用する言語

メインはC++で開発しようと思っています。
が、辛くならない程度にPythonでも並行してプログラムしてみようかと思います。
CMakeはビルドツールで、開発環境に合わせてファイルを生成してくれます。UnixLinuxだとMakefileWindowsだとソリューションファイルを作ってくれます。Macは・・・リンゴアレルギーなのでわかりません。

  • 使用するライブラリ

OpenCVは言わずと知れた画像処理プログラムです。フル活用します。
Qt(PythonではPySide)はUnixWindowsなどOSを気にせず使えるGUI作成のライブラリです。OpenCVにも簡易のGUIが用意されているのでそれで済ましてしまうかもしれません。

今回は予告、ということで・・・

ここまで書いたら疲れてしまいました。けど今回作ろうとしているものの目標や前提条件などをしっかり説明できたと思うので良しとします。
これから地道に更新していけたらいいなー