erfanasgari21 commited on
Commit
8ca6224
·
1 Parent(s): 48b1346

cursor review

Browse files
config/config.yaml CHANGED
@@ -42,19 +42,21 @@ synthesis:
42
  area_ratio_min: 0.5
43
  area_ratio_max: 0.8
44
  severity_levels: ["easy", "medium", "hard"]
45
- # alpha_i, beta_i per severity level (i = 1..3), tune as needed
46
- alpha: [0.3, 0.6, 0.9]
47
  beta: [0.10, 0.05, 0.0]
48
  ssim_thresholds: [0.9, 0.7] # boundaries splitting easy/medium/hard by SSIM
49
 
50
  model:
51
  in_channels: 3
52
  base_channels: 32
53
- diffneck_factor: 0.1
54
  localization_scales: 6
55
  dilation_rates: [1, 1, 2, 4, 8, 16] # scale1..2 plain conv, scale3..6 dilated
56
  reconstruction_norm: "instance" # instance norm in reconstruction sub-net
57
  localization_norm: "batch" # batch norm in localization sub-net
 
 
58
 
59
  train:
60
  batch_size_mvtec: 20
 
42
  area_ratio_min: 0.5
43
  area_ratio_max: 0.8
44
  severity_levels: ["easy", "medium", "hard"]
45
+ # α_i = Gaussian smooth kernel sizes (easy→smaller); β_i bias per level
46
+ alpha_kernels: [3, 7, 15]
47
  beta: [0.10, 0.05, 0.0]
48
  ssim_thresholds: [0.9, 0.7] # boundaries splitting easy/medium/hard by SSIM
49
 
50
  model:
51
  in_channels: 3
52
  base_channels: 32
53
+ diffneck_factor: 0.1 # separate-path balance (paper / Fig.8)
54
  localization_scales: 6
55
  dilation_rates: [1, 1, 2, 4, 8, 16] # scale1..2 plain conv, scale3..6 dilated
56
  reconstruction_norm: "instance" # instance norm in reconstruction sub-net
57
  localization_norm: "batch" # batch norm in localization sub-net
58
+ anomaly_smooth_kernel: 7 # Softmax → Gaussian smooth → sum
59
+ anomaly_smooth_sigma: 1.5
60
 
61
  train:
62
  batch_size_mvtec: 20
