Spaces:
Running on Zero
Running on Zero
| import base64 | |
| import io | |
| import os | |
| from collections import Counter | |
| import torch | |
| from fastapi import FastAPI, File, HTTPException, UploadFile | |
| from fastapi.responses import JSONResponse | |
| from PIL import Image, ImageDraw, ImageFont | |
| from transformers import RTDetrForObjectDetection, RTDetrImageProcessor | |
| MODEL_ID = os.getenv("MODEL_ID", "./model") | |
| CONFIDENCE_THRESHOLD = float(os.getenv("CONFIDENCE_THRESHOLD", "0.35")) | |
| MAX_IMAGE_MB = int(os.getenv("MAX_IMAGE_MB", "15")) | |
| DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| processor = RTDetrImageProcessor.from_pretrained(MODEL_ID) | |
| model = RTDetrForObjectDetection.from_pretrained(MODEL_ID) | |
| model.to(DEVICE) | |
| model.eval() | |
| app = FastAPI( | |
| title="Ice Cream Counter API", | |
| version="1.0.0", | |
| description="RT-DETR ice cream product detector and counter. No YOLO.", | |
| ) | |
| def health(): | |
| return { | |
| "status": "ok", | |
| "model": MODEL_ID, | |
| "device": str(DEVICE), | |
| "threshold": CONFIDENCE_THRESHOLD, | |
| } | |
| def annotate(image, detections): | |
| image = image.copy() | |
| draw = ImageDraw.Draw(image) | |
| try: | |
| font = ImageFont.truetype("DejaVuSans.ttf", 18) | |
| except Exception: | |
| font = ImageFont.load_default() | |
| for d in detections: | |
| x1, y1, x2, y2 = d["box"] | |
| label = f'{d["class"]} {d["confidence"]:.2f}' | |
| draw.rectangle([x1, y1, x2, y2], outline="red", width=3) | |
| bbox = draw.textbbox((x1, y1), label, font=font) | |
| draw.rectangle(bbox, fill="red") | |
| draw.text((x1, y1), label, fill="white", font=font) | |
| return image | |
| async def predict( | |
| file: UploadFile = File(...), | |
| return_image: bool = True, | |
| ): | |
| if not file.content_type or not file.content_type.startswith("image/"): | |
| raise HTTPException(400, "Upload a JPG, PNG, WEBP, or other image file.") | |
| raw = await file.read() | |
| if len(raw) > MAX_IMAGE_MB * 1024 * 1024: | |
| raise HTTPException( | |
| 413, | |
| f"Image is too large. Maximum is {MAX_IMAGE_MB} MB.", | |
| ) | |
| try: | |
| image = Image.open(io.BytesIO(raw)).convert("RGB") | |
| except Exception as exc: | |
| raise HTTPException(400, f"Could not read image: {exc}") | |
| inputs = processor(images=image, return_tensors="pt") | |
| inputs = { | |
| k: v.to(DEVICE) if torch.is_tensor(v) else v | |
| for k, v in inputs.items() | |
| } | |
| with torch.inference_mode(): | |
| outputs = model(**inputs) | |
| target_sizes = torch.tensor( | |
| [[image.height, image.width]], | |
| device=DEVICE, | |
| ) | |
| result = processor.post_process_object_detection( | |
| outputs, | |
| threshold=CONFIDENCE_THRESHOLD, | |
| target_sizes=target_sizes, | |
| )[0] | |
| detections = [] | |
| counts = Counter() | |
| for score, label, box in zip( | |
| result["scores"], | |
| result["labels"], | |
| result["boxes"], | |
| ): | |
| score_value = float(score.item()) | |
| label_id = int(label.item()) | |
| class_name = model.config.id2label[label_id] | |
| coords = [round(float(x), 2) for x in box.tolist()] | |
| detections.append( | |
| { | |
| "class": class_name, | |
| "confidence": round(score_value, 4), | |
| "box": coords, | |
| } | |
| ) | |
| counts[class_name] += 1 | |
| response = { | |
| "total": len(detections), | |
| "counts": dict(sorted(counts.items())), | |
| "detections": detections, | |
| } | |
| if return_image: | |
| annotated = annotate(image, detections) | |
| buf = io.BytesIO() | |
| annotated.save(buf, format="JPEG", quality=90) | |
| response["annotated_image_base64"] = base64.b64encode( | |
| buf.getvalue() | |
| ).decode("ascii") | |
| return JSONResponse(response) | |