File size: 8,147 Bytes
c4b649d
 
 
 
 
 
 
 
 
 
 
 
423faf7
c4b649d
 
 
 
 
 
 
 
 
 
423faf7
c4b649d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10575e1
 
c4b649d
 
 
 
 
423faf7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c4b649d
 
 
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 (ResNet-50)",
    description="Hệ thống AI Chẩn đoán Mức độ Bệnh Võng mạc Tiểu đường (ResNet-50 Baseline)",
    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": "#ef4444", # 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": "#8b5cf6", # Purple
        "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": "ResNet50",
            "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)