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+3P1t(1−t)2+3P2t2(1−t)+P3t3,t∈[0,1]
2 平滑有锯齿的多边形
平滑边缘是在 opencv 基础上完成的,主要思路如图所示:
图中,第一步是缩小尺寸,是为了缩小边缘的干扰,然后在缩小图的基础之上做中值滤波。这里为什么不直接对原图做中值滤波呢?主要是因为对原图做中值滤波需要较大的参数,计算量呈指数级增长,经测试,对上面的原图做中值滤波需要把参数设为25,运算时间70ms以上,大大超出了预期,所以采用对缩小图做中值滤波,然后把得到的顶点等比例还原。把顶点等比例还原后,为了减少对后面处理的干扰,需要删除距离很近的点,距离多少算近呢?可以用一个参数控制,随时调节。到这里,已经得到了一个基本轮廓,图案有明显的转折点,最后一步就是来平滑这些转折点。
最后一步的平滑用了 Bezier 三阶
曲线公式(看一看上面的公式),所以计算两点之间的曲线需要另外两个控制点,所以需要把所有的控制点找出来,最后再用公式计算曲线上的点,方案参考下图:
步骤说明:
- 1 找到每条边的中点(或三等分点或n等分点)
- 2 连接顶点两边的中点
- 3 垂直平移该线段直到经过顶点P
- 4 此时线段两端点就是控制点
- 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(b1b1+a1a1a1b1b2+a1a1a2,b1b1+a1a1b1b1b2+a1a2b1)
3 代码
看完了上面这些内容,就可以自己实现了,有兴趣可以参考这里的代码:
主函数
void main()
{
std::string path = "~/mask/" + std::to_string(num) + ".jpg"; // 图片路径
cv::Mat img = cv::imread(path, 1);
// 如果img是多通道的,需要转换为单通道,如果是单通道就不要这三行
std::vector<cv::Mat> 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<std::vector<cv::Point>> contours;
std::vector<cv::Vec4i> 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<cv::Point> &contour, int distence)
{
std::vector<cv::Point> 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<cv::Point> &contour, int num, int strength)
{
std::vector<cv::Point> out;
std::vector<cv::Point> 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);
}