APIN

warming up the server…

first visit may take up to 30s · subsequent visits are cached

APIN organization logo
APIN Pipeline Atlas
Field Laboratory · Architecture
openapi.json

Pipeline Atlas

How a leaf photo becomes a diagnosis. The four-module architecture, layer by layer, with calibration metrics, a live request log, and the data behind every number on this page. Take the tour from the top, or jump straight to any module on the left.

checking modules… git ·········· device ······ /status ↗ /health ↗ /docs ↗
Inferences served
···
predictions · all-time
Top disease · last 7 days
···
awaiting traffic
Test quality
···
macro F1 · held-out test
Live now
···
awaiting first visitor
checking compute
The whole pipeline · click any block to jump to its module
INPUT ROUTER SPECIALISTS OUTPUT leaf image 224 × 224 router DINOv2 ViT 4-way head ~22.06 M params · frozen Module 2 · Tomato V3 + SP-LoRA · 6 classes Module 4 · APIN ensemble okra + brassica · 9 classes Module 3 · Chilli router-only · no specialist yet One router · three specialists · honest rejection over fabrication

§ What this page is #

APIN is a plant disease diagnosis system that runs four separate models in concert. The first, the router, looks at an incoming leaf image and decides which crop it is. The other three are specialists, each trained to recognise the diseases of one crop family: tomato, the okra/brassica pair (which share a single ensemble), and chilli (where the disease specialist has not been built yet, so the system reports an honest "router rejected" instead of guessing).

The architecture is split this way for a real reason: a single monolithic model trained to identify every disease of every crop gets dragged down by the worst-represented class. By specialising, each specialist sees only its own crop's distribution, and the router takes the responsibility for routing-versus-specialising entirely off the specialists' plate. The cost of the split is the router itself, which adds one inexpensive forward pass per request.

Below, each module gets its own section: architecture, data, metrics, calibration, a live test you can run, and an honest interpretation. The right rail tracks where you are. The live request log on the next page shows real traffic this server is handling. Press ? at any time for keyboard shortcuts; g opens a glossary panel from the right.

The request lifecycle #

Eight steps separate an uploaded JPEG from a returned diagnosis. Every interactive playground on the /docs page is a real walk-through of this same sequence.

Upload. The browser sends the image as multipart form data over HTTPS.
Validate. A quality gate checks blur, resolution, channel ratios, and file size. Bad inputs are rejected with a clean error envelope.
Route. The router classifies the crop. If confidence is at least 0.40, the request goes to that specialist; otherwise it falls back to the APIN ensemble.
Specialise. The chosen module runs its forward pass. Tomato uses V3 + SP-LoRA; APIN uses four parallel signals fused by a stacking gate.
Calibrate. Per-class temperature scaling adjusts confidences, and a conformal set is computed so the response carries an honest "set of plausible classes" rather than just a single argmax.
Detect OOD. A Mahalanobis distance check decides whether the image is too unlike the training distribution to act on. If so, the model abstains.
Tier. The decision logic assigns a tier from 1A (clean, all signals agree) down to 5 (hard abstention). The tier governs the recommended action.
Envelope. The result is wrapped in the standard nine-key success envelope and returned with a request_id, latency, and the API version.
Note

Calibration is what makes the confidence number on the response actually mean something. A vanilla softmax can report "87% confident" while being right anywhere from 60% to 95% of the time. After the calibration step here, an 87% prediction is right about 86% to 88% of the time. The "Calibration deep dive" section toward the end of this page covers why and how.

§ Live request log #

Below is every request this server has handled in the last few minutes, in real time. The log skips static assets (the favicon, logo, CSS, JavaScript) and the log endpoint's own self-polls. It carries path templates only · never request bodies, never authorization headers, never user identifiers. It is safe to look at and useful for getting a feel for what the system is actually doing right now.

Live · polling every 5 seconds
waiting for the first request…

§ The four modules #

The detailed module sections (architecture, data, metrics, live tests, interpretation) ship across the v1 build. Below are the section banners with each module's at-a-glance summary so the navigation, status indicators, and jump-targets are already in place. Click any banner to land here from the left navigation.

Module 1
The Crop Router

A frozen DINOv2Self-supervised vision transformer trained on natural images. Used here as a frozen feature extractor for the 4-way crop classifier head. ViT feeding a 4-way linear head. Identifies tomato, okra, brassica, and chilli. Confidence below 0.40 falls back to the APIN ensemble.

checking… introduced in v1.0

Architecture

The router is the first stage every request hits. Its job is small and well-defined: given a leaf image, decide which of four crops it is (tomato, okra, brassica, chilli) and pass that decision plus a confidence score to the rest of the pipeline. Everything downstream depends on this being right.

