OpenCV 5.0.0
Open Source Computer Vision
読み込み中...
検索中...
見つかりません
🤖 AIによる機械翻訳(非公式) — これは OpenCV 5.0.0 公式リファレンス(英語)を AI (Claude) で自動翻訳したものです。訳に誤りを含む場合があります。正確な情報は 公式英語版(原文) を参照してください。
エピポーラ幾何

目的

本節では、

  • 多視点幾何の基礎について学ぶ
  • エピポール、エピポーラ線、エピポーラ拘束などが何であるかを見ていく。

基本概念

ピンホールカメラで画像を撮影すると、重要な情報、すなわち画像の奥行き(depth)が失われる。言い換えると、3Dから2Dへの変換であるため、画像中の各点がカメラからどれだけ離れているかが分からなくなる。そこで、これらのカメラを使って奥行き情報を求められるかどうかが重要な問題となる。その答えは、複数のカメラを使うことである。我々の目も同様の仕組みで働いており、2台のカメラ(両目)を使う。これをステレオビジョン(stereo vision)と呼ぶ。では、この分野でOpenCVが何を提供しているか見ていこう。

(Gary Bradsky 著の Learning OpenCV には、この分野に関する多くの情報が記載されている。)

奥行き画像に入る前に、まずマルチビュー幾何における基本概念をいくつか理解しよう。本節ではエピポーラ幾何(epipolar geometry)を扱う。下の図を見てほしい。2台のカメラで同じシーンを撮影する基本的な構成を示している。

image

