calibrage auto

This commit is contained in:
2026-04-21 23:51:01 +02:00
parent 04da5da162
commit bc72850034
17 changed files with 1138 additions and 942 deletions
+14 -103
View File
@@ -1,9 +1,9 @@
'''
modules/tube_aligner.py
Created on 17 avr. 2026
@author: denis
'''
# modules/tube_aligner.py
import cv2
import logging
@@ -19,13 +19,11 @@ class TubeAligner:
def __init__(
self,
px_per_mm : float = 10.0,
grbl_threshold_px : int = 20,
dead_zone_px : int = 5,
debug : bool = False, # ← activable depuis la vue
display = None,
debug : bool = False, # ← activable depuis la vue
display = None, # display function
):
self.TUBE_DIAMETER_MM = 16.0
self.grbl_threshold_px = grbl_threshold_px
self.dead_zone_px = dead_zone_px
self.debug = debug
@@ -35,7 +33,8 @@ class TubeAligner:
# Détection principale
# ------------------------------------------------------------------ #
def detect_tube(self, frame: np.ndarray) -> dict:
def detect_tube(self, frame: np.ndarray, tube_diameter: float = 16.0) -> dict:
TUBE_DIAMETER_MM = tube_diameter
h, w = frame.shape[:2]
cx_img = w // 2
cy_img = h // 2
@@ -93,16 +92,14 @@ class TubeAligner:
ty = int(np.mean(all_cy))
tr = int(np.mean(all_r))
if tr > 0:
self.px_per_mm = (2 * tr) / 16.0
self.px_per_mm = (2 * tr) / TUBE_DIAMETER_MM
offset_x_px = tx - cx_img
offset_y_px = ty - cy_img
#offset_x_mm = offset_x_px / self.px_per_mm
#offset_y_mm = offset_y_px /self. px_per_mm
offset_x_mm = offset_y_px /self. px_per_mm # (X CNC = Y image)
offset_y_mm = -offset_x_px / self.px_per_mm # (Y CNC = -X image)
offset_y_mm = -offset_x_px / self.px_per_mm # (Y CNC = -X image)
dist_px = float(np.sqrt(offset_x_px**2 + offset_y_px**2))
if dist_px <= self.dead_zone_px:
@@ -162,83 +159,7 @@ class TubeAligner:
# Redimensionne à la taille originale pour ne pas changer le pipeline
return cv2.resize(cropped, (w, h), interpolation=cv2.INTER_LINEAR)
def _detect_center_stable(
self,
capture_func, # callable() → frame bytes
n_samples: int = 5,
delay_s: float = 0.3,
) -> tuple[float, float] | None:
"""
Capture N frames et retourne le centre moyen du tube.
Réduit l'erreur de détection d'un facteur √N.
:param capture_func: callable sans argument → bytes JPEG
:param n_samples: nombre de captures à moyenner
:param delay_s: pause entre chaque capture
:return: (cx_mean, cy_mean) ou None si échec
"""
import time
centers = []
for i in range(n_samples):
if i > 0:
time.sleep(delay_s)
frame_bytes = capture_func()
nparr = np.frombuffer(frame_bytes, np.uint8)
frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if frame is None:
continue
detection = self.detect_tube(frame)
if detection["detected"]:
centers.append((detection["tube_cx"], detection["tube_cy"]))
logger.debug(
"_detect_center_stable [%d/%d] : cx=%d cy=%d",
i+1, n_samples,
detection["tube_cx"], detection["tube_cy"],
)
else:
logger.warning("_detect_center_stable [%d/%d] : tube non détecté", i+1, n_samples)
if len(centers) < 3:
logger.error("_detect_center_stable : seulement %d détections valides", len(centers))
return None
# Filtre les valeurs aberrantes (écart > 2 sigma)
cx_arr = np.array([c[0] for c in centers], dtype=float)
cy_arr = np.array([c[1] for c in centers], dtype=float)
cx_mean, cx_std = np.mean(cx_arr), np.std(cx_arr)
cy_mean, cy_std = np.mean(cy_arr), np.std(cy_arr)
mask = (
(np.abs(cx_arr - cx_mean) <= 2 * cx_std) &
(np.abs(cy_arr - cy_mean) <= 2 * cy_std)
)
filtered = [(cx_arr[i], cy_arr[i]) for i in range(len(centers)) if mask[i]]
if not filtered:
filtered = centers # fallback si tout est filtré
cx_final = float(np.mean([c[0] for c in filtered]))
cy_final = float(np.mean([c[1] for c in filtered]))
logger.info(
"_detect_center_stable : %d/%d valides cx=%.1f±%.1f cy=%.1f±%.1f",
len(filtered), n_samples,
cx_final, cx_std, cy_final, cy_std,
)
return cx_final, cy_final
def calib_reset(self):
pass
# ------------------------------------------------------------------ #
# ------------------------------------------------------------------ #
# Dessin debug
# ------------------------------------------------------------------ #
@@ -259,26 +180,18 @@ class TubeAligner:
# Cercle intérieur du tube
cv2.circle(frame, (tx, ty), tr, color, 2, cv2.LINE_AA)
# Rayon de zone morte (dead zone) en vert clair
cv2.circle(frame, (cx_img, cy_img), self.dead_zone_px,
(0, 255, 100), 1, cv2.LINE_AA)
cv2.circle(frame, (cx_img, cy_img), self.dead_zone_px, (0, 255, 100), 1, cv2.LINE_AA)
# Rayon seuil GRBL en rouge pointillé (simulé par cercle fin)
cv2.circle(frame, (cx_img, cy_img), self.grbl_threshold_px,
(0, 80, 255), 1, cv2.LINE_AA)
cv2.circle(frame, (cx_img, cy_img), self.grbl_threshold_px, (0, 80, 255), 1, cv2.LINE_AA)
# Croix centre image
cv2.drawMarker(frame, (cx_img, cy_img),
(255, 255, 255), cv2.MARKER_CROSS, 24, 1, cv2.LINE_AA)
cv2.drawMarker(frame, (cx_img, cy_img), (255, 255, 255), cv2.MARKER_CROSS, 24, 1, cv2.LINE_AA)
# Centre tube
cv2.circle(frame, (tx, ty), 5, color, -1, cv2.LINE_AA)
# Vecteur offset centre image → centre tube
if dist_px > self.dead_zone_px:
cv2.arrowedLine(frame, (cx_img, cy_img), (tx, ty),
color, 2, cv2.LINE_AA, tipLength=0.2)
cv2.arrowedLine(frame, (cx_img, cy_img), (tx, ty), color, 2, cv2.LINE_AA, tipLength=0.2)
# Panneau info — fond semi-transparent
overlay = frame.copy()
@@ -298,10 +211,8 @@ class TubeAligner:
cv2.FONT_HERSHEY_SIMPLEX, 0.48, col, 1, cv2.LINE_AA)
# Légende zones
cv2.putText(frame, "dead zone", (cx_img + self.dead_zone_px + 3, cy_img - 3),
cv2.FONT_HERSHEY_SIMPLEX, 0.35, (0, 255, 100), 1)
cv2.putText(frame, "GRBL threshold", (cx_img + self.grbl_threshold_px + 3, cy_img + 6),
cv2.FONT_HERSHEY_SIMPLEX, 0.35, (0, 80, 255), 1)
cv2.putText(frame, "dead zone", (cx_img + self.dead_zone_px + 3, cy_img - 3), cv2.FONT_HERSHEY_SIMPLEX, 0.35, (0, 255, 100), 1)
cv2.putText(frame, "GRBL threshold", (cx_img + self.grbl_threshold_px + 3, cy_img + 6), cv2.FONT_HERSHEY_SIMPLEX, 0.35, (0, 80, 255), 1)
return frame
def _draw_debug_no_detection(self, frame, cx_img, cy_img) -> np.ndarray: