平滑边缘

原创文章点击这里

1 Bezier数学原理

二阶贝塞尔曲线

图中,

P

1

P_1

P1​ 为控制点,参数

t

t

t 从

[

0

,

1

]

[0,1]

[0,1] 中取值,可以用下面的公式计算出从

P

0

P_0

P0​ 演变为

P

2

P_2

P2​ 过程中的点:

B

(

t

)

=

(

1

t

)

2

P

0

+

2

t

(

1

t

)

P

1

+

t

2

P

2

,

t

[

0

,

1

]

B(t)=(1-t)^2 P_0 + 2t(1-t)P_1 + t^2P_2, t\in[0,1]

B(t)=(1−t)2P0​+2t(1−t)P1​+t2P2​,t∈[0,1]

三阶贝塞尔曲线

图中,

P

1

P_1

P1​ 和

P

2

P_2

P2​ 为控制点,参数

t

t

t 从

[

0

,

1

]

[0,1]

[0,1] 中取值,可以用下面的公式计算出从

P

0

P_0

P0​ 演变为

P

3

P_3

P3​ 过程中的点:

B

(

t

)

=

P

0

(

1

t

)

3

+

3

P

1

t

(

1

t

)

2

+

3

P

2

t

2

(

1

t

)

+

P

3

t

3

,

t

[

0

,

1

]

B(t)=P_0(1-t)^3+3P_1t(1-t)^2+3P_2t^2(1-t)+P_3t^3,t\in[0,1]

B(t)=P0​(1−t)3+3P1​t(1−t)2+3P2​t2(1−t)+P3​t3,t∈[0,1]

2 平滑有锯齿的多边形

平滑边缘是在 opencv 基础上完成的,主要思路如图所示:

图中,第一步是缩小尺寸,是为了缩小边缘的干扰,然后在缩小图的基础之上做中值滤波。这里为什么不直接对原图做中值滤波呢?主要是因为对原图做中值滤波需要较大的参数,计算量呈指数级增长,经测试,对上面的原图做中值滤波需要把参数设为25,运算时间70ms以上,大大超出了预期,所以采用对缩小图做中值滤波,然后把得到的顶点等比例还原。把顶点等比例还原后,为了减少对后面处理的干扰,需要删除距离很近的点,距离多少算近呢?可以用一个参数控制,随时调节。到这里,已经得到了一个基本轮廓,图案有明显的转折点,最后一步就是来平滑这些转折点。

最后一步的平滑用了 Bezier 三阶曲线公式(看一看上面的公式),所以计算两点之间的曲线需要另外两个控制点,所以需要把所有的控制点找出来,最后再用公式计算曲线上的点,方案参考下图:

步骤说明:

1 找到每条边的中点(或三等分点或n等分点)2 连接顶点两边的中点3 垂直平移该线段直到经过顶点P4 此时线段两端点就是控制点5 用控制点和顶点计算曲线上的点

明白了上面的图,基本就知道写代码的思路了,可能唯一有点疑问的是那条线段平移怎么计算,那就再看下图,在看图之前,得先知道图中的点 A、B 是那两个“中点”,把其中一个中点作为原点,P 是对应的顶点,所以 A、B、P 是已知的三个点:

看了图应该就明白,需要求解的是向量

C

P

\vec{CP}

CP

,两个中点就可以根据这个向量计算出控制。那点 C 的坐标怎么计算,好吧,高中知识,求两条直线的交点,直接看计算结果吧:

中点

A

(

0

,

0

)

A(0,0)

A(0,0) 中点

B

(

a

1

,

b

1

)

B(a_1,b_1)

B(a1​,b1​) 顶点

P

(

a

2

,

b

2

)

P(a_2,b_2)

P(a2​,b2​) 垂足

C

(

a

1

b

1

b

2

+

a

1

a

1

a

2

b

1

b

1

+

a

1

a

1

,

b

1

b

1

b

2

+

a

1

a

2

b

1

b

1

b

1

+

a

1

a

1

)

C(\frac{a_1b_1b_2+a_1a_1a_2}{b_1b_1+a_1a_1}, \frac{b_1b_1b_2+a_1a_2b_1}{b_1b_1+a_1a_1})

C(b1​b1​+a1​a1​a1​b1​b2​+a1​a1​a2​​,b1​b1​+a1​a1​b1​b1​b2​+a1​a2​b1​​)

3 代码

看完了上面这些内容,就可以自己实现了,有兴趣可以参考这里的代码:

