P-Karthik-Mohan commited on
Commit
83a443e
·
verified ·
1 Parent(s): d6d1c7a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +368 -12
app.py CHANGED
@@ -1,24 +1,380 @@
1
  # app.py
 
 
 
 
 
 
 
 
 
 
 
 
2
  import gradio as gr
3
- from transformers import pipeline
4
 
5
- # Load a ready-made image classification model
6
- classifier = pipeline("image-classification", model="google/vit-base-patch16-224")
 
 
 
 
7
 
8
- # Define prediction function
9
- def predict_image(img):
10
- results = classifier(img)
11
- # Take top 3 predictions
12
- return {res["label"]: float(res["score"]) for res in results[:3]}
 
13
 
14
- # Create Gradio interface
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  demo = gr.Interface(
16
  fn=predict_image,
17
  inputs=gr.Image(type="pil"),
18
  outputs=gr.Label(num_top_classes=3),
19
- title="Simple Image Classifier",
20
- description="Upload an image and let AI classify it!"
 
21
  )
22
 
23
  if __name__ == "__main__":
24
- demo.launch()
 
1
  # app.py
2
+ import os
3
+ import io
4
+ import zipfile
5
+ import json
6
+ import shutil
7
+ from pathlib import Path
8
+ from PIL import Image
9
+ import torch
10
+ from torchvision import models, transforms, datasets
11
+ from torch.utils.data import DataLoader, random_split
12
+ import torch.nn as nn
13
+ import torch.optim as optim
14
  import gradio as gr
15
+ import time
16
 
17
+ ROOT = Path(".")
18
+ DATA_ZIP_NAME = "dataset.zip" # upload your Roboflow export here
19
+ WORK_DIR = ROOT / "roboflow_dataset"
20
+ CLASSIFY_DIR = ROOT / "classification_data"
21
+ MODEL_PATH = ROOT / "model.pth"
22
+ CLASSES_JSON = ROOT / "classes.json"
23
 
24
+ # Training config (tweak if needed)
25
+ BATCH_SIZE = 16
26
+ IMG_SIZE = 224
27
+ NUM_EPOCHS = int(os.environ.get("NUM_EPOCHS", 3)) # small default for Spaces CPU
28
+ LR = 1e-3
29
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
30
 
