Medical AI · Chest X-Ray · TensorFlow · ONNX · FastAPI · Kafka · MLflow · Airflow

Lung Disease Detection: End-to-End Medical AI Platform

A full-stack AI system for chest X-ray analysis: dataset engineering, TFRecord generation, six model families, Optuna search, lung segmentation, binary ensemble triage, disease sub-classification, ONNX conversion, FastAPI inference, React UI, database logging, Kafka event pipelines, Docker Compose runtimes, MLflow registry, Airflow retraining, Hugging Face model delivery, and Supabase object storage for prediction artifacts.
Research Lab HF Model Repo HF Space Web App Dataset
CXR Images
0
Deployed Models
0
Best Binary F1
0
Disease F1
0
Segmentation Dice
0
Event Consumers
0
Modeling
TensorFlow/Keras
Optimization
Optuna
Dataset
TFRecord
API
FastAPI
Frontend
HTML/CSS/JS
Cloud DB
Postgres
Local DB
MSSQL
Deployment
Docker
Registry
MLflow
Pipelines
Airflow
Events
Kafka
Object Storage
Supabase
System Map

Research to Production Architecture

The project is organized around the complete lifecycle: data, modeling, segmentation, runtime inference, serving, and operations.

Data Layer
21,165 pairs

Aggregates four chest X-ray classes, creates metadata and mapping JSON files, pairs each image with a lung mask, and serializes the dataset into TFRecords.

Research Layer
6 models

Trains four healthy/unhealthy classifiers, one disease classifier, one U-Net/Xception segmentation model, and a multi-task segmentation/classification prototype.

Serving Layer
ONNX default

Loads metadata-driven Keras or ONNX artifacts, segments lung fields, crops ROI, runs ensemble voting, optionally classifies disease, and returns visual artifacts.

Operations Layer
MLOps

Uses Docker Compose, SQL Server/Postgres, Kafka consumers, MLflow registry, Airflow DAGs, Hugging Face model download, and Supabase object storage for source, mask, ROI, and overlay artifacts.

Dataset Foundation

COVID-19 Radiography Database Preparation

The data layer converts a public medical imaging dataset into project-ready metadata, mask pairs, mappings, and TFRecords.

The project uses the COVID-19 Radiography Database, which contains chest X-ray images and corresponding lung masks for four classes: COVID, Normal, Lung Opacity, and Viral Pneumonia. The first notebooks consolidate image/mask paths, create class mappings for binary and multi-class tasks, and turn the raw folder tree into durable metadata files used by both research and deployment.

Original classesCOVID, Normal, Viral Pneumonia, and Lung_Opacity.
Binary taskNormal becomes Healthy; COVID, Viral Pneumonia, and Lung Opacity become Unhealthy.
Disease taskOnly unhealthy ROI images are classified into COVID, Viral Pneumonia, or Lung Opacity.
COVID3,616
Normal10,192
Lung Opacity6,012
Viral Pneumonia1,345
ArtifactRoleUsed By
all_metadata.csvImage-level metadata for 21,165 CXR records.EDA, model training, MLOps evaluation.
all_image_mask_pairs.csvPairs each X-ray with its lung mask.Segmentation, ROI cropping, mask-aware classification.
class_mapping.jsonOriginal four-class mapping.Research notebooks.
healthy_binary_mapping.jsonHealthy/unhealthy output mapping.Binary ensemble and API response labels.
diseases_mapping.jsonCOVID, Viral Pneumonia, Lung Opacity mapping.Disease classifier and deployment assets.
Dataset Dependencies

Data Engineering Libraries & Dependencies

The dataset notebooks use lightweight but important tooling for image paths, mappings, serialization, and TensorFlow input pipelines.

Pandas + NumPyBuild metadata tables, count classes, validate image/mask pairs, and prepare arrays for downstream TFRecord creation.
glob + os + JSONTraverse class folders, collect image and mask paths, and persist binary/multiclass mappings as project contracts.
TensorFlow TFRecord APIsSerialize images, masks, labels, and metadata into 10 sharded TFRecord files of roughly 77 MB each.
Medical dataset citationThe source README documents the COVID-19 Radiography Database, its contributing sources, class counts, and citation requirements.
TFRecord Pipeline

Serialized Training Data

The TFRecord notebook turns the large image/mask dataset into repeatable TensorFlow input shards.

create_tfrecords.ipynb
def create_tfrecord(df, output_path, shard_size):
    for shard_idx, shard in enumerate(split_dataframe(df, shard_size)):
        writer = tf.io.TFRecordWriter(f"{output_path}_{shard_idx:02d}.tfrecord")
        for row in shard.itertuples():
            example = serialize_example(
                image_path=row.image_path,
                mask_path=row.mask_path,
                label=row.class_index,
                binary_label=row.binary_label,
            )
            writer.write(example.SerializeToString())
shards.txt
data_00.tfrecord ... data_09.tfrecord
10 total shards
~77 MB per shard
image + mask + class label + binary label
used by training, evaluation, and MLOps jobs
Lung medical hero visual
Medical AI context image used as a page-level visual anchor.
Image mask crop pipeline
Image + mask supervision concept: predict lung mask, then crop the image around the relevant region.
EDA and Data Quality

Dataset Checks Before Training

The EDA work is not decorative. It protects the training pipeline from missing masks, broken mappings, imbalance, and inconsistent class contracts.

Pair Integrity
image + mask

Every X-ray must have a matching lung mask. This matters because segmentation, ROI cropping, EfficientNet's mask channel, and masked-background models all depend on mask availability.

Class Imbalance
Normal heavy

Normal scans are the largest class and Viral Pneumonia is the smallest. The project separates binary and disease tasks so imbalance is handled where it actually affects the decision.

Mapping Consistency
contracts

Research mappings and deployment mappings are saved explicitly so notebook labels, API labels, frontend labels, and database labels stay aligned.

Serialization Check
10 shards

TFRecord generation turns scattered image folders into deterministic shards that can be reused by notebooks, evaluation jobs, and monthly MLOps tasks.

EDA QuestionWhat Was CheckedWhy It Matters
Are masks available?Image paths and mask paths are paired in a single metadata file.The segmentation model and ROI-based classifiers require mask supervision.
Are class labels stable?JSON mappings are generated for original four-class, binary, and disease-only tasks.Prevents label mismatch between research, deployment, and UI labels.
Is the task split sensible?Normal is preserved for binary triage, while unhealthy classes become disease classifier inputs.Avoids asking the disease model to classify healthy scans as a disease.
Can the data stream repeatably?TFRecord shards store image, mask, class label, and binary label.Training and retraining jobs can consume the same artifact format.
Training Libraries First

Modeling Libraries & Training Dependencies

The training tab starts with dependencies because every notebook uses a slightly different mix of Keras backbones, mask processing, callbacks, metrics, and optimization tools.

TensorFlow / KerasTransfer-learning backbones, custom heads, metrics, callbacks, saved .keras artifacts, TFRecord datasets, and model export preparation.
Mask + ROI preprocessingEvery binary model uses the lung mask differently: ROI crop, mask channel, masked background, or mask-as-RGB input.
AUC, precision, recall, F1Model comparison is centered on F1 for classification, while detailed sections still show AUC, precision, recall, accuracy, threshold, and validation loss.
OptunaUsed for DenseNet binary and disease classifier architecture search, optimizer search, staged LR, dropout, label smoothing, and class-weight tuning.
YAML + JSON contractsFinal training decisions are saved into model metadata, mappings, Optuna JSON, and deployment model folders.
Training Strategy

