Improve assets folder selection and fix macOS tkinter crash

- Replace tkinter with PyQt5 QFileDialog as primary folder selector to fix macOS crashes
- Add specialized assets_folder property handling in dashboard with validation
- Integrate improved folder dialog utility with ExactModelNode
- Provide detailed validation feedback and user-friendly tooltips
- Maintain backward compatibility with tkinter as fallback

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mason 2025-08-14 11:26:23 +08:00
parent 48acae9c74
commit ec940c3f2f
4 changed files with 152 additions and 244 deletions

View File

@ -305,47 +305,46 @@ class ExactModelNode(BaseNode):
print(f"Warning: Could not setup custom property handlers: {e}")
def select_assets_folder(self):
"""Method to open folder selection dialog for assets folder using tkinter."""
"""Method to open folder selection dialog for assets folder using improved utility."""
if not NODEGRAPH_AVAILABLE:
return ""
try:
import tkinter as tk
from tkinter import filedialog
from utils.folder_dialog import select_assets_folder
# Create a root window but keep it hidden
root = tk.Tk()
root.withdraw() # Hide the main window
root.attributes('-topmost', True) # Bring dialog to front
# Get current folder path as initial directory
current_folder = ""
try:
current_folder = self.get_property('assets_folder') or ""
except:
pass
# Open folder selection dialog
folder_path = filedialog.askdirectory(
title="Select Assets Folder",
initialdir="",
mustexist=True
)
# Use the specialized assets folder dialog with validation
result = select_assets_folder(initial_dir=current_folder)
# Destroy the root window
root.destroy()
if folder_path:
# Validate the selected folder structure
if self._validate_assets_folder(folder_path):
# Set the property
if NODEGRAPH_AVAILABLE:
self.set_property('assets_folder', folder_path)
print(f"Assets folder set to: {folder_path}")
return folder_path
if result['path']:
# Set the property
if NODEGRAPH_AVAILABLE:
self.set_property('assets_folder', result['path'])
# Print validation results
if result['valid']:
print(f"✓ Valid Assets folder set to: {result['path']}")
if 'details' in result and 'available_series' in result['details']:
series = result['details']['available_series']
print(f" Available series: {', '.join(series)}")
else:
print(f"Warning: Selected folder does not have the expected structure")
print("Expected structure: Assets/Firmware/ and Assets/Models/ with series subfolders")
# Still set it, but warn user
if NODEGRAPH_AVAILABLE:
self.set_property('assets_folder', folder_path)
return folder_path
print(f"⚠ Assets folder set to: {result['path']}")
print(f" Warning: {result['message']}")
print(" Expected structure: Assets/Firmware/ and Assets/Models/ with series subfolders")
return result['path']
else:
print("No folder selected")
return ""
except ImportError:
print("tkinter not available, falling back to simple input")
print("utils.folder_dialog not available, falling back to simple input")
# Fallback to manual input
folder_path = input("Enter Assets folder path: ").strip()
if folder_path and NODEGRAPH_AVAILABLE:

View File

