Shuxian commited on
Commit
255fc2e
·
1 Parent(s): 53d9d1c

a stable version

Browse files
README.md CHANGED
@@ -1,14 +1,63 @@
1
- ---
2
- title: SGDNet Fish Classification
3
- emoji: 🦀
4
- colorFrom: blue
5
- colorTo: purple
6
- sdk: gradio
7
- sdk_version: 6.5.1
8
- app_file: app.py
9
- pinned: false
10
- license: gpl-3.0
11
- short_description: SGD Network for robust fish classification
12
- ---
13
-
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SGD-Net: Shape-Guided Dual-Attention Network for Fish Classification
2
+
3
+ Hugging Face Spaces demo for SGD-Net fish classification system.
4
+
5
+ ## Overview
6
+
7
+ SGD-Net is a deep learning framework designed for robust fish classification (wild vs. farmed) under challenging environmental conditions. The method combines RGB and depth information through a novel shape-guided dual-attention mechanism, achieving superior performance and robustness compared to baseline methods.
8
+
9
+ ## Features
10
+
11
+ - **Multi-modal Fusion**: Integrates RGB and depth information for enhanced feature representation
12
+ - **Shape-Guided Attention**: Leverages depth-estimated shape information to guide RGB feature learning
13
+ - **Dual-Attention Mechanism**: Combines shape-guided attention and attention gates for better feature fusion
14
+ - **Real-time Inference**: Fast classification with pre-trained models
15
+ - **Grad-CAM Visualization**: Visualize model attention with heatmaps
16
+
17
+ ## Usage
18
+
19
+ Upload an image of a fish, and the system will:
20
+ 1. Detect if it's a yellow croaker (confidence threshold: 0.5)
21
+ 2. If detected, classify it as Wild or Farmed using SGD-Net
22
+ 3. Display detection mask and Grad-CAM heatmap for interpretability
23
+
24
+ ## Model Architecture
25
+
26
+ - **Backbone**: ResNet50
27
+ - **Depth Decoder**: UNet-style decoder with attention gates
28
+ - **Classification Head**: Fully connected layers with dropout
29
+
30
+ ## Model Files
31
+
32
+ - `sgd_net_checkpoint.pth`: Trained SGD-Net model weights
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install -r requirements.txt
38
+ ```
39
+
40
+ ## Local Testing
41
+
42
+ ```bash
43
+ python app.py
44
+ ```
45
+
46
+ The application will start at `http://127.0.0.1:7860`
47
+
48
+ ## Citation
49
+
50
+ If you use this code in your research, please cite:
51
+
52
+ ```bibtex
53
+ @article{sgdnet2024,
54
+ title={SGD-Net: Shape-Guided Dual-Attention Network for Robust Fish Classification},
55
+ author={Your Name},
56
+ journal={Your Journal},
57
+ year={2024}
58
+ }
59
+ ```
60
+
61
+ ## License
62
+
63
+ See LICENSE file for details.
SETUP.md ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face Spaces 设置说明
2
+
3
+ ## 需要复制的模型文件
4
+
5
+ 在部署到 Hugging Face Spaces 之前,需要将以下模型文件复制到 `huggingface_space/` 目录:
6
+
7
+ ### 1. YOLO 模型
8
+ - **源文件路径**: `E:\ECSF\dahuangyu\code\runs\segment\train4\weights\best.pt`
9
+ - **目标文件名**: `huggingface_space/yolo_model.pt`
10
+ - **说明**: YOLO分割模型,用于检测大黄鱼并生成mask
11
+
12
+ ### 2. SGD-Net 模型
13
+ - **源文件路径**: `experiments\exp3_4_strategy_ours_stage2_finetune\checkpoint_final.pth`
14
+ - **目标文件名**: `huggingface_space/sgd_net_checkpoint.pth`
15
+ - **说明**: 训练好的SGD-Net分类模型权重
16
+
17
+ ## 复制命令(Windows PowerShell)
18
+
19
+ ```powershell
20
+ # 复制YOLO模型
21
+ Copy-Item "E:\ECSF\dahuangyu\code\runs\segment\train4\weights\best.pt" -Destination "huggingface_space\yolo_model.pt"
22
+
23
+ # 复制SGD-Net模型
24
+ Copy-Item "experiments\exp3_4_strategy_ours_stage2_finetune\checkpoint_final.pth" -Destination "huggingface_space\sgd_net_checkpoint.pth"
25
+ ```
26
+
27
+ ## 文件结构
28
+
29
+ 部署后的文件结构应该是:
30
+
31
+ ```
32
+ huggingface_space/
33
+ ├── app.py # Gradio应用主文件
34
+ ├── gradcam.py # Grad-CAM实现
35
+ ├── models/
36
+ │ ├── __init__.py
37
+ │ └── sgd_net.py # SGD-Net模型定义
38
+ ├── requirements.txt # 依赖
39
+ ├── README.md # 说明文档
40
+ ├── SETUP.md # 设置说明(本文件)
41
+ ├── yolo_model.pt # YOLO模型文件(需要复制)
42
+ └── sgd_net_checkpoint.pth # SGD-Net模型文件(需要复制)
43
+ ```
44
+
45
+ ## 部署到 Hugging Face Spaces
46
+
47
+ 1. 将整个 `huggingface_space/` 目录上传到 Hugging Face Spaces
48
+ 2. 确保所有模型文件都已上传
49
+ 3. Hugging Face Spaces 会自动安装 `requirements.txt` 中的依赖
50
+ 4. 应用会自动运行 `app.py`
51
+
52
+ ## 注意事项
53
+
54
+ - 模型文件可能较大(几百MB),上传需要一些时间
55
+ - 确保模型文件路径在代码中正确
56
+ - 首次运行时,timm会自动下载ResNet50的预训练权重
app.py ADDED
@@ -0,0 +1,408 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hugging Face Spaces App for SGD-Net Fish Classification
3
+ Enhanced with Grad-CAM visualization
4
+ """
5
+ import gradio as gr
6
+ import torch
7
+ import torch.nn.functional as F
8
+ import numpy as np
9
+ import cv2
10
+ from pathlib import Path
11
+ from ultralytics import YOLO
12
+ import albumentations as A
13
+ from albumentations.pytorch import ToTensorV2
14
+ import matplotlib.pyplot as plt
15
+ from matplotlib.colors import LinearSegmentedColormap
16
+
17
+ from models.sgd_net import ResNetSGDNet
18
+ from gradcam import GradCAM, get_target_layer
19
+
20
+ # Model paths
21
+ YOLO_MODEL_PATH = "yolo_model.pt"
22
+ SGD_NET_CHECKPOINT = "sgd_net_checkpoint.pth"
23
+
24
+ # SGD-Net configuration
25
+ SGD_NET_CONFIG = {
26
+ "backbone": "resnet50",
27
+ "num_classes": 2,
28
+ "pretrained_path": None,
29
+ "use_shape_attention": False,
30
+ "use_attention_gate": True
31
+ }
32
+
33
+ # Global variables for models
34
+ yolo_model = None
35
+ sgd_net_model = None
36
+ gradcam = None
37
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
38
+
39
+ # Preprocessing transform
40
+ transform = A.Compose([
41
+ A.Resize(256, 256),
42
+ A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
43
+ ToTensorV2()
44
+ ])
45
+
46
+
47
+ def load_models():
48
+ """Load YOLO and SGD-Net models"""
49
+ global yolo_model, sgd_net_model, gradcam
50
+
51
+ # Load YOLO model
52
+ print("Loading YOLO model...")
53
+ try:
54
+ yolo_model = YOLO(YOLO_MODEL_PATH)
55
+ print("YOLO model loaded successfully")
56
+ except Exception as e:
57
+ print(f"Error loading YOLO model: {e}")
58
+ raise
59
+
60
+ # Load SGD-Net model
61
+ print("Loading SGD-Net model...")
62
+ sgd_net_model = ResNetSGDNet(
63
+ backbone_name=SGD_NET_CONFIG["backbone"],
64
+ num_classes=SGD_NET_CONFIG["num_classes"],
65
+ pretrained=True,
66
+ pretrained_path=SGD_NET_CONFIG.get("pretrained_path"),
67
+ use_shape_attention=SGD_NET_CONFIG["use_shape_attention"],
68
+ use_attention_gate=SGD_NET_CONFIG["use_attention_gate"]
69
+ )
70
+
71
+ # Load checkpoint
72
+ print(f"Loading checkpoint from: {SGD_NET_CHECKPOINT}")
73
+ checkpoint = torch.load(SGD_NET_CHECKPOINT, map_location=device, weights_only=False)
74
+
75
+ # Handle different checkpoint formats
76
+ if isinstance(checkpoint, dict):
77
+ if 'model_state_dict' in checkpoint:
78
+ state_dict = checkpoint['model_state_dict']
79
+ elif 'state_dict' in checkpoint:
80
+ state_dict = checkpoint['state_dict']
81
+ else:
82
+ state_dict = checkpoint
83
+ else:
84
+ state_dict = checkpoint
85
+
86
+ # Filter mismatched keys
87
+ model_state_dict = sgd_net_model.state_dict()
88
+ filtered_state_dict = {}
89
+ for k, v in state_dict.items():
90
+ if k in model_state_dict and model_state_dict[k].shape == v.shape:
91
+ filtered_state_dict[k] = v
92
+ elif k.startswith('module.'):
93
+ k_new = k[7:]
94
+ if k_new in model_state_dict and model_state_dict[k_new].shape == v.shape:
95
+ filtered_state_dict[k_new] = v
96
+
97
+ sgd_net_model.load_state_dict(filtered_state_dict, strict=False)
98
+ sgd_net_model = sgd_net_model.to(device)
99
+ sgd_net_model.eval()
100
+ print("SGD-Net model loaded successfully")
101
+
102
+ # Initialize Grad-CAM
103
+ print("Initializing Grad-CAM...")
104
+ target_layer = get_target_layer(sgd_net_model)
105
+ if target_layer is not None:
106
+ gradcam = GradCAM(sgd_net_model, target_layer)
107
+ print("Grad-CAM initialized successfully")
108
+ else:
109
+ print("Warning: Could not find target layer for Grad-CAM")
110
+
111
+
112
+ def get_yolo_mask_and_confidence(rgb_image, conf_thres=0.5):
113
+ """
114
+ Generate mask using YOLO and return confidence
115
+
116
+ Returns:
117
+ mask: Binary mask (H, W) uint8, 255=foreground, 0=background
118
+ confidence: Maximum confidence, returns 0.0 if no detection
119
+ """
120
+ results = yolo_model.predict(rgb_image, conf=conf_thres, verbose=False)
121
+ result = results[0]
122
+
123
+ confidence = 0.0
124
+ mask = np.zeros(rgb_image.shape[:2], dtype=np.uint8)
125
+
126
+ if result.masks is not None and len(result.masks) > 0:
127
+ boxes = result.boxes
128
+ if len(boxes) > 0:
129
+ confs = boxes.conf.cpu().numpy()
130
+ best_idx = int(np.argmax(confs))
131
+ confidence = float(confs[best_idx])
132
+
133
+ mask_data = result.masks[best_idx].data.cpu().numpy()
134
+ if mask_data.ndim == 3:
135
+ mask_data = mask_data[0]
136
+
137
+ mask = (mask_data * 255).astype(np.uint8)
138
+
139
+ if mask.shape != rgb_image.shape[:2]:
140
+ mask = cv2.resize(mask, (rgb_image.shape[1], rgb_image.shape[0]),
141
+ interpolation=cv2.INTER_NEAREST)
142
+
143
+ return mask, confidence
144
+
145
+
146
+ def crop_image_by_mask(rgb_image, mask, expand_ratio=0.1):
147
+ """
148
+ Crop image and mask based on mask bounding box
149
+
150
+ Args:
151
+ rgb_image: RGB image (H, W, 3) numpy array
152
+ mask: Binary mask (H, W) numpy array, 255=foreground, 0=background
153
+ expand_ratio: Expansion ratio for bounding box
154
+
155
+ Returns:
156
+ rgb_cropped: Cropped RGB image
157
+ mask_cropped: Cropped mask
158
+ """
159
+ mask_binary = mask > 0
160
+ if mask_binary.sum() > 0:
161
+ coords = np.column_stack(np.where(mask_binary))
162
+ y_min, x_min = coords.min(axis=0)
163
+ y_max, x_max = coords.max(axis=0)
164
+
165
+ H_img, W_img = rgb_image.shape[:2]
166
+ margin_y = int((y_max - y_min) * expand_ratio)
167
+ margin_x = int((x_max - x_min) * expand_ratio)
168
+
169
+ y_min = max(0, y_min - margin_y)
170
+ y_max = min(H_img, y_max + margin_y)
171
+ x_min = max(0, x_min - margin_x)
172
+ x_max = min(W_img, x_max + margin_x)
173
+
174
+ # Crop RGB and mask based on bounding box
175
+ rgb_cropped = rgb_image[y_min:y_max, x_min:x_max].copy()
176
+ mask_cropped = mask[y_min:y_max, x_min:x_max].copy()
177
+ else:
178
+ # If mask is empty, use full image
179
+ rgb_cropped = rgb_image
180
+ mask_cropped = mask
181
+
182
+ return rgb_cropped, mask_cropped
183
+
184
+
185
+ def preprocess_image(rgb_image, mask):
186
+ """
187
+ Preprocess image: crop by mask, resize and normalize
188
+
189
+ Args:
190
+ rgb_image: RGB image (H, W, 3) numpy array
191
+ mask: Binary mask (H, W) numpy array
192
+
193
+ Returns:
194
+ tensor: Preprocessed image tensor (1, 3, 256, 256)
195
+ rgb_cropped: Cropped RGB image (for visualization)
196
+ mask_cropped: Cropped mask (for visualization)
197
+ """
198
+ # Step 1: Crop image and mask based on mask bounding box
199
+ rgb_cropped, mask_cropped = crop_image_by_mask(rgb_image, mask, expand_ratio=0.1)
200
+
201
+ # Step 2: Resize and normalize
202
+ transformed = transform(image=rgb_cropped)
203
+ rgb_tensor = transformed['image']
204
+ rgb_tensor = rgb_tensor.unsqueeze(0)
205
+
206
+ return rgb_tensor, rgb_cropped, mask_cropped
207
+
208
+
209
+ def generate_gradcam_heatmap(cam, original_image, mask):
210
+ """
211
+ Generate Grad-CAM heatmap overlaid on original image (only within mask)
212
+
213
+ Args:
214
+ cam: CAM heatmap (H, W) numpy array, values in [0, 1]
215
+ original_image: Original image (H, W, 3) numpy array
216
+ mask: Binary mask (H, W) numpy array, 255=foreground, 0=background
217
+
218
+ Returns:
219
+ overlay: Overlaid image (H, W, 3) numpy array
220
+ """
221
+ # Resize CAM to original image size
222
+ h, w = original_image.shape[:2]
223
+ cam_resized = cv2.resize(cam, (w, h), interpolation=cv2.INTER_LINEAR)
224
+
225
+ # Resize mask to match image size if needed
226
+ if mask.shape[:2] != (h, w):
227
+ mask_resized = cv2.resize(mask, (w, h), interpolation=cv2.INTER_NEAREST)
228
+ else:
229
+ mask_resized = mask
230
+
231
+ # Normalize mask to [0, 1]
232
+ mask_normalized = (mask_resized > 0).astype(np.float32)
233
+
234
+ # Apply mask to CAM (only show attention within mask)
235
+ cam_masked = cam_resized * mask_normalized
236
+
237
+ # Create heatmap colormap (jet colormap)
238
+ cam_colored = plt.cm.jet(cam_masked)[:, :, :3] # (H, W, 3) RGB
239
+ cam_colored = (cam_colored * 255).astype(np.uint8)
240
+
241
+ # Overlay: original * (1 - alpha) + heatmap * alpha, but only within mask
242
+ alpha = 0.4
243
+ mask_3d = np.stack([mask_normalized] * 3, axis=2) # (H, W, 3)
244
+
245
+ overlay = (original_image.astype(np.float32) * (1 - alpha * mask_3d) +
246
+ cam_colored.astype(np.float32) * (alpha * mask_3d)).astype(np.uint8)
247
+
248
+ return overlay
249
+
250
+
251
+ def classify_fish(image):
252
+ """
253
+ Classify fish image
254
+
255
+ Args:
256
+ image: PIL Image or numpy array
257
+
258
+ Returns:
259
+ tuple: (result_text, rgb_image, mask_image, gradcam_image)
260
+ """
261
+ if image is None:
262
+ return "Please upload an image", None, None, None
263
+
264
+ # Convert to numpy array (RGB format)
265
+ if hasattr(image, 'convert'):
266
+ rgb_image = np.array(image.convert('RGB'))
267
+ else:
268
+ rgb_image = np.array(image)
269
+ if len(rgb_image.shape) == 3 and rgb_image.shape[2] == 4: # RGBA
270
+ rgb_image = rgb_image[:, :, :3]
271
+
272
+ # Step 1: YOLO detection
273
+ mask, confidence = get_yolo_mask_and_confidence(rgb_image, conf_thres=0.5)
274
+
275
+ # Step 2: Check confidence
276
+ if confidence < 0.5:
277
+ result_text = f"**Detection Confidence: {confidence:.3f} < 0.5**\n\n**Result: Not Larimichthys crocea**"
278
+ mask_display = cv2.cvtColor(mask, cv2.COLOR_GRAY2RGB)
279
+ return result_text, rgb_image, mask_display, None
280
+
281
+ # Step 3: Crop and preprocess image for classification
282
+ rgb_tensor, rgb_cropped, mask_cropped = preprocess_image(rgb_image, mask)
283
+ rgb_tensor = rgb_tensor.to(device)
284
+
285
+ # Inference
286
+ with torch.no_grad():
287
+ cls_logits, _ = sgd_net_model(rgb_tensor)
288
+ probs = F.softmax(cls_logits, dim=1)
289
+ pred_class = cls_logits.argmax(dim=1).item()
290
+ prob_wild = probs[0, 0].item()
291
+ prob_farmed = probs[0, 1].item()
292
+
293
+ # Generate Grad-CAM heatmap (only within mask)
294
+ gradcam_image = None
295
+ if gradcam is not None:
296
+ try:
297
+ cam = gradcam.generate_cam(rgb_tensor, class_idx=pred_class)
298
+ # Use cropped image and mask for Grad-CAM visualization
299
+ gradcam_image = generate_gradcam_heatmap(cam, rgb_cropped, mask_cropped)
300
+ except Exception as e:
301
+ print(f"Error generating Grad-CAM: {e}")
302
+ gradcam_image = None
303
+
304
+ # Generate result text
305
+ class_name = "Wild" if pred_class == 0 else "Farmed"
306
+
307
+ result_text = f"""**Detection Confidence: {confidence:.3f} ≥ 0.5**
308
+
309
+ **Classification Result: {class_name}**
310
+
311
+ **Probability Distribution:**
312
+ - Wild: **{prob_wild:.1%}**
313
+ - Farmed: **{prob_farmed:.1%}**"""
314
+
315
+ # Convert mask to 3-channel for display (use cropped mask)
316
+ mask_display = cv2.cvtColor(mask_cropped, cv2.COLOR_GRAY2RGB)
317
+
318
+ # Use cropped image for display
319
+ return result_text, rgb_cropped, mask_display, gradcam_image
320
+
321
+
322
+ # Create Gradio interface
323
+ def create_interface():
324
+ # Custom CSS styles
325
+ custom_css = """
326
+ .gradio-container {
327
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
328
+ }
329
+ .main-header {
330
+ text-align: center;
331
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
332
+ padding: 20px;
333
+ border-radius: 10px;
334
+ color: white;
335
+ margin-bottom: 20px;
336
+ }
337
+ .result-box {
338
+ background: #f8f9fa;
339
+ border-radius: 8px;
340
+ padding: 15px;
341
+ border-left: 4px solid #667eea;
342
+ }
343
+ """
344
+
345
+ with gr.Blocks(title="Illumination-Invariant Fish Classification", css=custom_css, theme=gr.themes.Soft()) as demo:
346
+ # Title
347
+ gr.Markdown("""
348
+ <div class="main-header">
349
+ <h1>Illumination-Invariant Identification of Wild and Farmed Larimichthys crocea</h1>
350
+ <p style="font-size: 14px; margin-top: 10px;">via Cross-Modal 3D Shape Priors</p>
351
+ </div>
352
+ """)
353
+
354
+ with gr.Row():
355
+ with gr.Column(scale=1):
356
+ image_input = gr.Image(
357
+ label="Upload Image",
358
+ type="numpy",
359
+ height=300
360
+ )
361
+ classify_btn = gr.Button(
362
+ "Classify",
363
+ variant="primary",
364
+ size="lg"
365
+ )
366
+
367
+ with gr.Column(scale=2):
368
+ result_output = gr.Textbox(
369
+ label="Classification Result",
370
+ lines=8,
371
+ elem_classes=["result-box"]
372
+ )
373
+
374
+ with gr.Row():
375
+ with gr.Column():
376
+ image_output = gr.Image(
377
+ label="Original Image",
378
+ height=300
379
+ )
380
+
381
+ with gr.Column():
382
+ mask_output = gr.Image(
383
+ label="Detection Mask",
384
+ height=300
385
+ )
386
+
387
+ with gr.Column():
388
+ gradcam_output = gr.Image(
389
+ label="Grad-CAM Heatmap",
390
+ height=300
391
+ )
392
+
393
+ classify_btn.click(
394
+ fn=classify_fish,
395
+ inputs=image_input,
396
+ outputs=[result_output, image_output, mask_output, gradcam_output]
397
+ )
398
+
399
+ return demo
400
+
401
+
402
+ if __name__ == "__main__":
403
+ # Load models
404
+ load_models()
405
+
406
+ # Create and launch application
407
+ demo = create_interface()
408
+ demo.launch(share=False)
gradcam.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Grad-CAM implementation for visualization
3
+ """
4
+ import torch
5
+ import torch.nn.functional as F
6
+ import numpy as np
7
+
8
+
9
+ class GradCAM:
10
+ """Grad-CAM实现"""
11
+
12
+ def __init__(self, model, target_layer):
13
+ self.model = model
14
+ self.target_layer = target_layer
15
+ self.gradients = None
16
+ self.activations = None
17
+ self.forward_handle = None
18
+ self.backward_handle = None
19
+
20
+ # 强制开启梯度(确保梯度能回传)
21
+ for param in self.model.parameters():
22
+ param.requires_grad = True
23
+
24
+ # 注册hook
25
+ self.hook_layers()
26
+
27
+ def hook_layers(self):
28
+ """注册forward和backward hook"""
29
+ if self.forward_handle is not None:
30
+ self.forward_handle.remove()
31
+ if self.backward_handle is not None:
32
+ self.backward_handle.remove()
33
+
34
+ def backward_hook(module, grad_in, grad_out):
35
+ """标准的backward hook签名:(module, grad_in, grad_out)"""
36
+ if grad_out[0] is not None:
37
+ self.gradients = grad_out[0]
38
+
39
+ def forward_hook(module, input, output):
40
+ """forward hook保存激活值"""
41
+ self.activations = output
42
+
43
+ self.forward_handle = self.target_layer.register_forward_hook(forward_hook)
44
+ self.backward_handle = self.target_layer.register_backward_hook(backward_hook)
45
+
46
+ def generate_cam(self, input_tensor, class_idx=None):
47
+ """生成CAM热力图"""
48
+ self.gradients = None
49
+ self.activations = None
50
+
51
+ # 设置为train模式以允许梯度回传
52
+ self.model.train()
53
+ # 禁用dropout和batchnorm的随机性
54
+ for m in self.model.modules():
55
+ if isinstance(m, (torch.nn.BatchNorm2d, torch.nn.Dropout)):
56
+ m.eval()
57
+
58
+ input_tensor = input_tensor.requires_grad_(True)
59
+
60
+ # Forward pass
61
+ output = self.model(input_tensor)
62
+
63
+ # 如果是SGD-Net,取分类输出
64
+ if isinstance(output, tuple):
65
+ output = output[0]
66
+
67
+ # 如果没有指定类别,使用预测类别
68
+ if class_idx is None:
69
+ class_idx = output.argmax(dim=1).item()
70
+
71
+ # Backward pass
72
+ self.model.zero_grad()
73
+ output[0, class_idx].backward(retain_graph=True)
74
+
75
+ # 如果gradients仍然是None或全0,使用fallback方法
76
+ if self.gradients is None or (hasattr(self.gradients, 'abs') and self.gradients.abs().max().item() < 1e-8):
77
+ if self.activations is not None:
78
+ if len(self.activations.shape) == 4:
79
+ activations = self.activations[0] # (C, H, W)
80
+ else:
81
+ activations = self.activations
82
+ cam = torch.mean(activations.abs(), dim=0) # (H, W)
83
+ cam_min = cam.min()
84
+ cam_max = cam.max()
85
+ if cam_max > cam_min + 1e-8:
86
+ cam = (cam - cam_min) / (cam_max - cam_min)
87
+ else:
88
+ cam = torch.ones_like(cam) * 0.5
89
+ return cam.detach().cpu().numpy()
90
+
91
+ if self.gradients is None or self.activations is None:
92
+ return np.ones((8, 8)) * 0.5
93
+
94
+ # 计算权重(全局平均池化梯度)
95
+ if len(self.gradients.shape) == 4:
96
+ gradients = self.gradients[0] # (C, H, W)
97
+ else:
98
+ gradients = self.gradients
99
+
100
+ if len(self.activations.shape) == 4:
101
+ activations = self.activations[0] # (C, H, W)
102
+ else:
103
+ activations = self.activations
104
+
105
+ # 计算权重(对每个通道的梯度进行全局平均池化)
106
+ weights = torch.mean(gradients, dim=(1, 2), keepdim=True) # (C, 1, 1)
107
+
108
+ # 加权求和
109
+ cam = torch.sum(weights * activations, dim=0) # (H, W)
110
+ cam = F.relu(cam) # ReLU(只保留正梯度)
111
+
112
+ # 移除左上角异常高值(针对SGD-Net)
113
+ h, w = cam.shape
114
+ corner_h = h // 3
115
+ corner_w = w // 3
116
+ corner_region = cam[:corner_h, :corner_w]
117
+ rest_region = torch.cat([
118
+ cam[corner_h:, :].flatten(),
119
+ cam[:, corner_w:].flatten()
120
+ ])
121
+
122
+ if rest_region.numel() > 0:
123
+ rest_median = torch.median(rest_region)
124
+ rest_std = torch.std(rest_region)
125
+ corner_max = torch.max(corner_region)
126
+ corner_mean = torch.mean(corner_region)
127
+
128
+ if corner_max > rest_median + 1.5 * rest_std or corner_mean > rest_median + 0.5 * rest_std:
129
+ for i in range(corner_h):
130
+ for j in range(corner_w):
131
+ dist_to_edge = min(i, j)
132
+ weight = max(0.0, 1.0 - dist_to_edge / max(corner_h, corner_w) * 2.0)
133
+ weight = min(1.0, weight)
134
+ cam[i, j] = cam[i, j] * (1 - weight) + rest_median * weight
135
+
136
+ # 归一化
137
+ cam_min = cam.min()
138
+ cam_max = cam.max()
139
+
140
+ if cam_max > cam_min + 1e-8:
141
+ cam = (cam - cam_min) / (cam_max - cam_min)
142
+ else:
143
+ cam = torch.ones_like(cam) * 0.5
144
+
145
+ cam = torch.clamp(cam, 0, 1)
146
+
147
+ return cam.detach().cpu().numpy()
148
+
149
+ def __del__(self):
150
+ """清理hook"""
151
+ if self.forward_handle is not None:
152
+ self.forward_handle.remove()
153
+ if self.backward_handle is not None:
154
+ self.backward_handle.remove()
155
+
156
+
157
+ def get_target_layer(model):
158
+ """获取目标层用于Grad-CAM(针对ResNetSGDNet)"""
159
+ backbone = model.backbone
160
+
161
+ # ResNet - 取layer4的最后一个block的最后一个conv
162
+ if hasattr(backbone, 'layer4'):
163
+ layer4 = backbone.layer4
164
+ if hasattr(layer4, '__len__') and len(layer4) > 0:
165
+ last_block = layer4[-1]
166
+ if hasattr(last_block, 'conv3'):
167
+ return last_block.conv3
168
+ elif hasattr(last_block, 'conv2'):
169
+ return last_block.conv2
170
+ else:
171
+ return last_block
172
+ else:
173
+ return layer4
174
+ else:
175
+ # 默认取最后一个模块
176
+ modules = list(backbone.children())
177
+ return modules[-1] if modules else None
models/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Models package
models/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (141 Bytes). View file
 
