在 ”光流跟蹤“ 中,使用了 Harris 角點作為 LK 光流跟蹤輸入點。角點定義為在兩個方向上均有較大梯度變化的小區域,使用自相關函數描述。
自相關函數為為圖像平移前后某一個區域的相似度度量。圖像可以看作二維平面上的連續函數,使用泰勒級數可以將自相關函數轉換為自相關矩陣。
通過分析自相關矩陣的特征值,可以判斷在該區域內各個方向上梯度變化情況,從而決定是否為角點。
在 opencv 中,cv::goodFeatrueToTrack 函數可以提取 Harris 角點,其中參數 useHarrisDetector 決定使用 Harris 判定依據還是 Shi-Tomasi 判定依據。
使用 cv::GFTTDetector 可以實現與 cv::goodFeatrueToTrack 基本一致的功能。然而,cv::GFTTDetector 繼承自 cv::Feature2D,將角點視為通用關鍵點,可以對角點做更多后續處理(如特征點描述)。
以下是 cv::GFTTDetector 的構造函數,其參數與 cv::goodFeatrueToTrack 基本一致,如下:
static Ptr<GFTTDetector> create( int maxCorners=1000, double qualityLevel=0.01, double minDistance=1,
int blockSize=3, bool useHarrisDetector=false, double k=0.04 );
其中,maxCorners 決定提取關鍵點最大個數;
qualityLevel 表示關鍵點強度閾值,只保留大於該閾值的關鍵點(閾值 = 最強關鍵點強度 * qualityLevel);
minDistance 決定關鍵點間的最小距離;
blockSize 決定自相關函數累加區域,也決定了 Sobel 梯度時窗口尺寸;
useHarrisDetector 決定使用 Harris 判定依據還是 Shi-Tomasi 判定依據;
k 僅在使用 Harris 判定依據時有效;
以下代碼使用 cv::GFTTDetector 檢測角點並繪制角點:
1 // 檢測 keypoints 2 std::vector<cv::KeyPoint> key_points; 3 cv::Ptr<cv::GFTTDetector> gftt = cv::GFTTDetector::create(1000, .3, 5., 3, false); 4 gftt->detect(img, key_points); 5 6 // 使用 opencv 內置函數繪制 keypoints 7 cv::Mat key_points_img; 8 cv::drawKeypoints(img, key_points, key_points_img, 9 cv::Scalar(255, 0, 0), cv::DrawMatchesFlags::DEFAULT); 10 11 // 讀取 cv::KeyPoint 繪制 keypoints 12 cv::Mat key_points_img2; 13 cv::cvtColor(img, key_points_img2, CV_GRAY2BGR); 14 for (int i = 0; i < key_points.size(); ++i) 15 { 16 cv::Point c(key_points[i].pt.x, key_points[i].pt.y); 17 float r = key_points[i].size / 2.; 18 int draw_r = r * 2.; 19 cv::circle(key_points_img2, c, draw_r, cv::Scalar(255, 0, 0)); 20 }
cv::GFTTDetector 內部如何實現角點檢測呢,opencv 提供了以下幾個基本函數:
1)void cornerEigenValsAndVecs( InputArray src, OutputArray dst,
int blockSize, int ksize,
int borderType = BORDER_DEFAULT );
2)void cornerHarris( InputArray src, OutputArray dst, int blockSize,
int ksize, double k,
int borderType = BORDER_DEFAULT );
3) void cornerMinEigenVal( InputArray src, OutputArray dst,
int blockSize, int ksize = 3,
int borderType = BORDER_DEFAULT );
其中,函數1 僅僅級數圖像上每個點上自相關函數的特征值與特征向量,函數2 在 函數1 基礎上應用 Harris 判定依據檢測角點,函數3 在 函數1 基礎上應用 Shi-Tomasi 判定依據檢測角點。
以上函數參數含義與 cv::GFTTDetector create 函數基本一致,其中 blockSize 表示自相關函數計算窗口,ksize 表示 Sobel 函數計算窗口。
通過以上函數,可以自己構造一個角點檢測算法:
1 void harrisKeyPoints(cv::Mat& img, 2 std::vector<cv::Point>& key_points, std::vector<cv::Point2f>& eigen_vals) 3 { 4 cv::Mat smt_img; 5 cv::GaussianBlur(img, smt_img, cv::Size(3, 3), 0); 6 7 // 使用 cornerEigenValsAndVecs 提取特征值與特征向量 8 cv::Mat eigen_img = cv::Mat::zeros(img.size(), CV_32FC(6)); 9 cv::cornerEigenValsAndVecs(smt_img, eigen_img, 3, 3); 10 11 // 計算每個點上最小特征值 12 cv::Mat min_eigen_img = cv::Mat::zeros(eigen_img.size(), CV_32FC1); 13 cv::Mat max_eigen_img = cv::Mat::zeros(eigen_img.size(), CV_32FC1); 14 for (int y = 0; y < eigen_img.rows; ++y) 15 { 16 float *eigen_data = eigen_img.ptr<float>(y); 17 float *min_eigen_data = min_eigen_img.ptr<float>(y); 18 float *max_eigen_data = max_eigen_img.ptr<float>(y); 19 for (int x = 0; x < eigen_img.cols; ++x) 20 { 21 float eigen_val1 = eigen_data[x * 6]; 22 float eigen_val2 = eigen_data[x * 6 + 1]; 23 if (eigen_val1 > eigen_val2) 24 std::swap(eigen_val1, eigen_val2); 25 26 min_eigen_data[x] = eigen_val1; 27 max_eigen_data[x] = eigen_val2; 28 } 29 } 30 31 // 計算特征值閾值 32 double min_eigen = 0.; 33 double max_eigen = 0.; 34 minMaxIdx(min_eigen_img, &min_eigen, &max_eigen); 35 float eigen_threshold = max_eigen * .15; // 保留 15% 最大強度特征點 36 37 // 保留滿足條件的特征點 38 const int min_dist_permitted = 10 * 10; // 特征點間最小距離 39 const int max_size_permitted = 50; // 特征點最大數量 40 41 key_points.clear(); 42 eigen_vals.clear(); 43 int key_points_size = 0; 44 45 float max_eigen_val = 0.; 46 float min_eigen_val = 10000.; 47 int min_eigen_val_idx = -1; 48 49 for (int y = 0; y < min_eigen_img.rows; ++y) 50 { 51 float *data = min_eigen_img.ptr<float>(y); 52 for (int x = 0; x < min_eigen_img.cols; ++x) 53 { 54 if (data[x] > eigen_threshold) 55 { 56 if (key_points_size < max_size_permitted) 57 { 58 bool already_exists = false; 59 for (int i = 0; i < key_points_size; ++i) 60 { 61 int dist = (x - key_points[i].x) * (x - key_points[i].x) + 62 (y - key_points[i].y) * (y - key_points[i].y); 63 if (dist < min_dist_permitted) 64 { 65 already_exists = true; 66 if (eigen_vals[i].x < data[x]) 67 { 68 // 列表中存在鄰近點且特征值較小,使用新值替代 69 key_points[i].x = x; 70 key_points[i].y = y; 71 eigen_vals[i].x = data[x]; 72 eigen_vals[i].y = max_eigen_img.ptr<float>(y)[x]; 73 74 if (max_eigen_val < data[x]) 75 max_eigen_val = data[x]; 76 if (min_eigen_val > data[x]) 77 { 78 min_eigen_val = data[x]; 79 min_eigen_val_idx = i; 80 } 81 } 82 } 83 } 84 85 if (!already_exists) 86 { 87 // 列表中不存在鄰近點,添加新值到列表中 88 key_points.push_back(cv::Point(x, y)); 89 eigen_vals.push_back(cv::Point2f(data[x], max_eigen_img.ptr<float>(y)[x])); 90 ++key_points_size; 91 92 if (max_eigen_val < data[x]) 93 max_eigen_val = data[x]; 94 if (min_eigen_val > data[x]) 95 { 96 min_eigen_val = data[x]; 97 min_eigen_val_idx = key_points_size - 1; 98 } 99 } 100 } 101 else 102 { 103 // 特征值小於列表中最小值,不更新 104 if (data[x] <= min_eigen_val) 105 continue; 106 107 // 是否存在距離較近元素 108 bool already_exists = false; 109 for (int i = 0; i < key_points_size; ++i) 110 { 111 int dist = (x - key_points[i].x) * (x - key_points[i].x) + 112 (y - key_points[i].y) * (y - key_points[i].y); 113 if (dist < min_dist_permitted) 114 { 115 already_exists = true; 116 if (eigen_vals[i].x < data[x]) 117 { 118 key_points[i].x = x; 119 key_points[i].y = y; 120 eigen_vals[i].x = data[x]; 121 eigen_vals[i].y = max_eigen_img.ptr<float>(y)[x]; 122 123 if (max_eigen_val < data[x]) 124 max_eigen_val = data[x]; 125 } 126 } 127 } 128 129 // 當不存在較近元素時替換最小元素 130 if (!already_exists) 131 { 132 key_points[min_eigen_val_idx].x = x; 133 key_points[min_eigen_val_idx].y = y; 134 eigen_vals[min_eigen_val_idx].x = data[x]; 135 eigen_vals[min_eigen_val_idx].y = max_eigen_img.ptr<float>(y)[x]; 136 137 // 重新搜索最大最小元素 138 max_eigen_val = 0.; 139 min_eigen_val = 10000.; 140 min_eigen_val_idx = -1; 141 142 for (int i = 0; i < key_points_size; ++i) 143 { 144 float val = eigen_vals[i].x; 145 if (max_eigen_val < val) 146 max_eigen_val = val; 147 if (min_eigen_val > val) 148 { 149 min_eigen_val = val; 150 min_eigen_val_idx = i; 151 } 152 } 153 } 154 } 155 } 156 } 157 } 158 159 // 繪制特征點 160 cv::Mat color_img; 161 cv::cvtColor(img, color_img, CV_GRAY2BGR); 162 for (int i = 0; i < key_points.size(); ++i) 163 cv::circle(color_img, key_points[i], 5, cv::Scalar(255, 0, 0), 1, cv::LINE_AA); 164 165 }
由於每一個關鍵點對應一對特征值,這里使用特征值作為關鍵點的簡單描述符,嘗試關鍵點匹配(效果較差),代碼如下:
1 void matchKeypoints(std::vector<cv::Point>& prev_key_points, std::vector<cv::Point2f>& prev_eigen_vals, 2 std::vector<cv::Point>& next_key_points, std::vector<cv::Point2f>& next_eigen_vals, std::vector<cv::Point>& match) 3 { 4 5 std::vector<int> prev_used; 6 std::vector<int> next_used; 7 for (int i = 0; i < prev_eigen_vals.size(); ++i) 8 prev_used.push_back(0); 9 for (int i = 0; i < next_eigen_vals.size(); ++i) 10 next_used.push_back(0); 11 12 match.clear(); 13 for (int i = 0; i < prev_eigen_vals.size(); ++i) 14 { 15 if (prev_used[i] > 0) 16 continue; 17 18 float min_dist = .0001 * .0001; 19 int min_dist_idx = -1; 20 for (int j = 0; j < next_eigen_vals.size(); ++j) 21 { 22 if (next_used[j] > 0) 23 continue; 24 25 float dist = (prev_eigen_vals[i].x - next_eigen_vals[j].x) * 26 (prev_eigen_vals[i].x - next_eigen_vals[j].x) + 27 (prev_eigen_vals[i].y - next_eigen_vals[j].y) * 28 (prev_eigen_vals[i].y - next_eigen_vals[j].y); 29 if (dist < min_dist) 30 { 31 min_dist = dist; 32 min_dist_idx = j; 33 } 34 } 35 36 if (min_dist_idx < 0) 37 continue; 38 39 float min_dist2 = .0001 * .0001; 40 int min_dist_idx2 = -1; 41 for (int k = 0; k < prev_eigen_vals.size(); ++k) 42 { 43 if (prev_used[k] > 0) 44 continue; 45 46 float dist = (prev_eigen_vals[k].x - next_eigen_vals[min_dist_idx].x) * 47 (prev_eigen_vals[k].x - next_eigen_vals[min_dist_idx].x) + 48 (prev_eigen_vals[k].y - next_eigen_vals[min_dist_idx].y) * 49 (prev_eigen_vals[k].y - next_eigen_vals[min_dist_idx].y); 50 if (dist < min_dist2) 51 { 52 min_dist2 = dist; 53 min_dist_idx2 = k; 54 } 55 } 56 57 if (min_dist < min_dist2) 58 { 59 if (min_dist_idx >= 0) 60 { 61 match.push_back(cv::Point(i, min_dist_idx)); 62 prev_used[i] = 1; 63 next_used[min_dist_idx] = 1; 64 } 65 } 66 else 67 { 68 if (min_dist_idx2 >= 0) 69 { 70 match.push_back(cv::Point(min_dist_idx2, min_dist_idx)); 71 prev_used[min_dist_idx2] = 1; 72 next_used[min_dist_idx] = 1; 73 } 74 } 75 } 76 }
1 // 分別求兩幅圖像關鍵點,將對應特征值向量作為關鍵點描述符 2 std::vector<cv::Point> key_points; 3 std::vector<cv::Point2f> eigen_vals; 4 harrisKeyPoints(img, key_points, eigen_vals, 1); 5 6 std::vector<cv::Point> key_points2; 7 std::vector<cv::Point2f> eigen_vals2; 8 harrisKeyPoints(img2, key_points2, eigen_vals2, 0); 9 10 // 簡單匹配 11 std::vector<cv::Point> match; 12 matchKeypoints(key_points, eigen_vals, key_points2, eigen_vals2, match); 13 14 // 輸出匹配結果 15 cv::Mat match_img = cv::Mat::zeros(img.rows, img.cols * 2 + 4, CV_8UC1); 16 for (int y = 0; y < match_img.rows; ++y) 17 { 18 uchar *dst_data1 = match_img.ptr<uchar>(y); 19 uchar *dst_data2 = dst_data1 + img.cols + 4; 20 uchar *src_data1 = img.ptr<uchar>(y); 21 uchar *src_data2 = img2.ptr<uchar>(y); 22 23 memcpy(dst_data1, src_data1, img.cols); 24 memcpy(dst_data2, src_data2, img2.cols); 25 } 26 27 for (int i = 0; i < match.size(); ++i) 28 { 29 cv::Point pt1(key_points[match[i].x]); 30 cv::Point pt2(key_points2[match[i].y]); 31 pt2.x += img.cols + 4; 32 cv::line(match_img, pt1, pt2, cv::Scalar(255), 2); 33 } 34
參考資料 Learning OpenCV 3 Adrian Kaehler & Gary Bradski