Six-Model Research Program

The project trains binary triage models, a disease classifier, and segmentation models rather than relying on one monolithic classifier.

The research layer is split by clinical decision: first isolate lungs, then decide healthy versus unhealthy, then classify the disease if pathology is detected. Binary classification is intentionally ensemble-based: DenseNet121, EfficientNetV2B3, InceptionV3, and MobileNetV3 vote on the final healthy/unhealthy decision. A separate DenseNet121 model handles disease sub-classification for unhealthy scans.

ModelTaskPreprocessingThreshold / OutputF1-First Metrics
EfficientNetV2B3Binary healthy/unhealthy4-channel RGB + mask, normalize [-1,1]Threshold 0.85F1 0.9544, AUC 0.9871, precision 0.9641, recall 0.9448
DenseNet121Binary healthy/unhealthyLung ROI + DenseNet preprocessingThreshold 0.91F1 0.9245, AUC 0.9711, precision 0.9183, recall 0.9308
MobileNetV3Binary healthy/unhealthyMask-as-RGB, masked inputThreshold 0.50F1 0.9206, AUC 0.9730, precision 0.9259, recall 0.9153
InceptionV3Binary healthy/unhealthyMask-as-RGB, background fill -1.0Threshold 0.50F1 0.9160, AUC 0.9699, precision 0.9244, recall 0.9077
DenseNet121COVID / Viral Pneumonia / Lung OpacityLung ROI + DenseNet preprocessing3-class softmaxF1 0.9782, AUC 0.9982, precision 0.9781, recall 0.9763
U-Net XceptionLung segmentation256x256 RGB inputMask threshold 0.50Accuracy 0.9885, Dice 0.9660, IoU 0.9533
Research Notebook Map

Training Subsections and Notebook Coverage

The training tab is split like a small research menu because the project contains several separate experiments, not one notebook.

Notebook / File GroupResearch RoleImportant Details Preserved in Page
MobileNetV3-healthy-binary_classification.ipynbLightweight binary classifierMask-as-RGB preprocessing, threshold 0.50, F1 0.9206, AUC 0.9730.
InceptionV3-healthy-binary_classification.ipynbBinary classifier with Inception featuresMasked background fill, threshold 0.50, F1 0.9160, AUC 0.9699.
EfficientNetV2B3-healthy-binary_classification.ipynbStrongest binary modelRGB plus mask channel input, threshold 0.85, best binary F1 0.9544, AUC 0.9871.
DenseNet121-healthy_binary_classification.ipynbROI-based binary modelDenseNet preprocessing, ROI crop, conservative threshold 0.91, F1 0.9245.
DenseNet121-healthy_binary_optuna_phase1/2.ipynbBinary hyperparameter searchClassifier head 1024/512, AdamW, cosine decay, dropout, label smoothing, staged LR.
DenseNet121-diseases-multiclass_classification.ipynbDisease sub-classifierThree-class unhealthy diagnosis with AUC 0.9982 and F1 0.9782.
DenseNet121-diseases_optuna_phase1/2.ipynbDisease optimizationCompact 256/64 head, class-weight power, label smoothing, dropout 0.45.
metadata.yaml filesDeployment contractThresholds, class labels, preprocessing, artifact paths, ONNX paths, and reported metrics.
Tabbed Model Training

Model-by-Model Training Details

Each tab keeps the same structure: libraries first, architecture, training configuration, callbacks, metrics, and a compact code panel.

Healthy/Unhealthy

MobileNetV3 Lightweight Voter

F10.9206
Model RoleCompact binary voter that adds a lightweight architecture to the ensemble and reduces dependence on heavy backbones.
ArchitectureImageNet MobileNetV3 backbone, global pooling, dropout, sigmoid binary head.
ConfigMask-as-RGB input, threshold 0.50, healthy/unhealthy labels, Keras + ONNX artifacts.
CallbacksCheckpoint best validation model, early stopping, LR reduction, validation metric tracking.
F1AUCPrecisionRecallNotebook Peaks
0.92060.97300.92590.9153val acc 0.9242, val precision 0.9381, val recall 0.9420
MobileNet architecture
base = MobileNetV3Large(include_top=False, weights="imagenet", input_shape=(256, 256, 3))
base.trainable = False
x = GlobalAveragePooling2D()(base.output)
x = Dropout(dropout)(x)
out = Dense(1, activation="sigmoid")(x)
model = Model(base.input, out)
model.compile(optimizer=Adam(lr), loss="binary_crossentropy", metrics=[AUC(), Precision(), Recall()])
MobileNet callbacks
callbacks = [
    ModelCheckpoint(best_path, monitor="val_loss", save_best_only=True),
    EarlyStopping(monitor="val_loss", patience=patience, restore_best_weights=True),
    ReduceLROnPlateau(monitor="val_loss", factor=0.2, patience=3),
]
MobileNet fine-tuning
history_head = model.fit(train_ds, validation_data=val_ds, callbacks=callbacks)
base.trainable = True
freeze_lower_layers(base, keep_top_blocks=True)
model.compile(optimizer=Adam(low_lr), loss="binary_crossentropy", metrics=metrics)
history_ft = model.fit(train_ds, validation_data=val_ds, callbacks=callbacks)
Healthy/Unhealthy

InceptionV3 Masked-Background Voter

F10.9160
Model RoleMasked-background binary voter that tests a deeper convolutional feature extractor with non-lung pixels suppressed.
ArchitectureImageNet InceptionV3 backbone with binary sigmoid head and fine-tuned upper layers.
ConfigMask-as-RGB input, background fill -1.0, threshold 0.50, Keras + ONNX runtime.
CallbacksValidation loss monitoring, checkpointing, early stopping, learning-rate control.
F1AUCPrecisionRecallNotebook Peaks
0.91600.96990.92440.9077val acc 0.9171, val precision 0.9464, val recall 0.9562
Inception architecture
base = InceptionV3(include_top=False, weights="imagenet", input_shape=(256, 256, 3))
x = GlobalAveragePooling2D()(base.output)
x = Dropout(dropout)(x)
out = Dense(1, activation="sigmoid")(x)
model = Model(base.input, out)
Inception callbacks
callbacks = [
    ModelCheckpoint(best_path, monitor="val_loss", save_best_only=True),
    EarlyStopping(monitor="val_loss", patience=patience, restore_best_weights=True),
    ReduceLROnPlateau(monitor="val_loss", factor=0.2, patience=3),
]
Inception masked fine-tuning
mask_rgb = tf.repeat(mask[..., :1], repeats=3, axis=-1)
background = tf.ones_like(image) * -1.0
masked_image = tf.where(mask_rgb > 0.5, image, background)
model_input = tf.keras.applications.inception_v3.preprocess_input(masked_image)
prob = model(model_input)
label = int(prob >= 0.50)
Healthy/Unhealthy

EfficientNetV2B3 Four-Channel Voter

