eMilnc commited on
Commit
c025059
·
verified ·
1 Parent(s): 55e008d

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. src/ViTAE_library.py +188 -0
  2. src/test_model.py +171 -0
  3. src/test_model_tpl.py +173 -0
src/ViTAE_library.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.optim as optim
4
+ from torch.utils.data import Dataset, DataLoader
5
+ from pathlib import Path
6
+ from tqdm import tqdm
7
+ import random
8
+ import matplotlib.pyplot as plt
9
+
10
+ class IrisTensorDataset(Dataset):
11
+ def __init__(self, file_list):
12
+ self.file_list = file_list
13
+
14
+ def __len__(self):
15
+ return len(self.file_list)
16
+
17
+ def __getitem__(self, idx):
18
+ tensor_path = self.file_list[idx]
19
+ img_tensor = torch.load(tensor_path)
20
+ return img_tensor, img_tensor
21
+
22
+
23
+ class PatchEmbedding(nn.Module):
24
+ def __init__(self, in_channels=1, patch_size=16, embed_dim=256):
25
+ super().__init__()
26
+ self.patch_size = patch_size
27
+ self.proj = nn.Conv2d(in_channels, embed_dim, kernel_size=patch_size, stride=patch_size)
28
+
29
+ def forward(self, x):
30
+ x = self.proj(x)
31
+ x = x.flatten(2)
32
+ x = x.transpose(1, 2)
33
+ return x
34
+
35
+ class ViTAutoencoder(nn.Module):
36
+ def __init__(self, img_size=224, patch_size=16, in_channels=1, embed_dim=256, latent_dim=512, num_heads=16, num_layers=6):
37
+ super().__init__()
38
+ self.patch_size = patch_size
39
+ self.num_patches = (img_size // patch_size) ** 2
40
+ self.embed_dim = embed_dim
41
+
42
+ self.patch_embed = PatchEmbedding(in_channels, patch_size, embed_dim)
43
+ self.encoder_pos_embed = nn.Parameter(torch.zeros(1, self.num_patches, embed_dim))
44
+
45
+ encoder_layer = nn.TransformerEncoderLayer(d_model=embed_dim, nhead=num_heads, dim_feedforward=embed_dim*4, batch_first=True)
46
+ self.transformer_encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
47
+
48
+ self.to_latent = nn.Linear(self.num_patches * embed_dim,latent_dim)
49
+ self.from_latent = nn.Linear(latent_dim,self.num_patches * embed_dim)
50
+
51
+ self.decoder_pos_embed = nn.Parameter(torch.zeros(1, self.num_patches, embed_dim))
52
+ decoder_layer = nn.TransformerDecoderLayer(d_model=embed_dim, nhead=num_heads, dim_feedforward=embed_dim*4, batch_first=True)
53
+ self.transformer_decoder = nn.TransformerDecoder(decoder_layer, num_layers=num_layers)
54
+
55
+ self.head = nn.Linear(embed_dim, patch_size * patch_size * in_channels)
56
+
57
+ def encode(self, x):
58
+ x = self.patch_embed(x) + self.encoder_pos_embed
59
+ x = self.transformer_encoder(x)
60
+ x = x.flatten(1)
61
+ latent = self.to_latent(x)
62
+ return latent
63
+
64
+ def decode(self, latent, batch_size):
65
+ x = self.from_latent(latent)
66
+ x = x.view(batch_size, self.num_patches, self.embed_dim)
67
+ x = x + self.decoder_pos_embed
68
+ x = self.transformer_decoder(x, x)
69
+ x = self.head(x)
70
+
71
+ grid_size = int(self.num_patches ** 0.5)
72
+ x = x.view(batch_size, grid_size, grid_size, 1, self.patch_size, self.patch_size)
73
+ x = x.permute(0, 3, 1, 4, 2, 5).contiguous()
74
+ recon_img = x.view(batch_size, 1, grid_size * self.patch_size, grid_size * self.patch_size)
75
+ return recon_img
76
+
77
+ def forward(self, x):
78
+ batch_size = x.shape[0]
79
+ latent = self.encode(x)
80
+ recon_img = self.decode(latent, batch_size)
81
+ return recon_img
82
+
83
+
84
+ def trainModel(max_epoch, model, optimizer, criterion, train_loader, val_loader, train_loss_list, val_loss_list, SAVE_PATH, PATIENCE):
85
+ device = 'cuda' if torch.cuda.is_available() else 'cpu'
86
+ for epoch in range(1, max_epoch + 1):
87
+ # -- Training Phase --
88
+ model.to(device)
89
+ model.train()
90
+ running_train_loss = 0.0
91
+
92
+ train_bar = tqdm(train_loader, desc=f"Epoch {epoch}/{max_epoch} [Train]")
93
+ for inputs, targets in train_bar:
94
+ inputs, targets = inputs.to(device), targets.to(device)
95
+ optimizer.zero_grad()
96
+ outputs = model(inputs)
97
+ loss = criterion(outputs, targets)
98
+ loss.backward()
99
+ optimizer.step()
100
+
101
+ running_train_loss += loss.item() * inputs.size(0)
102
+ train_bar.set_postfix({'Loss': f"{loss.item():.4f}"})
103
+
104
+ epoch_train_loss = running_train_loss / (len(train_loader) * train_loader.batch_size)
105
+ train_loss_list.append(epoch_train_loss)
106
+
107
+ # -- Validation Phase --
108
+ model.eval()
109
+ running_val_loss = 0.0
110
+ with torch.no_grad():
111
+ for inputs, targets in val_loader:
112
+ inputs, targets = inputs.to(device), targets.to(device)
113
+ outputs = model(inputs)
114
+ loss = criterion(outputs, targets)
115
+ running_val_loss += loss.item() * inputs.size(0)
116
+
117
+ epoch_val_loss = running_val_loss / len(val_loader) * val_loader.batch_size
118
+ val_loss_list.append(epoch_val_loss)
119
+
120
+ print(f"Summary - Train Loss: {epoch_train_loss:.5f} | Val Loss: {epoch_val_loss:.5f}")
121
+
122
+ # -- Autosave & Early Stopping Logic --
123
+ if epoch_val_loss < best_val_loss:
124
+ best_val_loss = epoch_val_loss
125
+ epochs_without_improvement = 0
126
+ torch.save(model.state_dict(), SAVE_PATH)
127
+ print(f"--> Validation improved. Model saved to {SAVE_PATH}")
128
+ else:
129
+ epochs_without_improvement += 1
130
+ print(f"--> No improvement for {epochs_without_improvement} epoch(s).")
131
+
132
+ if epochs_without_improvement >= PATIENCE:
133
+ print(f"\n[Early Stopping Triggered] Stopping at epoch {epoch} due to tolerance limits.")
134
+ break
135
+
136
+ plt.figure(figsize=(10, 6))
137
+ plt.plot(range(1, len(train_loss_list) + 1), train_loss_list, label='Training Loss', marker='o')
138
+ plt.plot(range(1, len(val_loss_list) + 1), val_loss_list, label='Validation Loss', marker='x')
139
+ plt.title('Training and Validation Loss over Epochs')
140
+ plt.xlabel('Epochs')
141
+ plt.ylabel('Mean Squared Error (MSE)')
142
+ plt.legend()
143
+ plt.grid(True)
144
+ plt.show()
145
+
146
+
147
+ def ViTAEModelTest(data, model, type):
148
+ device = 'cuda' if torch.cuda.is_available() else 'cpu'
149
+ model.to(device)
150
+ criterion = nn.MSELoss()
151
+ if type.lower() == 'batch':
152
+ running_test_loss = 0.0
153
+ with torch.no_grad():
154
+ for inputs, targets in tqdm(data, desc="Evaluating Test Set"):
155
+ inputs, targets = inputs.to(device), targets.to(device)
156
+ outputs = model(inputs)
157
+ loss = criterion(outputs, targets)
158
+ running_test_loss += loss.item() * inputs.size(0)
159
+
160
+ final_test_loss = running_test_loss / len(data) * data.batch_size
161
+ print(f"\nFinal Test Set Loss (MSE): {final_test_loss:.5f}")
162
+
163
+ elif type.lower() == 'single':
164
+ original_data=data.unsqueeze(0)
165
+ recon_data=model(original_data)
166
+
167
+ original_array=original_data.squeeze().detach().cpu().numpy()
168
+ recon_array=recon_data.squeeze().detach().cpu().numpy()
169
+ diff_img_from_file = original_array - recon_array
170
+
171
+ fig, axes = plt.subplots(1, 3, figsize=(10, 5))
172
+
173
+ # 4. Plot the first image on the left axis (axes[0])
174
+ axes[0].imshow(original_array, cmap='gray')
175
+ axes[0].set_title('Original Image')
176
+ axes[0].axis('off') # Optional: hides grid lines and pixel numbers
177
+
178
+ # 5. Plot the second image on the right axis (axes[1])
179
+ axes[1].imshow(recon_array, cmap='gray')
180
+ axes[1].set_title('Reconstructed Image')
181
+ axes[1].axis('off') # Optional: hides grid lines and pixel numbers
182
+
183
+ axes[2].imshow(diff_img_from_file, cmap='gray')
184
+ axes[2].set_title('Difference Image')
185
+ axes[2].axis('off')
186
+ # 6. Display the plot cleanly
187
+ plt.tight_layout() # Prevents labels/titles from overlapping
188
+ plt.show()
src/test_model.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Iris Matching Model Tester
3
+ ===========================
4
+ ทดสอบโมเดลจับคู่ม่านตา (ViTAutoencoder + MatchCNN_V2)
5
+
6
+ วิธีใช้:
7
+ python test_model.py <ภาพม่านตา1> <ภาพม่านตา2>
8
+
9
+ ตัวอย่าง:
10
+ python test_model.py sample_A.jpg sample_B.jpg
11
+
12
+ ไฟล์ที่ต้องมีอยู่ในโฟลเดอร์เดียวกับสคริปต์นี้ (หรือแก้ path ด้านล่าง):
13
+ - best_match_cnn.pth (มีอยู่แล้ว)
14
+ - best_vit_autoencoder.pth (ยังไม่มี - ต้องดาวน์โหลดจาก Drive: IrisPeriNectec/)
15
+ - ViTAE_library.py (ยังไม่มี - ต้องดาวน์โหลดจาก Drive: IrisPeriNectec/)
16
+ """
17
+
18
+ import os
19
+ import sys
20
+ import argparse
21
+
22
+ import torch
23
+ import torch.nn as nn
24
+ import torch.nn.functional as F
25
+ from torchvision import transforms
26
+ from PIL import Image
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Config — แก้ path ตรงนี้ถ้าไฟล์ไม่ได้อยู่โฟลเดอร์เดียวกับสคริปต์
30
+ # ---------------------------------------------------------------------------
31
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
32
+ AUTOENCODER_WEIGHTS = os.path.join(SCRIPT_DIR, "..", "Model", "best_vit_autoencoder.pth")
33
+ MATCHCNN_WEIGHTS = os.path.join(SCRIPT_DIR, "..", "Model", "best_match_cnn.pth")
34
+ VITAE_LIBRARY_DIR = SCRIPT_DIR # โฟลเดอร์ที่มี ViTAE_library.py
35
+ MATCH_THRESHOLD = 0.5
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # MatchCNN_V2 — copy มาจาก NewGoodcnnV1.ipynb / Testcnn.ipynb ทุกตัวอักษร
40
+ # ---------------------------------------------------------------------------
41
+ class MatchCNN_V2(nn.Module):
42
+ def __init__(self):
43
+ super().__init__()
44
+ self.cnn_extractor = nn.Sequential(
45
+ nn.Conv2d(2, 32, 5, padding=2),
46
+ nn.ReLU(),
47
+ nn.MaxPool2d(2),
48
+ nn.Conv2d(32, 64, 3, padding=1),
49
+ nn.ReLU(),
50
+ nn.MaxPool2d(2),
51
+ nn.Conv2d(64, 128, 3, padding=1),
52
+ nn.ReLU(),
53
+ nn.MaxPool2d(2),
54
+ nn.Flatten(),
55
+ )
56
+ self.fc_input_size = 100352
57
+
58
+ self.classifier = nn.Sequential(
59
+ nn.Linear(self.fc_input_size + 1, 256),
60
+ nn.ReLU(),
61
+ nn.Dropout(0.5),
62
+ nn.Linear(256, 1),
63
+ nn.Sigmoid(),
64
+ )
65
+
66
+ def forward(self, input1, input2, cos_sim):
67
+ combined_imgs = torch.cat([input1, input2], dim=1)
68
+ cnn_features = self.cnn_extractor(combined_imgs)
69
+ fused = torch.cat([cnn_features, cos_sim], dim=1)
70
+ return self.classifier(fused).squeeze()
71
+
72
+
73
+ def check_dependencies():
74
+ """เช็คว่าไฟล์ที่จำเป็นครบไหมก่อนรัน แจ้ง error ที่อ่านง่าย"""
75
+ missing = []
76
+ if not os.path.exists(MATCHCNN_WEIGHTS):
77
+ missing.append(f" - {MATCHCNN_WEIGHTS}")
78
+ if not os.path.exists(AUTOENCODER_WEIGHTS):
79
+ missing.append(f" - {AUTOENCODER_WEIGHTS}")
80
+ vitae_path = os.path.join(VITAE_LIBRARY_DIR, "ViTAE_library.py")
81
+ if not os.path.exists(vitae_path):
82
+ missing.append(f" - {vitae_path}")
83
+
84
+ if missing:
85
+ print("❌ ไฟล์ที่จำเป็นยังไม่ครบ ขาดไฟล์ต่อไปนี้:")
86
+ for m in missing:
87
+ print(m)
88
+ print(
89
+ "\nไปดาวน์โหลดจาก Google Drive โฟลเดอร์ 'IrisPeriNectec' "
90
+ "(best_vit_autoencoder.pth และ ViTAE_library.py) "
91
+ "แล้ววางไว้ในโฟลเดอร์เดียวกับสคริปต์นี้ครับ"
92
+ )
93
+ sys.exit(1)
94
+
95
+
96
+ def load_models(device):
97
+ check_dependencies()
98
+
99
+ sys.path.insert(0, VITAE_LIBRARY_DIR)
100
+ from ViTAE_library import ViTAutoencoder # noqa: E402 (import ต้องมาหลัง sys.path.insert)
101
+
102
+ print("กำลังโหลด ViTAutoencoder ...")
103
+ autoencoder = ViTAutoencoder(
104
+ img_size=224,
105
+ patch_size=16,
106
+ in_channels=1,
107
+ embed_dim=256,
108
+ latent_dim=512,
109
+ num_heads=16,
110
+ num_layers=6,
111
+ ).to(device)
112
+ autoencoder.load_state_dict(torch.load(AUTOENCODER_WEIGHTS, map_location=device))
113
+ autoencoder.eval()
114
+
115
+ print("กำลังโหลด MatchCNN_V2 ...")
116
+ model = MatchCNN_V2().to(device)
117
+ model.load_state_dict(torch.load(MATCHCNN_WEIGHTS, map_location=device))
118
+ model.eval()
119
+
120
+ print("✅ โหลดโมเดลครบแล้ว\n")
121
+ return autoencoder, model
122
+
123
+
124
+ def predict_pair(path1, path2, autoencoder, model, device, transform):
125
+ """คืนค่า match probability (0-1) ของภาพม่านตาสองรูป"""
126
+ for p in (path1, path2):
127
+ if not os.path.exists(p):
128
+ raise FileNotFoundError(f"ไม่พบไฟล์ภาพ: {p}")
129
+
130
+ img1 = transform(Image.open(path1).convert("L")).unsqueeze(0).to(device)
131
+ img2 = transform(Image.open(path2).convert("L")).unsqueeze(0).to(device)
132
+
133
+ with torch.no_grad():
134
+ t1 = autoencoder(img1)
135
+ t2 = autoencoder(img2)
136
+ cos_sim = F.cosine_similarity(t1.view(1, -1), t2.view(1, -1), dim=1).unsqueeze(1)
137
+ prob = model(t1, t2, cos_sim)
138
+
139
+ return prob.item()
140
+
141
+
142
+ def main():
143
+ parser = argparse.ArgumentParser(description="ทดสอบโมเดลจับคู่ม่านตา")
144
+ parser.add_argument("image1", help="path ภาพม่านตารูปที่ 1")
145
+ parser.add_argument("image2", help="path ภาพม่านตารูปที่ 2")
146
+ args = parser.parse_args()
147
+
148
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
149
+ print(f"ใช้งานบน device: {device}\n")
150
+
151
+ autoencoder, model = load_models(device)
152
+
153
+ transform = transforms.Compose(
154
+ [transforms.Resize((224, 224)), transforms.ToTensor()]
155
+ )
156
+
157
+ prob = predict_pair(args.image1, args.image2, autoencoder, model, device, transform)
158
+
159
+ print("=" * 45)
160
+ print(f"ภาพ 1: {args.image1}")
161
+ print(f"ภาพ 2: {args.image2}")
162
+ print(f"Match probability: {prob:.4f}")
163
+ if prob > MATCH_THRESHOLD:
164
+ print("ผลลัพธ์: ✅ MATCH (น่าจะเป็นคนเดียวกัน)")
165
+ else:
166
+ print("ผลลัพธ์: ❌ NO MATCH (น่าจะคนละคน)")
167
+ print("=" * 45)
168
+
169
+
170
+ if __name__ == "__main__":
171
+ main()
src/test_model_tpl.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Iris Matching Model Tester — TPL (Template) Pipeline
3
+ ======================================================
4
+ ทดสอบโมเดลจับคู่ม่านตา เวอร์ชัน Template-based
5
+ (ViTAutoencoder.encode() -> 512-dim template -> FC matcher)
6
+
7
+ วิธีใช้:
8
+ python test_model_tpl.py <ภาพม่านตา1> <ภาพม่านตา2>
9
+
10
+ ไฟล์ที่ต้องมีอยู่ในโฟลเดอร์เดียวกับสคริปต์นี้:
11
+ - scar_encoder_triplet.pth (ViT encoder เทรนด้วย triplet loss)
12
+ - best_match_cnn(TPL).pth (FC matcher — ชื่อไฟล์มีคลา์มีคหว้ง cnn ค้างมาจากเดิม
13
+ แต่ตัวจริงเป็น fully-connected)
14
+ - ViTAE_library.py (นิยามคลาส ViTAutoencoder)
15
+
16
+ หมายเหตุสำคัญ:
17
+ คลาส MatchFC_TPL ด้านล่างนี้ "reverse-engineer" มาจากการอ่าน shape ของ
18
+ state_dict ใน best_match_cnn(TPL).pth โดยตรง (ไม่มี notebook ต้นฉบับให้ดู)
19
+ โครงสร้างชั้น Linear ทุกชั้นตรงกับ state_dict 100% (ตรวจสอบแล้ว)
20
+ แต่ตัว activation function ระหว่างชั้น (ReLU) เป็นการอนุมานจากรูปแบบเดียวกับ
21
+ MatchCNN_V2 ตัวเดิมของผู้เขียนคนเดียวกัน ถ้าผลลัพธ์ดูแปลกๆ (เช่น probability
22
+ กระจุกที่ 0.5 หรือ 0/1 สุดขั้วผิดปกติ) ให้ลองเทียบกับ v2.ipynb/ANNtest.ipynb
23
+ ถ้ามีต้นฉบับจริง
24
+ """
25
+
26
+ import os
27
+ import sys
28
+ import argparse
29
+
30
+ import torch
31
+ import torch.nn as nn
32
+ import torch.nn.functional as F
33
+ from torchvision import transforms
34
+ from PIL import Image
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Config
38
+ # ---------------------------------------------------------------------------
39
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
40
+ ENCODER_WEIGHTS = os.path.join(SCRIPT_DIR, "..", "Model", "scar_encoder_triplet.pth")
41
+ MATCHER_WEIGHTS = os.path.join(SCRIPT_DIR, "..", "Model", "best_match_cnn(TPL).pth")
42
+ VITAE_LIBRARY_DIR = SCRIPT_DIR
43
+ MATCH_THRESHOLD = 0.5
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # MatchFC_TPL — reverse-engineered จาก state_dict ของ best_match_cnn(TPL).pth
48
+ #
49
+ # feature_processing.0.weight (512, 1024) -> Linear(1024, 512)
50
+ # feature_processing.2.weight (256, 512) -> Linear(512, 256)
51
+ # classifier.0.weight (128, 257) -> Linear(257, 128)
52
+ # classifier.3.weight (1, 128) -> Linear(128, 1)
53
+ #
54
+ # 1024 = template1(512) + template2(512) ต่อกัน
55
+ # 257 = fused_feature(256) + cos_sim(1) ต่อกัน
56
+ # ---------------------------------------------------------------------------
57
+ class MatchFC_TPL(nn.Module):
58
+ def __init__(self):
59
+ super().__init__()
60
+ self.feature_processing = nn.Sequential(
61
+ nn.Linear(1024, 512),
62
+ nn.ReLU(),
63
+ nn.Linear(512, 256),
64
+ nn.ReLU(),
65
+ )
66
+ self.classifier = nn.Sequential(
67
+ nn.Linear(256 + 1, 128),
68
+ nn.ReLU(),
69
+ nn.Dropout(0.5),
70
+ nn.Linear(128, 1),
71
+ nn.Sigmoid(),
72
+ )
73
+
74
+ def forward(self, template1, template2, cos_sim):
75
+ combined = torch.cat([template1, template2], dim=1) # (B, 1024)
76
+ feat = self.feature_processing(combined) # (B, 256)
77
+ fused = torch.cat([feat, cos_sim], dim=1) # (B, 257)
78
+ return self.classifier(fused).squeeze()
79
+
80
+
81
+ def check_dependencies():
82
+ missing = []
83
+ if not os.path.exists(ENCODER_WEIGHTS):
84
+ missing.append(f" - {ENCODER_WEIGHTS}")
85
+ if not os.path.exists(MATCHER_WEIGHTS):
86
+ missing.append(f" - {MATCHER_WEIGHTS}")
87
+ vitae_path = os.path.join(VITAE_LIBRARY_DIR, "ViTAE_library.py")
88
+ if not os.path.exists(vitae_path):
89
+ missing.append(f" - {vitae_path}")
90
+
91
+ if missing:
92
+ print("❌ ไฟล์ที่จำเป็นยังไม่ครบ ขาดไฟล์ต่อไปนี้:")
93
+ for m in missing:
94
+ print(m)
95
+ sys.exit(1)
96
+
97
+
98
+ def load_models(device):
99
+ check_dependencies()
100
+
101
+ sys.path.insert(0, VITAE_LIBRARY_DIR)
102
+ from ViTAE_library import ViTAutoencoder # noqa: E402
103
+
104
+ print("กำลังโหลด ViT encoder (triplet) ...")
105
+ encoder = ViTAutoencoder(
106
+ img_size=224,
107
+ patch_size=16,
108
+ in_channels=1,
109
+ embed_dim=256,
110
+ latent_dim=512,
111
+ num_heads=16,
112
+ num_layers=6,
113
+ ).to(device)
114
+ encoder.load_state_dict(torch.load(ENCODER_WEIGHTS, map_location=device))
115
+ encoder.eval()
116
+
117
+ print("กำลังโหลด MatchFC_TPL ...")
118
+ matcher = MatchFC_TPL().to(device)
119
+ matcher.load_state_dict(torch.load(MATCHER_WEIGHTS, map_location=device))
120
+ matcher.eval()
121
+
122
+ print("✅ โหลดโมเดลครบแล้ว\n")
123
+ return encoder, matcher
124
+
125
+
126
+ def predict_pair(path1, path2, encoder, matcher, device, transform):
127
+ for p in (path1, path2):
128
+ if not os.path.exists(p):
129
+ raise FileNotFoundError(f"ไม่พบไฟล์ภาพ: {p}")
130
+
131
+ img1 = transform(Image.open(path1).convert("L")).unsqueeze(0).to(device)
132
+ img2 = transform(Image.open(path2).convert("L")).unsqueeze(0).to(device)
133
+
134
+ with torch.no_grad():
135
+ # .encode() ให้ template 512 มิติตรงๆ (ไม่ผ่าน decode กลับเป็นภาพ)
136
+ t1 = encoder.encode(img1) # (1, 512)
137
+ t2 = encoder.encode(img2) # (1, 512)
138
+ cos_sim = F.cosine_similarity(t1, t2, dim=1).unsqueeze(1) # (1, 1)
139
+ prob = matcher(t1, t2, cos_sim)
140
+
141
+ return prob.item()
142
+
143
+
144
+ def main():
145
+ parser = argparse.ArgumentParser(description="ทดสอบโมเดลจับคู่ม่านตา (TPL pipeline)")
146
+ parser.add_argument("image1", help="path ภาพม่านตารูปที่ 1")
147
+ parser.add_argument("image2", help="path ภาพม่านตารูปที่ 2")
148
+ args = parser.parse_args()
149
+
150
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
151
+ print(f"ใช้งานบน device: {device}\n")
152
+
153
+ encoder, matcher = load_models(device)
154
+
155
+ transform = transforms.Compose(
156
+ [transforms.Resize((224, 224)), transforms.ToTensor()]
157
+ )
158
+
159
+ prob = predict_pair(args.image1, args.image2, encoder, matcher, device, transform)
160
+
161
+ print("=" * 45)
162
+ print(f"ภาพ 1: {args.image1}")
163
+ print(f"ภาพ 2: {args.image2}")
164
+ print(f"Match probability: {prob:.4f}")
165
+ if prob > MATCH_THRESHOLD:
166
+ print("ขลลัพธ์: ✅ MATCH (๙่าจะเป็นคนเดียวกัน)")
167
+ else:
168
+ print("ขลลัพธ์: ❌ NO MATCH (๙่าจะคนละคน)")
169
+ print("=" * 45)
170
+
171
+
172
+ if __name__ == "__main__":
173
+ main()