@ -1,193 +0,0 @@
import kp
from collections import defaultdict
from typing import Union
import os
import sys
import argparse
import time
import threading
import queue
import numpy as np
import cv2
# PWD = os.path.dirname(os.path.abspath(__file__))
# sys.path.insert(1, os.path.join(PWD, '..'))
IMAGE_FILE_PATH = r"c:\Users\mason\Downloads\kneron_plus_v3.1.2\kneron_plus\res\images\people_talk_in_street_640x640.bmp"
LOOP_TIME = 100
def _image_send_function(_device_group: kp.DeviceGroup,
_loop_time: int,
_generic_inference_input_descriptor: kp.GenericImageInferenceDescriptor,
_image: Union[bytes, np.ndarray],
_image_format: kp.ImageFormat) -> None:
for _loop in range(_loop_time):
try:
_generic_inference_input_descriptor.inference_number = _loop
_generic_inference_input_descriptor.input_node_image_list = [kp.GenericInputNodeImage(
image=_image,
image_format=_image_format,
resize_mode=kp.ResizeMode.KP_RESIZE_ENABLE,
padding_mode=kp.PaddingMode.KP_PADDING_CORNER,
normalize_mode=kp.NormalizeMode.KP_NORMALIZE_KNERON
)]
kp.inference.generic_image_inference_send(device_group=device_groups[1],
generic_inference_input_descriptor=_generic_inference_input_descriptor)
except kp.ApiKPException as exception:
print(' - Error: inference failed, error = {}'.format(exception))
exit(0)
def _result_receive_function(_device_group: kp.DeviceGroup,
_loop_time: int,
_result_queue: queue.Queue) -> None:
_generic_raw_result = None
for _loop in range(_loop_time):
try:
_generic_raw_result = kp.inference.generic_image_inference_receive(device_group=device_groups[1])
if _generic_raw_result.header.inference_number != _loop:
print(' - Error: incorrect inference_number {} at frame {}'.format(
_generic_raw_result.header.inference_number, _loop))
print('.', end='', flush=True)
except kp.ApiKPException as exception:
print(' - Error: inference failed, error = {}'.format(exception))
exit(0)
_result_queue.put(_generic_raw_result)
model_path = ["C:\\Users\\mason\\Downloads\\kneron_plus_v3.1.2\\kneron_plus\\res\\models\\KL520\\yolov5-noupsample_w640h640_kn-model-zoo\\kl520_20005_yolov5-noupsample_w640h640.nef", r"C:\Users\mason\Downloads\kneron_plus_v3.1.2\kneron_plus\res\models\KL720\yolov5-noupsample_w640h640_kn-model-zoo\kl720_20005_yolov5-noupsample_w640h640.nef"]
SCPU_FW_PATH_520 = "C:\\Users\\mason\\Downloads\\kneron_plus_v3.1.2\\kneron_plus\\res\\firmware\\KL520\\fw_scpu.bin"
NCPU_FW_PATH_520 = "C:\\Users\\mason\\Downloads\\kneron_plus_v3.1.2\\kneron_plus\\res\\firmware\\KL520\\fw_ncpu.bin"
SCPU_FW_PATH_720 = "C:\\Users\\mason\\Downloads\\kneron_plus_v3.1.2\\kneron_plus\\res\\firmware\\KL720\\fw_scpu.bin"
NCPU_FW_PATH_720 = "C:\\Users\\mason\\Downloads\\kneron_plus_v3.1.2\\kneron_plus\\res\\firmware\\KL720\\fw_ncpu.bin"
device_list = kp.core.scan_devices()
grouped_devices = defaultdict(list)
for device in device_list.device_descriptor_list:
grouped_devices[device.product_id].append(device.usb_port_id)
print(f"Found device groups: {dict(grouped_devices)}")
device_groups = []
for product_id, usb_port_id in grouped_devices.items():
try:
group = kp.core.connect_devices(usb_port_id)
device_groups.append(group)
print(f"Successfully connected to group for product ID {product_id} with ports{usb_port_id}")
except kp.ApiKPException as e:
print(f"Failed to connect to group for product ID {product_id}: {e}")
print(device_groups)
print('[Set Device Timeout]')
kp.core.set_timeout(device_group=device_groups[0], milliseconds=5000)
kp.core.set_timeout(device_group=device_groups[1], milliseconds=5000)
print(' - Success')
try:
print('[Upload Firmware]')
kp.core.load_firmware_from_file(device_group=device_groups[0],
scpu_fw_path=SCPU_FW_PATH_520,
ncpu_fw_path=NCPU_FW_PATH_520)
kp.core.load_firmware_from_file(device_group=device_groups[1],
scpu_fw_path=SCPU_FW_PATH_720,
ncpu_fw_path=NCPU_FW_PATH_720)
print(' - Success')
except kp.ApiKPException as exception:
print('Error: upload firmware failed, error = \'{}\''.format(str(exception)))
exit(0)
print('[Upload Model]')
model_nef_descriptors = []
# for group in device_groups:
model_nef_descriptor = kp.core.load_model_from_file(device_group=device_groups[0], file_path=model_path[0])
model_nef_descriptors.append(model_nef_descriptor)
model_nef_descriptor = kp.core.load_model_from_file(device_group=device_groups[1], file_path=model_path[1])
model_nef_descriptors.append(model_nef_descriptor)
print(' - Success')
"""
prepare the image
"""
print('[Read Image]')
img = cv2.imread(filename=IMAGE_FILE_PATH)
img_bgr565 = cv2.cvtColor(src=img, code=cv2.COLOR_BGR2BGR565)
print(' - Success')
"""
prepare generic image inference input descriptor
"""
print(model_nef_descriptors)
generic_inference_input_descriptor = kp.GenericImageInferenceDescriptor(
model_id=model_nef_descriptors[1].models[0].id,
)
"""
starting inference work
"""
print('[Starting Inference Work]')
print(' - Starting inference loop {} times'.format(LOOP_TIME))
print(' - ', end='')
result_queue = queue.Queue()
send_thread = threading.Thread(target=_image_send_function, args=(device_groups[1],
LOOP_TIME,
generic_inference_input_descriptor,
img_bgr565,
kp.ImageFormat.KP_IMAGE_FORMAT_RGB565))
receive_thread = threading.Thread(target=_result_receive_function, args=(device_groups[1],
LOOP_TIME,
result_queue))
start_inference_time = time.time()
send_thread.start()
receive_thread.start()
try:
while send_thread.is_alive():
send_thread.join(1)
while receive_thread.is_alive():
receive_thread.join(1)
except (KeyboardInterrupt, SystemExit):
print('\n - Received keyboard interrupt, quitting threads.')
exit(0)
end_inference_time = time.time()
time_spent = end_inference_time - start_inference_time
try:
generic_raw_result = result_queue.get(timeout=3)
except Exception as exception:
print('Error: Result queue is empty !')
exit(0)
print()
print('[Result]')
print(" - Total inference {} images".format(LOOP_TIME))
print(" - Time spent: {:.2f} secs, FPS = {:.1f}".format(time_spent, LOOP_TIME / time_spent))
"""
retrieve inference node output
"""
print('[Retrieve Inference Node Output ]')
inf_node_output_list = []
for node_idx in range(generic_raw_result.header.num_output_node):
inference_float_node_output = kp.inference.generic_inference_retrieve_float_node(node_idx=node_idx,
generic_raw_result=generic_raw_result,
channels_ordering=kp.ChannelOrdering.KP_CHANNEL_ORDERING_CHW)
inf_node_output_list.append(inference_float_node_output)
print(' - Success')
print('[Result]')
print(inf_node_output_list)

