Spaces:
Running
Running
Add initial FastAPI application with Docker setup and environment configuration
Browse files- Docker.fastapi +42 -0
- Dockerfile +29 -0
- app.py +487 -0
- docker-compose.yml +18 -0
- environment.env +3 -0
- requirements.txt +13 -0
- start.sh +17 -0
Docker.fastapi
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.11-slim
|
2 |
+
|
3 |
+
# Install system dependencies with security updates
|
4 |
+
RUN apt-get update && apt-get install -y \
|
5 |
+
curl \
|
6 |
+
&& apt-get upgrade -y \
|
7 |
+
&& apt-get clean \
|
8 |
+
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
9 |
+
|
10 |
+
# Create non-root user for security
|
11 |
+
RUN groupadd -r appuser && useradd -r -g appuser -m appuser
|
12 |
+
|
13 |
+
# Set working directory
|
14 |
+
WORKDIR /app
|
15 |
+
|
16 |
+
# Copy requirements and install Python packages
|
17 |
+
COPY requirements.txt .
|
18 |
+
RUN pip3 install --no-cache-dir --upgrade pip && \
|
19 |
+
pip3 install --no-cache-dir -r requirements.txt
|
20 |
+
|
21 |
+
# Copy FastAPI application
|
22 |
+
COPY app.py .
|
23 |
+
|
24 |
+
# Create logs directory and change ownership
|
25 |
+
RUN mkdir -p /app/logs && \
|
26 |
+
chown -R appuser:appuser /app
|
27 |
+
|
28 |
+
# Set environment variables
|
29 |
+
ENV HOME=/home/appuser
|
30 |
+
|
31 |
+
# Switch to non-root user
|
32 |
+
USER appuser
|
33 |
+
|
34 |
+
# Expose port
|
35 |
+
EXPOSE 7860
|
36 |
+
|
37 |
+
# Health check
|
38 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
39 |
+
CMD curl -f http://localhost:7860/health || exit 1
|
40 |
+
|
41 |
+
# Start FastAPI application
|
42 |
+
CMD ["python3", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
Dockerfile
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Base image
|
2 |
+
FROM python:3.10-slim
|
3 |
+
|
4 |
+
# Set working directory
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Install system dependencies
|
8 |
+
RUN apt-get update && apt-get install -y \
|
9 |
+
git \
|
10 |
+
ffmpeg \
|
11 |
+
libgl1 \
|
12 |
+
libglib2.0-0 \
|
13 |
+
&& rm -rf /var/lib/apt/lists/*
|
14 |
+
|
15 |
+
# Install Python dependencies
|
16 |
+
COPY requirement.txt .
|
17 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
18 |
+
|
19 |
+
# Copy app code
|
20 |
+
COPY app.py .
|
21 |
+
|
22 |
+
# Copy .env if present (optional, for Cloudinary config)
|
23 |
+
# COPY .env .
|
24 |
+
|
25 |
+
# Expose the port used by FastAPI (default: 7860)
|
26 |
+
EXPOSE 7860
|
27 |
+
|
28 |
+
# Start the FastAPI app with uvicorn
|
29 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
app.py
ADDED
@@ -0,0 +1,487 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI, HTTPException
|
2 |
+
from pydantic import BaseModel, HttpUrl
|
3 |
+
from transformers import SegformerImageProcessor, AutoModelForSemanticSegmentation
|
4 |
+
from PIL import Image, ImageEnhance, ImageFilter
|
5 |
+
import torch.nn as nn
|
6 |
+
import torch
|
7 |
+
import cv2
|
8 |
+
import numpy as np
|
9 |
+
import os
|
10 |
+
import requests
|
11 |
+
import io
|
12 |
+
from datetime import datetime
|
13 |
+
from scipy import ndimage
|
14 |
+
import json
|
15 |
+
import tempfile
|
16 |
+
import shutil
|
17 |
+
from typing import List, Dict, Optional
|
18 |
+
import uuid
|
19 |
+
import asyncio
|
20 |
+
from concurrent.futures import ThreadPoolExecutor
|
21 |
+
import logging
|
22 |
+
import cloudinary
|
23 |
+
import cloudinary.uploader
|
24 |
+
from cloudinary.utils import cloudinary_url
|
25 |
+
import os
|
26 |
+
from dotenv import load_dotenv
|
27 |
+
|
28 |
+
|
29 |
+
# Configure logging
|
30 |
+
logging.basicConfig(level=logging.INFO)
|
31 |
+
logger = logging.getLogger(__name__)
|
32 |
+
|
33 |
+
app = FastAPI(title="Fashion Segmentation API", version="1.0.0")
|
34 |
+
|
35 |
+
# Request/Response models
|
36 |
+
class SegmentationRequest(BaseModel):
|
37 |
+
image_url: HttpUrl
|
38 |
+
settings: Optional[Dict] = {
|
39 |
+
"padding": 15,
|
40 |
+
"background": "white",
|
41 |
+
"quality": "high",
|
42 |
+
"outline": "grey_2px"
|
43 |
+
}
|
44 |
+
|
45 |
+
class SegmentInfo(BaseModel):
|
46 |
+
class_id: int
|
47 |
+
class_name: str
|
48 |
+
filename: str
|
49 |
+
category: str
|
50 |
+
pixel_count: int
|
51 |
+
coverage_percent: float
|
52 |
+
cloudinary_url: str
|
53 |
+
public_id: str
|
54 |
+
|
55 |
+
class SegmentationResponse(BaseModel):
|
56 |
+
success: bool
|
57 |
+
processing_time: float
|
58 |
+
total_segments: int
|
59 |
+
segments: List[SegmentInfo]
|
60 |
+
metadata: Dict
|
61 |
+
|
62 |
+
# Global model storage
|
63 |
+
model_cache = {}
|
64 |
+
executor = ThreadPoolExecutor(max_workers=4)
|
65 |
+
|
66 |
+
# Constants
|
67 |
+
SEGFORMER_LABELS = {
|
68 |
+
0: "Background", 1: "Hat", 2: "Hair", 3: "Sunglasses", 4: "Upper-clothes",
|
69 |
+
5: "Skirt", 6: "Pants", 7: "Dress", 8: "Belt", 9: "Left-shoe", 10: "Right-shoe",
|
70 |
+
11: "Face", 12: "Left-leg", 13: "Right-leg", 14: "Left-arm", 15: "Right-arm",
|
71 |
+
16: "Bag", 17: "Scarf"
|
72 |
+
}
|
73 |
+
|
74 |
+
CLOTHING_ITEMS = {4, 5, 6, 7, 8, 17} # Upper-clothes, Skirt, Pants, Dress, Belt, Scarf
|
75 |
+
ACCESSORIES = {1, 3, 9, 10, 16} # Hat, Sunglasses, Left-shoe, Right-shoe, Bag
|
76 |
+
BODY_PARTS = {2, 11, 12, 13, 14, 15} # Hair, Face, Left-leg, Right-leg, Left-arm, Right-arm
|
77 |
+
|
78 |
+
|
79 |
+
load_dotenv()
|
80 |
+
|
81 |
+
# Cloudinary Configuration
|
82 |
+
CLOUDINARY_CONFIG = {
|
83 |
+
"cloud_name": os.getenv("CLOUDINARY_CLOUD_NAME"),
|
84 |
+
"api_key": os.getenv("CLOUDINARY_API_KEY"),
|
85 |
+
"api_secret": os.getenv("CLOUDINARY_API_SECRET")
|
86 |
+
}
|
87 |
+
|
88 |
+
# Configure Cloudinary
|
89 |
+
cloudinary.config(
|
90 |
+
cloud_name=CLOUDINARY_CONFIG["cloud_name"],
|
91 |
+
api_key=CLOUDINARY_CONFIG["api_key"],
|
92 |
+
api_secret=CLOUDINARY_CONFIG["api_secret"],
|
93 |
+
secure=True
|
94 |
+
)
|
95 |
+
|
96 |
+
async def load_model():
|
97 |
+
"""Load the segmentation model asynchronously"""
|
98 |
+
if "model" not in model_cache:
|
99 |
+
logger.info("Loading SegFormer model...")
|
100 |
+
try:
|
101 |
+
processor = SegformerImageProcessor.from_pretrained("mattmdjaga/segformer_b2_clothes")
|
102 |
+
model = AutoModelForSemanticSegmentation.from_pretrained("mattmdjaga/segformer_b2_clothes")
|
103 |
+
model_cache["processor"] = processor
|
104 |
+
model_cache["model"] = model
|
105 |
+
logger.info("Model loaded successfully!")
|
106 |
+
except Exception as e:
|
107 |
+
logger.error(f"Model loading failed: {e}")
|
108 |
+
raise HTTPException(status_code=500, detail=f"Model loading failed: {e}")
|
109 |
+
|
110 |
+
return model_cache["processor"], model_cache["model"]
|
111 |
+
|
112 |
+
def download_image(url: str) -> Image.Image:
|
113 |
+
"""Download image from URL"""
|
114 |
+
try:
|
115 |
+
response = requests.get(str(url), timeout=30)
|
116 |
+
response.raise_for_status()
|
117 |
+
|
118 |
+
image = Image.open(io.BytesIO(response.content))
|
119 |
+
if image.mode != 'RGB':
|
120 |
+
image = image.convert('RGB')
|
121 |
+
|
122 |
+
|
123 |
+
print("Image downloaaded succcessfully:",url)
|
124 |
+
|
125 |
+
return image
|
126 |
+
except Exception as e:
|
127 |
+
raise HTTPException(status_code=400, detail=f"Failed to download image: {e}")
|
128 |
+
|
129 |
+
def enhance_image_quality(image):
|
130 |
+
"""Enhance image quality for high-quality output"""
|
131 |
+
if isinstance(image, np.ndarray):
|
132 |
+
if len(image.shape) == 3 and image.shape[2] == 3:
|
133 |
+
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
134 |
+
pil_image = Image.fromarray(image)
|
135 |
+
else:
|
136 |
+
pil_image = image
|
137 |
+
|
138 |
+
# High quality enhancement
|
139 |
+
pil_image = pil_image.filter(ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3))
|
140 |
+
enhancer = ImageEnhance.Sharpness(pil_image)
|
141 |
+
pil_image = enhancer.enhance(1.3)
|
142 |
+
enhancer = ImageEnhance.Contrast(pil_image)
|
143 |
+
pil_image = enhancer.enhance(1.15)
|
144 |
+
enhancer = ImageEnhance.Color(pil_image)
|
145 |
+
pil_image = enhancer.enhance(1.1)
|
146 |
+
|
147 |
+
return cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
|
148 |
+
|
149 |
+
def get_category_folder(class_id):
|
150 |
+
"""Get appropriate folder for class"""
|
151 |
+
if class_id in CLOTHING_ITEMS:
|
152 |
+
return "clothing"
|
153 |
+
elif class_id in ACCESSORIES:
|
154 |
+
return "accessories"
|
155 |
+
elif class_id in BODY_PARTS:
|
156 |
+
return "body_parts"
|
157 |
+
else:
|
158 |
+
return "clothing" # default
|
159 |
+
|
160 |
+
def upload_to_cloudinary(file_path: str, public_id: str, folder: str = "fashion_segments") -> Dict:
|
161 |
+
"""Upload file to Cloudinary and return response with URLs"""
|
162 |
+
try:
|
163 |
+
# Upload to Cloudinary
|
164 |
+
upload_result = cloudinary.uploader.upload(
|
165 |
+
file_path,
|
166 |
+
public_id=f"{folder}/{public_id}",
|
167 |
+
folder=folder,
|
168 |
+
resource_type="image",
|
169 |
+
format="png",
|
170 |
+
quality="auto:best",
|
171 |
+
fetch_format="auto"
|
172 |
+
)
|
173 |
+
|
174 |
+
# Generate optimized URL
|
175 |
+
optimized_url, _ = cloudinary_url(
|
176 |
+
upload_result['public_id'],
|
177 |
+
format="png",
|
178 |
+
quality="auto:best",
|
179 |
+
fetch_format="auto"
|
180 |
+
)
|
181 |
+
|
182 |
+
return {
|
183 |
+
'url': upload_result.get('secure_url', upload_result.get('url')),
|
184 |
+
'optimized_url': optimized_url,
|
185 |
+
'public_id': upload_result['public_id'],
|
186 |
+
'version': upload_result.get('version'),
|
187 |
+
'format': upload_result.get('format'),
|
188 |
+
'width': upload_result.get('width'),
|
189 |
+
'height': upload_result.get('height'),
|
190 |
+
'bytes': upload_result.get('bytes')
|
191 |
+
}
|
192 |
+
|
193 |
+
except Exception as e:
|
194 |
+
logger.error(f"Cloudinary upload failed: {e}")
|
195 |
+
raise HTTPException(status_code=500, detail=f"Upload failed: {e}")
|
196 |
+
|
197 |
+
def process_segmentation(image: Image.Image, processor, model, settings: Dict) -> tuple:
|
198 |
+
"""Process image segmentation"""
|
199 |
+
# Process with model
|
200 |
+
inputs = processor(images=image, return_tensors="pt")
|
201 |
+
outputs = model(**inputs)
|
202 |
+
logits = outputs.logits.cpu()
|
203 |
+
|
204 |
+
# Resize to original image size
|
205 |
+
upsampled_logits = nn.functional.interpolate(
|
206 |
+
logits,
|
207 |
+
size=image.size[::-1], # height, width
|
208 |
+
mode="bilinear",
|
209 |
+
align_corners=False,
|
210 |
+
)
|
211 |
+
|
212 |
+
pred_seg = upsampled_logits.argmax(dim=1)[0]
|
213 |
+
|
214 |
+
# Extract bounding boxes
|
215 |
+
unique_classes = torch.unique(pred_seg)
|
216 |
+
segment_data = {}
|
217 |
+
total_pixels = pred_seg.numel()
|
218 |
+
|
219 |
+
for class_id in unique_classes:
|
220 |
+
coords = torch.where(pred_seg == class_id)
|
221 |
+
y_coords = coords[0].numpy()
|
222 |
+
x_coords = coords[1].numpy()
|
223 |
+
|
224 |
+
min_x, max_x = int(x_coords.min()), int(x_coords.max())
|
225 |
+
min_y, max_y = int(y_coords.min()), int(y_coords.max())
|
226 |
+
pixel_count = len(x_coords)
|
227 |
+
coverage = (pixel_count / total_pixels) * 100
|
228 |
+
|
229 |
+
segment_data[int(class_id)] = {
|
230 |
+
'bbox': (min_x, min_y, max_x, max_y),
|
231 |
+
'pixel_count': pixel_count,
|
232 |
+
'coverage_percent': coverage
|
233 |
+
}
|
234 |
+
|
235 |
+
return pred_seg, segment_data
|
236 |
+
|
237 |
+
def extract_segments(image: Image.Image, pred_seg, segment_data: Dict, settings: Dict) -> List[Dict]:
|
238 |
+
"""Extract individual segments and upload to Cloudinary"""
|
239 |
+
image_np = np.array(image)
|
240 |
+
image_bgr = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)
|
241 |
+
label_map = pred_seg.numpy().astype(np.uint8)
|
242 |
+
h, w = label_map.shape
|
243 |
+
|
244 |
+
extracted_segments = []
|
245 |
+
padding = settings.get("padding", 15)
|
246 |
+
|
247 |
+
# Create temporary directory for processing
|
248 |
+
temp_dir = tempfile.mkdtemp()
|
249 |
+
session_id = str(uuid.uuid4())[:8] # Shorter session ID
|
250 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
251 |
+
|
252 |
+
try:
|
253 |
+
for class_id, info in segment_data.items():
|
254 |
+
if class_id == 0: # Skip background
|
255 |
+
continue
|
256 |
+
|
257 |
+
x1, y1, x2, y2 = info['bbox']
|
258 |
+
|
259 |
+
# Apply padding
|
260 |
+
x1 = max(0, x1 - padding)
|
261 |
+
y1 = max(0, y1 - padding)
|
262 |
+
x2 = min(w - 1, x2 + padding)
|
263 |
+
y2 = min(h - 1, y2 + padding)
|
264 |
+
|
265 |
+
# Enhanced mask processing
|
266 |
+
mask = (label_map == class_id).astype(np.uint8)
|
267 |
+
mask_filled = ndimage.binary_fill_holes(mask).astype(np.uint8)
|
268 |
+
|
269 |
+
# Adaptive kernel size
|
270 |
+
segment_area = np.sum(mask_filled)
|
271 |
+
kernel_size = max(3, min(7, int(np.sqrt(segment_area) / 100)))
|
272 |
+
kernel = np.ones((kernel_size, kernel_size), np.uint8)
|
273 |
+
|
274 |
+
mask_cleaned = cv2.morphologyEx(mask_filled, cv2.MORPH_CLOSE, kernel, iterations=2)
|
275 |
+
mask_cleaned = cv2.morphologyEx(mask_cleaned, cv2.MORPH_OPEN, kernel, iterations=1)
|
276 |
+
|
277 |
+
# Smooth edges
|
278 |
+
mask_smooth = cv2.GaussianBlur(mask_cleaned.astype(np.float32), (3, 3), 1.0)
|
279 |
+
|
280 |
+
# Crop
|
281 |
+
cropped_mask_smooth = mask_smooth[y1:y2+1, x1:x2+1]
|
282 |
+
cropped_image = image_bgr[y1:y2+1, x1:x2+1]
|
283 |
+
|
284 |
+
# Create white background with grey outline
|
285 |
+
background = np.full(cropped_image.shape, 248, dtype=np.uint8)
|
286 |
+
mask_uint8 = (cropped_mask_smooth * 255).astype(np.uint8)
|
287 |
+
contours, _ = cv2.findContours(mask_uint8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
288 |
+
|
289 |
+
outline_mask = np.zeros_like(mask_uint8)
|
290 |
+
cv2.drawContours(outline_mask, contours, -1, 255, thickness=2)
|
291 |
+
kernel = np.ones((3,3), np.uint8)
|
292 |
+
outline_mask = cv2.dilate(outline_mask, kernel, iterations=1)
|
293 |
+
|
294 |
+
mask_3d = np.stack([cropped_mask_smooth] * 3, axis=2)
|
295 |
+
outline_3d = np.stack([outline_mask/255.0] * 3, axis=2)
|
296 |
+
grey_color = np.full(cropped_image.shape, 128, dtype=np.uint8)
|
297 |
+
|
298 |
+
# Composite image
|
299 |
+
final_image = (cropped_image * mask_3d +
|
300 |
+
grey_color * outline_3d * (1 - mask_3d) +
|
301 |
+
background * (1 - mask_3d) * (1 - outline_3d)).astype(np.uint8)
|
302 |
+
|
303 |
+
# Enhance quality
|
304 |
+
final_image = enhance_image_quality(final_image)
|
305 |
+
|
306 |
+
# Save temporarily
|
307 |
+
class_name = SEGFORMER_LABELS.get(class_id, f"Class_{class_id}")
|
308 |
+
category_folder = get_category_folder(class_id)
|
309 |
+
filename = f"{class_id:02d}_{class_name.replace(' ', '_')}_{info['pixel_count']}px.png"
|
310 |
+
|
311 |
+
temp_filepath = os.path.join(temp_dir, filename)
|
312 |
+
cv2.imwrite(temp_filepath, final_image)
|
313 |
+
|
314 |
+
# Create public_id for Cloudinary
|
315 |
+
public_id = f"{timestamp}_{session_id}_{category_folder}_{class_id:02d}_{class_name.replace(' ', '_')}"
|
316 |
+
|
317 |
+
# Upload to Cloudinary
|
318 |
+
cloudinary_result = upload_to_cloudinary(
|
319 |
+
temp_filepath,
|
320 |
+
public_id,
|
321 |
+
folder=f"fashion_segments/{category_folder}"
|
322 |
+
)
|
323 |
+
|
324 |
+
extracted_segments.append({
|
325 |
+
'class_id': class_id,
|
326 |
+
'class_name': class_name,
|
327 |
+
'filename': filename,
|
328 |
+
'category': category_folder,
|
329 |
+
'pixel_count': info['pixel_count'],
|
330 |
+
'coverage_percent': info['coverage_percent'],
|
331 |
+
'cloudinary_url': cloudinary_result['optimized_url'],
|
332 |
+
'public_id': cloudinary_result['public_id']
|
333 |
+
})
|
334 |
+
|
335 |
+
logger.info(f"Extracted and uploaded: {class_name} ({info['pixel_count']:,} pixels, {info['coverage_percent']:.1f}% coverage)")
|
336 |
+
|
337 |
+
finally:
|
338 |
+
# Cleanup temporary directory
|
339 |
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
340 |
+
|
341 |
+
return extracted_segments
|
342 |
+
|
343 |
+
@app.on_event("startup")
|
344 |
+
async def startup_event():
|
345 |
+
"""Load model on startup"""
|
346 |
+
await load_model()
|
347 |
+
|
348 |
+
@app.get("/")
|
349 |
+
async def root():
|
350 |
+
return {"message": "Fashion Segmentation API with Cloudinary is running!", "version": "1.0.0"}
|
351 |
+
|
352 |
+
@app.get("/health")
|
353 |
+
async def health_check():
|
354 |
+
return {
|
355 |
+
"status": "healthy",
|
356 |
+
"model_loaded": "model" in model_cache,
|
357 |
+
"cloudinary_configured": bool(CLOUDINARY_CONFIG["cloud_name"])
|
358 |
+
}
|
359 |
+
|
360 |
+
@app.post("/segment", response_model=SegmentationResponse)
|
361 |
+
async def segment_fashion_items(request: SegmentationRequest):
|
362 |
+
"""
|
363 |
+
Segment fashion items from an image URL and return Cloudinary URLs for extracted segments
|
364 |
+
"""
|
365 |
+
start_time = datetime.now()
|
366 |
+
|
367 |
+
try:
|
368 |
+
# Load model
|
369 |
+
processor, model = await load_model()
|
370 |
+
|
371 |
+
# Download image
|
372 |
+
logger.info(f"Downloading image from: {request.image_url}")
|
373 |
+
image = download_image(request.image_url)
|
374 |
+
original_size = image.size
|
375 |
+
|
376 |
+
# Process segmentation in thread pool
|
377 |
+
loop = asyncio.get_event_loop()
|
378 |
+
pred_seg, segment_data = await loop.run_in_executor(
|
379 |
+
executor, process_segmentation, image, processor, model, request.settings
|
380 |
+
)
|
381 |
+
|
382 |
+
# Extract segments and upload to Cloudinary
|
383 |
+
extracted_segments = await loop.run_in_executor(
|
384 |
+
executor, extract_segments, image, pred_seg, segment_data, request.settings
|
385 |
+
)
|
386 |
+
|
387 |
+
# Calculate processing time
|
388 |
+
end_time = datetime.now()
|
389 |
+
processing_time = (end_time - start_time).total_seconds()
|
390 |
+
|
391 |
+
# Prepare response
|
392 |
+
segments = [SegmentInfo(**segment) for segment in extracted_segments]
|
393 |
+
|
394 |
+
metadata = {
|
395 |
+
'processing_time': processing_time,
|
396 |
+
'image_size': original_size,
|
397 |
+
'total_segments': len(segments),
|
398 |
+
'settings': request.settings,
|
399 |
+
'timestamp': datetime.now().isoformat(),
|
400 |
+
'storage_provider': 'cloudinary'
|
401 |
+
}
|
402 |
+
|
403 |
+
logger.info(f"Processing complete: {len(segments)} segments extracted and uploaded in {processing_time:.2f}s")
|
404 |
+
|
405 |
+
return SegmentationResponse(
|
406 |
+
success=True,
|
407 |
+
processing_time=processing_time,
|
408 |
+
total_segments=len(segments),
|
409 |
+
segments=segments,
|
410 |
+
metadata=metadata
|
411 |
+
)
|
412 |
+
|
413 |
+
except Exception as e:
|
414 |
+
logger.error(f"Processing failed: {e}")
|
415 |
+
return SegmentationResponse(
|
416 |
+
success=False,
|
417 |
+
processing_time=(datetime.now() - start_time).total_seconds(),
|
418 |
+
total_segments=0,
|
419 |
+
segments=[],
|
420 |
+
metadata={"error": str(e), "storage_provider": "cloudinary"}
|
421 |
+
)
|
422 |
+
|
423 |
+
@app.post("/segment/batch")
|
424 |
+
async def segment_multiple_images(image_urls: List[HttpUrl]):
|
425 |
+
"""
|
426 |
+
Process multiple images in batch
|
427 |
+
"""
|
428 |
+
results = []
|
429 |
+
|
430 |
+
for url in image_urls:
|
431 |
+
try:
|
432 |
+
request = SegmentationRequest(image_url=url)
|
433 |
+
result = await segment_fashion_items(request)
|
434 |
+
results.append({"url": str(url), "result": result})
|
435 |
+
except Exception as e:
|
436 |
+
results.append({"url": str(url), "error": str(e)})
|
437 |
+
|
438 |
+
return {"batch_results": results}
|
439 |
+
|
440 |
+
@app.delete("/segment/{public_id}")
|
441 |
+
async def delete_segment(public_id: str):
|
442 |
+
"""
|
443 |
+
Delete a segment from Cloudinary by public_id
|
444 |
+
"""
|
445 |
+
try:
|
446 |
+
result = cloudinary.uploader.destroy(public_id)
|
447 |
+
return {"success": True, "result": result}
|
448 |
+
except Exception as e:
|
449 |
+
logger.error(f"Failed to delete {public_id}: {e}")
|
450 |
+
raise HTTPException(status_code=500, detail=f"Deletion failed: {e}")
|
451 |
+
|
452 |
+
@app.get("/segment/transform/{public_id}")
|
453 |
+
async def get_transformed_url(
|
454 |
+
public_id: str,
|
455 |
+
width: Optional[int] = None,
|
456 |
+
height: Optional[int] = None,
|
457 |
+
quality: Optional[str] = "auto",
|
458 |
+
format: Optional[str] = "auto"
|
459 |
+
):
|
460 |
+
"""
|
461 |
+
Get a transformed URL for a segment with specified dimensions and quality
|
462 |
+
"""
|
463 |
+
try:
|
464 |
+
transformations = {
|
465 |
+
"quality": quality,
|
466 |
+
"fetch_format": format
|
467 |
+
}
|
468 |
+
|
469 |
+
if width:
|
470 |
+
transformations["width"] = width
|
471 |
+
if height:
|
472 |
+
transformations["height"] = height
|
473 |
+
|
474 |
+
url, options = cloudinary_url(public_id, **transformations)
|
475 |
+
|
476 |
+
return {
|
477 |
+
"original_public_id": public_id,
|
478 |
+
"transformed_url": url,
|
479 |
+
"transformations": transformations
|
480 |
+
}
|
481 |
+
except Exception as e:
|
482 |
+
logger.error(f"Failed to generate transformed URL: {e}")
|
483 |
+
raise HTTPException(status_code=500, detail=f"URL generation failed: {e}")
|
484 |
+
|
485 |
+
if __name__ == "__main__":
|
486 |
+
import uvicorn
|
487 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
docker-compose.yml
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
services:
|
2 |
+
fastapi:
|
3 |
+
build:
|
4 |
+
context: .
|
5 |
+
dockerfile: Dockerfile.fastapi
|
6 |
+
container_name: fashion-analyzer-api
|
7 |
+
restart: unless-stopped
|
8 |
+
ports:
|
9 |
+
|
10 |
+
"7860:7860"
|
11 |
+
networks:
|
12 |
+
dress-segmentation
|
13 |
+
volumes:
|
14 |
+
./logs:/app/logs
|
15 |
+
|
16 |
+
networks:
|
17 |
+
dress-segmentation:
|
18 |
+
driver: bridge
|
environment.env
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
CLOUDINARY_CLOUD_NAME=dyvuvklpk
|
2 |
+
CLOUDINARY_API_KEY=526511578726322
|
3 |
+
CLOUDINARY_API_SECRET=vNX68D7DcmIS6qUupIJgWFRK6Cc
|
requirements.txt
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
fastapi
|
2 |
+
uvicorn[standard]
|
3 |
+
transformers
|
4 |
+
torch
|
5 |
+
torchvision
|
6 |
+
pillow
|
7 |
+
opencv-python
|
8 |
+
numpy
|
9 |
+
scipy
|
10 |
+
requests
|
11 |
+
pydantic
|
12 |
+
cloudinary
|
13 |
+
python-multipart
|
start.sh
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/bin/bash
|
2 |
+
|
3 |
+
Debug information,
|
4 |
+
echo "=== Debug Information ==="
|
5 |
+
echo "Current user: $(whoami)"
|
6 |
+
echo "User ID: $(id)"
|
7 |
+
echo "Current directory: $(pwd)"
|
8 |
+
echo "Home directory: $HOME"
|
9 |
+
echo "PATH: $PATH"
|
10 |
+
echo "========================="
|
11 |
+
|
12 |
+
echo "Starting Fashion Analyzer with transformers-based AI models..."
|
13 |
+
|
14 |
+
Start FastAPI on port 7860 (HF Spaces requirement),
|
15 |
+
echo "Starting FastAPI server on port 7860..."
|
16 |
+
cd /app
|
17 |
+
python3 app.py
|