CNN Image Classification on DICOM Data (Research Example)
Solution for the use case Medical Image Classification for Research and Education.
- AI tools
- TensorFlow, Keras, ResNet50 (Keras Applications), pydicom, Grad-CAM
- Language
- Python
- Published
- 2025-07-08
Approach
Research and education example. Not a medical device, not for diagnosis or clinical decisions.
Shows the technical building blocks of a medical image classification experiment with TensorFlow/Keras and pydicom:
- DICOM loading: rescale slope and intercept, windowing (Window Center/Width or percentiles), MONOCHROME1 inversion, resize, normalization to [0, 1].
- Removal of direct identifiers (patient name, ID, institution, private tags) before data enters an experiment. This is not a complete de-identification profile.
- Two binary classifiers: a small CNN that trains on CPU in seconds, and transfer learning with an ImageNet-pretrained ResNet50 backbone and a new head.
- Evaluation with sensitivity and specificity on a held-out split, and Grad-CAM heatmaps to inspect which image regions drive the output.
The tests use synthetic DICOM files and synthetic images (a bright blob as the "finding"), not patient data.
Implementation steps
- Get a dataset with a license that allows your use, and approval where your institution requires it. Keep patient data out of the repository.
- Remove direct identifiers with deidentify() or, better, a vetted de-identification tool, and split the data by patient, not by image.
- Load each DICOM file with pydicom: apply rescale slope/intercept, window to [0, 1], invert MONOCHROME1 images and resize to the model input size.
- Start with the small CNN to check the pipeline end to end, then switch to the ResNet50 transfer model with a frozen backbone.
- Train with mild augmentation (small rotation, zoom, contrast; no horizontal flip) and early stopping on a validation split.
- Report sensitivity and specificity on the held-out test split and choose the threshold for the trade-off your study needs.
- Inspect Grad-CAM heatmaps for a sample of images to find shortcuts such as text markers or device artifacts.
- Document data source, splits, metrics and known limitations; an external validation set from another site is needed before any claim about generalization.
Code (Python)
"""Two model options for binary image classification (e.g. "finding" vs. "no finding").
- small_cnn: trains from scratch on CPU in seconds; used in the tests.
- resnet50_transfer: ImageNet-pretrained ResNet50 backbone (weights are
downloaded by Keras on first use) with a new classification head.
Research/education code: not a medical device, not for clinical use.
"""
from __future__ import annotations
import keras
from keras import layers
def augmentation() -> keras.Sequential:
# Mild geometric/contrast changes. No horizontal flip: anatomy is not symmetric.
return keras.Sequential([layers.RandomRotation(0.03), layers.RandomZoom(0.1), layers.RandomContrast(0.1)],
name="augmentation")
def small_cnn(size: int = 128) -> keras.Model:
inputs = keras.Input((size, size, 1))
x = augmentation()(inputs)
for filters in (16, 32, 64):
x = layers.Conv2D(filters, 3, padding="same", activation="relu")(x)
x = layers.MaxPooling2D()(x)
x = layers.Conv2D(64, 3, padding="same", activation="relu", name="last_conv")(x)
x = layers.GlobalMaxPooling2D()(x) # a small local finding should count, wherever it is
x = layers.Dropout(0.2)(x)
outputs = layers.Dense(1, activation="sigmoid")(x)
return _compile(keras.Model(inputs, outputs, name="small_cnn"))
def resnet50_transfer(size: int = 224, weights: str | None = "imagenet", train_backbone: bool = False) -> keras.Model:
inputs = keras.Input((size, size, 1))
x = augmentation()(inputs)
x = layers.Concatenate()([x, x, x]) # grayscale -> 3 channels for the ImageNet backbone
x = layers.Rescaling(255.0)(x) # inputs are in [0, 1]; ResNet preprocessing expects [0, 255]
x = keras.applications.resnet.preprocess_input(x)
backbone = keras.applications.ResNet50(include_top=False, weights=weights, input_shape=(size, size, 3))
backbone.trainable = train_backbone # start with a frozen backbone, fine-tune later
x = backbone(x, training=False)
x = layers.GlobalAveragePooling2D()(x)
x = layers.Dropout(0.3)(x)
outputs = layers.Dense(1, activation="sigmoid")(x)
return _compile(keras.Model(inputs, outputs, name="resnet50_transfer"))
def _compile(model: keras.Model, lr: float = 1e-3) -> keras.Model:
model.compile(optimizer=keras.optimizers.Adam(lr), loss="binary_crossentropy",
metrics=[keras.metrics.AUC(name="auc"), keras.metrics.Recall(name="sensitivity")])
return model
Performance
Expectations, not measured results:
- The small CNN trains on 160 synthetic 48 x 48 images on a CPU in well under a minute; the tests require at least 85 % accuracy on that synthetic task, which says nothing about medical images.
- ResNet50 at 224 x 224 needs a GPU for training on real dataset sizes; inference on a CPU takes a fraction of a second per image.
- Report sensitivity and specificity with confidence intervals on a held-out, patient-level split of a public or approved dataset.
Lessons learned
A researcher runs one command to train and gets a JSON report with sensitivity, specificity and accuracy. A Grad-CAM heatmap per image shows where the model looked. There is no clinical user interface: outputs are for experiments and teaching, not for patient care.