前言

  这是本博客网格生成算法系列的第三篇,第一篇里面介绍了最为流行的MarchingCubes算法,第二篇中使用新三角形表来对MC算法进行了简化改进,形成了SMC算法。而这篇将介绍一种新的不同与MC算法思路的新网格生成算法,叫做Cuberille法,这种算法的思想相比MC算法要简单,更加易于实现。

体素立方体模型

  根据第一篇的介绍,我们知道MC算法的基本模型是把组成三维图像的体素都当作空间上的点而8体素组成的体元作为立方体单元。相比于MC算法,Cuberille算法是把体素都想象成立方体,而没有所谓体元的概念了。这一点其实更加符合人们对图像的最初认识,最初接触到计算机中的二维图像的时候,一般人理解像素都是将其当成一个涂了颜色的小方块。而Cuberille算法的理解正是把组成三维图像的体素也当成涂了颜色的小立方体块。例如一个三维图像中所有的实点(这里的实点和MC算法里一样指的是内容区域的体素,下文的虚点同理指背景区域的体素)组成一个猪头的形状,那么就可以认为这个猪头是很多很小的立方体方块堆砌而成的。

         
猪头        三维图像中的猪头体素方块集合

基于正方形片为边界建模

  使用小立方体堆砌组成内容区域,那么这个内容区域的边界必然就是很多正方形面片组成的Mesh,由于每个正方形面片能分成两个三角形面片,那么实际上这个表面的Mesh也是三角网格。下文的重点是研究如何找到这些组成边界面的小正方形面片。

  首先,小正方形面片总是介于实点和虚点之间,下图使用二维的情况来说明这个边界。

   
例子1   例子2   说明

  可以看出,实点和虚点的边界面总是存在一个边界,其长度等于单位长度。那么只要找到图像中所有这样的虚实点交界处的边界,将其焊接起来就能组成内容的表面模型。

  因而总结出Cbuerille算法的主题思路如下:

  1. 创建一个空的Mesh
  2. 按层、列、行三重循环遍历所有的体素V
    1. 假如V为实点
      1. 获取V的六邻域体素集合Adj6(V)
      2. 遍历Adj6(V)中的体素T
        1. 若T超出图像范围或者T为虚点
        2. 创建介于V与T之间的正方形片,并加入到Mesh

  下一步就是探讨如何来为实体素V和它的6邻接邻居虚点T(这个T也可能是超出图像范围的点,这里当其为虚点也一样的处理)中间建立方形片并加入到Mesh。为了实现高效统一的创建面片的方案,有必要对邻域和顶点进行编号。下图是一个邻域编号。显示了一个体素六个方向邻域的编号与对应坐标计算方式:

邻接体素编号 相对偏移
0 (0,1,0)
1 (0,-1,0)
2 (1,0,0)
3 (-1,0,0)
4  (0,0,1)
5 (0,0,-1) 
6邻域指示 6邻域邻接体素编号 邻接体素方位对照表

  同时为了表示立方体8个体素位置的顶点,我们也为他进行编号如下图表所示:

体素编号 体素偏移
0 (0,1,1)
1 (1,1,1)
2 (1,0,1)
3 (0,0,1)
4 (0,0,0)
5 (1,0,0)
6 (1,1,0)
7 (0,1,0)
预览图 体素方位对照表

  这样一个立方体的6个正方形面里的三角形组成就可以使用下面的图表来表示:

