博客主頁:【夜泉_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語言初學者,如果你有任何疑問或建議,歡迎隨時留言討論!讓我們一起學習,共同進步!