Commit ab21277c authored by RadmirG's avatar RadmirG
Browse files

PointCloudParser integrated into CityDoctor data model

parent 4c723c5a
Pipeline #12503 passed with stage
in 2 minutes and 14 seconds
/*
* 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.io.Serial;
import java.util.Arrays;
import java.util.Objects;
import de.hft.stuttgart.citydoctor2.math.Vector3d;
import de.hft.stuttgart.citydoctor2.parser.pointcloud.Classification;
/**
* Represents one point of a point cloud with optional attributes such as
* RGB color, intensity, and classification.
*/
public class Point extends Vector3d {
@Serial
private static final long serialVersionUID = 1L;
private int[] rgb;
private Classification classification;
public Point() {
super();
}
public Point(double x, double y, double z) {
super(x, y, z);
}
public Point(double x, double y, double z, int[] rgb) {
super(x, y, z);
setRgb(rgb);
}
public Point(double x, double y, double z, Classification classification) {
super(x, y, z);
this.classification = classification;
}
public Point(double x, double y, double z, int[] rgb, Classification classification) {
super(x, y, z);
setRgb(rgb);
this.classification = classification;
}
public Point(Point point) {
super(point.getX(), point.getY(), point.getZ());
this.rgb = point.rgb != null ? Arrays.copyOf(point.rgb, point.rgb.length) : null;
this.classification = point.classification != null ? point.classification : null;
}
/**
* Returns a defensive copy of the RGB triplet.
*/
public int[] getRgb() {
return rgb != null ? Arrays.copyOf(rgb, rgb.length) : null;
}
public void setRgb(int[] rgb) {
if (rgb == null) {
this.rgb = null;
return;
}
if (rgb.length != 3) {
throw new IllegalArgumentException("rgb must have exactly 3 elements: [r, g, b]");
}
this.rgb = Arrays.copyOf(rgb, rgb.length);
}
public boolean hasRgb() {
return rgb != null;
}
public Classification getClassification() {
return classification;
}
public void setClassification(Classification classification) {
this.classification = classification;
}
public boolean hasClassification() {
return classification != null;
}
public void setCoordinates(double x, double y, double z) {
setX(x);
setY(y);
setZ(z);
}
public Point copy() {
return new Point(this);
}
@Override
public String toString() {
return "Point{" +
"x=" + getX() +
", y=" + getY() +
", z=" + getZ() +
", rgb=" + Arrays.toString(rgb) +
", classification=" + classification +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point point)) return false;
return Double.compare(getX(), point.getX()) == 0
&& Double.compare(getY(), point.getY()) == 0
&& Double.compare(getZ(), point.getZ()) == 0
&& Objects.equals(classification, point.classification)
&& Arrays.equals(rgb, point.rgb);
}
@Override
public int hashCode() {
int result = Objects.hash(getX(), getY(), getZ(), classification);
result = 31 * result + Arrays.hashCode(rgb);
return result;
}
}
\ No newline at end of file
......@@ -19,91 +19,124 @@
package de.hft.stuttgart.citydoctor2.datastructure;
import java.util.Arrays;
import de.hft.stuttgart.citydoctor2.parser.pointcloud.Classification;
import java.util.*;
/**
* 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;
public class PointCloud {
// 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);
public boolean hasRgb;
public boolean hasClassification;
public List<Point> points;
public PointCloud() {
this.points = new ArrayList<>();
}
public PointCloud(List<Point> points) {
Objects.requireNonNull(points, "points must not be null");
this.points = new ArrayList<>(points);
}
public PointCloud(List<Point> points, boolean hasRgb, boolean hasClassification) {
this.points = points;
this.hasRgb = hasRgb;
this.hasClassification = hasClassification;
}
public int size() {
return points.size();
}
public boolean isEmpty() {
return points.isEmpty();
}
public List<Point> getPoints() {
// Callers cannot modify it otherwise it throws UnsupportedOperationException
return Collections.unmodifiableList(points);
}
public Point getPoint(int index) {
return points.get(index);
}
public void addPoint(Point point) {
points.add(Objects.requireNonNull(point, "point must not be null"));
}
public void addPoints(Collection<Point> newPoints) {
Objects.requireNonNull(newPoints, "newPoints must not be null");
for (Point p : newPoints) {
addPoint(p);
}
}
public void clear() {
points.clear();
}
public boolean hasRgb() {
return hasRgb;
}
public boolean hasClassification() {
return hasClassification;
}
public Set<Classification> getAvailableClassifications() {
if (!hasClassification) {
return Collections.emptySet();
}
Set<Classification> classes = new LinkedHashSet<>();
for (Point p : points) {
if (p.hasClassification()) {
classes.add(p.getClassification());
}
}
return classes;
}
public PointCloud filterByClassification(Classification classification) {
Objects.requireNonNull(classification, "classification must not be null");
List<Point> filtered = new ArrayList<>();
for (Point p : points) {
if (p.hasClassification() && classification.equals(p.getClassification())) {
filtered.add(p.copy());
}
}
return new PointCloud(filtered);
}
public PointCloud copy() {
List<Point> copied = new ArrayList<>(points.size());
for (Point p : points) {
copied.add(p.copy());
}
return new PointCloud(copied);
}
public BoundingBox getBoundingBox() {
return BoundingBox.ofPoints(points);
}
@Override
public String toString() {
return "PointCloud{" +
"size=" + points.size() +
", hasRgb=" + hasRgb() +
", hasClassification=" + hasClassification() +
'}';
}
}
\ No newline at end of file
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
public interface Classification {
/**
* Numeric code as stored or interpreted in the source format.
*/
int getCode();
/**
* Human-readable name.
*/
String getLabel();
/**
* Name of the source classification system, e.g. LAS, PLY, XYZ.
*/
String getSystem();
}
\ No newline at end of file
......@@ -24,9 +24,12 @@ import java.nio.file.Path;
import java.util.Locale;
import de.hft.stuttgart.citydoctor2.datastructure.PointCloud;
import de.hft.stuttgart.citydoctor2.parser.pointcloud.las.LasReader;
import de.hft.stuttgart.citydoctor2.parser.pointcloud.ply.PlyReader;
import de.hft.stuttgart.citydoctor2.parser.pointcloud.xyz.XyzReader;
/**
* A utility class for parsing and loading point cloud data from external files into
* A utility class for parsing and loading point cloud data (PC) from external files into
* {@link PointCloud} objects.
*
* @author Radmir Gesler
......@@ -41,7 +44,7 @@ public class PointCloudParser {
}
/**
* Reads a point cloud from the given file. The method determines the appropriate
* Delegate a PC-file to a specified PC reader. 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)
......@@ -57,12 +60,9 @@ public class PointCloudParser {
*/
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);
}
......
......@@ -17,53 +17,64 @@
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
package de.hft.stuttgart.citydoctor2.parser.pointcloud.las;
import java.util.Arrays;
import de.hft.stuttgart.citydoctor2.parser.pointcloud.Classification;
/**
* 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 {
public enum LasClassification implements Classification {
CREATED_NEVER_CLASSIFIED(0, "Created, Never Classified"),
UNCLASSIFIED(1, "Unclassified"),
GROUND(2, "Ground"),
LOW_VEGETATION(3, "Low Vegetation"),
MEDIUM_VEGETATION(4, "Medium Vegetation"),
HIGH_VEGETATION(5, "High Vegetation"),
BUILDING(6, "Building"),
LOW_POINT_NOISE(7, "Low Point (Noise)"),
MODEL_KEY_POINT(8, "Model Key-Point (Mass Point)"),
WATER(9, "Water"),
RAIL(10, "Rail"),
ROAD_SURFACE(11, "Road Surface"),
OVERLAP_POINT(12, "Overlap Point"),
WIRE_GUARD(13, "Wire Guard"),
WIRE_CONDUCTOR(14, "Wire Conductor"),
TRANSMISSION_TOWER(15, "Transmission Tower"),
WIRE_STRUCTURE_CONNECTOR(16, "Wire-Structure Connector"),
BRIDGE_DECK(17, "Bridge Deck"),
HIGH_NOISE(18, "High Noise"),
OVERHEAD_STRUCTURE(19, "Overhead Structure"),
IGNORED_GROUND(20, "Ignored Ground"),
SNOW(21, "Snow"),
TEMPORAL_EXCLUSION(22, "Temporal Exclusion");
private byte[] data;
private int size;
private final int code;
private final String label;
public ByteGrowableBuffer() {
this(1024);
LasClassification(int code, String label) {
this.code = code;
this.label = label;
}
public ByteGrowableBuffer(int initialCapacity) {
this.data = new byte[Math.max(16, initialCapacity)];
this.size = 0;
@Override
public int getCode() {
return code;
}
public void add(byte value) {
ensureCapacity(size + 1);
data[size++] = value;
@Override
public String getLabel() {
return label;
}
public int size() {
return size;
@Override
public String getSystem() {
return "LAS";
}
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;
public static LasClassification fromCode(int code) {
for (LasClassification c : values()) {
if (c.code == code) {
return c;
}
}
data = Arrays.copyOf(data, newCapacity);
return null;
}
}
\ No newline at end of file
......@@ -17,27 +17,21 @@
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
package de.hft.stuttgart.citydoctor2.parser.pointcloud.las;
import com.github.mreutegg.laszip4j.LASPoint;
import com.github.mreutegg.laszip4j.LASReader;
import de.hft.stuttgart.citydoctor2.datastructure.Point;
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.
* The {@code LasReader} class provides methods for reading LAS/LAZ files
* into a {@link PointCloud} data model of citydoctor2. The class contains utilities for handling LAS file
* headers, choosing point counts, and reading data.
*
* @author Radmir Gesler
*/
......@@ -49,14 +43,7 @@ public final class LasReader {
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);
}
return readLasPointCloud(reader);
} catch (Exception e) {
throw (e instanceof IOException)
? (IOException) e
......@@ -83,13 +70,7 @@ public final class LasReader {
int[] legacyByReturn = safeIntArray(header::getLegacyNumberOfPointsByReturn);
return new HeaderInfo(
versionMajor,
versionMinor,
legacyPointCount,
pointCount,
legacyByReturn
);
return new HeaderInfo(versionMajor, versionMinor, legacyPointCount, pointCount, legacyByReturn);
}
/**
......@@ -97,74 +78,27 @@ public final class LasReader {
* 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
* @param lasHeader 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;
private static int chooseExpectedPointCount(HeaderInfo lasHeader) throws IOException {
long pointCount;
if (h.versionMajor > 1 || (h.versionMajor == 1 && h.versionMinor >= 4)) {
n = h.pointCount > 0 ? h.pointCount : h.legacyPointCount;
if (lasHeader.versionMajor > 1 || (lasHeader.versionMajor == 1 && lasHeader.versionMinor >= 4)) {
pointCount = lasHeader.pointCount > 0 ? lasHeader.pointCount : lasHeader.legacyPointCount;
} else {
n = h.legacyPointCount > 0 ? h.legacyPointCount : h.pointCount;
pointCount = lasHeader.legacyPointCount > 0 ? lasHeader.legacyPointCount : lasHeader.pointCount;
}
if (n <= 0) {
if (pointCount <= 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);
if (pointCount > Integer.MAX_VALUE) {
throw new IOException("Point cloud too large for in-memory loading: " + pointCount);
}
return new PointCloud(x, y, z, null, intensity, classification, i);
return (int) pointCount;
}
/**
......@@ -177,30 +111,22 @@ public final class LasReader {
* @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);
private static PointCloud readLasPointCloud(LASReader reader) {
PointCloud pointCloud = new PointCloud();
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());
Point point = new Point(p.getX(), p.getY(), p.getZ());
if (p.hasRGB()) {
int red = (int) p.getRed();
int green = (int) p.getGreen();
int blue = (int) p.getBlue();
point.setRgb(new int[]{red, green, blue});
}
point.setClassification(LasClassification.fromCode(p.getClassification()));
pointCloud.addPoint(point);
}
return new PointCloud(
xs.toArray(),
ys.toArray(),
zs.toArray(),
null,
intensities.toArray(),
classifications.toArray(),
xs.size()
);
return pointCloud;
}
/**
......@@ -238,13 +164,6 @@ public final class LasReader {
* 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 {
......@@ -255,12 +174,6 @@ public final class LasReader {
* 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 {
......@@ -285,6 +198,5 @@ public final class LasReader {
long legacyPointCount,
long pointCount,
int[] legacyByReturn
) {
}
) { }
}
\ No newline at end of file
......@@ -17,52 +17,25 @@
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
package de.hft.stuttgart.citydoctor2.parser.pointcloud.ply;
import java.util.Arrays;
import de.hft.stuttgart.citydoctor2.parser.pointcloud.Classification;
/**
* 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 enum PlyClassification implements Classification {
;
public int size() {
return size;
@Override
public int getCode() {
return 0;
}
public float[] toArray() {
return Arrays.copyOf(data, size);
@Override
public String getLabel() {
return "";
}
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);
@Override
public String getSystem() {
return "";
}
}
\ No newline at end of file
......@@ -17,11 +17,11 @@
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
package de.hft.stuttgart.citydoctor2.parser.pointcloud.ply;
import de.hft.stuttgart.citydoctor2.datastructure.Point;
import de.hft.stuttgart.citydoctor2.datastructure.PointCloud;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.io.BufferedInputStream;
......@@ -37,19 +37,10 @@ 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
* The {@code PlyReader} class for reading PLY (Polygon File Format) files and converting them into
* a {@link PointCloud} data model of citydoctor2. 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
*/
......@@ -63,7 +54,7 @@ public final class PlyReader {
BINARY_LITTLE_ENDIAN
}
static PointCloud read(Path file) throws IOException {
public static PointCloud read(Path file) throws IOException {
Header header = readHeader(file);
if (header.vertexCount <= 0) {
......@@ -73,26 +64,19 @@ public final class PlyReader {
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;
PointCloud pointCloud = new PointCloud();
try (InputStream is = new BufferedInputStream(new FileInputStream(file.toFile()))) {
skipHeaderBytes(is);
if (header.format == PlyFormat.ASCII) {
readAsciiVertices(is, header, x, y, z, rgb);
pointCloud = readAsciiVertices(is, header);
} else {
readBinaryLittleEndianVertices(is, header, x, y, z, rgb);
pointCloud = readBinaryLittleEndianVertices(is, header);
}
}
return new PointCloud(x, y, z, rgb, null, null, header.vertexCount);
return pointCloud;
}
/**
......@@ -168,143 +152,165 @@ public final class PlyReader {
}
/**
* 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.
* Reads a set of 3D points (vertices) in ASCII format from the provided input stream and
* constructs a point cloud. Each vertex must include x, y, and z coordinates, and optionally
* may include RGB color information (red, green, blue channels).
*
* @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.
* @param is the input stream containing the ASCII encoded PLY vertex data.
* @param header the header object that describes the properties and format
* of the PLY file, including vertex properties and their count.
* @return a {@code PointCloud} object containing the parsed vertices with
* their coordinates and optional color information.
* @throws IOException if the input stream cannot be read, if mandatory vertex
* properties (x, y, z) are missing, or if the vertex data
* format or count is invalid.
*/
private static void readAsciiVertices(InputStream is, Header header,
float[] x, float[] y, float[] z, int[] rgb) throws IOException {
private static PointCloud readAsciiVertices(InputStream is, Header header) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.US_ASCII));
if (!header.hasProperty("x") || !header.hasProperty("y") || !header.hasProperty("z"))
throw new IOException("PLY vertex properties must include x, y, and z");
if (header.vertexCount < 0)
throw new IOException("PLY vertex count must not be negative");
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.US_ASCII));
boolean hasRgb = header.hasProperty("red") && header.hasProperty("green") && header.hasProperty("blue");
PointCloud pointCloud = new PointCloud();
for (int i = 0; i < header.vertexCount; i++) {
String line = br.readLine();
while (line != null && line.trim().isEmpty()) {
line = br.readLine();
}
String line;
do { line = reader.readLine();
} while (line != null && line.trim().isEmpty()); // Skips empty lines.
if (line == null) {
throw new EOFException("Unexpected EOF while reading ASCII PLY vertices");
throw new EOFException( "Unexpected EOF while reading ASCII PLY vertex " + i
+ " of " + header.vertexCount);
}
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);
if (parts.length != header.vertexProperties.length) {
throw new IOException( "Invalid field count for PLY vertex " + i
+ ": expected " + header.vertexProperties.length
+ ", found " + parts.length);
}
float px = 0f, py = 0f, pz = 0f;
int r = 0, g = 0, b = 0;
double xCoord = 0.0, yCoord = 0.0, zCoord = 0.0;
int red = 0, green = 0, blue = 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));
String property = header.vertexProperties[p].name;
String value = parts[p];
if (property.equals("x")) {
xCoord = Double.parseDouble(value);
} else if (property.equals("y")) {
yCoord = Double.parseDouble(value);
} else if (property.equals("z")) {
zCoord = Double.parseDouble(value);
} else if (hasRgb) {
if (property.equals("red")) {
red = clamp255(parseIntSafe(value));
} else if (property.equals("green")) {
green = clamp255(parseIntSafe(value));
} else if (property.equals("blue")) {
blue = clamp255(parseIntSafe(value));
}
}
}
x[i] = px;
y[i] = py;
z[i] = pz;
if (rgb != null) {
rgb[i] = (r << 16) | (g << 8) | b;
}
if (!Double.isFinite(xCoord)
|| !Double.isFinite(yCoord)
|| !Double.isFinite(zCoord))
throw new IOException("Non-finite coordinates at PLY vertex " + i);
Point point = new Point(xCoord, yCoord, zCoord);
if (hasRgb)
point.setRgb(new int[]{red, green, blue});
pointCloud.addPoint(point);
}
return pointCloud;
}
/**
* 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.
* Reads a binary little-endian encoded list of vertices from the given input stream,
* based on the properties defined in the PLY header.
*
* @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.
* @param is The input stream containing the binary little-endian vertex data.
* @param header The PLY header that describes the structure of the vertex data.
* Must include the properties "x", "y", and "z", and optionally
* "red", "green", and "blue" for RGB color information.
* @return A {@code PointCloud} object containing the vertices read from the input stream.
* Each vertex includes its coordinates and optional RGB color values.
* @throws IOException If there is an error reading the input stream, if the vertex count in
* the header is negative, if required properties (x, y, z) are missing,
* or if a property has an unsupported type or non-finite coordinates.
*/
private static void readBinaryLittleEndianVertices(InputStream is, Header header,
float[] x, float[] y, float[] z, int[] rgb) throws IOException {
private static PointCloud readBinaryLittleEndianVertices(InputStream is, Header header)
throws IOException {
if (!header.hasProperty("x") || !header.hasProperty("y") || !header.hasProperty("z"))
throw new IOException("PLY vertex properties must include x, y, and z");
if (header.vertexCount < 0)
throw new IOException("PLY vertex count must not be negative");
DataInputStream din = new DataInputStream(is);
boolean hasRgb = header.hasProperty("red")
&& header.hasProperty("green")
&& header.hasProperty("blue");
PointCloud pointCloud = new PointCloud();
for (int i = 0; i < header.vertexCount; i++) {
float px = 0f, py = 0f, pz = 0f;
int r = 0, g = 0, b = 0;
double xCoord = 0.0, yCoord = 0.0, zCoord = 0.0;
int red = 0, green = 0, blue = 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;
double value = switch (prop.type) {
case FLOAT -> readFloatLE(din);
case DOUBLE -> readDoubleLE(din);
case UCHAR -> din.readUnsignedByte();
case CHAR -> din.readByte();
case USHORT -> readUShortLE(din);
case SHORT -> (short) readUShortLE(din);
case UINT -> Integer.toUnsignedLong(readIntLE(din));
case INT -> readIntLE(din);
default -> throw new IOException(
"Unsupported binary PLY property type: " + prop.type);
};
String property = prop.name;
if (property.equals("x")) {
xCoord = value;
} else if (property.equals("y")) {
yCoord = value;
} else if (property.equals("z")) {
zCoord = value;
} else if (hasRgb) {
if (property.equals("red")) {
red = clamp255((int) value);
} else if (property.equals("green")) {
green = clamp255((int) value);
} else if (property.equals("blue")) {
blue = clamp255((int) value);
}
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;
}
if (!Double.isFinite(xCoord)
|| !Double.isFinite(yCoord)
|| !Double.isFinite(zCoord))
throw new IOException("Non-finite coordinates at PLY vertex " + i);
Point point = new Point(xCoord, yCoord, zCoord);
if (hasRgb)
point.setRgb(new int[]{red, green, blue});
pointCloud.addPoint(point);
}
return pointCloud;
}
/**
......
......@@ -17,53 +17,25 @@
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud.xyz;
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
import de.hft.stuttgart.citydoctor2.parser.pointcloud.Classification;
import java.util.Arrays;
public enum XyzClassification implements Classification {
;
/**
* 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;
@Override
public int getCode() {
return 0;
}
public short[] toArray() {
return Arrays.copyOf(data, size);
@Override
public String getLabel() {
return "";
}
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);
@Override
public String getSystem() {
return "";
}
}
\ No newline at end of file
......@@ -18,8 +18,9 @@
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
package de.hft.stuttgart.citydoctor2.parser.pointcloud.xyz;
import de.hft.stuttgart.citydoctor2.datastructure.Point;
import de.hft.stuttgart.citydoctor2.datastructure.PointCloud;
import java.io.BufferedReader;
......@@ -58,10 +59,9 @@ 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);
public static PointCloud read(Path file) throws IOException {
PointCloud pointCloud = new PointCloud();
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream(file.toFile()), StandardCharsets.UTF_8))) {
......@@ -79,20 +79,13 @@ public final class XyzReader {
continue;
}
xs.add(Float.parseFloat(parts[0]));
ys.add(Float.parseFloat(parts[1]));
zs.add(Float.parseFloat(parts[2]));
Point point = new Point(Double.parseDouble(parts[0]),
Double.parseDouble(parts[1]),
Double.parseDouble(parts[2]));
pointCloud.addPoint(point);
}
}
return new PointCloud(
xs.toArray(),
ys.toArray(),
zs.toArray(),
null,
null,
null,
xs.size()
);
return pointCloud;
}
}
\ No newline at end of file
......@@ -18,8 +18,8 @@
*/
package de.hft.stuttgart.citydoctor2.parser;
import static org.junit.Assert.assertSame;
import de.hft.stuttgart.citydoctor2.datastructure.BoundarySurface;
import de.hft.stuttgart.citydoctor2.datastructure.BoundingBox;
import de.hft.stuttgart.citydoctor2.datastructure.PointCloud;
import de.hft.stuttgart.citydoctor2.parser.pointcloud.PointCloudParser;
import org.junit.Test;
......@@ -28,36 +28,47 @@ import java.io.IOException;
import java.nio.file.Path;
/**
* Unit test class for validating the functionality of the PointCloudParser.
* This class primarily ensures that point cloud data is correctly parsed from various file formats
* and verifies the integrity of the resulting PointCloud object through a series of tests.
*
* @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");
testHelperForPointCloud("*.ply","src/test/resources/point_clouds/duedo_small.ply");
testHelperForPointCloud("*.ply","src/test/resources/point_clouds/duedo_small_bin.ply");
testHelperForPointCloud("*.xyz","src/test/resources/point_clouds/duedo_small.xyz");
testHelperForPointCloud("*.pts","src/test/resources/point_clouds/duedo_small.pts");
testHelperForPointCloud("*.txt","src/test/resources/point_clouds/duedo_small.txt");
testHelperForPointCloud("*.las","src/test/resources/point_clouds/duedo_small.las");
testHelperForPointCloud("*.laz","src/test/resources/point_clouds/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()) {
PointCloud pointCloud = PointCloudParser.read(Path.of(file));
if (sanityCheck(pointCloud)) {
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(" Loaded points: " + pointCloud.size());
System.out.println(" First point: " + pointCloud.points.get(0).getX()
+ ", " + pointCloud.points.get(0).getY()
+ ", " + pointCloud.points.get(0).getZ());
System.out.println(" Test " + file + " passed");
} else {
System.out.println(" Test " + file + " NOT passed");
}
}
private boolean sanityCheck(PointCloud pc){
return pc.size() > 0;
}
}
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