use crate::DOWNLOAD_RETRIES;
use dataset_core::{DatasetError, acquire_dataset, download_to_with_retries, gunzip};
use ndarray::{Array1, Array2};
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
pub(super) const IMAGE_ROWS: usize = 28;
pub(super) const IMAGE_COLS: usize = 28;
pub(super) const N_PIXELS: usize = IMAGE_ROWS * IMAGE_COLS;
pub(super) const N_CLASSES: usize = 10;
const IDX_IMAGES_MAGIC: u32 = 2051;
const IDX_LABELS_MAGIC: u32 = 2049;
const IDX_IMAGES_HEADER_LEN: usize = 16;
const IDX_LABELS_HEADER_LEN: usize = 8;
pub(super) type IdxImageData = (Array2<u8>, Array1<u8>);
pub(super) struct Partition {
pub images_url: &'static str,
pub images_filename: &'static str,
pub images_sha256: &'static str,
pub labels_url: &'static str,
pub labels_filename: &'static str,
pub labels_sha256: &'static str,
pub n_samples: usize,
}
fn read_be_u32(header: &[u8], offset: usize) -> u32 {
u32::from_be_bytes([
header[offset],
header[offset + 1],
header[offset + 2],
header[offset + 3],
])
}
fn header_error(dataset_name: &str, field_name: &str, value: u32) -> DatasetError {
DatasetError::invalid_value(dataset_name, field_name, &value.to_string(), 1)
}
fn acquire_idx_file(
dir: &str,
dataset_name: &str,
url: &str,
filename: &str,
expected_sha256: &str,
) -> Result<PathBuf, DatasetError> {
let gz_filename = format!("{filename}.gz");
acquire_dataset(
dir,
filename,
dataset_name,
Some(expected_sha256),
|temp_path| {
download_to_with_retries(url, temp_path, Some(&gz_filename), DOWNLOAD_RETRIES)?;
let idx_path = temp_path.join(filename);
gunzip(&temp_path.join(&gz_filename), &idx_path)?;
Ok(idx_path)
},
)
}
fn read_idx_images(
dataset_name: &str,
file_path: &Path,
n_samples: usize,
pixels: &mut Vec<u8>,
) -> Result<(), DatasetError> {
let mut file = File::open(file_path)?;
let mut header = [0u8; IDX_IMAGES_HEADER_LEN];
file.read_exact(&mut header)?;
let magic = read_be_u32(&header, 0);
if magic != IDX_IMAGES_MAGIC {
return Err(header_error(dataset_name, "idx_images_magic", magic));
}
let count = read_be_u32(&header, 4) as usize;
if count != n_samples {
return Err(DatasetError::length_mismatch(
dataset_name,
"images",
n_samples,
count,
));
}
let rows = read_be_u32(&header, 8);
let cols = read_be_u32(&header, 12);
if rows as usize != IMAGE_ROWS {
return Err(header_error(dataset_name, "image_rows", rows));
}
if cols as usize != IMAGE_COLS {
return Err(header_error(dataset_name, "image_cols", cols));
}
let before = pixels.len();
file.read_to_end(pixels)?;
let read = pixels.len() - before;
let expected = n_samples * N_PIXELS;
if read != expected {
return Err(DatasetError::length_mismatch(
dataset_name,
"pixels",
expected,
read,
));
}
Ok(())
}
fn read_idx_labels(
dataset_name: &str,
file_path: &Path,
n_samples: usize,
labels: &mut Vec<u8>,
) -> Result<(), DatasetError> {
let mut file = File::open(file_path)?;
let mut header = [0u8; IDX_LABELS_HEADER_LEN];
file.read_exact(&mut header)?;
let magic = read_be_u32(&header, 0);
if magic != IDX_LABELS_MAGIC {
return Err(header_error(dataset_name, "idx_labels_magic", magic));
}
let count = read_be_u32(&header, 4) as usize;
if count != n_samples {
return Err(DatasetError::length_mismatch(
dataset_name,
"labels",
n_samples,
count,
));
}
let before = labels.len();
file.read_to_end(labels)?;
let read = labels.len() - before;
if read != n_samples {
return Err(DatasetError::length_mismatch(
dataset_name,
"labels",
n_samples,
read,
));
}
for (offset, &label) in labels[before..].iter().enumerate() {
if label as usize >= N_CLASSES {
return Err(DatasetError::invalid_value(
dataset_name,
"label",
&label.to_string(),
before + offset + 1,
));
}
}
Ok(())
}
pub(super) fn load_partitions(
dir: &str,
dataset_name: &str,
subset: &'static [&'static Partition],
) -> Result<IdxImageData, DatasetError> {
let n_samples: usize = subset.iter().map(|partition| partition.n_samples).sum();
let mut pixels: Vec<u8> = Vec::with_capacity(n_samples * N_PIXELS);
let mut labels: Vec<u8> = Vec::with_capacity(n_samples);
for partition in subset {
let images_path = acquire_idx_file(
dir,
dataset_name,
partition.images_url,
partition.images_filename,
partition.images_sha256,
)?;
let labels_path = acquire_idx_file(
dir,
dataset_name,
partition.labels_url,
partition.labels_filename,
partition.labels_sha256,
)?;
read_idx_images(dataset_name, &images_path, partition.n_samples, &mut pixels)?;
read_idx_labels(dataset_name, &labels_path, partition.n_samples, &mut labels)?;
}
if labels.is_empty() {
return Err(DatasetError::empty_dataset(dataset_name));
}
let features_array = Array2::from_shape_vec((n_samples, N_PIXELS), pixels)
.map_err(|e| DatasetError::array_shape_error(dataset_name, "features", e))?;
let labels_array = Array1::from_vec(labels);
Ok((features_array, labels_array))
}