【Python】RANSACアルゴリズムによる楕円検出【OpenCV】

【Python】RANSACアルゴリズムによる楕円検出【OpenCV】

本記事はアフィリエイト広告(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

流れとしては、

  1. グレースケール変換
  2. ガウシアンフィルタによるノイズ除去
  3. 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次方程式で表すことができます。

Ax2+Bxy+Cy2+Dx+Ey+F=0Ax^2 + Bxy + Cy^2 + Dx + Ey + F = 0

次に、仮楕円の中心と楕円以外の各点との距離を算出します。

rp=xr2+yr2r_p = \sqrt{x_r^2 + y_r^2}

その点と同じ角度Θにおける楕円の半径を求めます。

r(θ)=ab(bcosθ)2+(asinθ)2r(\theta)= \frac{ab} {\sqrt{(b\cos\theta)^2+(a\sin\theta)^2}}

最後に両者の差分を取ります。

d=|rpr(θ)|d = |r_p – r(\theta)|

今回のコードではこの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アルゴリズムあり
RANSACアルゴリズムなし

特徴点からそのまま楕円フィッティングした画像はノイズに引っ張られて右下が外れてしまっているのに対し、RANSACアルゴリズムで外れ値を除去した特徴点からフィッティングしたほうは外周にフィットしていることが分かります。

スポンサーリンク

6. 補足

OpenCVには円検出用の関数(HoughCircles())は実装されていますが、楕円を直接検出することはできません。
一応自分で実装すればハフ変換でも楕円検出は可能ですが、計算量やメモリ使用量が大きくなるという課題があります。
今回のケースではある程度位置と形状が分かっていることを想定しているので、RANSACアルゴリズムのほうが有効です。

ただ、画像全体から複数の楕円を検出したい場合などはハフ変換が有効な場合があります。

スポンサーリンク

7. まとめ

前回解説した直線検出ではRANSACが有効な例が思いつかなかったので、今回は歯車にRANSACを適用して楕円フィッティングを行う例を紹介しました。

検査の現場でもこのような円形の形状や位置を検出する場面は多いので、RANSACアルゴリズムを利用して外れ値の影響を抑えることで、より安定した形状検出につなげることができるのではないかと考えています。

今回は以上です。

スポンサーリンク

8. 参考サイト

楕円当てはめのPython実装
https://qiita.com/Hiroaki-K4/items/e29cc422d7205cf19d05

9. 関連書籍

スポンサーリンク

コメント

タイトルとURLをコピーしました