Newer
Older
RobotCar / docs / 03_TECH / TECH_04_線検出精度向上方針.txt
========================================================================
線検出精度向上方針 (Line Detection Accuracy Improvement)
========================================================================


1. 概要 (Overview)
------------------------------------------------------------------------

    1-0. 目的

    黒線の検出精度がコースアウトの有無を左右する最重要ファクターで
    あることを示し,現在の手法の課題と改善手法の選択肢を定義する.

    1-1. 線検出がコースアウトの最重要ファクターである理由

    自律走行の制御フローは以下の通りである.

        カメラ画像 → 線検出 → position_error/heading/curvature
                  → PD 制御 → 操舵量

    PD 制御以降はいかにパラメータを調整しても,入力である
    position_error が誤っていれば正しい操舵は得られない.
    すなわち,線検出の精度がシステム全体の制御品質の上限を決める.

    ・線が正しく検出できる → 正確な偏差 → 正確な操舵
    ・線の検出が崩れる   → 誤った偏差 → 反対方向への操舵 → コースアウト

    速度を上げるほど 1 フレームあたりの移動距離が増えるため,
    検出の崩れが即コースアウトにつながる.高速化と精度向上は
    不可分の関係にある.

    1-2. パイプライン構成

    線検出パイプラインは 4 段階で構成される.
    各段階ごとに手法を選択・組み合わせることで精度を向上させる.

        [Stage 1] 前処理(照明正規化)
        [Stage 2] 二値化(線と背景の分離)
        [Stage 3] 後処理(穴埋め・ノイズ除去・幅正規化)
        [Stage 4] 特徴抽出(中心線 → 多項式フィッティング)


2. 検出対象の特性 (Target Characteristics)
------------------------------------------------------------------------

    2-1. 黒線の物理的特性

    本システムの走行コースは,道路の白線に近い性質を持つ黒線である.

    ・形状: 幅一定の単一直線(カーブも含むが幾何学的に連続)
    ・色: 高輝度の床面に対して明確に暗い(理想条件下)
    ・連続性: 途切れがなく,2次多項式で十分近似できる
    ・幅: 画像上で数ピクセル〜数十ピクセル程度

    2-2. 理想条件と実環境の乖離

    理想条件(均一な拡散光,均質な床面)では固定閾値の2値化でも
    明確に検出できる.しかし実環境では以下の要因が存在する.

    ・光源の影響
        - 蛍光灯・LED 照明による局所的な輝度ムラ
        - 窓からの外光による時間変動
        - カメラの自動露出による全体輝度の変動

    ・影の影響
        - ロボット車体自体が床に影を落とす
        - 床の継ぎ目・段差による輝度の乱れ

    ・床面の影響
        - 床材の光沢による反射スポット
        - 黒線上の光沢(テープの反射)

    これらにより,黒線と背景の輝度差が局所的に縮小・逆転し,
    固定閾値の2値化では正しい線形状を得られないことがある.

    2-3. カメラ高さ・線幅の固定による幾何学的制約

    本システムでは以下の2点が実走行中に変化しない.

    ・カメラの地面からの高さ(固定マウント)
    ・黒線の実際の幅(コース仕様による固定値)

    透視投影の関係から,行(y 座標)が変わると
    カメラから黒線までの距離が変化し,画像上の線幅(ピクセル)も変わる.

        近距離(画像下端,y = Y_END):   線幅 = width_near [px]
        遠距離(画像上端,y = Y_START): 線幅 = width_far  [px]

    この関係は画像の y 座標を入力として線形補間で近似できる.

        expected_width(y) = width_near
            + (width_far - width_near) * (Y_END - y) / (Y_END - Y_START)

    この既知の幾何学情報を利用することで,以下が可能になる.

    ・各行の「正常な線幅」の範囲を事前に計算できる
    ・陰による線幅の広がりを行ごとに検出できる
    ・モルフォロジーパラメータ(Black-hat カーネル,等方クロージングサイズ等)
      を線幅から自動的に決定でき,手動調整の余地を減らせる

    ■ width_near / width_far の測定方法

    実際のカメラ映像の二値化画像を観察し,
    画像下端付近の行と上端付近の行それぞれで白ピクセルの幅を計測する.
    計測後はコードまたはパラメータファイルに固定値として記録する.

    2-4. 実環境で発生する2つの典型的な劣化

    ■ 穴(光による欠損)

    黒線テープの表面が光を反射し,反射部分の輝度が背景と同程度に
    なるため,二値化後に線の中央が白抜け(穴)になる現象.
    多項式フィッティングの点が欠損し,検出が不安定になる.

    ■ 広がり(陰による膨張)

    車体の影や照明ムラにより黒線の周囲の床が暗くなり,
    二値化後に線の幅が実際より広がる現象.
    広がった行はピクセル数が多いためフィッティングを支配し,
    線の位置がずれる原因になる.


