main_window.py 58 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451
  1. """
  2. Main Window Module
  3. The main application window that integrates all components:
  4. - Dashboard with all panels
  5. - Tab navigation
  6. - Menu bar
  7. - Worker thread management
  8. - File dialogs and processing
  9. """
  10. import sys
  11. from datetime import datetime
  12. from pathlib import Path
  13. from typing import Optional, Dict
  14. from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
  15. QTabWidget, QFrame, QLabel, QFileDialog, QMessageBox, QPushButton, QStyle)
  16. from PyQt5.QtCore import Qt, QThreadPool, QTimer, QSize
  17. from PyQt5.QtGui import QFont, QPixmap, QIcon
  18. from ui.panels import (SystemStatusPanel, QuickActionsPanel, RecentResultsPanel,
  19. SystemInfoPanel, LiveFeedPanel)
  20. from ui.tabs import RipenessTab, QualityTab, MaturityTab, ParametersTab, ReportsTab
  21. from ui.dialogs import AboutDialog, HelpDialog, ManualInputDialog, CameraAppCheckDialog
  22. from models import AudioModel, DefectModel, LoculeModel, MaturityModel, ShapeModel
  23. from workers import AudioWorker, DefectWorker, LoculeWorker, MaturityWorker, ShapeWorker
  24. from utils.config import (WINDOW_TITLE, WINDOW_WIDTH, WINDOW_HEIGHT, DEVICE_ID,
  25. get_device, DEFAULT_DIRS, FILE_FILTERS, PROJECT_ROOT)
  26. from utils.data_manager import DataManager
  27. from utils.process_utils import get_missing_camera_apps, get_running_camera_apps
  28. from utils.camera_automation import SecondLookAutomation, EOSUtilityAutomation, AnalyzIRAutomation, CameraAutomationError
  29. from resources.styles import MAIN_WINDOW_STYLE, TAB_WIDGET_STYLE, HEADER_ICON_BUTTON_STYLE
  30. class DuDONGMainWindow(QMainWindow):
  31. """
  32. Main application window for DuDONG Grading System.
  33. Integrates all UI components, manages worker threads, and handles
  34. user interactions for ripeness and quality classification.
  35. Attributes:
  36. thread_pool: QThreadPool for managing worker threads
  37. models: Dict of loaded AI models
  38. status_panels: Dict of UI panel references
  39. """
  40. def __init__(self):
  41. """Initialize the main window."""
  42. super().__init__()
  43. self.setWindowTitle(WINDOW_TITLE)
  44. self.setGeometry(100, 100, WINDOW_WIDTH, WINDOW_HEIGHT)
  45. # Initialize thread pool
  46. self.thread_pool = QThreadPool()
  47. print(f"Thread pool initialized with {self.thread_pool.maxThreadCount()} threads")
  48. # Initialize data manager for persistence
  49. self.data_manager = DataManager()
  50. print("Data manager initialized")
  51. # Initialize models (will be loaded on demand)
  52. self.models = {
  53. 'audio': None,
  54. 'defect': None,
  55. 'locule': None,
  56. 'maturity': None,
  57. 'shape': None
  58. }
  59. # Track processing state
  60. self.is_processing = False
  61. self.report_results = {} # Store results for reports tab
  62. self.current_analysis_id = None # Track current analysis for saving
  63. self.analysis_start_time = None # Track when analysis started
  64. # Track application start time for uptime calculation
  65. self.app_start_time = datetime.now()
  66. # Cache GPU status to avoid repeated checks
  67. self._gpu_status_cache = None
  68. self._last_model_count = 0
  69. # Initialize UI
  70. self.init_ui()
  71. # Load models in background
  72. self.load_models()
  73. # Start timer for status updates
  74. self.init_timer()
  75. def init_ui(self):
  76. """Initialize the user interface."""
  77. # Set window style
  78. self.setStyleSheet(MAIN_WINDOW_STYLE)
  79. # Central widget
  80. central_widget = QWidget()
  81. self.setCentralWidget(central_widget)
  82. main_layout = QVBoxLayout(central_widget)
  83. main_layout.setContentsMargins(0, 0, 0, 0)
  84. main_layout.setSpacing(0)
  85. # Header
  86. header = self.create_header()
  87. main_layout.addWidget(header)
  88. # Tab widget for different views
  89. self.tab_widget = QTabWidget()
  90. self.tab_widget.setStyleSheet(TAB_WIDGET_STYLE)
  91. # Dashboard tab (main)
  92. dashboard = self.create_dashboard()
  93. self.tab_widget.addTab(dashboard, "Dashboard")
  94. # Processing tabs - HIDDEN FOR NOW
  95. self.ripeness_tab = RipenessTab()
  96. self.ripeness_tab.load_audio_requested.connect(self.on_ripeness_load_audio)
  97. ripeness_index = self.tab_widget.addTab(self.ripeness_tab, "Ripeness")
  98. self.tab_widget.setTabVisible(ripeness_index, False)
  99. self.quality_tab = QualityTab()
  100. self.quality_tab.load_image_requested.connect(self.on_quality_load_image)
  101. quality_index = self.tab_widget.addTab(self.quality_tab, "Quality")
  102. self.tab_widget.setTabVisible(quality_index, False)
  103. self.maturity_tab = MaturityTab()
  104. self.maturity_tab.load_tiff_requested.connect(self.on_maturity_load_tiff)
  105. maturity_index = self.tab_widget.addTab(self.maturity_tab, "Maturity")
  106. self.tab_widget.setTabVisible(maturity_index, False)
  107. # Placeholder tabs (to be implemented in future) - HIDDEN FOR NOW
  108. self.parameters_tab = ParametersTab()
  109. parameters_index = self.tab_widget.addTab(self.parameters_tab, "Parameters")
  110. self.tab_widget.setTabVisible(parameters_index, False)
  111. self.reports_tab = ReportsTab()
  112. self.reports_tab.go_to_dashboard.connect(self.on_go_to_dashboard)
  113. self.tab_widget.addTab(self.reports_tab, "Reports")
  114. main_layout.addWidget(self.tab_widget)
  115. # Status bar
  116. status_bar = self.create_status_bar()
  117. main_layout.addWidget(status_bar)
  118. # Initial update of system info panel with existing data
  119. self.update_system_info_panel()
  120. def create_header(self) -> QFrame:
  121. """
  122. Create the application header.
  123. Returns:
  124. QFrame: Header widget
  125. """
  126. header = QFrame()
  127. header.setFixedHeight(80)
  128. header.setStyleSheet("background-color: #2c3e50;")
  129. layout = QHBoxLayout(header)
  130. layout.setContentsMargins(20, 10, 20, 10)
  131. # Logo on the left
  132. logo_path = PROJECT_ROOT / "assets" / "logos" / "dudong_logo.png"
  133. if logo_path.exists():
  134. logo_label = QLabel()
  135. logo_pixmap = QPixmap(str(logo_path))
  136. # Scale logo to fit header height (80px - margins)
  137. scaled_logo = logo_pixmap.scaledToHeight(60, Qt.SmoothTransformation)
  138. logo_label.setPixmap(scaled_logo)
  139. logo_label.setFixedWidth(60)
  140. layout.addWidget(logo_label)
  141. layout.addSpacing(15)
  142. else:
  143. # Debug: print path not found
  144. print(f"[DEBUG] Logo not found at: {logo_path}")
  145. # Title
  146. title = QLabel(WINDOW_TITLE)
  147. title.setStyleSheet("color: white; font-size: 22px; font-weight: bold;")
  148. layout.addWidget(title)
  149. layout.addStretch()
  150. # Icon buttons on the right
  151. # Help/Support button
  152. help_btn = QPushButton()
  153. help_icon = self.style().standardIcon(QStyle.SP_MessageBoxQuestion)
  154. help_btn.setIcon(help_icon)
  155. help_btn.setIconSize(QSize(24, 24))
  156. help_btn.setStyleSheet(HEADER_ICON_BUTTON_STYLE)
  157. help_btn.setToolTip("Help & Support (F1)")
  158. help_btn.clicked.connect(self.show_help)
  159. layout.addWidget(help_btn)
  160. # About/Info button
  161. about_btn = QPushButton()
  162. about_icon = self.style().standardIcon(QStyle.SP_MessageBoxInformation)
  163. about_btn.setIcon(about_icon)
  164. about_btn.setIconSize(QSize(24, 24))
  165. about_btn.setStyleSheet(HEADER_ICON_BUTTON_STYLE)
  166. about_btn.setToolTip("About DuDONG")
  167. about_btn.clicked.connect(self.show_about)
  168. layout.addWidget(about_btn)
  169. # Exit button
  170. exit_btn = QPushButton()
  171. exit_icon = self.style().standardIcon(QStyle.SP_MessageBoxCritical)
  172. exit_btn.setIcon(exit_icon)
  173. exit_btn.setIconSize(QSize(24, 24))
  174. exit_btn.setStyleSheet(HEADER_ICON_BUTTON_STYLE)
  175. exit_btn.setToolTip("Exit Application (Ctrl+Q)")
  176. exit_btn.clicked.connect(self.close)
  177. layout.addWidget(exit_btn)
  178. return header
  179. def create_dashboard(self) -> QWidget:
  180. """
  181. Create the main dashboard view.
  182. Returns:
  183. QWidget: Dashboard widget
  184. """
  185. dashboard = QWidget()
  186. layout = QVBoxLayout(dashboard)
  187. # Top row: Status, Actions, and Results
  188. top_layout = QHBoxLayout()
  189. # System Status Panel (left)
  190. self.status_panel = SystemStatusPanel()
  191. self.status_panel.setMinimumWidth(360)
  192. # Pass models reference to status panel
  193. self.status_panel.set_models_reference(self.models)
  194. top_layout.addWidget(self.status_panel, 1)
  195. # Middle column: Quick Actions and Recent Results
  196. middle_layout = QVBoxLayout()
  197. # Quick Actions Panel
  198. self.actions_panel = QuickActionsPanel()
  199. self.actions_panel.setMinimumWidth(380)
  200. self.actions_panel.setMaximumHeight(350) # Increased for new button
  201. # Connect signals
  202. self.actions_panel.analyze_durian_clicked.connect(self.on_analyze_durian_clicked)
  203. self.actions_panel.ripeness_clicked.connect(self.on_ripeness_clicked)
  204. self.actions_panel.quality_clicked.connect(self.on_quality_clicked)
  205. self.actions_panel.calibration_clicked.connect(self.on_calibration_clicked)
  206. self.actions_panel.batch_clicked.connect(self.on_batch_clicked)
  207. middle_layout.addWidget(self.actions_panel)
  208. # Recent Results Panel (pass DataManager for database integration)
  209. self.results_panel = RecentResultsPanel(data_manager=self.data_manager)
  210. self.results_panel.setMinimumWidth(380)
  211. # Connect view button signal to handler
  212. self.results_panel.view_analysis_requested.connect(self.on_view_analysis)
  213. middle_layout.addWidget(self.results_panel)
  214. top_layout.addLayout(middle_layout, 2)
  215. layout.addLayout(top_layout)
  216. # Bottom row: System Info and Live Feeds
  217. bottom_layout = QHBoxLayout()
  218. # System Information Panel
  219. self.info_panel = SystemInfoPanel()
  220. self.info_panel.setMinimumWidth(560)
  221. bottom_layout.addWidget(self.info_panel, 2)
  222. # Live Feed Panel
  223. self.feed_panel = LiveFeedPanel()
  224. bottom_layout.addWidget(self.feed_panel, 1)
  225. layout.addLayout(bottom_layout)
  226. return dashboard
  227. def create_status_bar(self) -> QFrame:
  228. """
  229. Create the application status bar.
  230. Returns:
  231. QFrame: Status bar widget
  232. """
  233. status_bar = QFrame()
  234. status_bar.setFixedHeight(40)
  235. status_bar.setStyleSheet("background-color: #34495e;")
  236. layout = QHBoxLayout(status_bar)
  237. layout.setContentsMargins(20, 0, 20, 0)
  238. # Left side: detailed status
  239. self.status_text = QLabel("Ripeness Classifier Active | Model: RipeNet | GPU: -- | Processing: IDLE")
  240. self.status_text.setStyleSheet("color: #ecf0f1; font-size: 12px;")
  241. layout.addWidget(self.status_text)
  242. layout.addStretch()
  243. # Right side: ready indicator
  244. self.ready_indicator = QLabel("● READY FOR TESTING")
  245. self.ready_indicator.setStyleSheet("color: #27ae60; font-size: 12px; font-weight: bold;")
  246. layout.addWidget(self.ready_indicator)
  247. return status_bar
  248. def init_timer(self):
  249. """Initialize update timer."""
  250. self.timer = QTimer()
  251. self.timer.timeout.connect(self.update_status_bar)
  252. self.timer.start(1000) # Update every second
  253. def update_status_bar(self):
  254. """
  255. Update status bar with current time and info.
  256. Optimized to minimize overhead:
  257. - GPU status is cached and only checked once at startup
  258. - Model count is cached until it changes
  259. - Only text updates when status actually changes
  260. """
  261. # Only update if footer components exist
  262. if not hasattr(self, 'status_text') or not hasattr(self, 'ready_indicator'):
  263. return
  264. # Get model load status (lightweight check)
  265. model_status = self.get_model_load_status()
  266. loaded_count = sum(1 for status in model_status.values() if status)
  267. # Cache GPU status after first check (it won't change during runtime)
  268. if self._gpu_status_cache is None:
  269. try:
  270. import torch
  271. self._gpu_status_cache = "Active" if torch.cuda.is_available() else "N/A"
  272. except:
  273. self._gpu_status_cache = "N/A"
  274. gpu_status = self._gpu_status_cache
  275. # Get processing status - only show "Processing" when actually processing
  276. if self.is_processing:
  277. processing_status = "Processing"
  278. ready_text = "● PROCESSING"
  279. ready_color = "#f39c12" # Orange
  280. else:
  281. processing_status = "IDLE"
  282. ready_text = "● READY FOR TESTING"
  283. ready_color = "#27ae60" # Green
  284. # Build status text (only if something changed to reduce UI updates)
  285. models_info = f"{loaded_count}/5"
  286. status = f"DuDONG Active | Model: {models_info} | GPU: {gpu_status} | Processing: {processing_status}"
  287. # Only update text if it actually changed
  288. if self.status_text.text() != status:
  289. self.status_text.setText(status)
  290. if self.ready_indicator.text() != ready_text:
  291. self.ready_indicator.setText(ready_text)
  292. self.ready_indicator.setStyleSheet(f"color: {ready_color}; font-size: 12px; font-weight: bold;")
  293. def load_models(self):
  294. """Load AI models in background."""
  295. device = get_device()
  296. try:
  297. # Audio model (CPU for TensorFlow)
  298. print("Loading audio model...")
  299. self.models['audio'] = AudioModel(device='cpu')
  300. if self.models['audio'].load():
  301. self.status_panel.update_model_status('ripeness', 'online', 'Loaded')
  302. print("✓ Audio model loaded")
  303. else:
  304. self.status_panel.update_model_status('ripeness', 'offline', 'Failed')
  305. print("✗ Audio model failed to load")
  306. except Exception as e:
  307. print(f"Error loading audio model: {e}")
  308. self.status_panel.update_model_status('ripeness', 'offline', 'Error')
  309. try:
  310. # Defect model (GPU)
  311. print("Loading defect model...")
  312. self.models['defect'] = DefectModel(device=device)
  313. if self.models['defect'].load():
  314. self.status_panel.update_model_status('quality', 'online', 'Loaded')
  315. print("✓ Defect model loaded")
  316. else:
  317. self.status_panel.update_model_status('quality', 'offline', 'Failed')
  318. print("✗ Defect model failed to load")
  319. except Exception as e:
  320. print(f"Error loading defect model: {e}")
  321. self.status_panel.update_model_status('quality', 'offline', 'Error')
  322. try:
  323. # Locule model (GPU)
  324. print("Loading locule model...")
  325. self.models['locule'] = LoculeModel(device=device)
  326. if self.models['locule'].load():
  327. self.status_panel.update_model_status('defect', 'online', 'Loaded')
  328. print("✓ Locule model loaded")
  329. else:
  330. self.status_panel.update_model_status('defect', 'offline', 'Failed')
  331. print("✗ Locule model failed to load")
  332. except Exception as e:
  333. print(f"Error loading locule model: {e}")
  334. self.status_panel.update_model_status('defect', 'offline', 'Error')
  335. try:
  336. # Maturity model (GPU)
  337. print("Loading maturity model...")
  338. self.models['maturity'] = MaturityModel(device=device)
  339. if self.models['maturity'].load():
  340. print("✓ Maturity model loaded")
  341. else:
  342. print("✗ Maturity model failed to load")
  343. except Exception as e:
  344. print(f"Error loading maturity model: {e}")
  345. try:
  346. # Shape model (GPU)
  347. print("Loading shape model...")
  348. shape_model_path = PROJECT_ROOT / "model_files" / "shape.pt"
  349. self.models['shape'] = ShapeModel(str(shape_model_path), device=device)
  350. if self.models['shape'].load():
  351. print("✓ Shape model loaded")
  352. else:
  353. print("✗ Shape model failed to load")
  354. except Exception as e:
  355. print(f"Error loading shape model: {e}")
  356. # Refresh system status display after all models are loaded
  357. if hasattr(self, 'status_panel'):
  358. self.status_panel.refresh_status()
  359. print("✓ System status panel updated")
  360. def get_model_load_status(self) -> Dict[str, bool]:
  361. """
  362. Get the load status of all AI models.
  363. Returns:
  364. Dict mapping model key to loaded status
  365. """
  366. return {
  367. 'audio': self.models['audio'].is_loaded if self.models['audio'] else False,
  368. 'defect': self.models['defect'].is_loaded if self.models['defect'] else False,
  369. 'locule': self.models['locule'].is_loaded if self.models['locule'] else False,
  370. 'maturity': self.models['maturity'].is_loaded if self.models['maturity'] else False,
  371. 'shape': self.models['shape'].is_loaded if self.models['shape'] else False,
  372. }
  373. def update_system_info_panel(self):
  374. """
  375. Update System Information Panel with real-time statistics.
  376. Gathers data from:
  377. - DataManager: daily count, average processing time, model accuracy stats
  378. - App state: uptime
  379. Then calls update methods on self.info_panel to display the values.
  380. Note: Memory info is displayed in SystemStatusPanel, not here.
  381. """
  382. try:
  383. if not hasattr(self, 'info_panel'):
  384. return
  385. # Calculate uptime
  386. uptime_delta = datetime.now() - self.app_start_time
  387. uptime_hours = uptime_delta.seconds // 3600
  388. uptime_minutes = (uptime_delta.seconds % 3600) // 60
  389. # Get statistics from DataManager
  390. daily_count = self.data_manager.get_daily_analysis_count()
  391. avg_processing_time = self.data_manager.get_average_processing_time()
  392. accuracy_stats = self.data_manager.get_model_accuracy_stats()
  393. # Update panel with all values
  394. self.info_panel.update_uptime(uptime_hours, uptime_minutes)
  395. self.info_panel.update_throughput(daily_count)
  396. self.info_panel.update_processing_time(avg_processing_time)
  397. # Update model accuracy stats
  398. for model_name, accuracy in accuracy_stats.items():
  399. self.info_panel.update_accuracy(model_name, accuracy)
  400. print("✓ System info panel updated")
  401. except Exception as e:
  402. print(f"Error updating system info panel: {e}")
  403. import traceback
  404. traceback.print_exc()
  405. # ==================== Quick Action Handlers ====================
  406. def on_analyze_durian_clicked(self, manual_mode: bool):
  407. """
  408. Handle analyze durian button click.
  409. Args:
  410. manual_mode: True if manual input mode is selected, False for auto mode
  411. """
  412. if manual_mode:
  413. # Manual mode: Show dialog for file selection
  414. self._handle_manual_input_mode()
  415. else:
  416. # Auto mode: Check if camera apps are running
  417. self._handle_auto_mode()
  418. def _handle_auto_mode(self):
  419. """Handle automatic camera control mode."""
  420. print("Checking for camera applications...")
  421. # Check which camera apps are running
  422. missing_apps = get_missing_camera_apps()
  423. running_apps = get_running_camera_apps()
  424. print(f"Running camera apps: {running_apps}")
  425. print(f"Missing camera apps: {missing_apps}")
  426. if missing_apps:
  427. # Show dialog about missing apps
  428. dialog = CameraAppCheckDialog(missing_apps, self)
  429. dialog.exec_()
  430. print("Auto mode requires all camera applications to be running.")
  431. return
  432. # All apps are running - proceed with automated capture
  433. print("All camera applications detected. Attempting automated capture...")
  434. # Start with 2nd Look (multispectral) automation
  435. self._attempt_second_look_capture()
  436. def _attempt_second_look_capture(self):
  437. """Attempt to capture multispectral image from 2nd Look."""
  438. try:
  439. print("Initializing 2nd Look automation...")
  440. # Create automation instance
  441. second_look = SecondLookAutomation()
  442. # Check if window is open
  443. if not second_look.is_window_open():
  444. QMessageBox.warning(
  445. self,
  446. "2nd Look Not Responsive",
  447. "2nd Look window is not open or not responding.\n\n"
  448. "Please ensure 2nd Look is properly running before attempting automated capture."
  449. )
  450. print("2nd Look window not found or not responsive")
  451. return
  452. print("Capturing multispectral image from 2nd Look...")
  453. # Perform capture
  454. captured_file = second_look.capture()
  455. if captured_file:
  456. print(f"Successfully captured: {captured_file}")
  457. # Create inputs dict with captured multispectral file
  458. inputs = {
  459. 'dslr': '',
  460. 'multispectral': captured_file,
  461. 'thermal': '',
  462. 'audio': ''
  463. }
  464. # Process the capture
  465. self._process_manual_inputs(inputs)
  466. # Cleanup automation instance
  467. try:
  468. second_look.cleanup()
  469. except Exception as e:
  470. print(f"Warning during cleanup: {e}")
  471. else:
  472. QMessageBox.warning(
  473. self,
  474. "Capture Failed",
  475. "Failed to capture multispectral image from 2nd Look.\n\n"
  476. "Please verify:\n"
  477. "1. 2nd Look has an image loaded\n"
  478. "2. The camera is properly connected\n"
  479. "3. File system has write permissions"
  480. )
  481. print("Capture failed - file not created")
  482. except CameraAutomationError as e:
  483. QMessageBox.critical(
  484. self,
  485. "Camera Automation Error",
  486. f"Error during automated capture:\n\n{str(e)}"
  487. )
  488. print(f"Automation error: {e}")
  489. except Exception as e:
  490. QMessageBox.critical(
  491. self,
  492. "Unexpected Error",
  493. f"Unexpected error during automated capture:\n\n{str(e)}"
  494. )
  495. print(f"Unexpected error: {e}")
  496. def _attempt_eos_capture(self):
  497. """Attempt to capture image from EOS Utility DSLR."""
  498. try:
  499. print("Initializing EOS Utility automation...")
  500. # Create automation instance
  501. eos_utility = EOSUtilityAutomation()
  502. # Check if window is open
  503. if not eos_utility.is_window_open():
  504. QMessageBox.warning(
  505. self,
  506. "EOS Utility Not Responsive",
  507. "EOS Utility window is not open or not responding.\n\n"
  508. "Please ensure EOS Utility is properly running before attempting automated capture."
  509. )
  510. print("EOS Utility window not found or not responsive")
  511. return
  512. print("Capturing image from EOS Utility...")
  513. # Perform capture
  514. captured_file = eos_utility.capture()
  515. if captured_file:
  516. print(f"Successfully captured: {captured_file}")
  517. # Create inputs dict with captured DSLR file
  518. inputs = {
  519. 'dslr': captured_file,
  520. 'multispectral': '',
  521. 'thermal': '',
  522. 'audio': ''
  523. }
  524. # Process the capture
  525. self._process_manual_inputs(inputs)
  526. # Cleanup automation instance
  527. try:
  528. eos_utility.cleanup()
  529. except Exception as e:
  530. print(f"Warning during cleanup: {e}")
  531. else:
  532. QMessageBox.warning(
  533. self,
  534. "Capture Failed",
  535. "Failed to capture image from EOS Utility.\n\n"
  536. "Please verify:\n"
  537. "1. EOS Utility has a camera connected\n"
  538. "2. The camera is properly initialized\n"
  539. "3. File system has write permissions"
  540. )
  541. print("Capture failed - file not created")
  542. except CameraAutomationError as e:
  543. QMessageBox.critical(
  544. self,
  545. "Camera Automation Error",
  546. f"Error during automated capture:\n\n{str(e)}"
  547. )
  548. print(f"Automation error: {e}")
  549. except Exception as e:
  550. QMessageBox.critical(
  551. self,
  552. "Unexpected Error",
  553. f"Unexpected error during automated capture:\n\n{str(e)}"
  554. )
  555. print(f"Unexpected error: {e}")
  556. def _attempt_analyzir_capture(self):
  557. """Attempt to capture thermal data from AnalyzIR."""
  558. try:
  559. print("Initializing AnalyzIR automation...")
  560. # Create automation instance
  561. analyzir = AnalyzIRAutomation()
  562. # Check if window is open
  563. if not analyzir.is_window_open():
  564. QMessageBox.warning(
  565. self,
  566. "AnalyzIR Not Responsive",
  567. "AnalyzIR Venus window is not open or not responding.\n\n"
  568. "Please ensure AnalyzIR is properly running before attempting automated capture."
  569. )
  570. print("AnalyzIR window not found or not responsive")
  571. return
  572. print("Capturing thermal data from AnalyzIR...")
  573. # Perform capture
  574. captured_file = analyzir.capture()
  575. if captured_file:
  576. print(f"Successfully captured: {captured_file}")
  577. # Create inputs dict with captured thermal file
  578. inputs = {
  579. 'dslr': '',
  580. 'multispectral': '',
  581. 'thermal': captured_file,
  582. 'audio': ''
  583. }
  584. # Process the capture
  585. self._process_manual_inputs(inputs)
  586. # Cleanup automation instance
  587. try:
  588. analyzir.cleanup()
  589. except Exception as e:
  590. print(f"Warning during cleanup: {e}")
  591. else:
  592. QMessageBox.warning(
  593. self,
  594. "Capture Failed",
  595. "Failed to capture thermal data from AnalyzIR.\n\n"
  596. "Please verify:\n"
  597. "1. AnalyzIR has thermal image data loaded\n"
  598. "2. The IR camera (FOTRIC 323) is properly connected\n"
  599. "3. File system has write permissions"
  600. )
  601. print("Capture failed - file not created")
  602. except CameraAutomationError as e:
  603. QMessageBox.critical(
  604. self,
  605. "Camera Automation Error",
  606. f"Error during automated capture:\n\n{str(e)}"
  607. )
  608. print(f"Automation error: {e}")
  609. except Exception as e:
  610. QMessageBox.critical(
  611. self,
  612. "Unexpected Error",
  613. f"Unexpected error during automated capture:\n\n{str(e)}"
  614. )
  615. print(f"Unexpected error: {e}")
  616. def _handle_manual_input_mode(self):
  617. """Handle manual input mode with file dialogs."""
  618. print("Opening manual input dialog...")
  619. dialog = ManualInputDialog(self)
  620. dialog.inputs_confirmed.connect(self._process_manual_inputs)
  621. dialog.exec_()
  622. def _process_manual_inputs(self, inputs: dict):
  623. """
  624. Process manual camera inputs with RGB and multispectral models.
  625. Args:
  626. inputs: Dictionary with keys 'dslr_side', 'dslr_top', 'multispectral', 'thermal', 'audio'
  627. """
  628. print("Processing manual inputs:")
  629. print(f" DSLR Side: {inputs.get('dslr_side', 'Not provided')}")
  630. print(f" DSLR Top: {inputs.get('dslr_top', 'Not provided')}")
  631. print(f" Multispectral: {inputs.get('multispectral', 'Not provided')}")
  632. print(f" Thermal: {inputs.get('thermal', 'Not provided')}")
  633. print(f" Audio: {inputs.get('audio', 'Not provided')}")
  634. # Create analysis record
  635. report_id = f"DUR-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
  636. self.current_analysis_id = self.data_manager.create_analysis(report_id, DEVICE_ID)
  637. self.analysis_start_time = datetime.now()
  638. if not self.current_analysis_id:
  639. QMessageBox.critical(self, "Error", "Failed to create analysis record. Cannot proceed.")
  640. return
  641. print(f"Created analysis record: {report_id} (ID: {self.current_analysis_id})")
  642. # Save input files
  643. for input_type, file_path in inputs.items():
  644. if file_path:
  645. self.data_manager.save_input_file(self.current_analysis_id, input_type, file_path)
  646. # Navigate to Reports tab
  647. self.tab_widget.setCurrentIndex(5)
  648. # Store inputs in reports tab
  649. self.reports_tab.input_data = inputs
  650. self.reports_tab.current_analysis_id = self.current_analysis_id
  651. self.reports_tab.current_report_id = report_id # Store the actual report ID
  652. self.reports_tab.data_manager = self.data_manager
  653. # Reset results and tracking
  654. self.report_results = {}
  655. self.models_to_run = [] # Track which models we'll run
  656. # Determine which models to run based on inputs
  657. if inputs.get('dslr_side'):
  658. print("DSLR Side (Defect Model) detected...")
  659. self.models_to_run.append('defect')
  660. if inputs.get('dslr_top'):
  661. print("DSLR Top (Locule Model) detected...")
  662. self.models_to_run.append('locule')
  663. if inputs.get('multispectral'):
  664. print("Multispectral TIFF (Maturity Model) detected...")
  665. self.models_to_run.append('maturity')
  666. # Shape processing uses dslr_side if available AND model is loaded
  667. if inputs.get('dslr_side') and self.models.get('shape') and self.models['shape'].is_loaded:
  668. print("Shape Classification will be processed...")
  669. self.models_to_run.append('shape')
  670. # Audio ripeness processing uses audio file if available AND model is loaded
  671. if inputs.get('audio') and self.models.get('audio') and self.models['audio'].is_loaded:
  672. print("Audio Ripeness Classification will be processed...")
  673. self.models_to_run.append('audio')
  674. elif inputs.get('audio'):
  675. print("⚠️ Audio file provided but audio model not loaded!")
  676. # Start loading state if models to process
  677. if len(self.models_to_run) > 0:
  678. self.is_processing = True
  679. self.reports_tab.set_loading(True)
  680. else:
  681. # No models to run, just show the inputs
  682. self.reports_tab.generate_report(inputs)
  683. return
  684. # Process DSLR Side View with Defect Model
  685. if inputs.get('dslr_side'):
  686. worker = DefectWorker(inputs['dslr_side'], self.models['defect'])
  687. worker.signals.started.connect(lambda: self.on_worker_started("Defect Analysis"))
  688. worker.signals.result_ready.connect(self.on_defect_report_result)
  689. worker.signals.error.connect(lambda msg: self.on_worker_error_manual(msg, 'defect'))
  690. worker.signals.finished.connect(lambda: self.on_worker_finished())
  691. worker.signals.progress.connect(self.on_worker_progress)
  692. self.thread_pool.start(worker)
  693. # Process DSLR Top View with Locule Model
  694. if inputs.get('dslr_top'):
  695. worker = LoculeWorker(inputs['dslr_top'], self.models['locule'])
  696. worker.signals.started.connect(lambda: self.on_worker_started("Locule Analysis"))
  697. worker.signals.result_ready.connect(self.on_locule_report_result)
  698. worker.signals.error.connect(lambda msg: self.on_worker_error_manual(msg, 'locule'))
  699. worker.signals.finished.connect(lambda: self.on_worker_finished())
  700. worker.signals.progress.connect(self.on_worker_progress)
  701. self.thread_pool.start(worker)
  702. # Process Multispectral with Maturity Model
  703. if inputs.get('multispectral'):
  704. worker = MaturityWorker(inputs['multispectral'], self.models['maturity'])
  705. worker.signals.started.connect(lambda: self.on_worker_started("Maturity Classification"))
  706. worker.signals.result_ready.connect(self.on_maturity_report_result)
  707. worker.signals.error.connect(lambda msg: self.on_worker_error_manual(msg, 'maturity'))
  708. worker.signals.finished.connect(lambda: self.on_worker_finished())
  709. worker.signals.progress.connect(self.on_worker_progress)
  710. self.thread_pool.start(worker)
  711. # Process DSLR Side View with Shape Model (uses same image as defect)
  712. if inputs.get('dslr_side') and self.models.get('shape') and self.models['shape'].is_loaded:
  713. worker = ShapeWorker(inputs['dslr_side'], self.models['shape'])
  714. worker.signals.started.connect(lambda: self.on_worker_started("Shape Classification"))
  715. worker.signals.result_ready.connect(self.on_shape_report_result)
  716. worker.signals.error.connect(lambda msg: self.on_worker_error_manual(msg, 'shape'))
  717. worker.signals.finished.connect(lambda: self.on_worker_finished())
  718. worker.signals.progress.connect(self.on_worker_progress)
  719. self.thread_pool.start(worker)
  720. # Process Audio File with Audio Ripeness Model
  721. if inputs.get('audio') and self.models.get('audio') and self.models['audio'].is_loaded:
  722. print(f"Starting AudioWorker for: {inputs['audio']}")
  723. worker = AudioWorker(inputs['audio'], self.models['audio'])
  724. worker.signals.started.connect(lambda: self.on_worker_started("Audio Ripeness Classification"))
  725. worker.signals.result_ready.connect(self.on_audio_report_result)
  726. worker.signals.error.connect(lambda msg: self.on_worker_error_manual(msg, 'audio'))
  727. worker.signals.finished.connect(lambda: self.on_worker_finished())
  728. worker.signals.progress.connect(self.on_worker_progress)
  729. self.thread_pool.start(worker)
  730. def on_ripeness_clicked(self):
  731. """Handle ripeness classifier button click - switch to Ripeness tab."""
  732. # Switch to Ripeness tab (index 1)
  733. self.tab_widget.setCurrentIndex(1)
  734. # The tab will handle file loading via its own signal
  735. # Trigger file dialog immediately
  736. self.on_ripeness_load_audio()
  737. def on_ripeness_load_audio(self):
  738. """Handle audio file loading for ripeness classification."""
  739. if self.is_processing:
  740. QMessageBox.warning(self, "Processing", "Please wait for current processing to complete.")
  741. return
  742. # Open file dialog for audio
  743. # Use non-native dialog to avoid Windows shell freezing issues
  744. file_path, _ = QFileDialog.getOpenFileName(
  745. self,
  746. "Select Audio File",
  747. DEFAULT_DIRS['audio'],
  748. FILE_FILTERS['audio'],
  749. options=QFileDialog.DontUseNativeDialog
  750. )
  751. if not file_path:
  752. return
  753. print(f"Processing audio file: {file_path}")
  754. self.is_processing = True
  755. self.current_audio_file = file_path # Store for result handler
  756. # Set loading state on ripeness tab
  757. self.ripeness_tab.set_loading(True)
  758. # Create and start worker
  759. worker = AudioWorker(file_path, self.models['audio'])
  760. worker.signals.started.connect(lambda: self.on_worker_started("Audio Processing"))
  761. worker.signals.result_ready.connect(self.on_audio_result)
  762. worker.signals.error.connect(self.on_worker_error)
  763. worker.signals.finished.connect(lambda: self.on_worker_finished())
  764. worker.signals.progress.connect(self.on_worker_progress)
  765. self.thread_pool.start(worker)
  766. def on_quality_clicked(self):
  767. """Handle quality classifier button click - switch to Quality tab."""
  768. # Switch to Quality tab (index 2)
  769. self.tab_widget.setCurrentIndex(2)
  770. # Trigger file dialog via the control panel
  771. # Use QTimer to ensure the tab is fully visible before opening dialog
  772. if hasattr(self.quality_tab, 'control_panel'):
  773. QTimer.singleShot(100, self.quality_tab.control_panel._open_file_dialog)
  774. def on_quality_load_image(self):
  775. """Handle image file loading for quality classification."""
  776. if self.is_processing:
  777. QMessageBox.warning(self, "Processing", "Please wait for current processing to complete.")
  778. return
  779. # Open file dialog for image
  780. # Use non-native dialog to avoid Windows shell freezing issues
  781. file_path, _ = QFileDialog.getOpenFileName(
  782. self,
  783. "Select Image File",
  784. DEFAULT_DIRS['image'],
  785. FILE_FILTERS['image'],
  786. options=QFileDialog.DontUseNativeDialog
  787. )
  788. if not file_path:
  789. return
  790. print(f"Processing image file: {file_path}")
  791. self.is_processing = True
  792. self.current_image_file = file_path # Store for result handler
  793. # Set loading state on quality tab
  794. self.quality_tab.set_loading(True)
  795. # Create and start worker
  796. worker = DefectWorker(file_path, self.models['defect'])
  797. worker.signals.started.connect(lambda: self.on_worker_started("Defect Detection"))
  798. worker.signals.result_ready.connect(self.on_defect_result)
  799. worker.signals.error.connect(self.on_worker_error)
  800. worker.signals.finished.connect(lambda: self.on_worker_finished())
  801. worker.signals.progress.connect(self.on_worker_progress)
  802. self.thread_pool.start(worker)
  803. def on_maturity_load_tiff(self):
  804. """Handle TIFF file loading for maturity classification."""
  805. if self.is_processing:
  806. QMessageBox.warning(self, "Processing", "Please wait for current processing to complete.")
  807. return
  808. # Open file dialog for TIFF
  809. # Use non-native dialog to avoid Windows shell freezing issues
  810. file_path, _ = QFileDialog.getOpenFileName(
  811. self,
  812. "Select Multispectral TIFF File",
  813. DEFAULT_DIRS.get('image', str(Path.home())),
  814. FILE_FILTERS.get('tiff', "TIFF Files (*.tif *.tiff);;All Files (*.*)"),
  815. options=QFileDialog.DontUseNativeDialog
  816. )
  817. if not file_path:
  818. return
  819. print(f"Processing TIFF file: {file_path}")
  820. self.is_processing = True
  821. self.current_tiff_file = file_path # Store for result handler
  822. # Set loading state on maturity tab
  823. self.maturity_tab.set_loading(True)
  824. # Create and start worker
  825. worker = MaturityWorker(file_path, self.models['maturity'])
  826. worker.signals.started.connect(lambda: self.on_worker_started("Maturity Classification"))
  827. worker.signals.result_ready.connect(self.on_maturity_result)
  828. worker.signals.error.connect(self.on_worker_error)
  829. worker.signals.finished.connect(lambda: self.on_worker_finished())
  830. worker.signals.progress.connect(self.on_worker_progress)
  831. self.thread_pool.start(worker)
  832. def on_calibration_clicked(self):
  833. """Handle calibration button click."""
  834. QMessageBox.information(
  835. self,
  836. "System Calibration",
  837. "Calibration feature coming soon!\n\n"
  838. "This will allow you to:\n"
  839. "- Adjust detection thresholds\n"
  840. "- Fine-tune model parameters\n"
  841. "- Calibrate camera settings"
  842. )
  843. def on_batch_clicked(self):
  844. """Handle batch mode button click."""
  845. QMessageBox.information(
  846. self,
  847. "Batch Processing",
  848. "Batch mode coming soon!\n\n"
  849. "This will allow you to:\n"
  850. "- Process multiple files at once\n"
  851. "- Export results to CSV/Excel\n"
  852. "- Generate batch reports"
  853. )
  854. # ==================== Worker Signal Handlers ====================
  855. def on_worker_started(self, task_name: str):
  856. """Handle worker started signal."""
  857. print(f"{task_name} started")
  858. self.status_text.setText(f"Processing: {task_name}...")
  859. def on_worker_progress(self, percentage: int, message: str):
  860. """Handle worker progress signal."""
  861. print(f"Progress: {percentage}% - {message}")
  862. self.status_text.setText(f"{message} ({percentage}%)")
  863. def on_worker_finished(self):
  864. """Handle worker finished signal."""
  865. self.is_processing = False
  866. self.status_text.setText("Ready")
  867. print("Processing completed")
  868. def on_worker_error(self, error_msg: str):
  869. """Handle worker error signal."""
  870. self.is_processing = False
  871. QMessageBox.critical(self, "Processing Error", f"An error occurred:\n\n{error_msg}")
  872. print(f"Error: {error_msg}")
  873. def on_worker_error_manual(self, error_msg: str, model_name: str):
  874. """
  875. Handle worker error in manual input mode.
  876. Instead of crashing, record the error and continue with other models.
  877. """
  878. print(f"Error in {model_name} model: {error_msg}")
  879. # Mark this model as completed with no result (error case)
  880. self.report_results[model_name] = {
  881. 'error': True,
  882. 'error_msg': error_msg
  883. }
  884. # Check if all models have answered (success or error)
  885. self._check_all_reports_ready()
  886. def on_audio_result(self, waveform_image, spectrogram_image, class_name, confidence, probabilities, knock_count):
  887. """Handle audio processing result for Ripeness tab (standalone mode)."""
  888. print(f"Audio result: {class_name} ({confidence:.2%}) - {knock_count} knocks")
  889. # Update ripeness tab with spectrogram results
  890. self.ripeness_tab.update_results(
  891. spectrogram_image,
  892. class_name.capitalize() if class_name else "Unknown",
  893. probabilities,
  894. self.current_audio_file
  895. )
  896. # Map quality (for now, use confidence-based grading)
  897. if confidence >= 0.90:
  898. quality = "Grade A"
  899. elif confidence >= 0.75:
  900. quality = "Grade B"
  901. else:
  902. quality = "Grade C"
  903. # Add to results table
  904. self.results_panel.add_result(class_name.capitalize() if class_name else "Unknown", quality, confidence)
  905. def on_maturity_result(self, gradcam_image, class_name, confidence, probabilities):
  906. """Handle maturity processing result."""
  907. print(f"Maturity result: {class_name} ({confidence:.2f}%)")
  908. # Update maturity tab with results
  909. self.maturity_tab.update_results(
  910. gradcam_image,
  911. class_name,
  912. probabilities,
  913. self.current_tiff_file
  914. )
  915. # Add to results table (if available)
  916. # This can be extended later to add maturity results to dashboard
  917. def on_maturity_report_result(self, gradcam_image, class_name, confidence, probabilities):
  918. """Handle maturity processing result for Reports tab."""
  919. print(f"Maturity report result: {class_name} ({confidence:.2f}%)")
  920. self.report_results['maturity'] = {
  921. 'gradcam_image': gradcam_image,
  922. 'class_name': class_name,
  923. 'confidence': confidence,
  924. 'probabilities': probabilities
  925. }
  926. # Save result to database
  927. if self.current_analysis_id:
  928. # Convert confidence from percentage to 0-1 if needed
  929. conf_value = confidence / 100.0 if confidence > 1.0 else confidence
  930. self.data_manager.save_result(
  931. self.current_analysis_id,
  932. 'maturity',
  933. {
  934. 'predicted_class': class_name,
  935. 'confidence': conf_value,
  936. 'probabilities': probabilities,
  937. 'processing_time': 0.0,
  938. 'metadata': {}
  939. }
  940. )
  941. # Save Grad-CAM visualization
  942. if gradcam_image:
  943. self.data_manager.save_visualization(
  944. self.current_analysis_id,
  945. 'maturity_gradcam',
  946. gradcam_image,
  947. 'png'
  948. )
  949. self._check_all_reports_ready()
  950. def on_defect_report_result(self, annotated_image, primary_class, class_counts, total_detections):
  951. """Handle defect processing result for Reports tab (side view)."""
  952. print(f"Defect report result: {primary_class} ({total_detections} detections)")
  953. self.report_results['defect'] = {
  954. 'annotated_image': annotated_image,
  955. 'primary_class': primary_class,
  956. 'class_counts': class_counts,
  957. 'total_detections': total_detections
  958. }
  959. # Save result to database
  960. if self.current_analysis_id:
  961. metadata = {
  962. 'total_detections': total_detections,
  963. 'class_counts': class_counts
  964. }
  965. self.data_manager.save_result(
  966. self.current_analysis_id,
  967. 'defect',
  968. {
  969. 'predicted_class': primary_class,
  970. 'confidence': 0.85, # Default confidence
  971. 'probabilities': {},
  972. 'processing_time': 0.0,
  973. 'metadata': metadata
  974. }
  975. )
  976. # Save visualization
  977. if annotated_image:
  978. self.data_manager.save_visualization(
  979. self.current_analysis_id,
  980. 'defect_annotated',
  981. annotated_image,
  982. 'jpg'
  983. )
  984. self._check_all_reports_ready()
  985. def on_locule_report_result(self, annotated_image, locule_count):
  986. """Handle locule processing result for Reports tab (top view)."""
  987. print(f"Locule report result: {locule_count} locules detected")
  988. self.report_results['locule'] = {
  989. 'annotated_image': annotated_image,
  990. 'locule_count': locule_count
  991. }
  992. # Save result to database
  993. if self.current_analysis_id:
  994. metadata = {
  995. 'locule_count': locule_count
  996. }
  997. self.data_manager.save_result(
  998. self.current_analysis_id,
  999. 'locule',
  1000. {
  1001. 'predicted_class': f'{locule_count} locules',
  1002. 'confidence': 0.90, # Default confidence
  1003. 'probabilities': {},
  1004. 'processing_time': 0.0,
  1005. 'metadata': metadata
  1006. }
  1007. )
  1008. # Save visualization
  1009. if annotated_image:
  1010. self.data_manager.save_visualization(
  1011. self.current_analysis_id,
  1012. 'locule_annotated',
  1013. annotated_image,
  1014. 'jpg'
  1015. )
  1016. self._check_all_reports_ready()
  1017. def on_shape_report_result(self, annotated_image, shape_class, class_id, confidence):
  1018. """Handle shape classification result for Reports tab."""
  1019. print(f"Shape report result: {shape_class} (confidence: {confidence:.3f})")
  1020. self.report_results['shape'] = {
  1021. 'annotated_image': annotated_image,
  1022. 'shape_class': shape_class,
  1023. 'class_id': class_id,
  1024. 'confidence': confidence
  1025. }
  1026. # Save result to database
  1027. if self.current_analysis_id:
  1028. self.data_manager.save_result(
  1029. self.current_analysis_id,
  1030. 'shape',
  1031. {
  1032. 'predicted_class': shape_class,
  1033. 'confidence': confidence,
  1034. 'probabilities': {},
  1035. 'processing_time': 0.0,
  1036. 'metadata': {'class_id': class_id}
  1037. }
  1038. )
  1039. # Save visualization
  1040. if annotated_image:
  1041. self.data_manager.save_visualization(
  1042. self.current_analysis_id,
  1043. 'shape_annotated',
  1044. annotated_image,
  1045. 'jpg'
  1046. )
  1047. self._check_all_reports_ready()
  1048. def on_audio_report_result(self, waveform_image, spectrogram_image, ripeness_class, confidence, probabilities, knock_count):
  1049. """Handle audio ripeness classification result for Reports tab."""
  1050. print(f"✓ Audio report result: {ripeness_class} ({confidence:.2%}) - {knock_count} knocks")
  1051. print(f" Waveform image: {type(waveform_image)} size={waveform_image.size() if hasattr(waveform_image, 'size') else 'N/A'}")
  1052. print(f" Spectrogram image: {type(spectrogram_image)} size={spectrogram_image.size() if hasattr(spectrogram_image, 'size') else 'N/A'}")
  1053. print(f" Probabilities: {probabilities}")
  1054. self.report_results['audio'] = {
  1055. 'waveform_image': waveform_image,
  1056. 'spectrogram_image': spectrogram_image,
  1057. 'ripeness_class': ripeness_class,
  1058. 'confidence': confidence,
  1059. 'probabilities': probabilities,
  1060. 'knock_count': knock_count
  1061. }
  1062. # Save result to database
  1063. if self.current_analysis_id:
  1064. # Convert confidence from percentage to 0-1 if needed
  1065. conf_value = confidence / 100.0 if confidence > 1.0 else confidence
  1066. metadata = {
  1067. 'knock_count': knock_count
  1068. }
  1069. self.data_manager.save_result(
  1070. self.current_analysis_id,
  1071. 'audio',
  1072. {
  1073. 'predicted_class': ripeness_class,
  1074. 'confidence': conf_value,
  1075. 'probabilities': probabilities,
  1076. 'processing_time': 0.0,
  1077. 'metadata': metadata
  1078. }
  1079. )
  1080. # Save waveform visualization
  1081. if waveform_image:
  1082. self.data_manager.save_visualization(
  1083. self.current_analysis_id,
  1084. 'audio_waveform',
  1085. waveform_image,
  1086. 'png'
  1087. )
  1088. # Save spectrogram visualization
  1089. if spectrogram_image:
  1090. self.data_manager.save_visualization(
  1091. self.current_analysis_id,
  1092. 'audio_spectrogram',
  1093. spectrogram_image,
  1094. 'png'
  1095. )
  1096. self._check_all_reports_ready()
  1097. def _check_all_reports_ready(self):
  1098. """
  1099. Check if all pending reports are ready (success or error),
  1100. then generate combined report with available data.
  1101. """
  1102. if not hasattr(self, 'models_to_run'):
  1103. return # Safety check
  1104. # Check if all models have reported (success or error)
  1105. models_answered = set(self.report_results.keys())
  1106. models_expected = set(self.models_to_run)
  1107. print(f"Models to run: {models_expected}")
  1108. print(f"Models answered: {models_answered}")
  1109. if models_answered >= models_expected and len(models_expected) > 0:
  1110. # All models have answered (success or failure)
  1111. print("All models answered - generating report with available data...")
  1112. self.reports_tab.generate_report_with_rgb_and_multispectral(
  1113. self.reports_tab.input_data,
  1114. self.report_results
  1115. )
  1116. # Finalize analysis in database
  1117. if self.current_analysis_id and hasattr(self.reports_tab, 'current_overall_grade'):
  1118. grade = getattr(self.reports_tab, 'current_overall_grade', 'B')
  1119. description = getattr(self.reports_tab, 'current_grade_description', '')
  1120. if self.analysis_start_time:
  1121. total_time = (datetime.now() - self.analysis_start_time).total_seconds()
  1122. else:
  1123. total_time = 0.0
  1124. self.data_manager.finalize_analysis(
  1125. self.current_analysis_id,
  1126. grade,
  1127. description,
  1128. total_time
  1129. )
  1130. print(f"Finalized analysis with grade {grade}")
  1131. # Always refresh recent results panel to show new analysis (after report is generated)
  1132. self.results_panel.refresh_from_database()
  1133. # Update system info panel with new statistics
  1134. self.update_system_info_panel()
  1135. self.reports_tab.set_loading(False)
  1136. self.is_processing = False
  1137. def on_defect_result(self, annotated_image, primary_class, class_counts, total_detections):
  1138. """Handle defect detection result."""
  1139. print(f"Defect result: {primary_class} ({total_detections} detections)")
  1140. # Update quality tab with results
  1141. self.quality_tab.update_results(
  1142. annotated_image,
  1143. primary_class,
  1144. class_counts,
  1145. total_detections,
  1146. self.current_image_file
  1147. )
  1148. # Map to quality grade
  1149. if primary_class == "No Defects":
  1150. quality = "Grade A"
  1151. confidence = 95.0
  1152. elif primary_class == "Minor Defects":
  1153. quality = "Grade B"
  1154. confidence = 80.0
  1155. else: # Reject
  1156. quality = "Grade C"
  1157. confidence = 70.0
  1158. # Add to results table (use "N/A" for ripeness since it's quality-only)
  1159. self.results_panel.add_result("N/A", quality, confidence)
  1160. # ==================== Menu Handlers ====================
  1161. def show_about(self):
  1162. """Show the about dialog."""
  1163. dialog = AboutDialog(self)
  1164. dialog.exec_()
  1165. def show_help(self):
  1166. """Show the help dialog."""
  1167. dialog = HelpDialog(self)
  1168. dialog.exec_()
  1169. def on_go_to_dashboard(self):
  1170. """Handle request to go back to dashboard from reports tab."""
  1171. self.tab_widget.setCurrentIndex(0) # Switch to Dashboard (index 0)
  1172. def on_view_analysis(self, report_id: str):
  1173. """
  1174. Handle view analysis request from recent results panel.
  1175. Args:
  1176. report_id: The report ID to load and display
  1177. """
  1178. # Validate report_id
  1179. if not report_id or report_id == 'N/A':
  1180. QMessageBox.warning(self, "Error", f"Invalid report ID: {report_id}")
  1181. return
  1182. # Ensure data manager is set
  1183. if not self.data_manager:
  1184. QMessageBox.warning(self, "Error", "Data manager not initialized")
  1185. return
  1186. self.reports_tab.data_manager = self.data_manager
  1187. # Switch to Reports tab (index 5)
  1188. self.tab_widget.setCurrentIndex(5)
  1189. # Load the analysis from database
  1190. success = self.reports_tab.load_analysis_from_db(report_id)
  1191. if not success:
  1192. QMessageBox.warning(self, "Error", f"Could not load analysis: {report_id}")
  1193. def closeEvent(self, event):
  1194. """Handle window close event."""
  1195. # Wait for all threads to finish
  1196. if self.thread_pool.activeThreadCount() > 0:
  1197. reply = QMessageBox.question(
  1198. self,
  1199. "Processing in Progress",
  1200. "Processing is still running. Are you sure you want to exit?",
  1201. QMessageBox.Yes | QMessageBox.No,
  1202. QMessageBox.No
  1203. )
  1204. if reply == QMessageBox.No:
  1205. event.ignore()
  1206. return
  1207. print("Application closing...")
  1208. self.thread_pool.waitForDone(3000) # Wait up to 3 seconds
  1209. event.accept()