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" index 3793ab1..4117323 100644 --- "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" @@ -15,27 +15,30 @@ 1-1. 基本方針 ・オーバーレイはカメラ映像に重ねて描画する. - ・手動操作中でもオーバーレイが有効なら線検出を実行する. + ・線検出は接続中は常に実行し,検出情報は映像下のラベルに表示する. ・自動操縦中は操舵量計算で実行済みの検出結果を再利用する. 2. 表示項目 (Overlay Items) ------------------------------------------------------------------------ - 2-1. 一覧 + 2-1. 画像オーバーレイ(チェックボックスで切替) ・二値化画像: 二値化結果を赤色の半透明で重ねる(不透明度 0.4) ・検出領域: 検出対象領域の枠を青色で表示 ・フィッティング曲線: 多項式の曲線を緑色で描画 ・中心線: 画像の中心 x に垂直線を描画(黄色) - ・検出情報: 位置偏差・傾き・曲率の数値を画像左上に表示 - 2-2. 描画色 (BGR) + 2-2. 検出情報ラベル(常時表示) + + 映像の下に配置されたラベルに,位置偏差・傾き・曲率の数値を + テキストで表示する.線が未検出の場合は「---」を表示する. + + 2-3. 描画色 (BGR) ・フィッティング曲線: (0, 255, 0) 緑 ・中心線: (0, 255, 255) 黄 ・検出領域: (255, 0, 0) 青 - ・テキスト: (255, 255, 255) 白 ・二値化オーバーレイ: 赤チャンネルに二値化画像を割り当て @@ -50,6 +53,6 @@ 3-2. 動作モードとの関係 - ・手動操作中: オーバーレイが 1 つでも ON なら線検出を実行する + ・手動操作中: 線検出を常に実行し,検出情報ラベルを更新する ・自動操縦中: 操舵量計算の線検出結果をそのまま使用する ・未接続時: オーバーレイは表示されない(映像がないため) diff --git a/src/common/config.py b/src/common/config.py index 8973b3b..4a9e106 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -31,11 +31,17 @@ # ── 画像設定 ────────────────────────────────────────────── -# カメラ画像の幅 (px) -FRAME_WIDTH: int = 320 +# カメラ撮影時の幅 (px) +CAPTURE_WIDTH: int = 320 -# カメラ画像の高さ (px) -FRAME_HEIGHT: int = 240 +# カメラ撮影時の高さ (px) +CAPTURE_HEIGHT: int = 240 + +# 処理・送信時の幅 (px)(撮影後に縮小) +FRAME_WIDTH: int = 40 + +# 処理・送信時の高さ (px)(撮影後に縮小) +FRAME_HEIGHT: int = 30 # JPEG 圧縮品質 (0-100) JPEG_QUALITY: int = 55 diff --git a/src/pc/gui/main_window.py b/src/pc/gui/main_window.py index c5696eb..2725f0c 100644 --- a/src/pc/gui/main_window.py +++ b/src/pc/gui/main_window.py @@ -34,7 +34,12 @@ save_control, save_detect_params, ) +from pc.steering.base import SteeringBase from pc.steering.pd_control import PdControl, PdParams +from pc.steering.pursuit_control import ( + PursuitControl, + PursuitParams, +) from pc.steering.param_store import ( ImagePreset, PdPreset, @@ -62,8 +67,8 @@ 1000 / config.CONTROL_PUBLISH_HZ ) -# 映像表示のスケール倍率 -DISPLAY_SCALE: float = 2.0 +# 映像表示のスケール倍率(40x30 → 640x480 相当) +DISPLAY_SCALE: float = 16.0 # 手動操作の throttle / steer 量 MANUAL_THROTTLE: float = 0.5 @@ -94,6 +99,12 @@ params=pd_params, image_params=image_params, ) + self._pursuit_control = PursuitControl( + image_params=image_params, + ) + + # 現在の制御手法("pd" or "pursuit") + self._steering_method: str = "pd" # 最新フレームの保持(自動操縦で使用) self._latest_frame: np.ndarray | None = None @@ -115,7 +126,9 @@ self.setCentralWidget(central) root_layout = QHBoxLayout(central) - # 左側: 映像表示 + # 左側: 映像表示 + 検出情報 + left_layout = QVBoxLayout() + self._video_label = QLabel("カメラ映像待機中...") self._video_label.setAlignment( Qt.AlignmentFlag.AlignCenter, @@ -128,7 +141,22 @@ "background-color: #222;" " color: #aaa; font-size: 16px;" ) - root_layout.addWidget(self._video_label, stretch=3) + left_layout.addWidget(self._video_label) + + self._detect_info_label = QLabel( + "pos: --- head: --- curv: ---" + ) + self._detect_info_label.setAlignment( + Qt.AlignmentFlag.AlignLeft, + ) + self._detect_info_label.setStyleSheet( + "font-size: 14px; font-family: monospace;" + " color: #0f0; background-color: #222;" + " padding: 4px;" + ) + left_layout.addWidget(self._detect_info_label) + + root_layout.addLayout(left_layout, stretch=3) # 右側: スクロール可能なコントロールパネル scroll = QScrollArea() @@ -197,48 +225,150 @@ # 余白を下に詰める control_layout.addStretch() + @property + def _active_control(self) -> SteeringBase: + """現在選択中の制御クラスを返す""" + if self._steering_method == "pursuit": + return self._pursuit_control + return self._pd_control + def _setup_param_ui( self, parent_layout: QVBoxLayout, ) -> None: - """PD パラメータ調整 UI を構築する""" - group = QGroupBox("PD 制御パラメータ") + """制御パラメータ調整 UI を構築する""" + group = QGroupBox("制御パラメータ") layout = QVBoxLayout() group.setLayout(layout) - form = QFormLayout() - layout.addLayout(form) + # 制御手法の選択 + self._steering_combo = QComboBox() + self._steering_combo.addItem( + "PD 制御", "pd", + ) + self._steering_combo.addItem( + "2点パシュート", "pursuit", + ) + layout.addWidget(self._steering_combo) + + # --- PD パラメータ --- + self._pd_param_form = QFormLayout() + layout.addLayout(self._pd_param_form) params = self._pd_control.params self._spin_kp = self._create_spin( params.kp, 0.0, 5.0, 0.05, ) - form.addRow("Kp (位置):", self._spin_kp) + self._pd_param_form.addRow( + "Kp (位置):", self._spin_kp, + ) self._spin_kh = self._create_spin( params.kh, 0.0, 5.0, 0.05, ) - form.addRow("Kh (傾き):", self._spin_kh) + self._pd_param_form.addRow( + "Kh (傾き):", self._spin_kh, + ) self._spin_kd = self._create_spin( params.kd, 0.0, 5.0, 0.05, ) - form.addRow("Kd (微分):", self._spin_kd) + self._pd_param_form.addRow( + "Kd (微分):", self._spin_kd, + ) self._spin_max_steer_rate = self._create_spin( params.max_steer_rate, 0.01, 1.0, 0.01, ) - form.addRow("操舵制限:", self._spin_max_steer_rate) + self._pd_param_form.addRow( + "操舵制限:", self._spin_max_steer_rate, + ) self._spin_max_throttle = self._create_spin( params.max_throttle, 0.0, 1.0, 0.05, ) - form.addRow("最大速度:", self._spin_max_throttle) + self._pd_param_form.addRow( + "最大速度:", self._spin_max_throttle, + ) self._spin_speed_k = self._create_spin( params.speed_k, 0.0, 5.0, 0.05, ) - form.addRow("減速係数:", self._spin_speed_k) + self._pd_param_form.addRow( + "減速係数:", self._spin_speed_k, + ) + + # PD 固有ウィジェットリスト(表示切替用) + self._pd_widgets: list[QWidget] = [ + self._spin_kp, self._spin_kh, + self._spin_kd, + ] + + # --- Pursuit パラメータ --- + self._pursuit_param_form = QFormLayout() + layout.addLayout(self._pursuit_param_form) + + pp = self._pursuit_control.params + + self._spin_near_ratio = self._create_spin( + pp.near_ratio, 0.0, 1.0, 0.05, + ) + self._pursuit_param_form.addRow( + "近目標(比率):", self._spin_near_ratio, + ) + + self._spin_far_ratio = self._create_spin( + pp.far_ratio, 0.0, 1.0, 0.05, + ) + self._pursuit_param_form.addRow( + "遠目標(比率):", self._spin_far_ratio, + ) + + self._spin_k_near = self._create_spin( + pp.k_near, 0.0, 5.0, 0.05, + ) + self._pursuit_param_form.addRow( + "K_near:", self._spin_k_near, + ) + + self._spin_k_far = self._create_spin( + pp.k_far, 0.0, 5.0, 0.05, + ) + self._pursuit_param_form.addRow( + "K_far:", self._spin_k_far, + ) + + self._spin_pursuit_steer_rate = self._create_spin( + pp.max_steer_rate, 0.01, 1.0, 0.01, + ) + self._pursuit_param_form.addRow( + "操舵制限:", self._spin_pursuit_steer_rate, + ) + + self._spin_pursuit_throttle = self._create_spin( + pp.max_throttle, 0.0, 1.0, 0.05, + ) + self._pursuit_param_form.addRow( + "最大速度:", self._spin_pursuit_throttle, + ) + + self._spin_pursuit_speed_k = self._create_spin( + pp.speed_k, 0.0, 10.0, 0.1, + ) + self._pursuit_param_form.addRow( + "減速係数:", self._spin_pursuit_speed_k, + ) + + # Pursuit 固有ウィジェットリスト(表示切替用) + self._pursuit_widgets: list[QWidget] = [ + self._spin_near_ratio, + self._spin_far_ratio, + self._spin_k_near, + self._spin_k_far, + self._spin_pursuit_steer_rate, + self._spin_pursuit_throttle, + self._spin_pursuit_speed_k, + ] # --- プリセット管理 --- self._pd_preset_combo = QComboBox() @@ -273,6 +403,9 @@ layout.addLayout(btn_layout) # コールバック接続 + self._steering_combo.currentIndexChanged.connect( + self._on_steering_method_changed, + ) for spin in [ self._spin_kp, self._spin_kh, self._spin_kd, self._spin_max_steer_rate, @@ -281,12 +414,19 @@ spin.valueChanged.connect( self._on_param_changed, ) + for spin in self._pursuit_widgets: + spin.valueChanged.connect( + self._on_pursuit_param_changed, + ) self._pd_preset_combo.currentIndexChanged \ .connect(self._on_pd_preset_selected) self._pd_presets: list[PdPreset] = [] self._refresh_pd_presets() + # 初期表示の更新 + self._on_steering_method_changed() + parent_layout.addWidget(group) def _setup_image_param_ui( @@ -379,6 +519,19 @@ {"dual_norm"}, ) + self._spin_global_thresh = QSpinBox() + self._spin_global_thresh.setRange(0, 255) + self._spin_global_thresh.setValue( + ip.global_thresh, + ) + self._spin_global_thresh.setSpecialValueText( + "無効", + ) + self._add_image_row( + "固定閾値:", self._spin_global_thresh, + {"dual_norm"}, + ) + # --- 案B/C: 適応的閾値 --- self._spin_adaptive_block = QSpinBox() self._spin_adaptive_block.setRange(3, 999) @@ -429,6 +582,48 @@ {"blackhat", "dual_norm", "robust"}, ) + # --- 案B: 段階クロージング --- + self._spin_stage_close_small = QSpinBox() + self._spin_stage_close_small.setRange(1, 999) + self._spin_stage_close_small.setSingleStep(2) + self._spin_stage_close_small.setValue( + ip.stage_close_small, + ) + self._add_image_row( + "段階穴埋め(小):", + self._spin_stage_close_small, + {"dual_norm"}, + ) + + self._spin_stage_min_area = QSpinBox() + self._spin_stage_min_area.setRange(0, 99999) + self._spin_stage_min_area.setValue( + ip.stage_min_area, + ) + self._spin_stage_min_area.setSpecialValueText( + "無効", + ) + self._add_image_row( + "孤立除去面積:", + self._spin_stage_min_area, + {"dual_norm"}, + ) + + self._spin_stage_close_large = QSpinBox() + self._spin_stage_close_large.setRange(0, 999) + self._spin_stage_close_large.setSingleStep(2) + self._spin_stage_close_large.setValue( + ip.stage_close_large, + ) + self._spin_stage_close_large.setSpecialValueText( + "無効", + ) + self._add_image_row( + "段階穴埋め(大):", + self._spin_stage_close_large, + {"dual_norm"}, + ) + # --- ロバストフィッティング(全手法共通) --- all_methods = { "blackhat", "dual_norm", "robust", "valley", @@ -627,6 +822,7 @@ if self._auto_save_enabled: new_ip = load_detect_params(method) self._pd_control.image_params = new_ip + self._pursuit_control.image_params = new_ip self._sync_image_spinboxes() save_control( self._pd_control.params, method, @@ -671,6 +867,9 @@ self._spin_bg_blur_ksize.setValue( ip.bg_blur_ksize, ) + self._spin_global_thresh.setValue( + ip.global_thresh, + ) self._spin_adaptive_block.setValue( ip.adaptive_block, ) @@ -686,6 +885,15 @@ self._spin_min_line_width.setValue( ip.min_line_width, ) + self._spin_stage_close_small.setValue( + ip.stage_close_small, + ) + self._spin_stage_min_area.setValue( + ip.stage_min_area, + ) + self._spin_stage_close_large.setValue( + ip.stage_close_large, + ) self._spin_median_ksize.setValue( ip.median_ksize, ) @@ -740,6 +948,9 @@ ip.bg_blur_ksize = ( self._spin_bg_blur_ksize.value() ) + ip.global_thresh = ( + self._spin_global_thresh.value() + ) # 案B/C: 適応的閾値 ip.adaptive_block = ( self._spin_adaptive_block.value() @@ -755,6 +966,16 @@ ip.min_line_width = ( self._spin_min_line_width.value() ) + # 案B: 段階クロージング + ip.stage_close_small = ( + self._spin_stage_close_small.value() + ) + ip.stage_min_area = ( + self._spin_stage_min_area.value() + ) + ip.stage_close_large = ( + self._spin_stage_close_large.value() + ) # ロバストフィッティング ip.median_ksize = ( self._spin_median_ksize.value() @@ -837,6 +1058,7 @@ try: ip = self._image_presets[idx].image_params self._pd_control.image_params = ip + self._pursuit_control.image_params = ip self._sync_image_spinboxes() finally: self._auto_save_enabled = True @@ -989,7 +1211,6 @@ ("検出領域", "detect_region"), ("フィッティング曲線", "poly_curve"), ("中心線", "center_line"), - ("検出情報", "info_text"), ] for label, attr in items: cb = QCheckBox(label) @@ -1001,15 +1222,6 @@ parent_layout.addWidget(group) - def _has_any_overlay(self) -> bool: - """いずれかのオーバーレイが有効かを返す""" - f = self._overlay_flags - return ( - f.binary or f.detect_region - or f.poly_curve or f.center_line - or f.info_text - ) - @staticmethod def _create_spin( value: float, min_val: float, @@ -1027,7 +1239,7 @@ return spin def _on_param_changed(self) -> None: - """パラメータ SpinBox の値が変更されたときに反映する""" + """PD パラメータ SpinBox の値が変更されたときに反映する""" p = self._pd_control.params p.kp = self._spin_kp.value() p.kh = self._spin_kh.value() @@ -1044,6 +1256,54 @@ self._pd_control.image_params.method, ) + def _on_pursuit_param_changed(self) -> None: + """Pursuit パラメータの変更を反映する""" + p = self._pursuit_control.params + p.near_ratio = self._spin_near_ratio.value() + p.far_ratio = self._spin_far_ratio.value() + p.k_near = self._spin_k_near.value() + p.k_far = self._spin_k_far.value() + p.max_steer_rate = ( + self._spin_pursuit_steer_rate.value() + ) + p.max_throttle = ( + self._spin_pursuit_throttle.value() + ) + p.speed_k = self._spin_pursuit_speed_k.value() + + def _on_steering_method_changed(self) -> None: + """制御手法の変更を反映する""" + method = self._steering_combo.currentData() + self._steering_method = method + is_pd = method == "pd" + + # PD 固有ウィジェットの表示切替 + for w in self._pd_widgets: + w.setVisible(is_pd) + label = self._pd_param_form.labelForField(w) + if label: + label.setVisible(is_pd) + + # 共通ウィジェット(操舵制限/最大速度/減速係数) + for w in [ + self._spin_max_steer_rate, + self._spin_max_throttle, + self._spin_speed_k, + ]: + w.setVisible(is_pd) + label = self._pd_param_form.labelForField(w) + if label: + label.setVisible(is_pd) + + # Pursuit ウィジェットの表示切替 + for w in self._pursuit_widgets: + w.setVisible(not is_pd) + label = ( + self._pursuit_param_form.labelForField(w) + ) + if label: + label.setVisible(not is_pd) + def _setup_timers(self) -> None: """タイマーを設定する""" # 映像更新用 @@ -1108,7 +1368,7 @@ def _enable_auto(self) -> None: """自動操縦を開始する""" self._is_auto = True - self._pd_control.reset() + self._active_control.reset() self._pressed_keys.clear() self._auto_btn.setText("自動操縦 OFF") self._status_label.setText("接続中 (自動操縦)") @@ -1131,26 +1391,38 @@ return self._latest_frame = frame - # 自動操縦時は操舵量を計算 + # 線検出は常に実行(検出情報ラベル表示のため) if self._is_auto: - output = self._pd_control.compute(frame) + ctrl = self._active_control + output = ctrl.compute(frame) self._throttle = output.throttle self._steer = output.steer self._update_control_label() self._last_detect_result = ( - self._pd_control.last_detect_result + ctrl.last_detect_result ) - elif self._has_any_overlay(): - # 手動操作中でもオーバーレイ用に線検出を実行 + else: self._last_detect_result = detect_line( frame, self._pd_control.image_params, ) - else: - self._last_detect_result = None self._display_frame(frame) + def _update_detect_info_label(self) -> None: + """検出情報ラベルを更新する""" + r = self._last_detect_result + if r is None or not r.detected: + self._detect_info_label.setText( + "pos: --- head: --- curv: ---" + ) + return + self._detect_info_label.setText( + f"pos: {r.position_error:+.3f}" + f" head: {r.heading:+.4f}" + f" curv: {r.curvature:+.6f}" + ) + def _display_frame(self, frame: np.ndarray) -> None: """NumPy 配列の画像を QLabel に表示する @@ -1166,6 +1438,9 @@ self._overlay_flags, ) + # 検出情報をラベルに表示 + self._update_detect_info_label() + # BGR → RGB 変換 rgb = bgr[:, :, ::-1].copy() h, w, ch = rgb.shape @@ -1173,12 +1448,16 @@ rgb.data, w, h, ch * w, QImage.Format.Format_RGB888, ) - # スケーリングして表示 - scaled_w = int(w * DISPLAY_SCALE) - scaled_h = int(h * DISPLAY_SCALE) + # 表示サイズは常に原寸基準で固定 + disp_w = int( + config.FRAME_WIDTH * DISPLAY_SCALE, + ) + disp_h = int( + config.FRAME_HEIGHT * DISPLAY_SCALE, + ) pixmap = QPixmap.fromImage(image).scaled( - scaled_w, - scaled_h, + disp_w, + disp_h, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation, ) diff --git a/src/pc/vision/overlay.py b/src/pc/vision/overlay.py index 188a1e8..860e0f9 100644 --- a/src/pc/vision/overlay.py +++ b/src/pc/vision/overlay.py @@ -9,14 +9,11 @@ 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_LINE: tuple = (0, 255, 0) COLOR_CENTER: tuple = (0, 255, 255) -COLOR_TEXT: tuple = (255, 255, 255) COLOR_REGION: tuple = (255, 0, 0) # 二値化オーバーレイの不透明度 @@ -32,13 +29,11 @@ detect_region: 検出領域の枠 poly_curve: フィッティング曲線 center_line: 画像中心線 - info_text: 検出情報の数値表示 """ binary: bool = False detect_region: bool = False poly_curve: bool = False center_line: bool = False - info_text: bool = False def draw_overlay( @@ -57,6 +52,7 @@ オーバーレイ描画済みの画像 """ display = frame.copy() + h, w = display.shape[:2] if result is None: return display @@ -71,21 +67,18 @@ if flags.detect_region: cv2.rectangle( display, - (0, line_detector.DETECT_Y_START), - ( - config.FRAME_WIDTH - 1, - line_detector.DETECT_Y_END - 1, - ), + (0, 0), + (w - 1, h - 1), COLOR_REGION, 1, ) # 画像中心線 if flags.center_line: - center_x = config.FRAME_WIDTH // 2 + center_x = w // 2 cv2.line( display, (center_x, 0), - (center_x, config.FRAME_HEIGHT), + (center_x, h), COLOR_CENTER, 1, ) @@ -93,10 +86,6 @@ if flags.poly_curve and result.poly_coeffs is not None: _draw_poly_curve(display, result.poly_coeffs) - # 検出情報の数値表示 - if flags.info_text: - _draw_info_text(display, result) - return display @@ -132,55 +121,23 @@ frame: 描画先の画像 coeffs: 多項式の係数 """ + h, w = frame.shape[:2] poly = np.poly1d(coeffs) - y_start = line_detector.DETECT_Y_START - y_end = line_detector.DETECT_Y_END # 曲線上の点を生成 - ys = np.arange(y_start, y_end) + ys = np.arange(0, h) xs = poly(ys) # 画像範囲内の点のみ描画 points = [] for x, y in zip(xs, ys): ix = int(round(x)) - if 0 <= ix < config.FRAME_WIDTH: + if 0 <= ix < w: points.append([ix, int(y)]) if len(points) >= 2: pts = np.array(points, dtype=np.int32) cv2.polylines( frame, [pts], False, - COLOR_LINE, 2, - ) - - -def _draw_info_text( - frame: np.ndarray, - result: LineDetectResult, -) -> None: - """検出情報の数値を画像に描画する - - Args: - frame: 描画先の画像 - result: 線検出の結果 - """ - if not result.detected: - cv2.putText( - frame, "LINE: N/A", (5, 15), - cv2.FONT_HERSHEY_SIMPLEX, 0.4, - COLOR_TEXT, 1, - ) - return - - lines = [ - f"pos: {result.position_error:+.3f}", - f"head: {result.heading:+.4f}", - f"curv: {result.curvature:+.6f}", - ] - for i, text in enumerate(lines): - cv2.putText( - frame, text, (5, 15 + i * 15), - cv2.FONT_HERSHEY_SIMPLEX, 0.35, - COLOR_TEXT, 1, + COLOR_LINE, 1, ) diff --git a/src/pi/camera/capture.py b/src/pi/camera/capture.py index 3e63916..9b89163 100644 --- a/src/pi/camera/capture.py +++ b/src/pi/camera/capture.py @@ -3,6 +3,7 @@ Picamera2 を使用してカメラ画像を取得するモジュール """ +import cv2 import numpy as np from picamera2 import Picamera2 @@ -20,7 +21,10 @@ self._camera = Picamera2() camera_config = self._camera.create_preview_configuration( main={ - "size": (config.FRAME_WIDTH, config.FRAME_HEIGHT), + "size": ( + config.CAPTURE_WIDTH, + config.CAPTURE_HEIGHT, + ), "format": "YUV420", }, ) @@ -30,11 +34,21 @@ def capture(self) -> np.ndarray: """1フレームを取得する + 撮影後に INTER_AREA で縮小して返す + Returns: グレースケールの画像(NumPy 配列) """ yuv = self._camera.capture_array() - return yuv[:config.FRAME_HEIGHT, :config.FRAME_WIDTH] + gray = yuv[ + :config.CAPTURE_HEIGHT, + :config.CAPTURE_WIDTH, + ] + return cv2.resize( + gray, + (config.FRAME_WIDTH, config.FRAME_HEIGHT), + interpolation=cv2.INTER_AREA, + ) def stop(self) -> None: """カメラを停止する"""