Sweethome3d的Flood-fill算法实现

1,025 阅读5分钟

项目中要用到3D建模,对比了网上的各种3D建模软件,最终sweethome3d被选中作为原型. sweethome3d有以下几个优点:

  1. 采用Java实现
  2. 拥有客户端和web端
  3. 插件易于开发
  4. 可以集成各种模型组件
  5. 开源

需求

在sweethome3d建模软件上实现一个插件,可以根据房屋单层模型自动构建出一栋楼,由于不需要查看楼内结构,只需要保存楼层外壳即可,这样可以简化模型结构,加快模型渲染.
与sweethome3d官方沟通,得知软件已支持多楼层复制,但不支持单层模型约简,故核心难点是如何将包含内部结构的单层模型约简成单层外壳,将其内部结构剔除. 需要实现的效果如下

原始模型

原始单层模型

约简后的模型

约简后的单层模型

算法设计

算法原理

可以使用泛洪填充算法(Flood Fill Algorithm)做模型约简.
算法实现的核心思想是,将模型包裹在一个最小矩形内,在矩形边界上找一个点,向矩形内部逐步扩散,当触碰到模型边界时即表明该边界属于外壳,边界可以保留,该扩散点停止扩散.如此可以实现一种类似"水漫金山"的方式,将模型外壳识别出来

算法分析

递归泛洪算法

一个点做扩散时可以按四领域或八领域的方式扩散 四领域
四邻域的泛洪填充算法,寻找点p(x, y)的上下左右四个临近点,如果没有 被填充,则填充它们,并且继续寻找它们的四邻域点,直到封闭区域完全被填充

四领域

八领域
八邻域的填充算法,则是在四邻域的基础上增加了左上,左下,右上,右下四个相邻点。 并递归寻找它们的八邻域点填充,直到区域完全被填充。

这两种扩散方式本质上是一样的,这样的递归有两个问题:

  1. 栈溢出:算法基于递归实现,容易出现导致栈溢出,在实际测试中,对25m*20m的模型就已经栈溢出
  2. 运行缓慢: 25m*20m面积的模型测试时间长度超过5mins

线性泛洪算法

采用Queue-Linear Flood Fill算法可避免递归导致的栈溢出问题,算法复杂度为O(n),对同一25m*20m的模型,算法运行时间缩短至10%

线性泛洪算法是非递归方法,以横坐标为基准,逐渐上下扩展,扩展时按行来走,每次都是计算行的范围,把每个行内的点入队,对行内所有点,逐一出队,再对出队的点上下扩展,将没走过的点入队,当所有点遍历完,队列为空,结束算法

构建起始线

构建起始线

上下扩展

上下扩展
具体算法解释可参见Queue-Linear Flood Fill: A Fast Flood Fill Algorithm

算法实现思路

采用线性泛洪算法可以识别出最外围的边界线,但模型内部的线还需要剔除.有些线在模型内部,直接剔除即可,但有些边界线是从模型外部延伸到内部,此时需要保留其外部部分,剔除其处于内部部分.

可以将所有线段按交点切割,再判断每条线段是否处于外边界即可
如下,不同颜色标识出切割后的每一条线段

切割线

优化

  1. 范围缩放
    默认Sweethome3d的距离单位是1cm,如果每一步扩散时都是按单位长度扩散,由于房屋范围过大,每次的步长为1cm就太浪费了,只要保证步长不超过墙壁厚度即可,这里把每次的扩散步长设为墙厚度,故而模型另起一个二维矩阵int[][] pointMatrix,对模型做了映射,矩阵里的[0][1],和[0][2]实际相隔距离不是1cm而是墙的厚度
  2. 缓存接触点
    缓存接触点,当使用线性泛洪算法找到接触外围的边界点时,缓存该点,在剔除内部线段时,只需要使用缓存点去判断是否接触该线段,与缓存点接触的则可判为外围边界线,否则为内部线,就不用再遍历所有的点了,只遍历和外部有关的点

如下图,所有的接触点(红色点)将形成一层膜包裹住整个模型

接触点

核心算法

/**
 * 非递归方法,以横坐标为基准,上下扩展 按行来走,每次都是计算行的范围,把每个行内的点入队 对行内所有点,出队,上下扩展,再入队
 * 当所有点遍历完,队列为空,结束
 * 
 * @see https 
 *      ://stackoverflow.com/questions/8070401/android-flood-fill-algorithm
 *      算法复杂度 O(n)
 * @param x
 * @param y
 */
public void floodFill(int x, int y) {
	// 起始点,左右横向走
	LinearFill(x, y);
	FloodFillRange range;
	int downY, upY;
	while (ranges.size() > 0) {
		range = ranges.remove(); // 出队
		downY = range.Y - 1; // 上下扩展
		upY = range.Y + 1;

		for (int i = range.startX; i <= range.endX; i++) {
			// 边界内,没碰到墙,就记下这一行,入队
			if (upY < (pointMatrix[0].length) && (pointMatrix[i][upY] == 0)
					&& !isWallTouch(i, upY))
				LinearFill(i, upY);
			if (downY >= 0 && (pointMatrix[i][downY] == 0)
					&& !isWallTouch(i, downY))
				LinearFill(i, downY);
		}
	}
}

/**
 * 左右横向走,入队
 */
protected void LinearFill(int x, int y) {
	// 寻找左边界点
	int leftX = x;
	while (true) {
		pointMatrix[leftX][y] = 1; // 标记已经走过
		leftX--;
		// 边界检查 是否已经走过 是否是目标点,比如是否已经触及墙壁
		if (leftX < 0 || (pointMatrix[leftX][y] == 1)
				|| isWallTouch(leftX, y)) {
			break;
		}
	}
	leftX++;

	// 寻找右边界点
	int rightX = x;
	while (true) {
		pointMatrix[rightX][y] = 1;
		rightX++;
		if (rightX >= pointMatrix.length || (pointMatrix[rightX][y] == 1)
				|| isWallTouch(rightX, y)) {
			break;
		}
	}
	rightX--;

	// 把左右点加入队列
	FloodFillRange r = new FloodFillRange(leftX, rightX, y);
	ranges.offer(r);
}

源码地址:FloodFill

参考资料

  1. sweethome3d
  2. Android flood-fill algorithm
  3. 图像处理------泛洪填充算法(Flood Fill Algorithm)油漆桶功能
  4. Queue-Linear Flood Fill: A Fast Flood Fill Algorithm