shun-ren commited on
Commit
0f049b3
·
1 Parent(s): 58886b6

added more py files for build import

Browse files
Files changed (9) hide show
  1. data.py +84 -0
  2. dataset.py +111 -0
  3. eval.py +108 -0
  4. explain.py +110 -0
  5. images_combined.py +105 -0
  6. main.py +99 -0
  7. own_images.py +65 -0
  8. tempCodeRunnerFile.py +4 -0
  9. train.py +244 -0
data.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -------------------------------------------------------------
2
+ # This script handles ALL dataset-related logic:
3
+ # 1. Builds image transformations for training and testing
4
+ # 2. Loads datasets from "train", "val", "test" folders
5
+ # 3. Creates DataLoaders for batching
6
+ # 4. Exports label names to "models/labels.json" for later use
7
+ # -------------------------------------------------------------
8
+
9
+ from pathlib import Path
10
+ import os, json, torch
11
+ from typing import Dict, Tuple
12
+ from torchvision import datasets, transforms
13
+ from torch.utils.data import DataLoader
14
+
15
+ # ------------------------------
16
+ # BUILD TRANSFORMS (AUGMENTATION)
17
+ # ------------------------------
18
+ def build_transforms(img_size: int = 224) -> Tuple[transforms.Compose, transforms.Compose]:
19
+
20
+ # --- Training transforms ---
21
+ # These help the model generalize better by creating variations of input images
22
+ train_tf = transforms.Compose([
23
+ transforms.Resize((img_size, img_size)),
24
+ transforms.RandomHorizontalFlip(),
25
+ transforms.ColorJitter(0.2, 0.2, 0.2, 0.1),
26
+ transforms.RandomRotation(45, fill=(0,)), # Background filled with black
27
+ transforms.RandomHorizontalFlip(p=0.5),
28
+ transforms.ToTensor(),
29
+ transforms.Normalize([0.485, 0.456, 0.406],
30
+ [0.229, 0.224, 0.225]),
31
+ ])
32
+
33
+ # --- Evaluation transforms ---
34
+ # Only resize + normalize (no randomness to keep results consistent)
35
+ eval_tf = transforms.Compose([
36
+ transforms.Resize((img_size, img_size)),
37
+ transforms.ToTensor(),
38
+ transforms.Normalize([0.485, 0.456, 0.406],
39
+ [0.229, 0.224, 0.225]),
40
+ ])
41
+ return train_tf, eval_tf
42
+
43
+
44
+ # ------------------------------
45
+ # LOAD DATASETS & BUILD LOADERS
46
+ # ------------------------------
47
+ # Load datasets and create data loaders
48
+ def load_data(data_root: str, bs: int, num_workers: int = 2) -> Dict:
49
+
50
+ # Get transformation pipelines
51
+ train_tf, eval_tf = build_transforms(224)
52
+
53
+ # --- Load image folders ---
54
+ # Each folder should look like:
55
+ # data_root/train/class_name/*.jpg
56
+ # data_root/val/class_name/*.jpg
57
+ # data_root/test/class_name/*.jpg
58
+ dsets = {
59
+ "train": datasets.ImageFolder(os.path.join(data_root, "train"), transform=train_tf),
60
+ "val": datasets.ImageFolder(os.path.join(data_root, "val"), transform=eval_tf),
61
+ "test": datasets.ImageFolder(os.path.join(data_root, "test"), transform=eval_tf),
62
+ }
63
+
64
+ # --- Save label mapping to file ---
65
+ # This creates a file: models/labels.json
66
+ # Example content: {"0": "paper", "1": "plastic", "2": "metal"}
67
+ Path("models").mkdir(parents=True, exist_ok=True)
68
+ with open("models/labels.json", "w") as f:
69
+ json.dump({v: k for k, v in dsets["train"].class_to_idx.items()}, f, indent=2)
70
+
71
+ # --- Check if GPU is available ---
72
+ # If yes, use "pinned memory" to load faster
73
+ pin = torch.cuda.is_available()
74
+
75
+ # --- Build DataLoaders ---
76
+ # DataLoader helps in batching, shuffling, and parallel loading
77
+ loaders = {
78
+ "train": DataLoader(dsets["train"], batch_size=bs, shuffle=True, num_workers=num_workers, pin_memory=pin),
79
+ "val": DataLoader(dsets["val"], batch_size=bs, shuffle=False, num_workers=num_workers, pin_memory=pin),
80
+ "test": DataLoader(dsets["test"], batch_size=bs, shuffle=False, num_workers=num_workers, pin_memory=pin),
81
+ }
82
+
83
+ # Return both datasets and loaders for training scripts
84
+ return {"dsets": dsets, "loaders": loaders}
dataset.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -------------------------------------------------------------
2
+ # This script:
3
+ # Downloads the Trash-Type Image Dataset from Kaggle
4
+ # Keeps only the classes: paper, plastic, metal (groups others)
5
+ # Splits data into train / val / test
6
+ # Copies them into the folder structure:
7
+ # data/images/train/paper/
8
+ # data/images/val/plastic/
9
+ # data/images/test/metal/
10
+ # -------------------------------------------------------------
11
+
12
+ import os, shutil, random
13
+ from pathlib import Path
14
+ import kagglehub
15
+
16
+ # ---- CLASS FILTERING ----
17
+ CLASS_KEEP = {"paper", "plastic", "metal"} # Classes we care about
18
+ TO_OTHERS = {"glass", "cardboard", "trash"} # Classes to group as "others"
19
+
20
+ # ---- TARGET DIRECTORY ----
21
+ TARGET = Path("data/images") # Where to save the new dataset
22
+
23
+ # ---- DATA SPLIT RATIOS ----
24
+ SPLITS = {"train":0.7,"val":0.15,"test":0.15}
25
+
26
+ # ---- IMAGE EXTENSIONS TO LOOK FOR ----
27
+ IMG_EXTS = (".jpg",".jpeg",".png",".bmp",".webp")
28
+
29
+ # ---- SET FIXED RANDOM SEED (for reproducibility) ----
30
+ random.seed(42)
31
+
32
+ # -------------------------------------------------------------
33
+ # STEP 1: INFER CLASS FROM FILE PATH
34
+ # -------------------------------------------------------------
35
+ def infer_class(path: Path) -> str:
36
+ parts = [p.lower() for p in path.parts]
37
+ for p in reversed(parts): # Search backwards in path parts
38
+ if p in CLASS_KEEP:
39
+ return p
40
+ if p in TO_OTHERS:
41
+ return "others"
42
+ return "others" # Fallback if no match found
43
+
44
+ # -------------------------------------------------------------
45
+ # STEP 2: FIND ALL IMAGES IN THE DOWNLOADED DATASET
46
+ # -------------------------------------------------------------
47
+ def gather_images(root: Path):
48
+ imgs = []
49
+ for fp in root.rglob("*"): # Recursively walk through subfolders
50
+ if fp.suffix.lower() in IMG_EXTS:
51
+ cls = infer_class(fp.parent)
52
+ imgs.append((fp, cls))
53
+ return imgs
54
+
55
+ # -------------------------------------------------------------
56
+ # STEP 3: SPLIT INTO TRAIN/VAL/TEST AND COPY FILES
57
+ # -------------------------------------------------------------
58
+ def split_and_copy(items):
59
+
60
+ by_cls = {}
61
+
62
+ # Group all images by class
63
+ for fp, cls in items:
64
+ by_cls.setdefault(cls, []).append(fp)
65
+
66
+ TARGET.mkdir(parents=True, exist_ok=True)
67
+
68
+ # For each class, randomly split its images
69
+ for cls, files in by_cls.items():
70
+
71
+ random.shuffle(files)
72
+ n = len(files); n_tr = int(SPLITS["train"]*n); n_va = int(SPLITS["val"]*n)
73
+
74
+ # Split into train, val, test
75
+ splits = {"train":files[:n_tr], "val":files[n_tr:n_tr+n_va], "test":files[n_tr+n_va:]}
76
+
77
+ # Copy images to correct folders
78
+ for split, fps in splits.items():
79
+ outdir = TARGET / split / cls
80
+ outdir.mkdir(parents=True, exist_ok=True)
81
+ for src in fps:
82
+ shutil.copy(src, outdir / src.name)
83
+
84
+ # Print summary
85
+ print(f"{cls}: {n} -> train {len(splits['train'])}, val {len(splits['val'])}, test {len(splits['test'])}")
86
+
87
+ # -------------------------------------------------------------
88
+ # STEP 4: MAIN FUNCTION – DOWNLOAD & PREPARE DATASET
89
+ # -------------------------------------------------------------
90
+ def main():
91
+
92
+ # Download dataset (cached locally after first time)
93
+ root = Path(kagglehub.dataset_download("farzadnekouei/trash-type-image-dataset"))
94
+ print("Dataset at:", root)
95
+
96
+ # Collect all image paths + inferred labels
97
+ items = gather_images(root)
98
+
99
+ if not items:
100
+ raise SystemExit("No images found. Check dataset layout.")
101
+ print("Classes detected:", sorted({c for _, c in items}))
102
+
103
+ # Split and copy into new structure
104
+ split_and_copy(items)
105
+ print("Flattened dataset ready in:", TARGET)
106
+
107
+ # -------------------------------------------------------------
108
+ # ENTRY POINT (only runs when executed directly)
109
+ # -------------------------------------------------------------
110
+ if __name__ == "__main__":
111
+ main()
eval.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -------------------------------------------------------------
2
+ # This script evaluates your trained model:
3
+ # Runs inference on the test dataset
4
+ # Calculates accuracy, precision, recall, F1-score
5
+ # Plots a confusion matrix
6
+ # Saves everything in the /reports folder
7
+ # -------------------------------------------------------------
8
+
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Dict, List
12
+
13
+ import numpy as np
14
+ import torch
15
+ from sklearn.metrics import classification_report, confusion_matrix
16
+ import matplotlib.pyplot as plt
17
+
18
+ # -------------------------------------------------------------
19
+ # HELPER FUNCTION: Plot Confusion Matrix
20
+ # -------------------------------------------------------------
21
+ def _plot_confusion_matrix(cm: np.ndarray, class_names: List[str], title: str, outpath: str = "reports/confusion_matrix.png"):
22
+
23
+ cm = np.array(cm) # Ensure it’s a NumPy array
24
+
25
+ # Create figure and axes for plotting
26
+ fig, ax = plt.subplots(figsize=(7, 6))
27
+ im = ax.imshow(cm) # Display matrix as an image (heatmap)
28
+
29
+ # Label the x and y axes with class names
30
+ ax.set_xticks(range(len(class_names)))
31
+ ax.set_yticks(range(len(class_names)))
32
+ ax.set_xticklabels(class_names, rotation=45, ha="right")
33
+ ax.set_yticklabels(class_names)
34
+ ax.set_xlabel("Predicted label")
35
+ ax.set_ylabel("True label")
36
+ ax.set_title(title)
37
+
38
+ # Add numeric values inside each cell
39
+ for i in range(cm.shape[0]):
40
+ for j in range(cm.shape[1]):
41
+ ax.text(j, i, str(cm[i, j]), ha="center", va="center")
42
+
43
+ # Add colorbar to show intensity scale
44
+ cbar = fig.colorbar(im, ax=ax)
45
+ cbar.ax.set_ylabel("counts") # Label for the colorbar
46
+
47
+ fig.tight_layout()
48
+
49
+ # Save the figure to file
50
+ Path(outpath).parent.mkdir(parents=True, exist_ok=True)
51
+ plt.savefig(outpath, dpi=200)
52
+ plt.close(fig)
53
+
54
+ # -------------------------------------------------------------
55
+ # MAIN FUNCTION: Evaluate Model on Test Data
56
+ # -------------------------------------------------------------
57
+ def eval_on_test(model, loader, class_names: List[str], device) -> Dict:
58
+
59
+ model.eval() # Switch model to evaluation mode (disables dropout/batchnorm)
60
+ y_true, y_pred = [], [] # Store true and predicted labels
61
+
62
+ # Disable gradient computation (faster inference)
63
+ with torch.no_grad():
64
+ for xb, yb in loader: # Loop through test batches
65
+ xb = xb.to(device, non_blocking=True) # Move data to device
66
+ out = model(xb).cpu() # Forward pass and move output to CPU
67
+ y_true += yb.tolist() # Collect true labels
68
+ y_pred += out.argmax(1).tolist() # Collect predicted labels
69
+
70
+ # ---- PRINT TEXT REPORT ----
71
+ print("\n=== Classification Report ===")
72
+ print(classification_report(y_true, y_pred, target_names=class_names, digits=4))
73
+
74
+ # ---- CONFUSION MATRIX ----
75
+ labels = list(range(len(class_names))) # Numeric labels [0, 1, 2, ...]
76
+ cm = confusion_matrix(y_true, y_pred, labels=labels)
77
+ print("Confusion Matrix:\n", cm)
78
+
79
+ # ---- CALCULATE OVERALL ACCURACY ----
80
+ total = cm.sum()
81
+ acc = float(cm.trace()) / float(total) if total > 0 else 0.0 # Correct predictions / Total predictions
82
+
83
+ # ---- SAVE METRICS INTO A DICTIONARY ----
84
+ metrics = {
85
+ "accuracy": acc,
86
+ "labels": class_names,
87
+ "classification_report": classification_report(
88
+ y_true, y_pred, target_names=class_names, digits=4, output_dict=True
89
+ ),
90
+ "confusion_matrix": cm.tolist(),
91
+ }
92
+
93
+ # ---- SAVE METRICS TO FILE ----
94
+ Path("reports").mkdir(parents=True, exist_ok=True)
95
+ with open("reports/metrics.json", "w") as f:
96
+ json.dump(metrics, f, indent=2)
97
+
98
+ # ---- PLOT AND SAVE CONFUSION MATRIX IMAGE ----
99
+ _plot_confusion_matrix(
100
+ cm,
101
+ class_names,
102
+ title="Recycle Classifier Confusion Matrix",
103
+ outpath="reports/confusion_matrix.png",
104
+ )
105
+
106
+ print("Saved: reports/metrics.json and reports/confusion_matrix.png")
107
+ return metrics
108
+
explain.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ import cv2
4
+ from PIL import Image
5
+ from data import build_transforms
6
+ from model import build_model
7
+ import json
8
+ from pathlib import Path
9
+
10
+
11
+ def generate_gradcam(img, model, device, class_names, threshold=0.5):
12
+
13
+ """
14
+ Generate Grad-CAM for a given image and model.
15
+
16
+ Args:
17
+ img (PIL.Image): Input image.
18
+ model (torch.nn.Module): Pretrained model.
19
+ device (torch.device): Torch device (cpu/cuda).
20
+ class_names (list): List of class labels.
21
+ threshold (float): Threshold for masking heatmap.
22
+
23
+ Returns:
24
+ overlay_rgb (PIL.Image): Overlay of heatmap on original image.
25
+ heatmap (np.ndarray): Heatmap array.
26
+ pred_label (str): Predicted class label.
27
+ confidence (float): Probability of predicted class.
28
+ """
29
+
30
+ # Load transforms
31
+ _, eval_transforms = build_transforms(224)
32
+
33
+ # Handle input type
34
+ if isinstance(img, str):
35
+ img = Image.open(img).convert("RGB")
36
+ elif not isinstance(img, Image.Image):
37
+ raise ValueError("Input must be a file path or PIL.Image")
38
+
39
+ img_tensor = eval_transforms(img).unsqueeze(0).to(device)
40
+
41
+ # Hook storage
42
+ feats, grads = [], []
43
+ def fwd_hook(m, i, o): feats.append(o)
44
+ def bwd_hook(m, gi, go): grads.append(go[0])
45
+
46
+ # Register hooks
47
+ layer = model.layer4[1].conv2
48
+ layer.register_forward_hook(fwd_hook)
49
+ layer.register_full_backward_hook(bwd_hook)
50
+
51
+ # Forward pass
52
+ out = model(img_tensor)
53
+ pred_idx = out.argmax(1).item()
54
+ score = out[0, pred_idx]
55
+
56
+ # Confidence (softmax)
57
+ probs = torch.softmax(out, dim=1)
58
+ confidence = probs[0, pred_idx].item()
59
+
60
+ # Backward pass
61
+ model.zero_grad()
62
+ score.backward()
63
+
64
+ # Grad-CAM calculation
65
+ grad = grads[0][0].detach().cpu().numpy()
66
+ feat = feats[0][0].detach().cpu().numpy()
67
+ weights = grad.mean(axis=(1, 2))
68
+ cam = np.maximum(np.sum(weights[:, None, None] * feat, axis=0), 0)
69
+ cam = cv2.resize(cam, img.size)
70
+ cam = cam / cam.max()
71
+
72
+ # Mask
73
+ mask = (cam > threshold).astype(np.uint8) * 255
74
+
75
+ # Heatmap
76
+ heatmap = cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_TURBO)
77
+ heatmap_masked = cv2.bitwise_and(heatmap, heatmap, mask=mask)
78
+ heatmap_rgb = cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB)
79
+
80
+ # Overlay
81
+ orig_bgr = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
82
+ overlay = cv2.addWeighted(orig_bgr, 0.5, heatmap_masked, 0.5, 0)
83
+ overlay_rgb = cv2.cvtColor(overlay, cv2.COLOR_BGR2RGB)
84
+
85
+ return Image.fromarray(overlay_rgb), heatmap_rgb, class_names[pred_idx], confidence
86
+
87
+
88
+ if __name__ == "__main__":
89
+
90
+ # Load labels
91
+ with open("models/labels.json", "r") as f:
92
+ idx2name = {int(k): v for k, v in json.load(f).items()}
93
+ class_names = [idx2name[i] for i in sorted(idx2name.keys())]
94
+
95
+ # Load model
96
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
97
+ model = build_model(num_classes=len(class_names), freeze_backbone=False, device=device)
98
+ model.load_state_dict(torch.load("models/resnet18_best.pt", map_location=device))
99
+ model.eval()
100
+ print("Model loaded.")
101
+
102
+ # Test with one image
103
+ img = Image.open("data/own_images/test/paper/paper_normal_dark_23sec_013.jpg").convert("RGB")
104
+ overlay, heatmap, pred_label, conf = generate_gradcam(img, model, device, class_names)
105
+
106
+ print(f"Final Prediction: {pred_label} ({conf:.2%})")
107
+ overlay.show() # Display overlay
108
+ heatmap_img = Image.fromarray(heatmap)
109
+ heatmap_img.show() # Display heatmap
110
+
images_combined.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -------------------------------------------------------------
2
+ # This script merges the two datasets:
3
+ # 1️⃣ Kaggle dataset (data/images)
4
+ # 2️⃣ Your own collected images (data/own_images)
5
+ #
6
+ # It combines both into one new dataset folder:
7
+ # data/combined/
8
+ #
9
+ # The structure after merging looks like:
10
+ # data/combined/
11
+ # ├── train/
12
+ # │ ├── paper/
13
+ # │ ├── plastic/
14
+ # │ └── metal/
15
+ # ├── val/
16
+ # └── test/
17
+ #
18
+ # So the model can train on all available images together.
19
+ # -------------------------------------------------------------
20
+
21
+ from pathlib import Path
22
+ import shutil
23
+
24
+ # ---- Define source and destination paths ----
25
+ SRC_K = Path("data/images") # Kaggle-prepared dataset
26
+ SRC_O = Path("data/own_images") # Your own collected images
27
+ DST = Path("data/combined") # Combined output dataset
28
+
29
+ # -------------------------------------------------------------
30
+ # FUNCTION: copy_all
31
+ # -------------------------------------------------------------
32
+ def copy_all(src_dir: Path, dst_dir: Path):
33
+
34
+ if not src_dir.exists():
35
+ return 0 # Skip if source folder doesn’t exist
36
+
37
+ dst_dir.mkdir(parents=True, exist_ok=True) # Make sure target exists
38
+ n = 0
39
+
40
+ # Copy all files from source to destination
41
+ for p in src_dir.glob("*"):
42
+ if p.is_file(): # Ignore subfolders
43
+ shutil.copy(p, dst_dir / p.name)
44
+ n += 1
45
+ return n # Return count of copied files
46
+
47
+ # -------------------------------------------------------------
48
+ # FUNCTION: classes_in
49
+ # -------------------------------------------------------------
50
+ def classes_in(root: Path):
51
+ out = set()
52
+ for split in ["train", "val", "test"]:
53
+ d = root / split
54
+ if d.exists():
55
+ for c in d.iterdir():
56
+ if c.is_dir():
57
+ out.add(c.name)
58
+ return out
59
+
60
+ # -------------------------------------------------------------
61
+ # STEP 1: Identify all classes across Kaggle + own datasets
62
+ # -------------------------------------------------------------
63
+ classes = sorted(classes_in(SRC_K) | classes_in(SRC_O)) # Combine both sets
64
+
65
+ if not classes:
66
+ raise SystemExit("No classes found in data/images or data/own_images. Build them first.")
67
+
68
+ print("Classes to combine:", classes)
69
+
70
+ # -------------------------------------------------------------
71
+ # STEP 2: Create folder structure for combined dataset
72
+ # -------------------------------------------------------------
73
+ for split in ["train", "val", "test"]:
74
+ for cls in classes:
75
+ (DST / split / cls).mkdir(parents=True, exist_ok=True)
76
+
77
+ # -------------------------------------------------------------
78
+ # STEP 3: Combine TRAIN images (both Kaggle + Own)
79
+ # -------------------------------------------------------------
80
+ for cls in classes:
81
+ n1 = copy_all(SRC_K / "train" / cls, DST / "train" / cls) # Copy from Kaggle
82
+ n2 = copy_all(SRC_O / "train" / cls, DST / "train" / cls) # Copy from own
83
+ print(f"train/{cls}: +{n1} (kaggle) +{n2} (own)")
84
+
85
+ # -------------------------------------------------------------
86
+ # STEP 4: Combine VALIDATION and TEST images
87
+ # -------------------------------------------------------------
88
+ for split in ["val", "test"]:
89
+
90
+ for cls in classes:
91
+
92
+ n = copy_all(SRC_K / split / cls, DST / split / cls) # Copy from Kaggle first
93
+
94
+ # If none, fill from own images
95
+ if n == 0:
96
+ n = copy_all(SRC_O / split / cls, DST / split / cls)
97
+ if n > 0:
98
+ print(f"{split}/{cls}: filled from own ({n} files)")
99
+ else:
100
+ print(f"{split}/{cls}: +{n} (kaggle)")
101
+
102
+ # -------------------------------------------------------------
103
+ # STEP 5: Done — print where the merged dataset is saved
104
+ # -------------------------------------------------------------
105
+ print("Combined dataset at:", DST)
main.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -------------------------------------------------------------
2
+ # HOW TO RUN THIS SCRIPT:
3
+ # -------------------------------------------------------------
4
+ # 1️⃣ Create a virtual environment:
5
+ # python -m venv .venv && .\.venv\Scripts\Activate
6
+ #
7
+ # 2️⃣ Install dependencies:
8
+ # pip install -r requirements.txt
9
+ #
10
+ # 3️⃣ Train and evaluate model:
11
+ # python src/main.py --data data/combined --epochs 15 --lr 3e-4 --bs 32
12
+ # -------------------------------------------------------------
13
+
14
+ import argparse, random
15
+ from pathlib import Path
16
+ from typing import Dict
17
+ import numpy as np
18
+ import torch
19
+
20
+ from data import load_data
21
+ from model import build_model
22
+ from train import train_model
23
+ from eval import eval_on_test
24
+
25
+ # -------------------------------------------------------------
26
+ # FUNCTION: set_seed
27
+ # -------------------------------------------------------------
28
+ def set_seed(seed: int = 42):
29
+ random.seed(seed); np.random.seed(seed)
30
+ torch.manual_seed(seed); torch.cuda.manual_seed_all(seed)
31
+ torch.backends.cudnn.deterministic = True
32
+ torch.backends.cudnn.benchmark = False
33
+
34
+ # -------------------------------------------------------------
35
+ # FUNCTION: make_dirs
36
+ # -------------------------------------------------------------
37
+ def make_dirs():
38
+ Path("models").mkdir(parents=True, exist_ok=True)
39
+ Path("reports").mkdir(parents=True, exist_ok=True)
40
+
41
+ # -------------------------------------------------------------
42
+ # FUNCTION: get_device
43
+ # -------------------------------------------------------------
44
+ def get_device():
45
+ return torch.device("cuda" if torch.cuda.is_available() else "cpu")
46
+
47
+ # -------------------------------------------------------------
48
+ # FUNCTION: parse_args
49
+ # -------------------------------------------------------------
50
+ def parse_args():
51
+ p = argparse.ArgumentParser(description="Recycle Material Classifier (ResNet-18)")
52
+ p.add_argument("--data", type=str, default="data/images", help="root with train/val/test")
53
+ p.add_argument("--epochs", type=int, default=10)
54
+ p.add_argument("--bs", type=int, default=32)
55
+ p.add_argument("--lr", type=float, default=1e-3)
56
+ p.add_argument("--freeze", action="store_true", help="freeze ResNet backbone")
57
+ p.add_argument("--seed", type=int, default=42)
58
+ return p.parse_args()
59
+
60
+ # -------------------------------------------------------------
61
+ # FUNCTION: main
62
+ # -------------------------------------------------------------
63
+ def main():
64
+
65
+ # 1️⃣ Read input arguments
66
+ args = parse_args()
67
+
68
+ # 2️⃣ Ensure results are reproducible
69
+ set_seed(args.seed)
70
+
71
+ # 3️⃣ Create folders for saving models/reports
72
+ make_dirs()
73
+
74
+ # 4️⃣ Detect whether to use GPU or CPU
75
+ device = get_device()
76
+ print("Device:", device)
77
+
78
+ # 5️⃣ Load dataset and dataloaders
79
+ out = load_data(args.data, args.bs)
80
+ dsets, loaders = out["dsets"], out["loaders"]
81
+ # Extract class names in correct order
82
+ class_names = [k for k, _ in sorted(dsets["train"].class_to_idx.items(), key=lambda x: x[1])]
83
+ print("Classes:", class_names)
84
+
85
+ # 6️⃣ Build model (ResNet18 with custom classification head)
86
+ model = build_model(num_classes=len(class_names), freeze_backbone=args.freeze, device=device)
87
+
88
+ # 7️⃣ Train model using training and validation sets
89
+ model, best_val = train_model(model, loaders, epochs=args.epochs, lr=args.lr, device=device)
90
+ print(f"Best val acc: {best_val:.4f}")
91
+
92
+ # 8️⃣ Evaluate final model on the test set and save reports
93
+ eval_on_test(model, loaders["test"], class_names, device)
94
+
95
+ # -------------------------------------------------------------
96
+ # ENTRY POINT (runs when executed directly)
97
+ # -------------------------------------------------------------
98
+ if __name__ == "__main__":
99
+ main()
own_images.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -------------------------------------------------------------
2
+ # This script prepares your OWN collected images
3
+ # (separate from the Kaggle dataset) by splitting them into:
4
+ # train / val / test folders
5
+ #
6
+ # Example output structure after running this:
7
+ # data/own_images/
8
+ # ├── train/
9
+ # │ ├── paper/
10
+ # │ ├── plastic/
11
+ # │ └── metal/
12
+ # ├── val/
13
+ # └── test/
14
+ # -------------------------------------------------------------
15
+
16
+ from pathlib import Path
17
+ import shutil, random
18
+
19
+ # ---- SOURCE & DESTINATION FOLDERS ----
20
+ SRC = Path("data/own_images") # Folder where your raw images are currently stored
21
+ DST = Path("data/own_images") # Output folder (same location, will create train/val/test inside)
22
+
23
+ # ---- SPLIT RATIOS ----
24
+ SPLIT = {"train":0.70, "val":0.15, "test":0.15}
25
+
26
+ # ---- SUPPORTED IMAGE EXTENSIONS ----
27
+ EXTS = {".jpg",".jpeg",".png",".bmp",".webp"}
28
+
29
+ # ---- FIX RANDOMNESS (for reproducible splits) ----
30
+ random.seed(42)
31
+
32
+ # -------------------------------------------------------------
33
+ # FUNCTION: gather()
34
+ # -------------------------------------------------------------
35
+ def gather(p): return [f for f in p.rglob("*") if f.suffix.lower() in EXTS]
36
+
37
+ # -------------------------------------------------------------
38
+ # FUNCTION: split_copy()
39
+ # -------------------------------------------------------------
40
+ def split_copy(cls):
41
+
42
+ # 1️⃣ Gather all image paths for this class
43
+ files = gather(SRC/cls); random.shuffle(files) # Shuffle before splitting for randomness
44
+
45
+ # 2️⃣ Compute how many go into each split
46
+ n=len(files); n_tr=int(n*SPLIT["train"]); n_va=int(n*SPLIT["val"])
47
+
48
+ # Slice list into 3 parts
49
+ parts = {"train":files[:n_tr], "val":files[n_tr:n_tr+n_va], "test":files[n_tr+n_va:]}
50
+
51
+ # 3️⃣ Copy files into their corresponding split folders
52
+ for split, arr in parts.items():
53
+ out = DST/split/cls; out.mkdir(parents=True, exist_ok=True) # Create directory if missing
54
+ for src in arr: shutil.copy(src, out/src.name) # Copy image file
55
+
56
+ # 4️⃣ Print summary for this class
57
+ print(f"{cls}: {n} -> {len(parts['train'])}/{len(parts['val'])}/{len(parts['test'])}")
58
+
59
+ # -------------------------------------------------------------
60
+ # MAIN EXECUTION: process all 3 recyclable material classes
61
+ # -------------------------------------------------------------
62
+ for cls in ["paper","plastic","metal"]:
63
+ split_copy(cls)
64
+
65
+ print("Done", DST)
tempCodeRunnerFile.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ webcam_input = gr.Camera(label="Take a picture", type="pil")
2
+ json_output2 = gr.JSON(label="Prediction (top class + confidence %)")
3
+ label_output2 = gr.Label(num_top_classes=3, label="Top-3 probabilities")
4
+ webcam_input.change(predict, inputs=webcam_input, outputs=[json_output2, label_output2])
train.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -------------------------------------------------------------
2
+ # This script handles the full training loop:
3
+ # 1️⃣ Train and validate a model for multiple epochs
4
+ # 2️⃣ Track accuracy, loss, learning rate, etc.
5
+ # 3️⃣ Save the best model (highest val accuracy)
6
+ # 4️⃣ Automatically stop early if val accuracy stops improving
7
+ # 5️⃣ Save training history and graphs
8
+ # -------------------------------------------------------------
9
+
10
+ import json, time
11
+ from pathlib import Path
12
+ import matplotlib.pyplot as plt
13
+
14
+ import torch
15
+ import torch.nn as nn
16
+ from typing import Tuple, Dict, List
17
+ from torch.utils.tensorboard import SummaryWriter
18
+ from tqdm.auto import tqdm
19
+
20
+ # -------------------------------------------------------------
21
+ # HELPER: Compute batch accuracy
22
+ # -------------------------------------------------------------
23
+ def accuracy_from_logits(logits: torch.Tensor, y: torch.Tensor) -> float:
24
+ preds = torch.argmax(logits, dim=1)
25
+ return (preds == y).float().mean().item()
26
+
27
+ # -------------------------------------------------------------
28
+ # HELPER: Format seconds to mm:ss (for clean epoch timing)
29
+ # -------------------------------------------------------------
30
+ def _fmt_time(s: float) -> str:
31
+ m, s = divmod(int(s), 60)
32
+ return f"{m:02d}:{s:02d}"
33
+
34
+ # -------------------------------------------------------------
35
+ # HELPER: Get all target labels from a dataset
36
+ # -------------------------------------------------------------
37
+ def _get_targets(ds) -> torch.Tensor:
38
+ if hasattr(ds, "targets"):
39
+ return torch.as_tensor(ds.targets)
40
+ return torch.as_tensor([t for _, t in ds.samples])
41
+
42
+ # -------------------------------------------------------------
43
+ # HELPER: Count how many samples per class
44
+ # -------------------------------------------------------------
45
+ def _class_counts(ds) -> List[str]:
46
+ if not hasattr(ds, "classes"): return []
47
+ y = _get_targets(ds)
48
+ counts = torch.bincount(y, minlength=len(ds.classes)).tolist()
49
+ return [f"{cls}({cnt})" for cls, cnt in zip(ds.classes, counts)]
50
+
51
+ # -------------------------------------------------------------
52
+ # HELPER: Count total and trainable parameters
53
+ # -------------------------------------------------------------
54
+ def _num_params(model) -> Dict[str, int]:
55
+ total = sum(p.numel() for p in model.parameters())
56
+ trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
57
+ return {"total": total, "trainable": trainable}
58
+
59
+ # -------------------------------------------------------------
60
+ # HELPER: Show current learning rate(s)
61
+ # -------------------------------------------------------------
62
+ def _current_lrs(optimizer) -> str:
63
+ lrs = sorted({pg["lr"] for pg in optimizer.param_groups})
64
+ if len(lrs) == 1: return f"{lrs[0]:.2e}"
65
+ return ", ".join(f"{lr:.2e}" for lr in lrs)
66
+
67
+ # -------------------------------------------------------------
68
+ # FUNCTION: Run one full epoch (Train OR Validation)
69
+ # -------------------------------------------------------------
70
+ def run_epoch(model,loader,criterion,optimizer,device,train: bool,epoch: int,epochs: int,) -> Tuple[float, float]:
71
+
72
+ model.train() if train else model.eval()
73
+ epoch_loss = 0.0
74
+ epoch_acc = 0.0
75
+ total = 0
76
+
77
+ # tqdm creates a nice progress bar in terminal
78
+ bar = tqdm(
79
+ loader,
80
+ desc=f"[{epoch:02d}/{epochs}] {'train' if train else 'val '}",
81
+ leave=False,
82
+ dynamic_ncols=True,
83
+ )
84
+
85
+ for xb, yb in bar:
86
+ xb = xb.to(device, non_blocking=True)
87
+ yb = yb.to(device, non_blocking=True)
88
+
89
+ if train:
90
+ optimizer.zero_grad()
91
+
92
+ with torch.set_grad_enabled(train):
93
+
94
+ out = model(xb) # Forward pass
95
+ loss = criterion(out, yb) # Compute loss
96
+
97
+ if train:
98
+ loss.backward() # Backpropagation
99
+ optimizer.step() # Update weights
100
+
101
+ # Update running totals
102
+ bsz = xb.size(0)
103
+ epoch_loss += loss.item() * bsz
104
+ epoch_acc += accuracy_from_logits(out, yb) * bsz
105
+ total += bsz
106
+
107
+ # Update progress bar text
108
+ bar.set_postfix(loss=f"{epoch_loss/max(total,1):.4f}",
109
+ acc=f"{epoch_acc/max(total,1):.4f}")
110
+
111
+ # Return average loss and accuracy
112
+ return epoch_loss / total, epoch_acc / total
113
+
114
+
115
+ # -------------------------------------------------------------
116
+ # FUNCTION: Save training curves (loss + accuracy)
117
+ # -------------------------------------------------------------
118
+ def _save_final_figure(history, best_val_acc, outpath="reports/curves_final.png", title="ResNet (Fine-tune)"):
119
+
120
+ Path(outpath).parent.mkdir(parents=True, exist_ok=True)
121
+ xs = range(1, len(history["train_loss"]) + 1)
122
+
123
+ fig, axs = plt.subplots(1, 2, figsize=(11, 4.5))
124
+
125
+ # ---- Loss subplot ----
126
+ axs[0].plot(xs, history["train_loss"], label="train")
127
+ axs[0].plot(xs, history["val_loss"], label="val")
128
+ axs[0].set_title("Loss"); axs[0].set_xlabel("Epoch"); axs[0].set_ylabel("Loss"); axs[0].legend()
129
+
130
+ # ---- Accuracy subplot ----
131
+ axs[1].plot(xs, history["train_acc"], label="train")
132
+ axs[1].plot(xs, history["val_acc"], label="val")
133
+ axs[1].set_title("Accuracy"); axs[1].set_xlabel("Epoch"); axs[1].set_ylabel("Acc"); axs[1].legend()
134
+
135
+ fig.suptitle(f"{title} | best val acc = {best_val_acc:.4f}")
136
+ fig.tight_layout(rect=[0, 0, 1, 0.95])
137
+ plt.savefig(outpath, dpi=200)
138
+ plt.close(fig)
139
+
140
+ # Save training history as JSON
141
+ with open("reports/history.json", "w") as f:
142
+ json.dump(history, f, indent=2)
143
+
144
+
145
+ # -------------------------------------------------------------
146
+ # MAIN FUNCTION: train_model
147
+ # -------------------------------------------------------------
148
+ def train_model(model, loaders, epochs, lr, device, patience=3):
149
+
150
+ # 1️⃣ Define loss, optimizer, and learning rate scheduler
151
+ criterion = nn.CrossEntropyLoss()
152
+ optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)
153
+ scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
154
+ optimizer, mode="max", factor=0.5, patience=1
155
+ )
156
+
157
+ writer = SummaryWriter("runs/recycle") # For TensorBoard visualization
158
+
159
+ # Print training setup summary
160
+ pstats = _num_params(model)
161
+ classes_line = ", ".join(_class_counts(loaders["train"].dataset)) or "(classes unavailable)"
162
+
163
+ print("\n" + "=" * 68)
164
+ print(" TRAINING START ")
165
+ print("-" * 68)
166
+ print(f" epochs: {epochs} | lr: {lr:.2e} | patience: {patience} | bs: {loaders['train'].batch_size}")
167
+ print(f" train batches: {len(loaders['train'])} | val batches: {len(loaders['val'])}")
168
+ print(f" params: {pstats['trainable']:,} trainable / {pstats['total']:,} total")
169
+ print(f" classes: {classes_line}")
170
+ print("=" * 68 + "\n")
171
+
172
+ # 2️⃣ Initialize trackers
173
+ best_val_acc, best_state, bad = 0.0, None, 0
174
+ history = {"train_loss": [], "val_loss": [], "train_acc": [], "val_acc": []}
175
+ saved_final = False
176
+
177
+ # 3️⃣ Training loop over all epochs
178
+ for ep in range(1, epochs + 1):
179
+ ep_t0 = time.time()
180
+
181
+ # Run training + validation
182
+ tr_loss, tr_acc = run_epoch(model, loaders["train"], criterion, optimizer, device, True, ep, epochs)
183
+ va_loss, va_acc = run_epoch(model, loaders["val"], criterion, optimizer, device, False, ep, epochs)
184
+
185
+ # Update scheduler based on validation accuracy
186
+ scheduler.step(va_acc)
187
+
188
+ # Store metrics in history
189
+ history["train_loss"].append(tr_loss); history["val_loss"].append(va_loss)
190
+ history["train_acc"].append(tr_acc); history["val_acc"].append(va_acc)
191
+
192
+ # Log to TensorBoard
193
+ writer.add_scalar("loss/train", tr_loss, ep)
194
+ writer.add_scalar("loss/val", va_loss, ep)
195
+ writer.add_scalar("acc/train", tr_acc, ep)
196
+ writer.add_scalar("acc/val", va_acc, ep)
197
+ writer.flush()
198
+
199
+
200
+ # Check for improvement
201
+ improved = va_acc > best_val_acc
202
+ if improved:
203
+ best_val_acc, best_state, bad = va_acc, model.state_dict(), 0
204
+ Path("models").mkdir(parents=True, exist_ok=True)
205
+ torch.save(best_state, "models/resnet18_best.pt")
206
+ else:
207
+ bad += 1 # No improvement counter
208
+
209
+ # Print summary for this epoch
210
+ print("\n" + "-" * 68)
211
+ print(f" EPOCH {ep:02d}/{epochs:02d} | time { _fmt_time(time.time()-ep_t0) } | lr { _current_lrs(optimizer) }")
212
+ print("-" * 68)
213
+ print(f" Train • loss {tr_loss:.4f} | acc {tr_acc:.4f}")
214
+ print(f" Val • loss {va_loss:.4f} | acc {va_acc:.4f} | " f"{'NEW BEST ✓' if improved else f'best {best_val_acc:.4f}'}")
215
+
216
+ if improved:
217
+ print(" Saved • models/resnet18_best.pt")
218
+
219
+ # Early stopping
220
+ if bad >= patience:
221
+ print("-" * 68)
222
+ print(f" EARLY STOP • no val acc improvement for {patience} epoch(s)")
223
+ print("-" * 68)
224
+ _save_final_figure(history, best_val_acc, outpath="reports/curves_final.png", title="ResNet (Fine-tune)")
225
+ saved_final = True
226
+ break
227
+
228
+ # 4️⃣ Load the best model weights
229
+ if best_state is not None:
230
+ model.load_state_dict(best_state)
231
+
232
+ # 5️⃣ Save final results if not done during early stop
233
+ if not saved_final:
234
+ _save_final_figure(history, best_val_acc, outpath="reports/curves_final.png", title="ResNet (Fine-tune)")
235
+
236
+ writer.close()
237
+
238
+ # 6️⃣ Summary printout
239
+ print("\n" + "=" * 68)
240
+ print(f" TRAINING DONE • best val acc = {best_val_acc:.4f}")
241
+ print(" Saved final curves: reports/curves_final.png")
242
+ print("=" * 68 + "\n")
243
+
244
+ return model, best_val_acc