3. 現在の手法と限界 (Current Approach and Limitations)
------------------------------------------------------------------------

    3-1. 現在の処理パイプライン

    ・Stage 1(前処理): グレースケール → CLAHE → ガウシアンブラー
    ・Stage 2(二値化): 固定閾値(BINARY_INV)
    ・Stage 3(後処理): オープニング → 横方向クロージング
    ・Stage 4(特徴抽出): 全白ピクセルに2次多項式フィッティング

    詳細は `TECH_01_操舵量計算仕様.txt` の
    「2. 画像処理パイプライン」を参照する.

    3-2. 現在の対策とその効果・限界

    ■ CLAHE(局所コントラスト強調)

    ・効果: 局所的な輝度ムラを補正し,黒線と背景の差を拡大する
    ・限界: 輝度差が完全になくなった領域では効果がない

    ■ 固定閾値二値化

    ・効果: 実装がシンプルで処理が高速
    ・限界: 1つの閾値で画像全体を判定するため,
        照明ムラが大きい場合に不均一な分離が生じる
        - 閾値を低くする → 反射スポットが黒線として誤検出
        - 閾値を高くする → 影の下の黒線が検出されない

    ■ モルフォロジー処理

    ・効果: 孤立ノイズの除去(オープニング),
        線の途切れの補間(横方向クロージング)
    ・限界: 線の位置が大きくずれたノイズには対処できない.
        誤検出された大きな塊はフィッティングを大幅に歪める

    ■ 全白ピクセルフィッティング

    ・効果: 実装が単純
    ・限界: 陰で幅が広がった行はピクセル数が多く,
        フィッティングへの寄与が大きいため線の位置がずれる

    3-3. 残存する課題

    ・強い照明ムラ環境では固定閾値が不安定になりやすい
    ・カメラの自動露出でシーンが変わると適切な閾値が変動する
    ・黒線上に光沢がある場合,線の中央が白抜けして線幅が細くなる
    ・陰による幅の広がりが全白ピクセルフィッティングを歪める


4. Stage 1: 前処理の手法比較 (Pre-processing)
------------------------------------------------------------------------

    4-0. 目的

    照明ムラや輝度変動の影響を低減し,後段の二値化を安定させる.

    4-1. CLAHE(現在の手法)

    局所領域ごとにヒストグラム均等化を行い,コントラストを強調する.

    ・計算量: 低
    ・穴への効果: △(反射が強い場合は輝度差を復元できない)
    ・陰への効果: ○(局所コントラスト向上で境界が明確になる)
    ・実装: `cv2.createCLAHE(clipLimit, tileGridSize)`
    ・備考: 現在の手法.照明ムラには有効だが根本的な正規化ではない

    4-2. 背景除算正規化

    画像を大きなカーネルでぼかした画像で割り,
    照明の勾配(低周波成分)を除去する.

    ・計算量: 低(ガウシアンブラー1回 + 除算)
    ・穴への効果: ○(局所的な高輝度を正規化できる)
    ・陰への効果: ◎(照明勾配が除去されるため暗い領域も正規化)
    ・実装:

        blur_bg = cv2.GaussianBlur(gray, (ksize, ksize), 0)
        normalized = (gray.astype(np.float32) * 255.0
                      / (blur_bg.astype(np.float32) + 1.0))
        normalized = np.clip(normalized, 0, 255).astype(np.uint8)

    ・パラメータ: ksize は線幅の 10 倍程度(大きいほど広い照明ムラに対応)
    ・備考: 照明の勾配が緩やかな場合に最も効果的.
        急激な明暗境界(影の縁)には効きにくい

    4-3. Black-hat 変換

    モルフォロジーのクロージング結果から原画像を引くことで,
    「背景より暗い構造」だけを直接抽出する.

    ・計算量: 低(モルフォロジー演算1回)
    ・穴への効果: ○(暗い構造として線全体を検出できる)
    ・陰への効果: ◎(背景の輝度変動が除去される)
    ・実装:

        kernel = cv2.getStructuringElement(
            cv2.MORPH_ELLIPSE, (ksize, ksize))
        blackhat = cv2.morphologyEx(
            gray, cv2.MORPH_BLACKHAT, kernel)

    ・パラメータ: ksize は線幅の 2〜3 倍(線より大きく,
        影より小さいサイズ)
    ・備考: 黒線検出に原理的に最もフィットした手法.
        「背景に対してどれだけ暗いか」を直接出力するため,
        背景の絶対輝度に依存しない

    4-4. ホモモルフィックフィルタ

    画像の対数を取り,FFT で低周波(照明成分)と
    高周波(反射率成分)を分離し,照明成分を抑制する.

    ・計算量: 中(FFT + 逆 FFT)
    ・穴への効果: ○(照明成分を分離するため反射の影響が減る)
    ・陰への効果: ◎(照明成分の除去が原理的に正確)
    ・実装: `np.fft.fft2` + ハイパスフィルタ + `np.fft.ifft2`
    ・備考: 理論的には最も正確だが,FFT の計算量が
        リアルタイム処理に影響する可能性がある.
        320x240 であれば実用範囲内

    4-5. LAB 色空間 L チャネル

    BGR → LAB 変換し,知覚均等な輝度チャネル L を使用する.

    ・計算量: 極低(色変換のみ)
    ・穴への効果: △(グレースケールと大差ない)
    ・陰への効果: △(輝度の表現が若干改善される程度)
    ・実装: `cv2.cvtColor(frame, cv2.COLOR_BGR2LAB)[:,:,0]`
    ・備考: 単体での効果は限定的.他の手法と組み合わせて使う


5. Stage 2: 二値化の手法比較 (Binarization)
------------------------------------------------------------------------

    5-0. 目的

    前処理後の画像から黒線と背景を分離する.

    5-1. 固定閾値(現在の手法)

    1つの閾値で画像全体を二値化する.

    ・計算量: 極低
    ・穴への効果: ✕(反射で輝度が上がった部分は検出できない)
    ・陰への効果: ✕(影で暗くなった床を誤検出する)
    ・実装: `cv2.threshold(src, thresh, 255, THRESH_BINARY_INV)`
    ・備考: Stage 1 で照明が正規化されていれば十分機能する.
        Stage 1 が弱い場合は照明ムラに脆弱

    5-2. 大津の方法(Otsu)

    ヒストグラムからクラス間分散を最大化する閾値を自動決定する.

    ・計算量: 極低(ヒストグラム計算のみ)
    ・穴への効果: ✕(グローバル閾値のため局所問題に弱い)
    ・陰への効果: △(全体の明暗変化には追従する)
    ・実装: `cv2.threshold(src, 0, 255,
        THRESH_BINARY_INV + THRESH_OTSU)`
    ・備考: 閾値の手動調整が不要になる利点がある.
        ただし局所的な照明ムラには固定閾値と同様に弱い

    5-3. 適応的閾値(ガウシアン加重平均)

    各ピクセルの周囲 blockSize x blockSize 領域の
    ガウシアン加重平均から閾値を算出する.

    ・計算量: 低(積分画像ベース)
    ・穴への効果: ○(局所的に閾値が変わるため反射領域にも対応)
    ・陰への効果: ○(影のある領域で閾値が下がるため追従できる)
    ・実装:

        cv2.adaptiveThreshold(
            src, 255,
            cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
            cv2.THRESH_BINARY_INV,
            blockSize, C)

    ・パラメータ: blockSize(局所領域サイズ,奇数),
        C(閾値からの減算定数)
    ・備考: 局所照明ムラへの耐性が固定閾値より大幅に向上する.
        Stage 1 が弱い場合の有力な選択肢

    5-4. 適応的閾値(平均値)

    ガウシアン加重ではなく単純平均で閾値を算出する.

    ・計算量: 低
    ・穴への効果: ○
    ・陰への効果: △(ガウシアンより若干ノイズに弱い)
    ・実装: `cv2.ADAPTIVE_THRESH_MEAN_C` を指定
    ・備考: ガウシアン版より高速だが精度はやや劣る

    5-5. Sauvola 閾値

    局所の平均値と標準偏差から閾値を計算する.
    コントラストが低い領域(= 線がない領域)を自動的に
    背景として扱える特性がある.

    ・計算量: 低〜中(積分画像 + 二乗積分画像)
    ・穴への効果: ○
    ・陰への効果: ◎(低コントラスト領域を無視できる)
    ・実装: OpenCV 標準にないため自前実装が必要

        mean = cv2.blur(src, (k, k))
        sq_mean = cv2.blur(
            (src.astype(np.float32)) ** 2, (k, k))
        std = np.sqrt(sq_mean - mean.astype(np.float32) ** 2)
        thresh = mean * (1.0 + coeff * (std / 128.0 - 1.0))

    ・備考: 文書画像の文字検出で実績が高い.
        黒線検出にも適している

    5-6. 行ごと Otsu

    画像を行(または行のブロック)単位で分割し,
    行ごとに Otsu の方法で閾値を決定する.

    ・計算量: 低(行数分の Otsu 計算)
    ・穴への効果: ○(行単位で閾値が変わる)
    ・陰への効果: ○(行ごとの照明変化に追従できる)
    ・実装: for ループで行ブロックごとに `cv2.threshold` + Otsu
    ・備考: 縦方向の照明勾配に強い.横方向のムラには弱い


