import sys import locale # Force UTF-8 BEFORE any other import try: locale.setlocale(locale.LC_ALL, "C.UTF-8") except Exception: pass try: sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") except Exception: pass import numpy as np import cv2 import base64 import logging from fastapi import FastAPI, File, UploadFile, HTTPException, Query, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse, Response, HTMLResponse from pydantic import BaseModel logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) from face_detection import crop_face, detect_face, draw_face_box, _crop_region from predict import detect_acne from severity import calculate_severity from treatment import get_consultation from rules import get_expert_recommendation app = FastAPI(title="Acne Detection API", version="1.0.0") @app.exception_handler(Exception) async def global_exception_handler(request, exc): logger.error(f"Unhandled exception: {exc}") return JSONResponse( status_code=500, content={"detail": "Terjadi kesalahan server internal"} ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) @app.get("/favicon.ico") async def favicon(): return Response(status_code=204) # -- HTML UI --------------------------------------------------------- @app.get("/", response_class=HTMLResponse) def index(): return """ Acne Detection - Interactive Test

Memproses...

Acne Detection - Interactive Test

Hy-opus Skripsi - YOLOv26s - Geser slider untuk melihat perubahan deteksi secara langsung

Upload
[IMG]

Klik atau drag & drop gambar wajah

Confidence Threshold
Min. confidence 0.05
Default: 0.05
IoU Threshold (NMS)
IoU 0.45
Default: 0.45
Face Zoom
Padding (zoom) 0.20
Default: 0.20
Face Shift
Geser ke atas 0.15
Default: 0.15 (frontal)
Face Zoom - Side Profile
Padding (zoom) 0.30
Default: 0.30 (side profile, zoom out)
Face Shift - Side Profile
Geser ke atas 0.15
Default: 0.15 (side profile)
Status
Upload gambar untuk memulai tes
Original
-
Face Detection
-
Acne Detections
-
""" # -- helpers ---------------------------------------------------------- async def _run_prediction(image_array, conf_threshold, iou_threshold, padding, shift_up=0.15, padding_side=0.30, shift_up_side=0.15, skin_type="berminyak"): face_result = detect_face(image_array) face_crop = crop_face(image_array, padding=padding, shift_up=shift_up, padding_side=padding_side, shift_up_side=shift_up_side) target = face_crop if face_crop is not None else image_array result = detect_acne(target, conf_threshold=conf_threshold, iou_threshold=iou_threshold) severity = calculate_severity(result["summary"]) expert = get_expert_recommendation( detected_classes=result["detected_classes"], summary=result["summary"], skin_type=skin_type, severity_level=severity["level"], ) if face_result is not None: x, y, fw, fh = face_result["bounds"] orientation = face_result.get("orientation", "frontal") p = padding_side if orientation == "side_profile" else padding s = shift_up_side if orientation == "side_profile" else shift_up face_boxed = draw_face_box(image_array, face_result) face_crop_boxed = _crop_region(face_boxed, x, y, fw, fh, p, shift_up=s) _, buffer = cv2.imencode('.jpg', face_crop_boxed, [cv2.IMWRITE_JPEG_QUALITY, 85]) face_image_b64 = base64.b64encode(buffer).decode('utf-8') else: face_image_b64 = None face_info = None if face_result is not None: x, y, w, h = face_result["bounds"] face_info = { "detected": True, "method": face_result["method"], "score": face_result["score"], "bounds": {"x": x, "y": y, "w": w, "h": h}, "orientation": face_result.get("orientation", "frontal"), } else: face_info = {"detected": False, "method": None, "score": None, "bounds": None, "orientation": None} return { "status": "success", "face_detected": face_crop is not image_array, "face_info": face_info, "face_image": face_image_b64, "severity": severity, "expert_rule": expert["expert_rule"], "recommendation": expert["recommendation"], "daily_skincare": expert["daily_skincare"], "nonmedikamentosa": expert["nonmedikamentosa"], "maintenance": expert["maintenance"], "active_ingredient_info": expert["active_ingredient_info"], "nodul_alert": expert.get("nodul_alert"), "consultation": get_consultation(severity["level"]), **result, } # -- API: upload file (multipart) ------------------------------------ @app.post("/api/predict") async def predict( image: UploadFile = File(..., description="Gambar wajah untuk dideteksi"), conf_threshold: float = Query(0.05, ge=0.05, le=1.0), iou_threshold: float = Query(0.45, ge=0.0, le=1.0), padding: float = Query(0.2, ge=0.0, le=0.5), shift_up: float = Query(0.15, ge=0.0, le=0.5), padding_side: float = Query(0.30, ge=0.0, le=0.5), shift_up_side: float = Query(0.15, ge=0.0, le=0.5), skin_type: str = Query("berminyak", description="Tipe kulit: berminyak, kering, sensitif, kombinasi"), ): if not image.content_type.startswith("image/"): raise HTTPException(status_code=400, detail="File harus berupa gambar.") raw = await image.read() image_array = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR) if image_array is None: raise HTTPException(status_code=400, detail="Gagal membaca gambar.") return await _run_prediction(image_array, conf_threshold, iou_threshold, padding, shift_up, padding_side, shift_up_side, skin_type) # -- API: JSON body (base64) ----------------------------------------- class PredictRequest(BaseModel): image: str conf_threshold: float = 0.05 iou_threshold: float = 0.45 padding: float = 0.2 shift_up: float = 0.15 padding_side: float = 0.30 shift_up_side: float = 0.15 skin_type: str = "berminyak" @app.post("/api/predict/json") async def predict_json(body: PredictRequest): try: raw = base64.b64decode(body.image) except Exception: raise HTTPException(status_code=400, detail="Gagal decode base64.") image_array = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR) if image_array is None: raise HTTPException(status_code=400, detail="Gagal membaca gambar.") return await _run_prediction(image_array, body.conf_threshold, body.iou_threshold, body.padding, body.shift_up, body.padding_side, body.shift_up_side, body.skin_type) # -- API: return annotated image directly as JPEG ------------------- @app.post("/api/predict/image") async def predict_image( image: UploadFile = File(..., description="Gambar wajah untuk dideteksi"), conf_threshold: float = Query( 0.05, ge=0.05, le=1.0, description="Confidence threshold (0.05-1.0)", ), iou_threshold: float = Query( 0.45, ge=0.0, le=1.0, description="IoU threshold untuk NMS (0.0-1.0)", ), padding: float = Query( 0.3, ge=0.0, le=0.5, description="Face crop padding untuk frontal (0.0-0.5)", ), shift_up: float = Query( 0.15, ge=0.0, le=0.5, description="Shift crop ke atas untuk frontal (0.0-0.5)", ), padding_side: float = Query( 0.30, ge=0.0, le=0.5, description="Face crop padding untuk side profile (0.0-0.5)", ), shift_up_side: float = Query( 0.15, ge=0.0, le=0.5, description="Shift crop ke atas untuk side profile (0.0-0.5)", ), ): """Return gambar annotated langsung sebagai JPEG.""" if not image.content_type.startswith("image/"): raise HTTPException(status_code=400, detail="File harus berupa gambar.") raw = await image.read() image_array = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR) if image_array is None: raise HTTPException(status_code=400, detail="Gagal membaca gambar.") face_crop = crop_face(image_array, padding=padding, shift_up=shift_up, padding_side=padding_side, shift_up_side=shift_up_side) target = face_crop if face_crop is not None else image_array result = detect_acne(target, conf_threshold=conf_threshold, iou_threshold=iou_threshold) img_bytes = base64.b64decode(result["annotated_image"]) return Response(content=img_bytes, media_type="image/jpeg") @app.get("/api/classes") def get_classes(): return { "classes": ["comedone", "nodules", "papules", "pustules"], "model": "YOLOv26s", "input_size": "640x640", }