優化 Inference process, 增加Utilities頁面和功能, 修改UI介面 ( 20250324 in update diary)
This commit is contained in:
parent
d58cec837b
commit
527b183576
@ -1,7 +1,7 @@
|
||||
from enum import Enum
|
||||
import os
|
||||
# APPDATA_PATH = os.environ.get("LOCALAPPDATA")
|
||||
APPDATA_PATH = "/Users/mason/Developer/Kneron-Academy/test_images"
|
||||
APPDATA_PATH = os.environ.get("LOCALAPPDATA")
|
||||
# APPDATA_PATH = "/Users/mason/Developer/Kneron-Academy/test_images"
|
||||
# 取得專案根目錄的絕對路徑並設定 UXUI_ASSETS 為絕對路徑
|
||||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
UXUI_ASSETS = os.path.join(PROJECT_ROOT, "uxui", "")
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# src/controllers/device_controller.py
|
||||
from PyQt5.QtWidgets import QWidget, QListWidgetItem
|
||||
from PyQt5.QtGui import QPixmap
|
||||
from PyQt5.QtGui import QPixmap, QIcon
|
||||
from PyQt5.QtCore import Qt
|
||||
import os
|
||||
|
||||
@ -19,6 +19,7 @@ class DeviceController:
|
||||
print("Refreshing devices...")
|
||||
device_descriptors = check_available_device()
|
||||
self.connected_devices = []
|
||||
# print(self.connected_devices)
|
||||
|
||||
if device_descriptors.device_descriptor_number > 0:
|
||||
self.parse_and_store_devices(device_descriptors.device_descriptor_list)
|
||||
@ -34,6 +35,7 @@ class DeviceController:
|
||||
def parse_and_store_devices(self, devices):
|
||||
"""Parse device information and store it"""
|
||||
for device in devices:
|
||||
try:
|
||||
product_id = hex(device.product_id).strip().lower()
|
||||
dongle = DongleModelMap.get(product_id, "unknown")
|
||||
device.dongle = dongle
|
||||
@ -55,6 +57,57 @@ class DeviceController:
|
||||
self.connected_devices[existing_device_index] = new_device
|
||||
else:
|
||||
self.connected_devices.append(new_device)
|
||||
except Exception as e:
|
||||
print(f"Error processing device: {e}")
|
||||
|
||||
def display_devices(self, devices):
|
||||
"""Display the connected devices in the UI"""
|
||||
try:
|
||||
if not hasattr(self.main_window, 'device_list_widget'):
|
||||
print("Warning: main_window does not have device_list_widget attribute")
|
||||
return
|
||||
|
||||
self.main_window.device_list_widget.clear()
|
||||
|
||||
if not devices:
|
||||
print("No devices to display")
|
||||
return
|
||||
|
||||
for device in devices:
|
||||
try:
|
||||
product_id = hex(device.product_id).strip().lower()
|
||||
icon_path = os.path.join(UXUI_ASSETS, DongleIconMap.get(product_id, "unknown_dongle.png"))
|
||||
|
||||
item = QListWidgetItem()
|
||||
item.setData(Qt.UserRole, device)
|
||||
|
||||
pixmap = QPixmap(icon_path)
|
||||
icon = QIcon(pixmap) # Convert QPixmap to QIcon
|
||||
item.setIcon(icon)
|
||||
|
||||
# Set device name as the display text
|
||||
dongle_name = DongleModelMap.get(product_id, "Unknown Device")
|
||||
item.setText(f"{dongle_name} (KN: {device.kn_number})")
|
||||
|
||||
self.main_window.device_list_widget.addItem(item)
|
||||
print(f"Added device to list: {dongle_name} (KN: {device.kn_number})")
|
||||
except Exception as e:
|
||||
print(f"Error adding device to list: {e}")
|
||||
except Exception as e:
|
||||
print(f"Error in display_devices: {e}")
|
||||
|
||||
def get_devices(self):
|
||||
"""Get the list of connected devices"""
|
||||
try:
|
||||
device_descriptors = check_available_device()
|
||||
if device_descriptors.device_descriptor_number > 0:
|
||||
# Parse and store devices to ensure connected_devices is updated
|
||||
self.parse_and_store_devices(device_descriptors.device_descriptor_list)
|
||||
return device_descriptors.device_descriptor_list
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"Error in get_devices: {e}")
|
||||
return []
|
||||
|
||||
def get_selected_device(self):
|
||||
"""Get the currently selected device"""
|
||||
@ -69,7 +122,9 @@ class DeviceController:
|
||||
for index in range(list_widget.count()):
|
||||
item = list_widget.item(index)
|
||||
widget = list_widget.itemWidget(item)
|
||||
if widget: # Check if widget exists before setting style
|
||||
widget.setStyleSheet("background: none;")
|
||||
|
||||
list_item_widget = list_widget.itemWidget(list_item)
|
||||
if list_item_widget: # Check if widget exists before setting style
|
||||
list_item_widget.setStyleSheet("background-color: lightblue;")
|
||||
@ -1,9 +1,10 @@
|
||||
# src/controllers/inference_controller.py
|
||||
import os, queue, cv2, json
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
from PyQt5.QtWidgets import QMessageBox, QApplication
|
||||
from PyQt5.QtCore import QTimer, Qt
|
||||
|
||||
from src.models.inference_worker import InferenceWorkerThread
|
||||
from src.config import UTILS_DIR, FW_DIR
|
||||
from src.config import UTILS_DIR, FW_DIR, DongleModelMap
|
||||
|
||||
class InferenceController:
|
||||
def __init__(self, main_window, device_controller):
|
||||
@ -12,17 +13,23 @@ class InferenceController:
|
||||
self.inference_worker = None
|
||||
self.inference_queue = queue.Queue(maxsize=10)
|
||||
self.current_tool_config = None
|
||||
self.previous_tool_config = None
|
||||
self._camera_was_active = False
|
||||
# 儲存原始影格尺寸,用於邊界框縮放計算
|
||||
self.original_frame_width = 640 # 預設值
|
||||
self.original_frame_height = 480 # 預設值
|
||||
|
||||
def select_tool(self, tool_config):
|
||||
"""Select an AI tool and configure inference"""
|
||||
print("Selected tool:", tool_config.get("display_name"))
|
||||
"""選擇AI工具並配置推論"""
|
||||
try:
|
||||
print("選擇工具:", tool_config.get("display_name"))
|
||||
self.current_tool_config = tool_config
|
||||
|
||||
# Get mode and model name
|
||||
# 獲取模式和模型名稱
|
||||
mode = tool_config.get("mode", "")
|
||||
model_name = tool_config.get("model_name", "")
|
||||
|
||||
# Load detailed model configuration
|
||||
# 載入詳細模型配置
|
||||
model_path = os.path.join(UTILS_DIR, mode, model_name)
|
||||
model_config_path = os.path.join(model_path, "config.json")
|
||||
|
||||
@ -32,21 +39,55 @@ class InferenceController:
|
||||
detailed_config = json.load(f)
|
||||
tool_config = {**tool_config, **detailed_config}
|
||||
except Exception as e:
|
||||
print(f"Error reading model config: {e}")
|
||||
print(f"讀取模型配置時發生錯誤: {e}")
|
||||
|
||||
# Get tool input type
|
||||
# 獲取工具輸入類型
|
||||
input_info = tool_config.get("input_info", {})
|
||||
tool_type = input_info.get("type", "video")
|
||||
once_mode = True if tool_type == "image" else False
|
||||
|
||||
# Prepare input parameters
|
||||
# 檢查是否從視訊模式切換到圖片模式,或從圖片模式切換到視訊模式
|
||||
previous_tool_type = "video"
|
||||
if hasattr(self, 'previous_tool_config') and self.previous_tool_config:
|
||||
previous_input_info = self.previous_tool_config.get("input_info", {})
|
||||
previous_tool_type = previous_input_info.get("type", "video")
|
||||
|
||||
# 清空推論佇列,確保在模式切換時不會使用舊數據
|
||||
self._clear_inference_queue()
|
||||
|
||||
# 儲存當前工具類型以供下次比較
|
||||
self.previous_tool_config = tool_config
|
||||
|
||||
# 準備輸入參數
|
||||
input_params = tool_config.get("input_parameters", {}).copy()
|
||||
|
||||
# Configure device-related settings
|
||||
selected_device = self.device_controller.get_selected_device()
|
||||
if selected_device:
|
||||
# Get usb_port_id (check if it's a dictionary or object)
|
||||
if isinstance(selected_device, dict):
|
||||
input_params["usb_port_id"] = selected_device.get("usb_port_id", 0)
|
||||
dongle = selected_device.get("dongle", "unknown")
|
||||
product_id = selected_device.get("product_id", "unknown")
|
||||
else:
|
||||
input_params["usb_port_id"] = getattr(selected_device, "usb_port_id", 0)
|
||||
product_id = getattr(selected_device, "product_id", "unknown")
|
||||
|
||||
# Ensure product_id is in the right format for lookup
|
||||
# Convert to lowercase hex string if it's a number
|
||||
if isinstance(product_id, int):
|
||||
product_id = hex(product_id).lower()
|
||||
# If it's a string but doesn't start with '0x', add it
|
||||
elif isinstance(product_id, str) and not product_id.startswith('0x'):
|
||||
try:
|
||||
# Try to convert to int first, then to hex format
|
||||
product_id = hex(int(product_id, 0)).lower()
|
||||
except ValueError:
|
||||
# If conversion fails, keep as is
|
||||
pass
|
||||
|
||||
# Map product_id to dongle type/series
|
||||
dongle = DongleModelMap.get(product_id, "unknown")
|
||||
print(f"Selected device: product_id={product_id}, mapped to={dongle}")
|
||||
|
||||
# Verify device compatibility
|
||||
compatible_devices = tool_config.get("compatible_devices", [])
|
||||
@ -121,19 +162,99 @@ class InferenceController:
|
||||
|
||||
# Start camera if needed
|
||||
if tool_type == "video":
|
||||
# If camera was previously active but disconnected for image processing
|
||||
if self._camera_was_active and self.main_window.media_controller.video_thread is not None:
|
||||
# Reconnect the signal
|
||||
self.main_window.media_controller.video_thread.change_pixmap_signal.connect(
|
||||
self.main_window.media_controller.update_image
|
||||
)
|
||||
print("Camera reconnected for video processing")
|
||||
else:
|
||||
# Start camera normally
|
||||
self.main_window.media_controller.start_camera()
|
||||
else:
|
||||
print("Tool mode is not video, camera not started")
|
||||
# For image tools, temporarily pause the camera but don't stop it completely
|
||||
# This allows switching back to video tools without restarting the camera
|
||||
if self.main_window.media_controller.video_thread is not None:
|
||||
# Save current state to indicate camera was running
|
||||
self._camera_was_active = True
|
||||
# Disconnect signal to prevent processing frames during image inference
|
||||
self.main_window.media_controller.video_thread.change_pixmap_signal.disconnect()
|
||||
print("Camera paused for image processing")
|
||||
else:
|
||||
self._camera_was_active = False
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"選擇工具時發生錯誤: {e}")
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
|
||||
def _clear_inference_queue(self):
|
||||
"""清空推論佇列中的所有數據"""
|
||||
try:
|
||||
# 清空現有佇列
|
||||
while not self.inference_queue.empty():
|
||||
try:
|
||||
self.inference_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
break
|
||||
print("推論佇列已清空")
|
||||
except Exception as e:
|
||||
print(f"清空推論佇列時發生錯誤: {e}")
|
||||
|
||||
def add_frame_to_queue(self, frame):
|
||||
"""Add a frame to the inference queue"""
|
||||
"""將影格添加到推論佇列"""
|
||||
try:
|
||||
# 更新原始影格尺寸
|
||||
if frame is not None and hasattr(frame, 'shape'):
|
||||
height, width = frame.shape[:2]
|
||||
self.original_frame_width = width
|
||||
self.original_frame_height = height
|
||||
|
||||
# 添加到佇列
|
||||
if not self.inference_queue.full():
|
||||
self.inference_queue.put(frame)
|
||||
except Exception as e:
|
||||
print(f"添加影格到佇列時發生錯誤: {e}")
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
|
||||
def stop_inference(self):
|
||||
"""Stop the inference worker"""
|
||||
if self.inference_worker:
|
||||
self.inference_worker.stop()
|
||||
self.inference_worker = None
|
||||
|
||||
def process_uploaded_image(self, file_path):
|
||||
"""處理上傳的圖片並進行推論"""
|
||||
try:
|
||||
if not os.path.exists(file_path):
|
||||
print(f"錯誤: 檔案不存在 {file_path}")
|
||||
return
|
||||
|
||||
# 清空推論佇列,確保只處理最新的圖片
|
||||
self._clear_inference_queue()
|
||||
|
||||
# 讀取圖片
|
||||
img = cv2.imread(file_path)
|
||||
if img is None:
|
||||
print(f"錯誤: 無法讀取圖片 {file_path}")
|
||||
return
|
||||
|
||||
# 更新推論工作器參數
|
||||
if self.inference_worker:
|
||||
self.inference_worker.input_params["file_path"] = file_path
|
||||
|
||||
# 將圖片添加到推論佇列
|
||||
if not self.inference_queue.full():
|
||||
self.inference_queue.put(img)
|
||||
print(f"已將圖片 {file_path} 添加到推論佇列")
|
||||
else:
|
||||
print("警告: 推論佇列已滿")
|
||||
else:
|
||||
print("錯誤: 推論工作器未初始化")
|
||||
except Exception as e:
|
||||
print(f"處理上傳圖片時發生錯誤: {e}")
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
@ -1,8 +1,8 @@
|
||||
import cv2
|
||||
import os
|
||||
from PyQt5.QtWidgets import QFileDialog
|
||||
from PyQt5.QtGui import QPixmap
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QPixmap, QPainter, QPen, QFont, QColor
|
||||
from PyQt5.QtCore import Qt, QRect
|
||||
|
||||
from src.models.video_thread import VideoThread
|
||||
from src.utils.image_utils import qimage_to_numpy
|
||||
@ -15,43 +15,158 @@ class MediaController:
|
||||
self.recording = False
|
||||
self.recording_audio = False
|
||||
self.recorded_frames = []
|
||||
self._signal_was_connected = False # Track if signal was previously connected
|
||||
self._inference_paused = False # 追蹤推論是否暫停
|
||||
|
||||
def start_camera(self):
|
||||
"""Start the camera for video capture"""
|
||||
"""啟動相機進行視訊擷取"""
|
||||
try:
|
||||
if self.video_thread is None:
|
||||
print("初始化相機執行緒...")
|
||||
# 先清除畫布上的任何文字或圖像
|
||||
if hasattr(self.main_window, 'canvas_label'):
|
||||
self.main_window.canvas_label.clear()
|
||||
|
||||
self.video_thread = VideoThread()
|
||||
if not self._signal_was_connected:
|
||||
try:
|
||||
self.video_thread.change_pixmap_signal.connect(self.update_image)
|
||||
self._signal_was_connected = True
|
||||
print("相機信號連接成功")
|
||||
except Exception as e:
|
||||
print(f"連接相機信號時發生錯誤: {e}")
|
||||
|
||||
# 啟動相機執行緒
|
||||
self.video_thread.start()
|
||||
print("Camera started")
|
||||
print("相機執行緒啟動成功")
|
||||
else:
|
||||
print("Camera already running")
|
||||
print("相機已經在運行中")
|
||||
except Exception as e:
|
||||
print(f"啟動相機時發生錯誤: {e}")
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
|
||||
def stop_camera(self):
|
||||
"""Stop the camera"""
|
||||
"""停止相機"""
|
||||
try:
|
||||
if self.video_thread is not None:
|
||||
print("停止相機執行緒")
|
||||
# 確保先斷開信號連接
|
||||
if self._signal_was_connected:
|
||||
try:
|
||||
self.video_thread.change_pixmap_signal.disconnect()
|
||||
self._signal_was_connected = False
|
||||
print("已斷開相機信號連接")
|
||||
except Exception as e:
|
||||
print(f"斷開信號連接時發生錯誤: {e}")
|
||||
|
||||
# 停止執行緒
|
||||
self.video_thread.stop()
|
||||
self.video_thread = None
|
||||
print("Camera stopped")
|
||||
print("相機已完全停止")
|
||||
except Exception as e:
|
||||
print(f"停止相機時發生錯誤: {e}")
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
|
||||
def update_image(self, qt_image):
|
||||
"""Update the image display and pass to inference queue"""
|
||||
"""更新圖像顯示並處理推論"""
|
||||
try:
|
||||
# Update canvas display
|
||||
canvas_size = self.main_window.canvas_label.size()
|
||||
scaled_image = qt_image.scaled(
|
||||
canvas_size.width() - 20,
|
||||
canvas_size.height() - 20,
|
||||
Qt.KeepAspectRatio,
|
||||
Qt.SmoothTransformation
|
||||
)
|
||||
self.main_window.canvas_label.setPixmap(QPixmap.fromImage(scaled_image))
|
||||
# 更新畫布上的圖像
|
||||
if hasattr(self.main_window, 'canvas_label'):
|
||||
pixmap = QPixmap.fromImage(qt_image)
|
||||
|
||||
# Convert QImage to numpy array and add to inference queue
|
||||
# 如果有邊界框,繪製它
|
||||
if hasattr(self.main_window, 'current_bounding_box') and self.main_window.current_bounding_box is not None:
|
||||
painter = QPainter(pixmap)
|
||||
pen = QPen(Qt.red)
|
||||
pen.setWidth(2)
|
||||
painter.setPen(pen)
|
||||
|
||||
# 獲取邊界框
|
||||
bbox_info = self.main_window.current_bounding_box
|
||||
|
||||
# 檢查邊界框格式
|
||||
if isinstance(bbox_info, dict) and "bounding box" in bbox_info:
|
||||
# 從字典中獲取邊界框座標
|
||||
bbox = bbox_info["bounding box"]
|
||||
if len(bbox) >= 4:
|
||||
# 繪製矩形
|
||||
x1, y1, x2, y2 = bbox[0], bbox[1], bbox[2], bbox[3]
|
||||
painter.drawRect(QRect(x1, y1, x2 - x1, y2 - y1))
|
||||
|
||||
# 如果有結果標籤,繪製它
|
||||
if "result" in bbox_info:
|
||||
font = QFont()
|
||||
font.setPointSize(10)
|
||||
painter.setFont(font)
|
||||
painter.setPen(QColor(255, 0, 0)) # 紅色
|
||||
|
||||
# 計算標籤位置(邊界框上方)
|
||||
label_x = x1
|
||||
label_y = y1 - 10
|
||||
|
||||
# 確保標籤在畫布範圍內
|
||||
if label_y < 10:
|
||||
label_y = y2 + 15 # 如果上方空間不足,放在底部
|
||||
|
||||
painter.drawText(label_x, label_y, bbox_info["result"])
|
||||
elif isinstance(bbox_info, list) and len(bbox_info) >= 4:
|
||||
# 直接使用列表作為邊界框座標
|
||||
x1, y1, x2, y2 = bbox_info[0], bbox_info[1], bbox_info[2], bbox_info[3]
|
||||
painter.drawRect(QRect(x1, y1, x2 - x1, y2 - y1))
|
||||
|
||||
# 如果有標籤,繪製它
|
||||
if len(bbox_info) > 4 and bbox_info[4]:
|
||||
font = QFont()
|
||||
font.setPointSize(10)
|
||||
painter.setFont(font)
|
||||
painter.setPen(QColor(255, 0, 0)) # 紅色
|
||||
|
||||
# 計算標籤位置(邊界框上方)
|
||||
label_x = x1
|
||||
label_y = y1 - 10
|
||||
|
||||
# 確保標籤在畫布範圍內
|
||||
if label_y < 10:
|
||||
label_y = y2 + 15 # 如果上方空間不足,放在底部
|
||||
|
||||
painter.drawText(label_x, label_y, bbox_info[4])
|
||||
|
||||
painter.end()
|
||||
|
||||
# 顯示圖像
|
||||
self.main_window.canvas_label.setPixmap(pixmap)
|
||||
|
||||
# 只有在推論未暫停時才將影格添加到推論佇列
|
||||
if not self._inference_paused:
|
||||
frame_np = qimage_to_numpy(qt_image)
|
||||
self.inference_controller.add_frame_to_queue(frame_np)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in update_image: {e}")
|
||||
print(f"更新圖像時發生錯誤: {e}")
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
|
||||
def reconnect_camera_signal(self):
|
||||
"""Reconnect the camera signal if it was previously disconnected"""
|
||||
if self.video_thread is not None and self._signal_was_connected:
|
||||
try:
|
||||
# Check if the signal is already connected to avoid duplicate connections
|
||||
# PyQt doesn't provide a direct way to check if a signal is connected,
|
||||
# so we use a try-except block
|
||||
try:
|
||||
# Attempt to disconnect first to avoid multiple connections
|
||||
self.video_thread.change_pixmap_signal.disconnect(self.update_image)
|
||||
except TypeError:
|
||||
# Signal was not connected, which is fine
|
||||
pass
|
||||
|
||||
# Reconnect the signal
|
||||
self.video_thread.change_pixmap_signal.connect(self.update_image)
|
||||
print("Camera signal reconnected")
|
||||
except Exception as e:
|
||||
print(f"Error reconnecting camera signal: {e}")
|
||||
|
||||
def record_video(self, button=None):
|
||||
"""Start or stop video recording"""
|
||||
@ -164,3 +279,25 @@ class MediaController:
|
||||
print(f"Screenshot saved to {filename}")
|
||||
except Exception as e:
|
||||
print(f"Error taking screenshot: {e}")
|
||||
|
||||
def toggle_inference_pause(self):
|
||||
"""切換推論暫停狀態"""
|
||||
try:
|
||||
self._inference_paused = not self._inference_paused
|
||||
if self._inference_paused:
|
||||
# 暫停時清除邊界框
|
||||
self.main_window.current_bounding_box = None
|
||||
else:
|
||||
# 恢復推論時,確保相機仍在運行
|
||||
if self.video_thread is None or not self.video_thread.isRunning():
|
||||
self.start_camera()
|
||||
return self._inference_paused
|
||||
except Exception as e:
|
||||
print(f"切換推論暫停狀態時發生錯誤: {e}")
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
return self._inference_paused
|
||||
|
||||
def is_inference_paused(self):
|
||||
"""檢查推論是否暫停"""
|
||||
return self._inference_paused
|
||||
@ -43,6 +43,15 @@ class InferenceWorkerThread(QThread):
|
||||
continue
|
||||
|
||||
if self.last_frame is not None:
|
||||
# 檢查當前幀與上一幀的尺寸是否相同
|
||||
if frame.shape != self.last_frame.shape:
|
||||
print(f"幀尺寸變更: 從 {self.last_frame.shape} 變更為 {frame.shape}")
|
||||
# 尺寸不同時,重置上一幀和緩存結果
|
||||
self.last_frame = None
|
||||
self.cached_result = None
|
||||
else:
|
||||
# 只有在尺寸相同時才進行 MSE 計算
|
||||
try:
|
||||
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)
|
||||
@ -50,6 +59,11 @@ class InferenceWorkerThread(QThread):
|
||||
self._running = False
|
||||
break
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"計算 MSE 時發生錯誤: {e}")
|
||||
# 發生錯誤時重置上一幀和緩存結果
|
||||
self.last_frame = None
|
||||
self.cached_result = None
|
||||
|
||||
try:
|
||||
result = self.inference_module.inference(frame, params=self.input_params)
|
||||
|
||||
@ -8,23 +8,78 @@ class VideoThread(QThread):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._run_flag = True
|
||||
self._camera_open_attempts = 0
|
||||
self._max_attempts = 3
|
||||
|
||||
def run(self):
|
||||
# 嘗試多次開啟相機
|
||||
while self._camera_open_attempts < self._max_attempts and self._run_flag:
|
||||
self._camera_open_attempts += 1
|
||||
print(f"嘗試開啟相機 (嘗試 {self._camera_open_attempts}/{self._max_attempts})...")
|
||||
|
||||
# 嘗試使用DirectShow後端,通常在Windows上更快
|
||||
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
|
||||
if not cap.isOpened():
|
||||
print("無法使用DirectShow開啟相機,嘗試預設後端")
|
||||
cap = cv2.VideoCapture(0)
|
||||
if not cap.isOpened():
|
||||
print("Cannot open camera")
|
||||
self._run_flag = False
|
||||
print(f"無法使用任何後端開啟相機,等待1秒後重試...")
|
||||
import time
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
# 設置相機屬性以獲得更好的性能
|
||||
# 降低解析度以提高啟動速度和幀率
|
||||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
|
||||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
|
||||
cap.set(cv2.CAP_PROP_FPS, 30)
|
||||
|
||||
# 設置緩衝區大小為1,減少延遲
|
||||
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
||||
|
||||
# 預熱相機,丟棄前幾幀以加快穩定速度
|
||||
for _ in range(5):
|
||||
cap.read()
|
||||
|
||||
# 相機開啟成功,重置嘗試計數
|
||||
self._camera_open_attempts = 0
|
||||
|
||||
# 主循環
|
||||
while self._run_flag:
|
||||
ret, frame = cap.read()
|
||||
if ret:
|
||||
# Convert to RGB format
|
||||
# 轉換為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)
|
||||
else:
|
||||
print("無法讀取相機幀,相機可能已斷開連接")
|
||||
break
|
||||
|
||||
# 釋放相機資源
|
||||
cap.release()
|
||||
|
||||
# 如果是因為停止信號而退出循環,則不再重試
|
||||
if not self._run_flag:
|
||||
break
|
||||
|
||||
print("相機連接中斷,嘗試重新連接...")
|
||||
|
||||
if self._camera_open_attempts >= self._max_attempts:
|
||||
print("達到最大嘗試次數,無法開啟相機")
|
||||
|
||||
def stop(self):
|
||||
"""停止執行緒"""
|
||||
try:
|
||||
print("正在停止相機執行緒...")
|
||||
self._run_flag = False
|
||||
# 等待執行緒完成
|
||||
if self.isRunning():
|
||||
self.wait()
|
||||
print("相機執行緒已停止")
|
||||
except Exception as e:
|
||||
print(f"停止相機執行緒時發生錯誤: {e}")
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
@ -1,46 +1,47 @@
|
||||
# import kp
|
||||
|
||||
# def check_available_device():
|
||||
# try:
|
||||
# print("checking available devices")
|
||||
# device_descriptors = kp.core.scan_devices()
|
||||
# return device_descriptors
|
||||
# except Exception as e:
|
||||
# print(f"Error scanning devices: {e}")
|
||||
# # 返回一個空的設備描述符或模擬數據
|
||||
# class EmptyDescriptor:
|
||||
# def __init__(self):
|
||||
# self.device_descriptor_number = 0
|
||||
# self.device_descriptor_list = []
|
||||
# return EmptyDescriptor()
|
||||
import kp
|
||||
|
||||
def check_available_device():
|
||||
# 模擬設備描述符
|
||||
try:
|
||||
print("checking available devices")
|
||||
device_descriptors = kp.core.scan_devices()
|
||||
print("device_descriptors", device_descriptors)
|
||||
return device_descriptors
|
||||
except Exception as e:
|
||||
print(f"Error scanning devices: {e}")
|
||||
# 返回一個空的設備描述符或模擬數據
|
||||
class EmptyDescriptor:
|
||||
def __init__(self):
|
||||
self.device_descriptor_number = 0
|
||||
self.device_descriptor_list = [{
|
||||
"usb_port_id": 4,
|
||||
"vendor_id": "0x3231",
|
||||
"product_id": "0x720",
|
||||
"link_speed": "UsbSpeed.KP_USB_SPEED_SUPER",
|
||||
"kn_number": "0xB306224C",
|
||||
"is_connectable": True,
|
||||
"usb_port_path": "4-1",
|
||||
"firmware": "KDP2 Comp/F"
|
||||
},
|
||||
{
|
||||
"usb_port_id": 5,
|
||||
"vendor_id": "0x3231",
|
||||
"product_id": "0x520",
|
||||
"link_speed": "UsbSpeed.KP_USB_SPEED_SUPER",
|
||||
"kn_number": "0xB306224C",
|
||||
"is_connectable": True,
|
||||
"usb_port_path": "4-1",
|
||||
"firmware": "KDP2 Comp/F"
|
||||
}]
|
||||
self.device_descriptor_list = []
|
||||
return EmptyDescriptor()
|
||||
|
||||
# def check_available_device():
|
||||
# # 模擬設備描述符
|
||||
# print("checking available devices")
|
||||
# class EmptyDescriptor:
|
||||
# def __init__(self):
|
||||
# self.device_descriptor_number = 0
|
||||
# self.device_descriptor_list = [{
|
||||
# "usb_port_id": 4,
|
||||
# "vendor_id": "0x3231",
|
||||
# "product_id": "0x720",
|
||||
# "link_speed": "UsbSpeed.KP_USB_SPEED_SUPER",
|
||||
# "kn_number": "0xB306224C",
|
||||
# "is_connectable": True,
|
||||
# "usb_port_path": "4-1",
|
||||
# "firmware": "KDP2 Comp/F"
|
||||
# },
|
||||
# {
|
||||
# "usb_port_id": 5,
|
||||
# "vendor_id": "0x3231",
|
||||
# "product_id": "0x520",
|
||||
# "link_speed": "UsbSpeed.KP_USB_SPEED_SUPER",
|
||||
# "kn_number": "0xB306224C",
|
||||
# "is_connectable": True,
|
||||
# "usb_port_path": "4-1",
|
||||
# "firmware": "KDP2 Comp/F"
|
||||
# }]
|
||||
# return EmptyDescriptor()
|
||||
# device_descriptors = [
|
||||
# {
|
||||
# "usb_port_id": 4,
|
||||
|
||||
@ -1,70 +1,177 @@
|
||||
import os
|
||||
import shutil
|
||||
from PyQt5.QtWidgets import QFileDialog, QMessageBox
|
||||
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QApplication
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QImage, QPixmap
|
||||
import cv2
|
||||
|
||||
class FileService:
|
||||
def __init__(self, main_window, upload_dir):
|
||||
self.main_window = main_window
|
||||
self.upload_dir = upload_dir
|
||||
self.destination = None
|
||||
self._camera_was_active = False # Track if camera was active before upload
|
||||
|
||||
def upload_file(self):
|
||||
"""Handle file upload process"""
|
||||
"""處理檔案上傳流程"""
|
||||
try:
|
||||
print("Calling QFileDialog.getOpenFileName")
|
||||
# 1. 先完全停止相機(如果正在運行)
|
||||
if hasattr(self.main_window, 'media_controller') and self.main_window.media_controller.video_thread is not None:
|
||||
print("上傳前停止相機")
|
||||
try:
|
||||
# 儲存狀態以指示相機正在運行
|
||||
self._camera_was_active = True
|
||||
# 顯示上傳中提示
|
||||
if hasattr(self.main_window, 'canvas_label'):
|
||||
self.main_window.canvas_label.setText("準備上傳檔案...")
|
||||
self.main_window.canvas_label.setAlignment(Qt.AlignCenter)
|
||||
self.main_window.canvas_label.setStyleSheet("color: white; font-size: 24px;")
|
||||
# 確保 UI 更新
|
||||
QApplication.processEvents()
|
||||
|
||||
# 只暫停推論,不完全停止相機
|
||||
if not self.main_window.media_controller._inference_paused:
|
||||
self.main_window.media_controller.toggle_inference_pause()
|
||||
|
||||
# 斷開信號連接但不停止相機執行緒
|
||||
if hasattr(self.main_window.media_controller, '_signal_was_connected') and self.main_window.media_controller._signal_was_connected:
|
||||
try:
|
||||
self.main_window.media_controller.video_thread.change_pixmap_signal.disconnect()
|
||||
self.main_window.media_controller._signal_was_connected = False
|
||||
print("已暫時斷開相機信號連接")
|
||||
except Exception as e:
|
||||
print(f"斷開信號連接時發生錯誤: {e}")
|
||||
except Exception as e:
|
||||
print(f"準備上傳時發生錯誤: {e}")
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
# 即使發生錯誤,也繼續嘗試上傳
|
||||
else:
|
||||
self._camera_was_active = False
|
||||
|
||||
print("呼叫 QFileDialog.getOpenFileName")
|
||||
options = QFileDialog.Options()
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
self.main_window,
|
||||
"Upload File",
|
||||
"上傳檔案",
|
||||
"",
|
||||
"All Files (*)",
|
||||
"所有檔案 (*)",
|
||||
options=options
|
||||
)
|
||||
print("File path obtained:", file_path)
|
||||
print("檔案路徑取得:", file_path)
|
||||
|
||||
if file_path:
|
||||
print("Checking if upload directory exists")
|
||||
print("檢查上傳目錄是否存在")
|
||||
if not os.path.exists(self.upload_dir):
|
||||
os.makedirs(self.upload_dir)
|
||||
print(f"Created UPLOAD_DIR: {self.upload_dir}")
|
||||
print(f"建立上傳目錄: {self.upload_dir}")
|
||||
|
||||
print("Checking if original file exists:", file_path)
|
||||
print("檢查原始檔案是否存在:", file_path)
|
||||
if not os.path.exists(file_path):
|
||||
self.show_message(QMessageBox.Critical, "Error", "Selected file not found")
|
||||
self.show_message(QMessageBox.Critical, "錯誤", "選擇的檔案不存在")
|
||||
return None
|
||||
|
||||
file_name = os.path.basename(file_path)
|
||||
self.destination = os.path.join(self.upload_dir, file_name)
|
||||
print("Target path:", self.destination)
|
||||
print("目標路徑:", self.destination)
|
||||
|
||||
# Check if target path is writable
|
||||
# 檢查目標路徑是否可寫入
|
||||
try:
|
||||
print("Testing file write permission")
|
||||
print("測試檔案寫入權限")
|
||||
with open(self.destination, 'wb') as test_file:
|
||||
pass
|
||||
os.remove(self.destination)
|
||||
print("Test file creation and deletion successful")
|
||||
print("測試檔案建立和刪除成功")
|
||||
except PermissionError:
|
||||
self.show_message(QMessageBox.Critical, "Error", "Cannot write to target directory")
|
||||
self.show_message(QMessageBox.Critical, "錯誤", "無法寫入目標目錄")
|
||||
return None
|
||||
|
||||
print("Starting file copy")
|
||||
print("開始檔案複製")
|
||||
try:
|
||||
shutil.copy2(file_path, self.destination)
|
||||
print("File copy complete")
|
||||
self.show_message(QMessageBox.Information, "Success", f"File uploaded to: {self.destination}")
|
||||
print("檔案複製成功")
|
||||
|
||||
# 更新主視窗目的地
|
||||
self.main_window.destination = self.destination
|
||||
print(f"更新主視窗目的地: {self.main_window.destination}")
|
||||
|
||||
# 處理上傳的影像
|
||||
if self.main_window.inference_controller.current_tool_config:
|
||||
print("使用推論控制器處理上傳的影像")
|
||||
# 先在畫布上顯示影像
|
||||
try:
|
||||
# 載入和顯示影像
|
||||
image = cv2.imread(self.destination)
|
||||
if image is not None:
|
||||
# 轉換為 RGB 顯示
|
||||
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||
height, width, channel = image_rgb.shape
|
||||
bytes_per_line = channel * width
|
||||
qt_image = QImage(image_rgb.data, width, height, bytes_per_line, QImage.Format_RGB888)
|
||||
|
||||
# 將影像縮放以適應畫布
|
||||
canvas_size = self.main_window.canvas_label.size()
|
||||
scaled_image = qt_image.scaled(
|
||||
int(canvas_size.width() * 0.95),
|
||||
int(canvas_size.height() * 0.95),
|
||||
Qt.KeepAspectRatio,
|
||||
Qt.SmoothTransformation
|
||||
)
|
||||
self.main_window.canvas_label.setPixmap(QPixmap.fromImage(scaled_image))
|
||||
print("影像顯示在畫布上")
|
||||
except Exception as e:
|
||||
print(f"顯示影像時發生錯誤: {e}")
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
|
||||
# 然後使用推論處理它
|
||||
self.main_window.inference_controller.process_uploaded_image(self.destination)
|
||||
|
||||
return self.destination
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print("檔案複製過程中發生錯誤:\n", traceback.format_exc())
|
||||
self.show_message(QMessageBox.Critical, "錯誤", f"上傳錯誤: {str(e)}")
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print("Exception during upload process:\n", traceback.format_exc())
|
||||
self.show_message(QMessageBox.Critical, "Error", f"Upload error: {str(e)}")
|
||||
print("上傳過程中發生錯誤:\n", traceback.format_exc())
|
||||
self.show_message(QMessageBox.Critical, "錯誤", f"上傳錯誤: {str(e)}")
|
||||
return None
|
||||
finally:
|
||||
# 如果相機之前是活動的,嘗試恢復相機連接
|
||||
if self._camera_was_active and hasattr(self.main_window, 'media_controller'):
|
||||
try:
|
||||
# 延遲一點時間以確保處理完成
|
||||
QApplication.processEvents()
|
||||
|
||||
# 如果相機執行緒仍然存在但信號已斷開,重新連接信號
|
||||
if (self.main_window.media_controller.video_thread is not None and
|
||||
not self.main_window.media_controller._signal_was_connected):
|
||||
try:
|
||||
self.main_window.media_controller.video_thread.change_pixmap_signal.connect(
|
||||
self.main_window.media_controller.update_image
|
||||
)
|
||||
self.main_window.media_controller._signal_was_connected = True
|
||||
print("已重新連接相機信號")
|
||||
except Exception as e:
|
||||
print(f"重新連接相機信號時發生錯誤: {e}")
|
||||
|
||||
# 如果推論被暫停,恢復推論
|
||||
if self.main_window.media_controller._inference_paused:
|
||||
self.main_window.media_controller.toggle_inference_pause()
|
||||
print("已恢復推論")
|
||||
|
||||
except Exception as e:
|
||||
print(f"恢復相機時發生錯誤: {e}")
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
|
||||
def show_message(self, icon, title, message):
|
||||
"""Display a message box with custom styling"""
|
||||
"""顯示自訂樣式的訊息盒"""
|
||||
msgBox = QMessageBox(self.main_window)
|
||||
msgBox.setIcon(icon)
|
||||
msgBox.setWindowTitle(title)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# import kp
|
||||
import kp
|
||||
import cv2, os, shutil, sys
|
||||
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QLabel, QPushButton,
|
||||
QComboBox, QFileDialog, QMessageBox, QHBoxLayout, QDialog, QListWidget,
|
||||
|
||||
@ -15,6 +15,7 @@ def create_canvas_area(parent):
|
||||
canvas_label = QLabel()
|
||||
canvas_label.setAlignment(Qt.AlignCenter)
|
||||
canvas_label.setStyleSheet("border: none; background: transparent;")
|
||||
canvas_label.setMinimumSize(880, 730) # Set minimum size to ensure proper display
|
||||
canvas_layout.addWidget(canvas_label)
|
||||
|
||||
return canvas_frame, canvas_label
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
from PyQt5.QtWidgets import QFrame, QVBoxLayout, QHBoxLayout, QLabel, QListWidget, QWidget, QPushButton
|
||||
from PyQt5.QtWidgets import QFrame, QVBoxLayout, QHBoxLayout, QLabel, QListWidget, QWidget, QPushButton, QListWidgetItem
|
||||
from PyQt5.QtSvg import QSvgWidget
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QPixmap
|
||||
from PyQt5.QtGui import QPixmap, QIcon
|
||||
import os
|
||||
|
||||
from src.config import SECONDARY_COLOR, UXUI_ASSETS, BUTTON_STYLE, DongleIconMap
|
||||
from src.config import SECONDARY_COLOR, UXUI_ASSETS, BUTTON_STYLE, DongleIconMap, DongleModelMap
|
||||
|
||||
def create_device_layout(parent, device_controller):
|
||||
"""Create the device list layout"""
|
||||
@ -40,6 +40,26 @@ def create_device_layout(parent, device_controller):
|
||||
|
||||
# Device list
|
||||
device_list_widget = QListWidget(parent)
|
||||
device_list_widget.setStyleSheet("""
|
||||
QListWidget {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
QListWidget::item {
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QListWidget::item:selected {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
""")
|
||||
|
||||
# Connect item selection signal
|
||||
device_list_widget.itemClicked.connect(lambda item: device_controller.select_device(
|
||||
item.data(Qt.UserRole), item, device_list_widget
|
||||
))
|
||||
|
||||
devices_layout.addWidget(device_list_widget)
|
||||
|
||||
# Detail button
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QListWidget, QListWidgetItem
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QListWidget, QListWidgetItem, QFrame, QGridLayout, QSizePolicy
|
||||
from PyQt5.QtSvg import QSvgWidget
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtCore import Qt, QSize, QMargins
|
||||
from PyQt5.QtGui import QPixmap, QIcon, QColor
|
||||
import os
|
||||
|
||||
from src.config import SECONDARY_COLOR, BUTTON_STYLE, UXUI_ASSETS
|
||||
from src.config import SECONDARY_COLOR, BUTTON_STYLE, UXUI_ASSETS, DongleIconMap
|
||||
|
||||
def create_device_popup(parent, device_controller):
|
||||
"""Create a popup window for device connection management"""
|
||||
@ -19,10 +20,34 @@ def create_device_popup(parent, device_controller):
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
}}
|
||||
QLabel {{
|
||||
color: white;
|
||||
}}
|
||||
QListWidget {{
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 5px;
|
||||
color: white;
|
||||
}}
|
||||
QListWidget::item {{
|
||||
border-radius: 5px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 5px;
|
||||
}}
|
||||
QListWidget::item:selected {{
|
||||
background-color: rgba(52, 152, 219, 0.5);
|
||||
}}
|
||||
QFrame.device-info {{
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
margin-top: 15px;
|
||||
}}
|
||||
""")
|
||||
|
||||
popup_layout = QVBoxLayout(popup)
|
||||
popup_layout.setContentsMargins(0, 0, 0, 0)
|
||||
popup_layout.setContentsMargins(20, 20, 20, 20)
|
||||
popup_layout.setSpacing(15)
|
||||
|
||||
# Title row
|
||||
title_layout = QHBoxLayout()
|
||||
@ -37,40 +62,253 @@ def create_device_popup(parent, device_controller):
|
||||
container_layout.addWidget(device_icon)
|
||||
|
||||
popup_label = QLabel("Device Connection")
|
||||
popup_label.setStyleSheet("color: white; font-size: 32px;")
|
||||
popup_label.setStyleSheet("color: white; font-size: 32px; font-weight: bold;")
|
||||
container_layout.addWidget(popup_label)
|
||||
|
||||
container_layout.setAlignment(Qt.AlignCenter)
|
||||
title_layout.addWidget(title_container)
|
||||
popup_layout.addLayout(title_layout)
|
||||
|
||||
# Device list
|
||||
# Device list section - 設置為可捲動
|
||||
list_section = QFrame(popup)
|
||||
list_section.setStyleSheet("border: none; background: transparent;")
|
||||
list_layout = QVBoxLayout(list_section)
|
||||
list_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Device list - 設置為可捲動的列表
|
||||
device_list_widget_popup = QListWidget(popup)
|
||||
popup_layout.addWidget(device_list_widget_popup)
|
||||
device_list_widget_popup.setMinimumHeight(250) # 增加整個列表的高度
|
||||
# 啟用垂直捲動條
|
||||
device_list_widget_popup.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
# 設置列表項高度和圖示大小
|
||||
device_list_widget_popup.setIconSize(QSize(15, 15)) # 增大圖示
|
||||
list_layout.addWidget(device_list_widget_popup)
|
||||
|
||||
# Store reference to this list widget for later use
|
||||
parent.device_list_widget_popup = device_list_widget_popup
|
||||
|
||||
# Comment out the device details section
|
||||
"""
|
||||
# Device details section (initially hidden)
|
||||
device_details = QFrame(popup)
|
||||
device_details.setObjectName("device-details")
|
||||
device_details.setProperty("class", "device-info")
|
||||
device_details.setVisible(False) # Initially hidden
|
||||
|
||||
details_layout = QGridLayout(device_details)
|
||||
details_layout.setColumnStretch(1, 1)
|
||||
# 增加行間距
|
||||
details_layout.setVerticalSpacing(2)
|
||||
|
||||
# Device Type
|
||||
device_type_label_title = QLabel("Device Type:")
|
||||
device_type_label_title.setStyleSheet("font-size: 14px;")
|
||||
details_layout.addWidget(device_type_label_title, 0, 0)
|
||||
device_type_label = QLabel("-")
|
||||
device_type_label.setObjectName("device-type")
|
||||
device_type_label.setStyleSheet("font-size: 14px;")
|
||||
details_layout.addWidget(device_type_label, 0, 1)
|
||||
|
||||
# Port ID
|
||||
port_id_label_title = QLabel("Port ID:")
|
||||
port_id_label_title.setStyleSheet("font-size: 14px;")
|
||||
details_layout.addWidget(port_id_label_title, 1, 0)
|
||||
port_id_label = QLabel("-")
|
||||
port_id_label.setObjectName("port-id")
|
||||
port_id_label.setStyleSheet("font-size: 14px;")
|
||||
details_layout.addWidget(port_id_label, 1, 1)
|
||||
|
||||
# KN Number
|
||||
kn_number_label_title = QLabel("KN Number:")
|
||||
kn_number_label_title.setStyleSheet("font-size: 14px;")
|
||||
details_layout.addWidget(kn_number_label_title, 2, 0)
|
||||
kn_number_label = QLabel("-")
|
||||
kn_number_label.setObjectName("kn-number")
|
||||
kn_number_label.setStyleSheet("font-size: 14px;")
|
||||
details_layout.addWidget(kn_number_label, 2, 1)
|
||||
|
||||
# Status
|
||||
status_label_title = QLabel("Status:")
|
||||
status_label_title.setStyleSheet("font-size: 14px;")
|
||||
details_layout.addWidget(status_label_title, 3, 0)
|
||||
status_label = QLabel("-")
|
||||
status_label.setObjectName("status")
|
||||
status_label.setStyleSheet("font-size: 14px;")
|
||||
details_layout.addWidget(status_label, 3, 1)
|
||||
|
||||
# Store references to labels
|
||||
parent.device_detail_labels = {
|
||||
"device-type": device_type_label,
|
||||
"port-id": port_id_label,
|
||||
"kn-number": kn_number_label,
|
||||
"status": status_label,
|
||||
"frame": device_details
|
||||
}
|
||||
|
||||
# Connect item selection to show details
|
||||
device_list_widget_popup.itemClicked.connect(lambda item: show_device_details(parent, item))
|
||||
|
||||
list_layout.addWidget(device_details)
|
||||
"""
|
||||
popup_layout.addWidget(list_section)
|
||||
|
||||
# Button area
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setAlignment(Qt.AlignCenter)
|
||||
|
||||
refresh_button = QPushButton("Refresh")
|
||||
refresh_button.clicked.connect(device_controller.refresh_devices)
|
||||
refresh_button.setFixedSize(110, 45)
|
||||
refresh_button.clicked.connect(lambda: refresh_devices(parent, device_controller))
|
||||
refresh_button.setFixedSize(150, 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.setFixedSize(150, 45)
|
||||
done_button.clicked.connect(parent.hide_device_popup)
|
||||
button_layout.addWidget(done_button)
|
||||
|
||||
button_layout.setSpacing(10)
|
||||
popup_layout.addSpacing(20)
|
||||
button_layout.setSpacing(20)
|
||||
# 減少底部間距,讓按鈕更靠近底部
|
||||
popup_layout.addSpacing(5)
|
||||
popup_layout.addLayout(button_layout)
|
||||
|
||||
return popup
|
||||
except Exception as e:
|
||||
print(f"Error in create_device_popup: {e}")
|
||||
return QWidget(parent)
|
||||
|
||||
# Also need to comment out the show_device_details function since it's no longer used
|
||||
|
||||
# def show_device_details(parent, item):
|
||||
# """Show details for the selected device"""
|
||||
# try:
|
||||
# # Get the device data from the item
|
||||
# device_data = item.data(Qt.UserRole)
|
||||
# print("device_data", device_data)
|
||||
# if not device_data:
|
||||
# return
|
||||
|
||||
# # Update the detail labels
|
||||
# labels = parent.device_detail_labels
|
||||
|
||||
# # Set values
|
||||
# labels["device-type"].setText(device_data.get("dongle", "-"))
|
||||
# labels["port-id"].setText(str(device_data.get("usb_port_id", "-")))
|
||||
# labels["kn-number"].setText(str(device_data.get("kn_number", "-")))
|
||||
# labels["status"].setText("Connected" if device_data.get("is_connectable", True) else "Not Available")
|
||||
|
||||
# # Show the details frame
|
||||
# labels["frame"].setVisible(True)
|
||||
# except Exception as e:
|
||||
# print(f"Error showing device details: {e}")
|
||||
|
||||
|
||||
def refresh_devices(parent, device_controller):
|
||||
"""Refresh the device list and update the UI"""
|
||||
try:
|
||||
# Call the refresh method from device controller
|
||||
device_controller.refresh_devices()
|
||||
|
||||
# Clear the list
|
||||
parent.device_list_widget_popup.clear()
|
||||
|
||||
# Comment out the details frame visibility setting
|
||||
"""
|
||||
# Hide details frame
|
||||
if hasattr(parent, "device_detail_labels") and "frame" in parent.device_detail_labels:
|
||||
parent.device_detail_labels["frame"].setVisible(False)
|
||||
"""
|
||||
|
||||
# Track unique device models to avoid duplicates
|
||||
seen_models = set()
|
||||
|
||||
# Add devices to the list
|
||||
for device in device_controller.connected_devices:
|
||||
try:
|
||||
item = QListWidgetItem()
|
||||
|
||||
# Store the device data
|
||||
item.setData(Qt.UserRole, device)
|
||||
|
||||
# 使用自定義 widget 來顯示設備信息
|
||||
widget = QWidget()
|
||||
widget.setFixedHeight(60) # 固定高度
|
||||
|
||||
# 使用水平佈局
|
||||
main_layout = QHBoxLayout(widget)
|
||||
# 減少內邊距保持足夠空間
|
||||
main_layout.setContentsMargins(8, 4, 8, 4)
|
||||
main_layout.setSpacing(5)
|
||||
|
||||
# 左側圖示容器(帶有背景色)
|
||||
icon_container = QFrame()
|
||||
icon_container.setFixedSize(30, 30)
|
||||
icon_container.setStyleSheet("background-color: #182D4B; border-radius: 5px;")
|
||||
|
||||
icon_layout = QVBoxLayout(icon_container)
|
||||
icon_layout.setContentsMargins(0, 0, 0, 0)
|
||||
icon_layout.setAlignment(Qt.AlignCenter)
|
||||
|
||||
# 獲取設備圖示
|
||||
product_id = hex(device.get("product_id", 0)).strip().lower()
|
||||
icon_path = os.path.join(UXUI_ASSETS, "Assets_png", DongleIconMap.get(product_id, "unknown_dongle.png"))
|
||||
|
||||
if os.path.exists(icon_path):
|
||||
icon_label = QLabel()
|
||||
pixmap = QPixmap(icon_path)
|
||||
# 確保圖標大小適中
|
||||
scaled_pixmap = pixmap.scaled(30, 30, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
icon_label.setPixmap(scaled_pixmap)
|
||||
icon_layout.addWidget(icon_label)
|
||||
|
||||
main_layout.addWidget(icon_container)
|
||||
|
||||
# 右側信息區域
|
||||
info_container = QWidget()
|
||||
info_layout = QVBoxLayout(info_container)
|
||||
info_layout.setContentsMargins(0, 0, 0, 0)
|
||||
# info_layout.setSpacing(2)
|
||||
|
||||
# 設備名稱和型號
|
||||
dongle_name = device.get("dongle", "Unknown Device")
|
||||
kn_number = device.get("kn_number", "Unknown")
|
||||
status = "Connected" if device.get("is_connectable", True) else "Not Available"
|
||||
|
||||
# 只顯示型號名稱,避免重複
|
||||
device_model_key = f"{dongle_name}_{product_id}"
|
||||
if device_model_key in seen_models:
|
||||
# 只顯示狀態
|
||||
device_label_text = ""
|
||||
else:
|
||||
seen_models.add(device_model_key)
|
||||
device_label_text = f"{dongle_name}"
|
||||
|
||||
device_label = QLabel(device_label_text)
|
||||
device_label.setStyleSheet("color: white; font-weight: bold; font-size: 16px;")
|
||||
info_layout.addWidget(device_label)
|
||||
|
||||
# KN 號碼
|
||||
kn_label = QLabel(f"KN: {kn_number}")
|
||||
kn_label.setStyleSheet("color: white; font-size: 14px;")
|
||||
info_layout.addWidget(kn_label)
|
||||
|
||||
# 將信息容器添加到主佈局
|
||||
main_layout.addWidget(info_container, 1) # 設置伸展因子為1
|
||||
|
||||
# 狀態標籤(右側)
|
||||
status_label = QLabel(status)
|
||||
status_label.setStyleSheet("color: white; font-size: 12px;")
|
||||
main_layout.addWidget(status_label, 0, Qt.AlignRight | Qt.AlignVCenter) # 右對齊
|
||||
|
||||
# 設置自定義 widget 為列表項的 widget
|
||||
parent.device_list_widget_popup.addItem(item)
|
||||
parent.device_list_widget_popup.setItemWidget(item, widget)
|
||||
|
||||
# 設置項目大小
|
||||
item.setSizeHint(widget.sizeHint())
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error adding device to list: {e}")
|
||||
except Exception as e:
|
||||
print(f"Error refreshing devices: {e}")
|
||||
@ -14,15 +14,22 @@ def create_media_panel(parent, media_controller, file_service):
|
||||
media_layout = QVBoxLayout(media_panel)
|
||||
media_layout.setAlignment(Qt.AlignCenter)
|
||||
|
||||
# 確保使用正確的路徑分隔符
|
||||
assets_path = UXUI_ASSETS.replace('\\', '/')
|
||||
if not assets_path.endswith('/'):
|
||||
assets_path += '/'
|
||||
|
||||
# Media button information
|
||||
media_buttons_info = [
|
||||
('screenshot', os.path.join(UXUI_ASSETS, "Assets_svg/bt_function_screencapture_normal.svg"),
|
||||
('screenshot', os.path.join(assets_path, "Assets_svg/bt_function_screencapture_normal.svg").replace('\\', '/'),
|
||||
media_controller.take_screenshot),
|
||||
('upload file', os.path.join(UXUI_ASSETS, "Assets_svg/bt_function_upload_normal.svg"),
|
||||
('upload file', os.path.join(assets_path, "Assets_svg/bt_function_upload_normal.svg").replace('\\', '/'),
|
||||
file_service.upload_file),
|
||||
('voice', os.path.join(UXUI_ASSETS, "Assets_svg/ic_recording_voice.svg"),
|
||||
('pause/resume', os.path.join(assets_path, "Assets_svg/btn_result_image_delete_hover.svg").replace('\\', '/'),
|
||||
lambda: toggle_pause_button(parent, media_controller)),
|
||||
('voice', os.path.join(assets_path, "Assets_svg/ic_recording_voice.svg").replace('\\', '/'),
|
||||
lambda: media_controller.record_audio(None)),
|
||||
('video', os.path.join(UXUI_ASSETS, "Assets_svg/ic_recording_camera.svg"),
|
||||
('video', os.path.join(assets_path, "Assets_svg/ic_recording_camera.svg").replace('\\', '/'),
|
||||
lambda: media_controller.record_video(None)),
|
||||
]
|
||||
|
||||
@ -52,13 +59,43 @@ def create_media_panel(parent, media_controller, file_service):
|
||||
icon.setFixedSize(40, 40)
|
||||
button_layout.addWidget(icon)
|
||||
|
||||
# 為暫停按鈕儲存參考
|
||||
if button_name == 'pause/resume':
|
||||
parent.pause_button = button
|
||||
parent.pause_icon = icon
|
||||
parent.pause_icon_path = icon_path
|
||||
parent.play_icon_path = os.path.join(assets_path, "Assets_svg/bt_function_video_hover.svg").replace('\\', '/')
|
||||
|
||||
button.clicked.connect(callback)
|
||||
media_layout.addWidget(button, alignment=Qt.AlignCenter)
|
||||
|
||||
media_panel.setLayout(media_layout)
|
||||
media_panel.setFixedSize(90, 240)
|
||||
media_panel.setFixedSize(90, 290) # 增加高度以容納新按鈕
|
||||
|
||||
return media_panel
|
||||
except Exception as e:
|
||||
print(f"Error in create_media_panel: {e}")
|
||||
return QFrame(parent)
|
||||
|
||||
def toggle_pause_button(parent, media_controller):
|
||||
"""切換暫停/恢復按鈕圖示並觸發相應功能"""
|
||||
try:
|
||||
is_paused = media_controller.toggle_inference_pause()
|
||||
|
||||
# 切換圖示
|
||||
if is_paused:
|
||||
# 檢查文件是否存在
|
||||
if os.path.exists(parent.play_icon_path):
|
||||
parent.pause_icon.load(parent.play_icon_path)
|
||||
else:
|
||||
print(f"警告: 播放圖標文件不存在: {parent.play_icon_path}")
|
||||
else:
|
||||
# 檢查文件是否存在
|
||||
if os.path.exists(parent.pause_icon_path):
|
||||
parent.pause_icon.load(parent.pause_icon_path)
|
||||
else:
|
||||
print(f"警告: 暫停圖標文件不存在: {parent.pause_icon_path}")
|
||||
except Exception as e:
|
||||
print(f"切換暫停按鈕時發生錯誤: {e}")
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
@ -17,116 +17,181 @@ class LoginScreen(QWidget):
|
||||
def init_ui(self):
|
||||
# Basic window setup
|
||||
self.setGeometry(100, 100, *WINDOW_SIZE)
|
||||
self.setStyleSheet(f"background-color: {BACKGROUND_COLOR};")
|
||||
self.setStyleSheet(f"background-color: #F5F7FA;") # Light gray background
|
||||
|
||||
# Main layout
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(40, 40, 40, 40)
|
||||
layout.setSpacing(20)
|
||||
|
||||
# Header with logo
|
||||
header_frame = QFrame(self)
|
||||
header_frame.setStyleSheet("background-color: #2C3E50; border-radius: 10px;")
|
||||
header_frame.setFixedHeight(100)
|
||||
|
||||
header_layout = QHBoxLayout(header_frame)
|
||||
header_layout.setContentsMargins(20, 0, 20, 0)
|
||||
|
||||
# Logo
|
||||
logo_label = QLabel(self)
|
||||
logo_path = os.path.join(UXUI_ASSETS, "Assets_png/kneron_logo.png")
|
||||
if os.path.exists(logo_path):
|
||||
logo_pixmap = QPixmap(logo_path)
|
||||
logo_label.setPixmap(logo_pixmap)
|
||||
scaled_logo = logo_pixmap.scaled(150, 60, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
logo_label.setPixmap(scaled_logo)
|
||||
logo_label.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(logo_label)
|
||||
|
||||
# Title
|
||||
title_label = QLabel("Login", self)
|
||||
title_label.setAlignment(Qt.AlignCenter)
|
||||
title_label.setFont(QFont("Arial", 24, QFont.Bold))
|
||||
layout.addWidget(title_label)
|
||||
header_layout.addWidget(logo_label, alignment=Qt.AlignCenter)
|
||||
layout.addWidget(header_frame)
|
||||
|
||||
# Login form container
|
||||
form_container = QFrame(self)
|
||||
form_container.setFrameShape(QFrame.StyledPanel)
|
||||
form_container.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
border: 1px solid #E0E0E0;
|
||||
}
|
||||
""")
|
||||
form_layout = QVBoxLayout(form_container)
|
||||
form_layout.setContentsMargins(30, 30, 30, 30)
|
||||
form_layout.setSpacing(20)
|
||||
|
||||
# Title
|
||||
title_label = QLabel("Login", self)
|
||||
title_label.setAlignment(Qt.AlignCenter)
|
||||
title_label.setStyleSheet("font-size: 28px; font-weight: bold; color: #2C3E50; margin-bottom: 10px;")
|
||||
form_layout.addWidget(title_label)
|
||||
|
||||
# Server type
|
||||
server_label = QLabel("Server Authentication Type", self)
|
||||
server_label.setFont(QFont("Arial", 12))
|
||||
server_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #2C3E50;")
|
||||
form_layout.addWidget(server_label)
|
||||
|
||||
self.server_combo = QComboBox(self)
|
||||
self.server_combo.addItems(["Standard Password Authentication", "Other Authentication Method"])
|
||||
self.server_combo.setFont(QFont("Arial", 10))
|
||||
self.server_combo.setMinimumHeight(40)
|
||||
self.server_combo.setStyleSheet("""
|
||||
QComboBox {
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 5px;
|
||||
padding: 5px 10px;
|
||||
background-color: white;
|
||||
font-size: 14px;
|
||||
color: #2C3E50;
|
||||
}
|
||||
QComboBox::drop-down {
|
||||
border: none;
|
||||
width: 30px;
|
||||
}
|
||||
QComboBox::down-arrow {
|
||||
image: url(""" + os.path.join(UXUI_ASSETS, "Assets_svg/ic_result_folder_normal.svg").replace("\\", "/") + """);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
border: 1px solid #E0E0E0;
|
||||
selection-background-color: #ECF0F1;
|
||||
selection-color: #2C3E50;
|
||||
background-color: white;
|
||||
outline: none;
|
||||
}
|
||||
""")
|
||||
form_layout.addWidget(self.server_combo)
|
||||
|
||||
form_layout.addSpacing(10)
|
||||
|
||||
# Username
|
||||
username_label = QLabel("Username", self)
|
||||
username_label.setFont(QFont("Arial", 12))
|
||||
username_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #2C3E50;")
|
||||
form_layout.addWidget(username_label)
|
||||
|
||||
self.username_input = QLineEdit(self)
|
||||
self.username_input.setPlaceholderText("Enter your username")
|
||||
self.username_input.setMinimumHeight(40)
|
||||
self.username_input.setFont(QFont("Arial", 10))
|
||||
self.username_input.setStyleSheet("""
|
||||
QLineEdit {
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 5px;
|
||||
padding: 5px 10px;
|
||||
background-color: white;
|
||||
font-size: 14px;
|
||||
color: #2C3E50;
|
||||
}
|
||||
QLineEdit:focus {
|
||||
border: 1px solid #3498DB;
|
||||
}
|
||||
""")
|
||||
form_layout.addWidget(self.username_input)
|
||||
|
||||
form_layout.addSpacing(10)
|
||||
|
||||
# Password
|
||||
password_label = QLabel("Password", self)
|
||||
password_label.setFont(QFont("Arial", 12))
|
||||
password_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #2C3E50;")
|
||||
form_layout.addWidget(password_label)
|
||||
|
||||
self.password_input = QLineEdit(self)
|
||||
self.password_input.setPlaceholderText("Enter your password")
|
||||
self.password_input.setEchoMode(QLineEdit.Password)
|
||||
self.password_input.setMinimumHeight(40)
|
||||
self.password_input.setFont(QFont("Arial", 10))
|
||||
self.password_input.setStyleSheet("""
|
||||
QLineEdit {
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 5px;
|
||||
padding: 5px 10px;
|
||||
background-color: white;
|
||||
font-size: 14px;
|
||||
color: #2C3E50;
|
||||
}
|
||||
QLineEdit:focus {
|
||||
border: 1px solid #3498DB;
|
||||
}
|
||||
""")
|
||||
form_layout.addWidget(self.password_input)
|
||||
|
||||
form_layout.addSpacing(20)
|
||||
|
||||
# Error message (hidden by default)
|
||||
self.error_label = QLabel("", self)
|
||||
self.error_label.setStyleSheet("color: red;")
|
||||
self.error_label.setFont(QFont("Arial", 10))
|
||||
self.error_label.setStyleSheet("color: #E74C3C; font-size: 14px;")
|
||||
self.error_label.hide()
|
||||
form_layout.addWidget(self.error_label)
|
||||
|
||||
# Buttons
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(15)
|
||||
|
||||
back_button = QPushButton("Back", self)
|
||||
back_button.setMinimumHeight(40)
|
||||
back_button.setFont(QFont("Arial", 12))
|
||||
back_button.setMinimumHeight(45)
|
||||
back_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #757575;
|
||||
background-color: #95A5A6;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
padding: 5px 15px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #616161;
|
||||
background-color: #7F8C8D;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #616A6B;
|
||||
}
|
||||
""")
|
||||
back_button.clicked.connect(self.back_to_selection.emit)
|
||||
|
||||
login_button = QPushButton("Login", self)
|
||||
login_button.setMinimumHeight(40)
|
||||
login_button.setFont(QFont("Arial", 12))
|
||||
login_button.setMinimumHeight(45)
|
||||
login_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #1E88E5;
|
||||
background-color: #3498DB;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
padding: 5px 15px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #1976D2;
|
||||
background-color: #2980B9;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #1F618D;
|
||||
}
|
||||
""")
|
||||
login_button.clicked.connect(self.attempt_login)
|
||||
@ -139,6 +204,12 @@ class LoginScreen(QWidget):
|
||||
# Add form to main layout
|
||||
layout.addWidget(form_container, 1)
|
||||
|
||||
# Footer
|
||||
footer_label = QLabel(" 2025 Innovedus Inc. All rights reserved.", self)
|
||||
footer_label.setAlignment(Qt.AlignCenter)
|
||||
footer_label.setStyleSheet("font-size: 12px; color: #95A5A6;")
|
||||
layout.addWidget(footer_label)
|
||||
|
||||
def attempt_login(self):
|
||||
username = self.username_input.text()
|
||||
password = self.password_input.text()
|
||||
@ -149,7 +220,6 @@ class LoginScreen(QWidget):
|
||||
return
|
||||
|
||||
# Simulate login success (in a real app, you would validate with your server)
|
||||
# For demo, accept any non-empty username/password
|
||||
self.login_success.emit()
|
||||
|
||||
def show_error(self, message):
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import os, sys, json, queue, numpy as np
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QGridLayout, QFrame, QMessageBox
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QGridLayout, QFrame, QMessageBox, QApplication, QListWidgetItem
|
||||
from PyQt5.QtCore import Qt, QTimer
|
||||
from PyQt5.QtGui import QPixmap, QMovie
|
||||
|
||||
@ -13,7 +13,7 @@ from src.views.components.canvas_area import create_canvas_area
|
||||
from src.views.components.device_list import create_device_layout
|
||||
from src.views.components.toolbox import create_ai_toolbox
|
||||
from src.views.components.media_panel import create_media_panel
|
||||
from src.views.components.device_popup import create_device_popup
|
||||
from src.views.components.device_popup import create_device_popup, refresh_devices
|
||||
|
||||
class MainWindow(QWidget):
|
||||
def __init__(self):
|
||||
@ -30,6 +30,7 @@ class MainWindow(QWidget):
|
||||
|
||||
# Set up UI and configuration
|
||||
self.destination = None
|
||||
self.current_bounding_box = None # 儲存當前的邊界框資訊
|
||||
self.generate_global_config()
|
||||
self.init_ui()
|
||||
|
||||
@ -86,12 +87,19 @@ class MainWindow(QWidget):
|
||||
|
||||
# 2. Set up popup and mask
|
||||
self.device_popup_mask_setup()
|
||||
print("Setting up popup mask")
|
||||
|
||||
# 3. Refresh devices
|
||||
self.device_controller.refresh_devices()
|
||||
# 3. Refresh devices - do this after UI is fully set up
|
||||
QTimer.singleShot(100, self.device_controller.refresh_devices)
|
||||
|
||||
# 4. Show popup
|
||||
self.auto_start_camera()
|
||||
|
||||
# # 4. Show popup
|
||||
self.show_device_popup()
|
||||
print("Popup window setup complete")
|
||||
|
||||
# # 5. Start camera automatically after a short delay
|
||||
# QTimer.singleShot(100, self.auto_start_camera)
|
||||
except Exception as e:
|
||||
print(f"Error in show_device_popup_and_main_page: {e}")
|
||||
|
||||
@ -167,6 +175,12 @@ class MainWindow(QWidget):
|
||||
|
||||
def show_device_popup(self):
|
||||
try:
|
||||
# 顯示彈窗前刷新設備列表
|
||||
# 使用新的 refresh_devices 函數,而不是直接調用 device_controller
|
||||
from src.views.components.device_popup import refresh_devices
|
||||
refresh_devices(self, self.device_controller)
|
||||
|
||||
# 顯示彈窗
|
||||
self.overlay.show()
|
||||
except Exception as e:
|
||||
print(f"Error in show_device_popup: {e}")
|
||||
@ -188,15 +202,35 @@ class MainWindow(QWidget):
|
||||
print(f"Error in clear_layout: {e}")
|
||||
|
||||
def handle_inference_result(self, result):
|
||||
"""Handle inference results"""
|
||||
# Print result to console
|
||||
print("Inference result received:", result)
|
||||
"""處理來自推論工作器的結果"""
|
||||
try:
|
||||
# 輸出結果到控制台
|
||||
print("收到推論結果:", result)
|
||||
|
||||
# Create QMessageBox
|
||||
# 檢查結果是否包含邊界框資訊
|
||||
if isinstance(result, dict) and "bounding box" in result:
|
||||
# 更新當前邊界框資訊
|
||||
self.current_bounding_box = result["bounding box"]
|
||||
|
||||
# 如果結果中有標籤,將其添加到邊界框中
|
||||
if "result" in result:
|
||||
# 將標籤添加為邊界框列表的第5個元素
|
||||
if isinstance(self.current_bounding_box, list) and len(self.current_bounding_box) >= 4:
|
||||
# 確保邊界框列表至少有5個元素
|
||||
while len(self.current_bounding_box) < 5:
|
||||
self.current_bounding_box.append(None)
|
||||
# 設置第5個元素為結果標籤
|
||||
self.current_bounding_box[4] = result["result"]
|
||||
|
||||
# 不需要顯示彈窗,因為邊界框會直接繪製在畫面上
|
||||
return
|
||||
|
||||
# 對於非邊界框結果,使用彈窗顯示
|
||||
# 創建QMessageBox
|
||||
msgBox = QMessageBox(self)
|
||||
msgBox.setWindowTitle("Inference Result")
|
||||
msgBox.setWindowTitle("推論結果")
|
||||
|
||||
# Format text based on result type
|
||||
# 根據結果類型格式化文字
|
||||
if isinstance(result, dict):
|
||||
result_str = "\n".join(f"{key}: {value}" for key, value in result.items())
|
||||
else:
|
||||
@ -205,14 +239,19 @@ class MainWindow(QWidget):
|
||||
msgBox.setText(result_str)
|
||||
msgBox.setStandardButtons(QMessageBox.Ok)
|
||||
|
||||
# Set style
|
||||
# 設置樣式
|
||||
msgBox.setStyleSheet("""
|
||||
QLabel {
|
||||
color: white;
|
||||
}
|
||||
""")
|
||||
|
||||
# 顯示彈窗
|
||||
msgBox.exec_()
|
||||
except Exception as e:
|
||||
print(f"處理推論結果時發生錯誤: {e}")
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
|
||||
def show_no_device_gif(self):
|
||||
"""Show a GIF indicating no devices are connected"""
|
||||
@ -250,3 +289,22 @@ class MainWindow(QWidget):
|
||||
def generate_global_config(self):
|
||||
"""Generate global configuration if needed"""
|
||||
return self.config_utils.generate_global_config()
|
||||
|
||||
def auto_start_camera(self):
|
||||
"""自動啟動相機並顯示預覽"""
|
||||
print("開始啟動相機...")
|
||||
try:
|
||||
# 先清除畫布,顯示加載中的信息
|
||||
if hasattr(self, 'canvas_label'):
|
||||
self.canvas_label.setText("相機啟動中...")
|
||||
self.canvas_label.setAlignment(Qt.AlignCenter)
|
||||
self.canvas_label.setStyleSheet("color: white; font-size: 24px;")
|
||||
# 確保UI更新
|
||||
QApplication.processEvents()
|
||||
|
||||
# 在單獨的線程中啟動相機,避免阻塞UI
|
||||
self.media_controller.start_camera()
|
||||
except Exception as e:
|
||||
print(f"啟動相機時發生錯誤: {e}")
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
@ -1,8 +1,8 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
from PyQt5.QtGui import QPixmap, QFont
|
||||
from PyQt5.QtGui import QPixmap, QFont, QIcon
|
||||
import os
|
||||
from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR
|
||||
from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR, APP_NAME
|
||||
|
||||
class SelectionScreen(QWidget):
|
||||
# Signals for navigation
|
||||
@ -16,77 +16,174 @@ class SelectionScreen(QWidget):
|
||||
def init_ui(self):
|
||||
# Basic window setup
|
||||
self.setGeometry(100, 100, *WINDOW_SIZE)
|
||||
self.setStyleSheet(f"background-color: {BACKGROUND_COLOR};")
|
||||
self.setStyleSheet(f"background-color: #F8F9FA;") # Slightly lighter background
|
||||
|
||||
# Main layout
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(40, 40, 40, 40)
|
||||
layout.setSpacing(20)
|
||||
|
||||
# Header with logo
|
||||
header_frame = QFrame(self)
|
||||
header_frame.setStyleSheet("background-color: #34495E; border-radius: 10px;") # Softer dark blue
|
||||
header_frame.setFixedHeight(100)
|
||||
|
||||
header_layout = QHBoxLayout(header_frame)
|
||||
header_layout.setContentsMargins(20, 0, 20, 0)
|
||||
|
||||
# Logo
|
||||
logo_label = QLabel(self)
|
||||
logo_path = os.path.join(UXUI_ASSETS, "Assets_png/kneron_logo.png")
|
||||
if os.path.exists(logo_path):
|
||||
logo_pixmap = QPixmap(logo_path)
|
||||
logo_label.setPixmap(logo_pixmap)
|
||||
scaled_logo = logo_pixmap.scaled(150, 60, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
logo_label.setPixmap(scaled_logo)
|
||||
logo_label.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(logo_label)
|
||||
|
||||
header_layout.addWidget(logo_label, alignment=Qt.AlignCenter)
|
||||
layout.addWidget(header_frame)
|
||||
|
||||
# Content container
|
||||
content_container = QFrame(self)
|
||||
content_container.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #E0E0E0;
|
||||
}
|
||||
""")
|
||||
content_layout = QVBoxLayout(content_container)
|
||||
content_layout.setContentsMargins(30, 30, 30, 30)
|
||||
content_layout.setSpacing(20)
|
||||
|
||||
# Title
|
||||
title_label = QLabel("Kneron Academy", self)
|
||||
title_label = QLabel(APP_NAME, self)
|
||||
title_label.setAlignment(Qt.AlignCenter)
|
||||
title_label.setFont(QFont("Arial", 24, QFont.Bold))
|
||||
layout.addWidget(title_label)
|
||||
title_label.setStyleSheet("font-size: 28px; font-weight: bold; color: #34495E;")
|
||||
content_layout.addWidget(title_label)
|
||||
|
||||
# Subtitle
|
||||
subtitle_label = QLabel("Please select an option to continue", self)
|
||||
subtitle_label = QLabel("請選擇您要使用的功能", self)
|
||||
subtitle_label.setAlignment(Qt.AlignCenter)
|
||||
subtitle_label.setFont(QFont("Arial", 14))
|
||||
layout.addWidget(subtitle_label)
|
||||
subtitle_label.setStyleSheet("font-size: 16px; color: #7F8C8D;")
|
||||
content_layout.addWidget(subtitle_label)
|
||||
|
||||
# Add some space
|
||||
layout.addSpacing(40)
|
||||
content_layout.addSpacing(20)
|
||||
|
||||
# Button container
|
||||
button_container = QWidget(self)
|
||||
button_layout = QHBoxLayout(button_container)
|
||||
button_layout.setContentsMargins(50, 0, 50, 0)
|
||||
button_layout.setContentsMargins(20, 0, 20, 0)
|
||||
button_layout.setSpacing(40) # Increased spacing between buttons
|
||||
|
||||
# Utilities button
|
||||
utilities_button = QPushButton("Utilities", self)
|
||||
utilities_button.setMinimumHeight(80)
|
||||
utilities_button.setFont(QFont("Arial", 14))
|
||||
utilities_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #1E88E5;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
# Create card-style buttons with icons and descriptions
|
||||
|
||||
# Utilities card button
|
||||
utilities_card = QFrame(self)
|
||||
utilities_card.setMinimumSize(300, 250)
|
||||
utilities_card.setCursor(Qt.PointingHandCursor)
|
||||
utilities_card.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: white;
|
||||
border-radius: 15px;
|
||||
border: 1px solid #E0E0E0;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #1976D2;
|
||||
QFrame:hover {
|
||||
background-color: #F5F9FF;
|
||||
border: 1px solid #5DADE2;
|
||||
}
|
||||
""")
|
||||
utilities_button.clicked.connect(self.open_utilities.emit)
|
||||
|
||||
# Demo App button
|
||||
demo_button = QPushButton("Demo App", self)
|
||||
demo_button.setMinimumHeight(80)
|
||||
demo_button.setFont(QFont("Arial", 14))
|
||||
demo_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #43A047;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
utilities_layout = QVBoxLayout(utilities_card)
|
||||
utilities_layout.setContentsMargins(20, 20, 20, 20)
|
||||
utilities_layout.setSpacing(15)
|
||||
utilities_layout.setAlignment(Qt.AlignCenter)
|
||||
|
||||
# Icon for utilities
|
||||
utilities_icon_label = QLabel()
|
||||
utilities_icon_path = os.path.join(UXUI_ASSETS, "Assets_svg/ic_dialog_device.svg")
|
||||
if os.path.exists(utilities_icon_path):
|
||||
utilities_icon = QPixmap(utilities_icon_path)
|
||||
scaled_icon = utilities_icon.scaled(64, 64, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
utilities_icon_label.setPixmap(scaled_icon)
|
||||
utilities_icon_label.setAlignment(Qt.AlignCenter)
|
||||
utilities_layout.addWidget(utilities_icon_label, alignment=Qt.AlignCenter)
|
||||
|
||||
# Title for utilities
|
||||
utilities_title = QLabel("設備管理工具", self)
|
||||
utilities_title.setAlignment(Qt.AlignCenter)
|
||||
utilities_title.setStyleSheet("font-size: 22px; font-weight: bold; color: #34495E;")
|
||||
utilities_layout.addWidget(utilities_title)
|
||||
|
||||
# Description for utilities
|
||||
utilities_desc = QLabel("連接、管理和更新您的 Kneron 設備", self)
|
||||
utilities_desc.setAlignment(Qt.AlignCenter)
|
||||
utilities_desc.setWordWrap(True)
|
||||
utilities_desc.setStyleSheet("font-size: 14px; color: #7F8C8D;")
|
||||
utilities_layout.addWidget(utilities_desc)
|
||||
|
||||
# Connect the card to the signal
|
||||
utilities_card.mousePressEvent = lambda event: self.open_utilities.emit()
|
||||
|
||||
# Demo App card button
|
||||
demo_card = QFrame(self)
|
||||
demo_card.setMinimumSize(300, 250)
|
||||
demo_card.setCursor(Qt.PointingHandCursor)
|
||||
demo_card.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: white;
|
||||
border-radius: 15px;
|
||||
border: 1px solid #E0E0E0;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #388E3C;
|
||||
QFrame:hover {
|
||||
background-color: #F5FFF7;
|
||||
border: 1px solid #7DCEA0;
|
||||
}
|
||||
""")
|
||||
demo_button.clicked.connect(self.open_demo_app.emit)
|
||||
|
||||
# Add buttons to layout
|
||||
button_layout.addWidget(utilities_button)
|
||||
button_layout.addWidget(demo_button)
|
||||
demo_layout = QVBoxLayout(demo_card)
|
||||
demo_layout.setContentsMargins(20, 20, 20, 20)
|
||||
demo_layout.setSpacing(15)
|
||||
demo_layout.setAlignment(Qt.AlignCenter)
|
||||
|
||||
layout.addWidget(button_container)
|
||||
layout.addStretch(1) # Push everything up
|
||||
# Icon for demo app
|
||||
demo_icon_label = QLabel()
|
||||
demo_icon_path = os.path.join(UXUI_ASSETS, "Assets_svg/ic_recording_camera.svg")
|
||||
if os.path.exists(demo_icon_path):
|
||||
demo_icon = QPixmap(demo_icon_path)
|
||||
scaled_icon = demo_icon.scaled(64, 64, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
demo_icon_label.setPixmap(scaled_icon)
|
||||
demo_icon_label.setAlignment(Qt.AlignCenter)
|
||||
demo_layout.addWidget(demo_icon_label, alignment=Qt.AlignCenter)
|
||||
|
||||
# Title for demo app
|
||||
demo_title = QLabel("AI 演示應用", self)
|
||||
demo_title.setAlignment(Qt.AlignCenter)
|
||||
demo_title.setStyleSheet("font-size: 22px; font-weight: bold; color: #34495E;")
|
||||
demo_layout.addWidget(demo_title)
|
||||
|
||||
# Description for demo app
|
||||
demo_desc = QLabel("使用 AI 工具箱探索和測試 Kneron 設備的功能", self)
|
||||
demo_desc.setAlignment(Qt.AlignCenter)
|
||||
demo_desc.setWordWrap(True)
|
||||
demo_desc.setStyleSheet("font-size: 14px; color: #7F8C8D;")
|
||||
demo_layout.addWidget(demo_desc)
|
||||
|
||||
# Connect the card to the signal
|
||||
demo_card.mousePressEvent = lambda event: self.open_demo_app.emit()
|
||||
|
||||
# Add cards to layout
|
||||
button_layout.addWidget(utilities_card)
|
||||
button_layout.addWidget(demo_card)
|
||||
|
||||
content_layout.addWidget(button_container)
|
||||
|
||||
# Add content container to main layout
|
||||
layout.addWidget(content_container, 1)
|
||||
|
||||
# Footer
|
||||
footer_label = QLabel(" 2025 Innovedus Inc. All rights reserved.", self)
|
||||
footer_label.setAlignment(Qt.AlignCenter)
|
||||
footer_label.setStyleSheet("font-size: 12px; color: #95A5A6;")
|
||||
layout.addWidget(footer_label)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -33,3 +33,42 @@ Times: 8.5 Hours
|
||||
3. 修改 popup windows 中顯示的 dongle 型號 -> 使用類似 完成事項4 的方式使用之前的 mapping
|
||||
4. 修改 inference output format including bb and single model
|
||||
5. multi-dongle inference
|
||||
|
||||
|
||||
|
||||
## 20250324
|
||||
1. 推論引擎優化
|
||||
幀尺寸不匹配問題修復
|
||||
修復了在暫停視頻模式後上傳圖片時出現的尺寸不匹配錯誤
|
||||
在 InferenceWorkerThread 的 run 方法中添加了幀尺寸檢查
|
||||
實現了比較當前幀與上一幀尺寸的機制,當尺寸不同時重置緩存
|
||||
解決了 "operands could not be broadcast together with shapes" 錯誤
|
||||
推論佇列管理優化
|
||||
添加了 _clear_inference_queue 方法,實現安全清空推論佇列
|
||||
在模式切換時清空佇列,避免使用舊數據
|
||||
在處理新上傳圖片前清空佇列
|
||||
重構了 process_uploaded_image 方法,提高可讀性和錯誤處理
|
||||
2. Utilities 畫面功能增強
|
||||
驅動程式安裝功能
|
||||
添加了紫色的「Install Driver」按鈕
|
||||
實現了 install_drivers 方法,支援多種 Kneron 裝置
|
||||
使用 kp.core.install_driver_for_windows API 安裝驅動程式
|
||||
添加了進度條顯示和詳細的錯誤處理
|
||||
Purchased Items 頁面改進
|
||||
移除了登入要求,讓用戶無需登入即可查看已購買項目
|
||||
修改了頁面佈局,使其在同一個視窗中顯示,而非彈出視窗
|
||||
添加了表格顯示已購買項目,包含產品、模型、版本和相容裝置資訊
|
||||
實現了「Refresh Items」按鈕功能
|
||||
表格功能增強
|
||||
添加了勾選框功能,允許用戶選擇多個項目
|
||||
實現了整行選擇功能,點擊任一單元格時整行變色
|
||||
修改了「Download Selected Items」按鈕功能,實現批量下載選中項目
|
||||
簡化了表格結構,移除了單獨的「Action」列和下載按鈕
|
||||
3. 用戶界面優化
|
||||
視覺設計改進
|
||||
統一了頁面風格和配色
|
||||
改進了按鈕和表格的樣式
|
||||
優化了佈局結構,提供更一致的用戶體驗
|
||||
導航優化
|
||||
實現了 Utilities 和 Purchased Items 頁面之間的無縫切換
|
||||
改進了頁面標題和描述,提供更清晰的功能說明
|
||||
Loading…
x
Reference in New Issue
Block a user