34
34
from typing import Optional
35
35
from datetime import datetime
36
36
import re
37
+ from tkinter import filedialog
38
+ from PIL import ImageGrab
37
39
38
40
# Third Party Imports
39
41
import numpy as np
@@ -89,6 +91,9 @@ def __init__(
89
91
command = self .populate_plots ,
90
92
)
91
93
94
+ # Add trace to save a screenshot of the popup
95
+ self .view .buttons ["save_image" ].configure (command = self .capture_image )
96
+
92
97
# Initialize plots (empty)
93
98
self .initialize_plots ()
94
99
@@ -105,6 +110,43 @@ def close_popup(self) -> None:
105
110
106
111
logger .debug ("Diagnostics popup closed and sub-controller deleted." )
107
112
113
+ def capture_image (self ) -> None :
114
+
115
+ # Create default filename with timestamp
116
+ timestamp = datetime .now ().strftime ("%Y%m%d_%H%M%S" )
117
+ default_filename = f"diagnostics_{ timestamp } .png"
118
+
119
+ # Set default directory to user's home directory
120
+ default_dir = os .path .expanduser ("~" )
121
+
122
+ # Open file dialog
123
+ file_path = filedialog .asksaveasfilename (
124
+ initialdir = default_dir ,
125
+ initialfile = default_filename ,
126
+ defaultextension = ".png" ,
127
+ filetypes = [("PNG files" , "*.png" ), ("All files" , "*.*" )],
128
+ )
129
+
130
+ # If user cancels, return without saving
131
+ if not file_path :
132
+ return
133
+
134
+ # Make sure that the save dialog has time to close before capturing the screenshot
135
+ self .view .popup .after_idle (lambda : self ._take_screenshot (file_path ))
136
+
137
+ def _take_screenshot (self , file_path ):
138
+ # Get the window geometry
139
+ x = self .view .popup .winfo_rootx ()
140
+ y = self .view .popup .winfo_rooty ()
141
+ width = self .view .popup .winfo_width ()
142
+ height = self .view .popup .winfo_height ()
143
+
144
+ # Capture the screenshot
145
+ screenshot = ImageGrab .grab (bbox = (x , y , x + width , y + height ))
146
+ screenshot .save (file_path )
147
+
148
+ logger .info (f"Diagnostics screenshot saved to: { file_path } " )
149
+
108
150
def initialize_plots (self ):
109
151
"""Initialize empty plots with axes but no data."""
110
152
# Y-axis labels for each plot
@@ -144,142 +186,94 @@ def populate_plots(self):
144
186
model_log , controller_log = load_latest_log_file ()
145
187
146
188
# Plot the histogram of the display times from the controller log
147
- self .plot_display_time_histogram (controller_log )
148
-
149
- # Plot the duration of time necessary to transfer the data between processes.
150
-
151
- #
152
- # # Generate dummy data
153
- # time_points = np.linspace(0, 60, 100) # 60 seconds of data
154
- #
155
- # # Different data patterns for each plot
156
- # data_sets = {
157
- # 1: 10
158
- # + 5 * np.sin(np.linspace(0, 4 * np.pi, 100))
159
- # + np.random.normal(0, 1, 100), # IPC latency
160
- # 2: 30
161
- # + 5 * np.sin(np.linspace(0, 3 * np.pi, 100))
162
- # + np.random.normal(0, 2, 100), # Display FPS
163
- # 3: 25
164
- # + 10 * np.sin(np.linspace(0, 2 * np.pi, 100))
165
- # + np.random.normal(0, 3, 100), # Saving rate
166
- # 4: 15
167
- # + 7 * np.sin(np.linspace(0, 5 * np.pi, 100))
168
- # + np.random.normal(0, 1.5, 100), # Histogram update
169
- # 5: 50
170
- # + 20 * np.sin(np.linspace(0, 2.5 * np.pi, 100))
171
- # + np.random.normal(0, 5, 100), # Processing time
172
- # 6: np.cumsum(np.random.normal(0, 5, 100))
173
- # + 500, # Memory usage (growing trend)
174
- # }
175
- #
176
- # # Y-axis labels for each plot
177
- # y_labels = {
178
- # 1: "Latency (ms)",
179
- # 2: "FPS",
180
- # 3: "MB/s",
181
- # 4: "Time (ms)",
182
- # 5: "Time (ms)",
183
- # 6: "Memory (MB)",
184
- # }
185
- #
186
- # # Update each plot
187
- # for i in range(2, 7):
188
- # # Get figure and clear it
189
- # fig = self.view.inputs[f"diagnostics_{i}"]
190
- # ax = fig.axes[0]
191
- # ax.clear()
192
- #
193
- # # Plot the data
194
- # ax.plot(time_points, data_sets[i], linewidth=2)
195
- #
196
- # # Add labels and grid
197
- # ax.set_xlabel("Time (s)")
198
- # ax.set_ylabel(y_labels[i])
199
- # ax.grid(True, linestyle="--", alpha=0.7)
200
- #
201
- # # Add some stats
202
- # mean_val = np.mean(data_sets[i])
203
- # ax.text(
204
- # 0.05,
205
- # 0.95,
206
- # f"Mean: {mean_val:.2f}",
207
- # transform=ax.transAxes,
208
- # fontsize=9,
209
- # va="top",
210
- # bbox=dict(boxstyle="round", alpha=0.1),
211
- # )
212
- #
213
- # # Draw the canvas
214
- # self.view.inputs[f"canvas_{i}"].draw()
215
- # self.view.inputs[f"canvas_{i}"].get_tk_widget().pack(
216
- # fill="both", expand=True
217
- # )
189
+ pattern = r"camera_view: Displaying image took (\d+\.\d+) seconds"
190
+ times = self .extract_times (controller_log , pattern )
191
+ self .plot_histogram (panel = 1 , times = times )
192
+
193
+ # Plot the histogram of the times necessary to populate the histogram.
194
+ pattern = r"histogram: Histogram populated in (\d+\.\d+) seconds"
195
+ times = self .extract_times (controller_log , pattern )
196
+ self .plot_histogram (panel = 2 , times = times )
197
+
198
+ # Plot the time to move the z and f stages during a z-stack.
199
+ pattern = (
200
+ r"common_features: Z- and F-position move duration: (\d+\.\d+) seconds"
201
+ )
202
+ times = self .extract_times (model_log , pattern )
203
+ self .plot_histogram (panel = 3 , times = times )
204
+
205
+ # Plot the time necessary to acquire a new image.
206
+ pattern = r"model: New image acquired in (\d+\.\d+) seconds"
207
+ times = self .extract_times (model_log , pattern )
208
+ self .plot_histogram (panel = 4 , times = times )
218
209
219
210
@staticmethod
220
- def extract_display_times (log_content ):
211
+ def extract_times (log_content , pattern ):
221
212
"""
222
213
Extract image display times from controller log content.
223
214
224
215
Parameters
225
216
----------
226
217
log_content : list or None
227
218
List of log lines from the controller log
219
+ pattern : str
220
+ Regular expression pattern to match display time entries
228
221
229
222
Returns
230
223
-------
231
- list
232
- A list of float values representing display times in seconds
224
+ times : list
225
+ A list of float values representing times in seconds
233
226
"""
234
227
235
228
if not log_content :
236
- return []
237
-
238
- # Pattern to match display time entries
239
- pattern = r"camera_view: Displaying image took (\d+\.\d+) seconds"
229
+ return None
240
230
241
231
# Extract all matching times
242
- display_times = []
232
+ times = []
243
233
for line in log_content :
244
234
match = re .search (pattern , line )
245
235
if match :
246
- display_times .append (float (match .group (1 )))
236
+ times .append (float (match .group (1 )))
247
237
248
- return display_times
238
+ times = times if len (times ) > 0 else None
239
+ return times
249
240
250
- def plot_display_time_histogram (self , controller_log ):
241
+ def plot_histogram (self , panel , times ):
251
242
"""
252
243
Create a histogram of image display times with statistics.
253
244
254
245
Parameters
255
246
----------
256
- controller_log : str
257
- The content of the controller log file as a string or list of lines
247
+ panel : int
248
+ The panel number to plot the histogram on (1-6)
249
+ times : list
250
+ A list of times in seconds to plot
258
251
"""
259
-
260
- fig = self .view .inputs [f"diagnostics_1" ]
252
+ fig = self .view .inputs [f"diagnostics_{ panel } " ]
261
253
ax = fig .axes [0 ]
262
254
ax .clear ()
263
255
264
- display_times = self .extract_display_times (controller_log )
265
-
266
- if not display_times :
256
+ if times is None :
267
257
ax .text (
268
258
0.5 ,
269
259
0.5 ,
270
- "No display time data available" ,
260
+ "No data available" ,
271
261
ha = "center" ,
272
262
va = "center" ,
273
263
transform = ax .transAxes ,
274
264
)
265
+ self .view .inputs [f"canvas_{ panel } " ].draw ()
266
+ self .view .inputs [f"canvas_{ panel } " ].get_tk_widget ().pack (
267
+ fill = "both" , expand = True
268
+ )
275
269
return
276
270
277
271
# Convert to milliseconds for better readability
278
- display_times_ms = [t * 1000 for t in display_times ]
272
+ times_ms = [t * 1000 for t in times ]
279
273
280
274
# Plot histogram
281
275
_ , _ , _ = ax .hist (
282
- display_times_ms ,
276
+ times_ms ,
283
277
bins = 20 ,
284
278
alpha = 0.7 ,
285
279
color = "skyblue" ,
@@ -288,8 +282,8 @@ def plot_display_time_histogram(self, controller_log):
288
282
)
289
283
290
284
# Calculate statistics
291
- mean_val = np .mean (display_times_ms )
292
- std_val = np .std (display_times_ms )
285
+ mean_val = np .mean (times_ms )
286
+ std_val = np .std (times_ms )
293
287
294
288
# Add vertical lines for mean and std dev
295
289
ax .axvline (
@@ -308,17 +302,13 @@ def plot_display_time_histogram(self, controller_log):
308
302
)
309
303
ax .axvline (mean_val - std_val , color = "g" , linestyle = "dotted" , linewidth = 2 )
310
304
311
- # Add text with statistics
312
- stats_text = f"Mean: { mean_val :.2f} ms\n Std Dev: { std_val :.2f} ms\n Samples: { len (display_times_ms )} "
313
- ax .text (
314
- 0.05 ,
315
- 0.95 ,
316
- stats_text ,
317
- transform = ax .transAxes ,
318
- fontsize = 9 ,
319
- va = "top" ,
320
- bbox = dict (boxstyle = "round" , alpha = 0.5 ),
305
+ # Add text with statistics as a title
306
+ stats_text = (
307
+ f"Mean: { mean_val :.2f} ms."
308
+ f"Std Dev: { std_val :.2f} ms."
309
+ f"N: { len (times_ms )} "
321
310
)
311
+ ax .set_title (stats_text , fontsize = 9 )
322
312
323
313
# Update labels
324
314
ax .set_xlabel ("Time (ms)" )
@@ -328,5 +318,7 @@ def plot_display_time_histogram(self, controller_log):
328
318
ax .grid (True , linestyle = "--" , alpha = 0.7 )
329
319
330
320
# Draw the canvas
331
- self .view .inputs [f"canvas_1" ].draw ()
332
- self .view .inputs [f"canvas_1" ].get_tk_widget ().pack (fill = "both" , expand = True )
321
+ self .view .inputs [f"canvas_{ panel } " ].draw ()
322
+ self .view .inputs [f"canvas_{ panel } " ].get_tk_widget ().pack (
323
+ fill = "both" , expand = True
324
+ )
0 commit comments