2018年8月31日金曜日

OpenCVでYOLOv3のサンプルが動かなかったのでPRした

今回、OpenCVに初めてIssueとPull requestを発行して、Margeされたのでその経緯を書きたいと思う。
また、同じエラーの人のために(3.4.2、3.4.3で出ると思います)。




発端

OpenCV 3.4.2でYOLOv3がサポートされたので、動作確認しようと思ったのが発端。



しかし、サンプルを動かしたのだが、エラーとなってしまった。。。

サンプルはPython(samples/dnn/object_detection.py)。
自分の環境は以下。

  • OpenCV => 3.4.2
  • OS => Fedora 28
  • Compiler => GCC 8.1
  • Python3 => 3.6.6

実行したときのコマンド
$ python3 object_detection.py --input=./test.jpg --model=./yolov3.weights --config=./yolov3.cfg --classes=./object_detection_classes_coco.txt --scale=0.00392 --width=416 --height=416

発生したエラー
Traceback (most recent call last):
File "object_detection.py", line 196, in 
postprocess(frame, outs)
File "object_detection.py", line 162, in postprocess
drawPred(classIds[i], confidences[i], left, top, left + width, top + height)
File "object_detection.py", line 73, in drawPred
cv.rectangle(frame, (left, top), (right, bottom), (0, 255, 0))
TypeError: integer argument expected, got float


調べてみた

ググってみても、同じようにコケている人もおらず、C++では動いていそう。

怒られている理由は「cv.rectangleの引数には整数を指定しろ!」。
確かに座標(left, top, right, bottom)は整数である必要がある。
おそらく座標のパラメータが整数でない型なんだなーと思っていた。
実際はleftとtopが浮動小数点だった。
はい。。。Pythonさん、すいません。。。


最初は環境??とか思っていたが、原因はサンプルの以下の2行が原因だとわかった。
https://github.com/opencv/opencv/blob/3.4.2/samples/dnn/object_detection.py#L145-L146
left = center_x - width / 2
top = center_y - height / 2

Python3の場合、整数除算の結果は浮動小数点である。
(Python2は整数除算の結果は整数なので、Python2で実行すると問題ない)

なので、整数型に変換してあげると動いた!
left = int(center_x - width / 2)
top = int(center_y - height / 2)


IssueとPull Requestを出してみる

今までお世話になったOpenCVなので、勇気をだして初めてのIssueとPull Requestを送ってみることにした。

Pull Requestを出すにあたって公式も含めて以下を参考にした(ありがとうございます)。


Issue、Pull Requestとも反応が早いことにびっくり!
簡単だったこともあと思うが、1日もせず、マージされクローズしてしまった。

初めて出してみた感想は
  • 英語、やっぱりだめ、、、もっと勉強しないと
    (Google翻訳すごい)
  • 焦ってはだめ
    (Enterキーを間違って押してIssueのタイトルが変になってしまった)
  • 反応早い!ソッコーで反応くる
  • しかも、解決方法を複数提案
    あ、、、そういう方法もあるねと勉強になる!
  • good first issueなんてラベルがつくとテンション上がる!
  • マージされたらテンションMax!!
  • あれ?Issue誰クローズするの?僕??と思っていたら、クローズされた
    (これなにがただしいんだろう?)
  • はじめの一歩はとても戸惑うが、機会があれば次もチャレンジ!

次のリリース(3.4.4)が楽しみ!

2018年8月20日月曜日

OpenCVでカメラパラメータの解像度を変更したい

OpenCVでカメラキャリブレーションを行ったときと異なる解像度で補正したい場合がある。
例えば、Raspberry Pi + PiCameraのような組込用途の場合、高い解像度でインタラクティブなキャリブレーションを行う場合、以下のような制約がでてくる。

  • カメラキャプチャの画像を表示するモニターがない
    (まあ、自分がFullHDより大きいモニターを持っていないのが原因なんだけど・・・)
  • キャリブレーションの処理が遅くなる
    Raspberry Pi 3 Model B+ でも、1640 x 1232の解像度でCircle Patternの場合、フレームレートがかなり遅くなる(chAruco Patternはそこまで遅くならない)。

まあ、撮影したパターンをマシンパワーのあるPCなどで解析することも可能なのだが、やはり、"インタラクティブ"にできることはメリットが大きいと思う。

OpenCVの場合、cv::getOptimalNewCameraMatrixを使うことで、カメラパラメータの解像度を変更することができる。

cv::getOptimalNewCameraMatrixは、解像度の変更だけでなく、歪み係数とフリースケールパラメータ(alpha)から周りの画像の欠損を考慮したカメラパラメータを作成してくれる(*1)。
歪み係数は解像度に依存しない(*2)。

Pythonでcv::getOptimalNewCameraMatrixを使って解像度を変更してみる。

