EyeeSEE / app.py
Nj-1111's picture
Update app.py
8d62536 verified
Raw
History Blame
9.09 kB
"""
Glaucoma CDSS
=============
Gradio UI : /
REST API : /predict (JSON) | /predict/pdf (PDF file) | /health
"""
import os, sys, tempfile, shutil
from pathlib import Path
import cv2
import numpy as np
import gradio as gr
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.responses import JSONResponse, FileResponse
sys.path.insert(0, os.path.dirname(__file__))
from phase3pipeline import Phase3Pipeline
# ── config ────────────────────────────────────────────────────────────────────
REPO_ID = "Nj-1111/EyeeSEE"
EPOCH = None
TOKEN = os.getenv("HF_TOKEN_2") or os.getenv("HF_TOKEN")
# ─────────────────────────────────────────────────────────────────────────────
print(f"Loading model repo={REPO_ID} epoch={'latest' if EPOCH is None else EPOCH}")
pipeline = Phase3Pipeline(
repo_id=REPO_ID, epoch=EPOCH, mc_passes=1,
uncertainty_threshold=0.05, token=TOKEN,
)
print("Model ready.")
# ── shared helpers ────────────────────────────────────────────────────────────
def _load_image(path: str) -> np.ndarray:
img = cv2.imread(path)
if img is None:
raw = np.fromfile(path, dtype=np.uint8)
img = cv2.imdecode(raw, cv2.IMREAD_COLOR)
if img is None:
raise ValueError(f"Cannot decode image: {path}")
return img
def _run_pipeline(path: str) -> dict:
img = _load_image(path)
result = pipeline.run(img)
return result["report"]
def _build_text(r: dict) -> str:
isnt = r["isnt"]
lines = [
"=" * 52,
" GLAUCOMA CDSS β€” CLINICAL REPORT",
"=" * 52,
f" vCDR : {r['vcdr']:.4f}",
f" Risk Level : {r['risk_level']}",
f" Uncertainty : {r['uncertainty']:.6f}",
f" Sanity Check : {'PASSED' if r['sanity_passed'] else 'CORRECTED (auto)'}",
f" ISNT Rule : {'Satisfied' if isnt['rule_satisfied'] else 'Violated'}",
"",
" ISNT Rim Thickness",
f" Inferior : {isnt['inferior']:.2f}",
f" Superior : {isnt['superior']:.2f}",
f" Nasal : {isnt['nasal']:.2f}",
f" Temporal : {isnt['temporal']:.2f}",
"",
" Structural",
f" Disc Area : {r['disc_area_px']:,} px",
f" Cup Area : {r['cup_area_px']:,} px",
f" Cup/Disc : {r['cup_area_px'] / max(r['disc_area_px'], 1) * 100:.1f}%",
f" MC Passes : {pipeline.mc_passes}",
]
if r.get("warnings"):
lines += ["", " Warnings"]
for w in r["warnings"]:
lines.append(f" ! {w}")
lines += [
"", "=" * 52,
" DISCLAIMER: Research prototype. Not a certified medical device.",
" Validate with a qualified ophthalmologist.",
"=" * 52,
]
return "\n".join(lines)
def _build_pdf(text: str) -> str:
from fpdf import FPDF
safe = text.replace("β€”", "-").replace("β€’", "-").replace("β†’", "->")
pdf = FPDF()
pdf.add_page()
pdf.set_auto_page_break(auto=True, margin=15)
pdf.set_font("Courier", size=10)
for line in safe.splitlines():
pdf.multi_cell(0, 6, txt=line)
out = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")
pdf.output(out.name)
return out.name
def _save_upload(upload: UploadFile) -> str:
suffix = Path(upload.filename).suffix or ".jpg"
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
shutil.copyfileobj(upload.file, tmp)
tmp.flush()
return tmp.name
# ── FastAPI app + endpoints ───────────────────────────────────────────────────
app = FastAPI(title="Glaucoma CDSS API", version="1.0")
@app.get("/health")
def health():
"""Check that the model is loaded and ready."""
return {
"status": "ok",
"repo": REPO_ID,
"epoch": "latest" if EPOCH is None else EPOCH,
}
@app.post("/predict")
async def predict(file: UploadFile = File(...)):
"""
Run glaucoma screening on an uploaded fundus image.
Request : multipart/form-data field name = file
Accepts : JPEG, PNG
Response : JSON clinical report
JSON fields
-----------
vcdr float vertical cup-to-disc ratio
risk_level string Healthy | Glaucoma Suspect | High Risk
uncertainty float MC-Dropout variance (lower = more confident)
sanity_passed bool True if cup is naturally inside disc
isnt object inferior/superior/nasal/temporal rim thickness
structural object disc_area_px, cup_area_px, cup_disc_pct
warnings list clinical warning strings
"""
path = _save_upload(file)
try:
r = _run_pipeline(path)
except Exception as e:
raise HTTPException(status_code=422, detail=str(e))
finally:
os.remove(path)
return JSONResponse({
"vcdr": r["vcdr"],
"risk_level": r["risk_level"],
"uncertainty": r["uncertainty"],
"sanity_passed": r["sanity_passed"],
"isnt": {
"inferior": r["isnt"]["inferior"],
"superior": r["isnt"]["superior"],
"nasal": r["isnt"]["nasal"],
"temporal": r["isnt"]["temporal"],
"rule_satisfied": r["isnt"]["rule_satisfied"],
},
"structural": {
"disc_area_px": r["disc_area_px"],
"cup_area_px": r["cup_area_px"],
"cup_disc_pct": round(r["cup_area_px"] / max(r["disc_area_px"], 1) * 100, 1),
},
"warnings": r.get("warnings", []),
})
@app.post("/predict/pdf")
async def predict_pdf(file: UploadFile = File(...)):
"""
Run glaucoma screening and return a downloadable PDF report.
Request : multipart/form-data field name = file
Accepts : JPEG, PNG
Response : application/pdf
"""
path = _save_upload(file)
try:
r = _run_pipeline(path)
text = _build_text(r)
pdf = _build_pdf(text)
except Exception as e:
raise HTTPException(status_code=422, detail=str(e))
finally:
os.remove(path)
return FileResponse(
pdf,
media_type="application/pdf",
filename="glaucoma_report.pdf",
)
# ── Gradio UI (mounted at /) ──────────────────────────────────────────────────
def analyse(file_path):
if file_path is None:
return "No file uploaded.", None, "Please upload a JPEG or PNG fundus image."
try:
r = _run_pipeline(file_path)
text = _build_text(r)
pdf = _build_pdf(text)
return text, pdf, "Analysis completed."
except Exception as e:
return f"Error: {e}", None, f"Analysis failed: {e}"
def set_busy():
return gr.update(interactive=False), "Running analysis, please wait..."
def set_ready():
return gr.update(interactive=True)
with gr.Blocks(title="Glaucoma CDSS") as gradio_ui:
gr.Markdown("## Glaucoma CDSS\nUpload a retinal fundus image to receive a clinical screening report.")
with gr.Row():
file_in = gr.File(
label="Fundus Image (JPEG / PNG)",
file_types=[".jpg", ".jpeg", ".png"],
type="filepath",
)
run_btn = gr.Button("Analyse", variant="primary")
status_box = gr.Textbox(label="Status", value="Awaiting upload.", interactive=False)
report_box = gr.Textbox(label="Clinical Report", lines=28, interactive=False)
pdf_out = gr.File(label="Download PDF Report")
gr.Markdown("""
---
### How to interpret results
| vCDR | Indication |
|------|------------|
| 0.30 – 0.50 | Usually healthy |
| 0.50 – 0.65 | Borderline |
| 0.65 – 0.80 | Glaucoma suspect |
| > 0.80 | High risk |
**ISNT Rule** β€” healthy nerves follow Inferior > Superior > Nasal > Temporal.
Violation suggests neuro-retinal rim thinning.
**Uncertainty** β€” values above 0.05 indicate low model confidence (check image quality).
---
*Research prototype β€” NOT a medical device. All results must be reviewed by a qualified ophthalmologist.*
""")
run_btn.click(
fn=set_busy, inputs=None, outputs=[run_btn, status_box], queue=False,
).then(
fn=analyse, inputs=[file_in], outputs=[report_box, pdf_out, status_box],
).then(
fn=set_ready, inputs=None, outputs=[run_btn], queue=False,
)
app = gr.mount_gradio_app(app, gradio_ui, path="/")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7860)