图像数据到网格数据-3——实现Cuberille算法
前言
这是本博客网格生成算法系列的第三篇,第一篇里面介绍了最为流行的MarchingCubes算法,第二篇中使用新三角形表来对MC算法进行了简化改进,形成了SMC算法。而这篇将介绍一种新的不同与MC算法思路的新网格生成算法,叫做Cuberille法,这种算法的思想相比MC算法要简单,更加易于实现。
体素立方体模型
根据第一篇的介绍,我们知道MC算法的基本模型是把组成三维图像的体素都当作空间上的点而8体素组成的体元作为立方体单元。相比于MC算法,Cuberille算法是把体素都想象成立方体,而没有所谓体元的概念了。这一点其实更加符合人们对图像的最初认识,最初接触到计算机中的二维图像的时候,一般人理解像素都是将其当成一个涂了颜色的小方块。而Cuberille算法的理解正是把组成三维图像的体素也当成涂了颜色的小立方体块。例如一个三维图像中所有的实点(这里的实点和MC算法里一样指的是内容区域的体素,下文的虚点同理指背景区域的体素)组成一个猪头的形状,那么就可以认为这个猪头是很多很小的立方体方块堆砌而成的。
猪头 | 三维图像中的猪头体素方块集合 |
基于正方形片为边界建模
使用小立方体堆砌组成内容区域,那么这个内容区域的边界必然就是很多正方形面片组成的Mesh,由于每个正方形面片能分成两个三角形面片,那么实际上这个表面的Mesh也是三角网格。下文的重点是研究如何找到这些组成边界面的小正方形面片。
首先,小正方形面片总是介于实点和虚点之间,下图使用二维的情况来说明这个边界。
例子1 | 例子2 | 说明 |
可以看出,实点和虚点的边界面总是存在一个边界,其长度等于单位长度。那么只要找到图像中所有这样的虚实点交界处的边界,将其焊接起来就能组成内容的表面模型。
因而总结出Cbuerille算法的主题思路如下:
- 创建一个空的Mesh
- 按层、列、行三重循环遍历所有的体素V
- 假如V为实点
- 获取V的六邻域体素集合Adj6(V)
- 遍历Adj6(V)中的体素T
- 若T超出图像范围或者T为虚点
- 创建介于V与T之间的正方形片,并加入到Mesh
- 假如V为实点
下一步就是探讨如何来为实体素V和它的6邻接邻居虚点T(这个T也可能是超出图像范围的点,这里当其为虚点也一样的处理)中间建立方形片并加入到Mesh。为了实现高效统一的创建面片的方案,有必要对邻域和顶点进行编号。下图是一个邻域编号。显示了一个体素六个方向邻域的编号与对应坐标计算方式:
|
||||||||||||||||
6邻域指示 | 6邻域邻接体素编号 | 邻接体素方位对照表 |
同时为了表示立方体8个体素位置的顶点,我们也为他进行编号如下图表所示:
|
|||||||||||||||||||
预览图 | 体素方位对照表 |
这样一个立方体的6个正方形面里的三角形组成就可以使用下面的图表来表示:
|
||||||||||||||||||||||||||||
预览图(只标示出了前方向的正方形) | 三角形顶点对照表 |
这样只需要知道T是V的第几个邻居就可以创建三角片了。下面的步骤描述了这个逻辑过程。
- 根据T的索引 r 找出其第一个三角片的三个顶点索引。
- 根据三个顶点索引找出三个顶点坐标偏移量
- 使用V的坐标加上偏移量计算出三角形三个顶点的坐标位置
- 创建并添加这个三角形。
- 根据T的索引 r 找出其第二个三角片的三个顶点索引。
- 根据三个顶点索引找出三个顶点坐标偏移量
- 使用V的坐标加上偏移量计算出三角形三个顶点的坐标位置
- 创建并添加这个三角形。
注意这里对偏移量做了一些处理,本来应该的偏移量应该是用浮点数0.5来组成的,如下图所示,焊接点都处在体素位置中点处,所以三角形顶点都应该是“X.5”形式的浮点数。而上文的过程中使用的偏移量(如顶点对应表中的偏移量)是将所有点的坐标都加了0.5之后的位置。之所以使用平移0.5之后的坐标,是希望利用整数点坐标来使用哈希表焊接。那么有必要在算法结束的时候把所有的点坐标-0.5来恢复真正的位置。不过有时由于只需要结果的形状一致,所以也不做处理。
最后是用C#实现的Cuberille算法的代码,其中涉及到的bitmap类,哈希表类,Meshbuilder类在前几篇文章中多次提到过,就不重复粘贴代码了。
public struct Int16Triple
{
public int X;
public int Y;
public int Z;
public Int16Triple(int x, int y, int z)
{
X = x;
Y = y;
Z = z;
}
}
public struct FloatTriple
{
public float X;
public float Y;
public float Z;
public FloatTriple(float x, float y, float z)
{
X = x;
Y = y;
Z = z;
}
}
public class CuberilleProcessor
{
public static Int16Triple[][] AdjIndexToVertexIndices = new Int16Triple[6][]
{
new Int16Triple[2] { new Int16Triple(0, 1, 6), new Int16Triple(0, 6, 7) },
new Int16Triple[2] { new Int16Triple(3, 4, 5), new Int16Triple(3, 5, 2) },
new Int16Triple[2] { new Int16Triple(1, 2, 5), new Int16Triple(1, 5, 6) },
new Int16Triple[2] { new Int16Triple(0, 7, 4), new Int16Triple(0, 4, 3) },
new Int16Triple[2] { new Int16Triple(0, 3, 2), new Int16Triple(0, 2, 1) },
new Int16Triple[2] { new Int16Triple(4, 7, 6), new Int16Triple(4, 6, 5) },
};
public static Int16Triple[] VertexIndexToPositionDelta = new Int16Triple[8]
{
new Int16Triple(0, 1, 1),
new Int16Triple(1, 1, 1),
new Int16Triple(1, 0, 1),
new Int16Triple(0, 0, 1),
new Int16Triple(0, 0, 0),
new Int16Triple(1, 0, 0),
new Int16Triple(1, 1, 0),
new Int16Triple(0, 1, 0),
};
BitMap3d bmp;
public CuberilleProcessor(BitMap3d bitmap)
{
bmp = bitmap;
}
public Mesh GeneratorSurface()
{
int Width = bmp.width;
int Height = bmp.height;
int Depth = bmp.depth;
Int16Triple[] adjPoints6 = new Int16Triple[6];
MeshBuilder_IntegerVertex mb = new MeshBuilder_IntegerVertex(bmp.width, bmp.height, bmp.depth); for (int k = 0; k <= Depth - 1; k++)
{
for (int j = 0; j <= Height - 1; j++)
{
for (int i = 0; i <= Width - 1; i++)
{
if (IsInside(i,j,k))
{
Int16Triple p = new Int16Triple(i, j, k);
InitAdj6(adjPoints6,p);
for (int r = 0; r < adjPoints6.Length; r++)
{
Int16Triple t = adjPoints6[r];
if (!IsInside(t.X,t.Y,t.Z))
{
ExtractSquare(r,p,mb);
}
}
}
}
}
}
Mesh m= mb.GetMesh();
for (int i = 0; i < m.Vertices.Count; i++)
{
Point3d p = m.Vertices[i];
p.X -= 0.5f;
p.Y -= 0.5f;
p.Z -= 0.5f;
}//若需要真实位置,则都得平移回去
return m;
} private void ExtractSquare(int r,Int16Triple p, MeshBuilder_IntegerVertex mb)
{
int p0x, p0y, p0z, p1x, p1y, p1z, p2x, p2y, p2z;//
Int16Triple deltaA0 = VertexIndexToPositionDelta[AdjIndexToVertexIndices[r][0].X];
Int16Triple deltaA1 = VertexIndexToPositionDelta[AdjIndexToVertexIndices[r][0].Y];
Int16Triple deltaA2 = VertexIndexToPositionDelta[AdjIndexToVertexIndices[r][0].Z];
p0x = p.X + deltaA0.X;
p0y = p.Y + deltaA0.Y;
p0z = p.Z + deltaA0.Z;
p1x = p.X + deltaA1.X;
p1y = p.Y + deltaA1.Y;
p1z = p.Z + deltaA1.Z;
p2x = p.X + deltaA2.X;
p2y = p.Y + deltaA2.Y;
p2z = p.Z + deltaA2.Z;
mb.AddTriangle(new Int16Triple(p0x, p0y, p0z), new Int16Triple(p1x, p1y, p1z), new Int16Triple(p2x, p2y, p2z)); Int16Triple deltaB0 = VertexIndexToPositionDelta[AdjIndexToVertexIndices[r][1].X];
Int16Triple deltaB1 = VertexIndexToPositionDelta[AdjIndexToVertexIndices[r][1].Y];
Int16Triple deltaB2 = VertexIndexToPositionDelta[AdjIndexToVertexIndices[r][1].Z]; p0x = p.X + deltaB0.X;
p0y = p.Y + deltaB0.Y;
p0z = p.Z + deltaB0.Z;
p1x = p.X + deltaB1.X;
p1y = p.Y + deltaB1.Y;
p1z = p.Z + deltaB1.Z;
p2x = p.X + deltaB2.X;
p2y = p.Y + deltaB2.Y;
p2z = p.Z + deltaB2.Z;
mb.AddTriangle(new Int16Triple(p0x, p0y, p0z), new Int16Triple(p1x, p1y, p1z), new Int16Triple(p2x, p2y, p2z));
}
public virtual bool IsInside(int x, int y, int z)
{
if (x <= 0 || y <= 0 || z <= 0 || x > bmp.width || y > bmp.height || z > bmp.depth)
return false;
else
{
return bmp.GetPixel(x, y, z) == BitMap3d.WHITE;
}
}//judge if a voxel is inside the surface public static void InitAdj6(Int16Triple[] adjPoints6,Int16Triple p)
{
adjPoints6[0].X = p.X;
adjPoints6[0].Y = p.Y + 1;
adjPoints6[0].Z = p.Z; adjPoints6[1].X = p.X;
adjPoints6[1].Y = p.Y - 1;
adjPoints6[1].Z = p.Z; adjPoints6[2].X = p.X + 1;
adjPoints6[2].Y = p.Y;
adjPoints6[2].Z = p.Z; adjPoints6[3].X = p.X - 1;
adjPoints6[3].Y = p.Y;
adjPoints6[3].Z = p.Z; adjPoints6[4].X = p.X;
adjPoints6[4].Y = p.Y;
adjPoints6[4].Z = p.Z + 1; adjPoints6[5].X = p.X;
adjPoints6[5].Y = p.Y;
adjPoints6[5].Z = p.Z - 1;
}//initialize poistions of the 6-adjacency points
}
算法结果
算法使用Engine.raw数据,生成的Cuberille表面放大后的效果图如下,可以看出确实是由方块组成的模型:
同时使用SMC算法对Engine数据生成表面,放在一起进行对比呈现,同时比较他们的网格规模的图表如下:
- | SMC算法 | Cuberille算法 |
预览图 | ||
顶点数 | 216147 | 311263 |
三角形数 | 432370 | 622638 |
可以看出Cuberille的显示效果显然不如SMC算法平滑(自然也不会比经典MC算法平滑),同时相比SMC算法输出规模更大。这些都是Cuberille算法的缺点,所以现在实际应用场合中较少使用这个算法,不过在一些特殊场合,还是会见到这个算法的身影。
本文的代码可见本人的github:https://github.com/chnhideyoshi/SeededGrow2d/tree/master/CubicSurface
爬网的太疯狂了,转载本文要注明出处啊:http://www.cnblogs.com/chnhideyoshi/
图像数据到网格数据-3——实现Cuberille算法的更多相关文章
- 图像数据到网格数据-1——MarchingCubes算法
原文:http://blog.csdn.net/u013339596/article/details/19167907 概述 之前的博文已经完整的介绍了三维图像数据和三角形网格数据.在实际应用中,利用 ...
- 图像数据到网格数据-2——改进的SMC算法的实现
概要 本篇接上一篇继续介绍网格生成算法,同时不少内容继承自上篇.上篇介绍了经典的三维图像网格生成算法MarchingCubes,并且基于其思想和三角形表实现了对样例数据的网格构建.本篇继续探讨网格生成 ...
- 图像数据到网格数据-1——Marching Cubes算法的一种实现
概述 之前的博文已经完整的介绍了三维图像数据和三角形网格数据.在实际应用中,利用遥感硬件或者各种探测仪器,可以获得表征现实世界中物体的三维图像.比如利用CT机扫描人体得到人体断层扫描图像,就是一个表征 ...
- CUDA 实现JPEG图像解码为RGB数据
了解JPEG数据格式的人应该easy想到.其对图像以8*8像素块大小进行切割压缩的方法非常好用并行处理的思想来实现.而其实英伟达的CUDA自v5.5開始也提供了JPEG编解码的演示样例.该演示样例存储 ...
- KubeCon 2021|使用 eBPF 代替 iptables 优化服务网格数据面性能
作者 刘旭,腾讯云高级工程师,专注容器云原生领域,有多年大规模 Kubernetes 集群管理及微服务治理经验,现负责腾讯云服务网格 TCM 数据面产品架构设计和研发工作. 引言 目前以 Istio[ ...
- 北京市行政村边界shp数据/北京市乡镇边界/北京市土地利用分类数据/北京市气象数据/降雨量分布数据/太阳辐射数据
数据下载链接:数据下载链接 北京是一座有着三千多年历史的古都,在不同的朝代有着不同的称谓,大致算起来有二十多个别称.北京地势西北高.东南低.西部.北部和东北部三面环山,东南部是一片缓缓向渤海倾斜的 ...
- 广西省行政村边界shp数据/广西省乡镇边界/广西省土地利用分类数据/广西省气象数据/降雨量分布数据/太阳辐射数据
数据下载链接:数据下载链接 广西壮族自治区,地处中国南部,北回归线横贯中部,属亚热带季风气候区.南北以贺州--东兰一线为界,此界以北属中亚热带季风气候区,以南属南亚热带季风气候区. 数据范围:全 ...
- 第二篇:R语言数据可视化之数据塑形技术
前言 绘制统计图形时,半数以上的时间会花在调用绘图命令之前的数据塑型操作上.因为在把数据送进绘图函数前,还得将数据框转换为适当格式才行. 本文将给出使用R语言进行数据塑型的一些基本的技巧,更多技术细节 ...
- 数据库之mysql篇(3)—— mysql创建/修改数据表/操作表数据
创建数据表:create table 数据表名 1.创建表规范 create table 表名( 列名 数据类型 是否为空 自动排序/默认值 主键/外键/唯一键, 列名 数据类型 ...
随机推荐
- Virtual Destructor
Deleting a derived class object using a pointer to a base class that has a non-virtual destructor re ...
- SpringMVC(4):文件上传与下载
一,文件上传 文件上传是项目开发中最常见的功能之一 ,springMVC 可以很好的支持文件上传,但是SpringMVC上下文中默认没有装配MultipartResolver,因此默认情况下其不能处理 ...
- mysql 报 'Host ‘XXXXXX’ is blocked because of many connection errors'
1. 问题:服务启动时,日志报错,导致启动失败: Caused by: com.mysql.cj.exceptions.CJException: null, message from server: ...
- 【面试】【Linux】【Web】基础概念
1. HTTP https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol 2. TCP handshake https://en.wikipe ...
- 用运oracel中的伪列rownum分页
在实际应用中我们经常碰到这样的问题,比如一张表比较大,我们只要其中的查看其中的前几条数据,或者对分页处理数据.在这些情况下我们都需要用到rownum.因此我们要理解rownum的原理和使用方法. Or ...
- Jmeter初级入门教程
<jmeter:菜鸟入门到进阶>系列 创建一个简单的自动化脚本 创建线程组[Thread Group]: 右击[TestPlan]选择[Add]--[Thread(Users)]--[Th ...
- Django auth
auth是django一个自带的用户验证系统,使用它可以减少我们的开发流程. 基本使用 大体流程: 自定义类 from django.contrib.auth.models import Abstra ...
- 【力扣】122. 买卖股票的最佳时机 II
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格. 设计一个算法来计算你所能获取的最大利润.你可以尽可能地完成更多的交易(多次买卖一支股票). 注意:你不能同时参与多笔交易(你必须在再次 ...
- java代码从出生到执行的过程浅析
阅读<深入理解java虚拟机 第二版 JVM高级特性与最佳实践> - jdk版本为1.6 1.什么是编译型语言.解释型语言 解释型语言:源代码不是直接翻译成机器语言,而是先翻译成中间代码, ...
- 时间同步——TSN协议802.1AS介绍
前言之前的主题TSN的发展历史和协议族现状介绍了TSN技术的缘起,最近一期的主题TSN协议导读从定时与同步.延时.可靠性.资源管理四个方面,帮助大家了解TSN协议族包含哪些子协议,以及这些子协议的作用 ...