本記事はアフィリエイト広告(PR)を含みます
以前の記事ではRANSACアルゴリズムを使った直線検出について解説しました。
実際に直線検出を行う場合、ハフ変換で済んでしまう場合も多いので、別の例でもRANSACの効果を確認してみます。
そこで、今回は楕円検出を行い、RANSACアルゴリズムを適用した場合と適用しなかった場合を比較してみたいと思います。
1. 楕円検出でRANSACアルゴリズムが有効な理由
楕円フィッティングは与えられた点群に対して最も適合する楕円を求めます。
しかし、実際の画像から取得したエッジ点にはノイズが多く含まれている場合があります。
例えば、このような歯車の画像から最外径の楕円を検出するとします。

この場合、歯車の一部が破損していて綺麗にエッジが取れません。

そして、そのエッジから楕円フィッティングを行うと右側のノイズに引っ張られてしまいます。

そこでRANSACを使用することにより、エッジから外れ値を除外して楕円フィッティングを行うことができ、歯車本来の外周に近い楕円を求めやすくなります。
2. エッジ検出
まずはエッジ検出用の関数を実装します。
def detect_edges(
image,
threshold1=50,
threshold2=150
):
"""
入力画像からCannyエッジを検出する。
Parameters
----------
image : numpy.ndarray
入力画像
threshold1 : int
Cannyの下側閾値
threshold2 : int
Cannyの上側閾値
Returns
-------
edge : numpy.ndarray
エッジ画像
"""
# グレースケール化
gray = cv2.cvtColor(
image,
cv2.COLOR_BGR2GRAY
)
# ノイズ低減
blur = cv2.GaussianBlur(
gray,
(5, 5),
0
)
# Cannyエッジ検出
edge = cv2.Canny(
blur,
threshold1,
threshold2
)
return edge
流れとしては、
- グレースケール変換
- ガウシアンフィルタによるノイズ除去
- Cannyによるエッジ検出
となります。
Cannyに入力する閾値は適宜調整してください。
3. 特徴点抽出
次に、検出したエッジから外周の特徴点だけを抽出します。
エッジデータに対してそのままRANSACで外れ値を除外しようとしても、検出したい楕円が指定されていないので意図しない結果になってしまう可能性があります。
そこで、ある程度検出したい領域の特徴点を抽出しておく必要があります。
def detect_edge_points(
edge,
radius=470,
radius_range=30,
angle_step=1.0
):
"""
画像中心から360度探索し、
各角度について指定半径付近の
エッジ点を1点取得する。
Parameters
----------
edge : numpy.ndarray
エッジ画像
radius : int
基準半径
radius_range : int
半径方向の探索範囲
angle_step : float
角度の刻み幅
Returns
-------
points : numpy.ndarray
検出した点群 (x, y)
"""
height, width = edge.shape
# 画像中心
center_x = width / 2.0
center_y = height / 2.0
detected_points = []
# 基準半径に近い順に探索するための
# オフセットを作成
radius_offsets = [0]
for d in range(1, radius_range + 1):
radius_offsets.append(-d)
radius_offsets.append(d)
# 0~360度を探索
angles = np.arange(
0,
360,
angle_step
)
for angle in angles:
theta = np.deg2rad(angle)
# 半径方向を探索
for offset in radius_offsets:
current_radius = radius + offset
x = int(round(
center_x
+ current_radius * np.cos(theta)
))
y = int(round(
center_y
+ current_radius * np.sin(theta)
))
# 画像範囲外ならスキップ
if not (
0 <= x < width
and 0 <= y < height
):
continue
# エッジを検出
if edge[y, x] > 0:
detected_points.append(
(x, y)
)
# この角度では1点だけ取得
break
return np.array(
detected_points,
dtype=np.float32
)
この関数では画像中心から360°方向に探索し、指定した半径付近にあるエッジ点を各角度から1点ずつ取得します。
radius=470としているのは画像中心から歯車の外周までの半径がだいたい470pxだったので今回は470にしてしています。
エッジ画像に対する特徴点はこのようになりました。
右側のノイズを拾ってしまっているのが分かります。

