AgriTech plant disease & pest classification service. A full-stack reference app: fine-tune a YOLO11-cls model on the public PlantVillage dataset, export to ONNX, serve behind a FastAPI backend, and consume from a Vue 3 SPA. Built as a portfolio capstone aligned with the detection-style pipelines used in modern AgTech products (Solinftec Solix, Climate Fieldview, etc.).
🇧🇷 Versão em português · 🇺🇸 English version
- Model switcher (pytorch vs onnx) —
/model/info?backend=…and/detect?backend=…now let the caller pick which backend runs. The factory builds both backends when both weights files are present, and the route handler picks the one requested with transparent fallback. - Batch detection —
POST /detect/batchaccepts up to 10 images in one call and returns aBatchPredictionwith per-image results plus aggregate counts. The frontend auto-routes 1 image to/detectand 2+ images to/detect/batch. - Stats endpoint —
GET /statsreturns total inferences, per-backend counts, last-minute count, p50 / p95 / max latency, and uptime. Surfaced as a card on the home page. - Rate limiting —
LoginRateLimiter-style token bucket on/detectand/detect/batch. Defaults: 60 requests per minute per bearer-token or client IP, with a burst of 10. Returns 429Retry-Afterwhen exceeded.
- Richer
/health— now also reportsavailable_backendsandmodel_loaded. Returns 200 even without a model loaded. - Multi-image frontend — the file input is now
multiple, the predictor previews thumbnails in a 5-column grid, and each result is rendered as its own card with a separatePredictionList. - Tests —
test_stats_and_ratelimit.pywith 3 new unit tests; updated smoke tests to match the v0.2.0 contract. All 7 tests pass.
- Trains a YOLO11n-cls model on the 54k-image PlantVillage dataset (38 classes, 14 crops).
- Exports the fine-tuned weights to ONNX for fast CPU inference.
- Serves a REST API (
/health,/model/info,POST /detect) that accepts a leaf image and returns the top-K predicted classes with confidences. - A small Vue 3 SPA lets you drag-and-drop a leaf and see the predictions in a clean list with confidence bars.
I am preparing for a Junior Web Developer role at an AgTech company (Solinftec's public stack on GitHub shows a YOLOv5 fork for in-field pest detection, so this project mirrors that choice at portfolio scale). I wanted a small but end-to-end example that demonstrates:
- I can train and export a vision model, not just call an API.
- I can build a clean REST API with FastAPI + Pydantic.
- I can ship a usable Vue 3 frontend.
- I think about deployment (ONNX, multi-stage Docker, CI).
# From the repo root, in bash (WSL / git-bash / MSYS)
cd backend
./setup.sh # creates .venv with Python 3.14 and installs deps
./test.sh # pytest (4 tests, no model needed)
./lint.sh # ruff
./run.sh # uvicorn on http://localhost:8000From the frontend/ directory:
npm install
npm run dev # http://localhost:5173 (proxies /api to :8000)docker compose up --build
# Frontend: http://localhost:5173
# Backend: http://localhost:8000 (Swagger at /docs)Note: the model weights are not in the image. Either run
python -m scripts.train once on the host (writes into
backend/artifacts/, which is mounted as a volume) or drop a
pre-trained model.onnx there.
.
├── backend/ # FastAPI service
│ ├── app/
│ │ ├── api/ # routes + Pydantic schemas
│ │ ├── core/ # configuration
│ │ └── ml/ # ONNX + PyTorch backends (Strategy pattern)
│ ├── scripts/
│ │ ├── train.py # fine-tune YOLO11-cls on PlantVillage
│ │ └── export_onnx.py # re-export .pt -> .onnx
│ ├── tests/
│ ├── setup.{bat,sh} test.{bat,sh} lint.{bat,sh} run.{bat,sh}
│ └── Dockerfile
├── frontend/ # Vue 3 SPA
│ ├── src/
│ │ ├── components/ # Predictor, PredictionList, ModelStatus
│ │ ├── api/ # axios client
│ │ ├── types/ # TypeScript shapes
│ │ └── utils/ # class-name formatting
│ ├── Dockerfile + nginx.conf
│ └── package.json
├── docker-compose.yml # backend + frontend
├── .github/workflows/ # backend-ci + frontend-ci
└── README.md
| Method | Path | Description |
|---|---|---|
GET |
/health |
Liveness + model status |
GET |
/model/info |
Model metadata (class names, imgsz, backend) |
POST |
/detect |
Multipart upload of an image; returns top-K predictions |
Full schema: http://localhost:8000/docs (auto-generated by FastAPI).
cd backend
./setup.sh
./run.sh false # don't start uvicorn; just install
.venv/Scripts/python.exe -m scripts.train --epochs 5 --imgsz 224 --batch 32 --device cpu
# or with GPU: --device 0
# Artifacts land in backend/artifacts/best.pt + backend/artifacts/model.onnxA 5-epoch CPU run on PlantVillage takes ~1-2 hours; a GPU run is
~10-15 minutes. The included pytest smoke tests do not require a
trained model — they verify the app boots and returns proper 4xx/5xx.
- How to fine-tune a YOLO11-cls model on a custom dataset using the
Ultralytics Python API and the
hf:dataset prefix for Hugging Face Hub datasets. - How to export a model to ONNX and serve it without PyTorch at runtime (much smaller image, faster cold start).
- How to design a backend-agnostic inference layer with a Strategy pattern (ONNX and PyTorch backends implement the same interface).
- How to write smoke tests that don't need the heavy model weights
(TestClient +
app.stateinjection).
- Add bounding-box detection by switching to YOLO11n (detection) on a dataset like PlantDoc or CottonWeedDet12.
- Add Grad-CAM saliency maps so the user can see why the model predicted what it predicted.
- Add a "upload to retrain" endpoint so field photos can improve the model over time.
- Move inference to a GPU pod behind a queue (Celery / RQ).
MIT — see LICENSE.
- Model switcher (pytorch vs onnx) —
/model/info?backend=…e/detect?backend=…agora deixam o cliente escolher qual backend roda. A factory constrói ambos os backends quando os pesos existem, e o handler escolhe o pedido com fallback transparente. - Detecção em batch —
POST /detect/batchaceita até 10 imagens numa única chamada e retorna umBatchPredictioncom resultados por imagem + contagens agregadas. O frontend roteia 1 imagem pra/detecte 2+ imagens pra/detect/batchautomaticamente. - Endpoint de stats —
GET /statsretorna total de inferências, contagem por backend, último minuto, latência p50/p95/max e uptime. Exibido como card na home. - Rate limiting — token bucket estilo
LoginRateLimiterno/detecte/detect/batch. Default: 60 requests por minuto por bearer-token ou IP, com burst de 10. Retorna 429 +Retry-Afterquando excede. /healthmais rico — agora também reportaavailable_backendsemodel_loaded. Retorna 200 mesmo sem modelo carregado.- Frontend multi-imagem — input agora é
multiple, o predictor mostra thumbnails numa grade de 5 colunas, e cada resultado vira um card separado com seu próprioPredictionList. - Testes —
test_stats_and_ratelimit.pycom 3 testes novos; smoke tests atualizados pro contrato v0.2.0. 7/7 passam.
- Treina um modelo YOLO11n-cls no PlantVillage (54k imagens, 38 classes, 14 culturas).
- Exporta os pesos pra ONNX, inferência rápida em CPU.
- Expõe uma API REST (
/health,/model/info,POST /detect) que recebe uma imagem de folha e retorna top-K classes com confiança. - Uma SPA Vue 3 permite arrastar-e-soltar a folha e ver as predições em lista limpa com barras de confiança.
Estou me preparando pra Analista Desenvolvimento Web Jr numa AgTech (o stack público da Solinftec no GitHub mostra um fork do YOLOv5 pra detecção de pragas em campo, então este projeto espelha essa escolha em escala de portfólio). Eu queria um exemplo pequeno mas end-to-end que demonstrasse:
- Sei treinar e exportar um modelo de visão, não só chamar API.
- Sei construir uma API REST limpa com FastAPI + Pydantic.
- Sei entregar um frontend Vue 3 usável.
- Penso em deploy (ONNX, multi-stage Docker, CI).
Opção A — local com os scripts do venv (sem Docker):
# Da raiz do repo, em bash (WSL / git-bash / MSYS)
cd backend
./setup.sh # cria .venv com Python 3.14 e instala deps
./test.sh # pytest (4 testes, sem precisar de modelo)
./lint.sh # ruff
./run.sh # uvicorn em http://localhost:8000Em frontend/:
npm install
npm run dev # http://localhost:5173 (proxia /api para :8000)Opção B — Docker Compose (tudo em containers):
docker compose up --build
# Frontend: http://localhost:5173
# Backend: http://localhost:8000 (Swagger em /docs)Os pesos do modelo não vão na imagem. Rode
python -m scripts.train uma vez no host (gera em
backend/artifacts/, montado como volume) ou coloque um model.onnx
já treinado lá.
| Método | Path | Descrição |
|---|---|---|
GET |
/health |
Liveness + status do modelo |
GET |
/model/info |
Metadados do modelo (classes, imgsz, backend) |
POST |
/detect |
Upload multipart de imagem; retorna top-K predições |
Schema completo: http://localhost:8000/docs.
cd backend
./setup.sh
.venv/Scripts/python.exe -m scripts.train --epochs 5 --imgsz 224 --batch 32 --device cpu
# ou com GPU: --device 0
# Artefatos em backend/artifacts/best.pt + backend/artifacts/model.onnxCPU em 5 épocas no PlantVillage leva 1-2h; GPU leva 10-15 min. Os testes de smoke inclusos não precisam de modelo treinado.
- Como fazer fine-tuning de YOLO11-cls num dataset customizado usando
a API Python do Ultralytics e o prefixo
hf:pra datasets do Hugging Face. - Como exportar pra ONNX e servir sem PyTorch em runtime (imagem muito menor, cold start mais rápido).
- Como desenhar uma camada de inferência backend-agnóstica com Strategy pattern (ONNX e PyTorch implementam a mesma interface).
- Como escrever smoke tests que não precisam dos pesos do modelo
(TestClient +
app.state).
- Trocar pra detecção com bounding boxes usando YOLO11n num dataset tipo PlantDoc ou CottonWeedDet12.
- Adicionar Grad-CAM pra explicar visualmente a predição.
- Adicionar endpoint "upload pra retreinar" pra fotos de campo melhorarem o modelo ao longo do tempo.
- Mover inferência pra um pod com GPU atrás de fila (Celery / RQ).
MIT — veja LICENSE.
Issues e PRs são bem-vindos! Por favor leia CONTRIBUTING.md.
Davi Roque — daviroque.luiz03@gmail.com · LinkedIn · GitHub
Built as a portfolio capstone while preparing for a Junior Web Developer role at an AgTech company (Solinftec — public job posting 4425371310).