ミラーレスを使ってマニュアルレンズ遊びの続き。今回はオリンパスのボディーキャップレンズ BCL-0980(9mm F8.0 Fisheye)で遊びます。レンズとしての個性はもちろん、Fisheyeならではの広い画角。この画角で撮れる歪みの大きい画はとても面白いのです。ただ被写体や場合によってはやりすぎ感が出ます。
今回はそのレンズの歪み補正に関しても、Python+OpenCVを使ってやってみようと思います。
ボディーキャップレンズ BCL-0980(9mm F8.0 Fisheye)
その名の通りボディーキャップ並みに小さいフィッシュアイ(魚眼)レンズです。フォーカスも絞りももちろんズームも無いレンズでシャッター切るだけの簡単レンズといえば簡単レンズです。厳密にはフォーカスは0.2mと無限遠の2つだけは選べます。近くを撮るか遠くを撮るかの切り替えだけです。質感もプラスチック。姉妹品に15mmのボディーキャップレンズ BCL-1580ってのもあります。
メーカーのレンズのスペックを見ると、安くて小型であるにも関わらず、4群5枚(非球面レンズ2枚)と頑張ってます。ただ、オリンパスさんとしてのくくりは交換レンズではなく関連アクセサリの下にあります…。レンズキャップとかフードと同じくくりです。
リアルボディキャップと並べてみました。
もちろん電子接点もありません。AFのスピードも関係ないので、古いカメラでも遜色なく使えます。
写りとしてはいわゆる魚眼。とんでもなく広い範囲撮影できますが、とんでもなく歪みます。それがこのレンズの味です。
小さいレンズってことで、少し古いですが、PanasonicのDMC-GM1に装着して撮影することが多いです。レンズ交換式カメラとしては最小の構成ではないでしょうかね。
このカメラはファインダー(EVF)もないのですが、このレンズはフォーカスもないし、目の前の大体のものが写るので、ライブビューでも撮影は余裕でできます。ツーリングの時にもついでに入れとくかの感覚で持っていけます。ツーリングキャンプでは必ず持っていきます。違う意味でいいレンズです。
とても広さが際立ちます。ただこの写真からだと周辺光量ダウンの様子もわかってしまいます。まぁこれも含めて味とします。
いわゆる一眼レフで撮影してやったぜ感は全くないのですが、一風変わった写真がこのカメラで撮れるのは面白いです。動画なんかを取るといわゆるGoPro的な感じになります。個人的にはTimelapseを撮ると面白いと思っています。
歪み補正
被写体によっては気になるのが歪み。このレンズの場合、魚眼なのでもちろんそんなことは想定内なのですが、たまに補正したくなります。例えば一部トリミングしたいときなんかは、端の被写体はそれはそれはもう残念な感じになります。画角を雑に決めて撮影すると、どうしても写真のトリミングしたくなります。
このレンズで格子の壁を撮影したものが以下ですが、とてつもなくゆがんでいることが分かります。
RAW現像の場合には現像ソフトである程度は補正できるのですが、このレンズ補正しきれません。SILKYPIX Developer Studio という現像ソフトがこのカメラの場合は使えますが、その補正限界(下の画だと-50)までパラメータ振っても治りきりません。
まぁソフトの仕様として、魚眼を補正してもらおうとは思わないので、それはそれでよいのですが、ちょっとどこまで補正できるのかPythonで簡単に試してみます。(以前この仕組みに関しては少し考察しました。)
歪みモデル
この樽型や糸巻型の歪みは、広く放射状歪みとして知られているモデルのようです。\(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で解かせるためにもう一つ、円周方向の歪み(接線歪み)に関してのモデルも考える必要がありますが、今回その歪みはないと仮定します。
もう少し詳しくはこちら。とはいえこっちもだいぶ不真面目なので、深い理解をしたい方はちゃんと調べた方がいいです。この3つのパラメータを与えるだけで補正します。
本当はチェッカーパターンとかを撮影してちゃんとするんでしょうけど、簡単に試すを基本方針として据えたので。
コードとしては、歪み補正に加え、端っこをトリミングして拡大まで行っても、OpenCVの力を使うことで以下で済みます。
import numpy as np import cv2 from PIL import Image # 定数 scale = 0.95 # 画像を開く image = Image.open(fname) img = np.array(image, dtype=np.uint8) # 歪み補正 f = max([h, w]) mtx = np.array([[f, 0., w / 2], [0., f, h / 2], [0., 0., 1 ]]) # 歪みパラメータ(固定値) dist = np.array([-0.63, -0.2, 0, 0, 0.8]) 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) # 拡大+shift(トリミング) mapx = map[0]*scale + (1-scale)*w/2 mapy = map[1]*scale + (1-scale)*h/2 img = cv2.remap(img, mapx, mapy, cv2.INTER_CUBIC) # 4:3 -> 3:2比率への変換 (高さを 8/9する) (トリミング) strt_y = h*1//18 end_y = h*17//18 img = img[strt_y:end_y,:,:]
この処理の効果は
こんな感じになります。中心部を気持ちトリミングしていますが、隅っこには不自然な歪みが残っています。これも取り去りたければ上記のコードで示しているところのscaleのパラメータをもう少し小さくすればさらに中心部だけ切り出せます。今回のパラメータでのトリミング範囲はギリギリを攻めました。
上が補正前、これはこれでよいのですが、
建物のラインがまっすぐに補正されました。
歪みパラメータは色々試した結果です。あまりやりすぎると補正が効いている中央部の切り出せる箇所が小さくなり、せっかくの広い画角が台無しになるので、ほどほどでよいと割り切りました。
EXIF付与
あとカメラと情報をやり取りする電子接点のないレンズなので、レンズ側からなんの情報も取れません。焦点距離と絞りの情報くらいはEXIFに残しておきたいので、それだけ加工します。これにはpiexifというライブラリを使いました。この使い方に関しては以前記事にしたので参考まで。
通常最初から入っているようなライブラリではないので、pip install piexifをしてあげる必要があります。
これで、タグの編集が可能になります。今回は焦点距離と絞りの他に、画像サイズも変わるので、その情報だけ追加更新して保存するように記述します。
import piexif # Exif付与 exif_dict = piexif.load(fname) exif_dict["0th"][piexif.ImageIFD.ImageWidth] = img.shape[1] exif_dict["0th"][piexif.ImageIFD.ImageLength] = img.shape[0] exif_dict["Exif"][piexif.ExifIFD.FocalLength] = (90, 10) # 焦点距離 9mm exif_dict["Exif"][piexif.ExifIFD.FNumber] = (80, 10) # 絞り(F8固定) exif_dict["Exif"][piexif.ExifIFD.FocalLengthIn35mmFilm] = 18 # マイクロフォーサーズなので倍 exif_dict["Exif"][piexif.ExifIFD.PixelXDimension] = img.shape[1] exif_dict["Exif"][piexif.ExifIFD.PixelYDimension] = img.shape[0] exif_bytes = piexif.dump(exif_dict)
これもこれだけ…。なんとも簡単。これをしとけばWindowsであればエクスプローラーの表示から「詳細ウィンドウ」を出すと
こんな情報が付与されていることが見て取れます。ナイスです。
補正例
参考までに。曲がったものがまっすぐに補正されていることが分かります。画角の比較でM.ZUIKO DIGITAL 17mm F2.8で同じ被写体を撮ったものを並べてあります。こいつもだいぶ広角側のレンズですが、それよりも1段広くとれていることが分かります。
明るさの補正はしてないです。ひとまず画角だけ参考にしてください。
補正前 (Panasonic / DMC-GM1)
補正後 遠景の被写体はそれほど補正効果を実感できないですね。
Panasonic / DMC-GM1 / M.ZUIKO DIGITAL 17mm / F11 / 1/500秒 色合いの違いはまぁ置いといてだいぶ広いです。
補正前(Panasonic / DMC-GM1)
補正後 建物のラインがまっすぐになりました。
Panasonic / DMC-GM1 / M.ZUIKO DIGITAL 17mm / F5 / 1/320秒 参考まで。
まとめ
かなりカジュアルなレンズなので、ほんとボディキャップの代わりに持っていても損はないと思います。もう一つ15㎜のボディーキャップレンズ BCL-1580ってのもあります。焦点距離17mmのパンケーキレンズを持っているので、こっちの9mmの方を買ってしまいましたが、こちらも持っててもよさそうな一品です。こっちの方が気持ち安いです。
コード
全コード載せておきます。このコードではついでにこのレンズで出てしまう周辺減光と、色収差の補正もしてあります。補正値は適当ですが。そこまでしてもこの記述量で済むことにやはり驚きます。コピペで使えると思います。
import numpy as np import cv2 from PIL import Image import piexif # 定数 scale = 0.95 fname = "P1610872.JPG" fsave = "P1610872_1.JPG" # 画像を開く image = Image.open(fname) img = np.array(image, dtype=np.uint8) h, w = img.shape[:2] # 収差補正(Greenを拡大) green = cv2.resize(img[:,:,1], None, fx = 1.0005, fy = 1.0005, interpolation = cv2.INTER_CUBIC) difx = (green.shape[1] - w) // 2 dify = (green.shape[0] - h) // 2 img[:,:,1] = green[dify:dify+h,difx:difx+w] # 周辺減光補正 size = max([h, w]) # 幅、高の大きい方を確保 x = np.linspace(-w/size, w/size, w) y = np.linspace(-h/size, h/size, h) # 長い方の辺が1になるように正規化 xx, yy = np.meshgrid(x, y) r = np.sqrt(xx**2 + yy**2) gain = 0.4*r + 1 # 減光補正パラメータ(固定値) gainmap = np.dstack([gain, gain, gain]) # RGB同じゲイン img = np.clip(img * gainmap, 0., 255).astype(np.uint8) # 歪み補正 f = max([h, w]) mtx = np.array([[f, 0., w / 2], [0., f, h / 2], [0., 0., 1 ]]) # 歪み補正パラメータ(固定値) dist = np.array([-0.63, -0.2, 0, 0, 0.8]) 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) # 拡大+shift mapx = map[0]*scale + (1-scale)*w/2 mapy = map[1]*scale + (1-scale)*h/2 img = cv2.remap(img, mapx, mapy, cv2.INTER_CUBIC) # 4:3 -> 3:2比率への変換 (高さを 8/9する) strt_y = h*1//18 end_y = h*17//18 img = img[strt_y:end_y,:,:] # Exif付与 exif_dict = piexif.load(fname) exif_dict["0th"][piexif.ImageIFD.ImageWidth] = img.shape[1] exif_dict["0th"][piexif.ImageIFD.ImageLength] = img.shape[0] exif_dict["Exif"][piexif.ExifIFD.FocalLength] = (90, 10) exif_dict["Exif"][piexif.ExifIFD.FNumber] = (80, 10) exif_dict["Exif"][piexif.ExifIFD.FocalLengthIn35mmFilm] = 18 exif_dict["Exif"][piexif.ExifIFD.PixelXDimension] = img.shape[1] exif_dict["Exif"][piexif.ExifIFD.PixelYDimension] = img.shape[0] exif_bytes = piexif.dump(exif_dict) # 保存 im = Image.fromarray(img) im.save(fsave, "jpeg", exif=exif_bytes, quality=95)
コメント