Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b24be4d94 |
+51
-74
@@ -50,6 +50,31 @@ N_BINS = 8 # default: orientamento mod π (no 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:
|
||||
"""IoU tra due poligoni convessi (4 vertici, float32) via cv2.intersectConvexConvex.
|
||||
|
||||
@@ -145,6 +170,7 @@ class LineShapeMatcher:
|
||||
top_score_factor: float = 0.5,
|
||||
n_threads: int | None = None,
|
||||
use_polarity: bool = False,
|
||||
use_gpu: bool = False,
|
||||
) -> None:
|
||||
self.num_features = num_features
|
||||
self.weak_grad = weak_grad
|
||||
@@ -164,6 +190,11 @@ class LineShapeMatcher:
|
||||
# template e' direzionale.
|
||||
self.use_polarity = use_polarity
|
||||
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.template_size: tuple[int, int] = (0, 0)
|
||||
@@ -179,10 +210,15 @@ class LineShapeMatcher:
|
||||
return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||
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)
|
||||
gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3)
|
||||
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) # [-π, π]
|
||||
if self.use_polarity:
|
||||
# Mod 2π: bin 0..15 codifica direzione + polarity edge.
|
||||
@@ -426,19 +462,29 @@ class LineShapeMatcher:
|
||||
"""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).
|
||||
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
|
||||
k = 2 * self.spread_radius + 1
|
||||
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
|
||||
dtype = np.uint16 if nb > 8 else np.uint8
|
||||
spread = np.zeros((H, W), dtype=dtype)
|
||||
for b in range(nb):
|
||||
mask_b = ((bins == b) & valid).astype(np.uint8)
|
||||
d = cv2.dilate(mask_b, kernel)
|
||||
spread |= (d.astype(dtype) << b)
|
||||
if self.use_gpu:
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
@@ -740,63 +786,6 @@ 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,
|
||||
@@ -885,7 +874,6 @@ 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:
|
||||
@@ -1253,17 +1241,6 @@ 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]
|
||||
|
||||
Reference in New Issue
Block a user