Qt -使用OpenCV得到SDF

博客主页:【夜泉_ly

本文专栏:【暂无】

欢迎点赞👍收藏⭐关注❤️

目录

本文的目标,

是简单学习并使用OpenCV的相关函数,

并获得QImage的SDF(Signed Distance Field 有向距离场)

至于我要用QImage的SDF来做什么,嗯,以后再说。

cv::Mat

这个可以理解为OpenCV的QImage,嗯。

简单看看就行。

首先,Mat是可以存QImage并显示的。

其次,Mat是可以手搓的。

我们先手搓一个白底的黑色正方形,看看效果:

cpp 复制代码
void Widget::on_pushButton_clicked()
{
    cv::Mat testMat(201, 201, CV_8UC1);
    for(int i = 0; i <= 200; i++){
        for(int j = 0; j <= 200; j++){
            // testMat[i][j] = 0; 这不行呢
            if((90 < i && i < 110) && (90 < j && j < 110)) {
                testMat.at<uchar>(i, j) = 0;   // 0   - 黑
            } else {
                testMat.at<uchar>(i, j) = 255; // 255 - 白
            }
        }
    }
    cv::imshow("testMat", testMat);
}


CV_8UC1 ,表示 8 位单通道,即灰度图,这个之后会用。

先试试把QImage转为灰度图:

cpp 复制代码
cv::Mat image_to_CV_8UC1(const QImage& image){
    int w = image.width(),
        h = image.height();
    cv::Mat mat(w, h, CV_8UC1);
    for(int i = 0; i < w; i++){
        for(int j = 0; j < h; j++){ // 注: 只能设为全0,或全1
            mat.at<uchar>(i, j) = (image.pixelColor(i, j).alpha() == 0) ? 0 : 255;
        }
    }
    if(mat.empty()) return mat;
    cv::imshow("image", mat);
    return mat;
}

跑一下,发现图像被逆时针转了九十度。

cv::Mat 的这个构造,传的分别是 ( 行, 列, 类型)

QImage 的 width 是宽, height 是高,刚好反了:

改了顺序过后就对了:

cpp 复制代码
cv::Mat image_to_CV_8UC1(const QImage& image){
    int r = image.height(), c = image.width();
    cv::Mat mat(r, c, CV_8UC1);
    for(int i = 0; i < r; i++){
        for(int j = 0; j < c; j++){ // 注意下面 image 是 (j, i)
            mat.at<uchar>(i, j) = (image.pixelColor(j, i).alpha() == 0) ? 0 : 255;
        }
    }
    if(mat.empty()) return mat;
    cv::imshow("image", mat);
    return mat;
}

再来试试翻转,我们需要把0变非0,把非0变0,

这个用条件判断加赋值有点慢,

不过,OpenCV 有现成的函数 bitwise_not :

cpp 复制代码
void Widget::on_pushButton_2_clicked(bool checked)
{
    cv::Mat mat_front = image_to_CV_8UC1(_image);
    cv::Mat mat_back;
    cv::bitwise_not(mat_front, mat_back);
    if(checked) cv::imshow("image", mat_front);
    else cv::imshow("image", mat_back);
}

distanceTransform

这个可以用来计算每个非零像素点到最近的零像素点的距离。

嗯,有点抽象,不过刚刚我们学会了手绘 Mat,

那我们可以先做个实验,看看这个距离到底是什么:

cpp 复制代码
void Widget::on_pushButton_3_clicked()
{
    cv::Mat src(21, 21, CV_8UC1);
    for(int r = 0; r <= 20; r++){
        for(int c = 0; c <= 20; c++){
            if((5 < r && r < 15) && (5 < c && c < 15)) {
                src.at<uchar>(r, c) = 0;   // 0   - 黑
            } else {
                src.at<uchar>(r, c) = 255; // 255 - 白
            }
        }
    }
    cv::Mat dst;
    cv::distanceTransform(src, dst, cv::DIST_L2, 3);
    QString ret;
    for(int r = 0; r <= 20; r++){
        for(int c = 0; c <= 20; c++){
            float f = dst.at<float>(r, c);
            ret += QString::number(f).rightJustified(9, ' ');
        }   ret += "\n";
    }
    cv::imshow("dst", dst);
    std::cout << ret.toStdString();
}

