Commit 4c723c5a authored by Radmir Gesler's avatar Radmir Gesler
Browse files

The first look to a point cloud (PC) readers for different PC formats,...

The first look to a point cloud (PC) readers for different PC formats, including a junit test for all parser components and test data.
parent d3ca6500
Pipeline #12358 passed with stage
in 2 minutes and 12 seconds
......@@ -84,6 +84,12 @@
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>com.github.mreutegg</groupId>
<artifactId>laszip4j</artifactId>
<version>0.20</version>
</dependency>
</dependencies>
<build>
<resources>
......
......@@ -66,6 +66,8 @@ public class CityDoctorModel {
private CityGMLVersion cityGMLVersion;
private final CityObjectCache cache;
// private PointCloud pointCloud;
public CityDoctorModel(ParserConfiguration config, File file, CityObjectCache cache) {
if (config == null) {
throw new IllegalArgumentException("Parser configuration may not be null");
......
/*
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CityDoctor2 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.datastructure;
import java.util.Arrays;
/**
* Represents a collection of points in a three-dimensional space, with optional attributes
* such as color, intensity, and classification. Each point is defined by its X, Y, and Z coordinates.
*
* This class is immutable and ensures that all points are stored in separate arrays for each
* attribute. It also supports basic validation and slicing functionality.
*
* @author Radmir Gesler
*/
public final class PointCloud {
public final float[] x;
public final float[] y;
public final float[] z;
// Optional attributes if existing
public final int[] rgb; // packed 0xRRGGBB
public final short[] intensity; // LAS intensity
public final byte[] classification; // LAS classification
public final int size;
public PointCloud(float[] x, float[] y, float[] z,
int[] rgb, short[] intensity, byte[] classification,
int size) {
this.x = x;
this.y = y;
this.z = z;
this.rgb = rgb;
this.intensity = intensity;
this.classification = classification;
this.size = size;
}
/**
* Performs a sanity check on the internal state of the PointCloud to ensure data consistency.
* This method validates the integrity of the core arrays (x, y, z) and optional attributes
* (rgb, intensity, classification) based on the expected size of the PointCloud.
*
* Throws an exception if one of the following conditions is detected:
* - The x, y, or z arrays are null.
* - The length of the x, y, or z arrays is smaller than the specified size of the PointCloud.
* - If the rgb array is not null, its length is checked to ensure it matches or exceeds the size.
* - If the intensity array is not null, its length is checked to ensure it matches or exceeds the size.
* - If the classification array is not null, its length is checked to ensure it matches or exceeds the size.
*
* @throws IllegalStateException if any of the above conditions are violated.
*/
public boolean sanityCheck() {
if (x == null || y == null || z == null)
throw new IllegalStateException("XYZ missing");
if (x.length < size || y.length < size || z.length < size)
throw new IllegalStateException("Array too small");
if (rgb != null && rgb.length < size)
throw new IllegalStateException("RGB array too small");
if (intensity != null && intensity.length < size)
throw new IllegalStateException("Intensity array too small");
if (classification != null && classification.length < size)
throw new IllegalStateException("Classification array too small");
return true;
}
/**
* Extracts a subset of points from the current PointCloud instance based on the specified range.
*
* The method creates a new PointCloud containing only the points within the specified
* range [fromInclusive, toExclusive). If any of the optional attributes (rgb, intensity,
* classification) exist, their values are also subset accordingly. If the range is invalid
* (e.g., toExclusive is less than or equal to fromInclusive), an empty PointCloud will be returned.
*
* @param fromInclusive the starting index (inclusive) of the range of points to include in the slice
* @param toExclusive the ending index (exclusive) of the range of points to include in the slice
* @return a new PointCloud object containing the subset of points within the specified range
*/
public PointCloud slice(int fromInclusive, int toExclusive) {
int n = Math.max(0, toExclusive - fromInclusive);
float[] nx = Arrays.copyOfRange(x, fromInclusive, toExclusive);
float[] ny = Arrays.copyOfRange(y, fromInclusive, toExclusive);
float[] nz = Arrays.copyOfRange(z, fromInclusive, toExclusive);
int[] nrgb = rgb == null ? null : Arrays.copyOfRange(rgb, fromInclusive, toExclusive);
short[] nint = intensity == null ? null : Arrays.copyOfRange(intensity, fromInclusive, toExclusive);
byte[] ncls = classification == null ? null : Arrays.copyOfRange(classification, fromInclusive, toExclusive);
return new PointCloud(nx, ny, nz, nrgb, nint, ncls, n);
}
}
\ No newline at end of file
/*-
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CityDoctor2 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
import java.util.Arrays;
/**
* A dynamic byte buffer that can grow in size as needed.
* This class provides functionality to add bytes to the buffer
* and retrieve its contents or size. It ensures that the internal
* storage grows dynamically to accommodate new data.
*
* @author Radmir Gesler
*/
public final class ByteGrowableBuffer {
private byte[] data;
private int size;
public ByteGrowableBuffer() {
this(1024);
}
public ByteGrowableBuffer(int initialCapacity) {
this.data = new byte[Math.max(16, initialCapacity)];
this.size = 0;
}
public void add(byte value) {
ensureCapacity(size + 1);
data[size++] = value;
}
public int size() {
return size;
}
public byte[] toArray() {
return Arrays.copyOf(data, size);
}
private void ensureCapacity(int minCapacity) {
if (minCapacity <= data.length) {
return;
}
int newCapacity = data.length + Math.max(1024, data.length / 2);
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
}
data = Arrays.copyOf(data, newCapacity);
}
}
\ No newline at end of file
/*-
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CityDoctor2 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
import java.util.Arrays;
/**
* A growable buffer for float values that dynamically increases its capacity
* as elements are added. This class provides efficient memory management by
* resizing the internal storage when necessary.
*
* @author Radmir Gesler
*/
public final class FloatGrowableBuffer {
private float[] data;
private int size;
public FloatGrowableBuffer() {
this(1024);
}
public FloatGrowableBuffer(int initialCapacity) {
this.data = new float[Math.max(16, initialCapacity)];
this.size = 0;
}
public void add(float value) {
ensureCapacity(size + 1);
data[size++] = value;
}
public int size() {
return size;
}
public float[] toArray() {
return Arrays.copyOf(data, size);
}
private void ensureCapacity(int minCapacity) {
if (minCapacity <= data.length) {
return;
}
int newCapacity = data.length + Math.max(1024, data.length / 2);
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
}
data = Arrays.copyOf(data, newCapacity);
}
}
\ No newline at end of file
/*-
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CityDoctor2 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
import com.github.mreutegg.laszip4j.LASPoint;
import com.github.mreutegg.laszip4j.LASReader;
import de.hft.stuttgart.citydoctor2.datastructure.PointCloud;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays;
/**
* The {@code LasReader} class provides static methods for reading LAS/LAZ files
* into point cloud data. The class contains utilities for handling LAS file
* headers, choosing point counts, and reading data efficiently into memory.
*
* The implementation is tolerant to LAS version differences:
* - LAS 1.4+: prefers the extended point count
* - LAS <= 1.3: prefers the legacy point count
* - falls back to dynamic reading if header counts are unusable
*
* This class is immutable and cannot be instantiated.
*
* @author Radmir Gesler
*/
public final class LasReader {
private LasReader() {}
public static PointCloud read(Path file) throws IOException {
LASReader reader = new LASReader(new File(file.toString()));
try {
HeaderInfo h = readHeaderInfo(reader);
int expectedCount = chooseExpectedPointCount(h);
if (expectedCount > 0) {
return readPreallocated(reader, expectedCount);
} else {
return readDynamic(reader);
}
} catch (Exception e) {
throw (e instanceof IOException)
? (IOException) e
: new IOException("Failed to read LAS/LAZ: " + file, e);
}
}
/**
* Reads and constructs a {@link HeaderInfo} object using the header information
* from the given {@link LASReader}.
*
* @param reader the LASReader instance from which the header information is extracted
* @return a HeaderInfo object containing the version details, point counts, and
* legacy point count by return extracted from the LAS file header
*/
private static HeaderInfo readHeaderInfo(LASReader reader) {
var header = reader.getHeader();
int versionMajor = header.getVersionMajor();
int versionMinor = header.getVersionMinor();
long legacyPointCount = safeLong(header::getLegacyNumberOfPointRecords);
long pointCount = safeLong(header::getNumberOfPointRecords);
int[] legacyByReturn = safeIntArray(header::getLegacyNumberOfPointsByReturn);
return new HeaderInfo(
versionMajor,
versionMinor,
legacyPointCount,
pointCount,
legacyByReturn
);
}
/**
* Determines the expected number of points in a point cloud based on the header information.
* This method accounts for version-specific differences and validates that the size
* does not exceed the maximum integer value, which is required for in-memory operations.
*
* @param h the {@link HeaderInfo} instance containing version details and point counts
* extracted from a LAS/LAZ file header.
* @return the expected number of points in the point cloud as an integer.
* @throws IOException if the number of points is too large to be processed in memory.
*/
private static int chooseExpectedPointCount(HeaderInfo h) throws IOException {
long n;
if (h.versionMajor > 1 || (h.versionMajor == 1 && h.versionMinor >= 4)) {
n = h.pointCount > 0 ? h.pointCount : h.legacyPointCount;
} else {
n = h.legacyPointCount > 0 ? h.legacyPointCount : h.pointCount;
}
if (n <= 0) {
return 0;
}
if (n > Integer.MAX_VALUE) {
throw new IOException("Point cloud too large for in-memory loading: " + n);
}
return (int) n;
}
/**
* Reads a LAS point cloud using pre-allocated arrays for efficiency, based on an expected number of points.
* The method dynamically adjusts array sizes if the actual number of points exceeds the initial expectation.
* If the header underestimates the point count, the arrays are grown defensively.
*
* @param reader the LASReader instance used to read points from the LAS/LAZ file
* @param expectedCount the initial estimated number of points in the LAS/LAZ file
* @return a PointCloud object containing all the points read from the LAS/LAZ file, including
* their coordinates, intensity, and classification
*/
private static PointCloud readPreallocated(LASReader reader, int expectedCount) {
float[] x = new float[expectedCount];
float[] y = new float[expectedCount];
float[] z = new float[expectedCount];
short[] intensity = new short[expectedCount];
byte[] classification = new byte[expectedCount];
int i = 0;
for (LASPoint p : reader.getPoints()) {
if (i >= x.length) {
int newSize = x.length + Math.max(1024, x.length / 2);
x = Arrays.copyOf(x, newSize);
y = Arrays.copyOf(y, newSize);
z = Arrays.copyOf(z, newSize);
intensity = Arrays.copyOf(intensity, newSize);
classification = Arrays.copyOf(classification, newSize);
}
x[i] = (float) p.getX();
y[i] = (float) p.getY();
z[i] = (float) p.getZ();
intensity[i] = (short) p.getIntensity();
classification[i] = (byte) p.getClassification();
i++;
}
if (i != x.length) {
x = Arrays.copyOf(x, i);
y = Arrays.copyOf(y, i);
z = Arrays.copyOf(z, i);
intensity = Arrays.copyOf(intensity, i);
classification = Arrays.copyOf(classification, i);
}
return new PointCloud(x, y, z, null, intensity, classification, i);
}
/**
* Reads and constructs a {@link PointCloud} object dynamically by processing all the points
* in the provided {@link LASReader}. This method allocates and grows buffers as needed
* to store point data, such as coordinates, intensity, and classification, while processing
* the LAS file contents.
*
* @param reader the {@link LASReader} instance used to read points from the LAS/LAZ file
* @return a {@link PointCloud} object containing the points read from the LAS/LAZ file,
* including their X, Y, Z coordinates, intensity, and classification
*/
private static PointCloud readDynamic(LASReader reader) {
FloatGrowableBuffer xs = new FloatGrowableBuffer(16_384);
FloatGrowableBuffer ys = new FloatGrowableBuffer(16_384);
FloatGrowableBuffer zs = new FloatGrowableBuffer(16_384);
ShortGrowableBuffer intensities = new ShortGrowableBuffer(16_384);
ByteGrowableBuffer classifications = new ByteGrowableBuffer(16_384);
for (LASPoint p : reader.getPoints()) {
xs.add((float) p.getX());
ys.add((float) p.getY());
zs.add((float) p.getZ());
intensities.add((short) p.getIntensity());
classifications.add((byte) p.getClassification());
}
return new PointCloud(
xs.toArray(),
ys.toArray(),
zs.toArray(),
null,
intensities.toArray(),
classifications.toArray(),
xs.size()
);
}
/**
* Safely retrieves a long value from the given {@link LongSupplierEx} instance, handling any exceptions
* that may occur by returning a default value of 0L.
*
* @param s the {@link LongSupplierEx} instance that supplies a long value, potentially throwing an exception
* @return the long value provided by the {@link LongSupplierEx}, or 0L if an exception occurs
*/
private static long safeLong(LongSupplierEx s) {
try {
return s.getAsLong();
} catch (Throwable t) {
return 0L;
}
}
/**
* Safely retrieves an integer array from the given {@link IntArraySupplierEx} instance,
* handling any exceptions or null values by returning an empty array.
*
* @param s the {@link IntArraySupplierEx} instance that supplies an integer array, potentially throwing an exception
* @return the integer array provided by the {@link IntArraySupplierEx}, or an empty array if an exception occurs or the supplied array is null
*/
private static int[] safeIntArray(IntArraySupplierEx s) {
try {
int[] arr = s.get();
return arr != null ? arr : new int[0];
} catch (Throwable t) {
return new int[0];
}
}
/**
* Represents a functional interface that supplies a long value and allows for checked exceptions.
* This is similar to {@link java.util.function.LongSupplier}, but it supports operations
* that may throw a checked exception.
*
* Implementations of this interface are primarily used in cases where
* a long value needs to be retrieved, but the operation may encounter an exception.
*
* Functional Interface:
* This interface is a functional interface, meaning it can be used
* as the assignment target for a lambda expression or method reference.
*/
@FunctionalInterface
private interface LongSupplierEx {
long getAsLong() throws Exception;
}
/**
* Represents a functional interface that supplies an integer array and may throw an exception.
* This interface is typically used in contexts where an integer array needs to be provided,
* but the operation might fail and throw a checked exception.
*
* It is intended to be utilized with methods that safely handle potentially
* exceptional cases when retrieving integer arrays.
*
* Functional method:
* - {@code int[] get()} : Supplies an integer array and allows exception handling.
*/
@FunctionalInterface
private interface IntArraySupplierEx {
int[] get() throws Exception;
}
/**
* Represents header information extracted from a LAS/LAZ file.
* This record encapsulates version details, point counts, and
* legacy-specific data extracted from the file header.
*
* Fields:
* - versionMajor: The major version number of the LAS/LAZ file format.
* - versionMinor: The minor version number of the LAS/LAZ file format.
* - legacyPointCount: The number of points as indicated by legacy headers, which may not account for version-specific enhancements.
* - pointCount: The total number of points in the LAS/LAZ file, accounting for format version differences.
* - legacyByReturn: An array specifying the legacy point count distribution across different return classifications.
*/
private record HeaderInfo(
int versionMajor,
int versionMinor,
long legacyPointCount,
long pointCount,
int[] legacyByReturn
) {
}
}
\ No newline at end of file
/*-
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CityDoctor2 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
import de.hft.stuttgart.citydoctor2.datastructure.PointCloud;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
/**
* A utility class for reading PLY (Polygon File Format) files and converting them into
* a {@link PointCloud} object. Supports PLY files with both ASCII and binary
* little-endian formats.
*
* The class provides methods to process the PLY header, parse vertex data,
* and handle optional RGB color attributes. It ensures correctness by
* re-opening files where necessary to overcome buffering limitations.
*
* The PLY format is often used to represent 3D data such as point clouds,
* which contain geometric and attribute information for each point in space.
*
* This class is not instantiable; all methods are static.
*
* @author Radmir Gesler
*/
public final class PlyReader {
private PlyReader() {
}
private enum PlyFormat {
ASCII,
BINARY_LITTLE_ENDIAN
}
static PointCloud read(Path file) throws IOException {
Header header = readHeader(file);
if (header.vertexCount <= 0) {
throw new IOException("PLY has no vertex element or vertex count is 0: " + file);
}
if (!header.hasProperty("x") || !header.hasProperty("y") || !header.hasProperty("z")) {
throw new IOException("PLY vertices must contain x, y, z properties: " + file);
}
float[] x = new float[header.vertexCount];
float[] y = new float[header.vertexCount];
float[] z = new float[header.vertexCount];
boolean hasRgb = header.hasProperty("red")
&& header.hasProperty("green")
&& header.hasProperty("blue");
int[] rgb = hasRgb ? new int[header.vertexCount] : null;
try (InputStream is = new BufferedInputStream(new FileInputStream(file.toFile()))) {
skipHeaderBytes(is);
if (header.format == PlyFormat.ASCII) {
readAsciiVertices(is, header, x, y, z, rgb);
} else {
readBinaryLittleEndianVertices(is, header, x, y, z, rgb);
}
}
return new PointCloud(x, y, z, rgb, null, null, header.vertexCount);
}
/**
* Reads and parses the header of a PLY file from the specified file path. The header contains
* metadata about the PLY format, such as the file format, vertex count, and properties.
*
* @param file The path to the PLY file whose header is to be read.
* @return A {@code Header} object containing information about the PLY format, vertex count,
* and vertex properties.
* @throws IOException If an I/O error occurs, or the header format is invalid/missing required metadata.
*/
private static Header readHeader(Path file) throws IOException {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream(file.toFile()), StandardCharsets.US_ASCII))) {
String line = br.readLine();
if (line == null || !line.trim().equals("ply")) {
throw new IOException("Not a PLY file: missing 'ply' header");
}
PlyFormat format = null;
int vertexCount = -1;
boolean inVertexElement = false;
List<Property> vertexProperties = new ArrayList<>();
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("comment")) {
continue;
}
if (line.startsWith("format")) {
if (line.contains("ascii")) {
format = PlyFormat.ASCII;
} else if (line.contains("binary_little_endian")) {
format = PlyFormat.BINARY_LITTLE_ENDIAN;
} else {
throw new IOException("Unsupported PLY format: " + line);
}
} else if (line.startsWith("element")) {
String[] parts = line.split("\\s+");
if (parts.length >= 3 && parts[1].equals("vertex")) {
vertexCount = Integer.parseInt(parts[2]);
inVertexElement = true;
} else {
inVertexElement = false;
}
} else if (line.startsWith("property") && inVertexElement) {
String[] parts = line.split("\\s+");
if (parts.length >= 3) {
PropertyType type = PropertyType.fromPlyName(parts[1]);
String name = parts[2];
vertexProperties.add(new Property(type, name));
}
} else if (line.equals("end_header")) {
break;
}
}
if (format == null) {
throw new IOException("PLY header missing format");
}
if (vertexCount < 0) {
throw new IOException("PLY header missing vertex element");
}
if (vertexProperties.isEmpty()) {
throw new IOException("PLY vertex has no properties");
}
return new Header(format, vertexCount, vertexProperties.toArray(new Property[0]));
}
}
/**
* Reads ASCII vertex data from the provided input stream and populates the given arrays
* with vertex positions and optionally RGB color values if the RGB array is supplied.
*
* @param is The input stream positioned at the start of vertex data in an ASCII PLY file.
* @param header The PLY header containing metadata such as vertex count and property definitions.
* @param x An array to store the x-coordinates of the vertices.
* @param y An array to store the y-coordinates of the vertices.
* @param z An array to store the z-coordinates of the vertices.
* @param rgb An optional array to store the RGB color values of the vertices, where each value
* is a packed integer in the form 0xRRGGBB. Pass null if no color information is needed.
* @throws IOException If an I/O error occurs, the end of the file is reached unexpectedly, or the format of
* the vertex data does not match the expectations defined in the header.
*/
private static void readAsciiVertices(InputStream is, Header header,
float[] x, float[] y, float[] z, int[] rgb) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.US_ASCII));
for (int i = 0; i < header.vertexCount; i++) {
String line = br.readLine();
while (line != null && line.trim().isEmpty()) {
line = br.readLine();
}
if (line == null) {
throw new EOFException("Unexpected EOF while reading ASCII PLY vertices");
}
String[] parts = line.trim().split("\\s+");
if (parts.length < header.vertexProperties.length) {
throw new IOException("PLY vertex line has too few fields at index " + i);
}
float px = 0f, py = 0f, pz = 0f;
int r = 0, g = 0, b = 0;
for (int p = 0; p < header.vertexProperties.length; p++) {
String prop = header.vertexProperties[p].name;
String val = parts[p];
if (prop.equals("x")) {
px = Float.parseFloat(val);
} else if (prop.equals("y")) {
py = Float.parseFloat(val);
} else if (prop.equals("z")) {
pz = Float.parseFloat(val);
} else if (rgb != null) {
if (prop.equals("red")) {
r = clamp255(parseIntSafe(val));
} else if (prop.equals("green")) {
g = clamp255(parseIntSafe(val));
} else if (prop.equals("blue")) {
b = clamp255(parseIntSafe(val));
}
}
}
x[i] = px;
y[i] = py;
z[i] = pz;
if (rgb != null) {
rgb[i] = (r << 16) | (g << 8) | b;
}
}
}
/**
* Reads binary vertex data in little-endian format from the specified input stream
* and populates the given arrays with vertex positions and optionally RGB color values
* if the RGB array is supplied.
*
* @param is The input stream positioned at the start of vertex data in a binary
* little-endian PLY file.
* @param header The PLY header containing metadata such as vertex count and property definitions.
* @param x An array to store the x-coordinates of the vertices. The array must
* have a size equal to the vertex count specified in the header.
* @param y An array to store the y-coordinates of the vertices. The array must
* have a size equal to the vertex count specified in the header.
* @param z An array to store the z-coordinates of the vertices. The array must
* have a size equal to the vertex count specified in the header.
* @param rgb An optional array to store the RGB color values of the vertices, where each
* value is a packed integer in the form 0xRRGGBB. The array must have a size
* equal to the vertex count in the header. Pass null if no color information
* is needed.
* @throws IOException If an I/O error occurs, the end of the file is reached unexpectedly,
* or the format of the vertex data does not match the expectations
* defined in the header.
*/
private static void readBinaryLittleEndianVertices(InputStream is, Header header,
float[] x, float[] y, float[] z, int[] rgb) throws IOException {
DataInputStream din = new DataInputStream(is);
for (int i = 0; i < header.vertexCount; i++) {
float px = 0f, py = 0f, pz = 0f;
int r = 0, g = 0, b = 0;
for (Property prop : header.vertexProperties) {
switch (prop.type) {
case FLOAT -> {
float v = readFloatLE(din);
if (prop.name.equals("x")) px = v;
else if (prop.name.equals("y")) py = v;
else if (prop.name.equals("z")) pz = v;
}
case DOUBLE -> {
double dv = readDoubleLE(din);
float v = (float) dv;
if (prop.name.equals("x")) px = v;
else if (prop.name.equals("y")) py = v;
else if (prop.name.equals("z")) pz = v;
}
case UCHAR, CHAR -> {
int v = din.readUnsignedByte();
if (rgb != null) {
if (prop.name.equals("red")) r = v;
else if (prop.name.equals("green")) g = v;
else if (prop.name.equals("blue")) b = v;
}
}
case USHORT, SHORT -> {
readUShortLE(din); // currently ignored
}
case UINT, INT -> {
readIntLE(din); // currently ignored
}
default -> throw new IOException("Unsupported binary PLY property type: " + prop.type);
}
}
x[i] = px;
y[i] = py;
z[i] = pz;
if (rgb != null) {
rgb[i] = (r << 16) | (g << 8) | b;
}
}
}
/**
* Skips the PLY header bytes from the given input stream until the "end_header" marker
* is encountered. This method ensures that the stream's position is advanced beyond
* the header section of a PLY file, which typically contains descriptive metadata.
*
* @param is The input stream containing the PLY file data. The stream should be positioned
* at the start of the header. This method will read and skip bytes until the
* header terminator "end_header" is found or an error occurs.
* @throws IOException If an I/O error occurs while reading the stream, or if the header
* does not terminate with "end_header".
*/
private static void skipHeaderBytes(InputStream is) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream(256);
int b;
while ((b = is.read()) != -1) {
buffer.write(b);
String s = buffer.toString(StandardCharsets.US_ASCII);
if (s.contains("end_header")) {
while (b != -1 && b != '\n') {
b = is.read();
}
return;
}
if (buffer.size() > 4096) {
byte[] arr = buffer.toByteArray();
buffer.reset();
int keep = Math.min(1024, arr.length);
buffer.write(arr, arr.length - keep, keep);
}
}
throw new EOFException("PLY header not terminated by end_header");
}
/**
* Safely parses a string into an integer, handling cases where the string represents a floating-point number
* by converting it to the nearest integer value.
*
* @param s The string to be parsed into an integer. If the string contains a decimal point, it will be parsed
* as a floating-point number and then cast to an integer.
* @return The integer representation of the input string. If the string contains a decimal point, the result
* is the integer part obtained by casting the parsed floating-point number.
* @throws NumberFormatException If the string is not a valid representation of a number.
*/
private static int parseIntSafe(String s) {
if (s.indexOf('.') >= 0) {
return (int) Float.parseFloat(s);
}
return Integer.parseInt(s);
}
/**
* Clamps the given integer value to the range [0, 255].
* If the input value is less than 0, it returns 0. If the input value is greater than 255, it returns 255.
* Otherwise, it returns the input value unchanged.
*
* @param v The integer value to be clamped.
* @return The clamped value, constrained within the range [0, 255].
*/
private static int clamp255(int v) {
return Math.max(0, Math.min(255, v));
}
/**
* Reads a 32-bit integer in little-endian byte order from the provided DataInputStream.
* This method assumes that the next 4 bytes in the stream represent an integer value
* in little-endian format (the least significant byte first).
*
* @param din The DataInputStream from which the integer is to be read. The stream must
* have at least 4 bytes available to be read.
* @return The 32-bit integer value read from the stream in little-endian byte order.
* @throws IOException If an I/O error occurs during reading, such as if the stream does
* not contain enough bytes.
*/
private static int readIntLE(DataInputStream din) throws IOException {
int b1 = din.readUnsignedByte();
int b2 = din.readUnsignedByte();
int b3 = din.readUnsignedByte();
int b4 = din.readUnsignedByte();
return (b4 << 24) | (b3 << 16) | (b2 << 8) | b1;
}
/**
* Reads an unsigned 16-bit integer (2 bytes) in little-endian order from the provided DataInputStream.
* The method interprets the first byte as the least significant byte and the second byte as the most significant byte.
*
* @param din The DataInputStream from which the unsigned 16-bit integer is to be read.
* The stream must have at least 2 bytes available to read.
* @return The unsigned 16-bit integer value read from the stream in little-endian byte order.
* @throws IOException If an I/O error occurs during reading, such as if the stream does not contain enough bytes.
*/
private static int readUShortLE(DataInputStream din) throws IOException {
int b1 = din.readUnsignedByte();
int b2 = din.readUnsignedByte();
return (b2 << 8) | b1;
}
/**
* Reads a 32-bit floating-point value in little-endian format from the provided DataInputStream.
* The method interprets the next 4 bytes in the stream as an integer in little-endian byte order
* and converts it into a floating-point value using {@code Float.intBitsToFloat}.
*
* @param din The DataInputStream from which the floating-point value is to be read.
* The stream must have at least 4 bytes available to read.
* @return The 32-bit floating-point value read from the stream in little-endian byte order.
* @throws IOException If an I/O error occurs while reading, such as if the stream does not
* contain enough bytes.
*/
private static float readFloatLE(DataInputStream din) throws IOException {
return Float.intBitsToFloat(readIntLE(din));
}
/**
* Reads an 8-byte double value from a {@link DataInputStream} in little-endian order.
*
* @param din the {@link DataInputStream} from which data will be read
* @return the double value read from the stream
* @throws IOException if an I/O error occurs during reading
*/
private static double readDoubleLE(DataInputStream din) throws IOException {
long b1 = din.readUnsignedByte();
long b2 = din.readUnsignedByte();
long b3 = din.readUnsignedByte();
long b4 = din.readUnsignedByte();
long b5 = din.readUnsignedByte();
long b6 = din.readUnsignedByte();
long b7 = din.readUnsignedByte();
long b8 = din.readUnsignedByte();
long bits = (b8 << 56) | (b7 << 48) | (b6 << 40) | (b5 << 32)
| (b4 << 24) | (b3 << 16) | (b2 << 8) | b1;
return Double.longBitsToDouble(bits);
}
/**
* Represents the header of a PLY file, containing information about the file format,
* vertex count, and vertex properties.
*
* This class provides details such as the format of the PLY file (e.g., ASCII or
* binary), the number of vertices in the file, and an array of properties associated
* with the vertices. These properties define attributes such as position, color,
* and others that may be present in the file.
*
* Instances of this class are immutable once created.
*/
private static final class Header {
final PlyFormat format;
final int vertexCount;
final Property[] vertexProperties;
Header(PlyFormat format, int vertexCount, Property[] vertexProperties) {
this.format = format;
this.vertexCount = vertexCount;
this.vertexProperties = vertexProperties;
}
boolean hasProperty(String name) {
for (Property p : vertexProperties) {
if (p.name.equals(name)) {
return true;
}
}
return false;
}
}
/**
* Enum representing various property types that can be parsed from a PLY file.
*/
private enum PropertyType {
CHAR,
UCHAR,
SHORT,
USHORT,
INT,
UINT,
FLOAT,
DOUBLE;
static PropertyType fromPlyName(String plyType) throws IOException {
return switch (plyType) {
case "char", "int8" -> CHAR;
case "uchar", "uint8" -> UCHAR;
case "short", "int16" -> SHORT;
case "ushort", "uint16" -> USHORT;
case "int", "int32" -> INT;
case "uint", "uint32" -> UINT;
case "float", "float32" -> FLOAT;
case "double", "float64" -> DOUBLE;
default -> throw new IOException("Unsupported PLY property type: " + plyType);
};
}
}
/**
* Represents a property with a specified type and name.
*
* This class is a static, final container that encapsulates the relationship
* between a property type and its associated name. It is immutable and
* designed to be thread-safe.
*/
private static final class Property {
final PropertyType type;
final String name;
Property(PropertyType type, String name) {
this.type = type;
this.name = name;
}
}
}
\ No newline at end of file
/*
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CityDoctor2 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
import java.io.*;
import java.nio.file.Path;
import java.util.Locale;
import de.hft.stuttgart.citydoctor2.datastructure.PointCloud;
/**
* A utility class for parsing and loading point cloud data from external files into
* {@link PointCloud} objects.
*
* @author Radmir Gesler
*/
public class PointCloudParser {
public enum Format {
PLY,
XYZ, // generic x y z text (also covers .pts with xyz only)
LAS, // optional, requires laszip4j
LAZ // optional, requires laszip4j
}
/**
* Reads a point cloud from the given file. The method determines the appropriate
* reader based on the file's extension and parses the data into a {@code PointCloud} object.
* Supported file formats include:
* - .ply (Polygon File Format)
* - .xyz, .pts, .txt (Plain text point cloud)
* - .las, .laz (LASer file format and its compressed variant)
*
* If the file extension is not recognized or not supported, an {@code IOException}
* is thrown.
*
* @param file the {@code Path} of the point cloud file to be read
* @return a {@code PointCloud} object representing the parsed point cloud data
* @throws IOException if an I/O error occurs during reading or if the file format is unsupported
*/
public static PointCloud read(Path file) throws IOException {
String name = file.getFileName().toString().toLowerCase(Locale.ROOT);
if (name.endsWith(".ply")) return PlyReader.read(file);
if (name.endsWith(".xyz") || name.endsWith(".pts") || name.endsWith(".txt")) return XyzReader.read(file);
if (name.endsWith(".las") || name.endsWith(".laz")) return LasReader.read(file);
throw new IOException("Unsupported point cloud format: " + file);
}
/* Maybe for GUI dropdown?*/
public static PointCloud read(Path file, Format format) throws IOException {
return switch (format) {
case PLY -> PlyReader.read(file);
case XYZ -> XyzReader.read(file);
case LAS, LAZ -> LasReader.read(file);
};
}
}
/*-
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CityDoctor2 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
import java.util.Arrays;
/**
* A dynamic buffer that holds short values and can grow automatically as new
* elements are added. This buffer provides functionality for appending elements,
* obtaining the current size, and converting the buffer to an array.
*
* @author Radmir Gesler
*/
public final class ShortGrowableBuffer {
private short[] data;
private int size;
public ShortGrowableBuffer() {
this(1024);
}
public ShortGrowableBuffer(int initialCapacity) {
this.data = new short[Math.max(16, initialCapacity)];
this.size = 0;
}
public void add(short value) {
ensureCapacity(size + 1);
data[size++] = value;
}
public int size() {
return size;
}
public short[] toArray() {
return Arrays.copyOf(data, size);
}
private void ensureCapacity(int minCapacity) {
if (minCapacity <= data.length) {
return;
}
int newCapacity = data.length + Math.max(1024, data.length / 2);
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
}
data = Arrays.copyOf(data, newCapacity);
}
}
\ No newline at end of file
/*-
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CityDoctor2 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
import de.hft.stuttgart.citydoctor2.datastructure.PointCloud;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
/**
* A utility class for reading and parsing XYZ point cloud data from a file.
*
* The XYZ format is a plain text format where each line typically consists of three
* floating-point numbers specifying the X, Y, and Z coordinates of a point in
* three-dimensional space. Lines beginning with '#' or empty lines are considered
* comments or whitespace and are ignored during parsing.
*
* This class provides a method to read an XYZ file and convert its content into
* a {@code PointCloud} instance containing separate arrays for X, Y, and Z coordinates.
* The class is immutable, and its methods ensure the robustness of the parsing process.
*
* The reader uses internal growable buffers to store points while processing the input file,
* ensuring efficient memory allocation even for large datasets. Each coordinate is
* stored in a dedicated buffer, and the buffers are converted into arrays when the
* file processing completes.
*
* This utility class cannot be instantiated.
*
* This class is thread-safe only if used sequentially, as concurrent modification
* or access to the static method may cause unintended behavior.
*
* @author Radmir Gesler
*/
public final class XyzReader {
private XyzReader() {
}
static PointCloud read(Path file) throws IOException {
FloatGrowableBuffer xs = new FloatGrowableBuffer(16_384);
FloatGrowableBuffer ys = new FloatGrowableBuffer(16_384);
FloatGrowableBuffer zs = new FloatGrowableBuffer(16_384);
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream(file.toFile()), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
String[] parts = line.split("\\s+");
if (parts.length < 3) {
continue;
}
xs.add(Float.parseFloat(parts[0]));
ys.add(Float.parseFloat(parts[1]));
zs.add(Float.parseFloat(parts[2]));
}
}
return new PointCloud(
xs.toArray(),
ys.toArray(),
zs.toArray(),
null,
null,
null,
xs.size()
);
}
}
\ No newline at end of file
/*-
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CityDoctor2 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser;
import static org.junit.Assert.assertSame;
import de.hft.stuttgart.citydoctor2.datastructure.PointCloud;
import de.hft.stuttgart.citydoctor2.parser.pointcloud.PointCloudParser;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Path;
/**
*
* @author Radmir Gesler
*
*/
public class PointCloudParserTest {
@Test
public void testPointCloudParsing() throws IOException {
testHelperForPointCloud("*.ply","src/test/resources/duedo_small.ply");
testHelperForPointCloud("*.ply","src/test/resources/duedo_small_bin.ply");
testHelperForPointCloud("*.xyz","src/test/resources/duedo_small.xyz");
testHelperForPointCloud("*.pts","src/test/resources/duedo_small.pts");
testHelperForPointCloud("*.txt","src/test/resources/duedo_small.txt");
testHelperForPointCloud("*.las","src/test/resources/duedo_small.las");
testHelperForPointCloud("*.laz","src/test/resources/duedo_small.laz");
}
private void testHelperForPointCloud(String type, String file) throws IOException {
System.out.println("====================================================================");
System.out.println("Test " + type + " parsing from : " + file);
System.out.println("====================================================================");
PointCloud pc = PointCloudParser.read(Path.of(file));
if (pc.sanityCheck()) {
System.out.println(" Sanity check passed");
System.out.println(" Loaded points: " + pc.size);
System.out.println(" First point: " + pc.x[0] + ", " + pc.y[0] + ", " + pc.z[0]);
System.out.println(" Test " + file + " passed");
} else {
System.out.println(" Test " + file + " NOT passed");
}
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment