From cb10957fa6e354f86b8bc5afa689e1ef8241608b Mon Sep 17 00:00:00 2001 From: denis defolie Date: Sat, 16 May 2026 12:20:25 +0200 Subject: [PATCH] metrics --- .../home/locale/en/LC_MESSAGES/django.mo | Bin 47829 -> 50306 bytes .../home/locale/en/LC_MESSAGES/django.po | 540 +++++++---- .../home/locale/fr/LC_MESSAGES/django.po | 531 +++++++---- .../modules/capture_interface.py | 21 +- .../modules/planarian_metrics.py | 147 ++- .../modules/planarian_metrics2.py | 891 ++++++++++++++++++ test_tube_scanner/planarian/views.py | 23 +- test_tube_scanner/scanner/models.py | 7 +- test_tube_scanner/scanner/multiwell.py | 3 + test_tube_scanner/scanner/process.py | 34 +- 10 files changed, 1759 insertions(+), 438 deletions(-) create mode 100644 test_tube_scanner/modules/planarian_metrics2.py diff --git a/test_tube_scanner/home/locale/en/LC_MESSAGES/django.mo b/test_tube_scanner/home/locale/en/LC_MESSAGES/django.mo index bdcb4b4b476d22f8079a4fd7b781927a0c1c2288..631a16d1b14849dc2c571b8d64eaae214b7f6401 100644 GIT binary patch delta 16632 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 X+n@JcsRhN0?7MM%dEtX6CI$U3OOYjA delta 14417 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

9}&n#n4xiLar~$`{r< zsFnEbS3UC~65Opl)1%dSI3!uK5Avx=hUhA4gd!;_i5gy8rJrm3y|)*j15WBSB$oO8SzHbdAO&(qNNy z))Q;ik+j)M{0VUz%)NMFoLb~R#CurAR_Om%x{droe3A44@eK4uF7xkyMU_b3Q<#^^ z;VSr_ONUHXByCdh719clzU7llXU=uvC&Uj?SEw!5zR$`j;ZJ>AHky2Z{{Ff~P>+X> z$E~DiG}uoXL#(Tty`e8{bgd=5mJ{(UR~@N4by2py1-?jGUE4+^lm84`BX6XWMjL

