Pymecavideo 8.0
Étude cinématique à l'aide de vidéos
coordWidget.py
1# -*- coding: utf-8 -*-
2
3"""
4 coordWidget, 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, QPushButton
31
32import os, time, re, sys
33import locale
34
35from version import Version
36from vecteur import vecteur
37from image_widget import ImageWidget
38from globdef import cible_icon, DOCUMENT_PATH, inhibe, pattern_float
39from toQimage import toQImage
40from suivi_auto import SelRectWidget
41from detect import filter_picture
42from cadreur import Cadreur, openCvReader
43from export import Export, EXPORT_FORMATS
44from dbg import Dbg
45
46import interfaces.icon_rc
47
48from interfaces.Ui_coordWidget import Ui_coordWidget
49from etatsCoord import Etats
50
51class CoordWidget(QWidget, Ui_coordWidget, Etats):
52 """
53 Widget principal de l'onglet coordonnées
54
55 paramètres du constructeur :
56 @param parent l'onglet des coordonnées
57 """
58 def __init__(self, parent):
59 QWidget.__init__(self, parent)
60 Ui_coordWidget.__init__(self)
61 Etats.__init__(self)
62 self.setupUi(self)
63 # remplit l'exportCombo
64 self.exportCombo.addItem('Exporter vers...')
65 # Ajoute les différents formats d'exportation
66 for key in sorted(EXPORT_FORMATS.keys()):
67 self.exportCombo.addItem(EXPORT_FORMATS[key]['nom'])
68 self.connecte_ui()
69 self.masse_objet = 0 # masse du premier objet suivi ???? à déboguer
70 return
71
72 def setApp(self, app):
73 """
74 Crée des liens avec la fenêtre principale, le débogueur,
75 le wigdet de pointage
76 @param app la fenêtre principale
77 """
78 self.app = app
79 self.dbg = app.dbg
80 self.pointage = app.pointage
81 return
82
83 def connecte_ui(self):
84 """
85 Connecte les signaux des sous-widgets
86 """
87 self.exportCombo.currentIndexChanged.connect(self.export)
88 self.pushButton_nvl_echelle.clicked.connect(self.recommence_echelle)
89 self.checkBox_Ec.stateChanged.connect(self.affiche_tableau)
90 self.checkBox_Epp.stateChanged.connect(self.affiche_tableau)
91 self.checkBox_Em.stateChanged.connect(self.affiche_tableau)
92 self.pushButton_select_all_table.clicked.connect(self.presse_papier)
93 return
94
95 def export(self, choix_export=None):
96 self.dbg.p(2, "rentre dans 'export'")
97 """
98 Traite le signal venu de exportCombo, puis remet l\'index de ce
99 combo à zéro.
100 """
101 # Si appel depuis les QActions, choix_export contient la clé du dico
102 if not choix_export:
103 # Si appel depuis le comboBox, on cherche l'index
104 choix_export = self.exportCombo.currentIndex()
105 if choix_export > 0:
106 # Les choix d'export du comboBox commencent à l'index 1. Le dico EXPORT_FORMATS commence à 1 et pas à zéro
107 self.exportCombo.setCurrentIndex(0)
108 self.affiche_tableau()
109 Export(self, choix_export)
110 return
111
112 def recommence_echelle(self):
113 self.app.new_echelle.emit()
114 return
115
116 def affiche_tableau(self):
117 """
118 lancée à chaque affichage du tableau, recalcule les coordonnées
119 à afficher à partir des listes de points.
120 """
121 self.dbg.p(2, "rentre dans 'affiche_tableau'")
122
123 # active ou désactive les checkbox énergies
124 # (n'ont un intérêt que si l'échelle est déterminée)
125 if self.pointage.echelle_image:
126 self.checkBox_Ec.setEnabled(True)
127 self.checkBox_Epp.setEnabled(True)
128 if self.checkBox_Ec.isChecked() and self.checkBox_Epp.isChecked():
129 self.checkBox_Em.setEnabled(True)
130 else:
131 # s'il manque Ec ou Epp on décoche Em
132 self.checkBox_Em.setChecked(False)
133 else:
134 self.checkBox_Ec.setEnabled(False)
135 self.checkBox_Em.setEnabled(False)
136 self.checkBox_Epp.setEnabled(False)
137
138 # masse de l'objet ATTENTION : QUID SI PLUSIEURS OBJETS ?
139 if self.checkBox_Ec.isChecked():
140 self.masse_objet = self.masse(1)
141 self.checkBox_Ec.setChecked(self.masse_objet != 0)
142 # initialise tout le tableau (nb de colonnes, unités etc.)
143 self.cree_tableau(nb_suivis = self.pointage.nb_obj)
144 # le compte de colonnes supplémentaires pour chaque objet
145 colonnes_sup = self.checkBox_Ec.isChecked() + \
146 self.checkBox_Epp.isChecked() + \
147 self.checkBox_Em.isChecked()
148 # le numéro de la dernière colonne où on peut refaire les points
149 colonne_refait_points = self.pointage.nb_obj * (2 + colonnes_sup) + 1
150
151 def cb_temps(i, t):
152 # marque la date dans la colonne de gauche
153 self.tableWidget.setItem(i, 0, QTableWidgetItem(f"{t:.3f}"))
154 return
155
156 def cb_point(i, t, j, obj, p, v):
157 # marque les coordonnées x et y de chaque objet, deux colonnes
158 # suivies par des colonnes supplémentaires (Ec, Epp, Em), et après
159 # avoir épuisé le compte d'objets, un colonne pour permettre de
160 # refaire le pointage
161 col = 1 + (2 + colonnes_sup) * j
162 if p:
163 self.tableWidget.setItem(
164 i, col, QTableWidgetItem(f"{p.x:.4g}"))
165 col += 1
166 self.tableWidget.setItem(
167 i, col, QTableWidgetItem(f"{p.y:.4g}"))
168 col+= 1
169 if colonnes_sup:
170 m = self.masse(obj)
171 # Énergie cinétique si nécessaire
172 if self.checkBox_Ec.isChecked():
173 if v is not None:
174 Ec = 0.5 * m * v.norme ** 2
175 self.tableWidget.setItem(
176 i, col, QTableWidgetItem(f"{Ec:.4g}"))
177 col += 1
178 # Énergie potentielle de pesanteur si nécessaire
179 if self.checkBox_Epp.isChecked():
180 Epp = m * 9.81 * p.y # TODO faire varier g
181 self.tableWidget.setItem(
182 i, col, QTableWidgetItem(f"{Epp:.4g}"))
183 col += 1
184 # Énergie mécanique si nécessaire
185 if self.checkBox_Em.isChecked():
186 if v is not None:
187 self.tableWidget.setItem(
188 i, col, QTableWidgetItem(f"{Ec+Epp:.4g}"))
189 col += 1
190 # dernière colonne : un bouton pour refaire le pointage
191 # n'existe que s'il y a eu un pointage
192 derniere = self.pointage.nb_obj * (2 + colonnes_sup) +1
193 self.tableWidget.setCellWidget(
194 i, derniere, self.bouton_refaire(i))
195 return
196
197 self.pointage.iteration_data(
198 cb_temps, cb_point,
199 unite = "m" if self.pointage.echelle_image else "px")
200
201 # rajoute des boutons pour refaire le pointage
202 # au voisinage immédiat des zones de pointage
203 colonne = self.pointage.nb_obj * (2 + colonnes_sup) +1
204 if self.pointage.premiere_image() is None: return
205 if self.pointage.premiere_image() > 1:
206 i = self.pointage.premiere_image() - 2
207 self.tableWidget.setCellWidget(i, colonne, self.bouton_refaire(i))
208 if self.pointage.derniere_image() < len(self.pointage):
209 i = self.pointage.derniere_image()
210 self.tableWidget.setCellWidget(i, colonne, self.bouton_refaire(i))
211 return
212
213 def presse_papier(self):
214 """Sélectionne la totalité du tableau de coordonnées
215 et l'exporte dans le presse-papier (l'exportation est implicitement
216 héritée de la classe utilisée pour faire le tableau). Les
217 séparateurs décimaux sont automatiquement remplacés par des virgules
218 si la locale est française.
219 """
220 self.dbg.p(2, "rentre dans 'presse_papier'")
221 self.affiche_tableau()
222 trange = QTableWidgetSelectionRange(0, 0,
223 self.tableWidget.rowCount() - 1,
224 self.tableWidget.columnCount() - 1)
225 self.tableWidget.setRangeSelected(trange, True)
226 # copie en format TSV vers le presse-papier
227 # merci à tyrtamos : voir https://www.developpez.net/forums/d1502290/autres-langages/python/gui/pyqt/rendre-copiable-qtablewidget/
228 # emplacement sélectionné pour copier dans le clipboard
229 selected = self.tableWidget.selectedRanges()
230 # construction du texte à copier, ligne par ligne et colonne par colonne
231 texte = ""
232 for i in range(selected[0].topRow(), selected[0].bottomRow() + 1):
233 for j in range(selected[0].leftColumn(), selected[0].rightColumn() + 1):
234 try:
235 texte += self.tableWidget.item(i, j).text() + "\t"
236 except AttributeError:
237 # quand une case n'a jamais été initialisée
238 texte += "\t"
239 texte = texte[:-1] + "\n" # le [:-1] élimine le '\t' en trop
240 # enregistrement dans le clipboard
241 QApplication.clipboard().setText(texte)
242 return
243
244 def cree_tableau(self, nb_suivis=1):
245 """
246 Crée un tableau de coordonnées neuf dans l'onglet idoine.
247 @param nb_suivis le nombre d'objets suivis (1 par défaut)
248 """
249 self.dbg.p(2, "rentre dans 'cree_tableau'")
250 self.tableWidget.clear()
251 self.tableWidget.setRowCount(1)
252 #le compte de colonnes supplémentaires pour chaque objet
253 colonnes_sup = self.checkBox_Ec.isChecked() + \
254 self.checkBox_Epp.isChecked() + \
255 self.checkBox_Em.isChecked()
256
257 # 2 colonnes par objet, colonnes_sup colonnes par objet
258 # une pour la date, une pour refaire le pointage
259 self.tableWidget.setColumnCount(nb_suivis * (2 + colonnes_sup) + 2)
260
261 self.tableWidget.setDragEnabled(True)
262 # on met des titres aux colonnes.
263 self.tableWidget.setHorizontalHeaderItem(
264 0, QTableWidgetItem('t (s)'))
265 self.tableWidget.setRowCount(len(self.pointage.data))
266 for i in range(nb_suivis):
267 unite = "m" if self.pointage.echelle_image \
268 else "px"
269 self.tableWidget.setHorizontalHeaderItem(
270 1 + (2+colonnes_sup) * i, QTableWidgetItem(
271 f"X{i + 1} ({unite})"))
272 self.tableWidget.setHorizontalHeaderItem(
273 2 + (2+colonnes_sup) * i, QTableWidgetItem(
274 f"Y{i + 1} ({unite})"))
275 for j in range(colonnes_sup):
276 cptr = 0
277 if self.checkBox_Ec.isChecked():
278 self.tableWidget.setHorizontalHeaderItem(
279 3+cptr + (2+colonnes_sup)*i, QTableWidgetItem(f"Ec{1 + i} (J)"))
280 cptr += 1
281 if self.checkBox_Epp.isChecked():
282 self.tableWidget.setHorizontalHeaderItem(
283 3+cptr + (2+colonnes_sup)*i, QTableWidgetItem(f"Epp{1 + i} (J)"))
284 cptr += 1
285 if self.checkBox_Em.isChecked():
286 self.tableWidget.setHorizontalHeaderItem(
287 3+cptr + (2+colonnes_sup)*i, QTableWidgetItem(f"Em{1 + i} (J)"))
288 cptr += 1
289 #dernier pour le bouton
290 self.tableWidget.setHorizontalHeaderItem(
291 nb_suivis * 2 + 1 + colonnes_sup*nb_suivis,
292 QTableWidgetItem("Refaire le point"))
293 return
294
295 def recalculLesCoordonnees(self):
296 """
297 permet de remplir le tableau des coordonnées à la demande.
298 Se produit quand on ouvre un fichier pymecavideo ou quand on
299 redéfinit l'échelle
300 """
301 self.dbg.p(2, "rentre dans 'recalculLesCoordonnees'")
302 nb_suivis = self.pointage.nb_obj
303
304 def cb_temps(i, t):
305 # marque la date dans la colonne de gauche
306 self.tableWidget.setItem(i, 0, QTableWidgetItem(f"{t:.3f}"))
307 return
308
309 def cb_point(i, t, j, obj, p, v):
310 # marque les coordonnées x et y de chaque objet, deux colonnes
311 # par deux colonnes.
312 if p:
313 self.tableWidget.setItem(
314 i, j*(nb_suivis)+1, QTableWidgetItem(str(p.x)))
315 self.tableWidget.setItem(
316 i, j*(nb_suivis) + 2, QTableWidgetItem(str(p.y)))
317 return
318
319 # dans le tableau, l'unité est le mètre.
320 self.pointage.iteration_data(cb_temps, cb_point, unite = "m")
321 return
322
323 def bouton_refaire(self, ligne):
324 """
325 Crée un bouton servant à refaire un pointage, pour la donnée
326 affichée dans une ligne du tableau
327 @param ligne une ligne du tableau (indexée à partir de 0)
328 @return un bouton
329 """
330 b = QPushButton()
331 b.setIcon(QIcon(":/data/icones/curseur_cible.svg"))
332 b.setToolTip(self.tr(
333 "refaire le pointage\n de l'image {numero}").format(
334 numero = ligne + 1))
335 b.setFlat(True)
336 b.clicked.connect(lambda state: \
337 self.pointage.refait_point_depuis_tableau( b ))
338 b.index_image = ligne + 1
339 return b
340
341 def masse(self, obj):
342 """
343 Renseigne la masse d'un objet. L'implémentation est actuellement
344 incomplète : une seule masse est autorisée, pour tous les objets
345 donc on ne tient pas compte du paramètre obj
346 @param obj un objet suivi
347 @return la masse de cet objet
348 """
349 if self.masse_objet == 0:
350 masse_objet_raw, ok = QInputDialog.getText(
351 None,
352 self.tr("Masse de l'objet"),
353 self.tr("Quelle est la masse de l'objet ? (en kg)"),
354 text ="1.0")
355 masse_objet_raw = masse_objet_raw.replace(",", ".")
356 ok = ok and pattern_float.match(masse_objet_raw)
357 masse_objet = float(masse_objet_raw)
358 if masse_objet <= 0 or not ok:
359 self.affiche_statut.emit(self.tr(
360 "Merci d'indiquer une masse valable"))
361 return None
362 self.masse_objet = masse_objet
363 return self.masse_objet
364
Widget principal de l'onglet coordonnées.
Definition: coordWidget.py:57
def presse_papier(self)
Sélectionne la totalité du tableau de coordonnées et l'exporte dans le presse-papier (l'exportation e...
Definition: coordWidget.py:219
def affiche_tableau(self)
lancée à chaque affichage du tableau, recalcule les coordonnées à afficher à partir des listes de poi...
Definition: coordWidget.py:120
def bouton_refaire(self, ligne)
Crée un bouton servant à refaire un pointage, pour la donnée affichée dans une ligne du tableau.
Definition: coordWidget.py:329
def cree_tableau(self, nb_suivis=1)
Crée un tableau de coordonnées neuf dans l'onglet idoine.
Definition: coordWidget.py:248
def setApp(self, app)
Crée des liens avec la fenêtre principale, le débogueur, le wigdet de pointage.
Definition: coordWidget.py:77
def masse(self, obj)
Renseigne la masse d'un objet.
Definition: coordWidget.py:348
def recalculLesCoordonnees(self)
permet de remplir le tableau des coordonnées à la demande.
Definition: coordWidget.py:300
def connecte_ui(self)
Connecte les signaux des sous-widgets.
Definition: coordWidget.py:86
def export(self, choix_export=None)
Definition: coordWidget.py:95
Une classe qui permet de définir les états pour le ccordWidget debut, A, AB, B, C,...
Definition: etatsCoord.py:34
Une classe qui gère l'exportation des données.
Definition: export.py:621