6. Stage 3: 後処理の手法比較 (Post-processing)
------------------------------------------------------------------------

    6-0. 目的

    二値化後の画像から穴を埋め,ノイズを除去し,
    線の幅を正規化して特徴抽出の精度を向上させる.

    6-1. オープニング(現在の手法)

    収縮 → 膨張で孤立した小さなノイズピクセルを除去する.

    ・計算量: 極低
    ・穴への効果: -(穴の除去には寄与しない)
    ・陰への効果: △(小さな誤検出は除去できる)
    ・備考: 現在の手法.孤立ノイズ除去として引き続き有効

    6-2. 横方向クロージング(現在の手法)

    膨張 → 収縮で線の横方向の途切れを補間する.

    ・計算量: 極低
    ・穴への効果: ○(横方向の途切れを埋める)
    ・陰への効果: -
    ・備考: 現在の手法.横長の穴には有効だが丸い穴には効きにくい

    6-3. 等方クロージング

    円形カーネルによるクロージングで,
    方向を問わず穴(光スポット等)を埋める.

    ・計算量: 極低
    ・穴への効果: ◎(丸い穴にも対応できる)
    ・陰への効果: -
    ・実装:

        hole_kernel = cv2.getStructuringElement(
            cv2.MORPH_ELLIPSE, (hole_size, hole_size))
        binary = cv2.morphologyEx(
            binary, cv2.MORPH_CLOSE, hole_kernel)

    ・パラメータ: hole_size は想定される穴の最大径
    ・備考: 横方向クロージングの代替または補完として使用する

    6-4. 距離変換 + 閾値マスク

    二値画像の各白ピクセルについて,最も近い黒ピクセルまでの
    距離を算出し,閾値以上の距離(= 中心部)だけを残す.

    ・計算量: 低
    ・穴への効果: -(穴の除去には寄与しない.先にクロージングが必要)
    ・陰への効果: ◎(陰で広がった外縁は距離が短いため除去される)
    ・実装:

        dist = cv2.distanceTransform(
            binary, cv2.DIST_L2, 5)
        _, center_mask = cv2.threshold(
            dist, half_width_px, 255, cv2.THRESH_BINARY)

    ・パラメータ: half_width_px は黒線の幅の半分(ピクセル単位)
    ・備考: 陰による幅の広がりを削り取る最も直接的な手法.
        クロージングで穴を埋めた後に適用すると効果的

    6-5. スケルトン化(細線化)

    二値画像の塊を反復的に収縮し,
    1ピクセル幅の中心線(骨格)を抽出する.

    ・計算量: 中(反復処理のため塊の幅に比例)
    ・穴への効果: -(穴があると骨格が分断される.先にクロージングが必要)
    ・陰への効果: ◎(幅に関わらず中心線が得られる)
    ・実装: `cv2.ximgproc.thinning(binary)`
    ・依存: `opencv-contrib-python` の `ximgproc` モジュールが必要
    ・備考: 幅の正規化としては最も確実だが,
        追加依存と計算量を考慮する必要がある

    6-6. 連結成分フィルタ

    連結成分分析で各塊の面積・アスペクト比を算出し,
    線として不適切な塊(小さすぎる,横に広すぎる等)を除去する.

    ・計算量: 低
    ・穴への効果: -
    ・陰への効果: ○(大きな誤検出塊を面積で除去できる)
    ・実装:

        n, labels, stats, _ = cv2.connectedComponentsWithStats(
            binary)
        # stats で面積・幅・高さを確認し,
        # 異常な塊のラベルを 0(背景)に置換

    ・備考: 線から離れた場所の大きな誤検出に有効.
        線に隣接した陰は連結成分が線と結合するため効かない

    6-7. 幅フィルタ(行ごと,固定閾値)

    各行の白ピクセルの幅(右端 - 左端)を計算し,
    期待される線幅の範囲外の行を除外する.

    ・計算量: 極低
    ・穴への効果: -
    ・陰への効果: ◎(幅が広すぎる行を直接除外できる)
    ・実装:

        for y in range(height):
            xs = np.where(binary[y] > 0)[0]
            if len(xs) > 0:
                width = xs[-1] - xs[0] + 1
                if width > max_line_width:
                    binary[y] = 0  # 幅が異常な行を除去

    ・パラメータ: max_line_width は線幅の最大許容値(ピクセル)
    ・備考: 実装が最も簡単で陰への効果が高い.
        ただし単一の固定閾値では透視投影により近距離の行で
        上限が厳しすぎる,遠距離の行では甘すぎる問題がある.
        透視補正付き(6-8 節)を推奨する

    6-8. 幅フィルタ(行ごと,透視補正付き)

    「2-3. カメラ高さ・線幅の固定による幾何学的制約」で述べた
    線幅の透視変化を考慮し,各行の期待幅を線形補間で算出して
    フィルタ閾値を行ごとに動的に決定する.

    ・計算量: 極低
    ・穴への効果: -(幅フィルタは穴の補間をしない)
    ・陰への効果: ◎(行ごとに正確な上限を設定できる)
    ・実装:

        def _apply_width_filter(
            binary: np.ndarray,
            width_near: int,    # 画像下端での期待線幅 [px]
            width_far: int,     # 画像上端での期待線幅 [px]
            tolerance: float,   # 上限倍率(例: 1.8)
        ) -> np.ndarray:
            result = binary.copy()
            h = binary.shape[0]
            for y_local in range(h):
                xs = np.where(binary[y_local] > 0)[0]
                if len(xs) == 0:
                    continue
                # 行ごとの期待幅を線形補間
                t = (h - 1 - y_local) / max(h - 1, 1)
                expected = width_far + (width_near - width_far) * (1.0 - t)
                max_w = expected * tolerance
                actual_w = int(xs[-1]) - int(xs[0]) + 1
                if actual_w > max_w:
                    result[y_local] = 0
            return result

    ・パラメータ:
        - width_near: 画像下端での期待線幅(実測値,px)
        - width_far:  画像上端での期待線幅(実測値,px)
        - width_tolerance: 上限倍率(推奨 1.8〜2.0)

    ・備考: width_near / width_far は実際の二値化画像から一度計測して
        固定値に設定する.以後はパラメータ調整が不要になる.
        陰の片側広がりは除外されずに中心がずれる弱点は残るが,
        距離変換マスク(6-4 節)と組み合わせることで軽減できる.
        6-7 よりも適用精度が高く,こちらを優先して使用すること


