diff --git a/.github/templates/docker_context/docker_context.sh b/.github/templates/docker_context/docker_context.sh index e8dc0d1f..dda15b60 100755 --- a/.github/templates/docker_context/docker_context.sh +++ b/.github/templates/docker_context/docker_context.sh @@ -39,6 +39,7 @@ while read -r module; do # Temporarily skip perception services that have too large image size if [[ "$service_out" == "lane_detection" ]] || \ [[ "$service_out" == "camera_object_detection" ]] || \ + [[ "$service_out" == "lidar_object_detection" ]] || \ [[ "$service_out" == "semantic_segmentation" ]]; then continue fi diff --git a/docker/perception/lidar_object_detection/lidar_object_detection.Dockerfile b/docker/perception/lidar_object_detection/lidar_object_detection.Dockerfile index 6deba0bc..e231e2ac 100644 --- a/docker/perception/lidar_object_detection/lidar_object_detection.Dockerfile +++ b/docker/perception/lidar_object_detection/lidar_object_detection.Dockerfile @@ -1,55 +1,92 @@ -ARG BASE_IMAGE=ghcr.io/watonomous/wato_monorepo/base:humble-ubuntu22.04 - +ARG BASE_IMAGE=ghcr.io/watonomous/wato_monorepo/base:cuda11.7-humble-ubuntu22.04-devel ################################ Source ################################ FROM ${BASE_IMAGE} as source - WORKDIR ${AMENT_WS}/src - # Copy in source code COPY src/perception/lidar_object_detection lidar_object_detection -COPY src/wato_msgs/sample_msgs sample_msgs - # Scan for rosdeps RUN apt-get -qq update && rosdep update && \ rosdep install --from-paths . --ignore-src -r -s \ - | grep 'apt-get install' \ - | awk '{print $3}' \ - | sort > /tmp/colcon_install_list - + | grep 'apt-get install' \ + | awk '{print $3}' \ + | sort > /tmp/colcon_install_list +WORKDIR /home/bolty +RUN git clone https://github.com/WATonomous/OpenPCDet.git && \ + cd OpenPCDet \ + && git checkout 06fd4f862329625ff9ed850464330816e54531f8 +################################ INSTALL OpenCV with CUDA Support ############## +WORKDIR /opt +RUN git clone -b 4.x https://github.com/opencv/opencv.git && \ + cd opencv && git checkout 4.5.5 +RUN git clone -b 4.x https://github.com/opencv/opencv_contrib.git && \ + cd opencv_contrib && git checkout 4.5.5 +WORKDIR /opt/opencv/build +RUN cmake -D CMAKE_BUILD_TYPE=RELEASE \ + -D CMAKE_INSTALL_PREFIX=/usr/local \ + -D OPENCV_GENERATE_PKGCONFIG=ON \ + -D BUILD_EXAMPLES=OFF \ + -D INSTALL_PYTHON_EXAMPLES=OFF \ + -D INSTALL_C_EXAMPLES=OFF \ + -D PYTHON_EXECUTABLE=$(which python3) \ + -D PYTHON3_INCLUDE_DIR=$(python3 -c "from distutils.sysconfig import get_python_inc; print(get_python_inc())") \ + -D PYTHON3_PACKAGES_PATH=$(python3 -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())") \ + -D BUILD_opencv_python3=ON \ + -D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules/ \ + -D WITH_GSTREAMER=ON \ + -D WITH_CUDA=ON \ + -D ENABLE_PRECOMPILED_HEADERS=OFF \ + .. && make -j$(nproc) && make install && ldconfig +RUN rm -rf /opt/opencv /opt/opencv_contrib ################################# Dependencies ################################ FROM ${BASE_IMAGE} as dependencies - +RUN apt-get update && apt-get install -y \ + git zip unzip libssl-dev libcairo2-dev lsb-release libgoogle-glog-dev libgflags-dev libatlas-base-dev libeigen3-dev software-properties-common \ + build-essential cmake pkg-config libapr1-dev autoconf automake libtool curl libc6 libboost-all-dev debconf libomp5 libstdc++6 \ + libqt5core5a libqt5xml5 libqt5gui5 libqt5widgets5 libqt5concurrent5 libqt5opengl5 libcap2 libusb-1.0-0 libatk-adaptor neovim \ + python3-pip python3-setuptools \ + && apt-get -qq autoremove -y && apt-get -qq autoclean && apt-get -qq clean && \ + rm -rf /var/lib/apt/lists/* /root/* /root/.ros /tmp/* /usr/share/doc +# Set environment variables +ENV CUDA_HOME /usr/local/cuda +ENV LD_LIBRARY_PATH /usr/local/cuda/lib64:${LD_LIBRARY_PATH} +ENV PATH /usr/local/cuda/bin:${PATH} +ENV OpenCV_DIR=/usr/share/OpenCV +# Install Python dependencies +RUN pip3 install torch==1.13.1+cu116 torchvision==0.14.1+cu116 torchaudio==0.13.1 --extra-index-url https://download.pytorch.org/whl/cu116 +RUN pip3 install torch-scatter -f https://data.pyg.org/whl/torch-1.13.1+cu116.html +ENV TORCH_CUDA_ARCH_LIST="3.5;5.0;6.0;6.1;7.0;7.5;8.0;8.6+PTX" +RUN pip3 install spconv-cu116 pyquaternion numpy==1.23 pillow==8.4 mayavi open3d av2 # Install Rosdep requirements COPY --from=source /tmp/colcon_install_list /tmp/colcon_install_list +RUN apt-get update && apt-fast update RUN apt-fast install -qq -y --no-install-recommends $(cat /tmp/colcon_install_list) - # Copy in source code from source stage WORKDIR ${AMENT_WS} COPY --from=source ${AMENT_WS}/src src - # Dependency Cleanup WORKDIR / RUN apt-get -qq autoremove -y && apt-get -qq autoclean && apt-get -qq clean && \ rm -rf /root/* /root/.ros /tmp/* /var/lib/apt/lists/* /usr/share/doc/* - -################################ Build ################################ +################################ Build ####################################### FROM dependencies as build - +COPY --from=source /home/bolty/OpenPCDet /home/bolty/OpenPCDet +WORKDIR /home/bolty/OpenPCDet +RUN pip3 install -r requirements.txt +RUN pip3 install kornia==0.6.8 +RUN pip3 install nuscenes-devkit==1.0.5 +WORKDIR /home/bolty/OpenPCDet +RUN python3 setup.py develop # Build ROS2 packages WORKDIR ${AMENT_WS} RUN . /opt/ros/$ROS_DISTRO/setup.sh && \ colcon build \ - --cmake-args -DCMAKE_BUILD_TYPE=Release - + --cmake-args -DCMAKE_BUILD_TYPE=Release # Entrypoint will run before any CMD on launch. Sources ~/opt//setup.bash and ~/ament_ws/install/setup.bash COPY docker/wato_ros_entrypoint.sh ${AMENT_WS}/wato_ros_entrypoint.sh ENTRYPOINT ["./wato_ros_entrypoint.sh"] - -################################ Prod ################################ +################################ Prod ######################################### FROM build as deploy - # Source Cleanup and Security Setup RUN chown -R $USER:$USER ${AMENT_WS} RUN rm -rf src/* - USER ${USER} diff --git a/modules/docker-compose.perception.yaml b/modules/docker-compose.perception.yaml index f095ff37..ecc82108 100644 --- a/modules/docker-compose.perception.yaml +++ b/modules/docker-compose.perception.yaml @@ -34,7 +34,6 @@ services: - /mnt/wato-drive2/perception_models/yolov8m.pt:/perception_models/yolov8m.pt - /mnt/wato-drive2/perception_models/traffic_light.pt:/perception_models/traffic_light.pt - /mnt/wato-drive2/perception_models/traffic_signs_v0.pt:/perception_models/traffic_signs_v1.pt - lidar_object_detection: build: context: .. @@ -44,8 +43,9 @@ services: - "${PERCEPTION_LIDAR_OBJECT_DETECTION_IMAGE}:build_main" target: deploy image: "${PERCEPTION_LIDAR_OBJECT_DETECTION_IMAGE}:${TAG}" - command: /bin/bash -c "ros2 launch lidar_object_detection lidar_object_detection.launch.py" - + command: /bin/bash -c "ros2 launch lidar_object_detection nuscenes_launch.py" + volumes: + - /mnt/wato-drive2/perception_models/transfusion_trained_model.pth:/home/bolty/OpenPCDet/models/transfusion_trained_model.pth semantic_segmentation: build: context: .. @@ -97,4 +97,4 @@ services: - "${PERCEPTION_DEPTH_ESTIMATION_IMAGE}:build_main" target: deploy image: "${PERCEPTION_DEPTH_ESTIMATION_IMAGE}:${TAG}" - command: /bin/bash -c "ros2 launch depth_estimation eve.launch.py" + command: /bin/bash -c "ros2 launch depth_estimation eve.launch.py" \ No newline at end of file diff --git a/src/perception/lidar_object_detection/CMakeLists.txt b/src/perception/lidar_object_detection/CMakeLists.txt deleted file mode 100644 index 3a5b0750..00000000 --- a/src/perception/lidar_object_detection/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -cmake_minimum_required(VERSION 3.8) -project(lidar_object_detection) - -if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall -Wextra -Wpedantic) -endif() - -# find dependencies -find_package(ament_cmake REQUIRED) -# uncomment the following section in order to fill in -# further dependencies manually. -# find_package( REQUIRED) - -ament_package() diff --git a/src/perception/lidar_object_detection/lidar_object_detection/config/eve_config.yaml b/src/perception/lidar_object_detection/lidar_object_detection/config/eve_config.yaml new file mode 100644 index 00000000..7117fddb --- /dev/null +++ b/src/perception/lidar_object_detection/lidar_object_detection/config/eve_config.yaml @@ -0,0 +1,5 @@ +lidar_object_detection_node: + ros__parameters: + lidar_topic: /velodyne_points + model_config_path: /home/bolty/OpenPCDet/tools/cfgs/nuscenes_models/transfusion_lidar.yaml + model_path: /home/bolty/OpenPCDet/models/transfusion_trained_model.pth diff --git a/src/perception/lidar_object_detection/lidar_object_detection/config/nuscenes_config.yaml b/src/perception/lidar_object_detection/lidar_object_detection/config/nuscenes_config.yaml new file mode 100644 index 00000000..b1d8c58b --- /dev/null +++ b/src/perception/lidar_object_detection/lidar_object_detection/config/nuscenes_config.yaml @@ -0,0 +1,6 @@ +lidar_object_detection_node: + ros__parameters: + lidar_topic: /LIDAR_TOP + model_config_path: /home/bolty/OpenPCDet/tools/cfgs/nuscenes_models/transfusion_lidar.yaml + model_path: /home/bolty/OpenPCDet/models/cbgs_transfusion_lidar.pth + enable_detection: true diff --git a/src/perception/lidar_object_detection/lidar_object_detection/launch/eve_launch.py b/src/perception/lidar_object_detection/lidar_object_detection/launch/eve_launch.py new file mode 100644 index 00000000..78f2c2ac --- /dev/null +++ b/src/perception/lidar_object_detection/lidar_object_detection/launch/eve_launch.py @@ -0,0 +1,27 @@ +from launch import LaunchDescription +from launch_ros.actions import Node +from ament_index_python.packages import get_package_share_directory +import os + + +def generate_launch_description(): + ld = LaunchDescription() + config = os.path.join( + get_package_share_directory('lidar_object_detection'), + 'config', + 'eve_config.yaml' + ) + + # nodes + lidar_object_detection = Node( + package='lidar_object_detection', + executable='lidar_object_detection_node', + name='lidar_object_detection_node', + parameters=[config], + arguments=['--ros-args', '--log-level', 'info'] + ) + + # finalize + ld.add_action(lidar_object_detection) + + return ld diff --git a/src/perception/lidar_object_detection/lidar_object_detection/launch/nuscenes_launch.py b/src/perception/lidar_object_detection/lidar_object_detection/launch/nuscenes_launch.py new file mode 100644 index 00000000..64cc1ef5 --- /dev/null +++ b/src/perception/lidar_object_detection/lidar_object_detection/launch/nuscenes_launch.py @@ -0,0 +1,27 @@ +from launch import LaunchDescription +from launch_ros.actions import Node +from ament_index_python.packages import get_package_share_directory +import os + + +def generate_launch_description(): + ld = LaunchDescription() + config = os.path.join( + get_package_share_directory('lidar_object_detection'), + 'config', + 'nuscenes_config.yaml' + ) + + # nodes + lidar_object_detection = Node( + package='lidar_object_detection', + executable='lidar_object_detection_node', + name='lidar_object_detection_node', + parameters=[config], + arguments=['--ros-args', '--log-level', 'info'] + ) + + # finalize + ld.add_action(lidar_object_detection) + + return ld diff --git a/src/perception/lidar_object_detection/lidar_object_detection/lidar_object_detection/__init__.py b/src/perception/lidar_object_detection/lidar_object_detection/lidar_object_detection/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/perception/lidar_object_detection/lidar_object_detection/lidar_object_detection/label_server.py b/src/perception/lidar_object_detection/lidar_object_detection/lidar_object_detection/label_server.py new file mode 100644 index 00000000..92035ced --- /dev/null +++ b/src/perception/lidar_object_detection/lidar_object_detection/lidar_object_detection/label_server.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +import rclpy +from rclpy.node import Node +from vision_msgs.msg import VisionInfo + + +class LabelServer(Node): + def __init__(self): + super().__init__('label_server') + self.publisher_ = self.create_publisher(VisionInfo, 'vision_info', 10) + self.label_mapping = { + 1: "car", + 2: "pedestrian", + 3: "cyclist", + } + self.publish_labels() + + def publish_labels(self): + vision_info_msg = VisionInfo() + vision_info_msg.database_location = "memory" + vision_info_msg.database_version = "1.0" + for label_id, class_name in self.label_mapping.items(): + vision_info_msg.class_map[label_id] = class_name + self.publisher_.publish(vision_info_msg) + + +def main(args=None): + rclpy.init(args=args) + label_server = LabelServer() + rclpy.spin(label_server) + label_server.destroy_node() + rclpy.shutdown() + + +if __name__ == '__main__': + main() diff --git a/src/perception/lidar_object_detection/lidar_object_detection/lidar_object_detection/lidar_object_detection_node.py b/src/perception/lidar_object_detection/lidar_object_detection/lidar_object_detection/lidar_object_detection_node.py new file mode 100644 index 00000000..d81a21bc --- /dev/null +++ b/src/perception/lidar_object_detection/lidar_object_detection/lidar_object_detection/lidar_object_detection_node.py @@ -0,0 +1,173 @@ +# pylint: disable=wrong-import-position +from sensor_msgs.msg import PointCloud2 +from vision_msgs.msg import ObjectHypothesisWithPose, Detection3D, Detection3DArray, VisionInfo +from visualization_msgs.msg import Marker, MarkerArray +from pcdet.config import cfg, cfg_from_yaml_file +from pcdet.datasets import DatasetTemplate +from pcdet.models import build_network, load_data_to_gpu +from pcdet.utils import common_utils +import torch +import numpy as np +from rclpy.node import Node +import rclpy +import argparse +import sys +sys.path.append("/home/bolty/OpenPCDet") + + +class LidarObjectDetection(Node): + def __init__(self): + super().__init__('lidar_object_detection') + self.declare_parameter("model_path") + self.declare_parameter("model_config_path") + self.declare_parameter("lidar_topic") + self.model_path = self.get_parameter("model_path").value + self.model_config_path = self.get_parameter("model_config_path").value + self.lidar_data = self.get_parameter("lidar_topic").value + self.publish_detection = self.get_parameter( + 'enable_detection').get_parameter_value().bool_value + + self.label_mapping = {} + self.subscription = self.create_subscription( + VisionInfo, + 'vision_info', + self.vision_info_callback, + 10) + self.viz_publisher = self.create_publisher(MarkerArray, "/lidar_detections_viz", 10) + self.detections_publisher = self.create_publisher(Detection3DArray, "/lidar_detections", 10) + + self.subscription = self.create_subscription( + PointCloud2, self.lidar_data, self.point_cloud_callback, 10 + ) + + args, cfg = self.parse_config() + self.logger = common_utils.create_logger() + + self.lidar_dataloader = LidarDatalodaer( + dataset_cfg=cfg.DATA_CONFIG, + class_names=cfg.CLASS_NAMES, + training=False, + logger=self.logger, + ) + + self.model = build_network( + model_cfg=cfg.MODEL, num_class=len(cfg.CLASS_NAMES), dataset=self.lidar_dataloader + ) + self.model.load_params_from_file(filename=args.ckpt, logger=self.logger, to_cpu=True) + self.model.cuda() + self.model.eval() + + def vision_info_callback(self, msg): + self.label_mapping = msg.class_map + + def point_cloud_callback(self, msg): + points = self.pointcloud2_to_xyz_array(msg) + data_dict = { + "points": points, + "frame_id": msg.header.frame_id, + } + data_dict = self.lidar_dataloader.prepare_data(data_dict=data_dict) + data_dict = self.lidar_dataloader.collate_batch([data_dict]) + load_data_to_gpu(data_dict) + + with torch.no_grad(): + pred_dicts, _ = self.model.forward(data_dict) + + original_timestamp = msg.header.stamp + self.publish_bounding_boxes(msg, pred_dicts, original_timestamp) + + def pointcloud2_to_xyz_array(self, cloud_msg): + num_points = cloud_msg.width * cloud_msg.height + cloud_array = np.frombuffer(cloud_msg.data, dtype=np.float32) + num_fields = cloud_msg.point_step // 4 + cloud_array = cloud_array.reshape(num_points, num_fields) + if cloud_array.shape[1] > 4: + cloud_array = cloud_array[:, :4] + if cloud_array.shape[1] <= 4: + timestamp = np.full((num_points, 1), fill_value=0.0, dtype=np.float32) + cloud_array = np.hstack((cloud_array, timestamp)) + return cloud_array + + def publish_bounding_boxes(self, pointcloud_msg, pred_dicts, original_timestamp): + marker_array = MarkerArray() + detections = Detection3DArray() + detections.header = pointcloud_msg.header + for idx, (box, score) in enumerate(zip(pred_dicts[0]["pred_boxes"], pred_dicts[0]["pred_scores"])): + if score > 0.6: + marker = Marker() + marker.header = pointcloud_msg.header + marker.header.stamp = original_timestamp + marker.id = idx + marker.type = Marker.CUBE + marker.action = Marker.ADD + marker.pose.position.x = float(box[0]) + marker.pose.position.y = float(box[1]) + marker.pose.position.z = float(box[2]) + + # Calculate orientation quaternion + yaw = float(box[6]) + 1e-10 + marker.pose.orientation.z = np.sin(yaw / 2) + marker.pose.orientation.w = np.cos(yaw / 2) + + marker.scale.x = float(box[3]) + marker.scale.y = float(box[4]) + marker.scale.z = float(box[5]) + marker.color.a = 0.8 + marker.color.r = 1.0 + marker.color.g = 0.0 + marker.color.b = float(pred_dicts[0]["pred_labels"][idx]) / 3 + + class_id = pred_dicts[0]["pred_labels"][idx] + class_name = self.label_mapping.get(class_id, "unknown") + marker.text = class_name + marker_array.markers.append(marker) + + detection = Detection3D() + detection.header = pointcloud_msg.header + detection.bbox.center.position.x = float(box[0]) + detection.bbox.center.position.y = float(box[1]) + detection.bbox.center.position.z = float(box[2]) + detection.bbox.size.x = float(box[3]) + detection.bbox.size.y = float(box[4]) + detection.bbox.size.z = float(box[5]) + detected_object = ObjectHypothesisWithPose() + detected_object.hypothesis.class_id = str(pred_dicts[0]["pred_labels"][idx]) + detected_object.hypothesis.score = float(pred_dicts[0]["pred_scores"][idx]) + detection.results.append(detected_object) + detections.detections.append(detection) + if self.publish_detection: + self.viz_publisher.publish(marker_array) + self.detections_publisher.publish(detections) + + def parse_config(self): + parser = argparse.ArgumentParser(description="arg parser") + parser.add_argument( + "--cfg_file", + type=str, + default=self.model_config_path, + help="specify the config for demo", + ) + parser.add_argument( + "--ckpt", type=str, default=self.model_path, help="specify the pretrained model" + ) + + args, _ = parser.parse_known_args() + cfg_from_yaml_file(args.cfg_file, cfg) + return args, cfg + + +class LidarDatalodaer(DatasetTemplate): + def __init__(self, dataset_cfg, class_names, training=True, logger=None, ext=".bin"): + super().__init__(dataset_cfg=dataset_cfg, class_names=class_names, training=training, logger=logger) + + +def main(args=None): + rclpy.init(args=args) + node = LidarObjectDetection() + rclpy.spin(node) + node.destroy_node() + rclpy.shutdown() + + +if __name__ == '__main__': + main() diff --git a/src/perception/lidar_object_detection/lidar_object_detection/package.xml b/src/perception/lidar_object_detection/lidar_object_detection/package.xml new file mode 100644 index 00000000..57b447bb --- /dev/null +++ b/src/perception/lidar_object_detection/lidar_object_detection/package.xml @@ -0,0 +1,26 @@ + + + + lidar_object_detection + 0.0.0 + TODO: Package description + bolty + TODO: License declaration + + rclpy + std_msgs + rospy + sensor_msgs + vision_msgs + tf + visualization_msgs + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + diff --git a/src/perception/lidar_object_detection/lidar_object_detection/resource/lidar_object_detection b/src/perception/lidar_object_detection/lidar_object_detection/resource/lidar_object_detection new file mode 100644 index 00000000..e69de29b diff --git a/src/perception/lidar_object_detection/lidar_object_detection/setup.cfg b/src/perception/lidar_object_detection/lidar_object_detection/setup.cfg new file mode 100644 index 00000000..4cbdef16 --- /dev/null +++ b/src/perception/lidar_object_detection/lidar_object_detection/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/lidar_object_detection +[install] +install_scripts=$base/lib/lidar_object_detection diff --git a/src/perception/lidar_object_detection/lidar_object_detection/setup.py b/src/perception/lidar_object_detection/lidar_object_detection/setup.py new file mode 100644 index 00000000..c0459e9a --- /dev/null +++ b/src/perception/lidar_object_detection/lidar_object_detection/setup.py @@ -0,0 +1,32 @@ +import os +from setuptools import find_packages, setup +from glob import glob + +package_name = 'lidar_object_detection' + +setup( + name=package_name, + version='0.0.0', + packages=find_packages(exclude=['test']), + data_files=[ + ('share/ament_index/resource_index/packages', + ['resource/' + package_name]), + ('share/' + package_name, ['package.xml']), + (os.path.join('share', package_name, 'launch'), + glob('launch/*.py')), + (os.path.join('share', package_name, 'config'), glob('config/*.yaml')), + ], + install_requires=['setuptools'], + zip_safe=True, + maintainer='Dan Huynh', + maintainer_email='danielrhuynh@watonomous.ca', + description='TODO: Package description', + license='TODO: License declaration', + tests_require=['pytest'], + entry_points={ + 'console_scripts': [ + 'lidar_object_detection_node = lidar_object_detection.lidar_object_detection_node:main', + 'label_server = lidar_object_detection.label_server:main', + ], + }, +) diff --git a/src/perception/lidar_object_detection/package.xml b/src/perception/lidar_object_detection/package.xml deleted file mode 100644 index 5dc0a5df..00000000 --- a/src/perception/lidar_object_detection/package.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - lidar_object_detection - 0.0.0 - TODO: Package description - bolty - TODO: License declaration - - ament_cmake - - - - ament_cmake - - diff --git a/src/samples/python/aggregator/setup.py b/src/samples/python/aggregator/setup.py index b0afb9f6..f77c1804 100755 --- a/src/samples/python/aggregator/setup.py +++ b/src/samples/python/aggregator/setup.py @@ -14,7 +14,7 @@ # Include our package.xml file (os.path.join('share', package_name), ['package.xml']), # Include all launch files. - (os.path.join('share', package_name, 'launch'), \ + (os.path.join('share', package_name, 'launch'), glob(os.path.join('launch', '*.launch.py'))), ], install_requires=['setuptools'],