use std::fs::File;
use std::io::{BufReader, Seek, Write};
use std::path::Path;
use npyz::npz::{NpzArchive, NpzWriter};
use npyz::{AutoSerialize, Deserialize, Serialize, WriterBuilder};
use zip::write::FileOptions;
use zip::CompressionMethod;
use super::IoError;
use crate::representation::{EventFrame, EventFrameData, RepresentationKind};
use crate::{EventStream, EventStreamBuilder};
const EVENT_DATA_KEY: &str = "event_data";
const N_IMAGENET_WIDTH: usize = 640;
const N_IMAGENET_HEIGHT: usize = 480;
const DEFAULT_SCALE_MS: f64 = 0.001;
#[derive(npyz::Deserialize)]
struct NImageNetEvent {
x: u16,
y: u16,
t: u16,
p: bool,
}
pub fn read_npz(
path: impl AsRef<Path>,
sensor: Option<(usize, usize)>,
) -> Result<EventStream, IoError> {
let mut archive = open_archive(path)?;
if archive
.by_name(EVENT_DATA_KEY)
.map_err(IoError::Io)?
.is_some()
{
read_n_imagenet(&mut archive, sensor)
} else {
read_native_stream(&mut archive, sensor)
}
}
fn read_n_imagenet(
archive: &mut NpzArchive<BufReader<File>>,
sensor: Option<(usize, usize)>,
) -> Result<EventStream, IoError> {
let (width, height) = sensor.unwrap_or((N_IMAGENET_WIDTH, N_IMAGENET_HEIGHT));
let npy = archive
.by_name(EVENT_DATA_KEY)
.map_err(IoError::Io)?
.ok_or_else(|| IoError::Format("missing event_data array".to_owned()))?;
let [event_count] = npy.shape() else {
return Err(IoError::Format(
"event_data must be one-dimensional".to_owned(),
));
};
let event_count = usize::try_from(*event_count)
.map_err(|_| IoError::Format("event_data is too large".to_owned()))?;
let records = npy
.data::<NImageNetEvent>()
.map_err(|error| IoError::Format(error.to_string()))?;
let mut builder =
EventStreamBuilder::with_capacity(width, height, DEFAULT_SCALE_MS, event_count);
for record in records {
let record = record.map_err(|error| IoError::Format(error.to_string()))?;
if !builder.push(record.x, record.y, i64::from(record.t), record.p) {
return Err(IoError::Format(format!(
"event coordinate ({}, {}) exceeds sensor size {}x{}",
record.x, record.y, width, height
)));
}
}
Ok(builder.build())
}
fn read_native_stream(
archive: &mut NpzArchive<BufReader<File>>,
sensor: Option<(usize, usize)>,
) -> Result<EventStream, IoError> {
let xs = read_array::<u16>(archive, "x")?;
let ys = read_array::<u16>(archive, "y")?;
let ts = read_array::<i64>(archive, "t")?;
let ps = read_array::<u8>(archive, "p")?;
let count = xs.len();
if ys.len() != count || ts.len() != count || ps.len() != count {
return Err(IoError::Format(
"event columns x/y/t/p have mismatched lengths".to_owned(),
));
}
let scale = read_scalar::<f64>(archive, "timestamp_scale_ms")?.unwrap_or(DEFAULT_SCALE_MS);
let (width, height) = match sensor {
Some(size) => size,
None => match (
read_scalar::<u64>(archive, "width")?,
read_scalar::<u64>(archive, "height")?,
) {
(Some(width), Some(height)) => (width as usize, height as usize),
_ => infer_size(&xs, &ys),
},
};
let mut builder = EventStreamBuilder::with_capacity(width, height, scale, count);
for index in 0..count {
builder.push(xs[index], ys[index], ts[index], ps[index] != 0);
}
Ok(builder.build())
}
pub fn write_npz_stream(path: impl AsRef<Path>, stream: &EventStream) -> Result<(), IoError> {
let mut npz = NpzWriter::create(path).map_err(IoError::Io)?;
write_array(&mut npz, "t", stream.ts())?;
write_array(&mut npz, "x", stream.xs())?;
write_array(&mut npz, "y", stream.ys())?;
let polarities: Vec<u8> = stream.ps().iter().map(|&p| u8::from(p)).collect();
write_array(&mut npz, "p", &polarities)?;
let (width, height) = stream.sensor_size();
write_array(&mut npz, "width", &[width as u64])?;
write_array(&mut npz, "height", &[height as u64])?;
write_array(
&mut npz,
"timestamp_scale_ms",
&[stream.timestamp_scale_ms()],
)?;
finish(npz)
}
pub fn write_npz_frame(path: impl AsRef<Path>, frame: &EventFrame) -> Result<(), IoError> {
let (channels, height, width) = frame.shape();
let mut npz = NpzWriter::create(path).map_err(IoError::Io)?;
write_array(
&mut npz,
"shape",
&[channels as u64, height as u64, width as u64],
)?;
write_array(&mut npz, "kind", frame.kind().as_str().as_bytes())?;
write_array(
&mut npz,
"channel_names",
frame.channel_names().join("\n").as_bytes(),
)?;
let code = match frame.data() {
EventFrameData::U8(values) => {
write_array(&mut npz, "data", values)?;
0u8
}
EventFrameData::U16(values) => {
write_array(&mut npz, "data", values)?;
1
}
EventFrameData::U64(values) => {
write_array(&mut npz, "data", values)?;
2
}
EventFrameData::F32(values) => {
write_array(&mut npz, "data", values)?;
3
}
};
write_array(&mut npz, "dtype", &[code])?;
finish(npz)
}
pub fn read_npz_frame(path: impl AsRef<Path>) -> Result<EventFrame, IoError> {
let mut archive = open_archive(path)?;
let shape = read_array::<u64>(&mut archive, "shape")?;
let &[channels, height, width] = shape.as_slice() else {
return Err(IoError::Format(
"frame 'shape' must have exactly three elements".to_owned(),
));
};
let (channels, height, width) = (channels as usize, height as usize, width as usize);
let kind = String::from_utf8(read_array::<u8>(&mut archive, "kind")?)
.map_err(|_| IoError::Format("frame 'kind' is not valid UTF-8".to_owned()))?;
let kind = RepresentationKind::from_tag(&kind)
.ok_or_else(|| IoError::Format(format!("unknown representation kind '{kind}'")))?;
let names = String::from_utf8(read_array::<u8>(&mut archive, "channel_names")?)
.map_err(|_| IoError::Format("frame 'channel_names' is not valid UTF-8".to_owned()))?;
let channel_names: Vec<String> = if names.is_empty() {
Vec::new()
} else {
names.split('\n').map(str::to_owned).collect()
};
let code = read_scalar::<u8>(&mut archive, "dtype")?
.ok_or_else(|| IoError::Format("missing frame 'dtype' code".to_owned()))?;
let data = match code {
0 => EventFrameData::U8(read_array::<u8>(&mut archive, "data")?),
1 => EventFrameData::U16(read_array::<u16>(&mut archive, "data")?),
2 => EventFrameData::U64(read_array::<u64>(&mut archive, "data")?),
3 => EventFrameData::F32(read_array::<f32>(&mut archive, "data")?),
other => return Err(IoError::Format(format!("unknown frame dtype code {other}"))),
};
if data.len() != channels * height * width {
return Err(IoError::Format(
"frame 'data' length does not match its shape".to_owned(),
));
}
Ok(EventFrame::from_parts(
data,
width,
height,
kind,
channel_names,
))
}
fn open_archive(path: impl AsRef<Path>) -> Result<NpzArchive<BufReader<File>>, IoError> {
NpzArchive::open(path).map_err(|error| {
if error.kind() == std::io::ErrorKind::InvalidData {
IoError::Format(error.to_string())
} else {
IoError::Io(error)
}
})
}
fn read_array<T: Deserialize>(
archive: &mut NpzArchive<BufReader<File>>,
name: &str,
) -> Result<Vec<T>, IoError> {
archive
.by_name(name)
.map_err(IoError::Io)?
.ok_or_else(|| IoError::Format(format!("missing array '{name}'")))?
.into_vec::<T>()
.map_err(IoError::Io)
}
fn read_scalar<T: Deserialize>(
archive: &mut NpzArchive<BufReader<File>>,
name: &str,
) -> Result<Option<T>, IoError> {
match archive.by_name(name).map_err(IoError::Io)? {
Some(npy) => Ok(npy.into_vec::<T>().map_err(IoError::Io)?.into_iter().next()),
None => Ok(None),
}
}
fn write_array<T, W>(npz: &mut NpzWriter<W>, name: &str, data: &[T]) -> Result<(), IoError>
where
T: Serialize + AutoSerialize + Clone,
W: Write + Seek,
{
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
let mut writer = npz
.array::<T>(name, options)
.map_err(IoError::Io)?
.default_dtype()
.shape(&[data.len() as u64])
.begin_nd()
.map_err(IoError::Io)?;
writer.extend(data.iter().cloned()).map_err(IoError::Io)?;
writer.finish().map_err(IoError::Io)
}
fn finish<W: Write + Seek>(mut npz: NpzWriter<W>) -> Result<(), IoError> {
npz.zip_writer()
.finish()
.map(|_| ())
.map_err(|error| IoError::Format(format!("zip: {error}")))
}
fn infer_size(xs: &[u16], ys: &[u16]) -> (usize, usize) {
let width = xs.iter().copied().max().map_or(1, |x| usize::from(x) + 1);
let height = ys.iter().copied().max().map_or(1, |y| usize::from(y) + 1);
(width, height)
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::{read_npz, read_npz_frame, write_npz_frame, write_npz_stream};
use crate::representation::{Representation, VoxelGrid};
use crate::{EventStream, EventStreamBuilder};
fn temp_path(tag: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
std::env::temp_dir().join(format!("eventcv_{tag}_{nanos}.npz"))
}
fn sample_stream() -> EventStream {
let mut builder = EventStreamBuilder::new(20, 15, 0.5); for &(x, y, t, p) in &[
(0u16, 0u16, 10i64, true),
(19, 14, 2_000, false),
(5, 7, 3_000, true),
] {
builder.push(x, y, t, p);
}
builder.build()
}
fn assert_streams_eq(a: &EventStream, b: &EventStream) {
assert_eq!(a.xs(), b.xs());
assert_eq!(a.ys(), b.ys());
assert_eq!(a.ts(), b.ts());
assert_eq!(a.ps(), b.ps());
assert_eq!(a.sensor_size(), b.sensor_size());
assert_eq!(a.timestamp_scale_ms(), b.timestamp_scale_ms());
}
#[test]
fn native_stream_round_trips_exactly() {
let stream = sample_stream();
let path = temp_path("npz_stream");
write_npz_stream(&path, &stream).unwrap();
assert_streams_eq(&read_npz(&path, None).unwrap(), &stream);
std::fs::remove_file(&path).ok();
}
#[test]
fn empty_stream_round_trips() {
let stream = EventStreamBuilder::new(8, 6, 0.001).build();
let path = temp_path("npz_empty");
write_npz_stream(&path, &stream).unwrap();
let loaded = read_npz(&path, None).unwrap();
assert!(loaded.is_empty());
assert_eq!(loaded.sensor_size(), (8, 6));
std::fs::remove_file(&path).ok();
}
#[test]
fn frame_round_trips_with_metadata() {
let frame = VoxelGrid::new(3, 30.0).generate(&sample_stream()).unwrap();
let path = temp_path("npz_frame");
write_npz_frame(&path, &frame).unwrap();
let loaded = read_npz_frame(&path).unwrap();
assert_eq!(loaded.shape(), frame.shape());
assert_eq!(loaded.kind(), frame.kind());
assert_eq!(loaded.channel_names(), frame.channel_names());
assert_eq!(loaded.data(), frame.data());
std::fs::remove_file(&path).ok();
}
}