cubert-gmbh/X4_SWIR_Industrial_Foreign_Object_Detection_Bedding
Hyperspectral Foreign-Object Detection in Bedding — Full Dataset (VIS + SWIR) A 6-band VIS+SWIR hyperspectral dataset for industrial foreign-object detection on a bedding substrate (a tray of wood-shaving / sawdust animal bedding). Captured with a Cubert Ultris X4 + SWIR rig — 6 spectral bands at 450 / 550 / 625 nm (VIS) and 1050 / 1200 / 1450 nm (SWIR), 2400 × 4900 pixels per frame. 252 frames (193 train · 59 val), 51 frames carry pixel-level polygon… See the full description on the dataset page: https://huggingface.co/datasets/cubert-gmbh/X4_SWIR_Industrial_Foreign_Object_Detection_Bedding.
<p align="center"> <img src="https://raw.githubusercontent.com/cubert-hyperspectral/cuvis.sdk/main/branding/logo/banner.png" alt="Cubert Hyperspectral" width="560"/> </p>
<p align="center"> <a href="https://docs.cuvis.ai"><img src="https://img.shields.io/badge/Docs-docs.cuvis.ai-0aa?logo=readthedocs&logoColor=white" alt="Cuvis.AI docs"/></a> <a href="https://github.com/cubert-hyperspectral/cuvis-ai"><img src="https://img.shields.io/badge/GitHub-cuvis--ai-24292e?logo=github" alt="Cuvis.AI on GitHub"/></a> <a href="https://huggingface.co/datasets/cubert-gmbh/XMRIndustrialForeignObjectDetection_Lentils"><img src="https://img.shields.io/badge/Sibling-Lentils%20dataset-ffd21e?logo=huggingface&logoColor=000" alt="Sibling lentils dataset"/></a> </p>
Hyperspectral Foreign-Object Detection in Bedding — Full Dataset (VIS + SWIR)
A 6-band VIS+SWIR hyperspectral dataset for industrial foreign-object detection on a bedding substrate (a tray of wood-shaving / sawdust animal bedding). Captured with a Cubert Ultris X4 + SWIR rig — 6 spectral bands at 450 / 550 / 625 nm (VIS) and 1050 / 1200 / 1450 nm (SWIR), 2400 × 4900 pixels per frame. 252 frames (193 train · 59 val), 51 frames carry pixel-level polygon annotations spanning 23 foreign-object classes.
This is the bedding counterpart to the 61-band lentils dataset at `cubert-gmbh/XMR_Industrial_Foreign_Object_Detection_Lentils`. Where the lentils set uses the visible-only 61-band Ultris XMR, this set pairs three visible bands with three SWIR bands — the SWIR channels make water, alcohol, transparent plastics and colour-matched polymers separable from the organic bedding even when they are near-invisible in RGB.
The dataset is structured for anomaly detection: the train split is 100 % normal/background (clean bedding, no foreign objects), and the val split holds the anomalous frames (foreign objects placed by hand) plus a few normal frames. This mirrors a real inspection deployment — you learn the appearance of clean product, then flag anything that deviates.
Summary
Foreign-object classes
Class id 0 (background) is the bedding substrate + tray. The 23 foreign-object classes and their annotated polygon counts (object-level, from the LabelMe source polygons) are:
The PLA_<colour>_<size> families are 3-D-printed PLA plastic fragments at five physical sizes (1, 2, 4, 8, 16 mm) in three colours (black, blue, white). The 2 mm and 1 mm fragments — only a handful of pixels each — are the hardest to localise and the most interesting test of spectral (vs spatial) discrimination. water and alcohol are liquid spills; POMC, PET, transparent_plastic are polymer pieces; leaf / fake_leaf are organic vs synthetic foliage.
⚠ See Known data issues below forfake_leaf(id 6), thePLA_blacK_4mmspelling, and theframe_10label gap.
Why VIS + SWIR
An RGB sensor collapses light into three visible bands. This rig adds three short-wave-infrared bands (1050 / 1200 / 1450 nm). The 1450 nm band sits on a water-absorption feature, so water and alcohol spills go dark in SWIR while looking like wet sawdust in RGB. Transparent and colour-matched plastics that blend into the bedding in the visible range scatter differently in SWIR. The six bands together give a compact material fingerprint that separates foreign objects which are near-isoluminant in RGB.
The example frames below render the same cube two ways: a VIS-RGB composite (625 / 550 / 450 nm → R/G/B) and a SWIR pseudo-RGB (1450 / 1200 / 1050 nm → R/G/B), both per-channel min-max to uint8.
Example frames
All examples are rendered directly from the .cu3s in this repo (native 2400 × 4900), per-channel min-max normalised to uint8. Annotated overlays draw the LabelMe polygons (native space).
Small PLA fragments (ok_nok, val) — the hard case
data/val/20250311_104919_frame_33_ok_nok_rdx_rwx.cu3s
Water spill (nok_ok, val)
data/val/20250311_101004_frame_9_nok_ok_rdx_rwx.cu3s · 17 annotated regions
Multi-object scene (nok_nok, val)
data/val/20250310_153943_frame_121_nok_nok_rdx_rwx.cu3s
Normal / background (ok_ok, val) — no foreign objects
data/val/20250310_083936_frame_29_ok_ok_rd4_rw8.cu3s
Known data issues
This dataset is released as-is from the lab capture, with three documented quirks. None of them block training/eval, but you should know about them:
- `frame_10` label gap (the documented fault).
data/val/20250311_101035_frame_10_nok_ok_rdx_rwx.cu3shas a `nok` (anomalous) filename but no annotation — its LabelMe polygon set and mask are absent. The filename label is correct (the frame does contain a foreign object), but the pixel-level ground truth was never drawn. It is flagged insplits.csvwithlabel_fault=1(the only such row). For image-level evaluation use the filename label (filename_label=1); for pixel-level evaluation this frame contributes no positives and should be excluded or treated as a known miss.
- `fake_leaf` (class id 6) has no polygon annotations. The class exists in the integer class map and appears as a few thin pixel slivers in the rasterised mask PNGs (
annotations_raw/), but there is no corresponding LabelMe polygon, so the COCO files contain 0 `fake_leaf` regions. Treatfake_leafas effectively unannotated at the object level.
- `PLA_blacK_4mm` spelling. Class id 9 is spelled with a capital
K(PLA_blacK_4mm) in the canonical class map, while the LabelMe source labels spell itPLA_black_4mm. The COCO build maps both case-insensitively to id 9. The id is authoritative; the string is kept verbatim for traceability.
Coordinate spaces
There are two pixel spaces in play — be explicit about which you use:
- Native (this repo's COCO + masks): 2400 × 4900. This is what
cuvisreturns when you open a.cu3sand readm.data["cube"].array. All COCO polygons (annotations_canonical/,data/<split>/<stem>.json) and the raw mask PNGs (annotations_raw/) live in this space. - Training crop: 1800 × 4300. The reference Dinomaly/EfficientAD pipelines crop the tray borders with
cube[300:-300, 300:-300]before training. If you reproduce those pipelines, apply the same crop to both cube and annotations; otherwise work in native space directly.
Repository layout
README.md
LICENSE (Apache-2.0)
.gitattributes (LFS for *.cu3s, *.png, *.mp4)
splits.csv # 1 row per frame (252 rows)
class_map.json # {class_name: id}, 0..23
annotations_canonical/
train_global_coco.json # merged COCO (train; all background)
val_global_coco.json # merged COCO (val; 261 polygons, native space)
annotations_raw/
labels/ # original LabelMe JSON (*_RGB.json) + rasterised
# mask PNGs (*_mask.png), verbatim, 2400×4900
README.md # raw-label format notes
assets/
examples/ # rendered VIS-RGB / SWIR / annotated PNGs
data/
train/
<stem>.cu3s # one hyperspectral frame (Ultris X4 + SWIR)
<stem>.json # per-cu3s COCO (image_id 0; background for train)
val/
<stem>.cu3s
<stem>.json # per-cu3s COCO (native 2400×4900)<stem> encodes capture metadata: YYYYMMDD_HHMMSS_frame_<n>_<tray-state>_<object-state>_<recipe> where the _ok_ok_ / _ok_nok_ / _nok_ok_ / _nok_nok_ token pair is the filename-level normal/anomalous label (see splits.csv → filename_label).
Per-<stem>.json COCO schema
{
"info": { "file_name": "<stem>.cu3s", "split": "train|val", "space": "native 4900x2400" },
"licenses": [ { "id": 0, "name": "Apache-2.0", "url": "…" } ],
"categories": [ { "id": 0..23, "name": "background|water|…|water&alcohol-tray" } ],
"images": [
{ "id": 0, "file_name": "<stem>.cu3s",
"width": 4900, "height": 2400, "channels": 6,
"wavelength": [450, 550, 625, 1050, 1200, 1450] }
],
"annotations": [
{ "id": …, "image_id": 0, "category_id": 1..23,
"bbox": [x, y, w, h], "segmentation": [[…polygon…]],
"area": <shoelace px area>, "iscrowd": 0 }
]
}Annotations are semantic polygons transcribed from the LabelMe source. Normal/background frames have an images entry and an empty annotations list. annotations_canonical/val_global_coco.json is the same content merged across the split with global image_id (0..58, matching splits.csv row order within val) and globally-unique annotation ids.
splits.csv columns
Splits
The all-normal train split is intentional: this is an anomaly-detection benchmark. Train an unsupervised / SSL model (e.g. Dinomaly or EfficientAD) on the clean bedding, then evaluate foreign-object localisation on the annotated val frames.
How to load
List the annotated val frames
import csv
from huggingface_hub import hf_hub_download
splits = hf_hub_download(
repo_id="cubert-gmbh/X4_SWIR_Industrial_Foreign_Object_Detection_Bedding",
repo_type="dataset", filename="splits.csv")
with open(splits) as f:
val_ann = [r for r in csv.DictReader(f)
if r["split"] == "val" and r["has_annotation"] == "1"]
print(len(val_ann), "annotated val frames")Stream one cu3s + COCO and render a VIS-RGB composite
from huggingface_hub import hf_hub_download
import json, cuvis, numpy as np
from PIL import Image
repo = "cubert-gmbh/X4_SWIR_Industrial_Foreign_Object_Detection_Bedding"
stem = "20250311_104919_frame_33_ok_nok_rdx_rwx"
cu3s = hf_hub_download(repo_id=repo, repo_type="dataset", filename=f"data/val/{stem}.cu3s")
js = hf_hub_download(repo_id=repo, repo_type="dataset", filename=f"data/val/{stem}.json")
cuvis.init() # or cuvis.init("/path/to/cuvis/settings")
sf = cuvis.SessionFile(cu3s)
cube = sf[0].data["cube"].array # (2400, 4900, 6) uint16 reflectance×10000
wl = list(sf[0].data["cube"].wavelength) # [450, 550, 625, 1050, 1200, 1450]
# VIS-RGB composite: 625 / 550 / 450 nm -> R / G / B
idx = [wl.index(t) for t in (625, 550, 450)]
sel = cube[..., idx].astype(np.float32)
u8 = np.zeros_like(sel, np.uint8)
for c in range(3):
lo, hi = np.percentile(sel[..., c], (0.5, 99.5))
u8[..., c] = (np.clip((sel[..., c] - lo) / max(hi - lo, 1e-6), 0, 1) * 255).astype(np.uint8)
Image.fromarray(u8, "RGB").save("frame_rgb.png")
anns = json.load(open(js))
print("polygons:", len(anns["annotations"]))Mirror everything to a local directory
huggingface-cli download \
cubert-gmbh/X4_SWIR_Industrial_Foreign_Object_Detection_Bedding \
--repo-type=dataset --local-dir=./bedding_fullOr fetch only the lightweight metadata (skip the 167 GB of cubes) with huggingface_hub.snapshot_download(..., allow_patterns=["*.csv","*.json","*.md","assets/**","annotations_raw/**"]).
Acquisition setup
- Camera: Cubert Ultris X4 + SWIR (6 bands: VIS 450/550/625 nm, SWIR 1050/1200/1450 nm)
- Subject: animal-bedding substrate (wood shavings / sawdust) in a tray, with hand-placed foreign objects (PLA fragments, liquids, polymer pieces, foliage)
- Processing mode: Reflectance (cu3s carry the calibration;
cuvisreturns u16 reflectance ×10000) - Native frame: 2400 × 4900 × 6
Lab proof-of-concept, not a production deployment study.
Citation
@misc{raj2026beddinghsi,
title = {Hyperspectral VIS+SWIR Foreign-Object Detection in Bedding Substrate},
author = {Raj, Anish},
institution = {Cubert GmbH},
year = {2026},
howpublished = {Hugging Face Datasets},
url = {https://huggingface.co/datasets/cubert-gmbh/X4_SWIR_Industrial_Foreign_Object_Detection_Bedding}
}License
Released under the Apache License 2.0 — see `LICENSE`. Matches the licensing of other Cubert public datasets on Hugging Face.
Contact
Recorded and processed by the AI Team @ Cubert.
- Author: Anish Raj — <raj@cubert-gmbh.de>
- Team: <cuvis.ai@cubert-gmbh.de>
