Skip to content

Commit fc5e1ea

Browse files
Update material
1 parent 0d7cb75 commit fc5e1ea

5 files changed

Lines changed: 10406 additions & 31 deletions

File tree

‎.gitignore‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ wasm/
3030
__marimo__/
3131
layouts/
3232
*.pkl
33-
todo.txt
33+
todo.txt
34+
demo_digits_extraction.ipynb

‎inference.py‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,38 +22,49 @@
2222

2323

2424
def extract_digit_from_digit_roi(image: np.ndarray) -> np.ndarray:
25-
image = img_as_ubyte(image)
25+
"""Assuming that a digit is drawn inside the ROI, we extract a crop around it and resize it to (50, 50)."""
26+
image = img_as_ubyte(image) # Convert to 8-bit
2627

28+
# Downscale by a factor 2
2729
resized = rescale(image, scale=0.5)
2830

31+
# Otsu threshold
2932
try:
3033
binary = resized > threshold_otsu(resized)
3134
except ValueError:
3235
return img_as_ubyte(np.zeros((50, 50)))
3336

37+
# Labelling
3438
labelled = label(binary)
3539

40+
# The white background is assumed to be the biggest object
3641
light_square = keep_n_biggest_objects(labelled, n=1)
3742

43+
# We assume that the digit is the biggest "hole" in that bright background
44+
# => If the digit is connected to the border it won't be detected
3845
filled_light_square = ndi.binary_fill_holes(light_square)
39-
4046
binary_in_light_square = np.logical_and(filled_light_square, light_square == 0)
41-
4247
labelled_in_light_square = label(binary_in_light_square)
4348

4449
n_objects = labelled_in_light_square.max()
4550
if n_objects == 0:
51+
# Case where the ROI is empty
4652
digit_crop = resized
4753
else:
54+
# Digit = biggest object
4855
digit = keep_n_biggest_objects(labelled_in_light_square, n=1)
56+
# Extract bounding box
4957
df = pd.DataFrame(
5058
regionprops_table(digit, intensity_image=resized, properties=["bbox"])
5159
)
5260
digit_row = df.iloc[0]
61+
# Crop around the digit
5362
digit_crop = resized[digit_row["bbox-0"] : digit_row["bbox-2"], digit_row["bbox-1"] : digit_row["bbox-3"]]
5463

64+
# Resize to a fixed size
5565
digit_crop = resize(digit_crop, output_shape=(50, 50))
5666

67+
# Convert to 8-bit
5768
digit_crop = img_as_ubyte(digit_crop)
5869

5970
return digit_crop

‎intro_to_ml.py‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def _():
147147
return (example_slider,)
148148

149149

