1 juin 2025

[Dynamo += UltraLeap] Interaction avec Leap Motion

 




Interagir sur des Géométries Dynamo avec les mains grâce à un contrôleur UltraLeap

#DynamoBIM #Python #LeapMotion #UltraLeap #AutodeskExpertElite #AutodeskCommunity 

if this article is not in your language, use the Google Translate widget ⬈ (bottom of page for Mobile version )


Contexte

Possédant un contrôleur Leap Motion, que j'avais autrefois utilisé avec l’un des premiers casques de réalité virtuelle (Oculus DK2), j’ai toujours rêvé d’interagir avec DynamoBIM à l’aide de gestes.

J’avais déjà tenté l’expérience par le passé, mais sans succès – principalement par manque de connaissances techniques à l’époque. Récemment, j’ai décidé de réessayer… et cette fois, je suis allé beaucoup plus loin !


Prérequis 

Pour reproduire cette expérience et connecter le Leap Motion à DynamoBIM, voici ce dont vous aurez besoin :

  • DynamoBIM 
  • Python3.xx installé en local
    (même version que celle utilisée par le moteur Python de Dynamo)
Dans cet exemple, nous utiliserons le moteur PythonNet3 qui est actuellement sous Python3.11

👉 Téléchargement : https://www.python.org/downloads/windows/
  • Un IDE tel que PyCharm (ou autre) avec Git installé
  • Un contrôleur Leap Motion (ou UltraLeap)

🖐️ Mon setup personnel :
  •     Contrôleur Leap Motion
  •     Logiciel de tracking associé : Leap Gemini v5.20.0
    👉 Téléchargement : https://www.ultraleap.com/downloads/leap-controller/


Résultat

-- 