哦,关于参数,

第一个是传入的 Mat,类型好像只能是 CV_8UC1,值只能是0或255。

第二个是得到的 Mat,只能用 at<float> 去取到它的值,这个值就是非0到最近0的距离。

第三个是距离的类型,这里用的 cv::DIST_L2,即欧几里得距离。

第四个是掩码,嗯,意义不明,取三就行。

打印结果如下:

cpp 复制代码
  8.21576  7.80147  7.38718  6.97289  6.55859   6.1443  5.73001  5.73001  5.73001  5.73001  5.73001  5.73001  5.73001  5.73001  5.73001   6.1443  6.55859  6.97289  7.38718  7.80147  8.21576
  7.80147  6.84647  6.43217  6.01788  5.60359   5.1893  4.77501  4.77501  4.77501  4.77501  4.77501  4.77501  4.77501  4.77501  4.77501   5.1893  5.60359  6.01788  6.43217  6.84647  7.80147
  7.38718  6.43217  5.47717  5.06288  4.64859   4.2343  3.82001  3.82001  3.82001  3.82001  3.82001  3.82001  3.82001  3.82001  3.82001   4.2343  4.64859  5.06288  5.47717  6.43217  7.38718
  6.97289  6.01788  5.06288  4.10788  3.69359   3.2793  2.86501  2.86501  2.86501  2.86501  2.86501  2.86501  2.86501  2.86501  2.86501   3.2793  3.69359  4.10788  5.06288  6.01788  6.97289
  6.55859  5.60359  4.64859  3.69359  2.73859   2.3243     1.91     1.91     1.91     1.91     1.91     1.91     1.91     1.91     1.91   2.3243  2.73859  3.69359  4.64859  5.60359  6.55859
   6.1443   5.1893   4.2343   3.2793   2.3243  1.36929 0.955002 0.955002 0.955002 0.955002 0.955002 0.955002 0.955002 0.955002 0.955002  1.36929   2.3243   3.2793   4.2343   5.1893   6.1443
  5.73001  4.77501  3.82001  2.86501     1.91 0.955002        0        0        0        0        0        0        0        0        0 0.955002     1.91  2.86501  3.82001  4.77501  5.73001
  5.73001  4.77501  3.82001  2.86501     1.91 0.955002        0        0        0        0        0        0        0        0        0 0.955002     1.91  2.86501  3.82001  4.77501  5.73001
  5.73001  4.77501  3.82001  2.86501     1.91 0.955002        0        0        0        0        0        0        0        0        0 0.955002     1.91  2.86501  3.82001  4.77501  5.73001
  5.73001  4.77501  3.82001  2.86501     1.91 0.955002        0        0        0        0        0        0        0        0        0 0.955002     1.91  2.86501  3.82001  4.77501  5.73001
  5.73001  4.77501  3.82001  2.86501     1.91 0.955002        0        0        0        0        0        0        0        0        0 0.955002     1.91  2.86501  3.82001  4.77501  5.73001
  5.73001  4.77501  3.82001  2.86501     1.91 0.955002        0        0        0        0        0        0        0        0        0 0.955002     1.91  2.86501  3.82001  4.77501  5.73001
  5.73001  4.77501  3.82001  2.86501     1.91 0.955002        0        0        0        0        0        0        0        0        0 0.955002     1.91  2.86501  3.82001  4.77501  5.73001
  5.73001  4.77501  3.82001  2.86501     1.91 0.955002        0        0        0        0        0        0        0        0        0 0.955002     1.91  2.86501  3.82001  4.77501  5.73001
  5.73001  4.77501  3.82001  2.86501     1.91 0.955002        0        0        0        0        0        0        0        0        0 0.955002     1.91  2.86501  3.82001  4.77501  5.73001
   6.1443   5.1893   4.2343   3.2793   2.3243  1.36929 0.955002 0.955002 0.955002 0.955002 0.955002 0.955002 0.955002 0.955002 0.955002  1.36929   2.3243   3.2793   4.2343   5.1893   6.1443
  6.55859  5.60359  4.64859  3.69359  2.73859   2.3243     1.91     1.91     1.91     1.91     1.91     1.91     1.91     1.91     1.91   2.3243  2.73859  3.69359  4.64859  5.60359  6.55859
  6.97289  6.01788  5.06288  4.10788  3.69359   3.2793  2.86501  2.86501  2.86501  2.86501  2.86501  2.86501  2.86501  2.86501  2.86501   3.2793  3.69359  4.10788  5.06288  6.01788  6.97289
  7.38718  6.43217  5.47717  5.06288  4.64859   4.2343  3.82001  3.82001  3.82001  3.82001  3.82001  3.82001  3.82001  3.82001  3.82001   4.2343  4.64859  5.06288  5.47717  6.43217  7.38718
  7.80147  6.84647  6.43217  6.01788  5.60359   5.1893  4.77501  4.77501  4.77501  4.77501  4.77501  4.77501  4.77501  4.77501  4.77501   5.1893  5.60359  6.01788  6.43217  6.84647  7.80147
  8.21576  7.80147  7.38718  6.97289  6.55859   6.1443  5.73001  5.73001  5.73001  5.73001  5.73001  5.73001  5.73001  5.73001  5.73001   6.1443  6.55859  6.97289  7.38718  7.80147  8.21576

