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,355 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Overview window for displaying wafer mapping with image thumbnails.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from PIL import Image
|
|
9
|
+
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QMessageBox, QPushButton, QHBoxLayout, QFileDialog
|
|
10
|
+
from PyQt5.QtCore import Qt
|
|
11
|
+
from PyQt5.QtGui import QGuiApplication, QKeyEvent
|
|
12
|
+
from matplotlib.figure import Figure
|
|
13
|
+
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
14
|
+
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
|
|
15
|
+
import matplotlib.pyplot as plt
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OverviewWindow(QWidget):
|
|
19
|
+
"""
|
|
20
|
+
A window that displays a full-screen overview of the wafer mapping
|
|
21
|
+
with image thumbnails at each coordinate position.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, coordinates, image_list, tiff_path, is_complus4t_mode=False,
|
|
25
|
+
image_type=None, number_type=None, button_frame=None, parent=None):
|
|
26
|
+
"""
|
|
27
|
+
Initialize the overview window.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
coordinates: DataFrame with columns ["defect_id", "X", "Y", "defect_size"]
|
|
31
|
+
image_list: List of PIL Images from the TIFF file
|
|
32
|
+
tiff_path: Path to the TIFF file
|
|
33
|
+
is_complus4t_mode: Boolean indicating if in COMPLUS4T mode
|
|
34
|
+
image_type: Image type index (for normal mode)
|
|
35
|
+
number_type: Number of image types (for normal mode)
|
|
36
|
+
button_frame: Reference to button frame for getting image settings
|
|
37
|
+
parent: Parent widget
|
|
38
|
+
"""
|
|
39
|
+
super().__init__(parent)
|
|
40
|
+
|
|
41
|
+
# Set window flags FIRST, before creating any widgets
|
|
42
|
+
self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint)
|
|
43
|
+
|
|
44
|
+
self.coordinates = coordinates
|
|
45
|
+
self.image_list = image_list
|
|
46
|
+
self.tiff_path = tiff_path
|
|
47
|
+
self.is_complus4t_mode = is_complus4t_mode
|
|
48
|
+
self.image_type = image_type
|
|
49
|
+
self.number_type = number_type
|
|
50
|
+
self.button_frame = button_frame
|
|
51
|
+
|
|
52
|
+
# Thumbnail size (adjust based on number of images)
|
|
53
|
+
self.thumbnail_size = self._calculate_thumbnail_size()
|
|
54
|
+
|
|
55
|
+
self.setWindowTitle("Overview - Wafer Mapping")
|
|
56
|
+
self._setup_ui()
|
|
57
|
+
self._create_overview_plot()
|
|
58
|
+
print(f"[DEBUG] OverviewWindow __init__ complete")
|
|
59
|
+
|
|
60
|
+
def keyPressEvent(self, event):
|
|
61
|
+
"""Handle key press events - close on Escape key."""
|
|
62
|
+
if event.key() == Qt.Key_Escape:
|
|
63
|
+
self.close()
|
|
64
|
+
else:
|
|
65
|
+
super().keyPressEvent(event)
|
|
66
|
+
|
|
67
|
+
def save_overview(self):
|
|
68
|
+
"""Save the overview plot to a file using the canvas."""
|
|
69
|
+
# Disable save button during save operation
|
|
70
|
+
save_button = self.findChild(QPushButton, "save_button")
|
|
71
|
+
if save_button:
|
|
72
|
+
save_button.setEnabled(False)
|
|
73
|
+
save_button.setText("Saving...")
|
|
74
|
+
|
|
75
|
+
# Process events to update UI
|
|
76
|
+
from PyQt5.QtWidgets import QApplication
|
|
77
|
+
QApplication.processEvents()
|
|
78
|
+
|
|
79
|
+
# Open file dialog to choose save location
|
|
80
|
+
file_path, selected_filter = QFileDialog.getSaveFileName(
|
|
81
|
+
self,
|
|
82
|
+
"Save Overview",
|
|
83
|
+
"overview.png",
|
|
84
|
+
"PNG Files (*.png);;PDF Files (*.pdf);;SVG Files (*.svg);;All Files (*)"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if file_path:
|
|
88
|
+
try:
|
|
89
|
+
print(f"[DEBUG] Starting save to: {file_path}")
|
|
90
|
+
|
|
91
|
+
# Determine DPI based on file format (lower for memory efficiency)
|
|
92
|
+
if file_path.lower().endswith('.pdf'):
|
|
93
|
+
dpi = 150 # Lower DPI for PDF
|
|
94
|
+
elif file_path.lower().endswith('.svg'):
|
|
95
|
+
dpi = 100 # SVG is vector, DPI less critical
|
|
96
|
+
else:
|
|
97
|
+
dpi = 150 # Reduced from 300 for PNG to save memory
|
|
98
|
+
|
|
99
|
+
# Save using canvas print_figure (more efficient than figure.savefig)
|
|
100
|
+
self.canvas.print_figure(
|
|
101
|
+
file_path,
|
|
102
|
+
dpi=dpi,
|
|
103
|
+
bbox_inches='tight', # Remove extra whitespace
|
|
104
|
+
facecolor='white', # White background
|
|
105
|
+
edgecolor='none',
|
|
106
|
+
format=None, # Let matplotlib determine format from extension
|
|
107
|
+
pad_inches=0.1 # Small padding
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Force garbage collection
|
|
111
|
+
import gc
|
|
112
|
+
gc.collect()
|
|
113
|
+
|
|
114
|
+
print(f"[DEBUG] Overview saved to: {file_path}")
|
|
115
|
+
except Exception as e:
|
|
116
|
+
print(f"[DEBUG] Error saving overview: {e}")
|
|
117
|
+
import traceback
|
|
118
|
+
traceback.print_exc()
|
|
119
|
+
# Show error message
|
|
120
|
+
msg = QMessageBox()
|
|
121
|
+
msg.setIcon(QMessageBox.Critical)
|
|
122
|
+
msg.setText(f"Error saving overview:\n{str(e)}")
|
|
123
|
+
msg.setWindowTitle("Save Error")
|
|
124
|
+
msg.exec_()
|
|
125
|
+
finally:
|
|
126
|
+
# Re-enable save button
|
|
127
|
+
if save_button:
|
|
128
|
+
save_button.setEnabled(True)
|
|
129
|
+
save_button.setText("💾 Save")
|
|
130
|
+
|
|
131
|
+
def _calculate_thumbnail_size(self):
|
|
132
|
+
"""Calculate appropriate thumbnail size based on number of images."""
|
|
133
|
+
if self.coordinates is None or len(self.coordinates) == 0:
|
|
134
|
+
return 50
|
|
135
|
+
|
|
136
|
+
num_images = len(self.coordinates)
|
|
137
|
+
|
|
138
|
+
# Adjust thumbnail size based on number of images
|
|
139
|
+
if num_images < 50:
|
|
140
|
+
return 80
|
|
141
|
+
elif num_images < 100:
|
|
142
|
+
return 60
|
|
143
|
+
elif num_images < 200:
|
|
144
|
+
return 40
|
|
145
|
+
else:
|
|
146
|
+
return 30
|
|
147
|
+
|
|
148
|
+
def _setup_ui(self):
|
|
149
|
+
"""Set up the UI components."""
|
|
150
|
+
# Get screen size for figure sizing
|
|
151
|
+
screen = QGuiApplication.primaryScreen().geometry()
|
|
152
|
+
print(f"[DEBUG] Screen size: {screen.width()}x{screen.height()}")
|
|
153
|
+
|
|
154
|
+
# Create main layout
|
|
155
|
+
main_layout = QVBoxLayout(self)
|
|
156
|
+
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
157
|
+
|
|
158
|
+
# Create button layout for buttons (top right)
|
|
159
|
+
button_layout = QHBoxLayout()
|
|
160
|
+
button_layout.setContentsMargins(10, 10, 10, 10)
|
|
161
|
+
button_layout.addStretch() # Push buttons to the right
|
|
162
|
+
|
|
163
|
+
# Create save button
|
|
164
|
+
save_button = QPushButton("💾 Save")
|
|
165
|
+
save_button.setObjectName("save_button") # Set object name for later reference
|
|
166
|
+
save_button.setStyleSheet("""
|
|
167
|
+
QPushButton {
|
|
168
|
+
background-color: #4CAF50;
|
|
169
|
+
color: white;
|
|
170
|
+
border: 2px solid #388E3C;
|
|
171
|
+
border-radius: 5px;
|
|
172
|
+
padding: 8px 16px;
|
|
173
|
+
font-size: 14px;
|
|
174
|
+
font-weight: bold;
|
|
175
|
+
}
|
|
176
|
+
QPushButton:hover {
|
|
177
|
+
background-color: #388E3C;
|
|
178
|
+
}
|
|
179
|
+
QPushButton:pressed {
|
|
180
|
+
background-color: #2E7D32;
|
|
181
|
+
}
|
|
182
|
+
QPushButton:disabled {
|
|
183
|
+
background-color: #CCCCCC;
|
|
184
|
+
color: #666666;
|
|
185
|
+
}
|
|
186
|
+
""")
|
|
187
|
+
save_button.clicked.connect(self.save_overview)
|
|
188
|
+
save_button.setFixedSize(100, 35)
|
|
189
|
+
button_layout.addWidget(save_button)
|
|
190
|
+
|
|
191
|
+
# Create close button
|
|
192
|
+
close_button = QPushButton("✕ Close")
|
|
193
|
+
close_button.setStyleSheet("""
|
|
194
|
+
QPushButton {
|
|
195
|
+
background-color: #F44336;
|
|
196
|
+
color: white;
|
|
197
|
+
border: 2px solid #D32F2F;
|
|
198
|
+
border-radius: 5px;
|
|
199
|
+
padding: 8px 16px;
|
|
200
|
+
font-size: 14px;
|
|
201
|
+
font-weight: bold;
|
|
202
|
+
}
|
|
203
|
+
QPushButton:hover {
|
|
204
|
+
background-color: #D32F2F;
|
|
205
|
+
}
|
|
206
|
+
QPushButton:pressed {
|
|
207
|
+
background-color: #B71C1C;
|
|
208
|
+
}
|
|
209
|
+
""")
|
|
210
|
+
close_button.clicked.connect(self.close)
|
|
211
|
+
close_button.setFixedSize(100, 35)
|
|
212
|
+
button_layout.addWidget(close_button)
|
|
213
|
+
|
|
214
|
+
main_layout.addLayout(button_layout)
|
|
215
|
+
|
|
216
|
+
# Create matplotlib figure and canvas (full screen size)
|
|
217
|
+
self.figure = Figure(figsize=(screen.width()/100, screen.height()/100), dpi=100)
|
|
218
|
+
self.canvas = FigureCanvas(self.figure)
|
|
219
|
+
main_layout.addWidget(self.canvas)
|
|
220
|
+
|
|
221
|
+
# Set window flags BEFORE creating widgets to ensure it appears on top
|
|
222
|
+
# Note: Window flags should be set before adding widgets
|
|
223
|
+
print(f"[DEBUG] Setting window flags...")
|
|
224
|
+
|
|
225
|
+
def _create_overview_plot(self):
|
|
226
|
+
"""Create the overview plot with image thumbnails."""
|
|
227
|
+
if self.coordinates is None or self.coordinates.empty:
|
|
228
|
+
self.ax = self.figure.add_subplot(111)
|
|
229
|
+
self.ax.text(0.5, 0.5, 'No coordinates available',
|
|
230
|
+
ha='center', va='center', transform=self.ax.transAxes)
|
|
231
|
+
self.canvas.draw()
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
self.ax = self.figure.add_subplot(111)
|
|
235
|
+
self.ax.set_xlabel('X (mm)', fontsize=32) # Doubled from 16
|
|
236
|
+
self.ax.set_ylabel('Y (mm)', fontsize=32) # Doubled from 16
|
|
237
|
+
|
|
238
|
+
# Double the font size for tick labels (xtick and ytick)
|
|
239
|
+
self.ax.tick_params(axis='both', which='major', labelsize=32) # Doubled from default ~16
|
|
240
|
+
|
|
241
|
+
# Get coordinates
|
|
242
|
+
x_coords = self.coordinates['X'].values
|
|
243
|
+
y_coords = self.coordinates['Y'].values
|
|
244
|
+
|
|
245
|
+
# Apply *10 scaling factor for display (convert cm to mm)
|
|
246
|
+
x_coords_scaled = x_coords * 10
|
|
247
|
+
y_coords_scaled = y_coords * 10
|
|
248
|
+
|
|
249
|
+
# Calculate radius for scaling (also scaled by 10)
|
|
250
|
+
max_val = max(abs(x_coords_scaled).max(), abs(y_coords_scaled).max())
|
|
251
|
+
|
|
252
|
+
if pd.isna(max_val) or not np.isfinite(max_val):
|
|
253
|
+
radius = 100 # 10 * 10
|
|
254
|
+
elif max_val <= 50: # 5 * 10
|
|
255
|
+
radius = 50
|
|
256
|
+
elif max_val <= 75: # 7.5 * 10
|
|
257
|
+
radius = 75
|
|
258
|
+
elif max_val <= 100: # 10 * 10
|
|
259
|
+
radius = 100
|
|
260
|
+
elif max_val <= 150: # 15 * 10
|
|
261
|
+
radius = 150
|
|
262
|
+
else:
|
|
263
|
+
radius = max_val
|
|
264
|
+
|
|
265
|
+
# Set limits (scaled by 10)
|
|
266
|
+
self.ax.set_xlim(-radius - 10, radius + 10)
|
|
267
|
+
self.ax.set_ylim(-radius - 10, radius + 10)
|
|
268
|
+
|
|
269
|
+
# Draw circle (radius scaled by 10)
|
|
270
|
+
circle = plt.Circle((0, 0), radius, color='black', fill=False, linewidth=1)
|
|
271
|
+
self.ax.add_patch(circle)
|
|
272
|
+
self.ax.set_aspect('equal')
|
|
273
|
+
|
|
274
|
+
# No grid (removed)
|
|
275
|
+
|
|
276
|
+
# Place image thumbnails at each coordinate (using scaled coordinates)
|
|
277
|
+
self._place_image_thumbnails(x_coords_scaled, y_coords_scaled, radius)
|
|
278
|
+
|
|
279
|
+
self.figure.subplots_adjust(left=0.1, right=0.95, top=0.95, bottom=0.1)
|
|
280
|
+
self.canvas.draw()
|
|
281
|
+
|
|
282
|
+
def _place_image_thumbnails(self, x_coords, y_coords, radius):
|
|
283
|
+
"""
|
|
284
|
+
Place image thumbnails at each coordinate position.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
x_coords: Array of X coordinates
|
|
288
|
+
y_coords: Array of Y coordinates
|
|
289
|
+
radius: Radius for scaling
|
|
290
|
+
"""
|
|
291
|
+
if not self.image_list or len(self.image_list) == 0:
|
|
292
|
+
# If no images, just show points
|
|
293
|
+
self.ax.scatter(x_coords, y_coords, color='blue', marker='o', s=50, alpha=0.5)
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
# Calculate image spacing to avoid overlap
|
|
297
|
+
# Estimate spacing based on radius and number of points
|
|
298
|
+
num_points = len(x_coords)
|
|
299
|
+
spacing_factor = (2 * radius) / max(num_points ** 0.5, 1)
|
|
300
|
+
|
|
301
|
+
# Place thumbnails
|
|
302
|
+
for idx, (x, y) in enumerate(zip(x_coords, y_coords)):
|
|
303
|
+
try:
|
|
304
|
+
# Get the corresponding image
|
|
305
|
+
if self.is_complus4t_mode:
|
|
306
|
+
# COMPLUS4T: use defect_id to get image
|
|
307
|
+
defect_id = int(self.coordinates.iloc[idx]['defect_id'])
|
|
308
|
+
image_idx = defect_id - 1 # defect_id starts at 1, index starts at 0
|
|
309
|
+
else:
|
|
310
|
+
# Normal mode: use image_type and number_type if available
|
|
311
|
+
if self.image_type is not None and self.number_type is not None:
|
|
312
|
+
image_idx = self.image_type + (idx * self.number_type)
|
|
313
|
+
else:
|
|
314
|
+
# Fallback: try to get from button_frame
|
|
315
|
+
if self.button_frame:
|
|
316
|
+
result = self.button_frame.get_selected_image()
|
|
317
|
+
if result is not None:
|
|
318
|
+
img_type, num_type = result
|
|
319
|
+
image_idx = img_type + (idx * num_type)
|
|
320
|
+
else:
|
|
321
|
+
image_idx = idx
|
|
322
|
+
else:
|
|
323
|
+
image_idx = idx
|
|
324
|
+
|
|
325
|
+
# Check if image index is valid
|
|
326
|
+
if 0 <= image_idx < len(self.image_list):
|
|
327
|
+
pil_image = self.image_list[image_idx]
|
|
328
|
+
|
|
329
|
+
# Resize image to thumbnail size
|
|
330
|
+
thumbnail = pil_image.resize(
|
|
331
|
+
(self.thumbnail_size, self.thumbnail_size),
|
|
332
|
+
Image.Resampling.LANCZOS
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Convert PIL to numpy array
|
|
336
|
+
img_array = np.array(thumbnail.convert('RGB'))
|
|
337
|
+
|
|
338
|
+
# Create OffsetImage for matplotlib
|
|
339
|
+
im = OffsetImage(img_array, zoom=1.0)
|
|
340
|
+
|
|
341
|
+
# Create AnnotationBbox to place image at coordinate
|
|
342
|
+
ab = AnnotationBbox(im, (x, y), frameon=False, pad=0)
|
|
343
|
+
self.ax.add_artist(ab)
|
|
344
|
+
else:
|
|
345
|
+
# If image not available, show a small point
|
|
346
|
+
self.ax.scatter([x], [y], color='gray', marker='o', s=20, alpha=0.5)
|
|
347
|
+
|
|
348
|
+
except Exception as e:
|
|
349
|
+
# If error loading image, show a small point
|
|
350
|
+
print(f"Error loading image for coordinate ({x}, {y}): {e}")
|
|
351
|
+
self.ax.scatter([x], [y], color='gray', marker='o', s=20, alpha=0.5)
|
|
352
|
+
|
|
353
|
+
# Also show points for reference (semi-transparent)
|
|
354
|
+
self.ax.scatter(x_coords, y_coords, color='red', marker='o', s=5, alpha=0.2)
|
|
355
|
+
|
semapp/Plot/styles.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Styles definitions for the GUI components."""
|
|
2
|
+
|
|
3
|
+
# Button styles
|
|
4
|
+
OPEN_BUTTON_STYLE = """
|
|
5
|
+
QPushButton {
|
|
6
|
+
font-size: 16px;
|
|
7
|
+
background-color: #ffcc80;
|
|
8
|
+
border: 2px solid #8c8c8c;
|
|
9
|
+
border-radius: 10px;
|
|
10
|
+
padding: 5px;
|
|
11
|
+
height: 50px;
|
|
12
|
+
}
|
|
13
|
+
QPushButton:hover {
|
|
14
|
+
background-color: #ffb74d;
|
|
15
|
+
}
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
SMALL_BUTTON_STYLE = """
|
|
19
|
+
QPushButton {
|
|
20
|
+
font-size: 11px;
|
|
21
|
+
background-color: #ffcc80;
|
|
22
|
+
border: 2px solid #8c8c8c;
|
|
23
|
+
border-radius: 5px;
|
|
24
|
+
padding: 3px;
|
|
25
|
+
height: 30px;
|
|
26
|
+
max-width: 120px;
|
|
27
|
+
}
|
|
28
|
+
QPushButton:hover {
|
|
29
|
+
background-color: #ffb74d;
|
|
30
|
+
}
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# Message box styles
|
|
34
|
+
MESSAGE_BOX_STYLE = """
|
|
35
|
+
QMessageBox {
|
|
36
|
+
background-color: white;
|
|
37
|
+
}
|
|
38
|
+
QMessageBox QLabel {
|
|
39
|
+
color: #333;
|
|
40
|
+
font-size: 14px;
|
|
41
|
+
}
|
|
42
|
+
QPushButton {
|
|
43
|
+
background-color: #ffcc80;
|
|
44
|
+
border: 2px solid #8c8c8c;
|
|
45
|
+
border-radius: 5px;
|
|
46
|
+
padding: 5px 15px;
|
|
47
|
+
font-size: 12px;
|
|
48
|
+
}
|
|
49
|
+
QPushButton:hover {
|
|
50
|
+
background-color: #ffb74d;
|
|
51
|
+
}
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
# Frame styles
|
|
55
|
+
FRAME_STYLE = "background-color: white;"
|