31
+
32
+ def safe_mkdir(p: Path):
33
+ p.mkdir(parents=True, exist_ok=True)
34
+
35
+
36
+ def extract_zip_to_workdir(zip_path: Path, out_dir: Path):
37
+ if out_dir.exists():
38
+ shutil.rmtree(out_dir)
39
+ safe_mkdir(out_dir)
40
+ with zipfile.ZipFile(zip_path, "r") as z:
41
+ z.extractall(out_dir)
42
+
43
+
44
+ def find_classes_mapping(workdir: Path):
45
+ # Roboflow usually includes a data.yaml or classes.txt or a names list.
46
+ # Try common locations.
47
+ data_yaml = workdir / "data.yaml"
48
+ classes_txt = workdir / "classes.txt"
49
+ # Sometimes Roboflow includes a folder "labels" and a file "labels.names" or "classes.txt"
50
+ if classes_txt.exists():
51
+ names = [x.strip() for x in classes_txt.read_text().splitlines() if x.strip()]
52
+ return names
53
+ if data_yaml.exists():
54
+ import yaml # note: pyyaml must be in requirements if needed
55
+ try:
56
+ parsed = yaml.safe_load(data_yaml.read_text())
57
+ if "names" in parsed:
58
+ # could be list or dict
59
+ n = parsed["names"]
60
+ if isinstance(n, dict):
61
+ return [n[k] for k in sorted(n.keys(), key=lambda x: int(x))]
62
+ elif isinstance(n, list):
63
+ return n
64
+ except Exception:
65
+ pass
66
+ # fallback: try to find a file named "classes.txt" or "labels.names"
67
+ for candidate in workdir.rglob("classes.txt"):
68
+ names = [x.strip() for x in candidate.read_text().splitlines() if x.strip()]
69
+ if names:
70
+ return names
71
+ for candidate in workdir.rglob("labels.names"):
72
+ names = [x.strip() for x in candidate.read_text().splitlines() if x.strip()]
73
+ if names:
74
+ return names
75
+ # last resort: scan label files to get max class index, produce numeric names
76
+ max_idx = -1
77
+ for lbl in workdir.rglob("labels/*.txt"):
78
+ for line in lbl.read_text().splitlines():
79
+ parts = line.strip().split()
80
+ if len(parts) >= 1:
81
+ try:
82
+ idx = int(float(parts[0]))
83
+ max_idx = max(max_idx, idx)
84
+ except:
85
+ pass
86
+ if max_idx >= 0:
87
+ return [f"class_{i}" for i in range(max_idx + 1)]
88
+ return []
89
+
90
+
91
+ def convert_roboflow_detection_to_classification(workdir: Path, outdir: Path):
92
+ """
93
+ Creates a folder-structured classification dataset:
94
+ outdir/train/<class_name>/*.jpg
95
+ outdir/valid/<class_name>/*.jpg
96
+ It uses label files (YOLO txt) to assign the main class for each image.
97
+ If bounding box info is available, it crops the bbox; otherwise it copies the image.
98
+ """
99
+ if outdir.exists():
100
+ shutil.rmtree(outdir)
101
+ safe_mkdir(outdir)
102
+
103
+ # Try common image and label folders
104
+ images_dirs = []
105
+ labels_dirs = []
106
+ for p in workdir.iterdir():
107
+ if p.is_dir():
108
+ if p.name.lower() in ("images", "image", "images/train", "train", "valid", "test"):
109
+ images_dirs.append(p)
110
+ if p.name.lower() in ("labels", "annotations"):
111
+ labels_dirs.append(p)
112
+
113
+ # simpler approach: look for 'images' and 'labels' in any depth
114
+ images_all = list(workdir.rglob("images/*")) + list(workdir.rglob("images/*/*"))
115
+ if not images_all:
116
+ # fallback to all popular image file types in workdir
117
+ images_all = [p for p in workdir.rglob("*") if p.suffix.lower() in (".jpg", ".jpeg", ".png")]
118
+
119
+ # mapping of image filename (no path) to its full path
120
+ img_map = {p.name: p for p in images_all}
121
+
122
+ # find label files
123
+ label_files = list(workdir.rglob("labels/*.txt")) + list(workdir.rglob("labels/*/*.txt"))
124
+ if not label_files:
125
+ # some exports put labels alongside images with same base name but different extension
126
+ label_files = [p for p in workdir.rglob("*.txt") if p.stem in img_map]
127
+
128
+ # find class names
129
+ classes = find_classes_mapping(workdir)
130
+ if not classes:
131
+ # if not available, default to single class "unknown"
132
+ classes = ["class_0"]
133
+
134
+ # prepare train/valid split target folders (Roboflow often has train/valid folders; try to preserve)
135
+ # We'll just create train and valid
136
+ train_out = outdir / "train"
137
+ valid_out = outdir / "valid"
138
+ safe_mkdir(train_out)
139
+ safe_mkdir(valid_out)
140
+
141
+ # Load label->image mapping from label_files
142
+ # We'll assume label files mirror the image names: e.g., images/train/img1.jpg and labels/train/img1.txt
143
+ img_to_labels = {}
144
+ for lbl in label_files:
145
+ name = lbl.stem
146
+ if name in img_map:
147
+ img_to_labels[name] = lbl
148
+
149
+ # If Roboflow has images split into train/valid dirs, detect them
150
+ # Otherwise we'll create a split based on filenames (80/20)
151
+ # Build a dataset list
152
+ dataset_rows = []
153
+ for img_name, img_path in img_map.items():
154
+ lbl = img_to_labels.get(Path(img_name).stem)
155
+ # Determine main class for this image (first label line)
156
+ main_class = None
157
+ bbox = None
158
+ if lbl and lbl.exists():
159
+ lines = [l for l in lbl.read_text().splitlines() if l.strip()]
160
+ if lines:
161
+ parts = lines[0].split()
162
+ try:
163
+ cls_idx = int(float(parts[0]))
164
+ main_class = classes[cls_idx] if cls_idx < len(classes) else f"class_{cls_idx}"
165
+ if len(parts) >= 5:
166
+ # YOLO format: cls x_center y_center width height (normalized)
167
+ bbox = tuple(float(x) for x in parts[1:5])
168
+ except Exception:
169
+ pass
170
+ if not main_class:
171
+ # fallback: mark as unknown
172
+ main_class = "unknown"
173
+ if "unknown" not in classes:
174
+ classes.append("unknown")
175
+ dataset_rows.append((img_path, main_class, bbox))
176
+
177
+ # do deterministic split
178
+ dataset_rows.sort(key=lambda x: x[0].name)
179
+ split_idx = int(0.8 * len(dataset_rows))
180
+ train_rows = dataset_rows[:split_idx]
181
+ valid_rows = dataset_rows[split_idx:]
182
+
183
+ def save_rows(rows, dest_folder):
184
+ for img_path, cls_name, bbox in rows:
185
+ dest_cls = dest_folder / cls_name
186
+ safe_mkdir(dest_cls)
187
+ try:
188
+ img = Image.open(img_path).convert("RGB")
189
+ if bbox:
190
+ # bbox are normalized; convert to pixel coords
191
+ w, h = img.size
192
+ xc, yc, bw, bh = bbox
193
+ left = int((xc - bw / 2) * w)
194
+ right = int((xc + bw / 2) * w)
195
+ top = int((yc - bh / 2) * h)
196
+ bottom = int((yc + bh / 2) * h)
197
+ # clamp
198
+ left = max(0, left); right = min(w, right)
199
+ top = max(0, top); bottom = min(h, bottom)
200
+ if right - left > 10 and bottom - top > 10:
201
+ img = img.crop((left, top, right, bottom))
202
+ # save with a unique name
203
+ dest_path = dest_cls / img_path.name
204
+ img.save(dest_path)
205
+ except Exception as e:
206
+ print("Skipping", img_path, "due to", e)
207
+
208
+ save_rows(train_rows, train_out)
209
+ save_rows(valid_rows, valid_out)
210
+
211
+ # Save classes json
212
+ with open(CLASSES_JSON, "w") as f:
213
+ json.dump(classes, f)
214
+ return classes
215
+
216
+
217
+ def build_model(num_classes):
218
+ model = models.resnet18(pretrained=True)
219
+ in_features = model.fc.in_features
220
+ model.fc = nn.Linear(in_features, num_classes)
221
+ return model
222
+
223
+
224
+ def train_model(data_dir: Path, classes):
225
+ print("Starting training. This may take some time on CPU.")
226
+ num_classes = len(classes)
227
+ model = build_model(num_classes).to(DEVICE)
228
+
229
+ transform_train = transforms.Compose([
230
+ transforms.Resize((IMG_SIZE, IMG_SIZE)),
231
+ transforms.RandomHorizontalFlip(),
232
+ transforms.ToTensor(),
233
+ transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])
234
+ ])
235
+ transform_valid = transforms.Compose([
236
+ transforms.Resize((IMG_SIZE, IMG_SIZE)),
237
+ transforms.ToTensor(),
238
+ transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])
239
+ ])
240
+
241
+ dataset_train = datasets.ImageFolder(str(data_dir / "train"), transform=transform_train)
242
+ dataset_valid = datasets.ImageFolder(str(data_dir / "valid"), transform=transform_valid)
243
+
244
+ # If ImageFolder class mapping differs from classes list, use folder names.
245
+ # Dataloaders
246
+ if len(dataset_train) == 0:
247
+ raise RuntimeError("No training images found. Please check dataset structure.")
248
+
249
+ loader_train = DataLoader(dataset_train, batch_size=BATCH_SIZE, shuffle=True, num_workers=0)
250
+ loader_valid = DataLoader(dataset_valid, batch_size=BATCH_SIZE, shuffle=False, num_workers=0)
251
+
252
+ criterion = nn.CrossEntropyLoss()
253
+ optimizer = optim.Adam(model.parameters(), lr=LR)
254
+
255
+ best_val = 0.0
256
+ for epoch in range(NUM_EPOCHS):
257
+ model.train()
258
+ running = 0.0
259
+ for imgs, labels in loader_train:
260
+ imgs = imgs.to(DEVICE)
261
+ labels = labels.to(DEVICE)
262
+ optimizer.zero_grad()
263
+ outputs = model(imgs)
264
+ loss = criterion(outputs, labels)
265
+ loss.backward()
266
+ optimizer.step()
267
+ running += loss.item()
268
+ # validation
269
+ model.eval()
270
+ correct = 0
271
+ total = 0
272
+ with torch.no_grad():
273
+ for imgs, labels in loader_valid:
274
+ imgs = imgs.to(DEVICE)
275
+ labels = labels.to(DEVICE)
276
+ outputs = model(imgs)
277
+ _, preds = torch.max(outputs, 1)
278
+ correct += (preds == labels).sum().item()
279
+ total += labels.size(0)
280
+ acc = correct / total if total > 0 else 0.0
281
+ print(f"Epoch {epoch+1}/{NUM_EPOCHS}, loss={running:.4f}, val_acc={acc:.4f}")
282
+ if acc > best_val:
283
+ best_val = acc
284
+ # save best
285
+ torch.save({
286
+ "model_state": model.state_dict(),
287
+ "classes": classes
288
+ }, MODEL_PATH)
289
+ print("Training complete. Best val acc:", best_val)
290
+ # final save if not saved
291
+ if not MODEL_PATH.exists():
292
+ torch.save({
293
+ "model_state": model.state_dict(),
294
+ "classes": classes
295
+ }, MODEL_PATH)
296
+ return MODEL_PATH.exists()
297
+
298
+
299
+ def load_saved_model(path: Path):
300
+ data = torch.load(path, map_location=DEVICE)
301
+ classes = data.get("classes", None)
302
+ if not classes and Path(CLASSES_JSON).exists():
303
+ classes = json.loads(Path(CLASSES_JSON).read_text())
304
+ if not classes:
305
+ classes = [f"class_{i}" for i in range(2)]
306
+ model = build_model(len(classes))
307
+ model.load_state_dict(data["model_state"])
308
+ model.to(DEVICE).eval()
309
+ return model, classes
310
+
311
+
312
+ # Prepare model at startup
313
+ MODEL = None
314
+ BREEDS = None
315
+
316
+ def startup():
317
+ global MODEL, BREEDS
318
+ # If model exists, load directly
319
+ if Path(MODEL_PATH).exists():
320
+ try:
321
+ MODEL, BREEDS = load_saved_model(Path(MODEL_PATH))
322
+ print("Loaded existing model with classes:", BREEDS)
323
+ return
324
+ except Exception as e:
325
+ print("Failed to load existing model:", e)
326
+
327
+ # If dataset.zip exists, extract and convert, then train
328
+ if Path(DATA_ZIP_NAME).exists():
329
+ print("dataset.zip found. Extracting and preparing...")
330
+ extract_zip_to_workdir(Path(DATA_ZIP_NAME), WORK_DIR)
331
+ classes = convert_roboflow_detection_to_classification(WORK_DIR, CLASSIFY_DIR)
332
+ print("Prepared classification dataset with classes:", classes)
333
+ # train (may be slow on CPU)
334
+ try:
335
+ trained = train_model(CLASSIFY_DIR, classes)
336
+ if trained:
337
+ MODEL, BREEDS = load_saved_model(Path(MODEL_PATH))
338
+ except Exception as e:
339
+ print("Training failed:", e)
340
+ else:
341
+ print("No dataset.zip found. Please upload dataset.zip to the Space root or upload a model.pth")
342
+
343
+
344
+ # Prediction function
345
+ transform_predict = transforms.Compose([
346
+ transforms.Resize((IMG_SIZE, IMG_SIZE)),
347
+ transforms.ToTensor(),
348
+ transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])
349
+ ])
350
+
351
+ def predict_image(pil_img):
352
+ global MODEL, BREEDS
353
+ if MODEL is None:
354
+ return {"error": "Model not ready. Upload dataset.zip to train, or model.pth to load."}
355
+ img = pil_img.convert("RGB")
356
+ x = transform_predict(img).unsqueeze(0).to(DEVICE)
357
+ with torch.no_grad():
358
+ out = MODEL(x)
359
+ probs = torch.nn.functional.softmax(out[0], dim=0).cpu().numpy()
360
+ # top 3
361
+ indices = probs.argsort()[::-1][:3]
362
+ return {BREEDS[int(i)]: float(probs[int(i)]) for i in indices}
363
+
364
+ # Run startup (this will attempt to load or train)
365
+ start_time = time.time()
366
+ startup()
367
+ print("Startup complete in", time.time() - start_time, "seconds")
368
+
369
+ # Build Gradio app
370
  demo = gr.Interface(
371
  fn=predict_image,
372
  inputs=gr.Image(type="pil"),
373
  outputs=gr.Label(num_top_classes=3),
374
+ examples=[],
375
+ title="Cow Breed Classifier",
376
+ description="Upload a cow image. If you uploaded Roboflow dataset.zip to the Space root, the Space will auto-train on start (small number of epochs). If you already have a trained model.pth, upload that instead to skip training."
377
  )
378
 
379
  if __name__ == "__main__":
380
+ demo.launch(server_name="0.0.0.0", server_port=7860)