Skip to content

Commit

Permalink
Feat/native stl io (#419)
Browse files Browse the repository at this point in the history
* feat: simple binary stl writer added
* feat: simple binary stl reader added
* feat: simple ascii stl reader added
---------

Co-authored-by: dennis.madsen <dennis.madsen@unibas.ch>
Co-authored-by: Andreas Morel-Forster <Forster.Andreas@gmail.com>
  • Loading branch information
3 people authored Jan 22, 2024
1 parent 7da793f commit 0c3ac5b
Show file tree
Hide file tree
Showing 11 changed files with 6,500 additions and 55 deletions.
15 changes: 15 additions & 0 deletions src/main/scala/scalismo/io/FileReader.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package scalismo.io

import java.io.RandomAccessFile
import java.nio.ByteBuffer

object FileReader {
def readFileToByteBuffer(file: String): ByteBuffer = {
val raf = new RandomAccessFile(file, "r")
val channel = raf.getChannel
val buffer = ByteBuffer.allocate(channel.size.toInt)
channel.read(buffer)
buffer.rewind()
buffer
}
}
58 changes: 3 additions & 55 deletions src/main/scala/scalismo/io/MeshIO.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import scalismo.common.{PointId, Scalar, UnstructuredPoints}
import scalismo.geometry.*
import scalismo.hdf5json.HDFPath
import scalismo.io.statisticalmodel.{NDArray, StatisticalModelIOUtils}
import scalismo.io.stl.STL
import scalismo.mesh.TriangleMesh.*
import scalismo.mesh.*
import scalismo.utils.{MeshConversion, TetrahedralMeshConversion}
Expand Down Expand Up @@ -84,7 +85,7 @@ object MeshIO {
val filename = file.getAbsolutePath
filename match {
case f if f.endsWith(".vtk") => readVTK(file)
case f if f.endsWith(".stl") => readSTL(file)
case f if f.endsWith(".stl") => STL.read(file.toString)
case f if f.endsWith(".ply") => {
readPLY(file).map { res =>
res match {
Expand Down Expand Up @@ -361,7 +362,7 @@ 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(".stl") => writeSTL(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 Down Expand Up @@ -425,13 +426,6 @@ object MeshIO {
err
}

def writeSTL(surface: TriangleMesh[_3D], file: File): Try[Unit] = {
val vtkPd = MeshConversion.meshToVtkPolyData(surface)
val err = writeVTKPdAsSTL(vtkPd, file)
vtkPd.Delete()
err
}

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

val vtkPd = surface match {
Expand Down Expand Up @@ -500,23 +494,6 @@ object MeshIO {
succOrFailure
}

private def writeVTKPdAsSTL(vtkPd: vtkPolyData, file: File): Try[Unit] = {
val writer = new vtkSTLWriter()
writer.SetFileName(file.getAbsolutePath)
writer.SetInputData(vtkPd)
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()
succOrFailure
}

private def readVTKPolydata(file: File): Try[vtkPolyData] = {

val vtkReader = new vtkPolyDataReader()
Expand Down Expand Up @@ -544,35 +521,6 @@ object MeshIO {
}
}

private def readSTL(file: File, correctMesh: Boolean = false): Try[TriangleMesh[_3D]] = {
val stlReader = new vtkSTLReader()
stlReader.SetFileName(file.getAbsolutePath)

stlReader.MergingOn()

// With the default point locator, it may happen that the stlReader merges
// points that are very close by but not identical. To make sure that this never happens
// we explicitly specify the tolerance.
val pointLocator = new vtkMergePoints()
pointLocator.SetTolerance(0.0)

stlReader.SetLocator(pointLocator)
stlReader.Update()
val errCode = stlReader.GetErrorCode()
if (errCode != 0) {
return Failure(new IOException(s"Could not read stl mesh (received error code $errCode"))
}

val vtkPd = stlReader.GetOutput()
val mesh =
if (correctMesh) MeshConversion.vtkPolyDataToCorrectedTriangleMesh(vtkPd)
else MeshConversion.vtkPolyDataToTriangleMesh(vtkPd)

stlReader.Delete()
vtkPd.Delete()
mesh
}

private def getColorArray(polyData: vtkPolyData): Option[(String, vtkDataArray)] = {
if (polyData.GetPointData() == null || polyData.GetPointData().GetNumberOfArrays() == 0) None
else {
Expand Down
43 changes: 43 additions & 0 deletions src/main/scala/scalismo/io/stl/STL.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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.stl

import scalismo.geometry.{EuclideanVector3D, Point3D}
import scalismo.mesh.TriangleMesh3D

import java.io.{BufferedReader, FileReader}
import java.nio.ByteOrder
import scala.util.Try

object STL {
// Binary numbers are assumed to be little-endian in the STL format.
private[stl] val STL_BYTE_ORDER = ByteOrder.LITTLE_ENDIAN
private[stl] val STL_HEADER_LENGTH = 80

def write(mesh: TriangleMesh3D, filename: String): Try[Unit] = {
STLMeshWriter.write(mesh, filename, "Scalismo generated STL File")
}

def read(filename: String): Try[TriangleMesh3D] = {
val breader = new BufferedReader(new FileReader(filename))
val fileType = breader.readLine().take(5)
if (fileType == "solid") {
STLMeshReaderAscii.read(filename)
} else {
STLMeshReaderBinary.read(filename)
}
}
}
96 changes: 96 additions & 0 deletions src/main/scala/scalismo/io/stl/STLMeshReaderAscii.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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.stl

import scalismo.geometry.{EuclideanVector, EuclideanVector3D, Point3D}
import scalismo.mesh.TriangleMesh3D

import java.io.{BufferedReader, FileReader, IOException}
import scala.collection.mutable.ArrayBuffer
import scala.util.Try

object STLMeshReaderAscii {
def read(file: String): Try[TriangleMesh3D] = Try {
val breader = new BufferedReader(new FileReader(file))
val header = breader.readLine()

val triangles = ArrayBuffer.empty[STLTriangle]
var line: String = null
def hasLineToProcess: Boolean = {
line = breader.readLine();
line != null && !line.trim.startsWith("endsolid")
}
while (hasLineToProcess) {
line = line.trim.replaceAll(" +", " ")
val triangleStrings: Array[String] = Array(line) ++
(0 until 6).map(_ => breader.readLine().trim.replaceAll(" +", " ")).toArray
val triangle = parseTriangleStrings(triangleStrings)
triangles += triangle
}
breader.close()

STLTriangle.STLTrianglesToTriangleMesh(triangles.toSeq)
}

private def parseTriangleStrings(data: Seq[String]): STLTriangle = {
if (data.length != 7) {
throw new IOException("Wrong faces description format does not include all 7 descriptors.")
}

checkTag(data(1), "outer loop")
checkTag(data(5), "endloop")
checkTag(data(6), "endfacet")

STLTriangle(
parseNormalString(data(0)),
parseVertexString(data(2)),
parseVertexString(data(3)),
parseVertexString(data(4))
)
}

private def checkTag(part: String, tag: String): Unit = {
if (!part.startsWith(tag)) {
throw new IOException(f"Wrong start of line, expected ${tag} but start is ${part.take(tag.length)}")
}
}

private def splitChecked(part: String, tag: String, nElements: Int) = {
if (!part.startsWith(tag)) {
throw new IOException(f"Wrong identifier at beginning, expected ${tag} but start is ${part.take(tag.length)}")
}

val parts = part.replace(tag, "").trim.split(" ")
if (parts.length != nElements) {
throw new IOException(f"Wrong number of elements for ${tag}. Found ${parts.length}, expected ${nElements}")
}
parts
}

private def parseNormalString(part: String): EuclideanVector3D = {
val tag = "facet normal"
val expectedValues = 3
val parts = splitChecked(part, tag, expectedValues).map(_.toDouble)
EuclideanVector3D(parts(0), parts(1), parts(2))
}

private def parseVertexString(part: String): Point3D = {
val tag = "vertex"
val expectedValues = 3
val parts = splitChecked(part, tag, expectedValues).map(_.toDouble)
Point3D(parts(0), parts(1), parts(2))
}
}
61 changes: 61 additions & 0 deletions src/main/scala/scalismo/io/stl/STLMeshReaderBinary.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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.stl

import scalismo.geometry.Point3D
import scalismo.io.FileReader
import scalismo.io.stl.STL.{STL_BYTE_ORDER, STL_HEADER_LENGTH}
import scalismo.io.stl.STLTriangle
import scalismo.mesh.TriangleMesh3D

import java.nio.ByteBuffer
import scala.util.Try

object STLMeshReaderBinary {
def read(file: String): Try[TriangleMesh3D] = Try {
val dataBuffer = FileReader.readFileToByteBuffer(file)
dataBuffer.position(STL_HEADER_LENGTH).order(STL_BYTE_ORDER)

val numTriangles = readInt(dataBuffer)
val trianglesArray = new Array[STLTriangle](numTriangles)

val triangles = (0 until numTriangles).map { _ =>
val n = readVertex(dataBuffer).toVector
val p1 = readVertex(dataBuffer)
val p2 = readVertex(dataBuffer)
val p3 = readVertex(dataBuffer)
readShort(dataBuffer)
STLTriangle(n, p1, p2, p3)
}
STLTriangle.STLTrianglesToTriangleMesh(triangles)
}

private def readShort(bb: ByteBuffer): Short = {
bb.getShort
}

private def readInt(bb: ByteBuffer): Int = {
bb.getInt
}

private def readVertex(bb: ByteBuffer): Point3D = {
Point3D(readFloat(bb), readFloat(bb), readFloat(bb))
}

private def readFloat(bb: ByteBuffer): Float = {
bb.getFloat
}
}
Loading

0 comments on commit 0c3ac5b

Please sign in to comment.