View File

@ -43,6 +43,7 @@ except ImportError:
from config.theme import HARMONIOUS_THEME_STYLESHEET
from config.settings import get_settings
from utils.folder_dialog import select_assets_folder
try:
from core.nodes import (
InputNode, ModelNode, PreprocessNode, PostprocessNode, OutputNode,
@ -1323,8 +1324,74 @@ class IntegratedPipelineDashboard(QMainWindow):
if hasattr(node, '_property_options') and prop_name in node._property_options:
prop_options = node._property_options[prop_name]
# Check for file path properties first (from prop_options or name pattern)
if (prop_options and isinstance(prop_options, dict) and prop_options.get('type') == 'file_path') or \
# Special handling for assets_folder property
if prop_name == 'assets_folder':
# Assets folder property with validation and improved dialog
display_text = self.truncate_path_smart(str(prop_value)) if prop_value else 'Select Assets Folder...'
widget = QPushButton(display_text)
# Set fixed width and styling to prevent expansion
widget.setMaximumWidth(250)
widget.setMinimumWidth(200)
widget.setStyleSheet("""
QPushButton {
text-align: left;
padding: 5px 8px;
background-color: #45475a;
color: #cdd6f4;
border: 1px solid #585b70;
border-radius: 4px;
font-size: 10px;
}
QPushButton:hover {
background-color: #585b70;
border-color: #a6e3a1;
}
QPushButton:pressed {
background-color: #313244;
}
""")
# Store full path for tooltip and internal use
full_path = str(prop_value) if prop_value else ''
widget.setToolTip(f"Full path: {full_path}\n\nClick to browse for Assets folder\n(Should contain Firmware/ and Models/ subfolders)")
def browse_assets_folder():
# Use the specialized assets folder dialog with validation
result = select_assets_folder(initial_dir=full_path or '')
if result['path']:
# Update button text with truncated path
truncated_text = self.truncate_path_smart(result['path'])
widget.setText(truncated_text)
# Create detailed tooltip with validation results
tooltip_lines = [f"Full path: {result['path']}"]
if result['valid']:
tooltip_lines.append("✓ Valid Assets folder structure detected")
if 'details' in result and 'available_series' in result['details']:
series = result['details']['available_series']
tooltip_lines.append(f"Available series: {', '.join(series)}")
else:
tooltip_lines.append(f"{result['message']}")
tooltip_lines.append("\nClick to browse for Assets folder")
widget.setToolTip('\n'.join(tooltip_lines))
# Set property with full path
if hasattr(node, 'set_property'):
node.set_property(prop_name, result['path'])
# Show validation message to user
if not result['valid']:
QMessageBox.warning(self, "Assets Folder Validation",
f"Selected folder may not have the expected structure:\n\n{result['message']}\n\n"
"Expected structure:\nAssets/\n├── Firmware/\n│ └── KL520/, KL720/, etc.\n└── Models/\n └── KL520/, KL720/, etc.")
widget.clicked.connect(browse_assets_folder)
# Check for file path properties (from prop_options or name pattern)
elif (prop_options and isinstance(prop_options, dict) and prop_options.get('type') == 'file_path') or \
prop_name in ['model_path', 'source_path', 'destination']:
# File path property with smart truncation and width limits
display_text = self.truncate_path_smart(str(prop_value)) if prop_value else 'Select File...'

View File

@ -1,14 +1,12 @@
"""
Folder selection utilities using tkinter
Folder selection utilities using PyQt5 as primary, tkinter as fallback
"""
import tkinter as tk
from tkinter import filedialog
import os
def select_folder(title="Select Folder", initial_dir="", must_exist=True):
"""
Open a folder selection dialog using tkinter
Open a folder selection dialog using PyQt5 (preferred) or tkinter (fallback)
Args:
title (str): Dialog window title
@ -18,33 +16,70 @@ def select_folder(title="Select Folder", initial_dir="", must_exist=True):
Returns:
str: Selected folder path, or empty string if cancelled
"""
# Try PyQt5 first (more reliable on macOS)
try:
# Create a root window but keep it hidden
root = tk.Tk()
root.withdraw() # Hide the main window
root.attributes('-topmost', True) # Bring dialog to front
from PyQt5.QtWidgets import QApplication, QFileDialog
import sys
# Create QApplication if it doesn't exist
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
# Set initial directory
if not initial_dir:
initial_dir = os.getcwd()
elif not os.path.exists(initial_dir):
initial_dir = os.getcwd()
# Open folder selection dialog
folder_path = filedialog.askdirectory(
title=title,
initialdir=initial_dir,
mustexist=must_exist
folder_path = QFileDialog.getExistingDirectory(
None,
title,
initial_dir,
QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks
)
# Destroy the root window
root.destroy()
return folder_path if folder_path else ""
except ImportError:
print("tkinter not available")
return ""
print("PyQt5 not available, trying tkinter...")
# Fallback to tkinter
try:
import tkinter as tk
from tkinter import filedialog
# Create a root window but keep it hidden
root = tk.Tk()
root.withdraw() # Hide the main window
root.attributes('-topmost', True) # Bring dialog to front
# Set initial directory
if not initial_dir:
initial_dir = os.getcwd()
# Open folder selection dialog
folder_path = filedialog.askdirectory(
title=title,
initialdir=initial_dir,
mustexist=must_exist
)
# Destroy the root window
root.destroy()
return folder_path if folder_path else ""
except ImportError:
print("tkinter also not available")
return ""
except Exception as e:
print(f"Error opening tkinter folder dialog: {e}")
return ""
except Exception as e:
print(f"Error opening folder dialog: {e}")
print(f"Error opening PyQt5 folder dialog: {e}")
return ""
def select_assets_folder(initial_dir=""):