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

  1. Get a dataset with a license that allows your use, and approval where your institution requires it. Keep patient data out of the repository.
  2. Remove direct identifiers with deidentify() or, better, a vetted de-identification tool, and split the data by patient, not by image.
  3. Load each DICOM file with pydicom: apply rescale slope/intercept, window to [0, 1], invert MONOCHROME1 images and resize to the model input size.
  4. Start with the small CNN to check the pipeline end to end, then switch to the ResNet50 transfer model with a frozen backbone.
  5. Train with mild augmentation (small rotation, zoom, contrast; no horizontal flip) and early stopping on a validation split.
  6. Report sensitivity and specificity on the held-out test split and choose the threshold for the trade-off your study needs.
  7. Inspect Grad-CAM heatmaps for a sample of images to find shortcuts such as text markers or device artifacts.
  8. 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.

Back to the use case · All AI use cases