0. 低多边形风格概述
0.0. 定义及简介
视觉艺术中,采取尽量少的多边形对某一特定形象进行表现的艺术风格称为低多边形风格.低多边形风格以其硬件友好,视觉冲击(高对比度)强,风格简约而在近年来受到越来越多的设计者的青睐.现今该艺术风格领域的元老级人物属Timothy J.Reynolds,这里是他的一些作品
0.1. 发展历史
低多边形艺术风格最初可以追溯到计算机性能不足以支持大规模3d渲染的年代,那时的游戏模型等等往往不能做到十分精细.由于计算机进行3d模型的渲染是以三角形面片为单位进行的,过多的三角形面片往往会带来巨大的渲染负担,因此,low poly(低多边形)是当时迫于3d渲染技术不佳而被迫采取的手段.
时至今日,低多边形的美术风格仍然可见于诸多需要进行实时3d渲染的设备,甚至是动画成片中,但是其意义已不同于以往.虽然现今部分场合仍需要快速实时的3d渲染,但硬件设备的发展也能在一定程度上满足这种需要,因此低多边形艺术风格的再兴起很大程度上是跟随了审美取向的变化的.随着生活节奏的加快,近年来简约的艺术风格往往能迅速的抓住审美者的眼球,使我们暂时忘记琐碎的生活,同时兴起的metro风格也是一个佐证.
lowPoly风格在3d领域有着越来越广的应用,如游戏<纪念碑谷>,<纸境奇缘>(见上图)等均采取了这一艺术风格.随着低多边形风格在3d领域获得越来越多的青睐,平面设计领域也逐渐尝试引入该风格(见下图,ppt模板).
1. 滤镜思路
1.0. 总述
我们的目的是对一张给定的图片进行低多边形风格化处理,输入一张图片,输出一张处理后的图片.该处理应当包含如下两个步骤:
- 从输入图片中按一定方式选点,并按一定方式连接这些点,实现网格化.
- 对网格内的点和边界(分割线)上的点着色,着色应接近原图对应区域色调
为使得输出图片尽可能真实还原原图的表现内容,我们应当设计合理的
- 选点算法
- 连线算法
- 着色方法
由于我们只需要处理单张图片,因此处理速度不是我们优先考虑的问题,采用Matlab实现该滤镜.
1.1. 选点算法
选择多边形的顶点位置,这里需要注意的问题有如下几点:
- 选点贴合原图轮廓,避免连线后表现内容走形
- 选点不能过于密集,避免产生大量小色块
- 选点不能过于疏散,避免出现过大的色块
对于第一个问题,首先需要描绘原图中的形象轮廓,所谓轮廓就是指颜色突变处.这里只需用核 [-0.7,-2.1,-0.7;0,0,0;0.7,2.1,0.7]及其转置 对原图的RGB通道分别进行卷积即可(相当于计算3*3区域内上下/左右像素点RGB通道亮度之差),这里我们考虑了原图的色调可能比较一致,即某一通道或者某几个通道的特定组合上色彩区分不大,因此可以自定义这些通道对轮廓描绘过程的权重影响.代码如下:
% 读入图像
inputImg=imread('Lenna.jpg')
% 设置描边卷积核(边界检验范围)
edgeKernel=[-0.7,-2.1,-0.7;0,0,0;0.7,2.1,0.7];
% 设置RGB通道边界权重
weight=[0.2989,0.5807,0.1140];
% 拆分RGB通道并描边
R=inputImg(:,:,1); G=inputImg(:,:,2); B=inputImg(:,:,3); [l,h,~]=size(inputImg);
rEdge=(weight(1)*imfilter(R,edgeKernel,'conv')+weight(1)*imfilter(R,edgeKernel','conv'));
gEdge=(weight(2)*imfilter(G,edgeKernel,'conv')+weight(2)*imfilter(G,edgeKernel','conv'));
bEdge=(weight(3)*imfilter(B,edgeKernel,'conv')+weight(3)*imfilter(B,edgeKernel','conv'));
我们可以基于这些轮廓生成一张优先级热图,在高优先级区(数值较大)的位置优先选点,当然我们也可以为这种选择添加一定的随机性(利用轮廓生成热图时矩阵的每个元素乘以一个随机数,随机数的范围可以自定义).另外,我们有必要在图像外部选择一些点,它们与图像内的点的连线可以让图像上下左右四个边界处的表现不显得突兀,因此该热图应为轮廓矩阵的增广.除此之外,为了避免选点过密的问题,每当一个点被选中,都应该降低优先级热图上此点和此点附近的点的数值(即潜在被选优先级).代码如下:
% 设置选点个数:
pick_count=750;%正常选点个数(这些点的连线用于勾勒边缘)
% 设置噪点权重(0~1)
noise_weight=0.1;
% 设置概率缩减核(大小必须是奇数*奇数,这样可以使得被选中点处在核的正中):
possibility_density_shrinker=[...
1 1 1 1 1 2 3 2 1 1 1 1 1
1 1 1 2 2 3 4 3 2 2 1 1 1
1 1 2 3 4 4 9 4 4 3 2 1 1
1 2 3 4 9 9 9 9 9 4 3 2 1
1 2 4 9 9 9 9 9 9 9 4 2 1
2 3 4 9 9 9 9 9 9 9 4 3 2
3 4 9 9 9 9 9 9 9 9 9 4 3
2 3 4 9 9 9 9 9 9 9 4 3 2
1 2 4 9 9 9 9 9 9 9 4 2 1
1 2 3 4 9 9 9 9 9 4 3 2 1
1 1 2 3 4 4 9 4 4 3 2 1 1
1 1 1 2 2 3 4 3 2 2 1 1 1
1 1 1 1 1 2 3 2 1 1 1 1 1].^2;
% 生成原始概率图(应为增广矩阵),基于此图的概率选边缘点
[hf,lf]=size(possibility_density_shrinker);
probMap=double(rEdge+gEdge+bEdge); averange_pos=mean(probMap(:)); lt=4*lf+l; ht=4*hf+h;
probMap=[zeros(lf,ht);zeros(lf,hf),(2+2*noise_weight)*averange_pos*rand(lf,ht-2*hf),zeros(lf,hf);zeros(l,hf),(2+2*noise_weight)*averange_pos*rand(l,hf),probMap,(2+2*noise_weight)*averange_pos*rand(l,hf),zeros(l,hf);zeros(lf,hf),(2+2*noise_weight)*averange_pos*rand(lf,ht-2*hf),zeros(lf,hf);zeros(lf,ht)];
probMap=double(probMap).*(1-noise_weight+noise_weight*rand(size(probMap)));
figure,imshow(uint8(probMap));
segPoint=zeros(size(probMap));
coordList=zeros(pick_count,2);
%开始选点:
for i=1:pick_count
[~,sub]=max(probMap(:));
coordX=ceil(sub/(lt)); coordY=mod(sub,lt);
segPoint(coordY,coordX)=1;
coordList(i,1)=coordX; coordList(i,2)=coordY;
% 缩减已选点附近的点被选中的概率(避免选点过密及重复选点)
probMap(coordY-(hf-1)/2:coordY+(hf-1)/2,coordX-(lf-1)/2:coordX+(lf-1)/2)=probMap(coordY-(hf-1)/2:coordY+(hf-1)/2,coordX-(lf-1)/2:coordX+(lf-1)/2)./double(possibility_density_shrinker); probMap(coordY,coordX)=0;
end
雷娜图原图及处理结果如下:
1.2. 连线算法
选择完毕所有点后就可以开始连线了,这里提供两种连线思路:
1.2.1 Delaunay三角剖分
该方法是十分经典的有限元网格剖分方法,其最大化最小角的目标总能使得剖分出的三角形色块在视觉上表现的较为饱满.比较经典的剖分方法是Bowyer-Watson方法,该算法的基本流程如下:
- 构造超级三角形,覆盖所有散点,将此超级三角形放入三角形链表
- 插入一个散点,检查三角形链表中”外接圆包含该散点”的三角形,删去它们的公共边,然后连接该散点和这些三角形的端点,形成新的三角形
- 根据优化准则优化新三角形并将结果存储在三角形链表中
- 回到第2步,继续检查其它散点,直至全部散点插入完成
Delaunay三角剖分的实现代码如下:
%% 多边形剖分
tri=delaunay(coordList(:,1),coordList(:,2));
divMap=segPoint;
for i=1:length(tri)
divMap=plotLine(divMap,coordList(tri(i,1),1),coordList(tri(i,1),2),coordList(tri(i,2),1),coordList(tri(i,2),2),1);
divMap=plotLine(divMap,coordList(tri(i,2),1),coordList(tri(i,2),2),coordList(tri(i,3),1),coordList(tri(i,3),2),1);
divMap=plotLine(divMap,coordList(tri(i,3),1),coordList(tri(i,3),2),coordList(tri(i,1),1),coordList(tri(i,1),2),1);
end
其中,plotLine是我们自定义的连线函数:
function img=plotLine(img,x1,y1,x2,y2,marker)
dx=x1-x2;dy=y1-y2;
if abs(dx)>=abs(dy)
try
kk=dy/dx;
catch
img(y1:y2,x1)=img(y1:y2,x1)+marker;return
end
if x2>x1
for i=x1:x2
img(round(y1+kk*(i-x1)),i)=img(round(y1+kk*(i-x1)),i)+marker;
end
else
for i=x2:x1
img(round(y2+kk*(i-x2)),i)=img(round(y2+kk*(i-x2)),i)+marker;
end
end
else
try
kk=dx/dy;
catch
img(y1,x1:x2)=img(y1,x1:x2)+marker;return
end
if y2>y1
for i=y1:y2
img(i,round(x1+kk*(i-y1)))=img(i,round(x1+kk*(i-y1)))+marker;
end
else
for i=y2:y1
img(i,round(x2+kk*(i-y2)))=img(i,round(x2+kk*(i-y2)))+marker;
end
end
end
end
plotLine函数中try-catch的使用是为了避开斜率趋∞
∞
的情况,当然该函数仍然可以使用诸如Bresenham连线等经典算法代替,只需对代码稍加修改即可,相关算法是计算机图形学基础内容,不再赘述.
雷娜图的连线结果如下:
1.2.2 连线-贪心优化
为了更好地使得多边形的边界接近于原图中表现对象的轮廓,我们也可以考虑对连线进行适当的优化.我们可以定义一个偏离值来描述多边形边界与原图中轮廓线的偏离程度.理论上,原图轮廓上所有的像素点到多边形边界的最小距离的平均数(按轮廓灰度加权平均)可以充当偏离值,实际上这种做法十分费时.为此我们可以在原图轮廓线上随机选点构成校验点集,仅仅计算这些校验点到多边形边界的最小距离均值即可.优化思路如下:
- 选择两个具有公共边的三角形,且它们能够组成凸四边形,删去此公共边,连接原三角形的非公共顶点,形成两个新三角形.
- 比照新旧两种剖分方法带来的偏离值大小,若新方法带来的偏离值较小则使用新方法,反之舍弃.
- 重新选择两个三角形,返回步骤1,直至尝试次数足够或偏离值在满意的范围内.
1.3. 着色算法:
低多边形风格艺术中,色块内的颜色较为统一,这种统一有同色系内渐变(如明暗变化)和完全相同(所有像素的RGB值完全相同),无论是哪种着色方式,着色都是以色块为单位进行的,这就要求我们先标记出某一色块内所有的像素点.完成色块内的着色后还应对多边形的边界线进行着色,这里只需要取其相邻色块的颜色即可.
1.3.1 色块标记
色块内的像素点是相连的,这些像素点构成了一个连通域.这里我们需要确定并标记剖分后的四连通域(即认为每一个像素点是和上下左右相连的,斜对角方向的不算做相连,由全部这样相连的点构成的一块区域叫做四连通域).连通域判定最常用的算法是Haralick于1992年在其书中提出的方法(Haralick, Robert M., and Linda G. Shapiro, Computer and Robot Vision, Volume I, Addison-Wesley, 1992, 28-48),Matlab的内置函数bwlabel正是采用这一算法.我们这里直接采用该函数进行连通域判定(由于我们遇到的连通域都是凸的,这里仍有优化余地 //然而工头催收了……):
divMap=divMap(2*lf+1:2*lf+l,2*hf+1:2*hf+h);
% 先去掉之前添加的边框
[divDomain,domainCount]=bwlabel(2-divMap,4);
% 直接判定四连通域,输出分割图和连通域个数
1.3.2 对各连通域的内点着色
我们直接计算每个连通域内RGB通道亮度的算术均值(各点平权)作为目标图像中连通域内每个点的颜色,这种着色方法的优势在于迅速,缺点在于色块内没有颜色过渡,这里应视需要决定是否采用此方法.遍历各个连通域并按上述方法着色的代码如下:
outputImgR=zeros(size(R));
outputImgG=zeros(size(G));
outputImgB=zeros(size(B));
for i=1:domainCount
% 这里不要采用find函数,否则会拖慢处理速度
outputImgR(divDomain==i)=mean(R(divDomain==i));
outputImgB(divDomain==i)=mean(B(divDomain==i));
outputImgG(divDomain==i)=mean(G(divDomain==i));
end
完成连通域内点着色后的图像如下: