BrickScan is a mobile app that identifies LEGO pieces from a phone camera in real time and keeps a personal inventory of what you own. Point the camera at a brick, a pile of bricks, or a whole desk of loose parts — the app returns part numbers, colors, and names, and lets you tap-to-add to your collection.
Underneath the camera view is a multi-source recognition cascade that combines an on-device YOLO detector, a local CNN classifier, a CLIP-based k-NN catalog voter, a Gemini vision fallback, and a feedback flywheel that retrains on user corrections. Scans happen on-device when possible and fall back to the backend only when confidence is low.
onnxruntime-react-native, with transparent fallback to the
backend when the on-device model is disabled or fails. Toggle in
Settings → On-Device Detection.ml/scripts/retrain_from_feedback.py.SCAN_TTA_ENABLED — test-time augmentation (4× compute, ~1-2% accuracy)SCAN_GROUNDED_GEMINI — pass classifier's top-3 candidates to Gemini
as disambiguation hints instead of open-set classificationSCAN_COLLAPSE_VARIANTS — regex-based mold/print variant merging
(3001a/3001b/3001pr0001 → 3001)SCAN_COLOR_RERANK — downweight candidates whose catalog color is far
from the scan's dominant RGBSCAN_USE_CALIBRATION — apply per-source temperature scaling from
backend/data/calibration_temperatures.jsonSCAN_ALWAYS_RUN_GEMINI — bypass the "skip Gemini if Brickognize
confident" optimizationml/scripts/calibrate_temperatures.py).ml/scripts/eval_against_feedback.py replays feedback
images against the live backend and emits top-1/top-3 accuracy broken
down by source, with confusion pair analysis.ml/scripts/run_ab_eval.sh sweeps the flag space
(baseline / grounded / collapse / color / all-on) and emits a markdown
comparison report.git clone <repository-url>
cd brickscan
# 1. Environment
cp backend/.env.example backend/.env
# Fill in REBRICKABLE_API_KEY, BRICKLINK_API_KEY (optional), GEMINI_API_KEY
# (optional), JWT_SECRET_KEY (openssl rand -hex 32)
# 2. Bring up services
make up
# 3. Schema
make migrate
# 4. Rebrickable catalog import (one-time, ~2 min)
cd data_pipeline && ./download_rebrickable.sh ./rebrickable_data && cd ..
make import-data && make verify-data
# 5. Mobile
make install-mobile
cd mobile && npx expo start # Metro bundler
cd mobile/ios && pod install # iOS native deps (needed after Phase 5)
npx expo run:ios # build + launch to simulator
Interactive docs once the backend is running:
The backend has two parallel route trees reflecting the app's hybrid mode:
| Prefix | Purpose |
|---|---|
/auth | Registration, login, JWT refresh |
/api/scan | Legacy single-piece classifier endpoint |
/api/inventory | User inventory CRUD (requires auth) |
/api/parts, /api/sets | Rebrickable catalog search |
/api/local-inventory/scan | Hybrid cascade single-piece scan |
/api/local-inventory/scan-multi | Multi-piece detect + classify |
/api/local-inventory/scan-video | Temporal-aggregation video scan |
/api/local-inventory/feedback | Feedback flywheel ingest + eval export |
/bricklink | BrickLink price lookup + wanted-list XML |
Training scripts live in ml/ and run on the DGX Spark. The full pipeline:
# On Spark:
ssh spark
# 1. Build merged training set (HF synthetic + Nature 2023 real + labeled)
python3 ~/brickscan/ml/merge_all_datasets.py \
--output-dir ~/brickscan/ml/training_data/merged \
--hf-dir ~/brickscan/ml/training_data/huggingface_legobricks/images \
--nature-dir ~/brickscan/ml/training_data/nature_2023_real
# 2. Train MobileNetV3-Large classifier
python3 ~/brickscan/ml/train_mobilenetv3_gpu.py \
--data-dir ~/brickscan/ml/training_data/merged \
--output-dir ~/brickscan/ml/output/mobilenetv3_$(date +%Y%m%d) \
--epochs 25 --batch-size 384 --model-size large
# 3. Precompute CLIP catalog embeddings for the k-NN voter
python3 ~/brickscan/ml/precompute_clip_gpu.py \
--data-dir ~/brickscan/ml/training_data/merged \
--output ~/brickscan/ml/output/embeddings_cache.pkl
# 4. Quantize the YOLO detector for on-device use
backend/venv/bin/python ml/export/quantize_yolo_int8.py \
--input backend/models/yolo_lego.onnx \
--output backend/models/yolo_lego.int8.onnx \
--calib-dir /path/to/yolo_dataset/images/val
| Artifact | Path | Size | Notes |
|---|---|---|---|
| Local classifier | backend/models/lego_classifier.onnx + .data | 18 MB | MobileNetV3-Small, 502 classes, 91.6% cross-domain top-1 |
| Multi-piece detector (server) | backend/models/yolo_lego.onnx | 167 MB | YOLOv8-L, 28 classes (size, color). Not tracked in git. |
| On-device detector | mobile/assets/models/yolo_lego.int8.onnx | 58 MB | int8 QDQ-quantized YOLOv8-L. 0.964 mean IoU@matched vs fp32. |
| CLIP catalog embeddings | backend/data/embeddings_cache.pkl | 2 MB | 1072 classes × 512-dim |
| Per-source calibration | backend/data/calibration_temperatures.json | <1 KB | Temperatures fit from feedback eval set |
# Backend (runs in the docker container, pytest 8.3.5 pinned)
make test-backend
# Mobile (Jest unit tests)
cd mobile && npx jest
# ML pipeline specifically
cd mobile && npx jest src/__tests__/ml/
# → 16/16 green: letterbox math, RGBA→NCHW conversion, NMS, unletterbox
# Backend flag-gated cascade tests
docker-compose exec -T backend pytest \
tests/test_calibration.py \
tests/test_cascade_flags.py \
tests/test_eval_set_export.py
The backend test suite had a bootstrap bug (conftest.py importing
app.main that never existed) that was fixed in commit 47b1710. The pre-existing
test_auth.py / test_inventory.py / test_scan*.py suites still have
known breakage (UUID columns vs SQLite in-memory; sync tests requesting async
fixtures) — tracked as separate work items.
# Start/stop dev stack
make up # Services on 8000 (backend), 5432 (db), 6379 (redis), 8080 (adminer)
make down
# Logs + shells
make logs
make backend-shell
make db-shell
# Lint & format
make lint-backend
docker-compose exec backend black app/ tests/
# Migrations
docker-compose exec backend alembic revision -m "description"
docker-compose exec backend alembic downgrade -1
# Mobile
cd mobile && npx tsc --noEmit # type check
cd mobile && npx prettier --write src/
brickscan/
├── backend/
│ ├── main.py # FastAPI app entry point
│ ├── app/
│ │ ├── api/ # Authenticated REST routes
│ │ ├── local_inventory/ # Local-mode scan + feedback routes
│ │ ├── services/
│ │ │ ├── hybrid_recognition.py # Cascade + env-gated flags
│ │ │ ├── brickognize_client.py
│ │ │ ├── gemini_service.py
│ │ │ ├── ml_inference.py # ONNX runner for local classifier
│ │ │ ├── part_num_normalizer.py # Mold/variant collapse
│ │ │ └── color_extractor.py # Dominant-color histogram
│ │ ├── ml/ # EmbeddingLibrary (CLIP k-NN)
│ │ └── core/ # Auth, DB, config
│ ├── models/ # ONNX artifacts (gitignored except canonical)
│ ├── data/ # Calibration temps, CLIP embeddings
│ ├── tests/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── requirements-dev.txt
│
├── mobile/
│ ├── src/
│ │ ├── screens/
│ │ │ ├── ContinuousScanScreen.tsx # Live streaming scan
│ │ │ ├── ScanScreen.tsx
│ │ │ ├── InventoryScreen.tsx
│ │ │ └── SettingsScreen.tsx # On-device / local-only toggles
│ │ ├── ml/ # Phase 5 on-device pipeline
│ │ │ ├── preprocess.ts # Letterbox + NCHW Float32
│ │ │ ├── postprocess.ts # YOLOv8 head decode + NMS
│ │ │ ├── yoloDetector.ts # onnxruntime session wrapper
│ │ │ └── scanPipeline.ts # JPEG → RGBA → detect → pieces
│ │ ├── store/ # Zustand stores (auth, inventory)
│ │ ├── utils/
│ │ │ ├── bboxTracker.ts # IoU-based cross-frame tracking
│ │ │ ├── settingsFlags.ts # AsyncStorage flag keys
│ │ │ └── scanSession.ts # Session persistence
│ │ └── services/ # apiClient + screen helpers
│ ├── assets/models/ # int8 YOLO bundled into the app
│ ├── __tests__/ml/ # Jest unit tests
│ └── app.json
│
├── ml/
│ ├── scripts/ # Training, eval, calibration scripts
│ ├── export/ # ONNX export + int8 quantization
│ ├── training/ # Training pipelines (incl. YOLO distillation)
│ ├── blender/ # Synthetic render pipeline
│ └── data/ # Training data symlinks (all gitignored)
│
├── data_pipeline/ # Rebrickable CSV import utilities
├── docker-compose.yml
├── Makefile
└── docs/ # Phase 5 status, design notes
| Service | URL | What it buys you |
|---|---|---|
| Rebrickable | https://rebrickable.com/api/ | Free catalog data: 76K+ parts, 26K+ sets, 16K+ minifigs |
| BrickLink | https://www.bricklink.com | Free price guide data (OAuth 1.0a) — requires seller account |
| Google Gemini | https://ai.google.dev | Vision-language fallback for the recognition cascade |
Add each to backend/.env:
REBRICKABLE_API_KEY=...
BRICKLINK_CONSUMER_KEY=...
BRICKLINK_CONSUMER_SECRET=...
BRICKLINK_TOKEN_VALUE=...
BRICKLINK_TOKEN_SECRET=...
GEMINI_API_KEY=...
JWT_SECRET_KEY=... # openssl rand -hex 32
# Backend won't start: check the stack
docker-compose ps
docker-compose logs backend
# Database fresh reset (destroys data)
docker-compose down -v
make up && make migrate && make import-data
# Mobile build fails after Phase 5 changes
cd mobile && rm -rf node_modules && npm install
cd ios && pod install && cd ..
npx expo start --clear
# ONNX model missing on backend startup
# The canonical classifier is tracked at backend/models/lego_classifier.onnx.
# The YOLO detector (backend/models/yolo_lego.onnx, 167 MB) is NOT tracked —
# regenerate with `ml/export/to_onnx.py` or download from the team's model
# registry. The int8 derivative ships via git LFS / direct commit.
.pt checkpoints (live on the Spark, not in git)backend/models/spark_*/ is gitignored)GitHub Actions run on every push:
backend_ci.yml — ruff + black lint, mypy, pytest against PostgreSQL + Redismobile_ci.yml — TypeScript type-check, Jest unit tests, ESLintMIT.
Issues, feature requests, bug reports: GitHub Issues on this repo.
47 commits
Python
65.5%
TypeScript
27.4%
Shell
5.1%
Swift
1.6%
BrickScan is a mobile app that identifies LEGO pieces from a phone camera in real time and keeps a personal inventory of what you own. Point the camera at a brick, a pile of bricks, or a whole desk of loose parts — the app returns part numbers, colors, and names, and lets you tap-to-add to your collection.
Underneath the camera view is a multi-source recognition cascade that combines an on-device YOLO detector, a local CNN classifier, a CLIP-based k-NN catalog voter, a Gemini vision fallback, and a feedback flywheel that retrains on user corrections. Scans happen on-device when possible and fall back to the backend only when confidence is low.
onnxruntime-react-native, with transparent fallback to the
backend when the on-device model is disabled or fails. Toggle in
Settings → On-Device Detection.ml/scripts/retrain_from_feedback.py.SCAN_TTA_ENABLED — test-time augmentation (4× compute, ~1-2% accuracy)SCAN_GROUNDED_GEMINI — pass classifier's top-3 candidates to Gemini
as disambiguation hints instead of open-set classificationSCAN_COLLAPSE_VARIANTS — regex-based mold/print variant merging
(3001a/3001b/3001pr0001 → 3001)SCAN_COLOR_RERANK — downweight candidates whose catalog color is far
from the scan's dominant RGBSCAN_USE_CALIBRATION — apply per-source temperature scaling from
backend/data/calibration_temperatures.jsonSCAN_ALWAYS_RUN_GEMINI — bypass the "skip Gemini if Brickognize
confident" optimizationml/scripts/calibrate_temperatures.py).ml/scripts/eval_against_feedback.py replays feedback
images against the live backend and emits top-1/top-3 accuracy broken
down by source, with confusion pair analysis.ml/scripts/run_ab_eval.sh sweeps the flag space
(baseline / grounded / collapse / color / all-on) and emits a markdown
comparison report.git clone <repository-url>
cd brickscan
# 1. Environment
cp backend/.env.example backend/.env
# Fill in REBRICKABLE_API_KEY, BRICKLINK_API_KEY (optional), GEMINI_API_KEY
# (optional), JWT_SECRET_KEY (openssl rand -hex 32)
# 2. Bring up services
make up
# 3. Schema
make migrate
# 4. Rebrickable catalog import (one-time, ~2 min)
cd data_pipeline && ./download_rebrickable.sh ./rebrickable_data && cd ..
make import-data && make verify-data
# 5. Mobile
make install-mobile
cd mobile && npx expo start # Metro bundler
cd mobile/ios && pod install # iOS native deps (needed after Phase 5)
npx expo run:ios # build + launch to simulator
Interactive docs once the backend is running:
The backend has two parallel route trees reflecting the app's hybrid mode:
| Prefix | Purpose |
|---|---|
/auth | Registration, login, JWT refresh |
/api/scan | Legacy single-piece classifier endpoint |
/api/inventory | User inventory CRUD (requires auth) |
/api/parts, /api/sets | Rebrickable catalog search |
/api/local-inventory/scan | Hybrid cascade single-piece scan |
/api/local-inventory/scan-multi | Multi-piece detect + classify |
/api/local-inventory/scan-video | Temporal-aggregation video scan |
/api/local-inventory/feedback | Feedback flywheel ingest + eval export |
/bricklink | BrickLink price lookup + wanted-list XML |
Training scripts live in ml/ and run on the DGX Spark. The full pipeline:
# On Spark:
ssh spark
# 1. Build merged training set (HF synthetic + Nature 2023 real + labeled)
python3 ~/brickscan/ml/merge_all_datasets.py \
--output-dir ~/brickscan/ml/training_data/merged \
--hf-dir ~/brickscan/ml/training_data/huggingface_legobricks/images \
--nature-dir ~/brickscan/ml/training_data/nature_2023_real
# 2. Train MobileNetV3-Large classifier
python3 ~/brickscan/ml/train_mobilenetv3_gpu.py \
--data-dir ~/brickscan/ml/training_data/merged \
--output-dir ~/brickscan/ml/output/mobilenetv3_$(date +%Y%m%d) \
--epochs 25 --batch-size 384 --model-size large
# 3. Precompute CLIP catalog embeddings for the k-NN voter
python3 ~/brickscan/ml/precompute_clip_gpu.py \
--data-dir ~/brickscan/ml/training_data/merged \
--output ~/brickscan/ml/output/embeddings_cache.pkl
# 4. Quantize the YOLO detector for on-device use
backend/venv/bin/python ml/export/quantize_yolo_int8.py \
--input backend/models/yolo_lego.onnx \
--output backend/models/yolo_lego.int8.onnx \
--calib-dir /path/to/yolo_dataset/images/val
| Artifact | Path | Size | Notes |
|---|---|---|---|
| Local classifier | backend/models/lego_classifier.onnx + .data | 18 MB | MobileNetV3-Small, 502 classes, 91.6% cross-domain top-1 |
| Multi-piece detector (server) | backend/models/yolo_lego.onnx | 167 MB | YOLOv8-L, 28 classes (size, color). Not tracked in git. |
| On-device detector | mobile/assets/models/yolo_lego.int8.onnx | 58 MB | int8 QDQ-quantized YOLOv8-L. 0.964 mean IoU@matched vs fp32. |
| CLIP catalog embeddings | backend/data/embeddings_cache.pkl | 2 MB | 1072 classes × 512-dim |
| Per-source calibration | backend/data/calibration_temperatures.json | <1 KB | Temperatures fit from feedback eval set |
# Backend (runs in the docker container, pytest 8.3.5 pinned)
make test-backend
# Mobile (Jest unit tests)
cd mobile && npx jest
# ML pipeline specifically
cd mobile && npx jest src/__tests__/ml/
# → 16/16 green: letterbox math, RGBA→NCHW conversion, NMS, unletterbox
# Backend flag-gated cascade tests
docker-compose exec -T backend pytest \
tests/test_calibration.py \
tests/test_cascade_flags.py \
tests/test_eval_set_export.py
The backend test suite had a bootstrap bug (conftest.py importing
app.main that never existed) that was fixed in commit 47b1710. The pre-existing
test_auth.py / test_inventory.py / test_scan*.py suites still have
known breakage (UUID columns vs SQLite in-memory; sync tests requesting async
fixtures) — tracked as separate work items.
# Start/stop dev stack
make up # Services on 8000 (backend), 5432 (db), 6379 (redis), 8080 (adminer)
make down
# Logs + shells
make logs
make backend-shell
make db-shell
# Lint & format
make lint-backend
docker-compose exec backend black app/ tests/
# Migrations
docker-compose exec backend alembic revision -m "description"
docker-compose exec backend alembic downgrade -1
# Mobile
cd mobile && npx tsc --noEmit # type check
cd mobile && npx prettier --write src/
brickscan/
├── backend/
│ ├── main.py # FastAPI app entry point
│ ├── app/
│ │ ├── api/ # Authenticated REST routes
│ │ ├── local_inventory/ # Local-mode scan + feedback routes
│ │ ├── services/
│ │ │ ├── hybrid_recognition.py # Cascade + env-gated flags
│ │ │ ├── brickognize_client.py
│ │ │ ├── gemini_service.py
│ │ │ ├── ml_inference.py # ONNX runner for local classifier
│ │ │ ├── part_num_normalizer.py # Mold/variant collapse
│ │ │ └── color_extractor.py # Dominant-color histogram
│ │ ├── ml/ # EmbeddingLibrary (CLIP k-NN)
│ │ └── core/ # Auth, DB, config
│ ├── models/ # ONNX artifacts (gitignored except canonical)
│ ├── data/ # Calibration temps, CLIP embeddings
│ ├── tests/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── requirements-dev.txt
│
├── mobile/
│ ├── src/
│ │ ├── screens/
│ │ │ ├── ContinuousScanScreen.tsx # Live streaming scan
│ │ │ ├── ScanScreen.tsx
│ │ │ ├── InventoryScreen.tsx
│ │ │ └── SettingsScreen.tsx # On-device / local-only toggles
│ │ ├── ml/ # Phase 5 on-device pipeline
│ │ │ ├── preprocess.ts # Letterbox + NCHW Float32
│ │ │ ├── postprocess.ts # YOLOv8 head decode + NMS
│ │ │ ├── yoloDetector.ts # onnxruntime session wrapper
│ │ │ └── scanPipeline.ts # JPEG → RGBA → detect → pieces
│ │ ├── store/ # Zustand stores (auth, inventory)
│ │ ├── utils/
│ │ │ ├── bboxTracker.ts # IoU-based cross-frame tracking
│ │ │ ├── settingsFlags.ts # AsyncStorage flag keys
│ │ │ └── scanSession.ts # Session persistence
│ │ └── services/ # apiClient + screen helpers
│ ├── assets/models/ # int8 YOLO bundled into the app
│ ├── __tests__/ml/ # Jest unit tests
│ └── app.json
│
├── ml/
│ ├── scripts/ # Training, eval, calibration scripts
│ ├── export/ # ONNX export + int8 quantization
│ ├── training/ # Training pipelines (incl. YOLO distillation)
│ ├── blender/ # Synthetic render pipeline
│ └── data/ # Training data symlinks (all gitignored)
│
├── data_pipeline/ # Rebrickable CSV import utilities
├── docker-compose.yml
├── Makefile
└── docs/ # Phase 5 status, design notes
| Service | URL | What it buys you |
|---|---|---|
| Rebrickable | https://rebrickable.com/api/ | Free catalog data: 76K+ parts, 26K+ sets, 16K+ minifigs |
| BrickLink | https://www.bricklink.com | Free price guide data (OAuth 1.0a) — requires seller account |
| Google Gemini | https://ai.google.dev | Vision-language fallback for the recognition cascade |
Add each to backend/.env:
REBRICKABLE_API_KEY=...
BRICKLINK_CONSUMER_KEY=...
BRICKLINK_CONSUMER_SECRET=...
BRICKLINK_TOKEN_VALUE=...
BRICKLINK_TOKEN_SECRET=...
GEMINI_API_KEY=...
JWT_SECRET_KEY=... # openssl rand -hex 32
# Backend won't start: check the stack
docker-compose ps
docker-compose logs backend
# Database fresh reset (destroys data)
docker-compose down -v
make up && make migrate && make import-data
# Mobile build fails after Phase 5 changes
cd mobile && rm -rf node_modules && npm install
cd ios && pod install && cd ..
npx expo start --clear
# ONNX model missing on backend startup
# The canonical classifier is tracked at backend/models/lego_classifier.onnx.
# The YOLO detector (backend/models/yolo_lego.onnx, 167 MB) is NOT tracked —
# regenerate with `ml/export/to_onnx.py` or download from the team's model
# registry. The int8 derivative ships via git LFS / direct commit.
.pt checkpoints (live on the Spark, not in git)backend/models/spark_*/ is gitignored)GitHub Actions run on every push:
backend_ci.yml — ruff + black lint, mypy, pytest against PostgreSQL + Redismobile_ci.yml — TypeScript type-check, Jest unit tests, ESLintMIT.
Issues, feature requests, bug reports: GitHub Issues on this repo.
47 commits
Python
65.5%
TypeScript
27.4%
Shell
5.1%
Swift
1.6%