Spaces:
Sleeping
Sleeping
Deploy CellQuant
Browse files- .gitattributes +1 -34
- README.md +114 -7
- app.py +198 -0
- cellquant/__init__.py +6 -0
- cellquant/annotations.py +555 -0
- cellquant/data.py +181 -0
- cellquant/gemini_summary.py +71 -0
- cellquant/infer.py +107 -0
- cellquant/labels.py +58 -0
- cellquant/losses.py +48 -0
- cellquant/metrics.py +167 -0
- cellquant/model.py +99 -0
- cellquant/postprocess.py +230 -0
- evaluate.py +414 -0
- examples/F100_crop_512.png +3 -0
- reports/SUMMARY.md +72 -0
- reports/cv/cv_report.md +105 -0
- reports/cv/cv_results.json +0 -0
- reports/cv/postprocess_sweep.json +322 -0
- reports/final/final_fit.json +1820 -0
- requirements.txt +12 -0
- scripts/build_summary.py +82 -0
- scripts/deploy_hf.py +73 -0
- scripts/postprocess_sweep.py +85 -0
- tests/conftest.py +4 -0
- tests/test_annotations.py +115 -0
- tests/test_app.py +34 -0
- tests/test_data.py +39 -0
- tests/test_evaluate.py +26 -0
- tests/test_labels.py +16 -0
- tests/test_metrics.py +60 -0
- tests/test_model_infer.py +57 -0
- tests/test_postprocess.py +93 -0
- tests/test_review_fixes.py +157 -0
- train.py +152 -0
- weights/cellquant.pt +3 -0
- weights/cellquant_history.json +725 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,2 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
examples/F100_crop_512.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -1,13 +1,120 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version:
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: CellQuant
|
| 3 |
+
emoji: "⬜"
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 5.49.1
|
| 8 |
+
python_version: "3.11"
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
+
license: mit
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# CellQuant: nucleus, antibody and puncta counting for fluorescence sections
|
| 15 |
+
|
| 16 |
+
CellQuant counts the six categories that were annotated in CVAT on 35-day spinal-cord
|
| 17 |
+
sections stained with DAPI (blue), an antibody (green: Iba1 or TMEM119) and a red punctum
|
| 18 |
+
marker that the lab calls **"Nostril"**:
|
| 19 |
+
|
| 20 |
+
| Label | Meaning |
|
| 21 |
+
|---|---|
|
| 22 |
+
| Cell, Antibody + | nucleus with green antibody signal, no punctum |
|
| 23 |
+
| Cell, Antibody − | nucleus without antibody signal, no punctum |
|
| 24 |
+
| Cell, Nostril, Antibody + | nucleus containing a red punctum, antibody positive |
|
| 25 |
+
| Cell, Nostril, Antibody − | nucleus containing a red punctum, antibody negative |
|
| 26 |
+
| Nostril in Nucleus | red punctum inside a nucleus |
|
| 27 |
+
| Nostril External of Nucleus | red punctum outside every nucleus |
|
| 28 |
+
|
| 29 |
+
Every nucleus receives exactly one of the four *Cell* labels.
|
| 30 |
+
|
| 31 |
+
## How it works
|
| 32 |
+
|
| 33 |
+
* **Model** – a UNet with an ImageNet-pretrained ResNet34 encoder (`cellquant/model.py`) that
|
| 34 |
+
predicts, per pixel: nucleus interior / border / background, P(antibody +), P(nucleus has
|
| 35 |
+
punctum) and punctum in-nucleus / external / background. A full-resolution refinement stage
|
| 36 |
+
keeps 5-pixel puncta.
|
| 37 |
+
* **Instances** – a marker-controlled watershed on the interior/border maps separates touching
|
| 38 |
+
nuclei; puncta are connected components (`cellquant/postprocess.py`).
|
| 39 |
+
* **Labels** – antibody status is the mean probability inside each nucleus; a nucleus is
|
| 40 |
+
"Nostril" when a punctum classified "in nucleus" lies inside it (the annotation protocol;
|
| 41 |
+
the network's own nucleus-level head is available as an alternative in *Advanced*);
|
| 42 |
+
punctum type comes from the network (switchable to a purely geometric rule).
|
| 43 |
+
* **Training data** – nine 1024×1024 RGB images with 1,569 nucleus outlines and 1,057
|
| 44 |
+
punctum outlines, parsed directly from the CVAT zip exports (`cellquant/annotations.py`).
|
| 45 |
+
|
| 46 |
+
## Validation (see `reports/SUMMARY.md` and `reports/cv/cv_report.md`)
|
| 47 |
+
|
| 48 |
+
Leave-one-image-out cross-validation: nine models, each tested on an image it never saw, and
|
| 49 |
+
each image tested 23 times (repeat runs, TTA settings, 8 rotations/flips, 6 photometric
|
| 50 |
+
perturbations, rule variants). Micro-averaged over the nine held-out images:
|
| 51 |
+
|
| 52 |
+
| Measure | Precision | Recall | F1 |
|
| 53 |
+
|---|---:|---:|---:|
|
| 54 |
+
| Nucleus detection, IoU ≥ 0.5 | 77.5% | 78.4% | 77.9% |
|
| 55 |
+
| Nucleus detection, centroid inside the annotated nucleus | 87.0% | 88.0% | 87.5% |
|
| 56 |
+
| Punctum detection (centroid within 6 px) | 77.3% | 82.8% | 79.9% |
|
| 57 |
+
|
| 58 |
+
Among matched nuclei the antibody +/− call is right for 94% and the Nostril attribute for
|
| 59 |
+
96%; puncta are classified in-nucleus vs external correctly 93% of the time. Scores vary by
|
| 60 |
+
at most ±2 F1 points across the 14 rotated/perturbed runs of each image, and repeated runs are
|
| 61 |
+
bit-identical. The weak spots are antibody-negative nuclei (F1 32%; only 102 examples, 62 of
|
| 62 |
+
them in one image), the five-example "Cell, Nostril, Antibody −" class, and clusters of touching
|
| 63 |
+
nuclei that are sometimes merged. The shipped `weights/cellquant.pt` was trained on all nine
|
| 64 |
+
images with the same recipe.
|
| 65 |
+
|
| 66 |
+
## Using the Space
|
| 67 |
+
|
| 68 |
+
1. Upload one or more images (TIF/PNG/JPG, blue = nuclei, green = antibody, red = puncta).
|
| 69 |
+
Use the 8-bit RGB export of the microscope software (what the model was trained on);
|
| 70 |
+
16-bit inputs are auto-scaled so that their 99.95th percentile maps to 255. Multi-channel
|
| 71 |
+
TIFs with one channel per page (ImageJ composites, OME-TIFF) are supported; greyscale
|
| 72 |
+
inputs are flagged. Images larger than 1536 px are processed in overlapping tiles.
|
| 73 |
+
2. Optional: enable test-time augmentation (slower, slightly more stable).
|
| 74 |
+
3. Click **Analyse**. You get a counts table, colour overlays and a zip with
|
| 75 |
+
`counts.csv`, `objects.csv` (every nucleus and punctum with centroid, area, probabilities)
|
| 76 |
+
and `results.json`.
|
| 77 |
+
4. Optional Gemini summary: add a Space secret `GEMINI_API_KEY` (and optionally
|
| 78 |
+
`GEMINI_MODEL`, default `gemini-4-argon`). Gemini only rewrites the counts in prose and
|
| 79 |
+
comments on image quality; it never changes a number.
|
| 80 |
+
|
| 81 |
+
## Deploying to Hugging Face
|
| 82 |
+
|
| 83 |
+
The checkpoint is ~98 MB, so it must go through Git LFS (`.gitattributes` already tracks
|
| 84 |
+
`*.pt`). The simplest route uploads the folder with the CLI, which handles LFS automatically:
|
| 85 |
+
|
| 86 |
+
```bash
|
| 87 |
+
pip install -U huggingface_hub
|
| 88 |
+
huggingface-cli login
|
| 89 |
+
huggingface-cli upload <your-user>/cellquant . . --repo-type space --exclude "runs/*" --exclude "tests/*" --exclude ".pytest_cache/*"
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
Create the Space first (SDK: Gradio, hardware: free CPU) or add `--create`. Add the optional
|
| 93 |
+
`GEMINI_API_KEY` secret under *Settings → Variables and secrets*.
|
| 94 |
+
|
| 95 |
+
## Running locally
|
| 96 |
+
|
| 97 |
+
```bash
|
| 98 |
+
pip install -r requirements.txt
|
| 99 |
+
python app.py # http://localhost:7860
|
| 100 |
+
python -m pytest -q tests # unit + integration tests
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
Retrain when more annotated images are available (put the TIFs and the CVAT zips in one folder):
|
| 104 |
+
|
| 105 |
+
```bash
|
| 106 |
+
python train.py --data-dir /path/to/folder --out weights/cellquant.pt --iters 3000
|
| 107 |
+
python evaluate.py cv --data-dir /path/to/folder --runs-dir runs/cv --iters 3000
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
Training uses Apple-silicon MPS, CUDA or CPU automatically.
|
| 111 |
+
|
| 112 |
+
## Limitations
|
| 113 |
+
|
| 114 |
+
* The annotator skipped faint / out-of-focus nuclei; the model learned that criterion only
|
| 115 |
+
approximately, so very dim nuclei may be counted or not. Raise *Minimum nucleus area* or the
|
| 116 |
+
thresholds in *Advanced* to tune for your images.
|
| 117 |
+
* "Cell, Nostril, Antibody −" had only five training examples; its predictions are the
|
| 118 |
+
product of the (well-trained) antibody and punctum heads rather than a learned class.
|
| 119 |
+
* Images must have the same channel convention (blue nuclei, green antibody, red puncta) and a
|
| 120 |
+
similar magnification (nuclei ≈ 20–35 px across). The model tolerates ±25 % scale.
|
app.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""CellQuant Gradio app (Hugging Face Space entry point).
|
| 3 |
+
|
| 4 |
+
Upload one or more fluorescence images (TIF/PNG/JPG; blue nuclei, green antibody,
|
| 5 |
+
red puncta) and get per-image counts of the six annotation categories, colour
|
| 6 |
+
overlays, and downloadable CSV/JSON of every detected object.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import csv
|
| 11 |
+
import io
|
| 12 |
+
import json
|
| 13 |
+
import os
|
| 14 |
+
import sys
|
| 15 |
+
import tempfile
|
| 16 |
+
import zipfile
|
| 17 |
+
|
| 18 |
+
os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
|
| 19 |
+
|
| 20 |
+
import gradio as gr # noqa: E402
|
| 21 |
+
import numpy as np # noqa: E402
|
| 22 |
+
from PIL import Image # noqa: E402
|
| 23 |
+
|
| 24 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # run from any cwd / python -I
|
| 25 |
+
|
| 26 |
+
from cellquant import __version__ # noqa: E402
|
| 27 |
+
from cellquant.annotations import load_image
|
| 28 |
+
from cellquant.gemini_summary import enabled as gemini_enabled
|
| 29 |
+
from cellquant.gemini_summary import summarize as gemini_summarize
|
| 30 |
+
from cellquant.infer import Predictor
|
| 31 |
+
from cellquant.labels import ALL_LABELS, LABEL_COLORS
|
| 32 |
+
from cellquant.postprocess import PostConfig, render_overlay, result_to_json
|
| 33 |
+
|
| 34 |
+
WEIGHTS = os.environ.get("CELLQUANT_WEIGHTS", os.path.join(os.path.dirname(os.path.abspath(__file__)), "weights", "cellquant.pt"))
|
| 35 |
+
DEVICE = os.environ.get("CELLQUANT_DEVICE", "cpu")
|
| 36 |
+
|
| 37 |
+
EX_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "examples")
|
| 38 |
+
|
| 39 |
+
_predictors: dict[int, Predictor] = {}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def get_predictor(tta: int) -> Predictor:
|
| 43 |
+
if tta not in _predictors:
|
| 44 |
+
if not os.path.exists(WEIGHTS):
|
| 45 |
+
raise gr.Error(f"Model weights not found at {WEIGHTS}. Train with train.py or set CELLQUANT_WEIGHTS.")
|
| 46 |
+
_predictors[tta] = Predictor(WEIGHTS, device=DEVICE, tta=tta)
|
| 47 |
+
return _predictors[tta]
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _legend_html() -> str:
|
| 51 |
+
items = "".join(
|
| 52 |
+
f'<span style="display:inline-block;margin:2px 10px 2px 0"><span style="display:inline-block;width:14px;height:14px;'
|
| 53 |
+
f'background:rgb{c};border:1px solid #888;vertical-align:middle;margin-right:4px"></span>{lab}</span>'
|
| 54 |
+
for lab, c in LABEL_COLORS.items()
|
| 55 |
+
)
|
| 56 |
+
return f"<div>{items}</div>"
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def analyze_files(files, tta_choice, antibody_thr, cellnos_thr, min_cell_area, nostril_rule, cellnos_rule,
|
| 60 |
+
want_summary, progress=gr.Progress()):
|
| 61 |
+
if not files:
|
| 62 |
+
raise gr.Error("Upload at least one image.")
|
| 63 |
+
tta = {"none (fastest)": 1, "4x flips": 4, "8x rotations+flips": 8}[tta_choice]
|
| 64 |
+
cfg = PostConfig(antibody_thr=float(antibody_thr), cellnos_thr=float(cellnos_thr),
|
| 65 |
+
min_cell_area=int(min_cell_area), nostril_rule=nostril_rule, cellnos_rule=cellnos_rule)
|
| 66 |
+
pred = get_predictor(tta)
|
| 67 |
+
rows, gallery, per_image_json, csv_objects = [], [], {}, []
|
| 68 |
+
tmpdir = tempfile.mkdtemp(prefix="cellquant_")
|
| 69 |
+
overlays = {}
|
| 70 |
+
warnings_: list[str] = []
|
| 71 |
+
seen: dict[str, int] = {}
|
| 72 |
+
for i, f in enumerate(files):
|
| 73 |
+
path = f if isinstance(f, str) else f.name
|
| 74 |
+
name = os.path.basename(path)
|
| 75 |
+
k = seen.get(name, 0)
|
| 76 |
+
seen[name] = k + 1
|
| 77 |
+
if k: # two uploads with the same file name: keep both
|
| 78 |
+
stem, ext = os.path.splitext(name)
|
| 79 |
+
name = f"{stem}_{k + 1}{ext}"
|
| 80 |
+
progress((i, len(files)), desc=f"analysing {name}")
|
| 81 |
+
try:
|
| 82 |
+
img = load_image(path)
|
| 83 |
+
except Exception as e: # noqa: BLE001
|
| 84 |
+
raise gr.Error(f"{name}: cannot read image ({e})")
|
| 85 |
+
if np.array_equal(img[..., 0], img[..., 1]) and np.array_equal(img[..., 1], img[..., 2]):
|
| 86 |
+
warnings_.append(f"{name}: all three channels are identical (greyscale input); the model needs blue nuclei, green antibody and red puncta.")
|
| 87 |
+
res, _ = pred.predict(img, cfg)
|
| 88 |
+
ov = render_overlay(img, res, thickness=1)
|
| 89 |
+
overlays[name] = ov
|
| 90 |
+
out_png = os.path.join(tmpdir, os.path.splitext(name)[0] + "_overlay.png")
|
| 91 |
+
Image.fromarray(ov).save(out_png)
|
| 92 |
+
gallery.append((out_png, f"{name}: {res.counts['Cell, Antibody +'] + res.counts['Cell, Antibody -'] + res.counts['Cell, Nostril, Antibody +'] + res.counts['Cell, Nostril, Antibody -']} nuclei"))
|
| 93 |
+
rows.append([name] + [res.counts[lab] for lab in ALL_LABELS])
|
| 94 |
+
per_image_json[name] = {"image_size_hw": list(img.shape[:2]), **result_to_json(res)}
|
| 95 |
+
for c in res.cells:
|
| 96 |
+
csv_objects.append([name, "nucleus", c["id"], c["label"], round(c["area_px"], 1), round(c["centroid_xy"][0], 1), round(c["centroid_xy"][1], 1), round(c["p_antibody"], 3), round(c["p_nostril"], 3), c["n_puncta_inside"]])
|
| 97 |
+
for n_ in res.nostrils:
|
| 98 |
+
csv_objects.append([name, "punctum", n_["id"], n_["label"], round(n_["area_px"], 1), round(n_["centroid_xy"][0], 1), round(n_["centroid_xy"][1], 1), "", round(n_["p_in_nucleus"], 3), n_["cell_id"]])
|
| 99 |
+
# downloads
|
| 100 |
+
counts_csv = os.path.join(tmpdir, "counts.csv")
|
| 101 |
+
with open(counts_csv, "w", newline="") as fh:
|
| 102 |
+
w = csv.writer(fh)
|
| 103 |
+
w.writerow(["image"] + list(ALL_LABELS))
|
| 104 |
+
w.writerows(rows)
|
| 105 |
+
objects_csv = os.path.join(tmpdir, "objects.csv")
|
| 106 |
+
with open(objects_csv, "w", newline="") as fh:
|
| 107 |
+
w = csv.writer(fh)
|
| 108 |
+
w.writerow(["image", "kind", "id", "label", "area_px", "centroid_x", "centroid_y", "p_antibody", "p_nostril_or_in_nucleus", "n_puncta_inside_or_cell_id"])
|
| 109 |
+
w.writerows(csv_objects)
|
| 110 |
+
json_path = os.path.join(tmpdir, "results.json")
|
| 111 |
+
with open(json_path, "w") as fh:
|
| 112 |
+
json.dump({"cellquant_version": __version__, "weights": os.path.basename(WEIGHTS), "tta": tta,
|
| 113 |
+
"postprocess": cfg.__dict__, "images": per_image_json}, fh, indent=1)
|
| 114 |
+
zip_path = os.path.join(tmpdir, "cellquant_results.zip")
|
| 115 |
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
| 116 |
+
for p in (counts_csv, objects_csv, json_path):
|
| 117 |
+
zf.write(p, os.path.basename(p))
|
| 118 |
+
for p, _ in gallery:
|
| 119 |
+
zf.write(p, os.path.basename(p))
|
| 120 |
+
summary = "\n\n".join(f"Warning: {w}" for w in warnings_)
|
| 121 |
+
if want_summary:
|
| 122 |
+
if gemini_enabled():
|
| 123 |
+
first = next(iter(overlays.values()))
|
| 124 |
+
summary += ("\n\n" if summary else "") + gemini_summarize(
|
| 125 |
+
[{"image": r[0], "counts": dict(zip(ALL_LABELS, r[1:]))} for r in rows], overlay=Image.fromarray(first))
|
| 126 |
+
else:
|
| 127 |
+
summary += ("\n\n" if summary else "") + "Gemini summary is off: add a GEMINI_API_KEY secret to this Space to enable it."
|
| 128 |
+
return rows, gallery, zip_path, summary
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
FORCE_LIGHT_JS = """
|
| 132 |
+
function refresh() {
|
| 133 |
+
const url = new URL(window.location);
|
| 134 |
+
if (url.searchParams.get('__theme') !== 'light') {
|
| 135 |
+
url.searchParams.set('__theme', 'light');
|
| 136 |
+
window.location.href = url.href;
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
"""
|
| 140 |
+
|
| 141 |
+
CSS = """
|
| 142 |
+
body, .gradio-container { background: #fff !important; }
|
| 143 |
+
.gradio-container .prose, .gradio-container .prose * { color: #000; }
|
| 144 |
+
.gradio-container .prose a { color: #000; text-decoration: underline; }
|
| 145 |
+
.gradio-container table, .gradio-container td, .gradio-container th { color: #000; }
|
| 146 |
+
footer { display: none !important; }
|
| 147 |
+
"""
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def build_demo() -> gr.Blocks:
|
| 151 |
+
theme = gr.themes.Monochrome(primary_hue="neutral", neutral_hue="neutral",
|
| 152 |
+
font=["Helvetica Neue", "Helvetica", "Arial", "sans-serif"],
|
| 153 |
+
font_mono=["Menlo", "Consolas", "monospace"])
|
| 154 |
+
with gr.Blocks(title="CellQuant", theme=theme, css=CSS, js=FORCE_LIGHT_JS) as demo:
|
| 155 |
+
gr.Markdown(
|
| 156 |
+
f"# CellQuant {__version__}\n"
|
| 157 |
+
"Counts nuclei (antibody positive or negative), nuclei containing a red punctum (\"Nostril\") and puncta "
|
| 158 |
+
"inside or outside nuclei in RGB fluorescence images (blue = DAPI, green = antibody, red = puncta). "
|
| 159 |
+
"Trained on CVAT outlines of 35-day spinal-cord sections stained for Iba1 or TMEM119."
|
| 160 |
+
)
|
| 161 |
+
gr.HTML(_legend_html())
|
| 162 |
+
with gr.Row():
|
| 163 |
+
with gr.Column(scale=1):
|
| 164 |
+
files = gr.File(label="Images (TIF / PNG / JPG)", file_count="multiple", file_types=["image", ".tif", ".tiff"])
|
| 165 |
+
tta = gr.Radio(["none (fastest)", "4x flips", "8x rotations+flips"], value="none (fastest)", label="Test-time augmentation")
|
| 166 |
+
want_summary = gr.Checkbox(value=False, label="Plain-language summary (Gemini)", visible=True)
|
| 167 |
+
with gr.Accordion("Advanced", open=False):
|
| 168 |
+
antibody_thr = gr.Slider(0.1, 0.9, value=0.5, step=0.05, label="Antibody + threshold")
|
| 169 |
+
cellnos_thr = gr.Slider(0.1, 0.9, value=0.5, step=0.05, label="Nucleus 'Nostril' threshold")
|
| 170 |
+
min_cell_area = gr.Slider(10, 300, value=50, step=5, label="Minimum nucleus area (px)")
|
| 171 |
+
nostril_rule = gr.Radio(["head", "geometric"], value="head", label="Punctum type from: network head / position relative to nuclei")
|
| 172 |
+
cellnos_rule = gr.Radio(["contains", "either", "head"], value="contains", label="Nucleus gets 'Nostril' if: contains an in-nucleus punctum / that OR the network head / head only")
|
| 173 |
+
run = gr.Button("Analyse", variant="primary")
|
| 174 |
+
examples = sorted(os.path.join(EX_DIR, f) for f in os.listdir(EX_DIR)
|
| 175 |
+
if f.lower().endswith((".png", ".jpg", ".jpeg", ".tif", ".tiff"))) if os.path.isdir(EX_DIR) else []
|
| 176 |
+
if examples:
|
| 177 |
+
gr.Examples(examples=[[[e]] for e in examples], inputs=[files], label="Example (512 px crop of a training image)")
|
| 178 |
+
with gr.Column(scale=2):
|
| 179 |
+
table = gr.Dataframe(headers=["image"] + list(ALL_LABELS), label="Counts", interactive=False, wrap=True)
|
| 180 |
+
gallery = gr.Gallery(label="Overlays", columns=2, height="auto")
|
| 181 |
+
zip_out = gr.File(label="Download results (CSV + JSON + overlays)")
|
| 182 |
+
summary = gr.Markdown()
|
| 183 |
+
run.click(analyze_files, [files, tta, antibody_thr, cellnos_thr, min_cell_area, nostril_rule, cellnos_rule, want_summary],
|
| 184 |
+
[table, gallery, zip_out, summary])
|
| 185 |
+
gr.Markdown(
|
| 186 |
+
"Nucleus outlines come from a watershed over the network's interior and border maps. "
|
| 187 |
+
"A nucleus is labelled Nostril when a punctum classified as in-nucleus lies inside it. "
|
| 188 |
+
"Images larger than 1536 px are processed in overlapping tiles. Nothing leaves the Space except, "
|
| 189 |
+
"optionally, the counts and an overlay thumbnail sent to Gemini when the summary is requested."
|
| 190 |
+
)
|
| 191 |
+
return demo
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
if __name__ == "__main__":
|
| 195 |
+
if not os.path.exists(WEIGHTS):
|
| 196 |
+
raise SystemExit(f"CellQuant weights not found at {WEIGHTS}: run train.py --out weights/cellquant.pt or set CELLQUANT_WEIGHTS")
|
| 197 |
+
build_demo().launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)),
|
| 198 |
+
allowed_paths=[EX_DIR], show_error=True)
|
cellquant/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CellQuant: nucleus / antibody / puncta ("Nostril") quantification for fluorescence microscopy.
|
| 2 |
+
|
| 3 |
+
Trained on CVAT polygon annotations of 1024x1024 RGB images (blue = DAPI nuclei,
|
| 4 |
+
green = antibody stain such as Iba1 or Tmem119, red = puncta).
|
| 5 |
+
"""
|
| 6 |
+
__version__ = "1.0.0"
|
cellquant/annotations.py
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Read CVAT exports and rasterise them into instance label maps.
|
| 2 |
+
|
| 3 |
+
Supported inputs
|
| 4 |
+
----------------
|
| 5 |
+
* ``annotations.xml`` in the "CVAT for images 1.1" format (``<image>`` with
|
| 6 |
+
``<polygon>``, ``<polyline>``, ``<ellipse>`` children).
|
| 7 |
+
* CVAT task-backup zips containing ``annotations.json`` + ``task.json``.
|
| 8 |
+
* Either may be given as a path to a ``.zip`` or an extracted file.
|
| 9 |
+
|
| 10 |
+
Cells were traced as *closed* polylines (first and last vertex coincide within a
|
| 11 |
+
pixel in every file we received), so polylines are treated as polygons.
|
| 12 |
+
"""
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import io
|
| 16 |
+
import json
|
| 17 |
+
import math
|
| 18 |
+
import os
|
| 19 |
+
import re
|
| 20 |
+
import zipfile
|
| 21 |
+
from dataclasses import dataclass, field
|
| 22 |
+
import xml.etree.ElementTree as ET
|
| 23 |
+
|
| 24 |
+
import numpy as np
|
| 25 |
+
from skimage.draw import polygon as _sk_polygon
|
| 26 |
+
|
| 27 |
+
from .labels import (
|
| 28 |
+
ALL_LABELS,
|
| 29 |
+
cell_attributes,
|
| 30 |
+
is_cell_label,
|
| 31 |
+
is_nostril_label,
|
| 32 |
+
nostril_in_nucleus,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@dataclass
|
| 37 |
+
class Shape:
|
| 38 |
+
label: str
|
| 39 |
+
kind: str # "polygon" or "ellipse"
|
| 40 |
+
points: np.ndarray # (N, 2) float32 x,y vertices (ellipses are sampled)
|
| 41 |
+
source: str = "manual"
|
| 42 |
+
|
| 43 |
+
@property
|
| 44 |
+
def centroid(self) -> np.ndarray:
|
| 45 |
+
return self.points.mean(axis=0)
|
| 46 |
+
|
| 47 |
+
@property
|
| 48 |
+
def area(self) -> float:
|
| 49 |
+
x, y = self.points[:, 0], self.points[:, 1]
|
| 50 |
+
return 0.5 * abs(float(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))))
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@dataclass
|
| 54 |
+
class ImageAnnotation:
|
| 55 |
+
image_name: str
|
| 56 |
+
width: int
|
| 57 |
+
height: int
|
| 58 |
+
shapes: list[Shape] = field(default_factory=list)
|
| 59 |
+
|
| 60 |
+
@property
|
| 61 |
+
def key(self) -> str:
|
| 62 |
+
return image_key(self.image_name)
|
| 63 |
+
|
| 64 |
+
@property
|
| 65 |
+
def norm_name(self) -> str:
|
| 66 |
+
return normalized_name(self.image_name)
|
| 67 |
+
|
| 68 |
+
def count(self, label: str) -> int:
|
| 69 |
+
return sum(1 for s in self.shapes if s.label == label)
|
| 70 |
+
|
| 71 |
+
def counts(self) -> dict[str, int]:
|
| 72 |
+
return {lab: self.count(lab) for lab in ALL_LABELS}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# ----------------------------------------------------------------------------
|
| 76 |
+
# helpers
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def image_key(name: str) -> str:
|
| 80 |
+
"""First whitespace-delimited token of a file name, e.g. ``C110`` for
|
| 81 |
+
``C110 F 35D SC Iba W001 - Focused EDF Document_RGB copy.tif`` (the animal id)."""
|
| 82 |
+
base = os.path.basename(name)
|
| 83 |
+
tok = re.split(r"\s+", base.strip())[0]
|
| 84 |
+
return tok
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def normalized_name(name: str) -> str:
|
| 88 |
+
"""Canonical form of an image file name used to pair annotations with images:
|
| 89 |
+
basename without extension, lower-cased, whitespace collapsed, and trailing
|
| 90 |
+
"copy" / "(n)" suffixes removed (``F110 ... RGB copy.tif`` == ``F110 ... RGB.tif``)."""
|
| 91 |
+
base = os.path.splitext(os.path.basename(name))[0].strip().lower()
|
| 92 |
+
base = re.sub(r"\s+", " ", base)
|
| 93 |
+
while True:
|
| 94 |
+
new = re.sub(r"\s*\(\d+\)$", "", base)
|
| 95 |
+
new = re.sub(r"(\s|-|_)copy$", "", new)
|
| 96 |
+
if new == base:
|
| 97 |
+
return base
|
| 98 |
+
base = new.strip()
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def short_keys(basenames: list[str]) -> list[str]:
|
| 102 |
+
"""Short unique keys for image file names: the animal id (first token, original
|
| 103 |
+
case), extended with the well token (``W001``) or an index only when two
|
| 104 |
+
images share the animal id."""
|
| 105 |
+
firsts = [image_key(b) for b in basenames]
|
| 106 |
+
keys = []
|
| 107 |
+
for b, f in zip(basenames, firsts):
|
| 108 |
+
if firsts.count(f) == 1:
|
| 109 |
+
keys.append(f)
|
| 110 |
+
continue
|
| 111 |
+
m = re.search(r"\b(w\d+)\b", normalized_name(b))
|
| 112 |
+
keys.append(f"{f}_{m.group(1).upper()}" if m else f)
|
| 113 |
+
# still colliding (same animal and well): append an index
|
| 114 |
+
seen: dict[str, int] = {}
|
| 115 |
+
out = []
|
| 116 |
+
for k in keys:
|
| 117 |
+
if keys.count(k) > 1:
|
| 118 |
+
seen[k] = seen.get(k, 0) + 1
|
| 119 |
+
out.append(f"{k}_{seen[k]}")
|
| 120 |
+
else:
|
| 121 |
+
out.append(k)
|
| 122 |
+
return out
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def ellipse_points(cx: float, cy: float, rx: float, ry: float, rotation_deg: float = 0.0,
|
| 126 |
+
n: int = 48) -> np.ndarray:
|
| 127 |
+
t = np.linspace(0.0, 2.0 * math.pi, n, endpoint=False)
|
| 128 |
+
x = rx * np.cos(t)
|
| 129 |
+
y = ry * np.sin(t)
|
| 130 |
+
if rotation_deg:
|
| 131 |
+
a = math.radians(rotation_deg)
|
| 132 |
+
x, y = x * math.cos(a) - y * math.sin(a), x * math.sin(a) + y * math.cos(a)
|
| 133 |
+
return np.stack([cx + x, cy + y], axis=1).astype(np.float32)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _parse_points(s: str) -> np.ndarray:
|
| 137 |
+
pts = [[float(v) for v in p.split(",")] for p in s.strip().split(";") if p.strip()]
|
| 138 |
+
return np.asarray(pts, dtype=np.float32).reshape(-1, 2)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
# ----------------------------------------------------------------------------
|
| 142 |
+
# readers
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def parse_cvat_xml(xml_bytes: bytes) -> list[ImageAnnotation]:
|
| 146 |
+
root = ET.fromstring(xml_bytes)
|
| 147 |
+
out: list[ImageAnnotation] = []
|
| 148 |
+
for im in root.iter("image"):
|
| 149 |
+
ann = ImageAnnotation(image_name=im.get("name"), width=int(im.get("width")),
|
| 150 |
+
height=int(im.get("height")))
|
| 151 |
+
for sh in im:
|
| 152 |
+
label = sh.get("label")
|
| 153 |
+
if label is None:
|
| 154 |
+
continue
|
| 155 |
+
src = sh.get("source", "manual")
|
| 156 |
+
if sh.tag in ("polygon", "polyline"):
|
| 157 |
+
pts = _parse_points(sh.get("points", ""))
|
| 158 |
+
if len(pts) < 3:
|
| 159 |
+
continue
|
| 160 |
+
ann.shapes.append(Shape(label, "polygon", pts, src))
|
| 161 |
+
elif sh.tag == "ellipse":
|
| 162 |
+
cx, cy, rx, ry = (float(sh.get(k)) for k in ("cx", "cy", "rx", "ry"))
|
| 163 |
+
rot = float(sh.get("rotation", 0.0) or 0.0)
|
| 164 |
+
ann.shapes.append(Shape(label, "ellipse", ellipse_points(cx, cy, rx, ry, rot), src))
|
| 165 |
+
elif sh.tag == "box":
|
| 166 |
+
xtl, ytl, xbr, ybr = (float(sh.get(k)) for k in ("xtl", "ytl", "xbr", "ybr"))
|
| 167 |
+
pts = np.array([[xtl, ytl], [xbr, ytl], [xbr, ybr], [xtl, ybr]], dtype=np.float32)
|
| 168 |
+
ann.shapes.append(Shape(label, "polygon", pts, src))
|
| 169 |
+
# points / masks / tags are not used by this project
|
| 170 |
+
out.append(ann)
|
| 171 |
+
return out
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def parse_cvat_backup(zf: zipfile.ZipFile) -> list[ImageAnnotation]:
|
| 175 |
+
"""CVAT task backup: annotations.json (list of jobs with shapes) + manifest."""
|
| 176 |
+
ann_json = json.loads(zf.read("annotations.json"))
|
| 177 |
+
frames: list[tuple[str, int, int]] = []
|
| 178 |
+
for name in zf.namelist():
|
| 179 |
+
if name.endswith("manifest.jsonl"):
|
| 180 |
+
for line in zf.read(name).decode("utf-8").splitlines():
|
| 181 |
+
line = line.strip()
|
| 182 |
+
if not line:
|
| 183 |
+
continue
|
| 184 |
+
rec = json.loads(line)
|
| 185 |
+
if "name" in rec and "width" in rec:
|
| 186 |
+
frames.append((rec["name"] + rec.get("extension", ""), int(rec["width"]), int(rec["height"])))
|
| 187 |
+
out: list[ImageAnnotation] = []
|
| 188 |
+
jobs = ann_json if isinstance(ann_json, list) else [ann_json]
|
| 189 |
+
by_frame: dict[int, ImageAnnotation] = {}
|
| 190 |
+
for job in jobs:
|
| 191 |
+
for sh in job.get("shapes", []):
|
| 192 |
+
fr = int(sh.get("frame", 0))
|
| 193 |
+
if fr not in by_frame:
|
| 194 |
+
name, w, h = frames[fr] if fr < len(frames) else (f"frame_{fr}", 0, 0)
|
| 195 |
+
by_frame[fr] = ImageAnnotation(name, w, h)
|
| 196 |
+
label = sh.get("label")
|
| 197 |
+
pts = np.asarray(sh.get("points", []), dtype=np.float32).reshape(-1, 2)
|
| 198 |
+
kind = sh.get("type")
|
| 199 |
+
if kind in ("polygon", "polyline") and len(pts) >= 3:
|
| 200 |
+
by_frame[fr].shapes.append(Shape(label, "polygon", pts, sh.get("source", "manual")))
|
| 201 |
+
elif kind == "ellipse" and len(pts) >= 2:
|
| 202 |
+
# CVAT stores ellipse as [cx, cy, right_x, top_y]
|
| 203 |
+
cx, cy, rxp, typ = pts[0, 0], pts[0, 1], pts[1, 0], pts[1, 1]
|
| 204 |
+
rx, ry = abs(rxp - cx), abs(cy - typ)
|
| 205 |
+
by_frame[fr].shapes.append(Shape(label, "ellipse", ellipse_points(cx, cy, rx, ry, float(sh.get("rotation", 0.0) or 0.0)), sh.get("source", "manual")))
|
| 206 |
+
elif kind == "rectangle" and len(pts) >= 2:
|
| 207 |
+
(x0, y0), (x1, y1) = pts[0], pts[1]
|
| 208 |
+
by_frame[fr].shapes.append(Shape(label, "polygon", np.array([[x0, y0], [x1, y0], [x1, y1], [x0, y1]], np.float32), sh.get("source", "manual")))
|
| 209 |
+
for fr in sorted(by_frame):
|
| 210 |
+
out.append(by_frame[fr])
|
| 211 |
+
return out
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def load_annotation_file(path: str) -> list[ImageAnnotation]:
|
| 215 |
+
"""Load a CVAT export given as .zip (xml or backup), .xml or .json."""
|
| 216 |
+
if path.lower().endswith(".zip"):
|
| 217 |
+
with zipfile.ZipFile(path) as zf:
|
| 218 |
+
names = zf.namelist()
|
| 219 |
+
if "annotations.xml" in names:
|
| 220 |
+
return parse_cvat_xml(zf.read("annotations.xml"))
|
| 221 |
+
xmls = [n for n in names if n.lower().endswith(".xml")]
|
| 222 |
+
if xmls:
|
| 223 |
+
return parse_cvat_xml(zf.read(xmls[0]))
|
| 224 |
+
if "annotations.json" in names:
|
| 225 |
+
return parse_cvat_backup(zf)
|
| 226 |
+
raise ValueError(f"{path}: no annotations.xml / annotations.json inside the zip")
|
| 227 |
+
if path.lower().endswith(".xml"):
|
| 228 |
+
with open(path, "rb") as f:
|
| 229 |
+
return parse_cvat_xml(f.read())
|
| 230 |
+
raise ValueError(f"unsupported annotation file: {path}")
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
# ----------------------------------------------------------------------------
|
| 234 |
+
# rasterisation
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def rasterize_shape(shape: Shape, height: int, width: int) -> tuple[np.ndarray, np.ndarray]:
|
| 238 |
+
"""Return (rows, cols) of the pixels covered by a shape, clipped to the image.
|
| 239 |
+
|
| 240 |
+
Shapes too thin to enclose any pixel centre still mark their rounded
|
| 241 |
+
centroid, so that tiny puncta (area < 3 px^2) are never lost.
|
| 242 |
+
"""
|
| 243 |
+
pts = shape.points
|
| 244 |
+
rr, cc = _sk_polygon(pts[:, 1], pts[:, 0], shape=(height, width))
|
| 245 |
+
if rr.size == 0:
|
| 246 |
+
cx, cy = shape.centroid
|
| 247 |
+
r, c = int(round(cy)), int(round(cx))
|
| 248 |
+
if 0 <= r < height and 0 <= c < width:
|
| 249 |
+
rr, cc = np.array([r]), np.array([c])
|
| 250 |
+
return rr, cc
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
@dataclass
|
| 254 |
+
class RasterAnnotation:
|
| 255 |
+
"""Instance maps plus per-instance attribute tables (index 0 = background)."""
|
| 256 |
+
cell_inst: np.ndarray # int32 (H, W), 0 = background
|
| 257 |
+
cell_labels: list[str] # len = n_cells + 1, [0] = ""
|
| 258 |
+
nostril_inst: np.ndarray # int32 (H, W)
|
| 259 |
+
nostril_labels: list[str] # len = n_nostrils + 1
|
| 260 |
+
|
| 261 |
+
@property
|
| 262 |
+
def n_cells(self) -> int:
|
| 263 |
+
return len(self.cell_labels) - 1
|
| 264 |
+
|
| 265 |
+
@property
|
| 266 |
+
def n_nostrils(self) -> int:
|
| 267 |
+
return len(self.nostril_labels) - 1
|
| 268 |
+
|
| 269 |
+
def cell_antibody(self) -> np.ndarray:
|
| 270 |
+
"""bool per instance id (index 0 unused)."""
|
| 271 |
+
return np.array([False] + [cell_attributes(l)[0] for l in self.cell_labels[1:]])
|
| 272 |
+
|
| 273 |
+
def cell_has_nostril(self) -> np.ndarray:
|
| 274 |
+
return np.array([False] + [cell_attributes(l)[1] for l in self.cell_labels[1:]])
|
| 275 |
+
|
| 276 |
+
def nostril_inside(self) -> np.ndarray:
|
| 277 |
+
return np.array([False] + [nostril_in_nucleus(l) for l in self.nostril_labels[1:]])
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def rasterize(ann: ImageAnnotation, height: int | None = None, width: int | None = None) -> RasterAnnotation:
|
| 281 |
+
H = height or ann.height
|
| 282 |
+
W = width or ann.width
|
| 283 |
+
cells = [s for s in ann.shapes if is_cell_label(s.label)]
|
| 284 |
+
nos = [s for s in ann.shapes if is_nostril_label(s.label)]
|
| 285 |
+
# Paint large shapes first so that any (rare) overlap keeps the smaller one.
|
| 286 |
+
cells.sort(key=lambda s: -s.area)
|
| 287 |
+
nos.sort(key=lambda s: -s.area)
|
| 288 |
+
cell_inst = np.zeros((H, W), dtype=np.int32)
|
| 289 |
+
cell_labels = [""]
|
| 290 |
+
for s in cells:
|
| 291 |
+
rr, cc = rasterize_shape(s, H, W)
|
| 292 |
+
if rr.size == 0:
|
| 293 |
+
continue
|
| 294 |
+
cell_labels.append(s.label)
|
| 295 |
+
cell_inst[rr, cc] = len(cell_labels) - 1
|
| 296 |
+
nos_inst = np.zeros((H, W), dtype=np.int32)
|
| 297 |
+
nos_labels = [""]
|
| 298 |
+
for s in nos:
|
| 299 |
+
rr, cc = rasterize_shape(s, H, W)
|
| 300 |
+
if rr.size == 0:
|
| 301 |
+
continue
|
| 302 |
+
nos_labels.append(s.label)
|
| 303 |
+
nos_inst[rr, cc] = len(nos_labels) - 1
|
| 304 |
+
# Drop instances that were completely over-painted (keeps ids contiguous).
|
| 305 |
+
cell_inst, cell_labels = _compact(cell_inst, cell_labels)
|
| 306 |
+
nos_inst, nos_labels = _compact(nos_inst, nos_labels)
|
| 307 |
+
return RasterAnnotation(cell_inst, cell_labels, nos_inst, nos_labels)
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def _compact(inst: np.ndarray, labels: list[str]) -> tuple[np.ndarray, list[str]]:
|
| 311 |
+
present = np.unique(inst)
|
| 312 |
+
present = present[present > 0]
|
| 313 |
+
if len(present) == len(labels) - 1:
|
| 314 |
+
return inst, labels
|
| 315 |
+
remap = np.zeros(len(labels), dtype=np.int32)
|
| 316 |
+
new_labels = [""]
|
| 317 |
+
for old in present:
|
| 318 |
+
new_labels.append(labels[old])
|
| 319 |
+
remap[old] = len(new_labels) - 1
|
| 320 |
+
return remap[inst], new_labels
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
# ----------------------------------------------------------------------------
|
| 324 |
+
# dataset discovery
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
IMAGE_EXTS = (".tif", ".tiff", ".png", ".jpg", ".jpeg")
|
| 328 |
+
ANN_EXTS = (".zip", ".xml")
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
@dataclass
|
| 332 |
+
class Sample:
|
| 333 |
+
key: str
|
| 334 |
+
image_path: str
|
| 335 |
+
annotation: ImageAnnotation
|
| 336 |
+
annotation_path: str
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
class _quiet_tifffile:
|
| 340 |
+
"""Silence tifffile's log line about the inconsistent OME block in the Keyence exports."""
|
| 341 |
+
|
| 342 |
+
def __enter__(self):
|
| 343 |
+
import logging
|
| 344 |
+
self.lg = logging.getLogger("tifffile")
|
| 345 |
+
self.lvl = self.lg.level
|
| 346 |
+
self.lg.setLevel(logging.ERROR)
|
| 347 |
+
return self
|
| 348 |
+
|
| 349 |
+
def __exit__(self, *exc):
|
| 350 |
+
self.lg.setLevel(self.lvl)
|
| 351 |
+
return False
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
def image_size(path: str) -> tuple[int, int]:
|
| 355 |
+
"""(height, width) read from the file header only."""
|
| 356 |
+
if path.lower().endswith((".tif", ".tiff")):
|
| 357 |
+
import tifffile
|
| 358 |
+
with _quiet_tifffile(), tifffile.TiffFile(path) as tf:
|
| 359 |
+
try:
|
| 360 |
+
shp = tf.series[0].shape
|
| 361 |
+
except Exception: # noqa: BLE001
|
| 362 |
+
shp = tf.pages[0].shape
|
| 363 |
+
if len(shp) == 3 and shp[0] in (3, 4) and shp[-1] > 4:
|
| 364 |
+
return int(shp[1]), int(shp[2])
|
| 365 |
+
return int(shp[0]), int(shp[1])
|
| 366 |
+
from PIL import Image
|
| 367 |
+
with Image.open(path) as im:
|
| 368 |
+
w, h = im.size
|
| 369 |
+
return h, w
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def discover(data_dir: str, verbose: bool = True, check_sizes: bool = True) -> tuple[list[Sample], list[str]]:
|
| 373 |
+
"""Pair every annotation export in ``data_dir`` with its image.
|
| 374 |
+
|
| 375 |
+
Returns (samples, unannotated_image_paths). Pairing uses the image name
|
| 376 |
+
recorded *inside* the annotation (not the zip name): first an exact match on
|
| 377 |
+
the normalised file name (extension, case, whitespace and "copy"/"(n)"
|
| 378 |
+
suffixes ignored), else the animal id (first token) when it is unambiguous.
|
| 379 |
+
Byte-identical duplicate copies of an image collapse to one entry; two
|
| 380 |
+
*different* images or two different annotation files that map to the same
|
| 381 |
+
name are reported and refused rather than silently dropped. Sample keys are
|
| 382 |
+
the animal id, extended with the well token when one animal has several
|
| 383 |
+
sections (``F110_W002``).
|
| 384 |
+
"""
|
| 385 |
+
files = sorted(os.listdir(data_dir))
|
| 386 |
+
images: dict[str, list[str]] = {}
|
| 387 |
+
for f in files:
|
| 388 |
+
if f.lower().endswith(IMAGE_EXTS):
|
| 389 |
+
images.setdefault(normalized_name(f), []).append(os.path.join(data_dir, f))
|
| 390 |
+
# collapse byte-identical copies; refuse different files with the same normalised name
|
| 391 |
+
image_path: dict[str, str] = {}
|
| 392 |
+
for n, paths in images.items():
|
| 393 |
+
if len(paths) > 1:
|
| 394 |
+
digests = {_digest(p) for p in paths}
|
| 395 |
+
if len(digests) > 1:
|
| 396 |
+
raise ValueError(f"{data_dir}: {len(paths)} different image files share the name '{n}': {[os.path.basename(p) for p in paths]}")
|
| 397 |
+
image_path[n] = paths[0]
|
| 398 |
+
by_first: dict[str, list[str]] = {}
|
| 399 |
+
for n in image_path:
|
| 400 |
+
by_first.setdefault(image_key(n), []).append(n)
|
| 401 |
+
|
| 402 |
+
paired: dict[str, tuple[ImageAnnotation, str]] = {} # norm image name -> (annotation, annotation path)
|
| 403 |
+
for f in files:
|
| 404 |
+
if not f.lower().endswith(ANN_EXTS):
|
| 405 |
+
continue
|
| 406 |
+
path = os.path.join(data_dir, f)
|
| 407 |
+
try:
|
| 408 |
+
anns = load_annotation_file(path)
|
| 409 |
+
except Exception as e: # noqa: BLE001
|
| 410 |
+
if verbose:
|
| 411 |
+
print(f"[discover] skip {f}: {e}")
|
| 412 |
+
continue
|
| 413 |
+
for ann in anns:
|
| 414 |
+
n = ann.norm_name
|
| 415 |
+
if n not in image_path:
|
| 416 |
+
cands = by_first.get(image_key(n), [])
|
| 417 |
+
if len(cands) == 1:
|
| 418 |
+
n = cands[0]
|
| 419 |
+
else:
|
| 420 |
+
if verbose:
|
| 421 |
+
print(f"[discover] {f}: no image for '{ann.image_name}'" + (f" (ambiguous: {cands})" if cands else ""))
|
| 422 |
+
continue
|
| 423 |
+
if n in paired:
|
| 424 |
+
prev_ann, prev_path = paired[n]
|
| 425 |
+
if _shapes_equal(prev_ann, ann):
|
| 426 |
+
if verbose:
|
| 427 |
+
print(f"[discover] {f}: same annotations as {os.path.basename(prev_path)} for '{ann.image_name}', keeping the first")
|
| 428 |
+
continue
|
| 429 |
+
raise ValueError(f"{data_dir}: two different annotation files for image '{ann.image_name}': "
|
| 430 |
+
f"{os.path.basename(prev_path)} and {f}")
|
| 431 |
+
paired[n] = (ann, path)
|
| 432 |
+
names = sorted(paired)
|
| 433 |
+
keys = short_keys([os.path.basename(image_path[n]) for n in names])
|
| 434 |
+
samples = []
|
| 435 |
+
for n, k in zip(names, keys):
|
| 436 |
+
ann, apath = paired[n]
|
| 437 |
+
ipath = image_path[n]
|
| 438 |
+
if check_sizes:
|
| 439 |
+
h, w = image_size(ipath)
|
| 440 |
+
if (h, w) != (ann.height, ann.width):
|
| 441 |
+
raise ValueError(f"{os.path.basename(ipath)}: image is {w}x{h} but the annotation canvas is {ann.width}x{ann.height}")
|
| 442 |
+
samples.append(Sample(k, ipath, ann, apath))
|
| 443 |
+
unannotated = [p for n, p in image_path.items() if n not in paired]
|
| 444 |
+
return samples, sorted(unannotated)
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
def _digest(path: str) -> str:
|
| 448 |
+
import hashlib
|
| 449 |
+
h = hashlib.sha256()
|
| 450 |
+
with open(path, "rb") as f:
|
| 451 |
+
for chunk in iter(lambda: f.read(1 << 20), b""):
|
| 452 |
+
h.update(chunk)
|
| 453 |
+
return h.hexdigest()
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
def _shapes_equal(a: ImageAnnotation, b: ImageAnnotation, tol: float = 0.5) -> bool:
|
| 457 |
+
"""Same labels and the same shapes (centroids within ``tol`` px, same vertex
|
| 458 |
+
count), in any order. Tolerant to the coordinate rounding that differs
|
| 459 |
+
between CVAT's XML (2 decimals) and JSON exports."""
|
| 460 |
+
if len(a.shapes) != len(b.shapes):
|
| 461 |
+
return False
|
| 462 |
+
pool = list(b.shapes)
|
| 463 |
+
for s in a.shapes:
|
| 464 |
+
c = s.centroid
|
| 465 |
+
hit = None
|
| 466 |
+
for j, t in enumerate(pool):
|
| 467 |
+
if t.label == s.label and len(t.points) == len(s.points) and float(np.abs(t.centroid - c).max()) <= tol:
|
| 468 |
+
hit = j
|
| 469 |
+
break
|
| 470 |
+
if hit is None:
|
| 471 |
+
return False
|
| 472 |
+
pool.pop(hit)
|
| 473 |
+
return True
|
| 474 |
+
|
| 475 |
+
|
| 476 |
+
def load_image(path: str) -> np.ndarray:
|
| 477 |
+
"""Load an RGB uint8 image (H, W, 3) from tif/png/jpg.
|
| 478 |
+
|
| 479 |
+
* TIFFs are read as a whole series, so channel-per-page files (ImageJ
|
| 480 |
+
composites, OME-TIFF ``CYX``) load as 3 channels; stacks with more than
|
| 481 |
+
four planes are refused rather than silently reduced to one plane.
|
| 482 |
+
* Compressed TIFFs that tifffile cannot decode fall back to Pillow.
|
| 483 |
+
* Palette / CMYK / LA images are converted to RGB(A) by Pillow.
|
| 484 |
+
* 16-bit inputs are rescaled by their 99.95th percentile (see to_rgb_uint8).
|
| 485 |
+
"""
|
| 486 |
+
if path.lower().endswith((".tif", ".tiff")):
|
| 487 |
+
import logging
|
| 488 |
+
import tifffile
|
| 489 |
+
lg = logging.getLogger("tifffile")
|
| 490 |
+
lvl = lg.level
|
| 491 |
+
lg.setLevel(logging.ERROR) # the Keyence exports carry an inconsistent OME block; tifffile logs it
|
| 492 |
+
try:
|
| 493 |
+
with tifffile.TiffFile(path) as tf:
|
| 494 |
+
try:
|
| 495 |
+
a = tf.series[0].asarray()
|
| 496 |
+
except Exception: # noqa: BLE001 - malformed series metadata: stack the pages
|
| 497 |
+
pages = [p.asarray() for p in tf.pages]
|
| 498 |
+
a = pages[0] if len(pages) == 1 else np.stack(pages)
|
| 499 |
+
except Exception as e: # noqa: BLE001 - e.g. LZW/JPEG without imagecodecs
|
| 500 |
+
lg.setLevel(lvl)
|
| 501 |
+
from PIL import Image
|
| 502 |
+
try:
|
| 503 |
+
a = _pil_array(Image.open(path))
|
| 504 |
+
except Exception:
|
| 505 |
+
raise ValueError(f"cannot decode TIFF {os.path.basename(path)}: {e}") from e
|
| 506 |
+
finally:
|
| 507 |
+
lg.setLevel(lvl)
|
| 508 |
+
a = np.asarray(a)
|
| 509 |
+
if a.ndim == 3 and a.shape[0] > 4 and a.shape[-1] > 4:
|
| 510 |
+
raise ValueError(f"{os.path.basename(path)}: {a.shape[0]}-plane stack; export a single RGB plane (blue nuclei, green antibody, red puncta)")
|
| 511 |
+
if a.ndim == 4:
|
| 512 |
+
raise ValueError(f"{os.path.basename(path)}: unsupported {a.ndim}-D TIFF of shape {a.shape}")
|
| 513 |
+
else:
|
| 514 |
+
from PIL import Image
|
| 515 |
+
a = _pil_array(Image.open(path))
|
| 516 |
+
return to_rgb_uint8(a)
|
| 517 |
+
|
| 518 |
+
|
| 519 |
+
def _pil_array(im) -> np.ndarray:
|
| 520 |
+
"""Pillow image -> array with a known mode (keeps 16-bit/float grey untouched)."""
|
| 521 |
+
if im.mode in ("RGB", "RGBA", "L", "I", "I;16", "I;16B", "I;16L", "F"):
|
| 522 |
+
return np.asarray(im)
|
| 523 |
+
if im.mode in ("LA", "PA"):
|
| 524 |
+
return np.asarray(im.convert("RGBA"))
|
| 525 |
+
return np.asarray(im.convert("RGB"))
|
| 526 |
+
|
| 527 |
+
|
| 528 |
+
def to_rgb_uint8(a: np.ndarray) -> np.ndarray:
|
| 529 |
+
a = np.asarray(a)
|
| 530 |
+
if a.ndim == 3 and a.shape[0] in (3, 4) and a.shape[-1] > 4:
|
| 531 |
+
a = np.moveaxis(a, 0, -1) # channel-first (C, H, W) -> channel-last
|
| 532 |
+
if a.ndim == 2:
|
| 533 |
+
a = np.repeat(a[..., None], 3, axis=-1)
|
| 534 |
+
if a.ndim != 3:
|
| 535 |
+
raise ValueError(f"expected an RGB image, got shape {a.shape}")
|
| 536 |
+
if a.shape[-1] == 4:
|
| 537 |
+
a = a[..., :3]
|
| 538 |
+
if a.shape[-1] != 3:
|
| 539 |
+
raise ValueError(f"expected an RGB image, got shape {a.shape}")
|
| 540 |
+
if a.dtype != np.uint8:
|
| 541 |
+
a = a.astype(np.float32)
|
| 542 |
+
if a.size == 0:
|
| 543 |
+
return np.zeros(a.shape, np.uint8)
|
| 544 |
+
hi = float(a.max())
|
| 545 |
+
if np.issubdtype(np.asarray(a).dtype, np.floating) and hi <= 1.0 and float(a.min()) >= 0.0:
|
| 546 |
+
scale = 255.0 # float image in [0, 1]
|
| 547 |
+
else:
|
| 548 |
+
# Robust auto-scale for 16-bit / float data: the 99.95th percentile maps to 255,
|
| 549 |
+
# so one saturated pixel cannot darken the whole image.
|
| 550 |
+
ref = float(np.percentile(a, 99.95))
|
| 551 |
+
if ref <= 0:
|
| 552 |
+
ref = hi if hi > 0 else 1.0
|
| 553 |
+
scale = 255.0 / ref
|
| 554 |
+
a = np.clip(np.round(a * scale), 0, 255).astype(np.uint8)
|
| 555 |
+
return np.ascontiguousarray(a)
|
cellquant/data.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Training targets and augmentation.
|
| 2 |
+
|
| 3 |
+
Targets built from a :class:`RasterAnnotation` (all (H, W)):
|
| 4 |
+
cell3 int64 0 bg, 1 nucleus interior, 2 nucleus border (2-px inner ring)
|
| 5 |
+
antibody float 1 where nucleus is antibody+, 0 where antibody-; valid on nucleus px
|
| 6 |
+
cellnos float 1 where nucleus has a punctum ("Nostril"); valid on nucleus px
|
| 7 |
+
cellmask float 1 on nucleus pixels (loss mask for the two attribute heads)
|
| 8 |
+
nos3 int64 0 bg, 1 punctum in nucleus, 2 punctum external
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import math
|
| 13 |
+
from dataclasses import dataclass
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
import torch
|
| 17 |
+
from scipy import ndimage as ndi
|
| 18 |
+
from skimage.segmentation import find_boundaries
|
| 19 |
+
|
| 20 |
+
from .annotations import RasterAnnotation
|
| 21 |
+
|
| 22 |
+
BORDER_WIDTH = 2 # pixels of the inner border ring
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def make_targets(R: RasterAnnotation) -> dict[str, np.ndarray]:
|
| 26 |
+
inst = R.cell_inst
|
| 27 |
+
fg = inst > 0
|
| 28 |
+
border = find_boundaries(inst, mode="inner")
|
| 29 |
+
if BORDER_WIDTH > 1:
|
| 30 |
+
border = ndi.binary_dilation(border, iterations=BORDER_WIDTH - 1) & fg
|
| 31 |
+
cell3 = np.zeros(inst.shape, dtype=np.int64)
|
| 32 |
+
cell3[fg] = 1
|
| 33 |
+
cell3[border] = 2
|
| 34 |
+
ab = R.cell_antibody().astype(np.float32)[inst]
|
| 35 |
+
cn = R.cell_has_nostril().astype(np.float32)[inst]
|
| 36 |
+
ninst = R.nostril_inst
|
| 37 |
+
nin = R.nostril_inside()
|
| 38 |
+
nos3 = np.zeros(inst.shape, dtype=np.int64)
|
| 39 |
+
nos3[ninst > 0] = np.where(nin[ninst[ninst > 0]], 1, 2)
|
| 40 |
+
return {
|
| 41 |
+
"cell3": cell3,
|
| 42 |
+
"antibody": ab,
|
| 43 |
+
"cellnos": cn,
|
| 44 |
+
"cellmask": fg.astype(np.float32),
|
| 45 |
+
"nos3": nos3,
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@dataclass
|
| 50 |
+
class AugConfig:
|
| 51 |
+
crop: int = 512
|
| 52 |
+
scale_min: float = 0.8
|
| 53 |
+
scale_max: float = 1.25
|
| 54 |
+
gain_min: float = 0.6
|
| 55 |
+
gain_max: float = 1.5
|
| 56 |
+
gamma_min: float = 0.7
|
| 57 |
+
gamma_max: float = 1.4
|
| 58 |
+
noise_max: float = 0.03
|
| 59 |
+
blur_max: float = 1.0
|
| 60 |
+
p_blur: float = 0.3
|
| 61 |
+
p_channel_shift: float = 0.5 # per-channel additive offset
|
| 62 |
+
offset_max: float = 0.03
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class PatchSampler:
|
| 66 |
+
"""Random augmented patches from whole images kept in memory.
|
| 67 |
+
|
| 68 |
+
Geometric: random scale (zoom), random crop, random dihedral transform.
|
| 69 |
+
Photometric: per-channel gain, gamma, additive offset, Gaussian noise, blur.
|
| 70 |
+
Label maps use nearest-neighbour resampling.
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
def __init__(self, images: list[np.ndarray], targets: list[dict[str, np.ndarray]],
|
| 74 |
+
cfg: AugConfig | None = None, seed: int = 0):
|
| 75 |
+
assert len(images) == len(targets) and len(images) > 0
|
| 76 |
+
self.images = images
|
| 77 |
+
self.targets = targets
|
| 78 |
+
self.cfg = cfg or AugConfig()
|
| 79 |
+
self.rng = np.random.default_rng(seed)
|
| 80 |
+
|
| 81 |
+
def _one(self) -> tuple[np.ndarray, dict[str, np.ndarray]]:
|
| 82 |
+
cfg, rng = self.cfg, self.rng
|
| 83 |
+
i = int(rng.integers(len(self.images)))
|
| 84 |
+
img, tg = self.images[i], self.targets[i]
|
| 85 |
+
H, W = img.shape[:2]
|
| 86 |
+
s = float(rng.uniform(cfg.scale_min, cfg.scale_max)) # zoom factor applied to the crop
|
| 87 |
+
src = int(round(cfg.crop / s)) # source window size
|
| 88 |
+
src = max(32, min(src, H, W))
|
| 89 |
+
y0 = int(rng.integers(0, H - src + 1))
|
| 90 |
+
x0 = int(rng.integers(0, W - src + 1))
|
| 91 |
+
patch = img[y0:y0 + src, x0:x0 + src].astype(np.float32) / 255.0
|
| 92 |
+
tpatch = {k: v[y0:y0 + src, x0:x0 + src] for k, v in tg.items()}
|
| 93 |
+
if src != cfg.crop:
|
| 94 |
+
zoom = cfg.crop / src
|
| 95 |
+
patch = ndi.zoom(patch, (zoom, zoom, 1), order=1)
|
| 96 |
+
tpatch = {k: ndi.zoom(v, (zoom, zoom), order=0) for k, v in tpatch.items()}
|
| 97 |
+
patch = patch[:cfg.crop, :cfg.crop]
|
| 98 |
+
tpatch = {k: v[:cfg.crop, :cfg.crop] for k, v in tpatch.items()}
|
| 99 |
+
if patch.shape[0] != cfg.crop or patch.shape[1] != cfg.crop: # rounding guard
|
| 100 |
+
ph, pw = cfg.crop - patch.shape[0], cfg.crop - patch.shape[1]
|
| 101 |
+
patch = np.pad(patch, ((0, ph), (0, pw), (0, 0)), mode="reflect")
|
| 102 |
+
tpatch = {k: np.pad(v, ((0, ph), (0, pw)), mode="reflect") for k, v in tpatch.items()}
|
| 103 |
+
# dihedral
|
| 104 |
+
k = int(rng.integers(4))
|
| 105 |
+
flip = bool(rng.integers(2))
|
| 106 |
+
patch = np.rot90(patch, k, axes=(0, 1))
|
| 107 |
+
tpatch = {kk: np.rot90(v, k, axes=(0, 1)) for kk, v in tpatch.items()}
|
| 108 |
+
if flip:
|
| 109 |
+
patch = patch[:, ::-1]
|
| 110 |
+
tpatch = {kk: v[:, ::-1] for kk, v in tpatch.items()}
|
| 111 |
+
# photometric
|
| 112 |
+
gain = rng.uniform(cfg.gain_min, cfg.gain_max, size=3).astype(np.float32)
|
| 113 |
+
gamma = float(rng.uniform(cfg.gamma_min, cfg.gamma_max))
|
| 114 |
+
patch = np.clip(patch * gain, 0, 1) ** gamma
|
| 115 |
+
if rng.random() < cfg.p_channel_shift:
|
| 116 |
+
patch = patch + rng.uniform(-cfg.offset_max, cfg.offset_max, size=3).astype(np.float32)
|
| 117 |
+
if rng.random() < cfg.p_blur:
|
| 118 |
+
sig = float(rng.uniform(0.3, cfg.blur_max))
|
| 119 |
+
patch = ndi.gaussian_filter(patch, sigma=(sig, sig, 0))
|
| 120 |
+
sigma_n = float(rng.uniform(0, cfg.noise_max))
|
| 121 |
+
if sigma_n > 0:
|
| 122 |
+
patch = patch + rng.normal(0, sigma_n, size=patch.shape).astype(np.float32)
|
| 123 |
+
patch = np.clip(patch, 0, 1).astype(np.float32)
|
| 124 |
+
return np.ascontiguousarray(patch), {k: np.ascontiguousarray(v) for k, v in tpatch.items()}
|
| 125 |
+
|
| 126 |
+
def batch(self, n: int) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:
|
| 127 |
+
xs, ts = [], []
|
| 128 |
+
for _ in range(n):
|
| 129 |
+
x, t = self._one()
|
| 130 |
+
xs.append(x)
|
| 131 |
+
ts.append(t)
|
| 132 |
+
x = torch.from_numpy(np.stack(xs)).permute(0, 3, 1, 2).contiguous()
|
| 133 |
+
out = {}
|
| 134 |
+
for k in ts[0]:
|
| 135 |
+
arr = np.stack([t[k] for t in ts])
|
| 136 |
+
out[k] = torch.from_numpy(arr)
|
| 137 |
+
return x, out
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class Prefetcher:
|
| 141 |
+
"""Builds augmented batches in background threads so the GPU never waits.
|
| 142 |
+
|
| 143 |
+
Each worker owns its own sampler (independent RNG stream) and its own
|
| 144 |
+
queue; ``next()`` round-robins over the workers, so the batch sequence is
|
| 145 |
+
fully determined by (seed, workers) regardless of thread scheduling.
|
| 146 |
+
"""
|
| 147 |
+
|
| 148 |
+
def __init__(self, images, targets, cfg: AugConfig, batch: int, seed: int = 0, workers: int = 3, depth: int = 6):
|
| 149 |
+
import queue
|
| 150 |
+
import threading
|
| 151 |
+
workers = max(1, int(workers))
|
| 152 |
+
self.batch = batch
|
| 153 |
+
self.workers = workers
|
| 154 |
+
self._i = 0
|
| 155 |
+
self._stop = threading.Event()
|
| 156 |
+
self.qs: list["queue.Queue"] = [queue.Queue(maxsize=max(1, depth // workers)) for _ in range(workers)]
|
| 157 |
+
self._threads = []
|
| 158 |
+
for w in range(workers):
|
| 159 |
+
s = PatchSampler(images, targets, cfg, seed=seed * 1000 + w)
|
| 160 |
+
th = threading.Thread(target=self._work, args=(s, self.qs[w]), daemon=True)
|
| 161 |
+
th.start()
|
| 162 |
+
self._threads.append(th)
|
| 163 |
+
|
| 164 |
+
def _work(self, sampler: PatchSampler, q) -> None:
|
| 165 |
+
import queue
|
| 166 |
+
while not self._stop.is_set():
|
| 167 |
+
b = sampler.batch(self.batch)
|
| 168 |
+
while not self._stop.is_set():
|
| 169 |
+
try:
|
| 170 |
+
q.put(b, timeout=0.5)
|
| 171 |
+
break
|
| 172 |
+
except queue.Full:
|
| 173 |
+
continue
|
| 174 |
+
|
| 175 |
+
def next(self):
|
| 176 |
+
q = self.qs[self._i]
|
| 177 |
+
self._i = (self._i + 1) % self.workers
|
| 178 |
+
return q.get()
|
| 179 |
+
|
| 180 |
+
def close(self) -> None:
|
| 181 |
+
self._stop.set()
|
cellquant/gemini_summary.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Optional plain-language summary of the counts via Gemini.
|
| 2 |
+
|
| 3 |
+
Enabled only when the ``GEMINI_API_KEY`` environment variable (a Hugging Face
|
| 4 |
+
Space *secret*) is set. The model id comes from ``GEMINI_MODEL``. Gemini never
|
| 5 |
+
produces or alters any count: it only receives the numbers computed by the
|
| 6 |
+
trained network and a thumbnail of the overlay, and is instructed to report
|
| 7 |
+
them verbatim.
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
|
| 13 |
+
from PIL import Image
|
| 14 |
+
|
| 15 |
+
DEFAULT_MODEL = os.environ.get("GEMINI_MODEL", "gemini-4-argon")
|
| 16 |
+
FALLBACK_MODELS = ("gemini-3.8-flash", "gemini-2.5-flash")
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def enabled() -> bool:
|
| 20 |
+
return bool(os.environ.get("GEMINI_API_KEY"))
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _prompt(rows: list[dict]) -> str:
|
| 24 |
+
lines = []
|
| 25 |
+
for r in rows:
|
| 26 |
+
lines.append(f"- {r['image']}: " + ", ".join(f"{k} = {v}" for k, v in r["counts"].items()))
|
| 27 |
+
return (
|
| 28 |
+
"You are helping a neuroscience lab read the output of an automated image analysis "
|
| 29 |
+
"of spinal-cord fluorescence sections (blue = DAPI nuclei, green = microglial antibody "
|
| 30 |
+
"stain such as Iba1 or TMEM119, red = puncta that the lab calls 'Nostril').\n"
|
| 31 |
+
"A trained segmentation network produced these counts per image:\n"
|
| 32 |
+
+ "\n".join(lines)
|
| 33 |
+
+ "\n\nLabel meanings: every nucleus is in exactly ONE of the four 'Cell' categories (they are mutually "
|
| 34 |
+
"exclusive, so total nuclei = sum of the four). 'Cell, Antibody +' = nucleus with green antibody signal and no "
|
| 35 |
+
"punctum; 'Cell, Antibody -' = nucleus without antibody signal and no punctum; 'Cell, Nostril, Antibody +/-' = "
|
| 36 |
+
"nucleus containing a red punctum, with/without antibody signal. 'Nostril in Nucleus' = punctum inside a nucleus; "
|
| 37 |
+
"'Nostril External of Nucleus' = punctum outside nuclei.\n"
|
| 38 |
+
"Write a concise summary (max 150 words) for the lab notebook: restate the counts exactly as given "
|
| 39 |
+
"(do not round, estimate or invent numbers), give the fraction of nuclei that are antibody positive "
|
| 40 |
+
"and the fraction that contain a punctum, and mention anything in the overlay thumbnail that looks "
|
| 41 |
+
"like a quality problem (out of focus, very dark, debris, edge artefacts). If nothing looks wrong, say so."
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def summarize(rows: list[dict], overlay: Image.Image | None = None, model: str | None = None) -> str:
|
| 46 |
+
"""rows: [{"image": name, "counts": {label: int}}]. Returns the summary text or an error message."""
|
| 47 |
+
key = os.environ.get("GEMINI_API_KEY")
|
| 48 |
+
if not key:
|
| 49 |
+
return "Gemini summary disabled: set the GEMINI_API_KEY secret."
|
| 50 |
+
try:
|
| 51 |
+
from google import genai
|
| 52 |
+
except ImportError:
|
| 53 |
+
return "Gemini summary unavailable: install google-genai."
|
| 54 |
+
contents: list = [_prompt(rows)]
|
| 55 |
+
if overlay is not None:
|
| 56 |
+
thumb = overlay.copy()
|
| 57 |
+
thumb.thumbnail((768, 768))
|
| 58 |
+
contents.append(thumb)
|
| 59 |
+
client = genai.Client(api_key=key)
|
| 60 |
+
candidates = [model or DEFAULT_MODEL] + [m for m in FALLBACK_MODELS if m != (model or DEFAULT_MODEL)]
|
| 61 |
+
last = ""
|
| 62 |
+
for mid in candidates:
|
| 63 |
+
try:
|
| 64 |
+
resp = client.models.generate_content(model=mid, contents=contents)
|
| 65 |
+
text = (resp.text or "").strip()
|
| 66 |
+
return text if text else "Gemini returned an empty response."
|
| 67 |
+
except Exception as e: # noqa: BLE001 - try the next model id, then surface the error to the UI
|
| 68 |
+
last = f"{type(e).__name__}: {e}"
|
| 69 |
+
if "NOT_FOUND" not in str(e) and "not found" not in str(e).lower():
|
| 70 |
+
break
|
| 71 |
+
return f"Gemini summary failed ({last})"
|
cellquant/infer.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Inference: image -> probabilities -> instances. CPU-friendly, optional TTA and tiling."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from .annotations import to_rgb_uint8
|
| 8 |
+
from .model import CellUNet, load_checkpoint, pad_to_multiple
|
| 9 |
+
from .postprocess import PostConfig, Result, analyze, logits_to_probs
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def pick_device(pref: str = "auto") -> torch.device:
|
| 13 |
+
if pref != "auto":
|
| 14 |
+
return torch.device(pref)
|
| 15 |
+
if torch.cuda.is_available():
|
| 16 |
+
return torch.device("cuda")
|
| 17 |
+
if torch.backends.mps.is_available():
|
| 18 |
+
return torch.device("mps")
|
| 19 |
+
return torch.device("cpu")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# The 8 dihedral transforms as (k rotations, flip) with their inverses handled below.
|
| 23 |
+
DIHEDRAL = [(k, f) for f in (False, True) for k in range(4)]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _fwd_t(x: torch.Tensor, k: int, flip: bool) -> torch.Tensor:
|
| 27 |
+
if flip:
|
| 28 |
+
x = torch.flip(x, dims=[-1])
|
| 29 |
+
return torch.rot90(x, k, dims=[-2, -1])
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _inv_t(x: torch.Tensor, k: int, flip: bool) -> torch.Tensor:
|
| 33 |
+
x = torch.rot90(x, -k, dims=[-2, -1])
|
| 34 |
+
if flip:
|
| 35 |
+
x = torch.flip(x, dims=[-1])
|
| 36 |
+
return x
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class Predictor:
|
| 40 |
+
def __init__(self, weights: str | CellUNet, device: str = "auto", tta: int = 1,
|
| 41 |
+
tile: int = 1024, overlap: int = 96, max_whole: int = 1536):
|
| 42 |
+
"""tta: 1 (none), 4 (flips + 180 rotation) or 8 (full dihedral group)."""
|
| 43 |
+
self.device = pick_device(device)
|
| 44 |
+
if isinstance(weights, CellUNet):
|
| 45 |
+
self.model, self.meta = weights, {}
|
| 46 |
+
else:
|
| 47 |
+
self.model, self.meta = load_checkpoint(weights)
|
| 48 |
+
self.model.to(self.device).eval()
|
| 49 |
+
assert tta in (1, 4, 8)
|
| 50 |
+
self.tta = tta
|
| 51 |
+
self.tile, self.overlap, self.max_whole = tile, overlap, max_whole
|
| 52 |
+
|
| 53 |
+
@torch.no_grad()
|
| 54 |
+
def _probs_tensor(self, x: torch.Tensor) -> torch.Tensor:
|
| 55 |
+
"""x: (1, 3, H, W) float in [0,1] on device, H, W multiples of 32 -> (8-ch probabilities) (1, 8, H, W)."""
|
| 56 |
+
transforms = DIHEDRAL[:1] if self.tta == 1 else ([(0, False), (2, False), (0, True), (2, True)] if self.tta == 4 else DIHEDRAL)
|
| 57 |
+
acc = None
|
| 58 |
+
for k, f in transforms:
|
| 59 |
+
out = self.model(_fwd_t(x, k, f))
|
| 60 |
+
out = _inv_t(out, k, f)
|
| 61 |
+
p = torch.cat([out[:, 0:3].softmax(1), out[:, 3:4].sigmoid(), out[:, 4:5].sigmoid(), out[:, 5:8].softmax(1)], dim=1)
|
| 62 |
+
acc = p if acc is None else acc + p
|
| 63 |
+
return acc / len(transforms)
|
| 64 |
+
|
| 65 |
+
@torch.no_grad()
|
| 66 |
+
def probs(self, img: np.ndarray) -> dict[str, np.ndarray]:
|
| 67 |
+
img = to_rgb_uint8(img)
|
| 68 |
+
H, W = img.shape[:2]
|
| 69 |
+
x = torch.from_numpy(img).permute(2, 0, 1).float().div_(255.0).unsqueeze(0)
|
| 70 |
+
if max(H, W) <= self.max_whole:
|
| 71 |
+
xp, _ = pad_to_multiple(x, 32)
|
| 72 |
+
p = self._probs_tensor(xp.to(self.device))[0, :, :H, :W].cpu().numpy()
|
| 73 |
+
else:
|
| 74 |
+
p = self._tiled(x)[:, :H, :W]
|
| 75 |
+
return {"cell": p[0:3], "antibody": p[3], "cellnos": p[4], "nos": p[5:8]}
|
| 76 |
+
|
| 77 |
+
def _tiled(self, x: torch.Tensor) -> np.ndarray:
|
| 78 |
+
"""Overlapping tiles blended with a linear ramp (no visible seams)."""
|
| 79 |
+
_, _, H, W = x.shape
|
| 80 |
+
t, o = self.tile, self.overlap
|
| 81 |
+
step = t - o
|
| 82 |
+
acc = np.zeros((8, H, W), np.float32)
|
| 83 |
+
wsum = np.zeros((1, H, W), np.float32)
|
| 84 |
+
ramp = np.minimum(np.arange(t) + 1, np.arange(t)[::-1] + 1).astype(np.float32)
|
| 85 |
+
ramp = np.minimum(ramp, o) / o
|
| 86 |
+
w2 = (ramp[:, None] * ramp[None, :])[None]
|
| 87 |
+
ys = list(range(0, max(H - t, 0) + 1, step)) or [0]
|
| 88 |
+
xs = list(range(0, max(W - t, 0) + 1, step)) or [0]
|
| 89 |
+
if ys[-1] + t < H:
|
| 90 |
+
ys.append(H - t)
|
| 91 |
+
if xs[-1] + t < W:
|
| 92 |
+
xs.append(W - t)
|
| 93 |
+
for y0 in ys:
|
| 94 |
+
for x0 in xs:
|
| 95 |
+
y0c, x0c = max(0, y0), max(0, x0)
|
| 96 |
+
tile = x[:, :, y0c:y0c + t, x0c:x0c + t]
|
| 97 |
+
th, tw = tile.shape[-2:]
|
| 98 |
+
tp, _ = pad_to_multiple(tile, 32)
|
| 99 |
+
p = self._probs_tensor(tp.to(self.device))[0, :, :th, :tw].cpu().numpy()
|
| 100 |
+
w = w2[:, :th, :tw]
|
| 101 |
+
acc[:, y0c:y0c + th, x0c:x0c + tw] += p * w
|
| 102 |
+
wsum[:, y0c:y0c + th, x0c:x0c + tw] += w
|
| 103 |
+
return acc / np.maximum(wsum, 1e-6)
|
| 104 |
+
|
| 105 |
+
def predict(self, img: np.ndarray, cfg: PostConfig | None = None) -> tuple[Result, dict[str, np.ndarray]]:
|
| 106 |
+
probs = self.probs(img)
|
| 107 |
+
return analyze(probs, cfg), probs
|
cellquant/labels.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Label vocabulary shared by training, post-processing and the app."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
# The six CVAT labels exactly as they appear in the annotation exports.
|
| 5 |
+
CELL_LABELS = (
|
| 6 |
+
"Cell, Antibody +",
|
| 7 |
+
"Cell, Antibody -",
|
| 8 |
+
"Cell, Nostril, Antibody +",
|
| 9 |
+
"Cell, Nostril, Antibody -",
|
| 10 |
+
)
|
| 11 |
+
NOSTRIL_LABELS = (
|
| 12 |
+
"Nostril in Nucleus",
|
| 13 |
+
"Nostril External of Nucleus",
|
| 14 |
+
)
|
| 15 |
+
ALL_LABELS = CELL_LABELS + NOSTRIL_LABELS
|
| 16 |
+
|
| 17 |
+
# Display colours (RGB) used for overlays, one per label.
|
| 18 |
+
LABEL_COLORS = {
|
| 19 |
+
"Cell, Antibody +": (0, 255, 0),
|
| 20 |
+
"Cell, Antibody -": (255, 0, 0),
|
| 21 |
+
"Cell, Nostril, Antibody +": (0, 255, 255),
|
| 22 |
+
"Cell, Nostril, Antibody -": (255, 0, 255),
|
| 23 |
+
"Nostril in Nucleus": (255, 255, 0),
|
| 24 |
+
"Nostril External of Nucleus": (255, 128, 0),
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def is_cell_label(label: str) -> bool:
|
| 29 |
+
return label.startswith("Cell")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def is_nostril_label(label: str) -> bool:
|
| 33 |
+
return label.startswith("Nostril")
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def cell_attributes(label: str) -> tuple[bool, bool]:
|
| 37 |
+
"""Return (antibody_positive, has_nostril) for a cell label."""
|
| 38 |
+
if not is_cell_label(label):
|
| 39 |
+
raise ValueError(f"not a cell label: {label!r}")
|
| 40 |
+
antibody_positive = label.rstrip().endswith("+")
|
| 41 |
+
has_nostril = "Nostril" in label
|
| 42 |
+
return antibody_positive, has_nostril
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def cell_label(antibody_positive: bool, has_nostril: bool) -> str:
|
| 46 |
+
"""Compose the CVAT cell label from its two attributes."""
|
| 47 |
+
sign = "+" if antibody_positive else "-"
|
| 48 |
+
return f"Cell, Nostril, Antibody {sign}" if has_nostril else f"Cell, Antibody {sign}"
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def nostril_label(in_nucleus: bool) -> str:
|
| 52 |
+
return "Nostril in Nucleus" if in_nucleus else "Nostril External of Nucleus"
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def nostril_in_nucleus(label: str) -> bool:
|
| 56 |
+
if not is_nostril_label(label):
|
| 57 |
+
raise ValueError(f"not a nostril label: {label!r}")
|
| 58 |
+
return "in Nucleus" in label
|
cellquant/losses.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Loss for the multi-head output (see model.py for the channel layout)."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
|
| 7 |
+
from .model import ANTIBODY_CH, CELLNOS_CH, CELL_SLICE, NOS_SLICE
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def soft_dice(probs: torch.Tensor, target_onehot: torch.Tensor, eps: float = 1.0) -> torch.Tensor:
|
| 11 |
+
"""Mean soft Dice loss over the given channels (B, C, H, W)."""
|
| 12 |
+
dims = (0, 2, 3)
|
| 13 |
+
inter = (probs * target_onehot).sum(dims)
|
| 14 |
+
denom = probs.sum(dims) + target_onehot.sum(dims)
|
| 15 |
+
return (1 - (2 * inter + eps) / (denom + eps)).mean()
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def masked_bce(logit: torch.Tensor, target: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 19 |
+
l = F.binary_cross_entropy_with_logits(logit, target, reduction="none")
|
| 20 |
+
return (l * mask).sum() / mask.sum().clamp_min(1.0)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def multitask_loss(out: torch.Tensor, t: dict[str, torch.Tensor]) -> tuple[torch.Tensor, dict[str, float]]:
|
| 24 |
+
dev = out.device
|
| 25 |
+
cell_logits = out[:, CELL_SLICE]
|
| 26 |
+
nos_logits = out[:, NOS_SLICE]
|
| 27 |
+
cell3 = t["cell3"].to(dev)
|
| 28 |
+
nos3 = t["nos3"].to(dev)
|
| 29 |
+
w_cell = torch.tensor([1.0, 1.0, 2.0], device=dev)
|
| 30 |
+
w_nos = torch.tensor([1.0, 4.0, 4.0], device=dev)
|
| 31 |
+
ce_cell = F.cross_entropy(cell_logits, cell3, weight=w_cell)
|
| 32 |
+
ce_nos = F.cross_entropy(nos_logits, nos3, weight=w_nos)
|
| 33 |
+
p_cell = cell_logits.softmax(1)
|
| 34 |
+
p_nos = nos_logits.softmax(1)
|
| 35 |
+
oh_cell = F.one_hot(cell3, 3).permute(0, 3, 1, 2).float()
|
| 36 |
+
oh_nos = F.one_hot(nos3, 3).permute(0, 3, 1, 2).float()
|
| 37 |
+
dice_cell = soft_dice(p_cell[:, 1:], oh_cell[:, 1:])
|
| 38 |
+
dice_nos = soft_dice(p_nos[:, 1:], oh_nos[:, 1:])
|
| 39 |
+
mask = t["cellmask"].to(dev)
|
| 40 |
+
bce_ab = masked_bce(out[:, ANTIBODY_CH], t["antibody"].to(dev), mask)
|
| 41 |
+
bce_cn = masked_bce(out[:, CELLNOS_CH], t["cellnos"].to(dev), mask)
|
| 42 |
+
total = ce_cell + dice_cell + ce_nos + dice_nos + bce_ab + bce_cn
|
| 43 |
+
parts = {
|
| 44 |
+
"ce_cell": ce_cell.item(), "dice_cell": dice_cell.item(),
|
| 45 |
+
"ce_nos": ce_nos.item(), "dice_nos": dice_nos.item(),
|
| 46 |
+
"bce_ab": bce_ab.item(), "bce_cn": bce_cn.item(), "total": total.item(),
|
| 47 |
+
}
|
| 48 |
+
return total, parts
|
cellquant/metrics.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Instance-level evaluation against CVAT ground truth."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
from scipy import ndimage as ndi
|
| 6 |
+
from scipy.optimize import linear_sum_assignment
|
| 7 |
+
|
| 8 |
+
from .labels import ALL_LABELS, CELL_LABELS, NOSTRIL_LABELS, cell_attributes, nostril_in_nucleus
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def iou_matrix(gt: np.ndarray, pred: np.ndarray) -> np.ndarray:
|
| 12 |
+
ng, npred = int(gt.max()), int(pred.max())
|
| 13 |
+
if ng == 0 or npred == 0:
|
| 14 |
+
return np.zeros((ng, npred), dtype=np.float64)
|
| 15 |
+
pairs = gt.astype(np.int64) * (npred + 1) + pred.astype(np.int64)
|
| 16 |
+
counts = np.bincount(pairs.ravel(), minlength=(ng + 1) * (npred + 1)).reshape(ng + 1, npred + 1)
|
| 17 |
+
inter = counts[1:, 1:].astype(np.float64)
|
| 18 |
+
area_g = counts[1:, :].sum(1, keepdims=True)
|
| 19 |
+
area_p = counts[:, 1:].sum(0, keepdims=True)
|
| 20 |
+
union = area_g + area_p - inter
|
| 21 |
+
return inter / np.maximum(union, 1)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def match_by_iou(gt: np.ndarray, pred: np.ndarray, thr: float = 0.5) -> list[tuple[int, int, float]]:
|
| 25 |
+
"""Hungarian one-to-one matching; returns (gt_id, pred_id, iou) pairs with iou >= thr."""
|
| 26 |
+
iou = iou_matrix(gt, pred)
|
| 27 |
+
if iou.size == 0:
|
| 28 |
+
return []
|
| 29 |
+
valid = iou >= thr
|
| 30 |
+
if not valid.any():
|
| 31 |
+
return []
|
| 32 |
+
# maximise the number of valid matches first, IoU only as a tie-break
|
| 33 |
+
r, c = linear_sum_assignment(-(valid.astype(np.float64) + iou * 1e-3))
|
| 34 |
+
return [(int(i) + 1, int(j) + 1, float(iou[i, j])) for i, j in zip(r, c) if valid[i, j]]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def centroids(inst: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
| 38 |
+
n = int(inst.max())
|
| 39 |
+
if n == 0:
|
| 40 |
+
return np.zeros((0, 2)), np.zeros(0)
|
| 41 |
+
idx = np.arange(1, n + 1)
|
| 42 |
+
ones = np.ones_like(inst, dtype=np.float32)
|
| 43 |
+
c = np.array(ndi.center_of_mass(ones, inst, idx), dtype=np.float64)
|
| 44 |
+
a = np.array(ndi.sum_labels(ones, inst, idx), dtype=np.float64)
|
| 45 |
+
return c, a
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def match_by_centroid(gt: np.ndarray, pred: np.ndarray, base_thr: float = 6.0) -> list[tuple[int, int, float]]:
|
| 49 |
+
"""Hungarian matching on centroid distance; a pair matches when the distance
|
| 50 |
+
is below max(base_thr, equivalent radius of the GT object)."""
|
| 51 |
+
cg, ag = centroids(gt)
|
| 52 |
+
cp, _ = centroids(pred)
|
| 53 |
+
if len(cg) == 0 or len(cp) == 0:
|
| 54 |
+
return []
|
| 55 |
+
d = np.sqrt(((cg[:, None, :] - cp[None, :, :]) ** 2).sum(-1))
|
| 56 |
+
thr = np.maximum(base_thr, np.sqrt(ag / np.pi))[:, None]
|
| 57 |
+
cost = np.where(d <= thr, d, 1e6)
|
| 58 |
+
r, c = linear_sum_assignment(cost)
|
| 59 |
+
return [(int(i) + 1, int(j) + 1, float(d[i, j])) for i, j in zip(r, c) if d[i, j] <= thr[i, 0]]
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def prf(tp: int, fp: int, fn: int) -> dict[str, float]:
|
| 63 |
+
p = tp / (tp + fp) if tp + fp else 0.0
|
| 64 |
+
r = tp / (tp + fn) if tp + fn else 0.0
|
| 65 |
+
f = 2 * p * r / (p + r) if p + r else 0.0
|
| 66 |
+
return {"tp": tp, "fp": fp, "fn": fn, "precision": p, "recall": r, "f1": f}
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _class_scores(gt_labels: list[str], pred_labels: list[str], matches, classes) -> dict:
|
| 70 |
+
"""Per-class P/R/F1 where a TP needs a matched instance with the same label."""
|
| 71 |
+
gt_matched = {g: p for g, p, _ in matches}
|
| 72 |
+
pred_matched = {p: g for g, p, _ in matches}
|
| 73 |
+
out = {}
|
| 74 |
+
for c in classes:
|
| 75 |
+
tp = sum(1 for g, p, _ in matches if gt_labels[g] == c and pred_labels[p] == c)
|
| 76 |
+
n_gt = sum(1 for l in gt_labels[1:] if l == c)
|
| 77 |
+
n_pred = sum(1 for l in pred_labels[1:] if l == c)
|
| 78 |
+
out[c] = prf(tp, n_pred - tp, n_gt - tp)
|
| 79 |
+
out[c]["gt_count"] = n_gt
|
| 80 |
+
out[c]["pred_count"] = n_pred
|
| 81 |
+
return out
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def match_by_centroid_in_mask(gt_inst: np.ndarray, pred_inst: np.ndarray) -> list[tuple[int, int, float]]:
|
| 85 |
+
"""One-to-one matching where a predicted object matches the GT object that
|
| 86 |
+
contains its centroid (ties broken by IoU). Mirrors how a person counts."""
|
| 87 |
+
cp, _ = centroids(pred_inst)
|
| 88 |
+
if len(cp) == 0 or gt_inst.max() == 0:
|
| 89 |
+
return []
|
| 90 |
+
iou = iou_matrix(gt_inst, pred_inst)
|
| 91 |
+
H, W = gt_inst.shape
|
| 92 |
+
cost = np.full(iou.shape, 1e6)
|
| 93 |
+
for j, (cy, cx) in enumerate(cp):
|
| 94 |
+
g = int(gt_inst[min(H - 1, max(0, int(round(cy)))), min(W - 1, max(0, int(round(cx))))])
|
| 95 |
+
if g > 0:
|
| 96 |
+
cost[g - 1, j] = 1.0 - iou[g - 1, j]
|
| 97 |
+
r, c = linear_sum_assignment(cost)
|
| 98 |
+
return [(int(i) + 1, int(j) + 1, float(iou[i, j])) for i, j in zip(r, c) if cost[i, j] < 1e6]
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def evaluate_cells(gt_inst: np.ndarray, gt_labels: list[str], pred_inst: np.ndarray, pred_labels: list[str],
|
| 102 |
+
iou_thr: float = 0.5) -> dict:
|
| 103 |
+
matches = match_by_iou(gt_inst, pred_inst, iou_thr)
|
| 104 |
+
n_gt, n_pred = int(gt_inst.max()), int(pred_inst.max())
|
| 105 |
+
det = prf(len(matches), n_pred - len(matches), n_gt - len(matches))
|
| 106 |
+
det["mean_iou_matched"] = float(np.mean([m[2] for m in matches])) if matches else 0.0
|
| 107 |
+
# looser, count-oriented matchings
|
| 108 |
+
m30 = match_by_iou(gt_inst, pred_inst, 0.3)
|
| 109 |
+
det["iou30"] = prf(len(m30), n_pred - len(m30), n_gt - len(m30))
|
| 110 |
+
mc = match_by_centroid_in_mask(gt_inst, pred_inst)
|
| 111 |
+
det["centroid"] = prf(len(mc), n_pred - len(mc), n_gt - len(mc))
|
| 112 |
+
# attribute accuracy on matched pairs
|
| 113 |
+
ab_ok = nos_ok = both_ok = 0
|
| 114 |
+
conf: dict[str, dict[str, int]] = {c: {d: 0 for d in CELL_LABELS} for c in CELL_LABELS}
|
| 115 |
+
for g, p, _ in matches:
|
| 116 |
+
ga, gn = cell_attributes(gt_labels[g])
|
| 117 |
+
pa, pn = cell_attributes(pred_labels[p])
|
| 118 |
+
ab_ok += ga == pa
|
| 119 |
+
nos_ok += gn == pn
|
| 120 |
+
both_ok += (ga == pa) and (gn == pn)
|
| 121 |
+
conf[gt_labels[g]][pred_labels[p]] += 1
|
| 122 |
+
nm = max(len(matches), 1)
|
| 123 |
+
return {
|
| 124 |
+
"detection": det,
|
| 125 |
+
"attribute_accuracy": {"antibody": ab_ok / nm, "nostril": nos_ok / nm, "both": both_ok / nm, "n_matched": len(matches)},
|
| 126 |
+
"per_class": _class_scores(gt_labels, pred_labels, matches, CELL_LABELS),
|
| 127 |
+
"confusion": conf,
|
| 128 |
+
"n_gt": n_gt,
|
| 129 |
+
"n_pred": n_pred,
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def evaluate_nostrils(gt_inst: np.ndarray, gt_labels: list[str], pred_inst: np.ndarray, pred_labels: list[str],
|
| 134 |
+
dist_thr: float = 6.0) -> dict:
|
| 135 |
+
matches = match_by_centroid(gt_inst, pred_inst, dist_thr)
|
| 136 |
+
n_gt, n_pred = int(gt_inst.max()), int(pred_inst.max())
|
| 137 |
+
det = prf(len(matches), n_pred - len(matches), n_gt - len(matches))
|
| 138 |
+
det["mean_dist_matched"] = float(np.mean([m[2] for m in matches])) if matches else 0.0
|
| 139 |
+
type_ok = sum(1 for g, p, _ in matches if nostril_in_nucleus(gt_labels[g]) == nostril_in_nucleus(pred_labels[p]))
|
| 140 |
+
return {
|
| 141 |
+
"detection": det,
|
| 142 |
+
"type_accuracy": type_ok / max(len(matches), 1),
|
| 143 |
+
"per_class": _class_scores(gt_labels, pred_labels, matches, NOSTRIL_LABELS),
|
| 144 |
+
"n_gt": n_gt,
|
| 145 |
+
"n_pred": n_pred,
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def count_errors(gt_counts: dict[str, int], pred_counts: dict[str, int]) -> dict[str, dict]:
|
| 150 |
+
out = {}
|
| 151 |
+
for lab in ALL_LABELS:
|
| 152 |
+
g, p = int(gt_counts.get(lab, 0)), int(pred_counts.get(lab, 0))
|
| 153 |
+
out[lab] = {"gt": g, "pred": p, "abs_err": abs(p - g), "rel_err": (abs(p - g) / g) if g else (0.0 if p == 0 else float("inf"))}
|
| 154 |
+
return out
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def micro_average(per_image: list[dict], classes) -> dict:
|
| 158 |
+
"""Sum TP/FP/FN over images and recompute P/R/F1 per class."""
|
| 159 |
+
out = {}
|
| 160 |
+
for c in classes:
|
| 161 |
+
tp = sum(r[c]["tp"] for r in per_image)
|
| 162 |
+
fp = sum(r[c]["fp"] for r in per_image)
|
| 163 |
+
fn = sum(r[c]["fn"] for r in per_image)
|
| 164 |
+
out[c] = prf(tp, fp, fn)
|
| 165 |
+
out[c]["gt_count"] = sum(r[c]["gt_count"] for r in per_image)
|
| 166 |
+
out[c]["pred_count"] = sum(r[c]["pred_count"] for r in per_image)
|
| 167 |
+
return out
|
cellquant/model.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Multi-head UNet for nucleus / antibody / puncta segmentation.
|
| 2 |
+
|
| 3 |
+
Output channels (all at full input resolution):
|
| 4 |
+
0..2 cell classes : background / nucleus interior / nucleus border (softmax)
|
| 5 |
+
3 antibody logit : P(antibody positive) (meaningful on nucleus pixels)
|
| 6 |
+
4 nostril-attr logit: P(nucleus contains punctum) (meaningful on nucleus pixels)
|
| 7 |
+
5..7 punctum classes : background / in nucleus / external (softmax)
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
import torch.nn as nn
|
| 13 |
+
import torch.nn.functional as F
|
| 14 |
+
|
| 15 |
+
N_OUT = 8
|
| 16 |
+
CELL_SLICE = slice(0, 3)
|
| 17 |
+
ANTIBODY_CH = 3
|
| 18 |
+
CELLNOS_CH = 4
|
| 19 |
+
NOS_SLICE = slice(5, 8)
|
| 20 |
+
|
| 21 |
+
# ImageNet statistics; inputs are RGB in [0, 1].
|
| 22 |
+
MEAN = (0.485, 0.456, 0.406)
|
| 23 |
+
STD = (0.229, 0.224, 0.225)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _block(cin: int, cout: int) -> nn.Sequential:
|
| 27 |
+
return nn.Sequential(
|
| 28 |
+
nn.Conv2d(cin, cout, 3, padding=1, bias=False), nn.BatchNorm2d(cout), nn.ReLU(inplace=True),
|
| 29 |
+
nn.Conv2d(cout, cout, 3, padding=1, bias=False), nn.BatchNorm2d(cout), nn.ReLU(inplace=True),
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class CellUNet(nn.Module):
|
| 34 |
+
"""UNet with a timm ResNet encoder (features at strides 2..32) and a
|
| 35 |
+
full-resolution refinement stage so that 5-pixel puncta are resolved."""
|
| 36 |
+
|
| 37 |
+
def __init__(self, encoder: str = "resnet34", pretrained: bool = False, n_out: int = N_OUT):
|
| 38 |
+
super().__init__()
|
| 39 |
+
import timm
|
| 40 |
+
self.encoder_name = encoder
|
| 41 |
+
self.encoder = timm.create_model(encoder, pretrained=pretrained, features_only=True)
|
| 42 |
+
chs = list(self.encoder.feature_info.channels()) # e.g. [64, 64, 128, 256, 512]
|
| 43 |
+
self.reductions = list(self.encoder.feature_info.reduction())
|
| 44 |
+
dec = []
|
| 45 |
+
prev = chs[-1]
|
| 46 |
+
for c in reversed(chs[:-1]):
|
| 47 |
+
dec.append(_block(prev + c, c))
|
| 48 |
+
prev = c
|
| 49 |
+
self.decoder = nn.ModuleList(dec)
|
| 50 |
+
self.stem = nn.Sequential(nn.Conv2d(3, 16, 3, padding=1, bias=False), nn.BatchNorm2d(16), nn.ReLU(inplace=True))
|
| 51 |
+
self.full = _block(prev + 16, 32)
|
| 52 |
+
self.head = nn.Conv2d(32, n_out, 1)
|
| 53 |
+
self.register_buffer("mean", torch.tensor(MEAN).view(1, 3, 1, 1))
|
| 54 |
+
self.register_buffer("std", torch.tensor(STD).view(1, 3, 1, 1))
|
| 55 |
+
|
| 56 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 57 |
+
"""x: (B, 3, H, W) float in [0, 1]; H and W must be multiples of 32."""
|
| 58 |
+
H, W = x.shape[-2:]
|
| 59 |
+
xn = (x - self.mean) / self.std
|
| 60 |
+
feats = self.encoder(xn)
|
| 61 |
+
y = feats[-1]
|
| 62 |
+
for blk, f in zip(self.decoder, reversed(feats[:-1])):
|
| 63 |
+
y = F.interpolate(y, size=f.shape[-2:], mode="bilinear", align_corners=False)
|
| 64 |
+
y = blk(torch.cat([y, f], dim=1))
|
| 65 |
+
y = F.interpolate(y, size=(H, W), mode="bilinear", align_corners=False)
|
| 66 |
+
y = self.full(torch.cat([y, self.stem(x)], dim=1))
|
| 67 |
+
return self.head(y)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def pad_to_multiple(x: torch.Tensor, m: int = 32) -> tuple[torch.Tensor, tuple[int, int]]:
|
| 71 |
+
"""Reflect-pad the bottom/right of (B, C, H, W) to multiples of ``m``."""
|
| 72 |
+
H, W = x.shape[-2:]
|
| 73 |
+
ph = (m - H % m) % m
|
| 74 |
+
pw = (m - W % m) % m
|
| 75 |
+
if ph or pw:
|
| 76 |
+
# reflect padding requires pad < dim; fall back to replicate for tiny inputs
|
| 77 |
+
mode = "reflect" if (ph < H and pw < W) else "replicate"
|
| 78 |
+
x = F.pad(x, (0, pw, 0, ph), mode=mode)
|
| 79 |
+
return x, (ph, pw)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def save_checkpoint(model: CellUNet, path: str, extra: dict | None = None) -> None:
|
| 83 |
+
payload = {
|
| 84 |
+
"state_dict": model.state_dict(),
|
| 85 |
+
"encoder": model.encoder_name,
|
| 86 |
+
"n_out": model.head.out_channels,
|
| 87 |
+
}
|
| 88 |
+
if extra:
|
| 89 |
+
payload.update(extra)
|
| 90 |
+
torch.save(payload, path)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def load_checkpoint(path: str, map_location: str = "cpu") -> tuple[CellUNet, dict]:
|
| 94 |
+
ck = torch.load(path, map_location=map_location, weights_only=False)
|
| 95 |
+
model = CellUNet(ck["encoder"], pretrained=False, n_out=ck.get("n_out", N_OUT))
|
| 96 |
+
model.load_state_dict(ck["state_dict"])
|
| 97 |
+
model.eval()
|
| 98 |
+
meta = {k: v for k, v in ck.items() if k != "state_dict"}
|
| 99 |
+
return model, meta
|
cellquant/postprocess.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Turn network probabilities into nucleus / punctum instances, labels and counts."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from dataclasses import asdict, dataclass, field
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
from scipy import ndimage as ndi
|
| 8 |
+
from skimage.segmentation import find_boundaries, watershed
|
| 9 |
+
|
| 10 |
+
from .labels import ALL_LABELS, LABEL_COLORS, cell_label, nostril_label
|
| 11 |
+
from .model import ANTIBODY_CH, CELLNOS_CH, CELL_SLICE, NOS_SLICE
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class PostConfig:
|
| 16 |
+
cell_fg_thr: float = 0.5 # P(interior) + P(border) > thr -> nucleus foreground
|
| 17 |
+
marker_thr: float = 0.5 # P(interior) > thr -> watershed markers
|
| 18 |
+
min_marker_area: int = 12 # px
|
| 19 |
+
min_cell_area: int = 50 # px (smallest traced nucleus in the training set is 35 px)
|
| 20 |
+
nos_fg_thr: float = 0.5 # 1 - P(background) > thr -> punctum foreground
|
| 21 |
+
min_nos_area: int = 3 # px
|
| 22 |
+
antibody_thr: float = 0.5
|
| 23 |
+
cellnos_thr: float = 0.5
|
| 24 |
+
nostril_rule: str = "head" # "head": punctum type from the network; "geometric": inside a nucleus => in nucleus
|
| 25 |
+
cellnos_rule: str = "contains" # "head" | "contains" | "either": how a nucleus gets the "Nostril" attribute
|
| 26 |
+
# ("contains" mirrors the annotation protocol and scored best in cross-validation)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def logits_to_probs(logits: np.ndarray) -> dict[str, np.ndarray]:
|
| 30 |
+
"""logits: (8, H, W) float -> dict of probabilities."""
|
| 31 |
+
def softmax(z):
|
| 32 |
+
z = z - z.max(axis=0, keepdims=True)
|
| 33 |
+
e = np.exp(z)
|
| 34 |
+
return e / e.sum(axis=0, keepdims=True)
|
| 35 |
+
def sigmoid(z):
|
| 36 |
+
return 1.0 / (1.0 + np.exp(-z))
|
| 37 |
+
return {
|
| 38 |
+
"cell": softmax(logits[CELL_SLICE].astype(np.float32)),
|
| 39 |
+
"antibody": sigmoid(logits[ANTIBODY_CH].astype(np.float32)),
|
| 40 |
+
"cellnos": sigmoid(logits[CELLNOS_CH].astype(np.float32)),
|
| 41 |
+
"nos": softmax(logits[NOS_SLICE].astype(np.float32)),
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def segment_cells(p_cell: np.ndarray, cfg: PostConfig) -> np.ndarray:
|
| 46 |
+
"""p_cell: (3, H, W) softmax (bg, interior, border) -> int32 instance map."""
|
| 47 |
+
p_int, p_bor = p_cell[1], p_cell[2]
|
| 48 |
+
fg = (p_int + p_bor) > cfg.cell_fg_thr
|
| 49 |
+
markers, n = ndi.label(p_int > cfg.marker_thr)
|
| 50 |
+
if n:
|
| 51 |
+
sizes = np.bincount(markers.ravel())
|
| 52 |
+
small = sizes < cfg.min_marker_area
|
| 53 |
+
small[0] = False
|
| 54 |
+
markers[small[markers]] = 0
|
| 55 |
+
markers, n = ndi.label(markers > 0)
|
| 56 |
+
inst = watershed(-p_int, markers, mask=fg) if n else np.zeros(fg.shape, np.int32)
|
| 57 |
+
inst = inst.astype(np.int32)
|
| 58 |
+
# foreground components that received no marker become instances of their own
|
| 59 |
+
orphan, m = ndi.label(fg & (inst == 0))
|
| 60 |
+
if m:
|
| 61 |
+
sizes = np.bincount(orphan.ravel())
|
| 62 |
+
keep = sizes >= cfg.min_cell_area
|
| 63 |
+
keep[0] = False
|
| 64 |
+
orphan[~keep[orphan]] = 0
|
| 65 |
+
orphan_ids = np.unique(orphan)
|
| 66 |
+
orphan_ids = orphan_ids[orphan_ids > 0]
|
| 67 |
+
base = inst.max()
|
| 68 |
+
for j, oid in enumerate(orphan_ids, start=1):
|
| 69 |
+
inst[orphan == oid] = base + j
|
| 70 |
+
inst = _fill_and_filter(inst, cfg.min_cell_area)
|
| 71 |
+
return inst
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _fill_and_filter(inst: np.ndarray, min_area: int) -> np.ndarray:
|
| 75 |
+
"""Fill holes inside every instance, drop instances below ``min_area`` and
|
| 76 |
+
keep only the largest connected piece of each instance; relabel 1..n."""
|
| 77 |
+
out = np.zeros_like(inst)
|
| 78 |
+
nxt = 1
|
| 79 |
+
for i, sl in enumerate(ndi.find_objects(inst), start=1):
|
| 80 |
+
if sl is None:
|
| 81 |
+
continue
|
| 82 |
+
m = inst[sl] == i
|
| 83 |
+
lab, k = ndi.label(m)
|
| 84 |
+
if k > 1: # keep the largest piece
|
| 85 |
+
sizes = np.bincount(lab.ravel())
|
| 86 |
+
sizes[0] = 0
|
| 87 |
+
m = lab == sizes.argmax()
|
| 88 |
+
# fill holes, but never absorb pixels that belong to another instance
|
| 89 |
+
m = m | (ndi.binary_fill_holes(m) & (inst[sl] == 0))
|
| 90 |
+
region = out[sl]
|
| 91 |
+
w = m & (region == 0)
|
| 92 |
+
if w.sum() < min_area:
|
| 93 |
+
continue
|
| 94 |
+
region[w] = nxt
|
| 95 |
+
nxt += 1
|
| 96 |
+
return out
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def segment_nostrils(p_nos: np.ndarray, cfg: PostConfig) -> np.ndarray:
|
| 100 |
+
fg = (1.0 - p_nos[0]) > cfg.nos_fg_thr
|
| 101 |
+
inst, n = ndi.label(fg) # 4-connectivity keeps neighbouring puncta apart
|
| 102 |
+
if n == 0:
|
| 103 |
+
return inst.astype(np.int32)
|
| 104 |
+
sizes = np.bincount(inst.ravel())
|
| 105 |
+
keep = sizes >= cfg.min_nos_area
|
| 106 |
+
keep[0] = False
|
| 107 |
+
inst[~keep[inst]] = 0
|
| 108 |
+
inst, _ = ndi.label(inst > 0)
|
| 109 |
+
return inst.astype(np.int32)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
@dataclass
|
| 113 |
+
class Result:
|
| 114 |
+
cell_inst: np.ndarray
|
| 115 |
+
nostril_inst: np.ndarray
|
| 116 |
+
cells: list[dict] = field(default_factory=list)
|
| 117 |
+
nostrils: list[dict] = field(default_factory=list)
|
| 118 |
+
counts: dict[str, int] = field(default_factory=dict)
|
| 119 |
+
config: dict = field(default_factory=dict)
|
| 120 |
+
|
| 121 |
+
@property
|
| 122 |
+
def cell_labels(self) -> list[str]:
|
| 123 |
+
return [""] + [c["label"] for c in self.cells]
|
| 124 |
+
|
| 125 |
+
@property
|
| 126 |
+
def nostril_labels(self) -> list[str]:
|
| 127 |
+
return [""] + [n["label"] for n in self.nostrils]
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def analyze(probs: dict[str, np.ndarray], cfg: PostConfig | None = None) -> Result:
|
| 131 |
+
cfg = cfg or PostConfig()
|
| 132 |
+
cell_inst = segment_cells(probs["cell"], cfg)
|
| 133 |
+
nos_inst = segment_nostrils(probs["nos"], cfg)
|
| 134 |
+
n_cells = int(cell_inst.max())
|
| 135 |
+
n_nos = int(nos_inst.max())
|
| 136 |
+
H, W = cell_inst.shape
|
| 137 |
+
idx = np.arange(1, n_cells + 1)
|
| 138 |
+
# per-nucleus statistics
|
| 139 |
+
if n_cells:
|
| 140 |
+
areas = ndi.sum_labels(np.ones_like(cell_inst, dtype=np.float32), cell_inst, idx)
|
| 141 |
+
ab = ndi.mean(probs["antibody"], cell_inst, idx)
|
| 142 |
+
cn = ndi.mean(probs["cellnos"], cell_inst, idx)
|
| 143 |
+
cents = ndi.center_of_mass(np.ones_like(cell_inst, dtype=np.float32), cell_inst, idx)
|
| 144 |
+
slices = ndi.find_objects(cell_inst)
|
| 145 |
+
# puncta
|
| 146 |
+
nostrils: list[dict] = []
|
| 147 |
+
contains_in: np.ndarray = np.zeros(n_cells + 1, dtype=np.int32)
|
| 148 |
+
contains_any: np.ndarray = np.zeros(n_cells + 1, dtype=np.int32)
|
| 149 |
+
if n_nos:
|
| 150 |
+
nidx = np.arange(1, n_nos + 1)
|
| 151 |
+
nareas = ndi.sum_labels(np.ones_like(nos_inst, dtype=np.float32), nos_inst, nidx)
|
| 152 |
+
p_in = ndi.sum_labels(probs["nos"][1], nos_inst, nidx)
|
| 153 |
+
p_ext = ndi.sum_labels(probs["nos"][2], nos_inst, nidx)
|
| 154 |
+
ncents = ndi.center_of_mass(np.ones_like(nos_inst, dtype=np.float32), nos_inst, nidx)
|
| 155 |
+
for j in range(n_nos):
|
| 156 |
+
cy, cx = ncents[j]
|
| 157 |
+
r, c = min(H - 1, max(0, int(round(cy)))), min(W - 1, max(0, int(round(cx))))
|
| 158 |
+
cell_id = int(cell_inst[r, c])
|
| 159 |
+
if cell_id == 0: # a punctum touching a nucleus counts as inside it (majority of its pixels)
|
| 160 |
+
ids = cell_inst[nos_inst == j + 1]
|
| 161 |
+
ids = ids[ids > 0]
|
| 162 |
+
if ids.size and ids.size * 2 >= nareas[j]:
|
| 163 |
+
cell_id = int(np.bincount(ids).argmax())
|
| 164 |
+
head_in = bool(p_in[j] >= p_ext[j])
|
| 165 |
+
in_nuc = head_in if cfg.nostril_rule == "head" else cell_id > 0
|
| 166 |
+
nostrils.append({
|
| 167 |
+
"id": j + 1,
|
| 168 |
+
"label": nostril_label(in_nuc),
|
| 169 |
+
"area_px": float(nareas[j]),
|
| 170 |
+
"centroid_xy": [float(cx), float(cy)],
|
| 171 |
+
"p_in_nucleus": float(p_in[j] / (p_in[j] + p_ext[j] + 1e-9)),
|
| 172 |
+
"cell_id": cell_id,
|
| 173 |
+
})
|
| 174 |
+
if cell_id:
|
| 175 |
+
contains_any[cell_id] += 1
|
| 176 |
+
if in_nuc:
|
| 177 |
+
contains_in[cell_id] += 1
|
| 178 |
+
cells: list[dict] = []
|
| 179 |
+
for i in range(n_cells):
|
| 180 |
+
cid = i + 1
|
| 181 |
+
head_flag = bool(cn[i] > cfg.cellnos_thr)
|
| 182 |
+
cont_flag = bool(contains_in[cid] > 0)
|
| 183 |
+
if cfg.cellnos_rule == "head":
|
| 184 |
+
has_nos = head_flag
|
| 185 |
+
elif cfg.cellnos_rule == "contains":
|
| 186 |
+
has_nos = cont_flag
|
| 187 |
+
else:
|
| 188 |
+
has_nos = head_flag or cont_flag
|
| 189 |
+
sl = slices[i]
|
| 190 |
+
cells.append({
|
| 191 |
+
"id": cid,
|
| 192 |
+
"label": cell_label(bool(ab[i] > cfg.antibody_thr), has_nos),
|
| 193 |
+
"area_px": float(areas[i]),
|
| 194 |
+
"centroid_xy": [float(cents[i][1]), float(cents[i][0])],
|
| 195 |
+
"bbox_xyxy": [int(sl[1].start), int(sl[0].start), int(sl[1].stop), int(sl[0].stop)],
|
| 196 |
+
"p_antibody": float(ab[i]),
|
| 197 |
+
"p_nostril": float(cn[i]),
|
| 198 |
+
"n_puncta_inside": int(contains_in[cid]),
|
| 199 |
+
})
|
| 200 |
+
counts = {lab: 0 for lab in ALL_LABELS}
|
| 201 |
+
for c in cells:
|
| 202 |
+
counts[c["label"]] += 1
|
| 203 |
+
for n_ in nostrils:
|
| 204 |
+
counts[n_["label"]] += 1
|
| 205 |
+
return Result(cell_inst, nos_inst, cells, nostrils, counts, asdict(cfg))
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def render_overlay(img: np.ndarray, res: Result, thickness: int = 1) -> np.ndarray:
|
| 209 |
+
"""Draw coloured outlines (nuclei: inner boundary, puncta: outer ring) on a copy of ``img``."""
|
| 210 |
+
out = img.copy()
|
| 211 |
+
if res.cells:
|
| 212 |
+
lut = np.zeros((len(res.cells) + 1, 3), dtype=np.uint8)
|
| 213 |
+
for c in res.cells:
|
| 214 |
+
lut[c["id"]] = LABEL_COLORS[c["label"]]
|
| 215 |
+
b = find_boundaries(res.cell_inst, mode="inner")
|
| 216 |
+
if thickness > 1:
|
| 217 |
+
b = ndi.binary_dilation(b, iterations=thickness - 1) & (res.cell_inst > 0)
|
| 218 |
+
out[b] = lut[res.cell_inst[b]]
|
| 219 |
+
if res.nostrils:
|
| 220 |
+
lut = np.zeros((len(res.nostrils) + 1, 3), dtype=np.uint8)
|
| 221 |
+
for n_ in res.nostrils:
|
| 222 |
+
lut[n_["id"]] = LABEL_COLORS[n_["label"]]
|
| 223 |
+
dil = ndi.grey_dilation(res.nostril_inst, size=(3, 3))
|
| 224 |
+
ring = (dil > 0) & (res.nostril_inst == 0)
|
| 225 |
+
out[ring] = lut[dil[ring]]
|
| 226 |
+
return out
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def result_to_json(res: Result) -> dict:
|
| 230 |
+
return {"counts": res.counts, "cells": res.cells, "nostrils": res.nostrils, "config": res.config}
|
evaluate.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Leave-one-image-out cross-validation with repeated, perturbed tests per image.
|
| 3 |
+
|
| 4 |
+
python evaluate.py cv --data-dir DATA --runs-dir runs/cv --iters 3000
|
| 5 |
+
python evaluate.py final --data-dir DATA --weights weights/cellquant.pt
|
| 6 |
+
|
| 7 |
+
``cv`` trains one model per annotated image with that image held out (unless the
|
| 8 |
+
fold checkpoint already exists), then tests the held-out image many times:
|
| 9 |
+
|
| 10 |
+
* base run (no TTA), TTA x4 and TTA x8
|
| 11 |
+
* 8 dihedral variants (rotate/flip the image, predict, undo the transform)
|
| 12 |
+
* 6 photometric perturbations (per-channel gain, gamma, noise; fixed seeds)
|
| 13 |
+
* 3 identical repeats to prove determinism
|
| 14 |
+
* post-processing rule variants (how puncta get their type and how nuclei
|
| 15 |
+
get the "Nostril" attribute)
|
| 16 |
+
|
| 17 |
+
``final`` reports the fit of one model on its own training images (NOT a
|
| 18 |
+
generalisation estimate) and writes overlays for un-annotated images.
|
| 19 |
+
"""
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import argparse
|
| 23 |
+
import hashlib
|
| 24 |
+
import json
|
| 25 |
+
import os
|
| 26 |
+
import subprocess
|
| 27 |
+
import sys
|
| 28 |
+
import time
|
| 29 |
+
|
| 30 |
+
import numpy as np
|
| 31 |
+
from PIL import Image
|
| 32 |
+
|
| 33 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 34 |
+
from cellquant.annotations import discover, load_image, rasterize # noqa: E402
|
| 35 |
+
from cellquant.infer import Predictor # noqa: E402
|
| 36 |
+
from cellquant.labels import ALL_LABELS, CELL_LABELS, NOSTRIL_LABELS # noqa: E402
|
| 37 |
+
from cellquant.metrics import count_errors, evaluate_cells, evaluate_nostrils, micro_average # noqa: E402
|
| 38 |
+
from cellquant.postprocess import PostConfig, Result, analyze, render_overlay # noqa: E402
|
| 39 |
+
|
| 40 |
+
DIHEDRAL = [(k, f) for f in (False, True) for k in range(4)]
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def fwd_np(a: np.ndarray, k: int, flip: bool) -> np.ndarray:
|
| 44 |
+
if flip:
|
| 45 |
+
a = a[..., ::-1]
|
| 46 |
+
return np.rot90(a, k, axes=(-2, -1))
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def inv_np(a: np.ndarray, k: int, flip: bool) -> np.ndarray:
|
| 50 |
+
a = np.rot90(a, -k, axes=(-2, -1))
|
| 51 |
+
if flip:
|
| 52 |
+
a = a[..., ::-1]
|
| 53 |
+
return np.ascontiguousarray(a)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def photometric(img: np.ndarray, seed: int) -> np.ndarray:
|
| 57 |
+
rng = np.random.default_rng(seed)
|
| 58 |
+
x = img.astype(np.float32) / 255.0
|
| 59 |
+
gain = rng.uniform(0.7, 1.4, size=3).astype(np.float32)
|
| 60 |
+
gamma = float(rng.uniform(0.8, 1.25))
|
| 61 |
+
x = np.clip(x * gain, 0, 1) ** gamma
|
| 62 |
+
x = x + rng.normal(0, float(rng.uniform(0, 0.02)), size=x.shape).astype(np.float32)
|
| 63 |
+
return (np.clip(x, 0, 1) * 255).round().astype(np.uint8)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def fp_analysis(img: np.ndarray, R, res: Result) -> dict:
|
| 67 |
+
"""Characterise unmatched predicted nuclei: how many overlap an annotated nucleus
|
| 68 |
+
at all (outline/split disagreement) versus none (extra detection), and how
|
| 69 |
+
bright/large they are compared with matched ones."""
|
| 70 |
+
from scipy import ndimage as ndi
|
| 71 |
+
from cellquant.metrics import match_by_iou
|
| 72 |
+
m = match_by_iou(R.cell_inst, res.cell_inst, 0.5)
|
| 73 |
+
mp = {p for _, p, _ in m}
|
| 74 |
+
mg = {g for g, _, _ in m}
|
| 75 |
+
n_pred = int(res.cell_inst.max())
|
| 76 |
+
if n_pred == 0:
|
| 77 |
+
return {"n_fp": 0}
|
| 78 |
+
ids = np.arange(1, n_pred + 1)
|
| 79 |
+
blue = ndi.mean(img[..., 2].astype(np.float32), res.cell_inst, ids)
|
| 80 |
+
areas = np.bincount(res.cell_inst.ravel())[1:]
|
| 81 |
+
gt_cover = ndi.mean((R.cell_inst > 0).astype(np.float32), res.cell_inst, ids)
|
| 82 |
+
fp = np.array([i not in mp for i in ids])
|
| 83 |
+
matched = ~fp
|
| 84 |
+
fn_ids = [g for g in range(1, R.n_cells + 1) if g not in mg]
|
| 85 |
+
fn_cover = [float((res.cell_inst[R.cell_inst == g] > 0).mean()) for g in fn_ids]
|
| 86 |
+
return {
|
| 87 |
+
"n_fp": int(fp.sum()),
|
| 88 |
+
"n_fp_overlapping_gt": int((fp & (gt_cover > 0.3)).sum()),
|
| 89 |
+
"n_fp_no_gt_overlap": int((fp & (gt_cover <= 0.3)).sum()),
|
| 90 |
+
"fp_median_blue": float(np.median(blue[fp])) if fp.any() else None,
|
| 91 |
+
"matched_median_blue": float(np.median(blue[matched])) if matched.any() else None,
|
| 92 |
+
"fp_median_area": float(np.median(areas[fp])) if fp.any() else None,
|
| 93 |
+
"matched_median_area": float(np.median(areas[matched])) if matched.any() else None,
|
| 94 |
+
"n_fn": len(fn_ids),
|
| 95 |
+
"n_fn_covered_by_prediction": int(sum(c > 0.3 for c in fn_cover)),
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def matched_pairs(R, res: Result) -> list[dict]:
|
| 100 |
+
"""Per matched nucleus (IoU >= 0.5): GT label and the network's attribute probabilities,
|
| 101 |
+
so that attribute thresholds can be studied across all held-out images."""
|
| 102 |
+
from cellquant.metrics import match_by_iou
|
| 103 |
+
by_id = {c["id"]: c for c in res.cells}
|
| 104 |
+
out = []
|
| 105 |
+
for g, p, iou in match_by_iou(R.cell_inst, res.cell_inst, 0.5):
|
| 106 |
+
c = by_id[p]
|
| 107 |
+
out.append({"gt": R.cell_labels[g], "pred": c["label"], "p_antibody": c["p_antibody"],
|
| 108 |
+
"p_nostril": c["p_nostril"], "n_puncta_inside": c["n_puncta_inside"], "iou": iou})
|
| 109 |
+
return out
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def score(R, res: Result, gt_counts: dict, img: np.ndarray | None = None) -> dict:
|
| 113 |
+
ce = evaluate_cells(R.cell_inst, R.cell_labels, res.cell_inst, res.cell_labels)
|
| 114 |
+
ne = evaluate_nostrils(R.nostril_inst, R.nostril_labels, res.nostril_inst, res.nostril_labels)
|
| 115 |
+
return {
|
| 116 |
+
"cells": ce,
|
| 117 |
+
"nostrils": ne,
|
| 118 |
+
"counts": count_errors(gt_counts, res.counts),
|
| 119 |
+
"fp_analysis": fp_analysis(img, R, res) if img is not None else None,
|
| 120 |
+
"pairs": matched_pairs(R, res) if img is not None else None,
|
| 121 |
+
"summary": {
|
| 122 |
+
"cell_det_f1": ce["detection"]["f1"],
|
| 123 |
+
"cell_det_precision": ce["detection"]["precision"],
|
| 124 |
+
"cell_det_recall": ce["detection"]["recall"],
|
| 125 |
+
"cell_mean_iou": ce["detection"]["mean_iou_matched"],
|
| 126 |
+
"cell_det_f1_iou30": ce["detection"]["iou30"]["f1"],
|
| 127 |
+
"cell_det_f1_centroid": ce["detection"]["centroid"]["f1"],
|
| 128 |
+
"cell_attr_antibody": ce["attribute_accuracy"]["antibody"],
|
| 129 |
+
"cell_attr_nostril": ce["attribute_accuracy"]["nostril"],
|
| 130 |
+
"cell_attr_both": ce["attribute_accuracy"]["both"],
|
| 131 |
+
"nos_det_f1": ne["detection"]["f1"],
|
| 132 |
+
"nos_det_precision": ne["detection"]["precision"],
|
| 133 |
+
"nos_det_recall": ne["detection"]["recall"],
|
| 134 |
+
"nos_type_acc": ne["type_accuracy"],
|
| 135 |
+
**{f"f1[{c}]": ce["per_class"][c]["f1"] for c in CELL_LABELS},
|
| 136 |
+
**{f"f1[{c}]": ne["per_class"][c]["f1"] for c in NOSTRIL_LABELS},
|
| 137 |
+
},
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def inst_hash(res: Result) -> str:
|
| 142 |
+
h = hashlib.sha256()
|
| 143 |
+
h.update(res.cell_inst.tobytes())
|
| 144 |
+
h.update(res.nostril_inst.tobytes())
|
| 145 |
+
h.update(json.dumps(res.counts, sort_keys=True).encode())
|
| 146 |
+
return h.hexdigest()[:16]
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def test_image(weights: str, img: np.ndarray, R, gt_counts: dict, device: str, out_prefix: str | None = None) -> dict:
|
| 150 |
+
cfg = PostConfig()
|
| 151 |
+
out: dict = {"runs": {}}
|
| 152 |
+
p1 = Predictor(weights, device=device, tta=1)
|
| 153 |
+
# base + determinism
|
| 154 |
+
hashes = []
|
| 155 |
+
for i in range(3):
|
| 156 |
+
res, probs = p1.predict(img, cfg)
|
| 157 |
+
hashes.append(inst_hash(res))
|
| 158 |
+
base_res, base_probs = res, probs
|
| 159 |
+
out["deterministic"] = len(set(hashes)) == 1
|
| 160 |
+
out["hashes"] = hashes
|
| 161 |
+
out["runs"]["base"] = score(R, base_res, gt_counts, img)
|
| 162 |
+
out["base_counts"] = base_res.counts
|
| 163 |
+
if out_prefix:
|
| 164 |
+
Image.fromarray(render_overlay(img, base_res)).save(out_prefix + "_pred.png")
|
| 165 |
+
# TTA
|
| 166 |
+
for tta in (4, 8):
|
| 167 |
+
res, _ = Predictor(weights, device=device, tta=tta).predict(img, cfg)
|
| 168 |
+
out["runs"][f"tta{tta}"] = score(R, res, gt_counts)
|
| 169 |
+
# dihedral robustness
|
| 170 |
+
for k, f in DIHEDRAL:
|
| 171 |
+
timg = np.ascontiguousarray(fwd_np(np.moveaxis(img, -1, 0), k, f))
|
| 172 |
+
timg = np.moveaxis(timg, 0, -1)
|
| 173 |
+
probs = p1.probs(timg)
|
| 174 |
+
probs = {kk: inv_np(v, k, f) for kk, v in probs.items()}
|
| 175 |
+
res = analyze(probs, cfg)
|
| 176 |
+
out["runs"][f"dihedral_k{k}_f{int(f)}"] = score(R, res, gt_counts)
|
| 177 |
+
# photometric perturbations
|
| 178 |
+
for seed in range(6):
|
| 179 |
+
res, _ = p1.predict(photometric(img, 1000 + seed), cfg)
|
| 180 |
+
out["runs"][f"photometric_{seed}"] = score(R, res, gt_counts)
|
| 181 |
+
# post-processing rule variants (same probabilities as the base run)
|
| 182 |
+
for nr in ("head", "geometric"):
|
| 183 |
+
for cr in ("head", "contains", "either"):
|
| 184 |
+
res = analyze(base_probs, PostConfig(nostril_rule=nr, cellnos_rule=cr))
|
| 185 |
+
out["runs"][f"rule_nos={nr}_cell={cr}"] = score(R, res, gt_counts)
|
| 186 |
+
return out
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def aggregate(per_key: dict[str, dict]) -> dict:
|
| 190 |
+
keys = sorted(per_key)
|
| 191 |
+
agg: dict = {"images": keys}
|
| 192 |
+
# micro-averaged per-class scores on the base run
|
| 193 |
+
cells = [per_key[k]["runs"]["base"]["cells"]["per_class"] for k in keys]
|
| 194 |
+
nos = [per_key[k]["runs"]["base"]["nostrils"]["per_class"] for k in keys]
|
| 195 |
+
agg["micro_base"] = {**micro_average(cells, CELL_LABELS), **micro_average(nos, NOSTRIL_LABELS)}
|
| 196 |
+
for variant in ("tta4", "tta8"):
|
| 197 |
+
cells = [per_key[k]["runs"][variant]["cells"]["per_class"] for k in keys]
|
| 198 |
+
nos = [per_key[k]["runs"][variant]["nostrils"]["per_class"] for k in keys]
|
| 199 |
+
agg[f"micro_{variant}"] = {**micro_average(cells, CELL_LABELS), **micro_average(nos, NOSTRIL_LABELS)}
|
| 200 |
+
# detection totals
|
| 201 |
+
for variant in ("base", "tta4", "tta8"):
|
| 202 |
+
tp = sum(per_key[k]["runs"][variant]["cells"]["detection"]["tp"] for k in keys)
|
| 203 |
+
fp = sum(per_key[k]["runs"][variant]["cells"]["detection"]["fp"] for k in keys)
|
| 204 |
+
fn = sum(per_key[k]["runs"][variant]["cells"]["detection"]["fn"] for k in keys)
|
| 205 |
+
ntp = sum(per_key[k]["runs"][variant]["nostrils"]["detection"]["tp"] for k in keys)
|
| 206 |
+
nfp = sum(per_key[k]["runs"][variant]["nostrils"]["detection"]["fp"] for k in keys)
|
| 207 |
+
nfn = sum(per_key[k]["runs"][variant]["nostrils"]["detection"]["fn"] for k in keys)
|
| 208 |
+
from cellquant.metrics import prf
|
| 209 |
+
agg[f"detection_{variant}"] = {"cells": prf(tp, fp, fn), "nostrils": prf(ntp, nfp, nfn)}
|
| 210 |
+
for loose in ("iou30", "centroid"):
|
| 211 |
+
ltp = sum(per_key[k]["runs"][variant]["cells"]["detection"][loose]["tp"] for k in keys)
|
| 212 |
+
lfp = sum(per_key[k]["runs"][variant]["cells"]["detection"][loose]["fp"] for k in keys)
|
| 213 |
+
lfn = sum(per_key[k]["runs"][variant]["cells"]["detection"][loose]["fn"] for k in keys)
|
| 214 |
+
agg[f"detection_{variant}"][f"cells_{loose}"] = prf(ltp, lfp, lfn)
|
| 215 |
+
# per-image summary table + robustness (mean/std over dihedral & photometric runs)
|
| 216 |
+
summ = {}
|
| 217 |
+
for k in keys:
|
| 218 |
+
runs = per_key[k]["runs"]
|
| 219 |
+
rob = [v["summary"] for n, v in runs.items() if n.startswith(("dihedral", "photometric"))]
|
| 220 |
+
rob_stats = {}
|
| 221 |
+
for m in ("cell_det_f1", "cell_attr_both", "nos_det_f1", "nos_type_acc"):
|
| 222 |
+
vals = np.array([r[m] for r in rob])
|
| 223 |
+
rob_stats[m] = {"mean": float(vals.mean()), "std": float(vals.std()), "min": float(vals.min()), "max": float(vals.max()), "n": int(len(vals))}
|
| 224 |
+
summ[k] = {"base": runs["base"]["summary"], "tta4": runs["tta4"]["summary"], "tta8": runs["tta8"]["summary"],
|
| 225 |
+
"robustness": rob_stats, "deterministic": per_key[k]["deterministic"],
|
| 226 |
+
"counts": runs["base"]["counts"], "fp_analysis": runs["base"].get("fp_analysis")}
|
| 227 |
+
agg["per_image"] = summ
|
| 228 |
+
# rule variants: mean cell_attr_both and nostril type acc across images
|
| 229 |
+
rules = {}
|
| 230 |
+
for n in per_key[keys[0]]["runs"]:
|
| 231 |
+
if n.startswith("rule_"):
|
| 232 |
+
nm = [per_key[k]["runs"][n]["cells"]["attribute_accuracy"]["n_matched"] for k in keys]
|
| 233 |
+
ok = [per_key[k]["runs"][n]["cells"]["attribute_accuracy"]["both"] * m for k, m in zip(keys, nm)]
|
| 234 |
+
ntp = [per_key[k]["runs"][n]["nostrils"]["detection"]["tp"] for k in keys]
|
| 235 |
+
tok = [per_key[k]["runs"][n]["nostrils"]["type_accuracy"] * t for k, t in zip(keys, ntp)]
|
| 236 |
+
rules[n] = {
|
| 237 |
+
"pooled_cell_attr_both": float(sum(ok) / max(1, sum(nm))),
|
| 238 |
+
"pooled_nos_type_acc": float(sum(tok) / max(1, sum(ntp))),
|
| 239 |
+
"micro_f1": {**micro_average([per_key[k]["runs"][n]["cells"]["per_class"] for k in keys], CELL_LABELS),
|
| 240 |
+
**micro_average([per_key[k]["runs"][n]["nostrils"]["per_class"] for k in keys], NOSTRIL_LABELS)},
|
| 241 |
+
}
|
| 242 |
+
agg["rule_variants"] = rules
|
| 243 |
+
agg["all_deterministic"] = all(per_key[k]["deterministic"] for k in keys)
|
| 244 |
+
# antibody threshold sweep on all held-out matched nuclei (base run)
|
| 245 |
+
pairs = [p for k in keys for p in (per_key[k]["runs"]["base"].get("pairs") or [])]
|
| 246 |
+
if pairs:
|
| 247 |
+
from cellquant.labels import cell_attributes
|
| 248 |
+
y = np.array([cell_attributes(p["gt"])[0] for p in pairs]) # True = antibody +
|
| 249 |
+
s_ = np.array([p["p_antibody"] for p in pairs])
|
| 250 |
+
sweep = []
|
| 251 |
+
for thr in np.round(np.arange(0.3, 0.91, 0.05), 2):
|
| 252 |
+
pred = s_ > thr
|
| 253 |
+
tp_neg = int((~pred & ~y).sum()); fp_neg = int((~pred & y).sum()); fn_neg = int((pred & ~y).sum())
|
| 254 |
+
from cellquant.metrics import prf
|
| 255 |
+
r = prf(tp_neg, fp_neg, fn_neg)
|
| 256 |
+
sweep.append({"thr": float(thr), "neg_precision": r["precision"], "neg_recall": r["recall"], "neg_f1": r["f1"],
|
| 257 |
+
"accuracy": float((pred == y).mean())})
|
| 258 |
+
agg["antibody_threshold_sweep"] = {"n_matched": int(len(y)), "n_negative": int((~y).sum()), "sweep": sweep}
|
| 259 |
+
return agg
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def fmt_pct(x: float) -> str:
|
| 263 |
+
return f"{100*x:5.1f}%"
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def write_report(agg: dict, path: str, title: str, note: str = "") -> None:
|
| 267 |
+
L = [f"# {title}", ""]
|
| 268 |
+
if note:
|
| 269 |
+
L += [note, ""]
|
| 270 |
+
L += ["## Micro-averaged per-class scores (base run, no TTA)", "",
|
| 271 |
+
"| Label | GT | Pred | TP | FP | FN | Precision | Recall | F1 |", "|---|---:|---:|---:|---:|---:|---:|---:|---:|"]
|
| 272 |
+
for lab in ALL_LABELS:
|
| 273 |
+
r = agg["micro_base"][lab]
|
| 274 |
+
L.append(f"| {lab} | {r['gt_count']} | {r['pred_count']} | {r['tp']} | {r['fp']} | {r['fn']} | {fmt_pct(r['precision'])} | {fmt_pct(r['recall'])} | {fmt_pct(r['f1'])} |")
|
| 275 |
+
L += ["", "Class-agnostic detection (IoU >= 0.5 for nuclei; centroid distance for puncta):", ""]
|
| 276 |
+
for v in ("base", "tta4", "tta8"):
|
| 277 |
+
c, n = agg[f"detection_{v}"]["cells"], agg[f"detection_{v}"]["nostrils"]
|
| 278 |
+
c3, cc = agg[f"detection_{v}"]["cells_iou30"], agg[f"detection_{v}"]["cells_centroid"]
|
| 279 |
+
L.append(f"- {v}: nuclei P {fmt_pct(c['precision'])} R {fmt_pct(c['recall'])} F1 {fmt_pct(c['f1'])} (IoU≥0.5); F1 {fmt_pct(c3['f1'])} (IoU≥0.3); F1 {fmt_pct(cc['f1'])} (centroid-in-nucleus) | puncta P {fmt_pct(n['precision'])} R {fmt_pct(n['recall'])} F1 {fmt_pct(n['f1'])}")
|
| 280 |
+
L += ["", "## Per-image results (each image held out of training)", "",
|
| 281 |
+
"| Image | Nuclei F1 (IoU≥0.5) | F1 (IoU≥0.3) | F1 (centroid) | mean IoU | Antibody acc | Nostril-attr acc | Puncta F1 | Puncta type acc | Deterministic |", "|---|---:|---:|---:|---:|---:|---:|---:|---:|---|"]
|
| 282 |
+
for k, s in agg["per_image"].items():
|
| 283 |
+
b = s["base"]
|
| 284 |
+
L.append(f"| {k} | {fmt_pct(b['cell_det_f1'])} | {fmt_pct(b['cell_det_f1_iou30'])} | {fmt_pct(b['cell_det_f1_centroid'])} | {b['cell_mean_iou']:.3f} | {fmt_pct(b['cell_attr_antibody'])} | {fmt_pct(b['cell_attr_nostril'])} | {fmt_pct(b['nos_det_f1'])} | {fmt_pct(b['nos_type_acc'])} | {'yes' if s['deterministic'] else 'NO'} |")
|
| 285 |
+
L += ["", "## Repeated tests per image (8 rotations/flips + 6 photometric perturbations = 14 runs)", "",
|
| 286 |
+
"| Image | Nuclei F1 mean ± sd [min, max] | Attr acc mean ± sd | Puncta F1 mean ± sd [min, max] |", "|---|---|---|---|"]
|
| 287 |
+
for k, s in agg["per_image"].items():
|
| 288 |
+
r = s["robustness"]
|
| 289 |
+
a, b, c = r["cell_det_f1"], r["cell_attr_both"], r["nos_det_f1"]
|
| 290 |
+
L.append(f"| {k} | {100*a['mean']:.1f} ± {100*a['std']:.1f} [{100*a['min']:.1f}, {100*a['max']:.1f}] | {100*b['mean']:.1f} ± {100*b['std']:.1f} | {100*c['mean']:.1f} ± {100*c['std']:.1f} [{100*c['min']:.1f}, {100*c['max']:.1f}] |")
|
| 291 |
+
L += ["", "## Why nuclei F1 is below 100 %: unmatched predictions and misses (base run)", "",
|
| 292 |
+
"| Image | FP total | FP overlapping a GT nucleus | FP with no GT overlap | FP / matched median blue | FP / matched median area | FN total | FN covered by a prediction |", "|---|---:|---:|---:|---|---|---:|---:|"]
|
| 293 |
+
for k, s in agg["per_image"].items():
|
| 294 |
+
f = s.get("fp_analysis") or {}
|
| 295 |
+
if not f or f.get("n_fp") is None:
|
| 296 |
+
continue
|
| 297 |
+
L.append(f"| {k} | {f['n_fp']} | {f.get('n_fp_overlapping_gt', 0)} | {f.get('n_fp_no_gt_overlap', 0)} | {f.get('fp_median_blue') or 0:.0f} / {f.get('matched_median_blue') or 0:.0f} | {f.get('fp_median_area') or 0:.0f} / {f.get('matched_median_area') or 0:.0f} | {f.get('n_fn', 0)} | {f.get('n_fn_covered_by_prediction', 0)} |")
|
| 298 |
+
L += ["", "## Counts per image (ground truth / predicted, base run)", "",
|
| 299 |
+
"| Image | " + " | ".join(ALL_LABELS) + " |", "|---|" + "---|" * len(ALL_LABELS)]
|
| 300 |
+
for k, s in agg["per_image"].items():
|
| 301 |
+
L.append(f"| {k} | " + " | ".join(f"{s['counts'][lab]['gt']} / {s['counts'][lab]['pred']}" for lab in ALL_LABELS) + " |")
|
| 302 |
+
L += ["", "## Post-processing rule variants (micro F1 across held-out images; accuracies pooled over matched objects)", "",
|
| 303 |
+
"| Variant | " + " | ".join(ALL_LABELS) + " | attr acc (matched nuclei) | puncta type acc (matched puncta) |", "|---|" + "---|" * (len(ALL_LABELS) + 2)]
|
| 304 |
+
for n, r in agg["rule_variants"].items():
|
| 305 |
+
L.append(f"| {n} | " + " | ".join(fmt_pct(r['micro_f1'][lab]['f1']) for lab in ALL_LABELS) + f" | {fmt_pct(r['pooled_cell_attr_both'])} | {fmt_pct(r['pooled_nos_type_acc'])} |")
|
| 306 |
+
if agg.get("antibody_threshold_sweep"):
|
| 307 |
+
sw = agg["antibody_threshold_sweep"]
|
| 308 |
+
L += ["", f"## Antibody threshold sweep on the {sw['n_matched']} matched held-out nuclei ({sw['n_negative']} antibody-negative)", "",
|
| 309 |
+
"| threshold | Antibody − precision | Antibody − recall | Antibody − F1 | overall accuracy |", "|---:|---:|---:|---:|---:|"]
|
| 310 |
+
for r in sw["sweep"]:
|
| 311 |
+
L.append(f"| {r['thr']:.2f} | {fmt_pct(r['neg_precision'])} | {fmt_pct(r['neg_recall'])} | {fmt_pct(r['neg_f1'])} | {fmt_pct(r['accuracy'])} |")
|
| 312 |
+
L.append("")
|
| 313 |
+
with open(path, "w") as f:
|
| 314 |
+
f.write("\n".join(L))
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def cmd_cv(a):
|
| 318 |
+
samples, _ = discover(a.data_dir)
|
| 319 |
+
keys = a.keys.split(",") if a.keys else [s.key for s in samples]
|
| 320 |
+
os.makedirs(a.runs_dir, exist_ok=True)
|
| 321 |
+
per_key = {}
|
| 322 |
+
for s in samples:
|
| 323 |
+
if s.key not in keys:
|
| 324 |
+
continue
|
| 325 |
+
ck = os.path.join(a.runs_dir, f"fold_{s.key}.pt")
|
| 326 |
+
if not os.path.exists(ck) or a.retrain:
|
| 327 |
+
cmd = [sys.executable, "-I", os.path.join(os.path.dirname(__file__), "train.py"), "--data-dir", a.data_dir,
|
| 328 |
+
"--holdout", s.key, "--iters", str(a.iters), "--out", ck, "--seed", str(a.seed), "--device", a.device]
|
| 329 |
+
print("TRAIN:", " ".join(cmd), flush=True)
|
| 330 |
+
with open(os.path.join(a.runs_dir, f"train_{s.key}.log"), "w") as log:
|
| 331 |
+
subprocess.run(cmd, check=True, stdout=log, stderr=subprocess.STDOUT)
|
| 332 |
+
from cellquant.model import load_checkpoint
|
| 333 |
+
_, meta = load_checkpoint(ck)
|
| 334 |
+
if meta.get("holdout") != s.key or s.key in meta.get("train_keys", []):
|
| 335 |
+
raise SystemExit(f"{ck}: trained with holdout={meta.get('holdout')!r}, train_keys={meta.get('train_keys')}; "
|
| 336 |
+
f"refusing to score {s.key} with it (delete the file or use --retrain)")
|
| 337 |
+
eval_json = os.path.join(a.runs_dir, f"{s.key}_eval.json")
|
| 338 |
+
if a.reuse_eval and os.path.exists(eval_json) and os.path.getmtime(eval_json) > os.path.getmtime(ck):
|
| 339 |
+
with open(eval_json) as f:
|
| 340 |
+
per_key[s.key] = json.load(f)
|
| 341 |
+
b = per_key[s.key]["runs"]["base"]["summary"]
|
| 342 |
+
print(f"[{s.key}] reused {os.path.basename(eval_json)}: nuclei F1 {b['cell_det_f1']:.3f} attr {b['cell_attr_both']:.3f} puncta F1 {b['nos_det_f1']:.3f}", flush=True)
|
| 343 |
+
continue
|
| 344 |
+
img = load_image(s.image_path)
|
| 345 |
+
R = rasterize(s.annotation, img.shape[0], img.shape[1])
|
| 346 |
+
t = time.time()
|
| 347 |
+
res = test_image(ck, img, R, s.annotation.counts(), a.device, out_prefix=os.path.join(a.runs_dir, s.key))
|
| 348 |
+
res["train_keys"] = list(meta.get("train_keys", []))
|
| 349 |
+
res["checkpoint"] = os.path.basename(ck)
|
| 350 |
+
per_key[s.key] = res
|
| 351 |
+
b = res["runs"]["base"]["summary"]
|
| 352 |
+
print(f"[{s.key}] held-out: nuclei F1 {b['cell_det_f1']:.3f} attr {b['cell_attr_both']:.3f} puncta F1 {b['nos_det_f1']:.3f} type {b['nos_type_acc']:.3f} deterministic={res['deterministic']} ({time.time()-t:.0f}s)", flush=True)
|
| 353 |
+
with open(os.path.join(a.runs_dir, f"{s.key}_eval.json"), "w") as f:
|
| 354 |
+
json.dump(res, f, indent=1)
|
| 355 |
+
agg = aggregate(per_key)
|
| 356 |
+
totals = {lab: sum(s.annotation.count(lab) for s in samples) for lab in ALL_LABELS}
|
| 357 |
+
agg["dataset_totals"] = totals
|
| 358 |
+
with open(os.path.join(a.runs_dir, "cv_results.json"), "w") as f:
|
| 359 |
+
json.dump({"aggregate": agg, "per_image": per_key}, f, indent=1)
|
| 360 |
+
n_nuc = sum(totals[c] for c in CELL_LABELS)
|
| 361 |
+
n_pun = sum(totals[c] for c in NOSTRIL_LABELS)
|
| 362 |
+
write_report(agg, os.path.join(a.runs_dir, "cv_report.md"), "Leave-one-image-out cross-validation",
|
| 363 |
+
f"{len(per_key)} folds over {len(samples)} annotated images ({n_nuc} nucleus and {n_pun} punctum outlines); "
|
| 364 |
+
f"each image scored by a model whose checkpoint records that image as held out. Iterations per fold: {a.iters}.")
|
| 365 |
+
print("wrote", os.path.join(a.runs_dir, "cv_report.md"))
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
def cmd_final(a):
|
| 369 |
+
samples, unann = discover(a.data_dir)
|
| 370 |
+
os.makedirs(a.out_dir, exist_ok=True)
|
| 371 |
+
pred = Predictor(a.weights, device=a.device, tta=1)
|
| 372 |
+
rows = {}
|
| 373 |
+
for s in samples:
|
| 374 |
+
img = load_image(s.image_path)
|
| 375 |
+
R = rasterize(s.annotation, img.shape[0], img.shape[1])
|
| 376 |
+
res, _ = pred.predict(img, PostConfig())
|
| 377 |
+
rows[s.key] = score(R, res, s.annotation.counts())
|
| 378 |
+
Image.fromarray(render_overlay(img, res)).save(os.path.join(a.out_dir, f"{s.key}_pred.png"))
|
| 379 |
+
b = rows[s.key]["summary"]
|
| 380 |
+
print(f"[{s.key}] TRAINING-SET fit: nuclei F1 {b['cell_det_f1']:.3f} attr {b['cell_attr_both']:.3f} puncta F1 {b['nos_det_f1']:.3f}")
|
| 381 |
+
for p in unann:
|
| 382 |
+
img = load_image(p)
|
| 383 |
+
res, _ = pred.predict(img, PostConfig())
|
| 384 |
+
key = os.path.basename(p).split(" ")[0]
|
| 385 |
+
Image.fromarray(render_overlay(img, res)).save(os.path.join(a.out_dir, f"{key}_unlabeled_pred.png"))
|
| 386 |
+
rows[f"{key} (no annotation)"] = {"counts": res.counts}
|
| 387 |
+
print(f"[{key}] un-annotated image counts: {res.counts}")
|
| 388 |
+
with open(os.path.join(a.out_dir, "final_fit.json"), "w") as f:
|
| 389 |
+
json.dump(rows, f, indent=1)
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
def main(argv=None):
|
| 393 |
+
ap = argparse.ArgumentParser()
|
| 394 |
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
| 395 |
+
c = sub.add_parser("cv")
|
| 396 |
+
c.add_argument("--data-dir", required=True)
|
| 397 |
+
c.add_argument("--runs-dir", default="runs/cv")
|
| 398 |
+
c.add_argument("--iters", type=int, default=3000)
|
| 399 |
+
c.add_argument("--keys", default="")
|
| 400 |
+
c.add_argument("--seed", type=int, default=0)
|
| 401 |
+
c.add_argument("--device", default="auto")
|
| 402 |
+
c.add_argument("--retrain", action="store_true")
|
| 403 |
+
c.add_argument("--reuse-eval", action="store_true", help="reuse <key>_eval.json files newer than their checkpoint")
|
| 404 |
+
f = sub.add_parser("final")
|
| 405 |
+
f.add_argument("--data-dir", required=True)
|
| 406 |
+
f.add_argument("--weights", required=True)
|
| 407 |
+
f.add_argument("--out-dir", default="reports/final")
|
| 408 |
+
f.add_argument("--device", default="auto")
|
| 409 |
+
a = ap.parse_args(argv)
|
| 410 |
+
{"cv": cmd_cv, "final": cmd_final}[a.cmd](a)
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
if __name__ == "__main__":
|
| 414 |
+
main()
|
examples/F100_crop_512.png
ADDED
|
Git LFS Details
|
reports/SUMMARY.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CellQuant: validation summary
|
| 2 |
+
|
| 3 |
+
Data: 9 annotated 1024×1024 images, 1569 nucleus outlines and 1057 punctum outlines.
|
| 4 |
+
Protocol: leave-one-image-out cross-validation (one model per image, 3000 iterations each, held-out image never seen in training), then a final model trained on all images with the same recipe. Each held-out image was tested 23 times: 3 identical runs (determinism), no-TTA / 4× / 8× test-time augmentation, 8 rotations and flips, 6 photometric perturbations, and 6 post-processing rule variants. The only choice made on held-out data is the rule that gives a nucleus the Nostril attribute: of three candidate rules, 'contains an in-nucleus punctum' (the annotation protocol) scored best and is the default; see the rule-variant table in cv_report.md.
|
| 5 |
+
|
| 6 |
+
## Headline numbers (micro-averaged over all held-out images, no TTA)
|
| 7 |
+
|
| 8 |
+
| Measure | Precision | Recall | F1 |
|
| 9 |
+
|---|---:|---:|---:|
|
| 10 |
+
| Nucleus detection, IoU ≥ 0.5 | 77.5% | 78.4% | 77.9% |
|
| 11 |
+
| Nucleus detection, IoU ≥ 0.3 | 85.6% | 86.7% | 86.2% |
|
| 12 |
+
| Nucleus detection, centroid inside GT nucleus | 87.0% | 88.0% | 87.5% |
|
| 13 |
+
| Punctum detection (centroid ≤ 6 px) | 77.3% | 82.8% | 79.9% |
|
| 14 |
+
| Nucleus detection, IoU ≥ 0.5, 8× TTA | 78.1% | 78.9% | 78.5% |
|
| 15 |
+
|
| 16 |
+
Per-class F1 (a true positive needs a matched object **and** the right label):
|
| 17 |
+
|
| 18 |
+
| Label | GT | Pred | F1 |
|
| 19 |
+
|---|---:|---:|---:|
|
| 20 |
+
| Cell, Antibody + | 1061 | 1179 | 72.5% |
|
| 21 |
+
| Cell, Antibody - | 105 | 39 | 31.9% |
|
| 22 |
+
| Cell, Nostril, Antibody + | 398 | 370 | 71.6% |
|
| 23 |
+
| Cell, Nostril, Antibody - | 5 | 0 | 0.0% |
|
| 24 |
+
| Nostril in Nucleus | 525 | 529 | 78.9% |
|
| 25 |
+
| Nostril External of Nucleus | 532 | 603 | 70.3% |
|
| 26 |
+
|
| 27 |
+
## Per held-out image
|
| 28 |
+
|
| 29 |
+
| Image | Nuclei F1 | Antibody acc | Nostril-attr acc | Puncta F1 | Nuclei F1 over 14 perturbed runs (mean ± sd) | Deterministic |
|
| 30 |
+
|---|---:|---:|---:|---:|---|---|
|
| 31 |
+
| C100 | 81.2% | 99.3% | 98.6% | 74.4% | 82.5 ± 1.0 | yes |
|
| 32 |
+
| C102 | 90.6% | 100.0% | 90.8% | 76.3% | 89.9 ± 1.8 | yes |
|
| 33 |
+
| C110 | 78.5% | 100.0% | 91.9% | 84.3% | 77.5 ± 1.1 | yes |
|
| 34 |
+
| C112 | 75.0% | 96.6% | 97.4% | 75.8% | 73.6 ± 1.5 | yes |
|
| 35 |
+
| F100 | 75.1% | 97.8% | 97.1% | 85.2% | 72.2 ± 1.3 | yes |
|
| 36 |
+
| F110 | 72.0% | 96.0% | 94.9% | 78.7% | 71.7 ± 1.9 | yes |
|
| 37 |
+
| F113 | 65.4% | 100.0% | 97.1% | 85.2% | 65.6 ± 2.1 | yes |
|
| 38 |
+
| F114 | 78.8% | 93.3% | 94.4% | 67.5% | 77.8 ± 1.7 | yes |
|
| 39 |
+
| F99 | 84.4% | 77.5% | 99.5% | 71.4% | 85.3 ± 1.2 | yes |
|
| 40 |
+
|
| 41 |
+
## Final model (trained on all images) – fit on its own training images, NOT a generalisation estimate
|
| 42 |
+
|
| 43 |
+
| Image | Nuclei F1 | Attribute acc | Puncta F1 |
|
| 44 |
+
|---|---:|---:|---:|
|
| 45 |
+
| C100 | 86.7% | 98.7% | 82.1% |
|
| 46 |
+
| C102 | 93.3% | 94.4% | 84.6% |
|
| 47 |
+
| C110 | 88.6% | 95.7% | 85.0% |
|
| 48 |
+
| C112 | 83.8% | 99.2% | 86.2% |
|
| 49 |
+
| F100 | 87.2% | 96.2% | 90.7% |
|
| 50 |
+
| F110 | 91.7% | 94.8% | 87.2% |
|
| 51 |
+
| F113 | 83.7% | 97.7% | 87.1% |
|
| 52 |
+
| F114 | 88.3% | 93.4% | 83.1% |
|
| 53 |
+
| F99 | 89.7% | 91.2% | 75.7% |
|
| 54 |
+
|
| 55 |
+
## Antibody − threshold (held-out matched nuclei: 1230, of which 87 negative)
|
| 56 |
+
|
| 57 |
+
Default threshold 0.5. Best Antibody − F1 in the sweep: 52.3% at threshold 0.85 (precision 62.9%, recall 44.8%); the full curve is in cv_report.md. The default was kept because the class is dominated by a single image (F99 holds 62 of the 102 negatives).
|
| 58 |
+
|
| 59 |
+
## What limits the scores
|
| 60 |
+
|
| 61 |
+
* **Touching nuclei are sometimes merged** into one outline (worst on F113); this is the main source of IoU-0.5 misses. The marker/threshold sweep (`runs/cv/postprocess_sweep.json`) is flat, so it is a network-border issue, not a post-processing one.
|
| 62 |
+
* **Annotation completeness**: most unmatched predictions overlap an annotated nucleus (outline disagreement) and the rest are dimmer, smaller objects the annotator skipped; many faint red dots were likewise not annotated, which caps external-punctum precision.
|
| 63 |
+
* **Antibody − is rare** (102 of 1569 nuclei, 62 in one image), and **Cell, Nostril, Antibody −** has five examples; their scores are not reliable.
|
| 64 |
+
* Scores are from nine images of one experiment (35-day spinal cord, Iba1/TMEM119). New staining batches or magnifications need a check against a few annotated images.
|
| 65 |
+
|
| 66 |
+
## Reproduce
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
python evaluate.py cv --data-dir <folder with TIFs and CVAT zips> --runs-dir runs/cv --iters 3000
|
| 70 |
+
python evaluate.py final --data-dir <folder> --weights weights/cellquant.pt
|
| 71 |
+
python scripts/build_summary.py
|
| 72 |
+
```
|
reports/cv/cv_report.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Leave-one-image-out cross-validation
|
| 2 |
+
|
| 3 |
+
9 folds over 9 annotated images (1569 nucleus and 1057 punctum outlines); each image scored by a model whose checkpoint records that image as held out. Iterations per fold: 3000.
|
| 4 |
+
|
| 5 |
+
## Micro-averaged per-class scores (base run, no TTA)
|
| 6 |
+
|
| 7 |
+
| Label | GT | Pred | TP | FP | FN | Precision | Recall | F1 |
|
| 8 |
+
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
| 9 |
+
| Cell, Antibody + | 1061 | 1179 | 812 | 367 | 249 | 68.9% | 76.5% | 72.5% |
|
| 10 |
+
| Cell, Antibody - | 105 | 39 | 23 | 16 | 82 | 59.0% | 21.9% | 31.9% |
|
| 11 |
+
| Cell, Nostril, Antibody + | 398 | 370 | 275 | 95 | 123 | 74.3% | 69.1% | 71.6% |
|
| 12 |
+
| Cell, Nostril, Antibody - | 5 | 0 | 0 | 0 | 5 | 0.0% | 0.0% | 0.0% |
|
| 13 |
+
| Nostril in Nucleus | 525 | 529 | 416 | 113 | 109 | 78.6% | 79.2% | 78.9% |
|
| 14 |
+
| Nostril External of Nucleus | 532 | 603 | 399 | 204 | 133 | 66.2% | 75.0% | 70.3% |
|
| 15 |
+
|
| 16 |
+
Class-agnostic detection (IoU >= 0.5 for nuclei; centroid distance for puncta):
|
| 17 |
+
|
| 18 |
+
- base: nuclei P 77.5% R 78.4% F1 77.9% (IoU≥0.5); F1 86.2% (IoU≥0.3); F1 87.5% (centroid-in-nucleus) | puncta P 77.3% R 82.8% F1 79.9%
|
| 19 |
+
- tta4: nuclei P 78.0% R 78.3% F1 78.2% (IoU≥0.5); F1 86.7% (IoU≥0.3); F1 88.3% (centroid-in-nucleus) | puncta P 77.8% R 84.2% F1 80.9%
|
| 20 |
+
- tta8: nuclei P 78.1% R 78.9% F1 78.5% (IoU≥0.5); F1 86.8% (IoU≥0.3); F1 88.3% (centroid-in-nucleus) | puncta P 78.5% R 84.5% F1 81.4%
|
| 21 |
+
|
| 22 |
+
## Per-image results (each image held out of training)
|
| 23 |
+
|
| 24 |
+
| Image | Nuclei F1 (IoU≥0.5) | F1 (IoU≥0.3) | F1 (centroid) | mean IoU | Antibody acc | Nostril-attr acc | Puncta F1 | Puncta type acc | Deterministic |
|
| 25 |
+
|---|---:|---:|---:|---:|---:|---:|---:|---:|---|
|
| 26 |
+
| C100 | 81.2% | 88.4% | 89.5% | 0.783 | 99.3% | 98.6% | 74.4% | 100.0% | yes |
|
| 27 |
+
| C102 | 90.6% | 91.7% | 92.7% | 0.810 | 100.0% | 90.8% | 76.3% | 95.0% | yes |
|
| 28 |
+
| C110 | 78.5% | 85.4% | 88.1% | 0.752 | 100.0% | 91.9% | 84.3% | 92.4% | yes |
|
| 29 |
+
| C112 | 75.0% | 85.9% | 87.2% | 0.727 | 96.6% | 97.4% | 75.8% | 88.0% | yes |
|
| 30 |
+
| F100 | 75.1% | 84.9% | 84.9% | 0.761 | 97.8% | 97.1% | 85.2% | 97.0% | yes |
|
| 31 |
+
| F110 | 72.0% | 80.0% | 85.1% | 0.788 | 96.0% | 94.9% | 78.7% | 90.8% | yes |
|
| 32 |
+
| F113 | 65.4% | 80.1% | 83.3% | 0.732 | 100.0% | 97.1% | 85.2% | 80.8% | yes |
|
| 33 |
+
| F114 | 78.8% | 85.3% | 84.5% | 0.794 | 93.3% | 94.4% | 67.5% | 95.0% | yes |
|
| 34 |
+
| F99 | 84.4% | 91.9% | 92.3% | 0.802 | 77.5% | 99.5% | 71.4% | 93.3% | yes |
|
| 35 |
+
|
| 36 |
+
## Repeated tests per image (8 rotations/flips + 6 photometric perturbations = 14 runs)
|
| 37 |
+
|
| 38 |
+
| Image | Nuclei F1 mean ± sd [min, max] | Attr acc mean ± sd | Puncta F1 mean ± sd [min, max] |
|
| 39 |
+
|---|---|---|---|
|
| 40 |
+
| C100 | 82.5 ± 1.0 [81.2, 84.9] | 98.2 ± 0.8 | 74.7 ± 4.3 [68.1, 81.1] |
|
| 41 |
+
| C102 | 89.9 ± 1.8 [87.0, 93.7] | 91.9 ± 2.3 | 77.4 ± 1.9 [74.4, 80.9] |
|
| 42 |
+
| C110 | 77.5 ± 1.1 [75.3, 79.3] | 91.1 ± 1.7 | 84.1 ± 0.6 [83.0, 85.5] |
|
| 43 |
+
| C112 | 73.6 ± 1.5 [70.5, 75.2] | 94.3 ± 0.8 | 77.2 ± 1.5 [75.8, 80.6] |
|
| 44 |
+
| F100 | 72.2 ± 1.3 [69.1, 75.1] | 94.3 ± 2.0 | 85.7 ± 1.6 [82.1, 88.2] |
|
| 45 |
+
| F110 | 71.7 ± 1.9 [69.1, 75.7] | 87.9 ± 1.6 | 78.8 ± 3.1 [73.2, 82.8] |
|
| 46 |
+
| F113 | 65.6 ± 2.1 [62.1, 69.9] | 96.2 ± 0.8 | 81.9 ± 3.1 [77.2, 87.1] |
|
| 47 |
+
| F114 | 77.8 ± 1.7 [74.7, 80.9] | 88.4 ± 1.0 | 68.9 ± 2.5 [64.8, 73.5] |
|
| 48 |
+
| F99 | 85.3 ± 1.2 [82.9, 87.5] | 76.9 ± 1.5 | 75.3 ± 7.2 [57.1, 85.7] |
|
| 49 |
+
|
| 50 |
+
## Why nuclei F1 is below 100 %: unmatched predictions and misses (base run)
|
| 51 |
+
|
| 52 |
+
| Image | FP total | FP overlapping a GT nucleus | FP with no GT overlap | FP / matched median blue | FP / matched median area | FN total | FN covered by a prediction |
|
| 53 |
+
|---|---:|---:|---:|---|---|---:|---:|
|
| 54 |
+
| C100 | 37 | 27 | 10 | 48 / 76 | 264 / 642 | 31 | 28 |
|
| 55 |
+
| C102 | 14 | 3 | 11 | 21 / 52 | 332 / 807 | 4 | 1 |
|
| 56 |
+
| C110 | 45 | 30 | 15 | 21 / 43 | 282 / 634 | 36 | 25 |
|
| 57 |
+
| C112 | 49 | 40 | 9 | 24 / 28 | 354 / 664 | 29 | 24 |
|
| 58 |
+
| F100 | 42 | 24 | 18 | 17 / 33 | 601 / 744 | 49 | 46 |
|
| 59 |
+
| F110 | 48 | 41 | 7 | 46 / 61 | 166 / 676 | 29 | 17 |
|
| 60 |
+
| F113 | 47 | 31 | 16 | 10 / 24 | 713 / 702 | 61 | 55 |
|
| 61 |
+
| F114 | 35 | 24 | 11 | 73 / 89 | 702 / 652 | 62 | 53 |
|
| 62 |
+
| F99 | 41 | 34 | 7 | 36 / 56 | 435 / 595 | 38 | 35 |
|
| 63 |
+
|
| 64 |
+
## Counts per image (ground truth / predicted, base run)
|
| 65 |
+
|
| 66 |
+
| Image | Cell, Antibody + | Cell, Antibody - | Cell, Nostril, Antibody + | Cell, Nostril, Antibody - | Nostril in Nucleus | Nostril External of Nucleus |
|
| 67 |
+
|---|---|---|---|---|---|---|
|
| 68 |
+
| C100 | 165 / 167 | 0 / 4 | 13 / 13 | 0 / 0 | 14 / 15 | 6 / 8 |
|
| 69 |
+
| C102 | 41 / 52 | 0 / 0 | 50 / 49 | 0 / 0 | 60 / 73 | 74 / 105 |
|
| 70 |
+
| C110 | 61 / 84 | 1 / 1 | 122 / 108 | 0 / 0 | 169 / 165 | 207 / 244 |
|
| 71 |
+
| C112 | 124 / 141 | 5 / 1 | 17 / 24 | 0 / 0 | 23 / 27 | 2 / 14 |
|
| 72 |
+
| F100 | 87 / 97 | 6 / 6 | 93 / 76 | 0 / 0 | 118 / 122 | 44 / 26 |
|
| 73 |
+
| F110 | 87 / 119 | 7 / 2 | 33 / 26 | 1 / 0 | 57 / 38 | 137 / 101 |
|
| 74 |
+
| F113 | 138 / 127 | 3 / 1 | 22 / 21 | 0 / 0 | 26 / 23 | 8 / 4 |
|
| 75 |
+
| F114 | 175 / 155 | 21 / 11 | 42 / 49 | 4 / 0 | 52 / 62 | 38 / 85 |
|
| 76 |
+
| F99 | 183 / 237 | 62 / 13 | 6 / 4 | 0 / 0 | 6 / 4 | 16 / 16 |
|
| 77 |
+
|
| 78 |
+
## Post-processing rule variants (micro F1 across held-out images; accuracies pooled over matched objects)
|
| 79 |
+
|
| 80 |
+
| Variant | Cell, Antibody + | Cell, Antibody - | Cell, Nostril, Antibody + | Cell, Nostril, Antibody - | Nostril in Nucleus | Nostril External of Nucleus | attr acc (matched nuclei) | puncta type acc (matched puncta) |
|
| 81 |
+
|---|---|---|---|---|---|---|---|---|
|
| 82 |
+
| rule_nos=head_cell=head | 72.0% | 31.9% | 68.5% | 0.0% | 78.9% | 70.3% | 88.8% | 93.1% |
|
| 83 |
+
| rule_nos=head_cell=contains | 72.5% | 31.9% | 71.6% | 0.0% | 78.9% | 70.3% | 90.2% | 93.1% |
|
| 84 |
+
| rule_nos=head_cell=either | 72.4% | 31.9% | 69.3% | 0.0% | 78.9% | 70.3% | 89.4% | 93.1% |
|
| 85 |
+
| rule_nos=geometric_cell=head | 72.0% | 31.9% | 68.5% | 0.0% | 78.8% | 70.3% | 88.8% | 93.0% |
|
| 86 |
+
| rule_nos=geometric_cell=contains | 72.5% | 31.9% | 71.6% | 0.0% | 78.8% | 70.3% | 90.2% | 93.0% |
|
| 87 |
+
| rule_nos=geometric_cell=either | 72.4% | 31.9% | 69.3% | 0.0% | 78.8% | 70.3% | 89.4% | 93.0% |
|
| 88 |
+
|
| 89 |
+
## Antibody threshold sweep on the 1230 matched held-out nuclei (87 antibody-negative)
|
| 90 |
+
|
| 91 |
+
| threshold | Antibody − precision | Antibody − recall | Antibody − F1 | overall accuracy |
|
| 92 |
+
|---:|---:|---:|---:|---:|
|
| 93 |
+
| 0.30 | 85.0% | 19.5% | 31.8% | 94.1% |
|
| 94 |
+
| 0.35 | 78.3% | 20.7% | 32.7% | 94.0% |
|
| 95 |
+
| 0.40 | 76.9% | 23.0% | 35.4% | 94.1% |
|
| 96 |
+
| 0.45 | 74.2% | 26.4% | 39.0% | 94.1% |
|
| 97 |
+
| 0.50 | 74.2% | 26.4% | 39.0% | 94.1% |
|
| 98 |
+
| 0.55 | 75.0% | 31.0% | 43.9% | 94.4% |
|
| 99 |
+
| 0.60 | 73.7% | 32.2% | 44.8% | 94.4% |
|
| 100 |
+
| 0.65 | 72.1% | 35.6% | 47.7% | 94.5% |
|
| 101 |
+
| 0.70 | 68.1% | 36.8% | 47.8% | 94.3% |
|
| 102 |
+
| 0.75 | 65.4% | 39.1% | 48.9% | 94.2% |
|
| 103 |
+
| 0.80 | 63.6% | 40.2% | 49.3% | 94.1% |
|
| 104 |
+
| 0.85 | 62.9% | 44.8% | 52.3% | 94.2% |
|
| 105 |
+
| 0.90 | 59.7% | 46.0% | 51.9% | 94.0% |
|
reports/cv/cv_results.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
reports/cv/postprocess_sweep.json
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"marker_thr": 0.7,
|
| 4 |
+
"cell_fg_thr": 0.4,
|
| 5 |
+
"min_marker_area": 30,
|
| 6 |
+
"nuc_f1_iou50": 0.7971642378889325,
|
| 7 |
+
"nuc_f1_centroid": 0.8853879480110279,
|
| 8 |
+
"f1_ab_pos": 0.7316276537833424,
|
| 9 |
+
"f1_ab_neg": 0.2903225806451613,
|
| 10 |
+
"f1_nos_pos": 0.7247386759581882,
|
| 11 |
+
"f1_pin": 0.7816411682892906,
|
| 12 |
+
"per_image": {
|
| 13 |
+
"C100": 0.83,
|
| 14 |
+
"C102": 0.912,
|
| 15 |
+
"C110": 0.796,
|
| 16 |
+
"C112": 0.768,
|
| 17 |
+
"F113": 0.654,
|
| 18 |
+
"F114": 0.797,
|
| 19 |
+
"F99": 0.838
|
| 20 |
+
}
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"marker_thr": 0.8,
|
| 24 |
+
"cell_fg_thr": 0.4,
|
| 25 |
+
"min_marker_area": 30,
|
| 26 |
+
"nuc_f1_iou50": 0.7962456003128667,
|
| 27 |
+
"nuc_f1_centroid": 0.8838482596793116,
|
| 28 |
+
"f1_ab_pos": 0.7347379794705564,
|
| 29 |
+
"f1_ab_neg": 0.2903225806451613,
|
| 30 |
+
"f1_nos_pos": 0.7058823529411765,
|
| 31 |
+
"f1_pin": 0.7816411682892906,
|
| 32 |
+
"per_image": {
|
| 33 |
+
"C100": 0.845,
|
| 34 |
+
"C102": 0.892,
|
| 35 |
+
"C110": 0.777,
|
| 36 |
+
"C112": 0.748,
|
| 37 |
+
"F113": 0.665,
|
| 38 |
+
"F114": 0.797,
|
| 39 |
+
"F99": 0.849
|
| 40 |
+
}
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"marker_thr": 0.8,
|
| 44 |
+
"cell_fg_thr": 0.5,
|
| 45 |
+
"min_marker_area": 30,
|
| 46 |
+
"nuc_f1_iou50": 0.7952941176470589,
|
| 47 |
+
"nuc_f1_centroid": 0.8847058823529411,
|
| 48 |
+
"f1_ab_pos": 0.7316017316017317,
|
| 49 |
+
"f1_ab_neg": 0.29268292682926833,
|
| 50 |
+
"f1_nos_pos": 0.7060869565217391,
|
| 51 |
+
"f1_pin": 0.7816411682892906,
|
| 52 |
+
"per_image": {
|
| 53 |
+
"C100": 0.833,
|
| 54 |
+
"C102": 0.887,
|
| 55 |
+
"C110": 0.779,
|
| 56 |
+
"C112": 0.726,
|
| 57 |
+
"F113": 0.673,
|
| 58 |
+
"F114": 0.799,
|
| 59 |
+
"F99": 0.86
|
| 60 |
+
}
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"marker_thr": 0.7,
|
| 64 |
+
"cell_fg_thr": 0.5,
|
| 65 |
+
"min_marker_area": 30,
|
| 66 |
+
"nuc_f1_iou50": 0.7946287519747236,
|
| 67 |
+
"nuc_f1_centroid": 0.8862559241706163,
|
| 68 |
+
"f1_ab_pos": 0.7262813522355507,
|
| 69 |
+
"f1_ab_neg": 0.29268292682926833,
|
| 70 |
+
"f1_nos_pos": 0.7250437828371278,
|
| 71 |
+
"f1_pin": 0.7816411682892906,
|
| 72 |
+
"per_image": {
|
| 73 |
+
"C100": 0.818,
|
| 74 |
+
"C102": 0.906,
|
| 75 |
+
"C110": 0.798,
|
| 76 |
+
"C112": 0.739,
|
| 77 |
+
"F113": 0.662,
|
| 78 |
+
"F114": 0.799,
|
| 79 |
+
"F99": 0.845
|
| 80 |
+
}
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"marker_thr": 0.8,
|
| 84 |
+
"cell_fg_thr": 0.4,
|
| 85 |
+
"min_marker_area": 12,
|
| 86 |
+
"nuc_f1_iou50": 0.7940717628705148,
|
| 87 |
+
"nuc_f1_centroid": 0.8814352574102964,
|
| 88 |
+
"f1_ab_pos": 0.7323640280021542,
|
| 89 |
+
"f1_ab_neg": 0.2903225806451613,
|
| 90 |
+
"f1_nos_pos": 0.7046632124352332,
|
| 91 |
+
"f1_pin": 0.7816411682892906,
|
| 92 |
+
"per_image": {
|
| 93 |
+
"C100": 0.845,
|
| 94 |
+
"C102": 0.892,
|
| 95 |
+
"C110": 0.773,
|
| 96 |
+
"C112": 0.744,
|
| 97 |
+
"F113": 0.658,
|
| 98 |
+
"F114": 0.797,
|
| 99 |
+
"F99": 0.849
|
| 100 |
+
}
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"marker_thr": 0.7,
|
| 104 |
+
"cell_fg_thr": 0.4,
|
| 105 |
+
"min_marker_area": 12,
|
| 106 |
+
"nuc_f1_iou50": 0.7934143473147784,
|
| 107 |
+
"nuc_f1_centroid": 0.8812230497843984,
|
| 108 |
+
"f1_ab_pos": 0.7284552845528455,
|
| 109 |
+
"f1_ab_neg": 0.2903225806451613,
|
| 110 |
+
"f1_nos_pos": 0.7197231833910035,
|
| 111 |
+
"f1_pin": 0.7816411682892906,
|
| 112 |
+
"per_image": {
|
| 113 |
+
"C100": 0.821,
|
| 114 |
+
"C102": 0.912,
|
| 115 |
+
"C110": 0.792,
|
| 116 |
+
"C112": 0.761,
|
| 117 |
+
"F113": 0.654,
|
| 118 |
+
"F114": 0.796,
|
| 119 |
+
"F99": 0.834
|
| 120 |
+
}
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"marker_thr": 0.8,
|
| 124 |
+
"cell_fg_thr": 0.5,
|
| 125 |
+
"min_marker_area": 12,
|
| 126 |
+
"nuc_f1_iou50": 0.7931169339069223,
|
| 127 |
+
"nuc_f1_centroid": 0.8822839264763395,
|
| 128 |
+
"f1_ab_pos": 0.7292340884573895,
|
| 129 |
+
"f1_ab_neg": 0.29268292682926833,
|
| 130 |
+
"f1_nos_pos": 0.704861111111111,
|
| 131 |
+
"f1_pin": 0.7816411682892906,
|
| 132 |
+
"per_image": {
|
| 133 |
+
"C100": 0.833,
|
| 134 |
+
"C102": 0.887,
|
| 135 |
+
"C110": 0.775,
|
| 136 |
+
"C112": 0.721,
|
| 137 |
+
"F113": 0.667,
|
| 138 |
+
"F114": 0.799,
|
| 139 |
+
"F99": 0.86
|
| 140 |
+
}
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"marker_thr": 0.6,
|
| 144 |
+
"cell_fg_thr": 0.4,
|
| 145 |
+
"min_marker_area": 30,
|
| 146 |
+
"nuc_f1_iou50": 0.7930352196280174,
|
| 147 |
+
"nuc_f1_centroid": 0.8816778789077958,
|
| 148 |
+
"f1_ab_pos": 0.7275711159737418,
|
| 149 |
+
"f1_ab_neg": 0.2903225806451613,
|
| 150 |
+
"f1_nos_pos": 0.7180385288966724,
|
| 151 |
+
"f1_pin": 0.7816411682892906,
|
| 152 |
+
"per_image": {
|
| 153 |
+
"C100": 0.825,
|
| 154 |
+
"C102": 0.912,
|
| 155 |
+
"C110": 0.785,
|
| 156 |
+
"C112": 0.768,
|
| 157 |
+
"F113": 0.65,
|
| 158 |
+
"F114": 0.787,
|
| 159 |
+
"F99": 0.839
|
| 160 |
+
}
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"marker_thr": 0.5,
|
| 164 |
+
"cell_fg_thr": 0.4,
|
| 165 |
+
"min_marker_area": 30,
|
| 166 |
+
"nuc_f1_iou50": 0.792063492063492,
|
| 167 |
+
"nuc_f1_centroid": 0.880952380952381,
|
| 168 |
+
"f1_ab_pos": 0.728171334431631,
|
| 169 |
+
"f1_ab_neg": 0.2903225806451613,
|
| 170 |
+
"f1_nos_pos": 0.7145359019264448,
|
| 171 |
+
"f1_pin": 0.7816411682892906,
|
| 172 |
+
"per_image": {
|
| 173 |
+
"C100": 0.823,
|
| 174 |
+
"C102": 0.912,
|
| 175 |
+
"C110": 0.785,
|
| 176 |
+
"C112": 0.782,
|
| 177 |
+
"F113": 0.647,
|
| 178 |
+
"F114": 0.78,
|
| 179 |
+
"F99": 0.836
|
| 180 |
+
}
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"marker_thr": 0.6,
|
| 184 |
+
"cell_fg_thr": 0.5,
|
| 185 |
+
"min_marker_area": 30,
|
| 186 |
+
"nuc_f1_iou50": 0.792063492063492,
|
| 187 |
+
"nuc_f1_centroid": 0.8825396825396825,
|
| 188 |
+
"f1_ab_pos": 0.7243835616438358,
|
| 189 |
+
"f1_ab_neg": 0.29268292682926833,
|
| 190 |
+
"f1_nos_pos": 0.7183098591549296,
|
| 191 |
+
"f1_pin": 0.7816411682892906,
|
| 192 |
+
"per_image": {
|
| 193 |
+
"C100": 0.819,
|
| 194 |
+
"C102": 0.906,
|
| 195 |
+
"C110": 0.787,
|
| 196 |
+
"C112": 0.739,
|
| 197 |
+
"F113": 0.658,
|
| 198 |
+
"F114": 0.793,
|
| 199 |
+
"F99": 0.847
|
| 200 |
+
}
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"marker_thr": 0.5,
|
| 204 |
+
"cell_fg_thr": 0.5,
|
| 205 |
+
"min_marker_area": 30,
|
| 206 |
+
"nuc_f1_iou50": 0.7914012738853503,
|
| 207 |
+
"nuc_f1_centroid": 0.8821656050955414,
|
| 208 |
+
"f1_ab_pos": 0.7253714914694551,
|
| 209 |
+
"f1_ab_neg": 0.29268292682926833,
|
| 210 |
+
"f1_nos_pos": 0.7147887323943662,
|
| 211 |
+
"f1_pin": 0.7816411682892906,
|
| 212 |
+
"per_image": {
|
| 213 |
+
"C100": 0.817,
|
| 214 |
+
"C102": 0.906,
|
| 215 |
+
"C110": 0.787,
|
| 216 |
+
"C112": 0.752,
|
| 217 |
+
"F113": 0.656,
|
| 218 |
+
"F114": 0.788,
|
| 219 |
+
"F99": 0.844
|
| 220 |
+
}
|
| 221 |
+
},
|
| 222 |
+
{
|
| 223 |
+
"marker_thr": 0.7,
|
| 224 |
+
"cell_fg_thr": 0.5,
|
| 225 |
+
"min_marker_area": 12,
|
| 226 |
+
"nuc_f1_iou50": 0.7908805031446541,
|
| 227 |
+
"nuc_f1_centroid": 0.8820754716981133,
|
| 228 |
+
"f1_ab_pos": 0.7231270358306189,
|
| 229 |
+
"f1_ab_neg": 0.29268292682926833,
|
| 230 |
+
"f1_nos_pos": 0.7200000000000002,
|
| 231 |
+
"f1_pin": 0.7816411682892906,
|
| 232 |
+
"per_image": {
|
| 233 |
+
"C100": 0.809,
|
| 234 |
+
"C102": 0.906,
|
| 235 |
+
"C110": 0.794,
|
| 236 |
+
"C112": 0.732,
|
| 237 |
+
"F113": 0.662,
|
| 238 |
+
"F114": 0.797,
|
| 239 |
+
"F99": 0.842
|
| 240 |
+
}
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
"marker_thr": 0.6,
|
| 244 |
+
"cell_fg_thr": 0.4,
|
| 245 |
+
"min_marker_area": 12,
|
| 246 |
+
"nuc_f1_iou50": 0.7899093417422152,
|
| 247 |
+
"nuc_f1_centroid": 0.8789909341742216,
|
| 248 |
+
"f1_ab_pos": 0.724006532389766,
|
| 249 |
+
"f1_ab_neg": 0.2903225806451613,
|
| 250 |
+
"f1_nos_pos": 0.7132867132867132,
|
| 251 |
+
"f1_pin": 0.7816411682892906,
|
| 252 |
+
"per_image": {
|
| 253 |
+
"C100": 0.816,
|
| 254 |
+
"C102": 0.912,
|
| 255 |
+
"C110": 0.783,
|
| 256 |
+
"C112": 0.761,
|
| 257 |
+
"F113": 0.645,
|
| 258 |
+
"F114": 0.787,
|
| 259 |
+
"F99": 0.839
|
| 260 |
+
}
|
| 261 |
+
},
|
| 262 |
+
{
|
| 263 |
+
"marker_thr": 0.5,
|
| 264 |
+
"cell_fg_thr": 0.4,
|
| 265 |
+
"min_marker_area": 12,
|
| 266 |
+
"nuc_f1_iou50": 0.7898694103680254,
|
| 267 |
+
"nuc_f1_centroid": 0.8793035219628017,
|
| 268 |
+
"f1_ab_pos": 0.726079825041006,
|
| 269 |
+
"f1_ab_neg": 0.2903225806451613,
|
| 270 |
+
"f1_nos_pos": 0.712280701754386,
|
| 271 |
+
"f1_pin": 0.7816411682892906,
|
| 272 |
+
"per_image": {
|
| 273 |
+
"C100": 0.819,
|
| 274 |
+
"C102": 0.912,
|
| 275 |
+
"C110": 0.783,
|
| 276 |
+
"C112": 0.78,
|
| 277 |
+
"F113": 0.643,
|
| 278 |
+
"F114": 0.78,
|
| 279 |
+
"F99": 0.834
|
| 280 |
+
}
|
| 281 |
+
},
|
| 282 |
+
{
|
| 283 |
+
"marker_thr": 0.5,
|
| 284 |
+
"cell_fg_thr": 0.5,
|
| 285 |
+
"min_marker_area": 12,
|
| 286 |
+
"nuc_f1_iou50": 0.7898291617004372,
|
| 287 |
+
"nuc_f1_centroid": 0.881207787048073,
|
| 288 |
+
"f1_ab_pos": 0.7240811848601207,
|
| 289 |
+
"f1_ab_neg": 0.29268292682926833,
|
| 290 |
+
"f1_nos_pos": 0.7125220458553791,
|
| 291 |
+
"f1_pin": 0.7816411682892906,
|
| 292 |
+
"per_image": {
|
| 293 |
+
"C100": 0.812,
|
| 294 |
+
"C102": 0.906,
|
| 295 |
+
"C110": 0.785,
|
| 296 |
+
"C112": 0.75,
|
| 297 |
+
"F113": 0.654,
|
| 298 |
+
"F114": 0.788,
|
| 299 |
+
"F99": 0.844
|
| 300 |
+
}
|
| 301 |
+
},
|
| 302 |
+
{
|
| 303 |
+
"marker_thr": 0.6,
|
| 304 |
+
"cell_fg_thr": 0.5,
|
| 305 |
+
"min_marker_area": 12,
|
| 306 |
+
"nuc_f1_iou50": 0.7892447607750098,
|
| 307 |
+
"nuc_f1_centroid": 0.8809806247528668,
|
| 308 |
+
"f1_ab_pos": 0.7212220403709765,
|
| 309 |
+
"f1_ab_neg": 0.29268292682926833,
|
| 310 |
+
"f1_nos_pos": 0.7135325131810192,
|
| 311 |
+
"f1_pin": 0.7816411682892906,
|
| 312 |
+
"per_image": {
|
| 313 |
+
"C100": 0.81,
|
| 314 |
+
"C102": 0.906,
|
| 315 |
+
"C110": 0.785,
|
| 316 |
+
"C112": 0.732,
|
| 317 |
+
"F113": 0.656,
|
| 318 |
+
"F114": 0.793,
|
| 319 |
+
"F99": 0.847
|
| 320 |
+
}
|
| 321 |
+
}
|
| 322 |
+
]
|
reports/final/final_fit.json
ADDED
|
@@ -0,0 +1,1820 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"C100": {
|
| 3 |
+
"cells": {
|
| 4 |
+
"detection": {
|
| 5 |
+
"tp": 153,
|
| 6 |
+
"fp": 22,
|
| 7 |
+
"fn": 25,
|
| 8 |
+
"precision": 0.8742857142857143,
|
| 9 |
+
"recall": 0.8595505617977528,
|
| 10 |
+
"f1": 0.8668555240793201,
|
| 11 |
+
"mean_iou_matched": 0.7974392864551416,
|
| 12 |
+
"iou30": {
|
| 13 |
+
"tp": 165,
|
| 14 |
+
"fp": 10,
|
| 15 |
+
"fn": 13,
|
| 16 |
+
"precision": 0.9428571428571428,
|
| 17 |
+
"recall": 0.9269662921348315,
|
| 18 |
+
"f1": 0.934844192634561
|
| 19 |
+
},
|
| 20 |
+
"centroid": {
|
| 21 |
+
"tp": 165,
|
| 22 |
+
"fp": 10,
|
| 23 |
+
"fn": 13,
|
| 24 |
+
"precision": 0.9428571428571428,
|
| 25 |
+
"recall": 0.9269662921348315,
|
| 26 |
+
"f1": 0.934844192634561
|
| 27 |
+
}
|
| 28 |
+
},
|
| 29 |
+
"attribute_accuracy": {
|
| 30 |
+
"antibody": 0.9934640522875817,
|
| 31 |
+
"nostril": 0.9934640522875817,
|
| 32 |
+
"both": 0.9869281045751634,
|
| 33 |
+
"n_matched": 153
|
| 34 |
+
},
|
| 35 |
+
"per_class": {
|
| 36 |
+
"Cell, Antibody +": {
|
| 37 |
+
"tp": 139,
|
| 38 |
+
"fp": 22,
|
| 39 |
+
"fn": 26,
|
| 40 |
+
"precision": 0.8633540372670807,
|
| 41 |
+
"recall": 0.8424242424242424,
|
| 42 |
+
"f1": 0.852760736196319,
|
| 43 |
+
"gt_count": 165,
|
| 44 |
+
"pred_count": 161
|
| 45 |
+
},
|
| 46 |
+
"Cell, Antibody -": {
|
| 47 |
+
"tp": 0,
|
| 48 |
+
"fp": 2,
|
| 49 |
+
"fn": 0,
|
| 50 |
+
"precision": 0.0,
|
| 51 |
+
"recall": 0.0,
|
| 52 |
+
"f1": 0.0,
|
| 53 |
+
"gt_count": 0,
|
| 54 |
+
"pred_count": 2
|
| 55 |
+
},
|
| 56 |
+
"Cell, Nostril, Antibody +": {
|
| 57 |
+
"tp": 12,
|
| 58 |
+
"fp": 0,
|
| 59 |
+
"fn": 1,
|
| 60 |
+
"precision": 1.0,
|
| 61 |
+
"recall": 0.9230769230769231,
|
| 62 |
+
"f1": 0.9600000000000001,
|
| 63 |
+
"gt_count": 13,
|
| 64 |
+
"pred_count": 12
|
| 65 |
+
},
|
| 66 |
+
"Cell, Nostril, Antibody -": {
|
| 67 |
+
"tp": 0,
|
| 68 |
+
"fp": 0,
|
| 69 |
+
"fn": 0,
|
| 70 |
+
"precision": 0.0,
|
| 71 |
+
"recall": 0.0,
|
| 72 |
+
"f1": 0.0,
|
| 73 |
+
"gt_count": 0,
|
| 74 |
+
"pred_count": 0
|
| 75 |
+
}
|
| 76 |
+
},
|
| 77 |
+
"confusion": {
|
| 78 |
+
"Cell, Antibody +": {
|
| 79 |
+
"Cell, Antibody +": 139,
|
| 80 |
+
"Cell, Antibody -": 1,
|
| 81 |
+
"Cell, Nostril, Antibody +": 0,
|
| 82 |
+
"Cell, Nostril, Antibody -": 0
|
| 83 |
+
},
|
| 84 |
+
"Cell, Antibody -": {
|
| 85 |
+
"Cell, Antibody +": 0,
|
| 86 |
+
"Cell, Antibody -": 0,
|
| 87 |
+
"Cell, Nostril, Antibody +": 0,
|
| 88 |
+
"Cell, Nostril, Antibody -": 0
|
| 89 |
+
},
|
| 90 |
+
"Cell, Nostril, Antibody +": {
|
| 91 |
+
"Cell, Antibody +": 1,
|
| 92 |
+
"Cell, Antibody -": 0,
|
| 93 |
+
"Cell, Nostril, Antibody +": 12,
|
| 94 |
+
"Cell, Nostril, Antibody -": 0
|
| 95 |
+
},
|
| 96 |
+
"Cell, Nostril, Antibody -": {
|
| 97 |
+
"Cell, Antibody +": 0,
|
| 98 |
+
"Cell, Antibody -": 0,
|
| 99 |
+
"Cell, Nostril, Antibody +": 0,
|
| 100 |
+
"Cell, Nostril, Antibody -": 0
|
| 101 |
+
}
|
| 102 |
+
},
|
| 103 |
+
"n_gt": 178,
|
| 104 |
+
"n_pred": 175
|
| 105 |
+
},
|
| 106 |
+
"nostrils": {
|
| 107 |
+
"detection": {
|
| 108 |
+
"tp": 16,
|
| 109 |
+
"fp": 3,
|
| 110 |
+
"fn": 4,
|
| 111 |
+
"precision": 0.8421052631578947,
|
| 112 |
+
"recall": 0.8,
|
| 113 |
+
"f1": 0.8205128205128205,
|
| 114 |
+
"mean_dist_matched": 1.157546253191996
|
| 115 |
+
},
|
| 116 |
+
"type_accuracy": 1.0,
|
| 117 |
+
"per_class": {
|
| 118 |
+
"Nostril in Nucleus": {
|
| 119 |
+
"tp": 12,
|
| 120 |
+
"fp": 0,
|
| 121 |
+
"fn": 2,
|
| 122 |
+
"precision": 1.0,
|
| 123 |
+
"recall": 0.8571428571428571,
|
| 124 |
+
"f1": 0.923076923076923,
|
| 125 |
+
"gt_count": 14,
|
| 126 |
+
"pred_count": 12
|
| 127 |
+
},
|
| 128 |
+
"Nostril External of Nucleus": {
|
| 129 |
+
"tp": 4,
|
| 130 |
+
"fp": 3,
|
| 131 |
+
"fn": 2,
|
| 132 |
+
"precision": 0.5714285714285714,
|
| 133 |
+
"recall": 0.6666666666666666,
|
| 134 |
+
"f1": 0.6153846153846153,
|
| 135 |
+
"gt_count": 6,
|
| 136 |
+
"pred_count": 7
|
| 137 |
+
}
|
| 138 |
+
},
|
| 139 |
+
"n_gt": 20,
|
| 140 |
+
"n_pred": 19
|
| 141 |
+
},
|
| 142 |
+
"counts": {
|
| 143 |
+
"Cell, Antibody +": {
|
| 144 |
+
"gt": 165,
|
| 145 |
+
"pred": 161,
|
| 146 |
+
"abs_err": 4,
|
| 147 |
+
"rel_err": 0.024242424242424242
|
| 148 |
+
},
|
| 149 |
+
"Cell, Antibody -": {
|
| 150 |
+
"gt": 0,
|
| 151 |
+
"pred": 2,
|
| 152 |
+
"abs_err": 2,
|
| 153 |
+
"rel_err": Infinity
|
| 154 |
+
},
|
| 155 |
+
"Cell, Nostril, Antibody +": {
|
| 156 |
+
"gt": 13,
|
| 157 |
+
"pred": 12,
|
| 158 |
+
"abs_err": 1,
|
| 159 |
+
"rel_err": 0.07692307692307693
|
| 160 |
+
},
|
| 161 |
+
"Cell, Nostril, Antibody -": {
|
| 162 |
+
"gt": 0,
|
| 163 |
+
"pred": 0,
|
| 164 |
+
"abs_err": 0,
|
| 165 |
+
"rel_err": 0.0
|
| 166 |
+
},
|
| 167 |
+
"Nostril in Nucleus": {
|
| 168 |
+
"gt": 14,
|
| 169 |
+
"pred": 12,
|
| 170 |
+
"abs_err": 2,
|
| 171 |
+
"rel_err": 0.14285714285714285
|
| 172 |
+
},
|
| 173 |
+
"Nostril External of Nucleus": {
|
| 174 |
+
"gt": 6,
|
| 175 |
+
"pred": 7,
|
| 176 |
+
"abs_err": 1,
|
| 177 |
+
"rel_err": 0.16666666666666666
|
| 178 |
+
}
|
| 179 |
+
},
|
| 180 |
+
"fp_analysis": null,
|
| 181 |
+
"pairs": null,
|
| 182 |
+
"summary": {
|
| 183 |
+
"cell_det_f1": 0.8668555240793201,
|
| 184 |
+
"cell_det_precision": 0.8742857142857143,
|
| 185 |
+
"cell_det_recall": 0.8595505617977528,
|
| 186 |
+
"cell_mean_iou": 0.7974392864551416,
|
| 187 |
+
"cell_det_f1_iou30": 0.934844192634561,
|
| 188 |
+
"cell_det_f1_centroid": 0.934844192634561,
|
| 189 |
+
"cell_attr_antibody": 0.9934640522875817,
|
| 190 |
+
"cell_attr_nostril": 0.9934640522875817,
|
| 191 |
+
"cell_attr_both": 0.9869281045751634,
|
| 192 |
+
"nos_det_f1": 0.8205128205128205,
|
| 193 |
+
"nos_det_precision": 0.8421052631578947,
|
| 194 |
+
"nos_det_recall": 0.8,
|
| 195 |
+
"nos_type_acc": 1.0,
|
| 196 |
+
"f1[Cell, Antibody +]": 0.852760736196319,
|
| 197 |
+
"f1[Cell, Antibody -]": 0.0,
|
| 198 |
+
"f1[Cell, Nostril, Antibody +]": 0.9600000000000001,
|
| 199 |
+
"f1[Cell, Nostril, Antibody -]": 0.0,
|
| 200 |
+
"f1[Nostril in Nucleus]": 0.923076923076923,
|
| 201 |
+
"f1[Nostril External of Nucleus]": 0.6153846153846153
|
| 202 |
+
}
|
| 203 |
+
},
|
| 204 |
+
"C102": {
|
| 205 |
+
"cells": {
|
| 206 |
+
"detection": {
|
| 207 |
+
"tp": 90,
|
| 208 |
+
"fp": 12,
|
| 209 |
+
"fn": 1,
|
| 210 |
+
"precision": 0.8823529411764706,
|
| 211 |
+
"recall": 0.989010989010989,
|
| 212 |
+
"f1": 0.9326424870466321,
|
| 213 |
+
"mean_iou_matched": 0.8122454665114849,
|
| 214 |
+
"iou30": {
|
| 215 |
+
"tp": 90,
|
| 216 |
+
"fp": 12,
|
| 217 |
+
"fn": 1,
|
| 218 |
+
"precision": 0.8823529411764706,
|
| 219 |
+
"recall": 0.989010989010989,
|
| 220 |
+
"f1": 0.9326424870466321
|
| 221 |
+
},
|
| 222 |
+
"centroid": {
|
| 223 |
+
"tp": 90,
|
| 224 |
+
"fp": 12,
|
| 225 |
+
"fn": 1,
|
| 226 |
+
"precision": 0.8823529411764706,
|
| 227 |
+
"recall": 0.989010989010989,
|
| 228 |
+
"f1": 0.9326424870466321
|
| 229 |
+
}
|
| 230 |
+
},
|
| 231 |
+
"attribute_accuracy": {
|
| 232 |
+
"antibody": 1.0,
|
| 233 |
+
"nostril": 0.9444444444444444,
|
| 234 |
+
"both": 0.9444444444444444,
|
| 235 |
+
"n_matched": 90
|
| 236 |
+
},
|
| 237 |
+
"per_class": {
|
| 238 |
+
"Cell, Antibody +": {
|
| 239 |
+
"tp": 41,
|
| 240 |
+
"fp": 15,
|
| 241 |
+
"fn": 0,
|
| 242 |
+
"precision": 0.7321428571428571,
|
| 243 |
+
"recall": 1.0,
|
| 244 |
+
"f1": 0.8453608247422679,
|
| 245 |
+
"gt_count": 41,
|
| 246 |
+
"pred_count": 56
|
| 247 |
+
},
|
| 248 |
+
"Cell, Antibody -": {
|
| 249 |
+
"tp": 0,
|
| 250 |
+
"fp": 0,
|
| 251 |
+
"fn": 0,
|
| 252 |
+
"precision": 0.0,
|
| 253 |
+
"recall": 0.0,
|
| 254 |
+
"f1": 0.0,
|
| 255 |
+
"gt_count": 0,
|
| 256 |
+
"pred_count": 0
|
| 257 |
+
},
|
| 258 |
+
"Cell, Nostril, Antibody +": {
|
| 259 |
+
"tp": 44,
|
| 260 |
+
"fp": 2,
|
| 261 |
+
"fn": 6,
|
| 262 |
+
"precision": 0.9565217391304348,
|
| 263 |
+
"recall": 0.88,
|
| 264 |
+
"f1": 0.9166666666666666,
|
| 265 |
+
"gt_count": 50,
|
| 266 |
+
"pred_count": 46
|
| 267 |
+
},
|
| 268 |
+
"Cell, Nostril, Antibody -": {
|
| 269 |
+
"tp": 0,
|
| 270 |
+
"fp": 0,
|
| 271 |
+
"fn": 0,
|
| 272 |
+
"precision": 0.0,
|
| 273 |
+
"recall": 0.0,
|
| 274 |
+
"f1": 0.0,
|
| 275 |
+
"gt_count": 0,
|
| 276 |
+
"pred_count": 0
|
| 277 |
+
}
|
| 278 |
+
},
|
| 279 |
+
"confusion": {
|
| 280 |
+
"Cell, Antibody +": {
|
| 281 |
+
"Cell, Antibody +": 41,
|
| 282 |
+
"Cell, Antibody -": 0,
|
| 283 |
+
"Cell, Nostril, Antibody +": 0,
|
| 284 |
+
"Cell, Nostril, Antibody -": 0
|
| 285 |
+
},
|
| 286 |
+
"Cell, Antibody -": {
|
| 287 |
+
"Cell, Antibody +": 0,
|
| 288 |
+
"Cell, Antibody -": 0,
|
| 289 |
+
"Cell, Nostril, Antibody +": 0,
|
| 290 |
+
"Cell, Nostril, Antibody -": 0
|
| 291 |
+
},
|
| 292 |
+
"Cell, Nostril, Antibody +": {
|
| 293 |
+
"Cell, Antibody +": 5,
|
| 294 |
+
"Cell, Antibody -": 0,
|
| 295 |
+
"Cell, Nostril, Antibody +": 44,
|
| 296 |
+
"Cell, Nostril, Antibody -": 0
|
| 297 |
+
},
|
| 298 |
+
"Cell, Nostril, Antibody -": {
|
| 299 |
+
"Cell, Antibody +": 0,
|
| 300 |
+
"Cell, Antibody -": 0,
|
| 301 |
+
"Cell, Nostril, Antibody +": 0,
|
| 302 |
+
"Cell, Nostril, Antibody -": 0
|
| 303 |
+
}
|
| 304 |
+
},
|
| 305 |
+
"n_gt": 91,
|
| 306 |
+
"n_pred": 102
|
| 307 |
+
},
|
| 308 |
+
"nostrils": {
|
| 309 |
+
"detection": {
|
| 310 |
+
"tp": 121,
|
| 311 |
+
"fp": 31,
|
| 312 |
+
"fn": 13,
|
| 313 |
+
"precision": 0.7960526315789473,
|
| 314 |
+
"recall": 0.9029850746268657,
|
| 315 |
+
"f1": 0.8461538461538461,
|
| 316 |
+
"mean_dist_matched": 1.0261222516695998
|
| 317 |
+
},
|
| 318 |
+
"type_accuracy": 1.0,
|
| 319 |
+
"per_class": {
|
| 320 |
+
"Nostril in Nucleus": {
|
| 321 |
+
"tp": 54,
|
| 322 |
+
"fp": 8,
|
| 323 |
+
"fn": 6,
|
| 324 |
+
"precision": 0.8709677419354839,
|
| 325 |
+
"recall": 0.9,
|
| 326 |
+
"f1": 0.8852459016393444,
|
| 327 |
+
"gt_count": 60,
|
| 328 |
+
"pred_count": 62
|
| 329 |
+
},
|
| 330 |
+
"Nostril External of Nucleus": {
|
| 331 |
+
"tp": 67,
|
| 332 |
+
"fp": 23,
|
| 333 |
+
"fn": 7,
|
| 334 |
+
"precision": 0.7444444444444445,
|
| 335 |
+
"recall": 0.9054054054054054,
|
| 336 |
+
"f1": 0.8170731707317074,
|
| 337 |
+
"gt_count": 74,
|
| 338 |
+
"pred_count": 90
|
| 339 |
+
}
|
| 340 |
+
},
|
| 341 |
+
"n_gt": 134,
|
| 342 |
+
"n_pred": 152
|
| 343 |
+
},
|
| 344 |
+
"counts": {
|
| 345 |
+
"Cell, Antibody +": {
|
| 346 |
+
"gt": 41,
|
| 347 |
+
"pred": 56,
|
| 348 |
+
"abs_err": 15,
|
| 349 |
+
"rel_err": 0.36585365853658536
|
| 350 |
+
},
|
| 351 |
+
"Cell, Antibody -": {
|
| 352 |
+
"gt": 0,
|
| 353 |
+
"pred": 0,
|
| 354 |
+
"abs_err": 0,
|
| 355 |
+
"rel_err": 0.0
|
| 356 |
+
},
|
| 357 |
+
"Cell, Nostril, Antibody +": {
|
| 358 |
+
"gt": 50,
|
| 359 |
+
"pred": 46,
|
| 360 |
+
"abs_err": 4,
|
| 361 |
+
"rel_err": 0.08
|
| 362 |
+
},
|
| 363 |
+
"Cell, Nostril, Antibody -": {
|
| 364 |
+
"gt": 0,
|
| 365 |
+
"pred": 0,
|
| 366 |
+
"abs_err": 0,
|
| 367 |
+
"rel_err": 0.0
|
| 368 |
+
},
|
| 369 |
+
"Nostril in Nucleus": {
|
| 370 |
+
"gt": 60,
|
| 371 |
+
"pred": 62,
|
| 372 |
+
"abs_err": 2,
|
| 373 |
+
"rel_err": 0.03333333333333333
|
| 374 |
+
},
|
| 375 |
+
"Nostril External of Nucleus": {
|
| 376 |
+
"gt": 74,
|
| 377 |
+
"pred": 90,
|
| 378 |
+
"abs_err": 16,
|
| 379 |
+
"rel_err": 0.21621621621621623
|
| 380 |
+
}
|
| 381 |
+
},
|
| 382 |
+
"fp_analysis": null,
|
| 383 |
+
"pairs": null,
|
| 384 |
+
"summary": {
|
| 385 |
+
"cell_det_f1": 0.9326424870466321,
|
| 386 |
+
"cell_det_precision": 0.8823529411764706,
|
| 387 |
+
"cell_det_recall": 0.989010989010989,
|
| 388 |
+
"cell_mean_iou": 0.8122454665114849,
|
| 389 |
+
"cell_det_f1_iou30": 0.9326424870466321,
|
| 390 |
+
"cell_det_f1_centroid": 0.9326424870466321,
|
| 391 |
+
"cell_attr_antibody": 1.0,
|
| 392 |
+
"cell_attr_nostril": 0.9444444444444444,
|
| 393 |
+
"cell_attr_both": 0.9444444444444444,
|
| 394 |
+
"nos_det_f1": 0.8461538461538461,
|
| 395 |
+
"nos_det_precision": 0.7960526315789473,
|
| 396 |
+
"nos_det_recall": 0.9029850746268657,
|
| 397 |
+
"nos_type_acc": 1.0,
|
| 398 |
+
"f1[Cell, Antibody +]": 0.8453608247422679,
|
| 399 |
+
"f1[Cell, Antibody -]": 0.0,
|
| 400 |
+
"f1[Cell, Nostril, Antibody +]": 0.9166666666666666,
|
| 401 |
+
"f1[Cell, Nostril, Antibody -]": 0.0,
|
| 402 |
+
"f1[Nostril in Nucleus]": 0.8852459016393444,
|
| 403 |
+
"f1[Nostril External of Nucleus]": 0.8170731707317074
|
| 404 |
+
}
|
| 405 |
+
},
|
| 406 |
+
"C110": {
|
| 407 |
+
"cells": {
|
| 408 |
+
"detection": {
|
| 409 |
+
"tp": 164,
|
| 410 |
+
"fp": 22,
|
| 411 |
+
"fn": 20,
|
| 412 |
+
"precision": 0.8817204301075269,
|
| 413 |
+
"recall": 0.8913043478260869,
|
| 414 |
+
"f1": 0.8864864864864864,
|
| 415 |
+
"mean_iou_matched": 0.800839882168248,
|
| 416 |
+
"iou30": {
|
| 417 |
+
"tp": 173,
|
| 418 |
+
"fp": 13,
|
| 419 |
+
"fn": 11,
|
| 420 |
+
"precision": 0.9301075268817204,
|
| 421 |
+
"recall": 0.9402173913043478,
|
| 422 |
+
"f1": 0.9351351351351351
|
| 423 |
+
},
|
| 424 |
+
"centroid": {
|
| 425 |
+
"tp": 172,
|
| 426 |
+
"fp": 14,
|
| 427 |
+
"fn": 12,
|
| 428 |
+
"precision": 0.9247311827956989,
|
| 429 |
+
"recall": 0.9347826086956522,
|
| 430 |
+
"f1": 0.9297297297297297
|
| 431 |
+
}
|
| 432 |
+
},
|
| 433 |
+
"attribute_accuracy": {
|
| 434 |
+
"antibody": 1.0,
|
| 435 |
+
"nostril": 0.9573170731707317,
|
| 436 |
+
"both": 0.9573170731707317,
|
| 437 |
+
"n_matched": 164
|
| 438 |
+
},
|
| 439 |
+
"per_class": {
|
| 440 |
+
"Cell, Antibody +": {
|
| 441 |
+
"tp": 54,
|
| 442 |
+
"fp": 19,
|
| 443 |
+
"fn": 7,
|
| 444 |
+
"precision": 0.7397260273972602,
|
| 445 |
+
"recall": 0.8852459016393442,
|
| 446 |
+
"f1": 0.8059701492537313,
|
| 447 |
+
"gt_count": 61,
|
| 448 |
+
"pred_count": 73
|
| 449 |
+
},
|
| 450 |
+
"Cell, Antibody -": {
|
| 451 |
+
"tp": 1,
|
| 452 |
+
"fp": 0,
|
| 453 |
+
"fn": 0,
|
| 454 |
+
"precision": 1.0,
|
| 455 |
+
"recall": 1.0,
|
| 456 |
+
"f1": 1.0,
|
| 457 |
+
"gt_count": 1,
|
| 458 |
+
"pred_count": 1
|
| 459 |
+
},
|
| 460 |
+
"Cell, Nostril, Antibody +": {
|
| 461 |
+
"tp": 102,
|
| 462 |
+
"fp": 10,
|
| 463 |
+
"fn": 20,
|
| 464 |
+
"precision": 0.9107142857142857,
|
| 465 |
+
"recall": 0.8360655737704918,
|
| 466 |
+
"f1": 0.8717948717948717,
|
| 467 |
+
"gt_count": 122,
|
| 468 |
+
"pred_count": 112
|
| 469 |
+
},
|
| 470 |
+
"Cell, Nostril, Antibody -": {
|
| 471 |
+
"tp": 0,
|
| 472 |
+
"fp": 0,
|
| 473 |
+
"fn": 0,
|
| 474 |
+
"precision": 0.0,
|
| 475 |
+
"recall": 0.0,
|
| 476 |
+
"f1": 0.0,
|
| 477 |
+
"gt_count": 0,
|
| 478 |
+
"pred_count": 0
|
| 479 |
+
}
|
| 480 |
+
},
|
| 481 |
+
"confusion": {
|
| 482 |
+
"Cell, Antibody +": {
|
| 483 |
+
"Cell, Antibody +": 54,
|
| 484 |
+
"Cell, Antibody -": 0,
|
| 485 |
+
"Cell, Nostril, Antibody +": 3,
|
| 486 |
+
"Cell, Nostril, Antibody -": 0
|
| 487 |
+
},
|
| 488 |
+
"Cell, Antibody -": {
|
| 489 |
+
"Cell, Antibody +": 0,
|
| 490 |
+
"Cell, Antibody -": 1,
|
| 491 |
+
"Cell, Nostril, Antibody +": 0,
|
| 492 |
+
"Cell, Nostril, Antibody -": 0
|
| 493 |
+
},
|
| 494 |
+
"Cell, Nostril, Antibody +": {
|
| 495 |
+
"Cell, Antibody +": 4,
|
| 496 |
+
"Cell, Antibody -": 0,
|
| 497 |
+
"Cell, Nostril, Antibody +": 102,
|
| 498 |
+
"Cell, Nostril, Antibody -": 0
|
| 499 |
+
},
|
| 500 |
+
"Cell, Nostril, Antibody -": {
|
| 501 |
+
"Cell, Antibody +": 0,
|
| 502 |
+
"Cell, Antibody -": 0,
|
| 503 |
+
"Cell, Nostril, Antibody +": 0,
|
| 504 |
+
"Cell, Nostril, Antibody -": 0
|
| 505 |
+
}
|
| 506 |
+
},
|
| 507 |
+
"n_gt": 184,
|
| 508 |
+
"n_pred": 186
|
| 509 |
+
},
|
| 510 |
+
"nostrils": {
|
| 511 |
+
"detection": {
|
| 512 |
+
"tp": 347,
|
| 513 |
+
"fp": 93,
|
| 514 |
+
"fn": 29,
|
| 515 |
+
"precision": 0.7886363636363637,
|
| 516 |
+
"recall": 0.9228723404255319,
|
| 517 |
+
"f1": 0.8504901960784313,
|
| 518 |
+
"mean_dist_matched": 0.8862671910950328
|
| 519 |
+
},
|
| 520 |
+
"type_accuracy": 0.9798270893371758,
|
| 521 |
+
"per_class": {
|
| 522 |
+
"Nostril in Nucleus": {
|
| 523 |
+
"tp": 148,
|
| 524 |
+
"fp": 34,
|
| 525 |
+
"fn": 21,
|
| 526 |
+
"precision": 0.8131868131868132,
|
| 527 |
+
"recall": 0.8757396449704142,
|
| 528 |
+
"f1": 0.8433048433048432,
|
| 529 |
+
"gt_count": 169,
|
| 530 |
+
"pred_count": 182
|
| 531 |
+
},
|
| 532 |
+
"Nostril External of Nucleus": {
|
| 533 |
+
"tp": 192,
|
| 534 |
+
"fp": 66,
|
| 535 |
+
"fn": 15,
|
| 536 |
+
"precision": 0.7441860465116279,
|
| 537 |
+
"recall": 0.927536231884058,
|
| 538 |
+
"f1": 0.8258064516129032,
|
| 539 |
+
"gt_count": 207,
|
| 540 |
+
"pred_count": 258
|
| 541 |
+
}
|
| 542 |
+
},
|
| 543 |
+
"n_gt": 376,
|
| 544 |
+
"n_pred": 440
|
| 545 |
+
},
|
| 546 |
+
"counts": {
|
| 547 |
+
"Cell, Antibody +": {
|
| 548 |
+
"gt": 61,
|
| 549 |
+
"pred": 73,
|
| 550 |
+
"abs_err": 12,
|
| 551 |
+
"rel_err": 0.19672131147540983
|
| 552 |
+
},
|
| 553 |
+
"Cell, Antibody -": {
|
| 554 |
+
"gt": 1,
|
| 555 |
+
"pred": 1,
|
| 556 |
+
"abs_err": 0,
|
| 557 |
+
"rel_err": 0.0
|
| 558 |
+
},
|
| 559 |
+
"Cell, Nostril, Antibody +": {
|
| 560 |
+
"gt": 122,
|
| 561 |
+
"pred": 112,
|
| 562 |
+
"abs_err": 10,
|
| 563 |
+
"rel_err": 0.08196721311475409
|
| 564 |
+
},
|
| 565 |
+
"Cell, Nostril, Antibody -": {
|
| 566 |
+
"gt": 0,
|
| 567 |
+
"pred": 0,
|
| 568 |
+
"abs_err": 0,
|
| 569 |
+
"rel_err": 0.0
|
| 570 |
+
},
|
| 571 |
+
"Nostril in Nucleus": {
|
| 572 |
+
"gt": 169,
|
| 573 |
+
"pred": 182,
|
| 574 |
+
"abs_err": 13,
|
| 575 |
+
"rel_err": 0.07692307692307693
|
| 576 |
+
},
|
| 577 |
+
"Nostril External of Nucleus": {
|
| 578 |
+
"gt": 207,
|
| 579 |
+
"pred": 258,
|
| 580 |
+
"abs_err": 51,
|
| 581 |
+
"rel_err": 0.2463768115942029
|
| 582 |
+
}
|
| 583 |
+
},
|
| 584 |
+
"fp_analysis": null,
|
| 585 |
+
"pairs": null,
|
| 586 |
+
"summary": {
|
| 587 |
+
"cell_det_f1": 0.8864864864864864,
|
| 588 |
+
"cell_det_precision": 0.8817204301075269,
|
| 589 |
+
"cell_det_recall": 0.8913043478260869,
|
| 590 |
+
"cell_mean_iou": 0.800839882168248,
|
| 591 |
+
"cell_det_f1_iou30": 0.9351351351351351,
|
| 592 |
+
"cell_det_f1_centroid": 0.9297297297297297,
|
| 593 |
+
"cell_attr_antibody": 1.0,
|
| 594 |
+
"cell_attr_nostril": 0.9573170731707317,
|
| 595 |
+
"cell_attr_both": 0.9573170731707317,
|
| 596 |
+
"nos_det_f1": 0.8504901960784313,
|
| 597 |
+
"nos_det_precision": 0.7886363636363637,
|
| 598 |
+
"nos_det_recall": 0.9228723404255319,
|
| 599 |
+
"nos_type_acc": 0.9798270893371758,
|
| 600 |
+
"f1[Cell, Antibody +]": 0.8059701492537313,
|
| 601 |
+
"f1[Cell, Antibody -]": 1.0,
|
| 602 |
+
"f1[Cell, Nostril, Antibody +]": 0.8717948717948717,
|
| 603 |
+
"f1[Cell, Nostril, Antibody -]": 0.0,
|
| 604 |
+
"f1[Nostril in Nucleus]": 0.8433048433048432,
|
| 605 |
+
"f1[Nostril External of Nucleus]": 0.8258064516129032
|
| 606 |
+
}
|
| 607 |
+
},
|
| 608 |
+
"C112": {
|
| 609 |
+
"cells": {
|
| 610 |
+
"detection": {
|
| 611 |
+
"tp": 124,
|
| 612 |
+
"fp": 26,
|
| 613 |
+
"fn": 22,
|
| 614 |
+
"precision": 0.8266666666666667,
|
| 615 |
+
"recall": 0.8493150684931506,
|
| 616 |
+
"f1": 0.8378378378378378,
|
| 617 |
+
"mean_iou_matched": 0.7526341825198912,
|
| 618 |
+
"iou30": {
|
| 619 |
+
"tp": 134,
|
| 620 |
+
"fp": 16,
|
| 621 |
+
"fn": 12,
|
| 622 |
+
"precision": 0.8933333333333333,
|
| 623 |
+
"recall": 0.9178082191780822,
|
| 624 |
+
"f1": 0.9054054054054055
|
| 625 |
+
},
|
| 626 |
+
"centroid": {
|
| 627 |
+
"tp": 133,
|
| 628 |
+
"fp": 17,
|
| 629 |
+
"fn": 13,
|
| 630 |
+
"precision": 0.8866666666666667,
|
| 631 |
+
"recall": 0.910958904109589,
|
| 632 |
+
"f1": 0.8986486486486487
|
| 633 |
+
}
|
| 634 |
+
},
|
| 635 |
+
"attribute_accuracy": {
|
| 636 |
+
"antibody": 0.9919354838709677,
|
| 637 |
+
"nostril": 1.0,
|
| 638 |
+
"both": 0.9919354838709677,
|
| 639 |
+
"n_matched": 124
|
| 640 |
+
},
|
| 641 |
+
"per_class": {
|
| 642 |
+
"Cell, Antibody +": {
|
| 643 |
+
"tp": 104,
|
| 644 |
+
"fp": 22,
|
| 645 |
+
"fn": 20,
|
| 646 |
+
"precision": 0.8253968253968254,
|
| 647 |
+
"recall": 0.8387096774193549,
|
| 648 |
+
"f1": 0.832,
|
| 649 |
+
"gt_count": 124,
|
| 650 |
+
"pred_count": 126
|
| 651 |
+
},
|
| 652 |
+
"Cell, Antibody -": {
|
| 653 |
+
"tp": 4,
|
| 654 |
+
"fp": 1,
|
| 655 |
+
"fn": 1,
|
| 656 |
+
"precision": 0.8,
|
| 657 |
+
"recall": 0.8,
|
| 658 |
+
"f1": 0.8000000000000002,
|
| 659 |
+
"gt_count": 5,
|
| 660 |
+
"pred_count": 5
|
| 661 |
+
},
|
| 662 |
+
"Cell, Nostril, Antibody +": {
|
| 663 |
+
"tp": 15,
|
| 664 |
+
"fp": 4,
|
| 665 |
+
"fn": 2,
|
| 666 |
+
"precision": 0.7894736842105263,
|
| 667 |
+
"recall": 0.8823529411764706,
|
| 668 |
+
"f1": 0.8333333333333333,
|
| 669 |
+
"gt_count": 17,
|
| 670 |
+
"pred_count": 19
|
| 671 |
+
},
|
| 672 |
+
"Cell, Nostril, Antibody -": {
|
| 673 |
+
"tp": 0,
|
| 674 |
+
"fp": 0,
|
| 675 |
+
"fn": 0,
|
| 676 |
+
"precision": 0.0,
|
| 677 |
+
"recall": 0.0,
|
| 678 |
+
"f1": 0.0,
|
| 679 |
+
"gt_count": 0,
|
| 680 |
+
"pred_count": 0
|
| 681 |
+
}
|
| 682 |
+
},
|
| 683 |
+
"confusion": {
|
| 684 |
+
"Cell, Antibody +": {
|
| 685 |
+
"Cell, Antibody +": 104,
|
| 686 |
+
"Cell, Antibody -": 0,
|
| 687 |
+
"Cell, Nostril, Antibody +": 0,
|
| 688 |
+
"Cell, Nostril, Antibody -": 0
|
| 689 |
+
},
|
| 690 |
+
"Cell, Antibody -": {
|
| 691 |
+
"Cell, Antibody +": 1,
|
| 692 |
+
"Cell, Antibody -": 4,
|
| 693 |
+
"Cell, Nostril, Antibody +": 0,
|
| 694 |
+
"Cell, Nostril, Antibody -": 0
|
| 695 |
+
},
|
| 696 |
+
"Cell, Nostril, Antibody +": {
|
| 697 |
+
"Cell, Antibody +": 0,
|
| 698 |
+
"Cell, Antibody -": 0,
|
| 699 |
+
"Cell, Nostril, Antibody +": 15,
|
| 700 |
+
"Cell, Nostril, Antibody -": 0
|
| 701 |
+
},
|
| 702 |
+
"Cell, Nostril, Antibody -": {
|
| 703 |
+
"Cell, Antibody +": 0,
|
| 704 |
+
"Cell, Antibody -": 0,
|
| 705 |
+
"Cell, Nostril, Antibody +": 0,
|
| 706 |
+
"Cell, Nostril, Antibody -": 0
|
| 707 |
+
}
|
| 708 |
+
},
|
| 709 |
+
"n_gt": 146,
|
| 710 |
+
"n_pred": 150
|
| 711 |
+
},
|
| 712 |
+
"nostrils": {
|
| 713 |
+
"detection": {
|
| 714 |
+
"tp": 25,
|
| 715 |
+
"fp": 8,
|
| 716 |
+
"fn": 0,
|
| 717 |
+
"precision": 0.7575757575757576,
|
| 718 |
+
"recall": 1.0,
|
| 719 |
+
"f1": 0.8620689655172413,
|
| 720 |
+
"mean_dist_matched": 0.934525036066851
|
| 721 |
+
},
|
| 722 |
+
"type_accuracy": 1.0,
|
| 723 |
+
"per_class": {
|
| 724 |
+
"Nostril in Nucleus": {
|
| 725 |
+
"tp": 23,
|
| 726 |
+
"fp": 2,
|
| 727 |
+
"fn": 0,
|
| 728 |
+
"precision": 0.92,
|
| 729 |
+
"recall": 1.0,
|
| 730 |
+
"f1": 0.9583333333333334,
|
| 731 |
+
"gt_count": 23,
|
| 732 |
+
"pred_count": 25
|
| 733 |
+
},
|
| 734 |
+
"Nostril External of Nucleus": {
|
| 735 |
+
"tp": 2,
|
| 736 |
+
"fp": 6,
|
| 737 |
+
"fn": 0,
|
| 738 |
+
"precision": 0.25,
|
| 739 |
+
"recall": 1.0,
|
| 740 |
+
"f1": 0.4,
|
| 741 |
+
"gt_count": 2,
|
| 742 |
+
"pred_count": 8
|
| 743 |
+
}
|
| 744 |
+
},
|
| 745 |
+
"n_gt": 25,
|
| 746 |
+
"n_pred": 33
|
| 747 |
+
},
|
| 748 |
+
"counts": {
|
| 749 |
+
"Cell, Antibody +": {
|
| 750 |
+
"gt": 124,
|
| 751 |
+
"pred": 126,
|
| 752 |
+
"abs_err": 2,
|
| 753 |
+
"rel_err": 0.016129032258064516
|
| 754 |
+
},
|
| 755 |
+
"Cell, Antibody -": {
|
| 756 |
+
"gt": 5,
|
| 757 |
+
"pred": 5,
|
| 758 |
+
"abs_err": 0,
|
| 759 |
+
"rel_err": 0.0
|
| 760 |
+
},
|
| 761 |
+
"Cell, Nostril, Antibody +": {
|
| 762 |
+
"gt": 17,
|
| 763 |
+
"pred": 19,
|
| 764 |
+
"abs_err": 2,
|
| 765 |
+
"rel_err": 0.11764705882352941
|
| 766 |
+
},
|
| 767 |
+
"Cell, Nostril, Antibody -": {
|
| 768 |
+
"gt": 0,
|
| 769 |
+
"pred": 0,
|
| 770 |
+
"abs_err": 0,
|
| 771 |
+
"rel_err": 0.0
|
| 772 |
+
},
|
| 773 |
+
"Nostril in Nucleus": {
|
| 774 |
+
"gt": 23,
|
| 775 |
+
"pred": 25,
|
| 776 |
+
"abs_err": 2,
|
| 777 |
+
"rel_err": 0.08695652173913043
|
| 778 |
+
},
|
| 779 |
+
"Nostril External of Nucleus": {
|
| 780 |
+
"gt": 2,
|
| 781 |
+
"pred": 8,
|
| 782 |
+
"abs_err": 6,
|
| 783 |
+
"rel_err": 3.0
|
| 784 |
+
}
|
| 785 |
+
},
|
| 786 |
+
"fp_analysis": null,
|
| 787 |
+
"pairs": null,
|
| 788 |
+
"summary": {
|
| 789 |
+
"cell_det_f1": 0.8378378378378378,
|
| 790 |
+
"cell_det_precision": 0.8266666666666667,
|
| 791 |
+
"cell_det_recall": 0.8493150684931506,
|
| 792 |
+
"cell_mean_iou": 0.7526341825198912,
|
| 793 |
+
"cell_det_f1_iou30": 0.9054054054054055,
|
| 794 |
+
"cell_det_f1_centroid": 0.8986486486486487,
|
| 795 |
+
"cell_attr_antibody": 0.9919354838709677,
|
| 796 |
+
"cell_attr_nostril": 1.0,
|
| 797 |
+
"cell_attr_both": 0.9919354838709677,
|
| 798 |
+
"nos_det_f1": 0.8620689655172413,
|
| 799 |
+
"nos_det_precision": 0.7575757575757576,
|
| 800 |
+
"nos_det_recall": 1.0,
|
| 801 |
+
"nos_type_acc": 1.0,
|
| 802 |
+
"f1[Cell, Antibody +]": 0.832,
|
| 803 |
+
"f1[Cell, Antibody -]": 0.8000000000000002,
|
| 804 |
+
"f1[Cell, Nostril, Antibody +]": 0.8333333333333333,
|
| 805 |
+
"f1[Cell, Nostril, Antibody -]": 0.0,
|
| 806 |
+
"f1[Nostril in Nucleus]": 0.9583333333333334,
|
| 807 |
+
"f1[Nostril External of Nucleus]": 0.4
|
| 808 |
+
}
|
| 809 |
+
},
|
| 810 |
+
"F100": {
|
| 811 |
+
"cells": {
|
| 812 |
+
"detection": {
|
| 813 |
+
"tp": 160,
|
| 814 |
+
"fp": 21,
|
| 815 |
+
"fn": 26,
|
| 816 |
+
"precision": 0.8839779005524862,
|
| 817 |
+
"recall": 0.8602150537634409,
|
| 818 |
+
"f1": 0.871934604904632,
|
| 819 |
+
"mean_iou_matched": 0.8049454720324757,
|
| 820 |
+
"iou30": {
|
| 821 |
+
"tp": 169,
|
| 822 |
+
"fp": 12,
|
| 823 |
+
"fn": 17,
|
| 824 |
+
"precision": 0.9337016574585635,
|
| 825 |
+
"recall": 0.9086021505376344,
|
| 826 |
+
"f1": 0.9209809264305177
|
| 827 |
+
},
|
| 828 |
+
"centroid": {
|
| 829 |
+
"tp": 168,
|
| 830 |
+
"fp": 13,
|
| 831 |
+
"fn": 18,
|
| 832 |
+
"precision": 0.9281767955801105,
|
| 833 |
+
"recall": 0.9032258064516129,
|
| 834 |
+
"f1": 0.9155313351498637
|
| 835 |
+
}
|
| 836 |
+
},
|
| 837 |
+
"attribute_accuracy": {
|
| 838 |
+
"antibody": 0.98125,
|
| 839 |
+
"nostril": 0.98125,
|
| 840 |
+
"both": 0.9625,
|
| 841 |
+
"n_matched": 160
|
| 842 |
+
},
|
| 843 |
+
"per_class": {
|
| 844 |
+
"Cell, Antibody +": {
|
| 845 |
+
"tp": 79,
|
| 846 |
+
"fp": 17,
|
| 847 |
+
"fn": 8,
|
| 848 |
+
"precision": 0.8229166666666666,
|
| 849 |
+
"recall": 0.9080459770114943,
|
| 850 |
+
"f1": 0.8633879781420765,
|
| 851 |
+
"gt_count": 87,
|
| 852 |
+
"pred_count": 96
|
| 853 |
+
},
|
| 854 |
+
"Cell, Antibody -": {
|
| 855 |
+
"tp": 4,
|
| 856 |
+
"fp": 2,
|
| 857 |
+
"fn": 2,
|
| 858 |
+
"precision": 0.6666666666666666,
|
| 859 |
+
"recall": 0.6666666666666666,
|
| 860 |
+
"f1": 0.6666666666666666,
|
| 861 |
+
"gt_count": 6,
|
| 862 |
+
"pred_count": 6
|
| 863 |
+
},
|
| 864 |
+
"Cell, Nostril, Antibody +": {
|
| 865 |
+
"tp": 71,
|
| 866 |
+
"fp": 8,
|
| 867 |
+
"fn": 22,
|
| 868 |
+
"precision": 0.8987341772151899,
|
| 869 |
+
"recall": 0.7634408602150538,
|
| 870 |
+
"f1": 0.8255813953488371,
|
| 871 |
+
"gt_count": 93,
|
| 872 |
+
"pred_count": 79
|
| 873 |
+
},
|
| 874 |
+
"Cell, Nostril, Antibody -": {
|
| 875 |
+
"tp": 0,
|
| 876 |
+
"fp": 0,
|
| 877 |
+
"fn": 0,
|
| 878 |
+
"precision": 0.0,
|
| 879 |
+
"recall": 0.0,
|
| 880 |
+
"f1": 0.0,
|
| 881 |
+
"gt_count": 0,
|
| 882 |
+
"pred_count": 0
|
| 883 |
+
}
|
| 884 |
+
},
|
| 885 |
+
"confusion": {
|
| 886 |
+
"Cell, Antibody +": {
|
| 887 |
+
"Cell, Antibody +": 79,
|
| 888 |
+
"Cell, Antibody -": 1,
|
| 889 |
+
"Cell, Nostril, Antibody +": 0,
|
| 890 |
+
"Cell, Nostril, Antibody -": 0
|
| 891 |
+
},
|
| 892 |
+
"Cell, Antibody -": {
|
| 893 |
+
"Cell, Antibody +": 2,
|
| 894 |
+
"Cell, Antibody -": 4,
|
| 895 |
+
"Cell, Nostril, Antibody +": 0,
|
| 896 |
+
"Cell, Nostril, Antibody -": 0
|
| 897 |
+
},
|
| 898 |
+
"Cell, Nostril, Antibody +": {
|
| 899 |
+
"Cell, Antibody +": 3,
|
| 900 |
+
"Cell, Antibody -": 0,
|
| 901 |
+
"Cell, Nostril, Antibody +": 71,
|
| 902 |
+
"Cell, Nostril, Antibody -": 0
|
| 903 |
+
},
|
| 904 |
+
"Cell, Nostril, Antibody -": {
|
| 905 |
+
"Cell, Antibody +": 0,
|
| 906 |
+
"Cell, Antibody -": 0,
|
| 907 |
+
"Cell, Nostril, Antibody +": 0,
|
| 908 |
+
"Cell, Nostril, Antibody -": 0
|
| 909 |
+
}
|
| 910 |
+
},
|
| 911 |
+
"n_gt": 186,
|
| 912 |
+
"n_pred": 181
|
| 913 |
+
},
|
| 914 |
+
"nostrils": {
|
| 915 |
+
"detection": {
|
| 916 |
+
"tp": 142,
|
| 917 |
+
"fp": 9,
|
| 918 |
+
"fn": 20,
|
| 919 |
+
"precision": 0.9403973509933775,
|
| 920 |
+
"recall": 0.8765432098765432,
|
| 921 |
+
"f1": 0.9073482428115016,
|
| 922 |
+
"mean_dist_matched": 1.2296582069626907
|
| 923 |
+
},
|
| 924 |
+
"type_accuracy": 0.9929577464788732,
|
| 925 |
+
"per_class": {
|
| 926 |
+
"Nostril in Nucleus": {
|
| 927 |
+
"tp": 102,
|
| 928 |
+
"fp": 8,
|
| 929 |
+
"fn": 16,
|
| 930 |
+
"precision": 0.9272727272727272,
|
| 931 |
+
"recall": 0.864406779661017,
|
| 932 |
+
"f1": 0.8947368421052632,
|
| 933 |
+
"gt_count": 118,
|
| 934 |
+
"pred_count": 110
|
| 935 |
+
},
|
| 936 |
+
"Nostril External of Nucleus": {
|
| 937 |
+
"tp": 39,
|
| 938 |
+
"fp": 2,
|
| 939 |
+
"fn": 5,
|
| 940 |
+
"precision": 0.9512195121951219,
|
| 941 |
+
"recall": 0.8863636363636364,
|
| 942 |
+
"f1": 0.9176470588235294,
|
| 943 |
+
"gt_count": 44,
|
| 944 |
+
"pred_count": 41
|
| 945 |
+
}
|
| 946 |
+
},
|
| 947 |
+
"n_gt": 162,
|
| 948 |
+
"n_pred": 151
|
| 949 |
+
},
|
| 950 |
+
"counts": {
|
| 951 |
+
"Cell, Antibody +": {
|
| 952 |
+
"gt": 87,
|
| 953 |
+
"pred": 96,
|
| 954 |
+
"abs_err": 9,
|
| 955 |
+
"rel_err": 0.10344827586206896
|
| 956 |
+
},
|
| 957 |
+
"Cell, Antibody -": {
|
| 958 |
+
"gt": 6,
|
| 959 |
+
"pred": 6,
|
| 960 |
+
"abs_err": 0,
|
| 961 |
+
"rel_err": 0.0
|
| 962 |
+
},
|
| 963 |
+
"Cell, Nostril, Antibody +": {
|
| 964 |
+
"gt": 93,
|
| 965 |
+
"pred": 79,
|
| 966 |
+
"abs_err": 14,
|
| 967 |
+
"rel_err": 0.15053763440860216
|
| 968 |
+
},
|
| 969 |
+
"Cell, Nostril, Antibody -": {
|
| 970 |
+
"gt": 0,
|
| 971 |
+
"pred": 0,
|
| 972 |
+
"abs_err": 0,
|
| 973 |
+
"rel_err": 0.0
|
| 974 |
+
},
|
| 975 |
+
"Nostril in Nucleus": {
|
| 976 |
+
"gt": 118,
|
| 977 |
+
"pred": 110,
|
| 978 |
+
"abs_err": 8,
|
| 979 |
+
"rel_err": 0.06779661016949153
|
| 980 |
+
},
|
| 981 |
+
"Nostril External of Nucleus": {
|
| 982 |
+
"gt": 44,
|
| 983 |
+
"pred": 41,
|
| 984 |
+
"abs_err": 3,
|
| 985 |
+
"rel_err": 0.06818181818181818
|
| 986 |
+
}
|
| 987 |
+
},
|
| 988 |
+
"fp_analysis": null,
|
| 989 |
+
"pairs": null,
|
| 990 |
+
"summary": {
|
| 991 |
+
"cell_det_f1": 0.871934604904632,
|
| 992 |
+
"cell_det_precision": 0.8839779005524862,
|
| 993 |
+
"cell_det_recall": 0.8602150537634409,
|
| 994 |
+
"cell_mean_iou": 0.8049454720324757,
|
| 995 |
+
"cell_det_f1_iou30": 0.9209809264305177,
|
| 996 |
+
"cell_det_f1_centroid": 0.9155313351498637,
|
| 997 |
+
"cell_attr_antibody": 0.98125,
|
| 998 |
+
"cell_attr_nostril": 0.98125,
|
| 999 |
+
"cell_attr_both": 0.9625,
|
| 1000 |
+
"nos_det_f1": 0.9073482428115016,
|
| 1001 |
+
"nos_det_precision": 0.9403973509933775,
|
| 1002 |
+
"nos_det_recall": 0.8765432098765432,
|
| 1003 |
+
"nos_type_acc": 0.9929577464788732,
|
| 1004 |
+
"f1[Cell, Antibody +]": 0.8633879781420765,
|
| 1005 |
+
"f1[Cell, Antibody -]": 0.6666666666666666,
|
| 1006 |
+
"f1[Cell, Nostril, Antibody +]": 0.8255813953488371,
|
| 1007 |
+
"f1[Cell, Nostril, Antibody -]": 0.0,
|
| 1008 |
+
"f1[Nostril in Nucleus]": 0.8947368421052632,
|
| 1009 |
+
"f1[Nostril External of Nucleus]": 0.9176470588235294
|
| 1010 |
+
}
|
| 1011 |
+
},
|
| 1012 |
+
"F110": {
|
| 1013 |
+
"cells": {
|
| 1014 |
+
"detection": {
|
| 1015 |
+
"tp": 116,
|
| 1016 |
+
"fp": 9,
|
| 1017 |
+
"fn": 12,
|
| 1018 |
+
"precision": 0.928,
|
| 1019 |
+
"recall": 0.90625,
|
| 1020 |
+
"f1": 0.9169960474308302,
|
| 1021 |
+
"mean_iou_matched": 0.8161455695675424,
|
| 1022 |
+
"iou30": {
|
| 1023 |
+
"tp": 120,
|
| 1024 |
+
"fp": 5,
|
| 1025 |
+
"fn": 8,
|
| 1026 |
+
"precision": 0.96,
|
| 1027 |
+
"recall": 0.9375,
|
| 1028 |
+
"f1": 0.9486166007905138
|
| 1029 |
+
},
|
| 1030 |
+
"centroid": {
|
| 1031 |
+
"tp": 121,
|
| 1032 |
+
"fp": 4,
|
| 1033 |
+
"fn": 7,
|
| 1034 |
+
"precision": 0.968,
|
| 1035 |
+
"recall": 0.9453125,
|
| 1036 |
+
"f1": 0.9565217391304348
|
| 1037 |
+
}
|
| 1038 |
+
},
|
| 1039 |
+
"attribute_accuracy": {
|
| 1040 |
+
"antibody": 0.9827586206896551,
|
| 1041 |
+
"nostril": 0.9655172413793104,
|
| 1042 |
+
"both": 0.9482758620689655,
|
| 1043 |
+
"n_matched": 116
|
| 1044 |
+
},
|
| 1045 |
+
"per_class": {
|
| 1046 |
+
"Cell, Antibody +": {
|
| 1047 |
+
"tp": 82,
|
| 1048 |
+
"fp": 10,
|
| 1049 |
+
"fn": 5,
|
| 1050 |
+
"precision": 0.8913043478260869,
|
| 1051 |
+
"recall": 0.9425287356321839,
|
| 1052 |
+
"f1": 0.9162011173184358,
|
| 1053 |
+
"gt_count": 87,
|
| 1054 |
+
"pred_count": 92
|
| 1055 |
+
},
|
| 1056 |
+
"Cell, Antibody -": {
|
| 1057 |
+
"tp": 5,
|
| 1058 |
+
"fp": 0,
|
| 1059 |
+
"fn": 2,
|
| 1060 |
+
"precision": 1.0,
|
| 1061 |
+
"recall": 0.7142857142857143,
|
| 1062 |
+
"f1": 0.8333333333333333,
|
| 1063 |
+
"gt_count": 7,
|
| 1064 |
+
"pred_count": 5
|
| 1065 |
+
},
|
| 1066 |
+
"Cell, Nostril, Antibody +": {
|
| 1067 |
+
"tp": 23,
|
| 1068 |
+
"fp": 5,
|
| 1069 |
+
"fn": 10,
|
| 1070 |
+
"precision": 0.8214285714285714,
|
| 1071 |
+
"recall": 0.696969696969697,
|
| 1072 |
+
"f1": 0.7540983606557378,
|
| 1073 |
+
"gt_count": 33,
|
| 1074 |
+
"pred_count": 28
|
| 1075 |
+
},
|
| 1076 |
+
"Cell, Nostril, Antibody -": {
|
| 1077 |
+
"tp": 0,
|
| 1078 |
+
"fp": 0,
|
| 1079 |
+
"fn": 1,
|
| 1080 |
+
"precision": 0.0,
|
| 1081 |
+
"recall": 0.0,
|
| 1082 |
+
"f1": 0.0,
|
| 1083 |
+
"gt_count": 1,
|
| 1084 |
+
"pred_count": 0
|
| 1085 |
+
}
|
| 1086 |
+
},
|
| 1087 |
+
"confusion": {
|
| 1088 |
+
"Cell, Antibody +": {
|
| 1089 |
+
"Cell, Antibody +": 82,
|
| 1090 |
+
"Cell, Antibody -": 0,
|
| 1091 |
+
"Cell, Nostril, Antibody +": 0,
|
| 1092 |
+
"Cell, Nostril, Antibody -": 0
|
| 1093 |
+
},
|
| 1094 |
+
"Cell, Antibody -": {
|
| 1095 |
+
"Cell, Antibody +": 2,
|
| 1096 |
+
"Cell, Antibody -": 5,
|
| 1097 |
+
"Cell, Nostril, Antibody +": 0,
|
| 1098 |
+
"Cell, Nostril, Antibody -": 0
|
| 1099 |
+
},
|
| 1100 |
+
"Cell, Nostril, Antibody +": {
|
| 1101 |
+
"Cell, Antibody +": 4,
|
| 1102 |
+
"Cell, Antibody -": 0,
|
| 1103 |
+
"Cell, Nostril, Antibody +": 23,
|
| 1104 |
+
"Cell, Nostril, Antibody -": 0
|
| 1105 |
+
},
|
| 1106 |
+
"Cell, Nostril, Antibody -": {
|
| 1107 |
+
"Cell, Antibody +": 0,
|
| 1108 |
+
"Cell, Antibody -": 0,
|
| 1109 |
+
"Cell, Nostril, Antibody +": 0,
|
| 1110 |
+
"Cell, Nostril, Antibody -": 0
|
| 1111 |
+
}
|
| 1112 |
+
},
|
| 1113 |
+
"n_gt": 128,
|
| 1114 |
+
"n_pred": 125
|
| 1115 |
+
},
|
| 1116 |
+
"nostrils": {
|
| 1117 |
+
"detection": {
|
| 1118 |
+
"tp": 157,
|
| 1119 |
+
"fp": 9,
|
| 1120 |
+
"fn": 37,
|
| 1121 |
+
"precision": 0.9457831325301205,
|
| 1122 |
+
"recall": 0.8092783505154639,
|
| 1123 |
+
"f1": 0.8722222222222222,
|
| 1124 |
+
"mean_dist_matched": 0.7899268230101212
|
| 1125 |
+
},
|
| 1126 |
+
"type_accuracy": 0.9936305732484076,
|
| 1127 |
+
"per_class": {
|
| 1128 |
+
"Nostril in Nucleus": {
|
| 1129 |
+
"tp": 48,
|
| 1130 |
+
"fp": 4,
|
| 1131 |
+
"fn": 9,
|
| 1132 |
+
"precision": 0.9230769230769231,
|
| 1133 |
+
"recall": 0.8421052631578947,
|
| 1134 |
+
"f1": 0.8807339449541285,
|
| 1135 |
+
"gt_count": 57,
|
| 1136 |
+
"pred_count": 52
|
| 1137 |
+
},
|
| 1138 |
+
"Nostril External of Nucleus": {
|
| 1139 |
+
"tp": 108,
|
| 1140 |
+
"fp": 6,
|
| 1141 |
+
"fn": 29,
|
| 1142 |
+
"precision": 0.9473684210526315,
|
| 1143 |
+
"recall": 0.7883211678832117,
|
| 1144 |
+
"f1": 0.8605577689243028,
|
| 1145 |
+
"gt_count": 137,
|
| 1146 |
+
"pred_count": 114
|
| 1147 |
+
}
|
| 1148 |
+
},
|
| 1149 |
+
"n_gt": 194,
|
| 1150 |
+
"n_pred": 166
|
| 1151 |
+
},
|
| 1152 |
+
"counts": {
|
| 1153 |
+
"Cell, Antibody +": {
|
| 1154 |
+
"gt": 87,
|
| 1155 |
+
"pred": 92,
|
| 1156 |
+
"abs_err": 5,
|
| 1157 |
+
"rel_err": 0.05747126436781609
|
| 1158 |
+
},
|
| 1159 |
+
"Cell, Antibody -": {
|
| 1160 |
+
"gt": 7,
|
| 1161 |
+
"pred": 5,
|
| 1162 |
+
"abs_err": 2,
|
| 1163 |
+
"rel_err": 0.2857142857142857
|
| 1164 |
+
},
|
| 1165 |
+
"Cell, Nostril, Antibody +": {
|
| 1166 |
+
"gt": 33,
|
| 1167 |
+
"pred": 28,
|
| 1168 |
+
"abs_err": 5,
|
| 1169 |
+
"rel_err": 0.15151515151515152
|
| 1170 |
+
},
|
| 1171 |
+
"Cell, Nostril, Antibody -": {
|
| 1172 |
+
"gt": 1,
|
| 1173 |
+
"pred": 0,
|
| 1174 |
+
"abs_err": 1,
|
| 1175 |
+
"rel_err": 1.0
|
| 1176 |
+
},
|
| 1177 |
+
"Nostril in Nucleus": {
|
| 1178 |
+
"gt": 57,
|
| 1179 |
+
"pred": 52,
|
| 1180 |
+
"abs_err": 5,
|
| 1181 |
+
"rel_err": 0.08771929824561403
|
| 1182 |
+
},
|
| 1183 |
+
"Nostril External of Nucleus": {
|
| 1184 |
+
"gt": 137,
|
| 1185 |
+
"pred": 114,
|
| 1186 |
+
"abs_err": 23,
|
| 1187 |
+
"rel_err": 0.1678832116788321
|
| 1188 |
+
}
|
| 1189 |
+
},
|
| 1190 |
+
"fp_analysis": null,
|
| 1191 |
+
"pairs": null,
|
| 1192 |
+
"summary": {
|
| 1193 |
+
"cell_det_f1": 0.9169960474308302,
|
| 1194 |
+
"cell_det_precision": 0.928,
|
| 1195 |
+
"cell_det_recall": 0.90625,
|
| 1196 |
+
"cell_mean_iou": 0.8161455695675424,
|
| 1197 |
+
"cell_det_f1_iou30": 0.9486166007905138,
|
| 1198 |
+
"cell_det_f1_centroid": 0.9565217391304348,
|
| 1199 |
+
"cell_attr_antibody": 0.9827586206896551,
|
| 1200 |
+
"cell_attr_nostril": 0.9655172413793104,
|
| 1201 |
+
"cell_attr_both": 0.9482758620689655,
|
| 1202 |
+
"nos_det_f1": 0.8722222222222222,
|
| 1203 |
+
"nos_det_precision": 0.9457831325301205,
|
| 1204 |
+
"nos_det_recall": 0.8092783505154639,
|
| 1205 |
+
"nos_type_acc": 0.9936305732484076,
|
| 1206 |
+
"f1[Cell, Antibody +]": 0.9162011173184358,
|
| 1207 |
+
"f1[Cell, Antibody -]": 0.8333333333333333,
|
| 1208 |
+
"f1[Cell, Nostril, Antibody +]": 0.7540983606557378,
|
| 1209 |
+
"f1[Cell, Nostril, Antibody -]": 0.0,
|
| 1210 |
+
"f1[Nostril in Nucleus]": 0.8807339449541285,
|
| 1211 |
+
"f1[Nostril External of Nucleus]": 0.8605577689243028
|
| 1212 |
+
}
|
| 1213 |
+
},
|
| 1214 |
+
"F113": {
|
| 1215 |
+
"cells": {
|
| 1216 |
+
"detection": {
|
| 1217 |
+
"tp": 131,
|
| 1218 |
+
"fp": 19,
|
| 1219 |
+
"fn": 32,
|
| 1220 |
+
"precision": 0.8733333333333333,
|
| 1221 |
+
"recall": 0.803680981595092,
|
| 1222 |
+
"f1": 0.8370607028753994,
|
| 1223 |
+
"mean_iou_matched": 0.7799368783421308,
|
| 1224 |
+
"iou30": {
|
| 1225 |
+
"tp": 146,
|
| 1226 |
+
"fp": 4,
|
| 1227 |
+
"fn": 17,
|
| 1228 |
+
"precision": 0.9733333333333334,
|
| 1229 |
+
"recall": 0.8957055214723927,
|
| 1230 |
+
"f1": 0.9329073482428116
|
| 1231 |
+
},
|
| 1232 |
+
"centroid": {
|
| 1233 |
+
"tp": 144,
|
| 1234 |
+
"fp": 6,
|
| 1235 |
+
"fn": 19,
|
| 1236 |
+
"precision": 0.96,
|
| 1237 |
+
"recall": 0.8834355828220859,
|
| 1238 |
+
"f1": 0.9201277955271565
|
| 1239 |
+
}
|
| 1240 |
+
},
|
| 1241 |
+
"attribute_accuracy": {
|
| 1242 |
+
"antibody": 1.0,
|
| 1243 |
+
"nostril": 0.9770992366412213,
|
| 1244 |
+
"both": 0.9770992366412213,
|
| 1245 |
+
"n_matched": 131
|
| 1246 |
+
},
|
| 1247 |
+
"per_class": {
|
| 1248 |
+
"Cell, Antibody +": {
|
| 1249 |
+
"tp": 108,
|
| 1250 |
+
"fp": 21,
|
| 1251 |
+
"fn": 30,
|
| 1252 |
+
"precision": 0.8372093023255814,
|
| 1253 |
+
"recall": 0.782608695652174,
|
| 1254 |
+
"f1": 0.8089887640449438,
|
| 1255 |
+
"gt_count": 138,
|
| 1256 |
+
"pred_count": 129
|
| 1257 |
+
},
|
| 1258 |
+
"Cell, Antibody -": {
|
| 1259 |
+
"tp": 2,
|
| 1260 |
+
"fp": 0,
|
| 1261 |
+
"fn": 1,
|
| 1262 |
+
"precision": 1.0,
|
| 1263 |
+
"recall": 0.6666666666666666,
|
| 1264 |
+
"f1": 0.8,
|
| 1265 |
+
"gt_count": 3,
|
| 1266 |
+
"pred_count": 2
|
| 1267 |
+
},
|
| 1268 |
+
"Cell, Nostril, Antibody +": {
|
| 1269 |
+
"tp": 18,
|
| 1270 |
+
"fp": 1,
|
| 1271 |
+
"fn": 4,
|
| 1272 |
+
"precision": 0.9473684210526315,
|
| 1273 |
+
"recall": 0.8181818181818182,
|
| 1274 |
+
"f1": 0.8780487804878049,
|
| 1275 |
+
"gt_count": 22,
|
| 1276 |
+
"pred_count": 19
|
| 1277 |
+
},
|
| 1278 |
+
"Cell, Nostril, Antibody -": {
|
| 1279 |
+
"tp": 0,
|
| 1280 |
+
"fp": 0,
|
| 1281 |
+
"fn": 0,
|
| 1282 |
+
"precision": 0.0,
|
| 1283 |
+
"recall": 0.0,
|
| 1284 |
+
"f1": 0.0,
|
| 1285 |
+
"gt_count": 0,
|
| 1286 |
+
"pred_count": 0
|
| 1287 |
+
}
|
| 1288 |
+
},
|
| 1289 |
+
"confusion": {
|
| 1290 |
+
"Cell, Antibody +": {
|
| 1291 |
+
"Cell, Antibody +": 108,
|
| 1292 |
+
"Cell, Antibody -": 0,
|
| 1293 |
+
"Cell, Nostril, Antibody +": 0,
|
| 1294 |
+
"Cell, Nostril, Antibody -": 0
|
| 1295 |
+
},
|
| 1296 |
+
"Cell, Antibody -": {
|
| 1297 |
+
"Cell, Antibody +": 0,
|
| 1298 |
+
"Cell, Antibody -": 2,
|
| 1299 |
+
"Cell, Nostril, Antibody +": 0,
|
| 1300 |
+
"Cell, Nostril, Antibody -": 0
|
| 1301 |
+
},
|
| 1302 |
+
"Cell, Nostril, Antibody +": {
|
| 1303 |
+
"Cell, Antibody +": 3,
|
| 1304 |
+
"Cell, Antibody -": 0,
|
| 1305 |
+
"Cell, Nostril, Antibody +": 18,
|
| 1306 |
+
"Cell, Nostril, Antibody -": 0
|
| 1307 |
+
},
|
| 1308 |
+
"Cell, Nostril, Antibody -": {
|
| 1309 |
+
"Cell, Antibody +": 0,
|
| 1310 |
+
"Cell, Antibody -": 0,
|
| 1311 |
+
"Cell, Nostril, Antibody +": 0,
|
| 1312 |
+
"Cell, Nostril, Antibody -": 0
|
| 1313 |
+
}
|
| 1314 |
+
},
|
| 1315 |
+
"n_gt": 163,
|
| 1316 |
+
"n_pred": 150
|
| 1317 |
+
},
|
| 1318 |
+
"nostrils": {
|
| 1319 |
+
"detection": {
|
| 1320 |
+
"tp": 27,
|
| 1321 |
+
"fp": 1,
|
| 1322 |
+
"fn": 7,
|
| 1323 |
+
"precision": 0.9642857142857143,
|
| 1324 |
+
"recall": 0.7941176470588235,
|
| 1325 |
+
"f1": 0.8709677419354839,
|
| 1326 |
+
"mean_dist_matched": 0.8145690279889126
|
| 1327 |
+
},
|
| 1328 |
+
"type_accuracy": 0.9259259259259259,
|
| 1329 |
+
"per_class": {
|
| 1330 |
+
"Nostril in Nucleus": {
|
| 1331 |
+
"tp": 21,
|
| 1332 |
+
"fp": 2,
|
| 1333 |
+
"fn": 5,
|
| 1334 |
+
"precision": 0.9130434782608695,
|
| 1335 |
+
"recall": 0.8076923076923077,
|
| 1336 |
+
"f1": 0.8571428571428572,
|
| 1337 |
+
"gt_count": 26,
|
| 1338 |
+
"pred_count": 23
|
| 1339 |
+
},
|
| 1340 |
+
"Nostril External of Nucleus": {
|
| 1341 |
+
"tp": 4,
|
| 1342 |
+
"fp": 1,
|
| 1343 |
+
"fn": 4,
|
| 1344 |
+
"precision": 0.8,
|
| 1345 |
+
"recall": 0.5,
|
| 1346 |
+
"f1": 0.6153846153846154,
|
| 1347 |
+
"gt_count": 8,
|
| 1348 |
+
"pred_count": 5
|
| 1349 |
+
}
|
| 1350 |
+
},
|
| 1351 |
+
"n_gt": 34,
|
| 1352 |
+
"n_pred": 28
|
| 1353 |
+
},
|
| 1354 |
+
"counts": {
|
| 1355 |
+
"Cell, Antibody +": {
|
| 1356 |
+
"gt": 138,
|
| 1357 |
+
"pred": 129,
|
| 1358 |
+
"abs_err": 9,
|
| 1359 |
+
"rel_err": 0.06521739130434782
|
| 1360 |
+
},
|
| 1361 |
+
"Cell, Antibody -": {
|
| 1362 |
+
"gt": 3,
|
| 1363 |
+
"pred": 2,
|
| 1364 |
+
"abs_err": 1,
|
| 1365 |
+
"rel_err": 0.3333333333333333
|
| 1366 |
+
},
|
| 1367 |
+
"Cell, Nostril, Antibody +": {
|
| 1368 |
+
"gt": 22,
|
| 1369 |
+
"pred": 19,
|
| 1370 |
+
"abs_err": 3,
|
| 1371 |
+
"rel_err": 0.13636363636363635
|
| 1372 |
+
},
|
| 1373 |
+
"Cell, Nostril, Antibody -": {
|
| 1374 |
+
"gt": 0,
|
| 1375 |
+
"pred": 0,
|
| 1376 |
+
"abs_err": 0,
|
| 1377 |
+
"rel_err": 0.0
|
| 1378 |
+
},
|
| 1379 |
+
"Nostril in Nucleus": {
|
| 1380 |
+
"gt": 26,
|
| 1381 |
+
"pred": 23,
|
| 1382 |
+
"abs_err": 3,
|
| 1383 |
+
"rel_err": 0.11538461538461539
|
| 1384 |
+
},
|
| 1385 |
+
"Nostril External of Nucleus": {
|
| 1386 |
+
"gt": 8,
|
| 1387 |
+
"pred": 5,
|
| 1388 |
+
"abs_err": 3,
|
| 1389 |
+
"rel_err": 0.375
|
| 1390 |
+
}
|
| 1391 |
+
},
|
| 1392 |
+
"fp_analysis": null,
|
| 1393 |
+
"pairs": null,
|
| 1394 |
+
"summary": {
|
| 1395 |
+
"cell_det_f1": 0.8370607028753994,
|
| 1396 |
+
"cell_det_precision": 0.8733333333333333,
|
| 1397 |
+
"cell_det_recall": 0.803680981595092,
|
| 1398 |
+
"cell_mean_iou": 0.7799368783421308,
|
| 1399 |
+
"cell_det_f1_iou30": 0.9329073482428116,
|
| 1400 |
+
"cell_det_f1_centroid": 0.9201277955271565,
|
| 1401 |
+
"cell_attr_antibody": 1.0,
|
| 1402 |
+
"cell_attr_nostril": 0.9770992366412213,
|
| 1403 |
+
"cell_attr_both": 0.9770992366412213,
|
| 1404 |
+
"nos_det_f1": 0.8709677419354839,
|
| 1405 |
+
"nos_det_precision": 0.9642857142857143,
|
| 1406 |
+
"nos_det_recall": 0.7941176470588235,
|
| 1407 |
+
"nos_type_acc": 0.9259259259259259,
|
| 1408 |
+
"f1[Cell, Antibody +]": 0.8089887640449438,
|
| 1409 |
+
"f1[Cell, Antibody -]": 0.8,
|
| 1410 |
+
"f1[Cell, Nostril, Antibody +]": 0.8780487804878049,
|
| 1411 |
+
"f1[Cell, Nostril, Antibody -]": 0.0,
|
| 1412 |
+
"f1[Nostril in Nucleus]": 0.8571428571428572,
|
| 1413 |
+
"f1[Nostril External of Nucleus]": 0.6153846153846154
|
| 1414 |
+
}
|
| 1415 |
+
},
|
| 1416 |
+
"F114": {
|
| 1417 |
+
"cells": {
|
| 1418 |
+
"detection": {
|
| 1419 |
+
"tp": 211,
|
| 1420 |
+
"fp": 25,
|
| 1421 |
+
"fn": 31,
|
| 1422 |
+
"precision": 0.8940677966101694,
|
| 1423 |
+
"recall": 0.871900826446281,
|
| 1424 |
+
"f1": 0.8828451882845189,
|
| 1425 |
+
"mean_iou_matched": 0.8168843572713155,
|
| 1426 |
+
"iou30": {
|
| 1427 |
+
"tp": 222,
|
| 1428 |
+
"fp": 14,
|
| 1429 |
+
"fn": 20,
|
| 1430 |
+
"precision": 0.940677966101695,
|
| 1431 |
+
"recall": 0.9173553719008265,
|
| 1432 |
+
"f1": 0.9288702928870294
|
| 1433 |
+
},
|
| 1434 |
+
"centroid": {
|
| 1435 |
+
"tp": 221,
|
| 1436 |
+
"fp": 15,
|
| 1437 |
+
"fn": 21,
|
| 1438 |
+
"precision": 0.9364406779661016,
|
| 1439 |
+
"recall": 0.9132231404958677,
|
| 1440 |
+
"f1": 0.9246861924686192
|
| 1441 |
+
}
|
| 1442 |
+
},
|
| 1443 |
+
"attribute_accuracy": {
|
| 1444 |
+
"antibody": 0.95260663507109,
|
| 1445 |
+
"nostril": 0.981042654028436,
|
| 1446 |
+
"both": 0.933649289099526,
|
| 1447 |
+
"n_matched": 211
|
| 1448 |
+
},
|
| 1449 |
+
"per_class": {
|
| 1450 |
+
"Cell, Antibody +": {
|
| 1451 |
+
"tp": 148,
|
| 1452 |
+
"fp": 28,
|
| 1453 |
+
"fn": 27,
|
| 1454 |
+
"precision": 0.8409090909090909,
|
| 1455 |
+
"recall": 0.8457142857142858,
|
| 1456 |
+
"f1": 0.8433048433048433,
|
| 1457 |
+
"gt_count": 175,
|
| 1458 |
+
"pred_count": 176
|
| 1459 |
+
},
|
| 1460 |
+
"Cell, Antibody -": {
|
| 1461 |
+
"tp": 12,
|
| 1462 |
+
"fp": 2,
|
| 1463 |
+
"fn": 9,
|
| 1464 |
+
"precision": 0.8571428571428571,
|
| 1465 |
+
"recall": 0.5714285714285714,
|
| 1466 |
+
"f1": 0.6857142857142857,
|
| 1467 |
+
"gt_count": 21,
|
| 1468 |
+
"pred_count": 14
|
| 1469 |
+
},
|
| 1470 |
+
"Cell, Nostril, Antibody +": {
|
| 1471 |
+
"tp": 37,
|
| 1472 |
+
"fp": 9,
|
| 1473 |
+
"fn": 5,
|
| 1474 |
+
"precision": 0.8043478260869565,
|
| 1475 |
+
"recall": 0.8809523809523809,
|
| 1476 |
+
"f1": 0.8409090909090908,
|
| 1477 |
+
"gt_count": 42,
|
| 1478 |
+
"pred_count": 46
|
| 1479 |
+
},
|
| 1480 |
+
"Cell, Nostril, Antibody -": {
|
| 1481 |
+
"tp": 0,
|
| 1482 |
+
"fp": 0,
|
| 1483 |
+
"fn": 4,
|
| 1484 |
+
"precision": 0.0,
|
| 1485 |
+
"recall": 0.0,
|
| 1486 |
+
"f1": 0.0,
|
| 1487 |
+
"gt_count": 4,
|
| 1488 |
+
"pred_count": 0
|
| 1489 |
+
}
|
| 1490 |
+
},
|
| 1491 |
+
"confusion": {
|
| 1492 |
+
"Cell, Antibody +": {
|
| 1493 |
+
"Cell, Antibody +": 148,
|
| 1494 |
+
"Cell, Antibody -": 1,
|
| 1495 |
+
"Cell, Nostril, Antibody +": 4,
|
| 1496 |
+
"Cell, Nostril, Antibody -": 0
|
| 1497 |
+
},
|
| 1498 |
+
"Cell, Antibody -": {
|
| 1499 |
+
"Cell, Antibody +": 9,
|
| 1500 |
+
"Cell, Antibody -": 12,
|
| 1501 |
+
"Cell, Nostril, Antibody +": 0,
|
| 1502 |
+
"Cell, Nostril, Antibody -": 0
|
| 1503 |
+
},
|
| 1504 |
+
"Cell, Nostril, Antibody +": {
|
| 1505 |
+
"Cell, Antibody +": 0,
|
| 1506 |
+
"Cell, Antibody -": 0,
|
| 1507 |
+
"Cell, Nostril, Antibody +": 37,
|
| 1508 |
+
"Cell, Nostril, Antibody -": 0
|
| 1509 |
+
},
|
| 1510 |
+
"Cell, Nostril, Antibody -": {
|
| 1511 |
+
"Cell, Antibody +": 0,
|
| 1512 |
+
"Cell, Antibody -": 0,
|
| 1513 |
+
"Cell, Nostril, Antibody +": 0,
|
| 1514 |
+
"Cell, Nostril, Antibody -": 0
|
| 1515 |
+
}
|
| 1516 |
+
},
|
| 1517 |
+
"n_gt": 242,
|
| 1518 |
+
"n_pred": 236
|
| 1519 |
+
},
|
| 1520 |
+
"nostrils": {
|
| 1521 |
+
"detection": {
|
| 1522 |
+
"tp": 81,
|
| 1523 |
+
"fp": 24,
|
| 1524 |
+
"fn": 9,
|
| 1525 |
+
"precision": 0.7714285714285715,
|
| 1526 |
+
"recall": 0.9,
|
| 1527 |
+
"f1": 0.8307692307692307,
|
| 1528 |
+
"mean_dist_matched": 0.9417483459046142
|
| 1529 |
+
},
|
| 1530 |
+
"type_accuracy": 1.0,
|
| 1531 |
+
"per_class": {
|
| 1532 |
+
"Nostril in Nucleus": {
|
| 1533 |
+
"tp": 46,
|
| 1534 |
+
"fp": 8,
|
| 1535 |
+
"fn": 6,
|
| 1536 |
+
"precision": 0.8518518518518519,
|
| 1537 |
+
"recall": 0.8846153846153846,
|
| 1538 |
+
"f1": 0.8679245283018868,
|
| 1539 |
+
"gt_count": 52,
|
| 1540 |
+
"pred_count": 54
|
| 1541 |
+
},
|
| 1542 |
+
"Nostril External of Nucleus": {
|
| 1543 |
+
"tp": 35,
|
| 1544 |
+
"fp": 16,
|
| 1545 |
+
"fn": 3,
|
| 1546 |
+
"precision": 0.6862745098039216,
|
| 1547 |
+
"recall": 0.9210526315789473,
|
| 1548 |
+
"f1": 0.7865168539325842,
|
| 1549 |
+
"gt_count": 38,
|
| 1550 |
+
"pred_count": 51
|
| 1551 |
+
}
|
| 1552 |
+
},
|
| 1553 |
+
"n_gt": 90,
|
| 1554 |
+
"n_pred": 105
|
| 1555 |
+
},
|
| 1556 |
+
"counts": {
|
| 1557 |
+
"Cell, Antibody +": {
|
| 1558 |
+
"gt": 175,
|
| 1559 |
+
"pred": 176,
|
| 1560 |
+
"abs_err": 1,
|
| 1561 |
+
"rel_err": 0.005714285714285714
|
| 1562 |
+
},
|
| 1563 |
+
"Cell, Antibody -": {
|
| 1564 |
+
"gt": 21,
|
| 1565 |
+
"pred": 14,
|
| 1566 |
+
"abs_err": 7,
|
| 1567 |
+
"rel_err": 0.3333333333333333
|
| 1568 |
+
},
|
| 1569 |
+
"Cell, Nostril, Antibody +": {
|
| 1570 |
+
"gt": 42,
|
| 1571 |
+
"pred": 46,
|
| 1572 |
+
"abs_err": 4,
|
| 1573 |
+
"rel_err": 0.09523809523809523
|
| 1574 |
+
},
|
| 1575 |
+
"Cell, Nostril, Antibody -": {
|
| 1576 |
+
"gt": 4,
|
| 1577 |
+
"pred": 0,
|
| 1578 |
+
"abs_err": 4,
|
| 1579 |
+
"rel_err": 1.0
|
| 1580 |
+
},
|
| 1581 |
+
"Nostril in Nucleus": {
|
| 1582 |
+
"gt": 52,
|
| 1583 |
+
"pred": 54,
|
| 1584 |
+
"abs_err": 2,
|
| 1585 |
+
"rel_err": 0.038461538461538464
|
| 1586 |
+
},
|
| 1587 |
+
"Nostril External of Nucleus": {
|
| 1588 |
+
"gt": 38,
|
| 1589 |
+
"pred": 51,
|
| 1590 |
+
"abs_err": 13,
|
| 1591 |
+
"rel_err": 0.34210526315789475
|
| 1592 |
+
}
|
| 1593 |
+
},
|
| 1594 |
+
"fp_analysis": null,
|
| 1595 |
+
"pairs": null,
|
| 1596 |
+
"summary": {
|
| 1597 |
+
"cell_det_f1": 0.8828451882845189,
|
| 1598 |
+
"cell_det_precision": 0.8940677966101694,
|
| 1599 |
+
"cell_det_recall": 0.871900826446281,
|
| 1600 |
+
"cell_mean_iou": 0.8168843572713155,
|
| 1601 |
+
"cell_det_f1_iou30": 0.9288702928870294,
|
| 1602 |
+
"cell_det_f1_centroid": 0.9246861924686192,
|
| 1603 |
+
"cell_attr_antibody": 0.95260663507109,
|
| 1604 |
+
"cell_attr_nostril": 0.981042654028436,
|
| 1605 |
+
"cell_attr_both": 0.933649289099526,
|
| 1606 |
+
"nos_det_f1": 0.8307692307692307,
|
| 1607 |
+
"nos_det_precision": 0.7714285714285715,
|
| 1608 |
+
"nos_det_recall": 0.9,
|
| 1609 |
+
"nos_type_acc": 1.0,
|
| 1610 |
+
"f1[Cell, Antibody +]": 0.8433048433048433,
|
| 1611 |
+
"f1[Cell, Antibody -]": 0.6857142857142857,
|
| 1612 |
+
"f1[Cell, Nostril, Antibody +]": 0.8409090909090908,
|
| 1613 |
+
"f1[Cell, Nostril, Antibody -]": 0.0,
|
| 1614 |
+
"f1[Nostril in Nucleus]": 0.8679245283018868,
|
| 1615 |
+
"f1[Nostril External of Nucleus]": 0.7865168539325842
|
| 1616 |
+
}
|
| 1617 |
+
},
|
| 1618 |
+
"F99": {
|
| 1619 |
+
"cells": {
|
| 1620 |
+
"detection": {
|
| 1621 |
+
"tp": 227,
|
| 1622 |
+
"fp": 28,
|
| 1623 |
+
"fn": 24,
|
| 1624 |
+
"precision": 0.8901960784313725,
|
| 1625 |
+
"recall": 0.9043824701195219,
|
| 1626 |
+
"f1": 0.8972332015810275,
|
| 1627 |
+
"mean_iou_matched": 0.8280746396024866,
|
| 1628 |
+
"iou30": {
|
| 1629 |
+
"tp": 238,
|
| 1630 |
+
"fp": 17,
|
| 1631 |
+
"fn": 13,
|
| 1632 |
+
"precision": 0.9333333333333333,
|
| 1633 |
+
"recall": 0.9482071713147411,
|
| 1634 |
+
"f1": 0.9407114624505929
|
| 1635 |
+
},
|
| 1636 |
+
"centroid": {
|
| 1637 |
+
"tp": 238,
|
| 1638 |
+
"fp": 17,
|
| 1639 |
+
"fn": 13,
|
| 1640 |
+
"precision": 0.9333333333333333,
|
| 1641 |
+
"recall": 0.9482071713147411,
|
| 1642 |
+
"f1": 0.9407114624505929
|
| 1643 |
+
}
|
| 1644 |
+
},
|
| 1645 |
+
"attribute_accuracy": {
|
| 1646 |
+
"antibody": 0.920704845814978,
|
| 1647 |
+
"nostril": 0.9911894273127754,
|
| 1648 |
+
"both": 0.9118942731277533,
|
| 1649 |
+
"n_matched": 227
|
| 1650 |
+
},
|
| 1651 |
+
"per_class": {
|
| 1652 |
+
"Cell, Antibody +": {
|
| 1653 |
+
"tp": 162,
|
| 1654 |
+
"fp": 41,
|
| 1655 |
+
"fn": 21,
|
| 1656 |
+
"precision": 0.7980295566502463,
|
| 1657 |
+
"recall": 0.8852459016393442,
|
| 1658 |
+
"f1": 0.839378238341969,
|
| 1659 |
+
"gt_count": 183,
|
| 1660 |
+
"pred_count": 203
|
| 1661 |
+
},
|
| 1662 |
+
"Cell, Antibody -": {
|
| 1663 |
+
"tp": 42,
|
| 1664 |
+
"fp": 6,
|
| 1665 |
+
"fn": 20,
|
| 1666 |
+
"precision": 0.875,
|
| 1667 |
+
"recall": 0.6774193548387096,
|
| 1668 |
+
"f1": 0.7636363636363636,
|
| 1669 |
+
"gt_count": 62,
|
| 1670 |
+
"pred_count": 48
|
| 1671 |
+
},
|
| 1672 |
+
"Cell, Nostril, Antibody +": {
|
| 1673 |
+
"tp": 3,
|
| 1674 |
+
"fp": 1,
|
| 1675 |
+
"fn": 3,
|
| 1676 |
+
"precision": 0.75,
|
| 1677 |
+
"recall": 0.5,
|
| 1678 |
+
"f1": 0.6,
|
| 1679 |
+
"gt_count": 6,
|
| 1680 |
+
"pred_count": 4
|
| 1681 |
+
},
|
| 1682 |
+
"Cell, Nostril, Antibody -": {
|
| 1683 |
+
"tp": 0,
|
| 1684 |
+
"fp": 0,
|
| 1685 |
+
"fn": 0,
|
| 1686 |
+
"precision": 0.0,
|
| 1687 |
+
"recall": 0.0,
|
| 1688 |
+
"f1": 0.0,
|
| 1689 |
+
"gt_count": 0,
|
| 1690 |
+
"pred_count": 0
|
| 1691 |
+
}
|
| 1692 |
+
},
|
| 1693 |
+
"confusion": {
|
| 1694 |
+
"Cell, Antibody +": {
|
| 1695 |
+
"Cell, Antibody +": 162,
|
| 1696 |
+
"Cell, Antibody -": 4,
|
| 1697 |
+
"Cell, Nostril, Antibody +": 0,
|
| 1698 |
+
"Cell, Nostril, Antibody -": 0
|
| 1699 |
+
},
|
| 1700 |
+
"Cell, Antibody -": {
|
| 1701 |
+
"Cell, Antibody +": 14,
|
| 1702 |
+
"Cell, Antibody -": 42,
|
| 1703 |
+
"Cell, Nostril, Antibody +": 0,
|
| 1704 |
+
"Cell, Nostril, Antibody -": 0
|
| 1705 |
+
},
|
| 1706 |
+
"Cell, Nostril, Antibody +": {
|
| 1707 |
+
"Cell, Antibody +": 2,
|
| 1708 |
+
"Cell, Antibody -": 0,
|
| 1709 |
+
"Cell, Nostril, Antibody +": 3,
|
| 1710 |
+
"Cell, Nostril, Antibody -": 0
|
| 1711 |
+
},
|
| 1712 |
+
"Cell, Nostril, Antibody -": {
|
| 1713 |
+
"Cell, Antibody +": 0,
|
| 1714 |
+
"Cell, Antibody -": 0,
|
| 1715 |
+
"Cell, Nostril, Antibody +": 0,
|
| 1716 |
+
"Cell, Nostril, Antibody -": 0
|
| 1717 |
+
}
|
| 1718 |
+
},
|
| 1719 |
+
"n_gt": 251,
|
| 1720 |
+
"n_pred": 255
|
| 1721 |
+
},
|
| 1722 |
+
"nostrils": {
|
| 1723 |
+
"detection": {
|
| 1724 |
+
"tp": 14,
|
| 1725 |
+
"fp": 1,
|
| 1726 |
+
"fn": 8,
|
| 1727 |
+
"precision": 0.9333333333333333,
|
| 1728 |
+
"recall": 0.6363636363636364,
|
| 1729 |
+
"f1": 0.7567567567567568,
|
| 1730 |
+
"mean_dist_matched": 0.6631874114708707
|
| 1731 |
+
},
|
| 1732 |
+
"type_accuracy": 1.0,
|
| 1733 |
+
"per_class": {
|
| 1734 |
+
"Nostril in Nucleus": {
|
| 1735 |
+
"tp": 4,
|
| 1736 |
+
"fp": 0,
|
| 1737 |
+
"fn": 2,
|
| 1738 |
+
"precision": 1.0,
|
| 1739 |
+
"recall": 0.6666666666666666,
|
| 1740 |
+
"f1": 0.8,
|
| 1741 |
+
"gt_count": 6,
|
| 1742 |
+
"pred_count": 4
|
| 1743 |
+
},
|
| 1744 |
+
"Nostril External of Nucleus": {
|
| 1745 |
+
"tp": 10,
|
| 1746 |
+
"fp": 1,
|
| 1747 |
+
"fn": 6,
|
| 1748 |
+
"precision": 0.9090909090909091,
|
| 1749 |
+
"recall": 0.625,
|
| 1750 |
+
"f1": 0.7407407407407406,
|
| 1751 |
+
"gt_count": 16,
|
| 1752 |
+
"pred_count": 11
|
| 1753 |
+
}
|
| 1754 |
+
},
|
| 1755 |
+
"n_gt": 22,
|
| 1756 |
+
"n_pred": 15
|
| 1757 |
+
},
|
| 1758 |
+
"counts": {
|
| 1759 |
+
"Cell, Antibody +": {
|
| 1760 |
+
"gt": 183,
|
| 1761 |
+
"pred": 203,
|
| 1762 |
+
"abs_err": 20,
|
| 1763 |
+
"rel_err": 0.1092896174863388
|
| 1764 |
+
},
|
| 1765 |
+
"Cell, Antibody -": {
|
| 1766 |
+
"gt": 62,
|
| 1767 |
+
"pred": 48,
|
| 1768 |
+
"abs_err": 14,
|
| 1769 |
+
"rel_err": 0.22580645161290322
|
| 1770 |
+
},
|
| 1771 |
+
"Cell, Nostril, Antibody +": {
|
| 1772 |
+
"gt": 6,
|
| 1773 |
+
"pred": 4,
|
| 1774 |
+
"abs_err": 2,
|
| 1775 |
+
"rel_err": 0.3333333333333333
|
| 1776 |
+
},
|
| 1777 |
+
"Cell, Nostril, Antibody -": {
|
| 1778 |
+
"gt": 0,
|
| 1779 |
+
"pred": 0,
|
| 1780 |
+
"abs_err": 0,
|
| 1781 |
+
"rel_err": 0.0
|
| 1782 |
+
},
|
| 1783 |
+
"Nostril in Nucleus": {
|
| 1784 |
+
"gt": 6,
|
| 1785 |
+
"pred": 4,
|
| 1786 |
+
"abs_err": 2,
|
| 1787 |
+
"rel_err": 0.3333333333333333
|
| 1788 |
+
},
|
| 1789 |
+
"Nostril External of Nucleus": {
|
| 1790 |
+
"gt": 16,
|
| 1791 |
+
"pred": 11,
|
| 1792 |
+
"abs_err": 5,
|
| 1793 |
+
"rel_err": 0.3125
|
| 1794 |
+
}
|
| 1795 |
+
},
|
| 1796 |
+
"fp_analysis": null,
|
| 1797 |
+
"pairs": null,
|
| 1798 |
+
"summary": {
|
| 1799 |
+
"cell_det_f1": 0.8972332015810275,
|
| 1800 |
+
"cell_det_precision": 0.8901960784313725,
|
| 1801 |
+
"cell_det_recall": 0.9043824701195219,
|
| 1802 |
+
"cell_mean_iou": 0.8280746396024866,
|
| 1803 |
+
"cell_det_f1_iou30": 0.9407114624505929,
|
| 1804 |
+
"cell_det_f1_centroid": 0.9407114624505929,
|
| 1805 |
+
"cell_attr_antibody": 0.920704845814978,
|
| 1806 |
+
"cell_attr_nostril": 0.9911894273127754,
|
| 1807 |
+
"cell_attr_both": 0.9118942731277533,
|
| 1808 |
+
"nos_det_f1": 0.7567567567567568,
|
| 1809 |
+
"nos_det_precision": 0.9333333333333333,
|
| 1810 |
+
"nos_det_recall": 0.6363636363636364,
|
| 1811 |
+
"nos_type_acc": 1.0,
|
| 1812 |
+
"f1[Cell, Antibody +]": 0.839378238341969,
|
| 1813 |
+
"f1[Cell, Antibody -]": 0.7636363636363636,
|
| 1814 |
+
"f1[Cell, Nostril, Antibody +]": 0.6,
|
| 1815 |
+
"f1[Cell, Nostril, Antibody -]": 0.0,
|
| 1816 |
+
"f1[Nostril in Nucleus]": 0.8,
|
| 1817 |
+
"f1[Nostril External of Nucleus]": 0.7407407407407406
|
| 1818 |
+
}
|
| 1819 |
+
}
|
| 1820 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 2 |
+
torch>=2.2
|
| 3 |
+
torchvision>=0.17
|
| 4 |
+
timm>=0.9.7
|
| 5 |
+
numpy>=1.26
|
| 6 |
+
scipy>=1.11
|
| 7 |
+
scikit-image>=0.22
|
| 8 |
+
tifffile>=2023.7
|
| 9 |
+
imagecodecs>=2023.7
|
| 10 |
+
Pillow>=10
|
| 11 |
+
gradio>=5.0
|
| 12 |
+
google-genai>=1.0
|
scripts/build_summary.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Write reports/SUMMARY.md from runs/cv/cv_results.json and reports/final/final_fit.json."""
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
|
| 9 |
+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 10 |
+
sys.path.insert(0, ROOT)
|
| 11 |
+
from cellquant.labels import ALL_LABELS, CELL_LABELS, NOSTRIL_LABELS # noqa: E402
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def pct(x: float) -> str:
|
| 15 |
+
return f"{100 * x:.1f}%"
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def main():
|
| 19 |
+
cv = json.load(open(os.path.join(ROOT, "runs", "cv", "cv_results.json")))
|
| 20 |
+
agg = cv["aggregate"]
|
| 21 |
+
per = agg["per_image"]
|
| 22 |
+
fit = json.load(open(os.path.join(ROOT, "reports", "final", "final_fit.json")))
|
| 23 |
+
tot = agg["dataset_totals"]
|
| 24 |
+
n_nuc = sum(tot[c] for c in CELL_LABELS)
|
| 25 |
+
n_pun = sum(tot[c] for c in NOSTRIL_LABELS)
|
| 26 |
+
det = agg["detection_base"]
|
| 27 |
+
det8 = agg["detection_tta8"]
|
| 28 |
+
L = ["# CellQuant: validation summary", "",
|
| 29 |
+
f"Data: {len(per)} annotated 1024×1024 images, {n_nuc} nucleus outlines and {n_pun} punctum outlines.",
|
| 30 |
+
"Protocol: leave-one-image-out cross-validation (one model per image, 3000 iterations each, held-out image never seen in training), "
|
| 31 |
+
"then a final model trained on all images with the same recipe. Each held-out image was tested 23 times: 3 identical runs "
|
| 32 |
+
"(determinism), no-TTA / 4× / 8× test-time augmentation, 8 rotations and flips, 6 photometric perturbations, and 6 post-processing rule variants. "
|
| 33 |
+
"The only choice made on held-out data is the rule that gives a nucleus the Nostril attribute: of three candidate rules, "
|
| 34 |
+
"'contains an in-nucleus punctum' (the annotation protocol) scored best and is the default; see the rule-variant table in cv_report.md.", "",
|
| 35 |
+
"## Headline numbers (micro-averaged over all held-out images, no TTA)", "",
|
| 36 |
+
"| Measure | Precision | Recall | F1 |", "|---|---:|---:|---:|",
|
| 37 |
+
f"| Nucleus detection, IoU ≥ 0.5 | {pct(det['cells']['precision'])} | {pct(det['cells']['recall'])} | {pct(det['cells']['f1'])} |",
|
| 38 |
+
f"| Nucleus detection, IoU ≥ 0.3 | {pct(det['cells_iou30']['precision'])} | {pct(det['cells_iou30']['recall'])} | {pct(det['cells_iou30']['f1'])} |",
|
| 39 |
+
f"| Nucleus detection, centroid inside GT nucleus | {pct(det['cells_centroid']['precision'])} | {pct(det['cells_centroid']['recall'])} | {pct(det['cells_centroid']['f1'])} |",
|
| 40 |
+
f"| Punctum detection (centroid ≤ 6 px) | {pct(det['nostrils']['precision'])} | {pct(det['nostrils']['recall'])} | {pct(det['nostrils']['f1'])} |",
|
| 41 |
+
f"| Nucleus detection, IoU ≥ 0.5, 8× TTA | {pct(det8['cells']['precision'])} | {pct(det8['cells']['recall'])} | {pct(det8['cells']['f1'])} |",
|
| 42 |
+
"", "Per-class F1 (a true positive needs a matched object **and** the right label):", "",
|
| 43 |
+
"| Label | GT | Pred | F1 |", "|---|---:|---:|---:|"]
|
| 44 |
+
for lab in ALL_LABELS:
|
| 45 |
+
r = agg["micro_base"][lab]
|
| 46 |
+
L.append(f"| {lab} | {r['gt_count']} | {r['pred_count']} | {pct(r['f1'])} |")
|
| 47 |
+
L += ["", "## Per held-out image", "",
|
| 48 |
+
"| Image | Nuclei F1 | Antibody acc | Nostril-attr acc | Puncta F1 | Nuclei F1 over 14 perturbed runs (mean ± sd) | Deterministic |",
|
| 49 |
+
"|---|---:|---:|---:|---:|---|---|"]
|
| 50 |
+
for k, s in per.items():
|
| 51 |
+
b = s["base"]; r = s["robustness"]["cell_det_f1"]
|
| 52 |
+
L.append(f"| {k} | {pct(b['cell_det_f1'])} | {pct(b['cell_attr_antibody'])} | {pct(b['cell_attr_nostril'])} | {pct(b['nos_det_f1'])} | "
|
| 53 |
+
f"{100*r['mean']:.1f} ± {100*r['std']:.1f} | {'yes' if s['deterministic'] else 'no'} |")
|
| 54 |
+
L += ["", "## Final model (trained on all images) – fit on its own training images, NOT a generalisation estimate", "",
|
| 55 |
+
"| Image | Nuclei F1 | Attribute acc | Puncta F1 |", "|---|---:|---:|---:|"]
|
| 56 |
+
for k, v in fit.items():
|
| 57 |
+
if "summary" in v:
|
| 58 |
+
s = v["summary"]
|
| 59 |
+
L.append(f"| {k} | {pct(s['cell_det_f1'])} | {pct(s['cell_attr_both'])} | {pct(s['nos_det_f1'])} |")
|
| 60 |
+
sw = agg.get("antibody_threshold_sweep")
|
| 61 |
+
if sw:
|
| 62 |
+
best = max(sw["sweep"], key=lambda r: r["neg_f1"])
|
| 63 |
+
L += ["", f"## Antibody − threshold (held-out matched nuclei: {sw['n_matched']}, of which {sw['n_negative']} negative)", "",
|
| 64 |
+
f"Default threshold 0.5. Best Antibody − F1 in the sweep: {pct(best['neg_f1'])} at threshold {best['thr']:.2f} "
|
| 65 |
+
f"(precision {pct(best['neg_precision'])}, recall {pct(best['neg_recall'])}); the full curve is in cv_report.md. "
|
| 66 |
+
"The default was kept because the class is dominated by a single image (F99 holds 62 of the 102 negatives)."]
|
| 67 |
+
L += ["", "## What limits the scores", "",
|
| 68 |
+
"* **Touching nuclei are sometimes merged** into one outline (worst on F113); this is the main source of IoU-0.5 misses. The marker/threshold sweep (`runs/cv/postprocess_sweep.json`) is flat, so it is a network-border issue, not a post-processing one.",
|
| 69 |
+
"* **Annotation completeness**: most unmatched predictions overlap an annotated nucleus (outline disagreement) and the rest are dimmer, smaller objects the annotator skipped; many faint red dots were likewise not annotated, which caps external-punctum precision.",
|
| 70 |
+
"* **Antibody − is rare** (102 of 1569 nuclei, 62 in one image), and **Cell, Nostril, Antibody −** has five examples; their scores are not reliable.",
|
| 71 |
+
"* Scores are from nine images of one experiment (35-day spinal cord, Iba1/TMEM119). New staining batches or magnifications need a check against a few annotated images.",
|
| 72 |
+
"", "## Reproduce", "",
|
| 73 |
+
"```bash", "python evaluate.py cv --data-dir <folder with TIFs and CVAT zips> --runs-dir runs/cv --iters 3000",
|
| 74 |
+
"python evaluate.py final --data-dir <folder> --weights weights/cellquant.pt", "python scripts/build_summary.py", "```", ""]
|
| 75 |
+
out = os.path.join(ROOT, "reports", "SUMMARY.md")
|
| 76 |
+
with open(out, "w") as f:
|
| 77 |
+
f.write("\n".join(L))
|
| 78 |
+
print("wrote", out)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
if __name__ == "__main__":
|
| 82 |
+
main()
|
scripts/deploy_hf.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Create / update the CellQuant Space on Hugging Face and wait until it runs.
|
| 3 |
+
|
| 4 |
+
python scripts/deploy_hf.py --repo <user>/cellquant [--public] [--no-wait]
|
| 5 |
+
|
| 6 |
+
Uses the token from `hf auth login` (or HF_TOKEN). Large files (the .pt checkpoint)
|
| 7 |
+
are uploaded through LFS automatically. Then polls the Space runtime and, once it is
|
| 8 |
+
RUNNING, calls its API with the bundled example image to prove it works.
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import os
|
| 14 |
+
import sys
|
| 15 |
+
import time
|
| 16 |
+
|
| 17 |
+
from huggingface_hub import HfApi, get_token, whoami
|
| 18 |
+
|
| 19 |
+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 20 |
+
IGNORE = ["runs/**", "runs/*", ".pytest_cache/**", "**/__pycache__/**", "__pycache__/**", "*.pyc",
|
| 21 |
+
"reports/final/*.png", "reports/cv/*.png", "*.log", ".gitignore", ".DS_Store"]
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def main(argv=None):
|
| 25 |
+
ap = argparse.ArgumentParser()
|
| 26 |
+
ap.add_argument("--repo", default=None, help="<user>/<space-name>; default <me>/cellquant")
|
| 27 |
+
ap.add_argument("--public", action="store_true")
|
| 28 |
+
ap.add_argument("--no-wait", action="store_true")
|
| 29 |
+
ap.add_argument("--timeout", type=int, default=1800)
|
| 30 |
+
a = ap.parse_args(argv)
|
| 31 |
+
api = HfApi()
|
| 32 |
+
me = whoami()["name"]
|
| 33 |
+
repo = a.repo or f"{me}/cellquant"
|
| 34 |
+
if not os.path.exists(os.path.join(ROOT, "weights", "cellquant.pt")):
|
| 35 |
+
raise SystemExit("weights/cellquant.pt missing: train first")
|
| 36 |
+
url = api.create_repo(repo, repo_type="space", space_sdk="gradio", private=not a.public, exist_ok=True)
|
| 37 |
+
print("space:", url)
|
| 38 |
+
api.upload_folder(folder_path=ROOT, repo_id=repo, repo_type="space", ignore_patterns=IGNORE,
|
| 39 |
+
commit_message="Deploy CellQuant")
|
| 40 |
+
print("uploaded")
|
| 41 |
+
if a.no_wait:
|
| 42 |
+
return
|
| 43 |
+
t0 = time.time()
|
| 44 |
+
last = None
|
| 45 |
+
while time.time() - t0 < a.timeout:
|
| 46 |
+
rt = api.get_space_runtime(repo)
|
| 47 |
+
stage = str(rt.stage)
|
| 48 |
+
if stage != last:
|
| 49 |
+
print(f"[{time.time()-t0:5.0f}s] stage {stage} hardware {rt.hardware}", flush=True)
|
| 50 |
+
last = stage
|
| 51 |
+
if stage.endswith("RUNNING"):
|
| 52 |
+
break
|
| 53 |
+
if "ERROR" in stage or stage.endswith("STOPPED"):
|
| 54 |
+
print("build failed; last log lines:")
|
| 55 |
+
try:
|
| 56 |
+
print(api.get_space_runtime(repo))
|
| 57 |
+
except Exception as e: # noqa: BLE001
|
| 58 |
+
print(e)
|
| 59 |
+
sys.exit(1)
|
| 60 |
+
time.sleep(15)
|
| 61 |
+
else:
|
| 62 |
+
raise SystemExit("timed out waiting for the Space")
|
| 63 |
+
# smoke test through the Gradio API
|
| 64 |
+
from gradio_client import Client, handle_file
|
| 65 |
+
client = Client(repo, hf_token=get_token())
|
| 66 |
+
ex = os.path.join(ROOT, "examples", "F100_crop_512.png")
|
| 67 |
+
t = time.time()
|
| 68 |
+
out = client.predict([handle_file(ex)], "none (fastest)", 0.5, 0.5, 50, "head", "contains", False, api_name="/analyze_files")
|
| 69 |
+
print(f"API call ok in {time.time()-t:.1f}s; counts row: {out[0]['data'][0] if isinstance(out[0], dict) else out[0]}")
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
if __name__ == "__main__":
|
| 73 |
+
main()
|
scripts/postprocess_sweep.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Sweep post-processing thresholds on held-out fold predictions (no retraining).
|
| 3 |
+
|
| 4 |
+
python scripts/postprocess_sweep.py --data-dir DATA --runs-dir runs/cv [--keys A,B] [--device mps]
|
| 5 |
+
|
| 6 |
+
For every fold checkpoint the held-out image is predicted once; every PostConfig in
|
| 7 |
+
the grid is then scored on the same probabilities. Prints micro-averaged nucleus
|
| 8 |
+
detection F1 (IoU >= 0.5 and centroid rule) and per-class F1 per setting.
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import itertools
|
| 14 |
+
import json
|
| 15 |
+
import os
|
| 16 |
+
import sys
|
| 17 |
+
|
| 18 |
+
import numpy as np
|
| 19 |
+
|
| 20 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 21 |
+
from cellquant.annotations import discover, load_image, rasterize # noqa: E402
|
| 22 |
+
from cellquant.infer import Predictor # noqa: E402
|
| 23 |
+
from cellquant.labels import CELL_LABELS, NOSTRIL_LABELS # noqa: E402
|
| 24 |
+
from cellquant.metrics import evaluate_cells, evaluate_nostrils, micro_average, prf # noqa: E402
|
| 25 |
+
from cellquant.postprocess import PostConfig, analyze # noqa: E402
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def main(argv=None):
|
| 29 |
+
ap = argparse.ArgumentParser()
|
| 30 |
+
ap.add_argument("--data-dir", required=True)
|
| 31 |
+
ap.add_argument("--runs-dir", default="runs/cv")
|
| 32 |
+
ap.add_argument("--keys", default="")
|
| 33 |
+
ap.add_argument("--device", default="auto")
|
| 34 |
+
ap.add_argument("--out", default="")
|
| 35 |
+
a = ap.parse_args(argv)
|
| 36 |
+
samples, _ = discover(a.data_dir, verbose=False)
|
| 37 |
+
keys = a.keys.split(",") if a.keys else [s.key for s in samples]
|
| 38 |
+
grid = [PostConfig(marker_thr=m, cell_fg_thr=f, min_marker_area=ma)
|
| 39 |
+
for m, f, ma in itertools.product((0.5, 0.6, 0.7, 0.8), (0.4, 0.5), (12, 30))]
|
| 40 |
+
per_cfg = {i: [] for i in range(len(grid))}
|
| 41 |
+
for s in samples:
|
| 42 |
+
if s.key not in keys:
|
| 43 |
+
continue
|
| 44 |
+
ck = os.path.join(a.runs_dir, f"fold_{s.key}.pt")
|
| 45 |
+
if not os.path.exists(ck):
|
| 46 |
+
print("missing", ck)
|
| 47 |
+
continue
|
| 48 |
+
img = load_image(s.image_path)
|
| 49 |
+
R = rasterize(s.annotation, img.shape[0], img.shape[1])
|
| 50 |
+
probs = Predictor(ck, device=a.device, tta=1).probs(img)
|
| 51 |
+
for i, cfg in enumerate(grid):
|
| 52 |
+
res = analyze(probs, cfg)
|
| 53 |
+
ce = evaluate_cells(R.cell_inst, R.cell_labels, res.cell_inst, res.cell_labels)
|
| 54 |
+
ne = evaluate_nostrils(R.nostril_inst, R.nostril_labels, res.nostril_inst, res.nostril_labels)
|
| 55 |
+
per_cfg[i].append({"key": s.key, "det": ce["detection"], "cells": ce["per_class"], "nos": ne["per_class"],
|
| 56 |
+
"attr": ce["attribute_accuracy"]})
|
| 57 |
+
print("done", s.key, flush=True)
|
| 58 |
+
rows = []
|
| 59 |
+
for i, cfg in enumerate(grid):
|
| 60 |
+
rs = per_cfg[i]
|
| 61 |
+
if not rs:
|
| 62 |
+
continue
|
| 63 |
+
tp = sum(r["det"]["tp"] for r in rs); fp = sum(r["det"]["fp"] for r in rs); fn = sum(r["det"]["fn"] for r in rs)
|
| 64 |
+
ctp = sum(r["det"]["centroid"]["tp"] for r in rs); cfp = sum(r["det"]["centroid"]["fp"] for r in rs); cfn = sum(r["det"]["centroid"]["fn"] for r in rs)
|
| 65 |
+
mc = micro_average([r["cells"] for r in rs], CELL_LABELS)
|
| 66 |
+
mn = micro_average([r["nos"] for r in rs], NOSTRIL_LABELS)
|
| 67 |
+
per_img = {r["key"]: round(r["det"]["f1"], 3) for r in rs}
|
| 68 |
+
rows.append({"marker_thr": cfg.marker_thr, "cell_fg_thr": cfg.cell_fg_thr, "min_marker_area": cfg.min_marker_area,
|
| 69 |
+
"nuc_f1_iou50": prf(tp, fp, fn)["f1"], "nuc_f1_centroid": prf(ctp, cfp, cfn)["f1"],
|
| 70 |
+
"f1_ab_pos": mc["Cell, Antibody +"]["f1"], "f1_ab_neg": mc["Cell, Antibody -"]["f1"],
|
| 71 |
+
"f1_nos_pos": mc["Cell, Nostril, Antibody +"]["f1"], "f1_pin": mn["Nostril in Nucleus"]["f1"],
|
| 72 |
+
"per_image": per_img})
|
| 73 |
+
rows.sort(key=lambda r: -r["nuc_f1_iou50"])
|
| 74 |
+
print(f"{'marker':>6} {'fg':>4} {'minA':>4} | {'nucF1@.5':>8} {'nucF1cen':>8} | {'Ab+':>6} {'Ab-':>6} {'NosAb+':>6} {'PinNuc':>6} | per image")
|
| 75 |
+
for r in rows:
|
| 76 |
+
print(f"{r['marker_thr']:6.2f} {r['cell_fg_thr']:4.1f} {r['min_marker_area']:4d} | {100*r['nuc_f1_iou50']:8.1f} {100*r['nuc_f1_centroid']:8.1f} | "
|
| 77 |
+
f"{100*r['f1_ab_pos']:6.1f} {100*r['f1_ab_neg']:6.1f} {100*r['f1_nos_pos']:6.1f} {100*r['f1_pin']:6.1f} | "
|
| 78 |
+
+ " ".join(f"{k}:{100*v:.0f}" for k, v in r['per_image'].items()))
|
| 79 |
+
if a.out:
|
| 80 |
+
with open(a.out, "w") as f:
|
| 81 |
+
json.dump(rows, f, indent=1)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
if __name__ == "__main__":
|
| 85 |
+
main()
|
tests/conftest.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
tests/test_annotations.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import zipfile
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
import pytest
|
| 6 |
+
|
| 7 |
+
from cellquant.annotations import (
|
| 8 |
+
ImageAnnotation,
|
| 9 |
+
Shape,
|
| 10 |
+
discover,
|
| 11 |
+
image_key,
|
| 12 |
+
load_annotation_file,
|
| 13 |
+
parse_cvat_xml,
|
| 14 |
+
rasterize,
|
| 15 |
+
to_rgb_uint8,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
XML = """<?xml version="1.0" encoding="utf-8"?>
|
| 19 |
+
<annotations>
|
| 20 |
+
<version>1.1</version>
|
| 21 |
+
<meta><task><size>1</size></task></meta>
|
| 22 |
+
<image id="0" name="K1 demo - RGB copy.tif" width="100" height="80">
|
| 23 |
+
<polyline label="Cell, Antibody +" source="manual" occluded="0" points="10.0,10.0;30.0,10.0;30.0,30.0;10.0,30.0;10.0,10.0" z_order="0"></polyline>
|
| 24 |
+
<polygon label="Cell, Nostril, Antibody -" source="manual" occluded="0" points="50,50;70,50;70,70;50,70" z_order="0"></polygon>
|
| 25 |
+
<ellipse label="Nostril in Nucleus" source="manual" occluded="0" cx="60" cy="60" rx="3" ry="2" z_order="0"></ellipse>
|
| 26 |
+
<polyline label="Nostril External of Nucleus" source="manual" occluded="0" points="90.2,5.1;90.4,5.3;90.6,5.1" z_order="0"></polyline>
|
| 27 |
+
<box label="Cell, Antibody -" source="manual" occluded="0" xtl="80" ytl="60" xbr="95" ybr="75" z_order="0"></box>
|
| 28 |
+
</image>
|
| 29 |
+
</annotations>
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def test_parse_xml_shapes_and_counts():
|
| 34 |
+
anns = parse_cvat_xml(XML.encode())
|
| 35 |
+
assert len(anns) == 1
|
| 36 |
+
a = anns[0]
|
| 37 |
+
assert (a.width, a.height) == (100, 80)
|
| 38 |
+
assert a.key == "K1"
|
| 39 |
+
c = a.counts()
|
| 40 |
+
assert c["Cell, Antibody +"] == 1 and c["Cell, Nostril, Antibody -"] == 1 and c["Cell, Antibody -"] == 1
|
| 41 |
+
assert c["Nostril in Nucleus"] == 1 and c["Nostril External of Nucleus"] == 1
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def test_rasterize_areas_and_attributes():
|
| 45 |
+
a = parse_cvat_xml(XML.encode())[0]
|
| 46 |
+
R = rasterize(a)
|
| 47 |
+
assert R.cell_inst.shape == (80, 100)
|
| 48 |
+
assert R.n_cells == 3 and R.n_nostrils == 2
|
| 49 |
+
areas = np.bincount(R.cell_inst.ravel())[1:]
|
| 50 |
+
# 20x20 squares rasterise to ~21x21 pixels (inclusive edges) -> 400..441
|
| 51 |
+
assert all(380 <= x <= 450 for x in areas[:2]) or all(380 <= x <= 450 for x in sorted(areas)[-2:])
|
| 52 |
+
# tiny polyline (sub-pixel) keeps its centroid pixel
|
| 53 |
+
nareas = np.bincount(R.nostril_inst.ravel())[1:]
|
| 54 |
+
assert nareas.min() >= 1
|
| 55 |
+
# ellipse rx=3, ry=2 -> pi*6 ~ 19 px
|
| 56 |
+
assert 10 <= nareas.max() <= 30
|
| 57 |
+
ab = R.cell_antibody()
|
| 58 |
+
hn = R.cell_has_nostril()
|
| 59 |
+
labels = R.cell_labels
|
| 60 |
+
for i, lab in enumerate(labels):
|
| 61 |
+
if i == 0:
|
| 62 |
+
continue
|
| 63 |
+
assert ab[i] == lab.endswith("+")
|
| 64 |
+
assert hn[i] == ("Nostril" in lab)
|
| 65 |
+
assert R.nostril_inside().sum() == 1
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def test_image_key_handles_copies_and_double_spaces():
|
| 69 |
+
assert image_key("F114 F 35D SC Iba W002 - Focused EDF Document_RGB.tif") == "F114"
|
| 70 |
+
assert image_key("C110 F 35D SC Iba W001 - Focused EDF Document_RGB copy (1).tif") == "C110"
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def test_discover_pairs_by_internal_name(tmp_path):
|
| 74 |
+
from PIL import Image
|
| 75 |
+
Image.fromarray(np.zeros((80, 100, 3), np.uint8)).save(tmp_path / "K1 demo - RGB copy (1).png")
|
| 76 |
+
Image.fromarray(np.zeros((80, 100, 3), np.uint8)).save(tmp_path / "K2 other.png") # unannotated
|
| 77 |
+
with zipfile.ZipFile(tmp_path / "WRONGNAME_Annotated.zip", "w") as zf:
|
| 78 |
+
zf.writestr("annotations.xml", XML)
|
| 79 |
+
samples, unann = discover(str(tmp_path), verbose=False)
|
| 80 |
+
assert [s.key for s in samples] == ["K1"]
|
| 81 |
+
assert len(unann) == 1 and unann[0].endswith("K2 other.png")
|
| 82 |
+
assert samples[0].annotation.count("Cell, Antibody +") == 1
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def test_load_annotation_file_zip_and_xml(tmp_path):
|
| 86 |
+
(tmp_path / "a.xml").write_text(XML)
|
| 87 |
+
with zipfile.ZipFile(tmp_path / "a.zip", "w") as zf:
|
| 88 |
+
zf.writestr("annotations.xml", XML)
|
| 89 |
+
assert len(load_annotation_file(str(tmp_path / "a.xml"))[0].shapes) == 5
|
| 90 |
+
assert len(load_annotation_file(str(tmp_path / "a.zip"))[0].shapes) == 5
|
| 91 |
+
with pytest.raises(ValueError):
|
| 92 |
+
load_annotation_file(str(tmp_path / "nothing.txt"))
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def test_to_rgb_uint8_variants():
|
| 96 |
+
g = np.full((4, 5), 7, np.uint8)
|
| 97 |
+
assert to_rgb_uint8(g).shape == (4, 5, 3)
|
| 98 |
+
chw = np.zeros((3, 4, 5), np.uint8)
|
| 99 |
+
assert to_rgb_uint8(chw).shape == (4, 5, 3)
|
| 100 |
+
rgba = np.zeros((4, 5, 4), np.uint8)
|
| 101 |
+
assert to_rgb_uint8(rgba).shape == (4, 5, 3)
|
| 102 |
+
u16 = np.full((4, 5, 3), 4000, np.uint16)
|
| 103 |
+
out = to_rgb_uint8(u16)
|
| 104 |
+
assert out.dtype == np.uint8 and out.max() == 255
|
| 105 |
+
f01 = np.full((4, 5, 3), 0.5, np.float32)
|
| 106 |
+
assert to_rgb_uint8(f01).max() in (127, 128)
|
| 107 |
+
with pytest.raises(ValueError):
|
| 108 |
+
to_rgb_uint8(np.zeros((40, 50, 2), np.uint8))
|
| 109 |
+
with pytest.raises(ValueError):
|
| 110 |
+
to_rgb_uint8(np.zeros((4, 50, 2), np.uint8)) # 4 rows is not "RGBA channel-first"
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def test_shape_area_shoelace():
|
| 114 |
+
s = Shape("Cell, Antibody +", "polygon", np.array([[0, 0], [10, 0], [10, 10], [0, 10]], np.float32))
|
| 115 |
+
assert abs(s.area - 100) < 1e-6
|
tests/test_app.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
import pytest
|
| 5 |
+
from PIL import Image
|
| 6 |
+
|
| 7 |
+
WEIGHTS = os.environ.get("CELLQUANT_WEIGHTS", os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "weights", "cellquant.pt"))
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@pytest.mark.skipif(not os.path.exists(WEIGHTS), reason="trained weights not present")
|
| 11 |
+
def test_analyze_files_end_to_end(tmp_path):
|
| 12 |
+
import app
|
| 13 |
+
rng = np.random.default_rng(0)
|
| 14 |
+
img = (rng.random((200, 240, 3)) * 50).astype(np.uint8)
|
| 15 |
+
p1 = tmp_path / "a.png"
|
| 16 |
+
Image.fromarray(img).save(p1)
|
| 17 |
+
import tifffile
|
| 18 |
+
p2 = tmp_path / "b.tif"
|
| 19 |
+
tifffile.imwrite(str(p2), img)
|
| 20 |
+
rows, gallery, zip_path, summary = app.analyze_files([str(p1), str(p2)], "none (fastest)", 0.5, 0.5, 50, "head", "contains", False)
|
| 21 |
+
assert len(rows) == 2 and rows[0][0] == "a.png" and len(rows[0]) == 7
|
| 22 |
+
assert len(gallery) == 2 and os.path.exists(gallery[0][0])
|
| 23 |
+
assert os.path.exists(zip_path) and summary == ""
|
| 24 |
+
import zipfile
|
| 25 |
+
with zipfile.ZipFile(zip_path) as zf:
|
| 26 |
+
names = zf.namelist()
|
| 27 |
+
assert "counts.csv" in names and "objects.csv" in names and "results.json" in names
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def test_build_demo_constructs():
|
| 31 |
+
pytest.importorskip("gradio")
|
| 32 |
+
import app
|
| 33 |
+
demo = app.build_demo()
|
| 34 |
+
assert demo is not None
|
tests/test_data.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
from cellquant.annotations import RasterAnnotation
|
| 4 |
+
from cellquant.data import AugConfig, PatchSampler, make_targets
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def _raster():
|
| 8 |
+
inst = np.zeros((64, 64), np.int32)
|
| 9 |
+
inst[10:30, 10:30] = 1
|
| 10 |
+
inst[30:50, 30:50] = 2 # touches instance 1 at a corner
|
| 11 |
+
nos = np.zeros((64, 64), np.int32)
|
| 12 |
+
nos[20:23, 20:23] = 1
|
| 13 |
+
nos[5:7, 50:52] = 2
|
| 14 |
+
return RasterAnnotation(inst, ["", "Cell, Nostril, Antibody +", "Cell, Antibody -"], nos,
|
| 15 |
+
["", "Nostril in Nucleus", "Nostril External of Nucleus"])
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def test_make_targets_layout():
|
| 19 |
+
T = make_targets(_raster())
|
| 20 |
+
assert set(np.unique(T["cell3"])) == {0, 1, 2}
|
| 21 |
+
assert ((T["cell3"] > 0) == (T["cellmask"] > 0)).all()
|
| 22 |
+
# border ring is 2 px wide: a 20x20 square has 20*20 - 16*16 = 144 border px
|
| 23 |
+
assert int((T["cell3"][10:30, 10:30] == 2).sum()) == 144
|
| 24 |
+
assert T["antibody"][15, 15] == 1 and T["antibody"][40, 40] == 0
|
| 25 |
+
assert T["cellnos"][15, 15] == 1 and T["cellnos"][40, 40] == 0
|
| 26 |
+
assert T["nos3"][21, 21] == 1 and T["nos3"][5, 50] == 2 and T["nos3"][0, 0] == 0
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def test_sampler_batch_shapes_and_reproducibility():
|
| 30 |
+
img = (np.random.default_rng(0).random((64, 64, 3)) * 255).astype(np.uint8)
|
| 31 |
+
T = make_targets(_raster())
|
| 32 |
+
cfg = AugConfig(crop=32)
|
| 33 |
+
a = PatchSampler([img], [T], cfg, seed=3).batch(2)
|
| 34 |
+
b = PatchSampler([img], [T], cfg, seed=3).batch(2)
|
| 35 |
+
x, t = a
|
| 36 |
+
assert x.shape == (2, 3, 32, 32) and x.min() >= 0 and x.max() <= 1
|
| 37 |
+
assert t["cell3"].shape == (2, 32, 32) and t["cell3"].dtype.is_floating_point is False
|
| 38 |
+
assert np.allclose(x.numpy(), b[0].numpy())
|
| 39 |
+
assert all(np.array_equal(t[k].numpy(), b[1][k].numpy()) for k in t)
|
tests/test_evaluate.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
import evaluate as ev
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def test_dihedral_transforms_invert_for_2d_and_3d():
|
| 7 |
+
rng = np.random.default_rng(0)
|
| 8 |
+
a2 = rng.random((5, 7))
|
| 9 |
+
a3 = rng.random((3, 5, 7))
|
| 10 |
+
for k, f in ev.DIHEDRAL:
|
| 11 |
+
assert np.array_equal(ev.inv_np(ev.fwd_np(a2, k, f), k, f), a2)
|
| 12 |
+
assert np.array_equal(ev.inv_np(ev.fwd_np(a3, k, f), k, f), a3)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_dihedral_transforms_are_distinct():
|
| 16 |
+
a = np.arange(12).reshape(3, 4).astype(float)
|
| 17 |
+
outs = [ev.fwd_np(a, k, f).tobytes() + bytes(ev.fwd_np(a, k, f).shape) for k, f in ev.DIHEDRAL]
|
| 18 |
+
assert len(set(outs)) == 8
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_photometric_is_deterministic_and_valid():
|
| 22 |
+
img = (np.random.default_rng(1).random((16, 16, 3)) * 255).astype(np.uint8)
|
| 23 |
+
a = ev.photometric(img, 5)
|
| 24 |
+
b = ev.photometric(img, 5)
|
| 25 |
+
assert np.array_equal(a, b) and a.dtype == np.uint8 and a.shape == img.shape
|
| 26 |
+
assert not np.array_equal(ev.photometric(img, 6), a)
|
tests/test_labels.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from cellquant.labels import ALL_LABELS, CELL_LABELS, NOSTRIL_LABELS, cell_attributes, cell_label, nostril_in_nucleus, nostril_label
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def test_cell_label_roundtrip():
|
| 5 |
+
for lab in CELL_LABELS:
|
| 6 |
+
a, n = cell_attributes(lab)
|
| 7 |
+
assert cell_label(a, n) == lab
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def test_nostril_label_roundtrip():
|
| 11 |
+
for lab in NOSTRIL_LABELS:
|
| 12 |
+
assert nostril_label(nostril_in_nucleus(lab)) == lab
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_label_sets_disjoint():
|
| 16 |
+
assert len(ALL_LABELS) == 6 and set(CELL_LABELS).isdisjoint(NOSTRIL_LABELS)
|
tests/test_metrics.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
from cellquant.metrics import count_errors, evaluate_cells, evaluate_nostrils, match_by_centroid, match_by_iou, micro_average
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def _two_squares():
|
| 7 |
+
inst = np.zeros((50, 50), np.int32)
|
| 8 |
+
inst[5:15, 5:15] = 1
|
| 9 |
+
inst[30:45, 30:45] = 2
|
| 10 |
+
return inst
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def test_perfect_prediction_scores_one():
|
| 14 |
+
inst = _two_squares()
|
| 15 |
+
labels = ["", "Cell, Antibody +", "Cell, Antibody -"]
|
| 16 |
+
r = evaluate_cells(inst, labels, inst.copy(), list(labels))
|
| 17 |
+
assert r["detection"]["f1"] == 1.0 and r["attribute_accuracy"]["both"] == 1.0
|
| 18 |
+
assert r["per_class"]["Cell, Antibody +"]["f1"] == 1.0
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_wrong_label_counts_as_fp_and_fn_for_classes_but_detected():
|
| 22 |
+
inst = _two_squares()
|
| 23 |
+
gt = ["", "Cell, Antibody +", "Cell, Antibody -"]
|
| 24 |
+
pred = ["", "Cell, Antibody +", "Cell, Antibody +"]
|
| 25 |
+
r = evaluate_cells(inst, gt, inst.copy(), pred)
|
| 26 |
+
assert r["detection"]["tp"] == 2
|
| 27 |
+
assert r["per_class"]["Cell, Antibody -"]["fn"] == 1 and r["per_class"]["Cell, Antibody +"]["fp"] == 1
|
| 28 |
+
assert r["attribute_accuracy"]["antibody"] == 0.5
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def test_shifted_prediction_fails_iou_but_passes_centroid():
|
| 32 |
+
gt = _two_squares()
|
| 33 |
+
pred = np.zeros_like(gt)
|
| 34 |
+
pred[9:19, 9:19] = 1 # IoU with square 1 = 36/164 < 0.5, centroid (14,14) is inside GT square 1
|
| 35 |
+
pred[30:45, 30:45] = 2
|
| 36 |
+
assert len(match_by_iou(gt, pred, 0.5)) == 1
|
| 37 |
+
r = evaluate_cells(gt, ["", "Cell, Antibody +", "Cell, Antibody +"], pred, ["", "Cell, Antibody +", "Cell, Antibody +"])
|
| 38 |
+
assert r["detection"]["tp"] == 1 and r["detection"]["centroid"]["tp"] == 2
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def test_nostril_matching_by_distance():
|
| 42 |
+
gt = np.zeros((40, 40), np.int32)
|
| 43 |
+
gt[10:13, 10:13] = 1
|
| 44 |
+
gt[30:33, 30:33] = 2
|
| 45 |
+
pred = np.zeros_like(gt)
|
| 46 |
+
pred[12:15, 12:15] = 1 # 2 px away -> match
|
| 47 |
+
pred[5:8, 30:33] = 2 # 25 px away -> no match
|
| 48 |
+
m = match_by_centroid(gt, pred, 6.0)
|
| 49 |
+
assert len(m) == 1 and m[0][:2] == (1, 1)
|
| 50 |
+
r = evaluate_nostrils(gt, ["", "Nostril in Nucleus", "Nostril External of Nucleus"], pred, ["", "Nostril in Nucleus", "Nostril in Nucleus"])
|
| 51 |
+
assert r["detection"]["tp"] == 1 and r["detection"]["fp"] == 1 and r["detection"]["fn"] == 1
|
| 52 |
+
assert r["type_accuracy"] == 1.0
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test_count_errors_and_micro_average():
|
| 56 |
+
ce = count_errors({"Cell, Antibody +": 10}, {"Cell, Antibody +": 8})
|
| 57 |
+
assert ce["Cell, Antibody +"]["abs_err"] == 2 and abs(ce["Cell, Antibody +"]["rel_err"] - 0.2) < 1e-9
|
| 58 |
+
per = [{"A": {"tp": 1, "fp": 1, "fn": 0, "gt_count": 1, "pred_count": 2}}, {"A": {"tp": 3, "fp": 0, "fn": 1, "gt_count": 4, "pred_count": 3}}]
|
| 59 |
+
m = micro_average(per, ["A"])
|
| 60 |
+
assert m["A"]["tp"] == 4 and m["A"]["fp"] == 1 and m["A"]["fn"] == 1 and abs(m["A"]["f1"] - 0.8) < 1e-9
|
tests/test_model_infer.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
import pytest
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from cellquant.model import CellUNet, N_OUT, load_checkpoint, pad_to_multiple, save_checkpoint
|
| 8 |
+
|
| 9 |
+
WEIGHTS = os.environ.get("CELLQUANT_WEIGHTS", os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "weights", "cellquant.pt"))
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def test_pad_to_multiple():
|
| 13 |
+
x = torch.zeros(1, 3, 70, 100)
|
| 14 |
+
xp, (ph, pw) = pad_to_multiple(x, 32)
|
| 15 |
+
assert xp.shape[-2:] == (96, 128) and (ph, pw) == (26, 28)
|
| 16 |
+
x2, (ph2, pw2) = pad_to_multiple(torch.zeros(1, 3, 64, 64), 32)
|
| 17 |
+
assert x2.shape[-2:] == (64, 64) and (ph2, pw2) == (0, 0)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def test_model_forward_and_checkpoint_roundtrip(tmp_path):
|
| 21 |
+
m = CellUNet("resnet34", pretrained=False).eval()
|
| 22 |
+
x = torch.rand(1, 3, 64, 96)
|
| 23 |
+
with torch.no_grad():
|
| 24 |
+
y = m(x)
|
| 25 |
+
assert y.shape == (1, N_OUT, 64, 96)
|
| 26 |
+
p = tmp_path / "m.pt"
|
| 27 |
+
save_checkpoint(m, str(p), {"note": "test"})
|
| 28 |
+
m2, meta = load_checkpoint(str(p))
|
| 29 |
+
assert meta["note"] == "test" and meta["encoder"] == "resnet34"
|
| 30 |
+
with torch.no_grad():
|
| 31 |
+
assert torch.allclose(m2(x), y, atol=1e-5)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@pytest.mark.skipif(not os.path.exists(WEIGHTS), reason="trained weights not present")
|
| 35 |
+
def test_predictor_deterministic_and_tiling_consistent():
|
| 36 |
+
from cellquant.infer import Predictor
|
| 37 |
+
rng = np.random.default_rng(0)
|
| 38 |
+
img = (rng.random((300, 420, 3)) * 60).astype(np.uint8)
|
| 39 |
+
p = Predictor(WEIGHTS, device="cpu", tta=1)
|
| 40 |
+
a = p.probs(img)
|
| 41 |
+
b = p.probs(img)
|
| 42 |
+
assert all(np.array_equal(a[k], b[k]) for k in a)
|
| 43 |
+
assert a["cell"].shape == (3, 300, 420)
|
| 44 |
+
# force tiling and compare with the whole-image pass
|
| 45 |
+
pt = Predictor(WEIGHTS, device="cpu", tta=1, tile=256, overlap=64, max_whole=128)
|
| 46 |
+
c = pt.probs(img)
|
| 47 |
+
diff = np.abs(c["cell"] - a["cell"]).mean()
|
| 48 |
+
assert diff < 0.05, diff
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@pytest.mark.skipif(not os.path.exists(WEIGHTS), reason="trained weights not present")
|
| 52 |
+
def test_tta_outputs_valid_probabilities():
|
| 53 |
+
from cellquant.infer import Predictor
|
| 54 |
+
img = (np.random.default_rng(1).random((128, 160, 3)) * 60).astype(np.uint8)
|
| 55 |
+
for tta in (4, 8):
|
| 56 |
+
pr = Predictor(WEIGHTS, device="cpu", tta=tta).probs(img)
|
| 57 |
+
assert np.allclose(pr["cell"].sum(0), 1, atol=1e-4) and np.allclose(pr["nos"].sum(0), 1, atol=1e-4)
|
tests/test_postprocess.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
from cellquant.labels import ALL_LABELS
|
| 4 |
+
from cellquant.postprocess import PostConfig, analyze, logits_to_probs, render_overlay, segment_cells
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def _disc(h, w, cy, cx, r):
|
| 8 |
+
yy, xx = np.mgrid[:h, :w]
|
| 9 |
+
return (yy - cy) ** 2 + (xx - cx) ** 2 <= r * r
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _probs(h=96, w=96):
|
| 13 |
+
"""Two touching discs; the left one antibody+ with a punctum inside; one external punctum."""
|
| 14 |
+
d1, d2 = _disc(h, w, 48, 30, 14), _disc(h, w, 48, 56, 14)
|
| 15 |
+
fg = d1 | d2
|
| 16 |
+
inner = _disc(h, w, 48, 30, 11) | _disc(h, w, 48, 56, 11)
|
| 17 |
+
cell = np.zeros((3, h, w), np.float32)
|
| 18 |
+
cell[1][inner] = 0.9
|
| 19 |
+
cell[2][fg & ~inner] = 0.9
|
| 20 |
+
cell[0] = 1 - cell[1] - cell[2]
|
| 21 |
+
antibody = np.where(d1, 0.95, 0.05).astype(np.float32)
|
| 22 |
+
cellnos = np.full((h, w), 0.1, np.float32)
|
| 23 |
+
nos = np.zeros((3, h, w), np.float32)
|
| 24 |
+
p_in = _disc(h, w, 48, 30, 2)
|
| 25 |
+
p_ext = _disc(h, w, 10, 80, 2)
|
| 26 |
+
nos[1][p_in] = 0.9
|
| 27 |
+
nos[2][p_ext] = 0.9
|
| 28 |
+
nos[0] = 1 - nos[1] - nos[2]
|
| 29 |
+
return {"cell": cell, "antibody": antibody, "cellnos": cellnos, "nos": nos}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def test_touching_discs_split_into_two_instances():
|
| 33 |
+
inst = segment_cells(_probs()["cell"], PostConfig())
|
| 34 |
+
assert inst.max() == 2
|
| 35 |
+
areas = np.bincount(inst.ravel())[1:]
|
| 36 |
+
assert all(500 <= a <= 700 for a in areas) # pi*14^2 ~ 616
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_analyze_labels_and_counts():
|
| 40 |
+
res = analyze(_probs(), PostConfig())
|
| 41 |
+
assert len(res.cells) == 2 and len(res.nostrils) == 2
|
| 42 |
+
labels = sorted(c["label"] for c in res.cells)
|
| 43 |
+
assert labels == ["Cell, Antibody -", "Cell, Nostril, Antibody +"]
|
| 44 |
+
assert sorted(n["label"] for n in res.nostrils) == ["Nostril External of Nucleus", "Nostril in Nucleus"]
|
| 45 |
+
assert sum(res.counts.values()) == 4 and set(res.counts) == set(ALL_LABELS)
|
| 46 |
+
inside = [n for n in res.nostrils if n["label"] == "Nostril in Nucleus"][0]
|
| 47 |
+
assert inside["cell_id"] == [c for c in res.cells if c["label"].startswith("Cell, Nostril")][0]["id"]
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def test_cellnos_rules():
|
| 51 |
+
p = _probs()
|
| 52 |
+
head_only = analyze(p, PostConfig(cellnos_rule="head"))
|
| 53 |
+
assert all(not c["label"].startswith("Cell, Nostril") for c in head_only.cells)
|
| 54 |
+
contains = analyze(p, PostConfig(cellnos_rule="contains"))
|
| 55 |
+
assert sum(c["label"].startswith("Cell, Nostril") for c in contains.cells) == 1
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def test_geometric_nostril_rule_overrides_head():
|
| 59 |
+
p = _probs()
|
| 60 |
+
# make the network call the inside punctum "external"; geometric rule should still say inside
|
| 61 |
+
p["nos"][1], p["nos"][2] = p["nos"][2].copy(), p["nos"][1].copy()
|
| 62 |
+
head = analyze(p, PostConfig(nostril_rule="head"))
|
| 63 |
+
geo = analyze(p, PostConfig(nostril_rule="geometric"))
|
| 64 |
+
assert head.counts["Nostril in Nucleus"] == 1 and geo.counts["Nostril in Nucleus"] == 1
|
| 65 |
+
# with head rule the punctum inside the disc is now labelled external
|
| 66 |
+
inside_disc = [n for n in head.nostrils if n["cell_id"] > 0][0]
|
| 67 |
+
assert inside_disc["label"] == "Nostril External of Nucleus"
|
| 68 |
+
assert [n for n in geo.nostrils if n["cell_id"] > 0][0]["label"] == "Nostril in Nucleus"
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_min_area_filters_small_instances():
|
| 72 |
+
p = _probs()
|
| 73 |
+
assert len(analyze(p, PostConfig(min_cell_area=1000)).cells) == 0
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def test_logits_to_probs_and_overlay():
|
| 77 |
+
logits = np.random.default_rng(0).normal(size=(8, 40, 40)).astype(np.float32)
|
| 78 |
+
pr = logits_to_probs(logits)
|
| 79 |
+
assert np.allclose(pr["cell"].sum(0), 1, atol=1e-5) and np.allclose(pr["nos"].sum(0), 1, atol=1e-5)
|
| 80 |
+
assert pr["antibody"].min() >= 0 and pr["antibody"].max() <= 1
|
| 81 |
+
res = analyze(_probs(), PostConfig())
|
| 82 |
+
img = np.zeros((96, 96, 3), np.uint8)
|
| 83 |
+
ov = render_overlay(img, res)
|
| 84 |
+
assert ov.shape == img.shape and ov.any()
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def test_empty_image_gives_no_objects():
|
| 88 |
+
h = w = 64
|
| 89 |
+
p = {"cell": np.stack([np.ones((h, w)), np.zeros((h, w)), np.zeros((h, w))]).astype(np.float32),
|
| 90 |
+
"antibody": np.zeros((h, w), np.float32), "cellnos": np.zeros((h, w), np.float32),
|
| 91 |
+
"nos": np.stack([np.ones((h, w)), np.zeros((h, w)), np.zeros((h, w))]).astype(np.float32)}
|
| 92 |
+
res = analyze(p, PostConfig())
|
| 93 |
+
assert res.cells == [] and res.nostrils == [] and sum(res.counts.values()) == 0
|
tests/test_review_fixes.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Regression tests for the defects found in the adversarial review."""
|
| 2 |
+
import os
|
| 3 |
+
import zipfile
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import pytest
|
| 7 |
+
import tifffile
|
| 8 |
+
from PIL import Image
|
| 9 |
+
|
| 10 |
+
from cellquant.annotations import discover, load_image, normalized_name, short_keys, to_rgb_uint8
|
| 11 |
+
from cellquant.data import AugConfig, Prefetcher, make_targets
|
| 12 |
+
from cellquant.annotations import RasterAnnotation
|
| 13 |
+
from cellquant.metrics import match_by_iou
|
| 14 |
+
from cellquant.postprocess import PostConfig, _fill_and_filter, analyze
|
| 15 |
+
|
| 16 |
+
XML = """<?xml version="1.0" encoding="utf-8"?>
|
| 17 |
+
<annotations><version>1.1</version><meta><task><size>1</size></task></meta>
|
| 18 |
+
<image id="0" name="{name}" width="{w}" height="{h}">
|
| 19 |
+
<polyline label="Cell, Antibody +" source="manual" occluded="0" points="10,10;30,10;30,30;10,30;10,10" z_order="0"></polyline>
|
| 20 |
+
</image></annotations>"""
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _rgb(h=64, w=64):
|
| 24 |
+
a = np.zeros((h, w, 3), np.uint8)
|
| 25 |
+
a[..., 0], a[..., 1], a[..., 2] = 200, 100, 50
|
| 26 |
+
return a
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def test_channel_per_page_tif_loads_three_channels(tmp_path):
|
| 30 |
+
rgb = _rgb()
|
| 31 |
+
p1 = tmp_path / "ij.tif"
|
| 32 |
+
tifffile.imwrite(str(p1), np.moveaxis(rgb, -1, 0), imagej=True, metadata={"axes": "CYX"})
|
| 33 |
+
p2 = tmp_path / "ome.ome.tif"
|
| 34 |
+
tifffile.imwrite(str(p2), np.moveaxis(rgb, -1, 0), ome=True, metadata={"axes": "CYX"})
|
| 35 |
+
for p in (p1, p2):
|
| 36 |
+
a = load_image(str(p))
|
| 37 |
+
assert a.shape == (64, 64, 3)
|
| 38 |
+
assert a[1, 1].tolist() == [200, 100, 50], str(p)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def test_plain_tif_still_loads_and_large_stack_refused(tmp_path):
|
| 42 |
+
p = tmp_path / "plain.tif"
|
| 43 |
+
tifffile.imwrite(str(p), _rgb())
|
| 44 |
+
assert load_image(str(p))[0, 0].tolist() == [200, 100, 50]
|
| 45 |
+
stack = tmp_path / "stack.tif"
|
| 46 |
+
tifffile.imwrite(str(stack), np.zeros((7, 32, 32), np.uint8), imagej=True, metadata={"axes": "ZYX"})
|
| 47 |
+
with pytest.raises(ValueError):
|
| 48 |
+
load_image(str(stack))
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def test_lzw_tif_via_pillow_fallback_or_codecs(tmp_path):
|
| 52 |
+
p = tmp_path / "lzw.tif"
|
| 53 |
+
Image.fromarray(_rgb()).save(str(p), compression="tiff_lzw")
|
| 54 |
+
assert load_image(str(p))[2, 2].tolist() == [200, 100, 50]
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def test_palette_png_converted_to_rgb(tmp_path):
|
| 58 |
+
p = tmp_path / "pal.png"
|
| 59 |
+
Image.fromarray(_rgb()).convert("P", palette=Image.ADAPTIVE, colors=4).save(str(p))
|
| 60 |
+
a = load_image(str(p))
|
| 61 |
+
assert a.shape == (64, 64, 3)
|
| 62 |
+
assert not (np.array_equal(a[..., 0], a[..., 1]) and np.array_equal(a[..., 1], a[..., 2]))
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def test_16bit_scaling_ignores_single_saturated_pixel():
|
| 66 |
+
a = np.full((64, 64, 3), 1000, np.uint16)
|
| 67 |
+
b = a.copy()
|
| 68 |
+
b[0, 0, :] = 65535
|
| 69 |
+
ua, ub = to_rgb_uint8(a), to_rgb_uint8(b)
|
| 70 |
+
assert ua[10, 10, 0] == 255 and ub[10, 10, 0] == 255 # the one hot pixel does not change the gain
|
| 71 |
+
assert to_rgb_uint8(np.full((8, 8, 3), 0.5, np.float32))[0, 0, 0] in (127, 128)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def test_enclosed_instance_survives_and_ids_are_contiguous():
|
| 75 |
+
inst = np.zeros((40, 40), np.int32)
|
| 76 |
+
yy, xx = np.mgrid[:40, :40]
|
| 77 |
+
ring = ((yy - 20) ** 2 + (xx - 20) ** 2 <= 15 ** 2) & ((yy - 20) ** 2 + (xx - 20) ** 2 > 8 ** 2)
|
| 78 |
+
inner = (yy - 20) ** 2 + (xx - 20) ** 2 <= 6 ** 2
|
| 79 |
+
inst[ring] = 1
|
| 80 |
+
inst[inner] = 2
|
| 81 |
+
out = _fill_and_filter(inst, min_area=5)
|
| 82 |
+
ids = np.unique(out)
|
| 83 |
+
assert ids.tolist() == [0, 1, 2]
|
| 84 |
+
assert out[20, 20] == 2 and out[20, 7] == 1
|
| 85 |
+
# the 2-px moat between ring and inner disc is background in the input and gets filled into the ring
|
| 86 |
+
assert out[20, 13] == 1
|
| 87 |
+
# analyze() must not crash on such a map either
|
| 88 |
+
h = w = 40
|
| 89 |
+
p_cell = np.zeros((3, h, w), np.float32)
|
| 90 |
+
p_cell[1][inner] = 0.9
|
| 91 |
+
p_cell[1][ring] = 0.9
|
| 92 |
+
p_cell[2] = 0
|
| 93 |
+
p_cell[0] = 1 - p_cell[1]
|
| 94 |
+
probs = {"cell": p_cell, "antibody": np.zeros((h, w), np.float32), "cellnos": np.zeros((h, w), np.float32),
|
| 95 |
+
"nos": np.stack([np.ones((h, w)), np.zeros((h, w)), np.zeros((h, w))]).astype(np.float32)}
|
| 96 |
+
res = analyze(probs, PostConfig(min_cell_area=5))
|
| 97 |
+
assert len(res.cells) == int(res.cell_inst.max())
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def test_match_by_iou_prefers_number_of_valid_matches():
|
| 101 |
+
# GT A overlaps pred X with IoU 0.6 and pred Y with IoU 0.45; GT B overlaps X with 0.45.
|
| 102 |
+
# Summed-IoU assignment would pick (A,Y)+(B,X)=0.9 < (A,X)=0.6 ... make the sums favour the wrong choice:
|
| 103 |
+
gt = np.zeros((30, 60), np.int32)
|
| 104 |
+
pred = np.zeros((30, 60), np.int32)
|
| 105 |
+
gt[0:10, 0:20] = 1 # A: 200 px
|
| 106 |
+
gt[10:20, 0:20] = 2 # B: 200 px
|
| 107 |
+
pred[0:10, 0:20] = 1 # X == A exactly (IoU 1.0 with A)
|
| 108 |
+
pred[5:15, 0:20] = 2 # Y straddles A and B (IoU 100/300 = 0.33 with each)
|
| 109 |
+
pred[0:10, 0:20] = 1 # X overwrites the top of Y
|
| 110 |
+
m = match_by_iou(gt, pred, 0.5)
|
| 111 |
+
assert (1, 1) in [(g, p) for g, p, _ in m]
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def test_short_keys_disambiguate_wells():
|
| 115 |
+
names = ["C110 F 35D SC Iba W001 - Focused EDF Document_RGB copy.tif", "C110 F 35D SC Iba W003 - Focused EDF Document_RGB.tif", "F99 M 35D SC Iba W004 - Focused EDF Document_RGB.tif"]
|
| 116 |
+
assert short_keys(names) == ["C110_W001", "C110_W003", "F99"]
|
| 117 |
+
assert normalized_name("F114 F 35D SC Iba W002 - Focused EDF Document_RGB copy (1).tif") == "f114 f 35d sc iba w002 - focused edf document_rgb"
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def test_discover_two_sections_same_animal_and_size_check(tmp_path):
|
| 121 |
+
for n in ("C110 F 35D SC Iba W001 - RGB.png", "C110 F 35D SC Iba W003 - RGB.png"):
|
| 122 |
+
Image.fromarray(np.zeros((40, 50, 3), np.uint8)).save(tmp_path / n)
|
| 123 |
+
for i, n in enumerate(("C110 F 35D SC Iba W001 - RGB copy.tif", "C110 F 35D SC Iba W003 - RGB.tif")):
|
| 124 |
+
with zipfile.ZipFile(tmp_path / f"ann{i}.zip", "w") as zf:
|
| 125 |
+
zf.writestr("annotations.xml", XML.format(name=n, w=50, h=40))
|
| 126 |
+
samples, unann = discover(str(tmp_path), verbose=False)
|
| 127 |
+
assert [s.key for s in samples] == ["C110_W001", "C110_W003"] and unann == []
|
| 128 |
+
# a mismatching canvas is refused
|
| 129 |
+
with zipfile.ZipFile(tmp_path / "ann0.zip", "w") as zf:
|
| 130 |
+
zf.writestr("annotations.xml", XML.format(name="C110 F 35D SC Iba W001 - RGB.tif", w=500, h=400))
|
| 131 |
+
with pytest.raises(ValueError):
|
| 132 |
+
discover(str(tmp_path), verbose=False)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def test_discover_refuses_two_different_images_with_same_name(tmp_path):
|
| 136 |
+
Image.fromarray(np.zeros((40, 50, 3), np.uint8)).save(tmp_path / "K1 a - RGB.png")
|
| 137 |
+
Image.fromarray(np.ones((40, 50, 3), np.uint8)).save(tmp_path / "K1 a - RGB copy.png")
|
| 138 |
+
with zipfile.ZipFile(tmp_path / "ann.zip", "w") as zf:
|
| 139 |
+
zf.writestr("annotations.xml", XML.format(name="K1 a - RGB.png", w=50, h=40))
|
| 140 |
+
with pytest.raises(ValueError):
|
| 141 |
+
discover(str(tmp_path), verbose=False)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def test_prefetcher_is_deterministic():
|
| 145 |
+
inst = np.zeros((64, 64), np.int32)
|
| 146 |
+
inst[10:30, 10:30] = 1
|
| 147 |
+
R = RasterAnnotation(inst, ["", "Cell, Antibody +"], np.zeros((64, 64), np.int32), [""])
|
| 148 |
+
img = (np.random.default_rng(0).random((64, 64, 3)) * 255).astype(np.uint8)
|
| 149 |
+
T = make_targets(R)
|
| 150 |
+
def stream(seed):
|
| 151 |
+
pf = Prefetcher([img], [T], AugConfig(crop=32), batch=2, seed=seed, workers=3, depth=6)
|
| 152 |
+
out = [pf.next()[0].numpy().copy() for _ in range(9)]
|
| 153 |
+
pf.close()
|
| 154 |
+
return out
|
| 155 |
+
a, b = stream(4), stream(4)
|
| 156 |
+
assert all(np.array_equal(x, y) for x, y in zip(a, b))
|
| 157 |
+
assert not all(np.array_equal(x, y) for x, y in zip(a, stream(5)))
|
train.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Train CellQuant on CVAT-annotated images.
|
| 3 |
+
|
| 4 |
+
Examples
|
| 5 |
+
--------
|
| 6 |
+
Final model on every annotated image:
|
| 7 |
+
python train.py --data-dir /path/to/images_and_zips --out weights/cellquant.pt
|
| 8 |
+
Leave-one-image-out fold (held-out image is only used for monitoring):
|
| 9 |
+
python train.py --data-dir ... --holdout C110 --out runs/fold_C110.pt
|
| 10 |
+
"""
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import argparse
|
| 14 |
+
import json
|
| 15 |
+
import math
|
| 16 |
+
import os
|
| 17 |
+
import sys
|
| 18 |
+
import time
|
| 19 |
+
|
| 20 |
+
import numpy as np
|
| 21 |
+
import torch
|
| 22 |
+
|
| 23 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 24 |
+
from cellquant.annotations import discover, load_image, rasterize # noqa: E402
|
| 25 |
+
from cellquant.data import AugConfig, Prefetcher, make_targets # noqa: E402
|
| 26 |
+
from cellquant.infer import Predictor, pick_device # noqa: E402
|
| 27 |
+
from cellquant.losses import multitask_loss # noqa: E402
|
| 28 |
+
from cellquant.metrics import evaluate_cells, evaluate_nostrils # noqa: E402
|
| 29 |
+
from cellquant.model import CellUNet, save_checkpoint # noqa: E402
|
| 30 |
+
from cellquant.postprocess import PostConfig, analyze # noqa: E402
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def parse_args(argv=None):
|
| 34 |
+
ap = argparse.ArgumentParser()
|
| 35 |
+
ap.add_argument("--data-dir", required=True)
|
| 36 |
+
ap.add_argument("--out", required=True)
|
| 37 |
+
ap.add_argument("--holdout", default="", help="image key to leave out (e.g. C110); empty = train on all")
|
| 38 |
+
ap.add_argument("--iters", type=int, default=3000)
|
| 39 |
+
ap.add_argument("--batch", type=int, default=4)
|
| 40 |
+
ap.add_argument("--crop", type=int, default=512)
|
| 41 |
+
ap.add_argument("--lr", type=float, default=3e-4)
|
| 42 |
+
ap.add_argument("--weight-decay", type=float, default=1e-4)
|
| 43 |
+
ap.add_argument("--warmup", type=int, default=100)
|
| 44 |
+
ap.add_argument("--encoder", default="resnet34")
|
| 45 |
+
ap.add_argument("--no-pretrained", action="store_true")
|
| 46 |
+
ap.add_argument("--seed", type=int, default=0)
|
| 47 |
+
ap.add_argument("--device", default="auto")
|
| 48 |
+
ap.add_argument("--log-every", type=int, default=50)
|
| 49 |
+
ap.add_argument("--val-every", type=int, default=500)
|
| 50 |
+
ap.add_argument("--workers", type=int, default=3, help="augmentation threads")
|
| 51 |
+
return ap.parse_args(argv)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def lr_at(it: int, total: int, base: float, warmup: int) -> float:
|
| 55 |
+
if it < warmup:
|
| 56 |
+
return base * (it + 1) / warmup
|
| 57 |
+
t = (it - warmup) / max(1, total - warmup)
|
| 58 |
+
return base * (0.02 + 0.98 * 0.5 * (1 + math.cos(math.pi * t)))
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def quick_val(model: CellUNet, img: np.ndarray, R, device) -> dict:
|
| 62 |
+
pred = Predictor(model, device=str(device), tta=1)
|
| 63 |
+
res, _ = pred.predict(img, PostConfig())
|
| 64 |
+
ce = evaluate_cells(R.cell_inst, R.cell_labels, res.cell_inst, res.cell_labels)
|
| 65 |
+
ne = evaluate_nostrils(R.nostril_inst, R.nostril_labels, res.nostril_inst, res.nostril_labels)
|
| 66 |
+
return {
|
| 67 |
+
"cell_det_f1": ce["detection"]["f1"],
|
| 68 |
+
"cell_attr_both": ce["attribute_accuracy"]["both"],
|
| 69 |
+
"nos_det_f1": ne["detection"]["f1"],
|
| 70 |
+
"nos_type_acc": ne["type_accuracy"],
|
| 71 |
+
"n_pred_cells": ce["n_pred"], "n_gt_cells": ce["n_gt"],
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def main(argv=None):
|
| 76 |
+
a = parse_args(argv)
|
| 77 |
+
torch.manual_seed(a.seed)
|
| 78 |
+
np.random.seed(a.seed)
|
| 79 |
+
device = pick_device(a.device)
|
| 80 |
+
samples, _ = discover(a.data_dir)
|
| 81 |
+
if not samples:
|
| 82 |
+
raise SystemExit("no annotated images found")
|
| 83 |
+
train = [s for s in samples if s.key != a.holdout]
|
| 84 |
+
held = [s for s in samples if s.key == a.holdout]
|
| 85 |
+
if a.holdout and not held:
|
| 86 |
+
raise SystemExit(f"holdout {a.holdout!r} not among {[s.key for s in samples]}")
|
| 87 |
+
print(f"device={device} train={[s.key for s in train]} holdout={[s.key for s in held]}")
|
| 88 |
+
images, targets = [], []
|
| 89 |
+
for s in train:
|
| 90 |
+
img = load_image(s.image_path)
|
| 91 |
+
R = rasterize(s.annotation, img.shape[0], img.shape[1])
|
| 92 |
+
images.append(img)
|
| 93 |
+
targets.append(make_targets(R))
|
| 94 |
+
sampler = Prefetcher(images, targets, AugConfig(crop=a.crop), batch=a.batch, seed=a.seed, workers=a.workers)
|
| 95 |
+
held_img = held_R = None
|
| 96 |
+
if held:
|
| 97 |
+
held_img = load_image(held[0].image_path)
|
| 98 |
+
held_R = rasterize(held[0].annotation, held_img.shape[0], held_img.shape[1])
|
| 99 |
+
|
| 100 |
+
model = CellUNet(a.encoder, pretrained=not a.no_pretrained).to(device)
|
| 101 |
+
opt = torch.optim.AdamW(model.parameters(), lr=a.lr, weight_decay=a.weight_decay)
|
| 102 |
+
history = {"train": [], "val": []}
|
| 103 |
+
t0 = time.time()
|
| 104 |
+
run = {}
|
| 105 |
+
for it in range(a.iters):
|
| 106 |
+
lr = lr_at(it, a.iters, a.lr, a.warmup)
|
| 107 |
+
for g in opt.param_groups:
|
| 108 |
+
g["lr"] = lr
|
| 109 |
+
model.train()
|
| 110 |
+
x, t = sampler.next()
|
| 111 |
+
out = model(x.to(device))
|
| 112 |
+
loss, parts = multitask_loss(out, t)
|
| 113 |
+
opt.zero_grad(set_to_none=True)
|
| 114 |
+
loss.backward()
|
| 115 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0)
|
| 116 |
+
opt.step()
|
| 117 |
+
for k, v in parts.items():
|
| 118 |
+
run[k] = run.get(k, 0.0) + v
|
| 119 |
+
if (it + 1) % a.log_every == 0:
|
| 120 |
+
avg = {k: v / a.log_every for k, v in run.items()}
|
| 121 |
+
run = {}
|
| 122 |
+
avg["iter"] = it + 1
|
| 123 |
+
avg["lr"] = lr
|
| 124 |
+
avg["elapsed_s"] = time.time() - t0
|
| 125 |
+
history["train"].append(avg)
|
| 126 |
+
print(f"it {it+1:5d} loss {avg['total']:.3f} cell {avg['ce_cell']:.3f}/{avg['dice_cell']:.3f} "
|
| 127 |
+
f"nos {avg['ce_nos']:.3f}/{avg['dice_nos']:.3f} ab {avg['bce_ab']:.3f} cn {avg['bce_cn']:.3f} "
|
| 128 |
+
f"lr {lr:.2e} {avg['elapsed_s']:.0f}s", flush=True)
|
| 129 |
+
if held and ((it + 1) % a.val_every == 0 or it + 1 == a.iters):
|
| 130 |
+
model.eval()
|
| 131 |
+
v = quick_val(model, held_img, held_R, device)
|
| 132 |
+
v["iter"] = it + 1
|
| 133 |
+
history["val"].append(v)
|
| 134 |
+
print(f" val[{a.holdout}] cell F1 {v['cell_det_f1']:.3f} attr {v['cell_attr_both']:.3f} "
|
| 135 |
+
f"nos F1 {v['nos_det_f1']:.3f} type {v['nos_type_acc']:.3f} (pred {v['n_pred_cells']} / gt {v['n_gt_cells']} cells)", flush=True)
|
| 136 |
+
sampler.close()
|
| 137 |
+
model.eval().cpu()
|
| 138 |
+
os.makedirs(os.path.dirname(os.path.abspath(a.out)), exist_ok=True)
|
| 139 |
+
save_checkpoint(model, a.out, {
|
| 140 |
+
"config": vars(a),
|
| 141 |
+
"train_keys": [s.key for s in train],
|
| 142 |
+
"holdout": a.holdout,
|
| 143 |
+
"history": history,
|
| 144 |
+
"labels": list(__import__("cellquant.labels", fromlist=["ALL_LABELS"]).ALL_LABELS),
|
| 145 |
+
})
|
| 146 |
+
with open(os.path.splitext(a.out)[0] + "_history.json", "w") as f:
|
| 147 |
+
json.dump(history, f, indent=1)
|
| 148 |
+
print(f"saved {a.out} ({time.time()-t0:.0f}s)")
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
if __name__ == "__main__":
|
| 152 |
+
main()
|
weights/cellquant.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b6d252d9f4c03aedcf717649cc766fb47a503acc7cca8135b8d43bc684838959
|
| 3 |
+
size 98290507
|
weights/cellquant_history.json
ADDED
|
@@ -0,0 +1,725 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"train": [
|
| 3 |
+
{
|
| 4 |
+
"ce_cell": 0.9070329773426056,
|
| 5 |
+
"dice_cell": 0.8730328452587127,
|
| 6 |
+
"ce_nos": 1.0830040109157562,
|
| 7 |
+
"dice_nos": 0.9898272252082825,
|
| 8 |
+
"bce_ab": 0.524188295006752,
|
| 9 |
+
"bce_cn": 0.6183760815858841,
|
| 10 |
+
"total": 4.9954613590240475,
|
| 11 |
+
"iter": 50,
|
| 12 |
+
"lr": 0.00015,
|
| 13 |
+
"elapsed_s": 59.566773891448975
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"ce_cell": 0.7300815117359162,
|
| 17 |
+
"dice_cell": 0.8212458372116089,
|
| 18 |
+
"ce_nos": 0.7789127671718598,
|
| 19 |
+
"dice_nos": 0.9821323096752167,
|
| 20 |
+
"bce_ab": 0.2914739960432053,
|
| 21 |
+
"bce_cn": 0.3936499571800232,
|
| 22 |
+
"total": 3.9974963092803955,
|
| 23 |
+
"iter": 100,
|
| 24 |
+
"lr": 0.0003,
|
| 25 |
+
"elapsed_s": 109.73091292381287
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"ce_cell": 0.5734736859798432,
|
| 29 |
+
"dice_cell": 0.7798113775253296,
|
| 30 |
+
"ce_nos": 0.574396123290062,
|
| 31 |
+
"dice_nos": 0.9748807692527771,
|
| 32 |
+
"bce_ab": 0.23322827845811844,
|
| 33 |
+
"bce_cn": 0.33575250566005704,
|
| 34 |
+
"total": 3.4715427684783937,
|
| 35 |
+
"iter": 150,
|
| 36 |
+
"lr": 0.00029979294712266676,
|
| 37 |
+
"elapsed_s": 160.73545289039612
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"ce_cell": 0.44446682393550874,
|
| 41 |
+
"dice_cell": 0.7253605389595031,
|
| 42 |
+
"ce_nos": 0.4329249274730682,
|
| 43 |
+
"dice_nos": 0.9701189231872559,
|
| 44 |
+
"bce_ab": 0.20956841707229615,
|
| 45 |
+
"bce_cn": 0.32494749456644056,
|
| 46 |
+
"total": 3.107387113571167,
|
| 47 |
+
"iter": 200,
|
| 48 |
+
"lr": 0.0002991554114956718,
|
| 49 |
+
"elapsed_s": 211.49982213974
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"ce_cell": 0.35687846958637237,
|
| 53 |
+
"dice_cell": 0.6721045792102813,
|
| 54 |
+
"ce_nos": 0.32840390503406525,
|
| 55 |
+
"dice_nos": 0.9639902281761169,
|
| 56 |
+
"bce_ab": 0.17408027544617652,
|
| 57 |
+
"bce_cn": 0.3147788044810295,
|
| 58 |
+
"total": 2.810236248970032,
|
| 59 |
+
"iter": 250,
|
| 60 |
+
"lr": 0.00029808917686451123,
|
| 61 |
+
"elapsed_s": 255.0301797389984
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"ce_cell": 0.30268543839454654,
|
| 65 |
+
"dice_cell": 0.6246984755992889,
|
| 66 |
+
"ce_nos": 0.24472756624221803,
|
| 67 |
+
"dice_nos": 0.9488238263130188,
|
| 68 |
+
"bce_ab": 0.1679477596282959,
|
| 69 |
+
"bce_cn": 0.264477858543396,
|
| 70 |
+
"total": 2.5533609533309938,
|
| 71 |
+
"iter": 300,
|
| 72 |
+
"lr": 0.00029659737067906796,
|
| 73 |
+
"elapsed_s": 305.7051999568939
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"ce_cell": 0.2692413151264191,
|
| 77 |
+
"dice_cell": 0.5903848171234131,
|
| 78 |
+
"ce_nos": 0.18806805282831193,
|
| 79 |
+
"dice_nos": 0.9169448506832123,
|
| 80 |
+
"bce_ab": 0.1362369504570961,
|
| 81 |
+
"bce_cn": 0.2647552129626274,
|
| 82 |
+
"total": 2.365631227493286,
|
| 83 |
+
"iter": 350,
|
| 84 |
+
"lr": 0.00029468436866391876,
|
| 85 |
+
"elapsed_s": 356.85323691368103
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"ce_cell": 0.24572547733783723,
|
| 89 |
+
"dice_cell": 0.5541446590423584,
|
| 90 |
+
"ce_nos": 0.14375351637601852,
|
| 91 |
+
"dice_nos": 0.8989665198326111,
|
| 92 |
+
"bce_ab": 0.1442735657095909,
|
| 93 |
+
"bce_cn": 0.25428884506225585,
|
| 94 |
+
"total": 2.241152620315552,
|
| 95 |
+
"iter": 400,
|
| 96 |
+
"lr": 0.0002923557819835801,
|
| 97 |
+
"elapsed_s": 408.5768549442291
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
"ce_cell": 0.23625801086425782,
|
| 101 |
+
"dice_cell": 0.5364066225290298,
|
| 102 |
+
"ce_nos": 0.11115477636456489,
|
| 103 |
+
"dice_nos": 0.87745032787323,
|
| 104 |
+
"bce_ab": 0.1183611673116684,
|
| 105 |
+
"bce_cn": 0.22456467717885972,
|
| 106 |
+
"total": 2.1041955590248107,
|
| 107 |
+
"iter": 450,
|
| 108 |
+
"lr": 0.00028961844078399557,
|
| 109 |
+
"elapsed_s": 459.0741550922394
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
"ce_cell": 0.21822448045015336,
|
| 113 |
+
"dice_cell": 0.5127934575080871,
|
| 114 |
+
"ce_nos": 0.0872543377429247,
|
| 115 |
+
"dice_nos": 0.8334435498714448,
|
| 116 |
+
"bce_ab": 0.1045101822167635,
|
| 117 |
+
"bce_cn": 0.2290149673819542,
|
| 118 |
+
"total": 1.9852409744262696,
|
| 119 |
+
"iter": 500,
|
| 120 |
+
"lr": 0.0002864803741585413,
|
| 121 |
+
"elapsed_s": 509.7258110046387
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"ce_cell": 0.21543064922094346,
|
| 125 |
+
"dice_cell": 0.49275926291942596,
|
| 126 |
+
"ce_nos": 0.07010223433375358,
|
| 127 |
+
"dice_nos": 0.795907906293869,
|
| 128 |
+
"bce_ab": 0.10423918575048446,
|
| 129 |
+
"bce_cn": 0.21024613380432128,
|
| 130 |
+
"total": 1.8886853814125062,
|
| 131 |
+
"iter": 550,
|
| 132 |
+
"lr": 0.0002829507865973116,
|
| 133 |
+
"elapsed_s": 560.3340859413147
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"ce_cell": 0.21174423784017563,
|
| 137 |
+
"dice_cell": 0.4891924291849136,
|
| 138 |
+
"ce_nos": 0.06100128136575222,
|
| 139 |
+
"dice_nos": 0.7548426115512847,
|
| 140 |
+
"bce_ab": 0.09695948652923107,
|
| 141 |
+
"bce_cn": 0.20541400745511054,
|
| 142 |
+
"total": 1.8191540384292602,
|
| 143 |
+
"iter": 600,
|
| 144 |
+
"lr": 0.0002790400309887635,
|
| 145 |
+
"elapsed_s": 610.7946240901947
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"ce_cell": 0.20390785247087478,
|
| 149 |
+
"dice_cell": 0.4746441453695297,
|
| 150 |
+
"ce_nos": 0.04720834020525217,
|
| 151 |
+
"dice_nos": 0.709533805847168,
|
| 152 |
+
"bce_ab": 0.09941868580877782,
|
| 153 |
+
"bce_cn": 0.19302350237965585,
|
| 154 |
+
"total": 1.727736325263977,
|
| 155 |
+
"iter": 650,
|
| 156 |
+
"lr": 0.0002747595782529114,
|
| 157 |
+
"elapsed_s": 660.7677178382874
|
| 158 |
+
},
|
| 159 |
+
{
|
| 160 |
+
"ce_cell": 0.2039007693529129,
|
| 161 |
+
"dice_cell": 0.46679477393627167,
|
| 162 |
+
"ce_nos": 0.048075792603194716,
|
| 163 |
+
"dice_nos": 0.6227267402410507,
|
| 164 |
+
"bce_ab": 0.08658930584788323,
|
| 165 |
+
"bce_cn": 0.18947327509522438,
|
| 166 |
+
"total": 1.617560646533966,
|
| 167 |
+
"iter": 700,
|
| 168 |
+
"lr": 0.0002701219836951418,
|
| 169 |
+
"elapsed_s": 711.0392649173737
|
| 170 |
+
},
|
| 171 |
+
{
|
| 172 |
+
"ce_cell": 0.200768101811409,
|
| 173 |
+
"dice_cell": 0.45868332326412203,
|
| 174 |
+
"ce_nos": 0.03979761004447937,
|
| 175 |
+
"dice_nos": 0.5989265680313111,
|
| 176 |
+
"bce_ab": 0.07302961058914662,
|
| 177 |
+
"bce_cn": 0.1712094095349312,
|
| 178 |
+
"total": 1.5424146223068238,
|
| 179 |
+
"iter": 750,
|
| 180 |
+
"lr": 0.0002651408501793399,
|
| 181 |
+
"elapsed_s": 760.5930259227753
|
| 182 |
+
},
|
| 183 |
+
{
|
| 184 |
+
"ce_cell": 0.19177152156829835,
|
| 185 |
+
"dice_cell": 0.4472357839345932,
|
| 186 |
+
"ce_nos": 0.03094205627217889,
|
| 187 |
+
"dice_nos": 0.5603820198774337,
|
| 188 |
+
"bce_ab": 0.08034474328160286,
|
| 189 |
+
"bce_cn": 0.14997318997979164,
|
| 190 |
+
"total": 1.4606493067741395,
|
| 191 |
+
"iter": 800,
|
| 192 |
+
"lr": 0.0002598307882283455,
|
| 193 |
+
"elapsed_s": 810.9303019046783
|
| 194 |
+
},
|
| 195 |
+
{
|
| 196 |
+
"ce_cell": 0.19938127607107162,
|
| 197 |
+
"dice_cell": 0.44567417323589326,
|
| 198 |
+
"ce_nos": 0.04125866884365678,
|
| 199 |
+
"dice_nos": 0.4803364199399948,
|
| 200 |
+
"bce_ab": 0.07717311266809702,
|
| 201 |
+
"bce_cn": 0.1591106753051281,
|
| 202 |
+
"total": 1.4029343223571777,
|
| 203 |
+
"iter": 850,
|
| 204 |
+
"lr": 0.00025420737316877263,
|
| 205 |
+
"elapsed_s": 857.9806337356567
|
| 206 |
+
},
|
| 207 |
+
{
|
| 208 |
+
"ce_cell": 0.1933874309062958,
|
| 209 |
+
"dice_cell": 0.4387037259340286,
|
| 210 |
+
"ce_nos": 0.036275244057178496,
|
| 211 |
+
"dice_nos": 0.48662609457969663,
|
| 212 |
+
"bce_ab": 0.06898861289024354,
|
| 213 |
+
"bce_cn": 0.15267845779657363,
|
| 214 |
+
"total": 1.3766595673561097,
|
| 215 |
+
"iter": 900,
|
| 216 |
+
"lr": 0.00024828709944589296,
|
| 217 |
+
"elapsed_s": 908.7008759975433
|
| 218 |
+
},
|
| 219 |
+
{
|
| 220 |
+
"ce_cell": 0.19506525486707688,
|
| 221 |
+
"dice_cell": 0.4412110126018524,
|
| 222 |
+
"ce_nos": 0.035951650235801935,
|
| 223 |
+
"dice_nos": 0.43454607129096984,
|
| 224 |
+
"bce_ab": 0.06932192835956812,
|
| 225 |
+
"bce_cn": 0.14733665704727172,
|
| 226 |
+
"total": 1.32343257188797,
|
| 227 |
+
"iter": 950,
|
| 228 |
+
"lr": 0.00024208733224258537,
|
| 229 |
+
"elapsed_s": 959.4800848960876
|
| 230 |
+
},
|
| 231 |
+
{
|
| 232 |
+
"ce_cell": 0.1945480227470398,
|
| 233 |
+
"dice_cell": 0.43224972307682036,
|
| 234 |
+
"ce_nos": 0.0335944464802742,
|
| 235 |
+
"dice_nos": 0.40378140449523925,
|
| 236 |
+
"bce_ab": 0.06522910814732313,
|
| 237 |
+
"bce_cn": 0.13741043150424959,
|
| 238 |
+
"total": 1.2668131160736085,
|
| 239 |
+
"iter": 1000,
|
| 240 |
+
"lr": 0.0002356262565442632,
|
| 241 |
+
"elapsed_s": 1009.7932658195496
|
| 242 |
+
},
|
| 243 |
+
{
|
| 244 |
+
"ce_cell": 0.18734178960323333,
|
| 245 |
+
"dice_cell": 0.4267615270614624,
|
| 246 |
+
"ce_nos": 0.036363077107816937,
|
| 247 |
+
"dice_nos": 0.4053969383239746,
|
| 248 |
+
"bce_ab": 0.0643582896143198,
|
| 249 |
+
"bce_cn": 0.12727220386266708,
|
| 250 |
+
"total": 1.2474938321113587,
|
| 251 |
+
"iter": 1050,
|
| 252 |
+
"lr": 0.0002289228237991784,
|
| 253 |
+
"elapsed_s": 1059.9658198356628
|
| 254 |
+
},
|
| 255 |
+
{
|
| 256 |
+
"ce_cell": 0.18910773277282714,
|
| 257 |
+
"dice_cell": 0.42362256467342374,
|
| 258 |
+
"ce_nos": 0.0346462229359895,
|
| 259 |
+
"dice_nos": 0.3729756760597229,
|
| 260 |
+
"bce_ab": 0.06174593850970268,
|
| 261 |
+
"bce_cn": 0.13085844799876212,
|
| 262 |
+
"total": 1.2129565787315368,
|
| 263 |
+
"iter": 1100,
|
| 264 |
+
"lr": 0.0002219966963305587,
|
| 265 |
+
"elapsed_s": 1110.0916857719421
|
| 266 |
+
},
|
| 267 |
+
{
|
| 268 |
+
"ce_cell": 0.1822684535384178,
|
| 269 |
+
"dice_cell": 0.4143069595098495,
|
| 270 |
+
"ce_nos": 0.03322938298806548,
|
| 271 |
+
"dice_nos": 0.3389162081480026,
|
| 272 |
+
"bce_ab": 0.05988596722483635,
|
| 273 |
+
"bce_cn": 0.11884681917726994,
|
| 274 |
+
"total": 1.1474537920951844,
|
| 275 |
+
"iter": 1150,
|
| 276 |
+
"lr": 0.00021486818966362585,
|
| 277 |
+
"elapsed_s": 1160.7585599422455
|
| 278 |
+
},
|
| 279 |
+
{
|
| 280 |
+
"ce_cell": 0.18991790741682052,
|
| 281 |
+
"dice_cell": 0.41873910784721374,
|
| 282 |
+
"ce_nos": 0.036337544079869985,
|
| 283 |
+
"dice_nos": 0.34970403611660006,
|
| 284 |
+
"bce_ab": 0.05970212183892727,
|
| 285 |
+
"bce_cn": 0.1334019273519516,
|
| 286 |
+
"total": 1.1878026413917542,
|
| 287 |
+
"iter": 1200,
|
| 288 |
+
"lr": 0.00020755821293665946,
|
| 289 |
+
"elapsed_s": 1211.4072179794312
|
| 290 |
+
},
|
| 291 |
+
{
|
| 292 |
+
"ce_cell": 0.17697676301002502,
|
| 293 |
+
"dice_cell": 0.40928772032260896,
|
| 294 |
+
"ce_nos": 0.03304771558381617,
|
| 295 |
+
"dice_nos": 0.3399574244022369,
|
| 296 |
+
"bce_ab": 0.04348419822752476,
|
| 297 |
+
"bce_cn": 0.1121597220748663,
|
| 298 |
+
"total": 1.1149135494232179,
|
| 299 |
+
"iter": 1250,
|
| 300 |
+
"lr": 0.00020008820757089248,
|
| 301 |
+
"elapsed_s": 1262.1290378570557
|
| 302 |
+
},
|
| 303 |
+
{
|
| 304 |
+
"ce_cell": 0.17598221957683563,
|
| 305 |
+
"dice_cell": 0.40642625391483306,
|
| 306 |
+
"ce_nos": 0.033407045025378466,
|
| 307 |
+
"dice_nos": 0.310367751121521,
|
| 308 |
+
"bce_ab": 0.0478444230183959,
|
| 309 |
+
"bce_cn": 0.09985558047890664,
|
| 310 |
+
"total": 1.073883285522461,
|
| 311 |
+
"iter": 1300,
|
| 312 |
+
"lr": 0.0001924800843791277,
|
| 313 |
+
"elapsed_s": 1311.735403060913
|
| 314 |
+
},
|
| 315 |
+
{
|
| 316 |
+
"ce_cell": 0.18522560983896255,
|
| 317 |
+
"dice_cell": 0.403508597612381,
|
| 318 |
+
"ce_nos": 0.036090935450047254,
|
| 319 |
+
"dice_nos": 0.3127438998222351,
|
| 320 |
+
"bce_ab": 0.04713532615453005,
|
| 321 |
+
"bce_cn": 0.11126945935189723,
|
| 322 |
+
"total": 1.0959738206863403,
|
| 323 |
+
"iter": 1350,
|
| 324 |
+
"lr": 0.00018475615929754922,
|
| 325 |
+
"elapsed_s": 1362.2759177684784
|
| 326 |
+
},
|
| 327 |
+
{
|
| 328 |
+
"ce_cell": 0.18232185065746306,
|
| 329 |
+
"dice_cell": 0.40336430847644805,
|
| 330 |
+
"ce_nos": 0.0384017390338704,
|
| 331 |
+
"dice_nos": 0.30273705661296846,
|
| 332 |
+
"bce_ab": 0.04570051861926913,
|
| 333 |
+
"bce_cn": 0.11299202486872673,
|
| 334 |
+
"total": 1.0855175077915191,
|
| 335 |
+
"iter": 1400,
|
| 336 |
+
"lr": 0.0001769390879292361,
|
| 337 |
+
"elapsed_s": 1412.6486489772797
|
| 338 |
+
},
|
| 339 |
+
{
|
| 340 |
+
"ce_cell": 0.18443019971251487,
|
| 341 |
+
"dice_cell": 0.41006422102451323,
|
| 342 |
+
"ce_nos": 0.038777465261518956,
|
| 343 |
+
"dice_nos": 0.293556307554245,
|
| 344 |
+
"bce_ab": 0.040375117510557175,
|
| 345 |
+
"bce_cn": 0.0971392347663641,
|
| 346 |
+
"total": 1.0643425357341767,
|
| 347 |
+
"iter": 1450,
|
| 348 |
+
"lr": 0.00016905179909137546,
|
| 349 |
+
"elapsed_s": 1462.6795949935913
|
| 350 |
+
},
|
| 351 |
+
{
|
| 352 |
+
"ce_cell": 0.17869871109724045,
|
| 353 |
+
"dice_cell": 0.40553332567214967,
|
| 354 |
+
"ce_nos": 0.03795233623124659,
|
| 355 |
+
"dice_nos": 0.2916260594129562,
|
| 356 |
+
"bce_ab": 0.04099308693781495,
|
| 357 |
+
"bce_cn": 0.0865385101735592,
|
| 358 |
+
"total": 1.0413420403003693,
|
| 359 |
+
"iter": 1500,
|
| 360 |
+
"lr": 0.00016111742756109098,
|
| 361 |
+
"elapsed_s": 1512.4294548034668
|
| 362 |
+
},
|
| 363 |
+
{
|
| 364 |
+
"ce_cell": 0.1764592757821083,
|
| 365 |
+
"dice_cell": 0.3958728986978531,
|
| 366 |
+
"ce_nos": 0.035169387739151715,
|
| 367 |
+
"dice_nos": 0.27471464276313784,
|
| 368 |
+
"bce_ab": 0.035233155209571126,
|
| 369 |
+
"bce_cn": 0.08008314736187458,
|
| 370 |
+
"total": 0.9975325107574463,
|
| 371 |
+
"iter": 1550,
|
| 372 |
+
"lr": 0.00015315924621715527,
|
| 373 |
+
"elapsed_s": 1562.1074118614197
|
| 374 |
+
},
|
| 375 |
+
{
|
| 376 |
+
"ce_cell": 0.17475848585367204,
|
| 377 |
+
"dice_cell": 0.3929943698644638,
|
| 378 |
+
"ce_nos": 0.03353961421176791,
|
| 379 |
+
"dice_nos": 0.3009433788061142,
|
| 380 |
+
"bce_ab": 0.03854949928820133,
|
| 381 |
+
"bce_cn": 0.08339719414710998,
|
| 382 |
+
"total": 1.0241825413703918,
|
| 383 |
+
"iter": 1600,
|
| 384 |
+
"lr": 0.00014520059777662684,
|
| 385 |
+
"elapsed_s": 1612.9562838077545
|
| 386 |
+
},
|
| 387 |
+
{
|
| 388 |
+
"ce_cell": 0.18072757810354234,
|
| 389 |
+
"dice_cell": 0.39913213849067686,
|
| 390 |
+
"ce_nos": 0.039328129449859264,
|
| 391 |
+
"dice_nos": 0.2857843828201294,
|
| 392 |
+
"bce_ab": 0.03886710917577148,
|
| 393 |
+
"bce_cn": 0.09502572849392892,
|
| 394 |
+
"total": 1.0388650584220886,
|
| 395 |
+
"iter": 1650,
|
| 396 |
+
"lr": 0.00013726482632663912,
|
| 397 |
+
"elapsed_s": 1663.1288919448853
|
| 398 |
+
},
|
| 399 |
+
{
|
| 400 |
+
"ce_cell": 0.1759936460852623,
|
| 401 |
+
"dice_cell": 0.39650719165802,
|
| 402 |
+
"ce_nos": 0.032182310023345054,
|
| 403 |
+
"dice_nos": 0.2933516526222229,
|
| 404 |
+
"bce_ab": 0.03653882872313261,
|
| 405 |
+
"bce_cn": 0.08788134880363942,
|
| 406 |
+
"total": 1.0224549758434296,
|
| 407 |
+
"iter": 1700,
|
| 408 |
+
"lr": 0.00012937520885217038,
|
| 409 |
+
"elapsed_s": 1712.6260061264038
|
| 410 |
+
},
|
| 411 |
+
{
|
| 412 |
+
"ce_cell": 0.17498762905597687,
|
| 413 |
+
"dice_cell": 0.38849363803863524,
|
| 414 |
+
"ce_nos": 0.03151271438226104,
|
| 415 |
+
"dice_nos": 0.27745749831199645,
|
| 416 |
+
"bce_ab": 0.038630726784467696,
|
| 417 |
+
"bce_cn": 0.08467736341059208,
|
| 418 |
+
"total": 0.995759572982788,
|
| 419 |
+
"iter": 1750,
|
| 420 |
+
"lr": 0.0001215548869606366,
|
| 421 |
+
"elapsed_s": 1763.162414073944
|
| 422 |
+
},
|
| 423 |
+
{
|
| 424 |
+
"ce_cell": 0.16943098440766335,
|
| 425 |
+
"dice_cell": 0.38974578082561495,
|
| 426 |
+
"ce_nos": 0.030594841404818,
|
| 427 |
+
"dice_nos": 0.2775198197364807,
|
| 428 |
+
"bce_ab": 0.03063900684937835,
|
| 429 |
+
"bce_cn": 0.07809612043201923,
|
| 430 |
+
"total": 0.9760265576839448,
|
| 431 |
+
"iter": 1800,
|
| 432 |
+
"lr": 0.00011382679900356957,
|
| 433 |
+
"elapsed_s": 1812.9093608856201
|
| 434 |
+
},
|
| 435 |
+
{
|
| 436 |
+
"ce_cell": 0.17392412394285203,
|
| 437 |
+
"dice_cell": 0.39087122797966,
|
| 438 |
+
"ce_nos": 0.03287358325906098,
|
| 439 |
+
"dice_nos": 0.27309031009674073,
|
| 440 |
+
"bce_ab": 0.03879237989895046,
|
| 441 |
+
"bce_cn": 0.07375255063176155,
|
| 442 |
+
"total": 0.9833041846752166,
|
| 443 |
+
"iter": 1850,
|
| 444 |
+
"lr": 0.0001062136127944796,
|
| 445 |
+
"elapsed_s": 1862.8471250534058
|
| 446 |
+
},
|
| 447 |
+
{
|
| 448 |
+
"ce_cell": 0.1721265098452568,
|
| 449 |
+
"dice_cell": 0.3918482345342636,
|
| 450 |
+
"ce_nos": 0.03052111998666078,
|
| 451 |
+
"dice_nos": 0.26571854174137116,
|
| 452 |
+
"bce_ab": 0.028863372541964055,
|
| 453 |
+
"bce_cn": 0.07009109571576118,
|
| 454 |
+
"total": 0.9591688740253449,
|
| 455 |
+
"iter": 1900,
|
| 456 |
+
"lr": 9.873765912025276e-05,
|
| 457 |
+
"elapsed_s": 1913.0803048610687
|
| 458 |
+
},
|
| 459 |
+
{
|
| 460 |
+
"ce_cell": 0.17321533888578414,
|
| 461 |
+
"dice_cell": 0.3879319250583649,
|
| 462 |
+
"ce_nos": 0.03197065497748554,
|
| 463 |
+
"dice_nos": 0.2754987716674805,
|
| 464 |
+
"bce_ab": 0.03000295827165246,
|
| 465 |
+
"bce_cn": 0.0681322829797864,
|
| 466 |
+
"total": 0.966751925945282,
|
| 467 |
+
"iter": 1950,
|
| 468 |
+
"lr": 9.142086624110557e-05,
|
| 469 |
+
"elapsed_s": 1962.4520688056946
|
| 470 |
+
},
|
| 471 |
+
{
|
| 472 |
+
"ce_cell": 0.16933307886123658,
|
| 473 |
+
"dice_cell": 0.3823584771156311,
|
| 474 |
+
"ce_nos": 0.03379744639620185,
|
| 475 |
+
"dice_nos": 0.2684609520435333,
|
| 476 |
+
"bce_ab": 0.03405721362680197,
|
| 477 |
+
"bce_cn": 0.07046777069568634,
|
| 478 |
+
"total": 0.9584749519824982,
|
| 479 |
+
"iter": 2000,
|
| 480 |
+
"lr": 8.428469557121958e-05,
|
| 481 |
+
"elapsed_s": 2011.636960029602
|
| 482 |
+
},
|
| 483 |
+
{
|
| 484 |
+
"ce_cell": 0.17340840995311738,
|
| 485 |
+
"dice_cell": 0.3878352814912796,
|
| 486 |
+
"ce_nos": 0.03318483327515423,
|
| 487 |
+
"dice_nos": 0.2580646830797195,
|
| 488 |
+
"bce_ab": 0.029467624574899674,
|
| 489 |
+
"bce_cn": 0.07082617726176978,
|
| 490 |
+
"total": 0.9527869963645935,
|
| 491 |
+
"iter": 2050,
|
| 492 |
+
"lr": 7.735007872871564e-05,
|
| 493 |
+
"elapsed_s": 2061.974331855774
|
| 494 |
+
},
|
| 495 |
+
{
|
| 496 |
+
"ce_cell": 0.17320298582315444,
|
| 497 |
+
"dice_cell": 0.3853736054897308,
|
| 498 |
+
"ce_nos": 0.028748751198872925,
|
| 499 |
+
"dice_nos": 0.26544315218925474,
|
| 500 |
+
"bce_ab": 0.033497555926442144,
|
| 501 |
+
"bce_cn": 0.06046421557664871,
|
| 502 |
+
"total": 0.9467302620410919,
|
| 503 |
+
"iter": 2100,
|
| 504 |
+
"lr": 7.063735613961179e-05,
|
| 505 |
+
"elapsed_s": 2112.548271894455
|
| 506 |
+
},
|
| 507 |
+
{
|
| 508 |
+
"ce_cell": 0.1658191041648388,
|
| 509 |
+
"dice_cell": 0.3796718597412109,
|
| 510 |
+
"ce_nos": 0.03187917632050812,
|
| 511 |
+
"dice_nos": 0.26365615785121915,
|
| 512 |
+
"bce_ab": 0.030561734307557344,
|
| 513 |
+
"bce_cn": 0.05981527805328369,
|
| 514 |
+
"total": 0.9314033150672912,
|
| 515 |
+
"iter": 2150,
|
| 516 |
+
"lr": 6.416621737584896e-05,
|
| 517 |
+
"elapsed_s": 2162.667312860489
|
| 518 |
+
},
|
| 519 |
+
{
|
| 520 |
+
"ce_cell": 0.1701272311806679,
|
| 521 |
+
"dice_cell": 0.37863297820091246,
|
| 522 |
+
"ce_nos": 0.027262243572622538,
|
| 523 |
+
"dice_nos": 0.26668123960494994,
|
| 524 |
+
"bce_ab": 0.04351606283336878,
|
| 525 |
+
"bce_cn": 0.06579859033226967,
|
| 526 |
+
"total": 0.9520183408260345,
|
| 527 |
+
"iter": 2200,
|
| 528 |
+
"lr": 5.795564340238374e-05,
|
| 529 |
+
"elapsed_s": 2213.4271240234375
|
| 530 |
+
},
|
| 531 |
+
{
|
| 532 |
+
"ce_cell": 0.1703978794813156,
|
| 533 |
+
"dice_cell": 0.38433976292610167,
|
| 534 |
+
"ce_nos": 0.03619127416983247,
|
| 535 |
+
"dice_nos": 0.24586948692798616,
|
| 536 |
+
"bce_ab": 0.02381640898063779,
|
| 537 |
+
"bce_cn": 0.06208158168941736,
|
| 538 |
+
"total": 0.922696396112442,
|
| 539 |
+
"iter": 2250,
|
| 540 |
+
"lr": 5.202385090274833e-05,
|
| 541 |
+
"elapsed_s": 2263.827299118042
|
| 542 |
+
},
|
| 543 |
+
{
|
| 544 |
+
"ce_cell": 0.16755644142627715,
|
| 545 |
+
"dice_cell": 0.3778986144065857,
|
| 546 |
+
"ce_nos": 0.03260435258969665,
|
| 547 |
+
"dice_nos": 0.25375066459178924,
|
| 548 |
+
"bce_ab": 0.029958658767864108,
|
| 549 |
+
"bce_cn": 0.05888926636427641,
|
| 550 |
+
"total": 0.9206579995155334,
|
| 551 |
+
"iter": 2300,
|
| 552 |
+
"lr": 4.638823884637875e-05,
|
| 553 |
+
"elapsed_s": 2313.582683801651
|
| 554 |
+
},
|
| 555 |
+
{
|
| 556 |
+
"ce_cell": 0.1692182320356369,
|
| 557 |
+
"dice_cell": 0.3797942841053009,
|
| 558 |
+
"ce_nos": 0.031096484493464232,
|
| 559 |
+
"dice_nos": 0.25585519909858706,
|
| 560 |
+
"bce_ab": 0.025209803096950055,
|
| 561 |
+
"bce_cn": 0.06410345062613487,
|
| 562 |
+
"total": 0.925277452468872,
|
| 563 |
+
"iter": 2350,
|
| 564 |
+
"lr": 4.106533745443955e-05,
|
| 565 |
+
"elapsed_s": 2366.780472755432
|
| 566 |
+
},
|
| 567 |
+
{
|
| 568 |
+
"ce_cell": 0.1647430169582367,
|
| 569 |
+
"dice_cell": 0.37565741121768953,
|
| 570 |
+
"ce_nos": 0.029058436192572118,
|
| 571 |
+
"dice_nos": 0.2536770069599152,
|
| 572 |
+
"bce_ab": 0.026647686092182994,
|
| 573 |
+
"bce_cn": 0.05636850781738758,
|
| 574 |
+
"total": 0.9061520624160767,
|
| 575 |
+
"iter": 2400,
|
| 576 |
+
"lr": 3.607075971383718e-05,
|
| 577 |
+
"elapsed_s": 2420.7595388889313
|
| 578 |
+
},
|
| 579 |
+
{
|
| 580 |
+
"ce_cell": 0.1587481266260147,
|
| 581 |
+
"dice_cell": 0.37386213421821596,
|
| 582 |
+
"ce_nos": 0.027811604756861924,
|
| 583 |
+
"dice_nos": 0.2525290942192078,
|
| 584 |
+
"bce_ab": 0.027055934872478246,
|
| 585 |
+
"bce_cn": 0.05388368744403124,
|
| 586 |
+
"total": 0.8938905835151673,
|
| 587 |
+
"iter": 2450,
|
| 588 |
+
"lr": 3.1419155581639394e-05,
|
| 589 |
+
"elapsed_s": 2472.9100120067596
|
| 590 |
+
},
|
| 591 |
+
{
|
| 592 |
+
"ce_cell": 0.16981444805860518,
|
| 593 |
+
"dice_cell": 0.3800900149345398,
|
| 594 |
+
"ce_nos": 0.02935561618767679,
|
| 595 |
+
"dice_nos": 0.2700779044628143,
|
| 596 |
+
"bce_ab": 0.026528375521302223,
|
| 597 |
+
"bce_cn": 0.05692515041679144,
|
| 598 |
+
"total": 0.932791496515274,
|
| 599 |
+
"iter": 2500,
|
| 600 |
+
"lr": 2.7124169014226923e-05,
|
| 601 |
+
"elapsed_s": 2525.758083820343
|
| 602 |
+
},
|
| 603 |
+
{
|
| 604 |
+
"ce_cell": 0.16598297476768495,
|
| 605 |
+
"dice_cell": 0.37718391716480254,
|
| 606 |
+
"ce_nos": 0.030217354064807297,
|
| 607 |
+
"dice_nos": 0.2526162987947464,
|
| 608 |
+
"bce_ab": 0.027398303812369705,
|
| 609 |
+
"bce_cn": 0.05613136574625969,
|
| 610 |
+
"total": 0.9095302104949952,
|
| 611 |
+
"iter": 2550,
|
| 612 |
+
"lr": 2.319839794721846e-05,
|
| 613 |
+
"elapsed_s": 2578.604254961014
|
| 614 |
+
},
|
| 615 |
+
{
|
| 616 |
+
"ce_cell": 0.16501545295119285,
|
| 617 |
+
"dice_cell": 0.37883709609508515,
|
| 618 |
+
"ce_nos": 0.029584614327177405,
|
| 619 |
+
"dice_nos": 0.24774192690849303,
|
| 620 |
+
"bce_ab": 0.022402014611288906,
|
| 621 |
+
"bce_cn": 0.057471454478800296,
|
| 622 |
+
"total": 0.9010525631904602,
|
| 623 |
+
"iter": 2600,
|
| 624 |
+
"lr": 1.9653357343555055e-05,
|
| 625 |
+
"elapsed_s": 2630.874783039093
|
| 626 |
+
},
|
| 627 |
+
{
|
| 628 |
+
"ce_cell": 0.16610188841819762,
|
| 629 |
+
"dice_cell": 0.3750225841999054,
|
| 630 |
+
"ce_nos": 0.03136977587360889,
|
| 631 |
+
"dice_nos": 0.24075563967227936,
|
| 632 |
+
"bce_ab": 0.023018423346802593,
|
| 633 |
+
"bce_cn": 0.054641781151294706,
|
| 634 |
+
"total": 0.8909100878238678,
|
| 635 |
+
"iter": 2650,
|
| 636 |
+
"lr": 1.6499445418129828e-05,
|
| 637 |
+
"elapsed_s": 2683.160297870636
|
| 638 |
+
},
|
| 639 |
+
{
|
| 640 |
+
"ce_cell": 0.16699187859892844,
|
| 641 |
+
"dice_cell": 0.37973670542240145,
|
| 642 |
+
"ce_nos": 0.031007613111287354,
|
| 643 |
+
"dice_nos": 0.25119431257247926,
|
| 644 |
+
"bce_ab": 0.02384809180162847,
|
| 645 |
+
"bce_cn": 0.05100016433745622,
|
| 646 |
+
"total": 0.9037787771224975,
|
| 647 |
+
"iter": 2700,
|
| 648 |
+
"lr": 1.3745913138031944e-05,
|
| 649 |
+
"elapsed_s": 2734.7909269332886
|
| 650 |
+
},
|
| 651 |
+
{
|
| 652 |
+
"ce_cell": 0.1654239809513092,
|
| 653 |
+
"dice_cell": 0.373527038693428,
|
| 654 |
+
"ce_nos": 0.026881940369494258,
|
| 655 |
+
"dice_nos": 0.25084660708904266,
|
| 656 |
+
"bce_ab": 0.03065326821990311,
|
| 657 |
+
"bce_cn": 0.05875104777514935,
|
| 658 |
+
"total": 0.9060838842391967,
|
| 659 |
+
"iter": 2750,
|
| 660 |
+
"lr": 1.1400837087866401e-05,
|
| 661 |
+
"elapsed_s": 2785.7226259708405
|
| 662 |
+
},
|
| 663 |
+
{
|
| 664 |
+
"ce_cell": 0.164136121571064,
|
| 665 |
+
"dice_cell": 0.37796851992607117,
|
| 666 |
+
"ce_nos": 0.03165758168324828,
|
| 667 |
+
"dice_nos": 0.2464037835597992,
|
| 668 |
+
"bce_ab": 0.02406527513638139,
|
| 669 |
+
"bce_cn": 0.05659090880304575,
|
| 670 |
+
"total": 0.9008221983909607,
|
| 671 |
+
"iter": 2800,
|
| 672 |
+
"lr": 9.471095779739621e-06,
|
| 673 |
+
"elapsed_s": 2837.11731505394
|
| 674 |
+
},
|
| 675 |
+
{
|
| 676 |
+
"ce_cell": 0.16365474253892898,
|
| 677 |
+
"dice_cell": 0.3711686569452286,
|
| 678 |
+
"ce_nos": 0.03501185901463032,
|
| 679 |
+
"dice_nos": 0.26115318775177004,
|
| 680 |
+
"bce_ab": 0.020862439805641772,
|
| 681 |
+
"bce_cn": 0.06006271541118622,
|
| 682 |
+
"total": 0.9119135963916779,
|
| 683 |
+
"iter": 2850,
|
| 684 |
+
"lr": 7.96234947739845e-06,
|
| 685 |
+
"elapsed_s": 2888.5047039985657
|
| 686 |
+
},
|
| 687 |
+
{
|
| 688 |
+
"ce_cell": 0.1650400999188423,
|
| 689 |
+
"dice_cell": 0.37380745112895963,
|
| 690 |
+
"ce_nos": 0.027719355495646594,
|
| 691 |
+
"dice_nos": 0.24648674428462983,
|
| 692 |
+
"bce_ab": 0.030483954632654787,
|
| 693 |
+
"bce_cn": 0.05518790137022734,
|
| 694 |
+
"total": 0.898725507259369,
|
| 695 |
+
"iter": 2900,
|
| 696 |
+
"lr": 6.8790235937012375e-06,
|
| 697 |
+
"elapsed_s": 2939.583750963211
|
| 698 |
+
},
|
| 699 |
+
{
|
| 700 |
+
"ce_cell": 0.1652056121826172,
|
| 701 |
+
"dice_cell": 0.37707124650478363,
|
| 702 |
+
"ce_nos": 0.027215274684131145,
|
| 703 |
+
"dice_nos": 0.24094445288181304,
|
| 704 |
+
"bce_ab": 0.02379746310412884,
|
| 705 |
+
"bce_cn": 0.05311750005930662,
|
| 706 |
+
"total": 0.8873515450954437,
|
| 707 |
+
"iter": 2950,
|
| 708 |
+
"lr": 6.224295710119515e-06,
|
| 709 |
+
"elapsed_s": 2990.7978727817535
|
| 710 |
+
},
|
| 711 |
+
{
|
| 712 |
+
"ce_cell": 0.15959388136863709,
|
| 713 |
+
"dice_cell": 0.3714492267370224,
|
| 714 |
+
"ce_nos": 0.030986426128074528,
|
| 715 |
+
"dice_nos": 0.2347310698032379,
|
| 716 |
+
"bce_ab": 0.018123713415116073,
|
| 717 |
+
"bce_cn": 0.05247432488948107,
|
| 718 |
+
"total": 0.8673586428165436,
|
| 719 |
+
"iter": 3000,
|
| 720 |
+
"lr": 6.000086256343946e-06,
|
| 721 |
+
"elapsed_s": 3042.6654930114746
|
| 722 |
+
}
|
| 723 |
+
],
|
| 724 |
+
"val": []
|
| 725 |
+
}
|