diff --git a/etc/install-sys.sh b/etc/install-sys.sh index a43e39d..69efef9 100755 --- a/etc/install-sys.sh +++ b/etc/install-sys.sh @@ -8,7 +8,7 @@ sudo apt update # system echo "==== system essential" -sudo apt -y install build-essential openssl git pkg-config redis supervisor sqlitebrowser samba-client cifs-utils +sudo apt -y install build-essential openssl git pkg-config redis supervisor sqlitebrowser samba-client cifs-utils gettext echo "==== python3 install" sudo apt -y install python3-dev python3-pip python3-venv default-libmysqlclient-dev libmariadb-dev libpq-dev python3-picamera2 diff --git a/test_tube_scanner/home/locale/en/LC_MESSAGES/django.po b/test_tube_scanner/home/locale/en/LC_MESSAGES/django.po index 0254c44..3d9265f 100644 --- a/test_tube_scanner/home/locale/en/LC_MESSAGES/django.po +++ b/test_tube_scanner/home/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-02-24 08:51+0100\n" +"POT-Creation-Date: 2026-04-21 23:48+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,49 +17,9 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" + #: home/apps.py:7 -msgid "Automation" -msgstr "" - -#: home/management/commands/init_data.py:63 -msgid "Base configuration" -msgstr "" - -#: home/management/commands/init_data.py:144 -msgid "Non" -msgstr "" - -#: home/management/commands/init_data.py:145 -#: home/management/commands/init_data.py:148 -msgid "Salon" -msgstr "" - -#: home/management/commands/init_data.py:146 -msgid "Cellier" -msgstr "" - -#: home/management/commands/init_data.py:147 -msgid "Cuisine" -msgstr "" - -#: home/management/commands/init_data.py:149 -msgid "Entrée" -msgstr "" - -#: home/management/commands/init_data.py:150 -msgid "Repas" -msgstr "" - -#: home/management/commands/init_data.py:151 -msgid "Chambre 1" -msgstr "" - -#: home/management/commands/init_data.py:152 -msgid "Chambre 2" -msgstr "" - -#: home/management/commands/init_data.py:153 -msgid "Chambre 3" +msgid "Scanner de tube à essais" msgstr "" #: home/templates/inc/404.html:9 @@ -80,6 +40,736 @@ msgid "" "administrator." msgstr "" +#: scanner/models.py:17 +msgid "Haut gauche" +msgstr "" + +#: scanner/models.py:18 +msgid "Haut droit" +msgstr "" + +#: scanner/models.py:19 +msgid "Bas gauche" +msgstr "" + +#: scanner/models.py:20 +msgid "Bas droit" +msgstr "" + +#: scanner/models.py:21 +msgid "Bas milieu" +msgstr "" + +#: scanner/models.py:22 +msgid "Haut milieu" +msgstr "" + +#: scanner/models.py:26 scanner/models.py:31 +msgid "MP4" +msgstr "" + +#: scanner/models.py:27 +msgid "XVID" +msgstr "" + +#: scanner/models.py:32 +msgid "AVI" +msgstr "" + +#: scanner/models.py:36 +msgid "Nom de la Configuration" +msgstr "" + +#: scanner/models.py:36 +msgid "Nom de la configuration" +msgstr "" + +#: scanner/models.py:36 +msgid "Configuration par défaut" +msgstr "" + +#: scanner/models.py:39 +msgid "Barre latérale" +msgstr "" + +#: scanner/models.py:39 +msgid "Largeur barre latérale (css)" +msgstr "" + +#: scanner/models.py:40 +msgid "Colonnes de la grille par défaut" +msgstr "" + +#: scanner/models.py:40 +msgid "Nombre de colonnes de la grille par défaut" +msgstr "" + +#: scanner/models.py:42 +msgid "Fourcc" +msgstr "" + +#: scanner/models.py:42 +msgid "Opencv fourcc format" +msgstr "" + +#: scanner/models.py:43 +msgid "Video type" +msgstr "" + +#: scanner/models.py:43 +msgid "Opencv video type" +msgstr "" + +#: scanner/models.py:45 +msgid "Grbl Xmax" +msgstr "" + +#: scanner/models.py:45 +msgid "CNC Grbl Xmax en mm" +msgstr "" + +#: scanner/models.py:46 +msgid "Grbl Ymax" +msgstr "" + +#: scanner/models.py:46 +msgid "CNC Grbl Ymax en mm" +msgstr "" + +#: scanner/models.py:47 +msgid "Pixel / mm" +msgstr "" + +#: scanner/models.py:47 +msgid "Rapport pixel / déplacement en pixel/mm" +msgstr "" + +#: scanner/models.py:50 +msgid "Utiliser rpicam" +msgstr "" + +#: scanner/models.py:50 +msgid "Par défaaut. Sinon USB webcam" +msgstr "" + +#: scanner/models.py:51 +msgid "Index de la webcam" +msgstr "" + +#: scanner/models.py:51 +msgid "Index de la webcam (0, 1, ...) si présente" +msgstr "" + +#: scanner/models.py:52 +msgid "Qualité JPEG" +msgstr "" + +#: scanner/models.py:52 +msgid "Qualité JPEG (1-100) pour les images exportées" +msgstr "" + +#: scanner/models.py:53 +msgid "Qualité JPEG pour les vidéos" +msgstr "" + +#: scanner/models.py:53 +msgid "Qualité JPEG (1-100) pour les images extraites des vidéos" +msgstr "" + +#: scanner/models.py:54 +msgid "Fréquence vidéos (fps)" +msgstr "" + +#: scanner/models.py:54 +msgid "Fréquence d'extraction des images des vidéos (images par seconde)" +msgstr "" + +#: scanner/models.py:55 +msgid "Largeur de capture vidéo" +msgstr "" + +#: scanner/models.py:55 +msgid "Largeur de capture vidéo en pixels" +msgstr "" + +#: scanner/models.py:56 +msgid "Hauteur de capture vidéo" +msgstr "" + +#: scanner/models.py:56 +msgid "Hauteur de capture vidéo en pixels" +msgstr "" + +#: scanner/models.py:58 +msgid "Rayon de découpe pour la calibration" +msgstr "" + +#: scanner/models.py:58 +msgid "Rayon en pixels pour découper les images de calibration en px" +msgstr "" + +#: scanner/models.py:59 +msgid "Multi-puits de calibration par défaut" +msgstr "" + +#: scanner/models.py:59 +msgid "Position du multi-puits de calibration par défaut" +msgstr "" + +#: scanner/models.py:60 +msgid "Vitesse de calibration" +msgstr "" + +#: scanner/models.py:60 +msgid "Vitesse de déplacement pour la calibration en mm/mn" +msgstr "" + +#: scanner/models.py:61 +msgid "Pas de calibration" +msgstr "" + +#: scanner/models.py:61 +msgid "Pas de déplacement pour la calibration en mm" +msgstr "" + +#: scanner/models.py:62 +msgid "Duruée calibration" +msgstr "" + +#: scanner/models.py:62 +msgid "Durée de pose entre chaque puits en s" +msgstr "" + +#: scanner/models.py:63 +msgid "Actif" +msgstr "" + +#: scanner/models.py:67 +msgid "Configuration" +msgstr "" + +#: scanner/models.py:75 +msgid "Nom" +msgstr "" + +#: scanner/models.py:75 +msgid "Nom du puit: Ai..Di" +msgstr "" + +#: scanner/models.py:79 scanner/models.py:155 +msgid "Puit" +msgstr "" + +#: scanner/models.py:80 +msgid "Puits" +msgstr "" + +#: scanner/models.py:87 +msgid "Label" +msgstr "" + +#: scanner/models.py:87 +msgid "Label du multi-puit" +msgstr "" + +#: scanner/models.py:89 +msgid "Position" +msgstr "" + +#: scanner/models.py:89 +msgid "Position du multi-puits sur la table" +msgstr "" + +#: scanner/models.py:90 +msgid "Par défaut" +msgstr "" + +#: scanner/models.py:90 +msgid "Multi-puit par défaut" +msgstr "" + +#: scanner/models.py:92 +msgid "Colonnes" +msgstr "" + +#: scanner/models.py:92 +msgid "Nombre de colonnes" +msgstr "" + +#: scanner/models.py:93 +msgid "Lignes" +msgstr "" + +#: scanner/models.py:93 +msgid "Nombre de lignes" +msgstr "" + +#: scanner/models.py:94 +msgid "Diamètre" +msgstr "" + +#: scanner/models.py:94 +msgid "Diamètre des tubes en mm" +msgstr "" + +#: scanner/models.py:96 +msgid "Définition" +msgstr "" + +#: scanner/models.py:96 +msgid "Définition des lignes" +msgstr "" + +#: scanner/models.py:97 +msgid "Ordre ligne" +msgstr "" + +#: scanner/models.py:97 +msgid "Ordre ligne de puit. Lecture en serpentin dans le sens des +- X" +msgstr "" + +#: scanner/models.py:99 scanner/models.py:158 +msgid "Ordre" +msgstr "" + +#: scanner/models.py:99 +msgid "Ordre de lecture du multi-puit" +msgstr "" + +#: scanner/models.py:100 scanner/templates/scanner/calibration.html:54 +msgid "Durée" +msgstr "" + +#: scanner/models.py:100 +msgid "Durée du film en secondes" +msgstr "" + +#: scanner/models.py:101 +msgid "Origine X" +msgstr "" + +#: scanner/models.py:101 +msgid "Base origine X en mm" +msgstr "" + +#: scanner/models.py:102 +msgid "Origine Y" +msgstr "" + +#: scanner/models.py:102 +msgid "Base origine Y en mm" +msgstr "" + +#: scanner/models.py:104 +msgid "Pas X" +msgstr "" + +#: scanner/models.py:104 +msgid "Pas ou interval sur X en mm" +msgstr "" + +#: scanner/models.py:105 +msgid "Pas Y" +msgstr "" + +#: scanner/models.py:105 +msgid "Pas ou interval sur Y en mm" +msgstr "" + +#: scanner/models.py:106 scanner/templates/scanner/calibration.html:23 +msgid "Vitesse" +msgstr "" + +#: scanner/models.py:106 +msgid "Vitesse déplacement en mm/mn " +msgstr "" + +#: scanner/models.py:108 +msgid "Positions" +msgstr "" + +#: scanner/models.py:108 +msgid "Positions des puits générées ?" +msgstr "" + +#: scanner/models.py:109 scanner/models.py:246 scanner/models.py:276 +msgid "Active" +msgstr "" + +#: scanner/models.py:145 scanner/models.py:146 scanner/models.py:156 +#: scanner/models.py:206 +msgid "Multi-puits" +msgstr "" + +#: scanner/models.py:158 +msgid "Ordre de lecture du puit" +msgstr "" + +#: scanner/models.py:159 +msgid "X" +msgstr "" + +#: scanner/models.py:159 +msgid "Axe X en mm" +msgstr "" + +#: scanner/models.py:160 +msgid "Y" +msgstr "" + +#: scanner/models.py:160 +msgid "Axe Y en mm" +msgstr "" + +#: scanner/models.py:166 +msgid "Position d'un puit" +msgstr "" + +#: scanner/models.py:167 +msgid "Position des puits" +msgstr "" + +#: scanner/models.py:203 +msgid "Titre de l'observation" +msgstr "" + +#: scanner/models.py:204 +msgid "Commentaires" +msgstr "" + +#: scanner/models.py:204 +msgid "Descriptions de l'observations" +msgstr "" + +#: scanner/models.py:207 scanner/models.py:250 +msgid "Date de création" +msgstr "" + +#: scanner/models.py:208 +msgid "Date de début" +msgstr "" + +#: scanner/models.py:209 scanner/models.py:251 +msgid "Date de fin" +msgstr "" + +#: scanner/models.py:213 scanner/models.py:341 +msgid "Observation" +msgstr "" + +#: scanner/models.py:214 +msgid "Observations" +msgstr "" + +#: scanner/models.py:229 +msgid "Observation multi-puits détail" +msgstr "" + +#: scanner/models.py:230 +msgid "Observations multi-puits détails" +msgstr "" + +#: scanner/models.py:239 +msgid "En attente" +msgstr "" + +#: scanner/models.py:240 +msgid "En cours" +msgstr "" + +#: scanner/models.py:241 +msgid "Terminé" +msgstr "" + +#: scanner/models.py:242 +msgid "Erreur" +msgstr "" + +#: scanner/models.py:244 +msgid "Nom de la session" +msgstr "" + +#: scanner/models.py:244 +msgid "Session d'observations. 4 Multi-puits maximum" +msgstr "" + +#: scanner/models.py:247 +msgid "Date d'exportation" +msgstr "" + +#: scanner/models.py:247 +msgid "Date d'exportation prévue" +msgstr "" + +#: scanner/models.py:248 +msgid "Date du balayage" +msgstr "" + +#: scanner/models.py:248 +msgid "Date du balayage prévue" +msgstr "" + +#: scanner/models.py:253 +msgid "Status exportation" +msgstr "" + +#: scanner/models.py:256 +msgid "Export médias" +msgstr "" + +#: scanner/models.py:257 +msgid "Programmation de l'exportation des vidéos et images" +msgstr "" + +#: scanner/models.py:259 +msgid "Exportation terminée à" +msgstr "" + +#: scanner/models.py:261 +msgid "Status scanning" +msgstr "" + +#: scanner/models.py:264 scanner/templates/scanner/main.html:35 +msgid "Lancer le balayage" +msgstr "" + +#: scanner/models.py:265 +msgid "Programmation du lancement du balayage" +msgstr "" + +#: scanner/models.py:267 +msgid "Balayage terminé à" +msgstr "" + +#: scanner/models.py:272 +msgid "Session d'observation" +msgstr "" + +#: scanner/models.py:273 +msgid "Sessions d'observation" +msgstr "" + +#: scanner/models.py:276 +msgid "Terminée" +msgstr "" + +#: scanner/models.py:340 +msgid "Session" +msgstr "" + +#: scanner/models.py:362 +msgid "Session observations" +msgstr "" + +#: scanner/models.py:363 +msgid "Sessions observations" +msgstr "" + +#: scanner/process.py:331 +msgid "La session est nulle!..." +msgstr "" + +#: scanner/templates/scanner/base.html:5 +msgid "Barre de côté" +msgstr "" + +#: scanner/templates/scanner/base.html:11 +msgid "Ecran plein" +msgstr "" + +#: scanner/templates/scanner/base.html:25 +msgid "Déconnexion" +msgstr "" + +#: scanner/templates/scanner/base.html:32 +msgid "Mémoire partagée" +msgstr "" + +#: scanner/templates/scanner/base.html:33 +msgid "Disque RAM" +msgstr "" + +#: scanner/templates/scanner/base.html:34 +msgid "Mémoire RAM" +msgstr "" + +#: scanner/templates/scanner/base.html:35 +msgid "Charge CPU" +msgstr "" + +#: scanner/templates/scanner/base.html:36 +msgid "Disque /" +msgstr "" + +#: scanner/templates/scanner/base.html:42 +msgid "Grille" +msgstr "" + +#: scanner/templates/scanner/base.html:70 +msgid "Administration" +msgstr "" + +#: scanner/templates/scanner/base.html:75 +msgid "Administration base de données" +msgstr "" + +#: scanner/templates/scanner/base.html:78 +msgid "Base de données Reductstore" +msgstr "" + +#: scanner/templates/scanner/base.html:81 +msgid "Base de données mariadb" +msgstr "" + +#: scanner/templates/scanner/base.html:84 +msgid "Portainer" +msgstr "" + +#: scanner/templates/scanner/base.html:87 +msgid "Supervisor" +msgstr "" + +#: scanner/templates/scanner/base.html:90 +msgid "Logs des planifications" +msgstr "" + +#: scanner/templates/scanner/base.html:93 +msgid "Logs des services" +msgstr "" + +#: scanner/templates/scanner/base.html:96 scanner/views.py:92 +msgid "Calibration" +msgstr "" + +#: scanner/templates/scanner/base.html:105 scanner/views.py:82 +msgid "Balayage multi-puits" +msgstr "" + +#: scanner/templates/scanner/base.html:108 scanner/views.py:132 +msgid "Gestionnaire d'images" +msgstr "" + +#: scanner/templates/scanner/base.html:111 +msgid "Redifusion vidéos" +msgstr "" + +#: scanner/templates/scanner/calibration.html:15 +msgid "Position multi-puit" +msgstr "" + +#: scanner/templates/scanner/calibration.html:36 +msgid "Pas" +msgstr "" + +#: scanner/templates/scanner/calibration.html:55 +msgid "Durée entre vidéos" +msgstr "" + +#: scanner/templates/scanner/calibration.html:83 +msgid "Définir la base (x, y) du multi-puit" +msgstr "" + +#: scanner/templates/scanner/calibration.html:84 +msgid "Définir base" +msgstr "" + +#: scanner/templates/scanner/calibration.html:99 +msgid "Sauvegarder la position du puit courant" +msgstr "" + +#: scanner/templates/scanner/calibration.html:120 +msgid "Précéd." +msgstr "" + +#: scanner/templates/scanner/calibration.html:124 +msgid "Centrer manuellement" +msgstr "" + +#: scanner/templates/scanner/calibration.html:125 +msgid "Centrer" +msgstr "" + +#: scanner/templates/scanner/calibration.html:130 +msgid "Suivant" +msgstr "" + +#: scanner/templates/scanner/calibration.html:135 +msgid "Centrage auto et sauvegarde des positions" +msgstr "" + +#: scanner/templates/scanner/calibration.html:136 +msgid "Calibrage auto" +msgstr "" + +#: scanner/templates/scanner/calibration.html:146 +msgid "Aller à la base" +msgstr "" + +#: scanner/templates/scanner/calibration.html:151 +msgid "Debug" +msgstr "" + +#: scanner/templates/scanner/calibration.html:156 +#: scanner/templates/scanner/main.html:31 +msgid "Axes" +msgstr "" + +#: scanner/templates/scanner/calibration.html:161 +#: scanner/templates/scanner/main.html:32 +msgid "Recadrer" +msgstr "" + +#: scanner/templates/scanner/calibration.html:165 +msgid "Rayon" +msgstr "" + +#: scanner/templates/scanner/calibration.html:166 +msgid "Rayon du recadrement" +msgstr "" + +#: scanner/templates/scanner/calibration.html:167 +msgid "Rayon de cadrage" +msgstr "" + +#: scanner/templates/scanner/calibration.html:171 +msgid "Tester le cirduit" +msgstr "" + +#: scanner/templates/scanner/calibration.html:174 +#: scanner/templates/scanner/main.html:37 +msgid "ARRET" +msgstr "" + +#: scanner/templates/scanner/images.html:9 +#: scanner/templates/scanner/replay.html:10 +msgid "Retour accueil" +msgstr "" + +#: scanner/templates/scanner/images.html:13 +#: scanner/templates/scanner/replay.html:14 +msgid "Session d'observations" +msgstr "" + +#: scanner/templates/scanner/images.html:26 +msgid "Exporter l'ensemble des images" +msgstr "" + +#: scanner/templates/scanner/images.html:43 +msgid "Télécharger" +msgstr "" + +#: scanner/templates/scanner/main.html:15 +msgid "Choix de la session" +msgstr "" + +#: scanner/templates/scanner/replay.html:27 +msgid "Exporter l'ensemble des vidéos" +msgstr "" + +#: scanner/views.py:174 +msgid "Gestionnaire de vidéos" +msgstr "" + #: templates/admin/base_site.html:4 msgid "Django site admin" msgstr "" diff --git a/test_tube_scanner/modules/capture_interface.py b/test_tube_scanner/modules/capture_interface.py index 2cd0ef5..1c71915 100644 --- a/test_tube_scanner/modules/capture_interface.py +++ b/test_tube_scanner/modules/capture_interface.py @@ -45,11 +45,11 @@ class VideoCaptureInterface(abc.ABC): Les sous-classes doivent implémenter les méthodes abstraites pour gérer le matériel spécifique. """ - + # Cadence par défaut en images par seconde DEFAULT_FPS: float = 5.0 - def __init__(self, fps: float = DEFAULT_FPS, use_tracking: bool = False, px_per_mm: float = 2.15, display=None): + def __init__(self, fps: float = DEFAULT_FPS, use_tracking: bool = False, display=None, parent=None): """ Initialise l'interface de capture. @@ -58,6 +58,7 @@ class VideoCaptureInterface(abc.ABC): self._fps: float = fps self.use_tracking = use_tracking self.display = display + self.parent = parent self._interval: float = 1.0 / fps # Intervalle en secondes entre chaque capture self._running: bool = False # Indique si la capture est en cours self._thread: Optional[threading.Thread] = None @@ -73,16 +74,16 @@ class VideoCaptureInterface(abc.ABC): min_area_px = settings.TRACKER_MIN_AREA, ) self._aligner = TubeAligner( - px_per_mm = px_per_mm, # à calibrer selon la caméra grbl_threshold_px = 20, # au-delà → correction GRBL dead_zone_px = 5, # en-dessous → rien à faire display = display, ) - self._last_detection = None # résultat du dernier alignement + self._last_detection = None # résultat du dernier alignement # calibrage ou lecture réelle # - def align_on_well_arrival(self, frame: bytes, cnc_controller) -> dict: + + def align_on_well_arrival(self, frame: bytes, cnc_controller, tube_diameter: float = 16.0) -> dict: """ Appelé UNE FOIS à l'arrivée sur un nouveau puits. Détecte le tube, décide l'action, exécute la correction. @@ -93,7 +94,7 @@ class VideoCaptureInterface(abc.ABC): """ nparr = np.frombuffer(frame, np.uint8) img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) - detection = self._aligner.detect_tube(img) + detection = self._aligner.detect_tube(img, tube_diameter=tube_diameter) # Stockage pour process_frame self._last_detection = detection @@ -108,7 +109,7 @@ class VideoCaptureInterface(abc.ABC): dy_mm = detection["offset_y_mm"] msg = f"align_on_well_arrival: correction CNC move_relative(dx={dx_mm:.3f}, dy={dy_mm:.3f})" - #cnc_controller.move_relative(dx=-dx_mm, dy=-dy_mm, feed=150) + cnc_controller.move_relative(dx=dx_mm, dy=dy_mm, feed=150) self._tracker.reset() self._last_detection["action"] = "none" @@ -118,7 +119,7 @@ class VideoCaptureInterface(abc.ABC): logger.info(msg) self.display(state='detect_tube', msg=msg) - return detection + return detection def on_well_change(self): @@ -276,18 +277,14 @@ class VideoCaptureInterface(abc.ABC): # Mode debug if self._aligner.debug: - self._last_detection = detection = self._aligner.detect_tube(frame) - annotated = detection.get('frame_annotated') + self._last_detection = self._aligner.detect_tube(frame, self.parent.data.tube_diameter or 16.0) + annotated = self._last_detection.get('frame_annotated') frame = annotated if annotated is not None else frame - ''' - else: - detection = self._last_detection or {} - - # --- Crop logiciel si nécessaire --- - if (detection.get("action") == "crop" and detection.get("detected") and not self._aligner.debug ): - frame = self._aligner.crop_to_tube(frame, detection) - ''' - + + #if (self._last_detection.get("action") == "crop" and self._last_detection.get("detected")): + # frame = self._aligner.crop_to_tube(frame, self._last_detection) + + # mode racking if self.use_tracking: ts = datetime.now(timezone.utc).timestamp() frame, metrics = self._tracker.process(frame, ts) diff --git a/test_tube_scanner/modules/grbl.py b/test_tube_scanner/modules/grbl.py index dc8465f..a4513e1 100644 --- a/test_tube_scanner/modules/grbl.py +++ b/test_tube_scanner/modules/grbl.py @@ -12,7 +12,7 @@ import logging import serial import time import threading -from typing import Optional + logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -194,7 +194,7 @@ class GRBLController: x, y = self.get_mpos() # Position actuelle self.move_to(x + dx, y + dy, feed=feed) - def move_relative_(self, dx=0, dy=0, feed=1000): + def move_relative__(self, dx=0, dy=0, feed=1000): self.send("G91") # Mode relatif cmd = f"G0 X{dx} Y{dy} F{feed}" self.send(cmd) diff --git a/test_tube_scanner/modules/picamera2_capture.py b/test_tube_scanner/modules/picamera2_capture.py index de5d09b..b4a3847 100644 --- a/test_tube_scanner/modules/picamera2_capture.py +++ b/test_tube_scanner/modules/picamera2_capture.py @@ -46,8 +46,8 @@ class PiCamera2Capture(VideoCaptureInterface): camera_index: int = 0, use_video_config: bool = True, use_tracking: bool = False, - px_per_mm: float = 2.1, display = None, + parent = None ): """ @@ -59,7 +59,7 @@ class PiCamera2Capture(VideoCaptureInterface): :param use_video_config: True = VideoConfiguration (flux continu, basse latence) False = StillConfiguration (haute résolution, plus lent) """ - super().__init__(fps=fps, use_tracking=use_tracking, px_per_mm=px_per_mm, display=display) + super().__init__(fps=fps, use_tracking=use_tracking, display=display, parent=parent) self._width: int = width self._height: int = height self._jpeg_quality: int = jpeg_quality diff --git a/test_tube_scanner/modules/picamera2_capture_basic.py b/test_tube_scanner/modules/picamera2_capture_basic.py index 3c3a496..76ffe65 100644 --- a/test_tube_scanner/modules/picamera2_capture_basic.py +++ b/test_tube_scanner/modules/picamera2_capture_basic.py @@ -37,6 +37,9 @@ class PiCamera2Capture(VideoCaptureInterface): jpeg_quality: int = 85, camera_index: int = 0, use_video_config: bool = True, + use_tracking: bool = False, + display = None, + parent=None, ): """ :param fps: Cadence cible en images par seconde @@ -47,7 +50,7 @@ class PiCamera2Capture(VideoCaptureInterface): :param use_video_config: True = configuration VideoConfiguration (flux continu) False = StillConfiguration (haute résolution, plus lent) """ - super().__init__(fps=fps) + super().__init__(fps=fps, use_tracking=use_tracking, display=display, parent=parent) self._width: int = width self._height: int = height self._jpeg_quality: int = jpeg_quality diff --git a/test_tube_scanner/modules/tube_aligner.py b/test_tube_scanner/modules/tube_aligner.py index 3e176c2..eb00f51 100644 --- a/test_tube_scanner/modules/tube_aligner.py +++ b/test_tube_scanner/modules/tube_aligner.py @@ -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: diff --git a/test_tube_scanner/modules/tube_aligner_old.py b/test_tube_scanner/modules/tube_aligner_old.py deleted file mode 100644 index 2d38fec..0000000 --- a/test_tube_scanner/modules/tube_aligner_old.py +++ /dev/null @@ -1,230 +0,0 @@ -''' -Created on 17 avr. 2026 - -@author: denis -''' -# modules/tube_aligner.py - -import cv2 -import logging -import numpy as np - -logger = logging.getLogger(__name__) - - -class TubeAligner: - """ - Détecte le cercle du tube à essai dans une frame (vue par dessous, - éclairage par dessus → cercle clair sur fond sombre). - Calcule le décalage entre le centre du tube et le centre de l'image. - Décide d'une correction GRBL (grand écart) ou d'un recadrage (petit écart). - """ - - # Seuil en pixels : au-delà → correction GRBL, en-dessous → recadrage - GRBL_THRESHOLD_PX = 20 - # Tolérance : en-dessous → pas de correction nécessaire - DEAD_ZONE_PX = 5 - - def __init__( - self, - px_per_mm: float = 10.0, # facteur d'échelle calibration (px/mm) - grbl_threshold_px: int = 20, - dead_zone_px: int = 5, - debug: bool = False, # ← activable depuis la vue - ): - self.px_per_mm = px_per_mm - self.grbl_threshold_px = grbl_threshold_px - self.dead_zone_px = dead_zone_px - - def detect_tube(self, frame: np.ndarray) -> dict: - """ - Détecte le cercle du tube et calcule le décalage par rapport au centre image. - - :param frame: Frame BGR (numpy array) - :return: dict avec cercle détecté, décalage px et mm, action recommandée - """ - h, w = frame.shape[:2] - cx_img = w // 2 - cy_img = h // 2 - - result = { - "detected" : False, - "tube_cx" : None, - "tube_cy" : None, - "tube_radius" : None, - "offset_x_px" : None, - "offset_y_px" : None, - "offset_x_mm" : None, - "offset_y_mm" : None, - "action" : "none", # "none" | "crop" | "grbl" - "grbl_gcode" : None, - "frame_annotated": None, - } - - # Prétraitement : niveaux de gris + flou - gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - #blurred = cv2.GaussianBlur(gray, (9, 9), 2) - blurred = cv2.GaussianBlur(gray, (15, 15), 3) - - # param1 : seuil Canny haut, param2 : seuil accumulation (plus bas = plus permissif) - min_radius = int(min(w, h) * 0.26) # ~260px sur 1000px - max_radius = int(min(w, h) * 0.36) # ~360px sur 1000px — bord intérieur du verre - - circles = cv2.HoughCircles( - blurred, - cv2.HOUGH_GRADIENT, - dp = 1.2, - minDist = min(w, h) // 2, # un seul tube attendu - param1 = 50, - param2 = 30, - minRadius = min_radius, - maxRadius = max_radius, - ) - - if circles is None: - logger.warning("TubeAligner: aucun cercle détecté") - result["frame_annotated"] = self._annotate(frame.copy(), cx_img, cy_img, None) - return result - - circles = np.round(circles[0, :]).astype(int) - - # Prend le cercle le plus proche du centre image - best = min( - circles, - key=lambda c: np.sqrt((c[0] - cx_img)**2 + (c[1] - cy_img)**2) - ) - tx, ty, tr = int(best[0]), int(best[1]), int(best[2]) - - # Décalage : positif = tube à droite/bas du centre image - 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 - - dist_px = np.sqrt(offset_x_px**2 + offset_y_px**2) - - # Décision d'action - if dist_px <= self.dead_zone_px: - action = "none" - grbl_gcode = None - elif dist_px <= self.grbl_threshold_px: - action = "crop" - grbl_gcode = None - else: - action = "grbl" - # G91 = coordonnées relatives, G0 = déplacement rapide - # Inversion du signe : si tube est à droite (+X image), - # la CNC doit reculer (-X GRBL) pour recentrer - #cmd = f"G53 G1 X{x:.2f} Y{y:.2f} F{feed}" - - grbl_gcode = ( - f"G91\n" - f"G1 X{-offset_x_mm:.3f} Y{-offset_y_mm:.3f}\n" - f"G90" - ) - logger.info( - "TubeAligner: décalage %.1fpx (%.2fmm, %.2fmm) → GRBL: %s", - dist_px, offset_x_mm, offset_y_mm, grbl_gcode.replace('\n', ' | ') - ) - - result.update({ - "detected" : True, - "tube_cx" : tx, - "tube_cy" : ty, - "tube_radius" : tr, - "offset_x_px" : offset_x_px, - "offset_y_px" : offset_y_px, - "offset_x_mm" : round(offset_x_mm, 3), - "offset_y_mm" : round(offset_y_mm, 3), - "action" : action, - "grbl_gcode" : None, - "frame_annotated": self._annotate( - frame.copy(), cx_img, cy_img, (tx, ty, tr), offset_x_px, offset_y_px - ), - }) - - return result - - def crop_to_tube(self, frame: np.ndarray, detection: dict) -> np.ndarray: - """ - Recadrage logiciel : recentre l'image sur le tube détecté. - Utilisé quand action == "crop". - """ - if not detection["detected"]: - return frame - - tx = detection["tube_cx"] - ty = detection["tube_cy"] - tr = detection["tube_radius"] - h, w = frame.shape[:2] - - # Fenêtre carrée autour du centre du tube - half = tr - x1 = max(tx - half, 0) - y1 = max(ty - half, 0) - x2 = min(tx + half, w) - y2 = min(ty + half, h) - - cropped = frame[y1:y2, x1:x2] - - # Redimensionne à la taille originale pour ne pas changer le pipeline - return cv2.resize(cropped, (w, h), interpolation=cv2.INTER_LINEAR) - - def _annotate( - self, - frame: np.ndarray, - cx_img: int, - cy_img: int, - circle: tuple | None, - offset_x: int = 0, - offset_y: int = 0, - ) -> np.ndarray: - """ - Dessine le cercle détecté, le centre image et le vecteur de décalage. - """ - # Croix centre image - cv2.drawMarker( - frame, (cx_img, cy_img), - (0, 255, 0), cv2.MARKER_CROSS, 20, 1, cv2.LINE_AA - ) - - if circle is None: - cv2.putText(frame, "Tube non detecte", (10, 30), - cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 1, cv2.LINE_AA) - return frame - - tx, ty, tr = circle - - # Cercle du tube en cyan - cv2.circle(frame, (tx, ty), tr, (255, 255, 0), 2, cv2.LINE_AA) - - # Centre du tube en rouge - cv2.circle(frame, (tx, ty), 4, (0, 0, 255), -1, cv2.LINE_AA) - - # Vecteur décalage (centre image → centre tube) - if abs(offset_x) > 2 or abs(offset_y) > 2: - cv2.arrowedLine( - frame, - (cx_img, cy_img), - (tx, ty), - (0, 100, 255), 2, cv2.LINE_AA, tipLength=0.2 - ) - - # Texte décalage - dist = np.sqrt(offset_x**2 + offset_y**2) - cv2.putText( - frame, - f"dx={offset_x:+d}px dy={offset_y:+d}px dist={dist:.1f}px", - (10, 28), - cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1, cv2.LINE_AA, - ) - cv2.putText( - frame, - f"r={tr}px", - (10, 48), - cv2.FONT_HERSHEY_SIMPLEX, 0.45, (200, 200, 200), 1, cv2.LINE_AA, - ) - - return frame - - \ No newline at end of file diff --git a/test_tube_scanner/modules/tube_aligner_old2.py b/test_tube_scanner/modules/tube_aligner_old2.py deleted file mode 100644 index a775b21..0000000 --- a/test_tube_scanner/modules/tube_aligner_old2.py +++ /dev/null @@ -1,283 +0,0 @@ -''' -Created on 17 avr. 2026 - -@author: denis -''' -# modules/tube_aligner.py - -import cv2 -import logging -import numpy as np - -logger = logging.getLogger(__name__) - - -class TubeAligner: - - GRBL_THRESHOLD_PX = 20 - DEAD_ZONE_PX = 5 - - 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 - ): - self.px_per_mm = px_per_mm - self.grbl_threshold_px = grbl_threshold_px - self.dead_zone_px = dead_zone_px - self.debug = debug - - # Etat calibration - self._calib_step = 0 # 0=idle 1=point A enregistré - self._calib_pos_A_px = None # centre tube point A en px - self._calib_mpos_A = None # position CNC point A en mm - - # ------------------------------------------------------------------ # - # Détection principale - # ------------------------------------------------------------------ # - - def detect_tube(self, frame: np.ndarray) -> dict: - h, w = frame.shape[:2] - cx_img = w // 2 - cy_img = h // 2 - - result = { - "detected" : False, - "tube_cx" : None, - "tube_cy" : None, - "tube_radius" : None, - "offset_x_px" : 0, - "offset_y_px" : 0, - "offset_x_mm" : 0.0, - "offset_y_mm" : 0.0, - "action" : "none", - "frame_annotated": None, - } - - gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - blurred = cv2.GaussianBlur(gray, (15, 15), 3) - - circles = cv2.HoughCircles( - blurred, - cv2.HOUGH_GRADIENT, - dp = 1.2, - minDist = min(w, h) // 2, - param1 = 50, - param2 = 30, - minRadius = int(min(w, h) * 0.26), - maxRadius = int(min(w, h) * 0.36), - ) - - frame_out = frame.copy() - - if circles is None: - logger.warning("TubeAligner: aucun cercle détecté") - if self.debug: - frame_out = self._draw_debug_no_detection(frame_out, cx_img, cy_img) - result["frame_annotated"] = frame_out - return result - - circles = np.round(circles[0, :]).astype(int) - best = min(circles, key=lambda c: np.sqrt((c[0]-cx_img)**2 + (c[1]-cy_img)**2)) - tx, ty, tr = int(best[0]), int(best[1]), int(best[2]) - - 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 - dist_px = np.sqrt(offset_x_px**2 + offset_y_px**2) - - if dist_px <= self.dead_zone_px: - action = "none" - elif dist_px <= self.grbl_threshold_px: - action = "crop" - else: - action = "grbl" - - if self.debug: - frame_out = self._draw_debug( - frame_out, cx_img, cy_img, - tx, ty, tr, - offset_x_px, offset_y_px, - offset_x_mm, offset_y_mm, - dist_px, action, - ) - - - result.update({ - "detected" : True, - "tube_cx" : tx, - "tube_cy" : ty, - "tube_radius" : tr, - "offset_x_px" : offset_x_px, - "offset_y_px" : offset_y_px, - "offset_x_mm" : round(offset_x_mm, 3), - "offset_y_mm" : round(offset_y_mm, 3), - "action" : action, - "frame_annotated": frame_out, - }) - return result - - # ------------------------------------------------------------------ # - # Calibration px/mm — 2 points - # ------------------------------------------------------------------ # - - def calib_record_point_A(self, detection: dict, mpos: tuple) -> bool: - """ - Enregistre le point A (position CNC + centre tube en px). - Appeler quand la CNC est immobile sur le point A. - - :param detection: résultat de detect_tube() - :param mpos: (x_mm, y_mm) retourné par cnc.get_mpos() - :return: True si enregistré - """ - if not detection["detected"]: - logger.warning("calib_record_point_A: tube non détecté") - return False - - self._calib_pos_A_px = (detection["tube_cx"], detection["tube_cy"]) - self._calib_mpos_A = mpos - self._calib_step = 1 - logger.info("Calibration point A : px=%s mpos=%s", self._calib_pos_A_px, mpos) - return True - - def calib_record_point_B(self, detection: dict, mpos: tuple) -> dict | None: - """ - Enregistre le point B et calcule px_per_mm. - Appeler après déplacement CNC manuel d'une distance connue. - - :param detection: résultat de detect_tube() - :param mpos: (x_mm, y_mm) retourné par cnc.get_mpos() - :return: dict résultat calibration ou None si échec - """ - if self._calib_step != 1: - logger.warning("calib_record_point_B: point A non enregistré") - return None - - if not detection["detected"]: - logger.warning("calib_record_point_B: tube non détecté") - return None - - pos_B_px = (detection["tube_cx"], detection["tube_cy"]) - mpos_B = mpos - - # Déplacement en px - dpx = np.sqrt( - (pos_B_px[0] - self._calib_pos_A_px[0])**2 + - (pos_B_px[1] - self._calib_pos_A_px[1])**2 - ) - # Déplacement en mm (distance euclidienne CNC) - dmm = np.sqrt( - (mpos_B[0] - self._calib_mpos_A[0])**2 + - (mpos_B[1] - self._calib_mpos_A[1])**2 - ) - - if dmm < 0.1 or dpx < 2: - logger.warning("Déplacement trop faible : dpx=%.1f dmm=%.3f", dpx, dmm) - return None - - px_per_mm_new = dpx / dmm - self.px_per_mm = px_per_mm_new - self._calib_step = 0 - - result = { - "px_per_mm" : round(px_per_mm_new, 4), - "mm_per_px" : round(dmm / dpx, 6), - "delta_px" : round(dpx, 2), - "delta_mm" : round(dmm, 3), - "point_A_px" : self._calib_pos_A_px, - "point_B_px" : pos_B_px, - "mpos_A" : self._calib_mpos_A, - "mpos_B" : mpos_B, - } - logger.info("Calibration OK : %.4f px/mm (%.6f mm/px)", px_per_mm_new, dmm/dpx) - return result - - def calib_reset(self): - self._calib_step = 0 - self._calib_pos_A_px = None - self._calib_mpos_A = None - - # ------------------------------------------------------------------ # - # Dessin debug - # ------------------------------------------------------------------ # - - def _draw_debug( - self, frame, cx_img, cy_img, - tx, ty, tr, - offset_x_px, offset_y_px, - offset_x_mm, offset_y_mm, - dist_px, action, - ) -> np.ndarray: - - # Couleur selon action - color = { - "none" : (0, 255, 0), # vert — centré - "crop" : (0, 200, 255), # orange — recadrage - "grbl" : (0, 0, 255), # rouge — correction CNC - }.get(action, (200, 200, 200)) - - # 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) - - # 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) - - # Croix centre image - 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) - - # Panneau info — fond semi-transparent - overlay = frame.copy() - cv2.rectangle(overlay, (8, 8), (400, 130), (0, 0, 0), -1) - cv2.addWeighted(overlay, 0.45, frame, 0.55, 0, frame) - - lines = [ - (f"Tube cx={tx} cy={ty} r={tr}px", (0, 255, 180)), - (f"Offset dx={offset_x_px:+d}px dy={offset_y_px:+d}px", color), - (f"Offset dx={offset_x_mm:+.3f}mm dy={offset_y_mm:+.3f}mm", color), - (f"Dist={dist_px:.1f}px action={action.upper()}", color), - (f"px/mm={self.px_per_mm:.4f}", (180, 180, 180)), - ] - for i, (text, col) in enumerate(lines): - cv2.putText(frame, text, (14, 30 + i * 20), - 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 - 3), - cv2.FONT_HERSHEY_SIMPLEX, 0.35, (0, 80, 255), 1) - - # Indicateur calibration en cours - if self._calib_step == 1: - cv2.putText(frame, "CALIB — En attente point B", - (14, frame.shape[0] - 14), - cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 200, 255), 2, cv2.LINE_AA) - - return frame - - def _draw_debug_no_detection(self, frame, cx_img, cy_img) -> np.ndarray: - cv2.drawMarker(frame, (cx_img, cy_img), - (255, 255, 255), cv2.MARKER_CROSS, 24, 1, cv2.LINE_AA) - cv2.putText(frame, "Tube non detecte", (14, 30), - cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2, cv2.LINE_AA) - return frame - - - \ No newline at end of file diff --git a/test_tube_scanner/modules/videofile_capture.py b/test_tube_scanner/modules/videofile_capture.py index ee752a3..80d0faf 100644 --- a/test_tube_scanner/modules/videofile_capture.py +++ b/test_tube_scanner/modules/videofile_capture.py @@ -40,8 +40,8 @@ class VideoFileCapture(VideoCaptureInterface): height: Optional[int] = None, video_lists = [], use_tracking: bool = False, - px_per_mm: float = 2.1, display = None, + parent = None, ): """ :param video_file: fichier video @@ -50,7 +50,7 @@ class VideoFileCapture(VideoCaptureInterface): :param width: Largeur souhaitée (None = valeur par défaut du pilote) :param height: Hauteur souhaitée (None = valeur par défaut du pilote) """ - super().__init__(fps=fps, use_tracking=use_tracking, px_per_mm=px_per_mm, display=display) + super().__init__(fps=fps, use_tracking=use_tracking, display=display, parent=parent) self._video_file: str = video_file self._jpeg_quality: int = jpeg_quality self._width: Optional[int] = width diff --git a/test_tube_scanner/modules/webcam_capture.py b/test_tube_scanner/modules/webcam_capture.py index cd8d7bf..281d819 100644 --- a/test_tube_scanner/modules/webcam_capture.py +++ b/test_tube_scanner/modules/webcam_capture.py @@ -39,8 +39,8 @@ class WebcamCapture(VideoCaptureInterface): width: Optional[int] = None, height: Optional[int] = None, use_tracking: bool = False, - px_per_mm: float = 2.1, display = None, + parent = None, ): """ :param device_index: Index du périphérique V4L2 (0 = première webcam) @@ -49,7 +49,7 @@ class WebcamCapture(VideoCaptureInterface): :param width: Largeur souhaitée (None = valeur par défaut du pilote) :param height: Hauteur souhaitée (None = valeur par défaut du pilote) """ - super().__init__(fps=fps, use_tracking=use_tracking, px_per_mm=px_per_mm, display=display) + super().__init__(fps=fps, use_tracking=use_tracking, display=display, parent=parent) self._device_index: int = device_index self._jpeg_quality: int = jpeg_quality self._width: Optional[int] = width diff --git a/test_tube_scanner/scanner/multiwell.py b/test_tube_scanner/scanner/multiwell.py index f67a87f..e1e7562 100644 --- a/test_tube_scanner/scanner/multiwell.py +++ b/test_tube_scanner/scanner/multiwell.py @@ -1,15 +1,20 @@ ''' +scanner/multiwell.py + WellIterator: Itérateur personnalisé pour naviguer dans les Wells + MultiWellManager: Manager des multi-puits + Created on 20 avr. 2026 @author: denis ''' +import cv2 +import numpy as np import logging import time -from django.utils.translation import gettext_lazy as _ from threading import Thread, Event +#from django.utils.translation import gettext_lazy as _ from django.utils import timezone from django.utils.html import mark_safe -from modules import grbl from . import models @@ -74,14 +79,12 @@ class MultiWellManager: self.process = process self.cnc_controller = process.grbl self.stop_playing = Event() - self.well_iterator = None - - self.scanner = None - + self.well_iterator = None self.multiwel = None self.set_default_values() self.set_multiwell() - + self.scan_thread = None + self.test_thread = None def set_default_values(self, feed=None, step=None, duration=None): self._feed = feed or self.process.conf.calibration_default_feed @@ -103,6 +106,7 @@ class MultiWellManager: self._ybase = self.multiwell.ybase self._dx = self.multiwell.dx self._dy = self.multiwell.dy + return self.multiwell.config() @@ -176,14 +180,18 @@ class MultiWellManager: def halt_scanning(self): self.process.data.record = False - return self.stop_playing.set() - + self.stop_playing.set() + self.scan_thread = None + self.test_thread = None + self.well_iterator.reset() + def scanning(self, sid): try: - session = models.Session.objects.get(pk=sid) - observations = models.SessionObservation.observation_by_session(sid) - Thread(target=self._start_scanning, args=(session, observations, ), daemon=True).start() + if self.scan_thread is None: + session = models.Session.objects.get(pk=sid) + observations = models.SessionObservation.observation_by_session(sid) + self.scan_thread = Thread(target=self._start_scanning, args=(session, observations, ), daemon=True).start() except Exception as e: print("MultiWellManager::scan error", e) @@ -208,34 +216,62 @@ class MultiWellManager: def set_well_position(self): w = self.well_iterator.get_current() - w.x, w.y = self.cnc_controller.get_mpos() - w.save() - return {"state": "well_position", "msg": f">>> saved ({w.x}, {w.y})"} + if w: + w.x, w.y = self.cnc_controller.get_mpos() + w.save() + if w.order == 0: + models.MultiWell.objects.filter(position__exact=w.multiwell.position).update(xbase=w.x, ybase=w.y) + return {"state": "well_position", "msg": f">>> saved ({w.x}, {w.y})"} + return {"state": "well_position", "msg": f">>> pas de puit"} - def _scanning_test(self, xnext=0, ynext=0): + def _scanning_test(self, auto=False): self.stop_playing = Event() + self.process.data.tube_diameter = self.multiwell.diameter + cam = self.process.cam + duration = self.duration if not auto else 45.0 + start_test = time.monotonic() + for w in self.well_iterator: if self.stop_playing.is_set(): break + self.cnc_controller.wait_for(2.0) self.cnc_controller.move_to(w.x, w.y, feed=w.multiwell.feed) - + self.process._send(current=w.order) + start = time.monotonic() while not self.stop_playing.is_set(): - if time.monotonic() - start > self.duration: + if time.monotonic() - start > duration: break - self.cnc_controller.wait_for(1.0) - - logger.info(f"Arrêter la simulation") - + if auto and cam._last_detection: + if cam._last_detection.get('action')=="grbl": + self.cnc_controller.wait_for(5.0) + dx_mm = cam._last_detection["offset_x_mm"] + dy_mm = cam._last_detection["offset_y_mm"] + + self.cnc_controller.move_to(self.cnc_controller.x + dx_mm, self.cnc_controller.y + dy_mm, feed=150) + msg = f"Correction CNC move_relative(dx={dx_mm:.3f}, dy={dy_mm:.3f})" + self.process._send(state='center', msg=msg) + elif cam._last_detection.get('action') in ['none', 'crop']: + msg = f"ok {w.multiwell.position}-{w.well.name}: ({self.cnc_controller.x}, {self.cnc_controller.y})" + logger.info(msg) + self.process._send(state='save', msg=msg) + w.x, w.y = self.cnc_controller.x, self.cnc_controller.y + w.save() + if w.order == 0: + models.MultiWell.objects.filter(position__exact=self.position).update(xbase=w.x, ybase=w.y) + break + self.cnc_controller.wait_for(0.1) + logger.info("Fin du centrage") self.well_iterator.reset() - logger.info(f"Scan terminé — retour à l'origine (X={xnext:.1f} Y={ynext:.1f})") - self.cnc_controller.move_to(xnext, ynext, feed=self.multiwell.feed*2) + logger.info(f"Scan terminé — retour à l'origine (X=0, Y=0) en {time.monotonic()-start_test} s") + self.cnc_controller.move_to(0, 0, feed=self.multiwell.feed*2) - def scan_test(self): - Thread(target=self._scanning_test, daemon=True).start() + def scan_test(self, auto=False): + if self.test_thread is None: + self.test_thread = Thread(target=self._scanning_test, args=(auto, ), daemon=True).start() @property @@ -303,8 +339,11 @@ class MultiWellManager: self._dy = value - def set_xy_step(self): - models.MultiWell.objects.filter(position__exact=self.position).update(dx=self.dx, dy=self.dy) + def get_well_order(self): + w = self.well_iterator.get_current() + if w: + return w.order + return None def set_position(self): @@ -312,10 +351,21 @@ class MultiWellManager: self.cnc_controller.wait_for(2.0) models.MultiWell.objects.filter(position__exact=self.position).update(xbase=x, ybase=y) self._xbase, self._ybase = x, y + w = self.well_iterator.seek(0) # base puit 0 + w.x, w.y = x, y + w.save() def calib_toggle_debug(self): """ Active / désactive le mode debug sur le stream.""" aligner = self.process.cam._aligner aligner.debug = not aligner.debug - return {"state": "debug", "msg": f"Debug: {aligner.debug}"} + return {"state": "debug", "msg": f"Debug: {aligner.debug}"} + + def set_calib_debug(self, value=True): + """ Active / désactive le mode debug sur le stream.""" + aligner = self.process.cam._aligner + aligner.debug = value + return {"state": "debug", "msg": f"Debug: {aligner.debug}"} + + diff --git a/test_tube_scanner/scanner/process.py b/test_tube_scanner/scanner/process.py index 3ecc933..629e888 100644 --- a/test_tube_scanner/scanner/process.py +++ b/test_tube_scanner/scanner/process.py @@ -4,7 +4,6 @@ import os os.environ['OPENCV_LOG_LEVEL']="0" os.environ['OPENCV_FFMPEG_LOGLEVEL']="0" import cv2 -import numpy as np from django.utils.translation import gettext_lazy as _ from datetime import datetime @@ -36,7 +35,8 @@ class ProcessData: record: bool = False uuid: str = None session: int = 0 - frame: bytes = None + tube_diameter: float = 16.0 + logger = get_task_logger(__name__) redisDB = Redis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=0, decode_responses=True) @@ -148,7 +148,7 @@ class ScannerProcess(Task): self.cam = None self.grbl = None self.crop = None - self.multiwel = None + #self.multiwel = None self.conf = None self.record_queue = Queue() self.data = ProcessData() @@ -187,9 +187,9 @@ class ScannerProcess(Task): height=self.video_height, jpeg_quality=self.video_quality, use_tracking=self.use_tracking, - px_per_mm = self.conf.px_per_mm, display=self._display, video_lists=[], + parent=self, ) ''' settings.MEDIA_ROOT / 'simulation' / 'part1-5fps.mp4', @@ -209,8 +209,8 @@ class ScannerProcess(Task): height=self.video_height, jpeg_quality=self.video_quality, use_tracking=self.use_tracking, - px_per_mm = self.conf.px_per_mm, display=self._display, + parent=self, ) else: from modules.picamera2_capture import PiCamera2Capture @@ -220,8 +220,8 @@ class ScannerProcess(Task): height=self.video_height, jpeg_quality=self.video_quality, use_tracking=self.use_tracking, - px_per_mm = self.conf.px_per_mm, display=self._display, + parent=self, ) self.cam.set_frame_callback(self._on_frame) @@ -263,12 +263,9 @@ class ScannerProcess(Task): self._send(**msg) def _on_frame(self, jpeg_bytes: bytes, ts: datetime, metrics: dict) -> None: - self.data.frame = jpeg_bytes if self.data.record: - # record images self.record_queue.put((self.data.uuid, ts, jpeg_bytes, metrics)) if self.data.play: - # play image self._send(ts=ts.timestamp(), jpeg=base64.b64encode(jpeg_bytes).decode(), **metrics) def _recording(self): @@ -399,37 +396,32 @@ class ScannerProcess(Task): elif topic == 'goto_xy': self.grbl.move_to(self.manager.xbase, self.manager.ybase, feed=self.manager.feed) - self.manager.well_iterator.reset() + self.manager.well_iterator.seek(0) elif topic == 'xy_base': self.manager.set_position() - elif topic == 'dx': - self.manager.dx = float(value) - - elif topic == 'dy': - self.manager.dy = float(value) - - elif topic == 'xy_step': - self.manager.set_xy_step() - elif topic == 'test': - pass - #self.manager.scan_test() + self.manager.scan_test() continue + + elif topic == 'auto': + self.manager.set_calib_debug(True) + self.cam.set_circular_crop(self.crop) + self.manager.scan_test(auto=True) + continue + elif topic == 'center': - #self.manager.scan_test() dx_mm = self.cam._last_detection["offset_x_mm"] dy_mm = self.cam._last_detection["offset_y_mm"] self.grbl.move_to(self.grbl.x + dx_mm, self.grbl.y + dy_mm, feed=150) - msg = f"Correction CNC move_relative(dx={dx_mm:.3f}, dy={dy_mm:.3f})" self._send(state='center', msg=msg) continue elif topic == 'halt': self.manager.halt_scanning() - continue + elif topic == 'calib_debug': msg = self.manager.calib_toggle_debug() @@ -439,23 +431,19 @@ class ScannerProcess(Task): elif topic == 'previous': msg = self.manager.previous_well() self._send(**msg) - continue - + elif topic == 'next': msg = self.manager.next_well() self._send(**msg) - continue - + elif topic == 'goto': msg = self.manager.goto_well(int(value)) self._send(**msg) - continue - + elif topic == 'set_well': msg = self.manager.set_well_position() self._send(**msg) - continue - + self._send( xbase=self.manager.xbase, ybase=self.manager.ybase, @@ -466,8 +454,8 @@ class ScannerProcess(Task): dx=self.manager.dx, dy=self.manager.dy, buttons=buttons, + current=self.manager.get_well_order(), ) - except Exception as e: logger.error(f'scanner listen_to_redis: {e}') finally: diff --git a/test_tube_scanner/scanner/static/scanner/js/calibration.js b/test_tube_scanner/scanner/static/scanner/js/calibration.js index 1729647..7cc7b3f 100644 --- a/test_tube_scanner/scanner/static/scanner/js/calibration.js +++ b/test_tube_scanner/scanner/static/scanner/js/calibration.js @@ -1,88 +1,76 @@ class ScannerManager { - constructor(container) { + constructor(container, options = {}) { this.container = container; this.socket = null; - this.axes = 0; - this.cropping = 0; - this.debug_count = 0 + this.debug_count = 0; + + this.ts = options.ts; + this.cx = options.cx; + this.cy = options.cy; + this.x = options.x; + this.y = options.y; + this.xbase= options.xbase; + this.ybase= options.ybase; + this.test = options.test; + this.halt = options.halt; + this.speed_px_s = options.speed_px_s; + this.axial_speed = options.axial_speed; + this.axial_pos = options.axial_pos; + this.area_px = options.area_px; + this.frame_count = options.frame_count; + this.goto_0 = options.goto_0; + this.goto_xy = options.goto_xy; + this.xy_base = options.xy_base; + this.up = options.up; + this.down = options.down; + this.left = options.left; + this.right = options.right; + this.duration= options.duration; + this.feed = options.feed; + this.step = options.step; + this.well = options.well; + this.debug = options.debug; + this.calib_debug = options.calib_debug; + this.calib_center= options.calib_center; + this.previous = options.previous; + this.next = options.next; + this.set_well = options.set_well; + this.well_btn = options.well_btn; + this.median = options.median; + this.crop = options.crop; + this.crop_radius = options.crop_radius; + this.calib_auto = options.calib_auto; } + + init_controls() { + this.up.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "up" }); }); + this.down.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "down" }); }); + this.left.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "left" }); }); + this.right.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "right" }); }); - init_controls() { - this.ts = sId("_ts"); - this.cx = sId("_cx"); - this.cy = sId("_cy"); - this.speed_px_s = sId("_speed_px_s"); - this.axial_speed = sId("_axial_speed"); - this.axial_pos = sId("_axial_pos"); - this.area_px = sId("_area_px"); - this.frame_count = sId("_count"); + this.goto_0.addEventListener('click', (e) => { this.clear_buttons(); this._send({ type: 'calibrate', topic: "goto_0" }); }); + this.goto_xy.addEventListener('click', (e) => { this.clear_buttons(); this._send({ type: 'calibrate', topic: "goto_xy" }); }); + this.xy_base.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "xy_base" }); }); - const goto_0 = sId("_goto-0"); - const goto_xy = sId("_goto-xy"); - const xy_base = sId("_xy-base"); - const xy_step = sId("_xy-step"); - const up = sId("_up"); - const down = sId("_down"); - const left = sId("_left"); - const right = sId("_right"); - this.duration = sId("_duration"); - this.feed = sId("_feed"); - this.step = sId("_step"); - this.well = sId("_well"); - this.x = sId("_x"); - this.y = sId("_y"); - this.dx = sId("_dx"); - this.dy = sId("_dy"); - this.xbase = sId("_xbase"); - this.ybase = sId("_ybase"); - this.debug = sId("_debug"); - this.well_btn = sId("_well_btn"); - - const test = sId("_test"); - const halt = sId("_halt"); - - const calib_debug = sId("_calib_debug"); - const calib_center = sId("_calib_center"); - const previous = sId("_previous"); - const next = sId("_next"); - const set_well = sId("_set_well"); - - const median = sId("_median"); - const crop = sId("_crop"); - const crop_radius = sId("_crop_radius"); + this.calib_debug.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "calib_debug" }); }); + this.previous.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "previous" }); }); + this.next.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "next" }); }); + this.set_well.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "set_well" }); }); + this.median.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "median" }); }); + this.crop.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "crop" }); }); + this.crop_radius.addEventListener('change',(e) => { this._send({ type: 'calibrate', topic: "crop_radius", value: this.crop_radius.value }); }); + this.well.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "position", value: e.target.value }); }); + this.step.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "step", value: e.target.value }); }); + this.feed.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "feed", value: e.target.value }); }); + this.duration.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "duration", value: e.target.value }); }); - up.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "up" }); }); - down.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "down" }); }); - left.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "left" }); }); - right.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "right" }); }); - - goto_0.addEventListener('click', (e) => { this.clear_buttons(); this._send({ type: 'calibrate', topic: "goto_0" }); }); - goto_xy.addEventListener('click', (e) => { this.clear_buttons(); this._send({ type: 'calibrate', topic: "goto_xy" }); }); - xy_base.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "xy_base" }); }); - xy_step.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "xy_step" }); }); - - calib_debug.addEventListener('click',(e) => { this._send({ type: 'calibrate', topic: "calib_debug" }); }); - previous.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "previous" }); }); - next.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "next" }); }); - set_well.addEventListener('click',(e) => { this._send({ type: 'calibrate', topic: "set_well" }); }); - - median.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "median" }); }); - crop.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "crop" }); }); - crop_radius.addEventListener('change',(e) => { this._send({ type: 'calibrate', topic: "crop_radius", value: crop_radius.value }); }); - this.well.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "position", value: e.target.value }); }); - this.step.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "step", value: e.target.value }); }); - this.feed.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "feed", value: e.target.value }); }); - this.duration.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "duration", value: e.target.value }); }); - - this.dx.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "dx", value: e.target.value }); }); - this.dy.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "dy", value: e.target.value }); }); - - test.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "test" }); }); - calib_center.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "center" }); }); - halt.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "halt" }); }); + this.test.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "test" }); }); + this.calib_center.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "center" }); }); + this.calib_auto.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "auto" }); }); + this.halt.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "halt" }); }); } registerSocket(socket) { @@ -91,10 +79,9 @@ class ScannerManager { } update(payload) { - try { + try { if (payload.jpeg) { this.container.src = `data:image/jpeg;base64,${payload.jpeg}`; } if (payload.xbase) { this.xbase.textContent = payload.xbase; this.ybase.textContent = payload.ybase; } - if (payload.dxy) { this.dy.value=payload.dy; this.dx.value=payload.dx; } if (payload.xy) { this.x.textContent=payload.x.toFixed(2); this.y.textContent=payload.y.toFixed(2); } if (payload.state) { this.debug.insertAdjacentHTML('afterbegin', `
  • [ ${++this.debug_count} - ${payload.state} ]: ${payload.msg}
  • `); } if (payload.ts) { this.ts.textContent = timestampToLocalISOString(payload.ts); } @@ -108,6 +95,12 @@ class ScannerManager { this.frame_count.textContent = payload.count; } if (payload.buttons) { this.well_btn.innerHTML = payload.buttons; } + if (payload.current >= 0) { + document.querySelectorAll('button.w3-button.well').forEach(btn => { + if (btn.value==payload.current) { btn.classList.add('w3-green'); return; } + btn.classList.remove('w3-green'); + }); + } } catch(e) { console.log(e); } } @@ -115,8 +108,6 @@ class ScannerManager { clear_buttons() { document.querySelectorAll('button.w3-button.well').forEach(btn => {btn.classList.remove('w3-green'); }); } goto_well(b) { this.clear_buttons(); b.classList.add('w3-green'); this._send({ type: 'calibrate', topic: "goto", value: b.value }); } init() { - this.axes = 0; - this.cropping = 0; this.clear_buttons(); this._send({ type: 'calibrate', diff --git a/test_tube_scanner/scanner/static/scanner/js/main.js b/test_tube_scanner/scanner/static/scanner/js/main.js index 4c39c19..f369996 100644 --- a/test_tube_scanner/scanner/static/scanner/js/main.js +++ b/test_tube_scanner/scanner/static/scanner/js/main.js @@ -1,40 +1,34 @@ class ScannerManager { - constructor(container) { + constructor(container, options = {}) { this.container = container; this.socket = null; - this.axes = 0; - this.cropping = 1; - this.debug_count = 0 + this.debug_count = 0; + + this.ts = options.ts; + this.cx = options.cx; + this.cy = options.cy; + this.x = options.x; + this.y = options.y; + this.session = options.session; + this.scan_bt = options.scan; + this.halt_bt = options.halt; + this.debug = options.debug; + this.median = options.median; + this._rop = options.crop; + this.speed_px_s = options.speed_px_s; + this.axial_speed = options.axial_speed; + this.axial_pos = options.axial_pos; + this.area_px = options.area_px; + this.frame_count = options.frame_count; } - toggle_median() { this.axes = !this.axes; return this.axes; } - toggle_crop() { this.croping = !this.croping; return this.croping; } - init_controls() { - this.cx = sId("_cx"); - this.cy = sId("_cy"); - this.speed_px_s = sId("_speed_px_s"); - this.axial_speed = sId("_axial_speed"); - this.axial_pos = sId("_axial_pos"); - this.area_px = sId("_area_px"); - this.frame_count = sId("_count"); - - this.session= sId("_session"); - this.ts = sId("_ts"); - this.x = sId("_x"); - this.y = sId("_y"); - this.debug = sId("_debug"); - const scan = sId("_scan"); - const halt = sId("_halt"); - const median = sId("_median"); - const crop = sId("_crop"); - - median.addEventListener('click',(e) => { this._send({ type: 'calibrate', topic: "median", value: this.toggle_median() }); }); - crop.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "crop", value: this.toggle_crop() }); }); - scan.addEventListener('click', (e) => { this.scan(); }); - halt.addEventListener('click', (e) => { this.halt(); }); + this.median.addEventListener('click',(e) => { this._send({ type: 'calibrate', topic: "median" }); }); + this.crop.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "crop" }); }); + this.scan_bt.addEventListener('click', (e) => { this.scan(); }); + this.halt_bt.addEventListener('click', (e) => { this.halt(); }); } registerSocket(socket) { diff --git a/test_tube_scanner/scanner/templates/scanner/calibration.html b/test_tube_scanner/scanner/templates/scanner/calibration.html index 46135e2..3007214 100644 --- a/test_tube_scanner/scanner/templates/scanner/calibration.html +++ b/test_tube_scanner/scanner/templates/scanner/calibration.html @@ -11,7 +11,7 @@
    -
    +
    {% trans 'Position multi-puit' %}
    -
    +
    {% trans 'Pas' %}
    -
    +
    {% trans 'Durée' %}
    -
    -
    +
    +

    @@ -70,77 +73,105 @@
    - -
    -
    {% trans 'dx' %}
    -
    {% trans 'dy' %}
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    -
    +
    X
    Y
    -
    - -
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + - -
    - - - - - {% trans 'Rayon' %}: - -
    - +
    +
    + +
    +
    + +
    +
    + {% trans 'Rayon' %}
    + px +
    +
    +
    {% include 'scanner/scan-image.html' %} @@ -154,10 +185,49 @@ const container = sId("scan-img"); const ws_route = "{{ ws_route }}"; const use_tracking = "{{ use_tracking }}" == "True"; + + const options = { + ts : sId("_ts"), + cx : sId("_cx"), + cy : sId("_cy"), + x : sId("_x"), + y : sId("_y"), + xbase: sId("_xbase"), + ybase: sId("_ybase"), + test : sId("_test"), + halt : sId("_halt"), + speed_px_s : sId("_speed_px_s"), + axial_speed : sId("_axial_speed"), + axial_pos : sId("_axial_pos"), + area_px : sId("_area_px"), + frame_count : sId("_count"), + goto_0 : sId("_goto-0"), + goto_xy : sId("_goto-xy"), + xy_base : sId ("_xy-base"), + up : sId("_up"), + down : sId("_down"), + left : sId("_left"), + right : sId("_right"), + duration: sId("_duration"), + feed : sId("_feed"), + step : sId("_step"), + well : sId("_well"), + debug : sId("_debug"), + calib_debug : sId("_calib_debug"), + calib_auto : sId("_calib_auto"), + calib_center: sId("_calib_center"), + previous : sId("_previous"), + next : sId("_next"), + set_well : sId("_set_well"), + well_btn : sId("_well_btn"), + median : sId("_median"), + crop : sId("_crop"), + crop_radius : sId("_crop_radius") + }; {% endblock %}