Skip to content

Commit e6e7473

Browse files
committed
6 & 7
1 parent 2cf3a82 commit e6e7473

4 files changed

Lines changed: 843 additions & 21 deletions

File tree

Lines changed: 234 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,249 @@
11
---
22
# User change
3-
title: "Summary"
3+
title: "Inference and Model Evaluation"
44

55
weight: 6
66

77
layout: "learningpathall"
88
---
9-
## Summary ##
109

11-
In this Learning Path, you have created an Android application to capture and process camera images using OpenCV.
10+
## Objective ##
11+
In this section, we validate the digit recognizer by running inference on the validation dataset using both the PyTorch checkpoint and the exported ONNX model. We verify that PyTorch and ONNX Runtime produce consistent results, analyze class-level behavior using a confusion matrix, and generate visual diagnostics for debugging and documentation. This step acts as a final verification checkpoint before integrating the model into the full OpenCV-based Sudoku processing pipeline.
1212

13-
You performed the following steps:
13+
Before introducing geometric processing, grid detection, and perspective correction, it is important to confirm that the digit recognizer works reliably in isolation. By validating inference and analyzing errors at the digit level, we ensure that any future issues in the end-to-end system can be attributed to image processing or geometry rather than the classifier itself.
1414

15-
* Integrated the OpenCV library into your Android project.
15+
## Inference and Evaluation Script
16+
Create a new file named 04_Test.py and paste the script below into it. This script evaluates the digit recognizer in a way that closely mirrors deployment conditions. It compares PyTorch and ONNX Runtime inference, measures accuracy on the validation dataset, and generates visual diagnostics that reveal both strengths and remaining failure modes of the model.
1617

