Skip to main content

spatialrust_io/ply/
reader.rs

1use std::io::BufRead;
2
3use spatialrust_core::{
4    DType, PointBuffer, PointBufferSet, PointCloud, PointSchema, SpatialMetadata,
5};
6
7use crate::error::{ply_format, ply_parse, IoError};
8use crate::ply::header::{PlyFormat, PlyHeader, PlyPropertyKind};
9use crate::ply::schema::schema_from_ply_properties;
10use crate::{PointReader, ReadOptions};
11
12/// Reads point clouds from PLY files or streams.
13pub struct PlyReader<R: BufRead> {
14    reader: R,
15    header: PlyHeader,
16    metadata: SpatialMetadata,
17    schema: PointSchema,
18    loaded: bool,
19}
20
21impl<R: BufRead> PlyReader<R> {
22    /// Creates a reader and parses the PLY header eagerly.
23    pub fn new(mut reader: R) -> Result<Self, IoError> {
24        let header = PlyHeader::parse(&mut reader)?;
25        let schema = schema_from_ply_properties(&header.properties)?;
26        let metadata = metadata_from_header(&header);
27        Ok(Self { reader, header, metadata, schema, loaded: false })
28    }
29
30    /// Returns the parsed PLY header.
31    #[must_use]
32    pub fn header(&self) -> &PlyHeader {
33        &self.header
34    }
35
36    /// Reads the point cloud payload.
37    pub fn read_cloud(&mut self) -> Result<PointCloud, IoError> {
38        if self.loaded {
39            return Err(ply_format("PLY reader already consumed"));
40        }
41        self.loaded = true;
42        read_ply_body(&self.header, &mut self.reader, self.schema.clone(), self.metadata.clone())
43    }
44}
45
46impl<R: BufRead> PointReader for PlyReader<R> {
47    fn schema(&self) -> spatialrust_core::SpatialResult<PointSchema> {
48        Ok(self.schema.clone())
49    }
50
51    fn metadata(&self) -> spatialrust_core::SpatialResult<SpatialMetadata> {
52        Ok(self.metadata.clone())
53    }
54
55    fn read(&mut self, _options: &ReadOptions) -> spatialrust_core::SpatialResult<PointCloud> {
56        self.read_cloud().map_err(|error| spatialrust_core::SpatialError::Io(error.to_string()))
57    }
58}
59
60/// Reads a complete PLY file from any buffered reader.
61pub fn read_ply<R: BufRead>(reader: &mut R) -> Result<PointCloud, IoError> {
62    let header = PlyHeader::parse(reader)?;
63    let schema = schema_from_ply_properties(&header.properties)?;
64    let metadata = metadata_from_header(&header);
65    read_ply_body(&header, reader, schema, metadata)
66}
67
68fn read_ply_body<R: BufRead>(
69    header: &PlyHeader,
70    reader: &mut R,
71    schema: PointSchema,
72    metadata: SpatialMetadata,
73) -> Result<PointCloud, IoError> {
74    let mut buffers = PointBufferSet::new();
75    for field in schema.fields() {
76        buffers.insert(
77            field.name.clone(),
78            PointBuffer::with_capacity(field.dtype, header.vertex_count),
79        );
80    }
81
82    match header.format {
83        PlyFormat::Ascii => read_ascii_vertices(reader, header, &schema, &mut buffers)?,
84        PlyFormat::BinaryLittleEndian => {
85            read_binary_vertices(reader, header, &schema, &mut buffers)?
86        }
87    }
88
89    PointCloud::try_from_parts(schema, buffers, metadata).map_err(IoError::from)
90}
91
92fn metadata_from_header(_header: &PlyHeader) -> SpatialMetadata {
93    SpatialMetadata {
94        frame_id: spatialrust_core::FrameId::new("ply"),
95        timestamp: spatialrust_core::Timestamp::from_nanos(0),
96        sensor_origin: None,
97        unit: "meter".to_owned(),
98    }
99}
100
101fn read_ascii_vertices<R: BufRead>(
102    reader: &mut R,
103    header: &PlyHeader,
104    schema: &PointSchema,
105    buffers: &mut PointBufferSet,
106) -> Result<(), IoError> {
107    for vertex_index in 0..header.vertex_count {
108        let line = read_ascii_vertex_line(reader, vertex_index, header.vertex_count)?;
109        let mut tokens = line.split_whitespace();
110        for property in &header.properties {
111            let token = tokens.next().ok_or_else(|| {
112                ply_parse(format!(
113                    "missing token for property `{}` on vertex {vertex_index}",
114                    property.name
115                ))
116            })?;
117            let value = token
118                .parse::<f64>()
119                .map_err(|_| ply_parse(format!("invalid ASCII value `{token}`")))?;
120            push_to_field(buffers, schema, &property.name, value as f32)?;
121        }
122    }
123    Ok(())
124}
125
126fn read_ascii_vertex_line<R: BufRead>(
127    reader: &mut R,
128    vertex_index: usize,
129    vertex_count: usize,
130) -> Result<String, IoError> {
131    loop {
132        let mut line = String::new();
133        let read = reader.read_line(&mut line)?;
134        if read == 0 {
135            return Err(ply_parse(format!(
136                "unexpected EOF after {vertex_index} of {vertex_count} ASCII vertices"
137            )));
138        }
139        let trimmed = line.trim();
140        if trimmed.is_empty() || trimmed.starts_with('#') {
141            continue;
142        }
143        return Ok(trimmed.to_owned());
144    }
145}
146
147fn read_binary_vertices<R: BufRead>(
148    reader: &mut R,
149    header: &PlyHeader,
150    schema: &PointSchema,
151    buffers: &mut PointBufferSet,
152) -> Result<(), IoError> {
153    let mut payload = vec![0_u8; header.vertex_stride() * header.vertex_count];
154    std::io::Read::read_exact(&mut *reader, &mut payload).map_err(IoError::from)?;
155
156    for vertex_index in 0..header.vertex_count {
157        let start = vertex_index * header.vertex_stride();
158        let mut offset = 0usize;
159        for property in &header.properties {
160            let size = property.kind.size_bytes();
161            let chunk = &payload[start + offset..start + offset + size];
162            offset += size;
163            let value = read_binary_scalar(property.kind, chunk)?;
164            push_to_field(buffers, schema, &property.name, value)?;
165        }
166    }
167    Ok(())
168}
169
170fn read_binary_scalar(kind: PlyPropertyKind, chunk: &[u8]) -> Result<f32, IoError> {
171    let value = match kind {
172        PlyPropertyKind::Char => i8::from_le_bytes(chunk.try_into().expect("char")) as f32,
173        PlyPropertyKind::UChar => f32::from(chunk[0]),
174        PlyPropertyKind::Short => i16::from_le_bytes(chunk.try_into().expect("short")) as f32,
175        PlyPropertyKind::UShort => f32::from(u16::from_le_bytes(chunk.try_into().expect("ushort"))),
176        PlyPropertyKind::Int => i32::from_le_bytes(chunk.try_into().expect("int")) as f32,
177        PlyPropertyKind::UInt => u32::from_le_bytes(chunk.try_into().expect("uint")) as f32,
178        PlyPropertyKind::Float => f32::from_le_bytes(chunk.try_into().expect("float")),
179        PlyPropertyKind::Double => f64::from_le_bytes(chunk.try_into().expect("double")) as f32,
180    };
181    Ok(value)
182}
183
184fn push_to_field(
185    buffers: &mut PointBufferSet,
186    schema: &PointSchema,
187    name: &str,
188    value: f32,
189) -> Result<(), IoError> {
190    let field = schema
191        .fields()
192        .iter()
193        .find(|field| field.name == name)
194        .ok_or_else(|| ply_format(format!("schema missing mapped field `{name}`")))?;
195
196    let buffer = buffers
197        .get_mut(name)
198        .ok_or_else(|| ply_format(format!("buffer missing for field `{name}`")))?;
199
200    match field.dtype {
201        DType::F32 | DType::F16 => buffer.push_f32(value).map_err(IoError::from),
202        DType::F64 => buffer.push_f64(f64::from(value)).map_err(IoError::from),
203        DType::U8 => buffer.push_u8(value.round() as u8).map_err(IoError::from),
204        DType::U16 => buffer.push_u16(value.round() as u16).map_err(IoError::from),
205        DType::I32 => buffer.push_i32(value.round() as i32).map_err(IoError::from),
206        DType::U32 => {
207            let PointBuffer::U32(values) = buffer else {
208                return Err(IoError::Core(spatialrust_core::SpatialError::UnsupportedDType(
209                    field.dtype,
210                )));
211            };
212            values.push(value.round() as u32);
213            Ok(())
214        }
215    }
216}
217
218/// Reads a PLY file from disk.
219pub fn read_ply_file(path: impl AsRef<std::path::Path>) -> Result<PointCloud, IoError> {
220    let file = std::fs::File::open(path.as_ref())?;
221    let mut reader = std::io::BufReader::new(file);
222    read_ply(&mut reader)
223}
224
225#[cfg(test)]
226mod tests {
227    use super::read_ply;
228    use crate::ply::writer::{write_ply, PlyWriteFormat};
229    use spatialrust_core::{HasIntensity, HasPositions3, PointCloudBuilder, StandardSchemas};
230    use std::io::Cursor;
231
232    const SAMPLE_XYZ_ASCII: &str = "\
233ply
234format ascii 1.0
235element vertex 3
236property float x
237property float y
238property float z
239end_header
2400.0 0.0 0.0
2411.0 0.0 0.0
2420.0 1.0 0.0
243";
244
245    const SAMPLE_XYZI_ASCII: &str = "\
246ply
247format ascii 1.0
248element vertex 2
249property float x
250property float y
251property float z
252property float intensity
253end_header
2540.0 0.0 0.0 0.5
2551.0 0.0 0.0 0.8
256";
257
258    #[test]
259    fn reads_ascii_xyz() {
260        let mut reader = Cursor::new(SAMPLE_XYZ_ASCII.as_bytes());
261        let cloud = read_ply(&mut reader).unwrap();
262        assert_eq!(cloud.len(), 3);
263        let (x, y, z) = cloud.positions3().unwrap();
264        assert_eq!(x, &[0.0, 1.0, 0.0]);
265        assert_eq!(y, &[0.0, 0.0, 1.0]);
266        assert_eq!(z, &[0.0, 0.0, 0.0]);
267    }
268
269    #[test]
270    fn reads_ascii_xyzi() {
271        let mut reader = Cursor::new(SAMPLE_XYZI_ASCII.as_bytes());
272        let cloud = read_ply(&mut reader).unwrap();
273        assert_eq!(cloud.intensity().unwrap(), &[0.5, 0.8]);
274    }
275
276    #[test]
277    fn roundtrip_ascii_xyz() {
278        let mut builder = PointCloudBuilder::new(StandardSchemas::point_xyz());
279        builder.push_point([0.0, 0.0, 0.0]).unwrap();
280        builder.push_point([1.0, 2.0, 3.0]).unwrap();
281        let cloud = builder.build().unwrap();
282
283        let mut buffer = Vec::new();
284        write_ply(&mut buffer, &cloud, PlyWriteFormat::Ascii).unwrap();
285
286        let mut reader = Cursor::new(buffer);
287        let loaded = read_ply(&mut reader).unwrap();
288        assert_eq!(loaded.len(), cloud.len());
289        let (x, y, z) = loaded.positions3().unwrap();
290        assert_eq!(x, cloud.field("x").unwrap().as_f32().unwrap());
291        assert_eq!(y, cloud.field("y").unwrap().as_f32().unwrap());
292        assert_eq!(z, cloud.field("z").unwrap().as_f32().unwrap());
293    }
294
295    #[test]
296    fn roundtrip_binary_xyz() {
297        let mut builder = PointCloudBuilder::new(StandardSchemas::point_xyz());
298        builder.push_point([0.5, 1.5, 2.5]).unwrap();
299        let cloud = builder.build().unwrap();
300
301        let mut buffer = Vec::new();
302        write_ply(&mut buffer, &cloud, PlyWriteFormat::BinaryLittleEndian).unwrap();
303
304        let mut reader = Cursor::new(buffer);
305        let loaded = read_ply(&mut reader).unwrap();
306        let (x, _, _) = loaded.positions3().unwrap();
307        assert!((x[0] - 0.5).abs() < 1e-6);
308    }
309}