diff --git a/pm2d/line_matcher.py b/pm2d/line_matcher.py index ce035c6..4ea2ebf 100644 --- a/pm2d/line_matcher.py +++ b/pm2d/line_matcher.py @@ -740,6 +740,63 @@ class LineShapeMatcher: s2, cx2, cy2 = _score_at_angle(x2) return best + def _compute_recall( + self, spread0: np.ndarray, variant: _Variant, + cx: float, cy: float, angle_deg: float, + ) -> float: + """Frazione di feature template che combaciano nello spread scena + alla pose (cx, cy, angle, variant.scale). + + Riusa template_gray + warp per estrarre features alla pose esatta + (vs feature pre-computate alla pose della variante grezza). Ritorna + hits/N in [0, 1]. Halcon-equivalent: questo e' il "MinScore" originale. + """ + if self.template_gray is None: + return 1.0 + h, w = self.template_gray.shape + scale = variant.scale + sw = max(16, int(round(w * scale))) + sh = max(16, int(round(h * scale))) + gray_s = cv2.resize(self.template_gray, (sw, sh), interpolation=cv2.INTER_LINEAR) + mask_src = ( + self._train_mask if self._train_mask is not None + else np.full_like(self.template_gray, 255) + ) + mask_s = cv2.resize(mask_src, (sw, sh), interpolation=cv2.INTER_NEAREST) + diag = int(np.ceil(np.hypot(sh, sw))) + 6 + py = (diag - sh) // 2; px = (diag - sw) // 2 + gray_p = cv2.copyMakeBorder( + gray_s, py, diag - sh - py, px, diag - sw - px, cv2.BORDER_REPLICATE, + ) + mask_p = cv2.copyMakeBorder( + mask_s, py, diag - sh - py, px, diag - sw - px, + cv2.BORDER_CONSTANT, value=0, + ) + center = (diag / 2.0, diag / 2.0) + M = cv2.getRotationMatrix2D(center, angle_deg, 1.0) + gray_r = cv2.warpAffine(gray_p, M, (diag, diag), + flags=cv2.INTER_LINEAR, + borderMode=cv2.BORDER_REPLICATE) + mask_r = cv2.warpAffine(mask_p, M, (diag, diag), + flags=cv2.INTER_NEAREST, borderValue=0) + mag, bins = self._gradient(gray_r) + fx, fy, fb = self._extract_features(mag, bins, mask_r) + n_feat = len(fx) + if n_feat < 4: + return 0.0 + H, W = spread0.shape + spread_dtype = spread0.dtype.type + ix = int(round(cx)); iy = int(round(cy)) + hits = 0 + for i in range(n_feat): + xs = ix + int(fx[i] - center[0]) + ys = iy + int(fy[i] - center[1]) + if 0 <= xs < W and 0 <= ys < H: + bit = spread_dtype(1 << int(fb[i])) + if spread0[ys, xs] & bit: + hits += 1 + return hits / n_feat + def _verify_ncc( self, scene_gray: np.ndarray, cx: float, cy: float, angle_deg: float, scale: float, @@ -828,6 +885,7 @@ class LineShapeMatcher: greediness: float = 0.0, batch_top: bool = False, nms_iou_threshold: float = 0.3, + min_recall: float = 0.0, ) -> list[Match]: """ scale_penalty: se > 0, riduce lo score per match a scala diversa da 1.0: @@ -1195,6 +1253,17 @@ class LineShapeMatcher: if float(score_f) < min_score: continue + # Feature recall (Halcon MinScore-style): conta quante feature + # template effettivamente combaciano nello spread scena alla + # pose finale. Scarta se sotto min_recall (default 0 = off). + # Util contro match parziali ad alto NCC ma poche feature reali. + if min_recall > 0.0: + recall = self._compute_recall( + spread0, var, cx_f, cy_f, ang_f, + ) + if recall < min_recall: + continue + # Ri-traslo coord da spazio crop ROI a spazio scena originale. cx_out = cx_f + roi_offset[0] cy_out = cy_f + roi_offset[1]