Augmented Reality 3D object viewer in Python


Als je wel eens een 3D-design hebt gemaakt, herken je dit vast: je ontwerpt iets zorgvuldig, print het uit... en ontdekt dan dat het toch niet helemaal past of er niet uitziet zoals je had verwacht. Vaak kom je daar pas achter na de eerste of tweede print, wat natuurlijk tijd en materiaal kost. Ik liep tegen precies hetzelfde probleem aan. Daarom heb ik een simpel programma geschreven dat een live camerabeeld combineert met een .stl-bestand, zodat je met augmented reality direct kunt zien hoe je ontwerp in de echte wereld staat. Zo voorkom je onnodige prints en krijg je sneller een goed beeld van het eindresultaat.
Hoe werkt het?
Het idee is simpel: je streamt de camera van je telefoon naar je laptop, laat die live beelden uitlezen in een Python-programma, en laadt daaroverheen een 3D-render van je ontwerp. Vervolgens zorg je ervoor dat je met je muis de render kunt draaien, bewegen en in- en uitzoomen.
Om dit project te laten werken, moeten de volgende onderdelen goed samenwerken:
Je telefooncamera naar je laptop streamen
Dit zorgt ervoor dat de live beelden van je telefoon beschikbaar zijn op je laptop. Dit kan bijvoorbeeld via een app of netwerkverbinding die de camerafeed deelt.Je camerabeeld live laten uitlezen door Python
Het Python-programma ontvangt de videofeed en kan die in realtime verwerken. Hiervoor gebruiken we een library zoals OpenCV, die videobeelden kan binnenhalen en bewerken.Je .stl-bestand inlezen en tekenen
Het 3D-model van je ontwerp, opgeslagen als een .stl-bestand, wordt door het programma ingeladen en omgezet naar een 3D-render. Deze render tekenen we vervolgens over het live camerabeeld heen.Interactie met het 3D-model mogelijk maken
Via muisbewegingen kun je het model draaien, verplaatsen en in- en uitzoomen. Dit maakt het mogelijk om je ontwerp vanuit verschillende hoeken en afstanden te bekijken, alsof het echt in de ruimte staat.
Door deze onderdelen samen te brengen ontstaat een augmented reality ervaring waarmee je eenvoudig kunt zien hoe jouw 3D-ontwerp in de echte wereld past — zonder dat je eerst hoeft te printen!
Je telefooncamera naar je laptop streamen
Voor het streamen van je camera naar je laptop zijn er ontzettend veel apps en programma’s beschikbaar. Ik heb gekozen voor een simpel en gebruiksvriendelijk programma genaamd Iriun. Dit werkt heel makkelijk: je installeert de software op je Windows-laptop en de bijbehorende app op je telefoon. Vervolgens maak je via USB of via wifi (als je telefoon en laptop op hetzelfde netwerk zitten) automatisch verbinding. Je laptop herkent je telefooncamera dan als een gewone webcam.
Het mooiste is dat je verder niets hoeft in te stellen. Zolang je de app op je telefoon en het programma op je laptop open hebt, maken ze automatisch verbinding en begint de camerafeed direct te streamen. Super eenvoudig!
Je camerabeeld laten uitlezen door python
Een erg bekende Python-library die veel wordt gebruikt voor dit soort projecten is openCV. OpenCV is een open-source computer vision library die je kunt gebruiken om eenvoudig en snel dingen te doen zoals beeldherkenning, het bewerken van beelden of bijvoorbeeld het herkennen van ArUco-markers. Er zijn online talloze tutorials te vinden over hoe je je camerabeeld kunt uitlezen met OpenCV, maar gelukkig is het helemaal niet zo ingewikkeld.
cap = cv2.VideoCapture(1)
fx, fy = 1280, 720
cx, cy = fx/2, fy/2
camera_matrix = np.array([[fx,0,cx],
[0,fy,cy],
[0,0,1]], dtype=np.float32)
dist_coeffs = np.zeros((5,1))
Zoals hierboven te zien is, is het erg makkelijk om je camerabeeld in Python te gebruiken. Je geeft alleen aan welke camera-input je wilt gebruiken (meestal is 0
je standaard webcam, 1
is een externe camera) en welke resolutie je instelt.
Daarnaast kun je, als je meer precisie nodig hebt, een camera matrix toevoegen. Deze matrix beschrijft hoe je camera de echte wereld vertaalt naar pixels. Daarmee kun je vervormingen corrigeren en zorgen dat 3D-objecten realistischer en nauwkeuriger over je camerabeeld worden gelegd. Vooral in augmented reality-projecten, zoals dit, is dat handig om je 3D-modellen netjes te laten aansluiten op de echte wereld.
Je .stl-bestand inlezen en tekenen
Het lastigste deel van dit project was het netjes intekenen van de STL-bestanden in het live camerabeeld. Er bestaan namelijk Python-library’s die STL-bestanden als 3D-model kunnen weergeven, maar die openen zich altijd in een eigen 3D-viewer. Dat werkt prima om een ontwerp te bekijken, maar niet als je het model rechtstreeks over een videostream wilt projecteren.
Om het model in het camerabeeld te krijgen, moet je de 3D-coördinaten van het STL-bestand eerst omrekenen naar 2D-pixelposities. Dat gebeurt met cv2.projectPoints()
. Hiermee worden de punten van het 3D-model geprojecteerd op het camerabeeld, zodat je het ontwerp écht als een overlay ziet.
def draw_filled_mesh(frame, mesh, rvec, tvec, camera_matrix, dist_coeffs, color=(150, 150, 200), alpha=0.8):
vertices = mesh.vertices
faces = mesh.faces
imgpts, _ = cv2.projectPoints(vertices, rvec, tvec, camera_matrix, dist_coeffs)
imgpts = imgpts.reshape(-1, 2).astype(np.int32)
overlay = frame.copy()
for face in faces:
pts = imgpts[face]
cv2.fillPoly(overlay, [pts], color)
cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0, frame)
return frame
def draw_wireframe(frame, mesh, rvec, tvec, camera_matrix, dist_coeffs, color=(0, 255, 0), thickness=1):
vertices = mesh.vertices
edges = mesh.edges_unique
imgpts, _ = cv2.projectPoints(vertices, rvec, tvec, camera_matrix, dist_coeffs)
imgpts = imgpts.reshape(-1, 2).astype(int)
for edge in edges:
pt1 = tuple(imgpts[edge[0]])
pt2 = tuple(imgpts[edge[1]])
cv2.line(frame, pt1, pt2, color, thickness)
return frame
Om het STL-model goed zichtbaar te maken in het camerabeeld gebruik ik twee manieren van tekenen: gevuld en als draadmodel.
draw_filled_mesh
Deze functie tekent de vlakken van het 3D-model in. Eerst worden alle 3D-vertices van het model geprojecteerd naar 2D metcv2.projectPoints()
. Daarna pakt de code de faces (de driehoekjes waar het model uit is opgebouwd) en vult die één voor één in met een kleur. Door dit over een kopie van het camerabeeld te tekenen en vervolgens half-transparant samen te voegen metcv2.addWeighted()
, krijg je een mooi “glanzend” effect waarbij het model zichtbaar is maar de echte wereld er nog doorheen schijnt.draw_wireframe
Deze functie tekent juist de randen van het 3D-model. Ook hier worden de 3D-coördinaten omgezet naar 2D-pixels. Daarna loopt de code alle unieke randen van het model af en tekent metcv2.line()
groene lijntjes in het camerabeeld. Dit draadmodel geeft extra detail en maakt de vormen van het model beter herkenbaar.
Door beide functies samen te gebruiken ontstaat een duidelijk en realistisch beeld: je ziet het model als een transparant object met zowel vlakken als lijnen, waardoor het goed opgaat in de echte omgeving.
Interactie met het 3D-model mogelijk maken
Om ervoor te zorgen dat je het 3D-model goed kan oriënteren voor waar je het wil plaatsen moet je het kunnen verplaatsen over het camerabeeld, draaien en in en uitzoomen. Om het zo gebruiksvriendelijk te maken is er gekozen om dit allemaal met de muis te doen net zoals je dat zou doen bij een 3d-design programma zoals Fusion of Solidworks.
# Globale variabelen voor muis-rotatie
yaw, pitch = 0.0, 0.0
last_x, last_y = None, None
dragging_left = False
dragging_right = False
zoom = 1.0
tx, ty, tz = 0.0, 0.0, 0.0
def mouse_callback(event, x, y, flags, param):
global yaw, pitch, last_x, last_y
global dragging_left, dragging_right
global tx, ty, tz, zoom
if event == cv2.EVENT_RBUTTONDOWN:
dragging_left = True
last_x, last_y = x, y
elif event == cv2.EVENT_LBUTTONDOWN:
dragging_right = True
last_x, last_y = x, y
elif event == cv2.EVENT_MOUSEMOVE:
if dragging_left:
dx = x - last_x
dy = y - last_y
last_x, last_y = x, y
yaw += dx * 0.01
pitch += dy * 0.01
# pitch = max(min(pitch, np.pi/2), -np.pi/2)
elif dragging_right:
dx = x - last_x
dy = y - last_y
last_x, last_y = x, y
tx += dx * 0.001
ty -= dy * 0.001
elif event == cv2.EVENT_RBUTTONUP:
dragging_left = False
elif event == cv2.EVENT_LBUTTONUP:
dragging_right = False
elif event == cv2.EVENT_MOUSEWHEEL:
if flags > 0:
zoom += 0.05
else:
zoom -= 0.05
zoom = max(0.1, min(zoom, 3.0))
def get_rvec_from_yaw_pitch(yaw, pitch):
Ry = np.array([[np.cos(yaw), 0, np.sin(yaw)],
[0, 1, 0],
[-np.sin(yaw), 0, np.cos(yaw)]])
Rx = np.array([[1, 0, 0],
[0, np.cos(pitch), -np.sin(pitch)],
[0, np.sin(pitch), np.cos(pitch)]])
R = Ry @ Rx
rvec, _ = cv2.Rodrigues(R)
return rvec
In de code gebeurt dat via de functie mouse_callback
. Deze luistert naar muisacties en past de positie van het model aan: met de rechtermuisknop draaien, met de linkermuisknop verschuiven en met het scrollwiel in- of uitzoomen. De waarden voor rotatie, positie en zoom worden telkens opgeslagen in variabelen.
De functie get_rvec_from_yaw_pitch
zet die rotatievariabelen (yaw
en pitch
) vervolgens om naar een rotatiematrix die OpenCV kan gebruiken om het model correct over het camerabeeld te tekenen.
Eindresultaat
Als je dit en nog meer code allemaal samen voegt dan komt er een simpel programma uit dat ervoor zorgt dat je stl bestanden in het echt kan bekijken. Zoals je hieronder kan zien kan dit gebruikt worden om een beeld te krijgen van hoe je ontwerpen in de echte wereld eruit gaan zien.
Hierboven heb ik bijvoorbeeld een bakje ontworpen die ik naast het whiteboard wil hangen door de AR viewer weet ik nu ongeveer hoe die eruit gaat zien als ik hem uitprint.
Verbeterpunten
Als ik ooit nog aan deze code zou sleutelen zou ik een paar dingen toevoegen.
Betere manier van draaien. Het is op dit moment nog erg lastig om te draaien met alleen de muis.
Andere kleuren toevoegen. Zorgen voor meer kleur zodat je ook meteen dat kan testen wat er mooier uitziet.
Schaal toevoegen. Doormiddel van aruco markers of iets anders een schaal toevoegen zodat je weet hoe groot je design is op de camera.
Betere cameramatrix. Op dit moment is de cameramatrix niet gecalibreerd op mijn eigen camera. Als dit wel was gedaan was de camera nog preciezer geweest.
Subscribe to my newsletter
Read articles from Daniël R directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
