teeny_vision/mnist/
dataset.rs1use std::{
24 fs::File,
25 path::{Path, PathBuf},
26};
27
28use anyhow::{Result, anyhow};
29use arrow_array::{Array, BinaryArray, Int64Array, LargeBinaryArray, StructArray};
30use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
31
32pub struct MnistBatch {
34 pub images: Vec<f32>,
36 pub labels: Vec<u8>,
38 pub batch_size: usize,
40}
41
42impl MnistBatch {
43 pub fn image_numel(&self) -> usize {
45 self.batch_size * 28 * 28
46 }
47}
48
49pub struct MnistDataset {
51 path: PathBuf,
52 num_rows: usize,
53}
54
55impl MnistDataset {
56 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
58 let path = path.as_ref().to_path_buf();
59 let file = File::open(&path)?;
60 let builder = ParquetRecordBatchReaderBuilder::try_new(file)?;
61 let num_rows = builder.metadata().file_metadata().num_rows() as usize;
62 Ok(Self { path, num_rows })
63 }
64
65 pub fn len(&self) -> usize {
67 self.num_rows
68 }
69
70 pub fn is_empty(&self) -> bool {
72 self.num_rows == 0
73 }
74
75 pub fn read_batch(&self, offset: usize, count: usize) -> Result<MnistBatch> {
79 let actual = count.min(self.num_rows.saturating_sub(offset));
80 if actual == 0 {
81 return Err(anyhow!(
82 "offset {offset} is at or past the end of the dataset (len={})",
83 self.num_rows
84 ));
85 }
86
87 let file = File::open(&self.path)?;
88 let reader = ParquetRecordBatchReaderBuilder::try_new(file)?
89 .with_offset(offset)
90 .with_limit(actual)
91 .with_batch_size(actual)
92 .build()?;
93
94 let batch = reader
95 .into_iter()
96 .next()
97 .ok_or_else(|| anyhow!("parquet reader returned no record batches"))??;
98
99 decode_batch(&batch, actual)
100 }
101}
102
103fn decode_batch(batch: &arrow_array::RecordBatch, n: usize) -> Result<MnistBatch> {
104 let image_struct = batch
106 .column_by_name("image")
107 .ok_or_else(|| anyhow!("column 'image' not found in parquet file"))?
108 .as_any()
109 .downcast_ref::<StructArray>()
110 .ok_or_else(|| anyhow!("column 'image' is not a StructArray"))?;
111
112 let bytes_field = image_struct
114 .column_by_name("bytes")
115 .ok_or_else(|| anyhow!("image struct has no 'bytes' field"))?;
116
117 let label_col = batch
119 .column_by_name("label")
120 .ok_or_else(|| anyhow!("column 'label' not found in parquet file"))?
121 .as_any()
122 .downcast_ref::<Int64Array>()
123 .ok_or_else(|| anyhow!("column 'label' is not Int64Array"))?;
124
125 let mut images = Vec::with_capacity(n * 28 * 28);
126 let mut labels = Vec::with_capacity(n);
127
128 for i in 0..n {
129 let png_bytes: &[u8] = if let Some(col) = bytes_field.as_any().downcast_ref::<BinaryArray>()
130 {
131 col.value(i)
132 } else if let Some(col) = bytes_field.as_any().downcast_ref::<LargeBinaryArray>() {
133 col.value(i)
134 } else {
135 return Err(anyhow!(
136 "'bytes' field has unexpected Arrow type: {:?}",
137 bytes_field.data_type()
138 ));
139 };
140
141 let img = image::load_from_memory(png_bytes)
142 .map_err(|e| anyhow!("failed to decode PNG for sample {i}: {e}"))?
143 .into_luma8();
144
145 if img.width() != 28 || img.height() != 28 {
146 return Err(anyhow!(
147 "unexpected image dimensions for sample {i}: {}×{}",
148 img.width(),
149 img.height()
150 ));
151 }
152
153 for px in img.pixels() {
154 images.push(px.0[0] as f32 / 255.0);
155 }
156
157 labels.push(label_col.value(i) as u8);
158 }
159
160 Ok(MnistBatch {
161 images,
162 labels,
163 batch_size: n,
164 })
165}