CoolFace
Datasetpublic

Aspirin4/synthetic-glass-with-liquid-filled

๐Ÿฅƒ Glass Half Full โ€” Synthetic Glass with Liquid Filled 8,000 synthetic images of drinking glasses with varying liquid fill levels, rendered with Blender Cycles (physically-based path tracer) at 256ร—256 resolution. Every image ships with perfect YOLO-format bounding-box labels for two classes โ€” glass and liquid โ€” computed directly from 3D geometry (no human annotation). Built for the Existential Glass Analyzer, a browser-based model that answers the timeless question: is yourโ€ฆ See the full description on the dataset page: https://huggingface.co/datasets/Aspirin4/synthetic-glass-with-liquid-filled.

sourceHugging Faceapache-2.0updated 1mo agoView on Hugging Face
0likes98downloads
Dataset Card

๐Ÿฅƒ Glass Half Full โ€” Synthetic Glass with Liquid Filled

8,000 synthetic images of drinking glasses with varying liquid fill levels, rendered with Blender Cycles (physically-based path tracer) at 256ร—256 resolution. Every image ships with perfect YOLO-format bounding-box labels for two classes โ€” glass and liquid โ€” computed directly from 3D geometry (no human annotation).

Built for the Existential Glass Analyzer, a browser-based model that answers the timeless question: is your glass half full or half empty?

Images8,000 (train 6,394 / val 791 / test 815)
Resolution256 ร— 256, RGBA PNG
Classes0: glass, 1: liquid
Fill range~0.14 โ€“ 0.96 (liquid bbox height / glass bbox height)
Annotation styleYOLO + COCO (parquet)
RendererBlender Cycles, domain-randomized
LicenseApache 2.0

Table of contents


Quick start

python
from datasets import load_dataset

ds = load_dataset("Aspirin4/synthetic-glass-with-liquid-filled")
print(ds["train"][0]["objects"])   # bbox, category, area, iscrowd
print(ds["train"][0]["fill_ratio"])

# PyTorch + transforms if you need a dataloader
ds.set_format("torch")

YOLO (Ultralytics):

bash
yolo detect train data=data.yaml model=yolov8n.pt epochs=100 imgsz=256

Why synthetic?

No public dataset exists for "glass fill level detection." Manual labeling of transparent liquids is slow, subjective, and inconsistent โ€” where does the liquid really end when you can see through the glass?

Synthetic data solves this cleanly:

  • โ€”Mathematically exact bounding boxes โ€” labels are projected from 3D geometry, so they are pixel-perfect by construction (zero annotation cost, zero human error).
  • โ€”Full coverage of the fill range โ€” fill levels are sampled uniformly, so the model sees the entire 0โ€“100% continuum.
  • โ€”Controlled domain randomization โ€” lighting, camera, liquid color, ice, and environment are varied systematically, which is exactly what a downstream real-world model needs to generalize.

Data formats

The same 8,000 images are provided in three formats so the dataset works with any toolchain without conversion.

1. Parquet (recommended)

One row per image, COCO-style annotations, images referenced by path. These files are what power the dataset viewer on this page and what load_dataset reads:

  • โ€”train-00000-of-00001.parquet
  • โ€”val-00000-of-00001.parquet
  • โ€”test-00000-of-00001.parquet
python
from datasets import load_dataset

ds = load_dataset("Aspirin4/synthetic-glass-with-liquid-filled")
row = ds["train"][0]

row["image"]            # PIL image (decoded from images/glass_XXXXX.png)
row["image_id"]         # 4747
row["fill_ratio"]       # 0.179 (liquid height / glass height)
row["objects"]["bbox"]  # [[x_min, y_min, w, h], ...] in pixels
row["objects"]["category"]    # ['glass', 'liquid']
row["objects"]["category_id"] # [0, 1]
row["objects"]["area"]        # [13690.85, 1768.26] pxยฒ
row["objects"]["iscrowd"]     # [0, 0]

2. YOLO layout

Ready for Ultralytics / Darknet training out of the box. Split membership is defined by index files (one image path per line) rather than duplicated image folders, so images/ stays the single source of truth:

train.txt   # 6,394 image paths
val.txt     # 791 image paths
test.txt    # 815 image paths
data.yaml

data.yaml points at the index files and declares the class names:

yaml
path: .            # repo root (adjust if you clone elsewhere)
train: train.txt
val: val.txt
test: test.txt
nc: 2
names: ['glass', 'liquid']

Train with Ultralytics:

bash
yolo detect train data=data.yaml model=yolov8n.pt epochs=100 imgsz=256

3. Raw YOLO labels

The canonical source of truth โ€” one .txt per image in labels/:

# labels/glass_00000.txt
0 0.496711 0.489107 0.409185 0.726577    # glass:  class cx cy w h (normalized)
1 0.498119 0.616097 0.292432 0.434514    # liquid: class cx cy w h (normalized)

images/glass_00000.png โ†” labels/glass_00000.txt. All coordinates are normalized to [0, 1] relative to image width/height (YOLO convention).


Schema reference

Parquet columns

ColumnTypeDescription
imageImage (path)Path to images/glass_XXXXX.png; decoded by datasets
image_idint32Stable id, 0โ€“7999 (matches filename suffix)
widthint32256
heightint32256
fill_ratiofloat32liquid bbox height รท glass bbox height (derived)
objects.bboxlist[list[float]]COCO-style [x_min, y_min, w, h] in pixels
objects.categorylist[str]["glass", "liquid"]
objects.category_idlist[int32][0, 1]
objects.arealist[float]bbox area in pxยฒ (w ร— h)
objects.iscrowdlist[int32]always [0, 0] (no crowd objects)
Note on bbox convention: parquet uses COCO pixel coordinates ([x_min, y_min, w, h]); YOLO .txt files use normalized center form (cx, cy, w, h). To convert: x_min = (cx - w/2) * 256, y_min = (cy - h/2) * 256, w_px = w * 256, h_px = h * 256.

Class map

Class IDName
0glass
1liquid

Splits

Stratified 80 / 10 / 10 split by fill_ratio (seed 42). Stratification uses 20 quantile buckets of fill ratio so every split covers the full range of fill levels โ€” a naive random split would leave thin slices of the fill distribution only in one split.

SplitImagesFill ratio rangeFill ratio mean
train6,3940.142 โ€“ 0.9600.563
val7910.151 โ€“ 0.9600.564
test8150.147 โ€“ 0.9600.563

Split assignment is deterministic (seeded), so train.txt from the YOLO layout and train from the parquet contain the same images.


Fill ratio

The fill_ratio column is the core regression target for this dataset and is computed as:

fill_ratio = liquid_bbox_height / glass_bbox_height

Because both bounding boxes are projected from the same 3D geometry under the same camera, perspective foreshortening cancels out โ€” the ratio is a robust proxy for the true liquid level in the glass, largely independent of camera elevation and distance.

Decile histogram of fill ratio across the full dataset:

Fill range0.1โ€“0.20.2โ€“0.30.3โ€“0.40.4โ€“0.50.5โ€“0.60.6โ€“0.70.7โ€“0.80.8โ€“0.90.9โ€“1.0
Images875651,3501,2881,2061,3011,0121,016184

The distribution is approximately uniform across the middle of the range with fewer samples at the extremes (very empty / very full), matching the uniform sampling of liquid_fill_ratio โˆˆ [0.02, 0.98] in the generator combined with the projection step.


Generation pipeline

Generated with GlassHalfFull/scripts/blender/generate.py (Blender + Cycles):

liquid_fill_ratio = uniform(0.02, 0.98)   # sample fill level
liquid_height     = liquid_fill_ratio * GLASS_HEIGHT
โ†’ randomize scene (camera, lights, material, HDRI, ice, grain)
โ†’ render 256ร—256 (Cycles path tracer)
โ†’ project glass + liquid 3D bboxes โ†’ YOLO label

Domain randomization per frame

ParameterRange / options
Liquid palettewater (70%), juice (15%), coffee (10%), milk (5%)
Ice cubes30% chance, 1โ€“3 cubes, floating at surface
HDRIrandom environment map + random Z-rotation
Depth of fieldf-stop uniform(1.4, 8.0)
Graincompositor noise overlay, 3โ€“12% opacity
Camera azimuth0 โ€“ 360ยฐ
Camera elevation15 โ€“ 55ยฐ
Camera distance4 โ€“ 7 units
Camera target jitterยฑ0.10 units
Sun energy3.0 โ€“ 8.0
Fill light energy30 โ€“ 90
Glass tintrandomized transmission color
Wood texturerandom scale + rotation

The goal is systematic variation with exact labels โ€” everything that makes real-world glass detection hard (reflections, transparency, specular highlights, ice occlusion, colored liquids) is varied, while the ground truth stays mathematically exact.


Labeling methodology

Bounding boxes are not hand-annotated. The generator:

  1. 1.Takes the 8 corners of the glass object's world-space bounding box and the 8 corners of the liquid cylinder (radius = LIQUID_RADIUS, z = 0 โ†’ liquid_height).
  2. 2.Projects them to camera space with world_to_camera_view.
  3. 3.Flips Y (Blender camera has Y-down; YOLO has Y-up).
  4. 4.Clamps to [0, 1], drops boxes smaller than 0.001.

Because the liquid is a perfect cylinder of known height and the glass is a known primitive, the resulting boxes are exact for the rendered content โ€” no annotator subjectivity, no edge-case disagreements.


Dataset quality

Verified programmatically:

  • โ€”โœ… All 8,000 images have exactly two objects (one glass, one liquid)
  • โ€”โœ… All coordinates in [0, 1] โ€” zero out-of-bounds boxes
  • โ€”โœ… Class ids strictly {0, 1}
  • โ€”โœ… Every image has a matching label file (no orphans either way)
  • โ€”โœ… Image dimensions uniform (256ร—256)
  • โ€”โœ… Fill ratio continuous across 0.14 โ€“ 0.96

Limitations

  • โ€”Synthetic-to-real gap. Models trained on this dataset may overfit to Cycles lighting. The gap is most visible in specular highlights and shadow softness. For real-world deployment, fine-tune on a small set of real photos.
  • โ€”Single glass geometry. All renders use one glass model โ€” a model trained here may not generalize to shot glasses, tumblers, pint glasses, or bottles.
  • โ€”Resolution. 256ร—256 is modest; fine details (thin liquids, small ice) are coarse.
  • โ€”No negative examples. Every image contains exactly one glass โ€” no empty scenes, no multiple glasses, no occluders other than ice.

Changelog

  • โ€”v1.1 (2026-08-23)
  • โ€”Removed 9 non-dataset images (glass_09000โ€“glass_09008, demo images from the project website) that had been accidentally included โ†’ exactly 8,000 images.
  • โ€”Added parquet splits (train / val / test) with COCO-style annotations and derived fill_ratio.
  • โ€”Added YOLO split layout (train.txt, val.txt, test.txt, data.yaml).
  • โ€”Rewrote dataset card.
  • โ€”v1.0 โ€” original release: 8,009 images + labels as zips.

Attribution

Textures used during rendering:


License

Apache 2.0. See LICENSE for terms. You are free to use, modify, and redistribute with attribution.