diff --git a/CLAUDE.md b/CLAUDE.md index fbce7cd..2313597 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,6 +19,7 @@ - `docs/02_PLAN/PLAN_01_プロジェクト概要.txt` — 目的・目標・ハードウェア構成 - `docs/03_TECH/TECH_01_操舵量計算仕様.txt` — PD 制御、2領域偏差、速度制御 - `docs/03_TECH/TECH_02_システム構成仕様.txt` — Pi/PC の役割分担、通信フロー、設計方針 +- `docs/03_TECH/TECH_03_デバッグオーバーレイ仕様.txt` — オーバーレイ表示項目、描画色、GUI 操作 ### 環境(セットアップ時に参照) - `docs/04_ENV/ENV_01_技術スタック選定.txt` — ZMQ, PySide6, OpenCV, Picamera2, RPi.GPIO, python-dotenv diff --git "a/docs/03_TECH/TECH_01_\346\223\215\350\210\265\351\207\217\350\250\210\347\256\227\344\273\225\346\247\230.txt" "b/docs/03_TECH/TECH_01_\346\223\215\350\210\265\351\207\217\350\250\210\347\256\227\344\273\225\346\247\230.txt" index 94b66d6..bc6e7d7 100644 --- "a/docs/03_TECH/TECH_01_\346\223\215\350\210\265\351\207\217\350\250\210\347\256\227\344\273\225\346\247\230.txt" +++ "b/docs/03_TECH/TECH_01_\346\223\215\350\210\265\351\207\217\350\250\210\347\256\227\344\273\225\346\247\230.txt" @@ -28,8 +28,20 @@ 1. カメラからフレームを取得する. 2. グレースケールに変換する. 3. ガウシアンブラーを適用する(軽いノイズ除去). - 4. 二値化する(固定閾値 or 大津の方法). - 5. 近方領域・遠方領域それぞれで黒ピクセルの重心 x 座標を算出する. + 4. 固定閾値で二値化する(BINARY_INV). + 5. 横方向クロージングで反射による途切れを補間する. + 6. 近方領域・遠方領域それぞれで黒ピクセルの重心 x 座標を算出する. + + ■ 横方向クロージング + + 光の反射により線が途切れる問題への対策として, + 二値化後にモルフォロジーのクロージング処理(膨張→収縮)を行う. + 横長の楕円カーネルを使用することで,縦方向への影響を抑えつつ + 線の途切れを左右から埋める. + + ・カーネル形状: 横長楕円(幅 25 × 高さ 3) + ・効果: 反射で欠けた部分を,左右の検出済みピクセルからつなぐ + ・注意: カーネル幅は途切れの大きさに応じて調整する 2-2. 2領域方式 @@ -130,10 +142,12 @@ ■ 画像処理パラメータ - ・画像解像度: 未定(既存設定は 320x240) - ・二値化閾値: 実走テストで決定 - ・近方領域の y 範囲: 実走テストで決定 - ・遠方領域の y 範囲: 実走テストで決定 + ・画像解像度: 320x240 + ・ブラーカーネルサイズ: 5 + ・二値化閾値: 80(固定閾値) + ・クロージングカーネル: 幅 25 × 高さ 3(横長楕円) + ・近方領域の y 範囲: 画像高さの 70% ~ 100% + ・遠方領域の y 範囲: 画像高さの 30% ~ 50% ■ 偏差合成パラメータ diff --git "a/docs/03_TECH/TECH_03_\343\203\207\343\203\220\343\203\203\343\202\260\343\202\252\343\203\274\343\203\220\343\203\274\343\203\254\343\202\244\344\273\225\346\247\230.txt" "b/docs/03_TECH/TECH_03_\343\203\207\343\203\220\343\203\203\343\202\260\343\202\252\343\203\274\343\203\220\343\203\274\343\203\254\343\202\244\344\273\225\346\247\230.txt" new file mode 100644 index 0000000..d95088c --- /dev/null +++ "b/docs/03_TECH/TECH_03_\343\203\207\343\203\220\343\203\203\343\202\260\343\202\252\343\203\274\343\203\220\343\203\274\343\203\254\343\202\244\344\273\225\346\247\230.txt" @@ -0,0 +1,56 @@ +======================================================================== +デバッグオーバーレイ仕様 (Debug Overlay Specification) +======================================================================== + + +1. 概要 (Overview) +------------------------------------------------------------------------ + + 1-0. 目的 + + 画像処理パイプラインの動作を視覚的に確認するためのオーバーレイ + 表示機能を定義する.GUI のチェックボックスで項目ごとに ON/OFF + できる. + + 1-1. 基本方針 + + ・オーバーレイはカメラ映像に重ねて描画する. + ・手動操作中でもオーバーレイが有効なら線検出を実行する. + ・自動操縦中は操舵量計算で実行済みの検出結果を再利用する. + + +2. 表示項目 (Overlay Items) +------------------------------------------------------------------------ + + 2-1. 一覧 + + ・二値化画像: 二値化結果を赤色の半透明で重ねる(不透明度 0.4) + ・近方領域: 近方 ROI の枠を緑色で表示 + ・遠方領域: 遠方 ROI の枠を青色で表示 + ・重心マーカー: 検出した重心位置に円を描画 + ・中心線: 画像の中心 x に垂直線を描画(黄色) + ・偏差値: 近方・遠方の偏差数値を画像左上に表示 + + 2-2. 描画色 (BGR) + + ・近方関連(枠・重心): (0, 255, 0) 緑 + ・遠方関連(枠・重心): (255, 0, 0) 青 + ・中心線: (0, 255, 255) 黄 + ・テキスト: (255, 255, 255) 白 + ・二値化オーバーレイ: 赤チャンネルに二値化画像を割り当て + + +3. GUI 操作 (GUI Controls) +------------------------------------------------------------------------ + + 3-1. チェックボックス + + コントロールパネルに「デバッグ表示」グループを設け, + 各表示項目に対応するチェックボックスを配置する. + チェックの ON/OFF で即時に表示が切り替わる. + + 3-2. 動作モードとの関係 + + ・手動操作中: オーバーレイが 1 つでも ON なら線検出を実行する + ・自動操縦中: 操舵量計算の線検出結果をそのまま使用する + ・未接続時: オーバーレイは表示されない(映像がないため) diff --git "a/docs/04_ENV/ENV_04_\343\203\207\343\202\243\343\203\254\343\202\257\343\203\210\343\203\252\346\247\213\346\210\220.txt" "b/docs/04_ENV/ENV_04_\343\203\207\343\202\243\343\203\254\343\202\257\343\203\210\343\203\252\346\247\213\346\210\220.txt" index 52a3fef..bcc2b37 100644 --- "a/docs/04_ENV/ENV_04_\343\203\207\343\202\243\343\203\254\343\202\257\343\203\210\343\203\252\346\247\213\346\210\220.txt" +++ "b/docs/04_ENV/ENV_04_\343\203\207\343\202\243\343\203\254\343\202\257\343\203\210\343\203\252\346\247\213\346\210\220.txt" @@ -55,7 +55,8 @@ │ ├── base.py 共通インターフェース │ └── pd_control.py PD 制御の実装 └── vision/ 画像処理 - └── line_detector.py 線検出 + ├── line_detector.py 線検出 + └── overlay.py デバッグオーバーレイ描画 2-4. src/pi/ diff --git a/src/pc/gui/main_window.py b/src/pc/gui/main_window.py index 013f921..aea0edc 100644 --- a/src/pc/gui/main_window.py +++ b/src/pc/gui/main_window.py @@ -8,6 +8,7 @@ from PySide6.QtCore import Qt, QTimer from PySide6.QtGui import QImage, QKeyEvent, QPixmap from PySide6.QtWidgets import ( + QCheckBox, QDoubleSpinBox, QFormLayout, QGroupBox, @@ -22,6 +23,8 @@ from common import config from pc.comm.zmq_client import PcZmqClient from pc.steering.pd_control import PdControl, PdParams +from pc.vision.line_detector import LineDetectResult, detect_line +from pc.vision.overlay import OverlayFlags, draw_overlay # 映像更新間隔 (ms) FRAME_INTERVAL_MS: int = 33 @@ -59,6 +62,10 @@ # 最新フレームの保持(自動操縦で使用) self._latest_frame: np.ndarray | None = None + # オーバーレイ + self._overlay_flags = OverlayFlags() + self._last_detect_result: LineDetectResult | None = None + self._setup_ui() self._setup_timers() @@ -125,6 +132,9 @@ # PD パラメータ調整 self._setup_param_ui(control_layout) + # デバッグ表示 + self._setup_overlay_ui(control_layout) + # 操作ガイド guide = QLabel( "--- キー操作 ---\n" @@ -202,6 +212,41 @@ parent_layout.addWidget(group) + def _setup_overlay_ui( + self, parent_layout: QVBoxLayout, + ) -> None: + """デバッグ表示のチェックボックス UI を構築する""" + group = QGroupBox("デバッグ表示") + layout = QVBoxLayout() + group.setLayout(layout) + + items = [ + ("二値化画像", "binary"), + ("近方領域", "near_region"), + ("遠方領域", "far_region"), + ("重心マーカー", "centroids"), + ("中心線", "center_line"), + ("偏差値", "error_text"), + ] + for label, attr in items: + cb = QCheckBox(label) + cb.toggled.connect( + lambda checked, a=attr: + setattr(self._overlay_flags, a, checked), + ) + layout.addWidget(cb) + + parent_layout.addWidget(group) + + def _has_any_overlay(self) -> bool: + """いずれかのオーバーレイが有効かを返す""" + f = self._overlay_flags + return ( + f.binary or f.near_region or f.far_region + or f.centroids or f.center_line + or f.error_text + ) + @staticmethod def _create_spin( value: float, min_val: float, @@ -318,6 +363,14 @@ self._throttle = output.throttle self._steer = output.steer self._update_control_label() + self._last_detect_result = ( + self._pd_control.last_detect_result + ) + elif self._has_any_overlay(): + # 手動操作中でもオーバーレイ用に線検出を実行 + self._last_detect_result = detect_line(frame) + else: + self._last_detect_result = None self._display_frame(frame) @@ -327,6 +380,12 @@ Args: frame: BGR 形式の画像 """ + # オーバーレイ描画 + frame = draw_overlay( + frame, self._last_detect_result, + self._overlay_flags, + ) + # BGR → RGB 変換 rgb = frame[:, :, ::-1].copy() h, w, ch = rgb.shape diff --git a/src/pc/vision/line_detector.py b/src/pc/vision/line_detector.py index 8f7bced..1c77ff0 100644 --- a/src/pc/vision/line_detector.py +++ b/src/pc/vision/line_detector.py @@ -17,6 +17,10 @@ # 二値化の閾値(黒線検出用,この値以下を黒とみなす) BINARY_THRESHOLD: int = 80 +# 横方向クロージングのカーネルサイズ(反射による途切れを補間) +MORPH_CLOSE_WIDTH: int = 25 +MORPH_CLOSE_HEIGHT: int = 3 + # 近方領域の y 範囲(画像下部) NEAR_Y_START: int = int(config.FRAME_HEIGHT * 0.7) NEAR_Y_END: int = config.FRAME_HEIGHT @@ -63,12 +67,21 @@ 0, ) - # 二値化(黒線を白,背景を黒に反転) + # 固定閾値で二値化(黒線を白,背景を黒に反転) _, binary = cv2.threshold( blurred, BINARY_THRESHOLD, 255, cv2.THRESH_BINARY_INV, ) + # 横方向クロージング(反射で途切れた線を左右からつなぐ) + kernel = cv2.getStructuringElement( + cv2.MORPH_ELLIPSE, + (MORPH_CLOSE_WIDTH, MORPH_CLOSE_HEIGHT), + ) + binary = cv2.morphologyEx( + binary, cv2.MORPH_CLOSE, kernel, + ) + # 画像の中心 x 座標 center_x = config.FRAME_WIDTH / 2.0 diff --git a/src/pc/vision/overlay.py b/src/pc/vision/overlay.py new file mode 100644 index 0000000..2fb0809 --- /dev/null +++ b/src/pc/vision/overlay.py @@ -0,0 +1,189 @@ +""" +overlay +画像処理の結果をカメラ映像に重ねて描画するモジュール +チェックボックスで個別に ON/OFF できる +""" + +from dataclasses import dataclass + +import cv2 +import numpy as np + +from common import config +from pc.vision import line_detector +from pc.vision.line_detector import LineDetectResult + +# 描画色の定義 (BGR) +COLOR_NEAR: tuple = (0, 255, 0) +COLOR_FAR: tuple = (255, 0, 0) +COLOR_CENTER: tuple = (0, 255, 255) +COLOR_TEXT: tuple = (255, 255, 255) + +# 二値化オーバーレイの不透明度 +BINARY_OPACITY: float = 0.4 + + +@dataclass +class OverlayFlags: + """オーバーレイ表示項目のフラグ + + Attributes: + binary: 二値化画像の半透明表示 + near_region: 近方領域の枠 + far_region: 遠方領域の枠 + centroids: 重心マーカー + center_line: 画像中心線 + error_text: 偏差の数値表示 + """ + binary: bool = False + near_region: bool = False + far_region: bool = False + centroids: bool = False + center_line: bool = False + error_text: bool = False + + +def draw_overlay( + frame: np.ndarray, + result: LineDetectResult | None, + flags: OverlayFlags, +) -> np.ndarray: + """カメラ映像にオーバーレイを描画する + + Args: + frame: 元の BGR カメラ画像 + result: 線検出の結果(None の場合はオーバーレイなし) + flags: 表示項目のフラグ + + Returns: + オーバーレイ描画済みの画像 + """ + display = frame.copy() + + if result is None: + return display + + # 二値化画像の半透明オーバーレイ + if flags.binary and result.binary_image is not None: + display = _draw_binary_overlay( + display, result.binary_image, + ) + + # 近方領域の枠 + if flags.near_region: + cv2.rectangle( + display, + (0, line_detector.NEAR_Y_START), + ( + config.FRAME_WIDTH - 1, + line_detector.NEAR_Y_END - 1, + ), + COLOR_NEAR, 1, + ) + + # 遠方領域の枠 + if flags.far_region: + cv2.rectangle( + display, + (0, line_detector.FAR_Y_START), + ( + config.FRAME_WIDTH - 1, + line_detector.FAR_Y_END - 1, + ), + COLOR_FAR, 1, + ) + + # 画像中心線 + if flags.center_line: + center_x = config.FRAME_WIDTH // 2 + cv2.line( + display, + (center_x, 0), + (center_x, config.FRAME_HEIGHT), + COLOR_CENTER, 1, + ) + + # 重心マーカー + if flags.centroids: + if result.near_x is not None: + near_y = ( + line_detector.NEAR_Y_START + + line_detector.NEAR_Y_END + ) // 2 + cv2.circle( + display, + (int(result.near_x), near_y), + 6, COLOR_NEAR, -1, + ) + if result.far_x is not None: + far_y = ( + line_detector.FAR_Y_START + + line_detector.FAR_Y_END + ) // 2 + cv2.circle( + display, + (int(result.far_x), far_y), + 6, COLOR_FAR, -1, + ) + + # 偏差の数値表示 + if flags.error_text: + _draw_error_text(display, result) + + return display + + +def _draw_binary_overlay( + frame: np.ndarray, + binary: np.ndarray, +) -> np.ndarray: + """二値化画像を半透明で重ねる + + Args: + frame: 元の BGR 画像 + binary: 二値化画像(グレースケール) + + Returns: + 合成された画像 + """ + # 二値化画像を赤色の BGR に変換 + binary_bgr = np.zeros_like(frame) + binary_bgr[:, :, 2] = binary + + # 半透明合成 + return cv2.addWeighted( + frame, 1.0 - BINARY_OPACITY, + binary_bgr, BINARY_OPACITY, 0, + ) + + +def _draw_error_text( + frame: np.ndarray, + result: LineDetectResult, +) -> None: + """偏差の数値を画像に描画する + + Args: + frame: 描画先の画像 + result: 線検出の結果 + """ + near_str = ( + f"near: {result.near_error:+.3f}" + if result.near_x is not None + else "near: N/A" + ) + far_str = ( + f"far: {result.far_error:+.3f}" + if result.far_x is not None + else "far: N/A" + ) + cv2.putText( + frame, near_str, (5, 15), + cv2.FONT_HERSHEY_SIMPLEX, 0.4, + COLOR_TEXT, 1, + ) + cv2.putText( + frame, far_str, (5, 30), + cv2.FONT_HERSHEY_SIMPLEX, 0.4, + COLOR_TEXT, 1, + )