File size: 8,168 Bytes
47542cf 723547d 47542cf 723547d 47542cf b81a2b3 47542cf b81a2b3 47542cf b81a2b3 47542cf b81a2b3 47542cf b81a2b3 47542cf 8af33ff 47542cf 723547d 47542cf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | """
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
# Thêm đường dẫn package
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
# Khởi tạo ứng dụng FastAPI
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"
)
# Cấu hình CORS để Web Frontend (React / Vue / Flutter / Angular) truy cập được
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Lời khuyên y tế lâm sàng theo tiêu chuẩn ICDR 5 mức độ
CLINICAL_ADVICE = {
0: {
"title": "Mắt Bình Thường (No DR)",
"badge_color": "#10b981", # Green
# "advice": "Chưa phát hiện tổn thương võng mạc tiểu đường. Khuyến nghị khám mắt định kỳ 12 tháng/lần và kiểm soát chỉ số đường huyết tốt.",
"urgency": "Bình thường"
},
1: {
"title": "Bệnh Nhẹ (Mild DR)",
"badge_color": "#3b82f6", # Blue
# "advice": "Xuất hiện các vi phình mạch nhỏ. Khuyến nghị tái khám theo dõi chuyên khoa mắt sau 6 - 12 tháng và kiểm soát nghiêm ngặt đường huyết, huyết áp.",
"urgency": "Theo dõi định kỳ"
},
2: {
"title": "Bệnh Trung Bình (Moderate DR)",
"badge_color": "#f59e0b", # Orange/Yellow
# "advice": "Tổn thương xuất huyết/xuất tiết mức độ vừa. Cần thăm khám bác sĩ nhãn khoa trong 3 - 6 tháng để đánh giá hoàng điểm và can thiệp kịp thời.",
"urgency": "Khám chuyên khoa"
},
3: {
"title": "Bệnh Nặng (Severe DR)",
"badge_color": "#ff0000", # Light red
# "advice": "Tổn thương nghiêm trọng ở nhiều góc phần tư võng mạc. CẦN THIẾT chuyển khám chuyên khoa mắt gấp trong 2 - 4 tuần để xét can thiệp Laser/OCT.",
"urgency": "Cần can thiệp sớm"
},
4: {
"title": "Tăng Sinh Nguy Hiểm (Proliferative DR)",
"badge_color": "#5e0101", # Dark red
# "advice": "Tăng sinh tân mạch nguy cơ gây mờ mắt vĩnh viễn hoặc bong võng mạc! CẦN ĐIỀU TRỊ KHẨN CẤP tại trung tâm nhãn khoa chuyên sâu.",
"urgency": "KHẨN CẤP"
}
}
# Biến toàn cục lưu trữ DRPredictor instance
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:
# Đọc dữ liệu ảnh từ request upload
image_bytes = await file.read()
# Chạy dự đoán AI và trả về kết quả JSON
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)
# Generate GradCAM overlay BGR image
gradcam_bgr = generate_gradcam_for_image(pred_instance, image_input=image_bytes)
# Encode to Base64 PNG image
_, 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)
|