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:
parent
48acae9c74
commit
ec940c3f2f
@ -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):
|
||||
if result['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
|
||||
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(f"⚠ Assets folder set to: {result['path']}")
|
||||
print(f" Warning: {result['message']}")
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
193
mutliseries.py
193
mutliseries.py
@ -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)
|
||||
@ -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...'
|
||||
|
||||
@ -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,7 +16,40 @@ 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:
|
||||
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 = QFileDialog.getExistingDirectory(
|
||||
None,
|
||||
title,
|
||||
initial_dir,
|
||||
QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks
|
||||
)
|
||||
|
||||
return folder_path if folder_path else ""
|
||||
|
||||
except ImportError:
|
||||
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
|
||||
@ -41,10 +72,14 @@ def select_folder(title="Select Folder", initial_dir="", must_exist=True):
|
||||
return folder_path if folder_path else ""
|
||||
|
||||
except ImportError:
|
||||
print("tkinter not available")
|
||||
print("tkinter also not available")
|
||||
return ""
|
||||
except Exception as e:
|
||||
print(f"Error opening folder dialog: {e}")
|
||||
print(f"Error opening tkinter folder dialog: {e}")
|
||||
return ""
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error opening PyQt5 folder dialog: {e}")
|
||||
return ""
|
||||
|
||||
def select_assets_folder(initial_dir=""):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user