data/data_loader.py CHANGED
@@ -138,37 +138,73 @@ def get_panel_bbox(h: int, w: int, panel: str,
138
 
139
 
140
  def sample_texture_source(normal_img: np.ndarray, dtd_paths: List[str], size: Tuple[int, int]) -> np.ndarray:
 
 
141
  if len(dtd_paths) > 0 and random.random() < 0.5:
142
  path = random.choice(dtd_paths)
143
  tex = load_and_resize(path, size)
144
  else:
145
  tex = normal_img.copy()
146
- if random.random() < 0.5:
147
- tex = np.fliplr(tex).copy()
148
- if random.random() < 0.5:
149
- tex = np.flipud(tex).copy()
150
- k = random.choice([0, 1, 2, 3])
151
- tex = np.rot90(tex, k).copy()
152
- jitter = random.uniform(0.8, 1.2)
153
- tex = np.clip(tex * jitter, 0, 1)
154
- return tex
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
 
157
  def synthesize_anomaly(normal_img: np.ndarray,
158
  jnd_map: np.ndarray,
159
  dtd_paths: List[str],
160
- alpha: List[float] = (0.3, 0.6, 0.9),
161
  beta: List[float] = (0.10, 0.05, 0.0),
162
  area_ratio_min: float = 0.5,
163
- area_ratio_max: float = 0.8) -> Dict[str, np.ndarray]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  h, w, _ = normal_img.shape
165
  panel = random.choice(PANELS)
166
  y0, y1, x0, x1 = get_panel_bbox(h, w, panel, area_ratio_min, area_ratio_max)
167
 
168
  texture = sample_texture_source(normal_img, dtd_paths, (h, w))
169
 
170
- J = jnd_map[..., None]
171
-
172
  A = normal_img.copy()
173
  mask = np.zeros((h, w), dtype=np.float32)
174
 
@@ -177,30 +213,31 @@ def synthesize_anomaly(normal_img: np.ndarray,
177
  T_region = texture[region_slice]
178
  J_region = J[region_slice]
179
 
180
-
181
  A_region_accum = np.zeros_like(I_region)
182
- W_last = None
183
  for i in range(3):
184
- Wi = alpha[i] * (J_region + beta[i])
185
- Wi = np.clip(Wi, 0.0, 1.0)
186
- A_region_accum += Wi * T_region + (1 - Wi) * I_region
187
- W_last = Wi
 
 
188
  A_region = A_region_accum / 3.0
189
 
190
  A[region_slice] = A_region
191
  mask[y0:y1, x0:x1] = 1.0
192
 
193
-
194
  severity_mask = np.zeros((h, w), dtype=np.uint8)
195
  gray_orig = cv2.cvtColor((normal_img * 255).astype(np.uint8), cv2.COLOR_RGB2GRAY)
196
  gray_syn = cv2.cvtColor((A * 255).astype(np.uint8), cv2.COLOR_RGB2GRAY)
197
  _, ssim_map = ssim_fn(gray_orig, gray_syn, full=True)
198
  ssim_region = ssim_map[y0:y1, x0:x1]
199
 
 
200
  sev_region = np.zeros_like(ssim_region, dtype=np.uint8)
201
- sev_region[ssim_region < 0.7] = 3
202
- sev_region[(ssim_region >= 0.7) & (ssim_region < 0.9)] = 2
203
- sev_region[(ssim_region >= 0.9) & (ssim_region < 0.995)] = 1
204
  severity_mask[y0:y1, x0:x1] = sev_region
205
 
206
  return {
@@ -217,16 +254,24 @@ def synthesize_anomaly(normal_img: np.ndarray,
217
 
218
  class OmniALDataset(Dataset):
219
  def __init__(self, root: str, categories: List[str], dtd_paths: List[str],
220
- size: Tuple[int, int], dataset_name: str = "mvtec"):
 
221
  self.size = size
222
  self.dtd_paths = dtd_paths
223
  self.dataset_name = dataset_name
 
 
 
 
 
 
 
224
  self.samples = [] # list of (path, category)
225
  for cat in categories:
226
  paths = list_normal_images(root, cat, dataset_name)
227
  self.samples += [(p, cat) for p in paths]
228
  if len(self.samples) == 0:
229
- print(f"[WARN] {dataset_name} path {root} dosnt find ")
230
 
231
  def __len__(self):
232
  return len(self.samples)
@@ -236,9 +281,15 @@ class OmniALDataset(Dataset):
236
  img = load_and_resize(path, self.size)
237
  jnd = compute_jnd_map(img)
238
 
239
- syn = synthesize_anomaly(img, jnd, self.dtd_paths)
 
 
 
 
 
 
 
240
  anomaly_img = syn["anomaly_image"]
241
- anomaly_jnd = compute_jnd_map(anomaly_img)
242
  severity = syn["severity_mask"]
243
 
244
  to_tensor = lambda x: torch.from_numpy(x.transpose(2, 0, 1)).float() if x.ndim == 3 \
@@ -248,7 +299,6 @@ class OmniALDataset(Dataset):
248
  "image": to_tensor(img),
249
  "jnd": to_tensor(jnd),
250
  "anomaly_img": to_tensor(anomaly_img),
251
- "anomaly_jnd": to_tensor(anomaly_jnd),
252
  "severity": torch.from_numpy(severity).long(),
253
  "category": cat,
254
  }
@@ -272,7 +322,10 @@ def build_dataloader(cfg: dict, dataset_name: str = "mvtec", shuffle: bool = Tru
272
  size = tuple(cfg["data"]["visa_size"])
273
  batch_size = cfg["train"]["batch_size_visa"]
274
 
275
- dataset = OmniALDataset(root, categories, dtd_paths, size, dataset_name)
 
 
 
276
  loader = DataLoader(
277
  dataset,
278
  batch_size=batch_size,
 
138
 
139
 
140
  def sample_texture_source(normal_img: np.ndarray, dtd_paths: List[str], size: Tuple[int, int]) -> np.ndarray:
141
+ """Anomaly texture T: DTD or augmented normal (resize, crop, flip, color-jitter)."""
142
+ h, w = size
143
  if len(dtd_paths) > 0 and random.random() < 0.5:
144
  path = random.choice(dtd_paths)
145
  tex = load_and_resize(path, size)
146
  else:
147
  tex = normal_img.copy()
148
+
149
+ # Random resize then crop back to (h, w)
150
+ scale = random.uniform(0.8, 1.4)
151
+ nh, nw = max(int(h * scale), h), max(int(w * scale), w)
152
+ tex = cv2.resize(tex, (nw, nh), interpolation=cv2.INTER_LINEAR)
153
+ y0 = random.randint(0, max(nh - h, 0))
154
+ x0 = random.randint(0, max(nw - w, 0))
155
+ tex = tex[y0:y0 + h, x0:x0 + w]
156
+ if tex.shape[0] != h or tex.shape[1] != w:
157
+ tex = cv2.resize(tex, (w, h), interpolation=cv2.INTER_LINEAR)
158
+
159
+ if random.random() < 0.5:
160
+ tex = np.fliplr(tex).copy()
161
+ if random.random() < 0.5:
162
+ tex = np.flipud(tex).copy()
163
+ # Color jitter
164
+ jitter = random.uniform(0.8, 1.2)
165
+ brightness = random.uniform(-0.1, 0.1)
166
+ tex = np.clip(tex * jitter + brightness, 0, 1)
167
+ return tex.astype(np.float32)
168
+
169
+
170
+ def _smooth_weight(weight: np.ndarray, kernel_size: int) -> np.ndarray:
171
+ """α_i as a smooth kernel (paper): softens anomaly/normal boundary. Easy → smaller kernel."""
172
+ k = int(kernel_size)
173
+ if k < 1:
174
+ return weight
175
+ if k % 2 == 0:
176
+ k += 1
177
+ return cv2.GaussianBlur(weight, (k, k), 0)
178
 
179
 
180
  def synthesize_anomaly(normal_img: np.ndarray,
181
  jnd_map: np.ndarray,
182
  dtd_paths: List[str],
183
+ alpha_kernels: List[int] = (3, 7, 15),
184
  beta: List[float] = (0.10, 0.05, 0.0),
185
  area_ratio_min: float = 0.5,
186
+ area_ratio_max: float = 0.8,
187
+ ssim_thresholds: Tuple[float, float] = (0.9, 0.7),
188
+ # backward-compat alias used by older notebook cells
189
+ alpha: List = None) -> Dict[str, np.ndarray]:
190
+ """
191
+ Panel-guided synthesis (Eq. 1–2).
192
+ Wi = αi * (J + βi) where αi is a smoothing kernel (not a scalar).
193
+ A = mean_i (Wi*T + (1-Wi)*I) — average keeps pixels in [0,1] (paper writes Σ).
194
+ Severity GT refined via SSIM(I, A) into easy/medium/hard.
195
+ """
196
+ if alpha is not None:
197
+ # If caller still passes scalar alphas, treat odds-ish values as kernel sizes
198
+ alpha_kernels = [max(3, int(round(a * 15)) | 1) if isinstance(a, float) else int(a)
199
+ for a in alpha]
200
+
201
  h, w, _ = normal_img.shape
202
  panel = random.choice(PANELS)
203
  y0, y1, x0, x1 = get_panel_bbox(h, w, panel, area_ratio_min, area_ratio_max)
204
 
205
  texture = sample_texture_source(normal_img, dtd_paths, (h, w))
206
 
207
+ J = jnd_map.astype(np.float32)
 
208
  A = normal_img.copy()
209
  mask = np.zeros((h, w), dtype=np.float32)
210
 
 
213
  T_region = texture[region_slice]
214
  J_region = J[region_slice]
215
 
 
216
  A_region_accum = np.zeros_like(I_region)
 
217
  for i in range(3):
218
+ # Different random seeds / slight spatial jitter per level so regions may overlap
219
+ noise = np.random.rand(*J_region.shape).astype(np.float32) * 0.02
220
+ raw = np.clip(J_region + beta[i] + noise, 0.0, 1.0)
221
+ Wi = _smooth_weight(raw, alpha_kernels[i])
222
+ Wi = np.clip(Wi, 0.0, 1.0)[..., None]
223
+ A_region_accum += Wi * T_region + (1.0 - Wi) * I_region
224
  A_region = A_region_accum / 3.0
225
 
226
  A[region_slice] = A_region
227
  mask[y0:y1, x0:x1] = 1.0
228
 
229
+ # Refine & quantify into 3 defect levels via SSIM (paper Sec. 3.1)
230
  severity_mask = np.zeros((h, w), dtype=np.uint8)
231
  gray_orig = cv2.cvtColor((normal_img * 255).astype(np.uint8), cv2.COLOR_RGB2GRAY)
232
  gray_syn = cv2.cvtColor((A * 255).astype(np.uint8), cv2.COLOR_RGB2GRAY)
233
  _, ssim_map = ssim_fn(gray_orig, gray_syn, full=True)
234
  ssim_region = ssim_map[y0:y1, x0:x1]
235
 
236
+ thr_easy, thr_hard = ssim_thresholds # e.g. 0.9, 0.7
237
  sev_region = np.zeros_like(ssim_region, dtype=np.uint8)
238
+ sev_region[ssim_region < thr_hard] = 3 # hard
239
+ sev_region[(ssim_region >= thr_hard) & (ssim_region < thr_easy)] = 2 # medium
240
+ sev_region[(ssim_region >= thr_easy) & (ssim_region < 0.995)] = 1 # easy
241
  severity_mask[y0:y1, x0:x1] = sev_region
242
 
243
  return {
 
254
 
255
  class OmniALDataset(Dataset):
256
  def __init__(self, root: str, categories: List[str], dtd_paths: List[str],
257
+ size: Tuple[int, int], dataset_name: str = "mvtec",
258
+ synthesis_cfg: dict = None):
259
  self.size = size
260
  self.dtd_paths = dtd_paths
261
  self.dataset_name = dataset_name
262
+ syn = synthesis_cfg or {}
263
+ self.alpha_kernels = syn.get("alpha_kernels", [3, 7, 15])
264
+ self.beta = syn.get("beta", [0.10, 0.05, 0.0])
265
+ self.area_ratio_min = syn.get("area_ratio_min", 0.5)
266
+ self.area_ratio_max = syn.get("area_ratio_max", 0.8)
267
+ thr = syn.get("ssim_thresholds", [0.9, 0.7])
268
+ self.ssim_thresholds = (float(thr[0]), float(thr[1]))
269
  self.samples = [] # list of (path, category)
270
  for cat in categories:
271
  paths = list_normal_images(root, cat, dataset_name)
272
  self.samples += [(p, cat) for p in paths]
273
  if len(self.samples) == 0:
274
+ print(f"[WARN] {dataset_name}: no images found under {root}")
275
 
276
  def __len__(self):
277
  return len(self.samples)
 
281
  img = load_and_resize(path, self.size)
282
  jnd = compute_jnd_map(img)
283
 
284
+ syn = synthesize_anomaly(
285
+ img, jnd, self.dtd_paths,
286
+ alpha_kernels=self.alpha_kernels,
287
+ beta=self.beta,
288
+ area_ratio_min=self.area_ratio_min,
289
+ area_ratio_max=self.area_ratio_max,
290
+ ssim_thresholds=self.ssim_thresholds,
291
+ )
292
  anomaly_img = syn["anomaly_image"]
 
293
  severity = syn["severity_mask"]
294
 
295
  to_tensor = lambda x: torch.from_numpy(x.transpose(2, 0, 1)).float() if x.ndim == 3 \
 
299
  "image": to_tensor(img),
300
  "jnd": to_tensor(jnd),
301
  "anomaly_img": to_tensor(anomaly_img),
 
302
  "severity": torch.from_numpy(severity).long(),
303
  "category": cat,
304
  }
 
322
  size = tuple(cfg["data"]["visa_size"])
323
  batch_size = cfg["train"]["batch_size_visa"]
324
 
325
+ dataset = OmniALDataset(
326
+ root, categories, dtd_paths, size, dataset_name,
327
+ synthesis_cfg=cfg.get("synthesis", {}),
328
+ )
329
  loader = DataLoader(
330
  dataset,
331
  batch_size=batch_size,
models/__init__.py CHANGED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .model import (
2
+ OmniAL,
3
+ DCSABlock,
4
+ ReconstructionNet,
5
+ DiffNeck,
6
+ DiffNeckMultiScale,
7
+ LocalizationNet,
8
+ BasicBlock,
9
+ )
10
+
11
+ __all__ = [
12
+ "OmniAL",
13
+ "DCSABlock",
14
+ "ReconstructionNet",
15
+ "DiffNeck",
16
+ "DiffNeckMultiScale",
17
+ "LocalizationNet",
18
+ "BasicBlock",
19
+ ]
models/model.py CHANGED
@@ -4,7 +4,7 @@ import torch.nn.functional as F
4
 
5
 
6
  # ---------------------------------------------------------------------------
7
- # 0. توابع کمکی نرمال‌سازی
8
  # ---------------------------------------------------------------------------
9
 
10
  def get_norm_layer(norm_type: str, num_channels: int):
@@ -13,7 +13,7 @@ def get_norm_layer(norm_type: str, num_channels: int):
13
  elif norm_type == "batch":
14
  return nn.BatchNorm2d(num_channels)
15
  else:
16
- raise ValueError(f"norm_type نامعتبر: {norm_type}")
17
 
18
 
19
  # ---------------------------------------------------------------------------
@@ -37,56 +37,68 @@ class BasicBlock(nn.Module):
37
 
38
 
39
  # ---------------------------------------------------------------------------
40
- # 2. DCSA Block: Dilated Channel and Spatial Attention (CBAM + dilation)
 
 
41
  # ---------------------------------------------------------------------------
42
 
43
  class ChannelAttention(nn.Module):
 
 
44
  def __init__(self, channels: int, reduction: int = 8):
45
  super().__init__()
46
  self.avg_pool = nn.AdaptiveAvgPool2d(1)
47
- self.max_pool = nn.AdaptiveMaxPool2d(1)
48
  hidden = max(channels // reduction, 4)
49
  self.mlp = nn.Sequential(
50
  nn.Conv2d(channels, hidden, 1),
51
  nn.ReLU(inplace=True),
52
  nn.Conv2d(hidden, channels, 1),
53
  )
54
- self.sigmoid = nn.Sigmoid()
55
 
56
  def forward(self, x):
57
- avg_out = self.mlp(self.avg_pool(x))
58
- max_out = self.mlp(self.max_pool(x))
59
- attn = self.sigmoid(avg_out + max_out)
60
- return x * attn
61
 
62
 
63
  class SpatialAttention(nn.Module):
64
- def __init__(self, kernel_size: int = 3, dilation: int = 3):
 
 
65
  super().__init__()
66
- padding = dilation * (kernel_size - 1) // 2
67
- self.conv = nn.Conv2d(2, 1, kernel_size, padding=padding, dilation=dilation)
68
- self.sigmoid = nn.Sigmoid()
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
  def forward(self, x):
71
  avg_out = torch.mean(x, dim=1, keepdim=True)
72
  max_out, _ = torch.max(x, dim=1, keepdim=True)
73
  concat = torch.cat([avg_out, max_out], dim=1)
74
- attn = self.sigmoid(self.conv(concat))
75
- return x * attn
76
 
77
 
78
  class DCSABlock(nn.Module):
79
- def __init__(self, channels: int, factor: float = 0.1):
80
  super().__init__()
81
  self.channel_attn = ChannelAttention(channels)
82
- self.spatial_attn = SpatialAttention()
83
- self.factor = factor
84
 
85
  def forward(self, x):
86
- residual = x
87
  x = self.channel_attn(x)
88
  x = self.spatial_attn(x)
89
- return residual + self.factor * x
90
 
91
 
92
  # ---------------------------------------------------------------------------
@@ -94,38 +106,42 @@ class DCSABlock(nn.Module):
94
  # ---------------------------------------------------------------------------
95
 
96
  class ReconstructionNet(nn.Module):
97
- def __init__(self, in_ch: int = 3, base_ch: int = 32, norm_type: str = "instance"):
 
98
  super().__init__()
99
  chs = [base_ch, base_ch * 2, base_ch * 4, base_ch * 8]
100
- self.level_channels = [chs[0], chs[1], chs[2]] # کانال سطوح مشترک برای DiffNeck (d1/e1, d2/e2, d3/e3)
101
 
102
- # Encoder
103
  self.enc1 = BasicBlock(in_ch, chs[0], norm_type)
104
- self.dcsa1 = DCSABlock(chs[0])
105
  self.enc2 = BasicBlock(chs[0], chs[1], norm_type)
106
- self.dcsa2 = DCSABlock(chs[1])
107
  self.enc3 = BasicBlock(chs[1], chs[2], norm_type)
108
- self.dcsa3 = DCSABlock(chs[2])
109
  self.enc4 = BasicBlock(chs[2], chs[3], norm_type)
110
- self.dcsa4 = DCSABlock(chs[3])
111
  self.pool = nn.MaxPool2d(2)
112
 
113
-
114
  self.up3 = nn.ConvTranspose2d(chs[3], chs[2], 2, stride=2)
115
  self.dec3 = BasicBlock(chs[2] * 2, chs[2], norm_type)
 
116
  self.up2 = nn.ConvTranspose2d(chs[2], chs[1], 2, stride=2)
117
  self.dec2 = BasicBlock(chs[1] * 2, chs[1], norm_type)
 
118
  self.up1 = nn.ConvTranspose2d(chs[1], chs[0], 2, stride=2)
119
  self.dec1 = BasicBlock(chs[0] * 2, chs[0], norm_type)
 
120
  self.out_img = nn.Conv2d(chs[0], in_ch, 1)
121
 
122
-
123
  self.up3_jnd = nn.ConvTranspose2d(chs[3], chs[2], 2, stride=2)
124
  self.dec3_jnd = BasicBlock(chs[2] * 2, chs[2], norm_type)
 
125
  self.up2_jnd = nn.ConvTranspose2d(chs[2], chs[1], 2, stride=2)
126
  self.dec2_jnd = BasicBlock(chs[1] * 2, chs[1], norm_type)
 
127
  self.up1_jnd = nn.ConvTranspose2d(chs[1], chs[0], 2, stride=2)
128
  self.dec1_jnd = BasicBlock(chs[0] * 2, chs[0], norm_type)
 
129
  self.out_jnd = nn.Conv2d(chs[0], 1, 1)
130
 
131
  def forward(self, x):
@@ -137,57 +153,50 @@ class ReconstructionNet(nn.Module):
137
  p3 = self.pool(e3)
138
  e4 = self.dcsa4(self.enc4(p3))
139
 
140
- d3 = self.up3(e4)
141
- d3 = self.dec3(torch.cat([d3, e3], dim=1))
142
- d2 = self.up2(d3)
143
- d2 = self.dec2(torch.cat([d2, e2], dim=1))
144
- d1 = self.up1(d2)
145
- d1 = self.dec1(torch.cat([d1, e1], dim=1))
146
  rec_img = torch.sigmoid(self.out_img(d1))
147
 
148
-
149
- d3j = self.up3_jnd(e4)
150
- d3j = self.dec3_jnd(torch.cat([d3j, e3], dim=1))
151
- d2j = self.up2_jnd(d3j)
152
- d2j = self.dec2_jnd(torch.cat([d2j, e2], dim=1))
153
- d1j = self.up1_jnd(d2j)
154
- d1j = self.dec1_jnd(torch.cat([d1j, e1], dim=1))
155
  rec_jnd = torch.sigmoid(self.out_jnd(d1j))
156
 
157
-
158
  feats_inp = [e1, e2, e3]
159
  feats_rec = [d1, d2, d3]
160
  feats_jnd = [d1j, d2j, d3j]
161
-
162
  return rec_img, rec_jnd, feats_inp, feats_rec, feats_jnd
163
 
164
 
165
  # ---------------------------------------------------------------------------
166
- # 4. DiffNeck Module
167
  # ---------------------------------------------------------------------------
168
 
169
  class DiffNeckLevel(nn.Module):
170
-
171
- def __init__(self, in_ch: int, out_ch: int, norm_type: str = "batch"):
172
  super().__init__()
173
- self.block_rec_inp = BasicBlock(in_ch * 2, out_ch, norm_type)
174
- self.block_jnd_inp = BasicBlock(in_ch * 2, out_ch, norm_type)
175
- self.block_rec_jnd = BasicBlock(in_ch * 2, out_ch, norm_type)
 
176
 
177
  def forward(self, rec_feat, inp_feat, jnd_feat):
178
  d_rec_inp = self.block_rec_inp(torch.cat([rec_feat, inp_feat], dim=1))
179
  d_jnd_inp = self.block_jnd_inp(torch.cat([jnd_feat, inp_feat], dim=1))
180
- d_rec_jnd = self.block_rec_jnd(torch.cat([rec_feat, jnd_feat], dim=1))
181
- return d_rec_inp + d_jnd_inp + d_rec_jnd # "all levels/pairs of differences are summed up"
 
182
 
183
 
184
  class DiffNeckMultiScale(nn.Module):
 
 
185
  def __init__(self, channels_list, out_ch: int, factor: float = 0.1, norm_type: str = "batch"):
186
  super().__init__()
187
  self.levels = nn.ModuleList([
188
- DiffNeckLevel(ch, out_ch, norm_type) for ch in channels_list
189
  ])
190
- self.factor = factor
191
  self.out_ch = out_ch
192
 
193
  def forward(self, feats_rec, feats_inp, feats_jnd):
@@ -198,75 +207,78 @@ class DiffNeckMultiScale(nn.Module):
198
  if level_out.shape[-2:] != target_size:
199
  level_out = F.interpolate(level_out, size=target_size, mode="bilinear", align_corners=False)
200
  descriptor = level_out if descriptor is None else descriptor + level_out
 
201
 
202
- return descriptor * self.factor
 
 
203
 
204
 
205
  # ---------------------------------------------------------------------------
206
- # 5. Localization Sub-network (۶ مقیاس، Dilated Conv در مقیاس ۳-۶، Batch Norm)
207
  # ---------------------------------------------------------------------------
208
 
209
  class LocalizationNet(nn.Module):
210
  def __init__(self, in_ch: int = 6, base_ch: int = 32, num_classes: int = 4,
211
- dilation_rates=(1, 1, 2, 4, 8, 16), norm_type: str = "batch"):
 
212
  super().__init__()
213
  chs = [base_ch, base_ch * 2, base_ch * 4, base_ch * 4, base_ch * 8, base_ch * 8]
214
-
215
- self.enc1 = BasicBlock(in_ch, chs[0], norm_type, dilation=dilation_rates[0])
216
- self.enc2 = BasicBlock(chs[0], chs[1], norm_type, dilation=dilation_rates[1])
217
- self.enc3 = BasicBlock(chs[1], chs[2], norm_type, dilation=dilation_rates[2])
218
- self.enc4 = BasicBlock(chs[2], chs[3], norm_type, dilation=dilation_rates[3])
219
- self.enc5 = BasicBlock(chs[3], chs[4], norm_type, dilation=dilation_rates[4])
220
- self.enc6 = BasicBlock(chs[4], chs[5], norm_type, dilation=dilation_rates[5])
 
 
 
 
 
 
 
221
 
222
  self.pool = nn.MaxPool2d(2)
223
 
224
-
225
  self.up5 = nn.ConvTranspose2d(chs[5], chs[4], 2, stride=2)
226
  self.dec5 = BasicBlock(chs[4] * 2, chs[4], norm_type)
 
227
  self.up4 = nn.ConvTranspose2d(chs[4], chs[3], 2, stride=2)
228
  self.dec4 = BasicBlock(chs[3] * 2, chs[3], norm_type)
 
229
  self.up3 = nn.ConvTranspose2d(chs[3], chs[2], 2, stride=2)
230
  self.dec3 = BasicBlock(chs[2] * 2, chs[2], norm_type)
 
231
  self.up2 = nn.ConvTranspose2d(chs[2], chs[1], 2, stride=2)
232
  self.dec2 = BasicBlock(chs[1] * 2, chs[1], norm_type)
 
233
  self.up1 = nn.ConvTranspose2d(chs[1], chs[0], 2, stride=2)
234
  self.dec1 = BasicBlock(chs[0] * 2, chs[0], norm_type)
 
235
 
236
  self.out_seg = nn.Conv2d(chs[0], num_classes, 1)
237
-
238
  self.enc_channels = chs
239
 
240
  def forward(self, x):
241
- e1 = self.enc1(x)
242
- p1 = self.pool(e1)
243
- e2 = self.enc2(p1)
244
- p2 = self.pool(e2)
245
- e3 = self.enc3(p2)
246
- p3 = self.pool(e3)
247
- e4 = self.enc4(p3)
248
- p4 = self.pool(e4)
249
- e5 = self.enc5(p4)
250
- p5 = self.pool(e5)
251
- e6 = self.enc6(p5)
252
-
253
- d5 = self.up5(e6)
254
- d5 = self.dec5(torch.cat([d5, e5], dim=1))
255
- d4 = self.up4(d5)
256
- d4 = self.dec4(torch.cat([d4, e4], dim=1))
257
- d3 = self.up3(d4)
258
- d3 = self.dec3(torch.cat([d3, e3], dim=1))
259
- d2 = self.up2(d3)
260
- d2 = self.dec2(torch.cat([d2, e2], dim=1))
261
- d1 = self.up1(d2)
262
- d1 = self.dec1(torch.cat([d1, e1], dim=1))
263
-
264
- seg_logits = self.out_seg(d1) # (B, num_classes, H, W)
265
- return seg_logits
266
 
267
 
268
  # ---------------------------------------------------------------------------
269
- # 6. مدل کامل OmniAL
270
  # ---------------------------------------------------------------------------
271
 
272
  class OmniAL(nn.Module):
@@ -274,29 +286,39 @@ class OmniAL(nn.Module):
274
  super().__init__()
275
  m = cfg["model"]
276
  base_ch = m["base_channels"]
 
277
 
278
  self.reconstruction = ReconstructionNet(
279
- in_ch=m["in_channels"], base_ch=base_ch, norm_type=m["reconstruction_norm"]
 
 
 
280
  )
281
  self.diffneck = DiffNeckMultiScale(
282
  channels_list=self.reconstruction.level_channels,
283
  out_ch=base_ch,
284
- factor=m["diffneck_factor"],
285
  norm_type=m["localization_norm"],
286
  )
 
287
  self.localization = LocalizationNet(
288
- in_ch=m["in_channels"] + base_ch, base_ch=base_ch, num_classes=4,
289
- dilation_rates=tuple(m["dilation_rates"]), norm_type=m["localization_norm"]
 
 
 
 
290
  )
291
 
292
  def forward(self, anomaly_img: torch.Tensor):
293
  rec_img, rec_jnd, feats_inp, feats_rec, feats_jnd = self.reconstruction(anomaly_img)
294
-
295
  descriptor = self.diffneck(feats_rec, feats_inp, feats_jnd)
296
-
 
 
 
297
  loc_input = torch.cat([anomaly_img, descriptor], dim=1)
298
  seg_logits = self.localization(loc_input)
299
-
300
  return {
301
  "rec_img": rec_img,
302
  "rec_jnd": rec_jnd,
 
4
 
5
 
6
  # ---------------------------------------------------------------------------
7
+ # 0. Normalization helpers
8
  # ---------------------------------------------------------------------------
9
 
10
  def get_norm_layer(norm_type: str, num_channels: int):
 
13
  elif norm_type == "batch":
14
  return nn.BatchNorm2d(num_channels)
15
  else:
16
+ raise ValueError(f"Invalid norm_type: {norm_type}")
17
 
18
 
19
  # ---------------------------------------------------------------------------
 
37
 
38
 
39
  # ---------------------------------------------------------------------------
40
+ # 2. DCSA Block (paper Sec. 3.2)
41
+ # Channel: avg-pool + two 1x1 convs, ADD back to features
42
+ # Spatial: avg+max concat → standard 3x3 path + dilated 3x3 path (×0.1) → sum
43
  # ---------------------------------------------------------------------------
44
 
45
  class ChannelAttention(nn.Module):
46
+ """Paper: average pooling + two consecutive 1x1 convs; correlations added back."""
47
+
48
  def __init__(self, channels: int, reduction: int = 8):
49
  super().__init__()
50
  self.avg_pool = nn.AdaptiveAvgPool2d(1)
 
51
  hidden = max(channels // reduction, 4)
52
  self.mlp = nn.Sequential(
53
  nn.Conv2d(channels, hidden, 1),
54
  nn.ReLU(inplace=True),
55
  nn.Conv2d(hidden, channels, 1),
56
  )
 
57
 
58
  def forward(self, x):
59
+ attn = self.mlp(self.avg_pool(x))
60
+ return x + attn
 
 
61
 
62
 
63
  class SpatialAttention(nn.Module):
64
+ """Paper: avg+max concat, then standard and dilated 3x3-norm-relu paths; dilated ×0.1."""
65
+
66
+ def __init__(self, channels: int, dilation: int = 3, factor: float = 0.1, norm_type: str = "batch"):
67
  super().__init__()
68
+ self.factor = factor
69
+ # Input to both paths is 2-channel (avg || max); map to `channels` then back to attention map
70
+ self.std_path = nn.Sequential(
71
+ nn.Conv2d(2, channels, 3, padding=1, dilation=1),
72
+ get_norm_layer(norm_type, channels),
73
+ nn.ReLU(inplace=True),
74
+ nn.Conv2d(channels, 1, 1),
75
+ )
76
+ pad_d = dilation
77
+ self.dil_path = nn.Sequential(
78
+ nn.Conv2d(2, channels, 3, padding=pad_d, dilation=dilation),
79
+ get_norm_layer(norm_type, channels),
80
+ nn.ReLU(inplace=True),
81
+ nn.Conv2d(channels, 1, 1),
82
+ )
83
 
84
  def forward(self, x):
85
  avg_out = torch.mean(x, dim=1, keepdim=True)
86
  max_out, _ = torch.max(x, dim=1, keepdim=True)
87
  concat = torch.cat([avg_out, max_out], dim=1)
88
+ attn = self.std_path(concat) + self.factor * self.dil_path(concat)
89
+ return x + attn
90
 
91
 
92
  class DCSABlock(nn.Module):
93
+ def __init__(self, channels: int, factor: float = 0.1, norm_type: str = "batch"):
94
  super().__init__()
95
  self.channel_attn = ChannelAttention(channels)
96
+ self.spatial_attn = SpatialAttention(channels, factor=factor, norm_type=norm_type)
 
97
 
98
  def forward(self, x):
 
99
  x = self.channel_attn(x)
100
  x = self.spatial_attn(x)
101
+ return x
102
 
103
 
104
  # ---------------------------------------------------------------------------
 
106
  # ---------------------------------------------------------------------------
107
 
108
  class ReconstructionNet(nn.Module):
109
+ def __init__(self, in_ch: int = 3, base_ch: int = 32, norm_type: str = "instance",
110
+ dcsa_factor: float = 0.1):
111
  super().__init__()
112
  chs = [base_ch, base_ch * 2, base_ch * 4, base_ch * 8]
113
+ self.level_channels = [chs[0], chs[1], chs[2]]
114
 
 
115
  self.enc1 = BasicBlock(in_ch, chs[0], norm_type)
116
+ self.dcsa1 = DCSABlock(chs[0], factor=dcsa_factor, norm_type=norm_type)
117
  self.enc2 = BasicBlock(chs[0], chs[1], norm_type)
118
+ self.dcsa2 = DCSABlock(chs[1], factor=dcsa_factor, norm_type=norm_type)
119
  self.enc3 = BasicBlock(chs[1], chs[2], norm_type)
120
+ self.dcsa3 = DCSABlock(chs[2], factor=dcsa_factor, norm_type=norm_type)
121
  self.enc4 = BasicBlock(chs[2], chs[3], norm_type)
122
+ self.dcsa4 = DCSABlock(chs[3], factor=dcsa_factor, norm_type=norm_type)
123
  self.pool = nn.MaxPool2d(2)
124
 
 
125
  self.up3 = nn.ConvTranspose2d(chs[3], chs[2], 2, stride=2)
126
  self.dec3 = BasicBlock(chs[2] * 2, chs[2], norm_type)
127
+ self.dcsa_d3 = DCSABlock(chs[2], factor=dcsa_factor, norm_type=norm_type)
128
  self.up2 = nn.ConvTranspose2d(chs[2], chs[1], 2, stride=2)
129
  self.dec2 = BasicBlock(chs[1] * 2, chs[1], norm_type)
130
+ self.dcsa_d2 = DCSABlock(chs[1], factor=dcsa_factor, norm_type=norm_type)
131
  self.up1 = nn.ConvTranspose2d(chs[1], chs[0], 2, stride=2)
132
  self.dec1 = BasicBlock(chs[0] * 2, chs[0], norm_type)
133
+ self.dcsa_d1 = DCSABlock(chs[0], factor=dcsa_factor, norm_type=norm_type)
134
  self.out_img = nn.Conv2d(chs[0], in_ch, 1)
135
 
 
136
  self.up3_jnd = nn.ConvTranspose2d(chs[3], chs[2], 2, stride=2)
137
  self.dec3_jnd = BasicBlock(chs[2] * 2, chs[2], norm_type)
138
+ self.dcsa_d3j = DCSABlock(chs[2], factor=dcsa_factor, norm_type=norm_type)
139
  self.up2_jnd = nn.ConvTranspose2d(chs[2], chs[1], 2, stride=2)
140
  self.dec2_jnd = BasicBlock(chs[1] * 2, chs[1], norm_type)
141
+ self.dcsa_d2j = DCSABlock(chs[1], factor=dcsa_factor, norm_type=norm_type)
142
  self.up1_jnd = nn.ConvTranspose2d(chs[1], chs[0], 2, stride=2)
143
  self.dec1_jnd = BasicBlock(chs[0] * 2, chs[0], norm_type)
144
+ self.dcsa_d1j = DCSABlock(chs[0], factor=dcsa_factor, norm_type=norm_type)
145
  self.out_jnd = nn.Conv2d(chs[0], 1, 1)
146
 
147
  def forward(self, x):
 
153
  p3 = self.pool(e3)
154
  e4 = self.dcsa4(self.enc4(p3))
155
 
156
+ d3 = self.dcsa_d3(self.dec3(torch.cat([self.up3(e4), e3], dim=1)))
157
+ d2 = self.dcsa_d2(self.dec2(torch.cat([self.up2(d3), e2], dim=1)))
158
+ d1 = self.dcsa_d1(self.dec1(torch.cat([self.up1(d2), e1], dim=1)))
 
 
 
159
  rec_img = torch.sigmoid(self.out_img(d1))
160
 
161
+ d3j = self.dcsa_d3j(self.dec3_jnd(torch.cat([self.up3_jnd(e4), e3], dim=1)))
162
+ d2j = self.dcsa_d2j(self.dec2_jnd(torch.cat([self.up2_jnd(d3j), e2], dim=1)))
163
+ d1j = self.dcsa_d1j(self.dec1_jnd(torch.cat([self.up1_jnd(d2j), e1], dim=1)))
 
 
 
 
164
  rec_jnd = torch.sigmoid(self.out_jnd(d1j))
165
 
 
166
  feats_inp = [e1, e2, e3]
167
  feats_rec = [d1, d2, d3]
168
  feats_jnd = [d1j, d2j, d3j]
 
169
  return rec_img, rec_jnd, feats_inp, feats_rec, feats_jnd
170
 
171
 
172
  # ---------------------------------------------------------------------------
173
+ # 4. DiffNeck — (Rec,Inp), (JND,Inp), (Rec,JND,Inp); separate paths ×0.1
174
  # ---------------------------------------------------------------------------
175
 
176
  class DiffNeckLevel(nn.Module):
177
+ def __init__(self, in_ch: int, out_ch: int, factor: float = 0.1, norm_type: str = "batch"):
 
178
  super().__init__()
179
+ self.factor = factor
180
+ self.block_rec_inp = BasicBlock(in_ch * 2, out_ch, norm_type) # (Rec, Inp)
181
+ self.block_jnd_inp = BasicBlock(in_ch * 2, out_ch, norm_type) # (JND, Inp)
182
+ self.block_all = BasicBlock(in_ch * 3, out_ch, norm_type) # (Rec, JND, Inp)
183
 
184
  def forward(self, rec_feat, inp_feat, jnd_feat):
185
  d_rec_inp = self.block_rec_inp(torch.cat([rec_feat, inp_feat], dim=1))
186
  d_jnd_inp = self.block_jnd_inp(torch.cat([jnd_feat, inp_feat], dim=1))
187
+ d_all = self.block_all(torch.cat([rec_feat, jnd_feat, inp_feat], dim=1))
188
+ # Separate paths balanced by factor 0.1; unified path kept at full weight
189
+ return self.factor * (d_rec_inp + d_jnd_inp) + d_all
190
 
191
 
192
  class DiffNeckMultiScale(nn.Module):
193
+ """Alias-friendly name; also exposed as DiffNeck."""
194
+
195
  def __init__(self, channels_list, out_ch: int, factor: float = 0.1, norm_type: str = "batch"):
196
  super().__init__()
197
  self.levels = nn.ModuleList([
198
+ DiffNeckLevel(ch, out_ch, factor=factor, norm_type=norm_type) for ch in channels_list
199
  ])
 
200
  self.out_ch = out_ch
201
 
202
  def forward(self, feats_rec, feats_inp, feats_jnd):
 
207
  if level_out.shape[-2:] != target_size:
208
  level_out = F.interpolate(level_out, size=target_size, mode="bilinear", align_corners=False)
209
  descriptor = level_out if descriptor is None else descriptor + level_out
210
+ return descriptor
211
 
212
+
213
+ # Public alias so notebooks can `from models.model import DiffNeck`
214
+ DiffNeck = DiffNeckMultiScale
215
 
216
 
217
  # ---------------------------------------------------------------------------
218
+ # 5. Localization Sub-network (6 scales, dilated Basic+DCSA at scales 3–6, BN)
219
  # ---------------------------------------------------------------------------
220
 
221
  class LocalizationNet(nn.Module):
222
  def __init__(self, in_ch: int = 6, base_ch: int = 32, num_classes: int = 4,
223
+ dilation_rates=(1, 1, 2, 4, 8, 16), norm_type: str = "batch",
224
+ dcsa_factor: float = 0.1):
225
  super().__init__()
226
  chs = [base_ch, base_ch * 2, base_ch * 4, base_ch * 4, base_ch * 8, base_ch * 8]
227
+ rates = list(dilation_rates)
228
+
229
+ self.enc1 = BasicBlock(in_ch, chs[0], norm_type, dilation=rates[0])
230
+ self.dcsa1 = DCSABlock(chs[0], factor=dcsa_factor, norm_type=norm_type)
231
+ self.enc2 = BasicBlock(chs[0], chs[1], norm_type, dilation=rates[1])
232
+ self.dcsa2 = DCSABlock(chs[1], factor=dcsa_factor, norm_type=norm_type)
233
+ self.enc3 = BasicBlock(chs[1], chs[2], norm_type, dilation=rates[2])
234
+ self.dcsa3 = DCSABlock(chs[2], factor=dcsa_factor, norm_type=norm_type)
235
+ self.enc4 = BasicBlock(chs[2], chs[3], norm_type, dilation=rates[3])
236
+ self.dcsa4 = DCSABlock(chs[3], factor=dcsa_factor, norm_type=norm_type)
237
+ self.enc5 = BasicBlock(chs[3], chs[4], norm_type, dilation=rates[4])
238
+ self.dcsa5 = DCSABlock(chs[4], factor=dcsa_factor, norm_type=norm_type)
239
+ self.enc6 = BasicBlock(chs[4], chs[5], norm_type, dilation=rates[5])
240
+ self.dcsa6 = DCSABlock(chs[5], factor=dcsa_factor, norm_type=norm_type)
241
 
242
  self.pool = nn.MaxPool2d(2)
243
 
 
244
  self.up5 = nn.ConvTranspose2d(chs[5], chs[4], 2, stride=2)
245
  self.dec5 = BasicBlock(chs[4] * 2, chs[4], norm_type)
246
+ self.dcsa_d5 = DCSABlock(chs[4], factor=dcsa_factor, norm_type=norm_type)
247
  self.up4 = nn.ConvTranspose2d(chs[4], chs[3], 2, stride=2)
248
  self.dec4 = BasicBlock(chs[3] * 2, chs[3], norm_type)
249
+ self.dcsa_d4 = DCSABlock(chs[3], factor=dcsa_factor, norm_type=norm_type)
250
  self.up3 = nn.ConvTranspose2d(chs[3], chs[2], 2, stride=2)
251
  self.dec3 = BasicBlock(chs[2] * 2, chs[2], norm_type)
252
+ self.dcsa_d3 = DCSABlock(chs[2], factor=dcsa_factor, norm_type=norm_type)
253
  self.up2 = nn.ConvTranspose2d(chs[2], chs[1], 2, stride=2)
254
  self.dec2 = BasicBlock(chs[1] * 2, chs[1], norm_type)
255
+ self.dcsa_d2 = DCSABlock(chs[1], factor=dcsa_factor, norm_type=norm_type)
256
  self.up1 = nn.ConvTranspose2d(chs[1], chs[0], 2, stride=2)
257
  self.dec1 = BasicBlock(chs[0] * 2, chs[0], norm_type)
258
+ self.dcsa_d1 = DCSABlock(chs[0], factor=dcsa_factor, norm_type=norm_type)
259
 
260
  self.out_seg = nn.Conv2d(chs[0], num_classes, 1)
 
261
  self.enc_channels = chs
262
 
263
  def forward(self, x):
264
+ e1 = self.dcsa1(self.enc1(x))
265
+ e2 = self.dcsa2(self.enc2(self.pool(e1)))
266
+ e3 = self.dcsa3(self.enc3(self.pool(e2)))
267
+ e4 = self.dcsa4(self.enc4(self.pool(e3)))
268
+ e5 = self.dcsa5(self.enc5(self.pool(e4)))
269
+ e6 = self.dcsa6(self.enc6(self.pool(e5)))
270
+
271
+ d5 = self.dcsa_d5(self.dec5(torch.cat([self.up5(e6), e5], dim=1)))
272
+ d4 = self.dcsa_d4(self.dec4(torch.cat([self.up4(d5), e4], dim=1)))
273
+ d3 = self.dcsa_d3(self.dec3(torch.cat([self.up3(d4), e3], dim=1)))
274
+ d2 = self.dcsa_d2(self.dec2(torch.cat([self.up2(d3), e2], dim=1)))
275
+ d1 = self.dcsa_d1(self.dec1(torch.cat([self.up1(d2), e1], dim=1)))
276
+
277
+ return self.out_seg(d1)
 
 
 
 
 
 
 
 
 
 
 
278
 
279
 
280
  # ---------------------------------------------------------------------------
281
+ # 6. Full OmniAL model
282
  # ---------------------------------------------------------------------------
283
 
284
  class OmniAL(nn.Module):
 
286
  super().__init__()
287
  m = cfg["model"]
288
  base_ch = m["base_channels"]
289
+ factor = m.get("diffneck_factor", 0.1)
290
 
291
  self.reconstruction = ReconstructionNet(
292
+ in_ch=m["in_channels"],
293
+ base_ch=base_ch,
294
+ norm_type=m["reconstruction_norm"],
295
+ dcsa_factor=factor,
296
  )
297
  self.diffneck = DiffNeckMultiScale(
298
  channels_list=self.reconstruction.level_channels,
299
  out_ch=base_ch,
300
+ factor=factor,
301
  norm_type=m["localization_norm"],
302
  )
303
+ # Loc input: RGB anomaly image + DiffNeck descriptor
304
  self.localization = LocalizationNet(
305
+ in_ch=m["in_channels"] + base_ch,
306
+ base_ch=base_ch,
307
+ num_classes=4,
308
+ dilation_rates=tuple(m["dilation_rates"]),
309
+ norm_type=m["localization_norm"],
310
+ dcsa_factor=factor,
311
  )
312
 
313
  def forward(self, anomaly_img: torch.Tensor):
314
  rec_img, rec_jnd, feats_inp, feats_rec, feats_jnd = self.reconstruction(anomaly_img)
 
315
  descriptor = self.diffneck(feats_rec, feats_inp, feats_jnd)
316
+ if descriptor.shape[-2:] != anomaly_img.shape[-2:]:
317
+ descriptor = F.interpolate(
318
+ descriptor, size=anomaly_img.shape[-2:], mode="bilinear", align_corners=False
319
+ )
320
  loc_input = torch.cat([anomaly_img, descriptor], dim=1)
321
  seg_logits = self.localization(loc_input)
 
322
  return {
323
  "rec_img": rec_img,
324
  "rec_jnd": rec_jnd,
notebooks/exploratory.ipynb CHANGED
@@ -1,306 +1,381 @@
1
  {
2
- "cells": [
3
- {
4
- "cell_type": "markdown",
5
- "metadata": {},
6
- "source": [
7
- "# OmniAL — اجرای ماژولار در Jupyter\n",
8
- "\n",
9
- "این نوت‌بوک دقیقاً همان کارهایی را که `scripts/main.py` انجام می‌دهد، مرحله‌به‌مرحله و با فراخوانی توابع ماژولار از `data/`, `models/`, `utils/`, `scripts/` اجرا می‌کند.\n",
10
- "\n",
11
- "ترتیب اجرا:\n",
12
- "1. بارگذاری config\n",
13
- "2. تست فاز ۱: data loading و پیش‌پردازش\n",
14
- "3. تست فاز ۲: JND و Panel-guided Synthesis (نمایش بصری)\n",
15
- "4. ساخت DataLoader یکپارچه\n",
16
- "5. ساخت مدل OmniAL و تست forward pass\n",
17
- "6. آموزش (train loop)\n",
18
- "7. ارزیابی (evaluate)\n",
19
- "8. نمایش نتایج"
20
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  },
22
- {
23
- "cell_type": "markdown",
24
- "metadata": {},
25
- "source": ["## 0. Setup: افزودن مسیر پروژه به sys.path و import ماژول‌ها"]
26
- },
27
- {
28
- "cell_type": "code",
29
- "execution_count": null,
30
- "metadata": {},
31
- "outputs": [],
32
- "source": [
33
- "import os, sys\n",
34
- "sys.path.append(os.path.abspath(\"..\")) # ریشه‌ی homework_code/\n",
35
- "\n",
36
- "import yaml\n",
37
- "import torch\n",
38
- "import numpy as np\n",
39
- "\n",
40
- "from data.data_loader import (\n",
41
- " list_normal_images, list_dtd_textures, load_and_resize,\n",
42
- " compute_jnd_map, synthesize_anomaly, OmniALDataset, build_dataloader\n",
43
- ")\n",
44
- "from models.model import OmniAL, DCSABlock, ReconstructionNet, DiffNeck, LocalizationNet\n",
45
- "from utils.metrics import OmniALLoss, anomaly_score_from_seg, compute_image_auroc, compute_pixel_auroc, compute_pixel_ap\n",
46
- "from utils.visualization import show_synthesis_result, show_model_output, plot_loss_curves\n",
47
- "from scripts.train import build_model_and_optimizer, run_one_epoch, train_model\n",
48
- "from scripts.evaluate import evaluate_category, evaluate_dataset\n",
49
- "\n",
50
- "with open(\"../config/config.yaml\", \"r\", encoding=\"utf-8\") as f:\n",
51
- " cfg = yaml.safe_load(f)\n",
52
- "\n",
53
- "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n",
54
- "print(\"Device:\", device)\n",
55
- "cfg[\"train\"][\"device\"] = device\n",
56
- "cfg"
57
- ]
58
- },
59
- {
60
- "cell_type": "markdown",
61
- "metadata": {},
62
- "source": ["## 1. فاز ۱ — تست بارگذاری داده\n", "قبل از اجرا، مسیرهای `data/dataset/mvtec_ad`, `data/dataset/visa`, `data/dataset/dtd/images` را در `config/config.yaml` پر کن و دیتاست‌ها را دانلود/کپی کن."]
63
- },
64
- {
65
- "cell_type": "code",
66
- "execution_count": null,
67
- "metadata": {},
68
- "outputs": [],
69
- "source": [
70
- "# تست روی یک کلاس نمونه از MVTecAD\n",
71
- "category = cfg[\"data\"][\"mvtec_categories\"][0] # مثلا 'bottle'\n",
72
- "root = cfg[\"data\"][\"mvtec_root\"]\n",
73
- "size = tuple(cfg[\"data\"][\"mvtec_size\"])\n",
74
- "\n",
75
- "normal_paths = list_normal_images(root, category, \"mvtec\")\n",
76
- "dtd_paths = list_dtd_textures(cfg[\"data\"][\"dtd_root\"])\n",
77
- "\n",
78
- "print(f\"تعداد تصاویر normal برای '{category}': {len(normal_paths)}\")\n",
79
- "print(f\"تعداد تصاویر بافت DTD: {len(dtd_paths)}\")\n",
80
- "\n",
81
- "if len(normal_paths) > 0:\n",
82
- " img = load_and_resize(normal_paths[0], size)\n",
83
- " print(\"شکل تصویر لود‌شده:\", img.shape, \"| min/max:\", img.min(), img.max())\n",
84
- "else:\n",
85
- " print(\"[!] هیچ تصویری پیدا نشد — مسیر data.mvtec_root را در config.yaml بررسی کن.\")"
86
- ]
87
- },
88
- {
89
- "cell_type": "markdown",
90
- "metadata": {},
91
- "source": ["## 2. فاز ۲ — تست JND Map و Panel-guided Synthesis (نمایش بصری)"]
92
- },
93
- {
94
- "cell_type": "code",
95
- "execution_count": null,
96
- "metadata": {},
97
- "outputs": [],
98
- "source": [
99
- "if len(normal_paths) > 0:\n",
100
- " img = load_and_resize(normal_paths[0], size)\n",
101
- " jnd = compute_jnd_map(img)\n",
102
- " syn = synthesize_anomaly(img, jnd, dtd_paths,\n",
103
- " alpha=cfg[\"synthesis\"][\"alpha\"],\n",
104
- " beta=cfg[\"synthesis\"][\"beta\"],\n",
105
- " area_ratio_min=cfg[\"synthesis\"][\"area_ratio_min\"],\n",
106
- " area_ratio_max=cfg[\"synthesis\"][\"area_ratio_max\"])\n",
107
- "\n",
108
- " show_synthesis_result(img, jnd, syn[\"anomaly_image\"], syn[\"severity_mask\"], panel_name=syn[\"panel\"])"
109
- ]
110
- },
111
- {
112
- "cell_type": "code",
113
- "execution_count": null,
114
- "metadata": {},
115
- "outputs": [],
116
- "source": [
117
- "# چند نمونه‌ی دیگر برای دیدن تنوع پنل‌ها\n",
118
- "for i in range(3):\n",
119
- " if len(normal_paths) == 0:\n",
120
- " break\n",
121
- " img = load_and_resize(normal_paths[min(i, len(normal_paths)-1)], size)\n",
122
- " jnd = compute_jnd_map(img)\n",
123
- " syn = synthesize_anomaly(img, jnd, dtd_paths)\n",
124
- " show_synthesis_result(img, jnd, syn[\"anomaly_image\"], syn[\"severity_mask\"], panel_name=syn[\"panel\"])"
125
- ]
126
- },
127
- {
128
- "cell_type": "markdown",
129
- "metadata": {},
130
- "source": ["## 3. ساخت Dataset / DataLoader یکپارچه (Unified)"]
131
- },
132
- {
133
- "cell_type": "code",
134
- "execution_count": null,
135
- "metadata": {},
136
- "outputs": [],
137
- "source": [
138
- "train_loader, train_dataset = build_dataloader(cfg, dataset_name=\"mvtec\", shuffle=True)\n",
139
- "print(\"تعداد کل نمونه‌های آموزشی (همه کلاس‌ها):\", len(train_dataset))\n",
140
- "\n",
141
- "batch = next(iter(train_loader))\n",
142
- "for k, v in batch.items():\n",
143
- " if torch.is_tensor(v):\n",
144
- " print(k, v.shape, v.dtype)\n",
145
- " else:\n",
146
- " print(k, type(v), v[:5])"
147
- ]
148
- },
149
- {
150
- "cell_type": "markdown",
151
- "metadata": {},
152
- "source": ["## 4. فاز ۳ — تست معماری شبکه (اجزا به‌صورت جدا)"]
153
- },
154
- {
155
- "cell_type": "code",
156
- "execution_count": null,
157
- "metadata": {},
158
- "outputs": [],
159
- "source": [
160
- "# تست DCSA Block به تنهایی\n",
161
- "dummy = torch.randn(2, 32, 64, 64)\n",
162
- "dcsa = DCSABlock(32)\n",
163
- "out = dcsa(dummy)\n",
164
- "print(\"DCSA output shape:\", out.shape)"
165
- ]
166
- },
167
- {
168
- "cell_type": "code",
169
- "execution_count": null,
170
- "metadata": {},
171
- "outputs": [],
172
- "source": [
173
- "# تست Reconstruction Sub-network به تنهایی\n",
174
- "recon = ReconstructionNet(norm_type=cfg[\"model\"][\"reconstruction_norm\"])\n",
175
- "x = torch.randn(2, 3, 256, 256)\n",
176
- "rec_img, rec_jnd, feats = recon(x)\n",
177
- "print(\"rec_img:\", rec_img.shape, \"| rec_jnd:\", rec_jnd.shape)\n",
178
- "print(\"feature shapes:\", [f.shape for f in feats])"
179
- ]
180
- },
181
- {
182
- "cell_type": "code",
183
- "execution_count": null,
184
- "metadata": {},
185
- "outputs": [],
186
- "source": [
187
- "# تست کامل مدل OmniAL (forward pass end-to-end)\n",
188
- "model, optimizer, criterion, device = build_model_and_optimizer(cfg)\n",
189
- "x = torch.randn(2, 3, *size).to(device)\n",
190
- "out = model(x)\n",
191
- "for k, v in out.items():\n",
192
- " print(k, v.shape)"
193
- ]
194
- },
195
- {
196
- "cell_type": "markdown",
197
- "metadata": {},
198
- "source": ["## 5. فاز ۴ — تست Loss روی یک batch واقعی"]
199
- },
200
- {
201
- "cell_type": "code",
202
- "execution_count": null,
203
- "metadata": {},
204
- "outputs": [],
205
- "source": [
206
- "batch = next(iter(train_loader))\n",
207
- "anomaly_img = batch[\"anomaly_img\"].to(device)\n",
208
- "targets = {\n",
209
- " \"image\": batch[\"image\"].to(device),\n",
210
- " \"jnd\": batch[\"jnd\"].to(device),\n",
211
- " \"severity\": batch[\"severity\"].to(device),\n",
212
- "}\n",
213
- "\n",
214
- "outputs = model(anomaly_img)\n",
215
- "losses = criterion(outputs, targets)\n",
216
- "losses"
217
- ]
218
- },
219
- {
220
- "cell_type": "markdown",
221
- "metadata": {},
222
- "source": ["## 6. آموزش مدل (Training Loop)\n", "برای تست سریع، ابتدا `epochs` را در `config.yaml` کم بگذار (مثلا 2) تا فقط pipeline را چک کنی."]
223
- },
224
- {
225
- "cell_type": "code",
226
- "execution_count": null,
227
- "metadata": {},
228
- "outputs": [],
229
- "source": [
230
- "# برای تست سریع فقط چند epoch:\n",
231
- "cfg[\"train\"][\"epochs\"] = 2\n",
232
- "history = train_model(model, train_loader, optimizer, criterion, device, cfg)\n",
233
- "plot_loss_curves(history)"
234
- ]
235
- },
236
- {
237
- "cell_type": "markdown",
238
- "metadata": {},
239
- "source": ["## 7. نمایش خروجی مدل روی یک نمونه بعد از آموزش"]
240
- },
241
- {
242
- "cell_type": "code",
243
- "execution_count": null,
244
- "metadata": {},
245
- "outputs": [],
246
- "source": [
247
- "model.eval()\n",
248
- "with torch.no_grad():\n",
249
- " batch = next(iter(train_loader))\n",
250
- " anomaly_img = batch[\"anomaly_img\"].to(device)\n",
251
- " out = model(anomaly_img)\n",
252
- " seg_map = anomaly_score_from_seg(out[\"seg_logits\"])\n",
253
- "\n",
254
- "idx = 0\n",
255
- "show_model_output(\n",
256
- " anomaly_img[idx].cpu(), out[\"rec_img\"][idx].cpu(), out[\"rec_jnd\"][idx].cpu(), seg_map[idx].cpu()\n",
257
- ")"
258
- ]
259
- },
260
- {
261
- "cell_type": "markdown",
262
- "metadata": {},
263
- "source": ["## 8. فاز ۶ — ارزیابی روی مجموعه تست (Image-AUROC, Pixel-AUROC, Pixel-AP)"]
264
- },
265
- {
266
- "cell_type": "code",
267
- "execution_count": null,
268
- "metadata": {},
269
- "outputs": [],
270
- "source": [
271
- "results = evaluate_dataset(model, cfg, dataset_name=\"mvtec\", device=device)\n",
272
- "results"
273
- ]
274
- },
275
- {
276
- "cell_type": "markdown",
277
- "metadata": {},
278
- "source": ["## 9. (اختیاری) ذخیره‌ی مدل نهایی"]
279
- },
280
- {
281
- "cell_type": "code",
282
- "execution_count": null,
283
- "metadata": {},
284
- "outputs": [],
285
- "source": [
286
- "os.makedirs(cfg[\"logging\"][\"ckpt_dir\"], exist_ok=True)\n",
287
- "save_path = os.path.join(cfg[\"logging\"][\"ckpt_dir\"], \"omnial_final.pt\")\n",
288
- "torch.save(model.state_dict(), save_path)\n",
289
- "print(\"مدل ذخیره شد در:\", save_path)"
290
- ]
291
- }
292
- ],
293
- "metadata": {
294
- "kernelspec": {
295
- "display_name": "Python 3",
296
- "language": "python",
297
- "name": "python3"
298
- },
299
- "language_info": {
300
- "name": "python",
301
- "version": "3.10"
302
- }
303
- },
304
- "nbformat": 4,
305
- "nbformat_minor": 5
306
  }
 
1
  {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# OmniAL — اجرای ماژولار در Jupyter\n",
8
+ "\n",
9
+ "این نوت‌بوک دقیقاً همان کارهایی را که `scripts/main.py` انجام می‌دهد، مرحله‌به‌مرحله و با فراخوانی توابع ماژولار از `data/`, `models/`, `utils/`, `scripts/` اجرا می‌کند.\n",
10
+ "\n",
11
+ "ترتیب اجرا:\n",
12
+ "1. بارگذاری config\n",
13
+ "2. تست فاز ۱: data loading و پیش‌پردازش\n",
14
+ "3. تست فاز ۲: JND و Panel-guided Synthesis (نمایش بصری)\n",
15
+ "4. ساخت DataLoader یکپارچه\n",
16
+ "5. ساخت مدل OmniAL و تست forward pass\n",
17
+ "6. آموزش (train loop)\n",
18
+ "7. ارزیابی (evaluate)\n",
19
+ "8. نمایش نتایج"
20
+ ]
21
+ },
22
+ {
23
+ "cell_type": "markdown",
24
+ "metadata": {},
25
+ "source": [
26
+ "## 0. Setup: افزودن مسیر پروژه به sys.path و import ماژول‌ها"
27
+ ]
28
+ },
29
+ {
30
+ "cell_type": "code",
31
+ "execution_count": null,
32
+ "id": "cc35b5f4",
33
+ "metadata": {},
34
+ "outputs": [],
35
+ "source": [
36
+ "import os, sys\n",
37
+ "sys.path.append(os.path.abspath(\"..\")) # ریشه‌ی homework_code/\n",
38
+ "\n",
39
+ "import yaml\n",
40
+ "import torch\n",
41
+ "import numpy as np\n",
42
+ "\n",
43
+ "from data.data_loader import (\n",
44
+ " list_normal_images, list_dtd_textures, load_and_resize,\n",
45
+ " compute_jnd_map, synthesize_anomaly, OmniALDataset, build_dataloader\n",
46
+ ")\n",
47
+ "from models.model import OmniAL, DCSABlock, ReconstructionNet, DiffNeck, LocalizationNet\n",
48
+ "from utils.metrics import OmniALLoss, anomaly_score_from_seg, compute_image_auroc, compute_pixel_auroc, compute_pixel_ap\n",
49
+ "from utils.visualization import show_synthesis_result, show_model_output, plot_loss_curves\n",
50
+ "from scripts.train import build_model_and_optimizer, run_one_epoch, train_model\n",
51
+ "from scripts.evaluate import evaluate_category, evaluate_dataset\n",
52
+ "\n",
53
+ "with open(\"../config/config.yaml\", \"r\", encoding=\"utf-8\") as f:\n",
54
+ " cfg = yaml.safe_load(f)\n",
55
+ "\n",
56
+ "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n",
57
+ "print(\"Device:\", device)\n",
58
+ "cfg[\"train\"][\"device\"] = device\n",
59
+ "print(\"reconstruction_norm:\", cfg[\"model\"][\"reconstruction_norm\"],\n",
60
+ " \"| localization_norm:\", cfg[\"model\"][\"localization_norm\"])\n",
61
+ "print(\"alpha_kernels:\", cfg[\"synthesis\"][\"alpha_kernels\"], \"| beta:\", cfg[\"synthesis\"][\"beta\"])\n",
62
+ "cfg"
63
+ ]
64
+ },
65
+ {
66
+ "cell_type": "markdown",
67
+ "metadata": {},
68
+ "source": [
69
+ "## 1. فاز ۱ — تست بارگذاری داده\n",
70
+ "قبل از اجرا، مسیرهای `data/dataset/mvtec_ad`, `data/dataset/visa`, `data/dataset/dtd/images` را در `config/config.yaml` پر کن و دیتاست‌ها را دانلود/کپی کن."
71
+ ]
72
+ },
73
+ {
74
+ "cell_type": "code",
75
+ "execution_count": null,
76
+ "metadata": {},
77
+ "outputs": [],
78
+ "source": [
79
+ "# تست روی یک کلاس نمونه از MVTecAD\n",
80
+ "category = cfg[\"data\"][\"mvtec_categories\"][0] # مثلا 'bottle'\n",
81
+ "root = cfg[\"data\"][\"mvtec_root\"]\n",
82
+ "size = tuple(cfg[\"data\"][\"mvtec_size\"])\n",
83
+ "\n",
84
+ "normal_paths = list_normal_images(root, category, \"mvtec\")\n",
85
+ "dtd_paths = list_dtd_textures(cfg[\"data\"][\"dtd_root\"])\n",
86
+ "\n",
87
+ "print(f\"تعداد تصاویر normal برای '{category}': {len(normal_paths)}\")\n",
88
+ "print(f\"تعداد تصاویر بافت DTD: {len(dtd_paths)}\")\n",
89
+ "\n",
90
+ "if len(normal_paths) > 0:\n",
91
+ " img = load_and_resize(normal_paths[0], size)\n",
92
+ " print(\"شکل تصویر لود‌شده:\", img.shape, \"| min/max:\", img.min(), img.max())\n",
93
+ "else:\n",
94
+ " print(\"[!] هیچ تصویری پیدا نشد — مسیر data.mvtec_root را در config.yaml بررسی کن.\")"
95
+ ]
96
+ },
97
+ {
98
+ "cell_type": "markdown",
99
+ "metadata": {},
100
+ "source": [
101
+ "## 2. فاز ۲ — تست JND Map و Panel-guided Synthesis (نمایش بصری)"
102
+ ]
103
+ },
104
+ {
105
+ "cell_type": "code",
106
+ "execution_count": null,
107
+ "id": "bf0c6c98",
108
+ "metadata": {},
109
+ "outputs": [],
110
+ "source": [
111
+ "if len(normal_paths) > 0:\n",
112
+ " img = load_and_resize(normal_paths[0], size)\n",
113
+ " jnd = compute_jnd_map(img)\n",
114
+ " syn = synthesize_anomaly(\n",
115
+ " img, jnd, dtd_paths,\n",
116
+ " alpha_kernels=cfg[\"synthesis\"][\"alpha_kernels\"],\n",
117
+ " beta=cfg[\"synthesis\"][\"beta\"],\n",
118
+ " area_ratio_min=cfg[\"synthesis\"][\"area_ratio_min\"],\n",
119
+ " area_ratio_max=cfg[\"synthesis\"][\"area_ratio_max\"],\n",
120
+ " ssim_thresholds=tuple(cfg[\"synthesis\"][\"ssim_thresholds\"]),\n",
121
+ " )\n",
122
+ " assert syn[\"anomaly_image\"].shape == img.shape\n",
123
+ " assert syn[\"severity_mask\"].shape == img.shape[:2]\n",
124
+ " assert syn[\"panel\"] in [\"left\", \"right\", \"top\", \"bottom\", \"all\"]\n",
125
+ " print(\"panel:\", syn[\"panel\"], \"| severity unique:\", np.unique(syn[\"severity_mask\"]))\n",
126
+ " show_synthesis_result(img, jnd, syn[\"anomaly_image\"], syn[\"severity_mask\"], panel_name=syn[\"panel\"])"
127
+ ]
128
+ },
129
+ {
130
+ "cell_type": "code",
131
+ "execution_count": null,
132
+ "metadata": {},
133
+ "outputs": [],
134
+ "source": [
135
+ "# چند نمونه‌ی دیگر برای دیدن تنوع پنل‌ها\n",
136
+ "for i in range(3):\n",
137
+ " if len(normal_paths) == 0:\n",
138
+ " break\n",
139
+ " img = load_and_resize(normal_paths[min(i, len(normal_paths)-1)], size)\n",
140
+ " jnd = compute_jnd_map(img)\n",
141
+ " syn = synthesize_anomaly(img, jnd, dtd_paths)\n",
142
+ " show_synthesis_result(img, jnd, syn[\"anomaly_image\"], syn[\"severity_mask\"], panel_name=syn[\"panel\"])"
143
+ ]
144
+ },
145
+ {
146
+ "cell_type": "markdown",
147
+ "metadata": {},
148
+ "source": [
149
+ "## 3. ساخت Dataset / DataLoader یکپارچه (Unified)"
150
+ ]
151
+ },
152
+ {
153
+ "cell_type": "code",
154
+ "execution_count": null,
155
+ "metadata": {},
156
+ "outputs": [],
157
+ "source": [
158
+ "train_loader, train_dataset = build_dataloader(cfg, dataset_name=\"mvtec\", shuffle=True)\n",
159
+ "print(\"تعداد کل نمونه‌های آموزشی (همه کلاس‌ها):\", len(train_dataset))\n",
160
+ "\n",
161
+ "batch = next(iter(train_loader))\n",
162
+ "for k, v in batch.items():\n",
163
+ " if torch.is_tensor(v):\n",
164
+ " print(k, v.shape, v.dtype)\n",
165
+ " else:\n",
166
+ " print(k, type(v), v[:5])"
167
+ ]
168
+ },
169
+ {
170
+ "cell_type": "markdown",
171
+ "metadata": {},
172
+ "source": [
173
+ "## 4. فاز ۳ — تست معماری شبکه (اجزا به‌صورت جدا)"
174
+ ]
175
+ },
176
+ {
177
+ "cell_type": "code",
178
+ "execution_count": null,
179
+ "id": "79a07eb2",
180
+ "metadata": {},
181
+ "outputs": [],
182
+ "source": [
183
+ "# تست DCSA Block به تنهایی (channel ADD + dual-path spatial)\n",
184
+ "dummy = torch.randn(2, 32, 64, 64)\n",
185
+ "dcsa = DCSABlock(32, factor=0.1, norm_type=\"instance\")\n",
186
+ "out = dcsa(dummy)\n",
187
+ "assert out.shape == dummy.shape\n",
188
+ "print(\"DCSA output shape:\", out.shape)"
189
+ ]
190
+ },
191
+ {
192
+ "cell_type": "code",
193
+ "execution_count": null,
194
+ "id": "1e2a7446",
195
+ "metadata": {},
196
+ "outputs": [],
197
+ "source": [
198
+ "# تست Reconstruction Sub-network به تنهایی\n",
199
+ "recon = ReconstructionNet(norm_type=cfg[\"model\"][\"reconstruction_norm\"])\n",
200
+ "x = torch.randn(2, 3, 256, 256)\n",
201
+ "rec_img, rec_jnd, feats_inp, feats_rec, feats_jnd = recon(x)\n",
202
+ "assert rec_img.shape == (2, 3, 256, 256)\n",
203
+ "assert rec_jnd.shape == (2, 1, 256, 256)\n",
204
+ "print(\"rec_img:\", rec_img.shape, \"| rec_jnd:\", rec_jnd.shape)\n",
205
+ "print(\"feats_inp:\", [f.shape for f in feats_inp])\n",
206
+ "print(\"feats_rec:\", [f.shape for f in feats_rec])\n",
207
+ "print(\"feats_jnd:\", [f.shape for f in feats_jnd])\n",
208
+ "\n",
209
+ "# تست DiffNeck: مسیرهای (Rec,Inp), (JND,Inp), (Rec,JND,Inp)\n",
210
+ "diffneck = DiffNeck(\n",
211
+ " channels_list=recon.level_channels,\n",
212
+ " out_ch=cfg[\"model\"][\"base_channels\"],\n",
213
+ " factor=cfg[\"model\"][\"diffneck_factor\"],\n",
214
+ " norm_type=cfg[\"model\"][\"localization_norm\"],\n",
215
+ ")\n",
216
+ "desc = diffneck(feats_rec, feats_inp, feats_jnd)\n",
217
+ "assert desc.shape[0] == 2 and desc.shape[1] == cfg[\"model\"][\"base_channels\"]\n",
218
+ "print(\"DiffNeck descriptor:\", desc.shape)\n",
219
+ "\n",
220
+ "# تست LocalizationNet به تنهایی\n",
221
+ "loc = LocalizationNet(\n",
222
+ " in_ch=3 + cfg[\"model\"][\"base_channels\"],\n",
223
+ " base_ch=cfg[\"model\"][\"base_channels\"],\n",
224
+ " num_classes=4,\n",
225
+ " dilation_rates=tuple(cfg[\"model\"][\"dilation_rates\"]),\n",
226
+ " norm_type=cfg[\"model\"][\"localization_norm\"],\n",
227
+ ")\n",
228
+ "# pad descriptor to full res if needed\n",
229
+ "if desc.shape[-2:] != x.shape[-2:]:\n",
230
+ " import torch.nn.functional as F\n",
231
+ " desc_up = F.interpolate(desc, size=x.shape[-2:], mode=\"bilinear\", align_corners=False)\n",
232
+ "else:\n",
233
+ " desc_up = desc\n",
234
+ "loc_in = torch.cat([x, desc_up], dim=1)\n",
235
+ "seg = loc(loc_in)\n",
236
+ "assert seg.shape == (2, 4, 256, 256)\n",
237
+ "print(\"Localization seg_logits:\", seg.shape)"
238
+ ]
239
+ },
240
+ {
241
+ "cell_type": "code",
242
+ "execution_count": null,
243
+ "metadata": {},
244
+ "outputs": [],
245
+ "source": [
246
+ "# تست کامل مدل OmniAL (forward pass end-to-end)\n",
247
+ "model, optimizer, criterion, device = build_model_and_optimizer(cfg)\n",
248
+ "x = torch.randn(2, 3, *size).to(device)\n",
249
+ "out = model(x)\n",
250
+ "for k, v in out.items():\n",
251
+ " print(k, v.shape)"
252
+ ]
253
+ },
254
+ {
255
+ "cell_type": "markdown",
256
+ "metadata": {},
257
+ "source": [
258
+ "## 5. فاز ۴ — تست Loss روی یک batch واقعی"
259
+ ]
260
+ },
261
+ {
262
+ "cell_type": "code",
263
+ "execution_count": null,
264
+ "metadata": {},
265
+ "outputs": [],
266
+ "source": [
267
+ "batch = next(iter(train_loader))\n",
268
+ "anomaly_img = batch[\"anomaly_img\"].to(device)\n",
269
+ "targets = {\n",
270
+ " \"image\": batch[\"image\"].to(device),\n",
271
+ " \"jnd\": batch[\"jnd\"].to(device),\n",
272
+ " \"severity\": batch[\"severity\"].to(device),\n",
273
+ "}\n",
274
+ "\n",
275
+ "outputs = model(anomaly_img)\n",
276
+ "losses = criterion(outputs, targets)\n",
277
+ "losses"
278
+ ]
279
+ },
280
+ {
281
+ "cell_type": "markdown",
282
+ "metadata": {},
283
+ "source": [
284
+ "## 6. آموزش مدل (Training Loop)\n",
285
+ "برای تست سریع، ابتدا `epochs` را در `config.yaml` کم بگذار (مثلا 2) تا فقط pipeline را چک کنی."
286
+ ]
287
+ },
288
+ {
289
+ "cell_type": "code",
290
+ "execution_count": null,
291
+ "metadata": {},
292
+ "outputs": [],
293
+ "source": [
294
+ "# برای تست سریع فقط چند epoch:\n",
295
+ "cfg[\"train\"][\"epochs\"] = 2\n",
296
+ "history = train_model(model, train_loader, optimizer, criterion, device, cfg)\n",
297
+ "plot_loss_curves(history)"
298
+ ]
299
+ },
300
+ {
301
+ "cell_type": "markdown",
302
+ "metadata": {},
303
+ "source": [
304
+ "## 7. نمایش خروجی مدل روی یک نمونه بعد از آموزش"
305
+ ]
306
+ },
307
+ {
308
+ "cell_type": "code",
309
+ "execution_count": null,
310
+ "id": "83590624",
311
+ "metadata": {},
312
+ "outputs": [],
313
+ "source": [
314
+ "model.eval()\n",
315
+ "with torch.no_grad():\n",
316
+ " batch = next(iter(train_loader))\n",
317
+ " anomaly_img = batch[\"anomaly_img\"].to(device)\n",
318
+ " out = model(anomaly_img)\n",
319
+ " seg_map = anomaly_score_from_seg(\n",
320
+ " out[\"seg_logits\"],\n",
321
+ " smooth_kernel=cfg[\"model\"].get(\"anomaly_smooth_kernel\", 7),\n",
322
+ " smooth_sigma=cfg[\"model\"].get(\"anomaly_smooth_sigma\", 1.5),\n",
323
+ " )\n",
324
+ "\n",
325
+ "idx = 0\n",
326
+ "show_model_output(\n",
327
+ " anomaly_img[idx].cpu(), out[\"rec_img\"][idx].cpu(), out[\"rec_jnd\"][idx].cpu(), seg_map[idx].cpu()\n",
328
+ ")"
329
+ ]
330
+ },
331
+ {
332
+ "cell_type": "markdown",
333
+ "metadata": {},
334
+ "source": [
335
+ "## 8. فاز ۶ — ارزیابی روی مجموعه تست (Image-AUROC, Pixel-AUROC, Pixel-AP)"
336
+ ]
337
+ },
338
+ {
339
+ "cell_type": "code",
340
+ "execution_count": null,
341
+ "metadata": {},
342
+ "outputs": [],
343
+ "source": [
344
+ "results = evaluate_dataset(model, cfg, dataset_name=\"mvtec\", device=device)\n",
345
+ "results"
346
+ ]
347
+ },
348
+ {
349
+ "cell_type": "markdown",
350
+ "metadata": {},
351
+ "source": [
352
+ "## 9. (اختیاری) ذخیره‌ی مدل نهایی"
353
+ ]
354
+ },
355
+ {
356
+ "cell_type": "code",
357
+ "execution_count": null,
358
+ "metadata": {},
359
+ "outputs": [],
360
+ "source": [
361
+ "os.makedirs(cfg[\"logging\"][\"ckpt_dir\"], exist_ok=True)\n",
362
+ "save_path = os.path.join(cfg[\"logging\"][\"ckpt_dir\"], \"omnial_final.pt\")\n",
363
+ "torch.save(model.state_dict(), save_path)\n",
364
+ "print(\"مدل ذخیره شد در:\", save_path)"
365
+ ]
366
+ }
367
+ ],
368
+ "metadata": {
369
+ "kernelspec": {
370
+ "display_name": "Python 3",
371
+ "language": "python",
372
+ "name": "python3"
373
+ },
374
+ "language_info": {
375
+ "name": "python",
376
+ "version": "3.10"
377
+ }
378
  },
379
+ "nbformat": 4,
380
+ "nbformat_minor": 5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  }
scripts/evaluate.py CHANGED
@@ -16,7 +16,8 @@ from utils.metrics import anomaly_score_from_seg, compute_image_auroc, compute_p
16
 
17
 
18
  @torch.no_grad()
19
- def evaluate_category(model, root: str, category: str, size, device: str, dataset_name: str = "mvtec"):
 
20
  """
21
  مدل را روی تصاویر تست یک کلاس اجرا می‌کند و امتیازهای سطح تصویر/پیکسل را برمی‌گرداند.
22
  خروجی: dict شامل image_scores, image_labels, pixel_scores(list), pixel_labels(list)
@@ -32,7 +33,9 @@ def evaluate_category(model, root: str, category: str, size, device: str, datase
32
  img_t = torch.from_numpy(img.transpose(2, 0, 1)).float().unsqueeze(0).to(device)
33
 
34
  out = model(img_t)
35
- seg_map = anomaly_score_from_seg(out["seg_logits"])[0].cpu().numpy() # (H,W)
 
 
36
 
37
  label = 0 if defect_type == "good" else 1
38
  image_scores.append(seg_map.max())
@@ -67,9 +70,15 @@ def evaluate_dataset(model, cfg: dict, dataset_name: str = "mvtec", device: str
67
  categories = cfg["data"]["visa_categories"]
68
  size = tuple(cfg["data"]["visa_size"])
69
 
 
 
 
70
  results = {}
71
  for cat in categories:
72
- res = evaluate_category(model, root, cat, size, device, dataset_name)
 
 
 
73
  if len(np.unique(res["image_labels"])) < 2:
74
  print(f"[WARN] کلاس {cat}: هر دو لیبل normal/anomaly برای محاسبه AUROC موجود نیست.")
75
  continue
 
16
 
17
 
18
  @torch.no_grad()
19
+ def evaluate_category(model, root: str, category: str, size, device: str, dataset_name: str = "mvtec",
20
+ smooth_kernel: int = 7, smooth_sigma: float = 1.5):
21
  """
22
  مدل را روی تصاویر تست یک کلاس اجرا می‌کند و امتیازهای سطح تصویر/پیکسل را برمی‌گرداند.
23
  خروجی: dict شامل image_scores, image_labels, pixel_scores(list), pixel_labels(list)
 
33
  img_t = torch.from_numpy(img.transpose(2, 0, 1)).float().unsqueeze(0).to(device)
34
 
35
  out = model(img_t)
36
+ seg_map = anomaly_score_from_seg(
37
+ out["seg_logits"], smooth_kernel=smooth_kernel, smooth_sigma=smooth_sigma
38
+ )[0].cpu().numpy() # (H,W)
39
 
40
  label = 0 if defect_type == "good" else 1
41
  image_scores.append(seg_map.max())
 
70
  categories = cfg["data"]["visa_categories"]
71
  size = tuple(cfg["data"]["visa_size"])
72
 
73
+ smooth_k = cfg["model"].get("anomaly_smooth_kernel", 7)
74
+ smooth_s = cfg["model"].get("anomaly_smooth_sigma", 1.5)
75
+
76
  results = {}
77
  for cat in categories:
78
+ res = evaluate_category(
79
+ model, root, cat, size, device, dataset_name,
80
+ smooth_kernel=smooth_k, smooth_sigma=smooth_s,
81
+ )
82
  if len(np.unique(res["image_labels"])) < 2:
83
  print(f"[WARN] کلاس {cat}: هر دو لیبل normal/anomaly برای محاسبه AUROC موجود نیست.")
84
  continue
utils/metrics.py CHANGED
@@ -113,14 +113,30 @@ class OmniALLoss(nn.Module):
113
  # 2. معیارهای ارزیابی
114
  # ---------------------------------------------------------------------------
115
 
116
- def anomaly_score_from_seg(seg_logits: torch.Tensor) -> torch.Tensor:
 
 
 
 
 
 
 
 
 
117
  """
118
- نقشه‌ی امتیاز آنومالی پیوسته را از خروجی سگمنتیشن می‌سازد:
119
- مجموع احتمال کلاس‌های easy+medium+hard (یعنی 1 - prob(background)).
120
  """
121
  probs = F.softmax(seg_logits, dim=1)
122
- anomaly_prob = 1 - probs[:, 0, :, :] # (B,H,W)
123
- return anomaly_prob
 
 
 
 
 
 
 
124
 
125
 
126
  def compute_image_auroc(image_scores: np.ndarray, image_labels: np.ndarray) -> float:
 
113
  # 2. معیارهای ارزیابی
114
  # ---------------------------------------------------------------------------
115
 
116
+ def _gaussian_kernel_2d(kernel_size: int = 7, sigma: float = 1.5, device="cpu", dtype=torch.float32):
117
+ coords = torch.arange(kernel_size, dtype=dtype, device=device) - kernel_size // 2
118
+ g = torch.exp(-(coords ** 2) / (2 * sigma ** 2))
119
+ g = g / g.sum()
120
+ return g.unsqueeze(1) @ g.unsqueeze(0)
121
+
122
+
123
+ def anomaly_score_from_seg(seg_logits: torch.Tensor,
124
+ smooth_kernel: int = 7,
125
+ smooth_sigma: float = 1.5) -> torch.Tensor:
126
  """
127
+ Paper Sec. 3.3: Softmax → smoothness → sum of anomaly-level maps.
128
+ Classes: 0=background, 1=easy, 2=medium, 3=hard.
129
  """
130
  probs = F.softmax(seg_logits, dim=1)
131
+ anomaly = probs[:, 1:, :, :].sum(dim=1, keepdim=True) # (B,1,H,W)
132
+
133
+ if smooth_kernel and smooth_kernel > 1:
134
+ k = smooth_kernel if smooth_kernel % 2 == 1 else smooth_kernel + 1
135
+ kernel = _gaussian_kernel_2d(k, smooth_sigma, device=anomaly.device, dtype=anomaly.dtype)
136
+ kernel = kernel.view(1, 1, k, k)
137
+ anomaly = F.conv2d(anomaly, kernel, padding=k // 2)
138
+
139
+ return anomaly.squeeze(1) # (B,H,W)
140
 
141
 
142
  def compute_image_auroc(image_scores: np.ndarray, image_labels: np.ndarray) -> float: