"""
Report Section Builders
Functions for building individual report sections (info, results, visualizations, etc).
Each function returns a QGroupBox or QWidget ready to be added to the report layout.
"""
from datetime import datetime
from typing import Dict, Optional
from PyQt5.QtWidgets import QGroupBox, QVBoxLayout, QGridLayout, QLabel, QWidget, QHBoxLayout
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
from resources.styles import GROUP_BOX_STYLE
from utils.grade_calculator import (
calculate_durian_grade,
get_ripeness_color,
get_maturity_color,
get_grade_color
)
from ui.components.report_visualizations import create_clickable_image_widget
def get_local_datetime_string() -> str:
"""Get current datetime string formatted in the computer's local timezone."""
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def create_report_info_section(report_id: Optional[str] = None) -> QGroupBox:
"""
Create report information section with local timezone.
Args:
report_id: Report ID to display (or None to generate one)
Returns:
QGroupBox containing report information
"""
group = QGroupBox("Report Information")
group.setStyleSheet(GROUP_BOX_STYLE)
layout = QGridLayout()
# Report ID (use provided ID or generate one)
layout.addWidget(QLabel("Report ID:"), 0, 0)
if report_id:
report_id_label = QLabel(report_id)
else:
report_id_label = QLabel(f"DUR-{datetime.now().strftime('%Y%m%d-%H%M%S')}")
layout.addWidget(report_id_label, 0, 1)
# Date/Time in local timezone
layout.addWidget(QLabel("Generated:"), 0, 2)
layout.addWidget(QLabel(get_local_datetime_string()), 0, 3)
group.setLayout(layout)
return group
def create_empty_results_section() -> QGroupBox:
"""
Create analysis results section - shows message when no data available.
Returns:
QGroupBox with placeholder message
"""
group = QGroupBox("Analysis Results")
group.setStyleSheet(GROUP_BOX_STYLE + """
QGroupBox {
background-color: #ecf0f1;
font-weight: bold;
font-size: 14px;
}
""")
layout = QVBoxLayout()
# Display message when no analysis data is available
message_label = QLabel("Not enough information")
message_label.setFont(QFont("Arial", 14))
message_label.setAlignment(Qt.AlignCenter)
message_label.setStyleSheet("color: #7f8c8d; padding: 20px;")
layout.addWidget(message_label)
group.setLayout(layout)
return group
def create_model_results_section(
predicted_class: str,
confidence: float,
probabilities: Dict[str, float]
) -> QGroupBox:
"""
Create analysis results section with actual model predictions.
Args:
predicted_class: Predicted maturity class from model
confidence: Model confidence (0-1 scale)
probabilities: Dictionary of class probabilities
Returns:
QGroupBox containing model results
"""
group = QGroupBox("Analysis Results")
group.setStyleSheet(GROUP_BOX_STYLE + """
QGroupBox {
background-color: #ecf0f1;
font-weight: bold;
font-size: 14px;
}
""")
layout = QVBoxLayout()
# Determine overall grade based on maturity class
grade_map = {
'Immature': 'C',
'Mature': 'A',
'Overmature': 'B'
}
grade = grade_map.get(predicted_class, 'B')
# Overall Grade
grade_layout = QHBoxLayout()
grade_label = QLabel("Overall Grade:")
grade_label.setFont(QFont("Arial", 16, QFont.Bold))
grade_layout.addWidget(grade_label)
grade_value = QLabel(f"Class {grade}")
grade_value.setFont(QFont("Arial", 24, QFont.Bold))
grade_color = get_grade_color(grade)
grade_value.setStyleSheet(f"color: {grade_color};")
grade_layout.addWidget(grade_value)
grade_layout.addStretch()
layout.addLayout(grade_layout)
# Results grid
results_grid = QGridLayout()
# Maturity (from model)
results_grid.addWidget(QLabel("Maturity Status:"), 0, 0)
maturity_label = QLabel(predicted_class)
maturity_label.setStyleSheet(f"font-size: 14px; color: {get_maturity_color(predicted_class)};")
results_grid.addWidget(maturity_label, 0, 1)
# Confidence (from model)
confidence_pct = confidence * 100 if confidence <= 1.0 else confidence
results_grid.addWidget(QLabel(f"({confidence_pct:.1f}% confidence)"), 0, 2)
# All class probabilities
results_grid.addWidget(QLabel("Class Probabilities:"), 1, 0)
prob_text = " ".join([f"{k}: {v:.1%}" for k, v in probabilities.items()])
prob_label = QLabel(prob_text)
prob_label.setStyleSheet("font-size: 11px; color: #666;")
results_grid.addWidget(prob_label, 1, 1, 1, 2)
layout.addLayout(results_grid)
group.setLayout(layout)
return group
def create_analysis_results_section(results: Dict) -> QGroupBox:
"""
Create combined analysis results from all models.
Args:
results: Dictionary with processing results from all models
Keys: 'defect', 'locule', 'maturity', 'shape', 'audio'
Returns:
QGroupBox containing combined analysis results
"""
group = QGroupBox("Combined Analysis Results")
group.setStyleSheet(GROUP_BOX_STYLE + """
QGroupBox {
background-color: #ecf0f1;
font-weight: bold;
font-size: 14px;
}
""")
layout = QVBoxLayout()
# Extract data
locule_count = 0
has_defects = False
shape_class = None
maturity_class = None
# Locule analysis from top view
if 'locule' in results:
if results['locule'].get('error'):
layout.addWidget(QLabel(f"Locule Count (Top View): Error: {results['locule'].get('error_msg', 'Unknown error')}"))
else:
locule_count = results['locule'].get('locule_count', 0)
layout.addWidget(QLabel(f"Locule Count (Top View): {locule_count} locules"))
# Defect analysis from side view
if 'defect' in results:
if results['defect'].get('error'):
layout.addWidget(QLabel(f"Defect Status (Side View): Error: {results['defect'].get('error_msg', 'Unknown error')}"))
else:
primary_class = results['defect'].get('primary_class', 'Unknown')
total_detections = results['defect'].get('total_detections', 0)
has_defects = (primary_class != "No Defects")
layout.addWidget(QLabel(f"Defect Status (Side View): {primary_class} ({total_detections} detections)"))
# Shape analysis from side view
if 'shape' in results:
if results['shape'].get('error'):
layout.addWidget(QLabel(f"Shape Classification (Side View): Error: {results['shape'].get('error_msg', 'Unknown error')}"))
else:
shape_class = results['shape'].get('shape_class', 'Unknown')
confidence = results['shape'].get('confidence', 0)
layout.addWidget(QLabel(f"Shape Classification (Side View): {shape_class} ({confidence*100:.1f}%)"))
# Maturity analysis from multispectral (PART OF QUALITY GRADE)
if 'maturity' in results:
if results['maturity'].get('error'):
layout.addWidget(QLabel(f"Maturity (Multispectral): Error: {results['maturity'].get('error_msg', 'Unknown error')}"))
else:
maturity_class = results['maturity'].get('class_name', 'Unknown')
confidence = results['maturity'].get('confidence', 0)
layout.addWidget(QLabel(f"Maturity (Multispectral): {maturity_class} ({confidence*100:.1f}%)"))
# Audio ripeness analysis (SEPARATE FROM QUALITY GRADE)
ripeness_class = None
if 'audio' in results:
if results['audio'].get('error'):
layout.addWidget(QLabel(f"Ripeness Classification (Audio): Error: {results['audio'].get('error_msg', 'Unknown error')}"))
else:
ripeness_class = results['audio'].get('ripeness_class', 'Unknown')
confidence = results['audio'].get('confidence', 0)
knock_count = results['audio'].get('knock_count', 0)
# Main result
layout.addWidget(QLabel(f"Ripeness Classification (Audio): {ripeness_class} ({confidence*100:.1f}%)"))
# Detailed analysis
per_knock_preds = results['audio'].get('per_knock_predictions', [])
if per_knock_preds:
# Per-knock predictions
per_knock_classes = [p['class'].capitalize() for p in per_knock_preds]
per_knock_confidences = [p['confidence'] for p in per_knock_preds]
layout.addWidget(QLabel(f"Per-knock predictions: {', '.join(per_knock_classes)}"))
# Per-knock confidences
per_knock_conf_str = ', '.join([f"{c*100:.1f}%" for c in per_knock_confidences])
layout.addWidget(QLabel(f"Per-knock confidence: {per_knock_conf_str}"))
# Average probabilities
probabilities = results['audio'].get('probabilities', {})
if probabilities:
prob_str = ', '.join([f"{k}: {v*100:.1f}%" for k, v in probabilities.items()])
layout.addWidget(QLabel(f"Average probabilities: {prob_str}"))
# Calculate and display grade (includes maturity, excludes ripeness)
grade, grade_description = calculate_durian_grade(locule_count, has_defects, shape_class, maturity_class)
# Note: Ripeness is displayed separately and NOT included in quality grade
grade_color = get_grade_color(grade)
grade_widget = QLabel(f"Overall Grade: Class {grade}")
grade_widget.setStyleSheet(f"padding: 10px; background-color: white; border: 2px solid {grade_color};")
layout.addWidget(grade_widget)
layout.addWidget(QLabel(f"{grade_description}"))
group.setLayout(layout)
return group
def create_input_data_section(input_data: Dict[str, str]) -> QGroupBox:
"""
Create section showing input data (only for provided inputs).
Args:
input_data: Dictionary with input file paths
Returns:
QGroupBox containing input data previews
"""
group = QGroupBox("Input Data")
group.setStyleSheet(GROUP_BOX_STYLE)
layout = QVBoxLayout()
has_content = False
# Side View (Plain DSLR)
if input_data.get('dslr_side'):
dslr_side_widget = create_image_preview_widget("DSLR Side View", input_data['dslr_side'])
layout.addWidget(dslr_side_widget)
has_content = True
# Top View (RGB DSLR)
if input_data.get('dslr_top'):
dslr_top_widget = create_image_preview_widget("DSLR Top View (RGB)", input_data['dslr_top'])
layout.addWidget(dslr_top_widget)
has_content = True
# Thermal data (if provided)
if input_data.get('thermal'):
from ui.components.report_visualizations import create_thermal_widget
thermal_widget = create_thermal_widget(input_data['thermal'])
layout.addWidget(thermal_widget)
has_content = True
if not has_content:
layout.addWidget(QLabel("No input data provided"))
group.setLayout(layout)
return group
def create_input_data_with_gradcam(input_data: Dict[str, str], gradcam_image) -> QGroupBox:
"""
Create section showing input data with Grad-CAM visualization.
Args:
input_data: Dictionary with input file paths
gradcam_image: QImage of Grad-CAM visualization
Returns:
QGroupBox containing input data and Grad-CAM
"""
from ui.components.report_visualizations import create_gradcam_widget
group = QGroupBox("Input Data & Analysis Visualization")
group.setStyleSheet(GROUP_BOX_STYLE)
layout = QVBoxLayout()
# Grad-CAM visualization (from multispectral model)
if gradcam_image:
gradcam_widget = create_gradcam_widget(gradcam_image)
layout.addWidget(gradcam_widget)
# Side View (Plain DSLR)
if input_data.get('dslr_side'):
dslr_side_widget = create_image_preview_widget("DSLR Side View", input_data['dslr_side'])
layout.addWidget(dslr_side_widget)
# Top View (RGB DSLR)
if input_data.get('dslr_top'):
dslr_top_widget = create_image_preview_widget("DSLR Top View (RGB)", input_data['dslr_top'])
layout.addWidget(dslr_top_widget)
# Thermal data (if provided)
if input_data.get('thermal'):
from ui.components.report_visualizations import create_thermal_widget
thermal_widget = create_thermal_widget(input_data['thermal'])
layout.addWidget(thermal_widget)
if not gradcam_image and not any([
input_data.get('dslr_side'),
input_data.get('dslr_top'),
input_data.get('thermal')
]):
layout.addWidget(QLabel("No visualizations to display"))
group.setLayout(layout)
return group
def create_visualizations_section(input_data: Dict[str, str], results: Dict) -> QGroupBox:
"""
Create input data section with all model visualizations and clickable images.
Args:
input_data: Dictionary with input file paths
results: Dictionary with processing results from all models
Returns:
QGroupBox containing all visualizations
"""
group = QGroupBox("Analysis Visualizations")
group.setStyleSheet(GROUP_BOX_STYLE)
layout = QVBoxLayout()
# Grad-CAM from multispectral
if results.get('maturity') and results['maturity'].get('gradcam_image'):
gradcam_img = results['maturity']['gradcam_image']
gradcam_widget = create_clickable_image_widget(
gradcam_img,
"Grad-CAM Visualization (Maturity)",
"Model attention heatmap overlaid on 860nm NIR band",
max_width=500
)
layout.addWidget(gradcam_widget)
# Locule analysis image
if results.get('locule') and results['locule'].get('annotated_image'):
locule_img = results['locule']['annotated_image']
locule_widget = create_clickable_image_widget(
locule_img,
"Locule Analysis (Top View)",
"Detected and counted locules with color coding",
max_width=500
)
layout.addWidget(locule_widget)
# Defect analysis image
if results.get('defect') and results['defect'].get('annotated_image'):
defect_img = results['defect']['annotated_image']
defect_widget = create_clickable_image_widget(
defect_img,
"Defect Analysis (Side View)",
f"{results['defect'].get('primary_class', 'Unknown')} - {results['defect'].get('total_detections', 0)} detections",
max_width=500
)
layout.addWidget(defect_widget)
# Shape analysis image
if results.get('shape') and results['shape'].get('annotated_image'):
shape_img = results['shape']['annotated_image']
confidence = results['shape'].get('confidence', 0) * 100
shape_widget = create_clickable_image_widget(
shape_img,
"Shape Classification (Side View)",
f"{results['shape'].get('shape_class', 'Unknown')} ({confidence:.1f}% confidence)",
max_width=500
)
layout.addWidget(shape_widget)
# Audio ripeness - Waveform with knocks
if results.get('audio') and results['audio'].get('waveform_image'):
waveform_img = results['audio']['waveform_image']
knock_count = results['audio'].get('knock_count', 0)
waveform_widget = create_clickable_image_widget(
waveform_img,
f"Waveform with {knock_count} Detected Knocks",
"Audio waveform with knock detection markers",
max_width=600
)
layout.addWidget(waveform_widget)
# Audio ripeness - Mel Spectrogram with knocks
if results.get('audio') and results['audio'].get('spectrogram_image'):
spectrogram_img = results['audio']['spectrogram_image']
knock_count = results['audio'].get('knock_count', 0)
audio_widget = create_clickable_image_widget(
spectrogram_img,
f"Mel Spectrogram (64 Coefficients) - {knock_count} Knocks",
"Frequency-time representation of knock sounds",
max_width=600
)
layout.addWidget(audio_widget)
# Thermal if provided
if input_data.get('thermal'):
from ui.components.report_visualizations import create_thermal_widget
thermal_widget = create_thermal_widget(input_data['thermal'])
layout.addWidget(thermal_widget)
group.setLayout(layout)
return group
def create_image_preview_widget(title: str, file_path: str) -> QWidget:
"""
Create image preview widget from file path.
Args:
title: Title for the image
file_path: Path to the image file
Returns:
QWidget containing the image preview
"""
from ui.components.report_visualizations import create_image_preview_widget as _create_image
return _create_image(title, file_path)