Uploaded Complete App
Browse files- app.py +48 -0
- models/notebook/data_results.ipynb +0 -0
- models/python/dataset_dl.py +70 -0
- models/python/train.py +92 -0
- pet_classifier.pth +3 -0
app.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
from torchvision import models, transforms
|
| 5 |
+
from PIL import Image
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
# Load class names from your training dataset
|
| 9 |
+
CLASS_NAMES = sorted(os.listdir("oxford_pet_dataset/train")) # ensure these match training classes
|
| 10 |
+
|
| 11 |
+
# Load the model
|
| 12 |
+
@st.cache_resource
|
| 13 |
+
def load_model():
|
| 14 |
+
model = models.resnet18(pretrained=False)
|
| 15 |
+
model.fc = nn.Linear(model.fc.in_features, len(CLASS_NAMES))
|
| 16 |
+
model.load_state_dict(torch.load("pet_classifier.pth", map_location=torch.device("cpu")))
|
| 17 |
+
model.eval()
|
| 18 |
+
return model
|
| 19 |
+
|
| 20 |
+
model = load_model()
|
| 21 |
+
|
| 22 |
+
# Image transform (should match training)
|
| 23 |
+
transform = transforms.Compose([
|
| 24 |
+
transforms.Resize((224, 224)),
|
| 25 |
+
transforms.ToTensor(),
|
| 26 |
+
transforms.Normalize([0.5]*3, [0.5]*3)
|
| 27 |
+
])
|
| 28 |
+
|
| 29 |
+
# Streamlit UI
|
| 30 |
+
st.title("🐾 Oxford Pet Classifier")
|
| 31 |
+
st.write("Upload a photo of a cat or dog and I’ll try to guess the breed!")
|
| 32 |
+
|
| 33 |
+
uploaded_file = st.file_uploader("Choose an image...", type=["jpg", "jpeg", "png"])
|
| 34 |
+
|
| 35 |
+
if uploaded_file is not None:
|
| 36 |
+
image = Image.open(uploaded_file).convert("RGB")
|
| 37 |
+
st.image(image, caption="Uploaded Image", use_column_width=True)
|
| 38 |
+
|
| 39 |
+
# Preprocess
|
| 40 |
+
input_tensor = transform(image).unsqueeze(0) # (1, 3, 224, 224)
|
| 41 |
+
|
| 42 |
+
# Predict
|
| 43 |
+
with torch.no_grad():
|
| 44 |
+
outputs = model(input_tensor)
|
| 45 |
+
_, predicted = torch.max(outputs, 1)
|
| 46 |
+
predicted_label = CLASS_NAMES[predicted.item()]
|
| 47 |
+
|
| 48 |
+
st.markdown(f"### 🐕 Prediction: **{predicted_label.title()}**")
|
models/notebook/data_results.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
models/python/dataset_dl.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import tarfile
|
| 3 |
+
import urllib.request
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from sklearn.model_selection import train_test_split
|
| 6 |
+
import shutil
|
| 7 |
+
from collections import defaultdict
|
| 8 |
+
|
| 9 |
+
# URLs
|
| 10 |
+
images_url = "https://www.robots.ox.ac.uk/~vgg/data/pets/data/images.tar.gz"
|
| 11 |
+
annotations_url = "https://www.robots.ox.ac.uk/~vgg/data/pets/data/annotations.tar.gz"
|
| 12 |
+
|
| 13 |
+
# Paths
|
| 14 |
+
root_dir = Path("oxford_pet_dataset")
|
| 15 |
+
images_tar = root_dir / "images.tar.gz"
|
| 16 |
+
annotations_tar = root_dir / "annotations.tar.gz"
|
| 17 |
+
images_dir = root_dir / "images"
|
| 18 |
+
annotations_dir = root_dir / "annotations"
|
| 19 |
+
|
| 20 |
+
# Create directory
|
| 21 |
+
root_dir.mkdir(exist_ok=True)
|
| 22 |
+
|
| 23 |
+
# Download function
|
| 24 |
+
def download(url, path):
|
| 25 |
+
if not path.exists():
|
| 26 |
+
print(f"Downloading {url}...")
|
| 27 |
+
urllib.request.urlretrieve(url, path)
|
| 28 |
+
print(f"Downloaded to {path}")
|
| 29 |
+
else:
|
| 30 |
+
print(f"{path.name} already exists.")
|
| 31 |
+
|
| 32 |
+
# Extract function
|
| 33 |
+
def extract(tar_path, extract_to):
|
| 34 |
+
if not extract_to.exists():
|
| 35 |
+
print(f"Extracting {tar_path.name}...")
|
| 36 |
+
with tarfile.open(tar_path) as tar:
|
| 37 |
+
tar.extractall(path=extract_to.parent)
|
| 38 |
+
print(f"Extracted to {extract_to}")
|
| 39 |
+
else:
|
| 40 |
+
print(f"{extract_to.name} already extracted.")
|
| 41 |
+
|
| 42 |
+
# Download and extract
|
| 43 |
+
download(images_url, images_tar)
|
| 44 |
+
download(annotations_url, annotations_tar)
|
| 45 |
+
extract(images_tar, images_dir)
|
| 46 |
+
extract(annotations_tar, annotations_dir)
|
| 47 |
+
|
| 48 |
+
# Function to extract class name from filename
|
| 49 |
+
def get_class_name(filename):
|
| 50 |
+
# Format: 'Abyssinian_123.jpg' → 'abyssinian'
|
| 51 |
+
return filename.name.split("_")[0].lower()
|
| 52 |
+
|
| 53 |
+
# Group image files by class
|
| 54 |
+
class_to_files = defaultdict(list)
|
| 55 |
+
for img_path in images_dir.glob("*.jpg"):
|
| 56 |
+
cls = get_class_name(img_path)
|
| 57 |
+
class_to_files[cls].append(img_path)
|
| 58 |
+
|
| 59 |
+
# Split each class into train/val/test and copy
|
| 60 |
+
for cls, files in class_to_files.items():
|
| 61 |
+
train_cls, testval_cls = train_test_split(files, test_size=0.2, random_state=42)
|
| 62 |
+
val_cls, test_cls = train_test_split(testval_cls, test_size=0.5, random_state=42)
|
| 63 |
+
|
| 64 |
+
for split_name, split_data in zip(["train", "val", "test"], [train_cls, val_cls, test_cls]):
|
| 65 |
+
split_cls_dir = root_dir / split_name / cls
|
| 66 |
+
split_cls_dir.mkdir(parents=True, exist_ok=True)
|
| 67 |
+
for file in split_data:
|
| 68 |
+
shutil.copy(file, split_cls_dir / file.name)
|
| 69 |
+
|
| 70 |
+
print("✅ Dataset is now organized by class for ImageFolder.")
|
models/python/train.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
import torch.optim as optim
|
| 5 |
+
from torchvision import datasets, transforms, models
|
| 6 |
+
from torch.utils.data import DataLoader
|
| 7 |
+
from tqdm import tqdm
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
# Set device
|
| 11 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 12 |
+
|
| 13 |
+
# Paths
|
| 14 |
+
root_dir = Path("/oxford_pet_dataset")
|
| 15 |
+
train_dir = root_dir / "train"
|
| 16 |
+
val_dir = root_dir / "val"
|
| 17 |
+
|
| 18 |
+
# Parameters
|
| 19 |
+
BATCH_SIZE = 32
|
| 20 |
+
EPOCHS = 10
|
| 21 |
+
NUM_CLASSES = len(os.listdir(train_dir)) # Assumes one folder per class
|
| 22 |
+
|
| 23 |
+
# Transforms
|
| 24 |
+
train_transforms = transforms.Compose([
|
| 25 |
+
transforms.Resize((224, 224)),
|
| 26 |
+
transforms.RandomHorizontalFlip(),
|
| 27 |
+
transforms.ToTensor(),
|
| 28 |
+
transforms.Normalize([0.5]*3, [0.5]*3)
|
| 29 |
+
])
|
| 30 |
+
|
| 31 |
+
val_transforms = transforms.Compose([
|
| 32 |
+
transforms.Resize((224, 224)),
|
| 33 |
+
transforms.ToTensor(),
|
| 34 |
+
transforms.Normalize([0.5]*3, [0.5]*3)
|
| 35 |
+
])
|
| 36 |
+
|
| 37 |
+
# Datasets
|
| 38 |
+
train_dataset = datasets.ImageFolder(train_dir, transform=train_transforms)
|
| 39 |
+
val_dataset = datasets.ImageFolder(val_dir, transform=val_transforms)
|
| 40 |
+
|
| 41 |
+
# DataLoaders
|
| 42 |
+
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
|
| 43 |
+
val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE)
|
| 44 |
+
|
| 45 |
+
# Model
|
| 46 |
+
model = models.resnet18(pretrained=True)
|
| 47 |
+
model.fc = nn.Linear(model.fc.in_features, NUM_CLASSES)
|
| 48 |
+
model = model.to(device)
|
| 49 |
+
|
| 50 |
+
# Loss and optimizer
|
| 51 |
+
criterion = nn.CrossEntropyLoss()
|
| 52 |
+
optimizer = optim.Adam(model.parameters(), lr=1e-4)
|
| 53 |
+
|
| 54 |
+
# Training loop
|
| 55 |
+
for epoch in range(EPOCHS):
|
| 56 |
+
model.train()
|
| 57 |
+
train_loss, train_correct = 0.0, 0
|
| 58 |
+
|
| 59 |
+
for inputs, labels in tqdm(train_loader, desc=f"Epoch {epoch+1}/{EPOCHS} [Train]"):
|
| 60 |
+
inputs, labels = inputs.to(device), labels.to(device)
|
| 61 |
+
|
| 62 |
+
optimizer.zero_grad()
|
| 63 |
+
outputs = model(inputs)
|
| 64 |
+
loss = criterion(outputs, labels)
|
| 65 |
+
loss.backward()
|
| 66 |
+
optimizer.step()
|
| 67 |
+
|
| 68 |
+
train_loss += loss.item() * inputs.size(0)
|
| 69 |
+
train_correct += (outputs.argmax(1) == labels).sum().item()
|
| 70 |
+
|
| 71 |
+
train_acc = train_correct / len(train_dataset)
|
| 72 |
+
|
| 73 |
+
# Validation
|
| 74 |
+
model.eval()
|
| 75 |
+
val_loss, val_correct = 0.0, 0
|
| 76 |
+
|
| 77 |
+
with torch.no_grad():
|
| 78 |
+
for inputs, labels in tqdm(val_loader, desc=f"Epoch {epoch+1}/{EPOCHS} [Val]"):
|
| 79 |
+
inputs, labels = inputs.to(device), labels.to(device)
|
| 80 |
+
outputs = model(inputs)
|
| 81 |
+
loss = criterion(outputs, labels)
|
| 82 |
+
|
| 83 |
+
val_loss += loss.item() * inputs.size(0)
|
| 84 |
+
val_correct += (outputs.argmax(1) == labels).sum().item()
|
| 85 |
+
|
| 86 |
+
val_acc = val_correct / len(val_dataset)
|
| 87 |
+
|
| 88 |
+
print(f"Epoch {epoch+1}: Train Acc: {train_acc:.4f}, Val Acc: {val_acc:.4f}")
|
| 89 |
+
|
| 90 |
+
# Save model
|
| 91 |
+
torch.save(model.state_dict(), "pet_classifier.pth")
|
| 92 |
+
print("Model saved as pet_classifier.pth")
|
pet_classifier.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:617c30e1fffc5689181299f3c6d7778ca5503b765de987779993776821fd3636
|
| 3 |
+
size 44857584
|