[natnet] display object in GCS based on optitrack markerset (#2958)

* [natnet] display object in GCS based on optitrack markerset

update natnet client to get markerset

* [natnet] Add a minimum refresh period for objects display.

Co-authored-by: Fabien-B <fabien.bonneval@gmail.com>
This commit is contained in:
Gautier Hattenberger
2022-12-16 16:00:36 +01:00
committed by GitHub
parent ce19885a1e
commit 3502cab856
3 changed files with 240 additions and 2 deletions
+2
View File
@@ -0,0 +1,2 @@
<program command="sw/ground_segment/python/natnet3.x/display_objects.py" name="Display Objects from Natnet" icon="natnet3.svg" />
@@ -13,7 +13,7 @@ FloatValue = struct.Struct( '<f' )
DoubleValue = struct.Struct( '<d' )
class NatNetClient:
def __init__( self, server="127.0.0.1", multicast="239.255.42.99", commandPort=1510, dataPort=1511, rigidBodyListener=None, newFrameListener=None, rigidBodyListListener=None, verbose=False, version=(3,0,0,0) ):
def __init__( self, server="127.0.0.1", multicast="239.255.42.99", commandPort=1510, dataPort=1511, rigidBodyListener=None, newFrameListener=None, rigidBodyListListener=None, markerSetListener=None, verbose=False, version=(3,0,0,0) ):
# IP address of the NatNet server.
self.serverIPAddress = server
@@ -36,6 +36,9 @@ class NatNetClient:
self.rigidBodyListListener = rigidBodyListListener
self.rigidBodyList = []
# Set this to a callback method of your choice to receive markerset data at each frame
self.markerSetListener = markerSetListener
# NatNet stream version. This will be updated to the actual version the server is using during initialization.
self.__natNetStreamVersion = version
@@ -194,10 +197,15 @@ class NatNetClient:
offset += 4
self.__trace( "Marker Count:", markerCount )
posList = []
for j in range( 0, markerCount ):
pos = Vector3.unpack( data[offset:offset+12] )
posList.append(pos)
offset += 12
#self.__trace( "\tMarker", j, ":", pos[0],",", pos[1],",", pos[2] )
self.__trace( "\tMarker", j, ":", pos[0],",", pos[1],",", pos[2] )
if self.markerSetListener is not None:
self.markerSetListener(modelName, posList)
# Unlabeled markers count (4 bytes)
unlabeledMarkersCount = int.from_bytes( data[offset:offset+4], byteorder='little' )
+228
View File
@@ -0,0 +1,228 @@
#!/usr/bin/env python3
#
# Copyright (C) 2022 Gautier Hattenberger <gautier.hattenberger@enac.fr>
#
# This file is part of paparazzi.
#
# paparazzi is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# paparazzi is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with paparazzi; see the file COPYING. If not, see
# <http://www.gnu.org/licenses/>.
#
'''
Receive marker set from optitrack system and display a convex hull for
each selected objects in the GCS with the SHAPE message
Exemple to display "Building_XYZ" object at Enac:
./display_objects.py -r 43.5640917,1.4829201 -up z_up -n 'Building_.*'
'''
import sys
from os import path, getenv
from time import time, sleep
import numpy as np
import re
from scipy.spatial import ConvexHull
import argparse
# import NatNet client
from NatNetClient import NatNetClient
# if PAPARAZZI_HOME not set, then assume the tree containing this
# file is a reasonable substitute
PPRZ_HOME = getenv("PAPARAZZI_HOME", path.normpath(path.join(path.dirname(path.abspath(__file__)), '../../../../')))
sys.path.append(PPRZ_HOME + "/var/lib/python")
from pprzlink.ivy import IvyMessagesInterface
from pprzlink.message import PprzMessage
# parse args
parser = argparse.ArgumentParser()
parser.add_argument('-n', '--name', dest='name', required=True, help="Regular expression of the names of the object to display")
parser.add_argument('-r', '--ref', dest='ref', required=True, help="Reference position in lat,long format in degrees")
parser.add_argument('-c', '--color', dest='color', default='white', help="Display color")
parser.add_argument('-sn', '--show_name', dest='show_name', action='store_true', help="Display the name of the object")
parser.add_argument('-t', '--timeout', dest='timeout', default=2., type=float, help="Timeout for removing object from scene")
parser.add_argument('-th', '--threshold', dest='threshold', default=0.1, type=float, help="Threshold distance to consider a moving object in meter")
parser.add_argument('-b', '--ivy_bus', dest='ivy_bus', help="Ivy bus address and port")
parser.add_argument('-s', '--server', dest='server', default="127.0.0.1", help="NatNet server IP address")
parser.add_argument('-m', '--multicast_addr', dest='multicast', default="239.255.42.99", help="NatNet server multicast address")
parser.add_argument('-dp', '--data_port', dest='data_port', type=int, default=1511, help="NatNet server data socket UDP port")
parser.add_argument('-cp', '--command_port', dest='command_port', type=int, default=1510, help="NatNet server command socket UDP port")
parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', help="display debug messages")
parser.add_argument('-vv', '--very_verbose', dest='very_verbose', action='store_true', help="display debug messages and optitrack debug messages")
parser.add_argument('-f', '--freq', dest='freq', default=1., type=float, help="transmit frequency")
parser.add_argument('-rp', '--refresh_period', dest='refresh_period', default=4., type=float, help="refresh period at which message is sent event if the object is not moving")
parser.add_argument('-up', '--up_axis', dest='up_axis', choices=['y_up', 'z_up'], default='y_up', help="Optitrack Up axis: y_up or z_up.")
parser.add_argument('-o', '--old_natnet', dest='old_natnet', action='store_true', help="Change the NatNet version to 2.9")
args = parser.parse_args()
# message period
period = 1./args.freq
# mean earth radius from IUGG
R_earth = 6371008.7714
# reference position lat0, long0
lat0, long0 = args.ref.split(",")
lat0 = float(lat0)
R_cos_lat0 = R_earth*np.cos(np.deg2rad(lat0))
long0 = float(long0)
# dictionary of id and received time by set name
markerset = {}
current_index = 1
# index of horizontal X and Y coordinates
X_AXIS = 0
if args.up_axis == 'y_up':
Y_AXIS = 2
Y_SIGN = -1
else:
Y_AXIS = 1
Y_SIGN = 1
# start ivy interface
if args.ivy_bus is not None:
ivy = IvyMessagesInterface("display_objects", ivy_bus=args.ivy_bus)
else:
ivy = IvyMessagesInterface("display_objects")
def is_moving(old_pos, new_pos):
'''
check if the markerset position have changed (at least one of the marker)
'''
try:
for po, pn in zip(old_pos, new_pos):
if abs(po[0]-pn[0]) > args.threshold or abs(po[1]-pn[1]) > args.threshold or abs(po[2] - pn[2]) > args.threshold:
return True # at least one marker has moved
return False # object has not moved
except:
# if failing, update position
return True
def receiveMarkerSet(name, posList):
'''
callback for markerset with name and marker position as input
'''
global current_index
# check if name is matching regexp
name = name.decode('utf-8')
if re.fullmatch(args.name, name) is not None:
# check if message should be sent (first time or period)
send = False
now = time()
if name in markerset:
dt = now - markerset[name]['time']
dt_refresh = now - markerset[name]['time_refresh']
if dt >= period:
# period elapsed, check if moved
markerset[name]['time'] = now
moved = is_moving(markerset[name]['pos'], posList)
if moved:
send = True
markerset[name]['pos'] = posList
if dt_refresh >= args.refresh_period:
# refresh period elapsed, send anyway
send = True
else:
send = True
markerset[name] = {'time_refresh': now, 'time': now, 'id': current_index, 'pos': posList }
current_index += 1
if args.very_verbose:
print(name, posList, time)
if send:
if args.verbose and (not args.very_verbose):
print(name, posList, now)
# build list of 2D points and compute convex hull
points = [(pos[X_AXIS], Y_SIGN*pos[Y_AXIS]) for pos in posList]
hull = ConvexHull(points)
# build lists of polygon corners to display in lat long
latitudes = [ int(1e7 * (lat0 + np.rad2deg(points[i][1] / R_earth))) for i in hull.vertices ]
longitudes = [ int(1e7 * (long0 + np.rad2deg(points[i][0] / R_cos_lat0))) for i in hull.vertices ]
# send SHAPE message
shape = PprzMessage("ground", "SHAPE")
shape['id'] = markerset[name]['id']
shape['linecolor'] = '"{}"'.format(args.color)
shape['fillcolor'] = '"{}"'.format(args.color)
shape['opacity'] = 1 # light
shape['shape'] = 1 # polygon
shape['status'] = 0 # create or update
shape['latarr'] = latitudes
shape['lonarr'] = longitudes
shape['radius'] = 0. # not relevant
if args.show_name:
shape['text'] = name
else:
shape['text'] = '" "'
ivy.send(shape)
markerset[name]['time_refresh'] = now
sleep(0.01)
def check_timeout():
'''
check timeout and if needed, remove object from scene and list
'''
now = time()
for name in list(markerset.keys()):
if now - markerset[name]['time'] > args.timeout:
shape = PprzMessage("ground", "SHAPE")
shape['id'] = markerset[name]['id']
shape['status'] = 1 # delete
# fill the rest even if not needed
shape['linecolor'] = '" "'
shape['fillcolor'] = '" "'
shape['opacity'] = 0
shape['shape'] = 1
shape['latarr'] = [0]
shape['lonarr'] = [0]
shape['radius'] = 0.
shape['text'] = '" "'
ivy.send(shape)
del markerset[name]
# start natnet interface
natnet_version = (3,0,0,0)
if args.old_natnet:
natnet_version = (2,9,0,0)
natnet = NatNetClient(
server=args.server,
markerSetListener=receiveMarkerSet,
dataPort=args.data_port,
commandPort=args.command_port,
verbose=args.very_verbose,
version=natnet_version)
print("Starting Object Display interface at %s" % (args.server))
try:
# Start up the streaming client.
# This will run perpetually, and operate on a separate thread.
natnet.run()
while True:
sleep(1)
check_timeout()
except (KeyboardInterrupt, SystemExit):
print("Shutting down ivy and natnet interfaces...")
natnet.stop()
ivy.shutdown()
except OSError:
print("Natnet connection error")
natnet.stop()
ivy.stop()
exit(-1)