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
semapp/Plot/utils.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for frame management and screenshot functionality.
|
|
3
|
+
|
|
4
|
+
This module provides functions to create save buttons and manage frame layouts
|
|
5
|
+
in the application. It includes functionality for capturing and saving
|
|
6
|
+
combined screenshots of multiple frames.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import csv
|
|
11
|
+
import pandas as pd
|
|
12
|
+
from PyQt5.QtCore import Qt
|
|
13
|
+
from PyQt5.QtWidgets import QPushButton, QFileDialog
|
|
14
|
+
from PyQt5.QtGui import QPixmap, QPainter
|
|
15
|
+
|
|
16
|
+
# Style constants
|
|
17
|
+
SAVE_BUTTON_STYLE = """
|
|
18
|
+
QPushButton {
|
|
19
|
+
font-size: 16px;
|
|
20
|
+
background-color: #e1bee7;
|
|
21
|
+
border: 2px solid #8c8c8c;
|
|
22
|
+
border-radius: 10px;
|
|
23
|
+
padding: 5px;
|
|
24
|
+
height: 20px;
|
|
25
|
+
}
|
|
26
|
+
QPushButton:hover {
|
|
27
|
+
background-color: #ce93d8;
|
|
28
|
+
}
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
BUTTON_POSITION = {
|
|
32
|
+
'row': 5, # Shifted down by 1 row
|
|
33
|
+
'column': 0,
|
|
34
|
+
'row_span': 1,
|
|
35
|
+
'column_span': 6
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def update_merged_csv_results(wafer_folder, filename, result, image_size_um):
|
|
40
|
+
"""
|
|
41
|
+
Update the merged_threshold_results.csv file by replacing the row with the same filename.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
wafer_folder: Path to the wafer folder containing the CSV
|
|
45
|
+
filename: Name of the processed image file
|
|
46
|
+
result: Result dictionary from SEMThresholdProcessor.process_single_image
|
|
47
|
+
image_size_um: Image size in micrometers
|
|
48
|
+
"""
|
|
49
|
+
csv_path = os.path.join(wafer_folder, "merged_threshold_results.csv")
|
|
50
|
+
|
|
51
|
+
# Calculate statistics from the result
|
|
52
|
+
particles = result['detected_particles']
|
|
53
|
+
binary_image = result['binary_image']
|
|
54
|
+
|
|
55
|
+
# Calculate statistics for this file
|
|
56
|
+
import numpy as np
|
|
57
|
+
import cv2
|
|
58
|
+
|
|
59
|
+
mask_particles = np.zeros_like(binary_image, dtype=np.uint8)
|
|
60
|
+
for particle in particles:
|
|
61
|
+
cv2.fillPoly(mask_particles, [particle['contour']], 200)
|
|
62
|
+
|
|
63
|
+
num_particles = len(particles)
|
|
64
|
+
total_area_pixels = int(np.count_nonzero(mask_particles == 200))
|
|
65
|
+
avg_area_pixels = total_area_pixels / num_particles if num_particles > 0 else 0
|
|
66
|
+
|
|
67
|
+
# Calculate areas in µm²
|
|
68
|
+
height, width = binary_image.shape
|
|
69
|
+
pixel_area_um2 = (image_size_um ** 2) / (height * width)
|
|
70
|
+
|
|
71
|
+
total_area_um2 = total_area_pixels * pixel_area_um2
|
|
72
|
+
avg_area_um2 = avg_area_pixels * pixel_area_um2
|
|
73
|
+
total_area_percentage = (total_area_pixels / (height * width)) * 100
|
|
74
|
+
|
|
75
|
+
# Calculate density
|
|
76
|
+
surface_area_um2 = image_size_um ** 2
|
|
77
|
+
density = num_particles / surface_area_um2 if surface_area_um2 > 0 else 0
|
|
78
|
+
|
|
79
|
+
# Create new row data
|
|
80
|
+
new_row = {
|
|
81
|
+
'Filename': filename,
|
|
82
|
+
'Page_Index': 1, # Single image, so page index is 1
|
|
83
|
+
'Density': density,
|
|
84
|
+
'Avg_area_um2': avg_area_um2,
|
|
85
|
+
'Total_area_percentage': total_area_percentage,
|
|
86
|
+
'Num_particles': num_particles,
|
|
87
|
+
'Total_area_um2': total_area_um2
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Check if CSV file exists
|
|
91
|
+
if os.path.exists(csv_path):
|
|
92
|
+
# Read existing CSV
|
|
93
|
+
df = pd.read_csv(csv_path)
|
|
94
|
+
|
|
95
|
+
# Check if filename already exists
|
|
96
|
+
existing_row_index = df[df['Filename'] == filename].index
|
|
97
|
+
|
|
98
|
+
if len(existing_row_index) > 0:
|
|
99
|
+
# Replace existing row
|
|
100
|
+
df.loc[existing_row_index[0]] = new_row
|
|
101
|
+
print(f"Updated existing row for {filename} in merged_threshold_results.csv")
|
|
102
|
+
else:
|
|
103
|
+
# Add new row
|
|
104
|
+
df = pd.concat([df, pd.DataFrame([new_row])], ignore_index=True)
|
|
105
|
+
print(f"Added new row for {filename} in merged_threshold_results.csv")
|
|
106
|
+
|
|
107
|
+
# Save updated CSV
|
|
108
|
+
df.to_csv(csv_path, index=False)
|
|
109
|
+
print(f"Updated CSV saved: {csv_path}")
|
|
110
|
+
else:
|
|
111
|
+
# Create new CSV file
|
|
112
|
+
df = pd.DataFrame([new_row])
|
|
113
|
+
df.to_csv(csv_path, index=False)
|
|
114
|
+
print(f"Created new CSV file: {csv_path}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def create_savebutton(layout, frame_left, frame_right, button_frame=None):
|
|
118
|
+
"""
|
|
119
|
+
Create save buttons to capture frames and process images.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
layout: The layout where the save buttons will be added
|
|
123
|
+
frame_left: The left frame to capture
|
|
124
|
+
frame_right: The right frame to capture
|
|
125
|
+
button_frame: Reference to button frame for accessing sliders and parameters
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
None
|
|
129
|
+
"""
|
|
130
|
+
def save_image():
|
|
131
|
+
"""
|
|
132
|
+
Capture and save specific frames as a combined image.
|
|
133
|
+
|
|
134
|
+
Creates a screenshot of both left and right frames, combines them
|
|
135
|
+
horizontally and saves the result as a PNG file.
|
|
136
|
+
"""
|
|
137
|
+
screen_left = frame_left.grab()
|
|
138
|
+
screen_right = frame_right.grab()
|
|
139
|
+
|
|
140
|
+
file_name, _ = QFileDialog.getSaveFileName(
|
|
141
|
+
parent=None,
|
|
142
|
+
caption="Save screenshot",
|
|
143
|
+
directory="",
|
|
144
|
+
filter="PNG Files (*.png);;All Files (*)"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if file_name:
|
|
148
|
+
combined_width = screen_left.width() + screen_right.width()
|
|
149
|
+
combined_height = max(screen_left.height(), screen_right.height())
|
|
150
|
+
combined_pixmap = QPixmap(combined_width, combined_height)
|
|
151
|
+
|
|
152
|
+
# Fill the combined QPixmap with a white background
|
|
153
|
+
combined_pixmap.fill(Qt.white)
|
|
154
|
+
|
|
155
|
+
painter = QPainter(combined_pixmap)
|
|
156
|
+
painter.drawPixmap(0, 0, screen_left)
|
|
157
|
+
painter.drawPixmap(screen_left.width(), 0, screen_right)
|
|
158
|
+
painter.end()
|
|
159
|
+
|
|
160
|
+
# Save the combined image
|
|
161
|
+
combined_pixmap.save(file_name, "PNG")
|
|
162
|
+
|
|
163
|
+
def save_processed_image():
|
|
164
|
+
"""
|
|
165
|
+
Process and save the current image using SEMThresholdProcessor.
|
|
166
|
+
"""
|
|
167
|
+
if not button_frame:
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
# Get parameters from sliders
|
|
171
|
+
threshold = button_frame.get_threshold_value()
|
|
172
|
+
min_size = button_frame.get_min_size_value()
|
|
173
|
+
|
|
174
|
+
# Get image size from settings data
|
|
175
|
+
image_size_um = 5.0 # Default value
|
|
176
|
+
if hasattr(button_frame, 'settings_window') and button_frame.settings_window:
|
|
177
|
+
settings_data = button_frame.settings_window.data
|
|
178
|
+
if settings_data and len(settings_data) > 0:
|
|
179
|
+
# Get the first entry's scale value
|
|
180
|
+
first_entry = settings_data[0]
|
|
181
|
+
if "Scale" in first_entry:
|
|
182
|
+
scale_str = first_entry["Scale"]
|
|
183
|
+
# Extract numeric value from scale (e.g., "5x5" -> 5.0)
|
|
184
|
+
try:
|
|
185
|
+
# Handle formats like "5x5", "5", "5.0x5.0"
|
|
186
|
+
if 'x' in scale_str:
|
|
187
|
+
image_size_um = float(scale_str.split('x')[0])
|
|
188
|
+
else:
|
|
189
|
+
image_size_um = float(scale_str)
|
|
190
|
+
except (ValueError, IndexError):
|
|
191
|
+
image_size_um = 5.0 # Fallback to default
|
|
192
|
+
print(f"Using image size from settings: {image_size_um} um")
|
|
193
|
+
else:
|
|
194
|
+
print("No Scale found in settings data")
|
|
195
|
+
else:
|
|
196
|
+
print("No settings data available")
|
|
197
|
+
else:
|
|
198
|
+
print("No settings window available")
|
|
199
|
+
|
|
200
|
+
# Get selected wafer
|
|
201
|
+
selected_wafer = button_frame.get_selected_option()
|
|
202
|
+
if not selected_wafer:
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
# Get current image path from plot frame
|
|
206
|
+
current_image_path = None
|
|
207
|
+
if hasattr(button_frame, 'plot_frame') and button_frame.plot_frame:
|
|
208
|
+
current_image_path = button_frame.plot_frame.get_current_image_path()
|
|
209
|
+
|
|
210
|
+
if current_image_path:
|
|
211
|
+
# Get current position from plot frame
|
|
212
|
+
current_x = None
|
|
213
|
+
current_y = None
|
|
214
|
+
if hasattr(button_frame, 'plot_frame') and button_frame.plot_frame:
|
|
215
|
+
# Try to get the last clicked position
|
|
216
|
+
if hasattr(button_frame.plot_frame, 'last_clicked_position'):
|
|
217
|
+
current_x, current_y = button_frame.plot_frame.last_clicked_position
|
|
218
|
+
else:
|
|
219
|
+
print("No clicked position available")
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
if current_x is not None and current_y is not None:
|
|
223
|
+
# Create custom filename using template (without "pos")
|
|
224
|
+
filename = f"{image_size_um}_{current_x:.1f}_{current_y:.1f}.tiff"
|
|
225
|
+
|
|
226
|
+
# Save in wafer subfolder with custom filename
|
|
227
|
+
wafer_folder = os.path.join(button_frame.dirname, str(selected_wafer))
|
|
228
|
+
os.makedirs(wafer_folder, exist_ok=True)
|
|
229
|
+
custom_path = os.path.join(wafer_folder, filename)
|
|
230
|
+
|
|
231
|
+
# Get the currently displayed image (with threshold applied)
|
|
232
|
+
if hasattr(button_frame.plot_frame, 'image_list') and button_frame.plot_frame.image_list:
|
|
233
|
+
current_index = button_frame.plot_frame.current_index
|
|
234
|
+
if 0 <= current_index < len(button_frame.plot_frame.image_list):
|
|
235
|
+
# Get the processed image (with threshold applied)
|
|
236
|
+
processed_image = button_frame.plot_frame.image_list[current_index]
|
|
237
|
+
|
|
238
|
+
# Apply threshold processing to get the final image
|
|
239
|
+
thresholded_image = button_frame.plot_frame._apply_threshold(processed_image, threshold)
|
|
240
|
+
|
|
241
|
+
# Save the thresholded image directly
|
|
242
|
+
thresholded_image.save(custom_path)
|
|
243
|
+
print(f"Processed image saved to: {custom_path}")
|
|
244
|
+
|
|
245
|
+
# Now process with SEMThresholdProcessor to generate CSV
|
|
246
|
+
from semapp.Processing.threshold import SEMThresholdProcessor
|
|
247
|
+
processor = SEMThresholdProcessor(
|
|
248
|
+
threshold=threshold,
|
|
249
|
+
min_size=min_size,
|
|
250
|
+
image_size_um=image_size_um,
|
|
251
|
+
save_results=True,
|
|
252
|
+
verbose=True
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Process the saved image to generate CSV
|
|
256
|
+
result = processor.process_single_image(custom_path, show_result=False)
|
|
257
|
+
if result:
|
|
258
|
+
print(f"CSV results saved for: {custom_path}")
|
|
259
|
+
|
|
260
|
+
# Update the merged_threshold_results.csv file
|
|
261
|
+
update_merged_csv_results(wafer_folder, filename, result, image_size_um)
|
|
262
|
+
else:
|
|
263
|
+
print("Failed to generate CSV results")
|
|
264
|
+
else:
|
|
265
|
+
print("Invalid image index")
|
|
266
|
+
else:
|
|
267
|
+
print("No image list available")
|
|
268
|
+
else:
|
|
269
|
+
print("No valid position coordinates available")
|
|
270
|
+
|
|
271
|
+
# Create and configure the save buttons
|
|
272
|
+
processed_button = QPushButton("Save processed image")
|
|
273
|
+
processed_button.setStyleSheet(SAVE_BUTTON_STYLE)
|
|
274
|
+
processed_button.clicked.connect(save_processed_image)
|
|
275
|
+
|
|
276
|
+
save_button = QPushButton("Screenshot")
|
|
277
|
+
save_button.setStyleSheet(SAVE_BUTTON_STYLE)
|
|
278
|
+
save_button.clicked.connect(save_image)
|
|
279
|
+
|
|
280
|
+
# Add buttons to the layout
|
|
281
|
+
layout.addWidget(
|
|
282
|
+
processed_button,
|
|
283
|
+
BUTTON_POSITION['row'],
|
|
284
|
+
BUTTON_POSITION['column'],
|
|
285
|
+
1, # row_span
|
|
286
|
+
3 # column_span (half width)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
layout.addWidget(
|
|
290
|
+
save_button,
|
|
291
|
+
BUTTON_POSITION['row'],
|
|
292
|
+
BUTTON_POSITION['column'] + 3, # Start after processed button
|
|
293
|
+
1, # row_span
|
|
294
|
+
3 # column_span (half width)
|
|
295
|
+
)
|