Skip to content

Commit

Permalink
Feat/native ply io (#418)
Browse files Browse the repository at this point in the history
* feat: native ply writer implementation (20 times faster than VTK)
* feat: native ply binary reader for triangleMesh
* feat: added writing support of VertexColorMesh3D
* feat: added support for ascii format
  • Loading branch information
Dennis Madsen authored Jan 22, 2024
1 parent 0c3ac5b commit c93cfde
Show file tree
Hide file tree
Showing 9 changed files with 656 additions and 132 deletions.
137 changes: 9 additions & 128 deletions src/main/scala/scalismo/io/MeshIO.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import scalismo.mesh.TriangleMesh.*
import scalismo.mesh.*
import scalismo.utils.{MeshConversion, TetrahedralMeshConversion}
import vtk.*
import scalismo.io.ply.PLY

import scala.reflect.ClassTag
import scala.util.{Failure, Success, Try}
Expand Down Expand Up @@ -87,11 +88,9 @@ object MeshIO {
case f if f.endsWith(".vtk") => readVTK(file)
case f if f.endsWith(".stl") => STL.read(file.toString)
case f if f.endsWith(".ply") => {
readPLY(file).map { res =>
res match {
case Right(vertexColor) => vertexColor.shape
case Left(shape) => shape
}
PLY.read(file).map {
case Right(vertexColor) => vertexColor.shape
case Left(shape) => shape
}
}
case _ =>
Expand All @@ -103,11 +102,9 @@ object MeshIO {
val filename = file.getAbsolutePath
filename match {
case f if f.endsWith(".ply") =>
readPLY(file).map { r =>
r match {
case Right(colorMesh3D) => colorMesh3D
case Left(_) => throw new Exception("Indicated PLY file does not contain color values.")
}
PLY.read(file).map {
case Right(colorMesh3D) => colorMesh3D
case Left(_) => throw new Exception("Indicated PLY file does not contain color values.")
}

case _ =>
Expand Down Expand Up @@ -362,8 +359,8 @@ object MeshIO {
filename match {
case f if f.endsWith(".h5") => writeHDF5(mesh, file)
case f if f.endsWith(".vtk") => writeVTK(mesh, file)
case f if f.endsWith(".ply") => PLY.write(mesh, file)
case f if f.endsWith(".stl") => STL.write(mesh, file.toString)
case f if f.endsWith(".ply") => writePLY(Left(mesh), file)
case _ =>
Failure(new IOException("Unknown file type received" + filename))
}
Expand All @@ -378,7 +375,7 @@ object MeshIO {
def writeVertexColorMesh3D(mesh: VertexColorMesh3D, file: File): Try[Unit] = {
val filename = file.getAbsolutePath
filename match {
case f if f.endsWith(".ply") => writePLY(Right(mesh), file)
case f if f.endsWith(".ply") => PLY.write(mesh, file)
case _ =>
Failure(new IOException("Unknown file type received" + filename))
}
Expand Down Expand Up @@ -426,57 +423,6 @@ object MeshIO {
err
}

private def writePLY(surface: Either[TriangleMesh[_3D], VertexColorMesh3D], file: File): Try[Unit] = {

val vtkPd = surface match {
case Right(colorMesh) => MeshConversion.meshToVtkPolyData(colorMesh.shape)
case Left(shapeOnly) => MeshConversion.meshToVtkPolyData(shapeOnly)
}

// add the colours if it is a vertex color
surface match {
case Right(colorMesh) => {

val vtkColors = new vtkUnsignedCharArray()
vtkColors.SetNumberOfComponents(4)

// Add the three colors we have created to the array
for (id <- colorMesh.shape.pointSet.pointIds) {
val color = colorMesh.color(id)
vtkColors.InsertNextTuple4((color.r * 255).toShort,
(color.g * 255).toShort,
(color.b * 255).toShort,
color.a * 255
)
}
vtkColors.SetName("RGBA")
vtkPd.GetPointData().SetScalars(vtkColors)

}
case _ => {}
}
val writer = new vtkPLYWriter()
writer.SetFileName(file.getAbsolutePath)
writer.SetArrayName("RGBA")
writer.SetComponent(0)
writer.SetEnableAlpha(true)
writer.SetInputData(vtkPd)
writer.SetColorModeToDefault()
writer.SetFileTypeToBinary()
writer.Update()

val succOrFailure = if (writer.GetErrorCode() != 0) {
Failure(
new IOException(s"could not write file ${file.getAbsolutePath} (received error code ${writer.GetErrorCode})")
)
} else {
Success(())
}
writer.Delete()
vtkPd.Delete()
succOrFailure
}

private def writeVTKPdasVTK(vtkPd: vtkPolyData, file: File): Try[Unit] = {
val writer = new vtkPolyDataWriter()
writer.SetFileName(file.getAbsolutePath)
Expand Down Expand Up @@ -532,71 +478,6 @@ object MeshIO {
}
}

private def readPLY(file: File): Try[Either[TriangleMesh[_3D], VertexColorMesh3D]] = {

// read the ply header to find out if the ply is a textured mesh in ASCII (in which case we return a failure since VTKPLYReader Update() would crash otherwise)

if (!file.exists()) {
val filename = file.getCanonicalFile
Failure(new IOException(s"Could not read ply file with name $filename. Reason: The file does not exist"))
} else {
val breader = new BufferedReader(new FileReader(file))
val lineIterator = Iterator.continually(breader.readLine())

val headerLines = lineIterator.dropWhile(_ != "ply").takeWhile(_ != "end_header").toIndexedSeq

if (headerLines.exists(_.contains("TextureFile")) && headerLines.exists(_.contains("format ascii"))) {
Failure(
new IOException(
"PLY file $filename seems to be a textured mesh in ASCII format which creates issues with the VTK ply reader. Please convert it to a binary ply or to a vertex color or shape only ply."
)
)
} else {
readPLYUsingVTK(file)
}
}
}

private def readPLYUsingVTK(file: File): Try[Either[TriangleMesh[_3D], VertexColorMesh3D]] = {
val filename = file.getCanonicalFile
val plyReader = new vtkPLYReader()
plyReader.SetFileName(file.getAbsolutePath)
plyReader.Update()

val errCode = plyReader.GetErrorCode()
if (errCode != 0) {
return Failure(new IOException(s"Could not read ply mesh $filename (received VTK error code $errCode"))
}

val vtkPd = plyReader.GetOutput()
val mesh = for {
meshGeometry <- MeshConversion.vtkPolyDataToTriangleMesh(vtkPd)
} yield {
getColorArray(vtkPd) match {
case Some(("RGBA", colorArray)) => {

val colors = for (i <- 0 until colorArray.GetNumberOfTuples().toInt) yield {
val rgba = colorArray.GetTuple4(i)
RGBA(rgba(0) / 255.0, rgba(1) / 255.0, rgba(2) / 255.0, rgba(3) / 255.0)
}
Right(VertexColorMesh3D(meshGeometry, new SurfacePointProperty[RGBA](meshGeometry.triangulation, colors)))
}
case Some(("RGB", colorArray)) => {
val colors = for (i <- 0 until colorArray.GetNumberOfTuples().toInt) yield {
val rgb = colorArray.GetTuple3(i)
RGBA(RGB(rgb(0) / 255.0, rgb(1) / 255.0, rgb(2) / 255.0))
}
Right(VertexColorMesh3D(meshGeometry, new SurfacePointProperty[RGBA](meshGeometry.triangulation, colors)))
}
case Some(_) => Left(meshGeometry)
case None => Left(meshGeometry)
}
}
plyReader.Delete()
vtkPd.Delete()
mesh
}

private def NDArrayToPointSeq(ndarray: NDArray[Double]): IndexedSeq[Point[_3D]] = {
// take block of 3, map them to 3dPoints and convert the resulting array to an indexed seq
ndarray.data.grouped(3).map(grp => Point(grp(0).toFloat, grp(1).toFloat, grp(2).toFloat)).toIndexedSeq
Expand Down
42 changes: 42 additions & 0 deletions src/main/scala/scalismo/io/ply/PLY.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2015 University of Basel, Graphics and Vision Research Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package scalismo.io.ply

import scalismo.geometry._3D
import scalismo.mesh.{TriangleMesh, VertexColorMesh3D}

import java.io.{File, IOException}
import scala.util.{Failure, Try}

object PLY {

def write(mesh: TriangleMesh[_3D], filename: File): Try[Unit] = {
PLYMeshWriter.write(mesh, None, filename)
}

def write(mesh: VertexColorMesh3D, filename: File): Try[Unit] = {
PLYMeshWriter.write(mesh.shape, Option(mesh.color.pointData.iterator), filename)
}

def read(file: File): Try[Either[TriangleMesh[_3D], VertexColorMesh3D]] = {
if (!file.exists()) {
val filename = file.getCanonicalFile
Failure(new IOException(s"Could not read ply file with name $filename. Reason: The file does not exist"))
} else {
PLYMeshReader.readFileAndParseHeader(file)
}
}
}
138 changes: 138 additions & 0 deletions src/main/scala/scalismo/io/ply/PLYHeader.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Copyright 2015 University of Basel, Graphics and Vision Research Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package scalismo.io.ply

import java.io.IOException
import scala.collection.mutable.ArrayBuffer
import scala.util.{Failure, Success, Try}

object PLYHeader {

def createHeader(numVertices: Int, numFaces: Int, vertexColors: Boolean): String = {
val header = new StringBuilder
header.append("ply\nformat binary_little_endian 1.0\ncomment Scalismo generated PLY File\n")
header.append(f"element vertex $numVertices\nproperty float x\nproperty float y\nproperty float z\n")
if (vertexColors) {
header.append("property uchar red\nproperty uchar green\nproperty uchar blue\nproperty uchar alpha\n")
}
header.append(f"element face $numFaces\nproperty list uchar int vertex_indices\nend_header\n")
header.toString()
}

def parseHeader(header: Array[String]): HeaderInfo = {
val fileComments: ArrayBuffer[String] = ArrayBuffer.empty[String]
var fileFormat: Option[PLYFormat] = None
val fileElements: ArrayBuffer[PLYElement] = ArrayBuffer.empty[PLYElement]

var cnt = 0
while (cnt < header.length) {
val line = header(cnt)
val lineSplit = line.split(" ")
val name = lineSplit.head
if (name == "comment" || name == "obj_info") {
fileComments += lineSplit.drop(1).mkString(" ")
} else if (lineSplit.length > 2) {
if (name == "format") {
lineSplit(1) match {
case PLY_FORMAT_ASCII =>
fileFormat = Option(PLYFormat(PLYTypeFormat.ASCII, lineSplit(2)))
case PLY_FORMAT_BIG =>
fileFormat = Option(PLYFormat(PLYTypeFormat.BINARY_BIG_ENDIAN, lineSplit(2)))
case PLY_FORMAT_LITTLE =>
fileFormat = Option(PLYFormat(PLYTypeFormat.BINARY_LITTLE_ENDIAN, lineSplit(2)))
case _ => Failure(new IOException(s"Unsupported PLY format"))
}
}
if (name == "element") {
val elementFormat = lineSplit(1) match {
case PLY_ELEMENT_VERTEX =>
Success(PLYElementFormat.VERTEX)
case PLY_ELEMENT_FACE =>
Success(PLYElementFormat.FACE)
case _ => Failure(new IOException(s"Unsupported property format"))
}
val elementCount = lineSplit(2).toInt
val elementProperties: ArrayBuffer[PLYProperty] = ArrayBuffer.empty[PLYProperty]
while (cnt < header.length - 1 && (header(cnt + 1).split(" ").head) == "property") {
val nextLine = header(cnt + 1).split(" ")
if (nextLine(1) == "list") {
elementProperties += PLYProperty(nextLine(2), nextLine(4), Option(nextLine(3)))
} else {
elementProperties += PLYProperty(nextLine(1), nextLine(2))
}
cnt += 1
}
fileElements += PLYElement(elementFormat.get, elementCount, elementProperties.toArray)
}
}
cnt += 1
}

val vertexInfo = fileElements.find(_.format == PLYElementFormat.VERTEX).get
val faceInfo = fileElements.find(_.format == PLYElementFormat.FACE).get
val faceDefined = validateElementFace(faceInfo)
val vertexDefined = validateElementVertex(vertexInfo)
if (!vertexDefined.status) {
throw new IOException(
"Unsupported element property provided"
)
} else if (!vertexDefined.is3DVertex) {
throw new IOException(
"Vertex (x,y,z) not defined in file."
)
} else if (!faceDefined) {
throw new IOException(
"Face element defined but no property defined."
)
}
HeaderInfo(
format = fileFormat.get,
vertexInfo = vertexInfo,
faceInfo = faceInfo,
comments = fileComments.toArray,
headerLength = header.map(_.getBytes("UTF-8").length).sum + header.length
)
}

private def validateElementFace(element: PLYElement): Boolean = {
if (element.count == 0) true
else {
element.properties.nonEmpty && element.properties.exists(prop => VERTEX_INDEX_PROPERTY_NAMES.contains(prop.name))
}
}

private def validateElementVertex(element: PLYElement): PLYItemsDefined = {
val xyz = Seq("x", "y", "z")
val n = Seq("nx", "ny", "nz")
val color = Seq("red", "green", "blue")
val colora = Seq("red", "green", "blue", "alpha")
val st = Seq("s", "t")
val uv = Seq("u", "v")
val texture = Seq("texture_u", "texture_v")
val all = xyz ++ n ++ color ++ colora ++ st ++ uv ++ texture
val names = element.properties.map(_.name)
val is3DVertexDefined = xyz.forall(names.contains)
val is3DNormalDefined = n.forall(names.contains)
val is3DVertexColorDefined = color.forall(names.contains) || colora.forall(names.contains)
val is3DUVsDefined =
st.forall(names.contains) ||
uv.forall(names.contains) ||
texture.forall(names.contains)
val status = element.properties.map(_.name).forall(item => all.contains(item))
PLYItemsDefined(status, is3DVertexDefined, is3DNormalDefined, is3DVertexColorDefined, is3DUVsDefined)
}

}
Loading

0 comments on commit c93cfde

Please sign in to comment.