""" SIFT / ORB matching: one query image vs the fixed template set. Template keypoints/descriptors are precomputed once at startup and reused for every request -- only the query image is processed per request. """ import cv2 import numpy as np import config _sift = None _orb = None _template_features = {"SIFT": None, "ORB": None} # name -> {template_name: {"kp","des"}} def _get_sift(): global _sift if _sift is None: _sift = cv2.SIFT_create() return _sift def _get_orb(): global _orb if _orb is None: _orb = cv2.ORB_create(nfeatures=config.ORB_N_FEATURES) return _orb def _detector_for(method): return _get_sift() if method == "SIFT" else _get_orb() def _matcher_for(method): if method == "SIFT": index_params = dict(algorithm=1, trees=5) # FLANN_INDEX_KDTREE else: index_params = dict(algorithm=6, table_number=6, key_size=12, multi_probe_level=1) # FLANN_INDEX_LSH search_params = dict(checks=50) return cv2.FlannBasedMatcher(index_params, search_params) def extract_features(method, bgr, mask): gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) detector = _detector_for(method) kp, des = detector.detectAndCompute(gray, mask) return kp, des def set_template_features(method, template_name, kp, des): if _template_features[method] is None: _template_features[method] = {} _template_features[method][template_name] = {"kp": kp, "des": des} def get_template_features(method): return _template_features[method] or {} def match_pair(method, kp_q, des_q, kp_t, des_t): """Returns (inlier_count, confidence_pct).""" if des_q is None or des_t is None or len(kp_q) == 0 or len(kp_t) == 0: return 0, 0.0 matcher = _matcher_for(method) try: raw_matches = matcher.knnMatch(des_q, des_t, k=2) except cv2.error: return 0, 0.0 good = [] for pair in raw_matches: if len(pair) != 2: continue m, n = pair if m.distance < config.LOWE_RATIO * n.distance: good.append(m) if len(good) < config.MIN_RAW_MATCHES: return 0, 0.0 src = np.float32([kp_q[m.queryIdx].pt for m in good]).reshape(-1, 1, 2) dst = np.float32([kp_t[m.trainIdx].pt for m in good]).reshape(-1, 1, 2) _, ransac_mask = cv2.findHomography(src, dst, cv2.RANSAC, 5.0) if ransac_mask is None: return 0, 0.0 inlier_count = int(ransac_mask.sum()) confidence_pct = (inlier_count / len(good)) * 100 if good else 0.0 return inlier_count, confidence_pct def match_against_templates(method, kp_q, des_q): """Returns a list of {"template": name, "score": int, "confidence": float}, sorted by score descending, for every precomputed template.""" results = [] for template_name, data in get_template_features(method).items(): inlier_count, confidence_pct = match_pair( method, kp_q, des_q, data["kp"], data["des"] ) results.append({ "template": template_name, "score": inlier_count, "confidence": round(confidence_pct, 1), }) results.sort(key=lambda r: r["score"], reverse=True) return results