邻接方位编号 方位描述 三角形顶点索引
0 上面 (0,1,6)
(0,6,7)
1 下面 (3,4,5)
(3,5,2)
2 右面 (1,2,5)
(1,5,6)
3 左面 (0,7,4)
(0,4,3)
4 前面 (0,3,2)
(0,2,1)
5 后面 (4,7,6)
(4,6,5)
预览图(只标示出了前方向的正方形) 三角形顶点对照表

  这样只需要知道T是V的第几个邻居就可以创建三角片了。下面的步骤描述了这个逻辑过程。

  1. 根据T的索引 r 找出其第一个三角片的三个顶点索引。
  2. 根据三个顶点索引找出三个顶点坐标偏移量
  3. 使用V的坐标加上偏移量计算出三角形三个顶点的坐标位置
  4. 创建并添加这个三角形。
  5. 根据T的索引 r 找出其第二个三角片的三个顶点索引。
  6. 根据三个顶点索引找出三个顶点坐标偏移量
  7. 使用V的坐标加上偏移量计算出三角形三个顶点的坐标位置
  8. 创建并添加这个三角形。

  注意这里对偏移量做了一些处理,本来应该的偏移量应该是用浮点数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. 图像数据到网格数据-1——MarchingCubes算法

    原文:http://blog.csdn.net/u013339596/article/details/19167907 概述 之前的博文已经完整的介绍了三维图像数据和三角形网格数据.在实际应用中,利用 ...

  2. 图像数据到网格数据-2——改进的SMC算法的实现

    概要 本篇接上一篇继续介绍网格生成算法,同时不少内容继承自上篇.上篇介绍了经典的三维图像网格生成算法MarchingCubes,并且基于其思想和三角形表实现了对样例数据的网格构建.本篇继续探讨网格生成 ...

  3. 图像数据到网格数据-1——Marching Cubes算法的一种实现

    概述 之前的博文已经完整的介绍了三维图像数据和三角形网格数据.在实际应用中,利用遥感硬件或者各种探测仪器,可以获得表征现实世界中物体的三维图像.比如利用CT机扫描人体得到人体断层扫描图像,就是一个表征 ...

  4. CUDA 实现JPEG图像解码为RGB数据

    了解JPEG数据格式的人应该easy想到.其对图像以8*8像素块大小进行切割压缩的方法非常好用并行处理的思想来实现.而其实英伟达的CUDA自v5.5開始也提供了JPEG编解码的演示样例.该演示样例存储 ...

  5. KubeCon 2021|使用 eBPF 代替 iptables 优化服务网格数据面性能

    作者 刘旭,腾讯云高级工程师,专注容器云原生领域,有多年大规模 Kubernetes 集群管理及微服务治理经验,现负责腾讯云服务网格 TCM 数据面产品架构设计和研发工作. 引言 目前以 Istio[ ...

  6. 北京市行政村边界shp数据/北京市乡镇边界/北京市土地利用分类数据/北京市气象数据/降雨量分布数据/太阳辐射数据

     数据下载链接:数据下载链接​ 北京是一座有着三千多年历史的古都,在不同的朝代有着不同的称谓,大致算起来有二十多个别称.北京地势西北高.东南低.西部.北部和东北部三面环山,东南部是一片缓缓向渤海倾斜的 ...

  7. 广西省行政村边界shp数据/广西省乡镇边界/广西省土地利用分类数据/广西省气象数据/降雨量分布数据/太阳辐射数据

    ​  数据下载链接:数据下载链接 广西壮族自治区,地处中国南部,北回归线横贯中部,属亚热带季风气候区.南北以贺州--东兰一线为界,此界以北属中亚热带季风气候区,以南属南亚热带季风气候区. 数据范围:全 ...

  8. 第二篇:R语言数据可视化之数据塑形技术

    前言 绘制统计图形时,半数以上的时间会花在调用绘图命令之前的数据塑型操作上.因为在把数据送进绘图函数前,还得将数据框转换为适当格式才行. 本文将给出使用R语言进行数据塑型的一些基本的技巧,更多技术细节 ...

  9. 数据库之mysql篇(3)—— mysql创建/修改数据表/操作表数据

    创建数据表:create table 数据表名 1.创建表规范 create table 表名( 列名   数据类型    是否为空   自动排序/默认值  主键/外键/唯一键, 列名   数据类型 ...

随机推荐

  1. PhoneGap打包webApp

    因为我只弄了Andriod的环境,所以在此只以Andriod为例. 使用PhoneGap搭建Android开发的项目整体步骤如下: 安装java环境. 安装ant构建工具. 安装android的开发环 ...

  2. OpenStack之九: 创建一个实例

    官网地址 https://docs.openstack.org/install-guide/launch-instance-networks-provider.html #:导入变量 [root@co ...

  3. 【Linux】【Services】【Project】Haproxy Keepalived Postfix实现邮件网关Cluster

    1. 简介: 1.1. 背景:公司使用exchange服务器作为邮件服务器,但是使用Postfix作为邮件网关实现病毒检测,内容过滤,反垃圾邮件等功能.原来的架构非常简单,只有两台机器,一个负责进公司 ...

  4. jstl中的choose标签

    <%@ page contentType="text/html;charset=UTF-8" language="java" %><%@ ta ...

  5. 【Linux】【CentOS】【FTP】FTP服务器安装与配置(vsftpd、lftp)

    [初次学习.配置的笔记,如有不当,欢迎在评论区纠正 -- 萌狼蓝天 @ 2021-12-02] 基本概念 FTP访问方式 实体账号:本地账户 来宾账户:guest 匿名登录:anonymous fp ...

  6. Blazor中的无状态组件

    声明:本文将RenderFragment称之为组件DOM树或者是组件DOM节点,将*.razor称之为组件. 1. 什么是无状态组件 如果了解React,那就应该清楚,React中存在着一种组件,它只 ...

  7. CF706A Beru-taxi 题解

    Content 有一个人在点 \((a,b)\) 等出租车.已知他周围共有 \(n\) 辆出租车,其中第 \(i\) 辆车在点 \((x_i,y_i)\) 上,速度为 \(v_i\).这个人想打能让他 ...

  8. 后端调用WebApi

    using System;using System.Collections.Generic;using System.Linq;using System.Net.Http;using System.W ...

  9. Solon 1.6.10 重要发布,现在有官网喽!

    关于官网 千呼万唤始出来: https://solon.noear.org .整了一个月多了,总体样子有了...还得不断接着整! 关于 Solon Solon 是一个轻量级应用开发框架.支持 Web. ...

  10. c++关于使用new的纠正

    自己之前纠正过这个问题,但还是忘了.今天再拿出来. 今天主要总结关于使用 c++ 标准中的 new 关键字. [结论] A.处理new可能抛出的异常 B.针对new使用std::nothrow不抛出异 ...