fspi-analysis 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ __version__ = "0.0.1"
2
+
3
+ # src/fspi_analysis/__init__.py
4
+
5
+ # ---- Start of new debug block ---- Used this to debug the import issue
6
+ # import sys
7
+ # print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
8
+ # print("DEBUG: TOP LEVEL OF src/fspi_analysis/__init__.py IS BEING EXECUTED NOW!")
9
+ # print(f"DEBUG: Current time: {__import__('datetime').datetime.now()}")
10
+ # sys.stdout.flush() # Force output to terminal
File without changes
@@ -0,0 +1,465 @@
1
+ import napari
2
+ from magicgui import magicgui, widgets
3
+ from skimage import data, io, filters, color, util
4
+ from magicgui.widgets import PushButton, Container, FloatSlider, IntSlider, Label, FloatSpinBox
5
+ import pathlib
6
+ import traceback
7
+ import pandas as pd
8
+ from datetime import datetime
9
+ import numpy as np
10
+
11
+
12
+
13
+ # --- Global variables specific to the widget's state (if any) ---last_loaded_directory = None
14
+ last_loaded_directory = None
15
+ last_loaded_stem = None
16
+ status_label = widgets.Label(value="Welcome! Load an image to start.")
17
+ path_min_y = None
18
+ path_max_y = None
19
+ path_y_difference = None
20
+ confirmed_y_depth = None # This will store the user-confirmed Y value
21
+ depth_line_layer_name = 'depth_selection_line'
22
+ depth_slider = IntSlider(value=100, min=0, max=200, label="Depth (Y):", visible=False)
23
+ confirm_depth_button = PushButton(text="Confirm Depth Y", visible=False)
24
+ determine_depth_button = PushButton(text="Determine Custom Depth")
25
+ threshold_value_widget = FloatSlider(value=0.5, min=0.0, max=1.0, step=0.005, label="Threshold:")
26
+ real_core_size_widget = FloatSpinBox(value=6, min=0.001, step=0.1, label="Real Core Size (cm):")
27
+ core_width_px_widget = FloatSpinBox(value=671, min=1.0, step=1, label="Core Width (px): ")
28
+ viewer = napari.current_viewer()
29
+
30
+
31
+
32
+ # --- align_image_to_path function (stores path Y stats) ---
33
+ def align_image_to_path(image_layer, shapes_layer, viewer):
34
+ global status_label, path_min_y, path_max_y, path_y_difference
35
+ if not image_layer: status_label.value = "ERROR: Image layer not found for alignment."; return False
36
+ if not shapes_layer: status_label.value = "ERROR: Shapes layer not found for alignment."; return False
37
+ try:
38
+ original_image = image_layer.data; H, W = original_image.shape[:2]
39
+ if not shapes_layer.data: status_label.value = "No alignment path drawn."; return False
40
+ last_path_data_for_stats = None; last_path_data_for_alignment = None
41
+ for i in range(len(shapes_layer.data) - 1, -1, -1):
42
+ current_shape_type_list_or_str = shapes_layer.shape_type; actual_shape_type = ''
43
+ if isinstance(current_shape_type_list_or_str, list):
44
+ if i < len(current_shape_type_list_or_str): actual_shape_type = current_shape_type_list_or_str[i]
45
+ else: continue
46
+ else: actual_shape_type = current_shape_type_list_or_str
47
+ if actual_shape_type == 'path':
48
+ if i < len(shapes_layer.data):
49
+ last_path_data_for_alignment = shapes_layer.data[i]
50
+ last_path_data_for_stats = shapes_layer.data[i]
51
+ break
52
+ if last_path_data_for_alignment is None: status_label.value = "No 'path' shape found for alignment."; return False
53
+ if last_path_data_for_alignment.shape[1] != 2 or last_path_data_for_alignment.shape[0] < 1: status_label.value = "Path data is invalid."; return False
54
+ if last_path_data_for_stats is not None and last_path_data_for_stats.shape[0] > 0:
55
+ path_y_coords = last_path_data_for_stats[:, 0]
56
+ path_min_y = float(np.min(path_y_coords)); path_max_y = float(np.max(path_y_coords))
57
+ path_y_difference = path_max_y - path_min_y
58
+ else: path_min_y, path_max_y, path_y_difference = None, None, None
59
+ polyline_coords = last_path_data_for_alignment; sorted_indices = np.argsort(polyline_coords[:, 1])
60
+ px_sorted, py_sorted = polyline_coords[sorted_indices, 1], polyline_coords[sorted_indices, 0]
61
+ unique_px, unique_indices = np.unique(px_sorted, return_index=True); unique_py = py_sorted[unique_indices]
62
+ if unique_px.size == 0 : status_label.value = "No valid points for interpolation."; return False
63
+ if unique_px.size < 2:
64
+ if unique_px.size == 1: y_val_for_all_cols = np.mean(py_sorted); interpolated_y_line = np.full(W, y_val_for_all_cols)
65
+ else: status_label.value = "Not enough unique x-points for alignment path."; return False
66
+ else: interpolated_y_line = np.interp(np.arange(W), unique_px, unique_py)
67
+ if original_image.ndim == 3 and original_image.shape[-1] > 1: aligned_image = np.zeros_like(original_image)
68
+ else: aligned_image = np.zeros_like(original_image)
69
+ for x_col in range(W):
70
+ y_line_at_x = int(round(interpolated_y_line[x_col])); y_line_at_x_clipped = np.clip(y_line_at_x, 0, H - 1)
71
+ pixels_to_crop_from_top = y_line_at_x_clipped; num_pixels_to_keep = H - pixels_to_crop_from_top
72
+ if num_pixels_to_keep > 0:
73
+ source_start_y, source_end_y = pixels_to_crop_from_top, H
74
+ dest_start_y, dest_end_y = 0, num_pixels_to_keep
75
+ if original_image.ndim == 2: aligned_image[dest_start_y:dest_end_y, x_col] = original_image[source_start_y:source_end_y, x_col]
76
+ elif original_image.ndim == 3: aligned_image[dest_start_y:dest_end_y, x_col, :] = original_image[source_start_y:source_end_y, x_col, :]
77
+ try:
78
+ aligned_layer = viewer.layers['aligned_image']; aligned_layer.data = aligned_image
79
+ aligned_layer.contrast_limits = image_layer.contrast_limits; aligned_layer.colormap = image_layer.colormap
80
+ aligned_layer.blending = image_layer.blending; aligned_layer.visible = True
81
+ except KeyError:
82
+ viewer.add_image(aligned_image, name='aligned_image', colormap=image_layer.colormap.name,
83
+ contrast_limits=image_layer.contrast_limits, blending=image_layer.blending)
84
+ num_points_in_path = 0
85
+ if last_path_data_for_stats is not None : num_points_in_path = last_path_data_for_stats.shape[0]
86
+ message = f"Image aligned. Path Y range: [{path_min_y:.1f}-{path_max_y:.1f}]." if path_min_y is not None else "Image aligned."
87
+ status_label.value = message
88
+ return True
89
+ except Exception as e: status_label.value = f"ERROR during alignment: {e}"; traceback.print_exc(); return False
90
+
91
+
92
+ def clear_alignment_paths():
93
+ global viewer, status_label
94
+ try:
95
+ shapes_layer = viewer.layers['alignment_path']; shapes_layer.data = []
96
+ status_label.value = "Alignment paths cleared."
97
+ except (NameError, KeyError): status_label.value = "Could not find alignment_path layer to clear."
98
+ except Exception as e: status_label.value = f"Error clearing paths: {e}"
99
+
100
+ @magicgui(call_button="Load Image File", image_path={"label": "Choose Image:", "mode": "r"})
101
+ def load_new_image(image_path: pathlib.Path):
102
+ global viewer, last_loaded_directory, last_loaded_stem, status_label
103
+ reset_viewer_state(called_internally=True)
104
+ if not image_path or not image_path.is_file(): status_label.value = "No valid file selected."; return
105
+ status_label.value = f"Loading: {image_path.name}..."
106
+ try:
107
+ loaded_image_data = io.imread(image_path); original_layer_name = 'original_image'
108
+ try:
109
+ original_layer = viewer.layers[original_layer_name]; original_layer.data = loaded_image_data
110
+ original_layer.reset_contrast_limits()
111
+ except KeyError:
112
+ original_layer = viewer.add_image(loaded_image_data, name=original_layer_name)
113
+ original_layer.reset_contrast_limits()
114
+ last_loaded_directory = image_path.parent; last_loaded_stem = image_path.stem
115
+ shapes_layer_name = 'alignment_path'
116
+ try:
117
+ shapes_layer_index = viewer.layers.index(shapes_layer_name); target_index = len(viewer.layers) - 1
118
+ if shapes_layer_index != target_index: viewer.layers.move(shapes_layer_index, target_index)
119
+ except (ValueError, KeyError): pass
120
+ status_label.value = f"Loaded: {image_path.name}. Viewer reset."
121
+ except Exception as e: status_label.value = f"Error loading {image_path.name}: {e}"; traceback.print_exc()
122
+
123
+ def _get_thresholdable_gray_image(source_image_layer):
124
+ if source_image_layer is None: return None
125
+ image_data = source_image_layer.data; gray_image = None
126
+ try:
127
+ if image_data.ndim == 3 and image_data.shape[-1] in [3, 4]:
128
+ img_float = util.img_as_float(image_data[...,:3]); gray_image = color.rgb2gray(img_float)
129
+ elif image_data.ndim == 2:
130
+ if np.issubdtype(image_data.dtype, np.integer): gray_image = util.img_as_float(image_data)
131
+ elif image_data.min() >= 0 and image_data.max() <= 1: gray_image = image_data
132
+ else:
133
+ min_val, max_val = np.min(image_data), np.max(image_data)
134
+ if max_val > min_val: gray_image = (image_data - min_val) / (max_val - min_val)
135
+ else: gray_image = np.zeros_like(image_data, dtype=float)
136
+ else: return None
137
+ return gray_image
138
+ except Exception as e: print(f"ERROR in _get_thresholdable_gray_image: {e}"); traceback.print_exc(); return None
139
+
140
+ def calculate_auto_threshold():
141
+ global viewer, threshold_value_widget, status_label
142
+ status_label.value = "Calculating auto threshold..."
143
+ try:
144
+ aligned_layer = viewer.layers['aligned_image']; gray_image = _get_thresholdable_gray_image(aligned_layer)
145
+ if gray_image is not None:
146
+ auto_thresh_value = filters.threshold_otsu(gray_image); threshold_value_widget.value = auto_thresh_value
147
+ _update_threshold_preview()
148
+ try:
149
+ if 'original_image' in viewer.layers: viewer.layers['original_image'].visible = False
150
+ if 'aligned_image' in viewer.layers: viewer.layers['aligned_image'].visible = False
151
+ if 'thresholded_mask' in viewer.layers: viewer.layers['thresholded_mask'].visible = True
152
+ except KeyError: pass
153
+ status_label.value = f"Auto threshold: {auto_thresh_value:.4f}. Layers hidden."
154
+ else: status_label.value = "Could not prepare image for auto threshold."
155
+ except KeyError: status_label.value = "ERROR: 'aligned_image' layer not found. Align first."
156
+ except Exception as e: status_label.value = f"Error in auto threshold: {e}"; traceback.print_exc()
157
+
158
+ def _update_threshold_preview():
159
+ global viewer, threshold_value_widget, status_label; mask_layer_name = 'thresholded_mask'
160
+ try:
161
+ if 'aligned_image' not in viewer.layers: return
162
+ aligned_layer = viewer.layers['aligned_image']; gray_image = _get_thresholdable_gray_image(aligned_layer)
163
+ if gray_image is not None:
164
+ threshold = threshold_value_widget.value; binary_mask = gray_image > threshold
165
+ mask_int = binary_mask.astype(np.uint8)
166
+ try:
167
+ mask_layer = viewer.layers[mask_layer_name]; mask_layer.data = mask_int
168
+ except KeyError:
169
+ viewer.add_labels(mask_int, name=mask_layer_name, visible=True)
170
+ if mask_layer_name in viewer.layers:
171
+ viewer.layers[mask_layer_name].color_mode = 'direct'
172
+ viewer.layers[mask_layer_name].color = {0: 'transparent', 1: 'yellow'}
173
+ viewer.layers[mask_layer_name].opacity = 0.6
174
+ viewer.layers[mask_layer_name].visible = True
175
+ except Exception as e: status_label.value = f"Live preview error: {e}"; traceback.print_exc()
176
+
177
+ def apply_manual_threshold():
178
+ global viewer, threshold_value_widget, status_label
179
+ _update_threshold_preview()
180
+ if 'thresholded_mask' in viewer.layers:
181
+ status_label.value = f"Threshold {threshold_value_widget.value:.4f} applied. Mask updated."
182
+ elif 'aligned_image' not in viewer.layers:
183
+ status_label.value = "Cannot apply: 'aligned_image' layer not found."
184
+ else:
185
+ status_label.value = "Mask layer could not be created/updated."
186
+
187
+ def process_and_save():
188
+ global viewer, last_loaded_directory, last_loaded_stem, status_label
189
+ global path_min_y, path_max_y, path_y_difference, confirmed_y_depth
190
+ status_label.value = "Processing and saving results..."
191
+
192
+ if last_loaded_directory is None or last_loaded_stem is None:
193
+ status_label.value = "ERROR: No custom image loaded. Load image first."; return
194
+ if 'thresholded_mask' not in viewer.layers:
195
+ status_label.value = "ERROR: 'thresholded_mask' layer not found. Apply threshold first."; return
196
+
197
+ try:
198
+ mask_layer = viewer.layers['thresholded_mask']
199
+ mask_data_full = mask_layer.data # This is the full mask
200
+ H_full, W = mask_data_full.shape
201
+ if W == 0: status_label.value = "ERROR: Mask width is zero."; return
202
+ except KeyError: status_label.value = "ERROR: Could not access 'thresholded_mask' data."; return
203
+ except Exception as e: status_label.value = f"ERROR accessing mask data: {e}"; traceback.print_exc(); return
204
+
205
+ # --- Save Mask Image (uses full mask_data_full) ---
206
+ try:
207
+ mask_filename = f"{last_loaded_stem}_Mask.png"
208
+ output_mask_path = last_loaded_directory / mask_filename
209
+ mask_to_save = (mask_data_full * 255).astype(np.uint8)
210
+ io.imsave(output_mask_path, mask_to_save, check_contrast=False)
211
+ print(f"Mask image saved to: {output_mask_path}")
212
+ except Exception as e: status_label.value = f"ERROR saving mask image: {e}"; traceback.print_exc(); return
213
+
214
+ # --- Calculations (Row sums on full mask, vertical distances on constrained mask) ---
215
+ try:
216
+ profile_filename = f"{last_loaded_stem}_Profile.csv"
217
+ output_profile_path = last_loaded_directory / profile_filename
218
+
219
+ # Row sums and relative count from the full mask
220
+ row_sums_full = np.sum(mask_data_full, axis=1)
221
+ depths_full = np.arange(H_full)
222
+ total_sum_of_mask_pixels_full = np.sum(mask_data_full)
223
+ if total_sum_of_mask_pixels_full > 0:
224
+ relative_counts_full = (row_sums_full / total_sum_of_mask_pixels_full) * 100
225
+ else:
226
+ relative_counts_full = np.zeros_like(row_sums_full, dtype=float)
227
+
228
+ # --- Vertical Distance Calculation ---
229
+ # Determine the mask to process for vertical distances based on confirmed_y_depth
230
+ mask_for_vertical_distances = mask_data_full # Default to full mask
231
+ effective_H_for_vertical_distances = H_full
232
+
233
+ if confirmed_y_depth is not None:
234
+ # Use rows 0 up to AND INCLUDING confirmed_y_depth
235
+ # Ensure confirmed_y_depth is within bounds of the mask
236
+ clamped_depth = min(int(confirmed_y_depth), H_full - 1)
237
+ roi_depth_info_str = f"Up to Y={clamped_depth} (inclusive)" ## For adding into summary text file later
238
+ if clamped_depth < 0 : # e.g. if confirmed_y_depth was 0, then +1 is 1, slice is :1 (row 0)
239
+ effective_H_for_vertical_distances = 0
240
+ else:
241
+ effective_H_for_vertical_distances = clamped_depth + 1
242
+
243
+ mask_for_vertical_distances = mask_data_full[:effective_H_for_vertical_distances, :]
244
+ print(f"Calculating vertical distances within Y range: 0-{clamped_depth} (inclusive)")
245
+ else:
246
+ print("No custom depth set, calculating vertical distances for full height.")
247
+
248
+
249
+ vertical_distances_roi = []
250
+ if effective_H_for_vertical_distances > 0 and mask_for_vertical_distances.shape[0] > 0 : # Check if there are rows
251
+ for x_col in range(W):
252
+ column_data = mask_for_vertical_distances[:, x_col]
253
+ on_pixels_indices = np.where(column_data == 1)[0]
254
+ if on_pixels_indices.size > 0: # If at least one 'on' pixel
255
+ topmost_y = np.min(on_pixels_indices)
256
+ bottommost_y = np.max(on_pixels_indices)
257
+ distance = bottommost_y - topmost_y + 1 # Includes both ends
258
+ vertical_distances_roi.append(distance)
259
+
260
+ mean_vertical_distance_roi = np.nan
261
+ median_vertical_distance_roi = np.nan
262
+ if vertical_distances_roi:
263
+ mean_vertical_distance_roi = np.mean(vertical_distances_roi)
264
+ median_vertical_distance_roi = np.median(vertical_distances_roi)
265
+ max_vertical_distance_roi = np.max(vertical_distances_roi)
266
+ print(f"Mean Vertical Distance (ROI): {mean_vertical_distance_roi:.2f}")
267
+ print(f"Median Vertical Distance (ROI): {median_vertical_distance_roi:.2f}")
268
+ else:
269
+ print("No vertical distances calculated in ROI (e.g., empty mask region).")
270
+ # ---------------------------------------
271
+
272
+ #real_core_size_cm = 0.0
273
+ core_width_in_px = 0.0
274
+ pixel_size_cm_per_px = 0.0
275
+ if real_core_size_widget is not None and core_width_px_widget is not None:
276
+ real_core_size_cm = real_core_size_widget.value
277
+ core_width_in_px = core_width_px_widget.value
278
+ pixel_size_micron_per_px = (real_core_size_cm*1000) / core_width_in_px
279
+ mean_vertical_distance_scaled = (mean_vertical_distance_roi*pixel_size_micron_per_px)
280
+ median_vertical_distance_scaled = (median_vertical_distance_roi*pixel_size_micron_per_px)
281
+ max_vertical_distance_roi_scaled = (max_vertical_distance_roi*pixel_size_micron_per_px)
282
+ alignment_path_scaled = (path_y_difference * pixel_size_micron_per_px)
283
+
284
+ # Create pandas DataFrame
285
+ # The per-row data should match the full height of the mask
286
+ df_data = {
287
+ 'Row': depths_full,
288
+ 'Row_Sums': row_sums_full,
289
+ 'Row_Sum_Relative_Count': relative_counts_full,
290
+ 'Surface_Path_Difference_Microns': alignment_path_scaled if path_y_difference is not None else np.nan,
291
+ 'Mean_Depth_Distance_Microns': mean_vertical_distance_scaled,
292
+ 'Median_Depth_Distance_Microns': median_vertical_distance_scaled,
293
+ 'Max_Depth_Distance_Microns': max_vertical_distance_roi_scaled,
294
+ 'Microns_per_Pixel': pixel_size_micron_per_px
295
+ }
296
+ df_columns = ['Row', 'Row_Sums', 'Row_Sum_Relative_Count', 'Microns_per_Pixel',
297
+ 'Surface_Path_Difference_Microns', 'Mean_Depth_Distance_Microns', 'Median_Depth_Distance_Microns', 'Max_Depth_Distance_Microns']
298
+ df = pd.DataFrame(df_data, columns=df_columns)
299
+
300
+ df.to_csv(output_profile_path, index=False, float_format='%.4f')
301
+ status_label.value = f"Mask & profile (with vertical distances) saved for {last_loaded_stem}."
302
+ except Exception as e: status_label.value = f"ERROR saving profile CSV: {e}"; traceback.print_exc()
303
+
304
+ try:
305
+ summary_filename = f"{last_loaded_stem}_Summary.txt"
306
+ output_summary_path = last_loaded_directory / summary_filename
307
+ with open(output_summary_path, 'w') as f:
308
+ f.write(f"Image Analysis Summary\n")
309
+ f.write(f"Original File Stem: {last_loaded_stem}\n")
310
+ f.write(f"Processing Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
311
+ f.write("-" * 30 + "\n")
312
+ f.write("Alignment Path Statistics:\n")
313
+ f.write(f" Path Min Y: {path_min_y:.2f} px\n" if path_min_y is not None else " Path Min Y: Not available\n")
314
+ f.write(f" Path Max Y: {path_max_y:.2f} px\n" if path_max_y is not None else " Path Max Y: Not available\n")
315
+ f.write("-" * 30 + "\n")
316
+ f.write("Scaling Factors:\n")
317
+ f.write(f" Real Core Size Input: {real_core_size_cm:.3f} cm\n")
318
+ f.write(f" Core Width Input: {core_width_in_px:.1f} px\n")
319
+ f.write(f" Pixel Size Calculated: {pixel_size_micron_per_px:.6f} micron/px\n")
320
+ f.write("-" * 30 + "\n")
321
+ f.write(f"ROI Vertical Distances (Processed Region: {roi_depth_info_str}):\n")
322
+ f.write(f"Mean Vertical Distance Px: {mean_vertical_distance_roi}):\n")
323
+ f.write(f"Median Vertical Distance Px: {median_vertical_distance_roi}):\n")
324
+ f.write(f"Max Vertical Distance Px: {max_vertical_distance_roi}):\n")
325
+
326
+
327
+
328
+ print(f"Summary data saved to: {output_summary_path}")
329
+ status_label.value = f"Results saved for {last_loaded_stem}."
330
+ except Exception as e: status_label.value = f"ERROR saving summary TXT: {e}"; traceback.print_exc()
331
+
332
+ def reset_viewer_state(called_internally=False):
333
+ global viewer, status_label, confirmed_y_depth
334
+ global path_min_y, path_max_y, path_y_difference # Access path globals
335
+ # Access depth determination widgets if they are global
336
+ global depth_slider, confirm_depth_button, determine_depth_button
337
+
338
+ if not called_internally:
339
+ status_label.value = "Resetting viewer state..."
340
+ try:
341
+ if 'aligned_image' in viewer.layers: viewer.layers.remove('aligned_image')
342
+ if 'thresholded_mask' in viewer.layers: viewer.layers.remove('thresholded_mask')
343
+ if depth_line_layer_name in viewer.layers: viewer.layers.remove(depth_line_layer_name)
344
+
345
+ if 'original_image' in viewer.layers: viewer.layers['original_image'].visible = True
346
+ clear_alignment_paths() # Keep its own status update or make it fully silent
347
+
348
+ confirmed_y_depth = None
349
+ path_min_y, path_max_y, path_y_difference = None, None, None # Reset path stats
350
+
351
+ if 'depth_slider' in globals() and depth_slider is not None and isinstance(depth_slider, widgets.Widget):
352
+ depth_slider.visible = False
353
+ if 'confirm_depth_button' in globals() and confirm_depth_button is not None and isinstance(confirm_depth_button, widgets.Widget):
354
+ confirm_depth_button.visible = False
355
+ if 'determine_depth_button' in globals() and determine_depth_button is not None and isinstance(determine_depth_button, widgets.Widget):
356
+ determine_depth_button.enabled = True
357
+
358
+ if not called_internally:
359
+ status_label.value = "Viewer reset. Ready for next image."
360
+ except Exception as e:
361
+ if not called_internally: status_label.value = f"Error during reset: {e}"
362
+ traceback.print_exc()
363
+
364
+ # --- Functions for Depth Determination ---
365
+ # (start_depth_determination, _update_depth_line, confirm_selected_depth)
366
+ def start_depth_determination():
367
+ global viewer, status_label, depth_slider, confirm_depth_button, determine_depth_button
368
+ status_label.value = "Starting depth determination..."
369
+ if 'aligned_image' not in viewer.layers:
370
+ status_label.value = "ERROR: 'aligned_image' layer not found. Align image first."; return
371
+ try:
372
+ aligned_layer = viewer.layers['aligned_image']; H, W = aligned_layer.data.shape[:2]
373
+ if depth_line_layer_name in viewer.layers: viewer.layers.remove(depth_line_layer_name)
374
+ y_initial = H // 2; line_data = np.array([[[y_initial, 0], [y_initial, W-1]]])
375
+ viewer.add_shapes(data=line_data, shape_type='line', edge_color='lime', edge_width=2,
376
+ name=depth_line_layer_name, ndim=2)
377
+ try:
378
+ idx = viewer.layers.index(depth_line_layer_name); viewer.layers.move(idx, len(viewer.layers)-1)
379
+ except (ValueError, KeyError): pass
380
+ depth_slider.min = 0; depth_slider.max = H - 1; depth_slider.value = y_initial
381
+ depth_slider.visible = True; confirm_depth_button.visible = True
382
+ determine_depth_button.enabled = False
383
+ status_label.value = "Adjust slider for depth, then click 'Confirm Depth Y'."
384
+ except Exception as e: status_label.value = f"Error starting depth determination: {e}"; traceback.print_exc()
385
+
386
+ def _update_depth_line():
387
+ global viewer, depth_slider, status_label
388
+ try:
389
+ if depth_line_layer_name not in viewer.layers or 'aligned_image' not in viewer.layers: return
390
+ line_layer = viewer.layers[depth_line_layer_name]
391
+ aligned_W = viewer.layers['aligned_image'].data.shape[1]; y_new = depth_slider.value
392
+ line_layer.data = [np.array([[y_new, 0], [y_new, aligned_W]])]
393
+ except Exception as e: status_label.value = f"Error updating depth line: {e}"
394
+
395
+ def confirm_selected_depth():
396
+ global viewer, status_label, depth_slider, confirm_depth_button, determine_depth_button, confirmed_y_depth
397
+ confirmed_y_depth = depth_slider.value
398
+ status_label.value = f"Custom depth Y = {confirmed_y_depth} confirmed."
399
+ print(f"Confirmed Y-depth: {confirmed_y_depth}")
400
+ try:
401
+ if depth_line_layer_name in viewer.layers: viewer.layers.remove(depth_line_layer_name)
402
+ except Exception as e: status_label.value = f"Error removing depth line: {e}"
403
+ depth_slider.visible = False; confirm_depth_button.visible = False
404
+ determine_depth_button.enabled = True
405
+
406
+ def manual_trigger_button_clicked():
407
+ global status_label
408
+ print("'Align Image' button clicked.")
409
+ current_image_layer, current_shapes_layer = None, None
410
+ try:
411
+ current_image_layer = viewer.layers['original_image']
412
+ current_shapes_layer = viewer.layers['alignment_path']
413
+ except KeyError as e: status_label.value = f"ERROR: {e}"; return
414
+ if current_shapes_layer.data and len(current_shapes_layer.data) > 0:
415
+ align_image_to_path(current_image_layer, current_shapes_layer, viewer)
416
+ else: status_label.value = "No path drawn on 'alignment_path' layer."
417
+
418
+
419
+ def F_Analysis_widget():
420
+
421
+
422
+ sample_image = data.astronaut()
423
+ image_layer = viewer.add_image(sample_image, name='original_image')
424
+ shapes_layer = viewer.add_shapes(data=None, ndim=2, shape_type='path', edge_width=2,
425
+ edge_color='cyan', face_color='transparent', name='alignment_path')
426
+ shapes_layer.mode = 'add_path'
427
+ print("Automatic alignment on draw DEACTIVATED.")
428
+
429
+ align_button = PushButton(text="Align Image to Last Path")
430
+ align_button.clicked.connect(manual_trigger_button_clicked)
431
+ clear_paths_button = PushButton(text="Clear Alignment Paths")
432
+ clear_paths_button.clicked.connect(clear_alignment_paths)
433
+ load_image_widget = load_new_image
434
+ load_image_widget.label = " "
435
+
436
+ depth_label = Label(value="--- Custom Depth Selection ---")
437
+ depth_slider.changed.connect(_update_depth_line)
438
+ determine_depth_button.clicked.connect(start_depth_determination)
439
+ confirm_depth_button.clicked.connect(confirm_selected_depth)
440
+
441
+ threshold_label = Label(value="--- Thresholding ---")
442
+ auto_threshold_button = PushButton(text="Auto Threshold")
443
+ auto_threshold_button.clicked.connect(calculate_auto_threshold)
444
+ apply_threshold_button = PushButton(text="Apply Threshold")
445
+ apply_threshold_button.clicked.connect(apply_manual_threshold)
446
+ threshold_value_widget.changed.connect(_update_threshold_preview)
447
+
448
+ save_label = Label(value="--- Output ---")
449
+ process_save_button = PushButton(text="Process Image and Save Results") # Renamed for clarity
450
+ process_save_button.clicked.connect(process_and_save)
451
+ reset_button = PushButton(text="Reset for Next Image")
452
+ reset_button.clicked.connect(lambda: reset_viewer_state(called_internally=False))
453
+
454
+ control_container = Container(widgets=[
455
+ load_image_widget, Label(value="--- Alignment ---"), align_button, clear_paths_button,
456
+ depth_label, determine_depth_button, depth_slider, confirm_depth_button,
457
+ threshold_label, auto_threshold_button, threshold_value_widget, apply_threshold_button,
458
+ save_label, real_core_size_widget,
459
+ core_width_px_widget, process_save_button, Label(value="--- General ---"), reset_button,
460
+ status_label
461
+ ])
462
+ # Add the container to the viewer
463
+ return control_container
464
+
465
+
@@ -0,0 +1,12 @@
1
+ name: fspi_analysis
2
+ display_name: Controls
3
+ visibility: public
4
+ contributions:
5
+ commands:
6
+ - id: fspi_analysis.make_F_Analysis_widget
7
+ python_name: fspi_analysis.fspi_gui:F_Analysis_widget
8
+ title: Make F Analysis Widget
9
+ widgets:
10
+ - command: fspi_analysis.make_F_Analysis_widget
11
+ display_name: FSPI Image Analysis
12
+