KNEO-Academy/src/views/mainWindows.py

1283 lines
53 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

''' 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": []}