Newer
Older
RobotCar / src / pc / vision / line_detector.py
"""
line_detector
カメラ画像から黒線の位置を検出するモジュール
近方・遠方の2領域で重心 x 座標を算出する
"""

from dataclasses import dataclass

import cv2
import numpy as np

from common import config

# ガウシアンブラーのカーネルサイズ
BLUR_KERNEL_SIZE: int = 5

# 二値化の閾値(黒線検出用,この値以下を黒とみなす)
BINARY_THRESHOLD: int = 80

# 近方領域の y 範囲(画像下部)
NEAR_Y_START: int = int(config.FRAME_HEIGHT * 0.7)
NEAR_Y_END: int = config.FRAME_HEIGHT

# 遠方領域の y 範囲(画像上部)
FAR_Y_START: int = int(config.FRAME_HEIGHT * 0.3)
FAR_Y_END: int = int(config.FRAME_HEIGHT * 0.5)


@dataclass
class LineDetectResult:
    """線検出の結果を格納するデータクラス

    Attributes:
        near_x: 近方領域の線の重心 x 座標(未検出時は None)
        far_x: 遠方領域の線の重心 x 座標(未検出時は None)
        near_error: 近方偏差(画像中心からのずれ)
        far_error: 遠方偏差(画像中心からのずれ)
        binary_image: 二値化後の画像(デバッグ用)
    """
    near_x: float | None
    far_x: float | None
    near_error: float
    far_error: float
    binary_image: np.ndarray | None


def detect_line(frame: np.ndarray) -> LineDetectResult:
    """画像から黒線の位置を検出する

    Args:
        frame: BGR 形式のカメラ画像

    Returns:
        線検出の結果
    """
    # グレースケール変換
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    # ガウシアンブラー
    blurred = cv2.GaussianBlur(
        gray,
        (BLUR_KERNEL_SIZE, BLUR_KERNEL_SIZE),
        0,
    )

    # 二値化(黒線を白,背景を黒に反転)
    _, binary = cv2.threshold(
        blurred, BINARY_THRESHOLD, 255,
        cv2.THRESH_BINARY_INV,
    )

    # 画像の中心 x 座標
    center_x = config.FRAME_WIDTH / 2.0

    # 近方領域の重心算出
    near_roi = binary[NEAR_Y_START:NEAR_Y_END, :]
    near_x = _calc_centroid_x(near_roi)

    # 遠方領域の重心算出
    far_roi = binary[FAR_Y_START:FAR_Y_END, :]
    far_x = _calc_centroid_x(far_roi)

    # 偏差の計算
    near_error = 0.0
    if near_x is not None:
        near_error = (center_x - near_x) / center_x

    far_error = 0.0
    if far_x is not None:
        far_error = (center_x - far_x) / center_x

    return LineDetectResult(
        near_x=near_x,
        far_x=far_x,
        near_error=near_error,
        far_error=far_error,
        binary_image=binary,
    )


def _calc_centroid_x(
    roi: np.ndarray,
) -> float | None:
    """ROI 内の白ピクセルの重心 x 座標を算出する

    Args:
        roi: 二値化された領域画像

    Returns:
        重心の x 座標,白ピクセルがない場合は None
    """
    moments = cv2.moments(roi)
    if moments["m00"] == 0:
        return None
    return moments["m10"] / moments["m00"]