F10.9544
Model RoleStrongest healthy/unhealthy branch, using the lung mask as an explicit fourth input channel.
ArchitectureEfficientNetV2B3 transfer learning adapted for RGB plus mask-channel input.
ConfigNormalize to [-1, 1], threshold 0.85, best binary F1 and AUC among deployed binary models.
CallbacksBest-checkpoint saving, early stopping, LR scheduling, validation AUC/precision/recall tracking.
F1AUCPrecisionRecallNotebook Peaks
0.95440.98710.96410.9448val acc 0.9574, val AUC 0.9873, val loss 0.2146
EfficientNet architecture
inputs = Input(shape=(256, 256, 4))
x = Conv2D(3, kernel_size=1, padding="same")(inputs)
base = EfficientNetV2B3(include_top=False, weights="imagenet")
x = base(x)
x = GlobalAveragePooling2D()(x)
out = Dense(1, activation="sigmoid")(x)
EfficientNet callbacks
callbacks = [
    ModelCheckpoint(best_path, monitor="val_auc", mode="max", save_best_only=True),
    EarlyStopping(monitor="val_loss", patience=patience, restore_best_weights=True),
    ReduceLROnPlateau(monitor="val_loss", patience=3),
]
EfficientNet 4-channel fine-tuning
mask_1c = tf.cast(mask[..., :1] > 0.5, tf.float32)
four_channel = tf.concat([image, mask_1c], axis=-1)
four_channel = (four_channel * 2.0) - 1.0
prob = efficientnet_binary(four_channel)
label = int(prob >= 0.85)
Healthy/Unhealthy

DenseNet121 ROI Binary Voter

F10.9245
Model RoleROI-based conservative binary voter, optimized with two Optuna phases and a high decision threshold.
ArchitectureDenseNet121 backbone, ROI crop input, 1024/512 dense head from Optuna phase 1.
ConfigThreshold 0.91, label smoothing 0.075, dropout 0.25, unfreeze from conv5_block1_0_bn.
CallbacksTwo-stage training, warmup 3, checkpointing, early stopping, cosine LR fine-tuning.
F1AUCPrecisionRecallOptuna
0.92450.97110.91830.9308P1 0.8882, P2 0.8968
DenseNet binary architecture
roi = crop_lung_roi(image, mask, target_size=(256, 256), threshold=0.5)
x = DenseNet121(include_top=False, weights="imagenet")(roi)
x = GlobalAveragePooling2D()(x)
x = Dense(1024, activation="relu")(x)
x = Dropout(0.25)(x)
x = Dense(512, activation="relu")(x)
out = Dense(1, activation="sigmoid")(x)
DenseNet binary callbacks
callbacks = [
    ModelCheckpoint(best_path, monitor="val_auc", mode="max", save_best_only=True),
    EarlyStopping(monitor="val_loss", patience=patience, restore_best_weights=True),
    LearningRateScheduler(cosine_warmup_schedule),
]
DenseNet binary fine-tuning
loss = BinaryCrossentropy(label_smoothing=0.075)
optimizer = AdamW(learning_rate=8.824e-4, weight_decay=1.045e-5)
fit_classifier_head()
unfreeze_from("conv5_block1_0_bn")
set_lr(2.047e-5)
fit_finetune()
Disease Classifier

DenseNet121 Disease Sub-Classifier

F10.9782
Model RoleDedicated disease classifier used only after the ensemble predicts unhealthy, avoiding disease labels for healthy scans.
ArchitectureDenseNet121 ROI classifier with compact 256/64 dense head from Optuna phase 1.
Config3-class softmax: COVID, Viral Pneumonia, Lung Opacity. Dropout 0.45, label smoothing 0.025.
CallbacksCheckpoint best model, early stopping, cosine LR, class-weight power 0.163 for imbalance.
F1AUCPrecisionRecallNotebook Peaks
0.97820.99820.97810.9763val AUC 0.9985, val loss 0.1672
Disease architecture
classes = ["COVID", "Viral Pneumonia", "Lung Opacity"]
roi = crop_lung_roi(image, mask, target_size=(256, 256))
x = DenseNet121(include_top=False, weights="imagenet")(roi)
x = GlobalAveragePooling2D()(x)
x = Dense(256, activation="relu")(x)
x = Dropout(0.45)(x)
x = Dense(64, activation="relu")(x)
out = Dense(3, activation="softmax")(x)
Disease callbacks
callbacks = [
    ModelCheckpoint(best_path, monitor="val_loss", save_best_only=True),
    EarlyStopping(monitor="val_loss", patience=patience, restore_best_weights=True),
    LearningRateScheduler(cosine_decay_with_warmup),
]
Disease fine-tuning config
loss = CategoricalCrossentropy(label_smoothing=0.025)
optimizer = AdamW(learning_rate=2.798e-4, weight_decay=5.429e-4)
class_weights = compute_class_weights(power=0.163)
fit_classifier_head()
unfreeze_top_dense_blocks()
fit_finetune()
Callbacks & Fine-Tuning

Callbacks and Fine-Tuning by Model

Every model has its own staged training and callback contract instead of one generic callback explanation.

