You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用SIFT/SURF等算法检测图像中所有匹配的菱形图案

多菱形目标检测改进实现

问题根源

你的代码只保留了第一个匹配结果,要检测所有菱形,需要筛选全部合格匹配对,再通过单应性变换定位目标,最后用非极大值抑制(NMS)去除重复/重叠的检测框。

推荐实现方案(Python + OpenCV)

用ORB算法(免费无专利限制,可替代SIFT/SURF),步骤如下:

  1. 加载模板图与大图,转为灰度图
  2. 初始化ORB检测器,提取特征点与描述子
  3. 用FLANN匹配器做特征匹配,过滤优质匹配对
  4. 通过单应性变换计算目标位置,生成候选框
  5. 非极大值抑制去除重叠框,保留唯一目标

代码示例

import cv2
import numpy as np

def non_max_suppression(boxes, scores, threshold=0.5):
    if len(boxes) == 0:
        return []
    
    boxes = np.array(boxes)
    scores = np.array(scores)
    
    x1 = boxes[:, 0]
    y1 = boxes[:, 1]
    x2 = boxes[:, 2]
    y2 = boxes[:, 3]
    
    areas = (x2 - x1 + 1) * (y2 - y1 + 1)
    order = scores.argsort()[::-1]
    
    keep = []
    while order.size > 0:
        i = order[0]
        keep.append(i)
        
        xx1 = np.maximum(x1[i], x1[order[1:]])
        yy1 = np.maximum(y1[i], y1[order[1:]])
        xx2 = np.minimum(x2[i], x2[order[1:]])
        yy2 = np.minimum(y2[i], y2[order[1:]])
        
        w = np.maximum(0.0, xx2 - xx1 + 1)
        h = np.maximum(0.0, yy2 - yy1 + 1)
        inter = w * h
        ovr = inter / (areas[i] + areas[order[1:]] - inter)
        
        inds = np.where(ovr <= threshold)[0]
        order = order[inds + 1]
    
    return boxes[keep].astype(int)

# 加载图像
template = cv2.imread('diamond_template.jpg', 0)
img = cv2.imread('poker_card.jpg', 0)
h, w = template.shape

# 初始化ORB检测器
orb = cv2.ORB_create(500)
kp1, des1 = orb.detectAndCompute(template, None)
kp2, des2 = orb.detectAndCompute(img, None)

# FLANN匹配器配置
FLANN_INDEX_LSH = 6
index_params = dict(algorithm=FLANN_INDEX_LSH,
                    table_number=6,
                    key_size=12,
                    multi_probe_level=1)
search_params = dict(checks=50)
flann = cv2.FlannBasedMatcher(index_params, search_params)
matches = flann.knnMatch(des1, des2, k=2)

# Lowe's ratio test过滤优质匹配
good_matches = []
for m, n in matches:
    if m.distance < 0.7 * n.distance:
        good_matches.append(m)

# 收集候选框与匹配得分
boxes = []
scores = []
MIN_MATCH_COUNT = 10
if len(good_matches) > MIN_MATCH_COUNT:
    src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
    dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)
    
    # 多次滑动取匹配子集,覆盖更多目标
    sample_count = 5
    for _ in range(sample_count):
        if len(good_matches) < MIN_MATCH_COUNT:
            break
        # 随机采样匹配对
        sample_indices = np.random.choice(len(good_matches), MIN_MATCH_COUNT, replace=False)
        sample_src = src_pts[sample_indices]
        sample_dst = dst_pts[sample_indices]
        
        M, mask = cv2.findHomography(sample_src, sample_dst, cv2.RANSAC, 5.0)
        if M is not None:
            # 计算目标框
            pts = np.float32([[0, 0], [0, h-1], [w-1, h-1], [w-1, 0]]).reshape(-1, 1, 2)
            dst = cv2.perspectiveTransform(pts, M)
            x_min = int(np.min(dst[:, 0, 0]))
            y_min = int(np.min(dst[:, 0, 1]))
            x_max = int(np.max(dst[:, 0, 0]))
            y_max = int(np.max(dst[:, 0, 1]))
            boxes.append([x_min, y_min, x_max, y_max])
            scores.append(len([m for i, m in enumerate(good_matches) if mask[i]]))

# 非极大值抑制去重
if boxes:
    final_boxes = non_max_suppression(boxes, scores, threshold=0.3)
    # 绘制结果
    img_color = cv2.imread('poker_card.jpg')
    for box in final_boxes:
        x1, y1, x2, y2 = box
        cv2.rectangle(img_color, (x1, y1), (x2, y2), (0, 255, 0), 2)
    cv2.imwrite('detected_result.jpg', img_color)
else:
    print("未检测到足够匹配的目标")

C++实现思路