额,似乎有偏差?感觉明明该是整数的点却是小数。

不过偏差不大,能用就行。

试试把掩码改为5,听说这个精确一些:

cpp 复制代码
      8.4   7.7969   7.1938   6.5907   6.3938   6.1969        6        6        6        6        6        6        6        6        6   6.1969   6.3938   6.5907   7.1938   7.7969      8.4
   7.7969        7   6.3969   5.7938   5.3938   5.1969        5        5        5        5        5        5        5        5        5   5.1969   5.3938   5.7938   6.3969        7   7.7969
   7.1938   6.3969      5.6   4.9969   4.3938   4.1969        4        4        4        4        4        4        4        4        4   4.1969   4.3938   4.9969      5.6   6.3969   7.1938
   6.5907   5.7938   4.9969      4.2   3.5969   3.1969        3        3        3        3        3        3        3        3        3   3.1969   3.5969      4.2   4.9969   5.7938   6.5907
   6.3938   5.3938   4.3938   3.5969      2.8   2.1969        2        2        2        2        2        2        2        2        2   2.1969      2.8   3.5969   4.3938   5.3938   6.3938
   6.1969   5.1969   4.1969   3.1969   2.1969      1.4        1        1        1        1        1        1        1        1        1      1.4   2.1969   3.1969   4.1969   5.1969   6.1969
        6        5        4        3        2        1        0        0        0        0        0        0        0        0        0        1        2        3        4        5        6
        6        5        4        3        2        1        0        0        0        0        0        0        0        0        0        1        2        3        4        5        6
        6        5        4        3        2        1        0        0        0        0        0        0        0        0        0        1        2        3        4        5        6
        6        5        4        3        2        1        0        0        0        0        0        0        0        0        0        1        2        3        4        5        6
        6        5        4        3        2        1        0        0        0        0        0        0        0        0        0        1        2        3        4        5        6
        6        5        4        3        2        1        0        0        0        0        0        0        0        0        0        1        2        3        4        5        6
        6        5        4        3        2        1        0        0        0        0        0        0        0        0        0        1        2        3        4        5        6
        6        5        4        3        2        1        0        0        0        0        0        0        0        0        0        1        2        3        4        5        6
        6        5        4        3        2        1        0        0        0        0        0        0        0        0        0        1        2        3        4        5        6
   6.1969   5.1969   4.1969   3.1969   2.1969      1.4        1        1        1        1        1        1        1        1        1      1.4   2.1969   3.1969   4.1969   5.1969   6.1969
   6.3938   5.3938   4.3938   3.5969      2.8   2.1969        2        2        2        2        2        2        2        2        2   2.1969      2.8   3.5969   4.3938   5.3938   6.3938
   6.5907   5.7938   4.9969      4.2   3.5969   3.1969        3        3        3        3        3        3        3        3        3   3.1969   3.5969      4.2   4.9969   5.7938   6.5907
   7.1938   6.3969      5.6   4.9969   4.3938   4.1969        4        4        4        4        4        4        4        4        4   4.1969   4.3938   4.9969      5.6   6.3969   7.1938
   7.7969        7   6.3969   5.7938   5.3938   5.1969        5        5        5        5        5        5        5        5        5   5.1969   5.3938   5.7938   6.3969        7   7.7969
      8.4   7.7969   7.1938   6.5907   6.3938   6.1969        6        6        6        6        6        6        6        6        6   6.1969   6.3938   6.5907   7.1938   7.7969      8.4