Stage 1
Classifier-head trainingFreeze MobileNetV3 and train the compact binary head on masked inputs.
Backbonefrozen
Inputmask-rgb
Headsigmoid
Stage 2
Light fine-tuningUnfreeze upper MobileNet blocks with a lower learning rate for final adaptation.
Threshold0.50
F10.9206
AUC0.9730
Control
CallbacksUse validation loss for checkpointing and early stopping, with LR reduction when validation stalls.
Monitorval_loss
Savebest
LRplateau
Persist
Deployment contractSave Keras, ONNX, metadata threshold, preprocessing flags, and binary class mapping.
FormatYAML
RuntimeONNX
Taskbinary
architecture
base = MobileNetV3Large(include_top=False, weights="imagenet")
x = GlobalAveragePooling2D()(base.output)
x = Dropout(dropout)(x)
out = Dense(1, activation="sigmoid")(x)
model = Model(base.input, out)
callbacks
callbacks = [
    ModelCheckpoint(best_path, monitor="val_loss", save_best_only=True),
    EarlyStopping(monitor="val_loss", patience=patience, restore_best_weights=True),
    ReduceLROnPlateau(monitor="val_loss", factor=0.2, patience=3),
]
fine-tuning
base.trainable = False
fit_classifier_head()
base.trainable = True
freeze_lower_layers(base)
compile_with_low_lr()
fit_finetune()
Stage 1
Classifier-head trainingFreeze InceptionV3 and train the binary head on mask-filled images.
Backbonefrozen
Fill-1.0
Headsigmoid
Stage 2
Top-block fine-tuningUnfreeze upper Inception blocks and continue with a lower learning rate.
Threshold0.50
F10.9160
AUC0.9699
Control
CallbacksCheckpoint the best model, restore best weights, and reduce LR on plateau.
Monitorval_loss
Savebest
LRplateau
Persist
Deployment contractStore background fill, mask-as-RGB flag, threshold, Keras artifact, and ONNX artifact.
FormatYAML
RuntimeONNX
Taskbinary
architecture
base = InceptionV3(include_top=False, weights="imagenet")
x = GlobalAveragePooling2D()(base.output)
x = Dropout(dropout)(x)
out = Dense(1, activation="sigmoid")(x)
model = Model(base.input, out)
callbacks
callbacks = [
    ModelCheckpoint(best_path, monitor="val_loss", save_best_only=True),
    EarlyStopping(monitor="val_loss", patience=patience, restore_best_weights=True),
    ReduceLROnPlateau(monitor="val_loss", patience=3),
]
fine-tuning
train_head_on_masked_images()
unfreeze_top_inception_blocks()
compile_with_low_lr()
fit_until_validation_loss_stops_improving()
save_best_model()
Stage 1
Four-channel head trainingTrain the classifier with RGB plus lung mask channel while the pretrained body is protected.
Input4ch
Norm[-1,1]
Headsigmoid
Stage 2
High-F1 fine-tuningFine-tune upper EfficientNet layers and select a stricter threshold.
Threshold0.85
F10.9544
AUC0.9871
Control
CallbacksMonitor validation AUC for checkpointing and validation loss for early stopping.
Monitorval_auc
Savemax
LRplateau
Persist
Deployment contractSave the 4-channel preprocessing contract, threshold, metrics, Keras model, and ONNX model.
FormatYAML
RuntimeONNX
Taskbinary
architecture
inputs = Input(shape=(256, 256, 4))
x = Conv2D(3, 1, padding="same")(inputs)
base = EfficientNetV2B3(include_top=False, weights="imagenet")
x = GlobalAveragePooling2D()(base(x))
out = Dense(1, activation="sigmoid")(x)
callbacks
callbacks = [
    ModelCheckpoint(best_path, monitor="val_auc", mode="max", save_best_only=True),
    EarlyStopping(monitor="val_loss", patience=patience, restore_best_weights=True),
    ReduceLROnPlateau(monitor="val_loss", patience=3),
]
fine-tuning
build_rgb_plus_mask_dataset()
train_classifier_head()
unfreeze_top_efficientnet_layers()
compile_with_lower_lr()
fit_and_select_threshold(0.85)
Stage 1
Optuna-selected head trainingTrain the 1024/512 DenseNet binary head on lung ROI crops.
Units1024/512
LR8.82e-4
Drop0.25
Stage 2
Partial unfreezingUnfreeze from conv5_block1_0_bn and continue at a very low learning rate.
Threshold0.91
F10.9245
LR22.05e-5
Control
CallbacksUse validation AUC checkpointing, early stopping, and cosine warmup schedule.
Monitorval_auc
Savemax
Warmup3
Persist
Deployment contractPersist ROI preprocessing, threshold 0.91, class mapping, Keras, ONNX, and Optuna JSON.
FormatYAML
OptunaP2
Taskbinary
architecture
roi = crop_lung_roi(image, mask)
x = DenseNet121(include_top=False, weights="imagenet")(roi)
x = GlobalAveragePooling2D()(x)
x = Dense(1024, activation="relu")(x)
x = Dropout(0.25)(x)
out = Dense(1, activation="sigmoid")(Dense(512, activation="relu")(x))
callbacks
callbacks = [
    ModelCheckpoint(best_path, monitor="val_auc", mode="max", save_best_only=True),
    EarlyStopping(monitor="val_loss", patience=patience, restore_best_weights=True),
    LearningRateScheduler(cosine_warmup_schedule),
]
fine-tuning
fit_head(lr=8.824e-4, warmup=3)
unfreeze_from("conv5_block1_0_bn")
set_lr(2.047e-5)
fit_finetune(label_smoothing=0.075)
deploy_threshold = 0.91
Stage 1
Disease head trainingTrain the 256/64 disease head on unhealthy lung ROI crops only.
Units256/64
LossCCE
Classes3
Stage 2
Class-weighted fine-tuningApply class-weight power, AdamW, label smoothing, and high dropout from Optuna phase 2.
F10.9782
Drop0.45
Power0.163
Control
CallbacksCheckpoint best validation loss, restore best weights, and schedule cosine decay with warmup.
Monitorval_loss
Savebest
Schedulecosine
Persist
Deployment contractPersist disease mapping, ROI preprocessing, Keras, ONNX, metrics, and Optuna JSON.
FormatYAML
RuntimeONNX
Taskdisease
architecture
roi = crop_lung_roi(image, mask)
x = DenseNet121(include_top=False, weights="imagenet")(roi)
x = GlobalAveragePooling2D()(x)
x = Dense(256, activation="relu")(x)
x = Dropout(0.45)(x)
out = Dense(3, activation="softmax")(Dense(64, activation="relu")(x))
callbacks
callbacks = [
    ModelCheckpoint(best_path, monitor="val_loss", save_best_only=True),
    EarlyStopping(monitor="val_loss", patience=patience, restore_best_weights=True),
    LearningRateScheduler(cosine_decay_with_warmup),
]
fine-tuning
class_weights = compute_class_weights(power=0.163)
fit_head(lr=2.798e-4)
unfreeze_top_dense_blocks()
fit_finetune(weight_decay=5.429e-4)
evaluate_macro_f1_precision_recall()
Fine-Tuning Theory

Why Multiple Binary Models?

The ensemble is a deliberate risk-control design: different architectures see the same CXR through different preprocessing views.

Mask-Aware Diversity
ensemble

DenseNet and the disease classifier use lung ROI crops. EfficientNet receives a fourth mask channel. Inception and MobileNet use mask-as-RGB plus masked background handling. This creates architectural and preprocessing diversity.

Voting Rule
> 2 votes
final_label = 1[sum(model_labels) > 2]

All four binary models produce probabilities and thresholded labels. The final label becomes unhealthy only when more than two models vote unhealthy; the final probability is the average probability across models.

Two-Stage Diagnosis
triage

The disease classifier runs only after the binary ensemble returns unhealthy. This prevents disease predictions from being shown for scans that the triage layer considers normal.

Clinical Framing
assistive

The platform is a technical prototype for screening support. It exposes probabilities, per-model disagreement, segmentation visuals, and artifacts rather than hiding uncertainty behind one label.

Model Visuals

Architectures Used in the Research Stack

Selected notebook assets document the main transfer-learning backbones and segmentation design.

DenseNet
DenseNet architecture
EfficientNetV2
EfficientNetV2 architecture
MobileNetV3
MobileNetV3 architecture
InceptionV3
InceptionV3 architecture
U-Net Xception
U-Net Xception architecture
U-Net Skip Connections
U-Net architecture
Segmentation Notebook

U-Net Xception Lung Masking

The segmentation model is the foundation for ROI extraction, artifact visualization, and mask-aware classification.

The segmentation notebook trains a U-Net-style model with an Xception backbone to predict binary lung masks from 256x256 chest X-rays. The mask is not only a visual output; it guides ROI cropping and helps multiple classifiers focus on lung tissue instead of labels, borders, shoulders, and background.

Accuracy0.9885
Dice0.9660
IoU0.9533
Mask Threshold0.50
Dice Coefficient
mask overlap
$$Dice = \\frac{2|P \\cap Y|}{|P| + |Y|}$$

Dice rewards overlap between predicted lung pixels and ground-truth lung pixels. It is useful for medical segmentation because foreground regions are often smaller than the background.

ROI Crop
deployment

At inference time the mask is thresholded, converted to a bounding box, expanded with a small margin, and resized back to 256x256. If no mask pixels exist, the pipeline safely falls back to the full image.

Segmentation Dependencies

Libraries & Model Dependencies

Segmentation has its own model, metrics, custom object, preprocessing, and deployment metadata.

TensorFlow / KerasDefines the U-Net/Xception segmentation network, callbacks, loss, and saved Keras model artifact.
Custom Dice metricThe deployment loader registers dice_coefficient as a custom object when loading the Keras segmentation model.
metadata.yamlStores model path, ONNX path, input size, mask threshold, output classes, accuracy, Dice, and IoU.
Matplotlib visual checksThe notebooks include many segmentation visual outputs for source images, masks, predictions, and multi-task comparisons.
Multi-Task Prototype

Segmentation + Classification Experiment

A separate notebook tests a shared model with segmentation and classification outputs.

