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
UTCm>#;@4~6zx)#)GCaKhA4F!@PXGV_
literal 0
HcmV?d00001
diff --git a/src/airsim/PythonClient/airsim/__pycache__/types.cpython-313.pyc b/src/airsim/PythonClient/airsim/__pycache__/types.cpython-313.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c139643fe44bb30c0fdf96f189bef3ee249eef58
GIT binary patch
literal 37624
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, msgpack decides to encode it as a string unfortunately.
+ result = self.client.call('simGetImage', camera_name, image_type, vehicle_name, external)
+ if (result == "" or result == "\0"):
+ return None
+ return result
+
+#camera control
+#simGetImage returns compressed png in array of bytes
+#image_type uses one of the ImageType members
+ def simGetImages(self, requests, vehicle_name = '', external = False):
+ """
+ Get multiple images
+
+ See https://microsoft.github.io/AirSim/image_apis/ for details and examples
+
+ Args:
+ requests (list[ImageRequest]): Images required
+ vehicle_name (str, optional): Name of vehicle associated with the camera
+ external (bool, optional): Whether the camera is an External Camera
+
+ Returns:
+ list[ImageResponse]:
+ """
+ responses_raw = self.client.call('simGetImages', requests, vehicle_name, external)
+ return [ImageResponse.from_msgpack(response_raw) for response_raw in responses_raw]
+
+
+
+#CinemAirSim
+ def simGetPresetLensSettings(self, camera_name, vehicle_name = '', external = False):
+ result = self.client.call('simGetPresetLensSettings', camera_name, vehicle_name, external)
+ if (result == "" or result == "\0"):
+ return None
+ return result
+
+ def simGetLensSettings(self, camera_name, vehicle_name = '', external = False):
+ result = self.client.call('simGetLensSettings', camera_name, vehicle_name, external)
+ if (result == "" or result == "\0"):
+ return None
+ return result
+
+ def simSetPresetLensSettings(self, preset_lens_settings, camera_name, vehicle_name = '', external = False):
+ self.client.call("simSetPresetLensSettings", preset_lens_settings, camera_name, vehicle_name, external)
+
+ def simGetPresetFilmbackSettings(self, camera_name, vehicle_name = '', external = False):
+ result = self.client.call('simGetPresetFilmbackSettings', camera_name, vehicle_name, external)
+ if (result == "" or result == "\0"):
+ return None
+ return result
+
+ def simSetPresetFilmbackSettings(self, preset_filmback_settings, camera_name, vehicle_name = '', external = False):
+ self.client.call("simSetPresetFilmbackSettings", preset_filmback_settings, camera_name, vehicle_name, external)
+
+ def simGetFilmbackSettings(self, camera_name, vehicle_name = '', external = False):
+ result = self.client.call('simGetFilmbackSettings', camera_name, vehicle_name, external)
+ if (result == "" or result == "\0"):
+ return None
+ return result
+
+ def simSetFilmbackSettings(self, sensor_width, sensor_height, camera_name, vehicle_name = '', external = False):
+ return self.client.call("simSetFilmbackSettings", sensor_width, sensor_height, camera_name, vehicle_name, external)
+
+ def simGetFocalLength(self, camera_name, vehicle_name = '', external = False):
+ return self.client.call("simGetFocalLength", camera_name, vehicle_name, external)
+
+ def simSetFocalLength(self, focal_length, camera_name, vehicle_name = '', external = False):
+ self.client.call("simSetFocalLength", focal_length, camera_name, vehicle_name, external)
+
+ def simEnableManualFocus(self, enable, camera_name, vehicle_name = '', external = False):
+ self.client.call("simEnableManualFocus", enable, camera_name, vehicle_name, external)
+
+ def simGetFocusDistance(self, camera_name, vehicle_name = '', external = False):
+ return self.client.call("simGetFocusDistance", camera_name, vehicle_name, external)
+
+ def simSetFocusDistance(self, focus_distance, camera_name, vehicle_name = '', external = False):
+ self.client.call("simSetFocusDistance", focus_distance, camera_name, vehicle_name, external)
+
+ def simGetFocusAperture(self, camera_name, vehicle_name = '', external = False):
+ return self.client.call("simGetFocusAperture", camera_name, vehicle_name, external)
+
+ def simSetFocusAperture(self, focus_aperture, camera_name, vehicle_name = '', external = False):
+ self.client.call("simSetFocusAperture", focus_aperture, camera_name, vehicle_name, external)
+
+ def simEnableFocusPlane(self, enable, camera_name, vehicle_name = '', external = False):
+ self.client.call("simEnableFocusPlane", enable, camera_name, vehicle_name, external)
+
+ def simGetCurrentFieldOfView(self, camera_name, vehicle_name = '', external = False):
+ return self.client.call("simGetCurrentFieldOfView", camera_name, vehicle_name, external)
+
+#End CinemAirSim
+ def simTestLineOfSightToPoint(self, point, vehicle_name = ''):
+ """
+ Returns whether the target point is visible from the perspective of the inputted vehicle
+
+ Args:
+ point (GeoPoint): target point
+ vehicle_name (str, optional): Name of vehicle
+
+ Returns:
+ [bool]: Success
+ """
+ return self.client.call('simTestLineOfSightToPoint', point, vehicle_name)
+
+ def simTestLineOfSightBetweenPoints(self, point1, point2):
+ """
+ Returns whether the target point is visible from the perspective of the source point
+
+ Args:
+ point1 (GeoPoint): source point
+ point2 (GeoPoint): target point
+
+ Returns:
+ [bool]: Success
+ """
+ return self.client.call('simTestLineOfSightBetweenPoints', point1, point2)
+
+ def simGetWorldExtents(self):
+ """
+ Returns a list of GeoPoints representing the minimum and maximum extents of the world
+
+ Returns:
+ list[GeoPoint]
+ """
+ responses_raw = self.client.call('simGetWorldExtents')
+ return [GeoPoint.from_msgpack(response_raw) for response_raw in responses_raw]
+
+ def simRunConsoleCommand(self, command):
+ """
+ Allows the client to execute a command in Unreal's native console, via an API.
+ Affords access to the countless built-in commands such as "stat unit", "stat fps", "open [map]", adjust any config settings, etc. etc.
+ Allows the user to create bespoke APIs very easily, by adding a custom event to the level blueprint, and then calling the console command "ce MyEventName [args]". No recompilation of AirSim needed!
+
+ Args:
+ command ([string]): Desired Unreal Engine Console command to run
+
+ Returns:
+ [bool]: Success
+ """
+ return self.client.call('simRunConsoleCommand', command)
+
+#gets the static meshes in the unreal scene
+ def simGetMeshPositionVertexBuffers(self):
+ """
+ Returns the static meshes that make up the scene
+
+ See https://microsoft.github.io/AirSim/meshes/ for details and how to use this
+
+ Returns:
+ list[MeshPositionVertexBuffersResponse]:
+ """
+ responses_raw = self.client.call('simGetMeshPositionVertexBuffers')
+ return [MeshPositionVertexBuffersResponse.from_msgpack(response_raw) for response_raw in responses_raw]
+
+ def simGetCollisionInfo(self, vehicle_name = ''):
+ """
+ Args:
+ vehicle_name (str, optional): Name of the Vehicle to get the info of
+
+ Returns:
+ CollisionInfo:
+ """
+ return CollisionInfo.from_msgpack(self.client.call('simGetCollisionInfo', vehicle_name))
+
+ def simSetVehiclePose(self, pose, ignore_collision, vehicle_name = ''):
+ """
+ Set the pose of the vehicle
+
+ If you don't want to change position (or orientation) then just set components of position (or orientation) to floating point nan values
+
+ Args:
+ pose (Pose): Desired Pose pf the vehicle
+ ignore_collision (bool): Whether to ignore any collision or not
+ vehicle_name (str, optional): Name of the vehicle to move
+ """
+ self.client.call('simSetVehiclePose', pose, ignore_collision, vehicle_name)
+
+ def simGetVehiclePose(self, vehicle_name = ''):
+ """
+ The position inside the returned Pose is in the frame of the vehicle's starting point
+
+ Args:
+ vehicle_name (str, optional): Name of the vehicle to get the Pose of
+
+ Returns:
+ Pose:
+ """
+ pose = self.client.call('simGetVehiclePose', vehicle_name)
+ return Pose.from_msgpack(pose)
+
+ def simSetTraceLine(self, color_rgba, thickness=1.0, vehicle_name = ''):
+ """
+ Modify the color and thickness of the line when Tracing is enabled
+
+ Tracing can be enabled by pressing T in the Editor or setting `EnableTrace` to `True` in the Vehicle Settings
+
+ Args:
+ color_rgba (list): desired RGBA values from 0.0 to 1.0
+ thickness (float, optional): Thickness of the line
+ vehicle_name (string, optional): Name of the vehicle to set Trace line values for
+ """
+ self.client.call('simSetTraceLine', color_rgba, thickness, vehicle_name)
+
+ def simGetObjectPose(self, object_name):
+ """
+ The position inside the returned Pose is in the world frame
+
+ Args:
+ object_name (str): Object to get the Pose of
+
+ Returns:
+ Pose:
+ """
+ pose = self.client.call('simGetObjectPose', object_name)
+ return Pose.from_msgpack(pose)
+
+ def simSetObjectPose(self, object_name, pose, teleport = True):
+ """
+ Set the pose of the object(actor) in the environment
+
+ The specified actor must have Mobility set to movable, otherwise there will be undefined behaviour.
+ See https://www.unrealengine.com/en-US/blog/moving-physical-objects for details on how to set Mobility and the effect of Teleport parameter
+
+ Args:
+ object_name (str): Name of the object(actor) to move
+ pose (Pose): Desired Pose of the object
+ teleport (bool, optional): Whether to move the object immediately without affecting their velocity
+
+ Returns:
+ bool: If the move was successful
+ """
+ return self.client.call('simSetObjectPose', object_name, pose, teleport)
+
+ def simGetObjectScale(self, object_name):
+ """
+ Gets scale of an object in the world
+
+ Args:
+ object_name (str): Object to get the scale of
+
+ Returns:
+ airsim.Vector3r: Scale
+ """
+ scale = self.client.call('simGetObjectScale', object_name)
+ return Vector3r.from_msgpack(scale)
+
+ def simSetObjectScale(self, object_name, scale_vector):
+ """
+ Sets scale of an object in the world
+
+ Args:
+ object_name (str): Object to set the scale of
+ scale_vector (airsim.Vector3r): Desired scale of object
+
+ Returns:
+ bool: True if scale change was successful
+ """
+ return self.client.call('simSetObjectScale', object_name, scale_vector)
+
+ def simListSceneObjects(self, name_regex = '.*'):
+ """
+ Lists the objects present in the environment
+
+ Default behaviour is to list all objects, regex can be used to return smaller list of matching objects or actors
+
+ Args:
+ name_regex (str, optional): String to match actor names against, e.g. "Cylinder.*"
+
+ Returns:
+ list[str]: List containing all the names
+ """
+ return self.client.call('simListSceneObjects', name_regex)
+
+ def simLoadLevel(self, level_name):
+ """
+ Loads a level specified by its name
+
+ Args:
+ level_name (str): Name of the level to load
+
+ Returns:
+ bool: True if the level was successfully loaded
+ """
+ return self.client.call('simLoadLevel', level_name)
+
+ def simListAssets(self):
+ """
+ Lists all the assets present in the Asset Registry
+
+ Returns:
+ list[str]: Names of all the assets
+ """
+ return self.client.call('simListAssets')
+
+ def simSpawnObject(self, object_name, asset_name, pose, scale, physics_enabled=False, is_blueprint=False):
+ """Spawned selected object in the world
+
+ Args:
+ object_name (str): Desired name of new object
+ asset_name (str): Name of asset(mesh) in the project database
+ pose (airsim.Pose): Desired pose of object
+ scale (airsim.Vector3r): Desired scale of object
+ physics_enabled (bool, optional): Whether to enable physics for the object
+ is_blueprint (bool, optional): Whether to spawn a blueprint or an actor
+
+ Returns:
+ str: Name of spawned object, in case it had to be modified
+ """
+ return self.client.call('simSpawnObject', object_name, asset_name, pose, scale, physics_enabled, is_blueprint)
+
+ def simDestroyObject(self, object_name):
+ """Removes selected object from the world
+
+ Args:
+ object_name (str): Name of object to be removed
+
+ Returns:
+ bool: True if object is queued up for removal
+ """
+ return self.client.call('simDestroyObject', object_name)
+
+ def simSetSegmentationObjectID(self, mesh_name, object_id, is_name_regex = False):
+ """
+ Set segmentation ID for specific objects
+
+ See https://microsoft.github.io/AirSim/image_apis/#segmentation for details
+
+ Args:
+ mesh_name (str): Name of the mesh to set the ID of (supports regex)
+ object_id (int): Object ID to be set, range 0-255
+
+ RBG values for IDs can be seen at https://microsoft.github.io/AirSim/seg_rgbs.txt
+ is_name_regex (bool, optional): Whether the mesh name is a regex
+
+ Returns:
+ bool: If the mesh was found
+ """
+ return self.client.call('simSetSegmentationObjectID', mesh_name, object_id, is_name_regex)
+
+ def simGetSegmentationObjectID(self, mesh_name):
+ """
+ Returns Object ID for the given mesh name
+
+ Mapping of Object IDs to RGB values can be seen at https://microsoft.github.io/AirSim/seg_rgbs.txt
+
+ Args:
+ mesh_name (str): Name of the mesh to get the ID of
+ """
+ return self.client.call('simGetSegmentationObjectID', mesh_name)
+
+ def simAddDetectionFilterMeshName(self, camera_name, image_type, mesh_name, vehicle_name = '', external = False):
+ """
+ Add mesh name to detect in wild card format
+
+ For example: simAddDetectionFilterMeshName("Car_*") will detect all instance named "Car_*"
+
+ Args:
+ camera_name (str): Name of the camera, for backwards compatibility, ID numbers such as 0,1,etc. can also be used
+ image_type (ImageType): Type of image required
+ mesh_name (str): mesh name in wild card format
+ vehicle_name (str, optional): Vehicle which the camera is associated with
+ external (bool, optional): Whether the camera is an External Camera
+
+ """
+ self.client.call('simAddDetectionFilterMeshName', camera_name, image_type, mesh_name, vehicle_name, external)
+
+ def simSetDetectionFilterRadius(self, camera_name, image_type, radius_cm, vehicle_name = '', external = False):
+ """
+ Set detection radius for all cameras
+
+ Args:
+ camera_name (str): Name of the camera, for backwards compatibility, ID numbers such as 0,1,etc. can also be used
+ image_type (ImageType): Type of image required
+ radius_cm (int): Radius in [cm]
+ vehicle_name (str, optional): Vehicle which the camera is associated with
+ external (bool, optional): Whether the camera is an External Camera
+ """
+ self.client.call('simSetDetectionFilterRadius', camera_name, image_type, radius_cm, vehicle_name, external)
+
+ def simClearDetectionMeshNames(self, camera_name, image_type, vehicle_name = '', external = False):
+ """
+ Clear all mesh names from detection filter
+
+ Args:
+ camera_name (str): Name of the camera, for backwards compatibility, ID numbers such as 0,1,etc. can also be used
+ image_type (ImageType): Type of image required
+ vehicle_name (str, optional): Vehicle which the camera is associated with
+ external (bool, optional): Whether the camera is an External Camera
+
+ """
+ self.client.call('simClearDetectionMeshNames', camera_name, image_type, vehicle_name, external)
+
+ def simGetDetections(self, camera_name, image_type, vehicle_name = '', external = False):
+ """
+ Get current detections
+
+ Args:
+ camera_name (str): Name of the camera, for backwards compatibility, ID numbers such as 0,1,etc. can also be used
+ image_type (ImageType): Type of image required
+ vehicle_name (str, optional): Vehicle which the camera is associated with
+ external (bool, optional): Whether the camera is an External Camera
+
+ Returns:
+ DetectionInfo array
+ """
+ responses_raw = self.client.call('simGetDetections', camera_name, image_type, vehicle_name, external)
+ return [DetectionInfo.from_msgpack(response_raw) for response_raw in responses_raw]
+
+ def simPrintLogMessage(self, message, message_param = "", severity = 0):
+ """
+ Prints the specified message in the simulator's window.
+
+ If message_param is supplied, then it's printed next to the message and in that case if this API is called with same message value
+ but different message_param again then previous line is overwritten with new line (instead of API creating new line on display).
+
+ For example, `simPrintLogMessage("Iteration: ", to_string(i))` keeps updating same line on display when API is called with different values of i.
+ The valid values of severity parameter is 0 to 3 inclusive that corresponds to different colors.
+
+ Args:
+ message (str): Message to be printed
+ message_param (str, optional): Parameter to be printed next to the message
+ severity (int, optional): Range 0-3, inclusive, corresponding to the severity of the message
+ """
+ self.client.call('simPrintLogMessage', message, message_param, severity)
+
+ def simGetCameraInfo(self, camera_name, vehicle_name = '', external=False):
+ """
+ Get details about the camera
+
+ Args:
+ camera_name (str): Name of the camera, for backwards compatibility, ID numbers such as 0,1,etc. can also be used
+ vehicle_name (str, optional): Vehicle which the camera is associated with
+ external (bool, optional): Whether the camera is an External Camera
+
+ Returns:
+ CameraInfo:
+ """
+#TODO : below str() conversion is only needed for legacy reason and should be removed in future
+ return CameraInfo.from_msgpack(self.client.call('simGetCameraInfo', str(camera_name), vehicle_name, external))
+
+ def simGetDistortionParams(self, camera_name, vehicle_name = '', external = False):
+ """
+ Get camera distortion parameters
+
+ Args:
+ camera_name (str): Name of the camera, for backwards compatibility, ID numbers such as 0,1,etc. can also be used
+ vehicle_name (str, optional): Vehicle which the camera is associated with
+ external (bool, optional): Whether the camera is an External Camera
+
+ Returns:
+ List (float): List of distortion parameter values corresponding to K1, K2, K3, P1, P2 respectively.
+ """
+
+ return self.client.call('simGetDistortionParams', str(camera_name), vehicle_name, external)
+
+ def simSetDistortionParams(self, camera_name, distortion_params, vehicle_name = '', external = False):
+ """
+ Set camera distortion parameters
+
+ Args:
+ camera_name (str): Name of the camera, for backwards compatibility, ID numbers such as 0,1,etc. can also be used
+ distortion_params (dict): Dictionary of distortion param names and corresponding values
+ {"K1": 0.0, "K2": 0.0, "K3": 0.0, "P1": 0.0, "P2": 0.0}
+ vehicle_name (str, optional): Vehicle which the camera is associated with
+ external (bool, optional): Whether the camera is an External Camera
+ """
+
+ for param_name, value in distortion_params.items():
+ self.simSetDistortionParam(camera_name, param_name, value, vehicle_name, external)
+
+ def simSetDistortionParam(self, camera_name, param_name, value, vehicle_name = '', external = False):
+ """
+ Set single camera distortion parameter
+
+ Args:
+ camera_name (str): Name of the camera, for backwards compatibility, ID numbers such as 0,1,etc. can also be used
+ param_name (str): Name of distortion parameter
+ value (float): Value of distortion parameter
+ vehicle_name (str, optional): Vehicle which the camera is associated with
+ external (bool, optional): Whether the camera is an External Camera
+ """
+ self.client.call('simSetDistortionParam', str(camera_name), param_name, value, vehicle_name, external)
+
+ def simSetCameraPose(self, camera_name, pose, vehicle_name = '', external = False):
+ """
+ - Control the pose of a selected camera
+
+ Args:
+ camera_name (str): Name of the camera to be controlled
+ pose (Pose): Pose representing the desired position and orientation of the camera
+ vehicle_name (str, optional): Name of vehicle which the camera corresponds to
+ external (bool, optional): Whether the camera is an External Camera
+ """
+#TODO : below str() conversion is only needed for legacy reason and should be removed in future
+ self.client.call('simSetCameraPose', str(camera_name), pose, vehicle_name, external)
+
+ def simSetCameraFov(self, camera_name, fov_degrees, vehicle_name = '', external = False):
+ """
+ - Control the field of view of a selected camera
+
+ Args:
+ camera_name (str): Name of the camera to be controlled
+ fov_degrees (float): Value of field of view in degrees
+ vehicle_name (str, optional): Name of vehicle which the camera corresponds to
+ external (bool, optional): Whether the camera is an External Camera
+ """
+#TODO : below str() conversion is only needed for legacy reason and should be removed in future
+ self.client.call('simSetCameraFov', str(camera_name), fov_degrees, vehicle_name, external)
+
+ def simGetGroundTruthKinematics(self, vehicle_name = ''):
+ """
+ Get Ground truth kinematics of the vehicle
+
+ The position inside the returned KinematicsState is in the frame of the vehicle's starting point
+
+ Args:
+ vehicle_name (str, optional): Name of the vehicle
+
+ Returns:
+ KinematicsState: Ground truth of the vehicle
+ """
+ kinematics_state = self.client.call('simGetGroundTruthKinematics', vehicle_name)
+ return KinematicsState.from_msgpack(kinematics_state)
+ simGetGroundTruthKinematics.__annotations__ = {'return': KinematicsState}
+
+ def simSetKinematics(self, state, ignore_collision, vehicle_name = ''):
+ """
+ Set the kinematics state of the vehicle
+
+ If you don't want to change position (or orientation) then just set components of position (or orientation) to floating point nan values
+
+ Args:
+ state (KinematicsState): Desired Pose pf the vehicle
+ ignore_collision (bool): Whether to ignore any collision or not
+ vehicle_name (str, optional): Name of the vehicle to move
+ """
+ self.client.call('simSetKinematics', state, ignore_collision, vehicle_name)
+
+ def simGetGroundTruthEnvironment(self, vehicle_name = ''):
+ """
+ Get ground truth environment state
+
+ The position inside the returned EnvironmentState is in the frame of the vehicle's starting point
+
+ Args:
+ vehicle_name (str, optional): Name of the vehicle
+
+ Returns:
+ EnvironmentState: Ground truth environment state
+ """
+ env_state = self.client.call('simGetGroundTruthEnvironment', vehicle_name)
+ return EnvironmentState.from_msgpack(env_state)
+ simGetGroundTruthEnvironment.__annotations__ = {'return': EnvironmentState}
+
+
+#sensor APIs
+ def getImuData(self, imu_name = '', vehicle_name = ''):
+ """
+ Args:
+ imu_name (str, optional): Name of IMU to get data from, specified in settings.json
+ vehicle_name (str, optional): Name of vehicle to which the sensor corresponds to
+
+ Returns:
+ ImuData:
+ """
+ return ImuData.from_msgpack(self.client.call('getImuData', imu_name, vehicle_name))
+
+ def getBarometerData(self, barometer_name = '', vehicle_name = ''):
+ """
+ Args:
+ barometer_name (str, optional): Name of Barometer to get data from, specified in settings.json
+ vehicle_name (str, optional): Name of vehicle to which the sensor corresponds to
+
+ Returns:
+ BarometerData:
+ """
+ return BarometerData.from_msgpack(self.client.call('getBarometerData', barometer_name, vehicle_name))
+
+ def getMagnetometerData(self, magnetometer_name = '', vehicle_name = ''):
+ """
+ Args:
+ magnetometer_name (str, optional): Name of Magnetometer to get data from, specified in settings.json
+ vehicle_name (str, optional): Name of vehicle to which the sensor corresponds to
+
+ Returns:
+ MagnetometerData:
+ """
+ return MagnetometerData.from_msgpack(self.client.call('getMagnetometerData', magnetometer_name, vehicle_name))
+
+ def getGpsData(self, gps_name = '', vehicle_name = ''):
+ """
+ Args:
+ gps_name (str, optional): Name of GPS to get data from, specified in settings.json
+ vehicle_name (str, optional): Name of vehicle to which the sensor corresponds to
+
+ Returns:
+ GpsData:
+ """
+ return GpsData.from_msgpack(self.client.call('getGpsData', gps_name, vehicle_name))
+
+ def getDistanceSensorData(self, distance_sensor_name = '', vehicle_name = ''):
+ """
+ Args:
+ distance_sensor_name (str, optional): Name of Distance Sensor to get data from, specified in settings.json
+ vehicle_name (str, optional): Name of vehicle to which the sensor corresponds to
+
+ Returns:
+ DistanceSensorData:
+ """
+ return DistanceSensorData.from_msgpack(self.client.call('getDistanceSensorData', distance_sensor_name, vehicle_name))
+
+ def getLidarData(self, lidar_name = '', vehicle_name = ''):
+ """
+ Args:
+ lidar_name (str, optional): Name of Lidar to get data from, specified in settings.json
+ vehicle_name (str, optional): Name of vehicle to which the sensor corresponds to
+
+ Returns:
+ LidarData:
+ """
+ return LidarData.from_msgpack(self.client.call('getLidarData', lidar_name, vehicle_name))
+
+ def simGetLidarSegmentation(self, lidar_name = '', vehicle_name = ''):
+ """
+ NOTE: Deprecated API, use `getLidarData()` API instead
+ Returns Segmentation ID of each point's collided object in the last Lidar update
+
+ Args:
+ lidar_name (str, optional): Name of Lidar sensor
+ vehicle_name (str, optional): Name of the vehicle wth the sensor
+
+ Returns:
+ list[int]: Segmentation IDs of the objects
+ """
+ logging.warning("simGetLidarSegmentation API is deprecated, use getLidarData() API instead")
+ return self.getLidarData(lidar_name, vehicle_name).segmentation
+
+#Plotting APIs
+ def simFlushPersistentMarkers(self):
+ """
+ Clear any persistent markers - those plotted with setting `is_persistent=True` in the APIs below
+ """
+ self.client.call('simFlushPersistentMarkers')
+
+ def simPlotPoints(self, points, color_rgba=[1.0, 0.0, 0.0, 1.0], size = 10.0, duration = -1.0, is_persistent = False):
+ """
+ Plot a list of 3D points in World NED frame
+
+ Args:
+ points (list[Vector3r]): List of Vector3r objects
+ color_rgba (list, optional): desired RGBA values from 0.0 to 1.0
+ size (float, optional): Size of plotted point
+ duration (float, optional): Duration (seconds) to plot for
+ is_persistent (bool, optional): If set to True, the desired object will be plotted for infinite time.
+ """
+ self.client.call('simPlotPoints', points, color_rgba, size, duration, is_persistent)
+
+ def simPlotLineStrip(self, points, color_rgba=[1.0, 0.0, 0.0, 1.0], thickness = 5.0, duration = -1.0, is_persistent = False):
+ """
+ Plots a line strip in World NED frame, defined from points[0] to points[1], points[1] to points[2], ... , points[n-2] to points[n-1]
+
+ Args:
+ points (list[Vector3r]): List of 3D locations of line start and end points, specified as Vector3r objects
+ color_rgba (list, optional): desired RGBA values from 0.0 to 1.0
+ thickness (float, optional): Thickness of line
+ duration (float, optional): Duration (seconds) to plot for
+ is_persistent (bool, optional): If set to True, the desired object will be plotted for infinite time.
+ """
+ self.client.call('simPlotLineStrip', points, color_rgba, thickness, duration, is_persistent)
+
+ def simPlotLineList(self, points, color_rgba=[1.0, 0.0, 0.0, 1.0], thickness = 5.0, duration = -1.0, is_persistent = False):
+ """
+ Plots a line strip in World NED frame, defined from points[0] to points[1], points[2] to points[3], ... , points[n-2] to points[n-1]
+
+ Args:
+ points (list[Vector3r]): List of 3D locations of line start and end points, specified as Vector3r objects. Must be even
+ color_rgba (list, optional): desired RGBA values from 0.0 to 1.0
+ thickness (float, optional): Thickness of line
+ duration (float, optional): Duration (seconds) to plot for
+ is_persistent (bool, optional): If set to True, the desired object will be plotted for infinite time.
+ """
+ self.client.call('simPlotLineList', points, color_rgba, thickness, duration, is_persistent)
+
+ def simPlotArrows(self, points_start, points_end, color_rgba=[1.0, 0.0, 0.0, 1.0], thickness = 5.0, arrow_size = 2.0, duration = -1.0, is_persistent = False):
+ """
+ Plots a list of arrows in World NED frame, defined from points_start[0] to points_end[0], points_start[1] to points_end[1], ... , points_start[n-1] to points_end[n-1]
+
+ Args:
+ points_start (list[Vector3r]): List of 3D start positions of arrow start positions, specified as Vector3r objects
+ points_end (list[Vector3r]): List of 3D end positions of arrow start positions, specified as Vector3r objects
+ color_rgba (list, optional): desired RGBA values from 0.0 to 1.0
+ thickness (float, optional): Thickness of line
+ arrow_size (float, optional): Size of arrow head
+ duration (float, optional): Duration (seconds) to plot for
+ is_persistent (bool, optional): If set to True, the desired object will be plotted for infinite time.
+ """
+ self.client.call('simPlotArrows', points_start, points_end, color_rgba, thickness, arrow_size, duration, is_persistent)
+
+
+ def simPlotStrings(self, strings, positions, scale = 5, color_rgba=[1.0, 0.0, 0.0, 1.0], duration = -1.0):
+ """
+ Plots a list of strings at desired positions in World NED frame.
+
+ Args:
+ strings (list[String], optional): List of strings to plot
+ positions (list[Vector3r]): List of positions where the strings should be plotted. Should be in one-to-one correspondence with the strings' list
+ scale (float, optional): Font scale of transform name
+ color_rgba (list, optional): desired RGBA values from 0.0 to 1.0
+ duration (float, optional): Duration (seconds) to plot for
+ """
+ self.client.call('simPlotStrings', strings, positions, scale, color_rgba, duration)
+
+ def simPlotTransforms(self, poses, scale = 5.0, thickness = 5.0, duration = -1.0, is_persistent = False):
+ """
+ Plots a list of transforms in World NED frame.
+
+ Args:
+ poses (list[Pose]): List of Pose objects representing the transforms to plot
+ scale (float, optional): Length of transforms' axes
+ thickness (float, optional): Thickness of transforms' axes
+ duration (float, optional): Duration (seconds) to plot for
+ is_persistent (bool, optional): If set to True, the desired object will be plotted for infinite time.
+ """
+ self.client.call('simPlotTransforms', poses, scale, thickness, duration, is_persistent)
+
+ def simPlotTransformsWithNames(self, poses, names, tf_scale = 5.0, tf_thickness = 5.0, text_scale = 10.0, text_color_rgba = [1.0, 0.0, 0.0, 1.0], duration = -1.0):
+ """
+ Plots a list of transforms with their names in World NED frame.
+
+ Args:
+ poses (list[Pose]): List of Pose objects representing the transforms to plot
+ names (list[string]): List of strings with one-to-one correspondence to list of poses
+ tf_scale (float, optional): Length of transforms' axes
+ tf_thickness (float, optional): Thickness of transforms' axes
+ text_scale (float, optional): Font scale of transform name
+ text_color_rgba (list, optional): desired RGBA values from 0.0 to 1.0 for the transform name
+ duration (float, optional): Duration (seconds) to plot for
+ """
+ self.client.call('simPlotTransformsWithNames', poses, names, tf_scale, tf_thickness, text_scale, text_color_rgba, duration)
+
+ def cancelLastTask(self, vehicle_name = ''):
+ """
+ Cancel previous Async task
+
+ Args:
+ vehicle_name (str, optional): Name of the vehicle
+ """
+ self.client.call('cancelLastTask', vehicle_name)
+
+#Recording APIs
+ def startRecording(self):
+ """
+ Start Recording
+
+ Recording will be done according to the settings
+ """
+ self.client.call('startRecording')
+
+ def stopRecording(self):
+ """
+ Stop Recording
+ """
+ self.client.call('stopRecording')
+
+ def isRecording(self):
+ """
+ Whether Recording is running or not
+
+ Returns:
+ bool: True if Recording, else False
+ """
+ return self.client.call('isRecording')
+
+ def simSetWind(self, wind):
+ """
+ Set simulated wind, in World frame, NED direction, m/s
+
+ Args:
+ wind (Vector3r): Wind, in World frame, NED direction, in m/s
+ """
+ self.client.call('simSetWind', wind)
+
+ def simCreateVoxelGrid(self, position, x, y, z, res, of):
+ """
+ Construct and save a binvox-formatted voxel grid of environment
+
+ Args:
+ position (Vector3r): Position around which voxel grid is centered in m
+ x, y, z (int): Size of each voxel grid dimension in m
+ res (float): Resolution of voxel grid in m
+ of (str): Name of output file to save voxel grid as
+
+ Returns:
+ bool: True if output written to file successfully, else False
+ """
+ return self.client.call('simCreateVoxelGrid', position, x, y, z, res, of)
+
+#Add new vehicle via RPC
+ def simAddVehicle(self, vehicle_name, vehicle_type, pose, pawn_path = ""):
+ """
+ Create vehicle at runtime
+
+ Args:
+ vehicle_name (str): Name of the vehicle being created
+ vehicle_type (str): Type of vehicle, e.g. "simpleflight"
+ pose (Pose): Initial pose of the vehicle
+ pawn_path (str, optional): Vehicle blueprint path, default empty wbich uses the default blueprint for the vehicle type
+
+ Returns:
+ bool: Whether vehicle was created
+ """
+ return self.client.call('simAddVehicle', vehicle_name, vehicle_type, pose, pawn_path)
+
+ def listVehicles(self):
+ """
+ Lists the names of current vehicles
+
+ Returns:
+ list[str]: List containing names of all vehicles
+ """
+ return self.client.call('listVehicles')
+
+ def getSettingsString(self):
+ """
+ Fetch the settings text being used by AirSim
+
+ Returns:
+ str: Settings text in JSON format
+ """
+ return self.client.call('getSettingsString')
+
+#----------------------------------- Multirotor APIs ---------------------------------------------
+class MultirotorClient(VehicleClient, object):
+ def __init__(self, ip = "", port = 41451, timeout_value = 3600):
+ super(MultirotorClient, self).__init__(ip, port, timeout_value)
+
+ def takeoffAsync(self, timeout_sec = 20, vehicle_name = ''):
+ """
+ Takeoff vehicle to 3m above ground. Vehicle should not be moving when this API is used
+
+ Args:
+ timeout_sec (int, optional): Timeout for the vehicle to reach desired altitude
+ vehicle_name (str, optional): Name of the vehicle to send this command to
+
+ Returns:
+ msgpackrpc.future.Future: future. call .join() to wait for method to finish. Example: client.METHOD().join()
+ """
+ return self.client.call_async('takeoff', timeout_sec, vehicle_name)
+
+ def landAsync(self, timeout_sec = 60, vehicle_name = ''):
+ """
+ Land the vehicle
+
+ Args:
+ timeout_sec (int, optional): Timeout for the vehicle to land
+ vehicle_name (str, optional): Name of the vehicle to send this command to
+
+ Returns:
+ msgpackrpc.future.Future: future. call .join() to wait for method to finish. Example: client.METHOD().join()
+ """
+ return self.client.call_async('land', timeout_sec, vehicle_name)
+
+ def goHomeAsync(self, timeout_sec = 3e+38, vehicle_name = ''):
+ """
+ Return vehicle to Home i.e. Launch location
+
+ Args:
+ timeout_sec (int, optional): Timeout for the vehicle to reach desired altitude
+ vehicle_name (str, optional): Name of the vehicle to send this command to
+
+ Returns:
+ msgpackrpc.future.Future: future. call .join() to wait for method to finish. Example: client.METHOD().join()
+ """
+ return self.client.call_async('goHome', timeout_sec, vehicle_name)
+
+#APIs for control
+ def moveByVelocityBodyFrameAsync(self, vx, vy, vz, duration, drivetrain = DrivetrainType.MaxDegreeOfFreedom, yaw_mode = YawMode(), vehicle_name = ''):
+ """
+ Args:
+ vx (float): desired velocity in the X axis of the vehicle's local NED frame.
+ vy (float): desired velocity in the Y axis of the vehicle's local NED frame.
+ vz (float): desired velocity in the Z axis of the vehicle's local NED frame.
+ duration (float): Desired amount of time (seconds), to send this command for
+ drivetrain (DrivetrainType, optional):
+ yaw_mode (YawMode, optional):
+ vehicle_name (str, optional): Name of the multirotor to send this command to
+
+ Returns:
+ msgpackrpc.future.Future: future. call .join() to wait for method to finish. Example: client.METHOD().join()
+ """
+ return self.client.call_async('moveByVelocityBodyFrame', vx, vy, vz, duration, drivetrain, yaw_mode, vehicle_name)
+
+ def moveByVelocityZBodyFrameAsync(self, vx, vy, z, duration, drivetrain = DrivetrainType.MaxDegreeOfFreedom, yaw_mode = YawMode(), vehicle_name = ''):
+ """
+ Args:
+ vx (float): desired velocity in the X axis of the vehicle's local NED frame
+ vy (float): desired velocity in the Y axis of the vehicle's local NED frame
+ z (float): desired Z value (in local NED frame of the vehicle)
+ duration (float): Desired amount of time (seconds), to send this command for
+ drivetrain (DrivetrainType, optional):
+ yaw_mode (YawMode, optional):
+ vehicle_name (str, optional): Name of the multirotor to send this command to
+
+ Returns:
+ msgpackrpc.future.Future: future. call .join() to wait for method to finish. Example: client.METHOD().join()
+ """
+
+ return self.client.call_async('moveByVelocityZBodyFrame', vx, vy, z, duration, drivetrain, yaw_mode, vehicle_name)
+
+ def moveByAngleZAsync(self, pitch, roll, z, yaw, duration, vehicle_name = ''):
+ logging.warning("moveByAngleZAsync API is deprecated, use moveByRollPitchYawZAsync() API instead")
+ return self.client.call_async('moveByRollPitchYawZ', roll, -pitch, -yaw, z, duration, vehicle_name)
+
+ def moveByAngleThrottleAsync(self, pitch, roll, throttle, yaw_rate, duration, vehicle_name = ''):
+ logging.warning("moveByAngleThrottleAsync API is deprecated, use moveByRollPitchYawrateThrottleAsync() API instead")
+ return self.client.call_async('moveByRollPitchYawrateThrottle', roll, -pitch, -yaw_rate, throttle, duration, vehicle_name)
+
+ def moveByVelocityAsync(self, vx, vy, vz, duration, drivetrain = DrivetrainType.MaxDegreeOfFreedom, yaw_mode = YawMode(), vehicle_name = ''):
+ """
+ Args:
+ vx (float): desired velocity in world (NED) X axis
+ vy (float): desired velocity in world (NED) Y axis
+ vz (float): desired velocity in world (NED) Z axis
+ duration (float): Desired amount of time (seconds), to send this command for
+ drivetrain (DrivetrainType, optional):
+ yaw_mode (YawMode, optional):
+ vehicle_name (str, optional): Name of the multirotor to send this command to
+
+ Returns:
+ msgpackrpc.future.Future: future. call .join() to wait for method to finish. Example: client.METHOD().join()
+ """
+ return self.client.call_async('moveByVelocity', vx, vy, vz, duration, drivetrain, yaw_mode, vehicle_name)
+
+ def moveByVelocityZAsync(self, vx, vy, z, duration, drivetrain = DrivetrainType.MaxDegreeOfFreedom, yaw_mode = YawMode(), vehicle_name = ''):
+ return self.client.call_async('moveByVelocityZ', vx, vy, z, duration, drivetrain, yaw_mode, vehicle_name)
+
+ def moveOnPathAsync(self, path, velocity, timeout_sec = 3e+38, drivetrain = DrivetrainType.MaxDegreeOfFreedom, yaw_mode = YawMode(),
+ lookahead = -1, adaptive_lookahead = 1, vehicle_name = ''):
+ return self.client.call_async('moveOnPath', path, velocity, timeout_sec, drivetrain, yaw_mode, lookahead, adaptive_lookahead, vehicle_name)
+
+ def moveToPositionAsync(self, x, y, z, velocity, timeout_sec = 3e+38, drivetrain = DrivetrainType.MaxDegreeOfFreedom, yaw_mode = YawMode(),
+ lookahead = -1, adaptive_lookahead = 1, vehicle_name = ''):
+ return self.client.call_async('moveToPosition', x, y, z, velocity, timeout_sec, drivetrain, yaw_mode, lookahead, adaptive_lookahead, vehicle_name)
+
+ def moveToGPSAsync(self, latitude, longitude, altitude, velocity, timeout_sec = 3e+38, drivetrain = DrivetrainType.MaxDegreeOfFreedom, yaw_mode = YawMode(),
+ lookahead = -1, adaptive_lookahead = 1, vehicle_name = ''):
+ return self.client.call_async('moveToGPS', latitude, longitude, altitude, velocity, timeout_sec, drivetrain, yaw_mode, lookahead, adaptive_lookahead, vehicle_name)
+
+ def moveToZAsync(self, z, velocity, timeout_sec = 3e+38, yaw_mode = YawMode(), lookahead = -1, adaptive_lookahead = 1, vehicle_name = ''):
+ return self.client.call_async('moveToZ', z, velocity, timeout_sec, yaw_mode, lookahead, adaptive_lookahead, vehicle_name)
+
+ def moveByManualAsync(self, vx_max, vy_max, z_min, duration, drivetrain = DrivetrainType.MaxDegreeOfFreedom, yaw_mode = YawMode(), vehicle_name = ''):
+ """
+ - Read current RC state and use it to control the vehicles.
+
+ Parameters sets up the constraints on velocity and minimum altitude while flying. If RC state is detected to violate these constraints
+ then that RC state would be ignored.
+
+ Args:
+ vx_max (float): max velocity allowed in x direction
+ vy_max (float): max velocity allowed in y direction
+ vz_max (float): max velocity allowed in z direction
+ z_min (float): min z allowed for vehicle position
+ duration (float): after this duration vehicle would switch back to non-manual mode
+ drivetrain (DrivetrainType): when ForwardOnly, vehicle rotates itself so that its front is always facing the direction of travel. If MaxDegreeOfFreedom then it doesn't do that (crab-like movement)
+ yaw_mode (YawMode): Specifies if vehicle should face at given angle (is_rate=False) or should be rotating around its axis at given rate (is_rate=True)
+ vehicle_name (str, optional): Name of the multirotor to send this command to
+ Returns:
+ msgpackrpc.future.Future: future. call .join() to wait for method to finish. Example: client.METHOD().join()
+ """
+ return self.client.call_async('moveByManual', vx_max, vy_max, z_min, duration, drivetrain, yaw_mode, vehicle_name)
+
+ def rotateToYawAsync(self, yaw, timeout_sec = 3e+38, margin = 5, vehicle_name = ''):
+ return self.client.call_async('rotateToYaw', yaw, timeout_sec, margin, vehicle_name)
+
+ def rotateByYawRateAsync(self, yaw_rate, duration, vehicle_name = ''):
+ return self.client.call_async('rotateByYawRate', yaw_rate, duration, vehicle_name)
+
+ def hoverAsync(self, vehicle_name = ''):
+ return self.client.call_async('hover', vehicle_name)
+
+ def moveByRC(self, rcdata = RCData(), vehicle_name = ''):
+ return self.client.call('moveByRC', rcdata, vehicle_name)
+
+#low - level control API
+ def moveByMotorPWMsAsync(self, front_right_pwm, rear_left_pwm, front_left_pwm, rear_right_pwm, duration, vehicle_name = ''):
+ """
+ - Directly control the motors using PWM values
+
+ Args:
+ front_right_pwm (float): PWM value for the front right motor (between 0.0 to 1.0)
+ rear_left_pwm (float): PWM value for the rear left motor (between 0.0 to 1.0)
+ front_left_pwm (float): PWM value for the front left motor (between 0.0 to 1.0)
+ rear_right_pwm (float): PWM value for the rear right motor (between 0.0 to 1.0)
+ duration (float): Desired amount of time (seconds), to send this command for
+ vehicle_name (str, optional): Name of the multirotor to send this command to
+ Returns:
+ msgpackrpc.future.Future: future. call .join() to wait for method to finish. Example: client.METHOD().join()
+ """
+ return self.client.call_async('moveByMotorPWMs', front_right_pwm, rear_left_pwm, front_left_pwm, rear_right_pwm, duration, vehicle_name)
+
+ def moveByRollPitchYawZAsync(self, roll, pitch, yaw, z, duration, vehicle_name = ''):
+ """
+ - z is given in local NED frame of the vehicle.
+ - Roll angle, pitch angle, and yaw angle set points are given in **radians**, in the body frame.
+ - The body frame follows the Front Left Up (FLU) convention, and right-handedness.
+
+ - Frame Convention:
+ - X axis is along the **Front** direction of the quadrotor.
+
+ | Clockwise rotation about this axis defines a positive **roll** angle.
+ | Hence, rolling with a positive angle is equivalent to translating in the **right** direction, w.r.t. our FLU body frame.
+
+ - Y axis is along the **Left** direction of the quadrotor.
+
+ | Clockwise rotation about this axis defines a positive **pitch** angle.
+ | Hence, pitching with a positive angle is equivalent to translating in the **front** direction, w.r.t. our FLU body frame.
+
+ - Z axis is along the **Up** direction.
+
+ | Clockwise rotation about this axis defines a positive **yaw** angle.
+ | Hence, yawing with a positive angle is equivalent to rotated towards the **left** direction wrt our FLU body frame. Or in an anticlockwise fashion in the body XY / FL plane.
+
+ Args:
+ roll (float): Desired roll angle, in radians.
+ pitch (float): Desired pitch angle, in radians.
+ yaw (float): Desired yaw angle, in radians.
+ z (float): Desired Z value (in local NED frame of the vehicle)
+ duration (float): Desired amount of time (seconds), to send this command for
+ vehicle_name (str, optional): Name of the multirotor to send this command to
+
+ Returns:
+ msgpackrpc.future.Future: future. call .join() to wait for method to finish. Example: client.METHOD().join()
+ """
+ return self.client.call_async('moveByRollPitchYawZ', roll, -pitch, -yaw, z, duration, vehicle_name)
+
+ def moveByRollPitchYawThrottleAsync(self, roll, pitch, yaw, throttle, duration, vehicle_name = ''):
+ """
+ - Desired throttle is between 0.0 to 1.0
+ - Roll angle, pitch angle, and yaw angle are given in **degrees** when using PX4 and in **radians** when using SimpleFlight, in the body frame.
+ - The body frame follows the Front Left Up (FLU) convention, and right-handedness.
+
+ - Frame Convention:
+ - X axis is along the **Front** direction of the quadrotor.
+
+ | Clockwise rotation about this axis defines a positive **roll** angle.
+ | Hence, rolling with a positive angle is equivalent to translating in the **right** direction, w.r.t. our FLU body frame.
+
+ - Y axis is along the **Left** direction of the quadrotor.
+
+ | Clockwise rotation about this axis defines a positive **pitch** angle.
+ | Hence, pitching with a positive angle is equivalent to translating in the **front** direction, w.r.t. our FLU body frame.
+
+ - Z axis is along the **Up** direction.
+
+ | Clockwise rotation about this axis defines a positive **yaw** angle.
+ | Hence, yawing with a positive angle is equivalent to rotated towards the **left** direction wrt our FLU body frame. Or in an anticlockwise fashion in the body XY / FL plane.
+
+ Args:
+ roll (float): Desired roll angle.
+ pitch (float): Desired pitch angle.
+ yaw (float): Desired yaw angle.
+ throttle (float): Desired throttle (between 0.0 to 1.0)
+ duration (float): Desired amount of time (seconds), to send this command for
+ vehicle_name (str, optional): Name of the multirotor to send this command to
+
+ Returns:
+ msgpackrpc.future.Future: future. call .join() to wait for method to finish. Example: client.METHOD().join()
+ """
+ return self.client.call_async('moveByRollPitchYawThrottle', roll, -pitch, -yaw, throttle, duration, vehicle_name)
+
+ def moveByRollPitchYawrateThrottleAsync(self, roll, pitch, yaw_rate, throttle, duration, vehicle_name = ''):
+ """
+ - Desired throttle is between 0.0 to 1.0
+ - Roll angle, pitch angle, and yaw rate set points are given in **radians**, in the body frame.
+ - The body frame follows the Front Left Up (FLU) convention, and right-handedness.
+
+ - Frame Convention:
+ - X axis is along the **Front** direction of the quadrotor.
+
+ | Clockwise rotation about this axis defines a positive **roll** angle.
+ | Hence, rolling with a positive angle is equivalent to translating in the **right** direction, w.r.t. our FLU body frame.
+
+ - Y axis is along the **Left** direction of the quadrotor.
+
+ | Clockwise rotation about this axis defines a positive **pitch** angle.
+ | Hence, pitching with a positive angle is equivalent to translating in the **front** direction, w.r.t. our FLU body frame.
+
+ - Z axis is along the **Up** direction.
+
+ | Clockwise rotation about this axis defines a positive **yaw** angle.
+ | Hence, yawing with a positive angle is equivalent to rotated towards the **left** direction wrt our FLU body frame. Or in an anticlockwise fashion in the body XY / FL plane.
+
+ Args:
+ roll (float): Desired roll angle, in radians.
+ pitch (float): Desired pitch angle, in radians.
+ yaw_rate (float): Desired yaw rate, in radian per second.
+ throttle (float): Desired throttle (between 0.0 to 1.0)
+ duration (float): Desired amount of time (seconds), to send this command for
+ vehicle_name (str, optional): Name of the multirotor to send this command to
+
+ Returns:
+ msgpackrpc.future.Future: future. call .join() to wait for method to finish. Example: client.METHOD().join()
+ """
+ return self.client.call_async('moveByRollPitchYawrateThrottle', roll, -pitch, -yaw_rate, throttle, duration, vehicle_name)
+
+ def moveByRollPitchYawrateZAsync(self, roll, pitch, yaw_rate, z, duration, vehicle_name = ''):
+ """
+ - z is given in local NED frame of the vehicle.
+ - Roll angle, pitch angle, and yaw rate set points are given in **radians**, in the body frame.
+ - The body frame follows the Front Left Up (FLU) convention, and right-handedness.
+
+ - Frame Convention:
+ - X axis is along the **Front** direction of the quadrotor.
+
+ | Clockwise rotation about this axis defines a positive **roll** angle.
+ | Hence, rolling with a positive angle is equivalent to translating in the **right** direction, w.r.t. our FLU body frame.
+
+ - Y axis is along the **Left** direction of the quadrotor.
+
+ | Clockwise rotation about this axis defines a positive **pitch** angle.
+ | Hence, pitching with a positive angle is equivalent to translating in the **front** direction, w.r.t. our FLU body frame.
+
+ - Z axis is along the **Up** direction.
+
+ | Clockwise rotation about this axis defines a positive **yaw** angle.
+ | Hence, yawing with a positive angle is equivalent to rotated towards the **left** direction wrt our FLU body frame. Or in an anticlockwise fashion in the body XY / FL plane.
+
+ Args:
+ roll (float): Desired roll angle, in radians.
+ pitch (float): Desired pitch angle, in radians.
+ yaw_rate (float): Desired yaw rate, in radian per second.
+ z (float): Desired Z value (in local NED frame of the vehicle)
+ duration (float): Desired amount of time (seconds), to send this command for
+ vehicle_name (str, optional): Name of the multirotor to send this command to
+
+ Returns:
+ msgpackrpc.future.Future: future. call .join() to wait for method to finish. Example: client.METHOD().join()
+ """
+ return self.client.call_async('moveByRollPitchYawrateZ', roll, -pitch, -yaw_rate, z, duration, vehicle_name)
+
+ def moveByAngleRatesZAsync(self, roll_rate, pitch_rate, yaw_rate, z, duration, vehicle_name = ''):
+ """
+ - z is given in local NED frame of the vehicle.
+ - Roll rate, pitch rate, and yaw rate set points are given in **radians**, in the body frame.
+ - The body frame follows the Front Left Up (FLU) convention, and right-handedness.
+
+ - Frame Convention:
+ - X axis is along the **Front** direction of the quadrotor.
+
+ | Clockwise rotation about this axis defines a positive **roll** angle.
+ | Hence, rolling with a positive angle is equivalent to translating in the **right** direction, w.r.t. our FLU body frame.
+
+ - Y axis is along the **Left** direction of the quadrotor.
+
+ | Clockwise rotation about this axis defines a positive **pitch** angle.
+ | Hence, pitching with a positive angle is equivalent to translating in the **front** direction, w.r.t. our FLU body frame.
+
+ - Z axis is along the **Up** direction.
+
+ | Clockwise rotation about this axis defines a positive **yaw** angle.
+ | Hence, yawing with a positive angle is equivalent to rotated towards the **left** direction wrt our FLU body frame. Or in an anticlockwise fashion in the body XY / FL plane.
+
+ Args:
+ roll_rate (float): Desired roll rate, in radians / second
+ pitch_rate (float): Desired pitch rate, in radians / second
+ yaw_rate (float): Desired yaw rate, in radians / second
+ z (float): Desired Z value (in local NED frame of the vehicle)
+ duration (float): Desired amount of time (seconds), to send this command for
+ vehicle_name (str, optional): Name of the multirotor to send this command to
+
+ Returns:
+ msgpackrpc.future.Future: future. call .join() to wait for method to finish. Example: client.METHOD().join()
+ """
+ return self.client.call_async('moveByAngleRatesZ', roll_rate, -pitch_rate, -yaw_rate, z, duration, vehicle_name)
+
+ def moveByAngleRatesThrottleAsync(self, roll_rate, pitch_rate, yaw_rate, throttle, duration, vehicle_name = ''):
+ """
+ - Desired throttle is between 0.0 to 1.0
+ - Roll rate, pitch rate, and yaw rate set points are given in **radians**, in the body frame.
+ - The body frame follows the Front Left Up (FLU) convention, and right-handedness.
+
+ - Frame Convention:
+ - X axis is along the **Front** direction of the quadrotor.
+
+ | Clockwise rotation about this axis defines a positive **roll** angle.
+ | Hence, rolling with a positive angle is equivalent to translating in the **right** direction, w.r.t. our FLU body frame.
+
+ - Y axis is along the **Left** direction of the quadrotor.
+
+ | Clockwise rotation about this axis defines a positive **pitch** angle.
+ | Hence, pitching with a positive angle is equivalent to translating in the **front** direction, w.r.t. our FLU body frame.
+
+ - Z axis is along the **Up** direction.
+
+ | Clockwise rotation about this axis defines a positive **yaw** angle.
+ | Hence, yawing with a positive angle is equivalent to rotated towards the **left** direction wrt our FLU body frame. Or in an anticlockwise fashion in the body XY / FL plane.
+
+ Args:
+ roll_rate (float): Desired roll rate, in radians / second
+ pitch_rate (float): Desired pitch rate, in radians / second
+ yaw_rate (float): Desired yaw rate, in radians / second
+ throttle (float): Desired throttle (between 0.0 to 1.0)
+ duration (float): Desired amount of time (seconds), to send this command for
+ vehicle_name (str, optional): Name of the multirotor to send this command to
+
+ Returns:
+ msgpackrpc.future.Future: future. call .join() to wait for method to finish. Example: client.METHOD().join()
+ """
+ return self.client.call_async('moveByAngleRatesThrottle', roll_rate, -pitch_rate, -yaw_rate, throttle, duration, vehicle_name)
+
+ def setAngleRateControllerGains(self, angle_rate_gains=AngleRateControllerGains(), vehicle_name = ''):
+ """
+ - Modifying these gains will have an affect on *ALL* move*() APIs.
+ This is because any velocity setpoint is converted to an angle level setpoint which is tracked with an angle level controllers.
+ That angle level setpoint is itself tracked with and angle rate controller.
+ - This function should only be called if the default angle rate control PID gains need to be modified.
+
+ Args:
+ angle_rate_gains (AngleRateControllerGains):
+ - Correspond to the roll, pitch, yaw axes, defined in the body frame.
+ - Pass AngleRateControllerGains() to reset gains to default recommended values.
+ vehicle_name (str, optional): Name of the multirotor to send this command to
+ """
+ self.client.call('setAngleRateControllerGains', *(angle_rate_gains.to_lists()+(vehicle_name,)))
+
+ def setAngleLevelControllerGains(self, angle_level_gains=AngleLevelControllerGains(), vehicle_name = ''):
+ """
+ - Sets angle level controller gains (used by any API setting angle references - for ex: moveByRollPitchYawZAsync(), moveByRollPitchYawThrottleAsync(), etc)
+ - Modifying these gains will also affect the behaviour of moveByVelocityAsync() API.
+ This is because the AirSim flight controller will track velocity setpoints by converting them to angle set points.
+ - This function should only be called if the default angle level control PID gains need to be modified.
+ - Passing AngleLevelControllerGains() sets gains to default airsim values.
+
+ Args:
+ angle_level_gains (AngleLevelControllerGains):
+ - Correspond to the roll, pitch, yaw axes, defined in the body frame.
+ - Pass AngleLevelControllerGains() to reset gains to default recommended values.
+ vehicle_name (str, optional): Name of the multirotor to send this command to
+ """
+ self.client.call('setAngleLevelControllerGains', *(angle_level_gains.to_lists()+(vehicle_name,)))
+
+ def setVelocityControllerGains(self, velocity_gains=VelocityControllerGains(), vehicle_name = ''):
+ """
+ - Sets velocity controller gains for moveByVelocityAsync().
+ - This function should only be called if the default velocity control PID gains need to be modified.
+ - Passing VelocityControllerGains() sets gains to default airsim values.
+
+ Args:
+ velocity_gains (VelocityControllerGains):
+ - Correspond to the world X, Y, Z axes.
+ - Pass VelocityControllerGains() to reset gains to default recommended values.
+ - Modifying velocity controller gains will have an affect on the behaviour of moveOnSplineAsync() and moveOnSplineVelConstraintsAsync(), as they both use velocity control to track the trajectory.
+ vehicle_name (str, optional): Name of the multirotor to send this command to
+ """
+ self.client.call('setVelocityControllerGains', *(velocity_gains.to_lists()+(vehicle_name,)))
+
+
+ def setPositionControllerGains(self, position_gains=PositionControllerGains(), vehicle_name = ''):
+ """
+ Sets position controller gains for moveByPositionAsync.
+ This function should only be called if the default position control PID gains need to be modified.
+
+ Args:
+ position_gains (PositionControllerGains):
+ - Correspond to the X, Y, Z axes.
+ - Pass PositionControllerGains() to reset gains to default recommended values.
+ vehicle_name (str, optional): Name of the multirotor to send this command to
+ """
+ self.client.call('setPositionControllerGains', *(position_gains.to_lists()+(vehicle_name,)))
+
+#query vehicle state
+ def getMultirotorState(self, vehicle_name = ''):
+ """
+ The position inside the returned MultirotorState is in the frame of the vehicle's starting point
+
+ Args:
+ vehicle_name (str, optional): Vehicle to get the state of
+
+ Returns:
+ MultirotorState:
+ """
+ return MultirotorState.from_msgpack(self.client.call('getMultirotorState', vehicle_name))
+ getMultirotorState.__annotations__ = {'return': MultirotorState}
+#query rotor states
+ def getRotorStates(self, vehicle_name = ''):
+ """
+ Used to obtain the current state of all a multirotor's rotors. The state includes the speeds,
+ thrusts and torques for all rotors.
+
+ Args:
+ vehicle_name (str, optional): Vehicle to get the rotor state of
+
+ Returns:
+ RotorStates: Containing a timestamp and the speed, thrust and torque of all rotors.
+ """
+ return RotorStates.from_msgpack(self.client.call('getRotorStates', vehicle_name))
+ getRotorStates.__annotations__ = {'return': RotorStates}
+
+#----------------------------------- Car APIs ---------------------------------------------
+class CarClient(VehicleClient, object):
+ def __init__(self, ip = "", port = 41451, timeout_value = 3600):
+ super(CarClient, self).__init__(ip, port, timeout_value)
+
+ def setCarControls(self, controls, vehicle_name = ''):
+ """
+ Control the car using throttle, steering, brake, etc.
+
+ Args:
+ controls (CarControls): Struct containing control values
+ vehicle_name (str, optional): Name of vehicle to be controlled
+ """
+ self.client.call('setCarControls', controls, vehicle_name)
+
+ def getCarState(self, vehicle_name = ''):
+ """
+ The position inside the returned CarState is in the frame of the vehicle's starting point
+
+ Args:
+ vehicle_name (str, optional): Name of vehicle
+
+ Returns:
+ CarState:
+ """
+ state_raw = self.client.call('getCarState', vehicle_name)
+ return CarState.from_msgpack(state_raw)
+
+ def getCarControls(self, vehicle_name=''):
+ """
+ Args:
+ vehicle_name (str, optional): Name of vehicle
+
+ Returns:
+ CarControls:
+ """
+ controls_raw = self.client.call('getCarControls', vehicle_name)
+ return CarControls.from_msgpack(controls_raw)
\ No newline at end of file
diff --git a/src/airsim/PythonClient/airsim/pfm.py b/src/airsim/PythonClient/airsim/pfm.py
new file mode 100644
index 0000000000..6f9f963a8a
--- /dev/null
+++ b/src/airsim/PythonClient/airsim/pfm.py
@@ -0,0 +1,85 @@
+import numpy as np
+import matplotlib.pyplot as plt
+import re
+import sys
+import pdb
+
+
+def read_pfm(file):
+ """ Read a pfm file """
+ file = open(file, 'rb')
+
+ color = None
+ width = None
+ height = None
+ scale = None
+ endian = None
+
+ header = file.readline().rstrip()
+ header = str(bytes.decode(header, encoding='utf-8'))
+ if header == 'PF':
+ color = True
+ elif header == 'Pf':
+ color = False
+ else:
+ raise Exception('Not a PFM file.')
+
+ pattern = r'^(\d+)\s(\d+)\s$'
+ temp_str = str(bytes.decode(file.readline(), encoding='utf-8'))
+ dim_match = re.match(pattern, temp_str)
+ if dim_match:
+ width, height = map(int, dim_match.groups())
+ else:
+ temp_str += str(bytes.decode(file.readline(), encoding='utf-8'))
+ dim_match = re.match(pattern, temp_str)
+ if dim_match:
+ width, height = map(int, dim_match.groups())
+ else:
+ raise Exception('Malformed PFM header: width, height cannot be found')
+
+ scale = float(file.readline().rstrip())
+ if scale < 0: # little-endian
+ endian = '<'
+ scale = -scale
+ else:
+ endian = '>' # big-endian
+
+ data = np.fromfile(file, endian + 'f')
+ shape = (height, width, 3) if color else (height, width)
+
+ data = np.reshape(data, shape)
+ # DEY: I don't know why this was there.
+ file.close()
+
+ return data, scale
+
+
+def write_pfm(file, image, scale=1):
+ """ Write a pfm file """
+ file = open(file, 'wb')
+
+ color = None
+
+ if image.dtype.name != 'float32':
+ raise Exception('Image dtype must be float32.')
+
+ if len(image.shape) == 3 and image.shape[2] == 3: # color image
+ color = True
+ elif len(image.shape) == 2 or len(image.shape) == 3 and image.shape[2] == 1: # greyscale
+ color = False
+ else:
+ raise Exception('Image must have H x W x 3, H x W x 1 or H x W dimensions.')
+
+ file.write(bytes('PF\n', 'UTF-8') if color else bytes('Pf\n', 'UTF-8'))
+ temp_str = '%d %d\n' % (image.shape[1], image.shape[0])
+ file.write(bytes(temp_str, 'UTF-8'))
+
+ endian = image.dtype.byteorder
+
+ if endian == '<' or endian == '=' and sys.byteorder == 'little':
+ scale = -scale
+
+ temp_str = '%f\n' % scale
+ file.write(bytes(temp_str, 'UTF-8'))
+
+ image.tofile(file)
diff --git a/src/airsim/PythonClient/airsim/types.py b/src/airsim/PythonClient/airsim/types.py
new file mode 100644
index 0000000000..7aef005549
--- /dev/null
+++ b/src/airsim/PythonClient/airsim/types.py
@@ -0,0 +1,580 @@
+from __future__ import print_function
+import msgpackrpc #install as admin: pip install msgpack-rpc-python
+import numpy as np #pip install numpy
+import math
+
+class MsgpackMixin:
+ def __repr__(self):
+ from pprint import pformat
+ return "<" + type(self).__name__ + "> " + pformat(vars(self), indent=4, width=1)
+
+ def to_msgpack(self, *args, **kwargs):
+ return self.__dict__
+
+ @classmethod
+ def from_msgpack(cls, encoded):
+ obj = cls()
+ #obj.__dict__ = {k.decode('utf-8'): (from_msgpack(v.__class__, v) if hasattr(v, "__dict__") else v) for k, v in encoded.items()}
+ obj.__dict__ = { k : (v if not isinstance(v, dict) else getattr(getattr(obj, k).__class__, "from_msgpack")(v)) for k, v in encoded.items()}
+ #return cls(**msgpack.unpack(encoded))
+ return obj
+
+class _ImageType(type):
+ @property
+ def Scene(cls):
+ return 0
+ def DepthPlanar(cls):
+ return 1
+ def DepthPerspective(cls):
+ return 2
+ def DepthVis(cls):
+ return 3
+ def DisparityNormalized(cls):
+ return 4
+ def Segmentation(cls):
+ return 5
+ def SurfaceNormals(cls):
+ return 6
+ def Infrared(cls):
+ return 7
+ def OpticalFlow(cls):
+ return 8
+ def OpticalFlowVis(cls):
+ return 9
+
+ def __getattr__(self, key):
+ if key == 'DepthPlanner':
+ print('\033[31m'+"DepthPlanner has been (correctly) renamed to DepthPlanar. Please use ImageType.DepthPlanar instead."+'\033[0m')
+ raise AttributeError
+
+class ImageType(metaclass=_ImageType):
+ Scene = 0
+ DepthPlanar = 1
+ DepthPerspective = 2
+ DepthVis = 3
+ DisparityNormalized = 4
+ Segmentation = 5
+ SurfaceNormals = 6
+ Infrared = 7
+ OpticalFlow = 8
+ OpticalFlowVis = 9
+
+class DrivetrainType:
+ MaxDegreeOfFreedom = 0
+ ForwardOnly = 1
+
+class LandedState:
+ Landed = 0
+ Flying = 1
+
+class WeatherParameter:
+ Rain = 0
+ Roadwetness = 1
+ Snow = 2
+ RoadSnow = 3
+ MapleLeaf = 4
+ RoadLeaf = 5
+ Dust = 6
+ Fog = 7
+ Enabled = 8
+
+class Vector2r(MsgpackMixin):
+ x_val = 0.0
+ y_val = 0.0
+
+ def __init__(self, x_val = 0.0, y_val = 0.0):
+ self.x_val = x_val
+ self.y_val = y_val
+
+class Vector3r(MsgpackMixin):
+ x_val = 0.0
+ y_val = 0.0
+ z_val = 0.0
+
+ def __init__(self, x_val = 0.0, y_val = 0.0, z_val = 0.0):
+ self.x_val = x_val
+ self.y_val = y_val
+ self.z_val = z_val
+
+ @staticmethod
+ def nanVector3r():
+ return Vector3r(np.nan, np.nan, np.nan)
+
+ def containsNan(self):
+ return (math.isnan(self.x_val) or math.isnan(self.y_val) or math.isnan(self.z_val))
+
+ def __add__(self, other):
+ return Vector3r(self.x_val + other.x_val, self.y_val + other.y_val, self.z_val + other.z_val)
+
+ def __sub__(self, other):
+ return Vector3r(self.x_val - other.x_val, self.y_val - other.y_val, self.z_val - other.z_val)
+
+ def __truediv__(self, other):
+ if type(other) in [int, float] + np.sctypes['int'] + np.sctypes['uint'] + np.sctypes['float']:
+ return Vector3r( self.x_val / other, self.y_val / other, self.z_val / other)
+ else:
+ raise TypeError('unsupported operand type(s) for /: %s and %s' % ( str(type(self)), str(type(other))) )
+
+ def __mul__(self, other):
+ if type(other) in [int, float] + np.sctypes['int'] + np.sctypes['uint'] + np.sctypes['float']:
+ return Vector3r(self.x_val*other, self.y_val*other, self.z_val*other)
+ else:
+ raise TypeError('unsupported operand type(s) for *: %s and %s' % ( str(type(self)), str(type(other))) )
+
+ def dot(self, other):
+ if type(self) == type(other):
+ return self.x_val*other.x_val + self.y_val*other.y_val + self.z_val*other.z_val
+ else:
+ raise TypeError('unsupported operand type(s) for \'dot\': %s and %s' % ( str(type(self)), str(type(other))) )
+
+ def cross(self, other):
+ if type(self) == type(other):
+ cross_product = np.cross(self.to_numpy_array(), other.to_numpy_array())
+ return Vector3r(cross_product[0], cross_product[1], cross_product[2])
+ else:
+ raise TypeError('unsupported operand type(s) for \'cross\': %s and %s' % ( str(type(self)), str(type(other))) )
+
+ def get_length(self):
+ return ( self.x_val**2 + self.y_val**2 + self.z_val**2 )**0.5
+
+ def distance_to(self, other):
+ return ( (self.x_val-other.x_val)**2 + (self.y_val-other.y_val)**2 + (self.z_val-other.z_val)**2 )**0.5
+
+ def to_Quaternionr(self):
+ return Quaternionr(self.x_val, self.y_val, self.z_val, 0)
+
+ def to_numpy_array(self):
+ return np.array([self.x_val, self.y_val, self.z_val], dtype=np.float32)
+
+ def __iter__(self):
+ return iter((self.x_val, self.y_val, self.z_val))
+
+class Quaternionr(MsgpackMixin):
+ w_val = 0.0
+ x_val = 0.0
+ y_val = 0.0
+ z_val = 0.0
+
+ def __init__(self, x_val = 0.0, y_val = 0.0, z_val = 0.0, w_val = 1.0):
+ self.x_val = x_val
+ self.y_val = y_val
+ self.z_val = z_val
+ self.w_val = w_val
+
+ @staticmethod
+ def nanQuaternionr():
+ return Quaternionr(np.nan, np.nan, np.nan, np.nan)
+
+ def containsNan(self):
+ return (math.isnan(self.w_val) or math.isnan(self.x_val) or math.isnan(self.y_val) or math.isnan(self.z_val))
+
+ def __add__(self, other):
+ if type(self) == type(other):
+ return Quaternionr( self.x_val+other.x_val, self.y_val+other.y_val, self.z_val+other.z_val, self.w_val+other.w_val )
+ else:
+ raise TypeError('unsupported operand type(s) for +: %s and %s' % ( str(type(self)), str(type(other))) )
+
+ def __mul__(self, other):
+ if type(self) == type(other):
+ t, x, y, z = self.w_val, self.x_val, self.y_val, self.z_val
+ a, b, c, d = other.w_val, other.x_val, other.y_val, other.z_val
+ return Quaternionr( w_val = a*t - b*x - c*y - d*z,
+ x_val = b*t + a*x + d*y - c*z,
+ y_val = c*t + a*y + b*z - d*x,
+ z_val = d*t + z*a + c*x - b*y)
+ else:
+ raise TypeError('unsupported operand type(s) for *: %s and %s' % ( str(type(self)), str(type(other))) )
+
+ def __truediv__(self, other):
+ if type(other) == type(self):
+ return self * other.inverse()
+ elif type(other) in [int, float] + np.sctypes['int'] + np.sctypes['uint'] + np.sctypes['float']:
+ return Quaternionr( self.x_val / other, self.y_val / other, self.z_val / other, self.w_val / other)
+ else:
+ raise TypeError('unsupported operand type(s) for /: %s and %s' % ( str(type(self)), str(type(other))) )
+
+ def dot(self, other):
+ if type(self) == type(other):
+ return self.x_val*other.x_val + self.y_val*other.y_val + self.z_val*other.z_val + self.w_val*other.w_val
+ else:
+ raise TypeError('unsupported operand type(s) for \'dot\': %s and %s' % ( str(type(self)), str(type(other))) )
+
+ def cross(self, other):
+ if type(self) == type(other):
+ return (self * other - other * self) / 2
+ else:
+ raise TypeError('unsupported operand type(s) for \'cross\': %s and %s' % ( str(type(self)), str(type(other))) )
+
+ def outer_product(self, other):
+ if type(self) == type(other):
+ return ( self.inverse()*other - other.inverse()*self ) / 2
+ else:
+ raise TypeError('unsupported operand type(s) for \'outer_product\': %s and %s' % ( str(type(self)), str(type(other))) )
+
+ def rotate(self, other):
+ if type(self) == type(other):
+ if other.get_length() == 1:
+ return other * self * other.inverse()
+ else:
+ raise ValueError('length of the other Quaternionr must be 1')
+ else:
+ raise TypeError('unsupported operand type(s) for \'rotate\': %s and %s' % ( str(type(self)), str(type(other))) )
+
+ def conjugate(self):
+ return Quaternionr(-self.x_val, -self.y_val, -self.z_val, self.w_val)
+
+ def star(self):
+ return self.conjugate()
+
+ def inverse(self):
+ return self.star() / self.dot(self)
+
+ def sgn(self):
+ return self/self.get_length()
+
+ def get_length(self):
+ return ( self.x_val**2 + self.y_val**2 + self.z_val**2 + self.w_val**2 )**0.5
+
+ def to_numpy_array(self):
+ return np.array([self.x_val, self.y_val, self.z_val, self.w_val], dtype=np.float32)
+
+ def __iter__(self):
+ return iter((self.x_val, self.y_val, self.z_val, self.w_val))
+
+class Pose(MsgpackMixin):
+ position = Vector3r()
+ orientation = Quaternionr()
+
+ def __init__(self, position_val = None, orientation_val = None):
+ position_val = position_val if position_val is not None else Vector3r()
+ orientation_val = orientation_val if orientation_val is not None else Quaternionr()
+ self.position = position_val
+ self.orientation = orientation_val
+
+ @staticmethod
+ def nanPose():
+ return Pose(Vector3r.nanVector3r(), Quaternionr.nanQuaternionr())
+
+ def containsNan(self):
+ return (self.position.containsNan() or self.orientation.containsNan())
+
+ def __iter__(self):
+ return iter((self.position, self.orientation))
+
+class CollisionInfo(MsgpackMixin):
+ has_collided = False
+ normal = Vector3r()
+ impact_point = Vector3r()
+ position = Vector3r()
+ penetration_depth = 0.0
+ time_stamp = 0.0
+ object_name = ""
+ object_id = -1
+
+class GeoPoint(MsgpackMixin):
+ latitude = 0.0
+ longitude = 0.0
+ altitude = 0.0
+
+class YawMode(MsgpackMixin):
+ is_rate = True
+ yaw_or_rate = 0.0
+ def __init__(self, is_rate = True, yaw_or_rate = 0.0):
+ self.is_rate = is_rate
+ self.yaw_or_rate = yaw_or_rate
+
+class RCData(MsgpackMixin):
+ timestamp = 0
+ pitch, roll, throttle, yaw = (0.0,)*4 #init 4 variable to 0.0
+ switch1, switch2, switch3, switch4 = (0,)*4
+ switch5, switch6, switch7, switch8 = (0,)*4
+ is_initialized = False
+ is_valid = False
+ def __init__(self, timestamp = 0, pitch = 0.0, roll = 0.0, throttle = 0.0, yaw = 0.0, switch1 = 0,
+ switch2 = 0, switch3 = 0, switch4 = 0, switch5 = 0, switch6 = 0, switch7 = 0, switch8 = 0, is_initialized = False, is_valid = False):
+ self.timestamp = timestamp
+ self.pitch = pitch
+ self.roll = roll
+ self.throttle = throttle
+ self.yaw = yaw
+ self.switch1 = switch1
+ self.switch2 = switch2
+ self.switch3 = switch3
+ self.switch4 = switch4
+ self.switch5 = switch5
+ self.switch6 = switch6
+ self.switch7 = switch7
+ self.switch8 = switch8
+ self.is_initialized = is_initialized
+ self.is_valid = is_valid
+
+class ImageRequest(MsgpackMixin):
+ camera_name = '0'
+ image_type = ImageType.Scene
+ pixels_as_float = False
+ compress = False
+
+ def __init__(self, camera_name, image_type, pixels_as_float = False, compress = True):
+ # todo: in future remove str(), it's only for compatibility to pre v1.2
+ self.camera_name = str(camera_name)
+ self.image_type = image_type
+ self.pixels_as_float = pixels_as_float
+ self.compress = compress
+
+
+class ImageResponse(MsgpackMixin):
+ image_data_uint8 = np.uint8(0)
+ image_data_float = 0.0
+ camera_position = Vector3r()
+ camera_orientation = Quaternionr()
+ time_stamp = np.uint64(0)
+ message = ''
+ pixels_as_float = 0.0
+ compress = True
+ width = 0
+ height = 0
+ image_type = ImageType.Scene
+
+class CarControls(MsgpackMixin):
+ throttle = 0.0
+ steering = 0.0
+ brake = 0.0
+ handbrake = False
+ is_manual_gear = False
+ manual_gear = 0
+ gear_immediate = True
+
+ def __init__(self, throttle = 0, steering = 0, brake = 0,
+ handbrake = False, is_manual_gear = False, manual_gear = 0, gear_immediate = True):
+ self.throttle = throttle
+ self.steering = steering
+ self.brake = brake
+ self.handbrake = handbrake
+ self.is_manual_gear = is_manual_gear
+ self.manual_gear = manual_gear
+ self.gear_immediate = gear_immediate
+
+
+ def set_throttle(self, throttle_val, forward):
+ if (forward):
+ self.is_manual_gear = False
+ self.manual_gear = 0
+ self.throttle = abs(throttle_val)
+ else:
+ self.is_manual_gear = False
+ self.manual_gear = -1
+ self.throttle = - abs(throttle_val)
+
+class KinematicsState(MsgpackMixin):
+ position = Vector3r()
+ orientation = Quaternionr()
+ linear_velocity = Vector3r()
+ angular_velocity = Vector3r()
+ linear_acceleration = Vector3r()
+ angular_acceleration = Vector3r()
+
+class EnvironmentState(MsgpackMixin):
+ position = Vector3r()
+ geo_point = GeoPoint()
+ gravity = Vector3r()
+ air_pressure = 0.0
+ temperature = 0.0
+ air_density = 0.0
+
+class CarState(MsgpackMixin):
+ speed = 0.0
+ gear = 0
+ rpm = 0.0
+ maxrpm = 0.0
+ handbrake = False
+ collision = CollisionInfo()
+ kinematics_estimated = KinematicsState()
+ timestamp = np.uint64(0)
+
+class MultirotorState(MsgpackMixin):
+ collision = CollisionInfo()
+ kinematics_estimated = KinematicsState()
+ gps_location = GeoPoint()
+ timestamp = np.uint64(0)
+ landed_state = LandedState.Landed
+ rc_data = RCData()
+ ready = False
+ ready_message = ""
+ can_arm = False
+
+class RotorStates(MsgpackMixin):
+ timestamp = np.uint64(0)
+ rotors = []
+
+class ProjectionMatrix(MsgpackMixin):
+ matrix = []
+
+class CameraInfo(MsgpackMixin):
+ pose = Pose()
+ fov = -1
+ proj_mat = ProjectionMatrix()
+
+class LidarData(MsgpackMixin):
+ point_cloud = 0.0
+ time_stamp = np.uint64(0)
+ pose = Pose()
+ segmentation = 0
+
+class ImuData(MsgpackMixin):
+ time_stamp = np.uint64(0)
+ orientation = Quaternionr()
+ angular_velocity = Vector3r()
+ linear_acceleration = Vector3r()
+
+class BarometerData(MsgpackMixin):
+ time_stamp = np.uint64(0)
+ altitude = Quaternionr()
+ pressure = Vector3r()
+ qnh = Vector3r()
+
+class MagnetometerData(MsgpackMixin):
+ time_stamp = np.uint64(0)
+ magnetic_field_body = Vector3r()
+ magnetic_field_covariance = 0.0
+
+class GnssFixType(MsgpackMixin):
+ GNSS_FIX_NO_FIX = 0
+ GNSS_FIX_TIME_ONLY = 1
+ GNSS_FIX_2D_FIX = 2
+ GNSS_FIX_3D_FIX = 3
+
+class GnssReport(MsgpackMixin):
+ geo_point = GeoPoint()
+ eph = 0.0
+ epv = 0.0
+ velocity = Vector3r()
+ fix_type = GnssFixType()
+ time_utc = np.uint64(0)
+
+class GpsData(MsgpackMixin):
+ time_stamp = np.uint64(0)
+ gnss = GnssReport()
+ is_valid = False
+
+class DistanceSensorData(MsgpackMixin):
+ time_stamp = np.uint64(0)
+ distance = 0.0
+ min_distance = 0.0
+ max_distance = 0.0
+ relative_pose = Pose()
+
+class Box2D(MsgpackMixin):
+ min = Vector2r()
+ max = Vector2r()
+
+class Box3D(MsgpackMixin):
+ min = Vector3r()
+ max = Vector3r()
+
+class DetectionInfo(MsgpackMixin):
+ name = ''
+ geo_point = GeoPoint()
+ box2D = Box2D()
+ box3D = Box3D()
+ relative_pose = Pose()
+
+class PIDGains():
+ """
+ Struct to store values of PID gains. Used to transmit controller gain values while instantiating
+ AngleLevel/AngleRate/Velocity/PositionControllerGains objects.
+
+ Attributes:
+ kP (float): Proportional gain
+ kI (float): Integrator gain
+ kD (float): Derivative gain
+ """
+ def __init__(self, kp, ki, kd):
+ self.kp = kp
+ self.ki = ki
+ self.kd = kd
+
+ def to_list(self):
+ return [self.kp, self.ki, self.kd]
+
+class AngleRateControllerGains():
+ """
+ Struct to contain controller gains used by angle level PID controller
+
+ Attributes:
+ roll_gains (PIDGains): kP, kI, kD for roll axis
+ pitch_gains (PIDGains): kP, kI, kD for pitch axis
+ yaw_gains (PIDGains): kP, kI, kD for yaw axis
+ """
+ def __init__(self, roll_gains = PIDGains(0.25, 0, 0),
+ pitch_gains = PIDGains(0.25, 0, 0),
+ yaw_gains = PIDGains(0.25, 0, 0)):
+ self.roll_gains = roll_gains
+ self.pitch_gains = pitch_gains
+ self.yaw_gains = yaw_gains
+
+ def to_lists(self):
+ return [self.roll_gains.kp, self.pitch_gains.kp, self.yaw_gains.kp], [self.roll_gains.ki, self.pitch_gains.ki, self.yaw_gains.ki], [self.roll_gains.kd, self.pitch_gains.kd, self.yaw_gains.kd]
+
+class AngleLevelControllerGains():
+ """
+ Struct to contain controller gains used by angle rate PID controller
+
+ Attributes:
+ roll_gains (PIDGains): kP, kI, kD for roll axis
+ pitch_gains (PIDGains): kP, kI, kD for pitch axis
+ yaw_gains (PIDGains): kP, kI, kD for yaw axis
+ """
+ def __init__(self, roll_gains = PIDGains(2.5, 0, 0),
+ pitch_gains = PIDGains(2.5, 0, 0),
+ yaw_gains = PIDGains(2.5, 0, 0)):
+ self.roll_gains = roll_gains
+ self.pitch_gains = pitch_gains
+ self.yaw_gains = yaw_gains
+
+ def to_lists(self):
+ return [self.roll_gains.kp, self.pitch_gains.kp, self.yaw_gains.kp], [self.roll_gains.ki, self.pitch_gains.ki, self.yaw_gains.ki], [self.roll_gains.kd, self.pitch_gains.kd, self.yaw_gains.kd]
+
+class VelocityControllerGains():
+ """
+ Struct to contain controller gains used by velocity PID controller
+
+ Attributes:
+ x_gains (PIDGains): kP, kI, kD for X axis
+ y_gains (PIDGains): kP, kI, kD for Y axis
+ z_gains (PIDGains): kP, kI, kD for Z axis
+ """
+ def __init__(self, x_gains = PIDGains(0.2, 0, 0),
+ y_gains = PIDGains(0.2, 0, 0),
+ z_gains = PIDGains(2.0, 2.0, 0)):
+ self.x_gains = x_gains
+ self.y_gains = y_gains
+ self.z_gains = z_gains
+
+ def to_lists(self):
+ return [self.x_gains.kp, self.y_gains.kp, self.z_gains.kp], [self.x_gains.ki, self.y_gains.ki, self.z_gains.ki], [self.x_gains.kd, self.y_gains.kd, self.z_gains.kd]
+
+class PositionControllerGains():
+ """
+ Struct to contain controller gains used by position PID controller
+
+ Attributes:
+ x_gains (PIDGains): kP, kI, kD for X axis
+ y_gains (PIDGains): kP, kI, kD for Y axis
+ z_gains (PIDGains): kP, kI, kD for Z axis
+ """
+ def __init__(self, x_gains = PIDGains(0.25, 0, 0),
+ y_gains = PIDGains(0.25, 0, 0),
+ z_gains = PIDGains(0.25, 0, 0)):
+ self.x_gains = x_gains
+ self.y_gains = y_gains
+ self.z_gains = z_gains
+
+ def to_lists(self):
+ return [self.x_gains.kp, self.y_gains.kp, self.z_gains.kp], [self.x_gains.ki, self.y_gains.ki, self.z_gains.ki], [self.x_gains.kd, self.y_gains.kd, self.z_gains.kd]
+
+class MeshPositionVertexBuffersResponse(MsgpackMixin):
+ position = Vector3r()
+ orientation = Quaternionr()
+ vertices = 0.0
+ indices = 0.0
+ name = ''
diff --git a/src/airsim/PythonClient/airsim/utils.py b/src/airsim/PythonClient/airsim/utils.py
new file mode 100644
index 0000000000..7f866d7d0a
--- /dev/null
+++ b/src/airsim/PythonClient/airsim/utils.py
@@ -0,0 +1,208 @@
+import numpy as np #pip install numpy
+import math
+import time
+import sys
+import os
+import inspect
+import types
+import re
+import logging
+
+from .types import *
+
+
+def string_to_uint8_array(bstr):
+ return np.fromstring(bstr, np.uint8)
+
+def string_to_float_array(bstr):
+ return np.fromstring(bstr, np.float32)
+
+def list_to_2d_float_array(flst, width, height):
+ return np.reshape(np.asarray(flst, np.float32), (height, width))
+
+def get_pfm_array(response):
+ return list_to_2d_float_array(response.image_data_float, response.width, response.height)
+
+
+def get_public_fields(obj):
+ return [attr for attr in dir(obj)
+ if not (attr.startswith("_")
+ or inspect.isbuiltin(attr)
+ or inspect.isfunction(attr)
+ or inspect.ismethod(attr))]
+
+
+
+def to_dict(obj):
+ return dict([attr, getattr(obj, attr)] for attr in get_public_fields(obj))
+
+
+def to_str(obj):
+ return str(to_dict(obj))
+
+
+def write_file(filename, bstr):
+ """
+ Write binary data to file.
+ Used for writing compressed PNG images
+ """
+ with open(filename, 'wb') as afile:
+ afile.write(bstr)
+
+# helper method for converting getOrientation to roll/pitch/yaw
+# https:#en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles
+
+def to_eularian_angles(q):
+ z = q.z_val
+ y = q.y_val
+ x = q.x_val
+ w = q.w_val
+ ysqr = y * y
+
+ # roll (x-axis rotation)
+ t0 = +2.0 * (w*x + y*z)
+ t1 = +1.0 - 2.0*(x*x + ysqr)
+ roll = math.atan2(t0, t1)
+
+ # pitch (y-axis rotation)
+ t2 = +2.0 * (w*y - z*x)
+ if (t2 > 1.0):
+ t2 = 1
+ if (t2 < -1.0):
+ t2 = -1.0
+ pitch = math.asin(t2)
+
+ # yaw (z-axis rotation)
+ t3 = +2.0 * (w*z + x*y)
+ t4 = +1.0 - 2.0 * (ysqr + z*z)
+ yaw = math.atan2(t3, t4)
+
+ return (pitch, roll, yaw)
+
+
+def to_quaternion(pitch, roll, yaw):
+ t0 = math.cos(yaw * 0.5)
+ t1 = math.sin(yaw * 0.5)
+ t2 = math.cos(roll * 0.5)
+ t3 = math.sin(roll * 0.5)
+ t4 = math.cos(pitch * 0.5)
+ t5 = math.sin(pitch * 0.5)
+
+ q = Quaternionr()
+ q.w_val = t0 * t2 * t4 + t1 * t3 * t5 #w
+ q.x_val = t0 * t3 * t4 - t1 * t2 * t5 #x
+ q.y_val = t0 * t2 * t5 + t1 * t3 * t4 #y
+ q.z_val = t1 * t2 * t4 - t0 * t3 * t5 #z
+ return q
+
+
+def wait_key(message = ''):
+ ''' Wait for a key press on the console and return it. '''
+ if message != '':
+ print (message)
+
+ result = None
+ if os.name == 'nt':
+ import msvcrt
+ result = msvcrt.getch()
+ else:
+ import termios
+ fd = sys.stdin.fileno()
+
+ oldterm = termios.tcgetattr(fd)
+ newattr = termios.tcgetattr(fd)
+ newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO
+ termios.tcsetattr(fd, termios.TCSANOW, newattr)
+
+ try:
+ result = sys.stdin.read(1)
+ except IOError:
+ pass
+ finally:
+ termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm)
+
+ return result
+
+
+def read_pfm(file):
+ """ Read a pfm file """
+ file = open(file, 'rb')
+
+ color = None
+ width = None
+ height = None
+ scale = None
+ endian = None
+
+ header = file.readline().rstrip()
+ header = str(bytes.decode(header, encoding='utf-8'))
+ if header == 'PF':
+ color = True
+ elif header == 'Pf':
+ color = False
+ else:
+ raise Exception('Not a PFM file.')
+
+ temp_str = str(bytes.decode(file.readline(), encoding='utf-8'))
+ dim_match = re.match(r'^(\d+)\s(\d+)\s$', temp_str)
+ if dim_match:
+ width, height = map(int, dim_match.groups())
+ else:
+ raise Exception('Malformed PFM header.')
+
+ scale = float(file.readline().rstrip())
+ if scale < 0: # little-endian
+ endian = '<'
+ scale = -scale
+ else:
+ endian = '>' # big-endian
+
+ data = np.fromfile(file, endian + 'f')
+ shape = (height, width, 3) if color else (height, width)
+
+ data = np.reshape(data, shape)
+ # DEY: I don't know why this was there.
+ file.close()
+
+ return data, scale
+
+
+def write_pfm(file, image, scale=1):
+ """ Write a pfm file """
+ file = open(file, 'wb')
+
+ color = None
+
+ if image.dtype.name != 'float32':
+ raise Exception('Image dtype must be float32.')
+
+ if len(image.shape) == 3 and image.shape[2] == 3: # color image
+ color = True
+ elif len(image.shape) == 2 or len(image.shape) == 3 and image.shape[2] == 1: # grayscale
+ color = False
+ else:
+ raise Exception('Image must have H x W x 3, H x W x 1 or H x W dimensions.')
+
+ file.write('PF\n'.encode('utf-8') if color else 'Pf\n'.encode('utf-8'))
+ temp_str = '%d %d\n' % (image.shape[1], image.shape[0])
+ file.write(temp_str.encode('utf-8'))
+
+ endian = image.dtype.byteorder
+
+ if endian == '<' or endian == '=' and sys.byteorder == 'little':
+ scale = -scale
+
+ temp_str = '%f\n' % scale
+ file.write(temp_str.encode('utf-8'))
+
+ image.tofile(file)
+
+
+def write_png(filename, image):
+ """ image must be numpy array H X W X channels
+ """
+ import cv2 # pip install opencv-python
+
+ ret = cv2.imwrite(filename, image)
+ if not ret:
+ logging.error(f"Writing PNG file {filename} failed")
diff --git a/src/airsim/PythonClient/build_api_docs.sh b/src/airsim/PythonClient/build_api_docs.sh
new file mode 100644
index 0000000000..382c3122b7
--- /dev/null
+++ b/src/airsim/PythonClient/build_api_docs.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+cd docs
+make html
diff --git a/src/airsim/PythonClient/car/__pycache__/setup_path.cpython-313.pyc b/src/airsim/PythonClient/car/__pycache__/setup_path.cpython-313.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..8bbf2e97b9f5ea8705393b9c8d38174a7639a888
GIT binary patch
literal 3101
zcmcIm&2JM&6rc6S+KwF@$Kj(Pz!IV+n>bc!1r7+BB2YP`B#}+jO0l%s*c-FOS)184
z23iqLD6|Lg0U4x9gbE2F^}wOW{s}!K-BP1bZtaCzkde6d&8*j6r*8R>I#Ficyf^R7
z?EAeB*SovB5R9LCeqR{$A@nER*u&Ex>R$nI7o|{&n?l%HIn1%W3%l6r#_m065T#uG
zDCIulvNW+L?MVi0bFKAAgA`DX6Fo2JZaUKvBh=7G^(=7jqBNqn!A%@?r#>!82(59`B2Qceop_4y
zdg9j#c!^Q&yrL1WUd*coaqCJ^bYXfQ2~ep-kLmRNfdb}d)2s5&`#%7MKjVF8K-sXL5UiV7q5~~PBG4?_zUH-lGn+urhDT~
zK8+4N4$}?vSKqGc*~-~k-_S5;pC9?z-j#$On}tx531!#xw>3gepG(E{NwV^|9=s+zxwhTl@9e5wm
zS`gT-r;(2L(+LbBEWqUSEv->4x3<@)CVzk`H6s5VJi0nzMq}%tSVt$)NMKGZ!VBNt
ziF43{kC!2)Z-RTM4K|X^(q(g?3w7u$pzX4IGxVW%*XLTjU30xbzeC@jbJ>|O(s-zR
z5WXX1zq;dh&?>cUq&xt0qw~UV2>6?)`NT$&7MX1WewTRdo9TIxpJp*Zcv;1|x^yj&
zVNDFW7N%yeeQEK9r9!rpR|KtK2#bZ1mK9V@D6%YseLkz;xG<-yS`PTSA?Nc-7Q~s0
za!wIOifYkj3zj)CE_PWZ;6u2imHou?bwSk#uUt`eLx)a%RVO@HR4`Qbd?A;E8;Gwg
zV+{z=O?c}GBxsps1%m{wPq3;7jVwWERQRZz&7NXS{T%xoR_6D$Do6+MA<);a!E^(?
z>OsAGYmwoN$gmj^SNP|_$eo4nPj2=PJQ%(|{2+EewtD%=6|;Y0Wy;C>=INB#{~7Qa
zZQ%N?>-R3!!lN7E(aoM{)nD;f!_J%L^<@>aaWfX1}MWBQ}XdKmqX
zZ7(MN6PBYs#nAl_t%5quxh*%xaj$nEZhsB!d(DqMDd1MK3z
E0f+!f3IG5A
literal 0
HcmV?d00001
diff --git a/src/airsim/PythonClient/car/car_collision.py b/src/airsim/PythonClient/car/car_collision.py
new file mode 100644
index 0000000000..f3b75374eb
--- /dev/null
+++ b/src/airsim/PythonClient/car/car_collision.py
@@ -0,0 +1,39 @@
+import setup_path
+import airsim
+
+import pprint
+import time
+
+# connect to the AirSim simulator
+client = airsim.CarClient()
+client.confirmConnection()
+client.enableApiControl(True)
+car_controls = airsim.CarControls()
+
+client.reset()
+
+client.simPrintLogMessage("Hello", "345", 2)
+
+# go forward
+car_controls.throttle = 0.5
+car_controls.steering = 0
+client.setCarControls(car_controls)
+
+while True:
+ # get state of the car
+ car_state = client.getCarState()
+ print("Speed %d, Gear %d" % (car_state.speed, car_state.gear))
+
+ collision_info = client.simGetCollisionInfo()
+
+ if collision_info.has_collided:
+ print("Collision at pos %s, normal %s, impact pt %s, penetration %f, name %s, obj id %d" % (
+ pprint.pformat(collision_info.position),
+ pprint.pformat(collision_info.normal),
+ pprint.pformat(collision_info.impact_point),
+ collision_info.penetration_depth, collision_info.object_name, collision_info.object_id))
+ break
+
+ time.sleep(0.1)
+
+client.enableApiControl(False)
diff --git a/src/airsim/PythonClient/car/car_lidar.py b/src/airsim/PythonClient/car/car_lidar.py
new file mode 100644
index 0000000000..37b5844509
--- /dev/null
+++ b/src/airsim/PythonClient/car/car_lidar.py
@@ -0,0 +1,95 @@
+# Python client example to get Lidar data from a car
+#
+
+import setup_path
+import airsim
+
+import sys
+import math
+import time
+import argparse
+import pprint
+import numpy
+
+# Makes the drone fly and get Lidar data
+class LidarTest:
+
+ def __init__(self):
+
+ # connect to the AirSim simulator
+ self.client = airsim.CarClient()
+ self.client.confirmConnection()
+ self.client.enableApiControl(True)
+ self.car_controls = airsim.CarControls()
+
+ def execute(self):
+
+ for i in range(3):
+
+ state = self.client.getCarState()
+ s = pprint.pformat(state)
+ #print("state: %s" % s)
+
+ # go forward
+ self.car_controls.throttle = 0.5
+ self.car_controls.steering = 0
+ self.client.setCarControls(self.car_controls)
+ print("Go Forward")
+ time.sleep(3) # let car drive a bit
+
+ # Go forward + steer right
+ self.car_controls.throttle = 0.5
+ self.car_controls.steering = 1
+ self.client.setCarControls(self.car_controls)
+ print("Go Forward, steer right")
+ time.sleep(3) # let car drive a bit
+
+ airsim.wait_key('Press any key to get Lidar readings')
+
+ for i in range(1,3):
+ lidarData = self.client.getLidarData();
+ if (len(lidarData.point_cloud) < 3):
+ print("\tNo points received from Lidar data")
+ else:
+ points = self.parse_lidarData(lidarData)
+ print("\tReading %d: time_stamp: %d number_of_points: %d" % (i, lidarData.time_stamp, len(points)))
+ print("\t\tlidar position: %s" % (pprint.pformat(lidarData.pose.position)))
+ print("\t\tlidar orientation: %s" % (pprint.pformat(lidarData.pose.orientation)))
+ time.sleep(5)
+
+ def parse_lidarData(self, data):
+
+ # reshape array of floats to array of [X,Y,Z]
+ points = numpy.array(data.point_cloud, dtype=numpy.dtype('f4'))
+ points = numpy.reshape(points, (int(points.shape[0]/3), 3))
+
+ return points
+
+ def write_lidarData_to_disk(self, points):
+ # TODO
+ print("not yet implemented")
+
+ def stop(self):
+
+ airsim.wait_key('Press any key to reset to original state')
+
+ self.client.reset()
+
+ self.client.enableApiControl(False)
+ print("Done!\n")
+
+# main
+if __name__ == "__main__":
+ args = sys.argv
+ args.pop(0)
+
+ arg_parser = argparse.ArgumentParser("Lidar.py makes car move and gets Lidar data")
+
+ arg_parser.add_argument('-save-to-disk', type=bool, help="save Lidar data to disk", default=False)
+
+ args = arg_parser.parse_args(args)
+ lidarTest = LidarTest()
+ try:
+ lidarTest.execute()
+ finally:
+ lidarTest.stop()
diff --git a/src/airsim/PythonClient/car/car_monitor.py b/src/airsim/PythonClient/car/car_monitor.py
new file mode 100644
index 0000000000..c82fef6d97
--- /dev/null
+++ b/src/airsim/PythonClient/car/car_monitor.py
@@ -0,0 +1,26 @@
+import setup_path
+import airsim
+
+import cv2 #conda install opencv
+import time
+
+# connect to the AirSim simulator
+client = airsim.CarClient()
+client.confirmConnection()
+car_controls = airsim.CarControls()
+
+start = time.time()
+
+print("Time,Speed,Gear,PX,PY,PZ,OW,OX,OY,OZ")
+
+# monitor car state while you drive it manually.
+while (cv2.waitKey(1) & 0xFF) == 0xFF:
+ # get state of the car
+ car_state = client.getCarState()
+ pos = car_state.kinematics_estimated.position
+ orientation = car_state.kinematics_estimated.orientation
+ milliseconds = (time.time() - start) * 1000
+ print("%s,%d,%d,%f,%f,%f,%f,%f,%f,%f" % \
+ (milliseconds, car_state.speed, car_state.gear, pos.x_val, pos.y_val, pos.z_val,
+ orientation.w_val, orientation.x_val, orientation.y_val, orientation.z_val))
+ time.sleep(0.1)
diff --git a/src/airsim/PythonClient/car/car_stress_test.py b/src/airsim/PythonClient/car/car_stress_test.py
new file mode 100644
index 0000000000..e7bcf539ce
--- /dev/null
+++ b/src/airsim/PythonClient/car/car_stress_test.py
@@ -0,0 +1,51 @@
+import setup_path
+import airsim
+
+import time
+
+# connect to the AirSim simulator
+client = airsim.CarClient()
+client.confirmConnection()
+client.enableApiControl(True)
+car_controls = airsim.CarControls()
+
+for idx in range(3000):
+ # get state of the car
+ car_state = client.getCarState()
+ print("Speed %d, Gear %d" % (car_state.speed, car_state.gear))
+
+ # go forward
+ car_controls.throttle = 0.5
+ car_controls.steering = 0
+ client.setCarControls(car_controls)
+ time.sleep(1) # let car drive a bit
+
+ # Go forward + steer right
+ car_controls.throttle = 0.5
+ car_controls.steering = 1
+ client.setCarControls(car_controls)
+ time.sleep(1) # let car drive a bit
+
+ # go reverse
+ car_controls.throttle = -0.5
+ car_controls.is_manual_gear = True;
+ car_controls.manual_gear = -1
+ car_controls.steering = 0
+ client.setCarControls(car_controls)
+ time.sleep(1) # let car drive a bit
+ car_controls.is_manual_gear = False; # change back gear to auto
+ car_controls.manual_gear = 0
+
+ # apply breaks
+ car_controls.brake = 1
+ client.setCarControls(car_controls)
+ time.sleep(1) # let car drive a bit
+ car_controls.brake = 0 #remove break
+
+ #restore to original state
+ client.reset()
+
+client.enableApiControl(False)
+
+
+
diff --git a/src/airsim/PythonClient/car/car_time_of_day.py b/src/airsim/PythonClient/car/car_time_of_day.py
new file mode 100644
index 0000000000..2ff7bf5888
--- /dev/null
+++ b/src/airsim/PythonClient/car/car_time_of_day.py
@@ -0,0 +1,79 @@
+# Python client example to change time-of-day using APIs
+#
+
+import setup_path
+import airsim
+
+import sys
+import math
+import time
+import argparse
+import pprint
+import numpy
+
+# Changes time of the day and makes the car move around
+class TimeOfDayTest:
+
+ def __init__(self):
+
+ # connect to the AirSim simulator
+ self.client = airsim.CarClient()
+ self.client.confirmConnection()
+ self.client.enableApiControl(True)
+ self.car_controls = airsim.CarControls()
+
+ def execute(self):
+
+ for i in range(8):
+
+ # flip between specific time and default time
+ enabled = False
+ if (i % 2 == 0):
+ enabled = True
+
+ self.setTimeOfDay(enabled, "2018-11-27 {}:00:00".format(8+ (i * 2)))
+
+ # go forward
+ self.car_controls.throttle = 0.5
+ self.car_controls.steering = 0
+ self.client.setCarControls(self.car_controls)
+ print("Go Forward")
+ time.sleep(3) # let car drive a bit
+
+ # Go forward + steer right
+ self.car_controls.throttle = 0.5
+ self.car_controls.steering = 1
+ self.client.setCarControls(self.car_controls)
+ print("Go Forward, steer right")
+ time.sleep(3) # let car drive a bit
+
+ def setTimeOfDay(self, enabled, time_of_day):
+ if (enabled):
+ airsim.wait_key('Press any key to change time of day to [{}]'.format(time_of_day))
+ self.client.simSetTimeOfDay(enabled, time_of_day)
+ else:
+ airsim.wait_key('Press any key to change time of day to default time')
+ self.client.simSetTimeOfDay(enabled)
+
+ def stop(self):
+
+ airsim.wait_key('Press any key to reset to original state')
+
+ self.client.reset()
+
+ self.client.enableApiControl(False)
+ print("Done!\n")
+
+# main
+if __name__ == "__main__":
+ args = sys.argv
+ args.pop(0)
+
+ arg_parser = argparse.ArgumentParser("TimeOfDay.py changes time of the day")
+
+ args = arg_parser.parse_args(args)
+ timeOfDayTest = TimeOfDayTest()
+ try:
+ timeOfDayTest.execute()
+ finally:
+ timeOfDayTest.stop()
diff --git a/src/airsim/PythonClient/car/distance_sensor_multi.py b/src/airsim/PythonClient/car/distance_sensor_multi.py
new file mode 100644
index 0000000000..5e4660ba69
--- /dev/null
+++ b/src/airsim/PythonClient/car/distance_sensor_multi.py
@@ -0,0 +1,49 @@
+import airsim
+import time
+
+'''
+An example script showing usage of Distance sensor to measure distance between 2 Car vehicles
+Settings -
+
+{
+ "SettingsVersion": 1.2,
+ "SimMode": "Car",
+ "Vehicles": {
+ "Car1": {
+ "VehicleType": "PhysXCar",
+ "AutoCreate": true,
+ "Sensors": {
+ "Distance": {
+ "SensorType": 5,
+ "Enabled" : true,
+ "DrawDebugPoints": true
+ }
+ }
+ },
+ "Car2": {
+ "VehicleType": "PhysXCar",
+ "AutoCreate": true,
+ "X": 10, "Y": 0, "Z": 0,
+ "Sensors": {
+ "Distance": {
+ "SensorType": 5,
+ "Enabled" : true,
+ "DrawDebugPoints": true
+ }
+ }
+ }
+ }
+}
+
+Car2 is placed in front of Car 1
+
+'''
+
+client = airsim.CarClient()
+client.confirmConnection()
+
+while True:
+ data_car1 = client.getDistanceSensorData(vehicle_name="Car1")
+ data_car2 = client.getDistanceSensorData(vehicle_name="Car2")
+ print(f"Distance sensor data: Car1: {data_car1.distance}, Car2: {data_car2.distance}")
+ time.sleep(1.0)
\ No newline at end of file
diff --git a/src/airsim/PythonClient/car/drive_straight.py b/src/airsim/PythonClient/car/drive_straight.py
new file mode 100644
index 0000000000..6783ef8038
--- /dev/null
+++ b/src/airsim/PythonClient/car/drive_straight.py
@@ -0,0 +1,51 @@
+import setup_path
+import airsim
+
+#from keras.models import load_model
+import sys
+import numpy as np
+
+#if (len(sys.argv) != 2):
+# print('usage: python drive.py ')
+# sys.exit()
+
+#print('Loading model...')
+#model = load_model(sys.argv[1])
+
+# connect to the AirSim simulator
+client = airsim.CarClient()
+client.confirmConnection()
+client.enableApiControl(True)
+car_controls = airsim.CarControls()
+
+car_controls.steering = 0
+car_controls.throttle = 0
+car_controls.brake = 0
+
+image_buf = np.zeros((1, 144, 256, 3))
+state_buf = np.zeros((1,4))
+
+def get_image():
+ image = client.simGetImages([airsim.ImageRequest("0", airsim.ImageType.Scene, False, False)])[0]
+ image1d = np.fromstring(image.image_data_uint8, dtype=np.uint8)
+ image_rgb = image1d.reshape(image.height, image.width, 3)
+ return image_rgb
+
+while (True):
+ car_state = client.getCarState()
+
+ print('car speed: {0}'.format(car_state.speed))
+
+ if (car_state.speed < 20):
+ car_controls.throttle = 1.0
+ else:
+ car_controls.throttle = 0.0
+
+ #state_buf[0] = np.array([car_controls.steering, car_controls.throttle, car_controls.brake, car_state.speed])
+ #model_output = model.predict([image_buf, state_buf])
+ #car_controls.steering = float(model_output[0][0])
+ car_controls.steering = 0
+
+ print('Sending steering = {0}, throttle = {1}'.format(car_controls.steering, car_controls.throttle))
+
+ client.setCarControls(car_controls)
\ No newline at end of file
diff --git a/src/airsim/PythonClient/car/hello_car.py b/src/airsim/PythonClient/car/hello_car.py
new file mode 100644
index 0000000000..d6c01728eb
--- /dev/null
+++ b/src/airsim/PythonClient/car/hello_car.py
@@ -0,0 +1,116 @@
+import setup_path
+import airsim
+import cv2
+import numpy as np
+import os
+import time
+import tempfile
+from pathlib import Path
+
+
+def setup_client():
+ """初始化并配置 AirSim 客户端"""
+ client = airsim.CarClient()
+ client.confirmConnection()
+ client.enableApiControl(True)
+ print(f"API Control enabled: {client.isApiControlEnabled()}")
+ return client
+
+
+def setup_image_directory():
+ """创建图像保存目录"""
+ tmp_dir = Path(tempfile.gettempdir()) / "airsim_car"
+ tmp_dir.mkdir(parents=True, exist_ok=True)
+ print(f"Saving images to {tmp_dir}")
+ return tmp_dir
+
+
+def execute_maneuver(client, throttle, steering, duration, gear_mode=None, description=""):
+ """执行单次驾驶操作"""
+ car_controls = airsim.CarControls()
+ car_controls.throttle = throttle
+ car_controls.steering = steering
+
+ if gear_mode == "reverse":
+ car_controls.is_manual_gear = True
+ car_controls.manual_gear = -1
+
+ client.setCarControls(car_controls)
+ print(description)
+ time.sleep(duration)
+
+ # 还原手动档
+ if gear_mode == "reverse":
+ car_controls.is_manual_gear = False
+ car_controls.manual_gear = 0
+ client.setCarControls(car_controls)
+
+
+def apply_brake(client, duration=3):
+ """应用刹车"""
+ car_controls = airsim.CarControls()
+ car_controls.brake = 1
+ client.setCarControls(car_controls)
+ print("Apply brakes")
+ time.sleep(duration)
+
+
+def save_image(response, filename):
+ """根据图像类型保存图像"""
+ if response.pixels_as_float:
+ print(f"Type {response.image_type}, size {len(response.image_data_float)}")
+ airsim.write_pfm(f'{filename}.pfm', airsim.get_pfm_array(response))
+ elif response.compress:
+ print(f"Type {response.image_type}, size {len(response.image_data_uint8)}")
+ airsim.write_file(f'{filename}.png', response.image_data_uint8)
+ else:
+ print(f"Type {response.image_type}, size {len(response.image_data_uint8)}")
+ img1d = np.frombuffer(response.image_data_uint8, dtype=np.uint8)
+ img_rgb = img1d.reshape(response.height, response.width, 3)
+ cv2.imwrite(f'{filename}.png', img_rgb)
+
+
+def capture_images(client, tmp_dir, idx):
+ """捕获并保存多种类型的图像"""
+ responses = client.simGetImages([
+ airsim.ImageRequest("0", airsim.ImageType.DepthVis),
+ airsim.ImageRequest("1", airsim.ImageType.DepthPerspective, True),
+ airsim.ImageRequest("1", airsim.ImageType.Scene),
+ airsim.ImageRequest("1", airsim.ImageType.Scene, False, False)
+ ])
+ print(f'Retrieved images: {len(responses)}')
+
+ for response_idx, response in enumerate(responses):
+ filename = tmp_dir / f"{idx}_{response.image_type}_{response_idx}"
+ save_image(response, str(filename))
+
+
+def main():
+ """主函数"""
+ client = setup_client()
+ tmp_dir = setup_image_directory()
+
+ try:
+ for idx in range(3):
+ # 获取车辆状态
+ car_state = client.getCarState()
+ print(f"Speed {car_state.speed}, Gear {car_state.gear}")
+
+ # 执行各种驾驶操作
+ execute_maneuver(client, 0.5, 0, 3, description="Go Forward")
+ execute_maneuver(client, 0.5, 1, 3, description="Go Forward, steer right")
+ execute_maneuver(client, -0.5, 0, 3, gear_mode="reverse", description="Go reverse")
+ apply_brake(client)
+
+ # 捕获图像
+ capture_images(client, tmp_dir, idx)
+
+ finally:
+ # 确保资源正确释放
+ client.reset()
+ client.enableApiControl(False)
+ print("Control released and simulator reset")
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/src/airsim/PythonClient/car/multi_agent_car.py b/src/airsim/PythonClient/car/multi_agent_car.py
new file mode 100644
index 0000000000..d696d32dc1
--- /dev/null
+++ b/src/airsim/PythonClient/car/multi_agent_car.py
@@ -0,0 +1,122 @@
+import airsim
+import cv2
+import numpy as np
+import os
+import setup_path
+import time
+
+# Use below in settings.json with blocks environment
+"""
+{
+ "SettingsVersion": 1.2,
+ "SimMode": "Car",
+
+ "Vehicles": {
+ "Car1": {
+ "VehicleType": "PhysXCar",
+ "X": 4, "Y": 0, "Z": -2
+ },
+ "Car2": {
+ "VehicleType": "PhysXCar",
+ "X": -4, "Y": 0, "Z": -2
+ }
+
+ }
+}
+"""
+
+# connect to the AirSim simulator
+client = airsim.CarClient()
+client.confirmConnection()
+client.enableApiControl(True, "Car1")
+client.enableApiControl(True, "Car2")
+
+car_controls1 = airsim.CarControls()
+car_controls2 = airsim.CarControls()
+
+
+for idx in range(3):
+ # get state of the car
+ car_state1 = client.getCarState("Car1")
+ print("Car1: Speed %d, Gear %d" % (car_state1.speed, car_state1.gear))
+ car_state2 = client.getCarState("Car2")
+ print("Car1: Speed %d, Gear %d" % (car_state2.speed, car_state2.gear))
+
+ # go forward
+ car_controls1.throttle = 0.5
+ car_controls1.steering = 0.5
+ client.setCarControls(car_controls1, "Car1")
+ print("Car1: Go Forward")
+
+ car_controls2.throttle = 0.5
+ car_controls2.steering = -0.5
+ client.setCarControls(car_controls2, "Car2")
+ print("Car2: Go Forward")
+ time.sleep(3) # let car drive a bit
+
+
+ # go reverse
+ car_controls1.throttle = -0.5
+ car_controls1.is_manual_gear = True;
+ car_controls1.manual_gear = -1
+ car_controls1.steering = -0.5
+ client.setCarControls(car_controls1, "Car1")
+ print("Car1: Go reverse, steer right")
+ car_controls1.is_manual_gear = False; # change back gear to auto
+ car_controls1.manual_gear = 0
+
+ car_controls2.throttle = -0.5
+ car_controls2.is_manual_gear = True;
+ car_controls2.manual_gear = -1
+ car_controls2.steering = 0.5
+ client.setCarControls(car_controls2, "Car2")
+ print("Car2: Go reverse, steer right")
+ car_controls2.is_manual_gear = False; # change back gear to auto
+ car_controls2.manual_gear = 0
+ time.sleep(3) # let car drive a bit
+
+
+ # apply breaks
+ car_controls1.brake = 1
+ client.setCarControls(car_controls1, "Car1")
+ print("Car1: Apply break")
+ car_controls1.brake = 0 #remove break
+
+ car_controls2.brake = 1
+ client.setCarControls(car_controls2, "Car2")
+ print("Car2: Apply break")
+ car_controls2.brake = 0 #remove break
+ time.sleep(3) # let car drive a bit
+
+ # get camera images from the car
+ responses1 = client.simGetImages([
+ airsim.ImageRequest("0", airsim.ImageType.DepthVis), #depth visualization image
+ airsim.ImageRequest("1", airsim.ImageType.Scene, False, False)], "Car1") #scene vision image in uncompressed RGB array
+ print('Car1: Retrieved images: %d' % (len(responses1)))
+ responses2 = client.simGetImages([
+ airsim.ImageRequest("0", airsim.ImageType.Segmentation), #depth visualization image
+ airsim.ImageRequest("1", airsim.ImageType.Scene, False, False)], "Car2") #scene vision image in uncompressed RGB array
+ print('Car2: Retrieved images: %d' % (len(responses2)))
+
+ for response in responses1 + responses2:
+ filename = 'c:/temp/car_multi_py' + str(idx)
+
+ if response.pixels_as_float:
+ print("Type %d, size %d" % (response.image_type, len(response.image_data_float)))
+ airsim.write_pfm(os.path.normpath(filename + '.pfm'), airsim.get_pfm_array(response))
+ elif response.compress: #png format
+ print("Type %d, size %d" % (response.image_type, len(response.image_data_uint8)))
+ airsim.write_file(os.path.normpath(filename + '.png'), response.image_data_uint8)
+ else: #uncompressed array
+ print("Type %d, size %d" % (response.image_type, len(response.image_data_uint8)))
+ img1d = np.fromstring(response.image_data_uint8, dtype=np.uint8) # get numpy array
+ img_rgb = img1d.reshape(response.height, response.width, 3) # reshape array to 3 channel image array H X W X 3
+ cv2.imwrite(os.path.normpath(filename + '.png'), img_rgb) # write to png
+
+#restore to original state
+client.reset()
+
+client.enableApiControl(False)
+
+
+
diff --git a/src/airsim/PythonClient/car/pause_continue_car.py b/src/airsim/PythonClient/car/pause_continue_car.py
new file mode 100644
index 0000000000..6ea48076b0
--- /dev/null
+++ b/src/airsim/PythonClient/car/pause_continue_car.py
@@ -0,0 +1,32 @@
+import setup_path
+import airsim
+
+import time
+
+# connect to the AirSim simulator
+client = airsim.CarClient()
+client.confirmConnection()
+client.enableApiControl(True)
+
+car_controls = airsim.CarControls()
+
+for i in range(1, 6):
+ print("Starting command")
+ car_controls.throttle = 0.5
+ car_controls.steering = 1
+ client.setCarControls(car_controls)
+ time.sleep(5) #run
+ print("Pausing after 5sec")
+ client.simPause(True)
+ time.sleep(5) #paused
+ print("Restarting command to run for 10sec")
+ client.simContinueForTime(10)
+ time.sleep(20)
+ print("Finishing rest of the command")
+ client.simPause(False)
+ time.sleep(10)
+ print("Finished cycle")
+
+
+
+
diff --git a/src/airsim/PythonClient/car/pause_test.py b/src/airsim/PythonClient/car/pause_test.py
new file mode 100644
index 0000000000..e8791871a6
--- /dev/null
+++ b/src/airsim/PythonClient/car/pause_test.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import airsim
+import time
+import numpy as np
+
+# connect to the AirSim simulator
+client = airsim.CarClient()
+client.confirmConnection()
+client.enableApiControl(True)
+car_controls = airsim.CarControls()
+
+# set the controls for car
+car_controls.throttle = -0.5
+car_controls.is_manual_gear = True
+car_controls.manual_gear = -1
+client.setCarControls(car_controls)
+
+# let car drive a bit
+time.sleep(10)
+
+client.simPause(True)
+car_position1 = client.getCarState().kinematics_estimated.position
+img_position1 = client.simGetImages([airsim.ImageRequest(0, airsim.ImageType.Scene)])[0].camera_position
+print(f"Before pause position: {car_position1}")
+print(f"Before pause diff: {car_position1.x_val - img_position1.x_val}, {car_position1.y_val - img_position1.y_val}, {car_position1.z_val - img_position1.z_val}")
+
+time.sleep(10)
+
+car_position2 = client.getCarState().kinematics_estimated.position
+img_position2 = client.simGetImages([airsim.ImageRequest(0, airsim.ImageType.Scene)])[0].camera_position
+print(f"After pause position: {car_position2}")
+print(f"After pause diff: {car_position2.x_val - img_position2.x_val}, {car_position2.y_val - img_position2.y_val}, {car_position2.z_val - img_position2.z_val}")
+client.simPause(False)
diff --git a/src/airsim/PythonClient/car/reset_test_car.py b/src/airsim/PythonClient/car/reset_test_car.py
new file mode 100644
index 0000000000..93b02b9825
--- /dev/null
+++ b/src/airsim/PythonClient/car/reset_test_car.py
@@ -0,0 +1,28 @@
+import setup_path
+import airsim
+
+import time
+
+# connect to the AirSim simulator
+client = airsim.CarClient()
+client.confirmConnection()
+client.enableApiControl(True)
+client.armDisarm(True)
+car_controls = airsim.CarControls()
+
+# go forward
+car_controls.throttle = 1
+car_controls.steering = 1
+client.setCarControls(car_controls)
+print("Go Forward")
+time.sleep(5) # let car drive a bit
+
+print("reset")
+client.reset()
+time.sleep(5) # let car drive a bit
+
+client.setCarControls(car_controls)
+print("Go Forward")
+time.sleep(5) # let car drive a bit
+
+
diff --git a/src/airsim/PythonClient/car/runtime_car.py b/src/airsim/PythonClient/car/runtime_car.py
new file mode 100644
index 0000000000..015cf90a42
--- /dev/null
+++ b/src/airsim/PythonClient/car/runtime_car.py
@@ -0,0 +1,75 @@
+import setup_path
+import airsim
+import time
+import sys
+import threading
+
+
+def runSingleCar(id: int):
+ client = airsim.CarClient()
+ client.confirmConnection()
+
+ vehicle_name = f"Car_{id}"
+ pose = airsim.Pose(airsim.Vector3r(0, 7*id, 0),
+ airsim.Quaternionr(0, 0, 0, 0))
+
+ print(f"Creating {vehicle_name}")
+ success = client.simAddVehicle(vehicle_name, "Physxcar", pose)
+
+ if not success:
+ print(f"Falied to create {vehicle_name}")
+ return
+
+ # Sleep for some time to wait for other vehicles to be created
+ time.sleep(1)
+
+ # driveCar(vehicle_name, client)
+ print(f"Driving {vehicle_name} for a few secs...")
+ client.enableApiControl(True, vehicle_name)
+
+ car_controls = airsim.CarControls()
+
+ # go forward
+ car_controls.throttle = 0.5
+ car_controls.steering = 0
+ client.setCarControls(car_controls, vehicle_name)
+ time.sleep(3) # let car drive a bit
+
+ # Go forward + steer right
+ car_controls.throttle = 0.5
+ car_controls.steering = 1
+ client.setCarControls(car_controls, vehicle_name)
+ time.sleep(3)
+
+ # go reverse
+ car_controls.throttle = -0.5
+ car_controls.is_manual_gear = True
+ car_controls.manual_gear = -1
+ car_controls.steering = 0
+ client.setCarControls(car_controls, vehicle_name)
+ time.sleep(3)
+ car_controls.is_manual_gear = False # change back gear to auto
+ car_controls.manual_gear = 0
+
+ # apply brakes
+ car_controls.brake = 1
+ client.setCarControls(car_controls, vehicle_name)
+ time.sleep(3)
+
+
+if __name__ == "__main__":
+ num_vehicles = 3
+
+ if len(sys.argv) == 2:
+ num_vehicles = int(sys.argv[1])
+
+ print(f"Creating {num_vehicles} vehicles")
+
+ threads = []
+ for id in range(num_vehicles, 0, -1):
+ t = threading.Thread(target=runSingleCar, args=(id,))
+ threads.append(t)
+ t.start()
+
+ for t in threads:
+ t.join()
diff --git a/src/airsim/PythonClient/car/setup_path.py b/src/airsim/PythonClient/car/setup_path.py
new file mode 100644
index 0000000000..362113fea8
--- /dev/null
+++ b/src/airsim/PythonClient/car/setup_path.py
@@ -0,0 +1,52 @@
+# Import this module to automatically setup path to local airsim module
+# This module first tries to see if airsim module is installed via pip
+# If it does then we don't do anything else
+# Else we look up grand-parent folder to see if it has airsim folder
+# and if it does then we add that in sys.path
+
+import os,sys,inspect,logging
+
+#this class simply tries to see if airsim
+class SetupPath:
+ @staticmethod
+ def getDirLevels(path):
+ path_norm = os.path.normpath(path)
+ return len(path_norm.split(os.sep))
+
+ @staticmethod
+ def getCurrentPath():
+ cur_filepath = os.path.abspath(inspect.getfile(inspect.currentframe()))
+ return os.path.dirname(cur_filepath)
+
+ @staticmethod
+ def getGrandParentDir():
+ cur_path = SetupPath.getCurrentPath()
+ if SetupPath.getDirLevels(cur_path) >= 2:
+ return os.path.dirname(os.path.dirname(cur_path))
+ return ''
+
+ @staticmethod
+ def getParentDir():
+ cur_path = SetupPath.getCurrentPath()
+ if SetupPath.getDirLevels(cur_path) >= 1:
+ return os.path.dirname(cur_path)
+ return ''
+
+ @staticmethod
+ def addAirSimModulePath():
+ # if airsim module is installed then don't do anything else
+ #import pkgutil
+ #airsim_loader = pkgutil.find_loader('airsim')
+ #if airsim_loader is not None:
+ # return
+
+ parent = SetupPath.getParentDir()
+ if parent != '':
+ airsim_path = os.path.join(parent, 'airsim')
+ client_path = os.path.join(airsim_path, 'client.py')
+ if os.path.exists(client_path):
+ sys.path.insert(0, parent)
+ else:
+ logging.warning("airsim module not found in parent folder. Using installed package (pip install airsim).")
+
+SetupPath.addAirSimModulePath()
diff --git a/src/airsim/PythonClient/computer_vision/capture_ir_segmentation.py b/src/airsim/PythonClient/computer_vision/capture_ir_segmentation.py
new file mode 100644
index 0000000000..a6aab5f548
--- /dev/null
+++ b/src/airsim/PythonClient/computer_vision/capture_ir_segmentation.py
@@ -0,0 +1,257 @@
+import numpy
+import cv2
+import time
+import sys
+import os
+import random
+import glob
+from airsim import *
+
+def rotation_matrix_from_angles(pry):
+ pitch = pry[0]
+ roll = pry[1]
+ yaw = pry[2]
+ sy = numpy.sin(yaw)
+ cy = numpy.cos(yaw)
+ sp = numpy.sin(pitch)
+ cp = numpy.cos(pitch)
+ sr = numpy.sin(roll)
+ cr = numpy.cos(roll)
+
+ Rx = numpy.array([
+ [1, 0, 0],
+ [0, cr, -sr],
+ [0, sr, cr]
+ ])
+
+ Ry = numpy.array([
+ [cp, 0, sp],
+ [0, 1, 0],
+ [-sp, 0, cp]
+ ])
+
+ Rz = numpy.array([
+ [cy, -sy, 0],
+ [sy, cy, 0],
+ [0, 0, 1]
+ ])
+
+ #Roll is applied first, then pitch, then yaw.
+ RyRx = numpy.matmul(Ry, Rx)
+ return numpy.matmul(Rz, RyRx)
+
+def project_3d_point_to_screen(subjectXYZ, camXYZ, camQuaternion, camProjMatrix4x4, imageWidthHeight):
+ #Turn the camera position into a column vector.
+ camPosition = numpy.transpose([camXYZ])
+
+ #Convert the camera's quaternion rotation to yaw, pitch, roll angles.
+ pitchRollYaw = utils.to_eularian_angles(camQuaternion)
+
+ #Create a rotation matrix from camera pitch, roll, and yaw angles.
+ camRotation = rotation_matrix_from_angles(pitchRollYaw)
+
+ #Change coordinates to get subjectXYZ in the camera's local coordinate system.
+ XYZW = numpy.transpose([subjectXYZ])
+ XYZW = numpy.add(XYZW, -camPosition)
+ print("XYZW: " + str(XYZW))
+ XYZW = numpy.matmul(numpy.transpose(camRotation), XYZW)
+ print("XYZW derot: " + str(XYZW))
+
+ #Recreate the perspective projection of the camera.
+ XYZW = numpy.concatenate([XYZW, [[1]]])
+ XYZW = numpy.matmul(camProjMatrix4x4, XYZW)
+ XYZW = XYZW / XYZW[3]
+
+ #Move origin to the upper-left corner of the screen and multiply by size to get pixel values. Note that screen is in y,-z plane.
+ normX = (1 - XYZW[0]) / 2
+ normY = (1 + XYZW[1]) / 2
+
+ return numpy.array([
+ imageWidthHeight[0] * normX,
+ imageWidthHeight[1] * normY
+ ]).reshape(2,)
+
+def get_image(x, y, z, pitch, roll, yaw, client):
+ """
+ title::
+ get_image
+
+ description::
+ Capture images (as numpy arrays) from a certain position.
+
+ inputs::
+ x
+ x position in meters
+ y
+ y position in meters
+ z
+ altitude in meters; remember NED, so should be negative to be
+ above ground
+ pitch
+ angle (in radians); in computer vision mode, this is camera angle
+ roll
+ angle (in radians)
+ yaw
+ angle (in radians)
+ client
+ connection to AirSim (e.g., client = MultirotorClient() for UAV)
+
+ returns::
+ position
+ AirSim position vector (access values with x_val, y_val, z_val)
+ angle
+ AirSim quaternion ("angles")
+ im
+ segmentation or IR image, depending upon palette in use (3 bands)
+ imScene
+ scene image (3 bands)
+
+ author::
+ Elizabeth Bondi
+ Shital Shah
+ """
+
+ #Set pose and sleep after to ensure the pose sticks before capturing image.
+ client.simSetVehiclePose(Pose(Vector3r(x, y, z), \
+ to_quaternion(pitch, roll, yaw)), True)
+ time.sleep(0.1)
+
+ #Capture segmentation (IR) and scene images.
+ responses = \
+ client.simGetImages([ImageRequest("0", ImageType.Infrared,
+ False, False),
+ ImageRequest("0", ImageType.Scene, \
+ False, False),
+ ImageRequest("0", ImageType.Segmentation, \
+ False, False)])
+
+ #Change images into numpy arrays.
+ img1d = numpy.fromstring(responses[0].image_data_uint8, dtype=numpy.uint8)
+ im = img1d.reshape(responses[0].height, responses[0].width, 4)
+
+ img1dscene = numpy.fromstring(responses[1].image_data_uint8, dtype=numpy.uint8)
+ imScene = img1dscene.reshape(responses[1].height, responses[1].width, 4)
+
+ return Vector3r(x, y, z), to_quaternion(pitch, roll, yaw),\
+ im[:,:,:3], imScene[:,:,:3] #get rid of alpha channel
+
+def main(client,
+ objectList,
+ pitch=numpy.radians(270), #image straight down
+ roll=0,
+ yaw=0,
+ z=-122,
+ writeIR=True,
+ writeScene=False,
+ irFolder='',
+ sceneFolder=''):
+ """
+ title::
+ main
+
+ description::
+ Follow objects of interest and record images while following.
+
+ inputs::
+ client
+ connection to AirSim (e.g., client = MultirotorClient() for UAV)
+ objectList
+ list of tag names within the AirSim environment, corresponding to
+ objects to follow (add tags by clicking on object, going to
+ Details, Actor, and Tags, then add component)
+ pitch
+ angle (in radians); in computer vision mode, this is camera angle
+ roll
+ angle (in radians)
+ yaw
+ angle (in radians)
+ z
+ altitude in meters; remember NED, so should be negative to be
+ above ground
+ write
+ if True, will write out the images
+ folder
+ path to a particular folder that should be used (then within that
+ folder, expected folders are ir and scene)
+
+ author::
+ Elizabeth Bondi
+ """
+ i = 0
+ for o in objectList:
+ startTime = time.time()
+ elapsedTime = 0
+ pose = client.simGetObjectPose(o);
+
+ #Capture images for a certain amount of time in seconds (half hour now)
+ while elapsedTime < 1800:
+ #Capture image - pose.position x_val access may change w/ AirSim
+ #version (pose.position.x_val new, pose.position[b'x_val'] old)
+ vector, angle, ir, scene = get_image(pose.position.x_val,
+ pose.position.y_val,
+ z,
+ pitch,
+ roll,
+ yaw,
+ client)
+
+ #Convert color scene image to BGR for write out with cv2.
+ r,g,b = cv2.split(scene)
+ scene = cv2.merge((b,g,r))
+
+ if writeIR:
+ cv2.imwrite(irFolder+'ir_'+str(i).zfill(5)+'.png', ir)
+ if writeScene:
+ cv2.imwrite(sceneFolder+'scene_'+str(i).zfill(5)+'.png',
+ scene)
+
+ i += 1
+ elapsedTime = time.time() - startTime
+ pose = client.simGetObjectPose(o);
+ camInfo = client.simGetCameraInfo("0")
+ object_xy_in_pic = project_3d_point_to_screen(
+ [pose.position.x_val, pose.position.y_val, pose.position.z_val],
+ [camInfo.pose.position.x_val, camInfo.pose.position.y_val, camInfo.pose.position.z_val],
+ camInfo.pose.orientation,
+ camInfo.proj_mat.matrix,
+ ir.shape[:2][::-1]
+ )
+ print("Object projected to pixel\n{!s}.".format(object_xy_in_pic))
+
+if __name__ == '__main__':
+
+ #Connect to AirSim, UAV mode.
+ client = MultirotorClient()
+ client.confirmConnection()
+
+ #Look for objects with names that match a regular expression.
+ poacherList = client.simListSceneObjects('.*?Poacher.*?')
+ elephantList = client.simListSceneObjects('.*?Elephant.*?')
+ crocList = client.simListSceneObjects('.*?Croc.*?')
+ hippoList = client.simListSceneObjects('.*?Hippo.*?')
+
+ objectList = elephantList
+
+ #Sample calls to main, varying camera angle and altitude.
+ #straight down, 400ft
+ main(client,
+ objectList,
+ irFolder=r'auto\winter\400ft\down')
+ #straight down, 200ft
+ main(client,
+ objectList,
+ z=-61,
+ irFolder=r'auto\winter\200ft\down')
+ #45 degrees, 200ft -- note that often object won't be scene since position
+ #is set exactly to object's
+ main(client,
+ objectList,
+ z=-61,
+ pitch=numpy.radians(315),
+ irFolder=r'auto\winter\200ft\45')
+ #45 degrees, 400ft -- note that often object won't be scene since position
+ #is set exactly to object's
+ main(client,
+ objectList,
+ pitch=numpy.radians(315),
+ irFolder=r'auto\winter\400ft\45')
\ No newline at end of file
diff --git a/src/airsim/PythonClient/computer_vision/create_ir_segmentation_map.py b/src/airsim/PythonClient/computer_vision/create_ir_segmentation_map.py
new file mode 100644
index 0000000000..065d6b74da
--- /dev/null
+++ b/src/airsim/PythonClient/computer_vision/create_ir_segmentation_map.py
@@ -0,0 +1,212 @@
+import numpy
+import cv2
+import time
+import sys
+import os
+import random
+from airsim import *
+
+def radiance(absoluteTemperature, emissivity, dx=0.01, response=None):
+ """
+ title::
+ radiance
+
+ description::
+ Calculates radiance and integrated radiance over a bandpass of 8 to 14
+ microns, given temperature and emissivity, using Planck's Law.
+
+ inputs::
+ absoluteTemperature
+ temperture of object in [K]
+
+ either a single temperature or a numpy
+ array of temperatures, of shape (temperatures.shape[0], 1)
+ emissivity
+ average emissivity (number between 0 and 1 representing the
+ efficiency with which it emits radiation; if 1, it is an ideal
+ blackbody) of object over the bandpass
+
+ either a single emissivity or a numpy array of emissivities, of
+ shape (emissivities.shape[0], 1)
+ dx
+ discrete spacing between the wavelengths for evaluation of
+ radiance and integration [default is 0.1]
+ response
+ optional response of the camera over the bandpass of 8 to 14
+ microns [default is None, for no response provided]
+
+ returns::
+ radiance
+ discrete spectrum of radiance over bandpass
+ integratedRadiance
+ integration of radiance spectrum over bandpass (to simulate
+ the readout from a sensor)
+
+ author::
+ Elizabeth Bondi
+ """
+ wavelength = numpy.arange(8,14,dx)
+ c1 = 1.19104e8 # (2 * 6.62607*10^-34 [Js] *
+ # (2.99792458 * 10^14 [micron/s])^2 * 10^12 to convert
+ # denominator from microns^3 to microns * m^2)
+ c2 = 1.43879e4 # (hc/k) [micron * K]
+ if response is not None:
+ radiance = response * emissivity * (c1 / ((wavelength**5) * \
+ (numpy.exp(c2 / (wavelength * absoluteTemperature )) - 1)))
+ else:
+ radiance = emissivity * (c1 / ((wavelength**5) * (numpy.exp(c2 / \
+ (wavelength * absoluteTemperature )) - 1)))
+ if absoluteTemperature.ndim > 1:
+ return radiance, numpy.trapz(radiance, dx=dx, axis=1)
+ else:
+ return radiance, numpy.trapz(radiance, dx=dx)
+
+
+def get_new_temp_emiss_from_radiance(tempEmissivity, response):
+ """
+ title::
+ get_new_temp_emiss_from_radiance
+
+ description::
+ Transform tempEmissivity from [objectName, temperature, emissivity]
+ to [objectName, "radiance"] using radiance calculation above.
+
+ input::
+ tempEmissivity
+ numpy array containing the temperature and emissivity of each
+ object (e.g., each row has: [objectName, temperature, emissivity])
+ response
+ camera response (same input as radiance, set to None if lacking
+ this information)
+
+ returns::
+ tempEmissivityNew
+ tempEmissivity, now with [objectName, "radiance"]; note that
+ integrated radiance (L) is divided by the maximum and multiplied
+ by 255 in order to simulate an 8 bit digital count observed by the
+ thermal sensor, since radiance and digital count are linearly
+ related, so it's [objectName, simulated thermal digital count]
+
+ author::
+ Elizabeth Bondi
+ """
+ numObjects = tempEmissivity.shape[0]
+
+ L = radiance(tempEmissivity[:,1].reshape((-1,1)).astype(numpy.float64),
+ tempEmissivity[:,2].reshape((-1,1)).astype(numpy.float64),
+ response=response)[1].flatten()
+ L = ((L / L.max()) * 255).astype(numpy.uint8)
+
+ tempEmissivityNew = numpy.hstack((
+ tempEmissivity[:,0].reshape((numObjects,1)),
+ L.reshape((numObjects,1))))
+
+ return tempEmissivityNew
+
+def set_segmentation_ids(segIdDict, tempEmissivityNew, client):
+ """
+ title::
+ set_segmentation_ids
+
+ description::
+ Set stencil IDs in environment so that stencil IDs correspond to
+ simulated thermal digital counts (e.g., if elephant has a simulated
+ digital count of 219, set stencil ID to 219).
+
+ input::
+ segIdDict
+ dictionary mapping environment object names to the object names in
+ the first column of tempEmissivityNew
+ tempEmissivityNew
+ numpy array containing object names and corresponding simulated
+ thermal digital count
+ client
+ connection to AirSim (e.g., client = MultirotorClient() for UAV)
+
+ author::
+ Elizabeth Bondi
+ """
+
+ #First set everything to 0.
+ success = client.simSetSegmentationObjectID("[\w]*", 0, True);
+ if not success:
+ print('There was a problem setting all segmentation object IDs to 0. ')
+ sys.exit(1)
+
+ #Next set all objects of interest provided to corresponding object IDs
+ #segIdDict values MUST match tempEmissivityNew labels.
+ for key in segIdDict:
+ objectID = int(tempEmissivityNew[numpy.where(tempEmissivityNew == \
+ segIdDict[key])[0],1][0])
+
+ success = client.simSetSegmentationObjectID("[\w]*"+key+"[\w]*",
+ objectID, True);
+ if not success:
+ print('There was a problem setting {0} segmentation object ID to {1!s}, or no {0} was found.'.format(key, objectID))
+
+ time.sleep(0.1)
+
+
+if __name__ == '__main__':
+
+ #Connect to AirSim, UAV mode.
+ client = MultirotorClient()
+ client.confirmConnection()
+
+ segIdDict = {'Base_Terrain':'soil',
+ 'elephant':'elephant',
+ 'zebra':'zebra',
+ 'Crocodile':'crocodile',
+ 'Rhinoceros':'rhinoceros',
+ 'Hippo':'hippopotamus',
+ 'Poacher':'human',
+ 'InstancedFoliageActor':'tree',
+ 'Water_Plane':'water',
+ 'truck':'truck'}
+
+ #Choose temperature values for winter or summer.
+ #"""
+ #winter
+ tempEmissivity = numpy.array([['elephant',290,0.96],
+ ['zebra',298,0.98],
+ ['rhinoceros',291,0.96],
+ ['hippopotamus',290,0.96],
+ ['crocodile',295,0.96],
+ ['human',292,0.985],
+ ['tree',273,0.952],
+ ['grass',273,0.958],
+ ['soil',278,0.914],
+ ['shrub',273,0.986],
+ ['truck',273,0.8],
+ ['water',273,0.96]])
+ #"""
+ """
+ #summer
+ tempEmissivity = numpy.array([['elephant',298,0.96],
+ ['zebra',307,0.98],
+ ['rhinoceros',299,0.96],
+ ['hippopotamus',298,0.96],
+ ['crocodile',303,0.96],
+ ['human',301,0.985],
+ ['tree',293,0.952],
+ ['grass',293,0.958],
+ ['soil',288,0.914],
+ ['shrub',293,0.986],
+ ['truck',293,0.8],
+ ['water',293,0.96]])
+ """
+
+ #Read camera response.
+ response = None
+ camResponseFile = 'camera_response.npy'
+ try:
+ numpy.load(camResponseFile)
+ except:
+ print("{} not found. Using default response.".format(camResponseFile))
+
+ #Calculate radiance.
+ tempEmissivityNew = get_new_temp_emiss_from_radiance(tempEmissivity,
+ response)
+
+ #Set IDs in AirSim environment.
+ set_segmentation_ids(segIdDict, tempEmissivityNew, client)
\ No newline at end of file
diff --git a/src/airsim/PythonClient/computer_vision/cv_capture.py b/src/airsim/PythonClient/computer_vision/cv_capture.py
new file mode 100644
index 0000000000..2701ce3009
--- /dev/null
+++ b/src/airsim/PythonClient/computer_vision/cv_capture.py
@@ -0,0 +1,55 @@
+# In settings.json first activate computer vision mode:
+# https://github.com/Microsoft/AirSim/blob/main/docs/image_apis.md#computer-vision-mode
+
+import setup_path
+import airsim
+
+import pprint
+import tempfile
+import os
+import time
+
+pp = pprint.PrettyPrinter(indent=4)
+
+client = airsim.VehicleClient()
+
+airsim.wait_key('Press any key to get camera parameters')
+for camera_id in range(2):
+ camera_info = client.simGetCameraInfo(str(camera_id))
+ print("CameraInfo %d: %s" % (camera_id, pp.pprint(camera_info)))
+
+airsim.wait_key('Press any key to get images')
+tmp_dir = os.path.join(tempfile.gettempdir(), "airsim_drone")
+print ("Saving images to %s" % tmp_dir)
+try:
+ for n in range(3):
+ os.makedirs(os.path.join(tmp_dir, str(n)))
+except OSError:
+ if not os.path.isdir(tmp_dir):
+ raise
+
+for x in range(50): # do few times
+ #xn = 1 + x*5 # some random number
+ client.simSetVehiclePose(airsim.Pose(airsim.Vector3r(x, 0, -2), airsim.to_quaternion(0, 0, 0)), True)
+ time.sleep(0.1)
+
+ responses = client.simGetImages([
+ airsim.ImageRequest("0", airsim.ImageType.Scene),
+ airsim.ImageRequest("1", airsim.ImageType.Scene),
+ airsim.ImageRequest("2", airsim.ImageType.Scene)])
+
+ for i, response in enumerate(responses):
+ if response.pixels_as_float:
+ print("Type %d, size %d, pos %s" % (response.image_type, len(response.image_data_float), pprint.pformat(response.camera_position)))
+ airsim.write_pfm(os.path.normpath(os.path.join(tmp_dir, str(x) + "_" + str(i) + '.pfm')), airsim.get_pfm_array(response))
+ else:
+ print("Type %d, size %d, pos %s" % (response.image_type, len(response.image_data_uint8), pprint.pformat(response.camera_position)))
+ airsim.write_file(os.path.normpath(os.path.join(tmp_dir, str(i), str(x) + "_" + str(i) + '.png')), response.image_data_uint8)
+
+ pose = client.simGetVehiclePose()
+ pp.pprint(pose)
+
+ time.sleep(3)
+
+# currently reset() doesn't work in CV mode. Below is the workaround
+client.simSetVehiclePose(airsim.Pose(airsim.Vector3r(0, 0, 0), airsim.to_quaternion(0, 0, 0)), True)
diff --git a/src/airsim/PythonClient/computer_vision/cv_mode.py b/src/airsim/PythonClient/computer_vision/cv_mode.py
new file mode 100644
index 0000000000..3c05e08709
--- /dev/null
+++ b/src/airsim/PythonClient/computer_vision/cv_mode.py
@@ -0,0 +1,64 @@
+# In settings.json first activate computer vision mode:
+# https://github.com/Microsoft/AirSim/blob/main/docs/image_apis.md#computer-vision-mode
+
+import setup_path
+import airsim
+
+import pprint
+import os
+import time
+import math
+import tempfile
+
+pp = pprint.PrettyPrinter(indent=4)
+
+client = airsim.VehicleClient()
+client.confirmConnection()
+
+airsim.wait_key('Press any key to set camera-0 gimbal to 15-degree pitch')
+camera_pose = airsim.Pose(airsim.Vector3r(0, 0, 0), airsim.to_quaternion(math.radians(15), 0, 0)) #radians
+client.simSetCameraPose("0", camera_pose)
+
+airsim.wait_key('Press any key to get camera parameters')
+for camera_name in range(5):
+ camera_info = client.simGetCameraInfo(str(camera_name))
+ print("CameraInfo %d:" % camera_name)
+ pp.pprint(camera_info)
+
+tmp_dir = os.path.join(tempfile.gettempdir(), "airsim_cv_mode")
+print ("Saving images to %s" % tmp_dir)
+try:
+ os.makedirs(tmp_dir)
+except OSError:
+ if not os.path.isdir(tmp_dir):
+ raise
+
+airsim.wait_key('Press any key to get images')
+for x in range(3): # do few times
+ z = x * -20 - 5 # some random number
+ client.simSetVehiclePose(airsim.Pose(airsim.Vector3r(z, z, z), airsim.to_quaternion(x / 3.0, 0, x / 3.0)), True)
+
+ responses = client.simGetImages([
+ airsim.ImageRequest("0", airsim.ImageType.DepthVis),
+ airsim.ImageRequest("1", airsim.ImageType.DepthPerspective, True),
+ airsim.ImageRequest("2", airsim.ImageType.Segmentation),
+ airsim.ImageRequest("3", airsim.ImageType.Scene),
+ airsim.ImageRequest("4", airsim.ImageType.DisparityNormalized),
+ airsim.ImageRequest("4", airsim.ImageType.SurfaceNormals)])
+
+ for i, response in enumerate(responses):
+ filename = os.path.join(tmp_dir, str(x) + "_" + str(i))
+ if response.pixels_as_float:
+ print("Type %d, size %d, pos %s" % (response.image_type, len(response.image_data_float), pprint.pformat(response.camera_position)))
+ airsim.write_pfm(os.path.normpath(filename + '.pfm'), airsim.get_pfm_array(response))
+ else:
+ print("Type %d, size %d, pos %s" % (response.image_type, len(response.image_data_uint8), pprint.pformat(response.camera_position)))
+ airsim.write_file(os.path.normpath(filename + '.png'), response.image_data_uint8)
+
+ pose = client.simGetVehiclePose()
+ pp.pprint(pose)
+
+ time.sleep(3)
+
+# currently reset() doesn't work in CV mode. Below is the workaround
+client.simSetVehiclePose(airsim.Pose(airsim.Vector3r(0, 0, 0), airsim.to_quaternion(0, 0, 0)), True)
diff --git a/src/airsim/PythonClient/computer_vision/cv_navigate.py b/src/airsim/PythonClient/computer_vision/cv_navigate.py
new file mode 100644
index 0000000000..592190f1a6
--- /dev/null
+++ b/src/airsim/PythonClient/computer_vision/cv_navigate.py
@@ -0,0 +1,273 @@
+
+# In settings.json first activate computer vision mode:
+# https://github.com/Microsoft/AirSim/blob/main/docs/image_apis.md#computer-vision-mode
+
+import setup_path
+import airsim
+
+import numpy as np
+import time
+import os
+import pprint
+import tempfile
+import math
+from math import *
+from scipy.misc import imsave
+
+from abc import ABC, abstractmethod
+
+#define abstract class to return next vector in the format (x,y,yaw)
+class AbstractClassGetNextVec(ABC):
+
+ @abstractmethod
+ def get_next_vec(self, depth, obj_sz, goal, pos):
+ print("Some implementation!")
+ return pos,yaw
+
+class ReactiveController(AbstractClassGetNextVec):
+ def get_next_vec(self, depth, obj_sz, goal, pos):
+ print("Some implementation!")
+ return
+
+class AvoidLeft(AbstractClassGetNextVec):
+
+ def __init__(self, hfov=radians(90), coll_thres=5, yaw=0, limit_yaw=5, step=0.1):
+ self.hfov = hfov
+ self.coll_thres = coll_thres
+ self.yaw = yaw
+ self.limit_yaw = limit_yaw
+ self.step = step
+
+ def get_next_vec(self, depth, obj_sz, goal, pos):
+
+ [h,w] = np.shape(depth)
+ [roi_h,roi_w] = compute_bb((h,w), obj_sz, self.hfov, self.coll_thres)
+
+ # compute vector, distance and angle to goal
+ t_vec, t_dist, t_angle = get_vec_dist_angle (goal, pos[:-1])
+
+ # compute box of interest
+ img2d_box = img2d[int((h-roi_h)/2):int((h+roi_h)/2),int((w-roi_w)/2):int((w+roi_w)/2)]
+
+ # scale by weight matrix (optional)
+ #img2d_box = np.multiply(img2d_box,w_mtx)
+
+ # detect collision
+ if (np.min(img2d_box) < coll_thres):
+ self.yaw = self.yaw - radians(self.limit_yaw)
+ else:
+ self.yaw = self.yaw + min (t_angle-self.yaw, radians(self.limit_yaw))
+
+ pos[0] = pos[0] + self.step*cos(self.yaw)
+ pos[1] = pos[1] + self.step*sin(self.yaw)
+
+ return pos, self.yaw,t_dist
+
+class AvoidLeftIgonreGoal(AbstractClassGetNextVec):
+
+ def __init__(self, hfov=radians(90), coll_thres=5, yaw=0, limit_yaw=5, step=0.1):
+ self.hfov = hfov
+ self.coll_thres = coll_thres
+ self.yaw = yaw
+ self.limit_yaw = limit_yaw
+ self.step = step
+
+ def get_next_vec(self, depth, obj_sz, goal, pos):
+
+ [h,w] = np.shape(depth)
+ [roi_h,roi_w] = compute_bb((h,w), obj_sz, self.hfov, self.coll_thres)
+
+ # compute box of interest
+ img2d_box = img2d[int((h-roi_h)/2):int((h+roi_h)/2),int((w-roi_w)/2):int((w+roi_w)/2)]
+
+ # detect collision
+ if (np.min(img2d_box) < coll_thres):
+ self.yaw = self.yaw - radians(self.limit_yaw)
+
+ pos[0] = pos[0] + self.step*cos(self.yaw)
+ pos[1] = pos[1] + self.step*sin(self.yaw)
+
+ return pos, self.yaw, 100
+
+class AvoidLeftRight(AbstractClassGetNextVec):
+ def get_next_vec(self, depth, obj_sz, goal, pos):
+ print("Some implementation!")
+ #Same as above but decide to go left or right based on average or some metric like that
+ return
+
+
+#compute resultant normalized vector, distance and angle
+def get_vec_dist_angle (goal, pos):
+ vec = np.array(goal) - np.array(pos)
+ dist = math.sqrt(vec[0]**2 + vec[1]**2)
+ angle = math.atan2(vec[1],vec[0])
+ if angle > math.pi:
+ angle -= 2*math.pi
+ elif angle < -math.pi:
+ angle += 2*math.pi
+ return vec/dist, dist, angle
+
+def get_local_goal (v, pos, theta):
+ return goal
+
+#compute bounding box size
+def compute_bb(image_sz, obj_sz, hfov, distance):
+ vfov = hfov2vfov(hfov,image_sz)
+ box_h = ceil(obj_sz[0] * image_sz[0] / (math.tan(hfov/2)*distance*2))
+ box_w = ceil(obj_sz[1] * image_sz[1] / (math.tan(vfov/2)*distance*2))
+ return box_h, box_w
+
+#convert horizonal fov to vertical fov
+def hfov2vfov(hfov, image_sz):
+ aspect = image_sz[0]/image_sz[1]
+ vfov = 2*math.atan( tan(hfov/2) * aspect)
+ return vfov
+
+#matrix with all ones
+def equal_weight_mtx(roi_h,roi_w):
+ return np.ones((roi_h,roi_w))
+
+#matrix with max weight in center and decreasing linearly with distance from center
+def linear_weight_mtx(roi_h,roi_w):
+ w_mtx = np.ones((roi_h,roi_w))
+ for j in range(0,roi_w):
+ for i in range(j,roi_h-j):
+ w_mtx[j:roi_h-j,i:roi_w-i] = (j+1)
+ return w_mtx
+
+#matrix with max weight in center and decreasing quadratically with distance from center
+def square_weight_mtx(roi_h,roi_w):
+ w_mtx = np.ones((roi_h,roi_w))
+ for j in range(0,roi_w):
+ for i in range(j,roi_h-j):
+ w_mtx[j:roi_h-j,i:roi_w-i] = (j+1)*(j+1)
+ return w_mtx
+
+def print_stats(img):
+ print ('Avg: ',np.average(img))
+ print ('Min: ',np.min(img))
+ print ('Max: ',np.max(img))
+ print('Img Sz: ',np.size(img))
+
+def generate_depth_viz(img,thres=0):
+ if thres > 0:
+ img[img > thres] = thres
+ else:
+ img = np.reciprocal(img)
+ return img
+
+def moveUAV(client,pos,yaw):
+ client.simSetVehiclePose(airsim.Pose(airsim.Vector3r(pos[0], pos[1], pos[2]), airsim.to_quaternion(0, 0, yaw)), True)
+
+
+pp = pprint.PrettyPrinter(indent=4)
+
+client = airsim.VehicleClient()
+client.confirmConnection()
+
+tmp_dir = os.path.join(tempfile.gettempdir(), "airsim_drone")
+#print ("Saving images to %s" % tmp_dir)
+#airsim.wait_key('Press any key to start')
+
+#Define start position, goal and size of UAV
+pos = [0,5,-1] #start position x,y,z
+goal = [120,0] #x,y
+uav_size = [0.29*3,0.98*2] #height:0.29 x width:0.98 - allow some tolerance
+
+#Define parameters and thresholds
+hfov = radians(90)
+coll_thres = 5
+yaw = 0
+limit_yaw = 5
+step = 0.1
+
+responses = client.simGetImages([
+ airsim.ImageRequest("1", airsim.ImageType.DepthPlanar, True)])
+response = responses[0]
+
+#initial position
+moveUAV(client,pos,yaw)
+
+#predictControl = AvoidLeftIgonreGoal(hfov, coll_thres, yaw, limit_yaw, step)
+predictControl = AvoidLeft(hfov, coll_thres, yaw, limit_yaw, step)
+
+for z in range(10000): # do few times
+
+ #time.sleep(1)
+
+ # get response
+ responses = client.simGetImages([
+ airsim.ImageRequest("1", airsim.ImageType.DepthPlanar, True)])
+ response = responses[0]
+
+ # get numpy array
+ img1d = response.image_data_float
+
+ # reshape array to 2D array H X W
+ img2d = np.reshape(img1d,(response.height, response.width))
+
+ [pos,yaw,target_dist] = predictControl.get_next_vec(img2d, uav_size, goal, pos)
+ moveUAV(client,pos,yaw)
+
+ if (target_dist < 1):
+ print('Target reached.')
+ airsim.wait_key('Press any key to continue')
+ break
+
+ # write to png
+ #imsave(os.path.normpath(os.path.join(tmp_dir, "depth_" + str(z) + '.png')), generate_depth_viz(img2d,5))
+
+ #pose = client.simGetPose()
+ #pp.pprint(pose)
+ #time.sleep(5)
+
+# currently reset() doesn't work in CV mode. Below is the workaround
+client.simSetVehiclePose(airsim.Pose(airsim.Vector3r(0, 0, 0), airsim.to_quaternion(0, 0, 0)), True)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#################### OLD CODE
+# timer = 0
+# time_obs = 50
+# bObstacle = False
+
+# if (bObstacle):
+# timer = timer + 1
+# if timer > time_obs:
+# bObstacle = False
+# timer = 0
+# else:
+# yaw = target_angle
+
+# print (target_angle,target_vec,target_dist,x,y,goal[0],goal[1])
+
+
+# if (np.average(img2d_box) < coll_thres):
+# img2d_box_l = img2d_box = img2d[int((h-roi_h)/2):int((h+roi_h)/2),int((w-roi_w)/2)-50:int((w+roi_w)/2)-50]
+# img2d_box_r = img2d_box = img2d[int((h-roi_h)/2):int((h+roi_h)/2),int((w-roi_w)/2)+50:int((w+roi_w)/2)+50]
+# img2d_box_l_avg = np.average(np.multiply(img2d_box_l,w_mtx))
+# img2d_box_r_avg = np.average(np.multiply(img2d_box_r,w_mtx))
+# print('left: ', img2d_box_l_avg)
+# print('right: ', img2d_box_r_avg)
+# if img2d_box_l_avg > img2d_box_r_avg:
+# ##Go LEFT
+# #y_offset = y_offset-1
+# yaw = yaw - radians(10)
+# bObstacle = True
+# else:
+# ##Go RIGHT
+# #y_offset = y_offset+1
+# yaw = yaw + radians(10)
+# bObstacle = true
+# print('yaw: ', yaw)
diff --git a/src/airsim/PythonClient/computer_vision/external_camera.py b/src/airsim/PythonClient/computer_vision/external_camera.py
new file mode 100644
index 0000000000..b41a072cba
--- /dev/null
+++ b/src/airsim/PythonClient/computer_vision/external_camera.py
@@ -0,0 +1,102 @@
+import setup_path
+import airsim
+import os
+import tempfile
+
+"""
+A simple script to test all the camera APIs. Change the camera name and whether it's an external camera
+
+Example Settings for external camera -
+
+{
+ "SettingsVersion": 1.2,
+ "SimMode": "Car",
+ "ExternalCameras": {
+ "fixed1": {
+ "X": 0, "Y": 0, "Z": -5,
+ "Pitch": -90, "Roll": 0, "Yaw": 0
+ }
+ }
+}
+"""
+
+# Just change the below to test different cameras easily!
+CAM_NAME = "fixed1"
+IS_EXTERNAL_CAM = True
+
+
+client = airsim.VehicleClient()
+client.confirmConnection()
+
+tmp_dir = os.path.join(tempfile.gettempdir(), "airsim_cv_mode")
+print ("Saving images to %s" % tmp_dir)
+try:
+ os.makedirs(tmp_dir)
+except OSError:
+ if not os.path.isdir(tmp_dir):
+ raise
+
+print(f"Camera: {CAM_NAME}, External = {IS_EXTERNAL_CAM}")
+
+# Test Camera info
+cam_info = client.simGetCameraInfo(CAM_NAME, external=IS_EXTERNAL_CAM)
+print(cam_info)
+
+# Test Image APIs
+airsim.wait_key('Press any key to get images')
+
+requests = [airsim.ImageRequest(CAM_NAME, airsim.ImageType.Scene),
+ airsim.ImageRequest(CAM_NAME, airsim.ImageType.DepthPlanar),
+ airsim.ImageRequest(CAM_NAME, airsim.ImageType.DepthVis),
+ airsim.ImageRequest(CAM_NAME, airsim.ImageType.Segmentation),
+ airsim.ImageRequest(CAM_NAME, airsim.ImageType.SurfaceNormals)]
+
+def save_images(responses, prefix = ""):
+ for i, response in enumerate(responses):
+ filename = os.path.join(tmp_dir, prefix + "_" + str(i))
+
+ if response.pixels_as_float:
+ print(f"Type {response.image_type}, size {len(response.image_data_float)}, pos {response.camera_position}")
+ airsim.write_pfm(os.path.normpath(filename + '.pfm'), airsim.get_pfm_array(response))
+ else:
+ print(f"Type {response.image_type}, size {len(response.image_data_uint8)}, pos {response.camera_position}")
+ airsim.write_file(os.path.normpath(filename + '.png'), response.image_data_uint8)
+
+
+responses = client.simGetImages(requests, external=IS_EXTERNAL_CAM)
+save_images(responses, "old_fov")
+
+
+# Test FoV API
+airsim.wait_key('Press any key to change FoV and get images')
+
+client.simSetCameraFov(CAM_NAME, 120, external=IS_EXTERNAL_CAM)
+
+responses = client.simGetImages(requests, external = IS_EXTERNAL_CAM)
+save_images(responses, "new_fov")
+
+new_cam_info = client.simGetCameraInfo(CAM_NAME, external=IS_EXTERNAL_CAM)
+print(f"Old FOV: {cam_info.fov}, New FOV: {new_cam_info.fov}")
+
+
+# Test Pose APIs
+new_pose = airsim.Pose(airsim.Vector3r(-10, -5, -5), airsim.to_quaternion(0.1, 0, 0.1))
+client.simSetCameraPose(CAM_NAME, new_pose, external=IS_EXTERNAL_CAM)
+
+responses = client.simGetImages(requests, external=IS_EXTERNAL_CAM)
+save_images(responses, "new_pose")
+
+new_cam_info = client.simGetCameraInfo(CAM_NAME, external=IS_EXTERNAL_CAM)
+print(f"Old Pose: {cam_info.pose}, New Pose: {new_cam_info.pose}")
+
+
+# Test Distortion params APIs
+dist_params = client.simGetDistortionParams(CAM_NAME, external=IS_EXTERNAL_CAM)
+print(f"Distortion Params: {dist_params}")
+
+new_params_dict = {"K1": 0.1, "K2": 0.01, "K3": 0.0, "P1": 0.0, "P2": 0.0}
+print(f"Setting distortion params as {new_params_dict}")
+client.simSetDistortionParams(CAM_NAME, new_params_dict, external=IS_EXTERNAL_CAM)
+
+dist_params = client.simGetDistortionParams(CAM_NAME, external=IS_EXTERNAL_CAM)
+print(f"Updated Distortion Params: {dist_params}")
diff --git a/src/airsim/PythonClient/computer_vision/fov_change.py b/src/airsim/PythonClient/computer_vision/fov_change.py
new file mode 100644
index 0000000000..8c15c09210
--- /dev/null
+++ b/src/airsim/PythonClient/computer_vision/fov_change.py
@@ -0,0 +1,53 @@
+import setup_path
+import airsim
+import os
+import tempfile
+
+client = airsim.VehicleClient()
+client.confirmConnection()
+
+tmp_dir = os.path.join(tempfile.gettempdir(), "airsim_cv_mode")
+print ("Saving images to %s" % tmp_dir)
+try:
+ os.makedirs(tmp_dir)
+except OSError:
+ if not os.path.isdir(tmp_dir):
+ raise
+
+CAM_NAME = "front_center"
+print(f"Camera: {CAM_NAME}")
+
+airsim.wait_key('Press any key to get camera parameters')
+
+cam_info = client.simGetCameraInfo(CAM_NAME)
+print(cam_info)
+
+airsim.wait_key(f'Press any key to get images, saving to {tmp_dir}')
+
+requests = [airsim.ImageRequest(CAM_NAME, airsim.ImageType.Scene),
+ airsim.ImageRequest(CAM_NAME, airsim.ImageType.DepthVis)]
+
+def save_images(responses, prefix = ""):
+ for i, response in enumerate(responses):
+ filename = os.path.join(tmp_dir, prefix + "_" + str(i))
+ if response.pixels_as_float:
+ print(f"Type {response.image_type}, size {len(response.image_data_float)}, pos {response.camera_position}")
+ airsim.write_pfm(os.path.normpath(filename + '.pfm'), airsim.get_pfm_array(response))
+ else:
+ print(f"Type {response.image_type}, size {len(response.image_data_uint8)}, pos {response.camera_position}")
+ airsim.write_file(os.path.normpath(filename + '.png'), response.image_data_uint8)
+
+
+responses = client.simGetImages(requests)
+save_images(responses, "old_fov")
+
+airsim.wait_key('Press any key to change FoV and get images')
+
+client.simSetCameraFov(CAM_NAME, 120)
+responses = client.simGetImages(requests)
+save_images(responses, "new_fov")
+
+new_cam_info = client.simGetCameraInfo(CAM_NAME)
+print(new_cam_info)
+
+print(f"Old FOV: {cam_info.fov}, New FOV: {new_cam_info.fov}")
diff --git a/src/airsim/PythonClient/computer_vision/getpos.py b/src/airsim/PythonClient/computer_vision/getpos.py
new file mode 100644
index 0000000000..52783e744e
--- /dev/null
+++ b/src/airsim/PythonClient/computer_vision/getpos.py
@@ -0,0 +1,17 @@
+# In settings.json first activate computer vision mode:
+# https://github.com/Microsoft/AirSim/blob/main/docs/image_apis.md#computer-vision-mode
+
+import setup_path
+import airsim
+
+client = airsim.VehicleClient()
+client.confirmConnection()
+
+pose = client.simGetVehiclePose()
+print("x={}, y={}, z={}".format(pose.position.x_val, pose.position.y_val, pose.position.z_val))
+
+angles = airsim.to_eularian_angles(client.simGetVehiclePose().orientation)
+print("pitch={}, roll={}, yaw={}".format(angles[0], angles[1], angles[2]))
+
+pose.position.x_val = pose.position.x_val + 1
+client.simSetVehiclePose(pose, True)
\ No newline at end of file
diff --git a/src/airsim/PythonClient/computer_vision/ground_truth.py b/src/airsim/PythonClient/computer_vision/ground_truth.py
new file mode 100644
index 0000000000..32c63796c6
--- /dev/null
+++ b/src/airsim/PythonClient/computer_vision/ground_truth.py
@@ -0,0 +1,25 @@
+# In settings.json first activate computer vision mode:
+# https://github.com/Microsoft/AirSim/blob/main/docs/image_apis.md#computer-vision-mode
+
+import setup_path
+import airsim
+
+import pprint
+import time
+import cv2 #conda install opencv
+
+client = airsim.VehicleClient()
+client.confirmConnection()
+
+print("Time,Speed,Gear,PX,PY,PZ,OW,OX,OY,OZ")
+
+# monitor car state while you drive it manually.
+while (cv2.waitKey(1) & 0xFF) == 0xFF:
+ kinematics = client.simGetGroundTruthKinematics()
+ environment = client.simGetGroundTruthEnvironment()
+
+ print("Kinematics: %s\nEnvironemt %s" % (
+ pprint.pformat(kinematics), pprint.pformat(environment)))
+ time.sleep(1)
+
+
diff --git a/src/airsim/PythonClient/computer_vision/image_benchmarker.py b/src/airsim/PythonClient/computer_vision/image_benchmarker.py
new file mode 100644
index 0000000000..769af97ef9
--- /dev/null
+++ b/src/airsim/PythonClient/computer_vision/image_benchmarker.py
@@ -0,0 +1,155 @@
+import setup_path
+import airsim
+from argparse import ArgumentParser
+import time
+import threading
+import numpy as np
+import cv2
+import tempfile
+import os
+
+
+cameraTypeMap = {
+ "depth": airsim.ImageType.DepthVis,
+ "segmentation": airsim.ImageType.Segmentation,
+ "seg": airsim.ImageType.Segmentation,
+ "scene": airsim.ImageType.Scene,
+ "disparity": airsim.ImageType.DisparityNormalized,
+ "normals": airsim.ImageType.SurfaceNormals
+}
+
+CAM_NAME = "front_center"
+DEBUG = False
+
+def saveImage(response, filename):
+ if response.pixels_as_float:
+ # airsim.write_pfm(os.path.normpath(filename + '.pfm'), airsim.get_pfm_array(response))
+ depth = np.array(response.image_data_float, dtype=np.float64)
+ depth = depth.reshape((response.height, response.width, -1))
+ depth = np.array(depth * 255, dtype=np.uint8)
+ # save pic
+ cv2.imwrite(os.path.normpath(filename + '.png'), depth)
+
+ elif response.compress: #png format
+ airsim.write_file(os.path.normpath(filename + '.png'), response.image_data_uint8)
+
+ else: #uncompressed array
+ img1d = np.fromstring(response.image_data_uint8, dtype=np.uint8) # get numpy array
+ img_rgb = img1d.reshape(response.height, response.width, 3) # reshape array to 3 channel image array H X W X 3
+ cv2.imwrite(os.path.normpath(filename + '.png'), img_rgb) # write to png
+
+class ImageBenchmarker():
+ def __init__(self,
+ img_benchmark_type = 'simGetImages',
+ viz_image_cv2 = False,
+ save_images = False,
+ img_type = "scene"):
+ self.airsim_client = airsim.VehicleClient()
+ self.airsim_client.confirmConnection()
+ self.image_benchmark_num_images = 0
+ self.image_benchmark_total_time = 0.0
+ self.avg_fps = 0.0
+ self.image_callback_thread = None
+ self.viz_image_cv2 = viz_image_cv2
+ self.save_images = save_images
+
+ self.img_type = cameraTypeMap[img_type]
+
+ if img_benchmark_type == "simGetImage":
+ self.image_callback_thread = threading.Thread(target=self.repeat_timer_img, args=(self.image_callback_benchmark_simGetImage, 0.001))
+ if img_benchmark_type == "simGetImages":
+ self.image_callback_thread = threading.Thread(target=self.repeat_timer_img, args=(self.image_callback_benchmark_simGetImages, 0.001))
+ self.is_image_thread_active = False
+
+ if self.save_images:
+ self.tmp_dir = os.path.join(tempfile.gettempdir(), "airsim_img_bm")
+ print(f"Saving images to {self.tmp_dir}")
+ try:
+ os.makedirs(self.tmp_dir)
+ except OSError:
+ if not os.path.isdir(self.tmp_dir):
+ raise
+
+ def start_img_benchmark_thread(self):
+ if not self.is_image_thread_active:
+ self.is_image_thread_active = True
+ self.benchmark_start_time = time.time()
+ self.image_callback_thread.start()
+ print("Started img image_callback thread")
+
+ def stop_img_benchmark_thread(self):
+ if self.is_image_thread_active:
+ self.is_image_thread_active = False
+ self.image_callback_thread.join()
+ print("Stopped image callback thread.")
+ print(f"FPS: {self.avg_fps} for {self.image_benchmark_num_images} images")
+
+ def repeat_timer_img(self, task, period):
+ while self.is_image_thread_active:
+ task()
+ time.sleep(period)
+
+ def update_benchmark_results(self):
+ self.image_benchmark_total_time = time.time() - self.benchmark_start_time
+ self.avg_fps = self.image_benchmark_num_images / self.image_benchmark_total_time
+ if self.image_benchmark_num_images % 10 == 0:
+ print(f"Result: {self.avg_fps} avg_fps for {self.image_benchmark_num_images} images")
+
+ def image_callback_benchmark_simGetImage(self):
+ self.image_benchmark_num_images += 1
+ image = self.airsim_client.simGetImage(CAM_NAME, self.img_type)
+ np_arr = np.frombuffer(image, dtype=np.uint8)
+ # Change the below dimensions appropriately for the camera settings
+ img_rgb = np_arr.reshape(240, 512, 4)
+
+ self.update_benchmark_results()
+
+ if self.viz_image_cv2:
+ cv2.imshow("img_rgb", img_rgb)
+ cv2.waitKey(1)
+
+ def image_callback_benchmark_simGetImages(self):
+ self.image_benchmark_num_images += 1
+ request = [airsim.ImageRequest(CAM_NAME, self.img_type, False, False)]
+ responses = self.airsim_client.simGetImages(request)
+ response = responses[0]
+
+ self.update_benchmark_results()
+
+ if DEBUG:
+ if response.pixels_as_float:
+ print(f"Type {response.image_type}, size {len(response.image_data_float)},"
+ f"height {response.height}, width {response.width}")
+ else:
+ print(f"Type {response.image_type}, size {len(response.image_data_uint8)},"
+ f"height {response.height}, width {response.width}")
+
+ if self.viz_image_cv2:
+ np_arr = np.frombuffer(response.image_data_uint8, dtype=np.uint8)
+ img = np_arr.reshape(response.height, response.width, -1)
+ cv2.imshow("img", img)
+ cv2.waitKey(1)
+
+ if self.save_images:
+ filename = os.path.join(self.tmp_dir, str(self.image_benchmark_num_images))
+ saveImage(response, filename)
+
+
+def main(args):
+ image_benchmarker = ImageBenchmarker(img_benchmark_type=args.img_benchmark_type, viz_image_cv2=args.viz_image_cv2,
+ save_images=args.save_images, img_type=args.img_type)
+
+ image_benchmarker.start_img_benchmark_thread()
+ time.sleep(args.time)
+ image_benchmarker.stop_img_benchmark_thread()
+
+if __name__ == "__main__":
+ parser = ArgumentParser()
+ parser.add_argument('--img_benchmark_type', type=str, choices=["simGetImage", "simGetImages"], default="simGetImages")
+ parser.add_argument('--enable_viz_image_cv2', dest='viz_image_cv2', action='store_true', default=False)
+ parser.add_argument('--save_images', dest='save_images', action='store_true', default=False)
+ parser.add_argument('--img_type', type=str, choices=cameraTypeMap.keys(), default="scene")
+ parser.add_argument('--time', help="Time in secs to run the benchmark for", type=int, default=30)
+
+ args = parser.parse_args()
+ main(args)
diff --git a/src/airsim/PythonClient/computer_vision/objects.py b/src/airsim/PythonClient/computer_vision/objects.py
new file mode 100644
index 0000000000..f5356c31cf
--- /dev/null
+++ b/src/airsim/PythonClient/computer_vision/objects.py
@@ -0,0 +1,72 @@
+# In settings.json first activate computer vision mode:
+# https://github.com/Microsoft/AirSim/blob/main/docs/image_apis.md#computer-vision-mode
+
+import setup_path
+import airsim
+
+import pprint
+
+client = airsim.VehicleClient()
+client.confirmConnection()
+
+# objects can be named in two ways:
+# 1. In UE Editor, select and object and change its name to something else. Note that you must *change* its name because
+# default name is auto-generated and varies from run-to-run.
+# 2. OR you can do this: In UE Editor select the object and then go to "Actor" section, click down arrow to see "Tags" property and add a tag there.
+#
+# The simGetObjectPose and simSetObjectPose uses first object that has specified name OR tag.
+# more info: https://answers.unrealengine.com/questions/543807/whats-the-difference-between-tag-and-tag.html
+# https://answers.unrealengine.com/revisions/790629.html
+
+# below works in Blocks environment
+
+#------------------------------------ Get current pose ------------------------------------------------
+
+# search object by name:
+pose1 = client.simGetObjectPose("OrangeBall");
+print("OrangeBall - Position: %s, Orientation: %s" % (pprint.pformat(pose1.position),
+ pprint.pformat(pose1.orientation)))
+
+# search another object by tag
+pose2 = client.simGetObjectPose("PulsingCone");
+print("PulsingCone - Position: %s, Orientation: %s" % (pprint.pformat(pose2.position),
+ pprint.pformat(pose2.orientation)))
+
+# search non-existent object
+pose3 = client.simGetObjectPose("Non-Existent"); # should return nan pose
+print("Non-Existent - Position: %s, Orientation: %s" % (pprint.pformat(pose3.position),
+ pprint.pformat(pose3.orientation)))
+
+
+#------------------------------------ Set new pose ------------------------------------------------
+
+# here we move with teleport enabled so collisions are ignored
+pose1.position = pose1.position + airsim.Vector3r(-2, -2, -2)
+success = client.simSetObjectPose("OrangeBall", pose1, True);
+airsim.wait_key("OrangeBall moved. Success: %i" % (success))
+
+# here we move with teleport enabled so collisions are not ignored
+pose2.position = pose2.position + airsim.Vector3r(3, 3, -2)
+success = client.simSetObjectPose("PulsingCone", pose2, False);
+airsim.wait_key("PulsingCone moved. Success: %i" % (success))
+
+# move non-existent object
+success = client.simSetObjectPose("Non-Existent", pose2); # should return nan pose
+airsim.wait_key("Non-Existent moved. Success: %i" % (success))
+
+#------------------------------------ Get new pose ------------------------------------------------
+
+
+pose1 = client.simGetObjectPose("OrangeBall");
+print("OrangeBall - Position: %s, Orientation: %s" % (pprint.pformat(pose1.position),
+ pprint.pformat(pose1.orientation)))
+
+# search another object by tag
+pose2 = client.simGetObjectPose("PulsingCone");
+print("PulsingCone - Position: %s, Orientation: %s" % (pprint.pformat(pose2.position),
+ pprint.pformat(pose2.orientation)))
+
+# search non-existent object
+pose3 = client.simGetObjectPose("Non-Existent"); # should return nan pose
+print("Non-Existent - Position: %s, Orientation: %s" % (pprint.pformat(pose3.position),
+ pprint.pformat(pose3.orientation)))
\ No newline at end of file
diff --git a/src/airsim/PythonClient/computer_vision/seg_palette.py b/src/airsim/PythonClient/computer_vision/seg_palette.py
new file mode 100644
index 0000000000..664769cc9e
--- /dev/null
+++ b/src/airsim/PythonClient/computer_vision/seg_palette.py
@@ -0,0 +1,48 @@
+import numpy
+import random
+
+# requires Python 3.5.3 :: Anaconda 4.4.0
+# pip install opencv-python
+import cv2
+import pprint
+
+
+def generate_color_palette(numPixelsWide, outputFile):
+ random.seed(42)
+
+ palette = numpy.zeros((1, 256 * numPixelsWide, 3))
+ possibilities = [list(range(256)), list(range(256)), list(range(256))]
+
+ colors = [[0] * 3 for i in range(256)]
+
+ choice = 0
+ j = 0
+ for i in range(3):
+ palette[0, j * numPixelsWide : (j + 1) * numPixelsWide, i] = choice
+ colors[j][i] = choice
+
+ for i in range(3):
+ for j in range(1, 255):
+ choice = random.sample(possibilities[i], 1)[0]
+ possibilities[i].remove(choice)
+ palette[0, j * numPixelsWide : (j + 1) * numPixelsWide, i] = choice
+ colors[j][i] = choice
+
+ choice = 255
+ j = 255
+ for i in range(3):
+ palette[0, j * numPixelsWide : (j + 1) * numPixelsWide, i] = choice
+ colors[j][i] = choice
+
+ cv2.imwrite(outputFile, palette, [cv2.IMWRITE_PNG_COMPRESSION, 0])
+
+ rgb_file = open("rgbs.txt", "w")
+ for j in range(256):
+ rgb_file.write("%d\t%s\n" % (j, str(list(reversed(colors[j])))))
+ rgb_file.close()
+
+
+if __name__ == "__main__":
+ numPixelsWide = 4
+ outputFile = "seg_color_palette.png"
+ generate_color_palette(numPixelsWide, outputFile)
diff --git a/src/airsim/PythonClient/computer_vision/segmentation.py b/src/airsim/PythonClient/computer_vision/segmentation.py
new file mode 100644
index 0000000000..8a6a550fb5
--- /dev/null
+++ b/src/airsim/PythonClient/computer_vision/segmentation.py
@@ -0,0 +1,63 @@
+# In settings.json first activate computer vision mode:
+# https://github.com/Microsoft/AirSim/blob/main/docs/image_apis.md#computer-vision-mode
+
+import airsim
+import cv2
+import numpy as np
+import setup_path
+
+client = airsim.VehicleClient()
+client.confirmConnection()
+
+airsim.wait_key('Press any key to set all object IDs to 0')
+found = client.simSetSegmentationObjectID("[\w]*", 0, True);
+print("Done: %r" % (found))
+
+#for block environment
+
+airsim.wait_key('Press any key to change one ground object ID')
+found = client.simSetSegmentationObjectID("Ground", 20);
+print("Done: %r" % (found))
+
+#regex are case insensitive
+airsim.wait_key('Press any key to change all ground object ID')
+found = client.simSetSegmentationObjectID("ground[\w]*", 22, True);
+print("Done: %r" % (found))
+
+##for neighborhood environment
+
+#set object ID for sky
+found = client.simSetSegmentationObjectID("SkySphere", 42, True);
+print("Done: %r" % (found))
+
+#below doesn't work yet. You must set CustomDepthStencilValue in Unreal Editor for now
+airsim.wait_key('Press any key to set Landscape object ID to 128')
+found = client.simSetSegmentationObjectID("[\w]*", 128, True);
+print("Done: %r" % (found))
+
+#get segmentation image in various formats
+responses = client.simGetImages([
+ airsim.ImageRequest("0", airsim.ImageType.Segmentation, True), #depth in perspective projection
+ airsim.ImageRequest("0", airsim.ImageType.Segmentation, False, False)]) #scene vision image in uncompressed RGBA array
+print('Retrieved images: %d', len(responses))
+
+#save segmentation images in various formats
+for idx, response in enumerate(responses):
+ filename = 'c:/temp/py_seg_' + str(idx)
+
+ if response.pixels_as_float:
+ print("Type %d, size %d" % (response.image_type, len(response.image_data_float)))
+ #airsim.write_pfm(os.path.normpath(filename + '.pfm'), airsim.get_pfm_array(response))
+ elif response.compress: #png format
+ print("Type %d, size %d" % (response.image_type, len(response.image_data_uint8)))
+ #airsim.write_file(os.path.normpath(filename + '.png'), response.image_data_uint8)
+ else: #uncompressed array - numpy demo
+ print("Type %d, size %d" % (response.image_type, len(response.image_data_uint8)))
+ img1d = np.fromstring(response.image_data_uint8, dtype=np.uint8) #get numpy array
+ img_rgb = img1d.reshape(response.height, response.width, 3) #reshape array to 3 channel image array H X W X 3
+ # cv2.imwrite(os.path.normpath(filename + '.png'), img_rgb) # write to png
+
+ #find unique colors
+ print(np.unique(img_rgb[:,:,0], return_counts=True)) #red
+ print(np.unique(img_rgb[:,:,1], return_counts=True)) #green
+ print(np.unique(img_rgb[:,:,2], return_counts=True)) #blue
\ No newline at end of file
diff --git a/src/airsim/PythonClient/computer_vision/setup_path.py b/src/airsim/PythonClient/computer_vision/setup_path.py
new file mode 100644
index 0000000000..362113fea8
--- /dev/null
+++ b/src/airsim/PythonClient/computer_vision/setup_path.py
@@ -0,0 +1,52 @@
+# Import this module to automatically setup path to local airsim module
+# This module first tries to see if airsim module is installed via pip
+# If it does then we don't do anything else
+# Else we look up grand-parent folder to see if it has airsim folder
+# and if it does then we add that in sys.path
+
+import os,sys,inspect,logging
+
+#this class simply tries to see if airsim
+class SetupPath:
+ @staticmethod
+ def getDirLevels(path):
+ path_norm = os.path.normpath(path)
+ return len(path_norm.split(os.sep))
+
+ @staticmethod
+ def getCurrentPath():
+ cur_filepath = os.path.abspath(inspect.getfile(inspect.currentframe()))
+ return os.path.dirname(cur_filepath)
+
+ @staticmethod
+ def getGrandParentDir():
+ cur_path = SetupPath.getCurrentPath()
+ if SetupPath.getDirLevels(cur_path) >= 2:
+ return os.path.dirname(os.path.dirname(cur_path))
+ return ''
+
+ @staticmethod
+ def getParentDir():
+ cur_path = SetupPath.getCurrentPath()
+ if SetupPath.getDirLevels(cur_path) >= 1:
+ return os.path.dirname(cur_path)
+ return ''
+
+ @staticmethod
+ def addAirSimModulePath():
+ # if airsim module is installed then don't do anything else
+ #import pkgutil
+ #airsim_loader = pkgutil.find_loader('airsim')
+ #if airsim_loader is not None:
+ # return
+
+ parent = SetupPath.getParentDir()
+ if parent != '':
+ airsim_path = os.path.join(parent, 'airsim')
+ client_path = os.path.join(airsim_path, 'client.py')
+ if os.path.exists(client_path):
+ sys.path.insert(0, parent)
+ else:
+ logging.warning("airsim module not found in parent folder. Using installed package (pip install airsim).")
+
+SetupPath.addAirSimModulePath()
diff --git a/src/airsim/PythonClient/detection/detection.py b/src/airsim/PythonClient/detection/detection.py
new file mode 100644
index 0000000000..388228826a
--- /dev/null
+++ b/src/airsim/PythonClient/detection/detection.py
@@ -0,0 +1,43 @@
+import setup_path
+import airsim
+import cv2
+import numpy as np
+import pprint
+
+# connect to the AirSim simulator
+client = airsim.VehicleClient()
+client.confirmConnection()
+
+# set camera name and image type to request images and detections
+camera_name = "0"
+image_type = airsim.ImageType.Scene
+
+# set detection radius in [cm]
+client.simSetDetectionFilterRadius(camera_name, image_type, 200 * 100)
+# add desired object name to detect in wild card/regex format
+client.simAddDetectionFilterMeshName(camera_name, image_type, "Cylinder*")
+
+
+while True:
+ rawImage = client.simGetImage(camera_name, image_type)
+ if not rawImage:
+ continue
+ png = cv2.imdecode(airsim.string_to_uint8_array(rawImage), cv2.IMREAD_UNCHANGED)
+ cylinders = client.simGetDetections(camera_name, image_type)
+ if cylinders:
+ for cylinder in cylinders:
+ s = pprint.pformat(cylinder)
+ print("Cylinder: %s" % s)
+
+ cv2.rectangle(png,(int(cylinder.box2D.min.x_val),int(cylinder.box2D.min.y_val)),(int(cylinder.box2D.max.x_val),int(cylinder.box2D.max.y_val)),(255,0,0),2)
+ cv2.putText(png, cylinder.name, (int(cylinder.box2D.min.x_val),int(cylinder.box2D.min.y_val - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (36,255,12))
+
+
+ cv2.imshow("AirSim", png)
+ if cv2.waitKey(1) & 0xFF == ord('q'):
+ break
+ elif cv2.waitKey(1) & 0xFF == ord('c'):
+ client.simClearDetectionMeshNames(camera_name, image_type)
+ elif cv2.waitKey(1) & 0xFF == ord('a'):
+ client.simAddDetectionFilterMeshName(camera_name, image_type, "Cylinder*")
+cv2.destroyAllWindows()
diff --git a/src/airsim/PythonClient/detection/setup_path.py b/src/airsim/PythonClient/detection/setup_path.py
new file mode 100644
index 0000000000..a33b94789c
--- /dev/null
+++ b/src/airsim/PythonClient/detection/setup_path.py
@@ -0,0 +1,52 @@
+# Import this module to automatically setup path to local airsim module
+# This module first tries to see if airsim module is installed via pip
+# If it does then we don't do anything else
+# Else we look up grand-parent folder to see if it has airsim folder
+# and if it does then we add that in sys.path
+
+import os,sys,logging
+
+#this class simply tries to see if airsim
+class SetupPath:
+ @staticmethod
+ def getDirLevels(path):
+ path_norm = os.path.normpath(path)
+ return len(path_norm.split(os.sep))
+
+ @staticmethod
+ def getCurrentPath():
+ cur_filepath = __file__
+ return os.path.dirname(cur_filepath)
+
+ @staticmethod
+ def getGrandParentDir():
+ cur_path = SetupPath.getCurrentPath()
+ if SetupPath.getDirLevels(cur_path) >= 2:
+ return os.path.dirname(os.path.dirname(cur_path))
+ return ''
+
+ @staticmethod
+ def getParentDir():
+ cur_path = SetupPath.getCurrentPath()
+ if SetupPath.getDirLevels(cur_path) >= 1:
+ return os.path.dirname(cur_path)
+ return ''
+
+ @staticmethod
+ def addAirSimModulePath():
+ # if airsim module is installed then don't do anything else
+ #import pkgutil
+ #airsim_loader = pkgutil.find_loader('airsim')
+ #if airsim_loader is not None:
+ # return
+
+ parent = SetupPath.getParentDir()
+ if parent != '':
+ airsim_path = os.path.join(parent, 'airsim')
+ client_path = os.path.join(airsim_path, 'client.py')
+ if os.path.exists(client_path):
+ sys.path.insert(0, parent)
+ else:
+ logging.warning("airsim module not found in parent folder. Using installed package (pip install airsim).")
+
+SetupPath.addAirSimModulePath()
diff --git a/src/airsim/PythonClient/docs/Makefile b/src/airsim/PythonClient/docs/Makefile
new file mode 100644
index 0000000000..298ea9e213
--- /dev/null
+++ b/src/airsim/PythonClient/docs/Makefile
@@ -0,0 +1,19 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
\ No newline at end of file
diff --git a/src/airsim/PythonClient/docs/conf.py b/src/airsim/PythonClient/docs/conf.py
new file mode 100644
index 0000000000..2df296ed16
--- /dev/null
+++ b/src/airsim/PythonClient/docs/conf.py
@@ -0,0 +1,205 @@
+# -*- coding: utf-8 -*-
+#
+# Configuration file for the Sphinx documentation builder.
+#
+# This file does only contain a selection of the most common options. For a
+# full list see the documentation:
+# http://www.sphinx-doc.org/en/master/config
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+import sys
+sys.path.insert(0, os.path.abspath('..'))
+
+import sphinx_rtd_theme
+from airsim import __version__
+
+# -- Project information -----------------------------------------------------
+
+project = u'airsim'
+copyright = u'2020, Shital Shah, Ratnesh Madaan, Sai Vemprala, Nicholas Gyde'
+author = u'Shital Shah, Ratnesh Madaan, Sai Vemprala, Nicholas Gyde'
+
+# The short X.Y version
+version = __version__
+# The full version, including alpha/beta/rc tags
+release = version
+
+
+# -- General configuration ---------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.autosummary',
+ 'sphinx.ext.autosectionlabel',
+ 'sphinx.ext.doctest',
+ 'sphinx.ext.intersphinx',
+ 'sphinx.ext.todo',
+ 'sphinx.ext.mathjax',
+ 'sphinx.ext.coverage',
+ 'sphinx.ext.napoleon',
+ 'sphinx.ext.viewcode',
+ 'sphinx_rtd_theme'
+]
+
+autodoc_default_flags = ['members']
+autosummary_generate = True
+autosectionlabel_prefix_document = True
+autosectionlabel_maxdepth = 4
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store']
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = None
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+# html_theme = 'alabaster'
+html_theme = "sphinx_rtd_theme"
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Custom sidebar templates, must be a dictionary that maps document names
+# to template names.
+#
+# The default sidebars (for documents that don't match any pattern) are
+# defined by theme itself. Builtin themes are using these templates by
+# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
+# 'searchbox.html']``.
+#
+# html_sidebars = {}
+
+
+# -- Options for HTMLHelp output ---------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'airsimdoc'
+
+
+# -- Options for LaTeX output ------------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #
+ # 'papersize': 'letterpaper',
+
+ # The font size ('10pt', '11pt' or '12pt').
+ #
+ # 'pointsize': '10pt',
+
+ # Additional stuff for the LaTeX preamble.
+ #
+ # 'preamble': '',
+
+ # Latex figure (float) alignment
+ #
+ # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (master_doc, 'airsim.tex', u'airsim Documentation',
+ u'Ratnesh Madaan, Matthew Brown, Nicholas Gyde', 'manual'),
+]
+
+
+# -- Options for manual page output ------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (master_doc, 'airsim', u'airsim Documentation',
+ [author], 1)
+]
+
+
+# -- Options for Texinfo output ----------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (master_doc, 'airsim', u'airsim Documentation',
+ author, 'airsim', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+
+# -- Options for Epub output -------------------------------------------------
+
+# Bibliographic Dublin Core info.
+epub_title = project
+
+# The unique identifier of the text. This can be a ISBN number
+# or the project homepage.
+#
+# epub_identifier = ''
+
+# A unique identification for the text.
+#
+# epub_uid = ''
+
+# A list of files that should not be packed into the epub file.
+epub_exclude_files = ['search.html']
+
+
+# -- Extension configuration -------------------------------------------------
+
+# -- Options for intersphinx extension ---------------------------------------
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {'https://docs.python.org/': None}
+
+# -- Options for todo extension ----------------------------------------------
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = True
diff --git a/src/airsim/PythonClient/docs/index.rst b/src/airsim/PythonClient/docs/index.rst
new file mode 100644
index 0000000000..c256e3fcd4
--- /dev/null
+++ b/src/airsim/PythonClient/docs/index.rst
@@ -0,0 +1,42 @@
+.. airsim documentation master file, created by
+ sphinx-quickstart on Sun Aug 4 14:58:26 2019.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+airsim
+=========================================
+This page documents `airsim`_, the python package to be used for `Microsoft AirSim`_.
+
+.. _`airsim`: https://pypi.org/project/airsim/
+.. _`Microsoft AirSim`: https://github.com/microsoft/AirSim
+
+.. toctree::
+ :maxdepth: 3
+
+* :ref:`genindex`
+* :ref:`modindex`
+
+.. autoclass:: airsim.client.VehicleClient
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: airsim.client.MultirotorClient
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: airsim.client.CarClient
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. automodule:: airsim.types
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. automodule:: airsim.utils
+ :members:
+ :undoc-members:
+ :show-inheritance:
\ No newline at end of file
diff --git a/src/airsim/PythonClient/docs/make.bat b/src/airsim/PythonClient/docs/make.bat
new file mode 100644
index 0000000000..7893348a1b
--- /dev/null
+++ b/src/airsim/PythonClient/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
+
+:end
+popd
diff --git a/src/airsim/PythonClient/environment/change_texture_example.py b/src/airsim/PythonClient/environment/change_texture_example.py
new file mode 100644
index 0000000000..b10a8aff51
--- /dev/null
+++ b/src/airsim/PythonClient/environment/change_texture_example.py
@@ -0,0 +1,7 @@
+import airsim
+
+c = airsim.MultirotorClient()
+c.confirmConnection()
+
+c.simSetObjectMaterialFromTexture("OrangeBall", "sample_texture.jpg")
+
diff --git a/src/airsim/PythonClient/environment/create_objects.py b/src/airsim/PythonClient/environment/create_objects.py
new file mode 100644
index 0000000000..5dc445d1ce
--- /dev/null
+++ b/src/airsim/PythonClient/environment/create_objects.py
@@ -0,0 +1,32 @@
+import setup_path
+import airsim
+import random
+import time
+
+client = airsim.VehicleClient()
+client.confirmConnection()
+
+assets = client.simListAssets()
+print(f"Assets: {assets}")
+
+scale = airsim.Vector3r(1.0, 1.0, 1.0)
+
+# asset_name = random.choice(assets)
+asset_name = '1M_Cube_Chamfer'
+
+desired_name = f"{asset_name}_spawn_{random.randint(0, 100)}"
+pose = airsim.Pose(position_val=airsim.Vector3r(5.0, 0.0, 0.0))
+
+obj_name = client.simSpawnObject(desired_name, asset_name, pose, scale, True)
+
+print(f"Created object {obj_name} from asset {asset_name} "
+ f"at pose {pose}, scale {scale}")
+
+all_objects = client.simListSceneObjects()
+if obj_name not in all_objects:
+ print(f"Object {obj_name} not present!")
+
+time.sleep(10.0)
+
+print(f"Destroying {obj_name}")
+client.simDestroyObject(obj_name)
diff --git a/src/airsim/PythonClient/environment/light_control.py b/src/airsim/PythonClient/environment/light_control.py
new file mode 100644
index 0000000000..39fa0f06ed
--- /dev/null
+++ b/src/airsim/PythonClient/environment/light_control.py
@@ -0,0 +1,23 @@
+import airsim
+import time
+
+client = airsim.VehicleClient()
+client.confirmConnection()
+
+# Access an existing light in the world
+lights = client.simListSceneObjects("PointLight.*")
+pose = client.simGetObjectPose(lights[0])
+scale = airsim.Vector3r(1, 1, 1)
+
+# Destroy the light
+client.simDestroyObject(lights[0])
+time.sleep(1)
+
+# Create a new light at the same pose
+new_light_name = client.simSpawnObject("PointLight", "PointLightBP", pose, scale, False, True)
+time.sleep(1)
+
+# Change the light's intensity
+for i in range(20):
+ client.simSetLightIntensity(new_light_name, i * 100)
+ time.sleep(0.5)
diff --git a/src/airsim/PythonClient/environment/plot_markers.py b/src/airsim/PythonClient/environment/plot_markers.py
new file mode 100644
index 0000000000..d005ff8338
--- /dev/null
+++ b/src/airsim/PythonClient/environment/plot_markers.py
@@ -0,0 +1,63 @@
+import setup_path
+import airsim
+from airsim import Vector3r, Quaternionr, Pose
+from airsim.utils import to_quaternion
+import numpy as np
+import time
+
+client = airsim.VehicleClient()
+client.confirmConnection()
+
+# plot red arrows for 30 seconds
+client.simPlotArrows(points_start = [Vector3r(x,y,0) for x, y in zip(np.linspace(0,10,20), np.linspace(0,0,20))],
+ points_end = [Vector3r(x,y,0) for x, y in zip(np.linspace(0,10,20), np.linspace(10,10,20))],
+ color_rgba = [1.0, 0.0, 1.0, 1.0], duration = 30.0, arrow_size = 10, thickness = 1)
+
+# plot magenta arrows for 15 seconds
+client.simPlotArrows(points_start = [Vector3r(x,y,-3) for x, y in zip(np.linspace(0,10,20), np.linspace(0,0,20))],
+ points_end = [Vector3r(x,y,-5) for x, y in zip(np.linspace(0,10,20), np.linspace(10,20,20))],
+ color_rgba = [1.0, 1.0, 0.0, 1.0], duration = 15.0, arrow_size = 20, thickness = 3)
+
+# plot red arrows for 10 seconds
+client.simPlotArrows(points_start = [Vector3r(x,y,z) for x, y, z in zip(np.linspace(0,10,20), np.linspace(0,0,20), np.linspace(-3,-10, 20))],
+ points_end = [Vector3r(x,y,z) for x, y, z in zip(np.linspace(0,10,20), np.linspace(10,20,20), np.linspace(-5,-8, 20))],
+ color_rgba = [1.0, 0.0, 0.0, 1.0], duration = 10.0, arrow_size = 100, thickness = 5)
+
+# plot 2 white arrows which are persistent
+client.simPlotArrows(points_start = [Vector3r(x,y,-2) for x, y in zip(np.linspace(0,10,20), np.linspace(0,20,20))],
+ points_end = [Vector3r(x,y,-5) for x, y in zip(np.linspace(3,17,20), np.linspace(5,28,20))],
+ color_rgba = [1.0, 1.0, 1.0, 1.0], duration = 5.0, arrow_size = 100, thickness = 1, is_persistent = True)
+
+# plot points
+client.simPlotPoints(points = [Vector3r(x,y,-5) for x, y in zip(np.linspace(0,-10,20), np.linspace(0,-20,20))], color_rgba=[1.0, 0.0, 0.0, 1.0], size = 25, duration = 20.0, is_persistent = False)
+client.simPlotPoints(points = [Vector3r(x,y,z) for x, y, z in zip(np.linspace(0,-10,20), np.linspace(0,-20,20), np.linspace(0,-5,20))], color_rgba=[0.0, 0.0, 1.0, 1.0], size = 10, duration = 20.0, is_persistent = False)
+client.simPlotPoints(points = [Vector3r(x,y,z) for x, y, z in zip(np.linspace(0,10,20), np.linspace(0,-20,20), np.linspace(0,-7,20))], color_rgba=[1.0, 0.0, 1.0, 1.0], size = 15, duration = 20.0, is_persistent = False)
+
+# plot line strip. 0-1, 1-2, 2-3
+client.simPlotLineStrip(points = [Vector3r(x,y,-5) for x, y in zip(np.linspace(0,-10,10), np.linspace(0,-20,10))], color_rgba=[1.0, 0.0, 0.0, 1.0], thickness = 5, duration = 30.0, is_persistent = False)
+
+# plot line list. 0-1, 2-3, 4-5. Must be even.
+client.simPlotLineList(points = [Vector3r(x,y,-7) for x, y in zip(np.linspace(0,-10,10), np.linspace(0,-20,10))], color_rgba=[1.0, 0.0, 0.0, 1.0], thickness = 5, duration = 30.0, is_persistent = False)
+
+# plot transforms
+client.simPlotStrings(strings = ["Microsoft AirSim" for i in range(5)], positions = [Vector3r(x,y,-1) for x, y in zip(np.linspace(0,5,5), np.linspace(0,0,5))],
+ scale = 1, color_rgba=[1.0, 1.0, 1.0, 1.0], duration = 1200.0)
+
+# client.simPlotTransforms(poses = [Pose(position_val=Vector3r(x,y,0), orientation_val=to_quaternion(pitch=0.0, roll=0.0, yaw=yaw)) for x, y, yaw in zip(np.linspace(0,10,10), np.linspace(0,0,10), np.linspace(0,np.pi,10))],
+# scale = 35, thickness = 5, duration = 1200.0, is_persistent = False)
+
+# client.simPlotTransforms(poses = [Pose(position_val=Vector3r(x,y,0), orientation_val=to_quaternion(pitch=0.0, roll=roll, yaw=0.0)) for x, y, roll in zip(np.linspace(0,10,10), np.linspace(1,1,10), np.linspace(0,np.pi,10))],
+# scale = 35, thickness = 5, duration = 1200.0, is_persistent = False)
+
+client.simPlotTransformsWithNames(poses = [Pose(position_val=Vector3r(x,y,0), orientation_val=to_quaternion(pitch=0.0, roll=0.0, yaw=yaw)) for x, y, yaw in zip(np.linspace(0,10,10), np.linspace(0,0,10), np.linspace(0,np.pi,10))],
+ names=["tf_yaw_" + str(idx) for idx in range(10)], tf_scale = 35, tf_thickness = 5, text_scale = 1, text_color_rgba = [1.0, 1.0, 1.0, 1.0], duration = 1200.0)
+
+client.simPlotTransformsWithNames(poses = [Pose(position_val=Vector3r(x,y,0), orientation_val=to_quaternion(pitch=0.0, roll=roll, yaw=0.0)) for x, y, roll in zip(np.linspace(0,10,10), np.linspace(1,1,10), np.linspace(0,np.pi,10))],
+ names=["tf_roll_" + str(idx) for idx in range(10)], tf_scale = 35, tf_thickness = 5, text_scale = 1, text_color_rgba = [1.0, 1.0, 1.0, 1.0], duration = 1200.0)
+
+client.simPlotTransformsWithNames(poses = [Pose(position_val=Vector3r(x,y,0), orientation_val=to_quaternion(pitch=pitch, roll=0.0, yaw=0.0)) for x, y, pitch in zip(np.linspace(0,10,10), np.linspace(-1,-1,10), np.linspace(0,np.pi,10))],
+ names=["tf_pitch_" + str(idx) for idx in range(10)], tf_scale = 35, tf_thickness = 5, text_scale = 1, text_color_rgba = [1.0, 1.0, 1.0, 1.0], duration = 1200.0)
+
+time.sleep(20.0)
+
+client.simFlushPersistentMarkers()
\ No newline at end of file
diff --git a/src/airsim/PythonClient/environment/sample_texture.jpg b/src/airsim/PythonClient/environment/sample_texture.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..90812505111c61e1e639b7794c4b100d693f2a77
GIT binary patch
literal 1101411
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