Pymecavideo 8.0
Étude cinématique à l'aide de vidéos
pointage.py
1# -*- coding: utf-8 -*-
2
3"""
4 pointage, a module for pymecavideo:
5 a program to track moving points in a video frameset
6
7 Copyright (C) 2023 Georges Khaznadar <georgesk@debian.org>
8
9 This program is free software: you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation, either version 3 of the License, or
12 (at your option) any later version.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
21"""
22
23from PyQt6.QtCore import QObject
24from PyQt6.QtGui import QMouseEvent
25
26from vecteur import vecteur
27from echelle import echelle
28
29from collections import deque
30
31class Pointage(QObject):
32 """
33 Une classe pour représenter les pointages : séquences éventuellement
34 creuses, de quadruplets (date, désignation d'objet, vecteur)
35 """
36 def __init__(self):
37 QObject.__init__(self)
38 self.init_pointage()
39 return
40
41 def init_pointage(self):
42 """
43 self.data y est un dictionaire ordonné, qui a pour clés des dates
44 croissantes ; chaque date renvoie un dictionnaire de type
45 désignation d'objet => vecteur.
46
47 self.suivis est une liste limitative de désignations d'objets
48
49 self.deltaT est l'intervalle de temps entre deux images d'une vidéo
50
51 self.echelle est l'échelle en px par mètre
52
53 """
54 self.data = None # les données de pointage
55 self.dates = None # liste des index temporels
56 self.suivis = None # la liste des objets mobiles suivis
57 self.deltaT = None # intervalle de temps entre deux images
58 self.echelle = None # pixels par mètre
59 self.origine = None # position de l'origine sur les images
60 self.echelle_image = echelle() # objet gérant l'échelle
61 self.sens_X = 1 # sens de l'axe des abscisses
62 self.sens_Y = 1 # sens de l'axe des ordonnées
63 self.defaits = deque() # pile des pointages défaits
64 return
65
66 def defaire(self):
67 """
68 retire le dernier pointage de self.data et l'empile dans
69 self.defaits
70 """
71 der = self.derniere_image()
72 if der:
73 t = self.dates[der - 1]
74 self.defaits.append(self.data[t])
75 self.data[t] = {obj: None for obj in self.suivis}
76 return
77
78 def refaire(self):
79 """
80 dépile un pointage de self.defaits et le rajoute à la fin de
81 self.data
82 """
83 if len (self.defaits) > 0:
84 der = self.derniere_image()
85 if der and der < self.image_max:
86 t = self.dates[der]
87 else:
88 t = self.dates[0]
89 pointage = self.defaits.pop()
90 self.data[t] = pointage
91 return
92
93 def peut_defaire(self):
94 """
95 @return vrai si on peut défaire un pointage
96 """
97 return bool(self.derniere_image())
98
99 def peut_refaire(self):
100 """
101 @return vrai si on peut refaire un pointage
102 """
103 return len(self.defaits) > 0
104
105 def purge_defaits(self):
106 """
107 purge les données à refaire si
108 on vient de cliquer sur la vidéo pour un pointage
109 """
110 self.defaits = deque()
111 return
112
113
114 def clearEchelle(self):
115 """
116 oublie la valeur de self.echelle_image
117 """
118 self.echelle_image = echelle()
119 return
120
121 def dimensionne(self, n_suivis, deltaT, n_images):
122 """
123 Crée les structures de données quand on en connaît par avance
124 le nombre
125 @param n_suivis le nombre d'objets à suivre par pointage
126 @param deltaT l'intervalle de temps
127 @param n_images le nombre d'images de la vidéo étudiée
128 """
129 self.suivis = list(range(1, n_suivis+1)) # nombres 1, 2, ...
130 self.deltaT = deltaT
131 self.dates = [deltaT * i for i in range(n_images)]
132 self.data = {}
133 for index in range(n_images):
134 # crée une structure avec pour chaque date, un dictionnaire
135 # désignation d'objet => vecteur ; les vecteurs sont initialement
136 # indéfinis (représentés par None)
137 self.data[index*deltaT] = {o: None for o in self.suivis}
138 return
139
140 def pointe(self, objet, position, index=None, date=None):
141 """
142 ajoute un pointage aux données ; on peut soit préciser l'index
143 et la date s'en déduit, soit directement la date
144 @param objet la désignation d'un objet suivi ; couramment : un nombre
145 @param position
146 @param index s'il est donné la date est index * self.deltaT
147 @param date permet de donner directement la date ; l'index reste
148 prioritaire
149 """
150 if index is None and date is None:
151 raise Exception(
152 "index et date tous deux inconnus pour Pointage.pointe")
153 if isinstance(position, QMouseEvent):
154 position = vecteur(qPoint = position.position())
155 elif isinstance(position, vecteur):
156 pass
157 else:
158 raise Exception("dans Pointage.pointe, la position est soit QMouseEvent, soit vecteur")
159 if index is not None:
160 date = index * self.deltaT
161 if date not in self.data:
162 raise Exception(f"date incorrecte dans Pointage.pointe : {date}")
163 self.data[date][objet] = position
164 return
165
166 def position(self, objet, index=None, date=None, unite="px"):
167 """
168 ajoute un pointage aux données ; on peut soit préciser l'index
169 et la date s'en déduit, soit directement la date
170 @param objet la désignation d'un objet suivi ; couramment : un nombre
171 @param index s'il est donné la date est index * self.deltaT
172 @param date permet de donner directement la date ; l'index reste
173 prioritaire
174 @param unite l'unité du vecteur position : peut être "px" pour pixel
175 (par défaut) ou "m" pour mètre
176
177 @return un vecteur : position de l'objet à la date donnée
178 """
179 if index is None and date is None:
180 raise Exception(
181 "index et date tous deux inconnus pour Pointage.position")
182 if index is not None:
183 date = index * self.deltaT
184 if date not in self.dates:
185 raise Exception("date incorrecte dans Pointage.pointe")
186 if unite =="px":
187 return self.data[date][objet]
188 elif unite == "m":
189 return self.data[date][objet]*(1/self.echelle)
190 else:
191 raise Exception(f"dans Pointage.position, unité illégale {unite}")
192
193 def __str__(self):
194 return self.csv_string()
195
196 def __len__(self):
197 if self.dates is not None:
198 return len(self.dates)
199 return 0
200
201 def __bool__(self):
202 """
203 @return faux si toutes les pointages sont None
204 """
205 if self.dates is None or not self.suivis:
206 return False
207 for t in self.dates:
208 if self.data[t][self.suivis[0]] is not None:
209 return True
210 return False
211
212 def premiere_image(self):
213 """
214 donne le numéro de la première image pointée (1 au minimum),
215 ou None si aucun pointage n'est fait
216 """
217 for i, t in enumerate(self.dates):
218 if self.data[t][self.suivis[0]] is not None:
219 return i + 1
220 return None
221
222 def derniere_image(self):
223 """
224 donne le numéro de la dernière image pointée (on compte à partir de 1),
225 ou None si aucun pointage n'est fait
226 """
227 for i, t in zip(list(range(len(self.dates))[::-1]), self.dates[::-1]):
228 if self.data[t][self.suivis[0]] is not None:
229 return i + 1
230 return None
231
232 def csv_string(self, sep =";", unite="px", debut=1, origine=vecteur(0,0)):
233 """
234 renvoie self.data sous une forme acceptable (CSV)
235 @param sep le séparateur de champ, point-virgule par défaut.
236 @param unite l'unité du vecteur position : peut être "px" pour pixel
237 (par défaut) ou "m" pour mètre
238 @param debut la première image qui a été pointée
239 @param origine un vecteur pour l'origine du repère ; (0,0) par défaut
240 """
241 if unite == "px":
242 mul =1
243 elif unite == "m":
244 mul = self.echelle_image.mParPx()
245 else:
246 raise Exception(f"dans Pointage.trajectoire, unité illégale {unite}")
247
248 result=[]
249 en_tete = ["t"]
250 for o in self.suivis:
251 en_tete.append(f"x{o}")
252 en_tete.append(f"y{o}")
253 result.append(sep.join(en_tete))
254 dates = list(self.data.keys()) # toutes les dates
255 dates_pointees = dates[debut-1:] # dates qui commencent au début du pointage
256 dd = zip(dates, dates_pointees) # zip des deux listes précédentes
257 for t, t_point in list(dd) :
258 # l'itération ne commence qu'à la première image pointée
259 # t est une date qui commence à zéro
260 # t_point commence au premier pointage
261 ligne = [f"{t:.3f}"]
262 for o in self.suivis:
263 if self.data[t_point][o] is not None:
264 ligne.append(f"{self.sens_X * (self.data[t_point][o].x - origine.x) * mul:4g}")
265 ligne.append(f"{- self.sens_Y * (self.data[t_point][o].y - origine.y) * mul:4g}")
266 result.append(sep.join(ligne))
267 result.append("") # pour finir sur un saut de ligne
268 return "\n".join(result)
269
270 def trajectoire(self, objet, mode="liste", unite = "px"):
271 """
272 @param objet la désignation d'un objet suivi ; couramment : un nombre
273 @param mode "liste" ou "dico" ("liste" par défaut)
274 @param unite l'unité du vecteur position : peut être "px" pour pixel
275 (par défaut) ou "m" pour mètre
276
277 @return une liste de vecteurs (ou None quand la position est inconnue)
278 les mode = "liste", sinon un dictionnaire date=>vecteur
279 """
280 if unite == "px":
281 mul =1
282 elif unite == "m":
283 mul = 1/self.echelle
284 else:
285 raise Exception(f"dans Pointage.trajectoire, unité illégale {unite}")
286 if mode == "liste":
287 return [self.data[t][objet]*mul for t in self.dates]
288 return {t: self.data[t][objet]*mul for t in self.dates}
289
290 def une_trajectoire(self, obj):
291 """
292 renvoie la séquence de positions d'un objet pointé (seulement là
293 où il a été pointé, ni avant, ni après)
294 @param obj un des objets mobiles pointés
295 @return une liste [instance de vecteur, ...]
296 """
297 return [self.data[t][obj] for t in self.dates if self.data[t][obj]]
298
299 def les_trajectoires(self):
300 """
301 renvoie un dictionnaire objet => trajectoire de l'objet
302 @return { objet: [instance de vecteur, ...], ...}
303 """
304 return {obj: self.une_trajectoire(obj) for obj in self.suivis}
305
306 def index_trajectoires(self, debut = 1):
307 """
308 renvoie la liste des numéros des images pointés au long des
309 trajectoire. N.B. : la première image d'un film est numérotée 1
310 @param debut permet de choisir le numéro de la toute première image
311 du film (1 par défaut)
312 """
313 return [i + debut for i,t in enumerate(self.dates)
314 if self.data[t][self.suivis[0]]]
315
316 def pointEnMetre(self, p):
317 """
318 renvoie un point, dont les coordonnées sont en mètre, dans un
319 référentiel "à l'endroit"
320 @param p un point en "coordonnées d'écran"
321 """
322 self.dbg.p(2, "rentre dans 'pointEnMetre'")
323 if p is None: return None
324 return vecteur(
325 self.sens_X * (p.x - self.origine.x) * self.echelle_image.mParPx(),
326 self.sens_Y * (self.origine.y - p.y) * self.echelle_image.mParPx())
327
328 def iteration_data(self, callback_t, callback_p, unite="px"):
329 """
330 Une routine d'itération généralisée qui permet de lancer une action
331 spécifique pour chaque date et une action pour chaque pointage.
332
333 @param callback_t est None, ou une fonction de rappel dont les
334 paramètres sont i (index commençant à 0), t (la date) ;
335 cette fonction de rappel prend soin des « lignes » de données
336 @param callback_p est None, ou une fonction de rappel dont les
337 paramètres sont i, t, j (index d'objet commençant à 0),
338 obj (un objet suivi) et p son pointage de type vecteur,
339 v sa vitesse, de type vecteur ; cette fonction de rappel
340 prend soin de chacune des « cases » de données
341 @param unite ("px", pour pixels, par défaut) si l'unité est "px",
342 les données brutes du pointage en pixels sont renvoyées ; si
343 l'unité est "m" alors les coordonnées du point sont en mètre
344 """
345 if self.dates is None: return # pas de données, pas d'itértation !
346 precedents = [None] * self.nb_obj # points precedents, un par objet
347 for i,t in enumerate(self.dates):
348 if callback_t is not None:
349 callback_t(i,t)
350 for j, obj in enumerate(self.suivis):
351 if callback_p is not None:
352 p = self.data[t][obj]
353 if unite == "m": p = self.pointEnMetre(p)
354 if p is not None and precedents[j] is not None:
355 v = (p - precedents[j]) * (1 / self.deltaT)
356 else:
357 v = None
358 precedents[j] = p
359 callback_p(i, t, j, obj, p, v)
360 return
361
362 def iteration_objet(self, cb_o, cb_p, unite = "px"):
363 """
364 Permet de lancer une itération pour chacun des objets suivis
365 @param cb_o une fonction de rappel, utilisée itérativement pour
366 chaque objet. Les paramètres de cette fonction sont :
367 i : un index d'objet débutant à 0, obj : un objet suivi
368 @param cb_p une fonction de rappel, utilisée pour chacun des points
369 du pointage. Les paramètres de cette fonction sont :
370 i : un index d'objet débutant à 0, obj : un objet suivi,
371 p : un pointage (de type vecteur)
372 @param unite ("px", pour pixels, par défaut) si l'unité est "px",
373 les données brutes du pointage en pixels sont renvoyées ; si
374 l'unité est "m" alors les coordonnées du point sont en mètre
375 """
376 for i, o in enumerate(self.suivis):
377 cb_o(i, o)
378 for t in self.dates:
379 p = self.data[t][o]
380 if unite == "m":
381 p = self.pointEnMetre(p)
382 cb_p(i, o, p)
383 return
384
385 def liste_t_pointes(self):
386 """
387 renvoie la liste des dates où on a pointé des positions
388 @return une liste [float, ...]
389 """
390 return [t for t in self.dates
391 if self.data[t][self.suivis[0]] is not None]
392
393 def liste_pointages(self, obj=None):
394 """
395 renvoie la liste des pointages pour un objet
396 @param obj désigne l'objet choisi; si obj est None,
397 c'est le premier des objets
398 """
399 if obj is None: obj = self.suivis[0]
400 return [self.data[t][obj] for t in self.dates
401 if self.data[t][obj] is not None]
402
403 @property
404 def nb_obj(self):
405 """
406 @return le nombre d'objets suivis
407 """
408 if self.suivis is None: return 0
409 return len(self.suivis)
410
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 __bool__(self)
Definition: pointage.py:204
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 une_trajectoire(self, obj)
renvoie la séquence de positions d'un objet pointé (seulement là où il a été pointé,...
Definition: pointage.py:296
def index_trajectoires(self, debut=1)
renvoie la liste des numéros des images pointés au long des trajectoire.
Definition: pointage.py:312
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 position(self, objet, index=None, date=None, unite="px")
ajoute un pointage aux données ; on peut soit préciser l'index et la date s'en déduit,...
Definition: pointage.py:178
def iteration_objet(self, cb_o, cb_p, unite="px")
Permet de lancer une itération pour chacun des objets suivis.
Definition: pointage.py:375
def iteration_data(self, callback_t, callback_p, unite="px")
Une routine d'itération généralisée qui permet de lancer une action spécifique pour chaque date et un...
Definition: pointage.py:344
def les_trajectoires(self)
renvoie un dictionnaire objet => trajectoire de l'objet
Definition: pointage.py:303
def trajectoire(self, objet, mode="liste", unite="px")
Definition: pointage.py:279
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 liste_pointages(self, obj=None)
renvoie la liste des pointages pour un objet
Definition: pointage.py:398
def liste_t_pointes(self)
renvoie la liste des dates où on a pointé des positions
Definition: pointage.py:389
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
def init_pointage(self)
self.data y est un dictionaire ordonné, qui a pour clés des dates croissantes ; chaque date renvoie u...
Definition: pointage.py:53
une classe pour des vecteurs 2D ; les coordonnées sont flottantes, et on peut accéder à celles-ci par...
Definition: vecteur.py:44