如何使用JavaScript生成lowpoly风格图像?

[图片] 在指定区域内,生成类似三角形网格 并着色的算法,怎么实现? 试了好久,水平有限,希望大神们不吝赐教
关注者
1,284
被浏览
104,771

17 个回答

===== 更新 =====

算法已发表论文:

Zhang, Wenli; Xiao, Shuangjiu; Shi, Xin, "Low-poly style image and video processing," in Systems, Signals and Image Processing (IWSSIP), 2015 International Conference on , vol., no., pp.97-100, 10-12 Sept. 2015

URL:

IEEE Xplore Abstract

==============

被邀请挺久了,一直忍着没答!

在做完

百度 IFEOvilia/Polyvia · GitHub

项目之后,在

小结博客

里也没细说算法,因为我当时正准备在发论文。现在论文已经被接收了(等正式发表了我来补引用和 PDF),所以来仔细说说我的算法,也正好让大家提提改进的意见,因为我觉得还有很大改进空间。

本文主要就算法层面进行讨论,不涉及具体的 WebGL 实现细节和着色器编程知识,希望有点编程经验的同学都能无障碍阅读。

不允许转载,保留所有权利。不允许未经许可以任何形式使用本文的图。

题目的描述比较模糊,首先明确一下解决的问题。其他答案中给出的

wagerfield/flat-surface-shader · GitHub

qrohlf/trianglify · GitHub

这些项目,都是针对生成带有渐变效果的 low-poly 图,更像是纹理生成。而我研究的是,如何通过一张输入图像,输出其对应的 low-poly 图,这一问题显然比纹理生成难得多。

比如对于以下输入图(摄影作者:

@正一

是论文三作)

输出的效果是:

当然,仔细看可以看到很多瑕疵,比如左边靠下蓝绿色中间的边缘没有很好地被保留下来等等。但是整体的效果应该还算比较美观的,对于细节比较丰富的地方,取的三角形比较多,反之则反。

在我做这个项目之前已经发现了一些类似的实现,其中我觉得最有效的是:

timbennett/delaunay · GitHub

,我觉得从结果上来说已经比较接近完美了,那还有什么做的必要呢?我当时也是这么问作为 IFE 导师的

@沈毅

的,我不记得他当时的回答了……反正就记得不是很有说服力哈哈。然后过了几天我想到,可以做一个在浏览器内获取本地摄像头图像处理后的视频版,于是我就开始做这个项目了。

为了回答这个问题,我特地跟 timbennett 的效果图对比了一下(想要试玩的话在 GitHub 上下载他的项目放在本地服务器下,他给的链接挂了),用的还是上面的输入图,同样取 1000 个点组成三角形,他的输出是这样的:

看起来还是我的算法略胜一筹捏哈哈~(我的评价标准是边缘信息更好地被保留下来,且具有更好的艺术性。可能还是有一定主观因素在内,但是我想应该还是我的效果更好一点吧?)

聪明的同学已经发现了,效果的好坏主要取决于选了什么位置的点组成三角形。如果你是完全随机选的,那么效果就可能是这样的:

看我算法的效果图应该可以猜到,取图像边缘上的点更容易获得更好的效果,这是因为如果边上的两个点被选中了,那么丢给后面的 Delaunay 三角化步骤生成的三角形的一条边就会在边缘上,那就没有上图随机取点造成的这种感觉了。

