1import cv2
2import numpy as np
3from misc import build_axis, build_cube, build_grid, Mode
4
5
6def translate_3d(x, y, z):
7 """
8 **TODO**: Return a homogeneous translation matrix which moves coordinates by (x, y, z) as presented during lecture.
9
10 :return: Translation matrix moving coordinates by (x, y, z)
11 :return type: 4x4 np.array
12 """
13 return np.array(
14 [
15 [1.0, 0.0, 0.0, x],
16 [0.0, 1.0, 0.0, y],
17 [0.0, 0.0, 1.0, z],
18 [0.0, 0.0, 0.0, 1.0],
19 ]
20 )
21
22
23def scale(x, y, z):
24 """
25 **TODO**: Return a homogeneous scaling matrix which scales axes by (x, y, z) as presented during lecture.
26
27 :return: Scaling matrix by (x, y, z)
28 :return type: 4x4 np.array
29 """
30 return np.array(
31 [
32 [x, 0.0, 0.0, 0.0],
33 [0.0, y, 0.0, 0.0],
34 [0.0, 0.0, z, 0.0],
35 [0.0, 0.0, 0.0, 1.0],
36 ]
37 )
38
39
40def rotateX(radians):
41 """
42 **TODO**: Return a homogeneous rotation matrix which rotates around the
43 X-axis by a given amount as presented during the lecture.
44
45 :param radians: Angle by how far to rotate (in radians)
46 :return: Rotation matrix around X-axis by given amount
47 :return type: 4x4 np.array
48 """
49 s, c = np.sin(radians), np.cos(radians)
50
51 return np.array(
52 [
53 [1.0, 0.0, 0.0, 0.0],
54 [0.0, c, -s, 0.0],
55 [0.0, s, c, 0.0],
56 [0.0, 0.0, 0.0, 1.0],
57 ]
58 )
59
60
61def rotateY(radians):
62 """
63 **TODO**: Return a homogeneous rotation matrix which rotates around the
64 Y-axis by a given amount as presented during the lecture.
65
66 :param radians: Angle by how far to rotate (in radians)
67 :return: Rotation matrix around X-axis by given amount
68 :return type: 4x4 np.array
69 """
70 s, c = np.sin(radians), np.cos(radians)
71
72 return np.array(
73 [
74 [c, 0.0, s, 0.0],
75 [0.0, 1.0, 0.0, 0.0],
76 [-s, 0.0, c, 0.0],
77 [0.0, 0.0, 0.0, 1.0],
78 ]
79 )
80
81
82def rotateZ(radians):
83 """
84 **TODO**: Return a homogeneous rotation matrix which rotates around the
85 Z-axis by a given amount as presented during the lecture.
86
87 :param radians: Angle by how far to rotate (in radians)
88 :return: Rotation matrix around X-axis by given amount
89 :return type: 4x4 np.array
90 """
91 s, c = np.sin(radians), np.cos(radians)
92
93 return np.array(
94 [
95 [c, -s, 0.0, 0.0],
96 [s, c, 0.0, 0.0],
97 [0.0, 0.0, 1.0, 0.0],
98 [0.0, 0.0, 0.0, 1.0],
99 ]
100 )
101
102
103def rotateXYZ(x, y, z):
104 """
105 **TODO** Return a combined rotation matrix which first rotates around X, then Y then Z
106 by the given amounts of radians.
107
108 :param x: Angle to rotate around X (in radians)
109 :param y: Angle to rotate around Y (in radians)
110 :param z: Angle to rotate around Z (in radians)
111 :return: Homogeneous matrix applying rotations around X, Y and Z
112 :return type: 4x4 np.array
113 """
114 return rotateZ(z) @ rotateY(y) @ rotateX(x)
115
116
117def projection(c):
118 """
119 **TODO**: Return a projection matrix which projects along the Z-axis as presented during the lecture.
120
121 :param c: Focal length of pin hole camera
122 :return: Projection matrix
123 :return type: 4x4 np.array
124 """
125 return np.array(
126 [
127 [-c, 0.0, 0.0, 0.0],
128 [0.0, -c, 0.0, 0.0],
129 [0.0, 0.0, 1.0, 0.0],
130 [0.0, 0.0, 1.0, 0.0],
131 ]
132 )
133
134
135def ndc_to_image(W, H):
136 """
137 **TODO**: Return the matrix which transforms NDC-coordinates to pixel coordinates in the final image
138
139 Do the following transformation in this particular order
140
141 For this tutorial, you can use a combination of :py:func:`translate_3d` and :py:func:`scale`
142 or write the transformation matrix directly according to the script
143
144 - Scale by (W/2, H/2, 1.0)
145 - Translate by (W/2, H/2, 0.0)
146 """
147 return translate_3d(W / 2.0, H / 2.0, 0.0) @ scale(W / 2.0, H / 2.0, 1.0)
148
149
150def world_to_camera():
151 """
152 **TODO**: Return the matrix which transforms world to camera (view) coordinates.
153 For this tutorial, use a combination of :py:func:`translate_3d`,
154 :py:func:`rotateX` and :py:func:`rotateY`.
155
156 Do the following transformation in this particular order
157
158 - Rotate around Y by 35 degrees.
159 - Rotate around X by -30 degrees
160 - Translate by (0, 16, 164)
161
162 **Hint**: You can use `np.deg2rad <https://numpy.org/doc/2.1/reference/generated/numpy.deg2rad.html>`_ to convert degrees to radians.
163
164 :return: Homogeneous matrix defining the transformation from world to camera coordinate space
165 :return type: 4x4 np.array
166 """
167 return (
168 translate_3d(0, 16.0, 164.0)
169 @ rotateX(np.deg2rad(-30.0))
170 @ rotateY(np.deg2rad(35.0))
171 )
172
173
174def world_to_image(W, H, c):
175 """
176 **TODO**: Return the complete projection matrix mapping from world coordinates to image coordinates.
177 This is a concatenation of the `world_to_camera` matrix, the projection matrix and the mapping from
178 NDC to image coordinates. Do the following transformations in the particular order
179
180 - Map :py:func:`world_to_camera` by calling the respective function
181 - Project along the z axis by calling :py:func:`projection`
182 - Map :py:func:`ndc_to_image` by calling the respective function
183
184 :return: Homogeneous matrix defining the transformation from world to image coordinates
185 :return type: 4x4 np.array
186 """
187 return ndc_to_image(W, H) @ projection(c) @ world_to_camera()
188
189
190def local_to_world(objectScale, objectRotate, objectTranslate, objectOrbit):
191 """
192 **TODO**: Return the transformation from local coordinates to world coordinates.
193 Apply the following transformations in this particular order
194
195 - Scale by the parameters provided in objectScale
196 - Rotate by the parameters provided in objectRotate
197 - Translate by the parameters provided in objectTranslate
198 - Rotate again by the parameters provided in objectOrbit.
199
200 *Note*: Because the second rotation happens after the translation it will "orbit" around the
201 center.
202
203 :param objectScale: 3 element array with scaling parameters
204 :param objectRotate: 3 element array with rotation parameters (in radians)
205 :param objectTranslate: 3 element array with translation parameters
206 :param objectOrbit: 3 element array with orbiting parameters (in radians)
207 """
208 return (
209 rotateXYZ(objectOrbit[0], objectOrbit[1], objectOrbit[2])
210 @ translate_3d(objectTranslate[0], objectTranslate[1], objectTranslate[2])
211 @ rotateXYZ(objectRotate[0], objectRotate[1], objectRotate[2])
212 @ scale(objectScale[0], objectScale[1], objectScale[2])
213 )
214
215
216def project_vertexbuffer(local_to_image, vertices):
217 """
218 **TODO**: Project all vertices in the provided vertex buffer using the given transformation
219 and convert to euclidean coordinates by dividing by the w-component of each vertex.
220
221 :param vertices: Vertex buffer (4xN Matrix)
222 :param local_to_image: Transformation matrix to apply (4x4 Matrix)
223 :return: Transformed vertices in euclidean space (w == 1)
224 """
225 # First, project all vertices using the given transformation
226 vertices = local_to_image @ vertices
227
228 # Now divide by w to convert to euclidean coordinates
229 vertices /= vertices[3, :]
230
231 return vertices
232
233
234def draw(mesh, local_to_image, canvas, col):
235 """
236 Draws a given mesh using the provided projection matrix into the given canvas using provided color.
237
238 :param mesh: 2-Tuple (vertices, indices) containing both the vertex buffer as well as the index buffer
239 :param local_to_image: Transformation matrix to transform vertices from local space to image space
240 :param canvas: OpenCV image to draw into (3 channel RGB, np.float32)
241 :param col: Color to draw (3-Tuple with (B, G, R) color intensities ranging from 0.0 to 1.0 each)
242 """
243 # Unpack mesh
244 vertices, indices = mesh
245
246 # Project vertices
247 vertices = project_vertexbuffer(local_to_image, vertices)
248
249 # Go through list of indices
250 for lineIndex in range(0, indices.shape[0], 2):
251 # Indirect access
252 indexA = indices[lineIndex]
253 indexB = indices[lineIndex + 1]
254 A = vertices[:, indexA]
255 B = vertices[:, indexB]
256
257 cv2.line(canvas, (int(A[0]), int(A[1])), (int(B[0]), int(B[1])), col)
258
259
260# Geschafft, ab hier brauchen Sie nichts mehr zu implementieren!
261if __name__ == "__main__":
262 # Wir starten im Modus "Verschiebung"
263 mode = Mode.TRANSLATE
264
265 # Erzeuge die Meshes für das Koordinatensystem, den Würfel und die Achsen
266 gridMesh = build_grid(np.linspace(-64.0, 64.0, 9), np.linspace(-64.0, 64.0, 9))
267 cubeMesh = build_cube()
268 axisX = build_axis(32.0, 0.0, 0.0)
269 axisY = build_axis(0.0, 32.0, 0.0)
270 axisZ = build_axis(0.0, 0.0, 32.0)
271
272 # Unser Bild soll 1024x1024 Pixel haben
273 image_shape = (1024, 1024)
274
275 # Wir brauchen die zentrale Transformation von Weltkoordinaten nach Bildkoordinaten
276 w2i = world_to_image(image_shape[1], image_shape[0], 1.25)
277
278 # Die Welt-Transformation der Achsen (passend verschoben)
279 axis_to_world = translate_3d(-64.0, -16.0, -64.0)
280
281 # Die Welt-Transformation des Koordinatensystems (nach unten verschoben)
282 grid_to_world = translate_3d(0.0, -16.0, 0.0)
283
284 # Die Texte im UI
285 modeTexts = ["(1) Scale", "(2) Translate", "(3) Rotate", "(4) Orbit"]
286
287 # Anfangsparameter für die Objekttransformation des Würfels
288 objectScale = np.array([16.0, 16.0, 16.0])
289 objectTranslate = np.array([0.0, 0.0, 0.0])
290 objectRotate = np.array([0.0, 0.0, 0.0])
291 objectOrbit = np.array([0.0, 0.0, 0.0])
292
293 # Endloßßschleife (mit ESC unterbrechen)
294 while True:
295 # Baue die aktuelle Welt-Transformation für den Würfel
296 cube_to_world = local_to_world(
297 objectScale, objectRotate, objectTranslate, objectOrbit
298 )
299
300 # Starte mit einem leeren (schwarzen) Bild
301 canvas = np.zeros((image_shape[0], image_shape[1], 3))
302
303 # Zeichne die verschiedenen Komponenten in ihren jeweiligen Farben.
304 # Verwende dabei die passenden Transformationen
305 draw(gridMesh, w2i @ grid_to_world, canvas, (0.2, 0.2, 0.2))
306 draw(axisX, w2i @ axis_to_world, canvas, (0.0, 0.0, 1.0))
307 draw(axisY, w2i @ axis_to_world, canvas, (0.0, 1.0, 0.0))
308 draw(axisZ, w2i @ axis_to_world, canvas, (1.0, 0.0, 0.0))
309 draw(cubeMesh, w2i @ cube_to_world, canvas, (1.0, 1.0, 1.0))
310
311 # Zeichne das User-Interface
312 for index, modeText in enumerate(modeTexts):
313 x = 16 + 120 * index
314 col = (1.0, 1.0, 1.0)
315 if index == int(mode) - 1:
316 col = (0.4, 0.6, 1.0)
317
318 if mode == Mode.TRANSLATE:
319 vx, vy, vz = (
320 objectTranslate[0],
321 objectTranslate[1],
322 objectTranslate[2],
323 )
324
325 if mode == Mode.SCALE:
326 vx, vy, vz = objectScale[0], objectScale[1], objectScale[2]
327
328 if mode == Mode.ROTATE:
329 vx, vy, vz = (
330 np.rad2deg(objectRotate[0]),
331 np.rad2deg(objectRotate[1]),
332 np.rad2deg(objectRotate[2]),
333 )
334
335 if mode == Mode.ORBIT:
336 vx, vy, vz = (
337 np.rad2deg(objectOrbit[0]),
338 np.rad2deg(objectOrbit[1]),
339 np.rad2deg(objectOrbit[2]),
340 )
341
342 cv2.putText(
343 canvas,
344 f"{vx:.2f}, {vy:.2f}, {vz:.2f}",
345 (x, 40),
346 cv2.FONT_HERSHEY_SIMPLEX,
347 0.5,
348 (0.7, 0.7, 0.7),
349 2,
350 )
351
352 cv2.putText(
353 canvas, modeText, (x, 16), cv2.FONT_HERSHEY_SIMPLEX, 0.5, col, 2
354 )
355
356 # Übergebe das Bild ans Betriebssystem und warte auf einen Tastendruck
357 cv2.imshow("Canvas", canvas)
358
359 key = cv2.waitKey(0)
360 if key == ord("1"):
361 mode = Mode.SCALE
362
363 if key == ord("2"):
364 mode = Mode.TRANSLATE
365
366 if key == ord("3"):
367 mode = Mode.ROTATE
368
369 if key == ord("4"):
370 mode = Mode.ORBIT
371
372 delta = np.array([0.0, 0.0, 0.0])
373 if key == ord("a"):
374 delta[0] = 1.0
375 if key == ord("d"):
376 delta[0] = -1.0
377 if key == ord("w"):
378 delta[1] = 1.0
379 if key == ord("s"):
380 delta[1] = -1.0
381 if key == ord("+"):
382 delta[2] = 1.0
383 if key == ord("-"):
384 delta[2] = -1.0
385
386 if mode == Mode.SCALE:
387 objectScale += delta
388
389 if mode == Mode.TRANSLATE:
390 objectTranslate += delta
391
392 if mode == Mode.ROTATE:
393 objectRotate += np.deg2rad(delta)
394
395 if mode == Mode.ORBIT:
396 objectOrbit += np.deg2rad(delta)
397
398 if key == 27:
399 break