一、背景
一张图片是由一个一个像素点组成的,每一个像素点有特定的下标及色彩值,这样我们就可以把一张图片看作是准三维点云。之所以叫准三维,是因为每个(x,y)只能对应一个z值。在Matlab程序中,用imread命令导入一张图片,会发现导入的结果是一个矩阵。用surf命令可以很直观地看到这个三维点云构成的三维曲面。实际上,这个矩阵已经能够非常清晰地描述这个三维曲面了。然而,事实就是这么残酷。有些商业软件只能识别特定的三维曲面,比如非常常见的STL文件。STL文件有二进制和ASCII两种编码方式,这两种方式各有优缺点:二进制编码文件体积小,但文本很不直观;ASCII编码非常直观,相应的文件体积较大。通常二进制编码的体积为ASCII编码的五分之一,而不管什么编码都遵循一定的规则。规则写好了,软件识别起来就没有区别了,所以有些比较坑的软件还只支持二进制编码的STL文件。
在逆向工程上,如果测量软件没有相应的功能的话,测出来的结果就会是一个二维矩阵。矩阵下标表示xy位置,值表示高度。三维点云有相应的软件处理成三维曲面,一般这样的软件功能都很强大,当然也不是免费的,体积也会比较大,运气不好还破解不了。因此,有必要采用一种简便的转换方法。
二、解决思路
回到Matlab的surf命令,对得到的三维曲面进行放大,会发现这个曲面是由一个一个四边形构成的。这就给我们提供了一个思路:每四个点可以构成两个三角形,而三角形正好的STL文件的基础。
三、C#代码实现
我们用一个double类型的二维数组来表示一个曲面,数组的下标表示xy坐标,值表示z坐标。四个点可以表示两个三角形,通过二重循环来遍历所有的点,如下图所示:
SLM文件中,一个三角形由一个法向量和三个点构成,点的坐标可以由数组得到,但法向量必须根据这三个点来求。将这个算法写成一个函数,方便调用。
具体实现见代码:
private static double[] Vectorxyz(double x1, double y1, double z1,
double x2, double y2, double z2,
double x3, double y3, double z3)
{
var A = y1 * z2 - y1 * z3 - z1 * y2 + z1 * y3 + y2 * z3 - y3 * z2;
var B = -x1 * z2 + x1 * z3 + z1 * x2 - z1 * x3 - x2 * z3 + x3 * z2;
var C = x1 * y2 - x1 * y3 - y1 * x2 + y1 * x3 + x2 * y3 - x3 * y2;
var vectorLength = Math.Sqrt(A * A + B * B + C * C);
return new double[] { A / vectorLength, B / vectorLength, C / vectorLength };
}
随后,我们用一个二重循环将所有点构成三角形写入文件。以下代码将三角形以ASCII编码写入STL文件中。代码中,filename为要写入的文件名,如"test.stl" 。inputMatrix为double类型的二维数组,xmax和ymax分别为这个数组第一维和第二维长度。也可以手动指定更小的值,即将其中一部分转换成STL文件。
StreamWriter sw = new StreamWriter(filename);
sw.WriteLine("solid XW STL");
double[] vectorxyz = new double[3];
for (int x = 0; x < xmax - 1; x++)
{
for (int y = 0; y < ymax; y++)
{
vectorxyz = Vectorxyz(x, y, inputMatrix[x, y],
x, y + 1, inputMatrix[x, y + 1],
x + 1, y + 1, inputMatrix[x + 1, y + 1]);
sw.WriteLine($" facet normal {vectorxyz[0].ToString("E")} {vectorxyz[1].ToString("E")} {vectorxyz[2].ToString("E")}");
sw.WriteLine(" outer loop");
sw.WriteLine($" vertex {x.ToString("E")} {y.ToString("E")} {inputMatrix[x, y].ToString("E")}");
sw.WriteLine($" vertex {x.ToString("E")} {(y + 1).ToString("E")} {inputMatrix[x, y + 1].ToString("E")}");
sw.WriteLine($" vertex {(x + 1).ToString("E")} {(y + 1).ToString("E")} {inputMatrix[x + 1, y + 1].ToString("E")}");
sw.WriteLine(" endloop");
sw.WriteLine(" endfacet");
vectorxyz = Vectorxyz(x, y, inputMatrix[x, y],
x + 1, y + 1, inputMatrix[x + 1, y + 1],
x + 1, y, inputMatrix[x + 1, y]);
sw.WriteLine($" facet normal {vectorxyz[0].ToString("E")} {vectorxyz[1].ToString("E")} {vectorxyz[2].ToString("E")}");
sw.WriteLine(" outer loop");
sw.WriteLine($" vertex {x.ToString("E")} {y.ToString("E")} {inputMatrix[x, y].ToString("E")}");
sw.WriteLine($" vertex {(x + 1).ToString("E")} {(y + 1).ToString("E")} {inputMatrix[x + 1, y + 1].ToString("E")}");
sw.WriteLine($" vertex {(x + 1).ToString("E")} {y.ToString("E")} {inputMatrix[x + 1, y].ToString("E")}");
sw.WriteLine(" endloop");
sw.WriteLine(" endfacet");
}
}
sw.WriteLine("endsolid XW STL");
sw.Close();
以上面上示意图为例,我们将其生成STL文件,如下图所示:
二进制编码与ASCII编码类似,区别在于写入二进制文件,去掉了很多多余的信息,因此能够做到ASCII体积的五分之一,代码就是可以性差。
两者的大小对比如下图所示:
大家可以自己研究一下STL文件的二进制编码方式,按照类似的方法即可实现,也可以评论区留言。