使い方


カメラキャリブレーション

まずは、640 x 480でカメラキャリブレーションを行う。
その時のカメラパラメータ抜粋。

<cameraResolution>
  640 480</cameraResolution>
<cameraMatrix type_id="opencv-matrix">
  <rows>3</rows>
  <cols>3</cols>
  <dt>d</dt>
  <data>
    5.3447832911092576e+02 0. 3.1759911211440613e+02 0.
    5.3447832911092576e+02 2.3505836099223782e+02 0. 0. 1.</data></cameraMatrix>
...
<dist_coeffs type_id="opencv-matrix">
  <rows>1</rows>
  <cols>5</cols>
  <dt>d</dt>
  <data>
    2.7780784561125338e-01 -7.6282457474037868e-01 0. 0.
    6.2366706868917043e-01</data></dist_coeffs>

カメラパラメータの読み込み

前回のブログの通り、cv::FileStorageを使ってカメラパラメータ、歪み係数を読み込む。
fs = cv2.FileStorage("PathToCameraParameterFile", cv2.FILE_STORAGE_READ)
if fs.isOpened():
        width = (int)(camera_fs.getNode("cameraResolution").at(0).real())
        height = (int)(camera_fs.getNode("cameraResolution").at(1).real())
        camera_matrix = camera_fs.getNode("cameraMatrix").mat()
    dist_coeffs = camera_fs.getNode("dist_coeffs").mat()

新しいカメラパラメータの作成

cv::getOptimalNewCameraMatrixを使って新しいカメラパラメータを生成する。
このとき、引数のnewImgSizeに新しい解像度を指定する。
    frame_width = 1640
    frame_height = 1232
    new_camera_matrix, roi = cv2.getOptimalNewCameraMatrix(camera_matrix, dist_coeffs,
            (width, height), 0.0, (frame_width, frame_height))

戻り値のnew_camera_matrixに新しい解像度のカメラパラメータが返る。

確認

念の為、カメラパラメータの値を確認する。
確認方法は、前回のブログcv::calibrationMatrixValuesを使ってみる。
Fx, Fy, Cx, Cy はカメラパラメータを出力。

まずは、解像度を変更しない場合(640 x 480 → 640 x 480)
  Fx   =  550.963135
  Fy   =  551.406250
  Cx   =  317.560888
  Cy   =  235.058842
  FOVX =  60.295405
  FOVY =  47.039412
  Focal length    =  3.168038
  Principal point =  1.825975, 1.351588
  Aspect ratio    =  1.000804

次に、解像度を変更する場合(640 x 480 → 1640 x 1232)
  Fx   =  1411.843018
  Fy   =  1415.276123
  Cx   =  813.749768
  Cy   =  603.317729
  FOVX =  60.295406
  FOVY =  47.039410
  Focal length    =  3.168038
  Principal point =  1.825975, 1.351588
  Aspect ratio    =  1.002432

カメラパラメータが適切に変更されていることがわかった。

参考情報



2018年8月18日土曜日

OpenCVのcv::calibrationMatrixValuesで焦点距離を求める

OpenCVの"opencv_interactive-calibration"やカメラキャリブレーションで求めたカメラパラメータからmm単位での焦点距離などを求めたいときがある。
自作している3Dスキャナーでも使用したい。

(OenCVでは)カメラパラメータのfx, fyはピクセル単位で扱われるため、以下のように計算すればmm単位での焦点距離は求められる。

焦点距離(mm) = fx × W ÷ w
 fx : 焦点距離(pixel)
 W  : イメージセンサーの幅(mm)
 w  : 画像の幅(pixel)

電卓で計算すればよいのだけど、OpenCVにはcv::calibrationMatrixValuesのAPIで焦点距離を含むカメラのスペックを計算してくれる。

せっかくなので、このAPIをPythonで使って見ようと思う。

使い方

まず、カメラパラメータと画像の解像度を読み込む。
これは、"opencv_interactive-calibration"で出力したファイルから読み込んでいる。
fs = cv2.FileStorage("PathToCameraParameterFile", cv2.FILE_STORAGE_READ)
if fs.isOpened():
        width = (int)(camera_fs.getNode("cameraResolution").at(0).real())
        height = (int)(camera_fs.getNode("cameraResolution").at(1).real())
        camera_matrix = camera_fs.getNode("cameraMatrix").mat()


そして、calibrationMatrixValuesを呼び出す。
fovx, fovy, focal_length, principal_point, aspect_ratio = cv2.calibrationMatrixValues(camera_matrix,
            (width, height), aperture_width, aperture_height)

