Spin-Image
Spin-Image
Spin-Image(自转图像)是一种表面表示技术,主要用于三维场景下的表面匹配和目标识别。
Spin-Image使用面向对象的坐标系统对任意表面的全局属性进行编码。
Spin-Image 生成算法
-
定义Oriented Point: Oriented Point是带有方向的三维表面点(由曲面顶点的三维位置\(p\)和曲面法线\(n\)所组成)
-
以Oriented Point为轴生成一个圆柱坐标系:点\(x\)在此坐标系表示为\((\alpha,\beta)\),其中\(\alpha\)为点\(x\)到到中心轴线\(L\)的(非负)垂直距离,\(\beta\)为点\(x\)到切平面\(P\)的符号距离。
-
定义Spin-Image的参数:Spin-Image是一个具有一定大小(行列数),分辨率(二维网格大小)的二维图像。
Spin-Image 大小(size)使用以下公式进行计算:
\[i_{\max }=\frac{2 \beta_{\max }}{b}+1 ,\quad j_{\max }=\frac{a_{\max }}{b}+1 \]其中\(\alpha_{max},\beta_{max}\)为所有不同oriented point基的\(\alpha ,| \beta|\)的最大值,其中\(b\)为桶的宽度。
分辨率,即桶的宽度,通常取网格所有边的平均值:
\[b=r=\frac{1}{N} \sum_{i=1}^{N}\left|e_i\right| \] -
Snip map(\(S_0\)):将圆柱体中的三维坐标投影到二维Spin-Image之中,这一过程可以理解为一个Spin-Image绕法向量\(n\)旋转360度,扫到的三维空间上的点会落到Spin-Image,使用如下公式:
\[\begin{gathered} S_{O}: R^{3} \rightarrow R^{2} \\ S_{O}(x) \rightarrow(a, \beta)=\left(\sqrt{\|x-p\|^{2}-\left(n \cdot(x-p))^{2}\right.}, n \cdot(x-p)\right) \end{gathered} \]我们仅仅需要一个oriented point即可以生成一个spin-Image,我们生成一个2维数组用来表示生成的spin-Image。为了生成spin-Image,我们将2D的点\((\alpha,\beta)\)的结果累加到离散的桶之中。
同时我们需要考虑数据中的噪声影响,因此2-D点的贡献值通过双线性插值扩散到周围的4个桶之中。
2-D点\((\alpha,\beta)\)使用下面的公式累加到离散的桶:
\[i=\left\lfloor\frac{\beta_{\max }-\beta}{b}\right\rfloor \quad j=\left\lfloor\frac{a}{b}\right\rfloor \]其中,用来计算对应网格强度的双线性的权重为:
\[a=(\beta_{\max }-\beta)-i b\ \ \ b=\alpha-j b \] -
这样我们便可以获得spin-Image,如下图所示:
Spin-Image实现(unity)
生成图片参数
public float CalcMeshResolution(Vector3[] vertices, int[] triangles)
{
float totalLength = 0;
int verticesNums = vertices.Length;
for (int i = 0; i < verticesNums / 3; i += 3)
{
Vector3 p0 = vertices[triangles[i * 3]];
Vector3 p1 = vertices[triangles[i * 3 + 1]];
Vector3 p2 = vertices[triangles[i * 3 + 2]];
totalLength += (p0 - p1).magnitude;
totalLength += (p0 - p2).magnitude;
totalLength += (p1 - p2).magnitude;
}
Debug.Log("Calc Resolution Finish!");
return totalLength / (verticesNums / 3);
}
public void CreateSpinImageParameters(Mesh modelMesh)
{
binSize = CalcMeshResolution(modelMesh.vertices, modelMesh.triangles);
//calc spin-image size
Vector3 boundMin = modelMesh.bounds.min;
Vector3 boundMax = modelMesh.bounds.max;
Vector3 boundVector = boundMax - boundMin;
betaMax = Mathf.Sqrt(Vector3.Dot(boundVector, boundVector));
alphaMax = Mathf.Max(Mathf.Max(boundVector.x, boundVector.y), boundVector.z);
Debug.Log("CalcParameters!");
}
这里计算\(\alpha_{max}\)与\(\beta_{max}\)(即图片的大小)的计算我并没有采用计算所有oriented point中的最大值;而是采取了一种粗略估计的方法,根据当前模型的包围盒大小,取包围盒的最大/最小值点的距离(一般来说可以会比上面方法计算的值大)。最后取两个值的最大值作为宽高(即等宽高)
计算网格强度(CPU)
按照上面提到的公式对对应桶的网格强度进行计算,我们将根据强度的最大值将网格强度映射到0到1,并使用灰度图进行显示。
计算权重a
和b
的公式这里使用:
public void CreateSpinImageFromCPU(OrientedPoint op)
{
CreateSpinImageParameters(currModelMesh);
int width = Mathf.CeilToInt((2 * betaMax) / binSize + 1);
int height = Mathf.CeilToInt(alphaMax / binSize + 1);
imageWidth = Mathf.Max(width,height);
float[] idensity = new float[imageWidth * imageWidth];
foreach (var point in currModelMesh.vertices)
{
//spin-map
Vector3 xp = point - op.Position;
float beta = Vector3.Dot(op.Normal, xp);
float alpha = Mathf.Sqrt(Vector3.Dot(xp, xp) - Mathf.Pow(beta, 2));
//spin-bin
int i = Mathf.FloorToInt((betaMax - beta) / binSize); //col
int j = Mathf.FloorToInt(alpha / binSize); //row
//bilinearWeight
float a = (betaMax - beta) / binSize - i;
float b = alpha / binSize - j;
float ij = (1 - a) * (1 - b);
float i1j = a * (1 - b);
float ij1 = (1 - a) * b;
float i1j1 = a * b;
//unity reverse y
i = imageWidth - i;
idensity[i * imageWidth + j] += ij;
idensity[(i + 1) * imageWidth + j] += i1j;
idensity[i * imageWidth + j + 1] += ij1;
idensity[(i + 1) * imageWidth + j + 1] += i1j1;
}
textureData = new Color32[imageWidth * imageWidth];
float maxIdensity = 0;
foreach (var item in idensity)
{
maxIdensity = Mathf.Max(maxIdensity, item);
}
for (int i = 0; i < imageWidth * imageWidth; ++i)
{
textureData[i] = Color.white - new Color(idensity[i], idensity[i], idensity[i], 0) / maxIdensity;
}
Texture2D tempTexture = new(imageWidth, imageWidth, TextureFormat.ARGB32, true);
tempTexture.SetPixels32(textureData);
tempTexture.Apply();
spinImage = tempTexture;
Debug.Log("CPU Generated!");
}
计算网格强度(GPU)
采用HLSL的computer shader进行并行计算网格强度,但是在GPU并行上有个问题得注意:对网格强度的计算需要采用原子操作,但是HLSL并没有提供浮点数的原子操作,我们需要自己实现浮点数的原子操作,如下:
globallycoherent RWByteAddressBuffer g_BinBuffer : register(u0);
void InterlockedAddFloat(uint addr, float value)
{
uint comp;
uint orig = g_BinBuffer.Load(addr * 4);
[allow_uav_condition]
do
{
g_BinBuffer.InterlockedCompareExchange(addr * 4, comp = orig, asuint(asfloat(orig) + value), orig);
}
while (orig != comp);
}
采用字节地址缓冲区,浮点数使用32位进行存储,读取时候使用asfloat
函数将数据视为浮点数进行读取,然后进行浮点数的加法,最后将得到的结果视为uint
类型;最后采用InterlockedCompareExchange
原子函数进行来实现原子操作。
在GPU计算也是主要分为两趟Pass
- 第一趟:在一维数组上计算网格强度。
- 第二趟:将得到的网格强度映射到0-1,最后并赋给对于的图片像素位置。
HLSL核心代码:
#pragma kernel CreateSpinImageData
#pragma kernel CreateSpinImage
[numthreads(512,1,1)]
void CreateSpinImageData(uint3 DTid : SV_DispatchThreadID)
{
if (DTid.x >= g_PointNums)
{
return;
}
float3 currPoint = g_MeshPointsBuffer[DTid.x];
//spin-map
float3 xp = currPoint - g_OrientedPointBuffer[0].position;
float beta = dot(g_OrientedPointBuffer[0].normal, xp);
float alpha = sqrt(dot(xp, xp) - pow(beta, 2));
//Bin
uint i = floor((g_BetaMax - beta) / g_BinSize);
uint j = floor(alpha / g_BinSize);
//BilinearWeight
float a = (g_BetaMax - beta) - i * g_BinSize;
float b = alpha - j * g_BinSize;
float ij = (1 - a) * (1 - b);
float i1j = a * (1 - b);
float ij1 = (1 - a) * b;
float i1j1 = a * b;
//unity reverse y
i = g_ImageWidth - i;
InterlockedAddFloat(i * g_ImageWidth + j, ij);
InterlockedAddFloat((i + 1) * g_ImageWidth + j, i1j);
InterlockedAddFloat(i * g_ImageWidth + j + 1, ij1);
InterlockedAddFloat((i + 1) * g_ImageWidth + j + 1, i1j1);
}
[numthreads(32, 32, 1)]
void CreateSpinImage(uint3 DTid : SV_DispatchThreadID)
{
uint index = DTid.y * g_ImageWidth + DTid.x;
float data = asfloat(g_BinBuffer.Load(index * 4));
float r = 1.0f - data / g_MaxIdensity;
float4 res = float4(r, r, r, 1.0f);
g_SpinImage[DTid.xy] = (res);
}
参考资料
Spin Images(Georgios Papadimitriou)