Homogene Koordinaten - Musterlösung

  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