report_visualizations.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. """
  2. Report Visualization Components
  3. Reusable visualization widgets for creating thermal heatmaps, audio spectrograms,
  4. and image previews for report generation.
  5. """
  6. from pathlib import Path
  7. from typing import Optional
  8. from PyQt5.QtWidgets import QWidget, QFrame, QVBoxLayout, QLabel
  9. from PyQt5.QtCore import Qt
  10. from PyQt5.QtGui import QFont, QPixmap, QImage
  11. import numpy as np
  12. import matplotlib
  13. matplotlib.use('Agg')
  14. import matplotlib.pyplot as plt
  15. from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
  16. from matplotlib.colors import LinearSegmentedColormap
  17. try:
  18. import tensorflow as tf
  19. except:
  20. tf = None
  21. try:
  22. from PIL import Image as PILImage
  23. HAS_PIL = True
  24. except ImportError:
  25. HAS_PIL = False
  26. from ui.components.visualization_widgets import ClickableImageWidget
  27. from ui.dialogs.image_preview_dialog import ImagePreviewDialog
  28. def create_gradcam_widget(gradcam_image: QImage) -> QWidget:
  29. """
  30. Create Grad-CAM visualization preview widget.
  31. Args:
  32. gradcam_image: QImage of the Grad-CAM visualization
  33. Returns:
  34. QWidget containing the Grad-CAM visualization
  35. """
  36. widget = QFrame()
  37. widget.setFrameStyle(QFrame.Box)
  38. widget.setStyleSheet("background-color: white; border: 1px solid #bdc3c7;")
  39. layout = QVBoxLayout(widget)
  40. # Title
  41. title_label = QLabel("<b>Grad-CAM Visualization</b>")
  42. title_label.setFont(QFont("Arial", 12))
  43. layout.addWidget(title_label)
  44. # Description
  45. desc_label = QLabel("Model attention heatmap overlaid on 860nm NIR band")
  46. desc_label.setStyleSheet("color: #7f8c8d; font-size: 10px;")
  47. layout.addWidget(desc_label)
  48. # Image preview
  49. try:
  50. if not gradcam_image.isNull():
  51. # Scale to reasonable size
  52. scaled_image = gradcam_image.scaledToWidth(400, Qt.SmoothTransformation)
  53. pixmap = QPixmap.fromImage(scaled_image)
  54. image_label = QLabel()
  55. image_label.setPixmap(pixmap)
  56. image_label.setAlignment(Qt.AlignCenter)
  57. layout.addWidget(image_label)
  58. else:
  59. layout.addWidget(QLabel("Grad-CAM image is empty"))
  60. except Exception as e:
  61. layout.addWidget(QLabel(f"Error displaying Grad-CAM: {e}"))
  62. return widget
  63. def create_image_preview_widget(title: str, file_path: str) -> QWidget:
  64. """
  65. Create image preview widget from file path.
  66. Args:
  67. title: Title for the image
  68. file_path: Path to the image file
  69. Returns:
  70. QWidget containing the image preview
  71. """
  72. widget = QFrame()
  73. widget.setFrameStyle(QFrame.Box)
  74. widget.setStyleSheet("background-color: white; border: 1px solid #bdc3c7;")
  75. layout = QVBoxLayout(widget)
  76. # Title
  77. title_label = QLabel(f"<b>{title}</b>")
  78. title_label.setFont(QFont("Arial", 12))
  79. layout.addWidget(title_label)
  80. # File path
  81. path_label = QLabel(f"File: {Path(file_path).name}")
  82. path_label.setStyleSheet("color: #7f8c8d; font-size: 10px;")
  83. layout.addWidget(path_label)
  84. # Image preview
  85. try:
  86. pixmap = QPixmap(file_path)
  87. if not pixmap.isNull():
  88. # Scale to reasonable size
  89. scaled_pixmap = pixmap.scaled(400, 300, Qt.KeepAspectRatio, Qt.SmoothTransformation)
  90. image_label = QLabel()
  91. image_label.setPixmap(scaled_pixmap)
  92. image_label.setAlignment(Qt.AlignCenter)
  93. layout.addWidget(image_label)
  94. else:
  95. layout.addWidget(QLabel("Could not load image"))
  96. except Exception as e:
  97. layout.addWidget(QLabel(f"Error loading image: {e}"))
  98. return widget
  99. def create_thermal_widget(csv_path: str) -> QWidget:
  100. """
  101. Create thermal CSV data preview widget with heatmap visualization.
  102. Args:
  103. csv_path: Path to thermal CSV file
  104. Returns:
  105. QWidget containing the thermal heatmap visualization
  106. """
  107. widget = QFrame()
  108. widget.setFrameStyle(QFrame.Box)
  109. widget.setStyleSheet("background-color: white; border: 1px solid #bdc3c7;")
  110. layout = QVBoxLayout(widget)
  111. # Title
  112. title_label = QLabel("<b>Thermal Image Analysis</b>")
  113. title_label.setFont(QFont("Arial", 12))
  114. layout.addWidget(title_label)
  115. # File path
  116. path_label = QLabel(f"File: {Path(csv_path).name}")
  117. path_label.setStyleSheet("color: #7f8c8d; font-size: 10px;")
  118. layout.addWidget(path_label)
  119. # Load and visualize thermal data
  120. try:
  121. # Load CSV file - FLIR Analyzer exports temperature data as comma-separated values
  122. data = []
  123. with open(csv_path, 'r') as f:
  124. for line in f:
  125. try:
  126. row = [float(x.strip()) for x in line.strip().split(',')]
  127. if row: # Skip empty rows
  128. data.append(row)
  129. except ValueError:
  130. # Skip rows that can't be converted to float
  131. continue
  132. if not data:
  133. layout.addWidget(QLabel("No valid thermal data found in CSV"))
  134. return widget
  135. # Convert to numpy array
  136. thermal_data = np.array(data)
  137. # Calculate statistics
  138. temp_min = thermal_data.min()
  139. temp_max = thermal_data.max()
  140. temp_mean = thermal_data.mean()
  141. temp_std = thermal_data.std()
  142. # Create heatmap visualization
  143. fig, ax = plt.subplots(figsize=(8, 6), dpi=100)
  144. # Create FLIR-style colormap: dark blue (cold) -> purple -> red -> yellow (hot)
  145. colors_flir = ['#000040', '#0000ff', '#0080ff', '#00ffff',
  146. '#00ff00', '#ffff00', '#ff8000', '#ff0000']
  147. cmap_flir = LinearSegmentedColormap.from_list('flir_style', colors_flir, N=256)
  148. # Create the heatmap
  149. im = ax.imshow(thermal_data, cmap=cmap_flir, origin='upper', aspect='auto')
  150. # Add colorbar
  151. cbar = plt.colorbar(im, ax=ax, label='Temperature (°C)')
  152. # Labels and title
  153. ax.set_xlabel('X Pixels')
  154. ax.set_ylabel('Y Pixels')
  155. ax.set_title('Thermal Camera Heatmap', fontsize=12, fontweight='bold')
  156. # Adjust layout
  157. plt.tight_layout()
  158. # Convert to QImage
  159. canvas = FigureCanvas(fig)
  160. canvas.draw()
  161. width_px, height_px = fig.get_size_inches() * fig.get_dpi()
  162. width_px, height_px = int(width_px), int(height_px)
  163. img = QImage(canvas.buffer_rgba(), width_px, height_px, QImage.Format_ARGB32)
  164. img = img.rgbSwapped()
  165. # Display image
  166. image_label = QLabel()
  167. pixmap = QPixmap.fromImage(img)
  168. # Scale to fit reasonable size
  169. if pixmap.width() > 600:
  170. scaled_pixmap = pixmap.scaledToWidth(600, Qt.SmoothTransformation)
  171. else:
  172. scaled_pixmap = pixmap
  173. image_label.setPixmap(scaled_pixmap)
  174. image_label.setAlignment(Qt.AlignCenter)
  175. layout.addWidget(image_label)
  176. plt.close(fig)
  177. # Add statistics below the heatmap
  178. stats_label = QLabel(
  179. f"<b>Temperature Statistics:</b><br>"
  180. f"Min: {temp_min:.2f}°C | Max: {temp_max:.2f}°C | "
  181. f"Mean: {temp_mean:.2f}°C | Std Dev: {temp_std:.2f}°C<br>"
  182. f"<i>Image dimensions: {thermal_data.shape[0]} × {thermal_data.shape[1]} pixels</i>"
  183. )
  184. stats_label.setStyleSheet("color: #555; font-size: 11px; padding: 5px;")
  185. stats_label.setAlignment(Qt.AlignCenter)
  186. layout.addWidget(stats_label)
  187. except Exception as e:
  188. layout.addWidget(QLabel(f"Error processing thermal data: {str(e)}"))
  189. return widget
  190. def create_audio_spectrogram_widget(audio_path: str) -> QWidget:
  191. """
  192. Create audio spectrogram widget.
  193. Args:
  194. audio_path: Path to audio WAV file
  195. Returns:
  196. QWidget containing the audio spectrogram
  197. """
  198. widget = QFrame()
  199. widget.setFrameStyle(QFrame.Box)
  200. widget.setStyleSheet("background-color: white; border: 1px solid #bdc3c7;")
  201. layout = QVBoxLayout(widget)
  202. # Title
  203. title_label = QLabel("<b>Audio Spectrogram</b>")
  204. title_label.setFont(QFont("Arial", 12))
  205. layout.addWidget(title_label)
  206. # File path
  207. path_label = QLabel(f"File: {Path(audio_path).name}")
  208. path_label.setStyleSheet("color: #7f8c8d; font-size: 10px;")
  209. layout.addWidget(path_label)
  210. # Generate spectrogram
  211. try:
  212. if tf is not None:
  213. # Load and process audio
  214. x = tf.io.read_file(str(audio_path))
  215. x, sample_rate = tf.audio.decode_wav(x, desired_channels=1, desired_samples=16000)
  216. x = tf.squeeze(x, axis=-1)
  217. waveform = x
  218. # Generate spectrogram
  219. spectrogram = tf.signal.stft(waveform, frame_length=255, frame_step=128)
  220. spectrogram = tf.abs(spectrogram)
  221. spectrogram = spectrogram[..., tf.newaxis]
  222. # Plot spectrogram
  223. fig, ax = plt.subplots(1, 1, figsize=(8, 3))
  224. if len(spectrogram.shape) > 2:
  225. spectrogram = np.squeeze(spectrogram, axis=-1)
  226. log_spec = np.log(spectrogram.T + np.finfo(float).eps)
  227. height = log_spec.shape[0]
  228. width = log_spec.shape[1]
  229. X = np.linspace(0, np.size(spectrogram), num=width, dtype=int)
  230. Y = range(height)
  231. ax.pcolormesh(X, Y, log_spec)
  232. ax.set_ylabel('Frequency')
  233. ax.set_xlabel('Time')
  234. ax.set_title('Audio Spectrogram')
  235. # Convert to QPixmap
  236. canvas = FigureCanvas(fig)
  237. canvas.draw()
  238. width_px, height_px = fig.get_size_inches() * fig.get_dpi()
  239. width_px, height_px = int(width_px), int(height_px)
  240. img = QImage(canvas.buffer_rgba(), width_px, height_px, QImage.Format_ARGB32)
  241. img = img.rgbSwapped()
  242. pixmap = QPixmap(img)
  243. image_label = QLabel()
  244. image_label.setPixmap(pixmap)
  245. image_label.setAlignment(Qt.AlignCenter)
  246. layout.addWidget(image_label)
  247. plt.close(fig)
  248. else:
  249. layout.addWidget(QLabel("TensorFlow not available - cannot generate spectrogram"))
  250. except Exception as e:
  251. layout.addWidget(QLabel(f"Error generating spectrogram: {e}"))
  252. return widget
  253. def create_clickable_image_widget(
  254. image_data,
  255. title: str,
  256. description: str = "",
  257. max_width: int = 500
  258. ) -> QWidget:
  259. """
  260. Create a clickable image widget that opens preview dialog on click.
  261. Args:
  262. image_data: QPixmap or QImage to display
  263. title: Title for the visualization
  264. description: Optional description text
  265. max_width: Maximum width for display image
  266. Returns:
  267. QWidget containing the clickable image
  268. """
  269. # Convert QImage to QPixmap if needed
  270. if isinstance(image_data, QImage):
  271. pixmap = QPixmap.fromImage(image_data)
  272. else:
  273. pixmap = image_data
  274. widget = QFrame()
  275. widget.setFrameStyle(QFrame.Box)
  276. widget.setStyleSheet("background-color: white; border: 1px solid #bdc3c7;")
  277. layout = QVBoxLayout(widget)
  278. layout.setAlignment(Qt.AlignCenter)
  279. layout.setContentsMargins(10, 10, 10, 10)
  280. # Title
  281. title_label = QLabel(f"<b>{title}</b>")
  282. title_label.setFont(QFont("Arial", 12))
  283. layout.addWidget(title_label)
  284. # Description (if provided)
  285. if description:
  286. desc_label = QLabel(description)
  287. desc_label.setStyleSheet("color: #7f8c8d; font-size: 10px;")
  288. layout.addWidget(desc_label)
  289. # Image label (clickable)
  290. image_label = QLabel()
  291. image_label.setCursor(Qt.PointingHandCursor)
  292. # Scale pixmap to fit within max_width and max_height, maintaining aspect ratio
  293. max_height = 600 # Set max height to prevent stretching
  294. scaled_pixmap = pixmap.scaledToWidth(max_width, Qt.SmoothTransformation) if pixmap.width() > max_width else pixmap
  295. # If height exceeds max_height after width scaling, scale by height instead
  296. if scaled_pixmap.height() > max_height:
  297. scaled_pixmap = pixmap.scaledToHeight(max_height, Qt.SmoothTransformation)
  298. image_label.setPixmap(scaled_pixmap)
  299. image_label.setAlignment(Qt.AlignCenter)
  300. # Store original pixmap for preview
  301. image_label.original_pixmap = pixmap
  302. image_label.preview_title = title
  303. # Connect click event
  304. def show_preview(event):
  305. dialog = ImagePreviewDialog(image_label.original_pixmap, title=image_label.preview_title, parent=widget)
  306. dialog.exec_()
  307. image_label.mousePressEvent = show_preview
  308. layout.addWidget(image_label, alignment=Qt.AlignCenter)
  309. return widget
  310. def convert_pixmap_to_pil(pixmap: QPixmap) -> Optional['PILImage.Image']:
  311. """
  312. Convert QPixmap to PIL Image for PDF generation.
  313. Args:
  314. pixmap: QPixmap to convert
  315. Returns:
  316. PIL Image or None if conversion fails
  317. """
  318. if not HAS_PIL or pixmap.isNull():
  319. return None
  320. try:
  321. # Convert QPixmap to QImage
  322. qimage = pixmap.toImage()
  323. width = qimage.width()
  324. height = qimage.height()
  325. # Convert QImage to numpy array
  326. ptr = qimage.bits()
  327. ptr.setsize(qimage.byteCount())
  328. arr = np.array(ptr).reshape(height, width, 4)
  329. # Convert RGBA to RGB if needed
  330. if arr.shape[2] == 4:
  331. arr = arr[:, :, :3]
  332. # Create PIL Image
  333. pil_image = PILImage.fromarray(arr, 'RGB')
  334. return pil_image
  335. except Exception as e:
  336. print(f"Error converting QPixmap to PIL: {e}")
  337. return None