Architecturally, the router is a frozen DINOv2 vision transformer feeding a single 4-way linear head. The backbone weights do not change during router training; only the head learns. This is deliberate. DINOv2 was pre-trained self- supervised on hundreds of millions of natural images and its feature space already separates "leafy green plant" from "rest of the world" cleanly. Asking it to also learn "tomato vs okra vs brassica vs chilli" only takes a handful of fine-tuning epochs on the linear head, on top of a ~22-million-parameter backbone (DINOv2 ViT-Small with 4 register tokens, 22.06 M params) that stays fixed.

The frozen-backbone choice has a second benefit: the router never overfits to the training distribution. Because the backbone weights are immutable, the only way the router can adapt is by re-weighting the existing DINOv2 features. That keeps the four-way decision boundary smooth and robust to lighting changes, camera angles, and the kind of domain shift between lab-collected and field-collected photographs that plagues most plant-disease classifiers.

On the confidence side: the router outputs a four-element softmax vector. If the maximum element is at least 0.40, the request is routed to that crop's specialist; if below, the system falls back to the APIN okra/brassica ensemble as a best-effort guess. The 0.40 threshold was chosen on a held-out validation split as the value that minimises both wrong-crop routing and false fallbacks; the confidence histogram on the Metrics tab visualises why.

Module 2
The Tomato Pipeline

A V3 backbone in parallel with an SP-LoRA fine-tune branch, fused by a small stacking gate. Diagnoses six tomato conditions including late blight, septoria leaf spot, and the yellow-leaf-curl virus.

checking… introduced in v1.0

Architecture

The tomato specialist is not a single model; it is a two-branch ensemble whose outputs are combined in probability space. Branch one is V3: a DINOv2-Small backbone with low-rank adaptation and FiLM conditioning, originally trained for 10 classes and re-used here for the deployed 6. Branch two is a separate single-pass LoRA run on a DINOv2-Reg-Base backbone, trained directly on 6 classes, epoch 13. Both branches see the same input image; they vote on the class with their respective probability outputs.

The fusion is 50/50 probability averaging, but with one critical asymmetry: V3's logits are divided by a temperature of 0.5 before softmax (sharpening V3 by a factor of two), while the LoRA branch's logits are softmaxed at T=1.0. The effect is that V3 dominates on high-confidence cases (its sharpened argmax overrides the unsharpened LoRA argmax on confident frames; specific magnitudes vary per image and surface once the metrics extraction runs), while on uncertain cases the two contribute comparably. This asymmetric sharpening is deliberate; the validation lift over V3-alone was measured with exactly this setup (see Decision 56 in ladi_decisions.md).

Input preprocessing differs per branch and also matters. V3 takes a 224x224 stretch-resize plus LAB-CLAHE on the L channel plus ImageNet normalisation. LoRA takes an 800-pixel cap, letterboxed to 392 with padding value 114, then LAB-CLAHE plus normalisation. The end-to-end forward pass at inference time runs both pipelines and emits a single fused probability distribution over six classes: tomato_late_blight, tomato_septoria_leaf_spot, tomato_foliar_spot, tomato_mosaic_virus, tomato_yellow_leaf_curl_virus, and tomato_healthy.

Module 3
Chilli

The router identifies chilli leaves; the chilli disease specialist has not been built yet. A chilli frame currently returns a ROUTER_REJECTED response with a clear message. The section here documents the current state honestly, with a roadmap of what would need to ship to bring a real specialist online.

router-only specialist on roadmap

Current state

APIN's DINOv2-backed crop router already recognises chilli leaves; it was trained on five chilli source datasets (Bangladesh, Karnataka, Mendeley, iNaturalist, anthracnose- focused Kaggle) and learns the difference between a chilli leaf and the other three crops the system handles.

What's missing is the second step: a chilli disease specialist that takes a chilli leaf and assigns it a disease class. That model has not been built. Until it ships, every chilli frame that reaches /api/predict/full with the router agreeing it is chilli returns a clean rejection envelope and the request stops.

Below is the exact response shape an integrator currently has to handle. The envelope follows APIN's standard 8-key error format so client code does not need a special case for chilli; it only needs to recognise the error.code = "router_rejected" sentinel.

