S3CUR/badger-55-watermeter
badger model 55 water-meter drum digits (v2) Hand-verified slot crops + continuous-angle labels from a residential Badger Model 55 water meter, captured over ~2 months by an on-prem USB webcam + on-device VLM/CNN reader (/nfs/ai/metermaid-*). The project's internal training pipelines feed off the same data; this directory is a publishable mirror, separate from the active tagging pipeline, with junk filtered. What's new in v2 (2026-05-24) Self-contained parquets… See the full description on the dataset page: https://huggingface.co/datasets/S3CUR/badger-55-watermeter.
badger model 55 water-meter drum digits (v2)
Hand-verified slot crops + continuous-angle labels from a residential Badger Model 55 water meter, captured over ~2 months by an on-prem USB webcam + on-device VLM/CNN reader (/nfs/ai/metermaid-*). The project's internal training pipelines feed off the same data; this directory is a publishable mirror, separate from the active tagging pipeline, with junk filtered.
What's new in v2 (2026-05-24)
- Self-contained parquets, no loose images. Both splits embed the full JPEG bytes inline as an
image_bytescolumn. The dataset is now two files at the repo root (slots.parquet,captures.parquet) instead of thousands of loose JPEGs inslots/images/andcaptures/images/. A cold pull is one HTTP request per split; no more rate-limited per-file HEAD storms.
What's still inherited from v1 (the multi-source retagging release)
- Every committed slot row was re-tagged by hand with multi-source evidence on screen (bao P90 + qwen3-vl single-slot probe + DINOv2 cosine cousin) after dHash label-propagation contamination was discovered in v0.
capturesrows have all 8 slots filled by a priority stack (fixed → human → sdr → bao_p90); thesourcescolumn carries the per-slot provenance.mechanismcolumn corrected to the empirically-supported direction:d7is the continuous-rotation drum,d4–d6are the Geneva-snap drums.
Layout
badger-55-watermeter/
├── README.md
├── slots.parquet # 4,548 rows, ~29 MB (one row per slot crop)
└── captures.parquet # 2,506 rows, ~994 MB (one row per full-frame meter shot)Two views:
- `slots` — single-digit crops with a continuous angle label.
- `captures` — full meter face per row, all 8 slots populated, with a per-slot
sourcesarray telling you the provenance of each digit and ansdr_*triplet describing the radio-anchored bound.
All JPEGs are re-encoded through Pillow with EXIF / ICC / comment metadata explicitly stripped before being embedded. No meter serial number, device ID, or street-level identifier is present anywhere in the dataset.
Reading the dataset
The simplest way:
import pandas as pd
import io
from PIL import Image
from huggingface_hub import hf_hub_download
# Slots view — single-digit crops
slots_path = hf_hub_download('S3CUR/badger-55-watermeter',
filename='slots.parquet', repo_type='dataset')
df = pd.read_parquet(slots_path)
print(df.iloc[0][['slot', 'digit', 'theta_deg', 'tier', 'mechanism']])
img = Image.open(io.BytesIO(df.iloc[0]['image_bytes']))
img.show()Or via the datasets library, which casts image_bytes to a PIL image automatically when the column is named image:
from datasets import load_dataset
ds = load_dataset('S3CUR/badger-55-watermeter', 'slots')['train']
# `ds[0]['image_bytes']` is raw bytes; decode with PIL or OpenCV as above.slots.parquet schema
captures.parquet schema
One row per full-frame meter capture. Platinum frames are not in this view — they're a rapid-roll session, not normal operational captures.
SDR anchor stack
The meter's Itron radio (SCM+) transmits its consumption register every ~hour. Each packet's reading_raw is a lower bound on the current consumption at packet time (×100 to convert to 8-digit-display units — the radio drops the bottom 10-gallon resolution). For each capture at time T we look at:
- P_before — last SDR packet with
received_at < T - P_after — first SDR packet with
received_at >= T
In v2 every capture has SDR coverage — distribution: 1,742 `lock`, 764 `bracket_diff`, 0 `time_growth`, 0 `no_sdr`.
Tiers
- `platinum` (1,035 frames, all d7) — the
platinum_d7atlas: a high-FPS rapid-roll capture of a single digit rotating continuously, hand-anchored to ground-truth angles at every ~0.35° step. The highest-precision angular labels in the dataset. - `gold` (3,513 frames across d4/d5/d6/d7) — production-camera crops reviewed in a tagger UI by a human, each committed with a precise theta to a platinum reference. Every gold row was hand-touched with bao P90 + qwen single-slot + DINOv2 cousin evidence visible on screen. Multiple internal pools feed this tier:
- base pools (
gold_d{4..7},gold_d4_v2,gold_d5_misreads_v1) - manual-flag harvests from the web UI (
gold_d{N}_manual_flags) - envelope-truth harvests where SDR pins the upper digits (
gold_d{N}_envelope_misreads)
Mechanism note (read this before training)
The Badger 55 dial isn't a single class of drum:
- `d7` is geared directly to flow and rotates continuously. It spends roughly half its time at non-integer angles, smoothly traversing every value 0°–360°. Theta is the real label here —
digitis just a derived quantization. The platinum atlas captures this continuous rotation densely (~0.35° per frame). - `d4`–`d6` are higher-order drums coupled to the drum below them through a Geneva mechanism (and their own gear chain). They stay parked at an integer angle (0°, 36°, 72°…) until the digit immediately below them rolls 9→0, at which point they snap one position. In this dataset, d4 sits on an integer angle 99.9% of the time, d5 96%, d6 66% — the rest is mid-snap. Mid-snap is the genuinely-hard regime for these slots, and the
gold_d{N}_manual_flagspools were curated to oversample them.
Theta is the right label for both — use digit only when you need classification. For d4–d6, an angular error of even a few degrees almost always still produces the right integer digit because the drum is parked on it. For d7, angle accuracy matters because the digit is genuinely continuous; the model needs to learn the mapping from the rotating digit's appearance to its phase angle, not just to a discrete class.
The mechanism column in slots.parquet carries this: 'continuous' for d7 (and the platinum atlas), 'geneva' for d4–d6.
Dataset versions through v1.0 had this inverted in both the `mechanism` column and the README's mechanism section. Corrected 2026-05-24 based on the empirical theta distribution above.
Splits
Hash-based on (source_pool, frame): md5 % 100 → 0–79 train, 80–89 val, 90–99 test. Stable across re-runs of the build script. Not stratified by digit or pool — feel free to re-split for your task.
Known limits
- Single meter, single household, single camera. No diversity in lighting/bezel/angle.
- Digit distribution skews to actual readings. During collection the meter was in the ~648K–668K gallon range; d2/d3 are dominated by
6,6, and d4 by7,8. Class weights / resampling help. - No mid-roll flag. Derive it yourself from
theta_deg: the band center for digitdisd * 36° + 18°; mid-roll is anywhere ≥ ~9° from band center. - 423 captures dropped from the captures view because their original full-frame source image was purged during a service decomposition. The slot crops from those captures still appear in the
slotsview — they just don't roll up into a full-meter row.
Build pipeline (for reference)
# Step 1: rebuild from gold pool reviewed.csv commits
/nfs/ai/metermaid-bao/venv/bin/python3 package.py --clean
# Step 2: enrich captures view with SDR + bao P90 anchors
/nfs/ai/metermaid-bao/venv/bin/python3 enrich_captures.pySource filters applied during build:
- Only
action='commit'rows from each pool'sreviewed.csv - Multiple commits per frame → latest wins (so a later disqualify also overrides an earlier commit)
- User-flagged mis-crops in
/nfs/ai/metermaid-training/datasets/.disqualify.csvexcluded
enrich_captures.py runs a PII guard before writing — the build refuses to emit any column whose name matches meter_serial, serial, device_id, or endpoint_id.
Related artifacts
The trained reader model and a clean-room demo that consumes this dataset will live at <https://huggingface.co/S3CUR/badger-55-meterreader> (pending publish). That repo is the place to look for inference code, weights, and a runnable end-to-end demo.
License
Released under **CC-BY-4.0**. You may use, modify, and redistribute (including commercially) provided you attribute the dataset. No fee, no warranty.
Attribution
Dataset author has chosen to remain pseudonymous. Cite as:
badger model 55 water-meter drum digits dataset. Three, 2026.
No author name, email, or institutional affiliation is associated with this release.
