DEV Community

Cover image for What I learned merging seven public datasets to train a single-class shelf detector
MORINAGA
MORINAGA

Posted on

What I learned merging seven public datasets to train a single-class shelf detector

The conclusion first: licence filtering matters more than dataset size when you plan to use the model commercially. I merged seven public Roboflow datasets into 11,667 images — all CC BY 4.0 — trained YOLO11n on Apple MPS, and reached mAP50 0.844 on a held-out test set of 277 images and 1,255 instances. The model now runs on a Raspberry Pi 3 as a proof-of-concept empty-shelf detector. It cost zero cloud GPU spend and took about a day of scripting before I touched any training code.

Why the camera never identifies products

Before dataset sourcing makes sense, the problem framing matters — because the framing determines which datasets are useful.

The shelf scanner I built does not try to recognise products. It detects empty space only. Single class: empty_space. When a detection fires, the gap's position is looked up in a planogram — a shelf-layout document that maps shelf positions to SKUs. The planogram already knows what belongs in that slot.

The rationale is practical. Training a per-product recogniser means a dataset for every SKU in the store, constant retraining as products rotate, and enough confidence to avoid confusing look-alikes. Empty-space detection generalises across stores and product ranges because an absence looks the same regardless of what was supposed to be there. The annotation burden drops, and I can draw on generic retail-shelf imagery rather than a custom photo collection.

This framing is what makes Roboflow Universe usable. There are hundreds of labelled shelf datasets there, most annotated at product level. A meaningful subset has "empty" or "gap" annotations; the rest can contribute negative examples. The single-class design keeps the merge logic simple: remap every relevant annotation to empty_space and discard the rest.

The licence filter

Roboflow Universe hosts datasets under mixed licences: CC BY 4.0, CC BY-NC 4.0 (non-commercial), CC0, and various custom terms. My filter was strict: CC BY 4.0 only.

That licence permits commercial adaptation with attribution. If this PoC moves past a home shelf into a real deployment, the training data's licence needs to support that. CC BY-NC 4.0 — which covers a large number of the most complete retail-shelf datasets — does not.

The cost of this choice: the pool shrinks. I reviewed more datasets than I used and excluded several well-annotated ones purely on licence grounds. A few had no licence listed at all, which I treated as "all rights reserved" and skipped. The honest way to handle unlicensed datasets is the same as the honest way to handle unlicensed code: if the terms are unclear, do not build on it. The time cost of chasing dataset authors for clarification was not worth it given the alternatives.

The parallel in my directory-site work is similar — GitHub's API returns licence values that cannot always be displayed verbatim, and the safest call is to surface what the API provides and flag unknowns rather than infer. Same principle: licence ambiguity defaults to exclusion.

What 11,667 images across seven datasets actually looks like

After filtering, I had seven datasets that:

  • Contained "empty shelf" or "gap" annotations, or enough context to derive empty space from product-level annotations
  • Were CC BY 4.0
  • Had at least 200 images (anything smaller would contribute noise more than signal)

The merged split after deduplication and class remapping:

Split Images
Train 9,358
Validation 1,162
Test 1,147
Total 11,667

The split was not a blind concatenation. Each source dataset's own train/valid/test assignment was preserved where it existed, then redistributed to approximate an 80/10/10 split across the combined corpus. The critical check: cross-dataset contamination, where a test image from source A appears in the training split from source B.

I computed perceptual hashes across the merged corpus and verified that no image hash crossed the train/test boundary. At 11,667 images this took a few minutes in Python. It is the kind of boring check that determines whether the test metric is honest.

Fine-tuning YOLO11n on Apple MPS — zero cloud GPU spend

Training used Ultralytics on my Mac with Apple's MPS backend. Config:

model: YOLO11n
device: mps
imgsz: 640
batch: 16
epochs: 60
Enter fullscreen mode Exit fullscreen mode

I also trained YOLO11s (the next size up) for comparison. MPS handles both at batch=16 without issues. Wall-clock time was a few hours per run — longer than renting an A100, but the cost was electricity. The verified_at discipline I applied to articles comes from the same instinct: logging when you actually ran something is different from logging that it "should work."

Validation metrics at epoch 60:

Model Precision Recall mAP50
YOLO11n 0.782 0.734 0.792
YOLO11s 0.833 0.736 0.820

YOLO11s scores better, as expected. The gap is real — 0.792 vs 0.820 on validation — but narrower than I expected given the size difference. On a Pi 3 ARM CPU via NCNN, the inference time difference matters more than that gap, so I deployed YOLO11n. The deployment tradeoffs are a separate topic.

What the test-set numbers actually measure

After training, I evaluated on a held-out test set the model had not seen during training or hyperparameter tuning: 277 images, 1,255 instances.