ROUTER_REJECTED · example response body
{
  "api_version": "1.0",
  "request_id": "req_a4b9c2e7d1f803a5",
  "endpoint": "/api/predict/full",
  "ok": false,
  "processed_at": "2026-05-23T11:04:12.337Z",
  "processing_time_ms": 82,
  "status_code": 409,
  "error": {
    "code": "router_rejected",
    "message": "Detected crop: chilli. This is outside the deployed specialist set.",
    "hint": "A chilli specialist is on the roadmap; for now, route the request to a human agronomist or use the fallback APIN ensemble at your own risk.",
    "router_crop": "chilli",
    "router_confidence": 0.94
  }
}
Why a hard reject instead of a degraded guess: returning a fabricated disease label from a model that has never seen the disease distribution is worse than telling the integrator we don't know. Honest failure is cheaper than wrong confidence.
Module 4
The APIN Okra/Brassica Ensemble

Four parallel signals: a DINOv3-ConvNeXt-Small primary classifier, an EfficientNet-B0 secondary, a frozen DINOv2 representation, and a Plant-Signal-Vector engineered-feature channel. Outputs are fused by a learned stacking MLP, then per-class temperature-scaled and wrapped with a conformal prediction set.

checking… introduced in v1.0

Architecture

APIN is a 4-signal stacking ensemble. Four independent backbones each emit a 9-class probability over the ensemble's shared okra+brassica label space, and a learned stacking MLP fuses those 36 probabilities into one 9-class output. Per-class temperature scaling and conformal prediction sets wrap the head.

Why four signals (not one)

Each backbone has a different inductive bias: DINOv3-ConvNeXt is convolutional and texture-led, EfficientNet-B0 is small but heavily tuned, DINOv2-ViT-S is self-supervised and shape-led, PSV is engineered colour/vein features. When one backbone is confidently wrong on an OOD image, the others usually disagree. The stacking MLP learns where to trust whom.

The stacking gate

A tiny MLP (36 inputs → 64 hidden → 9 outputs) reads all four probability vectors and produces a fused output. It's the only trainable component downstream of the frozen signals, and it was fit on the held-out calibration split; never on the same images used to train the four signals. That separation is what lets temperature scaling and conformal prediction calibrate honestly.

the diagram is wired to the live /api/info backbone manifest; signal pills tint by the latest reliability score

Live internals
Forward Pass · live model internals

A scroll-driven tour through every model in the pipeline. Each stage shows real activations captured from the deployed weights on three stratified test images. Toggle between Router, Tomato, APIN, and Chilli; scroll to step through stages; press c to highlight the honest caveats; press Play to autoadvance.

checking… 4 models · 21 stages · 3 test leaves per stage
Router · stage 1 of 4

Input tensor

Loading hand-drafted narration…

Loading…

class confidence by depth how each class's softmax evolves across the 12 transformer blocks
latency budget wall-clock per stage on the deployed RTX 4060 · ··· ms total

§ Calibration deep dive #

An 87% confidence number should mean "right 87 times out of 100" on the calibration distribution. Without intervention, modern ensembles report 87% but are right 75% of the time. This section shows how APIN gets that number honest, and what we measure to prove it.

Temperature scaling

per-class scalar that divides each logit before softmax

