Image generation
This commit is contained in:
179
np.py
179
np.py
@@ -1,6 +1,8 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import time
|
import time
|
||||||
import math
|
import math
|
||||||
|
import os # Importiert für die Verzeichniserstellung
|
||||||
|
from PIL import Image, ImageDraw # Importiert für die Bildverarbeitung
|
||||||
|
|
||||||
# --- Hilfsfunktionen ---
|
# --- Hilfsfunktionen ---
|
||||||
|
|
||||||
@@ -20,9 +22,14 @@ def look_at_matrix(eye, target, up):
|
|||||||
:return: 4x4 View-Matrix (NumPy-Array).
|
:return: 4x4 View-Matrix (NumPy-Array).
|
||||||
"""
|
"""
|
||||||
# Kamerakoordinatensystem berechnen
|
# Kamerakoordinatensystem berechnen
|
||||||
# z-Achse der Kamera (schaut oft *entgegen* der Blickrichtung)
|
|
||||||
forward = normalize(eye - target) # Konvention: Kamera schaut entlang -Z
|
forward = normalize(eye - target) # Konvention: Kamera schaut entlang -Z
|
||||||
if np.linalg.norm(np.cross(up, forward)) < 1e-6:
|
if np.linalg.norm(forward) < 1e-9: # Verhindert Division durch Null oder NaN, wenn eye==target
|
||||||
|
# Wenn Kamera und Ziel am selben Ort sind, ist die Richtung undefiniert.
|
||||||
|
# Wähle eine Standardrichtung, z.B. entlang -Z der Welt.
|
||||||
|
forward = np.array([0.0, 0.0, 1.0])
|
||||||
|
|
||||||
|
# Prüfe, ob 'up' und 'forward' (fast) parallel sind
|
||||||
|
if abs(np.dot(normalize(up), forward)) > 0.999:
|
||||||
# Fallback, wenn 'up' und 'forward' (fast) parallel sind
|
# Fallback, wenn 'up' und 'forward' (fast) parallel sind
|
||||||
# Wähle einen anderen temporären Up-Vektor
|
# Wähle einen anderen temporären Up-Vektor
|
||||||
if abs(forward[1]) < 0.99: # Wenn forward nicht fast vertikal ist
|
if abs(forward[1]) < 0.99: # Wenn forward nicht fast vertikal ist
|
||||||
@@ -36,15 +43,13 @@ def look_at_matrix(eye, target, up):
|
|||||||
# Den tatsächlichen Up-Vektor der Kamera neu berechnen, damit er orthogonal ist
|
# Den tatsächlichen Up-Vektor der Kamera neu berechnen, damit er orthogonal ist
|
||||||
camera_up = normalize(np.cross(forward, right))
|
camera_up = normalize(np.cross(forward, right))
|
||||||
|
|
||||||
# Rotationsmatrix erstellen (transformiert von Welt zu Kamera-Ausrichtung)
|
# Rotationsmatrix erstellen
|
||||||
# Dies ist die Inverse (oder Transponierte, da Rotationsmatrix) der Matrix,
|
|
||||||
# die die Kamera-Basisvektoren in Weltkoordinaten ausdrückt.
|
|
||||||
rotation = np.identity(4)
|
rotation = np.identity(4)
|
||||||
rotation[0, 0:3] = right
|
rotation[0, 0:3] = right
|
||||||
rotation[1, 0:3] = camera_up
|
rotation[1, 0:3] = camera_up
|
||||||
rotation[2, 0:3] = forward # Beachte: forward zeigt von target zu eye
|
rotation[2, 0:3] = forward
|
||||||
|
|
||||||
# Translationsmatrix erstellen (verschiebt die Welt, sodass die Kamera am Ursprung ist)
|
# Translationsmatrix erstellen
|
||||||
translation = np.identity(4)
|
translation = np.identity(4)
|
||||||
translation[0:3, 3] = -eye
|
translation[0:3, 3] = -eye
|
||||||
|
|
||||||
@@ -63,73 +68,100 @@ def perspective_projection(point_cam_space, fov_deg, aspect_ratio, near, far):
|
|||||||
:param far: Ferne Clipping-Ebene.
|
:param far: Ferne Clipping-Ebene.
|
||||||
:return: Projizierter 2D-Punkt in NDC (NumPy-Array) oder None, wenn außerhalb.
|
:return: Projizierter 2D-Punkt in NDC (NumPy-Array) oder None, wenn außerhalb.
|
||||||
"""
|
"""
|
||||||
# Überprüfen, ob der Punkt vor der Kamera und innerhalb der Clipping-Ebenen liegt
|
# Kamera schaut entlang -Z, Z muss negativ sein und innerhalb der Clipping-Grenzen liegen
|
||||||
# Kamera schaut entlang -Z, also muss Z negativ sein.
|
|
||||||
if point_cam_space[2] > -near or point_cam_space[2] < -far:
|
if point_cam_space[2] > -near or point_cam_space[2] < -far:
|
||||||
return None # Außerhalb des Sichtfelds (vor near oder hinter far)
|
return None # Außerhalb des Sichtfelds (vor near oder hinter far)
|
||||||
|
|
||||||
# Einfache Perspektivprojektion (vereinfachte Formel)
|
# Verhindert Division durch Null, falls Punkt genau auf der Kameraebene liegt (z=0)
|
||||||
|
# Obwohl das Clipping oben dies meist abfängt, ist es eine zusätzliche Sicherheit.
|
||||||
|
if abs(point_cam_space[2]) < 1e-9:
|
||||||
|
return None
|
||||||
|
|
||||||
# Skalierungsfaktor basierend auf FOV
|
# Skalierungsfaktor basierend auf FOV
|
||||||
f = 1.0 / math.tan(math.radians(fov_deg) / 2.0)
|
f = 1.0 / math.tan(math.radians(fov_deg) / 2.0)
|
||||||
|
|
||||||
# Projizierte Koordinaten auf der Bildebene (vor Normalisierung)
|
# Projizierte Koordinaten auf der Bildebene (NDC)
|
||||||
# Annahme: Bildebene bei z = -near (oder -1, je nach Konvention)
|
x_ndc = (f / aspect_ratio) * point_cam_space[0] / -point_cam_space[2]
|
||||||
# Hier verwenden wir die gebräuchlichere Projektionsmatrix-Formel-Basis:
|
y_ndc = f * point_cam_space[1] / -point_cam_space[2]
|
||||||
x_proj = (f / aspect_ratio) * point_cam_space[0] / -point_cam_space[2]
|
|
||||||
y_proj = f * point_cam_space[1] / -point_cam_space[2]
|
|
||||||
|
|
||||||
# Normalisierte Gerätekoordinaten (NDC) sind typischerweise im Bereich [-1, 1]
|
# Überprüfen, ob die projizierten Koordinaten innerhalb des NDC-Bereichs [-1, 1] liegen
|
||||||
# Hier geben wir die projizierten Koordinaten zurück, wie sie auf einer Bildebene
|
# Dies ist ein zusätzlicher Clipping-Schritt für die XY-Ebene
|
||||||
# bei z=-1 erscheinen würden, skaliert durch FOV und Aspekt.
|
if not (-1 <= x_ndc <= 1 and -1 <= y_ndc <= 1):
|
||||||
# Eine vollständige Projektionsmatrix würde auch Z für Tiefentests umwandeln.
|
return None # Außerhalb des seitlichen Sichtfelds
|
||||||
return np.array([x_proj, y_proj])
|
|
||||||
|
|
||||||
|
return np.array([x_ndc, y_ndc])
|
||||||
|
|
||||||
|
# --- NEU: Funktion zur Umwandlung von NDC in Pixelkoordinaten ---
|
||||||
|
def ndc_to_pixel(ndc_coords, width, height):
|
||||||
|
""" Wandelt NDC-Koordinaten (-1 bis 1) in Pixelkoordinaten (0 bis width/height) um. """
|
||||||
|
# x_ndc = -1 -> pixel_x = 0
|
||||||
|
# x_ndc = +1 -> pixel_x = width
|
||||||
|
pixel_x = (ndc_coords[0] + 1.0) / 2.0 * width
|
||||||
|
# y_ndc = -1 -> pixel_y = height (unten im Bild)
|
||||||
|
# y_ndc = +1 -> pixel_y = 0 (oben im Bild)
|
||||||
|
# Wir müssen die Y-Koordinate invertieren, da NDC +Y oben ist, Pixel +Y aber unten.
|
||||||
|
pixel_y = (1.0 - ndc_coords[1]) / 2.0 * height
|
||||||
|
|
||||||
|
# In Integer umwandeln und sicherstellen, dass sie innerhalb der Grenzen liegen
|
||||||
|
px = int(round(pixel_x))
|
||||||
|
py = int(round(pixel_y))
|
||||||
|
|
||||||
|
# Clipping auf Bildgrenzen (obwohl NDC-Clipping dies meist unnötig macht)
|
||||||
|
px = max(0, min(width - 1, px))
|
||||||
|
py = max(0, min(height - 1, py))
|
||||||
|
|
||||||
|
return px, py
|
||||||
|
|
||||||
# --- Simulationsparameter ---
|
# --- 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]) # Startposition des Objekts (x, y, z)
|
||||||
# Einfache lineare Bewegung
|
|
||||||
# object_velocity = np.array([1.0, 0.5, -0.8]) # Bewegung pro Sekunde
|
|
||||||
# Alternative: Kreisbewegung in der XZ-Ebene um (0, 0, 10) mit Radius 5
|
|
||||||
radius = 5.0
|
radius = 5.0
|
||||||
angular_speed = math.radians(45) # 45 Grad pro Sekunde
|
angular_speed = math.radians(45) # 45 Grad pro Sekunde
|
||||||
|
|
||||||
# Kameraeinstellungen
|
# Kameraeinstellungen
|
||||||
# Jede Kamera hat eine Position ('pos'), einen Punkt, auf den sie schaut ('target'),
|
|
||||||
# und einen 'up'-Vektor, der die Orientierung definiert.
|
|
||||||
cameras = [
|
cameras = [
|
||||||
{
|
{
|
||||||
"name": "Kamera 1 (Frontal)",
|
"name": "Kamera_1_Frontal", # Namen ohne Leerzeichen für Dateinamen
|
||||||
"pos": np.array([0.0, 0.0, 0.0]),
|
"pos": np.array([0.0, 0.0, 0.0]),
|
||||||
"target": object_start_pos, # Schaut initial auf den Startpunkt
|
"target": object_start_pos,
|
||||||
"up": np.array([0.0, 1.0, 0.0]), # Standard-Ausrichtung (Y ist oben)
|
"up": np.array([0.0, 1.0, 0.0]),
|
||||||
"fov_deg": 60, # Field of View in Grad
|
"fov_deg": 60,
|
||||||
"aspect_ratio": 16.0 / 9.0 # Typisches Breitbild
|
"aspect_ratio": 16.0 / 9.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Kamera 2 (Seitlich Links)",
|
"name": "Kamera_2_Seitlich_Links",
|
||||||
"pos": np.array([-15.0, 0.0, 5.0]),
|
"pos": np.array([-15.0, 0.0, 5.0]),
|
||||||
"target": object_start_pos,
|
"target": object_start_pos,
|
||||||
"up": np.array([0.0, 1.0, 0.0]),
|
"up": np.array([0.0, 1.0, 0.0]),
|
||||||
"fov_deg": 45,
|
"fov_deg": 45,
|
||||||
"aspect_ratio": 1.0 # Quadratisch
|
"aspect_ratio": 1.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Kamera 3 (Von Oben)",
|
"name": "Kamera_3_Von_Oben",
|
||||||
"pos": np.array([0.0, 15.0, 10.0]),
|
"pos": np.array([0.0, 15.0, 10.0]),
|
||||||
"target": object_start_pos,
|
"target": object_start_pos,
|
||||||
"up": np.array([0.0, 0.0, -1.0]), # Z zeigt nach unten, da Y oben war
|
"up": np.array([0.0, 0.0, -1.0]), # Blickrichtung -Y, Z nach unten ist "up"
|
||||||
"fov_deg": 70,
|
"fov_deg": 70,
|
||||||
"aspect_ratio": 4.0 / 3.0
|
"aspect_ratio": 4.0 / 3.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
# Clipping-Ebenen (gemeinsam für alle Kameras hier, könnte pro Kamera sein)
|
# Clipping-Ebenen
|
||||||
near_plane = 0.1
|
near_plane = 0.1
|
||||||
far_plane = 100.0
|
far_plane = 100.0
|
||||||
|
|
||||||
simulation_duration = 10 # Sekunden
|
simulation_duration = 10 # Sekunden
|
||||||
time_step = 1 # Zeitintervall für "Aufnahmen" in Sekunden
|
time_step = 1 # Zeitintervall für "Aufnahmen" in Sekunden
|
||||||
|
|
||||||
|
# --- NEU: Bildparameter ---
|
||||||
|
IMG_WIDTH = 320 # Bildbreite in Pixel
|
||||||
|
IMG_HEIGHT = 180 # Bildhöhe in Pixel (Beispiel für 16:9)
|
||||||
|
OUTPUT_DIR = "./bilder" # Verzeichnis für die Bilder
|
||||||
|
|
||||||
|
# --- NEU: Ausgabeverzeichnis erstellen ---
|
||||||
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||||
|
print(f"Bilder werden in '{OUTPUT_DIR}' gespeichert.")
|
||||||
|
|
||||||
|
|
||||||
# --- Simulationsschleife ---
|
# --- Simulationsschleife ---
|
||||||
current_object_pos = object_start_pos.copy()
|
current_object_pos = object_start_pos.copy()
|
||||||
current_time = 0.0
|
current_time = 0.0
|
||||||
@@ -140,60 +172,89 @@ print("Starte Simulation...")
|
|||||||
while current_time <= simulation_duration:
|
while current_time <= simulation_duration:
|
||||||
print(f"\n--- Zeitpunkt: {current_time:.1f}s ---")
|
print(f"\n--- Zeitpunkt: {current_time:.1f}s ---")
|
||||||
|
|
||||||
# Objektposition aktualisieren (Beispiel: Kreisbewegung)
|
# Objektposition aktualisieren (Kreisbewegung)
|
||||||
angle = angular_speed * current_time
|
angle = angular_speed * current_time
|
||||||
current_object_pos[0] = radius * math.cos(angle) + 0 # Kreiszentrum X=0
|
current_object_pos[0] = radius * math.cos(angle) + 0
|
||||||
current_object_pos[2] = radius * math.sin(angle) + 10 # Kreiszentrum Z=10
|
current_object_pos[2] = radius * math.sin(angle) + 10
|
||||||
# Alternative: Lineare Bewegung
|
|
||||||
# current_object_pos = object_start_pos + object_velocity * current_time
|
|
||||||
|
|
||||||
print(f"Objekt Welt-Position: ({current_object_pos[0]:.2f}, {current_object_pos[1]:.2f}, {current_object_pos[2]:.2f})")
|
print(f"Objekt Welt-Position: ({current_object_pos[0]:.2f}, {current_object_pos[1]:.2f}, {current_object_pos[2]:.2f})")
|
||||||
|
|
||||||
# Objektposition in homogene Koordinaten umwandeln (für Matrixmultiplikation)
|
# Objektposition in homogene Koordinaten umwandeln
|
||||||
object_pos_h = np.append(current_object_pos, 1.0)
|
object_pos_h = np.append(current_object_pos, 1.0)
|
||||||
|
|
||||||
# Für jede Kamera die Ansicht berechnen
|
# Für jede Kamera die Ansicht berechnen und Bild erzeugen
|
||||||
for i, cam in enumerate(cameras):
|
for i, cam in enumerate(cameras):
|
||||||
print(f"\n Kamera {i+1}: {cam['name']}")
|
print(f"\n Kamera {i+1}: {cam['name']}")
|
||||||
print(f" Position: ({cam['pos'][0]:.2f}, {cam['pos'][1]:.2f}, {cam['pos'][2]:.2f})")
|
print(f" Position: ({cam['pos'][0]:.2f}, {cam['pos'][1]:.2f}, {cam['pos'][2]:.2f})")
|
||||||
|
|
||||||
# Kamera schaut immer auf die aktuelle Objektposition
|
# Kamera schaut immer auf die aktuelle Objektposition
|
||||||
current_target = current_object_pos
|
current_target = current_object_pos
|
||||||
|
# Korrigiere das Seitenverhältnis basierend auf den Bilddimensionen
|
||||||
|
# Dies überschreibt das manuell gesetzte Seitenverhältnis
|
||||||
|
effective_aspect_ratio = IMG_WIDTH / IMG_HEIGHT
|
||||||
print(f" Schaut auf (Target): ({current_target[0]:.2f}, {current_target[1]:.2f}, {current_target[2]:.2f})")
|
print(f" Schaut auf (Target): ({current_target[0]:.2f}, {current_target[1]:.2f}, {current_target[2]:.2f})")
|
||||||
print(f" Up-Vektor: ({cam['up'][0]:.2f}, {cam['up'][1]:.2f}, {cam['up'][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: {cam['aspect_ratio']:.2f}")
|
print(f" FOV: {cam['fov_deg']} Grad, Aspekt (für Projektion): {effective_aspect_ratio:.2f}")
|
||||||
|
|
||||||
|
|
||||||
# View-Matrix berechnen
|
# View-Matrix berechnen
|
||||||
view_mat = look_at_matrix(cam['pos'], current_target, cam['up'])
|
view_mat = look_at_matrix(cam['pos'], current_target, cam['up'])
|
||||||
|
|
||||||
# Objektposition in Kamera-Koordinaten transformieren
|
# Objektposition in Kamera-Koordinaten transformieren
|
||||||
# point_in_cam_space_h = view_mat @ object_pos_h # Python 3.5+ für @ Operator
|
|
||||||
point_in_cam_space_h = np.dot(view_mat, object_pos_h)
|
point_in_cam_space_h = np.dot(view_mat, object_pos_h)
|
||||||
|
# Sicherstellen, dass w positiv ist (sollte nach View-Transform 1 sein)
|
||||||
|
if point_in_cam_space_h[3] <= 0:
|
||||||
|
print(" Objekt hinter der Kamera nach View-Transformation (w <= 0).")
|
||||||
|
projected_point_ndc = None
|
||||||
|
point_in_cam_space = np.array([np.nan, np.nan, np.nan]) # Ungültig
|
||||||
|
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})")
|
||||||
|
|
||||||
# De-homogenisieren (falls w nicht 1 ist, hier sollte es 1 sein nach View-Transform)
|
# In 2D projizieren (NDC)
|
||||||
point_in_cam_space = point_in_cam_space_h[:3] / point_in_cam_space_h[3]
|
projected_point_ndc = perspective_projection(
|
||||||
|
point_in_cam_space,
|
||||||
|
cam['fov_deg'],
|
||||||
|
effective_aspect_ratio, # Benutze das tatsächliche Bildseitenverhältnis
|
||||||
|
near_plane,
|
||||||
|
far_plane
|
||||||
|
)
|
||||||
|
|
||||||
print(f" Objekt in Kamera-Koordinaten: ({point_in_cam_space[0]:.2f}, {point_in_cam_space[1]:.2f}, {point_in_cam_space[2]:.2f})")
|
# --- NEU: Bild erstellen und speichern ---
|
||||||
|
# Erstelle ein neues schwarzes Bild
|
||||||
# In 2D projizieren
|
# Passe Höhe ggf. an, wenn das Seitenverhältnis der Kamera nicht zum globalen passt
|
||||||
projected_point_ndc = perspective_projection(
|
# Hier verwenden wir globale Höhe, aber berechnen den Aspekt neu.
|
||||||
point_in_cam_space,
|
# Alternativ könnte man Höhe/Breite pro Kamera anpassen.
|
||||||
cam['fov_deg'],
|
img = Image.new('RGB', (IMG_WIDTH, IMG_HEIGHT), color='black')
|
||||||
cam['aspect_ratio'],
|
draw = ImageDraw.Draw(img)
|
||||||
near_plane,
|
|
||||||
far_plane
|
|
||||||
)
|
|
||||||
|
|
||||||
if projected_point_ndc is not None:
|
if projected_point_ndc is not None:
|
||||||
print(f" Projizierte 2D-Koordinaten (NDC): ({projected_point_ndc[0]:.3f}, {projected_point_ndc[1]:.3f})")
|
print(f" Projizierte 2D-Koordinaten (NDC): ({projected_point_ndc[0]:.3f}, {projected_point_ndc[1]:.3f})")
|
||||||
# NDC-Koordinaten (-1 bis 1) müssten noch auf Pixelkoordinaten umgerechnet werden,
|
|
||||||
# wenn man ein echtes Bild rendern wollte (z.B. X_pixel = (X_ndc + 1) * width / 2).
|
# NDC in Pixelkoordinaten umwandeln
|
||||||
|
px, py = ndc_to_pixel(projected_point_ndc, IMG_WIDTH, IMG_HEIGHT)
|
||||||
|
print(f" Pixelkoordinaten (im Bild): ({px}, {py})")
|
||||||
|
|
||||||
|
# Einen weißen Punkt (oder kleines Quadrat für bessere Sichtbarkeit) zeichnen
|
||||||
|
# draw.point((px, py), fill='white') # Einzelner Pixel
|
||||||
|
draw.rectangle([(px-1, py-1), (px+1, py+1)], fill='white', outline='white') # 3x3 Quadrat
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(" Objekt befindet sich ausserhalb des Sichtbereichs (Clipping).")
|
print(" Objekt befindet sich ausserhalb des Sichtbereichs (Clipping).")
|
||||||
|
# Das Bild bleibt schwarz, da nichts gezeichnet wird.
|
||||||
|
|
||||||
|
# Bild speichern
|
||||||
|
timestamp_str = f"{current_time:.1f}".replace('.', '_') # Punkt im Dateinamen vermeiden
|
||||||
|
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
|
# Zum nächsten Zeitschritt gehen
|
||||||
current_time += time_step
|
current_time += time_step
|
||||||
if current_time <= simulation_duration:
|
if current_time <= simulation_duration:
|
||||||
time.sleep(1) # Kurze Pause zur besseren Lesbarkeit der Ausgabe
|
# Kurze Pause zur besseren Lesbarkeit der Ausgabe (optional)
|
||||||
|
# time.sleep(0.1) # Reduziert, um die Simulation zu beschleunigen
|
||||||
|
pass # Keine Pause für schnellere Ausführung
|
||||||
|
|
||||||
print("\nSimulation beendet.")
|
print("\nSimulation beendet.")
|
||||||
Reference in New Issue
Block a user