|
|
@@ -1,4 +1,5 @@
|
|
|
import os
|
|
|
+import time
|
|
|
import logging
|
|
|
import threading
|
|
|
import queue
|
|
|
@@ -12,7 +13,6 @@ from .rtsp_player import RTSPPlayer
|
|
|
from .rfid_client import RFIDClient
|
|
|
from .modbus_client import ModbusClient
|
|
|
from .mqtt_client import MQTTClient
|
|
|
-import pyttsx3
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@@ -54,11 +54,13 @@ class MainWindow(QMainWindow):
|
|
|
self.prev_left_lug = None
|
|
|
self.prev_right_lug = None
|
|
|
|
|
|
- # 语音引擎
|
|
|
- self.tts_engine = pyttsx3.init()
|
|
|
- self.tts_engine.setProperty('rate', 150)
|
|
|
+ # 语音引擎(在工作线程内初始化)
|
|
|
self.last_voice_state = None
|
|
|
+ self._last_hooking_voice_time = 0.0
|
|
|
self.voice_enabled = True
|
|
|
+ self.ai_enabled = True
|
|
|
+ self._last_left_draw: list = []
|
|
|
+ self._last_right_draw: list = []
|
|
|
self._voice_queue = queue.Queue()
|
|
|
self._voice_thread = threading.Thread(target=self._voice_worker, daemon=True)
|
|
|
self._voice_thread.start()
|
|
|
@@ -238,6 +240,11 @@ class MainWindow(QMainWindow):
|
|
|
self.voice_checkbox.setStyleSheet("color: #606266; font-weight: bold;")
|
|
|
self.voice_checkbox.stateChanged.connect(self.on_voice_toggle)
|
|
|
|
|
|
+ self.ai_checkbox = QCheckBox("AI启用")
|
|
|
+ self.ai_checkbox.setChecked(True)
|
|
|
+ self.ai_checkbox.setStyleSheet("color: #606266; font-weight: bold;")
|
|
|
+ self.ai_checkbox.stateChanged.connect(self.on_ai_toggle)
|
|
|
+
|
|
|
self.auto_switch_btn = QPushButton("自动模式")
|
|
|
self.auto_switch_btn.setCheckable(True)
|
|
|
self.auto_switch_btn.setChecked(True)
|
|
|
@@ -249,6 +256,7 @@ class MainWindow(QMainWindow):
|
|
|
status_layout.addWidget(self.hook_status_label)
|
|
|
status_layout.addStretch()
|
|
|
status_layout.addWidget(self.voice_checkbox)
|
|
|
+ status_layout.addWidget(self.ai_checkbox)
|
|
|
status_layout.addWidget(self.auto_switch_btn)
|
|
|
main_layout.addWidget(status_widget)
|
|
|
|
|
|
@@ -273,6 +281,33 @@ class MainWindow(QMainWindow):
|
|
|
def on_voice_toggle(self, state):
|
|
|
self.voice_enabled = (state == Qt.Checked)
|
|
|
|
|
|
+ def on_ai_toggle(self, state):
|
|
|
+ enabled = (state == Qt.Checked)
|
|
|
+ if enabled == self.ai_enabled:
|
|
|
+ return
|
|
|
+ self.ai_enabled = enabled
|
|
|
+ if not enabled:
|
|
|
+ self.hook_state = HookState.IDLE
|
|
|
+ self.prev_state = HookState.IDLE
|
|
|
+ self.state_counter = 0
|
|
|
+ self.left_objects = []
|
|
|
+ self.right_objects = []
|
|
|
+ self.prev_left_hook = None
|
|
|
+ self.prev_right_hook = None
|
|
|
+ self.prev_left_lug = None
|
|
|
+ self.prev_right_lug = None
|
|
|
+ self.update_ui_for_state(HookState.IDLE)
|
|
|
+ if self.left_player:
|
|
|
+ self.left_player.set_ai_objects([])
|
|
|
+ if self.right_player:
|
|
|
+ self.right_player.set_ai_objects([])
|
|
|
+ else:
|
|
|
+ # 恢复最近一帧的标注框
|
|
|
+ if self.left_player and self._last_left_draw:
|
|
|
+ self.left_player.set_ai_objects(self._last_left_draw)
|
|
|
+ if self.right_player and self._last_right_draw:
|
|
|
+ self.right_player.set_ai_objects(self._last_right_draw)
|
|
|
+
|
|
|
def _speak(self, msg: str):
|
|
|
self._voice_queue.put(msg)
|
|
|
|
|
|
@@ -282,13 +317,19 @@ class MainWindow(QMainWindow):
|
|
|
pythoncom.CoInitialize()
|
|
|
except ImportError:
|
|
|
pass
|
|
|
+ try:
|
|
|
+ import win32com.client as _win32com
|
|
|
+ except ImportError:
|
|
|
+ logging.warning("pywin32 未安装,语音播报不可用")
|
|
|
+ return
|
|
|
while True:
|
|
|
msg = self._voice_queue.get()
|
|
|
if msg is None:
|
|
|
break
|
|
|
try:
|
|
|
- self.tts_engine.say(msg)
|
|
|
- self.tts_engine.runAndWait()
|
|
|
+ speaker = _win32com.Dispatch("SAPI.SpVoice")
|
|
|
+ speaker.Rate = 2
|
|
|
+ speaker.Speak(msg)
|
|
|
except Exception as e:
|
|
|
logging.warning(f"语音播报失败: {e}")
|
|
|
self._voice_queue.task_done()
|
|
|
@@ -481,6 +522,7 @@ class MainWindow(QMainWindow):
|
|
|
self.prev_left_lug = None
|
|
|
self.prev_right_lug = None
|
|
|
self.last_voice_state = None
|
|
|
+ self._last_hooking_voice_time = 0.0
|
|
|
|
|
|
if bucket:
|
|
|
left_cam = next((c for c in bucket.cameras if c.position == CameraPosition.LEFT), None)
|
|
|
@@ -599,6 +641,8 @@ class MainWindow(QMainWindow):
|
|
|
return
|
|
|
if not self.active_bucket:
|
|
|
return
|
|
|
+ if not self.ai_enabled:
|
|
|
+ return
|
|
|
for cam in self.active_bucket.cameras:
|
|
|
if cam.topic == topic:
|
|
|
self.update_video_size()
|
|
|
@@ -612,22 +656,31 @@ class MainWindow(QMainWindow):
|
|
|
display_width = 640
|
|
|
display_height = 360
|
|
|
|
|
|
- # 解析所有对象(包括 ready)用于状态判断
|
|
|
+ # 解析所有对象(包括 ready)
|
|
|
all_objects = self.parse_ai_objects(payload, display_width, display_height, include_ready=True)
|
|
|
- # 解析仅用于绘制的对象(排除 ready)
|
|
|
- draw_objects = self.parse_ai_objects(payload, display_width, display_height, include_ready=False)
|
|
|
|
|
|
if cam.position == CameraPosition.LEFT:
|
|
|
self.left_objects = all_objects
|
|
|
+ else:
|
|
|
+ self.right_objects = all_objects
|
|
|
+
|
|
|
+ # 触发状态评估(会标记配对的 hook/lug)
|
|
|
+ self.evaluate_hook_state()
|
|
|
+
|
|
|
+ # 构造绘制对象(排除 ready,无关目标变淡)
|
|
|
+ draw_objects = [o for o in all_objects if 'ready' not in o['label'].lower()]
|
|
|
+ for obj in draw_objects:
|
|
|
+ if not obj.get('paired'):
|
|
|
+ obj['dimmed'] = True
|
|
|
+
|
|
|
+ if cam.position == CameraPosition.LEFT:
|
|
|
+ self._last_left_draw = draw_objects
|
|
|
if self.left_player:
|
|
|
self.left_player.set_ai_objects(draw_objects)
|
|
|
else:
|
|
|
- self.right_objects = all_objects
|
|
|
+ self._last_right_draw = draw_objects
|
|
|
if self.right_player:
|
|
|
self.right_player.set_ai_objects(draw_objects)
|
|
|
-
|
|
|
- # 触发状态评估
|
|
|
- self.evaluate_hook_state()
|
|
|
break
|
|
|
|
|
|
# ---------- 挂钩状态评估 ----------
|
|
|
@@ -692,41 +745,54 @@ class MainWindow(QMainWindow):
|
|
|
new_state = HookState.RELEASING
|
|
|
else:
|
|
|
any_ready = any('ready' in o['label'].lower() for o in self.left_objects + self.right_objects)
|
|
|
- if any_ready:
|
|
|
+ has_intersecting_pair = self._has_intersecting_pair(left_pair, right_pair)
|
|
|
+ if any_ready and has_intersecting_pair:
|
|
|
new_state = HookState.READY
|
|
|
+ elif has_intersecting_pair:
|
|
|
+ new_state = HookState.HOOKING
|
|
|
else:
|
|
|
- intersecting = False
|
|
|
- if left_pair:
|
|
|
- h, l = left_pair
|
|
|
- if self.rects_intersect(h, l):
|
|
|
- intersecting = True
|
|
|
- if not intersecting and right_pair:
|
|
|
- h, l = right_pair
|
|
|
- if self.rects_intersect(h, l):
|
|
|
- intersecting = True
|
|
|
- if intersecting:
|
|
|
- new_state = HookState.HOOKING
|
|
|
- else:
|
|
|
- new_state = HookState.IDLE
|
|
|
+ new_state = HookState.IDLE
|
|
|
|
|
|
- # 防抖
|
|
|
+ # 防抖(非对称:进入非空闲状态需 STATE_THRESHOLD 帧确认,回到空闲只需 1 帧)
|
|
|
+ threshold = 1 if new_state == HookState.IDLE else self.STATE_THRESHOLD
|
|
|
if new_state == self.prev_state:
|
|
|
self.state_counter += 1
|
|
|
else:
|
|
|
self.prev_state = new_state
|
|
|
self.state_counter = 1
|
|
|
|
|
|
- if self.state_counter >= self.STATE_THRESHOLD:
|
|
|
+ if self.state_counter >= threshold:
|
|
|
if new_state != self.hook_state:
|
|
|
self.hook_state = new_state
|
|
|
self.update_ui_for_state(new_state)
|
|
|
self.state_counter = 0
|
|
|
|
|
|
+ # 标记配对的 hook/lug(用于绘制时变淡无关目标)
|
|
|
+ for obj in self.left_objects + self.right_objects:
|
|
|
+ obj.pop('paired', None)
|
|
|
+ if left_pair:
|
|
|
+ left_pair[0]['paired'] = True
|
|
|
+ left_pair[1]['paired'] = True
|
|
|
+ if right_pair:
|
|
|
+ right_pair[0]['paired'] = True
|
|
|
+ right_pair[1]['paired'] = True
|
|
|
+
|
|
|
def rects_intersect(self, a, b):
|
|
|
ax1, ay1, ax2, ay2 = a['x'], a['y'], a['x']+a['w'], a['y']+a['h']
|
|
|
bx1, by1, bx2, by2 = b['x'], b['y'], b['x']+b['w'], b['y']+b['h']
|
|
|
return not (ax2 < bx1 or bx2 < ax1 or ay2 < by1 or by2 < ay1)
|
|
|
|
|
|
+ def _has_intersecting_pair(self, left_pair, right_pair):
|
|
|
+ if left_pair:
|
|
|
+ h, l = left_pair
|
|
|
+ if self.rects_intersect(h, l):
|
|
|
+ return True
|
|
|
+ if right_pair:
|
|
|
+ h, l = right_pair
|
|
|
+ if self.rects_intersect(h, l):
|
|
|
+ return True
|
|
|
+ return False
|
|
|
+
|
|
|
# ---------- UI 更新与语音 ----------
|
|
|
def update_ui_for_state(self, state):
|
|
|
if state == HookState.IDLE:
|
|
|
@@ -760,10 +826,16 @@ class MainWindow(QMainWindow):
|
|
|
self.right_label.setStyleSheet(style)
|
|
|
|
|
|
# 语音播报(带开关和防重复)
|
|
|
- if self.voice_enabled and state in (HookState.HOOKING, HookState.READY) and state != self.last_voice_state:
|
|
|
- msg = "挂包中" if state == HookState.HOOKING else "挂包成功"
|
|
|
- self._speak(msg)
|
|
|
- self.last_voice_state = state
|
|
|
+ if self.voice_enabled and state in (HookState.HOOKING, HookState.READY):
|
|
|
+ now = time.time()
|
|
|
+ if state == HookState.HOOKING:
|
|
|
+ if state != self.last_voice_state or now - self._last_hooking_voice_time > 15:
|
|
|
+ self._speak("挂包中")
|
|
|
+ self.last_voice_state = state
|
|
|
+ self._last_hooking_voice_time = now
|
|
|
+ elif state != self.last_voice_state:
|
|
|
+ self._speak("挂包成功")
|
|
|
+ self.last_voice_state = state
|
|
|
|
|
|
# ---------- 定时任务 ----------
|
|
|
def check_idle(self):
|