Applied ML research
2025 to 2026Diabetic foot ulcer patch classification
Does a classifier trained on photographs from one source still work on photographs from another? It does not, and the direction of failure flips depending on which way you cross.
Scope, stated first
This work classifies small cropped photographs of skin as healthy or existing ulcer. It does not locate ulcers in whole foot photographs, it does not predict future ulceration, and it is not a diagnostic tool.
MSc research and development project at the University of Reading, supervised by Prof. Slawomir Nasuto, with clinical advisory input from Nick Galogly, Orthotics, Royal Berkshire Hospital. That input was advisory only. No NHS data, patients or systems were involved, and all images came from public research datasets.
The finding: the failure direction flips
Two independent public datasets. Dataset A is Kaggle DFU patches captured on consumer devices, a Galaxy Note 8 and an iPad. Dataset B is DFUNet Part A, captured by a different research group at a different clinic on a Nikon D3300 with a 40 mm macro lens. I ran all four train and test combinations under five fold cross validation, at the default 0.5 decision threshold, with sensitivity as the primary metric.
Crossing sources does not simply make everything worse. The kind of error changes with the direction of travel. A to B loses sensitivity, missing roughly 310 of 1,038 ulcer patches. B to A keeps almost every ulcer and loses specificity instead, flagging about one healthy patch in three. Those two failure modes carry very different costs, and a single summary score hides the difference completely.
| Train to test | Sensitivity | Specificity | ROC-AUC |
|---|---|---|---|
| A to A (same source) | 0.966 ± 0.026 | 0.975 ± 0.023 | 0.996 |
| A to B (cross source) | 0.701 ± 0.063 | 0.942 ± 0.021 | 0.908 |
| B to B (same source) | 0.945 ± 0.023 | 0.822 ± 0.027 | 0.963 |
| B to A (cross source) | 0.998 ± 0.002 | 0.684 ± 0.071 | 0.988 |
Five fold cross validation, decision threshold 0.5
Why: the scores move, the decision line does not
The model gives each patch a score between 0 and 1, and anything above 0.5 is called an ulcer. Changing the image source shifts those score distributions, while the 0.5 line stays where it is.
Ranking degrades far less than the operating point does. ROC-AUC falls from 0.996 within source to 0.908 across, a real decline but a small one next to the collapse in sensitivity. A large part of the gap is therefore a threshold problem rather than a failure of the learned features, though the AUC decline and the controls below show it is not purely that.
Two remedies, priced honestly
Moving the threshold after the fact works well in one direction. For B to A, shifting the line from 0.5 to 0.694 takes specificity from 0.684 to 0.946 while sensitivity falls only from 0.998 to 0.965. For A to B, moving to 0.414 lifts sensitivity from 0.701 to 0.806: better, but not recovered. Both thresholds were selected by looking at the evaluation labels, so they show what might be achievable with a proper per site calibration set, not a validated result.
Training on both sources is the stronger fix. A pooled model held 0.985 sensitivity and 0.954 specificity on held out Dataset A, and 0.924 and 0.880 on held out Dataset B. Nothing collapses in either direction.
Building evidence worth trusting
A data audit found that 32 per cent of Dataset A files were exact byte level duplicates. Removing them cut 1,055 files to 713 unique images, 240 healthy and 473 ulcer, and every figure above uses the cleaned set. No byte identical file appears in both datasets, though without patient identifiers, near duplicate or same patient overlap cannot be ruled out.
Dataset B's 1,679 patches were cropped from 665 source photographs, so splits are grouped and patches from one original photograph never straddle train and test. Dataset A carries no patient or source identifiers, so its own within source score is likely optimistic. That is stated rather than quoted as a headline.
Controls check that the numbers mean something. A network with random, untrained features still reaches ROC-AUC 0.934 within source but only 0.604 across, so a high in domain score partly reflects an easy dataset rather than a capable model. Shuffled labels collapse to about 0.6, as they should.
Alternatives were tested and rejected. DenseNet121 at 0.855 and EfficientNet-B0 at 0.838 transferred worse than ResNet50 at 0.908 on cross source AUC. Unfreezing the final block pushed within source AUC to roughly 1.000 while cross source sensitivity stayed at 0.691, a clean demonstration that in domain gains need not transfer.
One caveat I found in my own code: the backbone weights were frozen, but the BatchNorm running statistics were still adapting to the training source, which is a quiet form of fitting to the domain. It is written up as a limitation, and a locked BatchNorm rerun is scripted as the next step.
Limits
Patch level only. Nothing here shows that a user could photograph a whole foot and have the system find an ulcer. There are two datasets, no independent third site and no prospective evaluation.
Camera, clinic, patients and cropping all differ between the sources, so no single cause of the shift can be isolated. Neither dataset carries demographic or skin tone metadata, so performance across skin tones cannot be measured. The five cross validation folds share training data, so they are linked runs rather than independent experiments, and no formal significance is claimed.

Next step
Get in touch.
Want to talk about a role, a project or any of the work here? Get in touch and I will reply within a working day.
Available from September 2026 for full-time roles and selected freelance projects.