5.5 [C#]本のページ綴じ部分が歪んだデジカメ画像を補正したい


Home -> 雑用 -> 雑用メモ -> [5.5 [C#]本のページ綴じ部分が歪んだデジカメ画像を補正したい]

2015/04/02 公開
2015/04/09 追記
書きっぱなしの文章なので大変読み難い代物となっている。それでも良ければどうぞ。

見出し一覧

基本的に時系列順なので副題が前後しているが容赦していただきたい。

  1. 序: 画像の自由変形
  2. 前準備1: 画像の読込・表示・倍率変更
  3. 前準備2: 曲線の描画
  4. 画像の変形 [その1]
  5. グリッドの生成[1] - 格子点を求める [その1]
  6. 補正後の画像サイズ推定
  7. 画像の変形 [その2]
  8. グリッドの生成[2] - 格子点を求める [その2]
  9. グリッドの生成[3] - 格子点を求める [その3]
  10. MatからBitmapへの変換
  11. グリッドの生成[4] - 輪郭線からグリッドを自動調整
  12. 適応的閾値処理
  13. やり残し
  14. 最後に
  15. 付録
  16. 2015/04/09 追記分

序: 画像の自由変形

C#の話の前に前書きを。

デジカメで本の見開きページを撮影するとページ綴じ部分が大きく歪む。これをどうにかして補正したいと気まぐれで思った。
例えばこんな画像。

歪んだ画像の例 画像00: 歪んだ画像の例(https://infotomb.com/eg3rp.jpgより)

画像自体はくっきりしているものの、ページの輪郭線が画面からはみ出ているのでソフトウェアによる自動補正も難しい状態である。
ページ綴じ部分でも明度やコントラストは一定のままだが歪みは残ってしまっている。
何らかの都合があって歪みが出ないように撮影することができない場合は撮影後に補正するしかない。
またページ綴じ部分は暗く写りがちなので、明度・コントラスト・彩度もページ綴じ部分に近づくに従って段階的に補正が掛けられるようにしたい。

まずは変形だけでもどうにかしたい。 画像編集用ソフトといえばPhotoshopが有名。しかし残念ながら持ち合わせていないので普段はGIMPで間に合わせている。
まずは手持ちのGIMPで修正を試みた。

GIMPでどうにかできないか

無償で入手可能なのが強みのGIMPだが少し頑張ればPhotoshop並の編集も不可能ではないらしい。
そんなGIMPには画像を変形させるための機能がいくつか用意されている。
ツールボックスに最初からある「回転」「拡大・縮小」「剪断変形」「遠近法」「鏡像反転」「ケージ変形」や、
「フィルター」→「変形」にある「カーブに沿って曲げる」「レンズ補正」「レンズ効果」などがそれである。

これらのうち「遠近法」を用いることで画像に遠近感やパースを付けて変形したり、またその逆の変形をすることができる。
言葉ではよく分からないがつまり下の画像ような感じになる。4隅を指定すると遠近に応じた拡大・縮小も含めて補正してくれる。

遠近法逆変換 画像01: 遠近法逆変換

上の画像はは遠近法逆変換を行った例である。予めレンズ補正を行うことでよりよい結果を得ることができる。
しかし、遠近法変形では4隅しか指定できないので輪郭が大きく歪んでいる場合には上手く補正できない
GIMPの場合、出力画像のサイズや縦横比が元画像と同じになってしまうのも気に食わない。

「ケージ変形」は画像を自由変形させるための機能ということになっているが、自由と銘打っている割に自由ではなかったりする。
例えば下のような変形作業の様子を見てもらえば分かる通り、ケージを変形してもケージ内の画像はそのまま正直にケージを
追従してくれる訳ではないので、ケージ内の図を目的通りの形にするにはケージの形状を試行錯誤する必要がある

ケージ変形 画像02: ケージ変形

上の画像で左側の顔写真を囲っている線がケージである。これを長方形に整形するためには右側のようにケージを変形する必要がある。
見ての通りケージは長方形ではなくかなり歪んだ形状に調整しなければならない。何と融通が利かないことだろう。
このズレを設定を弄ってどうにか解消できないか一応調べてみたが皆こうなってしまうらしい。

そもそも今回行いたいのは「自由選択した領域→自由選択した別の領域」の変形ではなく、「自由選択した領域→長方形」なので
選択部分の出力が長方形になるように手間暇かけてケージを調整するのは実に馬鹿馬鹿しい。
出力画像の形状は決まっているのだから、輪郭線と4隅の位置を指定したら後は全て自動で補正してくれるのが理想的である。

それでは「カーブに沿って曲げる」はどうか。この変形では入力画像の4辺が出力画像でどのような形になるのか指定できる。
ちゃんと変形できそうな気がしないでもないが、カーブの調整がかなり面倒で綺麗な画像を得るのは難しい
できることなら、出力画像の4辺が入力画像でどの位置にあるのか指定するという今回とは逆のマッピングが好ましい。

カーブに沿って曲げる 画像03: カーブに沿って曲げる

もしかしたら他にもプラグインで丁度いいものがあるかもしれないと思ったが見つからなかった。
という訳でGIMP路線は諦めた。1枚1枚の補正に膨大な時間を費やす余裕と覚悟がない限りは無理だと悟った。

他のソフトでどうにかならないか

という訳で、これらの要件を部分的に満たすかもしれない別のソフトを探してみたところ、こんなのが出てきた。

  1. Adobe Photoshop * 画像、シェイプ、パスのワープ
    https://helpx.adobe.com/jp/photoshop/using/warp-images-shapes-paths.html
  2. Adobe Illustrator * エンベロープを使用した変形
    https://helpx.adobe.com/jp/illustrator/using/reshape-using-envelopes.html
  3. Grid Warp - Plugins - Publishing ONLY! - Paint.NET Forum
    http://forums.getpaint.net/index.php?showtopic=25327
  4. TransView(画像自由変形、歪み補正)の詳細情報 : Vector ソフトを探す!
    http://www.vector.co.jp/soft/winnt/art/se479932.html

[1]は変形後の4隅を指定して輪郭線は3次ベジェ曲線で調整するもの、[2]と[3]はグリッドを自由に動かすと画像がそれに追従するもの。
タダでやろうと思ったら[3]あたりが良さそうだが、出力画像の形状が決まっていて寧ろ変形前のグリッドを指定したい今回の場合は
グリッドの調整に手間取ることが容易に想像できる。ケージ変形より簡単にはなるが、まだ正確性に欠けそうである。
[4]は画像に写り込んでいる物体のグリッドを指定してそれを元に変形するもの。今回やりたい変形は正にこれなのだが、
たったこれだけのソフトが何と6,480円(税込)もする(2015年3月現在)。私感ではあるがボッタクリもいいところである。

有料ソフトばっかだったので多分探し方が悪いんだろうと思い、少し発想を変えて調べてみた。
スキャナなどの付属ソフトの中にはスキャンした本の歪みを補正する機能が備わっているものもある。
自炊用非破壊スキャナーなんかには必ずついている(はず)。それらをどうにかしてタダで使えないかと調べてみたところ以下のようなものが。

  1. A4フラットベッドスキャナー DS-5500 特長|製品情報|エプソン
    http://www.epson.jp/products/scanner/ds5500/tokucho_4.htm
  2. 本のゆがみを補正する
    http://www.pfu.fujitsu.com/imaging/downloads/manual/advanced/v62/jp/common/retouch_book_outlines.html
  3. iTunes の App Store で配信中の iPhone、iPod touch、iPad 用 Faster Scan HD - Scanner to Scan PDF, Print, Fax, Email, and Upload to Cloud Storages
    https://itunes.apple.com/jp/app/faster-scan-hd-scanner-to/id533616438?mt=8
  4. 冊子でも綴じたままPDF化! iPadのカメラをスキャナ化する:Faster Scan HD+ – PDF Scanner | iPhonePLUS
    http://iphone.ascii.jp/2012/07/25/faster-scan-hd-pdf-scanner/

[1]はページ綴じ部分の歪みと明度・コントラストを補正してくれるらしいが、全体的な歪みの補正機能は無いっぽい。
[2]はいい感じに補正してくれるが、本の輪郭が検出できないと補正してくれないらしい。自炊スキャン時には背景が必ず常に同じ色に
なる(普通は本の地色とのコントラストが大きい黒色になっている)ので、きちんとスキャンしていれば輪郭の検出は容易である。
しかしデジカメで適当に撮った画像はそうもいかない。背景は適当だし輪郭も画像からはみ出ているかもしれない。
そもそもスキャナから取り込んだ画像しか処理できないとかそんな感じに見受けられるので迂闊にインストールしたくない。
[3]はiPadなどのカメラで撮影した画像をその場で補正するもの。補正は割といい感じにしてくれるが、少々大雑把な感じを受ける。
縦の輪郭線は直線と打ち決めになっているし、補正も適当な短冊に区切ってそれぞれに射影変換ないしアフィン変換を施している模様。
下の通り補正結果の波打ち具合を見れば良く分かる。短冊が割と大きいので歪みが残ってしまう。補正画像は[4]のページにあったもの。

Faster Scan HD+による補正結果の例 画像04: Faster Scan HD+による補正結果の例

以上のような経緯から、既存のソフトウェアを吟味しながらいろいろ試すよりも目的に合った補正方法を自分で実装したほうが
早そうだと考え、C#の勉強も兼ねてそういうツールを自分で作ることにした。

前準備1: 画像の読込・表示・倍率変更

例によってVisual Studioを使うので、pictureBoxコントロールのPaintイベントをハンドリングして画像を描画する。
Bitmapで読み込んだものをDrawImageすればよい。幅と高さを指定することで倍率も設定できる。
今回はグリッドの編集用に余白も用意して表示する。コントロールの幅と高さを画像より大きく(2倍程度)取り、画像は中心に表示する。
画像が表示領域より大きい場合はスクロール表示できるようにする。

大きい画像だと沢山スクロールしなければいけなくなるので、表示倍率は後から変更できるようにしておく。
表示倍率が変更されたらInvalidate()Paintイベントを発生させて指定した倍率の画像を再描画させればよい。
またスクロールはスクロールバーだけでなくAdobe Readerなどの「手のひらツール」風に直感的にできたら編集も効率的にできる。

手のひらツールを実装すると画像を僅かにずらす度にPaintイベントが発生するので、表示したい画像は予めBitmapに読み込んで
内部で保持するようにする。Paintイベントの度に画像をファイルから再読み込みしていると少々派手にスクロールしただけで
メモリ不足になってしまうので、この対策は必須である。

前準備2: 曲線の描画

Paintイベントハンドラ内でe.Graphics.DrawCurve()することでとりあえず曲線を描画することができる。
しかし曲線上の任意の座標を取得することができないので、今回は曲線が通る座標を自分で求めてそれを元に描画する。
曲線が通る座標の間隔が「実用上十分に」短ければ、e.Graphics.DrawLines()で事実上の曲線が描ける。
曲線上の任意の座標が必要なのは、滑らかなグリッドを生成するためである。これを怠ると[画像04]のように格子内の歪みが残ってしまう。

e.Graphics.DrawCurve()で描画されるのはスプライン曲線である。任意個の制御点を全て通る滑らかな曲線が描かれる。
これを自分で実装すればよい。以下はActionScriptのサンプルである。

今回は上のページにあったSplineStream関数をC#に移植して使う。ActionScriptの方では返り値がオブジェクト型の配列
だが、移植時には返り値をPointF[]にしておくとe.Graphics.DrawLines()にそのまま使えたりと便利である。

後で調べたところ、移植したSplineStream関数で得られる曲線はe.Graphics.DrawCurve()で描かれる曲線と一般的には一致しないようだ。
テンションをどう弄っても一致しなかったのでアルゴリズムが微妙に違うようだが、今回は深入りしないことにする。
とりあえずどちらにせよ滑らかな曲線が描けるようになったのでそれで良しとする。
上のページには他にn次ベジェ曲線のサンプルなどもあるので時間があれば3次ベジェ曲線で実装してもよさそう。

画像の変形 [その1]

ここからが本題。時系列順に記すので副題がいろいろと前後しているけれどもお気になさらず。
まずC#というか.NETで画像の変形はどのように実装できるのか。

画像編集用の有名なC/C++ライブラリとしてOpenCVが挙げられる。実はOpenCVには.NET用ラッパーが複数存在するので
Visual Studioから使うこともできる。例えば以下のようなものがあるので好きなものを使えばよい。
でもできればNuGet経由でポンポン導入できるやつがいいよね。ライセンスもユルユルのやつの方が嬉しかったりするよね。

  1. opencvdotnet - .NET Framework Wrapper for Intel's OpenCV Package - Google Project Hosting
    https://code.google.com/p/opencvdotnet/
  2. SharperCV Project
    http://www.cs.ru.ac.za/research/groups/SharperCV/
  3. Emgu CV: OpenCV in .NET (C#, VB, C++ and more)
    http://www.emgu.com/wiki/index.php/Main_Page
  4. shimat/opencvsharp · GitHub
    https://github.com/shimat/opencvsharp

という訳で[3]Emgu CVか[4]OpenCvSharpのどっちかを使おうかなーという感じになった。
まあどっちでもいいけどライセンスめんどいしOpenCvSharpでいっか、ということでここではOpenCvSharpを使う。
どうせEmgu CVでも同じことができるはずなので困ったらそっちに移植すればいいし今はラッパーには拘らないことにする。

それで何でOpenCVを使わにゃあならんのかというと、こいつに付いているcv::remap関数を使いたいから。この関数は
出力画像の各ピクセルの値を元画像のどこからマッピングするか自由に指定できる汎用的な幾何学変換を行うもの。
しかもサンプリングする点は特定のピクセルだけでなく浮動小数点で微妙な位置を指定することもできる。更に更に、その時は
適当なピクセルを内挿することになるのだけれど、その時の補間アルゴリズムも指定できる。まさに神。
詳しくは以下をどうぞ。

小難しい話はアレかと思って上の参考ページに載っている数式をもっと簡単に表現できないかと考えたりもしたが
結局は数学的表現が一番簡単だったので以下に自己流解釈の概略を示す。

入力画像をsrc、出力画像をdstと表記し、画像上の任意の位置r(x,y)のピクセル値をsrc(r)dst(r)のように表記できることにする。
今回行いたいのは、出力画像上の全てのr(x,y)についてdst(r)src(r_prime)で表すこと、すなわち全てのr(x,y)に対応する
r_prime(x_prime,y_prime)を求めるr→r_primeの変換を行うことである。てか下の画像見てもらった方が早い気がする。

rとr_primeの対応関係 画像05: r(x,y)r_prime(x_prime,y_prime)の対応関係

ただし、OpenCvに渡すことができるのは、全てのr(x,y)に対応したr→r_primeを表すオブジェクトr_prime(x_prime,y_prime)
2次元配列ではない。代わりに、r→x_primer→y_primeを表す2つの2次元配列それぞれから独自に作成した2つのマップを渡す。
上の公式ページではr→x_primer→y_primeの変換を行う関数をそれぞれf_x (x,y)f_y (x,y)と表している。

上の画像ではグリッド境界線が入力画像からはみ出しているが、この辺はOpenCvが適当に処理してくれる。
存在しないピクセルの外挿方法はいくつか提供されていて、デフォルトでは全て0とするようになっている。
マップに負の座標とか突っ込んだらエラー吐きそうなもんだけどこういう所は融通が利いている。さすがOpenCv。神。

グリッドの生成[1] - 格子点を求める [その1]

画像から任意の範囲を選択し、その輪郭線というか周上の基準点を元にグリッドを生成したい。
グリッドを描くだけなら輪郭線の形状からグリッドの形状を適当に推定して描けばよいが、これだと縦横のグリッドが交差する点、
つまり格子点を求めるために曲線と曲線の交点を求めなければならないので計算量も多くなるし面倒臭そう。

できることなら最初から格子点を求めてそれを元に曲線を描画したい。実際に画像を変形するときには基準点の数を出力画像の
解像度に見合った数だけ生成し、それを元に求めた各格子点により出力画像1px分の色情報を入力画像からサンプリングする。
つまり格子点の座標を適当に求めることさえできれば、周上の基準点を密にすることで無限に滑らかな画像変形が可能となる。

ではどうやって格子点を求めるか。とりあえず格子点を基準点の座標から求めるための公式めいたものを作ってみよう。
基準点を動かしたらそれにつられて格子点がもっともらしく移動するようにしなければならないし、基準点が規則正しく等間隔に
並んで輪郭線が長方形になっているとき(初期状態時)は格子点も規則正しく配列して直行するグリッドを構成しなければならない。

そこでまず最初に考えた方法が「内分の公式」の改変バージョンである。
これなら、グリッドが初期状態の時にも条件を満たすことが自明ともいえる。これがどういうものか、一応説明しておく。

以下のようにグリッドを割り振ったとする。図では綺麗なグリッドとなっているが一般的には歪んだグリッドについて考える。
便宜のため、その上端・下端とx方向i番目のグリッドとの交点の位置をそれぞれA_iB_iとする。
同様にして、左端・右端とy方向j番目のグリッドとの交点の位置をそれぞれC_jD_jとする。
これらA_iB_iC_jD_jに対応する点のことをここではグリッドの制御点と呼ぶことにする。
また、x方向、y方向それぞれのグリッドの最大インデックスをそれぞれmnとする。
x方向i番目のグリッドとy方向j番目のグリッドの交点となる格子点の位置をR_ijとする。

グリッドと格子点の割り当て 画像06: グリッドと格子点の割り当て

ここで、A_iB_iC_jD_jが全てのijについて既知のとき、これらを用いてR_ijを表すことを考え、以下の関係(式1)
eq
を仮定する。この式は少なくともグリッドが長方形で周上の格子点が各辺で等間隔に配置されているときには全てのijについて
成立するが、これが歪んだグリッドの格子点についてもおおよそ成り立つのではないかという予測のもとでこの式を用いる。
実際にはグリッドのゆがみが大きくなると誤差が無視できない程に大きくなってしまうが、その話は後のセクションで。
グリッド幅を十分に小さくしてR_ijを細かくサンプリングすれば、それをそのまま出力画像のドナー点として用いることができる。

ここまでの仮定を元にグリッドを描画してみたところ以下の画像のようになった。

グリッドの描画結果(その1) 画像07: グリッドの描画結果(その1)

ユーザは赤および水色の点を自由に移動できる。緑色の点の座標は(式1)を元に算出されたものである。
上端・下端周辺のグリッドの"間延び"が補正できれば一番いいが、その話は後のセクションで。

補正後の画像サイズ推定

出力画像の縦横比が決まっている場合について、その画像サイズをどのように設定すればよい結果が得られるだろうか。
極端に小さいと情報を削ってしまうことになるし、また大きすぎると不要な情報をたくさん詰め込むことになる。
これを考慮して以下のように考えた。

グリッド線をなす曲線はそれを構成する座標が既知なので、曲線の全長を求めることができる。
まずはグリッドを十分に細かく取り、横方向に走っているグリッド線の長さを全て求める。
そしてそれらのうち最も長いものを出力画像の仮の横幅とする。仮の縦幅も同様に計算する。
縦横比が一致しない場合には、幅が小さくならないようにどちらか一方を大きくして目的の縦横比にする。

実際にはグリッドの割り振りを考慮して適当な整数値に丸める必要がある。その辺は適当に。
実は[画像07]に補正画像の出力サイズ計算がチラッと載ってしまっているが、大体こんな感じでもっともらしく見える計算をしてくれる。

画像の変形 [その2]

いよいよ画像の出力である。cv::remapを用いた変形はOpenCvSharpではどのようにして記述できるか。
参考になりそうな使用例がないか調べてみた。

  1. ステレオカメラ作成の道
    http://www.slideshare.net/ytanno/ss-21302350
  2. OpenCVのundistort(レンズ歪み補正)で端っこが欠けてしまうのをなんとかする - Qiita
    http://qiita.com/jellied_unagi/items/36796d48d7d8a5fb3e42
  3. OpenCvSharpをつかう その17(NuGetで導入) - schima.hatenablog.com
    http://schima.hatenablog.com/entry/2013/12/15/110513
  4. OpenCvSharpをつかう その11(画像のサイズ変更) - schima.hatenablog.com
    http://schima.hatenablog.com/entry/20091031/1256975329
  5. OpenCvSharpをつかう その21(C++ API 概要) - schima.hatenablog.com
    http://schima.hatenablog.com/entry/2014/03/29/140106

という訳でまずはnew IplImage()で画像を読み込み、Cv.CreateMat()で適当にCvMatを作成、出力用のIplImageも用意しておいてCv.Remap
それらを突っ込む。これでうまくいくかと思ったがエラーが出て上手くいかない。dll内部でエラーが上手くハンドリングされて
いないらしくMessageが空なので何をどう書き換えればいいのやら全く分からない。

そこで少々調べてみたら[5]のページを発見。こっち使えばいいかなーと思ってとりあえずやってみる。画像はnew Mat()で読み込む。
マップもnew Mat()で作成する。この時は2次元配列を突っ込めばおk。MatType.CV_32FC1の指定も忘れずに。
あとは全部Cv2.Remap()に突っ込むだけ。Cv2のときプレビューウィンドウ周りがどうやったらいいかイマイチ不明だったので
Mat.ToIplImage()したものをCvWindowに突っ込んでおいた。

こんな感じで適当に書いたら以下のような出力が得られた。

補正画像の出力結果(その1) 画像08: 補正画像の出力結果(その1)

ちょっとグニャッとなっているが、まあ許容範囲内といえばそう感じられないこともないような微妙な結果に。
頑張ってグリッドを調整してやればいい感じの出力が得られるかと思って頑張ってみたが、やっぱりある程度歪んでしまう。
よりよい結果を得るために、先ほど放置したグリッド上端・下端周辺の"間延び"を補正できないか試すことにした。

グリッドの生成[2] - 格子点を求める [その2]

グリッドが間延びするのは、下の図で例えば上端において本来A_iと一致するはずのR_i0が実際にはずれた位置に存在することに起因する。
グリッドの描画にはR_i0ではなくA_iを用いていたため間延びを引き起こしていた。

(式1)で得られるグリッドともっともらしいグリッドのズレ 画像09: (式1)で得られるグリッドともっともらしいグリッドのズレ

そこで、まず周上の全てのR_i0R_inR_j0R_jmについて本来の位置とのズレを計算し、補正ベクトルdA_idB_idC_jdD_j
以下のように定義する(式2)。
eq2
これらを用いて一般的なR_ijを補正することを考える。上式より、(式3)
eq3
なので、これらと同様にR_ijを補正して得られる新たな点をR_ij^*、そのとき用いた補正ベクトルをdR_ijと表すことにする。
dR_ijにはdA_idB_idC_jdD_jが一定の割合で影響を及ぼしているもととして、これらの一次結合のみによってdR_ijが表されるものと仮定する
影響力の大きさを"内分の公式"を応用して割り当てることで以下の関係が得られる(式4)。
eq4
この関係は、以下の関係(式5)
eq5
を満たすので、少なくとも周上とその周辺に関してはもっともらしいグリッドが得られることが保証されている。

R_ij^*を元にしてグリッドを描画すると、以下のようにもっともらしい格子点が得られた。

(式4)で得られるグリッド 画像10: (式4)で得られるグリッド

また、これを元に出力された画像は以下。[画像08]と比べてよい結果が得られたことがわかる。

[画像10]のグリッドによる出力画像 画像11: [画像10]のグリッドによる出力画像

しかしこのままでは、グリッドを計算するためにまず周上の補正ベクトルをすべて求めて、内部の格子点を求める際にそれらの割合を
計算するので、少なくとも2つのループが必要になってしまう。この計算過程はもっと簡略化できないだろうか。ということでその3へ続く。

グリッドの生成[3] - 格子点を求める [その3]

計算量を減らすために式を変形したい。今までに出てきた式から、以下の関係が導出される(式6.1)。
eq6.1
同様にして、以下の関係も導出できる(式6.2~6.4)。
eq6.2
また、以下の関係(式7)
eq7
より、(式6.1~6.4)は以下のように表せる(式8.1~8.4)。
eq8
すなわち、dA_idB_idC_jdD_jは制御点A_iB_iC_jD_jおよび4隅A_0A_mB_0B_mの位置のみに依存する。

ここで、以下の(式9)
eq9
に(式8.1~8.4)を代入することを考える。以下の関係(式10.1~10.4)
eq10
を(式9)に代入すると下のように変形することができる(式11)。
eq11
R_ijA_iB_iC_jD_jのみに依存するので、R_ij^*A_iB_iC_jD_jA_0A_mB_0B_mのみによって表すことができる。
これら各点は現状ではユーザの操作によって指定することになっているので全て既知であり、よってR_ij^*は直接計算が可能である。
(式2)・(式4)を直接用いた場合に比べてコードを随分簡略化することができるため、普通はこちらの式を用いるのがよい。
ただし当然ながらこの式を用いても出力結果は[画像11]と同じである。
また、この式は(式2)・(式4)を直接用いるのに比べてあまり直感的ではないのでコメントで注釈を付け加えておいたほうが良い。

MatからBitmapへの変換

ここまではもっともらしい格子点の座標を求めることに重点を置いてきたので出力はプレビューウィンドウで妥協していたが、
実用においてはBitmapか何かに読み込んでjpgなり何なりのファイルに出力したい。
しかし内部ではMatとして保持しているので、これをBitmapに変換しなければならない。じゃあそれをどうやるか。

インテリセンスでCv2にそれらしいメソッドが存在しないか一通り眺めてみたが見つからなかったので他を調べてみたら
公式GitHubにサンプルがあった。親切すぎる。

using OpenCvSharp.Extensions;って書いておくとBitmapConverter.ToBitmap(Mat)Bitmapが出てくる。後は煮るなり焼くなり。
ここでは別のパネルに出力画像を表示することにする。

グリッドの生成[4] - 輪郭線からグリッドを自動調整

自動調整なしだとどうなるか

ここまでで、"4隅と制御点の全てが適切に設定されたときについては"よい出力が得られるようになった。
それでは、4隅と制御点の全てを適切に設定することは容易だろうか。これを確かめるために下のようなことを試みた。

グリッドを等間隔に設定した場合 画像12: グリッドを等間隔に設定した場合

ここでは[画像01]で用いたものと同じ画像にグリッドを設定している。
[画像01]とは異なり、遠近を考慮せずグリッド間隔を完全に同じになるように設定している。この出力は下のようになる。

パースが考慮されていない出力 画像13: パースが考慮されていない出力

見ての通り、画像左右方向の歪みを生じ、左側ほど間延びし、右側ほど圧縮された格好になっている。
看板左端と右端の余白の幅や看板背景のタイルに着目すると解かりやすい。
[画像01]のような出力を得ようと思ったらパースを考慮してグリッド間隔を細かく調整しなければならない。
この例においてただ[画像01]のような出力を得たいだけの場合には最初から射影変換を用いればいいが、射影変換は4頂点しか指定できない。

輪郭が与えられた時にグリッドを自動で決定するアルゴリズムなんてのは恐らくこの世には既に存在するとは思う。
しかし見つからないものは仕方がないので自分で考えてそれっぽく作るしかない。

グリッド自動調整の仮実装 [その1]

数学的にどうとかそういうのは置いといて、とりあえずやってみるだけやってみた。
まず横方向なら横方向でグリッド線の長さを全部出して、その比に従ってグリッドを縦方向に再配置したらいい感じになるんじゃね?と。
ただし、[画像01]でわかるように縦幅が半分になる遠さだと横幅は半分以下になるように再配置しなければならない。
そこで、素の長さではなく長さの累乗を用いて比の計算を行うことにした。
このときの指数を仮に「グリッド長比較次数」と名付けておく。この値がいくらになるかはやってみないとわからない。
付け焼刃でしかないことを承知でやってみた結果が以下。ここでの比較次数は2.0を用いている。つまり長さ二乗で比較している。

自動調整されたグリッド 画像14: 自動調整されたグリッド

自動調整されたグリッドによる出力 画像15: 自動調整されたグリッドによる出力

精度確認の為に壁面タイル部分を多めに入れてみたが、想像していたよりはマシな結果になっているように感じられる。
付け焼刃にしては上等といったところ。左右方向のズレはほぼ完全に補正されていると言っていい。
しかし今度は一部で縦方向に圧縮がかかってしまっている。看板の「5F」と「1F」の縦幅が異なるのを見てもらえると分かりやすい。
壁面タイルも中段に比べて画像上方・下方では潰れた格好になっているのが分かるだろう。
これは[画像14]で横方向に伸びるグリッド線が画像上部・下部では斜めになり長さが大きくなっていることを考慮していないのが原因である。

グリッド自動調整の仮実装 [その2]

これを考慮してグリッドを設定するためにはどう考えればよいか。
ひとまずグリッドが基準よりも斜めになっているときにその傾きを考慮すればよさそう。
そこで、線が一つ前の線に対して傾いていない場合の仮想的な長さを求めてそれを「比較用の長さ」として用いることにしてみた。

では「比較用の長さ」とやらをどのように算出するかであるが、単に角度が開いていたら余弦を掛け合わせてやればよさそう。
余弦は内積から求められる。高校数学の範囲なので省略。
でも一乗では補正しきれないだろうから累乗したほうがいいはず。何乗すればいいのかは実際にやって様子見する。
この時の指数を仮に「グリッド線方向差感度」と呼ぶことにする。
そのあたりの調整を行いつつ出力した結果が以下。この出力の為に余弦の8乗を掛け合わせている。

パースをある程度考慮したグリッドによる出力 画像16: パースをある程度考慮したグリッドによる出力

[画像15]に比べて歪みがかなり補正されているのがわかるだろう。ここまでの過程で射影変換に近いこともできるようになった。
それではこの方法でどんな輪郭線に対しても適切なグリッドが得られるだろうか。実はそうとも限らないのである。

グリッド自動調整の仮実装 [その3]

再び本のページ綴じ部分の歪みについて考える。
ここまでで実装した機能を用いれば今までで一番良い結果を今までで一番簡単に得られるはずである。
そこで、今までよりも良い結果が得られるまでグリッドを調整して得られた出力が以下である。

仮実装のグリッド調整+手動の調整で得られた出力 画像17: 仮実装のグリッド調整+手動の調整で得られた出力

まあまあ綺麗に補正できているように見えるが、この品質を出すためにはやはりページ綴じ部分のグリッド調整がまだ必要である。
これも自動でやってほしいと思い、以下のようなことを考えた。

グリッドが密になるべき部分は、画像上で遠くにある又は遠近の変化が大きいことになる。今からはこの遠近の変化を考える。
遠近の変化が大きいとグリッド線の長さも大きく変化する。長さが変化すると始点・終点同士を結んだ線の角度も大きくなる。
角度なら内積から余弦。さっきと同じ。これの累乗を掛け合わせて補正する。
このときの指数も仮に「長さ変化感度」と名付けてパラメータ化しておく。

という訳でここまででグリッド調整用のパラメータが3つ出てきたので、これらを画像に従って調整するという形に落ち着いた。
これで、輪郭調整とパラメータ調整+グリッド自動調整によりもっともらしい画像が簡単に得られるようになった(はず)。

パースをそこそこ考慮したグリッド(一部) 画像18: パースをそこそこ考慮したグリッド(一部)

パースをそこそこ考慮したグリッドによる出力 画像19: パースをそこそこ考慮したグリッドによる出力

どうだろうか。恐らく[画像17]に比べて左右方向の歪みが綺麗に補正されているように感じられるだろう。
射影変換のような既存アルゴリズムと併用すれば多くの画像はこれで補正が可能になったといえよう。

適応的閾値処理

歪みを補正したい画像というのは、本などのように文字情報のみにより構成されていることも多い。
そういった画像はふつう2値化して文字を読みやすくしたりするが、せっかくなのでOpenCVでやってみる。
実はOpenCvSharpは本家OpenCV以上に充実した適応的閾値処理を提供している。これを使わない手はない。

一応GIMPでも補正してみる。こちらはただの閾値処理なので補正がうまくいかないこともままある。
本をデジカメで撮影したような画像は部分によって明るく写ったり暗く写ったりするので閾値が一意には決め辛いのである。

GIMPを用いた2値化 画像20: GIMPを用いた2値化

上の例では、画像中央上部は白飛びし中央下部は黒く潰れてしまっている。これを解消するのが「適応的閾値処理」である。
じゃあどうやるか。といっても全部下のページに書いてある。まあ見といて。

注意する点があるとすれば、Cv2.AdaptiveThreshold()CV_8UC1(8bitモノクロデータ)しか受け付けない点である。
今まではずっとCV_8UC3(24bitカラーデータ)のまま処理してきたので、渡す前にCv2.CvtColor()する必要がある。
またBinarizer.SauvolaFast()を使うとより高度なアルゴリズムで閾値処理ができる。こちらは本家OpenCVにはない機能。

GaussianC形式で閾値処理を行った結果 画像21: GaussianC形式で閾値処理を行った結果

Sauvolaの手法で閾値処理を行った結果 画像22: Sauvolaの手法で閾値処理を行った結果(b=23, k=0.12, r=33)

一見[画像21]の方が綺麗だが、実は等倍だと[画像22]の方が綺麗に表示される。それほど大きな差でもないのでここでは省略する。
それよりも[画像20]と比較して端っこ辺りがちゃんと処理されている点に注目してほしい。
こういう便利機能がサクッと使えてしまうのがOpenCVやOpenCvSharpの魅力である。

本当は適応的明度コントラスト処理をしたい。でも自分で実装するのめんどいからまた今度。

やり残し

正変換・逆変換への対応

GIMPの遠近法変形には正変換と逆変換が存在している。
正変換はsrc→dstの正順マッピング、逆変換はdst→srcの逆マッピングなので、実は今までやってきたのは全て逆変換である。
記事の目的は逆変換のみだったので正変換は実装しなかったが、できるならできたほうが嬉しい。

OpenCVはアフィン変換や透視変換において正順マッピングが与えられたときに逆マッピングを求める機能がついている。
しかし残念ながら汎用的な幾何学変換において逆順マッピングを計算してくれる機能は付いていない。
よってこの機能を自分で実装しなければならないが、ただの行列演算で済むOpenCV付属機能とは訳が違うので面倒。
それに正順マッピングは既存ソフトウェアでも実現可能な変形である。だから気が向いたら実装する、くらいの気分で。

自由変形への対応

今回やったマッピング方法を応用すれば自由領域から自由領域へのマッピングもできそう。
でもめんどいからまた今度。当分はケージ変形で我慢。

適応的明るさ調整

閾値処理だとどうやってもモノクロになってしまう訳で、実際はカラーの情報を残しつつ鮮鋭化したい事の方が多い。
それも画像の部分部分の写りの明るさを考慮した方法でやりたい。何らかの方法が思いついたらやってみるつもり。

最後に

書き始めは3月だったのに書き終えた頃には4月になってしまった。
自分で記事を作るという過程の面倒さを実感するとともに、コピーではなく自分で記事を書いている人たちへの畏敬の念を抱いた。
というか記事に限らずプログラムだろうと芸術だろうと自分で作る人ってすごいなーと。

パクリや焼き直しの糞記事がやたらと持て囃されるこのご時世、0を1にできる人達には改めて敬意を表したい。
世の中には「0から1よりも1から100」などと宣う輩もいるようだが、我々が100の利益を享受できるのは最初の1のお蔭である。
こうして簡単に画像を弄るプログラムを組めるのも、VisualStudioやらOpenCVやらOpenCvSharpやらが無償で利用できるお蔭なのである。
という訳でMicrosoft社・Intel社・shimat氏・その他関係各位に感謝。

この記事はそれほど高尚なものではないが、巡り巡って自分以外の誰かの役に立ってくれることを期待する。
そんなことが果たしてあるのかというと実に微妙な話ではあるが。

また、この記事では各所から引っ張ってきた画像をいろいろと引用させてもらった。
実のところ今回の試行錯誤は[画像00]を見つけたのが全ての起点である。
最後になってしまったが図らずも私に切っ掛けを与えてくださった各氏に感謝の意を表したい。

付録

[画像22]までで用いてきた画像はページ左半分が欠けているが、実はその部分を写した画像も存在する(https://infotomb.com/47v91.jpg)。
これと組み合わせて下のように1ページ分の文章を復元してみた。

ページの復元 画像23: ページの復元

もうちょい大きめのやつをこっちに上げておいた。それほど高品質ではないので期待は禁物。

興味のある人は他のページも復元してみるといいかもしれない。ただしページ内に写真があるので処理が面倒になりそう。
スキャナが使える人は最初からそっち使った方が100倍楽。
写真を補正するにしてもPhotoshopがあるなら最初からそっち使った方がいいかもしれない。

2015/04/09 追記分

C#から使うことのできるOpenCVのラッパーがもう1つ見つかったのでメモしておく。

「OpenCVLib」。最終更新は2010/04/21だが解説が日本語であったりとかOpenCvSharpよりも本家に近い形で書けたりとか
関数のほとんどが実装されていたりとか、魅力あり。でも今回はOpenCvSharpで作ってしまったので試す機会があったら使うかも。


管理人Twitter: @su_te_ak/◆mmft4k9vgtL6
要望等はTwitterへ

Home -> 雑用 -> 雑用メモ -> [5.5 [C#]本のページ綴じ部分が歪んだデジカメ画像を補正したい]

ここ以降は鯖が勝手に付加するやつです