Setting Up Plant Identification (Inference Service)¶
This page describes how to set up the inference-service for self-hosted plant identification (REQ-029-A). The inference service is an optional component — Kamerplanter works fully without it; plant identification will be unavailable.
Overview: What Gets Installed?¶
The inference service (src/inference-service/) is a standalone FastAPI microservice that:
- loads the DINOv2 model (ViT-S/14, Apache-2.0, ~21 M parameters) as an ONNX artefact,
- preprocesses images and converts them into embedding vectors (384 dimensions),
- matches these vectors against a reference index in pgvector and returns the most similar plant species.
The service is an optional, standalone module. There are two deployment topologies:
- Development (Skaffold): The service runs in its own Helm release
kamerplanter-recognition(Skaffold profilerecognition) and shares the pgvector store with the knowledge service (kamerplanter-kirelease, databasekamerplanter_vectors, its own tablespecies_embeddings). Thekamerplanter-kivectordb must be running when therecognitionprofile starts. - Production (ArgoCD, single-release): The inference service and a dedicated pgvector instance run in the same Helm release as the backend and frontend (
kamerplanter). No separate release, no shared pgvector store with the knowledge service. Available from image tag v0.0.17 onwards (CI publishes thekamerplanter-inference-serviceimage to GHCR starting from this release).
In both topologies the service is only reachable within the cluster (Kubernetes ClusterIP).
Activation Order¶
Follow the order
Execute the three steps in the order shown. If you set INFERENCE_SERVICE_ENABLED=true before the reference index is populated, local identification is unavailable — the backend then falls back directly to Pl@ntNet (only if a PLANTNET_API_KEY is configured; see the Environment Variables table).
Step 1: Start the Inference Service — Development (Skaffold)¶
# In the project directory (development):
# The recognition profile shares the pgvector DB with the ki stack,
# so start both modules together:
skaffold dev -m ki -m recognition
The recognition profile starts only the inference-service (its own release kamerplanter-recognition); the ki profile provides the shared pgvector DB. On first start, the ONNX model is exported during the build step — this takes 5–15 minutes depending on your hardware (see Resource Requirements below).
Model export is cached
After the first build, the model lives in the layer cache. Subsequent starts complete in seconds.
Check that the service is running:
# Port-forward (local development):
kubectl port-forward svc/kamerplanter-recognition 8090:8000 -n default
# Check readiness (is the model loaded?):
curl http://localhost:8090/ready
# Expected response: {"status": "ok"}
# Retrieve model information:
curl http://localhost:8090/modelinfo
# Response includes: model, dim, input_size, license, checksum
Step 2: Populate the Reference Index¶
The reference index contains embedding vectors for all plant species from the master data. It is populated by a Celery task that fetches reference images from GBIF and Wikimedia Commons (CC0/CC-BY licences only), embeds them, and stores the vectors in pgvector. Original images are not stored.
This step also populates the UI images in the species view
After the task completes, thumbnails appear in the species list and a full reference image gallery appears on each species detail page. Before the first acquisition run, both views show a placeholder notice. Licence attribution (CC-BY) is stored automatically in the metadata and displayed in the UI. For more information: Reference Images in the Species View.
# Start the Celery task for all species (one-time run; takes several hours):
kubectl exec -it deploy/kamerplanter-backend -n default -- \
celery -A app.tasks call \
app.tasks.reference_image_tasks.acquire_all_reference_images_task
# Alternatively: via the backend API (admin endpoint):
curl -X POST http://localhost:8000/api/v1/admin/reference-images/acquire \
-H "Authorization: Bearer <admin-token>"
Monitor progress:
# Query the coverage report (how many species are identifiable?):
curl http://localhost:8000/api/v1/admin/reference-images/coverage \
-H "Authorization: Bearer <admin-token>"
The response shows per species how many reference images were accepted and whether the species is considered "identifiable" (usable_for_recognition, at least 5 accepted references):
{
"total_species": 66,
"usable_species": 48,
"entries": [
{
"species_key": "species_alocasia_zebrina",
"scientific_name": "Alocasia zebrina",
"accepted": 2,
"candidates_found": 7,
"usable_for_recognition": false,
"license_breakdown": {"CC0": 1, "CC_BY": 1}
}
]
}
Coverage gaps
Species with fewer than 5 accepted reference images do not appear in identification results. The system communicates this honestly in the UI. Common causes for gaps: rare species, exotic houseplants, or species without CC0/CC-BY photos in GBIF.
Admin API endpoints for reference image acquisition (overview):
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/admin/reference-images/acquire | Start an acquisition run for all species |
POST | /api/v1/admin/reference-images/acquire/{species_key} | Start or repeat the acquisition run for a single species |
GET | /api/v1/admin/reference-images/coverage | Coverage report: identifiable species, species below threshold |
All three endpoints require a valid admin token (Authorization: Bearer <admin-token>). They are accessible via the regular backend ingress — no separate port-forward is needed.
Step 3: Enable the Local Path¶
Set the environment variable in the backend:
Only enable once the index is populated
If INFERENCE_SERVICE_ENABLED=true is set and the reference index is empty, the system falls back to Pl@ntNet — but only if a Pl@ntNet key is configured and consent has been granted. If neither is the case, identification returns no results.
Helm Configuration¶
Development (Skaffold)¶
In the development workflow the inference service is started via the Skaffold profile recognition as a separate Helm release kamerplanter-recognition. Configuration lives in helm/kamerplanter/values-dev-recognition.yaml — the pgvector store is shared with the kamerplanter-ki release (see Overview above).
Production (ArgoCD — Single-Release)¶
Minimum image tag: v0.0.17
The CI image ghcr.io/nolte/kamerplanter-inference-service is only published from release v0.0.17 onwards (this is the first release that includes the build-inference-service CI job). ArgoCD Applications must pin to v0.0.17 or later — earlier tags do not contain the image.
In production the inference service and a dedicated pgvector instance (vectordb) run inside the same Helm release as the backend and frontend (release name kamerplanter). The chart helm/kamerplanter/values.yaml ships two additional controllers that are disabled by default: vectordb and inference-service. The operator enables them via valuesObject in the ArgoCD Application.
One Secret key: POSTGRES_PASSWORD
Before the first deployment the operator sets exactly one key in kamerplanter-secrets: POSTGRES_PASSWORD. The vectordb container (PostgreSQL 18) mandates that name; the inference-service derives its own VECTORDB_PASSWORD from the same key via secretKeyRef (the chart wires this up). No password is ever inlined in the chart or in Git, and both containers pull just that one key — not the whole Secret via envFrom.
Generate and set the key (existing Secret keys are preserved):
PW=$(openssl rand -base64 24)
kubectl patch secret kamerplanter-secrets -n kamerplanter --type merge \
-p "{\"stringData\":{\"POSTGRES_PASSWORD\":\"$PW\"}}"
If kamerplanter-secrets does not exist yet:
kubectl create secret generic kamerplanter-secrets -n kamerplanter \
--from-literal=POSTGRES_PASSWORD="$(openssl rand -base64 24)"
When kamerplanter-secrets is fed by ESO / Sealed Secrets / Vault, add POSTGRES_PASSWORD at that source instead.
ArgoCD Application — spec.sources[].helm.valuesObject:
controllers:
vectordb:
enabled: true
inference-service:
enabled: true
backend:
containers:
main:
env:
INFERENCE_SERVICE_ENABLED: "true"
INFERENCE_SERVICE_URL: "http://kamerplanter-inference-service:8000"
service:
vectordb:
enabled: true
inference-service:
enabled: true
persistence:
inference-service-tmp:
enabled: true
networkpolicies:
vectordb:
enabled: true
inference-service:
enabled: true
Resources and security context (chart defaults — do not override unless necessary):
| Component | CPU request/limit | RAM request/limit | Notes |
|---|---|---|---|
vectordb | 50m / 500m | 128Mi / 512Mi | StatefulSet, 5Gi PVC (/var/lib/postgresql/data, PGDATA = .../pgdata), uid/gid/fsGroup 999 (postgres), helm.sh/resource-policy: keep |
inference-service | 250m / 2 | 512Mi / 2Gi | readOnlyRootFilesystem: true, memory-backed /tmp emptyDir |
In-cluster service hostnames (release name kamerplanter):
- Backend → inference service:
http://kamerplanter-inference-service:8000 - Inference service → pgvector:
kamerplanter-vectordb:5432(chart default forVECTORDB_HOST)
Activation Order in Production¶
Follow the order (production)
Deploy the stack (vectordb + inference-service) and populate the reference index before setting INFERENCE_SERVICE_ENABLED=true. Otherwise recognition finds no reference index and returns no results.
- Deploy the ArgoCD Application with the
valuesObjectfields shown above (withoutINFERENCE_SERVICE_ENABLED: "true"). Wait untilkamerplanter-vectordbandkamerplanter-inference-serviceareReady. - Populate the reference index (identical to Step 2 in the development path above — the
kubectl exectarget isdeploy/kamerplanter-backend). - Add
INFERENCE_SERVICE_ENABLED: "true"to thevaluesObjectand synchronise the Application.
Resource Requirements¶
| Scenario | RAM | CPU | Latency/request |
|---|---|---|---|
| DINOv2 ViT-S/14 | 512 MB – 1 GB | 0.5–2 cores | 500ms–2s (CPU) |
Raspberry Pi / ARM
DINOv2 ViT-S/14 runs on ARM64 (Raspberry Pi ⅘, Apple Silicon). Latency is higher (~3–8s) but sufficient for batch indexing and interactive identification.
Environment Variables¶
| Variable | Required | Default | Description |
|---|---|---|---|
VECTORDB_HOST | No | localhost | Hostname of the pgvector database. In-cluster value depends on topology: DEV/separate-release → kamerplanter-ki-vectordb (shared KI stack); production/single-release → kamerplanter-vectordb (dedicated, chart default) |
VECTORDB_PORT | No | 5432 | Port of the pgvector database |
VECTORDB_DATABASE | No | kamerplanter_vectors | Database name |
VECTORDB_USERNAME | No | postgres | Database user |
VECTORDB_PASSWORD | No | changeme | Database password. In production (single-release) derived via secretKeyRef from the one Secret key POSTGRES_PASSWORD — do not set separately |
MODEL_NAME | No | dinov2_vits14 | ONNX model name |
MODEL_PATH | No | /app/models/dinov2 | Directory containing the ONNX model artefact (model.onnx) |
CONFIDENCE_AUTO_ACCEPT | No | 0.85 | Confidence threshold for direct acceptance |
CONFIDENCE_SHOW_RESULTS | No | 0.10 | Minimum confidence to appear in the list |
| Variable (Backend) | Required | Default | Description |
|---|---|---|---|
INFERENCE_SERVICE_ENABLED | No | false | Enable the local inference path |
INFERENCE_SERVICE_URL | No | http://kamerplanter-recognition:8000 | Internal URL of the inference service |
PLANTNET_API_KEY | No | — | Pl@ntNet API key for fallback (optional) |
Inference Service Endpoints (Internal)¶
These endpoints are only reachable within the cluster and are not exposed via the ingress.
| Method | Path | Description |
|---|---|---|
POST | /embed | Single image → embedding vector |
POST | /embed/batch | Multiple images → embedding vectors (acquisition) |
POST | /match | Image → top-k most similar species with confidence (query param k, default 5, max 50) |
POST | /reference | Store embedding + provenance in pgvector |
GET | /reference/{species_key} | Retrieve the indexed references for a species |
DELETE | /reference/{species_key} | Delete references for a species (re-index) |
GET | /health | Liveness probe |
GET | /ready | Readiness probe (model loaded?) |
GET | /modelinfo | Model name, dimensions, input size, licence, checksum |
Licences and Legal Notes¶
| Component | Licence | Note |
|---|---|---|
| DINOv2 base backbone (Meta) | Apache-2.0 | Verify LICENSE in the official repo before production use |
| ONNX Runtime (Microsoft) | MIT | — |
| Reference images (GBIF) | CC0 / CC-BY | Only these licences are indexed |
| PlantCLEF fine-tuned weights | CC-BY-NC | Not used (non-commercial restriction) |
| Pl@ntNet API (fallback) | ToS, free ≤500/day | Only with user consent, only as fallback |
Do not use PlantCLEF weights
The DINOv2 weights fine-tuned on the PlantCLEF-2024 dataset are licensed under CC-BY-NC (non-commercial). These weights are not used. Kamerplanter exclusively uses the Apache-2.0-licensed base backbone from facebookresearch/dinov2.
Troubleshooting¶
The inference service fails to start — error: model not found
The ONNX artefact may not have been exported. Check the build log of the inference-service image for the export_dinov2_onnx.py step. Run skaffold build -m recognition again.
Identification always returns 'no results' even though the service is running
Check whether the reference index is populated (/api/v1/admin/reference-images/coverage). An empty index produces no matches. Run acquire_all_reference_images_task (Step 2).
The Celery task runs for a very long time — is that normal?
Yes. Fetching GBIF data for all configured species — with up to 40 image candidates each, computing embeddings, and building the index — can take several hours depending on the number of species, hardware, and network speed. The task is idempotent — you can restart it if interrupted.
How do I update the reference index for a single species?
Use the admin endpoint POST /api/v1/admin/reference-images/acquire/{species_key} — it triggers the acquire_reference_images_task Celery task for that species internally.
See Also¶
- Plant Identification (User Guide)
- Image Recognition Architecture
- Deployment Profiles — Which AI components are included in which profile?
- ArgoCD Deployment — Production deployment with ArgoCD (Application configuration, sync strategies)
- Helm Charts — General Helm configuration
- Environment Variables