imicpe 1.0.7__tar.gz → 1.0.8.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {imicpe-1.0.7 → imicpe-1.0.8.1}/PKG-INFO +1 -1
  2. {imicpe-1.0.7 → imicpe-1.0.8.1}/pyproject.toml +1 -1
  3. imicpe-1.0.8.1/src/imicpe/_version.py +1 -0
  4. imicpe-1.0.8.1/src/imicpe/reco3D/__init__.py +4 -0
  5. imicpe-1.0.8.1/src/imicpe/reco3D/angles.py +117 -0
  6. imicpe-1.0.8.1/src/imicpe/reco3D/calculPose.py +284 -0
  7. imicpe-1.0.8.1/src/imicpe/reco3D/evalTrajecto.py +167 -0
  8. imicpe-1.0.8.1/src/imicpe/reco3D/vue3D.py +216 -0
  9. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe.egg-info/PKG-INFO +1 -1
  10. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe.egg-info/SOURCES.txt +6 -1
  11. imicpe-1.0.7/src/imicpe/_version.py +0 -1
  12. {imicpe-1.0.7 → imicpe-1.0.8.1}/DESCRIPTION.md +0 -0
  13. {imicpe-1.0.7 → imicpe-1.0.8.1}/LICENSE +0 -0
  14. {imicpe-1.0.7 → imicpe-1.0.8.1}/README.md +0 -0
  15. {imicpe-1.0.7 → imicpe-1.0.8.1}/setup.cfg +0 -0
  16. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe/__init__.py +0 -0
  17. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe/cs/__init__.py +0 -0
  18. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe/cs/l1.py +0 -0
  19. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe/cs/masks.py +0 -0
  20. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe/cs/metrics.py +0 -0
  21. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe/cs/operators.py +0 -0
  22. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe/cs/shepp_logan_phantom.py +0 -0
  23. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe/cs/tikhonov.py +0 -0
  24. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe/optim/__init__.py +0 -0
  25. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe/optim/metrics.py +0 -0
  26. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe/optim/operators.py +0 -0
  27. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe/optim/pnnDataset.py +0 -0
  28. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe/optim/pnnTrainer.py +0 -0
  29. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe/optim/pnnUtils.py +0 -0
  30. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe.egg-info/dependency_links.txt +0 -0
  31. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe.egg-info/requires.txt +0 -0
  32. {imicpe-1.0.7 → imicpe-1.0.8.1}/src/imicpe.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imicpe
3
- Version: 1.0.7
3
+ Version: 1.0.8.1
4
4
  Summary: Toolbox for Maths,Signal,Image Teaching @ CPE
5
5
  Author-email: Marion Foare <marion.foare@cpe.fr>, Eric Van Reeth <eric.vanreeth@cpe.fr>, Arthur Gautheron <arthur.gautheron@cpe.fr>
6
6
  License: MIT License
@@ -14,7 +14,7 @@ mypkg = ["*.txt", "*.mat", "*.npy"]
14
14
  #[tool.setuptools_scm]
15
15
 
16
16
  [project]
17
- version="1.0.7"
17
+ version="1.0.8.1"
18
18
  name = "imicpe"