Results: precision 0.82, recall 0.786, mAP50 0.844.

What 0.844 does and does not mean:

It does mean: the model correctly localises and classifies empty shelf regions in images it hasn't seen before, at standard IoU thresholds. That is a useful result on a dataset I consider honest (deduplication-verified, licence-filtered, no contamination across train/test).

It does not mean: it works on retail shelves in the wild. All 11,667 images came from public datasets, which skew toward controlled store photography. My PoC runs against a bookshelf at home — a different context with different lighting, angles, and object types. The held-out test is an honest measure of the model's generalisation within this specific data distribution, not across all possible shelves.

It does not mean: these precision/recall numbers are optimal for the use case. A false negative (missed empty gap) is a missed restocking event. A false positive (flagging a non-existent gap) wastes time. I left the detection threshold at 0.35 and used post-processing to absorb errors. The three post-processing layers — ROI masking, baseline subtraction, and temporal majority vote — do more practical work than the last few mAP points would.

The distinction between validation curve and held-out test matters. The validation metric (0.792) guided training decisions. The test metric (0.844) is what I report as the honest result. They're different numbers from different data.

What licence filtering costs and why I'd do it again

The CC BY 4.0 constraint has a real price: fewer images and fewer dataset authors. Some of the most annotated shelf datasets in Roboflow Universe are CC BY-NC. If I had relaxed the filter, the training set could plausibly have been 2x or 3x larger, and the model would likely score higher.

That tradeoff is context-dependent. For internal use only — a research demo, a personal project — NC-licensed data is fine. For a prototype heading toward a commercial deployment, the "we'll sort the licences later" plan tends to produce a retrain-from-scratch situation at the worst moment.

I have seen the same pattern in content pipelines. Auditing published articles after the fact found errors that had accumulated for months — the detection lag problem described in the PDCA detection lag post. The same logic applies to dataset provenance: the earlier you encode the constraint, the less remediation work accumulates downstream. The output inspection principle — checking what came out, not just that the process ran — applies at every layer of this kind of pipeline.

The other thing filtering teaches: what you excluded. Reviewing datasets that did not qualify gave me a clearer picture of what was available and what assumptions the existing datasets make. That survey is worth doing even if you end up using all seven datasets you find.

Current status and limits

The system has produced 19 scans on one bookshelf and correctly detected a gap. That is the operational claim — a PoC on one shelf, not a deployed system, not a multi-store result.

What is real: the dataset merge, the training run, the NCNN export, and the confirmed detection on hardware. What is not measured: power draw, accuracy on retail shelves with varying lighting, performance with a moving camera or a different product category.

I'll publish real operational numbers when they exist.


FAQ

Q: How do I check for cross-dataset image contamination?

Compute a perceptual hash (pHash) or MD5 of each image in the merged dataset before splitting. Any hash that appears in both your training and test split is contamination. At 11,667 images this takes a few minutes in Python with the imagehash library. The script compares sets, not individual files, and exits with a list of collision paths if any cross the train/test boundary.

Q: Can I train on CC BY-NC datasets for a personal project and then switch later?

The licence that applies is the one in effect when you trained the model. A model derived from NC-licensed data carries that restriction regardless of what you relicense downstream. In practice, "switch later" means retrain from scratch. The filter up front is cheaper.

Q: YOLO11n vs YOLO11s — which makes sense to deploy on a Pi 3?

I deployed YOLO11n. The mAP50 gap (0.792 vs 0.820 on validation) is real but narrow. On a Pi 3 ARM CPU via NCNN, inference time matters more than that gap when your scan cadence is hourly and post-processing absorbs errors. If you have a Pi 4 or 5 with more RAM and faster cores, YOLO11s becomes more reasonable.

Q: What Roboflow datasets qualify under CC BY 4.0 for shelf detection?

I'm not publishing the specific list because it changes as dataset owners update their licences. The reliable path: search Roboflow Universe for "shelf" or "retail shelf", filter by "CC BY 4.0" in the licence dropdown, and verify the licence on the dataset page before downloading. Licence display in search results and the actual dataset licence page have sometimes disagreed — check the canonical source.

Q: Why 416px inference when training used 640px?

The NCNN runtime supports inference at a different resolution than training. On the Pi 3 with 906 MB RAM, 416px reduces memory pressure and speeds up inference. The tradeoff is small accuracy loss — acceptable when the temporal majority vote (detect in 2 of 3 consecutive scans) absorbs per-scan errors anyway.


Related: The three post-processing layers that made the shelf detector practical on a Pi 3 · Repos I keep returning to: NCNN, Roboflow Universe, Turso, Pagefind

Part of an ongoing 6-month experiment running three AI-curated directory sites. The technical claims here are real; this article was AI-assisted.

Top comments (0)