&bCk@Y?U(p2 z=`G4GkcJR%we1L8G%zH+~l@uD#I3MW?fnJv%+{<%7u2#Cte^GF^oXIX}1X^K)CCGykO&9H!toX(;J^@@?s$K~8giJK`S7Ymi2g zqDfmwNtBnS-PgoNNV*DRa}zq%iC?qjUs!eCa<94E>zZnF{c$>#-`EBNsa#CiSkeUI zGnB_;0?tEyl6Cbb{uG;GQR=D@Kf)rUsia`i22x&9dD{F;{fi`fxtu{1ensVvq;ZsP z#67vyxR`t=+GOEm>`hs5;`5~c5TC-|&<8J|u1Tcw4WNBa>?rVQyJ@kk2wTk8+f59D*N`qb;XLuziz*I+}+|G>Jqfx6kmwTTB1 z``CL3bFM&swy^iAz(u`QC+FJE&+n*gk2^>+NlVDzCS}qv4WlX36-DYy+CkY{Se<(P zQ+o*cW~82^DgS!-+tlS=<0zj<8cS-W|NqSx8V@F;zg+$B5(ZK>4c{d7q3l)EA0@iF zlB$yy&_>rsq%7L>B;{WEJ3`lP+va_2V~YOg=YQvK{tD`4t1sId4`2t%R+4@r{u~>T z{CQY9`Co`%v2B-OQ&KtV8k0hZACUAnK{nRJWRk8qq|yrh^?YTWEK&y=>pE-5`T2`2 zd&jE6OyVd~ZQ?3C@MBwdBIgeN!9spBDU7<@Ya;n8q!3%?8bA<2tsjND%Ho@(W26M~ zMM-UFQ;qyE(gNZ^*b9r{c+v~RtFRF2dV|zidD2zlew44ro1~hQt-~NvMDG0SP?&0O zzClG38x#Kbx=0&cJxSxKpRP8fU!famsJ+L_wwp)UF5o%#6ty4aOG(zj2Na1{y z{Ofv7Jd^SaI&EifI%-`;-CwqC9DYlARnh_CBs+*q;&{sDsE)D5nl&2pDa4IPp*CO3+L3!lkxEckk5rA=wU3}VK?Bl4;uIPeBmNWfkiSLB zz3%4**p4=vF`HC?HnT`y5$l>yT`BTMP1fm0oR6ey5p}w1Vtvj3Fq!LA`ciP3I25nh z#&vAtcPNh`ZcS=Je1$ZGxG^S@(n&{1L6pVgK#~th*Jrk`1;odRAK~w+;A-qu(F7?} zl;GyIn4k0)N!JLxg-fUl_RLNE)XTG~UMnBZ`G%XkJQEwY@^R;B_Jb#=c`+Za%I?@! zW!-aI)%64>F9~$-?>Nj|xl@FDZKqK8)J{!3e|K8pX@hQ?6H?UJIDRz<^FoY5KpIx$$sw7rn$VvxU;A4_Dq`b zdw^&Eyb)f3t;Y`-G(2O(I8V%isa~E%3tM}6ZZGolahJ@x;t5^0!`uDnrJ9}>SM&&Q zf4MF_Z~DmLqlRUS%NP*dZ=8G7`Uaj0>&yDMy\n" "Language-Team: LANGUAGE \n" @@ -33,6 +33,7 @@ msgstr "Procedure, screen organization and available commands" #: home/templates/docs/doc_calibration.html:29 #: home/templates/docs/doc_database.html:29 #: home/templates/docs/doc_experiment.html:30 +#: home/templates/docs/doc_medias.html:33 #: home/templates/docs/doc_results.html:32 #: home/templates/docs/doc_scanning.html:31 msgid "Navigation" @@ -80,7 +81,7 @@ msgstr "Prerequisite" #: home/templates/docs/doc_database.html:363 #: home/templates/docs/doc_database.html:366 #: home/templates/docs/doc_database.html:367 -#: scanner/templates/scanner/base.html:132 scanner/views.py:133 +#: scanner/templates/scanner/base.html:132 scanner/views.py:134 msgid "Calibration" msgstr "Calibration" @@ -229,6 +230,8 @@ msgstr "Generated positions" #: home/templates/docs/doc_calibration.html:217 #: home/templates/docs/doc_calibration.html:260 +#: home/templates/docs/doc_medias.html:125 +#: home/templates/docs/doc_medias.html:237 #: home/templates/docs/doc_results.html:81 #: home/templates/docs/doc_scanning.html:105 msgid "ATTENTION" @@ -437,29 +440,29 @@ msgstr "Planarian Scanner — Storage infrastructure and data models" msgid "Authentification" msgstr "Authentication" -#: home/templates/docs/doc_database.html:46 scanner/models.py:90 +#: home/templates/docs/doc_database.html:46 scanner/models.py:91 msgid "Configuration" msgstr "Configuration" #: home/templates/docs/doc_database.html:49 -#: planarian/templates/planarian/export_csv.html:37 scanner/models.py:103 +#: planarian/templates/planarian/export_csv.html:37 scanner/models.py:104 msgid "Puits" msgstr "Wells" -#: home/templates/docs/doc_database.html:52 scanner/models.py:169 -#: scanner/models.py:170 scanner/models.py:180 scanner/models.py:243 +#: home/templates/docs/doc_database.html:52 scanner/models.py:170 +#: scanner/models.py:171 scanner/models.py:181 scanner/models.py:244 msgid "Multi-puits" msgstr "Multi-well" -#: home/templates/docs/doc_database.html:55 scanner/models.py:132 +#: home/templates/docs/doc_database.html:55 scanner/models.py:133 msgid "Positions" msgstr "Positions" -#: home/templates/docs/doc_database.html:58 scanner/models.py:264 +#: home/templates/docs/doc_database.html:58 scanner/models.py:265 msgid "Expériences" msgstr "Experiences" -#: home/templates/docs/doc_database.html:61 scanner/models.py:312 +#: home/templates/docs/doc_database.html:61 scanner/models.py:313 msgid "Sessions" msgstr "Sessions" @@ -548,15 +551,15 @@ msgstr "Capture" msgid "Fichier vidéo" msgstr "Video file" -#: home/templates/docs/doc_database.html:225 scanner/models.py:60 +#: home/templates/docs/doc_database.html:225 scanner/models.py:61 msgid "Fréquence vidéos (fps)" msgstr "Frequency videos (fps)" -#: home/templates/docs/doc_database.html:226 scanner/models.py:61 +#: home/templates/docs/doc_database.html:226 scanner/models.py:62 msgid "Largeur de capture vidéo" msgstr "Video capture width" -#: home/templates/docs/doc_database.html:227 scanner/models.py:62 +#: home/templates/docs/doc_database.html:227 scanner/models.py:63 msgid "Hauteur de capture vidéo" msgstr "Video capture height" @@ -624,11 +627,12 @@ msgstr "HD --> BD +Y" msgid "Géométrie" msgstr "Geometry" -#: home/templates/docs/doc_database.html:309 scanner/models.py:118 +#: home/templates/docs/doc_database.html:309 scanner/models.py:119 msgid "Nombre de lignes" msgstr "Number of lines" -#: home/templates/docs/doc_database.html:310 scanner/models.py:117 +#: home/templates/docs/doc_database.html:310 +#: home/templates/docs/doc_medias.html:59 scanner/models.py:118 msgid "Nombre de colonnes" msgstr "Number of columns" @@ -652,11 +656,11 @@ msgstr "Reading Order" msgid "Durée de capture (calibration)" msgstr "Capture time (calibration)" -#: home/templates/docs/doc_database.html:326 scanner/models.py:125 +#: home/templates/docs/doc_database.html:326 scanner/models.py:126 msgid "Origine X" msgstr "Origin X" -#: home/templates/docs/doc_database.html:331 scanner/models.py:126 +#: home/templates/docs/doc_database.html:331 scanner/models.py:127 msgid "Origine Y" msgstr "Origin Y" @@ -697,7 +701,7 @@ msgid "Axe Y (mm)" msgstr "Y axis (mm)" #: home/templates/docs/doc_database.html:374 planarian/models.py:36 -#: scanner/models.py:185 +#: scanner/models.py:186 msgid "Pixels par mm" msgstr "Pixels per mm" @@ -726,7 +730,7 @@ msgstr "Multi-well linked" msgid "Durée de capture (secondes)" msgstr "Catch time (seconds)" -#: home/templates/docs/doc_database.html:400 scanner/models.py:240 +#: home/templates/docs/doc_database.html:400 scanner/models.py:241 msgid "Identifiant d'expérience" msgstr "Experience ID" @@ -812,8 +816,10 @@ msgstr "Order of establishment" #: home/templates/docs/doc_experiment.html:33 #: home/templates/docs/doc_experiment.html:125 -#: planarian/templates/planarian/base.html:14 scanner/models.py:311 -#: scanner/models.py:380 scanner/templates/scanner/images.html:13 +#: planarian/templates/planarian/base.html:14 scanner/models.py:312 +#: scanner/models.py:381 scanner/templates/scanner/export_medias.html:9 +#: scanner/templates/scanner/export_medias.html:35 +#: scanner/templates/scanner/images.html:13 #: scanner/templates/scanner/replay.html:14 msgid "Session" msgstr "Session" @@ -827,8 +833,8 @@ msgstr "Data" #: home/templates/docs/doc_experiment.html:36 #: home/templates/docs/doc_experiment.html:195 -#: planarian/templates/planarian/export_csv.html:33 scanner/models.py:263 -#: scanner/models.py:381 scanner/models.py:411 +#: planarian/templates/planarian/export_csv.html:33 scanner/models.py:264 +#: scanner/models.py:382 scanner/models.py:412 msgid "Expérience" msgstr "Experience" @@ -920,10 +926,11 @@ msgstr "Title" #: home/templates/docs/doc_experiment.html:140 #: home/templates/docs/doc_experiment.html:208 +#: scanner/templates/scanner/export_medias.html:31 msgid "Auteur" msgstr "Author" -#: home/templates/docs/doc_experiment.html:149 scanner/models.py:403 +#: home/templates/docs/doc_experiment.html:149 scanner/models.py:404 msgid "Expériences d'une session" msgstr "Experiences of a session" @@ -939,7 +946,7 @@ msgstr "Maximum 4" msgid "Options tâches périodiques" msgstr "Periodic Task Options" -#: home/templates/docs/doc_experiment.html:168 scanner/models.py:281 +#: home/templates/docs/doc_experiment.html:168 msgid "Date d'exportation" msgstr "Date of export" @@ -951,7 +958,7 @@ msgstr "" "Creates a delayed task to automatically export images and videos to the " "selected date." -#: home/templates/docs/doc_experiment.html:178 scanner/models.py:282 +#: home/templates/docs/doc_experiment.html:178 msgid "Date du balayage" msgstr "Date of scanning" @@ -960,7 +967,7 @@ msgid "" "Planifie le déclenchement du balayage de la session à une date ultérieure." msgstr "Plan to trigger the session scan at a later date." -#: home/templates/docs/doc_experiment.html:209 scanner/models.py:239 +#: home/templates/docs/doc_experiment.html:209 scanner/models.py:240 msgid "Commentaires" msgstr "Remarks" @@ -1094,12 +1101,16 @@ msgid "Importer des configurations depuis CSV" msgstr "Import configurations from CSV" #: home/templates/docs/doc_experiment.html:462 +#: home/templates/docs/doc_medias.html:36 +#: home/templates/docs/doc_medias.html:39 #: home/templates/docs/doc_results.html:34 #: home/templates/docs/doc_results.html:60 msgid "Menu gauche" msgstr "Left Menu" #: home/templates/docs/doc_experiment.html:464 +#: home/templates/docs/doc_medias.html:97 +#: home/templates/docs/doc_medias.html:164 #: home/templates/docs/doc_results.html:69 msgid "Sélectionner la session" msgstr "Select session" @@ -1149,6 +1160,158 @@ msgstr "Export a CSV template..." msgid "Vérifier l'emplacement du fichier téléchargé (dossier Download)." msgstr "Check the location of the downloaded file (Download folder)." +#: home/templates/docs/doc_medias.html:25 +msgid "Gestionnaire de médias" +msgstr "Media managers" + +#: home/templates/docs/doc_medias.html:26 +msgid "Consultation, rediffusion et export des images et vidéos" +msgstr "Consultation, rebroadcast and export of images and video" + +#: home/templates/docs/doc_medias.html:34 +#: home/templates/docs/doc_medias.html:52 +msgid "Grille d'affichage" +msgstr "Display grid" + +#: home/templates/docs/doc_medias.html:35 +#: home/templates/docs/doc_medias.html:86 +#: scanner/templates/scanner/base.html:146 scanner/views.py:188 +msgid "Gestionnaire d'images" +msgstr "Image Manager" + +#: home/templates/docs/doc_medias.html:37 +msgid "Affichage & export" +msgstr "Display & export" + +#: home/templates/docs/doc_medias.html:38 +msgid "Rediffusion vidéos" +msgstr "Video replay" + +#: home/templates/docs/doc_medias.html:40 +msgid "Lecteur & export" +msgstr "Reader & export" + +#: home/templates/docs/doc_medias.html:51 +msgid "Affichage" +msgstr "Display" + +#: home/templates/docs/doc_medias.html:53 +msgid "Contrôle situé en haut à gauche de l'interface." +msgstr "Control located at the top left of the interface." + +#: home/templates/docs/doc_medias.html:60 +msgid "" +"Réduire ou augmenter dynamiquement le nombre de colonnes d'affichage des " +"médias." +msgstr "Dynamically reduce or increase the number of media display columns" + +#: home/templates/docs/doc_medias.html:85 +#: home/templates/docs/doc_scanning.html:111 +msgid "Images" +msgstr "Images" + +#: home/templates/docs/doc_medias.html:91 +#: home/templates/docs/doc_medias.html:158 +msgid "Menu gauche — Sélection" +msgstr "" + +#: home/templates/docs/doc_medias.html:98 +#: home/templates/docs/doc_medias.html:165 +msgid "Cocher l'expérience" +msgstr "Check the desired experience" + +#: home/templates/docs/doc_medias.html:99 +#: home/templates/docs/doc_medias.html:166 +#: home/templates/docs/doc_results.html:145 +msgid "Choisir le puit" +msgstr "Choose the well" + +#: home/templates/docs/doc_medias.html:108 +msgid "Affichage des images & export" +msgstr "Viewing logs at the bottom of the page" + +#: home/templates/docs/doc_medias.html:114 +msgid "Téléchargement unitaire" +msgstr "Download" + +#: home/templates/docs/doc_medias.html:115 +msgid "Chaque image affichée dispose d'un bouton de téléchargement individuel." +msgstr "Each image displayed has an individual download button." + +#: home/templates/docs/doc_medias.html:124 +msgid "Exporter l'ensemble des images" +msgstr "Export all videos" + +#: home/templates/docs/doc_medias.html:130 +#: home/templates/docs/doc_medias.html:242 +#: home/templates/docs/doc_results.html:168 +msgid "La procédure peut être longue." +msgstr "The procedure can be lengthy." + +#: home/templates/docs/doc_medias.html:134 +#: home/templates/docs/doc_medias.html:246 +msgid "Préférer l'export différé." +msgstr "Prefer deferred export." + +#: home/templates/docs/doc_medias.html:139 +#: home/templates/docs/doc_medias.html:251 +msgid "Export différé recommandé" +msgstr "Delayed export recommended" + +#: home/templates/docs/doc_medias.html:152 +msgid "Vidéos" +msgstr "Live video" + +#: home/templates/docs/doc_medias.html:153 +msgid "Rediffusion de vidéos" +msgstr "Video replay" + +#: home/templates/docs/doc_medias.html:175 +msgid "Lecteur vidéo & export" +msgstr "Video player & export" + +#: home/templates/docs/doc_medias.html:181 +msgid "Contrôles de lecture" +msgstr "Reading controls" + +#: home/templates/docs/doc_medias.html:186 +msgid "Marche" +msgstr "On" + +#: home/templates/docs/doc_medias.html:190 +msgid "Pause" +msgstr "Pause" + +#: home/templates/docs/doc_medias.html:194 +#: home/templates/docs/doc_scanning.html:35 +msgid "Arrêt" +msgstr "Stop" + +#: home/templates/docs/doc_medias.html:201 +msgid "Slider vitesse de lecture" +msgstr "Reading speed slider" + +#: home/templates/docs/doc_medias.html:205 +msgid "Slider timeline (position dans la vidéo)" +msgstr "Slider timeline (position in the video)" + +#: home/templates/docs/doc_medias.html:215 +msgid "Téléchargements" +msgstr "Download" + +#: home/templates/docs/doc_medias.html:219 +msgid "Télécharger l'image courante" +msgstr "Download current image" + +#: home/templates/docs/doc_medias.html:223 +msgid "Télécharger la vidéo complète" +msgstr "Download full video" + +#: home/templates/docs/doc_medias.html:236 +#: scanner/templates/scanner/replay.html:29 +msgid "Exporter l'ensemble des vidéos" +msgstr "Export all videos" + #: home/templates/docs/doc_results.html:24 msgid "Exploiter les résultats" msgstr "Exploiting results" @@ -1232,10 +1395,6 @@ msgstr "Right side — Download client" msgid "Paramètres" msgstr "Parameters" -#: home/templates/docs/doc_results.html:145 -msgid "Choisir le puit" -msgstr "Choose the well" - #: home/templates/docs/doc_results.html:149 msgid "Choisir l'index du planaire" msgstr "Choose the planar index" @@ -1256,10 +1415,6 @@ msgstr "Generate Button" msgid "Télécharge le fichier CSV directement sur le poste client." msgstr "Download the CSV file directly to the client station." -#: home/templates/docs/doc_results.html:168 -msgid "La procédure peut être longue." -msgstr "The procedure can be lengthy." - #: home/templates/docs/doc_scanning.html:23 #: home/templates/docs/doc_scanning.html:49 #: scanner/templates/scanner/base.html:50 @@ -1271,14 +1426,10 @@ msgid "Lancement, suivi et procédure en cas d'arrêt" msgstr "Launch, follow-up and procedure in case of arrest" #: home/templates/docs/doc_scanning.html:34 -#: home/templates/docs/doc_scanning.html:80 scanner/models.py:298 +#: home/templates/docs/doc_scanning.html:80 scanner/models.py:299 msgid "Lancer le balayage" msgstr "Run Scan" -#: home/templates/docs/doc_scanning.html:35 -msgid "Arrêt" -msgstr "Judgment" - #: home/templates/docs/doc_scanning.html:36 msgid "Procédure après arrêt" msgstr "Post-stop procedure" @@ -1316,10 +1467,6 @@ msgstr "Step by step from all the wells of the session." msgid "Les logs s'affichent en bas de l'écran en temps réel." msgstr "Logs are displayed at the bottom of the screen in real time." -#: home/templates/docs/doc_scanning.html:111 -msgid "Images" -msgstr "Images" - #: home/templates/docs/doc_scanning.html:112 msgid "Les images capturées jusqu'à l'arrêt seront enregistrées." msgstr "The images captured until the stop will be recorded." @@ -1445,8 +1592,8 @@ msgstr "Export a CSV template from these configurations" msgid "Identifiant expérience" msgstr "Identifier experience" -#: planarian/models.py:24 scanner/models.py:102 scanner/models.py:179 -#: scanner/models.py:412 +#: planarian/models.py:24 scanner/models.py:103 scanner/models.py:180 +#: scanner/models.py:413 msgid "Puit" msgstr "Fish" @@ -1463,12 +1610,12 @@ msgstr "Description" msgid "Créé le" msgstr "Created on" -#: planarian/models.py:30 scanner/models.py:133 scanner/models.py:280 -#: scanner/models.py:315 scanner/models.py:413 +#: planarian/models.py:30 scanner/models.py:134 scanner/models.py:281 +#: scanner/models.py:316 scanner/models.py:414 msgid "Active" msgstr "Active" -#: planarian/models.py:37 scanner/models.py:185 +#: planarian/models.py:37 scanner/models.py:186 msgid "Facteur de calibration optique" msgstr "Optical calibration factor" @@ -1504,7 +1651,7 @@ msgstr "Vertical" msgid "Horizontal" msgstr "Horizontal" -#: planarian/models.py:65 scanner/models.py:74 +#: planarian/models.py:65 scanner/models.py:75 msgid "Axe du tube" msgstr "Tube axis" @@ -1524,11 +1671,11 @@ msgstr "Well area ratio, e.g. 0.10 for 10%" msgid "Nombre de planaires" msgstr "Number of planners" -#: planarian/models.py:84 scanner/models.py:78 +#: planarian/models.py:84 scanner/models.py:79 msgid "Taille du kernel" msgstr "Size of the kernel" -#: planarian/models.py:85 scanner/models.py:78 +#: planarian/models.py:85 scanner/models.py:79 msgid "" "taille du kernel elliptique de fusion des fragments (px). Augmenter si " "fragments résiduels" @@ -1536,11 +1683,11 @@ msgstr "" "size of the elliptic fragment fusion kernel (px). Increase if residual " "fragments" -#: planarian/models.py:90 scanner/models.py:79 +#: planarian/models.py:90 scanner/models.py:80 msgid "Distance " msgstr "Distance " -#: planarian/models.py:91 scanner/models.py:79 +#: planarian/models.py:91 scanner/models.py:80 msgid "" "Distance min entre deux contours pour les considérer comme individus " "distincts. Défaut : 40px. Augmenter si IDs multiples persistent" @@ -1617,12 +1764,14 @@ msgid "Configurations des expériences" msgstr "Configurations of experiments" #: planarian/templates/planarian/base.html:10 +#: scanner/templates/scanner/export_medias.html:5 #: scanner/templates/scanner/images.html:9 #: scanner/templates/scanner/replay.html:10 msgid "Retour accueil" msgstr "Back home" #: planarian/templates/planarian/base.html:13 +#: scanner/templates/scanner/export_medias.html:8 #: scanner/templates/scanner/images.html:12 msgid "Ensemble d'expériences" msgstr "Set of experiments" @@ -1704,6 +1853,7 @@ msgstr "Generate and download CSV" #: planarian/templates/planarian/export_csv.html:95 #: planarian/templates/planarian/import_params.html:77 +#: scanner/templates/scanner/export_medias.html:50 msgid "Annuler" msgstr "Cancel" @@ -1733,6 +1883,9 @@ msgstr "Immobile / Mobile / Highly mobile" #: planarian/templates/planarian/export_csv.html:159 #: planarian/templates/planarian/import_params.html:268 +#: scanner/templates/scanner/export_medias.html:56 +#: scanner/templates/scanner/images.html:56 +#: scanner/templates/scanner/replay.html:72 msgid "Choisir une session" msgstr "Select session" @@ -1895,34 +2048,34 @@ msgstr "File ready to import" msgid "Puis une expérience." msgstr "Then an experiment." -#: planarian/views.py:140 +#: planarian/views.py:137 #, python-format msgid "Erreur lors de l'export CSV: %(error)s" msgstr "Error exporting CSV: %(error)s" -#: planarian/views.py:157 +#: planarian/views.py:154 msgid "Export vers un fichier CSV depuis ReductStore" msgstr "Export to a CSV file from ReduceStore" -#: planarian/views.py:172 +#: planarian/views.py:169 msgid "Aucune donnée trouvée." msgstr "No data found." -#: planarian/views.py:209 +#: planarian/views.py:206 #, python-format msgid "Import terminé : %(c)d créés, %(u)d mis à jour, %(e)d erreurs." msgstr "Import completed: %(c)d created, %(u)d updated, %(e)d errors." -#: planarian/views.py:246 +#: planarian/views.py:243 #, python-format msgid "Colonnes manquantes : %(cols)s" msgstr "Missing columns: %(cols)s" -#: planarian/views.py:253 +#: planarian/views.py:250 msgid "Importer des configurations depuis un fichier CSV" msgstr "Import configurations from a CSV file" -#: planarian/views.py:295 +#: planarian/views.py:292 msgid "Métriques de tracking d'un planaire" msgstr "Tracking Metrics of a Planar" @@ -2051,255 +2204,256 @@ msgid "CNC Grbl Ymax en mm" msgstr "CNC Grbl Ymax in mm" #: scanner/models.py:56 -msgid "Type de capture" -msgstr "Type of catch" +msgid "" +"Type de capture. Nécessite un redémarrage en cas de modification à chaud!" +msgstr "Type of capture. Requires a reboot if hot-edited!" -#: scanner/models.py:57 +#: scanner/models.py:58 msgid "Index de la webcam" msgstr "Webcam Index" -#: scanner/models.py:57 +#: scanner/models.py:58 msgid "Index de la webcam (0, 1, ...) si présente" msgstr "Webcam index (0, 1, ...) if present" -#: scanner/models.py:58 +#: scanner/models.py:59 msgid "Qualité JPEG" msgstr "JPEG quality" -#: scanner/models.py:58 +#: scanner/models.py:59 msgid "Qualité JPEG (1-100) pour les images exportées" msgstr "JPEG quality (1-100) for exported images" -#: scanner/models.py:59 +#: scanner/models.py:60 msgid "Qualité JPEG pour les vidéos" msgstr "JPEG quality for videos" -#: scanner/models.py:59 +#: scanner/models.py:60 msgid "Qualité JPEG (1-100) pour les images extraites des vidéos" msgstr "JPEG quality (1-100) for images extracted from videos" -#: scanner/models.py:60 +#: scanner/models.py:61 msgid "Fréquence d'extraction des images des vidéos (images par seconde)" msgstr "Video image extraction frequency (images per second)" -#: scanner/models.py:61 +#: scanner/models.py:62 msgid "Largeur de capture vidéo en pixels" msgstr "Video capture width in pixels" -#: scanner/models.py:62 +#: scanner/models.py:63 msgid "Hauteur de capture vidéo en pixels" msgstr "Video capture height in pixels" -#: scanner/models.py:64 +#: scanner/models.py:65 msgid "Simuler balayage" msgstr "Simulate Scanning" -#: scanner/models.py:64 +#: scanner/models.py:65 msgid "Autorise la simulation du balayage" msgstr "Allows scanning simulation" -#: scanner/models.py:65 +#: scanner/models.py:66 msgid "Rayon de découpe pour la calibration" msgstr "Cutting radius for calibration" -#: scanner/models.py:65 +#: scanner/models.py:66 msgid "Rayon en pixels pour découper les images de calibration en px" msgstr "Pixel radius for px calibration images" -#: scanner/models.py:66 +#: scanner/models.py:67 msgid "Multi-puits de calibration par défaut" msgstr "Default multi-scaling wells" -#: scanner/models.py:66 +#: scanner/models.py:67 msgid "Position du multi-puits de calibration par défaut" msgstr "Default multi-sink calibration position" -#: scanner/models.py:67 +#: scanner/models.py:68 msgid "Vitesse de calibration" msgstr "Calibration speed" -#: scanner/models.py:67 +#: scanner/models.py:68 msgid "Vitesse de déplacement pour la calibration en mm/mn" msgstr "Speed of movement for calibration in mm/min" -#: scanner/models.py:68 +#: scanner/models.py:69 msgid "Pas de calibration" msgstr "No calibration" -#: scanner/models.py:68 +#: scanner/models.py:69 msgid "Pas de déplacement pour la calibration en mm" msgstr "No displacement for calibration in mm" -#: scanner/models.py:69 +#: scanner/models.py:70 msgid "Duruée calibration" msgstr "Dured calibration" -#: scanner/models.py:69 +#: scanner/models.py:70 msgid "Durée de pose entre chaque puits en s" msgstr "Installation time between each well in s" -#: scanner/models.py:71 +#: scanner/models.py:72 msgid "Suivi" msgstr "Monitoring" -#: scanner/models.py:71 +#: scanner/models.py:72 msgid "Suivi et analyse des planaires" msgstr "Monitoring and analysis of planners" -#: scanner/models.py:72 +#: scanner/models.py:73 msgid "Réglage dans calibration" msgstr "Adjustment in calibration" -#: scanner/models.py:72 +#: scanner/models.py:73 msgid "Autorise le réglage des valeurs par défaut dans la calibration" msgstr "Allow default setting in calibration" -#: scanner/models.py:74 +#: scanner/models.py:75 msgid "Axe du puit" msgstr "Well axis" -#: scanner/models.py:75 +#: scanner/models.py:76 msgid "Surface minimale" msgstr "Minimum surface area" -#: scanner/models.py:75 +#: scanner/models.py:76 msgid "surface minimale d'un contour pour être considéré valide (px²)" msgstr "minimum surface of a contour to be considered valid (px2)" -#: scanner/models.py:76 +#: scanner/models.py:77 msgid "surface maximale " msgstr "maximum surface area" -#: scanner/models.py:76 +#: scanner/models.py:77 msgid "surface maximale d'un contour en fraction de la frame (défaut 10%)" msgstr "maximum surface of a frame fractional contour (default 10%)" -#: scanner/models.py:77 +#: scanner/models.py:78 msgid "Max planaire" msgstr "Max planar" -#: scanner/models.py:77 +#: scanner/models.py:78 msgid "nombre maximum de planaires à suivre simultanément (1-10)" msgstr "maximum number of planaries to follow simultaneously (1-10)" -#: scanner/models.py:81 +#: scanner/models.py:82 msgid "Actif" msgstr "Assets" -#: scanner/models.py:98 +#: scanner/models.py:99 msgid "Nom" msgstr "Name" -#: scanner/models.py:98 +#: scanner/models.py:99 msgid "Nom du puit: Ai..Di" msgstr "Name of well: Ai.. Di" -#: scanner/models.py:112 +#: scanner/models.py:113 msgid "Label" msgstr "Label" -#: scanner/models.py:112 +#: scanner/models.py:113 msgid "Label du multi-puit" msgstr "Multi-well label" -#: scanner/models.py:114 +#: scanner/models.py:115 msgid "Position" msgstr "Position" -#: scanner/models.py:114 +#: scanner/models.py:115 msgid "Position du multi-puits sur la table" msgstr "Multi-well position on the table" -#: scanner/models.py:115 +#: scanner/models.py:116 msgid "Par défaut" msgstr "Default" -#: scanner/models.py:115 +#: scanner/models.py:116 msgid "Multi-puit par défaut" msgstr "Default multi-sink" -#: scanner/models.py:117 +#: scanner/models.py:118 msgid "Colonnes" msgstr "Columns" -#: scanner/models.py:118 +#: scanner/models.py:119 msgid "Lignes" msgstr "Lines" -#: scanner/models.py:119 +#: scanner/models.py:120 msgid "Diamètre" msgstr "Diameter" -#: scanner/models.py:119 +#: scanner/models.py:120 msgid "Diamètre des tubes en mm" msgstr "Tube diameter in mm" -#: scanner/models.py:120 +#: scanner/models.py:121 msgid "Définition" msgstr "Definition" -#: scanner/models.py:120 +#: scanner/models.py:121 msgid "Définition des lignes" msgstr "Definition of lines" -#: scanner/models.py:121 +#: scanner/models.py:122 msgid "Ordre ligne" msgstr "Order line" -#: scanner/models.py:121 +#: scanner/models.py:122 msgid "Ordre ligne de puit. Lecture en serpentin dans le sens des +- X" msgstr "Well line order. Snake reading in the direction of +- X" -#: scanner/models.py:123 scanner/models.py:182 +#: scanner/models.py:124 scanner/models.py:183 msgid "Ordre" msgstr "Order" -#: scanner/models.py:123 +#: scanner/models.py:124 msgid "Ordre de lecture du multi-puit" msgstr "Multi-well reading order" -#: scanner/models.py:124 scanner/models.py:244 +#: scanner/models.py:125 scanner/models.py:245 #: scanner/templates/scanner/calibration.html:187 msgid "Durée" msgstr "Duration" -#: scanner/models.py:124 +#: scanner/models.py:125 msgid "Durée de capture en secondes pour la calibration" msgstr "Capture time in seconds for calibration" -#: scanner/models.py:125 +#: scanner/models.py:126 msgid "Base origine X en mm" msgstr "Base origin X in mm" -#: scanner/models.py:126 +#: scanner/models.py:127 msgid "Base origine Y en mm" msgstr "Base origin Y in mm" -#: scanner/models.py:128 +#: scanner/models.py:129 msgid "Pas X" msgstr "Not X" -#: scanner/models.py:128 +#: scanner/models.py:129 msgid "Pas ou interval sur X en mm" msgstr "Not or interval on X in mm" -#: scanner/models.py:129 +#: scanner/models.py:130 msgid "Pas Y" msgstr "Not Y" -#: scanner/models.py:129 +#: scanner/models.py:130 msgid "Pas ou interval sur Y en mm" msgstr "Not or interval on Y in mm" -#: scanner/models.py:130 scanner/templates/scanner/calibration.html:23 +#: scanner/models.py:131 scanner/templates/scanner/calibration.html:23 msgid "Vitesse" msgstr "Speed" -#: scanner/models.py:130 +#: scanner/models.py:131 msgid "Vitesse déplacement en mm/mn " msgstr "Movement speed in mm/min" -#: scanner/models.py:132 +#: scanner/models.py:133 msgid "" "Positions des puits générées ?. Non => efface WellPosition et recalcule les " "positions" @@ -2307,148 +2461,157 @@ msgstr "" "Positions of generated wells ?. No => erase WellPosition and recalculate " "positions" -#: scanner/models.py:182 +#: scanner/models.py:183 msgid "Ordre de lecture du puit" msgstr "Reading order of the well" -#: scanner/models.py:183 +#: scanner/models.py:184 msgid "X" msgstr "X" -#: scanner/models.py:183 +#: scanner/models.py:184 msgid "Axe X en mm" msgstr "X axis in mm" -#: scanner/models.py:184 +#: scanner/models.py:185 msgid "Y" msgstr "Y" -#: scanner/models.py:184 +#: scanner/models.py:185 msgid "Axe Y en mm" msgstr "Y axis in mm" -#: scanner/models.py:199 +#: scanner/models.py:200 msgid "Position du puit" msgstr "Well position" -#: scanner/models.py:200 +#: scanner/models.py:201 msgid "Position des puits" msgstr "Position of wells" -#: scanner/models.py:238 +#: scanner/models.py:239 msgid "Titre de l'expérience" msgstr "Title of experience" -#: scanner/models.py:239 +#: scanner/models.py:240 msgid "Descriptions de l'expérience" msgstr "Descriptions of experience" -#: scanner/models.py:244 +#: scanner/models.py:245 msgid "Durée de la prise de vue en secondes" msgstr "Time taken in seconds" -#: scanner/models.py:246 scanner/models.py:284 +#: scanner/models.py:247 scanner/models.py:285 +#: scanner/templates/scanner/export_medias.html:39 msgid "Date de création" msgstr "Date of establishment" -#: scanner/models.py:247 +#: scanner/models.py:248 msgid "Date de début" msgstr "Start date" -#: scanner/models.py:248 scanner/models.py:285 +#: scanner/models.py:249 scanner/models.py:286 msgid "Date de fin" msgstr "End date" -#: scanner/models.py:273 +#: scanner/models.py:274 msgid "En attente" msgstr "Pending" -#: scanner/models.py:274 +#: scanner/models.py:275 msgid "En cours" msgstr "Ongoing" -#: scanner/models.py:275 +#: scanner/models.py:276 msgid "Terminé" msgstr "Completed" -#: scanner/models.py:276 +#: scanner/models.py:277 msgid "Erreur" msgstr "Error" -#: scanner/models.py:278 +#: scanner/models.py:279 msgid "Nom de la session" msgstr "Name of session" -#: scanner/models.py:278 +#: scanner/models.py:279 msgid "Session d'expérience. 4 Multi-puits maximum" msgstr "Experience session. 4 Maximum multi-well" -#: scanner/models.py:281 +#: scanner/models.py:282 +msgid "Exportation auto" +msgstr "Export auto" + +#: scanner/models.py:282 msgid "Date d'exportation prévue" msgstr "Expected export date" -#: scanner/models.py:282 +#: scanner/models.py:283 +msgid "Bbalayage auto" +msgstr "Scanning auto" + +#: scanner/models.py:283 msgid "Date du balayage prévue" msgstr "Date of planned scan" -#: scanner/models.py:287 +#: scanner/models.py:288 msgid "Status exportation" msgstr "Export status" -#: scanner/models.py:290 +#: scanner/models.py:291 msgid "Export médias" msgstr "Export media" -#: scanner/models.py:291 +#: scanner/models.py:292 msgid "Programmation de l'exportation des vidéos et images" msgstr "Programming the export of videos and images" -#: scanner/models.py:293 +#: scanner/models.py:294 msgid "Exportation terminée à" msgstr "Export completed at" -#: scanner/models.py:295 +#: scanner/models.py:296 msgid "Status scanning" msgstr "Status scanning" -#: scanner/models.py:299 +#: scanner/models.py:300 msgid "Programmation du lancement du balayage" msgstr "Scanning Launch Programming" -#: scanner/models.py:301 +#: scanner/models.py:302 msgid "Balayage terminé à" msgstr "Sweeping completed at" -#: scanner/models.py:315 +#: scanner/models.py:316 msgid "Terminée" msgstr "Completed" -#: scanner/models.py:402 +#: scanner/models.py:403 msgid "Expérience d'une session" msgstr "Experience of a session" -#: scanner/models.py:426 +#: scanner/models.py:427 msgid "Expérience puit" msgstr "Experience well" -#: scanner/models.py:427 +#: scanner/models.py:428 msgid "Expériences puits" msgstr "Well experiments" -#: scanner/process.py:381 +#: scanner/process.py:397 msgid "La session est nulle!..." msgstr "The session sucks!..." -#: scanner/process.py:388 +#: scanner/process.py:404 msgid "Balayage démarré..." msgstr "Scanning started..." -#: scanner/process.py:391 +#: scanner/process.py:407 msgid "Erreur lors du démarrage du balayage..." msgstr "Error starting scanning..." #: scanner/tasks.py:21 -msgid "Balayage démarré." +msgid "Scanner démarré." msgstr "Scanning started..." #: scanner/tasks.py:27 @@ -2553,7 +2716,7 @@ msgstr "Creation of experience sessions" msgid "Gestions des expériences" msgstr "Experience management" -#: scanner/templates/scanner/base.html:137 scanner/views.py:123 +#: scanner/templates/scanner/base.html:137 scanner/views.py:124 msgid "Balayage multi-puits" msgstr "Multi-well sweeping" @@ -2565,14 +2728,14 @@ msgstr "Results" msgid "Export CSV des expériences" msgstr "CSV export of experiments" -#: scanner/templates/scanner/base.html:146 scanner/views.py:187 -msgid "Gestionnaire d'images" -msgstr "Image Manager" - #: scanner/templates/scanner/base.html:149 msgid "Redifusion vidéos" msgstr "Redifusion videos" +#: scanner/templates/scanner/base.html:152 +msgid "Exporter les fichiers multimédias" +msgstr "Export media files" + #: scanner/templates/scanner/calibration.html:15 msgid "Position multi-puit" msgstr "Multi-sink position" @@ -2728,28 +2891,40 @@ msgstr "Test for circumference" msgid "ARRET" msgstr "JUDGMENT" +#: scanner/templates/scanner/export_medias.html:21 +msgid "L'explortation des fichiers multimédia peut durer plusieurs heures." +msgstr "" + +#: scanner/templates/scanner/export_medias.html:22 +msgid "Préférer l'exportation différée." +msgstr "Prefer deferred export." + +#: scanner/templates/scanner/export_medias.html:42 +msgid "Status" +msgstr "Status" + +#: scanner/templates/scanner/export_medias.html:47 +msgid "Exporter" +msgstr "Import" + #: scanner/templates/scanner/images.html:27 msgid "Exporter l'ensemble des images de la session" msgstr "Export All Images for that session" -#: scanner/templates/scanner/images.html:45 +#: scanner/templates/scanner/images.html:46 msgid "Télécharger" msgstr "Download" -#: scanner/templates/scanner/images.html:65 +#: scanner/templates/scanner/images.html:71 msgid "Confirmer l'export des images." msgstr "Export must be confirmed." -#: scanner/templates/scanner/images.html:66 +#: scanner/templates/scanner/images.html:72 msgid "ATTENTION le processus peut être long!" msgstr "BEWARE The procedure can be lengthy." -#: scanner/templates/scanner/replay.html:29 -msgid "Exporter l'ensemble des vidéos" -msgstr "Export all videos" - -#: scanner/templates/scanner/replay.html:78 -msgid "Confirmer l'export des vidéos. ATTENTION le processus peut être long!." +#: scanner/templates/scanner/replay.html:84 +msgid "Confirmer l'export des vidéos. ATTENTION le processus peut être long!" msgstr "Confirm videos exportation. BEWARE the procedure can be lengthy." #: scanner/templates/scanner/scanning.html:14 @@ -2773,23 +2948,27 @@ msgstr "scanning" msgid "Simuler" msgstr "Simulate" -#: scanner/views.py:111 +#: scanner/views.py:112 msgid "Documentation" msgstr "Documentation" -#: scanner/views.py:222 scanner/views.py:273 +#: scanner/views.py:221 +msgid "Exporter les fichiers médias" +msgstr "Export media" + +#: scanner/views.py:252 scanner/views.py:303 msgid "Erreur d'exportation" msgstr "Export error" -#: scanner/views.py:246 +#: scanner/views.py:276 msgid "Gestionnaire de vidéos" msgstr "Video Manager" -#: scanner/views.py:268 +#: scanner/views.py:298 msgid "Images téléchargées" msgstr "Downloaded images" -#: scanner/views.py:271 +#: scanner/views.py:301 msgid "Vidéos téléchargées" msgstr "Videos downloaded" @@ -2800,3 +2979,6 @@ msgstr "Django website admin" #: templates/admin/base_site.html:10 msgid "Django administration" msgstr "Django administration" + +#~ msgid "Type de capture" +#~ msgstr "Type of catch" diff --git a/test_tube_scanner/home/locale/fr/LC_MESSAGES/django.po b/test_tube_scanner/home/locale/fr/LC_MESSAGES/django.po index 16af20c..4bfa288 100644 --- a/test_tube_scanner/home/locale/fr/LC_MESSAGES/django.po +++ b/test_tube_scanner/home/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-15 15:08+0200\n" +"POT-Creation-Date: 2026-05-16 09:14+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -33,6 +33,7 @@ msgstr "" #: home/templates/docs/doc_calibration.html:29 #: home/templates/docs/doc_database.html:29 #: home/templates/docs/doc_experiment.html:30 +#: home/templates/docs/doc_medias.html:33 #: home/templates/docs/doc_results.html:32 #: home/templates/docs/doc_scanning.html:31 msgid "Navigation" @@ -80,7 +81,7 @@ msgstr "" #: home/templates/docs/doc_database.html:363 #: home/templates/docs/doc_database.html:366 #: home/templates/docs/doc_database.html:367 -#: scanner/templates/scanner/base.html:132 scanner/views.py:133 +#: scanner/templates/scanner/base.html:132 scanner/views.py:134 msgid "Calibration" msgstr "" @@ -227,6 +228,8 @@ msgstr "" #: home/templates/docs/doc_calibration.html:217 #: home/templates/docs/doc_calibration.html:260 +#: home/templates/docs/doc_medias.html:125 +#: home/templates/docs/doc_medias.html:237 #: home/templates/docs/doc_results.html:81 #: home/templates/docs/doc_scanning.html:105 msgid "ATTENTION" @@ -433,29 +436,29 @@ msgstr "" msgid "Authentification" msgstr "" -#: home/templates/docs/doc_database.html:46 scanner/models.py:90 +#: home/templates/docs/doc_database.html:46 scanner/models.py:91 msgid "Configuration" msgstr "" #: home/templates/docs/doc_database.html:49 -#: planarian/templates/planarian/export_csv.html:37 scanner/models.py:103 +#: planarian/templates/planarian/export_csv.html:37 scanner/models.py:104 msgid "Puits" msgstr "" -#: home/templates/docs/doc_database.html:52 scanner/models.py:169 -#: scanner/models.py:170 scanner/models.py:180 scanner/models.py:243 +#: home/templates/docs/doc_database.html:52 scanner/models.py:170 +#: scanner/models.py:171 scanner/models.py:181 scanner/models.py:244 msgid "Multi-puits" msgstr "" -#: home/templates/docs/doc_database.html:55 scanner/models.py:132 +#: home/templates/docs/doc_database.html:55 scanner/models.py:133 msgid "Positions" msgstr "" -#: home/templates/docs/doc_database.html:58 scanner/models.py:264 +#: home/templates/docs/doc_database.html:58 scanner/models.py:265 msgid "Expériences" msgstr "" -#: home/templates/docs/doc_database.html:61 scanner/models.py:312 +#: home/templates/docs/doc_database.html:61 scanner/models.py:313 msgid "Sessions" msgstr "" @@ -544,15 +547,15 @@ msgstr "" msgid "Fichier vidéo" msgstr "" -#: home/templates/docs/doc_database.html:225 scanner/models.py:60 +#: home/templates/docs/doc_database.html:225 scanner/models.py:61 msgid "Fréquence vidéos (fps)" msgstr "" -#: home/templates/docs/doc_database.html:226 scanner/models.py:61 +#: home/templates/docs/doc_database.html:226 scanner/models.py:62 msgid "Largeur de capture vidéo" msgstr "" -#: home/templates/docs/doc_database.html:227 scanner/models.py:62 +#: home/templates/docs/doc_database.html:227 scanner/models.py:63 msgid "Hauteur de capture vidéo" msgstr "" @@ -620,11 +623,12 @@ msgstr "" msgid "Géométrie" msgstr "" -#: home/templates/docs/doc_database.html:309 scanner/models.py:118 +#: home/templates/docs/doc_database.html:309 scanner/models.py:119 msgid "Nombre de lignes" msgstr "" -#: home/templates/docs/doc_database.html:310 scanner/models.py:117 +#: home/templates/docs/doc_database.html:310 +#: home/templates/docs/doc_medias.html:59 scanner/models.py:118 msgid "Nombre de colonnes" msgstr "" @@ -648,11 +652,11 @@ msgstr "" msgid "Durée de capture (calibration)" msgstr "" -#: home/templates/docs/doc_database.html:326 scanner/models.py:125 +#: home/templates/docs/doc_database.html:326 scanner/models.py:126 msgid "Origine X" msgstr "" -#: home/templates/docs/doc_database.html:331 scanner/models.py:126 +#: home/templates/docs/doc_database.html:331 scanner/models.py:127 msgid "Origine Y" msgstr "" @@ -693,7 +697,7 @@ msgid "Axe Y (mm)" msgstr "" #: home/templates/docs/doc_database.html:374 planarian/models.py:36 -#: scanner/models.py:185 +#: scanner/models.py:186 msgid "Pixels par mm" msgstr "" @@ -722,7 +726,7 @@ msgstr "" msgid "Durée de capture (secondes)" msgstr "" -#: home/templates/docs/doc_database.html:400 scanner/models.py:240 +#: home/templates/docs/doc_database.html:400 scanner/models.py:241 msgid "Identifiant d'expérience" msgstr "" @@ -805,8 +809,10 @@ msgstr "" #: home/templates/docs/doc_experiment.html:33 #: home/templates/docs/doc_experiment.html:125 -#: planarian/templates/planarian/base.html:14 scanner/models.py:311 -#: scanner/models.py:380 scanner/templates/scanner/images.html:13 +#: planarian/templates/planarian/base.html:14 scanner/models.py:312 +#: scanner/models.py:381 scanner/templates/scanner/export_medias.html:9 +#: scanner/templates/scanner/export_medias.html:35 +#: scanner/templates/scanner/images.html:13 #: scanner/templates/scanner/replay.html:14 msgid "Session" msgstr "" @@ -820,8 +826,8 @@ msgstr "" #: home/templates/docs/doc_experiment.html:36 #: home/templates/docs/doc_experiment.html:195 -#: planarian/templates/planarian/export_csv.html:33 scanner/models.py:263 -#: scanner/models.py:381 scanner/models.py:411 +#: planarian/templates/planarian/export_csv.html:33 scanner/models.py:264 +#: scanner/models.py:382 scanner/models.py:412 msgid "Expérience" msgstr "" @@ -912,10 +918,11 @@ msgstr "" #: home/templates/docs/doc_experiment.html:140 #: home/templates/docs/doc_experiment.html:208 +#: scanner/templates/scanner/export_medias.html:31 msgid "Auteur" msgstr "" -#: home/templates/docs/doc_experiment.html:149 scanner/models.py:403 +#: home/templates/docs/doc_experiment.html:149 scanner/models.py:404 msgid "Expériences d'une session" msgstr "" @@ -931,7 +938,7 @@ msgstr "" msgid "Options tâches périodiques" msgstr "" -#: home/templates/docs/doc_experiment.html:168 scanner/models.py:281 +#: home/templates/docs/doc_experiment.html:168 msgid "Date d'exportation" msgstr "" @@ -941,7 +948,7 @@ msgid "" "la date choisie." msgstr "" -#: home/templates/docs/doc_experiment.html:178 scanner/models.py:282 +#: home/templates/docs/doc_experiment.html:178 msgid "Date du balayage" msgstr "" @@ -950,7 +957,7 @@ msgid "" "Planifie le déclenchement du balayage de la session à une date ultérieure." msgstr "" -#: home/templates/docs/doc_experiment.html:209 scanner/models.py:239 +#: home/templates/docs/doc_experiment.html:209 scanner/models.py:240 msgid "Commentaires" msgstr "" @@ -1084,12 +1091,16 @@ msgid "Importer des configurations depuis CSV" msgstr "" #: home/templates/docs/doc_experiment.html:462 +#: home/templates/docs/doc_medias.html:36 +#: home/templates/docs/doc_medias.html:39 #: home/templates/docs/doc_results.html:34 #: home/templates/docs/doc_results.html:60 msgid "Menu gauche" msgstr "" #: home/templates/docs/doc_experiment.html:464 +#: home/templates/docs/doc_medias.html:97 +#: home/templates/docs/doc_medias.html:164 #: home/templates/docs/doc_results.html:69 msgid "Sélectionner la session" msgstr "" @@ -1139,6 +1150,158 @@ msgstr "" msgid "Vérifier l'emplacement du fichier téléchargé (dossier Download)." msgstr "" +#: home/templates/docs/doc_medias.html:25 +msgid "Gestionnaire de médias" +msgstr "" + +#: home/templates/docs/doc_medias.html:26 +msgid "Consultation, rediffusion et export des images et vidéos" +msgstr "" + +#: home/templates/docs/doc_medias.html:34 +#: home/templates/docs/doc_medias.html:52 +msgid "Grille d'affichage" +msgstr "" + +#: home/templates/docs/doc_medias.html:35 +#: home/templates/docs/doc_medias.html:86 +#: scanner/templates/scanner/base.html:146 scanner/views.py:188 +msgid "Gestionnaire d'images" +msgstr "" + +#: home/templates/docs/doc_medias.html:37 +msgid "Affichage & export" +msgstr "" + +#: home/templates/docs/doc_medias.html:38 +msgid "Rediffusion vidéos" +msgstr "" + +#: home/templates/docs/doc_medias.html:40 +msgid "Lecteur & export" +msgstr "" + +#: home/templates/docs/doc_medias.html:51 +msgid "Affichage" +msgstr "" + +#: home/templates/docs/doc_medias.html:53 +msgid "Contrôle situé en haut à gauche de l'interface." +msgstr "" + +#: home/templates/docs/doc_medias.html:60 +msgid "" +"Réduire ou augmenter dynamiquement le nombre de colonnes d'affichage des " +"médias." +msgstr "" + +#: home/templates/docs/doc_medias.html:85 +#: home/templates/docs/doc_scanning.html:111 +msgid "Images" +msgstr "" + +#: home/templates/docs/doc_medias.html:91 +#: home/templates/docs/doc_medias.html:158 +msgid "Menu gauche — Sélection" +msgstr "" + +#: home/templates/docs/doc_medias.html:98 +#: home/templates/docs/doc_medias.html:165 +msgid "Cocher l'expérience" +msgstr "" + +#: home/templates/docs/doc_medias.html:99 +#: home/templates/docs/doc_medias.html:166 +#: home/templates/docs/doc_results.html:145 +msgid "Choisir le puit" +msgstr "" + +#: home/templates/docs/doc_medias.html:108 +msgid "Affichage des images & export" +msgstr "" + +#: home/templates/docs/doc_medias.html:114 +msgid "Téléchargement unitaire" +msgstr "" + +#: home/templates/docs/doc_medias.html:115 +msgid "Chaque image affichée dispose d'un bouton de téléchargement individuel." +msgstr "" + +#: home/templates/docs/doc_medias.html:124 +msgid "Exporter l'ensemble des images" +msgstr "" + +#: home/templates/docs/doc_medias.html:130 +#: home/templates/docs/doc_medias.html:242 +#: home/templates/docs/doc_results.html:168 +msgid "La procédure peut être longue." +msgstr "" + +#: home/templates/docs/doc_medias.html:134 +#: home/templates/docs/doc_medias.html:246 +msgid "Préférer l'export différé." +msgstr "" + +#: home/templates/docs/doc_medias.html:139 +#: home/templates/docs/doc_medias.html:251 +msgid "Export différé recommandé" +msgstr "" + +#: home/templates/docs/doc_medias.html:152 +msgid "Vidéos" +msgstr "" + +#: home/templates/docs/doc_medias.html:153 +msgid "Rediffusion de vidéos" +msgstr "" + +#: home/templates/docs/doc_medias.html:175 +msgid "Lecteur vidéo & export" +msgstr "" + +#: home/templates/docs/doc_medias.html:181 +msgid "Contrôles de lecture" +msgstr "" + +#: home/templates/docs/doc_medias.html:186 +msgid "Marche" +msgstr "" + +#: home/templates/docs/doc_medias.html:190 +msgid "Pause" +msgstr "" + +#: home/templates/docs/doc_medias.html:194 +#: home/templates/docs/doc_scanning.html:35 +msgid "Arrêt" +msgstr "" + +#: home/templates/docs/doc_medias.html:201 +msgid "Slider vitesse de lecture" +msgstr "" + +#: home/templates/docs/doc_medias.html:205 +msgid "Slider timeline (position dans la vidéo)" +msgstr "" + +#: home/templates/docs/doc_medias.html:215 +msgid "Téléchargements" +msgstr "" + +#: home/templates/docs/doc_medias.html:219 +msgid "Télécharger l'image courante" +msgstr "" + +#: home/templates/docs/doc_medias.html:223 +msgid "Télécharger la vidéo complète" +msgstr "" + +#: home/templates/docs/doc_medias.html:236 +#: scanner/templates/scanner/replay.html:29 +msgid "Exporter l'ensemble des vidéos" +msgstr "" + #: home/templates/docs/doc_results.html:24 msgid "Exploiter les résultats" msgstr "" @@ -1219,10 +1382,6 @@ msgstr "" msgid "Paramètres" msgstr "" -#: home/templates/docs/doc_results.html:145 -msgid "Choisir le puit" -msgstr "" - #: home/templates/docs/doc_results.html:149 msgid "Choisir l'index du planaire" msgstr "" @@ -1243,10 +1402,6 @@ msgstr "" msgid "Télécharge le fichier CSV directement sur le poste client." msgstr "" -#: home/templates/docs/doc_results.html:168 -msgid "La procédure peut être longue." -msgstr "" - #: home/templates/docs/doc_scanning.html:23 #: home/templates/docs/doc_scanning.html:49 #: scanner/templates/scanner/base.html:50 @@ -1258,14 +1413,10 @@ msgid "Lancement, suivi et procédure en cas d'arrêt" msgstr "" #: home/templates/docs/doc_scanning.html:34 -#: home/templates/docs/doc_scanning.html:80 scanner/models.py:298 +#: home/templates/docs/doc_scanning.html:80 scanner/models.py:299 msgid "Lancer le balayage" msgstr "" -#: home/templates/docs/doc_scanning.html:35 -msgid "Arrêt" -msgstr "" - #: home/templates/docs/doc_scanning.html:36 msgid "Procédure après arrêt" msgstr "" @@ -1303,10 +1454,6 @@ msgstr "" msgid "Les logs s'affichent en bas de l'écran en temps réel." msgstr "" -#: home/templates/docs/doc_scanning.html:111 -msgid "Images" -msgstr "" - #: home/templates/docs/doc_scanning.html:112 msgid "Les images capturées jusqu'à l'arrêt seront enregistrées." msgstr "" @@ -1428,8 +1575,8 @@ msgstr "" msgid "Identifiant expérience" msgstr "" -#: planarian/models.py:24 scanner/models.py:102 scanner/models.py:179 -#: scanner/models.py:412 +#: planarian/models.py:24 scanner/models.py:103 scanner/models.py:180 +#: scanner/models.py:413 msgid "Puit" msgstr "" @@ -1446,12 +1593,12 @@ msgstr "" msgid "Créé le" msgstr "" -#: planarian/models.py:30 scanner/models.py:133 scanner/models.py:280 -#: scanner/models.py:315 scanner/models.py:413 +#: planarian/models.py:30 scanner/models.py:134 scanner/models.py:281 +#: scanner/models.py:316 scanner/models.py:414 msgid "Active" msgstr "" -#: planarian/models.py:37 scanner/models.py:185 +#: planarian/models.py:37 scanner/models.py:186 msgid "Facteur de calibration optique" msgstr "" @@ -1487,7 +1634,7 @@ msgstr "" msgid "Horizontal" msgstr "" -#: planarian/models.py:65 scanner/models.py:74 +#: planarian/models.py:65 scanner/models.py:75 msgid "Axe du tube" msgstr "" @@ -1507,21 +1654,21 @@ msgstr "" msgid "Nombre de planaires" msgstr "" -#: planarian/models.py:84 scanner/models.py:78 +#: planarian/models.py:84 scanner/models.py:79 msgid "Taille du kernel" msgstr "" -#: planarian/models.py:85 scanner/models.py:78 +#: planarian/models.py:85 scanner/models.py:79 msgid "" "taille du kernel elliptique de fusion des fragments (px). Augmenter si " "fragments résiduels" msgstr "" -#: planarian/models.py:90 scanner/models.py:79 +#: planarian/models.py:90 scanner/models.py:80 msgid "Distance " msgstr "" -#: planarian/models.py:91 scanner/models.py:79 +#: planarian/models.py:91 scanner/models.py:80 msgid "" "Distance min entre deux contours pour les considérer comme individus " "distincts. Défaut : 40px. Augmenter si IDs multiples persistent" @@ -1596,12 +1743,14 @@ msgid "Configurations des expériences" msgstr "" #: planarian/templates/planarian/base.html:10 +#: scanner/templates/scanner/export_medias.html:5 #: scanner/templates/scanner/images.html:9 #: scanner/templates/scanner/replay.html:10 msgid "Retour accueil" msgstr "" #: planarian/templates/planarian/base.html:13 +#: scanner/templates/scanner/export_medias.html:8 #: scanner/templates/scanner/images.html:12 msgid "Ensemble d'expériences" msgstr "" @@ -1678,6 +1827,7 @@ msgstr "" #: planarian/templates/planarian/export_csv.html:95 #: planarian/templates/planarian/import_params.html:77 +#: scanner/templates/scanner/export_medias.html:50 msgid "Annuler" msgstr "" @@ -1707,6 +1857,9 @@ msgstr "" #: planarian/templates/planarian/export_csv.html:159 #: planarian/templates/planarian/import_params.html:268 +#: scanner/templates/scanner/export_medias.html:56 +#: scanner/templates/scanner/images.html:56 +#: scanner/templates/scanner/replay.html:72 msgid "Choisir une session" msgstr "" @@ -1867,34 +2020,34 @@ msgstr "" msgid "Puis une expérience." msgstr "" -#: planarian/views.py:140 +#: planarian/views.py:137 #, python-format msgid "Erreur lors de l'export CSV: %(error)s" msgstr "" -#: planarian/views.py:157 +#: planarian/views.py:154 msgid "Export vers un fichier CSV depuis ReductStore" msgstr "" -#: planarian/views.py:172 +#: planarian/views.py:169 msgid "Aucune donnée trouvée." msgstr "" -#: planarian/views.py:209 +#: planarian/views.py:206 #, python-format msgid "Import terminé : %(c)d créés, %(u)d mis à jour, %(e)d erreurs." msgstr "" -#: planarian/views.py:246 +#: planarian/views.py:243 #, python-format msgid "Colonnes manquantes : %(cols)s" msgstr "" -#: planarian/views.py:253 +#: planarian/views.py:250 msgid "Importer des configurations depuis un fichier CSV" msgstr "" -#: planarian/views.py:295 +#: planarian/views.py:292 msgid "Métriques de tracking d'un planaire" msgstr "" @@ -2023,397 +2176,407 @@ msgid "CNC Grbl Ymax en mm" msgstr "" #: scanner/models.py:56 -msgid "Type de capture" +msgid "" +"Type de capture. Nécessite un redémarrage en cas de modification à chaud!" msgstr "" -#: scanner/models.py:57 +#: scanner/models.py:58 msgid "Index de la webcam" msgstr "" -#: scanner/models.py:57 +#: scanner/models.py:58 msgid "Index de la webcam (0, 1, ...) si présente" msgstr "" -#: scanner/models.py:58 +#: scanner/models.py:59 msgid "Qualité JPEG" msgstr "" -#: scanner/models.py:58 +#: scanner/models.py:59 msgid "Qualité JPEG (1-100) pour les images exportées" msgstr "" -#: scanner/models.py:59 +#: scanner/models.py:60 msgid "Qualité JPEG pour les vidéos" msgstr "" -#: scanner/models.py:59 +#: scanner/models.py:60 msgid "Qualité JPEG (1-100) pour les images extraites des vidéos" msgstr "" -#: scanner/models.py:60 +#: scanner/models.py:61 msgid "Fréquence d'extraction des images des vidéos (images par seconde)" msgstr "" -#: scanner/models.py:61 +#: scanner/models.py:62 msgid "Largeur de capture vidéo en pixels" msgstr "" -#: scanner/models.py:62 +#: scanner/models.py:63 msgid "Hauteur de capture vidéo en pixels" msgstr "" -#: scanner/models.py:64 +#: scanner/models.py:65 msgid "Simuler balayage" msgstr "" -#: scanner/models.py:64 +#: scanner/models.py:65 msgid "Autorise la simulation du balayage" msgstr "" -#: scanner/models.py:65 +#: scanner/models.py:66 msgid "Rayon de découpe pour la calibration" msgstr "" -#: scanner/models.py:65 +#: scanner/models.py:66 msgid "Rayon en pixels pour découper les images de calibration en px" msgstr "" -#: scanner/models.py:66 +#: scanner/models.py:67 msgid "Multi-puits de calibration par défaut" msgstr "" -#: scanner/models.py:66 +#: scanner/models.py:67 msgid "Position du multi-puits de calibration par défaut" msgstr "" -#: scanner/models.py:67 +#: scanner/models.py:68 msgid "Vitesse de calibration" msgstr "" -#: scanner/models.py:67 +#: scanner/models.py:68 msgid "Vitesse de déplacement pour la calibration en mm/mn" msgstr "" -#: scanner/models.py:68 +#: scanner/models.py:69 msgid "Pas de calibration" msgstr "" -#: scanner/models.py:68 +#: scanner/models.py:69 msgid "Pas de déplacement pour la calibration en mm" msgstr "" -#: scanner/models.py:69 +#: scanner/models.py:70 msgid "Duruée calibration" msgstr "" -#: scanner/models.py:69 +#: scanner/models.py:70 msgid "Durée de pose entre chaque puits en s" msgstr "" -#: scanner/models.py:71 +#: scanner/models.py:72 msgid "Suivi" msgstr "" -#: scanner/models.py:71 +#: scanner/models.py:72 msgid "Suivi et analyse des planaires" msgstr "" -#: scanner/models.py:72 +#: scanner/models.py:73 msgid "Réglage dans calibration" msgstr "" -#: scanner/models.py:72 +#: scanner/models.py:73 msgid "Autorise le réglage des valeurs par défaut dans la calibration" msgstr "" -#: scanner/models.py:74 +#: scanner/models.py:75 msgid "Axe du puit" msgstr "" -#: scanner/models.py:75 +#: scanner/models.py:76 msgid "Surface minimale" msgstr "" -#: scanner/models.py:75 +#: scanner/models.py:76 msgid "surface minimale d'un contour pour être considéré valide (px²)" msgstr "" -#: scanner/models.py:76 +#: scanner/models.py:77 msgid "surface maximale " msgstr "" -#: scanner/models.py:76 +#: scanner/models.py:77 msgid "surface maximale d'un contour en fraction de la frame (défaut 10%)" msgstr "" -#: scanner/models.py:77 +#: scanner/models.py:78 msgid "Max planaire" msgstr "" -#: scanner/models.py:77 +#: scanner/models.py:78 msgid "nombre maximum de planaires à suivre simultanément (1-10)" msgstr "" -#: scanner/models.py:81 +#: scanner/models.py:82 msgid "Actif" msgstr "" -#: scanner/models.py:98 +#: scanner/models.py:99 msgid "Nom" msgstr "" -#: scanner/models.py:98 +#: scanner/models.py:99 msgid "Nom du puit: Ai..Di" msgstr "" -#: scanner/models.py:112 +#: scanner/models.py:113 msgid "Label" msgstr "" -#: scanner/models.py:112 +#: scanner/models.py:113 msgid "Label du multi-puit" msgstr "" -#: scanner/models.py:114 +#: scanner/models.py:115 msgid "Position" msgstr "" -#: scanner/models.py:114 +#: scanner/models.py:115 msgid "Position du multi-puits sur la table" msgstr "" -#: scanner/models.py:115 +#: scanner/models.py:116 msgid "Par défaut" msgstr "" -#: scanner/models.py:115 +#: scanner/models.py:116 msgid "Multi-puit par défaut" msgstr "" -#: scanner/models.py:117 +#: scanner/models.py:118 msgid "Colonnes" msgstr "" -#: scanner/models.py:118 +#: scanner/models.py:119 msgid "Lignes" msgstr "" -#: scanner/models.py:119 +#: scanner/models.py:120 msgid "Diamètre" msgstr "" -#: scanner/models.py:119 +#: scanner/models.py:120 msgid "Diamètre des tubes en mm" msgstr "" -#: scanner/models.py:120 +#: scanner/models.py:121 msgid "Définition" msgstr "" -#: scanner/models.py:120 +#: scanner/models.py:121 msgid "Définition des lignes" msgstr "" -#: scanner/models.py:121 +#: scanner/models.py:122 msgid "Ordre ligne" msgstr "" -#: scanner/models.py:121 +#: scanner/models.py:122 msgid "Ordre ligne de puit. Lecture en serpentin dans le sens des +- X" msgstr "" -#: scanner/models.py:123 scanner/models.py:182 +#: scanner/models.py:124 scanner/models.py:183 msgid "Ordre" msgstr "" -#: scanner/models.py:123 +#: scanner/models.py:124 msgid "Ordre de lecture du multi-puit" msgstr "" -#: scanner/models.py:124 scanner/models.py:244 +#: scanner/models.py:125 scanner/models.py:245 #: scanner/templates/scanner/calibration.html:187 msgid "Durée" msgstr "" -#: scanner/models.py:124 +#: scanner/models.py:125 msgid "Durée de capture en secondes pour la calibration" msgstr "" -#: scanner/models.py:125 +#: scanner/models.py:126 msgid "Base origine X en mm" msgstr "" -#: scanner/models.py:126 +#: scanner/models.py:127 msgid "Base origine Y en mm" msgstr "" -#: scanner/models.py:128 +#: scanner/models.py:129 msgid "Pas X" msgstr "" -#: scanner/models.py:128 +#: scanner/models.py:129 msgid "Pas ou interval sur X en mm" msgstr "" -#: scanner/models.py:129 +#: scanner/models.py:130 msgid "Pas Y" msgstr "" -#: scanner/models.py:129 +#: scanner/models.py:130 msgid "Pas ou interval sur Y en mm" msgstr "" -#: scanner/models.py:130 scanner/templates/scanner/calibration.html:23 +#: scanner/models.py:131 scanner/templates/scanner/calibration.html:23 msgid "Vitesse" msgstr "" -#: scanner/models.py:130 +#: scanner/models.py:131 msgid "Vitesse déplacement en mm/mn " msgstr "" -#: scanner/models.py:132 +#: scanner/models.py:133 msgid "" "Positions des puits générées ?. Non => efface WellPosition et recalcule les " "positions" msgstr "" -#: scanner/models.py:182 +#: scanner/models.py:183 msgid "Ordre de lecture du puit" msgstr "" -#: scanner/models.py:183 +#: scanner/models.py:184 msgid "X" msgstr "" -#: scanner/models.py:183 +#: scanner/models.py:184 msgid "Axe X en mm" msgstr "" -#: scanner/models.py:184 +#: scanner/models.py:185 msgid "Y" msgstr "" -#: scanner/models.py:184 +#: scanner/models.py:185 msgid "Axe Y en mm" msgstr "" -#: scanner/models.py:199 +#: scanner/models.py:200 msgid "Position du puit" msgstr "" -#: scanner/models.py:200 +#: scanner/models.py:201 msgid "Position des puits" msgstr "" -#: scanner/models.py:238 +#: scanner/models.py:239 msgid "Titre de l'expérience" msgstr "" -#: scanner/models.py:239 +#: scanner/models.py:240 msgid "Descriptions de l'expérience" msgstr "" -#: scanner/models.py:244 +#: scanner/models.py:245 msgid "Durée de la prise de vue en secondes" msgstr "" -#: scanner/models.py:246 scanner/models.py:284 +#: scanner/models.py:247 scanner/models.py:285 +#: scanner/templates/scanner/export_medias.html:39 msgid "Date de création" msgstr "" -#: scanner/models.py:247 +#: scanner/models.py:248 msgid "Date de début" msgstr "" -#: scanner/models.py:248 scanner/models.py:285 +#: scanner/models.py:249 scanner/models.py:286 msgid "Date de fin" msgstr "" -#: scanner/models.py:273 +#: scanner/models.py:274 msgid "En attente" msgstr "" -#: scanner/models.py:274 +#: scanner/models.py:275 msgid "En cours" msgstr "" -#: scanner/models.py:275 +#: scanner/models.py:276 msgid "Terminé" msgstr "" -#: scanner/models.py:276 +#: scanner/models.py:277 msgid "Erreur" msgstr "" -#: scanner/models.py:278 +#: scanner/models.py:279 msgid "Nom de la session" msgstr "" -#: scanner/models.py:278 +#: scanner/models.py:279 msgid "Session d'expérience. 4 Multi-puits maximum" msgstr "" -#: scanner/models.py:281 -msgid "Date d'exportation prévue" +#: scanner/models.py:282 +msgid "Exportation auto" msgstr "" #: scanner/models.py:282 +msgid "Date d'exportation prévue" +msgstr "" + +#: scanner/models.py:283 +msgid "Bbalayage auto" +msgstr "" + +#: scanner/models.py:283 msgid "Date du balayage prévue" msgstr "" -#: scanner/models.py:287 +#: scanner/models.py:288 msgid "Status exportation" msgstr "" -#: scanner/models.py:290 +#: scanner/models.py:291 msgid "Export médias" msgstr "" -#: scanner/models.py:291 +#: scanner/models.py:292 msgid "Programmation de l'exportation des vidéos et images" msgstr "" -#: scanner/models.py:293 +#: scanner/models.py:294 msgid "Exportation terminée à" msgstr "" -#: scanner/models.py:295 +#: scanner/models.py:296 msgid "Status scanning" msgstr "" -#: scanner/models.py:299 +#: scanner/models.py:300 msgid "Programmation du lancement du balayage" msgstr "" -#: scanner/models.py:301 +#: scanner/models.py:302 msgid "Balayage terminé à" msgstr "" -#: scanner/models.py:315 +#: scanner/models.py:316 msgid "Terminée" msgstr "" -#: scanner/models.py:402 +#: scanner/models.py:403 msgid "Expérience d'une session" msgstr "" -#: scanner/models.py:426 +#: scanner/models.py:427 msgid "Expérience puit" msgstr "" -#: scanner/models.py:427 +#: scanner/models.py:428 msgid "Expériences puits" msgstr "" -#: scanner/process.py:381 +#: scanner/process.py:397 msgid "La session est nulle!..." msgstr "" -#: scanner/process.py:388 +#: scanner/process.py:404 msgid "Balayage démarré..." msgstr "" -#: scanner/process.py:391 +#: scanner/process.py:407 msgid "Erreur lors du démarrage du balayage..." msgstr "" @@ -2523,7 +2686,7 @@ msgstr "" msgid "Gestions des expériences" msgstr "" -#: scanner/templates/scanner/base.html:137 scanner/views.py:123 +#: scanner/templates/scanner/base.html:137 scanner/views.py:124 msgid "Balayage multi-puits" msgstr "" @@ -2535,14 +2698,14 @@ msgstr "" msgid "Export CSV des expériences" msgstr "" -#: scanner/templates/scanner/base.html:146 scanner/views.py:187 -msgid "Gestionnaire d'images" -msgstr "" - #: scanner/templates/scanner/base.html:149 msgid "Redifusion vidéos" msgstr "" +#: scanner/templates/scanner/base.html:152 +msgid "Exporter les fichiers multimédias" +msgstr "" + #: scanner/templates/scanner/calibration.html:15 msgid "Position multi-puit" msgstr "" @@ -2696,27 +2859,39 @@ msgstr "" msgid "ARRET" msgstr "" +#: scanner/templates/scanner/export_medias.html:21 +msgid "L'explortation des fichiers multimédia peut durer plusieurs heures." +msgstr "" + +#: scanner/templates/scanner/export_medias.html:22 +msgid "Préférer l'exportation différée." +msgstr "" + +#: scanner/templates/scanner/export_medias.html:42 +msgid "Status" +msgstr "" + +#: scanner/templates/scanner/export_medias.html:47 +msgid "Exporter" +msgstr "" + #: scanner/templates/scanner/images.html:27 msgid "Exporter l'ensemble des images de la session" msgstr "" -#: scanner/templates/scanner/images.html:45 +#: scanner/templates/scanner/images.html:46 msgid "Télécharger" msgstr "" -#: scanner/templates/scanner/images.html:65 +#: scanner/templates/scanner/images.html:71 msgid "Confirmer l'export des images." msgstr "" -#: scanner/templates/scanner/images.html:66 +#: scanner/templates/scanner/images.html:72 msgid "ATTENTION le processus peut être long!" msgstr "" -#: scanner/templates/scanner/replay.html:29 -msgid "Exporter l'ensemble des vidéos" -msgstr "" - -#: scanner/templates/scanner/replay.html:78 +#: scanner/templates/scanner/replay.html:84 msgid "Confirmer l'export des vidéos. ATTENTION le processus peut être long!" msgstr "" @@ -2741,23 +2916,27 @@ msgstr "" msgid "Simuler" msgstr "" -#: scanner/views.py:111 +#: scanner/views.py:112 msgid "Documentation" msgstr "" -#: scanner/views.py:222 scanner/views.py:273 +#: scanner/views.py:221 +msgid "Exporter les fichiers médias" +msgstr "" + +#: scanner/views.py:252 scanner/views.py:303 msgid "Erreur d'exportation" msgstr "" -#: scanner/views.py:246 +#: scanner/views.py:276 msgid "Gestionnaire de vidéos" msgstr "" -#: scanner/views.py:268 +#: scanner/views.py:298 msgid "Images téléchargées" msgstr "" -#: scanner/views.py:271 +#: scanner/views.py:301 msgid "Vidéos téléchargées" msgstr "" diff --git a/test_tube_scanner/modules/capture_interface.py b/test_tube_scanner/modules/capture_interface.py index fafd959..ebabd62 100644 --- a/test_tube_scanner/modules/capture_interface.py +++ b/test_tube_scanner/modules/capture_interface.py @@ -21,6 +21,7 @@ import logging from datetime import datetime, timezone from pathlib import Path from typing import Optional, Callable, TYPE_CHECKING +from asgiref.sync import async_to_sync from django.conf import settings from modules.planarian_tracker import PlanarianTracker @@ -85,6 +86,7 @@ class VideoCaptureInterface(abc.ABC): self._tracker = None self._metrics = None self._params = None + self._clientDB = self.parent.metricDB # Tracker générique, pour simulation self.on_test_well_change(**self.DEFAULT_TRACKER_CONFIG) @@ -105,8 +107,9 @@ class VideoCaptureInterface(abc.ABC): except Exception as e: logger.error(f"Error creating tracker with conf {cfg}: {e}") self._tracker = None + - def on_well_change(self, cfg, draw_contours=False): + def on_well_change(self, cfg, uuid="", draw_contours=False): """ Appelé par la CNC lors du changement de puits. Réinitialise le fond appris et l'état inter-frame du tracker. @@ -115,10 +118,20 @@ class VideoCaptureInterface(abc.ABC): if not self.use_tracking or not cfg: return - params = cfg.to_params_dict() - self._params = ExperimentParams(params) - #self._metrics = self._params.build_metrics() + # 1. Sauvegarder les résumés du puits qu'on quitte + if self._metrics and self._params: + for pid, m in enumerate(self._metrics): + async_to_sync(self._clientDB.store_summary)( + summary = m.summary(), + experiment = self._params.experiment, + well = self._params.well, + planarian = pid, + uuid = uuid, + ) + # 2. Reconstruire pour le nouveau puits + params = cfg.to_params_dict() + self._params = ExperimentParams(params) self._metrics = [self._params.build_metrics() for _ in range(self._params.planarian_count)] self._tracker = PlanarianTracker( diff --git a/test_tube_scanner/modules/planarian_metrics.py b/test_tube_scanner/modules/planarian_metrics.py index 49c1e2c..21e9fca 100644 --- a/test_tube_scanner/modules/planarian_metrics.py +++ b/test_tube_scanner/modules/planarian_metrics.py @@ -18,23 +18,11 @@ Métriques résumé (summary) : chemo_latency_s, chemo_mean_dist_mm Social : social_pct_time_avoiding, social_pct_time_aggregating, social_mean_nn_mm, social_contact_events - - Architecture : - PlanarianTracker.process() → dict brut (cx, cy, speed_px_s, ...) - EthoVisionMetrics.update() → enrichit avec métriques EthoVision - ReductStoreClient.store() → stocke dans ReductStore avec labels - ReductStoreClient.export_csv() → exporte vers CSV - Schéma des labels ReductStore : - experiment : identifiant de l'expérience (ex: "exp_2026_04_25") - well : identifiant du puits (ex: "A1", "B3") - planarian : index du planaire dans le puits (ex: "0", "1") - bucket : nom du bucket (ex: "planarian_metrics") - Created on 25 avr. 2026 @author: denis """ -#import asyncio + import csv import io import json @@ -42,8 +30,8 @@ import logging import math import os import time -from datetime import datetime, timezone +from datetime import datetime, timezone from typing import Optional logger = logging.getLogger(__name__) @@ -618,7 +606,7 @@ class ExperimentParams: def from_csv_file(cls, filepath: str) -> list: """Charge toutes les expériences d'un fichier CSV.""" results = [] - with open(filepath, newline="", encoding="utf-8-sig") as f: + with open(filepath, newline="", encoding="utf-8") as f: for row in csv.DictReader(f): try: results.append(cls.from_csv_row(row)) @@ -666,12 +654,11 @@ class ReductStoreClient: self.token = token self.bucket_name = bucket self.quota_type = quota_type - self.quota_size = quota_size - self.entry_name = "metrics" - self._client = None - self._bucket = None + self.quota_size = quota_size + self._client = None + self._bucket = None + - async def _create_bucket(self): from reduct import Client, BucketSettings self._client = Client(self.url, api_token=self.token) @@ -681,13 +668,25 @@ class ReductStoreClient: exist_ok=True, ) return await self._client.create_bucket(self.bucket_name, settings, exist_ok=True) - - + async def connect(self): """Initialise la connexion et crée le bucket si nécessaire.""" self._bucket = await self._create_bucket() logger.info(f"ReductStore connecté : {self.url} / {self.bucket_name}") + ''' + async def connect(self): + """Initialise la connexion et crée le bucket si nécessaire.""" + from reduct import Client, BucketSettings, QuotaType + self._client = Client(self.url, api_token=self.token) + self._bucket = await self._client.create_bucket( + self.bucket_name, + BucketSettings(quota_type=QuotaType.NONE), + exist_ok=True, + ) + logger.info(f"ReductStore connecté : {self.url} / {self.bucket_name}") + ''' + async def store_metric( self, record: dict, @@ -704,48 +703,99 @@ class ReductStoreClient: Le timestamp est rendu unique par planaire en ajoutant l'index du planaire comme offset sub-microseconde — évite le 409 Conflict quand plusieurs planaires du même puits écrivent dans la même frame. + + Args: + record : dict de métriques (issu de EthoVisionMetrics.update()) + experiment : identifiant de l'expérience + well : identifiant du puits + planarian : index du planaire (défaut 0) + record_type : "frame" ou "summary" + uuid : identifiant unique de session (permet de filtrer + plusieurs sessions d'un même puits/expérience) + ts_us : timestamp en microsecondes (défaut : maintenant) """ if self._bucket is None: await self.connect() # ts_us de base + offset planaire (0, 1, 2…) pour unicité garantie - base_ts = ts_us or int(time.time() * 1_000_000) + base_ts = ts_us or int(time.time() * 1_000_000) unique_ts = base_ts + planarian - + labels = { + "experiment": experiment, + "well": well, + "planarian": str(planarian), + "record_type": record_type, + } + if uuid: + labels["uuid"] = uuid await self._bucket.write( entry_name = "metrics", data = json.dumps(record).encode("utf-8"), timestamp = unique_ts, - labels = { - "experiment": experiment, - "well": well, - "planarian": str(planarian), - "record_type": record_type, - "uuid": uuid, - }, + labels = labels, content_type = "application/json", ) - async def store_summary(self, summary: dict, experiment: str, - well: str, planarian: int = 0): - """Stocke le résumé de fin de session.""" - await self.store_metric(summary, experiment, well, planarian, "summary") + async def store_summary( + self, + summary: dict, + experiment: str, + well: str, + planarian: int = 0, + uuid: str = "", + ): + """ + Stocke le résumé de fin de session dans ReductStore. + + Args: + summary : dict issu de EthoVisionMetrics.summary() + experiment : identifiant de l'expérience + well : identifiant du puits + planarian : index du planaire + uuid : identifiant unique de session (même valeur que + celle utilisée dans store_metric pour cette session) + """ + await self.store_metric( + record = summary, + experiment = experiment, + well = well, + planarian = planarian, + record_type = "summary", + uuid = uuid, + ) async def get_tracking_data( self, experiment: str, well: str, planarian: int = 0, - record_type: str = "metrics", + record_type: str = "frame", + uuid: str = "", start: Optional[datetime] = None, stop: Optional[datetime] = None, ) -> list: - """Récupère les enregistrements filtrés par labels.""" + """ + Récupère les enregistrements filtrés par labels. + + Args: + experiment : identifiant de l'expérience + well : identifiant du puits + planarian : index du planaire + record_type : "frame" | "summary" + uuid : filtre sur une session spécifique (optionnel — + si vide, retourne toutes les sessions) + start, stop : plage temporelle (datetime UTC, optionnel) + """ if self._bucket is None: await self.connect() - kwargs = {"include": { - "experiment": experiment, "well": well, - "planarian": str(planarian), "record_type": record_type, - }} + labels = { + "experiment": experiment, + "well": well, + "planarian": str(planarian), + "record_type": record_type, + } + if uuid: + labels["uuid"] = uuid + kwargs = {"include": labels} if start: kwargs["start"] = int(start.timestamp() * 1_000_000) if stop: @@ -812,7 +862,8 @@ class ReductStoreClient: experiment: str, well: str, planarian: int = 0, - record_type: str = "metrics", + record_type: str = "frame", + uuid: str = "", output_dir: str = ".", start: Optional[datetime] = None, stop: Optional[datetime] = None, @@ -828,13 +879,15 @@ class ReductStoreClient: planarian : index du planaire record_type : "frame" | "summary" output_dir : répertoire de sortie (défaut : répertoire courant) + uuid : filtre sur une session spécifique (optionnel — + si vide, retourne toutes les sessions) start, stop : plage temporelle (datetime UTC, optionnel) Returns: tuple (filepath, nb_lignes) """ records = await self.get_tracking_data( - experiment, well, planarian, record_type, start, stop) + experiment, well, planarian, record_type, uuid, start, stop) if not records: logger.warning(f"Aucune donnée pour {experiment}/{well}/{planarian}") return "", 0 @@ -857,7 +910,8 @@ class ReductStoreClient: experiment: str, well: str, planarian: int = 0, - record_type: str = "metrics", + record_type: str = "frame", + uuid: str = "", start: Optional[datetime] = None, stop: Optional[datetime] = None, ) -> tuple: @@ -869,7 +923,7 @@ class ReductStoreClient: tuple (contenu_csv_str, nb_lignes) """ records = await self.get_tracking_data( - experiment, well, planarian, record_type, start, stop) + experiment, well, planarian, record_type, uuid, start, stop) if not records: return "", 0 records = self._convert_timestamps(records) @@ -888,5 +942,4 @@ class ReductStoreClient: """ self._client = None self._bucket = None - logger.info("ReductStore déconnecté") - + logger.info("ReductStore déconnecté") diff --git a/test_tube_scanner/modules/planarian_metrics2.py b/test_tube_scanner/modules/planarian_metrics2.py new file mode 100644 index 0000000..540d2bb --- /dev/null +++ b/test_tube_scanner/modules/planarian_metrics2.py @@ -0,0 +1,891 @@ +""" +modules/planarian_metrics.py + +Intégration des métriques EthoVision XT + comportementales dans PlanarianScanner. + +Métriques par frame : + Mobilité : velocity, distance, moving, mobility_state + Thigmo : dist_to_wall_mm, near_wall + Photo : dist_to_light_mm, heading_to_light_deg, fleeing_light + Chemo : dist_to_food_mm, heading_to_food_deg, approaching_food, in_food_zone + Social : nearest_neighbour_mm, in_avoid_zone, in_aggreg_zone, chem_repulsion_level + +Métriques résumé (summary) : + Mobilité : movedCenter_pointTotal_mm, velocity_mean_mm_s, durations par état + Thigmo : thigmotaxis_pct_time_near_wall + Photo : photo_pct_time_fleeing, photo_mean_dist_mm, photo_latency_s + Chemo : chemo_pct_time_approaching, chemo_pct_time_in_zone, + chemo_latency_s, chemo_mean_dist_mm + Social : social_pct_time_avoiding, social_pct_time_aggregating, + social_mean_nn_mm, social_contact_events + + Architecture : + PlanarianTracker.process() → dict brut (cx, cy, speed_px_s, ...) + EthoVisionMetrics.update() → enrichit avec métriques EthoVision + ReductStoreClient.store() → stocke dans ReductStore avec labels + ReductStoreClient.export_csv() → exporte vers CSV + + Schéma des labels ReductStore : + experiment : identifiant de l'expérience (ex: "exp_2026_04_25") + well : identifiant du puits (ex: "A1", "B3") + planarian : index du planaire dans le puits (ex: "0", "1") + bucket : nom du bucket (ex: "planarian_metrics") + +Created on 25 avr. 2026 +@author: denis +""" +#import asyncio +import csv +import io +import json +import logging +import math +import os +import time +from datetime import datetime, timezone + +from typing import Optional + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Constantes EthoVision (seuils de mobilité par défaut) +# --------------------------------------------------------------------------- + +THRESH_IMMOBILE_DEFAULT = 0.2 # en-dessous : Immobile (mm/s) +THRESH_MOBILE_DEFAULT = 1.5 # entre les deux : Mobile, au-delà : Highly mobile + +STATE_IMMOBILE = "Immobile" +STATE_MOBILE = "Mobile" +STATE_HIGH_MOBILE = "Highly mobile" + +# Paramètres comportementaux (défauts) +BEHAVIOUR_DEFAULTS = { + # Thigmotactisme + "thigmotaxis_wall_dist_mm": 1.0, + # Phototactisme + "photo_mode": "none", + "photo_strength": 0.0, + "photo_x": 0.5, + "photo_y": 0.5, + "photo_flee_angle_deg": 90.0, # angle max tête/source pour considérer "fuite" + # Chimiotactisme + "chemo_strength": 0.0, + "chemo_x": 0.5, + "chemo_y": 0.5, + "chemo_radius_mm": 2.0, + "chemo_approach_angle_deg": 90.0, # angle max tête/nourriture pour considérer "approche" + # Interactions inter-individus + "avoid_radius_mm": 3.0, + "aggreg_radius_mm": 6.0, +} + + +# --------------------------------------------------------------------------- +# Helpers géométriques +# --------------------------------------------------------------------------- + +def _angle_between_deg(vx1: float, vy1: float, vx2: float, vy2: float) -> float: + """ + Calcule l'angle en degrés entre deux vecteurs 2D. + Retourne 0.0 si l'un des vecteurs est nul. + + Args: + vx1, vy1 : premier vecteur + vx2, vy2 : second vecteur + + Returns: + angle en degrés [0, 180] + """ + n1 = math.sqrt(vx1**2 + vy1**2) + n2 = math.sqrt(vx2**2 + vy2**2) + if n1 < 1e-9 or n2 < 1e-9: + return 0.0 + cos_a = max(-1.0, min(1.0, (vx1 * vx2 + vy1 * vy2) / (n1 * n2))) + return math.degrees(math.acos(cos_a)) + + +def _heading_to_target_deg( + cx: float, cy: float, + tx: float, ty: float, + dx: float, dy: float, +) -> float: + """ + Calcule l'angle entre la direction de déplacement et le vecteur vers une cible. + + Args: + cx, cy : position courante + tx, ty : position cible + dx, dy : vecteur de déplacement (cx - prev_cx, cy - prev_cy) + + Returns: + angle en degrés [0, 180] — 0 = va droit vers la cible, 180 = fuit + """ + to_target_x = tx - cx + to_target_y = ty - cy + return _angle_between_deg(dx, dy, to_target_x, to_target_y) + + +# --------------------------------------------------------------------------- +# Classe EthoVisionMetrics +# --------------------------------------------------------------------------- + +class EthoVisionMetrics: + """ + Calcule et accumule toutes les métriques comportementales compatibles + EthoVision XT à partir des données brutes de PlanarianTracker. + + Métriques calculées : + - Mobilité EthoVision (distance, vitesse, états Immobile/Mobile/Très mobile) + - Thigmotactisme (distance paroi, % temps près du bord) + - Phototactisme (distance source, orientation, % fuite, latence) + - Chimiotactisme (distance nourriture, % approche, % zone, latence) + - Interactions inter-individus (voisin le plus proche, évitement, + agrégation, répulsion chimique, événements de contact) + + Une instance par planaire suivi. + + Usage : + metrics = EthoVisionMetrics(px_per_mm=26.25, fps=10, behaviour={...}) + for frame in capture: + raw = tracker.process(frame, ts) + record = metrics.update( + raw, + well_radius_mm = 8.0, + arena_center_px = (250, 250), + photo_source_px = (100, 100), + others_pos_mm = [(x1,y1), (x2,y2)], + chem_level = 0.3, + ) + await client.store_metric(record, ...) + summary = metrics.summary() + """ + + def __init__( + self, + px_per_mm: float, + fps: float, + thresh_immobile: float = THRESH_IMMOBILE_DEFAULT, + thresh_mobile: float = THRESH_MOBILE_DEFAULT, + behaviour: Optional[dict] = None, + ): + """ + Args: + px_per_mm : facteur de conversion pixels → mm + fps : fréquence de capture (images/s) + thresh_immobile : seuil vitesse Immobile/Mobile (mm/s) + thresh_mobile : seuil vitesse Mobile/Très mobile (mm/s) + behaviour : dict de paramètres comportementaux (cf. BEHAVIOUR_DEFAULTS) + """ + self.px_per_mm = px_per_mm + self.fps = fps + self.dt = 1.0 / fps + self.thresh_immobile = thresh_immobile + self.thresh_mobile = thresh_mobile + self.beh = {**BEHAVIOUR_DEFAULTS, **(behaviour or {})} + + # --- Accumulateurs mobilité --- + self.total_distance_mm = 0.0 + self.duration_moving_s = 0.0 + self.duration_stopped_s = 0.0 + self.frame_count = 0 + + self._mob_counts = {STATE_IMMOBILE: 0, STATE_MOBILE: 0, STATE_HIGH_MOBILE: 0} + self._mob_durations = {STATE_IMMOBILE: 0.0, STATE_MOBILE: 0.0, STATE_HIGH_MOBILE: 0.0} + self._current_state = None + + # --- Accumulateurs thigmotactisme --- + self._near_wall_frames = 0 + + # --- Accumulateurs phototactisme --- + self._flee_frames = 0 # frames en fuite + self._photo_dist_sum = 0.0 # somme distances source + self._photo_dist_count = 0 + self._photo_latency_s = None # temps avant 1ère fuite (s) + + # --- Accumulateurs chimiotactisme --- + self._approach_frames = 0 # frames en approche nourriture + self._in_zone_frames = 0 # frames dans la zone nourriture + self._chemo_dist_sum = 0.0 + self._chemo_dist_count = 0 + self._chemo_latency_s = None # temps avant 1ère entrée zone (s) + + # --- Accumulateurs interactions inter-individus --- + self._avoid_frames = 0 # frames en zone d'évitement + self._aggreg_frames = 0 # frames en zone d'agrégation + self._nn_sum = 0.0 # somme distances voisin le plus proche + self._nn_count = 0 + self._contact_events = 0 # transitions False→True de in_avoid_zone + self._prev_in_avoid = False + + # --- Position précédente (vecteur de déplacement) --- + self._prev_cx_mm = None + self._prev_cy_mm = None + self._prev_ts = None + + # ------------------------------------------------------------------ # + # Helpers internes + # ------------------------------------------------------------------ # + + def _px_to_mm(self, px: float) -> float: + """Convertit des pixels en millimètres.""" + return px / self.px_per_mm + + def _classify(self, v: float) -> str: + """Classifie la vitesse en état de mobilité EthoVision.""" + if v <= self.thresh_immobile: + return STATE_IMMOBILE + elif v <= self.thresh_mobile: + return STATE_MOBILE + return STATE_HIGH_MOBILE + + def _elapsed_s(self) -> float: + """Temps écoulé depuis le début de la session (s).""" + return self.frame_count * self.dt + + # ------------------------------------------------------------------ # + # Méthode principale + # ------------------------------------------------------------------ # + + def update( + self, + raw: dict, + well_radius_mm: float = 8.0, + arena_center_px: tuple = (250, 250), + photo_source_px: Optional[tuple] = None, + others_pos_mm: Optional[list] = None, + chem_level: float = 0.0, + ) -> dict: + """ + Calcule toutes les métriques comportementales pour une frame. + + Args: + raw : dict brut de PlanarianTracker.process() + clés : detected, cx, cy, speed_px_s, ts + well_radius_mm : rayon du puits en mm + arena_center_px : centre de l'arène en pixels (cx, cy) + photo_source_px : position de la source lumineuse en pixels (ou None) + others_pos_mm : liste de (x_mm, y_mm) des autres planaires + chem_level : concentration chimique locale [0-1] (depuis ChemicalMap) + + Returns: + dict complet prêt pour ReductStore + """ + self.frame_count += 1 + ts = raw.get("timestamp", time.time()) + + if not raw.get("detected", False): + self.duration_stopped_s += self.dt + state = self._current_state or STATE_IMMOBILE + self._mob_durations[state] += self.dt + return {"timestamp": ts, "detected": False} + + # --- Position en mm (relative au centre de l'arène) --- + cx_px = raw["cx"] - arena_center_px[0] + cy_px = raw["cy"] - arena_center_px[1] + cx_mm = self._px_to_mm(cx_px) + cy_mm = self._px_to_mm(cy_px) + + # --- Vitesse / distance --- + speed_px_s = raw.get("speed_px_s", 0.0) + velocity_mm_s = self._px_to_mm(speed_px_s) + dist_mm = velocity_mm_s * self.dt + self.total_distance_mm += dist_mm + + # Vecteur de déplacement (pour calculs d'angle) + if self._prev_cx_mm is not None: + move_dx = cx_mm - self._prev_cx_mm + move_dy = cy_mm - self._prev_cy_mm + else: + move_dx, move_dy = 0.0, 0.0 + + # --- Mobilité --- + is_moving = velocity_mm_s > self.thresh_immobile + if is_moving: + self.duration_moving_s += self.dt + else: + self.duration_stopped_s += self.dt + + new_state = self._classify(velocity_mm_s) + if new_state != self._current_state: + self._mob_counts[new_state] += 1 + self._current_state = new_state + self._mob_durations[new_state] += self.dt + + # ================================================================ + # THIGMOTACTISME + # ================================================================ + well_radius_px = well_radius_mm * self.px_per_mm + dist_center_px = math.sqrt(cx_px**2 + cy_px**2) + dist_wall_mm = self._px_to_mm(well_radius_px - dist_center_px) + near_wall_thr = self.beh.get("thigmotaxis_wall_dist_mm", 1.0) + is_near_wall = dist_wall_mm < near_wall_thr + if is_near_wall: + self._near_wall_frames += 1 + + # ================================================================ + # PHOTOTACTISME + # ================================================================ + photo_mode = self.beh.get("photo_mode", "none") + dist_light_mm = 0.0 + heading_light_deg = 0.0 + fleeing_light = False + + if photo_mode != "none" and photo_source_px is not None: + lx_px = photo_source_px[0] - arena_center_px[0] + ly_px = photo_source_px[1] - arena_center_px[1] + lx_mm = self._px_to_mm(lx_px) + ly_mm = self._px_to_mm(ly_px) + + dl = math.sqrt((cx_mm - lx_mm)**2 + (cy_mm - ly_mm)**2) + dist_light_mm = dl + + self._photo_dist_sum += dl + self._photo_dist_count += 1 + + # Angle entre déplacement et direction vers la source + heading_light_deg = _heading_to_target_deg( + cx_mm, cy_mm, lx_mm, ly_mm, move_dx, move_dy + ) + # Fuite = planaire s'éloigne de la source (angle > seuil) + flee_thr = self.beh.get("photo_flee_angle_deg", 90.0) + fleeing_light = (heading_light_deg > flee_thr) and is_moving + + if fleeing_light: + self._flee_frames += 1 + if self._photo_latency_s is None: + self._photo_latency_s = self._elapsed_s() + + # ================================================================ + # CHIMIOTACTISME + # ================================================================ + chemo_x_frac = self.beh.get("chemo_x", 0.5) + chemo_y_frac = self.beh.get("chemo_y", 0.5) + chemo_r_mm = self.beh.get("chemo_radius_mm", 2.0) + chemo_strength= self.beh.get("chemo_strength", 0.0) + + dist_food_mm = 0.0 + heading_food_deg = 0.0 + approaching_food = False + in_food_zone = False + + if chemo_strength > 0.0: + # Position nourriture en mm relative au centre + fx_mm = (chemo_x_frac - 0.5) * 2.0 * well_radius_mm + fy_mm = (chemo_y_frac - 0.5) * 2.0 * well_radius_mm + + df = math.sqrt((cx_mm - fx_mm)**2 + (cy_mm - fy_mm)**2) + dist_food_mm = df + + self._chemo_dist_sum += df + self._chemo_dist_count += 1 + + in_food_zone = df <= chemo_r_mm + if in_food_zone: + self._in_zone_frames += 1 + if self._chemo_latency_s is None: + self._chemo_latency_s = self._elapsed_s() + + heading_food_deg = _heading_to_target_deg( + cx_mm, cy_mm, fx_mm, fy_mm, move_dx, move_dy + ) + approach_thr = self.beh.get("chemo_approach_angle_deg", 90.0) + approaching_food = (heading_food_deg < approach_thr) and is_moving + + if approaching_food: + self._approach_frames += 1 + + # ================================================================ + # INTERACTIONS INTER-INDIVIDUS + # ================================================================ + avoid_r_mm = self.beh.get("avoid_radius_mm", 3.0) + aggreg_r_mm = self.beh.get("aggreg_radius_mm", 6.0) + + nearest_nn_mm = float("inf") + in_avoid_zone = False + in_aggreg_zone = False + + if others_pos_mm: + for ox_mm, oy_mm in others_pos_mm: + d = math.sqrt((cx_mm - ox_mm)**2 + (cy_mm - oy_mm)**2) + if d < nearest_nn_mm: + nearest_nn_mm = d + + if nearest_nn_mm < avoid_r_mm: + in_avoid_zone = True + self._avoid_frames += 1 + elif nearest_nn_mm < aggreg_r_mm: + in_aggreg_zone = True + self._aggreg_frames += 1 + + self._nn_sum += nearest_nn_mm + self._nn_count += 1 + + # Événement de contact : transition vers zone d'évitement + if in_avoid_zone and not self._prev_in_avoid: + self._contact_events += 1 + else: + nearest_nn_mm = 0.0 + + self._prev_in_avoid = in_avoid_zone + + # --- Mise à jour position précédente --- + self._prev_cx_mm = cx_mm + self._prev_cy_mm = cy_mm + self._prev_ts = ts + + # ================================================================ + # RECORD COMPLET + # ================================================================ + return { + # Identification + "timestamp": ts, + "detected": True, + # Position (mm, relative au centre) + "x_mm": round(cx_mm, 4), + "y_mm": round(cy_mm, 4), + # Position brute pixels + "cx_px": raw["cx"], + "cy_px": raw["cy"], + # Mobilité EthoVision + "velocity_mm_s": round(velocity_mm_s, 4), + "distance_mm": round(dist_mm, 4), + "total_distance_mm": round(self.total_distance_mm, 4), + "moving": int(is_moving), + "duration_moving_s": round(self.duration_moving_s, 3), + "duration_stopped_s": round(self.duration_stopped_s, 3), + "mobility_state": new_state, + "mobility_immobile_freq": self._mob_counts[STATE_IMMOBILE], + "mobility_immobile_duration_s": round(self._mob_durations[STATE_IMMOBILE], 3), + "mobility_mobile_freq": self._mob_counts[STATE_MOBILE], + "mobility_mobile_duration_s": round(self._mob_durations[STATE_MOBILE], 3), + "mobility_high_mobile_freq": self._mob_counts[STATE_HIGH_MOBILE], + "mobility_high_mobile_duration_s": round(self._mob_durations[STATE_HIGH_MOBILE], 3), + # Thigmotactisme + "dist_to_wall_mm": round(dist_wall_mm, 4), + "near_wall": int(is_near_wall), + # Phototactisme + "dist_to_light_mm": round(dist_light_mm, 4), + "heading_to_light_deg": round(heading_light_deg, 2), + "fleeing_light": int(fleeing_light), + # Chimiotactisme + "dist_to_food_mm": round(dist_food_mm, 4), + "heading_to_food_deg": round(heading_food_deg, 2), + "approaching_food": int(approaching_food), + "in_food_zone": int(in_food_zone), + # Interactions inter-individus + "nearest_neighbour_mm": round(nearest_nn_mm, 4) if nearest_nn_mm != float("inf") else 0.0, + "in_avoid_zone": int(in_avoid_zone), + "in_aggreg_zone": int(in_aggreg_zone), + "chem_repulsion_level": round(chem_level, 4), + # Passthrough tracker + "area_px": raw.get("area_px", 0), + "axial_pos": raw.get("axial_pos", 0.0), + "axial_speed": raw.get("axial_speed", 0.0), + } + + # ------------------------------------------------------------------ # + # Résumé de session + # ------------------------------------------------------------------ # + + def summary(self) -> dict: + """ + Retourne le résumé global de la session. + Nomenclature EthoVision XT + métriques comportementales. + À appeler en fin d'expérience. + + Returns: + dict avec toutes les métriques agrégées + """ + total_s = self.frame_count * self.dt + det = max(self._photo_dist_count, 1) # frames avec détection + + return { + # Identification session + "total_frames": self.frame_count, + "total_duration_s": round(total_s, 3), + # --- Mobilité EthoVision --- + "movedCenter_pointTotal_mm": round(self.total_distance_mm, 4), + "velocity_mean_mm_s": round( + self.total_distance_mm / total_s if total_s > 0 else 0.0, 4), + "movement_moving_duration_s": round(self.duration_moving_s, 3), + "movement_not_moving_duration_s": round(self.duration_stopped_s, 3), + "mobility_immobile_frequency": self._mob_counts[STATE_IMMOBILE], + "mobility_immobile_duration_s": round(self._mob_durations[STATE_IMMOBILE], 3), + "mobility_mobile_frequency": self._mob_counts[STATE_MOBILE], + "mobility_mobile_duration_s": round(self._mob_durations[STATE_MOBILE], 3), + "mobility_highly_mobile_frequency": self._mob_counts[STATE_HIGH_MOBILE], + "mobility_highly_mobile_duration_s": round(self._mob_durations[STATE_HIGH_MOBILE], 3), + # --- Thigmotactisme --- + "thigmotaxis_pct_time_near_wall": round( + 100.0 * self._near_wall_frames / max(self.frame_count, 1), 2), + # --- Phototactisme --- + "photo_pct_time_fleeing": round( + 100.0 * self._flee_frames / max(self.frame_count, 1), 2), + "photo_mean_dist_mm": round( + self._photo_dist_sum / max(self._photo_dist_count, 1), 4), + "photo_latency_s": round(self._photo_latency_s, 3) + if self._photo_latency_s is not None else None, + # --- Chimiotactisme --- + "chemo_pct_time_approaching": round( + 100.0 * self._approach_frames / max(self.frame_count, 1), 2), + "chemo_pct_time_in_zone": round( + 100.0 * self._in_zone_frames / max(self.frame_count, 1), 2), + "chemo_latency_s": round(self._chemo_latency_s, 3) + if self._chemo_latency_s is not None else None, + "chemo_mean_dist_mm": round( + self._chemo_dist_sum / max(self._chemo_dist_count, 1), 4), + # --- Interactions inter-individus --- + "social_pct_time_avoiding": round( + 100.0 * self._avoid_frames / max(self.frame_count, 1), 2), + "social_pct_time_aggregating": round( + 100.0 * self._aggreg_frames / max(self.frame_count, 1), 2), + "social_mean_nn_mm": round( + self._nn_sum / max(self._nn_count, 1), 4), + "social_contact_events": self._contact_events, + } + + def reset(self): + """Réinitialise tous les accumulateurs (changement de puits ou planaire).""" + self.__init__( + self.px_per_mm, self.fps, + self.thresh_immobile, self.thresh_mobile, self.beh, + ) + + @staticmethod + def _empty_record(ts: float) -> dict: + """Enregistrement vide (planaire non détecté).""" + return {"timestamp": ts, "detected": False} + + +# --------------------------------------------------------------------------- +# Paramètres expérimentaux +# --------------------------------------------------------------------------- + +class ExperimentParams: + """ + Conteneur des paramètres d'une expérience. + Instanciable depuis un dict, un fichier CSV ou un modèle Django. + """ + + REQUIRED = {"experiment", "well", "px_per_mm", "fps"} + + DEFAULTS = { + "well_radius_mm": 8.0, + "thresh_immobile": THRESH_IMMOBILE_DEFAULT, + "thresh_mobile": THRESH_MOBILE_DEFAULT, + "planarian_count": 1, + "tube_axis": "vertical", + "min_area_px": 20, + "max_area_ratio": 0.10, + **BEHAVIOUR_DEFAULTS, + } + + def __init__(self, data: dict): + missing = self.REQUIRED - set(data.keys()) + if missing: + raise ValueError(f"Paramètres manquants : {missing}") + merged = {**self.DEFAULTS, **data} + for k, v in merged.items(): + setattr(self, k, self._cast(k, v)) + + @staticmethod + def _cast(key: str, value): + """Cast automatique des valeurs CSV (toutes en string) vers le bon type.""" + float_keys = { + "px_per_mm", "fps", "well_radius_mm", "thresh_immobile", "thresh_mobile", + "photo_strength", "photo_x", "photo_y", "photo_flee_angle_deg", + "chemo_strength", "chemo_x", "chemo_y", "chemo_radius_mm", + "chemo_approach_angle_deg", "thigmotaxis_wall_dist_mm", + "avoid_radius_mm", "aggreg_radius_mm", "max_area_ratio", + } + int_keys = {"planarian_count", "min_area_px"} + if key in float_keys: + return float(value) + if key in int_keys: + return int(value) + if isinstance(value, str) and value.lower() in ("true", "false"): + return value.lower() == "true" + return value + + @classmethod + def from_csv_row(cls, row: dict) -> "ExperimentParams": + """Instancie depuis une ligne de csv.DictReader.""" + return cls(row) + + @classmethod + def from_csv_file(cls, filepath: str) -> list: + """Charge toutes les expériences d'un fichier CSV.""" + results = [] + with open(filepath, newline="", encoding="utf-8-sig") as f: + for row in csv.DictReader(f): + try: + results.append(cls.from_csv_row(row)) + except ValueError as e: + logger.warning(f"Ligne ignorée : {e} — {row}") + return results + + def to_dict(self) -> dict: + """Sérialise en dict.""" + return {k: getattr(self, k) + for k in {**self.DEFAULTS, **{r: None for r in self.REQUIRED}}} + + def build_metrics(self) -> EthoVisionMetrics: + """Construit l'instance EthoVisionMetrics pour ces paramètres.""" + behaviour = {k: getattr(self, k) for k in BEHAVIOUR_DEFAULTS if hasattr(self, k)} + return EthoVisionMetrics( + px_per_mm = self.px_per_mm, + fps = self.fps, + thresh_immobile = self.thresh_immobile, + thresh_mobile = self.thresh_mobile, + behaviour = behaviour, + ) + + +# --------------------------------------------------------------------------- +# Client ReductStore +# --------------------------------------------------------------------------- + +class ReductStoreClient: + """ + Interface asynchrone avec ReductStore pour PlanarianScanner. + + Labels : experiment | well | planarian | record_type (frame|summary) + """ + + def __init__( + self, + url: str = "http://localhost:8383", + token: str = "", + bucket: str = "planarian_metrics", + quota_type=None, + quota_size=1000_000_000 + ): + self.url = url + self.token = token + self.bucket_name = bucket + self.quota_type = quota_type + self.quota_size = quota_size + self.entry_name = "metrics" + self._client = None + self._bucket = None + + + async def _create_bucket(self): + from reduct import Client, BucketSettings + self._client = Client(self.url, api_token=self.token) + settings = BucketSettings( + quota_type=self.quota_type, + quota_size=self.quota_size, + exist_ok=True, + ) + return await self._client.create_bucket(self.bucket_name, settings, exist_ok=True) + + + async def connect(self): + """Initialise la connexion et crée le bucket si nécessaire.""" + self._bucket = await self._create_bucket() + logger.info(f"ReductStore connecté : {self.url} / {self.bucket_name}") + + async def store_metric( + self, + record: dict, + experiment: str, + well: str, + planarian: int = 0, + record_type: str = "frame", + uuid: str = "", + ts_us: Optional[int] = None, + ): + """ + Stocke un enregistrement dans ReductStore. + + Le timestamp est rendu unique par planaire en ajoutant l'index + du planaire comme offset sub-microseconde — évite le 409 Conflict + quand plusieurs planaires du même puits écrivent dans la même frame. + """ + if self._bucket is None: + await self.connect() + # ts_us de base + offset planaire (0, 1, 2…) pour unicité garantie + base_ts = ts_us or int(time.time() * 1_000_000) + unique_ts = base_ts + planarian + + await self._bucket.write( + entry_name = "metrics", + data = json.dumps(record).encode("utf-8"), + timestamp = unique_ts, + labels = { + "experiment": experiment, + "well": well, + "planarian": str(planarian), + "record_type": record_type, + "uuid": uuid, + }, + content_type = "application/json", + ) + + async def store_summary(self, summary: dict, experiment: str, well: str, planarian: int = 0): + """Stocke le résumé de fin de session.""" + await self.store_metric(summary, experiment, well, planarian, "summary") + + async def get_tracking_data( + self, + experiment: str, + well: str, + planarian: int = 0, + record_type: str = "metrics", + start: Optional[datetime] = None, + stop: Optional[datetime] = None, + ) -> list: + """Récupère les enregistrements filtrés par labels.""" + if self._bucket is None: + await self.connect() + kwargs = {"include": { + "experiment": experiment, "well": well, + "planarian": str(planarian), "record_type": record_type, + }} + if start: + kwargs["start"] = int(start.timestamp() * 1_000_000) + if stop: + kwargs["stop"] = int(stop.timestamp() * 1_000_000) + records = [] + async for rec in self._bucket.query("metrics", **kwargs): + try: + records.append(json.loads(await rec.read_all())) + except Exception as e: + logger.warning(f"Entrée illisible ignorée : {e}") + return records + + @staticmethod + def _convert_timestamps(records: list) -> list: + """ + Convertit le champ 'timestamp' (epoch float secondes) en ISO 8601 UTC + dans chaque enregistrement. + + Args: + records : liste de dicts issus de ReductStore + + Returns: + nouvelle liste avec timestamp converti (originaux non modifiés) + """ + converted = [] + for r in records: + row = dict(r) + ts = row.get("timestamp") + if ts is not None: + try: + row["timestamp"] = ( + datetime.fromtimestamp(float(ts), tz=timezone.utc) + .strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z" + ) + except (ValueError, TypeError, OSError): + pass + converted.append(row) + return converted + + @staticmethod + def _build_filepath(output_dir: str, experiment: str, + well: str, planarian: int, record_type: str) -> str: + """ + Construit le chemin du fichier CSV de sortie. + Nom : __planaire_.csv + + Args: + output_dir : répertoire de sortie (créé si absent) + experiment : identifiant de l'expérience + well : identifiant du puits + planarian : index du planaire + record_type : "frame" ou "summary" + + Returns: + chemin absolu du fichier CSV + """ + dirpath = os.path.abspath(output_dir) + os.makedirs(dirpath, exist_ok=True) + filename = f"{experiment}_{well}_planaire{planarian:02d}_{record_type}.csv" + return os.path.join(dirpath, filename) + + async def export_csv( + self, + experiment: str, + well: str, + planarian: int = 0, + record_type: str = "metrics", + output_dir: str = ".", + start: Optional[datetime] = None, + stop: Optional[datetime] = None, + ) -> tuple: + """ + Exporte les données depuis ReductStore vers un fichier CSV. + Le répertoire de sortie est choisi via output_dir. + Le champ timestamp est converti en ISO 8601 UTC. + + Args: + experiment : identifiant de l'expérience + well : identifiant du puits + planarian : index du planaire + record_type : "frame" | "summary" + output_dir : répertoire de sortie (défaut : répertoire courant) + start, stop : plage temporelle (datetime UTC, optionnel) + + Returns: + tuple (filepath, nb_lignes) + """ + records = await self.get_tracking_data( + experiment, well, planarian, record_type, start, stop) + if not records: + logger.warning(f"Aucune donnée pour {experiment}/{well}/{planarian}") + return "", 0 + + records = self._convert_timestamps(records) + filepath = self._build_filepath(output_dir, experiment, well, + planarian, record_type) + fieldnames = list(dict.fromkeys(k for r in records for k in r.keys())) + + with open(filepath, "w", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore") + writer.writeheader() + writer.writerows(records) + + logger.info(f"Export CSV : {len(records)} lignes → {filepath}") + return filepath, len(records) + + async def export_csv_response( + self, + experiment: str, + well: str, + planarian: int = 0, + record_type: str = "metrics", + start: Optional[datetime] = None, + stop: Optional[datetime] = None, + ) -> tuple: + """ + Génère le contenu CSV en mémoire (pour réponse HTTP Django). + Le champ timestamp est converti en ISO 8601 UTC. + + Returns: + tuple (contenu_csv_str, nb_lignes) + """ + records = await self.get_tracking_data( + experiment, well, planarian, record_type, start, stop) + if not records: + return "", 0 + records = self._convert_timestamps(records) + fieldnames = list(dict.fromkeys(k for r in records for k in r.keys())) + out = io.StringIO() + writer = csv.DictWriter(out, fieldnames=fieldnames, extrasaction="ignore") + writer.writeheader() + writer.writerows(records) + return out.getvalue(), len(records) + + async def close(self): + """ + Ferme la connexion ReductStore. + Note : reduct-py >= 1.x ne nécessite pas de fermeture explicite — + la méthode est conservée pour compatibilité d'interface. + """ + self._client = None + self._bucket = None + logger.info("ReductStore déconnecté") + diff --git a/test_tube_scanner/planarian/views.py b/test_tube_scanner/planarian/views.py index c7565b1..5c5529f 100644 --- a/test_tube_scanner/planarian/views.py +++ b/test_tube_scanner/planarian/views.py @@ -21,7 +21,7 @@ from modules.planarian_metrics import ExperimentParams, ReductStoreClient from modules.system_stats import get_cached_stats, start_background_updater from scanner.constants import ScannerConstants from .tasks import export_experiment_metrics, export_session_metrics -from scanner import models +from scanner import models, views as scanner_views from .models import ExperimentConfig @@ -64,30 +64,17 @@ def global_context(request, **ctx): ) -def get_active_experiments(session, expid=None): - if session: - experiments = models.SessionExperiment.experiment_by_session(session.id, active=True) or [] - eid = [str(e.id) for e in experiments] - if experiments and not expid or expid not in eid: - return experiments, experiments[0] - - for e in experiments: - if expid == str(e.id): - return experiments, e - return [], None - - def get_active_session(request, session_id=None, experiment_id=None): cursid = session_id or request.POST.get('_sid') expid = experiment_id or request.POST.get('_expid') current_session = models.Session.get_session(cursid) - experiments, current_experiment = get_active_experiments(current_session, expid) + experiments, current_experiment = scanner_views.get_not_active_experiments(current_session, expid) context = dict( current_session = current_session, current_experiment = current_experiment, experiments=experiments or [], - sessions=models.Session.objects.filter(active=True).all(), + sessions=models.Session.objects.filter(active=False).all(), well_choices=models.Well.objects.order_by('name').all(), ) return context @@ -118,6 +105,8 @@ def export_metrics(request): def export_csv(request): d = request.POST + print("export_csv===========", d) + @async_to_sync async def _do_export(): client = _get_reduct_client() @@ -141,7 +130,7 @@ def export_csv(request): csv_content, n = _do_export() logger.info(f"Export CSV: {n} lignes, content size={len(csv_content)}") filename = ( - f"{d['experiment']}_{d['well']}-planaire{d['planarian']}" + f"{d['experiment']}_{d['well']}-planaire-{d['planarian']}" f"_{d['record_type']}.csv" ) return csv_content, filename diff --git a/test_tube_scanner/scanner/models.py b/test_tube_scanner/scanner/models.py index 8d0be6c..03f6bc7 100644 --- a/test_tube_scanner/scanner/models.py +++ b/test_tube_scanner/scanner/models.py @@ -53,7 +53,8 @@ class Configuration(models.Model): grbl_xmax = models.FloatField(_("Grbl Xmax"), help_text=_("CNC Grbl Xmax en mm"), blank=False, default=350.0) grbl_ymax = models.FloatField(_("Grbl Ymax"), help_text=_("CNC Grbl Ymax en mm"), blank=False, default=250.0) # camera configuration - capture_type = models.CharField(_("Capture"), help_text=_("Type de capture"), default='rpi', max_length=8, choices=CAPTURE_TYPE, null=True, blank=False) + capture_type = models.CharField(_("Capture"), help_text=_("Type de capture. Nécessite un redémarrage en cas de modification à chaud!"), default='rpi', max_length=8, choices=CAPTURE_TYPE, null=True, blank=False) + webcam_device_index = models.PositiveSmallIntegerField(_("Index de la webcam"), help_text=_("Index de la webcam (0, 1, ...) si présente"), default=2) image_quality = models.PositiveSmallIntegerField(_("Qualité JPEG"), help_text=_("Qualité JPEG (1-100) pour les images exportées"), default=90) video_jpeg_quality = models.PositiveSmallIntegerField(_("Qualité JPEG pour les vidéos"), help_text=_("Qualité JPEG (1-100) pour les images extraites des vidéos"), default=90) @@ -278,8 +279,8 @@ class Session(models.Model): name = models.CharField(_("Nom de la session"), help_text=_("Session d'expérience. 4 Multi-puits maximum"), max_length=100, null=True, blank=False) author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True) active = models.BooleanField(_("Active"), default=True) - expected_export = models.DateTimeField(_("Date d'exportation"), help_text=_("Date d'exportation prévue"), null=True, blank=True) - expected_scanning = models.DateTimeField(_("Date du balayage"), help_text=_("Date du balayage prévue"), null=True, blank=True) + expected_export = models.DateTimeField(_("Exportation auto"), help_text=_("Date d'exportation prévue"), null=True, blank=True) + expected_scanning = models.DateTimeField(_("Bbalayage auto"), help_text=_("Date du balayage prévue"), null=True, blank=True) created = models.DateTimeField(_("Date de création"), default=timezone.now) finished = models.DateTimeField (_("Date de fin"), null=True, blank=True) diff --git a/test_tube_scanner/scanner/multiwell.py b/test_tube_scanner/scanner/multiwell.py index b45b71d..5d67132 100644 --- a/test_tube_scanner/scanner/multiwell.py +++ b/test_tube_scanner/scanner/multiwell.py @@ -238,6 +238,9 @@ class MultiWellManager: def _start_scanning(self, session, experiments, simulate=False): result = False try: + conf = self.process.cam.get_config() + self.process.cam.use_tracking = conf.use_tracking + self.process.cam._aligner.debug = False self.stop_playing.clear() diff --git a/test_tube_scanner/scanner/process.py b/test_tube_scanner/scanner/process.py index 0690973..36f796d 100644 --- a/test_tube_scanner/scanner/process.py +++ b/test_tube_scanner/scanner/process.py @@ -160,27 +160,37 @@ class ScannerProcess(Task): self.data = ProcessData() self.manager = None self.recordDB = CameraRecordManager(cameraDB) + self.metricDB = planarianDB def __call__(self, *args, **kwargs): return self.start(*args, **kwargs) def set_crop_radius(self, radius): return CircularCrop(radius=radius, strategy=CropStrategy.CROP_JPEG, jpeg_quality=self.image_quality) + + def get_config(self): + ''' + Constants: + reset si besoin les constantes vidéo + ''' + self.conf = ScannerConstants().get() + self.use_tracking = self.conf.tracking + + self.video_quality = self.conf.video_jpeg_quality + self.image_quality = self.conf.image_quality + self.video_fps = self.conf.video_frame_rate + self.video_width = self.conf.video_width_capture + self.video_height = self.conf.video_height_capture + self.crop_radius = self.conf.calibration_crop_radius + + self.video_jpg_quality = [int(cv2.IMWRITE_JPEG_QUALITY), self.video_quality] + self.image_jpg_quality = [int(cv2.IMWRITE_JPEG_QUALITY), self.image_quality] + return self.conf + def start(self, *args, **kwargs): try: - self.conf = ScannerConstants().get() - self.use_tracking = self.conf.tracking - - self.video_quality = self.conf.video_jpeg_quality - self.image_quality = self.conf.image_quality - self.video_fps = self.conf.video_frame_rate - self.video_width = self.conf.video_width_capture - self.video_height = self.conf.video_height_capture - self.crop_radius = self.conf.calibration_crop_radius - - self.video_jpg_quality = [int(cv2.IMWRITE_JPEG_QUALITY), self.video_quality] - self.image_jpg_quality = [int(cv2.IMWRITE_JPEG_QUALITY), self.image_quality] + self.get_config() self.grbl_xmax = self.conf.grbl_xmax self.grbl_ymax = self.conf.grbl_ymax