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 @@ ...@@ -19,91 +19,124 @@
package de.hft.stuttgart.citydoctor2.datastructure; 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 * 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. * 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 * @author Radmir Gesler
*/ */
public final class PointCloud { public class PointCloud {
public final float[] x;
public final float[] y;
public final float[] z;
// Optional attributes if existing // Optional attributes if existing
public final int[] rgb; // packed 0xRRGGBB public boolean hasRgb;
public final short[] intensity; // LAS intensity public boolean hasClassification;
public final byte[] classification; // LAS classification
public List<Point> points;
public final int size;
public PointCloud() {
public PointCloud(float[] x, float[] y, float[] z, this.points = new ArrayList<>();
int[] rgb, short[] intensity, byte[] classification, }
int size) {
this.x = x; public PointCloud(List<Point> points) {
this.y = y; Objects.requireNonNull(points, "points must not be null");
this.z = z; this.points = new ArrayList<>(points);
this.rgb = rgb; }
this.intensity = intensity;
this.classification = classification; public PointCloud(List<Point> points, boolean hasRgb, boolean hasClassification) {
this.size = size; this.points = points;
} this.hasRgb = hasRgb;
this.hasClassification = hasClassification;
/** }
* 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 public int size() {
* (rgb, intensity, classification) based on the expected size of the PointCloud. return points.size();
* }
* Throws an exception if one of the following conditions is detected:
* - The x, y, or z arrays are null. public boolean isEmpty() {
* - The length of the x, y, or z arrays is smaller than the specified size of the PointCloud. return points.isEmpty();
* - 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. public List<Point> getPoints() {
* // Callers cannot modify it otherwise it throws UnsupportedOperationException
* @throws IllegalStateException if any of the above conditions are violated. return Collections.unmodifiableList(points);
*/ }
public boolean sanityCheck() {
if (x == null || y == null || z == null) public Point getPoint(int index) {
throw new IllegalStateException("XYZ missing"); return points.get(index);
if (x.length < size || y.length < size || z.length < size) }
throw new IllegalStateException("Array too small");
if (rgb != null && rgb.length < size) public void addPoint(Point point) {
throw new IllegalStateException("RGB array too small"); points.add(Objects.requireNonNull(point, "point must not be null"));
if (intensity != null && intensity.length < size) }
throw new IllegalStateException("Intensity array too small");
if (classification != null && classification.length < size) public void addPoints(Collection<Point> newPoints) {
throw new IllegalStateException("Classification array too small"); Objects.requireNonNull(newPoints, "newPoints must not be null");
return true; for (Point p : newPoints) {
} addPoint(p);
}
/** }
* Extracts a subset of points from the current PointCloud instance based on the specified range.
* public void clear() {
* The method creates a new PointCloud containing only the points within the specified points.clear();
* 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. public boolean hasRgb() {
* return hasRgb;
* @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 boolean hasClassification() {
*/ return hasClassification;
public PointCloud slice(int fromInclusive, int toExclusive) { }
int n = Math.max(0, toExclusive - fromInclusive);
float[] nx = Arrays.copyOfRange(x, fromInclusive, toExclusive); public Set<Classification> getAvailableClassifications() {
float[] ny = Arrays.copyOfRange(y, fromInclusive, toExclusive); if (!hasClassification) {
float[] nz = Arrays.copyOfRange(z, fromInclusive, toExclusive); return Collections.emptySet();
int[] nrgb = rgb == null ? null : Arrays.copyOfRange(rgb, fromInclusive, toExclusive); }
short[] nint = intensity == null ? null : Arrays.copyOfRange(intensity, fromInclusive, toExclusive); Set<Classification> classes = new LinkedHashSet<>();
byte[] ncls = classification == null ? null : Arrays.copyOfRange(classification, fromInclusive, toExclusive); for (Point p : points) {
return new PointCloud(nx, ny, nz, nrgb, nint, ncls, n); 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; ...@@ -24,9 +24,12 @@ import java.nio.file.Path;
import java.util.Locale; import java.util.Locale;
import de.hft.stuttgart.citydoctor2.datastructure.PointCloud; 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. * {@link PointCloud} objects.
* *
* @author Radmir Gesler * @author Radmir Gesler
...@@ -41,7 +44,7 @@ public class PointCloudParser { ...@@ -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. * reader based on the file's extension and parses the data into a {@code PointCloud} object.
* Supported file formats include: * Supported file formats include:
* - .ply (Polygon File Format) * - .ply (Polygon File Format)
...@@ -57,12 +60,9 @@ public class PointCloudParser { ...@@ -57,12 +60,9 @@ public class PointCloudParser {
*/ */
public static PointCloud read(Path file) throws IOException { public static PointCloud read(Path file) throws IOException {
String name = file.getFileName().toString().toLowerCase(Locale.ROOT); String name = file.getFileName().toString().toLowerCase(Locale.ROOT);
if (name.endsWith(".ply")) return PlyReader.read(file); 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(".xyz") || name.endsWith(".pts") || name.endsWith(".txt")) return XyzReader.read(file);
if (name.endsWith(".las") || name.endsWith(".laz")) return LasReader.read(file); if (name.endsWith(".las") || name.endsWith(".laz")) return LasReader.read(file);
throw new IOException("Unsupported point cloud format: " + file); throw new IOException("Unsupported point cloud format: " + file);
} }
......
...@@ -17,53 +17,64 @@ ...@@ -17,53 +17,64 @@
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>. * 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;
/** public enum LasClassification implements Classification {
* A dynamic byte buffer that can grow in size as needed. CREATED_NEVER_CLASSIFIED(0, "Created, Never Classified"),
* This class provides functionality to add bytes to the buffer UNCLASSIFIED(1, "Unclassified"),
* and retrieve its contents or size. It ensures that the internal GROUND(2, "Ground"),
* storage grows dynamically to accommodate new data. LOW_VEGETATION(3, "Low Vegetation"),
* MEDIUM_VEGETATION(4, "Medium Vegetation"),
* @author Radmir Gesler HIGH_VEGETATION(5, "High Vegetation"),
*/ BUILDING(6, "Building"),
public final class ByteGrowableBuffer { 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 final int code;
private int size; private final String label;
public ByteGrowableBuffer() {
this(1024);
}
public ByteGrowableBuffer(int initialCapacity) { LasClassification(int code, String label) {
this.data = new byte[Math.max(16, initialCapacity)]; this.code = code;
this.size = 0; this.label = label;
} }
public void add(byte value) { @Override
ensureCapacity(size + 1); public int getCode() {
data[size++] = value; return code;
} }
public int size() { @Override
return size; public String getLabel() {
return label;
} }
public byte[] toArray() { @Override
return Arrays.copyOf(data, size); public String getSystem() {
return "LAS";
} }
private void ensureCapacity(int minCapacity) { public static LasClassification fromCode(int code) {
if (minCapacity <= data.length) { for (LasClassification c : values()) {
return; if (c.code == code) {
return c;
} }
int newCapacity = data.length + Math.max(1024, data.length / 2);
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
} }
data = Arrays.copyOf(data, newCapacity); return null;
} }
} }
\ No newline at end of file
...@@ -17,27 +17,21 @@ ...@@ -17,27 +17,21 @@
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>. * 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.LASPoint;
import com.github.mreutegg.laszip4j.LASReader; import com.github.mreutegg.laszip4j.LASReader;
import de.hft.stuttgart.citydoctor2.datastructure.Point;
import de.hft.stuttgart.citydoctor2.datastructure.PointCloud; import de.hft.stuttgart.citydoctor2.datastructure.PointCloud;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Arrays;
/** /**
* The {@code LasReader} class provides static methods for reading LAS/LAZ files * The {@code LasReader} class provides methods for reading LAS/LAZ files
* into point cloud data. The class contains utilities for handling LAS file * into a {@link PointCloud} data model of citydoctor2. The class contains utilities for handling LAS file
* headers, choosing point counts, and reading data efficiently into memory. * headers, choosing point counts, and reading data.
*
* 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 * @author Radmir Gesler
*/ */
...@@ -49,14 +43,7 @@ public final class LasReader { ...@@ -49,14 +43,7 @@ public final class LasReader {
LASReader reader = new LASReader(new File(file.toString())); LASReader reader = new LASReader(new File(file.toString()));
try { try {
HeaderInfo h = readHeaderInfo(reader); return readLasPointCloud(reader);
int expectedCount = chooseExpectedPointCount(h);
if (expectedCount > 0) {
return readPreallocated(reader, expectedCount);
} else {
return readDynamic(reader);
}
} catch (Exception e) { } catch (Exception e) {
throw (e instanceof IOException) throw (e instanceof IOException)
? (IOException) e ? (IOException) e
...@@ -83,13 +70,7 @@ public final class LasReader { ...@@ -83,13 +70,7 @@ public final class LasReader {
int[] legacyByReturn = safeIntArray(header::getLegacyNumberOfPointsByReturn); int[] legacyByReturn = safeIntArray(header::getLegacyNumberOfPointsByReturn);
return new HeaderInfo( return new HeaderInfo(versionMajor, versionMinor, legacyPointCount, pointCount, legacyByReturn);
versionMajor,
versionMinor,
legacyPointCount,
pointCount,
legacyByReturn
);
} }
/** /**
...@@ -97,74 +78,27 @@ public final class LasReader { ...@@ -97,74 +78,27 @@ public final class LasReader {
* This method accounts for version-specific differences and validates that the size * 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. * 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. * extracted from a LAS/LAZ file header.
* @return the expected number of points in the point cloud as an integer. * @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. * @throws IOException if the number of points is too large to be processed in memory.
*/ */
private static int chooseExpectedPointCount(HeaderInfo h) throws IOException { private static int chooseExpectedPointCount(HeaderInfo lasHeader) throws IOException {
long n; long pointCount;
if (h.versionMajor > 1 || (h.versionMajor == 1 && h.versionMinor >= 4)) { if (lasHeader.versionMajor > 1 || (lasHeader.versionMajor == 1 && lasHeader.versionMinor >= 4)) {
n = h.pointCount > 0 ? h.pointCount : h.legacyPointCount; pointCount = lasHeader.pointCount > 0 ? lasHeader.pointCount : lasHeader.legacyPointCount;
} else { } 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; return 0;
} }
if (n > Integer.MAX_VALUE) { if (pointCount > Integer.MAX_VALUE) {
throw new IOException("Point cloud too large for in-memory loading: " + n); throw new IOException("Point cloud too large for in-memory loading: " + pointCount);
} }
return (int) n; return (int) pointCount;
}
/**
* 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);
} }
/** /**
...@@ -177,30 +111,22 @@ public final class LasReader { ...@@ -177,30 +111,22 @@ public final class LasReader {
* @return a {@link PointCloud} object containing the points read 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 * including their X, Y, Z coordinates, intensity, and classification
*/ */
private static PointCloud readDynamic(LASReader reader) { private static PointCloud readLasPointCloud(LASReader reader) {
FloatGrowableBuffer xs = new FloatGrowableBuffer(16_384); PointCloud pointCloud = new PointCloud();
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()) { for (LASPoint p : reader.getPoints()) {
xs.add((float) p.getX()); Point point = new Point(p.getX(), p.getY(), p.getZ());
ys.add((float) p.getY()); if (p.hasRGB()) {
zs.add((float) p.getZ()); int red = (int) p.getRed();
intensities.add((short) p.getIntensity()); int green = (int) p.getGreen();
classifications.add((byte) p.getClassification()); int blue = (int) p.getBlue();
point.setRgb(new int[]{red, green, blue});
}
point.setClassification(LasClassification.fromCode(p.getClassification()));
pointCloud.addPoint(point);
} }
return new PointCloud( return pointCloud;
xs.toArray(),
ys.toArray(),
zs.toArray(),
null,
intensities.toArray(),
classifications.toArray(),
xs.size()
);
} }
/** /**
...@@ -238,13 +164,6 @@ public final class LasReader { ...@@ -238,13 +164,6 @@ public final class LasReader {
* Represents a functional interface that supplies a long value and allows for checked exceptions. * 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 * This is similar to {@link java.util.function.LongSupplier}, but it supports operations
* that may throw a checked exception. * 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 @FunctionalInterface
private interface LongSupplierEx { private interface LongSupplierEx {
...@@ -255,12 +174,6 @@ public final class LasReader { ...@@ -255,12 +174,6 @@ public final class LasReader {
* Represents a functional interface that supplies an integer array and may throw an 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, * 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. * 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 @FunctionalInterface
private interface IntArraySupplierEx { private interface IntArraySupplierEx {
...@@ -285,6 +198,5 @@ public final class LasReader { ...@@ -285,6 +198,5 @@ public final class LasReader {
long legacyPointCount, long legacyPointCount,
long pointCount, long pointCount,
int[] legacyByReturn int[] legacyByReturn
) { ) { }
}
} }
\ No newline at end of file
...@@ -17,52 +17,25 @@ ...@@ -17,52 +17,25 @@
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>. * 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;
/** public enum PlyClassification implements 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() { @Override
this(1024); public int getCode() {
return 0;
} }
public FloatGrowableBuffer(int initialCapacity) { @Override
this.data = new float[Math.max(16, initialCapacity)]; public String getLabel() {
this.size = 0; return "";
} }
public void add(float value) { @Override
ensureCapacity(size + 1); public String getSystem() {
data[size++] = value; return "";
}
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
...@@ -17,11 +17,11 @@ ...@@ -17,11 +17,11 @@
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>. * 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 de.hft.stuttgart.citydoctor2.datastructure.PointCloud;
import java.io.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Path; import java.nio.file.Path;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
...@@ -37,19 +37,10 @@ import java.util.ArrayList; ...@@ -37,19 +37,10 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
* A utility class for reading PLY (Polygon File Format) files and converting them into * The {@code PlyReader} class for reading PLY (Polygon File Format) files and converting them into
* a {@link PointCloud} object. Supports PLY files with both ASCII and binary * a {@link PointCloud} data model of citydoctor2. Supports PLY files with both ASCII and binary
* little-endian formats. * 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 * @author Radmir Gesler
*/ */
...@@ -63,7 +54,7 @@ public final class PlyReader { ...@@ -63,7 +54,7 @@ public final class PlyReader {
BINARY_LITTLE_ENDIAN BINARY_LITTLE_ENDIAN
} }
static PointCloud read(Path file) throws IOException { public static PointCloud read(Path file) throws IOException {
Header header = readHeader(file); Header header = readHeader(file);
if (header.vertexCount <= 0) { if (header.vertexCount <= 0) {
...@@ -73,26 +64,19 @@ public final class PlyReader { ...@@ -73,26 +64,19 @@ public final class PlyReader {
throw new IOException("PLY vertices must contain x, y, z properties: " + file); throw new IOException("PLY vertices must contain x, y, z properties: " + file);
} }
float[] x = new float[header.vertexCount]; PointCloud pointCloud = new PointCloud();
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()))) { try (InputStream is = new BufferedInputStream(new FileInputStream(file.toFile()))) {
skipHeaderBytes(is); skipHeaderBytes(is);
if (header.format == PlyFormat.ASCII) { if (header.format == PlyFormat.ASCII) {
readAsciiVertices(is, header, x, y, z, rgb); pointCloud = readAsciiVertices(is, header);
} else { } 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 { ...@@ -168,143 +152,165 @@ public final class PlyReader {
} }
/** /**
* Reads ASCII vertex data from the provided input stream and populates the given arrays * Reads a set of 3D points (vertices) in ASCII format from the provided input stream and
* with vertex positions and optionally RGB color values if the RGB array is supplied. * 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 is the input stream containing the ASCII encoded PLY vertex data.
* @param header The PLY header containing metadata such as vertex count and property definitions. * @param header the header object that describes the properties and format
* @param x An array to store the x-coordinates of the vertices. * of the PLY file, including vertex properties and their count.
* @param y An array to store the y-coordinates of the vertices. * @return a {@code PointCloud} object containing the parsed vertices with
* @param z An array to store the z-coordinates of the vertices. * their coordinates and optional color information.
* @param rgb An optional array to store the RGB color values of the vertices, where each value * @throws IOException if the input stream cannot be read, if mandatory vertex
* is a packed integer in the form 0xRRGGBB. Pass null if no color information is needed. * properties (x, y, z) are missing, or if the vertex data
* @throws IOException If an I/O error occurs, the end of the file is reached unexpectedly, or the format of * format or count is invalid.
* the vertex data does not match the expectations defined in the header.
*/ */
private static void readAsciiVertices(InputStream is, Header header, private static PointCloud readAsciiVertices(InputStream is, Header header) throws IOException {
float[] x, float[] y, float[] z, int[] rgb) 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");
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.US_ASCII)); 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++) { for (int i = 0; i < header.vertexCount; i++) {
String line = br.readLine(); String line;
while (line != null && line.trim().isEmpty()) { do { line = reader.readLine();
line = br.readLine(); } while (line != null && line.trim().isEmpty()); // Skips empty lines.
}
if (line == null) { 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+"); 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; double xCoord = 0.0, yCoord = 0.0, zCoord = 0.0;
int r = 0, g = 0, b = 0; int red = 0, green = 0, blue = 0;
for (int p = 0; p < header.vertexProperties.length; p++) { for (int p = 0; p < header.vertexProperties.length; p++) {
String prop = header.vertexProperties[p].name; String property = header.vertexProperties[p].name;
String val = parts[p]; String value = parts[p];
if (prop.equals("x")) { if (property.equals("x")) {
px = Float.parseFloat(val); xCoord = Double.parseDouble(value);
} else if (prop.equals("y")) { } else if (property.equals("y")) {
py = Float.parseFloat(val); yCoord = Double.parseDouble(value);
} else if (prop.equals("z")) { } else if (property.equals("z")) {
pz = Float.parseFloat(val); zCoord = Double.parseDouble(value);
} else if (rgb != null) { } else if (hasRgb) {
if (prop.equals("red")) { if (property.equals("red")) {
r = clamp255(parseIntSafe(val)); red = clamp255(parseIntSafe(value));
} else if (prop.equals("green")) { } else if (property.equals("green")) {
g = clamp255(parseIntSafe(val)); green = clamp255(parseIntSafe(value));
} else if (prop.equals("blue")) { } else if (property.equals("blue")) {
b = clamp255(parseIntSafe(val)); blue = clamp255(parseIntSafe(value));
} }
} }
} }
x[i] = px; if (!Double.isFinite(xCoord)
y[i] = py; || !Double.isFinite(yCoord)
z[i] = pz; || !Double.isFinite(zCoord))
if (rgb != null) { throw new IOException("Non-finite coordinates at PLY vertex " + i);
rgb[i] = (r << 16) | (g << 8) | b;
} 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 * Reads a binary little-endian encoded list of vertices from the given input stream,
* and populates the given arrays with vertex positions and optionally RGB color values * based on the properties defined in the PLY header.
* if the RGB array is supplied.
* *
* @param is The input stream positioned at the start of vertex data in a binary * @param is The input stream containing the binary little-endian vertex data.
* little-endian PLY file. * @param header The PLY header that describes the structure of the vertex data.
* @param header The PLY header containing metadata such as vertex count and property definitions. * Must include the properties "x", "y", and "z", and optionally
* @param x An array to store the x-coordinates of the vertices. The array must * "red", "green", and "blue" for RGB color information.
* have a size equal to the vertex count specified in the header. * @return A {@code PointCloud} object containing the vertices read from the input stream.
* @param y An array to store the y-coordinates of the vertices. The array must * Each vertex includes its coordinates and optional RGB color values.
* have a size equal to the vertex count specified in the header. * @throws IOException If there is an error reading the input stream, if the vertex count in
* @param z An array to store the z-coordinates of the vertices. The array must * the header is negative, if required properties (x, y, z) are missing,
* have a size equal to the vertex count specified in the header. * or if a property has an unsupported type or non-finite coordinates.
* @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, private static PointCloud readBinaryLittleEndianVertices(InputStream is, Header header)
float[] x, float[] y, float[] z, int[] rgb) throws IOException { 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); 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++) { for (int i = 0; i < header.vertexCount; i++) {
float px = 0f, py = 0f, pz = 0f; double xCoord = 0.0, yCoord = 0.0, zCoord = 0.0;
int r = 0, g = 0, b = 0; int red = 0, green = 0, blue = 0;
for (Property prop : header.vertexProperties) { for (Property prop : header.vertexProperties) {
switch (prop.type) { double value = switch (prop.type) {
case FLOAT -> { case FLOAT -> readFloatLE(din);
float v = readFloatLE(din); case DOUBLE -> readDoubleLE(din);
if (prop.name.equals("x")) px = v; case UCHAR -> din.readUnsignedByte();
else if (prop.name.equals("y")) py = v; case CHAR -> din.readByte();
else if (prop.name.equals("z")) pz = v; case USHORT -> readUShortLE(din);
} case SHORT -> (short) readUShortLE(din);
case DOUBLE -> { case UINT -> Integer.toUnsignedLong(readIntLE(din));
double dv = readDoubleLE(din); case INT -> readIntLE(din);
float v = (float) dv; default -> throw new IOException(
if (prop.name.equals("x")) px = v; "Unsupported binary PLY property type: " + prop.type);
else if (prop.name.equals("y")) py = v; };
else if (prop.name.equals("z")) pz = v;
} String property = prop.name;
case UCHAR, CHAR -> {
int v = din.readUnsignedByte(); if (property.equals("x")) {
if (rgb != null) { xCoord = value;
if (prop.name.equals("red")) r = v; } else if (property.equals("y")) {
else if (prop.name.equals("green")) g = v; yCoord = value;
else if (prop.name.equals("blue")) b = v; } else if (property.equals("z")) {
} zCoord = value;
} } else if (hasRgb) {
case USHORT, SHORT -> { if (property.equals("red")) {
readUShortLE(din); // currently ignored red = clamp255((int) value);
} } else if (property.equals("green")) {
case UINT, INT -> { green = clamp255((int) value);
readIntLE(din); // currently ignored } else if (property.equals("blue")) {
blue = clamp255((int) value);
} }
default -> throw new IOException("Unsupported binary PLY property type: " + prop.type);
} }
} }
x[i] = px; if (!Double.isFinite(xCoord)
y[i] = py; || !Double.isFinite(yCoord)
z[i] = pz; || !Double.isFinite(zCoord))
if (rgb != null) { throw new IOException("Non-finite coordinates at PLY vertex " + i);
rgb[i] = (r << 16) | (g << 8) | b;
} 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 @@ ...@@ -17,53 +17,25 @@
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>. * 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 {
;
/** @Override
* A dynamic buffer that holds short values and can grow automatically as new public int getCode() {
* elements are added. This buffer provides functionality for appending elements, return 0;
* 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) { @Override
ensureCapacity(size + 1); public String getLabel() {
data[size++] = value; return "";
} }
public int size() { @Override
return size; public String getSystem() {
} return "";
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
...@@ -18,8 +18,9 @@ ...@@ -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 de.hft.stuttgart.citydoctor2.datastructure.PointCloud;
import java.io.BufferedReader; import java.io.BufferedReader;
...@@ -58,10 +59,9 @@ public final class XyzReader { ...@@ -58,10 +59,9 @@ public final class XyzReader {
private XyzReader() { private XyzReader() {
} }
static PointCloud read(Path file) throws IOException { public static PointCloud read(Path file) throws IOException {
FloatGrowableBuffer xs = new FloatGrowableBuffer(16_384);
FloatGrowableBuffer ys = new FloatGrowableBuffer(16_384); PointCloud pointCloud = new PointCloud();
FloatGrowableBuffer zs = new FloatGrowableBuffer(16_384);
try (BufferedReader br = new BufferedReader( try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream(file.toFile()), StandardCharsets.UTF_8))) { new InputStreamReader(new FileInputStream(file.toFile()), StandardCharsets.UTF_8))) {
...@@ -79,20 +79,13 @@ public final class XyzReader { ...@@ -79,20 +79,13 @@ public final class XyzReader {
continue; continue;
} }
xs.add(Float.parseFloat(parts[0])); Point point = new Point(Double.parseDouble(parts[0]),
ys.add(Float.parseFloat(parts[1])); Double.parseDouble(parts[1]),
zs.add(Float.parseFloat(parts[2])); Double.parseDouble(parts[2]));
pointCloud.addPoint(point);
} }
} }
return new PointCloud( return pointCloud;
xs.toArray(),
ys.toArray(),
zs.toArray(),
null,
null,
null,
xs.size()
);
} }
} }
\ No newline at end of file
...@@ -18,8 +18,8 @@ ...@@ -18,8 +18,8 @@
*/ */
package de.hft.stuttgart.citydoctor2.parser; 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.datastructure.PointCloud;
import de.hft.stuttgart.citydoctor2.parser.pointcloud.PointCloudParser; import de.hft.stuttgart.citydoctor2.parser.pointcloud.PointCloudParser;
import org.junit.Test; import org.junit.Test;
...@@ -28,36 +28,47 @@ import java.io.IOException; ...@@ -28,36 +28,47 @@ import java.io.IOException;
import java.nio.file.Path; 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 * @author Radmir Gesler
*
*/ */
public class PointCloudParserTest { public class PointCloudParserTest {
@Test @Test
public void testPointCloudParsing() throws IOException { public void testPointCloudParsing() throws IOException {
testHelperForPointCloud("*.ply","src/test/resources/duedo_small.ply"); testHelperForPointCloud("*.ply","src/test/resources/point_clouds/duedo_small.ply");
testHelperForPointCloud("*.ply","src/test/resources/duedo_small_bin.ply"); testHelperForPointCloud("*.ply","src/test/resources/point_clouds/duedo_small_bin.ply");
testHelperForPointCloud("*.xyz","src/test/resources/duedo_small.xyz"); testHelperForPointCloud("*.xyz","src/test/resources/point_clouds/duedo_small.xyz");
testHelperForPointCloud("*.pts","src/test/resources/duedo_small.pts"); testHelperForPointCloud("*.pts","src/test/resources/point_clouds/duedo_small.pts");
testHelperForPointCloud("*.txt","src/test/resources/duedo_small.txt"); testHelperForPointCloud("*.txt","src/test/resources/point_clouds/duedo_small.txt");
testHelperForPointCloud("*.las","src/test/resources/duedo_small.las"); testHelperForPointCloud("*.las","src/test/resources/point_clouds/duedo_small.las");
testHelperForPointCloud("*.laz","src/test/resources/duedo_small.laz"); testHelperForPointCloud("*.laz","src/test/resources/point_clouds/duedo_small.laz");
} }
private void testHelperForPointCloud(String type, String file) throws IOException { private void testHelperForPointCloud(String type, String file) throws IOException {
System.out.println("===================================================================="); System.out.println("====================================================================");
System.out.println("Test " + type + " parsing from : " + file); System.out.println("Test " + type + " parsing from : " + file);
System.out.println("===================================================================="); System.out.println("====================================================================");
PointCloud pc = PointCloudParser.read(Path.of(file)); PointCloud pointCloud = PointCloudParser.read(Path.of(file));
if (pc.sanityCheck()) { if (sanityCheck(pointCloud)) {
System.out.println(" Sanity check passed"); System.out.println(" Sanity check passed");
System.out.println(" Loaded points: " + pc.size); System.out.println(" Loaded points: " + pointCloud.size());
System.out.println(" First point: " + pc.x[0] + ", " + pc.y[0] + ", " + pc.z[0]); 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"); System.out.println(" Test " + file + " passed");
} else { } else {
System.out.println(" Test " + file + " NOT passed"); 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