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つの典型的な劣化

    ■ 穴(光による欠損)

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

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

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


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 は線幅の最大許容値(ピクセル)
    ・備考: 実装が最も簡単で陰への効果が高い.
        ただし陰が線の片側だけに広がった場合は中心がずれる


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 に段階的に進めることを推奨する


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

    10-1. 定性評価

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

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

    10-2. 定量評価

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

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