左のカメラだけを使う場合、画像中の点 \(x\) に対応する3D点を求めることはできない。なぜなら、直線 \(OX\) 上のすべての点が画像平面上の同じ点に投影されるからである。しかし、右の画像も考えてみよう。すると、直線 \(OX\) 上の異なる点は、右の平面上の異なる点( \(x'\) )に投影される。よって、この2枚の画像があれば、正しい3D点を三角測量で求めることができる。これが全体の考え方である。

\(OX\) 上の異なる点の投影は、右の平面上で1本の直線(直線 \(l'\) )を形成する。これを点 \(x\) に対応するエピライン(epiline)と呼ぶ。つまり、右の画像で点 \(x\) を見つけるには、このエピラインに沿って探索すればよい。点はこの直線上のどこかにあるはずである(別の画像で対応点を見つけるには、画像全体を探索する必要はなく、エピラインに沿って探索すればよい、と考えるとよい。これにより性能と精度が向上する)。これをエピポーラ拘束(Epipolar Constraint)と呼ぶ。同様に、すべての点はもう一方の画像に対応するエピラインを持つ。平面 \(XOO'\) をエピポーラ平面(Epipolar Plane)と呼ぶ。

\(O\) と \(O'\) はカメラ中心である。上記の構成から分かるように、右カメラ \(O'\) の投影は、左の画像上の点 \(e\) に見える。これをエピポール(epipole)と呼ぶ。エピポールは、カメラ中心を通る直線と画像平面との交点である。同様に \(e'\) は左カメラのエピポールである。場合によっては、エピポールが画像内で見つけられないことがあり、画像の外側にあることもある(これは一方のカメラがもう一方を見ていないことを意味する)。

すべてのエピラインはそのエピポールを通る。よって、エピポールの位置を求めるには、多くのエピラインを求めてそれらの交点を求めればよい。

そこで本セッションでは、エピポーラ線とエピポールを求めることに焦点を当てる。ただし、それらを求めるには、さらに2つの要素、基礎行列(Fundamental Matrix, F)基本行列(Essential Matrix, E)が必要である。基本行列は並進と回転に関する情報を含んでおり、これらはグローバル座標系における1台目のカメラに対する2台目のカメラの位置を表す。下の図を見てほしい(画像提供: Gary Bradsky 著 Learning OpenCV)。

image

しかし、測定はピクセル座標で行うほうが好ましい。基礎行列は基本行列と同じ情報に加えて、両カメラの内部パラメータに関する情報も含んでおり、2台のカメラをピクセル座標で関連付けることができる。(平行化された画像を使い、焦点距離で割って点を正規化すれば、 \(F=E\) となる)。簡単に言うと、基礎行列 F は、一方の画像中の点を、もう一方の画像中の直線(エピライン)に対応付ける。これは両画像の対応点から計算される。基礎行列を求めるには、このような対応点が最低8点必要である(8点アルゴリズムを使う場合)。より多くの点を使うほうが望ましく、より頑健な結果を得るには RANSAC を用いる。

コード

まず、基礎行列を求めるために、2枚の画像間でできるだけ多くの対応を見つける必要がある。このために、FLANN ベースのマッチャーと比率テストを用いた SIFT 記述子を使う。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img1 = cv.imread('myleft.jpg', cv.IMREAD_GRAYSCALE) #queryimage # left image
img2 = cv.imread('myright.jpg', cv.IMREAD_GRAYSCALE) #trainimage # right image
sift = cv.SIFT_create()
# find the keypoints and descriptors with SIFT
kp1, des1 = sift.detectAndCompute(img1,None)
kp2, des2 = sift.detectAndCompute(img2,None)
# FLANN parameters
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
search_params = dict(checks=50)
flann = cv.FlannBasedMatcher(index_params,search_params)
matches = flann.knnMatch(des1,des2,k=2)
pts1 = []
pts2 = []
# ratio test as per Lowe's paper
for i,(m,n) in enumerate(matches):
if m.distance < 0.8*n.distance:
pts2.append(kp2[m.trainIdx].pt)
pts1.append(kp1[m.queryIdx].pt)
Flann-based descriptor matcher.
Definition features.hpp:1260
Mat imread(const String &filename, int flags=IMREAD_COLOR_BGR)
Loads an image from a file.

これで両画像から得た最良の対応のリストが手に入った。では、基礎行列を求めよう。

pts1 = np.int32(pts1)
pts2 = np.int32(pts2)
F, mask = cv.findFundamentalMat(pts1,pts2,cv.FM_LMEDS)
# We select only inlier points
pts1 = pts1[mask.ravel()==1]
pts2 = pts2[mask.ravel()==1]
Mat findFundamentalMat(InputArray points1, InputArray points2, int method, double ransacReprojThreshold, double confidence, int maxIters, OutputArray mask=noArray())
Calculates a fundamental matrix from the corresponding points in two images.

次にエピラインを求める。1枚目の画像中の点に対応するエピラインは、2枚目の画像上に描画される。よって、ここでは正しい画像を指定することが重要である。直線の配列が得られる。そこで、これらの直線を画像上に描画する新しい関数を定義する。

def drawlines(img1,img2,lines,pts1,pts2):
''' img1 - image on which we draw the epilines for the points in img2
lines - corresponding epilines '''
r,c = img1.shape
img1 = cv.cvtColor(img1,cv.COLOR_GRAY2BGR)
img2 = cv.cvtColor(img2,cv.COLOR_GRAY2BGR)
for r,pt1,pt2 in zip(lines,pts1,pts2):
color = tuple(np.random.randint(0,255,3).tolist())
x0,y0 = map(int, [0, -r[2]/r[1] ])
x1,y1 = map(int, [c, -(r[2]+r[0]*c)/r[1] ])
img1 = cv.line(img1, (x0,y0), (x1,y1), color,1)
img1 = cv.circle(img1,tuple(pt1),5,color,-1)
img2 = cv.circle(img2,tuple(pt2),5,color,-1)
return img1,img2
void cvtColor(InputArray src, OutputArray dst, int code, int dstCn=0, AlgorithmHint hint=cv::ALGO_HINT_DEFAULT)
Converts an image from one color space to another.
void line(InputOutputArray img, Point pt1, Point pt2, const Scalar &color, int thickness=1, int lineType=LINE_8, int shift=0)
Draws a line segment connecting two points.
void circle(InputOutputArray img, Point center, int radius, const Scalar &color, int thickness=1, int lineType=LINE_8, int shift=0)
Draws a circle.

では、両方の画像でエピラインを求めて描画する。

# Find epilines corresponding to points in right image (second image) and
# drawing its lines on left image
lines1 = cv.computeCorrespondEpilines(pts2.reshape(-1,1,2), 2,F)
lines1 = lines1.reshape(-1,3)
img5,img6 = drawlines(img1,img2,lines1,pts1,pts2)
# Find epilines corresponding to points in left image (first image) and
# drawing its lines on right image
lines2 = cv.computeCorrespondEpilines(pts1.reshape(-1,1,2), 1,F)
lines2 = lines2.reshape(-1,3)
img3,img4 = drawlines(img2,img1,lines2,pts2,pts1)
plt.subplot(121),plt.imshow(img5)
plt.subplot(122),plt.imshow(img3)
plt.show()
void computeCorrespondEpilines(InputArray points, int whichImage, InputArray F, OutputArray lines)
For points in an image of a stereo pair, computes the corresponding epilines in the other image.

以下が得られる結果である。

image

左の画像では、すべてのエピラインが右側の画像外の1点に収束しているのが分かる。その交点がエピポールである。

より良い結果を得るには、解像度が高く、非平面的な点が多い画像を使うべきである。

演習

  1. 重要なトピックの一つにカメラの前進運動がある。この場合、エピポールは両画像で同じ位置に見え、エピラインは固定点から放射状に出る。この議論を参照
  2. 基礎行列の推定は、対応の品質や外れ値などに敏感である。選択したすべての対応が同一平面上にある場合、結果はさらに悪化する。この議論を確認