Spaces:
Sleeping
Sleeping
First commit
Browse files- .dockerignore +9 -0
- .gitattributes +1 -0
- Dockerfile +21 -0
- README.md +25 -3
- app/__init__.py +0 -0
- app/cd.pt +3 -0
- app/main.py +26 -0
- app/model.py +22 -0
- app/predict.py +34 -0
- requirements.txt +9 -0
.dockerignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
mlops
|
| 2 |
+
model_funcs
|
| 3 |
+
__pycache__
|
| 4 |
+
*.pyc
|
| 5 |
+
*.log
|
| 6 |
+
.git
|
| 7 |
+
.venv
|
| 8 |
+
*.swp
|
| 9 |
+
*.swo
|
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
app/cd.pt filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12.3
|
| 2 |
+
|
| 3 |
+
# Create non-root user with UID 1000
|
| 4 |
+
RUN useradd -m -u 1000 user
|
| 5 |
+
USER user
|
| 6 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 7 |
+
|
| 8 |
+
WORKDIR /app
|
| 9 |
+
|
| 10 |
+
# Copy requirements and install
|
| 11 |
+
COPY --chown=user requirements.txt .
|
| 12 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 13 |
+
|
| 14 |
+
# Copy app code
|
| 15 |
+
COPY --chown=user app/ app/
|
| 16 |
+
|
| 17 |
+
# Expose port 7860 (used by HF Spaces)
|
| 18 |
+
EXPOSE 7860
|
| 19 |
+
|
| 20 |
+
# Run FastAPI with uvicorn on the required port
|
| 21 |
+
CMD ["python3", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,10 +1,32 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: red
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Cat vs Dog Classifier 🐶🐱
|
| 3 |
+
emoji: 🐾
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: red
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
+
# 🐾 Cat vs Dog Classifier - FastAPI + PyTorch
|
| 11 |
+
|
| 12 |
+
This Space hosts a FastAPI-based image classification API that distinguishes between **cats** and **dogs** using a fine-tuned **ResNet50** model.
|
| 13 |
+
|
| 14 |
+
## 🚀 How It Works
|
| 15 |
+
|
| 16 |
+
- Accepts an uploaded image via a POST request.
|
| 17 |
+
- Processes the image using `albumentations` and a pre-trained PyTorch model.
|
| 18 |
+
- Returns `"cat"` or `"dog"` based on prediction confidence.
|
| 19 |
+
|
| 20 |
+
## 🧪 Try It Out
|
| 21 |
+
|
| 22 |
+
You can interact with the `/predict-image` endpoint using tools like:
|
| 23 |
+
- `curl`
|
| 24 |
+
- Postman
|
| 25 |
+
- Any frontend that supports `multipart/form-data` file uploads.
|
| 26 |
+
|
| 27 |
+
### Example:
|
| 28 |
+
|
| 29 |
+
```bash
|
| 30 |
+
curl -X POST \
|
| 31 |
+
-F "file=@your_image.jpg" \
|
| 32 |
+
https://Klasta-testing_spaces.hf.space/predict-image
|
app/__init__.py
ADDED
|
File without changes
|
app/cd.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ba6880fed2ae00203357b8de47b92f31d1d7ad4fb6b5c45b79968e47328aa583
|
| 3 |
+
size 94343841
|
app/main.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fastapi
|
| 2 |
+
import io
|
| 3 |
+
from fastapi import UploadFile, File
|
| 4 |
+
from PIL import Image
|
| 5 |
+
from .predict import predict_image
|
| 6 |
+
|
| 7 |
+
app = fastapi.FastAPI()
|
| 8 |
+
|
| 9 |
+
@app.get("/")
|
| 10 |
+
def root():
|
| 11 |
+
return {"message": "API is working!"}
|
| 12 |
+
|
| 13 |
+
@app.post("/predict-image")
|
| 14 |
+
async def upload_image(file: UploadFile = File(...)):
|
| 15 |
+
if not file.content_type.startswith("image/"):
|
| 16 |
+
return {"error": "Please upload an image to proceed"}
|
| 17 |
+
|
| 18 |
+
contents = await file.read()
|
| 19 |
+
|
| 20 |
+
try:
|
| 21 |
+
image = Image.open(io.BytesIO(contents))
|
| 22 |
+
except Exception as e:
|
| 23 |
+
return {"error": f"Failed to open image: {str(e)}"}
|
| 24 |
+
|
| 25 |
+
pred = predict_image(image)
|
| 26 |
+
return {"prediction": pred}
|
app/model.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import torchvision.models as models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class CatvsDogResNet50(nn.Module):
|
| 7 |
+
def __init__(self, freeze_backbone: bool = True):
|
| 8 |
+
super().__init__()
|
| 9 |
+
self.backbone = models.resnet50(pretrained=True)
|
| 10 |
+
|
| 11 |
+
if freeze_backbone:
|
| 12 |
+
for param in self.backbone.parameters():
|
| 13 |
+
param.requires_grad = False
|
| 14 |
+
|
| 15 |
+
num_ftrs = self.backbone.fc.in_features
|
| 16 |
+
self.backbone.fc = nn.Sequential(
|
| 17 |
+
nn.Dropout(0.5),
|
| 18 |
+
nn.Linear(num_ftrs, 1),
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 22 |
+
return self.backbone(x)
|
app/predict.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/predict.py
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import numpy as np
|
| 5 |
+
from PIL import Image
|
| 6 |
+
import albumentations as A
|
| 7 |
+
from albumentations.pytorch import ToTensorV2
|
| 8 |
+
from .model import CatvsDogResNet50
|
| 9 |
+
|
| 10 |
+
transform = A.Compose(
|
| 11 |
+
[
|
| 12 |
+
A.Resize(224, 224),
|
| 13 |
+
A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
|
| 14 |
+
ToTensorV2(),
|
| 15 |
+
]
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
device = "cuda" if torch.cuda.is_available() else "cpu" # Fallback to CPU if no GPU
|
| 19 |
+
model = CatvsDogResNet50(freeze_backbone=True)
|
| 20 |
+
model.load_state_dict(torch.load(r"app/cd.pt", map_location=device))
|
| 21 |
+
model = model.to(device).eval()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def predict_image(image: Image.Image) -> str:
|
| 25 |
+
img = image.convert("RGB")
|
| 26 |
+
img = np.array(img)
|
| 27 |
+
img = transform(image=img)["image"]
|
| 28 |
+
img = img.unsqueeze(0).to(device)
|
| 29 |
+
|
| 30 |
+
with torch.inference_mode():
|
| 31 |
+
out = model(img)
|
| 32 |
+
prob = torch.sigmoid(out).item()
|
| 33 |
+
pred = "dog" if prob > 0.5 else "cat"
|
| 34 |
+
return pred
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.12
|
| 2 |
+
uvicorn==0.34.3
|
| 3 |
+
torch==2.7.1
|
| 4 |
+
torchvision==0.22.1
|
| 5 |
+
albumentations==2.0.8
|
| 6 |
+
pillow==11.2.1
|
| 7 |
+
numpy==2.1.3
|
| 8 |
+
python-multipart==0.0.20
|
| 9 |
+
mlflow==3.1.0
|