好,既然我们要取边缘上的点,那么很容易想到可以先用 Sobel 之类的边缘检测算法获取边缘,得到的结果是这样的:(使用 WebGL 着色器加速运算:

Polyvia/three.js.EdgeShader.js at gh-pages · Ovilia/Polyvia · GitHub

那么怎么取边缘上的点呢?显然不能每个点都取,否则就太多了,low-poly 本来就是要取更少的点达到更抽象的效果。这里我觉得是我可以改进的地方,因为我的方法比较简单粗暴,就是在边缘上随机取点,这样的方法实现起来简单,效果也不差。但是如果能够更有规律地取点,还是可以有效改善最终效果的,这点稍后再说。

能不能只取边缘上的点作为组成三角形的点呢?

通常这不是一个好主意。因为它使得很多三角形退化成非常尖锐的锐角三角形。

为什么这里说退化呢?这是因为后面一步的 Delaunay 三角化的作用就是将输入的一组点尽可能地避免变成锐角三角形,从而达到更好的视觉效果。

所以,除了边缘上的点有一定概率会被选作组成三角形之外,我们另外以一个更小的概率在全图中随机取点。这样就非常有效地避免了非常锐的锐角三角形出现。下图为选中的点的位置。

然后,我们使用 Delaunay 三角化将选中的点组成一个个三角形。Delaunay 三角化是图形学中一种比较常用的算法,具体细节我不展开了,总之它的作用上文已经提到了,就是使得生成的三角形们尽量不是非常锐的锐角三角形。下图为 Delaunay 算法得到的三角形们。使用的 Delaunay 算法是第三方库:

ironwallaby/delaunay · GitHub

最后,就到了给三角形上色的阶段了。当然像 k-means 这种可以获取三角形颜色类别的方法更高大上,但是为了满足实时性需求,我们还是使用简单粗暴的办法就好,而且效果也非常不错。具体的做法就是使用每个三角形重心处的颜色作为三角形的颜色。重心位置的计算是非常简单的,只要把三个顶点的位置求平均数就可以了。然后就能得到上面的结果图了。

这么说下来的逻辑应该挺清楚的吧?不清楚的话再来看这张整体流程图。

以上,就是图像处理的全部流程了,知道原理后其实也不难对吧?我搜来搜去没找到发表的 low-poly 论文,想着可以做鼻祖了!然而评委评论意见里告诉了我这篇 2015 年 4 月份发表的论文:

Gai, Meng, and G. Wang. "Artistic Low Poly rendering for images." The Visual Computer (2015): 1-10

然后我对比了一下,发现这篇论文的效果比我厉害多了……

(a) 是输入图像;(b) 是

iTunes 的 App Store 中的“Art camera TRIGRAFF”

这个软件生成的结果;(c) 是这篇论文的效果;(d) 是我的算法的效果。(前三张图来自这篇论文)

相形见拙啊!!!

主要还是在选择组成三角形的点的算法上,我的随机算法虽然不错,但是跟人家有目的性地在边缘上选取点的方法还是弱了不少的。(这篇论文我还没细看,回头再仔细研究研究)

但是!!!你知道这篇论文生成 500x800 左右的图要多少时间么?4.7 秒!我的呢?0.3 秒!所以呢,也并没有输的那么惨烈对吧~


以上就是图像方面的算法了。下面讲视频相关部分。


视频的每一帧当作图像处理就好了吗?那我也太水了……

主要的问题在于,由于我的点是随机取的,所以每帧取到的点不一样,就表现为屏幕一直在闪烁,因为三角形的位置和颜色变化非常大。为了解决这一问题,我们就希望上一帧被选中的点,如果在这一帧中仍然在边缘,则它被选中的概率将大得多。


这似乎能比较有效地消抖,但是又带来一个新的问题,就是随着时间的进行,越来越多的边缘点被保留下来,那么留给其他地方的点就越来越少了。具体表现为,如果背景有个沙发抱枕,我人在前面晃悠,结果沙发抱枕边缘上的点越取越多,我的脸就变得一片模糊了,因为没多少点留给我的脸了。(欧,我的脸~~你能想象我把这样的图放到论文里去了么……)


解决方法是,设一个 5% 左右的淘汰率,对于上一帧被选中,并且这一帧仍然在边缘上的点,仍然有 5% 的概率在这一帧不被选中,这样就保证了点的流通性,同时也能排除一些抖动的干扰。

说是这么说啦,但实际效果是,抖动的消除还是不是很理想。有兴趣的话你可以自己玩玩看:Polyvia





最后,放一些由于篇幅不够没能放到论文中的效果图。(顺便夸一下 @正一 找的原图很棒~ 原图都是 Public Domain 的)

你自己也来试试吧:Polyvia


如果对于算法方面有些建议的话,欢迎给我反馈:Issues · Ovilia/Polyvia · GitHub 或邮件我 me at zhangwenli.com。




写知乎回答比写论文顺畅多了,论文大概就是故意写得让人看不懂的吧……

等我论文正式发表了,如果心情好的话,可以考虑放视频(里面有我真人解说!深 js 上已经剧透过了啦……不过!还有我的配音!!保证不让你们失望!)。心情好的途径包括第一次回答一个点赞数上万的问题啦,GitHub star 过两百啦

Ovilia/Polyvia · GitHub

~\(^o^)/~

最后,再次感谢

@沈毅

在这个项目中对我的悉心指导,以及

@正一

的各种帮忙~

@羡辙

回答中提到的论文作者羞射前来报到。

算法都在论文中,详细可阅读: Gai M, Wang G. Artistic Low Poly rendering for images[J]. The Visual Computer, 2015: 1-10

主要思路点一下吧:用边缘提取产生约束边进行带约束的Delaunay三角化;用Voronoi迭代优化顶点位置。另外显著区域提取是凑数的可有可无。主要时间开销也是在显著区域提取和CVD迭代两步。单纯做边缘提取Delaunay剖分也是很快的。

结果不是太满意,主要难点在于图像中高层语义信息的保持。而边缘提取等局部算子只能提供低层次信息。

贴几张论文里的大图吧: