如何識別兩張圖片的重疊區(qū)域

圖片重疊區(qū)域識別有很多應(yīng)用場景,例如全景照片的合成等。這篇文章介紹一種圖片重疊區(qū)域識別的方法。

1. 整體流程

識別重疊區(qū)的步驟包含:

  1. 查找兩張圖片的特征點;
  2. 匹配特征點;
  3. 計算單應(yīng)矩陣;
  4. 基于單應(yīng)矩陣,計算第二張圖片的四個頂點在圖片1中的位置;
  5. 計算 (4) 中得到的位置與圖片邊緣的交點,得到重疊區(qū)域的頂點;
  6. 同理得到圖片1的頂點在圖片2中覆蓋區(qū)域的頂點。

來看代碼和效果:

2. 查找和匹配特征點

找特征點的方式有很多,這里用的是 SIFT,也可以用 SURF、ORB 都行。目的是為了計算出單應(yīng)矩陣。
Matcher 選的是 FlannBasedMatcher, 選擇 BruteForceMatcher 也是可以的。

int main() {
    // 讀取圖片
    auto photo1 = cv::imread("../IMG_1841.jpeg");
    auto photo2 = cv::imread("../IMG_1846.jpeg");
    
    // 分別檢測兩張圖片的特征點和描述子
    cv::Mat descriptor1, descriptor2;
    std::vector<cv::KeyPoint> keyPoint1, keyPoint2;
    cv::Ptr<cv::SiftFeatureDetector> detector = cv::SiftFeatureDetector::create(300);
    detector->detectAndCompute(photo1, cv::Mat(), keyPoint1, descriptor1);
    detector->detectAndCompute(photo2, cv::Mat(), keyPoint2, descriptor2);
    
    // 特征點匹配
    cv::FlannBasedMatcher matcher;
    std::vector<cv::DMatch> matches;
    matcher.match(descriptor1, descriptor2, matches);
   
   // ① ↓

經(jīng)過這兩步我們就能得到匹配的特征點了,但這些匹配對有很多不正確的:


匹配后的特征點

通過漢明距離過濾掉部分匹配對:

    // ① ↓
    
    // 獲取匹配對漢明距離的最小值和最大值
    double minDistance = 1000, maxDistance = 0;
    for (auto& match : matches) {
        double distance = match.distance;
        if (distance < minDistance) { minDistance = distance; }
        if (distance > maxDistance) { maxDistance = distance; }
    }

    // 對匹配對按照漢明距離篩選
    std::vector<cv::DMatch> goodMatches;
    for (auto& match : matches) {
        if (match.distance <= 5 * minDistance) {
            goodMatches.push_back(match);
        }
    }
    
    // ② ↓

過濾后的匹配對:


挑選后的匹配對

不同的照片這一步得到的匹配對數(shù)量不一樣,通常變化不大的場景留下的匹配對會更多。


3. 單應(yīng)矩陣和重疊區(qū)域的計算

單應(yīng)矩陣約束了同一3D空間點在兩個像素平面的2D齊次坐標。有了單應(yīng)矩陣我們就能計算圖片2的頂點在圖片1中應(yīng)該在什么位置。

    // ② ↓
    
    // 找出關(guān)鍵點對應(yīng)的像素坐標
    std::vector<cv::Point2f> points1, points2;
    for (auto &match : goodMatches) {
        points1.push_back(keyPoint2[match.trainIdx].pt);
        points2.push_back(keyPoint1[match.queryIdx].pt);
    }

    // 計算單應(yīng)矩陣
    cv::Mat H = findHomography(points1, points2, cv::RANSAC);
    
    // 拿到圖片的四個頂點
    std::vector<cv::Point> vertex = {
        {0, 0},
        {0, photo2.rows},
        {photo2.cols, photo2.rows},
        {photo2.cols, 0}
    };
    
    // 用于保存 照片2 的頂點 在 照片1 中的位置
    std::vector<cv::Point> vertices2AtPhoto1;

    // 遍歷 圖片2 的四個頂點,根據(jù)單應(yīng)矩陣,計算出這四個頂點在 圖片1 中的位置
    for (auto& point : pointsB) {
        cv::Point temp;
        // 這個方法在下面??
        transformPoint(H, point, temp);
        vertices2AtPhoto1.push_back(temp);
    }
    
    // ④ ↓

其中 transformPoint 為:

/**
 * 計算坐標 in 經(jīng)過 單應(yīng)矩陣 homography 變換回去的坐標,并保存在 out 中。
 */
void transformPoint(const cv::Mat& homography, const cv::Point& in, cv::Point& out) {
    // 頂點的齊次坐標
    cv::Vec3d homogeneous = {(double)in.x, (double)in.y, 1.0};

    // 計算這個點在圖1上的位置
    auto temp = (cv::Mat)(homography * homogeneous);
    cv::Vec3d point = temp.col(0);
    auto x = point[0];
    auto y = point[1];
    auto z = point[2];
    out.x = cvRound(x / z);
    out.y = cvRound(y / z);
}

這樣我們就得到了圖2的頂點在圖1中的位置,但是這個位置可能已經(jīng)超出圖片1的視野外了,所以想要計算重疊區(qū)域,還需要計算圖2變換后的頂點與圖一的交點。交點圍成的多邊形就是重疊區(qū)域。這是個純數(shù)學問題:

    // ④ ↓
    
    // 計算 圖像B 的頂點在圖像A中 和 圖像A邊框 的交點
    std::vector<cv::Point> outIntersection1;
    getPolyIntersections(pointsB, vertices2AtPhoto1, outIntersection1);
    
    // ⑤ ↓ 

其中 getPolyIntersections 及相關(guān) callee 方法我放在了文章最后。

同樣,我們計算出圖1在圖2中的多邊形頂點:

    // ⑤ ↓ 

    // 用同樣的方式,計算出圖片1的四個頂點,在圖片2中與圖片2的交點。
    auto hInverses = H.inv();
    std::vector<cv::Point> outIntersection2;

    // 遍歷 圖片2 的四個頂點,根據(jù)單應(yīng)矩陣,計算出這四個頂點在 圖片1 中的位置
    for (auto& point : vertex) {
        cv::Point temp;
        transformPoint(hInverses, point, temp);
        outIntersection2.push_back(temp);
    }
    
    // ⑥ ↓ 

為了更好的顯示和后續(xù)覆蓋度的計算等,我們可以把這些頂點按照順時針排個序:

    // ⑥ ↓ 
    // 將頂點按照順時針排序,構(gòu)成凸多邊形
    std::vector<cv::Point> poly1;
    convexHull(outIntersection1, poly1, true, true);

    std::vector<cv::Point> poly2;
    convexHull(outIntersection2, poly2, true, true);
    // ⑥ ↓ 

看看效果:

最終效果

綠色區(qū)域就是兩張照片覆蓋的部分。


4. 多邊形計算相關(guān)的方法

getPolyIntersections 及相關(guān) callee 方法:

/**
 * 計算兩個多邊形的交點。
 * 如果沒有交點返回 false。有交點則保存到 outIntersections 中。
 */
bool getPolyIntersections(const std::vector<cv::Point> &poly1, const std::vector<cv::Point> &poly2, std::vector<cv::Point> &outIntersections) {
    // 至少是個三角形
    if (poly1.size() < 3 || poly2.size() < 3) {
        return false;
    }

    // 計算多邊形交點
    long x, y;
    for (auto i = 0; i < poly1.size(); i++) {
        auto nextIdx1 = (i + 1) % poly1.size();
        for (auto j = 0; j < poly2.size(); j++) {
            auto nextIdx2 = (j + 1) % poly2.size();
            // 計算兩個線段的交點
            bool isCross = getSegmentIntersection(
                    poly1[i],
                    poly1[nextIdx1],
                    poly2[j],
                    poly2[nextIdx2],
                    x,
                    y
            );
            if (isCross) {
                outIntersections.emplace_back(x, y);
            }
        }
    }

    // 計算多邊形內(nèi)部點
    for (const auto &point : poly1) {
        if (isPointInPolygon(poly2, point)) {
            outIntersections.push_back(point);
        }
    }
    for (const auto &point : poly2) {
        if (isPointInPolygon(poly1, point)) {
            outIntersections.push_back(point);
        }
    }

    // 如果沒有交點
    return !outIntersections.empty();
}


/**
 * 獲取兩個線段的交點。
 *
 * @retun false, 如果沒有交點。
 */
bool getSegmentIntersection(const cv::Point &p1, const cv::Point &p2, const cv::Point &q1, const cv::Point &q2, long &outX, long &outY) {
    // 判斷兩條線段是否相交
    if (!isSegmentCross(p1.x, p1.y, p2.x, p2.y, q1.x, q1.y, q2.x, q2.y)) {
        return false;
    }

    // 求交點
    long left, right;
    left = (q2.x - q1.x) * (p1.y - p2.y) - (p2.x - p1.x) * (q1.y - q2.y);
    right = (p1.y - q1.y) * (p2.x - p1.x) * (q2.x - q1.x) + q1.x * (q2.y - q1.y) * (p2.x - p1.x) -
            p1.x * (p2.y - p1.y) * (q2.x - q1.x);
    outX = (int) ((double) right / (double) left);

    left = (p1.x - p2.x) * (q2.y - q1.y) - (p2.y - p1.y) * (q1.x - q2.x);
    right = p2.y * (p1.x - p2.x) * (q2.y - q1.y) + (q2.x - p2.x) * (q2.y - q1.y) * (p1.y - p2.y) -
            q2.y * (q1.x - q2.x) * (p2.y - p1.y);
    outY = (int) ((double) right / (double) left);
    return true;
}


/**
 * 判斷兩條線段是否相交。
 */
bool isSegmentCross(double aX, double aY, double bX, double bY,
                    double cX, double cY, double dX, double dY) {
    // 先做排斥實驗,如果兩條線段組成的兩個矩形沒有重疊,則兩條線段一定沒有相交
    if (!isRectCross(aX, aY, bX, bY, cX, cY, dX, dY)) {
        return false;
    }

    // 跨立實驗:
    // 已知: 如果 α × β < 0, 則 β 在 α 的順時針方向;同理,>0則在逆時針方向。
    // 設(shè)線段l1的端點是 A->B,  線段l2的端點是 C->D
    // 先令 α = AC, β = AB 得到 α × β => temp1
    // 再令 β = AB, γ = AD 得到 β × γ => temp2
    // 如果 temp1 · temp2 > 0, 說明 α -> β -> γ 都是相同方向的,也就是 AB 線段可能穿過了 CD 線段
    // 同樣的方式,我們可以算出 CD 線段是否穿過了 AB 線段
    // 如果兩個條件都滿足,則 AB線段 與 CD線段 一定相交
    // 在計算機中,向量叉積表示為:    α × β = α.x · β.y - α.y · β.x

    bool temp1 = mayHasIntersection(aX, aY, bX, bY, cX, cY, dX, dY);
    if (!temp1) {
        return false;
    }

    return mayHasIntersection(cX, cY, dX, dY, aX, aY, bX, bY);
}


/**
 * 從線段1的視角觸發(fā),判斷線段1是否有可能與線段2相交
 */
bool mayHasIntersection(double aX, double aY, double bX, double bY,
                        double cX, double cY, double dX, double dY) {
    auto acX = cX - aX;
    auto acY = cY - aY;
    auto abX = bX - aX;
    auto abY = bY - aY;
    auto adX = dX - aX;
    auto adY = dY - aY;
    auto temp1 = (acX * abY) - (abX * acY);
    auto temp2 = (acX * adY) - (adX * acY);
    return temp1 * temp2 > 0;
}


/**
 * 判斷兩個矩形(豎邊都平行于Y軸)是否有重疊。
 * AB 構(gòu)成一個矩形,CD 構(gòu)成一個矩形。
 */
bool isRectCross(double aX, double aY, double bX, double bY,
                 double cX, double cY, double dX, double dY) {
    // 如果 X 軸沒有相交,肯定沒有重疊
    if (std::min(aX, bX) > std::max(cX, dX) /* R2在R1左邊 */ || std::min(cX, dX) > std::max(aX, bX) /* R1在R2左邊 */ ) {
        return false;
    }

    // 如果 Y 軸沒有相交,肯定沒有重疊
    if (std::min(aY, bY) > std::max(cY, dY) /* R1在R2上邊 */ || std::min(cY, dY) > std::max(aY, bY) /* R2在R1上邊 */  ) {
        return false;
    }

    return true;
}


/**
 * 判斷一個坐標點是否在一個多邊形中。
 */
template<typename Point_t>
static bool isPointInPolygon(const std::vector<Point_t> &poly, const Point_t &pt) {
    size_t i, j;
    bool c = false;
    for (i = 0, j = poly.size() - 1; i < poly.size(); j = i++) {
        if ((((poly[i].y <= pt.y) && (pt.y < poly[j].y)) ||
             ((poly[j].y <= pt.y) && (pt.y < poly[i].y)))
            && (pt.x < (poly[j].x - poly[i].x) * (pt.y - poly[i].y) / (poly[j].y - poly[i].y) + poly[i].x)) {
            c = !c;
        }
    }
    return c;
}

希望對你有用~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容