models/__pycache__/sgd_net.cpython-310.pyc ADDED
Binary file (13.3 kB). View file
 
models/sgd_net.py ADDED
@@ -0,0 +1,694 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Shape-Guided Dual-Attention Network (SGD-Net)
3
+ """
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+ import timm
8
+ import os
9
+ from pathlib import Path
10
+ from typing import List, Tuple, Optional
11
+
12
+
13
+ class AttentionGate(nn.Module):
14
+ """Attention gate for skip connections in decoder"""
15
+
16
+ def __init__(self, F_g, F_l, F_int):
17
+ super(AttentionGate, self).__init__()
18
+ self.W_g = nn.Sequential(
19
+ nn.Conv2d(F_g, F_int, kernel_size=1, stride=1, padding=0, bias=True),
20
+ nn.BatchNorm2d(F_int)
21
+ )
22
+
23
+ self.W_x = nn.Sequential(
24
+ nn.Conv2d(F_l, F_int, kernel_size=1, stride=1, padding=0, bias=True),
25
+ nn.BatchNorm2d(F_int)
26
+ )
27
+
28
+ self.psi = nn.Sequential(
29
+ nn.Conv2d(F_int, 1, kernel_size=1, stride=1, padding=0, bias=True),
30
+ nn.BatchNorm2d(1),
31
+ nn.Sigmoid()
32
+ )
33
+
34
+ self.relu = nn.ReLU(inplace=True)
35
+
36
+ def forward(self, g, x):
37
+ g1 = self.W_g(g)
38
+ x1 = self.W_x(x)
39
+
40
+ # Handle size mismatch (upsample g1 to match x1 if needed)
41
+ if g1.shape[2:] != x1.shape[2:]:
42
+ g1 = F.interpolate(g1, size=x1.shape[2:], mode='bilinear', align_corners=False)
43
+
44
+ psi = self.relu(g1 + x1)
45
+ psi = self.psi(psi)
46
+ return x * psi
47
+
48
+
49
+ class ShapeGuidedAttention(nn.Module):
50
+ """
51
+ Shape-Guided Attention module
52
+ Uses decoder features to generate attention mask for RGB features
53
+ """
54
+
55
+ def __init__(self, rgb_channels, decoder_channels):
56
+ super(ShapeGuidedAttention, self).__init__()
57
+ self.decoder_conv = nn.Sequential(
58
+ nn.Conv2d(decoder_channels, rgb_channels, kernel_size=1),
59
+ nn.BatchNorm2d(rgb_channels),
60
+ nn.Sigmoid()
61
+ )
62
+
63
+ def forward(self, rgb_feat, decoder_feat):
64
+ """
65
+ Args:
66
+ rgb_feat: RGB features from backbone
67
+ decoder_feat: Intermediate decoder features
68
+
69
+ Returns:
70
+ Enhanced RGB features with shape-guided attention
71
+ """
72
+ attention_mask = self.decoder_conv(decoder_feat)
73
+
74
+ # Handle size mismatch (resize attention_mask to match rgb_feat if needed)
75
+ if attention_mask.shape[2:] != rgb_feat.shape[2:]:
76
+ attention_mask = F.interpolate(
77
+ attention_mask,
78
+ size=rgb_feat.shape[2:],
79
+ mode='bilinear',
80
+ align_corners=False
81
+ )
82
+
83
+ enhanced_rgb = rgb_feat * attention_mask
84
+ return enhanced_rgb
85
+
86
+
87
+ class DepthDecoder(nn.Module):
88
+ """UNet-style decoder for depth prediction"""
89
+
90
+ def __init__(self, encoder_channels, decoder_channels=[256, 128, 64, 32], use_attention_gate=True):
91
+ super(DepthDecoder, self).__init__()
92
+ self.decoder_channels = decoder_channels
93
+ self.use_attention_gate = use_attention_gate
94
+
95
+ # Reverse encoder channels for skip connections
96
+ # Exclude first (input) layer, keep only skip connection features
97
+ skip_channels = encoder_channels[::-1][1:] # Exclude first (input) layer
98
+
99
+ # Adjust decoder_channels to match available skip connections
100
+ num_skip = len(skip_channels)
101
+ num_decoder = len(decoder_channels)
102
+
103
+ if num_decoder > num_skip:
104
+ # If we have more decoder stages than skip connections, reduce decoder stages
105
+ decoder_channels = decoder_channels[:num_skip]
106
+ self.decoder_channels = decoder_channels
107
+
108
+ # Decoder blocks
109
+ self.decoder_blocks = nn.ModuleList()
110
+ in_channels = encoder_channels[-1]
111
+
112
+ for i, out_channels in enumerate(decoder_channels):
113
+ # Upsample + conv (always first)
114
+ self.decoder_blocks.append(
115
+ nn.Sequential(
116
+ nn.ConvTranspose2d(in_channels, out_channels, kernel_size=2, stride=2),
117
+ nn.BatchNorm2d(out_channels),
118
+ nn.ReLU(inplace=True),
119
+ nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
120
+ nn.BatchNorm2d(out_channels),
121
+ nn.ReLU(inplace=True)
122
+ )
123
+ )
124
+
125
+ # Attention gate (if enabled and we have a corresponding skip connection)
126
+ if self.use_attention_gate and i < len(skip_channels):
127
+ # Attention gate: F_g is upsampled feature (out_channels), F_l is skip (skip_channels[i])
128
+ self.decoder_blocks.append(
129
+ AttentionGate(F_g=out_channels, F_l=skip_channels[i], F_int=out_channels)
130
+ )
131
+ use_attention = True
132
+ else:
133
+ use_attention = False
134
+
135
+ in_channels = out_channels
136
+
137
+ # Store whether to use attention for each stage
138
+ self.use_attention = [self.use_attention_gate and i < len(skip_channels) for i in range(len(decoder_channels))]
139
+
140
+ # Channel reduction layers for combining upsampled features with attended skip connections
141
+ self.channel_reduce = nn.ModuleList()
142
+ for i, out_ch in enumerate(decoder_channels):
143
+ if i < len(skip_channels):
144
+ # Need to reduce: out_ch (upsampled) + skip_channels[i] (attended skip) -> out_ch
145
+ self.channel_reduce.append(
146
+ nn.Conv2d(out_ch + skip_channels[i], out_ch, kernel_size=1)
147
+ )
148
+ else:
149
+ self.channel_reduce.append(None)
150
+
151
+ # Final upsampling to input size (256x256)
152
+ # Calculate how many more upsampling steps needed
153
+ # Encoder outputs: [64x64, 32x32, 16x16, 8x8] for 256x256 input
154
+ # After decoder: should be 64x64, need to upsample 4x to get 256x256
155
+ final_upsample_factor = 4 # 64 -> 256 (4x)
156
+ self.final_upsample = nn.Sequential(
157
+ nn.ConvTranspose2d(decoder_channels[-1], decoder_channels[-1], kernel_size=4, stride=4),
158
+ nn.BatchNorm2d(decoder_channels[-1]),
159
+ nn.ReLU(inplace=True)
160
+ )
161
+
162
+ # Final depth prediction
163
+ self.depth_head = nn.Sequential(
164
+ nn.Conv2d(decoder_channels[-1], 1, kernel_size=1),
165
+ nn.ReLU(inplace=True) # Depth should be positive
166
+ )
167
+
168
+ def forward(self, encoder_features):
169
+ """
170
+ Args:
171
+ encoder_features: List of features from encoder [f1, f2, f3, f4]
172
+
173
+ Returns:
174
+ depth: Predicted depth map (B, 1, H, W)
175
+ intermediate_features: Intermediate decoder features for shape-guided attention
176
+ """
177
+ # Reverse for decoder (from deep to shallow)
178
+ skip_features = encoder_features[::-1]
179
+ x = skip_features[0] # Deepest feature
180
+
181
+ intermediate_features = []
182
+ block_idx = 0
183
+
184
+ # Decode with skip connections
185
+ for i in range(len(self.decoder_channels)):
186
+ # Upsample first
187
+ x = self.decoder_blocks[block_idx](x)
188
+ block_idx += 1
189
+
190
+ # Apply attention gate to skip connection (if enabled and available)
191
+ if self.use_attention[i] and (i + 1) < len(skip_features):
192
+ # Attention gate: x (upsampled) is g, skip_features[i+1] is x (skip)
193
+ skip_att = self.decoder_blocks[block_idx](x, skip_features[i + 1])
194
+ block_idx += 1
195
+ # Concatenate and reduce channels
196
+ x = torch.cat([x, skip_att], dim=1)
197
+ x = self.channel_reduce[i](x)
198
+ elif not self.use_attention[i] and (i + 1) < len(skip_features):
199
+ # No attention gate: directly concatenate skip connection
200
+ x = torch.cat([x, skip_features[i + 1]], dim=1)
201
+ x = self.channel_reduce[i](x)
202
+
203
+ intermediate_features.append(x)
204
+
205
+ # Final upsampling to input size (256x256)
206
+ x = self.final_upsample(x)
207
+
208
+ # Ensure output is exactly 256x256 (in case of rounding errors)
209
+ if x.shape[2] != 256 or x.shape[3] != 256:
210
+ x = F.interpolate(x, size=(256, 256), mode='bilinear', align_corners=False)
211
+
212
+ # Predict depth
213
+ depth = self.depth_head(x)
214
+
215
+ return depth, intermediate_features
216
+
217
+
218
+ class SGDNet(nn.Module):
219
+ """
220
+ Shape-Guided Dual-Attention Network
221
+ """
222
+
223
+ def __init__(self, backbone_name='convnextv2_tiny', num_classes=2, pretrained=True, pretrained_path=None, use_shape_attention=True, use_attention_gate=True):
224
+ super(SGDNet, self).__init__()
225
+
226
+ # Load backbone
227
+ # If pretrained_path is provided, load from local file
228
+ # Otherwise, use pretrained flag (downloads if True)
229
+ if pretrained_path is not None:
230
+ # Load model without pretrained weights first
231
+ self.backbone = timm.create_model(
232
+ backbone_name,
233
+ pretrained=False,
234
+ features_only=True,
235
+ out_indices=(0, 1, 2, 3)
236
+ )
237
+ # Load pretrained weights from local file, or download if not found
238
+ if not os.path.exists(pretrained_path):
239
+ print(f"Pretrained path not found: {pretrained_path}")
240
+ print("Attempting to download pretrained model...")
241
+ # Download and save as .pth format
242
+ try:
243
+ temp_model = timm.create_model(backbone_name, pretrained=True, features_only=True, out_indices=(0, 1, 2, 3))
244
+ # Save state dict as .pth file
245
+ pretrained_path_obj = Path(pretrained_path)
246
+ pretrained_path_obj.parent.mkdir(parents=True, exist_ok=True)
247
+ torch.save(temp_model.state_dict(), pretrained_path)
248
+ print(f"Downloaded and saved pretrained model to: {pretrained_path}")
249
+ del temp_model
250
+ except Exception as e:
251
+ print(f"Failed to download pretrained model: {e}")
252
+ raise FileNotFoundError(f"Pretrained model file not found and download failed: {pretrained_path}")
253
+
254
+ if os.path.exists(pretrained_path):
255
+ # Check file extension first
256
+ file_ext = Path(pretrained_path).suffix.lower()
257
+
258
+ if file_ext == '.safetensors':
259
+ # Load safetensors format (HuggingFace)
260
+ try:
261
+ from safetensors import safe_open
262
+ state_dict = {}
263
+ with safe_open(pretrained_path, framework="pt", device="cpu") as f:
264
+ for key in f.keys():
265
+ state_dict[key] = f.get_tensor(key)
266
+ print(f"Loaded safetensors from: {pretrained_path}")
267
+ except ImportError:
268
+ raise ImportError("safetensors library is required to load .safetensors files. Install with: pip install safetensors")
269
+ except Exception as e:
270
+ raise RuntimeError(f"Failed to load safetensors file: {e}")
271
+ else:
272
+ # Load .pth or .pt format
273
+ try:
274
+ from timm.models import load_pretrained
275
+ load_pretrained(self.backbone, pretrained_path, strict=False)
276
+ print(f"Loaded pretrained weights via timm from: {pretrained_path}")
277
+ return # Success, exit early
278
+ except:
279
+ # Fallback: manual loading with torch.load
280
+ checkpoint = torch.load(pretrained_path, map_location='cpu', weights_only=False)
281
+ # Handle different checkpoint formats
282
+ if isinstance(checkpoint, dict):
283
+ if 'state_dict' in checkpoint:
284
+ state_dict = checkpoint['state_dict']
285
+ elif 'model' in checkpoint:
286
+ state_dict = checkpoint['model']
287
+ else:
288
+ state_dict = checkpoint
289
+ else:
290
+ state_dict = checkpoint
291
+
292
+ # Filter and load only backbone weights (for safetensors or manual torch.load)
293
+ backbone_state_dict = {}
294
+ for k, v in state_dict.items():
295
+ # Remove 'backbone.' prefix if present
296
+ new_k = k.replace('backbone.', '')
297
+ # Skip non-backbone layers
298
+ if not any(x in new_k for x in ['depth_decoder', 'class_head', 'shape_attention']):
299
+ backbone_state_dict[new_k] = v
300
+
301
+ missing_keys, unexpected_keys = self.backbone.load_state_dict(backbone_state_dict, strict=False)
302
+ if missing_keys:
303
+ print(f"Warning: Missing keys when loading pretrained: {len(missing_keys)} keys")
304
+ if unexpected_keys:
305
+ print(f"Warning: Unexpected keys when loading pretrained: {len(unexpected_keys)} keys")
306
+ print(f"Successfully loaded pretrained weights from: {pretrained_path}")
307
+ else:
308
+ raise FileNotFoundError(f"Pretrained model file not found: {pretrained_path}")
309
+ else:
310
+ self.backbone = timm.create_model(
311
+ backbone_name,
312
+ pretrained=pretrained,
313
+ features_only=True,
314
+ out_indices=(0, 1, 2, 3)
315
+ )
316
+
317
+ # Get encoder channels
318
+ with torch.no_grad():
319
+ dummy_input = torch.randn(1, 3, 256, 256)
320
+ features = self.backbone(dummy_input)
321
+ encoder_channels = [f.shape[1] for f in features]
322
+
323
+ # Depth decoder
324
+ self.depth_decoder = DepthDecoder(encoder_channels, use_attention_gate=use_attention_gate)
325
+
326
+ # Shape-guided attention (uses intermediate decoder features)
327
+ self.use_shape_attention = use_shape_attention
328
+ if use_shape_attention:
329
+ # Attention for mid-level features (optional, for decoder guidance)
330
+ self.shape_attention_mid = ShapeGuidedAttention(
331
+ rgb_channels=encoder_channels[2], # Use mid-level RGB features
332
+ decoder_channels=self.depth_decoder.decoder_channels[-2] # Second-to-last decoder channel
333
+ )
334
+ # Attention for deepest features (used for classification)
335
+ self.shape_attention_deep = ShapeGuidedAttention(
336
+ rgb_channels=encoder_channels[-1], # Use deepest RGB features
337
+ decoder_channels=self.depth_decoder.decoder_channels[-1] # Last decoder channel
338
+ )
339
+ else:
340
+ self.shape_attention_mid = None
341
+ self.shape_attention_deep = None
342
+
343
+ # Feature fusion: RGB + Depth
344
+ # We'll use the last decoder intermediate feature (before final upsampling)
345
+ # This has decoder_channels[-1] channels
346
+ depth_feature_channels = self.depth_decoder.decoder_channels[-1]
347
+ fused_channels = encoder_channels[-1] + depth_feature_channels
348
+
349
+ # Classification head (now accepts fused features)
350
+ self.class_head = nn.Sequential(
351
+ nn.AdaptiveAvgPool2d(1),
352
+ nn.Flatten(),
353
+ nn.Linear(fused_channels, 256),
354
+ nn.ReLU(inplace=True),
355
+ nn.Dropout(0.5),
356
+ nn.Linear(256, num_classes)
357
+ )
358
+
359
+ def forward(self, x):
360
+ """
361
+ Args:
362
+ x: Input RGB image (B, 3, H, W)
363
+
364
+ Returns:
365
+ cls_logits: Classification logits (B, num_classes)
366
+ depth: Predicted depth map (B, 1, H, W)
367
+ """
368
+ # Extract multi-scale features from backbone
369
+ encoder_features = self.backbone(x)
370
+
371
+ # Decode depth
372
+ depth, decoder_intermediate = self.depth_decoder(encoder_features)
373
+
374
+ # Get RGB features for classification (deepest level)
375
+ rgb_features = encoder_features[-1] # (B, C_rgb, H_rgb, W_rgb)
376
+
377
+ # Get depth features for fusion (last intermediate decoder feature)
378
+ depth_features = decoder_intermediate[-1] # (B, C_depth, H_depth, W_depth)
379
+
380
+ # Adapt depth features to match RGB feature spatial size
381
+ # First, get spatial dimensions of RGB features
382
+ B, C_rgb, H_rgb, W_rgb = rgb_features.shape
383
+ _, C_depth, H_depth, W_depth = depth_features.shape
384
+
385
+ # Resize depth features to match RGB feature spatial size
386
+ if (H_depth, W_depth) != (H_rgb, W_rgb):
387
+ depth_features = F.interpolate(
388
+ depth_features,
389
+ size=(H_rgb, W_rgb),
390
+ mode='bilinear',
391
+ align_corners=False
392
+ )
393
+
394
+ # Apply shape-guided attention to RGB features (if enabled)
395
+ if self.use_shape_attention:
396
+ # Apply attention to mid-level features (helps decoder learn better depth)
397
+ if self.shape_attention_mid is not None:
398
+ enhanced_rgb_mid = self.shape_attention_mid(
399
+ encoder_features[2], # Mid-level RGB features
400
+ decoder_intermediate[-2] # Second-to-last decoder feature
401
+ )
402
+
403
+ # Apply attention to deepest features (used for classification)
404
+ if self.shape_attention_deep is not None:
405
+ rgb_features = self.shape_attention_deep(
406
+ rgb_features, # Deepest RGB features
407
+ depth_features # Last decoder feature (already resized to match)
408
+ )
409
+
410
+ # Feature fusion: concatenate RGB and depth features
411
+ fused_features = torch.cat([rgb_features, depth_features], dim=1) # (B, C_rgb + C_depth, H, W)
412
+
413
+ # Classification using fused features
414
+ cls_logits = self.class_head(fused_features)
415
+
416
+ return cls_logits, depth
417
+
418
+
419
+ class ResNetSGDNet(nn.Module):
420
+ """
421
+ ResNet-based variant for ablation study
422
+ """
423
+
424
+ def __init__(self, backbone_name='resnet50', num_classes=2, pretrained=True, pretrained_path=None, use_shape_attention=True, use_attention_gate=True):
425
+ super(ResNetSGDNet, self).__init__()
426
+
427
+ # Load ResNet backbone
428
+ # If pretrained_path is provided, load from local file
429
+ if pretrained_path is not None:
430
+ self.backbone = timm.create_model(
431
+ backbone_name,
432
+ pretrained=False,
433
+ features_only=True,
434
+ out_indices=(0, 1, 2, 3)
435
+ )
436
+ # Load pretrained weights from local file, or download if not found
437
+ if not os.path.exists(pretrained_path):
438
+ print(f"Pretrained path not found: {pretrained_path}")
439
+ print("Attempting to download pretrained model...")
440
+ # Download and save as .pth format
441
+ try:
442
+ temp_model = timm.create_model(backbone_name, pretrained=True, features_only=True, out_indices=(0, 1, 2, 3))
443
+ # Save state dict as .pth file
444
+ pretrained_path_obj = Path(pretrained_path)
445
+ pretrained_path_obj.parent.mkdir(parents=True, exist_ok=True)
446
+ torch.save(temp_model.state_dict(), pretrained_path)
447
+ print(f"Downloaded and saved pretrained model to: {pretrained_path}")
448
+ del temp_model
449
+ except Exception as e:
450
+ print(f"Failed to download pretrained model: {e}")
451
+ raise FileNotFoundError(f"Pretrained model file not found and download failed: {pretrained_path}")
452
+
453
+ if os.path.exists(pretrained_path):
454
+ # Check file extension
455
+ file_ext = Path(pretrained_path).suffix.lower()
456
+
457
+ if file_ext == '.safetensors':
458
+ # Load safetensors format (HuggingFace)
459
+ try:
460
+ from safetensors import safe_open
461
+ state_dict = {}
462
+ with safe_open(pretrained_path, framework="pt", device="cpu") as f:
463
+ for key in f.keys():
464
+ state_dict[key] = f.get_tensor(key)
465
+ print(f"Loaded safetensors from: {pretrained_path}")
466
+ except ImportError:
467
+ raise ImportError("safetensors library is required to load .safetensors files. Install with: pip install safetensors")
468
+ except Exception as e:
469
+ raise RuntimeError(f"Failed to load safetensors file: {e}")
470
+ else:
471
+ # Load .pth or .pt format
472
+ try:
473
+ from timm.models import load_pretrained
474
+ load_pretrained(self.backbone, pretrained_path, strict=False)
475
+ print(f"Loaded pretrained weights via timm from: {pretrained_path}")
476
+ return # Success, exit early
477
+ except:
478
+ # Fallback: manual loading with torch.load
479
+ checkpoint = torch.load(pretrained_path, map_location='cpu', weights_only=False)
480
+ if isinstance(checkpoint, dict):
481
+ if 'state_dict' in checkpoint:
482
+ state_dict = checkpoint['state_dict']
483
+ elif 'model' in checkpoint:
484
+ state_dict = checkpoint['model']
485
+ else:
486
+ state_dict = checkpoint
487
+ else:
488
+ state_dict = checkpoint
489
+
490
+ # Filter and load only backbone weights
491
+ backbone_state_dict = {}
492
+ for k, v in state_dict.items():
493
+ new_k = k.replace('backbone.', '')
494
+ if not any(x in new_k for x in ['depth_decoder', 'class_head', 'shape_attention']):
495
+ backbone_state_dict[new_k] = v
496
+
497
+ missing_keys, unexpected_keys = self.backbone.load_state_dict(backbone_state_dict, strict=False)
498
+ if missing_keys:
499
+ print(f"Warning: Missing keys when loading pretrained: {len(missing_keys)} keys")
500
+ if unexpected_keys:
501
+ print(f"Warning: Unexpected keys when loading pretrained: {len(unexpected_keys)} keys")
502
+ print(f"Successfully loaded pretrained weights from: {pretrained_path}")
503
+ else:
504
+ raise FileNotFoundError(f"Pretrained model file not found: {pretrained_path}")
505
+ else:
506
+ self.backbone = timm.create_model(
507
+ backbone_name,
508
+ pretrained=pretrained,
509
+ features_only=True,
510
+ out_indices=(0, 1, 2, 3)
511
+ )
512
+
513
+ # Get encoder channels
514
+ with torch.no_grad():
515
+ dummy_input = torch.randn(1, 3, 256, 256)
516
+ features = self.backbone(dummy_input)
517
+ encoder_channels = [f.shape[1] for f in features]
518
+
519
+ # Depth decoder
520
+ self.depth_decoder = DepthDecoder(encoder_channels, use_attention_gate=use_attention_gate)
521
+
522
+ # Shape-guided attention
523
+ self.use_shape_attention = use_shape_attention
524
+ if use_shape_attention:
525
+ # Attention for mid-level features (optional, for decoder guidance)
526
+ self.shape_attention_mid = ShapeGuidedAttention(
527
+ rgb_channels=encoder_channels[2], # Use mid-level RGB features
528
+ decoder_channels=self.depth_decoder.decoder_channels[-2] # Second-to-last decoder channel
529
+ )
530
+ # Attention for deepest features (used for classification)
531
+ self.shape_attention_deep = ShapeGuidedAttention(
532
+ rgb_channels=encoder_channels[-1], # Use deepest RGB features
533
+ decoder_channels=self.depth_decoder.decoder_channels[-1] # Last decoder channel
534
+ )
535
+ else:
536
+ self.shape_attention_mid = None
537
+ self.shape_attention_deep = None
538
+
539
+ # Feature fusion: RGB + Depth
540
+ # We'll use the last decoder intermediate feature (before final upsampling)
541
+ depth_feature_channels = self.depth_decoder.decoder_channels[-1]
542
+ fused_channels = encoder_channels[-1] + depth_feature_channels
543
+
544
+ # Classification head (now accepts fused features)
545
+ self.class_head = nn.Sequential(
546
+ nn.AdaptiveAvgPool2d(1),
547
+ nn.Flatten(),
548
+ nn.Linear(fused_channels, 256),
549
+ nn.ReLU(inplace=True),
550
+ nn.Dropout(0.5),
551
+ nn.Linear(256, num_classes)
552
+ )
553
+
554
+ def forward(self, x):
555
+ encoder_features = self.backbone(x)
556
+ depth, decoder_intermediate = self.depth_decoder(encoder_features)
557
+
558
+ # Get RGB features for classification (deepest level)
559
+ rgb_features = encoder_features[-1] # (B, C_rgb, H_rgb, W_rgb)
560
+
561
+ # Get depth features for fusion (last intermediate decoder feature)
562
+ depth_features = decoder_intermediate[-1] # (B, C_depth, H_depth, W_depth)
563
+
564
+ # Adapt depth features to match RGB feature spatial size
565
+ # First, get spatial dimensions of RGB features
566
+ B, C_rgb, H_rgb, W_rgb = rgb_features.shape
567
+ _, C_depth, H_depth, W_depth = depth_features.shape
568
+
569
+ # Resize depth features to match RGB feature spatial size
570
+ if (H_depth, W_depth) != (H_rgb, W_rgb):
571
+ depth_features = F.interpolate(
572
+ depth_features,
573
+ size=(H_rgb, W_rgb),
574
+ mode='bilinear',
575
+ align_corners=False
576
+ )
577
+
578
+ # Apply shape-guided attention to RGB features (if enabled)
579
+ if self.use_shape_attention:
580
+ # Apply attention to mid-level features (helps decoder learn better depth)
581
+ if self.shape_attention_mid is not None:
582
+ enhanced_rgb_mid = self.shape_attention_mid(
583
+ encoder_features[2], # Mid-level RGB features
584
+ decoder_intermediate[-2] # Second-to-last decoder feature
585
+ )
586
+
587
+ # Apply attention to deepest features (used for classification)
588
+ if self.shape_attention_deep is not None:
589
+ rgb_features = self.shape_attention_deep(
590
+ rgb_features, # Deepest RGB features
591
+ depth_features # Last decoder feature (already resized to match)
592
+ )
593
+
594
+ # Feature fusion: concatenate RGB and depth features
595
+ fused_features = torch.cat([rgb_features, depth_features], dim=1) # (B, C_rgb + C_depth, H, W)
596
+
597
+ # Classification using fused features
598
+ cls_logits = self.class_head(fused_features)
599
+ return cls_logits, depth
600
+
601
+
602
+ def get_timm_cache_dir():
603
+ """
604
+ Get the cache directory where timm stores pretrained models
605
+
606
+ Returns:
607
+ Path to timm cache directory
608
+ """
609
+ # Check TORCH_HOME environment variable first
610
+ torch_home = os.environ.get('TORCH_HOME')
611
+ if torch_home:
612
+ cache_dir = Path(torch_home) / 'hub' / 'checkpoints'
613
+ else:
614
+ # Default location on Windows
615
+ if os.name == 'nt': # Windows
616
+ cache_dir = Path.home() / '.cache' / 'torch' / 'hub' / 'checkpoints'
617
+ else: # Linux/Mac
618
+ cache_dir = Path.home() / '.cache' / 'torch' / 'hub' / 'checkpoints'
619
+
620
+ return cache_dir
621
+
622
+
623
+ def find_pretrained_model(backbone_name: str, cache_dir: Optional[Path] = None) -> Optional[str]:
624
+ """
625
+ Find pretrained model file in cache directory
626
+
627
+ Args:
628
+ backbone_name: Name of the backbone model (e.g., 'convnextv2_tiny', 'resnet50')
629
+ cache_dir: Optional cache directory path. If None, uses default timm cache.
630
+
631
+ Returns:
632
+ Path to pretrained model file if found, None otherwise
633
+ """
634
+ if cache_dir is None:
635
+ cache_dir = get_timm_cache_dir()
636
+
637
+ # Common model file patterns
638
+ patterns = [
639
+ f"{backbone_name}*.pth",
640
+ f"{backbone_name}*.pt",
641
+ f"*{backbone_name}*.pth",
642
+ f"*{backbone_name}*.pt",
643
+ ]
644
+
645
+ if not cache_dir.exists():
646
+ return None
647
+
648
+ # Search for model files
649
+ for pattern in patterns:
650
+ matches = list(cache_dir.glob(pattern))
651
+ if matches:
652
+ # Return the most recent file if multiple matches
653
+ return str(max(matches, key=lambda p: p.stat().st_mtime))
654
+
655
+ return None
656
+
657
+
658
+ def download_and_save_pretrained_model(backbone_name: str, save_dir: Optional[Path] = None) -> str:
659
+ """
660
+ Download pretrained model and save to specified directory
661
+
662
+ Args:
663
+ backbone_name: Name of the backbone model
664
+ save_dir: Directory to save the model. If None, saves to timm cache.
665
+
666
+ Returns:
667
+ Path to saved model file
668
+ """
669
+ if save_dir is None:
670
+ save_dir = get_timm_cache_dir()
671
+ save_dir = Path(save_dir)
672
+ save_dir.mkdir(parents=True, exist_ok=True)
673
+
674
+ # Create model to trigger download
675
+ model = timm.create_model(backbone_name, pretrained=True, features_only=False)
676
+
677
+ # Get the default pretrained config to find the model URL/file
678
+ from timm.models import safe_model_name
679
+ safe_name = safe_model_name(backbone_name)
680
+
681
+ # Try to find the downloaded file in cache
682
+ cache_dir = get_timm_cache_dir()
683
+ model_file = find_pretrained_model(backbone_name, cache_dir)
684
+
685
+ if model_file:
686
+ print(f"Model downloaded to: {model_file}")
687
+ return model_file
688
+ else:
689
+ # If not found, save the state dict manually
690
+ save_path = save_dir / f"{safe_name}_pretrained.pth"
691
+ torch.save(model.state_dict(), save_path)
692
+ print(f"Model saved to: {save_path}")
693
+ return str(save_path)
694
+
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ torch>=1.12.0
2
+ torchvision>=0.13.0
3
+ timm>=0.6.0
4
+ albumentations>=1.1.0
5
+ opencv-python>=4.5.0
6
+ numpy>=1.21.0
7
+ ultralytics>=8.0.0
8
+ gradio>=3.0.0
9
+ Pillow>=9.0.0
10
+ matplotlib>=3.5.0
sgd_net_checkpoint.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2bcba89c16de381ac7e137959436ecbc562057cf506d5419cac458e3b3b05f44
3
+ size 125507694
yolo_model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:33172ace0b55b8a3f34aef9585a4e6167dac4a3f45eeae164b8ea74edf4e814c
3
+ size 5997028