続・rawpyでPython現像 (魚眼・歪曲補正)

python

 python上でraw現像を行って行きます。これまでrawpyを使った現像に関していろいろ調べてきました。ただ、このライブラリのお手軽現像関数postprocessに少し足りない機能があり、それらの補填です。

 これまで、濃度域毎のトーン調整、彩度調整、シャープネス調整、周辺光量調整と作ってきました。今回はその第4弾。レンズの歪みを補正する歪曲補正処理になります。Pythonを使って、樽型や糸巻型の歪み補正ができるようにしてみようと思います。今回はOpenCVの力を使って簡単に済ませてみました。ただし前提知識が必要なので、そのあたりメモっておきます。

 rawpyの現像関数の詳細に関してはこちら

歪曲補正

 写真でいう歪曲は、いわゆる四角が四角として撮影できない歪みを指します。基本的にはレンズ起因で発生し、広角レンズでブロック壁とか撮るとわかりやすいです。魚眼レンズはむしろその歪みを楽しんだりしますね。レンズ中心から外に向かって歪みが大きくなっていきます。代表的な歪み方としては、樽型と、糸巻型の2つがよく知られており、今回もこの補正を狙います。

 これが樽型の歪みのイメージ。広角レンズでよく見ますね。安いアクションカメラはこの手の歪みを持ってます。

 これがその逆で糸巻型のイメージ。望遠側だと起こりがちです。これらの歪みの複合ともいえる陣笠型ってのもあるみたいです。

 他にも歪み方として、レンズとセンサが平行になっていないことに起因する歪み(円周方向の歪み)というものもあるようです。普段使うカメラでそれが気になったことはないですけどね。

 こんな歪み方みたいです。傾いて見えます。たぶんこんな感じ。

 いわゆるレタッチソフト系ではこの歪みを補正する機能が備わっていることが多いですが、残念ながらrawpyにはそれがありません。残念です。自力で何とかしましょう。

モデル

 この樽型や糸巻型の歪みは、広く放射状歪みとして知られているモデルのようです。\(r\)をレンズ中心からの距離として、歪んだ後の座標系\(x_{distort}\)は

$$x_{distort}=x(1+k_{1}r^{2}+k_{2}r^{4}+k_{3}r^{6})$$

 として扱うことができるようです。\(k_{1}\)から \(k_{3}\) はそのパラメータになります。本来無限に続くのでしょうが、現実的には3つ程度で打ち切って問題ないようです。偶関数で規定されています。この逆補正をしてやればよいことになります。レンズ中心から離れるほど歪みが大きくなっていくことになります。\(y\)座標に関しても同様です。

 この後OpenCVで解かせるためにもう一つ、円周方向の歪み(接線歪み)に関してのモデルも軽く見てみます。

$$x_{distort}=x+[2p_{1}xy+p_{2}(r^{2}+2x^{2}]$$

 ちょっとイメージしにくいです。3次元的にとある場所にあるものをピンホール位置を起点に透視投影することを考え、その画像面が傾いていることを仮定すると、上のような式になるのかなと想像しますが、詳しくは追いません。同じ位置にあるのに、大きく見えているとすればそれは手前に傾いていて、逆に小さく見えれば遠くに行ってしまっている。という考えで方程式を解けばなんかできそうな気がします。

 今回これらの歪みに関しては補正を考えていないので、この補正係数にあたる\(p_{1}\)と \(p_{2}\) に関しては0としようと思います。

キャリブレーション

 この歪みの推定は、一般的にはカメラキャリブレーションと呼ばれるもので、サイズ、間隔既知のチェッカーボードのようなものを撮影してその歪みを推定するものです。が、今回はそんなもの無視します。写真撮る時そんなもの合わせて撮影するこたぁないので。

 なので今回は放射状歪みのパラメータ \(k_{1}\)、\(k_{2}\) 、\(k_{3}\) を適当に画像に合わせて動かして、値を決めてやろうと思います。

 ただしやはりOpenCVで解かせるにあたって、最低限理解しておかないといけないパラメータがあります。カメラ行列と呼ばれるパラメータで、画像解像度(サイズ)が変わればそれも変更してあげる必要があります。式としては、

$$A=\begin{bmatrix}f_{x} & 0 &c_{x}\\0 & f_{y} & c_{x}\\0 & 0 & 1 \end{bmatrix}$$

 こんな感じで、焦点距離と画像中心を求める必要があります。ただこれも適当に決めてやるしかありません。\(f\)に関しては画像サイズからスケーリングして、\(c\)に関しては画像の中心座標をそれとして扱うことにします。この単位はpixelです。

コード

 コードです。恐ろしく短いです…。小難しいカメラキャリブレーションを全部すっ飛ばして、適当に決めた補正パラメータで画像を変換しているだけです。

 今回カメラ行列における焦点距離\(f\)は画像の長い方の辺の長さとしました。決めの問題です。こうすることで、解像度(画像サイズ)が異なる画像に対しても同じ歪み補正パラメータで同じような歪み補正ができるようになります。例えば800×600の画像で\(f=800\)を与えた場合と400×300の画像で\(f=400\)を与えた場合で、同じ補正値\(k_{1}\)、\(k_{2}\) 、\(k_{3}\)で相対的に同じ歪みが補正できます。

 加えて中心座標\(c\)は画像の中心座標としました。

import rawpy
import numpy as np
import cv2

raw = rawpy.imread(filename)
img = raw.postprocess(use_camera_wb=True,
                      output_bps=16)[:,:,[2,1,0]]

h, w = img.shape[:2]
f = max([h, w])

mtx = np.array([[f,    0.,  w / 2],
                [0.,   f,   h / 2],
                [0.,   0.,  1    ]])

dist = np.array([-0.1, -0.05, 0, 0, 0])

img = cv2.undistort(img, mtx, dist)

cv2.imwrite(outfile, (img/256).astype(np.uint8))

 コードはこれだけです。16bit現像にはここでもこだわっています。このサンプルでは、 補正値\(k_{1}\)、\(k_{2}\) 、\(k_{3}\) に負の値を入れているので、樽型歪みを補正する方向に動きます。なので、このコードの結果、画像の隅っこが消えてしまいます。

 どうやらundistort関数の補間演算は線形補間(バイリニア)のようです。指定するオプションはなさそうです。undistortを使わず、initUndistortRectifyMapとremapのコンビネーションを使うのも手のようです。この場合remapでは補間方法が選べます。

# img = cv2.undistort(img, mtx, dist)

map = cv2.initUndistortRectifyMap(mtx, dist, np.eye(3), mtx, (img.shape[1], img.shape[0]), cv2.CV_32FC1)
img = cv2.remap(img, map[0], map[1], cv2.INTER_CUBIC)

 こうすることで回りくどいですが同じ歪み補正効果で、画素補間方法違いが実現できます。こうすることで、mapの再利用ができて、同じ補正を複数の画像に対して加える場合には計算量が削減できるという利点もあります。

 加えて隅っこの画像が消えて困るような場合には、スケーリングも合わせて行わせることもできます。getOptimalNewCameraMatrix関数を使ってあげることで、歪みと倍率、同時にremapできるようになります。

# img = cv2.undistort(img, mtx, dist)

n_mtx = cv2.getOptimalNewCameraMatrix(mtx, dist, (img.shape[1], img.shape[0]), 1)[0]
map = cv2.initUndistortRectifyMap(mtx, dist, np.eye(3), n_mtx, (img.shape[1], img.shape[0]), cv2.CV_32FC1)
img = cv2.remap(img, map[0], map[1], cv2.INTER_CUBIC)

 このコードだと逆に糸巻型を補正した際に出てくる四隅の黒も最小限に抑えてくれるので、画質の面でも、画角の面でもこちらの記述の方がよさそうな気がします。

結果

 結果です。ソース画像としては、9mm f8 フィッシュアイ ボディキャップレンズ BCL-0980で撮影した、超歪み画像です。いわゆる魚眼レンズです。この歪みを取ってみます。

 被写体は…ゴミ捨て場の網目です。格子がわかりやすかったので…。

 すごく歪んでます。これまでこのレンズでは遠景しかとっていなかったので、ここまでとは正直びっくりしました。

まずは、定石通り2次の項まで使った

dist = np.array([-0.4, -0.2, 0, 0, 0])

で試すと、

治りかかっていますが、M字型に補正がされています。レンズ中心から遠くに行くほど過補正な感じです。なので遠くに行くほど効果が強い3次の項で逆向きに補正してみます。

dist = np.array([-0.5, -0.15, 0, 0, 0.5])

すると

 まぁ見れたもんです。まだ少し樽型なので、もう少しパラメータは追い込めそうですが、ここまでにしときます。魚眼を使ったひどい歪みは3次の項まで使った補正が必要そうです。

 こっちでもう少し頑張ってみました。

まとめ

 rawpy postprocessでは処理できない、現像のプロセスではあると便利な歪みの補正のコードを、書いてみました。結果も思った通り動いてそうです。正直OpenCVの威力をまざまざと見せつけられた記述になっています。お手軽すぎます。

 深い理解をしようと思うと、少し難しいカメラモデルの知識が必要になりますが、現像時の歪みの補正だけを目的とした場合には、理解放棄しても便利に使えます。突っ込んだ理解が必要な場合には頑張ってお勉強が必要です。

コメント

タイトルとURLをコピーしました