上一篇笔记中讲到多边形的填充算法,即遍历每个像素点,计算该像素点和多边形任意相邻两个点构成的向量的叉乘结果的方向(即Z值的正负)是否一致,以判别该像素点是否在多边形内部。实验证明该方法是可行的,但是具有局限性,即它只适用于凸多边形,对于凹多边形,我们可以看一下结果。
黄色线框是一个凹多边形的外轮廓,内部的红色区域是上一篇笔记中使用的填充方式的填充结果。
以上结果可以自行分析。那如何解决凹多边形的填充?为了方便说明,我们将多边形的顶点按顺时针做一个标记,如下图
我们基于上一篇笔记的方式进行改进。首先,我们使用上一篇笔记中的填充方式,依次填充三角形ABC,ACD,ADE,那么将得到如下结果
可以看到多边形“饱满”了很多,但是多出了一块多余的“赘肉”,原因也很简单,它是在画ACD和ADE的时候多出来的。
那有什么方式能将这块“赘肉”去掉呢。仔细观察我们发现,如果将AED中减掉ACD的部分,那不就刚好得到想要的填充结果了吗!再进一步寻找着三个三角形的特性,我们发现:ABC和ADE三个点都是顺时针,而ACD的三个点是逆时针!那么我们就可以利用这个特性,得到我们想要的结果了!
所以思路如下
(1)选择一个固定定点(如点A);
(2)顺时针(逆时针也行)依次和其他相邻的两个顶点构成一个三角形(得到三角形ABC,ACD,ADE);
(3)计算每个三角形中固定点(点A)与另外两个顶点组成的两个向量的叉乘结果中Z值的正负;
(4)取一张结果图大小的【中间图】,并全部置0;
(5)填充第(2)步中的所有三角形,并结合第(3)中的Z值结果,对于在三角形中的点,在【中间图】内进行+1或-1;
(6)最终【中间图】中不为0的的点即为填充结果。
核心C代码如下(核心代码带了抗锯齿的处理)
double* pixV = new double[_mainMatImg.rows * _mainMatImg.cols / 2];
memset(pixV, 0, sizeof(double)*_mainMatImg.rows * _mainMatImg.cols / 2);
for (int i = 2; i < polyEdgeNum; ++i)
{
ptIdxs[1] = i - 1;
ptIdxs[2] = i;
// 获取当前三角形的外接框
bounds[0] = _mainMatImg.rows;
bounds[1] = 0;
bounds[2] = _mainMatImg.cols;
bounds[3] = 0;
//
for (int j = 0; j < 3; ++j)
{
if (bounds[0] > pts[ptIdxs[j]][1])bounds[0] = pts[ptIdxs[j]][1];
if (bounds[1] < pts[ptIdxs[j]][1])bounds[1] = pts[ptIdxs[j]][1];
if (bounds[2] > pts[ptIdxs[j]][0])bounds[2] = pts[ptIdxs[j]][0];
if (bounds[3] < pts[ptIdxs[j]][0])bounds[3] = pts[ptIdxs[j]][0];
}
// 遍历外接框,计算每个像素是否在该三角形中
for (int y = bounds[0]; y <= bounds[1]; ++y)
{
for (int x = bounds[2]; x <= bounds[3]; ++x)
{
// 此处将一个像素通过“超分辨率”提升为 antiTime*antiTime 个像素
posPtCnt = 0;
for (int yy = 0; yy < antiTime; ++yy)
{
for (int xx = 0; xx < antiTime; ++xx)
{
// 将每个像素和三角形三个顶点连成向量
for (eNum = 0; eNum < 3; ++eNum)
{
// 求多边形形边向量
normal[0][0] = pts[ptIdxs[(eNum + 1) % 3]][0] - pts[ptIdxs[eNum]][0];
normal[0][1] = pts[ptIdxs[(eNum + 1) % 3]][1] - pts[ptIdxs[eNum]][1];
// 求当前“超分辨率”点和多边形当前边起始点的向量
normal[1][0] = (x - 0.5 + 1.0 / (antiTime * 2) + xx * 1.0 / antiTime) - pts[ptIdxs[eNum]][0];
normal[1][1] = (y - 0.5 + 1.0 / (antiTime * 2) + yy * 1.0 / antiTime) - pts[ptIdxs[eNum]][1];
// 将上述两个向量叉乘以求方向
vxvRst = normal[0][0] * normal[1][1] - normal[0][1] * normal[1][0];
//
if (eNum == 0)
{
ifNegative = (vxvRst > 0 ? false : true);
}
else
{
if (ifNegative == true && vxvRst > 0
|| ifNegative == false && vxvRst < 0)
{
break;
}
}
}
//
if (eNum >= 3)
{
++posPtCnt;
}
}
}
// dTmp即求得的当前像素点的透明度值
dTmp = posPtCnt * 1.0 / (antiTime * antiTime);
//
pixV[y * _mainMatImg.cols / 2 + x] += dTmp * normalPos[i - 2];
}
}
}
结果如下
后记
(1)改算法的适用性:该方式是用于凸多边形吗?如果明白了上面的绘制过程,就会发现该方法同样适用于凸多边形!而且同样适用于类似这种中间挖空的环形多边形
(2)性能如何:改算法不仅更通用,而且相比较于上一篇笔记中使用的填充方式,其计算量甚至更小。以下是两者在同一个凸多边形的填充过程中计算量的结果比较,图中右边部分表示每个像素点的计算量,亮度越高表示该像素点的计算量越大
可以看到下图中的颜色分别较为均匀,亮度也居中。通过输出统计结果得到,上图中所有计算量为351534,单个像素点的最大计算量为8;下图中所有像素点的计算总量为382656,单个像素点的最大计算量为5;上图的单个像素点虽然大于下图,但是由于其将多边形细分为一小块一小块的三角形,所以其总计算量反而减少,边数越多这种差异应该会被拉大。
单纯看图像可能不够直观,我们也看一下两种计算方式的单像素点计算量分布
可以看到旧方式下像素的计算量几种分布在5,而新方式下的像素计算量则分布较为均匀。有兴趣可以增加多边形的边数看看结果是如何变化的。