| """ |
| main_api.py |
| =========== |
| REST API Web Server khởi chạy mô hình AI chẩn đoán Bệnh Võng mạc Tiểu đường bằng FastAPI. |
| Dành cho người làm Backend / Frontend gọi API suy luận tại endpoint `/api/predict`. |
| """ |
|
|
| import os |
| import sys |
| import base64 |
| import cv2 |
| import numpy as np |
| from fastapi import FastAPI, File, UploadFile, HTTPException, Response |
| from fastapi.middleware.cors import CORSMiddleware |
| from fastapi.responses import JSONResponse |
|
|
| |
| current_dir = os.path.dirname(os.path.abspath(__file__)) |
| if current_dir not in sys.path: |
| sys.path.insert(0, current_dir) |
|
|
| from predictor import DRPredictor |
| from preprocessing import load_image, full_preprocess_pipeline |
| from gradcam_visualizer import generate_gradcam_for_image |
|
|
| |
| app = FastAPI( |
| title="Diabetic Retinopathy Classification API", |
| description="Hệ thống AI Chẩn đoán Mức độ Bệnh Võng mạc Tiểu đường (EfficientNet-B4 + CBAM)", |
| version="1.0.0", |
| docs_url="/docs", |
| redoc_url="/redoc" |
| ) |
|
|
| |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"], |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| |
| CLINICAL_ADVICE = { |
| 0: { |
| "title": "Mắt Bình Thường (No DR)", |
| "badge_color": "#10b981", |
| |
| "urgency": "Bình thường" |
| }, |
| 1: { |
| "title": "Bệnh Nhẹ (Mild DR)", |
| "badge_color": "#3b82f6", |
| |
| "urgency": "Theo dõi định kỳ" |
| }, |
| 2: { |
| "title": "Bệnh Trung Bình (Moderate DR)", |
| "badge_color": "#f59e0b", |
| |
| "urgency": "Khám chuyên khoa" |
| }, |
| 3: { |
| "title": "Bệnh Nặng (Severe DR)", |
| "badge_color": "#ff0000", |
| |
| "urgency": "Cần can thiệp sớm" |
| }, |
| 4: { |
| "title": "Tăng Sinh Nguy Hiểm (Proliferative DR)", |
| "badge_color": "#5e0101", |
| |
| "urgency": "KHẨN CẤP" |
| } |
| } |
|
|
| |
| predictor: DRPredictor = None |
|
|
|
|
| def get_predictor() -> DRPredictor: |
| """Tải lazy DRPredictor nếu chưa được khởi tạo thành công.""" |
| global predictor |
| if predictor is None: |
| print("[INFO] Dang nap mo hinh AI vao RAM/GPU...") |
| try: |
| predictor = DRPredictor() |
| print(f"[SUCCESS] Da nap thanh cong mo hinh AI tren thiet bi: {predictor.device}") |
| except Exception as e: |
| print(f"[ERROR] Loi khoi tao mo hinh AI: {e}") |
| raise e |
| return predictor |
|
|
|
|
| @app.on_event("startup") |
| def startup_event(): |
| try: |
| get_predictor() |
| except Exception as e: |
| print(f"[WARNING] Startup predictor deferred: {e}") |
|
|
|
|
| @app.get("/") |
| def root(): |
| """Endpoint gốc kiểm tra trạng thái dịch vụ.""" |
| return { |
| "message": "AI Diabetic Retinopathy API Service is running.", |
| "docs_url": "/docs", |
| "health_check": "/api/info", |
| "predict_endpoint": "POST /api/predict" |
| } |
|
|
|
|
| @app.get("/api/info") |
| def get_info(): |
| """Lấy thông tin hệ thống và trạng thái mô hình.""" |
| try: |
| pred_instance = get_predictor() |
| return { |
| "status": "online", |
| "model_name": "EfficientNetB4_CBAM", |
| "num_classes": 5, |
| "device": str(pred_instance.device), |
| "classes": pred_instance.class_names |
| } |
| except Exception as e: |
| return { |
| "status": "error_loading_model", |
| "error": str(e) |
| } |
|
|
|
|
| @app.post("/api/predict") |
| async def predict_image(file: UploadFile = File(...)): |
| """ |
| Endpoint chính tiếp nhận ảnh đáy mắt (Fundus Image) và trả về kết quả chẩn đoán JSON. |
| """ |
| try: |
| pred_instance = get_predictor() |
| except Exception as e: |
| raise HTTPException(status_code=500, detail=f"Loi khoi tao mo hinh AI: {str(e)}") |
|
|
| if not file.content_type.startswith("image/"): |
| raise HTTPException(status_code=400, detail="File tải lên không phải là định dạng ảnh hợp lệ.") |
|
|
| try: |
| |
| image_bytes = await file.read() |
|
|
| |
| return pred_instance.predict(image_bytes, use_ben_graham=True) |
|
|
| except Exception as e: |
| raise HTTPException(status_code=500, detail=f"Lỗi trong quá trình xử lý ảnh: {str(e)}") |
|
|
|
|
| @app.post("/api/predict_gradcam") |
| async def predict_image_with_gradcam(file: UploadFile = File(...)): |
| """ |
| Endpoint mở rộng: Chẩn đoán Mức độ DR và trả về bản đồ nhiệt Grad-CAM định dạng Base64. |
| """ |
| try: |
| pred_instance = get_predictor() |
| except Exception as e: |
| raise HTTPException(status_code=500, detail=f"Lỗi khởi tạo mô hình AI: {str(e)}") |
|
|
| if not file.content_type.startswith("image/"): |
| raise HTTPException(status_code=400, detail="File tải lên không phải là định dạng ảnh hợp lệ.") |
|
|
| try: |
| image_bytes = await file.read() |
| prediction = pred_instance.predict(image_bytes, use_ben_graham=True) |
|
|
| |
| gradcam_bgr = generate_gradcam_for_image(pred_instance, image_input=image_bytes) |
|
|
| |
| _, buffer = cv2.imencode('.png', gradcam_bgr) |
| base64_str = base64.b64encode(buffer).decode('utf-8') |
| gradcam_data_url = f"data:image/png;base64,{base64_str}" |
|
|
| prediction["gradcam_image_base64"] = gradcam_data_url |
| prediction["clinical_advice"] = CLINICAL_ADVICE.get(prediction["class_id"], {}) |
| return prediction |
|
|
| except Exception as e: |
| raise HTTPException(status_code=500, detail=f"Lỗi trong quá trình tạo Grad-CAM: {str(e)}") |
|
|
|
|
| @app.post("/api/predict_gradcam_image") |
| async def predict_image_and_return_gradcam_png(file: UploadFile = File(...)): |
| """ |
| Endpoint xem ảnh trực quan: Trả về TRỰC TIẾP file ảnh PNG chứa Grad-CAM heatmap hiển thị ngay trên Swagger UI. |
| """ |
| try: |
| pred_instance = get_predictor() |
| except Exception as e: |
| raise HTTPException(status_code=500, detail=f"Lỗi khởi tạo mô hình AI: {str(e)}") |
|
|
| if not file.content_type.startswith("image/"): |
| raise HTTPException(status_code=400, detail="File tải lên không phải là định dạng ảnh hợp lệ.") |
|
|
| try: |
| image_bytes = await file.read() |
| gradcam_bgr = generate_gradcam_for_image(pred_instance, image_input=image_bytes) |
| _, buffer = cv2.imencode('.png', gradcam_bgr) |
| return Response(content=buffer.tobytes(), media_type="image/png") |
|
|
| except Exception as e: |
| raise HTTPException(status_code=500, detail=f"Lỗi tạo ảnh Grad-CAM: {str(e)}") |
|
|
|
|
| if __name__ == "__main__": |
| import uvicorn |
| uvicorn.run("main_api:app", host="0.0.0.0", port=8000, reload=True) |
|
|