From 71eb12b535de46fe7be41f4ec834b349826d57de Mon Sep 17 00:00:00 2001 From: Alok Sharma <84761994+NOEMOJI041@users.noreply.github.com> Date: Mon, 18 Sep 2023 18:44:35 +0530 Subject: [PATCH 01/17] Added RetinaNet Plugin for `object_detection` package (#37) * Added RetinaNet plugin --- object_detection/config/params.yaml | 5 +- .../object_detection/Detectors/RetinaNet.py | 129 +++++------------- 2 files changed, 39 insertions(+), 95 deletions(-) diff --git a/object_detection/config/params.yaml b/object_detection/config/params.yaml index 909a042..94d7f1a 100644 --- a/object_detection/config/params.yaml +++ b/object_detection/config/params.yaml @@ -3,9 +3,10 @@ object_detection: input_img_topic: color_camera/image_raw output_bb_topic: object_detection/img_bb output_img_topic: object_detection/img + publish_output_img: 1 model_params: - detector_type: YOLOv5 + detector_type: RetinaNet model_dir_path: models/ - weight_file_name: auto_final.onnx + weight_file_name: resnet50_coco_best_v2.1.0.h5 confidence_threshold : 0.7 show_fps : 1 \ No newline at end of file diff --git a/object_detection/object_detection/Detectors/RetinaNet.py b/object_detection/object_detection/Detectors/RetinaNet.py index 8a54122..eb04a4c 100755 --- a/object_detection/object_detection/Detectors/RetinaNet.py +++ b/object_detection/object_detection/Detectors/RetinaNet.py @@ -1,75 +1,39 @@ -#!/usr/bin/env python3 +import os -from tensorflow import keras from keras_retinanet import models -from keras_retinanet.utils.image import read_image_bgr, preprocess_image, resize_image -from keras_retinanet.utils.visualization import draw_box, draw_caption -from keras_retinanet.utils.colors import label_color -import matplotlib.pyplot as plt -import cv2 -import os +from keras_retinanet.utils.image import preprocess_image, resize_image import numpy as np -import time -import matplotlib.pyplot as plt +from ..DetectorBase import DetectorBase -class RetinaNet: - def __init__(self, model_dir_path, weight_file_name, conf_threshold = 0.7, - score_threshold = 0.4, nms_threshold = 0.25, is_cuda = 0, show_fps = 1): - self.model_dir_path = model_dir_path - self.weight_file_name = weight_file_name +class RetinaNet(DetectorBase) : + def __init(self) : - self.predictions = [] - self.conf_threshold = conf_threshold - self.show_fps = show_fps - self.is_cuda = is_cuda + super.__init__() - if self.show_fps : - self.frame_count = 0 - self.total_frames = 0 - self.fps = -1 - self.start = time.time_ns() + def build_model(self, model_dir_path, weight_file_name) : + model_path = os.path.join(model_dir_path, weight_file_name) - self.labels_to_names = self.load_classes() - self.build_model() - - def build_model(self) : - - try : - self.model_path = os.path.join(self.model_dir_path, self.weight_file_name) - self.model = models.load_model(self.model_path, backbone_name='resnet50') - - except : - raise Exception("Error loading given model from path: {}. Maybe the file doesn't exist?".format(self.model_path)) - + try: + self.model = models.load_model(model_path, backbone_name='resnet50') + except: + raise Exception("Error loading given model from path: {}. Maybe the file doesn't exist?".format(model_path)) - def load_classes(self): + def load_classes(self, model_dir_path) : self.class_list = [] - with open(self.model_dir_path + "/classes.txt", "r") as f: + with open(model_dir_path + "/classes.txt", "r") as f: self.class_list = [cname.strip() for cname in f.readlines()] return self.class_list - - def create_predictions_list(self, class_ids, confidences, boxes): - for i in range(len(class_ids)): - obj_dict = { - "class_id": class_ids[i], - "confidence": confidences[i], - "box": boxes[i] - } - - self.predictions.append(obj_dict) - - - def get_predictions(self, cv_image): + def get_predictions(self, cv_image) : if cv_image is None: # TODO: show warning message (different color, maybe) - return None,None - - else : + return None + + else : # copy to draw on self.frame = cv_image.copy() @@ -77,46 +41,25 @@ def get_predictions(self, cv_image): input = preprocess_image(self.frame) input, scale = resize_image(input) - self.frame_count += 1 - self.total_frames += 1 - # process image - start = time.time() - boxes, scores, labels = self.model.predict_on_batch(np.expand_dims(input, axis=0)) - #print("processing time: ", time.time() - start) + boxes_all, confidences_all, class_ids_all = self.model.predict_on_batch(np.expand_dims(input, axis=0)) + + boxes, confidences, class_ids = [], [], [] + + for index in range(len(confidences_all[0])) : + if confidences_all[0][index]!=-1 : + confidences.append(confidences_all[0][index]) + boxes.append(boxes_all[0][index]) + class_ids.append(class_ids_all[0][index]) + # correct for image scale - boxes /= scale - - self.create_predictions_list(labels, scores, boxes) - - # visualize detections - for box, score, label in zip(boxes[0], scores[0], labels[0]): - # scores are sorted so we can break - if score < self.conf_threshold: - break - - color = label_color(label) - - b = box.astype(int) - draw_box(self.frame, b, color=color) - - caption = "{} {:.3f}".format(self.labels_to_names[label], score) - #print(self.labels_to_names[label]) - draw_caption(self.frame, b, caption) - - if self.show_fps : - if self.frame_count >= 30: - self.end = time.time_ns() - self.fps = 1000000000 * self.frame_count / (self.end - self.start) - self.frame_count = 0 - self.start = time.time_ns() - - if self.fps > 0: - self.fps_label = "FPS: %.2f" % self.fps - cv2.putText(self.frame, self.fps_label, (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) - - return (self.predictions, self.frame) - + #boxes = [x/scale for x in boxes] + boxes = [[int(coord/scale) for coord in box] for box in boxes] + + super().create_predictions_list(class_ids, confidences, boxes) - \ No newline at end of file + return self.predictions + + + From 8c9515b4a581b1daf94038d7154a3bfb6fccd316 Mon Sep 17 00:00:00 2001 From: Alok Sharma <84761994+NOEMOJI041@users.noreply.github.com> Date: Fri, 29 Sep 2023 16:45:05 +0530 Subject: [PATCH 02/17] Added package dependencies for `rosdep` (#36) * added package depend * added gz_sim depend * updated readme(rosdep) * added bash commands for percep_ws * added python path note --- README.md | 37 ++++++++++++++++++++++++++++++---- object_detection/package.xml | 4 +++- perception_bringup/package.xml | 3 ++- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 49abc22..3f1fea6 100644 --- a/README.md +++ b/README.md @@ -126,11 +126,22 @@ Refer to the official [ROS 2 installation guide](https://docs.ros.org/en/humble/ Now go ahead and clone this repository inside the "src" folder of the workspace you just created. ```bash - cd percep_ws/src - git clone git@github.com:atom-robotics-lab/ros-perception-pipeline.git + cd percep_ws && git clone git@github.com:atom-robotics-lab/ros-perception-pipeline.git src/ ``` +3. Install dependencies using rosdep -3. Compile the package + Update Your rosdep before installation. + + ```bash + rosdep update + ``` + + This command installs all the packages that the packages in your catkin workspace depend upon but are missing on your computer. + ```bash + rosdep install --from-paths src --ignore-src -r -y + ``` + +4. Compile the package Follow this execution to compile your ROS 2 package @@ -138,7 +149,8 @@ Refer to the official [ROS 2 installation guide](https://docs.ros.org/en/humble/ colcon build --symlink-install ``` -4. Source your workspace +5. Source your workspace + ```bash source install/local_setup.bash ``` @@ -181,8 +193,25 @@ file according to your present working directory ros2 run object_detection ObjectDetection --ros-args --params-file src/ros-perception-pipeline/object_detection/config/object_detection.yaml ``` +**Note :** If your imports are not working while using a virtual environment, you'll need to manually set your `PYTHONPATH` environment variable. +Follow these steps to do this : + +1. Activate your virtual environment + +2. Find out the path of your virtual environment's Python installation + ```bash + which Python + ``` + +3. Set your `PYTHONPATH` + ```bash + export PYTHONPATH = {insert_your_python_path_here} + ``` + + ### 3. Changing the Detector + To change the object detector being used, you can change the parameters inside the object_detection.yaml file location inside the **config** folder. diff --git a/object_detection/package.xml b/object_detection/package.xml index 068e66a..8d10362 100644 --- a/object_detection/package.xml +++ b/object_detection/package.xml @@ -11,7 +11,9 @@ ament_flake8 ament_pep257 python3-pytest - + vision_msgs + cv_bridge + ament_python diff --git a/perception_bringup/package.xml b/perception_bringup/package.xml index fafefbc..3f1d1cd 100644 --- a/perception_bringup/package.xml +++ b/perception_bringup/package.xml @@ -11,7 +11,8 @@ ament_lint_auto ament_lint_common - + ros_gz + ros_gz_bridge ament_cmake From 8a4340d869d67c62072e7e9894985d2e8ddd6801 Mon Sep 17 00:00:00 2001 From: Arjun K Haridas <51917087+topguns837@users.noreply.github.com> Date: Tue, 10 Oct 2023 22:43:36 +0530 Subject: [PATCH 03/17] Added .github folder with templates for feature request, bug report and pull requests (#42) * Added .github folder with templates for feature request and bug report * Added template for pull requests --- .github/issues_and_bugs/bug_report.md | 43 ++++++++++++++++++++++ .github/issues_and_bugs/feature_request.md | 23 ++++++++++++ .github/pull_request/pull_request.md | 30 +++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 .github/issues_and_bugs/bug_report.md create mode 100644 .github/issues_and_bugs/feature_request.md create mode 100644 .github/pull_request/pull_request.md diff --git a/.github/issues_and_bugs/bug_report.md b/.github/issues_and_bugs/bug_report.md new file mode 100644 index 0000000..9a7ddda --- /dev/null +++ b/.github/issues_and_bugs/bug_report.md @@ -0,0 +1,43 @@ +--- +name: Bug report +about: Report a bug +labels: bug +--- + +## Environment 🖥️ +* OS Version: + +* Python Version: + +* Do you have a dedicated GPU ? + +* NVIDIA Driver Version (if applicable): + +* CUDA Version (if applicable): + +* Are you running this project natively or using Docker ? + +* Is the issue present in the main branch or any of the development branch ? + +
+ +``` +# paste log here +``` + +
+ +## Description 📖 +* Expected behavior: +* Actual behavior: + +## Steps to reproduce 👀 + + +1. +2. +3. + +## Output 💥 + \ No newline at end of file diff --git a/.github/issues_and_bugs/feature_request.md b/.github/issues_and_bugs/feature_request.md new file mode 100644 index 0000000..80fb539 --- /dev/null +++ b/.github/issues_and_bugs/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Request a new feature +labels: enhancement +--- + +## Desired behavior + + +## Alternatives considered + + +## Implementation suggestion + + +## Examples of this feature in some other project (if applicable) + + +## Additional context + \ No newline at end of file diff --git a/.github/pull_request/pull_request.md b/.github/pull_request/pull_request.md new file mode 100644 index 0000000..ac31a7e --- /dev/null +++ b/.github/pull_request/pull_request.md @@ -0,0 +1,30 @@ +# Description 📖 + +Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change. + +Fixes # (issue) + +## Type of change 📜 + +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update +- [ ] This change requires testing before it can be merged into the main/development branch + +# How Has This Been Tested? 👀 + +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration + +- [ ] Test A +- [ ] Test B + +**Test Configuration** 🖥️ +* OS version: +* Hardware: +* NVIDIA Driver: +* CUDA Version: + + From 9f0e1056429a2d54251040b993b1c7441bfd0981 Mon Sep 17 00:00:00 2001 From: Abhishek Garg <132362014+abhishekgarg2511@users.noreply.github.com> Date: Tue, 10 Oct 2023 22:56:00 +0530 Subject: [PATCH 04/17] Added Apache 2.0 License (#41) * Added LICENSE * minor changes * license name added * copyright(c) added --- LICENSE | 195 +++++++++++++++++++++++++++++++++ object_detection/package.xml | 2 +- perception_bringup/package.xml | 2 +- 3 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..919de01 --- /dev/null +++ b/LICENSE @@ -0,0 +1,195 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + Copyright (c)2023 A.T.O.M ROBOTICS + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + + + \ No newline at end of file diff --git a/object_detection/package.xml b/object_detection/package.xml index 8d10362..482516c 100644 --- a/object_detection/package.xml +++ b/object_detection/package.xml @@ -5,7 +5,7 @@ 0.0.0 TODO: Package description singh - TODO: License declaration + Apache 2.0 ament_copyright ament_flake8 diff --git a/perception_bringup/package.xml b/perception_bringup/package.xml index 3f1d1cd..a4894b2 100644 --- a/perception_bringup/package.xml +++ b/perception_bringup/package.xml @@ -5,7 +5,7 @@ 0.0.0 TODO: Package description singh - TODO: License declaration + Apache 2.0 ament_cmake From a790df1a473fbbba94133d939340e9e81ef940b8 Mon Sep 17 00:00:00 2001 From: Arjun K Haridas <51917087+topguns837@users.noreply.github.com> Date: Tue, 10 Oct 2023 23:20:26 +0530 Subject: [PATCH 05/17] Added Templates for P.R., Issues and bug reports (#43) * Added .github folder with templates for feature request and bug report * Added template for pull requests * Re-arranged .github template files --- .github/ISSUE_TEMPLATE/bug_report.md | 43 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 23 ++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 30 ++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..9a7ddda --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,43 @@ +--- +name: Bug report +about: Report a bug +labels: bug +--- + +## Environment 🖥️ +* OS Version: + +* Python Version: + +* Do you have a dedicated GPU ? + +* NVIDIA Driver Version (if applicable): + +* CUDA Version (if applicable): + +* Are you running this project natively or using Docker ? + +* Is the issue present in the main branch or any of the development branch ? + +
+ +``` +# paste log here +``` + +
+ +## Description 📖 +* Expected behavior: +* Actual behavior: + +## Steps to reproduce 👀 + + +1. +2. +3. + +## Output 💥 + \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..80fb539 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Request a new feature +labels: enhancement +--- + +## Desired behavior + + +## Alternatives considered + + +## Implementation suggestion + + +## Examples of this feature in some other project (if applicable) + + +## Additional context + \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..ac31a7e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,30 @@ +# Description 📖 + +Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change. + +Fixes # (issue) + +## Type of change 📜 + +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update +- [ ] This change requires testing before it can be merged into the main/development branch + +# How Has This Been Tested? 👀 + +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration + +- [ ] Test A +- [ ] Test B + +**Test Configuration** 🖥️ +* OS version: +* Hardware: +* NVIDIA Driver: +* CUDA Version: + + From ea73ffd503a3ac8372b815c9b3dd61a1f4875627 Mon Sep 17 00:00:00 2001 From: topguns837 Date: Tue, 10 Oct 2023 23:23:32 +0530 Subject: [PATCH 06/17] Removed unecessary folders in .github --- .github/issues_and_bugs/bug_report.md | 43 ---------------------- .github/issues_and_bugs/feature_request.md | 23 ------------ .github/pull_request/pull_request.md | 30 --------------- 3 files changed, 96 deletions(-) delete mode 100644 .github/issues_and_bugs/bug_report.md delete mode 100644 .github/issues_and_bugs/feature_request.md delete mode 100644 .github/pull_request/pull_request.md diff --git a/.github/issues_and_bugs/bug_report.md b/.github/issues_and_bugs/bug_report.md deleted file mode 100644 index 9a7ddda..0000000 --- a/.github/issues_and_bugs/bug_report.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -name: Bug report -about: Report a bug -labels: bug ---- - -## Environment 🖥️ -* OS Version: - -* Python Version: - -* Do you have a dedicated GPU ? - -* NVIDIA Driver Version (if applicable): - -* CUDA Version (if applicable): - -* Are you running this project natively or using Docker ? - -* Is the issue present in the main branch or any of the development branch ? - -
- -``` -# paste log here -``` - -
- -## Description 📖 -* Expected behavior: -* Actual behavior: - -## Steps to reproduce 👀 - - -1. -2. -3. - -## Output 💥 - \ No newline at end of file diff --git a/.github/issues_and_bugs/feature_request.md b/.github/issues_and_bugs/feature_request.md deleted file mode 100644 index 80fb539..0000000 --- a/.github/issues_and_bugs/feature_request.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: Feature request -about: Request a new feature -labels: enhancement ---- - -## Desired behavior - - -## Alternatives considered - - -## Implementation suggestion - - -## Examples of this feature in some other project (if applicable) - - -## Additional context - \ No newline at end of file diff --git a/.github/pull_request/pull_request.md b/.github/pull_request/pull_request.md deleted file mode 100644 index ac31a7e..0000000 --- a/.github/pull_request/pull_request.md +++ /dev/null @@ -1,30 +0,0 @@ -# Description 📖 - -Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change. - -Fixes # (issue) - -## Type of change 📜 - -Please delete options that are not relevant. - -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] This change requires a documentation update -- [ ] This change requires testing before it can be merged into the main/development branch - -# How Has This Been Tested? 👀 - -Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration - -- [ ] Test A -- [ ] Test B - -**Test Configuration** 🖥️ -* OS version: -* Hardware: -* NVIDIA Driver: -* CUDA Version: - - From b1816ad2119f8f8eeb76e6fabd2bb2a4a0c3dab1 Mon Sep 17 00:00:00 2001 From: Krachitkumar Date: Thu, 14 Dec 2023 22:29:21 +0530 Subject: [PATCH 07/17] added image preprocessing node --- image_preprocessing/CMakeLists.txt | 36 +++++++++++++++++++++++ image_preprocessing/package.xml | 18 ++++++++++++ image_preprocessing/src/test_node.cpp | 42 +++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 image_preprocessing/CMakeLists.txt create mode 100644 image_preprocessing/package.xml create mode 100644 image_preprocessing/src/test_node.cpp diff --git a/image_preprocessing/CMakeLists.txt b/image_preprocessing/CMakeLists.txt new file mode 100644 index 0000000..fe95cae --- /dev/null +++ b/image_preprocessing/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.5) +#project(image_publisher) +project(image_preprocessing) +# Find dependencies +find_package(ament_cmake REQUIRED) +find_package(rclcpp REQUIRED) +find_package(sensor_msgs REQUIRED) + +# Add executable +add_executable(test_node src/test_node.cpp) + +# Include directories for the executable +target_include_directories(test_node + PRIVATE + ${sensor_msgs_INCLUDE_DIRS} +) + +# Link the executable to the required libraries +ament_target_dependencies(test_node + rclcpp + sensor_msgs +) + +# Install the executable +install(TARGETS + test_node + DESTINATION lib/${PROJECT_NAME} +) + +# Install launch files, config files, and other directories if necessary +# install(DIRECTORY launch +# DESTINATION share/${PROJECT_NAME} +# ) + +# Install the CMake package file +ament_package() diff --git a/image_preprocessing/package.xml b/image_preprocessing/package.xml new file mode 100644 index 0000000..0ad68f3 --- /dev/null +++ b/image_preprocessing/package.xml @@ -0,0 +1,18 @@ + + + + image_preprocessing + 0.0.0 + TODO: Package description + rachit + TODO: License declaration + + ament_cmake + + ament_lint_auto + ament_lint_common + + + ament_cmake + + diff --git a/image_preprocessing/src/test_node.cpp b/image_preprocessing/src/test_node.cpp new file mode 100644 index 0000000..dfc7566 --- /dev/null +++ b/image_preprocessing/src/test_node.cpp @@ -0,0 +1,42 @@ +#include "rclcpp/rclcpp.hpp" +#include "sensor_msgs/msg/image.hpp" + +class ImagePublisherNode : public rclcpp::Node { +public: + ImagePublisherNode() : Node("image_publisher_node") { + // Create a subscription to the "/color_camera/image_raw" topic + imagesubscription = create_subscription( + "/color_camera/image_raw", 10, [this](const sensor_msgs::msg::Image::SharedPtr msg) { + // Callback function for the subscription + publishImage(msg); + // std::cout<<"Publishing Image"<("img_pub", 10); + + // Set the publishing rate to 10 Hz + publishtimer = create_wall_timer(std::chrono::milliseconds(100), [this]() { + // Timer callback for publishing at 10 Hz + // You can perform any additional processing here if needed + }); + } + +private: + void publishImage(const sensor_msgs::msg::Image::SharedPtr msg) { + auto loaned_msg = std::make_unique(*msg); + imagepublisher->publish(std::move(loaned_msg)); + + } + + rclcpp::Subscription::SharedPtr imagesubscription; + rclcpp::Publisher::SharedPtr imagepublisher; + rclcpp::TimerBase::SharedPtr publishtimer; +}; + +int main(int argc, char** argv) { + rclcpp::init(argc, argv); + rclcpp::spin(std::make_shared()); + rclcpp::shutdown(); + return 0; +} \ No newline at end of file From ae51a4e50b5c862b2262cf7bb4ab045742d98c53 Mon Sep 17 00:00:00 2001 From: Krachitkumar Date: Sat, 16 Dec 2023 13:21:35 +0530 Subject: [PATCH 08/17] resolved cv_bridge error --- image_preprocessing/CMakeLists.txt | 5 ++++- image_preprocessing/src/test_node.cpp | 22 ++++++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/image_preprocessing/CMakeLists.txt b/image_preprocessing/CMakeLists.txt index fe95cae..f8170f6 100644 --- a/image_preprocessing/CMakeLists.txt +++ b/image_preprocessing/CMakeLists.txt @@ -5,7 +5,8 @@ project(image_preprocessing) find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) find_package(sensor_msgs REQUIRED) - +find_package(cv_bridge REQUIRED) +find_package(OpenCV REQUIRED) # Add executable add_executable(test_node src/test_node.cpp) @@ -19,6 +20,8 @@ target_include_directories(test_node ament_target_dependencies(test_node rclcpp sensor_msgs + cv_bridge + OpenCV ) # Install the executable diff --git a/image_preprocessing/src/test_node.cpp b/image_preprocessing/src/test_node.cpp index dfc7566..3fbe9d3 100644 --- a/image_preprocessing/src/test_node.cpp +++ b/image_preprocessing/src/test_node.cpp @@ -1,5 +1,8 @@ #include "rclcpp/rclcpp.hpp" #include "sensor_msgs/msg/image.hpp" +#include "cv_bridge/cv_bridge.h" +#include "sensor_msgs/image_encodings.hpp" +#include class ImagePublisherNode : public rclcpp::Node { public: @@ -9,7 +12,6 @@ class ImagePublisherNode : public rclcpp::Node { "/color_camera/image_raw", 10, [this](const sensor_msgs::msg::Image::SharedPtr msg) { // Callback function for the subscription publishImage(msg); - // std::cout<<"Publishing Image"<(*msg); - imagepublisher->publish(std::move(loaned_msg)); - + // Convert sensor_msgs::Image to cv::Mat using cv_bridge + cv_bridge::CvImageConstPtr cv_ptr; + try { + cv_ptr = cv_bridge::toCvCopy(msg, sensor_msgs::image_encodings::BGR8); + } catch (cv_bridge::Exception& e) { + RCLCPP_ERROR(this->get_logger(), "cv_bridge exception: %s", e.what()); + return; + } + + // Add text to the image + cv::putText(cv_ptr->image, "Img Processed", cv::Point(10, 50), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(255, 255, 255), 2); + + // Publish the modified image + auto modified_msg = cv_bridge::CvImage(msg->header, "bgr8", cv_ptr->image).toImageMsg(); + imagepublisher->publish(modified_msg); } rclcpp::Subscription::SharedPtr imagesubscription; From a4cbce78875b819dc5b648fda0fda9f5d34f8348 Mon Sep 17 00:00:00 2001 From: topguns837 Date: Sat, 16 Dec 2023 13:57:57 +0530 Subject: [PATCH 09/17] Resolved OpenCV issue --- image_preprocessing/CMakeLists.txt | 6 +++- image_preprocessing/src/test_node.cpp | 42 ++++++++++++++------------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/image_preprocessing/CMakeLists.txt b/image_preprocessing/CMakeLists.txt index f8170f6..fa3edfa 100644 --- a/image_preprocessing/CMakeLists.txt +++ b/image_preprocessing/CMakeLists.txt @@ -1,12 +1,15 @@ cmake_minimum_required(VERSION 3.5) -#project(image_publisher) + project(image_preprocessing) + # Find dependencies find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) find_package(sensor_msgs REQUIRED) find_package(cv_bridge REQUIRED) +find_package(image_transport REQUIRED) find_package(OpenCV REQUIRED) + # Add executable add_executable(test_node src/test_node.cpp) @@ -21,6 +24,7 @@ ament_target_dependencies(test_node rclcpp sensor_msgs cv_bridge + image_transport OpenCV ) diff --git a/image_preprocessing/src/test_node.cpp b/image_preprocessing/src/test_node.cpp index 3fbe9d3..68ea62d 100644 --- a/image_preprocessing/src/test_node.cpp +++ b/image_preprocessing/src/test_node.cpp @@ -1,51 +1,53 @@ #include "rclcpp/rclcpp.hpp" #include "sensor_msgs/msg/image.hpp" #include "cv_bridge/cv_bridge.h" -#include "sensor_msgs/image_encodings.hpp" -#include +#include "opencv2/opencv.hpp" class ImagePublisherNode : public rclcpp::Node { public: ImagePublisherNode() : Node("image_publisher_node") { - // Create a subscription to the "/color_camera/image_raw" topic + imagesubscription = create_subscription( "/color_camera/image_raw", 10, [this](const sensor_msgs::msg::Image::SharedPtr msg) { - // Callback function for the subscription - publishImage(msg); + imageCallback(msg); }); - // Create a publisher for the "img_pub" topic imagepublisher = create_publisher("img_pub", 10); - // Set the publishing rate to 10 Hz publishtimer = create_wall_timer(std::chrono::milliseconds(100), [this]() { - // Timer callback for publishing at 10 Hz - // You can perform any additional processing here if needed }); } private: - void publishImage(const sensor_msgs::msg::Image::SharedPtr msg) { - // Convert sensor_msgs::Image to cv::Mat using cv_bridge - cv_bridge::CvImageConstPtr cv_ptr; + void imageCallback(const sensor_msgs::msg::Image::SharedPtr msg) { + + cv_bridge::CvImagePtr cv_ptr; + try { cv_ptr = cv_bridge::toCvCopy(msg, sensor_msgs::image_encodings::BGR8); - } catch (cv_bridge::Exception& e) { - RCLCPP_ERROR(this->get_logger(), "cv_bridge exception: %s", e.what()); + } + catch(cv_bridge::Exception& e) { + RCLCPP_ERROR(get_logger(), "cv_bridge exception: %s", e.what()); return; } - // Add text to the image - cv::putText(cv_ptr->image, "Img Processed", cv::Point(10, 50), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(255, 255, 255), 2); + imageTranspose(cv_ptr->image); + } + + void imageTranspose(cv::Mat& image) { + cv::transpose(image, image); + publishImage(image); + } - // Publish the modified image - auto modified_msg = cv_bridge::CvImage(msg->header, "bgr8", cv_ptr->image).toImageMsg(); - imagepublisher->publish(modified_msg); + void publishImage(cv::Mat& image) { + output_msg = cv_bridge::CvImage(std_msgs::msg::Header(), "bgr8", image).toImageMsg(); + imagepublisher->publish(*output_msg.get()); } rclcpp::Subscription::SharedPtr imagesubscription; rclcpp::Publisher::SharedPtr imagepublisher; rclcpp::TimerBase::SharedPtr publishtimer; + sensor_msgs::msg::Image::SharedPtr output_msg; }; int main(int argc, char** argv) { @@ -53,4 +55,4 @@ int main(int argc, char** argv) { rclcpp::spin(std::make_shared()); rclcpp::shutdown(); return 0; -} \ No newline at end of file +} From deb97ebede7d68d4c9d696362dc1ebd92d13d80f Mon Sep 17 00:00:00 2001 From: deepansh Date: Sun, 17 Dec 2023 19:10:44 +0530 Subject: [PATCH 10/17] fixed YOLOv8.py to use teh DetectorBase class --- .../object_detection/Detectors/YOLOv8.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/object_detection/object_detection/Detectors/YOLOv8.py b/object_detection/object_detection/Detectors/YOLOv8.py index f76c201..8f59e9c 100755 --- a/object_detection/object_detection/Detectors/YOLOv8.py +++ b/object_detection/object_detection/Detectors/YOLOv8.py @@ -3,7 +3,10 @@ import os import time -class YOLOv8: +from ..DetectorBase import DetectorBase + + +class YOLOv8(DetectorBase): def __init__(self, model_dir_path, weight_file_name, conf_threshold = 0.7, score_threshold = 0.4, nms_threshold = 0.25, show_fps = 1, is_cuda = 0): @@ -48,16 +51,6 @@ def load_classes(self): return self.class_list - # create list of dictionary containing predictions - def create_predictions_list(self, class_ids, confidences, boxes): - - for i in range(len(class_ids)): - obj_dict = { - "class_id": class_ids[i], - "confidence": confidences[i], - "box": boxes[i] - } - self.predictions.append(obj_dict) def get_predictions(self, cv_image): @@ -81,7 +74,7 @@ def get_predictions(self, cv_image): confidence.append(box.conf) bb.append(box.xyxy) - self.create_predictions_list(class_id,confidence,bb) + super().create_predictions_list(class_id, confidence, bb) result = self.model.predict(self.frame, conf = self.conf_threshold) output_frame = result[0].plot() # Frame with bounding boxes From 6dbb0eebc9038667037d800765a303fbcb1ca61e Mon Sep 17 00:00:00 2001 From: deepansh Date: Sun, 17 Dec 2023 19:41:05 +0530 Subject: [PATCH 11/17] changed tab size in YOLOv8.py --- .../object_detection/Detectors/YOLOv8.py | 148 +++++++++--------- 1 file changed, 75 insertions(+), 73 deletions(-) diff --git a/object_detection/object_detection/Detectors/YOLOv8.py b/object_detection/object_detection/Detectors/YOLOv8.py index 8f59e9c..79a6d72 100755 --- a/object_detection/object_detection/Detectors/YOLOv8.py +++ b/object_detection/object_detection/Detectors/YOLOv8.py @@ -7,90 +7,92 @@ class YOLOv8(DetectorBase): - def __init__(self, model_dir_path, weight_file_name, conf_threshold = 0.7, - score_threshold = 0.4, nms_threshold = 0.25, - show_fps = 1, is_cuda = 0): + def __init__(self, model_dir_path, weight_file_name, conf_threshold = 0.7, + score_threshold = 0.4, nms_threshold = 0.25, + show_fps = 1, is_cuda = 0): + + super().__init__() + + self.model_dir_path = model_dir_path + self.weight_file_name = weight_file_name + + + self.conf_threshold = conf_threshold + self.show_fps = show_fps + self.is_cuda = is_cuda + + #FPS + if self.show_fps : + self.frame_count = 0 + self.total_frames = 0 + self.fps = -1 + self.start = time.time_ns() + self.frame = None + + + self.predictions = [] + self.build_model() + self.load_classes() + + + def build_model(self) : + + try : + model_path = os.path.join(self.model_dir_path, self.weight_file_name) + self.model = YOLO(model_path) + + except : + raise Exception("Error loading given model from path: {}. Maybe the file doesn't exist?".format(model_path)) - self.model_dir_path = model_dir_path - self.weight_file_name = weight_file_name + def load_classes(self): - - self.conf_threshold = conf_threshold - self.show_fps = show_fps - self.is_cuda = is_cuda + self.class_list = [] - #FPS - if self.show_fps : - self.frame_count = 0 - self.total_frames = 0 - self.fps = -1 - self.start = time.time_ns() - self.frame = None + with open(self.model_dir_path + "/classes.txt", "r") as f: + self.class_list = [cname.strip() for cname in f.readlines()] + return self.class_list - self.predictions = [] - self.build_model() - self.load_classes() + def get_predictions(self, cv_image): - def build_model(self) : + if cv_image is None: + # TODO: show warning message (different color, maybe) + return None,None + + else : + self.frame = cv_image + self.frame_count += 1 + self.total_frames += 1 - try : - model_path = os.path.join(self.model_dir_path, self.weight_file_name) - self.model = YOLO(model_path) - - except : - raise Exception("Error loading given model from path: {}. Maybe the file doesn't exist?".format(model_path)) - - def load_classes(self): + class_id = [] + confidence = [] + bb = [] + result = self.model.predict(self.frame, conf = self.conf_threshold) # Perform object detection on image + row = result[0].boxes - self.class_list = [] + for box in row: + class_id.append(box.cls) + confidence.append(box.conf) + bb.append(box.xyxy) - with open(self.model_dir_path + "/classes.txt", "r") as f: - self.class_list = [cname.strip() for cname in f.readlines()] + super().create_predictions_list(class_id, confidence, bb) + result = self.model.predict(self.frame, conf = self.conf_threshold) + output_frame = result[0].plot() # Frame with bounding boxes - return self.class_list + print("frame_count : ", self.frame_count) - def get_predictions(self, cv_image): + if self.show_fps : + if self.frame_count >= 30: + self.end = time.time_ns() + self.fps = 1000000000 * self.frame_count / (self.end - self.start) + self.frame_count = 0 + self.start = time.time_ns() - if cv_image is None: - # TODO: show warning message (different color, maybe) - return None,None - - else : - self.frame = cv_image - self.frame_count += 1 - self.total_frames += 1 - - class_id = [] - confidence = [] - bb = [] - result = self.model.predict(self.frame, conf = self.conf_threshold) # Perform object detection on image - row = result[0].boxes - - for box in row: - class_id.append(box.cls) - confidence.append(box.conf) - bb.append(box.xyxy) - - super().create_predictions_list(class_id, confidence, bb) - result = self.model.predict(self.frame, conf = self.conf_threshold) - output_frame = result[0].plot() # Frame with bounding boxes - - print("frame_count : ", self.frame_count) - - - if self.show_fps : - if self.frame_count >= 30: - self.end = time.time_ns() - self.fps = 1000000000 * self.frame_count / (self.end - self.start) - self.frame_count = 0 - self.start = time.time_ns() - - if self.fps > 0: - self.fps_label = "FPS: %.2f" % self.fps - cv2.putText(output_frame, self.fps_label, (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) - - return self.predictions, output_frame - \ No newline at end of file + if self.fps > 0: + self.fps_label = "FPS: %.2f" % self.fps + cv2.putText(output_frame, self.fps_label, (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) + + return self.predictions, output_frame + \ No newline at end of file From ae65ab7021690bb9d8803a593b0738a62b8cf9fb Mon Sep 17 00:00:00 2001 From: deepansh Date: Thu, 1 Feb 2024 22:43:44 +0530 Subject: [PATCH 12/17] fixed overload resolution issue --- object_detection/object_detection/ObjectDetection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/object_detection/object_detection/ObjectDetection.py b/object_detection/object_detection/ObjectDetection.py index 3bbf7ca..12d20a2 100644 --- a/object_detection/object_detection/ObjectDetection.py +++ b/object_detection/object_detection/ObjectDetection.py @@ -96,7 +96,7 @@ def detection_cb(self, img_msg): print("Image input from topic : {} is empty".format(self.input_img_topic)) else : for prediction in predictions: - left, top, width, height = prediction['box'] + left, top, width, height = map(int, prediction['box'][0]) right = left + width bottom = top + height From efb16f72432fc4db908daf98b1954108122f8829 Mon Sep 17 00:00:00 2001 From: Arjun K Haridas <51917087+topguns837@users.noreply.github.com> Date: Mon, 5 Feb 2024 01:06:39 +0530 Subject: [PATCH 13/17] Dockerized object_detection package (#32) * initialized docker file * git clone in dockerfile * git checkout to topguns/dockerfile added in dockerfile * replaced COPY with cp command * replaced ssh with https link of the repo * seperate RUN for each bash command * added / after mkdir commands * removed trailing && after RUN commands * correction in directory structure * cloning directly into topguns/dockerfile branch * removed copying docker scripts to container command * trying to install Python requirements using requirements.txt * changed matplotlib version to 3.7.2 in requirements.txt * conflicts in requirements.txt * changed numpy version in requirements.txt * enter_bash.sh working * testing base.sh and start.sh * added cv-bridge dependency * working on exposing ros2 topics inside the container to host machine * tested docker container, clip and ship * changed model dir path for docker container * cleaned Dockerfile * added requirement.txt, docker instructions * Initialized new Dockerfile with nvidia/cuda as base image * Changes in Readme * Minor changes to detector scripts * Docker container working * Added code to install nano in Dockerfile * Added arg in dockerfile to specify CUDA Version and added docker stop in run_devel.sh * Added feature to enter bash if container is running and remove the container after exit in run_devel. Changed tensorflow version to 2.14.0 * Added feature in run_devel to build docker image if it doesnt exist * Updated readme * Updated readme * correction in run_devel.sh * Attempting to build OpenCV from source * Added option to select base image in run_devel.sh * Env Variable for percep_ws * Updated readme * README.md changes * additions to README.md (#50) * fixed superimposed predictions on consecutive frames * Resolved the error : can't convert cuda:0 device type tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory * Added output class labels and confidences to output image, fixed issue with wrong sizes of bounding boxes * Removed code for building opencv from source from the dockerfiles * Code cleanup and fixed minor bugs * Code cleanup and fixed minor bugs * Changed model weight location --------- Co-authored-by: AaksharGarg Co-authored-by: deepansh Co-authored-by: Deepansh Goel <56270096+sudo-deep@users.noreply.github.com> Co-authored-by: topguns837 Co-authored-by: Jasmeet Singh --- .gitignore | 87 +++++++- Dockerfile.cuda | 80 +++++++ Dockerfile.ubuntu | 77 +++++++ README.md | 109 ++++++---- docker_scripts/ddsconfig.xml | 8 + docker_scripts/run_devel.sh | 72 +++++++ image_preprocessing/CMakeLists.txt | 43 ---- image_preprocessing/package.xml | 18 -- image_preprocessing/src/test_node.cpp | 58 ------ object_detection/config/params.yaml | 10 +- .../launch/object_detection.launch.py | 8 +- .../object_detection/DetectorBase.py | 1 + .../Detectors/EfficientDet.py | 196 ------------------ .../object_detection/Detectors/RetinaNet.py | 1 - .../object_detection/Detectors/YOLOv5.py | 7 +- .../object_detection/Detectors/YOLOv8.py | 123 ++++------- .../object_detection/ObjectDetection.py | 23 +- object_detection/package.xml | 2 +- object_detection/requirements.txt | 10 +- 19 files changed, 455 insertions(+), 478 deletions(-) create mode 100644 Dockerfile.cuda create mode 100644 Dockerfile.ubuntu create mode 100644 docker_scripts/ddsconfig.xml create mode 100755 docker_scripts/run_devel.sh delete mode 100644 image_preprocessing/CMakeLists.txt delete mode 100644 image_preprocessing/package.xml delete mode 100644 image_preprocessing/src/test_node.cpp delete mode 100644 object_detection/object_detection/Detectors/EfficientDet.py diff --git a/.gitignore b/.gitignore index 684bac1..9f2ae87 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,82 @@ -#Ignore the test node -/object_detection/object_detection/test_node.py +# Custom settings asset +*.settings.asset* -#Ignore pycache dirs -object_detection/object_detection/Detectors/__pycache__/ -object_detection/object_detection/__pycache__/ +# Visual Studio 2015 cache directory +/Project/.vs/ -#Ignore .vscode dir -.vscode \ No newline at end of file +/[Ll]ibrary/ +/[Tt]emp/ +/[Oo]bj/ +/[Bb]uild/ +/[Bb]uilds/ +/[Ll]ogs/ +/[Mm]emoryCaptures/ + +# Autogenerated VS/MD/Consulo solution and project files +*.csproj +*.unityproj +*.sln +*.suo +*.tmp +*.user +*.userprefs +*.pidb +*.booproj +*.svd +*.pdb + +# Unity3D generated meta files +*.pidb.meta + +# Builds +*.apk +*.unitypackage +*.app +*.exe +*.x86_64 +*.x86 + +# Generated doc folders +/docs/html + +# Mac hidden files +*.DS_Store +*/.ipynb_checkpoints +*/.idea +*.pyc +*.idea/misc.xml +*.idea/modules.xml +*.idea/ +*.iml +*.cache +*/build/ +*/dist/ +*.egg-info* +*.eggs* +*.gitignore.swp + +# VSCode hidden files +.vscode/ + +.DS_Store +.ipynb_checkpoints + +# pytest cache +*.pytest_cache/ + +# Ignore PyPi build files. +dist/ +build/ + +# Python virtual environment +venv/ +.mypy_cache/ + +# Code coverage report +.coverage +coverage.xml +/htmlcov/ + +**/UserSettings/* + +ROSConnectionPrefab.prefab \ No newline at end of file diff --git a/Dockerfile.cuda b/Dockerfile.cuda new file mode 100644 index 0000000..f784bd2 --- /dev/null +++ b/Dockerfile.cuda @@ -0,0 +1,80 @@ +# Use cuda_version arg to take CUDA version as input from user +ARG cuda_version=11.8.0 + +# Use NVIDA-CUDA's base image +FROM nvcr.io/nvidia/cuda:${cuda_version}-devel-ubuntu22.04 + +# Prevent console from interacting with the user +ARG DEBIAN_FRONTEND=noninteractive + +# Prevent hash mismatch error for apt-get update, qqq makes the terminal quiet while downloading pkgs +RUN apt-get clean && rm -rf /var/lib/apt/lists/* && apt-get update -yqqq + +# Set folder for RUNTIME_DIR. Only to prevent warnings when running RViz2 and Gz +RUN mkdir tmp/runtime-root && chmod 0700 tmp/runtime-root +ENV XDG_RUNTIME_DIR='/tmp/runtime-root' + +RUN apt-get update + +RUN apt-get install --no-install-recommends -yqqq \ + apt-utils \ + nano \ + git + +# Using shell to use bash commands like 'source' +SHELL ["/bin/bash", "-c"] + +# Python Dependencies +RUN apt-get install --no-install-recommends -yqqq \ + python3-pip + +# Add locale +RUN locale && \ + apt update && apt install --no-install-recommends -yqqq locales && \ + locale-gen en_US en_US.UTF-8 && \ + update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 && \ + export LANG=en_US.UTF-8 && \ + locale + +# Setup the sources +RUN apt-get update && apt-get install --no-install-recommends -yqqq software-properties-common curl && \ + add-apt-repository universe && \ + curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | tee /etc/apt/sources.list.d/ros2.list > /dev/nul + +# Install ROS 2 Humble +RUN apt update && apt install --no-install-recommends -yqqq \ + ros-humble-ros-base \ + ros-dev-tools + +# Install cv-bridge +RUN apt update && apt install --no-install-recommends -yqqq \ + ros-humble-cv-bridge + +# Target workspace for ROS2 packages +ARG WORKSPACE=/root/percep_ws + +# Add target workspace in environment +ENV WORKSPACE=$WORKSPACE + +# Creating the models folder +RUN mkdir -p $WORKSPACE/models && \ + mkdir -p $WORKSPACE/src + +# Installing Python dependencies +COPY object_detection/requirements.txt . +RUN pip3 install -r requirements.txt + +# ROS Dependencies +RUN apt update && apt install --no-install-recommends -yqqq \ + ros-humble-cyclonedds \ + ros-humble-rmw-cyclonedds-cpp + +# Use cyclone DDS by default +ENV RMW_IMPLEMENTATION rmw_cyclonedds_cpp + +WORKDIR /root/percep_ws + +# Update .bashrc +RUN echo "source /opt/ros/humble/setup.bash" >> /root/.bashrc + diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu new file mode 100644 index 0000000..2a05f69 --- /dev/null +++ b/Dockerfile.ubuntu @@ -0,0 +1,77 @@ +# Use Ubuntu's base image +FROM ubuntu:22.04 + +# Prevent console from interacting with the user +ARG DEBIAN_FRONTEND=noninteractive + +# Prevent hash mismatch error for apt-get update, qqq makes the terminal quiet while downloading pkgs +RUN apt-get clean && rm -rf /var/lib/apt/lists/* && apt-get update -yqqq + +# Set folder for RUNTIME_DIR. Only to prevent warnings when running RViz2 and Gz +RUN mkdir tmp/runtime-root && chmod 0700 tmp/runtime-root +ENV XDG_RUNTIME_DIR='/tmp/runtime-root' + +RUN apt-get update + +RUN apt-get install --no-install-recommends -yqqq \ + apt-utils \ + nano \ + git + +# Using shell to use bash commands like 'source' +SHELL ["/bin/bash", "-c"] + +# Python Dependencies +RUN apt-get install --no-install-recommends -yqqq \ + python3-pip + +# Add locale +RUN locale && \ + apt update && apt install --no-install-recommends -yqqq locales && \ + locale-gen en_US en_US.UTF-8 && \ + update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 && \ + export LANG=en_US.UTF-8 && \ + locale + +# Setup the sources +RUN apt-get update && apt-get install --no-install-recommends -yqqq software-properties-common curl && \ + add-apt-repository universe && \ + curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | tee /etc/apt/sources.list.d/ros2.list > /dev/nul + +# Install ROS 2 Humble +RUN apt update && apt install --no-install-recommends -yqqq \ + ros-humble-ros-base \ + ros-dev-tools + +# Install cv-bridge +RUN apt update && apt install --no-install-recommends -yqqq \ + ros-humble-cv-bridge + +# Target workspace for ROS2 packages +ARG WORKSPACE=/root/percep_ws + +# Add target workspace in environment +ENV WORKSPACE=$WORKSPACE + +# Creating the models folder +RUN mkdir -p $WORKSPACE/models && \ + mkdir -p $WORKSPACE/src + +# Installing Python dependencies +COPY object_detection/requirements.txt . +RUN pip3 install -r requirements.txt + +# ROS Dependencies +RUN apt update && apt install --no-install-recommends -yqqq \ + ros-humble-cyclonedds \ + ros-humble-rmw-cyclonedds-cpp + +# Use cyclone DDS by default +ENV RMW_IMPLEMENTATION rmw_cyclonedds_cpp + +WORKDIR /root/percep_ws + +# Update .bashrc +RUN echo "source /opt/ros/humble/setup.bash" >> /root/.bashrc + diff --git a/README.md b/README.md index 3f1fea6..448d24a 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,19 @@ These components can be stitched together to make a custom pipeline for any use- Follow these steps to setup this project on your systm ### Prerequisites +Install the binary Gazebo Garden/ROS 2 Humble packages: + +* Follow [these](https://gazebosim.org/docs/garden/install_ubuntu#binary-installation-on-ubuntu) instructions to install gz-garden from packages.osrfoundation.org repository. +* Install ros_gz + * From the non official binary packages from apt: + + * ```apt-get install ros-humble-ros-gzgarden``` + * Build from source: + * Refer to the [official ros_gz repository](https://github.com/gazebosim/ros_gz/tree/humble#from-source) + +Install docker and add it to user group: + +* Refer to this [link](https://cloudyuga.guru/hands_on_lab/docker-as-non-root-user) Follow these steps to install ROS Humble and OpenCV * ROS Humble @@ -116,45 +129,54 @@ Refer to the official [ROS 2 installation guide](https://docs.ros.org/en/humble/ ### Installation -1. Make a new workspace +1. **Run using Docker** + ```bash - mkdir -p percep_ws/src + cd docker_scripts + export PERCEP_WS_PATH= + ./run_devel.sh ``` -2. Clone the ROS-Perception-Pipeline repository - - Now go ahead and clone this repository inside the "src" folder of the workspace you just created. - - ```bash - cd percep_ws && git clone git@github.com:atom-robotics-lab/ros-perception-pipeline.git src/ - ``` -3. Install dependencies using rosdep - - Update Your rosdep before installation. - - ```bash - rosdep update - ``` +2. **Run natively** + + 1. Make a new workspace + ```bash + mkdir -p percep_ws/src + ``` + + 2. Clone the ROS-Perception-Pipeline repository + + Now go ahead and clone this repository inside the "src" folder of the workspace you just created. + + ```bash + cd percep_ws/src && git clone git@github.com:atom-robotics-lab/ros-perception-pipeline.git + ``` + 3. Install dependencies using rosdep + + Update Your rosdep before installation. + + ```bash + rosdep update + ``` + + This command installs all the packages that the packages in your catkin workspace depend upon but are missing on your computer. + ```bash + rosdep install --from-paths src --ignore-src -r -y + ``` + + 4. Compile the package + + Follow this execution to compile your ROS 2 package - This command installs all the packages that the packages in your catkin workspace depend upon but are missing on your computer. - ```bash - rosdep install --from-paths src --ignore-src -r -y - ``` - -4. Compile the package - - Follow this execution to compile your ROS 2 package - - ```bash - colcon build --symlink-install - ``` - -5. Source your workspace - - ```bash - source install/local_setup.bash - ``` - + ```bash + colcon build --symlink-install + ``` + + 5. Source your workspace + + ```bash + source install/local_setup.bash + ``` @@ -178,7 +200,7 @@ Don't forget to click on the **play** button on the bottom left corner of the Ig
-### 2. Launch the Object Detection node +### 2.1 Launch the Object Detection node natively
Use the pip install command as shown below to install the required packages. @@ -190,7 +212,7 @@ Use the command given below to run the ObjectDetection node. Remember to change file according to your present working directory ```bash -ros2 run object_detection ObjectDetection --ros-args --params-file src/ros-perception-pipeline/object_detection/config/object_detection.yaml +ros2 launch object_detection object_detection.launch.py ``` **Note :** If your imports are not working while using a virtual environment, you'll need to manually set your `PYTHONPATH` environment variable. @@ -208,6 +230,16 @@ Follow these steps to do this : export PYTHONPATH = {insert_your_python_path_here} ``` +### 2.2 Launch the Object Detection node using Docker +
+ +We can use the Docker image built previously to run the `object_detection` package + + ```bash + colcon build --symlink-install + source install.setup.bash + ros2 launch object_detection object_detection.launch.py + ``` ### 3. Changing the Detector @@ -228,8 +260,7 @@ ros2 run rqt_image_view rqt_image_view
-

(back to top)

- +

(back to top)

+ + diff --git a/docker_scripts/run_devel.sh b/docker_scripts/run_devel.sh new file mode 100755 index 0000000..95b35a8 --- /dev/null +++ b/docker_scripts/run_devel.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +xhost +local:root + +IMAGE_NAME="object_detection" +IMAGE_TAG="latest" +CONTAINER_NAME="object_detection" + +# Build the image if it doesn't exist +if docker inspect "$IMAGE_NAME:$IMAGE_TAG" &> /dev/null; then + echo "The image $IMAGE_NAME:$IMAGE_TAG exists." + +else + echo "The image $IMAGE_NAME:$IMAGE_TAG does not exist. Building the image...." + + echo "Choose the base image:" + echo "1. NVIDIA CUDA image" + echo "2. Ubuntu 22.04 image" + read -p "Enter your choice (1 or 2): " base_image_choice + + # If the user input is blank or not 1 or 2, default to NVIDIA CUDA image + if [ -z "$base_image_choice" ] || [ "$base_image_choice" != "1" ] && [ "$base_image_choice" != "2" ]; then + base_image_choice="1" + fi + + # Choose the appropriate Dockerfile based on user input + if [ "$base_image_choice" == "1" ]; then + DOCKERFILE="Dockerfile.cuda" + + echo "Enter your preferred CUDA Version (default set to 11.8.0) : " + read cuda_version + + # If the user input is blank, use 11.8.0 as the cuda_version + if [ -z "$cuda_version" ]; then + cuda_version="11.8.0" + fi + + cd .. + docker build --build-arg cuda_version="$cuda_version" -f "$DOCKERFILE" -t "$IMAGE_NAME:$IMAGE_TAG" . + echo "Completed building the docker image" + else + DOCKERFILE="Dockerfile.ubuntu" + + cd .. + docker build -f "$DOCKERFILE" -t "$IMAGE_NAME:$IMAGE_TAG" . + fi +fi + +# Enter into the container if it is already running +if [ "$(docker ps -a --quiet --filter status=running --filter name=$CONTAINER_NAME)" ]; then + echo -e "\nAttaching to running container: $CONTAINER_NAME" + docker exec -it $CONTAINER_NAME /bin/bash $@ + exit 0 +fi + +# Check if the PERCEP_WS_PATH environment variable is empty +if [ -z "$PERCEP_WS_PATH" ]; then + echo -e "\nThe environment variable : PERCEP_WS_PATH is empty. Point it to the path of the ROS 2 workspace in which the ros-perception-pipeline project is kept !!" + exit 1 +fi + +# Run the docker container +docker run --gpus all --shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 \ +-it --rm --privileged --net=host --ipc=host \ +--name $CONTAINER_NAME \ +-v $PERCEP_WS_PATH/src/:/root/percep_ws/src \ +-v $PERCEP_WS_PATH/models/:/root/percep_ws/models/ \ +-v ddsconfig.xml:/ddsconfig.xml \ +--env CYCLONEDDS_URI=/ddsconfig.xml \ +--env="QT_X11_NO_MITSHM=1" \ +--env="DISPLAY" \ +object_detection:latest diff --git a/image_preprocessing/CMakeLists.txt b/image_preprocessing/CMakeLists.txt deleted file mode 100644 index fa3edfa..0000000 --- a/image_preprocessing/CMakeLists.txt +++ /dev/null @@ -1,43 +0,0 @@ -cmake_minimum_required(VERSION 3.5) - -project(image_preprocessing) - -# Find dependencies -find_package(ament_cmake REQUIRED) -find_package(rclcpp REQUIRED) -find_package(sensor_msgs REQUIRED) -find_package(cv_bridge REQUIRED) -find_package(image_transport REQUIRED) -find_package(OpenCV REQUIRED) - -# Add executable -add_executable(test_node src/test_node.cpp) - -# Include directories for the executable -target_include_directories(test_node - PRIVATE - ${sensor_msgs_INCLUDE_DIRS} -) - -# Link the executable to the required libraries -ament_target_dependencies(test_node - rclcpp - sensor_msgs - cv_bridge - image_transport - OpenCV -) - -# Install the executable -install(TARGETS - test_node - DESTINATION lib/${PROJECT_NAME} -) - -# Install launch files, config files, and other directories if necessary -# install(DIRECTORY launch -# DESTINATION share/${PROJECT_NAME} -# ) - -# Install the CMake package file -ament_package() diff --git a/image_preprocessing/package.xml b/image_preprocessing/package.xml deleted file mode 100644 index 0ad68f3..0000000 --- a/image_preprocessing/package.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - image_preprocessing - 0.0.0 - TODO: Package description - rachit - TODO: License declaration - - ament_cmake - - ament_lint_auto - ament_lint_common - - - ament_cmake - - diff --git a/image_preprocessing/src/test_node.cpp b/image_preprocessing/src/test_node.cpp deleted file mode 100644 index 68ea62d..0000000 --- a/image_preprocessing/src/test_node.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include "rclcpp/rclcpp.hpp" -#include "sensor_msgs/msg/image.hpp" -#include "cv_bridge/cv_bridge.h" -#include "opencv2/opencv.hpp" - -class ImagePublisherNode : public rclcpp::Node { -public: - ImagePublisherNode() : Node("image_publisher_node") { - - imagesubscription = create_subscription( - "/color_camera/image_raw", 10, [this](const sensor_msgs::msg::Image::SharedPtr msg) { - imageCallback(msg); - }); - - imagepublisher = create_publisher("img_pub", 10); - - publishtimer = create_wall_timer(std::chrono::milliseconds(100), [this]() { - }); - } - -private: - void imageCallback(const sensor_msgs::msg::Image::SharedPtr msg) { - - cv_bridge::CvImagePtr cv_ptr; - - try { - cv_ptr = cv_bridge::toCvCopy(msg, sensor_msgs::image_encodings::BGR8); - } - catch(cv_bridge::Exception& e) { - RCLCPP_ERROR(get_logger(), "cv_bridge exception: %s", e.what()); - return; - } - - imageTranspose(cv_ptr->image); - } - - void imageTranspose(cv::Mat& image) { - cv::transpose(image, image); - publishImage(image); - } - - void publishImage(cv::Mat& image) { - output_msg = cv_bridge::CvImage(std_msgs::msg::Header(), "bgr8", image).toImageMsg(); - imagepublisher->publish(*output_msg.get()); - } - - rclcpp::Subscription::SharedPtr imagesubscription; - rclcpp::Publisher::SharedPtr imagepublisher; - rclcpp::TimerBase::SharedPtr publishtimer; - sensor_msgs::msg::Image::SharedPtr output_msg; -}; - -int main(int argc, char** argv) { - rclcpp::init(argc, argv); - rclcpp::spin(std::make_shared()); - rclcpp::shutdown(); - return 0; -} diff --git a/object_detection/config/params.yaml b/object_detection/config/params.yaml index 94d7f1a..eb69547 100644 --- a/object_detection/config/params.yaml +++ b/object_detection/config/params.yaml @@ -1,12 +1,12 @@ object_detection: ros__parameters: - input_img_topic: color_camera/image_raw + input_img_topic: /kitti/camera/color/left/image_raw output_bb_topic: object_detection/img_bb output_img_topic: object_detection/img publish_output_img: 1 model_params: - detector_type: RetinaNet - model_dir_path: models/ - weight_file_name: resnet50_coco_best_v2.1.0.h5 - confidence_threshold : 0.7 + detector_type: YOLOv5 + model_dir_path: /root/percep_ws/models/yolov5 + weight_file_name: yolov5.onnx + confidence_threshold : 0.5 show_fps : 1 \ No newline at end of file diff --git a/object_detection/launch/object_detection.launch.py b/object_detection/launch/object_detection.launch.py index f1f1980..5d7c830 100644 --- a/object_detection/launch/object_detection.launch.py +++ b/object_detection/launch/object_detection.launch.py @@ -13,14 +13,9 @@ # limitations under the License. import os -import sys from ament_index_python.packages import get_package_share_directory - from launch import LaunchDescription -from launch.actions import IncludeLaunchDescription, DeclareLaunchArgument -from launch.substitutions import LaunchConfiguration -from launch.launch_description_sources import PythonLaunchDescriptionSource from launch_ros.actions import Node @@ -38,7 +33,8 @@ def generate_launch_description(): name = 'object_detection', executable = 'ObjectDetection', parameters = [params], - output="screen" + output="screen", + emulate_tty = True ) diff --git a/object_detection/object_detection/DetectorBase.py b/object_detection/object_detection/DetectorBase.py index 801b5cc..7ffe56d 100644 --- a/object_detection/object_detection/DetectorBase.py +++ b/object_detection/object_detection/DetectorBase.py @@ -8,6 +8,7 @@ def __init__(self) -> None: self.predictions = [] def create_predictions_list(self, class_ids, confidences, boxes): + self.predictions = [] for i in range(len(class_ids)): obj_dict = { "class_id": class_ids[i], diff --git a/object_detection/object_detection/Detectors/EfficientDet.py b/object_detection/object_detection/Detectors/EfficientDet.py deleted file mode 100644 index e5a5f49..0000000 --- a/object_detection/object_detection/Detectors/EfficientDet.py +++ /dev/null @@ -1,196 +0,0 @@ -import tensorflow_hub as hub -import cv2 -import numpy -import pandas as pd -import tensorflow as tf -import matplotlib.pyplot as plt - -import tempfile - - -# For drawing onto the image. -import numpy as np -from PIL import Image -from PIL import ImageColor -from PIL import ImageDraw -from PIL import ImageFont -from PIL import ImageOps - -# For measuring the inference time. -import time - -class EfficientDet: - def __init__(self, model_dir_path, weight_file_name, conf_threshold = 0.7, score_threshold = 0.25, nms_threshold = 0.4): - - self.frame_count = 0 - self.total_frames = 0 - self.fps = -1 - self.start = time.time_ns() - self.frame = None - - self.model_dir_path = model_dir_path - self.weight_file_name = weight_file_name - self.conf=conf_threshold - - # Resizing image - self.img_height=800 - self.img_width=800 - self.predictions=[] - - self.build_model() - self.load_classes() - - def build_model(self) : - module_handle="https://tfhub.dev/tensorflow/efficientdet/d0/1" - # Loading model directly from TensorFlow Hub - self.detector = hub.load(module_handle) - - - def load_classes(self): - self.labels = [] - with open(self.model_dir_path + "/classes.txt", "r") as f: - self.labels = [cname.strip() for cname in f.readlines()] - return self.labels - - def display_image(self,image): - cv2.imshow("result", image) - cv2.waitKey(0) - cv2.destroyAllWindows() - - def draw_bounding_box_on_image(self,image,ymin,xmin,ymax,xmax,color,font,thickness=4,display_str_list=()): - """Adds a bounding box to an image.""" - draw = ImageDraw.Draw(image) - im_width, im_height = image.size - (left, right, top, bottom) = (xmin * im_width, xmax * im_width, - ymin * im_height, ymax * im_height) - draw.line([(left, top), (left, bottom), (right, bottom), (right, top), - (left, top)], - width=thickness, - fill=color) - # If the total height of the display strings added to the top of the bounding - # box exceeds the top of the image, stack the strings below the bounding box - # instead of above. - display_str_heights = [font.getsize(ds)[1] for ds in display_str_list] - # Each display_str has a top and bottom margin of 0.05x. - total_display_str_height = (1 + 2 * 0.05) * sum(display_str_heights) - - if top > total_display_str_height: - text_bottom = top - else: - text_bottom = top + total_display_str_height - # Reverse list and print from bottom to top. - for display_str in display_str_list[::-1]: - text_width, text_height = font.getsize(display_str) - margin = np.ceil(0.05 * text_height) - draw.rectangle([(left, text_bottom - text_height - 2 * margin), - (left + text_width, text_bottom)], - fill=color) - draw.text((left + margin, text_bottom - text_height - margin), - display_str, - fill="black", - font=font) - text_bottom -= text_height - 2 * margin - - # create list of dictionary containing predictions - def create_predictions_list(self, class_ids, confidences, boxes): - for i in range(len(class_ids)): - obj_dict = { - "class_id": class_ids[i], - "confidence": confidences[i], - "box": boxes[i] - } - self.predictions.append(obj_dict) - - def draw_boxes(self,image,boxes,class_ids,confidences,max_boxes=10): - """Overlay labeled boxes on an image with formatted scores and label names.""" - colors = list(ImageColor.colormap.values()) - - try: - font = ImageFont.truetype("/usr/share/fonts/truetype/liberation/LiberationSansNarrow-Regular.ttf", - 25) - except IOError: - print("Font not found, using default font.") - font = ImageFont.load_default() - - for i in range(min(boxes.shape[0], max_boxes)): - if confidences[i] >= self.conf: - ymin, xmin, ymax, xmax = tuple(boxes[i]) - display_str = "{}: {}%".format(self.labels[class_ids[i]], int(100 * confidences[i])) - color = colors[hash(class_ids[i]) % len(colors)] - image_pil = Image.fromarray(np.uint8(image)).convert("RGB") - self.draw_bounding_box_on_image(image_pil,ymin,xmin,ymax,xmax,color,font,display_str_list=[display_str]) - np.copyto(image, np.array(image_pil)) - return image - - def load_img(self,path): - img = tf.io.read_file(path) - img = tf.image.decode_jpeg(img, channels=3) - return img - - def get_predictions(self,cv_image): - - if cv_image is None: - # TODO: show warning message (different color, maybe) - return None,None - - else : - #Convert img to RGB - self.frame = cv_image - - self.frame_count += 1 - self.total_frames += 1 - - rgb = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB) - - # COnverting to uint8 - rgb_tensor = tf.convert_to_tensor(rgb, dtype=tf.uint8) - - #Add dims to rgb_tensor - rgb_tensor = tf.expand_dims(rgb_tensor , 0) - - - result = self.detector(rgb_tensor) - result = {key:value.numpy() for key,value in result.items()} - - self.create_predictions_list(result["detection_boxes"][0],result["detection_classes"][0], result["detection_scores"][0]) - image_with_boxes = self.draw_boxes(cv_image,result["detection_boxes"][0],result["detection_classes"][0], result["detection_scores"][0]) - - # fps - if self.frame_count >= 30: - self.end = time.time_ns() - self.fps = 1000000000 * self.frame_count / (self.end - self.start) - self.frame_count = 0 - self.start = time.time_ns() - - if self.fps > 0: - self.fps_label = "FPS: %.2f" % self.fps - cv2.putText(self.frame, self.fps_label, (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) - - return [self.predictions, image_with_boxes] - - - def detect_img(self,image_url): - start_time = time.time() - self.run_detector(self.detector, image_url)#Convert img to RGB - rgb = cv2.cvtColor(cv_image, cv2.COLOR_BGR2RGB) - # COnverting to uint8 - rgb_tensor = tf.convert_to_tensor(rgb, dtype=tf.uint8) - #Add dims to rgb_tensor - rgb_tensor = tf.expand_dims(rgb_tensor , 0) - start_time = time.time() - result = self.detector(rgb_tensor) - end_time = time.time() - result = {key:value.numpy() for key,value in result.items()} - print("Found %d objects." % len(result["detection_scores"])) - print("Inference time: ", end_time-start_time) - self.create_predictions_list(cv_image,result["detection_boxes"][0],result["detection_classes"][0], result["detection_scores"][0]) - image_with_boxes = self.draw_boxes(cv_image,result["detection_boxes"][0],result["detection_classes"][0], result["detection_scores"][0]) - self.display_image(self.predictions,image_with_boxes) - - end_time = time.time() - print("Inference time:",end_time-start_time) - -if __name__=='__main__': - # Load model - det = EfficientDet() - det.detect_img("/home/sanchay/yolo_catkin/src/yolov8_test/scripts/dog_cat.jpg") \ No newline at end of file diff --git a/object_detection/object_detection/Detectors/RetinaNet.py b/object_detection/object_detection/Detectors/RetinaNet.py index eb04a4c..8c1ddb3 100755 --- a/object_detection/object_detection/Detectors/RetinaNet.py +++ b/object_detection/object_detection/Detectors/RetinaNet.py @@ -62,4 +62,3 @@ def get_predictions(self, cv_image) : return self.predictions - diff --git a/object_detection/object_detection/Detectors/YOLOv5.py b/object_detection/object_detection/Detectors/YOLOv5.py index 571f3d0..4363c33 100644 --- a/object_detection/object_detection/Detectors/YOLOv5.py +++ b/object_detection/object_detection/Detectors/YOLOv5.py @@ -1,4 +1,3 @@ -import time import os import cv2 import numpy as np @@ -7,7 +6,7 @@ class YOLOv5(DetectorBase): - def __init__(self, conf_threshold = 0.7, score_threshold = 0.4, nms_threshold = 0.25, is_cuda = 0): + def __init__(self, conf_threshold = 0.7, score_threshold = 0.4, nms_threshold = 0.25, is_cuda = 1): super().__init__() @@ -144,8 +143,6 @@ def get_predictions(self, cv_image): outs = self.detect(inputImage) class_ids, confidences, boxes = self.wrap_detection(inputImage, outs[0]) - super().create_predictions_list(class_ids, confidences, boxes) - - print("Detected ids: ", class_ids) + super().create_predictions_list(class_ids, confidences, boxes) return self.predictions \ No newline at end of file diff --git a/object_detection/object_detection/Detectors/YOLOv8.py b/object_detection/object_detection/Detectors/YOLOv8.py index 79a6d72..e1f7d59 100755 --- a/object_detection/object_detection/Detectors/YOLOv8.py +++ b/object_detection/object_detection/Detectors/YOLOv8.py @@ -1,98 +1,55 @@ -import cv2 from ultralytics import YOLO import os -import time from ..DetectorBase import DetectorBase class YOLOv8(DetectorBase): - def __init__(self, model_dir_path, weight_file_name, conf_threshold = 0.7, - score_threshold = 0.4, nms_threshold = 0.25, - show_fps = 1, is_cuda = 0): - - super().__init__() - - self.model_dir_path = model_dir_path - self.weight_file_name = weight_file_name - - - self.conf_threshold = conf_threshold - self.show_fps = show_fps - self.is_cuda = is_cuda - - #FPS - if self.show_fps : - self.frame_count = 0 - self.total_frames = 0 - self.fps = -1 - self.start = time.time_ns() - self.frame = None - - - self.predictions = [] - self.build_model() - self.load_classes() - - - def build_model(self) : - - try : - model_path = os.path.join(self.model_dir_path, self.weight_file_name) - self.model = YOLO(model_path) - - except : - raise Exception("Error loading given model from path: {}. Maybe the file doesn't exist?".format(model_path)) + def __init__(self, conf_threshold = 0.7): - def load_classes(self): - - self.class_list = [] - - with open(self.model_dir_path + "/classes.txt", "r") as f: - self.class_list = [cname.strip() for cname in f.readlines()] - - return self.class_list - - - def get_predictions(self, cv_image): - - if cv_image is None: - # TODO: show warning message (different color, maybe) - return None,None - - else : - self.frame = cv_image - self.frame_count += 1 - self.total_frames += 1 - - class_id = [] - confidence = [] - bb = [] - result = self.model.predict(self.frame, conf = self.conf_threshold) # Perform object detection on image - row = result[0].boxes + super().__init__() + + self.conf_threshold = conf_threshold - for box in row: - class_id.append(box.cls) - confidence.append(box.conf) - bb.append(box.xyxy) + def build_model(self, model_dir_path, weight_file_name) : - super().create_predictions_list(class_id, confidence, bb) - result = self.model.predict(self.frame, conf = self.conf_threshold) - output_frame = result[0].plot() # Frame with bounding boxes + try : + model_path = os.path.join(model_dir_path, weight_file_name) + self.model = YOLO(model_path) + + except : + raise Exception("Error loading given model from path: {}. Maybe the file doesn't exist?".format(model_path)) + - print("frame_count : ", self.frame_count) + def load_classes(self, model_dir_path): + self.class_list = [] - if self.show_fps : - if self.frame_count >= 30: - self.end = time.time_ns() - self.fps = 1000000000 * self.frame_count / (self.end - self.start) - self.frame_count = 0 - self.start = time.time_ns() + with open(model_dir_path + "/classes.txt", "r") as f: + self.class_list = [cname.strip() for cname in f.readlines()] - if self.fps > 0: - self.fps_label = "FPS: %.2f" % self.fps - cv2.putText(output_frame, self.fps_label, (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) + return self.class_list - return self.predictions, output_frame - \ No newline at end of file + def get_predictions(self, cv_image): + + if cv_image is None: + # TODO: show warning message (different color, maybe) + return None,None + + else : + self.frame = cv_image + class_id = [] + confidence = [] + boxes = [] + + result = self.model.predict(self.frame, conf = self.conf_threshold, verbose = False) # Perform object detection on image + row = result[0].boxes.cpu() + + for box in row: + class_id.append(box.cls.numpy().tolist()[0]) + confidence.append(box.conf.numpy().tolist()[0]) + boxes.append(box.xyxy.numpy().tolist()[0]) + + super().create_predictions_list(class_id, confidence, boxes) + + return self.predictions \ No newline at end of file diff --git a/object_detection/object_detection/ObjectDetection.py b/object_detection/object_detection/ObjectDetection.py index 12d20a2..72dac99 100644 --- a/object_detection/object_detection/ObjectDetection.py +++ b/object_detection/object_detection/ObjectDetection.py @@ -80,7 +80,7 @@ def load_detector(self): detector_mod = importlib.import_module(".Detectors." + self.detector_type, "object_detection") detector_class = getattr(detector_mod, self.detector_type) self.detector = detector_class() - + self.detector.build_model(self.model_dir_path, self.weight_file_name) self.detector.load_classes(self.model_dir_path) @@ -91,21 +91,25 @@ def detection_cb(self, img_msg): cv_image = self.bridge.imgmsg_to_cv2(img_msg, "bgr8") predictions = self.detector.get_predictions(cv_image=cv_image) - + if predictions == None : print("Image input from topic : {} is empty".format(self.input_img_topic)) else : for prediction in predictions: - left, top, width, height = map(int, prediction['box'][0]) - right = left + width - bottom = top + height + x1, y1, x2, y2 = map(int, prediction['box']) + + # Draw the bounding box + cv_image = cv2.rectangle(cv_image, (x1, y1), (x2, y2), (0,255,0),1) - #Draw the bounding box - cv_image = cv2.rectangle(cv_image,(left,top),(right, bottom),(0,255,0),1) + # Show names of classes on the output image + class_id = int(prediction['class_id']) + class_name = self.detector.class_list[class_id] + label = f"{class_name} : {prediction['confidence']:.2f}" + + cv_image = cv2.putText(cv_image, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) output = self.bridge.cv2_to_imgmsg(cv_image, "bgr8") self.img_pub.publish(output) - print(predictions) def main(): @@ -120,6 +124,3 @@ def main(): if __name__=="__main__" : main() - - - diff --git a/object_detection/package.xml b/object_detection/package.xml index 482516c..fc13de3 100644 --- a/object_detection/package.xml +++ b/object_detection/package.xml @@ -3,7 +3,7 @@ object_detection 0.0.0 - TODO: Package description + This is a ROS 2 package aimed at providing an interfacing between Deep Learning models and the ROS architecture using a plug-and-play modular arhitecture singh Apache 2.0 diff --git a/object_detection/requirements.txt b/object_detection/requirements.txt index 71ac88e..06197c0 100644 --- a/object_detection/requirements.txt +++ b/object_detection/requirements.txt @@ -1,9 +1,9 @@ +tensorflow==2.14.0 +tensorflow-hub==0.13.0 keras-retinanet==1.0.0 -matplotlib==3.5.4 -numpy==1.25.0 +matplotlib==3.7.2 +numpy==1.26.1 opencv-python==4.7.0.72 pandas==2.0.3 pillow==9.5.0 -tensorflow==2.12.0 -tensorflow-hub==0.13.0 -ultralytics==8.0.124 \ No newline at end of file +ultralytics==8.0.124 From 9eb121ab49979c4eab5bd2c1c03f59eda8804ca7 Mon Sep 17 00:00:00 2001 From: Jasmeet Singh Date: Fri, 9 Feb 2024 01:36:52 +0530 Subject: [PATCH 14/17] Added Build and Test Workflows (#58) * Added build and test workflow * Corrected IMAGE_NAME varibles in run_devel.sh script * Corrected pull_requests trigger name * Removed -it option from docker run & added docker attach step * Removed PERCEP_WS_PATH environment variable and used github.workspace instead * Corrected container name in docker run * Corrected volume mount path * Removed not required arguments from docker run * Removed docker attach & used docker exec for colcon build * Added | character in run for multi-line command * Added container_name environment variable * Added jobs for lint and test * Updated linting job, added matrix for linter * Added package name field to lint job * Removed pep8 and mypy linter from action workflows * Added copyright notice, corrected according to lint * Updated worflow to pass arguments for linter, made some corrections * Removed arguments from workflow * Updated flake8 test to use setup.cfg as the config * Updated workflow to pass config argument to flake8 lint * Fixed variable name typo * Corrected variable used for config path * Updated lin job to setup ros humble * Added ros distribution field for lint steps * Updated flake8 config from default ament_flake8 config * Added project wide flake8 config file, some more lint corrections --- .flake8 | 6 + .github/workflows/build_and_test.yml | 65 +++++++++++ docker_scripts/run_devel.sh | 6 +- .../launch/object_detection.launch.py | 19 ++- .../object_detection/DetectorBase.py | 19 ++- .../object_detection/Detectors/RetinaNet.py | 67 ++++++----- .../object_detection/Detectors/YOLOv5.py | 62 ++++++---- .../object_detection/Detectors/YOLOv8.py | 109 ++++++++++-------- .../object_detection/ObjectDetection.py | 86 ++++++++------ object_detection/setup.cfg | 6 + object_detection/setup.py | 5 +- object_detection/test/test_flake8.py | 9 +- .../launch/playground.launch.py | 92 +++++++-------- 13 files changed, 346 insertions(+), 205 deletions(-) create mode 100644 .flake8 create mode 100644 .github/workflows/build_and_test.yml diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..9b0d3a7 --- /dev/null +++ b/.flake8 @@ -0,0 +1,6 @@ +[flake8] +extend-ignore = B902,C816,D100,D101,D102,D103,D104,D105,D106,D107,D203,D212,D404,I202,Q000 +import-order-style = google +max-line-length = 125 +show-source = true +statistics = true \ No newline at end of file diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml new file mode 100644 index 0000000..8bea564 --- /dev/null +++ b/.github/workflows/build_and_test.yml @@ -0,0 +1,65 @@ +name: Build and Test Workflows + +on: + pull_request: + branches: + - main + - develop + push: + branches: + - main + +jobs: + ament_lint: + name: Lint and Codecheck + runs-on: ubuntu-latest + steps: + - name: Code Checkout + uses: actions/checkout@v4 + - name: Setup ROS + uses: ros-tooling/setup-ros@master + - name: Ament Copyright Lint + uses: ros-tooling/action-ros-lint@master + with: + distribution: humble + linter: copyright + package-name: "*" + - name: Ament Flake8 Lint + uses: ros-tooling/action-ros-lint@master + with: + distribution: humble + linter: flake8 + package-name: "*" + arguments: '--config=${{ github.workspace }}/.flake8' + - name: Ament PEP257 Lint + uses: ros-tooling/action-ros-lint@master + with: + distribution: humble + linter: pep257 + package-name: "*" + - name: Ament xmllint Lint + uses: ros-tooling/action-ros-lint@master + with: + distribution: humble + linter: xmllint + package-name: "*" + + build_source: + name: Build Docker Image and ROS 2 Packages + runs-on: ubuntu-latest + env: + CONTAINER_NAME: perception_pipeline + PERCEP_WS: /root/percep_ws + steps: + - name: Code Checkout + uses: actions/checkout@v4 + - name: Build Docker Image + run: docker build . --file Dockerfile.ubuntu -t ${{ github.repository }}:latest + - name: Run Docker Image + run: | + docker run -it -d --name $CONTAINER_NAME \ + -v ${{ github.workspace }}:$PERCEP_WS/src \ + ${{ github.repository }}:latest + - name: Build ROS 2 Packages in Container + run: docker exec $CONTAINER_NAME bash -c \ + "source /opt/ros/humble/setup.bash && cd $PERCEP_WS && colcon build" diff --git a/docker_scripts/run_devel.sh b/docker_scripts/run_devel.sh index 95b35a8..a29ce24 100755 --- a/docker_scripts/run_devel.sh +++ b/docker_scripts/run_devel.sh @@ -2,9 +2,9 @@ xhost +local:root -IMAGE_NAME="object_detection" +IMAGE_NAME="perception_pipeline" IMAGE_TAG="latest" -CONTAINER_NAME="object_detection" +CONTAINER_NAME="perception_pipeline" # Build the image if it doesn't exist if docker inspect "$IMAGE_NAME:$IMAGE_TAG" &> /dev/null; then @@ -69,4 +69,4 @@ docker run --gpus all --shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 --env CYCLONEDDS_URI=/ddsconfig.xml \ --env="QT_X11_NO_MITSHM=1" \ --env="DISPLAY" \ -object_detection:latest +$IMAGE_NAME:$IMAGE_TAG diff --git a/object_detection/launch/object_detection.launch.py b/object_detection/launch/object_detection.launch.py index 5d7c830..65804df 100644 --- a/object_detection/launch/object_detection.launch.py +++ b/object_detection/launch/object_detection.launch.py @@ -1,10 +1,10 @@ -# Copyright (c) 2018 Intel Corporation +# Copyright (c) 2023 A.T.O.M ROBOTICS # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -27,15 +27,14 @@ def generate_launch_description(): 'config', 'params.yaml' ) - - node=Node( - package = 'object_detection', - name = 'object_detection', - executable = 'ObjectDetection', - parameters = [params], + + node = Node( + package='object_detection', + name='object_detection', + executable='ObjectDetection', + parameters=[params], output="screen", - emulate_tty = True + emulate_tty=True ) - return LaunchDescription([node]) diff --git a/object_detection/object_detection/DetectorBase.py b/object_detection/object_detection/DetectorBase.py index 7ffe56d..d0b11f5 100644 --- a/object_detection/object_detection/DetectorBase.py +++ b/object_detection/object_detection/DetectorBase.py @@ -1,4 +1,19 @@ +# Copyright (c) 2023 A.T.O.M ROBOTICS +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from abc import ABC, abstractmethod + import numpy as np @@ -17,7 +32,7 @@ def create_predictions_list(self, class_ids, confidences, boxes): } self.predictions.append(obj_dict) - + @abstractmethod def build_model(self, model_dir_path: str, weight_file_name: str) -> None: pass @@ -28,4 +43,4 @@ def load_classes(self, model_dir_path: str) -> None: @abstractmethod def get_predictions(self, cv_image: np.ndarray) -> list[dict]: - pass \ No newline at end of file + pass diff --git a/object_detection/object_detection/Detectors/RetinaNet.py b/object_detection/object_detection/Detectors/RetinaNet.py index 8c1ddb3..1005034 100755 --- a/object_detection/object_detection/Detectors/RetinaNet.py +++ b/object_detection/object_detection/Detectors/RetinaNet.py @@ -1,3 +1,17 @@ +# Copyright (c) 2023 A.T.O.M ROBOTICS +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import os from keras_retinanet import models @@ -7,58 +21,55 @@ from ..DetectorBase import DetectorBase -class RetinaNet(DetectorBase) : - def __init(self) : +class RetinaNet(DetectorBase): + def __init(self): super.__init__() - def build_model(self, model_dir_path, weight_file_name) : + def build_model(self, model_dir_path, weight_file_name): model_path = os.path.join(model_dir_path, weight_file_name) - - try: + + try: self.model = models.load_model(model_path, backbone_name='resnet50') - except: - raise Exception("Error loading given model from path: {}. Maybe the file doesn't exist?".format(model_path)) + except Exception as e: + print("Loading the model failed with exception {}".format(e)) + raise Exception("Error loading given model from path: {}.".format(model_path) + + "Maybe the file doesn't exist?") - def load_classes(self, model_dir_path) : + def load_classes(self, model_dir_path): self.class_list = [] - + with open(model_dir_path + "/classes.txt", "r") as f: self.class_list = [cname.strip() for cname in f.readlines()] - + return self.class_list - def get_predictions(self, cv_image) : + def get_predictions(self, cv_image): if cv_image is None: # TODO: show warning message (different color, maybe) return None - - else : - + else: # copy to draw on self.frame = cv_image.copy() # preprocess image for network - input = preprocess_image(self.frame) - input, scale = resize_image(input) - + processed_img = preprocess_image(self.frame) + processed_img, scale = resize_image(processed_img) + # process image - boxes_all, confidences_all, class_ids_all = self.model.predict_on_batch(np.expand_dims(input, axis=0)) + boxes_all, confidences_all, class_ids_all = self.model.predict_on_batch(np.expand_dims(processed_img, axis=0)) boxes, confidences, class_ids = [], [], [] - - for index in range(len(confidences_all[0])) : - if confidences_all[0][index]!=-1 : + + for index in range(len(confidences_all[0])): + if confidences_all[0][index] != -1: confidences.append(confidences_all[0][index]) boxes.append(boxes_all[0][index]) class_ids.append(class_ids_all[0][index]) - - + # correct for image scale - #boxes = [x/scale for x in boxes] + # boxes = [x/scale for x in boxes] boxes = [[int(coord/scale) for coord in box] for box in boxes] - + super().create_predictions_list(class_ids, confidences, boxes) - - return self.predictions - + return self.predictions diff --git a/object_detection/object_detection/Detectors/YOLOv5.py b/object_detection/object_detection/Detectors/YOLOv5.py index 4363c33..3a7825d 100644 --- a/object_detection/object_detection/Detectors/YOLOv5.py +++ b/object_detection/object_detection/Detectors/YOLOv5.py @@ -1,4 +1,19 @@ +# Copyright (c) 2023 A.T.O.M ROBOTICS +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import os + import cv2 import numpy as np @@ -6,7 +21,9 @@ class YOLOv5(DetectorBase): - def __init__(self, conf_threshold = 0.7, score_threshold = 0.4, nms_threshold = 0.25, is_cuda = 1): + def __init__(self, conf_threshold=0.7, + score_threshold=0.4, nms_threshold=0.25, + is_cuda=1): super().__init__() @@ -17,17 +34,19 @@ def __init__(self, conf_threshold = 0.7, score_threshold = 0.4, nms_threshold = self.INPUT_HEIGHT = 640 self.CONFIDENCE_THRESHOLD = conf_threshold - self.is_cuda = is_cuda + self.is_cuda = is_cuda - - # load model and prepare its backend to either run on GPU or CPU, see if it can be added in constructor + # load model and prepare its backend to either run on GPU or CPU, + # see if it can be added in constructor def build_model(self, model_dir_path, weight_file_name): model_path = os.path.join(model_dir_path, weight_file_name) try: self.net = cv2.dnn.readNet(model_path) - except: - raise Exception("Error loading given model from path: {}. Maybe the file doesn't exist?".format(model_path)) + except Exception as e: + print("Loading the model failed with exception {}".format(e)) + raise Exception("Error loading given model from path: {}.".format(model_path) + + "Maybe the file doesn't exist?") if self.is_cuda: print("is_cuda was set to True. Attempting to use CUDA") @@ -38,24 +57,23 @@ def build_model(self, model_dir_path, weight_file_name): self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU) - # load classes.txt that contains mapping of model with labels - # TODO: add try/except to raise exception that tells the use to check the name if it is classes.txt + # TODO: add try/except to raise exception that tells the use to + # check the name if it is classes.txt def load_classes(self, model_dir_path): self.class_list = [] with open(model_dir_path + "/classes.txt", "r") as f: self.class_list = [cname.strip() for cname in f.readlines()] return self.class_list - def detect(self, image): - # convert image to 640x640 - blob = cv2.dnn.blobFromImage(image, 1/255.0, (self.INPUT_WIDTH, self.INPUT_HEIGHT), swapRB=True, crop=False) + # convert image to 640x640 + blob = cv2.dnn.blobFromImage(image, 1/255.0, (self.INPUT_WIDTH, self.INPUT_HEIGHT), + swapRB=True, crop=False) self.net.setInput(blob) preds = self.net.forward() return preds - # extract bounding box, class IDs and confidences of detected objects # YOLOv5 returns a 3D tensor of dimension 25200*(5 + n_classes) def wrap_detection(self, input_image, output_data): @@ -68,7 +86,7 @@ def wrap_detection(self, input_image, output_data): image_width, image_height, _ = input_image.shape x_factor = image_width / self.INPUT_WIDTH - y_factor = image_height / self.INPUT_HEIGHT + y_factor = image_height / self.INPUT_HEIGHT # Iterate through all the 25200 vectors for r in range(rows): @@ -77,7 +95,7 @@ def wrap_detection(self, input_image, output_data): # Continue only if Pc > conf_threshold confidence = row[4] if confidence >= self.CONFIDENCE_THRESHOLD: - + # One-hot encoded vector representing class of object classes_scores = row[5:] @@ -95,7 +113,7 @@ def wrap_detection(self, input_image, output_data): class_ids.append(class_id) - x, y, w, h = row[0].item(), row[1].item(), row[2].item(), row[3].item() + x, y, w, h = row[0].item(), row[1].item(), row[2].item(), row[3].item() left = int((x - 0.5 * w) * x_factor) top = int((y - 0.5 * h) * y_factor) width = int(w * x_factor) @@ -104,7 +122,7 @@ def wrap_detection(self, input_image, output_data): boxes.append(box) # removing intersecting bounding boxes - indexes = cv2.dnn.NMSBoxes(boxes, confidences, 0.25, 0.45) + indexes = cv2.dnn.NMSBoxes(boxes, confidences, 0.25, 0.45) result_class_ids = [] result_confidences = [] @@ -117,7 +135,6 @@ def wrap_detection(self, input_image, output_data): return result_class_ids, result_confidences, result_boxes - # makes image square with dimension max(h, w) def format_yolov5(self): row, col, _ = self.frame.shape @@ -126,14 +143,13 @@ def format_yolov5(self): result[0:row, 0:col] = self.frame return result - def get_predictions(self, cv_image): - #Clear list + # Clear list self.predictions = [] if cv_image is None: # TODO: show warning message (different color, maybe) - return None,None + return None, None else: self.frame = cv_image @@ -143,6 +159,6 @@ def get_predictions(self, cv_image): outs = self.detect(inputImage) class_ids, confidences, boxes = self.wrap_detection(inputImage, outs[0]) - super().create_predictions_list(class_ids, confidences, boxes) - - return self.predictions \ No newline at end of file + super().create_predictions_list(class_ids, confidences, boxes) + + return self.predictions diff --git a/object_detection/object_detection/Detectors/YOLOv8.py b/object_detection/object_detection/Detectors/YOLOv8.py index e1f7d59..9bb1918 100755 --- a/object_detection/object_detection/Detectors/YOLOv8.py +++ b/object_detection/object_detection/Detectors/YOLOv8.py @@ -1,55 +1,66 @@ -from ultralytics import YOLO +# Copyright (c) 2023 A.T.O.M ROBOTICS +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import os +from ultralytics import YOLO + from ..DetectorBase import DetectorBase class YOLOv8(DetectorBase): - def __init__(self, conf_threshold = 0.7): - - super().__init__() - - self.conf_threshold = conf_threshold - - def build_model(self, model_dir_path, weight_file_name) : - - try : - model_path = os.path.join(model_dir_path, weight_file_name) - self.model = YOLO(model_path) - - except : - raise Exception("Error loading given model from path: {}. Maybe the file doesn't exist?".format(model_path)) - - - def load_classes(self, model_dir_path): - - self.class_list = [] - - with open(model_dir_path + "/classes.txt", "r") as f: - self.class_list = [cname.strip() for cname in f.readlines()] - - return self.class_list - - def get_predictions(self, cv_image): - - if cv_image is None: - # TODO: show warning message (different color, maybe) - return None,None - - else : - self.frame = cv_image - class_id = [] - confidence = [] - boxes = [] - - result = self.model.predict(self.frame, conf = self.conf_threshold, verbose = False) # Perform object detection on image - row = result[0].boxes.cpu() - - for box in row: - class_id.append(box.cls.numpy().tolist()[0]) - confidence.append(box.conf.numpy().tolist()[0]) - boxes.append(box.xyxy.numpy().tolist()[0]) - - super().create_predictions_list(class_id, confidence, boxes) - - return self.predictions \ No newline at end of file + + def __init__(self, conf_threshold=0.7): + super().__init__() + self.conf_threshold = conf_threshold + + def build_model(self, model_dir_path, weight_file_name): + try: + model_path = os.path.join(model_dir_path, weight_file_name) + self.model = YOLO(model_path) + except Exception as e: + print("Loading model failed with exception: {}".format(e)) + raise Exception("Error loading given model from path: {}.".format(model_path) + + " Maybe the file doesn't exist?") + + def load_classes(self, model_dir_path): + self.class_list = [] + + with open(model_dir_path + "/classes.txt", "r") as f: + self.class_list = [cname.strip() for cname in f.readlines()] + + return self.class_list + + def get_predictions(self, cv_image): + if cv_image is None: + # TODO: show warning message (different color, maybe) + return None, None + else: + self.frame = cv_image + class_id = [] + confidence = [] + boxes = [] + + # Perform object detection on image + result = self.model.predict(self.frame, conf=self.conf_threshold, verbose=False) + row = result[0].boxes.cpu() + + for box in row: + class_id.append(box.cls.numpy().tolist()[0]) + confidence.append(box.conf.numpy().tolist()[0]) + boxes.append(box.xyxy.numpy().tolist()[0]) + + super().create_predictions_list(class_id, confidence, boxes) + + return self.predictions diff --git a/object_detection/object_detection/ObjectDetection.py b/object_detection/object_detection/ObjectDetection.py index 72dac99..b2ff1e0 100644 --- a/object_detection/object_detection/ObjectDetection.py +++ b/object_detection/object_detection/ObjectDetection.py @@ -1,38 +1,52 @@ #! /usr/bin/env python3 -import os +# Copyright (c) 2023 A.T.O.M ROBOTICS +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import importlib +import os + +import cv2 + +from cv_bridge import CvBridge import rclpy from rclpy.node import Node from sensor_msgs.msg import Image -#from vision_msgs.msg import BoundingBox2D - -from cv_bridge import CvBridge -import cv2 class ObjectDetection(Node): + def __init__(self): super().__init__('object_detection') # create an empty list that will hold the names of all available detector self.available_detectors = [] - + # fill available_detectors with the detectors from Detectors dir self.discover_detectors() self.declare_parameters( namespace='', parameters=[ - - ('input_img_topic', ""), - ('output_bb_topic', ""), - ('output_img_topic', ""), - ('model_params.detector_type', ""), - ('model_params.model_dir_path', ""), - ('model_params.weight_file_name', ""), + ('input_img_topic', ''), + ('output_bb_topic', ''), + ('output_img_topic', ''), + ('model_params.detector_type', ''), + ('model_params.model_dir_path', ''), + ('model_params.weight_file_name', ''), ('model_params.confidence_threshold', 0.7), ('model_params.show_fps', 1), ] @@ -42,71 +56,69 @@ def __init__(self): self.input_img_topic = self.get_parameter('input_img_topic').value self.output_bb_topic = self.get_parameter('output_bb_topic').value self.output_img_topic = self.get_parameter('output_img_topic').value - + # model params self.detector_type = self.get_parameter('model_params.detector_type').value self.model_dir_path = self.get_parameter('model_params.model_dir_path').value self.weight_file_name = self.get_parameter('model_params.weight_file_name').value self.confidence_threshold = self.get_parameter('model_params.confidence_threshold').value self.show_fps = self.get_parameter('model_params.show_fps').value - + # raise an exception if specified detector was not found if self.detector_type not in self.available_detectors: - raise ModuleNotFoundError(self.detector_type + " Detector specified in config was not found. " + - "Check the Detectors dir for available detectors.") + raise ModuleNotFoundError(self.detector_type + " Detector specified in config was not found. " + + "Check the Detectors dir for available detectors.") else: self.load_detector() - - + self.img_pub = self.create_publisher(Image, self.output_img_topic, 10) self.bb_pub = None self.img_sub = self.create_subscription(Image, self.input_img_topic, self.detection_cb, 10) self.bridge = CvBridge() - def discover_detectors(self): curr_dir = os.path.dirname(__file__) - dir_contents = os.listdir(curr_dir + "/Detectors") + dir_contents = os.listdir(curr_dir + "/Detectors") for entity in dir_contents: if entity.endswith('.py'): self.available_detectors.append(entity[:-3]) self.available_detectors.remove('__init__') - - + def load_detector(self): - detector_mod = importlib.import_module(".Detectors." + self.detector_type, "object_detection") + detector_mod = importlib.import_module(".Detectors." + self.detector_type, + "object_detection") detector_class = getattr(detector_mod, self.detector_type) self.detector = detector_class() self.detector.build_model(self.model_dir_path, self.weight_file_name) self.detector.load_classes(self.model_dir_path) - print("Your detector : {} has been loaded !".format(self.detector_type)) - - + print("Your detector: {} has been loaded !".format(self.detector_type)) + def detection_cb(self, img_msg): cv_image = self.bridge.imgmsg_to_cv2(img_msg, "bgr8") predictions = self.detector.get_predictions(cv_image=cv_image) - - if predictions == None : - print("Image input from topic : {} is empty".format(self.input_img_topic)) - else : + + if predictions is None: + print("Image input from topic: {} is empty".format(self.input_img_topic)) + else: for prediction in predictions: - x1, y1, x2, y2 = map(int, prediction['box']) + x1, y1, x2, y2 = map(int, prediction['box']) # Draw the bounding box - cv_image = cv2.rectangle(cv_image, (x1, y1), (x2, y2), (0,255,0),1) + cv_image = cv2.rectangle(cv_image, (x1, y1), (x2, y2), (0, 255, 0), 1) # Show names of classes on the output image class_id = int(prediction['class_id']) class_name = self.detector.class_list[class_id] - label = f"{class_name} : {prediction['confidence']:.2f}" + label = f"{class_name}: {prediction['confidence']:.2f}" - cv_image = cv2.putText(cv_image, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) + cv_image = cv2.putText(cv_image, label, (x1, y1 - 5), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) output = self.bridge.cv2_to_imgmsg(cv_image, "bgr8") self.img_pub.publish(output) @@ -115,12 +127,12 @@ def detection_cb(self, img_msg): def main(): rclpy.init() od = ObjectDetection() - try : + try: rclpy.spin(od) except Exception as e: print(e) -if __name__=="__main__" : +if __name__ == "__main__": main() diff --git a/object_detection/setup.cfg b/object_detection/setup.cfg index 90b06b2..2165e16 100644 --- a/object_detection/setup.cfg +++ b/object_detection/setup.cfg @@ -2,3 +2,9 @@ script_dir=$base/lib/object_detection [install] install_scripts=$base/lib/object_detection +[flake8] +extend-ignore = B902,C816,D100,D101,D102,D103,D104,D105,D106,D107,D203,D212,D404,I202,Q000 +import-order-style = google +max-line-length = 125 +show-source = true +statistics = true \ No newline at end of file diff --git a/object_detection/setup.py b/object_detection/setup.py index 6cec5fe..c1fcbea 100644 --- a/object_detection/setup.py +++ b/object_detection/setup.py @@ -1,6 +1,7 @@ -from setuptools import setup -import os from glob import glob +import os + +from setuptools import setup package_name = 'object_detection' diff --git a/object_detection/test/test_flake8.py b/object_detection/test/test_flake8.py index 27ee107..7932fbd 100644 --- a/object_detection/test/test_flake8.py +++ b/object_detection/test/test_flake8.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from os.path import abspath, dirname, join + from ament_flake8.main import main_with_errors import pytest @@ -19,7 +21,12 @@ @pytest.mark.flake8 @pytest.mark.linter def test_flake8(): - rc, errors = main_with_errors(argv=[]) + config_file = join(dirname(dirname(dirname(abspath(__file__)))), ".flake8") + args = [ + "--config={}".format(config_file) + ] + + rc, errors = main_with_errors(argv=args) assert rc == 0, \ 'Found %d code style errors / warnings:\n' % len(errors) + \ '\n'.join(errors) diff --git a/perception_bringup/launch/playground.launch.py b/perception_bringup/launch/playground.launch.py index 83868b0..a2bd10c 100644 --- a/perception_bringup/launch/playground.launch.py +++ b/perception_bringup/launch/playground.launch.py @@ -1,10 +1,10 @@ -# Copyright (c) 2018 Intel Corporation +# Copyright (c) 2023 A.T.O.M ROBOTICS # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -18,56 +18,48 @@ from ament_index_python.packages import get_package_share_directory from launch import LaunchDescription -from launch.actions import IncludeLaunchDescription, DeclareLaunchArgument -from launch.substitutions import LaunchConfiguration +from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription from launch.launch_description_sources import PythonLaunchDescriptionSource from launch_ros.actions import Node def generate_launch_description(): - - pkg_perception_bringup = get_package_share_directory("perception_bringup") - #pkg_ros_gz_sim = get_package_share_directory("ros_gz_sim") - - world_name = "playground" - - for arg in sys.argv: - if arg.startswith("world:="): - world_name = arg.split(":=")[1] - - world_sdf = pkg_perception_bringup + "/worlds/" + world_name + ".sdf" - - '''gz_sim = IncludeLaunchDescription( - PythonLaunchDescriptionSource( - os.path.join(pkg_ros_gz_sim, 'launch', 'gz_sim.launch.py')), - )''' - - gz_sim_share = get_package_share_directory("ros_gz_sim") - gz_sim = IncludeLaunchDescription( - PythonLaunchDescriptionSource(os.path.join(gz_sim_share, "launch", "gz_sim.launch.py")), - launch_arguments={ - "gz_args" : world_sdf - }.items() - ) - - parameter_bridge = Node(package="ros_gz_bridge", executable="parameter_bridge", - parameters = [ - {'config_file' : os.path.join(pkg_perception_bringup, "config", "bridge.yaml")} - ] - ) - - arg_gz_sim = DeclareLaunchArgument('gz_args', default_value=world_sdf) - - arg_world_name = DeclareLaunchArgument('world', default_value='playground_world' ) - - launch = [ - gz_sim, - parameter_bridge - ] - - args = [ - arg_gz_sim, - arg_world_name - ] - - return LaunchDescription(args + launch) + pkg_perception_bringup = get_package_share_directory("perception_bringup") + pkg_ros_gz_sim = get_package_share_directory("ros_gz_sim") + ros_gz_bridge_config = os.path.join(pkg_perception_bringup, "config", "bridge.yaml") + world_name = "playground" + + for arg in sys.argv: + if arg.startswith("world:="): + world_name = arg.split(":=")[1] + + world_sdf = pkg_perception_bringup + "/worlds/" + world_name + ".sdf" + + gz_sim = IncludeLaunchDescription( + PythonLaunchDescriptionSource( + os.path.join(pkg_ros_gz_sim, "launch", "gz_sim.launch.py") + ), + launch_arguments={ + "gz_args": world_sdf + }.items() + ) + + parameter_bridge = Node(package="ros_gz_bridge", executable="parameter_bridge", + parameters=[ + {'config_file': ros_gz_bridge_config} + ]) + + arg_gz_sim = DeclareLaunchArgument('gz_args', default_value=world_sdf) + arg_world_name = DeclareLaunchArgument('world', default_value='playground_world') + + launch = [ + gz_sim, + parameter_bridge + ] + + args = [ + arg_gz_sim, + arg_world_name + ] + + return LaunchDescription(args + launch) From eaadc67c8620e708b3448935b97fbde1eea7b16e Mon Sep 17 00:00:00 2001 From: Arjun K Haridas <51917087+topguns837@users.noreply.github.com> Date: Sun, 11 Feb 2024 00:02:37 +0530 Subject: [PATCH 15/17] Re-wrote YOLOv5 Plugin using torch instead of OpenCV (#59) * Re-wrote YOLOv5 Plugin using torch instead of OpenCV * Added copyright to YOLOv5 Detector * Removing flake8 linting issues * Removing flake8 linting issues --------- Co-authored-by: topguns837 --- .../object_detection/Detectors/YOLOv5.py | 139 +++--------------- 1 file changed, 20 insertions(+), 119 deletions(-) mode change 100644 => 100755 object_detection/object_detection/Detectors/YOLOv5.py diff --git a/object_detection/object_detection/Detectors/YOLOv5.py b/object_detection/object_detection/Detectors/YOLOv5.py old mode 100644 new mode 100755 index 3a7825d..4553004 --- a/object_detection/object_detection/Detectors/YOLOv5.py +++ b/object_detection/object_detection/Detectors/YOLOv5.py @@ -14,151 +14,52 @@ import os -import cv2 -import numpy as np +import torch from ..DetectorBase import DetectorBase class YOLOv5(DetectorBase): - def __init__(self, conf_threshold=0.7, - score_threshold=0.4, nms_threshold=0.25, - is_cuda=1): + def __init__(self, conf_threshold=0.7): super().__init__() + self.conf_threshold = conf_threshold - # opencv img input - self.frame = None - self.net = None - self.INPUT_WIDTH = 640 - self.INPUT_HEIGHT = 640 - self.CONFIDENCE_THRESHOLD = conf_threshold - - self.is_cuda = is_cuda - - # load model and prepare its backend to either run on GPU or CPU, - # see if it can be added in constructor def build_model(self, model_dir_path, weight_file_name): - model_path = os.path.join(model_dir_path, weight_file_name) - try: - self.net = cv2.dnn.readNet(model_path) + model_path = os.path.join(model_dir_path, weight_file_name) + self.model = torch.hub.load('ultralytics/yolov5:v6.0', 'custom', path=model_path, + force_reload=True) except Exception as e: - print("Loading the model failed with exception {}".format(e)) + print("Loading model failed with exception: {}".format(e)) raise Exception("Error loading given model from path: {}.".format(model_path) + - "Maybe the file doesn't exist?") + " Maybe the file doesn't exist?") - if self.is_cuda: - print("is_cuda was set to True. Attempting to use CUDA") - self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA) - self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA_FP16) - else: - print("is_cuda was set to False. Running on CPU") - self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) - self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU) - - # load classes.txt that contains mapping of model with labels - # TODO: add try/except to raise exception that tells the use to - # check the name if it is classes.txt def load_classes(self, model_dir_path): self.class_list = [] - with open(model_dir_path + "/classes.txt", "r") as f: - self.class_list = [cname.strip() for cname in f.readlines()] - return self.class_list - - def detect(self, image): - # convert image to 640x640 - blob = cv2.dnn.blobFromImage(image, 1/255.0, (self.INPUT_WIDTH, self.INPUT_HEIGHT), - swapRB=True, crop=False) - self.net.setInput(blob) - preds = self.net.forward() - return preds - - # extract bounding box, class IDs and confidences of detected objects - # YOLOv5 returns a 3D tensor of dimension 25200*(5 + n_classes) - def wrap_detection(self, input_image, output_data): - class_ids = [] - confidences = [] - boxes = [] - - rows = output_data.shape[0] - - image_width, image_height, _ = input_image.shape - - x_factor = image_width / self.INPUT_WIDTH - y_factor = image_height / self.INPUT_HEIGHT - - # Iterate through all the 25200 vectors - for r in range(rows): - row = output_data[r] - - # Continue only if Pc > conf_threshold - confidence = row[4] - if confidence >= self.CONFIDENCE_THRESHOLD: - # One-hot encoded vector representing class of object - classes_scores = row[5:] - - # Returns min and max values in a array alongwith their indices - _, _, _, max_indx = cv2.minMaxLoc(classes_scores) - - # Extract the column index of the maximum values in classes_scores - class_id = max_indx[1] - - # Continue of the class score is greater than a threshold - # class_score represents the probability of an object belonging to that class - if (classes_scores[class_id] > .25): - - confidences.append(confidence) - - class_ids.append(class_id) - - x, y, w, h = row[0].item(), row[1].item(), row[2].item(), row[3].item() - left = int((x - 0.5 * w) * x_factor) - top = int((y - 0.5 * h) * y_factor) - width = int(w * x_factor) - height = int(h * y_factor) - box = np.array([left, top, width, height]) - boxes.append(box) - - # removing intersecting bounding boxes - indexes = cv2.dnn.NMSBoxes(boxes, confidences, 0.25, 0.45) - - result_class_ids = [] - result_confidences = [] - result_boxes = [] - - for i in indexes: - result_confidences.append(confidences[i]) - result_class_ids.append(class_ids[i]) - result_boxes.append(boxes[i]) - - return result_class_ids, result_confidences, result_boxes + with open(os.path.join(model_dir_path, 'classes.txt')) as f: + self.class_list = [cname.strip() for cname in f.readlines()] - # makes image square with dimension max(h, w) - def format_yolov5(self): - row, col, _ = self.frame.shape - _max = max(col, row) - result = np.zeros((_max, _max, 3), np.uint8) - result[0:row, 0:col] = self.frame - return result + return self.class_list def get_predictions(self, cv_image): - # Clear list - self.predictions = [] - if cv_image is None: # TODO: show warning message (different color, maybe) return None, None else: self.frame = cv_image + class_id = [] + confidence = [] + boxes = [] - # make image square - inputImage = self.format_yolov5() + results = self.model(self.frame) - outs = self.detect(inputImage) - class_ids, confidences, boxes = self.wrap_detection(inputImage, outs[0]) + for *xyxy, conf, label in results.xyxy[0]: + class_id.append(int(label)) + confidence.append(conf.item()) + boxes.append([int(xy) for xy in xyxy]) - super().create_predictions_list(class_ids, confidences, boxes) + super().create_predictions_list(class_id, confidence, boxes) return self.predictions From 640747fee2fa5ad1f850547749fb44b53a9f79ff Mon Sep 17 00:00:00 2001 From: topguns837 Date: Sun, 11 Feb 2024 15:31:23 +0530 Subject: [PATCH 16/17] Add FPS calculations and added License and description to setup.cfg --- object_detection/config/params.yaml | 2 +- .../object_detection/ObjectDetection.py | 25 +++++++++++++++++++ object_detection/package.xml | 2 +- object_detection/setup.py | 4 +-- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/object_detection/config/params.yaml b/object_detection/config/params.yaml index eb69547..681c2b9 100644 --- a/object_detection/config/params.yaml +++ b/object_detection/config/params.yaml @@ -7,6 +7,6 @@ object_detection: model_params: detector_type: YOLOv5 model_dir_path: /root/percep_ws/models/yolov5 - weight_file_name: yolov5.onnx + weight_file_name: yolov5s.pt confidence_threshold : 0.5 show_fps : 1 \ No newline at end of file diff --git a/object_detection/object_detection/ObjectDetection.py b/object_detection/object_detection/ObjectDetection.py index b2ff1e0..5ac3b48 100644 --- a/object_detection/object_detection/ObjectDetection.py +++ b/object_detection/object_detection/ObjectDetection.py @@ -16,6 +16,7 @@ import importlib import os +import time import cv2 @@ -77,6 +78,12 @@ def __init__(self): self.bridge = CvBridge() + if self.show_fps: + self.start_time = time.time() + self.frame_count = 0 + self.total_elapsed_time = 0 + self.average_fps = 0 + def discover_detectors(self): curr_dir = os.path.dirname(__file__) dir_contents = os.listdir(curr_dir + "/Detectors") @@ -101,6 +108,8 @@ def load_detector(self): def detection_cb(self, img_msg): cv_image = self.bridge.imgmsg_to_cv2(img_msg, "bgr8") + start_time = time.time() + predictions = self.detector.get_predictions(cv_image=cv_image) if predictions is None: @@ -120,6 +129,22 @@ def detection_cb(self, img_msg): cv_image = cv2.putText(cv_image, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) + if self.show_fps: + elapsed_time = time.time() - start_time + self.frame_count += 1 + self.total_elapsed_time += elapsed_time + + # Write FPS on the frame + cv_image = cv2.putText(cv_image, f"FPS: {self.average_fps:.2f}", (10, 30), + cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) + + if time.time() - self.start_time >= 1.0: + self.average_fps = self.frame_count / self.total_elapsed_time + + self.frame_count = 0 + self.total_elapsed_time = 0 + self.start_time = time.time() + output = self.bridge.cv2_to_imgmsg(cv_image, "bgr8") self.img_pub.publish(output) diff --git a/object_detection/package.xml b/object_detection/package.xml index fc13de3..1bd237b 100644 --- a/object_detection/package.xml +++ b/object_detection/package.xml @@ -3,7 +3,7 @@ object_detection 0.0.0 - This is a ROS 2 package aimed at providing an interfacing between Deep Learning models and the ROS architecture using a plug-and-play modular arhitecture + This is a ROS 2 package aimed at providing an interfacing between Deep Learning models and the ROS architecture using a plug-and-play modular architecture singh Apache 2.0 diff --git a/object_detection/setup.py b/object_detection/setup.py index c1fcbea..5602255 100644 --- a/object_detection/setup.py +++ b/object_detection/setup.py @@ -21,8 +21,8 @@ zip_safe=True, maintainer='singh', maintainer_email='jasmeet0915@gmail.com', - description='TODO: Package description', - license='TODO: License declaration', + description='Plug-and-Play ROS 2 package for Perception in Robotics', + license='Apache 2.0', tests_require=['pytest'], entry_points={ 'console_scripts': [ From 10d83e63e7574393a8ba9de250ea3dcc5f01489f Mon Sep 17 00:00:00 2001 From: topguns837 Date: Wed, 14 Feb 2024 22:33:24 +0530 Subject: [PATCH 17/17] Made the detector_type param case-insensitive --- .../object_detection/ObjectDetection.py | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/object_detection/object_detection/ObjectDetection.py b/object_detection/object_detection/ObjectDetection.py index 5ac3b48..844e09f 100644 --- a/object_detection/object_detection/ObjectDetection.py +++ b/object_detection/object_detection/ObjectDetection.py @@ -65,12 +65,8 @@ def __init__(self): self.confidence_threshold = self.get_parameter('model_params.confidence_threshold').value self.show_fps = self.get_parameter('model_params.show_fps').value - # raise an exception if specified detector was not found - if self.detector_type not in self.available_detectors: - raise ModuleNotFoundError(self.detector_type + " Detector specified in config was not found. " + - "Check the Detectors dir for available detectors.") - else: - self.load_detector() + # Load the detector + self.load_detector() self.img_pub = self.create_publisher(Image, self.output_img_topic, 10) self.bb_pub = None @@ -95,15 +91,22 @@ def discover_detectors(self): self.available_detectors.remove('__init__') def load_detector(self): - detector_mod = importlib.import_module(".Detectors." + self.detector_type, - "object_detection") - detector_class = getattr(detector_mod, self.detector_type) - self.detector = detector_class() + for detector_name in self.available_detectors: + if self.detector_type.lower() == detector_name.lower(): + + detector_mod = importlib.import_module(".Detectors." + detector_name, + "object_detection") + detector_class = getattr(detector_mod, detector_name) + self.detector = detector_class() + + self.detector.build_model(self.model_dir_path, self.weight_file_name) + self.detector.load_classes(self.model_dir_path) - self.detector.build_model(self.model_dir_path, self.weight_file_name) - self.detector.load_classes(self.model_dir_path) + print("Your detector: {} has been loaded !".format(detector_name)) + return - print("Your detector: {} has been loaded !".format(self.detector_type)) + raise ModuleNotFoundError(self.detector_type + " Detector specified in config was not found. " + + "Check the Detectors dir for available detectors.") def detection_cb(self, img_msg): cv_image = self.bridge.imgmsg_to_cv2(img_msg, "bgr8")