ktongue commited on
Commit
20f04e0
·
verified ·
1 Parent(s): 55fb7d9

Update files

Browse files
Files changed (1) hide show
  1. preprocess_user_data.py +613 -0
preprocess_user_data.py ADDED
@@ -0,0 +1,613 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Preprocessing script for experimental images to extract displacement fields
4
+ for elastic parameter identification using PINN.
5
+
6
+ This script performs Digital Image Correlation (DIC) on experimental images
7
+ to extract u_x, u_y displacement fields, then computes stress fields.
8
+
9
+ Usage:
10
+ python preprocess_user_data.py --input /path/to/images/ --output /path/to/output/
11
+ --calibration 0.1 --geometry rectangular
12
+ """
13
+
14
+ import os
15
+ import argparse
16
+ import json
17
+ import zipfile
18
+ import tempfile
19
+ from pathlib import Path
20
+
21
+ import numpy as np
22
+ import cv2
23
+ from scipy import ndimage
24
+ from scipy.interpolate import griddata
25
+ import warnings
26
+
27
+ try:
28
+ import tifffile
29
+
30
+ HAS_TIFFILE = True
31
+ except ImportError:
32
+ HAS_TIFFILE = False
33
+ import numpy as np
34
+
35
+
36
+ class DICProcessor:
37
+ """
38
+ Digital Image Correlation processor for extracting displacement fields
39
+ from speckle pattern images.
40
+ """
41
+
42
+ def __init__(self, subset_size=64, step=8, corr_method=cv2.TM_CCOEFF_NORMED):
43
+ """
44
+ Initialize DIC processor.
45
+
46
+ Args:
47
+ subset_size: Size of the subset window for correlation (pixels)
48
+ step: Step size for grid points (pixels)
49
+ corr_method: OpenCV template matching method
50
+ """
51
+ self.subset_size = subset_size
52
+ self.step = step
53
+ self.corr_method = corr_method
54
+
55
+ def extract_displacement_field(self, ref_image, deformed_image, calibration=1.0):
56
+ """
57
+ Extract displacement field between reference and deformed images.
58
+
59
+ Args:
60
+ ref_image: Reference (undeformed) image
61
+ deformed_image: Deformed image
62
+ calibration: Pixel to physical unit conversion (mm/pixel)
63
+
64
+ Returns:
65
+ dict: Dictionary containing x, y coordinates and u_x, u_y displacements
66
+ """
67
+ if len(ref_image.shape) > 2:
68
+ ref_image = cv2.cvtColor(ref_image, cv2.COLOR_BGR2GRAY)
69
+ if len(deformed_image.shape) > 2:
70
+ deformed_image = cv2.cvtColor(deformed_image, cv2.COLOR_BGR2GRAY)
71
+
72
+ ref_image = np.float64(ref_image)
73
+ deformed_image = np.float64(deformed_image)
74
+
75
+ ref_image = (ref_image - ref_image.mean()) / ref_image.std()
76
+ deformed_image = (deformed_image - deformed_image.mean()) / deformed_image.std()
77
+
78
+ h, w = ref_image.shape
79
+ half_subset = self.subset_size // 2
80
+
81
+ y_coords = range(half_subset, h - half_subset, self.step)
82
+ x_coords = range(half_subset, w - half_subset, self.step)
83
+
84
+ u_x = np.zeros((len(y_coords), len(x_coords)))
85
+ u_y = np.zeros((len(y_coords), len(x_coords)))
86
+
87
+ valid_mask = np.zeros((len(y_coords), len(x_coords)), dtype=bool)
88
+
89
+ for i, y in enumerate(y_coords):
90
+ for j, x in enumerate(x_coords):
91
+ subset = ref_image[
92
+ y - half_subset : y + half_subset, x - half_subset : x + half_subset
93
+ ]
94
+
95
+ search_region = deformed_image[
96
+ max(0, y - half_subset - 50) : min(h, y + half_subset + 50),
97
+ max(0, x - half_subset - 50) : min(w, x + half_subset + 50),
98
+ ]
99
+
100
+ if (
101
+ search_region.shape[0] < self.subset_size
102
+ or search_region.shape[1] < self.subset_size
103
+ ):
104
+ continue
105
+
106
+ try:
107
+ result = cv2.matchTemplate(search_region, subset, self.corr_method)
108
+ min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
109
+
110
+ if self.corr_method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
111
+ match_loc = min_loc
112
+ else:
113
+ match_loc = max_loc
114
+
115
+ offset_y = match_loc[1] - 50
116
+ offset_x = match_loc[0] - 50
117
+
118
+ u_y[i, j] = offset_y
119
+ u_x[i, j] = offset_x
120
+ valid_mask[i, j] = True
121
+
122
+ except Exception:
123
+ continue
124
+
125
+ x_grid = np.array([x * calibration for x in x_coords])
126
+ y_grid = np.array([y * calibration for y in y_coords])
127
+
128
+ u_x = u_x * calibration
129
+ u_y = u_y * calibration
130
+
131
+ return {
132
+ "x": x_grid,
133
+ "y": y_grid,
134
+ "u_x": u_x,
135
+ "u_y": u_y,
136
+ "valid_mask": valid_mask,
137
+ "calibration": calibration,
138
+ }
139
+
140
+ def compute_strains(self, disp_data, lambda_val=1.0, mu_val=0.5):
141
+ """
142
+ Compute strain and stress fields from displacement data.
143
+
144
+ Args:
145
+ disp_data: Dictionary with x, y, u_x, u_y
146
+ lambda_val: First Lamé parameter (normalized)
147
+ mu_val: Second Lamé parameter (normalized)
148
+
149
+ Returns:
150
+ dict: Strain and stress fields
151
+ """
152
+ x = disp_data["x"]
153
+ y = disp_data["y"]
154
+ u_x = disp_data["u_x"]
155
+ u_y = disp_data["u_y"]
156
+
157
+ dx = x[1] - x[0] if len(x) > 1 else 1.0
158
+ dy = y[1] - y[0] if len(y) > 1 else 1.0
159
+
160
+ epsilon_xx = np.gradient(u_x, dx, axis=1)
161
+ epsilon_yy = np.gradient(u_y, dy, axis=0)
162
+ epsilon_xy = 0.5 * (np.gradient(u_x, dy, axis=0) + np.gradient(u_y, dx, axis=1))
163
+
164
+ sigma_xx = (lambda_val + 2 * mu_val) * epsilon_xx + lambda_val * epsilon_yy
165
+ sigma_yy = (lambda_val + 2 * mu_val) * epsilon_yy + lambda_val * epsilon_xx
166
+ sigma_xy = 2 * mu_val * epsilon_xy
167
+
168
+ return {
169
+ "epsilon_xx": epsilon_xx,
170
+ "epsilon_yy": epsilon_yy,
171
+ "epsilon_xy": epsilon_xy,
172
+ "sigma_xx": sigma_xx,
173
+ "sigma_yy": sigma_yy,
174
+ "sigma_xy": sigma_xy,
175
+ }
176
+
177
+ def normalize_to_pinn_format(self, disp_data, stress_data, domain_bounds=None):
178
+ """
179
+ Normalize data to PINN training format (domain [0,1] x [0,1]).
180
+
181
+ Args:
182
+ disp_data: Displacement data dictionary
183
+ stress_data: Stress data dictionary
184
+ domain_bounds: Optional (x_min, x_max, y_min, y_max) for normalization
185
+
186
+ Returns:
187
+ dict: Normalized data ready for PINN
188
+ """
189
+ x = disp_data["x"]
190
+ y = disp_data["y"]
191
+
192
+ if domain_bounds is None:
193
+ x_min, x_max = x.min(), x.max()
194
+ y_min, y_max = y.min(), y.max()
195
+ else:
196
+ x_min, x_max, y_min, y_max = domain_bounds
197
+
198
+ x_norm = (x - x_min) / (x_max - x_min)
199
+ y_norm = (y - y_min) / (y_max - y_min)
200
+
201
+ u_x_norm = disp_data["u_x"]
202
+ u_y_norm = disp_data["u_y"]
203
+ u_x_norm = (u_x_norm - u_x_norm.mean()) / u_x_norm.std()
204
+ u_y_norm = (u_y_norm - u_y_norm.mean()) / u_y_norm.std()
205
+
206
+ return {
207
+ "x_norm": x_norm,
208
+ "y_norm": y_norm,
209
+ "u_x": u_x_norm,
210
+ "u_y": u_y_norm,
211
+ "sigma_xx": stress_data["sigma_xx"],
212
+ "sigma_yy": stress_data["sigma_yy"],
213
+ "sigma_xy": stress_data["sigma_xy"],
214
+ "original_bounds": (x_min, x_max, y_min, y_max),
215
+ "calibration": disp_data["calibration"],
216
+ }
217
+
218
+
219
+ class ImageLoader:
220
+ """
221
+ Handles loading images from various sources (folder, zip, etc.)
222
+ """
223
+
224
+ SUPPORTED_FORMATS = {".tif", ".tiff", ".png", ".jpg", ".jpeg", ".bmp"}
225
+
226
+ @staticmethod
227
+ def load_images_from_folder(folder_path, sort_by_name=True):
228
+ """
229
+ Load all images from a folder.
230
+
231
+ Args:
232
+ folder_path: Path to folder containing images
233
+ sort_by_name: Whether to sort images by filename
234
+
235
+ Returns:
236
+ list: List of image arrays
237
+ """
238
+ folder = Path(folder_path)
239
+ image_files = []
240
+
241
+ for ext in ImageLoader.SUPPORTED_FORMATS:
242
+ image_files.extend(list(folder.glob(f"*{ext}")))
243
+ image_files.extend(list(folder.glob(f"*{ext.upper()}")))
244
+
245
+ if sort_by_name:
246
+ image_files = sorted(image_files)
247
+
248
+ images = []
249
+ for img_path in image_files:
250
+ img = ImageLoader.load_image(img_path)
251
+ if img is not None:
252
+ images.append(
253
+ {"path": str(img_path), "name": img_path.name, "data": img}
254
+ )
255
+
256
+ return images
257
+
258
+ @staticmethod
259
+ def load_images_from_zip(zip_path, extract_to=None):
260
+ """
261
+ Load images from a ZIP file, preserving order in filename.
262
+
263
+ Args:
264
+ zip_path: Path to ZIP file
265
+ extract_to: Optional folder to extract images
266
+
267
+ Returns:
268
+ list: List of image dictionaries
269
+ """
270
+ zip_path = Path(zip_path)
271
+
272
+ if extract_to is None:
273
+ extract_to = tempfile.mkdtemp()
274
+
275
+ with zipfile.ZipFile(zip_path, "r") as zf:
276
+ image_files = [
277
+ f
278
+ for f in zf.namelist()
279
+ if Path(f).suffix.lower() in ImageLoader.SUPPORTED_FORMATS
280
+ ]
281
+ image_files = sorted(image_files)
282
+
283
+ zf.extractall(extract_to)
284
+
285
+ return ImageLoader.load_images_from_folder(extract_to, sort_by_name=True)
286
+
287
+ @staticmethod
288
+ def load_image(path):
289
+ """
290
+ Load a single image from various formats.
291
+
292
+ Args:
293
+ path: Path to image file
294
+
295
+ Returns:
296
+ numpy array or None
297
+ """
298
+ path = Path(path)
299
+ suffix = path.suffix.lower()
300
+
301
+ try:
302
+ if suffix in [".tif", ".tiff"]:
303
+ if HAS_TIFFILE:
304
+ return tifffile.imread(str(path))
305
+ else:
306
+ return cv2.imread(str(path), cv2.IMREAD_UNCHANGED)
307
+ else:
308
+ return cv2.imread(str(path), cv2.IMREAD_GRAYSCALE)
309
+ except Exception as e:
310
+ print(f"Error loading {path}: {e}")
311
+ return None
312
+
313
+
314
+ class ExperimentalDataProcessor:
315
+ """
316
+ Main class for processing experimental images and preparing data for PINN.
317
+ """
318
+
319
+ def __init__(
320
+ self,
321
+ calibration=1.0,
322
+ geometry="rectangular",
323
+ domain_bounds=None,
324
+ subset_size=64,
325
+ step=8,
326
+ ):
327
+ """
328
+ Initialize processor.
329
+
330
+ Args:
331
+ calibration: Pixel to mm conversion
332
+ geometry: 'rectangular' or other
333
+ domain_bounds: (x_min, x_max, y_min, y_max) in mm
334
+ subset_size: DIC subset size
335
+ step: DIC step size
336
+ """
337
+ self.calibration = calibration
338
+ self.geometry = geometry
339
+ self.domain_bounds = domain_bounds
340
+ self.dic = DICProcessor(subset_size=subset_size, step=step)
341
+
342
+ def process_image_sequence(
343
+ self, images, reference_index=0, lambda_init=1.0, mu_init=0.5
344
+ ):
345
+ """
346
+ Process a sequence of images to extract displacement fields.
347
+
348
+ Args:
349
+ images: List of image dictionaries
350
+ reference_index: Index of reference (undeformed) image
351
+ lambda_init: Initial lambda for stress calculation
352
+ mu_init: Initial mu for stress calculation
353
+
354
+ Returns:
355
+ list: List of processed data dictionaries
356
+ """
357
+ if len(images) < 2:
358
+ raise ValueError("At least 2 images required (reference + deformed)")
359
+
360
+ ref_img = images[reference_index]["data"]
361
+ results = []
362
+
363
+ for i, img_dict in enumerate(images):
364
+ if i == reference_index:
365
+ continue
366
+
367
+ def_img = img_dict["data"]
368
+
369
+ disp_data = self.dic.extract_displacement_field(
370
+ ref_img, def_img, self.calibration
371
+ )
372
+
373
+ stress_data = self.dic.compute_strains(disp_data, lambda_init, mu_init)
374
+
375
+ normalized = self.dic.normalize_to_pinn_format(
376
+ disp_data, stress_data, self.domain_bounds
377
+ )
378
+
379
+ results.append(
380
+ {
381
+ "image_name": img_dict["name"],
382
+ "step": i,
383
+ "displacement": disp_data,
384
+ "stress": stress_data,
385
+ "normalized": normalized,
386
+ }
387
+ )
388
+
389
+ print(f"Processed: {img_dict['name']} (step {i})")
390
+
391
+ return results
392
+
393
+ def export_to_csv(self, processed_data, output_path):
394
+ """
395
+ Export processed data to CSV format for PINN training.
396
+
397
+ Args:
398
+ processed_data: List of processed data dictionaries
399
+ output_path: Path to output CSV file
400
+ """
401
+ import pandas as pd
402
+
403
+ all_points = []
404
+
405
+ for data in processed_data:
406
+ x = data["normalized"]["x_norm"].flatten()
407
+ y = data["normalized"]["y_norm"].flatten()
408
+ ux = data["normalized"]["u_x"].flatten()
409
+ uy = data["normalized"]["u_y"].flatten()
410
+ sxx = data["normalized"]["sigma_xx"].flatten()
411
+ syy = data["normalized"]["sigma_yy"].flatten()
412
+ sxy = data["normalized"]["sigma_xy"].flatten()
413
+
414
+ for i in range(len(x)):
415
+ all_points.append(
416
+ {
417
+ "x": x[i],
418
+ "y": y[i],
419
+ "u_x": ux[i],
420
+ "u_y": uy[i],
421
+ "sigma_xx": sxx[i],
422
+ "sigma_yy": syy[i],
423
+ "sigma_xy": sxy[i],
424
+ "step": data["step"],
425
+ }
426
+ )
427
+
428
+ df = pd.DataFrame(all_points)
429
+ df.to_csv(output_path, index=False)
430
+ print(f"Exported to: {output_path}")
431
+
432
+ return df
433
+
434
+ def export_to_numpy(self, processed_data, output_path):
435
+ """
436
+ Export processed data to numpy format.
437
+
438
+ Args:
439
+ processed_data: List of processed data dictionaries
440
+ output_path: Path to output .npz file
441
+ """
442
+ x_data = []
443
+ y_data = []
444
+ ux_data = []
445
+ uy_data = []
446
+ sxx_data = []
447
+ syy_data = []
448
+ sxy_data = []
449
+
450
+ for data in processed_data:
451
+ x_data.append(data["normalized"]["x_norm"])
452
+ y_data.append(data["normalized"]["y_norm"])
453
+ ux_data.append(data["normalized"]["u_x"])
454
+ uy_data.append(data["normalized"]["u_y"])
455
+ sxx_data.append(data["normalized"]["sigma_xx"])
456
+ syy_data.append(data["normalized"]["sigma_yy"])
457
+ sxy_data.append(data["normalized"]["sigma_xy"])
458
+
459
+ np.savez(
460
+ output_path,
461
+ x=np.array(x_data),
462
+ y=np.array(y_data),
463
+ u_x=np.array(ux_data),
464
+ u_y=np.array(uy_data),
465
+ sigma_xx=np.array(sxx_data),
466
+ sigma_yy=np.array(syy_data),
467
+ sigma_xy=np.array(sxy_data),
468
+ domain_bounds=self.domain_bounds,
469
+ calibration=self.calibration,
470
+ )
471
+ print(f"Exported to: {output_path}")
472
+
473
+ def save_metadata(self, processed_data, output_path, metadata=None):
474
+ """
475
+ Save processing metadata to JSON.
476
+
477
+ Args:
478
+ processed_data: List of processed data
479
+ output_path: Path to output JSON
480
+ metadata: Additional metadata dictionary
481
+ """
482
+ meta = {
483
+ "num_images": len(processed_data),
484
+ "calibration_mm_per_pixel": self.calibration,
485
+ "geometry": self.geometry,
486
+ "domain_bounds": self.domain_bounds,
487
+ "dic_parameters": {
488
+ "subset_size": self.dic.subset_size,
489
+ "step": self.dic.step,
490
+ },
491
+ "images": [
492
+ {"name": d["image_name"], "step": d["step"]} for d in processed_data
493
+ ],
494
+ }
495
+
496
+ if metadata:
497
+ meta.update(metadata)
498
+
499
+ with open(output_path, "w") as f:
500
+ json.dump(meta, f, indent=2)
501
+
502
+ print(f"Metadata saved to: {output_path}")
503
+
504
+
505
+ def main():
506
+ parser = argparse.ArgumentParser(
507
+ description="Process experimental images for PINN-based elastic parameter identification"
508
+ )
509
+
510
+ parser.add_argument(
511
+ "--input",
512
+ "-i",
513
+ required=True,
514
+ help="Input folder or ZIP file containing images",
515
+ )
516
+ parser.add_argument(
517
+ "--output", "-o", required=True, help="Output folder for processed data"
518
+ )
519
+ parser.add_argument(
520
+ "--calibration",
521
+ "-c",
522
+ type=float,
523
+ default=1.0,
524
+ help="Pixel to mm conversion (default: 1.0)",
525
+ )
526
+ parser.add_argument(
527
+ "--geometry",
528
+ "-g",
529
+ default="rectangular",
530
+ choices=["rectangular", "circular", "custom"],
531
+ help="Sample geometry (default: rectangular)",
532
+ )
533
+ parser.add_argument(
534
+ "--bounds",
535
+ nargs=4,
536
+ type=float,
537
+ metavar=("XMIN", "XMAX", "YMIN", "YMAX"),
538
+ help="Domain bounds in mm",
539
+ )
540
+ parser.add_argument(
541
+ "--reference",
542
+ "-r",
543
+ type=int,
544
+ default=0,
545
+ help="Reference image index (default: 0)",
546
+ )
547
+ parser.add_argument(
548
+ "--subset-size",
549
+ type=int,
550
+ default=64,
551
+ help="DIC subset size in pixels (default: 64)",
552
+ )
553
+ parser.add_argument(
554
+ "--step", type=int, default=8, help="DIC step size in pixels (default: 8)"
555
+ )
556
+ parser.add_argument("--zip", action="store_true", help="Input is a ZIP file")
557
+ parser.add_argument(
558
+ "--export-format",
559
+ choices=["csv", "numpy", "both"],
560
+ default="both",
561
+ help="Export format",
562
+ )
563
+
564
+ args = parser.parse_args()
565
+
566
+ os.makedirs(args.output, exist_ok=True)
567
+
568
+ print(f"Loading images from: {args.input}")
569
+
570
+ if args.zip or str(args.input).endswith(".zip"):
571
+ images = ImageLoader.load_images_from_zip(args.input)
572
+ else:
573
+ images = ImageLoader.load_images_from_folder(args.input)
574
+
575
+ print(f"Loaded {len(images)} images")
576
+
577
+ if len(images) < 2:
578
+ print("Error: Need at least 2 images")
579
+ return
580
+
581
+ domain_bounds = tuple(args.bounds) if args.bounds else None
582
+
583
+ processor = ExperimentalDataProcessor(
584
+ calibration=args.calibration,
585
+ geometry=args.geometry,
586
+ domain_bounds=domain_bounds,
587
+ subset_size=args.subset_size,
588
+ step=args.step,
589
+ )
590
+
591
+ print("Processing image sequence...")
592
+ processed_data = processor.process_image_sequence(
593
+ images, reference_index=args.reference
594
+ )
595
+
596
+ print("Exporting data...")
597
+
598
+ if args.export_format in ["csv", "both"]:
599
+ csv_path = os.path.join(args.output, "training_data.csv")
600
+ processor.export_to_csv(processed_data, csv_path)
601
+
602
+ if args.export_format in ["numpy", "both"]:
603
+ npz_path = os.path.join(args.output, "training_data.npz")
604
+ processor.export_to_numpy(processed_data, npz_path)
605
+
606
+ meta_path = os.path.join(args.output, "processing_metadata.json")
607
+ processor.save_metadata(processed_data, meta_path)
608
+
609
+ print(f"\nProcessing complete! Output in: {args.output}")
610
+
611
+
612
+ if __name__ == "__main__":
613
+ main()