report_sections.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. """
  2. Report Section Builders
  3. Functions for building individual report sections (info, results, visualizations, etc).
  4. Each function returns a QGroupBox or QWidget ready to be added to the report layout.
  5. """
  6. from datetime import datetime
  7. from typing import Dict, Optional
  8. from PyQt5.QtWidgets import QGroupBox, QVBoxLayout, QGridLayout, QLabel, QWidget, QHBoxLayout
  9. from PyQt5.QtCore import Qt
  10. from PyQt5.QtGui import QFont
  11. from resources.styles import GROUP_BOX_STYLE
  12. from utils.grade_calculator import (
  13. calculate_durian_grade,
  14. get_ripeness_color,
  15. get_maturity_color,
  16. get_grade_color
  17. )
  18. from ui.components.report_visualizations import create_clickable_image_widget
  19. def get_local_datetime_string() -> str:
  20. """Get current datetime string formatted in the computer's local timezone."""
  21. return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  22. def create_report_info_section(report_id: Optional[str] = None) -> QGroupBox:
  23. """
  24. Create report information section with local timezone.
  25. Args:
  26. report_id: Report ID to display (or None to generate one)
  27. Returns:
  28. QGroupBox containing report information
  29. """
  30. group = QGroupBox("Report Information")
  31. group.setStyleSheet(GROUP_BOX_STYLE)
  32. layout = QGridLayout()
  33. # Report ID (use provided ID or generate one)
  34. layout.addWidget(QLabel("<b>Report ID:</b>"), 0, 0)
  35. if report_id:
  36. report_id_label = QLabel(report_id)
  37. else:
  38. report_id_label = QLabel(f"DUR-{datetime.now().strftime('%Y%m%d-%H%M%S')}")
  39. layout.addWidget(report_id_label, 0, 1)
  40. # Date/Time in local timezone
  41. layout.addWidget(QLabel("<b>Generated:</b>"), 0, 2)
  42. layout.addWidget(QLabel(get_local_datetime_string()), 0, 3)
  43. group.setLayout(layout)
  44. return group
  45. def create_empty_results_section() -> QGroupBox:
  46. """
  47. Create analysis results section - shows message when no data available.
  48. Returns:
  49. QGroupBox with placeholder message
  50. """
  51. group = QGroupBox("Analysis Results")
  52. group.setStyleSheet(GROUP_BOX_STYLE + """
  53. QGroupBox {
  54. background-color: #ecf0f1;
  55. font-weight: bold;
  56. font-size: 14px;
  57. }
  58. """)
  59. layout = QVBoxLayout()
  60. # Display message when no analysis data is available
  61. message_label = QLabel("Not enough information")
  62. message_label.setFont(QFont("Arial", 14))
  63. message_label.setAlignment(Qt.AlignCenter)
  64. message_label.setStyleSheet("color: #7f8c8d; padding: 20px;")
  65. layout.addWidget(message_label)
  66. group.setLayout(layout)
  67. return group
  68. def create_model_results_section(
  69. predicted_class: str,
  70. confidence: float,
  71. probabilities: Dict[str, float]
  72. ) -> QGroupBox:
  73. """
  74. Create analysis results section with actual model predictions.
  75. Args:
  76. predicted_class: Predicted maturity class from model
  77. confidence: Model confidence (0-1 scale)
  78. probabilities: Dictionary of class probabilities
  79. Returns:
  80. QGroupBox containing model results
  81. """
  82. group = QGroupBox("Analysis Results")
  83. group.setStyleSheet(GROUP_BOX_STYLE + """
  84. QGroupBox {
  85. background-color: #ecf0f1;
  86. font-weight: bold;
  87. font-size: 14px;
  88. }
  89. """)
  90. layout = QVBoxLayout()
  91. # Determine overall grade based on maturity class
  92. grade_map = {
  93. 'Immature': 'C',
  94. 'Mature': 'A',
  95. 'Overmature': 'B'
  96. }
  97. grade = grade_map.get(predicted_class, 'B')
  98. # Overall Grade
  99. grade_layout = QHBoxLayout()
  100. grade_label = QLabel("Overall Grade:")
  101. grade_label.setFont(QFont("Arial", 16, QFont.Bold))
  102. grade_layout.addWidget(grade_label)
  103. grade_value = QLabel(f"Class {grade}")
  104. grade_value.setFont(QFont("Arial", 24, QFont.Bold))
  105. grade_color = get_grade_color(grade)
  106. grade_value.setStyleSheet(f"color: {grade_color};")
  107. grade_layout.addWidget(grade_value)
  108. grade_layout.addStretch()
  109. layout.addLayout(grade_layout)
  110. # Results grid
  111. results_grid = QGridLayout()
  112. # Maturity (from model)
  113. results_grid.addWidget(QLabel("<b>Maturity Status:</b>"), 0, 0)
  114. maturity_label = QLabel(predicted_class)
  115. maturity_label.setStyleSheet(f"font-size: 14px; color: {get_maturity_color(predicted_class)};")
  116. results_grid.addWidget(maturity_label, 0, 1)
  117. # Confidence (from model)
  118. confidence_pct = confidence * 100 if confidence <= 1.0 else confidence
  119. results_grid.addWidget(QLabel(f"({confidence_pct:.1f}% confidence)"), 0, 2)
  120. # All class probabilities
  121. results_grid.addWidget(QLabel("<b>Class Probabilities:</b>"), 1, 0)
  122. prob_text = " ".join([f"{k}: {v:.1%}" for k, v in probabilities.items()])
  123. prob_label = QLabel(prob_text)
  124. prob_label.setStyleSheet("font-size: 11px; color: #666;")
  125. results_grid.addWidget(prob_label, 1, 1, 1, 2)
  126. layout.addLayout(results_grid)
  127. group.setLayout(layout)
  128. return group
  129. def create_analysis_results_section(results: Dict) -> QGroupBox:
  130. """
  131. Create combined analysis results from all models.
  132. Args:
  133. results: Dictionary with processing results from all models
  134. Keys: 'defect', 'locule', 'maturity', 'shape', 'audio'
  135. Returns:
  136. QGroupBox containing combined analysis results
  137. """
  138. group = QGroupBox("Combined Analysis Results")
  139. group.setStyleSheet(GROUP_BOX_STYLE + """
  140. QGroupBox {
  141. background-color: #ecf0f1;
  142. font-weight: bold;
  143. font-size: 14px;
  144. }
  145. """)
  146. layout = QVBoxLayout()
  147. # Extract data
  148. locule_count = 0
  149. has_defects = False
  150. shape_class = None
  151. maturity_class = None
  152. # Locule analysis from top view
  153. if 'locule' in results:
  154. if results['locule'].get('error'):
  155. layout.addWidget(QLabel(f"<b>Locule Count (Top View):</b> <font color='#e74c3c'>Error: {results['locule'].get('error_msg', 'Unknown error')}</font>"))
  156. else:
  157. locule_count = results['locule'].get('locule_count', 0)
  158. layout.addWidget(QLabel(f"<b>Locule Count (Top View):</b> {locule_count} locules"))
  159. # Defect analysis from side view
  160. if 'defect' in results:
  161. if results['defect'].get('error'):
  162. layout.addWidget(QLabel(f"<b>Defect Status (Side View):</b> <font color='#e74c3c'>Error: {results['defect'].get('error_msg', 'Unknown error')}</font>"))
  163. else:
  164. primary_class = results['defect'].get('primary_class', 'Unknown')
  165. total_detections = results['defect'].get('total_detections', 0)
  166. has_defects = (primary_class != "No Defects")
  167. layout.addWidget(QLabel(f"<b>Defect Status (Side View):</b> {primary_class} ({total_detections} detections)"))
  168. # Shape analysis from side view
  169. if 'shape' in results:
  170. if results['shape'].get('error'):
  171. layout.addWidget(QLabel(f"<b>Shape Classification (Side View):</b> <font color='#e74c3c'>Error: {results['shape'].get('error_msg', 'Unknown error')}</font>"))
  172. else:
  173. shape_class = results['shape'].get('shape_class', 'Unknown')
  174. confidence = results['shape'].get('confidence', 0)
  175. layout.addWidget(QLabel(f"<b>Shape Classification (Side View):</b> {shape_class} ({confidence*100:.1f}%)"))
  176. # Maturity analysis from multispectral (PART OF QUALITY GRADE)
  177. if 'maturity' in results:
  178. if results['maturity'].get('error'):
  179. layout.addWidget(QLabel(f"<b>Maturity (Multispectral):</b> <font color='#e74c3c'>Error: {results['maturity'].get('error_msg', 'Unknown error')}</font>"))
  180. else:
  181. maturity_class = results['maturity'].get('class_name', 'Unknown')
  182. confidence = results['maturity'].get('confidence', 0)
  183. layout.addWidget(QLabel(f"<b>Maturity (Multispectral):</b> {maturity_class} ({confidence*100:.1f}%)"))
  184. # Audio ripeness analysis (SEPARATE FROM QUALITY GRADE)
  185. ripeness_class = None
  186. if 'audio' in results:
  187. if results['audio'].get('error'):
  188. layout.addWidget(QLabel(f"<b>Ripeness Classification (Audio):</b> <font color='#e74c3c'>Error: {results['audio'].get('error_msg', 'Unknown error')}</font>"))
  189. else:
  190. ripeness_class = results['audio'].get('ripeness_class', 'Unknown')
  191. confidence = results['audio'].get('confidence', 0)
  192. knock_count = results['audio'].get('knock_count', 0)
  193. # Main result
  194. layout.addWidget(QLabel(f"<b>Ripeness Classification (Audio):</b> {ripeness_class} ({confidence*100:.1f}%)"))
  195. # Detailed analysis
  196. per_knock_preds = results['audio'].get('per_knock_predictions', [])
  197. if per_knock_preds:
  198. # Per-knock predictions
  199. per_knock_classes = [p['class'].capitalize() for p in per_knock_preds]
  200. per_knock_confidences = [p['confidence'] for p in per_knock_preds]
  201. layout.addWidget(QLabel(f"<i>Per-knock predictions: {', '.join(per_knock_classes)}</i>"))
  202. # Per-knock confidences
  203. per_knock_conf_str = ', '.join([f"{c*100:.1f}%" for c in per_knock_confidences])
  204. layout.addWidget(QLabel(f"<i>Per-knock confidence: {per_knock_conf_str}</i>"))
  205. # Average probabilities
  206. probabilities = results['audio'].get('probabilities', {})
  207. if probabilities:
  208. prob_str = ', '.join([f"{k}: {v*100:.1f}%" for k, v in probabilities.items()])
  209. layout.addWidget(QLabel(f"<i>Average probabilities: {prob_str}</i>"))
  210. # Calculate and display grade (includes maturity, excludes ripeness)
  211. grade, grade_description = calculate_durian_grade(locule_count, has_defects, shape_class, maturity_class)
  212. # Note: Ripeness is displayed separately and NOT included in quality grade
  213. grade_color = get_grade_color(grade)
  214. grade_widget = QLabel(f"<b>Overall Grade:</b> <font color='{grade_color}' size='5'><b>Class {grade}</b></font>")
  215. grade_widget.setStyleSheet(f"padding: 10px; background-color: white; border: 2px solid {grade_color};")
  216. layout.addWidget(grade_widget)
  217. layout.addWidget(QLabel(f"<i>{grade_description}</i>"))
  218. group.setLayout(layout)
  219. return group
  220. def create_input_data_section(input_data: Dict[str, str]) -> QGroupBox:
  221. """
  222. Create section showing input data (only for provided inputs).
  223. Args:
  224. input_data: Dictionary with input file paths
  225. Returns:
  226. QGroupBox containing input data previews
  227. """
  228. group = QGroupBox("Input Data")
  229. group.setStyleSheet(GROUP_BOX_STYLE)
  230. layout = QVBoxLayout()
  231. has_content = False
  232. # Side View (Plain DSLR)
  233. if input_data.get('dslr_side'):
  234. dslr_side_widget = create_image_preview_widget("DSLR Side View", input_data['dslr_side'])
  235. layout.addWidget(dslr_side_widget)
  236. has_content = True
  237. # Top View (RGB DSLR)
  238. if input_data.get('dslr_top'):
  239. dslr_top_widget = create_image_preview_widget("DSLR Top View (RGB)", input_data['dslr_top'])
  240. layout.addWidget(dslr_top_widget)
  241. has_content = True
  242. # Thermal data (if provided)
  243. if input_data.get('thermal'):
  244. from ui.components.report_visualizations import create_thermal_widget
  245. thermal_widget = create_thermal_widget(input_data['thermal'])
  246. layout.addWidget(thermal_widget)
  247. has_content = True
  248. if not has_content:
  249. layout.addWidget(QLabel("No input data provided"))
  250. group.setLayout(layout)
  251. return group
  252. def create_input_data_with_gradcam(input_data: Dict[str, str], gradcam_image) -> QGroupBox:
  253. """
  254. Create section showing input data with Grad-CAM visualization.
  255. Args:
  256. input_data: Dictionary with input file paths
  257. gradcam_image: QImage of Grad-CAM visualization
  258. Returns:
  259. QGroupBox containing input data and Grad-CAM
  260. """
  261. from ui.components.report_visualizations import create_gradcam_widget
  262. group = QGroupBox("Input Data & Analysis Visualization")
  263. group.setStyleSheet(GROUP_BOX_STYLE)
  264. layout = QVBoxLayout()
  265. # Grad-CAM visualization (from multispectral model)
  266. if gradcam_image:
  267. gradcam_widget = create_gradcam_widget(gradcam_image)
  268. layout.addWidget(gradcam_widget)
  269. # Side View (Plain DSLR)
  270. if input_data.get('dslr_side'):
  271. dslr_side_widget = create_image_preview_widget("DSLR Side View", input_data['dslr_side'])
  272. layout.addWidget(dslr_side_widget)
  273. # Top View (RGB DSLR)
  274. if input_data.get('dslr_top'):
  275. dslr_top_widget = create_image_preview_widget("DSLR Top View (RGB)", input_data['dslr_top'])
  276. layout.addWidget(dslr_top_widget)
  277. # Thermal data (if provided)
  278. if input_data.get('thermal'):
  279. from ui.components.report_visualizations import create_thermal_widget
  280. thermal_widget = create_thermal_widget(input_data['thermal'])
  281. layout.addWidget(thermal_widget)
  282. if not gradcam_image and not any([
  283. input_data.get('dslr_side'),
  284. input_data.get('dslr_top'),
  285. input_data.get('thermal')
  286. ]):
  287. layout.addWidget(QLabel("No visualizations to display"))
  288. group.setLayout(layout)
  289. return group
  290. def create_visualizations_section(input_data: Dict[str, str], results: Dict) -> QGroupBox:
  291. """
  292. Create input data section with all model visualizations and clickable images.
  293. Args:
  294. input_data: Dictionary with input file paths
  295. results: Dictionary with processing results from all models
  296. Returns:
  297. QGroupBox containing all visualizations
  298. """
  299. group = QGroupBox("Analysis Visualizations")
  300. group.setStyleSheet(GROUP_BOX_STYLE)
  301. layout = QVBoxLayout()
  302. # Grad-CAM from multispectral
  303. if results.get('maturity') and results['maturity'].get('gradcam_image'):
  304. gradcam_img = results['maturity']['gradcam_image']
  305. gradcam_widget = create_clickable_image_widget(
  306. gradcam_img,
  307. "Grad-CAM Visualization (Maturity)",
  308. "Model attention heatmap overlaid on 860nm NIR band",
  309. max_width=500
  310. )
  311. layout.addWidget(gradcam_widget)
  312. # Locule analysis image
  313. if results.get('locule') and results['locule'].get('annotated_image'):
  314. locule_img = results['locule']['annotated_image']
  315. locule_widget = create_clickable_image_widget(
  316. locule_img,
  317. "Locule Analysis (Top View)",
  318. "Detected and counted locules with color coding",
  319. max_width=500
  320. )
  321. layout.addWidget(locule_widget)
  322. # Defect analysis image
  323. if results.get('defect') and results['defect'].get('annotated_image'):
  324. defect_img = results['defect']['annotated_image']
  325. defect_widget = create_clickable_image_widget(
  326. defect_img,
  327. "Defect Analysis (Side View)",
  328. f"{results['defect'].get('primary_class', 'Unknown')} - {results['defect'].get('total_detections', 0)} detections",
  329. max_width=500
  330. )
  331. layout.addWidget(defect_widget)
  332. # Shape analysis image
  333. if results.get('shape') and results['shape'].get('annotated_image'):
  334. shape_img = results['shape']['annotated_image']
  335. confidence = results['shape'].get('confidence', 0) * 100
  336. shape_widget = create_clickable_image_widget(
  337. shape_img,
  338. "Shape Classification (Side View)",
  339. f"{results['shape'].get('shape_class', 'Unknown')} ({confidence:.1f}% confidence)",
  340. max_width=500
  341. )
  342. layout.addWidget(shape_widget)
  343. # Audio ripeness - Waveform with knocks
  344. if results.get('audio') and results['audio'].get('waveform_image'):
  345. waveform_img = results['audio']['waveform_image']
  346. knock_count = results['audio'].get('knock_count', 0)
  347. waveform_widget = create_clickable_image_widget(
  348. waveform_img,
  349. f"Waveform with {knock_count} Detected Knocks",
  350. "Audio waveform with knock detection markers",
  351. max_width=600
  352. )
  353. layout.addWidget(waveform_widget)
  354. # Audio ripeness - Mel Spectrogram with knocks
  355. if results.get('audio') and results['audio'].get('spectrogram_image'):
  356. spectrogram_img = results['audio']['spectrogram_image']
  357. knock_count = results['audio'].get('knock_count', 0)
  358. audio_widget = create_clickable_image_widget(
  359. spectrogram_img,
  360. f"Mel Spectrogram (64 Coefficients) - {knock_count} Knocks",
  361. "Frequency-time representation of knock sounds",
  362. max_width=600
  363. )
  364. layout.addWidget(audio_widget)
  365. # Thermal if provided
  366. if input_data.get('thermal'):
  367. from ui.components.report_visualizations import create_thermal_widget
  368. thermal_widget = create_thermal_widget(input_data['thermal'])
  369. layout.addWidget(thermal_widget)
  370. group.setLayout(layout)
  371. return group
  372. def create_image_preview_widget(title: str, file_path: str) -> QWidget:
  373. """
  374. Create image preview widget from file path.
  375. Args:
  376. title: Title for the image
  377. file_path: Path to the image file
  378. Returns:
  379. QWidget containing the image preview
  380. """
  381. from ui.components.report_visualizations import create_image_preview_widget as _create_image
  382. return _create_image(title, file_path)