不过精确当然也有代价,比如运算速度肯定不如 3。

我们把数据改大,测测效率:

cpp 复制代码
#include <QElapsedTimer>
void mask_3_VS_5(const cv::Mat& src, int mask)
{
    QElapsedTimer timer;
    timer.start();

    for (int i = 0; i < 10; i++) {
        cv::Mat dst;
        cv::distanceTransform(src, dst, cv::DIST_L2, mask);
    }

    qint64 elapsed = timer.nsecsElapsed();
    qDebug() << "mask = " << mask << ", Elapsed time:" << elapsed / 1000000.0 << "ms";
}

void Widget::on_pushButton_4_clicked()
{
    cv::Mat src(10000, 10000, CV_8UC1);
    for(int r = 0; r < 10000; r++) for(int c = 0; c < 10000; c++)
        if((rand() % 100) < 50) src.at<uchar>(r, c) = 0;
        else src.at<uchar>(r, c) = 255;
    mask_3_VS_5(src, 5);
    mask_3_VS_5(src, 3);
}

10000 x 10000 的图,跑 10 次, 打印结果如下:

嗯,截图为证:

精度高的运行速度还更快!竟然还有这种好事😋。

获得SDF

我们已经得到了非0点到最近0点距离。

但SDF要求有正有负,即把图分为2份,一个外,一个内:

我们拿到一个QImage,把它转为 Mat。

其中,alpha,即透明度为 0,即纯黑的我们称为 内,其他的称为 外。

额,算了,换个说法,黑色就是障碍物,其他就是背景。

障碍物内的值为 负,外的值为 正。

那么我们拿着两个一减就得到了SDF:

cpp 复制代码
// image 中, alpha为 0 的表示背景
bool image_to_SDF(const QImage& image, cv::Mat* SDF)
{
    int r = image.height(), c = image.width();
    cv::Mat mat_background(r, c, CV_8UC1);
    for(int i = 0; i < r; i++){
        for(int j = 0; j < c; j++){
            mat_background.at<uchar>(i, j) = (image.pixelColor(j, i).alpha() == 0) ? 0 : 255;
        }
    }
    if(mat_background.empty()){
        qDebug() << "传了个空的image计算SDF";
        return false;
    } else {
        qDebug() << "准备计算sdf, 地图大小: rows = " << r << ", cols = " << c;
    }

    cv::Mat mat_background_dst; // 这里面为 0 的是障碍物, 为正的是背景
    cv::distanceTransform(mat_background, mat_background_dst, cv::DIST_L2, 5);

    cv::Mat mat_front(r, c, CV_8UC1); // 这里面为 0 的是障碍物
    cv::Mat mat_front_dst; // 这里面为 0 的是背景, 为正的是障碍物
    cv::bitwise_not(mat_background, mat_front);
    cv::distanceTransform(mat_front, mat_front_dst, cv::DIST_L2, 5);

    *SDF = mat_background_dst - mat_front_dst;
    return true;
}

