From f56b8c9e6505e7a9757385d0cb9322a9ed48d4f0 Mon Sep 17 00:00:00 2001 From: Loy van Beek Date: Sat, 5 Feb 2022 14:12:05 +0100 Subject: [PATCH 01/10] Create new module for estimating face properties using PyTorch --- .gitmodules | 3 + image_recognition_pytorch/CMakeLists.txt | 25 +++++ image_recognition_pytorch/README.md | 53 +++++++++ image_recognition_pytorch/doc/face.png | Bin 0 -> 25731 bytes image_recognition_pytorch/docs | 1 + image_recognition_pytorch/package.xml | 45 ++++++++ image_recognition_pytorch/rosdoc.yaml | 3 + .../scripts/download_model | 23 ++++ .../scripts/face_properties_node | 103 ++++++++++++++++++ .../scripts/get_face_properties | 26 +++++ image_recognition_pytorch/setup.py | 9 ++ .../src/image_recognition_pytorch/__init__.py | 1 + .../age_gender_estimator.py | 49 +++++++++ .../test/assets/age_28_gender_female.jpg | Bin 0 -> 9868 bytes .../test/assets/age_29_gender_male.jpg | Bin 0 -> 8515 bytes .../test/assets/age_33_gender_male.jpg | Bin 0 -> 10212 bytes image_recognition_pytorch/test/run_tests.bash | 2 + .../test/test_face_properties.py | 40 +++++++ .../src/image_recognition_rqt/test.py | 4 +- 19 files changed, 385 insertions(+), 2 deletions(-) create mode 100644 image_recognition_pytorch/CMakeLists.txt create mode 100644 image_recognition_pytorch/README.md create mode 100644 image_recognition_pytorch/doc/face.png create mode 160000 image_recognition_pytorch/docs create mode 100644 image_recognition_pytorch/package.xml create mode 100644 image_recognition_pytorch/rosdoc.yaml create mode 100755 image_recognition_pytorch/scripts/download_model create mode 100755 image_recognition_pytorch/scripts/face_properties_node create mode 100755 image_recognition_pytorch/scripts/get_face_properties create mode 100644 image_recognition_pytorch/setup.py create mode 100644 image_recognition_pytorch/src/image_recognition_pytorch/__init__.py create mode 100644 image_recognition_pytorch/src/image_recognition_pytorch/age_gender_estimator.py create mode 100644 image_recognition_pytorch/test/assets/age_28_gender_female.jpg create mode 100644 image_recognition_pytorch/test/assets/age_29_gender_male.jpg create mode 100644 image_recognition_pytorch/test/assets/age_33_gender_male.jpg create mode 100755 image_recognition_pytorch/test/run_tests.bash create mode 100644 image_recognition_pytorch/test/test_face_properties.py diff --git a/.gitmodules b/.gitmodules index e5ab9394..e3e0ce23 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,3 +28,6 @@ [submodule "image_recognition_util/docs"] path = image_recognition_util/docs url = https://github.com/tue-robotics/tue_documentation_python.git +[submodule "image_recognition_pytorch/docs"] + path = image_recognition_pytorch/docs + url = https://github.com/tue-robotics/tue_documentation_python.git diff --git a/image_recognition_pytorch/CMakeLists.txt b/image_recognition_pytorch/CMakeLists.txt new file mode 100644 index 00000000..a10619ad --- /dev/null +++ b/image_recognition_pytorch/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.0.2) +project(image_recognition_pytorch) + +find_package(catkin REQUIRED) + +catkin_python_setup() + +catkin_package() + +install(PROGRAMS + scripts/face_properties_node + scripts/get_face_properties + DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +) + +if (CATKIN_ENABLE_TESTING) + # Test catkin lint + find_program(CATKIN_LINT catkin_lint REQUIRED) + execute_process(COMMAND "${CATKIN_LINT}" "-q" "-W2" "${CMAKE_SOURCE_DIR}" RESULT_VARIABLE lint_result) + if(NOT ${lint_result} EQUAL 0) + message(FATAL_ERROR "catkin_lint failed") + endif() + + catkin_add_nosetests(test) +endif() diff --git a/image_recognition_pytorch/README.md b/image_recognition_pytorch/README.md new file mode 100644 index 00000000..4a9c4f7c --- /dev/null +++ b/image_recognition_pytorch/README.md @@ -0,0 +1,53 @@ +# Keras image recognition + +Image recognition with use of PyTorch. + +## Installation + +See https://github.com/tue-robotics/image_recognition for installation instructions. + +## ROS Node (face_properties_node) + +Age and gender estimation +``` +rosrun image_recognition_keras face_properties_node _weights_file_path:=[path_to_model] +``` + +Run the image_recognition_rqt test gui (https://github.com/tue-robotics/image_recognition_rqt) + + rosrun image_recognition_rqt test_gui + +Configure the service you want to call with the gear-wheel in the top-right corner of the screen. If everything is set-up, draw a rectangle in the image around a face: + +![Wide ResNet](doc/wide_resnet_test.png) + +## Scripts + +### Download model + +Download weights from github. + +``` +usage: download_model [-h] [--model_path MODEL_PATH] + +optional arguments: + -h, --help show this help message and exit + --model_path MODEL_PATH +``` + +### Get face properties (get_face_properties) + +Get the classification result of an input image: + +``` +rosrun image_recognition_pytorch get_face_properties `rospack find image_recognition_pytorch`/doc/face.png +``` + +![Example](doc/face.png) + +Output: + + [(50.5418073660112, array([0.5845756 , 0.41542447], dtype=float32))] + +## Troubleshooting + diff --git a/image_recognition_pytorch/doc/face.png b/image_recognition_pytorch/doc/face.png new file mode 100644 index 0000000000000000000000000000000000000000..d9ae94a394aa60bad823515b6b6265083c18e728 GIT binary patch literal 25731 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 literal 0 HcmV?d00001 diff --git a/image_recognition_pytorch/docs b/image_recognition_pytorch/docs new file mode 160000 index 00000000..6a785e90 --- /dev/null +++ b/image_recognition_pytorch/docs @@ -0,0 +1 @@ +Subproject commit 6a785e90b0039a84f683121fa4742b0c1196acd6 diff --git a/image_recognition_pytorch/package.xml b/image_recognition_pytorch/package.xml new file mode 100644 index 00000000..a60e551d --- /dev/null +++ b/image_recognition_pytorch/package.xml @@ -0,0 +1,45 @@ + + + + image_recognition_pytorch + 0.0.1 + The image_recognition_pytorch package + + Loy van Beek + + MIT + + catkin + + python-setuptools + python3-setuptools + + diagnostic_updater + image_recognition_msgs + image_recognition_util + python-numpy + python3-numpy + python-opencv + python3-opencv + rospy + + python-catkin-lint + python3-catkin-lint + python-future + python3-future + python-rospkg + python3-rospkg + + python-sphinx + python3-sphinx + python-sphinx-autoapi-pip + python-sphinx-rtd-theme-pip + python-yaml + python3-yaml + + + + + diff --git a/image_recognition_pytorch/rosdoc.yaml b/image_recognition_pytorch/rosdoc.yaml new file mode 100644 index 00000000..d6daeddf --- /dev/null +++ b/image_recognition_pytorch/rosdoc.yaml @@ -0,0 +1,3 @@ +- builder: sphinx + sphinx_root_dir: docs + name: Python API diff --git a/image_recognition_pytorch/scripts/download_model b/image_recognition_pytorch/scripts/download_model new file mode 100755 index 00000000..41f88012 --- /dev/null +++ b/image_recognition_pytorch/scripts/download_model @@ -0,0 +1,23 @@ +#!/usr/bin/env python +from __future__ import print_function +import os +import urllib + +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument('--model_path', default=os.path.expanduser('~/pytorch/models')) +args = parser.parse_args() + +os.system('mkdir -p {}'.format(args.model_path)) +local_path = os.path.join(args.model_path, 'face_properties_weights.28-3.73.hdf5') + +if not os.path.exists(local_path): + http_path = "https://github.com/tue-robotics/image_recognition/releases/download/" \ + "image_recognition_keras_face_properties_weights.28-3.73/" \ + "image_recognition_keras_face_properties_weights.28-3.73.hdf5" + print("Downloading model to {} ...".format(local_path)) + urllib.urlretrieve(http_path, local_path) + print("Model downloaded: {}".format(local_path)) +else: + print("Model already downloaded: {}".format(local_path)) diff --git a/image_recognition_pytorch/scripts/face_properties_node b/image_recognition_pytorch/scripts/face_properties_node new file mode 100755 index 00000000..50bbd6fe --- /dev/null +++ b/image_recognition_pytorch/scripts/face_properties_node @@ -0,0 +1,103 @@ +#!/usr/bin/env python +import os +import sys + +import diagnostic_updater +import rospy +from cv_bridge import CvBridge, CvBridgeError +from image_recognition_pytorch.age_gender_estimator import AgeGenderEstimator +from image_recognition_msgs.msg import FaceProperties +from image_recognition_msgs.srv import GetFaceProperties +from image_recognition_util import image_writer + + +class PytorchFaceProperties: + def __init__(self, weights_file_path, img_size, depth, width, save_images_folder): + """ + ROS node that wraps the PyTorch age gender estimator + """ + self._bridge = CvBridge() + self._properties_srv = rospy.Service('get_face_properties', GetFaceProperties, self._get_face_properties_srv) + self._estimator = AgeGenderEstimator(weights_file_path, img_size, depth, width) + + if save_images_folder: + self._save_images_folder = os.path.expanduser(save_images_folder) + if not os.path.exists(self._save_images_folder): + os.makedirs(self._save_images_folder) + else: + self._save_images_folder = None + + rospy.loginfo("WideResnetFaceProperties node initialized:") + rospy.loginfo(" - weights_file_path=%s", weights_file_path) + rospy.loginfo(" - img_size=%s", img_size) + rospy.loginfo(" - depth=%s", depth) + rospy.loginfo(" - width=%s", width) + rospy.loginfo(" - save_images_folder=%s", save_images_folder) + + def _get_face_properties_srv(self, req): + """ + Callback when the GetFaceProperties service is called + + :param req: Input images + :return: properties + """ + # Convert to opencv images + try: + bgr_images = [self._bridge.imgmsg_to_cv2(image, "bgr8") for image in req.face_image_array] + except CvBridgeError as e: + raise Exception("Could not convert image to opencv image: %s" % str(e)) + + rospy.loginfo("Estimating the age and gender of %d incoming images ...", len(bgr_images)) + estimations = self._estimator.estimate(bgr_images) + rospy.loginfo("Done") + + face_properties_array = [] + for (age, gender_prob) in estimations: + gender, gender_confidence = (FaceProperties.FEMALE, gender_prob[0]) if gender_prob[0] > 0.5 else (FaceProperties.MALE, gender_prob[1]) + + face_properties_array.append(FaceProperties( + age=int(age), + gender=gender, + gender_confidence=gender_confidence + )) + + # Store images if specified + if self._save_images_folder: + def _get_label(p): + return "age_%d_gender_%s" % (p.age, "male" if p.gender == FaceProperties.MALE else "female") + + image_writer.write_estimations(self._save_images_folder, bgr_images, + [_get_label(p) for p in face_properties_array], + suffix="_face_properties") + + # Service response + return {"properties_array": face_properties_array} + + +if __name__ == '__main__': + rospy.init_node("face_properties") + + try: + default_weights_path = os.path.expanduser('~/git/PyTorch-gender-age-estimation/models-2020-11-20-14-37/best-epoch47-0.9314.onnx') + weights_file_path = rospy.get_param("~weights_file_path", default_weights_path) + img_size = rospy.get_param("~image_size", 64) + depth = rospy.get_param("~depth", 16) + width = rospy.get_param("~width", 8) + save_images = rospy.get_param("~save_images", True) + + save_images_folder = None + if save_images: + save_images_folder = rospy.get_param("~save_images_folder", "/tmp/image_recognition_keras") + except KeyError as e: + rospy.logerr("Parameter %s not found" % e) + sys.exit(1) + + try: + PytorchFaceProperties(weights_file_path, img_size, depth, width, save_images_folder) + updater = diagnostic_updater.Updater() + updater.setHardwareID("none") + updater.add(diagnostic_updater.Heartbeat()) + rospy.Timer(rospy.Duration(1), lambda event: updater.force_update()) + rospy.spin() + except Exception as e: + rospy.logfatal(e) diff --git a/image_recognition_pytorch/scripts/get_face_properties b/image_recognition_pytorch/scripts/get_face_properties new file mode 100755 index 00000000..29db1a98 --- /dev/null +++ b/image_recognition_pytorch/scripts/get_face_properties @@ -0,0 +1,26 @@ +#!/usr/bin/env python +from __future__ import print_function +import argparse +from image_recognition_pytorch.age_gender_estimator import AgeGenderEstimator +import cv2 +import os + +# Assign description to the help doc +parser = argparse.ArgumentParser(description='Get face properties using PyTorch') + +# Add arguments +parser.add_argument('image', type=str, help='Image') +parser.add_argument('--weights-path', type=str, help='Path to the weights of the WideResnet model', + default=os.path.expanduser('~/git/PyTorch-gender-age-estimation/models-2020-11-20-14-37/best-epoch47-0.9314.onnx')) +parser.add_argument('--image-size', type=int, help='Size of the input image', default=64) +parser.add_argument('--depth', type=int, help='Depth of the network', default=16) +parser.add_argument('--width', type=int, help='Width of the network', default=8) + +args = parser.parse_args() + +# Read the image +img = cv2.imread(args.image) + +estimator = AgeGenderEstimator(args.weights_path, args.image_size, args.depth, args.width) + +print(estimator.estimate([img])) diff --git a/image_recognition_pytorch/setup.py b/image_recognition_pytorch/setup.py new file mode 100644 index 00000000..924427d7 --- /dev/null +++ b/image_recognition_pytorch/setup.py @@ -0,0 +1,9 @@ +from setuptools import setup +from catkin_pkg.python_setup import generate_distutils_setup + +d = generate_distutils_setup( + packages=['image_recognition_pytorch'], + package_dir={'': 'src'} +) + +setup(**d) diff --git a/image_recognition_pytorch/src/image_recognition_pytorch/__init__.py b/image_recognition_pytorch/src/image_recognition_pytorch/__init__.py new file mode 100644 index 00000000..89c696a9 --- /dev/null +++ b/image_recognition_pytorch/src/image_recognition_pytorch/__init__.py @@ -0,0 +1 @@ +from . import age_gender_estimator diff --git a/image_recognition_pytorch/src/image_recognition_pytorch/age_gender_estimator.py b/image_recognition_pytorch/src/image_recognition_pytorch/age_gender_estimator.py new file mode 100644 index 00000000..e3c546be --- /dev/null +++ b/image_recognition_pytorch/src/image_recognition_pytorch/age_gender_estimator.py @@ -0,0 +1,49 @@ +import cv2 +import numpy as np +import os.path + +import onnxruntime + +GENDER_DICT = {0: 'male', 1: 'female'} + + +class AgeGenderEstimator(object): + def __init__(self, weights_file_path, img_size=64, depth=16, width=8): + """ + Estimate the age and gender of the incoming image + + :param weights_file_path: path to a pre-trained keras network + """ + weights_file_path = os.path.expanduser(weights_file_path) + + if not os.path.isfile(weights_file_path): + raise IOError("Weights file {}, no such file ..".format(weights_file_path)) + + self._model = None + self._weights_file_path = weights_file_path + self._img_size = img_size + self._depth = depth + self._width = width + + def estimate(self, np_images): + """ + Estimate the age and gender of the face on the image + + :param np_images a numpy array of BGR images of faces of which the gender and the age has to be estimated + This is assumed to be segmented/cropped already! + :returns List of estimated age and gender score ([female, male]) tuples + """ + + # Model should be constructed in same thread as the inference + if self._model is None: + self._model = onnxruntime.InferenceSession(self._weights_file_path) + + results = [] + for np_image in np_images: + inputs = np.transpose(cv2.resize(np_image, (64, 64)), (2, 0, 1)) + inputs = np.expand_dims(inputs, 0).astype(np.float32) / 255. + predictions = self._model.run(['output'], input_feed={'input': inputs})[0][0] + # age p(male) p(female) + results += [(predictions[2], (predictions[0], predictions[1]))] + + return results diff --git a/image_recognition_pytorch/test/assets/age_28_gender_female.jpg b/image_recognition_pytorch/test/assets/age_28_gender_female.jpg new file mode 100644 index 0000000000000000000000000000000000000000..96feaf3ee66e9e9576633027f5b2d615373b9490 GIT binary patch literal 9868 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 literal 0 HcmV?d00001 diff --git a/image_recognition_pytorch/test/assets/age_29_gender_male.jpg b/image_recognition_pytorch/test/assets/age_29_gender_male.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dab7b7fc0de33c23f551f3c3c92deb0b82b05d1a GIT binary patch literal 8515 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/dev/null 2>&1 && pwd )" diff --git a/image_recognition_pytorch/test/test_face_properties.py b/image_recognition_pytorch/test/test_face_properties.py new file mode 100644 index 00000000..5a81e423 --- /dev/null +++ b/image_recognition_pytorch/test/test_face_properties.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +from __future__ import print_function + +import os +import re +from future.moves.urllib.request import urlretrieve + +import cv2 +import rospkg +from image_recognition_keras.age_gender_estimator import AgeGenderEstimator + + +def test_face_properties(): + local_path = "/tmp/age_gender_weights.hdf5" + + if not os.path.exists(local_path): + http_path = "https://github.com/tue-robotics/image_recognition/releases/download/" \ + "image_recognition_keras_face_properties_weights.28-3.73/" \ + "image_recognition_keras_face_properties_weights.28-3.73.hdf5" + urlretrieve(http_path, local_path) + print("Downloaded weights to {}".format(local_path)) + + def age_is_female_from_asset_name(asset_name): + age_str, gender_str = re.search("age_(\d+)_gender_(\w+)", asset_name).groups() + return int(age_str), gender_str == "female" + + assets_path = os.path.join(rospkg.RosPack().get_path("image_recognition_keras"), 'test/assets') + images_gt = [(cv2.imread(os.path.join(assets_path, asset)), age_is_female_from_asset_name(asset)) + for asset in os.listdir(assets_path)] + + estimations = AgeGenderEstimator(local_path, 64, 16, 8).estimate([image for image, _ in images_gt]) + for (_, (age_gt, is_female_gt)), (age, gender) in zip(images_gt, estimations): + age = int(age) + is_female = gender[0] > 0.5 + assert abs(age - age_gt) < 5 + assert is_female == is_female_gt + + +if __name__ == "__main__": + test_face_properties() diff --git a/image_recognition_rqt/src/image_recognition_rqt/test.py b/image_recognition_rqt/src/image_recognition_rqt/test.py index 8e9efa1e..72f70687 100644 --- a/image_recognition_rqt/src/image_recognition_rqt/test.py +++ b/image_recognition_rqt/src/image_recognition_rqt/test.py @@ -2,10 +2,10 @@ import rosservice import rostopic from cv_bridge import CvBridge, CvBridgeError -from dialogs import option_dialog, warning_dialog, info_dialog +from .dialogs import option_dialog, warning_dialog, info_dialog from image_recognition_msgs.msg import CategoryProbability, FaceProperties from image_recognition_msgs.srv import GetFaceProperties, Recognize -from image_widget import ImageWidget +from .image_widget import ImageWidget from python_qt_binding.QtCore import * from python_qt_binding.QtGui import * from python_qt_binding.QtWidgets import * From 5762a398cb05a59548256c6c4b74dc9567d763e7 Mon Sep 17 00:00:00 2001 From: Loy van Beek Date: Sat, 5 Feb 2022 14:32:34 +0100 Subject: [PATCH 02/10] Remove python2 support, no need for it anymore --- image_recognition_pytorch/package.xml | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/image_recognition_pytorch/package.xml b/image_recognition_pytorch/package.xml index a60e551d..2f270d6b 100644 --- a/image_recognition_pytorch/package.xml +++ b/image_recognition_pytorch/package.xml @@ -13,31 +13,24 @@ catkin - python-setuptools - python3-setuptools + python3-setuptools diagnostic_updater image_recognition_msgs image_recognition_util - python-numpy - python3-numpy - python-opencv - python3-opencv + python3-numpy + python3-opencv + python3-onnxruntime-pip rospy - python-catkin-lint - python3-catkin-lint - python-future - python3-future - python-rospkg - python3-rospkg + python3-catkin-lint + python3-future + python3-rospkg - python-sphinx - python3-sphinx + python3-sphinx python-sphinx-autoapi-pip python-sphinx-rtd-theme-pip - python-yaml - python3-yaml + python3-yaml From 01480a0b2b73fad5fab4240b569c4808c457c9ad Mon Sep 17 00:00:00 2001 From: Loy van Beek Date: Sat, 5 Feb 2022 14:33:05 +0100 Subject: [PATCH 03/10] Download model/weights automagically --- image_recognition_pytorch/scripts/download_model | 12 ++++++------ .../scripts/face_properties_node | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/image_recognition_pytorch/scripts/download_model b/image_recognition_pytorch/scripts/download_model index 41f88012..841665b6 100755 --- a/image_recognition_pytorch/scripts/download_model +++ b/image_recognition_pytorch/scripts/download_model @@ -1,7 +1,7 @@ #!/usr/bin/env python from __future__ import print_function import os -import urllib +import urllib.request import argparse @@ -10,14 +10,14 @@ parser.add_argument('--model_path', default=os.path.expanduser('~/pytorch/models args = parser.parse_args() os.system('mkdir -p {}'.format(args.model_path)) -local_path = os.path.join(args.model_path, 'face_properties_weights.28-3.73.hdf5') +local_path = os.path.join(args.model_path, 'best-epoch47-0.9314.onnx') if not os.path.exists(local_path): - http_path = "https://github.com/tue-robotics/image_recognition/releases/download/" \ - "image_recognition_keras_face_properties_weights.28-3.73/" \ - "image_recognition_keras_face_properties_weights.28-3.73.hdf5" + # TODO: Clone this for us + http_path = "https://github.com/Nebula4869/PyTorch-gender-age-estimation/raw/" \ + "038331d26fc1fbf24d00365d0eb9d0e5e828dda6/models-2020-11-20-14-37/best-epoch47-0.9314.onnx" print("Downloading model to {} ...".format(local_path)) - urllib.urlretrieve(http_path, local_path) + urllib.request.urlretrieve(http_path, local_path) print("Model downloaded: {}".format(local_path)) else: print("Model already downloaded: {}".format(local_path)) diff --git a/image_recognition_pytorch/scripts/face_properties_node b/image_recognition_pytorch/scripts/face_properties_node index 50bbd6fe..11ff54f3 100755 --- a/image_recognition_pytorch/scripts/face_properties_node +++ b/image_recognition_pytorch/scripts/face_properties_node @@ -78,7 +78,7 @@ if __name__ == '__main__': rospy.init_node("face_properties") try: - default_weights_path = os.path.expanduser('~/git/PyTorch-gender-age-estimation/models-2020-11-20-14-37/best-epoch47-0.9314.onnx') + default_weights_path = os.path.expanduser('~/pytorch/models/best-epoch47-0.9314.onnx') weights_file_path = rospy.get_param("~weights_file_path", default_weights_path) img_size = rospy.get_param("~image_size", 64) depth = rospy.get_param("~depth", 16) From a66f0f662a4459fe9ba48a2172026ab859b90301 Mon Sep 17 00:00:00 2001 From: Loy van Beek Date: Sat, 5 Feb 2022 15:19:16 +0100 Subject: [PATCH 04/10] Fixup some typos --- image_recognition_pytorch/scripts/face_properties_node | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/image_recognition_pytorch/scripts/face_properties_node b/image_recognition_pytorch/scripts/face_properties_node index 11ff54f3..77849907 100755 --- a/image_recognition_pytorch/scripts/face_properties_node +++ b/image_recognition_pytorch/scripts/face_properties_node @@ -27,7 +27,7 @@ class PytorchFaceProperties: else: self._save_images_folder = None - rospy.loginfo("WideResnetFaceProperties node initialized:") + rospy.loginfo("PytorchFaceProperties node initialized:") rospy.loginfo(" - weights_file_path=%s", weights_file_path) rospy.loginfo(" - img_size=%s", img_size) rospy.loginfo(" - depth=%s", depth) @@ -87,7 +87,7 @@ if __name__ == '__main__': save_images_folder = None if save_images: - save_images_folder = rospy.get_param("~save_images_folder", "/tmp/image_recognition_keras") + save_images_folder = rospy.get_param("~save_images_folder", "/tmp/image_recognition_pytorch") except KeyError as e: rospy.logerr("Parameter %s not found" % e) sys.exit(1) From 6fdb9a3949e1b7febcf2ced2898694c8341da951 Mon Sep 17 00:00:00 2001 From: Loy van Beek Date: Sat, 5 Feb 2022 15:28:20 +0100 Subject: [PATCH 05/10] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2c93a3c4..517bb2c6 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Package | Build status Xenial Kinetic x64 | Description [image_recognition_msgs](https://github.com/tue-robotics/image_recognition/tree/master/image_recognition_msgs) | [![Build Status](http://build.ros.org/job/Ksrc_uX__image_recognition_msgs__ubuntu_xenial__source/1//badge/icon)](http://build.ros.org/job/Ksrc_uX__image_recognition_msgs__ubuntu_xenial__source/1/) | Interface definition for image recognition [image_recognition_openface](https://github.com/tue-robotics/image_recognition/tree/master/image_recognition_openface) | [![Build Status](http://build.ros.org/job/Ksrc_uX__image_recognition_openface__ubuntu_xenial__source/1//badge/icon)](http://build.ros.org/job/Ksrc_uX__image_recognition_openface__ubuntu_xenial__source/1/) | ROS wrapper for Openface (https://github.com/cmusatyalab/openface) to detect and recognize faces in images. [image_recognition_openpose](https://github.com/tue-robotics/image_recognition/tree/master/image_recognition_openpose) | [![Build Status](http://build.ros.org/job/Ksrc_uX__image_recognition_openpose__ubuntu_xenial__source/1//badge/icon)](http://build.ros.org/job/Ksrc_uX__image_recognition_openpose_ubuntu_xenial__source/1/) | ROS wrapper for Openpose (https://github.com/CMU-Perceptual-Computing-Lab/) for getting poses of 2D images. +[image_recognition_pytorch](https://github.com/tue-robotics/image_recognition/tree/master/image_recognition_pytorch) | [![Build Status](http://build.ros.org/job/Ksrc_uX__image_recognition_pytorch__ubuntu_xenial__source/1//badge/icon)](http://build.ros.org/job/Ksrc_uX__image_recognition_pytorch_ubuntu_xenial__source/1/) | ROS wrapper around a PyTorch model for (https://github.com/Nebula4869/PyTorch-gender-age-estimation) for getting age & gender estimations on faces [image_recognition_rqt](https://github.com/tue-robotics/image_recognition/tree/master/image_recognition_rqt) | [![Build Status](http://build.ros.org/job/Ksrc_uX__image_recognition_rqt__ubuntu_xenial__source/1//badge/icon)](http://build.ros.org/job/Ksrc_uX__image_recognition_rqt__ubuntu_xenial__source/1/) | RQT tools with helpers testing this interface and training/labeling data. [image_recognition_skybiometry](https://github.com/tue-robotics/image_recognition/tree/master/image_recognition_skybiometry) | [![Build Status](http://build.ros.org/job/Ksrc_uX__image_recognition_skybiometry__ubuntu_xenial__source/1//badge/icon)](http://build.ros.org/job/Ksrc_uX__image_recognition_skybiometry_ubuntu_xenial__source/1/) | ROS wrapper for Skybiometry (https://skybiometry.com/) for getting face properties of a detected face, e.g. age estimation, gender estimation etc. [image_recognition_tensorflow](https://github.com/tue-robotics/image_recognition/tree/master/image_recognition_tensorflow) | [![Build Status](http://build.ros.org/job/Ksrc_uX__image_recognition_tensorflow__ubuntu_xenial__source/1//badge/icon)](http://build.ros.org/job/Ksrc_uX__image_recognition_tensorflow__ubuntu_xenial__source/1/) | Object recognition with use of Tensorflow. The user can retrain the top layers of a neural network to perform classification with its own dataset as described in [this tutorial](https://www.tensorflow.org/versions/r0.11/how_tos/image_retraining/index.html). From e68be5b76fd0cd18bb4cea41c1ef09f0aa8120e9 Mon Sep 17 00:00:00 2001 From: Arpit Aggarwal Date: Tue, 22 Feb 2022 20:50:37 +0100 Subject: [PATCH 06/10] docs: Update README with right package --- image_recognition_pytorch/README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/image_recognition_pytorch/README.md b/image_recognition_pytorch/README.md index 4a9c4f7c..ed285e19 100644 --- a/image_recognition_pytorch/README.md +++ b/image_recognition_pytorch/README.md @@ -1,6 +1,6 @@ -# Keras image recognition +# Image recognition pytorch -Image recognition with use of PyTorch. +Image recognition (age and gender estimation of a face) with use of PyTorch. ## Installation @@ -10,7 +10,7 @@ See https://github.com/tue-robotics/image_recognition for installation instructi Age and gender estimation ``` -rosrun image_recognition_keras face_properties_node _weights_file_path:=[path_to_model] +rosrun image_recognition_pytorch face_properties_node _weights_file_path:=[path_to_model] ``` Run the image_recognition_rqt test gui (https://github.com/tue-robotics/image_recognition_rqt) @@ -48,6 +48,3 @@ rosrun image_recognition_pytorch get_face_properties `rospack find image_recogni Output: [(50.5418073660112, array([0.5845756 , 0.41542447], dtype=float32))] - -## Troubleshooting - From 20a7c7a0f4a509ede9380f991f5ea19d1abbe453 Mon Sep 17 00:00:00 2001 From: Arpit Aggarwal Date: Tue, 22 Feb 2022 21:06:55 +0100 Subject: [PATCH 07/10] build(image_recognition_pytorch): Add missing dependency --- image_recognition_pytorch/package.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/image_recognition_pytorch/package.xml b/image_recognition_pytorch/package.xml index 2f270d6b..5b1566c1 100644 --- a/image_recognition_pytorch/package.xml +++ b/image_recognition_pytorch/package.xml @@ -21,6 +21,7 @@ python3-numpy python3-opencv python3-onnxruntime-pip + python3-pytorch-pip rospy python3-catkin-lint From 71abe78bf66a54ee22c3666cccd3cc0c255db453 Mon Sep 17 00:00:00 2001 From: Arpit Aggarwal Date: Tue, 22 Feb 2022 21:09:37 +0100 Subject: [PATCH 08/10] fix(image_recognition_pytorch): Fix default model path --- image_recognition_pytorch/scripts/download_model | 2 +- image_recognition_pytorch/scripts/face_properties_node | 2 +- image_recognition_pytorch/scripts/get_face_properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/image_recognition_pytorch/scripts/download_model b/image_recognition_pytorch/scripts/download_model index 841665b6..329484f7 100755 --- a/image_recognition_pytorch/scripts/download_model +++ b/image_recognition_pytorch/scripts/download_model @@ -6,7 +6,7 @@ import urllib.request import argparse parser = argparse.ArgumentParser() -parser.add_argument('--model_path', default=os.path.expanduser('~/pytorch/models')) +parser.add_argument('--model_path', default=os.path.expanduser('~/data/pytorch_models')) args = parser.parse_args() os.system('mkdir -p {}'.format(args.model_path)) diff --git a/image_recognition_pytorch/scripts/face_properties_node b/image_recognition_pytorch/scripts/face_properties_node index 77849907..6de5cac5 100755 --- a/image_recognition_pytorch/scripts/face_properties_node +++ b/image_recognition_pytorch/scripts/face_properties_node @@ -78,7 +78,7 @@ if __name__ == '__main__': rospy.init_node("face_properties") try: - default_weights_path = os.path.expanduser('~/pytorch/models/best-epoch47-0.9314.onnx') + default_weights_path = os.path.expanduser('~/data/pytorch_models/best-epoch47-0.9314.onnx') weights_file_path = rospy.get_param("~weights_file_path", default_weights_path) img_size = rospy.get_param("~image_size", 64) depth = rospy.get_param("~depth", 16) diff --git a/image_recognition_pytorch/scripts/get_face_properties b/image_recognition_pytorch/scripts/get_face_properties index 29db1a98..0b9cee82 100755 --- a/image_recognition_pytorch/scripts/get_face_properties +++ b/image_recognition_pytorch/scripts/get_face_properties @@ -11,7 +11,7 @@ parser = argparse.ArgumentParser(description='Get face properties using PyTorch' # Add arguments parser.add_argument('image', type=str, help='Image') parser.add_argument('--weights-path', type=str, help='Path to the weights of the WideResnet model', - default=os.path.expanduser('~/git/PyTorch-gender-age-estimation/models-2020-11-20-14-37/best-epoch47-0.9314.onnx')) + default=os.path.expanduser('~/data/pytorch_models/best-epoch47-0.9314.onnx')) parser.add_argument('--image-size', type=int, help='Size of the input image', default=64) parser.add_argument('--depth', type=int, help='Depth of the network', default=16) parser.add_argument('--width', type=int, help='Width of the network', default=8) From 369f91667d51725a4660dcee1846306a7e8a529d Mon Sep 17 00:00:00 2001 From: Arpit Aggarwal Date: Tue, 22 Feb 2022 21:10:58 +0100 Subject: [PATCH 09/10] feat(image_recognition_pytorch): Add support for GPU --- .../scripts/face_properties_node | 8 +++++--- .../age_gender_estimator.py | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/image_recognition_pytorch/scripts/face_properties_node b/image_recognition_pytorch/scripts/face_properties_node index 6de5cac5..b28c3310 100755 --- a/image_recognition_pytorch/scripts/face_properties_node +++ b/image_recognition_pytorch/scripts/face_properties_node @@ -12,13 +12,13 @@ from image_recognition_util import image_writer class PytorchFaceProperties: - def __init__(self, weights_file_path, img_size, depth, width, save_images_folder): + def __init__(self, weights_file_path, img_size, depth, width, save_images_folder, use_gpu): """ ROS node that wraps the PyTorch age gender estimator """ self._bridge = CvBridge() self._properties_srv = rospy.Service('get_face_properties', GetFaceProperties, self._get_face_properties_srv) - self._estimator = AgeGenderEstimator(weights_file_path, img_size, depth, width) + self._estimator = AgeGenderEstimator(weights_file_path, img_size, depth, width, use_gpu) if save_images_folder: self._save_images_folder = os.path.expanduser(save_images_folder) @@ -33,6 +33,7 @@ class PytorchFaceProperties: rospy.loginfo(" - depth=%s", depth) rospy.loginfo(" - width=%s", width) rospy.loginfo(" - save_images_folder=%s", save_images_folder) + rospy.loginfo(" - use_gpu=%s", use_gpu) def _get_face_properties_srv(self, req): """ @@ -84,6 +85,7 @@ if __name__ == '__main__': depth = rospy.get_param("~depth", 16) width = rospy.get_param("~width", 8) save_images = rospy.get_param("~save_images", True) + use_gpu = rospy.get_param("~use_gpu", False) save_images_folder = None if save_images: @@ -93,7 +95,7 @@ if __name__ == '__main__': sys.exit(1) try: - PytorchFaceProperties(weights_file_path, img_size, depth, width, save_images_folder) + PytorchFaceProperties(weights_file_path, img_size, depth, width, save_images_folder, use_gpu) updater = diagnostic_updater.Updater() updater.setHardwareID("none") updater.add(diagnostic_updater.Heartbeat()) diff --git a/image_recognition_pytorch/src/image_recognition_pytorch/age_gender_estimator.py b/image_recognition_pytorch/src/image_recognition_pytorch/age_gender_estimator.py index e3c546be..62cc9d22 100644 --- a/image_recognition_pytorch/src/image_recognition_pytorch/age_gender_estimator.py +++ b/image_recognition_pytorch/src/image_recognition_pytorch/age_gender_estimator.py @@ -8,11 +8,11 @@ class AgeGenderEstimator(object): - def __init__(self, weights_file_path, img_size=64, depth=16, width=8): + def __init__(self, weights_file_path, img_size=64, depth=16, width=8, use_gpu=False): """ Estimate the age and gender of the incoming image - :param weights_file_path: path to a pre-trained keras network + :param weights_file_path: path to a pre-trained network in onnx format """ weights_file_path = os.path.expanduser(weights_file_path) @@ -24,6 +24,7 @@ def __init__(self, weights_file_path, img_size=64, depth=16, width=8): self._img_size = img_size self._depth = depth self._width = width + self._use_gpu = use_gpu def estimate(self, np_images): """ @@ -36,7 +37,18 @@ def estimate(self, np_images): # Model should be constructed in same thread as the inference if self._model is None: - self._model = onnxruntime.InferenceSession(self._weights_file_path) + providers = ['CPUExecutionProvider'] + if self._use_gpu: + providers.append( + ('CUDAExecutionProvider', { + 'device_id': 0, + 'arena_extend_strategy': 'kNextPowerOfTwo', + 'gpu_mem_limit': 2 * 1024 * 1024 * 1024, + 'cudnn_conv_algo_search': 'EXHAUSTIVE', + 'do_copy_in_default_stream': True, + })), + + self._model = onnxruntime.InferenceSession(self._weights_file_path, providers=providers) results = [] for np_image in np_images: From 486dc6a60e7ef5b69886dc51b3455f065ed91b9d Mon Sep 17 00:00:00 2001 From: Arpit Aggarwal Date: Tue, 22 Feb 2022 21:12:43 +0100 Subject: [PATCH 10/10] feat(image_recognition_rqt): Fix python3 imports --- image_recognition_rqt/src/image_recognition_rqt/annotation.py | 4 ++-- image_recognition_rqt/src/image_recognition_rqt/manual.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/image_recognition_rqt/src/image_recognition_rqt/annotation.py b/image_recognition_rqt/src/image_recognition_rqt/annotation.py index 5eeedba8..45ae5a81 100644 --- a/image_recognition_rqt/src/image_recognition_rqt/annotation.py +++ b/image_recognition_rqt/src/image_recognition_rqt/annotation.py @@ -13,8 +13,8 @@ import re import rosservice -from image_widget import ImageWidget -from dialogs import option_dialog, warning_dialog +from .image_widget import ImageWidget +from .dialogs import option_dialog, warning_dialog from image_recognition_msgs.msg import Annotation from image_recognition_util import image_writer diff --git a/image_recognition_rqt/src/image_recognition_rqt/manual.py b/image_recognition_rqt/src/image_recognition_rqt/manual.py index a8f63deb..59f7197b 100644 --- a/image_recognition_rqt/src/image_recognition_rqt/manual.py +++ b/image_recognition_rqt/src/image_recognition_rqt/manual.py @@ -12,8 +12,8 @@ from cv_bridge import CvBridge, CvBridgeError from image_recognition_msgs.msg import CategoryProbability, Recognition -from image_widget import ImageWidget -from dialogs import option_dialog, warning_dialog +from .image_widget import ImageWidget +from .dialogs import option_dialog, warning_dialog from image_recognition_msgs.srv import Recognize, RecognizeResponse import re