如何识别两张图片的重叠区域

图片重叠区域识别有很多应用场景,例如全景照片的合成等。这篇文章介绍一种图片重叠区域识别的方法。

1. 整体流程

识别重叠区的步骤包含:

  1. 查找两张图片的特征点;
  2. 匹配特征点;
  3. 计算单应矩阵;
  4. 基于单应矩阵,计算第二张图片的四个顶点在图片1中的位置;
  5. 计算 (4) 中得到的位置与图片边缘的交点,得到重叠区域的顶点;
  6. 同理得到图片1的顶点在图片2中覆盖区域的顶点。

来看代码和效果:

2. 查找和匹配特征点

找特征点的方式有很多,这里用的是 SIFT,也可以用 SURF、ORB 都行。目的是为了计算出单应矩阵。
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 keyPoint1, keyPoint2;
    cv::Ptr detector = cv::SiftFeatureDetector::create(300);
    detector->detectAndCompute(photo1, cv::Mat(), keyPoint1, descriptor1);
    detector->detectAndCompute(photo2, cv::Mat(), keyPoint2, descriptor2);
    
    // 特征点匹配
    cv::FlannBasedMatcher matcher;
    std::vector matches;
    matcher.match(descriptor1, descriptor2, matches);
   
   // ① ↓

经过这两步我们就能得到匹配的特征点了,但这些匹配对有很多不正确的:


匹配后的特征点

通过汉明距离过滤掉部分匹配对:

    // ① ↓
    
    // 获取匹配对汉明距离的最小值和最大值
    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 goodMatches;
    for (auto& match : matches) {
        if (match.distance <= 5 * minDistance) {
            goodMatches.push_back(match);
        }
    }
    
    // ② ↓

过滤后的匹配对:


挑选后的匹配对

不同的照片这一步得到的匹配对数量不一样,通常变化不大的场景留下的匹配对会更多。


3. 单应矩阵和重叠区域的计算

单应矩阵约束了同一3D空间点在两个像素平面的2D齐次坐标。有了单应矩阵我们就能计算图片2的顶点在图片1中应该在什么位置。

    // ② ↓
    
    // 找出关键点对应的像素坐标
    std::vector points1, points2;
    for (auto &match : goodMatches) {
        points1.push_back(keyPoint2[match.trainIdx].pt);
        points2.push_back(keyPoint1[match.queryIdx].pt);
    }

    // 计算单应矩阵
    cv::Mat H = findHomography(points1, points2, cv::RANSAC);
    
    // 拿到图片的四个顶点
    std::vector vertex = {
        {0, 0},
        {0, photo2.rows},
        {photo2.cols, photo2.rows},
        {photo2.cols, 0}
    };
    
    // 用于保存 照片2 的顶点 在 照片1 中的位置
    std::vector vertices2AtPhoto1;

    // 遍历 图片2 的四个顶点,根据单应矩阵,计算出这四个顶点在 图片1 中的位置
    for (auto& point : pointsB) {
        cv::Point temp;
        // 这个方法在下面
        transformPoint(H, point, temp);
        vertices2AtPhoto1.push_back(temp);
    }
    
    // ④ ↓

其中 transformPoint 为:

/**
 * 计算坐标 in 经过 单应矩阵 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中的位置,但是这个位置可能已经超出图片1的视野外了,所以想要计算重叠区域,还需要计算图2变换后的顶点与图一的交点。交点围成的多边形就是重叠区域。这是个纯数学问题:

    // ④ ↓
    
    // 计算 图像B 的顶点在图像A中 和 图像A边框 的交点
    std::vector outIntersection1;
    getPolyIntersections(pointsB, vertices2AtPhoto1, outIntersection1);
    
    // ⑤ ↓ 

其中 getPolyIntersections 及相关 callee 方法我放在了文章最后。

同样,我们计算出图1在图2中的多边形顶点:

    // ⑤ ↓ 

    // 用同样的方式,计算出图片1的四个顶点,在图片2中与图片2的交点。
    auto hInverses = H.inv();
    std::vector outIntersection2;

    // 遍历 图片2 的四个顶点,根据单应矩阵,计算出这四个顶点在 图片1 中的位置
    for (auto& point : vertex) {
        cv::Point temp;
        transformPoint(hInverses, point, temp);
        outIntersection2.push_back(temp);
    }
    
    // ⑥ ↓ 

为了更好的显示和后续覆盖度的计算等,我们可以把这些顶点按照顺时针排个序:

    // ⑥ ↓ 
    // 将顶点按照顺时针排序,构成凸多边形
    std::vector poly1;
    convexHull(outIntersection1, poly1, true, true);

    std::vector poly2;
    convexHull(outIntersection2, poly2, true, true);
    // ⑥ ↓ 

看看效果:

最终效果

绿色区域就是两张照片覆盖的部分。


4. 多边形计算相关的方法

getPolyIntersections 及相关 callee 方法:

/**
 * 计算两个多边形的交点。
 * 如果没有交点返回 false。有交点则保存到 outIntersections 中。
 */
bool getPolyIntersections(const std::vector &poly1, const std::vector &poly2, std::vector &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);
            }
        }
    }

    // 计算多边形内部点
    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则在逆时针方向。
    // 设线段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的视角触发,判断线段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 构成一个矩形,CD 构成一个矩形。
 */
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
static bool isPointInPolygon(const std::vector &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;
}

希望对你有用~

你可能感兴趣的:(如何识别两张图片的重叠区域)