Priya Monisha commited on
Commit
2ea8a2d
·
1 Parent(s): 155a736

section-4: preprocessing + augmentation — verified end-to-end

Browse files

src/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

data/processed/smartvision_dataset/preprocessing_summary.json ADDED
@@ -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
+ }
docs/figures/preprocessing_01_augmented_samples.png ADDED

Git LFS Details

  • SHA256: b56b1dc5540af3d5ce830dbdb6812e2057f20f6da0c192ead5b7cbb59d6f4020
  • Pointer size: 132 Bytes
  • Size of remote file: 5.03 MB
notebooks/03_preprocessing.py ADDED
@@ -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)")
src/data/augmentor.py ADDED
@@ -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)
src/data/preprocessor.py ADDED
@@ -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