<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Medical Imaging |</title><link>https://72a35e7c.drgabrielae.pages.dev/tags/medical-imaging/</link><atom:link href="https://72a35e7c.drgabrielae.pages.dev/tags/medical-imaging/index.xml" rel="self" type="application/rss+xml"/><description>Medical Imaging</description><generator>HugoBlox Kit (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Tue, 03 Sep 2019 00:00:00 +0000</lastBuildDate><image><url>https://72a35e7c.drgabrielae.pages.dev/media/icon_hu_201ab77eed7e3f5f.png</url><title>Medical Imaging</title><link>https://72a35e7c.drgabrielae.pages.dev/tags/medical-imaging/</link></image><item><title>APTOS 2019 Blindness Detection</title><link>https://72a35e7c.drgabrielae.pages.dev/projects/aptos-2019-blindness-detection/</link><pubDate>Tue, 03 Sep 2019 00:00:00 +0000</pubDate><guid>https://72a35e7c.drgabrielae.pages.dev/projects/aptos-2019-blindness-detection/</guid><description>&lt;h2 id="overview"&gt;Overview&lt;/h2&gt;
&lt;p&gt;The
Kaggle challenge asks contestants to grade &lt;strong&gt;diabetic retinopathy (DR) severity&lt;/strong&gt; from a single retinal fundus image according to scale represented by five ordinal classes, following the &lt;strong&gt;International Clinical Diabetic Retinopathy (ICDR) severity scale&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grade&lt;/th&gt;
&lt;th&gt;ICDR label&lt;/th&gt;
&lt;th&gt;What a grader is looking for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;No apparent retinopathy&lt;/td&gt;
&lt;td&gt;no evidence of abnormalities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Mild NPDR&lt;/td&gt;
&lt;td&gt;microaneurysms only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Moderate NPDR&lt;/td&gt;
&lt;td&gt;more than microaneurysms, less than severe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Severe NPDR&lt;/td&gt;
&lt;td&gt;the &amp;ldquo;4-2-1&amp;rdquo; rule — haemorrhages in 4 quadrants, venous beading in 2+, or IRMA in 1+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Proliferative DR&lt;/td&gt;
&lt;td&gt;neovascularisation, vitreous or preretinal haemorrhage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Further information on this scoring can be found on PubMed
&lt;/p&gt;
&lt;p&gt;Predictions for this competition were scored by &lt;strong&gt;quadratic weighted kappa (QWK)&lt;/strong&gt;, a statistical metric that measures the agreement between two raters classifying something represented by an ordinal scale. Ordinal as in items that have some logical ordering. A score of 1 represents perfect agreement and a score of 0 is bad.&lt;/p&gt;
&lt;p&gt;External data was allowed, so the 2015 Diabetic Retinopathy dataset could be used. My competition entry reached a public leaderboard score of &lt;strong&gt;0.816&lt;/strong&gt;, not that it mattered as I forgot to then select a score for the final private leaderboard scoring!! All is good though, I welcomed my newborn baby into the world and became a dad instead!&lt;/p&gt;
&lt;p&gt;The most interesting part of the project was what it taught me about medical imaging and the differences between a &lt;em&gt;better image&lt;/em&gt; and a &lt;em&gt;better label&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="my-walkthrough"&gt;My Walkthrough&lt;/h2&gt;
&lt;p&gt;I started from a notebook recipe that was circulating on Kaggle at the time and then explored a few parts that I had opinions about.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Preprocessing&lt;/strong&gt; The Ben-Graham crop — A protocol that finds the circular fundus, crops to it, then subtracts a local average of the colour to flatten uneven illumination — plus a resize to 224px. This is the smallest resolution that pre-trained EfficientNet&amp;rsquo;s could support, I only had a 1080ti at the time locally to work with and wanted faster turnaround times for experiments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;De-Duplication&lt;/strong&gt; It was suspected collectively from contestants that some images from the 2015 dataset were part of the 2019 dataset, mainly due to large local CV scores compared to the public test leaderboards. Typical practice is to use a perceptual-hash audit across datasets, and then remove duplicates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backbone&lt;/strong&gt; EfficientNet-B0 with CBAM (Convolutional Block Attention Module) grafted in. The goal was to exploit transfer learning from B0&amp;rsquo;s imagenet weights, fine-tune on the 2015, then further fine-tune again on the 2019 dataset.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-validation&lt;/strong&gt; A 5-fold &lt;code&gt;StratifiedKFold&lt;/code&gt; protocol to measure baselines against the public test leaderboard scores.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Loss heads&lt;/strong&gt; I explored all the losses. Cross-entropy, focal, Lovász and ordinal heads, plus an MSE regression head on the label treated as a continuous value. No loss really dominated&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Certainty-aware inference&lt;/strong&gt; For each fold I ran 50 repeated forward passes with dropout enabled and kept, per image, the mean class probabilities and the standard deviation across passes. This was a cheap uncertainty estimate that I used for both model selection prior to stacking, and later for gating the pseudo-labels to QWK scores.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stacking&lt;/strong&gt; I had wanted to try stacking from the beginning of this competition, this had been proven effective from other competitive solutions in the past. So I trained XGBoost over the five class probabilities, then used a Rounder further optimized to fit ordinal thresholds for maximizing QWK rather than assuming equidistant scoring intervals 0.5/1.5/2.5/3.5.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I did accumulate a few dead-ends on the research side, mainly across different architectures (UNets, Inception-CBAM, AttentionResnet, InceptionResnet, etc.). I did look to see if augmentations could be optimized i.e. what improved and what didn&amp;rsquo;t to the scores, but this was just too uncertain. The one experiment worth writing about is the one that failed for a &lt;em&gt;conceptual&lt;/em&gt; reason rather than a technical one which I&amp;rsquo;ll elaborate on next.&lt;/p&gt;
&lt;h2 id="the-haze-removal-experiment"&gt;The &amp;ldquo;Haze&amp;rdquo; Removal Experiment&lt;/h2&gt;
&lt;p&gt;The reasoning that got me started on this experiment was straightforward. Looking at all the Fundus photographs, I could see they had a fogginess haze to them. Low contrast, with a light grey-whitish wash. I found there is a well-established algorithm to solve that problem in digital photographs. &lt;strong&gt;Dark Channel Prior (DCP)&lt;/strong&gt; by He, Sun &amp;amp; Tang. This algorithm notably won CPVR&amp;rsquo;s best paper award in 2009. My hypothesis was that if a dehazing step sharpened the retinal detail, the classifier would do better.&lt;/p&gt;
&lt;p&gt;I connected up four separate DCP implementations a naive per-pixel one, a Numba one, a pure-NumPy one, and finally the one I actually used, which leaned on OpenCV&amp;rsquo;s capabilities for speed.&lt;/p&gt;
&lt;p&gt;The protocol was to first crop the black border, detect the optic disc with a Hough circle algorithm so that every image was centred and scaled consistently, then run DCP over the images. I ran it over both the 2015 and 2019 training data successfully, then trained with the rest of the pipeline on the dehazed sets.&lt;/p&gt;
&lt;!-- The results were *suggestive* and completely uninterpretable:
| Run | Data | Best val QWK | Epochs |
|---|---|---|---|
| `2015-2019DehazedTesterOrdinal` | 2015+2019, dehazed | 0.6577 | 42 |
| `2015-2019DehazedTesterOrdinalfinetuned` | same, fine-tuned | **0.8928** | 114 |
| `300NoBen` | 2019, not dehazed | 0.8681 | 59 |
| `300_T2019_V2019_FineTune` | 2019, not dehazed | 0.8691 | 73 |
| `300_2019SimpleBCBAM` | 2019, not dehazed | 0.8802 | 100 |
The fine-tuned dehazed run landed at the very top of the non-dehazed band. But the two dehazed runs disagree with each other by **0.235 QWK**, and the only difference between them is a fine-tuning stage — so the number is dominated by training procedure, not by the preprocessing I was trying to evaluate. The runs also differ in epoch count, schedule and split, with no fixed seed. It is not a controlled comparison, and I should not have needed the benefit of hindsight to see that.
#TODO: redo comparison
--&gt;
&lt;figure&gt;&lt;img src="https://72a35e7c.drgabrielae.pages.dev/projects/aptos-2019-blindness-detection/dehaze_before_after_2015.jpg"
alt="2015 fundus images before and after Dark Channel Prior dehazing"&gt;&lt;figcaption&gt;
&lt;p&gt;The same pipeline on the 2015 set. Columns are the haziest image per grade. The change here is roughly three times larger than on 2019, and the bottom row shows why the tone row of the table matters: it is mostly saturation and contrast.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;&lt;img src="https://72a35e7c.drgabrielae.pages.dev/projects/aptos-2019-blindness-detection/dehaze_before_after.jpg"
alt="Fundus images before and after Dark Channel Prior dehazing"&gt;&lt;figcaption&gt;
&lt;p&gt;Before and after Dark Channel Prior on the four haziest images in the 2019 set — the dehazer at its strongest. Top row as captured, bottom row dehazed. Each column is labelled with the mean absolute pixel difference.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Ok great, much more vibrant images!&lt;/p&gt;
&lt;p&gt;But, when I evaluated the impact of that change, I scored worse on validation sets with dehazed images, than compared to the original as captured ones.&lt;/p&gt;
&lt;h3 id="why-it-was-never-going-to-work"&gt;Why it was never going to work&lt;/h3&gt;
&lt;p&gt;Pondering why the dehazing experiment failed so badly, it occurred to me that obviously the labels are a humans diagnosis of the fundus image as it was shown to them. Clinicians and trained graders look at thousands of these images, haze and all and assign a severity score regardless. It is possibly that a grader may struggle to see markers that lead to a particular severity conclusion, but the QWK metric scores the agreement between grades, not the quality or accuracy of the grade itself. The scale remains a qualitative assessment anyway.&lt;/p&gt;
&lt;p&gt;Diabetic retinopathy is not graded by how sharp or clean a photograph is. Its performed by identifying and counting specific lesions. So the assumption I had was wrong. I assumed a direct correlation between image quality and label quality.&lt;/p&gt;
&lt;!--
## How It Turned Out
My entry finished at **0.814 QWK on the public leaderboard** (certainty mode-vote across 5 folds), up from 0.75 for my first single-model submission.
The number I trust least — and the one worth reporting — is the gap between that and my cross-validation. Pooled out-of-fold QWK was **0.928** for the certainty model and **0.900** for the plain logits model, with the XGBoost stack averaging **0.909**. That is a CV-to-leaderboard gap of roughly **0.11–0.12**, and it is the most important finding in the project, because it says my local validation was not measuring what I thought it was measuring. The two likely causes are residual 2015↔2019 leakage that survived the duplicate audit, and a `StratifiedKFold` split that stratifies on label only and therefore does not control for patient or camera. Both inflate CV. I did confirm the certainty block is not a degenerate artifact — the std across the 50 passes is around 0.014, so it reflects genuine predictive disagreement rather than a collapsed inference mode.
I also ran a self-training round and stopped partway through it. The certainty gate selected 812 confident test images as extra training data, but the class balance was badly skewed — 508 of them were grade 2 — which is a known self-training failure mode: the gate rewards the class the model is already confident about. The retrain that would have consumed them never ran.
One last caveat that is specific to APTOS: it was a Kaggle **code** competition, so the private test set was only available inside the notebook runtime and the final standings were resolved server-side. No re-scoring or late CSV upload can reproduce a ranked private score, and the fold weights from 2019 are gone. The 0.814 is a recorded public-LB figure, not a reproducible result.
## Architectures Explored
- **ResNet** (18, 50) — the baseline family at the start
- **InceptionV3 + CBAM** — good, but heavier than it needed to be
- **EfficientNetB0** — the eventual backbone. With CBAM, the best size/accuracy trade-off by a distance
- **EfficientNetB3 / B5** — tried for capacity; B5 was slow without a clear return
- **AttentionResnet56** — the interesting attention variant, competitive but not better
- **SENet154** — squeeze-excitation at scale; too heavy for the gain
- **InceptionResNetV2** — explored as an alternative backbone
- **Hourglass** — encoder-decoder topology, no improvement over CBAM-EfficientNetB0
- **GroupNorm variants** — as an alternative to batch norm at small batch sizes --&gt;
&lt;h2 id="what-i-learned"&gt;What I Learned&lt;/h2&gt;
&lt;p&gt;The technical lesson I walked away with was more about the challenges associated with AI based medical labelling in general. When the target is based upon a clinician&amp;rsquo;s judgement, the ceiling is set by the judgement, not by the quality of imaging or medical report that led to that judgement. Sure it might help the human judgement be more accurate for a patients sake, catching disease early enough such that treatment becomes more effective. But for AI systems to be designed well enough for widespread medical usage, they have to be robust for all kinds of flaws and diversity seen across the data itself. This framing has stayed with me all these past years since.&lt;/p&gt;
&lt;h2 id="techniques-used-for-my-final-submission-pipeline"&gt;Techniques Used for my Final Submission Pipeline&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Preprocessing&lt;/strong&gt; — Ben-Graham crop and local-average colour subtraction, 224px, with duplicate auditing across the 2015 and 2019 sets&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two-stage training&lt;/strong&gt; — pretrain on the 2015 dataset, then fine-tune on APTOS 2019 with 5-fold &lt;code&gt;StratifiedKFold&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Model selection&lt;/strong&gt; — a &lt;code&gt;val_QWK − val_loss&lt;/code&gt; callback rather than validation loss alone, since QWK is the competition metric&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Certainty-aware inference&lt;/strong&gt; — 50 repeated forward passes per fold to get stable mean class probabilities plus a per-image standard deviation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test Time Augmentation&lt;/strong&gt; - Instead of performing 50 repeated passes on the full test dataset, fundus images were rotated 45-degrees 8 times instead. This was naturally a lot faster obviously.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ensembling&lt;/strong&gt; — mode-vote across the five folds&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stacking&lt;/strong&gt; — XGBoost regressor over the class probabilities, then an OptimizedRounder fitting ordinal thresholds directly to QWK&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Certainty-gated pseudo-labeling&lt;/strong&gt; — keep test images where the ensemble and the stack agree and certainty is high (ran, but the retrain never completed)&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>