Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d335f866a3 | |||
| 88f80a2cad | |||
| d52d0d0489 | |||
| 9451a418a6 | |||
| 2c9160e4be | |||
| 6d6dcc3b7a | |||
| ee1c4a8f92 |
+179
@@ -0,0 +1,179 @@
|
|||||||
|
"""Benchmark suite per LineShapeMatcher.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python -m pm2d.bench [--quick]
|
||||||
|
|
||||||
|
Misura tempi find() su 3 template-tipo × 3 scene-tipo × N config:
|
||||||
|
- Template: rettangolo 80×80, L-shape 120×120, cerchio 150×150
|
||||||
|
- Scene: pulita 800×600, cluttered 1080×1920, multi-pezzo 1080×1920
|
||||||
|
- Config: baseline, polarity, gpu, pyramid_propagate, greediness=0.7
|
||||||
|
|
||||||
|
Per ogni config stampa: ms/find, ms per fase (profile), n. match.
|
||||||
|
Output tabellare per detectare regressioni in CI.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import time
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from pm2d.line_matcher import LineShapeMatcher, opencl_available
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Sintetizzatori template/scena ----------
|
||||||
|
|
||||||
|
def _tpl_rect() -> np.ndarray:
|
||||||
|
t = np.zeros((80, 80, 3), np.uint8)
|
||||||
|
cv2.rectangle(t, (15, 15), (65, 65), (255, 255, 255), 3)
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def _tpl_lshape() -> np.ndarray:
|
||||||
|
t = np.zeros((120, 120, 3), np.uint8)
|
||||||
|
cv2.rectangle(t, (20, 20), (50, 100), (255, 255, 255), -1)
|
||||||
|
cv2.rectangle(t, (20, 70), (100, 100), (255, 255, 255), -1)
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def _tpl_circle() -> np.ndarray:
|
||||||
|
t = np.zeros((150, 150, 3), np.uint8)
|
||||||
|
cv2.circle(t, (75, 75), 60, (255, 255, 255), 4)
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def _scene_clean(W: int, H: int, n_pieces: int = 1) -> np.ndarray:
|
||||||
|
np.random.seed(0)
|
||||||
|
s = np.zeros((H, W, 3), np.uint8)
|
||||||
|
for _ in range(n_pieces):
|
||||||
|
cx = np.random.randint(80, W - 80)
|
||||||
|
cy = np.random.randint(80, H - 80)
|
||||||
|
cv2.rectangle(s, (cx - 25, cy - 25), (cx + 25, cy + 25), (255, 255, 255), 3)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def _scene_cluttered(W: int, H: int) -> np.ndarray:
|
||||||
|
np.random.seed(0)
|
||||||
|
s = np.random.randint(50, 200, (H, W, 3), np.uint8)
|
||||||
|
cv2.rectangle(s, (300, 200), (350, 250), (255, 255, 255), 3)
|
||||||
|
cv2.rectangle(s, (1500, 800), (1550, 850), (255, 255, 255), 3)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Single benchmark ----------
|
||||||
|
|
||||||
|
def _bench_config(template, scene, config_name: str,
|
||||||
|
init_kw: dict, find_kw: dict,
|
||||||
|
n_iter: int = 5) -> dict:
|
||||||
|
m = LineShapeMatcher(**init_kw)
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
n_var = m.train(template)
|
||||||
|
t_train = time.perf_counter() - t0
|
||||||
|
|
||||||
|
# Warmup (Numba JIT)
|
||||||
|
m.find(scene, **find_kw)
|
||||||
|
m.find(scene, **find_kw)
|
||||||
|
|
||||||
|
# Run
|
||||||
|
times_ms = []
|
||||||
|
for _ in range(n_iter):
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
matches = m.find(scene, **find_kw)
|
||||||
|
times_ms.append((time.perf_counter() - t0) * 1000.0)
|
||||||
|
|
||||||
|
# Profile (1 iter)
|
||||||
|
m.find(scene, profile=True, **find_kw)
|
||||||
|
prof = m.get_last_profile() or {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"config": config_name,
|
||||||
|
"n_variants": n_var,
|
||||||
|
"t_train_s": round(t_train, 3),
|
||||||
|
"ms_avg": round(float(np.mean(times_ms)), 1),
|
||||||
|
"ms_min": round(float(np.min(times_ms)), 1),
|
||||||
|
"ms_max": round(float(np.max(times_ms)), 1),
|
||||||
|
"n_matches": len(matches),
|
||||||
|
"profile_ms": {k: round(v, 1) for k, v in prof.items()},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Suite ----------
|
||||||
|
|
||||||
|
CONFIGS = [
|
||||||
|
("baseline",
|
||||||
|
{"angle_step_deg": 10, "pyramid_levels": 2},
|
||||||
|
{"min_score": 0.4, "verify_threshold": 0.2}),
|
||||||
|
("polarity",
|
||||||
|
{"angle_step_deg": 10, "pyramid_levels": 2, "use_polarity": True},
|
||||||
|
{"min_score": 0.4, "verify_threshold": 0.2}),
|
||||||
|
("propagate",
|
||||||
|
{"angle_step_deg": 10, "pyramid_levels": 3},
|
||||||
|
{"min_score": 0.4, "verify_threshold": 0.2,
|
||||||
|
"pyramid_propagate": True, "propagate_topk": 4}),
|
||||||
|
("greedy_07",
|
||||||
|
{"angle_step_deg": 10, "pyramid_levels": 2},
|
||||||
|
{"min_score": 0.4, "verify_threshold": 0.2, "greediness": 0.7}),
|
||||||
|
("stride2",
|
||||||
|
{"angle_step_deg": 10, "pyramid_levels": 2},
|
||||||
|
{"min_score": 0.4, "verify_threshold": 0.2, "coarse_stride": 2}),
|
||||||
|
]
|
||||||
|
|
||||||
|
if opencl_available():
|
||||||
|
CONFIGS.append(
|
||||||
|
("gpu_umat",
|
||||||
|
{"angle_step_deg": 10, "pyramid_levels": 2, "use_gpu": True},
|
||||||
|
{"min_score": 0.4, "verify_threshold": 0.2})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
SCENARIOS = [
|
||||||
|
("rect_80 vs scene_800x600", _tpl_rect, lambda: _scene_clean(800, 600, 1)),
|
||||||
|
("lshape_120 vs scene_1080x1920_clutter",
|
||||||
|
_tpl_lshape, lambda: _scene_cluttered(1920, 1080)),
|
||||||
|
("circle_150 vs scene_clean_3pieces",
|
||||||
|
_tpl_circle, lambda: _scene_clean(1920, 1080, 3)),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def run(quick: bool = False) -> int:
|
||||||
|
n_iter = 2 if quick else 5
|
||||||
|
print(f"=== PM2D Benchmark Suite ({len(SCENARIOS)} scenarios x "
|
||||||
|
f"{len(CONFIGS)} configs, n_iter={n_iter}) ===\n")
|
||||||
|
rows = []
|
||||||
|
for sc_name, tpl_fn, scn_fn in SCENARIOS:
|
||||||
|
template = tpl_fn()
|
||||||
|
scene = scn_fn()
|
||||||
|
print(f"--- Scenario: {sc_name} (tpl={template.shape}, "
|
||||||
|
f"scn={scene.shape}) ---")
|
||||||
|
for cfg_name, init_kw, find_kw in CONFIGS:
|
||||||
|
r = _bench_config(template, scene, cfg_name, init_kw, find_kw,
|
||||||
|
n_iter=n_iter)
|
||||||
|
r["scenario"] = sc_name
|
||||||
|
rows.append(r)
|
||||||
|
prof_str = " ".join(
|
||||||
|
f"{k}={v:.1f}" for k, v in r["profile_ms"].items()
|
||||||
|
)
|
||||||
|
print(f" {cfg_name:14s} {r['ms_avg']:6.1f}ms "
|
||||||
|
f"(min {r['ms_min']:.1f} max {r['ms_max']:.1f}) "
|
||||||
|
f"vars={r['n_variants']:3d} "
|
||||||
|
f"matches={r['n_matches']:2d}")
|
||||||
|
if prof_str:
|
||||||
|
print(f" profile: {prof_str}")
|
||||||
|
print()
|
||||||
|
print("=== Done ===")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
p = argparse.ArgumentParser(description="PM2D benchmark suite")
|
||||||
|
p.add_argument("--quick", action="store_true",
|
||||||
|
help="2 iterazioni per config invece di 5 (smoke test)")
|
||||||
|
args = p.parse_args(argv)
|
||||||
|
return run(quick=args.quick)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
sys.exit(main())
|
||||||
+91
-4
@@ -736,7 +736,24 @@ class LineShapeMatcher:
|
|||||||
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)
|
||||||
|
# XX optimization: skip dilate per bin senza pixel attivi.
|
||||||
|
# Su scene a bassa varianza orientation (es. pezzi industriali con
|
||||||
|
# poche direzioni dominanti) tipicamente 50-70% dei bin sono vuoti.
|
||||||
|
# Pre-calcolo bin presenti via mask globale; per bin assenti niente
|
||||||
|
# dilate (resta zero nel bitmap).
|
||||||
|
if isinstance(bins, np.ndarray):
|
||||||
|
valid_bins = bins[valid] if isinstance(valid, np.ndarray) else None
|
||||||
|
if valid_bins is not None and valid_bins.size > 0:
|
||||||
|
bin_present = np.zeros(nb, dtype=bool)
|
||||||
|
unique_bins = np.unique(valid_bins)
|
||||||
|
bin_present[unique_bins[unique_bins < nb]] = True
|
||||||
|
else:
|
||||||
|
bin_present = np.zeros(nb, dtype=bool)
|
||||||
|
else:
|
||||||
|
bin_present = np.ones(nb, dtype=bool)
|
||||||
for b in range(nb):
|
for b in range(nb):
|
||||||
|
if not bin_present[b]:
|
||||||
|
continue # XX: nessun pixel di questo bin sopra weak_grad
|
||||||
mask_b = ((bins == b) & valid).astype(np.uint8)
|
mask_b = ((bins == b) & valid).astype(np.uint8)
|
||||||
if self.use_gpu:
|
if self.use_gpu:
|
||||||
d = cv2.dilate(cv2.UMat(mask_b), kernel)
|
d = cv2.dilate(cv2.UMat(mask_b), kernel)
|
||||||
@@ -937,7 +954,7 @@ class LineShapeMatcher:
|
|||||||
# variante e' quantizzato a multipli di angle_step (5 deg default).
|
# variante e' quantizzato a multipli di angle_step (5 deg default).
|
||||||
# Refine angolare e' essenziale per orientamento sub-step.
|
# Refine angolare e' essenziale per orientamento sub-step.
|
||||||
if search_radius is None:
|
if search_radius is None:
|
||||||
search_radius = self._effective_angle_step() / 2.0
|
search_radius = self._effective_angle_step()
|
||||||
|
|
||||||
h, w = template_gray.shape
|
h, w = template_gray.shape
|
||||||
sw = max(16, int(round(w * scale)))
|
sw = max(16, int(round(w * scale)))
|
||||||
@@ -1025,8 +1042,12 @@ class LineShapeMatcher:
|
|||||||
# Score all'origine come riferimento (ang offset 0)
|
# Score all'origine come riferimento (ang offset 0)
|
||||||
s0, cx0_s, cy0_s = _score_at_angle(0.0)
|
s0, cx0_s, cy0_s = _score_at_angle(0.0)
|
||||||
best = (angle_deg, s0, cx0_s, cy0_s)
|
best = (angle_deg, s0, cx0_s, cy0_s)
|
||||||
tol = 0.1 # gradi
|
# Precisione angolare: 10 iter golden con tol 0.05 deg.
|
||||||
for _ in range(8):
|
# Compromesso speed/accuracy: il parabolic fit aggiuntivo era
|
||||||
|
# instabile su score map non-smooth (template simmetrici producono
|
||||||
|
# multipli local max ravvicinati che lo facevano divergere).
|
||||||
|
tol = 0.05
|
||||||
|
for _ in range(10):
|
||||||
if s1 > best[1]:
|
if s1 > best[1]:
|
||||||
best = (angle_deg + x1, s1, cx1, cy1)
|
best = (angle_deg + x1, s1, cx1, cy1)
|
||||||
if s2 > best[1]:
|
if s2 > best[1]:
|
||||||
@@ -1358,6 +1379,7 @@ class LineShapeMatcher:
|
|||||||
use_soft_score: bool = False,
|
use_soft_score: bool = False,
|
||||||
subpixel_lm: bool = False,
|
subpixel_lm: bool = False,
|
||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
|
profile: bool = False,
|
||||||
) -> list[Match]:
|
) -> list[Match]:
|
||||||
"""
|
"""
|
||||||
scale_penalty: se > 0, riduce lo score per match a scala diversa da 1.0:
|
scale_penalty: se > 0, riduce lo score per match a scala diversa da 1.0:
|
||||||
@@ -1390,6 +1412,7 @@ class LineShapeMatcher:
|
|||||||
"drop_recall_low": 0,
|
"drop_recall_low": 0,
|
||||||
"drop_bbox_out_of_scene": 0,
|
"drop_bbox_out_of_scene": 0,
|
||||||
"drop_nms_iou": 0,
|
"drop_nms_iou": 0,
|
||||||
|
"n_variants_pruned_histogram": 0,
|
||||||
"n_final": 0,
|
"n_final": 0,
|
||||||
"top_thresh_used": 0.0,
|
"top_thresh_used": 0.0,
|
||||||
"verify_threshold_used": float(verify_threshold),
|
"verify_threshold_used": float(verify_threshold),
|
||||||
@@ -1401,7 +1424,21 @@ class LineShapeMatcher:
|
|||||||
}
|
}
|
||||||
self._last_diag = diag
|
self._last_diag = diag
|
||||||
|
|
||||||
|
# GGG: profile mode → timing per fase, esposto via get_last_profile()
|
||||||
|
import time as _time
|
||||||
|
prof = {} if profile else None
|
||||||
|
_t_prev = _time.perf_counter() if profile else 0.0
|
||||||
|
def _checkpoint(name: str):
|
||||||
|
nonlocal _t_prev
|
||||||
|
if prof is None:
|
||||||
|
return
|
||||||
|
now = _time.perf_counter()
|
||||||
|
prof[name] = (now - _t_prev) * 1000.0 # ms
|
||||||
|
_t_prev = now
|
||||||
|
self._last_profile = prof
|
||||||
|
|
||||||
gray_full = self._to_gray(scene_bgr)
|
gray_full = self._to_gray(scene_bgr)
|
||||||
|
_checkpoint("to_gray")
|
||||||
# Applica ROI di ricerca: restringe scena a crop, ricorda offset per
|
# Applica ROI di ricerca: restringe scena a crop, ricorda offset per
|
||||||
# ri-traslare le coordinate dei match a fine pipeline.
|
# ri-traslare le coordinate dei match a fine pipeline.
|
||||||
if search_roi is not None:
|
if search_roi is not None:
|
||||||
@@ -1440,6 +1477,7 @@ class LineShapeMatcher:
|
|||||||
spread0 = None
|
spread0 = None
|
||||||
bit_active_full = None
|
bit_active_full = None
|
||||||
density_full = None
|
density_full = None
|
||||||
|
_checkpoint("spread_top")
|
||||||
if nms_radius is None:
|
if nms_radius is None:
|
||||||
nms_radius = max(8, min(self.template_size) // 2)
|
nms_radius = max(8, min(self.template_size) // 2)
|
||||||
# Pruning adattivo allo step angolare: con step piccolo (<= 3 deg)
|
# Pruning adattivo allo step angolare: con step piccolo (<= 3 deg)
|
||||||
@@ -1501,6 +1539,38 @@ class LineShapeMatcher:
|
|||||||
end = min(n, i + half + 1)
|
end = min(n, i + half + 1)
|
||||||
neighbor_map[vi_c] = vi_sorted[start:end]
|
neighbor_map[vi_c] = vi_sorted[start:end]
|
||||||
|
|
||||||
|
# VV: pruning preliminare via overlap istogramma orientation.
|
||||||
|
# Scene-bins-attivi vs variant-feature-bins. Se la variante ha bin
|
||||||
|
# dominanti che la scena non possiede → score impossibile, skip
|
||||||
|
# senza chiamare il kernel. Costo: O(n_variants * 8 ops).
|
||||||
|
scene_bins = np.array(
|
||||||
|
[bool((bit_active_top >> b) & 1) for b in range(self._n_bins)],
|
||||||
|
dtype=bool,
|
||||||
|
)
|
||||||
|
if scene_bins.any():
|
||||||
|
n_scene_active = int(scene_bins.sum())
|
||||||
|
# Soglia: variante deve avere >= 50% delle sue feature in bin
|
||||||
|
# presenti nella scena. Sotto = score certamente < 0.5.
|
||||||
|
pruned_idx_list = []
|
||||||
|
n_pruned = 0
|
||||||
|
for vi in coarse_idx_list:
|
||||||
|
lvl = self.variants[vi].levels[
|
||||||
|
min(top, len(self.variants[vi].levels) - 1)
|
||||||
|
]
|
||||||
|
if len(lvl.bin) == 0:
|
||||||
|
continue
|
||||||
|
feat_in_scene = int(np.isin(lvl.bin, np.where(scene_bins)[0]).sum())
|
||||||
|
ratio = feat_in_scene / len(lvl.bin)
|
||||||
|
if ratio < 0.5 * min_score:
|
||||||
|
n_pruned += 1
|
||||||
|
continue
|
||||||
|
pruned_idx_list.append(vi)
|
||||||
|
if n_pruned > 0 and pruned_idx_list:
|
||||||
|
coarse_idx_list = pruned_idx_list
|
||||||
|
diag["n_variants_pruned_histogram"] = n_pruned
|
||||||
|
else:
|
||||||
|
diag["n_variants_pruned_histogram"] = 0
|
||||||
|
|
||||||
# Pruning varianti via top-level (parallelizzato).
|
# Pruning varianti via top-level (parallelizzato).
|
||||||
# coarse_stride > 1: 1 pixel ogni stride (~stride^2 speed-up).
|
# coarse_stride > 1: 1 pixel ogni stride (~stride^2 speed-up).
|
||||||
# pyramid_propagate=True: top-K picchi per restringere full-res.
|
# pyramid_propagate=True: top-K picchi per restringere full-res.
|
||||||
@@ -1596,6 +1666,7 @@ class LineShapeMatcher:
|
|||||||
kept_variants: list[tuple[int, float]] = [
|
kept_variants: list[tuple[int, float]] = [
|
||||||
(vi, score_by_vi[vi]) for vi in expanded
|
(vi, score_by_vi[vi]) for vi in expanded
|
||||||
]
|
]
|
||||||
|
_checkpoint("top_pruning")
|
||||||
|
|
||||||
if not kept_variants:
|
if not kept_variants:
|
||||||
return []
|
return []
|
||||||
@@ -1702,6 +1773,7 @@ class LineShapeMatcher:
|
|||||||
|
|
||||||
raw.sort(key=lambda c: -c[0])
|
raw.sort(key=lambda c: -c[0])
|
||||||
diag["n_raw_candidates"] = len(raw)
|
diag["n_raw_candidates"] = len(raw)
|
||||||
|
_checkpoint("full_kernel")
|
||||||
|
|
||||||
# Mappa vi → score_map per subpixel/refinement
|
# Mappa vi → score_map per subpixel/refinement
|
||||||
score_maps = dict(candidates_per_var)
|
score_maps = dict(candidates_per_var)
|
||||||
@@ -1757,7 +1829,10 @@ class LineShapeMatcher:
|
|||||||
ang_f, score_f, cx_f, cy_f = self._refine_angle(
|
ang_f, score_f, cx_f, cy_f = self._refine_angle(
|
||||||
spread0, bit_active_full, self.template_gray, cx_f, cy_f,
|
spread0, bit_active_full, self.template_gray, cx_f, cy_f,
|
||||||
var.angle_deg, var.scale, mask_full,
|
var.angle_deg, var.scale, mask_full,
|
||||||
search_radius=self._effective_angle_step() / 2.0,
|
# Search radius esteso allo step pieno (era step/2):
|
||||||
|
# copre il caso peggiore in cui il picco vero e' all'estremo
|
||||||
|
# del bin angolare della variante grezza.
|
||||||
|
search_radius=self._effective_angle_step(),
|
||||||
original_score=score,
|
original_score=score,
|
||||||
)
|
)
|
||||||
# Halcon SubPixel='least_squares_high': refinement iterativo
|
# Halcon SubPixel='least_squares_high': refinement iterativo
|
||||||
@@ -1869,6 +1944,9 @@ class LineShapeMatcher:
|
|||||||
if len(kept) >= max_matches:
|
if len(kept) >= max_matches:
|
||||||
break
|
break
|
||||||
diag["n_final"] = len(kept)
|
diag["n_final"] = len(kept)
|
||||||
|
_checkpoint("refine_verify_nms")
|
||||||
|
if profile:
|
||||||
|
self._last_profile = prof
|
||||||
if debug:
|
if debug:
|
||||||
# Debug mode: stampa diagnostica su stderr per visibilita' immediata.
|
# Debug mode: stampa diagnostica su stderr per visibilita' immediata.
|
||||||
import sys as _sys
|
import sys as _sys
|
||||||
@@ -1892,6 +1970,15 @@ class LineShapeMatcher:
|
|||||||
f"final={diag['n_final']} (top_thresh={diag['top_thresh_used']:.2f})"
|
f"final={diag['n_final']} (top_thresh={diag['top_thresh_used']:.2f})"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_last_profile(self) -> dict | None:
|
||||||
|
"""Ritorna timing per fase dell'ultimo find(profile=True).
|
||||||
|
|
||||||
|
Chiavi: to_gray, spread_top, top_pruning, full_kernel,
|
||||||
|
refine_verify_nms (millisecondi). Util per identificare bottleneck
|
||||||
|
dove ottimizzare.
|
||||||
|
"""
|
||||||
|
return getattr(self, "_last_profile", None)
|
||||||
|
|
||||||
def get_last_diag(self) -> dict | None:
|
def get_last_diag(self) -> dict | None:
|
||||||
"""Ritorna dict diagnostica dell'ultima chiamata find().
|
"""Ritorna dict diagnostica dell'ultima chiamata find().
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -194,7 +194,9 @@ def _draw_matches(scene: np.ndarray, matches: list[Match],
|
|||||||
edge_mask = edge_mask & (warped_mask > 0)
|
edge_mask = edge_mask & (warped_mask > 0)
|
||||||
if edge_mask.any():
|
if edge_mask.any():
|
||||||
edge_overlay = np.zeros_like(out)
|
edge_overlay = np.zeros_like(out)
|
||||||
edge_overlay[edge_mask] = (0, 220, 0) # verde brillante
|
# Ciano (cambiato da verde): non collide col verde dell'asse
|
||||||
|
# Y dell'UCS che altrimenti scompariva nell'overlay edge.
|
||||||
|
edge_overlay[edge_mask] = (255, 200, 0) # ciano (BGR)
|
||||||
out = cv2.addWeighted(out, 1.0, edge_overlay, 0.6, 0)
|
out = cv2.addWeighted(out, 1.0, edge_overlay, 0.6, 0)
|
||||||
L = max(20, int(L_base * m.scale))
|
L = max(20, int(L_base * m.scale))
|
||||||
# X axis = rotazione di (1, 0) con cv2 matrix → (cos, -sin)
|
# X axis = rotazione di (1, 0) con cv2 matrix → (cos, -sin)
|
||||||
|
|||||||
@@ -102,8 +102,8 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Simmetria</label>
|
<label>Simmetria</label>
|
||||||
<select id="p-simmetria">
|
<select id="p-simmetria">
|
||||||
|
<option value="nessuna" selected>Nessuna (0..360°)</option>
|
||||||
<option value="invariante">Invariante (cerchi — no rotazione)</option>
|
<option value="invariante">Invariante (cerchi — no rotazione)</option>
|
||||||
<option value="nessuna">Nessuna (0..360°)</option>
|
|
||||||
<option value="bilaterale">Bilaterale (speculare 180°)</option>
|
<option value="bilaterale">Bilaterale (speculare 180°)</option>
|
||||||
<option value="rot_3">Rotazionale 3× (120°)</option>
|
<option value="rot_3">Rotazionale 3× (120°)</option>
|
||||||
<option value="rot_4">Rotazionale 4× (90°)</option>
|
<option value="rot_4">Rotazionale 4× (90°)</option>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ dependencies = [
|
|||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
pm2d-eval = "pm2d.eval:main"
|
pm2d-eval = "pm2d.eval:main"
|
||||||
|
pm2d-bench = "pm2d.bench:main"
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
|||||||
Reference in New Issue
Block a user