Semapp 1.0.5__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,1248 @@
1
+ """Module for buttons"""
2
+ # pylint: disable=no-name-in-module, trailing-whitespace, too-many-branches, too-many-statements
3
+
4
+
5
+ import sys
6
+ import os
7
+ import time
8
+ import re
9
+ from PyQt5.QtWidgets import QApplication
10
+ from PyQt5.QtWidgets import (
11
+ QWidget, QButtonGroup, QPushButton, QLabel, QGroupBox, QGridLayout,
12
+ QFileDialog, QProgressDialog, QRadioButton, QSizePolicy, QSlider)
13
+ from PyQt5.QtGui import QFont
14
+ from PyQt5.QtCore import Qt
15
+ from semapp.Layout.toast import show_toast
16
+ from semapp.Processing.processing import Process
17
+ from semapp.Layout.styles import (
18
+ RADIO_BUTTON_STYLE,
19
+ SETTINGS_BUTTON_STYLE,
20
+ RUN_BUTTON_STYLE,
21
+ GROUP_BOX_STYLE,
22
+ WAFER_BUTTON_DEFAULT_STYLE,
23
+ WAFER_BUTTON_EXISTING_STYLE,
24
+ WAFER_BUTTON_MISSING_STYLE,
25
+ SELECT_BUTTON_STYLE,
26
+ PATH_LABEL_STYLE,
27
+ )
28
+ from semapp.Layout.settings import SettingsWindow
29
+
30
+ class ButtonFrame(QWidget):
31
+ """Class to create the various buttons of the interface"""
32
+
33
+ def __init__(self, layout):
34
+ super().__init__()
35
+ self.layout = layout
36
+ self.folder_path = None
37
+ self.check_vars = {}
38
+ self.common_class = None
39
+ self.folder_path_label = None
40
+ self.radio_vars = {}
41
+ self.selected_option = None
42
+ self.selected_image = None
43
+ self.table_data = None
44
+ self.table_vars = None
45
+ self.image_slider = None # Slider for COMPLUS4T mode
46
+ self.slider_value = 0 # Current slider value
47
+ self.image_group_box = None # Store reference to image type group box
48
+ self.plot_frame = None # Reference to plot frame for updates
49
+
50
+ # Threshold slider components
51
+ self.threshold_slider = None # Slider for threshold (0-255)
52
+ self.threshold_value = 255 # Current threshold value (default 255)
53
+ self.threshold_label = None # Label for threshold value
54
+
55
+ # Min size slider components
56
+ self.min_size_slider = None # Slider for minimum particle size (1-100)
57
+ self.min_size_value = 2 # Current min size value
58
+ self.min_size_label = None # Label for min size value
59
+
60
+ # Store references to all buttons for enabling/disabling
61
+ self.all_buttons = []
62
+ self.all_radio_buttons = []
63
+
64
+ self.split_rename = QRadioButton("Split .tif and rename (w/ tag)")
65
+ self.split_rename_all = QRadioButton("Split .tif and rename (w/ tag)")
66
+ self.clean = QRadioButton("Clean")
67
+ self.clean_all = QRadioButton("Clean Batch")
68
+ self.create_folder = QRadioButton("Create folders")
69
+
70
+ # New threshold and mapping buttons
71
+ self.threshold = QRadioButton("Threshold")
72
+ self.mapping = QRadioButton("Mapping")
73
+ self.threshold_all = QRadioButton("Threshold")
74
+ self.mapping_all = QRadioButton("Mapping")
75
+
76
+ self.line_edits = {}
77
+
78
+ tool_radiobuttons = [self.split_rename,
79
+ self.split_rename_all, self.clean_all,
80
+ self.create_folder, self.threshold, self.mapping,
81
+ self.threshold_all, self.mapping_all, self.clean]
82
+
83
+ for radiobutton in tool_radiobuttons:
84
+ radiobutton.setStyleSheet(RADIO_BUTTON_STYLE)
85
+ # Store reference for enabling/disabling
86
+ self.all_radio_buttons.append(radiobutton)
87
+
88
+ # Example of adding them to a layout
89
+ self.entries = {}
90
+ self.dirname = None
91
+ self.dirname = None
92
+
93
+
94
+ max_characters = 30 # Set character limit
95
+ if self.dirname:
96
+ self.display_text = self.dirname if len(
97
+ self.dirname) <= max_characters else self.dirname[
98
+ :max_characters] + '...'
99
+
100
+ # Get the user's folder path (C:\Users\XXXXX)
101
+ self.user_folder = os.path.expanduser(
102
+ "~") # This gets C:\Users\XXXXX
103
+
104
+ # Define the new folder you want to create
105
+ self.new_folder = os.path.join(self.user_folder, "SEM")
106
+
107
+
108
+ # Create the folder if it doesn't exist
109
+ self.create_directory(self.new_folder)
110
+
111
+ self.button_group = QButtonGroup(self)
112
+
113
+ self.init_ui()
114
+
115
+ def create_directory(self, path):
116
+ """Create the directory if it does not exist."""
117
+ if not os.path.exists(path):
118
+ os.makedirs(path)
119
+
120
+ def init_ui(self):
121
+ """Initialize the user interface"""
122
+ # Add widgets to the grid layout provided by the main window
123
+
124
+ self.settings_window = SettingsWindow()
125
+ self.dir_box()
126
+ self.create_wafer()
127
+ self.create_radiobuttons_other()
128
+ self.create_radiobuttons()
129
+ self.create_radiobuttons_all()
130
+ self.image_radiobuttons()
131
+ self.create_threshold_slider()
132
+ self.add_settings_button()
133
+ self.create_run_button()
134
+ self.update_wafer()
135
+ self.settings_window.data_updated.connect(self.refresh_radiobuttons)
136
+
137
+
138
+ def add_settings_button(self):
139
+ """Add a Settings button that opens a new dialog"""
140
+ self.settings_button = QPushButton("Settings")
141
+ self.settings_button.setStyleSheet(SETTINGS_BUTTON_STYLE)
142
+ self.settings_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
143
+ self.settings_button.clicked.connect(self.open_settings_window)
144
+
145
+ # Store reference for enabling/disabling
146
+ self.all_buttons.append(self.settings_button)
147
+
148
+ self.layout.addWidget(self.settings_button, 0, 4, 1, 1)
149
+
150
+ def open_settings_window(self):
151
+ """Open the settings window"""
152
+
153
+ self.settings_window.exec_()
154
+
155
+ def dir_box(self):
156
+ """Create a smaller directory selection box"""
157
+
158
+ # Create layout for this frame
159
+ frame_dir = QGroupBox("Directory")
160
+
161
+ frame_dir.setStyleSheet(GROUP_BOX_STYLE)
162
+
163
+ # Button for selecting folder
164
+ self.select_folder_button = QPushButton("Select Parent Folder...")
165
+ self.select_folder_button.setStyleSheet(SELECT_BUTTON_STYLE)
166
+
167
+ # Store reference for enabling/disabling
168
+ self.all_buttons.append(self.select_folder_button)
169
+
170
+ # Create layout for the frame and reduce its margins
171
+ frame_dir_layout = QGridLayout()
172
+ frame_dir_layout.setContentsMargins(5, 20, 5, 5) # Reduced margins
173
+ frame_dir.setLayout(frame_dir_layout)
174
+
175
+ # label for folder path
176
+ if self.dirname:
177
+ self.folder_path_label = QLabel(self.display_text)
178
+ else:
179
+ self.folder_path_label = QLabel()
180
+
181
+ self.folder_path_label.setStyleSheet(PATH_LABEL_STYLE)
182
+
183
+ # Connect the button to folder selection method
184
+ self.select_folder_button.clicked.connect(self.on_select_folder_and_update)
185
+
186
+ # Add widgets to layout
187
+ frame_dir_layout.addWidget(self.select_folder_button, 0, 0, 1, 1)
188
+ frame_dir_layout.addWidget(self.folder_path_label, 1, 0, 1, 1)
189
+
190
+ # Add frame to the main layout with a smaller footprint
191
+ self.layout.addWidget(frame_dir, 0, 0)
192
+
193
+ def folder_var_changed(self):
194
+ """Update parent folder"""
195
+ return self.dirname
196
+
197
+ def on_select_folder_and_update(self):
198
+ """Method to select folder and update checkbuttons"""
199
+ self.select_folder()
200
+ self.update_wafer()
201
+ self.image_radiobuttons() # Refresh image type widget (radio buttons or slider)
202
+ self.create_threshold_slider() # Refresh threshold slider
203
+
204
+ # Check for KRONOS and launch detection if found
205
+ if self._check_kronos_in_dirname():
206
+ self.launch_detection_automatically()
207
+
208
+ def update_wafer(self):
209
+ """Update the appearance of radio buttons based on the existing
210
+ subdirectories in the specified directory."""
211
+ if self.dirname:
212
+ # List the subdirectories in the specified directory
213
+ subdirs = [d for d in os.listdir(self.dirname) if
214
+ os.path.isdir(os.path.join(self.dirname, d))]
215
+
216
+ # If there are no subdirectories, search for wafers in KLARF files
217
+ wafer_ids = []
218
+ if not subdirs:
219
+ wafer_ids = self.extract_wafer_ids_from_klarf()
220
+
221
+ # Update the style of radio buttons based on the subdirectory presence
222
+ for number in range(1, 27):
223
+ radio_button = self.radio_vars.get(number)
224
+ if radio_button:
225
+ if str(number) in subdirs or number in wafer_ids:
226
+ radio_button.setStyleSheet(WAFER_BUTTON_EXISTING_STYLE)
227
+ else:
228
+ radio_button.setStyleSheet(WAFER_BUTTON_MISSING_STYLE)
229
+ else:
230
+ # Default style for all radio buttons if no directory is specified
231
+ for number in range(1, 27):
232
+ radio_button = self.radio_vars.get(number)
233
+ radio_button.setStyleSheet(WAFER_BUTTON_MISSING_STYLE)
234
+
235
+ def extract_wafer_ids_from_klarf(self):
236
+ """Extract wafer IDs from KLARF files (.001) that contain COMPLUS4T."""
237
+ wafer_ids = []
238
+
239
+ if not self.dirname:
240
+ return wafer_ids
241
+
242
+ # Search for .001 files
243
+ try:
244
+ files = [f for f in os.listdir(self.dirname)
245
+ if f.endswith('.001') and os.path.isfile(os.path.join(self.dirname, f))]
246
+
247
+ for file in files:
248
+ file_path = os.path.join(self.dirname, file)
249
+ try:
250
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
251
+ content = f.read()
252
+
253
+ # Check if file contains "COMPLUS4T"
254
+ if 'COMPLUS4T' in content:
255
+ # Search for all lines with WaferID
256
+ # Pattern to extract number in quotes after WaferID
257
+ pattern = r'WaferID\s+"@(\d+)"'
258
+ matches = re.findall(pattern, content)
259
+
260
+ # Add found IDs (converted to int)
261
+ for match in matches:
262
+ wafer_id = int(match)
263
+ if wafer_id not in wafer_ids and 1 <= wafer_id <= 26:
264
+ wafer_ids.append(wafer_id)
265
+ except Exception as e:
266
+ pass # Error reading file
267
+
268
+ except Exception as e:
269
+ pass # Error listing files
270
+
271
+ return wafer_ids
272
+
273
+ def create_wafer(self):
274
+ """Create a grid of radio buttons for wafer slots with exclusive selection."""
275
+ group_box = QGroupBox("Wafer Slots") # Add a title to the group
276
+ group_box.setStyleSheet(GROUP_BOX_STYLE)
277
+
278
+ wafer_layout = QGridLayout()
279
+ wafer_layout.setContentsMargins(2, 20, 2, 2) # Reduce internal margins
280
+ wafer_layout.setSpacing(5) # Reduce spacing between widgets
281
+
282
+
283
+
284
+ # Add radio buttons from 1 to 24, with 12 buttons per row
285
+ for number in range(1, 27):
286
+ radio_button = QRadioButton(str(number))
287
+ radio_button.setStyleSheet(WAFER_BUTTON_DEFAULT_STYLE)
288
+
289
+ # Connect the radio button to a handler for exclusive selection
290
+ radio_button.toggled.connect(self.get_selected_option)
291
+ self.radio_vars[number] = radio_button
292
+
293
+ # Calculate the row and column for each radio button in the layout
294
+ row = (number - 1) // 13 # Row starts at 0
295
+ col = (number - 1) % 13 # Column ranges from 0 to 12
296
+
297
+ wafer_layout.addWidget(radio_button, row, col)
298
+
299
+ group_box.setLayout(wafer_layout)
300
+
301
+ # Add the QGroupBox to the main layout (updated position - takes 2 rows)
302
+ self.layout.addWidget(group_box, 1, 0, 2, 3)
303
+
304
+ def get_selected_option(self):
305
+ """Ensure only one radio button is selected at a time and track the selected button."""
306
+ selected_number = None # Variable to store the selected radio button number
307
+
308
+ # Iterate over all radio buttons
309
+ for number, radio_button in self.radio_vars.items():
310
+ if radio_button.isChecked():
311
+ selected_number = number # Track the currently selected radio button
312
+
313
+ if selected_number is not None:
314
+ self.selected_option = selected_number # Store the selected option for further use
315
+ return self.selected_option
316
+
317
+ def image_radiobuttons(self):
318
+ """Create a grid of radio buttons or slider for image type selection."""
319
+ # Remove old widget if it exists
320
+ if self.image_group_box is not None:
321
+ self.layout.removeWidget(self.image_group_box)
322
+ self.image_group_box.deleteLater()
323
+ self.image_group_box = None
324
+
325
+ # Reset variables
326
+ self.image_slider = None
327
+ self.table_vars = None
328
+ self.slider_value = 0
329
+
330
+ # Check if COMPLUS4T mode
331
+ is_complus4t = self._check_complus4t_in_dirname()
332
+ # Check if KRONOS mode
333
+ is_kronos = self._check_kronos_in_dirname()
334
+
335
+ # Change title based on mode
336
+ title = "Defect Size (um)" if (is_complus4t or is_kronos) else "Image type"
337
+ group_box = QGroupBox(title)
338
+ group_box.setStyleSheet(GROUP_BOX_STYLE)
339
+ self.image_group_box = group_box
340
+
341
+ wafer_layout = QGridLayout()
342
+ wafer_layout.setContentsMargins(2, 20, 2, 2)
343
+ wafer_layout.setSpacing(5)
344
+
345
+ if is_complus4t or is_kronos:
346
+ # COMPLUS4T mode: create slider from 0 to 100 nm
347
+ self.image_slider = QSlider(Qt.Horizontal)
348
+ self.image_slider.setMinimum(0)
349
+ self.image_slider.setMaximum(100)
350
+ self.image_slider.setValue(0)
351
+ self.image_slider.setTickPosition(QSlider.TicksBelow)
352
+ self.image_slider.setTickInterval(10)
353
+ self.image_slider.valueChanged.connect(self.on_slider_changed)
354
+
355
+ self.slider_value_label = QLabel("0 um")
356
+ self.slider_value_label.setStyleSheet("color: black; font-size: 20px; font-weight: bold; background-color: white;")
357
+ self.slider_value_label.setFixedWidth(80) # Fixed width for label
358
+
359
+ wafer_layout.addWidget(self.image_slider, 0, 0, 1, 1)
360
+ wafer_layout.addWidget(self.slider_value_label, 0, 1, 1, 1)
361
+ else:
362
+ # Normal mode: create radio buttons
363
+ self.table_data = self.settings_window.get_table_data()
364
+ number = len(self.table_data)
365
+
366
+ self.table_vars = {}
367
+
368
+ for i in range(number):
369
+ label = str(self.table_data[i]["Scale"]) + " - " + str(
370
+ self.table_data[i]["Image Type"])
371
+ radio_button = QRadioButton(label)
372
+ radio_button.setStyleSheet(WAFER_BUTTON_DEFAULT_STYLE)
373
+ radio_button.toggled.connect(self.get_selected_image)
374
+ self.table_vars[i] = radio_button
375
+
376
+ row = (i) // 4
377
+ col = (i) % 4
378
+ wafer_layout.addWidget(radio_button, row, col)
379
+
380
+ group_box.setLayout(wafer_layout)
381
+ self.layout.addWidget(group_box, 1, 4, 2, 1) # Takes 2 rows
382
+
383
+ def create_threshold_slider(self):
384
+ """Create a threshold slider for image processing (1-255)."""
385
+ # Remove old threshold slider if it exists
386
+ if self.threshold_slider is not None or self.min_size_slider is not None:
387
+ # Find and remove the threshold group box from layout
388
+ for i in range(self.layout.count()):
389
+ widget = self.layout.itemAt(i).widget()
390
+ if widget and hasattr(widget, 'title') and widget.title() == "Threshold":
391
+ self.layout.removeWidget(widget)
392
+ widget.deleteLater()
393
+ break
394
+
395
+ group_box = QGroupBox("Threshold")
396
+ group_box.setStyleSheet(GROUP_BOX_STYLE)
397
+
398
+ threshold_layout = QGridLayout()
399
+ threshold_layout.setContentsMargins(2, 20, 2, 2)
400
+ threshold_layout.setSpacing(5)
401
+
402
+ # Create threshold slider (0-255)
403
+ self.threshold_slider = QSlider(Qt.Horizontal)
404
+ self.threshold_slider.setMinimum(0)
405
+ self.threshold_slider.setMaximum(255)
406
+ self.threshold_slider.setTickPosition(QSlider.TicksBelow)
407
+ self.threshold_slider.setTickInterval(25)
408
+
409
+ # Initialize threshold value and label BEFORE connecting signal
410
+ self.threshold_value = 255 # Initialize threshold value to match slider default
411
+ self.threshold_label = QLabel("255")
412
+ self.threshold_label.setStyleSheet("color: black; font-size: 20px; font-weight: bold; background-color: #F5F5F5;")
413
+ self.threshold_label.setFixedWidth(50) # Fixed width for label
414
+
415
+ # Connect signal AFTER initializing values
416
+ self.threshold_slider.valueChanged.connect(self.on_threshold_changed)
417
+
418
+ # Set slider value AFTER connecting signal (this will trigger on_threshold_changed)
419
+ self.threshold_slider.setValue(255) # Default threshold
420
+
421
+ # Create label for threshold range (0-255) to the right of value label
422
+ self.threshold_range_label = QLabel("(0-255)")
423
+ self.threshold_range_label.setStyleSheet("color: black; font-size: 20px; background-color: #F5F5F5;")
424
+
425
+ # Create min size slider (1-100)
426
+ self.min_size_slider = QSlider(Qt.Horizontal)
427
+ self.min_size_slider.setMinimum(1)
428
+ self.min_size_slider.setMaximum(100)
429
+ self.min_size_slider.setValue(2) # Default min size
430
+ self.min_size_slider.setTickPosition(QSlider.TicksBelow)
431
+ self.min_size_slider.setTickInterval(10)
432
+ self.min_size_slider.valueChanged.connect(self.on_min_size_changed)
433
+
434
+ # Create label for min size value (to the right of slider)
435
+ self.min_size_label = QLabel("2")
436
+ self.min_size_label.setStyleSheet("color: black; font-size: 20px; font-weight: bold; background-color: #F5F5F5;")
437
+ self.min_size_label.setFixedWidth(50) # Fixed width for label
438
+
439
+ # Create label for min size unit (um) to the right of value label
440
+ self.min_size_unit_label = QLabel("(um)")
441
+ self.min_size_unit_label.setStyleSheet("color: black; font-size: 20px; background-color: #F5F5F5;")
442
+
443
+ # Add widgets to layout - labels to the right
444
+ threshold_layout.addWidget(self.threshold_slider, 0, 0, 1, 1)
445
+ threshold_layout.addWidget(self.threshold_label, 0, 1, 1, 1)
446
+ threshold_layout.addWidget(self.threshold_range_label, 0, 2, 1, 1)
447
+ threshold_layout.addWidget(self.min_size_slider, 1, 0, 1, 1)
448
+ threshold_layout.addWidget(self.min_size_label, 1, 1, 1, 1)
449
+ threshold_layout.addWidget(self.min_size_unit_label, 1, 2, 1, 1)
450
+
451
+ group_box.setLayout(threshold_layout)
452
+
453
+ # Add to main layout in position (1,3,2,1) - takes 2 rows
454
+ self.layout.addWidget(group_box, 1, 3, 2, 1)
455
+
456
+ def on_threshold_changed(self, value):
457
+ """Handle threshold slider value changes."""
458
+ self.threshold_value = value
459
+ self.threshold_label.setText(str(value))
460
+
461
+ # Trigger plot update if plot_frame is available
462
+ if self.plot_frame and hasattr(self.plot_frame, '_update_plot'):
463
+ self.plot_frame._update_plot()
464
+
465
+ # Trigger image update if plot_frame is available
466
+ if self.plot_frame and hasattr(self.plot_frame, 'show_image'):
467
+ self.plot_frame.show_image()
468
+
469
+ def on_min_size_changed(self, value):
470
+ """Handle min size slider value changes."""
471
+ self.min_size_value = value
472
+ self.min_size_label.setText(str(value))
473
+
474
+ # Trigger plot update if plot_frame is available
475
+ if self.plot_frame and hasattr(self.plot_frame, '_update_plot'):
476
+ self.plot_frame._update_plot()
477
+
478
+ # Trigger image update if plot_frame is available
479
+ if self.plot_frame and hasattr(self.plot_frame, 'show_image'):
480
+ self.plot_frame.show_image()
481
+
482
+ def get_threshold_value(self):
483
+ """Get the current threshold value."""
484
+ return self.threshold_value
485
+
486
+ def get_min_size_value(self):
487
+ """Get the current min size value."""
488
+ return self.min_size_value
489
+
490
+ def get_processing_parameters(self):
491
+ """Get all processing parameters including threshold and min size."""
492
+ return {
493
+ 'threshold': self.threshold_value,
494
+ 'min_size': self.min_size_value,
495
+ 'defect_size_threshold': self.slider_value if self.image_slider else None
496
+ }
497
+
498
+ def _check_complus4t_in_dirname(self):
499
+ if not self.dirname or not os.path.exists(self.dirname):
500
+ return False
501
+
502
+ try:
503
+ files = [f for f in os.listdir(self.dirname)
504
+ if f.endswith('.001') and os.path.isfile(os.path.join(self.dirname, f))]
505
+
506
+ for file in files:
507
+ file_path = os.path.join(self.dirname, file)
508
+ try:
509
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
510
+ content = f.read()
511
+ if 'COMPLUS4T' in content:
512
+ return True
513
+ except Exception as e:
514
+ pass # Error reading file
515
+ except Exception as e:
516
+ pass # Error listing files
517
+
518
+ return False
519
+
520
+ def _check_kronos_in_dirname(self):
521
+ """Check if KRONOS is present in KLARF files in the directory and subdirectories (only checks first 10 lines)."""
522
+ print("\n[DEBUG] _check_kronos_in_dirname called")
523
+ print(f"[DEBUG] dirname: {self.dirname}")
524
+
525
+ if not self.dirname or not os.path.exists(self.dirname):
526
+ print("[DEBUG] dirname is None or doesn't exist")
527
+ return False
528
+
529
+ print(f"[DEBUG] dirname exists: {os.path.exists(self.dirname)}")
530
+
531
+ try:
532
+ # Search in parent directory and all subdirectories
533
+ files = []
534
+ print(f"[DEBUG] Searching for .001 files in: {self.dirname}")
535
+
536
+ # Check parent directory
537
+ all_items = os.listdir(self.dirname)
538
+ print(f"[DEBUG] Total items in parent directory: {len(all_items)}")
539
+ for item in all_items:
540
+ item_path = os.path.join(self.dirname, item)
541
+ if os.path.isfile(item_path) and item.endswith('.001'):
542
+ files.append(item_path)
543
+ print(f"[DEBUG] Found .001 file in parent: {item}")
544
+ elif os.path.isdir(item_path):
545
+ print(f"[DEBUG] Found subdirectory: {item}")
546
+ # Search in subdirectory
547
+ try:
548
+ sub_items = os.listdir(item_path)
549
+ for sub_item in sub_items:
550
+ sub_item_path = os.path.join(item_path, sub_item)
551
+ if os.path.isfile(sub_item_path) and sub_item.endswith('.001'):
552
+ files.append(sub_item_path)
553
+ print(f"[DEBUG] Found .001 file in subdirectory {item}: {sub_item}")
554
+ except Exception as e:
555
+ print(f"[DEBUG] Error reading subdirectory {item}: {e}")
556
+
557
+ print(f"[DEBUG] Total .001 files found: {len(files)}")
558
+ for f in files:
559
+ print(f"[DEBUG] - {f}")
560
+
561
+ if not files:
562
+ print("[DEBUG] No .001 files found")
563
+ return False
564
+
565
+ for file_path in files:
566
+ print(f"[DEBUG] Checking file: {os.path.basename(file_path)}")
567
+ try:
568
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
569
+ # Only read first 10 lines
570
+ for i, line in enumerate(f):
571
+ if i >= 10: # Stop after 10 lines
572
+ print(f"[DEBUG] Read {i+1} lines, stopping")
573
+ break
574
+ print(f"[DEBUG] Line {i+1}: {line.strip()[:80]}") # Print first 80 chars
575
+ if 'KRONOS' in line:
576
+ print(f"[DEBUG] *** KRONOS FOUND in line {i+1}! ***")
577
+ return True
578
+ print(f"[DEBUG] No KRONOS found in {os.path.basename(file_path)}")
579
+ except Exception as e:
580
+ print(f"[DEBUG] Error reading file {file_path}: {e}")
581
+ except Exception as e:
582
+ print(f"[DEBUG] Error listing files: {e}")
583
+
584
+ print("[DEBUG] KRONOS not found in any .001 file")
585
+ return False
586
+
587
+ def launch_detection_automatically(self):
588
+ """Launch detection automatically when KRONOS is detected."""
589
+ from semapp.Processing.detection import Detection
590
+ from PyQt5.QtWidgets import QMessageBox
591
+
592
+ try:
593
+ print("\n" + "="*60)
594
+ print("KRONOS DETECTED - Launching automatic detection")
595
+ print("="*60)
596
+
597
+ # Initialize detector
598
+ detector = Detection(dirname=self.dirname)
599
+
600
+ # Find all subdirectories (1, 2, 3, etc.)
601
+ subdirs = [d for d in os.listdir(self.dirname)
602
+ if os.path.isdir(os.path.join(self.dirname, d)) and d.isdigit()]
603
+ subdirs.sort(key=lambda x: int(x))
604
+
605
+ if not subdirs:
606
+ print("No numbered subdirectories found")
607
+ return
608
+
609
+ print(f"Found {len(subdirs)} subdirectories to process")
610
+
611
+ # Process each subdirectory
612
+ for subdir in subdirs:
613
+ subdir_path = os.path.join(self.dirname, subdir)
614
+ csv_path = os.path.join(subdir_path, "detection_results.csv")
615
+
616
+ # Skip if already processed
617
+ if os.path.exists(csv_path):
618
+ print(f"Skipping {subdir} (already has detection_results.csv)")
619
+ continue
620
+
621
+ print(f"\nProcessing subdirectory: {subdir}")
622
+
623
+ # Find TIFF files in subdirectory
624
+ tiff_files = []
625
+ for root, dirs, files in os.walk(subdir_path):
626
+ for file in files:
627
+ if file.lower().endswith(('.tif', '.tiff')):
628
+ tiff_files.append(os.path.join(root, file))
629
+
630
+ if not tiff_files:
631
+ print(f"No TIFF files found in {subdir}")
632
+ continue
633
+
634
+ # Accumulate all results for this subdirectory
635
+ all_results = {}
636
+
637
+ # Process each TIFF file
638
+ for tiff_file in tiff_files:
639
+ print(f" Processing: {os.path.basename(tiff_file)}")
640
+ file_results = detector.detect_numbers_in_tiff(tiff_file, verbose=False)
641
+
642
+ # Store results with file path as key
643
+ all_results[tiff_file] = file_results
644
+
645
+ # Save all results to CSV in the subdirectory (once per subdirectory)
646
+ if all_results:
647
+ detector.save_results_to_csv(all_results, csv_path)
648
+ total_pages = sum(len(r) if isinstance(r, list) else 1 for r in all_results.values())
649
+ print(f" Saved {total_pages} results from {len(all_results)} files to {csv_path}")
650
+
651
+ print(f"Completed processing {subdir}")
652
+
653
+ print("\n" + "="*60)
654
+ print("AUTOMATIC DETECTION COMPLETED")
655
+ print("="*60)
656
+
657
+ # Show completion message
658
+ msg = QMessageBox()
659
+ msg.setIcon(QMessageBox.Information)
660
+ msg.setText(f"Automatic detection completed for {len(subdirs)} subdirectories")
661
+ msg.setWindowTitle("Detection Complete")
662
+ msg.exec_()
663
+
664
+ except Exception as e:
665
+ print(f"Error during automatic detection: {e}")
666
+ import traceback
667
+ traceback.print_exc()
668
+
669
+ # Show error message
670
+ msg = QMessageBox()
671
+ msg.setIcon(QMessageBox.Warning)
672
+ msg.setText(f"Error during detection: {str(e)}")
673
+ msg.setWindowTitle("Detection Error")
674
+ msg.exec_()
675
+
676
+ def on_slider_changed(self, value):
677
+ """Handle slider value changes and trigger plot update."""
678
+ self.slider_value = value
679
+ self.slider_value_label.setText(f"{value} um")
680
+
681
+ # Trigger plot refresh if plot_frame exists
682
+ if self.plot_frame is not None:
683
+ self.plot_frame._update_plot()
684
+
685
+ def refresh_radiobuttons(self):
686
+ """Recreates the radio buttons after updating the data in Settings."""
687
+ self.image_radiobuttons() # Call your method to recreate the radio buttons
688
+
689
+ def get_selected_image(self):
690
+ """Track the selected radio button or slider value."""
691
+ # Check if in COMPLUS4T mode (slider)
692
+ if self.image_slider is not None:
693
+ # Return slider value (0-100) as image type
694
+ return self.slider_value, 1
695
+
696
+ # Normal mode: radio buttons
697
+ selected_number = None
698
+ if self.table_vars:
699
+ n_types = len(self.table_vars.items())
700
+ # Iterate over all radio buttons
701
+ for number, radio_button in self.table_vars.items():
702
+ if radio_button.isChecked():
703
+ selected_number = number
704
+
705
+ if selected_number is not None:
706
+ self.selected_image = selected_number
707
+ return self.selected_image, n_types
708
+
709
+ return None
710
+
711
+ def create_radiobuttons(self):
712
+ """Create radio buttons for tools and a settings button."""
713
+
714
+ # Create a QGroupBox for "Functions (Wafer)"
715
+ frame = QGroupBox("Functions (Wafer)")
716
+ frame.setStyleSheet(GROUP_BOX_STYLE)
717
+
718
+ frame_layout = QGridLayout(frame)
719
+
720
+ # Add radio buttons to the frame layout
721
+ frame_layout.addWidget(self.split_rename, 0, 0)
722
+ frame_layout.addWidget(self.threshold, 1, 0)
723
+ frame_layout.addWidget(self.mapping, 2, 0)
724
+ frame_layout.setContentsMargins(5, 20, 5, 5)
725
+ # Add the frame to the main layout
726
+ self.layout.addWidget(frame, 0, 2) # Add frame to main layout
727
+
728
+ # Add buttons to the shared button group
729
+ self.button_group.addButton(self.split_rename)
730
+ self.button_group.addButton(self.threshold)
731
+ self.button_group.addButton(self.mapping)
732
+
733
+ def create_radiobuttons_all(self):
734
+ """Create radio buttons for tools and a settings button."""
735
+
736
+ # Create a QGroupBox for "Functions (Lot)"
737
+ frame = QGroupBox("Functions (Lot)")
738
+ frame.setStyleSheet(GROUP_BOX_STYLE)
739
+
740
+ frame_layout = QGridLayout(frame)
741
+
742
+ # Add radio buttons to the frame layout
743
+ frame_layout.addWidget(self.split_rename_all, 0, 0)
744
+ frame_layout.addWidget(self.threshold_all, 1, 0)
745
+ frame_layout.addWidget(self.mapping_all, 2, 0)
746
+
747
+ frame_layout.setContentsMargins(5, 20, 5, 5)
748
+ # Add the frame to the main layout
749
+ self.layout.addWidget(frame, 0, 3) # Add frame to main layout
750
+
751
+ # Add buttons to the shared button group
752
+ self.button_group.addButton(self.split_rename_all)
753
+ self.button_group.addButton(self.threshold_all)
754
+ self.button_group.addButton(self.mapping_all)
755
+
756
+
757
+ def create_radiobuttons_other(self):
758
+ """Create radio buttons for tools and a settings button."""
759
+
760
+ # Create a QGroupBox for "Functions (Other)"
761
+ frame = QGroupBox("Functions (Other)")
762
+ frame.setStyleSheet(GROUP_BOX_STYLE)
763
+
764
+ frame_layout = QGridLayout(frame)
765
+
766
+ # Add radio buttons to the frame layout
767
+ frame_layout.addWidget(self.create_folder, 0, 0)
768
+ frame_layout.addWidget(self.clean, 1, 0)
769
+ frame_layout.addWidget(self.clean_all, 2, 0)
770
+ frame_layout.setContentsMargins(5, 20, 5, 5)
771
+
772
+ # Add the frame to the main layout
773
+ self.layout.addWidget(frame, 0, 1) # Add frame to main layout
774
+
775
+ # Add buttons to the shared button group
776
+ self.button_group.addButton(self.create_folder)
777
+ self.button_group.addButton(self.clean)
778
+ self.button_group.addButton(self.clean_all)
779
+
780
+ def select_folder(self):
781
+ """Select a parent folder"""
782
+ folder = QFileDialog.getExistingDirectory(self, "Select a Folder")
783
+
784
+ if folder:
785
+ self.dirname = folder
786
+ max_characters = 20 # Set character limit
787
+
788
+ # Truncate text if it exceeds the limit
789
+ display_text = self.dirname if len(
790
+ self.dirname) <= max_characters else self.dirname[
791
+ :max_characters] + '...'
792
+ self.folder_path_label.setText(display_text)
793
+
794
+ def create_run_button(self):
795
+ """Create a button to run data processing"""
796
+
797
+ # Create the QPushButton
798
+ self.run_button = QPushButton("Run function")
799
+ self.run_button.setStyleSheet(RUN_BUTTON_STYLE)
800
+ self.run_button.setFixedWidth(150)
801
+ self.run_button.clicked.connect(self.run_data_processing)
802
+
803
+ # Store reference for enabling/disabling
804
+ self.all_buttons.append(self.run_button)
805
+
806
+ # Add the button to the layout at position (0, 3)
807
+ self.layout.addWidget(self.run_button, 0, 5)
808
+
809
+ def set_buttons_enabled(self, enabled):
810
+ """
811
+ Enable or disable all buttons and radio buttons.
812
+
813
+ Args:
814
+ enabled: Boolean, True to enable, False to disable
815
+ """
816
+ # Enable/disable all push buttons
817
+ for button in self.all_buttons:
818
+ if button:
819
+ button.setEnabled(enabled)
820
+
821
+ # Enable/disable all radio buttons
822
+ for radio_button in self.all_radio_buttons:
823
+ if radio_button:
824
+ radio_button.setEnabled(enabled)
825
+
826
+ # Enable/disable tool radio buttons
827
+ tool_radiobuttons = [
828
+ self.split_rename, self.split_rename_all, self.clean_all,
829
+ self.create_folder, self.threshold, self.mapping,
830
+ self.threshold_all, self.mapping_all, self.clean
831
+ ]
832
+ for radiobutton in tool_radiobuttons:
833
+ if radiobutton:
834
+ radiobutton.setEnabled(enabled)
835
+
836
+ # Enable/disable wafer radio buttons
837
+ for radio_button in self.radio_vars.values():
838
+ if radio_button:
839
+ radio_button.setEnabled(enabled)
840
+
841
+ def run_data_processing(self):
842
+ """Handles photoluminescence data processing and updates progress."""
843
+
844
+ scale_data = self.new_folder + os.sep + "settings_data.json"
845
+ wafer_number= self.get_selected_option()
846
+
847
+
848
+ if not self.dirname or not any([self.clean.isChecked()
849
+ or not self.split_rename.isChecked()
850
+ or not self.clean_all.isChecked()
851
+ or not self.split_rename_all.isChecked()
852
+ or self.threshold.isChecked()
853
+ or self.mapping.isChecked()
854
+ or self.threshold_all.isChecked()
855
+ or self.mapping_all.isChecked()
856
+ ]):
857
+ show_toast(self, "Please select a folder and a function",
858
+ duration=2000, notification_type="warning")
859
+ return
860
+
861
+ # Disable all buttons and set wait cursor
862
+ self.set_buttons_enabled(False)
863
+ QApplication.setOverrideCursor(Qt.WaitCursor)
864
+
865
+ try:
866
+ # Show toast notification
867
+ show_toast(self, "Processing in progress...",
868
+ duration=1000, notification_type="info")
869
+
870
+ # Initialize processing classes
871
+ sem_class = Process(self.dirname, wafer=wafer_number, scale = scale_data)
872
+ total_steps = 0
873
+ if self.split_rename.isChecked():
874
+ total_steps = 3
875
+ if self.clean.isChecked():
876
+ total_steps = 1
877
+
878
+ if self.split_rename_all.isChecked():
879
+ total_steps = 3
880
+ if self.clean_all.isChecked():
881
+ total_steps = 1
882
+ if self.create_folder.isChecked():
883
+ total_steps = 1
884
+
885
+ if self.threshold.isChecked():
886
+ total_steps = 1
887
+ if self.mapping.isChecked():
888
+ total_steps = 1
889
+ if self.threshold_all.isChecked():
890
+ total_steps = 1
891
+ if self.mapping_all.isChecked():
892
+ total_steps = 1
893
+
894
+
895
+ progress_dialog = QProgressDialog("Data processing in progress...",
896
+ "Cancel", 0, total_steps, self)
897
+
898
+ font = QFont()
899
+ font.setPointSize(20) # Set the font size to 14
900
+ # (or any size you prefer)
901
+ progress_dialog.setFont(font)
902
+
903
+ progress_dialog.setWindowTitle("Processing")
904
+ progress_dialog.setWindowModality(Qt.ApplicationModal)
905
+ progress_dialog.setAutoClose(
906
+ False) # Ensure the dialog is not closed automatically
907
+ progress_dialog.setCancelButton(None) # Hide the cancel button
908
+ progress_dialog.resize(400, 150) # Set a larger size for the dialog
909
+
910
+ progress_dialog.show()
911
+
912
+ QApplication.processEvents()
913
+
914
+ def execute_with_timer(task_name, task_function, *args, **kwargs):
915
+ """Executes a task and displays the time taken."""
916
+ start_time = time.time()
917
+ progress_dialog.setLabelText(task_name)
918
+ QApplication.processEvents() # Ensures the interface is updated
919
+
920
+ # For long-running tasks, we need to process events periodically
921
+ # This is especially important for rename operations
922
+ task_function(*args, **kwargs)
923
+
924
+ # Process events one more time after task completion
925
+ QApplication.processEvents()
926
+
927
+ elapsed_time = time.time() - start_time
928
+ pass # Task completed
929
+
930
+ if self.split_rename.isChecked():
931
+ execute_with_timer("Cleaning of folders", sem_class.clean)
932
+ execute_with_timer("Create folders",
933
+ sem_class.organize_and_rename_files)
934
+
935
+ execute_with_timer("Split w/ tag", sem_class.split_tiff)
936
+ execute_with_timer("Rename w/ tag", sem_class.rename)
937
+ self.update_wafer()
938
+
939
+ if self.split_rename_all.isChecked():
940
+ execute_with_timer("Cleaning of folders", sem_class.clean_all)
941
+ execute_with_timer("Create folders",
942
+ sem_class.organize_and_rename_files)
943
+
944
+ execute_with_timer("Split w/ tag", sem_class.split_tiff_all)
945
+ execute_with_timer("Rename w/ tag", sem_class.rename_all)
946
+ self.update_wafer()
947
+
948
+ if self.clean.isChecked():
949
+ execute_with_timer("Cleaning of folders", sem_class.clean)
950
+
951
+ if self.clean_all.isChecked():
952
+ execute_with_timer("Cleaning of folders", sem_class.clean_all)
953
+
954
+ if self.create_folder.isChecked():
955
+ execute_with_timer("Create folders", sem_class.organize_and_rename_files)
956
+ self.update_wafer()
957
+
958
+ if self.threshold.isChecked():
959
+ execute_with_timer("Threshold processing", self.process_threshold_wafer)
960
+
961
+ if self.mapping.isChecked():
962
+ execute_with_timer("Mapping processing", self.process_mapping_wafer)
963
+
964
+ if self.threshold_all.isChecked():
965
+ execute_with_timer("Threshold processing (all)", self.process_threshold_all)
966
+
967
+ if self.mapping_all.isChecked():
968
+ execute_with_timer("Mapping processing (all)", self.process_mapping_all)
969
+
970
+ progress_dialog.close()
971
+
972
+ # Show success toast
973
+ show_toast(self, "Processing completed successfully!",
974
+ duration=3000, notification_type="success")
975
+
976
+ except Exception as e:
977
+ # Show error toast
978
+ show_toast(self, f"Error during processing: {str(e)}",
979
+ duration=4000, notification_type="error")
980
+ print(f"Error in run_data_processing: {e}")
981
+
982
+ finally:
983
+ # Re-enable all buttons and restore cursor
984
+ self.set_buttons_enabled(True)
985
+ QApplication.restoreOverrideCursor()
986
+
987
+ def process_threshold_wafer(self):
988
+ """Process threshold for selected wafer."""
989
+ selected_wafer = self.get_selected_option()
990
+ if not selected_wafer:
991
+ print("No wafer selected")
992
+ return
993
+
994
+ # Get parameters from sliders
995
+ threshold = self.get_threshold_value()
996
+ min_size = self.get_min_size_value()
997
+
998
+ # Get image size from settings data
999
+ image_size_um = 5.0 # Default value
1000
+ if hasattr(self, 'settings_window') and self.settings_window:
1001
+ settings_data = self.settings_window.data
1002
+ if settings_data and len(settings_data) > 0:
1003
+ first_entry = settings_data[0]
1004
+ if "Scale" in first_entry:
1005
+ scale_str = first_entry["Scale"]
1006
+ try:
1007
+ if 'x' in scale_str:
1008
+ image_size_um = float(scale_str.split('x')[0])
1009
+ else:
1010
+ image_size_um = float(scale_str)
1011
+ except (ValueError, IndexError):
1012
+ image_size_um = 5.0
1013
+
1014
+ # Create wafer path
1015
+ wafer_path = os.path.join(self.dirname, str(selected_wafer))
1016
+
1017
+ if not os.path.exists(wafer_path):
1018
+ print(f"Wafer directory not found: {wafer_path}")
1019
+ return
1020
+
1021
+ print(f"Processing threshold for wafer {selected_wafer}")
1022
+ print(f"Parameters: threshold={threshold}, min_size={min_size}, image_size={image_size_um}")
1023
+ print(f"Path: {wafer_path}")
1024
+
1025
+ # Create processor with parameters from sliders
1026
+ from semapp.Processing.threshold import SEMThresholdProcessor
1027
+ processor = SEMThresholdProcessor(
1028
+ threshold=threshold,
1029
+ min_size=min_size,
1030
+ image_size_um=image_size_um,
1031
+ save_results=True,
1032
+ verbose=True
1033
+ )
1034
+
1035
+ # Get coordinates and image settings from plot_frame
1036
+ if not hasattr(self.plot_frame, 'coordinates') or self.plot_frame.coordinates is None:
1037
+ print("No coordinates available in plot_frame")
1038
+ return
1039
+
1040
+ # Get image type settings
1041
+ image_result = self.get_selected_image()
1042
+ if image_result is None:
1043
+ print("No image type selected")
1044
+ return
1045
+
1046
+ image_type, number_type = image_result
1047
+
1048
+ # Get scale from settings
1049
+ scale = "5x5" # Default scale
1050
+ if hasattr(self, 'settings_window') and self.settings_window:
1051
+ settings_data = self.settings_window.data
1052
+ if settings_data and len(settings_data) > 0:
1053
+ first_entry = settings_data[0]
1054
+ if "Scale" in first_entry:
1055
+ scale = first_entry["Scale"]
1056
+
1057
+ # Process directory (without plot_sem_data)
1058
+ results = processor.process_merged_tiff_directory(
1059
+ wafer_path,
1060
+ self.plot_frame.coordinates,
1061
+ image_type,
1062
+ number_type,
1063
+ scale,
1064
+ show_results=False
1065
+ )
1066
+
1067
+ # Consolidate CSV files
1068
+ consolidated_path = processor.consolidate_csv_files(wafer_path)
1069
+
1070
+ print(f"Threshold processing completed for wafer {selected_wafer}")
1071
+ print(f"Consolidated CSV saved to: {consolidated_path}")
1072
+
1073
+ def process_mapping_wafer(self):
1074
+ """Process mapping for selected wafer."""
1075
+ selected_wafer = self.get_selected_option()
1076
+ if not selected_wafer:
1077
+ print("No wafer selected")
1078
+ return
1079
+
1080
+ # Get parameters from sliders
1081
+ threshold = self.get_threshold_value()
1082
+ min_size = self.get_min_size_value()
1083
+
1084
+ # Get image size from settings data
1085
+ image_size_um = 5.0 # Default value
1086
+ if hasattr(self, 'settings_window') and self.settings_window:
1087
+ settings_data = self.settings_window.data
1088
+ if settings_data and len(settings_data) > 0:
1089
+ first_entry = settings_data[0]
1090
+ if "Scale" in first_entry:
1091
+ scale_str = first_entry["Scale"]
1092
+ try:
1093
+ if 'x' in scale_str:
1094
+ image_size_um = float(scale_str.split('x')[0])
1095
+ else:
1096
+ image_size_um = float(scale_str)
1097
+ except (ValueError, IndexError):
1098
+ image_size_um = 5.0
1099
+
1100
+ # Create wafer path
1101
+ wafer_path = os.path.join(self.dirname, str(selected_wafer))
1102
+
1103
+ if not os.path.exists(wafer_path):
1104
+ print(f"Wafer directory not found: {wafer_path}")
1105
+ return
1106
+
1107
+ print(f"Processing mapping for wafer {selected_wafer}")
1108
+ print(f"Parameters: threshold={threshold}, min_size={min_size}, image_size={image_size_um}")
1109
+ print(f"Path: {wafer_path}")
1110
+
1111
+ # Create processor with parameters from sliders
1112
+ from semapp.Processing.threshold import SEMThresholdProcessor
1113
+ processor = SEMThresholdProcessor(
1114
+ threshold=threshold,
1115
+ min_size=min_size,
1116
+ image_size_um=image_size_um,
1117
+ save_results=True,
1118
+ verbose=True
1119
+ )
1120
+
1121
+ # Plot SEM data (mapping)
1122
+ plot_files = processor.plot_sem_data(wafer_path, show_results=False)
1123
+
1124
+ print(f"Mapping processing completed for wafer {selected_wafer}")
1125
+ print(f"Plot files created: {plot_files}")
1126
+
1127
+ def process_threshold_all(self):
1128
+ """Process threshold for all wafers."""
1129
+ # Get parameters from sliders
1130
+ threshold = self.get_threshold_value()
1131
+ min_size = self.get_min_size_value()
1132
+
1133
+ # Get image size from settings data
1134
+ image_size_um = 5.0 # Default value
1135
+ if hasattr(self, 'settings_window') and self.settings_window:
1136
+ settings_data = self.settings_window.data
1137
+ if settings_data and len(settings_data) > 0:
1138
+ first_entry = settings_data[0]
1139
+ if "Scale" in first_entry:
1140
+ scale_str = first_entry["Scale"]
1141
+ try:
1142
+ if 'x' in scale_str:
1143
+ image_size_um = float(scale_str.split('x')[0])
1144
+ else:
1145
+ image_size_um = float(scale_str)
1146
+ except (ValueError, IndexError):
1147
+ image_size_um = 5.0
1148
+
1149
+ print(f"Processing threshold for all wafers")
1150
+ print(f"Parameters: threshold={threshold}, min_size={min_size}, image_size={image_size_um}")
1151
+ print(f"Parent directory: {self.dirname}")
1152
+
1153
+ # Create processor with parameters from sliders
1154
+ from semapp.Processing.threshold import SEMThresholdProcessor
1155
+ processor = SEMThresholdProcessor(
1156
+ threshold=threshold,
1157
+ min_size=min_size,
1158
+ image_size_um=image_size_um,
1159
+ save_results=True,
1160
+ verbose=True
1161
+ )
1162
+
1163
+ # Get coordinates and image settings from plot_frame
1164
+ if not hasattr(self.plot_frame, 'coordinates') or self.plot_frame.coordinates is None:
1165
+ print("No coordinates available in plot_frame")
1166
+ return
1167
+
1168
+ # Get image type settings
1169
+ image_result = self.get_selected_image()
1170
+ if image_result is None:
1171
+ print("No image type selected")
1172
+ return
1173
+
1174
+ image_type, number_type = image_result
1175
+
1176
+ # Get scale from settings
1177
+ scale = "5x5" # Default scale
1178
+ if hasattr(self, 'settings_window') and self.settings_window:
1179
+ settings_data = self.settings_window.data
1180
+ if settings_data and len(settings_data) > 0:
1181
+ first_entry = settings_data[0]
1182
+ if "Scale" in first_entry:
1183
+ scale = first_entry["Scale"]
1184
+
1185
+ # Process parent directory (all wafers)
1186
+ results = processor.process_merged_tiff_directory(
1187
+ self.dirname,
1188
+ self.plot_frame.coordinates,
1189
+ image_type,
1190
+ number_type,
1191
+ scale,
1192
+ show_results=False
1193
+ )
1194
+
1195
+ # Consolidate CSV files
1196
+ consolidated_path = processor.consolidate_csv_files(self.dirname)
1197
+
1198
+ print(f"Threshold processing completed for all wafers")
1199
+ print(f"Consolidated CSV saved to: {consolidated_path}")
1200
+
1201
+ def process_mapping_all(self):
1202
+ """Process mapping for all wafers."""
1203
+ # Get parameters from sliders
1204
+ threshold = self.get_threshold_value()
1205
+ min_size = self.get_min_size_value()
1206
+
1207
+ # Get image size from settings data
1208
+ image_size_um = 5.0 # Default value
1209
+ if hasattr(self, 'settings_window') and self.settings_window:
1210
+ settings_data = self.settings_window.data
1211
+ if settings_data and len(settings_data) > 0:
1212
+ first_entry = settings_data[0]
1213
+ if "Scale" in first_entry:
1214
+ scale_str = first_entry["Scale"]
1215
+ try:
1216
+ if 'x' in scale_str:
1217
+ image_size_um = float(scale_str.split('x')[0])
1218
+ else:
1219
+ image_size_um = float(scale_str)
1220
+ except (ValueError, IndexError):
1221
+ image_size_um = 5.0
1222
+
1223
+ print(f"Processing mapping for all wafers")
1224
+ print(f"Parameters: threshold={threshold}, min_size={min_size}, image_size={image_size_um}")
1225
+ print(f"Parent directory: {self.dirname}")
1226
+
1227
+ # Create processor with parameters from sliders
1228
+ from semapp.Processing.threshold import SEMThresholdProcessor
1229
+ processor = SEMThresholdProcessor(
1230
+ threshold=threshold,
1231
+ min_size=min_size,
1232
+ image_size_um=image_size_um,
1233
+ save_results=True,
1234
+ verbose=True
1235
+ )
1236
+
1237
+ # Plot SEM data (mapping) for all wafers
1238
+ plot_files = processor.plot_sem_data(self.dirname, show_results=False)
1239
+
1240
+ print(f"Mapping processing completed for all wafers")
1241
+ print(f"Plot files created: {plot_files}")
1242
+
1243
+
1244
+ if __name__ == "__main__":
1245
+ app = QApplication(sys.argv)
1246
+ settings_window = SettingsWindow()
1247
+ settings_window.show()
1248
+ sys.exit(app.exec_())