Output HeadSaved MetricInterpretation
segmentationval Dice up to 0.9589, val IoU up to 0.9451The shared model still learned strong lung masks.
classificationval classification accuracy up to 0.8887Useful prototype, but weaker than the dedicated ensemble strategy.
combined losssegmentation loss + classification lossDemonstrates multi-objective learning, but final production path keeps models modular.
roi.py
def crop_lung_roi(img, mask, target_size, threshold=0.5, margin_ratio=0.1):
    mask_2d = binary_mask_to_rgb_batch(mask)[0, :, :, 0] > threshold
    indices = np.argwhere(mask_2d)
    if indices.size > 0:
        y_min, x_min = indices.min(axis=0)
        y_max, x_max = indices.max(axis=0)
        cropped = img[0, y_start:y_end, x_start:x_end, :]
    else:
        cropped = img[0]
    return resize_float_image(cropped, target_size)
segmentation metadata
model: unet_xception-segmentation.keras
onnx_path: unet_xception-segmentation.onnx
input_size: [256, 256]
postprocessing.threshold: 0.5
metrics:
  accuracy: 0.9885
  dice_coefficient: 0.9660
  iou: 0.9533
Runtime Inference

Metadata-Driven Prediction Pipeline

The production detector is a staged pipeline: load image, segment, crop ROI, ensemble binary models, optionally classify disease, then save visuals.

01
Load imageAccept local path, URL, base64, or uploaded file; validate one source only; convert to RGB and resize to 256x256.
02
Segment lungsRun U-Net Xception using Keras or ONNX, threshold the mask, and keep it for ROI and visual output.
03
Run binary ensembleDenseNet, EfficientNet, Inception, and MobileNet each use metadata-defined preprocessing and thresholds.
04
Disease classifierIf the ensemble returns unhealthy, classify ROI into COVID, Viral Pneumonia, or Lung Opacity.
05
Save artifactsPersist source, mask, ROI, and overlay images locally or to Supabase storage, then return URLs and paths.
Source
Prediction source X-ray
Mask
Prediction mask
ROI
Prediction ROI
Overlay
Prediction overlay
Inference Dependencies

Runtime Libraries & Artifacts

The inference layer is driven by model metadata and can switch between Keras and ONNX execution.

ONNX RuntimeMODEL_RUNTIME=onnx is the default runtime for slim deployment, using CPUExecutionProvider for all six exported models.
TensorFlow / Keras fallbackThe same pipeline can load local Keras artifacts or MLflow registry models when MODEL_RUNTIME is not ONNX.
metadata.yaml per modelDefines model path, ONNX path, task, threshold, classes, preprocessing steps, input channels, and reported metrics.
Pillow + NumPyLoad images, EXIF-correct them, resize, convert arrays, save prediction artifacts, and overlay masks.
Hugging Face HubWhen enabled, startup downloads required model folders from a1mohamadd/lung-disease-detection if local artifacts are missing.
ONNX Conversion

Export and Validation

Six Keras models are exported to ONNX and validated against Keras outputs with tolerance checks.

export_models.py
MODEL_SPECS = (
    ExportSpec("binary_densenet", saved_models / "healthy_unhealthy/densenet"),
    ExportSpec("binary_efficientnet", saved_models / "healthy_unhealthy/efficientnet"),
    ExportSpec("binary_inception", saved_models / "healthy_unhealthy/inception"),
    ExportSpec("binary_mobilenet", saved_models / "healthy_unhealthy/mobilenet"),
    ExportSpec("diseases_densenet", saved_models / "diseases/densenet"),
    ExportSpec("segmentation_unet_xception", saved_models / "segmentation/unet_xception"),
)
validate_onnx_outputs.py
keras_output = keras_model.predict(sample, verbose=0)
onnx_output = session.run(None, {input_name: sample})[0]

max_abs = np.max(np.abs(keras_output - onnx_output))
mean_abs = np.mean(np.abs(keras_output - onnx_output))
ok = np.allclose(keras_output, onnx_output, rtol=1e-3, atol=1e-3)
ArtifactKeras SizeONNX SizeRuntime Use
InceptionV3 binary257.3 MB87.2 MBMask-as-RGB binary classifier.
EfficientNetV2B3 binary157.2 MB52.1 MB4-channel RGB+mask classifier.
DenseNet binary47.9 MB27.7 MBROI-based conservative ensemble voter.
Disease DenseNet47.8 MB27.7 MBCOVID / Viral Pneumonia / Lung Opacity classifier.
MobileNetV3 binary40.6 MB13.4 MBLightweight masked binary voter.
U-Net Xception23.8 MB7.9 MBLung mask generator.
Runtime Startup

Model Loading, Path Checks, and Warmup

The API does not wait until the first user request to discover missing models. Startup validates the whole inference stack.

01
Download if neededensure_models_available_from_huggingface() can pull required artifact folders when local models are absent.
02
Validate pathsStartup checks required saved model directories for segmentation, binary models, and disease classification.
03
Read metadataEach model folder must include metadata.yaml; thresholds and preprocessing come from metadata, not hardcoded UI text.
04
Create detectorLungDetection loads segmentation, four binary models, disease classifier, and the ensemble wrapper.
05
WarmupA dummy 256x256 image runs through the detector so runtime sessions are initialized before real traffic.
startup.py
async def lifespan(app):
    ensure_models_available_from_huggingface()
    check_paths_and_metadata()
    init_database()
    init_kafka_producer()
    app.state.detector = create_detector()
    warmup(app.state.detector)
    yield
input validation
accepted sources:
  image_path
  image_base64
  image_url
  multipart upload

rule:
  exactly one source must be provided
  image is converted to RGB
  EXIF orientation is corrected
  final model input is resized to 256x256
Preprocessing Contracts

How Each Model Receives the Same X-Ray Differently

The runtime pipeline is metadata-driven because every model expects a slightly different view of the image and mask.

ModelInput Built FromNormalizationReason
segmentation/unet_xceptionFull RGB imageTensorFlow image scalePredict a lung mask before downstream classifiers run.
binary/densenetLung ROI cropDenseNet preprocessingFocuses the classifier on lung fields only.
binary/efficientnetRGB image plus mask channel[-1, 1]Combines visual content with explicit segmentation context.
binary/inceptionMask-as-RGB with background fillInception preprocessingSuppresses non-lung pixels while preserving 3-channel input shape.
binary/mobilenetMask-as-RGB imagefloat32 identity / MobileNet styleLightweight ensemble member with masked input.
diseases/densenetLung ROI cropDenseNet preprocessingRuns only after binary ensemble says unhealthy.
FastAPI Application

API Startup, Prediction Routes, and UI Mount

The app is a real service layer with startup validation, warmup, prediction endpoints, logging, static assets, and a React SPA.

FastAPI initializes the detector during lifespan startup. Startup checks local or Hugging Face model availability, validates every model metadata file, initializes database tables if logging is enabled, initializes Kafka if configured, creates the detector, and performs a warmup inference with a dummy 256x256 image.

RoutePurposeImportant Behavior
GET /healthHealth checkReturns service status and API version.
GET /Root statusReturns service name, status, and version.
POST /predictJSON inferenceAccepts exactly one of image path, base64, or public URL.
POST /predict/uploadMultipart inferenceAccepts uploaded image file and returns the same prediction schema.
GET /logsPrediction historyRequires API key, supports limit and offset, returns DB-backed logs.
/uiReact frontendMounted as a static SPA with fallback to index.html.
/staticArtifactsServes prediction images and JSON mapping assets.
API Dependencies

Serving Libraries & Dependencies

