Grain detector (RF-DETR small), v1

Author: Niranjan Kukkala (niranjan.reddy.wo@gmail.com) License: CC BY-NC 4.0 (education and non-commercial use; follows the dataset license). Dataset: niru-5/manama_course_dataset_grain_detection. Code (annotation app, training, weight/cost tasks): niru-5/manama_data_collection_course. Citation: a citation will be added after the full release.

An object detector that finds and classifies small grains and seeds lying on a flat surface: corn, peanuts, popcorn_corn, pumpkin, sunflower, wheat. Each photo is expected to contain one kind of grain; the model draws one box per grain and names its class. It is a fine-tuned RF-DETR small (about 32 M parameters) and was trained for a course on object detection.

Intended use: education and non-commercial experiments (counting grains, estimating pile area or weight from detections, comparing evaluation settings). License: CC BY-NC 4.0, following the training dataset. Not for commercial use and not for any safety-relevant decision.

Training data

The public Grain Detection Dataset (course release): 470 photos cut into tiles of at most 1500 px with 20 % overlap (1,875 tiles, 27,611 boxes, six classes). This model was trained on the train split only (339 photos, 1,142 tiles, 19,429 boxes) and selected on the validation split (65 photos, 362 tiles). The test split (66 photos) was used only for the final report. The splits are by photo group, never by tile.

Honesty caveat: the dataset labels are automatic labels that have not been reviewed by a human. All numbers below measure agreement with those labels, not with the real grains. Real accuracy can be lower or higher.

Quick start

1. With transformers (loads the weights; a plain forward pass resizes the whole photo to 512 x 512, which is crude for dense photos: in a quick check on one test photo it found 56 boxes with wrong classes, whereas tiled detkit inference below found 54 correct ones. Use it as a loading example or tile the photo yourself, and use detkit for real results):

import torch
from PIL import Image
from transformers import AutoImageProcessor, AutoModelForObjectDetection

repo = "niru-5/manama_course_rfdetr_grain_detection"
processor = AutoImageProcessor.from_pretrained(repo)
model = AutoModelForObjectDetection.from_pretrained(repo).eval()

image = Image.open("my_photo.jpg").convert("RGB")
with torch.no_grad():
    out = model(**processor(images=image, return_tensors="pt"))
res = processor.post_process_object_detection(out, threshold=0.38, target_sizes=[image.size[::-1]])[0]
for score, label, box in zip(res["scores"], res["labels"], res["boxes"]):
    print(model.config.id2label[int(label)], round(float(score), 3), [round(float(v)) for v in box])

2. With the course toolkit (detkit), which tiles large photos (tile 1500, overlap 0.2) exactly as in training:

hf download niru-5/manama_course_rfdetr_grain_detection --local-dir weights/grain_rfdetr_small_v1/final
python -m detkit predict --model weights/grain_rfdetr_small_v1/final --images <photo> --score 0.38

detkit predict reads the tiling from detkit_meta.json (override with --tile 1500 --overlap 0.2); it writes predictions and an overlay image to --out. The same weights are used for box proposals in the annotation app (python -m detkit app --ckpt weights/grain_rfdetr_small_v1/final).

Training settings (short)

RF-DETR small, 6 classes, tile-level training, 30 epochs, effective batch size 16 (2 x 8 gradient accumulation), learning rate 1e-4 (cosine, 5 % warm-up), weight decay 1e-4, AdamW, images resized to 512 x 512 by the model's processor, augmentation on (horizontal flip, brightness/contrast, small colour and perspective changes), seed 1337. The checkpoint with the best validation mAP is shipped (the last epoch). A run without augmentation reached a lower validation mAP (0.870 vs 0.887 tile-level; 0.877 vs 0.893 photo-level).

Full training log and step-by-step reproduction (commands, versions, expected curve and scores): see TRAINING.md.

Recommended score threshold: 0.38

Chosen on the validation photos (best F1 at IoU 0.5, tiled inference). Do not use 0.5 by default: the model's scores are not calibrated probabilities. On the test photos the F1-optimal threshold would have been 0.49, so anything in 0.35 to 0.5 behaves similarly (test F1 0.972 at 0.38 versus 0.974 at 0.49).

Results (summary)

Everything is measured against the dataset's automatic, unreviewed labels (IoU 0.5, threshold 0.384 chosen on validation). Headline: test photos F1 0.972, AP@0.5 0.972, mAP@[.5:.95] 0.842. The full tables are in the next section; machine-readable numbers (per-class tables, count and area errors, AP by IoU) are in results/metrics.json; curves and sweeps are in results/ (*_pr_curve.png, *_f1_vs_threshold.png, *_count_scatter.png, *_per_class.csv, *_threshold_sweep.csv) and sample predictions on test photos in results/overlays/.

Results in detail (IoU 0.5 unless noted; threshold 0.38, chosen on validation only)

Photo level = full photos, tiled inference with tile 1500 and overlap 0.2. Tile level = the 1500 px tiles one by one.

split level precision recall F1 AP@0.5 mAP@[.5:.95] count MAE / MAPE
val (65 photos) photo 0.988 0.983 0.986 0.991 0.893 0.62 / 2.8 %
test (66 photos) photo 0.975 0.968 0.972 0.972 0.842 1.15 / 3.2 %
val (362 tiles) tile 0.943 0.985 0.964 0.991 0.892 0.68 / 12.0 %
test (371 tiles) tile 0.943 0.970 0.956 0.975 0.846 0.79 / 14.9 %

Count error per photo: the summed count over the photo is off by only -0.7 % on test (-0.6 % on val) in total; the total box area is 2.8 % above the labels on test photos. The tile-level count percentage is large because many tiles hold few grains and tile borders cut grains (a grain cut by a border is counted differently by labels and model).

Per class, test photos (threshold 0.38):

class boxes precision recall F1 AP@0.5
corn 297 0.987 0.987 0.987 0.987
peanuts 344 0.982 0.977 0.980 0.980
popcorn_corn 480 0.994 0.992 0.993 0.994
pumpkin 432 0.917 0.919 0.918 0.922
sunflower 380 0.992 0.945 0.968 0.956
wheat 695 0.981 0.984 0.983 0.987

Validation and test are small (about 65 photos each, a handful of independent groups per class), so per-class numbers are noisy: a few photos can move a class by several points.

Augmentation comparison (validation, same settings, threshold 0.384)

run level precision recall F1 AP@0.5 mAP@[.5:.95] count MAE / MAPE best-F1 threshold
augmentation on (shipped) photo 0.988 0.983 0.986 0.991 0.893 0.62 / 2.8 % 0.38
augmentation off photo 0.989 0.969 0.979 0.978 0.877 0.89 / 3.8 % 0.16
augmentation on (shipped) tile 0.943 0.985 0.964 0.991 0.892 0.68 / 12.0 % 0.76
augmentation off tile 0.974 0.969 0.971 0.979 0.874 0.45 / 7.7 % 0.29

Augmentation raised mAP and recall at photo level; the no-augmentation run has higher tile-level F1 at this fixed threshold only because its scores sit lower or higher (its best threshold is different), so compare mAP, not F1 at 0.384. The no-augmentation model was evaluated on validation only, never on test.

Count and area error (photo level, threshold 0.384)

split summed count error count MAE per photo count MAPE count R^2 summed box-area error
val -0.6 % 0.62 2.8 % see results/metrics.json -1.9 %
test -0.7 % 1.15 3.2 % 0.992 +2.8 %

Where it is weakest

  • Pumpkin seeds (lowest F1, 0.92 on test): peeled, pale kernels are sometimes called corn (seen in at least one test photo), and one test photo has labels that look wrong (tiny boxes in a regular grid), which alone costs recall and precision.
  • Sunflower seeds (recall 0.945): touching and overlapping seeds in dense piles are merged or missed.
  • Tight boxes: AP falls from 0.97 at IoU 0.5 to 0.68 at IoU 0.9 (test), so box edges are good but not precise; area estimates from boxes carry a few percent error.
  • Other backgrounds, lighting or camera distances: the dataset covers a few backgrounds and one lighting style, so expect worse results elsewhere. The model assumes one grain type per photo and does not separate mixed piles reliably.

Reproducibility

Full step-by-step guide (environment versions, data download, exact commands, expected learning curve and scores): TRAINING.md. Headline recipe: RF-DETR small (Roboflow/rf-detr-small), 30 epochs, effective batch 16 (2 x 8 gradient accumulation), learning rate 1e-4 (cosine, 5 % warm-up), AdamW weight decay 1e-4, seed 1337, augmentation on, tiles 1500 px / overlap 0.2, trained on the dataset's train split (dataset checksum: data fingerprint grep -E " (coco|photos|annotations)/" checksums.sha256 | sha256sum = ed266c44b5444739ddb0cda907e847f37dceff6eb99bb2f8a8ae4bbe3c5e0e2c). About 2 hours on one 8 GB GPU. Hyperparameters and per-epoch history: training_config/. Expected tolerance: seeds are fixed but GPU kernels are not forced deterministic, so a re-run is not bit-identical; expect validation mAP and F1 within about +-0.005 of the numbers above. Test scores are noisier (66 photos from a handful of independent groups per class).

Limitations and responsible use

Trained and evaluated on automatically labelled data of limited variety (see above). Check overlays on your own photos before trusting counts, and re-tune the threshold on photos you reviewed yourself. The weights inherit the dataset's non-commercial license.

Downloads last month
58
Safetensors
Model size
31.8M params
Tensor type
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for niru-5/manama_course_rfdetr_grain_detection

Finetuned
(11)
this model

Dataset used to train niru-5/manama_course_rfdetr_grain_detection

Evaluation results

  • F1 @ IoU 0.5, threshold 0.384 on Grain Detection Dataset (course release), test split, photo level
    test set self-reported
    0.972
  • Precision @ IoU 0.5 on Grain Detection Dataset (course release), test split, photo level
    test set self-reported
    0.975
  • Recall @ IoU 0.5 on Grain Detection Dataset (course release), test split, photo level
    test set self-reported
    0.968
  • AP@0.5 on Grain Detection Dataset (course release), test split, photo level
    test set self-reported
    0.972
  • mAP@[.5:.95] on Grain Detection Dataset (course release), test split, photo level
    test set self-reported
    0.842
  • F1 @ IoU 0.5, threshold 0.384 on Grain Detection Dataset (course release), validation split, photo level
    validation set self-reported
    0.986
  • AP@0.5 on Grain Detection Dataset (course release), validation split, photo level
    validation set self-reported
    0.991
  • mAP@[.5:.95] on Grain Detection Dataset (course release), validation split, photo level
    validation set self-reported
    0.893