diff --git a/np.py b/np.py index 298653d..c8bdec7 100644 --- a/np.py +++ b/np.py @@ -3,15 +3,15 @@ import time import math import os from PIL import Image, ImageDraw -import random # Importiert für Zufallszahlen +import random # --- Hilfsfunktionen --- def normalize(v): """ Normalisiert einen Vektor (macht seine Länge zu 1). """ norm = np.linalg.norm(v) - if norm < 1e-9: # Sicherer Vergleich mit kleiner Toleranz - return np.zeros_like(v) # Gibt Nullvektor zurück, wenn Norm zu klein ist + if norm < 1e-9: + return np.zeros_like(v) return v / norm def look_at_matrix(eye, target, up): @@ -22,42 +22,31 @@ def look_at_matrix(eye, target, up): :param up: Up-Vektor der Kamera (zeigt, wo "oben" für die Kamera ist, np.array). :return: 4x4 View-Matrix (NumPy-Array). """ - # Robustere Berechnung, falls eye und target sehr nah sind if np.linalg.norm(eye - target) < 1e-6: - # Wenn Kamera und Ziel fast am selben Ort sind, ist die Richtung undefiniert. - # Wähle eine Standardrichtung, z.B. Blick entlang -Z der Welt vom 'eye'. - target = eye - np.array([0.0, 0.0, 1.0]) + target = eye - np.array([0.0, 0.0, 1.0]) # Standard-Blickrichtung bei Kollision - forward = normalize(eye - target) # Konvention: Kamera schaut entlang -Z - - # Prüfe, ob 'up' und 'forward' (fast) parallel sind - # Normalisiere 'up' vor dem Test für mehr Robustheit + forward = normalize(eye - target) up_normalized = normalize(up) - if abs(np.dot(up_normalized, forward)) > 0.999: - # Fallback, wenn 'up' und 'forward' (fast) parallel sind - if abs(forward[1]) < 0.99: # Wenn forward nicht fast vertikal ist - temp_up = np.array([0.0, 1.0, 0.0]) - else: # Wenn forward fast vertikal ist - temp_up = np.array([0.0, 0.0, -1.0 if forward[1] > 0 else 1.0]) - right = normalize(np.cross(temp_up, forward)) - else: - # Nutze das normalisierte 'up' für die Berechnung - right = normalize(np.cross(up_normalized, forward)) - # Den tatsächlichen Up-Vektor der Kamera neu berechnen, damit er orthogonal ist + if abs(np.dot(up_normalized, forward)) > 0.999: + if abs(forward[1]) < 0.99: + temp_up = np.array([0.0, 1.0, 0.0]) + else: + temp_up = np.array([0.0, 0.0, -1.0 if forward[1] > 0 else 1.0]) + right = normalize(np.cross(temp_up, forward)) + else: + right = normalize(np.cross(up_normalized, forward)) + camera_up = normalize(np.cross(forward, right)) - # Rotationsmatrix erstellen rotation = np.identity(4) rotation[0, 0:3] = right rotation[1, 0:3] = camera_up rotation[2, 0:3] = forward - # Translationsmatrix erstellen translation = np.identity(4) translation[0:3, 3] = -eye - # View-Matrix: Zuerst Translation, dann Rotation view_mat = np.dot(rotation, translation) return view_mat @@ -65,79 +54,65 @@ def perspective_projection(point_cam_space, fov_deg, aspect_ratio, near, far): """ Projiziert einen Punkt aus dem Kamera-Koordinatenraum auf eine 2D-Ebene. Gibt normalisierte Gerätekoordinaten (NDC) zurück (-1 bis +1). - :param point_cam_space: Punkt in Kamera-Koordinaten (3D NumPy-Array). - :param fov_deg: Field of View (vertikal) in Grad. - :param aspect_ratio: Seitenverhältnis (Breite / Höhe) der Bildebene. - :param near: Nahe Clipping-Ebene. - :param far: Ferne Clipping-Ebene. - :return: Projizierter 2D-Punkt in NDC (NumPy-Array) oder None, wenn außerhalb. """ - # Kamera schaut entlang -Z, Z muss negativ sein und innerhalb der Clipping-Grenzen liegen if point_cam_space[2] > -near or point_cam_space[2] < -far: - return None # Außerhalb des Sichtfelds (vor near oder hinter far) - - # Verhindert Division durch Null, falls Punkt genau auf der Kameraebene liegt (z=0) + return None if abs(point_cam_space[2]) < 1e-9: return None - # Skalierungsfaktor basierend auf FOV f = 1.0 / math.tan(math.radians(fov_deg) / 2.0) - - # Projizierte Koordinaten auf der Bildebene (NDC) x_ndc = (f / aspect_ratio) * point_cam_space[0] / -point_cam_space[2] y_ndc = f * point_cam_space[1] / -point_cam_space[2] - # Überprüfen, ob die projizierten Koordinaten innerhalb des NDC-Bereichs [-1, 1] liegen if not (-1 <= x_ndc <= 1 and -1 <= y_ndc <= 1): - return None # Außerhalb des seitlichen Sichtfelds + return None return np.array([x_ndc, y_ndc]) def ndc_to_pixel(ndc_coords, width, height): """ Wandelt NDC-Koordinaten (-1 bis 1) in Pixelkoordinaten (0 bis width/height) um. """ pixel_x = (ndc_coords[0] + 1.0) / 2.0 * width - # Y-Koordinate invertieren (NDC +Y ist oben, Pixel +Y ist unten) - pixel_y = (1.0 - ndc_coords[1]) / 2.0 * height + pixel_y = (1.0 - ndc_coords[1]) / 2.0 * height # Y invertieren px = int(round(pixel_x)) py = int(round(pixel_y)) - # Clipping auf Bildgrenzen px = max(0, min(width - 1, px)) py = max(0, min(height - 1, py)) return px, py # --- Simulationsparameter --- -object_start_pos = np.array([0.0, 0.0, 10.0]) # Startposition des Objekts (x, y, z) +object_start_pos = np.array([0.0, 0.0, 10.0]) -# --- NEU: Parameter für zufällige Bewegung --- -# Einfacher Random Walk: In jedem Schritt eine zufällige Verschiebung -random_step_scale = 0.8 # Maximale Verschiebung pro Achse pro Zeitschritt -# Optional: Begrenzung des Bewegungsraums (Bounding Box) -# Damit das Objekt nicht zu weit wegdriftet +# --- NEU: Fixer Zielpunkt für die Kameras --- +# Die Kameras schauen immer auf die ursprüngliche Startposition des Objekts. +# Alternativ könnte hier jeder andere feste Punkt im Raum definiert werden. +fixed_camera_target = object_start_pos.copy() + +# Parameter für zufällige Bewegung +random_step_scale = 0.8 world_bounds_min = np.array([-10.0, -5.0, 5.0]) world_bounds_max = np.array([ 10.0, 5.0, 15.0]) - -# Kameraeinstellungen (Namen ohne Leerzeichen für Dateinamen angepasst) +# Kameraeinstellungen cameras = [ { "name": "Kamera_1_Frontal", "pos": np.array([0.0, 0.0, 0.0]), - "target": object_start_pos, # Schaut initial auf den Startpunkt + "target": fixed_camera_target, # Verwendet den fixen Zielpunkt "up": np.array([0.0, 1.0, 0.0]), "fov_deg": 60, }, { "name": "Kamera_2_Seitlich_Links", "pos": np.array([-15.0, 0.0, 5.0]), - "target": object_start_pos, + "target": fixed_camera_target, # Verwendet den fixen Zielpunkt "up": np.array([0.0, 1.0, 0.0]), "fov_deg": 45, }, { "name": "Kamera_3_Von_Oben", "pos": np.array([0.0, 15.0, 10.0]), - "target": object_start_pos, - "up": np.array([0.0, 0.0, -1.0]), # Blickrichtung -Y, Z nach unten ist "up" + "target": fixed_camera_target, # Verwendet den fixen Zielpunkt + "up": np.array([0.0, 0.0, -1.0]), "fov_deg": 70, } ] @@ -146,15 +121,15 @@ cameras = [ near_plane = 0.1 far_plane = 100.0 -simulation_duration = 10 # Sekunden -time_step = 1 # Zeitintervall für "Aufnahmen" in Sekunden +simulation_duration = 10 +time_step = 1 -# --- Bildparameter --- +# Bildparameter IMG_WIDTH = 320 IMG_HEIGHT = 180 OUTPUT_DIR = "./bilder" -# --- Ausgabeverzeichnis erstellen --- +# Ausgabeverzeichnis erstellen os.makedirs(OUTPUT_DIR, exist_ok=True) print(f"Bilder werden in '{OUTPUT_DIR}' gespeichert.") @@ -167,17 +142,12 @@ print("Starte Simulation...") while current_time <= simulation_duration: print(f"\n--- Zeitpunkt: {current_time:.1f}s ---") - # --- NEU: Objektposition zufällig aktualisieren --- - # Generiere eine zufällige Verschiebung für diesen Zeitschritt - delta_x = (random.random() - 0.5) * 2 * random_step_scale # Zufallszahl zwischen -scale und +scale + # Objektposition zufällig aktualisieren + delta_x = (random.random() - 0.5) * 2 * random_step_scale delta_y = (random.random() - 0.5) * 2 * random_step_scale delta_z = (random.random() - 0.5) * 2 * random_step_scale random_displacement = np.array([delta_x, delta_y, delta_z]) - - # Wende die Verschiebung an current_object_pos += random_displacement - - # Optional: Begrenze die Position auf die definierte Bounding Box current_object_pos = np.clip(current_object_pos, world_bounds_min, world_bounds_max) print(f"Objekt Welt-Position: ({current_object_pos[0]:.2f}, {current_object_pos[1]:.2f}, {current_object_pos[2]:.2f})") @@ -190,62 +160,58 @@ while current_time <= simulation_duration: print(f"\n Kamera {i+1}: {cam['name']}") print(f" Position: ({cam['pos'][0]:.2f}, {cam['pos'][1]:.2f}, {cam['pos'][2]:.2f})") - # Kamera schaut immer auf die aktuelle Objektposition - current_target = current_object_pos - # Seitenverhältnis basierend auf Bilddimensionen für die Projektion verwenden + # --- WICHTIGE ÄNDERUNG: Verwende den *fixen* Zielpunkt --- + # Die Kamera schaut *nicht* mehr auf current_object_pos, sondern auf cam['target'], + # welches im Kamera-Dictionary auf fixed_camera_target gesetzt wurde. + current_target_for_view = cam['target'] # Dies ist jetzt immer fixed_camera_target + effective_aspect_ratio = IMG_WIDTH / IMG_HEIGHT - print(f" Schaut auf (Target): ({current_target[0]:.2f}, {current_target[1]:.2f}, {current_target[2]:.2f})") + # Ausgabe zeigt den fixen Zielpunkt an + print(f" Schaut auf (Fixer Target): ({current_target_for_view[0]:.2f}, {current_target_for_view[1]:.2f}, {current_target_for_view[2]:.2f})") print(f" Up-Vektor: ({cam['up'][0]:.2f}, {cam['up'][1]:.2f}, {cam['up'][2]:.2f})") print(f" FOV: {cam['fov_deg']} Grad, Aspekt (für Projektion): {effective_aspect_ratio:.2f}") - # View-Matrix berechnen - view_mat = look_at_matrix(cam['pos'], current_target, cam['up']) + # View-Matrix mit dem fixen Zielpunkt berechnen + view_mat = look_at_matrix(cam['pos'], current_target_for_view, cam['up']) # Objektposition in Kamera-Koordinaten transformieren + # Dieser Schritt ist weiterhin nötig, um das *bewegliche* Objekt zu projizieren point_in_cam_space_h = np.dot(view_mat, object_pos_h) - if point_in_cam_space_h[3] <= 1e-9: # Sicherer Vergleich mit w + if point_in_cam_space_h[3] <= 1e-9: print(" Objekt hinter oder auf der Kameraebene (w <= 0).") projected_point_ndc = None - point_in_cam_space = np.array([np.nan, np.nan, np.nan]) # Ungültig + point_in_cam_space = np.array([np.nan, np.nan, np.nan]) else: point_in_cam_space = point_in_cam_space_h[:3] / point_in_cam_space_h[3] - print(f" Objekt in Kamera-Koordinaten: ({point_in_cam_space[0]:.2f}, {point_in_cam_space[1]:.2f}, {point_in_cam_space[2]:.2f})") + print(f" Bewegliches Objekt in Kamera-Koordinaten: ({point_in_cam_space[0]:.2f}, {point_in_cam_space[1]:.2f}, {point_in_cam_space[2]:.2f})") # Klärung der Ausgabe - # In 2D projizieren (NDC) + # Projiziere das *bewegliche* Objekt in 2D (NDC) projected_point_ndc = perspective_projection( point_in_cam_space, cam['fov_deg'], - effective_aspect_ratio, # Benutze das tatsächliche Bildseitenverhältnis + effective_aspect_ratio, near_plane, far_plane ) - # --- Bild erstellen und speichern --- + # Bild erstellen und speichern img = Image.new('RGB', (IMG_WIDTH, IMG_HEIGHT), color='black') draw = ImageDraw.Draw(img) if projected_point_ndc is not None: - print(f" Projizierte 2D-Koordinaten (NDC): ({projected_point_ndc[0]:.3f}, {projected_point_ndc[1]:.3f})") - # NDC in Pixelkoordinaten umwandeln + print(f" Projizierte 2D-Koordinaten des Objekts (NDC): ({projected_point_ndc[0]:.3f}, {projected_point_ndc[1]:.3f})") # Klärung der Ausgabe px, py = ndc_to_pixel(projected_point_ndc, IMG_WIDTH, IMG_HEIGHT) - print(f" Pixelkoordinaten (im Bild): ({px}, {py})") - # Einen weißen Punkt zeichnen (3x3 Quadrat für Sichtbarkeit) + print(f" Pixelkoordinaten des Objekts (im Bild): ({px}, {py})") draw.rectangle([(px-1, py-1), (px+1, py+1)], fill='white', outline='white') - else: - print(" Objekt befindet sich ausserhalb des Sichtbereichs (Clipping).") + print(" Bewegliches Objekt befindet sich ausserhalb des Sichtbereichs dieser Kamera.") # Klärung der Ausgabe - # Bild speichern timestamp_str = f"{current_time:.1f}".replace('.', '_') filename = f"{cam['name']}_t_{timestamp_str}.png" filepath = os.path.join(OUTPUT_DIR, filename) img.save(filepath) print(f" Bild gespeichert: {filepath}") - # Zum nächsten Zeitschritt gehen current_time += time_step - # Keine Pause, um die Simulation schneller laufen zu lassen - # if current_time <= simulation_duration: - # time.sleep(0.1) print("\nSimulation beendet.") \ No newline at end of file