''' Importing necessary libraries ''' import kp, cv2, os, sys, json, time, threading, queue, numpy as np, importlib.util, shlex, subprocess, shutil from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QLabel, QPushButton, QComboBox, QFileDialog, QMessageBox, QHBoxLayout, QDialog, QListWidget, QScrollArea, QFrame, QListWidgetItem, QTextEdit, QGridLayout) from PyQt5.QtSvg import QSvgWidget from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal from PyQt5.QtGui import QPixmap, QMovie, QImage from ..config import (UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR, SECONDARY_COLOR, BUTTON_STYLE, MASK_STYLE, PROJECT_ROOT, SCRIPT_CONFIG, UTILS_DIR , UPLOAD_DIR, FW_DIR, DongleModelMap, DongleIconMap) from ..services.device_service import check_available_device #──────────────────────────────────────────────────────────── # VideoThread:持續從攝影機擷取影像 class VideoThread(QThread): change_pixmap_signal = pyqtSignal(QImage) def __init__(self): super().__init__() self._run_flag = True def run(self): cap = cv2.VideoCapture(0) if not cap.isOpened(): print("Cannot open camera") self._run_flag = False while self._run_flag: ret, frame = cap.read() if ret: # 轉成 RGB 格式 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) height, width, channel = frame.shape bytes_per_line = channel * width qt_image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888) self.change_pixmap_signal.emit(qt_image) # 可依需求延遲控制幀率 cap.release() def stop(self): self._run_flag = False self.wait() #──────────────────────────────────────────────────────────── # 輔助 function:將 QImage 轉成 numpy 陣列 def qimage_to_numpy(qimage): qimage = qimage.convertToFormat(QImage.Format_RGB888) width = qimage.width() height = qimage.height() ptr = qimage.bits() ptr.setsize(qimage.byteCount()) arr = np.array(ptr).reshape(height, width, 3) return arr #──────────────────────────────────────────────────────────── # 動態載入 inference 模組的函式 def load_inference_module(mode, model_name): script_path = os.path.join(UTILS_DIR, mode, model_name, "script.py") module_name = f"{mode}_{model_name}" spec = importlib.util.spec_from_file_location(module_name, script_path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module #──────────────────────────────────────────────────────────── # InferenceWorkerThread:從 frame_queue 中取出 frame,依照設定頻率處理 frame, # 利用動態載入的 inference 模組進行推論,並對結果進行緩存。 class InferenceWorkerThread(QThread): # 傳出 inference 結果,型態可依需求調整(例如 dict 或 tuple) inference_result_signal = pyqtSignal(object) def __init__(self, frame_queue, mode, model_name, min_interval=0.5, mse_threshold=500, once_mode=False): """ frame_queue: 傳入的 frame 佇列(numpy 陣列) mode: 模式名稱 (如 'face_recognition') model_name: 模型名稱 (如 'face_detection') min_interval: 最小 inference 間隔 (秒) mse_threshold: 當前後 frame 之均方誤差低於此值則視為相似 """ super().__init__() self.frame_queue = frame_queue self.mode = mode self.model_name = model_name self.min_interval = min_interval self.mse_threshold = mse_threshold self._running = True self.once_mode = once_mode self.last_inference_time = 0 self.last_frame = None self.cached_result = None # 動態載入 inference 模組 script_path = os.path.join(UTILS_DIR, mode, model_name, "script.py") self.inference_module = load_inference_module(mode, model_name) def run(self): while self._running: try: # 若佇列空,等待 0.1 秒 frame = self.frame_queue.get(timeout=0.1) except queue.Empty: continue current_time = time.time() # 檢查頻率:如果離上次 inference 還不到 min_interval,則跳過處理 if current_time - self.last_inference_time < self.min_interval: continue # 如果有緩存結果且 frame 與上次非常相似則直接使用 if self.last_frame is not None: mse = np.mean((frame.astype(np.float32) - self.last_frame.astype(np.float32)) ** 2) if mse < self.mse_threshold and self.cached_result is not None: self.inference_result_signal.emit(self.cached_result) if self.once_mode: # 停止之後再跳出迴圈 self._running = False break continue # 呼叫動態載入的 inference 模組處理 frame try: # 使用從主程式傳遞過來的 input_params result = self.inference_module.inference(frame, params=self.input_params) except Exception as e: print(f"Inference error: {e}") result = None self.last_inference_time = current_time self.last_frame = frame.copy() self.cached_result = result self.inference_result_signal.emit(result) if self.once_mode: # 設定停止旗標,然後退出迴圈 self._running = False break # 當迴圈結束時呼叫 quit() 以讓 thread 結束 self.quit() def stop(self): self._running = False self.wait() class MainWindow(QWidget): def __init__(self): super().__init__() self.connected_devices = [] self.video_thread = None self.recording = False self.recording_audio = False self.video_writer = None self.recorded_frames = [] self.destination = None self.current_tool_config = None self.inference_worker = None self.inference_queue = queue.Queue(maxsize=10) # 確保目錄存在並更新配置 self.generate_global_config() self.init_ui() def init_ui(self): # 初始化UI (暫時不需要修改) try: # 基本視窗設定 self.setGeometry(100, 100, *WINDOW_SIZE) self.setWindowTitle('Innovedus AI Playground') self.setStyleSheet(f"background-color: {BACKGROUND_COLOR};") # 主要佈局 self.layout = QVBoxLayout(self) self.layout.setContentsMargins(0, 0, 0, 0) self.setLayout(self.layout) # 載入歡迎畫面 self.show_welcome_label() # 0.5秒後切換到主頁面和設備連接彈窗 QTimer.singleShot(500, self.show_device_popup_and_main_page) except Exception as e: print(f"Error in init_ui: {e}") def show_welcome_label(self): # 歡迎頁面(暫時不需要修改) try: welcome_label = QLabel(self) logo_path = os.path.join(UXUI_ASSETS, "Assets_png/kneron_logo.png") print(f"Loading logo from: {logo_path}") if not os.path.exists(logo_path): print(f"錯誤:找不到圖片檔案:{logo_path}") return welcome_pixmap = QPixmap(logo_path) if welcome_pixmap.isNull(): print(f"錯誤:無法載入圖片:{logo_path}") return welcome_label.setPixmap(welcome_pixmap) welcome_label.setAlignment(Qt.AlignCenter) self.layout.addWidget(welcome_label) except Exception as e: print(f"Error in show_welcome_label: {e}") def device_popup_mask_setup(self): # TODO: 需要修改 popup windows 的佈局 try: print("setting up popup mask") # 添加半透明遮罩 self.overlay = QWidget(self) # 確保遮罩層在最上層 self.overlay.raise_() self.overlay.setStyleSheet(MASK_STYLE) self.overlay.setGeometry(0, 0, self.width(), self.height()) self.overlay_layout = QVBoxLayout(self.overlay) # 設備連接彈窗 self.device_popup = self.create_device_popup() self.overlay_layout.addWidget(self.device_popup, alignment=Qt.AlignCenter) print("finish popup windows setup") except Exception as e: print(f"Error in device_popup_mask_setup: {e}") def show_device_popup_and_main_page(self): try: # 清除歡迎頁面 self.clear_layout() # 1. 先初始化主頁面 self.main_page = self.create_main_page() self.layout.addWidget(self.main_page) print("finish setup main page") # 2. 再設定 popup 和遮罩 self.device_popup_mask_setup() # 3. 刷新設備 self.refresh_devices() # 4. 顯示 popup self.show_device_popup() # 5. 啟動相機 -> 這邊先註解掉, 上傳資料和使用相機會打架 -> 移到 select tool 的部分決定是否啟動相機 (20250206) # self.start_camera() except Exception as e: print(f"Error in show_device_popup_and_main_page: {e}") def clear_layout(self): try: # 清除所有小工具 while self.layout.count(): child = self.layout.takeAt(0) if child.widget(): child.widget().deleteLater() except Exception as e: print(f"Error in clear_layout: {e}") def create_main_page(self): try: main_page = QWidget(self) main_layout = QHBoxLayout(main_page) main_page.setLayout(main_layout) # 左側佈局 left_layout = QVBoxLayout() main_layout.addLayout(left_layout, 1) # 添加 Kneron logo logo_label = QLabel() logo_path = os.path.join(UXUI_ASSETS, "Assets_png/kneron_logo.png") logo_pixmap = QPixmap(logo_path) scaled_logo = logo_pixmap.scaled(104, 40, Qt.KeepAspectRatio, Qt.SmoothTransformation) logo_label.setPixmap(scaled_logo) left_layout.addWidget(logo_label) # 添加其他元件 self.create_device_layout(left_layout) self.create_ai_toolbox(left_layout) # 右側佈局 right_container = QWidget() right_layout = QGridLayout(right_container) right_layout.setContentsMargins(0, 0, 0, 0) right_layout.setSpacing(0) main_layout.addWidget(right_container, 2) # 添加 Canvas Area self.canvas_area = self.create_canvas_area() right_layout.addWidget(self.canvas_area, 0, 0, 1, 1) # 添加到 (0,0) 格子,跨越1行1列 # 添加 Media Panel,並對齊到右下角 self.media_panel = self.create_media_panel() right_layout.addWidget(self.media_panel, 0, 0, Qt.AlignBottom | Qt.AlignRight) # 疊加在 (0,0) 格子,右下對齊 main_layout.addWidget(right_container, 2) return main_page except Exception as e: print(f"Error in create_main_page: {e}") # 主畫面右上角的 setting button, 還沒連接任何視窗畫面 def create_settings_button(self): """ 建立並回傳一個 'Settings' 按鈕 (內含 SVG) 的物件, 並設定好大小、樣式等。 """ button = QPushButton(self) button.setFixedSize(106, 24) # 您說明的尺寸 # 建立一個 QSvgWidget 放在按鈕上 svg_widget = QSvgWidget(os.path.join(UXUI_ASSETS, "Assets_svg", "btn_setting.svg")) svg_widget.setFixedSize(106, 24) # 建立水平布局,使 SVG 可以貼齊按鈕 layout = QHBoxLayout(button) layout.setContentsMargins(0, 0, 0, 0) # 去除預設邊距 layout.addWidget(svg_widget) # 若要讓按鈕有點透明或 hover/press 效果,可再加上 styleSheet button.setStyleSheet(""" QPushButton { border: none; background: transparent; } QPushButton:hover { background-color: rgba(255, 255, 255, 50); } QPushButton:pressed { background-color: rgba(255, 255, 255, 100); } """) # 可依需求綁定點擊事件 # button.clicked.connect(self.on_settings_button_clicked) return button def create_device_layout(self, layout): try: devices_frame = QFrame(self) devices_frame.setStyleSheet(f"border: none; background: {SECONDARY_COLOR}; border-radius: 15px;") # 根據連接設備數量設置高度 base_height = 250 # 基本高度 print(self.connected_devices) extra_height = 100 if len(self.connected_devices) > 1 else 0 # 如果設備超過2個,增加100 devices_frame.setFixedHeight(base_height + extra_height) devices_frame.setFixedWidth(240) devices_layout = QVBoxLayout(devices_frame) # 標題 title_layout = QHBoxLayout() # 容器來放置圖示和標籤 title_container = QWidget() container_layout = QHBoxLayout(title_container) container_layout.setSpacing(10) device_icon = QSvgWidget(os.path.join(UXUI_ASSETS, "Assets_svg/ic_window_device.svg")) device_icon.setFixedSize(20, 20) container_layout.addWidget(device_icon) title_label = QLabel("Device") title_label.setStyleSheet("color: white; font-size: 20px; font-weight: bold;") container_layout.addWidget(title_label) # 將容器添加到標題布局中 title_layout.addWidget(title_container) devices_layout.addLayout(title_layout) # 設備列表 self.device_list_widget = QListWidget(self) devices_layout.addWidget(self.device_list_widget) # 詳細按鈕 detail_button = QPushButton("Details", self) detail_button.setStyleSheet(BUTTON_STYLE) detail_button.setFixedSize(72, 30) # 固定按鈕大小 detail_button.clicked.connect(self.show_device_details) # 創建一個容器用於按鈕置中 button_container = QWidget() button_layout = QHBoxLayout(button_container) button_layout.addWidget(detail_button, alignment=Qt.AlignCenter) # 按鈕置中 button_layout.setContentsMargins(0, 0, 0, 0) # 去掉邊距 devices_layout.addWidget(button_container) # 添加容器到主布局 layout.addWidget(devices_frame) except Exception as e: print(f"Error in create_device_layout: {e}") def update_device_frame_size(self): """更新設備框架的大小""" if hasattr(self, 'device_list_widget'): frame = self.device_list_widget.parent() while not isinstance(frame, QFrame): frame = frame.parent() base_height = 300 extra_height = 100 if len(self.connected_devices) > 2 else 0 frame.setFixedHeight(base_height + extra_height) def select_device(self, device, list_item): """ 當使用者點選某個 dongle 項目時,記錄該設備資料,並更新 UI 樣式 """ self.selected_device = device print("選取的 dongle:", device) # 更新列表中所有項目的背景顏色(例如:清除其他項目的選取狀態) for index in range(self.device_list_widget.count()): item = self.device_list_widget.item(index) widget = self.device_list_widget.itemWidget(item) # 設定預設背景 widget.setStyleSheet("background: none;") # 將被選取的項目背景改變(例如:淺藍色) list_item_widget = self.device_list_widget.itemWidget(list_item) list_item_widget.setStyleSheet("background-color: lightblue;") def add_device_to_list(self, device): try: usb_port_id = device.get("usb_port_id") product_id = device.get("product_id") converted_product_id = hex(product_id).strip().lower() kn_number = device.get("kn_number") icon_filename = DongleIconMap.get(converted_product_id) icon_path = os.path.join(UXUI_ASSETS, "Assets_png", icon_filename) item_widget = QWidget() item_layout = QHBoxLayout(item_widget) item_layout.setContentsMargins(5, 5, 5, 5) # 疊放圖示和框框的容器 icon_container = QWidget() icon_container.setFixedSize(44, 44) # 藍色背景框 box_label = QLabel(icon_container) box_label.setFixedSize(35, 35) box_label.setStyleSheet(""" background-color: #182D4B; border-radius: 5px; """) box_label.move(4, 4) # 圖示 icon_label = QLabel(icon_container) icon_label.setFixedSize(29, 25) icon_label.setStyleSheet("background: transparent;") pixmap = QPixmap(icon_path) scaled_pixmap = pixmap.scaled(29, 25, Qt.KeepAspectRatio, Qt.SmoothTransformation) icon_label.setPixmap(scaled_pixmap) icon_label.setAttribute(Qt.WA_TranslucentBackground) icon_label.move(8, 9) item_layout.addWidget(icon_container) # 文字資訊 text_layout = QVBoxLayout() text_layout.setSpacing(0) label_text = QLabel("序號", self) label_text.setStyleSheet("color: white; font-size: 12px;") text_layout.addWidget(label_text) value_text = QLabel(f"{kn_number}", self) value_text.setStyleSheet("color: white; font-size: 12px;") text_layout.addWidget(value_text) item_layout.addLayout(text_layout) # 建立 QListWidgetItem 並關聯 widget list_item = QListWidgetItem() list_item.setSizeHint(item_widget.sizeHint()) self.device_list_widget.addItem(list_item) self.device_list_widget.setItemWidget(list_item, item_widget) # 綁定點擊事件:當使用者點擊該 widget 時,更新 selected_device item_widget.mousePressEvent = lambda event, dev=device, item=list_item: self.select_device(dev, item) except Exception as e: print(f"Error in add_device_to_list: {e}") # 當使用者選擇工具時更新 inference 模組 def select_tool(self, tool_config): print("選擇工具:", tool_config.get("display_name")) self.current_tool_config = tool_config # 獲取模式和模型名稱 mode = tool_config.get("mode", "") model_name = tool_config.get("model_name", "") # 構建模型路徑 model_path = os.path.join(UTILS_DIR, mode, model_name) # 讀取特定模型的詳細配置 model_config_path = os.path.join(model_path, "config.json") detailed_config = {} if os.path.exists(model_config_path): try: with open(model_config_path, "r", encoding="utf-8") as f: detailed_config = json.load(f) # 合併基本配置和詳細配置 tool_config = {**tool_config, **detailed_config} except Exception as e: print(f"Error reading model config: {e}") # 獲取工具的輸入類型 input_info = tool_config.get("input_info", {}) tool_type = input_info.get("type", "video") print("type:", tool_type) once_mode = True if tool_type == "image" else False # 組合input_params input_params = tool_config.get("input_parameters", {}).copy() # 處理設備相關設定 if hasattr(self, "selected_device") and self.selected_device: input_params["usb_port_id"] = self.selected_device.get("usb_port_id", 0) dongle = self.selected_device.get("dongle", "unknown") print("選取的 dongle:", dongle) # 檢查模型是否支援該設備 compatible_devices = tool_config.get("compatible_devices", []) if compatible_devices and dongle not in compatible_devices: self.show_custom_message( QMessageBox.Warning, "設備不兼容", f"所選模型不支援 {dongle} 設備。\n支援的設備: {', '.join(compatible_devices)}" ) return # 處理韌體路徑 scpu_path = os.path.join(FW_DIR, dongle, "fw_scpu.bin") ncpu_path = os.path.join(FW_DIR, dongle, "fw_ncpu.bin") input_params["scpu_path"] = scpu_path input_params["ncpu_path"] = ncpu_path else: # 預設設備處理邏輯不變 if self.connected_devices and len(self.connected_devices) > 0: input_params["usb_port_id"] = self.connected_devices[0].get("usb_port_id", 0) print("Warning: 沒有特別選取 dongle, 預設使用第一個設備") else: input_params["usb_port_id"] = 0 print("Warning: 沒有連接設備, 使用預設 usb_port_id 0") # 處理檔案輸入 if tool_type in ["image", "voice"]: # 處理邏輯不變 if hasattr(self, "destination") and self.destination: input_params["file_path"] = self.destination uploaded_img = cv2.imread(self.destination) if uploaded_img is not None: if not self.inference_queue.full(): self.inference_queue.put(uploaded_img) print("上傳的圖片已推入 inference queue") else: print("Warning: inference queue 已滿,無法推入上傳圖片") else: print("Warning: 無法讀取上傳的圖片") else: input_params["file_path"] = "" print("Warning: 需要檔案輸入,但尚未上傳檔案。") # 添加模型檔案路徑 if "model_file" in tool_config: model_file = tool_config["model_file"] model_file_path = os.path.join(model_path, model_file) input_params["model"] = model_file_path print("input_params:", input_params) # 如果已有 worker 運行,先停止舊 worker if self.inference_worker: self.inference_worker.stop() self.inference_worker = None # 建立新的 inference worker (使用修改後的參數) self.inference_worker = InferenceWorkerThread( self.inference_queue, mode, model_name, min_interval=0.5, mse_threshold=500, once_mode=once_mode ) self.inference_worker.input_params = input_params self.inference_worker.inference_result_signal.connect(self.handle_inference_result) self.inference_worker.start() print(f"Inference worker 已切換到模組:{mode}/{model_name}") if tool_type == "video": self.start_camera() else: print("工具模式為非 video,不啟動相機") # 修改 create_ai_toolbox 方法:每個按鈕點擊時呼叫 select_tool def create_ai_toolbox(self, layout): try: # 讀取 JSON 配置 print("config_path: ", SCRIPT_CONFIG) if os.path.exists(SCRIPT_CONFIG): with open(SCRIPT_CONFIG, "r", encoding="utf-8") as f: config = json.load(f) plugins = config.get("plugins", []) else: # 若無配置檔,則嘗試自動生成 plugins = self.generate_global_config().get("plugins", []) if not plugins: print("無法生成配置,使用空的工具列表") # 創建工具箱UI toolbox_frame = QFrame(self) toolbox_frame.setStyleSheet(f"border: none; background: {SECONDARY_COLOR}; border-radius: 15px;") toolbox_frame.setFixedHeight(450) toolbox_frame.setFixedWidth(240) toolbox_layout = QVBoxLayout(toolbox_frame) # 標題列 title_layout = QHBoxLayout() title_container = QWidget() container_layout = QHBoxLayout(title_container) container_layout.setSpacing(10) toolbox_icon = QSvgWidget(os.path.join(UXUI_ASSETS, "Assets_svg/ic_window_toolbox.svg")) toolbox_icon.setFixedSize(40, 40) container_layout.addWidget(toolbox_icon) title_label = QLabel("AI Toolbox") title_label.setStyleSheet("color: white; font-size: 20px; font-weight: bold;") container_layout.addWidget(title_label) title_layout.addWidget(title_container) toolbox_layout.addLayout(title_layout) # 建立工具按鈕 (分類顯示) for plugin in plugins: mode = plugin.get("mode", "") display_name = plugin.get("display_name", "") # 添加分類標題 category_label = QLabel(display_name) category_label.setStyleSheet("color: white; font-size: 16px; font-weight: bold; margin-top: 10px;") toolbox_layout.addWidget(category_label) # 添加該分類下的所有模型按鈕 for model in plugin.get("models", []): model_name = model.get("name", "") display_name = model.get("display_name", "") # 建立工具配置 tool_config = { "mode": mode, "model_name": model_name, "display_name": display_name, "description": model.get("description", ""), "compatible_devices": model.get("compatible_devices", []) } # 建立按鈕 button = QPushButton(display_name) button.clicked.connect(lambda checked, t=tool_config: self.select_tool(t)) button.setStyleSheet(BUTTON_STYLE) button.setFixedHeight(40) toolbox_layout.addWidget(button) layout.addWidget(toolbox_frame) except Exception as e: print(f"Error in create_ai_toolbox: {e}") def create_canvas_area(self): try: # Create frame container for canvas canvas_frame = QFrame(self) canvas_frame.setStyleSheet("border: 1px solid gray; background: black; border-radius: 20px;") canvas_frame.setFixedSize(900, 750) canvas_layout = QVBoxLayout(canvas_frame) canvas_layout.setContentsMargins(10, 10, 10, 10) # Create label for video display self.canvas_label = QLabel() self.canvas_label.setAlignment(Qt.AlignCenter) self.canvas_label.setStyleSheet("border: none; background: transparent;") canvas_layout.addWidget(self.canvas_label) return canvas_frame except Exception as e: print(f"Error in create_canvas_area: {e}") return QFrame(self) def create_media_panel(self): try: # 創建一個垂直佈局來放置按鈕 media_panel = QFrame(self) media_panel.setStyleSheet(f"background: {SECONDARY_COLOR}; border-radius: 20px;") media_layout = QVBoxLayout(media_panel) media_layout.setAlignment(Qt.AlignCenter) # 媒體按鈕資訊 media_buttons_info = [ ('screenshot', os.path.join(UXUI_ASSETS, "Assets_svg/bt_function_screencapture_normal.svg")), ('upload file', os.path.join(UXUI_ASSETS, "Assets_svg/bt_function_upload_normal.svg")), ('voice', os.path.join(UXUI_ASSETS, "Assets_svg/ic_recording_voice.svg")), ('video', os.path.join(UXUI_ASSETS, "Assets_svg/ic_recording_camera.svg")), ] for button_name, icon_path in media_buttons_info: button = QPushButton() button.setFixedSize(50, 50) button.setStyleSheet(""" QPushButton { background: transparent; color: white; border: 1px transparent; border-radius: 10px; } QPushButton:hover { background-color: rgba(255, 255, 255, 50); } QPushButton:pressed { background-color: rgba(255, 255, 255, 100); } """) button_layout = QHBoxLayout(button) button_layout.setContentsMargins(0, 0, 0, 0) button_layout.setAlignment(Qt.AlignCenter) icon = QSvgWidget(icon_path) icon.setFixedSize(40, 40) button_layout.addWidget(icon) if button_name == 'video': button.clicked.connect(self.record_video) elif button_name == 'voice': button.clicked.connect(self.record_audio) elif button_name == 'screenshot': button.clicked.connect(self.take_screenshot) elif button_name == 'upload file': button.clicked.connect(self.upload_file) media_layout.addWidget(button, alignment=Qt.AlignCenter) media_panel.setLayout(media_layout) media_panel.setFixedSize(90, 240) return media_panel except Exception as e: print(f"Error in create_media_panel: {e}") return QFrame(self) def create_device_popup(self): try: # 設備連接彈出視窗 popup = QWidget(self) popup_width = int(self.width() * 0.67) popup_height = int(self.height() * 0.67) popup.setFixedSize(popup_width, popup_height) popup.setStyleSheet(f""" QWidget {{ background-color: {SECONDARY_COLOR}; border-radius: 20px; padding: 20px; }} """) popup_layout = QVBoxLayout(popup) popup_layout.setContentsMargins(0, 0, 0, 0) # 標題列 title_layout = QHBoxLayout() title_layout.setAlignment(Qt.AlignCenter) title_container = QWidget() container_layout = QHBoxLayout(title_container) container_layout.setSpacing(10) device_icon = QSvgWidget(os.path.join(UXUI_ASSETS, "Assets_svg/ic_window_device.svg")) device_icon.setFixedSize(35, 35) container_layout.addWidget(device_icon) popup_label = QLabel("Device Connection") popup_label.setStyleSheet("color: white; font-size: 32px;") container_layout.addWidget(popup_label) container_layout.setAlignment(Qt.AlignCenter) title_layout.addWidget(title_container) popup_layout.addLayout(title_layout) # 設備列表 self.device_list_widget_popup = QListWidget(popup) popup_layout.addWidget(self.device_list_widget_popup) # 按鈕區域 button_layout = QHBoxLayout() refresh_button = QPushButton("Refresh") refresh_button.clicked.connect(self.refresh_devices) refresh_button.setFixedSize(110, 45) refresh_button.setStyleSheet(BUTTON_STYLE) button_layout.addWidget(refresh_button) done_button = QPushButton("Done") done_button.setStyleSheet(BUTTON_STYLE) done_button.setFixedSize(110, 45) done_button.clicked.connect(self.hide_device_popup) button_layout.addWidget(done_button) button_layout.setSpacing(10) popup_layout.addSpacing(20) popup_layout.addLayout(button_layout) self.device_popup = popup return popup except Exception as e: print(f"Error in create_device_popup: {e}") return QWidget(self) def refresh_devices(self): try: print("Refreshing devices...") device_descriptors = check_available_device() self.connected_devices = [] if device_descriptors.device_descriptor_number > 0: self.parse_and_store_devices(device_descriptors.device_descriptor_list) self.display_devices(device_descriptors.device_descriptor_list) else: self.show_no_device_gif() except Exception as e: print(f"Error in refresh_devices: {e}") def parse_and_store_devices(self, devices): try: for device in devices: # 如果 device.product_id 為整數,先轉成十六進位字串,然後再做 lower 和 strip 處理 product_id = hex(device.product_id).strip().lower() # 例如 4 -> "0x4", 256 -> "0x100" # 根據 DongleModelMap 得到型號,若無對應則回傳 "unknown" dongle = DongleModelMap.get(product_id, "unknown") # 將型號存入設備描述中 device.dongle = dongle new_device = { 'usb_port_id': device.usb_port_id, 'product_id': device.product_id, 'kn_number': device.kn_number, 'dongle': dongle # 加入型號資訊 } print(device) existing_device_index = next( (index for (index, d) in enumerate(self.connected_devices) if d['usb_port_id'] == new_device['usb_port_id']), None ) if existing_device_index is not None: self.connected_devices[existing_device_index] = new_device else: self.connected_devices.append(new_device) except Exception as e: print(f"Error in parse_and_store_devices: {e}") def display_devices(self, device_list): try: if hasattr(self, 'device_list_widget'): self.device_list_widget.clear() for device in self.connected_devices: self.add_device_to_list(device) if hasattr(self, 'device_list_widget_popup'): self.device_list_widget_popup.clear() for device in device_list: item_widget = QWidget() item_layout = QHBoxLayout(item_widget) item_layout.setContentsMargins(5, 5, 5, 5) icon = QSvgWidget(os.path.join(UXUI_ASSETS, "Assets_svg/ic_window_device.svg")) icon.setFixedSize(40, 40) item_layout.addWidget(icon) text_layout = QVBoxLayout() line1_label = QLabel(f"Dongle: {device.product_id}") line1_label.setStyleSheet("font-weight: bold; color: white;") text_layout.addWidget(line1_label) line2_label = QLabel(f"KN number: {device.kn_number}") line2_label.setStyleSheet("color: white;") text_layout.addWidget(line2_label) item_layout.addLayout(text_layout) list_item = QListWidgetItem() list_item.setSizeHint(item_widget.sizeHint()) self.device_list_widget_popup.addItem(list_item) self.device_list_widget_popup.setItemWidget(list_item, item_widget) except Exception as e: print(f"Error in display_devices: {e}") def show_no_device_gif(self): try: no_device_label = QLabel(self) no_device_movie = QMovie(os.path.join(UXUI_ASSETS, "no_device_temp.gif")) no_device_label.setMovie(no_device_movie) no_device_movie.start() no_device_label.setAlignment(Qt.AlignCenter) self.layout.addWidget(no_device_label) except Exception as e: print(f"Error in show_no_device_gif: {e}") # ───────────────────────────── # 自訂訊息框方法(第一個方法處理) def show_custom_message(self, icon, title, message): """ 建立一個自定義的 QMessageBox,並設定文字顏色為白色及背景色 """ msgBox = QMessageBox(self) msgBox.setIcon(icon) msgBox.setWindowTitle(title) msgBox.setText(message) msgBox.setStyleSheet(""" QLabel { color: white; } QPushButton { color: white; } QMessageBox { background-color: #2b2b2b; } """) msgBox.exec_() # ───────────────────────────── def upload_file(self): try: print("開始呼叫 QFileDialog.getOpenFileName") options = QFileDialog.Options() file_path, _ = QFileDialog.getOpenFileName(self, "Upload File", "", "All Files (*)", options=options) print("取得檔案路徑:", file_path) if file_path: print("檢查上傳目錄是否存在") if not os.path.exists(UPLOAD_DIR): os.makedirs(UPLOAD_DIR) print("建立 UPLOAD_DIR:", UPLOAD_DIR) print("檢查原始檔案是否存在:", file_path) if not os.path.exists(file_path): self.show_custom_message(QMessageBox.Critical, "錯誤", "找不到選擇的檔案") return file_name = os.path.basename(file_path) self.destination = os.path.join(UPLOAD_DIR, file_name) print("計算目標路徑:", self.destination) # 檢查目標路徑是否可寫 try: print("嘗試寫入測試檔案") with open(self.destination, 'wb') as test_file: pass os.remove(self.destination) print("測試檔案建立與刪除成功") except PermissionError as pe: self.show_custom_message(QMessageBox.Critical, "錯誤", "無法寫入目標資料夾") return print("開始複製檔案") shutil.copy2(file_path, self.destination) print("檔案複製完成") self.show_custom_message(QMessageBox.Information, "成功", f"檔案已上傳到:{self.destination}") except Exception as e: import traceback print("上傳過程中發生例外:\n", traceback.format_exc()) self.show_custom_message(QMessageBox.Critical, "錯誤", f"上傳過程發生錯誤:{str(e)}") def show_device_details(self): print("show_device_details") self.show_device_popup() # AI 模型功能實作 def run_face_detection(self): self.start_camera() print("Running Face Detection") def run_gender_age_detection(self): self.start_camera() print("Running Gender/Age Detection") def run_object_detection(self): self.start_camera() print("Running Object Detection") def run_mask_detection(self): self.start_camera() print("Running Mask Detection") # TODO: Implement custom model upload and usage def upload_model(self): try: print("Uploading Model") options = QFileDialog.Options() file_path, _ = QFileDialog.getOpenFileName(self, "Upload Model", "", "Model Files (*.nef);;All Files (*)", options=options) if file_path: if file_path.lower().endswith('.nef'): print(f"模型已上傳:{file_path}") # 此處添加上傳模型的處理邏輯 else: self.show_custom_message(QMessageBox.Critical, "錯誤", "無效的模型檔案格式。請選擇 .nef 檔案。") except Exception as e: print(f"Error in upload_model: {e}") #──────────────────────────── def start_camera(self): if self.video_thread is None: self.video_thread = VideoThread() self.video_thread.change_pixmap_signal.connect(self.update_image) self.video_thread.start() print("相機已啟動") # 啟動 inference worker self.inference_worker.start() else: print("相機已經在運行中") def stop_camera(self): if self.video_thread is not None: self.video_thread.stop() self.video_thread = None print("相機已停止") if self.inference_worker: self.inference_worker.stop() #──────────────────────────── # update_image:更新畫面並同時將 frame 推入 inference_queue def update_image(self, qt_image): try: # 更新 canvas 顯示 canvas_size = self.canvas_label.size() scaled_image = qt_image.scaled( canvas_size.width() - 20, canvas_size.height() - 20, Qt.KeepAspectRatio, Qt.SmoothTransformation ) self.canvas_label.setPixmap(QPixmap.fromImage(scaled_image)) # 轉換 QImage 成 numpy 陣列 frame_np = qimage_to_numpy(qt_image) # 若佇列未滿,放入最新 frame if not self.inference_queue.full(): self.inference_queue.put(frame_np) else: # 可選擇丟棄舊資料或覆蓋,這裡直接忽略新 frame pass except Exception as e: print(f"Error in update_image: {e}") #──────────────────────────── # 處理 inference 結果:將結果(bounding boxes、文字等)疊加在畫面上 def handle_inference_result(self, result): # 將結果打印到 console print("Inference result received:", result) # 建立 QMessageBox msgBox = QMessageBox(self) msgBox.setWindowTitle("推論結果") # 根據 result 格式化顯示文字 if isinstance(result, dict): result_str = "\n".join(f"{key}: {value}" for key, value in result.items()) else: result_str = str(result) msgBox.setText(result_str) msgBox.setStandardButtons(QMessageBox.Ok) # 設定樣式:改變 QLabel 和 QPushButton 的文字顏色為白色,並可設定背景色 msgBox.setStyleSheet(""" QLabel { color: white; } """) msgBox.exec_() def record_video(self): if not self.recording: try: self.recording = True self.recorded_frames = [] print("Started video recording") sender = self.sender() if sender: sender.setStyleSheet(""" QPushButton { background: rgba(255, 0, 0, 0.3); border: 1px solid red; border-radius: 10px; } """) except Exception as e: print(f"Error starting video recording: {e}") else: try: self.recording = False print("Stopped video recording") if self.recorded_frames: filename = QFileDialog.getSaveFileName(self, "Save Video", "", "Video Files (*.avi)")[0] if filename: height, width = self.recorded_frames[0].shape[:2] out = cv2.VideoWriter(filename, cv2.VideoWriter_fourcc(*'XVID'), 20.0, (width, height)) for frame in self.recorded_frames: out.write(frame) out.release() print(f"Video saved to {filename}") sender = self.sender() if sender: sender.setStyleSheet(""" QPushButton { background: transparent; border: 1px transparent; border-radius: 10px; } QPushButton:hover { background-color: rgba(255, 255, 255, 50); } """) except Exception as e: print(f"Error stopping video recording: {e}") def record_audio(self): if not self.recording_audio: try: self.recording_audio = True print("Started audio recording") sender = self.sender() if sender: sender.setStyleSheet(""" QPushButton { background: rgba(255, 0, 0, 0.3); border: 1px solid red; border-radius: 10px; } """) except Exception as e: print(f"Error starting audio recording: {e}") else: try: self.recording_audio = False print("Stopped audio recording") sender = self.sender() if sender: sender.setStyleSheet(""" QPushButton { background: transparent; border: 1px transparent; border-radius: 10px; } QPushButton:hover { background-color: rgba(255, 255, 255, 50); } """) except Exception as e: print(f"Error stopping audio recording: {e}") def take_screenshot(self): try: if self.canvas_label.pixmap(): filename = QFileDialog.getSaveFileName(self, "Save Screenshot", "", "Image Files (*.png *.jpg)")[0] if filename: self.canvas_label.pixmap().save(filename) print(f"Screenshot saved to {filename}") except Exception as e: print(f"Error taking screenshot: {e}") def closeEvent(self, event): try: if hasattr(self, 'video_thread') and self.video_thread is not None: self.stop_camera() if hasattr(self, 'recording') and self.recording: self.record_video() if hasattr(self, 'recording_audio') and self.recording_audio: self.record_audio() event.accept() except Exception as e: print(f"Error during closeEvent: {e}") event.accept() def show_device_popup(self): try: self.overlay.show() except Exception as e: print(f"Error in show_device_popup: {e}") def hide_device_popup(self): try: self.overlay.hide() except Exception as e: print(f"Error in hide_device_popup: {e}") def generate_global_config(self): """掃描目錄結構並生成全局配置檔案""" try: config = {"plugins": []} # 確認 utils 目錄存在 if not os.path.exists(UTILS_DIR): os.makedirs(UTILS_DIR, exist_ok=True) print(f"已建立 UTILS_DIR: {UTILS_DIR}") # 列出 utils 目錄下的所有項目以進行偵錯 print(f"UTILS_DIR 內容: {os.listdir(UTILS_DIR) if os.path.exists(UTILS_DIR) else '目錄不存在'}") # 掃描模式目錄(第一層子目錄) mode_dirs = [d for d in os.listdir(UTILS_DIR) if os.path.isdir(os.path.join(UTILS_DIR, d)) and not d.startswith('_')] print(f"找到的模式目錄: {mode_dirs}") for mode_name in mode_dirs: mode_path = os.path.join(UTILS_DIR, mode_name) mode_info = { "mode": mode_name, "display_name": mode_name.replace("_", " ").title(), "models": [] } # 列出該模式目錄下的所有項目以進行偵錯 print(f"模式 {mode_name} 的內容: {os.listdir(mode_path)}") # 掃描模型目錄(第二層子目錄) model_dirs = [d for d in os.listdir(mode_path) if os.path.isdir(os.path.join(mode_path, d)) and not d.startswith('_')] print(f"在模式 {mode_name} 中找到的模型: {model_dirs}") for model_name in model_dirs: model_path = os.path.join(mode_path, model_name) # 檢查模型配置檔案 model_config_path = os.path.join(model_path, "config.json") if os.path.exists(model_config_path): try: with open(model_config_path, "r", encoding="utf-8") as f: model_config = json.load(f) print(f"已成功讀取模型配置: {model_config_path}") model_summary = { "name": model_name, "display_name": model_config.get("display_name", model_name.replace("_", " ").title()), "description": model_config.get("description", ""), "compatible_devices": model_config.get("compatible_devices", []) } mode_info["models"].append(model_summary) except Exception as e: print(f"讀取模型配置時發生錯誤 {model_config_path}: {e}") else: print(f"未找到模型配置檔: {model_config_path}") # 可選:自動創建模板配置檔 self.create_model_config_template(model_path) # 只添加含有模型的模式 if mode_info["models"]: config["plugins"].append(mode_info) # 寫入配置檔 os.makedirs(os.path.dirname(SCRIPT_CONFIG), exist_ok=True) with open(SCRIPT_CONFIG, "w", encoding="utf-8") as f: json.dump(config, f, indent=2, ensure_ascii=False) print(f"全局配置已生成: {SCRIPT_CONFIG}") return config except Exception as e: print(f"生成全局配置時發生錯誤: {e}") import traceback traceback.print_exc() return {"plugins": []}