Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b24be4d94 |
+59
-91
@@ -50,6 +50,31 @@ N_BINS = 8 # default: orientamento mod π (no polarity)
|
|||||||
N_BINS_POL = 16 # use_polarity=True: orientamento mod 2π (con polarity)
|
N_BINS_POL = 16 # use_polarity=True: orientamento mod 2π (con polarity)
|
||||||
|
|
||||||
|
|
||||||
|
def opencl_available() -> bool:
|
||||||
|
"""Ritorna True se OpenCV ha backend OpenCL disponibile (GPU)."""
|
||||||
|
try:
|
||||||
|
return bool(cv2.ocl.haveOpenCL())
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def set_gpu_enabled(enabled: bool) -> bool:
|
||||||
|
"""Abilita/disabilita backend OpenCL globale di OpenCV.
|
||||||
|
|
||||||
|
Quando attivato, Sobel/dilate/warpAffine usano UMat con dispatch
|
||||||
|
automatico a kernel GPU (Intel UHD, AMD, NVIDIA via OpenCL ICD).
|
||||||
|
Speedup tipico: 1.5-3x su Sobel+dilate per scene 1920x1080,
|
||||||
|
overhead trascurabile per scene < 640px (transfer CPU<->GPU domina).
|
||||||
|
|
||||||
|
Halcon-equivalent: 'find_shape_model' con backend GPU integrato.
|
||||||
|
Ritorna True se l'attivazione e' riuscita.
|
||||||
|
"""
|
||||||
|
if not opencl_available():
|
||||||
|
return False
|
||||||
|
cv2.ocl.setUseOpenCL(bool(enabled))
|
||||||
|
return cv2.ocl.useOpenCL()
|
||||||
|
|
||||||
|
|
||||||
def _poly_iou(p1: np.ndarray, p2: np.ndarray) -> float:
|
def _poly_iou(p1: np.ndarray, p2: np.ndarray) -> float:
|
||||||
"""IoU tra due poligoni convessi (4 vertici, float32) via cv2.intersectConvexConvex.
|
"""IoU tra due poligoni convessi (4 vertici, float32) via cv2.intersectConvexConvex.
|
||||||
|
|
||||||
@@ -125,11 +150,6 @@ class _Variant:
|
|||||||
kw: int
|
kw: int
|
||||||
cx_local: float # centro-modello dentro al bbox kernel
|
cx_local: float # centro-modello dentro al bbox kernel
|
||||||
cy_local: float
|
cy_local: float
|
||||||
# Indice template view (X feature - multi-template ensemble).
|
|
||||||
# 0 = template principale del train(); 1+ = view aggiunte via
|
|
||||||
# add_template_view(). Usato in _verify_ncc/_compute_recall per
|
|
||||||
# scegliere il template gray corretto per match.
|
|
||||||
view_idx: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
class LineShapeMatcher:
|
class LineShapeMatcher:
|
||||||
@@ -150,6 +170,7 @@ class LineShapeMatcher:
|
|||||||
top_score_factor: float = 0.5,
|
top_score_factor: float = 0.5,
|
||||||
n_threads: int | None = None,
|
n_threads: int | None = None,
|
||||||
use_polarity: bool = False,
|
use_polarity: bool = False,
|
||||||
|
use_gpu: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.num_features = num_features
|
self.num_features = num_features
|
||||||
self.weak_grad = weak_grad
|
self.weak_grad = weak_grad
|
||||||
@@ -169,17 +190,17 @@ class LineShapeMatcher:
|
|||||||
# template e' direzionale.
|
# template e' direzionale.
|
||||||
self.use_polarity = use_polarity
|
self.use_polarity = use_polarity
|
||||||
self._n_bins = N_BINS_POL if use_polarity else N_BINS
|
self._n_bins = N_BINS_POL if use_polarity else N_BINS
|
||||||
|
# GPU offload per Sobel/dilate/warpAffine via cv2.UMat (OpenCL).
|
||||||
|
# Effettivo solo se opencl_available(); altrimenti silent fallback CPU.
|
||||||
|
self.use_gpu = bool(use_gpu and opencl_available())
|
||||||
|
if self.use_gpu:
|
||||||
|
cv2.ocl.setUseOpenCL(True)
|
||||||
|
|
||||||
self.variants: list[_Variant] = []
|
self.variants: list[_Variant] = []
|
||||||
self.template_size: tuple[int, int] = (0, 0)
|
self.template_size: tuple[int, int] = (0, 0)
|
||||||
self.template_gray: np.ndarray | None = None
|
self.template_gray: np.ndarray | None = None
|
||||||
# Maschera usata in training (propagata al refine per coerenza).
|
# Maschera usata in training (propagata al refine per coerenza).
|
||||||
self._train_mask: np.ndarray | None = None
|
self._train_mask: np.ndarray | None = None
|
||||||
# Multi-template ensemble (X feature): N view dello stesso pezzo
|
|
||||||
# (chiari/scuri, condizioni diverse). Template principale e' [0],
|
|
||||||
# view aggiunte via add_template_view() sono [1+]. Match restituisce
|
|
||||||
# la view che ha matchato meglio.
|
|
||||||
self._view_templates: list[tuple[np.ndarray, np.ndarray | None]] = []
|
|
||||||
|
|
||||||
# --- Helpers -------------------------------------------------------
|
# --- Helpers -------------------------------------------------------
|
||||||
|
|
||||||
@@ -189,10 +210,15 @@ class LineShapeMatcher:
|
|||||||
return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||||
return img
|
return img
|
||||||
|
|
||||||
def _gradient(self, gray: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
def _gradient(self, gray) -> tuple[np.ndarray, np.ndarray]:
|
||||||
|
# Accetta np.ndarray o cv2.UMat (per path GPU OpenCL).
|
||||||
gx = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3)
|
gx = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3)
|
||||||
gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3)
|
gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3)
|
||||||
mag = cv2.magnitude(gx, gy)
|
mag = cv2.magnitude(gx, gy)
|
||||||
|
# Quantizzazione orientation richiede CPU array (np ops): scarica
|
||||||
|
# da GPU se necessario.
|
||||||
|
if isinstance(gx, cv2.UMat):
|
||||||
|
gx = gx.get(); gy = gy.get(); mag = mag.get()
|
||||||
ang = np.arctan2(gy, gx) # [-π, π]
|
ang = np.arctan2(gy, gx) # [-π, π]
|
||||||
if self.use_polarity:
|
if self.use_polarity:
|
||||||
# Mod 2π: bin 0..15 codifica direzione + polarity edge.
|
# Mod 2π: bin 0..15 codifica direzione + polarity edge.
|
||||||
@@ -324,60 +350,8 @@ class LineShapeMatcher:
|
|||||||
self._train_mask = mask_full.copy()
|
self._train_mask = mask_full.copy()
|
||||||
|
|
||||||
self.variants.clear()
|
self.variants.clear()
|
||||||
# Reset view list: template principale = view 0
|
|
||||||
self._view_templates = [(gray.copy(), mask_full.copy())]
|
|
||||||
# Invalida cache feature di refine: il template e cambiato.
|
# Invalida cache feature di refine: il template e cambiato.
|
||||||
self._refine_feat_cache = {}
|
self._refine_feat_cache = {}
|
||||||
self._build_variants_for_view(gray, mask_full, view_idx=0)
|
|
||||||
self._dedup_variants()
|
|
||||||
return len(self.variants)
|
|
||||||
|
|
||||||
def add_template_view(
|
|
||||||
self, template_bgr: np.ndarray, mask: np.ndarray | None = None,
|
|
||||||
) -> int:
|
|
||||||
"""Aggiunge una view template extra all'ensemble (Halcon-style
|
|
||||||
create_aniso_shape_model con fusione N viste).
|
|
||||||
|
|
||||||
Genera varianti del nuovo template con stessi parametri (range
|
|
||||||
angle/scale) e le APPENDE a self.variants. NCC/recall usano
|
|
||||||
automaticamente il template della view che ha matchato.
|
|
||||||
|
|
||||||
Use case: pezzo che cambia aspetto (chiaro/scuro, prima/dopo
|
|
||||||
trattamento, illuminazioni diverse) → un solo matcher resistente.
|
|
||||||
|
|
||||||
Ritorna numero TOTALE varianti dopo l'aggiunta. Le view sono
|
|
||||||
indicizzate da 1 in poi (0 e' il template del train).
|
|
||||||
"""
|
|
||||||
if not self.variants:
|
|
||||||
raise RuntimeError(
|
|
||||||
"Chiamare train(template_principale) prima di add_template_view")
|
|
||||||
gray = self._to_gray(template_bgr)
|
|
||||||
h, w = gray.shape
|
|
||||||
if (w, h) != self.template_size:
|
|
||||||
# Resize per coerenza con bbox/poly
|
|
||||||
gray = cv2.resize(gray, self.template_size, interpolation=cv2.INTER_LINEAR)
|
|
||||||
if mask is not None:
|
|
||||||
mask = cv2.resize(mask, self.template_size, interpolation=cv2.INTER_NEAREST)
|
|
||||||
if mask is None:
|
|
||||||
mask_full = np.full(gray.shape, 255, dtype=np.uint8)
|
|
||||||
else:
|
|
||||||
mask_full = (mask > 0).astype(np.uint8) * 255
|
|
||||||
view_idx = len(self._view_templates)
|
|
||||||
self._view_templates.append((gray.copy(), mask_full.copy()))
|
|
||||||
n_before = len(self.variants)
|
|
||||||
self._build_variants_for_view(gray, mask_full, view_idx=view_idx)
|
|
||||||
self._dedup_variants()
|
|
||||||
return len(self.variants) - n_before
|
|
||||||
|
|
||||||
def _build_variants_for_view(
|
|
||||||
self, gray: np.ndarray, mask_full: np.ndarray, view_idx: int,
|
|
||||||
) -> None:
|
|
||||||
"""Estrae varianti rotate+scalate per UNA view template.
|
|
||||||
|
|
||||||
Estrazione algorithm identica al train() originale, separato per
|
|
||||||
riuso da add_template_view (multi-template ensemble).
|
|
||||||
"""
|
|
||||||
h, w = gray.shape
|
|
||||||
for s in self._scale_list():
|
for s in self._scale_list():
|
||||||
sw = max(16, int(round(w * s)))
|
sw = max(16, int(round(w * s)))
|
||||||
sh = max(16, int(round(h * s)))
|
sh = max(16, int(round(h * s)))
|
||||||
@@ -431,8 +405,9 @@ class LineShapeMatcher:
|
|||||||
levels=levels,
|
levels=levels,
|
||||||
kh=kh, kw=kw,
|
kh=kh, kw=kw,
|
||||||
cx_local=float(cx_local), cy_local=float(cy_local),
|
cx_local=float(cx_local), cy_local=float(cy_local),
|
||||||
view_idx=view_idx,
|
|
||||||
))
|
))
|
||||||
|
self._dedup_variants()
|
||||||
|
return len(self.variants)
|
||||||
|
|
||||||
def _dedup_variants(self) -> int:
|
def _dedup_variants(self) -> int:
|
||||||
"""Rimuove varianti con feature-set identico (post-quantizzazione).
|
"""Rimuove varianti con feature-set identico (post-quantizzazione).
|
||||||
@@ -487,19 +462,29 @@ class LineShapeMatcher:
|
|||||||
"""Spread bitmap: bit b acceso dove bin b è presente nel raggio.
|
"""Spread bitmap: bit b acceso dove bin b è presente nel raggio.
|
||||||
|
|
||||||
dtype: uint8 per N_BINS=8, uint16 per N_BINS_POL=16 (use_polarity).
|
dtype: uint8 per N_BINS=8, uint16 per N_BINS_POL=16 (use_polarity).
|
||||||
|
Se use_gpu=True: Sobel + dilate via cv2.UMat (OpenCL kernel GPU).
|
||||||
"""
|
"""
|
||||||
mag, bins = self._gradient(gray)
|
if self.use_gpu and not isinstance(gray, cv2.UMat):
|
||||||
|
gray_in = cv2.UMat(np.ascontiguousarray(gray))
|
||||||
|
else:
|
||||||
|
gray_in = gray
|
||||||
|
mag, bins = self._gradient(gray_in)
|
||||||
valid = mag >= self.weak_grad
|
valid = mag >= self.weak_grad
|
||||||
k = 2 * self.spread_radius + 1
|
k = 2 * self.spread_radius + 1
|
||||||
kernel = np.ones((k, k), dtype=np.uint8)
|
kernel = np.ones((k, k), dtype=np.uint8)
|
||||||
H, W = gray.shape
|
H, W = (gray.shape if isinstance(gray, np.ndarray)
|
||||||
|
else (gray.get().shape[0], gray.get().shape[1]))
|
||||||
nb = self._n_bins
|
nb = self._n_bins
|
||||||
dtype = np.uint16 if nb > 8 else np.uint8
|
dtype = np.uint16 if nb > 8 else np.uint8
|
||||||
spread = np.zeros((H, W), dtype=dtype)
|
spread = np.zeros((H, W), dtype=dtype)
|
||||||
for b in range(nb):
|
for b in range(nb):
|
||||||
mask_b = ((bins == b) & valid).astype(np.uint8)
|
mask_b = ((bins == b) & valid).astype(np.uint8)
|
||||||
d = cv2.dilate(mask_b, kernel)
|
if self.use_gpu:
|
||||||
spread |= (d.astype(dtype) << b)
|
d = cv2.dilate(cv2.UMat(mask_b), kernel)
|
||||||
|
d_np = d.get()
|
||||||
|
else:
|
||||||
|
d_np = cv2.dilate(mask_b, kernel)
|
||||||
|
spread |= (d_np.astype(dtype) << b)
|
||||||
return spread
|
return spread
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -801,23 +786,9 @@ class LineShapeMatcher:
|
|||||||
s2, cx2, cy2 = _score_at_angle(x2)
|
s2, cx2, cy2 = _score_at_angle(x2)
|
||||||
return best
|
return best
|
||||||
|
|
||||||
def _get_view_template(
|
|
||||||
self, view_idx: int,
|
|
||||||
) -> tuple[np.ndarray | None, np.ndarray | None]:
|
|
||||||
"""Ritorna (template_gray, mask) per la view specificata.
|
|
||||||
|
|
||||||
view_idx 0 = template principale (train), 1+ = view extra
|
|
||||||
aggiunte via add_template_view. Usato per scegliere il template
|
|
||||||
corretto in NCC/recall verification quando il matcher e'
|
|
||||||
ensemble multi-template.
|
|
||||||
"""
|
|
||||||
if 0 <= view_idx < len(self._view_templates):
|
|
||||||
return self._view_templates[view_idx]
|
|
||||||
return self.template_gray, self._train_mask
|
|
||||||
|
|
||||||
def _verify_ncc(
|
def _verify_ncc(
|
||||||
self, scene_gray: np.ndarray, cx: float, cy: float,
|
self, scene_gray: np.ndarray, cx: float, cy: float,
|
||||||
angle_deg: float, scale: float, view_idx: int = 0,
|
angle_deg: float, scale: float,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""NCC tra template warpato alla pose e scena sottostante.
|
"""NCC tra template warpato alla pose e scena sottostante.
|
||||||
|
|
||||||
@@ -829,9 +800,9 @@ class LineShapeMatcher:
|
|||||||
il matcher linemod può dare score alto su texture generiche ma
|
il matcher linemod può dare score alto su texture generiche ma
|
||||||
sovrapponendo il template gray i pixel non corrispondono.
|
sovrapponendo il template gray i pixel non corrispondono.
|
||||||
"""
|
"""
|
||||||
t, train_mask = self._get_view_template(view_idx)
|
if self.template_gray is None:
|
||||||
if t is None:
|
|
||||||
return 1.0
|
return 1.0
|
||||||
|
t = self.template_gray
|
||||||
h, w = t.shape
|
h, w = t.shape
|
||||||
cx_t = (w - 1) / 2.0
|
cx_t = (w - 1) / 2.0
|
||||||
cy_t = (h - 1) / 2.0
|
cy_t = (h - 1) / 2.0
|
||||||
@@ -856,8 +827,8 @@ class LineShapeMatcher:
|
|||||||
t, M, (cw, ch),
|
t, M, (cw, ch),
|
||||||
flags=cv2.INTER_LINEAR, borderValue=0,
|
flags=cv2.INTER_LINEAR, borderValue=0,
|
||||||
)
|
)
|
||||||
if train_mask is not None:
|
if self._train_mask is not None:
|
||||||
mask_src = train_mask
|
mask_src = self._train_mask
|
||||||
else:
|
else:
|
||||||
mask_src = np.full_like(t, 255)
|
mask_src = np.full_like(t, 255)
|
||||||
mask_w = cv2.warpAffine(
|
mask_w = cv2.warpAffine(
|
||||||
@@ -1260,10 +1231,7 @@ class LineShapeMatcher:
|
|||||||
# ranking/visualizzazione (uno score 1.0 vero richiede sia
|
# ranking/visualizzazione (uno score 1.0 vero richiede sia
|
||||||
# match shape sia template gray identici).
|
# match shape sia template gray identici).
|
||||||
if verify_ncc and float(score_f) < ncc_skip_above:
|
if verify_ncc and float(score_f) < ncc_skip_above:
|
||||||
ncc = self._verify_ncc(
|
ncc = self._verify_ncc(gray0, cx_f, cy_f, ang_f, var.scale)
|
||||||
gray0, cx_f, cy_f, ang_f, var.scale,
|
|
||||||
view_idx=getattr(var, "view_idx", 0),
|
|
||||||
)
|
|
||||||
if ncc < verify_threshold:
|
if ncc < verify_threshold:
|
||||||
continue
|
continue
|
||||||
score_f = (float(score_f) + max(0.0, ncc)) * 0.5
|
score_f = (float(score_f) + max(0.0, ncc)) * 0.5
|
||||||
|
|||||||
Reference in New Issue
Block a user