""" Report Visualization Components Reusable visualization widgets for creating thermal heatmaps, audio spectrograms, and image previews for report generation. """ from pathlib import Path from typing import Optional from PyQt5.QtWidgets import QWidget, QFrame, QVBoxLayout, QLabel from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont, QPixmap, QImage import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.colors import LinearSegmentedColormap try: import tensorflow as tf except: tf = None try: from PIL import Image as PILImage HAS_PIL = True except ImportError: HAS_PIL = False from ui.components.visualization_widgets import ClickableImageWidget from ui.dialogs.image_preview_dialog import ImagePreviewDialog def create_gradcam_widget(gradcam_image: QImage) -> QWidget: """ Create Grad-CAM visualization preview widget. Args: gradcam_image: QImage of the Grad-CAM visualization Returns: QWidget containing the Grad-CAM visualization """ widget = QFrame() widget.setFrameStyle(QFrame.Box) widget.setStyleSheet("background-color: white; border: 1px solid #bdc3c7;") layout = QVBoxLayout(widget) # Title title_label = QLabel("Grad-CAM Visualization") title_label.setFont(QFont("Arial", 12)) layout.addWidget(title_label) # Description desc_label = QLabel("Model attention heatmap overlaid on 860nm NIR band") desc_label.setStyleSheet("color: #7f8c8d; font-size: 10px;") layout.addWidget(desc_label) # Image preview try: if not gradcam_image.isNull(): # Scale to reasonable size scaled_image = gradcam_image.scaledToWidth(400, Qt.SmoothTransformation) pixmap = QPixmap.fromImage(scaled_image) image_label = QLabel() image_label.setPixmap(pixmap) image_label.setAlignment(Qt.AlignCenter) layout.addWidget(image_label) else: layout.addWidget(QLabel("Grad-CAM image is empty")) except Exception as e: layout.addWidget(QLabel(f"Error displaying Grad-CAM: {e}")) return widget 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 """ widget = QFrame() widget.setFrameStyle(QFrame.Box) widget.setStyleSheet("background-color: white; border: 1px solid #bdc3c7;") layout = QVBoxLayout(widget) # Title title_label = QLabel(f"{title}") title_label.setFont(QFont("Arial", 12)) layout.addWidget(title_label) # File path path_label = QLabel(f"File: {Path(file_path).name}") path_label.setStyleSheet("color: #7f8c8d; font-size: 10px;") layout.addWidget(path_label) # Image preview try: pixmap = QPixmap(file_path) if not pixmap.isNull(): # Scale to reasonable size scaled_pixmap = pixmap.scaled(400, 300, Qt.KeepAspectRatio, Qt.SmoothTransformation) image_label = QLabel() image_label.setPixmap(scaled_pixmap) image_label.setAlignment(Qt.AlignCenter) layout.addWidget(image_label) else: layout.addWidget(QLabel("Could not load image")) except Exception as e: layout.addWidget(QLabel(f"Error loading image: {e}")) return widget def create_thermal_widget(csv_path: str) -> QWidget: """ Create thermal CSV data preview widget with heatmap visualization. Args: csv_path: Path to thermal CSV file Returns: QWidget containing the thermal heatmap visualization """ widget = QFrame() widget.setFrameStyle(QFrame.Box) widget.setStyleSheet("background-color: white; border: 1px solid #bdc3c7;") layout = QVBoxLayout(widget) # Title title_label = QLabel("Thermal Image Analysis") title_label.setFont(QFont("Arial", 12)) layout.addWidget(title_label) # File path path_label = QLabel(f"File: {Path(csv_path).name}") path_label.setStyleSheet("color: #7f8c8d; font-size: 10px;") layout.addWidget(path_label) # Load and visualize thermal data try: # Load CSV file - FLIR Analyzer exports temperature data as comma-separated values data = [] with open(csv_path, 'r') as f: for line in f: try: row = [float(x.strip()) for x in line.strip().split(',')] if row: # Skip empty rows data.append(row) except ValueError: # Skip rows that can't be converted to float continue if not data: layout.addWidget(QLabel("No valid thermal data found in CSV")) return widget # Convert to numpy array thermal_data = np.array(data) # Calculate statistics temp_min = thermal_data.min() temp_max = thermal_data.max() temp_mean = thermal_data.mean() temp_std = thermal_data.std() # Create heatmap visualization fig, ax = plt.subplots(figsize=(8, 6), dpi=100) # Create FLIR-style colormap: dark blue (cold) -> purple -> red -> yellow (hot) colors_flir = ['#000040', '#0000ff', '#0080ff', '#00ffff', '#00ff00', '#ffff00', '#ff8000', '#ff0000'] cmap_flir = LinearSegmentedColormap.from_list('flir_style', colors_flir, N=256) # Create the heatmap im = ax.imshow(thermal_data, cmap=cmap_flir, origin='upper', aspect='auto') # Add colorbar cbar = plt.colorbar(im, ax=ax, label='Temperature (°C)') # Labels and title ax.set_xlabel('X Pixels') ax.set_ylabel('Y Pixels') ax.set_title('Thermal Camera Heatmap', fontsize=12, fontweight='bold') # Adjust layout plt.tight_layout() # Convert to QImage canvas = FigureCanvas(fig) canvas.draw() width_px, height_px = fig.get_size_inches() * fig.get_dpi() width_px, height_px = int(width_px), int(height_px) img = QImage(canvas.buffer_rgba(), width_px, height_px, QImage.Format_ARGB32) img = img.rgbSwapped() # Display image image_label = QLabel() pixmap = QPixmap.fromImage(img) # Scale to fit reasonable size if pixmap.width() > 600: scaled_pixmap = pixmap.scaledToWidth(600, Qt.SmoothTransformation) else: scaled_pixmap = pixmap image_label.setPixmap(scaled_pixmap) image_label.setAlignment(Qt.AlignCenter) layout.addWidget(image_label) plt.close(fig) # Add statistics below the heatmap stats_label = QLabel( f"Temperature Statistics:
" f"Min: {temp_min:.2f}°C | Max: {temp_max:.2f}°C | " f"Mean: {temp_mean:.2f}°C | Std Dev: {temp_std:.2f}°C
" f"Image dimensions: {thermal_data.shape[0]} × {thermal_data.shape[1]} pixels" ) stats_label.setStyleSheet("color: #555; font-size: 11px; padding: 5px;") stats_label.setAlignment(Qt.AlignCenter) layout.addWidget(stats_label) except Exception as e: layout.addWidget(QLabel(f"Error processing thermal data: {str(e)}")) return widget def create_audio_spectrogram_widget(audio_path: str) -> QWidget: """ Create audio spectrogram widget. Args: audio_path: Path to audio WAV file Returns: QWidget containing the audio spectrogram """ widget = QFrame() widget.setFrameStyle(QFrame.Box) widget.setStyleSheet("background-color: white; border: 1px solid #bdc3c7;") layout = QVBoxLayout(widget) # Title title_label = QLabel("Audio Spectrogram") title_label.setFont(QFont("Arial", 12)) layout.addWidget(title_label) # File path path_label = QLabel(f"File: {Path(audio_path).name}") path_label.setStyleSheet("color: #7f8c8d; font-size: 10px;") layout.addWidget(path_label) # Generate spectrogram try: if tf is not None: # Load and process audio x = tf.io.read_file(str(audio_path)) x, sample_rate = tf.audio.decode_wav(x, desired_channels=1, desired_samples=16000) x = tf.squeeze(x, axis=-1) waveform = x # Generate spectrogram spectrogram = tf.signal.stft(waveform, frame_length=255, frame_step=128) spectrogram = tf.abs(spectrogram) spectrogram = spectrogram[..., tf.newaxis] # Plot spectrogram fig, ax = plt.subplots(1, 1, figsize=(8, 3)) if len(spectrogram.shape) > 2: spectrogram = np.squeeze(spectrogram, axis=-1) log_spec = np.log(spectrogram.T + np.finfo(float).eps) height = log_spec.shape[0] width = log_spec.shape[1] X = np.linspace(0, np.size(spectrogram), num=width, dtype=int) Y = range(height) ax.pcolormesh(X, Y, log_spec) ax.set_ylabel('Frequency') ax.set_xlabel('Time') ax.set_title('Audio Spectrogram') # Convert to QPixmap canvas = FigureCanvas(fig) canvas.draw() width_px, height_px = fig.get_size_inches() * fig.get_dpi() width_px, height_px = int(width_px), int(height_px) img = QImage(canvas.buffer_rgba(), width_px, height_px, QImage.Format_ARGB32) img = img.rgbSwapped() pixmap = QPixmap(img) image_label = QLabel() image_label.setPixmap(pixmap) image_label.setAlignment(Qt.AlignCenter) layout.addWidget(image_label) plt.close(fig) else: layout.addWidget(QLabel("TensorFlow not available - cannot generate spectrogram")) except Exception as e: layout.addWidget(QLabel(f"Error generating spectrogram: {e}")) return widget def create_clickable_image_widget( image_data, title: str, description: str = "", max_width: int = 500 ) -> QWidget: """ Create a clickable image widget that opens preview dialog on click. Args: image_data: QPixmap or QImage to display title: Title for the visualization description: Optional description text max_width: Maximum width for display image Returns: QWidget containing the clickable image """ # Convert QImage to QPixmap if needed if isinstance(image_data, QImage): pixmap = QPixmap.fromImage(image_data) else: pixmap = image_data widget = QFrame() widget.setFrameStyle(QFrame.Box) widget.setStyleSheet("background-color: white; border: 1px solid #bdc3c7;") layout = QVBoxLayout(widget) layout.setAlignment(Qt.AlignCenter) layout.setContentsMargins(10, 10, 10, 10) # Title title_label = QLabel(f"{title}") title_label.setFont(QFont("Arial", 12)) layout.addWidget(title_label) # Description (if provided) if description: desc_label = QLabel(description) desc_label.setStyleSheet("color: #7f8c8d; font-size: 10px;") layout.addWidget(desc_label) # Image label (clickable) image_label = QLabel() image_label.setCursor(Qt.PointingHandCursor) # Scale pixmap to fit within max_width and max_height, maintaining aspect ratio max_height = 600 # Set max height to prevent stretching scaled_pixmap = pixmap.scaledToWidth(max_width, Qt.SmoothTransformation) if pixmap.width() > max_width else pixmap # If height exceeds max_height after width scaling, scale by height instead if scaled_pixmap.height() > max_height: scaled_pixmap = pixmap.scaledToHeight(max_height, Qt.SmoothTransformation) image_label.setPixmap(scaled_pixmap) image_label.setAlignment(Qt.AlignCenter) # Store original pixmap for preview image_label.original_pixmap = pixmap image_label.preview_title = title # Connect click event def show_preview(event): dialog = ImagePreviewDialog(image_label.original_pixmap, title=image_label.preview_title, parent=widget) dialog.exec_() image_label.mousePressEvent = show_preview layout.addWidget(image_label, alignment=Qt.AlignCenter) return widget def convert_pixmap_to_pil(pixmap: QPixmap) -> Optional['PILImage.Image']: """ Convert QPixmap to PIL Image for PDF generation. Args: pixmap: QPixmap to convert Returns: PIL Image or None if conversion fails """ if not HAS_PIL or pixmap.isNull(): return None try: # Convert QPixmap to QImage qimage = pixmap.toImage() width = qimage.width() height = qimage.height() # Convert QImage to numpy array ptr = qimage.bits() ptr.setsize(qimage.byteCount()) arr = np.array(ptr).reshape(height, width, 4) # Convert RGBA to RGB if needed if arr.shape[2] == 4: arr = arr[:, :, :3] # Create PIL Image pil_image = PILImage.fromarray(arr, 'RGB') return pil_image except Exception as e: print(f"Error converting QPixmap to PIL: {e}") return None