Skip to content

Commit 7e0006d

Browse files
committed
Data
1 parent e59c08d commit 7e0006d

2 files changed

Lines changed: 262 additions & 24 deletions

File tree

content/learning-paths/cross-platform/onnx/03_BuildingAndExportingModel.md

Lines changed: 0 additions & 24 deletions
This file was deleted.
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
---
2+
# User change
3+
title: "Preparing a Synthetic Sudoku Digit Dataset"
4+
5+
weight: 4
6+
7+
layout: "learningpathall"
8+
---
9+
10+
## Big picture
11+
Our end goal is a camera-to-solution Sudoku app that runs efficiently on Arm64 devices (e.g., Raspberry Pi or Android phones). ONNX is the glue: we’ll train the digit recognizer in PyTorch, export it to ONNX, and run it anywhere with ONNX Runtime (CPU EP on edge devices, NNAPI EP on Android). Everything around the model—grid detection, perspective rectification, and solving—stays deterministic and lightweight.
12+
13+
## Objective
14+
In this step, we will generate a custom dataset of Sudoku puzzles and their digit crops, which we’ll use to train a digit recognition model. Starting from a Hugging Face parquet dataset that provides paired puzzle/solution strings, we transform raw boards into realistic, book-style Sudoku pages, apply camera-like augmentations to mimic mobile captures, and automatically slice each page into 81 labeled cell images. This yields a large, diverse, perfectly labeled set of digits (0–9 with 0 = blank) without manual annotation. By the end, you’ll have a structured dataset ready to train a lightweight model in the next section.
15+
16+
## Why Synthetic Generation?
17+
When building a Sudoku digit recognizer, the hardest part is obtaining a well-labeled dataset that matches real capture conditions. MNIST contains handwritten digits, which differ from printed, grid-aligned Sudoku digits; relying on it alone hurts real-world performance.
18+
19+
By generating synthetic Sudoku pages directly from the parquet dataset, we get:
20+
1. Perfect labeling. Since the puzzle content is known, every cropped cell automatically comes with the correct label (digit or blank), eliminating manual annotation.
21+
2. Control over style. We can render Sudoku pages to look like those in printed books, with realistic fonts, grid lines, and difficulty levels controlled by how many cells are left blank.
22+
3. Robustness through augmentation: By applying perspective warps, blur, noise, and lighting variations, we simulate how a smartphone camera might capture a Sudoku page, improving the model’s ability to handle real-world photos.
23+
4. Scalability. With millions of Sudoku solutions available, we can easily generate tens of thousands of training samples in minutes, ensuring a dataset that is both large and diverse.
24+
25+
This synthetic data generation strategy allows us to create a custom-fit dataset for our Sudoku digit recognition problem, bridging the gap between clean digital puzzles and noisy real-world inputs.
26+
In this Learning Path, you have created an Android application to capture and process camera images using OpenCV.
27+
28+
## What we’ll produce
29+
By the end of this step, you will have two complementary datasets:
30+
1. Digit crops for training the classifier. A folder tree structured for torchvision.datasets.ImageFolder, containing tens of thousands of labeled 28×28 images of Sudoku digits (0–9, with 0 meaning blank):
31+
32+
```console
33+
data/
34+
train/
35+
0/....png (blank)
36+
1/....png
37+
...
38+
9/....png
39+
val/
40+
0/....png
41+
...
42+
9/....png
43+
```
44+
45+
These will be used in next step to train a lightweight model for digit recognition.
46+
47+
2. Rendered Sudoku grids for camera simulation. Full-page Sudoku images (both clean book-style and augmented camera-like versions) stored in:
48+
```console
49+
data/
50+
grids/
51+
train/
52+
000001_clean.png
53+
000001_cam.png
54+
...
55+
val/
56+
...
57+
```
58+
59+
These grid images allow us to later test the end-to-end pipeline: detect the board with OpenCV, rectify perspective, classify each cell using the ONNX digit recognizer, and then solve the Sudoku puzzle.
60+
61+
Together, these datasets provide both the micro-level data needed to train the digit recognizer and the macro-level data to simulate the camera pipeline for testing and deployment.
62+
63+
## Implementation
64+
Start by creating a new file 02_PrepareData.py and modify it as follows:
65+
```python
66+
import os, random, pathlib
67+
import numpy as np
68+
import cv2 as cv
69+
import pandas as pd
70+
from tqdm import tqdm
71+
72+
random.seed(0)
73+
74+
# Parameters
75+
PARQUET_PATH = "train_1.parquet" # path to your downloaded HF Parquet
76+
OUT_DIR = pathlib.Path("data")
77+
N_TRAIN = 1000 # how many puzzles to render for training
78+
N_VAL = 100 # how many for validation
79+
IMG_H, IMG_W = 1200, 800 # page size (portrait-ish)
80+
GRID_MARGIN = 60 # outer margin, px
81+
CELL_SIZE = 28 # output crop size for classifier (MNIST-like)
82+
FONT = cv.FONT_HERSHEY_SIMPLEX
83+
#
84+
85+
def str_to_grid(s: str):
86+
"""81-char '012345678' string -> 9x9 list of ints."""
87+
s = s.strip()
88+
assert len(s) == 81, f"bad length: {len(s)}"
89+
return [[int(s[9*r+c]) for c in range(9)] for r in range(9)]
90+
91+
def load_puzzles(parquet_path, n_train, n_val):
92+
"""Load puzzles/solutions; return two lists of 9x9 int grids for train/val."""
93+
df = pd.read_parquet(parquet_path, engine="pyarrow")
94+
# Shuffle reproducibly
95+
df = df.sample(frac=1.0, random_state=0).reset_index(drop=True)
96+
# Keep only needed columns if present
97+
need_cols = [c for c in ["puzzle", "solution"] if c in df.columns]
98+
if not need_cols or "puzzle" not in need_cols:
99+
raise ValueError(f"Expected 'puzzle' (and optionally 'solution') columns; got: {list(df.columns)}")
100+
101+
# Slice train/val partitions
102+
df_train = df.iloc[:n_train]
103+
df_val = df.iloc[n_train:n_train+n_val]
104+
105+
puzzles_train = [str_to_grid(p) for p in df_train["puzzle"].astype(str)]
106+
puzzles_val = [str_to_grid(p) for p in df_val["puzzle"].astype(str)]
107+
108+
# Solutions are optional (useful later for solver validation)
109+
solutions_train = [str_to_grid(s) for s in df_train["solution"].astype(str)] if "solution" in df_train else None
110+
solutions_val = [str_to_grid(s) for s in df_val["solution"].astype(str)] if "solution" in df_val else None
111+
112+
return (puzzles_train, solutions_train), (puzzles_val, solutions_val)
113+
114+
def draw_grid(img, size=9, margin=GRID_MARGIN):
115+
H, W = img.shape[:2]
116+
step = (min(H, W) - 2*margin) // size
117+
x0 = (W - size*step) // 2
118+
y0 = (H - size*step) // 2
119+
for i in range(size+1):
120+
thickness = 3 if i % 3 == 0 else 1
121+
# vertical
122+
cv.line(img, (x0 + i*step, y0), (x0 + i*step, y0 + size*step), (0, 0, 0), thickness)
123+
# horizontal
124+
cv.line(img, (x0, y0 + i*step), (x0 + size*step, y0 + i*step), (0, 0, 0), thickness)
125+
return (x0, y0, step)
126+
127+
def put_digit(img, r, c, d, x0, y0, step):
128+
if d == 0:
129+
return # blank cell
130+
text = str(d)
131+
scale = step / 60.0
132+
thickness = 2
133+
(tw, th), base = cv.getTextSize(text, FONT, scale, thickness)
134+
cx = x0 + c*step + (step - tw)//2
135+
cy = y0 + r*step + (step + th)//2 - th//4
136+
cv.putText(img, text, (cx, cy), FONT, scale, (0, 0, 0), thickness, cv.LINE_AA)
137+
138+
def render_page(puzzle9x9):
139+
page = np.full((IMG_H, IMG_W, 3), 255, np.uint8)
140+
x0, y0, step = draw_grid(page, 9, GRID_MARGIN)
141+
for r in range(9):
142+
for c in range(9):
143+
put_digit(page, r, c, puzzle9x9[r][c], x0, y0, step)
144+
return page, (x0, y0, step)
145+
146+
def aug_camera(img):
147+
"""Light camera-like augmentation: perspective jitter + optional Gaussian blur."""
148+
H, W = img.shape[:2]
149+
def jitter(pt, s=20):
150+
return (pt[0] + random.randint(-s, s), pt[1] + random.randint(-s, s))
151+
src = np.float32([(0, 0), (W, 0), (W, H), (0, H)])
152+
dst = np.float32([jitter((0,0)), jitter((W,0)), jitter((W,H)), jitter((0,H))])
153+
M = cv.getPerspectiveTransform(src, dst)
154+
warped = cv.warpPerspective(img, M, (W, H), flags=cv.INTER_LINEAR, borderValue=(220, 220, 220))
155+
if random.random() < 0.5:
156+
k = random.choice([1, 2])
157+
warped = cv.GaussianBlur(warped, (2*k+1, 2*k+1), 0)
158+
return warped
159+
160+
def ensure_dirs(split):
161+
for cls in range(10): # 0..9 (0 == blank)
162+
(OUT_DIR / split / str(cls)).mkdir(parents=True, exist_ok=True)
163+
164+
def save_crops(page, geom, puzzle9x9, split, base_id):
165+
x0, y0, step = geom
166+
idx = 0
167+
for r in range(9):
168+
for c in range(9):
169+
x1, y1 = x0 + c*step, y0 + r*step
170+
roi = page[y1:y1+step, x1:x1+step]
171+
g = cv.cvtColor(roi, cv.COLOR_BGR2GRAY)
172+
g = cv.resize(g, (CELL_SIZE, CELL_SIZE), interpolation=cv.INTER_AREA)
173+
label = puzzle9x9[r][c] # 0 for blank, 1..9 digits
174+
out_path = OUT_DIR / split / str(label) / f"{base_id}_{idx:02d}.png"
175+
cv.imwrite(str(out_path), g)
176+
idx += 1
177+
178+
def process_split(puzzles, split_name, n_limit):
179+
ensure_dirs(split_name)
180+
grid_dir = OUT_DIR / "grids" / split_name
181+
grid_dir.mkdir(parents=True, exist_ok=True)
182+
183+
N = min(n_limit, len(puzzles))
184+
for i in tqdm(range(N), desc=f"render {split_name}"):
185+
puzzle = puzzles[i]
186+
187+
# Clean page
188+
page, geom = render_page(puzzle)
189+
save_crops(page, geom, puzzle, split_name, base_id=f"{i:06d}_clean")
190+
cv.imwrite(str(grid_dir / f"{i:06d}_clean.png"), page)
191+
192+
# Camera-like
193+
warped = aug_camera(page)
194+
save_crops(warped, geom, puzzle, split_name, base_id=f"{i:06d}_cam")
195+
cv.imwrite(str(grid_dir / f"{i:06d}_cam.png"), warped)
196+
197+
def main():
198+
(p_train, _s_train), (p_val, _s_val) = load_puzzles(PARQUET_PATH, N_TRAIN, N_VAL)
199+
process_split(p_train, "train", N_TRAIN)
200+
process_split(p_val, "val", N_VAL)
201+
print("Done. Output under:", OUT_DIR.resolve())
202+
203+
if __name__ == "__main__":
204+
main()
205+
```
206+
207+
At the top, you set basic knobs for the generator: where to read the Parquet file, where to write outputs, how many puzzles to render for train/val, page size, grid margin, crop size, and the OpenCV font. Tweaking these lets you control dataset scale, visual style, and classifier input size (e.g., CELL_SIZE=32 if you want a slightly larger digit crop).
208+
209+
The method str_to_grid(s) converts an 81-character Sudoku string into a 9×9 list of integers. Each character represents a cell: 0 is blank, 1–9 are digits. This is the canonical internal representation used throughout the script.
210+
211+
Then, we have load_puzzles(parquet_path, n_train, n_val), which loads the dataset from Parquet, shuffles it deterministically, and slices it into train/val partitions. It returns the puzzles (and, if present, solutions) as 9×9 integer grids. In this step we only need puzzle for rendering and labeling digit crops (blanks included); solution is useful later for solver validation.
212+
213+
Subsequently, draw_grid(img, size=9, margin=GRID_MARGIN) draws a Sudoku grid on a blank page image. It computes the step size from the page dimensions and margin, then draws both thin inner lines and thick 3×3 box boundaries. It returns the top-left corner (x0, y0) and the cell size (step), which are reused to place digits and to locate each cell for cropping.
214+
215+
Next, put_digit(img, r, c, d, x0, y0, step) renders a single digit d at row r, column c inside the grid. The text is centered in the cell using the font metrics; if d == 0, it leaves the cell blank. This mirrors printed-book Sudoku styling so our crops look realistic.
216+
217+
Another method, render_page(puzzle9x9) builds a complete “book-style” Sudoku page: creates a white canvas, draws the grid, loops over all 81 cells, and writes digits using put_digit. It returns the page plus the grid geometry (x0, y0, step) for subsequent cropping.
218+
219+
A method aug_camera(img) applies a light, camera-like augmentation to mimic smartphone captures: a small perspective warp (random corner jitter) and optional Gaussian blur. The warp uses a light gray border fill so any exposed areas look like paper rather than colored artifacts. This produces a second version of each page that’s closer to real-world inputs.
220+
221+
Afterward, ensure_dirs(split) makes the class directories for a given split (train or val) so that crops can be saved in data/{split}/{class}/.... The classes are 0..9 with 0 = blank.
222+
223+
A method save_crops(page, geom, puzzle9x9, split, base_id) slices the page into 81 cell crops using the grid geometry, converts each crop to grayscale, resizes it to CELL_SIZE × CELL_SIZE, and saves it into the appropriate class directory based on the puzzle’s value at that cell (0..9). Using the puzzle for labels ensures we learn to recognize blanks as well as digits.
224+
225+
Then, process_split(puzzles, split_name, n_limit) is the workhorse for each partition. For each puzzle, it (1) renders a clean page, saves its 81 crops, and writes the full page under data/grids/{split}; then (2) generates an augmented “camera-like” version and saves its crops and full page too. This gives you both micro-level training data (crops) and macro-level test images (full grids) for the later camera pipeline.
226+
227+
Finally, main() loads train/val puzzles from Parquet and calls process_split for each. When it finishes, you’ll have:
228+
```console
229+
data/
230+
train/
231+
0/… 1/… … 9/…
232+
val/
233+
0/… … 9/…
234+
grids/
235+
train/ (..._clean.png, ..._cam.png)
236+
val/ (..._clean.png, ..._cam.png)
237+
```
238+
239+
## Launching instructions
240+
1. Install dependencies (inside your virtual env):
241+
```console
242+
pip install pandas pyarrow opencv-python tqdm numpy
243+
```
244+
245+
2. Place the Parquet file (e.g., train_1.parquet) next to the script or update PARQUET_PATH accordingly. Here we used the file from [this location](https://huggingface.co/datasets/Ritvik19/Sudoku-Dataset/blob/main/train_1.parquet).
246+
247+
3. Run the generator
248+
```console
249+
python 02_PrepareData.py
250+
```
251+
252+
4. Inspect outputs:
253+
* Digit crops live under data/train/{0..9}/ and data/val/{0..9}/.
254+
* Full-page grids (clean + camera-like) live under data/grids/train/ and data/grids/val/.
255+
256+
Tips
257+
* Start small (N_TRAIN=1000, N_VAL=100) to verify everything, then scale up.
258+
* If you want larger inputs for the classifier, increase CELL_SIZE to 32 or 40.
259+
* To make augmentation a bit stronger (more realistic), slightly increase the perspective jitter in aug_camera, add brightness/contrast jitter, or a faint gradient shadow overlay.
260+
261+
## Summary
262+
After running this step you’ll have a robust, labeled, Sudoku-specific dataset: thousands of digit crops (including blanks) for training and realistic full-page grids for pipeline testing. You’re ready for the next step—training the digit recognizer and exporting it to ONNX.

0 commit comments

Comments
 (0)