LeukoAI deploy commited on
Commit ·
6e502b4
0
Parent(s):
Deploy a23fa57 from synav/LeukoAI-dev
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +3 -0
- NOTICE +13 -0
- README.md +16 -0
- app/app.py +430 -0
- assets/brand/LeukoAI_icon_512.png +3 -0
- assets/brand/LeukoAI_logo.png +3 -0
- assets/brand/LeukoAI_logo_dark.png +3 -0
- assets/examples/aml/aml_field_01.jpg +3 -0
- assets/examples/aml/aml_field_02.jpg +3 -0
- assets/examples/aml/aml_field_03.jpg +3 -0
- assets/examples/aml/aml_field_04.jpg +3 -0
- assets/examples/aml/aml_field_05.jpg +3 -0
- assets/examples/aml/aml_field_06.jpg +3 -0
- assets/examples/aml/aml_field_07.jpg +3 -0
- assets/examples/aml/aml_field_08.jpg +3 -0
- assets/examples/aml/aml_field_09.jpg +3 -0
- assets/examples/aml/aml_field_10.jpg +3 -0
- assets/examples/cll/cll_field_01.jpg +3 -0
- assets/examples/cll/cll_field_02.jpg +3 -0
- assets/examples/cll/cll_field_03.jpg +3 -0
- assets/examples/cll/cll_field_04.jpg +3 -0
- assets/examples/cll/cll_field_05.jpg +3 -0
- assets/examples/cll/cll_field_06.jpg +3 -0
- assets/examples/cll/cll_field_07.jpg +3 -0
- assets/examples/cll/cll_field_08.jpg +3 -0
- assets/examples/cll/cll_field_09.jpg +3 -0
- assets/examples/cll/cll_field_10.jpg +3 -0
- assets/examples/cml/cml_field_01.jpg +3 -0
- assets/examples/cml/cml_field_02.jpg +3 -0
- assets/examples/cml/cml_field_03.jpg +3 -0
- assets/examples/cml/cml_field_04.jpg +3 -0
- assets/examples/cml/cml_field_05.jpg +3 -0
- assets/examples/cml/cml_field_06.jpg +3 -0
- assets/examples/cml/cml_field_07.jpg +3 -0
- assets/examples/cml/cml_field_08.jpg +3 -0
- assets/examples/cml/cml_field_09.jpg +3 -0
- assets/examples/cml/cml_field_10.jpg +3 -0
- assets/examples/normal/normal_field_01.jpg +3 -0
- assets/examples/normal/normal_field_02.jpg +3 -0
- assets/examples/normal/normal_field_03.jpg +3 -0
- assets/examples/normal/normal_field_04.jpg +3 -0
- assets/examples/normal/normal_field_05.jpg +3 -0
- assets/examples/normal/normal_field_06.jpg +3 -0
- assets/examples/normal/normal_field_07.jpg +3 -0
- assets/examples/normal/normal_field_08.jpg +3 -0
- assets/examples/normal/normal_field_09.jpg +3 -0
- assets/examples/normal/normal_field_10.jpg +3 -0
- assets/logo-mark.svg +5 -0
- leukoai/__init__.py +5 -0
- leukoai/experimental.py +66 -0
.gitattributes
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
NOTICE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LeukoAI
|
| 2 |
+
|
| 3 |
+
The original LeukoAI desktop application (toolv9) and its trained models were developed by
|
| 4 |
+
Sheikh Shakhbout Medical City (SSMC), Abu Dhabi, and Mohamed bin Zayed University of
|
| 5 |
+
Artificial Intelligence (MBZUAI). The application displays "Copyright (c) 2023 MBZUAI".
|
| 6 |
+
|
| 7 |
+
legacy/toolv9_recovered.py was recovered by decompiling toolv9.exe and is kept for reference
|
| 8 |
+
and verification only. The files in weights/ are the original model weights with training
|
| 9 |
+
state removed.
|
| 10 |
+
|
| 11 |
+
This repository is private. Public release or deployment of the code or models requires
|
| 12 |
+
written agreement from the rights holders. Published under the reference:
|
| 13 |
+
Naveed S, et al. J Med Artif Intell. 2025. doi:10.21037/jmai-24-74
|
README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: LeukoAI
|
| 3 |
+
emoji: 🔬
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 6.31.0
|
| 8 |
+
python_version: "3.11"
|
| 9 |
+
app_file: app/app.py
|
| 10 |
+
pinned: false
|
| 11 |
+
license: cc-by-nc-nd-4.0
|
| 12 |
+
short_description: Leukemia prediction from peripheral smear images (research)
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
LeukoAI research tool. Based on Naveed S et al., J Med Artif Intell 2025 (doi:10.21037/jmai-24-74).
|
| 16 |
+
Research use only; not a medical device.
|
app/app.py
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LeukoAI 1.1 web application (research use only).
|
| 2 |
+
|
| 3 |
+
Run locally: python app/app.py then open http://127.0.0.1:7860
|
| 4 |
+
Hugging Face: see docs/DEPLOY.md
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import csv
|
| 9 |
+
import glob
|
| 10 |
+
import html
|
| 11 |
+
import os
|
| 12 |
+
import sys
|
| 13 |
+
import tempfile
|
| 14 |
+
|
| 15 |
+
import gradio as gr
|
| 16 |
+
import torch
|
| 17 |
+
|
| 18 |
+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 19 |
+
sys.path.insert(0, ROOT)
|
| 20 |
+
from leukoai import load_models # noqa: E402
|
| 21 |
+
from leukoai.explain import gradcam, overlay # noqa: E402
|
| 22 |
+
from leukoai.models import DX1, DX2, DX3 # noqa: E402
|
| 23 |
+
from leukoai.pipeline import analyse_paths # noqa: E402
|
| 24 |
+
|
| 25 |
+
torch.set_num_threads(max(1, os.cpu_count() or 1))
|
| 26 |
+
MODELS = load_models(device="cpu")
|
| 27 |
+
MAX_IMAGES = int(os.environ.get("LEUKOAI_MAX_IMAGES", "100"))
|
| 28 |
+
ASSETS = os.path.join(ROOT, "assets")
|
| 29 |
+
|
| 30 |
+
PAPER = "https://doi.org/10.21037/jmai-24-74"
|
| 31 |
+
CODE = "https://github.com/synav/LeukoAI"
|
| 32 |
+
DATA = "https://doi.org/10.5281/zenodo.17814776"
|
| 33 |
+
CARD = "https://github.com/synav/LeukoAI/blob/main/MODEL_CARD.md"
|
| 34 |
+
|
| 35 |
+
EXAMPLES = {
|
| 36 |
+
key: sorted(glob.glob(os.path.join(ASSETS, "examples", key, "*.jpg")))
|
| 37 |
+
for key in ("aml", "cll", "cml", "normal")
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _logo_svg() -> str:
|
| 42 |
+
path = os.path.join(ASSETS, "logo-mark.svg")
|
| 43 |
+
if not os.path.exists(path):
|
| 44 |
+
return ""
|
| 45 |
+
with open(path) as f:
|
| 46 |
+
return f.read().replace("<svg ", '<svg aria-hidden="true" focusable="false" ', 1)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
LOGO_SVG = _logo_svg()
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _access_list():
|
| 53 |
+
"""Logins from the LEUKOAI_ACCESS secret: "user:password;user2:password2".
|
| 54 |
+
|
| 55 |
+
On Hugging Face (SPACE_ID set) the app refuses to start without it, so it can never be
|
| 56 |
+
opened to the public by accident. Locally it runs without a login unless the variable is set.
|
| 57 |
+
"""
|
| 58 |
+
raw = os.environ.get("LEUKOAI_ACCESS", "").strip()
|
| 59 |
+
pairs = [tuple(x.split(":", 1)) for x in raw.split(";") if ":" in x]
|
| 60 |
+
pairs = [(u.strip(), p.strip()) for u, p in pairs if u.strip() and p.strip()]
|
| 61 |
+
if not pairs and os.environ.get("SPACE_ID"):
|
| 62 |
+
raise SystemExit("LEUKOAI_ACCESS secret is not set: refusing to start without a login on Hugging Face.")
|
| 63 |
+
return pairs or None
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
AUTH_MESSAGE = ("LeukoAI is a research tool available to invited researchers, collaborators and journal "
|
| 67 |
+
"reviewers. To request access, contact Dr Syed Naveed (github.com/synav/LeukoAI). "
|
| 68 |
+
"Not a medical device; not for clinical use.")
|
| 69 |
+
FAVICON = os.path.join(ASSETS, "logo-mark.svg")
|
| 70 |
+
|
| 71 |
+
# ---------------------------------------------------------------- page text
|
| 72 |
+
|
| 73 |
+
HEAD = """
|
| 74 |
+
<meta name="description" content="LeukoAI: research tool that predicts leukemia and its subtype from peripheral blood smear images. Based on Naveed S et al., J Med Artif Intell 2025.">
|
| 75 |
+
<meta property="og:title" content="LeukoAI: leukemia prediction from blood smear images">
|
| 76 |
+
<meta property="og:description" content="Upload the microscope fields from one smear and get a case-level result with heatmaps. Research use only.">
|
| 77 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 78 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 79 |
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 80 |
+
<script>
|
| 81 |
+
(function () {
|
| 82 |
+
var u = new URL(window.location.href);
|
| 83 |
+
if (u.searchParams.get('__theme') !== 'light') { u.searchParams.set('__theme', 'light'); window.location.replace(u.toString()); }
|
| 84 |
+
})();
|
| 85 |
+
</script>
|
| 86 |
+
"""
|
| 87 |
+
|
| 88 |
+
HEADER = f"""
|
| 89 |
+
<header class="lk-nav">
|
| 90 |
+
<a class="lk-brand" href="#top" aria-label="LeukoAI home">
|
| 91 |
+
<span class="lk-mark">{LOGO_SVG}</span><span class="lk-wordmark">Leuko<span>AI</span></span>
|
| 92 |
+
</a>
|
| 93 |
+
<nav class="lk-links" aria-label="Main">
|
| 94 |
+
<a href="#tool">Try it</a>
|
| 95 |
+
<a href="#about">About</a>
|
| 96 |
+
<a href="{PAPER}" target="_blank" rel="noopener">Paper</a>
|
| 97 |
+
<a href="{CODE}" target="_blank" rel="noopener">Code</a>
|
| 98 |
+
<a href="{DATA}" target="_blank" rel="noopener">Data</a>
|
| 99 |
+
</nav>
|
| 100 |
+
</header>
|
| 101 |
+
"""
|
| 102 |
+
|
| 103 |
+
HERO = """
|
| 104 |
+
<section class="lk-hero" id="top">
|
| 105 |
+
<p class="lk-eyebrow">LeukoAI 1.1 · Research tool</p>
|
| 106 |
+
<h1>Leukemia prediction from peripheral blood smear images</h1>
|
| 107 |
+
<p class="lk-lead">Upload the microscope fields from one patient's smear. LeukoAI classifies every field and
|
| 108 |
+
combines them into one research prediction: acute or chronic leukemia, the likely subtype (ALL, AML, CLL, CML)
|
| 109 |
+
or no leukemia, with heatmaps showing where the model looked. For research and education only.</p>
|
| 110 |
+
<div class="lk-stats">
|
| 111 |
+
<div><span class="lk-num">417/417</span><span>images give identical results to the published desktop tool</span></div>
|
| 112 |
+
<div><span class="lk-num">0/417</span><span>real smear fields falsely flagged by the image check</span></div>
|
| 113 |
+
<div><span class="lk-num">No scanner</span><span>works on ordinary photos taken at the microscope</span></div>
|
| 114 |
+
</div>
|
| 115 |
+
</section>
|
| 116 |
+
"""
|
| 117 |
+
|
| 118 |
+
STEPS = """
|
| 119 |
+
<section class="lk-steps" aria-label="How it works">
|
| 120 |
+
<div><span class="lk-step">1</span><h3>Capture</h3><p>Photograph 20 or more fields of one smear at 1000× (oil immersion, May-Grünwald-Giemsa stain).</p></div>
|
| 121 |
+
<div><span class="lk-step">2</span><h3>Upload</h3><p>Select all the images at once (Ctrl+A in the folder) and accept the terms of use.</p></div>
|
| 122 |
+
<div><span class="lk-step">3</span><h3>Review</h3><p>Read the research prediction, the warnings and the heatmaps on the most influential fields.</p></div>
|
| 123 |
+
</section>
|
| 124 |
+
"""
|
| 125 |
+
|
| 126 |
+
PLACEHOLDER = """
|
| 127 |
+
<div class="lk-empty">
|
| 128 |
+
<p class="lk-empty-title">Your research prediction will appear here</p>
|
| 129 |
+
<p>Upload one patient's smear fields, or load an example case, then select <b>Analyse case</b>.
|
| 130 |
+
Analysis takes about 1–2 seconds per image.</p>
|
| 131 |
+
</div>
|
| 132 |
+
"""
|
| 133 |
+
|
| 134 |
+
ABOUT = f"""
|
| 135 |
+
<section class="lk-about" id="about">
|
| 136 |
+
<h2>About LeukoAI</h2>
|
| 137 |
+
<div class="lk-grid">
|
| 138 |
+
<div>
|
| 139 |
+
<h3>How it works</h3>
|
| 140 |
+
<p>Three DenseNet-121 models work in sequence: the first screens each field for leukemia, the second
|
| 141 |
+
separates acute from chronic, and the third suggests the subtype. A rule then combines the fields into one
|
| 142 |
+
case-level prediction: leukemia is predicted when more than 3 fields are classed leukemic.</p>
|
| 143 |
+
</div>
|
| 144 |
+
<div>
|
| 145 |
+
<h3>How it was validated</h3>
|
| 146 |
+
<p>The published study used 7,255 images from 162 adult patients. On 64 external patients the
|
| 147 |
+
acute / chronic / no-leukemia result reached an F1 score of 0.94 (accuracy 0.92); the five-class subtype
|
| 148 |
+
result reached a macro-F1 of 0.77. This web version reproduces the original desktop tool exactly.</p>
|
| 149 |
+
</div>
|
| 150 |
+
<div>
|
| 151 |
+
<h3>Limitations</h3>
|
| 152 |
+
<ul>
|
| 153 |
+
<li>Single centre, adult patients, 1000× MGG-stained peripheral smears only.</li>
|
| 154 |
+
<li>ALL vs AML separation is weak (F1 0.36 for ALL); treat the subtype as unconfirmed.</li>
|
| 155 |
+
<li>APML is not reported separately. Scores are not calibrated probabilities.</li>
|
| 156 |
+
</ul>
|
| 157 |
+
</div>
|
| 158 |
+
<div>
|
| 159 |
+
<h3>Privacy</h3>
|
| 160 |
+
<p>Images are processed on Hugging Face servers, which are outside the UAE, only to produce your result.
|
| 161 |
+
They are deleted automatically within about an hour and are never used for training. No usage analytics
|
| 162 |
+
are collected.</p>
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
<div class="lk-cite" id="terms">
|
| 166 |
+
<h3>Terms of use</h3>
|
| 167 |
+
<ul>
|
| 168 |
+
<li>LeukoAI is a research and education tool. It is not a medical device, has not been approved by any
|
| 169 |
+
regulator, and must not be used for diagnosis, screening or treatment decisions.</li>
|
| 170 |
+
<li>Access is limited to invited researchers, collaborators and journal reviewers, and may be withdrawn at any time.</li>
|
| 171 |
+
<li>You must be authorised to use any images you upload, and they must be de-identified: no names, record
|
| 172 |
+
numbers, dates or other identifiers in the image or file name.</li>
|
| 173 |
+
<li>You are responsible for complying with the data-protection and health-data laws that apply to you,
|
| 174 |
+
including rules on transferring health data outside your country.</li>
|
| 175 |
+
<li>The tool is provided as is, without warranty of any kind; predictions may be wrong.</li>
|
| 176 |
+
</ul>
|
| 177 |
+
</div>
|
| 178 |
+
<div class="lk-cite">
|
| 179 |
+
<h3>Cite</h3>
|
| 180 |
+
<p>Naveed S, et al. Novel hierarchical deep learning models predict type of leukemia from whole slide
|
| 181 |
+
microscopic images of peripheral blood. <i>Journal of Medical Artificial Intelligence</i>. 2025.
|
| 182 |
+
<a href="{PAPER}" target="_blank" rel="noopener">doi:10.21037/jmai-24-74</a></p>
|
| 183 |
+
<p class="lk-small">Model card: <a href="{CARD}" target="_blank" rel="noopener">github.com/synav/LeukoAI</a> ·
|
| 184 |
+
Public image dataset: <a href="{DATA}" target="_blank" rel="noopener">Zenodo 10.5281/zenodo.17814776</a></p>
|
| 185 |
+
</div>
|
| 186 |
+
</section>
|
| 187 |
+
"""
|
| 188 |
+
|
| 189 |
+
FOOTER = """
|
| 190 |
+
<footer class="lk-footer">
|
| 191 |
+
<p><b>Research use only.</b> LeukoAI is not a medical device and must not be used for diagnosis or treatment
|
| 192 |
+
decisions. See the <a href="#terms">terms of use</a>.</p>
|
| 193 |
+
<p>Developed by Dr Syed Naveed, FRCPath, and team at Sheikh Shakhbout Medical City, Abu Dhabi, with the
|
| 194 |
+
Mohamed bin Zayed University of Artificial Intelligence.</p>
|
| 195 |
+
</footer>
|
| 196 |
+
"""
|
| 197 |
+
|
| 198 |
+
CSS = """
|
| 199 |
+
:root, .gradio-container { --lk-ink:#14212E; --lk-muted:#55606E; --lk-line:#DCE3EA; --lk-ground:#F4F6F9;
|
| 200 |
+
--lk-brand:#2B6698; --lk-brand-dark:#1D4A70; --lk-card:#FFFFFF; }
|
| 201 |
+
html, body, gradio-app, .gradio-container { background: var(--lk-ground) !important; }
|
| 202 |
+
.gradio-container { max-width: min(1180px, 100vw) !important; margin: 0 auto !important; padding: 0 clamp(10px, 2.5vw, 16px) !important; overflow-x: hidden;
|
| 203 |
+
font-family: 'IBM Plex Sans', system-ui, sans-serif !important; color: var(--lk-ink); }
|
| 204 |
+
.lk-nav { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px;
|
| 205 |
+
padding: 18px 0 14px; border-bottom: 1px solid var(--lk-line); }
|
| 206 |
+
.lk-brand { display:inline-flex; align-items:center; gap:10px; text-decoration:none; }
|
| 207 |
+
.lk-mark { width: clamp(34px, 4.5vw, 42px); height: clamp(34px, 4.5vw, 42px); display:inline-block; }
|
| 208 |
+
.lk-mark svg { width:100%; height:100%; display:block; }
|
| 209 |
+
.lk-wordmark { font-weight:700; font-size: clamp(22px, 2.8vw, 27px); letter-spacing:-0.01em; color:var(--lk-ink); }
|
| 210 |
+
.lk-wordmark span { color: var(--lk-brand); }
|
| 211 |
+
.lk-links { display:flex; flex-wrap:wrap; gap: 4px 22px; }
|
| 212 |
+
.lk-links a { color: var(--lk-ink); text-decoration:none; font-weight:500; font-size:15px; padding:8px 0; }
|
| 213 |
+
.lk-links a:hover { color: var(--lk-brand); }
|
| 214 |
+
.lk-hero { padding: clamp(28px, 5vw, 44px) 0 clamp(20px, 3vw, 28px); }
|
| 215 |
+
.lk-eyebrow { font-family:'IBM Plex Mono', monospace; font-size:13px; letter-spacing:.08em; text-transform:uppercase;
|
| 216 |
+
color: var(--lk-brand); font-weight:600; margin:0 0 12px; }
|
| 217 |
+
.lk-hero h1 { font-size: clamp(30px, 4.4vw, 48px); line-height:1.1; letter-spacing:-.02em; margin:0 0 16px;
|
| 218 |
+
font-weight:700; color:var(--lk-ink); max-width: 860px; }
|
| 219 |
+
.lk-lead { font-size: clamp(16px, 2vw, 18px); line-height:1.55; color:var(--lk-muted); max-width: 760px; margin:0 0 28px; }
|
| 220 |
+
.lk-stats { display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:16px;
|
| 221 |
+
border-top: 2px solid var(--lk-ink); padding-top: 20px; }
|
| 222 |
+
.lk-stats div { display:flex; flex-direction:column; gap:6px; }
|
| 223 |
+
.lk-stats span { font-size:15px; line-height:1.45; color:var(--lk-muted); }
|
| 224 |
+
.lk-stats .lk-num { font-family:'IBM Plex Mono', monospace; font-size:28px; font-weight:600; color:var(--lk-brand); }
|
| 225 |
+
.lk-steps { display:grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap:16px; margin: 8px 0 28px; }
|
| 226 |
+
.lk-steps > div { background: var(--lk-card); border:1px solid var(--lk-line); border-radius:10px; padding:18px 20px; }
|
| 227 |
+
.lk-steps h3 { margin: 10px 0 6px; font-size:17px; color:var(--lk-ink); }
|
| 228 |
+
.lk-steps p { margin:0; color:var(--lk-muted); font-size:15px; line-height:1.5; }
|
| 229 |
+
.lk-step { display:inline-flex; align-items:center; justify-content:center; width:30px; height:30px; border-radius:50%;
|
| 230 |
+
background: var(--lk-brand); color:#fff; font-weight:600; font-size:15px; }
|
| 231 |
+
#tool { gap: 20px; scroll-margin-top: 16px; }
|
| 232 |
+
.lk-panel { background: var(--lk-card) !important; border:1px solid var(--lk-line) !important; border-radius:12px !important;
|
| 233 |
+
padding: clamp(12px, 2vw, 18px) !important; max-width: 100% !important; }
|
| 234 |
+
.gradio-container .main, .gradio-container .app { max-width: 100% !important; min-width: 0 !important; }
|
| 235 |
+
#tool { flex-wrap: wrap !important; max-width: 100% !important; }
|
| 236 |
+
#tool > .lk-panel:first-child { flex: 5 1 340px !important; min-width: min(340px, 100%) !important; }
|
| 237 |
+
#tool > .lk-panel:last-child { flex: 7 1 340px !important; min-width: min(340px, 100%) !important; }
|
| 238 |
+
.lk-panel-title { margin:0 0 4px; font-size:18px; font-weight:600; color:var(--lk-ink); }
|
| 239 |
+
.lk-panel-sub { margin:0 0 10px; font-size:14px; color:var(--lk-muted); line-height:1.5; }
|
| 240 |
+
.lk-ex-label { font-size:14px; color:var(--lk-muted); margin: 6px 0 0; }
|
| 241 |
+
.lk-empty { border: 2px dashed var(--lk-line); border-radius:10px; padding: 36px 24px; text-align:center; color:var(--lk-muted); }
|
| 242 |
+
.lk-empty-title { font-size:18px; font-weight:600; color:var(--lk-ink); margin:0 0 6px; }
|
| 243 |
+
.lk-result { border-radius:10px; padding:18px 20px; margin-bottom:12px; border:2px solid; background:#fff; }
|
| 244 |
+
.lk-result .lk-label { font-family:'IBM Plex Mono', monospace; font-size:12px; letter-spacing:.06em; text-transform:uppercase; color:var(--lk-muted); }
|
| 245 |
+
.lk-result .lk-dx { font-size:30px; font-weight:700; margin:4px 0; letter-spacing:-.01em; }
|
| 246 |
+
.lk-result .lk-sub { font-size:18px; color:var(--lk-ink); }
|
| 247 |
+
.lk-result .lk-meta { font-size:14px; color:var(--lk-muted); margin-top:10px; line-height:1.5; }
|
| 248 |
+
.lk-flag { padding:10px 14px; margin:8px 0; border-radius:8px; font-size:14px; line-height:1.5; color:#101828; }
|
| 249 |
+
.lk-about { padding: 40px 0 8px; scroll-margin-top: 16px; }
|
| 250 |
+
.lk-about h2 { font-size: 30px; margin:0 0 20px; color:var(--lk-ink); letter-spacing:-.01em; }
|
| 251 |
+
.lk-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 20px 32px; }
|
| 252 |
+
.lk-about h3 { font-size:17px; margin: 0 0 8px; color:var(--lk-ink); }
|
| 253 |
+
.lk-about p, .lk-about li { font-size:15px; line-height:1.6; color:var(--lk-muted); margin:0 0 8px; }
|
| 254 |
+
.lk-about ul, .lk-cite ul { padding-left: 18px; margin:0; }
|
| 255 |
+
.lk-cite li { font-size:15px; line-height:1.6; color:var(--lk-muted); margin:0 0 6px; }
|
| 256 |
+
.lk-about a, .lk-footer a { color: var(--lk-brand); }
|
| 257 |
+
.lk-cite { margin-top: 20px; padding: 18px 20px; background: var(--lk-card); border:1px solid var(--lk-line); border-radius:10px; }
|
| 258 |
+
.lk-small { font-size:14px !important; }
|
| 259 |
+
.lk-footer { border-top:1px solid var(--lk-line); margin-top: 32px; padding: 20px 0 8px; }
|
| 260 |
+
.lk-footer p { font-size:14px; color:var(--lk-muted); margin: 0 0 8px; line-height:1.5; }
|
| 261 |
+
"""
|
| 262 |
+
|
| 263 |
+
THEME = gr.themes.Base(
|
| 264 |
+
primary_hue=gr.themes.Color(c50="#EEF4FA", c100="#D6E5F2", c200="#AECBE5", c300="#7FAAD3", c400="#4F88BE",
|
| 265 |
+
c500="#2B6698", c600="#245A87", c700="#1D4A70", c800="#173B59", c900="#112C43",
|
| 266 |
+
c950="#0B1E2E"),
|
| 267 |
+
neutral_hue="slate",
|
| 268 |
+
font=[gr.themes.GoogleFont("IBM Plex Sans"), "system-ui", "sans-serif"],
|
| 269 |
+
font_mono=[gr.themes.GoogleFont("IBM Plex Mono"), "monospace"],
|
| 270 |
+
radius_size="md",
|
| 271 |
+
).set(button_primary_background_fill="#2B6698", button_primary_background_fill_hover="#1D4A70",
|
| 272 |
+
button_primary_text_color="#FFFFFF")
|
| 273 |
+
|
| 274 |
+
# ---------------------------------------------------------------- analysis
|
| 275 |
+
|
| 276 |
+
SEV_STYLE = {
|
| 277 |
+
"warning": ("#B42318", "#FEF3F2", "Warning"),
|
| 278 |
+
"caution": ("#93370D", "#FFFAEB", "Caution"),
|
| 279 |
+
"info": ("#175CD3", "#EFF8FF", "Note"),
|
| 280 |
+
}
|
| 281 |
+
DX_COLOUR = {"NO LEUKEMIA": "#027A48", "ACUTE LEUKEMIA": "#B42318", "CHRONIC LEUKEMIA": "#93370D"}
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def _result_html(rep) -> str:
|
| 285 |
+
r = rep.result
|
| 286 |
+
colour = DX_COLOUR[r.three_class]
|
| 287 |
+
title = r.three_class.capitalize().replace("leukemia", "leukemia")
|
| 288 |
+
subtype = "" if r.three_class == "NO LEUKEMIA" else (
|
| 289 |
+
f"<div class='lk-sub'>Likely subtype: <b>{html.escape(rep.subtype_display)}</b></div>")
|
| 290 |
+
votes = " · ".join(f"{k.title() if k == 'NO LEUKEMIA' else k} {v}" for k, v in r.votes.items())
|
| 291 |
+
parts = [
|
| 292 |
+
f"<div class='lk-result' style='border-color:{colour}'>",
|
| 293 |
+
"<div class='lk-label'>Research prediction · not for clinical use</div>",
|
| 294 |
+
f"<div class='lk-dx' style='color:{colour}'>{html.escape(title)}</div>{subtype}",
|
| 295 |
+
f"<div class='lk-meta'>{r.n_images} fields analysed · {r.n_leukemic} classed leukemic "
|
| 296 |
+
f"(needs more than {r.threshold}) · acute {r.n_acute}, chronic {r.n_chronic}<br>Subtype votes: {votes}</div>",
|
| 297 |
+
"<div class='lk-meta'><b>Not a diagnosis.</b> Research output only; it must not be used for patient care. "
|
| 298 |
+
"Confirm every case with standard laboratory work-up.</div>",
|
| 299 |
+
"</div>",
|
| 300 |
+
]
|
| 301 |
+
for fl in sorted(rep.flags, key=lambda f: ["warning", "caution", "info"].index(f.severity)):
|
| 302 |
+
fg, bg, label = SEV_STYLE[fl.severity]
|
| 303 |
+
parts.append(f"<div class='lk-flag' style='background:{bg};border-left:4px solid {fg}'>"
|
| 304 |
+
f"<b style='color:{fg}'>{label}:</b> {html.escape(fl.message)}</div>")
|
| 305 |
+
return "\n".join(parts)
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
def _per_image_rows(rep):
|
| 309 |
+
rows = []
|
| 310 |
+
for im in rep.images:
|
| 311 |
+
q = im.quality or {}
|
| 312 |
+
rows.append([im.name, DX1[im.k1].capitalize(), round(im.p1[1], 3), DX2[im.k2].capitalize(), DX3[im.k3],
|
| 313 |
+
round(max(im.p3), 3), "OK" if q.get("ok", True) else "; ".join(q.get("reasons", []))])
|
| 314 |
+
return rows
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def _write_csv(rep) -> str:
|
| 318 |
+
fd, path = tempfile.mkstemp(prefix="leukoai_", suffix=".csv")
|
| 319 |
+
with os.fdopen(fd, "w", newline="") as f:
|
| 320 |
+
w = csv.writer(f)
|
| 321 |
+
w.writerow(["image"] + [f"m1_{c}" for c in DX1] + [f"m2_{c}" for c in DX2] + [f"m3_{c}" for c in DX3]
|
| 322 |
+
+ ["quality_ok", "quality_reasons"])
|
| 323 |
+
for im in rep.images:
|
| 324 |
+
q = im.quality or {}
|
| 325 |
+
w.writerow([im.name] + [round(x, 5) for x in im.p1 + im.p2 + im.p3]
|
| 326 |
+
+ [q.get("ok", ""), "; ".join(q.get("reasons", []))])
|
| 327 |
+
r = rep.result
|
| 328 |
+
w.writerow([])
|
| 329 |
+
w.writerow(["case_result", r.three_class, "subtype", rep.subtype_display, "threshold", r.threshold])
|
| 330 |
+
for fl in rep.flags:
|
| 331 |
+
w.writerow(["flag", fl.code, fl.severity, fl.message])
|
| 332 |
+
return path
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
def analyse(files, threshold, show_heatmaps, consent):
|
| 336 |
+
if not consent:
|
| 337 |
+
raise gr.Error("Please accept the terms of use (tick the box above the Analyse button).")
|
| 338 |
+
if not files:
|
| 339 |
+
raise gr.Error("Please upload the field images from one patient's smear, or load an example case.")
|
| 340 |
+
paths = [f if isinstance(f, str) else f.name for f in files]
|
| 341 |
+
if len(paths) > MAX_IMAGES:
|
| 342 |
+
raise gr.Error(f"Please upload at most {MAX_IMAGES} images per case.")
|
| 343 |
+
|
| 344 |
+
rep = analyse_paths(paths, MODELS, threshold=int(threshold), mode="1.1")
|
| 345 |
+
by_name = {os.path.basename(p): p for p in paths}
|
| 346 |
+
|
| 347 |
+
gallery = []
|
| 348 |
+
if rep.result.images_of_interest:
|
| 349 |
+
chronic = rep.result.three_class == "CHRONIC LEUKEMIA"
|
| 350 |
+
# Model 2 (acute/chronic) heatmaps localise leukocytes well; Model 1 is usually
|
| 351 |
+
# saturated near 100% on leukemic fields, which makes its gradients uninformative.
|
| 352 |
+
model, cls = MODELS["m2"], (1 if chronic else 0)
|
| 353 |
+
from PIL import Image
|
| 354 |
+
for name in rep.result.images_of_interest:
|
| 355 |
+
img = Image.open(by_name[name]).convert("RGB")
|
| 356 |
+
if show_heatmaps:
|
| 357 |
+
img = overlay(img, gradcam(model, img, cls))
|
| 358 |
+
gallery.append((img, name))
|
| 359 |
+
|
| 360 |
+
headers = ["Image", "Screening", "Leukemia score", "Category", "Subtype vote", "Subtype score", "Image check"]
|
| 361 |
+
return (_result_html(rep), gr.update(value=gallery, visible=bool(gallery)),
|
| 362 |
+
gr.update(visible=True), gr.update(value=_per_image_rows(rep), headers=headers),
|
| 363 |
+
gr.update(value=_write_csv(rep), visible=True))
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
_HIDDEN = (PLACEHOLDER, gr.update(value=[], visible=False), gr.update(visible=False), None,
|
| 367 |
+
gr.update(value=None, visible=False))
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def load_example(key):
|
| 371 |
+
return (EXAMPLES[key], gr.update()) + _HIDDEN
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
def clear():
|
| 375 |
+
return (None, False) + _HIDDEN
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
# ---------------------------------------------------------------- layout
|
| 379 |
+
|
| 380 |
+
with gr.Blocks(title="LeukoAI · Leukemia prediction from blood smears", delete_cache=(1800, 3600),
|
| 381 |
+
analytics_enabled=False) as demo:
|
| 382 |
+
gr.HTML(HEADER)
|
| 383 |
+
gr.HTML(HERO)
|
| 384 |
+
gr.HTML(STEPS)
|
| 385 |
+
with gr.Row(elem_id="tool", equal_height=False):
|
| 386 |
+
with gr.Column(scale=5, min_width=320, elem_classes="lk-panel"):
|
| 387 |
+
gr.HTML("<p class='lk-panel-title'>Upload one patient's smear</p>"
|
| 388 |
+
"<p class='lk-panel-sub'>All fields from a single smear, 1000× MGG. JPEG, PNG or TIFF; up to "
|
| 389 |
+
f"{MAX_IMAGES} images.</p>")
|
| 390 |
+
files = gr.File(label="Field images", file_count="multiple",
|
| 391 |
+
file_types=["image", ".tif", ".tiff"], height=220)
|
| 392 |
+
gr.HTML("<p class='lk-ex-label'>No images to hand? Load a public example case "
|
| 393 |
+
"(from the LeukoAI Zenodo dataset):</p>")
|
| 394 |
+
with gr.Row():
|
| 395 |
+
ex_buttons = {k: gr.Button(label, size="sm", variant="secondary", min_width=70)
|
| 396 |
+
for k, label in (("aml", "AML"), ("cll", "CLL"), ("cml", "CML"), ("normal", "Normal"))}
|
| 397 |
+
with gr.Accordion("Settings", open=False):
|
| 398 |
+
threshold = gr.Slider(0, 10, value=3, step=1, label="Image count threshold",
|
| 399 |
+
info="Leukemia is reported when more than this many fields are classed "
|
| 400 |
+
"leukemic. Published setting: 3.")
|
| 401 |
+
show_heatmaps = gr.Checkbox(value=True, label="Show heatmaps on the most influential fields")
|
| 402 |
+
consent = gr.Checkbox(label="I am a healthcare professional or researcher. I will not use the output for "
|
| 403 |
+
"patient care. I am authorised to use these images and they contain no "
|
| 404 |
+
"patient identifiers. I accept the terms of use.")
|
| 405 |
+
with gr.Row():
|
| 406 |
+
run = gr.Button("Analyse case", variant="primary", scale=3)
|
| 407 |
+
reset = gr.Button("Clear", variant="secondary", scale=1)
|
| 408 |
+
with gr.Column(scale=7, min_width=320, elem_classes="lk-panel"):
|
| 409 |
+
result = gr.HTML(PLACEHOLDER)
|
| 410 |
+
gallery = gr.Gallery(label="Most influential fields (warm colours = where the model looked)",
|
| 411 |
+
columns=3, rows=1, height=260, object_fit="cover", visible=False)
|
| 412 |
+
with gr.Accordion("Per-field results", open=False, visible=False) as table_box:
|
| 413 |
+
table = gr.Dataframe(interactive=False, wrap=True)
|
| 414 |
+
download = gr.File(label="Download all results (CSV)", visible=False)
|
| 415 |
+
gr.HTML(ABOUT)
|
| 416 |
+
gr.HTML(FOOTER)
|
| 417 |
+
|
| 418 |
+
outputs = [result, gallery, table_box, table, download]
|
| 419 |
+
run.click(analyse, [files, threshold, show_heatmaps, consent], outputs)
|
| 420 |
+
for k, b in ex_buttons.items():
|
| 421 |
+
b.click(lambda k=k: load_example(k), None, [files, consent] + outputs)
|
| 422 |
+
reset.click(clear, None, [files, consent] + outputs)
|
| 423 |
+
|
| 424 |
+
if __name__ == "__main__":
|
| 425 |
+
demo.queue(max_size=20).launch(
|
| 426 |
+
server_name=os.environ.get("HOST") or os.environ.get("GRADIO_SERVER_NAME") or "127.0.0.1",
|
| 427 |
+
server_port=int(os.environ.get("PORT") or os.environ.get("GRADIO_SERVER_PORT") or 7860),
|
| 428 |
+
theme=THEME, css=CSS, head=HEAD, allowed_paths=[ASSETS],
|
| 429 |
+
favicon_path=FAVICON if os.path.exists(FAVICON) else None,
|
| 430 |
+
auth=_access_list(), auth_message=AUTH_MESSAGE)
|
assets/brand/LeukoAI_icon_512.png
ADDED
|
|
Git LFS Details
|
assets/brand/LeukoAI_logo.png
ADDED
|
Git LFS Details
|
assets/brand/LeukoAI_logo_dark.png
ADDED
|
Git LFS Details
|
assets/examples/aml/aml_field_01.jpg
ADDED
|
Git LFS Details
|
assets/examples/aml/aml_field_02.jpg
ADDED
|
Git LFS Details
|
assets/examples/aml/aml_field_03.jpg
ADDED
|
Git LFS Details
|
assets/examples/aml/aml_field_04.jpg
ADDED
|
Git LFS Details
|
assets/examples/aml/aml_field_05.jpg
ADDED
|
Git LFS Details
|
assets/examples/aml/aml_field_06.jpg
ADDED
|
Git LFS Details
|
assets/examples/aml/aml_field_07.jpg
ADDED
|
Git LFS Details
|
assets/examples/aml/aml_field_08.jpg
ADDED
|
Git LFS Details
|
assets/examples/aml/aml_field_09.jpg
ADDED
|
Git LFS Details
|
assets/examples/aml/aml_field_10.jpg
ADDED
|
Git LFS Details
|
assets/examples/cll/cll_field_01.jpg
ADDED
|
Git LFS Details
|
assets/examples/cll/cll_field_02.jpg
ADDED
|
Git LFS Details
|
assets/examples/cll/cll_field_03.jpg
ADDED
|
Git LFS Details
|
assets/examples/cll/cll_field_04.jpg
ADDED
|
Git LFS Details
|
assets/examples/cll/cll_field_05.jpg
ADDED
|
Git LFS Details
|
assets/examples/cll/cll_field_06.jpg
ADDED
|
Git LFS Details
|
assets/examples/cll/cll_field_07.jpg
ADDED
|
Git LFS Details
|
assets/examples/cll/cll_field_08.jpg
ADDED
|
Git LFS Details
|
assets/examples/cll/cll_field_09.jpg
ADDED
|
Git LFS Details
|
assets/examples/cll/cll_field_10.jpg
ADDED
|
Git LFS Details
|
assets/examples/cml/cml_field_01.jpg
ADDED
|
Git LFS Details
|
assets/examples/cml/cml_field_02.jpg
ADDED
|
Git LFS Details
|
assets/examples/cml/cml_field_03.jpg
ADDED
|
Git LFS Details
|
assets/examples/cml/cml_field_04.jpg
ADDED
|
Git LFS Details
|
assets/examples/cml/cml_field_05.jpg
ADDED
|
Git LFS Details
|
assets/examples/cml/cml_field_06.jpg
ADDED
|
Git LFS Details
|
assets/examples/cml/cml_field_07.jpg
ADDED
|
Git LFS Details
|
assets/examples/cml/cml_field_08.jpg
ADDED
|
Git LFS Details
|
assets/examples/cml/cml_field_09.jpg
ADDED
|
Git LFS Details
|
assets/examples/cml/cml_field_10.jpg
ADDED
|
Git LFS Details
|
assets/examples/normal/normal_field_01.jpg
ADDED
|
Git LFS Details
|
assets/examples/normal/normal_field_02.jpg
ADDED
|
Git LFS Details
|
assets/examples/normal/normal_field_03.jpg
ADDED
|
Git LFS Details
|
assets/examples/normal/normal_field_04.jpg
ADDED
|
Git LFS Details
|
assets/examples/normal/normal_field_05.jpg
ADDED
|
Git LFS Details
|
assets/examples/normal/normal_field_06.jpg
ADDED
|
Git LFS Details
|
assets/examples/normal/normal_field_07.jpg
ADDED
|
Git LFS Details
|
assets/examples/normal/normal_field_08.jpg
ADDED
|
Git LFS Details
|
assets/examples/normal/normal_field_09.jpg
ADDED
|
Git LFS Details
|
assets/examples/normal/normal_field_10.jpg
ADDED
|
Git LFS Details
|
assets/logo-mark.svg
ADDED
|
|
leukoai/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LeukoAI: leukemia prediction from peripheral blood smear images. Research use only."""
|
| 2 |
+
__version__ = "1.1.0.dev0"
|
| 3 |
+
|
| 4 |
+
from .models import DX1, DX2, DX3, load_models # noqa: F401
|
| 5 |
+
from .pipeline import analyse_images, analyse_paths # noqa: F401
|
leukoai/experimental.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""EXPERIMENTAL case rules. Research comparison only; NOT used by the web app.
|
| 2 |
+
|
| 3 |
+
Each rule changes exactly ONE step of the published rule (acute vs chronic),
|
| 4 |
+
so differences in results can be attributed to that step alone:
|
| 5 |
+
|
| 6 |
+
* Leukemia vs no leukemia: unchanged (more than T images classed leukemic by Model 1).
|
| 7 |
+
* Subtype: unchanged (Model 3 image votes within ALL/AML or CLL/CML; ties -> ALL/CLL).
|
| 8 |
+
* Acute vs chronic: replaced, as below.
|
| 9 |
+
|
| 10 |
+
Both rules are parameter-free and were specified on 29 Oct 2026, before any
|
| 11 |
+
further patient data were analysed, so they cannot have been tuned to the
|
| 12 |
+
validation cases. Motivation: in public LeukoAI images, 11 AML fields classed
|
| 13 |
+
chronic by Model 2 turned an AML folder into "CLL" under the published
|
| 14 |
+
"2 or more chronic images" step (docs/FINDINGS_v1.md).
|
| 15 |
+
|
| 16 |
+
EXP_MAJORITY
|
| 17 |
+
Chronic if more images are classed chronic than acute by Model 2
|
| 18 |
+
(all images counted, as in the published rule). Ties -> acute, because
|
| 19 |
+
missing acute leukemia is the more dangerous error.
|
| 20 |
+
|
| 21 |
+
EXP_MEANPROB
|
| 22 |
+
Among images classed leukemic by Model 1, average Model 2's acute and
|
| 23 |
+
chronic probabilities. Chronic if mean P(chronic) > mean P(acute);
|
| 24 |
+
ties -> acute. Uses confidence rather than votes.
|
| 25 |
+
"""
|
| 26 |
+
from __future__ import annotations
|
| 27 |
+
|
| 28 |
+
from .models import DX3
|
| 29 |
+
from .rules import ImageResult
|
| 30 |
+
|
| 31 |
+
RULES = ("exp_majority", "exp_meanprob")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _leukemic(images, threshold):
|
| 35 |
+
return sum(1 for im in images if im.k1 == 1) > threshold
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _subtype(images, chronic: bool) -> str:
|
| 39 |
+
votes = [sum(1 for im in images if im.k3 == k) for k in range(5)]
|
| 40 |
+
sub = votes[2:4] if chronic else votes[:2]
|
| 41 |
+
return DX3[sub.index(max(sub)) + (2 if chronic else 0)]
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _finish(images, chronic: bool):
|
| 45 |
+
return ("CHRONIC LEUKEMIA" if chronic else "ACUTE LEUKEMIA"), _subtype(images, chronic)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def exp_majority(images: list[ImageResult], threshold: int = 3):
|
| 49 |
+
if not _leukemic(images, threshold):
|
| 50 |
+
return "NO LEUKEMIA", "NO LEUKEMIA"
|
| 51 |
+
acute = sum(1 for im in images if im.k2 == 0)
|
| 52 |
+
chronic = sum(1 for im in images if im.k2 == 1)
|
| 53 |
+
return _finish(images, chronic > acute)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def exp_meanprob(images: list[ImageResult], threshold: int = 3):
|
| 57 |
+
if not _leukemic(images, threshold):
|
| 58 |
+
return "NO LEUKEMIA", "NO LEUKEMIA"
|
| 59 |
+
leuk = [im for im in images if im.k1 == 1]
|
| 60 |
+
pa = sum(im.p2[0] for im in leuk) / len(leuk)
|
| 61 |
+
pc = sum(im.p2[1] for im in leuk) / len(leuk)
|
| 62 |
+
return _finish(images, pc > pa)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def run_all(images: list[ImageResult], threshold: int = 3) -> dict:
|
| 66 |
+
return {"exp_majority": exp_majority(images, threshold), "exp_meanprob": exp_meanprob(images, threshold)}
|