use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Read, Write};
use std::path::Path;
use nalgebra::Vector3;
use rigidity_core::PointCloud;
use crate::IoError;
#[derive(Clone)]
struct Field {
name: String,
kind: char,
size: usize,
count: usize,
}
struct Header {
fields: Vec<Field>,
points: usize,
binary: bool,
}
pub fn read_pcd(path: &Path) -> Result<PointCloud, IoError> {
let mut reader = BufReader::new(File::open(path)?);
let header = read_header(&mut reader)?;
let (x, y, z) = coordinates(&header)?;
let stride: usize = header
.fields
.iter()
.map(|field| field.size * field.count)
.sum();
let mut rows: Vec<[f64; 3]> = Vec::with_capacity(header.points);
if header.binary {
let mut row = vec![0u8; stride];
for _ in 0..header.points {
reader.read_exact(&mut row)?;
rows.push([
value(&row, &header.fields, x)?,
value(&row, &header.fields, y)?,
value(&row, &header.fields, z)?,
]);
}
} else {
for line in reader.lines() {
let line = line?;
let numbers: Vec<&str> = line.split_whitespace().collect();
if numbers.is_empty() {
continue;
}
let pick = |at: usize| -> Result<f64, IoError> {
numbers
.get(at)
.ok_or_else(|| IoError::BadPcd("a row is shorter than the header".into()))?
.parse()
.map_err(|_| IoError::BadNumber(numbers[at].to_owned()))
};
rows.push([pick(x)?, pick(y)?, pick(z)?]);
if rows.len() == header.points {
break;
}
}
}
if rows.is_empty() {
return Ok(PointCloud::new());
}
let mut min = Vector3::repeat(f64::INFINITY);
let mut max = Vector3::repeat(f64::NEG_INFINITY);
for row in &rows {
let point = Vector3::new(row[0], row[1], row[2]);
min = min.inf(&point);
max = max.sup(&point);
}
let mut cloud = PointCloud::with_origin((min + max) * 0.5);
for row in rows {
cloud.push(Vector3::new(row[0], row[1], row[2]));
}
Ok(cloud)
}
pub fn write_pcd(cloud: &PointCloud, path: &Path) -> Result<(), IoError> {
let mut out = BufWriter::new(File::create(path)?);
writeln!(out, "# .PCD v0.7 - Point Cloud Data file format")?;
writeln!(out, "VERSION 0.7")?;
writeln!(out, "FIELDS x y z")?;
writeln!(out, "SIZE 8 8 8")?;
writeln!(out, "TYPE F F F")?;
writeln!(out, "COUNT 1 1 1")?;
writeln!(out, "WIDTH {}", cloud.len())?;
writeln!(out, "HEIGHT 1")?;
writeln!(out, "VIEWPOINT 0 0 0 1 0 0 0")?;
writeln!(out, "POINTS {}", cloud.len())?;
writeln!(out, "DATA binary")?;
for index in 0..cloud.len() {
let point = cloud.point(index);
for value in [point.x, point.y, point.z] {
out.write_all(&value.to_le_bytes())?;
}
}
out.flush()?;
Ok(())
}
fn read_header<R: BufRead>(reader: &mut R) -> Result<Header, IoError> {
let mut fields: Vec<String> = Vec::new();
let mut sizes: Vec<usize> = Vec::new();
let mut kinds: Vec<char> = Vec::new();
let mut counts: Vec<usize> = Vec::new();
let mut points = None;
let mut width = None;
let mut height = None;
loop {
let mut line = String::new();
if reader.read_line(&mut line)? == 0 {
return Err(IoError::BadPcd("the header never ended".into()));
}
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (key, rest) = line.split_once(char::is_whitespace).unwrap_or((line, ""));
let words: Vec<&str> = rest.split_whitespace().collect();
let numbers = |words: &[&str]| -> Result<Vec<usize>, IoError> {
words
.iter()
.map(|word| {
word.parse()
.map_err(|_| IoError::BadNumber((*word).to_owned()))
})
.collect()
};
match key.to_ascii_uppercase().as_str() {
"FIELDS" => fields = words.iter().map(|word| (*word).to_owned()).collect(),
"SIZE" => sizes = numbers(&words)?,
"TYPE" => {
kinds = words
.iter()
.filter_map(|word| word.chars().next())
.collect()
}
"COUNT" => counts = numbers(&words)?,
"WIDTH" => width = numbers(&words)?.first().copied(),
"HEIGHT" => height = numbers(&words)?.first().copied(),
"POINTS" => points = numbers(&words)?.first().copied(),
"DATA" => {
let binary = match words.first().map(|word| word.to_ascii_lowercase()) {
Some(word) if word == "binary" => true,
Some(word) if word == "ascii" => false,
other => {
return Err(IoError::UnsupportedPcd(
other.unwrap_or_else(|| "nothing".into()),
));
}
};
let points = points
.or_else(|| Some(width? * height?))
.ok_or_else(|| IoError::BadPcd("the header gives no point count".into()))?;
if fields.len() != sizes.len() || fields.len() != kinds.len() {
return Err(IoError::BadPcd(
"FIELDS, SIZE and TYPE disagree about how many columns there are".into(),
));
}
let fields = fields
.iter()
.enumerate()
.map(|(index, name)| Field {
name: name.clone(),
kind: kinds[index],
size: sizes[index],
count: counts.get(index).copied().unwrap_or(1),
})
.collect();
return Ok(Header {
fields,
points,
binary,
});
}
_ => {}
}
}
}
fn coordinates(header: &Header) -> Result<(usize, usize, usize), IoError> {
let find = |wanted: &str| {
header
.fields
.iter()
.position(|field| field.name.eq_ignore_ascii_case(wanted))
};
match (find("x"), find("y"), find("z")) {
(Some(x), Some(y), Some(z)) => Ok((x, y, z)),
_ => Err(IoError::BadPcd("no x, y and z fields".into())),
}
}
fn value(row: &[u8], fields: &[Field], wanted: usize) -> Result<f64, IoError> {
let at: usize = fields[..wanted]
.iter()
.map(|field| field.size * field.count)
.sum();
let field = &fields[wanted];
let bytes = row
.get(at..at + field.size)
.ok_or_else(|| IoError::BadPcd("a row is shorter than the header".into()))?;
let signed = |bytes: &[u8]| -> i64 {
let mut wide = [0u8; 8];
wide[..bytes.len()].copy_from_slice(bytes);
let raw = u64::from_le_bytes(wide);
let shift = 64 - bytes.len() * 8;
((raw << shift) as i64) >> shift
};
Ok(match (field.kind.to_ascii_uppercase(), field.size) {
('F', 4) => f64::from(f32::from_le_bytes(bytes.try_into().unwrap())),
('F', 8) => f64::from_le_bytes(bytes.try_into().unwrap()),
('U', _) => {
let mut wide = [0u8; 8];
wide[..bytes.len()].copy_from_slice(bytes);
u64::from_le_bytes(wide) as f64
}
('I', _) => signed(bytes) as f64,
(kind, size) => {
return Err(IoError::UnsupportedPcd(format!("{kind}{}", size * 8)));
}
})
}