1283 lines
53 KiB
Python
1283 lines
53 KiB
Python
''' 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": []} |