逻辑与Python完全一致,核心步骤:

  1. 用cv::ORB提取特征点与描述子
  2. cv::FlannBasedMatcher执行特征匹配,过滤优质匹配对
  3. 多次采样匹配对计算单应性矩阵,生成候选检测框
  4. 实现非极大值抑制函数去除重叠框
  5. 绘制最终检测结果

关键代码片段

#include <opencv2/opencv.hpp>
#include <vector>
#include <algorithm>
#include <numeric>

using namespace cv;
using namespace std;

vector<Rect> nonMaxSuppression(vector<Rect> boxes, vector<float> scores, float threshold) {
    vector<Rect> keep;
    if (boxes.empty()) return keep;

    vector<int> indices(boxes.size());
    iota(indices.begin(), indices.end(), 0);
    sort(indices.begin(), indices.end(), [&](int a, int b) { return scores[a] > scores[b]; });

    while (!indices.empty()) {
        int i = indices[0];
        keep.push_back(boxes[i]);

        vector<int> newIndices;
        for (size_t j = 1; j < indices.size(); j++) {
            int idx = indices[j];
            Rect inter = boxes[i] & boxes[idx];
            float areaInter = inter.area();
            float areaUnion = boxes[i].area() + boxes[idx].area() - areaInter;
            float overlap = areaInter / areaUnion;
            if (overlap <= threshold) {
                newIndices.push_back(idx);
            }
        }
        indices = newIndices;
    }
    return keep;
}

int main() {
    Mat templateImg = imread("diamond_template.jpg", IMREAD_GRAYSCALE);
    Mat img = imread("poker_card.jpg", IMREAD_GRAYSCALE);
    int h = templateImg.rows, w = templateImg.cols;

    Ptr<ORB> orb = ORB::create(500);
    vector<KeyPoint> kp1, kp2;
    Mat des1, des2;
    orb->detectAndCompute(templateImg, noArray(), kp1, des1);
    orb->detectAndCompute(img, noArray(), kp2, des2);

    Ptr<FlannBasedMatcher> matcher = FlannBasedMatcher::create(makePtr<flann::LshIndexParams>(6, 12, 1));
    vector<vector<DMatch>> matches;
    matcher->knnMatch(des1, des2, matches, 2);

    vector<DMatch> goodMatches;
    for (auto &m : matches) {
        if (m[0].distance < 0.7 * m[1].distance) {
            goodMatches.push_back(m[0]);
        }
    }

    vector<Rect> boxes;
    vector<float> scores;
    const int MIN_MATCH_COUNT = 10;
    if (goodMatches.size() > MIN_MATCH_COUNT) {
        vector<Point2f> srcPts, dstPts;
        for (auto &m : goodMatches) {
            srcPts.push_back(kp1[m.queryIdx].pt);
            dstPts.push_back(kp2[m.trainIdx].pt);
        }

        int sampleCount = 5;
        for (int i = 0; i < sampleCount; i++) {
            if (goodMatches.size() < MIN_MATCH_COUNT) break;
            vector<int> indices(goodMatches.size());
            iota(indices.begin(), indices.end(), 0);
            random_shuffle(indices.begin(), indices.end());
            vector<Point2f> sampleSrc, sampleDst;
            for (int j = 0; j < MIN_MATCH_COUNT; j++) {
                sampleSrc.push_back(srcPts[indices[j]]);
                sampleDst.push_back(dstPts[indices[j]]);
            }

            Mat M = findHomography(sampleSrc, sampleDst, RANSAC, 5.0);
            if (!M.empty()) {
                vector<Point2f> templateCorners = {Point2f(0,0), Point2f(0,h-1), Point2f(w-1,h-1), Point2f(w-1,0)};
                vector<Point2f> dstCorners;
                perspectiveTransform(templateCorners, dstCorners, M);

                Rect box = boundingRect(dstCorners);
                boxes.push_back(box);
                vector<uchar> mask;
                findHomography(sampleSrc, sampleDst, RANSAC, 5.0, mask);
                scores.push_back(count(mask.begin(), mask.end(), 1));
            }
        }
    }

    vector<Rect> finalBoxes = nonMaxSuppression(boxes, scores, 0.3);
    Mat imgColor = imread("poker_card.jpg");
    for (auto &box : finalBoxes) {
        rectangle(imgColor, box, Scalar(0,255,0), 2);
    }
    imwrite("detected_result.jpg", imgColor);

    return 0;
}

注意事项

  • 单应性变换天然适配菱形的旋转、缩放场景,比传统模板匹配鲁棒性更强
  • 可调整MIN_MATCH_COUNT、匹配距离阈值(0.7)平衡检测精度与召回率
  • 非极大值抑制的阈值(0.3)可根据目标重叠程度灵活调整
  • 若需使用SIFT/SURF,只需替换ORB初始化部分(SIFT需启用OpenCV的xfeatures2d模块)

内容的提问来源于stack exchange,提问作者codeDom

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 01:07:55