A desktop application for lung cancer risk estimation, combining validated deep learning and radiomics models with clinical and epidemiological risk factors.
- About
- Features
- Models
- Requirements
- Quick Start
- Batch Processing
- Architecture
- CI/CD
- Development
- Contributing
- Creating a Release
- Troubleshooting
- License
- Acknowledgments
PulmoRisk is a clinical decision support tool for estimating lung cancer risk from low-dose CT (LDCT) scans combined with patient clinical and epidemiological data. It integrates two complementary validated machine learning models into a single desktop application:
Sybil-Epi — A lung cancer risk prediction model that combines Sybil's deep learning CT analysis with 11 clinical and epidemiological factors: age, BMI, education level, ethnicity, COPD history, family lung cancer history, personal cancer history, smoking status, smoking duration, smoking intensity, and smoking quit time. The model outputs a calibrated 6-year lung cancer risk probability. No nodule annotation or segmentation is required. More information: journal.chestnet.org | GitHub
INTEGRAL-Radiomics — A radiomic feature extraction model that quantifies imaging biomarkers from CT scans, providing a complementary malignancy risk estimate based on tumour texture, shape, and intensity patterns. Requires a CT image and nodule mask in NRRD format. GitHub
⚠️ This tool is intended for research and clinical decision support only. Results should be interpreted by a qualified clinician and do not constitute a diagnosis.
- 🫁 Two validated models — Sybil-Epi (CT + clinical) and INTEGRAL-Radiomics (CT + radiomics)
- 📋 Single patient and batch modes — run one patient at a time or process a CSV of patients
- 🔄 Automatic R setup — INTEGRAL-Radiomics R dependencies install automatically on first launch
- 🪵 Live activity log — real-time inference progress and logging panel
- 🌗 Light / Dark / System theme — accessible contrast theme with full dark mode support
- 🐍 uv package manager — fast and reproducible dependency management
- 🐳 Docker development — containerised environment with docker-compose
- 🚀 GitHub Actions CI/CD — automated testing, linting, and multi-platform releases
- 🏗️ MVC architecture — clean separation of concerns
| Input | Details |
|---|---|
| CT scan folder | DICOM files (any standard LDCT series) |
| Age | Years |
| BMI | kg/m² |
| Education | 6-level scale |
| Ethnicity | White / Black / Asian / Other |
| COPD | Yes / No |
| Family lung cancer history | Yes / No |
| Personal cancer history | Yes / No |
| Smoking status | Current / Former |
| Smoking duration | Years |
| Smoking intensity | Cigarettes/day |
| Smoking quit time | Years since quitting |
Output: Calibrated 6-year lung cancer risk probability (Sybil-Epi ensemble score)
Alternatively, if you already have a Sybil 6-year risk score, you can enter it directly and skip CT inference entirely.
| Input | Details |
|---|---|
| CT image | NRRD format (.nrrd) |
| Nodule mask | NRRD format (.nrrd) |
| Age | Years |
| Sex | Male / Female |
| BMI | kg/m² |
| Family lung cancer history | Yes / No |
| COPD / emphysema | Yes / No |
| Former smoker | Yes / No |
| Smoking duration | Years |
| Cigarettes per day | Count |
| Quit time | Years since quitting |
Output: Probability of malignancy (pred_malignant)
- Python 3.10+
- R (required for INTEGRAL-Radiomics; detected automatically)
- On Linux:
python3-tk,libgl1,libglib2.0-0R packages (integralradand dependencies) are installed automatically on first launch via Posit Package Manager.
-
Start development environment:
docker-compose up -d app
-
Get shell in container:
docker-compose run --rm shell
-
Install uv:
curl -LsSf https://astral.sh/uv/install.sh | sh -
Install dependencies:
uv sync --all-extras
-
Run the application:
uv run python -m app.main
-
Run tests:
uv run pytest
-
Run linting:
uv run ruff check src/ tests/ uv run ruff format src/ tests/
Both models support CSV batch input. Pass a CSV file via the batch mode tab in the UI. On completion, results are written to <input>_scored.csv alongside the original file.
| Column | Description |
|---|---|
age |
Age in years |
bmi |
Body mass index (kg/m²) |
copd |
0 or 1 |
education |
1–6 (NLST codes) |
ethnicity |
1 = White, 2 = Black, 3 = Asian, 4 = Other |
family_lc_history |
0 or 1 |
personal_cancer_history |
0 or 1 |
smoking_duration |
Years |
smoking_intensity |
Cigarettes/day |
smoking_quit_time |
Years since quitting |
smoking_status |
0 = former, 1 = current |
ct_scan_dir |
Path to DICOM folder |
six_year_risk |
(optional) Pre-computed Sybil 6-year score |
| Column | Description |
|---|---|
image_file |
Path to CT image (NRRD) |
mask_file |
Path to nodule mask (NRRD) |
age |
Age in years |
female |
0 = male, 1 = female |
bmi |
Body mass index (kg/m²) |
fhlc |
Family lung cancer history: 0 or 1 |
copdemph |
COPD / emphysema: 0 or 1 |
formersmk |
Former smoker: 0 or 1 |
duration |
Smoking duration (years) |
cigday |
Cigarettes per day |
quittime |
Years since quitting |
tkinter-app/
├── .github/
│ └── workflows/
│ ├── ci.yml # CI pipeline
│ └── release.yml # Multi-platform releases
├── src/
│ └── app/
│ ├── __init__.py
│ ├── assets/ # Icons, themes
│ ├── config/ # Config files, Application settings
│ │ ├── __init__.py
│ │ └── settings.py
│ ├── main.py # Application entry point
│ ├── models/ # Data models
│ ├── views/ # UI components
│ │ ├── __init__.py
│ └── components/ # Reusable UI components
│ └── dialogs/ # Dialog Windows
│ │ └── main_view.py
│ └── controllers/ # Business logic
│ ├── __init__.py
│ └── app_controller.py
│ └── base_controller.py
│ └── utils/ # Utility functions
│ ├── __init__.py
│ └── event_bus.py # Threading logic
│ └── helpers.py # Helper functions
│ └── sybil_epi.py # Sybil Epi Scoring implementation
├── tests/
├── scripts/
│ └── build_linux.sh # Build script for linux distribution
│ └── convert_icon.py # Script to convert PNG icon to Windows .ico and macOS .icns
├── pyproject.toml # Project configuration
├── pulmorisk.spec # Spec file for pyinstaller for linux build
├── pulmorisk-mac.spec # Spec file for pyinstaller for MacOS build
├── pulmorisk-windows.spec # Spec file for pyinstaller for Windows build
├── uv.lock # Dependency lock file
├── Dockerfile # Container configuration
├── docker-compose.yml # Docker services
├── .dockerignore # Docker ignore file
├── .gitignore # Git ignore file
├── CHANGELOG.md # Release please auto generated changelog
└── README.md # This file
PulmoRisk follows the MVC (Model-View-Controller) pattern with a thread-safe event bus:
- Models (
models/patient_model.py) — Validated dataclasses for patient inputs. All range and business-rule checks run in__post_init__so every code path (UI, batch CSV, tests) gets the same validation. - Views (
views/) — CustomTkinter UI components. Views emit and react to events; they never call inference directly. - Controllers (
controllers/) — Coordinate models and views. Inference runs on background threads; results are posted back to the main thread viaEventBus. - EventBus (
utils/event_bus.py) — A thread-safe queue polled by Tkinter'safterloop at ~60 fps. All cross-thread communication goes through typedAppEventobjects.
Commits follow the Conventional Commits spec. Release Please automatically generates changelogs and bumps versions:
| Commit prefix | Version bump |
|---|---|
feat: |
Minor |
fix: |
Patch |
feat!: / BREAKING CHANGE |
Major |
Examples:
feat: add user authentication→ Bumps minor versionfix: resolve login button not working→ Bumps patch versionfeat(ui): add dark mode support→ Bumps minor version with scope
Conventional commits cheatsheet
On every merged release PR, GitHub Actions builds and uploads:
- Linux —
.AppImage(portable) and.debpackage - Windows —
.zipcontaining the PyInstaller one-dir bundle - macOS —
.dmgdisk image (Apple Silicon)
uv run pytestuv run ruff check src/ tests/
uv run ruff format src/ tests/uv run bandit -r src/VERSION=1.0.0 ./scripts/build_linux.sh bothProduces dist/PulmoRisk-<version>-x86_64.AppImage and dist/pulmorisk_<version>_amd64.deb.
- Fork the repository
- Create a feature branch:
git checkout -b feat/my-feature - Follow conventional commits
- Run tests and linting:
uv run pytest && uv run ruff check src/ - Open a pull request
- Make sure you have conventional commits
- When you push commits release please action will automatically create a release PR
- when you are ready merge the Release PR
- release-please pushes a v* tag
- The workflow will automatically build for all platforms
macOS may block PulmoRisk from opening because it comes from an unidentified developer. This happens when the application has not been notarized through Apple's verification process.
Follow these steps to allow PulmoRisk to run:
Go to:
System Settings → Privacy & Security
Scroll down to the Security section.
Find the PulmoRisk warning message and click:
Open Anyway
Click Open Anyway again when macOS asks for confirmation.
Enter your password or use Touch ID to complete the installation.
PulmoRisk should now open normally.
R not detected on startup — Install R from rig and ensure Rscript is on your PATH. PulmoRisk searches common install locations automatically (Homebrew, CRAN, rig on macOS; apt on Linux; Program Files on Windows).
INTEGRAL-Radiomics install fails — Check the Activity Log panel for the exact error. Common causes are missing system libraries (libssl-dev, libcurl4-openssl-dev) on Linux. The INTEGRAL tab will be unavailable but Sybil-Epi will still work.
GUI does not appear on Linux — Ensure DISPLAY is set and the X11 server is running. For Docker: xhost +local:docker before starting the container.
Blank CT inference result — Confirm the CT folder contains valid DICOM files and is not empty.
This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details.
- Sybil — MIT, Regina Barzilay Group, MIT CSAIL
- INTEGRAL-Radiomics — Hung Lab
- uv - Fast Python package management
- Docker - Containerization platform
- Ruff - Extremely fast Python linter
- GitHub Actions - CI/CD automation



