Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 71 additions & 62 deletions src/main/java/net/imagej/ops/geom/geom3d/DefaultVoxelization3D.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,89 +75,98 @@ public class DefaultVoxelization3D extends AbstractUnaryFunctionOp<Mesh, RandomA
@Parameter
private OpService ops;

// Constants
private static final double EPSILON = 1e-6;

@Override
public RandomAccessibleInterval<BitType> calculate(Mesh input) {

Img<BitType> outImg = ops.create().img(new FinalInterval(width, height, depth), new BitType());

Vertices verts = input.vertices();
RealPoint minPoint = new RealPoint(3);
RealPoint maxPoint = new RealPoint(3);

RealPoint minPoint = new RealPoint(verts.iterator().next());
RealPoint maxPoint = new RealPoint(verts.iterator().next());
// Initialize minPoint and maxPoint with the first vertex
minPoint.setPosition(verts.iterator().next());
maxPoint.setPosition(verts.iterator().next());

// Calculate min and max with padding
double[] padding = {0.5, 0.5, 0.5};
for (RealLocalizable v : verts) {
if (v.getDoublePosition(0) < minPoint.getDoublePosition(0))
minPoint.setPosition(v.getDoublePosition(0), 0);
if (v.getDoublePosition(1) < minPoint.getDoublePosition(1))
minPoint.setPosition(v.getDoublePosition(1), 1);
if (v.getDoublePosition(2) < minPoint.getDoublePosition(2))
minPoint.setPosition(v.getDoublePosition(2), 2);

if (v.getDoublePosition(0) > maxPoint.getDoublePosition(0))
maxPoint.setPosition(v.getDoublePosition(0), 0);
if (v.getDoublePosition(1) > maxPoint.getDoublePosition(1))
maxPoint.setPosition(v.getDoublePosition(1), 1);
if (v.getDoublePosition(2) > maxPoint.getDoublePosition(2))
maxPoint.setPosition(v.getDoublePosition(2), 2);
for (int d = 0; d < 3; d++) {
if (v.getDoublePosition(d) < minPoint.getDoublePosition(d)) {
minPoint.setPosition(v.getDoublePosition(d) - padding[d], d);
}
if (v.getDoublePosition(d) > maxPoint.getDoublePosition(d)) {
maxPoint.setPosition(v.getDoublePosition(d) + padding[d], d);
}
}
}

RealPoint dimPoint = new RealPoint((maxPoint.getDoublePosition(0) - minPoint.getDoublePosition(0)),
(maxPoint.getDoublePosition(1) - minPoint.getDoublePosition(1)),
(maxPoint.getDoublePosition(2) - minPoint.getDoublePosition(2)));

// Calculate dimPoint and step sizes
double[] dimPoint = new double[3];
double[] stepSizes = new double[3];
stepSizes[0] = dimPoint.getDoublePosition(0) / width;
stepSizes[1] = dimPoint.getDoublePosition(1) / height;
stepSizes[2] = dimPoint.getDoublePosition(2) / depth;

double[] voxelHalfsize = new double[3];
for (int k = 0; k < stepSizes.length; k++)
voxelHalfsize[k] = stepSizes[k] / 2.0;
for (int d = 0; d < 3; d++) {
dimPoint[d] = maxPoint.getDoublePosition(d) - minPoint.getDoublePosition(d);
stepSizes[d] = dimPoint[d] / new int[]{width, height, depth}[d];
voxelHalfsize[d] = stepSizes[d] / 2.0;
}

for (final Triangle tri : input.triangles()) {
final Vector3D v1 = new Vector3D(tri.v0x(), tri.v0y(), tri.v0z());
final Vector3D v2 = new Vector3D(tri.v1x(), tri.v1y(), tri.v1z());
final Vector3D v3 = new Vector3D(tri.v2x(), tri.v2y(), tri.v2z());

double[] minSubBoundary = new double[] {
Math.min(Math.min(v1.getX(), v2.getX()), v3.getX()) - minPoint.getDoublePosition(0),
Math.min(Math.min(v1.getY(), v2.getY()), v3.getY()) - minPoint.getDoublePosition(1),
Math.min(Math.min(v1.getZ(), v2.getZ()), v3.getZ()) - minPoint.getDoublePosition(2) };
double[] maxSubBoundary = new double[] {
Math.max(Math.max(v1.getX(), v2.getX()), v3.getX()) - minPoint.getDoublePosition(0),
Math.max(Math.max(v1.getY(), v2.getY()), v3.getY()) - minPoint.getDoublePosition(1),
Math.max(Math.max(v1.getZ(), v2.getZ()), v3.getZ()) - minPoint.getDoublePosition(2) };

RandomAccess<BitType> ra = outImg.randomAccess();// Should use the
// interval
// implementation
// for speed

long[] indices = new long[3];
for (indices[0] = (long) Math.floor(minSubBoundary[0] / stepSizes[0]); indices[0] < Math
.floor(maxSubBoundary[0] / stepSizes[0]); indices[0]++) {
for (indices[1] = (long) Math.floor(minSubBoundary[1] / stepSizes[1]); indices[1] < Math
.floor(maxSubBoundary[1] / stepSizes[1]); indices[1]++) {
for (indices[2] = (long) Math.floor(minSubBoundary[2] / stepSizes[2]); indices[2] < Math
.floor(maxSubBoundary[2] / stepSizes[2]); indices[2]++) {
ra.setPosition(indices);
if (!ra.get().get())// Don't check if voxel is already
// filled
{
double[] voxelCenter = new double[3];

for (int k = 0; k < 3; k++)
voxelCenter[k] = indices[k] * stepSizes[k] + voxelHalfsize[k];

if (triBoxOverlap(voxelCenter, voxelHalfsize, v1, v2, v3) == 1) {
Vector3D v1 = new Vector3D(tri.v0x(), tri.v0y(), tri.v0z());
Vector3D v2 = new Vector3D(tri.v1x(), tri.v1y(), tri.v1z());
Vector3D v3 = new Vector3D(tri.v2x(), tri.v2y(), tri.v2z());

double[] minSubBoundary = {
Math.min(v1.getX(), Math.min(v2.getX(), v3.getX())) - minPoint.getDoublePosition(0),
Math.min(v1.getY(), Math.min(v2.getY(), v3.getY())) - minPoint.getDoublePosition(1),
Math.min(v1.getZ(), Math.min(v2.getZ(), v3.getZ())) - minPoint.getDoublePosition(2)
};

double[] maxSubBoundary = {
Math.max(v1.getX(), Math.max(v2.getX(), v3.getX())) - minPoint.getDoublePosition(0),
Math.max(v1.getY(), Math.max(v2.getY(), v3.getY())) - minPoint.getDoublePosition(1),
Math.max(v1.getZ(), Math.max(v2.getZ(), v3.getZ())) - minPoint.getDoublePosition(2)
};

int[][] indicesRange = new int[3][2];
for (int d = 0; d < 3; d++) {
indicesRange[d][0] = (int) Math.max(0, Math.floor(minSubBoundary[d] / stepSizes[d]));
indicesRange[d][1] = (int) Math.min(new int[]{width, height, depth}[d], Math.ceil(maxSubBoundary[d] / stepSizes[d]) + 1);
}

double[][] corners = {
{-0.5, -0.5, -0.5}, {0.5, -0.5, -0.5}, {-0.5, 0.5, -0.5}, {0.5, 0.5, -0.5},
{-0.5, -0.5, 0.5}, {0.5, -0.5, 0.5}, {-0.5, 0.5, 0.5}, {0.5, 0.5, 0.5}
};

RandomAccess<BitType> ra = outImg.randomAccess();
for (int i = indicesRange[0][0]; i < indicesRange[0][1]; i++) {
for (int j = indicesRange[1][0]; j < indicesRange[1][1]; j++) {
for (int k = indicesRange[2][0]; k < indicesRange[2][1]; k++) {
ra.setPosition(new int[]{i, j, k});
if (!ra.get().get()) {
boolean intersected = false;
double[] voxelCenter = {i * stepSizes[0], j * stepSizes[1], k * stepSizes[2]};
for (double[] corner : corners) {
double[] shiftedCenter = new double[3];
for (int d = 0; d < 3; d++) {
shiftedCenter[d] = voxelCenter[d] + corner[d] * voxelHalfsize[d];
}
if (triBoxOverlap(shiftedCenter, voxelHalfsize, v1, v2, v3) == 1) {
intersected = true;
break;
}
}
if (intersected) {
ra.get().set(true);
}
}
}
}
}
}

return outImg;
}

Expand Down
96 changes: 81 additions & 15 deletions src/test/java/net/imagej/ops/geom/MeshFeatureTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,24 @@

import java.util.Iterator;

import ij.IJ;
import net.imagej.mesh.Mesh;
import net.imagej.mesh.Triangle;
import net.imagej.ops.Ops;
import net.imagej.ops.features.AbstractFeatureTest;
import net.imagej.ops.geom.geom3d.DefaultBoxivityMesh;
import net.imagej.ops.geom.geom3d.DefaultCompactness;
import net.imagej.ops.geom.geom3d.DefaultConvexityMesh;
import net.imagej.ops.geom.geom3d.DefaultMainElongation;
import net.imagej.ops.geom.geom3d.DefaultMarchingCubes;
import net.imagej.ops.geom.geom3d.DefaultMedianElongation;
import net.imagej.ops.geom.geom3d.DefaultSolidityMesh;
import net.imagej.ops.geom.geom3d.DefaultSparenessMesh;
import net.imagej.ops.geom.geom3d.DefaultSphericity;
import net.imagej.ops.geom.geom3d.DefaultSurfaceArea;
import net.imagej.ops.geom.geom3d.DefaultSurfaceAreaConvexHullMesh;
import net.imagej.ops.geom.geom3d.DefaultVerticesCountConvexHullMesh;
import net.imagej.ops.geom.geom3d.DefaultVerticesCountMesh;
import net.imagej.ops.geom.geom3d.DefaultVolumeConvexHullMesh;
import net.imagej.ops.geom.geom3d.DefaultVolumeMesh;
import net.imagej.ops.geom.geom3d.*;
import net.imagej.ops.morphology.fillHoles.DefaultFillHoles;
import net.imagej.ops.morphology.floodFill.DefaultFloodFill;
import net.imglib2.Cursor;
import net.imglib2.RandomAccessibleInterval;
import net.imglib2.img.Img;
import net.imglib2.img.array.ArrayImgs;
import net.imglib2.img.display.imagej.ImageJFunctions;
import net.imglib2.roi.labeling.LabelRegion;
import net.imglib2.type.logic.BitType;
import net.imglib2.type.numeric.real.DoubleType;

import net.imglib2.view.Views;
import org.junit.BeforeClass;
import org.junit.Test;

Expand Down Expand Up @@ -199,8 +195,78 @@ public void verticesCountMesh() {

}

/**
* Creates a 3D binary image of a sphere.
*
* @param r The radius of the sphere.
* @return A RandomAccessibleInterval representing the sphere.
*/
public RandomAccessibleInterval<BitType> generateSphere(int r) {
long[] dims = new long[] {2*r, 2*r, 2*r}; // Dimensions of the bounding box of the sphere
Img<BitType> sphereImg = ArrayImgs.bits(dims);

Cursor<BitType> cursor = sphereImg.localizingCursor();

// Center of the sphere
int cx = r;
int cy = r;
int cz = r;

while (cursor.hasNext()) {
cursor.fwd();
int x = cursor.getIntPosition(0) - cx;
int y = cursor.getIntPosition(1) - cy;
int z = cursor.getIntPosition(2) - cz;

if (x * x + y * y + z * z <= r * r) {
cursor.get().set(true);
}
}

return sphereImg;
}

public long compareImages(RandomAccessibleInterval<BitType> img1, RandomAccessibleInterval<BitType> img2) {
long diff = 0;
Cursor<BitType> cursor1 = Views.iterable(img1).cursor();
Cursor<BitType> cursor2 = Views.iterable(img2).cursor();

while (cursor1.hasNext() && cursor2.hasNext()) {
cursor1.fwd();
cursor2.fwd();

if (!cursor1.get().valueEquals(cursor2.get())) {
diff++;
}
}
return diff;
}

@Test
public void voxelization3D() {
// https://github.com/imagej/imagej-ops/issues/422
RandomAccessibleInterval<BitType> sphere = generateSphere(50);
final Mesh result = (Mesh) ops.run(DefaultMarchingCubes.class, sphere);

// The mesh is good by now, let's check the voxelization
RandomAccessibleInterval<BitType> voxelization = (RandomAccessibleInterval<BitType>) ops.run(DefaultVoxelization3D.class, result, sphere.dimension(0), sphere.dimension(1), sphere.dimension(2));

// Flood fill (ops implementation starts from borders)
RandomAccessibleInterval<BitType> filledVoxelization = (RandomAccessibleInterval<BitType>) ops.run(DefaultFillHoles.class, voxelization);

// Comparison
long diff = compareImages(sphere, filledVoxelization);
long total = 0;

Cursor<BitType> cursor = Views.iterable(sphere).cursor();
while (cursor.hasNext()) {
cursor.fwd();
if (cursor.get().get()) {
total++;
}
}

assertTrue("Voxelization does not match the original image closely enough.", diff / (double) total < 0.085);

}
}