7. Stage 4: 特徴抽出の手法比較 (Feature Extraction)
------------------------------------------------------------------------

    7-0. 目的

    二値化・後処理後の画像から線の位置・傾き・曲率を算出する.

    7-1. 全白ピクセルフィッティング(現在の手法)

    検出された全白ピクセルの (y, x) 座標に
    2次多項式 x = f(y) をフィッティングする.

    ・計算量: 低
    ・穴への効果: △(点が欠損した行の影響は小さいが精度は下がる)
    ・陰への効果: ✕(幅が広い行のピクセルがフィッティングを支配する)
    ・実装: `np.polyfit(ys, xs, 2)`
    ・備考: 現在の手法.Stage 3 で幅が正規化されていれば有効だが,
        陰の影響を受けやすい根本的な弱点がある

    7-2. 行ごと中心抽出 + フィッティング

    各行の白ピクセル群の中心(平均 x)を1点ずつ抽出し,
    中心点列に対してフィッティングする.

    ・計算量: 極低
    ・穴への効果: ○(穴のある行は中心が計算できず除外される)
    ・陰への効果: ◎(幅が広がっても中心位置はほぼ変わらない.
        各行が等しく1票なので幅による支配が発生しない)
    ・実装:

        centers_y, centers_x = [], []
        for y in range(height):
            xs = np.where(binary[y] > 0)[0]
            if len(xs) >= min_line_width:
                centers_y.append(y)
                centers_x.append(float(np.mean(xs)))
        coeffs = np.polyfit(centers_y, centers_x, 2)

    ・備考: コード変更が最小で効果が最大の改善.
        幅による重み付けの偏りを根本的に解消する

    7-3. 行ごと中央値抽出 + フィッティング

    7-2 の亜種.平均値の代わりに中央値を使用する.

    ・計算量: 極低
    ・穴への効果: ○
    ・陰への効果: ◎(中央値は外れ値に更に強い)
    ・実装: `np.mean(xs)` を `np.median(xs)` に変更
    ・備考: 陰が線の片側だけに広がった場合に平均値より頑健

    7-4. RANSAC フィッティング

    ランダムにサンプルした点から仮モデルを作り,
    外れ値を除去しながらフィッティングする.

    ・計算量: 中(反復回数 x サンプル数に比例)
    ・穴への効果: ◎(欠損があっても外れ値として除外)
    ・陰への効果: ◎(外れ値(= 陰のピクセル)を直接除外)
    ・実装: `sklearn.linear_model.RANSACRegressor` または自前実装
    ・依存: scikit-learn を使う場合は追加依存が発生
    ・備考: 最もロバストだが計算量が最大.
        行ごと中心抽出で十分な場合はオーバースペック

    7-5. 重み付きフィッティング

    距離変換値(= 線の中心からの距離)を重みとして
    フィッティングに使用する.中心に近いピクセルほど重みが大きい.

    ・計算量: 低
    ・穴への効果: ○
    ・陰への効果: ○(外縁のピクセルの重みが自動的に小さくなる)
    ・実装: `np.polyfit(ys, xs, 2, w=weights)`
    ・備考: 全ピクセルを使いつつ陰の影響を低減できる折衷案

    7-6. スプライン補間

    2次多項式の代わりにスプライン曲線を使用する.

    ・計算量: 低
    ・穴への効果: ○
    ・陰への効果: ○
    ・実装: `scipy.interpolate.UnivariateSpline`
    ・依存: scipy(多くの場合すでにインストール済み)
    ・備考: S 字カーブ等,2次多項式では表現できない
        複雑な形状に対応できる.ただし過学習のリスクがある


