OpenCV&Pythonで、簡単に魚眼レンズの歪み補正(Calibration)を行う方法

スマートホーム
スポンサーリンク

はじめに

防犯カメラなど、広範囲を撮影したい時には、複数のカメラで撮影するのも手ですが、魚眼レンズを使うのが手取り早いです。しかし、魚眼レンズで撮影した画像は、上の左の画像のように外側に行くほど歪んでしまいます。このため、物体検知や動体検知などを行うためには、レンズの歪みを補正する必要があります。

この魚眼レンズの歪み補正処理について、あまり日本語の情報が無かったので、まとめておきます。

用意するもの

①ボードコンピュータ

カメラで画像を撮影して、撮影した画像の歪み補正を行うコンピュータが必要です。Raspberry PiやJetson nanoなどのボードコンピュータが良いでしょう。

②魚眼レンズ付きカメラモジュール

今回の主役の魚眼レンズ付きカメラモジュールです。今回は180度まで撮影可能な以下のカメラを使います。USB接続で、4K@30fpsまでの動画撮影にも対応しています。

用意するものは、以上の2点です。

OpenCVによる歪み補正の方法

魚眼レンズには、120度・160度・180度など様々な広角のものが販売されています。よって、カメラによって画像の歪みの度合いが異なるので、使うカメラの歪み度合いに応じて、補正処理を行う必要があります。

魚眼レンズの歪み補正の計算ロジックは、OpenCVのカメラキャリブレーションのページに書かれていますが、行列計算や複雑な数式が出てきて、高校時代に数学Cが嫌いだった私には理解できません💦ただ、この難しい計算部分を全てやってくれる素晴らしい関数がOpenCVには用意されているのでこれを使います‼️

準備

まずは、歪み補正をするために各種準備を行います。

①チェスボードの準備

OpenCVの歪み計算は、白黒の均等な四角が書かれたチェスボードをカメラで撮影して、四角の歪み具合を検出することで行うそうです。なので、まずはチェスボードを用意する必要があります。チェスボードなんて家にない!と思った方、安心してください。このリンクからPDFをダウンロードして印刷すればOKです。

また、印刷したら黒い四角の一辺の長さを定規で測っておいてください。私の場合はA4の紙に印刷して、一辺の長さは23mmでした。

②OpenCV&Pythonの準備

次に、Raspberry PiもしくはJetson nanoに、OpenCVとPythonをインストールします。インストール方法は、いろいろ情報があるのでそちらを参照してください。今回は、以下を利用しています。

$ python
Python 2.7.17 (default, Jul 20 2020, 15:37:01) 
[GCC 7.5.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import cv2
>>> cv2.__version__
'4.3.0

また、以下のプログラムでは、USBカメラからの撮影にv4l2captureを使っているので、インストールされてない場合はインストールして下さい。

$pip3 install v4l2capture

魚眼レンズの歪み係数の取得

①プログラムの作成

まず、チェスボードを撮影して歪み係数を計算する、以下のPythonプログラムを作成します。11〜21行目の定数部分は、お使いの環境に合わせて適宜修正してください。プログラムの説明はコメントを見て下さい😊

# -*- coding: utf-8 -*-
import numpy as np
import cv2
import os
import v4l2capture
import select
import datetime
import time
from time import sleep

SQUARE_SIZE=23.0     #チェスボードの1つの四角の大きさ(mm)
BOARD_SIZE=(10,7)    #チェスボードの四角の数

WIDTH=1920           #横サイズ
HIGHT=1080           #縦サイズ
DEVICE="/dev/video0" #カメラデバイス

CAMERA_FILE="camera.csv" #出力ファイル
DIST_FILE="dist.csv"     #出力ファイル名

MAX_COUNT=25             #画像取得数

# メイン関数
def main():
    #チェスボードを設定
    pattern_points = np.zeros( (np.prod(BOARD_SIZE), 3), np.float32 ) 
    pattern_points[:,:2] = np.indices(BOARD_SIZE).T.reshape(-1, 2)
    pattern_points *= SQUARE_SIZE
    obj_points = []
    img_points = []
    
    # ビデオを開く
    video = v4l2capture.Video_device(DEVICE)
    sizeX, sizeY = video.set_format(WIDTH, HIGHT, fourcc='MJPG')    
    print("解像度={0}x{1}".format(sizeX, sizeY))

    #バッファを設定
    video.create_buffers(3)
    video.queue_all_buffers()

    #取り込みスタート
    video.start()

    #カメラが落ち着くまで待つ
    time.sleep(5)
    print("{0}枚の画像を撮影します。cキーで撮影。qキーで終了。".format(MAX_COUNT))
    cnt=0

    while(cnt<MAX_COUNT):
        # カメラから画像を取得
        select.select((video,), (), ())
        image_data = video.read_and_queue()
        img = cv2.imdecode(np.frombuffer(image_data, dtype=np.uint8), cv2.IMREAD_COLOR)
        
        #画面をリサイズして表示
        resized_img = cv2.resize(img,(800, 600))
        cv2.imshow('frame', resized_img)
        
        key = cv2.waitKey(1)
        if key == ord('c'):
            #グレースケールに変換
            img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

            #チェスボードが写っているか判定
            found, corner = cv2.findChessboardCorners(img_gray, BOARD_SIZE)
            if found:
                cnt=cnt+1
                print("チェスボードを検出={0}/{1}".format(cnt,MAX_COUNT))
                #チェスボードを計算
                term = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 30, 0.1)
                r = cv2.cornerSubPix(img_gray, corner, (5,5), (-1,-1), term)
                #img2 = cv2.drawChessboardCorners(img_gray, BOARD_SIZE, r, found)
                #チェスボード情報を保存
                obj_points.append(pattern_points)
                img_points.append(corner.reshape(-1, 2))
                img_ok=img_gray
                
        elif key == ord('q'): # qで終了                                                               
            break
        
    # カメラの歪みパラメータを計算
    rms, k, d, r, t = cv2.calibrateCamera(obj_points, img_points, (img_ok.shape[1],img_ok.shape[0]), None, None)
    # 計算結果を表示
    print ("RMS = ", rms)
    print ("K = \n", k)
    print ("d = ", d.ravel())

    #値をファイルに保存
    np.savetxt(CAMERA_FILE, k, delimiter =',',fmt="%0.14f")
    np.savetxt(DIST_FILE, d, delimiter =',',fmt="%0.14f")
            
    #カメラとウィンドウを閉じる
    video.close()
    cv2.destroyAllWindows()
    
if __name__ == '__main__':
    main()

②プログラムの実行

プログラムが完成したら以下のように実行します。「25枚の画像を撮影します。cキーで撮影。qキーで終了。」と表示されたら、カメラにチェスボード全体が映るようにして、キーボードの「c」キーを押します。

そして、チェスボードの位置をずらしながら25枚の写真を撮影します。魚眼レンズは端っこが大きくずれるので、カメラの四隅にチェスボードが多く映るようにして撮影するのがポイントです。25枚の写真を撮り終えたら、カメラの補正係数が、画面と「camera.csv」と「dist.csv」に出力されます。

$python 11_analyse.py 
解像度=1920x1080
25枚の画像を撮影します。cキーで撮影。qキーで終了。
チェスボードを検出=1/25
チェスボードを検出=2/25
・・・・・
チェスボードを検出=25/25
('RMS = ', 2.8435187631462675)
('K = \n', array([[  1.08515378e+03,   0.00000000e+00,   9.83885166e+02],
       [  0.00000000e+00,   1.08781630e+03,   5.36422266e+02],
       [  0.00000000e+00,   0.00000000e+00,   1.00000000e+00]]))
('d = ', array([-0.38861593,  0.12293225,  0.00313666, -0.00268991, -0.01614442]))

これでカメラの歪み係数の取得は完了です。出力されたファイルの数字を見ても意味は分かりませんが、まあ良し(笑)

魚眼レンズの歪み補正処理

それでは、取得したカメラの歪み係数を使って画像補正を行います。

①プログラムの作成

カメラで撮影した画像に歪み補正を行い画像ファイルとして保存するプログラムを作成します。11〜16行目の定数部分は、お使いの環境に合わせて修正して下さい。

# -*- coding: utf-8 -*-
import numpy as np
import cv2
import os
import v4l2capture
import select
import datetime
import time
from time import sleep

WIDTH=1920           #横サイズ
HIGHT=1080           #縦サイズ
DEVICE="/dev/video0" #カメラデバイス

CAMERA_FILE="camera.csv" #出力ファイル
DIST_FILE="dist.csv"     #出力ファイル名

#カメラの歪み係数を読み込む
def loadCalibrationFile():
    try:
        camera = np.loadtxt(CAMERA_FILE, delimiter=',')
        dist = np.loadtxt(DIST_FILE, delimiter=',')
    except Exception as e:
        raise e
    return camera, dist

# 画像を時刻で保存する関数
def saveImg(path, img):
    filename="./"+str(path)+".jpg"
    cv2.imwrite(filename, img) 
    print("写真を保存: "+filename)

# メイン関数
def main():
    #歪み係数ファイルを開き、マップを計算
    camera, dist = loadCalibrationFile()
    mapX, mapY = cv2.initUndistortRectifyMap(camera, dist, None, None, (WIDTH,HIGHT), cv2.CV_32FC1)
    
    # ビデオを開く
    video = v4l2capture.Video_device(DEVICE)
    sizeX, sizeY = video.set_format(WIDTH, HIGHT, fourcc='MJPG')    
    print("解像度={0}x{1}".format(sizeX, sizeY))

    #バッファを設定
    video.create_buffers(3)
    video.queue_all_buffers()

    #取り込みスタート
    video.start()

    #カメラが落ち着くまで待つ
    time.sleep(5)
    print("画像を撮影します。cキーで撮影。qキーで終了。")
    cnt=0

    while(True):
        # カメラから画像を取得
        select.select((video,), (), ())
        image_data = video.read_and_queue()
        img = cv2.imdecode(np.frombuffer(image_data, dtype=np.uint8), cv2.IMREAD_COLOR)
        
        #画面をリサイズして表示
        resized_img = cv2.resize(img,(800, 600))
        cv2.imshow('frame', resized_img)
        
        key = cv2.waitKey(1)
        if key == ord('c'):
            cnt=cnt+1
            #魚眼補正処理
            ret_img = cv2.remap(img, mapX, mapY, cv2.INTER_LINEAR)
            saveImg(cnt, ret_img)
                
        elif key == ord('q'): # qで終了                                                               
            break
                    
    #カメラとウィンドウを閉じる
    video.close()
    cv2.destroyAllWindows()
    
if __name__ == '__main__':
    main()

②プログラムの実行

プログラムを作成したら、早速実行してみます。「画像を撮影します。cキーで撮影。qキーで終了。」と表示されたら、キーボードの「c」キーを押します。

すると、写真が撮影され、魚眼補正がされた画像がファイルに保存されます。保存されたファイルを開いてみると、ちゃんと魚眼レンズの補正が行われ、まっすぐな写真になっている事が確認できます。

$python 21_calibrate.py
解像度=1920x1080
画像を撮影します。cキーで撮影。qキーで終了。
写真を保存: ./1.jpg
写真を保存: ./2.jpg
写真を保存: ./3.jpg

以上で魚眼レンズの補正は完了です。

おわりに

今回は、魚眼レンズで撮影した写真について、OpenCVを用いて歪み補正を行う方法を紹介しました。魚眼レンズの補正を行うことで、広範囲に撮影した画像から物体検知や動体検知が可能となります。

今回は静止画での歪み補正ですが、動画でも同じで、フレーム毎に歪み補正を行えばOKです。ただし、動画の場合にはフレーム数が圧倒的に多いので、CPUを使った補正処理では処理が追いつかない事が想定されます。動画の場合については、別途まとめたいと思います。

関連記事

記事が参考になったら、ブログランキングに協力(クリック)して貰えると嬉しいです。

昼間はIT企業に勤めてますが、プライベートでは「育児×家事×IoT」をテーマに家のスマートホーム化に取り組んでいます。Androidアプリも作っているので使って下さい。質問・コメントは、↓のコメント蘭でもFacebookメッセンジャーでもどちらでも大丈夫です。
E-mail:naka.kazz.d@gmail.com

naka-kazzをフォローする
スマートホーム開発者向け
スポンサーリンク
naka-kazzをフォローする
スマートホーム×DIY

コメント