| # Wrong-crop gate & field-change: production hardening |
|
|
| This documents the current heuristic, what a 57-image cross-crop sweep showed, |
| and the two model-based upgrades that take both features to production grade. |
|
|
| ## 1. Current state (shipped) |
|
|
| **Wrong-crop gate** (`ml/serve/inference_app.py::_cross_crop_check`): when the |
| selected crop's result is unsure, the image is scored against the other in-memory |
| crop models and the request is blocked (with a suggested crop) if a different |
| crop fits clearly better. Confident, in-catalog results skip the pass. |
|
|
| Thresholds were tuned from `scripts/cross_crop_sweep.py` (57 images, all 5 crops, |
| each scored by all 5 models): |
|
|
| | Thresholds (STRONG/MARGIN/OTHER_MIN) | False-reject | Catch | False-accept | |
| |---|---|---|---| |
| | 0.85 / 0.15 / 0.75 (initial) | 5.6% | 78.5% | 31 | |
| | **0.85 / 0.12 / 0.80 (deployed)** | **2.8%** | **78.5%** | 31 | |
| |
| **Field-change comparison** (`lib/healthComparison.ts`): continuous expected |
| health over the model's full probability distribution (replaced 4 fixed |
| severity buckets). Detects healthy↔diseased shifts and uncertain/borderline |
| moves; cannot measure spread of a confidently-identical disease. |
| |
| ## 2. What the sweep proved about the ceiling |
| |
| The heuristic compares five **disease** classifiers' confidences. That caps out |
| at ~78% catch / ~3% false-reject because: |
| |
| - The lone false-reject is a rice leaf the **rice model itself** scores 50% on |
| (already "no clear match") while corn scores 85%. |
| - False-accepts concentrate on out-of-crop overconfidence, worst among the |
| grasses and the weak rice model: rice→corn 5/7 slip, wheat→soybean 4/8, |
| rice→{others} ~3/7. |
| |
| Disease models are simply not calibrated for "is this even my crop?". |
| |
| ## 3. Upgrade A — dedicated crop-ID classifier (the real gate) |
| |
| A single 5-class "which crop is this leaf?" model. Crops are far more visually |
| separable than diseases within a crop, so expect >>95% accuracy from a small |
| model. It replaces "compare five disease models" with one reliable signal, which |
| should eliminate the rice false-rejects and most cross-crop false-accepts. |
| |
| **Data:** already on disk — every `ml/data/<crop>/**` image is implicitly |
| labeled by its crop. No new collection needed. Hold out by source/folder to |
| avoid the framing shortcut documented in the soybean/rice notes. |
| |
| **Train (needs a TF environment — not runnable in the Py3.14 repo venv):** |
| ``` |
| python scripts/train_crop_id.py --epochs 8 --out ml/models/crop_id |
| ``` |
| Scaffold provided in `scripts/train_crop_id.py` (EfficientNetB0, in-model |
| rescaling, TFLite export — mirrors the per-crop pipeline). |
| |
| **Serve:** load `crop_id` alongside the disease models; in `/predict`, run it |
| first. If `argmax(crop_id) != selected_crop` with margin ≥ τ, block and suggest |
| `argmax`. Keep the current heuristic as a fallback when the classifier is |
| absent. Re-run `scripts/cross_crop_sweep.py` to set τ and confirm |
| false-reject ≈ 0. |
| |
| ## 4. Upgrade B — severity / leaf-coverage model (real spread detection) |
| |
| The field-change comparison can't quantify worsening of a confirmed disease |
| because the classifier outputs identity, not severity. A pixel-color |
| "% leaf affected" heuristic was prototyped and rejected — too crop-dependent |
| (healthy soybean read 53% "damaged"). |
| |
| **Right approach:** a severity regressor/segmenter (e.g. lesion-area |
| segmentation, or an ordinal severity head) trained on **severity-labeled** data |
| (PlantVillage severity subsets, or in-house annotation). Output a 0–100 |
| affected-area per check; the comparison then trends the delta of a real |
| measurement instead of a label. This is a genuine data+training project, not a |
| config change. |
| |
| ## 5. Reproduce / re-tune |
| |
| ``` |
| # against the live (rate-limited) space: |
| python scripts/cross_crop_sweep.py --url https://jaithrap-cropintel.hf.space --pace 3.3 --out sweep.json |
| # against a local service: |
| python scripts/cross_crop_sweep.py --url http://127.0.0.1:8000 --out sweep.json |
| # re-analyze only: |
| python scripts/cross_crop_sweep.py --analyze sweep.json |
| ``` |
| |