🩺 Medical Scan Classifier — How a CNN Reads an Image
Watch a convolutional filter scan a synthetic medical scan, extract features, and output a diagnosis probability — with a live ROC curve showing the sensitivity/specificity tradeoff.
About the Medical Scan Classifier
This simulation shows, step by step, how a convolutional neural network turns a grid of pixel intensities into a diagnosis probability. A synthetic 24×24 "scan" is generated from one of three presets — Normal (pure noise), Early lesion (a faint elevated-intensity blob), or Advanced lesion (a strong blob) — and a fixed 3×3 kernel slides across it, producing a feature map. The feature map is max-pooled to a single activation and passed through a sigmoid to produce P(lesion), exactly the forward-pass mechanics used by real CNNs (minus the training).
Run a batch of randomly generated cases to build a confusion matrix and a receiver operating characteristic (ROC) curve, and drag the diagnosis threshold to see the sensitivity/specificity tradeoff that every real-world screening tool must navigate.
Frequently Asked Questions
What exactly is the convolution kernel doing here?
The blob-detector kernel is a small 3×3 weighted average (weights roughly [[1,2,1],[2,4,2],[1,2,1]]/16, a discrete Gaussian) that gets centred on every pixel in turn. At each position it multiplies the 9 pixels under it by the matching kernel weights and sums the result, producing one output value in the feature map. Because the weights are all positive and largest in the centre, this kernel responds strongly wherever a patch of the scan is uniformly bright — exactly the signature of a lesion blob — while mostly cancelling out isolated single-pixel noise.
Why max-pool instead of average-pool the feature map?
Global average pooling would dilute a small but intense lesion blob across all 576 cells of the feature map, since most of the scan is background noise. Global max pooling instead asks "what is the single strongest activation anywhere in the image?" — which is the right question for detecting a localised abnormality that could be anywhere in the field of view. This mirrors why real CNN classifiers for localised pathology often favour max- or attention-based pooling over plain averaging.
Why does moving the threshold slider change sensitivity and specificity in opposite directions?
Sensitivity (true positive rate) is the fraction of actual lesion cases the model flags positive; specificity (true negative rate) is the fraction of actual normal cases it correctly leaves negative. Lowering the threshold makes the model call more cases positive overall — catching more true lesions (higher sensitivity) but also misclassifying more normal scans as positive (lower specificity). Raising the threshold does the reverse. This tradeoff is fundamental to any binary classifier with overlapping score distributions; you cannot improve both by moving the threshold alone.
What does the ROC curve actually plot, and what is AUC?
The ROC (receiver operating characteristic) curve plots true positive rate (sensitivity) against false positive rate (1 − specificity) as the decision threshold sweeps from 1 down to 0. Each point on the curve corresponds to one possible threshold. A random guesser traces the diagonal line; a perfect classifier hugs the top-left corner. The area under the curve (AUC) summarises performance across all thresholds at once — an AUC of 0.5 means no better than chance, 1.0 means perfect separation. Because this simulation adds random per-case noise and randomises the blob's amplitude and position, the curve you see is realistically imperfect rather than a trivial step function.
Is this actually a trained neural network?
No — and that is deliberate. The kernels used here are fixed, hand-chosen weights, not the result of gradient descent on labelled training data. A real diagnostic CNN would learn its convolution weights (often across dozens of layers and hundreds of channels) by backpropagating a loss computed against thousands of expert-labelled scans. What this simulation does reproduce faithfully is the forward pass: convolution → feature map → pooling → sigmoid → probability, which is the same computational pipeline a trained network executes at inference time, just with weights chosen by hand instead of learned.
Why is the "Normal" preset not simply flat/zero?
Real imaging sensors always have background noise, and real anatomy has natural texture even when healthy. Modelling "Normal" as pure noise around a low baseline intensity (rather than a blank grid) forces the classifier to distinguish a genuine elevated-intensity structure from ordinary pixel-to-pixel variation — which is the actual challenge medical image classifiers face, and the reason a naive brightness threshold on raw pixels performs far worse than a convolution-based feature detector.
Why does the confusion matrix change even though I haven't run a new batch?
The confusion matrix and sensitivity/specificity/accuracy figures are recomputed instantly whenever you move the threshold slider, using the diagnosis probability already stored for every case run so far. Only the underlying probabilities require a batch run (or single case) to generate; classifying them as positive or negative against a given cutoff is just a comparison, so it can be recalculated live without regenerating any scans.
Why do Early and Advanced lesion cases sometimes get different scores even though the preset is the same?
Each case redraws its blob's amplitude, position and width from a random distribution around the preset's typical values, and adds fresh pixel noise. This mirrors real biological and imaging variability — no two real scans of the same pathology look identical. It's also what makes the ROC curve meaningful: if every "Early" case scored identically, sensitivity and specificity would jump in discrete steps rather than tracing a smooth curve.
Watch a convolutional filter scan a synthetic medical scan, extract features, and output a diagnosis probability — with a live ROC curve.
3D · Three.js / WebGL renderer · 60 FPS target · runs fully client-side, no install