Skip to content

Commit 54db586

Browse files
Add performance diagnostics and screenshot features
Introduces performance diagnostics, including histograms for image display, histogram update, stage move, and image acquisition times, parsed from log files. Adds a button to save a screenshot of the diagnostics popup.
1 parent 03a8fd5 commit 54db586

File tree

11 files changed

+308
-219
lines changed

11 files changed

+308
-219
lines changed

docs/capture_gui.py

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from mss import mss
2-
from PIL import Image
31
import tkinter as tk
42
import os
3+
4+
from navigate.tools.gui import capture_region, tk_window_bbox
55
from navigate.view.splash_screen import SplashScreen
66
from navigate.controller.controller import Controller
77
from navigate.tools.main_functions import create_parser, evaluate_parser_input_arguments
@@ -59,23 +59,6 @@ def get_controller():
5959
return root, controller
6060

6161

62-
def capture_region(x, y, w, h, out_path):
63-
with mss() as sct:
64-
img = sct.grab(
65-
{"left": int(x), "top": int(y), "width": int(w), "height": int(h)}
66-
)
67-
Image.frombytes("RGB", img.size, img.rgb).save(out_path)
68-
69-
70-
def tk_window_bbox(win, pad=0):
71-
win.update_idletasks()
72-
x = win.winfo_rootx() - pad
73-
y = win.winfo_rooty() - pad
74-
w = win.winfo_width() + 2 * pad
75-
h = win.winfo_height() + 2 * pad
76-
return x, y, w, h
77-
78-
7962
if __name__ == "__main__":
8063
root, controller = get_controller()
8164
current_directory = os.path.dirname(os.path.realpath(__file__))

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ dependencies = [
4040
'fsspec==2022.5.0; sys_platform == "darwin"',
4141
'h5py==3.7.0',
4242
'requests==2.28.1',
43-
'psutil==6.0.0'
43+
'psutil==6.0.0',
44+
"mss"
4445
]
4546

4647
[project.scripts]
@@ -69,7 +70,6 @@ docs = [
6970
"pyyaml",
7071
"pydata_sphinx_theme==0.10.0rc2",
7172
"sphinx-toolbox",
72-
"mss"
7373
]
7474

7575
robot = [

src/navigate/controller/controller.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,8 @@ def __init__(
278278

279279
#: StageController: Stage Sub-Controller.
280280
self.stage_controller = StageController(
281-
self.view.settings.stage_control_tab, self,
281+
self.view.settings.stage_control_tab,
282+
self,
282283
)
283284

284285
#: WaveformTabController: Waveform Display Sub-Controller.
@@ -726,7 +727,7 @@ def execute(self, command, *args):
726727
self.threads_pool.createThread(
727728
resourceName="model",
728729
target=self.update_stage_limits,
729-
args=(microscope_name,)
730+
args=(microscope_name,),
730731
)
731732

732733
elif command == "move_stage_and_update_info":
@@ -1166,7 +1167,7 @@ def capture_image(self, command, mode, *args):
11661167
break
11671168
# Receive the Image and log it.
11681169
image_id = self.show_img_pipe.recv()
1169-
logger.info(f"Navigate Controller - Received Image: {image_id}")
1170+
logger.info(f"Received image from the controller: {image_id}")
11701171

11711172
if image_id == "stop":
11721173
self.current_image_id = -1
@@ -1412,7 +1413,7 @@ def stop_stage(self):
14121413

14131414
def update_stage_limits(self, microscope_name: str) -> None:
14141415
"""Update stage limits on the device side
1415-
1416+
14161417
Parameters
14171418
----------
14181419
microscope_name : str

src/navigate/controller/sub_controllers/diagnostics_popup.py

Lines changed: 98 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
from typing import Optional
3535
from datetime import datetime
3636
import re
37+
from tkinter import filedialog
38+
from PIL import ImageGrab
3739

3840
# Third Party Imports
3941
import numpy as np
@@ -89,6 +91,9 @@ def __init__(
8991
command=self.populate_plots,
9092
)
9193

94+
# Add trace to save a screenshot of the popup
95+
self.view.buttons["save_image"].configure(command=self.capture_image)
96+
9297
# Initialize plots (empty)
9398
self.initialize_plots()
9499

@@ -105,6 +110,43 @@ def close_popup(self) -> None:
105110

106111
logger.debug("Diagnostics popup closed and sub-controller deleted.")
107112

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+
108150
def initialize_plots(self):
109151
"""Initialize empty plots with axes but no data."""
110152
# Y-axis labels for each plot
@@ -144,142 +186,94 @@ def populate_plots(self):
144186
model_log, controller_log = load_latest_log_file()
145187

146188
# 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)
218209

219210
@staticmethod
220-
def extract_display_times(log_content):
211+
def extract_times(log_content, pattern):
221212
"""
222213
Extract image display times from controller log content.
223214
224215
Parameters
225216
----------
226217
log_content : list or None
227218
List of log lines from the controller log
219+
pattern : str
220+
Regular expression pattern to match display time entries
228221
229222
Returns
230223
-------
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
233226
"""
234227

235228
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
240230

241231
# Extract all matching times
242-
display_times = []
232+
times = []
243233
for line in log_content:
244234
match = re.search(pattern, line)
245235
if match:
246-
display_times.append(float(match.group(1)))
236+
times.append(float(match.group(1)))
247237

248-
return display_times
238+
times = times if len(times) > 0 else None
239+
return times
249240

250-
def plot_display_time_histogram(self, controller_log):
241+
def plot_histogram(self, panel, times):
251242
"""
252243
Create a histogram of image display times with statistics.
253244
254245
Parameters
255246
----------
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
258251
"""
259-
260-
fig = self.view.inputs[f"diagnostics_1"]
252+
fig = self.view.inputs[f"diagnostics_{panel}"]
261253
ax = fig.axes[0]
262254
ax.clear()
263255

264-
display_times = self.extract_display_times(controller_log)
265-
266-
if not display_times:
256+
if times is None:
267257
ax.text(
268258
0.5,
269259
0.5,
270-
"No display time data available",
260+
"No data available",
271261
ha="center",
272262
va="center",
273263
transform=ax.transAxes,
274264
)
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+
)
275269
return
276270

277271
# 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]
279273

280274
# Plot histogram
281275
_, _, _ = ax.hist(
282-
display_times_ms,
276+
times_ms,
283277
bins=20,
284278
alpha=0.7,
285279
color="skyblue",
@@ -288,8 +282,8 @@ def plot_display_time_histogram(self, controller_log):
288282
)
289283

290284
# 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)
293287

294288
# Add vertical lines for mean and std dev
295289
ax.axvline(
@@ -308,17 +302,13 @@ def plot_display_time_histogram(self, controller_log):
308302
)
309303
ax.axvline(mean_val - std_val, color="g", linestyle="dotted", linewidth=2)
310304

311-
# Add text with statistics
312-
stats_text = f"Mean: {mean_val:.2f} ms\nStd Dev: {std_val:.2f} ms\nSamples: {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)}"
321310
)
311+
ax.set_title(stats_text, fontsize=9)
322312

323313
# Update labels
324314
ax.set_xlabel("Time (ms)")
@@ -328,5 +318,7 @@ def plot_display_time_histogram(self, controller_log):
328318
ax.grid(True, linestyle="--", alpha=0.7)
329319

330320
# 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

Comments
 (0)