← ALL POSTS
COMPUTER VISION

Counting thrips: when your dataset is a wriggling crowd

SEPTEMBER 2026
OBJECT DETECTIONFINE-GRAINED CLASSIFICATIONAZURE CONTAINER APPSNVIDIA TRITONMLOPSAGRICULTURE

One petri dish, uploaded as a single image, holding hundreds of thrips: some larvae, some adults, all of them millimeters long and most of them overlapping. The task: count every single one, and tell the life stages apart. Manual counting at that density is slow, error-prone, and nobody's favorite job. So we built an ML model to do it. Counting insects turns out to be one of the most honest tests of an object-detection system, because the data offers nowhere to hide.

Why this is hard

Thrips are tiny, and a dish doesn't arrange them politely. They cluster, they pile on top of each other, and they orient every which way. That breaks the assumptions most detection demos quietly rely on: well-separated objects, consistent scale, clean backgrounds.

Then there's the classification problem. Larvae and adults differ in size, but it's a continuum, not two neat piles. A large larva and a small adult can look frustratingly similar, and apparent size shifts with posture and orientation. So the model has to find near-invisible objects and make a fine-grained call on each one.

And the annotation cost is brutal. Every training label is a human clicking on a nearly invisible insect: hundreds or thousands of clicks per image, each one requiring judgment about life stage. Dataset creation was a significant fraction of the total project effort, not a prelude to it.

Counting is detection in disguise

The key architectural decision: don't build a counter. Build a detector plus classifier: find every thrips as an instance, classify each as larval or adult, and let the counts fall out by summation. A separate counting head would just be a worse detector with extra steps.

This framing also handles the density problem head-on. In a crowded dish, the failure mode isn't missing the concept of a thrips, it's double-counting one insect or merging two into one. Non-maximum suppression tuning and overlap-aware evaluation matter more here than they do in sparse scenes. We evaluated on dense, real dishes, not the easy sparse ones, because the sparse ones were never the job.

What made it work

  • Annotation protocol first. Clear, written rules for ambiguous cases (larva or small adult?), applied consistently by every annotator. Inconsistent labels at this scale don't average out, they become the model's confusion.
  • Respecting class imbalance. Larval-to-adult ratios vary dish to dish. The evaluation had to cover the full range, not just the balanced middle.
  • Density-stratified evaluation. We measured performance separately on sparse, medium, and packed dishes. Aggregate metrics hide exactly the regime you care about.

Under the hood

The stack here is less glamorous than the fungal pipeline: no foundation-model shortcut, just detection done carefully:

  • Detection → per-instance object detection. The model outputs a bounding box (or mask) per thrips, each with a larval/adult classification. Counts are derived by summation, never predicted directly. At millimeter scale, small-object detection lives or dies on anchor/proposal density and input resolution. Downsampling for speed is how you lose half your insects.
  • Classification → fine-grained head. Larval vs adult is a fine-grained problem: high intra-class variation (posture, orientation) and low inter-class separation (large larva vs small adult). The classifier needs the per-instance crop at full resolution, not the downscaled dish.
  • Evaluation → density-stratified. Standard mAP over the whole set hides the regime that matters, so we slice metrics by crowd density. A model that's 95% on sparse dishes and 70% on packed ones is a 70% model for all practical purposes.
  • Serving → containers, with Triton evaluated. Inference ran containerized on Azure Container Apps under the same MLOps discipline as the fungal pipeline: versioned models, reproducible runs, monitored predictions. We also tested NVIDIA Triton Inference Server for serving efficiency: dense dishes mean many instances per image, and batching that workload efficiently is a throughput problem as much as an accuracy one.

The unglamorous hero of this project wasn't a model at all: it was the annotation protocol: written rules for ambiguous life-stage calls, applied consistently across thousands of clicks. At this object scale, label consistency is model architecture.

Practical lessons

  • If you need counts, detect instances. Counting falls out of good detection; the reverse isn't true.
  • Annotate the hard cases deliberately. Overlapping clusters and borderline life stages are the dataset, not the exception.
  • Small objects punish sloppy evaluation. A few pixels of box jitter is noise on a car and catastrophic on a thrips. Pick metrics that reflect the object's scale.
  • Life-stage classification is fine-grained recognition wearing a counting costume. Treat it with the care fine-grained problems demand: consistent labels, per-class metrics, and skepticism toward aggregate accuracy.

The thrips counter never got the headline numbers the fungal pipeline did, but it may be the more instructive project: no foundation-model shortcut, no clean staging to hide behind, just dense, tiny, ambiguous objects and the unglamorous work of labeling them well enough for a model to learn. Some problems you solve with leverage. This one you solve with care.