The API section has its own deployment stack separate from model training.

FastAPI + UvicornServe prediction, upload, health, logs, static files, and frontend routes on port 8000 or Hugging Face port 7860.
Pydantic v2Validates request payloads and enforces exactly one input source for JSON predictions.
React SPA frontendThe built frontend provides upload/path/base64 modes, diagnosis gauge, ensemble matrix, visual artifact grid, docs, and API examples.
Logs API keyThe prediction endpoints are demo-open, while /logs requires X-Api-Key when log access is enabled.
CORS configurationCORS_ALLOW_ORIGINS supports GitHub Pages or other frontend origins in cloud deployment.
API Code

Prediction and Response Contract

The same inference service powers JSON and upload flows, then optionally sends results to Kafka or writes directly to the DB.

routes.py
@router.post("/predict", response_model=PredictResponse)
def predict_json(request: Request, req: PredictRequest, return_all: bool = True):
    request_id = str(uuid4())
    response = run_inference(
        detector=request.app.state.detector,
        image_path=req.image_path,
        image_base64=req.image_base64,
        image_url=req.image_url,
        return_all=return_all,
    )
    if AppConfig.KAFKA_ENABLED:
        publish_prediction_event(request_id=request_id, event=event)
    elif AppConfig.DB_LOGGING_ENABLED:
        persist_prediction_log(request_id=request_id, response=response)
    return response
PredictResponse
final_prob: float
final_probs_by_label: dict[str, float]
final_label: int
final_label_name: str | None
models_results: dict[str, ModelResult] | None
disease: DiseaseResult | None
source_url, mask_url, roi_url, overlay_url
source_path, mask_path, roi_path, overlay_path
Frontend

React Inference UI

The deployed UI is a browser-facing medical AI dashboard rather than a notebook-only demo.

Input modesUpload file, image URL, or base64/path-style API examples.
Result surfaceFinal diagnosis gauge, healthy/pathology probabilities, clinical note, and disease sub-scores.
Vision outputsSource, predicted mask, ROI crop, and overlay images are shown and can be opened in a lightbox.
Ensemble visibilityPer-model binary probabilities are displayed so disagreement is visible to the user.
Documentation pagesArchitecture, dataset, training, and API integration docs are bundled into the frontend.
Safety copyThe interface frames the output as AI assistance rather than a clinical diagnosis.
Service Internals

Error Handling, Artifacts, and API Documentation

The API layer includes user-facing docs, exception handling, artifact routing, and configuration switches for local and cloud deployment.

Exception Handlers
API safety

The service registers custom exception handlers so validation errors, inference failures, and unexpected runtime issues return structured responses instead of raw stack traces.

Artifact Router
visual outputs

Prediction source, mask, ROI, and overlay images are saved under date-based folders and returned as local URLs or Supabase signed URLs depending on configuration.

Frontend Docs
/ui/docs

The React UI contains pages for architecture, dataset, training, and integration so users can inspect how the model works without opening notebooks.

Config Switches
env driven

Runtime behavior is controlled by environment variables: ONNX vs Keras, Kafka on/off, DB on/off, HF download, storage backend, CORS, and API port.

artifact storage
prediction_id = uuid4().hex
date_folder = utc_today()

save:
  assets/predictions/YYYY-MM-DD/<prediction_id>/source.png
  assets/predictions/YYYY-MM-DD/<prediction_id>/mask.png
  assets/predictions/YYYY-MM-DD/<prediction_id>/roi.png
  assets/predictions/YYYY-MM-DD/<prediction_id>/overlay.png

return:
  *_path for local persistence
  *_url for browser display or Supabase signed URLs
frontend route map
/ui/app
/ui/docs/architecture
/ui/docs/dataset
/ui/docs/training
/ui/docs/integration

Main UI:
  upload panel
  diagnosis gauge
  source/mask/ROI/overlay grid
  ensemble model matrix
  clinical note and safety warning
MLOps Overview

Docker, Kafka, MLflow, Airflow, Databases, and Cloud Runtime

The operations layer covers local full-stack development, slim cloud runtime, event consumers, registry backfill, scheduled retraining, and model downloads.

ModeCompose / RuntimeServicesPurpose
finaldocker-compose.final.ymlApp, SQL Server, Kafka, Kafka UI, MLflow, Airflow, consumersComplete local production-like topology.
appdocker-compose.app.ymlApp, SQL Server, DB init, consumersApplication stack without separate MLflow/Airflow compose.
runtimedocker-compose.runtime.ymlSlim ONNX app + PostgresCloud-style deployment with Kafka and MLflow off.
mlopsdocker-compose.mlops.ymlMLflow, Airflow, SQL Server, Airflow PostgresRegistry, backfill, evaluation, and scheduled retraining.
kafkadocker-compose.kafka.ymlZookeeper, Kafka, Kafka UIEvent pipeline development and inspection.
HF Spaces.env.hf_spaces.templateONNX app, HF model download, Postgres, SupabasePublic deploy with external model/artifact services.
Operations Dependencies

MLOps & Deployment Libraries

These dependencies belong to deployment and operations, not the training notebooks.

Docker + Docker ComposePackage runtime images, SQL Server/Postgres, Kafka, MLflow, Airflow, app volumes, healthchecks, and environment-specific profiles.
SQLAlchemy + SQL Server/PostgresSupport both local SQL Server logging and cloud Postgres logging through normalized database URLs.
Confluent KafkaPublishes prediction events and powers DB, monitoring, analytics, doctor-image, and notification consumers.
MLflowBackfills notebook artifacts, logs metadata and metrics, registers six model specs, and supports production alias loading.
AirflowSchedules monthly model evaluation, retraining DAGs, and a full orchestrated retraining pipeline.
Hugging Face Hub + SupabaseDownload model artifacts at startup and optionally store prediction images as signed cloud URLs.
Database Design

Prediction Logging Schema

The database stores the final result, per-model ensemble decisions, disease outputs, and image artifact links.

TableMain FieldsReason
prediction_requestsrequest_id, input_type, final_label, final_prob, final_probs_json, errors, created_atCore prediction history.
prediction_binary_model_resultsmodel_name, label, label_name, prob, probs_jsonPer-model ensemble audit trail.
prediction_disease_resultslabel, label_name, probs_jsonStores disease classifier outputs when unhealthy.
prediction_image_linkssource, mask, ROI, overlay URLs and pathsConnects logs to generated medical visualization artifacts.
crud.py
record = PredictionRequest(
    request_id=request_id,
    input_type=input_type,
    final_label=response["final_label"],
    final_label_name=response.get("final_label_name"),
    final_prob=response["final_prob"],
    final_probs_json=json.dumps(final_probs),
)
db.add(record)
db.flush()
db.add(PredictionImageLink(...))
db.add(PredictionBinaryModelResult(...))
db.add(PredictionDiseaseResult(...))
database modes
DB_BACKEND=mssql      # local full stack
DB_BACKEND=postgres   # runtime / cloud
DATABASE_URL=...      # provider-injected URL wins
DB_LOGGING_ENABLED=true
LOGS_API_KEY=...
Kafka Event Pipeline

Prediction Event Consumers

Kafka decouples inference from logging, monitoring, analytics, doctor-image queues, and notifications.

