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语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!

相关推荐
QuantumStack1 小时前
【C++ 真题】P1104 生日
开发语言·c++·算法
天若有情6731 小时前
01_软件卓越之道:功能性与需求满足
c++·软件工程·软件
whoarethenext1 小时前
使用 C++/OpenCV 和 MFCC 构建双重认证智能门禁系统
开发语言·c++·opencv·mfcc
写个博客2 小时前
暑假算法日记第一天
算法
绿皮的猪猪侠2 小时前
算法笔记上机训练实战指南刷题
笔记·算法·pta·上机·浙大
hie988943 小时前
MATLAB锂离子电池伪二维(P2D)模型实现
人工智能·算法·matlab
Jay_5153 小时前
C++多态与虚函数详解:从入门到精通
开发语言·c++
杰克尼3 小时前
BM5 合并k个已排序的链表
数据结构·算法·链表
追风赶月、3 小时前
【QT】事件(鼠标、按键、定时器、窗口)
qt
.30-06Springfield3 小时前
决策树(Decision tree)算法详解(ID3、C4.5、CART)
人工智能·python·算法·决策树·机器学习