再简单测试一下:

cpp 复制代码
void Widget::on_pushButton_5_clicked()
{
    QImage image(21, 21, QImage::Format_ARGB32);
    for(int i = 0; i <= 20; i++){
        for(int j = 0; j <= 20; j++){
            if((5 < i && i < 15) && (5 < j && j < 15)) {
                image.setPixelColor(i, j, QColor(0, 0, 0, 0)); // 透明
            } else {
                image.setPixelColor(i, j, QColor(0, 0, 0, 255));
            }
        }
    }

    cv::Mat sdf;
    image_to_SDF(image, &sdf);
    QString ret;
    for(int r = 0; r < sdf.rows; r++){
        for(int c = 0; c < sdf.cols; c++){
            float f = sdf.at<float>(r, c);
            ret += QString::number(f).rightJustified(9, ' ');
        }   ret += "\n";
    }
    cv::imshow("sdf", sdf);
    std::cout << ret.toStdString();
}

输出结果:

不错,和预期的一致。

然后我们再测测性能,用 10000 x 10000 的 image 跑它10次:

cpp 复制代码
void Widget::on_pushButton_6_clicked()
{
    QImage image(10000, 10000, QImage::Format_ARGB32);
    for(int i = 0; i < 10000; i++){
        for(int j = 0; j < 10000; j++){
            int cur = rand() % 500 + 1;
            image.setPixelColor(i, j, QColor(0, 0, 0, cur < 255 ? cur : 0));
        }
    }

    QElapsedTimer timer;
    timer.start();
    qDebug() << "begin:" << timer.nsecsElapsed() / 1000000.0 << "ms";

    for (int i = 0; i < 10; i++) {
        cv::Mat sdf;
        image_to_SDF(image, &sdf);
        qDebug() << "第" << i << "次计算完成, time : " << timer.nsecsElapsed() / 1000000.0 << "ms";
    }

    qDebug() << "end:" << timer.nsecsElapsed() / 1000000.0 << "ms";
}

有点慢,但看了下,主要慢在我们每次都构造了个 cv::Mat sdf,

这里得判断 10000 x 10000次 image 是不是透明的。

那么优化方案就很明显了,我们别每次重新构造 cv::Mat 了,

我们在创建、修改 QImage时,顺便带一个 cv::Mat,

算 SDF 时,直接使用这个 cv::Mat 就行。

cpp 复制代码
bool get_SDF(const cv::Mat& background, cv::Mat* SDF)
{
    if(background.empty()){
        qDebug() << "传了个空的background计算SDF";
        return false;
    } else {
        qDebug() << "准备计算sdf, 地图大小: rows = " << background.rows << ", cols = " << background.cols;
    }

    cv::Mat background_dst; // 这里面为 0 的是障碍物, 为正的是背景
    cv::distanceTransform(background, background_dst, cv::DIST_L2, 5);

    cv::Mat front(background.rows, background.cols, CV_8UC1); // 这里面为 0 的是障碍物
    cv::Mat front_dst; // 这里面为 0 的是背景, 为正的是障碍物
    cv::bitwise_not(background, front);
    cv::distanceTransform(front, front_dst, cv::DIST_L2, 5);

    *SDF = background_dst - front_dst;
    return true;
}

void Widget::on_pushButton_7_clicked()
{
    QImage background_image(10000, 10000, QImage::Format_ARGB32);
    cv::Mat background_mat(10000, 10000, CV_8UC1);
    for(int r = 0; r < 10000; r++){
        for(int c = 0; c < 10000; c++){
            int alpha = (rand() % 2 == 0) ? 0 : (rand() % 255 + 1); // 差不多 50% 是 0
            background_image.setPixelColor(c, r, QColor(0, 0, 0, alpha));
            background_mat.at<uchar>(r, c) = (alpha == 0) ? 0 : 255;
        }
    }

    QElapsedTimer timer;
    timer.start();
    qDebug() << "begin:" << timer.nsecsElapsed() / 1000000.0 << "ms";

    for (int i = 0; i < 10; i++) {
        cv::Mat sdf;
        get_SDF(background_mat, &sdf);
        qDebug() << "第" << i << "次计算完成, time : " << timer.nsecsElapsed() / 1000000.0 << "ms";
    }

    qDebug() << "end:" << timer.nsecsElapsed() / 1000000.0 << "ms";
}