8. Stage 0: 撮影条件の最適化 (Camera Settings)
------------------------------------------------------------------------

    8-0. 目的

    ソフトウェア処理の前段階として,撮影条件を制御することで
    入力画像の品質を安定させる.

    8-1. カメラ露出の固定

    Picamera2 の自動露出(AE)を無効化し,固定値で撮影する.

    ・コスト: なし(ソフトウェア設定のみ)
    ・効果: ◎(フレーム間の輝度変動を排除し,閾値パラメータを安定させる)
    ・注意: 環境ごとに適切な露出値を設定する必要がある
    ・参照: `src/pi/camera/capture.py` の撮影パラメータ設定

    8-2. ホワイトバランスの固定

    自動ホワイトバランス(AWB)を無効化する.

    ・コスト: なし
    ・効果: ○(色味変動によるグレースケール値の揺れを防止)

    8-3. 赤外 LED + IR カメラ

    赤外光源と IR パスフィルタを使用して撮影する.

    ・コスト: 高(ハードウェア追加が必要)
    ・効果: ◎(黒は赤外線を吸収するため,可視光の影響を完全に排除)
    ・備考: コスト面で優先度は低いが,原理的には最も頑健な手法


9. 推奨する組み合わせ案 (Recommended Combinations)
------------------------------------------------------------------------

    9-0. 選定の考え方

    コースアウトを防ぐ上で「安定性」は「精度」より優先する.
    誤検出が 1 フレームでも入るとコースアウトしうるため,
    頑健性(ロバスト性)の高い手法を選択すること.

    以下の3案はいずれも穴・陰の両方に対して高い耐性を持つ.
    処理時間は 320x240 画像での概算値であり,
    30fps(~33ms/フレーム)に対していずれも十分な余裕がある.

    9-1. 案A: Black-hat 中心型(推奨)

    Black-hat が「背景より暗い構造」を直接抽出するため,
    照明正規化後の画像は非常にクリーンになり,
    固定閾値でも安定する.計算量が最も少なくパラメータも少ない.
    まず試すべき案である.

    ・パイプライン:

        グレースケール → Black-hat → ブラー → 固定閾値
          → 等方クロージング → 距離変換マスク
          → 行ごと中心抽出 → polyfit

    ・各段階の処理時間(概算):
        - Stage 1: Black-hat 変換                ~0.2ms
        - Stage 1: ガウシアンブラー              ~0.2ms
        - Stage 2: 固定閾値                      ~0.05ms
        - Stage 3: 等方クロージング              ~0.1ms
        - Stage 3: 距離変換 + 閾値マスク         ~0.2ms
        - Stage 4: 行ごと中心抽出 + polyfit      ~0.2ms
        - 合計                                   ~1.0ms

    ・穴: ◎(クロージングで穴を埋め,距離変換で中心を抽出)
    ・陰: ◎(Black-hat が背景輝度を除去 + 距離変換が外縁を削る
        + 中心抽出が幅変動を無視)
    ・変更量: 中
    ・追加パラメータ: 3個(Black-hat カーネルサイズ,
        等方クロージングサイズ,距離変換閾値)
    ・追加依存: なし

    9-2. 案B: 二重正規化型

    背景除算で大域的な照明勾配を除去した上で,
    適応的閾値が局所的なムラも処理する.
    原理の異なる2手法が補完し合う「二重防壁」構成であり,
    どちらか一方では対処できないケースにも対応できる.

    ・パイプライン:

        グレースケール → 背景除算正規化
          → 適応的閾値(ガウシアン)
          → 等方クロージング → 距離変換マスク
          → 行ごと中心抽出 → polyfit

    ・各段階の処理時間(概算):
        - Stage 1: 背景除算(大カーネルブラー + 除算) ~0.5ms
        - Stage 2: 適応的閾値(ガウシアン)             ~0.3ms
        - Stage 3: 等方クロージング                     ~0.1ms
        - Stage 3: 距離変換 + 閾値マスク                ~0.2ms
        - Stage 4: 行ごと中心抽出 + polyfit             ~0.2ms
        - 合計                                          ~1.3ms

    ・穴: ◎(背景除算で反射の影響を低減
        + 適応的閾値が局所的に追従)
    ・陰: ◎(背景除算が影の勾配を除去
        + 適応的閾値が局所閾値を調整)
    ・変更量: 中
    ・追加パラメータ: 4個(背景除算カーネルサイズ,
        blockSize,C,距離変換閾値)
    ・追加依存: なし

    9-3. 案C: 最高ロバスト型

    全段階で最もロバストな手法を選択した構成.
    Black-hat + 適応的閾値の二重正規化に加え,
    RANSAC で前段を突破した外れ値も排除する.
    全段階に防壁があるため,極端な照明環境でも破綻しにくい.

    ・パイプライン:

        グレースケール → Black-hat → 適応的閾値(ガウシアン)
          → 等方クロージング → 距離変換マスク
          → 行ごと中央値抽出 → RANSAC polyfit

    ・各段階の処理時間(概算):
        - Stage 1: Black-hat 変換               ~0.2ms
        - Stage 2: 適応的閾値(ガウシアン)     ~0.3ms
        - Stage 3: 等方クロージング             ~0.1ms
        - Stage 3: 距離変換 + 閾値マスク        ~0.2ms
        - Stage 4: 行ごと中央値抽出             ~0.2ms
        - Stage 4: RANSAC polyfit               ~1.5ms
        - 合計                                  ~2.5ms

    ・穴: ◎
    ・陰: ◎
    ・変更量: 大
    ・追加パラメータ: 5個(Black-hat カーネルサイズ,
        blockSize,C,距離変換閾値,RANSAC 閾値)
    ・追加依存: scikit-learn(RANSAC)
    ・備考: RANSAC が処理時間の大部分を占める.
        案A・B で十分な精度が得られる場合はオーバースペック

    9-4. 3案の総合比較

    ・処理時間:
        - 案A: ~1.0ms(対現在比 ~1.0x)
        - 案B: ~1.3ms(対現在比 ~1.3x)
        - 案C: ~2.5ms(対現在比 ~2.5x)

    ・穴耐性: 案A ◎,案B ◎,案C ◎
    ・陰耐性: 案A ◎,案B ◎,案C ◎
    ・追加パラメータ数: 案A 3個,案B 4個,案C 5個
    ・追加依存: 案A なし,案B なし,案C scikit-learn
    ・実装変更量: 案A 中,案B 中,案C 大

    いずれも 30fps に対して十分な余裕がある.
    まず案A を実装して効果を確認し,不足があれば
    案B・案C に段階的に進めることを推奨する

    9-5. 共通オプション: 透視補正付き幅フィルタの追加

    上記3案はいずれも等方クロージング + 距離変換マスク後に
    「透視補正付き幅フィルタ(6-8 節)」を追加できる.
    カメラ高さ・線幅が固定されているため,width_near・width_far を
    一度実測して固定すれば,パラメータ調整なしに
    陰で広がった行を正確に除外できる.

    ・処理時間への影響: 極小(+0.05ms 以下)
    ・効果: 距離変換では除去しきれなかった広がり行を補完的に除外
    ・適用場所: _apply_dist_mask の後,_fit_row_centers の前
    ・追加パラメータ:
        - width_near: 画像下端での期待線幅(px,実測値)
        - width_far:  画像上端での期待線幅(px,実測値)
        - width_tolerance: 上限倍率(デフォルト 1.8)


10. 評価方法 (Evaluation)
------------------------------------------------------------------------

    10-1. 定性評価

    デバッグオーバーレイ(`TECH_03_デバッグオーバーレイ仕様.txt` 参照)
    を使い,二値化画像と多項式フィッティング結果を目視確認する.

    ・確認ポイント
        - 二値化画像で黒線が一本の連続した塊として描かれているか
        - 床面(背景)の誤検出塊がないか
        - 多項式フィッティング線が実際の黒線に重なっているか
        - 光の穴や陰の広がりが後処理で正しく補正されているか

    10-2. 定量評価

    実走テストでコースアウトに至るまでの周回数・走行距離を記録し,
    手法変更前後で比較する.

    ・評価環境: 通常照明,強照明,照明ムラありの3条件を推奨
    ・計測項目: 周回数,コースアウト回数,position_error の分散


11. 案D: 谷検出+追跡型 (Valley Detection + Tracking)
------------------------------------------------------------------------

    11-1. 概要

    案A〜Cはいずれも二値化(固定閾値または適応的閾値)を経由するが,
    光の反射や影によって二値化の閾値設定が困難になる場面がある.
    案Dは二値化を完全に排除し,各行の輝度信号から直接「谷」(暗い
    領域)を検出することで,照明変動に対する根本的な耐性を得る.

    さらに,時系列追跡(トラッキング)により検出の安定性を確保し,
    一時的な検出失敗にも対応できる構成とする.

    11-2. アルゴリズム

    ■ 谷検出(行ごと,フレーム単位)

    (1) 画像全体を水平方向にガウシアンブラーする(カーネルサイズ:
        valley_gauss_ksize)
    (2) 各行の輝度信号から極小値(谷)を検出する
    (3) 各谷について以下を計算する:
        - 谷の中心 x 座標: 左右の肩の中点
        - 谷の深度: 左右の肩の平均輝度 - 谷底の輝度
        - 谷の幅: 左の肩から右の肩までのピクセル数
    (4) 以下のフィルタで不正な谷を除去する:
        - 最小深度フィルタ(valley_min_depth)
        - 透視補正付き幅フィルタ(width_near, width_far, width_tolerance)
        - 予測偏差フィルタ(valley_max_deviation,後述の追跡と連動)
    (5) 残った谷のうち最もスコアの高いものを採用する
        スコア = 深度 + 予測位置への近さ

    ■ 時系列追跡(フレーム間)

    (1) 前フレームの多項式係数から各行の x 座標を予測する
    (2) 予測から大きく外れた谷候補を棄却する(予測偏差フィルタ)
    (3) 検出成功時: 多項式係数を指数移動平均(EMA)で平滑化する
        smoothed = alpha × current + (1 - alpha) × prev
    (4) 検出失敗時: 前フレームの予測で補間する(コースティング)
        valley_coast_frames フレームまで継続し,超えると未検出を返す

    11-3. 案A〜Cとの違い

    ・二値化が不要: 輝度の相対的な谷を検出するため,閾値設定の問題
      が根本的に解消される
    ・時系列情報を活用: 案A〜Cは各フレームを独立に処理するが,
      案Dは前フレームの結果を予測・平滑化に活用する
    ・検出失敗への耐性: コースティングにより数フレームの検出失敗を
      許容できる(案A〜Cは即座に未検出となる)

    11-4. パラメータ一覧

    ・valley_gauss_ksize (デフォルト: 15)
        行ごとのガウシアン平滑化カーネルサイズ.
        大きくするとノイズに強くなるが,細い線を検出しにくくなる
    ・valley_min_depth (デフォルト: 15)
        谷として認識する最小深度(輝度差).
        小さくすると感度が上がるが誤検出も増える
    ・valley_max_deviation (デフォルト: 40)
        追跡予測からの最大許容偏差(px).
        小さくすると追跡が安定するが急カーブへの追従が遅れる
    ・valley_coast_frames (デフォルト: 3)
        検出失敗時に予測を継続するフレーム数.
        大きくすると一時的な見失いに強くなるが,
        誤った予測で走行し続けるリスクも増える
    ・valley_ema_alpha (デフォルト: 0.7)
        多項式係数の EMA 係数.1.0 で平滑化なし,
        小さくすると安定するがラグが増える
    ・ransac_thresh / ransac_iter(案Cと共通)
        有効にすると谷検出結果に RANSAC を適用する
    ・width_near / width_far / width_tolerance(共通)
        透視補正付き幅フィルタ.谷の幅の検証に使用する

    11-5. 実装ファイル

    ・src/pc/vision/line_detector.py
        - ValleyTracker クラス: 時系列追跡の状態管理
        - _find_row_valley(): 1行の谷検出
        - _detect_valley(): 案Dのメイン処理
        - _build_valley_binary(): デバッグ用二値画像生成
        - reset_valley_tracker(): 追跡状態のリセット
    ・src/pc/gui/main_window.py
        - 案D用パラメータの GUI コントロール
    ・src/pc/steering/pd_control.py
        - reset() 時に追跡状態もリセット