4. RANSACアルゴリズムによる外れ値除去
検出した特徴点にRANSACを適用して外れ値を除去します。
# -----------------------------
# 点と楕円との距離
# -----------------------------
def ellipse_distance(points, ellipse):
"""
各点と楕円との半径方向の距離を
ピクセル単位で求める。
"""
(cx, cy), (w, h), angle = ellipse
if w <= 0 or h <= 0:
return np.full(
len(points),
np.inf
)
# 長径・短径の半径
a = w / 2.0
b = h / 2.0
# 楕円中心を原点にする
x = points[:, 0] - cx
y = points[:, 1] - cy
# 楕円の回転を打ち消す
theta = np.deg2rad(angle)
cos_t = np.cos(theta)
sin_t = np.sin(theta)
xr = (
x * cos_t
+ y * sin_t
)
yr = (
-x * sin_t
+ y * cos_t
)
# 点の中心からの距離
point_radius = np.sqrt(
xr ** 2 + yr ** 2
)
# 点の角度
theta = np.arctan2(
yr,
xr
)
# その角度における楕円の半径
ellipse_radius = (
a * b
/ np.sqrt(
(b * np.cos(theta)) ** 2
+ (a * np.sin(theta)) ** 2
)
)
# 楕円との半径方向距離
distance = np.abs(
point_radius
- ellipse_radius
)
return distance
# -----------------------------
# RANSACによる外れ値除去
# -----------------------------
def ransac_filter_points(
points,
iterations=3000,
distance_threshold=1,
sample_size=5
):
"""
点群にRANSACを適用し、
最良の仮楕円に対するインライア点群を返す。
Parameters
----------
points : numpy.ndarray
入力点群 (x, y)
iterations : int
RANSACの試行回数
distance_threshold : float
インライアと判定する距離の閾値
sample_size : int
仮楕円作成に使用する点数
Returns
-------
inlier_points : numpy.ndarray
RANSACでインライアと判定された点群
"""
points = np.asarray(
points,
dtype=np.float32
)
num_points = len(points)
if num_points < sample_size:
raise ValueError(
"入力点数がsample_sizeより少ないです。"
)
if sample_size < 5:
raise ValueError(
"楕円モデルの作成には5点以上必要です。"
)
best_inlier_mask = None
best_inlier_count = 0
# -----------------------------
# RANSAC
# -----------------------------
for _ in range(iterations):
# ランダムに点を選択
indices = np.random.choice(
num_points,
sample_size,
replace=False
)
sample_points = points[
indices
]
# 仮の楕円モデルを作成
try:
ellipse = cv2.fitEllipse(
sample_points.reshape(
-1,
1,
2
)
)
except cv2.error:
continue
# 全点と仮楕円との距離
distances = ellipse_distance(
points,
ellipse
)
# インライア判定
inlier_mask = (
distances
<= distance_threshold
)
inlier_count = np.count_nonzero(
inlier_mask
)
# 最もインライア数が多い結果を保存
if inlier_count > best_inlier_count:
best_inlier_count = (
inlier_count
)
best_inlier_mask = (
inlier_mask
)
if best_inlier_mask is None:
raise RuntimeError(
"RANSACで有効なモデルを"
"作成できませんでした。"
)
# インライアだけを取得
inlier_points = points[
best_inlier_mask
]
return inlier_points
この関数では取得した点群からランダムに点を選択して仮の楕円を作り、その楕円に近い点をインライアと判定します。
楕円フィッティングを行うには最低5点必要なので、まずは点群の中から5点選択します。

選択した5点からcv2.fitEllipse()で仮の楕円を作成します。
楕円は一般的に次の2次方程式で表すことができます。

次に、仮楕円の中心と楕円以外の各点との距離を算出します。
その点と同じ角度Θにおける楕円の半径を求めます。
最後に両者の差分を取ります。
今回のコードではこのdが
distance_threshold=1
以下ならインライア、これより大きければアウトライアと判定します。

これを指定回数繰り返し、最もインライア数が多かった仮楕円のインライア点群を返します。

実際に先ほどの特徴点に対してRANSACアルゴリズムを適用した結果はこちら。

5. 楕円フィッティング
最後に、RANSACによって外れ値を除去した特徴点に対して楕円フィッティングを行います。
if len(inlier_points) < 5:
raise RuntimeError(
"楕円フィッティングには"
"5点以上必要です。"
)
final_ellipse = cv2.fitEllipse(
inlier_points.reshape(
-1,
1,
2
)
)
では、RANSACを適用した場合と適用しなかった場合で比較してみましょう。


特徴点からそのまま楕円フィッティングした画像はノイズに引っ張られて右下が外れてしまっているのに対し、RANSACアルゴリズムで外れ値を除去した特徴点からフィッティングしたほうは外周にフィットしていることが分かります。
6. 補足
OpenCVには円検出用の関数(HoughCircles())は実装されていますが、楕円を直接検出することはできません。
一応自分で実装すればハフ変換でも楕円検出は可能ですが、計算量やメモリ使用量が大きくなるという課題があります。
今回のケースではある程度位置と形状が分かっていることを想定しているので、RANSACアルゴリズムのほうが有効です。
ただ、画像全体から複数の楕円を検出したい場合などはハフ変換が有効な場合があります。
7. まとめ
前回解説した直線検出ではRANSACが有効な例が思いつかなかったので、今回は歯車にRANSACを適用して楕円フィッティングを行う例を紹介しました。
検査の現場でもこのような円形の形状や位置を検出する場面は多いので、RANSACアルゴリズムを利用して外れ値の影響を抑えることで、より安定した形状検出につなげることができるのではないかと考えています。
今回は以上です。
8. 参考サイト
楕円当てはめのPython実装
https://qiita.com/Hiroaki-K4/items/e29cc422d7205cf19d05



コメント