Pymecavideo 8.0
Étude cinématique à l'aide de vidéos
cadreur.py
1# -*- coding: utf-8 -*-
2
3"""
4 cadreur, 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
24import sys
25import os
26import time
27import subprocess
28import re
29import subprocess
30import shutil
31import numpy as np
32import cv2
33from PyQt6.QtCore import QObject, QThread, pyqtSignal, QLocale, QTranslator, Qt, QSize, QTimer, QCoreApplication, QMetaObject
34from PyQt6.QtGui import QKeySequence, QIcon, QPixmap, QImage
35from PyQt6.QtWidgets import QVBoxLayout, QLabel, QFrame, QGridLayout, QSlider, QDialogButtonBox, QDialog
36from vecteur import vecteur
37from itertools import cycle
38
39
40class Cadreur(QObject):
41 """
42 Un objet capable de recadrer une vidéo en suivant le déplacement
43 d'un point donné.
44 Paramètres du constructeur :
45 @param obj le numéro de l'objet qui doit rester immobile
46 @param app la fenêtre principale
47 @param titre le titre désiré pour la fenêtre
48 """
49
50 def __init__(self, obj, app, titre=None):
51 QObject.__init__(self)
52 self.app = app
53 self.pointage = app.pointage
54 self.video = app.pointage.video
55 if titre == None:
56 self.titre = str(self.tr("Presser la touche ESC pour sortir"))
57 self.obj = obj
58 # on s'intéresse à la trajectoire de l'objet servant de référentiel
59 self.trajectoire_obj = self.pointage.une_trajectoire(obj)
60 # on fait la liste des index où l'objet a été pointé (début à 0)
61 self.index_obj = self.pointage.index_trajectoires(debut = 0)
62 self.capture = cv2.VideoCapture(self.pointage.filename)
63 self.fps = self.capture.get(cv2.CAP_PROP_FPS)
64 self.delay = int(1000.0 / self.fps)
65 self.app.dbg.p(2, "In : Video, self.obj %s" %
66 (self.obj))
67 self.app.dbg.p(3, "In : Video, __init__, fps = %s and delay = %s" % (
68 self.fps, self.delay))
69
70 self.ralenti = 3
71 self.fini = False
72 self.maxcadre()
73
74 def echelleTaille(self):
75 """
76 Renvoie l'échelle qui permet de passer de l'image dans pymecavideo
77 à l'image effectivement trouvée dans le film, et la taille du film
78 @return un triplet échelle, largeur, hauteur (de l'image dans le widget de de pymecavideo)
79 """
80 m = self.pointage.imageExtraite.size()
81 echx = 1.0 * m.width() / self.video.image_w
82 echy = 1.0 * m.height() / self.video.image_h
83 ech = max(echx, echy)
84 return ech, int(m.width() / ech), int(m.height() / ech)
85
86 def controleRalenti(self, position):
87 """
88 fonction de rappel commandée par le bouton "Quitte"
89 """
90 self.ralenti = max([1, position])
91
92 def maxcadre(self):
93 """
94 calcule le plus grand cadre qui peut suivre le point n° obj
95 sans déborder du cadre de la vidéo. Initialise self.rayons qui indique
96 la taille de ce cadre, et self.decal qui est le décalage du point
97 à suivre par rapport au centre du cadre.
98 """
99 ech, w, h = self.echelleTaille()
100
101 agauche = []
102 dessus = []
103 for p in self.trajectoire_obj:
104 agauche.append(p.x)
105 dessus.append(p.y)
106 adroite = [w - x - 1 for x in agauche]
107 dessous = [h - y - 1 for y in dessus]
108
109 agauche = min(agauche)
110 adroite = min(adroite)
111 dessus = min(dessus)
112 dessous = min(dessous)
113 self.tl = vecteur(agauche, dessus) # topleft
114 self.sz = vecteur(adroite + agauche, dessous + dessus) # size
115
116 self.decal = vecteur((adroite - agauche) / 2, (dessous - dessus) / 2)
117 self.rayons = vecteur((agauche + adroite) / 2, (dessus + dessous) / 2)
118
119 def queryFrame(self):
120 """
121 récupère l'image suivante du film et traite le cas où OpenCV
122 ne sait pas le faire
123 @return une IplImage
124 """
125 if cv2.GrabFrame(self.capture):
126 return cv2.RetrieveFrame(self.capture)
127 else:
128 print(
129 "erreur, OpenCV 2.1 ne sait pas extraire des images du fichier", videofile)
130 sys.exit(1)
131
132 def montrefilm(self, fini=False):
133 """
134 Calcule et montre le film recadré à l'aide d'OpenCV
135 """
136 self.dialog = RalentiWidget(parentObject=self)
137 self.dialog.exec()
138
139 def rotateImage(self, img, angle):
140 if angle == 90:
141 return cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
142 elif angle == 270:
143 return cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
144 elif angle == 180:
145 return cv2.rotate(img, cv2.ROTATE_180)
146 else:
147 return img # angle=0
148
149from interfaces.Ui_ralenti_dialog import Ui_Dialog as Ralenti_Dialog
150
151class RalentiWidget(QDialog, Ralenti_Dialog):
153 """Affiche le film recadré"""
154
155 def __init__(self, parentObject):
156 QDialog.__init__(self)
157 Ralenti_Dialog.__init__(self)
158 self.cadreur = parentObject
159 self.ralenti = 1
160 self.images = cycle(self.cadreur.index_obj)
161 self.origines = cycle(self.cadreur.trajectoire_obj)
162 self.delay = self.cadreur.delay
163 self.ech, self.w, self.h = self.cadreur.echelleTaille()
164 self.timer = QTimer()
165 self.timer.setInterval(int(self.delay * self.ralenti))
166 self.timer.timeout.connect(self.affiche_image)
167
168 self.setupUi(self)
169 self.pushButton.clicked.connect(self.reject)
170 self.horizontalSlider.valueChanged.connect(self.change_ralenti)
171 self.label.resize(self.w, self.h)
172
173 self.timer.start()
174 return
175
176
177 def change_ralenti(self, ralenti):
178 self.ralenti = ralenti
179 self.label_2.setText(self.tr(
180 "Ralenti : 1/{}".format(ralenti)))
181 self.timer.setInterval(int(self.delay * self.ralenti))
182
183 def toQimage(self, img):
184 """Conversion image opencv en QPixmap"""
185 rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
186 h, w, ch = rgb.shape
187 bytes_per_line = ch * w
188 convert_to_Qt_format = QImage(
189 rgb.data, w, h, bytes_per_line, QImage.Format.Format_RGB888)
190 p = convert_to_Qt_format.scaled(self.w, self.h, Qt.AspectRatioMode.KeepAspectRatio)
191 return QPixmap.fromImage(p)
192
193 def affiche_image(self):
194 image_suivante = next(self.images)
195 p = next(self.origines)
196 hautgauche = (p + self.cadreur.decal -
197 self.cadreur.rayons) * self.ech
198 taille = self.cadreur.sz * self.ech
199 self.cadreur.capture.set(
200 cv2.CAP_PROP_POS_FRAMES, image_suivante)
201 status, img = self.cadreur.capture.read()
202 img = self.cadreur.rotateImage(img, self.cadreur.video.rotation)
203 w, h = int(taille.x), int(taille.y)
204 x, y = int(hautgauche.x), int(hautgauche.y)
205
206 crop_img = img[y:y+h, x:x+w]
207 # NOTE: its img[y: y + h, x: x + w] and *not* img[x: x + w, y: y + h]
208 self.label.setPixmap(self.toQimage(crop_img))
209 return
210
211class openCvReader:
212 """
213 Un lecteur de vidéos qui permet d'extraire les images une par une
214 """
215
216 def __init__(self, filename):
217 """
218 Le constructeur tente d'ouvrir le fichier video. En cas d'échec
219 la valeur booléenne de l'instance sera False. Le test de validité est
220 isolé dans un sous-shell
221 @param filename le nom d'un fichier vidéo
222 """
223 self.filename = filename
224 self.autoTest()
225 self.index_precedent = 0
226 self.cache = None # un cache pour l'image précédente
227 if self.ok:
228 self.capture = cv2.VideoCapture(self.filename)
229 return
230
231 def autoTest(self):
232 # if sys.platform == 'win32':
233 import testfilm
234 self.ok = testfilm.film(self.filename).ok
235 return
236 def __int__(self):
237 return int(self.ok)
238
239 def __nonzero__(self):
240 return self.ok
241
242 def getImage(self, index, angle=0, rgb=True):
243 """
244 récupère un array numpy
245 @param index le numéro de l'image, commence à 1.
246 @param angle 0, 90, 180 ou 270 : rotation de l'image (0 par défaut)
247 @apame rgb (vrai par defaut) s'il est faux l'image est au format BGR
248 @return le statut, l'image trouvée au format d'openCV
249 """
250 if self.capture:
251 if self.cache is not None and index == self.index_precedent:
252 img = self.cache
253 else:
254 if index != self.index_precedent+1:
255 # on ne force pas la position de lecture
256 # si on passe juste à l'image suivante
257 self.capture.set(cv2.CAP_PROP_POS_FRAMES, index-1)
258 _, img = self.capture.read()
259 # on cache l'image pour après
260 self.cache = img
261 # convertit dans le bon format de couleurs
262 if rgb: img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
263 self.index_precedent = index
264 return True, self.rotateImage(img, angle)
265 else:
266 return False, None
267
268 def rotateImage(self, img, angle):
269 if angle == 90:
270 return cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
271 elif angle == 270:
272 return cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
273 elif angle == 180:
274 return cv2.rotate(img, cv2.ROTATE_180)
275 else:
276 return img # angle==0
277
278 def recupere_avi_infos(self, angle=0):
279 """
280 Détermine les fps, le nombre de frames, la largeur, la hauteur d'un fichier vidéo
281 @return un quadruplet (framerate,nombre d'images,la largeur, la hauteur)
282 """
283 try:
284 fps = self.capture.get(cv2.CAP_PROP_FPS)
285 fcount = self.capture.get(cv2.CAP_PROP_FRAME_COUNT)
286
287 largeur = self.capture.get(cv2.CAP_PROP_FRAME_WIDTH)
288 hauteur = self.capture.get(cv2.CAP_PROP_FRAME_HEIGHT)
289 if angle % 180 == 90: # la vidéo est tournée à droite ou à gauche
290 largeur, hauteur = hauteur, largeur
291 except:
292 print("could not retrieve informations from the video file.")
293 print("assuming fps = 25, frame count = 10.")
294 return 25, 10, 320, 200
295 return int(fps), int(fcount), int(largeur), int(hauteur)
296
297 def __str__(self):
298 return f"<openCvReader instance: filename={self.filename}>"
299
300
301if __name__ == '__main__':
302 if len(sys.argv) > 1:
303 vidfile = sys.argv[1]
304 else:
305 vidfile = '/usr/share/python-mecavideo/video/g1.avi'
306 cvReader = openCvReader(vidfile)
307 if cvReader:
308 print("Ouverture du fichier %s réussie" % vidfile)
309 else:
310 print("Ouverture manquée pour le fichier %s" % vidfile)
Un objet capable de recadrer une vidéo en suivant le déplacement d'un point donné.
Definition: cadreur.py:48
def montrefilm(self, fini=False)
Calcule et montre le film recadré à l'aide d'OpenCV.
Definition: cadreur.py:135
def echelleTaille(self)
Renvoie l'échelle qui permet de passer de l'image dans pymecavideo à l'image effectivement trouvée da...
Definition: cadreur.py:79
def controleRalenti(self, position)
fonction de rappel commandée par le bouton "Quitte"
Definition: cadreur.py:89
def queryFrame(self)
récupère l'image suivante du film et traite le cas où OpenCV ne sait pas le faire
Definition: cadreur.py:124
def maxcadre(self)
calcule le plus grand cadre qui peut suivre le point n° obj sans déborder du cadre de la vidéo.
Definition: cadreur.py:98
Affiche le film recadré
Definition: cadreur.py:152
def change_ralenti(self, ralenti)
Definition: cadreur.py:177
def toQimage(self, img)
Conversion image opencv en QPixmap.
Definition: cadreur.py:184
Un lecteur de vidéos qui permet d'extraire les images une par une.
Definition: cadreur.py:214
def __init__(self, filename)
Le constructeur tente d'ouvrir le fichier video.
Definition: cadreur.py:222
def rotateImage(self, img, angle)
Definition: cadreur.py:268
def recupere_avi_infos(self, angle=0)
Détermine les fps, le nombre de frames, la largeur, la hauteur d'un fichier vidéo.
Definition: cadreur.py:282
def getImage(self, index, angle=0, rgb=True)
récupère un array numpy
Definition: cadreur.py:249
Une classe pour accéder aux images d'un film.
Definition: testfilm.py:15
une classe pour des vecteurs 2D ; les coordonnées sont flottantes, et on peut accéder à celles-ci par...
Definition: vecteur.py:44