Qt -使用OpenCV得到SDF
博客主页:【夜泉_ly】
本文专栏:【暂无】
欢迎点赞👍收藏⭐关注❤️
目录
- cv::Mat
- distanceTransform
- 获得SDF
本文的目标,
是简单学习并使用OpenCV的相关函数,
并获得QImage的SDF(Signed Distance Field 有向距离场)
至于我要用QImage的SDF来做什么,嗯,以后再说。
cv::Mat
这个可以理解为OpenCV的QImage,嗯。
简单看看就行。
首先,Mat是可以存QImage并显示的。
其次,Mat是可以手搓的。
我们先手搓一个白底的黑色正方形,看看效果:
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转为灰度图:
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,或全1mat.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 是高,刚好反了:
改了顺序过后就对了:
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 :
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,
那我们可以先做个实验,看看这个距离到底是什么:
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,即欧几里得距离。
第四个是掩码,嗯,意义不明,取三就行。
打印结果如下:
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.215767.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.801477.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.387186.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.972896.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.558596.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.14435.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.730015.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.730015.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.730015.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.730015.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.730015.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.730015.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.730015.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.730015.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.730016.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.14436.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.558596.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.972897.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.387187.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.801478.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,听说这个精确一些:
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.47.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.79697.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.19386.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.59076.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.39386.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.19696 5 4 3 2 1 0 0 0 0 0 0 0 0 0 1 2 3 4 5 66 5 4 3 2 1 0 0 0 0 0 0 0 0 0 1 2 3 4 5 66 5 4 3 2 1 0 0 0 0 0 0 0 0 0 1 2 3 4 5 66 5 4 3 2 1 0 0 0 0 0 0 0 0 0 1 2 3 4 5 66 5 4 3 2 1 0 0 0 0 0 0 0 0 0 1 2 3 4 5 66 5 4 3 2 1 0 0 0 0 0 0 0 0 0 1 2 3 4 5 66 5 4 3 2 1 0 0 0 0 0 0 0 0 0 1 2 3 4 5 66 5 4 3 2 1 0 0 0 0 0 0 0 0 0 1 2 3 4 5 66 5 4 3 2 1 0 0 0 0 0 0 0 0 0 1 2 3 4 5 66.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.19696.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.39386.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.59077.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.19387.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.79698.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。
我们把数据改大,测测效率:
#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:
// 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;
}
再简单测试一下:
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次:
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 就行。
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% 是 0background_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又要多久:
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% 是 0background_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% 是 0background_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语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!