平滑边缘

原创文章点击这里

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)=(1t)2P0+2t(1t)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(1t)3+3P1t(1t)2+3P2t2(1t)+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);
}
  • 5
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值