From e2857cd0ac6bb9e230afe4eaee1e5d1e753de2b6 Mon Sep 17 00:00:00 2001 From: Samuel Felton Date: Mon, 13 May 2024 15:52:49 +0200 Subject: [PATCH 01/12] Add publicists in python generator to expose protected members to Python --- .../bindings/include/visual_feature.hpp | 49 +++++++++++++++++++ modules/python/config/visual_features.json | 4 ++ .../generator/visp_python_bindgen/header.py | 28 +++++++++-- .../visp_python_bindgen/submodule.py | 10 ++-- .../generator/visp_python_bindgen/utils.py | 13 +++++ 5 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 modules/python/bindings/include/visual_feature.hpp diff --git a/modules/python/bindings/include/visual_feature.hpp b/modules/python/bindings/include/visual_feature.hpp new file mode 100644 index 0000000000..ed21adb2ab --- /dev/null +++ b/modules/python/bindings/include/visual_feature.hpp @@ -0,0 +1,49 @@ +/* + * ViSP, open source Visual Servoing Platform software. + * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * + * This software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * See the file LICENSE.txt at the root directory of this source + * distribution for additional information about the GNU GPL. + * + * For using ViSP with software that can not be combined with the GNU + * GPL, please contact Inria about acquiring a ViSP Professional + * Edition License. + * + * See https://visp.inria.fr for more information. + * + * This software was developed at: + * Inria Rennes - Bretagne Atlantique + * Campus Universitaire de Beaulieu + * 35042 Rennes Cedex + * France + * + * If you have questions regarding the use of this file, please contact + * Inria at visp@inria.fr + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + * + * Description: + * Python bindings. + */ + +#ifndef VISP_PYTHON_VISUAL_FEATURES_HPP +#define VISP_PYTHON_VISUAL_FEATURES_HPP + +#include + +class TrampolineBasicFeatures : public vpBasicFeature +{ +public: + + +}; + + + + +#endif diff --git a/modules/python/config/visual_features.json b/modules/python/config/visual_features.json index 8e345d95d6..cd1e340a85 100644 --- a/modules/python/config/visual_features.json +++ b/modules/python/config/visual_features.json @@ -3,6 +3,10 @@ "vpFeatureException.h" ], "classes": { + "vpBasicFeature": { + "trampoline": "PyBasicFeature", + "use_publicist": true + }, "vpGenericFeature": { "methods": [ { diff --git a/modules/python/generator/visp_python_bindgen/header.py b/modules/python/generator/visp_python_bindgen/header.py index b278c1035f..049179d895 100644 --- a/modules/python/generator/visp_python_bindgen/header.py +++ b/modules/python/generator/visp_python_bindgen/header.py @@ -501,29 +501,47 @@ def add_method_doc_to_pyargs(method: types.Method, py_arg_strs: List[str]) -> Li logging.error(error_generating_overloads) raise RuntimeError('Error generating overloads:\n' + '\n'.join(error_generating_overloads)) + # Generate members + + # Publicist "pattern": expose protected attributes to python via a derived class. + # See: https://pybind11.readthedocs.io/en/stable/advanced/classes.html#binding-protected-member-functions + use_publicist = cls_config['use_publicist'] + publicist_name = f'Publicist{name_python}' + publicist_str = None + if use_publicist: + publicist_str = f'class {publicist_name}: public {name_cpp} {{\n' + publicist_str += 'public:\n' + + field_dict = {} for field in cls.fields: if field.name in cls_config['ignored_attributes']: logging.info(f'Ignoring field in class/struct {name_cpp}: {field.name}') continue - if field.access == 'public': + if field.access == 'public' or (field.access == 'protected' and use_publicist): if is_unsupported_argument_type(field.type): continue field_type = get_type(field.type, owner_specs, header_env.mapping) - logging.info(f'Found field in class/struct {name_cpp}: {field_type} {field.name}') - field_name_python = field.name.lstrip('m_') + logging.info(f'Found field in class/struct {name_cpp}: {field_type} {field.name}') def_str = 'def_' def_str += 'readonly' if field.type.const else 'readwrite' if field.static: def_str += '_static' - field_str = f'{python_ident}.{def_str}("{field_name_python}", &{name_cpp}::{field.name});' + field_exposing_class = name_cpp if field.access == 'public' else publicist_name + + if field.access == 'protected': + publicist_str += f'\tusing {name_cpp}::{field.name};\n' + + field_str = f'{python_ident}.{def_str}("{field_name_python}", &{field_exposing_class}::{field.name});' field_dict[field_name_python] = field_str - classs_binding_defs = ClassBindingDefinitions(field_dict, methods_dict) + if use_publicist: + publicist_str += '};' + classs_binding_defs = ClassBindingDefinitions(field_dict, methods_dict, publicist_str) bindings_container.add_bindings(SingleObjectBindings(class_def_names, class_decl, classs_binding_defs, GenerationObjectType.Class)) name_cpp_no_template = '::'.join([seg.name for seg in cls.class_decl.typename.segments]) diff --git a/modules/python/generator/visp_python_bindgen/submodule.py b/modules/python/generator/visp_python_bindgen/submodule.py index ee7b61bf8d..5139aaa9de 100644 --- a/modules/python/generator/visp_python_bindgen/submodule.py +++ b/modules/python/generator/visp_python_bindgen/submodule.py @@ -145,6 +145,7 @@ def generate(self) -> None: header.generate_binding_code(module_bindings) includes.extend(header.includes) submodule_declaration = f'py::module_ submodule = m.def_submodule("{self.name}");\n' + publicists = module_bindings.get_publicists() bindings = module_bindings.get_definitions() declarations = module_bindings.get_declarations() user_defined_headers = '\n'.join(self.get_user_defined_headers()) @@ -164,6 +165,7 @@ def generate(self) -> None: /*User-defined headers (e.g. additional bindings)*/ {user_defined_headers} + /*Required headers that are not retrieved in submodule headers (e.g. there are forward definitions but no includes) */ {additional_required_headers} /*Submodule headers*/ @@ -171,6 +173,8 @@ def generate(self) -> None: namespace py = pybind11; +{publicists} + void {self.generation_function_name()}(py::module_ &m) {{ py::options options; options.disable_enum_members_docstring(); @@ -231,7 +235,9 @@ def get_class_config(self, class_name: str) -> Optional[Dict]: 'use_buffer_protocol': False, 'additional_bindings': None, 'ignore_repr': False, - 'is_virtual': False + 'is_virtual': False, + 'use_publicist': False, + 'trampoline': None } if 'classes' not in self.config: return default_config @@ -274,8 +280,6 @@ def get_method_config(self, class_name: Optional[str], method, owner_specs, head if method_matches_config(method, function_config, owner_specs, header_mapping): res.update(function_config) return res - - #import sys; sys.exit() return res def get_submodules(config_path: Path, generate_path: Path) -> List[Submodule]: diff --git a/modules/python/generator/visp_python_bindgen/utils.py b/modules/python/generator/visp_python_bindgen/utils.py index 1eeeb9140c..eddc1922aa 100644 --- a/modules/python/generator/visp_python_bindgen/utils.py +++ b/modules/python/generator/visp_python_bindgen/utils.py @@ -101,6 +101,8 @@ class ClassBindingDefinitions: ''' fields: Dict[str, str] methods: Dict[str, List[MethodBinding]] # Mapping from python method name to the bindings definitions. There can be overloads + publicist: Optional[str] # Additional class to expose protected members of this class + @dataclass class SingleObjectBindings: @@ -132,6 +134,17 @@ def get_declarations(self) -> str: decls.append(sob.declaration) return '\n'.join(decls) + def get_publicists(self) -> str: + publicists = [] + for sob in self.object_bindings: + odefs = sob.definitions + if isinstance(odefs, ClassBindingDefinitions): + if odefs.publicist is not None: + publicists.append(odefs.publicist) + + return '\n'.join(publicists) + + def get_definitions(self) -> str: defs = [] for sob in self.object_bindings: From 43fc280489baa129ab1e3931e841d7eb5fd72782 Mon Sep 17 00:00:00 2001 From: Samuel Felton Date: Mon, 13 May 2024 17:59:58 +0200 Subject: [PATCH 02/12] Inheritance for BasicFeature --- .../bindings/include/visual_feature.hpp | 49 ------- .../bindings/include/visual_features.hpp | 126 ++++++++++++++++++ modules/python/config/visual_features.json | 3 +- .../python/examples/yolo-centering-task.py | 17 +++ .../generator/visp_python_bindgen/header.py | 9 +- 5 files changed, 153 insertions(+), 51 deletions(-) delete mode 100644 modules/python/bindings/include/visual_feature.hpp create mode 100644 modules/python/bindings/include/visual_features.hpp create mode 100644 modules/python/examples/yolo-centering-task.py diff --git a/modules/python/bindings/include/visual_feature.hpp b/modules/python/bindings/include/visual_feature.hpp deleted file mode 100644 index ed21adb2ab..0000000000 --- a/modules/python/bindings/include/visual_feature.hpp +++ /dev/null @@ -1,49 +0,0 @@ -/* - * ViSP, open source Visual Servoing Platform software. - * Copyright (C) 2005 - 2023 by Inria. All rights reserved. - * - * This software is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * See the file LICENSE.txt at the root directory of this source - * distribution for additional information about the GNU GPL. - * - * For using ViSP with software that can not be combined with the GNU - * GPL, please contact Inria about acquiring a ViSP Professional - * Edition License. - * - * See https://visp.inria.fr for more information. - * - * This software was developed at: - * Inria Rennes - Bretagne Atlantique - * Campus Universitaire de Beaulieu - * 35042 Rennes Cedex - * France - * - * If you have questions regarding the use of this file, please contact - * Inria at visp@inria.fr - * - * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE - * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. - * - * Description: - * Python bindings. - */ - -#ifndef VISP_PYTHON_VISUAL_FEATURES_HPP -#define VISP_PYTHON_VISUAL_FEATURES_HPP - -#include - -class TrampolineBasicFeatures : public vpBasicFeature -{ -public: - - -}; - - - - -#endif diff --git a/modules/python/bindings/include/visual_features.hpp b/modules/python/bindings/include/visual_features.hpp new file mode 100644 index 0000000000..888e8b7951 --- /dev/null +++ b/modules/python/bindings/include/visual_features.hpp @@ -0,0 +1,126 @@ +/* + * ViSP, open source Visual Servoing Platform software. + * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * + * This software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * See the file LICENSE.txt at the root directory of this source + * distribution for additional information about the GNU GPL. + * + * For using ViSP with software that can not be combined with the GNU + * GPL, please contact Inria about acquiring a ViSP Professional + * Edition License. + * + * See https://visp.inria.fr for more information. + * + * This software was developed at: + * Inria Rennes - Bretagne Atlantique + * Campus Universitaire de Beaulieu + * 35042 Rennes Cedex + * France + * + * If you have questions regarding the use of this file, please contact + * Inria at visp@inria.fr + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + * + * Description: + * Python bindings. + */ + +#ifndef VISP_PYTHON_VISUAL_FEATURES_HPP +#define VISP_PYTHON_VISUAL_FEATURES_HPP + +#include + +#include + + +class TrampolineBasicFeature : public vpBasicFeature +{ +public: + using vpBasicFeature::vpBasicFeature; + + TrampolineBasicFeature() : vpBasicFeature() { } + TrampolineBasicFeature(const vpBasicFeature &f) : vpBasicFeature(f) { } + + + virtual void display(const vpCameraParameters &cam, const vpImage &I, + const vpColor &color = vpColor::green, unsigned int thickness = 1) const vp_override + { + PYBIND11_OVERRIDE_PURE( + void, /* Return type */ + vpBasicFeature, /* Parent class */ + display, /* Name of function in C++ (must match Python name) */ + cam, I, color, thickness + ); + } + virtual void display(const vpCameraParameters &cam, const vpImage &I, const vpColor &color = vpColor::green, + unsigned int thickness = 1) const vp_override + { + PYBIND11_OVERRIDE_PURE( + void, /* Return type */ + vpBasicFeature, /* Parent class */ + display, /* Name of function in C++ (must match Python name) */ + cam, I, color, thickness + ); + } + + virtual void init() vp_override + { + PYBIND11_OVERRIDE_PURE( + void, /* Return type */ + vpBasicFeature, /* Parent class */ + init, /* Name of function in C++ (must match Python name) */ // Intended comma at the end + ); + } + + virtual vpColVector error(const vpBasicFeature &s_star, unsigned int select = FEATURE_ALL) vp_override + { + PYBIND11_OVERRIDE( + vpColVector, /* Return type */ + vpBasicFeature, /* Parent class */ + error, /* Name of function in C++ (must match Python name) */ + s_star, /* Argument(s) */ + select + ); + } + + + virtual vpMatrix interaction(unsigned int select = FEATURE_ALL) vp_override + { + PYBIND11_OVERRIDE_PURE( + vpMatrix, /* Return type */ + vpBasicFeature, /* Parent class */ + interaction, /* Name of function in C++ (must match Python name) */ + select /* Argument(s) */ + ); + } + virtual void print(unsigned int select = FEATURE_ALL) const vp_override + { + PYBIND11_OVERRIDE_PURE( + void, /* Return type */ + vpBasicFeature, /* Parent class */ + print, /* Name of function in C++ (must match Python name) */ + select /* Argument(s) */ + ); + } + + virtual vpBasicFeature *duplicate() const vp_override + { + PYBIND11_OVERRIDE_PURE( + vpBasicFeature *, /* Return type */ + vpBasicFeature, /* Parent class */ + duplicate, /* Name of function in C++ (must match Python name) */ // Intended comma at the end + ); + } + +}; + + + + +#endif diff --git a/modules/python/config/visual_features.json b/modules/python/config/visual_features.json index cd1e340a85..2266e89561 100644 --- a/modules/python/config/visual_features.json +++ b/modules/python/config/visual_features.json @@ -2,9 +2,10 @@ "ignored_headers": [ "vpFeatureException.h" ], + "user_defined_headers": ["visual_features.hpp"], "classes": { "vpBasicFeature": { - "trampoline": "PyBasicFeature", + "trampoline": "TrampolineBasicFeature", "use_publicist": true }, "vpGenericFeature": { diff --git a/modules/python/examples/yolo-centering-task.py b/modules/python/examples/yolo-centering-task.py new file mode 100644 index 0000000000..bd3cec6728 --- /dev/null +++ b/modules/python/examples/yolo-centering-task.py @@ -0,0 +1,17 @@ + + +from visp.core import ColVector +from visp.visual_features import BasicFeature + +class FeatureCNNRep(BasicFeature): + def __init__(self): + BasicFeature.__init__(self) + + def error(self, s_star: 'FeatureCNNRep', select: int) -> ColVector: + return ColVector(0) + + +if __name__ == '__main__': + s = FeatureCNNRep() + sd = FeatureCNNRep() + e = s.error(sd, 0) diff --git a/modules/python/generator/visp_python_bindgen/header.py b/modules/python/generator/visp_python_bindgen/header.py index 049179d895..0b8d543ca8 100644 --- a/modules/python/generator/visp_python_bindgen/header.py +++ b/modules/python/generator/visp_python_bindgen/header.py @@ -310,6 +310,13 @@ def add_method_doc_to_pyargs(method: types.Method, py_arg_strs: List[str]) -> Li # Reference public base classes when creating pybind class binding base_class_strs = list(map(lambda base_class: get_typename(base_class.typename, owner_specs, header_env.mapping), filter(lambda b: b.access == 'public', cls.class_decl.bases))) + + # Add trampoline class if defined + # Trampoline classes allow classes defined in Python to override virtual methods declared in C++ + # For now (and probably forever?) trampolines should be defined by hand. + trampoline_name = cls_config['trampoline'] + if trampoline_name is not None: + base_class_strs.append(trampoline_name) # py::class template contains the class, its holder type, and its base clases. # The default holder type is std::unique_ptr. when the cpp function argument is a shared_ptr, Pybind will raise an error when calling the method. py_class_template_str = ', '.join([name_cpp, f'std::shared_ptr<{name_cpp}>'] + base_class_strs) @@ -354,7 +361,7 @@ def add_method_doc_to_pyargs(method: types.Method, py_arg_strs: List[str]) -> Li operators, basic_methods = split_methods_with_config(non_constructors, lambda m: get_name(m.name) in cpp_operator_names) # Constructors definitions - if not contains_pure_virtual_methods: + if not contains_pure_virtual_methods or trampoline_name is not None: for method, method_config in constructors: method_name = get_name(method.name) params_strs = [get_type(param.type, owner_specs, header_env.mapping) for param in method.parameters] From d4aa8a4ee4c19b1e9953208017fdacc4a968b2df Mon Sep 17 00:00:00 2001 From: Samuel Felton Date: Tue, 14 May 2024 13:27:24 +0200 Subject: [PATCH 03/12] Finish centering task example --- .../python/examples/yolo-centering-task.py | 177 +++++++++++++++++- 1 file changed, 168 insertions(+), 9 deletions(-) diff --git a/modules/python/examples/yolo-centering-task.py b/modules/python/examples/yolo-centering-task.py index bd3cec6728..58647fffef 100644 --- a/modules/python/examples/yolo-centering-task.py +++ b/modules/python/examples/yolo-centering-task.py @@ -1,17 +1,176 @@ -from visp.core import ColVector -from visp.visual_features import BasicFeature +from typing import Optional, Tuple +from visp.core import ColVector, Point, Color, PixelMeterConversion, Display +from visp.core import CameraParameters, HomogeneousMatrix , ExponentialMap, PoseVector -class FeatureCNNRep(BasicFeature): +from visp.core import ImageRGBa +from visp.robot import ImageSimulator +from visp.visual_features import BasicFeature, FeaturePoint +from visp.vs import Servo +from visp.gui import DisplayOpenCV + +try: + from ultralytics import YOLO +except ImportError: + print('This example requires yoloV8: pip install ultralytics') + +import time +from PIL import Image +import numpy as np +import matplotlib.pyplot as plt +from matplotlib import animation + +plt.rcParams['text.usetex'] = True + +def get_simulator(scene_image: ImageRGBa) -> ImageSimulator: + simulator = ImageSimulator() + l = 1.5 + L = 1 + scene_3d = [ + [-l, -L, 0.0], + [l, -L, 0.0], + [l, L, 0.0], + [-l, L, 0.0], + ] + simulator.init(scene_image, list(map(lambda X: Point(X), scene_3d))) + simulator.setCleanPreviousImage(True, color=Color.black) + simulator.setInterpolationType(ImageSimulator.InterpolationType.BILINEAR_INTERPOLATION) + return simulator + +class VSPlot(object): def __init__(self): - BasicFeature.__init__(self) + self.v = [] + self.error = [] + self.r = [] + self.I = [] - def error(self, s_star: 'FeatureCNNRep', select: int) -> ColVector: - return ColVector(0) + def on_iter(self, Idisp: ImageRGBa, v: ColVector, error: ColVector, cTw: HomogeneousMatrix) -> None: + self.I.append(Idisp) + self.v.append(v.numpy()[3:5].flatten()) + self.error.append(error.numpy().flatten()) + self.r.append(PoseVector(cTw).numpy()[3:5].flatten()) + def generate_anim(self): + self.error = np.asarray(self.error)[1:] + self.v = np.asarray(self.v)[1:] + self.r = np.asarray(self.r)[1:] + + + fig, axs = plt.subplots(2, 2, figsize=(15, 15 * (self.I[0].getHeight() / self.I[0].getWidth()))) + axs = [axs[0][0], axs[0][1], axs[1][0],axs[1][1]] + titles = ['I', 'Feature error', 'Velocity', 'Pose'] + legends = [ + None, + [r"$x$", r"$y$"], + [r"$\mathbf{\upsilon}_x$", r"$\mathbf{\upsilon}_y$"], + [r"$\theta\mathbf{u}_x$", r"$\theta\mathbf{u}_y$"], + ] + data = [None, self.error, self.v, self.r] + artists = [] + for i in range(len(axs)): + axs[i].set_title(titles[i]) + if data[i] is not None: + axs[i].set_xlabel('Iteration') + axs[i].grid() + axs[i].set_xlim(0, len(self.v)) + min_val, max_val = np.min(data[i]), np.max(data[i]) + margin = (max_val - min_val) * 0.05 + axs[i].set_ylim(min_val - margin, max_val + margin) + artists.append(axs[i].plot(data[i])) + axs[i].legend(legends[i]) + else: + artists.append(axs[i].imshow(self.I[0])) + axs[i].set_axis_off() + plt.tight_layout() + def animate(i): + xs = range(i) + artists[0].set_data(self.I[i]) + for j in range(2): + artists[1][j].set_data(xs, self.error[:i, j]) + artists[2][j].set_data(xs, self.v[:i, j]) + artists[3][j].set_data(xs, self.r[:i, j]) + return artists + + anim = animation.FuncAnimation(fig, animate, frames=len(self.v), interval=30, blit=False, repeat=False) + writervideo = animation.FFMpegWriter(fps=30) + anim.save('exp.mp4', writer=writervideo) + plt.savefig('exp.pdf') + plt.close() if __name__ == '__main__': - s = FeatureCNNRep() - sd = FeatureCNNRep() - e = s.error(sd, 0) + h, w = 480, 640 + Z = 5.0 + cam = CameraParameters(px=600, py=600, u0=w / 2.0, v0=h / 2.0) + detection_model = YOLO('yolov8n.pt') + # Initialize simulator + scene_image = np.asarray(Image.open('/mnt/d/Downloads/car-img.jpg')) + scene_image = np.concatenate((scene_image, np.ones_like(scene_image[..., 0:1]) * 255), axis=-1) + scene_image = ImageRGBa(scene_image) + simulator = get_simulator(scene_image) + + plotter = VSPlot() + + cTw = HomogeneousMatrix(-1.0, 0.5, Z, 0.0, 0.0, 0.0) + I = ImageRGBa(h, w) + Idisp = ImageRGBa(h, w) + + simulator.setCameraPosition(cTw) + simulator.getImage(I, cam) + + s = FeaturePoint() + s.buildFrom(0.0, 0.0, Z) + # Define centering task + xd, yd = PixelMeterConversion.convertPoint(cam, w / 2.0, h / 2.0) + sd = FeaturePoint() + sd.buildFrom(xd, yd, Z) + + task = Servo() + task.addFeature(s, sd) + task.setLambda(0.5) + task.setCameraDoF(ColVector([0, 0, 0, 1, 1, 0])) + task.setServo(Servo.ServoType.EYEINHAND_CAMERA) + task.setInteractionMatrixType(Servo.ServoIteractionMatrixType.DESIRED) + prev_v = ColVector(6, 0.0) + target_class = 2 # Car + + d = DisplayOpenCV() + d.init(I) + error_norm = 1e10 + # Servoing loop + while error_norm > 5e-6: + start = time.time() + # Data acquisition + simulator.getImage(I, cam) + # Build current features + results = detection_model(np.array(I.numpy()[..., 2::-1])) + boxes = map(lambda result: result.boxes, results) + boxes = filter(lambda box: box.cls is not None and len(box.cls) > 0 and box.cls[0] == target_class, boxes) + boxes = sorted(boxes, key=lambda box: box.conf[0]) + bbs = list(map(lambda box: box.xywh[0].cpu().numpy(), boxes)) + if len(bbs) > 0: + bb = bbs[-1] # Take highest confidence + u, v = bb[0], bb[1] + x, y = PixelMeterConversion.convertPoint(cam, u, v) + s.buildFrom(x, y, Z) + v = task.computeControlLaw() + prev_v = v + else: + v = prev_v + error: ColVector = task.getError() + error_norm = error.sumSquare() + + # Display and logging + Display.display(I) + sd.display(cam, I, Color.green) + s.display(cam, I, Color.red) + Display.getImage(I, Idisp) + Display.flush(I) + plotter.on_iter(Idisp, v, error, cTw) + + # Move robot/update simulator + cTcn = ExponentialMap.direct(v, time.time() - start) + cTw = cTcn.inverse() * cTw + simulator.setCameraPosition(cTw) + + plotter.generate_anim() From dcfbd46b9505913a0dd9db2855d345558d6f3109 Mon Sep 17 00:00:00 2001 From: Samuel Felton Date: Tue, 14 May 2024 14:55:03 +0200 Subject: [PATCH 04/12] Update example with model warmup to fix high velocity at first iteration --- .../python/examples/yolo-centering-task.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/modules/python/examples/yolo-centering-task.py b/modules/python/examples/yolo-centering-task.py index 58647fffef..8b765b085d 100644 --- a/modules/python/examples/yolo-centering-task.py +++ b/modules/python/examples/yolo-centering-task.py @@ -8,7 +8,8 @@ from visp.robot import ImageSimulator from visp.visual_features import BasicFeature, FeaturePoint from visp.vs import Servo -from visp.gui import DisplayOpenCV +from visp.gui import DisplayX + try: from ultralytics import YOLO @@ -20,6 +21,7 @@ import numpy as np import matplotlib.pyplot as plt from matplotlib import animation +import argparse plt.rcParams['text.usetex'] = True @@ -99,19 +101,25 @@ def animate(i): plt.close() if __name__ == '__main__': + parser = argparse.ArgumentParser('Centering task using a YOLO network') + parser.add_argument('--scene', type=str, help='Path to the scene') + parser.add_argument('--class-id', type=int, help='COCO class id of the object to track (e.g, 2 for a car)') + args = parser.parse_args() + h, w = 480, 640 Z = 5.0 + cam = CameraParameters(px=600, py=600, u0=w / 2.0, v0=h / 2.0) detection_model = YOLO('yolov8n.pt') # Initialize simulator - scene_image = np.asarray(Image.open('/mnt/d/Downloads/car-img.jpg')) + scene_image = np.asarray(Image.open(args.scene)) scene_image = np.concatenate((scene_image, np.ones_like(scene_image[..., 0:1]) * 255), axis=-1) scene_image = ImageRGBa(scene_image) simulator = get_simulator(scene_image) plotter = VSPlot() - cTw = HomogeneousMatrix(-1.0, 0.5, Z, 0.0, 0.0, 0.0) + cTw = HomogeneousMatrix(-2.0, 0.5, Z, 0.0, 0.0, 0.0) I = ImageRGBa(h, w) Idisp = ImageRGBa(h, w) @@ -130,12 +138,15 @@ def animate(i): task.setLambda(0.5) task.setCameraDoF(ColVector([0, 0, 0, 1, 1, 0])) task.setServo(Servo.ServoType.EYEINHAND_CAMERA) - task.setInteractionMatrixType(Servo.ServoIteractionMatrixType.DESIRED) + task.setInteractionMatrixType(Servo.ServoIteractionMatrixType.CURRENT) prev_v = ColVector(6, 0.0) - target_class = 2 # Car + target_class = args.class_id # Car - d = DisplayOpenCV() + d = DisplayX() d.init(I) + Display.display(I) + Display.flush(I) + _ = detection_model(np.array(I.numpy()[..., 2::-1])) error_norm = 1e10 # Servoing loop while error_norm > 5e-6: @@ -164,8 +175,8 @@ def animate(i): Display.display(I) sd.display(cam, I, Color.green) s.display(cam, I, Color.red) - Display.getImage(I, Idisp) Display.flush(I) + Display.getImage(I, Idisp) plotter.on_iter(Idisp, v, error, cTw) # Move robot/update simulator From 1dc7513c13cc240f4e69cc2b3e2c7e8229d17df3 Mon Sep 17 00:00:00 2001 From: Samuel Felton Date: Tue, 14 May 2024 15:17:23 +0200 Subject: [PATCH 05/12] Add generic display getter function --- modules/python/bindings/visp/display_utils.py | 99 +++++++++++++++++++ modules/python/examples/realsense-mbt.py | 6 +- modules/python/examples/synthetic-data-mbt.py | 6 +- .../python/examples/yolo-centering-task.py | 4 +- 4 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 modules/python/bindings/visp/display_utils.py diff --git a/modules/python/bindings/visp/display_utils.py b/modules/python/bindings/visp/display_utils.py new file mode 100644 index 0000000000..95546642e9 --- /dev/null +++ b/modules/python/bindings/visp/display_utils.py @@ -0,0 +1,99 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# Display helpers for ViSP +# +############################################################################# + +from typing import List, Optional + +from visp.core import Display + +VISP_DISPLAY_CLS_MAP = { + 'x': None, + 'opencv': None, + 'gtk': None, + 'win32': None, + 'gdi': None, +} + +try: + from visp.gui import DisplayX + VISP_DISPLAY_CLS_MAP['x'] = DisplayX +except ImportError: + pass + +try: + from visp.gui import DisplayOpenCV + VISP_DISPLAY_CLS_MAP['opencv'] = DisplayOpenCV +except ImportError: + pass + +try: + from visp.gui import DisplayGTK + VISP_DISPLAY_CLS_MAP['gtk'] = DisplayGTK +except ImportError: + pass + +try: + from visp.gui import DisplayWin32 + VISP_DISPLAY_CLS_MAP['win32'] = DisplayWin32 +except ImportError: + pass + +try: + from visp.gui import DisplayGDI + VISP_DISPLAY_CLS_MAP['gdi'] = DisplayGDI +except ImportError: + pass + +VISP_DEFAULT_DISPLAY_PREFERENCE = ['x', 'opencv', 'gtk', 'win32', 'gdi'] + + +def get_display(preferences: Optional[List[str]] = None) -> Optional[Display]: + ''' + Get a new ViSP display instance, dependending on what display driver is available. + + :param preference: An optional list of preferred backends to use. + The backends are tested in the order they are specified, and the first match is instanciated. + If preference is None, a default list of display is used. This list contains all basic displays. + The specified values are case insensitive and may include. See VISP_DEFAULT_DISPLAY_PREFERENCE for the available options + + :return: a new instance of a ViSP display if one of the requested backend has been found, None otherwise + ''' + + final_prefs: List[str] = preferences if preferences is not None else VISP_DEFAULT_DISPLAY_PREFERENCE + for preference in final_prefs: + pref_key = preference.lower() + display_opt = VISP_DISPLAY_CLS_MAP.get(pref_key) + if display_opt is not None: + return display_opt() + return None diff --git a/modules/python/examples/realsense-mbt.py b/modules/python/examples/realsense-mbt.py index 7c58c0ed47..d222f62ff8 100644 --- a/modules/python/examples/realsense-mbt.py +++ b/modules/python/examples/realsense-mbt.py @@ -48,7 +48,7 @@ from visp.core import ImageGray, ImageUInt16, ImageRGBa from visp.io import ImageIo from visp.mbt import MbGenericTracker -from visp.gui import DisplayOpenCV +from visp.display_utils import get_display import pyrealsense2 as rs @@ -175,11 +175,11 @@ def cam_from_rs_profile(profile) -> Tuple[CameraParameters, int, int]: tracker.setCameraTransformationMatrix('Camera2', depth_M_color) # Initialize displays - dI = DisplayOpenCV() + dI = get_display() dI.init(I, 0, 0, 'Color image') I_depth = None if args.disable_depth else ImageGray() - dDepth = DisplayOpenCV() + dDepth = get_display() if not args.disable_depth: ImageConvert.createDepthHistogram(frame_data.I_depth, I_depth) dDepth.init(I_depth, I.getWidth(), 0, 'Depth') diff --git a/modules/python/examples/synthetic-data-mbt.py b/modules/python/examples/synthetic-data-mbt.py index 4929afa721..5afced3300 100644 --- a/modules/python/examples/synthetic-data-mbt.py +++ b/modules/python/examples/synthetic-data-mbt.py @@ -46,7 +46,7 @@ from visp.core import ImageGray, ImageUInt16 from visp.io import ImageIo from visp.mbt import MbGenericTracker, MbTracker -from visp.gui import DisplayOpenCV +from visp.display_utils import get_display from visp.core import Color from visp.core import PixelMeterConversion @@ -208,11 +208,11 @@ def parse_camera_file(exp_config: MBTConfig, is_color: bool) -> CameraParameters tracker.setCameraTransformationMatrix('Camera2', depth_M_color) # Initialize displays - dI = DisplayOpenCV() + dI = get_display() dI.init(I, 0, 0, 'Color image') I_depth = None if args.disable_depth else ImageGray() - dDepth = DisplayOpenCV() + dDepth = get_display() if not args.disable_depth: ImageConvert.createDepthHistogram(frame_data.I_depth, I_depth) dDepth.init(I_depth, I.getWidth(), 0, 'Depth') diff --git a/modules/python/examples/yolo-centering-task.py b/modules/python/examples/yolo-centering-task.py index 8b765b085d..1e0b7b3c55 100644 --- a/modules/python/examples/yolo-centering-task.py +++ b/modules/python/examples/yolo-centering-task.py @@ -8,7 +8,7 @@ from visp.robot import ImageSimulator from visp.visual_features import BasicFeature, FeaturePoint from visp.vs import Servo -from visp.gui import DisplayX +from visp.display_utils import get_display try: @@ -142,7 +142,7 @@ def animate(i): prev_v = ColVector(6, 0.0) target_class = args.class_id # Car - d = DisplayX() + d = get_display() d.init(I) Display.display(I) Display.flush(I) From 736c24a734e65c76c935241d6d7336e94bc8542f Mon Sep 17 00:00:00 2001 From: Samuel Felton Date: Tue, 14 May 2024 18:08:53 +0200 Subject: [PATCH 06/12] Fix image rendering in yolo example --- modules/python/examples/yolo-centering-task.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/python/examples/yolo-centering-task.py b/modules/python/examples/yolo-centering-task.py index 1e0b7b3c55..9525c81cdf 100644 --- a/modules/python/examples/yolo-centering-task.py +++ b/modules/python/examples/yolo-centering-task.py @@ -48,7 +48,7 @@ def __init__(self): self.I = [] def on_iter(self, Idisp: ImageRGBa, v: ColVector, error: ColVector, cTw: HomogeneousMatrix) -> None: - self.I.append(Idisp) + self.I.append(Idisp.numpy().copy()) self.v.append(v.numpy()[3:5].flatten()) self.error.append(error.numpy().flatten()) self.r.append(PoseVector(cTw).numpy()[3:5].flatten()) @@ -59,7 +59,7 @@ def generate_anim(self): self.r = np.asarray(self.r)[1:] - fig, axs = plt.subplots(2, 2, figsize=(15, 15 * (self.I[0].getHeight() / self.I[0].getWidth()))) + fig, axs = plt.subplots(2, 2, figsize=(15, 15 * (self.I[0].shape[0] / self.I[0].shape[1]))) axs = [axs[0][0], axs[0][1], axs[1][0],axs[1][1]] titles = ['I', 'Feature error', 'Velocity', 'Pose'] legends = [ @@ -94,8 +94,8 @@ def animate(i): artists[3][j].set_data(xs, self.r[:i, j]) return artists - anim = animation.FuncAnimation(fig, animate, frames=len(self.v), interval=30, blit=False, repeat=False) - writervideo = animation.FFMpegWriter(fps=30) + anim = animation.FuncAnimation(fig, animate, frames=len(self.v), blit=False, repeat=False) + writervideo = animation.FFMpegWriter(fps=20) anim.save('exp.mp4', writer=writervideo) plt.savefig('exp.pdf') plt.close() @@ -149,7 +149,7 @@ def animate(i): _ = detection_model(np.array(I.numpy()[..., 2::-1])) error_norm = 1e10 # Servoing loop - while error_norm > 5e-6: + while error_norm > 5e-7: start = time.time() # Data acquisition simulator.getImage(I, cam) @@ -173,8 +173,8 @@ def animate(i): # Display and logging Display.display(I) - sd.display(cam, I, Color.green) - s.display(cam, I, Color.red) + sd.display(cam, I, Color.darkBlue, thickness=2) + s.display(cam, I, Color.darkRed, thickness=2) Display.flush(I) Display.getImage(I, Idisp) plotter.on_iter(Idisp, v, error, cTw) From 6f5ee3b054508146f28acc95fdaf9295067c62f3 Mon Sep 17 00:00:00 2001 From: Samuel Felton Date: Wed, 15 May 2024 18:20:00 +0200 Subject: [PATCH 07/12] Show python specific api in documentation --- .../doc/_templates/custom-module-template.rst | 2 +- modules/python/doc/api.rst.in | 15 ++++++++++++++- modules/python/doc/conf.py.in | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/modules/python/doc/_templates/custom-module-template.rst b/modules/python/doc/_templates/custom-module-template.rst index 22bd4a50b6..2820ce77e4 100644 --- a/modules/python/doc/_templates/custom-module-template.rst +++ b/modules/python/doc/_templates/custom-module-template.rst @@ -19,7 +19,7 @@ .. rubric:: {{ _('Functions') }} .. autosummary:: - :nosignatures: + :toctree: {% for item in functions %} {{ item }} {%- endfor %} diff --git a/modules/python/doc/api.rst.in b/modules/python/doc/api.rst.in index 004e63bc57..282d0bcfe1 100644 --- a/modules/python/doc/api.rst.in +++ b/modules/python/doc/api.rst.in @@ -1,7 +1,20 @@ .. _API reference: API reference -============== +===================== + +Python specific API +--------------------- + +.. autosummary:: + :toctree: _autosummary + :recursive: + :template: custom-module-template.rst + + visp.display_utils + +Modules API reference +------------------------ This API documentation is automatically generated by parsing the C++ documentation. diff --git a/modules/python/doc/conf.py.in b/modules/python/doc/conf.py.in index d1d18ac7cf..ba8e065a57 100644 --- a/modules/python/doc/conf.py.in +++ b/modules/python/doc/conf.py.in @@ -400,7 +400,7 @@ python_type_aliases = { "_visp.": "visp.", "visp._visp.": "visp." } - +autodoc_type_aliases = python_type_aliases autodoc_excludes = [ '__weakref__', '__doc__', '__module__', '__dict__', From f763ccf0982e8fa8b03b4970a5cdef138bb0894c Mon Sep 17 00:00:00 2001 From: Samuel Felton Date: Wed, 15 May 2024 19:18:24 +0200 Subject: [PATCH 08/12] generate subclasses --- .../generator/visp_python_bindgen/header.py | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/modules/python/generator/visp_python_bindgen/header.py b/modules/python/generator/visp_python_bindgen/header.py index 0b8d543ca8..4675bb76b5 100644 --- a/modules/python/generator/visp_python_bindgen/header.py +++ b/modules/python/generator/visp_python_bindgen/header.py @@ -244,23 +244,18 @@ def parse_sub_namespace(self, bindings_container: BindingsContainer, ns: Namespa logging.info(f'Parsing subnamespace {namespace_prefix + sub_ns}') self.parse_sub_namespace(bindings_container, ns.namespaces[sub_ns], namespace_prefix + sub_ns + '::', False) - def generate_class(self, bindings_container: BindingsContainer, cls: ClassScope, header_env: HeaderEnvironment) -> SingleObjectBindings: + def generate_class(self, bindings_container: BindingsContainer, cls: ClassScope, header_env: HeaderEnvironment, owner='submodule') -> None: ''' Generate the bindings for a single class: This method will generate one Python class per template instanciation. If the class has no template argument, then a single python class is generated If it is templated, the mapping (template argument types => Python class name) must be provided in the JSON config file + Subclasses are also generated ''' - def generate_class_with_potiental_specialization(name_python: str, owner_specs: 'OrderedDict[str, str]', cls_config: Dict) -> str: + def generate_class_with_potiental_specialization(name_python: str, owner_specs: 'OrderedDict[str, str]', cls_config: Dict) -> None: ''' Generate the bindings of a single class, handling a potential template specialization. - The handled information is: - - The inheritance of this class - - Its public fields that are not pointers - - Its constructors - - Most of its operators - - Its public methods ''' python_ident = f'py{name_python}' name_cpp = get_typename(cls.class_decl.typename, owner_specs, header_env.mapping) @@ -317,12 +312,13 @@ def add_method_doc_to_pyargs(method: types.Method, py_arg_strs: List[str]) -> Li trampoline_name = cls_config['trampoline'] if trampoline_name is not None: base_class_strs.append(trampoline_name) + # py::class template contains the class, its holder type, and its base clases. # The default holder type is std::unique_ptr. when the cpp function argument is a shared_ptr, Pybind will raise an error when calling the method. py_class_template_str = ', '.join([name_cpp, f'std::shared_ptr<{name_cpp}>'] + base_class_strs) doc_param = [] if class_doc is None else [class_doc.documentation] buffer_protocol_arg = ['py::buffer_protocol()'] if cls_config['use_buffer_protocol'] else [] - cls_argument_strs = ['submodule', f'"{name_python}"'] + doc_param + buffer_protocol_arg + cls_argument_strs = [owner, f'"{name_python}"'] + doc_param + buffer_protocol_arg class_decl = f'\tpy::class_ {python_ident} = py::class_<{py_class_template_str}>({", ".join(cls_argument_strs)});' @@ -551,11 +547,20 @@ def add_method_doc_to_pyargs(method: types.Method, py_arg_strs: List[str]) -> Li classs_binding_defs = ClassBindingDefinitions(field_dict, methods_dict, publicist_str) bindings_container.add_bindings(SingleObjectBindings(class_def_names, class_decl, classs_binding_defs, GenerationObjectType.Class)) - name_cpp_no_template = '::'.join([seg.name for seg in cls.class_decl.typename.segments]) + for subclass in cls.classes: + if subclass.class_decl.access != 'public': + continue + if name_is_anonymous(subclass.class_decl.typename): + logging.warning(f'Class {name_cpp} has a subclass that is hidden behind a typedef that was not generated!') + continue + self.generate_class(bindings_container, subclass, header_env, python_ident) + + + name_cpp_no_template = get_name(cls.class_decl.typename) logging.info(f'Parsing class "{name_cpp_no_template}"') if self.submodule.class_should_be_ignored(name_cpp_no_template): - return '' + return cls_config = self.submodule.get_class_config(name_cpp_no_template) @@ -569,10 +574,9 @@ def add_method_doc_to_pyargs(method: types.Method, py_arg_strs: List[str]) -> Li if len(set(refs_or_ptr_fields).difference(set(acknowledged_pointer_fields))) > 0: self.submodule.report.add_pointer_or_ref_holder(name_cpp_no_template, refs_or_ptr_fields) - if cls.class_decl.template is None: name_python = name_cpp_no_template.replace('vp', '') - return generate_class_with_potiental_specialization(name_python, {}, cls_config) + generate_class_with_potiental_specialization(name_python, {}, cls_config) else: if cls_config is None or 'specializations' not in cls_config or len(cls_config['specializations']) == 0: logging.warning(f'Could not find template specialization for class {name_cpp_no_template}: skipping!') From dcd21362280b6f51730e77f5cd0459b55a445a8c Mon Sep 17 00:00:00 2001 From: Samuel Felton Date: Thu, 23 May 2024 17:26:10 +0200 Subject: [PATCH 09/12] Small corrections for Yolo example --- modules/python/doc/conf.py.in | 5 ++ .../python/examples/yolo-centering-task.py | 48 ++++++++++--------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/modules/python/doc/conf.py.in b/modules/python/doc/conf.py.in index ba8e065a57..2d31c80f29 100644 --- a/modules/python/doc/conf.py.in +++ b/modules/python/doc/conf.py.in @@ -408,6 +408,11 @@ autodoc_excludes = [ '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__getattribute__', '__entries', ] + +autodoc_default_options = { + 'exclude-members': ','.join(autodoc_excludes) +} + def autodoc_skip_member(app, what, name, obj, skip, options): # Ref: https://stackoverflow.com/a/21449475/ diff --git a/modules/python/examples/yolo-centering-task.py b/modules/python/examples/yolo-centering-task.py index 9525c81cdf..b68e09bd4c 100644 --- a/modules/python/examples/yolo-centering-task.py +++ b/modules/python/examples/yolo-centering-task.py @@ -1,6 +1,6 @@ -from typing import Optional, Tuple +import visp from visp.core import ColVector, Point, Color, PixelMeterConversion, Display from visp.core import CameraParameters, HomogeneousMatrix , ExponentialMap, PoseVector @@ -25,15 +25,14 @@ plt.rcParams['text.usetex'] = True -def get_simulator(scene_image: ImageRGBa) -> ImageSimulator: - simulator = ImageSimulator() - l = 1.5 - L = 1 +def get_simulator(scene_path: str) -> ImageSimulator: + scene_image = np.asarray(Image.open(scene_path).convert('RGBA')) + scene_image = ImageRGBa(scene_image) + simulator = ImageSimulator() # Planar scene from single image + l, L = 1.5, 1.0 scene_3d = [ - [-l, -L, 0.0], - [l, -L, 0.0], - [l, L, 0.0], - [-l, L, 0.0], + [-l, -L, 0.0], [l, -L, 0.0], + [l, L, 0.0], [-l, L, 0.0], ] simulator.init(scene_image, list(map(lambda X: Point(X), scene_3d))) simulator.setCleanPreviousImage(True, color=Color.black) @@ -106,42 +105,41 @@ def animate(i): parser.add_argument('--class-id', type=int, help='COCO class id of the object to track (e.g, 2 for a car)') args = parser.parse_args() + detection_model = YOLO('yolov8n.pt') + h, w = 480, 640 Z = 5.0 - cam = CameraParameters(px=600, py=600, u0=w / 2.0, v0=h / 2.0) - detection_model = YOLO('yolov8n.pt') - # Initialize simulator - scene_image = np.asarray(Image.open(args.scene)) - scene_image = np.concatenate((scene_image, np.ones_like(scene_image[..., 0:1]) * 255), axis=-1) - scene_image = ImageRGBa(scene_image) - simulator = get_simulator(scene_image) + plotter = VSPlot() + # Initialization + simulator = get_simulator(args.scene) cTw = HomogeneousMatrix(-2.0, 0.5, Z, 0.0, 0.0, 0.0) I = ImageRGBa(h, w) Idisp = ImageRGBa(h, w) - simulator.setCameraPosition(cTw) simulator.getImage(I, cam) - s = FeaturePoint() - s.buildFrom(0.0, 0.0, Z) # Define centering task xd, yd = PixelMeterConversion.convertPoint(cam, w / 2.0, h / 2.0) sd = FeaturePoint() sd.buildFrom(xd, yd, Z) + s = FeaturePoint() + s.buildFrom(0.0, 0.0, Z) + task = Servo() task.addFeature(s, sd) task.setLambda(0.5) task.setCameraDoF(ColVector([0, 0, 0, 1, 1, 0])) task.setServo(Servo.ServoType.EYEINHAND_CAMERA) task.setInteractionMatrixType(Servo.ServoIteractionMatrixType.CURRENT) - prev_v = ColVector(6, 0.0) target_class = args.class_id # Car + prev_v = ColVector(6, 0.0) + d = get_display() d.init(I) Display.display(I) @@ -153,12 +151,16 @@ def animate(i): start = time.time() # Data acquisition simulator.getImage(I, cam) + def has_class_box(box): + return box.cls is not None and len(box.cls) > 0 and box.cls[0] == target_class + # Build current features - results = detection_model(np.array(I.numpy()[..., 2::-1])) - boxes = map(lambda result: result.boxes, results) - boxes = filter(lambda box: box.cls is not None and len(box.cls) > 0 and box.cls[0] == target_class, boxes) + results = detection_model(np.array(I.numpy()[..., 2::-1])) # Run detection + boxes = map(lambda result: result.boxes, results) # + boxes = filter(has_class_box, boxes) boxes = sorted(boxes, key=lambda box: box.conf[0]) bbs = list(map(lambda box: box.xywh[0].cpu().numpy(), boxes)) + if len(bbs) > 0: bb = bbs[-1] # Take highest confidence u, v = bb[0], bb[1] From b4627344d33e244d0c9817d117abb5ae324a5b8c Mon Sep 17 00:00:00 2001 From: Samuel Felton Date: Fri, 31 May 2024 17:49:02 +0200 Subject: [PATCH 10/12] Fix box indexing when multiple classes are found in the image --- .../python/examples/yolo-centering-task.py | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/modules/python/examples/yolo-centering-task.py b/modules/python/examples/yolo-centering-task.py index b68e09bd4c..1d91c53d3e 100644 --- a/modules/python/examples/yolo-centering-task.py +++ b/modules/python/examples/yolo-centering-task.py @@ -108,7 +108,7 @@ def animate(i): detection_model = YOLO('yolov8n.pt') h, w = 480, 640 - Z = 5.0 + Z = 3.0 cam = CameraParameters(px=600, py=600, u0=w / 2.0, v0=h / 2.0) @@ -116,7 +116,7 @@ def animate(i): # Initialization simulator = get_simulator(args.scene) - cTw = HomogeneousMatrix(-2.0, 0.5, Z, 0.0, 0.0, 0.0) + cTw = HomogeneousMatrix(-0.1, 0.1, Z, 0.0, 0.0, 0.0) I = ImageRGBa(h, w) Idisp = ImageRGBa(h, w) simulator.setCameraPosition(cTw) @@ -137,8 +137,10 @@ def animate(i): task.setServo(Servo.ServoType.EYEINHAND_CAMERA) task.setInteractionMatrixType(Servo.ServoIteractionMatrixType.CURRENT) target_class = args.class_id # Car + print(target_class) prev_v = ColVector(6, 0.0) + v = ColVector(6, 0.0) d = get_display() d.init(I) @@ -148,27 +150,38 @@ def animate(i): error_norm = 1e10 # Servoing loop while error_norm > 5e-7: + print('Error norm is', error_norm) + print('AAAAAAAAAAAAAA') start = time.time() # Data acquisition simulator.getImage(I, cam) def has_class_box(box): - return box.cls is not None and len(box.cls) > 0 and box.cls[0] == target_class + return box.cls is not None and len(box.cls) > 0 and box.cls[0] # Build current features - results = detection_model(np.array(I.numpy()[..., 2::-1])) # Run detection - boxes = map(lambda result: result.boxes, results) # - boxes = filter(has_class_box, boxes) - boxes = sorted(boxes, key=lambda box: box.conf[0]) - bbs = list(map(lambda box: box.xywh[0].cpu().numpy(), boxes)) - - if len(bbs) > 0: - bb = bbs[-1] # Take highest confidence + results = detection_model(np.array(I.numpy()[..., 2::-1]))[0] # Run detection + boxes = results.boxes + max_conf = 0.0 + idx = -1 + bb = None + for i in range(len(boxes.conf)): + if boxes.cls[i] == target_class and boxes.conf[i] > max_conf: + print('New max') + idx = i + max_conf = boxes.conf[i] + bb = boxes.xywh[i].cpu().numpy() + # boxes = filter(has_class_box, boxes) + # print('BOXES AFTER FILTER:', list(boxes)) + # boxes = sorted(boxes, key=lambda box: box.conf[0]) + # bbs = list(map(lambda box: box.xywh[0].cpu().numpy(), boxes)) + if bb is not None: u, v = bb[0], bb[1] x, y = PixelMeterConversion.convertPoint(cam, u, v) s.buildFrom(x, y, Z) v = task.computeControlLaw() prev_v = v else: + task.computeControlLaw() v = prev_v error: ColVector = task.getError() error_norm = error.sumSquare() @@ -179,6 +192,8 @@ def has_class_box(box): s.display(cam, I, Color.darkRed, thickness=2) Display.flush(I) Display.getImage(I, Idisp) + print(v) + print(v, error, cTw) plotter.on_iter(Idisp, v, error, cTw) # Move robot/update simulator From dac3e292a7365eabc220b3370f2bc44c5886d625 Mon Sep 17 00:00:00 2001 From: Samuel Felton Date: Mon, 3 Jun 2024 17:40:14 +0200 Subject: [PATCH 11/12] Yolo centering task on afma6 --- .../examples/yolo-centering-task-afma6.py | 347 ++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 modules/python/examples/yolo-centering-task-afma6.py diff --git a/modules/python/examples/yolo-centering-task-afma6.py b/modules/python/examples/yolo-centering-task-afma6.py new file mode 100644 index 0000000000..48a24e4153 --- /dev/null +++ b/modules/python/examples/yolo-centering-task-afma6.py @@ -0,0 +1,347 @@ + + +import visp +from visp.core import ColVector, Color, PixelMeterConversion, Display, Matrix +from visp.core import CameraParameters, HomogeneousMatrix , PoseVector, ImagePoint +from visp.core import ImageRGBa, ImageUInt16 +from visp.core import UnscentedKalman, UKSigmaDrawerMerwe + +from visp.visual_features import BasicFeature, FeaturePoint +from visp.vs import Servo +from visp.gui import ColorBlindFriendlyPalette +from visp.display_utils import get_display +from visp.robot import RobotAfma6, Robot + +from typing import List, Optional, Tuple +try: + from ultralytics import YOLO +except ImportError: + print('This example requires YoloV8: pip install ultralytics') + +import time +import numpy as np +import matplotlib.pyplot as plt +from matplotlib import animation +import argparse +import pyrealsense2 as rs +from functools import partial +plt.rcParams['text.usetex'] = False + + + +def fx(x: ColVector, dt: float) -> ColVector: + """ + @brief Process function that projects in time the internal state of the UKF. + + @param x The internal state of the UKF. + @param dt The sampling time: how far in the future are we projecting x. + + @return ColVector The updated internal state, projected in time, also known as the prior. + """ + return ColVector([ + x[0] + dt * x[1] + dt ** 2 * x[2], + x[1] + dt * x[2], + x[2], + x[3] + dt * x[4] + dt ** 2 * x[5], + x[4] + dt * x[5], + x[5] + ]) + + +def hx(x: ColVector) -> ColVector: + """ + @brief Measurement function that expresses the internal state of the UKF in the measurement space. + + @param x The internal state of the UKF. + + @return ColVector The internal state, expressed in the measurement space. + """ + return ColVector([ + x[0], + x[3] + ]) + +def add_state_vectors(a, b) -> ColVector: + """ + @brief Method that permits to add two state vectors. + + @param a The first state vector to which another state vector must be added. + @param b The other state vector that must be added to a. + + @return ColVector The sum a + b. + """ + return a + b + +def residual_state_vectors(a, b) -> ColVector: + """ + @brief Method that permits to substract a state vector to another. + + @param a The first state vector to which another state vector must be substracted. + @param b The other state vector that must be substracted to a. + + @return ColVector The substraction a - b. + """ + return a - b + +def generate_Q_matrix(dt: float, sigma: float) -> Matrix: + """ + @brief Method that generates the process covariance matrix for a process for which the + state vector can be written as (x, dx/dt)^T + + @param dt The sampling period. + + @return Matrix The corresponding process covariance matrix. + """ + return Matrix(np.asarray([ + [dt**4/4, dt**3/3, dt**2/2, 0, 0, 0], + [dt**3/3, dt**2/2, dt, 0, 0, 0], + [dt**2/2, dt, 1, 0, 0, 0], + [0, 0, 0, dt**4/4, dt**3/3, dt**2/2], + [0, 0, 0, dt**3/3, dt**2/2, dt], + [0, 0, 0, dt**2/2, dt, 1], + ]) * sigma) + + + +def read_data(I_rgba: ImageRGBa, I_depth: ImageUInt16, pipe: rs.pipeline, align: rs.align) -> None: + frames = pipe.wait_for_frames() + aligned_frames = align.process(frames) + I_np = np.asanyarray(aligned_frames.get_color_frame().as_frame().get_data()) + I_np = np.concatenate((I_np, np.ones_like(I_np[..., 0:1], dtype=np.uint8) * 255), axis=-1) + rgba_numpy_view = I_rgba.numpy() # writable numpy view of rgba image + np.copyto(rgba_numpy_view, I_np) + depth_numpy_view = I_depth.numpy() + depth_np = np.asanyarray(aligned_frames.get_depth_frame().as_frame().get_data()) + np.copyto(depth_numpy_view, depth_np) + +def cam_from_rs_profile(profile) -> Tuple[CameraParameters, int, int]: + intr = profile.as_video_stream_profile().get_intrinsics() # Downcast to video_stream_profile and fetch intrinsics + return CameraParameters(intr.fx, intr.fy, intr.ppx, intr.ppy), intr.height, intr.width + +class VSPlot(object): + def __init__(self): + self.v = [] + self.error = [] + self.r = [] + self.I = [] + + def on_iter(self, Idisp: ImageRGBa, v: ColVector, error: ColVector, cTw: HomogeneousMatrix) -> None: + self.I.append(Idisp.numpy().copy()) + self.v.append(v.numpy()[3:5].flatten()) + self.error.append(error.numpy().flatten()) + self.r.append(PoseVector(cTw).numpy()[3:5].flatten()) + + def generate_anim(self): + self.error = np.asarray(self.error)[1:] + self.v = np.asarray(self.v)[1:] + self.r = np.asarray(self.r)[1:] + + + fig, axs = plt.subplots(2, 2, figsize=(15, 15 * (self.I[0].shape[0] / self.I[0].shape[1]))) + axs = [axs[0][0], axs[0][1], axs[1][0],axs[1][1]] + titles = ['I', 'Feature error', 'Velocity', 'Pose'] + legends = [ + None, + [r"$x$", r"$y$"], + [r"$\mathbf{\upsilon}_x$", r"$\mathbf{\upsilon}_y$"], + [r"$\theta\mathbf{u}_x$", r"$\theta\mathbf{u}_y$"], + ] + data = [None, self.error, self.v, self.r] + artists = [] + for i in range(len(axs)): + axs[i].set_title(titles[i]) + if data[i] is not None: + axs[i].set_xlabel('Iteration') + axs[i].grid() + axs[i].set_xlim(0, len(self.v)) + min_val, max_val = np.min(data[i]), np.max(data[i]) + margin = (max_val - min_val) * 0.05 + axs[i].set_ylim(min_val - margin, max_val + margin) + artists.append(axs[i].plot(data[i])) + axs[i].legend(legends[i]) + else: + artists.append(axs[i].imshow(self.I[0])) + axs[i].set_axis_off() + plt.tight_layout() + def animate(i): + print(f'Processing frame {i+1}/{len(self.I)}') + xs = range(i) + artists[0].set_data(self.I[i]) + for j in range(2): + artists[1][j].set_data(xs, self.error[:i, j]) + artists[2][j].set_data(xs, self.v[:i, j]) + artists[3][j].set_data(xs, self.r[:i, j]) + return artists + + anim = animation.FuncAnimation(fig, animate, frames=len(self.v), blit=False, repeat=False) + writervideo = animation.FFMpegWriter(fps=30) + anim.save('exp.mp4', writer=writervideo) + plt.savefig('exp.pdf') + plt.close() + +if __name__ == '__main__': + parser = argparse.ArgumentParser('Centering task using a YOLO network') + parser.add_argument('--class-id', type=int, help='COCO class id of the object to track (e.g, 2 for a car)') + args = parser.parse_args() + + detection_model = YOLO('yolov8n.pt') + + plotter = VSPlot() + + # Initialization + print('Initializing Realsense camera...') + # Realsense2 + pipe = rs.pipeline() + config = rs.config() + fps = 30 + config.enable_stream(rs.stream.depth, 640, 480, rs.format.z16, fps) + config.enable_stream(rs.stream.color, 640, 480, rs.format.rgb8, fps) + cfg = pipe.start(config) + + # Initialize data + cam, h, w = cam_from_rs_profile(cfg.get_stream(rs.stream.color)) + depth_scale = cfg.get_device().first_depth_sensor().get_depth_scale() + print(f'Depth scale is {depth_scale}') + + I = ImageRGBa(h, w) + I_depth = ImageUInt16(h, w) + Idisp = ImageRGBa(h, w) + + # Align depth stream with color stream + align = rs.align(rs.stream.color) + get_images = partial(read_data, pipe=pipe, align=align) + + print('Initializing Afma6...') + robot = RobotAfma6() + robot.setPositioningVelocity(5.0) + print(robot.getPosition(Robot.ControlFrameType.REFERENCE_FRAME)) + + print('Moving Afma6 to starting pose...') + r = PoseVector(0.06706274856, 0.3844766362, -0.04551332622 , 0.3111005431, 0.3031078532, 0.01708581392) + cTw = HomogeneousMatrix(r) + + robot.setPosition(Robot.ControlFrameType.REFERENCE_FRAME, r) + + + print('Warming up camera...') + for _ in range(100): + get_images(I, I_depth) + + # Define kalman filter + + drawer = UKSigmaDrawerMerwe(6, alpha=0.0001, beta=2, kappa=-3, resFunc=residual_state_vectors, addFunc=add_state_vectors) + pixel_noise = 1 + noise_x, noise_y = [pixel_noise / f for f in [cam.get_px(), cam.get_py()]] + noise_vel = 1e-8 + noise_accel = 1e-12 + measure_covariance = Matrix([ + [noise_x ** 2, 0.0], + [0.0, noise_y ** 2] + ]) + process_covariance = Matrix([ + [noise_x ** 2, 0, 0, 0, 0, 0], + [0, noise_vel, 0, 0, 0, 0], + [0, 0, noise_accel, 0, 0, 0], + [0, 0, 0, noise_y ** 2, 0,0], + [0, 0, 0, 0, noise_vel,0], + [0, 0, 0, 0, 0, noise_accel], + ]) + kalman = UnscentedKalman(generate_Q_matrix(1 / fps, sigma=1e-8), measure_covariance, drawer, fx, hx) + + + # Define centering task + xd, yd = PixelMeterConversion.convertPoint(cam, w / 2.0, h / 2.0) + get_images(I, I_depth) + Zd = I_depth[h // 2, w // 2] * depth_scale + print(f'Desired depth is {Zd}') + sd = FeaturePoint() + sd.buildFrom(xd, yd, Zd) + + s = FeaturePoint() + s.buildFrom(0.0, 0.0, Zd) + + task = Servo() + task.addFeature(s, sd) + task.setLambda(0.4) + task.setCameraDoF(ColVector([0, 0, 0, 1, 1, 0])) + task.setServo(Servo.ServoType.EYEINHAND_CAMERA) + task.setInteractionMatrixType(Servo.ServoIteractionMatrixType.CURRENT) + target_class = args.class_id # Car + + v = ColVector(6, 0.0) + + d = get_display() + d.init(I) + Display.display(I) + Display.flush(I) + _ = detection_model(np.array(I.numpy()[..., 2::-1])) + error_norm = 1e10 + last_detection_time = -1.0 + first_iter = True + # Servoing loop + while error_norm > 5e-7: + start = time.time() + # Data acquisition + get_images(I, I_depth) + + def has_class_box(box): + return box.cls is not None and len(box.cls) > 0 and box.cls[0] + + # Build current features + results = detection_model(np.array(I.numpy()[..., 2::-1]))[0] # Run detection + boxes = results.boxes + max_conf = 0.0 + idx = -1 + bb = None + for i in range(len(boxes.conf)): + if boxes.cls[i] == target_class and boxes.conf[i] > max_conf: + idx = i + max_conf = boxes.conf[i] + bb = boxes.xywh[i].cpu().numpy() + + if bb is not None: + u, v = bb[0], bb[1] + x, y = PixelMeterConversion.convertPoint(cam, u, v) + if first_iter: + initial_state = ColVector([x, 0, 0, y, 0, 0]) + kalman.init(initial_state, process_covariance) + first_iter = False + kalman.filter(ColVector([x, y]), (1 / fps)) + kalman_state = kalman.getXest() + last_detection_time = time.time() + s.buildFrom(kalman_state[0], kalman_state[3], Zd) + v = task.computeControlLaw() + else: + if last_detection_time < 0.0: + raise RuntimeError('No detection at first iteration') + kalman.predict(time.time() - last_detection_time) + kalman_pred = kalman.getXpred() + s.buildFrom(kalman_pred[0], kalman_pred[3], Zd) + task.computeControlLaw() + + error: ColVector = task.getError() + error_norm = error.sumSquare() + + # Display and logging + Display.display(I) + current_color = ColorBlindFriendlyPalette(ColorBlindFriendlyPalette.SkyBlue).to_vpColor() + if bb is not None: + Display.displayRectangle(I, i=int(bb[1] - bb[3] / 2), j=int(bb[0] - bb[2] / 2), width=bb[2], height=bb[3], + color=current_color, fill=False, thickness=2) + sd.display(cam, I, ColorBlindFriendlyPalette(ColorBlindFriendlyPalette.Yellow).to_vpColor(), thickness=3) + s.display(cam, I, current_color, thickness=3) + Display.flush(I) + Display.getImage(I, Idisp) + robot.getPosition(Robot.ControlFrameType.REFERENCE_FRAME, r) + cTw.buildFrom(r) + plotter.on_iter(Idisp, v, error, cTw) + + # Move robot/update simulator + robot.setRobotState(Robot.RobotStateType.STATE_VELOCITY_CONTROL) + robot.setVelocity(Robot.ControlFrameType.CAMERA_FRAME, v) + print(f'Iter took {time.time() - start}s') + #simulator.setCameraPosition(cTw) + + robot.stopMotion() + + plotter.generate_anim() From 4dea2d9957134dcaccf5c1739fdaa330c1df4ed8 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Tue, 11 Jun 2024 17:56:53 +0200 Subject: [PATCH 12/12] Update copyright header --- modules/python/bindings/include/blob.hpp | 8 +- modules/python/bindings/include/core.hpp | 2 +- .../python/bindings/include/core/arrays.hpp | 16 +- .../include/core/image_conversions.hpp | 302 +++++++++--------- .../python/bindings/include/core/images.hpp | 8 +- .../bindings/include/core/pixel_meter.hpp | 2 +- .../python/bindings/include/core/utils.hpp | 2 +- modules/python/bindings/include/mbt.hpp | 48 +-- .../bindings/include/visual_features.hpp | 2 +- modules/python/bindings/visp/display_utils.py | 2 +- 10 files changed, 196 insertions(+), 196 deletions(-) diff --git a/modules/python/bindings/include/blob.hpp b/modules/python/bindings/include/blob.hpp index 07cf661513..30e389631a 100644 --- a/modules/python/bindings/include/blob.hpp +++ b/modules/python/bindings/include/blob.hpp @@ -1,6 +1,6 @@ /* * ViSP, open source Visual Servoing Platform software. - * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * Copyright (C) 2005 - 2024 by Inria. All rights reserved. * * This software is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -50,7 +50,7 @@ void bindings_vpDot2(py::class_, vpTracker> &pyD vpImage &I, vpColor col = vpColor::blue, bool trackDot = true) { - return vpDot2::defineDots(&dots[0], dots.size(), dotFile, I, col, trackDot); + return vpDot2::defineDots(&dots[0], dots.size(), dotFile, I, col, trackDot); }, R"doc( Wrapper for the defineDots method, see the C++ ViSP documentation. )doc", py::arg("dots"), py::arg("dotFile"), py::arg("I"), py::arg("color"), py::arg("trackDot") = true); @@ -59,8 +59,8 @@ Wrapper for the defineDots method, see the C++ ViSP documentation. vpImage &I, std::vector &cogs, std::optional> cogStar) { - vpImagePoint *desireds = cogStar ? &((*cogStar)[0]) : nullptr; - vpDot2::trackAndDisplay(&dots[0], dots.size(), I, cogs, desireds); + vpImagePoint *desireds = cogStar ? &((*cogStar)[0]) : nullptr; + vpDot2::trackAndDisplay(&dots[0], dots.size(), I, cogs, desireds); }, R"doc( Wrapper for the trackAndDisplay method, see the C++ ViSP documentation. )doc", py::arg("dots"), py::arg("I"), py::arg("cogs"), py::arg("desiredCogs")); diff --git a/modules/python/bindings/include/core.hpp b/modules/python/bindings/include/core.hpp index 99c8b7e8ee..67963a3619 100644 --- a/modules/python/bindings/include/core.hpp +++ b/modules/python/bindings/include/core.hpp @@ -1,6 +1,6 @@ /* * ViSP, open source Visual Servoing Platform software. - * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * Copyright (C) 2005 - 2024 by Inria. All rights reserved. * * This software is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/modules/python/bindings/include/core/arrays.hpp b/modules/python/bindings/include/core/arrays.hpp index 484f75affe..44c4b38603 100644 --- a/modules/python/bindings/include/core/arrays.hpp +++ b/modules/python/bindings/include/core/arrays.hpp @@ -1,6 +1,6 @@ /* * ViSP, open source Visual Servoing Platform software. - * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * Copyright (C) 2005 - 2024 by Inria. All rights reserved. * * This software is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -232,7 +232,7 @@ void bindings_vpArray2D(py::class_, std::shared_ptr>> vpArray2D result(shape[0], shape[1]); copy_data_from_np(np_array, result.data); return result; - }), R"doc( + }), R"doc( Construct a 2D ViSP array by **copying** a 2D numpy array. :param np_array: The numpy array to copy. @@ -256,7 +256,7 @@ void bindings_vpMatrix(py::class_, vpArray2D vpMatrix result(shape[0], shape[1]); copy_data_from_np(np_array, result.data); return result; - }), R"doc( + }), R"doc( Construct a matrix by **copying** a 2D numpy array. :param np_array: The numpy array to copy. @@ -288,7 +288,7 @@ void bindings_vpRotationMatrix(py::class_, vpColVector result(shape[0]); copy_data_from_np(np_array, result.data); return result; - }), R"doc( + }), R"doc( Construct a column vector by **copying** a 1D numpy array. :param np_array: The numpy 1D array to copy. @@ -394,7 +394,7 @@ void bindings_vpRowVector(py::class_, vpRowVector result(shape[0]); copy_data_from_np(np_array, result.data); return result; - }), R"doc( + }), R"doc( Construct a row vector by **copying** a 1D numpy array. :param np_array: The numpy 1D array to copy. diff --git a/modules/python/bindings/include/core/image_conversions.hpp b/modules/python/bindings/include/core/image_conversions.hpp index b0d5c04810..50d6cb092a 100644 --- a/modules/python/bindings/include/core/image_conversions.hpp +++ b/modules/python/bindings/include/core/image_conversions.hpp @@ -1,6 +1,6 @@ /* * ViSP, open source Visual Servoing Platform software. - * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * Copyright (C) 2005 - 2024 by Inria. All rights reserved. * * This software is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -79,35 +79,35 @@ struct SimpleConversionStruct { pyImageConvert.def_static(name.c_str(), [this](py::array_t &src, py::array_t &dest) { - py::buffer_info bufsrc = src.request(), bufdest = dest.request(); - if (bufsrc.ndim < 2 || bufdest.ndim < 2) { - throw std::runtime_error("Expected to have src and dest arrays with at least two dimensions."); - } - if (bufsrc.shape[0] != bufdest.shape[0] || bufsrc.shape[1] != bufdest.shape[1]) { - std::stringstream ss; - ss << "src and dest must have the same number of pixels, but got src = " << shape_to_string(bufsrc.shape); - ss << "and dest = " << shape_to_string(bufdest.shape); - throw std::runtime_error(ss.str()); - } - if (srcBytesPerPixel > 1 && (bufsrc.ndim != 3 || bufsrc.shape[2] != srcBytesPerPixel)) { - std::stringstream ss; - ss << "Source array should be a 3D array of shape (H, W, " << srcBytesPerPixel << ")"; - throw std::runtime_error(ss.str()); - } - else if (srcBytesPerPixel == 1 && bufsrc.ndim == 3 && bufsrc.shape[2] > 1) { - throw std::runtime_error("Source array should be a either a 2D array of shape H x W or a 3D array of shape (H, W, 1)"); - } - if (destBytesPerPixel > 1 && (bufdest.ndim != 3 || bufdest.shape[2] != destBytesPerPixel)) { - std::stringstream ss; - ss << "Destination array should be a 3D array of shape (H, W, " << destBytesPerPixel << ")"; - throw std::runtime_error(ss.str()); - } - else if (destBytesPerPixel == 1 && bufdest.ndim == 3 && bufdest.shape[2] > 1) { - throw std::runtime_error("Destination should be a either a 2D array of shape H x W or a 3D array of shape (H, W, 1)"); - } - unsigned char *src_ptr = static_cast(bufsrc.ptr); - unsigned char *dest_ptr = static_cast(bufdest.ptr); - call_conversion_fn(fn, src_ptr, dest_ptr, bufsrc.shape[0], bufsrc.shape[1]); + py::buffer_info bufsrc = src.request(), bufdest = dest.request(); + if (bufsrc.ndim < 2 || bufdest.ndim < 2) { + throw std::runtime_error("Expected to have src and dest arrays with at least two dimensions."); + } + if (bufsrc.shape[0] != bufdest.shape[0] || bufsrc.shape[1] != bufdest.shape[1]) { + std::stringstream ss; + ss << "src and dest must have the same number of pixels, but got src = " << shape_to_string(bufsrc.shape); + ss << "and dest = " << shape_to_string(bufdest.shape); + throw std::runtime_error(ss.str()); + } + if (srcBytesPerPixel > 1 && (bufsrc.ndim != 3 || bufsrc.shape[2] != srcBytesPerPixel)) { + std::stringstream ss; + ss << "Source array should be a 3D array of shape (H, W, " << srcBytesPerPixel << ")"; + throw std::runtime_error(ss.str()); + } + else if (srcBytesPerPixel == 1 && bufsrc.ndim == 3 && bufsrc.shape[2] > 1) { + throw std::runtime_error("Source array should be a either a 2D array of shape H x W or a 3D array of shape (H, W, 1)"); + } + if (destBytesPerPixel > 1 && (bufdest.ndim != 3 || bufdest.shape[2] != destBytesPerPixel)) { + std::stringstream ss; + ss << "Destination array should be a 3D array of shape (H, W, " << destBytesPerPixel << ")"; + throw std::runtime_error(ss.str()); + } + else if (destBytesPerPixel == 1 && bufdest.ndim == 3 && bufdest.shape[2] > 1) { + throw std::runtime_error("Destination should be a either a 2D array of shape H x W or a 3D array of shape (H, W, 1)"); + } + unsigned char *src_ptr = static_cast(bufsrc.ptr); + unsigned char *dest_ptr = static_cast(bufdest.ptr); + call_conversion_fn(fn, src_ptr, dest_ptr, bufsrc.shape[0], bufsrc.shape[1]); }, "See C++ documentation of the function for more info", py::arg("src"), py::arg("dest")); } }; @@ -127,35 +127,35 @@ struct SimpleConversionStruct { pyImageConvert.def_static(name.c_str(), [this](py::array_t &src, py::array_t &dest, bool flip) { - py::buffer_info bufsrc = src.request(), bufdest = dest.request(); - if (bufsrc.ndim < 2 || bufdest.ndim < 2) { - throw std::runtime_error("Expected to have src and dest arrays with at least two dimensions."); - } - if (bufsrc.shape[0] != bufdest.shape[0] || bufsrc.shape[1] != bufdest.shape[1]) { - std::stringstream ss; - ss << "src and dest must have the same number of pixels, but got src = " << shape_to_string(bufsrc.shape); - ss << "and dest = " << shape_to_string(bufdest.shape); - throw std::runtime_error(ss.str()); - } - if (srcBytesPerPixel > 1 && (bufsrc.ndim != 3 || bufsrc.shape[2] != srcBytesPerPixel)) { - std::stringstream ss; - ss << "Source array should be a 3D array of shape (H, W, " << srcBytesPerPixel << ")"; - throw std::runtime_error(ss.str()); - } - else if (srcBytesPerPixel == 1 && bufsrc.ndim == 3 && bufsrc.shape[2] > 1) { - throw std::runtime_error("Source array should be a either a 2D array of shape H x W or a 3D array of shape (H, W, 1)"); - } - if (destBytesPerPixel > 1 && (bufdest.ndim != 3 || bufdest.shape[2] != destBytesPerPixel)) { - std::stringstream ss; - ss << "Destination array should be a 3D array of shape (H, W, " << destBytesPerPixel << ")"; - throw std::runtime_error(ss.str()); - } - else if (destBytesPerPixel == 1 && bufdest.ndim == 3 && bufdest.shape[2] > 1) { - throw std::runtime_error("Destination should be a either a 2D array of shape H x W or a 3D array of shape (H, W, 1)"); - } - unsigned char *src_ptr = static_cast(bufsrc.ptr); - unsigned char *dest_ptr = static_cast(bufdest.ptr); - call_conversion_fn(fn, src_ptr, dest_ptr, bufsrc.shape[0], bufsrc.shape[1], flip); + py::buffer_info bufsrc = src.request(), bufdest = dest.request(); + if (bufsrc.ndim < 2 || bufdest.ndim < 2) { + throw std::runtime_error("Expected to have src and dest arrays with at least two dimensions."); + } + if (bufsrc.shape[0] != bufdest.shape[0] || bufsrc.shape[1] != bufdest.shape[1]) { + std::stringstream ss; + ss << "src and dest must have the same number of pixels, but got src = " << shape_to_string(bufsrc.shape); + ss << "and dest = " << shape_to_string(bufdest.shape); + throw std::runtime_error(ss.str()); + } + if (srcBytesPerPixel > 1 && (bufsrc.ndim != 3 || bufsrc.shape[2] != srcBytesPerPixel)) { + std::stringstream ss; + ss << "Source array should be a 3D array of shape (H, W, " << srcBytesPerPixel << ")"; + throw std::runtime_error(ss.str()); + } + else if (srcBytesPerPixel == 1 && bufsrc.ndim == 3 && bufsrc.shape[2] > 1) { + throw std::runtime_error("Source array should be a either a 2D array of shape H x W or a 3D array of shape (H, W, 1)"); + } + if (destBytesPerPixel > 1 && (bufdest.ndim != 3 || bufdest.shape[2] != destBytesPerPixel)) { + std::stringstream ss; + ss << "Destination array should be a 3D array of shape (H, W, " << destBytesPerPixel << ")"; + throw std::runtime_error(ss.str()); + } + else if (destBytesPerPixel == 1 && bufdest.ndim == 3 && bufdest.shape[2] > 1) { + throw std::runtime_error("Destination should be a either a 2D array of shape H x W or a 3D array of shape (H, W, 1)"); + } + unsigned char *src_ptr = static_cast(bufsrc.ptr); + unsigned char *dest_ptr = static_cast(bufdest.ptr); + call_conversion_fn(fn, src_ptr, dest_ptr, bufsrc.shape[0], bufsrc.shape[1], flip); }, "See C++ documentation of the function for more info", py::arg("src"), py::arg("dest"), py::arg("flip") = false); } }; @@ -176,39 +176,39 @@ struct ConversionFromYUVLike { pyImageConvert.def_static(name.c_str(), [this](py::array_t &src, py::array_t &dest) { - py::buffer_info bufsrc = src.request(), bufdest = dest.request(); - if (bufdest.ndim < 2) { - throw std::runtime_error("Expected to have dest array with at least two dimensions."); - } - - unsigned int height = bufdest.shape[0], width = bufdest.shape[1]; - - unsigned expectedSourceBytes = sourceBytesFn(height, width); - - unsigned actualBytes = 1; - for (unsigned int i = 0; i < bufsrc.ndim; ++i) { - actualBytes *= bufsrc.shape[i]; - } - - if (actualBytes != expectedSourceBytes) { - std::stringstream ss; - ss << "Expected to have " << expectedSourceBytes << " bytes in the input array, but got " << actualBytes << " elements."; - throw std::runtime_error(ss.str()); - } - - if (destBytesPerPixel > 1 && (bufdest.ndim != 3 || bufdest.shape[2] != destBytesPerPixel)) { - std::stringstream ss; - ss << "Destination array should be a 3D array of shape (H, W, " << destBytesPerPixel << ")"; - throw std::runtime_error(ss.str()); - } - else if (destBytesPerPixel == 1 && bufdest.ndim == 3 && bufdest.shape[2] > 1) { - throw std::runtime_error("Destination should be a either a 2D array of shape H x W or a 3D array of shape (H, W, 1)"); - } - - - unsigned char *src_ptr = static_cast(bufsrc.ptr); - unsigned char *dest_ptr = static_cast(bufdest.ptr); - call_conversion_fn(fn, src_ptr, dest_ptr, bufdest.shape[0], bufdest.shape[1]); + py::buffer_info bufsrc = src.request(), bufdest = dest.request(); + if (bufdest.ndim < 2) { + throw std::runtime_error("Expected to have dest array with at least two dimensions."); + } + + unsigned int height = bufdest.shape[0], width = bufdest.shape[1]; + + unsigned expectedSourceBytes = sourceBytesFn(height, width); + + unsigned actualBytes = 1; + for (unsigned int i = 0; i < bufsrc.ndim; ++i) { + actualBytes *= bufsrc.shape[i]; + } + + if (actualBytes != expectedSourceBytes) { + std::stringstream ss; + ss << "Expected to have " << expectedSourceBytes << " bytes in the input array, but got " << actualBytes << " elements."; + throw std::runtime_error(ss.str()); + } + + if (destBytesPerPixel > 1 && (bufdest.ndim != 3 || bufdest.shape[2] != destBytesPerPixel)) { + std::stringstream ss; + ss << "Destination array should be a 3D array of shape (H, W, " << destBytesPerPixel << ")"; + throw std::runtime_error(ss.str()); + } + else if (destBytesPerPixel == 1 && bufdest.ndim == 3 && bufdest.shape[2] > 1) { + throw std::runtime_error("Destination should be a either a 2D array of shape H x W or a 3D array of shape (H, W, 1)"); + } + + + unsigned char *src_ptr = static_cast(bufsrc.ptr); + unsigned char *dest_ptr = static_cast(bufdest.ptr); + call_conversion_fn(fn, src_ptr, dest_ptr, bufdest.shape[0], bufdest.shape[1]); }, py::arg("src"), py::arg("dest")); } }; @@ -252,16 +252,16 @@ void add_hsv_double_to_rgb_or_rgba_binding(py::class_ &src, py::array_t &dest) { - py::buffer_info bufsrc = src.request(), bufdest = dest.request(); - const unsigned height = bufsrc.shape[1]; - const unsigned width = bufsrc.shape[2]; - rgb_or_rgba_to_hsv_verification(bufdest, bufsrc, destBytes, height, width); + py::buffer_info bufsrc = src.request(), bufdest = dest.request(); + const unsigned height = bufsrc.shape[1]; + const unsigned width = bufsrc.shape[2]; + rgb_or_rgba_to_hsv_verification(bufdest, bufsrc, destBytes, height, width); - const double *h = static_cast(bufsrc.ptr); - const double *s = h + (height * width); - const double *v = s + (height * width); - unsigned char *dest_ptr = static_cast(bufdest.ptr); - fn(h, s, v, dest_ptr, height * width); + const double *h = static_cast(bufsrc.ptr); + const double *s = h + (height * width); + const double *v = s + (height * width); + unsigned char *dest_ptr = static_cast(bufdest.ptr); + fn(h, s, v, dest_ptr, height * width); }, "Convert from HSV Planes (as a 3 x H x W array) to a an RGB/RGBA array (as an H x W x 3 or H x W x 4 array)", py::arg("hsv"), py::arg("rgb")); } @@ -271,16 +271,16 @@ void add_hsv_uchar_to_rgb_or_rgba_binding(py::class_ &src, py::array_t &dest, bool h_full) { - py::buffer_info bufsrc = src.request(), bufdest = dest.request(); - const unsigned height = bufsrc.shape[1]; - const unsigned width = bufsrc.shape[2]; - rgb_or_rgba_to_hsv_verification(bufdest, bufsrc, destBytes, height, width); + py::buffer_info bufsrc = src.request(), bufdest = dest.request(); + const unsigned height = bufsrc.shape[1]; + const unsigned width = bufsrc.shape[2]; + rgb_or_rgba_to_hsv_verification(bufdest, bufsrc, destBytes, height, width); - const unsigned char *h = static_cast(bufsrc.ptr); - const unsigned char *s = h + (height * width); - const unsigned char *v = s + (height * width); - unsigned char *dest_ptr = static_cast(bufdest.ptr); - fn(h, s, v, dest_ptr, height * width, h_full); + const unsigned char *h = static_cast(bufsrc.ptr); + const unsigned char *s = h + (height * width); + const unsigned char *v = s + (height * width); + unsigned char *dest_ptr = static_cast(bufdest.ptr); + fn(h, s, v, dest_ptr, height * width, h_full); }, "Convert from HSV Planes (as a 3 x H x W array) to a an RGB/RGBA array (as an H x W x 3 or H x W x 4 array)", py::arg("hsv"), py::arg("rgb"), py::arg("h_full") = true); } @@ -291,16 +291,16 @@ void add_rgb_or_rgba_uchar_to_hsv_binding(py::class_ &src, py::array_t &dest, bool h_full) { - py::buffer_info bufsrc = src.request(), bufdest = dest.request(); - const unsigned height = bufdest.shape[1]; - const unsigned width = bufdest.shape[2]; - rgb_or_rgba_to_hsv_verification(bufsrc, bufdest, destBytes, height, width); + py::buffer_info bufsrc = src.request(), bufdest = dest.request(); + const unsigned height = bufdest.shape[1]; + const unsigned width = bufdest.shape[2]; + rgb_or_rgba_to_hsv_verification(bufsrc, bufdest, destBytes, height, width); - unsigned char *h = static_cast(bufdest.ptr); - unsigned char *s = h + (height * width); - unsigned char *v = s + (height * width); - const unsigned char *rgb = static_cast(bufsrc.ptr); - fn(rgb, h, s, v, height * width, h_full); + unsigned char *h = static_cast(bufdest.ptr); + unsigned char *s = h + (height * width); + unsigned char *v = s + (height * width); + const unsigned char *rgb = static_cast(bufsrc.ptr); + fn(rgb, h, s, v, height * width, h_full); }, "Convert from HSV Planes (as a 3 x H x W array) to a an RGB/RGBA array (as an H x W x 3 or H x W x 4 array)", py::arg("rgb"), py::arg("hsv"), py::arg("h_full") = true); } @@ -310,16 +310,16 @@ void add_rgb_or_rgba_double_to_hsv_binding(py::class_ &src, py::array_t &dest) { - py::buffer_info bufsrc = src.request(), bufdest = dest.request(); - const unsigned height = bufdest.shape[1]; - const unsigned width = bufdest.shape[2]; - rgb_or_rgba_to_hsv_verification(bufsrc, bufdest, destBytes, height, width); + py::buffer_info bufsrc = src.request(), bufdest = dest.request(); + const unsigned height = bufdest.shape[1]; + const unsigned width = bufdest.shape[2]; + rgb_or_rgba_to_hsv_verification(bufsrc, bufdest, destBytes, height, width); - double *h = static_cast(bufdest.ptr); - double *s = h + (height * width); - double *v = s + (height * width); - const unsigned char *rgb = static_cast(bufsrc.ptr); - fn(rgb, h, s, v, height * width); + double *h = static_cast(bufdest.ptr); + double *s = h + (height * width); + double *v = s + (height * width); + const unsigned char *rgb = static_cast(bufsrc.ptr); + fn(rgb, h, s, v, height * width); }, "Convert from HSV Planes (as a 3 x H x W array) to a an RGB/RGBA array (as an H x W x 3 or H x W x 4 array)", py::arg("rgb"), py::arg("hsv")); } @@ -331,29 +331,29 @@ void add_demosaic_to_rgba_fn(py::class_ &src, py::array_t &dest, unsigned int num_threads) { - py::buffer_info bufsrc = src.request(), bufdest = dest.request(); - const unsigned destBytes = 4; - - if (bufsrc.ndim != 2 || bufdest.ndim != 3) { - throw std::runtime_error("Expected to have source array with two dimensions and destination RGBA array with 3."); - } - if (bufdest.shape[2] != destBytes) { - std::stringstream ss; - ss << "Target array should be a 3D array of shape (H, W, " << destBytes << ")"; - throw std::runtime_error(ss.str()); - } - const unsigned height = bufdest.shape[0]; - const unsigned width = bufdest.shape[1]; - if (bufsrc.shape[0] != height || bufsrc.shape[1] != width) { - std::stringstream ss; - ss << "src and dest must have the same number of pixels, but got source with dimensions (" << height << ", " << width << ")"; - ss << "and RGB array with dimensions (" << bufdest.shape[0] << ", " << bufdest.shape[1] << ")"; - throw std::runtime_error(ss.str()); - } - - const DataType *bayer = static_cast(bufsrc.ptr); - DataType *rgba = static_cast(bufdest.ptr); - fn(bayer, rgba, height, width, num_threads); + py::buffer_info bufsrc = src.request(), bufdest = dest.request(); + const unsigned destBytes = 4; + + if (bufsrc.ndim != 2 || bufdest.ndim != 3) { + throw std::runtime_error("Expected to have source array with two dimensions and destination RGBA array with 3."); + } + if (bufdest.shape[2] != destBytes) { + std::stringstream ss; + ss << "Target array should be a 3D array of shape (H, W, " << destBytes << ")"; + throw std::runtime_error(ss.str()); + } + const unsigned height = bufdest.shape[0]; + const unsigned width = bufdest.shape[1]; + if (bufsrc.shape[0] != height || bufsrc.shape[1] != width) { + std::stringstream ss; + ss << "src and dest must have the same number of pixels, but got source with dimensions (" << height << ", " << width << ")"; + ss << "and RGB array with dimensions (" << bufdest.shape[0] << ", " << bufdest.shape[1] << ")"; + throw std::runtime_error(ss.str()); + } + + const DataType *bayer = static_cast(bufsrc.ptr); + DataType *rgba = static_cast(bufdest.ptr); + fn(bayer, rgba, height, width, num_threads); }, "Demosaic function implementation, see C++ documentation.", py::arg("bayer_data"), py::arg("rgba"), py::arg("num_threads") = 0); } diff --git a/modules/python/bindings/include/core/images.hpp b/modules/python/bindings/include/core/images.hpp index 2d7bd3630d..3588fe1e6f 100644 --- a/modules/python/bindings/include/core/images.hpp +++ b/modules/python/bindings/include/core/images.hpp @@ -1,6 +1,6 @@ /* * ViSP, open source Visual Servoing Platform software. - * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * Copyright (C) 2005 - 2024 by Inria. All rights reserved. * * This software is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -113,7 +113,7 @@ bindings_vpImage(py::class_, std::shared_ptr>> &pyImage) vpImage result(shape[0], shape[1]); copy_data_from_np(np_array, result.bitmap); return result; - }), R"doc( + }), R"doc( Construct an image by **copying** a 2D numpy array. :param np_array: The numpy array to copy. @@ -158,7 +158,7 @@ bindings_vpImage(py::class_, std::shared_ptr>> &pyImage) vpImage result(shape[0], shape[1]); copy_data_from_np(np_array, (NpRep *)result.bitmap); return result; - }), R"doc( + }), R"doc( Construct an image by **copying** a 3D numpy array. this numpy array should be of the form :math:`H \times W \times 4` where the 4 denotes the red, green, blue and alpha components of the image. @@ -203,7 +203,7 @@ bindings_vpImage(py::class_, std::shared_ptr>> &pyImage) vpImage result(shape[0], shape[1]); copy_data_from_np(np_array, (NpRep *)result.bitmap); return result; - }), R"doc( + }), R"doc( Construct an image by **copying** a 3D numpy array. this numpy array should be of the form :math:`H \times W \times 3` where the 3 denotes the red, green and blue components of the image. diff --git a/modules/python/bindings/include/core/pixel_meter.hpp b/modules/python/bindings/include/core/pixel_meter.hpp index 627b5ef3d5..40644d61cf 100644 --- a/modules/python/bindings/include/core/pixel_meter.hpp +++ b/modules/python/bindings/include/core/pixel_meter.hpp @@ -1,6 +1,6 @@ /* * ViSP, open source Visual Servoing Platform software. - * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * Copyright (C) 2005 - 2024 by Inria. All rights reserved. * * This software is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/modules/python/bindings/include/core/utils.hpp b/modules/python/bindings/include/core/utils.hpp index 404dbc53fd..11fcea004e 100644 --- a/modules/python/bindings/include/core/utils.hpp +++ b/modules/python/bindings/include/core/utils.hpp @@ -1,6 +1,6 @@ /* * ViSP, open source Visual Servoing Platform software. - * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * Copyright (C) 2005 - 2024 by Inria. All rights reserved. * * This software is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/modules/python/bindings/include/mbt.hpp b/modules/python/bindings/include/mbt.hpp index 8298b90e9b..30c2276bdd 100644 --- a/modules/python/bindings/include/mbt.hpp +++ b/modules/python/bindings/include/mbt.hpp @@ -1,6 +1,6 @@ /* * ViSP, open source Visual Servoing Platform software. - * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * Copyright (C) 2005 - 2024 by Inria. All rights reserved. * * This software is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -46,30 +46,30 @@ void bindings_vpMbGenericTracker(py::class_ *> &mapOfImages, std::map> &mapOfPointClouds) { - std::map mapOfWidths, mapOfHeights; - std::map mapOfVectors; - for (const auto &point_cloud_pair: mapOfPointClouds) { + std::map mapOfWidths, mapOfHeights; + std::map mapOfVectors; + for (const auto &point_cloud_pair: mapOfPointClouds) { - py::buffer_info buffer = point_cloud_pair.second.request(); - if (buffer.ndim != 3 && buffer.shape[2] != 3) { - std::stringstream ss; - ss << "Pointcloud error: pointcloud at key: " << point_cloud_pair.first << - " should be a 3D numpy array of dimensions H X W x 3"; - throw std::runtime_error(ss.str()); - } - const auto shape = buffer.shape; - mapOfHeights[point_cloud_pair.first] = shape[0]; - mapOfWidths[point_cloud_pair.first] = shape[1]; - vpMatrix pc(shape[0] * shape[1], 3); - const double *data = point_cloud_pair.second.unchecked<3>().data(0, 0, 0); - memcpy(pc.data, data, shape[0] * shape[1] * 3 * sizeof(double)); - mapOfVectors[point_cloud_pair.first] = std::move(pc); - } - std::map mapOfVectorPtrs; - for (const auto &p: mapOfVectors) { - mapOfVectorPtrs[p.first] = &(p.second); - } - self.track(mapOfImages, mapOfVectorPtrs, mapOfWidths, mapOfHeights); + py::buffer_info buffer = point_cloud_pair.second.request(); + if (buffer.ndim != 3 && buffer.shape[2] != 3) { + std::stringstream ss; + ss << "Pointcloud error: pointcloud at key: " << point_cloud_pair.first << + " should be a 3D numpy array of dimensions H X W x 3"; + throw std::runtime_error(ss.str()); + } + const auto shape = buffer.shape; + mapOfHeights[point_cloud_pair.first] = shape[0]; + mapOfWidths[point_cloud_pair.first] = shape[1]; + vpMatrix pc(shape[0] * shape[1], 3); + const double *data = point_cloud_pair.second.unchecked<3>().data(0, 0, 0); + memcpy(pc.data, data, shape[0] * shape[1] * 3 * sizeof(double)); + mapOfVectors[point_cloud_pair.first] = std::move(pc); + } + std::map mapOfVectorPtrs; + for (const auto &p: mapOfVectors) { + mapOfVectorPtrs[p.first] = &(p.second); + } + self.track(mapOfImages, mapOfVectorPtrs, mapOfWidths, mapOfHeights); }, R"doc( Perform tracking, with point clouds being represented as numpy arrays diff --git a/modules/python/bindings/include/visual_features.hpp b/modules/python/bindings/include/visual_features.hpp index 888e8b7951..f1a46a4f41 100644 --- a/modules/python/bindings/include/visual_features.hpp +++ b/modules/python/bindings/include/visual_features.hpp @@ -1,6 +1,6 @@ /* * ViSP, open source Visual Servoing Platform software. - * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * Copyright (C) 2005 - 2024 by Inria. All rights reserved. * * This software is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/modules/python/bindings/visp/display_utils.py b/modules/python/bindings/visp/display_utils.py index 95546642e9..78428c3608 100644 --- a/modules/python/bindings/visp/display_utils.py +++ b/modules/python/bindings/visp/display_utils.py @@ -1,7 +1,7 @@ ############################################################################# # # ViSP, open source Visual Servoing Platform software. -# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# Copyright (C) 2005 - 2024 by Inria. All rights reserved. # # This software is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by