print("  FOVX = ", "{:.6f}".format(fovx))
print("  FOVY = ", "{:.6f}".format(fovy))
print("  Focal length    = ", "{:.6f}".format(focal_length))
print("  Principal point = ", "{:.6f}, {:6f}".format(*principal_point))
print("  Aspect ratio    = ", "{:.6f}".format(aspect_ratio))
引数には以下を指定する。
  • cameraMatrix  カメラパラメータ(先程ファイルから得たパラメータ)
  • imageSize    画像の解像度(先程ファイルから得たパラメータ, 単位:pixel)
  • apertureWidth    イメージセンサーの幅(単位:mm)
  • apertureHeight    イメージセンサーの高さ(単位:mm)
戻り値にはカメラのスペックが返る。
  • fovx    垂直画角(単位:度)
  • fovy    水平画角(単位:度)
  • focalLength    焦点距離(単位:mm)
  • principalPoint    主点(単位:mm)
  • aspectRatio    fy / fz

試してみる

PiCamera V2.1でカメラキャリブレーションを行った結果で試してみる。
キャリブレーションした結果は以下の通りだった(chArucoパターンでキャリブレーション)。
<cameraResolution>
  640 480</cameraResolution>
<cameraMatrix type_id="opencv-matrix">
  <rows>3</rows>
  <cols>3</cols>
  <dt>d</dt>
  <data>
    5.3447832911092576e+02 0. 3.1759911211440613e+02 0.
    5.3447832911092576e+02 2.3505836099223782e+02 0. 0. 1.</data></cameraMatrix>

cv::calibrationMatrixValuesでカメラのスペックを出力してみる。
イメージセンサーのサイズは、公式ドキュメントから3.68 x 2.76 mm を指定。
FOVX =  61.818399
FOVY =  48.360546
Focal length    =  3.073250
Principal point =  1.826195, 1.351586
Aspect ratio    =  1.000000

公式のドキュメントからと比較すると
  • Sensor image area    3.68 x 2.76 mm (4.6 mm diagonal)
  • Focal length    3.04 mm
  • Horizontal field of view    62.2 degrees
  • Vertical field of view    48.8 degrees
なので、まずまず良いカメラキャリブレーションができたかな?

2018年8月16日木曜日

OpenCVのcv::FileStorageをPythonで扱う

OpenCVでXML/YAML/JSONのデータ構造を扱う

OpenCVでcvパラメータを扱う場合、cv::FileStorageクラスが用意されている。XML, YAML, JSONの各フォーマットのファイルからパラメータの読み書きができる。

利点は、
  • C++で簡単にパラメータのファイルを扱うことができる
  • cv::Mat, cv::Size等のOpenCVの形式でパラメータを読み書きできること

カメラキャリブレーションのopencv_interactive-calibrationもXML形式のファイルを出力するので、パラメータを読む際は、cv::FileStorageクラスが利用できる。

自作中のPaspberry Piの3Dスキャナーでもcv::FileStorageクラスを利用してパラメータの読み込みを行う予定。


cv::FileStorageをPythonで使う

手書きでパラメータのファイルを作成するより、ファイルを自動生成できたほうが良いと考え、Pythonでツールを作成したのだが、思ったように使えないことが判明した。
(Pythonでサクッと作ってしまえば良いと思っていた)


Pythonで扱う場合の問題(デメリット)

配列(シーケンス)のデータを出力できない

例えば、C++で解像度などcv::Sizeで保持されるデータを出力した場合、シーケンスとして出力される。

<cameraResolution>
  640 480</cameraResolution>

しかし、Pythonにはcv::Sizeはなく、タプルで扱うことになるが、タプルはcv::Matに変換されてしまうため、シーケンスとして出力されず、以下のように出力されてしまう。

<cameraResolution type_id="opencv-matrix">
  <rows>2</rows>
  <cols>1</cols>
  <dt>d</dt>
  <data>
    640. 480.</data></cameraResolution>

これは、Pythonの場合、タプル、リストはすべてcv::Matにバインディングされるためである。読み込み時もPythonであれば、問題はないのだが、C++の場合、cv::Matにしか使えなくなってしまう。


マッピングのデータを出力できない

C++では<< operatorで"{", "}"を出力することで、マッピングされたデータを出力できる。
(同じように"[", "]"でシーケンスのデータを出力できる)
詳細は、OpenCVのドキュメントを参照。

しかし、Pythonには<< operatorはバインディングされていないため出力できない。


代替え方法

もともと、cv::FileStorageはC++でパラメータを扱いやすくするためであり、Pythonで扱うこと自体があまり良い考えでないと思う。
Pythonで扱うのであれば、PyYAMLを使ったほうが良く、Pythonで出力したデータをC++のcv::FileStorageで読み込むこともできるはず。
ただし、PyYAMLではcv::Matの形式では出力できなくなるが、その場合は自作してしまえば良いとなる。