API
ProducerPublishes prediction.completed events to lung.predictions with request id, input type, timestamp, and response payload.
DB
consumer_dbConsumes events and writes structured records into prediction tables.
MON
consumer_monitoringMaintains a five-minute window with request count, unhealthy rate, and average final probability.
ANA
consumer_analyticsWrites analytics JSONL with final label, best model, model count, and disease label.
DOC
consumer_doctor_imagesCreates a doctor queue containing source, mask, ROI, and overlay artifact links.
MSG
consumer_notificationsFormats notification messages with final label, confidence, and disease prediction when present.
MLflow + Airflow

Registry Backfill, Monthly Evaluation, and Retraining

MLOps jobs make notebook work traceable and connect saved models to a registry-oriented deployment path.

Backfill
Post-hoc notebook backfillLogs notebook artifacts, metadata YAML, Optuna JSON, extracted notebook params, reported metrics, and optionally validation metrics to MLflow.
Models6
Run Typepost_hoc
Registryoptional
Monthly
Monthly evaluation loggingEvaluates registered/local models on TFRecords and logs metrics, params, dataset id, metadata, and run summaries.
Schedule@monthly
Batch16
Val Ratio0.2
Retrain
Monthly retraining pipelineLoads current model, splits TFRecords, retrains, evaluates, logs to MLflow, registers model, and promotes only if the promotion metric improves.
Epochs20
StageProduction
Promotemetric
Orchestrate
Airflow DAG dependency orderSegmentation retraining runs first, then four binary models run in parallel, then disease classification runs last.
DAGmonthly
Tasks6
Catchupfalse
Makefile targets
make mlops-up
make backfill
make backfill-eval
make final-up
make runtime-up

docker.ps1 runtime-build
docker.ps1 final-logs
docker.ps1 mlops-ps
orchestrate_retrain_pipeline.py
start >> trigger_segmentation
trigger_segmentation >> [
    trigger_binary_densenet,
    trigger_binary_efficientnet,
    trigger_binary_inception,
    trigger_binary_mobilenet,
] >> binary_done
binary_done >> trigger_diseases >> end
Cloud Runtime

Hugging Face, Postgres, and Supabase Path

The slim runtime removes Kafka/MLflow from the serving path and downloads ONNX artifacts when needed.

HF model downloadHF_MODEL_DOWNLOAD_ENABLED=true downloads required model folders from a1mohamadd/lung-disease-detection.
ONNX runtimeMODEL_RUNTIME=onnx keeps cloud inference CPU-friendly and avoids TensorFlow model loading in the slim image.
Postgres loggingCloud runtime uses DB_BACKEND=postgres or provider-injected DATABASE_URL.
Supabase artifactsPrediction source, mask, ROI, and overlay images can be uploaded to Supabase Storage with signed URLs.
Hugging Face SpacesPORT=7860, Kafka off, MLflow off, model download on, external DB and storage configured by secrets.
GitHub Pages frontendCORS is configured through CORS_ALLOW_ORIGINS so a static frontend can call the backend safely.
Registry Contracts

MLflow Model Specs and Promotion Metrics

The MLOps scripts know the six model families by explicit specs, so registry backfill and monthly evaluation are repeatable.

Registered NameTaskPromotion MetricArtifact Source
lung-binary-densenetHealthy/unhealthyval_f1DenseNet binary notebook + metadata.
lung-binary-efficientnetHealthy/unhealthyval_f1EfficientNetV2B3 binary notebook + metadata.
lung-binary-inceptionHealthy/unhealthyval_f1InceptionV3 binary notebook + metadata.
lung-binary-mobilenetHealthy/unhealthyval_f1MobileNetV3 binary notebook + metadata.
lung-diseases-densenetDisease classificationval_f1Disease DenseNet notebook + Optuna JSON.
lung-segmentation-unet-xceptionLung segmentationdice_coefficientSegmentation notebook + custom Dice metric.
post_hoc_backfill.py
for spec in POST_HOC_SPECS:
    with mlflow.start_run(run_name=spec.model_name):
        log_notebook(spec.notebook_path)
        log_artifacts(spec.support_files)
        log_optuna_json(spec.optuna_json)
        log_metadata_yaml(spec.metadata_path)
        log_params(extracted_params)
        log_metrics(reported_metrics)
        if register:
            mlflow.register_model(model_uri, spec.registered_name)
monthly_retrain.py
current = load_registered_or_local_model(spec)
train_ds, val_ds = split_tfrecords(tfrecord_dir)
history = current.fit(train_ds, validation_data=val_ds)
metrics = evaluate(current, val_ds)

if metrics[spec.promotion_metric] > previous_best:
    register_model(current, spec.registered_name)
    transition_or_alias_to_production(current)
Compose and Environment Design

Full Stack vs Runtime Stack

The deployment files separate heavy local MLOps infrastructure from the lean serving path used for cloud deployment.

Full Stack Compose
final

docker-compose.final.yml runs the app, SQL Server, Kafka, Kafka UI, MLflow, Airflow, DB init, post-hoc backfill profile, and all consumers for a local production-like lab.

Runtime Compose
slim

docker-compose.runtime.yml runs a smaller ONNX app image with Postgres. Kafka and MLflow are disabled by default to keep inference deployment practical.

Runtime Image
Dockerfile.runtime

The runtime Dockerfile installs runtime dependencies, copies app/frontend/assets, avoids baking all saved models into the image, and lets HF model download fill artifacts at startup.

PowerShell + Make
dev UX

The project includes both Makefile targets and docker.ps1 switches so local workflows can be started, stopped, inspected, and rebuilt consistently on Windows or Unix-like shells.

Env TemplatePrimary UseKey Switches
.env.templateLocal app defaultsSQL Server backend, Kafka, MLflow, Supabase toggles.
.env.compose.templateFull compose stackContainer hostnames, SQL Server credentials, Kafka brokers, MLflow URI.
.env.runtime.templateSlim ONNX runtimeMODEL_RUNTIME=onnx, Kafka off, MLflow off, Postgres DB, HF download on.
.env.hf_spaces.templateHugging Face SpacePORT=7860, external Postgres, Supabase storage, CORS origins, HF model repo.
CI/CD Libraries

Automation Libraries & Dependencies

The CI/CD tab starts with the tools that keep testing, packaging, and deployment repeatable.

GitHub ActionsRuns pull-request checks, main-branch release jobs, workflow dispatch, concurrency control, Docker build, and Hugging Face Space deployment.
Python 3.11Matches the CI runtime used by actions/setup-python@v5 and keeps the lightweight deployment test environment consistent.
pytest + pytest-covExecutes route, unit, service, contract, and MLOps tests while reporting coverage for app and kafka_pipeline.
FastAPI TestClientTests the health, JSON prediction, and upload endpoints without starting an external web server.
Docker BuildValidates that deployment/Dockerfile.runtime can package the slim ONNX-oriented runtime used by the cloud API.
Hugging Face CLIUploads the prepared runtime bundle to the Space and turns a successful main-branch build into a deployed API release.
curl health checkPolls the deployed /health endpoint so the workflow proves the Space became healthy after upload.
Quality Gate

Tests and CI/CD Pipeline

The CI/CD layer turns the project from a working prototype into a repeatable deployment workflow with automated checks before release.

Pull Request Safety
tests first

Every pull request runs the lightweight pytest suite before code can be merged. This protects API routes, preprocessing, model contracts, inference behavior, and MLOps utilities from accidental regressions.

Push to Main
release path

After tests pass on the main branch, CI builds the slim runtime Docker image, prepares the Hugging Face Space bundle, uploads it, and waits for the deployed health endpoint to respond.

External Services Off
mocked