17-
* Enabled camera permissions to ensure the application can access the device's camera.
18+
```python
19+
import os, numpy as np, torch
20+
from torchvision import datasets, transforms
21+
from torch.utils.data import DataLoader
22+
from tqdm import tqdm
23+
import matplotlib.pyplot as plt
1824

19-
* Set up `JavaCameraView` to capture real-time frames from the camera. You declared and initialized Mat objects to store and process camera frames.
25+
from digitnet_model import DigitNet
2026

21-
* Implemented adaptive thresholding using OpenCV's `Imgproc.adaptiveThreshold` to process the camera frames when a checkbox is checked.
27+
DATA_DIR = "data"
28+
ARTI_DIR = "artifacts"
29+
os.makedirs(ARTI_DIR, exist_ok=True)
2230

23-
By following these steps, you have successfully created an Android application that captures real-time images from the camera.
31+
ONNX_PATH = os.path.join(ARTI_DIR, "sudoku_digitnet.onnx") # fp32
2432

33+
# Same normalization as training (and force grayscale → 1 channel)
34+
tfm_val = transforms.Compose([
35+
transforms.Grayscale(num_output_channels=1),
36+
transforms.ToTensor(),
37+
transforms.Normalize((0.5,), (0.5,))
38+
])
39+
val_ds = datasets.ImageFolder(os.path.join(DATA_DIR, "val"), transform=tfm_val)
40+
val_loader = DataLoader(val_ds, batch_size=512, shuffle=False, num_workers=0)
41+
42+
DIGIT_NAMES = [str(i) for i in range(10)] # 0 = blank, 1..9 = digits
43+
44+
45+
def evaluate_pytorch(model, loader):
46+
model.eval()
47+
correct = total = 0
48+
with torch.no_grad():
49+
for x, y in loader:
50+
pred = model(x).argmax(1)
51+
correct += (pred == y).sum().item()
52+
total += y.numel()
53+
return correct / total if total else 0.0
54+
55+
56+
def confusion_matrix_onnx(onnx_model_path, loader):
57+
import onnxruntime as ort
58+
sess = ort.InferenceSession(onnx_model_path, providers=["CPUExecutionProvider"])
59+
mat = np.zeros((10, 10), dtype=np.int64)
60+
total = 0
61+
correct = 0
62+
for x, y in tqdm(loader, desc="ONNX eval"):
63+
# x: torch tensor [N,1,28,28] normalized to [-1,1]
64+
inp = x.numpy().astype(np.float32)
65+
logits = sess.run(["logits"], {"input": inp})[0] # [N,10]
66+
pred = logits.argmax(axis=1)
67+
y_np = y.numpy()
68+
for t, p in zip(y_np, pred):
69+
mat[t, p] += 1
70+
correct += (pred == y_np).sum()
71+
total += y_np.size
72+
acc = float(correct) / float(total) if total else 0.0
73+
return acc, mat
74+
75+
76+
def plot_confusion_matrix(cm, classes=DIGIT_NAMES, normalize=False, title="Confusion matrix", fname=None):
77+
"""Plot confusion matrix. If normalize=True, rows sum to 1."""
78+
cm_plot = cm.astype("float")
79+
if normalize:
80+
row_sums = cm_plot.sum(axis=1, keepdims=True) + 1e-12
81+
cm_plot = cm_plot / row_sums
82+
83+
plt.figure(figsize=(6, 5))
84+
plt.imshow(cm_plot, interpolation="nearest")
85+
plt.title(title)
86+
plt.colorbar()
87+
tick_marks = np.arange(len(classes))
88+
plt.xticks(tick_marks, classes)
89+
plt.yticks(tick_marks, classes)
90+
91+
# Label each cell
92+
thresh = cm_plot.max() / 2.0
93+
for i in range(cm_plot.shape[0]):
94+
for j in range(cm_plot.shape[1]):
95+
txt = f"{cm_plot[i, j]:.2f}" if normalize else f"{int(cm_plot[i, j])}"
96+
plt.text(j, i, txt,
97+
horizontalalignment="center",
98+
verticalalignment="center",
99+
fontsize=7,
100+
color="white" if cm_plot[i, j] > thresh else "black")
101+
102+
plt.ylabel("True label")
103+
plt.xlabel("Predicted label")
104+
plt.tight_layout()
105+
if fname:
106+
plt.savefig(fname, dpi=150)
107+
print(f"Saved: {fname}")
108+
plt.show()
109+
110+
111+
def sample_predictions_onnx(onnx_path, dataset, k=24, seed=0):
112+
"""Show a grid of sample predictions (mix of correct and misclassified)."""
113+
import onnxruntime as ort
114+
rng = np.random.default_rng(seed)
115+
sess = ort.InferenceSession(onnx_path, providers=["CPUExecutionProvider"])
116+
117+
# Over-sample candidates then choose some wrong + some right
118+
idxs = rng.choice(len(dataset), size=min(k * 2, len(dataset)), replace=False)
119+
imgs, ys, preds = [], [], []
120+
121+
for i in idxs:
122+
x, y = dataset[i] # x: [1,28,28] after transforms; y: int
123+
x_np = x.unsqueeze(0).numpy().astype(np.float32) # [1,1,28,28]
124+
logits = sess.run(["logits"], {"input": x_np})[0] # [1,10]
125+
p = int(np.argmax(logits, axis=1)[0])
126+
imgs.append(x.squeeze(0).numpy()) # [28,28]
127+
ys.append(int(y))
128+
preds.append(p)
129+
130+
mis_idx = [i for i, (t, p) in enumerate(zip(ys, preds)) if t != p]
131+
cor_idx = [i for i, (t, p) in enumerate(zip(ys, preds)) if t == p]
132+
picked = (mis_idx[:k // 2] + cor_idx[:k - len(mis_idx[:k // 2])])[:k]
133+
if not picked: # fallback
134+
picked = list(range(min(k, len(imgs))))
135+
136+
# Plot grid
137+
import math
138+
cols = 8
139+
rows = math.ceil(len(picked) / cols)
140+
plt.figure(figsize=(cols * 1.6, rows * 1.8))
141+
for j, idx in enumerate(picked):
142+
plt.subplot(rows, cols, j + 1)
143+
plt.imshow(imgs[idx], cmap="gray")
144+
t, p = ys[idx], preds[idx]
145+
title = f"T:{t} P:{p}"
146+
color = "green" if t == p else "red"
147+
plt.title(title, color=color, fontsize=9)
148+
plt.axis("off")
149+
plt.tight_layout()
150+
out = os.path.join(ARTI_DIR, "samples_grid.png")
151+
plt.savefig(out, dpi=150)
152+
print(f"Saved: {out}")
153+
plt.show()
154+
155+
def main():
156+
# Optional: evaluate the best PyTorch checkpoint for reference
157+
pt_ckpt = os.path.join(ARTI_DIR, "digitnet_best.pth")
158+
if os.path.exists(pt_ckpt):
159+
model = DigitNet()
160+
model.load_state_dict(torch.load(pt_ckpt, map_location="cpu"))
161+
pt_acc = evaluate_pytorch(model, val_loader)
162+
print(f"PyTorch val acc: {pt_acc:.4f}")
163+
else:
164+
print("No PyTorch checkpoint found; skipping PT eval.")
165+
166+
# Evaluate ONNX fp32
167+
if os.path.exists(ONNX_PATH):
168+
acc, cm = confusion_matrix_onnx(ONNX_PATH, val_loader)
169+
print(f"ONNX fp32 val acc: {acc:.4f}")
170+
print("Confusion matrix (rows=true, cols=pred):\n", cm)
171+
172+
# Plots: counts + normalized
173+
plot_confusion_matrix(cm, normalize=False,
174+
title="ONNX fp32 – Confusion (counts)",
175+
fname=os.path.join(ARTI_DIR, "cm_fp32_counts.png"))
176+
plot_confusion_matrix(cm, normalize=True,
177+
title="ONNX fp32 – Confusion (row-normalized)",
178+
fname=os.path.join(ARTI_DIR, "cm_fp32_norm.png"))
179+
180+
# Sample predictions grid
181+
try:
182+
sample_predictions_onnx(ONNX_PATH, val_ds, k=24)
183+
except Exception as e:
184+
print("Sample grid skipped:", e)
185+
else:
186+
print("Missing ONNX model:", ONNX_PATH)
187+
188+
if __name__ == "__main__":
189+
main()
190+
```
191+
192+
The script first loads the validation dataset using the same preprocessing pipeline as training, including forced grayscale conversion to ensure a single input channel. It then optionally evaluates the best PyTorch checkpoint (digitnet_best.pth) to establish a reference accuracy.
193+
194+
Next, the exported ONNX model (sudoku_digitnet.onnx) is loaded using ONNX Runtime and evaluated in batches. Because the model was exported with a dynamic batch dimension, inference can be performed efficiently on larger batches, which is representative of how the model will be used later in the pipeline.
195+
196+
The script expects two things from the earlier steps:
197+
1. A validation dataset stored under data/val/0..9/…
198+
2. A trained model exported in previous step and stored under artifacts/
199+
* artifacts/digitnet_best.pth (optional, PyTorch weights)
200+
* artifacts/sudoku_digitnet.onnx (required, ONNX model)
201+
202+
When you run the script, it first loads the validation dataset using the same preprocessing as training, including forcing grayscale so the input has a single channel. It then optionally evaluates the PyTorch checkpoint to provide a reference accuracy. After that, it runs batched inference with ONNX Runtime, computes an overall accuracy, and builds a confusion matrix (true class vs predicted class) that reveals which digits are being confused.
203+
204+
In addition to printing accuracy metrics, the script generates two types of diagnostic outputs:
205+
1. Confusion matrix visualizations, saved as:
206+
* artifacts/cm_fp32_counts.png (raw counts)
207+
* artifacts/cm_fp32_norm.png (row-normalized)
208+
2. A grid of example predictions, saved as:
209+
*artifacts/samples_grid.png
210+
211+
These artifacts provide both quantitative and qualitative insight into model performance.
212+
213+
In the sample grid, each tile shows one crop together with its True label (T:) and Predicted label (P:), with correct predictions highlighted in green and mistakes highlighted in red. This makes it easy to quickly verify that the classifier behaves sensibly and to spot remaining failure modes.
214+
215+
## Running the script
216+
Run the evaluation script from the project root:
217+
218+
```console
219+
python 04_Test.py
220+
```
221+
222+
In the example below, the PyTorch and ONNX accuracies match exactly, confirming that the export process preserved model behavior.
223+
224+
```console
225+
python3 04_Test.py
226+
PyTorch val acc: 0.9928
227+
ONNX eval: 100%|███████████████████████████████████████████████████████████| 32/32 [00:01<00:00, 21.06it/s]
228+
ONNX fp32 val acc: 0.9928
229+
Confusion matrix (rows=true, cols=pred):
230+
[[12623 7 0 0 0 0 0 0 0 0]
231+
[ 0 420 0 0 0 0 0 0 0 0]
232+
[ 0 0 331 0 4 0 1 0 0 0]
233+
[ 0 1 0 332 0 1 0 0 0 0]
234+
[ 0 0 0 0 460 0 0 0 0 0]
235+
[ 0 1 0 1 0 486 2 0 0 0]
236+
[ 1 0 0 0 0 19 387 0 1 2]
237+
[ 0 1 0 0 0 0 0 375 0 0]
238+
[ 0 0 0 0 0 6 27 0 297 10]
239+
[ 0 1 0 0 0 14 10 0 7 372]]
240+
Saved: artifacts/cm_fp32_counts.png
241+
```
242+
243+
![img1](Figures/01.png)
244+
The confusion matrix provides more insight than a single accuracy number. Each row corresponds to the true class, and each column corresponds to the predicted class. A strong diagonal indicates correct classification. In this output, blank cells (class 0) are almost always recognized correctly, while the remaining errors occur primarily between visually similar printed digits such as 6, 8, and 9.
245+
246+
This behavior is expected and indicates that the model has learned meaningful digit features. The remaining confusions are rare and can be addressed later through targeted augmentation or higher-resolution crops if needed.
247+
248+
## Summary
249+
With inference validated and error modes understood, the digit recognizer is now ready to be embedded into the full Sudoku image-processing pipeline, where OpenCV will be used to detect the grid, rectify perspective, segment cells, and run batched ONNX inference to reconstruct and solve complete puzzles.

0 commit comments

Comments
 (0)