Pymecavideo 8.0
Étude cinématique à l'aide de vidéos
pointageWidget.py
1# -*- coding: utf-8 -*-
2
3"""
4 pointageWidget, a module for pymecavideo:
5 a program to track moving points in a video frameset
6
7 Copyright (C) 2007 Jean-Baptiste Butet <ashashiwa@gmail.com>
8 Copyright (C) 2023 Georges Khaznadar <georgesk@debian.org>
9
10 This program is free software: you can redistribute it and/or modify
11 it under the terms of the GNU General Public License as published by
12 the Free Software Foundation, either version 3 of the License, or
13 (at your option) any later version.
14
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with this program. If not, see <http://www.gnu.org/licenses/>.
22"""
23
24from PyQt6.QtCore import QThread, pyqtSignal, QLocale, QTranslator, Qt, \
25 QSize, QTimer, QObject, QRect, QPoint, QPointF, QEvent
26from PyQt6.QtGui import QKeySequence, QIcon, QPixmap, QImage, QPainter, \
27 QCursor, QPen, QColor, QFont, QResizeEvent, QShortcut
28from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget, QLayout, \
29 QFileDialog, QTableWidgetItem, QInputDialog, QLineEdit, QMessageBox, \
30 QTableWidgetSelectionRange
31
32import os, time, re, sys
33import locale
34
35from version import Version
36from vecteur import vecteur
37from echelle import Echelle_TraceWidget
38from image_widget import ImageWidget
39from pointage import Pointage
40from globdef import cible_icon, DOCUMENT_PATH, inhibe, pattern_float
41from cadreur import openCvReader
42from toQimage import toQImage
43from suivi_auto import SelRectWidget
44from detect import filter_picture
45from dbg import Dbg
46from suivi_auto import SelRectWidget
47from choix_origine import ChoixOrigineWidget
48from pointage import Pointage
49from etatsPointage import Etats
50from echelle import EchelleWidget, echelle
51from detect import filter_picture
52
53import interfaces.icon_rc
54
55from interfaces.Ui_pointage import Ui_pointageWidget
56
58 """
59 Une classe qui affiche l'image d'une vidéo et qui gère le pointage
60 d'objets mobiles dans cette vidéo. Elle gère les données de pointage
61 et celles liées aux échelles de temps et d'espace
62
63 paramètres du constructeur :
64 @param parent un QWidget parent
65 @param verbosite entre 1 et 3 pour les messages de débogage
66 """
67
68 def __init__(self, parent, verbosite = 0):
69 self.dbg = Dbg(verbosite)
70 QWidget.__init__(self, parent)
71 Ui_pointageWidget.__init__(self)
72 Pointage.__init__(self)
73 Etats.__init__(self)
74 self.setupUi(self)
75
76 self.app = None # la fenêtre principale
77 self.dbg = None # le débogueur
78 self.etatetat = "A" # état initial ; différent de "debut"
79 self.image = None # l'image tirée du film
80 self.origineorigine = vecteur() # origine du repère
81 self.image_max = None # numéro de la dernière image de la vidéo
82 self.framerate = None # nombre d'images par seconde
83 # dimensions natives des images de la vidéo
84 self.largeurFilm, self.hauteurFilm = None, None
85 self.a_une_image = False # indication quant à une image disponible
86 self.imageExtraite = None # référence de l'image courante
87 self.lance_capturelance_capture = False # un pointage est en cours
88 self.echelle_trace = None # widget pour tracer l'échelle
89 self.selRect = None # un objet gérant la sélection par rectangle
90 self.lance_cature = False # devient vrai quand on commence à pointer
91 self.auto = False # devient vrai pour le pointage automatique
92 self.motifs_auto = [] # liste de motifs pour le suivi auto
93 self.pointsProbables = {} # dict. de points proches de la détection ?
94 self.refait_point = False # on doit repointer une date
95 self.pointageOK = False # il est possible de faire un pointage
96 self.filename = None # nom du fichier video
97 self.selRect = None # sélecteur de zones à suivre
98 self.indexMotif = 0 # le numéro du motif à entourer
99 self.pileDeDetections = [] # pile d'index d'images où détecter
100
101 # fait un beau gros curseur
102 cible_pix = QPixmap(cible_icon).scaledToHeight(32)
103 self.pointageCursor = QCursor(cible_pix)
104
105 self.connecte_ui()
106 self.connecte_signaux()
107 return
108
109 ########### signaux #####################
110 update_imgedit = pyqtSignal(int, int, int) # met à jour la dimension d'image
111 update_origine = pyqtSignal(float, float) # met à jour l'origine
112 dimension_data = pyqtSignal(int) # redimensionne les données
113 stopCalculs = pyqtSignal() # arrete le pointage auto
114 label_zoom = pyqtSignal(str) # change le label du zoom
115 update_zoom = pyqtSignal(vecteur) # agrandit une portion d'image
116 echelle_modif = pyqtSignal(str, str) # modifie le bouton d'échelle
117 apres_echelle = pyqtSignal() # après dénition de l'échelle
118 selection_motif_done = pyqtSignal() # prêt à commencer la détection
119 fin_pointage = pyqtSignal() # après un pointage
120 fin_pointage_manuel = pyqtSignal(QEvent) # après un pointage manuel
121 stop_n = pyqtSignal(str) # refait le texte du bouton STOP
122 change_axe_origine = pyqtSignal() # inverse un des axes du repère
123 sens_axes = pyqtSignal(int, int) # coche les cases des axes
124 montre_etalon = pyqtSignal() # montre l'étalon utilisé
125
126 ########### connexion des signaux #######
128 self.update_imgedit.connect(self.affiche_imgsize)
129 self.update_origine.connect(self.updateOrigine)
130 self.dimension_data.connect(self.redimensionne_data)
131 self.stopCalculs.connect(self.stopComputing)
132 self.label_zoom.connect(self.labelZoom)
133 self.update_zoom.connect(self.loupe)
134 self.echelle_modif.connect(self.setButtonEchelle)
135 self.apres_echelle.connect(self.restaureEtat)
136 self.fin_pointage.connect(self.termine_pointage)
138 self.selection_motif_done.connect(self.suiviDuMotif)
139 self.stop_n.connect(self.stop_setText)
140 self.sens_axes.connect(self.coche_axes)
141 self.montre_etalon.connect(self.feedbackEchelle)
142
143 return
144
145 def connecte_ui(self):
146 """
147 Connecte des signaux issus de l'UI
148 """
149 self.lineEdit_IPS.textChanged.connect(self.verifie_IPS)
150 self.spinBox_objets.valueChanged.connect(self.dimension_data)
151 self.pushButton_defait.clicked.connect(self.efface_point_precedent)
152 self.pushButton_refait.clicked.connect(self.refait_point_suivant)
153 self.Bouton_Echelle.clicked.connect(self.demande_echelle)
154 self.Bouton_lance_capture.clicked.connect(self.debut_capture)
155 self.pushButton_reinit.clicked.connect(self.reinitialise_capture)
156 self.pushButton_origine.clicked.connect(
157 self.nouvelle_origine)
158 self.checkBox_abscisses.stateChanged.connect(self.change_sens_X)
159 self.checkBox_ordonnees.stateChanged.connect(self.change_sens_Y)
160 self.pushButton_rot_droite.clicked.connect(self.tourne_droite)
161 self.pushButton_rot_gauche.clicked.connect(self.tourne_gauche)
162 self.pushButton_stopCalculs.clicked.connect(self.stopComputing)
163
164 return
165
166 def setApp(self, app):
167 """
168 Crée une relation avec la fenêtre principale, son débogueur et
169 ses préférences
170 @param app le fenêtre principale (QMainWindoWidget)
171 """
172 self.app = app
173 self.dbg = app.dbg
174 self.prefs = app.prefs
175 self.video.setParent(self)
176 self.zoom_zone.setApp(app)
177 self.change_axe_origine.connect(self.app.egalise_origine)
178 return
179
180
181 def updateOrigine(self, rx, ry):
182 """
183 Met à jour l'origine
184 @param rx ratio horizontal
185 @param ry ratio vertical
186 """
187 self.origineorigine = vecteur(self.origineorigine.x * rx, self.origineorigine.y * ry)
188 return
189
190 def affiche_imgsize(self, w, h, r):
191 """
192 Affiche la taille de l'image
193 @param w largeur de l'image
194 @param h hauteur de l'image
195 @param r rotation de l'image
196 """
197 self.imgdimEdit.setText(f"{w} x {h} ({r}°)")
198 return
199
200 def openTheFile(self, filename):
201 """
202 Ouvre le fichier de nom filename, enregistre les préférences de
203 fichier vidéo.
204 @param filename nom du fichier
205 """
206 self.dbg.p(2, "rentre dans 'openTheFile'")
207 if not filename :
208 return
209 self.filename = filename
210 if self.init_cvReader():
211 # le fichier vidéo est OK, et son format est reconnu
212 self.init_image()
213 # s'il y avait déjà une échelle, il faut l'oublier,
214 # quitter l'état A pour y revenir
215 if self.echelle_image:
216 self.clearEchelle()
217 self.app.change_etat.emit("debut")
218 self.app.change_etat.emit("A")
219 else:
220 QMessageBox.warning(
221 None,
222 self.tr("Erreur lors de la lecture du fichier"),
223 self.tr("Le fichier<b>{0}</b> ...\nn'est peut-être pas dans un format vidéo supporté.").format(
224 filename))
225 return
226
227 def apply_preferences(self, rouvre=False):
228 """
229 Récupère les préférences sauvegardées, et en applique les données
230 ici on s'occupe de ce qui se gère facilement au niveau du widget
231 video
232 @param rouvre est vrai quand on ouvre un fichier pymecavideo ;
233 il est faux par défaut
234 """
235 self.dbg.p(2, "rentre dans 'VideoWidget.apply_preferences'")
236 d = self.app.prefs.config["DEFAULT"]
237 self.filename = d["lastvideo"]
238 self.video.rotation = d.getint("rotation")
239 if os.path.isfile(self.filename):
240 self.openTheFile(self.filename)
241 else:
242 # si le fichier video n'existe pas, inutile d'aller plus
243 # loin dans la restauration des données !
244 return
246 self.sens_Xsens_X = d.getint("sens_x")
247 self.sens_Ysens_Y = d.getint("sens_y")
248 self.origineorigine = self.app.prefs.config.getvecteur("DEFAULT", "origine")
249 if rouvre:
250 # on est en train de réouvrir un fichier pymecavideo
251 # et on considère plus de données
252 #!!!! self.premiere_image_pointee = d.getint("index_depart")
253 self.deltatT = d.getfloat("deltat")
254 self.dimensionne(d.getint("nb_obj"), self.deltaTdeltaT, self.image_max)
255 self.echelle_image.longueur_reelle_etalon = d.getfloat('etalon_m')
256 p1 = self.app.prefs.config.getvecteur("DEFAULT", "etalon_org")
257 p2 = self.app.prefs.config.getvecteur("DEFAULT", "etalon_ext")
258 if p1 != vecteur(0,0) and p2 != vecteur(0,0):
259 self.echelle_image.p1 = p1
260 self.echelle_image.p2 = p2
261 else:
262 self.echelle_image.p1 = None
263 self.echelle_image.p2 = None
264 # coche les cases du sens des axes
265 self.sens_axes.emit(self.sens_Xsens_X, self.sens_Ysens_Y)
266 return
267
268 def init_cvReader(self):
269 """
270 Initialise le lecteur de flux vidéo pour OpenCV
271 et recode la vidéo si nécessaire.
272 """
273 self.dbg.p(2, "rentre dans 'init_cvReader', ouverture de %s" %
274 (self.filename))
275 self.cvReader = openCvReader(self.filename)
276 time.sleep(0.1)
277 if not self.cvReader.ok:
278 QMessageBox.warning(
279 None,
280 self.tr("Format vidéo non pris en charge"),
281 self.tr("Le format de cette vidéo n'est pas pris en charge par pymecavideo"))
282 else:
283 return True
284
285 def init_image(self):
286 """
287 initialise certaines variables lors le la mise en place d'une
288 nouvelle vidéo
289 """
290 self.dbg.p(2, "rentre dans 'init_image'")
291 self.index = 1
292 self.extract_image(1)
293 self.framerate, self.image_max, self.largeurFilm, self.hauteurFilm = \
294 self.cvReader.recupere_avi_infos(self.video.rotation)
295 self.ratio = self.largeurFilm / self.hauteurFilm
296 self.calcul_deltaT()
297 # on dimensionne les données pour les pointages
298 self.redimensionne_data(self.nb_obj)
299 self.affiche_image()
300 return
301
302 def extract_image(self, index):
303 """
304 extrait une image de la video à l'aide d'OpenCV ; met à jour
305 self.pointageOK s'il est licite de pointer dans l'état actuel;
306 met à jour le curseur à utiliser aussi
307 @param index le numéro de l'image (commence à 1)
308
309 @return un boolen (ok), et l'image au format d'openCV ; l'image
310 au bon format pour Qt est dans self.imageExtraite
311 """
312 self.dbg.p(2, "rentre dans 'extract_image' " + 'index : ' + str(index))
313 ok, image_opencv = self.cvReader.getImage(index, self.video.rotation)
314 if not ok:
315 self.app.affiche_statut.emit(
316 self.tr("Pymecavideo n'arrive pas à lire l'image"))
317 return False, None
318 self.a_une_image = ok
319 self.imageExtraite = toQImage(image_opencv)
320 # il est licite de pointer dans l'état D à condition
321 # qu'il n'y ait encore aucun pointage, ou que l'index soit
322 # connexe aux pointages existants
323 self.pointageOK = \
324 self.etatetat in ("D", "E") and \
325 (not self or index in range(self.premiere_image() - 1, \
326 self.derniere_image() + 2))
327 if self.pointageOK:
328 # beau gros curseur seulement si le pointage est licite ;
329 self.video.setCursor(self.pointageCursor)
330 else:
331 self.video.setCursor(Qt.CursorShape.ArrowCursor)
332 return ok, image_opencv
333
334 def calcul_deltaT(self, ips_from_line_edit=False, rouvre=False):
335 """
336 Détermination de l'intervalle de temps entre deux images.
337 Cela modifie self.deltaTdeltaT
338
339 @param ips_from_line_edit (faux par défaut) indique qu'on lit
340 deltaT depuis un champ de saisie
341 @param rouvre (faux par défaut) indique qu'on lit les données depuis
342 un fichier pymecavidéo
343 """
344 self.dbg.p(2, "rentre dans 'calcul_deltaT'")
345 if rouvre:
346 # se produit quand on lit un deltaT depuis un fichier mecavideo
347 IPS = round(1/self.deltaTdeltaT)
348 self.app.lineEdit_IPS.setText(str(IPS))
349 else:
350 if not ips_from_line_edit:
351 self.deltaTdeltaT = 1 / self.framerate
352 # mets à jour le widget contenant les IPS
353 self.lineEdit_IPS.setText(str(self.framerate))
354 else:
355 IPS = int(self.lineEdit_IPS.text())
356 self.framerate = IPS
357 self.deltaTdeltaT = 1 / IPS
358 return
359
360 def redimensionne_data(self, dim):
361 """
362 redimensionne self.data (fonction de rappel connectée au signal
363 self.dimension_data)
364 @param dim la nouvelle dimension des données
365 (en nombre d'objets à suivre)
366 """
367 self.dbg.p(2, "rentre dans 'redimensionne_data'")
368 if self.image_max and self.deltaTdeltaT:
369 self.dimensionne(dim, self.deltaTdeltaT, self.image_max)
370 # self.app.cree_tableau(nb_suivis = self.nb_obj)
371 return
372
373 def verifie_IPS(self):
374 self.dbg.p(2, "rentre dans 'verifie_IPS'")
375 # si ce qui est rentré n'est pas un entier
376 if not self.lineEdit_IPS.text().isdigit() and len(self.lineEdit_IPS.text()) > 0:
377 QMessageBox.warning(
378 self,
379 self.tr("Le nombre d'images par seconde doit être un entier"),
380 self.tr("merci de recommencer"))
381 else:
382 # la vérification est OK, donc on modifie l'intervalle de temps
383 # et on refait le tableau de données sans changer le nombre
384 # d'objets
385 self.framerate = int(self.lineEdit_IPS.text())
386 self.deltaTdeltaT = 1 / self.framerate
387 self.redimensionne_data(self.nb_obj)
388 return
389
390 def demande_echelle(self):
391 """
392 demande l'échelle interactivement
393 """
394 self.dbg.p(2, "rentre dans 'demande_echelle'")
395 reponse, ok = QInputDialog.getText(
396 None,
397 self.tr("Définir léchelle"),
398 self.tr("Quelle est la longueur en mètre de votre étalon sur l'image ?"),
399 text = f"{self.echelle_image.longueur_reelle_etalon:.3f}")
400 reponse = reponse.replace(",", ".")
401 ok = ok and pattern_float.match(reponse) and float(reponse) > 0
402 if not ok:
403 self.affiche_statut.emit(self.tr(
404 "Merci d'indiquer une échelle valable : {} ne peut pas être converti en nombre.").format(reponse))
405 self.demande_echelle()
406 return
407 reponse = float(reponse)
408 self.echelle_image.etalonneReel(reponse)
409 self.app.change_etat.emit("C")
410 job = EchelleWidget(self.video, self)
411 job.show()
412 return
413
414 def debut_capture(self):
415 """
416 Fonction de rappel du bouton Bouton_lance_capture
417
418 Passe à l'état D ou AB, selon self.checkBox_auto
419 """
420 self.app.change_etat.emit(
421 "AB" if self.checkBox_auto.isChecked() else "D")
422 return
423
424 def nouvelle_origine(self):
425 """
426 Permet de déplacer l'origine du référentiel de la caméra
427 """
428 self.dbg.p(2, "rentre dans 'nouvelle_origine'")
429 nvl_origine = QMessageBox.information(
430 self,
431 self.tr("NOUVELLE ORIGINE"),
432 self.tr("Choisissez, en cliquant sur la vidéo le point qui sera la nouvelle origine"))
433 ChoixOrigineWidget(self.video, self).show()
434 return
435
436 def change_sens_X(self):
437 self.dbg.p(2, "rentre dans 'change_sens_X'")
438 if self.checkBox_abscisses.isChecked():
439 self.sens_Xsens_X = -1
440 else:
441 self.sens_Xsens_X = 1
442 self.video.update()
443 self.change_axe_origine.emit()
444
445 def change_sens_Y(self):
446 self.dbg.p(2, "rentre dans 'change_sens_Y'")
447 if self.checkBox_ordonnees.isChecked():
448 self.sens_Ysens_Y = -1
449 else:
450 self.sens_Ysens_Y = 1
451 self.video.update()
452 self.change_axe_origine.emit()
453
454 def tourne_droite(self):
455 self.dbg.p(2, "rentre dans 'tourne_droite'")
456 self.tourne_image("droite")
457
458 def tourne_gauche(self):
459 self.dbg.p(2, "rentre dans 'tourne_droite'")
460 self.tourne_image("gauche")
461
462 def tourne_image(self, sens):
463 self.dbg.p(2, "rentre dans 'tourne_image'")
464 if sens == "droite":
465 increment = 90
466 elif sens == "gauche":
467 increment = -90
468 self.video.rotation = (self.video.rotation + increment) % 360
469 self.dbg.p(2, "Dans 'tourne_image' self rotation vaut" +
470 str(self.video.rotation))
471
472 # gestion de l'origine et de l'échelle :
473 self.dbg.p(3, f"Dans 'tourne_image' avant de tourner, self.origine {self.origine}, largeur video {self.video.width()}, hauteur video {self.video.height()}")
474 self.redimensionneSignal.emit(True)
475 return
476
477 def affiche_image(self, index= None):
478 '''
479 À condition qu'on ait ouvert le fichier vidéo,
480 extrait l'image courante ou l'image spécifiée
481 par l'index, et affiche cette image
482 @param index permet de modifier l'image courante si c'est un entier
483 (None par défaut)
484 '''
485 if index is not None: self.index = index
486 if not self.filename or self.index is None or self.image_max is None:
487 return
488 self.dbg.p(2, f"rentre dans 'affiche_image' self.index = {self.index} self.image_max = {self.image_max}")
489 if self.index <= self.image_max:
490 self.extract_image(self.index) # 2ms
491 self.video.placeImage(
492 self.imageExtraite, self.ratio, self.largeurFilm)
493 self.reinit_origine()
494 elif self.index > self.image_max:
495 self.index = self.image_max
496 self.lance_capturelance_capture = False
497 return
498
499 def reinit_origine(self):
500 """
501 Replace l'origine au centre de l'image
502 """
503 self.origineorigine = vecteur(self.video.image_w//2, self.video.image_h//2)
504 return
505
506 def reinitialise_capture(self):
507 """
508 Efface toutes les données de la capture en cours et prépare une nouvelle
509 session de capture. Retourne à l'état A
510 """
511 self.dbg.p(2, "rentre dans 'reinitialise_capture'")
512 # oublie self.echelle_image
513 self.clearEchelle()
514 if self.echelle_trace is not None:
515 self.echelle_trace.hide()
516 self.echelle_trace = None
517 self.echelle_modif.emit(self.tr("Définir l'échelle"),
518 "background-color:None;")
519 self.index = 1
520 self.remontre_image()
521 # reinitialisation du widget video
522 self.setCursor(Qt.CursorShape.ArrowCursor)
523 self.setEnabled(True)
524 self.reinit_origine()
525 self.pointsProbables = {}
526 self.motifs_auto = []
527 # retire les objets déjà pointés
528 self.redimensionne_data(self.nb_obj)
529 self.sens_Xsens_X = self.sens_Ysens_Y = 1
530 self.sens_axes.emit(self.sens_Xsens_X, self.sens_Ysens_Y)
531 self.app.change_etat.emit("A")
532 return
533
534 def remontre_image(self):
535 """
536 Il peut être nécessaire de remontrer l'image après un changement
537 de self.video.rotation
538 """
539 self.extract_image(self.index)
540 self.framerate, self.image_max, self.largeurFilm, self.hauteurFilm = \
541 self.cvReader.recupere_avi_infos(self.video.rotation)
542 self.ratio = self.largeurFilm / self.hauteurFilm
543 self.affiche_image()
544 return
545
546 def efface_point_precedent(self):
547 """
548 revient au point précédent
549 """
550 self.dbg.p(2, "rentre dans 'efface_point_precedent'")
551 if inhibe("defaire",100): return # corrige un bug de Qt 5.15
552 if not self.peut_defaire(): return
553 self.defaire()
554 # dernière image à afficher
555 der = self.derniere_image()
556 if der:
557 self.index = self.derniere_image() + 1
558 else:
559 self.purge_defaits() # si on a retiré le dernier pointage visible
560 if self.index > 1: self.index -= 1
561 self.prepare_futur_clic()
562 return
563
564 def refait_point_suivant(self):
565 """rétablit le point suivant après un effacement
566 """
567 self.dbg.p(2, "rentre dans 'refait_point_suivant'")
568 if inhibe("refaire",100): return # corrige un bug de Qt 5.15
569 self.refaire()
570 # ce serait moins long de remettre juste une ligne dans le tableau
571 self.app.recalculLesCoordonnees()
572 if self.index < self.image_max:
573 self.index += 1
574 self.prepare_futur_clic()
575 return
576
577 def enregistre_ui(self):
578 self.dbg.p(2, "rentre dans 'enregistre_ui'")
579 if self.data and self.echelle_image:
580 base_name = os.path.splitext(os.path.basename(self.filename))[0]
581 defaultName = os.path.join(DOCUMENT_PATH, base_name+'.mecavideo')
582 fichier = QFileDialog.getSaveFileName(
583 self,
584 self.tr("Enregistrer le projet pymecavideo"),
585 defaultName,
586 self.tr("Projet pymecavideo (*.mecavideo)"))
587 self.enregistre(fichier[0])
588 else :
589 QMessageBox.critical(None, self.tr("Erreur lors de l'enregistrement"), self.tr("Il manque les données, ou l'échelle"))
590 return
591
592 def enregistre(self, fichier):
593 """
594 Enregistre les données courantes dans un fichier,
595 à un format CSV, séparé par des tabulations
596 """
597 self.dbg.p(2, "rentre dans 'enregistre'")
598 sep_decimal = "."
599 if locale.getdefaultlocale()[0][0:2] == 'fr':
600 # en France, le séparateur décimal est la virgule
601 sep_decimal = ","
602 if not fichier:
603 return
604 # mise à jour des préférences, afin d'en faire aussi l'en-tête
605 # pour le fichier pymecavideo
606 self.savePrefs()
607 with open(fichier, 'w') as outfile:
608 message = self.tr("temps en seconde, positions en mètre")
609 outfile.write(self.entete_fichier(message))
610 donnees = self.csv_string(
611 sep = "\t", unite = "m",
612 debut = self.premiere_image(),
613 origine = self.origineorigine
614 ).replace(".",sep_decimal)
615 outfile.write(donnees)
616 return
617
618 def savePrefs(self):
619 d = self.app.prefs.defaults
620 d['version'] = f"pymecavideo {Version}"
621 d['proximite'] = str(self.app.radioButtonNearMouse.isChecked())
622 d['lastvideo'] = self.filename
623 d['videodir'] = os.path.dirname(self.filename)
624 d['niveaudbg'] = str(self.dbg.verbosite)
625 d['sens_x'] = str(self.sens_Xsens_X)
626 d['sens_y'] = str(self.sens_Ysens_Y)
627 d["taille_image"] = f"({self.image_w},{self.image_h})"
628 d['rotation'] = str(self.video.rotation)
629 d['origine'] = f"({round(self.origine.x)}, {round(self.origine.y)})"
630 d['index_depart'] = str(self.premiere_image())
631 d['etalon_m'] = str(self.echelle_image.longueur_reelle_etalon)
632 d['etalon_px'] = str(self.echelle_image.longueur_pixel_etalon())
633 d['etalon_org'] = self.echelle_image.p1.toIntStr() \
634 if self.echelle_image else "None"
635 d['etalon_ext'] = self.echelle_image.p2.toIntStr() \
636 if self.echelle_image else "None"
637 d['deltat'] = str(self.deltaTdeltaT)
638 d['nb_obj'] = str(len(self.suivis))
639 self.app.prefs.save()
640
641 def stopComputing(self):
642 self.dbg.p(2, "rentre dans 'stopComputing'")
643 self.pileDeDetections = [] # vide la liste des points à détecter encore
644 # la routine self.detecteUnPoint reviendra à l'état D après que
645 # le dernier objet aura été détecté
646 return
647
648 def labelZoom(self, label):
649 """
650 Met à jour le label au-dessus du zoom
651 @param label le nouveau label
652 """
653 self.zoomLabel.setText(label)
654 return
655
656 def imgControlImage(self, state):
657 """
658 Gère les deux widgets horizontalSlider et spinBox_image
659 @param state si state == True, les deux widgets sont activés
660 et leurs signaux valueChanged sont pris en compte ;
661 sinon ils sont désactivés ainsi que les signaux valueChanged
662 """
663 self.dbg.p(2, "rentre dans 'imgControlImage'")
664 if state:
665 self.horizontalSlider.setMinimum(1)
666 self.spinBox_image.setMinimum(1)
667 if self.image_max:
668 self.horizontalSlider.setMaximum(int(self.image_max))
669 self.spinBox_image.setMaximum(int(self.image_max))
670 self.horizontalSlider.valueChanged.connect(
672 self.spinBox_image.valueChanged.connect(self.sync_spinbox2others)
673 else:
674 if self.horizontalSlider.receivers(self.horizontalSlider.valueChanged):
675 self.horizontalSlider.valueChanged.disconnect()
676 if self.spinBox_image.receivers(self.spinBox_image.valueChanged):
677 self.spinBox_image.valueChanged.disconnect()
678 self.horizontalSlider.setEnabled(state)
679 self.spinBox_image.setEnabled(state)
680 return
681
682 def affiche_echelle(self):
683 """
684 affiche l'échelle courante pour les distances sur l'image
685 """
686 self.dbg.p(2, "rentre dans 'affiche_echelle'")
687 if self.echelle_image.isUndef():
688 self.echelleEdit.setText(
689 self.tr("indéf."))
690 else:
691 epxParM = self.echelle_image.pxParM()
692 if epxParM > 20:
693 self.echelleEdit.setText("%.1f" % epxParM)
694 else:
695 self.echelleEdit.setText("%8e" % epxParM)
696 self.echelleEdit.show()
697 self.Bouton_Echelle.show()
698 return
699
700 def enableDefaire(self, value):
701 """
702 Contrôle la possibilité de défaire un clic
703 @param value booléen
704 """
705 self.dbg.p(2, "rentre dans 'enableDefaire, %s'" % (str(value)))
706 self.pushButton_defait.setEnabled(value)
707 self.app.actionDefaire.setEnabled(value)
708 # permet de remettre l'interface à zéro
709 if not value:
710 self.imgControlImage(True)
711 return
712
713 def enableRefaire(self, value):
714 """
715 Contrôle la possibilité de refaire un clic
716 @param value booléen
717 """
718 self.dbg.p(2, "rentre dans 'enableRefaire, %s'" % (value))
719 self.pushButton_refait.setEnabled(value)
720 self.app.actionRefaire.setEnabled(value)
721 return
722
723 def loupe(self, position):
724 """
725 Agrandit deux fois une partie de self.video.image et la met
726 dans la zone du zoom, puis met à jour les affichages de coordonnées ;
727 sauf dans l'état B (pointage auto)
728 @param position le centre de la zone à agrandir
729 """
730 if self.etatetat == "B": return
731 self.zoom_zone.fait_crop(self.video.image, position)
732 xpx, ypx, xm, ym = self.coords(position)
733 self.editXpx.setText(f"{xpx}")
734 self.editYpx.setText(f"{ypx}")
735 self.editXm.setText(f"{xm}")
736 self.editYm.setText(f"{ym}")
737 return
738
739 def coords(self, p):
740 """
741 @param p un point, vecteur de coordonnées entières
742 @return les valeurs de x, y en px et puis en mètre (formatées :.2e)
743 """
744 # on se rapporte à l'origine du repère
745 p = p - self.origineorigine
746 # et aux sens des axes
747 p.redresse(self)
748
749 if not self.echelle_image:
750 return int(p.x), int(p.y), self.tr("indéf."), self.tr("indéf.")
751 return int(p.x), int(p.y), \
752 f"{p.x/self.echelle_image.pxParM():.2e}", \
753 f"{p.y/self.echelle_image.pxParM():.2e}"
754
755 def setButtonEchelle(self, text, style):
756 """
757 Signale fortement qu'il est possible de refaire l'échelle
758 @param text nouveau texte du bouton self.Bouton_Echelle
759 @param style un style CSS
760 """
761 self.Bouton_Echelle.setEnabled(True)
762 self.Bouton_Echelle.setText(text)
763 self.Bouton_Echelle.setStyleSheet(style)
764 return
765
766 def sync_slider2spinbox(self):
767 """
768 recopie la valeur du slider vers le spinbox
769 """
770 self.dbg.p(2, "rentre dans 'sync_slider2spinbox'")
771 self.spinBox_image.setValue(self.horizontalSlider.value())
772 return
773
774
775 def sync_spinbox2others(self):
776 """
777 Affiche l'image dont le numéro est dans self.pointage.spinBox_image et
778 synchronise self.horizontalSlider
779 """
780 self.dbg.p(2, "rentre dans 'sync_spinbox2others'")
781 self.index = self.spinBox_image.value()
782 self.horizontalSlider.setValue(self.index)
783 self.affiche_image()
784 return
785
786 def feedbackEchelle(self):
787 """
788 affiche une trace au-dessus du self.job, qui reflète les positions
789 retenues pour l'échelle
790 """
791 self.dbg.p(2, "rentre dans 'feedbackEchelle'")
792 if self.echelle_trace:
793 self.echelle_trace.hide()
795 self.video, self.echelle_image.p1, self.echelle_image.p2)
796 # on garde les valeurs pour le redimensionnement
797 self.echelle_trace.show()
798 if self.echelle:
799 self.echelle_modif.emit(self.tr("Refaire l'échelle"), "background-color:orange;")
800 return
801
802 def capture_auto(self):
803 """
804 fonction appelée au début de l'état AB : prépare la sélection
805 des motifs à suivre en capture automatique
806 """
807 self.dbg.p(2, "rentre dans 'capture_auto'")
808 self.auto = True # inhibe le pointage à la souris !
809 # recouvre l'image avec le widget selRect pour définir des
810 # rectangles pour chaque motif à suivre
811 self.zoomLabel.setText(self.tr("Zone à suivre n° {zone} x, y =").format(zone=self.suivis[0]))
812 self.selRect = SelRectWidget(self.video, self)
813 self.selRect.show()
814 return
815
816 def suiviDuMotif(self):
817 self.dbg.p(2, "rentre dans 'suiviDuMotif'")
818 if len(self.motifs_auto) == self.nb_obj:
819 self.dbg.p(3, "selection des motifs finie")
820 self.selRect.hide()
821 self.indexMotif = 0
822 self.pileDeDetections = []
823 for i in range(self.index, self.image_max+1):
824 self.pileDeDetections.append(i)
825 self.dbg.p(3, "self.pileDeDetections : %s" % self.pileDeDetections)
826 self.app.change_etat.emit("B")
827 else:
828 self.label_zoom.emit(self.tr("Zone à suivre n° {zone} x, y =").format(zone=self.suivis[len(self.motifs_auto)]))
829 return
830
831 # @time_it
832 def detecteUnPoint(self):
833 """
834 méthode (re)lancée pour les détections automatiques de points
835 traite une à une les données empilées dans self.pileDeDetections
836 et relance un signal si la pile n'est pas vide après chacun
837 des traitements.
838 """
839 self.dbg.p(2, f"rentre dans 'detecteUnPoint', pileDeDetection = {self.pileDeDetections}")
840 if self.pileDeDetections:
841 # on dépile un index de détections à faire et on met à jour
842 # le bouton de STOP
843 self.stop_n.emit(f"STOP ({self.pileDeDetections.pop(0)})")
844 ok, image = self.cvReader.getImage(
845 self.index, self.video.rotation, rgb=False)
846 # puis on boucle sur les objets à suivre et on
847 # détecte leurs positions
848 # Ça pourrait bien se faire dans des threads, en parallèle !!!
849 for i, part in enumerate(self.motifs_auto):
850 self.indexMotif = i
851 zone_proche = self.pointsProbables.get(self.objet_courantobjet_courant, None)
852 point = filter_picture(part, image, zone_proche)
854 echelle = self.video.image_w / self.largeurFilm
855 # on convertit selon l'échelle, et on recentre la détection
856 # par rapport au motif `part`
857 self.storePoint(vecteur(
858 echelle*(point[0]+part.shape[1]/2),
859 echelle*(point[1]+part.shape[0]/2)))
860 # le point étant détecté, on passe à l'objet suivant
861 # et si nécessaire à l'image suivante
862 self.objetSuivant()
863 # programme le suivi du point suivant après un délai de 50 ms,
864 # pour laisser une chance aux évènement de l'interface graphique
865 # d'être traités en priorité
866 QTimer.singleShot(50, self.detecteUnPoint)
867 else:
868 # fin de la détection automatique
869 self.auto = False
870 # si la pile d'images à détecter a été vidée par self.stopComputing,
871 # il faut passer à l'image suivante si possible
872 self.fin_pointage.emit()
873 self.app.change_etat.emit("D")
874 return
875
876 def storePoint(self, point):
877 """
878 enregistre un point, quand self.index et self.objet_courantobjet_courant
879 sont déjà bien réglés.
880 @param point la position à enregistrer
881 """
882 self.dbg.p(2, "rentre dans 'storePoint'")
883 if self.lance_capturelance_capture or self.auto:
884 self.pointe(self.objet_courantobjet_courant, point, index=self.index-1)
885 self.fin_pointage.emit()
886 return
887
888 def termine_pointage(self):
889 self.dbg.p(2, "rentre dans 'clic_sur_video'")
890 self.purge_defaits() # oublie les pointages à refaire
891 self.prepare_futur_clic()
892 self.app.sync_img2others(self.index)
893 return
894
895 def termine_pointage_manuel(self, event):
896 """
897 Fonction appelée en cas de pointage manuel sur l'image de la vidéo
898 après un mouserelease (bouton gauche)
899 """
900 if self.pointageOK:
901 self.app.change_etat.emit("E")
902 self.pointe(self.objet_courantobjet_courant, event, index=self.index-1)
903 self.objetSuivant()
904 self.fin_pointage.emit()
905 self.update_zoom.emit(vecteur(qPoint = event.position()))
906 self.video.update()
907 if self.refait_point and self.objet_courantobjet_courant == self.suivis[0]:
908 # on a été délégué pour corriger le tableau
909 # le dernier objet est pointé, retour au tableau de coords
910 self.refait_point = False
911 self.app.show_coord.emit()
912 return
913
914 def prepare_futur_clic(self):
915 """
916 Ajuste l'interface utilisateur pour attendre un nouveau clic
917 """
918 self.dbg.p(2, "rentre dans 'prepare_futur_clic'")
919 self.affiche_image()
921 self.enableDefaire(self.peut_defaire())
922 self.enableRefaire(self.peut_refaire())
923 return
924
925 def stop_setText(self, text):
926 """
927 Change le texte du bouton STOP
928 @param text le nouveau texte
929 """
930 self.pushButton_stopCalculs.setText(text)
931 return
932
933 def objetSuivant(self):
934 """
935 passage à l'objet suivant pour le pointage.
936 revient au premier objet quand on a fait le dernier, et
937 change d'image aussi
938 """
939 i = self.suivis.index(self.objet_courantobjet_courant)
940 if i < self.nb_obj - 1 :
941 self.objet_courantobjet_courant = self.suivis[i+1]
942 self.label_zoom.emit(self.tr("Pointage ({obj}) ; x, y =").format(obj = self.objet_courantobjet_courant))
943 else:
944 # on passe à l'image suivante, et on revient au premier objet
946 if self.index < self.image_max:
947 self.index +=1
948 # on revient à l'état D sauf en cas de suivi automatique
949 # auquel cas l'état E perdure
950 if not self.auto:
951 self.app.change_etat.emit("D")
952 else:
953 # on reste dans l'état E, néanmoins on synchronise
954 # les contrôles de l'image
955 self.app.image_n.emit(self.index)
956
957 return
958
959 def affiche_point_attendu(self, obj):
960 """
961 Renseigne sur le numéro d'objet du point attendu
962 affecte la ligne de statut et la ligne sous le zoom
963 @param obj l'objet courant
964 """
965 self.dbg.p(2, "rentre dans 'affiche_point_attendu'")
966 self.app.affiche_statut.emit(self.tr("Cliquez sur l'objet : {0}").format(obj))
967 return
968
969 def vecteursVitesse(self, echelle_vitesse):
970 """
971 Calcule les vecteurs vitesse affichables étant donné la collection
972 de points. Un vecteur vitesse a pour origine un point de la
973 trajectoire, et sa direction, sa norme sont basées sur le point
974 précédent et le point suivant ; il faut donc au moins trois pointages
975 pour que le résultat ne soit pas vide.
976
977 @param echelle_vitesse le nombre de pixels pour 1 m/s
978 @return un dictionnaire objet => [(org, ext), ...] où org et ext
979 sont l'origine et l'extrémité d'un vecteur vitesse
980 """
981 self.dbg.p(2, "rentre dans 'vecteursVitesse'")
982 result = {obj : [] for obj in self.suivis}
983 trajectoires = self.les_trajectoires()
984 for obj in self.suivis:
985 precedent = trajectoires[obj][0]
986 suivant = None
987 for i in range(1, len(trajectoires[obj]) - 1):
988 # itération le long de la trajectoire, sauf
989 # sur les points extrêmes.
990 if suivant:
991 point = suivant # le point est l'ancien suivant s'il existe
992 else:
993 point = trajectoires[obj][i]
994 suivant = trajectoires[obj][i+1]
995 vitesse = (self.pointEnMetre(suivant) - self.pointEnMetre(precedent)) * (1 / self.deltaTdeltaT / 2)
996 # attention, l'axe Y de l'écran est vers le bas !!
997 if self.sens_Ysens_Y == 1: vitesse.miroirY()
998 result[obj].append ((point, point + (vitesse * echelle_vitesse)))
999 precedent = point # on conserve les coordonnées pour la suite
1000 return result
1001
1002 def rouvre(self):
1003 """
1004 Ici c'est la partie dévolue au pointageWidget quand on rouvre un
1005 fichier pymecavideox
1006 """
1007 self.sens_axes.emit(self.sens_Xsens_X, self.sens_Ysens_Y)
1008 self.framerate, self.image_max, self.largeurFilm, self.hauteurFilm = \
1009 self.cvReader.recupere_avi_infos(self.video.rotation)
1010 self.ratio = self.largeurFilm / self.hauteurFilm
1011 # réapplique la préférence de deltat, comme openCV peut se tromper
1012 self.deltaTdeltaT = float(self.prefs.config["DEFAULT"]["deltat"])
1013 self.framerate = round(1/self.deltaTdeltaT)
1014 self.lineEdit_IPS.setText(f"{self.framerate}")
1015 self.sens_axes.emit(self.sens_Xsens_X, self.sens_Ysens_Y)
1016 # on met à jour le widget d'échelle
1017 self.affiche_echelle()
1018 self.feedbackEchelle()
1019 return
1020
1021 def coche_axes(self, x, y):
1022 """
1023 Met à jour les caches à cocher des axes
1024 @param x sens de l'axe x (+1 ou -1)
1025 @param y sens de l'axe y (+1 ou -1)
1026 """
1027 self.dbg.p(2, "rentre dans 'coche_axes'")
1028 self.checkBox_abscisses.setChecked(x < 0)
1029 self.checkBox_ordonnees.setChecked(y < 0)
1030 return
1031
1032 def restaure_pointages(self, data, premiere_image_pointee) :
1033 """
1034 Rejoue les pointages issus d'un fichier pymecavideo
1035 @param data une liste de listes de type [t, x1, y1, ..., xn, yn]
1036 @param premiere_image_pointee la toute première image pointée
1037 (au moins 1)
1038 """
1039 self.dimensionne(self.nb_obj, self.deltaTdeltaT, self.image_max)
1040 for i in range(len(data)) :
1041 for obj in self.suivis:
1042 j = int(obj)*2-1 # index du début des coordonnées xj, yj
1043 if len(data[i]) > j:
1044 x, y = data[i][j:j + 2]
1045 # À ce stade x et y sont en mètre
1046 # on remet ça en pixels
1047 x = self.origineorigine.x + self.sens_Xsens_X * \
1048 round(float(x) * self.echelle_image.pxParM())
1049 y = self.origineorigine.y - self.sens_Ysens_Y * \
1050 round(float(y) * self.echelle_image.pxParM())
1051 self.pointe(
1052 obj, vecteur(x, y),
1053 index = i + premiere_image_pointee - 1)
1054 # affiche la dernière image pointée
1055 der = self.derniere_image()
1056 if der is not None:
1057 if der < self.image_max:
1058 self.index = der + 1
1059 else:
1060 self.index = self.image_max
1061 else:
1062 self.index = 1
1063 self.prepare_futur_clic()
1064 self.echelle_modif.emit(self.tr("Refaire l'échelle"), "background-color:orange;")
1065 return
1066
1067 def refait_point_depuis_tableau(self, qpbn ):
1068 """
1069 fonction de rappel déclenchée quand on clique dans la dernière
1070 colonne du tableau
1071 @param qbbn le bouton qui a été cliqué pour en arriver là
1072 """
1073 self.dbg.p(2, "rentre dans 'refait_point_depuis_tableau'")
1074 self.refait_point=True
1075 self.objet_courantobjet_courant = self.suivis[0]
1076 self.index = qpbn.index_image
1077 self.prepare_futur_clic()
1078 self.app.show_video.emit()
1079 return
1080
Un lecteur de vidéos qui permet d'extraire les images une par une.
Definition: cadreur.py:214
Un widget pour choisir une nouvelle origine ; il est posé sur le widget vidéo, et durant sa vie,...
dbg.py, a module for pymecavideo: a program to track moving points in a video frameset
Definition: dbg.py:26
Widget qui permet de définir l'échelles.
Definition: echelle.py:100
Un widget transparent qui sert seulement à tracer l'échelle.
Definition: echelle.py:182
Une classe qui permet de définir les états pour le pointageWidget debut, A, AB, B,...
def restaureEtat(self)
Restauration de l'état A ou D après (re)définition de l'échelle.
Une classe qui affiche l'image d'une vidéo et qui gère le pointage d'objets mobiles dans cette vidéo.
def affiche_echelle(self)
affiche l'échelle courante pour les distances sur l'image
def setApp(self, app)
Crée une relation avec la fenêtre principale, son débogueur et ses préférences.
def capture_auto(self)
fonction appelée au début de l'état AB : prépare la sélection des motifs à suivre en capture automati...
def init_image(self)
initialise certaines variables lors le la mise en place d'une nouvelle vidéo
def loupe(self, position)
Agrandit deux fois une partie de self.video.image et la met dans la zone du zoom, puis met à jour les...
def redimensionne_data(self, dim)
redimensionne self.data (fonction de rappel connectée au signal self.dimension_data)
def affiche_image(self, index=None)
À condition qu'on ait ouvert le fichier vidéo, extrait l'image courante ou l'image spécifiée par l'in...
def sync_slider2spinbox(self)
recopie la valeur du slider vers le spinbox
def rouvre(self)
Ici c'est la partie dévolue au pointageWidget quand on rouvre un fichier pymecavideox.
def connecte_ui(self)
Connecte des signaux issus de l'UI.
def refait_point_suivant(self)
rétablit le point suivant après un effacement
def refait_point_depuis_tableau(self, qpbn)
fonction de rappel déclenchée quand on clique dans la dernière colonne du tableau
def affiche_imgsize(self, w, h, r)
Affiche la taille de l'image.
def feedbackEchelle(self)
affiche une trace au-dessus du self.job, qui reflète les positions retenues pour l'échelle
def sync_spinbox2others(self)
Affiche l'image dont le numéro est dans self.pointage.spinBox_image et synchronise self....
def efface_point_precedent(self)
revient au point précédent
def vecteursVitesse(self, echelle_vitesse)
Calcule les vecteurs vitesse affichables étant donné la collection de points.
def stop_setText(self, text)
Change le texte du bouton STOP.
def enableRefaire(self, value)
Contrôle la possibilité de refaire un clic.
def reinitialise_capture(self)
Efface toutes les données de la capture en cours et prépare une nouvelle session de capture.
def reinit_origine(self)
Replace l'origine au centre de l'image.
def init_cvReader(self)
Initialise le lecteur de flux vidéo pour OpenCV et recode la vidéo si nécessaire.
def objetSuivant(self)
passage à l'objet suivant pour le pointage.
def remontre_image(self)
Il peut être nécessaire de remontrer l'image après un changement de self.video.rotation.
def nouvelle_origine(self)
Permet de déplacer l'origine du référentiel de la caméra.
def enregistre(self, fichier)
Enregistre les données courantes dans un fichier, à un format CSV, séparé par des tabulations.
def setButtonEchelle(self, text, style)
Signale fortement qu'il est possible de refaire l'échelle.
def apply_preferences(self, rouvre=False)
Récupère les préférences sauvegardées, et en applique les données ici on s'occupe de ce qui se gère f...
def updateOrigine(self, rx, ry)
Met à jour l'origine.
def restaure_pointages(self, data, premiere_image_pointee)
Rejoue les pointages issus d'un fichier pymecavideo.
def coche_axes(self, x, y)
Met à jour les caches à cocher des axes.
def labelZoom(self, label)
Met à jour le label au-dessus du zoom.
def connecte_signaux(self)
connexion des signaux #######
update_imgedit
signaux #####################
def imgControlImage(self, state)
Gère les deux widgets horizontalSlider et spinBox_image.
def extract_image(self, index)
extrait une image de la video à l'aide d'OpenCV ; met à jour self.pointageOK s'il est licite de point...
def calcul_deltaT(self, ips_from_line_edit=False, rouvre=False)
Détermination de l'intervalle de temps entre deux images.
def openTheFile(self, filename)
Ouvre le fichier de nom filename, enregistre les préférences de fichier vidéo.
def detecteUnPoint(self)
méthode (re)lancée pour les détections automatiques de points traite une à une les données empilées d...
def prepare_futur_clic(self)
Ajuste l'interface utilisateur pour attendre un nouveau clic.
def affiche_point_attendu(self, obj)
Renseigne sur le numéro d'objet du point attendu affecte la ligne de statut et la ligne sous le zoom.
def storePoint(self, point)
enregistre un point, quand self.index et self.objet_courant sont déjà bien réglés.
def demande_echelle(self)
demande l'échelle interactivement
def enableDefaire(self, value)
Contrôle la possibilité de défaire un clic.
def termine_pointage_manuel(self, event)
Fonction appelée en cas de pointage manuel sur l'image de la vidéo après un mouserelease (bouton gauc...
def debut_capture(self)
Fonction de rappel du bouton Bouton_lance_capture.
Une classe pour représenter les pointages : séquences éventuellement creuses, de quadruplets (date,...
Definition: pointage.py:35
def pointe(self, objet, position, index=None, date=None)
ajoute un pointage aux données ; on peut soit préciser l'index et la date s'en déduit,...
Definition: pointage.py:149
def pointEnMetre(self, p)
renvoie un point, dont les coordonnées sont en mètre, dans un référentiel "à l'endroit"
Definition: pointage.py:321
def refaire(self)
dépile un pointage de self.defaits et le rajoute à la fin de self.data
Definition: pointage.py:82
def premiere_image(self)
donne le numéro de la première image pointée (1 au minimum), ou None si aucun pointage n'est fait
Definition: pointage.py:216
def les_trajectoires(self)
renvoie un dictionnaire objet => trajectoire de l'objet
Definition: pointage.py:303
def purge_defaits(self)
purge les données à refaire si on vient de cliquer sur la vidéo pour un pointage
Definition: pointage.py:109
def derniere_image(self)
donne le numéro de la dernière image pointée (on compte à partir de 1), ou None si aucun pointage n'e...
Definition: pointage.py:226
def defaire(self)
retire le dernier pointage de self.data et l'empile dans self.defaits
Definition: pointage.py:70
def peut_defaire(self)
Definition: pointage.py:96
def dimensionne(self, n_suivis, deltaT, n_images)
Crée les structures de données quand on en connaît par avance le nombre.
Definition: pointage.py:128
def peut_refaire(self)
Definition: pointage.py:102
def csv_string(self, sep=";", unite="px", debut=1, origine=vecteur(0, 0))
renvoie self.data sous une forme acceptable (CSV)
Definition: pointage.py:240
def clearEchelle(self)
oublie la valeur de self.echelle_image
Definition: pointage.py:117
Sert au retour visuel quand l'utilisateur doit sélectionner une zone rectangulaire pour le suivi auto...
Definition: suivi_auto.py:67
une classe pour des vecteurs 2D ; les coordonnées sont flottantes, et on peut accéder à celles-ci par...
Definition: vecteur.py:44