🐳

レーダー信号処理におけるCFAR検出アルゴリズムの実装

に公開

はじめに

レーダーやセンサーを用いた距離測定において、「そこに対象物が存在するか」を判断することは基本的かつ重要な課題です。しかし、実際の測定環境ではノイズが常に存在するため、単純に振幅の大きさだけで判断すると誤検出(False Alarm)が発生してしまいます。

CFAR(Constant False Alarm Rate) は、このような課題を解決するための適応的な閾値設定手法です。各測定点(ビン)に対して、周囲のノイズレベルを統計的に推定し、それに基づいて動的に検出閾値を決定します。これにより、ノイズレベルが変動する環境でも誤警報率を一定に保ちながらターゲットを検出できます。

CFARは以下のような特徴を持ちます:

  • 適応的な閾値設定:局所的なノイズレベルに応じて閾値を自動調整
  • 複数対象検出:閾値を超える全てのターゲットを同時に検出可能
  • ロバスト性:ノイズ環境の変化に強い

今回は、呼吸モニタリングシステムにおいてCA-CFAR(Cell Averaging CFAR)アルゴリズムを実装し、ターゲットの自動検出を実装しました。

この記事で説明すること

  • CFAR検出アルゴリズムの基本概念と種類
  • CA-CFAR(Cell Averaging CFAR)の動作原理
  • Pythonによる実装方法とコード解説
  • 距離スイープデータへの適用例
  • 実装における設定パラメータの選び方

CFARの説明

CA-CFAR(Cell Averaging CFAR)の構造

CA-CFARは、検出対象セル(CUT: Cell Under Test)の周囲にガードセル参照セルを配置し、参照セルの平均パワーからノイズレベルを推定する手法です。

基本構造:

処理フロー:

  1. ノイズレベル推定:左右の参照セルの平均パワーを計算
    noise_level = mean(左参照セル ∪ 右参照セル)

  2. 閾値計算:ノイズレベルにスケールファクタを乗算
    threshold = noise_level × scale_factor

  3. 検出判定:CUTの振幅が閾値を超えた場合にターゲット検出
    if amplitude[CUT] > threshold → ターゲット検出

なぜガードセルが必要か

今回の距離スイープデータでは信号強度が低く、ピークの半値幅が広いという特徴があります。ガードセルがないと、ターゲット信号の裾野が参照セルに含まれてしまい、ノイズレベルが過大評価されます。その結果、閾値が高くなりすぎて検出できなくなります。

ガードセルを設置することで、ターゲット信号の影響を避けつつ、その外側の参照セルから純粋なノイズレベルを推定できます。ターゲット信号の半値幅が狭ければガードセルを少なくし、半値幅が広ければその分、複数のbinにピークが存在するのでガードセルを増やします。ガードセルとはノイズを計算しない領域(bin)のことです。

実装コード

以下は、CA-CFARアルゴリズムの実装例です:

class CFAR_Processor:
 """CA-CFAR (Cell Averaging CFAR) プロセッサ"""
 
 def __init__(self, num_reference_cells=8, num_guard_cells=3, scale_factor=2.0):
     """
     Parameters:
     num_reference_cells: 各側の参照セル数
     num_guard_cells: 各側のガードセル数
     scale_factor: 閾値スケールファクタ (α)
     """
     self.num_ref = num_reference_cells
     self.num_guard = num_guard_cells
     self.scale_factor = scale_factor
     
     # ウィンドウサイズ計算
     self.window_size = 2 * (num_reference_cells + num_guard_cells) + 1
 
 def compute_threshold(self, amplitude_data):
     """CFAR閾値を計算"""
     n_bins = len(amplitude_data)
     threshold_array = np.zeros(n_bins)
     
     # dBを線形スケールに変換
     amplitude_linear = 10 ** (amplitude_data / 20.0)
     
     for cut_idx in range(n_bins):
         # 左側参照セルの範囲
         left_ref_start = max(0, cut_idx - self.num_ref - self.num_guard)
         left_ref_end = max(0, cut_idx - self.num_guard)
         
         # 右側参照セルの範囲
         right_ref_start = min(n_bins, cut_idx + self.num_guard + 1)
         right_ref_end = min(n_bins, cut_idx + self.num_guard + self.num_ref + 1)
         
         # 参照セルのデータを取得
         left_cells = amplitude_linear[left_ref_start:left_ref_end]
         right_cells = amplitude_linear[right_ref_start:right_ref_end]
         
         reference_cells = np.concatenate([left_cells, right_cells])
         
         if len(reference_cells) > 0:
             # ノイズレベル推定(線形スケールで平均)
             noise_level_linear = np.mean(reference_cells)
             # 閾値計算
             threshold_linear = noise_level_linear * self.scale_factor
             # dBスケールに戻す
             threshold_array[cut_idx] = 20 * np.log10(threshold_linear + 1e-12)
         else:
             threshold_array[cut_idx] = np.min(amplitude_data)
     
     return threshold_array
 
 def detect_targets(self, amplitude_data):
     """
     CFARでターゲット検出
     
     Returns:
     threshold_array: 各ビンの閾値
     detection_mask: 検出されたビンのマスク (True/False)
     detected_indices: 検出されたビンのインデックス
     """
     threshold_array = self.compute_threshold(amplitude_data)
     
     # 閾値を超えたビンを検出
     detection_mask = amplitude_data > threshold_array
     detected_indices = np.where(detection_mask)[0]
     
     return threshold_array, detection_mask, detected_indices