CI disables database logging, Kafka, MLflow, and Hugging Face model download so tests remain fast, deterministic, and safe to run without production secrets.

Deployment Proof
/health

The final deployment step polls the Hugging Face Space health endpoint. A release is considered complete only after the hosted API becomes healthy.

Test Coverage

What the Automated Tests Validate

The test suite focuses on the runtime pieces most likely to break production behavior.

Test AreaFilesPurpose
API routestests/api/test_routes.pyChecks health, prediction route behavior, validation errors, and API response structure.
Unit logictests/unit/Validates request parsing, preprocessing helpers, pipeline composition, and ensemble decision logic.
Inference servicetests/services/test_inference_service.pyTests runtime inference behavior without depending on large external model downloads.
Model contractstests/contracts/test_model_contracts.pyProtects metadata, class mapping, model output labels, and deployment contract assumptions.
MLOps utilitiestests/mlops/Checks ONNX release packaging and reviewed-data ingestion helpers used by operations workflows.
tests/api/test_routes.py
def test_health_endpoint():
    response = _client().get("/health")

    assert response.status_code == 200
    assert response.json() == {
        "name": None,
        "status": "ok",
        "version": "1.0.0",
    }

@patch("app.api.routes.run_inference")
def test_predict_endpoint_passes_json_input_to_inference(run_inference):
    run_inference.return_value = _prediction_response()

    response = _client().post(
        "/predict?return_all=false",
        json={"image_url": "https://example.com/xray.png"},
    )

    assert response.status_code == 200
    assert response.json()["final_label_name"] == "Healthy"
tests/contracts/test_model_contracts.py
def test_checked_in_class_mappings_match_api_labels():
    classification = json.loads(AppConfig.CLASSIFICATION_JSON.read_text())
    diseases = json.loads(AppConfig.DISEASES_JSON.read_text())

    assert classification == {"0": "Healthy", "1": "Unhealthy"}
    assert diseases == {
        "0": "COVID",
        "1": "Viral Pneumonia",
        "2": "Lung Opacity",
    }

def test_onnx_path_prefers_explicit_name_and_has_keras_fallback(tmp_path):
    explicit = get_onnx_model_path(
        tmp_path,
        {"model": {"path": "model.keras", "onnx_path": "export/model.onnx"}},
    )
    fallback = get_onnx_model_path(tmp_path, {"model": {"path": "model.keras"}})

    assert explicit == tmp_path / "export" / "model.onnx"
    assert fallback == tmp_path / "model.onnx"
GitHub Actions

CI/CD Job Flow

The workflow separates validation, image build, and production deployment into explicit dependent jobs.

1
TestsRuns on pull requests and pushes to main. Installs test dependencies in the deployment folder and runs pytest with coverage for app and Kafka pipeline code.
Python3.11
Commandpytest
TriggerPR/push
2
Runtime Docker buildRuns only on pushes after tests pass. Builds the Hugging Face runtime image from deployment/Dockerfile.runtime.
Needstests
Imageruntime
Branchmain
3
Deploy Hugging Face SpaceRuns only after tests and Docker build pass. Creates a space bundle, uploads it with the Hugging Face CLI, and checks the deployed /health endpoint.
SecretHF_TOKEN
TargetSpace
Gatehealth
.github/workflows/ci.yml
name: CI/CD

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  tests:
    name: Tests
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: deployment
    env:
      DB_LOGGING_ENABLED: "false"
      KAFKA_ENABLED: "false"
      MLFLOW_ENABLED: "false"
      HF_MODEL_DOWNLOAD_ENABLED: "false"
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
          cache: pip
      - name: Install test dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements-test.txt
      - name: Run lightweight test suite
        run: pytest --cov=app --cov=kafka_pipeline --cov-report=term-missing

  docker-build:
    if: github.event_name == 'push'
    needs: tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build Hugging Face runtime image
        run: docker build --file deployment/Dockerfile.runtime --tag lung-detection-runtime:${{ github.sha }} deployment

  deploy-space:
    if: github.event_name == 'push'
    needs: [tests, docker-build]
    runs-on: ubuntu-latest
    environment: production
    env:
      HF_TOKEN: ${{ secrets.HF_TOKEN }}
      HF_SPACE_ID: a1mohamadd/lung-disease-detection-api
      SPACE_HEALTH_URL: https://a1mohamadd-lung-disease-detection-api.hf.space/health
    steps:
      - uses: actions/checkout@v4
      - name: Install Hugging Face CLI
        run: pip install huggingface-hub==1.2.1
      - name: Prepare Space bundle
        run: |
          mkdir -p space-bundle/assets
          cp deployment/Dockerfile.runtime space-bundle/Dockerfile
          cp deployment/requirements-runtime.txt space-bundle/requirements-runtime.txt
          cp -R deployment/app space-bundle/app
          cp -R deployment/kafka_pipeline space-bundle/kafka_pipeline
          cp -R deployment/frontend space-bundle/frontend
          cp deployment/assets/*.json space-bundle/assets/
      - name: Upload runtime to Hugging Face Space
        run: hf upload "$HF_SPACE_ID" space-bundle . --repo-type space --commit-message "Deploy GitHub commit $GITHUB_SHA"
      - name: Wait for deployed API
        run: |
          for attempt in $(seq 1 45); do
            if curl --fail --silent --show-error "$SPACE_HEALTH_URL"; then
              echo "Space health check passed."
              exit 0
            fi
            sleep 20
          done
          exit 1
Commands and Environment

Local and CI Execution Pattern

The same test command can run locally, while CI adds deterministic environment flags and deployment-only jobs.

local test command
cd deployment
python -m pip install -r requirements-test.txt
pytest --cov=app --cov=kafka_pipeline --cov-report=term-missing
ci safety switches
DB_LOGGING_ENABLED=false
KAFKA_ENABLED=false
MLFLOW_ENABLED=false
HF_MODEL_DOWNLOAD_ENABLED=false

HF_TOKEN=GitHub secret used only by the deploy job
Pipeline RuleWhy It MattersProject Impact
Tests before buildStops broken runtime code before image creation.Protects API and inference behavior.
Build before deployConfirms the runtime Dockerfile and dependencies still package correctly.Protects Hugging Face Space release quality.
Secrets only in deployKeeps pull-request validation independent from production credentials.Safer open collaboration and repeatable checks.
Health check after uploadVerifies the hosted service starts successfully after deployment.Turns deployment into a measurable release gate.
Conclusion

Project Synthesis & Roadmap

A complete medical AI portfolio system: not only a model, but a research-to-operations platform.

Research result: the project builds a multi-model chest X-ray system from 21,165 image/mask pairs, with TFRecords, transfer learning, Optuna search, segmentation, healthy/unhealthy ensemble voting, and disease sub-classification.

Deployment result: the platform exposes the model through FastAPI and a React UI, saves source/mask/ROI/overlay artifacts, logs prediction history, supports ONNX CPU inference, and can download artifacts from Hugging Face at startup.

MLOps result: Docker Compose, SQL Server/Postgres, Kafka consumers, MLflow registry backfill, Airflow monthly evaluation/retraining, and cloud env templates make the system reproducible beyond notebooks.

Future directions:

  • Add formal medical validation, external test sets, calibration reports, and confidence intervals.
  • Add drift monitoring for scanner/source distribution and per-class disease performance.
  • Expose a safer clinician-review workflow with audit trails and model version display.
  • Move secrets and deployment targets fully into managed cloud infrastructure.
Amir Mohamad Askari · Lung Disease Detection · 2026