Spaces:
Sleeping
Sleeping
shun-ren commited on
Commit ·
0f049b3
1
Parent(s): 58886b6
added more py files for build import
Browse files- data.py +84 -0
- dataset.py +111 -0
- eval.py +108 -0
- explain.py +110 -0
- images_combined.py +105 -0
- main.py +99 -0
- own_images.py +65 -0
- tempCodeRunnerFile.py +4 -0
- 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
|