パラメータの選び方

参照セル数(num_reference_cells):

・少ない(8~15セル):局所的なノイズ変動に敏感、ピーク周辺の外側に設定
・多い(25~40セル):広域のノイズレベルを平均化、安定した推定

ガードセル数(num_guard_cells):

・ピーク幅に応じて設定(今回は広いピークのため25~30セル程度)
・狭いピーク:少ないガードセルで対応可能

スケールファクタ(scale_factor):

・1.5~2.0:高感度(誤検出リスク増)
・2.0~3.0:中感度
・3.0~5.0:低感度(見逃しリスク増)

距離スイープへの適用

実際の測定データでは、特定の探索範囲内でCFAR検出を行い、最も振幅の大きいターゲットを選択します:

def detect_peak_position_with_cfar(self, amplitude_data):
    """CFAR処理を用いたピーク位置検出"""
    try:
        # 探索範囲の設定
        search_start = max(0, self.peak_search_start)
        search_end = min(len(amplitude_data), self.peak_search_end)
        
        search_region = amplitude_data[search_start:search_end]
        
        if self.enable_cfar:
            # CFAR検出を実行
            threshold_array, detection_mask, detected_indices = \
                self.cfar_processor.detect_targets(search_region)
            
            if len(detected_indices) > 0:
                # 検出されたビンの中で最大振幅を持つビンを選択
                detected_amplitudes = search_region[detected_indices]
                max_detected_idx = detected_indices[np.argmax(detected_amplitudes)]
                absolute_peak_position = search_start + max_detected_idx
                
                cfar_info = {
                    'cfar_enabled': True,
                    'threshold_array': threshold_array,
                    'num_detected': len(detected_indices),
                    'detected_indices': detected_indices + search_start
                }
            else:
                # 検出されない場合はフォールバック
                relative_peak_idx = np.argmax(search_region)
                absolute_peak_position = search_start + relative_peak_idx
                cfar_info = {'cfar_enabled': True, 'fallback_used': True}
        
        return absolute_peak_position, cfar_info
        
    except Exception as e:
        return self.target_position, None

結果

実装したCFARアルゴリズムを呼吸モニタリングシステムに統合し、以下の結果を得ました:

検出性能

・自動ターゲット検出:100フレームごとにCFAR処理を実行し、ターゲット位置を自動更新
・検出精度:ノイズフロアから約10~15dB以上のピークを安定して検出
・誤検出の抑制:適切なガードセル設定により、ノイズによる誤検出を大幅に削減

実装パラメータ(最終設定)

self.analyzer = RealTimeVitalAnalyzer(
    cfar_num_ref=30,        # 参照セル数(各側)
    cfar_num_guard=10,      # ガードセル数(各側)
    cfar_scale=2.0          # スケールファクタ
)

この設定により、広いピーク幅を持つ信号に対しても安定した検出が可能になりました。

視覚化

距離スペクトラムのプロット上に、赤い点線でCFAR閾値を表示し、検出されたターゲット位置を白い縦線で示すことで、アルゴリズムの動作を直感的に確認できます。

CFAR閾値ライン(赤い点線)

self.curve_cfar_threshold = self.plot_range.plot(
    pen=pg.mkPen('r', width=2, style=QtCore.Qt.DashLine),
    name='CFAR Threshold'
)

ターゲット位置(白い縦線)

self.target_line = pg.InfiniteLine(
    pos=display_peak_distance_m,
    angle=90,
    pen=pg.mkPen('white', width=1, style=QtCore.Qt.DashLine)
)

まとめ

本記事では、レーダー信号処理におけるCFAR検出アルゴリズムについて解説し、Pythonによる実装例を示しました。

CFARのメリット:
・適応的な閾値設定により、ノイズ環境の変動に対してロバスト
・複数ターゲットの同時検出が可能
・誤警報率を一定に保ちながら検出を実現

実装のポイント:
・信号特性に応じたガードセル・参照セル数の調整が重要
・dB⇔線形スケールの変換を正しく行うこと
・スケールファクタで感度を調整可能

CFARは、レーダーだけでなく、超音波センサーやLiDARなど、様々なセンサーシステムに応用できる汎用的な手法です。今回の実装をベースに、用途に応じたカスタマイズを行うことで、より高精度な検出システムを構築できると思います。。

Discussion