19
19
  authors = [
20
20
  { name="Marion Foare", email="marion.foare@cpe.fr" },
@@ -0,0 +1 @@
1
+ __version__="1.0.8.1"
@@ -0,0 +1,4 @@
1
+ from .vue3D import vue3D
2
+ from .angles import ang2rot, rot2ang
3
+ from .evalTrajecto import evalTrajecto
4
+ from .calculPose import calculePose3D2D
@@ -0,0 +1,117 @@
1
+ import numpy as np
2
+
3
+ def ang2rot(ang):
4
+ """
5
+
6
+ Calcule la matrice de rotation 3x3 à partir de 3 angles (Euler Z–X–Y)
7
+ (ONERA/DTIM)
8
+
9
+ Parameters
10
+ ----------
11
+ ang : array_like of shape (3,)
12
+ Angles des rotations autour des axes z, x, y (en radians)
13
+
14
+ Returns
15
+ -------
16
+ R : ndarray of shape (3,3)
17
+ Matrice de rotation correspondante
18
+ """
19
+
20
+ ang = np.array(ang, dtype=float).flatten()
21
+ if ang.size != 3:
22
+ raise ValueError("ang doit être un vecteur de 3 éléments (z, x, y)")
23
+
24
+ cz, sz = np.cos(ang[0]), np.sin(ang[0])
25
+ cx, sx = np.cos(ang[1]), np.sin(ang[1])
26
+ cy, sy = np.cos(ang[2]), np.sin(ang[2])
27
+
28
+ # === Codage Euler z-x-y ===
29
+ Rz = np.array([
30
+ [cz, sz, 0],
31
+ [-sz, cz, 0],
32
+ [0, 0, 1]
33
+ ])
34
+
35
+ Rx = np.array([
36
+ [1, 0, 0],
37
+ [0, cx, sx],
38
+ [0, -sx, cx]
39
+ ])
40
+
41
+ Ry = np.array([
42
+ [cy, 0, -sy],
43
+ [0, 1, 0],
44
+ [sy, 0, cy]
45
+ ])
46
+
47
+ R = Rz @ Rx @ Ry
48
+ return R
49
+
50
+ def rot2ang(matR):
51
+ """
52
+ Conversion d'une matrice de rotation 3x3 en angles d'Euler (z, x, y)
53
+ Codage : Euler Z–X–Y
54
+ Retourne : ang (3x2) — deux solutions possibles
55
+ (ONERA/DTIM)
56
+
57
+ Parameters
58
+ ----------
59
+ R : ndarray (3,3)
60
+ Matrice de rotation correspondante
61
+
62
+ Returns
63
+ -------
64
+ ang : array_like (3,2)
65
+ Angles des rotations autour des axes z, x, y (en radians)
66
+ """
67
+
68
+ # Sécurité : s'assurer que matR est un array numpy 3x3
69
+ matR = np.array(matR, dtype=float).reshape(3, 3)
70
+
71
+ # Cas général
72
+ if abs(matR[2, 1]) != 1:
73
+ ang = np.zeros((3, 2)) # (3x2) comme MATLAB après transposition finale
74
+
75
+ # rotation angle around x axis
76
+ ang[1, 0] = np.arcsin(-matR[2, 1])
77
+ ang[1, 1] = np.sign(ang[1, 0]) * np.pi - ang[1, 0]
78
+
79
+ # rotation angle around y axis (colonne 3 MATLAB → ligne 2 ici avant transpose)
80
+ ang[2, 0] = np.arctan2(matR[2, 0] / np.cos(ang[1, 0]),
81
+ matR[2, 2] / np.cos(ang[1, 0]))
82
+ ang[2, 1] = np.arctan2(matR[2, 0] / np.cos(ang[1, 1]),
83
+ matR[2, 2] / np.cos(ang[1, 1]))
84
+
85
+ # rotation angle around z axis (colonne 1 MATLAB → ligne 0 ici avant transpose)
86
+ ang[0, 0] = np.arctan2(matR[0, 1] / np.cos(ang[1, 0]),
87
+ matR[1, 1] / np.cos(ang[1, 0]))
88
+ ang[0, 1] = np.arctan2(matR[0, 1] / np.cos(ang[1, 1]),
89
+ matR[1, 1] / np.cos(ang[1, 1]))
90
+
91
+ return ang # déjà (3x2) comme après transpose MATLAB
92
+
93
+ # Cas dégénéré : matR[3,2] = ±1 → gimbal lock
94
+ else:
95
+ ang = np.zeros(3)
96
+ # variable "roll" inexistante en MATLAB original → non utilisée ici
97
+ # on garde simplement les 3 angles comme définis par le cas particulier
98
+
99
+ if matR[2, 1] == 1:
100
+ ang[1] = -np.pi / 2
101
+ ang[2] = np.arctan2(-matR[0, 2], -matR[1, 2]) - ang[0]
102
+ if ang[2] > np.pi:
103
+ ang[2] -= 2 * np.pi
104
+ elif ang[2] < -np.pi:
105
+ ang[2] += 2 * np.pi
106
+
107
+ else: # matR[2,1] == -1
108
+ ang[1] = np.pi / 2
109
+ ang[2] = ang[0] - np.arctan2(matR[0, 2], matR[1, 2])
110
+ if ang[2] > np.pi:
111
+ ang[2] -= 2 * np.pi
112
+ elif ang[2] < -np.pi:
113
+ ang[2] += 2 * np.pi
114
+
115
+ # Le MATLAB retourne ang = ang'
116
+ return ang.reshape(3, 1)
117
+
@@ -0,0 +1,284 @@
1
+ from .angles import ang2rot, rot2ang
2
+ import numpy as np
3
+ from scipy.linalg import svd
4
+ from scipy.optimize import least_squares
5
+ # ===============================================
6
+ # === Fonction principale : affinageRT ==========
7
+ # ===============================================
8
+
9
+ def affinageRT(anginit, Tinit, matK, pos3D, pos2D):
10
+ """
11
+ Affine rotation et translation par minimisation de l'erreur de reprojection
12
+ Entrées :
13
+ anginit : angles initiaux (3x1)
14
+ Tinit : translation initiale (3x1)
15
+ matK : matrice intrinsèque 3x3
16
+ pos3D : points 3D (Nx3)
17
+ pos2D : points 2D (Nx2)
18
+ Sortie :
19
+ angfin : angles affinés (3x1)
20
+ Tfin : translation affinée (3x1)
21
+ rmsInit : RMS initial
22
+ rmsFin : RMS final
23
+ """
24
+
25
+ # ===============================================
26
+ # === Fonctions de rotation =====================
27
+ # ===============================================
28
+
29
+ # def ang2rot(ang):
30
+ # """Conversion angles (3x1) → matrice de rotation 3x3 (ordre Rx*Ry*Rz typique MATLAB)"""
31
+ # cx, cy, cz = np.cos(ang)
32
+ # sx, sy, sz = np.sin(ang)
33
+
34
+ # Rx = np.array([[1, 0, 0],
35
+ # [0, cx, -sx],
36
+ # [0, sx, cx]])
37
+ # Ry = np.array([[ cy, 0, sy],
38
+ # [ 0, 1, 0],
39
+ # [-sy, 0, cy]])
40
+ # Rz = np.array([[cz, -sz, 0],
41
+ # [sz, cz, 0],
42
+ # [ 0, 0, 1]])
43
+
44
+ # # MATLAB fait généralement R = Rz * Ry * Rx
45
+ # R = Rz @ Ry @ Rx
46
+ # return R
47
+
48
+
49
+ # ===============================================
50
+ # === Fonction de projection ====================
51
+ # ===============================================
52
+
53
+ def fct_proj_ck(d, X, K):
54
+ """
55
+ Projection 3D → 2D avec paramètres caméra.
56
+ Équivalent MATLAB de fct_proj_ck
57
+ Entrées :
58
+ d : vecteur [T; ang] de taille (6,)
59
+ X : points 3D (3xN)
60
+ K : matrice intrinsèque (3x3)
61
+ Sortie :
62
+ x : projections 2D (2xN)
63
+ """
64
+ alx = K[0, 0]
65
+ aly = K[1, 1]
66
+ x0 = K[0, 2]
67
+ y0 = K[1, 2]
68
+
69
+ R = ang2rot(d[3:6])
70
+ T = d[0:3].reshape(3, 1)
71
+
72
+ # Projection
73
+ D = R @ (X - T) # même que R*(X - repmat(T,1,N)) en MATLAB
74
+ x1 = alx * D[0, :] / D[2, :] + x0
75
+ y1 = aly * D[1, :] / D[2, :] + y0
76
+
77
+ x = np.vstack((x1, y1))
78
+ return x
79
+
80
+
81
+ # ===============================================
82
+ # === Fonction d'évaluation =====================
83
+ # ===============================================
84
+
85
+ def eval_fct_proj_ck(d, xref, X, K):
86
+ """
87
+ Évalue la fonction de reprojection et retourne les résidus
88
+ Entrées :
89
+ d : [T; ang] (6,)
90
+ xref : 2xN (positions 2D de référence)
91
+ X : 3xN (points 3D)
92
+ K : matrice intrinsèque
93
+ Sortie :
94
+ res : vecteur des résidus (2N,)
95
+ x : projections (2xN)
96
+ """
97
+ x = fct_proj_ck(d, X, K)
98
+ res = x - xref
99
+ res = res.flatten(order='F') # même ordre colonne que MATLAB
100
+ return res, x
101
+
102
+ nbPts = pos3D.shape[0]
103
+ if nbPts != pos2D.shape[0]:
104
+ print("Erreur : dimensions incompatibles entre pos3D et pos2D")
105
+ return None, None, None, None
106
+
107
+ # --- Calcul du résidu initial ---
108
+ Rinit = ang2rot(anginit.flatten())
109
+ Mproj3 = Rinit @ pos3D.T - Rinit @ Tinit.reshape(3, 1).repeat(nbPts, axis=1)
110
+ aux = matK @ Mproj3
111
+ mproj3 = aux[:2, :] / aux[2, :]
112
+ mproj3 = mproj3.T
113
+
114
+ residu3 = (mproj3.flatten() - pos2D.flatten())
115
+ rmsInit = np.sqrt(np.mean(residu3 ** 2))
116
+
117
+ # --- Optimisation non linéaire ---
118
+ dinit = np.concatenate((Tinit.flatten(), anginit.flatten()))
119
+
120
+ def fun(d):
121
+ res, _ = eval_fct_proj_ck(d, pos2D.T, pos3D.T, matK)
122
+ return res
123
+
124
+ res_lsq = least_squares(fun, dinit, method='lm', verbose=0)
125
+ dfin = res_lsq.x
126
+
127
+ # --- Résidu final ---
128
+ resfin, _ = eval_fct_proj_ck(dfin, pos2D.T, pos3D.T, matK)
129
+ rmsFin = np.sqrt(np.mean(resfin ** 2))
130
+
131
+ Tfin = dfin[0:3].reshape(3, 1)
132
+ angfin = dfin[3:6].reshape(3, 1)
133
+
134
+ # --- Résidu image final (comme MATLAB) ---
135
+ Rfin = ang2rot(angfin.flatten())
136
+ Mproj4 = Rfin @ pos3D.T - Rfin @ Tfin.repeat(nbPts, axis=1)
137
+ aux = matK @ Mproj4
138
+ mproj4 = aux[:2, :] / aux[2, :]
139
+ mproj4 = mproj4.T
140
+
141
+ residu4 = (mproj4.flatten() - pos2D.flatten())
142
+ rmsFin = np.sqrt(np.mean(residu4 ** 2))
143
+ return angfin.squeeze(), Tfin.squeeze(), rmsInit, rmsFin
144
+
145
+
146
+ def calculePose3D2D(pts3D, pts2D, matK, nbPts, AFFINAGE=False):
147
+ """
148
+ Calcule la pose d’une caméra (rotation et translation) à partir de correspondances 3D-2D.
149
+
150
+ Utilise la méthode DLT (Direct Linear Transform) avec une résolution par SVD pour estimer
151
+ la matrice de projection, puis extrait la rotation et la translation. Un affinage optionnel
152
+ permet d'améliorer la précision via une minimisation de l'erreur de reprojection.
153
+
154
+ Parameters
155
+ ----------
156
+ pts3D : ndarray of shape (nbPts, 3)
157
+ Coordonnées des points 3D dans le repère monde.
158
+
159
+ pts2D : ndarray of shape (nbPts, 2)
160
+ Coordonnées des points 2D correspondants dans l'image (en pixels).
161
+
162
+ matK : ndarray of shape (3, 3)
163
+ Matrice de calibration intrinsèque de la caméra.
164
+
165
+ nbPts : int
166
+ Nombre de points de correspondance 3D-2D utilisés pour le calcul.
167
+
168
+ AFFINAGE : bool, optional
169
+ Si True, active un affinage de la pose par minimisation de l'erreur de reprojection.
170
+ Par défaut à False.
171
+
172
+ Returns
173
+ -------
174
+ ang : ndarray of shape (3,)
175
+ Angles d'Euler (rx, ry, rz) en radians, représentant la rotation de la caméra.
176
+
177
+ T : ndarray of shape (3,)
178
+ Vecteur de translation de la caméra dans le repère monde.
179
+
180
+ rms_reproj : float
181
+ Erreur RMS (Root Mean Square) de reprojection entre les points 2D projetés et les
182
+ observations réelles.
183
+
184
+ Notes
185
+ -----
186
+ - La rotation est estimée à partir de la matrice de projection, et corrigée
187
+ pour s'assurer qu'elle appartient au groupe SO(3).
188
+ - Le vecteur d'angles `ang` est sélectionné comme la solution avec la norme minimale.
189
+ - Si `AFFINAGE` est activé, une fonction interne est utilisée pour optimiser
190
+ les paramètres `(R, T)` en minimisant l'erreur de reprojection.
191
+
192
+ Examples
193
+ --------
194
+ >>> ang, T, rms = calculePose3D2D(pts3D, pts2D, matK, len(pts3D), AFFINAGE=True)
195
+ >>> print("Angles (deg):", np.degrees(ang))
196
+ >>> print("Translation:", T)
197
+ >>> print("RMS reprojection error:", rms)
198
+ """
199
+ ROTPROJ=False
200
+
201
+ fx = matK[0, 0]
202
+ fy = matK[1, 1]
203
+ x0 = matK[0, 2]
204
+ y0 = matK[1, 2]
205
+ # --- Constitution du système ---
206
+ Kinv = np.linalg.inv(matK)
207
+ posN = Kinv @ np.vstack((pts2D.T, np.ones((1, nbPts))))
208
+ posN = posN.T
209
+ M1 = np.hstack([
210
+ -pts3D, -np.ones((nbPts, 1)),
211
+ np.zeros((nbPts, 4)),
212
+ (posN[:, [0]] * pts3D), posN[:, [0]]
213
+ ])
214
+
215
+ M2 = np.hstack([
216
+ np.zeros((nbPts, 4)),
217
+ -pts3D, -np.ones((nbPts, 1)),
218
+ (posN[:, [1]] * pts3D), posN[:, [1]]
219
+ ])
220
+
221
+ M = np.vstack([M1, M2])
222
+
223
+ # --- Résolution SVD ---
224
+ U, S, Vt = svd(M)
225
+ pest = Vt[-1, :] # dernière ligne
226
+ pest = pest / np.linalg.norm(pest[8:11])
227
+ Pest = pest.reshape(4,3, order='F').T # comme reshape(...,4,3)' en MATLAB
228
+
229
+ if np.linalg.det(Pest[:, :3]) < 0:
230
+ Pest = -Pest
231
+
232
+ # --- R/T et angles ---
233
+ if ROTPROJ:
234
+ Ur, Sr, Vr = svd(Pest[:, :3])
235
+ Rest = Ur @ Vr
236
+ else:
237
+ Rest = Pest[:3, :3]
238
+
239
+
240
+ angcandidat = rot2ang(Rest)
241
+ # Sélection du plus proche angle (ici, le plus faible)
242
+ imin = np.argmin(np.sum(angcandidat**2, axis=0))
243
+ ang = angcandidat[:, imin]
244
+ T = -Rest.T @ Pest[:, 3]
245
+ # --- Affinage optionnel ---
246
+ if AFFINAGE:
247
+ angfin, Tfin, rmsInit, rmsFin = affinageRT(ang, T, matK, pts3D, pts2D)
248
+ mvtfin = np.concatenate((angfin.flatten(), Tfin.flatten()))
249
+ ang = angfin
250
+ T = Tfin
251
+ else:
252
+ rmsInit = 0
253
+ rmsFin = 0
254
+
255
+ # --- Erreur de reprojection ---
256
+
257
+ Xw = pts3D.T
258
+ R = ang2rot(ang.flatten())
259
+ # Xc = R @ (Xw - T.reshape(3, 1))
260
+ # xn = Xc[:2, :] / Xc[2, :]
261
+ # x = np.vstack([fx * xn[0, :] + x0, fy * xn[1, :] + y0]).T
262
+ # res = (x - pts2D).flatten()
263
+ # rms_reproj = np.sqrt(np.mean(res**2))
264
+ # Transformation 3D → caméra
265
+ Xc = R @ (Xw - np.tile(T.reshape(3, 1), (1, nbPts))) # (3 x nbPts)
266
+
267
+ # Normalisation en coordonnées homogènes
268
+ xn = Xc[0:2, :] / np.tile(Xc[2, :], (2, 1)) # (2 x nbPts)
269
+
270
+ # Projection avec les paramètres intrinsèques
271
+ x = np.vstack((fx * xn[0, :] + x0, fy * xn[1, :] + y0)) # (2 x nbPts)
272
+ x = x.T # (nbPts x 2)
273
+
274
+ # Calcul du résidu et RMS
275
+ res = x.reshape(-1) - pts2D.reshape(-1)
276
+ rms_reproj = np.sqrt(np.mean(res**2))
277
+ # --- Affichage résultats ---
278
+ print(f"ang = [{ang[0]*180/np.pi:.2f}, {ang[1]*180/np.pi:.2f}, {ang[2]*180/np.pi:.2f}] deg")
279
+ print(f"T = [{T[0]:.4f}, {T[1]:.4f}, {T[2]:.4f}]")
280
+ if AFFINAGE:
281
+ print(f"nbPts={nbPts} --- RMS : avant={rmsInit:.4f}, après={rms_reproj:.4f}")
282
+ else:
283
+ print(f"nbPts={nbPts} --- RMS={rms_reproj:.4f}")
284
+ return ang, T, rms_reproj
@@ -0,0 +1,167 @@
1
+ def evalTrajecto(repBase, traj_pos, traj_ang, carte=None):
2
+ """
3
+ Évalue une trajectoire estimée par rapport à une trajectoire de référence.
4
+
5
+ Cette fonction compare une trajectoire estimée (positions et angles) avec une solution
6
+ de référence stockée dans des fichiers `.npz`. Elle calcule différents scores d'erreur
7
+ (distance, position, orientation) et affiche une visualisation 3D comparative.
8
+
9
+ Parameters
10
+ ----------
11
+ repBase : str
12
+ Chemin vers le dossier contenant les fichiers `solution.npz` et `info_seq.npz`.
13
+
14
+ traj_pos : ndarray of shape (3, N)
15
+ Positions estimées de la trajectoire. Chaque colonne représente une position 3D
16
+ (X, Y, Z) pour une image donnée.
17
+
18
+ traj_ang : ndarray of shape (3, N)
19
+ Angles d'Euler estimés (rx, ry, rz) pour chaque position. Les angles sont en radians.
20
+
21
+ carte : optional
22
+ Paramètre optionnel (non utilisé dans la fonction actuelle).
23
+
24
+ Returns
25
+ -------
26
+ score_pos1 : float
27
+ Pourcentage d'erreur relative entre la distance totale estimée et la distance réelle.
28
+
29
+ score_pos2 : float
30
+ Pourcentage maximal d'erreur relative entre les distances inter-images estimées et
31
+ celles de la trajectoire de référence.
32
+
33
+ score_ang : float
34
+ Erreur angulaire normalisée (en degrés par mètre), calculée entre la rotation finale
35
+ estimée et la rotation finale réelle.
36
+
37
+ score_posint : list of float
38
+ Intégrale des erreurs de position (X, Y, Z), calculées comme l'aire sous la courbe
39
+ des différences de position au cours du temps.
40
+
41
+ delta_pos : ndarray of shape (3, N-1)
42
+ Différences de position entre la solution de référence et la trajectoire estimée
43
+ pour chaque image (sauf la dernière).
44
+
45
+ Notes
46
+ -----
47
+ - Cette fonction utilise la bibliothèque Plotly pour afficher une figure 3D comparant
48
+ les trajectoires de référence et estimée.
49
+ - Les fichiers `solution.npz` doivent contenir les clés ``sol_pos`` et ``sol_ang``.
50
+ - Le fichier `info_seq.npz` doit contenir la clé ``Kleft``, même si elle n'est pas utilisée
51
+ dans cette fonction.
52
+ - Les angles d'Euler sont interprétés dans l'ordre ZYX pour la conversion en matrices de rotation.
53
+
54
+ Examples
55
+ --------
56
+ >>> score_pos1, score_pos2, score_ang, score_posint, delta_pos = evalTrajecto(
57
+ ... repBase='data/test_seq',
58
+ ... traj_pos=estimated_positions,
59
+ ... traj_ang=estimated_angles
60
+ ... )
61
+ """
62
+
63
+ import numpy as np
64
+ import plotly.graph_objects as go
65
+ import os
66
+
67
+ def ang2rot(angles):
68
+ """
69
+ Convert Euler angles (rx, ry, rz) to rotation matrix.
70
+ Angles are assumed in radians.
71
+ """
72
+ rx, ry, rz = angles
73
+ Rx = np.array([[1, 0, 0],
74
+ [0, np.cos(rx), -np.sin(rx)],
75
+ [0, np.sin(rx), np.cos(rx)]])
76
+ Ry = np.array([[np.cos(ry), 0, np.sin(ry)],
77
+ [0, 1, 0],
78
+ [-np.sin(ry), 0, np.cos(ry)]])
79
+ Rz = np.array([[np.cos(rz), -np.sin(rz), 0],
80
+ [np.sin(rz), np.cos(rz), 0],
81
+ [0, 0, 1]])
82
+ return Rz @ Ry @ Rx
83
+
84
+
85
+ def rodrigues(R):
86
+ """
87
+ Convert rotation matrix to rotation vector (axis-angle) or vice versa.
88
+ """
89
+ if R.shape == (3,):
90
+ # Rotation vector -> matrix
91
+ theta = np.linalg.norm(R)
92
+ if theta < 1e-12:
93
+ return np.eye(3)
94
+ omega = R / theta
95
+ K = np.array([[0, -omega[2], omega[1]],
96
+ [omega[2], 0, -omega[0]],
97
+ [-omega[1], omega[0], 0]])
98
+ return np.eye(3) + np.sin(theta)*K + (1-np.cos(theta))*(K@K)
99
+ elif R.shape == (3, 3):
100
+ # Matrix -> rotation vector
101
+ U, _, Vt = np.linalg.svd(R)
102
+ R = U @ Vt # project onto SO(3)
103
+ theta = np.arccos(np.clip((np.trace(R)-1)/2, -1, 1))
104
+ if theta < 1e-12:
105
+ return np.zeros(3)
106
+ else:
107
+ return (theta/(2*np.sin(theta))) * np.array([R[2,1]-R[1,2],
108
+ R[0,2]-R[2,0],
109
+ R[1,0]-R[0,1]])
110
+ else:
111
+ raise ValueError("Input must be 3-vector or 3x3 matrix")
112
+
113
+
114
+ # Chargement des données MATLAB
115
+ # solution = loadmat(f"{repBase}/solution.mat")
116
+ # info_seq = loadmat(f"{repBase}/info_seq.mat")
117
+ solution = np.load(os.path.join(repBase, 'solution.npz'))
118
+ info_seq = np.load(os.path.join(repBase, 'info_seq.npz'))
119
+ sol_pos = solution['sol_pos']
120
+ sol_ang = solution['sol_ang']
121
+ matK = info_seq['Kleft']
122
+
123
+ nbIma = sol_pos.shape[1]
124
+ interdist_est = []
125
+ delta_pos = []
126
+
127
+ # Calcul des distances et différences de positions
128
+ for cpt in range(nbIma-1):
129
+ interdist_est.append(np.linalg.norm(traj_pos[:,cpt+1]-traj_pos[:,cpt]))
130
+ delta_pos.append(sol_pos[:,cpt] - traj_pos[:,cpt])
131
+
132
+ interdist_est = np.array(interdist_est)
133
+ delta_pos = np.array(delta_pos).T # 3 x (nbIma-1)
134
+
135
+ totaldist_est = np.sum(interdist_est)
136
+ totaldist = np.sum(np.linalg.norm(np.diff(sol_pos, axis=1), axis=0))
137
+ interdist = np.linalg.norm(np.diff(sol_pos, axis=1), axis=0)
138
+
139
+ score_pos1 = 100 * np.abs(totaldist_est - totaldist) / totaldist
140
+ score_pos2 = 100 * np.max(np.abs(interdist_est - interdist) / interdist)
141
+
142
+ # Calcul de l'erreur angulaire
143
+ Rest = ang2rot(traj_ang[:, -1])
144
+ Rtrue = ang2rot(sol_ang[:, -1])
145
+ Rerr = Rest.T @ Rtrue
146
+ rodri = rodrigues(Rerr)
147
+ score_ang = np.linalg.norm(rodri) * 180 / (np.pi * totaldist)
148
+
149
+ score_posint = [np.trapezoid(delta_pos[0,:]), np.trapezoid(delta_pos[1,:]), np.trapezoid(delta_pos[2,:])]
150
+
151
+ print(f"score_pos1 = {score_pos1:.2f}")
152
+ print(f"score_pos2 = {score_pos2:.2f}")
153
+ print(f"score_ang = {score_ang:.2f}")
154
+ print(f"integral Delta pos X = {score_posint[0]:.2f}")
155
+ print(f"integral Delta pos Y = {score_posint[1]:.2f}")
156
+ print(f"integral Delta pos Z = {score_posint[2]:.2f}")
157
+
158
+ # ==== Affichage comparatif ====
159
+ fig = go.Figure()
160
+ fig.add_trace(go.Scatter3d(x=sol_pos[0,:], y=sol_pos[1,:], z=sol_pos[2,:], mode='lines', line=dict(color='red'), name='ref'))
161
+ fig.add_trace(go.Scatter3d(x=traj_pos[0,:], y=traj_pos[1,:], z=traj_pos[2,:], mode='lines', line=dict(color='blue'), name='estim'))
162
+ fig.update_layout(title='Comparaison estimation de la trajectoire 3D (rouge=ref)',
163
+ scene=dict(xaxis_title='X', yaxis_title='Y', zaxis_title='Z'),
164
+ legend=dict(x=0, y=1))
165
+ fig.show()
166
+
167
+ return score_pos1, score_pos2, score_ang, score_posint, delta_pos
@@ -0,0 +1,216 @@
1
+ import numpy as np
2
+ import plotly.graph_objects as go
3
+
4
+ def vue3D(fig, pos3D, matK, sizeim, attitude, position, camColor, operation):
5
+ """
6
+ Affiche une vue 3D interactive de nuages de points et de caméras avec Plotly.
7
+
8
+ Parameters
9
+ ----------
10
+ fig : plotly.graph_objects.Figure
11
+ Objet figure Plotly dans lequel la scène 3D est affichée ou mise à jour.
12
+ pos3D : ndarray
13
+ Coordonnées 3D des points dans le repère monde (array de forme (3, N)).
14
+ matK : ndarray
15
+ Matrice des paramètres intrinsèques de la caméra (3x3).
16
+ sizeim : tuple of int
17
+ Dimensions de l'image (hauteur, largeur), utilisées pour le champ de vision.
18
+ attitude : ndarray
19
+ Angles d'attitude de la caméra (angles d'Euler ou matrice de rotation).
20
+ position : ndarray
21
+ Position de la caméra dans le repère monde (vecteur de taille 3).
22
+ camColor : str or tuple
23
+ Couleur utilisée pour représenter la caméra dans la scène.
24
+ operation : {'create', 'addcam', 'addpts'}
25
+ Type d'opération à effectuer :
26
+ - 'create' : crée une nouvelle scène 3D,
27
+ - 'addcam' : ajoute une caméra à la scène,
28
+ - 'addpts' : ajoute un nuage de points à la scène.
29
+
30
+ Returns
31
+ -------
32
+ fig : plotly.graph_objects.Figure
33
+ Objet figure Plotly dans lequel la scène 3D est affichée ou mise à jour.
34
+
35
+ Notes
36
+ -----
37
+ Cette fonction permet de visualiser des scènes 3D de manière interactive avec Plotly.
38
+ Elle peut être utilisée pour représenter des caméras virtuelles, des trajectoires,
39
+ ou des nuages de points 3D dans un repère monde, avec une gestion dynamique
40
+ des éléments affichés selon l'opération demandée.
41
+
42
+ Example
43
+ -------
44
+ >>> from imicpe.reco3D import vue3D
45
+ >>> import plotly.graph_objects as go
46
+ >>> fig = go.Figure()
47
+ >>> vue3D(fig, pos3D, matK, (480, 640), attitude, position, 'blue', 'create')
48
+ """
49
+
50
+ # """
51
+ # 3D view function for displaying point clouds and cameras.
52
+
53
+ # :param fig: The figure handler
54
+ # :param pos3D: 3D positions of the points in world coordinates (3xN array)
55
+ # :param matK: Intrinsic camera parameters
56
+ # :param sizeim: Image size (height, width)
57
+ # :param attitude: Camera attitude angles (Euler angles)
58
+ # :param position: Camera position in world coordinates
59
+ # :param camColor: Camera display color
60
+ # :param operation: Display operation, 'create', 'addcam', or 'addpts'
61
+ # """
62
+ seuil_dist = 30 # threshold for deleting points that are too far away
63
+ # Check for correct number of arguments
64
+ if fig is not None and type(fig) is not go.Figure:
65
+ print('First argument must be a Plotly figure')
66
+ return
67
+
68
+ if len(attitude) == 0 or len(position) == 0:
69
+ print('Attitude and position should be provided')
70
+ return
71
+
72
+ if (pos3D.shape[0] > 3):
73
+ pos3D=pos3D.T
74
+ print('warning : le tableau pos3D doit etre 3xN')
75
+
76
+ # Prepare params
77
+ params = {
78
+ 'fig': fig,
79
+ 'pt3DStyle': 'blue',
80
+ 'camColor': camColor,
81
+ 'holdon': False if operation == 'create' else True,
82
+ 'tail': 1,
83
+ }
84
+
85
+ nb_points = pos3D.shape[1]
86
+
87
+
88
+ # Calculate distance between camera and 3D points
89
+ if len(position) == 0:
90
+ tensP = attitude
91
+ vec = pos3D - np.repeat(tensP[:, 3].reshape(3, 1), nb_points, axis=1)
92
+ dist = np.linalg.norm(vec, axis=0)
93
+ else:
94
+ Rloc = _euler_to_rot(attitude, 'zxy')
95
+ tensP = np.hstack([Rloc, -np.dot(Rloc, position.reshape(-1, 1))])
96
+ vec = pos3D - np.repeat(position.reshape(3, 1), nb_points, axis=1)
97
+ dist = np.linalg.norm(vec, axis=0)
98
+
99
+ # Filter points that are too far away
100
+ indOK = np.where(dist < seuil_dist)[0]
101
+ pos3D_filtered = pos3D[:, indOK]
102
+ if fig is None:
103
+ # Create the plotly figure
104
+ fig = go.Figure()
105
+
106
+ # Operation: 'addpts' means just add 3D points
107
+ if operation == 'addpts':
108
+ fig.add_trace(go.Scatter3d(
109
+ x=pos3D_filtered[0, :],
110
+ y=pos3D_filtered[1, :],
111
+ z=pos3D_filtered[2, :],
112
+ mode='markers',
113
+ marker=dict(color='red', size=5)
114
+ ))
115
+
116
+ # Operation: 'addcam' means add the camera only
117
+ elif operation == 'addcam':
118
+ _plot_camera(tensP, matK, sizeim, fig, camColor)
119
+
120
+ # Operation: 'create' means both camera and 3D points
121
+ else:
122
+ fig.add_trace(go.Scatter3d(
123
+ x=pos3D_filtered[0, :],
124
+ y=pos3D_filtered[1, :],
125
+ z=pos3D_filtered[2, :],
126
+ mode='markers',
127
+ marker=dict(color='blue', size=5)
128
+ ))
129
+ _plot_camera(tensP, matK, sizeim, fig, camColor)
130
+
131
+ # Set figure layout
132
+ fig.update_layout(
133
+ scene=dict(
134
+ xaxis_title='X',
135
+ yaxis_title='Y',
136
+ zaxis_title='Z',
137
+ ),
138
+ title=f"3D View",
139
+ width=800,
140
+ height=800,
141
+ )
142
+
143
+ return fig
144
+
145
+ def _plot_camera(tensP, matK, sizeim, fig, camColor):
146
+ """
147
+ Plots the camera as a cone shape in the 3D space.
148
+ """
149
+ if len(tensP.shape) < 3:
150
+ tensP = tensP[:,:,np.newaxis]
151
+ nb_cam = tensP.shape[2]
152
+
153
+ imasize = [sizeim[1], sizeim[0]]#np.fliplr(sizeim).squeeze() # Flip size to [width, height]
154
+ for cptC in range(nb_cam):
155
+ current_tensP = tensP[:, :, cptC]
156
+ cone_vertices = np.zeros((3, 5))
157
+ vtmp = np.array([
158
+ [1, imasize[0], imasize[0], 1],
159
+ [1, 1, imasize[1], imasize[1]],
160
+ [1, 1, 1, 1]
161
+ ])
162
+
163
+ matK_inv = np.linalg.inv(matK)
164
+ cone_vertices[:, 1:] = np.dot(matK_inv, vtmp) * 2 # Use tail length as scaling factor
165
+
166
+
167
+
168
+ cVWorld = np.dot(current_tensP[:, :3].T, (cone_vertices - current_tensP[:, 3].reshape(3, 1)))
169
+
170
+ # Add the cone edges to the plot
171
+ for i in range(4):
172
+ fig.add_trace(go.Scatter3d(
173
+ x=[cVWorld[0, 0], cVWorld[0, i + 1]],
174
+ y=[cVWorld[1, 0], cVWorld[1, i + 1]],
175
+ z=[cVWorld[2, 0], cVWorld[2, i + 1]],
176
+ mode='lines',
177
+ line=dict(color=camColor, width=2),
178
+ showlegend=False # Exclude from legend
179
+ ))
180
+
181
+ fig.add_trace(go.Scatter3d(
182
+ x=[cVWorld[0, i + 1], cVWorld[0, (i + 1) % 4 + 1]], # Cycle through the vertices
183
+ y=[cVWorld[1, i + 1], cVWorld[1, (i + 1) % 4 + 1]],
184
+ z=[cVWorld[2, i + 1], cVWorld[2, (i + 1) % 4 + 1]],
185
+ mode='lines',
186
+ line=dict(color=camColor, width=2),
187
+ showlegend=False # Exclude from legend
188
+ ))
189
+
190
+ def _euler_to_rot(euler_angles, euler_order='xyz'):
191
+ """
192
+ Convert Euler angles to rotation matrix using the specified order.
193
+ """
194
+ cx, cy, cz = np.cos(np.radians(euler_angles.squeeze()))
195
+ sx, sy, sz = np.sin(np.radians(euler_angles.squeeze()))
196
+
197
+ R_x = np.array([[1, 0, 0],
198
+ [0, cx, -sx],
199
+ [0, sx, cx]])
200
+
201
+ R_y = np.array([[cy, 0, sy],
202
+ [0, 1, 0],
203
+ [-sy, 0, cy]])
204
+
205
+ R_z = np.array([[cz, -sz, 0],
206
+ [sz, cz, 0],
207
+ [0, 0, 1]])
208
+
209
+ if euler_order == 'xyz':
210
+ return np.dot(R_z, np.dot(R_y, R_x))
211
+ elif euler_order == 'zyx':
212
+ return np.dot(R_x, np.dot(R_y, R_z))
213
+ # Other orders can be added as needed
214
+
215
+ return np.eye(3) # Default to identity if no valid order is found
216
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imicpe
3
- Version: 1.0.7
3
+ Version: 1.0.8.1
4
4
  Summary: Toolbox for Maths,Signal,Image Teaching @ CPE
5
5
  Author-email: Marion Foare <marion.foare@cpe.fr>, Eric Van Reeth <eric.vanreeth@cpe.fr>, Arthur Gautheron <arthur.gautheron@cpe.fr>
6
6
  License: MIT License
@@ -21,4 +21,9 @@ src/imicpe/optim/metrics.py
21
21
  src/imicpe/optim/operators.py
22
22
  src/imicpe/optim/pnnDataset.py
23
23
  src/imicpe/optim/pnnTrainer.py
24
- src/imicpe/optim/pnnUtils.py
24
+ src/imicpe/optim/pnnUtils.py
25
+ src/imicpe/reco3D/__init__.py
26
+ src/imicpe/reco3D/angles.py
27
+ src/imicpe/reco3D/calculPose.py
28
+ src/imicpe/reco3D/evalTrajecto.py
29
+ src/imicpe/reco3D/vue3D.py
@@ -1 +0,0 @@
1
- __version__="1.0.7"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes