Spaces:
Running on Zero
Running on Zero
File size: 11,263 Bytes
078e567 d07aeb2 5d31c67 078e567 5d31c67 d07aeb2 078e567 5d31c67 078e567 d07aeb2 5d31c67 812b6ee d79f8b5 812b6ee 5d31c67 d79f8b5 5d31c67 d79f8b5 812b6ee 4a5c225 d79f8b5 812b6ee d79f8b5 812b6ee d79f8b5 5d31c67 5c73a43 5d31c67 3c12e23 5c73a43 5d31c67 c9b579e 4a5c225 5c73a43 4a5c225 5c73a43 c9b579e 4a5c225 c9b579e 5d31c67 d07aeb2 5d31c67 d07aeb2 5c73a43 d07aeb2 5d31c67 d79f8b5 5d31c67 d79f8b5 5d31c67 d79f8b5 5d31c67 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | ---
title: AI Cell Quantification
emoji: "⬜"
colorFrom: gray
colorTo: gray
sdk: gradio
sdk_version: 5.49.1
python_version: "3.12"
suggested_hardware: zero-a10g
app_file: app.py
pinned: false
license: mit
---
# AI Cell Quantification (CellQuant): nucleus, antibody and puncta counting for fluorescence sections
CellQuant counts the six categories that were annotated in CVAT on 35-day spinal-cord
sections stained with DAPI (blue), an antibody (green: Iba1 or TMEM119) and a red punctum
marker that the lab calls **"Nostril"**:
| Label | Meaning |
|---|---|
| Cell, Antibody + | nucleus with green antibody signal, no punctum |
| Cell, Antibody − | nucleus without antibody signal, no punctum |
| Cell, Nostril, Antibody + | nucleus containing a red punctum, antibody positive |
| Cell, Nostril, Antibody − | nucleus containing a red punctum, antibody negative |
| Nostril in Nucleus | red punctum inside a nucleus |
| Nostril External of Nucleus | red punctum outside every nucleus |
Every nucleus receives exactly one of the four *Cell* labels.
## How it works
* **Model** – a UNet with an ImageNet-pretrained ResNet34 encoder (`cellquant/model.py`) that
predicts, per pixel: nucleus interior / border / background, P(antibody +), P(nucleus has
punctum), punctum in-nucleus / external / background, and the normalised distance to the
nucleus outline (1 at the centre of every nucleus, 0 at its outline). A full-resolution
refinement stage keeps 5-pixel puncta.
* **Instances** – nucleus foreground comes from the class map; markers (distance > 0.5) and the
watershed landscape come from the predicted distance map, which has one peak per nucleus and
a valley between touching ones; a nucleus whose peak predicted distance stays below 0.64 is
dropped (the distance head is trained to reach 1 in every annotated nucleus and 0 elsewhere,
so a low peak marks a faint blob the annotator would have skipped; `PostConfig.min_peak_dist`,
0 switches it off); puncta are connected components (`cellquant/postprocess.py`).
Checkpoints without the distance channel fall back to a watershed on the interior map.
* **Labels** – antibody status is the mean probability inside each nucleus; a nucleus is
"Nostril" when a punctum classified "in nucleus" lies inside it (the annotation protocol;
the network's own nucleus-level head is available as an alternative in *Advanced*);
punctum type comes from the network (switchable to a purely geometric rule).
* **Training data** – nine 1024×1024 RGB images with 1,569 nucleus outlines and 1,057
punctum outlines, parsed directly from the CVAT zip exports (`cellquant/annotations.py`).
## Validation (see `reports/SUMMARY.md`, `reports/cv_v3/cv_report.md` and `reports/cv_v3/comparison.md`)
Leave-one-image-out cross-validation: nine models, each tested on an image it never saw, and
each image tested 23 times (repeat runs, TTA settings, 8 rotations/flips, 6 photometric
perturbations, rule variants). Micro-averaged over the nine held-out images:
| Measure | Precision | Recall | F1 |
|---|---:|---:|---:|
| Nucleus detection, IoU ≥ 0.5 | 81.1% | 84.2% | 82.6% |
| Nucleus detection, centroid inside the annotated nucleus | 88.8% | 92.2% | 90.5% |
| Punctum detection (centroid within 6 px) | 76.4% | 84.8% | 80.4% |
With 8× test-time augmentation (the app's default) nucleus F1 is 82.9% and puncta F1 81.8%;
adding image scales on top (24× or 40× passes, `reports/cv_v3/tta_study.md`) gains nothing further
for nuclei and 0.4 points for puncta, at four to six times the run time, so the 8-member symmetry
group is where TTA stops paying. Among matched nuclei the antibody +/− call is right for 93.8% and the Nostril attribute for
95.7%; puncta are classified in-nucleus vs external correctly 93.4% of the time. Scores vary by
at most ±3 F1 points across the 14 rotated/perturbed runs of each image, and repeated runs are
bit-identical. The weak spots are antibody-negative nuclei (F1 40%; only 110 examples, 62 of
them in one image, and the class-balanced loss trades their precision for recall), the five-example
"Cell, Nostril, Antibody −" class, and dense clusters of touching nuclei (still the main source of misses,
although the distance-map watershed recovers many that the first version merged). The model predicts
1629 nuclei for the 1569 annotated ones across the nine held-out images. Earlier versions on the same
protocol: v1 (2-px border, watershed on the interior map, unweighted antibody loss) 77.9% nucleus F1,
87.5% by the centroid rule, 79.9% puncta F1 (`reports/cv/`); v2 (this model without the peak-distance
rule) 80.4%, 88.8% and 80.4% (`reports/cv_v2/`). The shipped `weights/cellquant.pt` was trained on all nine
images with the v2 recipe; the peak-distance rule is post-processing only.
## Using the Space
1. Upload one or more images (TIF/PNG/JPG, blue = nuclei, green = antibody, red = puncta).
Use the 8-bit RGB export of the microscope software (what the model was trained on);
16-bit inputs are auto-scaled so that their 99.95th percentile maps to 255. Multi-channel
TIFs with one channel per page (ImageJ composites, OME-TIFF) are supported; greyscale
inputs are flagged. Images larger than 1536 px are processed in overlapping tiles.
2. Every image is analysed eight times (four rotations × two flips) and the outputs averaged, the
setting that scored best in cross-validation (nucleus F1 +0.3, puncta F1 +1.4 points over a
single pass). `CELLQUANT_TTA=1` or `4` changes this for local runs.
3. Click **Analyse**. You get a counts table, colour overlays and a zip with
`counts.csv`, `objects.csv` (every nucleus and punctum with centroid, area, probabilities)
and `results.json`. The table and `counts.csv` have one row per image and eight count columns:
the four nucleus labels followed by their total, **All nuclei**, then the two punctum labels
followed by their total, **All puncta** (the same totals as in the validation table of
section 2; `results.json` carries them under `totals`). Each overlay caption gives the two totals.
4. A written summary follows the counts in both sections when the Space has a `GEMINI_API_KEY`
secret (optionally `GEMINI_MODEL`, default `gemini-4-argon`); without the key a one-line notice
takes its place. Gemini only restates the counts in prose and comments on image quality or on
what the missed objects have in common; it never changes a number, and it is called after the
GPU work, so it costs no GPU time.
## Checking the model against the annotations
The lower half of the Space, *Check the model against the 9 annotated images*, shows the nine
annotated images as a strip of thumbnails (the tenth file in the data folder is a byte-identical
copy of F113). Click one: the model runs on the full image (eight rotations and flips averaged),
its prediction is matched with the CVAT annotation object by object
(nuclei at IoU ≥ 0.5, puncta by centre distance ≤ 6 px), and you get
* three panels side by side: **1. Manual annotation**, **2. Model prediction** (both in the label
colours) and **3. Missed only**, the raw image with just the annotated objects the model did not
find outlined in red, each carrying its row number; every panel opens full screen;
* a per-label table of annotated vs predicted counts with the number matched, matched with a wrong
label, missed and extra;
* a list of every missed, extra or wrongly labelled object with its position (x, y in pixels) and
a note that says, for example, that a missed nucleus was merged into a neighbouring prediction
or that an extra prediction sits where nothing was annotated;
* a zip with five overlays (manual, predicted, missed only, extra only, all differences numbered)
and both tables;
* a written summary of the comparison (Gemini, when the key is set).
The bundled images are the training images, so this shows how the shipped model fits them,
not how it generalises (the held-out numbers above do that). The bundle is rebuilt with
```bash
python scripts/make_validation_set.py --data-dir /path/to/folder --out validation
```
and the section is hidden when `validation/` is empty.
## Deploying to Hugging Face
The checkpoint is ~98 MB, so it must go through Git LFS (`.gitattributes` already tracks
`*.pt`). The simplest route uploads the folder with the CLI, which handles LFS automatically:
```bash
pip install -U huggingface_hub
huggingface-cli login
huggingface-cli upload <your-user>/cellquant . . --repo-type space --exclude "runs/*" --exclude "tests/*" --exclude ".pytest_cache/*"
```
Create the Space first (SDK: Gradio) or add `--create`. Add the optional
`GEMINI_API_KEY` secret under *Settings → Variables and secrets*.
The Space runs on **ZeroGPU** (a shared NVIDIA GPU that is attached only while a call runs):
`app.py` imports `spaces` before torch, puts the model on `cuda` at start-up and wraps the two
inference functions in `@spaces.GPU` with a duration estimated from the number of images.
Visitors need no account: an unauthenticated visitor has 2 GPU minutes a day (about
60 full images at 8× TTA), a free account 5, a PRO account 40. Hosting a ZeroGPU Space needs a
Hugging Face account in good standing (two Spaces free; PRO accounts up to ten), Python 3.12 and
torch ≥ 2.8 (`README.md` front matter and `requirements.txt`). The deploy script does the whole
round trip and times a full image on the Space:
```bash
python scripts/deploy_hf.py --repo <your-user>/cellquant --hardware zero-a10g --bench
```
Without `spaces` installed, or on CPU hardware, the same code runs unchanged on the CPU
(`CELLQUANT_DEVICE` overrides the device).
## Running locally
```bash
pip install -r requirements.txt
python app.py # http://localhost:7860
python -m pytest -q tests # unit + integration tests
```
Retrain when more annotated images are available (put the TIFs and the CVAT zips in one folder):
```bash
python train.py --data-dir /path/to/folder --out weights/cellquant.pt --iters 3000
python evaluate.py cv --data-dir /path/to/folder --runs-dir runs/cv_v3 --iters 3000
python scripts/rule_study.py --data-dir /path/to/folder --runs-dir runs/cv_v2 --out runs/cv_v3/rule_study --weights weights/cellquant.pt
```
The last line re-chooses the peak-distance threshold from the training-set fit (per fold and for the
all-image model); copy the printed value into `PostConfig.min_peak_dist`.
Training uses Apple-silicon MPS, CUDA or CPU automatically.
## Limitations
* The annotator skipped faint / out-of-focus nuclei; the model learned that criterion only
approximately, so very dim nuclei may be counted or not. Raise *Minimum nucleus area* or the
thresholds in *Advanced* to tune for your images; `PostConfig(min_peak_dist=...)` (default
0.64, 0 = off) sets how confident the distance head must be before a nucleus is counted.
* "Cell, Nostril, Antibody −" had only five training examples; its predictions are the
product of the (well-trained) antibody and punctum heads rather than a learned class.
* Images must have the same channel convention (blue nuclei, green antibody, red puncta) and a
similar magnification (nuclei ≈ 20–35 px across). The model tolerates ±25 % scale.
|