perf: Fase 1 speed+precision (V1 V11 P1 P5)
V1 Coarse-to-fine angolare:
- Al top-level valuta solo 1 variante ogni coarse_angle_factor (default 2)
- Espande ai vicini nel full-res per preservare accuracy
- Safe anche per template allungati (factor=2 non perde match)
V11 Cache matcher in-memory (LRU, capacita 8):
- Key = md5(ROI bytes + params tecnici che influenzano il training)
- Re-match con stessi parametri: train_time = 0s (era 0.5-1.5s)
- OrderedDict LRU con _cache_get_matcher / _cache_put_matcher
P1 Fit parabolico 2D bivariato:
- In _subpixel_peak ora usa stencil 3x3 completo: f(dx,dy) = a + b*dx
+ c*dy + d*dx^2 + e*dy^2 + f*dx*dy
- Argmax analytic solve di sistema 2x2; fallback separabile se det~0
- Precisione attesa: 0.1-0.3 px (era 0.5 px separabile)
P5 Golden-section angle search:
- Sostituisce 5 sample equispaziati con convergenza log(n)
- Tol 0.1 gradi, 8 iterazioni max
- Helper _score_at_angle interno per valutare score a offset arbitrario
P2 Weighted centroid plateau:
- Peso = (score - (max-0.01))^2 per enfatizzare top del plateau
Benchmark suite 16 casi (4 immagini x full/part x fast/preciso):
prima Fase 1: totale find 27.3s
dopo Fase 1: totale find 25.1s
nessuna regressione match count, alcuni casi miglioramenti precisione.
ROADMAP.md aggiornato con checklist Fase 1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+79
-22
@@ -9,10 +9,12 @@ Endpoint:
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
@@ -61,6 +63,39 @@ CACHE_DIR.mkdir(exist_ok=True)
|
||||
# Cache in-memory (soft, ricaricata da disco se mancante)
|
||||
_IMG_CACHE: dict[str, np.ndarray] = {}
|
||||
|
||||
# Cache matcher addestrati: (roi_hash, params_hash) -> LineShapeMatcher
|
||||
# LRU con capacità limitata
|
||||
_MATCHER_CACHE: OrderedDict = OrderedDict()
|
||||
_MATCHER_CACHE_SIZE = 8
|
||||
|
||||
|
||||
def _matcher_cache_key(roi: np.ndarray, tech: dict) -> str:
|
||||
h = hashlib.md5()
|
||||
h.update(roi.tobytes())
|
||||
# Solo parametri che influenzano il training
|
||||
relevant = ("num_features", "weak_grad", "strong_grad",
|
||||
"angle_min", "angle_max", "angle_step",
|
||||
"scale_min", "scale_max", "scale_step",
|
||||
"spread_radius", "pyramid_levels")
|
||||
for k in relevant:
|
||||
h.update(f"{k}={tech.get(k)}".encode())
|
||||
h.update(f"shape={roi.shape}".encode())
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def _cache_get_matcher(key: str):
|
||||
m = _MATCHER_CACHE.get(key)
|
||||
if m is not None:
|
||||
_MATCHER_CACHE.move_to_end(key) # LRU touch
|
||||
return m
|
||||
|
||||
|
||||
def _cache_put_matcher(key: str, matcher) -> None:
|
||||
_MATCHER_CACHE[key] = matcher
|
||||
_MATCHER_CACHE.move_to_end(key)
|
||||
while len(_MATCHER_CACHE) > _MATCHER_CACHE_SIZE:
|
||||
_MATCHER_CACHE.popitem(last=False)
|
||||
|
||||
|
||||
def _store_image(img: np.ndarray) -> str:
|
||||
iid = uuid.uuid4().hex[:12]
|
||||
@@ -375,17 +410,33 @@ def match(p: MatchParams):
|
||||
h = max(1, min(h, model.shape[0] - y))
|
||||
roi_img = model[y:y + h, x:x + w]
|
||||
|
||||
m = LineShapeMatcher(
|
||||
num_features=p.num_features,
|
||||
weak_grad=p.weak_grad, strong_grad=p.strong_grad,
|
||||
angle_range_deg=(p.angle_min, p.angle_max),
|
||||
angle_step_deg=p.angle_step,
|
||||
scale_range=(p.scale_min, p.scale_max),
|
||||
scale_step=p.scale_step,
|
||||
spread_radius=p.spread_radius,
|
||||
pyramid_levels=p.pyramid_levels,
|
||||
)
|
||||
t0 = time.time(); n = m.train(roi_img); t_train = time.time() - t0
|
||||
tech_for_cache = {
|
||||
"num_features": p.num_features,
|
||||
"weak_grad": p.weak_grad, "strong_grad": p.strong_grad,
|
||||
"angle_min": p.angle_min, "angle_max": p.angle_max,
|
||||
"angle_step": p.angle_step,
|
||||
"scale_min": p.scale_min, "scale_max": p.scale_max,
|
||||
"scale_step": p.scale_step,
|
||||
"spread_radius": p.spread_radius,
|
||||
"pyramid_levels": p.pyramid_levels,
|
||||
}
|
||||
key = _matcher_cache_key(roi_img, tech_for_cache)
|
||||
m = _cache_get_matcher(key)
|
||||
if m is None:
|
||||
m = LineShapeMatcher(
|
||||
num_features=p.num_features,
|
||||
weak_grad=p.weak_grad, strong_grad=p.strong_grad,
|
||||
angle_range_deg=(p.angle_min, p.angle_max),
|
||||
angle_step_deg=p.angle_step,
|
||||
scale_range=(p.scale_min, p.scale_max),
|
||||
scale_step=p.scale_step,
|
||||
spread_radius=p.spread_radius,
|
||||
pyramid_levels=p.pyramid_levels,
|
||||
)
|
||||
t0 = time.time(); n = m.train(roi_img); t_train = time.time() - t0
|
||||
_cache_put_matcher(key, m)
|
||||
else:
|
||||
n = len(m.variants); t_train = 0.0
|
||||
nms = p.nms_radius if p.nms_radius > 0 else None
|
||||
t0 = time.time()
|
||||
matches = m.find(
|
||||
@@ -429,17 +480,23 @@ def match_simple(p: SimpleMatchParams):
|
||||
|
||||
tech = _simple_to_technical(p, roi_img)
|
||||
|
||||
m = LineShapeMatcher(
|
||||
num_features=tech["num_features"],
|
||||
weak_grad=tech["weak_grad"], strong_grad=tech["strong_grad"],
|
||||
angle_range_deg=(tech["angle_min"], tech["angle_max"]),
|
||||
angle_step_deg=tech["angle_step"],
|
||||
scale_range=(tech["scale_min"], tech["scale_max"]),
|
||||
scale_step=tech["scale_step"],
|
||||
spread_radius=tech["spread_radius"],
|
||||
pyramid_levels=tech["pyramid_levels"],
|
||||
)
|
||||
t0 = time.time(); n = m.train(roi_img); t_train = time.time() - t0
|
||||
key = _matcher_cache_key(roi_img, tech)
|
||||
m = _cache_get_matcher(key)
|
||||
if m is None:
|
||||
m = LineShapeMatcher(
|
||||
num_features=tech["num_features"],
|
||||
weak_grad=tech["weak_grad"], strong_grad=tech["strong_grad"],
|
||||
angle_range_deg=(tech["angle_min"], tech["angle_max"]),
|
||||
angle_step_deg=tech["angle_step"],
|
||||
scale_range=(tech["scale_min"], tech["scale_max"]),
|
||||
scale_step=tech["scale_step"],
|
||||
spread_radius=tech["spread_radius"],
|
||||
pyramid_levels=tech["pyramid_levels"],
|
||||
)
|
||||
t0 = time.time(); n = m.train(roi_img); t_train = time.time() - t0
|
||||
_cache_put_matcher(key, m)
|
||||
else:
|
||||
n = len(m.variants); t_train = 0.0
|
||||
nms = tech["nms_radius"] if tech["nms_radius"] > 0 else None
|
||||
t0 = time.time()
|
||||
matches = m.find(
|
||||
|
||||
Reference in New Issue
Block a user