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,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;"