From a1627975f97e3d40c32be61e6e219e9a9a8697f3 Mon Sep 17 00:00:00 2001 From: Brian Delhaisse Date: Sat, 16 Mar 2019 02:35:20 +0100 Subject: [PATCH] add algos, interfaces and bridges --- pyrobolearn/algos/__init__.py | 27 ++ pyrobolearn/algos/bo.py | 269 +++++++++++ pyrobolearn/algos/cem.py | 185 ++++++++ pyrobolearn/algos/cmaes.py | 184 ++++++++ pyrobolearn/algos/fd.py | 188 ++++++++ pyrobolearn/algos/neat_algo.py | 185 ++++++++ pyrobolearn/algos/power.py | 231 +++++++++ pyrobolearn/models/nn/ae.py | 2 +- pyrobolearn/models/nn/cnn.py | 2 +- pyrobolearn/models/nn/dnn.py | 2 +- pyrobolearn/models/nn/gan.py | 2 +- pyrobolearn/models/nn/mlp.py | 2 +- pyrobolearn/models/nn/neat_model.py | 2 +- pyrobolearn/models/nn/rcnn.py | 2 +- pyrobolearn/models/nn/rnn.py | 2 +- pyrobolearn/models/nn/vae.py | 2 +- pyrobolearn/robots/actuators/actuator.py | 2 +- pyrobolearn/robots/actuators/joints.py | 2 +- pyrobolearn/robots/base.py | 2 +- pyrobolearn/robots/sensors/camera.py | 2 +- pyrobolearn/robots/sensors/contact.py | 2 +- pyrobolearn/robots/sensors/force_torque.py | 2 +- pyrobolearn/robots/sensors/imu.py | 2 +- pyrobolearn/robots/sensors/joints.py | 2 +- pyrobolearn/robots/sensors/light.py | 2 +- pyrobolearn/robots/sensors/links.py | 2 +- pyrobolearn/robots/sensors/misc.py | 2 +- pyrobolearn/robots/sensors/sensor.py | 2 +- pyrobolearn/tools/README.md | 8 + pyrobolearn/tools/__init__.py | 6 + pyrobolearn/tools/bridges/__init__.py | 19 + pyrobolearn/tools/bridges/audio/__init__.py | 8 + .../tools/bridges/audio/robots/__init__.py | 6 + .../robots/bridge_speech_rotatory_uav.py | 51 ++ .../audio/robots/bridge_speech_wheeled.py | 83 ++++ .../tools/bridges/audio/world/__init__.py | 0 pyrobolearn/tools/bridges/bridge.py | 83 ++++ pyrobolearn/tools/bridges/camera/__init__.py | 8 + .../tools/bridges/camera/robots/__init__.py | 0 .../tools/bridges/camera/world/__init__.py | 0 .../tools/bridges/controllers/__init__.py | 8 + .../bridges/controllers/robots/__init__.py | 3 + .../robots/bridge_controller_wheeled.py | 79 ++++ .../bridges/controllers/world/__init__.py | 0 .../tools/bridges/mouse_keyboard/__init__.py | 4 + .../bridge_mousekeyboard_imitation_task.py | 375 +++++++++++++++ .../bridge_mousekeyboard_world.py | 437 ++++++++++++++++++ pyrobolearn/tools/bridges/vr/__init__.py | 8 + .../tools/bridges/vr/robots/__init__.py | 0 .../tools/bridges/vr/world/__init__.py | 0 pyrobolearn/tools/interfaces/__init__.py | 30 ++ .../tools/interfaces/audio/__init__.py | 3 + pyrobolearn/tools/interfaces/audio/audio.py | 340 ++++++++++++++ .../tools/interfaces/audio/microphone.py | 22 + pyrobolearn/tools/interfaces/audio/speaker.py | 8 + pyrobolearn/tools/interfaces/bci/__init__.py | 0 pyrobolearn/tools/interfaces/bci/bci.py | 19 + .../tools/interfaces/camera/__init__.py | 18 + .../tools/interfaces/camera/asus_xtion.py | 220 +++++++++ pyrobolearn/tools/interfaces/camera/camera.py | 28 ++ .../tools/interfaces/camera/description.md | 2 + pyrobolearn/tools/interfaces/camera/fer.py | 16 + pyrobolearn/tools/interfaces/camera/kinect.py | 357 ++++++++++++++ .../tools/interfaces/camera/openpose.py | 224 +++++++++ pyrobolearn/tools/interfaces/camera/webcam.py | 136 ++++++ .../tools/interfaces/controllers/__init__.py | 9 + .../interfaces/controllers/controller.py | 11 + .../interfaces/controllers/playstation.py | 272 +++++++++++ .../tools/interfaces/controllers/xbox.py | 267 +++++++++++ pyrobolearn/tools/interfaces/interface.py | 140 ++++++ .../interfaces/mouse_keyboard/__init__.py | 3 + .../mouse_keyboard/mousekeyboard.py | 228 +++++++++ .../tools/interfaces/robots/__init__.py | 3 + pyrobolearn/tools/interfaces/robots/robot.py | 11 + .../tools/interfaces/sensors/__init__.py | 7 + pyrobolearn/tools/interfaces/sensors/emg.py | 9 + .../tools/interfaces/sensors/sensor.py | 16 + .../tools/interfaces/suits/__init__.py | 6 + pyrobolearn/tools/interfaces/suits/suit.py | 9 + pyrobolearn/tools/interfaces/suits/xsens.py | 9 + pyrobolearn/tools/interfaces/vr/__init__.py | 11 + pyrobolearn/tools/interfaces/vr/htc.py | 23 + pyrobolearn/tools/interfaces/vr/oculus.py | 319 +++++++++++++ .../vr/oculus_windows/meshes/LICENSE.txt | 6 + .../vr/oculus_windows/meshes/LeftHand.mb | Bin 0 -> 539136 bytes .../vr/oculus_windows/meshes/RightHand.mb | Bin 0 -> 536180 bytes .../vr/oculus_windows/unity-scripts/Loom.cs | 121 +++++ .../unity-scripts/OculusHaptics.cs | 124 +++++ .../unity-scripts/PlayMovieTextureOnUI.cs | 43 ++ .../vr/oculus_windows/unity-scripts/TCP.cs | 71 +++ .../oculus_windows/unity-scripts/TCPImage.cs | 245 ++++++++++ .../unity-scripts/TestOculus.cs | 165 +++++++ pyrobolearn/tools/interfaces/vr/vr.py | 8 + setup.py | 2 +- 94 files changed, 6036 insertions(+), 22 deletions(-) create mode 100644 pyrobolearn/algos/__init__.py create mode 100644 pyrobolearn/algos/bo.py create mode 100755 pyrobolearn/algos/cem.py create mode 100755 pyrobolearn/algos/cmaes.py create mode 100755 pyrobolearn/algos/fd.py create mode 100755 pyrobolearn/algos/neat_algo.py create mode 100755 pyrobolearn/algos/power.py create mode 100644 pyrobolearn/tools/README.md create mode 100644 pyrobolearn/tools/__init__.py create mode 100644 pyrobolearn/tools/bridges/__init__.py create mode 100644 pyrobolearn/tools/bridges/audio/__init__.py create mode 100644 pyrobolearn/tools/bridges/audio/robots/__init__.py create mode 100644 pyrobolearn/tools/bridges/audio/robots/bridge_speech_rotatory_uav.py create mode 100644 pyrobolearn/tools/bridges/audio/robots/bridge_speech_wheeled.py create mode 100644 pyrobolearn/tools/bridges/audio/world/__init__.py create mode 100644 pyrobolearn/tools/bridges/bridge.py create mode 100644 pyrobolearn/tools/bridges/camera/__init__.py create mode 100644 pyrobolearn/tools/bridges/camera/robots/__init__.py create mode 100644 pyrobolearn/tools/bridges/camera/world/__init__.py create mode 100644 pyrobolearn/tools/bridges/controllers/__init__.py create mode 100644 pyrobolearn/tools/bridges/controllers/robots/__init__.py create mode 100644 pyrobolearn/tools/bridges/controllers/robots/bridge_controller_wheeled.py create mode 100644 pyrobolearn/tools/bridges/controllers/world/__init__.py create mode 100644 pyrobolearn/tools/bridges/mouse_keyboard/__init__.py create mode 100644 pyrobolearn/tools/bridges/mouse_keyboard/bridge_mousekeyboard_imitation_task.py create mode 100644 pyrobolearn/tools/bridges/mouse_keyboard/bridge_mousekeyboard_world.py create mode 100644 pyrobolearn/tools/bridges/vr/__init__.py create mode 100644 pyrobolearn/tools/bridges/vr/robots/__init__.py create mode 100644 pyrobolearn/tools/bridges/vr/world/__init__.py create mode 100644 pyrobolearn/tools/interfaces/__init__.py create mode 100644 pyrobolearn/tools/interfaces/audio/__init__.py create mode 100644 pyrobolearn/tools/interfaces/audio/audio.py create mode 100644 pyrobolearn/tools/interfaces/audio/microphone.py create mode 100644 pyrobolearn/tools/interfaces/audio/speaker.py create mode 100644 pyrobolearn/tools/interfaces/bci/__init__.py create mode 100644 pyrobolearn/tools/interfaces/bci/bci.py create mode 100644 pyrobolearn/tools/interfaces/camera/__init__.py create mode 100644 pyrobolearn/tools/interfaces/camera/asus_xtion.py create mode 100644 pyrobolearn/tools/interfaces/camera/camera.py create mode 100644 pyrobolearn/tools/interfaces/camera/description.md create mode 100644 pyrobolearn/tools/interfaces/camera/fer.py create mode 100644 pyrobolearn/tools/interfaces/camera/kinect.py create mode 100644 pyrobolearn/tools/interfaces/camera/openpose.py create mode 100644 pyrobolearn/tools/interfaces/camera/webcam.py create mode 100644 pyrobolearn/tools/interfaces/controllers/__init__.py create mode 100644 pyrobolearn/tools/interfaces/controllers/controller.py create mode 100755 pyrobolearn/tools/interfaces/controllers/playstation.py create mode 100644 pyrobolearn/tools/interfaces/controllers/xbox.py create mode 100644 pyrobolearn/tools/interfaces/interface.py create mode 100644 pyrobolearn/tools/interfaces/mouse_keyboard/__init__.py create mode 100644 pyrobolearn/tools/interfaces/mouse_keyboard/mousekeyboard.py create mode 100644 pyrobolearn/tools/interfaces/robots/__init__.py create mode 100644 pyrobolearn/tools/interfaces/robots/robot.py create mode 100644 pyrobolearn/tools/interfaces/sensors/__init__.py create mode 100644 pyrobolearn/tools/interfaces/sensors/emg.py create mode 100644 pyrobolearn/tools/interfaces/sensors/sensor.py create mode 100644 pyrobolearn/tools/interfaces/suits/__init__.py create mode 100644 pyrobolearn/tools/interfaces/suits/suit.py create mode 100644 pyrobolearn/tools/interfaces/suits/xsens.py create mode 100644 pyrobolearn/tools/interfaces/vr/__init__.py create mode 100644 pyrobolearn/tools/interfaces/vr/htc.py create mode 100644 pyrobolearn/tools/interfaces/vr/oculus.py create mode 100644 pyrobolearn/tools/interfaces/vr/oculus_windows/meshes/LICENSE.txt create mode 100644 pyrobolearn/tools/interfaces/vr/oculus_windows/meshes/LeftHand.mb create mode 100644 pyrobolearn/tools/interfaces/vr/oculus_windows/meshes/RightHand.mb create mode 100755 pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/Loom.cs create mode 100755 pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/OculusHaptics.cs create mode 100755 pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/PlayMovieTextureOnUI.cs create mode 100755 pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/TCP.cs create mode 100755 pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/TCPImage.cs create mode 100755 pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/TestOculus.cs create mode 100644 pyrobolearn/tools/interfaces/vr/vr.py diff --git a/pyrobolearn/algos/__init__.py b/pyrobolearn/algos/__init__.py new file mode 100644 index 0000000..6dd7eb0 --- /dev/null +++ b/pyrobolearn/algos/__init__.py @@ -0,0 +1,27 @@ + +# import RL algo +# from rl_algo import * + +# import CEM +from cem import CEM + +# import CMAES +from cmaes import CMAES + +# import from NEAT +from neat_algo import NEAT + +# import BO +from bo import BO + +# import FD +from fd import FD + +# import PoWER +from power import PoWER + +# import REINFORCE +# from reinforce import REINFORCE + +# import PPO +# from ppo import PPO diff --git a/pyrobolearn/algos/bo.py b/pyrobolearn/algos/bo.py new file mode 100644 index 0000000..2e01faa --- /dev/null +++ b/pyrobolearn/algos/bo.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python +"""Provide the Bayesian Optimization algorithm. +""" + +import numpy as np +import torch +import time +import GPy +import GPyOpt + +from pyrobolearn.envs import Env +from pyrobolearn.tasks import RLTask + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class BO(object): + r"""Bayesian Optimization + + Bayesian Optimization is a global (gradient-free), probabilistic, non-parametric, model-based, optimization of + black-box functions. + + Bayesian optimization can be formulated as an optimization problem: + + .. math:: \theta^* = arg\,max_{\theta} f(\theta) + + where :math:`\theta` are the parameters of the model we are trying to optimize, and :math:`f` is the unknown + objective function which is modeled using a probabilistic model such as a Gaussian Process (GP). By samp + + Popular acquisition functions which specify which parameters to test next by making a trade-off between + exploitation and exploration, include: + * Probability of Improvement (PI) [7]: + * Expected Improvement (EI) [8]: + * Upper Confidence Bound (UCB) [9]: + + Pseudo-Algo (from [3]): + D <-- if available: {\theta, f(\theta)} + Prior <-- if available: prior of the response surface + while optimize: + train a response surface from D + + References: + [1] "Bayesian Approach to Global Optimization: Theory and Applications", Mockus, 1989 + [2] "A Tutorial on Bayesian Optimization of Expensive Cost Functions, with Application to Active User Modeling + and Hierarchical Reinforcement Learning", Brochu et al., 2010 + [3] "Taking the Human Out of the Loop: a Review of Bayesian Optimization", Shahriari et al., 2016 + [4] "Bayesian Optimization for Learning Gaits under Uncertainty: An Experimental Comparison on a Dynamic + Bipedal Walker", Calandra et al., 2015 + [5] "Gaussian Processes for Machine Learning", Rasmussen and Williams, 2006 + [6] "GPyOpt: A Bayesian Optimization framework in python" (2016), https://github.com/SheffieldML/GPyOpt + [7] "A New Method of Locating the Maximum Point of an Arbitrary Multipeak Curve in the Presence of Noise", + Kushner, 1964 + [8] "The Application of Bayesian Methods for Seeking the Extremum", Mockus et al., 1978 + [9] "A Statistical Method for Global Optimization", Cox et al., 1997 + """ + + def __init__(self, task=None, policy=None, domain=(-3., 3.), num_workers=1): + """ + Initialize the Bayesian Optimization algorithm. + + Args: + task (RLTask, Env): RL task/env to run + policy (Policy): specify the policy (model) to optimize + num_workers (int): number of workers/jobs to run in parallel + """ + if isinstance(task, Env): + task = RLTask(task, policy) + self.task = task + self.policy = policy + self.num_workers = num_workers + + self.best_reward = -np.infty + self.best_parameters = None + + self.num_steps = 1000 + self.num_rollouts = 1 + self.verbose = False + self.episode = 0 + + self.domain = domain + + self.rewards = [] + + # def get_vectorized_parameters(self, to_numpy=True): + # parameters = self.policy.parameters + # + # vector = [] + # from_numpy = False + # for parameter in parameters: + # if isinstance(parameter, np.ndarray): + # from_numpy = True + # vector.append(parameter.reshape(-1)) + # + # if from_numpy: + # print(vector) + # vector = np.concatenate(vector) + # if not to_numpy: + # return torch.from_numpy(vector) + # else: + # vector = torch.cat(vector) + # if to_numpy: + # return vector.detach().numpy() + # return vector + # + # def set_vectorized_parameters(self, vector): + # # convert the vector to torch array + # if isinstance(vector, np.ndarray): + # vector = torch.from_numpy(vector).float() + # + # # set the parameters from the vectorized one + # idx = 0 + # for parameter in self.policy.parameters: + # size = parameter.nelement() + # parameter.data = vector[idx:idx+size].reshape(parameter.shape) + # idx += size + + def explore_and_evaluate(self, params): + # set policy parameters + # self.set_vectorized_parameters(params[0]) + self.policy.set_vectorized_parameters(params[0]) + + # run a number of rollouts + reward = [] + for rollout in range(self.num_rollouts): + rew = self.task.run(num_steps=self.num_steps, dt=1./240, use_terminating_condition=True, render=False) + reward.append(rew) + reward = np.mean(reward) + + self.rewards.append(reward) + + # print info + self.episode += 1 + if self.verbose: + print('Episode {} - reward: {}'.format(self.episode, reward)) + + return reward + + def train(self, num_steps=1000, num_rollouts=1, num_episodes=1, verbose=False, seed=None, max_time=3600): + # set few variables + self.num_steps = num_steps + self.num_rollouts = num_rollouts + self.episode = 0 + self.verbose = verbose + self.rewards = [] + + # set seed if specified + if seed is not None: + np.random.seed(seed) + + # init + # parameters = self.get_vectorized_parameters(to_numpy=True) + parameters = self.policy.get_vectorized_parameters(to_numpy=True) + + # define domain + domain = [{'name': 'params', 'type': 'continuous', 'domain': self.domain, 'dimensionality': len(parameters)}] + + # Solve the optimization + opt = GPyOpt.methods.BayesianOptimization(f=self.explore_and_evaluate, + domain=domain, + model_type='GP', # 'sparseGP' + acquisition_type='EI', # 'UCB'/'LCB', 'EI', 'MPI' + acquisition_optimizer_type='lbfgs', # 'DIRECT', 'CMA' + num_cores=self.num_workers, + verbosity=verbose, + maximize=True, # True + verbosity_model=False, # True + kernel=GPy.kern.RBF(input_dim=1)) + + # print(opt.model.kernel.name) + + # Run the optimization + max_iter = num_episodes # evaluation budget (min=4), nb_eval = 4 + max_iter + max_time = max_time # time budget + eps = 10e-6 # Minimum allows distance between the last two observations + + if verbose: + print('Optimizing...') + + start = time.time() + opt.run_optimization(max_iter, max_time, eps) + end = time.time() + + if verbose: + print('Done with total time: {}'.format(end - start)) + + # save best parameters and reward + self.best_parameters = opt.x_opt + self.best_reward = -opt.fx_opt + + # print best reward + if verbose: + print("\nBest reward found: {}".format(self.best_reward)) + + # set the best parameters + self.policy.set_vectorized_parameters(self.best_parameters) + + return self.rewards + + def test(self, num_steps=1000, dt=0, use_terminating_condition=False, render=True): + return self.task.run(num_steps=num_steps, dt=dt, use_terminating_condition=use_terminating_condition, + render=render) + + # def optimize(self, fct, num_iter=6, max_time=3600, seed=None, verbose=False): + # """ + # Optimize the given model based + # + # Args: + # num_iter (int): number of iteration + # max_time (float): + # seed (int): random seed + # verbose (bool): True if we should print information during the optimization process + # + # Returns: + # + # """ + # # set seed if specified + # if seed is not None: + # np.random.seed(seed) + # + # # define domain + # domain = [{'name': 'params', 'type': 'continuous', 'domain': (-1, 1), 'dimensionality': 1}] + # + # # Solve the optimization + # opt = GPyOpt.methods.BayesianOptimization(f=fct, + # domain=domain, + # model_type='GP', # 'sparseGP' + # acquisition_type='EI', # 'UCB'/'LCB', 'EI', 'MPI' + # acquisition_optimizer_type='lbfgs', # 'DIRECT', 'CMA' + # num_cores=1, + # verbosity=verbose, + # maximize=True, # True + # verbosity_model=False, # True + # kernel=GPy.kern.RBF(input_dim=1)) + # + # # print(opt.model.kernel.name) + # + # # Run the optimization + # max_iter = num_iter # evaluation budget (min=4), nb_eval = 4 + max_iter + # max_time = max_time # time budget + # eps = 10e-6 # Minimum allows distance between the last two observations + # + # if verbose: + # print('Optimizing...') + # + # start = time.time() + # opt.run_optimization(max_iter, max_time, eps) + # end = time.time() + # + # if verbose: + # print('Done with total time: {}'.format(end - start)) + # + # # save best parameters and reward + # self.best_parameters = opt.x_opt + # self.best_reward = opt.fx_opt + # + # return opt + + +# Tests +if __name__ == '__main__': + pass diff --git a/pyrobolearn/algos/cem.py b/pyrobolearn/algos/cem.py new file mode 100755 index 0000000..8648f47 --- /dev/null +++ b/pyrobolearn/algos/cem.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python +"""Provide the Cross-Entropy Method algorithm. + +This CEM is an evolutionary algorithm that explores in the parameter space of the policy in an episodic way. +""" + +import numpy as np +import torch +# from pathos.multiprocessing import Pool + +from pyrobolearn.envs import Env +from pyrobolearn.tasks import RLTask + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class CEM(object): # RLAlgo + r"""Cross-Entropy Method + + Type: population-based and model-free, exploration in parameter space, episode-based. + + The Cross-Entropy Method (CEM) is an evolutionary algorithm that explores in the parameter space of the policy + in an episodic way. That is, everytime the parameters are updated, the policy is run for a whole episode before + being evaluated. + + This algorithm works by first assuming that the parameters are generated from a multivariate normal distribution + with an initial mean and a fixed covariance matrix. Few samples are then drawn from this distribution to form + the initial population of parameter vectors. Each parameter vector is then set on the policy and evaluated on + the whole episode. The best parameter vectors which constitutes a fraction of the population and called + the elites are then selected. From them, a new mean and standard covariance matrix are computed and used to form + the new multivariate normal distribution from which the next population is generated. This process is carried + out for several generation. At the end, the best parameter (the elite) is returned. + + References: + [1] "The Cross-Entropy Method: A Unified Approach to Combinatorial Optimization, Monte-Carlo Simulation + and Machine Learning", Rubinstein et al., 2004 + [2] "A Tutorial on the Cross-Entropy Method", de Boer, 2003 + [3] "The Cross Entropy Method for Fast Policy Search", Mannor et al., 2003 (ICML) + + Interesting codes (the code in this file was inspired from the first reference): + - Schulman's presentation (2016) + - modular_rl: https://github.com/joschu/modular_rl + - pytorch-rl: https://github.com/khushhallchandra/pytorch-rl + - rllab: https://github.com/rll/rllab + """ + + def __init__(self, task, policy, population_size=20, elite_fraction=0.2, num_workers=1): + """ + Initialize the CEM algorithm. + + Args: + task (RLTask, Env): RL task/env to run + policy (Policy): specify the policy (model) to optimize + population_size (int): size of the population + elite_fraction (float): fraction of elites to use to compute the new mean and covariance matrix of the + multivariate normal distribution + num_workers (int): number of workers/jobs to run in parallel + """ + # create explorer + # create evaluator + # create updater + # super(CEM, self).__init__(self, explorer, evaluator, updater, num_workers=1) + + if isinstance(task, Env): + task = RLTask(task, policy) + self.task = task + self.policy = policy + self.population_size = population_size + self.elite_fraction = elite_fraction + self.num_workers = num_workers + + self.best_reward = -np.infty + self.best_parameters = None + + def get_vectorized_parameters(self, to_numpy=True): + parameters = self.policy.parameters + vector = torch.cat([parameter.reshape(-1) for parameter in parameters]) # np.concatenate = torch.cat + if to_numpy: + return vector.detach().numpy() + return vector + + def set_vectorized_parameters(self, vector): + # convert the vector to torch array + if isinstance(vector, np.ndarray): + vector = torch.from_numpy(vector).float() + + # set the parameters from the vectorized one + idx = 0 + for parameter in self.policy.parameters: + size = parameter.nelement() + parameter.data = vector[idx:idx+size].reshape(parameter.shape) + idx += size + + def train(self, num_steps=1000, num_rollouts=1, num_episodes=1, verbose=False, seed=None): + # set seed + if seed is not None: + np.random.seed(seed) + + # create recorders + max_rewards, avg_rewards = [], [] + + # init + theta_mean = self.get_vectorized_parameters(to_numpy=True) + theta_std = np.ones(len(theta_mean)) + # pool = Pool(self.num_workers) + + # for each episode/generation + for episode in range(num_episodes): + if verbose: + print('\nEpisode {}'.format(episode+1)) + + # 1. Explore + # sample parameter vectors + thetas = np.random.multivariate_normal(theta_mean, np.diag(theta_std), self.population_size) + + # perform one episode for each parameter + # jobs = [pool.apipe(self.task.run, num_steps, use_terminating_condition=False) for theta in thetas] + rewards = [] + for i, theta in enumerate(thetas): + # set policy parameters + self.set_vectorized_parameters(theta) + + # run a number of rollouts + reward = [] + for rollout in range(num_rollouts): + rew = self.task.run(num_steps=num_steps, use_terminating_condition=True, render=False) + reward.append(rew) + reward = np.mean(reward) + rewards.append(reward) + + # print info + if verbose: + print(' -- individual {} with avg reward of {}'.format(i+1, reward)) + + # 2. Evaluate (compute loss) + + # 3. Update + + # get elite parameters + num_elites = int(self.population_size * self.elite_fraction) + elite_ids = np.argsort(rewards)[-num_elites:] + elite_thetas = np.array([thetas[i] for i in elite_ids]) + + # update theta_mean and theta_std + theta_mean = elite_thetas.mean(axis=0) + theta_std = np.sqrt(np.mean((elite_thetas - theta_mean) ** 2, axis=0)) + + # 4. Save best reward and associated parameter + max_reward, avg_reward = np.max(rewards), np.mean(rewards) + if max_reward > self.best_reward: + self.best_reward = max_reward + self.best_parameters = thetas[elite_ids[-1]] + + # print info + if verbose: + print("Episode {} mean reward: {} max reward: {}".format(episode+1, avg_reward, max_reward)) + + # Save the evolution of the algo + avg_rewards.append(avg_reward) + max_rewards.append(max_reward) + + # print best reward + if verbose: + print("\nBest reward found: {}".format(self.best_reward)) + + # set the best parameters + self.set_vectorized_parameters(self.best_parameters) + + return avg_rewards, max_rewards + + def test(self, num_steps=1000, dt=0, use_terminating_condition=False, render=True): + return self.task.run(num_steps=num_steps, dt=dt, use_terminating_condition=use_terminating_condition, + render=render) + + +# Tests +if __name__ == '__main__': + pass diff --git a/pyrobolearn/algos/cmaes.py b/pyrobolearn/algos/cmaes.py new file mode 100755 index 0000000..ca14cd0 --- /dev/null +++ b/pyrobolearn/algos/cmaes.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python +"""Provide the Covariance Matrix Adaptation Evolution Strategy algorithm. + +'The Covariance Matrix Adaptation Evolution Strategy (CMA-ES) is a stochastic derivative-free numerical optimization +algorithm for difficult (non-convex, ill-conditioned, multi-modal, rugged, noisy) optimization problems in +continuous search spaces.' [1] + +References: + [1] "Python implementation of CMA-ES", Hansen et al., 2019 (https://github.com/CMA-ES/pycma) +""" + +import numpy as np + +try: + import cma +except ImportError as e: + raise ImportError(e.__str__() + "\n HINT: you can install CMA-ES or `pycma` directly via 'pip install cma'.") + +from pyrobolearn.envs import Env +from pyrobolearn.tasks import RLTask + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class CMAES(object): # Algo): + r"""Covariance Matrix Adaptation Evolution Strategy (CMA-ES) + + Type: population-based (genetic), stochastic and derivative-free, exploration in parameter space, optimization + for non-linear and non-convex functions, episode-based. + + 'The Covariance Matrix Adaptation Evolution Strategy (CMA-ES) is a stochastic derivative-free numerical + optimization algorithm for difficult (non-convex, ill-conditioned, multi-modal, rugged, noisy) optimization + problems in continuous search spaces.' [3] + + Complexity: + + References: + [1] "Completely Derandomized Self-Adaptation in Evolution Strategies", Hansen et al., 2001 + [2] "The CMA Evolution Strategy: A Tutorial", Hansen, 2016 + [3] "Python implementation of CMA-ES", Hansen et al., 2019: https://github.com/CMA-ES/pycma + [4] pycma API documentation: cma.gforge.inria.fr/apidocs-pycma + + Python Implementations: + - pycma: https://github.com/CMA-ES/pycma + - rllab: https://github.com/rll/rllab + - DEAP: https://github.com/DEAP/deap and http://deap.readthedocs.io/en/master/examples/cmaes.html + """ + + def __init__(self, task, policy, population_size=20, sigma=0.5, num_workers=1): + """ + Initialize the CMA-ES algorithm. + + Args: + task (RLTask, Env): RL task/env to run + policy (Policy): specify the policy (model) to optimize + population_size (int): size of the population + sigma (float): initial standard deviation for CMA-ES + num_workers (int): number of workers/jobs to run in parallel + """ + # create explorer + # create evaluator + # create updater + # super(CEM, self).__init__(self, explorer, evaluator, updater, num_workers=1) + + if isinstance(task, Env): + task = RLTask(task, policy) + self.task = task + self.policy = policy + self.population_size = population_size + self.sigma = sigma + self.num_workers = num_workers + self.es = None + + self.best_reward = -np.infty + self.best_parameters = None + + ############## + # Properties # + ############## + + @property + def population_size(self): + return self._population_size + + @population_size.setter + def population_size(self, size): + # check argument + if not isinstance(size, int): + raise TypeError("Expecting the population size to be an integer.") + if size < 1: + raise ValueError("Expecting the population size to be an integer bigger than 0.") + + # set population size + self._population_size = size + + ########### + # Methods # + ########### + + def explore_and_evaluate(self, params, num_steps, num_rollouts): + # set policy parameters + self.policy.set_vectorized_parameters(params) + + # run a number of rollouts + reward = [] + for rollout in range(num_rollouts): + rew = self.task.run(num_steps=num_steps, use_terminating_condition=True, render=False) + reward.append(rew) + reward = np.mean(reward) + + # return cost to minimize + return -reward + + def train(self, num_steps=1000, num_rollouts=1, num_episodes=1, verbose=False, seed=None): + # create CMA-ES + self.es = cma.CMAEvolutionStrategy(self.policy.get_vectorized_parameters(), sigma0=self.sigma, + inopts={'popsize': self.population_size}) # {'bounds': [-np.inf, np.inf]} + + # set seed + if seed is not None: + self.es.opts.set({'seed': seed}) + + # set number of iterations + self.es.opts.set({'maxiter': num_episodes}) + + # create recorders + max_rewards, avg_rewards = [], [] + + # optimize + # self.es.optimize(self.explore_and_evaluate, iterations=num_episodes, args=(num_steps, num_rollouts), + # verb_disp=int(verbose)) + # evaluate + # (solutions, costs) = self.es.ask_and_eval(self.explore_and_evaluate) + + # for each episode/generation + # while not self.es.stop(): + for episode in range(num_episodes): + if self.es.stop(): + break + + # optimize + parameters = self.es.ask() + costs = [self.explore_and_evaluate(params, num_steps, num_rollouts) for params in parameters] + self.es.tell(parameters, costs) + + # get rewards + max_rewards.append(-np.min(costs)) + avg_rewards.append(-np.mean(costs)) + + # print info + if verbose: + # self.es.disp(1) + print("Episode {} mean reward: {} max reward: {}".format(episode + 1, avg_rewards[-1], max_rewards[-1])) + + # if verbose: + # self.es.result_pretty() + + # print info + if verbose: + # self.es.disp(1) + print('Termination by {}'.format(self.es.stop())) + print('Best reward found = {}'.format(-self.es.result[1])) + # print('solution = {}'.format(self.es.result[0])) + + # get results (check documentation of ` _CMAEvolutionStrategyResult`) + self.best_parameters = self.es.result[0] + self.best_reward = -self.es.result[1] + + # set the best parameters + self.policy.set_vectorized_parameters(self.best_parameters) + + return avg_rewards, max_rewards + + def test(self, num_steps=1000, dt=0, use_terminating_condition=False, render=True): + return self.task.run(num_steps=num_steps, dt=dt, use_terminating_condition=use_terminating_condition, + render=render) diff --git a/pyrobolearn/algos/fd.py b/pyrobolearn/algos/fd.py new file mode 100755 index 0000000..4992bfb --- /dev/null +++ b/pyrobolearn/algos/fd.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python +"""Provide the Finite-Difference (FD) method algorithm. + +This FD method is a policy gradient algorithm that explores in the parameter space of the policy in an episodic way. +""" + +import numpy as np +import torch + +from pyrobolearn.envs import Env +from pyrobolearn.tasks import RLTask +# from rl_algo import GradientRLAlgo + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class FD(object): # GradientRLAlgo): + r"""Finite-Difference Policy Gradient Method. + + Type: policy gradient based (on-policy be definition) with exploration in the parameter space + + The goal of RL is to maximize the expected return: + + .. math:: J(\theta) = \int p(\tau) R(\tau) d\tau + + The Finite-Difference (FD) algorithm perturbs the parameter space of the policy and evaluate for each perturbation + the expected return J(\theta_i + \Delta_{\theta_i}) + + The gradient :math:`g_{FD} \approx \nabla_\theta J`is then given by: + + .. math:: `g_{FD} = (\Delta\Theta^\top \Delta\Theta)^{-1} \Delta\Theta^\top \Delta J` + + which is used to to perform a gradient ascent step: :math:`\theta_{i+1} = \theta_{i} + \eta g_{FD}`, + where :math:`\eta` is the learning rate coefficient. + + Properties: + * Exploration is performed in the parameter space of the policy + Pros: + * Easy to implement and test + * work with deterministic and stochastic rl + * highly efficient in simulation + Cons: + * the perturbation of the parameters is hard (especially with systems that can go unstable) + * O(M^3) for the time complexity (because of the matrix inversion), where M is the number of parameters + + Pseudo-algo: + + References: + [1] "Policy Gradient Methods" (http://www.scholarpedia.org/article/Policy_gradient_methods), Peters, 2010 + """ + + def __init__(self, task, policy, num_variations=None, std_dev=0.01, difference_type='central', learning_rate=0.001, + normalize_grad=False, num_workers=1): + # hyperparameters + """ + Initialize the FD algorithm. + + Args: + task (RLTask, Env): RL task/env to run + policy (Policy): specify the policy (model) to optimize + num_variations (None, int): number of times we vary the parameters by a small different increment. + If None, it will be twice the number of parameters as according to [1], it yields very accurate + gradient estimates. + std_dev (float): the small increments are generated from a Normal distribution center at 0 and + difference_type (str): there are two difference type of estimators: 'forward' or 'central'. + The forward-difference estimator computes the gradient using + :math:`J(\theta + \Delta\theta) - J(\theta)`, while the central-difference estimator computes the + gradient using :math:`J(\theta + \Delta\theta) - J(\theta - \Delta\theta)` + learning_rate (float): learning rate (=coefficient) for the gradient ascent step + normalize_grad (bool): specify if we should normalize the gradients + num_workers (int): number of workers/jobs to run in parallel + """ + # create explorer + # create evaluator + # create updater + # super(FD, self).__init__(self, explorer, evaluator, updater, num_workers=1) + + if isinstance(task, Env): + task = RLTask(task, policy) + self.task = task + self.policy = policy + self.num_workers = num_workers + + # set the number of variations (small increments to vary the parameters) + # From [1]: "Empirically it can be observed that taking the number of variations as twice the number + # of parameters yields very accurate gradient estimates" + if num_variations is None: + self.num_variations = 2 * self.policy.num_parameters + + # set standard deviation + self.stddev = np.abs(std_dev) + + # set difference type + if difference_type != 'forward' and difference_type != 'central': + raise ValueError("Expecting the 'difference_type' argument to be 'forward' or 'central'. Instead got " + "'{}'".format(difference_type)) + self.difference_type = difference_type + + # set other parameters + self.lr = learning_rate + self.normalize_grad = bool(normalize_grad) + + # remember best parameters + self.best_reward = -np.infty + self.best_parameters = None + + def explore_and_evaluate(self, params, num_steps, num_rollouts): + # set policy parameters + self.policy.set_vectorized_parameters(params) + + # run a number of rollouts + reward = [] + for rollout in range(num_rollouts): + rew = self.task.run(num_steps=num_steps, use_terminating_condition=True, render=False) + reward.append(rew) + reward = np.mean(reward) + + return reward + + def train(self, num_steps=1000, num_rollouts=1, num_episodes=1, verbose=False, seed=None): + # set seed + if seed is not None: + np.random.seed(seed) + + # for each episode + rewards = [] + for episode in range(num_episodes): + + # get parameters + params = self.policy.get_vectorized_parameters() + J_plus, J_minus = np.zeros(self.num_variations), np.zeros(self.num_variations) + Delta_Params = np.zeros((self.num_variations, len(params))) + + # evaluate with the current parameters + J = self.explore_and_evaluate(params, num_steps, num_rollouts) + rewards.append(J) + + # Save best reward and associated parameter + if J > self.best_reward: + self.best_reward = J + self.best_parameters = params + + # print info + if verbose: + print('\nEpisode {} - expected return: {}'.format(episode + 1, J)) + + # 1. Explore + for i in range(self.num_variations): + # sample parameter increment step vector + delta_params = np.random.normal(loc=0.0, scale=self.stddev, size=len(params)) + Delta_Params[i] = delta_params + + # estimate J(\theta + \delta) + new_params = params + delta_params + J_plus[i] = self.explore_and_evaluate(new_params, num_steps, num_rollouts) + + # estimate J(\theta - \delta) + if self.difference_type == 'forward': + J_minus[i] = J + elif self.difference_type == 'central': + new_params = params - delta_params + J_minus[i] = self.explore_and_evaluate(new_params, num_steps, num_rollouts) + else: + raise ValueError("Expecting the 'difference_type' argument to be 'forward' or 'central'. " + "Instead got '{}'".format(self.difference_type)) + # 2. Evaluate + + # 3. Update + delta_J = J_plus - J_minus + grad = np.linalg.pinv(Delta_Params).dot(delta_J) + if self.normalize_grad: + grad /= np.linalg.norm(grad) + params = params + self.lr * grad # TODO: allows the user to choose the optimizer + # self.optimizer.optimize(self.policy.get_params(), grad) + self.policy.set_vectorized_parameters(params) + + return rewards + + def test(self, num_steps=1000, dt=0, use_terminating_condition=False, render=True): + return self.task.run(num_steps=num_steps, dt=dt, use_terminating_condition=use_terminating_condition, + render=render) diff --git a/pyrobolearn/algos/neat_algo.py b/pyrobolearn/algos/neat_algo.py new file mode 100755 index 0000000..0688811 --- /dev/null +++ b/pyrobolearn/algos/neat_algo.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python +"""Define the NEAT algorithm. + +This uses the Neuro-Evolution through Augmenting topologies (NEAT) framework. It allows the evolution of not only the +parameters/weights but also the topological structure of neural networks. Note that the following algorithm only +works with neural networks and is thus tightly coupled with its associated policy/model. +""" + +import numpy as np + +from pyrobolearn.envs import Env +from pyrobolearn.tasks import RLTask +from pyrobolearn.policies import NEATPolicy +# from rl_algo import RLAlgo + +try: + import neat + # from neat import nn, population, config, statistics +except ImportError as e: + raise ImportError(e.__str__() + "\n HINT: you can install NEAT directly via 'pip install neat-python'.") + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class NEAT(object): # RLAlgo): + r"""NEAT: Neuro-Evolution through Augmenting Topologies + + Exploration is carried out in the parameter and hyperparameter spaces. + + Warnings: This algorithm is a little bit special and currently only works with the associated policy/learning model. + + References: + [1] "Evolving Neural Networks through Augmenting Topologies", Stanley et al., 2002 + [2] NEAT-Python + - documentation: https://neat-python.readthedocs.io/en/latest/index.html + - github repo: https://github.com/CodeReclaimers/neat-python + [3] PyTorch NEAT (built upon NEAT-Python): https://github.com/uber-research/PyTorch-NEAT + """ + + def __init__(self, task, policy, population_size=20, species_elitism=2, elitism=2, min_species_size=2, + survival_threshold=0.2, max_stagnation=15, compatibility_threshold=3, num_workers=1): + r""" + Initialize the NEAT algorithm. + + Args: + task (RLTask, Env): RL task/env to run + policy (Policy): specify the policy (model) to optimize + population_size (int): size of the population + elitism (int): The number of most-fit individuals in each species that will be preserved as-is from one + generation to the next. + num_workers (int): number of workers/jobs to run in parallel + """ + # create explorer + # create evaluator + # create updater + # super(NEAT, self).__init__(self, explorer, evaluator, updater, num_workers=1) + + # set task + if isinstance(task, Env): + task = RLTask(task, policy) + self.task = task + + # set policy + if policy is None: + policy = self.task.policies[0] # TODO: currently assume only 1 policy + if not isinstance(policy, NEATPolicy): + raise TypeError("Expecting the policy to be an instance of 'NEATPolicy'.") + self.policy = policy + + # set config file + # more info about genome's config file: https://neat-python.readthedocs.io/en/latest/config_file.html + # more info about activation fct: https://neat-python.readthedocs.io/en/latest/activation.html + config_dict = {'[NEAT]': {'fitness_criterion': 'max', + 'fitness_threshold': 100, + 'no_fitness_termination': True, + 'pop_size': population_size, + 'reset_on_extinction': True}, + '[DefaultSpeciesSet]': {'compatibility_threshold': compatibility_threshold}, + '[DefaultStagnation]': {'species_fitness_func': 'max', + 'max_stagnation': max_stagnation, + 'species_elitism': species_elitism}, + '[DefaultReproduction]': {'elitism': elitism, + 'survival_threshold': survival_threshold, + 'min_species_size': min_species_size}} + + # update config file of policy + self.policy.update_config(config_dict) + + # get population + self.population = self.policy.population + + # create useful variables + self.num_steps = 1000 + self.num_rollouts = 1 + self.verbose = False + self.episode = 0 + self.avg_rewards, self.max_rewards = [], [] + + self.best_reward = -np.infty + self.best_parameters = None + + def explore_and_evaluate(self, genomes, config): + # print info + self.episode += 1 + if self.verbose: + print('\nEpisode {}'.format(self.episode)) + + # for each individual in the population, evaluate it on the task + rewards = [] + for genome_id, genome in genomes: + # set genome + self.policy.set_network(genome, config) + + # run a number of rollouts + reward = [] + for rollout in range(self.num_rollouts): + # run the task + rew = self.task.run(num_steps=self.num_steps, use_terminating_condition=True, render=False) + reward.append(rew) + + # run in parallel + # jobs = [self.pool.apipe(evaluate, NEAT_Agent, self.env, self.cfg['num_steps'], + # genome, typeNN=self.type) for genome in genomes] + # for job, genome in zip(jobs, genomes): + # _, traj = job.get() + # genome.fitness = traj['tot_reward'] + + # set fitness value + reward = np.mean(reward) + genome.fitness = reward + rewards.append(reward) + + # save best reward and associated parameter/genome + if reward > self.best_reward: + self.best_reward = reward + self.best_parameters = genome + + # print info + if self.verbose: + print(' -- Genome id {} - reward: {}'.format(genome_id, reward)) + + # append rewards + self.avg_rewards.append(np.mean(rewards)) + self.max_rewards.append(np.max(rewards)) + + # print info + if self.verbose: + print('Episode {}: average reward = {} and best reward = {}'.format(self.episode, self.avg_rewards[-1], + self.max_rewards[-1])) + + def optimize(self): + pass + + def train(self, num_steps=1000, num_rollouts=1, num_episodes=1, verbose=False): + # set few variables + self.num_steps = num_steps + self.num_rollouts = num_rollouts + self.episode = 0 + self.verbose = verbose + self.avg_rewards, self.max_rewards = [], [] + + # run the algo for the specified number of generations / episodes + winner = self.population.run(self.explore_and_evaluate, num_episodes) + + # print best reward + if verbose: + print("\nBest reward found: {}".format(self.best_reward)) + + # set the best genome + self.policy.genome = winner + + # return the average rewards and max rewards per generation + return self.avg_rewards, self.max_rewards + + def test(self, num_steps=1000, dt=0, use_terminating_condition=False, render=True): + return self.task.run(num_steps=num_steps, dt=dt, use_terminating_condition=use_terminating_condition, + render=render) diff --git a/pyrobolearn/algos/power.py b/pyrobolearn/algos/power.py new file mode 100755 index 0000000..0aba55e --- /dev/null +++ b/pyrobolearn/algos/power.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python +"""Provide the PoWER reinforcement learning algorithm. + +This Policy learning by Weighting Exploration with the Returns (PoWER) algorithm is an model-free, on-policy, and +Expectation-Maximization (EM) algorithm. The exploration is carried out in the parameter space. +""" + +import numpy as np +import sys + +from pyrobolearn.envs import Env +from pyrobolearn.tasks import RLTask +# from rl_algo import EMRLAlgo + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class PoWER(object): # EMRLAlgo): + r"""PoWER: Policy learning by Weighting Exploration with the Returns + + Type:: this is a model-free, on-policy, episode-based, EM (Expectation-Maximization) algorithm. + + .. math:: TODO + + Properties: + * The learning model is linear with respect to the parameters (in order to compute the close form solution) + * Exploration is performed in the parameter space with a gaussian distribution over the parameters + Pros: + * Pros of using an EM algorithm (improvement on the lower bound) + Cons: + * Initialization of the EM algorithm is crucial; it works better when providing first few demonstrations + of the task to complete + * the rewards have to be strictly positives + * doesn't work with nonlinear models (in terms of the parameters) + + .. note:: + + * the reward has to be strictly positive in order to be a proper probability distribution (see [1]) + * this algorithm is popular and works well with Dynamic Movement Primitives + + The code is based on [2]. + + Pseudocode: + 1. Exploration: + 2. Evaluation: + 3. Update: + + Examples: + TODO + + References: + [1] "Policy Search for Motor Primitives in Robotics", Kober et al., 2010 + [2] Original Matlab code (Kober): http://www.ausy.tu-darmstadt.de/uploads/Member/JensKober/matlab_PoWER.zip + """ + + def __init__(self, task, policy, std_params=1., num_best_rollouts=10, num_workers=1): + """ + Initialize the PoWER algorithm. + + Args: + task (RLTask, Env): RL task/env to run + policy (Policy): specify the policy (model) to optimize + std_params (float): + """ + # create explorer + # create evaluator + # create updater + # super(PoWER, self).__init__(task, exploration_strategy, memory, hyperparameters) + + # set task + if isinstance(task, Env): + task = RLTask(task, policy) + if not isinstance(task, RLTask): + raise TypeError("Expecting task to be an instance of RLTask.") + self.task = task + + # set policy + self.policy = policy + if not self.policy.is_parametric(): + raise ValueError("The policy should be parametric") + if not self.policy.is_linear(): + raise ValueError("The policy should be linear with respect to the parameters") + + # set standard deviation of the parameters + self.std_params = std_params + + # set num best rollouts for memory + self.num_best_rollouts = num_best_rollouts + + # remember best parameters + self.best_reward = -np.infty + self.best_parameters = None + + ############## + # Properties # + ############## + + @property + def std_params(self): + return self._std_params + + @std_params.setter + def std_params(self, std_params): + if std_params < 0.: + std_params = 1. + self._std_params = std_params + + ########### + # Methods # + ########### + + def explore_and_evaluate(self, params, num_steps, num_rollouts): + # set policy parameters + self.policy.set_vectorized_parameters(params) + + # run a number of rollouts + reward = [] + for rollout in range(num_rollouts): + rew = self.task.run(num_steps=num_steps, use_terminating_condition=True, render=False) + if rew <= 0: + raise ValueError("With PoWER, the reward must be strictly positive.") + reward.append(rew) + reward = np.mean(reward) + + return reward + + # def train(self, num_episodes=1, num_steps=100, std_params=1., task=None, seed=None, debug=False): + def train(self, num_steps=1000, num_rollouts=1, num_episodes=1, verbose=False, seed=None): + # check parameters + if num_episodes < 1: + num_episodes = 1 + if num_steps < 1: + num_steps = 1 + if num_rollouts < 1: + num_rollouts = 1 + + # set random seed + if seed is not None: + np.random.seed(seed) + + # Initial policy parameters + num_params = self.policy.num_parameters + w = self.policy.get_vectorized_parameters() + + # define exploration parameters + mean = np.zeros(num_params) + C_init = np.ones(num_params) * np.sqrt(self.std_params) + C = C_init + + # Evaluate reward with current parameter + reward = self.explore_and_evaluate(w, num_steps=num_steps, num_rollouts=num_rollouts) + + # remember best set of weights and best reward + if self.best_reward < reward: + self.best_reward, self.best_parameters = reward, np.copy(w) + + # print info + if verbose: + print('Episode {}/{} with current and best reward: {}, {}'.format(0, num_episodes, reward, + self.best_reward)) + + # for each episode + rewards, memory = [reward], [] + memory = [] + for episode in range(num_episodes): + + # 1. Explore + # explore and sample + eps = np.random.multivariate_normal(mean, np.diag(C)) + weps = w + eps + reward = self.explore_and_evaluate(weps, num_steps=num_steps, num_rollouts=num_rollouts) + rewards.append(reward) + + # remember best set of weights and best reward + if self.best_reward < reward: + self.best_reward, self.best_parameters = reward, weps + + # print info + if verbose: + print('Episode {}/{} with current and best reward: {}, {}'.format(episode+1, num_episodes, reward, + self.best_reward)) + # record in memory + memory.append((reward, weps, C)) + + # 2. Evaluate + # Reweight: importance sampling + list.sort(memory, key=lambda x: x[0]) # in-place operation + memory = memory[-self.num_best_rollouts:] # just keep the best rollouts + + # 3. Update + # Update weight parameters + num, den = 0, 0 + for (rew, weps, Ceps) in memory[-self.num_best_rollouts:]: + # prec = np.linalg.inv(Ceps) + prec = np.linalg.inv(np.diag(Ceps)) + eps = weps - w + num += prec.dot(eps) * rew + den += prec * rew + w = w + np.linalg.inv(den + 1e-8).dot(num) + + # Update Covariance matrix + num, den = 0, 0 + for (rew, weps, Ceps) in memory[-self.num_best_rollouts:]: + eps = weps - w + # num += eps.dot(eps.T)*rew + num += (eps ** 2) * rew + den += rew + C = num / (den + 1e-10) + + # Apply an upper and lower limit to the exploration (so we still get a kind of exploration) + C = np.minimum(np.maximum(C, 0.1 * C_init), 10. * C_init) + + # print best reward + if verbose: + print("\nBest reward found: {}".format(self.best_reward)) + + # set the best parameters + self.policy.set_vectorized_parameters(self.best_parameters) + + return rewards + + def test(self, num_steps=1000, dt=0, use_terminating_condition=False, render=True): + return self.task.run(num_steps=num_steps, dt=dt, use_terminating_condition=use_terminating_condition, + render=render) diff --git a/pyrobolearn/models/nn/ae.py b/pyrobolearn/models/nn/ae.py index 55fd34c..55cbfc5 100644 --- a/pyrobolearn/models/nn/ae.py +++ b/pyrobolearn/models/nn/ae.py @@ -28,7 +28,7 @@ from dnn import NN __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/models/nn/cnn.py b/pyrobolearn/models/nn/cnn.py index e7bb442..12d264e 100644 --- a/pyrobolearn/models/nn/cnn.py +++ b/pyrobolearn/models/nn/cnn.py @@ -28,7 +28,7 @@ from dnn import NN __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/models/nn/dnn.py b/pyrobolearn/models/nn/dnn.py index e3e7c2f..7f25b67 100644 --- a/pyrobolearn/models/nn/dnn.py +++ b/pyrobolearn/models/nn/dnn.py @@ -27,7 +27,7 @@ import torch __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/models/nn/gan.py b/pyrobolearn/models/nn/gan.py index c3aeee6..b33f64a 100644 --- a/pyrobolearn/models/nn/gan.py +++ b/pyrobolearn/models/nn/gan.py @@ -30,7 +30,7 @@ from dnn import NN __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/models/nn/mlp.py b/pyrobolearn/models/nn/mlp.py index 439e86a..fdea93d 100644 --- a/pyrobolearn/models/nn/mlp.py +++ b/pyrobolearn/models/nn/mlp.py @@ -27,7 +27,7 @@ from dnn import NN, NNTorch __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/models/nn/neat_model.py b/pyrobolearn/models/nn/neat_model.py index 6fd4435..5959cd4 100644 --- a/pyrobolearn/models/nn/neat_model.py +++ b/pyrobolearn/models/nn/neat_model.py @@ -19,7 +19,7 @@ except ImportError as e: __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/models/nn/rcnn.py b/pyrobolearn/models/nn/rcnn.py index da54863..ab2914d 100644 --- a/pyrobolearn/models/nn/rcnn.py +++ b/pyrobolearn/models/nn/rcnn.py @@ -29,7 +29,7 @@ from dnn import NN __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/models/nn/rnn.py b/pyrobolearn/models/nn/rnn.py index e73e339..f65da33 100644 --- a/pyrobolearn/models/nn/rnn.py +++ b/pyrobolearn/models/nn/rnn.py @@ -28,7 +28,7 @@ from dnn import NN __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/models/nn/vae.py b/pyrobolearn/models/nn/vae.py index f9bde3c..0f14261 100644 --- a/pyrobolearn/models/nn/vae.py +++ b/pyrobolearn/models/nn/vae.py @@ -28,7 +28,7 @@ from dnn import NN __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/robots/actuators/actuator.py b/pyrobolearn/robots/actuators/actuator.py index 4f6406f..3576d64 100644 --- a/pyrobolearn/robots/actuators/actuator.py +++ b/pyrobolearn/robots/actuators/actuator.py @@ -10,7 +10,7 @@ simulation to reality. __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/robots/actuators/joints.py b/pyrobolearn/robots/actuators/joints.py index 18844d4..b47646f 100644 --- a/pyrobolearn/robots/actuators/joints.py +++ b/pyrobolearn/robots/actuators/joints.py @@ -9,7 +9,7 @@ from actuator import Actuator __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/robots/base.py b/pyrobolearn/robots/base.py index 3ecc77a..c2b1067 100644 --- a/pyrobolearn/robots/base.py +++ b/pyrobolearn/robots/base.py @@ -8,7 +8,7 @@ import quaternion __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/robots/sensors/camera.py b/pyrobolearn/robots/sensors/camera.py index 820705e..dfeb10f 100644 --- a/pyrobolearn/robots/sensors/camera.py +++ b/pyrobolearn/robots/sensors/camera.py @@ -11,7 +11,7 @@ from links import LinkSensor __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/robots/sensors/contact.py b/pyrobolearn/robots/sensors/contact.py index bb8ff4f..456ec89 100644 --- a/pyrobolearn/robots/sensors/contact.py +++ b/pyrobolearn/robots/sensors/contact.py @@ -7,7 +7,7 @@ from links import LinkSensor __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/robots/sensors/force_torque.py b/pyrobolearn/robots/sensors/force_torque.py index bed13ca..fdf80e4 100644 --- a/pyrobolearn/robots/sensors/force_torque.py +++ b/pyrobolearn/robots/sensors/force_torque.py @@ -9,7 +9,7 @@ from joints import JointSensor __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/robots/sensors/imu.py b/pyrobolearn/robots/sensors/imu.py index 8fa6826..f91aed2 100644 --- a/pyrobolearn/robots/sensors/imu.py +++ b/pyrobolearn/robots/sensors/imu.py @@ -11,7 +11,7 @@ from links import LinkSensor __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/robots/sensors/joints.py b/pyrobolearn/robots/sensors/joints.py index 8c9dd02..24e1ae5 100644 --- a/pyrobolearn/robots/sensors/joints.py +++ b/pyrobolearn/robots/sensors/joints.py @@ -11,7 +11,7 @@ from sensor import Sensor __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/robots/sensors/light.py b/pyrobolearn/robots/sensors/light.py index 58dcae8..de2f008 100644 --- a/pyrobolearn/robots/sensors/light.py +++ b/pyrobolearn/robots/sensors/light.py @@ -9,7 +9,7 @@ from links import LinkSensor __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/robots/sensors/links.py b/pyrobolearn/robots/sensors/links.py index f8a5480..2bb5b42 100644 --- a/pyrobolearn/robots/sensors/links.py +++ b/pyrobolearn/robots/sensors/links.py @@ -11,7 +11,7 @@ from sensor import Sensor __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/robots/sensors/misc.py b/pyrobolearn/robots/sensors/misc.py index 84fa7f7..77ea626 100644 --- a/pyrobolearn/robots/sensors/misc.py +++ b/pyrobolearn/robots/sensors/misc.py @@ -7,7 +7,7 @@ from links import Sensor, LinkSensor __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/robots/sensors/sensor.py b/pyrobolearn/robots/sensors/sensor.py index d16b1fc..88b434d 100644 --- a/pyrobolearn/robots/sensors/sensor.py +++ b/pyrobolearn/robots/sensors/sensor.py @@ -17,7 +17,7 @@ from pyrobolearn.utils.converter import QuaternionListConverter, NumpyListConver __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" __credits__ = ["Brian Delhaisse"] -__license__ = "(c) Brian Delhaisse" +__license__ = "MIT" __version__ = "1.0.0" __maintainer__ = "Brian Delhaisse" __email__ = "briandelhaisse@gmail.com" diff --git a/pyrobolearn/tools/README.md b/pyrobolearn/tools/README.md new file mode 100644 index 0000000..bc022ff --- /dev/null +++ b/pyrobolearn/tools/README.md @@ -0,0 +1,8 @@ +## Tools + +This folder the *interfaces* and the *bridges*. + +* The I/O *interfaces* get (or set) the data from (to) the hardware, process it, and store it inside the class. +* The *bridges* makes the connection between the interface and a component (such as the world or an element in that world such as a robot) in the framework. + +The separation between interfaces and bridges allows for better flexibility. For instance, a game controller interface allows us to get data from the hardware, process it, and store it inside the class. The bridge can then map the specific controller events to a robot. Moving a joystick up could mean to move a UAV robot up in the air, or move a wheeled robot forward. \ No newline at end of file diff --git a/pyrobolearn/tools/__init__.py b/pyrobolearn/tools/__init__.py new file mode 100644 index 0000000..bbde068 --- /dev/null +++ b/pyrobolearn/tools/__init__.py @@ -0,0 +1,6 @@ + +# import interfaces +# import interfaces + +# import bridges +# import bridges diff --git a/pyrobolearn/tools/bridges/__init__.py b/pyrobolearn/tools/bridges/__init__.py new file mode 100644 index 0000000..b1ed603 --- /dev/null +++ b/pyrobolearn/tools/bridges/__init__.py @@ -0,0 +1,19 @@ +# a bridge links an interface with something else + +# general bridge import +from bridge import Bridge + +# Bridge for mouse-keyboard interfaces +from mouse_keyboard import * + +# Bridge for audio interfaces +# from audio import * + +# Bridge for camera interfaces +# from camera import * + +# Bridge for controller interfaces +# from controllers import * + +# Bridge for VR interfaces +# from vr import * diff --git a/pyrobolearn/tools/bridges/audio/__init__.py b/pyrobolearn/tools/bridges/audio/__init__.py new file mode 100644 index 0000000..6ff5ddf --- /dev/null +++ b/pyrobolearn/tools/bridges/audio/__init__.py @@ -0,0 +1,8 @@ + +## Bridge for audio interfaces ## + +# Bridge between audio and robot +from robots import * + +# Bridge between audio and world +from world import * \ No newline at end of file diff --git a/pyrobolearn/tools/bridges/audio/robots/__init__.py b/pyrobolearn/tools/bridges/audio/robots/__init__.py new file mode 100644 index 0000000..33f9f0d --- /dev/null +++ b/pyrobolearn/tools/bridges/audio/robots/__init__.py @@ -0,0 +1,6 @@ + +# bridge between audio and wheeled robot +from bridge_speech_wheeled import * + +# bridge between audio and rototary wing UAV +from bridge_speech_rotatory_uav import * \ No newline at end of file diff --git a/pyrobolearn/tools/bridges/audio/robots/bridge_speech_rotatory_uav.py b/pyrobolearn/tools/bridges/audio/robots/bridge_speech_rotatory_uav.py new file mode 100644 index 0000000..bcad30e --- /dev/null +++ b/pyrobolearn/tools/bridges/audio/robots/bridge_speech_rotatory_uav.py @@ -0,0 +1,51 @@ +# Bridges between audio interface and rotatory wing robots + +from pyrobolearn.robots import RotaryWingUAV +from pyrobolearn.tools.interfaces.audio import SpeechRecognizerInterface +from pyrobolearn.tools.bridges.bridge import Bridge + + +class BridgeSpeechRecognizerRotatoryUAV(Bridge): + r"""Bridge Speech Wheeled Robot + + Bridge between the speech recognizer interface and a wheeled robot. You can give oral orders to the robot. + """ + + def __init__(self, interface, uav_robot, init_speed=1.): + if not isinstance(interface, SpeechRecognizerInterface): + raise TypeError("Expecting a speech recognizer interface") + if not isinstance(uav_robot, RotaryWingUAV): + raise TypeError("Expecting a wheeled robot") + super(BridgeSpeechRecognizerRotatoryUAV, self).__init__(interface) + self.robot = uav_robot + self.speed = init_speed + + def step(self): + data = self.interface.data + data = data.split() + # print('data: {}'.format(data)) + if data[0] == 'stop' or data[0] == 'stay': + self.robot.stop() + elif data[-1] == 'higher': + pass + elif data[-1] == 'lower': + pass + elif data[-1] == 'forward': + pass + elif data[-1] == 'backward': + pass + elif data == 'turn right': + pass + elif data == 'turn left': + pass + elif data[-1] == 'right': + pass + elif data[-1] == 'left': + pass + elif data[-1] == 'faster': + self.speed *= 2. + elif data[-1] == 'slower': + self.speed /= 2. + elif data: + pass + # print('I do not know the meaning of {}'.format(data)) diff --git a/pyrobolearn/tools/bridges/audio/robots/bridge_speech_wheeled.py b/pyrobolearn/tools/bridges/audio/robots/bridge_speech_wheeled.py new file mode 100644 index 0000000..3049dd7 --- /dev/null +++ b/pyrobolearn/tools/bridges/audio/robots/bridge_speech_wheeled.py @@ -0,0 +1,83 @@ +# Bridges between audio interface and wheeled robots + +import numpy as np + +from pyrobolearn.robots import WheeledRobot, AckermannWheeledRobot +from pyrobolearn.tools.interfaces.audio import SpeechRecognizerInterface +from pyrobolearn.tools.bridges.bridge import Bridge + + +class BridgeSpeechRecognizerWheeledRobot(Bridge): + r"""Bridge Speech Wheeled Robot + + Bridge between the speech recognizer interface and a wheeled robot. You can give oral orders to the robot. + """ + + def __init__(self, interface, wheeled_robot, init_speed=1.): + if not isinstance(interface, SpeechRecognizerInterface): + raise TypeError("Expecting a speech recognizer interface") + if not isinstance(wheeled_robot, WheeledRobot): + raise TypeError("Expecting a wheeled robot") + super(BridgeSpeechRecognizerWheeledRobot, self).__init__(interface) + self.robot = wheeled_robot + self.speed = init_speed + + def step(self): + data = self.interface.data + #print('data: {}'.format(data)) + if data == 'stop': + self.robot.stop() + elif data == 'move forward': + self.robot.driveForward(self.speed) + elif data == 'move backward': + self.robot.driveBackward(self.speed) + elif data == 'turn right': + pass + elif data == 'turn left': + pass + elif data == 'faster': + self.speed *= 2. + elif data == 'slower': + self.speed /= 2. + elif data: + pass + #print('I do not know the meaning of {}'.format(data)) + + +class BridgeSpeechRecognizerAckermannWheeledRobot(Bridge): + r"""Bridge Speech Ackermann Wheeled Robot + + Bridge between the speech recognizer interface and a wheeled robot (with ackermann steering). You can give oral + orders to the robot. + """ + + def __init__(self, interface, wheeled_robot, init_speed=1.): + if not isinstance(interface, SpeechRecognizerInterface): + raise TypeError("Expecting a speech recognizer interface") + if not isinstance(wheeled_robot, AckermannWheeledRobot): + raise TypeError("Expecting a wheeled robot of type Ackermann steering") + super(BridgeSpeechRecognizerAckermannWheeledRobot, self).__init__(interface) + self.robot = wheeled_robot + self.speed = init_speed + self.steering_angle = 0. + + def step(self): + data = self.interface.data + #print('data: {}'.format(data)) + if data == 'stop': + self.robot.stop() + elif data == 'move forward': + self.robot.driveForward(self.speed) + elif data == 'move backward': + self.robot.driveBackward(self.speed) + elif data == 'turn right': + self.robot.setSteering(np.deg2rad(-20)) + elif data == 'turn left': + self.robot.setSteering(np.deg2rad(20)) + elif data == 'faster': + self.speed *= 2. + elif data == 'slower': + self.speed /= 2. + elif data: + pass + #print('I do not know the meaning of {}'.format(data)) \ No newline at end of file diff --git a/pyrobolearn/tools/bridges/audio/world/__init__.py b/pyrobolearn/tools/bridges/audio/world/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyrobolearn/tools/bridges/bridge.py b/pyrobolearn/tools/bridges/bridge.py new file mode 100644 index 0000000..c8c1886 --- /dev/null +++ b/pyrobolearn/tools/bridges/bridge.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +"""Define the Bridge class. + +The Bridge class links an interface with something else. This can be the world (such as a robot, a camera, +a controller), a state / action, an environment, or a task. This is the main parent class from which any other +bridges inherit from. +""" + +from pyrobolearn.tools.interfaces import Interface + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class Bridge(object): + r"""Bridge class + + The Bridge class links an interface with something else in the world, such as a robot, a camera, a controller, etc. + This is the main parent class from which any other bridges inherit from. + + More specifically, the interface can be run in a separate thread or in the same thread than the main one. + The interface collects the input data/events and/or outputs the given data/events, but the interface does not + know how to connect to the rest of the code. The bridge is the one that possesses this knowledge + (see Bridge/Adapter design pattern). There can be multiple interfaces running (like for the audio and webcam), + and each interface can have multiple bridges. However, each bridge links only one specific interface to something + else. + + For instance, let's say we have an Xbox controller interface, and we want to control a quadcopter robot and + a wheeled robot. Everytime pushing the joystick forward results the interface to remember such event, but this + does not result in anything in the simulator. The associated bridge such as `BridgeXboxUAV` and + `BridgeUAVWheeled` knows what should be done if the joystick is moved forward. In the case of the quadcopter, + the bridge could for instance increase the speed of the propellers, and for the wheeled robot could result + to move this one forward. There could be other bridges linking the Xbox controller to other things in the world, + like the world camera, a visual object, etc. An interface can have multiple bridges, e.g. a bridge linking the + same Xbox controller to different cameras in the world. Pushing the joystick will result in the same movements + for all these cameras. + """ + + def __init__(self, interface, priority=None): + self.interface = interface + self.priority = priority + + ############## + # Properties # + ############## + + @property + def interface(self): + return self._interface + + @interface.setter + def interface(self, interface): + if not isinstance(interface, Interface): + raise TypeError("Expecting interface to be an instance of Interface, instead got {}".format(interface)) + self._interface = interface + + @property + def priority(self): + return self._priority + + @priority.setter + def priority(self, priority): + if priority is not None: + if not isinstance(priority, int): + raise TypeError("Expecting the priority to be an integer, instead got {}".format(priority)) + self._priority = priority + + ########### + # Methods # + ########### + + def step(self, update_interface=False): + """Main function that has to be overwritten by the user.""" + raise NotImplementedError + + def __call__(self, update_interface=False): + self.step(update_interface=update_interface) diff --git a/pyrobolearn/tools/bridges/camera/__init__.py b/pyrobolearn/tools/bridges/camera/__init__.py new file mode 100644 index 0000000..9082627 --- /dev/null +++ b/pyrobolearn/tools/bridges/camera/__init__.py @@ -0,0 +1,8 @@ + +## Bridge for camera interfaces ## + +# Bridge between camera and robot +from robots import * + +# Bridge between camera and world +from world import * \ No newline at end of file diff --git a/pyrobolearn/tools/bridges/camera/robots/__init__.py b/pyrobolearn/tools/bridges/camera/robots/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyrobolearn/tools/bridges/camera/world/__init__.py b/pyrobolearn/tools/bridges/camera/world/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyrobolearn/tools/bridges/controllers/__init__.py b/pyrobolearn/tools/bridges/controllers/__init__.py new file mode 100644 index 0000000..e7dae3b --- /dev/null +++ b/pyrobolearn/tools/bridges/controllers/__init__.py @@ -0,0 +1,8 @@ + +## Bridge for controller interfaces ## + +# Bridge between controller and robot +from robots import * + +# Bridge between controller and world +from world import * \ No newline at end of file diff --git a/pyrobolearn/tools/bridges/controllers/robots/__init__.py b/pyrobolearn/tools/bridges/controllers/robots/__init__.py new file mode 100644 index 0000000..771f249 --- /dev/null +++ b/pyrobolearn/tools/bridges/controllers/robots/__init__.py @@ -0,0 +1,3 @@ + +# bridge between controller and wheeled robot +from bridge_controller_wheeled import * diff --git a/pyrobolearn/tools/bridges/controllers/robots/bridge_controller_wheeled.py b/pyrobolearn/tools/bridges/controllers/robots/bridge_controller_wheeled.py new file mode 100644 index 0000000..826ad8f --- /dev/null +++ b/pyrobolearn/tools/bridges/controllers/robots/bridge_controller_wheeled.py @@ -0,0 +1,79 @@ + +# Bridges between controller interface and wheeled robots + +from pyrobolearn.robots import WheeledRobot, AckermannWheeledRobot +from pyrobolearn.tools.interfaces.controllers import XboxControllerInterface, XboxOneControllerInterface +from pyrobolearn.tools.bridges.bridge import Bridge + + +class BridgeXboxWheeledRobot(Bridge): + r"""Bridge Xbox Wheeled Robot + + Bridge between the Xbox controller interface and a wheeled robot. You can move the robot using the joystick. + """ + + def __init__(self, interface, wheeled_robot): + # quick checks + if not isinstance(interface, XboxControllerInterface): + raise TypeError("Expecting a speech recognizer interface") + if not isinstance(wheeled_robot, WheeledRobot): + raise TypeError("Expecting a wheeled robot") + + # call super class + super(BridgeXboxWheeledRobot, self).__init__(interface) + self.robot = wheeled_robot + self.speed = 1. + + def step(self): + x,y = self.interface.LJ + + +class BridgeXboxOneWheeledRobot(Bridge): + r"""Bridge Xbox Wheeled Robot + + Bridge between the Xbox One controller interface and a wheeled robot. You can move the robot using the joystick. + """ + + def __init__(self, interface, wheeled_robot): + # quick check + if not isinstance(interface, XboxOneControllerInterface): + raise TypeError("Expecting a speech recognizer interface") + if not isinstance(wheeled_robot, WheeledRobot): + raise TypeError("Expecting a wheeled robot") + + super(BridgeXboxOneWheeledRobot, self).__init__(interface) + self.robot = wheeled_robot + self.speed = 1. + + def step(self): + x,y = self.interface.LJ + + +class BridgeXboxOneAckermannWheeledRobot(Bridge): + r"""Bridge Xbox One Ackermann Wheeled Robot + + Bridge between the Xbox One controller interface and a wheeled robot. You can move the robot using the joystick. + """ + + def __init__(self, interface, wheeled_robot): + if not isinstance(interface, XboxOneControllerInterface): + raise TypeError("Expecting a speech recognizer interface") + if not isinstance(wheeled_robot, AckermannWheeledRobot): + raise TypeError("Expecting a wheeled robot") + super(BridgeXboxOneAckermannWheeledRobot, self).__init__(interface) + self.robot = wheeled_robot + self.speed = 1. + + def step(self): + x,y = self.interface.LJ + self.robot.setSteering(-x / 2.) + self.robot.driveForward(y * self.speed) + + if self.interface.A: + print('increasing speed +1') + self.speed += 1. + if self.interface.B: + print('decreasing speed -1') + self.speed -= 1. + if self.speed < 1.: + self.speed = 1. diff --git a/pyrobolearn/tools/bridges/controllers/world/__init__.py b/pyrobolearn/tools/bridges/controllers/world/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyrobolearn/tools/bridges/mouse_keyboard/__init__.py b/pyrobolearn/tools/bridges/mouse_keyboard/__init__.py new file mode 100644 index 0000000..475444b --- /dev/null +++ b/pyrobolearn/tools/bridges/mouse_keyboard/__init__.py @@ -0,0 +1,4 @@ + +# import +from bridge_mousekeyboard_world import BridgeMouseKeyboardWorld +from bridge_mousekeyboard_imitation_task import BridgeMouseKeyboardImitationTask diff --git a/pyrobolearn/tools/bridges/mouse_keyboard/bridge_mousekeyboard_imitation_task.py b/pyrobolearn/tools/bridges/mouse_keyboard/bridge_mousekeyboard_imitation_task.py new file mode 100644 index 0000000..730cee3 --- /dev/null +++ b/pyrobolearn/tools/bridges/mouse_keyboard/bridge_mousekeyboard_imitation_task.py @@ -0,0 +1,375 @@ +#!/usr/bin/env python +"""Define the Bridge between the mouse-keyboard interface and the world. + +Dependencies: +- `pyrobolearn.utils` +- `pyrobolearn.worlds` or `pyrobolearn.envs` +- `pyrobolearn.tools.interfaces.MouseKeyboardInterface` +- `pyrobolearn.tools.bridges.Bridge` +""" + +from pyrobolearn.utils.bullet_utils import RGBColor, Key + +from pyrobolearn.simulators import Simulator +from pyrobolearn.worlds import World +from pyrobolearn.envs import Env + +from pyrobolearn.tools.interfaces import MouseKeyboardInterface +# from pyrobolearn.tools.bridges import Bridge +from bridge_mousekeyboard_world import BridgeMouseKeyboardWorld +# from pyrobolearn.tasks.imitation_task import ILTask # Warning: circular dependency + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class BridgeMouseKeyboardImitationTask(BridgeMouseKeyboardWorld): # Bridge): + r"""Bridge Mouse-Keyboard Imitation Task + + Bridge between the mouse-keyboard and the world. + + Mouse: + * predefined in pybullet + * `scroll wheel`: zoom + * `ctrl`/`alt` + `scroll button`: move the camera using the mouse + * `ctrl`/`alt` + `left-click`: rotate the camera using the mouse + * `left-click` and drag: transport the object + * `left-click`: select/unselect an object (show bounding box, and print name/id in simulator) + * `right-click` and drag: perform IK on the selected link until the mouse button is released + * `ctrl` + `left-click`: select multiple object + + Keyboard: + * predefined in pybullet: + * `w`: show the wireframe (collision shapes) + * `s`: show the reference system + * `v`: show bounding boxes + * `g`: show/hide parts of the GUI the side columns (check `sim.configureDebugVisualizer(p.COV_ENABLE_GUI, 0)`) + * `esc`: quit the simulator + * `x`: change camera view such that it is perpendicular to the x-axis + * `y`: change camera view such that it is perpendicular to the y-axis + * `z`: change camera view such that it is perpendicular to the z-axis + * `j`: after selecting a robot, display the joint sliders to control them + * `t`: after selecting a robot link, display the cartesian sliders to control it using IK + * `b`: after selecting a link of the robot, display the bounding box around the selected link + * `d`: display what the robot sees (rgbd image) + * `u`: unselect any objects + * `r`: start/stop recording using the given recorder (if multiple recorders, select in the parameter column). + The recorders are the ones who know what/how to record and how to deal with the data. For instance, + if a policy needs to be trained, you have to interact with the recorder and not the interface. + * `f`: save recorded trajectory to file (append) + * `c`: clean and reset the given world + * `a`: apply force/torque on the selected link/joint using sliders (select the link/joint using the mouse) + * `h`: hide/show the complete GUI; enable/disable the rendering + * `m`: show/hide the workspace of the corresponding link (not implemented yet) + * `p`: show/hide recorded trajectories + * `shift`+`p`: remove recorded trajectories + * `o`: optimized the given policy to the demonstrated trajectories (if multiple policy, select one policy) + * `e`: test the optimized policy associated to the selected robot (if multiple policy, select one policy) + * `l`: change mode of locomotion (defined in the robot; select the controller/policy) + * ``: select and perform corresponding action (10 actions possible, as between 0 and 9) + * `top arrow`: move the selected robot forward + * `bottom arrow`: move the selected robot backward + * `left arrow`: turn the selected robot to the left + * `right arrow`: turn the selected robot to the right + * `space`: allow the robot to jump (if implemented) + """ + + def __init__(self, world, interface=None, imitation_task=None, priority=None, verbose=False): + """ + Initialize the Bridge between a Mouse-Keyboard interface and an imitation learning task. + + Args: + interface (MouseKeyboardInterface, Env, World): mouse keyboard interface. + If the interface is an instance of Env, World, or Simulator, it will create automatically + a mouse-keyboard interface. + priority (int): priority of the bridge. + verbose (bool): If True, print information on the standard output. + """ + # check interface + if not isinstance(interface, MouseKeyboardInterface): + if interface is None: + sim = world.simulator + elif isinstance(interface, Env): + sim = interface.simulator + elif isinstance(interface, World): + sim = interface.simulator + elif isinstance(interface, Simulator): + sim = interface + else: + raise TypeError("Expecting interface to be an instance of MouseKeyboardInterface, or World, " + "got instead {}".format(type(interface))) + interface = MouseKeyboardInterface(sim) + + # call superclass + super(BridgeMouseKeyboardImitationTask, self).__init__(world, interface, priority, verbose=verbose) + + # set task + self.task = imitation_task + + # define few variables + self.pausing = False + self.recording = False + self.training = False + self.testing = False + self.visual_points = [] # {} + # self.display_trajectories = True + + self.events_fn = {(Key.x,): self.change_camera_view_x, + (Key.y,): self.change_camera_view_y, + (Key.z,): self.change_camera_view_z, + (Key.enter,): self.reset_camera_view, + (Key.space,): self.pause, # pause the simulator + (Key.h,): self.gui, # hide/enable (actually stop/play) GUI + (Key.r,): self.reset_world, # reset world + (Key.j,): self.update_joint_sliders, # add/remove joint (space) sliders + (Key.t,): self.update_task_sliders, # add/remove task (space) sliders + (Key.p,): self.display_trajectories, # display/hide trajectories + (Key.ctrl, Key.p): self.reset_visual_points, # remove trajectories + (Key.u,): self.unselect, # unselect robot/link + (Key.ctrl, Key.r): self.record, # record + (Key.ctrl, Key.n): self.add_data_row_in_recorder, # add a new data row in the recorder's + # data "matrix" + (Key.shift, Key.r): self.end_recording, # end recording + (Key.ctrl, Key.s): self.save_recording, # save what has been recorded into a file + (Key.ctrl, Key.o): self.train, # optimize/train the policy + (Key.shift, Key.o): self.end_training, # end training + (Key.ctrl, Key.t): self.test, # test the policy + (Key.shift, Key.t): self.end_testing, # end testing + (Key.shift, Key.space): self.end_task, # end task + (Key.d,): lambda: None, # display robot's camera + (Key.m,): lambda: None, # display link workspace + (Key.b,): lambda: None, # bounding box + (Key.a,): lambda: None, # apply force/torque on selected link/joint + (Key.l,): lambda: None, + (Key.n0,): lambda: None, + (Key.n1,): lambda: None, + (Key.n2,): lambda: None, + (Key.n3,): lambda: None, + (Key.n4,): lambda: None, + (Key.n5,): lambda: None, + (Key.n6,): lambda: None, + (Key.n7,): lambda: None, + (Key.n8,): lambda: None, + (Key.n9,): lambda: None, + (Key.top_arrow,): lambda: None, + (Key.bottom_arrow,): lambda: None, + (Key.left_arrow,): lambda: None, + (Key.right_arrow,): lambda: None} + + # define few variables + self.enable_training = False + self.enable_recording = False + self.enable_testing = False + + # self.vs = sim.createVisualShape(sim.GEOM_SPHERE, radius=0.02, rgbaColor=(0, 0, 1, 1)) + # self.vs1 = sim.createVisualShape(sim.GEOM_SPHERE, radius=0.2, rgbaColor=(1, 0, 0, 1)) + # self.vs2 = sim.createVisualShape(sim.GEOM_SPHERE, radius=0.2, rgbaColor=(0, 1, 0, 1)) + + ############## + # Properties # + ############## + + @property + def pausing(self): + """Return True if we need to pause the simulator.""" + return self._pausing + + @pausing.setter + def pausing(self, boolean): + """Specify if we should pause the simulator.""" + self._pausing = boolean + + @property + def recording(self): + """Return True if we are in recording mode.""" + return self._recording + + @recording.setter + def recording(self, boolean): + """Set recording mode.""" + self._recording = boolean + + # if you are recording, you can not train or test + if self._recording: + self._training = False + self._testing = False + + # notify the task + if self.task is not None: + self.task.recording_enabled = boolean + + @property + def training(self): + """Return True if we are in training mode.""" + return self._training + + @training.setter + def training(self, boolean): + """Set training mode.""" + self._training = boolean + + # if you are training, you can not record or test + if self._training: + self._recording = False + self._testing = False + + # notify the task + if self.task is not None: + self.task.training_enabled = boolean + + @property + def testing(self): + """Return True if we are in testing mode.""" + return self._testing + + @testing.setter + def testing(self, boolean): + """Set testing mode.""" + self._testing = boolean + + # if you are testing, you can not record or train + if self._testing: + self._recording = False + self._training = False + + # notify the task + if self.task is not None: + self.task.testing_enabled = boolean + + @property + def task(self): + """Return the task instance.""" + return self._task + + @task.setter + def task(self, task): + """Set the task.""" + from pyrobolearn.tasks.imitation import ILTask # to avoid circular dependency + if task is not None and not isinstance(task, ILTask): + raise("Expecting the imitation task to be an instance of ILTask, instead got {}".format(type(task))) + self._task = task + + @property + def environment(self): + """Return the environment instance.""" + return self.task.environment + + # @property + # def world(self): + # """Return the world instance.""" + # return self.task.world + + # @property + # def simulator(self): + # """Return the simulator instance.""" + # return self.task.simulator + + @property + def recorders(self): + """Return the recorder.""" + return self.task.recorders + + ########### + # Methods # + ########### + + def print_debug(self, str1, str2='', condition=True): + if self.debug: + if condition: + print("MouseKeyboardInterface: "+str1) + else: + print("MouseKeyboardInterface: "+str2) + + # def step(self, update_interface=False): + # """Perform a step: map the mouse-keyboard interface to the imitation task""" + # # perform a step with the interface + # if update_interface: + # self.interface.step() + # + # # # record (if specified) + # # if self.recording: + # # for recorder in self.recorders: + # # recorder.record() + + def pause(self): + """pause the simulation""" + self.pausing = not self.pausing + self.print_debug('pause the simulator', 'unpause the simulator', self.pausing) + + def record(self): + """Start/Stop recording""" + # if self.recorders is not None: + self.recording = not self.recording + self.print_debug('start recording', 'stop recording', self.recording) + + def add_data_row_in_recorder(self): + """Add a new data row in the recorder's data 'matrix'.""" + if self.task is not None: + self.task.add_data_row_in_recorder() + + def save_recording(self): + """save recording into file""" + if self.task is not None: + self.print_debug('saving what has been recorded') + self.task.save_recorders() + + def reset_recorder(self): + """Reset the recorders.""" + if self.task is not None: + self.print_debug('reset recorders') + self.task.reset_recorders() + + def end_recording(self): + """End recording.""" + if self.task is not None: + self.task.end_recording = True + + def train(self): + """train the policies from the task""" + self.print_debug('train the task') + self.training = not self.training + + def end_training(self): + """End training.""" + if self.task is not None: + self.task.end_training = True + + def test(self): + """test the policies from the task""" + self.print_debug('test the task') + self.testing = not self.testing + + def end_testing(self): + """End testing.""" + if self.task is not None: + self.task.end_testing = True + + def end_task(self): + """End task.""" + if self.task is not None: + self.task.end_task = True + + def display_trajectories(self): + # if self.display_trajectories: # hide them (pybullet doesn't allow that for now, just remove body) + # for key in self.visual_points: + # self.sim.removeBody(self.visual_points[key]) + # else: + # for key in self.visual_points: + # bodyId = self.sim.createMultiBody(baseMass=0, baseVisualShapeIndex=self.vs, + # basePosition=key) + # self.visual_points[key] = bodyId + # self.display_trajectories = not self.display_trajectories + for i in range(len(self.visual_points[:-1])): + self.simulator.addUserDebugLine(self.visual_points[i], self.visual_points[i + 1], RGBColor.red, 1., 2.) + + def reset_visual_points(self): + # for key in self.visual_points: + # self.sim.removeBody(self.visual_points[key]) + # self.visual_points.clear() + self.visual_points = [] diff --git a/pyrobolearn/tools/bridges/mouse_keyboard/bridge_mousekeyboard_world.py b/pyrobolearn/tools/bridges/mouse_keyboard/bridge_mousekeyboard_world.py new file mode 100644 index 0000000..1721263 --- /dev/null +++ b/pyrobolearn/tools/bridges/mouse_keyboard/bridge_mousekeyboard_world.py @@ -0,0 +1,437 @@ +#!/usr/bin/env python +"""Define the Bridge between the mouse-keyboard interface and the world. + +Dependencies: +- `pyrobolearn.utils` +- `pyrobolearn.worlds` or `pyrobolearn.envs` +- `pyrobolearn.tools.interfaces.MouseKeyboardInterface` +- `pyrobolearn.tools.bridges.Bridge` +""" + +import numpy as np + +from pyrobolearn.utils.bullet_utils import RGBColor, Key +from pyrobolearn.utils.math_utils import Plane + +from pyrobolearn.worlds import World, WorldCamera +from pyrobolearn.envs import Env +from pyrobolearn.tools.interfaces import MouseKeyboardInterface +from pyrobolearn.tools.bridges import Bridge + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class BridgeMouseKeyboardWorld(Bridge): + r"""Bridge Mouse-Keyboard World + + Bridge between the mouse-keyboard and the world. + + Mouse: + * predefined in pybullet + * `scroll wheel`: zoom + * `ctrl`/`alt` + `scroll button`: move the camera using the mouse + * `ctrl`/`alt` + `left-click`: rotate the camera using the mouse + * `left-click` and drag: transport the object + * `left-click`: select/unselect an object (show bounding box, and print name/id in simulator) + * `right-click` and drag: perform IK on the selected link until the mouse button is released + * `ctrl` + `left-click`: select multiple object + + Keyboard: + * predefined in pybullet: + * `w`: show the wireframe (collision shapes) + * `s`: show the reference system + * `v`: show bounding boxes + * `g`: show/hide parts of the GUI the side columns (check `sim.configureDebugVisualizer(p.COV_ENABLE_GUI, 0)`) + * `esc`: quit the simulator + * `x`: change camera view such that it is perpendicular to the x-axis + * `y`: change camera view such that it is perpendicular to the y-axis + * `z`: change camera view such that it is perpendicular to the z-axis + * `j`: after selecting a robot, display the joint sliders to control them + * `t`: after selecting a robot link, display the cartesian sliders to control it using IK + * `b`: after selecting a link of the robot, display the bounding box around the selected link + * `d`: display what the robot sees (rgbd image) + * `u`: unselect any objects + * `c`: clean and reset the given world + * `a`: apply force/torque on the selected link/joint using sliders (select the link/joint using the mouse) + * `h`: hide/show the complete GUI; enable/disable the rendering + * `m`: show/hide the workspace of the corresponding link (not implemented yet) + * `l`: change mode of locomotion (defined in the robot; select the controller/policy) + * ``: select and perform corresponding action (10 actions possible, as between 0 and 9) + * `top arrow`: move the selected robot forward + * `bottom arrow`: move the selected robot backward + * `left arrow`: turn the selected robot to the left + * `right arrow`: turn the selected robot to the right + * `space`: allow the robot to jump (if implemented) + """ + + def __init__(self, world, interface=None, priority=None, verbose=False): + """ + Initialize the Bridge between a Mouse-Keyboard interface and the world. + + Args: + world (World): world instance. + interface (None, MouseKeyboardInterface): mouse keyboard interface. If None, it will create one. + priority (int): priority of the bridge. + verbose (bool): If True, print information on the standard output. + """ + # set world + self.world = world + + # check interface + if not isinstance(interface, MouseKeyboardInterface): + interface = MouseKeyboardInterface(self.simulator) + + # call superclass + super(BridgeMouseKeyboardWorld, self).__init__(interface, priority) + + # camera + self.world_camera = WorldCamera(self.simulator) + self._camera = None + self.default_camera = None + self.plane, self.depth = None, None + self.bounding_box = False + + self.robot, self.link_id = None, None + self.joint_sliders, self.task_sliders = False, {} + self.hiding_gui = False + self.pausing = False + self.debug = verbose + + self.events_fn = {(Key.x,): self.change_camera_view_x, + (Key.y,): self.change_camera_view_y, + (Key.z,): self.change_camera_view_z, + (Key.enter,): self.reset_camera_view, + (Key.space,): self.pause, # pause the simulator + (Key.h,): self.gui, # hide/enable (actually stop/play) GUI + (Key.r,): self.reset_world, # reset world + (Key.j,): self.update_joint_sliders, # add/remove joint (space) sliders + (Key.t,): self.update_task_sliders, # add/remove task (space) sliders + (Key.p,): lambda: None, + (Key.u,): self.unselect, # unselect robot/link + (Key.d,): lambda: None, # display robot's camera + (Key.m,): lambda: None, # display link workspace + (Key.b,): lambda: None, # bounding box + (Key.a,): lambda: None, # apply force/torque on selected link/joint + (Key.l,): lambda: None, + (Key.n0,): lambda: None, + (Key.n1,): lambda: None, + (Key.n2,): lambda: None, + (Key.n3,): lambda: None, + (Key.n4,): lambda: None, + (Key.n5,): lambda: None, + (Key.n6,): lambda: None, + (Key.n7,): lambda: None, + (Key.n8,): lambda: None, + (Key.n9,): lambda: None, + (Key.top_arrow,): lambda: None, + (Key.bottom_arrow,): lambda: None, + (Key.left_arrow,): lambda: None, + (Key.right_arrow,): lambda: None} + + # self.vs = self.simulator.createVisualShape(self.simulator.GEOM_SPHERE, radius=0.02, rgbaColor=(0, 0, 1, 1)) + # self.vs1 = self.simulator.createVisualShape(self.simulator.GEOM_SPHERE, radius=0.2, rgbaColor=(1, 0, 0, 1)) + # self.vs2 = self.simulator.createVisualShape(self.simulator.GEOM_SPHERE, radius=0.2, rgbaColor=(0, 1, 0, 1)) + + ############## + # Properties # + ############## + + @property + def world(self): + """Return the world instance.""" + return self._world + + @world.setter + def world(self, world): + """Set the world instance.""" + if isinstance(world, Env): + world = world.world + if not isinstance(world, World): + raise TypeError("Expecting the world to be an instance of World or Env, instead got {}".format(type(world))) + self._world = world + + @property + def simulator(self): + """Return the simulator instance.""" + return self.world.simulator + + @property + def camera(self): + if self._camera is None: + self._camera = self.world_camera.getDebugVisualizerCamera(convert=False) + if self.default_camera is None: + self.default_camera = self._camera + return self._camera + + ########### + # Methods # + ########### + + def print_debug(self, str1, str2='', condition=True): + if self.debug: + if condition: + print("MouseKeyboardInterface: "+str1) + else: + print("MouseKeyboardInterface: "+str2) + + def step(self, update_interface=False): + """Perform a step: map the mouse-keyboard interface to the world""" + # update interface + if update_interface: + self.interface() + + # check keyboard events + self.check_key_events() + + # check mouse events and map to + self.check_mouse_events() + + # update joint sliders if present (position control) + if self.joint_sliders: + self.robot.updateJointSlider() + + # update task sliders if present (IK) + if self.task_sliders: + pass + + # reset camera (the scrolling event is not detected) + self._camera = None + + # step in the world + if not self.pausing: + self.world.step() + + def change_camera_view_x(self): + """Change camera view X.""" + self.print_debug('change camera view X') + dist, target = self.camera[-2:] + self.simulator.resetDebugVisualizerCamera(cameraDistance=dist, cameraYaw=90., cameraPitch=0., + cameraTargetPosition=target) + + def change_camera_view_y(self): + """Change camera view Y.""" + self.print_debug('change camera view Y') + dist, target = self.camera[-2:] + self.simulator.resetDebugVisualizerCamera(cameraDistance=dist, cameraYaw=180., cameraPitch=0., + cameraTargetPosition=target) + + def change_camera_view_z(self): + """Change camera view Z.""" + self.print_debug('change camera view Z') + dist, target = self.camera[-2:] + self.simulator.resetDebugVisualizerCamera(cameraDistance=dist, cameraYaw=-90., cameraPitch=-89.99, + cameraTargetPosition=target) + + def reset_camera_view(self): + """Reset camera view.""" + self.print_debug('reset camera view') + if self.default_camera is not None: + yaw, pitch, dist, target = self.default_camera[-4:] + self.simulator.resetDebugVisualizerCamera(cameraDistance=dist, cameraYaw=yaw, cameraPitch=pitch, + cameraTargetPosition=target) + self.default_camera = None + + def pause(self): + """Pause the simulation.""" + self.pausing = not self.pausing + self.print_debug('pause the simulator', 'unpause the simulator', self.pausing) + + def gui(self): + """show/hide GUI.""" + self.hiding_gui = not self.hiding_gui + self.simulator.configureDebugVisualizer(self.simulator.COV_ENABLE_GUI, self.hiding_gui) + self.print_debug('hide the GUI', 'enable the GUI', self.hiding_gui) + + def reset_world(self): + """Reset the world.""" + self.print_debug('reset the world') + self.world.resetRobots() + self.simulator.removeAllUserDebugItems() + + def update_joint_sliders(self): + """Update joint sliders.""" + if self.robot is not None: + if self.joint_sliders: # remove joint sliders + self.robot.removeJointSlider() + self.print_debug('remove joint sliders') + else: # add joint sliders + self.robot.addJointSlider() + self.print_debug('add joint sliders') + self.joint_sliders = not self.joint_sliders + + def update_task_sliders(self): + """Update task sliders.""" + if self.robot is not None and self.link_id is not None: + if self.link_id in self.task_sliders: # remove task sliders + for idx in self.task_sliders[self.link_id]: + self.simulator.removeUserDebugItem(self.task_sliders[self.link_id][idx]) + self.task_sliders.pop(self.link_id) + self.print_debug('remove task sliders') + else: # add task sliders + self.task_sliders[self.link_id] = {} + pos = self.robot.getLinkWorldPositions(self.link_id) + for i, name in zip(pos, ['x', 'y', 'z']): + slider = self.simulator.addUserDebugParameter(name, i - 2., i + 2., i) + self.task_sliders[self.link_id][name] = slider + self.print_debug('add task sliders') + + def unselect(self): + """Unselect the robot and link id.""" + self.print_debug('unselect robot/link') + self.robot, self.link_id = None, None + + def add_world_text(self, string, position, color=(0.,0.,0.), size=1., lifetime=0.): + """Add world text.""" + self.print_debug('add world text') + self.simulator.addUserDebugText(string, position, color, size, lifetime) + + def add_screen_text(self, string, world_position, color=(0.,0.,0.), size=1., lifetime=0.): + """Add screen text.""" + self.print_debug('add screen text') + V, P, Vp, V_inv, P_inv, Vp_inv = self.world_camera.getMatrices(True) + position = self.world_camera.screenToWorld(world_position, Vp_inv, P_inv, V_inv)[:3] + self.simulator.addUserDebugText(string, position, color, size, lifetime) + + def check_key_events(self): + # call function corresponding to key combination + if self.interface.key_pressed: + key = tuple(self.interface.key_pressed) + if key in self.events_fn: + self.events_fn[key]() + + def check_mouse_events(self): + # check if the mouse has been released + if not self.interface.mouse_down: + self.plane, self.depth = None, None + + # check what object we are trying to grab with the mouse by checking collision + if self.interface.mouse_pressed: + V, P, Vp, V_inv, P_inv, Vp_inv = self.world_camera.getMatrices(True) + camera = self.world_camera.getDebugVisualizerCamera(convert=False) + + # from the point (x,y) on the screen, get nearest and farthest point on the screen + x_screen_init = np.array([self.interface.mouse_x, self.interface.mouse_y, 1., 1.]) + x_screen_final = np.array([self.interface.mouse_x, self.interface.mouse_y, 0., 1.]) + + # get the corresponding points in the world + x_world_init = self.world_camera.screenToWorld(x_screen_init, Vp_inv, P_inv, V_inv) + x_world_final = self.world_camera.screenToWorld(x_screen_final, Vp_inv, P_inv, V_inv) + + # check if there is a collision + # print(x_world_init[:3], x_world_final[:3]) + collision = self.simulator.rayTest(list(x_world_init[:3]), list(x_world_final[:3])) + + # if collision, proceed the inverse operation to get the depth on the screen + if len(collision) > 0: + object_id, link_id, hit_frac, hit_pos, hit_normal = collision[0] + # self.simulator.addUserDebugLine(list(x_world_init[:3]), list(x_world_final[:3]), (0, 0, 1)) + # bodyId = self.simulator.createMultiBody(baseMass=0, baseVisualShapeIndex=self.vs1, + # basePosition=list(x_world_init[:3])) + # bodyId = self.simulator.createMultiBody(baseMass=0, baseVisualShapeIndex=self.vs2, + # basePosition=list(x_world_final[:3])) + if object_id != -1 and self.world.isRobotId(object_id): # valid object + + # Set robot and link_id + self.robot, self.link_id = self.world.getRobot(object_id), link_id + + width, height = camera[:2] + x_screen = np.array([width/2, height/10, 0.95, 1]) + pos = self.world_camera.screenToWorld(x_screen, Vp_inv, P_inv, V_inv)[:3] + # self.simulator.addUserDebugText(str(self.robot) + ": " + self.robot.getLinkNames(self.link_id), + # pos, RGBColor.black, textSize=1) + + # calculate plane + # 1. compute the initial point on the plane (collision point) + if link_id == -1: # no link + x0 = np.array(self.simulator.getBasePositionAndOrientation(object_id)[0]) + else: # link + x0 = np.array(self.simulator.getLinkState(object_id, link_id)[0]) + + # 2. calculate normal (=targetPosition - eyePosition) to the plane + # normal = x_world_final[:3] - x_world_init[:3] + yaw, pitch, dist, target = camera[-4:] + yaw, pitch = np.deg2rad(yaw), np.deg2rad(pitch) + normal = dist * np.array([np.cos(pitch) * np.sin(yaw), + -np.cos(pitch) * np.cos(yaw), + -np.sin(pitch)]) + + # 3. create plane + self.plane = Plane(x0, normal) + + # calculate associate depth on the screen (because perspective projection) + hit_pos = np.array(list(hit_pos) + [1.]) + self.depth = self.world_camera.worldToScreen(hit_pos, V, P, Vp)[2] + + elif self.interface.mouse_down and self.interface.mouse_moving and self.robot is not None: + V, P, Vp, V_inv, P_inv, Vp_inv = self.world_camera.getMatrices(True) + camera = self.world_camera.getDebugVisualizerCamera(convert=False) + + if self.plane is not None: + # project the point on the screen to the world, and check where the line that starts from this point + # and is perpendicular to the plane (i.e. parallel to the normal) intersects with the aforementioned + # plane + x_screen = np.array([self.interface.mouse_x, self.interface.mouse_y, self.depth, 1]) + x_world = self.world_camera.screenToWorld(x_screen, Vp_inv, P_inv, V_inv)[:3] + point = self.plane.getIntersectionPoint(x_world) + + # # draw some spheres on the plane + # if self.display_trajectories: + # bodyId = self.simulator.createMultiBody(baseMass=0, baseVisualShapeIndex=self.vs, + # basePosition=point) + # self.visual_points[tuple(point)] = bodyId + # else: + # self.visual_points[tuple(point)] = None + + # # draw trajectories + # if self.display_trajectories: + # self.visual_points.append(point) + # if len(self.visual_points) > 1: + # self.simulator.addUserDebugLine(self.visual_points[-2], self.visual_points[-1], + # RGBColor.red, 1., 2.) + + # # perform inverse kinematics + # q = self.robot.calculateInverseKinematics(self.link, point) + # for i in range(self.robot.getNumberOfJoints()): + # self.robot.setJointPositions(i, q[i]) + + +# Tests +if __name__ == '__main__': + from pyrobolearn.simulators import BulletSim + from pyrobolearn.worlds import BasicWorld + import time + from itertools import count + + # create simulator + sim = BulletSim() + + # sim.configureDebugVisualizer(p.COV_ENABLE_GUI, 0) + # sim.configureDebugVisualizer(p.COV_ENABLE_RENDERING, 0) + # sim.configureDebugVisualizer(p.COV_ENABLE_TINY_RENDERER, 1) + # sim.configureDebugVisualizer(p.COV_ENABLE_WIREFRAME, 1) + # sim.configureDebugVisualizer(p.COV_ENABLE_Y_AXIS_UP, 0) + # sim.configureDebugVisualizer(p.COV_ENABLE_RGB_BUFFER_PREVIEW, 0) + # sim.configureDebugVisualizer(p.COV_ENABLE_DEPTH_BUFFER_PREVIEW, 0) + # sim.configureDebugVisualizer(p.COV_ENABLE_SEGMENTATION_MARK_PREVIEW, 0) + + # create World + world = BasicWorld(sim) + + # load robot + robot = world.loadRobot('baxter', useFixedBase=True) + + # create bridge/interface + bridge = BridgeMouseKeyboardWorld(world, verbose=True) + + for _ in count(): + bridge.step(update_interface=True) + # world.step() + time.sleep(1. / 100) diff --git a/pyrobolearn/tools/bridges/vr/__init__.py b/pyrobolearn/tools/bridges/vr/__init__.py new file mode 100644 index 0000000..0040712 --- /dev/null +++ b/pyrobolearn/tools/bridges/vr/__init__.py @@ -0,0 +1,8 @@ + +## Bridge for VR interfaces ## + +# Bridge between VR and robot +from robots import * + +# Bridge between VR and world +from world import * \ No newline at end of file diff --git a/pyrobolearn/tools/bridges/vr/robots/__init__.py b/pyrobolearn/tools/bridges/vr/robots/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyrobolearn/tools/bridges/vr/world/__init__.py b/pyrobolearn/tools/bridges/vr/world/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyrobolearn/tools/interfaces/__init__.py b/pyrobolearn/tools/interfaces/__init__.py new file mode 100644 index 0000000..15fb30f --- /dev/null +++ b/pyrobolearn/tools/interfaces/__init__.py @@ -0,0 +1,30 @@ + +# import the interfaces +from interface import * + +# mouse-keyboard interface +from mouse_keyboard import * + +# audio +# from audio import * + +# camera +# from camera import * + +# controllers +# from controllers import * + +# bci +# from bci import * + +# sensor suits +# from suits import * + +# sensors (in general, EMG, etc) +# from sensors import * + +# VR interfaces +# from vr import * + +# robot interfaces +# from robots import * diff --git a/pyrobolearn/tools/interfaces/audio/__init__.py b/pyrobolearn/tools/interfaces/audio/__init__.py new file mode 100644 index 0000000..474549f --- /dev/null +++ b/pyrobolearn/tools/interfaces/audio/__init__.py @@ -0,0 +1,3 @@ + +# import audio interfaces +from audio import * diff --git a/pyrobolearn/tools/interfaces/audio/audio.py b/pyrobolearn/tools/interfaces/audio/audio.py new file mode 100644 index 0000000..61c0f7a --- /dev/null +++ b/pyrobolearn/tools/interfaces/audio/audio.py @@ -0,0 +1,340 @@ +import os + +from pyrobolearn.tools.interfaces.interface import Interface, InputInterface, OutputInterface, InputOutputInterface + +# To use microphone (using PyAudio). This also needed for the 'speech_recognition' module. +try: + import pyaudio +except ImportError as e: + # `pip install --allow-external pyaudio --allow-unverified pyaudio pyaudio` ?? + string = "\nHint: try to install pyaudio by typing the following lines in the terminal: \n" \ + "sudo apt-get install libasound-dev portaudio19-dev libportaudio2 libportaudiocpp0\n" \ + "sudo apt-get install ffmpeg libav-tools\n" \ + "pip install pyaudio\n" + raise ImportError(e.__str__() + string) + +# Speech recognition +# Good tutorial: https://realpython.com/python-speech-recognition/#working-with-microphones +try: + import speech_recognition as sr +except ImportError as e: + string = "\nHint: try to install speech_recognition by typing the following lines in the terminal: \n" \ + "sudo apt-get install libpulse-dev" \ + "pip install pocketsphinx\n" \ + "pip install google-cloud-speech" \ + "pip install SpeechRecognition" + raise ImportError(e.__str__() + string) + +# Speech synthesis +# Good tutorial: https://pythonprogramminglanguage.com/text-to-speech/ +# If Python3.3 or higher: https://pypi.org/project/google_speech/ +try: + from gtts import gTTS +except ImportError as e: + string = "\nHint: try to install gTTS by typing: pip install gTTS" + raise ImportError(e.__str__() + string) + +# # Another one is `pyttsx3`, which is the best offline module (the problem is that it only supports english) +# # Documentation (with examples): pyttsx3.readthedocs.io/en/latest/ +# try: +# import pyttsx3 +# except ImportError as e: +# string = "\nHint: try to install pyttsx3 by typing: pip install pyttsx3" +# raise ImportError(e.__str__() + string) + +# what I also checked: `pyttsx` and `pyvona` +# import pyttsx +# import pyvona + + +# Translation +# Github repo: https://github.com/ssut/py-googletrans +# Tutorial: https://www.codeproject.com/Tips/1236705/How-to-Use-Google-Translator-in-Python +try: + from googletrans import Translator +except ImportError as e: + string = "\nHint: try to install googletrans by typing: pip install googletrans" + raise ImportError(e.__str__() + string) + + +# ChatterBot +# Github repo: https://github.com/gunthercox/ChatterBot +# Documentation: https://chatterbot.readthedocs.io/en/stable +# try: +# from chatterbot import ChatBot +# except ImportError as e: +# string = "\nHint: try to install chatterbot by typing: pip install chatterbot" +# raise ImportError(e.__str__() + string) + + +class AudioInterface(Interface): + r"""Audio Interface + + References: + [1] https://gist.github.com/mabdrabo/8678538 + [2] https://raspberrypi.stackexchange.com/questions/59852/pyaudio-does-not-detect-my-microphone-connected-via-usb-audio-adapter + [3] https://www.swharden.com/wp/2016-07-19-realtime-audio-visualization-in-python/ + [4] https://www.programcreek.com/python/example/52624/pyaudio.PyAudio + [5] https://stackoverflow.com/questions/35970282/what-are-chunks-samples-and-frames-when-using-pyaudio + + [6] https://realpython.com/python-speech-recognition/#working-with-microphones + """ + + def __init__(self): + super(AudioInterface, self).__init__() + self.port = pyaudio.PyAudio() + self.stream = self.port.open(format=pyaudio.paInt16, channels=2, rate=44100, input=True, + frames_per_buffer=1024) #input_device_index=) + + def printInfo(self): + for i in range(self.port.get_device_count()): + info = self.port.get_device_info_by_index(i) + print("###############################################################") + print("Index: {} - name: {} - rate: {} ".format(i, info['name'], info['defaultSampleRate'])) + print("Max input/output channels: {}, {}".format(info['maxInputChannels'], info['maxOutputChannels'])) + print("Input latency (low, high): {}, {}".format(info['defaultLowInputLatency'], + info['defaultHighInputLatency'])) + print("Output latency (low, high): {}, {}".format(info['defaultLowOutputLatency'], + info['defaultHighOutputLatency'])) + print("Is an input device? {}".format(self.isInputDevice(info))) + print("Is an output device? {}".format(self.isOutputDevice(info))) + + def isInputDevice(self, info): + return (info['maxInputChannels'] != 0) + + def isOutputDevice(self, info): + return (info['maxOutputChannels'] != 0) + + def step(self): + data = self.stream.read() + + def __del__(self): + self.stream.stop_stream() + self.stream.close() + + +class InputAudioInterface(InputInterface): + r"""Input Audio Interface. + + See `pyAudio`: https://people.csail.mit.edu/hubert/pyaudio/ + + In pyAudio: + * 'Rate': sampling rate, i.e. the number of frames per second + * 'chunk': arbitrary chosen number of frames the signals are split into + """ + pass + + +class OutputAudioInterface(OutputInterface): + r"""Output Audio Interface + + See `pyAudio`: https://people.csail.mit.edu/hubert/pyaudio/ + """ + pass + + +class InputOutputAudioInterface(InputOutputInterface): + r"""Input and Output Audio Interface. + + See `pyAudio`: https://people.csail.mit.edu/hubert/pyaudio/ + """ + pass + + +class SpeechRecognizerInterface(InputInterface): + r"""Speech Recognizer Interface + + References: + [1] Tutorial: https://realpython.com/python-speech-recognition/#working-with-microphones + """ + + def __init__(self, use_thread=False, lang='english', verbose=False): + self.recognizer = sr.Recognizer() + self.microphone = sr.Microphone() # device_index=-1 + + languages = {'french': 'fr', 'english': 'en', 'american english': 'en-US', 'british english': 'en-GB', + 'indian english': 'en-IN', 'italian': 'it', 'japanese': 'ja', 'korean': 'ko', 'german': 'de', + 'dutch': 'nl', 'spanish': 'es', 'spanish (peru)': 'es-PE', 'chinese': 'zh-CN', + 'mandarin': 'zh-CN', 'polish': 'pl', 'portuguese': 'pt', 'russian': 'ru', 'greek': 'el-GR'} + # Check < https://gist.github.com/traysr/2001377 > for more + self.lang = languages[lang] + + # string that is being said + self.data = '' + + super(SpeechRecognizerInterface, self).__init__(use_thread=use_thread, verbose=verbose) + + def run(self): + # listen to speech through the microphone + with self.microphone as source: + self.recognizer.adjust_for_ambient_noise(source) + print("I am listening...") + audio = self.recognizer.listen(source) # listen to what is being said + + # recognize speech (get the string from audio) + try: + print('Trying to understand what you just said...') + self.data = self.recognizer.recognize_google(audio, language=self.lang) + except sr.UnknownValueError: + print("Unable to recognize speech") + except sr.RequestError as e: + print("API unavailable".format(e)) + + if self.verbose: + print("You said: {}".format(self.data)) + + +class SpeechSynthesizerInterface(OutputInterface): + r"""Speech Synthesizer Interface + + References: + [1] tutorial: https://pythonprogramminglanguage.com/text-to-speech/ + [2] If Python3.3 or higher: https://pypi.org/project/google_speech/ + """ + + def __init__(self, use_thread=False, lang='english', verbose=False): + + languages = {'french': 'fr', 'english': 'en', 'american english': 'en-US', 'british english': 'en-GB', + 'indian english': 'en-IN', 'italian': 'it', 'japanese': 'ja', 'korean': 'ko', 'german': 'de', + 'dutch': 'nl', 'spanish': 'es', 'spanish (peru)': 'es-PE', 'chinese': 'zh-CN', + 'mandarin': 'zh-CN', 'polish': 'pl', 'portuguese': 'pt', 'russian': 'ru', 'greek': 'el-GR'} + # Check < https://gist.github.com/traysr/2001377 > for more + self.lang = languages[lang] + + self.updated = False + self.data = '' + + super(SpeechSynthesizerInterface, self).__init__(use_thread=use_thread, verbose=verbose) + + def run(self): + if self.updated: + # tts = text-to-speech + tts = gTTS(text=self.data, lang=self.lang) + tts.save('tmp.mp3') + os.system('mpg321 tmp.mp3') + os.system('rm tmp.mp3') + self.updated = False + + def update(self, data): + self.data = data + self.updated = True + + +class SpeechTranslatorInterface(InputOutputInterface): + r"""Speech Translator Interface + """ + + def __init__(self, use_thread=False, target_lang='english', from_lang='auto'): + self.translator = Translator() + + languages = {'french': 'fr', 'english': 'en', 'american english': 'en-US', 'british english': 'en-GB', + 'indian english': 'en-IN', 'italian': 'it', 'japanese': 'ja', 'korean': 'ko', 'german': 'de', + 'dutch': 'nl', 'spanish': 'es', 'spanish (peru)': 'es-PE', 'chinese': 'zh-CN', + 'mandarin': 'zh-CN', 'polish': 'pl', 'portuguese': 'pt', 'russian': 'ru', 'greek': 'el-GR', + 'auto': 'auto'} + # Check < https://gist.github.com/traysr/2001377 > for more + self.target_lang = languages[target_lang] + self.from_lang = languages[from_lang] + + self.updated = False + self.input_data = '' + self.data = '' + + super(SpeechTranslatorInterface, self).__init__(use_thread) + + def run(self): + # translate + if self.updated and self.target_lang != self.from_lang: + translated = self.translator.translate(self.input_data, dest=self.target_lang, src=self.from_lang) + self.data = translated.text + self.updated = False + + def update(self, data): + self.input_data = data + self.updated = True + + +class SpeechInterface(InputOutputInterface): + r"""Speech Interface + + This class performs speech recognition, translation, and synthesization. + """ + + def __init__(self, use_thread=False, target_lang='english', from_lang='english'): + self.recognizer = SpeechRecognizerInterface(use_thread=False, lang=from_lang) + self.translator = None + if target_lang != from_lang: + self.translator = SpeechTranslatorInterface(use_thread=False, target_lang=target_lang, + from_lang=from_lang) + self.synthesizer = SpeechSynthesizerInterface(use_thread=False, lang=target_lang) + + self.updated = False + self.input_data = '' + self.output_data = '' + + super(SpeechInterface, self).__init__(use_thread) + + def run(self): + pass + + def update(self, data): + pass + + +# Tests +if __name__ == '__main__': + interface = AudioInterface() + interface.printInfo() + + # recognize, translate and synthesize speech + english = set(['en', 'en-US', 'en-GB']) + languages = {'french': 'fr', 'english': 'en', 'american english': 'en-US', 'british english': 'en-GB', + 'indian english': 'en-IN', 'italian': 'it', 'japanese': 'ja', 'korean': 'ko', 'german': 'de', + 'dutch': 'nl', 'spanish': 'es', 'spanish (peru)': 'es-PE', 'chinese': 'zh-CN', + 'mandarin': 'zh-CN', 'polish': 'pl', 'portuguese': 'pt', 'russian': 'ru', 'greek': 'el-GR'} + # Check < https://gist.github.com/traysr/2001377 > for more + lang = languages['english'] + + recognizer = sr.Recognizer() + microphone = sr.Microphone() # device_index=-1 + + # print(microphone.list_microphone_names()) + + # listen to speech through the microphone + with microphone as source: + recognizer.adjust_for_ambient_noise(source) + print("Say something!") + audio = recognizer.listen(source) + + # recognize speech + print('processing...') + string = '' + try: + string = recognizer.recognize_google(audio, language=lang) + except sr.UnknownValueError: + print("Unable to recognize speech") + except sr.RequestError as e: + print("API unavailable".format(e)) + + print("You said: " + string) + + # translate it if other language than english + if lang not in english: + translator = Translator() + translated = translator.translate(string) # dest='en', src='auto') + print("which translates to: " + translated.text) + + # produce speech + print('Let me try to repeat what you just said:') + tts = gTTS(text=string, lang=lang) + tts.save('tmp.mp3') + os.system('mpg321 tmp.mp3') + os.system('rm tmp.mp3') + + # recognize speech using Sphinx + # try: + # print("Sphinx thinks you said '" + recognizer.recognize_sphinx(audio) + "'") + # except sr.UnknownValueError: + # print("Sphinx could not understand audio") + # except sr.RequestError as e: + # print("Sphinx error; {0}".format(e)) diff --git a/pyrobolearn/tools/interfaces/audio/microphone.py b/pyrobolearn/tools/interfaces/audio/microphone.py new file mode 100644 index 0000000..ab57454 --- /dev/null +++ b/pyrobolearn/tools/interfaces/audio/microphone.py @@ -0,0 +1,22 @@ + +from audio import InputAudioInterface + + +class MicrophoneInterface(InputAudioInterface): + r"""Microphone Interface. + + References: + [1] https://github.com/castorini/honk + [2] https://github.com/llSourcell/tensorflow_speech_recognition_demo/blob/master/speech_data.py + [3] https://github.com/SeanNaren/deepspeech.pytorch + [4] https://github.com/awni/speech + [5] https://github.com/tugstugi/pytorch-speech-commands + [6] https://cmusphinx.github.io/ + [7] https://realpython.com/python-speech-recognition/ + """ + def __init__(self): + super(MicrophoneInterface, self).__init__() + + +if __name__ == '__main__': + pass diff --git a/pyrobolearn/tools/interfaces/audio/speaker.py b/pyrobolearn/tools/interfaces/audio/speaker.py new file mode 100644 index 0000000..e0a60e3 --- /dev/null +++ b/pyrobolearn/tools/interfaces/audio/speaker.py @@ -0,0 +1,8 @@ + +from audio import OutputAudioInterface + + +class SpeakerInterface(OutputAudioInterface): + r"""Speaker Interface + """ + pass diff --git a/pyrobolearn/tools/interfaces/bci/__init__.py b/pyrobolearn/tools/interfaces/bci/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyrobolearn/tools/interfaces/bci/bci.py b/pyrobolearn/tools/interfaces/bci/bci.py new file mode 100644 index 0000000..c7e81a8 --- /dev/null +++ b/pyrobolearn/tools/interfaces/bci/bci.py @@ -0,0 +1,19 @@ + +from pyrobolearn.tools.interfaces.interface import InputInterface + + +class BCIInterface(InputInterface): + r"""Brain-Computer Interface + + This class defines the Brain-Computer Interface which allows to read from a EEG headset. + + Check the following references: + - OpenBCI: + - http://docs.openbci.com/OpenBCI%20Software/05-OpenBCI_Python + - https://github.com/OpenBCI/OpenBCI_Python + - OpenViBE: + - http://openvibe.inria.fr/ + - http://openvibe.inria.fr/tutorial-using-python-with-openvibe/#The+Python+Scripting+box + - https://github.com/dojeda/openvibe-python-tutorial + """ + pass diff --git a/pyrobolearn/tools/interfaces/camera/__init__.py b/pyrobolearn/tools/interfaces/camera/__init__.py new file mode 100644 index 0000000..478f9d1 --- /dev/null +++ b/pyrobolearn/tools/interfaces/camera/__init__.py @@ -0,0 +1,18 @@ + +# General import +from camera import CameraInterface + +# Webcam +from webcam import WebcamInterface + +# Asus Xtion +from asus_xtion import AsusXtionInterface + +# Kinect +from kinect import * + +# FER +from fer import FERInterface + +# OpenPose +from openpose import OpenPoseInterface diff --git a/pyrobolearn/tools/interfaces/camera/asus_xtion.py b/pyrobolearn/tools/interfaces/camera/asus_xtion.py new file mode 100644 index 0000000..21139f0 --- /dev/null +++ b/pyrobolearn/tools/interfaces/camera/asus_xtion.py @@ -0,0 +1,220 @@ + +import numpy as np + +try: + # References: + # - Installation: https://github.com/roboticslab-uc3m/installation-guides/blob/master/install-openni-nite.md + # - OpenNI: https://structure.io/openni + # - Github repo: https://github.com/occipital/openni2 + # - Python wrappers: https://github.com/severin-lemaignan/openni-python + # - OpenNI2-FreenectDriver: https://github.com/OpenKinect/libfreenect/tree/master/OpenNI2-FreenectDriver + + # from primesense import openni2, nite2 + from openni import openni2, nite2 + from openni.utils import InitializationError + + try: + openni2.initialize() + openni2.unload() + except InitializationError as e: + raise InitializationError(repr(e) + '\nYou have to export the path to the folder which contains ' + 'the library libOpenNI2.so. Depending on your architecture, try ' + 'to type one of the following command in the terminal: ' + '\nexport OPENNI2_REDIST=' + '\nexport OPENNI2_REDIST64=') + +except ImportError as e: + raise ImportError(repr(e) + '\nTry to install `openni`: pip install openni' + '\nTo install the OpenNI2-Freenect driver, install libfreenect manually, and ' + 'check the README in the `libfreenect/OpenNI2-FreenectDriver` folder') + + +from camera import CameraInterface + +# check https://github.com/roboticslab-uc3m/installation-guides/blob/master/install-openni-nite.md + + +class AsusXtionInterface(CameraInterface): + r"""Asus Xtion Interface + + Check OpenNI. Check also ROS. + + If using `openni`, connect the Asus Xtion to your computer, and type the following in the terminal: + $ NiViewer + to check if it is correctly detected and working properly. + + References: + [1] https://github.com/danielelic/PyOpenNI2-Utility + [2] https://github.com/kanishkaganguly/OpenNIMultiSensorCapture + [3] https://docs.opencv.org/3.1.0/d7/d6f/tutorial_kinect_openni.html + """ + + def __init__(self, use_thread=False, sleep_dt=0., verbose=False, use_rgb=True, use_depth=True, use_ir=False): + + # quick check + if use_rgb and use_ir: + raise ValueError("It is not possible to stream RGB images at the same as IR images, set one to False") + + # set variables + self.use_rgb = use_rgb + self.use_depth = use_depth + self.use_ir = use_ir + + # initialize openni2; you can give the path to the library as an argument. Otherwise, it will look for + # OPENNI2_REDIST and OPENNI2_REDIST64 environment variables. + openni2.initialize() + + # open all the devices + devices = openni2.Device.open_all() + + # get the correct device (PrimeSense) + self.device = None + for device in devices: + info = device.get_device_info() + if info.vendor == 'PrimeSense': # Asus Xtion Interface + self.device = device + break + + # If didn't find it, return an error + if self.device is None: + devices = [device.get_device_info() for device in devices] + raise ValueError("No Asus devices were detected; we found these devices instead: {}".format(devices)) + + if verbose: + print(self.device.get_device_info()) + + # create RGB, IR, and depth streams + self.streams = [] + if self.use_rgb: + self.rgb_stream = self.device.create_color_stream() + self.streams.append(self.rgb_stream) + + if self.use_ir: + self.ir_stream = self.device.create_ir_stream() + self.streams.append(self.ir_stream) + + if self.use_depth: + self.depth_stream = self.device.create_depth_stream() + self.streams.append(self.depth_stream) + + # start each stream + for stream in self.streams: + stream.start() + + # data + self.rgb = None + self.ir = None + self.depth = None + + super(AsusXtionInterface, self).__init__(use_thread=use_thread, sleep_dt=sleep_dt, verbose=verbose) + + def run(self): + data = [] + if self.use_rgb: + # read frame + frame = self.rgb_stream.read_frame() + buffer = frame.get_buffer_as_uint8() + img = np.frombuffer(buffer, dtype=np.uint8) + img = img.reshape(frame.height, frame.width, 3) + self.rgb = img + data.append(self.rgb) + + if self.use_ir: + # read frame + frame = self.ir_stream.read_frame() + buffer = frame.get_buffer_as_uint16() + img = np.frombuffer(buffer, dtype=np.uint16) + img = img.reshape(frame.height, frame.width) + self.ir = img + data.append(self.ir) + + if self.use_depth: + frame = self.depth_stream.read_frame() + buffer = frame.get_buffer_as_uint16() + img = np.frombuffer(buffer, dtype=np.uint16) + img = img.reshape(frame.height, frame.width) + self.depth = img + data.append(self.depth) + + return data + + def __del__(self): + # close all the streams + for stream in self.streams: + stream.close() + + # unload openni2 + openni2.unload() + + +if __name__ == '__main__': + # https://structure.io/openni + # https://github.com/occipital/OpenNI2 + # https://www.reddit.com/r/ROS/comments/6qejy0/openni_kinect_installation_on_kinetic_indigo/ + # https://github.com/cjcase/openGeppetto/wiki/Installing-OpenNI-on-Ubuntu + # https://github.com/jmendeth/PyOpenNI/blob/c7fa4fa01de3bb717ece5d036daaa343fe1c2ca9/examples/record.py + # https://pypi.org/project/openni/ + # https://roboram.wordpress.com/asus-xtion-pro-live-ubuntu-14-04-installation/ + # https://docs.opencv.org/3.1.0/d7/d6f/tutorial_kinect_openni.html + # http://euanfreeman.co.uk/pyopenni-and-opencv/ + # WARNINGS: THERE ARE 2 OPENNI: One is pyopenni and the other one is openni + # Use opencv with openni: https://gist.github.com/joinAero/1f76844278f141cea8338d1118423648 + # `sudo apt-get install libopenni-dev libopenni2-dev libopenni-sensor-primesense-dev` + # https://github.com/jmendeth/PyOpenNI/wiki/Building-on-Linux + + # capture = cv2.VideoCapture(cv2.CAP_OPENNI) + # print(capture.get(cv2.CAP_PROP_OPENNI_GENERATOR_PRESENT)) + # capture.set(cv2.CAP_OPENNI_IMAGE_GENERATOR_OUTPUT_MODE, cv2.CAP_OPENNI_VGA_30HZ) + # print(type(capture)) + # time.sleep(1.) + # ret = capture.grab() + # print(ret) + # ret, frame = capture.read() + # print(ret) + # ret, depth = capture.retrieve(cv2.CAP_OPENNI_DEPTH_MAP) + # + # print(depth.shape) + # plt.imshow(depth) + # plt.show() + + from itertools import count + import matplotlib.pyplot as plt + + # set what we want to use (note you can not get RGB and IR images at the same time) + use_rgb, use_depth, use_ir = False, True, True + + # create interface + interface = AsusXtionInterface(use_thread=False, sleep_dt=1. / 10, verbose=True, use_rgb=use_rgb, + use_depth=use_depth, use_ir=use_ir) + + # plotting using matplotlib in interactive mode + fig, axes = plt.subplots(1,2) + plots = [None]*2 + titles = [] + if use_rgb: titles.append('RGB') + if use_ir: titles.append('IR') + if use_depth: titles.append('Depth') + plt.ion() # interactive mode on + + for _ in count(): + # if don't use thread call `step` or `run` + data = interface.run() + + # get the frame and plot it with matplotlib + if plots[0] is None: + for i in range(len(plots)): + plots[i] = axes[i].imshow(data[i]) + axes[i].set_title(titles[i]) + else: + for plot, img in zip(plots, data): + plot.set_data(img) + + # pause a bit + plt.pause(0.01) + + # check if the figure is closed, and if so, get out of the loop + if not plt.fignum_exists(fig.number): + break + + plt.ioff() # interactive mode off + plt.show() diff --git a/pyrobolearn/tools/interfaces/camera/camera.py b/pyrobolearn/tools/interfaces/camera/camera.py new file mode 100644 index 0000000..da03e9e --- /dev/null +++ b/pyrobolearn/tools/interfaces/camera/camera.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +"""Define the main basic Camera interface + +This defines the main basic camera interface from which all other interfaces which uses a camera inherit from. +""" + +from pyrobolearn.tools.interfaces.interface import InputInterface + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class CameraInterface(InputInterface): + r"""Camera Interface. + + This is the abstract class Camera Interface which is inherited from all the interfaces that use cameras + such as webcams, kinects, asus xtion, etc. + """ + + def __init__(self, use_thread=False, sleep_dt=0, verbose=False): + super(CameraInterface, self).__init__(use_thread=use_thread, sleep_dt=sleep_dt, verbose=verbose) diff --git a/pyrobolearn/tools/interfaces/camera/description.md b/pyrobolearn/tools/interfaces/camera/description.md new file mode 100644 index 0000000..17ac345 --- /dev/null +++ b/pyrobolearn/tools/interfaces/camera/description.md @@ -0,0 +1,2 @@ +In this folder, we use the Python Wrapper for OpenPose, and apply it on pictures +streamed from a webcam or kinect. diff --git a/pyrobolearn/tools/interfaces/camera/fer.py b/pyrobolearn/tools/interfaces/camera/fer.py new file mode 100644 index 0000000..5d5d19b --- /dev/null +++ b/pyrobolearn/tools/interfaces/camera/fer.py @@ -0,0 +1,16 @@ + +from camera import CameraInterface + + +class FERInterface(CameraInterface): + r"""Facial Expression Recognition (FER) Interface + + References: + [1] EmoPy - A deep neural net toolkit for emotion analysis via Facial Expression Recognition: + https://github.com/thoughtworksarts/EmoPy + [2] http://sefiks.com/2018/01/01/facial-expression-recognition-with-keras/ + [3] https://github.com/a514514772/Real-Time-Facial-Expression-Recognition-with-DeepLearning + """ + + def __init__(self): + super(FERInterface, self).__init__() diff --git a/pyrobolearn/tools/interfaces/camera/kinect.py b/pyrobolearn/tools/interfaces/camera/kinect.py new file mode 100644 index 0000000..8f7f3f0 --- /dev/null +++ b/pyrobolearn/tools/interfaces/camera/kinect.py @@ -0,0 +1,357 @@ + +# import kinect library + +# select which library we want from {freenect, openni, pykinect} +# - freenect is a lower-level library which, in addition to access depth and color images, it also allows you to +# access to various the Kinect hardware (such as the sensors (e.g. accelerometer) and actuators (e.g. LED control) +# on the Kinect). +# - openni is a higher-level library which, in addition to depth and color images, it allows you to perform high-level +# tasks such as skeleton tracking, segmentation, gesture recognition, and others. It also allows you to use other +# devices such as the asus xtion as well. +# --> Note that there is an `OpenNI2-FreenectDriver` in the libfreenect repo, which is a bridge to libfreenect +# implemented as an OpenNI2 driver. It allows OpenNI2 to use Kinect hardware on Linux and OSX. +# - pykinect is a Windows library that allows the user to access the kinect (but cannot be used on Unix systems) +# +# For more info, check the following links: +# - https://robotics.stackexchange.com/questions/565/kinect-libfreenect-vs-opennisensorkinect +# - https://stackoverflow.com/questions/19181332/libfreenect-vs-openni + +import numpy as np + +# by default, use `openni` (optionally with the freenect driver) as it seems to be the most complete library +KINECT_LIBRARY = 'freenect' + +if KINECT_LIBRARY[-8:] == 'freenect': # 'libfreenect' or 'freenect' + # References: + # - OpenKinect: https://openkinect.org/wiki/Getting_Started + # - tuto: naman5.wordpress.com/2014/06/24/experimenting-with-kinect-using-opencv-python-and-open-kinect-libfreenect/ + + try: + import freenect + except ImportError as e: + raise ImportError(repr(e) + '\nTry to install `libfreenect` (manually in order to have the python wrappers):' + '\n# install dependencies' + '\nsudo apt-get install git-core cmake libglut3-dev pkg-config build-essential ' + 'libxmu-dev libxi-dev libusb-1.0-0-dev' + '\n# clone the repo' + '\ngit clone git://github.com/OpenKinect/libfreenect.git' + '\nsudo python setup.py install' + '\n# build the repo and install it' + '\ncd libfreenect; mkdir build; cd build;' + '\ncmake ..; make' + '\nsudo make install' + '\n\n# Install Python wrappers' + '\ncd ../wrappers/python' + '\nsudo python setup.py install') + + # If using `freenect`, connect the kinect to your computer, and type the following in the terminal: + # $ freenect-glview + # to check if it is correctly detected and working properly. + +elif KINECT_LIBRARY == 'openni': + # References: + # - Installation: https://github.com/roboticslab-uc3m/installation-guides/blob/master/install-openni-nite.md + # - OpenNI: https://structure.io/openni + # - Github repo: https://github.com/occipital/openni2 + # - Python wrappers: https://github.com/severin-lemaignan/openni-python + # - OpenNI2-FreenectDriver: https://github.com/OpenKinect/libfreenect/tree/master/OpenNI2-FreenectDriver + if __name__ == '__main__': + try: + from openni import openni2, nite2 + from openni.utils import InitializationError + try: + openni2.initialize() + openni2.unload() + except InitializationError as e: + raise InitializationError(repr(e) + '\nYou have to export the path to the folder which contains ' + 'the library libOpenNI2.so. Depending on your architecture, try ' + 'to type one of the following command in the terminal: ' + '\nexport OPENNI2_REDIST=' + '\nexport OPENNI2_REDIST64=') + except ImportError as e: + raise ImportError(repr(e) + '\nTry to install `openni`: pip install openni' + '\nTo install the OpenNI2-Freenect driver, install libfreenect manually, and ' + 'check the README in the `libfreenect/OpenNI2-FreenectDriver` folder') + + # Checks: + # - To check if `OpenNI` is correctly installed and work, check `NiViewer` binary application in the `OpenNI` + # package + # - To check if `Nite` is correctly installed and work, check `UserViewer` and `HandViewer` binaries in + # the `Nite2` package + + # Troubleshootings: + # - sudo ln -s /lib/x86_64 .. + # - Not detected + + +elif KINECT_LIBRARY == 'pykinect': # WARNING: ONLY WORKS ON WINDOWS + # Reference: + # - https://github.com/Microsoft/PTVS/wiki/PyKinect + # - https://possiblywrong.wordpress.com/2012/11/04/kinect-skeleton-tracking-with-visual-python/ + try: + import pykinect + except ImportError as e: + raise ImportError(repr(e) + '\nTry to install `pykinect`: pip install pykinect') + +else: + raise ValueError("The given KINECT_LIBRARY variable is not known, please select between {freenect, openni, " + "pykinect}") + + +# import interface +from camera import CameraInterface + + +class KinectInterface(CameraInterface): + r"""Kinect Interface + + This defines the kinect interface class. + + There are 3 different libraries `freenect`, `openni`, and `pykinect` that can be used. + - `freenect`: it is a lower-level library which, in addition to access depth and color images, it also allows you + to access to various the Kinect hardware (such as the sensors (e.g. accelerometer) and actuators (e.g. LED control) + on the Kinect). + - `openni`: it is a higher-level library which, in addition to depth and color images, it allows you to perform + high-level tasks such as skeleton tracking, segmentation, gesture recognition, and others. Note that there is an + `OpenNI2-FreenectDriver` in the libfreenect repo, which is a bridge to libfreenect implemented as an OpenNI2 + driver. It allows OpenNI2 to use Kinect hardware on Linux and OSX. + - `pykinect`: it is a Windows library that allows the user to access the kinect (but cannot be used on Unix + systems) + + References: + [1] libfreenect: + - Wiki: https://openkinect.org/wiki/Getting_Started + - Github repo: https://github.com/OpenKinect/libfreenect + [2] OpenNI: + - Homepage: https://structure.io/openni + - Github repo: https://github.com/occipital/openni2 + - Openni-Python: https://github.com/severin-lemaignan/openni-python + - OpenNI2-FreenectDriver: https://github.com/OpenKinect/libfreenect/tree/master/OpenNI2-FreenectDriver + [3] PyKinect: https://github.com/Microsoft/PTVS/wiki/PyKinect + """ + + def __init__(self, use_thread=False, sleep_dt=0., verbose=False): + super(KinectInterface, self).__init__(use_thread=use_thread, sleep_dt=sleep_dt, verbose=verbose) + + +class FreenectKinectInterface(KinectInterface): + r"""Freenect Kinect Interface + + Kinect interface using the `freenect` library. + + References: + [1] libfreenect: + - Wiki: https://openkinect.org/wiki/Getting_Started + - Github repo: https://github.com/OpenKinect/libfreenect + [2] Tutorial: naman5.wordpress.com/2014/06/24/experimenting-with-kinect-using-opencv-python\ + -and-open-kinect-libfreenect/ + """ + + def __init__(self, use_thread=False, sleep_dt=0., verbose=False): + + # data + self.rgb = None + self.depth = None + + super(FreenectKinectInterface, self).__init__(use_thread=use_thread, sleep_dt=sleep_dt, verbose=verbose) + + def get_image(self, convertTo=None): # cv2.COLOR_RGB2BGR): + array, _ = freenect.sync_get_video() + if convertTo is not None: + array = cv2.cvtColor(array, convertTo) + return array + + def get_depth(self): + array, _ = freenect.sync_get_depth() + array = array.astype(np.uint8) + return array + + def run(self): + self.rgb = self.get_image() + self.depth = self.get_depth() + + return self.rgb, self.depth + + +class OpenNIKinectInterface(KinectInterface): + r"""OpenNI Kinect Interface + + Kinect interface using the `openni` library. + + References: + [1] https://github.com/danielelic/PyOpenNI2-Utility + [2] https://github.com/kanishkaganguly/OpenNIMultiSensorCapture + [3] https://docs.opencv.org/3.1.0/d7/d6f/tutorial_kinect_openni.html + """ + + def __init__(self, use_thread=False, sleep_dt=0., verbose=False): + + # initialize openni2; you can give the path to the library as an argument. Otherwise, it will look for + # OPENNI2_REDIST and OPENNI2_REDIST64 environment variables. + openni2.initialize() + + # open all the devices + devices = openni2.Device.open_all() + + # get the correct device (Microsoft Kinect) + self.device = None + for device in devices: + info = device.get_device_info() + if info.vendor == 'Microsoft' and info.name == 'Kinect': # Kinect Interface + self.device = device + break + + # If didn't find it, return an error + if self.device is None: + devices = [device.get_device_info() for device in devices] + raise ValueError("No Asus devices were detected; we found these devices instead: {}".format(devices)) + + if verbose: + print(self.device.get_device_info()) + + # create RGB and depth streams + self.rgb_stream = self.device.create_color_stream() + self.depth_stream = self.device.create_depth_stream() + + # start the streams + self.rgb_stream.start() + self.depth_stream.start() + + # data + self.rgb = None + self.depth = None + + super(OpenNIKinectInterface, self).__init__(use_thread=use_thread, sleep_dt=sleep_dt, verbose=verbose) + + def run(self): + # read frames + rgb_frame = self.rgb_stream.read_frame() + depth_frame = self.depth_stream.read_frame() + + # get buffers + rgb = rgb_frame.get_buffer_as_uint8() + depth = depth_frame.get_buffer_as_uint16() + + # convert from buffers to images and reshape them + rgb = np.frombuffer(rgb, dtype=np.uint8) + rgb = rgb.reshape(rgb_frame.height, rgb_frame.width, 3) + depth = np.frombuffer(depth, dtype=np.uint16) + depth = depth.reshape(depth_frame.height, depth_frame.width) + + # save images and return them + self.rgb, self.depth = rgb, depth + return self.rgb, self.depth + + def __del__(self): + # close all the streams + self.rgb_stream.close() + self.depth_stream.close() + + # unload openni2 + openni2.unload() + + +# TODO: add pose and gesture types +class KinectSkeletonTrackingInterface(KinectInterface): + r"""Skeleton tracking + + Skeleton tracking using the openni and nite libraries. + + References: + [1] https://github.com/severin-lemaignan/openni-python + """ + + def __init__(self, use_thread=False, sleep_dt=0., verbose=False, track_hand=False): + + # initialize openni2 and nite2; you can give the path to the library as an argument. + # Otherwise, it will look for OPENNI2_REDIST / OPENNI2_REDIST64 and NITE2_REDIST / NITE2_REDIST64 environment + # variables. + openni2.initialize() + nite2.initialize() + + # open all the devices + devices = openni2.Device.open_all() + + # get the correct device (Microsoft Kinect) + self.device = None + for device in devices: + info = device.get_device_info() + if info.vendor == 'Microsoft' and info.name == 'Kinect': # Kinect Interface + self.device = device + break + + # If didn't find it, return an error + if self.device is None: + devices = [device.get_device_info() for device in devices] + raise ValueError("No Asus devices were detected; we found these devices instead: {}".format(devices)) + + if verbose: + print(self.device.get_device_info()) + + # create tracker for the hand or user depending on the given parameter + if track_hand: + self.tracker = nite2.HandTracker(self.device) + else: + self.tracker = nite2.UserTracker(self.device) + + # data + self.joints = ['head', 'neck', 'torso', 'left_shoulder', 'left_elbow', 'left_hand', 'left_hip', 'left_knee', + 'left_foot', 'right_shoulder', 'right_elbow', 'right_hand', 'right_hip', 'right_knee', + 'right_foot'] + joint = nite2.JointType + self.nite_joints = [joint.NITE_JOINT_HEAD, joint.NITE_JOINT_NECK, joint.NITE_JOINT_TORSO, + joint.NITE_JOINT_LEFT_SHOULDER, joint.NITE_JOINT_LEFT_ELBOW, joint.NITE_JOINT_LEFT_HAND, + joint.NITE_JOINT_LEFT_HIP, joint.NITE_JOINT_LEFT_KNEE, joint.NITE_JOINT_LEFT_FOOT, + joint.NITE_JOINT_RIGHT_SHOULDER, joint.NITE_JOINT_RIGHT_ELBOW, joint.NITE_JOINT_RIGHT_HAND, + joint.NITE_JOINT_RIGHT_HIP, joint.NITE_JOINT_RIGHT_KNEE, joint.NITE_JOINT_RIGHT_FOOT] + + self.data = {} + + super(KinectSkeletonTrackingInterface, self).__init__(use_thread=use_thread, sleep_dt=sleep_dt, + verbose=verbose) + + def run(self): + # read frame + frame = self.tracker.read_frame() + + # check if users in the frame + if frame.users: + # for each user in the frame + for user in frame.users: + # check if it is a new one + if user.is_new(): + if self.verbose: + print("New user detected! Calibrating...") + self.tracker.start_skeleton_tracking(user.id) + self.data = {user.id: {}} + + # check that the state has been correctly tracked + elif user.skeleton.state == nite2.SkeletonState.NITE_SKELETON_TRACKED: + + # go through each joint and update the user data + for joint, nite_joint in zip(self.joints, self.nite_joints): + j = user.skeleton.joints[nite_joint] + self.data[user.id][joint] = (j.position.x, j.position.y, j.position.z, j.positionConfidence) + + # self.data[user.id]['updated'] = True + + return self.data + + def __del__(self): + # unload nite2 and openni2 + nite2.unload() + openni2.unload() + + +class ROSKinectInterface(CameraInterface): + r"""ROS Kinect Interface + + References: + [1] http://wiki.ros.org/openni_camera + """ + pass + + +# Tests +if __name__ == '__main__': + pass diff --git a/pyrobolearn/tools/interfaces/camera/openpose.py b/pyrobolearn/tools/interfaces/camera/openpose.py new file mode 100644 index 0000000..a1af618 --- /dev/null +++ b/pyrobolearn/tools/interfaces/camera/openpose.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python +"""Define the Openpose Interface + +This extracts the human skeleton from an image or stream of images (from a webcam for instance) using the openpose +library. See `https://github.com/CMU-Perceptual-Computing-Lab/openpose` for more info about openpose. +""" + +import os +import cv2 +# import PyOpenPose as OP +try: + import pyopenpose +except ImportError as e: + raise ImportError(repr(e) + '\nTry to install `pyopenpose` by installing the openpose library. Check the script ' + '`pyrobolearn/scripts/install_openpose.sh` to install the library and the associated ' + 'python wrapper.') + +from camera import CameraInterface +from webcam import WebcamInterface + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class OpenPoseInterface(CameraInterface): + r"""OpenPose interface + + This class defines the OpenPose interface which uses a camera interface like a webcam or kinect to get the (2D or + 3D) pictures and map them to the human kinematic skeleton. + + References: + [1] OpenPose: github.com/CMU-Perceptual-Computing-Lab/openpose + [2] PyOpenPose (official): github.com/CMU-Perceptual-Computing-Lab/openpose/blob/master/doc/modules/python_module.md + [3] OpenPose output format: github.com/CMU-Perceptual-Computing-Lab/openpose/blob/master/doc/output.md + [4] PyOpenPose (python wrappers): github.com/FORTH-ModelBasedTracker/PyOpenPose + """ + + def __init__(self, camera=None, detect_face=False, detect_hands=False, openpose_path=None, + use_thread=False, sleep_dt=0, verbose=False): + + # save variables + self.detect_face = detect_face + self.detect_hands = detect_hands + + # Check the given camera + if camera is None: + # If None, get pictures from a webcam + camera = WebcamInterface(use_thread=False, convertTo=None, verbose=False) + self.camera_in_openpose = True + else: + self.camera_in_openpose = False + + self.camera = camera + + # Define the JOINTS + # define the 25 BODY joints + self.body_joints = ['Nose', 'Neck', 'RShoulder', 'RElbow', 'RWrist', 'LShoulder', 'LElbow', 'LWrist', 'MidHip', + 'RHip', 'RKnee', 'RAnkle', 'LHip', 'LKnee', 'LAnkle', 'REye', 'LEye', 'REar', 'LEar', + 'LBigToe', 'LSmallToe', 'LHeel', 'RBigToe', 'RSmallToe', 'RHeel', 'Background'] + self.body_joint_names_to_ids = dict(zip(self.body_joints, range(len(self.body_joints)))) + + # define the 21 HAND joints + # for each of the 4 main finger(s) (without thumb), there are 4 joints (proximal, middle, distal, tip) + self.hand_joints = ['Palm'] + ['Thumb' + str(i) for i in range(4)] + ['Index' + str(i) for i in range(4)] + \ + ['Middle' + str(i) for i in range(4)] + ['Ring' + str(i) for i in range(4)] + \ + ['Little' + str(i) for i in range(4)] + self.hand_joint_names_to_ids = dict(zip(self.hand_joints, range(len(self.hand_joints)))) + + # define the 70 FACE joints + # Note that in github.com/CMU-Perceptual-Computing-Lab/openpose/blob/master/doc/media/keypoints_face.png, + # the joints are not symmetric but are read from left to right. + # The joints are defined as: + # face (beard): [0, 16] + # right eyebrow: [17, 21] + # left eyebrow: [22,26] + # nose: [27, 35] + # right eye: [36, 41] + 68 + # left eye: [42, 47] + 69 + # mouth: [48, 67] + self.face_joints = ['Face' + str(i) for i in range(17)] + ['REyebrow' + str(i-17) for i in range(17, 22)] + \ + ['LEyebrow' + str(i-22) for i in range(22, 27)] + \ + ['Nose' + str(i-27) for i in range(27, 36)] + \ + ['REye' + str(i-36) for i in range(36, 42)] + \ + ['LEye' + str(i-42) for i in range(42, 48)] + \ + ['Mouth' + str(i-48) for i in range(48, 68)] + ['REye6'] + ['LEye6'] + self.face_joint_names_to_ids = dict(zip(self.face_joints, range(len(self.face_joints)))) + + # Check the path to the openpose folder (which contains various models and test images) + if openpose_path is None: + if 'OPENPOSE_PATH' not in os.environ: + raise ValueError("The OPENPOSE_PATH environment variable has not been set properly. Please " + "then provide the path to the openpose folder by specifying the `openpose_path` " + "argument") + openpose_path = os.environ['OPENPOSE_PATH'] + self.openpose_path = openpose_path + + # specify the parameters + params = dict() + params["model_folder"] = path + "models/" + if detect_face: + params["face"] = True + if detect_hands: + params["hands"] = True + + # configure openpose + self.openpose = pyopenpose.WrapperPython() + self.openpose.configure(params) + self.openpose.start() + + # define data holder + self.datum = pyopenpose.Datum() + + # define frame + self.frame = None + + # call superclass constructor + super(OpenPoseInterface, self).__init__(use_thread=use_thread, sleep_dt=sleep_dt, verbose=verbose) + + @property + def num_bodies(self): + return len(self.datum.poseKeypoints) + + @property + def body_keypoints(self): + """Return the keypoints (x,y,confidence) for each body part for each person""" + return self.datum.poseKeypoints + + @property + def left_hand_keypoints(self): + return self.datum.handKeypoints[0] + + @property + def right_hand_keypoints(self): + return self.datum.handKeypoints[1] + + @property + def hand_keypoints(self): + return self.left_hand_keypoints, self.right_hand_keypoints + + @property + def face_keypoints(self): + return self.datum.faceKeypoints + + @property + def keypoints(self): + return self.body_keypoints, self.face_keypoints, self.left_hand_keypoints, self.right_hand_keypoints + + @property + def heatmap(self): + return None + + @property + def input_image(self): + return self.datum.cvInputData + + @property + def output_image(self): + return self.datum.cvOutputData + + @property + def num_gpus(self): + return pyopenpose.get_gpu_number() + + def run(self, input_frame=None): + if input_frame is None: + # read image + if self.camera_in_openpose: + self.camera.run() + img = self.camera.frame + else: + if isinstance(input_frame, str): + img = cv2.imread(input_frame) + else: + img = input_frame + + # define data holder + self.datum = pyopenpose.Datum() + + # process image + self.datum.cvInputData = img + self.openpose.emplaceAndPop([self.datum]) + + # save frame + self.frame = self.datum.cvOutputData + + # define dictionary of keypoints + keypoints = dict() + keypoints['body'] = self.body_keypoints + if self.detect_face: + keypoints['face'] = self.face_keypoints + if self.detect_hands: + keypoints['left_hand'] = self.left_hand_keypoints + keypoints['right_hand'] = self.right_hand_keypoints + + # display image if specified + if self.verbose: + cv2.imshow('OpenPose frame', self.frame) + + # quit display if 'esc' button is pressed + key = cv2.waitKey(15) & 0xFF + if key == 27: + self.verbose = False + cv2.destroyWindow('frame') + + # return frame and the associated keypoints + return self.frame, keypoints + + +# Tests +if __name__ == '__main__': + path = '/home/brian/repos/openpose/' + interface = OpenPoseInterface(openpose_path=path) # , use_thread=False, sleep_dt=1./10, verbose=True) + + while True: + frame, keypoints = interface.run() + cv2.imshow("OpenPose 1.4.0 - Tutorial Python API", frame) + cv2.waitKey(15) diff --git a/pyrobolearn/tools/interfaces/camera/webcam.py b/pyrobolearn/tools/interfaces/camera/webcam.py new file mode 100644 index 0000000..64cadb5 --- /dev/null +++ b/pyrobolearn/tools/interfaces/camera/webcam.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python +"""Define the Webcam Interface + +This provides the main interface to get pictures from the specified webcam. +""" + + +import cv2 # OpenCV to capture image from webcam +import os + +from camera import CameraInterface + +# to close correctly the webcam once we run +os.environ["OPENCV_VIDEOIO_PRIORITY_MSMF"] = "0" + +# For multithreading, check also: +# - http://blog.blitzblit.com/2017/12/24/asynchronous-video-capture-in-python-with-opencv/ +# - https://nrsyed.com/2018/07/05/multithreading-with-opencv-python-to-improve-video-processing-performance/ + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class WebcamInterface(CameraInterface): + r"""Webcam Interface + + This class defines the webcam interface. It gets pictures from the webcam, which can then be processed by another + tool (such as openpose). + + References: + [1] https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_gui/py_video_display/py_video_display.html + """ + + def __init__(self, webcamId=0, saveVideo=False, filename='output.avi', fps=20, frameSize=(640,480), codec='XVID', + convertTo=cv2.COLOR_BGR2RGB, use_thread=False, sleep_dt=0, verbose=False): + + # create video capture object + self.capture = cv2.VideoCapture(webcamId) + + # create video writer + if saveVideo: + # define codec (DIVX, XVID, MJPG, X264, WMV1, WMV2) + codec = cv2.VideoWriter_fourcc(*codec) + # define video writer + self.writer = cv2.VideoWriter(filename, codec, fps, frameSize) + else: + self.writer = None + + # variables + self.convertTo = convertTo + self.verbose = False + + # camera image + self.frame = self.run() + + # call superclass constructor + super(WebcamInterface, self).__init__(use_thread=use_thread, sleep_dt=sleep_dt, verbose=verbose) + + def run(self): # , display=True, convertToGray=False): + # get the frame from the webcam + return_code, frame = self.capture.read() + + if not return_code: + raise ValueError("There was an error when reading the frame from the webcam") + + # convert to gray (if specified) + if self.convertTo == cv2.COLOR_BGR2GRAY: + frame = cv2.cvtColor(frame, self.convertTo) + + # display image if specified + if self.verbose: + cv2.imshow('frame', frame) + + # quit display if 'esc' button is pressed + key = cv2.waitKey(1) & 0xFF + if key == 27: + self.verbose = False + cv2.destroyWindow('frame') + + # convert to the specified format + if self.convertTo is not None: + frame = cv2.cvtColor(frame, self.convertTo) + + # save the frame for the video + if self.writer is not None: + self.writer.write(frame) + + # set and return the frame + self.frame = frame + return frame + + def __del__(self): + self.capture.release() + if self.writer is not None: + self.writer.release() + # cv2.destroyAllWindows() + + +# Test +if __name__ == '__main__': + from itertools import count + import matplotlib.pyplot as plt + + # create interface + interface = WebcamInterface(use_thread=True, sleep_dt=1./10, verbose=False) + + # plotting using matplotlib in interactive mode + fig = plt.figure() + plot = None + plt.ion() # interactive mode on + + for _ in count(): + # # if don't use thread call `step` or `run` (note that `run` returns the frame but not + # interface.step() + + # get the frame and plot it with matplotlib + frame = interface.frame + if plot is None: + plot = plt.imshow(frame) + else: + plot.set_data(frame) + plt.pause(0.01) + + # check if the figure is closed, and if so, get out of the loop + if not plt.fignum_exists(fig.number): + break + + plt.ioff() # interactive mode off + plt.show() diff --git a/pyrobolearn/tools/interfaces/controllers/__init__.py b/pyrobolearn/tools/interfaces/controllers/__init__.py new file mode 100644 index 0000000..822d35e --- /dev/null +++ b/pyrobolearn/tools/interfaces/controllers/__init__.py @@ -0,0 +1,9 @@ + +# import general game controller interface +from controller import GameControllerInterface + +# Xbox controller interface +from xbox import * + +# Playstation controller interface +from playstation import * diff --git a/pyrobolearn/tools/interfaces/controllers/controller.py b/pyrobolearn/tools/interfaces/controllers/controller.py new file mode 100644 index 0000000..fb7ec61 --- /dev/null +++ b/pyrobolearn/tools/interfaces/controllers/controller.py @@ -0,0 +1,11 @@ + +from pyrobolearn.tools.interfaces.interface import InputOutputInterface + + +class GameControllerInterface(InputOutputInterface): + r"""Game Controller Interface + + """ + + def __init__(self, use_thread=False, sleep_dt=0, verbose=False): + super(GameControllerInterface, self).__init__(use_thread=use_thread, sleep_dt=sleep_dt, verbose=verbose) diff --git a/pyrobolearn/tools/interfaces/controllers/playstation.py b/pyrobolearn/tools/interfaces/controllers/playstation.py new file mode 100755 index 0000000..8b45f87 --- /dev/null +++ b/pyrobolearn/tools/interfaces/controllers/playstation.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python +"""Define the PlayStation controller interface + +This provides the interfaces for the PlayStation controllers (PS3 and PS4) using the `inputs` library. +""" + +try: + from inputs import devices, get_gamepad +except ImportError as e: + raise ImportError(repr(e) + '\nTry to install `inputs`: pip install inputs') + +from controller import GameControllerInterface + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class PSControllerInterface(GameControllerInterface): + r"""PlayStation Controller Interface + + This provides the code for the PlayStation Controller interface. We use the `inputs` Python library [1, 2]. + If the PS controller is not detected, please install the necessary drivers. + + References: + [1] Python library `inputs`: https://inputs.readthedocs.io + [2] Hardware support: https://inputs.readthedocs.io/en/latest/user/hardwaresupport.html + """ + + def __init__(self, use_thread=False, controller_name='Sony Interactive Entertainment Wireless Controller'): + # Check if some gamepads are connected to the computer + gamepads = devices.gamepads + if len(gamepads) == 0: + raise ValueError("No gamepads/controllers were detected.") + + # Check if the specified gamepad is detected + self.gamepad = None + for gamepad in gamepads: + if controller_name in gamepad.name: + self.gamepad = gamepad + break + + if self.gamepad is None: + raise ValueError("The specified gamepad/controller was not detected.") + + # translation + buttons = ['BTN_SOUTH', 'BTN_EAST', 'BTN_WEST', 'BTN_NORTH', 'BTN_THUMBL', 'BTN_THUMBR', 'BTN_TL', 'BTN_TL2', + 'BTN_TR', 'BTN_TR2', 'BTN_START', 'BTN_SELECT', 'BTN_MODE', 'ABS_HAT0X', 'ABS_HAT0Y', 'ABS_Z', 'ABS_RZ', + 'ABS_X', 'ABS_Y', 'ABS_RX', 'ABS_RY'] + ps4_buttons = ['X', 'O', 'S', 'T', 'LJB', 'RJB', 'L1', 'L2', 'R1', 'R2', 'options', 'share', 'PS', 'L', 'R', 'RT', + 'LJX', 'LJY', 'RJX', 'RJY'] + self.map = dict(zip(buttons, ps4_buttons)) + self.inv_map = dict(zip(ps4_buttons, buttons)) + + # buttons and their values + self.buttons = dict(zip(ps4_buttons[:12], [0] * 12)) + self.buttons.update(dict(zip(['Dpad', 'LJ', 'RJ'], [[0, 0]] * 3))) + + # last updated button + self.last_updated_button = None + + super(PSControllerInterface, self).__init__(use_thread) + + ############## + # Properties # + ############## + + @property + def X(self): + """Button X""" + return self.buttons['X'] + + @property + def O(self): + """Button O (circle)""" + return self.buttons['O'] + + # alias (C = circle) + C = O + + @property + def S(self): + """Button Square""" + return self.buttons['S'] + + @property + def T(self): + """Button Triangle""" + return self.buttons['T'] + + @property + def LJB(self): + """Left Joystick Button""" + return self.buttons['LJB'] + + @property + def RJB(self): + """Right Joystick Button""" + return self.buttons['RJB'] + + @property + def LB(self): + """left bumper; button for left index finger""" + return self.buttons['LB'] + + @property + def RB(self): + """right bumper; button for right index finger""" + return self.buttons['RB'] + + @property + def menu(self): + """menu button""" + return self.buttons['menu'] + + @property + def view(self): + """view button""" + return self.buttons['view'] + + @property + def LT(self): + """Left trigger; button for left middle finger""" + return self.buttons['LT'] + + @property + def RT(self): + """Right trigger; button for right middle finger""" + return self.buttons['RT'] + + @property + def Dpad(self): + """Directional pad""" + return self.buttons['Dpad'] + + @property + def LJ(self): + """Left Joystick""" + return self.buttons['LJ'] + + @property + def RJ(self): + """Right Joystick""" + return self.buttons['RJ'] + + # aliases + left_joystick = LJ + right_joystick = RJ + + ########### + # Methods # + ########### + + def run(self): + # print('running') + events = self.gamepad.read() # blocking=False) # get_gamepad() + for event in events: + event_type, code, state = event.ev_type, event.code, event.state + self.__setitem(event_type, self.map.get(code), state) + + # display info + if self.verbose: + print("Pushed button {} - state = {}".format(self.last_updated_button, + self.buttons[self.last_updated_button])) + + def setLeftVibration(self, time_msec): + """Set the vibration for the left motor""" + self.gamepad.set_vibration(1, 0, time_msec) + # display info + if self.verbose: + print("Set vibration to the left motor for {} msec".format(time_msec)) + + def setRightVibration(self, time_msec): + """Set the vibration for the right motor""" + self.gamepad.set_vibration(0, 1, time_msec) + # display info + if self.verbose: + print("Set vibration to the right motor for {} msec".format(time_msec)) + + def setVibration(self, time_msec): + """Set the vibration for both motors""" + self.gamepad.set_vibration(1, 1, time_msec) + # display info + if self.verbose: + print("Set vibration to both motors for {} msec".format(time_msec)) + + def __getitem__(self, key): + """Return the specified button""" + if key is not None: + return self.buttons[key] + + def __setitem(self, event_type, key, value): + + if event_type == 'Absolute': + if key == 'LJX': + self.buttons['LJ'][0] = value / 32768. # values between [-32768, 32767] + self.last_updated_button = 'LJ' + elif key == 'LJY': + self.buttons['LJ'][1] = -1. * value / 32768. # values between [-32767, 32768] + self.last_updated_button = 'LJ' + elif key == 'RJX': + self.buttons['RJ'][0] = value / 32768. # values between [-32768, 32767] + self.last_updated_button = 'RJ' + elif key == 'RJY': + self.buttons['RJ'][1] = -1. * value / 32768. # values between [-32767, 32768] + self.last_updated_button = 'RJ' + elif key == 'DpadX': + self.buttons['Dpad'][0] = value # left (-1) and right (1) + self.last_updated_button = 'Dpad' + elif key == 'DpadY': + self.buttons['Dpad'][1] = -1 * value # down (-1) and high (1) + self.last_updated_button = 'Dpad' + elif key == 'LT' or key == 'RT': # max 1023 + self.buttons[key] = value / 1023. + self.last_updated_button = key + elif event_type == 'Key': + self.buttons[key] = value + self.last_updated_button = key + + +class PS3ControllerInterface(PSControllerInterface): + r"""PlayStation 3 Controller Interface + + This provides the code for the PS3 Controller interface. + + In order to make this code works, make sure you installed the `inputs` Python library [1,2]. + If the PS4 controller is not detected, please install the necessary drivers. + + References: + [1] Python library `inputs`: https://inputs.readthedocs.io + [2] Hardware support: https://inputs.readthedocs.io/en/latest/user/hardwaresupport.html + """ + + def __init__(self, use_thread=False): + super(PS3ControllerInterface, self).__init__(use_thread=use_thread, + controller_name='Sony PLAYSTATION(R)3 Controller') + + +class PS4ControllerInterface(PSControllerInterface): + r"""PlayStation 4 Controller Interface + + This provides the code for the PS4 Controller interface. + + In order to make this code works, make sure you installed the `inputs` Python library [1,2]. + If the PS4 controller is not detected, please install the necessary drivers. + + References: + [1] Python library `inputs`: https://inputs.readthedocs.io + [2] Hardware support: https://inputs.readthedocs.io/en/latest/user/hardwaresupport.html + """ + + def __init__(self, use_thread=False): + super(PS4ControllerInterface, self).__init__(use_thread=use_thread, + controller_name='Sony Interactive Entertainment Wireless Controller') + + +# Tests +if __name__ == '__main__': + device = devices.gamepads[0] + while True: + events = device.read() # blocking=False) # get_gamepad() + for event in events: + event_type, code, state = event.ev_type, event.code, event.state + if event_type != 'Absolute': + if code != 'SYN_REPORT': + print(code, state) diff --git a/pyrobolearn/tools/interfaces/controllers/xbox.py b/pyrobolearn/tools/interfaces/controllers/xbox.py new file mode 100644 index 0000000..e791bac --- /dev/null +++ b/pyrobolearn/tools/interfaces/controllers/xbox.py @@ -0,0 +1,267 @@ + +try: + from inputs import devices + # TODO: update the library inputs to make it non-blocking + # solution: use the fcntl to read in a non-blocking mode, change `InputDevice._get_data(self, read_size)`, + # instead of using `read`, use the fcntl library + # solution1: use threads + # References: + # [1] https://github.com/zeth/inputs/pull/9/commits/e1356b945c8f47667fe2f0b4f13b9e8e7b83238a (doesn't work) + # [2] https://github.com/rakshit97/usb-controller-as-mouse (gives a pointer) +except ImportError as e: + raise ImportError(repr(e) + '\nTry to install `inputs`: pip install inputs') + +from controller import GameControllerInterface + + +class XboxControllerInterface(GameControllerInterface): + r"""Xbox Controller Interface. + + This provides the code for the Xbox Controller interface. + + In order to make this code works, make sure you installed the `inputs` Python library [1]. If the Xbox controller + is not detected, please install the necessary driver. On Ubuntu 16.04, you can install the `xpad` driver by typing + the following commands (see [3]): + + ```bash + sudo apt-get install git + sudo apt-get install dkms + sudo git clone https://github.com/paroj/xpad.git /usr/src/xpad-0.4 + sudo dkms install -m xpad -v 0.4 + ``` + + Notes: I tried the `xboxdrv` driver (https://github.com/xboxdrv/xboxdrv) on Ubuntu 16.04 (kernel 4.1* and 4.4), + which was necessary for the following code repos: + * https://github.com/FRC4564/Xbox + * https://github.com/linusg/xbox360controller (Note that this requires at least Python 3.3) + but it didn't work. For more info, check [3] + + References: + [1] Python library `inputs`: https://inputs.readthedocs.io + [2] Hardware support: https://inputs.readthedocs.io/en/latest/user/hardwaresupport.html + [3] https://askubuntu.com/questions/783587/how-do-i-get-an-xbox-one-controller-to-work-with-16-04-not-steam + """ + + def __init__(self, use_thread=False, sleep_dt=0, verbose=False, controller_name='X-Box One'): + # Check if some gamepads are connected to the computer + gamepads = devices.gamepads + if len(gamepads) == 0: + raise ValueError("No gamepads/controllers were detected.") + + # Check if the specified gamepad is detected + self.gamepad = None + for gamepad in gamepads: + if controller_name in gamepad.name: + self.gamepad = gamepad + break + + if self.gamepad is None: + raise ValueError("The specified gamepad/controller was not detected.") + + # translation + buttons = ['BTN_SOUTH', 'BTN_EAST', 'BTN_WEST', 'BTN_NORTH', 'BTN_THUMBL', 'BTN_THUMBR', 'BTN_TL', 'BTN_TR', + 'BTN_START', 'BTN_SELECT', 'ABS_Z', 'ABS_RZ', 'ABS_HAT0X', 'ABS_HAT0Y', 'ABS_X', 'ABS_Y', 'ABS_RX', + 'ABS_RY'] + xbox_buttons = ['A', 'B', 'Y', 'X', 'LJB', 'RJB', 'LB', 'RB', 'menu', 'view', 'LT', 'RT', 'DpadX', 'DpadY', + 'LJX', 'LJY', 'RJX', 'RJY'] + self.map = dict(zip(buttons, xbox_buttons)) + + # buttons and their values + self.buttons = dict(zip(xbox_buttons[:12], [0]*12)) + self.buttons.update(dict(zip(['Dpad', 'LJ', 'RJ'], [[0,0]]*3))) + + # last updated button + self.last_updated_button = None + + super(XboxControllerInterface, self).__init__(use_thread=use_thread, sleep_dt=sleep_dt, verbose=verbose) + + ############## + # Properties # + ############## + + @property + def A(self): + """Button A""" + return self.buttons['A'] + + @property + def B(self): + """Button B""" + return self.buttons['B'] + + @property + def Y(self): + """Button Y""" + return self.buttons['Y'] + + @property + def X(self): + """Button X""" + return self.buttons['X'] + + @property + def LJB(self): + """Left Joystick Button""" + return self.buttons['LJB'] + + @property + def RJB(self): + """Right Joystick Button""" + return self.buttons['RJB'] + + @property + def LB(self): + """left bumper; button for left index finger""" + return self.buttons['LB'] + + @property + def RB(self): + """right bumper; button for right index finger""" + return self.buttons['RB'] + + @property + def menu(self): + """menu button""" + return self.buttons['menu'] + + @property + def view(self): + """view button""" + return self.buttons['view'] + + @property + def LT(self): + """Left trigger; button for left middle finger""" + return self.buttons['LT'] + + @property + def RT(self): + """Right trigger; button for right middle finger""" + return self.buttons['RT'] + + @property + def Dpad(self): + """Directional pad""" + return self.buttons['Dpad'] + + @property + def LJ(self): + """Left Joystick""" + return self.buttons['LJ'] + + @property + def RJ(self): + """Right Joystick""" + return self.buttons['RJ'] + + # aliases + left_joystick = LJ + right_joystick = RJ + + ########### + # Methods # + ########### + + def run(self): + #print('running') + events = self.gamepad.read() #blocking=False) # get_gamepad() + for event in events: + event_type, code, state = event.ev_type, event.code, event.state + self.__setitem(event_type, self.map.get(code), state) + + # display info + if self.verbose: + print("Pushed button {} - state = {}".format(self.last_updated_button, + self.buttons[self.last_updated_button])) + + def setLeftVibration(self, time_msec): + """Set the vibration for the left motor""" + self.gamepad.set_vibration(1, 0, time_msec) + # display info + if self.verbose: + print("Set vibration to the left motor for {} msec".format(time_msec)) + + def setRightVibration(self, time_msec): + """Set the vibration for the right motor""" + self.gamepad.set_vibration(0, 1, time_msec) + # display info + if self.verbose: + print("Set vibration to the right motor for {} msec".format(time_msec)) + + def setVibration(self, time_msec): + """Set the vibration for both motors""" + self.gamepad.set_vibration(1, 1, time_msec) + # display info + if self.verbose: + print("Set vibration to both motors for {} msec".format(time_msec)) + + def __getitem__(self, key): + """Return the specified button""" + if key is not None: + return self.buttons[key] + + def __setitem(self, event_type, key, value): + + if event_type == 'Absolute': + if key == 'LJX': + self.buttons['LJ'][0] = value / 32768. # values between [-32768, 32767] + self.last_updated_button = 'LJ' + elif key == 'LJY': + self.buttons['LJ'][1] = -1. * value / 32768. # values between [-32767, 32768] + self.last_updated_button = 'LJ' + elif key == 'RJX': + self.buttons['RJ'][0] = value / 32768. # values between [-32768, 32767] + self.last_updated_button = 'RJ' + elif key == 'RJY': + self.buttons['RJ'][1] = -1. * value / 32768. # values between [-32767, 32768] + self.last_updated_button = 'RJ' + elif key == 'DpadX': + self.buttons['Dpad'][0] = value # left (-1) and right (1) + self.last_updated_button = 'Dpad' + elif key == 'DpadY': + self.buttons['Dpad'][1] = -1 * value # down (-1) and high (1) + self.last_updated_button = 'Dpad' + elif key == 'LT' or key == 'RT': # max 1023 + self.buttons[key] = value / 1023. + self.last_updated_button = key + elif event_type == 'Key': + self.buttons[key] = value + self.last_updated_button = key + + +class Xbox360ControllerInterface(XboxControllerInterface): + r"""Xbox 360 Controller Interface + + """ + + def __init__(self, use_thread=False): + super(Xbox360ControllerInterface, self).__init__(use_thread=use_thread, controller_name='X-Box 360') + + +class XboxOneControllerInterface(XboxControllerInterface): + r"""Xbox One Controller Interface + + """ + + def __init__(self, use_thread=False): + super(XboxOneControllerInterface, self).__init__(use_thread=use_thread, controller_name='X-Box One') + + +# Tests +if __name__ == '__main__': + import time + from itertools import count + + # create interface + xbox = XboxOneControllerInterface() + + for _ in count(): + # run one step with the interface + xbox.run() # same as `step()` if we are not using threads + + # get the last update and print it + b = xbox.last_updated_button + print("Last updated button: {} with value: {}".format(b, xbox[b])) + + # sleep a bit + time.sleep(0.01) diff --git a/pyrobolearn/tools/interfaces/interface.py b/pyrobolearn/tools/interfaces/interface.py new file mode 100644 index 0000000..f2d53db --- /dev/null +++ b/pyrobolearn/tools/interfaces/interface.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python +"""Define the abstract interfaces + +This defines the Input/Output abstract interfaces. They allowed to get information from input systems (such as +the mouse, keyboard, microphone, webcam/kinect, VR/AR tools, game controllers, and so on), and/or send information +to output systems (such as speakers, game controllers, VR/AR tools, etc). +These interfaces are independent from the rest of the code; they are not coupled to the simulator, robots, or world. +The code that connects the interfaces to the simulator or elements inside this last one are the `bridges` defined +in `pyrobolearn/tools/bridges`. +""" + +import threading +import time + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +def thread_loop(run): + """decorator to make the function run in a loop if it is a thread""" + def fct(self, *args, **kwargs): + if self.use_thread: + while True: + run(*args, **kwargs) + else: + run(*args, **kwargs) + return fct + + +class Interface(object): + r"""Interface (abstract class) + + The interface links input systems (such as the mouse, keyboard, microphone, webcam/kinect, VR/AR tools, game + controllers, and so on), and/or output systems (such as speakers, game controllers, VR/AR tools, etc) to the + simulator. Specifically, it can connects such systems to the `world`, `simulator`, or `robot`. + + They can be divided into 3 classes: + * Input interfaces: these interfaces interpret the signals received by an input system, and act on the world. + Examples include the mouse, keyboard, microphone, webcam/kinect, joysticks, and others. + For more info, see the `InputInterface` class. + * Output interfaces: these interfaces received signals from the world, and output these through an output system. + For instance, a robot could 'speak' in the world, and this signal could be redirected to + computer speakers or headphones. For more info, see the `OutputInterface` class. + * Input and Output interfaces: these interfaces connects with a system that can be used as inputs and outputs. + Such instances include controllers with feedback (using vibration or force), + VR/AR tools, phones, etc. For more info, see the `InputOutputInterface` class. + """ + + def __init__(self, use_thread=False, sleep_dt=0, verbose=False): + self.use_thread = use_thread + self.dt = sleep_dt + self.data = None + self.verbose = verbose + + if self.use_thread: + self.thread = threading.Thread(target=self._run) + self.thread.start() + + def step(self): + """ + Perform one step with the interface; the interface checks the events (if it is not run in a separate thread) + """ + if not self.use_thread: + return self._run() + + def _run(self, *args, **kwargs): + """ + Code that calls the `run` method implemented by the user, and loop over it if we are in a thread. + """ + if self.use_thread: + while True: + self.run(*args, **kwargs) + time.sleep(self.dt) + else: + return self.run(*args, **kwargs) + + # @thread_loop + def run(self, *args, **kwargs): + """ + Code to be run by the interface. This needs to be implemented by the user + """ + pass + + def close(self): + """ + Stop and close the interface. + """ + pass + + def __call__(self): + self.step() + + def __del__(self): + self.close() + + +class InputInterface(Interface): + r"""Input Interface (abstract class) + + These interfaces interpret the signals received by an input system (such as a mouse, keyboard, microphone, + webcam/kinect, game controllers, VR/AR controllers), and act on the world. + """ + + def __init__(self, use_thread=False, sleep_dt=0, verbose=False): + super(InputInterface, self).__init__(use_thread=use_thread, sleep_dt=sleep_dt, verbose=verbose) + + +class OutputInterface(Interface): + r"""Output Interface (abstract class) + + These interfaces received signals from the world, and output these through an output system. For instance, + a robot could 'speak' in the world, and this signal could be redirected to computer speakers or headphones. + The screen of your computer is another obvious output system. + """ + + def __init__(self, use_thread=False, sleep_dt=0, verbose=False): + super(OutputInterface, self).__init__(use_thread=use_thread, sleep_dt=sleep_dt, verbose=verbose) + + +class InputOutputInterface(Interface): + r"""Input and Output interface (abstract class) + + These interfaces can receive and interpret signals from certain systems, and redirect or produce signals to be + sent to these same systems. Such instances include controllers with feedback (using vibration or force), + VR/AR tools, phones, and others. + + Specifically, you could send images captured by a camera in the world/simulator, stream these to a phone or VR/AR + headsets. At the same time, you could capture inputs from the screen (or other sensors) of the phone, or from VR + controllers, and stream these to this interface that will act on the world/simulator. + """ + + def __init__(self, use_thread=False, sleep_dt=0, verbose=False): + super(InputOutputInterface, self).__init__(use_thread=use_thread, sleep_dt=sleep_dt, verbose=verbose) diff --git a/pyrobolearn/tools/interfaces/mouse_keyboard/__init__.py b/pyrobolearn/tools/interfaces/mouse_keyboard/__init__.py new file mode 100644 index 0000000..b90e724 --- /dev/null +++ b/pyrobolearn/tools/interfaces/mouse_keyboard/__init__.py @@ -0,0 +1,3 @@ + +# import mouse keyboard interface +from mousekeyboard import MouseKeyboardInterface diff --git a/pyrobolearn/tools/interfaces/mouse_keyboard/mousekeyboard.py b/pyrobolearn/tools/interfaces/mouse_keyboard/mousekeyboard.py new file mode 100644 index 0000000..16c74d9 --- /dev/null +++ b/pyrobolearn/tools/interfaces/mouse_keyboard/mousekeyboard.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python +"""Define the mouse-keyboard interface. + +Dependencies: +- `pyrobolearn.tools.interfaces` +- `pyrobolearn.simulators` (only for the mouse keyboard interface; we get the events from the simulator) +""" + +import numpy as np + +from pyrobolearn.utils.bullet_utils import Key, Mouse +from pyrobolearn.tools.interfaces import InputInterface +from pyrobolearn.simulators import Simulator +# from pyrobolearn.worlds import World + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class MouseKeyboardInterface(InputInterface): + r"""Mouse Keyboard Interface for the simulator + + Provide the mouse keyboard interface which can for instance allows the user to interact with the world + (and thus the robots) using the mouse and keyboard. + + For IK, you can perform it: + * from one link to another link: select the first link by a `left-click` then the second link that you wish to move + with a `right-click` + * using the full-body: do not select any objects, and just `right-click` on the corresponding link + You can also use the sliders to move the corresponding link. When using the mouse, it will move the selected link + in the plane which is parallel to the camera (i.e. perpendicular to the axis that goes from the camera lens to + the link), and contains the CoM of the selected link. + + Mouse: + * predefined in pybullet + * `scroll wheel`: zoom + * `ctrl`/`alt` + `scroll button`: move the camera using the mouse + * `ctrl`/`alt` + `left-click`: rotate the camera using the mouse + * `left-click` and drag: transport the object + + Keyboard: + * predefined in pybullet: + * `w`: show the wireframe (collision shapes) + * `s`: show the reference system + * `v`: show bounding boxes + * `g`: show/hide parts of the GUI the side columns (check `sim.configureDebugVisualizer(p.COV_ENABLE_GUI, 0)`) + * `esc`: quit the simulator + + For programmers: + In Pybullet, `sim.getMouseEvents()` return a list of mouse events in the following format: + [(eventType, x, y, buttonIndex, buttonState)] + where + * `eventType` is 1 if the mouse is moving, 2 if a button has been pressed or released + * `(x,y)` are the mouse position on the screen (expressed in pixel, and returned as float (they should be int)) + * `buttonIndex` is -1 if nothing, 0 if left button, 1 if scroll wheel (pressed), 2 if right button + * `buttonState` is 0 if nothing, 3 if the button has been pressed, 4 is the button has been released, + 1 if the key is down (never observed), 2 if the key has been triggered (never observed). + + In Pybullet, `sim.getKeyboardEvents()` return a dictionary of key events in the following format: + {keyID: keyState} + where + * `keyID` is an integer (ascii code) representing the key. Some special keys like shift, arrows, and others are + are defined in pybullet such as `B3G_SHIFT`, `B3G_LEFT_ARROW`, `B3G_UP_ARROW`,... + * `keyState` is an integer. 3 if the button has been pressed, 1 if the key is down, 2 if the key has been + triggered, 4 if it has been released. + """ + + def __init__(self, simulator, verbose=False): + """ + Initialize the Mouse-Keyboard Interface. This interface is a little bit special in the sense that we use + the simulator to provide the mouse and keyboard events instead of using an external library. + + Args: + simulator (Simulator): simulator instance from which we capture mouse and keyboard events. + verbose (bool): If True, print information on the standard output. + """ + super(MouseKeyboardInterface, self).__init__(use_thread=False, sleep_dt=0, verbose=verbose) + + # set simulator + self.simulator = simulator + + # define variables for mouse events + # Note: the difference between pressed and moment, is that a key is pressed during a short instant, + # while the key can be down for a long period of time. + self.mouse_moving = False + self.mouse_pressed = False + self.left_click_pressed = False + self.right_click_pressed = False + self.left_click_down = False + self.right_click_down = False + self.mouse_x, self.mouse_y = 0, 0 + + # define variables for key events + self.key_pressed = [] + + ############## + # Properties # + ############## + + @property + def simulator(self): + """Return the simulator instance.""" + return self._simulator + + @simulator.setter + def simulator(self, simulator): + """Set the simulator instance.""" + # if isinstance(simulator, Simulator): + # pass + # elif isinstance(simulator, World): + # simulator = simulator.simulator + # elif isinstance(simulator, Env): + # simulator = simulator.world.simulator + # else: + # if not isinstance(simulator, Simulator): + # raise TypeError("Expecting the simulator to be an instance of Simulator, " + # "got instead {}".format(type(simulator))) + # if isinstance(simulator, World): + # simulator = simulator.simulator + self._simulator = simulator + + @property + def mouse_pressed(self): + return self.left_click_pressed or self.right_click_pressed + + @mouse_pressed.setter + def mouse_pressed(self, pressed): + self.left_click_pressed = pressed + self.right_click_pressed = pressed + + @property + def mouse_down(self): + return self.left_click_down or self.right_click_down + + ########### + # Methods # + ########### + + def check_key_events(self): + # get key events + events = self.simulator.getKeyboardEvents() + + # create new list of key pressed + self.key_pressed = [] + if Key.shift in events: + self.key_pressed.append(Key.shift) + if Key.alt in events: + self.key_pressed.append(Key.alt) + if Key.ctrl in events: + self.key_pressed.append(Key.ctrl) + + # go through each keyboard event + for key, state in events.items(): + if state == Key.pressed: # or state == Key.down: # the key is pressed or down + self.key_pressed.append(key) + + def check_mouse_events(self): + # get mouse events + events = self.simulator.getMouseEvents() + + # reset mouse events + self.mouse_moving, self.mouse_pressed = False, False + + # go through each mouse event + for event in events: + eventType, x, y, idx, state = event + self.mouse_x, self.mouse_y = x, y + + # check if mouse is moving + if eventType == Mouse.moving: + self.mouse_moving = True + + # check if button has been pressed or released + elif eventType == Mouse.button: + # check left click + if idx == Mouse.left_click: + if state == Mouse.pressed: + self.left_click_pressed = True + self.left_click_down = True + elif state == Mouse.released: + self.left_click_pressed = False + self.left_click_down = False + + # check right click + elif idx == Mouse.right_click: + if state == Mouse.pressed: + self.right_click_pressed = True + self.right_click_down = True + elif state == Mouse.released: + self.right_click_pressed = False + self.right_click_down = False + + def step(self): + """Perform a step with the interface.""" + # check key events + self.check_key_events() + + # check mouse events + self.check_mouse_events() + + if self.verbose: + print("\nKey pressed: {}".format(self.key_pressed)) + print("Mouse moving? {}".format(self.mouse_moving)) + print("Mouse pressed? {}".format(self.mouse_pressed)) + print("Mouse down? {}".format(self.mouse_down)) + + +# Tests +if __name__ == '__main__': + from pyrobolearn.simulators import BulletSim + import time + from itertools import count + + # create simulator + sim = BulletSim() + + # create interface + interface = MouseKeyboardInterface(sim, verbose=True) + + for _ in count(): + interface.step() + time.sleep(1. / 2) diff --git a/pyrobolearn/tools/interfaces/robots/__init__.py b/pyrobolearn/tools/interfaces/robots/__init__.py new file mode 100644 index 0000000..33db34b --- /dev/null +++ b/pyrobolearn/tools/interfaces/robots/__init__.py @@ -0,0 +1,3 @@ + +# General import +from robot import RobotInterface diff --git a/pyrobolearn/tools/interfaces/robots/robot.py b/pyrobolearn/tools/interfaces/robots/robot.py new file mode 100644 index 0000000..d317169 --- /dev/null +++ b/pyrobolearn/tools/interfaces/robots/robot.py @@ -0,0 +1,11 @@ + +# Import general interface +from pyrobolearn.tools.interfaces.interface import Interface, InputInterface, OutputInterface, InputOutputInterface + + +class RobotInterface(Interface): + r"""Robot Interface + + This interfaces receives data from the real robot, and cache it here. + """ + pass diff --git a/pyrobolearn/tools/interfaces/sensors/__init__.py b/pyrobolearn/tools/interfaces/sensors/__init__.py new file mode 100644 index 0000000..9599ab0 --- /dev/null +++ b/pyrobolearn/tools/interfaces/sensors/__init__.py @@ -0,0 +1,7 @@ + + +# general imports +from sensor import * + +# EMG sensor +from emg import EMGInterface diff --git a/pyrobolearn/tools/interfaces/sensors/emg.py b/pyrobolearn/tools/interfaces/sensors/emg.py new file mode 100644 index 0000000..9c92d3f --- /dev/null +++ b/pyrobolearn/tools/interfaces/sensors/emg.py @@ -0,0 +1,9 @@ + +from sensor import BioSensorInterface + + +class EMGInterface(BioSensorInterface): + r"""EMG sensor interface + + """ + pass diff --git a/pyrobolearn/tools/interfaces/sensors/sensor.py b/pyrobolearn/tools/interfaces/sensors/sensor.py new file mode 100644 index 0000000..b4a46d4 --- /dev/null +++ b/pyrobolearn/tools/interfaces/sensors/sensor.py @@ -0,0 +1,16 @@ + +from pyrobolearn.tools.interfaces import InputInterface + + +class SensorInterface(InputInterface): + r"""Sensor Interface + + """ + pass + + +class BioSensorInterface(InputInterface): + r"""Bio-Sensor Interface + + """ + pass \ No newline at end of file diff --git a/pyrobolearn/tools/interfaces/suits/__init__.py b/pyrobolearn/tools/interfaces/suits/__init__.py new file mode 100644 index 0000000..82edbeb --- /dev/null +++ b/pyrobolearn/tools/interfaces/suits/__init__.py @@ -0,0 +1,6 @@ + +# general import +from suit import SuitInterface + +# Xsens suit +from xsens import XsensSuitInterface diff --git a/pyrobolearn/tools/interfaces/suits/suit.py b/pyrobolearn/tools/interfaces/suits/suit.py new file mode 100644 index 0000000..f13fe1e --- /dev/null +++ b/pyrobolearn/tools/interfaces/suits/suit.py @@ -0,0 +1,9 @@ + +from pyrobolearn.tools.interfaces import InputInterface + + +class SuitInterface(InputInterface): + r"""Suit Interface + + """ + pass diff --git a/pyrobolearn/tools/interfaces/suits/xsens.py b/pyrobolearn/tools/interfaces/suits/xsens.py new file mode 100644 index 0000000..6a00171 --- /dev/null +++ b/pyrobolearn/tools/interfaces/suits/xsens.py @@ -0,0 +1,9 @@ + +from suit import SuitInterface + + +class XsensSuitInterface(SuitInterface): + r"""Xsens suit interface + + """ + pass diff --git a/pyrobolearn/tools/interfaces/vr/__init__.py b/pyrobolearn/tools/interfaces/vr/__init__.py new file mode 100644 index 0000000..5b72fc8 --- /dev/null +++ b/pyrobolearn/tools/interfaces/vr/__init__.py @@ -0,0 +1,11 @@ + +# general interface +from vr import VRInterface + +## VR interfaces ## + +# Oculus +from oculus import OculusInterface + +# HTC +from htc import HTCViveInterface \ No newline at end of file diff --git a/pyrobolearn/tools/interfaces/vr/htc.py b/pyrobolearn/tools/interfaces/vr/htc.py new file mode 100644 index 0000000..da91543 --- /dev/null +++ b/pyrobolearn/tools/interfaces/vr/htc.py @@ -0,0 +1,23 @@ + +from pyrobolearn.tools.interfaces.vr import VRInterface + + +class HTCViveInterface(VRInterface): + r"""HTC Vive Interface + + To install: + * SteamVR + * OpenVR + * PyOpenVR + + References: + [1] OpenVR: https://github.com/ValveSoftware/openvr + [2] PyOpenVR: https://github.com/cmbruns/pyopenvr + with wiki: https://github.com/cmbruns/pyopenvr/wiki/API-Documentation + [3] https://github.com/osudrl/CassieVrControls/wiki/OpenVR-Quick-Start + [4] https://github.com/osudrl/OpenVR-Tracking-Example + [5] How to use the HTC Vive Trackers in Ubuntu using Python 3.6: + https://gist.github.com/DanielArnett/c9a56c9c7cc0def20648480bca1f6772 + """ + def __init__(self, world, use_controllers=True, use_headset=True): + super(HTCViveInterface, self).__init__() \ No newline at end of file diff --git a/pyrobolearn/tools/interfaces/vr/oculus.py b/pyrobolearn/tools/interfaces/vr/oculus.py new file mode 100644 index 0000000..573ef72 --- /dev/null +++ b/pyrobolearn/tools/interfaces/vr/oculus.py @@ -0,0 +1,319 @@ +# Define the OculusTouch class which communicates with Unity (on Windows) using TCP. +# Currently, Oculus has only support for Windows systems. However, several +# libraries such as ROS only runs on Linux systems. Thus, we can run Unity on +# a Windows system, associate the Unity scripts with the Oculus GameObjects, and then +# run this file on a Unix system (such as Linux or MacOSX) which will communicate by TCP. +# The scripts for Unity can be found in the `unity-scripts` folder. +# +# Currently, this code is the server while the code running in Unity on Windows is the client. + +import Queue +import socket +import struct +from threading import Thread + +import cv2 + +from pyrobolearn.utils.bullet_utils import RGBAColor +from pyrobolearn.tools.interfaces.vr import VRInterface +# from pyrobolearn.worlds.world import BasicWorld + + +class OculusInterface(VRInterface): + r"""Oculus VR Interface + + Hardware: Oculus Rift headset + Touch controllers + """ + + def __init__(self, world, ip=True, port=5111, use_controllers=True, use_headset=False, use_threading=False, + rate=None): + """Initialize the Oculus interface. + + Args: + world (World, Env): the world + use_controllers (bool): True if you want to use the controllers (stream commands from the controllers to + the simulator, and send feedback + use_headset (bool): True if you want to use the headsets (stream the pictures from the simulator to the + headset, and the headset position/orientation to the simulator) + ip (str): IP of the system to stream and receive the data. This is useful if the VR hardware is not + supported on the current OS. For instance, if you are running on Linux and the VR hardware only runs + on Windows, then by specifying the Windows system IP, the data will be streamed between these 2 systems. + Note that when streaming between 2 systems, they have to agree about the data they are receiving and + sending. Thus, the (code of the) interface on the Windows system is completely dependent on this + interface. Use the corresponding code available in the `vr` folder, and follow the set-up instructions. + Be sure that you use the correct corresponding interface on the other system, which will depend on the + two previous arguments `use_controllers` and `use_headset`. + Note that we currently use `Unity` as the interface between the VR hardware and the Windows system, as + much of the code is already provided. A global overview of the set-up can be depicted as: + pyrobolearn (Linux/Mac) <--TCP/UDP--> Unity (Windows) <----> VR hardware + port (int): Port of the system to stream the data + use_threading (bool): True if you want to use threads to send the pictures + rate (int): acquisition rate of the camera images + """ + super(OculusInterface, self).__init__() + self.MSG_SIZE = 0 + + self.use_headset = use_headset # TODO: if we use the headset, we automatically use threads + self.use_threading = use_threading + + if use_controllers: + self.MSG_SIZE = 304 + + # Simulator world + self.world = world + self.sim = self.world.simulator + self.task = None + + # create visual spheres in the world for the hands + self.worldCamera = self.world.getMainCamera() + V, P, Vp, V_inv, P_inv, Vp_inv = self.worldCamera.getMatrices(True) + camera = self.worldCamera.getDebugVisualizerCamera(convert=False) + width, height = camera[:2] + posL = np.array([width / 2 - 20, height / 2, 0.95, 1]) + posR = np.array([width / 2 + 20, height / 2, 0.95, 1]) + posL = self.worldCamera.screenToWorld(posL, Vp_inv, P_inv, V_inv)[:3] + posR = self.worldCamera.screenToWorld(posR, Vp_inv, P_inv, V_inv)[:3] + self.leftSphere = self.world.loadVisualSphere(posL, radius=0.1, color=RGBAColor.red) # red + self.rightSphere = self.world.loadVisualSphere(posR, radius=0.1, color=RGBAColor.blue) # blue + + # Check IP: get IP of this computer if not provided + if ip: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + + # Threads + self.running = True + self.queue = Queue.Queue(10) + self.threads = [] + if self.use_headset: + thread = Thread(target=self.runThread, args=(ip, port + 1)) + self.threads.append(thread) + + # Connection over the network for joysticks + print('Creating socket') + self.sock = socket.socket(socket.AF_INET, # Internet + socket.SOCK_STREAM) # TCP = SOCK_STREAM + self.server_address = (ip, port) + self.sock.bind(self.server_address) + self.sock.listen(1) ## Only for TCP + print('Waiting for connection...') + self.connection, self.client_address = self.sock.accept() + + # VR + self.head, self.leftHand, self.rightHand = [], [], [] + self.leftJoystick, self.rightJoystick = [], [] + self.leftVibration, self.rightVibration = 0, 0 + self.vibrationTime = 1 + self.prevOculusHeadPos = None + + # Camera images + if rate is None: + rate = np.inf + self.cnt, self.rate = 0, rate + self.width, self.height = 400, 400 + self.encode_params = [int(cv2.IMWRITE_JPEG_QUALITY), 80] + + self.leftCollided = False + self.rightCollided = False + + def recv(self): + data = self.connection.recvfrom(self.MSG_SIZE) + poses = data[0].split(";") + for pose in poses: + name, value = pose.split("=") + values = np.array(value.split(",")) + values = np.asfarray(values, float) + # TODO: replace the if-else by dict of functions + if name == "H": # head: position + quaternion + self.head = [values[:3], values[3:]] + elif name == "L": # left hand: position + quaternion + self.leftHand = [values[:3], values[3:]] + elif name == "R": # right hand: position + quaternion + self.rightHand = [values[:3], values[3:]] + elif name == 'JL': # left joystick [touch, button, lateral, forward] + self.leftJoystick = [values[0], values[1], values[-2:]] + elif name == 'JR': # right joystick: [touch, button, lateral, forward] + self.rightJoystick = [values[0], values[1], values[-2:]] + # move the camera by rotating + yaw, pitch = values[-2:] + #self.worldCamera.addYawPitch(yaw, pitch, radian=False) + #print(pitch, yaw) + pos = self.worldCamera.targetPosition + dist = self.worldCamera.dist + elif name == 'BA': # button A: [touch, button] + pass + elif name == 'BB': # button B: [touch, button] + pass + elif name == 'BX': # button X: [touch, button] + pass + elif name == 'BY': # button Y: [touch, button] + pass + elif name == 'BS': # button start: button + pass + elif name == 'BLT': # button left index trigger: [button, trigger] + pass + elif name == 'BLH': # button left hand trigger: trigger + pass + elif name == 'BRT': # button right index trigger: [button, trigger] + pass + elif name == 'BRH': # button right hand trigger: trigger + pass + + ### update world ### + + headWorldPos = self.worldCamera.position + targetPos = self.worldCamera.targetPosition + forwardVec, upVec, lateralVec = self.worldCamera.getVectors() + if self.prevOculusHeadPos is None: + self.prevOculusHeadPos = self.head[0] + + # update world camera position and orientation based on Oculus headset and joysticks + #R = np.array(self.sim.getMatrixFromQuaternion(self.head[1])).reshape(3,3) + #targetPos = R.dot(targetPos) + #targetPos += (self.head[0] - self.prevOculusHeadPos) + lateral, forward = self.leftJoystick[-1] # move the camera by translating + targetPos += 0.1 * (forward * forwardVec + lateral * lateralVec) + self.worldCamera.targetPosition = targetPos + + # update hand positions in world + leftHandWorldPos = headWorldPos + (self.leftHand[0] - self.head[0]) + rightHandWorldPos = headWorldPos + (self.rightHand[0] - self.head[0]) + #self.world.moveObject(self.leftSphere, self.leftHand[0], (0, 0, 0, 1)) + #self.world.moveObject(self.rightSphere, self.rightHand[0], (0, 0, 0, 1)) + self.world.moveObject(self.leftSphere, leftHandWorldPos, (0, 0, 0, 1)) + self.world.moveObject(self.rightSphere, rightHandWorldPos, (0, 0, 0, 1)) + + # change color if hands collide with an object + self.leftCollided = self.updateSphereColor(self.leftSphere, self.leftCollided, + RGBAColor.orange, RGBAColor.red) + self.rightCollided = self.updateSphereColor(self.rightSphere, self.rightCollided, + RGBAColor.green, RGBAColor.blue) + + # get pictures for the eyes + if self.use_headset and (self.cnt % self.rate) == 0: + self.cnt = 0 + # get picture for the eyes + leftPic = self.getEyeRGBImage(headWorldPos, targetPos, lateralVec, beta=-0.32) + rightPic = self.getEyeRGBImage(headWorldPos, targetPos, lateralVec, beta=0.32) + #if self.use_threading: + # add them to the queue + self.queue.put((leftPic, rightPic)) + #else: + # # compress pictures and send them over the network + # self.compressAndSendPicture(leftPic, self.connection) + # self.compressAndSendPicture(rightPic, self.connection) + + self.cnt += 1 + + def updateSphereColor(self, sphere, hasCollidedPreviously, collisionColor, freeColor): + aabb = self.world.getObjectAABB(sphere) + + if len(self.world.getObjectIdsInAABB(aabb[0], aabb[1])) > 1: + update = not hasCollidedPreviously + else: + update = hasCollidedPreviously + + if update: + hasCollidedPreviously = not hasCollidedPreviously + if hasCollidedPreviously: + self.world.changeObjectColor(sphere, color=collisionColor) + else: + self.world.changeObjectColor(sphere, color=freeColor) + + return hasCollidedPreviously + + def runThread(self, ip, port): + # create socket for image + sock = socket.socket(socket.AF_INET, # Internet + socket.SOCK_STREAM) # UDP = SOCK_DGRAM / TCP = SOCK_STREAM + server_address = (ip, port) # ip and port + sock.bind(self.server_address) + sock.listen(1) ## Only for TCP + print('Thread: waiting for connection...') + connection, client_address = sock.accept() + print('Thread: connected') + + # run thread + while self.running: + # get images added in the queue + images = self.queue.get(block=True) + # compress pictures and send them over the network + for image in images: + self.compressAndSendPicture(image, connection) + #time.sleep(1./60) + + def getEyeRGBImage(self, headWorldPos, targetPos, lateralVec, beta=0.32): + eyePos = headWorldPos + beta * lateralVec + eyeTargetPos = targetPos + beta * lateralVec + V = self.sim.computeViewMatrix(cameraEyePosition=eyePos, cameraTargetPosition=eyeTargetPos, + cameraUpVector=(0,0,1)) + pic = np.array(self.sim.getCameraImage(self.width, self.height, viewMatrix=V)[2]) + pic = pic.reshape(self.width, self.height, 4)[:, :, :3] + return pic + + def compressAndSendPicture(self, image, connection): + retval, image = cv2.imencode('.jpg', image, self.encode_params) + image = image.tostring() + connection.sendall(struct.pack('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�@R`(#{a`95TR8@lJQyoX6<-pg{cY%Al$ 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