9 learned T values

    hover any class to highlight it on the right

    expected calibration error

    Per-image pre-temperature ("raw") vs post-temperature softmax for the three pool images needs a fresh extraction run to surface. Once the extractor lands, this panel will also show:

    • 9 raw probs vs 9 calibrated probs for the active image
    • hover any class to highlight the matching T value on the left

    ECE numbers above are computed from the calibration run (scripts/apin/caches/apin_calibration.json) when available.

    how the T values were fit

    Negative log-likelihood minimisation on the held-out calibration split. The single-parameter-per-class objective is convex and was solved with LBFGS. Classes the ensemble was over-confident on get T > 1 (flattens the softmax); under-confident classes get T < 1 (sharpens). The same procedure was used for tomato's 6-class output and the router's 4-class output.

    Conformal prediction set

    per-class thresholds that yield calibrated set-coverage

    per-class thresholds τ live · /api/info

      calibrated for ~90% coverage on the held-out split

      α slider · per-class threshold response live anchor · α=0.05

      α (miscoverage budget) 0.05 target coverage (1 − α) 95.0% mean τ across classes ·
      0.050.150.25 0.350.50

      drag the slider to see how τ shifts; α=0.05 is the live anchor from /api/info.conformal_thresholds. All α steps on the [0.05 .. 0.50] grid are measured from the conformal split via extract_phase_d_live.py.

      prediction-set size distribution live · sweep n=307 cal · 736 eval

      how often the conformal prediction set has 0, 1, 2, 3, 4, or 5+ classes at the current α. Empty sets are the model's honest "I don't know" answers; size-1 sets are confident calls.

      calibration sample distribution live · /api/info

      per-class counts used for fitting τ (held-out) and field re-calibration. The thinness of okra_enation (n=4 / 14) is why its τ swings hardest with α.

      Out-of-distribution detection

      CLS-embedding PCA · in-dist vs rejected scatter

      live · softmax-PCA proxy · n=500 of 736 field photos ·

      2D PCA of the per-image 9-class softmax vectors. The spec called for PCA of the 384-D CLS-token embeddings (which needs a backbone forward pass to produce); this softmax-vector projection is a data-grounded proxy that captures the same intuition: in-distribution images cluster at corners of the simplex (one class strongly predicted), high-entropy / OOD-like points spread toward the centroid (predictions split across classes).

      point colour = predicted class · darker outline = misclassified · hover for class + entropy + max probability. PC1 and PC2 explain ~43% of variance in the 9-D softmax space.

      Reliability diagram

      predicted confidence vs empirical accuracy

      per-signal reliability per class live · /api/info

      each cell is the empirical reliability of one signal for one class (1.0 = perfect agreement with ground truth on the held-out split)

      classic 10-bin reliability · per signal live · 736 field photos

      each panel is one backbone's confidence-vs-accuracy curve binned into 10 confidence brackets. Bars above the diagonal are over- confident; bars below are under-confident. PSV's bar in the high-confidence bin shows why its raw output needs a stacking MLP to be safe in production.

      ECE is the area-weighted absolute gap; lower is better-calibrated. n=736 final_val field photos · per-signal post-softmax · pre-stacking-MLP.

      Per-class miscalibration

      which classes hide bad behaviour at which confidence bins

      per-class miscalibration · 5 confidence bins live · 920 field photos

      each cell holds the signed gap (mean confidence − accuracy) for images of that predicted class in that confidence bin. Red = the model is overconfident (says 0.9 but is right less often); blue = underconfident. Empty cells = no predictions in that bin. Hover for the exact confidence / accuracy pair.

      cells with n < 5 predictions are dimmed (statistical noise). Hover any cell for n, mean confidence, accuracy, signed gap.

      complementary view · per-class quality on the locked test split

      per-class quality on the locked test split live · extractor

      n=920 · each cell shows 1 − metric (lower is better) so the worst- calibrated classes are the darkest. okra_enation's recall gap of 0.44 dominates · its 5 → powdery_mildew + 6 → brassica_alternaria confusions (see M4 confusion matrix) account for the bulk of that miscalibration.

      cell colour = quality gap to 1.0 · darker ochre = larger gap · green background = ≤ 0.05 gap (well-calibrated). Click a row to copy the class name.

      § Live metrics #

      What this page knows about the API right now. Polled every 5s. If no requests arrive for more than two minutes, the section dims to signal staleness.

      checking… uptime last request ago ttfb p50 err 5xx

      Throughput

      last requests, bucketed by minute (span: min)

      total in span: reqs · busiest minute:

      Latency distribution

      last requests

      break down by endpoint
      endpoint p50 p95 share

      Endpoint traffic

      last requests

      endpoint share count

      status mix

      User-flagged predictions

      data: pending extractor

      When a user clicks the 👎 button on a prediction (visible on the inference page), the disagreement is logged to a feedback table. This view will show:

      • total flags + flag rate over the last 30 days
      • top disagreements (model predicted X · user said Y)
      • per-class miscall rates

      Endpoint needed: GET /api/feedback/summary?since=30d

      § Data lineage #

      Every image that touched the model · where it came from, which split it ended up in, and a cryptographic guarantee that no test image appears in any training set. Numbers below are computed from the locked source map and recomputed every time it changes.

      Source-to-split flow

      images from source families, allocated across splits

      hover any source or split to highlight its connections · click a split to filter the provenance table below

      Dataset provenance

      every source dataset that fed any split · sortable by any column

      filter:
      source family license year crops images cite
      TOTAL

      Leakage guarantee

      pairwise check that no image path appears in two splits at once

        total paths checked: · last verified: · method:

        how the check works

        Every record in data/metadata/source_map.csv carries an image path. We build a set of paths per split, then count pairwise intersections. A non-zero intersection between test and any other split is a leak: the model would be evaluated on something it had already seen.

        A future hardening step is to replace path-identity with a SHA-256 hash of the pixel content, which catches cases where the same image was duplicated to a different filename. Path-identity is the stricter check we have today.

        what would happen on a leak

        The verifier would print every offending path with its two splits and exit with code 1. The training driver checks the same set and refuses to load a corrupt split, so the leak would block both evaluation and training pipelines.

        Quality filter

        images dropped before any split (deduplication · quality gates)