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,686 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for processing and renaming TIFF files based on CSV coordinates.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import glob
|
|
8
|
+
import shutil
|
|
9
|
+
import os
|
|
10
|
+
from PIL import Image
|
|
11
|
+
import pandas as pd
|
|
12
|
+
from semapp.Processing.klarf_reader import extract_positions
|
|
13
|
+
from semapp.Processing.rename_tif import rename_files, rename_files_all
|
|
14
|
+
from semapp.Processing.split_tif import split_tiff, split_tiff_all
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Process:
|
|
18
|
+
"""
|
|
19
|
+
A class to handle processing of TIFF files and renaming them based on
|
|
20
|
+
coordinates from KLARF files.
|
|
21
|
+
|
|
22
|
+
This class supports three modes:
|
|
23
|
+
- Normal mode: Standard KLARF format with subdirectories
|
|
24
|
+
- COMPLUS4T mode: Multi-wafer KLARF files in parent directory
|
|
25
|
+
- KRONOS mode: Special format with OCR-based number detection
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, dirname, wafer=None, scale=None):
|
|
29
|
+
"""
|
|
30
|
+
Initialize the processing instance with necessary parameters.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
dirname (str): The base directory for the files.
|
|
34
|
+
wafer (int, optional): The wafer number. Defaults to None.
|
|
35
|
+
scale (str, optional): The path to the settings JSON file. Defaults to None.
|
|
36
|
+
"""
|
|
37
|
+
self.dirname = dirname
|
|
38
|
+
self.scale_data = scale
|
|
39
|
+
self.wafer_number = str(wafer)
|
|
40
|
+
self.tiff_path = None
|
|
41
|
+
self.coordinates = None
|
|
42
|
+
self.settings = None
|
|
43
|
+
self.output_dir = None
|
|
44
|
+
self.load_json()
|
|
45
|
+
def load_json(self):
|
|
46
|
+
"""
|
|
47
|
+
Load the settings data from a JSON file.
|
|
48
|
+
|
|
49
|
+
The settings file should contain a list of dictionaries with
|
|
50
|
+
'Scale' and 'Image Type' keys. If the file is not found or
|
|
51
|
+
invalid, an empty list is used.
|
|
52
|
+
"""
|
|
53
|
+
if not self.scale_data:
|
|
54
|
+
self.settings = []
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
with open(self.scale_data, "r", encoding="utf-8") as file:
|
|
59
|
+
self.settings = json.load(file)
|
|
60
|
+
except FileNotFoundError:
|
|
61
|
+
# Settings file not found, starting fresh
|
|
62
|
+
self.settings = []
|
|
63
|
+
except json.JSONDecodeError:
|
|
64
|
+
# JSON decoding error
|
|
65
|
+
self.settings = []
|
|
66
|
+
except OSError:
|
|
67
|
+
# OS error when reading file
|
|
68
|
+
self.settings = []
|
|
69
|
+
def extract_positions(self, filepath):
|
|
70
|
+
"""
|
|
71
|
+
Extract defect positions from KLARF file.
|
|
72
|
+
|
|
73
|
+
Wrapper method that calls the klarf_reader module. Automatically
|
|
74
|
+
detects the mode (KRONOS, COMPLUS4T, or normal) and extracts
|
|
75
|
+
coordinates accordingly.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
filepath (str): Path to the KLARF (.001) file
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
pd.DataFrame: DataFrame with columns ["defect_id", "X", "Y", "defect_size"]
|
|
82
|
+
Returns empty DataFrame if file cannot be parsed.
|
|
83
|
+
"""
|
|
84
|
+
# Convert self.wafer_number to int if it's not None (for COMPLUS4T mode)
|
|
85
|
+
wafer_id = None
|
|
86
|
+
if self.wafer_number and self.wafer_number != "None":
|
|
87
|
+
try:
|
|
88
|
+
wafer_id = int(self.wafer_number)
|
|
89
|
+
except (ValueError, TypeError):
|
|
90
|
+
wafer_id = None
|
|
91
|
+
|
|
92
|
+
# Call the klarf_reader function
|
|
93
|
+
self.coordinates = extract_positions(filepath, wafer_id=wafer_id)
|
|
94
|
+
return self.coordinates
|
|
95
|
+
|
|
96
|
+
def rename(self):
|
|
97
|
+
"""
|
|
98
|
+
Rename TIFF files based on coordinates from KLARF file.
|
|
99
|
+
|
|
100
|
+
Wrapper method that calls the rename_tif module. Automatically
|
|
101
|
+
detects the mode and uses the appropriate renaming scheme:
|
|
102
|
+
- Normal mode: Uses scale and image type from settings
|
|
103
|
+
- COMPLUS4T mode: Uses only X and Y coordinates
|
|
104
|
+
- KRONOS mode: Uses only X and Y coordinates
|
|
105
|
+
|
|
106
|
+
The method verifies that no duplicate scale/image_type combinations
|
|
107
|
+
exist before proceeding.
|
|
108
|
+
"""
|
|
109
|
+
print("\n" + "="*80)
|
|
110
|
+
print("[DEBUG] rename() called")
|
|
111
|
+
print(f"[DEBUG] dirname: {self.dirname}")
|
|
112
|
+
print(f"[DEBUG] wafer_number: {self.wafer_number}")
|
|
113
|
+
print("="*80)
|
|
114
|
+
|
|
115
|
+
# Security check: verify no duplicate scale/image_type combinations
|
|
116
|
+
scale_image_combinations = []
|
|
117
|
+
for setting in self.settings:
|
|
118
|
+
combination = f"{setting['Scale']}_{setting['Image Type']}"
|
|
119
|
+
scale_image_combinations.append(combination)
|
|
120
|
+
|
|
121
|
+
# Check for duplicates
|
|
122
|
+
if len(scale_image_combinations) != len(set(scale_image_combinations)):
|
|
123
|
+
duplicate_combinations = []
|
|
124
|
+
seen = set()
|
|
125
|
+
for combo in scale_image_combinations:
|
|
126
|
+
if combo in seen:
|
|
127
|
+
duplicate_combinations.append(combo)
|
|
128
|
+
else:
|
|
129
|
+
seen.add(combo)
|
|
130
|
+
|
|
131
|
+
print(f"Warning: Duplicate scale/image_type combinations found: {duplicate_combinations}. "
|
|
132
|
+
f"Skipping rename operation.")
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
self.output_dir = os.path.join(self.dirname, self.wafer_number)
|
|
136
|
+
print(f"[DEBUG] output_dir: {self.output_dir}")
|
|
137
|
+
|
|
138
|
+
if not os.path.exists(self.output_dir):
|
|
139
|
+
print(f"[DEBUG] output_dir does not exist, returning")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
# Check if COMPLUS4T mode: .001 files are in parent directory
|
|
143
|
+
is_complus4t = self._check_complus4t_mode()
|
|
144
|
+
print(f"[DEBUG] is_complus4t: {is_complus4t}")
|
|
145
|
+
|
|
146
|
+
if is_complus4t:
|
|
147
|
+
# COMPLUS4T mode: .001 file is in parent directory
|
|
148
|
+
matching_files = glob.glob(os.path.join(self.dirname, '*.001'))
|
|
149
|
+
print(f"[DEBUG] COMPLUS4T: Found {len(matching_files)} .001 files in parent directory")
|
|
150
|
+
recipe_path = None
|
|
151
|
+
|
|
152
|
+
# Find the .001 file that contains the selected wafer ID
|
|
153
|
+
for file_path in matching_files:
|
|
154
|
+
print(f"[DEBUG] Checking file: {os.path.basename(file_path)} for wafer {self.wafer_number}")
|
|
155
|
+
if self._is_wafer_in_klarf(file_path, int(self.wafer_number)):
|
|
156
|
+
recipe_path = file_path
|
|
157
|
+
print(f"[DEBUG] Found matching .001 file: {recipe_path}")
|
|
158
|
+
break
|
|
159
|
+
else:
|
|
160
|
+
# Normal/KRONOS mode: .001 file is in wafer subdirectory
|
|
161
|
+
matching_files = glob.glob(os.path.join(self.output_dir, '*.001'))
|
|
162
|
+
print(f"[DEBUG] Normal/KRONOS: Found {len(matching_files)} .001 files in output_dir")
|
|
163
|
+
if matching_files:
|
|
164
|
+
recipe_path = matching_files[0]
|
|
165
|
+
print(f"[DEBUG] Using .001 file: {recipe_path}")
|
|
166
|
+
else:
|
|
167
|
+
recipe_path = None
|
|
168
|
+
|
|
169
|
+
if recipe_path:
|
|
170
|
+
# Check if KRONOS mode
|
|
171
|
+
is_kronos = self._check_kronos_mode(recipe_path)
|
|
172
|
+
print(f"[DEBUG] is_kronos: {is_kronos}")
|
|
173
|
+
# For COMPLUS4T, pass wafer_id explicitly. For normal mode, pass None
|
|
174
|
+
if is_complus4t:
|
|
175
|
+
# Temporarily store original wafer_number and set it for extract_positions
|
|
176
|
+
original_wafer_number = self.wafer_number
|
|
177
|
+
print(f"[DEBUG] COMPLUS4T: Setting wafer_number to {self.wafer_number} for extract_positions")
|
|
178
|
+
self.wafer_number = str(self.wafer_number)
|
|
179
|
+
self.coordinates = self.extract_positions(recipe_path)
|
|
180
|
+
self.wafer_number = original_wafer_number
|
|
181
|
+
print(f"[DEBUG] COMPLUS4T: Extracted {len(self.coordinates)} coordinates")
|
|
182
|
+
else:
|
|
183
|
+
# Normal mode: set wafer_number to None so extract_positions reads all defects
|
|
184
|
+
original_wafer_number = self.wafer_number
|
|
185
|
+
print(f"[DEBUG] Normal/KRONOS: Setting wafer_number to None for extract_positions")
|
|
186
|
+
self.wafer_number = None
|
|
187
|
+
self.coordinates = self.extract_positions(recipe_path)
|
|
188
|
+
self.wafer_number = original_wafer_number
|
|
189
|
+
print(f"[DEBUG] Normal/KRONOS: Extracted {len(self.coordinates)} coordinates")
|
|
190
|
+
else:
|
|
191
|
+
print(f"[DEBUG] Warning: No .001 file found for wafer {self.wafer_number}")
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
# Check if KRONOS mode (if not already checked)
|
|
195
|
+
if not is_complus4t and recipe_path:
|
|
196
|
+
is_kronos = self._check_kronos_mode(recipe_path)
|
|
197
|
+
else:
|
|
198
|
+
is_kronos = False
|
|
199
|
+
|
|
200
|
+
print(f"[DEBUG] Final is_kronos: {is_kronos}")
|
|
201
|
+
print(f"[DEBUG] Coordinates DataFrame:")
|
|
202
|
+
print(self.coordinates)
|
|
203
|
+
|
|
204
|
+
# Call the rename_tif module function
|
|
205
|
+
renamed_count = rename_files(
|
|
206
|
+
output_dir=self.output_dir,
|
|
207
|
+
coordinates=self.coordinates,
|
|
208
|
+
settings=self.settings,
|
|
209
|
+
is_kronos=is_kronos,
|
|
210
|
+
is_complus4t=is_complus4t
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
print(f"[DEBUG] Total files renamed: {renamed_count}")
|
|
214
|
+
print("="*80 + "\n")
|
|
215
|
+
|
|
216
|
+
def split_tiff(self):
|
|
217
|
+
"""
|
|
218
|
+
Split a merged TIFF file into individual TIFF files.
|
|
219
|
+
Wrapper method that calls the split_tif module.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
list: List of file paths of the generated TIFF files.
|
|
223
|
+
"""
|
|
224
|
+
# Check if COMPLUS4T mode: TIFF file is in parent directory
|
|
225
|
+
is_complus4t = self._check_complus4t_mode()
|
|
226
|
+
|
|
227
|
+
if is_complus4t:
|
|
228
|
+
# COMPLUS4T mode: TIFF file is in parent directory
|
|
229
|
+
tiff_files = glob.glob(os.path.join(self.dirname, '*.tiff'))
|
|
230
|
+
if not tiff_files:
|
|
231
|
+
tiff_files = glob.glob(os.path.join(self.dirname, '*.tif'))
|
|
232
|
+
if tiff_files:
|
|
233
|
+
self.tiff_path = tiff_files[0] # Use first TIFF file found
|
|
234
|
+
else:
|
|
235
|
+
return []
|
|
236
|
+
# Output directory is the wafer subdirectory
|
|
237
|
+
output_dir = os.path.join(self.dirname, self.wafer_number)
|
|
238
|
+
|
|
239
|
+
# For COMPLUS4T: extract positions first to get defect_id list
|
|
240
|
+
matching_files = glob.glob(os.path.join(self.dirname, '*.001'))
|
|
241
|
+
recipe_path = None
|
|
242
|
+
|
|
243
|
+
# Find the .001 file that contains the selected wafer ID
|
|
244
|
+
for file_path in matching_files:
|
|
245
|
+
if self._is_wafer_in_klarf(file_path, int(self.wafer_number)):
|
|
246
|
+
recipe_path = file_path
|
|
247
|
+
break
|
|
248
|
+
|
|
249
|
+
if not recipe_path:
|
|
250
|
+
print(f"Warning: No .001 file found for wafer {self.wafer_number}")
|
|
251
|
+
return []
|
|
252
|
+
|
|
253
|
+
# Extract coordinates to get defect_id list
|
|
254
|
+
self.coordinates = self.extract_positions(recipe_path)
|
|
255
|
+
|
|
256
|
+
if self.coordinates is None or self.coordinates.empty:
|
|
257
|
+
print(f"Warning: No coordinates found for wafer {self.wafer_number}")
|
|
258
|
+
return []
|
|
259
|
+
else:
|
|
260
|
+
# Normal/KRONOS mode: TIFF file is in wafer subdirectory
|
|
261
|
+
self.tiff_path = os.path.join(self.dirname,
|
|
262
|
+
self.wafer_number,
|
|
263
|
+
"data.tif")
|
|
264
|
+
output_dir = os.path.join(self.dirname, self.wafer_number)
|
|
265
|
+
self.coordinates = None # Not needed for normal/KRONOS mode
|
|
266
|
+
|
|
267
|
+
# Check if KRONOS mode
|
|
268
|
+
is_kronos = False
|
|
269
|
+
if not is_complus4t:
|
|
270
|
+
# Check for KRONOS in the wafer subdirectory
|
|
271
|
+
matching_files = glob.glob(os.path.join(output_dir, '*.001'))
|
|
272
|
+
if matching_files:
|
|
273
|
+
is_kronos = self._check_kronos_mode(matching_files[0])
|
|
274
|
+
|
|
275
|
+
# Call the split_tif module function
|
|
276
|
+
output_files = split_tiff(
|
|
277
|
+
tiff_path=self.tiff_path,
|
|
278
|
+
output_dir=output_dir,
|
|
279
|
+
coordinates=self.coordinates,
|
|
280
|
+
is_kronos=is_kronos,
|
|
281
|
+
is_complus4t=is_complus4t
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return output_files
|
|
285
|
+
|
|
286
|
+
def clean(self):
|
|
287
|
+
"""
|
|
288
|
+
Clean up the output directory by deleting any non-conforming TIFF files.
|
|
289
|
+
This method deletes any files that do not follow the expected naming
|
|
290
|
+
conventions (files not starting with "data" or
|
|
291
|
+
containing the word "page").
|
|
292
|
+
"""
|
|
293
|
+
self.output_dir = os.path.join(self.dirname, self.wafer_number)
|
|
294
|
+
|
|
295
|
+
if not os.path.exists(self.output_dir):
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
tiff_files = [f for f in os.listdir(self.output_dir)
|
|
299
|
+
if f.lower().endswith(('.tiff', '.tif'))]
|
|
300
|
+
|
|
301
|
+
# Delete non-conforming files
|
|
302
|
+
for file_name in tiff_files:
|
|
303
|
+
if not file_name.startswith("data") or "page" in file_name.lower() or file_name.endswith("001"):
|
|
304
|
+
file_path = os.path.join(self.output_dir, file_name)
|
|
305
|
+
os.remove(file_path)
|
|
306
|
+
|
|
307
|
+
def split_tiff_all(self):
|
|
308
|
+
"""
|
|
309
|
+
Split all merged TIFF files in the directory (including subdirectories)
|
|
310
|
+
into individual TIFF files.
|
|
311
|
+
Wrapper method that calls the split_tif module.
|
|
312
|
+
|
|
313
|
+
This method will look through all directories and split each `data.tif`
|
|
314
|
+
file into separate pages.
|
|
315
|
+
"""
|
|
316
|
+
# Check if COMPLUS4T mode
|
|
317
|
+
is_complus4t = self._check_complus4t_mode()
|
|
318
|
+
|
|
319
|
+
coordinates_dict = None
|
|
320
|
+
is_kronos = False
|
|
321
|
+
|
|
322
|
+
if is_complus4t:
|
|
323
|
+
# COMPLUS4T mode: need to extract coordinates for all wafers
|
|
324
|
+
matching_files = glob.glob(os.path.join(self.dirname, '*.001'))
|
|
325
|
+
if not matching_files:
|
|
326
|
+
print("Warning: No .001 file found in parent directory for COMPLUS4T mode")
|
|
327
|
+
return []
|
|
328
|
+
|
|
329
|
+
parent_recipe_path = matching_files[0]
|
|
330
|
+
coordinates_dict = {}
|
|
331
|
+
|
|
332
|
+
# Extract coordinates for each wafer
|
|
333
|
+
for subdir, _, _ in os.walk(self.dirname):
|
|
334
|
+
if subdir == self.dirname:
|
|
335
|
+
continue
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
wafer_num = int(os.path.basename(subdir))
|
|
339
|
+
except ValueError:
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
original_wafer_number = self.wafer_number
|
|
343
|
+
self.wafer_number = str(wafer_num)
|
|
344
|
+
coordinates = self.extract_positions(parent_recipe_path)
|
|
345
|
+
self.wafer_number = original_wafer_number
|
|
346
|
+
|
|
347
|
+
if coordinates is not None and not coordinates.empty:
|
|
348
|
+
coordinates_dict[wafer_num] = coordinates
|
|
349
|
+
else:
|
|
350
|
+
# Normal/KRONOS mode: check for KRONOS in first subdirectory
|
|
351
|
+
for subdir, _, _ in os.walk(self.dirname):
|
|
352
|
+
if subdir == self.dirname:
|
|
353
|
+
continue
|
|
354
|
+
|
|
355
|
+
matching_files = glob.glob(os.path.join(subdir, '*.001'))
|
|
356
|
+
if matching_files:
|
|
357
|
+
is_kronos = self._check_kronos_mode(matching_files[0])
|
|
358
|
+
break
|
|
359
|
+
|
|
360
|
+
# Call the split_tif module function
|
|
361
|
+
output_files = split_tiff_all(
|
|
362
|
+
dirname=self.dirname,
|
|
363
|
+
coordinates_dict=coordinates_dict,
|
|
364
|
+
is_kronos=is_kronos,
|
|
365
|
+
is_complus4t=is_complus4t
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
return output_files
|
|
369
|
+
|
|
370
|
+
def rename_all(self):
|
|
371
|
+
"""
|
|
372
|
+
Rename all TIFF files based on the coordinates from the
|
|
373
|
+
CSV file in all subdirectories.
|
|
374
|
+
Wrapper method that calls the rename_tif module.
|
|
375
|
+
|
|
376
|
+
This method will iterate through all subdirectories,
|
|
377
|
+
loading the CSV and settings, and renaming files accordingly.
|
|
378
|
+
"""
|
|
379
|
+
print("\n" + "="*80)
|
|
380
|
+
print("[DEBUG] rename_all() called")
|
|
381
|
+
print(f"[DEBUG] dirname: {self.dirname}")
|
|
382
|
+
print("="*80)
|
|
383
|
+
|
|
384
|
+
# Security check: verify no duplicate scale/image_type combinations
|
|
385
|
+
scale_image_combinations = []
|
|
386
|
+
for setting in self.settings:
|
|
387
|
+
combination = f"{setting['Scale']}_{setting['Image Type']}"
|
|
388
|
+
scale_image_combinations.append(combination)
|
|
389
|
+
|
|
390
|
+
# Check for duplicates
|
|
391
|
+
if len(scale_image_combinations) != len(set(scale_image_combinations)):
|
|
392
|
+
duplicate_combinations = []
|
|
393
|
+
seen = set()
|
|
394
|
+
for combo in scale_image_combinations:
|
|
395
|
+
if combo in seen:
|
|
396
|
+
duplicate_combinations.append(combo)
|
|
397
|
+
else:
|
|
398
|
+
seen.add(combo)
|
|
399
|
+
|
|
400
|
+
print(f"Warning: Duplicate scale/image_type combinations found: {duplicate_combinations}. "
|
|
401
|
+
f"Skipping rename operation.")
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
# Check if COMPLUS4T mode: .001 files are in parent directory
|
|
405
|
+
is_complus4t = self._check_complus4t_mode()
|
|
406
|
+
print(f"[DEBUG] is_complus4t: {is_complus4t}")
|
|
407
|
+
|
|
408
|
+
if is_complus4t:
|
|
409
|
+
# COMPLUS4T mode: .001 file is in parent directory, find it once
|
|
410
|
+
matching_files = glob.glob(os.path.join(self.dirname, '*.001'))
|
|
411
|
+
print(f"[DEBUG] COMPLUS4T: Found {len(matching_files)} .001 files in parent directory")
|
|
412
|
+
if not matching_files:
|
|
413
|
+
print("[DEBUG] Warning: No .001 file found in parent directory for COMPLUS4T mode")
|
|
414
|
+
return
|
|
415
|
+
parent_recipe_path = matching_files[0]
|
|
416
|
+
print(f"[DEBUG] COMPLUS4T: Using parent recipe_path: {parent_recipe_path}")
|
|
417
|
+
|
|
418
|
+
# Build coordinates dictionary for all wafers
|
|
419
|
+
coordinates_dict = {}
|
|
420
|
+
is_kronos = False
|
|
421
|
+
|
|
422
|
+
for subdir, _, _ in os.walk(self.dirname):
|
|
423
|
+
if subdir == self.dirname:
|
|
424
|
+
continue
|
|
425
|
+
|
|
426
|
+
output_dir = os.path.join(self.dirname, os.path.basename(subdir))
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
wafer_num = int(os.path.basename(subdir))
|
|
430
|
+
except ValueError:
|
|
431
|
+
continue
|
|
432
|
+
|
|
433
|
+
print(f"\n[DEBUG] Processing subdirectory: {subdir}")
|
|
434
|
+
print(f"[DEBUG] output_dir: {output_dir}")
|
|
435
|
+
print(f"[DEBUG] Extracted wafer_num: {wafer_num}")
|
|
436
|
+
|
|
437
|
+
if is_complus4t:
|
|
438
|
+
# COMPLUS4T mode: use parent .001 file and pass wafer_id
|
|
439
|
+
recipe_path = parent_recipe_path
|
|
440
|
+
print(f"[DEBUG] COMPLUS4T: Using recipe_path: {recipe_path} for wafer {wafer_num}")
|
|
441
|
+
original_wafer_number = self.wafer_number
|
|
442
|
+
self.wafer_number = str(wafer_num)
|
|
443
|
+
print(f"[DEBUG] COMPLUS4T: Setting wafer_number to {self.wafer_number} for extract_positions")
|
|
444
|
+
coordinates = self.extract_positions(recipe_path)
|
|
445
|
+
self.wafer_number = original_wafer_number
|
|
446
|
+
print(f"[DEBUG] COMPLUS4T: Extracted {len(coordinates)} coordinates for wafer {wafer_num}")
|
|
447
|
+
else:
|
|
448
|
+
# Normal/KRONOS mode: .001 file is in wafer subdirectory
|
|
449
|
+
matching_files = glob.glob(os.path.join(output_dir, '*.001'))
|
|
450
|
+
print(f"[DEBUG] Normal/KRONOS: Found {len(matching_files)} .001 files in output_dir")
|
|
451
|
+
if matching_files:
|
|
452
|
+
recipe_path = matching_files[0]
|
|
453
|
+
is_kronos = self._check_kronos_mode(recipe_path)
|
|
454
|
+
print(f"[DEBUG] is_kronos: {is_kronos}")
|
|
455
|
+
original_wafer_number = self.wafer_number
|
|
456
|
+
self.wafer_number = None
|
|
457
|
+
coordinates = self.extract_positions(recipe_path)
|
|
458
|
+
self.wafer_number = original_wafer_number
|
|
459
|
+
print(f"[DEBUG] Normal/KRONOS: Extracted {len(coordinates)} coordinates")
|
|
460
|
+
else:
|
|
461
|
+
print(f"[DEBUG] No .001 file found in {output_dir}, skipping")
|
|
462
|
+
continue
|
|
463
|
+
|
|
464
|
+
if coordinates is None or coordinates.empty:
|
|
465
|
+
print(f"[DEBUG] Warning: Coordinates are None or empty for wafer {wafer_num}")
|
|
466
|
+
continue
|
|
467
|
+
|
|
468
|
+
coordinates_dict[wafer_num] = coordinates
|
|
469
|
+
|
|
470
|
+
# Call the rename_tif module function
|
|
471
|
+
total_renamed = rename_files_all(
|
|
472
|
+
dirname=self.dirname,
|
|
473
|
+
coordinates_dict=coordinates_dict,
|
|
474
|
+
settings=self.settings,
|
|
475
|
+
is_kronos=is_kronos,
|
|
476
|
+
is_complus4t=is_complus4t
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
print(f"\n[DEBUG] Total files renamed across all directories: {total_renamed}")
|
|
480
|
+
print("="*80 + "\n")
|
|
481
|
+
|
|
482
|
+
def clean_all(self):
|
|
483
|
+
"""
|
|
484
|
+
Delete all non-conforming TIFF files in all subdirectories.
|
|
485
|
+
|
|
486
|
+
This method will remove any files that do not follow the expected
|
|
487
|
+
naming conventions in all directories.
|
|
488
|
+
"""
|
|
489
|
+
for subdir, _, _ in os.walk(self.dirname):
|
|
490
|
+
if subdir != self.dirname:
|
|
491
|
+
self.output_dir = os.path.join(self.dirname,
|
|
492
|
+
os.path.basename(subdir))
|
|
493
|
+
|
|
494
|
+
if not os.path.exists(self.output_dir):
|
|
495
|
+
continue
|
|
496
|
+
|
|
497
|
+
tiff_files = [f for f in os.listdir(self.output_dir)
|
|
498
|
+
if f.lower().endswith(('.tiff', '.tif'))]
|
|
499
|
+
for file_name in tiff_files:
|
|
500
|
+
if not file_name.startswith("data") or \
|
|
501
|
+
"page" in file_name.lower() or file_name.endswith("001"):
|
|
502
|
+
file_path = os.path.join(self.output_dir, file_name)
|
|
503
|
+
os.remove(file_path)
|
|
504
|
+
|
|
505
|
+
def organize_and_rename_files(self):
|
|
506
|
+
"""
|
|
507
|
+
Organize TIFF files into subfolders based
|
|
508
|
+
on the last split of their name
|
|
509
|
+
and rename the files to 'data.tif' in their respective subfolders.
|
|
510
|
+
"""
|
|
511
|
+
if not os.path.exists(self.dirname):
|
|
512
|
+
return
|
|
513
|
+
|
|
514
|
+
# Check if there are subdirectories
|
|
515
|
+
subdirs = [d for d in os.listdir(self.dirname) if
|
|
516
|
+
os.path.isdir(os.path.join(self.dirname, d))]
|
|
517
|
+
|
|
518
|
+
# Check if there are .tif files
|
|
519
|
+
tif_files = [f for f in os.listdir(self.dirname)
|
|
520
|
+
if f.lower().endswith(".tif") and os.path.isfile(os.path.join(self.dirname, f))]
|
|
521
|
+
|
|
522
|
+
# Iterate through files in the directory
|
|
523
|
+
for file_name in os.listdir(self.dirname):
|
|
524
|
+
if file_name.lower().endswith(".tif"):
|
|
525
|
+
parts = file_name.rsplit("_", 1)
|
|
526
|
+
if len(parts) < 2:
|
|
527
|
+
# Skip file with unexpected format
|
|
528
|
+
continue
|
|
529
|
+
|
|
530
|
+
# Use the last part (before extension) as the subfolder name
|
|
531
|
+
subfolder_name = parts[-1].split(".")[0]
|
|
532
|
+
subfolder_path = os.path.join(self.dirname, subfolder_name)
|
|
533
|
+
|
|
534
|
+
# Create the subfolder if it does not exist
|
|
535
|
+
os.makedirs(subfolder_path, exist_ok=True)
|
|
536
|
+
|
|
537
|
+
# Move and rename the file
|
|
538
|
+
source_path = os.path.join(self.dirname, file_name)
|
|
539
|
+
destination_path = os.path.join(subfolder_path, "data.tif")
|
|
540
|
+
shutil.move(source_path, destination_path)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
if file_name.lower().endswith(".001"):
|
|
544
|
+
parts = file_name.rsplit("_", 1)
|
|
545
|
+
if len(parts) < 2:
|
|
546
|
+
# Skip file with unexpected format
|
|
547
|
+
continue
|
|
548
|
+
|
|
549
|
+
# Use the last part (before extension) as the subfolder name
|
|
550
|
+
subfolder_name = parts[-1].split(".")[0]
|
|
551
|
+
subfolder_path = os.path.join(self.dirname, subfolder_name)
|
|
552
|
+
|
|
553
|
+
# Create the subfolder if it does not exist
|
|
554
|
+
os.makedirs(subfolder_path, exist_ok=True)
|
|
555
|
+
|
|
556
|
+
# Move and rename the file
|
|
557
|
+
source_path = os.path.join(self.dirname, file_name)
|
|
558
|
+
destination_path = os.path.join(subfolder_path, file_name)
|
|
559
|
+
shutil.move(source_path, destination_path)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
# If no subdirectories and no .tif files, create folders from KLARF files
|
|
563
|
+
if not subdirs and not tif_files:
|
|
564
|
+
wafer_ids = self.extract_wafer_ids_from_klarf()
|
|
565
|
+
|
|
566
|
+
if wafer_ids:
|
|
567
|
+
for wafer_id in wafer_ids:
|
|
568
|
+
subfolder_path = os.path.join(self.dirname, str(wafer_id))
|
|
569
|
+
os.makedirs(subfolder_path, exist_ok=True)
|
|
570
|
+
|
|
571
|
+
def _check_complus4t_mode(self):
|
|
572
|
+
"""Check if we are in COMPLUS4T mode (.001 files with COMPLUS4T in parent directory)."""
|
|
573
|
+
if not self.dirname or not os.path.exists(self.dirname):
|
|
574
|
+
return False
|
|
575
|
+
|
|
576
|
+
# Check for .001 files with COMPLUS4T in the parent directory
|
|
577
|
+
matching_files = glob.glob(os.path.join(self.dirname, '*.001'))
|
|
578
|
+
for file_path in matching_files:
|
|
579
|
+
try:
|
|
580
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
581
|
+
content = f.read()
|
|
582
|
+
if 'COMPLUS4T' in content:
|
|
583
|
+
return True
|
|
584
|
+
except Exception:
|
|
585
|
+
pass
|
|
586
|
+
|
|
587
|
+
return False
|
|
588
|
+
|
|
589
|
+
def _check_kronos_mode(self, filepath=None):
|
|
590
|
+
"""Check if we are in KRONOS mode (.001 files with KRONOS format)."""
|
|
591
|
+
if filepath:
|
|
592
|
+
# Check specific file
|
|
593
|
+
try:
|
|
594
|
+
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
|
595
|
+
for line in f:
|
|
596
|
+
if 'KRONOS' in line or re.search(r'WaferID\s+"Read Failed\.(\d+)"', line):
|
|
597
|
+
return True
|
|
598
|
+
except Exception:
|
|
599
|
+
pass
|
|
600
|
+
else:
|
|
601
|
+
# Check parent directory
|
|
602
|
+
if not self.dirname or not os.path.exists(self.dirname):
|
|
603
|
+
return False
|
|
604
|
+
|
|
605
|
+
matching_files = glob.glob(os.path.join(self.dirname, '*.001'))
|
|
606
|
+
for file_path in matching_files:
|
|
607
|
+
try:
|
|
608
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
609
|
+
for line in f:
|
|
610
|
+
if 'KRONOS' in line or re.search(r'WaferID\s+"Read Failed\.(\d+)"', line):
|
|
611
|
+
return True
|
|
612
|
+
except Exception:
|
|
613
|
+
pass
|
|
614
|
+
|
|
615
|
+
return False
|
|
616
|
+
|
|
617
|
+
def _is_wafer_in_klarf(self, file_path, wafer_id):
|
|
618
|
+
"""Check if a specific wafer ID is in the KLARF file."""
|
|
619
|
+
try:
|
|
620
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
621
|
+
for line in f:
|
|
622
|
+
# Check for COMPLUS4T format: WaferID "@11"
|
|
623
|
+
match = re.search(r'WaferID\s+"@(\d+)"', line)
|
|
624
|
+
if match:
|
|
625
|
+
if int(match.group(1)) == wafer_id:
|
|
626
|
+
return True
|
|
627
|
+
except Exception:
|
|
628
|
+
pass
|
|
629
|
+
return False
|
|
630
|
+
|
|
631
|
+
def extract_wafer_ids_from_klarf(self):
|
|
632
|
+
"""Extract wafer IDs from KLARF files (.001) that contain COMPLUS4T."""
|
|
633
|
+
wafer_ids = []
|
|
634
|
+
|
|
635
|
+
if not self.dirname:
|
|
636
|
+
return wafer_ids
|
|
637
|
+
|
|
638
|
+
# Search for .001 files
|
|
639
|
+
try:
|
|
640
|
+
files = [f for f in os.listdir(self.dirname)
|
|
641
|
+
if f.endswith('.001') and os.path.isfile(os.path.join(self.dirname, f))]
|
|
642
|
+
|
|
643
|
+
for file in files:
|
|
644
|
+
file_path = os.path.join(self.dirname, file)
|
|
645
|
+
try:
|
|
646
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
647
|
+
content = f.read()
|
|
648
|
+
|
|
649
|
+
# Check if file contains "COMPLUS4T"
|
|
650
|
+
if 'COMPLUS4T' in content:
|
|
651
|
+
# Search for all lines with WaferID
|
|
652
|
+
# Pattern to extract number in quotes after WaferID
|
|
653
|
+
pattern = r'WaferID\s+"@(\d+)"'
|
|
654
|
+
matches = re.findall(pattern, content)
|
|
655
|
+
|
|
656
|
+
# Add found IDs (converted to int)
|
|
657
|
+
for match in matches:
|
|
658
|
+
wafer_id = int(match)
|
|
659
|
+
if wafer_id not in wafer_ids and 1 <= wafer_id <= 26:
|
|
660
|
+
wafer_ids.append(wafer_id)
|
|
661
|
+
except Exception:
|
|
662
|
+
pass # Error reading file
|
|
663
|
+
|
|
664
|
+
except Exception:
|
|
665
|
+
pass # Error listing files
|
|
666
|
+
|
|
667
|
+
return wafer_ids
|
|
668
|
+
|
|
669
|
+
if __name__ == "__main__":
|
|
670
|
+
DIRNAME = r"C:\Users\TM273821\Desktop\SEM\D25S2039_200_MOS2_SIO2_API"
|
|
671
|
+
SCALE = r"C:\Users\TM273821\SEM\settings_data.json"
|
|
672
|
+
|
|
673
|
+
processor = Process(DIRNAME, wafer=18, scale=SCALE)
|
|
674
|
+
|
|
675
|
+
# Process files
|
|
676
|
+
# processor.organize_and_rename_files() # Organize and rename files
|
|
677
|
+
# processor.rename_wo_legend_all() # Preprocess all files in the directory
|
|
678
|
+
# processor.rename_wo_legend() # Preprocess specific wafer
|
|
679
|
+
|
|
680
|
+
# processor.split_tiff_all() # Preprocess specific wafer
|
|
681
|
+
# processor.split_tiff_all() # Preprocess specific wafer
|
|
682
|
+
# processor.split_tiff() # Preprocess specific wafer
|
|
683
|
+
processor.rename_all() # Preprocess specific wafer
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
|