Procédure
   
     1. Installer le SDK UltraLeap 
    Une fois installé, lancer le programme
    "C:\Program Files\Ultraleap\ControlPanel\Ultraleap Tray.exe"
    pour vérifier le tracking



            2. Lancer PyCharm et cloner le repo Github suivant

      https://github.com/ultraleap/leapc-python-bindings



      -

      3. Définir la version de Python

      La version doit être identique au moteur Python que vous allez utiliser sous Dynamo


      -
              4. Construire la solution

      suivre les instructions du README pour construire un nouveau module (package whl)
       



              5. Copie des librairies

      Une fois la solution compilée, on copie les librairies dans le dossier Lib Python de Dynamo

      Exemple du chemin du dossier Lib

      "C:\Users\<Utilisateur>\AppData\Local\python-3.11.0-embed-amd64\Lib\"

      -




      Important : le fichier .whl est à dézipper dans le dossier

      "C:\Users\<Utilisateur>\AppData\Local\python-3.11.0-embed-amd64\Lib\"




              6. Présentation du script Dynamo

      Il se compose de 2 nœuds Python :
      • Un pour créer une instance de connexion
      • Un pour tracker les mains/doigts et les transformer en géométries Dynamo

      Note : Les coordonnées sont celles du LeapMotion, il faudra prochainement que je regarde pour corriger le système de coordonnées

      Fonctionnalités rajoutées :
      • Création de solide de type Cuboid par pincement entre les 2 mains

      • Gestion des données en temps réel
        • Pour que l'animation soit fluide sans saturer Dynamo, on utilise System.AppDomain.CurrentDomain.SetData. Cela permet de stocker et de récupérer des géométries en mémoire entre deux exécutions du script.

      •     Nous rédigerons le sys.stdout vers la variable OUT via un objet en mémoire.
        Enfin, nous rajoutons un nœud DateTime.Now pour pouvoir utiliser le mode périodique
        -


        -
        code 1
        .
        
        # Load the Python Standard and DesignScript Libraries
        import sys
        import clr
        import System
        clr.AddReference('ProtoGeometry')
        from Autodesk.DesignScript.Geometry import *
        import Autodesk.DesignScript.Geometry as DS
        
        import sysconfig
        # standard library path
        sys.path.append(sysconfig.get_path("platstdlib"))
        # site-package library path
        sys.path.append(sysconfig.get_path("platlib"))
        import leap
        import time
        
        connection = leap.Connection()
        
        OUT =  connection
        


        code 2
        .
        
        # Load the Python Standard and DesignScript Libraries
        import sys
        import clr
        import System
        clr.AddReference('ProtoGeometry')
        import Autodesk.DesignScript.Geometry as DS
        import sysconfig
        # standard library path
        sys.path.append(sysconfig.get_path("platstdlib"))
        # site-package library path
        sys.path.append(sysconfig.get_path("platlib"))
        import leap
        from leap import datatypes as ldt
        import time
        
        # print variables in memory
        from io import StringIO    
        sys.stdout = StringIO()
        
        class MyListener(leap.Listener):
            def __init__(self):
                self.hands_format = "Skeleton"
                self.lst_points = []
                self.radius = 2
                
            def on_connection_event(self, event):
                pass
                #print("Connected")
        
            def on_device_event(self, event):
                try:
                    with event.device.open():
                        info = event.device.get_info()
                except leap.LeapCannotOpenDeviceError:
                    info = event.device.get_info()
        
                #print(f"Found device {info.serial}")
                
            def location_end_of_finger(self, hand: ldt.Hand, digit_idx: int) -> ldt.Vector:
                digit = hand.digits[digit_idx]
                return digit.distal.next_joint
                
            def sub_vectors(self, v1: ldt.Vector, v2: ldt.Vector) -> list:
                return map(float.__sub__, v1, v2)
                
            def fingers_pinching(self, thumb: ldt.Vector, index: ldt.Vector):
                diff = list(map(abs, self.sub_vectors(thumb, index)))
            
                if diff[0] < 20 and diff[1] < 20 and diff[2] < 20:
                    return True, diff
                else:
                    return False, diff
                
            def get_joint_position(self, bone):
                if bone:
                    return bone.x , bone.y , bone.z 
                else:
                    return None
        
            def on_tracking_event(self, event):
                geometries = []
                pinch_pts = []
                pinch_hand_type = set()
                #print(f"Frame {event.tracking_frame_id} with {len(event.hands)} hands.")
                for i in range(0, len(event.hands)):
                    hand = event.hands[i]
                    hand_type = "left" if str(hand.type) == "HandType.Left" else "right"
                    for index_digit in range(0, 5):
                        digit = hand.digits[index_digit]
                        for index_bone in range(0, 4):
                            bone = digit.bones[index_bone]
                            if self.hands_format == "Skeleton":
                                #print("Skeleton")
                                wrist = self.get_joint_position(hand.arm.next_joint)
                                elbow = self.get_joint_position(hand.arm.prev_joint)
                                if wrist:
                                    #print("wrist")
                                    pta = DS.Point.ByCoordinates(*wrist)
                                    geometries.append(DS.Sphere.ByCenterPointRadius(pta, self.radius ))
        
                                if elbow:
                                    #print("elbow")
                                    ptb = DS.Point.ByCoordinates(*elbow)
                                    geometries.append(DS.Sphere.ByCenterPointRadius(ptb, self.radius ))
        
                                if wrist and elbow:
                                    #print("wrist and elbow")
                                    try:
                                        geometries.append(DS.Line.ByStartPointEndPoint(pta, ptb))
                                    except Exception as ex:
                                         pass
        
                                bone_start = self.get_joint_position(bone.prev_joint)
                                bone_end = self.get_joint_position(bone.next_joint)
                                #print(f"{bone_start=}")
                                #print(f"{bone_end=}")
                                ptc = DS.Point.ByCoordinates(*bone_start)
                                geometries.append(DS.Sphere.ByCenterPointRadius(ptc, self.radius ))
                                ptd = DS.Point.ByCoordinates(*bone_end)
                                geometries.append(DS.Sphere.ByCenterPointRadius(ptd, self.radius ))
                                try:
                                    geometries.append(DS.Line.ByStartPointEndPoint(ptc, ptd))
                                except Exception as ex:
                                     pass
                                #                
                                # test if pinching
                                thumb = self.location_end_of_finger(hand, 0)
                                index = self.location_end_of_finger(hand, 1)
                                pinching, array = self.fingers_pinching(thumb, index)
                                #print(f"{pinching=}", array)
                                if pinching:
                                    pinch_pts.append(DS.Point.ByCoordinates(*array))
                                    pinch_hand_type.add(hand_type)
                    #
                    if len(pinch_hand_type) == 2 and len(geometries) >= 141:
                        bbx = DS.BoundingBox.ByGeometry([geometries[21], geometries[141]])
                        bbx_solid = DS.BoundingBox.ToCuboid(bbx)
                        geometries.insert(0, bbx_solid)
                    #
                    if len(geometries) > 0:
                        self.lst_points.append(geometries)
                    
        
        def main():
            global connection
            my_listener = MyListener()
            #connection = leap.Connection()
            connection.add_listener(my_listener)
            with connection.open():
                connection.set_tracking_mode(leap.TrackingMode.Desktop)
                time.sleep(0.1)
            connection.remove_listener(my_listener)
            if len(my_listener.lst_points) > 0:
                return my_listener.lst_points[-1]
            else:
                return []
        
        connection = IN[0]
        
        geoms = main()
        sys.stdout.seek(0)
        printResult =  sys.stdout.read()
        OUT =  geoms, printResult
        # reset the sys.stdout
        sys.stdout = sys.__stdout__
        sys.stderr = sys.__stderr__
        
        
        Variante du code 2 avec la mémorisation des solides créés

         
        
        # Load the Python Standard and DesignScript Libraries
        import sys
        import clr
        import System
        clr.AddReference('ProtoGeometry')
        import Autodesk.DesignScript.Geometry as DS
        
        import sysconfig
        # standard library path
        sys.path.append(sysconfig.get_path("platstdlib"))
        # site-package library path
        sys.path.append(sysconfig.get_path("platlib"))
        import leap
        from leap import datatypes as ldt
        import time
        
        from io import StringIO    
        sys.stdout = StringIO()
        
        class MyListener(leap.Listener):
            def __init__(self):
                self.hands_format = "Skeleton"
                self.lst_points = []
                self.lst_cuboid = []
                self.radius = 2
                
            def on_connection_event(self, event):
                pass
                #print("Connected")
        
            def on_device_event(self, event):
                try:
                    with event.device.open():
                        info = event.device.get_info()
                except leap.LeapCannotOpenDeviceError:
                    info = event.device.get_info()
        
                #print(f"Found device {info.serial}")
                
            def location_end_of_finger(self, hand: ldt.Hand, digit_idx: int) -> ldt.Vector:
                digit = hand.digits[digit_idx]
                return digit.distal.next_joint
                
            def sub_vectors(self, v1: ldt.Vector, v2: ldt.Vector) -> list:
                return map(float.__sub__, v1, v2)
                
            def fingers_pinching(self, thumb: ldt.Vector, index: ldt.Vector):
                diff = list(map(abs, self.sub_vectors(thumb, index)))
            
                if diff[0] < 20 and diff[1] < 20 and diff[2] < 20:
                    return True, diff
                else:
                    return False, diff
                
            def get_joint_position(self, bone):
                if bone:
                    #return int(bone.x + (self.screen_size[1] / 2)), int(bone.z + (self.screen_size[0] / 2))
                    return bone.x , bone.y , bone.z 
                else:
                    return None
        
            def on_tracking_event(self, event):
                geometries = []
                pinch_pts = []
                pinch_hand_type = set()
                #print(f"Frame {event.tracking_frame_id} with {len(event.hands)} hands.")
                for i in range(0, len(event.hands)):
                    hand = event.hands[i]
                    hand_type = "left" if str(hand.type) == "HandType.Left" else "right"
                    for index_digit in range(0, 5):
                        digit = hand.digits[index_digit]
                        for index_bone in range(0, 4):
                            bone = digit.bones[index_bone]
                            if self.hands_format == "Skeleton":
                                #print("Skeleton")
                                wrist = self.get_joint_position(hand.arm.next_joint)
                                elbow = self.get_joint_position(hand.arm.prev_joint)
                                if wrist:
                                    #print("wrist")
                                    pta = DS.Point.ByCoordinates(*wrist)
                                    geometries.append(DS.Sphere.ByCenterPointRadius(pta, self.radius ))
        
                                if elbow:
                                    #print("elbow")
                                    ptb = DS.Point.ByCoordinates(*elbow)
                                    geometries.append(DS.Sphere.ByCenterPointRadius(ptb, self.radius ))
        
                                if wrist and elbow:
                                    #print("wrist and elbow")
                                    try:
                                        geometries.append(DS.Line.ByStartPointEndPoint(pta, ptb))
                                    except Exception as ex:
                                         pass
        
                                bone_start = self.get_joint_position(bone.prev_joint)
                                bone_end = self.get_joint_position(bone.next_joint)
                                #print(f"{bone_start=}")
                                #print(f"{bone_end=}")
                                ptc = DS.Point.ByCoordinates(*bone_start)
                                geometries.append(DS.Sphere.ByCenterPointRadius(ptc, self.radius ))
                                ptd = DS.Point.ByCoordinates(*bone_end)
                                geometries.append(DS.Sphere.ByCenterPointRadius(ptd, self.radius ))
                                try:
                                    geometries.append(DS.Line.ByStartPointEndPoint(ptc, ptd))
                                except Exception as ex:
                                     pass
                                #                
                                # test if pinching
                                thumb = self.location_end_of_finger(hand, 0)
                                index = self.location_end_of_finger(hand, 1)
                                pinching, array = self.fingers_pinching(thumb, index)
                                #print(f"{pinching=}", array)
                                if pinching:
                                    pinch_pts.append(DS.Point.ByCoordinates(*array))
                                    pinch_hand_type.add(hand_type)
                    #
                    if len(pinch_hand_type) == 2 and len(geometries) >= 141:
                        bbx = DS.BoundingBox.ByGeometry([geometries[21], geometries[141]])
                        bbx_solid = DS.BoundingBox.ToCuboid(bbx)
                        self.lst_cuboid.append(bbx_solid)
                    #
                    if len(geometries) > 0:
                        self.lst_points.append(geometries)
                    
        
        def main():
            global connection
            my_listener = MyListener()
            #connection = leap.Connection()
            connection.add_listener(my_listener)
            with connection.open():
                connection.set_tracking_mode(leap.TrackingMode.Desktop)
                time.sleep(0.1)
            connection.remove_listener(my_listener)
            if len(my_listener.lst_points) > 0 and len(my_listener.lst_cuboid) > 0:
                return my_listener.lst_points[-1], my_listener.lst_cuboid[-1]
            elif len(my_listener.lst_points) > 0 and len(my_listener.lst_cuboid) == 0:
                return my_listener.lst_points[-1], None
            else:
                return [], None
        
        connection = IN[0]
        
        geoms, new_cuboid = main()
        datam = []
        
        
        dataKey = "LstGeoms"
        if len(geoms) == 0:
            System.AppDomain.CurrentDomain.SetData("_Dyn_Wireless_{}".format(dataKey), [])
            
        if isinstance(new_cuboid, DS.Cuboid):
            # set Data to memory
            data = new_cuboid
            # get Data in memory
            datam = System.AppDomain.CurrentDomain.GetData("_Dyn_Wireless_{}".format(dataKey))
            if datam is None and isinstance(data, DS.Cuboid):
                System.AppDomain.CurrentDomain.SetData("_Dyn_Wireless_{}".format(dataKey), [data])
            elif isinstance(data, DS.Cuboid):
                try:
                    datam.append(data)
                    # clean solids
                    temp = []
                    for solid in datam[::-1]:
                        if all(not solid.DoesIntersect(s) for s in temp):
                            temp.append(solid)
                    
                    System.AppDomain.CurrentDomain.SetData("_Dyn_Wireless_{}".format(dataKey), temp)
                except Exception as ex:
                    print(ex)
            else: 
                pass
        
        #
        datam = System.AppDomain.CurrentDomain.GetData("_Dyn_Wireless_{}".format(dataKey))
        sys.stdout.seek(0)
        printResult =  sys.stdout.read()
        OUT =  geoms, printResult, datam
        # reset the sys.stdout
        sys.stdout = sys.__stdout__
        sys.stderr = sys.__stderr__
        
        
        ..
        « Qui ne peut attaquer le raisonnement, attaque le raisonneur. »
        Paul Valéry

        0 commentaires:

        Enregistrer un commentaire