[コピペで試せる] Python OpenCV で行う、カメラキャリブレーション

## 初めに
「[法政大学情報科学部 Advent Calendar 2023](https://qiita.com/advent-calendar/2023/cis_hosei)」 22 日目の記事になります。
どうも、iwatanabeeです！
研究室では、画像処理の研究しています。

今回は、PythonとOpenCVで画像の歪みを補正する「カメラキャリブレーション」をしていきます。

最近、研究室の同期が「キャリブレーション」を使うらしいので、少しでも参考になればいいと思い、この記事を作成しました。

## 概要
画像処理をしたいときに、画像が歪んでいたり、ズレていたりしたら処理に影響が出てしまいます。
カメラで撮った画像は、丸みを帯びていたり、歪みが生じている可能性があるので、これらを修正するために「カメラキャリブレーション」という処理を行います。
「カメラキャリブレーション」とは、内部・外部パラメーター、レンズの歪収差係数を求め、画像を補正する処理です。

簡単に言うと、**どれくらい歪んでいるかを求めて、正しい画像に修正する処理**です。

この処理を行うために、OpenCvのカメラキャリブレーションのライブラリを使用していきます。
今回はわかりやすいように、歪みが大きいステレオカメラで撮影した画像を使います。

これは右斜め横から、ステレオカメラで撮った写真(左)とiPhoneで撮った写真(右)です。
左のほうは結構丸みを帯びています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/23886023-1f30-3537-aa2e-b87e610f1ee9.jpeg" width="50%"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/aaeb4c14-25f1-9d48-b234-b9d55580074a.jpeg" width="50%">


## カメラのパラメーター
写真を撮るときに、カメラは3次元の情報を2次元のデータに変換します。
その時に歪みなどが生じます。
ですので、3次元のデータ(外部パラメータ)や2次元のデータ(内部パラメータ)を元に歪みを修正します。
内部パラメータとは、カメラの焦点距離や写真の歪みの値などのことで、
外部パラメータは、位置や向いている方向などの値のことを言います。

## 手法
[Opencvのドキュメント](https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html)にあるカメラキャリブレーションのチュートリアルを参考にしました。
1. 修正したいカメラで「チェスボード」を撮り、Opencv の [findChessboardCorners](https://docs.opencv.org/4.x/d9/d0c/group__calib3d.html#ga93efa9b0aa890de240ca32b11253dd4a) でコーナーの位置を見つける
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/b0c8ad4a-6173-e07f-8c3d-b19d6e9879f3.png)
 (チュートリアルより↑)
 
2. [calibrateCamera](https://docs.opencv.org/4.x/d9/d0c/group__calib3d.html#ga3207604e4b1a1758aa66acb6ed5aa65d)を使用し、歪みのパラメータを出す
    ```python  
    再投影誤差:  1.4784437264904153
    カメラ行列: 
     [[852.65600337   0.         683.1527559 ]
     [  0.         850.88052634 499.25598778]
     [  0.           0.           1.        ]]
    歪み係数: 
     [[-3.90627899e-01  1.64628595e-01 -1.15782603e-03 -1.68067066e-04 -3.58304735e-02]]
    ```
3. パラメーターをもとに[undistort](https://docs.opencv.org/4.x/d9/d0c/group__calib3d.html#ga69f2545a8b62a6b0fc2ee060dc30559d)で歪みを補正し、画像を変換します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/1eb64d8f-7d3d-316e-7f95-ea62d86621f0.png)
 (チュートリアルより↑)


## 実際にやってみる
### チェスボードを、修正したいカメラで約20枚ほど撮る
・左右上下いろんな角度から撮る
・チェスボードができるだけ大きく映るように撮る
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/39570cfb-085b-ea53-fcf7-2a89c19e4323.jpeg" width="30%"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/72412d24-582e-c0a0-91ac-0d3416b43837.jpeg" width="30%"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/e3e25dde-a9d2-c678-af78-06738f8eacba.jpeg" width="30%">

### コードを実装
今回は、Google colab 上でプログラムを実装します。
[Opencvのカメラキャリブレーション](https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html)を参考に、以下のコードを実装しました。
まずは、Google drive のファイルを使用するためにドライブをマウントします。
``` python
from google.colab import drive
drive.mount('/content/drive')
```
先ほど撮った画像が入っているフォルダーのパスを指定します。
```python
file_path = '/image/*.jpg'
```
次に、チェスボードの格子点を見つけて、表示します。
チェスボードの大きさによって格子点の数が変わるので、変数として定義しました。
格子点を数え、cbrow と cbcol に代入します。
つまり、下のチェスボードの青い点の数と赤い点の数をそれぞれの変数に代入します。タイルの数ではないので、注意　↓

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/739702a8-518c-7862-4593-1bdf463d3ced.jpeg">
今回は、格子点が7個(青い点の数)と10個(赤い点の数)なので、以下のように代入します。

```diff_python
import numpy as np
import cv2 as cv
import glob
from google.colab.patches import cv2_imshow

# termination criteria
criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 30, 0.001)

+ # chessboard grid points
+ cbrow = 7
+ cbcol = 10

# prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
objp = np.zeros((cbrow * cbcol,3), np.float32)
objp[:,:2] = np.mgrid[0:cbcol,0:cbrow].T.reshape(-1,2)

# Arrays to store object points and image points from all the images.
objpoints = [] # 3d point in real world space
imgpoints = [] # 2d points in image plane.

images = glob.glob(file_path)

for fname in images:
    img = cv.imread(fname)
    gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)

    # Find the chess board corners
    ret, corners = cv.findChessboardCorners(gray,(cbcol,cbrow),None)

    # If found, add object points, image points (after refining them)
    if ret == True:
      objpoints.append(objp)
      corners2 = cv.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria)
      imgpoints.append(corners2)

      # Draw and display the corners
      cv.drawChessboardCorners(img, (cbcol,cbrow), corners2,ret)
      cv2_imshow(img)
      cv.waitKey(500)
    else:
      print("見つけられませんでした"+ fname)

cv.destroyAllWindows()
```
実行前と実行した後の比較です。しっかりとコーナーがとれていることが分かります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/39570cfb-085b-ea53-fcf7-2a89c19e4323.jpeg" width="50%"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/02a8ae23-dba9-4f6c-fd51-52de43119808.png" width="50%">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/72412d24-582e-c0a0-91ac-0d3416b43837.jpeg" width="50%"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/67d6d6fc-9be1-ac61-6251-0d36b8435870.png" width="50%">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/e3e25dde-a9d2-c678-af78-06738f8eacba.jpeg" width="50%"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/5bce51f3-0c37-b28f-6f26-0abfae1a142e.png" width="50%">

カメラキャリブレーションをして、どのくらい歪みのパラメータを出します。
```python
# カメラキャリブレーション
import pandas as pd

retval,mtx,dist,rvecs,tvecs = cv2.calibrateCamera(objpoints,imgpoints,gray.shape[::-1],None,None)

print("再投影誤差: " , retval)
print("カメラ行列:", "\n", mtx)
print("歪み係数:", "\n", dist)

rvecs_list = [vec.flatten() for vec in rvecs]
tvecs_list = [vec.flatten() for vec in tvecs]
df_r = pd.DataFrame(rvecs_list, columns=['X', 'Y', 'Z'])
df_t = pd.DataFrame(tvecs_list, columns=['X', 'Y', 'Z'])

print("回転ベクトル:")
print(df_r)
print("並進ベクトル:")
print(df_t)
```

<details><summary>上のコードを実行した結果</summary>

```py
再投影誤差:  1.4784437264904153
カメラ行列: 
 [[852.65600337   0.         683.1527559 ]
 [  0.         850.88052634 499.25598778]
 [  0.           0.           1.        ]]
歪み係数: 
 [[-3.90627899e-01  1.64628595e-01 -1.15782603e-03 -1.68067066e-04
  -3.58304735e-02]]
回転ベクトル:
           X         Y         Z
0  -0.365544  0.011984  3.103333
1   0.400793 -0.041737 -3.105192
2  -0.495379  0.029039  3.067169
3  -0.352353  0.112886 -3.053914
4  -0.566523  0.069933 -3.069064
5   0.066815 -0.055324 -3.115904
6  -0.049717  0.390782  3.093334
7  -0.433863  0.263007  3.025748
8   0.620224 -0.287184 -3.038019
9   0.496149 -0.475302 -3.050693
10 -0.072113  0.406728  3.094032
11 -0.309027 -0.419033 -2.981342
12 -0.684750 -0.090989 -3.003238
13 -0.033462 -1.193063  2.871082
14  0.594790  1.078648 -2.774555
15 -0.648846  0.594418 -2.782277
16 -0.109192 -0.039568  3.119014
17  0.748840  0.033861 -2.893906
18 -0.356475  0.111770 -2.964364
19 -0.200794 -0.336976  3.045980
並進ベクトル:
           X         Y          Z
0   3.884420  2.817445   8.430965
1   5.238084  2.778055   8.973617
2   5.828367  2.109768   9.538777
3   2.358785  2.232135   7.516289
4   2.351383  2.226250   7.407635
5   3.975461  2.126758   8.619124
6   4.640993  2.324188   6.886309
7   4.844123  1.977229   8.775239
8   5.070771  2.586556   9.404153
9   5.112153  2.002924   8.966233
10  4.386051  1.984836   6.961575
11  1.996052  1.322678   6.391628
12  1.835680  2.492204   7.286230
13  4.147323  1.932775   9.840662
14  3.744548  1.806862  12.614263
15  2.047684  4.339194   9.466524
16  4.571633  2.890404   8.287829
17  2.882845  4.172608  10.919147
18  2.676759  2.867964   7.532899
19  4.765142  2.900835  10.799487
```
</details>
最後にパラメータを使用して補正し、画像の表示をしていきます。

```py
for i, fname in enumerate(images):
  img = cv2.imread(fname)
  h,  w = img.shape[:2]
  newcameramtx, roi=cv2.getOptimalNewCameraMatrix(mtx,dist,(w,h),1,(w,h))

  # 歪補正
  dst = cv2.undistort(img, mtx, dist, None, newcameramtx)

  # 画像の切り落とし
  x,y,w,h = roi
  dst = dst[y:y+h, x:x+w]
  cv2_imshow(dst)
```
## 結果
右の画像が補正された画像です。しっかりとまっすぐになっていることが分かります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/39570cfb-085b-ea53-fcf7-2a89c19e4323.jpeg" width="50%"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/03be2ded-6dfa-e253-adb5-a2bec090eaec.png" width="50%">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/72412d24-582e-c0a0-91ac-0d3416b43837.jpeg" width="50%"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/d9529ace-7616-949f-3546-2bd5f3ce620b.png" width="50%">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/e3e25dde-a9d2-c678-af78-06738f8eacba.jpeg" width="50%"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865794/77e59b2e-5c4f-8d83-7cbe-fdc7c3dce79f.png" width="50%">