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.
- semapp/Layout/__init__.py +26 -0
- semapp/Layout/create_button.py +1248 -0
- semapp/Layout/main_window_att.py +54 -0
- semapp/Layout/settings.py +170 -0
- semapp/Layout/styles.py +152 -0
- semapp/Layout/toast.py +157 -0
- semapp/Plot/__init__.py +8 -0
- semapp/Plot/frame_attributes.py +690 -0
- semapp/Plot/overview_window.py +355 -0
- semapp/Plot/styles.py +55 -0
- semapp/Plot/utils.py +295 -0
- semapp/Processing/__init__.py +4 -0
- semapp/Processing/detection.py +513 -0
- semapp/Processing/klarf_reader.py +461 -0
- semapp/Processing/processing.py +686 -0
- semapp/Processing/rename_tif.py +498 -0
- semapp/Processing/split_tif.py +323 -0
- semapp/Processing/threshold.py +777 -0
- semapp/__init__.py +10 -0
- semapp/asset/icon.png +0 -0
- semapp/main.py +103 -0
- semapp-1.0.5.dist-info/METADATA +300 -0
- semapp-1.0.5.dist-info/RECORD +27 -0
- semapp-1.0.5.dist-info/WHEEL +5 -0
- semapp-1.0.5.dist-info/entry_points.txt +2 -0
- semapp-1.0.5.dist-info/licenses/LICENSE +674 -0
- semapp-1.0.5.dist-info/top_level.txt +1 -0
|
@@ -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_())
|