(~diO?pARYiSoOYmQFo; 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 literal 0 HcmV?d00001 diff --git a/pyrobolearn/tools/interfaces/vr/oculus_windows/meshes/RightHand.mb b/pyrobolearn/tools/interfaces/vr/oculus_windows/meshes/RightHand.mb new file mode 100644 index 0000000000000000000000000000000000000000..86c8eceb03b778b7a5ea67fb142192d7b549606b GIT binary patch literal 536180 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�oM;&Kc?^=p}w%EFj<~(&#sV|q9<4xYMrC!VL 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

-y$pJNGzmzY$n<;nMAOlq>t1)*bZVl(RpqWdGJjOa7TO z%)c%q$(g|Vp`~*!>eK6=sb$yCtz<2`UWPh~JL6~bGXI<*(>JSo24w9$S3D`r>)%?I zX^?w&qIqS);kLaFNz@c-H+G&M0+la~S`MA2b`_f|sh8tc(R;~#NG+>sqW)LT{JnSZ zg0N=dh;}WXPjPx_gz)$*-87uuyAbkKh5_P3X^-??Vr zkh$c$rHRG0nta7w%SF_CO#u_-J*L2VmrK8EJmGl!*Rs}iXpCBRy$p2}cap&VeMaqX zRdL!TIG3Z_4zC^_B76DoLu#``HQpQTxYR53 z=lAa;c~Z(kZWarKNb_PtBSeL256&!4ETIBPR-&tvnL&A*4`D?G_B{PG`x^;+y~mNc+h z{MTl8jqA|DF4v*56k}e7%EJM=V?Og|F07U~aNGV(30iu6v)U2fKU|~ndL3%1oH;#V zlz1KfYamqlA}AmPDqrq3_1|8Hx~;SFo|CLW9w;dCLVc>B$YUPDa@hSJ)nMZC;(490 zd?zT`z6^=W!7g;_vA=| zNgW)y`iE=p|MYdMk4fF*-woa!Mtbajw#z!#ktVIr`-_cNJB!V7&=99E)V=m&yKc2d zgzdiDw?;E%O{;a58IQ-?4~I6bHfnu$p-#J&kK7oZ<#~dH_9OlN&N4qoVf&+0i+z$V zj9M=r-OTl1ex2<0T}h|4D73zx{lL$k{hGRCTBm>ZEh%QceR0QVf3$pmd2`jH%JEFk z2Ugk2^L^?otmN2R{x9c_56jeTe5tcL76vw`mqTx@_}f2I%dUI9tYz2tV2(ebU(@t|ShQ@V%*QoL;cI@1(Q#ajB{mnDa8~d4P@Awr4+^ z>WV!=KkPyA<&BlY-4abZ*;77XAdH5Us{L(?5v`_#Wyzm1`}9290_&#=T3Kr6f&Wo0 zyS@i=6nC4a+inIlH&xj%|8sk~zWvS$um61yCbOWE*R8&*%L`61y?zu3E%E+guJlm( z(x~OofzM}qkKcWw*stm>{QvqrSZIjx*S?!(rwCml6}?eTWm}b{K-yqoShJSxCmVGM zO1tDo<>;jRCo0~kt~~t4a%|PP*{Z9|#zz4U&u&ckg)(UKzRRb_O}C%Q)bHYfE+-=U z{SE)~UU* z_x`!+4*Bo8EC*QBviyMa@p$vymX|g{cPV**`sqjYwcC6spLQW*}g#Ut?#B!pV!)cVeW{KqyYh8 z##fmtoon@_P^VqX7j_Ql+;z!yY;d-Dl@m{v{WG=f`W{Tyvg>uLqqs+QFTFkV@Zr+qCx2ggNQj53l9WVHZ=6 zXmF)^=vv2h>xl{-YHupvOQ_SX<>Pz2$8;>(Q8{<2#_7>Pxt04jzx!%gY3H-YI_0|V z*6Kr7_t31j3YGpx zwd{J`>L~7CHy3;GeZ8vHR<`XkF-4vG>Nhz4tz|jDvX)Js&k>A0--J?O&=Rjs3=zD1 zY1Q(^R)3Y;vg}r|xv~WR3=MI}3gUi^5!=17Vx|60fALiC(a<$g(JT9ei^cm-Pwy-A zXxH+I&fbUWHMS_{e;od6lM!!~`*TjMN_{zMUA}gLWaJRO7bPt|A%VN^*hr124rS+upR%quXnsh3d~Vhm!59Lc<^$VpouT; zZ3>ky_x0|-eNW|PSAF*SQjMGs^a)2ezM!QqHeQ;PTeEg;#`S&Ib@NUScv~QY{Xv_6 z`}Q+mD3=oVOuz4NOGhl+k8RN8;o@drDH9X*?NzS%@5=rumnwgMJf*WgtklTG8~hS} z6_#h{*il_Cw+pOSYhsaXdp|*K{-yC*Y;Kp@Y!e*R#r;#Tv*QX^7;dXxcFK+_^Aqi< zRQdmkGs{J z_^iObB+>MyrXB0;9hV(jobt(4yZyZR_~qJPY1$3;U^m!<-C&1ZH$P+jzO4Puo4psx z3{9;(PWe~cQij6HyMtxMOdoYsIHWlTT-HhHw5R|6Te+~n1AN;cTa&OH&&$p`5H=*R z{-Poi7no|f=yQwjzx>X(u6xbOmA`TOt&FsTI*6aOsLaQ}t%Ch^>E86p>znmm#kt9v z?0T1?MOcg)@afR0y-tjtyr_b@`R3crO(6PjCg>KHsb0B?yAL59n)PlKT%}p3W^Gyp z>slPz5`NrXu+rS;86N82jKq+e4WE`Bf}{`5&+w@`*EKj5{f+#KCzYqHQ%su~%)n=_8o7kGEo?M&VbTzJ7wT!zznL`}@)QnQ8>sPUc z{RjQ_^3xStulA?Ednz*EmMebeEc!2YxY?>)vwDSUZn38fY2G2IQzw71(=DA+ym5u8 zR~`kW==M!%H{I42U38fPKUd17cck7;clTNqN|tfc{dq`DaY0b4yzapuL;B~{5>*{i zICYxrSxY-UH+T2n7SuLKZl(?s?)}za@jDQYoxL%}C105;wJW;$kS?TSv+k{egPL{V z$6q@8>vnV~`gCb)s{v^#hHg^}1hTP;5iJx$BDiPGp4_k2S z4$%_k)~}AK)U5vZAB<@#=a~BIPBvfpI#K4Km8UK_b}K=1dsS73n~hIDrg?Q|u1;)~ zF4y34b35A}RI9x8E4MK<)UIB-w)-6WGDQ3mhPYB_(ml9Yr*0jnlKQ&hW$*MXy>WHr zNjopT?O!64-!+Pn)9%eWu>$Jy_6&Fy^O_PzN%B(WJd9CPz4s^6i& z?R=m1LET+X*SVb^+3&%=Py5bsobTxOcz*W|vD> z&N1g@w47s~^}XHHnW7liD$Y6NVebAr$NrsTat=ESD8?G;m_r`xCw}HwLtVFaUvw+| zSHtpKCM58hIc~i5e?G^e-lAHLh56{MfzA~DJIA8`UY}3$ufo#P=`)NkJ9<<3eWk1&OR44jzum_orxe$U_Q!il>Wq0DAJxQIhyJ}* zjJ?MG-N!!lJ{IR3YoOcpeEq_4eXay<&QQH~z_mj5|8sL}!=#X2hiaZs{rY!~Mf=^^ zfA_Kf{rj;v=U78sh0O&{7a!TP)T&gIDmUwy?&tq&b1ck~Jz!9yaK{%|{+(n0&M}vJ zW!y9RU*7Y0oU>f5((d;a(}r|wAJnOIhaTOy>+G*2<53#7 z8unIiE9o1$9k=!D&?wKhU-#7IWJ-$ni*DDJ$sGL%aj*GQQvci5-xi43>q#!3kh}SP z8*A_R^6RG>_Wa|pC;C-;8~yfO+ul312YXUZ+Iv`H`1K{nE($pfdwJuqC;C-;;bV5R zEq67a2YXUZ+Pi$CZ|>mY(>&MaJjQ<EDx}|h5AjJk8Q-SF$?7$j)+9DL>DP^JXJ6JHE9*fc@A;j6 zd8jMVuljYhjDMT1T|Cv5l#})@^_=aWChcz?@=(BO&qE$WziQ8(@YSJ=#V31+Ps+(W zjNP*5fo{V3*yN;N_ZNJ=?BGU=2fqqB{qm3p(XaZ|&(LmkxrW0$*pqV7Uc}y4*>89I z#)G{=ao7|6s=X0e=j0pMu$u>aQckt^bF&}57JsftbAA3WFQcl`CSh3Q^iBmdi_?4XXNXq%k1?fhrIV18HPZZOy1jr%cSlDpP&7ROB`yGy1 zL$@Tne;Yj><)mZRhvqKSs;D`p+-H&cRL2fq8{Vo>*7q^;CH;zA06q8>wE&*JF!Qqu zGgHL0C;gXl(yyJ}Urpj)OgZV7lvDk>5_BgW|Fr#zTqr&G6}3?A9lv?_xGTTMv?u+N za?-D9T^}y#nICiRd6j-iIn}Sb^-Fb4U$w2aUy%!{F@_##FKR&zw+COEzo%48dr@-I zuTlMXuL>V~B&MA7OX^eox-_9gvYO%#Zn@Qki=1338a>iK>5zj5n%&kxIo_@dw3{C3 zSJVPMHgVf3O*U?b=~tAT^uPY0(C~Xt#0Pb5`lMe{pXz@^-`cu{;*VZx`4zc9d&q0l z0=;VBj3O11Cih@Z%Bl7a5B)P$&m=Ro{Zi!u#E!P|e#sdvuCI$tPUiK-+2o-q`aab5 zKXSqM;D6MDe>B<9eD$k#@Zi6cQ~ke~I9KxC<9OIiE3c}Y^nY#ZFQSI{jH0(|B)Mm2mhlshV9jNuXreSLLMtS6g--<5!C>rD@eqi?~t*^>9lyiMCL zRZjXpVfyBJ_9s8ZCMW&hUHjB$x?;Sq)AB!Z?_WR6vHJgn|D_@cO8y8}3dC$Q9ombtD@)hyVFP5l?Q!h&_xVA*; zVF+K9V1-ya#7j$Wus^g z$#K2)-L|_qFY-)}XMHju^Zd`2w{>M>)bc-aAMlX>sC{7F>@K4UB@)lrYuR&I-ya6A z+qd927gV0*q+daEvLw&hp}V$UVjY!lthLvdymFnD*RktMby@IDSTPTNsq3*6Z28Ny z7Vmow(DqA}lX<;4uzA^T54j%E@?Vvc{vRDXt-#+M^Jx1YxxUBDYgRGd>Uvy68XTYI z)Pmu|LzM?-`j)Gf@rAOX*qWA$%1zZAugG;b<~&R%^s4Khco;LYa=O;HwxHZwlpCjO z^!}!8CM|sdk?Ua0Jm*HA?B`)8>1T^|*#{K4om<)eAYt+42j(ev8?M~C`Fu;I^~?(2 zzO;sD>W^H{wEYcG*E8`jRc7UCxgqO1B%G*R3Vt~~YxpHieX*=VBK`om4#|hJGAp-} zFFx5L=TznR$+h)pYU+zze?0i7u0N`OH+~=8sO6W6a(cw=iUl_e*3=hyz3M@qx~{1D z9&{hQpq|h?`m!_6+|z3;&1233u|BGDGRtQo79QMhSQ?w0 zte#7)9xT04enD(8^ zU(`OKWmnG2xaIPR`KVrZMCsGWYra_Km55XJt9l}CC+qd~y3;ZJ7yFJXr(VCbz5m1U z%Gq9Pj$^)9=AE>s&O0%SA0+G6D}Ux}9_&du)tVwsoHUetBL#ozXX zVT_ks-er7JPPKP+{;auDPAi|W%v)(M>U!W*gWh%c=fR$oQ|;ZXRk{xUwDTD`ueIkz zzNmS9dsX)I(rwN7q?~H+*n;I@QqV0A(tkP63W)KEsw*uyS@-{eoUHrUvv6>#YcH>y9+* z3Tuv2{y65D>~AFP%|A4~Lx~I?-Y2MXs=Y^l6bLGo=0ObqMZWT@^FuDckpty6PEt;- z>$v6AdWS-CUEcBnzp7qRG#Wu2FLWgSQA9X~t4$st~4l!(45MmDjVET+du5)7SPj%DGK-w#*Ni7mMD=>$aHnolWFdohRs(b86J%K0=rB z;H#q{g8ts9dl=O5(ip!2k@H8}Z`t3--|6>_?t2T&Ua(y`^I6x4yEb!4EMHoBBkMn= z-{Ly9fV_UoS44Wf@$Vu1&kk9kJm?p8sdkkI%H;m1ze^TyTdOV##Im1=>p0opC|?oz zZZf>cx@3h|;`Yuk$d#GK-k;>Igt*0NjP)t)_9Q#3m zsQsX&Pn}+g%3L6W8uL_@bF4G>pD+6%Li_!7fynt6GtXjOll|)YH{<6o^6&qyL`-{8 za&kTTaox&NhZB^GDJRwsRi9%$3N6&NT*xWS^{+tW{EhkkK-!b@S1#{$^uvO-EZgqE zo;uH^VBdD*53M=LCq7#Fa;yhpqnfy=k?Vv9|K&O% z-Rk^hXu6iAYkBZrT~DOoxa}>PG%K7?+b_quBm5uqoz4DBHpW{kPE}6zKXv1QEra>G zik6&Xy@|GQYt7Th^~J;dSJxNkI{T=};NQ-5?Vv2PoUGaQu0qV>rJ=WAEbEFGf7!3r z<zMi5HhbpIDr$ns0`?Sx)g4*^X*PEDeNPBXUW zYMd+3{AwO;d$FuT(w^LprM(-Q=cLCNFRl30eOtawK0S74n3U7XXDsWKv?tdoY43VS z&`jSgb`SQXoEqQR_c{098$DP%zBtygf>G<(1#?h^k+U`4=ZN(}l~e6q>vyox@BT5r zCn`R#DHyqq#hh>AeuC_G-cM+n<=OsCoi8X`PSrX(s&;$j{_~uPPJA|4`6;~GqsEVu zYu01rdK9xh#5!M4T|dLpLPczb5|I+OJw=dmoa$jd`5xkB=Nq??>ZJr;^(zKMLw{ziG#_>UzUHojEy zqRQ=OTMJ%()?6#@Lb1#zsZY)m$3A?b+`wL;8yhRPE{2t!xA26fzF6jq)F=4D&5e@UlivB}Bnl!w!U5-t8l{I;5wU$N|K z(l0q5Y z%H4_Wv$ULXK+FHaam+8-@6>mo-A}b@|29*(+p1xX?8h5u>WgE36_)eM;oq5oMTk^86z2L%%5z*DPN)d=a3jFOKXRzvO(Del1^jCQqNuQ)1ea z{!2OO*Ms*vPuQpa8dFaCCFNAV7FN5z;Kz&KY5NtqF2r1Cq&+#`rM)Ymo9hKGJm|rm zlvC}6XIOBu$;z49_TrfDg`?*CsH*SVzieX7Eq+J4GI{wtMjO)PQAjW&8* zZu-^u)eFREH^emkjGT~0_i+^S$3IWhgq+d-vz=9%v&CxKbD3y|cC;Kat@8NTzhL;J=)ZvJ4k3o8M@9JwDjg^xt*jzL?l} z%*L{@$w~jmjk13J)3YCAlav0RZ&F~!!lie$>pyZK@Zf*cLQs3r>`VvRocG|rlvC$h zjqSZNl^RxD+b>m4`rn~RHBvBm1Ln$enKF zH+XbV8|B7{J|(sstm1k8_(!e_G1jZ>qAq2>)GIF{mu30oewTaol!t2%Reo2vxiY0e z#*ua3vxbS6R^2m$V)t8JKk_?nRNmOPZEoPbYs%?PgY5&d7ghDT#Vh9>ZyotyOTD`p z+q%R3<=E}3>x0w&g-kiq?W%88u5}7}oaT?eRr{K8xm-xN`KOMDx_&tI-mJ4eeW{cK zlvA~TviCj8ds*?))Ejw0p?9wrRj<0H*)>)pjTa2 zoOf!t^^NbOs`buKc=3(0qwCe*j=mhC+$+6n+N{SLl<~_fMXTp} zp;Z_1fhflZ(Qfr7_Yd`fDY^d6tJ!19T=TgY>z9k1Ty7rSJG*$%`-$3hC?9}wJTRka zPtHGS?@Z%qgL>>A;lZ9dPsQcQl0_xv^{I6)HaWSCZc8^Z!_}Ag<=L+E|8kZ~&8~Ii zgJ{isMb7&eNSpaCU5k0W^0#z_43}$q&%qcxX0)7FRFUo#z(Hp>Vs6#*JVvPbzO*dXF^j>o#)Zy zw8lH?!6WT?tv-0&!8 z?C{GzD)N+PSybN z()2fSerVTMfLizHl{MX0sT4FhFq`sdNY4Ym6-uitY&ye|?RnN%^hegOhw)JNN2lL^ zJU%nM)bKaTEz9J9??MO1qStfXrWfn1d_c%~1LXAXA1iO@=A+!0U8KRHa(y-P5VOFZ==bIWzY4FW%zyXdN?%K1O}(D?Mb%GP|Bf3gM^hRq zJ^3j_xl{OkuF^BoYwGj7AFBG~JaOE(c=)KpsWoS2Dr-hhui3kFRxN$^?mg9cGE_Em8_NadaKIGxv@OUqV3&>4UbJu*80-*Im_g)%6%2L^+@g;sy?}X zpIF^}+26CmJoxW<-l}?{_RrPBW}ePoFW7@UDW~q&mc^xJHyr7~p6B_j+LQBH?sr!P zd{*}Mc0O2ji%+iK`P6wY-?xMZJigKPPV(5~q+<;&Ps(-rtg!YtMy>-M#!;>V(yxp`YGpKugsdN}Z~*vUp9^zNX$d>Qz2KrLLRf znv^`aZd=Uz!?Mm)IrWD0;K47C`+LV27gbKynY^9vEFbqeQ{`lx{ReWg&f=C+A3%yO zrVIiId>Ywh#;^VZsNs&XUtaPeS=$^sy=m{j3%d^uA|QP7k3x; z@p^G+va-K*ikImR^PMsA($wqux=z)ruDedXdnYelu^_X*axc}XlV=M2pkuDQ1)|q@3`L+@jOGI;i$J#YwN(5%^shOMX%?1q57+?^G?0@e%zPm zM3SS*?qgSqZCbx07QJGAXkS08dewcvncw>jPKM3N+FIE$ZN=LknzxBXZ{&RPkOxQo zJMK?C=sdGut1~&3&>@*qA1^m27QK=CLX3GT@AEnybaCEKIJkT9n$EqxRPGwDtUA)X zsZy(Br5pouag8lrn&T(tk@h;P>R0EPQ~%CoJv(e%pI^Ceozk`6{Mky4>8+PHl^;2w z->qKddQ?!(FS(9H?lYQxM9!xeajJUce3E*0djE9hz*fF^u4zxo$z|sDwD$c64CkGS zXF0hqY|YWV<@Yss_*Kg<`2dY}exzUO12)pH3tyyf#6N9&s+{!eVD>pP`4^j<^y^Hu z@%8xU!LP{qter3QfgS1Bwl0?^^bA_z!JeG=(yz-wi+*jtGB7qd>DRfxGcGI8E9UnZ z7M<{wUL{SFP}9_ zD=)4O?6}1#{Zk*JyqwqS13A(2qA4fq^goc(s6S18 zvc970(~LKcx>6sAag4{M;=g_O^wJXLYO{>Th9}OYsV|PYQXgP(JOJ3a+RkEuuW~36 z;cJTRv}<117LMFkw8vNWJ0B=Kx2Dt9KLf5Q*R$@MXTFtC)4w?Cudu8?hkwhfwfgEs ziml3{Z+upE89P)n&$7PcWovBvQr@WNLNw!0`<-z#x;de7@1@MQKWznT?98bVN8UV< z>$V4day@p$u|NHW`DgtuD|a`Y_^IMsA5DFc*IzN#TX7#WZys5n{_5vfoiFZOQM1Vk z<>su@cN#58t(+V_ZOLPswqDQIUt&Jx%_HmC;pYXzg$}+;e^9OsNK*0d<)NB>#=frR z%_Hkx{`Hv5%9a;TQ&g#VS$VMCYxZY#cWLU2z5es&k?X2M-_hCkcQ&6IuG~pEEMeA$ z6*Tq5UjKRX$aU2b@1{w0>V0K@|NpV~9RN`s&Ese^c15F!CF+SKwlgYV*9U^Vp`gYV zJ-7o-k2|~r6)VPGVvpU#-g|HHu=g5!1yn2`Xe_b&-#7c-ySsO@??}Ec>i7G@_aztD zx!Kv-+1Z($*;(#Uo?qG*y^rl9)gdngw=*gKK<^6pg4^tGP&%nHvEtX)+k3qzN}sa) zSFdU>|7TB~DDHh~3U{yYtS7p{+faPU`nP&jSRW#NN3PtS*?SA2uV%S#!zN((Ye*T>%4>z9`so~7!A0oD7Xc%}8izzgd|zK>LIB1a{G*{=xIe{~7%O9PMv&<>Z3o|ET;aq@&4HBk@g4c5&BD*IqG5mG0yj9Xn%CwLVwp!J+74VQ3Bo{ zd%g00Ty+xpE^K^D*e{Q*q37u~N4%W@4DV%rMa}Omr zkc)=}=X4!8<`F6vkf#9pib20^Juht${CIVxHw^medIrJ1zIs(kvu3Aoy6o2dlAS-d z&9~>W5**Nf6i2agMQEtKvvfmBa^Mak$etxzNsDoz-?_tp5CPWT?r2O z-(y3E^FeOSmEeH>loyHPtlekg^t;eQLcWF=XPBRq_+aR*MO^BW$!Eh7zf@S4k{)u} z_vpHy`bEThd#twPqUG(l%gHt4wy*1|fY*f{5em|Xc>@V~+wR&}B0qQIcGuB!_TOaV zb&(%ZUC0j+@vbR)Bl*Oj=Ui%)cBTAkl~TYf*%de+g!~Z^@0q8KCg(e{lDjvuTg`sO zR#Ci|ya@RsATLV#$(A32?*}wL1kfzJH+tE1BjJ&(KiD@Y9GrjL8Md?i$Q1@BIM`Rk zd=owndw$Mr?yr(9QMrTs5%zgkb(&uSbPeuNb=vv)jh*_Vbb)>Yx^9=-@h-^_?gU5a zqV@$Kyiz-t@2&IeaJqQ?phTBV9}Veuxc$R3DNg-Sx&RL7O4)n7)Xvb-PH>bis*ixK zZLf;TtrH2=gA4%;7Xg==v6w<}yT zB;oX|{PXbq!|D_c)am)eTEjl{{?UoQFir{w^KiF~s>G=>g>XB~zQ7H}2fI8H)36>l zT266i^ap-7o%i^Gzj5SwKVLH5mOA4$@23dOsbKH$bZd2YkNp(yyKjZm;} z;hgN_y?>v}?YBM{|NKcK?tw9H z)Zw-q4f!Fy>kSkyTR%{|u$~3H^Znf#^ewr9OIfo0z-*RQoKD0iU8k(=okiID>KK8n#_+Y&W_(lep8h$g@$fYga)9|ka7g2mJ z)}1%3J7L`2wC9#@$o-5ye{KDIqi z@xeKv5dXO|t^JJe-fiP9UJ5$9KFeejpR~U4`KEb30>0bXtSJ||1aMa#$Chi~tUHQN zT2DA1Yta3h798m4mQz>zX%hEf(yQS5^*3`zCln3#`K39ES6W95I$<3NbpBD=^mfa4 zZ@465(@aYYnNfVw`eERM^&{ZB+cCmYSQ_}eDvJEhKqFr?GnEDaTgkE2Xl!NcNXe;axY4sv~C#Vg>@s)H|0=YzY#UM zacTM!jc#9Th2oRe3j-hQV*j3r_n(yPdpP_Id-|2t@ zyEGkeAdil4*m$8J4A`&ucxk~HAr5&dcyRB8ay7X-t>X^oGu%P(xsX3vI7Yyiw)s}4 z%HQnbemYWN^XvSjQG71skLJ&a_?8d281}_sZbU-PP%du^6rT(EqlIDwe94y#8#c}= z!No0XRoi>cC={Oy`J?$WB7L>DR^Aa?n45XDPWb6}SlqRGO}6||c>?(ZyQXbwC2i=A zXxt6~JYA^0_gD6Ztxhb$?GRgj3_kxt-U}_@mU<`fb>%V%+=88P-Rf_6f$_t>5RFXE zL66hC7J(mUM(mv4^T;^v-tQipkImVI;&Y+LAzwu-FqgJ#!`lrLzT<{|75m5BK4npS zk{)M_7uJpFe{>+ zJ-p@iJvv_Y`l9j)>q{JOYUbVV))nV8@BA_cB&Nya8Rl0gB*Xg;;&CBr@7ouUk>P=U z9SX>#&a2b@)a85J(#o9|73e(`8wV7KQQ)&f0hzmvU$!Uz=PsD@wI}~a@wr$Rw1AAj zmkT}zA6n}a;_h~zmvesba}*zYUC{A@T_fPTS#4(=Ig;h#?|IB8pC~>Ta!>PR1bk-( zt{idcePiz4-j%->bVL0N$cItlAHOa{xjNjXOMk6)BloaEg;JqO=TZ7x>?<^1M#uwC ztn^@x?@c{-ZF%LR1O2L?_*}?6&6g4I?GEdIdwg0~F2&-$XhLv7nS4<>1-W*4uIFjX zmqG1Z+D`Zam4QY#e- zxV^6qOwX0a55=b}|DH5oM!*+;dc@u4`8>IsJ)U{@N|=S>QDBa6? z5H+UjjLyE?G4<7-7fyexfS0WoDPE8-3GW{jo~)Sm@&cFCD_bS+Vbc}xvh^dy3-T!8 zP1#YQ&a2okZfA|HaKD3g44b%f3g4@uZw&NPa&T| zjN3j4I^=z%t(7~r`d;~)JF+O?WuKb>y+Xc)i1%jw3q=9~)LeW*Axp21*fkfpyWN7u?p#4)Gfcc}Jxl3@^&%CxzWQ+3mU<^|a*1R2l-112 zuYi}WZz*0_UlQKCM?Wmw_3jLJ-r%-#bQbak6_Z|Ry*cfN{QU|~Sv`o!zTw@YpOMVf zefw|ac#yzN4=rYGd79i!W#VVs-;{2;?!@~n_n*vdIDuQsov1zG`Qc1W74W)PcS8Pw zgtzBJU;klcxl`@;y?*qRoR#xGGQG#gIX#63ZyXL5Q2BAOj)Z&yvHzh9UQBx&be2oo z(XZ_425Z>;3;6=zP>BXNMyC+}cYwp_gm}M#-V^cx#DeSbHOFl`=TITzyjBHpwI@ov68t+>#VANE)`4OYPGLe7Qy5hC7{UA<@Zx$VjAEMxI%nxt32 zE6KUjJ_>RuJUk=eZL|8?#4F!(zyCC=^w1jP6!5x`Z(27(+ON`@2Q@Tayvt4B(syIo z(hBPm;^a#6M)wtQA3VCV<*@UAS-1z24YzXcBRK>74?1ry_7zXMuZVaL?`eKEcULX< z&^@BX?)&793lp!4_25bO6%p^XD`~~9qP;-erewZ_9_ydX;TbDvz2spqJ*Dp2?_7BI% z@X&G5eM00{YQPW12R{ztR=$qO-?MNvg>ku9KSCV_5pUYci`&j#I>n7>-Y(bJ+oE zcwGUnv_Ck-2YG+d(+k!QOjBU9LJ4_3RLA!N3PO3|<5iav^JMZ1{H1YjNH-Lhft)@H2XO&MI86Q^UX1HQ zppV9r1^POb2$&T!-!slE}oY zQ~bD-MSg1-Qq&#AE3H3V?r7W?3>!DDL(Nx*7<2)gkT+ktTG#9GM{v3*9MCoSeBK>R z9#+QblJo~d4nZ#fzqU^rGGO7vUJSaZ{s6#}yE-J8_wK>zqHw_1BOBJuIed4b5**Nf zb4l04{@I7%^h^4PK|kmtAZt;vEsb6-U&^2#;HaKl*deOyt}HEax+ombf3C~z5xch) zRe}Th@AOYz8meE6(r?Qjz~zGG4*>l?MeQq>W$SXBej(2Q=(@0}+JeV>(0aR`0vx65 z`m}PU2}N+aB)f+}7wBW4>tLSs8wX44(Lyq*)MHe zDzV|waRMB~DKEC>=3H8}6Ss$L4^A=Yvh7E4VWpC*jKcGSJ!yUk^q0``&Wi8n^>(6* z@{8_AFi{t(FWH`BXnA}cx!8|r{t2KNsp)h0&fp$S{ZW3wJ_B^6Hd!}m(#E4saFi~( z9|6K8DJ%TnY+|f$X+LuEzrlW^b#926&kO#Qu*C7!2f^I!mU(x~&(#LAvua5D1B3sz z{o($)9zh`Cf;;t#;*WpO;E-AU`KEZ-1HJZwW3Jw)!|YT{K@t;6In` z)Mkbr6YA)Ie!6`}SElv#TXJ~?{tIwG*rnl>tfkNVhRQ`vN$=x&0NQCD_rB8gV%_*3 z+4`XU!Tvzk?N-mH0H^>5^j|ITWWEc0l>XKhA07^yZ(AK72K}J-fv;zNuKwowCSL~q zcKgHmX@32$rlI!}J;k~}ykH#5{NUA)MnB>Fp!k6P<2x%9$sfHFr(g0n81#ca2l_7* z{3&4ad~ydA<*VKPkf=6Q$z++Y1PA|Hx8m7q`eWPjAbox%IM6@m3vDm!yN=#?j+#a5^{~7e#^#6rH zdQ-w<=lfQaeyab0{=LuJoEx3R1E-6^0sR+KcdkA0^qmqM(0{Oc*yEzrbEEXz^3rhn zXuh$){G{p<0Uw{MpH+E=Vu|rmO zoPHPkiCulCKh|ciS3Wa-pTJYdm*e%p;na}>0`)g607NUymNHMueLD$o4tPSLz?|a&f}f%M~2lTZvzF zB#uX?fDg!DQ|cOk;q*)U1Y=!-e*pBKA2=}<5TWz~9PB0+VqWxc0SEM-T~;OD1su@7 zVT>BUaQfN%66LGSFG;)iSC@PHgBbh*IFRe`?28N6TZQM>dMe~Y(*A7w)`^_HB|^M& zJM~B9p863W_xInKwue>!9Uq^xZ#nJbv_IRvbus?!tW$*p8U5MqM|UO;-BkD~dY{!( zp$-`B&$iE8S^a&HodsGj`m@_VZdbT(@py3+9iOdEnA5t3b7`87Db$ncTIouyc~7Fb zyM=40I`8kwUDyb^s%wDnOXFAC>e;><}=4T4`rB+;<9o?cpF7Ec>YaLH>Rk#bP zb2EIxPjW$D^z6I!mxm~RNuM(42K^_{ee}hGz}5S|;O=JCRG+(e0k`+*m4x_}{ZPDY zJxBRL^&Gv2Gbpv=*_ifMxI1@${xV-&dG5mRm8Z>~{R4_u(o+n2LH`K!u3lE18yLZAg?oESe?&$YE)uvCI zgyNOfH3KiKSKGMd+#F4T+2@vt4V45A^LXJG#$|97l2h5c<7r`J_(kipN9S zg*r{I3#gk;`3a>_#?e%W7r-vjQJXLQy%TsH33c=L!=^`>!}W#j!S7xO~#OL_s-*IqP_ z7539#{BCGMZX__q1NsBTlh!_=;IjnWZ}g&htbk8R?z|vBmF;iQb~&6#U@0F0_l_*OEf-^6 z+4_RY5$FrxQ{w8-iY2E`W%LJd(BI`#Ri>B!<)9KA*bDO}we2*#njeEMN#8i}fjCYu}Yt<|$@<&v@BzpkWUtYBCoS?_AG>qvVd^m_pd|C5{vA1$? zV-~)iloqH%@k)A$ftT+4RF9u~F|qSsJr8q9`3wJAXXr?7{Jlp7%#BW9c!_7CbZ+1U z?PR@yCP;iX_vx>eQ{2_B4;9{DI0VHftzVp{-f$`Ud6<6CtB2qD_Z0Oici|V8T1MJH;nhexvleu7mwtQY$-b^ZLAN9 zPujN`_=J5M@TGNld8ucuaPAk?-2&VHe1+m;uS+^!VZR1^x3iBu);r=jcO~^XTJ#&-vJr|`S~s{nR+kUGA2YP4CiK*%yWk`-N+Am#)yxvsQ_b&32y=3*?(vI~RRKHk0 zuK$6(8gsvSOn=J@@{bkv5t^S2*D&J)~`sb_;`F;6DIe zcO%~RUh_K9sXt0TzyVzcYR3Py>lQg{lhIG*4E_{YsFnQpPVkI9R_&3x9P~pAD~9&i?APGonpl?q1cq3y=N~g|0VAALHwn=J5;of(u@*-qNcgsW+Yc-N{X; zoo~zEcalf218E&R!45na^i3b{9R6I=zWUuByjjgz8zq%#>|O|^(}mum`TIg1Y{!$? zDt}lonY^}KWS~CwdvsnvFM+s7`#T0l^X$_B2kSK*aIk*T0SEG&4mgm*bilzr?g)qR z8S>5*C_ZVw#N|uK9~bcT9X8|S@hI-jfd*bhiZ({^ zNpgV8myjP$&+Voe+clfIbqbfdd%%S7PRZP~%yl1Va-;GEdEU6b0=cDm;DX#n56ji{ zU@155-n)ZM`UD@~PIo;w%BRIWj9%OOeK@@|UtGYu;-})*YR(Sh9vqu z-;PD=op{;mq~UxO>ZA$&bp8pAgYI_Zu3f)v>EPWKrIVLSg?Sh2mVqX|cVgXqe%{Z7 z&W~fvJLHio;iLMW(s#M$qzSwJ)N!}()ZD)C%Tp+QlHO*Fm+E6dj*QC)Swt?6td3>T zC)vG>d4csJoGVXGDtL3hxeRCV7Q_`mlywq-`@)^Cveaue- z&vMDPv)umj+Z4|F^Qc=j9-qVXkT=+iuGcTr6I0?l__e}0Q+$(&K1t2XaknB927C6} zjE={JeMjqz3HIXsZKJLZ$p1BWvQ+%^uXe4(#zFISU5^9uXBEaFdXBnePi9qzmpSBcU`NnAT1URg@KLyQ?gur}xgXf_J@Iu%^Ekyi zNc)oRmw6t*4IV%4(TI*%{X#Dnb;4-7SSKtYQ<36VYIfjmygPZI)#M;dJ|TY+rkT5$6lddlc$@ ztU5UCTJM=Za;ZhrcIBJ4ii_&{#oXohP`=pm5uL^X_9x9B6zVc;uT$TWn9!I@dAYsq zutk?Jd94ZifXn&_UXbsmpbuz1oX~H)U#I!J{=XH|GLE7DA@2>xNBIov1NfX=a_O_L zUO30qVExkkIhuE~f2j8R^<^5Nb`a#nDfMSNr#Mx309WuOqf}CqV`bQqT63|4xGe| z%Endb^{u;X-Y8#S-GIIjrx%qw$UDNvLGi$T0eI5xw5<7RTLmY+*}=hj=(Xv{mrXAw z<9Y|`l;QnRzQMW@_KV|D2i!-TY{Nb9IQ8b{_|+)iBzua%H&{0UzJzV3XHVT0&Rw6> zjB`VOG5 z|D_zfy!PbCliZH8b;e9R`U}c$*dHACT@()F))5Y+&xO3wJQcD2-Y<>wycx#zgqgOC_Y{uak=5^y?E1n5B?rMY=vW6 zZ(mS%{cul|*b$HyfO@2?`C z^asVu>vtu3LH|qj5{^Y29o$PjiF=^^dA`nTt^!`R{8M^iACTz1o%p7^<(oU)hrG)UTdO&pS>?ox?z1u z{5UZ>?%059N4WbpPM_4|$g6;tz1}EZ*dHXkH?kbBz35q0E@9B@kSba4v+*kAX#ifD zry=qA-sYQi*IXIMotf@8dU3046t7H=@OGCs&BvgNOEg$8`GGtO1^JoX)*ho;^{))LpwG5*F$C7 zMWX8v>bk*r(Q^rJTK5gQ8{GJ-!5iOIbh6JVUBbHJ4dA4W54wNZ<`qsCg#-JyTlZ;u zcHfz+1V;lBUu@}K`;s?KzqBqH^uxLY`ltThI)A6wd<^>O`USuTS+_()>_^{I^A_I^ z}nEqmrLy& zy|~5bjSA~cTF1ElsYcf^sm}x?uJ`gy@Gn%3do(U5I_=M(CxrTRVqK}rn-jFN*YD0em3m+`;c*+g$*l7ATzpe^+UF@T_`gGp>Qb=2u|7G1l zR&IFRp-)tkycO_D>(y!AK#vG{5U>tAMPxtep?Qq18v%|wj!&Bnm}Nxkw0H~k=m1XC z`?CFk_bWO>bIX&NUp3>pQ|>@@9+cJMn|}v%G1&IU%n2Ff8%h!L`-6Lp`E4 zKWI`H#itxc@uqd>1bjD^9Gg0%;3Y0WU39BY^fVM7+y0~U!M-NoyHvd7%^}_0xjVfh z-QxnVdj;MQXNGm)q;GitiMDqU;vQXwXX|>t*>|Kq^4o9URe#0V^PPV*K zI_bVc&+Dygj`!%V_vcb_HlF%;<7G@9XxtgZP!tg+*o z`JcVjiMO|)<6!G~kW(6G2D(y44qNNzdkNpqgnDuyfV++-{(SdI6P$iY|2xr7`vd(C z{XKFkwNX`wv40A6=AgfA@BcJ@=r{uHPob_H%oB}w3;as%Q>WaG31hi^-+iMUbMOkr zFQ^xX>s5*m&H+UJH!PRy__DFwsvE!gH5%0en|E4A&T-yRIFS2vz=6G%4mglcM>ur6 zZ2bxIMdRauC+%U*wB@h*;qoEG(P6%H&)qwIIchCFUy^^~Bp;N&_VS&4v3VR({aoDA z3;Yg%($AJ}iVyq|alQ;AJv(~zdQ? z^Ln(_o0fA2EtSJjeCd=+s7prgV@UkK<`v$#!q*|~5A0!}|Iydsr^b1OG3cjuGu0z4 zcZ^%w^$;pY5RX>skIE(3CM^TL?N)v^qd$P7@z3eeZ=*V=p!b%&72?`-d{l0szerzx z6)^e(INIOjzm8yV6rWA+N_>~iH^AA|vr>jb#sgs{@$pnD1A1$Q=Si-yePvVa)8a7i}rUu$|G3cWE3s3+0_xGC)i7KN62lVfa>eh5; zmBmVMuuKm0S@W=N2f`0@o%4E1iGG_NyPIWxaOP3uObXR200;R^=k^87e%KbjckE5$ z{XqZn3*SC^*P7IpVB-V&J01Qa;Q5#2J_JU;LLLD4r9#}E=FM#%Q)j@UlRt7PmzvLN zv1ck5HsKrY@LqouuXIj@uTvq8&(Bxynfnu>TkXKdLE&H=x0htzwb3)H5*)1O%cdDI zg$`I5gD#uh8W8Kb%3PUa^apT2*Y+xl^G^1~>rZ*p`~sjW&khZ2Vvbn$u; zx9dPJ(7Gu?objhG%1th$CH024J=N`68Hw_r*IT&$rt=DVP0T|&c_A#S*&Hsl=9&@h zE}!8#JZ#$QVCDQMUiNtdoDU57Pdr9uwcuxKbc<);V8f=Wh3T|E~JrfeQUo{o(RFTA@A%?Vqk=&pI>&v1-Pw zRm+Mm;!+!r)HRw=j`ol7RUuCR^s`WhgI_Pd-;0QjpGVG!W$PaJNa3g*lj2vPtzSwY zy6zS72xxycyZKhZF==D{Uo!dwIND!)^sel6ZVbo!W3M}qN1BHq3cb7h$=y!ww@me_H+MNlo{7CGV)whfZFvb!cp=_N^Ae;u_`+h1r??d- zaqvI>`gO^URb0@r$5Er7jaQH#>3js=M`@k{*hRXBN44D!m1N8(^+N%0j(6`yy_QVI z<;>1s)xAFC-{X0#5*#SX)YrzON_Pt5^1-iLTwhWC&~+`wV}j+W4#gGAad|G-K-CG z#QfTJB4@o^?BuTaLPMnX0&$XPU3#QZFz|yNq@K`P5i;-g}QM`N3Q3;Q>B1)-K2Ydku!^k z?Qwzq?%b?9wrE^HyKCoO=jrF;H$GwP(LdiS(M1eHmG$hQ>_11;jo@y(C9OM=aL9?S zD;L^c9IyZ0dS`fh?bhJ;PIM*T{jvN?KX2|cZ_zTtNrpOTl>D)7}scWG}?l{K#5TvLs- zSnGp``~%vJ+{)d~9NfqheN~(X5+?G$zC7`^HQ}1)j2mu$S?~C?Uc7v7d#U~5M(eef z`vSOAi}Y`wUeC(iI?|)V(3A^edt6}nwqj%FXO3$}!{a!=m3eC|yH`hvE)rZ+ zS+9O`ack$RmkbeSmdK8Q=HI z^H+AFtHb)O@#pHdiEEH$R#Vol8Bn5d+OE7# zbRAo8Xu`pYzSes;CQey5`)6mmN*k+eIN)hLyli6Gg#j;}>AKa>bg%I8IL}6I4IZlI z@N}L2vshgvx`=_T;zk$!A@XXsb=F6lpWNBpyd-z&@Sw})r}f2o;BK_+6qVHFq4oT^ ze%boXZft$nKWo9de|;ylrLI{)oBb zr#jJaa6TC~T(KVXrdwzpbaZ8Qsa{8B>Z%rH3-E^+xQ|{n* zqpxL+A{7_t4|jgrnd@&4?y&Cjx18))qp9`Lvj)@a9~dRIr}~e*nY^hXx4r0<`9+et zaDT)fz4Aw+pT+jL_EDEl*PbvVuF2k?CWX(8jBD7udd25`-9^G=`X1;4g@18CLaEfS zw$?|3bj5NXk8`5y)UXX_j%0DSo|)LS{PTSio#;w=m7~{=@q?}BMs^#x@yeG@bafmV zxPEB8)p6zWw(r{RUKK)DwL>jdv{a%C6n=b`PSc}WRI(;l8`D2*NhK$`j*P3=E6*+T5?|&L!(}}L+FU~f)+*W73_Wb*Ox}JNS>B@4ag>h+bXx;J6oOgfS&mzu)_0+)EU8kpdpKEp} zfE&8~$jyrrXF1XJs71CjYp=|+UMM~A`mxA!PIPq~Iy}eSuI_Ppo=n*OOExW`>+2G2 z`t(qu3l#p9$F&lFoBP1}sCe0xo1@&F=^E>yZrl5|_2;9ZNqdqyJJFR=DY18q^_KPO zxYfOLZ=CK-SKi2{qdj7LzOtrnJhRZB(Dl`jS_l1<=mLc=S(GB@@lO#C-fH{n--W~E z9u)q+&94qqvaFj@A%yVj!RxE<^7HbwW6AQlBK;U$RN?>2dZL3q`9Su|kBFXlc4^Jo z?!2DZp!SjsX@oA42KO;GqWpROcweLs*@&h!U&6)l8_{w%?MelHkuyU$ zJNnCwmVM3&x7gg=d;%8SK3L;ohqx9+%KL_`=oZ(aW05LLs=QL73-tZJtS5YYjvaPC zUZJ~Br5BAB9E%$k_rsDpQx;_nWY9G{EQ0Jo|1wP?PvxKX{mRbMvPWpBA1?cQ4sB#8 zZ4i*j&K>_C?WBSr$+&b=`M6V<|MgE8g^Ul`4Sgva{8|P0+nrNLe`mqV_?0_CPZ%ZS zw(=dx+k<8DA#2YQ9pVTe>o=!9G{j&(VKRE1+FgCWahrCv=(NJWgPH_1^Y?4lh`e|r z{S=K2Gn=9#n`x`LX}(Shsqp8_#xuIFThyq*bm7cfJ_da_VS@B^5)*OkV_=hZ0Rgf; ziqk#zODy zS=PyDsoc0#K#+`zYXLugCj+!@F!`qzVf)3zv0OIm3Qt7=F)BToVRPc=u;X`9CgU@ zD|dLAtG*QGv4d?MGk21WKMI+PVsswKBvf01_B5QXw z2a0RQ(=E(U!#sC(&W7WE^RHZN#9+armG$9@_pa8Cr`xWfU2_fWXlb&&@ZoNE&pzN! zNL=;disv85kDFWO&W=1UU9fRLn?TvxDHa>7H);b+7F{*B6Q0rfj@H_Z#{29#e)>uU zsuX4VD0^Swd|3YXe(1SzrZ)ZPxv{v_$vp?%^-{fCDS#zu|eD+zakNfmR!&l_*8s}!|f9d^9vH7$4l*V;>z=rNc zE(BOp>+h+dpEHJ=ubEIV@0QoxasSw#s0c|hbgXa3Ql=6SJPwd3ao6X#~nyD!f)kN<4gDiI=%Px^!KAKboLBmD>V zJIJN2p4zj2{{#_E{we>S_`3*~NrAuiZkGrze(v6?d*-`iWm3KQz~4W2$BLNgXI$1I zzvg&0TV?f{+;Ga+0{i7t@N|tx=Opi(ILC ze?&UI6XC^A`S--%#dan6wLiWU;lzVAlA+5r?}J(4nM%L*X+H`Dgy7e_X# z_Waeg*<>>3drqz!DP(quBzzX1;&|l5aBGKkuHiju3zzmJa?pw2v?84RQ~o{icM(pB zzx*+@2q*un7Ymp&?WX+AB=d%0szaRoxM#*Zt$eoybFz2ZpIo->X>q;=j>2uuP$gU*r|U(@}1oySKZ!7z^&E z{1(x>#bx>R_#*G6ZVg_RoRpnxf(_mrpOf21YW{5gL*#>;Aa2W`pzrs*d&;HV{z}9{@L@p zZi6PqN&uh9e~5gr-ptW0u=m%CxGjy34?ghdwRNlK`Oa~V9&*X=hr9h=f3*lF|6~nA zgj3?Lq5~&bv%7_S?|L%T>QgA9O7UBBCs|L|KD6M_LN{*To(tVGX<6iavcHRbuukow zFLdVX3f2L?`kXpd#Mhd3aN_o@&-!tDz8+?L5xz}?lYg=Z7vYrntLVTDc`+~hq=V<` zZGe65y^{l%b6t}hkFxr3S>L3#bSL`Z!y|Pu6^oir_ji#GatgSE`LpM{H*h7FHmTX& zQn|K>aPm+2_r%{tI3@lnIdJu+FSMA-6hB*!8orfx&!F~%h~N6=<}dSVw2!ss*s^-I zia5xHvT6R1tTz$r(|?M5kW<1f&o?uA>Wo)h+KZ=|t3FsJ!pT47-xGfq;gtBRiA|D)RZc{fK;E(IvLCK3#oJiN8t?91E}b_qZ>*=U0(m z_MAR;i~m~#tEM{t`MLdwd}GliwzEE6eNTbEkk|Eh@rwP$Wv%?Y2l<7|8Z<7Jd^UI% z^ISzfKer!od@Q=ecGjn>?(kZul=!RUzp?O&e~-)M7tw?K(%|jkvhev}Sq6Wl6WmPv`2#8Y$38$4`N;b8 zsZ<*hcepIaZ^jWIF8}qzJy^h>@b8jbp3JvX`>1L^IVoIPMfWKUw2vjY#}q>RT@Ek) zF19Pluaf_^n+E9L2?vx;=MO&LUjOj4b%l8DMTgr-K{H~l?}I;B3OHafPajfrU`bCNjjRu+yee6HZnH9vz5H7RHIt36WNd_nWclee zhGxc4lc!r~!F`@DO8EMX+TJ=>sakcvm&?LighxeVwYpGEv>~dx+btj6`=8caYiV7) zG_YJXD}VUyJdWJj5I4KqwtT&bAWyrz$9!ZSxmcn$g@5Gj1vC-CauB{7Ih?yi1|{QH z9&@O{LGWemxnWKLGyPXO=CruooK|zI-E>{c(w#e zS5fXg-R8WAOE0D6n^{r=yO`j?c&k74SV+}?7Qnuq4!0v)!Xejr1Rh?0H9vm;?s3Q%T>?R4O{yP4)fv zs#cm9jjEN-plaVt<$zArSZ~l($@%^Fn*(m1UYkt8BD<%gUS+R)*HiAWx`r%^u_>VT$DW+CgJhse7w527{?@D;`V)=}}ke z&)7k4;VFTy*^i~SN^R^DX+IV_DxoX>D^5n{0gY0kmC|v}Z zBVr568UR>iNzNQ(2FX7vGFMTmkZ7}6M|fn3B9o>vg{q>$b!6r#kcH=G`)2KQkp@kO zj&Ov`MRcT=hqRxsoH@!!}@=AhSwLa-A7|^-z^Jm_jrLeSe)6 z{?JkrtTUANXd?i5RH&fp!83`7g@=c#KAAhIJ6;ZQK#OdKX@iU~hi=JG2)kkMYC}3m zP)7v!QI&BP3!bzbIm*~(i5D)3z`8kk@e_YU0{E3-B0{87IW+TNYD>7OZ(z74l88<< zyw%rY(T5pz+E#+uZ&6jn0fY1rI-x5Ds&=|i9brL;j@J^*4kf5ox`<$%nF;3`MHX{# zbQHnFpoj>eQ5Qng0}H^{sE_cAHuuq4+87NnET~qb58m0q+D+y#jgiPw!x*AIbY>fJ?=Kq5%XtZY{T_&@R5Z+Wzl({)19GwUqutw;Ss72UyA|g%EMlDJ=&+e8wO(?Umb#w%AltXwqWy)7L5eEY= z622i(`aU|05Ps4Fbp|5Ht#y6d1otHWfev14YHf;27ogb`*^KNLpp6hwh%#%8mI!?m z)$V9tM2cvu%0qDF$%Qle7Y~+}(M2gNXu7?TBLO`f0hwWx7lyu?7)y{TEX<&zfY?IN z>dk~n>~_6{P>X#L8Yq=mvsP~*ds9p+qPs|}b$7L^B)7vIoOGRwrA!@g%RnWF^kMw3y8 zff!82FrI0sfE)E>mx4xQcKC5ZV><-i zwPI7fR;x4eOoyG04Jeu~ekKFi>JfZou@hcXE)r`|A8bIS#U88~`9?nsmj$#DAz$Ed zNVwT#Bprw8iD8AYnl1{ZVc1zTyoEC;XO2NCox!35i>r)*x6ycG4=f$t99Gjm*-RfW zj6{nk01Q7hzcdKse_(Do*jDMWyF>~YHW%C8f)-b{7638rENGRiC(OojguvA?B}9}s zxokqKunRlcRrIS$HWffH>?yUfEhQmm+fizpeT3RjcI`5;2IRQyMEk|;CTJnNVA@Oo zkPdsv0TIhqQoFR1i08_ikHYLAHWKusXdh7sCEG}#6}O9^5wnS)!G%4fmi8gk7J|v6 zdpgq&Qm4lTQft}vk6PLGQK#STfe{lkgR#h5+dFEFD_cjcVCSeY8;AZ8wQmS6McYP@ zE!?i**&;Jk9CuAM*$82kDBClju=!mAv1Ndtd>d-V*kp%g!@&31_6sz-uwB#|(!FcD zMg0$K7PYdyqGs7DYL=Y><6+q-(8#b))XKJrpoNs}5&=lbCQ;iP8n`{;0D{>fMC^F4 zD1CN_AiyjeL}q{Rr$43eLmt5d(eZ|bV3^pf5Ycv_=SL_4(LxYy6qP#4?}Cj3L*BpS)=lZ zGDTuWDLJHVMouNn8t}ZqsYC2$K`*xCeRSe?2;MG+Pk{7|jMQm}2TeQ!4@-o}6ctX+ z*R(n<_7&PS%T1yiKAF?bCR2pWV1WVlC4O$GnT)cHk@kTBh3hn0OZ!NFs^2khCER4t zlC_GTV~9(vsop?Nd=;<|cKXw(zIalIQ4<_Zyf!m1+e2$2M@gt_7S-1js3$xFuJZ@> z$ixI|LVAS~|I07hKu&j2kbY(j>dN*sk%Rgl7>#sJ;N-{1A6c>Bgy|Oc47r_54_9IO zJBqV}jeBvBfEg%I%#MGXo?rij>d7_(Hzk$f#9StBME@Y4g_^E&3=TA5|P}6_`2FE_l*aya@#Try6py zrPW17g|`W9tk)T|0cKqvqF=F@jL`HWW;qFa7^8^GZ?r`6hi@S<7%1HsX;ek!(I>Kc zJMi3f==e==97w(TKyv8N%-FQOKZaFrp+HVP8`YHjsX;rP#bk))w^LPh?;7xr^Q;C( zM?~5IdRMEi;{S056oiCdM`m))%40?50nBZI8NP-vlUW}X9>Gfmu@`-f+JVw%;jQiT zVGMe-bgZDw$>A*{rp1StNz4^R9zp!WNPcr=v_~4E$;L@m6eB3BC2>}=b@4~bh#2yT zatwAv>id{*D^F0?C_)lVGxfsx8)1KjjnEQe&_^~2H}TmNNNX60(phu?WFO>@z!3mm zKQfd`h$fPcG6d>+M?*Y0nvWC{W#zmN^<^1X;mk-ZWE4<+N;geJS2_5L{2nU)5W`7l zuz4kF-Xfr{Ci;VUoSumnSP6_o;A?n(p%YS^d4&xGiR>gDm z^!Ry>4sSa08DwmAV2_&Z=&4;gdE&vBu$40$JP|6L&Yha+*a=4?xKpQg<OFtCSr$wM*ws?ZRpjoHc>RijJB%Z9&wLk{gL?>YuY5G&RdP zQw#c%?U;!WxKk#!G8{6o*_AUU!m%AOHSuE*Cyf8I3kQq`;SE!j2kLwQ2E_5=|Bg9b z{NGs)7vJX6*;2c9w8+V2dYmlbSkbkErB07?MUK=NjukocPKQ%P)|6|9N}Oo6GezST zf7_7~zQ%H*)ah}c$l0_D=ZT+~^f*px7fus7wq`m^y!KLXmiT66M~UbH7fzBr1lvJU z+xa_+&XGN!f@1`c0k%`5W;;Z*iQ&wsnU0WJ(FszM1QZ3;44gD30gCPV8C@oBt_Yu z0tO;J75ts$SJ};pqHhIoS{hYk_*Vd;>|+54%g=&Fm%f%g6z*@?Loj?Udk|NCmpv@o z_W~&5+93Xy*yO?o6TuYxFcBmp2qt8oX9U4`k+Azomk-CoyY}M%DxJO@z`F3~07k*5 zgLY-VP6Sc*?Py1?{5v@m+s6aQ^!j;nUgw~88 z7_foqL(*2%kA$X-AQ;aE_)+pVc>uQ039U+gCjevw!FV65c$}Yg8RicW3Ctf_lz(YDD8|en6!DN({;)SgYijdJ8DTJa;F0OVWQ4)&?w8BsvaGzkj4+s}tX2Qa)%c$XgEi)}>py86%#nhR8VG}{ z1sRbrF~;Gj8DNiLWD!D}bmLMflMo=uxvg?GAwapzCMzUWJJOw zj*{A!{B1Et1|%?ynFR^0ikXn~`&c$4w4fQ0(1K@0(x!~-V8917dlFjM8I<&kj4VoM z#4{;r6DymNwq``acs}qyYCP)_niMiGp`Dq139T8CFrE$Kk5Yyv5A)C5Q~sMW7xf=j zS_Ashbsa2yzTUqR3d5Yej93^iKIwAgcvdMpaT!;__$5VtM7JyBN|?MB2g~u||ITu{ z_%@dgSH_hvasxW!N?68~FsGOZvqpxZLzHnPjH+@_=NVVRfDIW}!tnYO8CSww*Pf6$ z8vk)g#ed~WSO6(V^J&W>I#TdaV_}i};4@-j8L=>F<6~CafZZUYG$N@dnNb?iwQDAr zP#L8WGfE>m-{r};6(*?LbXEX?#gtJR5zAD`D2IgKUl}DgO zNT-okAAz?26S1)BRjb!>R?=dDlIor{oEu%$DdCyp;QjG`5PxKmt^W@TB&N^f`*$K@ zn9G+D5hJph5fRI{9p<3v_!yLn0v$X^#_cfvK}uQ!kyU&nqe!Cb;u;x660u^>>2#4Y zZin&KRmSZwr${I(tuNzt*yngV%*hpz>LkkDkc?7^I_asgzY`Y=3^D5?$%E)LbI&8p ztceV##p+qFhBe_qR02$e3M&4cRX&vG;i0N;|MYwXc&U|dL0>&{de6+B4s|qtjg!0r z#OG1PM3SP@4ummZaFx@h5IiGq%F3$Dm$$vJR>?zns;-gNqe5K=D^Pr`%~sLhp&8UJ zc`2oDAoIBfzSY&ct`ZvN+pZFX?0r|{5(VF(1SLK{$$*od!^6)v2qtn*#YhZ1@^}Gq z00(1peD9S3#dz}-hNb-Ot2AR!@7n|;nF^R#o(ZAY z)kCpBNr%Y!+JPY!oz+%Rss=@P~hop)~k-` zdqfsclDgYe<5BmzO(bu$kw+*=p?Y8Pj3Rlr(inzKgYsjtfSUE7EHttnm6gWHdRSHh zkRF#s#8LjhECNE+nerpE&=&<1c5k8~$%_k^>=N5Tt20wB;*ZVp*c^_mT^I~tdN`X4 z{EYcUoBQaL>;+*Zwa~?&dO>Io(ip?|_b0LO2q3=jaw*_J)|wnhLWnS2fNf?B)f1(z zCk6i52+;?aVK#Uz+gOjj+h#FpB6;IR<-ym@48vbDweiYtY2tZ^H5;OyB+sql2Z0%@(LW5D*t|QRL>lPAjkqp`5(iW&>!YH=@sMIRa_= zVf&B+OkgB=qK`bSiM&NB2xDvV;2e3Au|NJ~C3#g++Vq-|V)}6365|8eO-P)Mywv7T zo*G2~3>xx2We99+$kOPeF+`Jx9*L9$kz&pU>`^@j#qp0{*s?hIX7Cm||HgEAJ_i@r z08U#=O|Z^T-Xp!Pk3$s6g-*+Gf>5a?>oLO#;#HmSLvo+Nqt)Th`thw9PLRkfKClQ3 ziHJ01I6=tSNrn@IzY?0^1koj=&TxXXI-_(>hZ!&#PEa3Rh7)Alarpm&T_?i{%5Z`} zEchq)KC%<^QLlT+!dhBWBzciur;7rQi51!MCr~N?^Z00SEjeXO|IU%PjKG$t(lP>D z7at>}u#>?j%+(PE%zZ={_I{0q|x5Q*45+4$00W8>Rwv%}ka)d^-+n~WXxMlJt+;eSpY zeS!!{`Yh;-_=qt?myskUZqdT0I>VNxX@8J!f>?>XR>-iWL!w+{lbKDC%`8G{E5b#2 zk{F_(Gi+&wx0?|kp}Ok-LVN^~`wUx}oZ$nBSTL*^yJmj2vq=}&pk&R6uN&Y*Ac)8yUQiA3njKMsMI>do3mf@?DZ$* za1m{%3$wk9r1WbmasGLKY^2) zPSJDWrP^dFLkJ^DVy){8<^LHW{FK_=A}p44Xm~%O{LC7aw7fo^nn#Ibs(OshzoqXT zyUSQ^|5MAY+GnBj|Bej0Q2ZIvXgjzQy`}#da z8?gAN$*d<=!Zduug?xql)*pi+TX7h97y|BX!vQX0?esU9BCu!?pKVKSC=t8VXdxHD zNI%Gz`P1Ka4ajXwA>tVV8lQ`#?d!z=KKq8>n9Vpt` z$fzNMK!*u39|YAM7`KPRb(*MFnn-`Wg|{j&Dx=~0oOJIC6JQF$-@x_NY6&ICu{-~& zky+EXO&^`vtk)8zF>Z|-^yC6JtZ=d%2(U$CK<~bh=?Nnfhx07NWDF#C*i;@Ca+%H0 zjQncB)+~q~UsGRe6IV>0K7o(=cU7kgnSFrK?Ke6{;dN zMjhGUb^N94G6w#dwd}k6Q<*vcgKAr(L;x|ZdDBF3!u zlOEYze#$k^KRPW`iU}}~DhLt8r&E-O_8mTY^h|+9rervf`N`!XUDSL=QbB9f&_qf_ zX=n$Q@CzphD((@U~f;PrTT!TimOe6FDT8vQ>p$}0xI1wLD zB4ji&rNN&?<|mhnzfIX^Bq3(I<2M)D!gfyg(RUGqN~WCHi?JP{pIl1*CS9MEjD+fl z-VV{FBY-R2tF&YN$0_w{)?rjCh=~AhN9boDBk3pVGZK&x6RApLB-Up-1O&=>^00P% z|2P7IQ^^z%jL=UmAb*Fb&qzSX9v?t1EJiupujkM3SvCnks~SOZO3O#JMObt)<;029 zPc9_srsy-05L&b@9r}Zc&_`Du2z^Y6a3b-OOGLUU`ivwZ(rgOV8+6KtngIfkD#&Cq zd^|lNqm3yEPUL-ZNk}(EpOGYxQ~M}lFf=rq`dVDb0||Qapeh0O<4FWcBU3IAGC#Rw z{0*8urEGwcs`4P&0Y)vPg(!Nh4S6t9oJT`UjsKuHkE}P9?V$*;ncQevBK@m&!o(wK7KNF7TIU_$x0&_9p^8Li4vkUnxgBOx%7 z@Krj@LO#m%(TzeU+L%%xBTqpLT+~+P5m9zH*VV9*gJ%o!<^IVV2KwOWD7-K`0pRgt z`9g4$oliib07y0z*#Rwj3%~pF+3b+3gh5HMOXA9syEIz90t-JR8h+!k3k_IylU5h1 zAyw4Ls{lrwHjpGRqIqr_Em4H}J~~LbB2ODYMQ$V?j@*gnUp}z-kq3RlNWv5uelRIf zE>aX2V+`RpMYG9(rdpA7I-1PouOkoCx8#$&(6p{zx|jey{ZKYwavRhB&)9Hz&IIYB z3_AK)205ljOUs9m?6W{p@*c}{T z>c&CMXDFTw#UrMVWhkEigNo;)rm6^T8ZpZwm9vyU_uA>Ie;Nz0^^_j(Ox`O?L;fcU zTN*mC#uPoX^b+Hsu0L`LnlPV{CX4cPlGK0uo#&s0imnV~M~=bFwHN$aL@GQwd`GWzz1g{pj!9ek!JC)prs=SAg#m_u(o`jc)7yy5-iZwE4(hHlkA`RxyqtCM{k3-!fA!p$ZlxfvYB-^fQ!2i0^@NC)!>&i~EX$iw1P`4k3&F7uOcDV( zZZpw-v6FacQL>c)ARTs+10vfK_OjV^X&(uzjcpr2KZJcn$!A%Y8`{E7#h9rC*aA3wsD zB;SVCDzXU+>q&Z;5B78P(Cp$VdUg8jm48xQSgrj0yqfhwhnn^5JY8XofF(3a&&|tZ zTG=L1r^g-O${UelUkbbj-*nfeN8A!R|5oRz2laetabHZAYg9rY&uC9uwo{kvq zn}--xT|F#>g9cq!50#p)r=+S@wJQ8m#`ZOV0JAQ_)JNxQFu>8Dg;GVb2yBGsz(*fI zO4vk7_eSA+IXohWEk!X=^f^3iwjZBSWou8zF<)1Y|JUBt{K#<~_g-0~9YGKhClI2T z5r-d^MMpc~%q}T+Frkx^)%E? zM$LlFVgj3dL`Q2mo~PK>j_K5Y4{AtBFT&ILEuQ#CsT~%mIU)Em3%wjbL#)YvJ&t9&3XpZ;C=G%`x6e z-&u~wvi)gHXEIr#el){dAwQ5UMkAa}qPK*S+(oN-yC)%ZcVlDae&}?Rwu5p1!%=TB zOtr8;&BrqCBb%AbP}wubd#{d&y+rDPVKt)Fp3He1P0Q1EcX~Xc8L-4Vl2SK@*@N~| zQx0`=m5{8VcKV$ zcKdV)8?;_`)*CaD@T`4=FLOY*x>y^A@r>CBYfSDxsh%mceppb|7bU*FiOl7I{wQN z?b6X&hy7$WUh-LT%^Nps^v_=IEGKhu^+uyvqks0QH59p&Jq0eM%b~0t!S;B7gYXu^ z2`1|??%5aH)81|PF!^t$Z-dz5Nr-avlv3AtxjPFCH0TKG42KNf_m?y}7N1Uj>gRwt<#(Sx*k*RBYuSQO-Cj+9Ni z=`!vl*k@1ZoH}bsZfh-LT2``FI${DvXhYGIEIYl?iQjXK{W1gcJkS2bwt%sSzfJAm zANJ$LNb(!P)LAbouuBExJsoFp;7hWc%)G*7-A7OEqv&xgq`>ZBPcL*3ye&sBE{ zADzquGdr2H{)mcZPXx)yj8+JE)JPbK4mQ6xoF#(L0EMWiWx(1cJJO7j-rH@@X}fUe zQDcsFaOV5EQ|&45EzRrATUOV@zS%pP8#irBLSLC?W}I2poac+M=qt7 zP~OzMx?^*L;WVC?#LP0LIG&Pd(T7ru%39Rl8_=j%MtRUy~93E>o$iN;v4Ib#a>_$BsRFi71Sx5R-IWQWT@#- zO}-slr#F%)yK4|j9>NU`KwcP-Sv z!f8tS>f#gRVR6!DQk%|FVh8bJIOF5WM_vCa@xD!W)6AcfkAe^iiOsS3NDe?e+$85o zrPuqt0bVPj+l2>o+Xd!0lALlK?F88gn9KaSO-W(;n^POO--D0f);&Q2y0$)?O;FXH zYf7_)`?Kzo+_iUJ`jeL;e0Fc|-=UxO^K!C?YVUr{{TW4f4-Ri-e?|Y$+BtlG=kU(9 zXsdt5w$G*CO?fgsnUB8n%jVV3|K>m5{mZ{vJNV;&fBi3_sP(~NZTdn=VY3s}=!kcE zYwuvr-y(oa@o0)$oONVXV0H|+ogk@zAE;_-A{WkJl3ma-vj28USIhq0*6xFSCHvLS zaIZM;o8&T7kJNq~{n_XL@{j-av;TVd_pf|H{PdC7S|6WV< zJ=g6Hqc=z{)jk@IqU(Y7X)g)(?@TA$U-`jTXVFzq`ETtXzqXv7Mm4^I|M1C=VeRw4 z7+%{N+KiaFfyR`+$In6%LxTL_$}jBi`0<~!#qqSwTwZacs9^Z45PMFRjHu0gNe}0h+ zyJ0!8!v^%w9Zz`Y_2_hGCm}j`#ySkOC$mnFZ+98)KOA&>Av$R1Kv~N(8FWK-Ikz}v zvuJyv{l`RJx1S%Lc6rg&^oOUtIW5M1JNzWrejHQ#{$A@=79-#J*PVlht^E3U=X10@ z-1+eCzF7M&MT7oq@_76#uJ=X}J(Wu^` z`X?5CNO){%^L~94MGE&;G@xB>uh*!LdI7*}U})9xSdv*Q{CZ@_-)q!q*D?Z2jv0Y2 z%dYTm=j7%ccdu6_O5+nFilVcbql04CQgMig5vNce8)ZT?tq{e#fRXC|hTvoRXhu!LNa4!(G)Z}? zzvk1dYd&x!8j!-3@!1IQc}tCF!{hVVRIpj~X?|~%=+7Z}aZz%9OYkKx%CVh{dMFw& z7E`#(1^gU@d65q*+_wck*H>!Pv{~B)@a25jf`nRuZ{CuIueD_WU(S~;%53#`uY2Ro zTh{P(7V_uco27boxN^Vs?1~!A{!=>&(esccXzTWtpInH87 zrsaSEJN}+ncf9#e4Gzmp{e7<}E|5?jw3^S?B3hmB*l+%rYjddr`v>)zkcO_+7f;Qu z=gsR7{)Yklvoqj73gCZSfPW*CKc3NvmI2{o5?+nl!v%znN$4tEK-idsRk*7{qn>?P z#n!M2R}_~sgbaFkXMbDVcRoOc`;%ykYjg3u^$BhwNpml_be&9z){(A;n!d-UHepl|O^tBERkx6A3_=57TYkOmdqoie)b@BgrMZ$-+A z?rtUB6-8Ebi}bor&bW%Oie8akExi?mTwSkB_y0hz8Qm2{R(fb9T=KmCCC@s~-@d;R zpEJ)skI&swJ}b{ckM6xy=&oV{rT@KZx@YkkPww|tq`!*jw2bI=q+H)C&rXkSxxUXZ zOV6b5Y&Na9`gwAf>pP>rvcA=R`ppOhFL)czRGid%;}Iq1?6XExTlHhg7viIEn~{0m z&RrZj9#16PJgPKA;&C|7`fAj{3O_|=x9c*l^(2s2dOf{5Ne0oxzEik2J)Sk{Xz=8_ z;OP3@Co~*Ap4Z%%r6eHDIlDmr-&srZ%ful~77wOO-J^ePjW(8KE0jXc(7nV6=gW67 z>!e47I{+v^+2{3aJbr&-qWU**pPuE}IbThSXM!ww?n-?$r~K`RRL&nor=IIA^4^lX zB{D$TUdwSY>LAU4ax|phNbnr2gQsGOpO+^%o#Zi-HaWD1llR|zFVd(7kd03?KM&Uo z;hNRBjSz056vz3Opq!i4xS*Ujs&PR%Z+f_EBA$2o&45DRCR6Z7+0n-b1-`X9J}CHi zity5|Ca)flz5!2Ca%)^r>PERA5x=0c4d0KXo;VkBJ>*9XI8wEq86Whg1{`Wde0acc ztQGO$fy2SVc$XK*JmfX8YBl^u5WmuVwd;R~|4Q?f@nQK_ny-ux%U?KOL(jcJ z18~z-+ug^92as+I%6>jRJb-j#PGa+C?IvfDtv(gQU_Fc_k20GUj#K^Jy7B0dY#)Zf;8ZT3NP1N zxnBg~}5c^gBnQ4duZ5UxckmJbh8$mLDydv?reItnX;})q0xo-sFjjH1&NbmTr#M5ZX7*%Wm0`$+*Mx1}g~ZZY77izsd4^2k~f7KQ-m3oKiypf}gLu zkgHea+@kHwSu)SBYJwnKdf>_{)AXE@>vtV}6bJP3oecxk|E)Edd2@L@ospwwqc-EI z@Yf6Top1z6nn+OEQ{k!pE6YcEsX#(4JG5(iIIdI0D#Mk+l88uZ!T<`_%<*&Mg{lfD zSgUw;`G(62Z?Mc+(X2}hE56?@)Ds*%LuZL$g}>lzNv6sA8{JSyqxxSIenUzUf5J0L z{F419^UDoXD|5u8=sW+| zgZr6>Gj^6Zvoe(MvvHV~Gq`_AevI?w4DMSV&X+S7FUfqm%j0T6d5cox^KrhQyeBD~ z^A}og4L@H{4viE}j?ctZT1p!bRp5LnZ6H*E3rHFC7pWH;KY4<(!zg|p&O8p0e%4@z zslo*Y$PQD53k;AQM&V?9n~QI}QQE*p;pKiS;~Ed{0vd&v@o7Gu(G+YTqwrGxTpZ)U zSG>P-`B)DhzQ2>>mvM^+cL9r%U)E9aeX$1&-`C;!D28jEvz?0H>FHi*UC%w2qdpmY z{xPXW$(8Sq4zfR~qO?Cc$o@3;(*Ecm`_qt1`zv{9B9-<>2T7kMP-%a3ko^Pq1*Q6- zsZvTG9VC648>)XzbeMdM4zhpXIaa>E(of*|)g?i2@-cPDF|f|=#~%u(3|;)(s$4!8 zT#aAxL5G}tX0IhjFUKjvH_TpJjZ+TLFnet^4*ha++C{TcPBj4q)27~Yxs`~Kkahq<`4Gf#xNPbY!<3Aw&Q| z!&Kn{X5hfI3Ktq6IZPEUG(d6~g_Ch%E-v*)Y1UYUmvNztOMSRqY875SC(p;F9tE@L zD!i0G7nk~QvjCOgb9pr%Z}w`1m*balsSoGX$kV^PKjXO62l8^H?RLqpN5uHjcgJlJ zp}&p=a2Qwm?pY!3HB%>utg@{?G}W<%b$^#k9iFYBMP5UeBH|UtqPjR+k7(}3@|a*f z;ssZ~rzA^D5|zC!3%Sg#qGYm}38rvqouDt-W<5|RXzMkRa_mN~8aG_8>8{cZiIn9= zS-qyY>&$l#>L;z&l#-;%@%`hXlh#+x&C>cwU!t2Ky;wimO!ZRfM_KJIXP)Gk-_FdX z=ZEzp*hZ=KZI;TLhp8Va$x>A*f4FS)#M1k!x|Ip0aH${plHCgFtG4E9=W$qzKD(=q z!?YCrORj&TUI2j2>7#I049-}i45@He1+J8(!qwKC-xn}b{V%!xrA$KO68=-bDx{CA zhM%=>2~f#-dCk^oli#`bqgvUSUwD7sd=&2Lnyn=_&Qy$4ILpthO2sJ8-YLH-WSF0s z?O%jn6*A1v%$6_0uL`O7HJlw}QNFaLX-9<;pDR|6tvUJZEB8Bo?%%(&y?b!`y@%Nu qFws|T-M~RU`0PF0+2>!(f9IEY-~JfKvfbWkeI(jn#P7TG_x}QR*1vxM literal 0 HcmV?d00001 diff --git a/pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/Loom.cs b/pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/Loom.cs new file mode 100755 index 0000000..e028237 --- /dev/null +++ b/pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/Loom.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using UnityEngine; +using System.Linq; + + +public class Loom : MonoBehaviour { + // taken from: http://www.programering.com/a/MzM1UzNwATg.html + // The important functions to use are: + // - RunAsync(Action) which runs a set of statements on another Thread + // - QueueOnMainThread(Action, [optional] float time) - which runs a set of statements on the main thread (with an optional delay) + + public static int maxThreads = 8; + static int numThreads; + + private static Loom _current; + private int _count; + static bool initialized; + + private List _actions = new List(); + private List _delayed = new List(); + List _currentDelayed = new List(); + List _currentActions = new List(); + + public struct DelayedQueueItem { + public float time; + public Action action; + } + + public static Loom Current { + get { + Initialize(); + return _current; + } + } + + void Awake() { + _current = this; + initialized = true; + } + + static void Initialize() { + if (!initialized) { + if (!Application.isPlaying) + return; + initialized = true; + var g = new GameObject("Loom"); + _current = g.AddComponent(); + } + } + + public static void QueueOnMainThread(Action action) { + QueueOnMainThread(action, 0f); + } + + public static void QueueOnMainThread(Action action, float time) { + if (time != 0) { + lock (Current._delayed) { + Current._delayed.Add(new DelayedQueueItem { time = Time.time + time, action = action }); + } + } else { + lock (Current._actions) { + Current._actions.Add(action); + } + } + } + + public static Thread RunAsync(Action a) { + Initialize(); + while (numThreads >= maxThreads) { + Thread.Sleep(1); + } + Interlocked.Increment(ref numThreads); + ThreadPool.QueueUserWorkItem(RunAction, a); + return null; + } + + private static void RunAction(object action) { + try { + ((Action)action)(); + } catch { + } finally { + Interlocked.Decrement(ref numThreads); + } + + } + + void OnDisable() { + if (_current == this) { + + _current = null; + } + } + + // Use this for initialization + void Start() { + } + + // Update is called once per frame + void Update() { + lock (_actions) { + _currentActions.Clear(); + _currentActions.AddRange(_actions); + _actions.Clear(); + } + foreach (var a in _currentActions) { + a(); + } + lock (_delayed) { + _currentDelayed.Clear(); + _currentDelayed.AddRange(_delayed.Where(d => d.time <= Time.time)); + foreach (var item in _currentDelayed) + _delayed.Remove(item); + } + foreach (var delayed in _currentDelayed) { + delayed.action(); + } + } +} diff --git a/pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/OculusHaptics.cs b/pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/OculusHaptics.cs new file mode 100755 index 0000000..d87b87b --- /dev/null +++ b/pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/OculusHaptics.cs @@ -0,0 +1,124 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +public enum VibrationForce +{ + Light, + Medium, + Hard, +} + + +public class OculusHaptics : MonoBehaviour +{ + OVRInput.Controller controllerMask; + + private OVRHapticsClip clipLight; + private OVRHapticsClip clipMedium; + private OVRHapticsClip clipHard; + + public float lowViveHaptics { get; private set; } + public float mediumViveHaptics { get; private set; } + public float hardViveHaptics { get; private set; } + + private byte[] noize; + + + private void Start() + { + InitializeOVRHaptics(); + //byte[] noize = { 250 }; + //clipHard = new OVRHapticsClip(noize, 1); + } + + private void InitializeOVRHaptics() + { + + //int cnt = 10; + //clipLight = new OVRHapticsClip(cnt); + //clipMedium = new OVRHapticsClip(cnt); + ////clipHard = new OVRHapticsClip(cnt); + //for (int i = 0; i < cnt; i++) + //{ + // clipLight.Samples[i] = i % 2 == 0 ? (byte)0 : (byte)45; + // clipMedium.Samples[i] = i % 2 == 0 ? (byte)0 : (byte)100; + // //clipHard.Samples[i] = i % 2 == 0 ? (byte)0 : (byte)180; + //} + + //clipLight = new OVRHapticsClip(clipLight.Samples, clipLight.Samples.Length); + //clipMedium = new OVRHapticsClip(clipMedium.Samples, clipMedium.Samples.Length); + //clipHard = new OVRHapticsClip(clipHard.Samples, clipHard.Samples.Length); + } + + + void OnEnable() + { + InitializeOVRHaptics(); + } + + public void Vibrate(VibrationForce vibrationForce) + { + //var channel = OVRHaptics.RightChannel; + //if (controllerMask == OVRInput.Controller.LTouch) + // channel = OVRHaptics.LeftChannel; + + //channel = OVRHaptics.Channels[1]; + + //switch (vibrationForce) + //{ + // case VibrationForce.Light: + // channel.Preempt(clipLight); + // break; + // case VibrationForce.Medium: + // channel.Preempt(clipMedium); + // break; + // case VibrationForce.Hard: + // channel.Preempt(clipHard); + // break; + //} + + + OVRHaptics.Channels[0].Clear(); + OVRHaptics.Channels[1].Clear(); + + if (vibrationForce == VibrationForce.Light) + noize = new byte[10] { 60, 60, 60, 60, 60, 60, 60, 60, 60, 60 }; + else if (vibrationForce == VibrationForce.Medium) + noize = new byte[10] { 120, 120, 120 , 120, 120, 120, 120, 120, 120, 120 }; + else + noize = new byte[10] { 240, 240, 240, 240, 240, 240, 240, 240, 240, 240 }; + + OVRHaptics.Channels[1].Preempt(new OVRHapticsClip(noize, 10)); + OVRHaptics.Process(); + } + + public IEnumerator VibrateTime(VibrationForce force, float time) + { + //bool forcedHaptic = true; + var channel = OVRHaptics.RightChannel; + if (controllerMask == OVRInput.Controller.LTouch) + channel = OVRHaptics.LeftChannel; + + for (float t = 0; t <= time; t += Time.deltaTime) + { + switch (force) + { + case VibrationForce.Light: + channel.Queue(clipLight); + break; + case VibrationForce.Medium: + channel.Queue(clipMedium); + break; + case VibrationForce.Hard: + channel.Queue(clipHard); + break; + } + } + yield return new WaitForSeconds(time); + channel.Clear(); + //forcedHaptic = false; + yield return null; + + } +} diff --git a/pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/PlayMovieTextureOnUI.cs b/pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/PlayMovieTextureOnUI.cs new file mode 100755 index 0000000..853f3e8 --- /dev/null +++ b/pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/PlayMovieTextureOnUI.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; + +public class PlayMovieTextureOnUI : MonoBehaviour { + + public RawImage rawimage; + public bool createWebcam; + Texture2D texture; + TCPImage tcpImage; + + // Use this for initialization + void Start () { + createWebcam = false; + if (createWebcam) + { + Debug.Log("Create Webcam"); + WebCamTexture webcamTexture = new WebCamTexture(); + //rawimage.texture = webcamTexture; + rawimage.material.mainTexture = webcamTexture; + Debug.Log("Play Webcam"); + webcamTexture.Play(); + Debug.Log("Play Webcam"); + } + + //rawimage.material.mainTexture = texture; + + String host = "10.255.24.97"; //"10.255.24.140"; + Int32 port = 5112; + tcpImage = new TCPImage(); + //tcpImage.setupSocket(Host, Port); + //tcpImage.setTexture(rawimage.material.mainTexture); + //tcpImage.setRawImage(rawimage); + tcpImage.init(host, port, rawimage); + } + + // Update is called once per frame + void Update () { + //tcpImage.readThread(); + } +} diff --git a/pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/TCP.cs b/pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/TCP.cs new file mode 100755 index 0000000..40e853d --- /dev/null +++ b/pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/TCP.cs @@ -0,0 +1,71 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using System.Collections; +using System; +using System.IO; +using System.Net.Sockets; + + +public class TCP : MonoBehaviour { + + internal Boolean socketReady = false; + TcpClient mySocket; + NetworkStream theStream; + StreamWriter theWriter; + StreamReader theReader; + //String Host = "localhost"; + //Int32 Port = 5111; + void Start() + { + } + void Update() + { + } + // ********************************************** + public void setupSocket(String Host, Int32 Port) + { + try + { + mySocket = new TcpClient(Host, Port); + theStream = mySocket.GetStream(); + theWriter = new StreamWriter(theStream); + theReader = new StreamReader(theStream); + socketReady = true; + } + catch (Exception e) + { + Debug.Log("Socket error: " + e); + } + } + public void writeSocket(string theLine) + { + + if (!socketReady) + return; + //Debug.Log("socket:" + theLine); + String foo = theLine + "\r\n"; + theWriter.Write(foo); + theWriter.Flush(); + } + public String readSocket() + { + if (!socketReady) + return ""; + //Debug.Log("reader"); + if (theStream.DataAvailable) + return theReader.ReadLine(); + //Debug.Log("reader2"); + return ""; + } + public void closeSocket() + { + if (!socketReady) + return; + theWriter.Close(); + theReader.Close(); + mySocket.Close(); + socketReady = false; + } + +} diff --git a/pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/TCPImage.cs b/pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/TCPImage.cs new file mode 100755 index 0000000..2f067e3 --- /dev/null +++ b/pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/TCPImage.cs @@ -0,0 +1,245 @@ +using System; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using UnityEngine; +using UnityEngine.UI; + +public class TCPImage : MonoBehaviour { + // mainly inspired by: https://stackoverflow.com/questions/42717713/unity-live-video-streaming + + // std variables + public bool enableLog = false; + private bool stop = false; + //private bool displayImage = false; + + // network variables + private TcpClient client; + private String host; + private Int32 port; + private bool socketReady = false; + NetworkStream stream; + + // Image variables + // nb of bytes used to specify the length of the image (needs to be the same on the server side) + private const int IMAGE_LENGTH = 4; // 4 bytes are enough (=int32) + Texture2D imageTexture; + RawImage rawImage; + + // Thread + Thread thread; + static readonly object lockObj = new object(); + byte[] imageBytes; + + + // Use this for initialization + void Start () { + } + + // Update is called once per frame + void Update () { + // display image + //if (displayImage) { + // bool loaded = false; + // lock (lockObj) { + // loaded = imageTexture.LoadImage(imageBytes); + // log("Image loaded: " + loaded); + // } + // rawImage.texture = imageTexture; + // displayImage = false; + // if (loaded) + // Debug.Log("Image loaded!"); + //} + } + + public void init(String host, Int32 port, RawImage image) { + // set variables + this.imageTexture = new Texture2D(0, 0); + this.rawImage = image; + this.host = host; + this.port = port; + + // create new thread that will update rawimage with the new incoming images + //thread = new Thread(readThread); + //thread.IsBackground = true; + //thread.Start(); + Loom.RunAsync(() => { + readThread(); + }); + } + + public void readThread() { + // create client (connect to server) + Debug.Log("Setup TCP Image connection"); + this.client = new TcpClient(this.host, this.port); + + // thread main loop: read images from the network + while (!this.stop) { + // read image size + int dim = readImageSize(IMAGE_LENGTH); + log("Dimension of image to be received: " + dim); + + // read image + if (dim > 0) { + readImage(dim); + } else { + logWarning("Lost Connection!!!"); + } + } + } + + /**Read the specified number of bytes from the network. + * param: bytes - this will be filled by what is received from the network. + */ + private bool read(byte[] bytes) { + stream = this.client.GetStream(); + int total = 0; + int size = bytes.Length; + while (total != size) { + int nbBytesRead = stream.Read(bytes, total, size - total); + if (nbBytesRead == 0) // disconnected + return false; + total += nbBytesRead; + } + return true; + } + + /**Read the size of the image that will sent through the network. + */ + private int readImageSize(int size) { + byte[] imageSizeBytes = new byte[size]; + bool connected = read(imageSizeBytes); + if (!connected) + return -1; + int imageSize = BitConverter.ToInt32(imageSizeBytes, 0); + return imageSize; + } + + /**Read the image from the network using the specified size. + */ + //private bool readImage(int size) { + private void readImage(int size) { + // read the image from the network + //byte[] imageBytes = new byte[size]; + bool connected = false; + //lock (lockObj) { + imageBytes = new byte[size]; + connected = read(imageBytes); + log("image loaded in bytes!"); + //displayImage = true; + //} + + bool ready = false; + + // displayImage + if (connected) { + Loom.QueueOnMainThread(() => { + displayImage(imageBytes); + ready = true; + }); + } + + // wait until old Image is displayed + while (!ready) + System.Threading.Thread.Sleep(1); + + //return connected; + } + + private void displayImage(byte[] imageBytes) { + bool loaded = imageTexture.LoadImage(imageBytes); + log("Image loaded: " + loaded); + this.rawImage.texture = imageTexture; + } + + private void log(string msg) { + if (enableLog) + Debug.Log(msg); + } + + private void logWarning(string msg) { + if (enableLog) + Debug.LogWarning(msg); + } + + private void OnApplicationQuit() { + logWarning("Quitting application!"); + this.stop = true; + if (this.client != null) + this.client.Close(); + } + + + + + + + public void setupSocket(String Host, Int32 Port) { + imageTexture = new Texture2D(0, 0); + try { + Debug.Log("Setup TCP IMAGE connection"); + client = new TcpClient(Host, Port); + //buffer = new byte[client.ReceiveBufferSize]; + //memStream = new MemoryStream(); + //stream = client.GetStream(); + socketReady = true; + } + catch (Exception e) + { + Debug.Log("Socket error: " + e); + } + if (socketReady) { + + } + } + + public void setRawImage(RawImage image) { + rawImage = image; + } + + //public String readThread() { + // if(socketReady) { + // //while(true) { // keep reading + // //Debug.Log("Collecting data..."); + // memStream = new MemoryStream(); + // while (true) { //(stream.DataAvailable) { // read an image + // int len = stream.Read(buffer, 0, buffer.Length); + // if (len <= 0) + // break; + // //Debug.Log(len); + // //Debug.Log(stream.DataAvailable); + // count += 1; + // Debug.Log(count); + // memStream.Write(buffer, 0, len); + // } + // count = 0; + // //Debug.Log("Data collected!"); + // // memStream.Seek(0, SeekOrigin.Begin); + // //StreamReader reader = new StreamReader(memStream); + // //string str = reader.ReadToEnd(); //Encoding.ASCII.GetString(memStream.ToArray()); + // string str = Encoding.ASCII.GetString(memStream.ToArray()); + // //Debug.Log(str.Length); + // //memStream.SetLength(0); + // return str; + // } else { + // Debug.log("socket not ready"); + // return ""; + // } + //} + + //public void readThread() { + // if (socketReady) { + // int dim = readImageSize(IMAGE_LENGTH); + // Debug.Log("Dimension of image to be received: " + dim); + // string str = ""; + // if (dim > 0) { + // str = readImage(dim); + // Debug.Log("Length of image received: " + str.Length); + // } else + // Debug.Log("Lost Connection!!!"); + // } else { + // Debug.Log("socket not ready!"); + // } + //} + +} diff --git a/pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/TestOculus.cs b/pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/TestOculus.cs new file mode 100755 index 0000000..55287a4 --- /dev/null +++ b/pyrobolearn/tools/interfaces/vr/oculus_windows/unity-scripts/TestOculus.cs @@ -0,0 +1,165 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using System; + +public class TestOculus : MonoBehaviour { + TCP tcp = new TCP(); + //UDP udp; + OVRCameraRig camera_rig; + OVRPlayerController player_controller; + OVRManager manager; + public bool enableLog = false; + public bool connectTCP = false; + //public bool connectUDP = false; + public AudioClip clip; + private List dataList; + private int total; + + OculusHaptics haptics; + + //TCPImage tcpImage; + + // Use this for initialization + void Start () { + connectTCP = true; + //connectUDP = false; + dataList = new List(); + + log("->Start()"); + + String Host = "10.255.24.97"; //"10.255.24.140"; + Int32 Port = 5113; + if (connectTCP) + tcp.setupSocket(Host, Port); + + //if (connectUDP) + //{ + // Debug.Log("Setting UDP"); + // udp = new UDP(); + // udp.setupUDP(Port); + // Debug.Log("UDP set"); + //} + + //tcpImage = new TCPImage(); + //tcpImage.setupSocket(Host, Port); + + + camera_rig = GameObject.FindObjectOfType(); + player_controller = GameObject.FindObjectOfType(); + manager = GameObject.FindObjectOfType(); + + log("Start()->"); + + // haptics + haptics = new OculusHaptics(); + } + + + + + + + // Update is called once per frame + void Update() + { + OVRInput.Update(); + log("->Update()!"); + + Transform root_anchor = camera_rig.trackingSpace; + Transform centerEyeAnchor = camera_rig.centerEyeAnchor; + Transform leftHandAnchor = camera_rig.leftHandAnchor; + Transform rightHandAnchor = camera_rig.rightHandAnchor; + + + Vector3 eye_P_right_hand = rightHandAnchor.position; // - center_eye_anchor.position; + + Quaternion rightHandQuat = rightHandAnchor.rotation; + Quaternion leftHandQuat = leftHandAnchor.rotation; + Quaternion headQuat = centerEyeAnchor.rotation; + + string msg; + + msg = string.Format("{0:N5}", -headQuat[2]) + ',' + string.Format("{0:N5}", headQuat[0]) + ',' + string.Format("{0:N5}", -headQuat[1]) + ',' + string.Format("{0:N5}", headQuat[3]); + //msg = string.Format("{0:N5}", eye_P_right_hand[2]) + ',' + string.Format("{0:N5}", -eye_P_right_hand[0]) + ',' + string.Format("{0:N5}", eye_P_right_hand[1]) + ','; + //msg += string.Format("{0:N5}", rightHandQuat[2]) + ',' + string.Format("{0:N5}", rightHandQuat[0]) + ',' + string.Format("{0:N5}", rightHandQuat[1]) + ',' + string.Format("{0:N5}", rightHandQuat[3]); + //msg += string.Format("{0:N5}", -right_hand_quat[2]) + ',' + string.Format("{0:N5}", right_hand_quat[3]) + ',' + string.Format("{0:N5}", right_hand_quat[1]) + ',' + string.Format("{0:N5}", right_hand_quat[0]); + + + log(msg); + + if (connectTCP) + { + //Debug.Log("tcp"); + //Debug.Log(msg); + tcp.writeSocket(msg); + //String str = tcp.readSocket(); + //Debug.Log(str); + //Debug.Log(str.Length); + //if (str.Equals("end")) { + // Debug.Log("End"); + // string data = string.Join("", dataList.ToArray()); + // dataList.Clear(); + // total = 0; + // Debug.Log("Total length: "); + // Debug.Log(data.Length); + //} else { + // if (str.Length != 0) { + // total += str.Length; + // Debug.Log(total); + // dataList.Add(str); + // } + //} + } + + //String s = tcpImage.readThread(); + //if (s.Length != 0) { + // Debug.Log("Length of image received: " + s.Length); + // //Debug.Log("First letters: " + s.Substring(0, 20)); + //} + //if (connectUDP) { + // string data = udp.readSocket(); + // Debug.Log(data.Length); + //} + + // haptics + // WARNING: To get it working, you have to wear the headset or activate its proximity sensor!! + if (OVRInput.Get(OVRInput.Button.One)) + { + + Debug.Log("Button A pressed!"); + haptics.Vibrate(VibrationForce.Light); + } + + if (OVRInput.Get(OVRInput.Button.Two)) + { + + Debug.Log("Button B pressed!"); + haptics.Vibrate(VibrationForce.Medium); + } + + if (OVRInput.Get(OVRInput.Button.Three)) + { + + Debug.Log("Button X pressed!"); + haptics.Vibrate(VibrationForce.Hard); + } + + log(" Update()->"); + } + + private void log(string msg) { + if (enableLog) + Debug.Log(msg); + } + + private void logWarning(string msg) { + if (enableLog) + Debug.LogWarning(msg); + } + + + + +} + diff --git a/pyrobolearn/tools/interfaces/vr/vr.py b/pyrobolearn/tools/interfaces/vr/vr.py new file mode 100644 index 0000000..9fbf83b --- /dev/null +++ b/pyrobolearn/tools/interfaces/vr/vr.py @@ -0,0 +1,8 @@ + +from pyrobolearn.tools.interfaces.interface import InputOutputInterface + + +class VRInterface(InputOutputInterface): + r"""Virtual Reality Interface + """ + pass diff --git a/setup.py b/setup.py index 63610d5..e22117f 100755 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( author_email='briandelhaisse@gmail.com', maintainer='Brian Delhaisse', maintainer_email='Brian Delhaisse', - license='(c) Brian Delhaisse', + license='MIT', url='https://github.com/robotlearn/pyrobolearn', platforms=['Linux Ubuntu'], python_requires='2.7'