MoleCheck: an on-device skin-lesion classifier
A Flutter app that classifies photos of moles as lower- or higher-risk, entirely on the phone — no server, no upload, works offline.
Try it in your browser →
Runs entirely on your device — your photo is never uploaded.
The full app: photograph or pick a mole and get an on-device estimate, with the same three-screen disclaimer flow. Nothing is uploaded.
Download APK →How to install (sideloading)
- On your Android phone, open this page and tap Download APK.
- When prompted, allow your browser to “install unknown apps” (Android asks once, for safety).
- Open the downloaded file and tap Install.
Educational app — not a medical device and not a diagnosis. It is distributed outside the Play Store and signed with a debug key, so Android will warn you before installing; that warning is expected for a sideloaded educational build. Built for arm64 phones (essentially all Android phones from the last several years). Android only — an iOS build can be produced from the same code.
At a glance
| Task | Binary classification — benign vs. malignant skin lesions from photos |
|---|---|
| Dataset | ISIC Archive, 11,720 dermatology images (superset of HAM10000) |
| Model | YOLO11s-cls, fine-tuned 40 epochs, 224×224 input |
| Result | ROC-AUC 0.914; 90% sensitivity at 75% specificity on held-out data |
| Deployment | TensorFlow Lite (float32), on-device via tflite_flutter |
| App | Flutter — one codebase for Android and iOS |
Why on-device
Photos of skin are sensitive. The simplest way to make the privacy story airtight is to never move the image at all: the model ships inside the app, inference runs on the phone's own hardware, and the app works in airplane mode. This also removes an entire category of engineering (servers, APIs, uptime, data retention policy) from the project.
Data and labels
Training used a public ISIC Archive collection of 11,720 dermatoscopic images — a superset of the classic HAM10000 dataset. Each image carries a diagnosis field that was mapped to a binary label: benign or malignant, with indeterminate cases excluded. The dataset is heavily imbalanced toward benign lesions, which shaped both training and how the decision threshold was chosen.
Model and training
The classifier is YOLO11s-cls — the classification variant of Ultralytics' YOLO11 — fine-tuned for 40 epochs on a local NVIDIA RTX 5060 Ti. A pretrained classification backbone with a direct export path was chosen deliberately: for a capstone, a strong, reproducible baseline beats a custom architecture that is harder to ship.
Choosing the threshold: sensitivity first
A default 0.5 threshold is the wrong choice for this problem. The two errors are not symmetric: telling a user a malignant lesion looks fine (a false negative) is far worse than flagging a benign one for a dermatologist visit (a false positive). The final model reaches ROC-AUC 0.914, and the decision threshold was set at 0.137 — the point where the model catches 90% of malignant lesions while still correctly clearing 75% of benign ones. The app is tuned to err on the side of "get it checked."
Progress across iterations
Every iteration of this project is recapped here as it ships, so you can watch the recognition numbers improve over time. The chart below also shows the journey within the first iteration: at epoch 1 the model scores about 80% — which sounds good until you notice that "always answer benign" scores 80% too, because most lesions in the dataset are benign. The learning that matters is everything above that dashed line.
The export toolchain (the part tutorials skip)
Ultralytics can export straight to TFLite — but as of version 8.4 that path only works on Linux and macOS. On Windows, the working route is:
PyTorch checkpoint → ONNX → onnx2tf → float32 TFLite.
The converted model matches PyTorch outputs to within 0.001, so nothing meaningful is lost in translation. The resulting model takes NHWC input at 224×224 with simple /255 normalization — details that must match exactly in the app code, or predictions are silently garbage.
The app
The Flutter app lets the user photograph or pick an image of a mole, runs the TFLite model locally, and presents the result as risk awareness — never as a diagnosis. A three-screen onboarding flow requires acknowledging the educational disclaimer before first use, and every result screen repeats the advice to consult a dermatologist.
Two dependency notes for anyone reproducing it: tflite_flutter needs
version 0.12.1 (0.11.0 collides with the TensorFlow Lite namespace under current
Android Gradle Plugin versions), and its Android module's Kotlin
jvmTarget must be pinned to 11 to avoid a Java/Kotlin target mismatch.
Limitations
- Trained on dermatoscopic research images, which look different from casual phone photos — real-world accuracy is expected to be lower.
- Public dermatology datasets under-represent darker skin tones; performance across skin types is not guaranteed to be uniform.
- A 90% sensitivity target still means the model misses roughly 1 in 10 malignant lesions on held-out data — which is exactly why it must never substitute for a clinician.