calibrage auto
This commit is contained in:
+1
-1
@@ -8,7 +8,7 @@ sudo apt update
|
|||||||
|
|
||||||
# system
|
# system
|
||||||
echo "==== system essential"
|
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"
|
echo "==== python3 install"
|
||||||
sudo apt -y install python3-dev python3-pip python3-venv default-libmysqlclient-dev libmariadb-dev libpq-dev python3-picamera2
|
sudo apt -y install python3-dev python3-pip python3-venv default-libmysqlclient-dev libmariadb-dev libpq-dev python3-picamera2
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -17,49 +17,9 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
#: home/apps.py:7
|
#: home/apps.py:7
|
||||||
msgid "Automation"
|
msgid "Scanner de tube à essais"
|
||||||
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"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: home/templates/inc/404.html:9
|
#: home/templates/inc/404.html:9
|
||||||
@@ -80,6 +40,736 @@ msgid ""
|
|||||||
"administrator."
|
"administrator."
|
||||||
msgstr ""
|
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
|
#: templates/admin/base_site.html:4
|
||||||
msgid "Django site admin"
|
msgid "Django site admin"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class VideoCaptureInterface(abc.ABC):
|
|||||||
# Cadence par défaut en images par seconde
|
# Cadence par défaut en images par seconde
|
||||||
DEFAULT_FPS: float = 5.0
|
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.
|
Initialise l'interface de capture.
|
||||||
|
|
||||||
@@ -58,6 +58,7 @@ class VideoCaptureInterface(abc.ABC):
|
|||||||
self._fps: float = fps
|
self._fps: float = fps
|
||||||
self.use_tracking = use_tracking
|
self.use_tracking = use_tracking
|
||||||
self.display = display
|
self.display = display
|
||||||
|
self.parent = parent
|
||||||
self._interval: float = 1.0 / fps # Intervalle en secondes entre chaque capture
|
self._interval: float = 1.0 / fps # Intervalle en secondes entre chaque capture
|
||||||
self._running: bool = False # Indique si la capture est en cours
|
self._running: bool = False # Indique si la capture est en cours
|
||||||
self._thread: Optional[threading.Thread] = None
|
self._thread: Optional[threading.Thread] = None
|
||||||
@@ -73,7 +74,6 @@ class VideoCaptureInterface(abc.ABC):
|
|||||||
min_area_px = settings.TRACKER_MIN_AREA,
|
min_area_px = settings.TRACKER_MIN_AREA,
|
||||||
)
|
)
|
||||||
self._aligner = TubeAligner(
|
self._aligner = TubeAligner(
|
||||||
px_per_mm = px_per_mm, # à calibrer selon la caméra
|
|
||||||
grbl_threshold_px = 20, # au-delà → correction GRBL
|
grbl_threshold_px = 20, # au-delà → correction GRBL
|
||||||
dead_zone_px = 5, # en-dessous → rien à faire
|
dead_zone_px = 5, # en-dessous → rien à faire
|
||||||
display = display,
|
display = display,
|
||||||
@@ -82,7 +82,8 @@ class VideoCaptureInterface(abc.ABC):
|
|||||||
|
|
||||||
# calibrage ou lecture réelle
|
# 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.
|
Appelé UNE FOIS à l'arrivée sur un nouveau puits.
|
||||||
Détecte le tube, décide l'action, exécute la correction.
|
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)
|
nparr = np.frombuffer(frame, np.uint8)
|
||||||
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
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
|
# Stockage pour process_frame
|
||||||
self._last_detection = detection
|
self._last_detection = detection
|
||||||
@@ -108,7 +109,7 @@ class VideoCaptureInterface(abc.ABC):
|
|||||||
dy_mm = detection["offset_y_mm"]
|
dy_mm = detection["offset_y_mm"]
|
||||||
|
|
||||||
msg = f"align_on_well_arrival: correction CNC move_relative(dx={dx_mm:.3f}, dy={dy_mm:.3f})"
|
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._tracker.reset()
|
||||||
self._last_detection["action"] = "none"
|
self._last_detection["action"] = "none"
|
||||||
@@ -276,18 +277,14 @@ class VideoCaptureInterface(abc.ABC):
|
|||||||
|
|
||||||
# Mode debug
|
# Mode debug
|
||||||
if self._aligner.debug:
|
if self._aligner.debug:
|
||||||
self._last_detection = detection = self._aligner.detect_tube(frame)
|
self._last_detection = self._aligner.detect_tube(frame, self.parent.data.tube_diameter or 16.0)
|
||||||
annotated = detection.get('frame_annotated')
|
annotated = self._last_detection.get('frame_annotated')
|
||||||
frame = annotated if annotated is not None else frame
|
frame = annotated if annotated is not None else frame
|
||||||
'''
|
|
||||||
else:
|
|
||||||
detection = self._last_detection or {}
|
|
||||||
|
|
||||||
# --- Crop logiciel si nécessaire ---
|
#if (self._last_detection.get("action") == "crop" and self._last_detection.get("detected")):
|
||||||
if (detection.get("action") == "crop" and detection.get("detected") and not self._aligner.debug ):
|
# frame = self._aligner.crop_to_tube(frame, self._last_detection)
|
||||||
frame = self._aligner.crop_to_tube(frame, detection)
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
# mode racking
|
||||||
if self.use_tracking:
|
if self.use_tracking:
|
||||||
ts = datetime.now(timezone.utc).timestamp()
|
ts = datetime.now(timezone.utc).timestamp()
|
||||||
frame, metrics = self._tracker.process(frame, ts)
|
frame, metrics = self._tracker.process(frame, ts)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import logging
|
|||||||
import serial
|
import serial
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -194,7 +194,7 @@ class GRBLController:
|
|||||||
x, y = self.get_mpos() # Position actuelle
|
x, y = self.get_mpos() # Position actuelle
|
||||||
self.move_to(x + dx, y + dy, feed=feed)
|
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
|
self.send("G91") # Mode relatif
|
||||||
cmd = f"G0 X{dx} Y{dy} F{feed}"
|
cmd = f"G0 X{dx} Y{dy} F{feed}"
|
||||||
self.send(cmd)
|
self.send(cmd)
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ class PiCamera2Capture(VideoCaptureInterface):
|
|||||||
camera_index: int = 0,
|
camera_index: int = 0,
|
||||||
use_video_config: bool = True,
|
use_video_config: bool = True,
|
||||||
use_tracking: bool = False,
|
use_tracking: bool = False,
|
||||||
px_per_mm: float = 2.1,
|
|
||||||
display = None,
|
display = None,
|
||||||
|
parent = None
|
||||||
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -59,7 +59,7 @@ class PiCamera2Capture(VideoCaptureInterface):
|
|||||||
:param use_video_config: True = VideoConfiguration (flux continu, basse latence)
|
:param use_video_config: True = VideoConfiguration (flux continu, basse latence)
|
||||||
False = StillConfiguration (haute résolution, plus lent)
|
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._width: int = width
|
||||||
self._height: int = height
|
self._height: int = height
|
||||||
self._jpeg_quality: int = jpeg_quality
|
self._jpeg_quality: int = jpeg_quality
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ class PiCamera2Capture(VideoCaptureInterface):
|
|||||||
jpeg_quality: int = 85,
|
jpeg_quality: int = 85,
|
||||||
camera_index: int = 0,
|
camera_index: int = 0,
|
||||||
use_video_config: bool = True,
|
use_video_config: bool = True,
|
||||||
|
use_tracking: bool = False,
|
||||||
|
display = None,
|
||||||
|
parent=None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
:param fps: Cadence cible en images par seconde
|
:param fps: Cadence cible en images par seconde
|
||||||
@@ -47,7 +50,7 @@ class PiCamera2Capture(VideoCaptureInterface):
|
|||||||
:param use_video_config: True = configuration VideoConfiguration (flux continu)
|
:param use_video_config: True = configuration VideoConfiguration (flux continu)
|
||||||
False = StillConfiguration (haute résolution, plus lent)
|
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._width: int = width
|
||||||
self._height: int = height
|
self._height: int = height
|
||||||
self._jpeg_quality: int = jpeg_quality
|
self._jpeg_quality: int = jpeg_quality
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'''
|
'''
|
||||||
|
modules/tube_aligner.py
|
||||||
Created on 17 avr. 2026
|
Created on 17 avr. 2026
|
||||||
|
|
||||||
@author: denis
|
@author: denis
|
||||||
'''
|
'''
|
||||||
# modules/tube_aligner.py
|
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import logging
|
import logging
|
||||||
@@ -19,13 +19,11 @@ class TubeAligner:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
px_per_mm : float = 10.0,
|
|
||||||
grbl_threshold_px : int = 20,
|
grbl_threshold_px : int = 20,
|
||||||
dead_zone_px : int = 5,
|
dead_zone_px : int = 5,
|
||||||
debug : bool = False, # ← activable depuis la vue
|
debug : bool = False, # ← activable depuis la vue
|
||||||
display = None,
|
display = None, # display function
|
||||||
):
|
):
|
||||||
self.TUBE_DIAMETER_MM = 16.0
|
|
||||||
self.grbl_threshold_px = grbl_threshold_px
|
self.grbl_threshold_px = grbl_threshold_px
|
||||||
self.dead_zone_px = dead_zone_px
|
self.dead_zone_px = dead_zone_px
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
@@ -35,7 +33,8 @@ class TubeAligner:
|
|||||||
# Détection principale
|
# 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]
|
h, w = frame.shape[:2]
|
||||||
cx_img = w // 2
|
cx_img = w // 2
|
||||||
cy_img = h // 2
|
cy_img = h // 2
|
||||||
@@ -93,16 +92,14 @@ class TubeAligner:
|
|||||||
ty = int(np.mean(all_cy))
|
ty = int(np.mean(all_cy))
|
||||||
tr = int(np.mean(all_r))
|
tr = int(np.mean(all_r))
|
||||||
if tr > 0:
|
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_x_px = tx - cx_img
|
||||||
offset_y_px = ty - cy_img
|
offset_y_px = ty - cy_img
|
||||||
#offset_x_mm = offset_x_px / self.px_per_mm
|
#offset_x_mm = offset_x_px / self.px_per_mm
|
||||||
#offset_y_mm = offset_y_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_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))
|
dist_px = float(np.sqrt(offset_x_px**2 + offset_y_px**2))
|
||||||
|
|
||||||
if dist_px <= self.dead_zone_px:
|
if dist_px <= self.dead_zone_px:
|
||||||
@@ -162,82 +159,6 @@ class TubeAligner:
|
|||||||
# Redimensionne à la taille originale pour ne pas changer le pipeline
|
# Redimensionne à la taille originale pour ne pas changer le pipeline
|
||||||
return cv2.resize(cropped, (w, h), interpolation=cv2.INTER_LINEAR)
|
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
|
# Dessin debug
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@@ -259,26 +180,18 @@ class TubeAligner:
|
|||||||
|
|
||||||
# Cercle intérieur du tube
|
# Cercle intérieur du tube
|
||||||
cv2.circle(frame, (tx, ty), tr, color, 2, cv2.LINE_AA)
|
cv2.circle(frame, (tx, ty), tr, color, 2, cv2.LINE_AA)
|
||||||
|
|
||||||
# Rayon de zone morte (dead zone) en vert clair
|
# Rayon de zone morte (dead zone) en vert clair
|
||||||
cv2.circle(frame, (cx_img, cy_img), self.dead_zone_px,
|
cv2.circle(frame, (cx_img, cy_img), self.dead_zone_px, (0, 255, 100), 1, cv2.LINE_AA)
|
||||||
(0, 255, 100), 1, cv2.LINE_AA)
|
|
||||||
|
|
||||||
# Rayon seuil GRBL en rouge pointillé (simulé par cercle fin)
|
# Rayon seuil GRBL en rouge pointillé (simulé par cercle fin)
|
||||||
cv2.circle(frame, (cx_img, cy_img), self.grbl_threshold_px,
|
cv2.circle(frame, (cx_img, cy_img), self.grbl_threshold_px, (0, 80, 255), 1, cv2.LINE_AA)
|
||||||
(0, 80, 255), 1, cv2.LINE_AA)
|
|
||||||
|
|
||||||
# Croix centre image
|
# Croix centre image
|
||||||
cv2.drawMarker(frame, (cx_img, cy_img),
|
cv2.drawMarker(frame, (cx_img, cy_img), (255, 255, 255), cv2.MARKER_CROSS, 24, 1, cv2.LINE_AA)
|
||||||
(255, 255, 255), cv2.MARKER_CROSS, 24, 1, cv2.LINE_AA)
|
|
||||||
|
|
||||||
# Centre tube
|
# Centre tube
|
||||||
cv2.circle(frame, (tx, ty), 5, color, -1, cv2.LINE_AA)
|
cv2.circle(frame, (tx, ty), 5, color, -1, cv2.LINE_AA)
|
||||||
|
|
||||||
# Vecteur offset centre image → centre tube
|
# Vecteur offset centre image → centre tube
|
||||||
if dist_px > self.dead_zone_px:
|
if dist_px > self.dead_zone_px:
|
||||||
cv2.arrowedLine(frame, (cx_img, cy_img), (tx, ty),
|
cv2.arrowedLine(frame, (cx_img, cy_img), (tx, ty), color, 2, cv2.LINE_AA, tipLength=0.2)
|
||||||
color, 2, cv2.LINE_AA, tipLength=0.2)
|
|
||||||
|
|
||||||
# Panneau info — fond semi-transparent
|
# Panneau info — fond semi-transparent
|
||||||
overlay = frame.copy()
|
overlay = frame.copy()
|
||||||
@@ -298,10 +211,8 @@ class TubeAligner:
|
|||||||
cv2.FONT_HERSHEY_SIMPLEX, 0.48, col, 1, cv2.LINE_AA)
|
cv2.FONT_HERSHEY_SIMPLEX, 0.48, col, 1, cv2.LINE_AA)
|
||||||
|
|
||||||
# Légende zones
|
# Légende zones
|
||||||
cv2.putText(frame, "dead zone", (cx_img + self.dead_zone_px + 3, cy_img - 3),
|
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.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, "GRBL threshold", (cx_img + self.grbl_threshold_px + 3, cy_img + 6),
|
|
||||||
cv2.FONT_HERSHEY_SIMPLEX, 0.35, (0, 80, 255), 1)
|
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
def _draw_debug_no_detection(self, frame, cx_img, cy_img) -> np.ndarray:
|
def _draw_debug_no_detection(self, frame, cx_img, cy_img) -> np.ndarray:
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -40,8 +40,8 @@ class VideoFileCapture(VideoCaptureInterface):
|
|||||||
height: Optional[int] = None,
|
height: Optional[int] = None,
|
||||||
video_lists = [],
|
video_lists = [],
|
||||||
use_tracking: bool = False,
|
use_tracking: bool = False,
|
||||||
px_per_mm: float = 2.1,
|
|
||||||
display = None,
|
display = None,
|
||||||
|
parent = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
:param video_file: fichier video
|
: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 width: Largeur souhaitée (None = valeur par défaut du pilote)
|
||||||
:param height: Hauteur 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._video_file: str = video_file
|
||||||
self._jpeg_quality: int = jpeg_quality
|
self._jpeg_quality: int = jpeg_quality
|
||||||
self._width: Optional[int] = width
|
self._width: Optional[int] = width
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ class WebcamCapture(VideoCaptureInterface):
|
|||||||
width: Optional[int] = None,
|
width: Optional[int] = None,
|
||||||
height: Optional[int] = None,
|
height: Optional[int] = None,
|
||||||
use_tracking: bool = False,
|
use_tracking: bool = False,
|
||||||
px_per_mm: float = 2.1,
|
|
||||||
display = None,
|
display = None,
|
||||||
|
parent = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
:param device_index: Index du périphérique V4L2 (0 = première webcam)
|
: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 width: Largeur souhaitée (None = valeur par défaut du pilote)
|
||||||
:param height: Hauteur 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._device_index: int = device_index
|
||||||
self._jpeg_quality: int = jpeg_quality
|
self._jpeg_quality: int = jpeg_quality
|
||||||
self._width: Optional[int] = width
|
self._width: Optional[int] = width
|
||||||
|
|||||||
@@ -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
|
Created on 20 avr. 2026
|
||||||
|
|
||||||
@author: denis
|
@author: denis
|
||||||
'''
|
'''
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from threading import Thread, Event
|
from threading import Thread, Event
|
||||||
|
#from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.html import mark_safe
|
from django.utils.html import mark_safe
|
||||||
from modules import grbl
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
@@ -75,13 +80,11 @@ class MultiWellManager:
|
|||||||
self.cnc_controller = process.grbl
|
self.cnc_controller = process.grbl
|
||||||
self.stop_playing = Event()
|
self.stop_playing = Event()
|
||||||
self.well_iterator = None
|
self.well_iterator = None
|
||||||
|
|
||||||
self.scanner = None
|
|
||||||
|
|
||||||
self.multiwel = None
|
self.multiwel = None
|
||||||
self.set_default_values()
|
self.set_default_values()
|
||||||
self.set_multiwell()
|
self.set_multiwell()
|
||||||
|
self.scan_thread = None
|
||||||
|
self.test_thread = None
|
||||||
|
|
||||||
def set_default_values(self, feed=None, step=None, duration=None):
|
def set_default_values(self, feed=None, step=None, duration=None):
|
||||||
self._feed = feed or self.process.conf.calibration_default_feed
|
self._feed = feed or self.process.conf.calibration_default_feed
|
||||||
@@ -103,6 +106,7 @@ class MultiWellManager:
|
|||||||
self._ybase = self.multiwell.ybase
|
self._ybase = self.multiwell.ybase
|
||||||
self._dx = self.multiwell.dx
|
self._dx = self.multiwell.dx
|
||||||
self._dy = self.multiwell.dy
|
self._dy = self.multiwell.dy
|
||||||
|
|
||||||
return self.multiwell.config()
|
return self.multiwell.config()
|
||||||
|
|
||||||
|
|
||||||
@@ -176,14 +180,18 @@ class MultiWellManager:
|
|||||||
|
|
||||||
def halt_scanning(self):
|
def halt_scanning(self):
|
||||||
self.process.data.record = False
|
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):
|
def scanning(self, sid):
|
||||||
try:
|
try:
|
||||||
|
if self.scan_thread is None:
|
||||||
session = models.Session.objects.get(pk=sid)
|
session = models.Session.objects.get(pk=sid)
|
||||||
observations = models.SessionObservation.observation_by_session(sid)
|
observations = models.SessionObservation.observation_by_session(sid)
|
||||||
Thread(target=self._start_scanning, args=(session, observations, ), daemon=True).start()
|
self.scan_thread = Thread(target=self._start_scanning, args=(session, observations, ), daemon=True).start()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("MultiWellManager::scan error", e)
|
print("MultiWellManager::scan error", e)
|
||||||
|
|
||||||
@@ -208,34 +216,62 @@ class MultiWellManager:
|
|||||||
|
|
||||||
def set_well_position(self):
|
def set_well_position(self):
|
||||||
w = self.well_iterator.get_current()
|
w = self.well_iterator.get_current()
|
||||||
|
if w:
|
||||||
w.x, w.y = self.cnc_controller.get_mpos()
|
w.x, w.y = self.cnc_controller.get_mpos()
|
||||||
w.save()
|
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">>> 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.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:
|
for w in self.well_iterator:
|
||||||
if self.stop_playing.is_set():
|
if self.stop_playing.is_set():
|
||||||
break
|
break
|
||||||
|
self.cnc_controller.wait_for(2.0)
|
||||||
self.cnc_controller.move_to(w.x, w.y, feed=w.multiwell.feed)
|
self.cnc_controller.move_to(w.x, w.y, feed=w.multiwell.feed)
|
||||||
|
self.process._send(current=w.order)
|
||||||
|
|
||||||
start = time.monotonic()
|
start = time.monotonic()
|
||||||
while not self.stop_playing.is_set():
|
while not self.stop_playing.is_set():
|
||||||
if time.monotonic() - start > self.duration:
|
if time.monotonic() - start > duration:
|
||||||
break
|
break
|
||||||
self.cnc_controller.wait_for(1.0)
|
if auto and cam._last_detection:
|
||||||
|
if cam._last_detection.get('action')=="grbl":
|
||||||
logger.info(f"Arrêter la simulation")
|
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()
|
self.well_iterator.reset()
|
||||||
|
|
||||||
logger.info(f"Scan terminé — retour à l'origine (X={xnext:.1f} Y={ynext:.1f})")
|
logger.info(f"Scan terminé — retour à l'origine (X=0, Y=0) en {time.monotonic()-start_test} s")
|
||||||
self.cnc_controller.move_to(xnext, ynext, feed=self.multiwell.feed*2)
|
self.cnc_controller.move_to(0, 0, feed=self.multiwell.feed*2)
|
||||||
|
|
||||||
|
|
||||||
def scan_test(self):
|
def scan_test(self, auto=False):
|
||||||
Thread(target=self._scanning_test, daemon=True).start()
|
if self.test_thread is None:
|
||||||
|
self.test_thread = Thread(target=self._scanning_test, args=(auto, ), daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -303,8 +339,11 @@ class MultiWellManager:
|
|||||||
self._dy = value
|
self._dy = value
|
||||||
|
|
||||||
|
|
||||||
def set_xy_step(self):
|
def get_well_order(self):
|
||||||
models.MultiWell.objects.filter(position__exact=self.position).update(dx=self.dx, dy=self.dy)
|
w = self.well_iterator.get_current()
|
||||||
|
if w:
|
||||||
|
return w.order
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def set_position(self):
|
def set_position(self):
|
||||||
@@ -312,6 +351,9 @@ class MultiWellManager:
|
|||||||
self.cnc_controller.wait_for(2.0)
|
self.cnc_controller.wait_for(2.0)
|
||||||
models.MultiWell.objects.filter(position__exact=self.position).update(xbase=x, ybase=y)
|
models.MultiWell.objects.filter(position__exact=self.position).update(xbase=x, ybase=y)
|
||||||
self._xbase, self._ybase = x, 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):
|
def calib_toggle_debug(self):
|
||||||
@@ -319,3 +361,11 @@ class MultiWellManager:
|
|||||||
aligner = self.process.cam._aligner
|
aligner = self.process.cam._aligner
|
||||||
aligner.debug = not aligner.debug
|
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}"}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import os
|
|||||||
os.environ['OPENCV_LOG_LEVEL']="0"
|
os.environ['OPENCV_LOG_LEVEL']="0"
|
||||||
os.environ['OPENCV_FFMPEG_LOGLEVEL']="0"
|
os.environ['OPENCV_FFMPEG_LOGLEVEL']="0"
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -36,7 +35,8 @@ class ProcessData:
|
|||||||
record: bool = False
|
record: bool = False
|
||||||
uuid: str = None
|
uuid: str = None
|
||||||
session: int = 0
|
session: int = 0
|
||||||
frame: bytes = None
|
tube_diameter: float = 16.0
|
||||||
|
|
||||||
|
|
||||||
logger = get_task_logger(__name__)
|
logger = get_task_logger(__name__)
|
||||||
redisDB = Redis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=0, decode_responses=True)
|
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.cam = None
|
||||||
self.grbl = None
|
self.grbl = None
|
||||||
self.crop = None
|
self.crop = None
|
||||||
self.multiwel = None
|
#self.multiwel = None
|
||||||
self.conf = None
|
self.conf = None
|
||||||
self.record_queue = Queue()
|
self.record_queue = Queue()
|
||||||
self.data = ProcessData()
|
self.data = ProcessData()
|
||||||
@@ -187,9 +187,9 @@ class ScannerProcess(Task):
|
|||||||
height=self.video_height,
|
height=self.video_height,
|
||||||
jpeg_quality=self.video_quality,
|
jpeg_quality=self.video_quality,
|
||||||
use_tracking=self.use_tracking,
|
use_tracking=self.use_tracking,
|
||||||
px_per_mm = self.conf.px_per_mm,
|
|
||||||
display=self._display,
|
display=self._display,
|
||||||
video_lists=[],
|
video_lists=[],
|
||||||
|
parent=self,
|
||||||
)
|
)
|
||||||
'''
|
'''
|
||||||
settings.MEDIA_ROOT / 'simulation' / 'part1-5fps.mp4',
|
settings.MEDIA_ROOT / 'simulation' / 'part1-5fps.mp4',
|
||||||
@@ -209,8 +209,8 @@ class ScannerProcess(Task):
|
|||||||
height=self.video_height,
|
height=self.video_height,
|
||||||
jpeg_quality=self.video_quality,
|
jpeg_quality=self.video_quality,
|
||||||
use_tracking=self.use_tracking,
|
use_tracking=self.use_tracking,
|
||||||
px_per_mm = self.conf.px_per_mm,
|
|
||||||
display=self._display,
|
display=self._display,
|
||||||
|
parent=self,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
from modules.picamera2_capture import PiCamera2Capture
|
from modules.picamera2_capture import PiCamera2Capture
|
||||||
@@ -220,8 +220,8 @@ class ScannerProcess(Task):
|
|||||||
height=self.video_height,
|
height=self.video_height,
|
||||||
jpeg_quality=self.video_quality,
|
jpeg_quality=self.video_quality,
|
||||||
use_tracking=self.use_tracking,
|
use_tracking=self.use_tracking,
|
||||||
px_per_mm = self.conf.px_per_mm,
|
|
||||||
display=self._display,
|
display=self._display,
|
||||||
|
parent=self,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.cam.set_frame_callback(self._on_frame)
|
self.cam.set_frame_callback(self._on_frame)
|
||||||
@@ -263,12 +263,9 @@ class ScannerProcess(Task):
|
|||||||
self._send(**msg)
|
self._send(**msg)
|
||||||
|
|
||||||
def _on_frame(self, jpeg_bytes: bytes, ts: datetime, metrics: dict) -> None:
|
def _on_frame(self, jpeg_bytes: bytes, ts: datetime, metrics: dict) -> None:
|
||||||
self.data.frame = jpeg_bytes
|
|
||||||
if self.data.record:
|
if self.data.record:
|
||||||
# record images
|
|
||||||
self.record_queue.put((self.data.uuid, ts, jpeg_bytes, metrics))
|
self.record_queue.put((self.data.uuid, ts, jpeg_bytes, metrics))
|
||||||
if self.data.play:
|
if self.data.play:
|
||||||
# play image
|
|
||||||
self._send(ts=ts.timestamp(), jpeg=base64.b64encode(jpeg_bytes).decode(), **metrics)
|
self._send(ts=ts.timestamp(), jpeg=base64.b64encode(jpeg_bytes).decode(), **metrics)
|
||||||
|
|
||||||
def _recording(self):
|
def _recording(self):
|
||||||
@@ -399,37 +396,32 @@ class ScannerProcess(Task):
|
|||||||
|
|
||||||
elif topic == 'goto_xy':
|
elif topic == 'goto_xy':
|
||||||
self.grbl.move_to(self.manager.xbase, self.manager.ybase, feed=self.manager.feed)
|
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':
|
elif topic == 'xy_base':
|
||||||
self.manager.set_position()
|
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':
|
elif topic == 'test':
|
||||||
pass
|
self.manager.scan_test()
|
||||||
#self.manager.scan_test()
|
|
||||||
continue
|
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':
|
elif topic == 'center':
|
||||||
#self.manager.scan_test()
|
|
||||||
dx_mm = self.cam._last_detection["offset_x_mm"]
|
dx_mm = self.cam._last_detection["offset_x_mm"]
|
||||||
dy_mm = self.cam._last_detection["offset_y_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)
|
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})"
|
msg = f"Correction CNC move_relative(dx={dx_mm:.3f}, dy={dy_mm:.3f})"
|
||||||
self._send(state='center', msg=msg)
|
self._send(state='center', msg=msg)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elif topic == 'halt':
|
elif topic == 'halt':
|
||||||
self.manager.halt_scanning()
|
self.manager.halt_scanning()
|
||||||
continue
|
|
||||||
|
|
||||||
elif topic == 'calib_debug':
|
elif topic == 'calib_debug':
|
||||||
msg = self.manager.calib_toggle_debug()
|
msg = self.manager.calib_toggle_debug()
|
||||||
@@ -439,22 +431,18 @@ class ScannerProcess(Task):
|
|||||||
elif topic == 'previous':
|
elif topic == 'previous':
|
||||||
msg = self.manager.previous_well()
|
msg = self.manager.previous_well()
|
||||||
self._send(**msg)
|
self._send(**msg)
|
||||||
continue
|
|
||||||
|
|
||||||
elif topic == 'next':
|
elif topic == 'next':
|
||||||
msg = self.manager.next_well()
|
msg = self.manager.next_well()
|
||||||
self._send(**msg)
|
self._send(**msg)
|
||||||
continue
|
|
||||||
|
|
||||||
elif topic == 'goto':
|
elif topic == 'goto':
|
||||||
msg = self.manager.goto_well(int(value))
|
msg = self.manager.goto_well(int(value))
|
||||||
self._send(**msg)
|
self._send(**msg)
|
||||||
continue
|
|
||||||
|
|
||||||
elif topic == 'set_well':
|
elif topic == 'set_well':
|
||||||
msg = self.manager.set_well_position()
|
msg = self.manager.set_well_position()
|
||||||
self._send(**msg)
|
self._send(**msg)
|
||||||
continue
|
|
||||||
|
|
||||||
self._send(
|
self._send(
|
||||||
xbase=self.manager.xbase,
|
xbase=self.manager.xbase,
|
||||||
@@ -466,8 +454,8 @@ class ScannerProcess(Task):
|
|||||||
dx=self.manager.dx,
|
dx=self.manager.dx,
|
||||||
dy=self.manager.dy,
|
dy=self.manager.dy,
|
||||||
buttons=buttons,
|
buttons=buttons,
|
||||||
|
current=self.manager.get_well_order(),
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f'scanner listen_to_redis: {e}')
|
logger.error(f'scanner listen_to_redis: {e}')
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -1,88 +1,76 @@
|
|||||||
|
|
||||||
class ScannerManager {
|
class ScannerManager {
|
||||||
|
|
||||||
constructor(container) {
|
constructor(container, options = {}) {
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.socket = null;
|
this.socket = null;
|
||||||
this.axes = 0;
|
this.debug_count = 0;
|
||||||
this.cropping = 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() {
|
init_controls() {
|
||||||
this.ts = sId("_ts");
|
this.up.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "up" }); });
|
||||||
this.cx = sId("_cx");
|
this.down.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "down" }); });
|
||||||
this.cy = sId("_cy");
|
this.left.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "left" }); });
|
||||||
this.speed_px_s = sId("_speed_px_s");
|
this.right.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "right" }); });
|
||||||
this.axial_speed = sId("_axial_speed");
|
|
||||||
this.axial_pos = sId("_axial_pos");
|
|
||||||
this.area_px = sId("_area_px");
|
|
||||||
this.frame_count = sId("_count");
|
|
||||||
|
|
||||||
const goto_0 = sId("_goto-0");
|
this.goto_0.addEventListener('click', (e) => { this.clear_buttons(); this._send({ type: 'calibrate', topic: "goto_0" }); });
|
||||||
const goto_xy = sId("_goto-xy");
|
this.goto_xy.addEventListener('click', (e) => { this.clear_buttons(); this._send({ type: 'calibrate', topic: "goto_xy" }); });
|
||||||
const xy_base = sId("_xy-base");
|
this.xy_base.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "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");
|
this.calib_debug.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "calib_debug" }); });
|
||||||
const halt = sId("_halt");
|
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" }); });
|
||||||
|
|
||||||
const calib_debug = sId("_calib_debug");
|
this.median.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "median" }); });
|
||||||
const calib_center = sId("_calib_center");
|
this.crop.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "crop" }); });
|
||||||
const previous = sId("_previous");
|
this.crop_radius.addEventListener('change',(e) => { this._send({ type: 'calibrate', topic: "crop_radius", value: this.crop_radius.value }); });
|
||||||
const next = sId("_next");
|
|
||||||
const set_well = sId("_set_well");
|
|
||||||
|
|
||||||
const median = sId("_median");
|
|
||||||
const crop = sId("_crop");
|
|
||||||
const crop_radius = sId("_crop_radius");
|
|
||||||
|
|
||||||
|
|
||||||
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.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.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.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.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.test.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "test" }); });
|
||||||
this.dy.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "dy", value: e.target.value }); });
|
this.calib_center.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "center" }); });
|
||||||
|
this.calib_auto.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "auto" }); });
|
||||||
test.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "test" }); });
|
this.halt.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "halt" }); });
|
||||||
calib_center.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "center" }); });
|
|
||||||
halt.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "halt" }); });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerSocket(socket) {
|
registerSocket(socket) {
|
||||||
@@ -94,7 +82,6 @@ class ScannerManager {
|
|||||||
try {
|
try {
|
||||||
if (payload.jpeg) { this.container.src = `data:image/jpeg;base64,${payload.jpeg}`; }
|
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.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.xy) { this.x.textContent=payload.x.toFixed(2); this.y.textContent=payload.y.toFixed(2); }
|
||||||
if (payload.state) { this.debug.insertAdjacentHTML('afterbegin', `<li>[ ${++this.debug_count} - ${payload.state} ]: ${payload.msg}</li>`); }
|
if (payload.state) { this.debug.insertAdjacentHTML('afterbegin', `<li>[ ${++this.debug_count} - ${payload.state} ]: ${payload.msg}</li>`); }
|
||||||
if (payload.ts) { this.ts.textContent = timestampToLocalISOString(payload.ts); }
|
if (payload.ts) { this.ts.textContent = timestampToLocalISOString(payload.ts); }
|
||||||
@@ -108,6 +95,12 @@ class ScannerManager {
|
|||||||
this.frame_count.textContent = payload.count;
|
this.frame_count.textContent = payload.count;
|
||||||
}
|
}
|
||||||
if (payload.buttons) { this.well_btn.innerHTML = payload.buttons; }
|
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); }
|
} 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'); }); }
|
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 }); }
|
goto_well(b) { this.clear_buttons(); b.classList.add('w3-green'); this._send({ type: 'calibrate', topic: "goto", value: b.value }); }
|
||||||
init() {
|
init() {
|
||||||
this.axes = 0;
|
|
||||||
this.cropping = 0;
|
|
||||||
this.clear_buttons();
|
this.clear_buttons();
|
||||||
this._send({
|
this._send({
|
||||||
type: 'calibrate',
|
type: 'calibrate',
|
||||||
|
|||||||
@@ -1,40 +1,34 @@
|
|||||||
|
|
||||||
class ScannerManager {
|
class ScannerManager {
|
||||||
|
|
||||||
constructor(container) {
|
constructor(container, options = {}) {
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.socket = null;
|
this.socket = null;
|
||||||
this.axes = 0;
|
this.debug_count = 0;
|
||||||
this.cropping = 1;
|
|
||||||
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() {
|
init_controls() {
|
||||||
this.cx = sId("_cx");
|
this.median.addEventListener('click',(e) => { this._send({ type: 'calibrate', topic: "median" }); });
|
||||||
this.cy = sId("_cy");
|
this.crop.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "crop" }); });
|
||||||
this.speed_px_s = sId("_speed_px_s");
|
this.scan_bt.addEventListener('click', (e) => { this.scan(); });
|
||||||
this.axial_speed = sId("_axial_speed");
|
this.halt_bt.addEventListener('click', (e) => { this.halt(); });
|
||||||
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(); });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerSocket(socket) {
|
registerSocket(socket) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="container w3-black">
|
<div class="container w3-black">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="w3-row w3-row-padding">
|
<div class="w3-row w3-row-padding">
|
||||||
<div class="w3-col" style="width:15%">
|
<div class="w3-col" style="width:20%">
|
||||||
{% trans 'Position multi-puit' %}<br>
|
{% trans 'Position multi-puit' %}<br>
|
||||||
<select id="_well" class="w3-select">
|
<select id="_well" class="w3-select">
|
||||||
{% for w in wells %}
|
{% for w in wells %}
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w3-col" style="width:15%">
|
<div class="w3-col" style="width:10%">
|
||||||
<div>{% trans 'Pas' %}</div>
|
<div>{% trans 'Pas' %}</div>
|
||||||
<select id="_step" class="w3-select">
|
<select id="_step" class="w3-select">
|
||||||
<option value="0.1">0.1 mm</option>
|
<option value="0.1">0.1 mm</option>
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
<option value="50.0">50.0 mm</option>
|
<option value="50.0">50.0 mm</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-col" style="width:15%">
|
<div class="w3-col" style="width:10%">
|
||||||
<div>{% trans 'Durée' %}</div>
|
<div>{% trans 'Durée' %}</div>
|
||||||
<select id="_duration" class="w3-select" title="{% trans 'Durée entre vidéos' %}">
|
<select id="_duration" class="w3-select" title="{% trans 'Durée entre vidéos' %}">
|
||||||
<option value="1.0">1 s</option>
|
<option value="1.0">1 s</option>
|
||||||
@@ -59,10 +59,13 @@
|
|||||||
<option value="4.0">4 s</option>
|
<option value="4.0">4 s</option>
|
||||||
<option value="5.0" selected>5 s</option>
|
<option value="5.0" selected>5 s</option>
|
||||||
<option value="10">10 s</option>
|
<option value="10">10 s</option>
|
||||||
|
<option value="15">15 s</option>
|
||||||
|
<option value="20">20 s</option>
|
||||||
|
<option value="30">30 s</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-col" style="width:40%">
|
<div class="w3-col" style="width:45%">
|
||||||
<div class="w3-margin-top w3-padding w3-right">
|
<div class="w3-margin-top w3-padding w3-right-align">
|
||||||
<span id="_ts"></span><br>
|
<span id="_ts"></span><br>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,77 +73,105 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="move w3-center">
|
<div class="move w3-center">
|
||||||
|
<div class="w3-row w3-border w3-bold w3-margin-bottom">
|
||||||
|
<div class="w3-half">X<br><span id="_x"></span></div>
|
||||||
|
<div class="w3-half">Y<br><span id="_y"></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="w3-row">
|
||||||
|
<div class="w3-col">
|
||||||
|
<button id="_xy-base" class="w3-button w3-dark-xlight w3-round-large w3-margin-small w3-block"
|
||||||
|
title="{% trans 'Définir la base (x, y) du multi-puit' %}">
|
||||||
|
{% trans 'Définir base' %}<br><i class="fa-solid fa-circle-check w3-text-orange w3-xxlarge"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="w3-col">
|
||||||
|
<button id="_right" class="w3-button w3-dark-xlight w3-round-large w3-padding-small">
|
||||||
|
+X<br><i class="fa-solid fa-circle-up w3-xxlarge"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="w3-third">
|
||||||
|
<button id="_down" class="w3-button w3-dark-xlight w3-round-large w3-padding-small">
|
||||||
|
-Y<br><i class="fa-solid fa-circle-arrow-left w3-xxlarge"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="w3-third">
|
||||||
|
<button id="_set_well" class="w3-button w3-dark-xlight w3-round-large w3-padding-small"
|
||||||
|
title="{% trans 'Sauvegarder la position du puit courant' %}">
|
||||||
|
<span class="w3-small">Position</span><br><i class="fa-solid fa-circle-check w3-xxlarge w3-text-orange"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="w3-third">
|
||||||
|
<button id="_up" class="w3-button w3-dark-xlight w3-round-large w3-padding-small">
|
||||||
|
+Y<br><i class="fa-solid fa-circle-arrow-right w3-xxlarge"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="w3-col">
|
||||||
|
<button id="_left" class="w3-button w3-dark-xlight w3-round-large w3-padding-small">
|
||||||
|
-X<br><i class="fa-solid fa-circle-down w3-xxlarge"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="w3-row">
|
<div class="w3-row">
|
||||||
<div class="w3-half">{% trans 'dx' %}<br><input id="_dx" type="number" min="15.0" max="25.0" step="0.01" value=""/></div>
|
<div id="_well_btn" class="w3-col w3-margin-top w3-margin-bottom"></div>
|
||||||
<div class="w3-half">{% trans 'dy' %}<br><input id="_dy" type="number" min="15.0" max="25.0" step="0.01" value=""/></div>
|
|
||||||
<div class="w3-col">
|
|
||||||
<button id="_xy-step" class="w3-button w3-warning w3-round-large w3-block w3-large">
|
|
||||||
<i class="fa-solid fa-left-right"></i> {% trans 'Définir (dx, dy)' %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div><button id="_up" class="w3-button w3-dark-xlight w3-round-large w3-margin-small w3-block">↑ +Y {% trans 'A Droite' %}</button></div>
|
<div class="w3-third">
|
||||||
<div><button id="_left" class="w3-button w3-dark-xlight w3-round-large w3-margin-small w3-block">← -X {% trans 'En bas' %}</button></div>
|
<button id="_previous" class="w3-button w3-dark-xlight w3-round-large w3-padding-small">
|
||||||
<div><button id="_right" class="w3-button w3-dark-xlight w3-round-large w3-margin-small w3-block">→ +X {% trans 'En haut' %}</button></div>
|
<span class="w3-small">{% trans 'Précéd.' %}</span><br><i class="fa-solid fa-circle-left w3-xxlarge"></i>
|
||||||
<div><button id="_down" class="w3-button w3-dark-xlight w3-round-large w3-margin-small w3-block">↓ -Y {% trans 'A Gauche' %}</button></div>
|
|
||||||
|
|
||||||
<div class="w3-row">
|
|
||||||
<div id="_well_btn" class="w3-col"></div>
|
|
||||||
<div class="w3-half">
|
|
||||||
<button id="_previous" class="w3-button w3-light-blue w3-round-large w3-block" title="{% trans 'Précédent' %}">
|
|
||||||
<i class="fa-solid fa-circle-left w3-large"></i>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-half">
|
<div class="w3-third">
|
||||||
<button id="_next" class="w3-button w3-light-blue w3-round-large w3-block" title="{% trans 'Suivant' %}">
|
<button id="_calib_center" class="w3-button w3-dark-xlight w3-round-large w3-padding-small" title="{% trans 'Centrer manuellement' %}">
|
||||||
<i class="fa-solid fa-circle-right w3-large"></i>
|
<span class="w3-small">{% trans 'Centrer' %}</span><br><i class="fa-regular fa-circle w3-xxlarge w3-text-orange"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="w3-third">
|
||||||
|
<button id="_next" class="w3-button w3-dark-xlight w3-round-large w3-padding-small">
|
||||||
|
<span class="w3-small">{% trans 'Suivant' %}</span><br><i class="fa-solid fa-circle-right w3-xxlarge"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-col">
|
<div class="w3-col">
|
||||||
<button id="_set_well" class="w3-button w3-warning w3-round-large w3-block">
|
<button id="_calib_auto" class="w3-button w3-dark-xlight w3-round-large w3-margin-small w3-block"
|
||||||
<i class="fa-solid fa-circle-check"></i> {% trans 'Définir position' %}
|
title="{% trans 'Centrage auto et sauvegarde des positions' %}">
|
||||||
</button>
|
{% trans 'Calibrage auto' %}<br><i class="fa-solid fa-robot w3-text-orange w3-xxlarge"></i>
|
||||||
</div>
|
|
||||||
<div class="w3-col">
|
|
||||||
<button id="_calib_center" class="w3-button w3-warning w3-round-large w3-block">
|
|
||||||
<i class="fa-regular fa-circle"></i> {% trans 'Centrer manuel' %}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="scan w3-center">
|
<div class="scan w3-center">
|
||||||
|
<button id="_goto-0" class="w3-button w3-dark-xlight w3-round w3-round-large w3-margin-small w3-block">
|
||||||
|
Origine (0, 0)<br><i class="fa-solid fa-house w3-text-amber w3-xlarge"></i>
|
||||||
|
</button>
|
||||||
|
<button id="_goto-xy" class="w3-button w3-dark-xlight w3-round-large w3-margin-small w3-block w3-margin-bottom">
|
||||||
|
{% trans 'Aller à la base' %}<br>(<span id="_xbase"></span>, <span id="_ybase"></span>)<br><i class="fa-solid fa-bullseye w3-text-amber w3-xlarge"></i>
|
||||||
|
</button>
|
||||||
<div class="w3-row">
|
<div class="w3-row">
|
||||||
<div class="w3-half">X<br><span id="_x"></span></div>
|
<div class="w3-half">
|
||||||
<div class="w3-half">Y<br><span id="_y"></span></div>
|
<button id="_calib_debug" class="w3-button w3-dark-xlight w3-round-large w3-padding-small">
|
||||||
|
{% trans 'Debug' %}<br><i class="fa-solid fa-triangle-exclamation w3-text-amber w3-xlarge"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="_goto-0" class="w3-button w3-light-blue w3-round w3-round-large w3-margin-small w3-block">Origine (0, 0)</button>
|
<div class="w3-half">
|
||||||
<button id="_goto-xy" class="w3-button w3-light-blue w3-round-large w3-margin-small w3-block w3-margin-bottom">
|
<button id="_median" class="w3-button w3-dark-xlight w3-round-large w3-padding-small">
|
||||||
{% trans 'Aller à la base' %}<br>(<span id="_xbase"></span>, <span id="_ybase"></span>)
|
{% trans 'Axes' %} <br><i class="fa-solid fa-crosshairs w3-text-amber w3-xlarge"></i>
|
||||||
</button>
|
</button>
|
||||||
<button id="_xy-base" class="w3-button w3-warning w3-round-large w3-margin-small w3-block">
|
</div>
|
||||||
<i class="fa-solid fa-circle-check"></i> {% trans 'Définir base' %}
|
<div class="w3-col">
|
||||||
|
<button id="_crop" class="w3-button w3-dark-xlight w3-round-large w3-padding-small w3-margin-top w3-block">
|
||||||
|
{% trans 'Recadrer' %}<br><i class="fa-solid fa-crop w3-text-amber w3-xlarge"></i>
|
||||||
</button>
|
</button>
|
||||||
<hr>
|
</div>
|
||||||
<button id="_calib_debug" class="w3-button w3-teal w3-round-large w3-margin-small w3-block">
|
<div class="w3-col w3-padding-small w3-border w3-round-large w3-margin-top">
|
||||||
<i class="fa-solid fa-triangle-exclamation"></i> {% trans 'Debug' %}
|
<span class="w3-bold w3-text-amber">{% trans 'Rayon' %}</span><br>
|
||||||
</button>
|
<input id="_crop_radius" type="number" min="100" max="1200" step="1" class="w3-round" title="{% trans 'Rayon du recadrement' %}"
|
||||||
<button id="_median" class="w3-button w3-teal w3-round-large w3-margin-small w3-block">
|
value="{{ conf.calibration_crop_radius }}" title="{% trans 'Rayon de cadrage' %}"> px
|
||||||
<i class="fa-solid fa-crosshairs"></i> {% trans 'Axes' %}
|
</div>
|
||||||
</button>
|
</div>
|
||||||
<button id="_crop" class="w3-button w3-teal w3-round-large w3-margin-small w3-block w3-margin-bottom">
|
<button id="_test" class="w3-button w3-dark-xlight w3-round-large w3-margin-small w3-block">
|
||||||
<i class="fa-solid fa-crop"></i> {% trans 'Recadrer' %}
|
{% trans 'Tester le cirduit' %}<br><i class="fa-solid fa-circle-check w3-text-orange w3-xxlarge"></i>
|
||||||
</button>
|
|
||||||
<span>
|
|
||||||
{% trans 'Rayon' %}: <input id="_crop_radius" type="number" min="100" max="1200" step="1" value="{{ conf.calibration_crop_radius }}" title="{% trans 'Rayon de cadrage' %}"/>
|
|
||||||
</span>
|
|
||||||
<hr>
|
|
||||||
<button id="_test" class="w3-button w3-warning w3-round-large w3-margin-small w3-block">
|
|
||||||
<i class="fa-solid fa-circle-check"></i> {% trans 'Tester le cirduit' %}
|
|
||||||
</button>
|
</button>
|
||||||
<button id="_halt" class="w3-button w3-red w3-round-large w3-margin-small w3-block">
|
<button id="_halt" class="w3-button w3-red w3-round-large w3-margin-small w3-block">
|
||||||
<i class="fa-solid fa-hand"></i> {% trans 'ARRET' %}
|
{% trans 'ARRET' %}<br><i class="fa-solid fa-hand w3-xxlarge"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% include 'scanner/scan-image.html' %}
|
{% include 'scanner/scan-image.html' %}
|
||||||
@@ -154,10 +185,49 @@
|
|||||||
const container = sId("scan-img");
|
const container = sId("scan-img");
|
||||||
const ws_route = "{{ ws_route }}";
|
const ws_route = "{{ ws_route }}";
|
||||||
const use_tracking = "{{ use_tracking }}" == "True";
|
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")
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/scanner/js/calibration.js"></script>
|
<script src="/static/scanner/js/calibration.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const manager = new ScannerManager(container);
|
const manager = new ScannerManager(container, options);
|
||||||
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
||||||
const wsUrl = `${protocol}://${location.host}/${ws_route}`;
|
const wsUrl = `${protocol}://${location.host}/${ws_route}`;
|
||||||
const socket = new MetadataSocket(wsUrl);
|
const socket = new MetadataSocket(wsUrl);
|
||||||
|
|||||||
@@ -47,19 +47,34 @@
|
|||||||
const container = sId("scan-img");
|
const container = sId("scan-img");
|
||||||
const ws_route = "{{ ws_route }}";
|
const ws_route = "{{ ws_route }}";
|
||||||
const use_tracking = "{{ use_tracking }}" == "True";
|
const use_tracking = "{{ use_tracking }}" == "True";
|
||||||
|
const options = {
|
||||||
|
ts: sId("_ts"),
|
||||||
|
cx: sId("_cx"),
|
||||||
|
cy: sId("_cy"),
|
||||||
|
x : sId("_x"),
|
||||||
|
y : sId("_y"),
|
||||||
|
session: sId("_session"),
|
||||||
|
scan : sId("_scan"),
|
||||||
|
halt : sId("_halt"),
|
||||||
|
debug : sId("_debug"),
|
||||||
|
median : sId("_median"),
|
||||||
|
crop : sId("_crop"),
|
||||||
|
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")
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/scanner/js/main.js"></script>
|
<script src="/static/scanner/js/main.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// ---- Point d'entrée ----
|
const manager = new ScannerManager(container, options);
|
||||||
(async () => {
|
|
||||||
const manager = new ScannerManager(container);
|
|
||||||
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
||||||
const wsUrl = `${protocol}://${location.host}/${ws_route}`;
|
const wsUrl = `${protocol}://${location.host}/${ws_route}`;
|
||||||
const socket = new MetadataSocket(wsUrl);
|
const socket = new MetadataSocket(wsUrl);
|
||||||
socket.setManager(manager);
|
socket.setManager(manager);
|
||||||
socket.connect();
|
socket.connect();
|
||||||
manager.registerSocket(socket);
|
manager.registerSocket(socket);
|
||||||
})();
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user