Spaces:
Sleeping
section-4: preprocessing + augmentation — verified end-to-end
Browse filessrc/data/preprocessor.py:
- validate_yolo_annotations(): 1550 train + 375 val files, 13758 annotations all valid
Fixed: assert <= 1.0 not < 1.0 (bbox_to_yolo clamps to min(1.0,...) so boundary values valid)
- create_yolo_data_yaml(): absolute paths, works in terminal + Colab
- verify_dataset_structure(): counts per split, detects image/label mismatches
src/data/augmentor.py:
- get_train_transforms(): flip, rotate±15, colorjitter, zoomout, resize 224, ToImage, Normalize(ImageNet)
- get_eval_transforms(): resize + normalize only
- denormalize(): for visualization
- torch wrapped in try/except (local fbgemm.dll issue)
notebooks/03_preprocessing.py:
- Dataset verified: 2500 classification, 1925 detection, labels match images
- YOLO annotations validated: 13758 annotations, all in valid range
- data.yaml regenerated with current absolute paths
- Augmented sample images saved to docs/figures/
- Tensor shapes confirmed: (3, 224, 224) float32, range ~[-2.1, 2.6] after ImageNet norm
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dataset_stats": {
|
| 3 |
+
"classification": {
|
| 4 |
+
"train": {
|
| 5 |
+
"total": 1725,
|
| 6 |
+
"per_class": {
|
| 7 |
+
"person": 69,
|
| 8 |
+
"bicycle": 69,
|
| 9 |
+
"car": 69,
|
| 10 |
+
"motorcycle": 69,
|
| 11 |
+
"airplane": 69,
|
| 12 |
+
"bus": 69,
|
| 13 |
+
"truck": 69,
|
| 14 |
+
"traffic light": 69,
|
| 15 |
+
"stop sign": 69,
|
| 16 |
+
"bench": 69,
|
| 17 |
+
"bird": 69,
|
| 18 |
+
"cat": 69,
|
| 19 |
+
"dog": 69,
|
| 20 |
+
"horse": 69,
|
| 21 |
+
"cow": 69,
|
| 22 |
+
"elephant": 69,
|
| 23 |
+
"bottle": 69,
|
| 24 |
+
"cup": 69,
|
| 25 |
+
"bowl": 69,
|
| 26 |
+
"pizza": 69,
|
| 27 |
+
"cake": 69,
|
| 28 |
+
"chair": 69,
|
| 29 |
+
"couch": 69,
|
| 30 |
+
"potted plant": 69,
|
| 31 |
+
"bed": 69
|
| 32 |
+
}
|
| 33 |
+
},
|
| 34 |
+
"val": {
|
| 35 |
+
"total": 375,
|
| 36 |
+
"per_class": {
|
| 37 |
+
"person": 15,
|
| 38 |
+
"bicycle": 15,
|
| 39 |
+
"car": 15,
|
| 40 |
+
"motorcycle": 15,
|
| 41 |
+
"airplane": 15,
|
| 42 |
+
"bus": 15,
|
| 43 |
+
"truck": 15,
|
| 44 |
+
"traffic light": 15,
|
| 45 |
+
"stop sign": 15,
|
| 46 |
+
"bench": 15,
|
| 47 |
+
"bird": 15,
|
| 48 |
+
"cat": 15,
|
| 49 |
+
"dog": 15,
|
| 50 |
+
"horse": 15,
|
| 51 |
+
"cow": 15,
|
| 52 |
+
"elephant": 15,
|
| 53 |
+
"bottle": 15,
|
| 54 |
+
"cup": 15,
|
| 55 |
+
"bowl": 15,
|
| 56 |
+
"pizza": 15,
|
| 57 |
+
"cake": 15,
|
| 58 |
+
"chair": 15,
|
| 59 |
+
"couch": 15,
|
| 60 |
+
"potted plant": 15,
|
| 61 |
+
"bed": 15
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
"test": {
|
| 65 |
+
"total": 400,
|
| 66 |
+
"per_class": {
|
| 67 |
+
"person": 16,
|
| 68 |
+
"bicycle": 16,
|
| 69 |
+
"car": 16,
|
| 70 |
+
"motorcycle": 16,
|
| 71 |
+
"airplane": 16,
|
| 72 |
+
"bus": 16,
|
| 73 |
+
"truck": 16,
|
| 74 |
+
"traffic light": 16,
|
| 75 |
+
"stop sign": 16,
|
| 76 |
+
"bench": 16,
|
| 77 |
+
"bird": 16,
|
| 78 |
+
"cat": 16,
|
| 79 |
+
"dog": 16,
|
| 80 |
+
"horse": 16,
|
| 81 |
+
"cow": 16,
|
| 82 |
+
"elephant": 16,
|
| 83 |
+
"bottle": 16,
|
| 84 |
+
"cup": 16,
|
| 85 |
+
"bowl": 16,
|
| 86 |
+
"pizza": 16,
|
| 87 |
+
"cake": 16,
|
| 88 |
+
"chair": 16,
|
| 89 |
+
"couch": 16,
|
| 90 |
+
"potted plant": 16,
|
| 91 |
+
"bed": 16
|
| 92 |
+
}
|
| 93 |
+
},
|
| 94 |
+
"total": 2500
|
| 95 |
+
},
|
| 96 |
+
"detection": {
|
| 97 |
+
"train": {
|
| 98 |
+
"images": 1550,
|
| 99 |
+
"labels": 1550
|
| 100 |
+
},
|
| 101 |
+
"val": {
|
| 102 |
+
"images": 375,
|
| 103 |
+
"labels": 375
|
| 104 |
+
},
|
| 105 |
+
"total_images": 1925
|
| 106 |
+
},
|
| 107 |
+
"issues": []
|
| 108 |
+
},
|
| 109 |
+
"image_size": 224,
|
| 110 |
+
"imagenet_mean": [
|
| 111 |
+
0.485,
|
| 112 |
+
0.456,
|
| 113 |
+
0.406
|
| 114 |
+
],
|
| 115 |
+
"imagenet_std": [
|
| 116 |
+
0.229,
|
| 117 |
+
0.224,
|
| 118 |
+
0.225
|
| 119 |
+
],
|
| 120 |
+
"augmentations_train": [
|
| 121 |
+
"RandomHorizontalFlip(p=0.5)",
|
| 122 |
+
"RandomRotation(degrees=15)",
|
| 123 |
+
"ColorJitter(brightness=0.2, contrast=0.2, saturation=0.1, hue=0.05)",
|
| 124 |
+
"RandomZoomOut(side_range=(1.0,1.3), p=0.3)",
|
| 125 |
+
"Resize(224,224)",
|
| 126 |
+
"Normalize(ImageNet)"
|
| 127 |
+
],
|
| 128 |
+
"augmentations_eval": [
|
| 129 |
+
"Resize(224,224)",
|
| 130 |
+
"Normalize(ImageNet)"
|
| 131 |
+
],
|
| 132 |
+
"yolo_validation": "passed",
|
| 133 |
+
"data_yaml": "C:\\Users\\Suba\\Documents\\Data science\\Guvi\\Proj 5 SmartVision AI - Intelligent Multi-Class Object Recognition System\\data\\processed\\smartvision_dataset\\detection\\data.yaml"
|
| 134 |
+
}
|
|
Git LFS Details
|
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# %% [markdown]
|
| 2 |
+
# # SmartVision AI — Section 4: Preprocessing + Augmentation
|
| 3 |
+
# Run locally: `python notebooks/03_preprocessing.py`
|
| 4 |
+
# No GPU needed.
|
| 5 |
+
#
|
| 6 |
+
# Covers:
|
| 7 |
+
# 1. Verify full dataset structure (2500 classification + 1925 detection)
|
| 8 |
+
# 2. Validate all YOLO annotations
|
| 9 |
+
# 3. Regenerate data.yaml with current absolute paths
|
| 10 |
+
# 4. Show augmentation pipeline + save sample augmented images
|
| 11 |
+
# 5. Confirm train/eval transforms produce correct tensor shapes
|
| 12 |
+
|
| 13 |
+
# %%
|
| 14 |
+
import sys
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 18 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 19 |
+
|
| 20 |
+
try:
|
| 21 |
+
from google.colab import drive
|
| 22 |
+
drive.mount('/content/drive')
|
| 23 |
+
COLAB_ROOT = '/content/drive/MyDrive/SmartVisionAI'
|
| 24 |
+
sys.path.insert(0, COLAB_ROOT)
|
| 25 |
+
print("Running in Colab")
|
| 26 |
+
except ImportError:
|
| 27 |
+
print("Running locally")
|
| 28 |
+
|
| 29 |
+
# %% [markdown]
|
| 30 |
+
# ## Step 1: Imports
|
| 31 |
+
|
| 32 |
+
# %%
|
| 33 |
+
import json
|
| 34 |
+
import warnings
|
| 35 |
+
warnings.filterwarnings("ignore")
|
| 36 |
+
|
| 37 |
+
import matplotlib.pyplot as plt
|
| 38 |
+
import matplotlib.gridspec as gridspec
|
| 39 |
+
import numpy as np
|
| 40 |
+
from PIL import Image
|
| 41 |
+
|
| 42 |
+
from config import (
|
| 43 |
+
CLASSES, NUM_CLASSES, DATA_PROCESSED_DIR, DOCS_FIGURES_DIR,
|
| 44 |
+
IMAGE_SIZE, IMAGES_PER_CLASS,
|
| 45 |
+
)
|
| 46 |
+
from src.data.preprocessor import (
|
| 47 |
+
validate_yolo_annotations,
|
| 48 |
+
create_yolo_data_yaml,
|
| 49 |
+
verify_dataset_structure,
|
| 50 |
+
)
|
| 51 |
+
from src.data.augmentor import (
|
| 52 |
+
TORCH_AVAILABLE, IMAGENET_MEAN, IMAGENET_STD,
|
| 53 |
+
get_train_transforms, get_eval_transforms, denormalize,
|
| 54 |
+
)
|
| 55 |
+
from src.utils.helpers import save_json
|
| 56 |
+
|
| 57 |
+
FIGURES_DIR = DOCS_FIGURES_DIR
|
| 58 |
+
FIGURES_DIR.mkdir(parents=True, exist_ok=True)
|
| 59 |
+
|
| 60 |
+
CLASSIFICATION_DIR = DATA_PROCESSED_DIR / "classification"
|
| 61 |
+
DETECTION_DIR = DATA_PROCESSED_DIR / "detection"
|
| 62 |
+
|
| 63 |
+
print(f"Torch available locally: {TORCH_AVAILABLE}")
|
| 64 |
+
print(f"Image size: {IMAGE_SIZE}x{IMAGE_SIZE}")
|
| 65 |
+
print(f"Classes: {NUM_CLASSES}")
|
| 66 |
+
|
| 67 |
+
# %% [markdown]
|
| 68 |
+
# ## Step 2: Verify Dataset Structure
|
| 69 |
+
|
| 70 |
+
# %%
|
| 71 |
+
print("Verifying dataset structure...")
|
| 72 |
+
stats = verify_dataset_structure()
|
| 73 |
+
|
| 74 |
+
print()
|
| 75 |
+
print("=== Classification ===")
|
| 76 |
+
for split in ["train", "val", "test"]:
|
| 77 |
+
total = stats["classification"][split]["total"]
|
| 78 |
+
expected = {"train": int(IMAGES_PER_CLASS * 0.70 * NUM_CLASSES),
|
| 79 |
+
"val": int(IMAGES_PER_CLASS * 0.15 * NUM_CLASSES),
|
| 80 |
+
"test": int(IMAGES_PER_CLASS * 0.15 * NUM_CLASSES)}
|
| 81 |
+
status = "OK" if total > 0 else "MISSING"
|
| 82 |
+
print(f" {status} {split:5s}: {total} images (expected ~{expected[split]})")
|
| 83 |
+
print(f" Total: {stats['classification']['total']}")
|
| 84 |
+
|
| 85 |
+
print()
|
| 86 |
+
print("=== Detection ===")
|
| 87 |
+
for split in ["train", "val"]:
|
| 88 |
+
d = stats["detection"][split]
|
| 89 |
+
match = "OK" if d["images"] == d["labels"] else "MISMATCH"
|
| 90 |
+
print(f" {match} {split:5s}: {d['images']} images, {d['labels']} labels")
|
| 91 |
+
print(f" Total: {stats['detection']['total_images']} detection images")
|
| 92 |
+
|
| 93 |
+
print()
|
| 94 |
+
if stats["issues"]:
|
| 95 |
+
print(f"ISSUES ({len(stats['issues'])}):")
|
| 96 |
+
for issue in stats["issues"]:
|
| 97 |
+
print(f" {issue}")
|
| 98 |
+
else:
|
| 99 |
+
print("No issues found.")
|
| 100 |
+
|
| 101 |
+
# %% [markdown]
|
| 102 |
+
# ## Step 3: Validate YOLO Annotations
|
| 103 |
+
|
| 104 |
+
# %%
|
| 105 |
+
print("Validating YOLO annotations...")
|
| 106 |
+
for split in ["train", "val"]:
|
| 107 |
+
lbl_dir = DETECTION_DIR / "labels" / split
|
| 108 |
+
validate_yolo_annotations(lbl_dir)
|
| 109 |
+
print("All YOLO annotations valid.")
|
| 110 |
+
|
| 111 |
+
# %% [markdown]
|
| 112 |
+
# ## Step 4: Regenerate data.yaml (absolute paths for current machine/Colab)
|
| 113 |
+
|
| 114 |
+
# %%
|
| 115 |
+
yaml_path = create_yolo_data_yaml(DETECTION_DIR)
|
| 116 |
+
print(f"data.yaml ready: {yaml_path}")
|
| 117 |
+
|
| 118 |
+
# %% [markdown]
|
| 119 |
+
# ## Step 5: Augmentation Pipeline Summary
|
| 120 |
+
|
| 121 |
+
# %%
|
| 122 |
+
print("Augmentation pipeline (train):")
|
| 123 |
+
print(" RandomHorizontalFlip(p=0.5)")
|
| 124 |
+
print(" RandomRotation(degrees=15)")
|
| 125 |
+
print(" ColorJitter(brightness=0.2, contrast=0.2, saturation=0.1, hue=0.05)")
|
| 126 |
+
print(" RandomZoomOut(side_range=(1.0, 1.3), p=0.3)")
|
| 127 |
+
print(f" Resize({IMAGE_SIZE}, {IMAGE_SIZE})")
|
| 128 |
+
print(" ToImage() + ToDtype(float32)")
|
| 129 |
+
print(f" Normalize(mean={IMAGENET_MEAN}, std={IMAGENET_STD})")
|
| 130 |
+
print()
|
| 131 |
+
print("Evaluation pipeline (val/test):")
|
| 132 |
+
print(f" Resize({IMAGE_SIZE}, {IMAGE_SIZE})")
|
| 133 |
+
print(" ToImage() + ToDtype(float32)")
|
| 134 |
+
print(f" Normalize(mean={IMAGENET_MEAN}, std={IMAGENET_STD})")
|
| 135 |
+
|
| 136 |
+
# %% [markdown]
|
| 137 |
+
# ## Step 6: Show Augmented Samples
|
| 138 |
+
# Shows original vs 4 augmented versions for one image per class (sample of 5 classes).
|
| 139 |
+
# Uses torch if available; falls back to PIL raw crops if not.
|
| 140 |
+
|
| 141 |
+
# %%
|
| 142 |
+
SAMPLE_CLASSES = ["car", "dog", "bottle", "airplane", "chair"]
|
| 143 |
+
|
| 144 |
+
if TORCH_AVAILABLE:
|
| 145 |
+
train_tf = get_train_transforms(IMAGE_SIZE)
|
| 146 |
+
eval_tf = get_eval_transforms(IMAGE_SIZE)
|
| 147 |
+
|
| 148 |
+
fig, axes = plt.subplots(len(SAMPLE_CLASSES), 5, figsize=(15, 3 * len(SAMPLE_CLASSES)))
|
| 149 |
+
fig.suptitle("Augmentation Samples (col 1 = eval/original, cols 2-5 = train augmented)",
|
| 150 |
+
fontsize=11, fontweight="bold")
|
| 151 |
+
|
| 152 |
+
for row, cls in enumerate(SAMPLE_CLASSES):
|
| 153 |
+
cls_dir = CLASSIFICATION_DIR / "train" / cls
|
| 154 |
+
imgs = sorted(cls_dir.glob("*.jpg")) if cls_dir.exists() else []
|
| 155 |
+
if not imgs:
|
| 156 |
+
for col in range(5):
|
| 157 |
+
axes[row][col].axis("off")
|
| 158 |
+
axes[row][col].set_title("no image" if col == 0 else "")
|
| 159 |
+
continue
|
| 160 |
+
|
| 161 |
+
pil_img = Image.open(imgs[0]).convert("RGB")
|
| 162 |
+
|
| 163 |
+
# Col 0: eval transform (clean, no augmentation)
|
| 164 |
+
eval_tensor = eval_tf(pil_img)
|
| 165 |
+
axes[row][0].imshow(denormalize(eval_tensor))
|
| 166 |
+
axes[row][0].set_title(f"{cls}\n(eval)", fontsize=8)
|
| 167 |
+
axes[row][0].axis("off")
|
| 168 |
+
|
| 169 |
+
# Cols 1-4: different augmented versions
|
| 170 |
+
for col in range(1, 5):
|
| 171 |
+
aug_tensor = train_tf(pil_img)
|
| 172 |
+
axes[row][col].imshow(denormalize(aug_tensor))
|
| 173 |
+
axes[row][col].set_title(f"aug {col}", fontsize=8)
|
| 174 |
+
axes[row][col].axis("off")
|
| 175 |
+
|
| 176 |
+
plt.tight_layout()
|
| 177 |
+
plt.savefig(FIGURES_DIR / "preprocessing_01_augmented_samples.png", dpi=150, bbox_inches="tight")
|
| 178 |
+
plt.show()
|
| 179 |
+
print("Saved: preprocessing_01_augmented_samples.png")
|
| 180 |
+
|
| 181 |
+
else:
|
| 182 |
+
# No torch — show raw PIL crops
|
| 183 |
+
print("torch not available locally — showing raw crops (augmentation runs in Colab/venv with GPU)")
|
| 184 |
+
fig, axes = plt.subplots(1, len(SAMPLE_CLASSES), figsize=(15, 3))
|
| 185 |
+
fig.suptitle("Raw Classification Crops (augmentation visualization requires torch)", fontsize=10)
|
| 186 |
+
|
| 187 |
+
for col, cls in enumerate(SAMPLE_CLASSES):
|
| 188 |
+
cls_dir = CLASSIFICATION_DIR / "train" / cls
|
| 189 |
+
imgs = sorted(cls_dir.glob("*.jpg")) if cls_dir.exists() else []
|
| 190 |
+
if imgs:
|
| 191 |
+
axes[col].imshow(Image.open(imgs[0]).convert("RGB"))
|
| 192 |
+
axes[col].set_title(cls, fontsize=9)
|
| 193 |
+
axes[col].axis("off")
|
| 194 |
+
|
| 195 |
+
plt.tight_layout()
|
| 196 |
+
plt.savefig(FIGURES_DIR / "preprocessing_01_augmented_samples.png", dpi=150, bbox_inches="tight")
|
| 197 |
+
plt.show()
|
| 198 |
+
print("Saved: preprocessing_01_augmented_samples.png (raw crops, no augmentation)")
|
| 199 |
+
|
| 200 |
+
# %% [markdown]
|
| 201 |
+
# ## Step 7: Tensor Shape Verification
|
| 202 |
+
|
| 203 |
+
# %%
|
| 204 |
+
if TORCH_AVAILABLE:
|
| 205 |
+
import torch
|
| 206 |
+
train_tf = get_train_transforms(IMAGE_SIZE)
|
| 207 |
+
eval_tf = get_eval_transforms(IMAGE_SIZE)
|
| 208 |
+
|
| 209 |
+
# Pick one real image
|
| 210 |
+
test_img_path = next((CLASSIFICATION_DIR / "train" / "car").glob("*.jpg"), None)
|
| 211 |
+
if test_img_path:
|
| 212 |
+
pil_img = Image.open(test_img_path).convert("RGB")
|
| 213 |
+
t = train_tf(pil_img)
|
| 214 |
+
e = eval_tf(pil_img)
|
| 215 |
+
|
| 216 |
+
assert t.shape == (3, IMAGE_SIZE, IMAGE_SIZE), f"Wrong train shape: {t.shape}"
|
| 217 |
+
assert e.shape == (3, IMAGE_SIZE, IMAGE_SIZE), f"Wrong eval shape: {e.shape}"
|
| 218 |
+
assert t.dtype == torch.float32
|
| 219 |
+
assert e.dtype == torch.float32
|
| 220 |
+
print(f"Train tensor: shape={tuple(t.shape)}, dtype={t.dtype}, range=[{t.min():.2f}, {t.max():.2f}]")
|
| 221 |
+
print(f"Eval tensor: shape={tuple(e.shape)}, dtype={e.dtype}, range=[{e.min():.2f}, {e.max():.2f}]")
|
| 222 |
+
print("Tensor shapes and dtypes OK")
|
| 223 |
+
else:
|
| 224 |
+
print("No car images found to test")
|
| 225 |
+
else:
|
| 226 |
+
print("Skipping tensor shape check (torch not available locally)")
|
| 227 |
+
print("Tensor shapes will be verified in Colab during Section 5 training.")
|
| 228 |
+
|
| 229 |
+
# %% [markdown]
|
| 230 |
+
# ## Step 8: Save Preprocessing Summary
|
| 231 |
+
|
| 232 |
+
# %%
|
| 233 |
+
summary = {
|
| 234 |
+
"dataset_stats": stats,
|
| 235 |
+
"image_size": IMAGE_SIZE,
|
| 236 |
+
"imagenet_mean": IMAGENET_MEAN,
|
| 237 |
+
"imagenet_std": IMAGENET_STD,
|
| 238 |
+
"augmentations_train": [
|
| 239 |
+
"RandomHorizontalFlip(p=0.5)",
|
| 240 |
+
"RandomRotation(degrees=15)",
|
| 241 |
+
"ColorJitter(brightness=0.2, contrast=0.2, saturation=0.1, hue=0.05)",
|
| 242 |
+
"RandomZoomOut(side_range=(1.0,1.3), p=0.3)",
|
| 243 |
+
f"Resize({IMAGE_SIZE},{IMAGE_SIZE})",
|
| 244 |
+
"Normalize(ImageNet)",
|
| 245 |
+
],
|
| 246 |
+
"augmentations_eval": [
|
| 247 |
+
f"Resize({IMAGE_SIZE},{IMAGE_SIZE})",
|
| 248 |
+
"Normalize(ImageNet)",
|
| 249 |
+
],
|
| 250 |
+
"yolo_validation": "passed",
|
| 251 |
+
"data_yaml": str(yaml_path),
|
| 252 |
+
}
|
| 253 |
+
save_json(summary, DATA_PROCESSED_DIR / "preprocessing_summary.json")
|
| 254 |
+
print(f"Summary saved: {DATA_PROCESSED_DIR / 'preprocessing_summary.json'}")
|
| 255 |
+
|
| 256 |
+
# %%
|
| 257 |
+
print()
|
| 258 |
+
print("=" * 60)
|
| 259 |
+
print("SECTION 4 COMPLETE -- Preprocessing + Augmentation")
|
| 260 |
+
print("=" * 60)
|
| 261 |
+
print(f" Classification: {stats['classification']['total']} images (2500)")
|
| 262 |
+
print(f" Detection: {stats['detection']['total_images']} images")
|
| 263 |
+
print(f" YOLO labels: validated OK")
|
| 264 |
+
print(f" data.yaml: regenerated with absolute paths")
|
| 265 |
+
print(f" Transforms: train (augmented) + eval (clean) defined")
|
| 266 |
+
print()
|
| 267 |
+
print("Next: Section 5 -- CNN Training")
|
| 268 |
+
print(" Set MODEL='vgg16' in notebooks/04_train_classifier.py")
|
| 269 |
+
print(" Run in Google Colab T4 (GPU required)")
|
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# filename: src/data/augmentor.py
|
| 2 |
+
# purpose: torchvision.transforms.v2 augmentation pipelines for SmartVision CNN training
|
| 3 |
+
# version: 1.0
|
| 4 |
+
|
| 5 |
+
# torch/torchvision wrapped at module level — not available in local venv (fbgemm.dll)
|
| 6 |
+
# Functions return None gracefully when torch is absent; notebooks guard with TORCH_AVAILABLE.
|
| 7 |
+
try:
|
| 8 |
+
import torchvision.transforms.v2 as T
|
| 9 |
+
import torch
|
| 10 |
+
TORCH_AVAILABLE = True
|
| 11 |
+
except ImportError:
|
| 12 |
+
TORCH_AVAILABLE = False
|
| 13 |
+
|
| 14 |
+
# ImageNet normalization — required for ALL torchvision pretrained models (Rule 9)
|
| 15 |
+
IMAGENET_MEAN = [0.485, 0.456, 0.406]
|
| 16 |
+
IMAGENET_STD = [0.229, 0.224, 0.225]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def get_train_transforms(image_size: int = 224):
|
| 20 |
+
"""
|
| 21 |
+
Augmentation pipeline for training split.
|
| 22 |
+
Applies spatial + colour augmentations then normalises with ImageNet stats.
|
| 23 |
+
Compatible with SmartVisionDataset which returns PIL images.
|
| 24 |
+
"""
|
| 25 |
+
if not TORCH_AVAILABLE:
|
| 26 |
+
return None
|
| 27 |
+
|
| 28 |
+
return T.Compose([
|
| 29 |
+
T.RandomHorizontalFlip(p=0.5),
|
| 30 |
+
T.RandomRotation(degrees=15),
|
| 31 |
+
T.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.1, hue=0.05),
|
| 32 |
+
T.RandomZoomOut(fill=0, side_range=(1.0, 1.3), p=0.3),
|
| 33 |
+
T.Resize((image_size, image_size)),
|
| 34 |
+
T.ToImage(),
|
| 35 |
+
T.ToDtype(torch.float32, scale=True),
|
| 36 |
+
T.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
|
| 37 |
+
])
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def get_eval_transforms(image_size: int = 224):
|
| 41 |
+
"""
|
| 42 |
+
Evaluation pipeline for val and test splits.
|
| 43 |
+
No augmentation — resize + normalize only.
|
| 44 |
+
Must be identical at training-time and inference-time to avoid train/serve skew.
|
| 45 |
+
"""
|
| 46 |
+
if not TORCH_AVAILABLE:
|
| 47 |
+
return None
|
| 48 |
+
|
| 49 |
+
return T.Compose([
|
| 50 |
+
T.Resize((image_size, image_size)),
|
| 51 |
+
T.ToImage(),
|
| 52 |
+
T.ToDtype(torch.float32, scale=True),
|
| 53 |
+
T.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
|
| 54 |
+
])
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def denormalize(tensor):
|
| 58 |
+
"""
|
| 59 |
+
Reverse ImageNet normalization for visualization.
|
| 60 |
+
tensor: CHW float32 torch.Tensor
|
| 61 |
+
Returns: HWC uint8 numpy array (0-255) suitable for plt.imshow
|
| 62 |
+
"""
|
| 63 |
+
if not TORCH_AVAILABLE:
|
| 64 |
+
return None
|
| 65 |
+
import numpy as np
|
| 66 |
+
|
| 67 |
+
mean = torch.tensor(IMAGENET_MEAN).view(3, 1, 1)
|
| 68 |
+
std = torch.tensor(IMAGENET_STD).view(3, 1, 1)
|
| 69 |
+
img = tensor.clone().cpu() * std + mean
|
| 70 |
+
img = img.clamp(0, 1)
|
| 71 |
+
return (img.permute(1, 2, 0).numpy() * 255).astype(np.uint8)
|
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# filename: src/data/preprocessor.py
|
| 2 |
+
# purpose: YOLO annotation validation, data.yaml generation, dataset structure verification
|
| 3 |
+
# version: 1.0
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import yaml
|
| 9 |
+
|
| 10 |
+
from config import CLASSES, NUM_CLASSES, DATA_PROCESSED_DIR, ARTIFACTS_DIR
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def validate_yolo_annotations(labels_dir: Path) -> None:
|
| 16 |
+
"""
|
| 17 |
+
Assert all YOLO label files have valid normalized values.
|
| 18 |
+
Raises AssertionError immediately on the first invalid line.
|
| 19 |
+
Call this before every YOLO training run.
|
| 20 |
+
"""
|
| 21 |
+
labels_dir = Path(labels_dir)
|
| 22 |
+
if not labels_dir.exists():
|
| 23 |
+
logger.warning(f"Labels dir not found: {labels_dir}")
|
| 24 |
+
return
|
| 25 |
+
|
| 26 |
+
files_checked = 0
|
| 27 |
+
lines_checked = 0
|
| 28 |
+
|
| 29 |
+
for txt_file in labels_dir.glob("*.txt"):
|
| 30 |
+
content = txt_file.read_text(encoding="utf-8").strip()
|
| 31 |
+
if not content:
|
| 32 |
+
continue
|
| 33 |
+
for line in content.splitlines():
|
| 34 |
+
parts = line.split()
|
| 35 |
+
if len(parts) < 5:
|
| 36 |
+
continue
|
| 37 |
+
class_id = int(parts[0])
|
| 38 |
+
x_c = float(parts[1])
|
| 39 |
+
y_c = float(parts[2])
|
| 40 |
+
w = float(parts[3])
|
| 41 |
+
h = float(parts[4])
|
| 42 |
+
|
| 43 |
+
assert 0 <= class_id < NUM_CLASSES, (
|
| 44 |
+
f"class_id {class_id} out of range [0, {NUM_CLASSES}) in {txt_file}"
|
| 45 |
+
)
|
| 46 |
+
# Use <= 1.0 — bbox_to_yolo clamps to min(1.0,...) so boundary values are valid
|
| 47 |
+
assert 0.0 < x_c <= 1.0, f"x_center {x_c} not in (0,1] in {txt_file}"
|
| 48 |
+
assert 0.0 < y_c <= 1.0, f"y_center {y_c} not in (0,1] in {txt_file}"
|
| 49 |
+
assert 0.0 < w <= 1.0, f"width {w} not in (0,1] in {txt_file}"
|
| 50 |
+
assert 0.0 < h <= 1.0, f"height {h} not in (0,1] in {txt_file}"
|
| 51 |
+
# Catch clearly wrong values (negative or >1)
|
| 52 |
+
assert x_c >= 0 and y_c >= 0, f"Negative center in {txt_file}"
|
| 53 |
+
lines_checked += 1
|
| 54 |
+
files_checked += 1
|
| 55 |
+
|
| 56 |
+
print(f"YOLO annotations valid: {files_checked} files, {lines_checked} annotations in {labels_dir.name}/")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def create_yolo_data_yaml(detection_dir: Path) -> Path:
|
| 60 |
+
"""
|
| 61 |
+
Generate data.yaml with absolute paths — works identically in terminal and Colab.
|
| 62 |
+
Always regenerate before YOLO training (Colab paths differ from local paths).
|
| 63 |
+
"""
|
| 64 |
+
detection_dir = Path(detection_dir)
|
| 65 |
+
config = {
|
| 66 |
+
"path": str(detection_dir.absolute()),
|
| 67 |
+
"train": "images/train",
|
| 68 |
+
"val": "images/val",
|
| 69 |
+
"nc": NUM_CLASSES,
|
| 70 |
+
"names": {i: cls for i, cls in enumerate(CLASSES)},
|
| 71 |
+
}
|
| 72 |
+
yaml_path = detection_dir / "data.yaml"
|
| 73 |
+
with open(yaml_path, "w", encoding="utf-8") as f:
|
| 74 |
+
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
|
| 75 |
+
|
| 76 |
+
# Verify it loads cleanly
|
| 77 |
+
with open(yaml_path, encoding="utf-8") as f:
|
| 78 |
+
loaded = yaml.safe_load(f)
|
| 79 |
+
assert loaded["nc"] == NUM_CLASSES
|
| 80 |
+
assert len(loaded["names"]) == NUM_CLASSES
|
| 81 |
+
|
| 82 |
+
logger.info(f"data.yaml written: {yaml_path}")
|
| 83 |
+
print(f"data.yaml OK: nc={loaded['nc']}, path={loaded['path']}")
|
| 84 |
+
return yaml_path
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def verify_dataset_structure() -> dict:
|
| 88 |
+
"""
|
| 89 |
+
Count images in every split/class folder and return a summary dict.
|
| 90 |
+
Confirms the dataset was built correctly before preprocessing.
|
| 91 |
+
"""
|
| 92 |
+
from config import TRAIN_SPLIT, VAL_SPLIT
|
| 93 |
+
|
| 94 |
+
cls_dir = DATA_PROCESSED_DIR / "classification"
|
| 95 |
+
det_dir = DATA_PROCESSED_DIR / "detection"
|
| 96 |
+
|
| 97 |
+
result: dict = {
|
| 98 |
+
"classification": {},
|
| 99 |
+
"detection": {},
|
| 100 |
+
"issues": [],
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
# Classification counts
|
| 104 |
+
for split in ["train", "val", "test"]:
|
| 105 |
+
split_total = 0
|
| 106 |
+
per_class: dict[str, int] = {}
|
| 107 |
+
for cls in CLASSES:
|
| 108 |
+
count = len(list((cls_dir / split / cls).glob("*.jpg"))) \
|
| 109 |
+
if (cls_dir / split / cls).exists() else 0
|
| 110 |
+
per_class[cls] = count
|
| 111 |
+
split_total += count
|
| 112 |
+
if count == 0:
|
| 113 |
+
result["issues"].append(f"classification/{split}/{cls}: 0 images")
|
| 114 |
+
result["classification"][split] = {"total": split_total, "per_class": per_class}
|
| 115 |
+
|
| 116 |
+
# Detection counts
|
| 117 |
+
for split in ["train", "val"]:
|
| 118 |
+
imgs = len(list((det_dir / "images" / split).glob("*.jpg"))) \
|
| 119 |
+
if (det_dir / "images" / split).exists() else 0
|
| 120 |
+
lbls = len(list((det_dir / "labels" / split).glob("*.txt"))) \
|
| 121 |
+
if (det_dir / "labels" / split).exists() else 0
|
| 122 |
+
result["detection"][split] = {"images": imgs, "labels": lbls}
|
| 123 |
+
if imgs != lbls:
|
| 124 |
+
result["issues"].append(
|
| 125 |
+
f"detection/{split}: images({imgs}) != labels({lbls})"
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
# Expected totals
|
| 129 |
+
total_cls = sum(
|
| 130 |
+
result["classification"][s]["total"] for s in ["train", "val", "test"]
|
| 131 |
+
)
|
| 132 |
+
result["classification"]["total"] = total_cls
|
| 133 |
+
result["detection"]["total_images"] = sum(
|
| 134 |
+
result["detection"][s]["images"] for s in ["train", "val"]
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
return result
|