嘿嘿,非常不错,效率高多了。

至于加载,那不是我们关心的,毕竟每个游戏登录时都会让你等半天。

嗯,不过我们可以看看创建一个 10000 x 10000 的 image要多久,以及带上一个 cv::Mat又要多久:

cpp 复制代码
void Widget::on_pushButton_8_clicked()
{
    QElapsedTimer timer;
    timer.start();
    qDebug() << "begin:" << timer.nsecsElapsed() / 1000000.0 << "ms";

    for(int i = 0; i < 1; i++){
        QImage background_image(10000, 10000, QImage::Format_ARGB32);
        for(int r = 0; r < 10000; r++){
            for(int c = 0; c < 10000; c++){
                int alpha = (rand() % 2 == 0) ? 0 : (rand() % 255 + 1); // 差不多 50% 是 0
                background_image.setPixelColor(c, r, QColor(0, 0, 0, alpha));
            }
        }
    }

    qDebug() << "end:" << timer.nsecsElapsed() / 1000000.0 << "ms";

    timer.restart();
    qDebug() << "begin:" << timer.nsecsElapsed() / 1000000.0 << "ms";

    for(int i = 0; i < 1; i++){
        QImage background_image(10000, 10000, QImage::Format_ARGB32);
        cv::Mat background_mat(10000, 10000, CV_8UC1);
        for(int r = 0; r < 10000; r++){
            for(int c = 0; c < 10000; c++){
                int alpha = (rand() % 2 == 0) ? 0 : (rand() % 255 + 1); // 差不多 50% 是 0
                background_image.setPixelColor(c, r, QColor(0, 0, 0, alpha));
                background_mat.at<uchar>(r, c) = (alpha == 0) ? 0 : 255;
            }
        }
    }

    qDebug() << "end:" << timer.nsecsElapsed() / 1000000.0 << "ms";
}

可以看到差不了多久,说明 cv::Mat 的创建还是很快的。


希望本篇文章对你有所帮助!并激发你进一步探索编程的兴趣!

本人仅是个C语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!

相关推荐
搞笑症患者13 分钟前
压缩感知(Compressed Sensing, CS)
算法·最小二乘法·压缩感知·正交匹配追踪omp·迭代阈值it算法
im_AMBER17 分钟前
Leetcode 101 对链表进行插入排序
数据结构·笔记·学习·算法·leetcode·排序算法
mjhcsp23 分钟前
C++ 后缀数组(SA):原理、实现与应用全解析
java·开发语言·c++·后缀数组sa
hui函数24 分钟前
如何解决 pip install 编译报错 ‘cl.exe’ not found(缺少 VS C++ 工具集)问题
开发语言·c++·pip
快手技术35 分钟前
AAAI 2026|全面发力!快手斩获 3 篇 Oral,12 篇论文入选!
前端·后端·算法
颜酱36 分钟前
前端算法必备:滑动窗口从入门到很熟练(最长/最短/计数三大类型)
前端·后端·算法
做科研的周师兄38 分钟前
【MATLAB 实战】栅格数据 K-Means 聚类(分块处理版)—— 解决大数据内存溢出、运行卡顿问题
人工智能·算法·机器学习·matlab·kmeans·聚类
X在敲AI代码39 分钟前
leetcodeD3
数据结构·算法
码农小韩1 小时前
基于Linux的C++学习——循环
linux·c语言·开发语言·c++·算法
消失的旧时光-19431 小时前
C++ 命名空间 namespace 讲透:从 std:: 到工程实践
开发语言·c++