主函数

void main()

{

std::string path = "~/mask/" + std::to_string(num) + ".jpg"; // 图片路径

cv::Mat img = cv::imread(path, 1);

// 如果img是多通道的,需要转换为单通道,如果是单通道就不要这三行

std::vector channals;

cv::split(img, channals);

img = channals[0].clone();

// 转换格式

img.convertTo(img, CV_8U, 255.0);

// 缩小尺寸

int Size = 5; // 缩小倍数

int W = 1000, H = 1000; //目标图像的尺寸

cv::resize(img, img, cv::Size(W / Size, H / Size));

// 中值滤波

cv::medianBlur(img, img, 19);

// 提取边缘,得到顶点

std::vector> contours;

std::vector hierarchy;

cv::findContours(img, contours, hierarchy, cv::RETR_EXTERNAL, CV_CHAIN_APPROX_TC89_KCOS, cv::Point());

// 放大顶点

for (ulong i = 0; i < contours.size(); i++) {

for (ulong j = 0; j < contours[i].size(); j++) {

contours[i][j].x *= Size;

contours[i][j].y *= Size;

}

}

//cv::Mat imageContours = cv::Mat::zeros(cv::Size(W, H), CV_8UC1);

// Bezier

for (ulong i = 0; i < contours.size(); i++) {

DeletePoint(contours[i], 20); // 删除比较近的顶点,表示20这个距离内的顶点会删除

Bezier(contours[i], 3, 3); // Bezier,表示在相邻顶点间插入3个曲线点,弯曲弱化程度为3

}

cv::drawContours(imageContours, contours, -1, cv::Scalar(255), 2, 8, hierarchy);

cv::imshow("2", imageContours);

if (cv::waitKey(0))

continue;

}

函数DeletePoint(删除比较近的点)

void DeletePoint(std::vector &contour, int distence)

{

std::vector out;

distence *= distence;

for (ulong i = 0; i < contour.size(); i++) {

if (i != 0) {

cv::Point D = contour[i] - contour[i - 1];

int d = D.x * D.x + D.y * D.y;

if (d < distence) {

continue;

}

}

out.push_back(contour[i]);

}

contour.swap(out);

}

贝塞尔曲线

void Bezier(std::vector &contour, int num, int strength)

{

std::vector out;

std::vector C_L, C_R;

cv::Point P_last, P_CUR, P_next;

for (ulong i = 0; i < contour.size(); i++) {

P_CUR = contour[i];

if (i == 0) {

P_last = contour[contour.size() - 1];

P_next = contour[i + 1];

} else if (i == contour.size() - 1) {

P_last = contour[i - 1];

P_next = contour[0];

} else {

P_last = contour[i - 1];

P_next = contour[i + 1];

}

cv::Point Z_L, Z_R;

Z_L = P_CUR * (strength - 1) / strength + P_last / strength;

Z_R = P_CUR * (strength - 1) / strength + P_next / strength;

cv::Point P_1 = Z_R - Z_L;

cv::Point P_2 = P_CUR - Z_L;

int a1, b1, a2, b2, x, y;

a1 = P_1.x; b1 = P_1.y; a2 = P_2.x; b2 = P_2.y;

if (a1 != 0 && b1 != 0) {

x = (a1 * b1 * b2 + a1 * a1 * a2) / (b1 * b1 + a1 * a1);

y = (b1 * b1 * b2 + a1 * a2 * b1) / (b1 * b1 + a1 * a1);

}

cv::Point MOVE = P_2 - cv::Point(x, y);

Z_L += MOVE;

Z_R += MOVE;

C_L.push_back(Z_L);

C_R.push_back(Z_R);

}

for (ulong i = 0; i < contour.size(); i++) {

out.push_back(contour[i]);

for (int j = 1; j <= num; j++) {

float t = j / (num + 1.0);

cv::Point P_I;

if (i != contour.size() - 1) {

P_I = (1 - t) * (1 - t) * (1 - t) * contour[i]

+ 3 * t * (1 - t) * (1 - t) * C_R[i]

+ 3 * t * t * (1 - t) * C_L[i + 1]

+ t * t * t * contour[i + 1];

} else {

P_I = (1 - t) * (1 - t) * (1 - t) * contour[i]

+ 3 * t * (1 - t) * (1 - t) * C_R[i]

+ 3 * t * t * (1 - t) * C_L[0]

+ t * t * t * contour[0];

}

out.push_back(P_I);

}

}

contour.swap(out);

}