150-
@app.cell
150+
@app.cell(hide_code=True)
151151
def _(example_slider):
152152
mo.md(f"""
153153
The selected value is: **{example_slider.value}**
@@ -283,6 +283,21 @@ def _(df, imread, np):
283283
return (images,)
284284

285285

286+
@app.cell(hide_code=True)
287+
def _(dataset_acquisition_fig):
288+
mo.md(f"""
289+
/// details | How was this image dataset created?
290+
291+
This dataset was created by imaging 3 by 4 grids of numbers written in a predefined order (the ground truth class labels were inferred from that order). The image processing steps used to extract regions of interest around the numbers are illustrated below:
292+
293+
{dataset_acquisition_fig}
294+
295+
The Python script used to implement these steps is available [here](https://github.com/EPFL-Center-for-Imaging/vision-workshop/blob/main/preprocessing.py).
296+
///
297+
""")
298+
return
299+
300+
286301
@app.cell(hide_code=True)
287302
def _(num_labels):
288303
mo.md(f"""
@@ -1435,6 +1450,14 @@ def _(x_train_projected):
14351450
return (extent,)
14361451

14371452

1453+
@app.cell(hide_code=True)
1454+
def _(public_folder_path):
1455+
_img_src = public_folder_path / "assets" / "training_fig.svg"
1456+
1457+
dataset_acquisition_fig = mo.vstack([mo.image(src=_img_src, alt="training_fig", height=600, caption="How the dataset was created")], align="center")
1458+
return (dataset_acquisition_fig,)
1459+
1460+
14381461
@app.cell(hide_code=True)
14391462
def _(public_folder_path):
14401463
_img_src = public_folder_path / "assets" / "inference.png"

‎preprocessing.py‎

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
"""
2+
Script used to extract digit images and the corresponding ground truth labels from images of training grids captured by a camera device.
3+
4+
The ground truth labels are inferred from the position of the digits in the grid, which is predetermined.
5+
"""
16
import os
27
import sys
38
from pathlib import Path
@@ -15,14 +20,15 @@
1520

1621

1722
def keep_n_biggest_objects(labelled: np.ndarray, n=1) -> np.ndarray:
23+
"""Remove all but the N biggest objects in a labelled array."""
1824
uniques, counts = np.unique(labelled, return_counts=1)
1925

2026
# Ignore the background if it's there
2127
if uniques[0] == 0:
2228
uniques = uniques[1:]
2329
counts = counts[1:]
2430

25-
# Sort unique values by counts (descending), then extract the n unique values corresponding to the biggest objects
31+
# Sort unique values by counts (descending), then extract the N unique values corresponding to the biggest objects
2632
biggest_labels = uniques[np.argsort(counts)[::-1][:n]]
2733

2834
biggest_objects_filt = np.isin(labelled, biggest_labels)
@@ -33,7 +39,8 @@ def keep_n_biggest_objects(labelled: np.ndarray, n=1) -> np.ndarray:
3339
return biggest_objects_mask
3440

3541

36-
def detect_digit_squares(image: np.ndarray, grid_shape: tuple) -> pd.DataFrame:
42+
def detect_digit_rois_in_training_grid(image: np.ndarray, grid_shape: tuple) -> pd.DataFrame:
43+
"""Extract digits from the image of a training grid."""
3744
# Convert the image to gray
3845
gray = rgb2gray(image)
3946

@@ -79,20 +86,15 @@ def detect_digit_squares(image: np.ndarray, grid_shape: tuple) -> pd.DataFrame:
7986
return df
8087

8188

82-
def extract_digit_from_digit_square(image: np.ndarray) -> np.ndarray:
89+
def extract_digit_from_digit_roi(image: np.ndarray) -> np.ndarray:
90+
"""Note: almost the same as in the inference script, but we print warnings."""
8391
image = img_as_ubyte(image)
84-
8592
resized = rescale(image, scale=0.5)
86-
8793
binary = resized > threshold_otsu(resized)
8894
labelled = label(binary)
89-
9095
light_square = keep_n_biggest_objects(labelled, n=1)
91-
9296
filled_light_square = ndi.binary_fill_holes(light_square)
93-
9497
binary_in_light_square = np.logical_and(filled_light_square, light_square == 0)
95-
9698
labelled_in_light_square = label(binary_in_light_square)
9799

98100
n_objects = labelled_in_light_square.max()
@@ -108,49 +110,54 @@ def extract_digit_from_digit_square(image: np.ndarray) -> np.ndarray:
108110
digit_crop = resized[digit_row["bbox-0"] : digit_row["bbox-2"], digit_row["bbox-1"] : digit_row["bbox-3"]]
109111

110112
digit_crop = resize(digit_crop, output_shape=(50, 50))
111-
112113
digit_crop = img_as_ubyte(digit_crop)
113114

114115
return digit_crop
115116

116117

117118
if __name__ == "__main__":
119+
# Run with, for example: `python preprocessing.py datasets/`
118120
_, root_img_folder = sys.argv
119121

120-
# We assume 3 rows and 4 columns, with labels in a fixed, given order:
122+
# We assume that the images contain a (N, M) grid of digits in a predetermined order:
121123
grid_shape = (3, 4)
122-
labels = np.array(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B"]).reshape(grid_shape)
124+
class_labels = np.array(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B"]).reshape(grid_shape)
123125

124126
root = Path(root_img_folder)
125127

126-
dst_root = root / "digits"
127-
if not dst_root.exists():
128-
os.mkdir(dst_root)
128+
training_images_dir = root / "raw_training_images"
129+
130+
dataset_dir = root / "dataset"
131+
if not dataset_dir.exists():
132+
os.mkdir(dataset_dir)
129133

130-
for lab in labels.ravel():
131-
dst_dir = dst_root / lab
134+
# Create a subdirectory for each class
135+
for lab in class_labels.ravel():
136+
dst_dir = dataset_dir / lab
132137
if not dst_dir.exists():
133138
os.mkdir(dst_dir)
134139

135-
for image_file in root.glob("*.jpg"):
140+
# Iterate over the training images
141+
for image_file in training_images_dir.glob("*.jpg"):
136142
print("---")
137143
print(image_file)
138144
print("---")
139145

140-
img = skimage.io.imread(image_file)
146+
training_img = skimage.io.imread(image_file)
141147

142148
# Extract `digit squares` crops, sorted by [centroid-0, centroid-1] so we can assume their class label.
143-
df = detect_digit_squares(img, grid_shape)
149+
df = detect_digit_rois_in_training_grid(training_img, grid_shape)
144150

145151
# Add the `class` label
146-
df["class"] = labels.ravel()
152+
df["class"] = class_labels.ravel()
147153

148154
for _, row in df.iterrows():
149-
digit_square_img = row["intensity_image"]
155+
digit_roi = row["intensity_image"]
156+
digit_cls = row["class"]
150157

151-
digit = extract_digit_from_digit_square(digit_square_img)
158+
digit_img = extract_digit_from_digit_roi(digit_roi)
152159

153-
# Save it in the corresponding class subfolder
154-
dst_file = dst_root / row["class"] / f"{image_file.stem}.png"
160+
# Save the digit image in the corresponding class subfolder
161+
dst_file = dataset_dir / digit_cls / f"{image_file.stem}.png"
155162

156-
skimage.io.imsave(dst_file, digit)
163+
skimage.io.imsave(dst_file, digit_img)

0 commit comments

Comments
 (0)