From 32cb68ffa7ef9acb368a9e0e7dccb44f2107f2ac Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Wed, 27 Mar 2024 18:59:18 +0100 Subject: [PATCH 01/32] Optimize hsv related conversion functions Move all the hsv functionalities in a separate file. --- modules/core/src/image/vpImageConvert.cpp | 319 +----------- modules/core/src/image/vpImageConvert_hsv.cpp | 472 ++++++++++++++++++ 2 files changed, 476 insertions(+), 315 deletions(-) create mode 100644 modules/core/src/image/vpImageConvert_hsv.cpp diff --git a/modules/core/src/image/vpImageConvert.cpp b/modules/core/src/image/vpImageConvert.cpp index b9a9b592e3..b4a079bbb1 100644 --- a/modules/core/src/image/vpImageConvert.cpp +++ b/modules/core/src/image/vpImageConvert.cpp @@ -4524,16 +4524,16 @@ void vpImageConvert::split(const vpImage &src, vpImage *p src.getWidth()); if (!pR) { - delete [] ptrR; + delete[] ptrR; } if (!pG) { - delete [] ptrG; + delete[] ptrG; } if (!pB) { - delete [] ptrB; + delete[] ptrB; } if (!pa) { - delete [] ptrA; + delete[] ptrA; } } #else @@ -4719,317 +4719,6 @@ void vpImageConvert::MONO16ToRGBa(unsigned char *grey16, unsigned char *rgba, un } } -/*! - * Convert an HSV image to a RGB or RGBa image depending on the value of \e step. - * \param[in] hue_ : Input image H channel. - * \param[in] saturation_ : Input image S channel. - * \param[in] value_ : Input image V channel. - * \param[out] rgb : Pointer to the 24-bit or 32-bits color image that should be allocated with a size of - * width * height * step. - * \param[in] size : The image size or the number of pixels corresponding to the image width * height. - * \param[in] step : Number of channels of the output color image; 3 for an RGB image, 4 for an RGBA image. - */ -void vpImageConvert::HSV2RGB(const double *hue_, const double *saturation_, const double *value_, unsigned char *rgb, - unsigned int size, unsigned int step) -{ - for (unsigned int i = 0; i < size; ++i) { - double hue = hue_[i], saturation = saturation_[i], value = value_[i]; - - if (vpMath::equal(saturation, 0.0, std::numeric_limits::epsilon())) { - hue = value; - saturation = value; - } - else { - double h = hue * 6.0; - double s = saturation; - double v = value; - - if (vpMath::equal(h, 6.0, std::numeric_limits::epsilon())) { - h = 0.0; - } - - double f = h - static_cast(h); - double p = v * (1.0 - s); - double q = v * (1.0 - (s * f)); - double t = v * (1.0 - (s * (1.0 - f))); - - switch (static_cast(h)) { - case 0: - hue = v; - saturation = t; - value = p; - break; - - case 1: - hue = q; - saturation = v; - value = p; - break; - - case 2: - hue = p; - saturation = v; - value = t; - break; - - case 3: - hue = p; - saturation = q; - value = v; - break; - - case 4: - hue = t; - saturation = p; - value = v; - break; - - default: // case 5: - hue = v; - saturation = p; - value = q; - break; - } - } - - rgb[i * step] = static_cast(vpMath::round(hue * 255.0)); - rgb[i * step + 1] = static_cast(vpMath::round(saturation * 255.0)); - rgb[i * step + 2] = static_cast(vpMath::round(value * 255.0)); - if (step == 4) {// alpha - rgb[i * step + 3] = vpRGBa::alpha_default; - } - } -} - -/*! - * Convert an RGB or RGBa color image depending on the value of \e step into an HSV image. - * \param[out] rgb : Pointer to the 24-bit or 32-bits color image that should be allocated with a size of - * width * height * step. - * \param[out] hue : Output H channel. - * \param[out] saturation : Output S channel. - * \param[out] value : Output V channel. - * \param[in] size : The image size or the number of pixels corresponding to the image width * height. - * \param[in] step : Number of channels of the input color image; 3 for an RGB image, 4 for an RGBA image. - */ -void vpImageConvert::RGB2HSV(const unsigned char *rgb, double *hue, double *saturation, double *value, - unsigned int size, unsigned int step) -{ - for (unsigned int i = 0; i < size; ++i) { - double red, green, blue; - double h, s, v; - double min, max; - - red = rgb[i * step] / 255.0; - green = rgb[i * step + 1] / 255.0; - blue = rgb[i * step + 2] / 255.0; - - if (red > green) { - max = ((std::max))(red, blue); - min = ((std::min))(green, blue); - } - else { - max = ((std::max))(green, blue); - min = ((std::min))(red, blue); - } - - v = max; - - if (!vpMath::equal(max, 0.0, std::numeric_limits::epsilon())) { - s = (max - min) / max; - } - else { - s = 0.0; - } - - if (vpMath::equal(s, 0.0, std::numeric_limits::epsilon())) { - h = 0.0; - } - else { - double delta = max - min; - if (vpMath::equal(delta, 0.0, std::numeric_limits::epsilon())) { - delta = 1.0; - } - - if (vpMath::equal(red, max, std::numeric_limits::epsilon())) { - h = (green - blue) / delta; - } - else if (vpMath::equal(green, max, std::numeric_limits::epsilon())) { - h = 2 + (blue - red) / delta; - } - else { - h = 4 + (red - green) / delta; - } - - h /= 6.0; - if (h < 0.0) { - h += 1.0; - } - else if (h > 1.0) { - h -= 1.0; - } - } - - hue[i] = h; - saturation[i] = s; - value[i] = v; - } -} - -/*! - Converts an array of hue, saturation and value to an array of RGBa values. - - Alpha component of the converted image is set to vpRGBa::alpha_default. - - \param[in] hue : Array of hue values (range between [0 - 1]). - \param[in] saturation : Array of saturation values (range between [0 - 1]). - \param[in] value : Array of value values (range between [0 - 1]). - \param[out] rgba : Pointer to the 32-bit RGBA image that should - be allocated with a size of width * height * 4. Alpha channel is here set to vpRGBa::alpha_default. - \param[in] size : The image size or the number of pixels corresponding to the image width * height. -*/ -void vpImageConvert::HSVToRGBa(const double *hue, const double *saturation, const double *value, unsigned char *rgba, - unsigned int size) -{ - vpImageConvert::HSV2RGB(hue, saturation, value, rgba, size, 4); -} - -/*! - Converts an array of hue, saturation and value to an array of RGBa values. - - Alpha component of the converted image is set to vpRGBa::alpha_default. - - \param[in] hue : Array of hue values (range between [0 - 255]). - \param[in] saturation : Array of saturation values (range between [0 - 255]). - \param[in] value : Array of value values (range between [0 - 255]). - \param[out] rgba : Pointer to the 32-bit RGBA image that should - be allocated with a size of width * height * 4. Alpha channel is here set to vpRGBa::alpha_default. - \param[in] size : The image size or the number of pixels corresponding to the image width * height. -*/ -void vpImageConvert::HSVToRGBa(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, - unsigned char *rgba, unsigned int size) -{ - for (unsigned int i = 0; i < size; ++i) { - double h = hue[i] / 255.0, s = saturation[i] / 255.0, v = value[i] / 255.0; - - vpImageConvert::HSVToRGBa(&h, &s, &v, (rgba + (i * 4)), 1); - } -} - -/*! - Converts an array of RGBa to an array of hue, saturation, value values. - The alpha channel is not used. - - \param[in] rgba : Pointer to the 32-bits RGBA bitmap. - \param[out] hue : Array of hue values converted from RGB color space (range - between [0 - 1]). - \param[out] saturation : Array of saturation values converted - from RGB color space (range between [0 - 1]). - \param[out] value : Array of value values converted from RGB color space (range between [0 - 1]). - \param[in] size : The image size or the number of pixels corresponding to the image width * height. -*/ -void vpImageConvert::RGBaToHSV(const unsigned char *rgba, double *hue, double *saturation, double *value, - unsigned int size) -{ - vpImageConvert::RGB2HSV(rgba, hue, saturation, value, size, 4); -} - -/*! - Converts an array of RGBa to an array of hue, saturation, value values. - The alpha channel is not used. - - \param[in] rgba : Pointer to the 32-bits RGBA bitmap. - \param[out] hue : Array of hue values converted from RGB color space (range between [0 - 255]). - \param[out] saturation : Array of saturation values converted - from RGB color space (range between [0 - 255]). - \param[out] value : Array of value values converted from RGB color space (range between [0 - 255]). - \param[in] size : The image size or the number of pixels corresponding to the image width * height. -*/ -void vpImageConvert::RGBaToHSV(const unsigned char *rgba, unsigned char *hue, unsigned char *saturation, - unsigned char *value, unsigned int size) -{ - for (unsigned int i = 0; i < size; ++i) { - double h, s, v; - vpImageConvert::RGBaToHSV((rgba + (i * 4)), &h, &s, &v, 1); - - hue[i] = static_cast(255.0 * h); - saturation[i] = static_cast(255.0 * s); - value[i] = static_cast(255.0 * v); - } -} - -/*! - Converts an array of hue, saturation and value to an array of RGB values. - - \param[in] hue : Array of hue values (range between [0 - 1]). - \param[in] saturation : Array of saturation values (range between [0 - 1]). - \param[in] value : Array of value values (range between [0 - 1]). - \param[out] rgb : Pointer to the 24-bit RGB image that should be allocated with a size of - width * height * 3. - \param[in] size : The image size or the number of pixels corresponding to the image width * height. -*/ -void vpImageConvert::HSVToRGB(const double *hue, const double *saturation, const double *value, unsigned char *rgb, - unsigned int size) -{ - vpImageConvert::HSV2RGB(hue, saturation, value, rgb, size, 3); -} - -/*! - Converts an array of hue, saturation and value to an array of RGB values. - - \param[in] hue : Array of hue values (range between [0 - 255]). - \param[in] saturation : Array of saturation values (range between [0 - 255]). - \param[in] value : Array of value values (range between [0 - 255]). - \param[out] rgb : Pointer to the 24-bit RGB image that should be allocated with a size of width * height * 3. - \param[in] size : The image size or the number of pixels corresponding to the image width * height. -*/ -void vpImageConvert::HSVToRGB(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, - unsigned char *rgb, unsigned int size) -{ - for (unsigned int i = 0; i < size; ++i) { - double h = hue[i] / 255.0, s = saturation[i] / 255.0, v = value[i] / 255.0; - - vpImageConvert::HSVToRGB(&h, &s, &v, (rgb + (i * 3)), 1); - } -} - -/*! - Converts an array of RGB to an array of hue, saturation, value values. - - \param[in] rgb : Pointer to the 24-bits RGB bitmap. - \param[out] hue : Array of hue values converted from RGB color space (range between [0 - 255]). - \param[out] saturation : Array of saturation values converted from RGB color space (range between [0 - 255]). - \param[out] value : Array of value values converted from RGB color space (range between [0 - 255]). - \param[in] size : The image size or the number of pixels corresponding to the image width * height. -*/ -void vpImageConvert::RGBToHSV(const unsigned char *rgb, double *hue, double *saturation, double *value, - unsigned int size) -{ - vpImageConvert::RGB2HSV(rgb, hue, saturation, value, size, 3); -} - -/*! - Converts an array of RGB to an array of hue, saturation, value values. - - \param[in] rgb : Pointer to the 24-bits RGB bitmap. - \param[out] hue : Array of hue values converted from RGB color space (range between [0 - 255]). - \param[out] saturation : Array of saturation values converted from RGB color space (range between [0 - 255]). - \param[out] value : Array of value values converted from RGB color space (range between [0 - 255]). - \param[in] size : The image size or the number of pixels corresponding to the image width * height. -*/ -void vpImageConvert::RGBToHSV(const unsigned char *rgb, unsigned char *hue, unsigned char *saturation, - unsigned char *value, unsigned int size) -{ - for (unsigned int i = 0; i < size; ++i) { - double h, s, v; - - vpImageConvert::RGBToHSV((rgb + (i * 3)), &h, &s, &v, 1); - - hue[i] = static_cast(255.0 * h); - saturation[i] = static_cast(255.0 * s); - value[i] = static_cast(255.0 * v); - } -} - // Bilinear /*! diff --git a/modules/core/src/image/vpImageConvert_hsv.cpp b/modules/core/src/image/vpImageConvert_hsv.cpp new file mode 100644 index 0000000000..0746144b72 --- /dev/null +++ b/modules/core/src/image/vpImageConvert_hsv.cpp @@ -0,0 +1,472 @@ +/**************************************************************************** + * + * 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: + * Convert image types. + * +*****************************************************************************/ + +/*! + \file vpImageConvert_hsv.cpp + \brief HSV from/to conversion. +*/ + +#if defined(_OPENMP) +#include +#endif + +#include +#include + +/*! + * Convert an HSV image to a RGB or RGBa image depending on the value of \e step. + * \param[in] hue_ : Input image H channel. + * \param[in] saturation_ : Input image S channel. + * \param[in] value_ : Input image V channel. + * \param[out] rgb : Pointer to the 24-bit or 32-bits color image that should be allocated with a size of + * width * height * step. + * \param[in] size : The image size or the number of pixels corresponding to the image width * height. + * \param[in] step : Number of channels of the output color image; 3 for an RGB image, 4 for an RGBA image. + */ +void vpImageConvert::HSV2RGB(const double *hue_, const double *saturation_, const double *value_, unsigned char *rgb, + unsigned int size, unsigned int step) +{ + for (unsigned int i = 0; i < size; ++i) { + double hue = hue_[i], saturation = saturation_[i], value = value_[i]; + + if (vpMath::equal(saturation, 0.0, std::numeric_limits::epsilon())) { + hue = value; + saturation = value; + } + else { + double h = hue * 6.0; + double s = saturation; + double v = value; + + if (vpMath::equal(h, 6.0, std::numeric_limits::epsilon())) { + h = 0.0; + } + + double f = h - static_cast(h); + double p = v * (1.0 - s); + double q = v * (1.0 - (s * f)); + double t = v * (1.0 - (s * (1.0 - f))); + + switch (static_cast(h)) { + case 0: + hue = v; + saturation = t; + value = p; + break; + + case 1: + hue = q; + saturation = v; + value = p; + break; + + case 2: + hue = p; + saturation = v; + value = t; + break; + + case 3: + hue = p; + saturation = q; + value = v; + break; + + case 4: + hue = t; + saturation = p; + value = v; + break; + + default: // case 5: + hue = v; + saturation = p; + value = q; + break; + } + } + + rgb[i * step] = static_cast(vpMath::round(hue * 255.0)); + rgb[i * step + 1] = static_cast(vpMath::round(saturation * 255.0)); + rgb[i * step + 2] = static_cast(vpMath::round(value * 255.0)); + if (step == 4) {// alpha + rgb[i * step + 3] = vpRGBa::alpha_default; + } + } +} + +/*! + * Convert an RGB or RGBa color image depending on the value of \e step into an HSV image. + * \param[out] rgb : Pointer to the 24-bit or 32-bits color image that should be allocated with a size of + * width * height * step. + * \param[out] hue : Output H channel with values in range [0, 1]. + * \param[out] saturation : Output S channel with values in range [0, 1]. + * \param[out] value : Output V channel with values in range [0, 1]. + * \param[in] size : The image size or the number of pixels corresponding to the image width * height. + * \param[in] step : Number of channels of the input color image; 3 for an RGB image, 4 for an RGBA image. + */ +void vpImageConvert::RGB2HSV(const unsigned char *rgb, double *hue, double *saturation, double *value, + unsigned int size, unsigned int step) +{ + for (unsigned int i = 0; i < size; ++i) { + double red, green, blue; + double h, s, v; + double min, max; + + red = rgb[i * step] / 255.0; + green = rgb[i * step + 1] / 255.0; + blue = rgb[i * step + 2] / 255.0; + + if (red > green) { + max = ((std::max))(red, blue); + min = ((std::min))(green, blue); + } + else { + max = ((std::max))(green, blue); + min = ((std::min))(red, blue); + } + + v = max; + + if (!vpMath::equal(max, 0.0, std::numeric_limits::epsilon())) { + s = (max - min) / max; + } + else { + s = 0.0; + } + + if (vpMath::equal(s, 0.0, std::numeric_limits::epsilon())) { + h = 0.0; + } + else { + double delta = max - min; + if (vpMath::equal(delta, 0.0, std::numeric_limits::epsilon())) { + delta = 1.0; + } + + if (vpMath::equal(red, max, std::numeric_limits::epsilon())) { + h = (green - blue) / delta; + } + else if (vpMath::equal(green, max, std::numeric_limits::epsilon())) { + h = 2 + (blue - red) / delta; + } + else { + h = 4 + (red - green) / delta; + } + + h /= 6.0; + if (h < 0.0) { + h += 1.0; + } + else if (h > 1.0) { + h -= 1.0; + } + } + + hue[i] = h; + saturation[i] = s; + value[i] = v; + } +} + +/*! + Converts an array of hue, saturation and value to an array of RGBa values. + + Alpha component of the converted image is set to vpRGBa::alpha_default. + + \param[in] hue : Array of hue values (range between [0 - 1]). + \param[in] saturation : Array of saturation values (range between [0 - 1]). + \param[in] value : Array of value values (range between [0 - 1]). + \param[out] rgba : Pointer to the 32-bit RGBA image that should + be allocated with a size of width * height * 4. Alpha channel is here set to vpRGBa::alpha_default. + \param[in] size : The image size or the number of pixels corresponding to the image width * height. +*/ +void vpImageConvert::HSVToRGBa(const double *hue, const double *saturation, const double *value, unsigned char *rgba, + unsigned int size) +{ + vpImageConvert::HSV2RGB(hue, saturation, value, rgba, size, 4); +} + +/*! + Converts an array of hue, saturation and value to an array of RGBa values. + + Alpha component of the converted image is set to vpRGBa::alpha_default. + + \param[in] hue : Array of hue values (range between [0 - 255]). + \param[in] saturation : Array of saturation values (range between [0 - 255]). + \param[in] value : Array of value values (range between [0 - 255]). + \param[out] rgba : Pointer to the 32-bit RGBA image that should + be allocated with a size of width * height * 4. Alpha channel is here set to vpRGBa::alpha_default. + \param[in] size : The image size or the number of pixels corresponding to the image width * height. +*/ +void vpImageConvert::HSVToRGBa(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, + unsigned char *rgba, unsigned int size) +{ + for (unsigned int i = 0; i < size; ++i) { + double h = hue[i] / 255.0, s = saturation[i] / 255.0, v = value[i] / 255.0; + + vpImageConvert::HSVToRGBa(&h, &s, &v, (rgba + (i * 4)), 1); + } +} + +/*! + Converts an array of RGBa to an array of hue, saturation, value values. + The alpha channel is not used. + + \param[in] rgba : Pointer to the 32-bits RGBA bitmap. + \param[out] hue : Array of hue values converted from RGB color space in range [0 - 1]. + \param[out] saturation : Array of saturation values converted from RGB color space in range [0 - 1]. + \param[out] value : Array of value values converted from RGB color space in range [0 - 1]. + \param[in] size : The image size or the number of pixels corresponding to the image width * height. +*/ +void vpImageConvert::RGBaToHSV(const unsigned char *rgba, double *hue, double *saturation, double *value, + unsigned int size) +{ + unsigned int step = 4; +#if defined(_OPENMP) +#pragma omp parallel for +#endif + for (unsigned int i = 0; i < size; ++i) { + double red, green, blue; + double h, s, v; + double min, max; + + unsigned int i_ = i * step; + + red = rgba[i_]; + green = rgba[++i_]; + blue = rgba[++i_]; + + if (red > green) { + max = std::max(red, blue); + min = std::min(green, blue); + } + else { + max = std::max(green, blue); + min = std::min(red, blue); + } + + v = max; + + if (!vpMath::equal(max, 0., std::numeric_limits::epsilon())) { + s = 255. * (max - min) / max; + } + else { + s = 0.; + } + + if (vpMath::equal(s, 0., std::numeric_limits::epsilon())) { + h = 0.; + } + else { + double delta = max - min; + if (vpMath::equal(delta, 0., std::numeric_limits::epsilon())) { + delta = 255.; + } + + if (vpMath::equal(red, max, std::numeric_limits::epsilon())) { + h = 43. * (green - blue) / delta; + } + else if (vpMath::equal(green, max, std::numeric_limits::epsilon())) { + h = 85. + 43. * (blue - red) / delta; + } + else { + h = 171. + 43. * (red - green) / delta; + } + + if (h < 0.) { + h += 255.; + } + else if (h > 255.) { + h -= 255.; + } + } + + hue[i] = h; + saturation[i] = s; + value[i] = v; + } +} + +/*! + Converts an array of RGBa to an array of hue, saturation, value values. + The alpha channel is not used. + + \param[in] rgba : Pointer to the 32-bits RGBA bitmap. + \param[out] hue : Array of hue values converted from RGB color space in range [0, 255]. + \param[out] saturation : Array of saturation values converted from RGB color space in range [0 - 255]. + \param[out] value : Array of value values converted from RGB color space in range [0 - 255]. + \param[in] size : The image size or the number of pixels corresponding to the image width * height. +*/ +void vpImageConvert::RGBaToHSV(const unsigned char *rgba, unsigned char *hue, unsigned char *saturation, + unsigned char *value, unsigned int size) +{ + unsigned int step = 4; +#if defined(_OPENMP) +#pragma omp parallel for +#endif + for (unsigned int i = 0; i < size; ++i) { + float red, green, blue; + float h, s, v; + float min, max; + unsigned int i_ = i * step; + + red = rgba[i_]; + green = rgba[++i_]; + blue = rgba[++i_]; + + if (red > green) { + max = std::max(red, blue); + min = std::min(green, blue); + } + else { + max = std::max(green, blue); + min = std::min(red, blue); + } + + v = max; + + if (!vpMath::equal(max, 0.f, std::numeric_limits::epsilon())) { + s = 255.f * (max - min) / max; + } + else { + s = 0.f; + } + + if (vpMath::equal(s, 0.f, std::numeric_limits::epsilon())) { + h = 0.f; + } + else { + float delta = max - min; + if (vpMath::equal(delta, 0.0, std::numeric_limits::epsilon())) { + delta = 255.f; + } + + if (vpMath::equal(red, max, std::numeric_limits::epsilon())) { + h = 43.f * (green - blue) / delta; + } + else if (vpMath::equal(green, max, std::numeric_limits::epsilon())) { + h = 85.f + 43.f * (blue - red) / delta; + } + else { + h = 171.f + 43.f * (red - green) / delta; + } + + if (h < 0.f) { + h += 255.f; + } + else if (h > 255.f) { + h -= 255.f; + } + } + + hue[i] = static_cast(h); + saturation[i] = static_cast(s); + value[i] = static_cast(v); + } +} + +/*! + Converts an array of hue, saturation and value to an array of RGB values. + + \param[in] hue : Array of hue values (range between [0 - 1]). + \param[in] saturation : Array of saturation values (range between [0 - 1]). + \param[in] value : Array of value values (range between [0 - 1]). + \param[out] rgb : Pointer to the 24-bit RGB image that should be allocated with a size of + width * height * 3. + \param[in] size : The image size or the number of pixels corresponding to the image width * height. +*/ +void vpImageConvert::HSVToRGB(const double *hue, const double *saturation, const double *value, unsigned char *rgb, + unsigned int size) +{ + vpImageConvert::HSV2RGB(hue, saturation, value, rgb, size, 3); +} + +/*! + Converts an array of hue, saturation and value to an array of RGB values. + + \param[in] hue : Array of hue values (range between [0 - 255]). + \param[in] saturation : Array of saturation values (range between [0 - 255]). + \param[in] value : Array of value values (range between [0 - 255]). + \param[out] rgb : Pointer to the 24-bit RGB image that should be allocated with a size of width * height * 3. + \param[in] size : The image size or the number of pixels corresponding to the image width * height. +*/ +void vpImageConvert::HSVToRGB(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, + unsigned char *rgb, unsigned int size) +{ + for (unsigned int i = 0; i < size; ++i) { + double h = hue[i] / 255.0, s = saturation[i] / 255.0, v = value[i] / 255.0; + + vpImageConvert::HSVToRGB(&h, &s, &v, (rgb + (i * 3)), 1); + } +} + +/*! + Converts an array of RGB to an array of hue, saturation, value values. + + \param[in] rgb : Pointer to the 24-bits RGB bitmap. + \param[out] hue : Array of hue values converted from RGB color space(range between[0 - 255]). + \param[out] saturation : Array of saturation values converted from RGB color space(range between[0 - 255]). + \param[out] value : Array of value values converted from RGB color space(range between[0 - 255]). + \param[in] size : The image size or the number of pixels corresponding to the image width * height. +*/ +void vpImageConvert::RGBToHSV(const unsigned char *rgb, double *hue, double *saturation, double *value, + unsigned int size) +{ + vpImageConvert::RGB2HSV(rgb, hue, saturation, value, size, 3); +} + +/*! + Converts an array of RGB to an array of hue, saturation, value values. + + \param[in] rgb : Pointer to the 24-bits RGB bitmap. + \param[out] hue : Array of hue values converted from RGB color space (range between [0 - 255]). + \param[out] saturation : Array of saturation values converted from RGB color space (range between [0 - 255]). + \param[out] value : Array of value values converted from RGB color space (range between [0 - 255]). + \param[in] size : The image size or the number of pixels corresponding to the image width * height. +*/ +void vpImageConvert::RGBToHSV(const unsigned char *rgb, unsigned char *hue, unsigned char *saturation, + unsigned char *value, unsigned int size) +{ + for (unsigned int i = 0; i < size; ++i) { + double h, s, v; + + vpImageConvert::RGBToHSV((rgb + (i * 3)), &h, &s, &v, 1); + + hue[i] = static_cast(255.0 * h); + saturation[i] = static_cast(255.0 * s); + value[i] = static_cast(255.0 * v); + } +} From 187d74761f65da3d36603140d169cd9489344459 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Thu, 28 Mar 2024 08:45:16 +0100 Subject: [PATCH 02/32] Keep compat in RGB2HSV with double parameters to ensure hsv are in range [0,1] --- modules/core/src/image/vpImageConvert_hsv.cpp | 75 ++----------------- 1 file changed, 8 insertions(+), 67 deletions(-) diff --git a/modules/core/src/image/vpImageConvert_hsv.cpp b/modules/core/src/image/vpImageConvert_hsv.cpp index 0746144b72..3c4d66fd04 100644 --- a/modules/core/src/image/vpImageConvert_hsv.cpp +++ b/modules/core/src/image/vpImageConvert_hsv.cpp @@ -140,14 +140,18 @@ void vpImageConvert::HSV2RGB(const double *hue_, const double *saturation_, cons void vpImageConvert::RGB2HSV(const unsigned char *rgb, double *hue, double *saturation, double *value, unsigned int size, unsigned int step) { +#if defined(_OPENMP) +#pragma omp parallel for +#endif for (unsigned int i = 0; i < size; ++i) { double red, green, blue; double h, s, v; double min, max; + unsigned int i_ = i * step; - red = rgb[i * step] / 255.0; - green = rgb[i * step + 1] / 255.0; - blue = rgb[i * step + 2] / 255.0; + red = rgb[i_] / 255.0; + green = rgb[++i_] / 255.0; + blue = rgb[++i_] / 255.0; if (red > green) { max = ((std::max))(red, blue); @@ -254,70 +258,7 @@ void vpImageConvert::HSVToRGBa(const unsigned char *hue, const unsigned char *sa void vpImageConvert::RGBaToHSV(const unsigned char *rgba, double *hue, double *saturation, double *value, unsigned int size) { - unsigned int step = 4; -#if defined(_OPENMP) -#pragma omp parallel for -#endif - for (unsigned int i = 0; i < size; ++i) { - double red, green, blue; - double h, s, v; - double min, max; - - unsigned int i_ = i * step; - - red = rgba[i_]; - green = rgba[++i_]; - blue = rgba[++i_]; - - if (red > green) { - max = std::max(red, blue); - min = std::min(green, blue); - } - else { - max = std::max(green, blue); - min = std::min(red, blue); - } - - v = max; - - if (!vpMath::equal(max, 0., std::numeric_limits::epsilon())) { - s = 255. * (max - min) / max; - } - else { - s = 0.; - } - - if (vpMath::equal(s, 0., std::numeric_limits::epsilon())) { - h = 0.; - } - else { - double delta = max - min; - if (vpMath::equal(delta, 0., std::numeric_limits::epsilon())) { - delta = 255.; - } - - if (vpMath::equal(red, max, std::numeric_limits::epsilon())) { - h = 43. * (green - blue) / delta; - } - else if (vpMath::equal(green, max, std::numeric_limits::epsilon())) { - h = 85. + 43. * (blue - red) / delta; - } - else { - h = 171. + 43. * (red - green) / delta; - } - - if (h < 0.) { - h += 255.; - } - else if (h > 255.) { - h -= 255.; - } - } - - hue[i] = h; - saturation[i] = s; - value[i] = v; - } + vpImageConvert::RGB2HSV(rgba, hue, saturation, value, size, 4); } /*! From 7de32ebb468e1339964715fc67d4017733e7b31e Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Thu, 28 Mar 2024 14:18:58 +0100 Subject: [PATCH 03/32] Fix error that occurs under windows: index variable in OpenMP 'for' statement must have signed integral type --- modules/core/src/image/vpImageConvert_hsv.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/core/src/image/vpImageConvert_hsv.cpp b/modules/core/src/image/vpImageConvert_hsv.cpp index 3c4d66fd04..dfda969336 100644 --- a/modules/core/src/image/vpImageConvert_hsv.cpp +++ b/modules/core/src/image/vpImageConvert_hsv.cpp @@ -140,14 +140,15 @@ void vpImageConvert::HSV2RGB(const double *hue_, const double *saturation_, cons void vpImageConvert::RGB2HSV(const unsigned char *rgb, double *hue, double *saturation, double *value, unsigned int size, unsigned int step) { + int size_ = static_cast(size); #if defined(_OPENMP) #pragma omp parallel for #endif - for (unsigned int i = 0; i < size; ++i) { + for (int i = 0; i < size_; ++i) { double red, green, blue; double h, s, v; double min, max; - unsigned int i_ = i * step; + int i_ = i * step; red = rgb[i_] / 255.0; green = rgb[++i_] / 255.0; @@ -274,11 +275,12 @@ void vpImageConvert::RGBaToHSV(const unsigned char *rgba, double *hue, double *s void vpImageConvert::RGBaToHSV(const unsigned char *rgba, unsigned char *hue, unsigned char *saturation, unsigned char *value, unsigned int size) { - unsigned int step = 4; + int step = 4; + int size_ = static_cast(size); #if defined(_OPENMP) #pragma omp parallel for #endif - for (unsigned int i = 0; i < size; ++i) { + for (int i = 0; i < size_; ++i) { float red, green, blue; float h, s, v; float min, max; From d0b5d2dd30304e5e471bdde74df924e47abc44e6 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Thu, 28 Mar 2024 14:20:01 +0100 Subject: [PATCH 04/32] =?UTF-8?q?Fix=20warning=20that=20occurs=20under=20w?= =?UTF-8?q?indows=20warning=20C4244:=20'argument'=20:=20conversion=20de=20?= =?UTF-8?q?'const=20=5FTy'=20en=20'const=20unsigned=20=5F=5Fint64',=20pert?= =?UTF-8?q?e=20possible=20de=20donn=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/core/include/visp3/core/vpMunkres.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/core/include/visp3/core/vpMunkres.h b/modules/core/include/visp3/core/vpMunkres.h index fa4dc73ee6..27d9c0c2a7 100644 --- a/modules/core/include/visp3/core/vpMunkres.h +++ b/modules/core/include/visp3/core/vpMunkres.h @@ -316,7 +316,7 @@ inline std::vector > vpMunkres::run(std::v { const auto original_row_size = static_cast(costs.size()); const auto original_col_size = static_cast(costs.front().size()); - const auto sq_size = std::max(original_row_size, original_col_size); + const size_t sq_size = static_cast(std::max(original_row_size, original_col_size)); auto mask = std::vector >(sq_size, std::vector(sq_size, vpMunkres::ZERO_T::NA)); auto row_cover = std::vector(sq_size, false); From 69f99e18cc390d8b96e41edd45f2c6ad54c6e93c Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Thu, 28 Mar 2024 19:48:37 +0100 Subject: [PATCH 05/32] Continue to improve RGB -> HSV conversion Add test for unsigned char conversion --- .../core/include/visp3/core/vpImageConvert.h | 2 + modules/core/src/image/vpImageConvert_hsv.cpp | 143 ++++++++++-------- .../image-with-dataset/testConversion.cpp | 27 +++- 3 files changed, 100 insertions(+), 72 deletions(-) diff --git a/modules/core/include/visp3/core/vpImageConvert.h b/modules/core/include/visp3/core/vpImageConvert.h index 6d48456f6b..b4bd9bdf6a 100644 --- a/modules/core/include/visp3/core/vpImageConvert.h +++ b/modules/core/include/visp3/core/vpImageConvert.h @@ -293,6 +293,8 @@ class VISP_EXPORT vpImageConvert unsigned int size, unsigned int step); static void RGB2HSV(const unsigned char *rgb, double *hue, double *saturation, double *value, unsigned int size, unsigned int step); + static void RGB2HSV(const unsigned char *rgb, unsigned char *hue, unsigned char *saturation, unsigned char *value, + unsigned int size, unsigned int step); private: static bool YCbCrLUTcomputed; diff --git a/modules/core/src/image/vpImageConvert_hsv.cpp b/modules/core/src/image/vpImageConvert_hsv.cpp index dfda969336..29ba9c8406 100644 --- a/modules/core/src/image/vpImageConvert_hsv.cpp +++ b/modules/core/src/image/vpImageConvert_hsv.cpp @@ -177,9 +177,6 @@ void vpImageConvert::RGB2HSV(const unsigned char *rgb, double *hue, double *satu } else { double delta = max - min; - if (vpMath::equal(delta, 0.0, std::numeric_limits::epsilon())) { - delta = 1.0; - } if (vpMath::equal(red, max, std::numeric_limits::epsilon())) { h = (green - blue) / delta; @@ -206,6 +203,81 @@ void vpImageConvert::RGB2HSV(const unsigned char *rgb, double *hue, double *satu } } +/*! + * Convert an RGB or RGBa color image depending on the value of \e step into an HSV image. + * \param[out] rgb : Pointer to the 24-bit or 32-bits color image that should be allocated with a size of + * width * height * step. + * \param[out] hue : Output H channel with values in range [0, 255]. + * \param[out] saturation : Output S channel with values in range [0, 255]. + * \param[out] value : Output V channel with values in range [0, 255]. + * \param[in] size : The image size or the number of pixels corresponding to the image width * height. + * \param[in] step : Number of channels of the input color image; 3 for an RGB image, 4 for an RGBA image. + */ +void vpImageConvert::RGB2HSV(const unsigned char *rgb, unsigned char *hue, unsigned char *saturation, unsigned char *value, + unsigned int size, unsigned int step) +{ + int size_ = static_cast(size); +#if defined(_OPENMP) +#pragma omp parallel for +#endif + for (int i = 0; i < size_; ++i) { + float red, green, blue; + float h, s, v; + float min, max; + unsigned int i_ = i * step; + + red = rgb[i_]; + green = rgb[++i_]; + blue = rgb[++i_]; + + if (red > green) { + max = std::max(red, blue); + min = std::min(green, blue); + } + else { + max = std::max(green, blue); + min = std::min(red, blue); + } + + v = max; + + if (!vpMath::equal(max, 0.f, std::numeric_limits::epsilon())) { + s = 255.f * (max - min) / max; + } + else { + s = 0.f; + } + + if (vpMath::equal(s, 0.f, std::numeric_limits::epsilon())) { + h = 0.f; + } + else { + float delta = max - min; + + if (vpMath::equal(red, max, std::numeric_limits::epsilon())) { + h = 43.f * (green - blue) / delta; + } + else if (vpMath::equal(green, max, std::numeric_limits::epsilon())) { + h = 85.f + 43.f * (blue - red) / delta; + } + else { + h = 171.f + 43.f * (red - green) / delta; + } + + if (h < 0.f) { + h += 255.f; + } + else if (h > 255.f) { + h -= 255.f; + } + } + + hue[i] = static_cast(h); + saturation[i] = static_cast(s); + value[i] = static_cast(v); + } +} + /*! Converts an array of hue, saturation and value to an array of RGBa values. @@ -275,70 +347,7 @@ void vpImageConvert::RGBaToHSV(const unsigned char *rgba, double *hue, double *s void vpImageConvert::RGBaToHSV(const unsigned char *rgba, unsigned char *hue, unsigned char *saturation, unsigned char *value, unsigned int size) { - int step = 4; - int size_ = static_cast(size); -#if defined(_OPENMP) -#pragma omp parallel for -#endif - for (int i = 0; i < size_; ++i) { - float red, green, blue; - float h, s, v; - float min, max; - unsigned int i_ = i * step; - - red = rgba[i_]; - green = rgba[++i_]; - blue = rgba[++i_]; - - if (red > green) { - max = std::max(red, blue); - min = std::min(green, blue); - } - else { - max = std::max(green, blue); - min = std::min(red, blue); - } - - v = max; - - if (!vpMath::equal(max, 0.f, std::numeric_limits::epsilon())) { - s = 255.f * (max - min) / max; - } - else { - s = 0.f; - } - - if (vpMath::equal(s, 0.f, std::numeric_limits::epsilon())) { - h = 0.f; - } - else { - float delta = max - min; - if (vpMath::equal(delta, 0.0, std::numeric_limits::epsilon())) { - delta = 255.f; - } - - if (vpMath::equal(red, max, std::numeric_limits::epsilon())) { - h = 43.f * (green - blue) / delta; - } - else if (vpMath::equal(green, max, std::numeric_limits::epsilon())) { - h = 85.f + 43.f * (blue - red) / delta; - } - else { - h = 171.f + 43.f * (red - green) / delta; - } - - if (h < 0.f) { - h += 255.f; - } - else if (h > 255.f) { - h -= 255.f; - } - } - - hue[i] = static_cast(h); - saturation[i] = static_cast(s); - value[i] = static_cast(v); - } + vpImageConvert::RGB2HSV(rgba, hue, saturation, value, size, 4); } /*! diff --git a/modules/core/test/image-with-dataset/testConversion.cpp b/modules/core/test/image-with-dataset/testConversion.cpp index 5386640292..053c6fab95 100644 --- a/modules/core/test/image-with-dataset/testConversion.cpp +++ b/modules/core/test/image-with-dataset/testConversion.cpp @@ -480,11 +480,10 @@ int main(int argc, const char **argv) // Convert a vpImage in RGB color space to a vpImage in // HSV color //////////////////////////////////// - std::cout << "** Convert a vpImage in RGB color space to a " - "vpImage in HSV color" - << std::endl; + std::cout << "** Convert a vpImage in RGB color space to a vpImage in HSV color" << std::endl; unsigned int size = Ic.getSize(); unsigned int w = Ic.getWidth(), h = Ic.getHeight(); + // Check the conversion RGBa <==> HSV(unsigned char) std::vector hue(size); std::vector saturation(size); std::vector value(size); @@ -500,7 +499,25 @@ int main(int argc, const char **argv) std::cout << " Resulting image saved in: " << filename << std::endl; vpImageIo::write(I_HSV, filename); - // Check the conversion RGBa <==> HSV + vpImage Ic_from_hsv(Ic.getHeight(), Ic.getWidth()); + vpImageConvert::HSVToRGBa(&hue.front(), &saturation.front(), &value.front(), reinterpret_cast(Ic_from_hsv.bitmap), size); + for (unsigned int i = 0; i < Ic.getHeight(); i++) { + for (unsigned int j = 0; j < Ic.getWidth(); j++) { + int precision = 10.; // Due to cast to unsigned char + if ((!vpMath::equal(static_cast(Ic[i][j].R), static_cast(Ic_from_hsv[i][j].R), precision)) + || (!vpMath::equal(static_cast(Ic[i][j].G), static_cast(Ic_from_hsv[i][j].G), precision)) + || (!vpMath::equal(static_cast(Ic[i][j].B), static_cast(Ic_from_hsv[i][j].B), precision))) { + std::cerr << "Ic[i][j].R=" << static_cast(Ic[i][j].R) + << " ; Ic_from_hsv[i][j].R=" << static_cast(Ic_from_hsv[i][j].R) << " precision: " << precision << std::endl; + std::cerr << "Ic[i][j].G=" << static_cast(Ic[i][j].G) + << " ; Ic_from_hsv[i][j].G=" << static_cast(Ic_from_hsv[i][j].G) << " precision: " << precision << std::endl; + std::cerr << "Ic[i][j].B=" << static_cast(Ic[i][j].B) + << " ; Ic_from_hsv[i][j].B=" << static_cast(Ic_from_hsv[i][j].B) << " precision: " << precision << std::endl; + throw vpException(vpException::fatalError, "Problem with conversion between RGB <==> HSV(unsigned char)"); + } + } + } + // Check the conversion RGBa <==> HSV(double) std::vector hue2(size); std::vector saturation2(size); std::vector value2(size); @@ -523,7 +540,7 @@ int main(int argc, const char **argv) << " ; I_HSV2RGBa[i][j].G=" << static_cast(I_HSV2RGBa[i][j].G) << std::endl; std::cerr << "Ic[i][j].B=" << static_cast(Ic[i][j].B) << " ; I_HSV2RGBa[i][j].B=" << static_cast(I_HSV2RGBa[i][j].B) << std::endl; - throw vpException(vpException::fatalError, "Problem with conversion between RGB <==> HSV"); + throw vpException(vpException::fatalError, "Problem with conversion between RGB <==> HSV(double)"); } } } From 2f94caee2d290b0cf669ff2b0316ae2f303d1972 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Tue, 2 Apr 2024 09:19:30 +0200 Subject: [PATCH 06/32] Improve and optimize RGB and RGBa to/from HSV conversion Introduce more tests --- .../core/include/visp3/core/vpImageConvert.h | 12 +- modules/core/src/image/vpImageConvert_hsv.cpp | 381 ++++++++++++------ .../testColorConversion.cpp | 214 ++++++++++ 3 files changed, 478 insertions(+), 129 deletions(-) diff --git a/modules/core/include/visp3/core/vpImageConvert.h b/modules/core/include/visp3/core/vpImageConvert.h index b4bd9bdf6a..e4931eb76e 100644 --- a/modules/core/include/visp3/core/vpImageConvert.h +++ b/modules/core/include/visp3/core/vpImageConvert.h @@ -233,18 +233,18 @@ class VISP_EXPORT vpImageConvert static void HSVToRGBa(const double *hue, const double *saturation, const double *value, unsigned char *rgba, unsigned int size); static void HSVToRGBa(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, - unsigned char *rgba, unsigned int size); + unsigned char *rgba, unsigned int size, bool h_full = true); static void RGBaToHSV(const unsigned char *rgba, double *hue, double *saturation, double *value, unsigned int size); static void RGBaToHSV(const unsigned char *rgba, unsigned char *hue, unsigned char *saturation, unsigned char *value, - unsigned int size); + unsigned int size, bool h_full = true); static void HSVToRGB(const double *hue, const double *saturation, const double *value, unsigned char *rgb, unsigned int size); static void HSVToRGB(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, - unsigned char *rgb, unsigned int size); + unsigned char *rgb, unsigned int size, bool h_full = true); static void RGBToHSV(const unsigned char *rgb, double *hue, double *saturation, double *value, unsigned int size); static void RGBToHSV(const unsigned char *rgb, unsigned char *hue, unsigned char *saturation, unsigned char *value, - unsigned int size); + unsigned int size, bool h_full = true); static void demosaicBGGRToRGBaBilinear(const uint8_t *bggr, uint8_t *rgba, unsigned int width, unsigned int height, unsigned int nThreads = 0); @@ -291,10 +291,12 @@ class VISP_EXPORT vpImageConvert static void HSV2RGB(const double *hue, const double *saturation, const double *value, unsigned char *rgba, unsigned int size, unsigned int step); + static void HSV2RGB(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, unsigned char *rgba, + unsigned int size, unsigned int step, bool h_full); static void RGB2HSV(const unsigned char *rgb, double *hue, double *saturation, double *value, unsigned int size, unsigned int step); static void RGB2HSV(const unsigned char *rgb, unsigned char *hue, unsigned char *saturation, unsigned char *value, - unsigned int size, unsigned int step); + unsigned int size, unsigned int step, bool h_full); private: static bool YCbCrLUTcomputed; diff --git a/modules/core/src/image/vpImageConvert_hsv.cpp b/modules/core/src/image/vpImageConvert_hsv.cpp index 29ba9c8406..8866900543 100644 --- a/modules/core/src/image/vpImageConvert_hsv.cpp +++ b/modules/core/src/image/vpImageConvert_hsv.cpp @@ -47,18 +47,23 @@ /*! * Convert an HSV image to a RGB or RGBa image depending on the value of \e step. - * \param[in] hue_ : Input image H channel. - * \param[in] saturation_ : Input image S channel. - * \param[in] value_ : Input image V channel. - * \param[out] rgb : Pointer to the 24-bit or 32-bits color image that should be allocated with a size of + * \param[in] hue_ : Image hue channel in range [0,1]. + * \param[in] saturation_ : Image saturation channel in range [0,1]. + * \param[in] value_ : Image value channel in range [0,1]. + * \param[out] rgb : Pointer to the RGB (24-bit) or RGBa (32-bits) color image that should be allocated with a size of * width * height * step. + * This array should be allocated prior to calling this function. * \param[in] size : The image size or the number of pixels corresponding to the image width * height. * \param[in] step : Number of channels of the output color image; 3 for an RGB image, 4 for an RGBA image. */ void vpImageConvert::HSV2RGB(const double *hue_, const double *saturation_, const double *value_, unsigned char *rgb, unsigned int size, unsigned int step) { - for (unsigned int i = 0; i < size; ++i) { + int size_ = static_cast(size); +#if defined(_OPENMP) +#pragma omp parallel for +#endif + for (int i = 0; i < size_; ++i) { double hue = hue_[i], saturation = saturation_[i], value = value_[i]; if (vpMath::equal(saturation, 0.0, std::numeric_limits::epsilon())) { @@ -118,24 +123,125 @@ void vpImageConvert::HSV2RGB(const double *hue_, const double *saturation_, cons } } - rgb[i * step] = static_cast(vpMath::round(hue * 255.0)); - rgb[i * step + 1] = static_cast(vpMath::round(saturation * 255.0)); - rgb[i * step + 2] = static_cast(vpMath::round(value * 255.0)); - if (step == 4) {// alpha - rgb[i * step + 3] = vpRGBa::alpha_default; + int i_step = i * step; + rgb[i_step] = static_cast(vpMath::round(hue * 255.0)); + rgb[++i_step] = static_cast(vpMath::round(saturation * 255.0)); + rgb[++i_step] = static_cast(vpMath::round(value * 255.0)); + if ((++i_step) == 3) { // alpha + rgb[i_step] = vpRGBa::alpha_default; + } + } +} + +/*! + * Convert an HSV image to a RGB or RGBa image depending on the value of \e step. + * \param[in] hue_ : Image hue channel. Range depends on `h_full` parameter. + * \param[in] saturation_ : Image saturation channel in range [0,255]. + * \param[in] value_ : Image value channel in range [0,255]. + * \param[out] rgb : Pointer to the RGB (24-bit) or RGBa (32-bits) color image that should be allocated with a size of + * width * height * step. + * This array should be allocated prior to calling this function. + * \param[in] size : The image size or the number of pixels corresponding to the image width * height. + * \param[in] step : Number of channels of the output color image; 3 for an RGB image, 4 for an RGBA image. + * \param[in] h_full : When true, hue range is in [0, 255]. When false, hue range is in [0, 180]. + */ +void vpImageConvert::HSV2RGB(const unsigned char *hue_, const unsigned char *saturation_, const unsigned char *value_, + unsigned char *rgb, unsigned int size, unsigned int step, bool h_full) +{ + float h_max; + if (h_full) { + h_max = 255.f; + } + else { + h_max = 180.f; + } + int size_ = static_cast(size); +#if defined(_OPENMP) +#pragma omp parallel for +#endif + for (int i = 0; i < size_; ++i) { + float hue = hue_[i] / h_max; + float saturation = saturation_[i] / 255.f; + float value = value_[i] / 255.f; + + if (vpMath::equal(saturation, 0.f, std::numeric_limits::epsilon())) { + hue = value; + saturation = value; + } + else { + float h = hue * 6.f; + float s = saturation; + float v = value; + + if (vpMath::equal(h, 6.f, std::numeric_limits::epsilon())) { + h = 0.0f; + } + float f = h - static_cast(h); + float p = v * (1.0f - s); + float q = v * (1.0f - (s * f)); + float t = v * (1.0f - (s * (1.0f - f))); + + switch (static_cast(h)) { + case 0: + hue = v; + saturation = t; + value = p; + break; + + case 1: + hue = q; + saturation = v; + value = p; + break; + + case 2: + hue = p; + saturation = v; + value = t; + break; + + case 3: + hue = p; + saturation = q; + value = v; + break; + + case 4: + hue = t; + saturation = p; + value = v; + break; + + default: // case 5: + hue = v; + saturation = p; + value = q; + break; + } + } + + int i_step = i * step; + rgb[i_step] = static_cast(hue * 255.f); + rgb[++i_step] = static_cast(saturation * 255.0f); + rgb[++i_step] = static_cast(value * 255.0f); + if ((++i_step) == 3) { // alpha + rgb[i_step] = vpRGBa::alpha_default; } } } /*! * Convert an RGB or RGBa color image depending on the value of \e step into an HSV image. - * \param[out] rgb : Pointer to the 24-bit or 32-bits color image that should be allocated with a size of + * \param[in] rgb : Pointer to the RGB (24-bits) or RGBa (32-bits) color image that should be allocated with a size of * width * height * step. - * \param[out] hue : Output H channel with values in range [0, 1]. - * \param[out] saturation : Output S channel with values in range [0, 1]. - * \param[out] value : Output V channel with values in range [0, 1]. + * \param[out] hue : Converted hue channel with values in range [0, 1]. + * This array of dimension `size` should be allocated prior to calling this function. + * \param[out] saturation : Converted saturation channel with values in range [0, 1]. + * This array of dimension `size` should be allocated prior to calling this function. + * \param[out] value : Converted value channel with values in range [0, 1]. + * This array of dimension `size` should be allocated prior to calling this function. * \param[in] size : The image size or the number of pixels corresponding to the image width * height. - * \param[in] step : Number of channels of the input color image; 3 for an RGB image, 4 for an RGBA image. + * \param[in] step : Number of channels of the input color image; 3 for an RGB image, 4 for an RGBa image. */ void vpImageConvert::RGB2HSV(const unsigned char *rgb, double *hue, double *saturation, double *value, unsigned int size, unsigned int step) @@ -205,18 +311,35 @@ void vpImageConvert::RGB2HSV(const unsigned char *rgb, double *hue, double *satu /*! * Convert an RGB or RGBa color image depending on the value of \e step into an HSV image. - * \param[out] rgb : Pointer to the 24-bit or 32-bits color image that should be allocated with a size of + * \param[in] rgb : Pointer to the RGB (24-bit) or RGBa (32-bits) color image that should be allocated with a size of * width * height * step. - * \param[out] hue : Output H channel with values in range [0, 255]. - * \param[out] saturation : Output S channel with values in range [0, 255]. - * \param[out] value : Output V channel with values in range [0, 255]. + * \param[out] hue : Converted hue channel. Range depends on `h_full` parameter. + * This array of dimension `size` should be allocated prior to calling this function. + * \param[out] saturation : Converted saturation channel with values in range [0, 255]. + * This array of dimension `size` should be allocated prior to calling this function. + * \param[out] value : Converted value channel with values in range [0, 255]. + * This array of dimension `size` should be allocated prior to calling this function. * \param[in] size : The image size or the number of pixels corresponding to the image width * height. * \param[in] step : Number of channels of the input color image; 3 for an RGB image, 4 for an RGBA image. + * \param[in] h_full : When true, hue range is in [0, 255]. When false, hue range is in [0, 180]. */ void vpImageConvert::RGB2HSV(const unsigned char *rgb, unsigned char *hue, unsigned char *saturation, unsigned char *value, - unsigned int size, unsigned int step) + unsigned int size, unsigned int step, bool h_full) { int size_ = static_cast(size); + std::vector h_scale(4); + if (h_full) { + h_scale[0] = 42.5f; + h_scale[1] = 85.f; + h_scale[2] = 170.f; + h_scale[3] = 255.f; + } + else { + h_scale[0] = 30.f; + h_scale[1] = 60.f; + h_scale[2] = 120.f; + h_scale[3] = 180.f; + } #if defined(_OPENMP) #pragma omp parallel for #endif @@ -255,20 +378,17 @@ void vpImageConvert::RGB2HSV(const unsigned char *rgb, unsigned char *hue, unsig float delta = max - min; if (vpMath::equal(red, max, std::numeric_limits::epsilon())) { - h = 43.f * (green - blue) / delta; + h = h_scale[0] * (green - blue) / delta; } else if (vpMath::equal(green, max, std::numeric_limits::epsilon())) { - h = 85.f + 43.f * (blue - red) / delta; + h = h_scale[1] + h_scale[0] * (blue - red) / delta; } else { - h = 171.f + 43.f * (red - green) / delta; + h = h_scale[2] + h_scale[0] * (red - green) / delta; } if (h < 0.f) { - h += 255.f; - } - else if (h > 255.f) { - h -= 255.f; + h += h_scale[3]; } } @@ -279,17 +399,18 @@ void vpImageConvert::RGB2HSV(const unsigned char *rgb, unsigned char *hue, unsig } /*! - Converts an array of hue, saturation and value to an array of RGBa values. - - Alpha component of the converted image is set to vpRGBa::alpha_default. - - \param[in] hue : Array of hue values (range between [0 - 1]). - \param[in] saturation : Array of saturation values (range between [0 - 1]). - \param[in] value : Array of value values (range between [0 - 1]). - \param[out] rgba : Pointer to the 32-bit RGBA image that should - be allocated with a size of width * height * 4. Alpha channel is here set to vpRGBa::alpha_default. - \param[in] size : The image size or the number of pixels corresponding to the image width * height. -*/ + * Converts an array of hue, saturation and value (HSV) to an array of RGBa values. + * + * Alpha component of the converted image is set to vpRGBa::alpha_default. + * + * \param[in] hue : Array of hue values in range [0,1]. + * \param[in] saturation : Array of saturation values in range [0,1]. + * \param[in] value : Array of value values in range [0,1]. + * \param[out] rgba : Pointer to the 32-bit RGBa image that should + * be allocated prior to calling this function with a size of width * height * 4. + * Alpha channel is here set to vpRGBa::alpha_default. + * \param[in] size : The image size or the number of pixels corresponding to the image width * height. + */ void vpImageConvert::HSVToRGBa(const double *hue, const double *saturation, const double *value, unsigned char *rgba, unsigned int size) { @@ -297,37 +418,38 @@ void vpImageConvert::HSVToRGBa(const double *hue, const double *saturation, cons } /*! - Converts an array of hue, saturation and value to an array of RGBa values. - - Alpha component of the converted image is set to vpRGBa::alpha_default. - - \param[in] hue : Array of hue values (range between [0 - 255]). - \param[in] saturation : Array of saturation values (range between [0 - 255]). - \param[in] value : Array of value values (range between [0 - 255]). - \param[out] rgba : Pointer to the 32-bit RGBA image that should - be allocated with a size of width * height * 4. Alpha channel is here set to vpRGBa::alpha_default. - \param[in] size : The image size or the number of pixels corresponding to the image width * height. -*/ + * Converts an array of hue, saturation and value (HSV) to an array of RGBa values. + * + * Alpha component of the converted image is set to vpRGBa::alpha_default. + * + * \param[in] hue : Array of hue values. Range depends on `h_full` parameter. + * \param[in] saturation : Array of saturation values in range [0,255]. + * \param[in] value : Array of value values in range [0,255]. + * \param[out] rgba : Pointer to the 32-bit RGBa image that should + * be allocated prior to calling this function with a size of width * height * 4. Alpha channel is here set + * to vpRGBa::alpha_default. + * \param[in] size : The image size or the number of pixels corresponding to the image width * height. + * \param[in] h_full : When true, hue range is in [0, 255]. When false, hue range is in [0, 180]. + */ void vpImageConvert::HSVToRGBa(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, - unsigned char *rgba, unsigned int size) + unsigned char *rgba, unsigned int size, bool h_full) { - for (unsigned int i = 0; i < size; ++i) { - double h = hue[i] / 255.0, s = saturation[i] / 255.0, v = value[i] / 255.0; - - vpImageConvert::HSVToRGBa(&h, &s, &v, (rgba + (i * 4)), 1); - } + vpImageConvert::HSV2RGB(hue, saturation, value, rgba, size, 4, h_full); } /*! - Converts an array of RGBa to an array of hue, saturation, value values. - The alpha channel is not used. - - \param[in] rgba : Pointer to the 32-bits RGBA bitmap. - \param[out] hue : Array of hue values converted from RGB color space in range [0 - 1]. - \param[out] saturation : Array of saturation values converted from RGB color space in range [0 - 1]. - \param[out] value : Array of value values converted from RGB color space in range [0 - 1]. - \param[in] size : The image size or the number of pixels corresponding to the image width * height. -*/ + * Converts an array of RGBa to an array of hue, saturation, value (HSV) values. + * The alpha channel is not used. + * + * \param[in] rgba : Pointer to the 32-bits RGBa bitmap. + * \param[out] hue : Array of hue values converted from RGB color space in range [0 - 1]. + * This array of dimension `size` should be allocated prior to calling this function. + * \param[out] saturation : Array of saturation values converted from RGB color space in range [0 - 1]. + * This array of dimension `size` should be allocated prior to calling this function. + * \param[out] value : Array of value values converted from RGB color space in range [0 - 1]. + * This array of dimension `size` should be allocated prior to calling this function. + * \param[in] size : The image size or the number of pixels corresponding to the image width * height. + */ void vpImageConvert::RGBaToHSV(const unsigned char *rgba, double *hue, double *saturation, double *value, unsigned int size) { @@ -335,31 +457,40 @@ void vpImageConvert::RGBaToHSV(const unsigned char *rgba, double *hue, double *s } /*! - Converts an array of RGBa to an array of hue, saturation, value values. - The alpha channel is not used. - - \param[in] rgba : Pointer to the 32-bits RGBA bitmap. - \param[out] hue : Array of hue values converted from RGB color space in range [0, 255]. - \param[out] saturation : Array of saturation values converted from RGB color space in range [0 - 255]. - \param[out] value : Array of value values converted from RGB color space in range [0 - 255]. - \param[in] size : The image size or the number of pixels corresponding to the image width * height. -*/ + * Converts an array of RGBa to an array of hue, saturation, value (HSV) values. + * The alpha channel is not used. + * + * \param[in] rgba : Pointer to the 32-bits RGBA bitmap that has a dimension of `size * 4`. + * \param[out] hue : Array of hue values converted from RGB color space. Range depends on `h_full` parameter. + * This array of dimension `size` should be allocated prior to calling this function. + * \param[out] saturation : Array of saturation values converted from RGB color space in range [0 - 255]. + * This array of dimension `size` should be allocated prior to calling this function. + * \param[out] value : Array of value values converted from RGB color space in range [0 - 255]. + * This array of dimension `size` should be allocated prior to calling this function. + * \param[in] size : The image size or the number of pixels corresponding to the image width * height. + * \param[in] h_full : When true, hue range is in [0, 255]. When false, hue range is in [0, 180]. + * + * \sa vpImageTools::inRange() + */ void vpImageConvert::RGBaToHSV(const unsigned char *rgba, unsigned char *hue, unsigned char *saturation, - unsigned char *value, unsigned int size) + unsigned char *value, unsigned int size, bool h_full) { - vpImageConvert::RGB2HSV(rgba, hue, saturation, value, size, 4); + vpImageConvert::RGB2HSV(rgba, hue, saturation, value, size, 4, h_full); } /*! - Converts an array of hue, saturation and value to an array of RGB values. - - \param[in] hue : Array of hue values (range between [0 - 1]). - \param[in] saturation : Array of saturation values (range between [0 - 1]). - \param[in] value : Array of value values (range between [0 - 1]). - \param[out] rgb : Pointer to the 24-bit RGB image that should be allocated with a size of - width * height * 3. - \param[in] size : The image size or the number of pixels corresponding to the image width * height. -*/ + * Converts an array of hue, saturation and value to an array of RGB values. + * + * \param[in] hue : Array of hue values in range [0,1]. + * The dimension of this array corresponds to `size` parameter. + * \param[in] saturation : Array of saturation values in range [0,1]. + * The dimension of this array corresponds to `size` parameter. + * \param[in] value : Array of value values in range [0,1]. + * The dimension of this array corresponds to `size` parameter. + * \param[out] rgb : Pointer to the 24-bit RGB image that should be allocated prior to calling this function + * with a size of `width * height * 3` where `width * height` corresponds to `size` parameter. + * \param[in] size : The image size or the number of pixels corresponding to the image `width * height`. + */ void vpImageConvert::HSVToRGB(const double *hue, const double *saturation, const double *value, unsigned char *rgb, unsigned int size) { @@ -367,33 +498,37 @@ void vpImageConvert::HSVToRGB(const double *hue, const double *saturation, const } /*! - Converts an array of hue, saturation and value to an array of RGB values. - - \param[in] hue : Array of hue values (range between [0 - 255]). - \param[in] saturation : Array of saturation values (range between [0 - 255]). - \param[in] value : Array of value values (range between [0 - 255]). - \param[out] rgb : Pointer to the 24-bit RGB image that should be allocated with a size of width * height * 3. - \param[in] size : The image size or the number of pixels corresponding to the image width * height. -*/ + * Converts an array of hue, saturation and value to an array of RGB values. + * + * \param[in] hue : Array of hue values. Range depends on `h_full` parameter. + * The dimension of this array corresponds to `size` parameter. + * \param[in] saturation : Array of saturation values in range [0,255]. + * The dimension of this array corresponds to `size` parameter. + * \param[in] value : Array of value values in range [0,255]. + * The dimension of this array corresponds to `size` parameter. + * \param[out] rgb : Pointer to the 24-bit RGB image that should be allocated prior to calling this function + * with a size of `width * height * 3` where `width * height` corresponds to `size` parameter. + * \param[in] size : The image size or the number of pixels corresponding to the image `width * height`. + * \param[in] h_full : When true, hue range is in [0, 255]. When false, hue range is in [0, 180]. + */ void vpImageConvert::HSVToRGB(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, - unsigned char *rgb, unsigned int size) + unsigned char *rgb, unsigned int size, bool h_full) { - for (unsigned int i = 0; i < size; ++i) { - double h = hue[i] / 255.0, s = saturation[i] / 255.0, v = value[i] / 255.0; - - vpImageConvert::HSVToRGB(&h, &s, &v, (rgb + (i * 3)), 1); - } + vpImageConvert::HSV2RGB(hue, saturation, value, rgb, size, 3, h_full); } /*! - Converts an array of RGB to an array of hue, saturation, value values. - - \param[in] rgb : Pointer to the 24-bits RGB bitmap. - \param[out] hue : Array of hue values converted from RGB color space(range between[0 - 255]). - \param[out] saturation : Array of saturation values converted from RGB color space(range between[0 - 255]). - \param[out] value : Array of value values converted from RGB color space(range between[0 - 255]). - \param[in] size : The image size or the number of pixels corresponding to the image width * height. -*/ + * Converts an array of RGB to an array of hue, saturation, value values. + * + * \param[in] rgb : Pointer to the 24-bits RGB bitmap. Its size corresponds to `size * 3`. + * \param[out] hue : Array of hue values in range [0,1] converted from RGB color space. + * This array should be allocated prior to calling this function. Its size corresponds to `size` parameter. + * \param[out] saturation : Array of saturation values in range [0,1] converted from RGB color space. + * This array should be allocated prior to calling this function. Its size corresponds to `size` parameter. + * \param[out] value : Array of value values in range [0,1] converted from RGB color space. + * This array should be allocated prior to calling this function. Its size corresponds to `size` parameter. + * \param[in] size : The image size or the number of pixels corresponding to the image width * height. + */ void vpImageConvert::RGBToHSV(const unsigned char *rgb, double *hue, double *saturation, double *value, unsigned int size) { @@ -401,24 +536,22 @@ void vpImageConvert::RGBToHSV(const unsigned char *rgb, double *hue, double *sat } /*! - Converts an array of RGB to an array of hue, saturation, value values. - - \param[in] rgb : Pointer to the 24-bits RGB bitmap. - \param[out] hue : Array of hue values converted from RGB color space (range between [0 - 255]). - \param[out] saturation : Array of saturation values converted from RGB color space (range between [0 - 255]). - \param[out] value : Array of value values converted from RGB color space (range between [0 - 255]). - \param[in] size : The image size or the number of pixels corresponding to the image width * height. -*/ + * Converts an array of RGB to an array of hue, saturation, value values. + * + * \param[in] rgb : Pointer to the 24-bits RGB bitmap. Its size corresponds to `size * 3`. + * \param[out] hue : Array of hue values converted from RGB color space. Range depends on `h_full` parameter. + * This array should be allocated prior to calling this function. Its size corresponds to `size` parameter. + * \param[out] saturation : Array of saturation values in range [0,255] converted from RGB color space. + * This array should be allocated prior to calling this function. Its size corresponds to `size` parameter. + * \param[out] value : Array of value values in range [0,255] converted from RGB color space. + * This array should be allocated prior to calling this function. Its size corresponds to `size` parameter. + * \param[in] size : The image size or the number of pixels corresponding to the image width * height. + * \param[in] h_full : When true, hue range is in [0, 255]. When false, hue range is in [0, 180]. + * + * \sa vpImageTools::inRange() + */ void vpImageConvert::RGBToHSV(const unsigned char *rgb, unsigned char *hue, unsigned char *saturation, - unsigned char *value, unsigned int size) + unsigned char *value, unsigned int size, bool h_full) { - for (unsigned int i = 0; i < size; ++i) { - double h, s, v; - - vpImageConvert::RGBToHSV((rgb + (i * 3)), &h, &s, &v, 1); - - hue[i] = static_cast(255.0 * h); - saturation[i] = static_cast(255.0 * s); - value[i] = static_cast(255.0 * v); - } + vpImageConvert::RGB2HSV(rgb, hue, saturation, value, size, 3, h_full); } diff --git a/modules/core/test/image-with-dataset/testColorConversion.cpp b/modules/core/test/image-with-dataset/testColorConversion.cpp index b9b1a43d67..3ee2ded944 100644 --- a/modules/core/test/image-with-dataset/testColorConversion.cpp +++ b/modules/core/test/image-with-dataset/testColorConversion.cpp @@ -1236,6 +1236,220 @@ TEST_CASE("Bayer conversion", "[image_conversion]") } #endif +template +bool test_hsv(const std::vector &hue, const std::vector &saturation, + const std::vector &value, const std::vector< std::vector > &rgb_truth, + const std::vector< std::vector > &hsv_truth, size_t step, size_t size, double max_range) +{ + // Compare HSV values + for (size_t i = 0; i < size; ++i) { + if (((hue[i]*max_range) != static_cast(hsv_truth[i][0])) || + ((saturation[i]*max_range) != static_cast(hsv_truth[i][1])) || + ((value[i]*max_range) != static_cast(hsv_truth[i][2]))) { + if (step == 3) { + std::cout << "Error in rgb to hsv conversion for rgb ("; + } + else { + std::cout << "Error in rgba to hsv conversion for rgba ("; + } + std::cout << static_cast(rgb_truth[i][0]) << "," + << static_cast(rgb_truth[i][1]) << "," + << static_cast(rgb_truth[i][2]) << "): Expected hsv value: (" + << static_cast(hsv_truth[i][0]) << "," + << static_cast(hsv_truth[i][1]) << "," + << static_cast(hsv_truth[i][2]) << ") converted value: (" + << static_cast(hue[i]) << "," + << static_cast(saturation[i]) << "," + << static_cast(value[i]) << ")" << std::endl; + return false; + } + } + return true; +} + +bool test_rgb(const std::vector &rgb, const std::vector< std::vector > rgb_truth, + const std::vector< std::vector > &hsv_truth, size_t step, size_t size, double epsilon = 0.) +{ + // Compare RGB values + if (epsilon > 0.) { + for (size_t i = 0; i < size; ++i) { + if ((!vpMath::equal(rgb[i*step], rgb_truth[i][0], epsilon)) || + (!vpMath::equal(rgb[i*step+1], rgb_truth[i][1], epsilon)) || + (!vpMath::equal(rgb[i*step+2], rgb_truth[i][2], epsilon))) { + std::cout << "Error in hsv to rgb conversion for hsv (" + << static_cast(hsv_truth[i][0]) << "," + << static_cast(hsv_truth[i][1]) << "," + << static_cast(hsv_truth[i][2]) << "): Expected rgb value: (" + << static_cast(rgb_truth[i][0]) << "," + << static_cast(rgb_truth[i][1]) << "," + << static_cast(rgb_truth[i][2]) << ") converted value: (" + << static_cast(rgb[i*step]) << "," + << static_cast(rgb[(i*step)+1]) << "," + << static_cast(rgb[(i*step)+2]) << ") epsilon: " << epsilon << std::endl; + return false; + } + } + } + else { + for (size_t i = 0; i < size; ++i) { + if ((rgb[i*step] != rgb_truth[i][0]) || (rgb[i*step+1] != rgb_truth[i][1]) || (rgb[i*step+2] != rgb_truth[i][2])) { + std::cout << "Error in hsv to rgb conversion for hsv (" + << static_cast(hsv_truth[i][0]) << "," + << static_cast(hsv_truth[i][1]) << "," + << static_cast(hsv_truth[i][2]) << "): Expected rgb value: (" + << static_cast(rgb_truth[i][0]) << "," + << static_cast(rgb_truth[i][1]) << "," + << static_cast(rgb_truth[i][2]) << ") converted value: (" + << static_cast(rgb[i*step]) << "," + << static_cast(rgb[(i*step)+1]) << "," + << static_cast(rgb[(i*step)+2]) << ")" << std::endl; + return false; + } + } + } + + return true; +} + +TEST_CASE("RGB to HSV conversion", "[image_conversion]") +{ + std::vector< std::vector > rgb_truth; + rgb_truth.push_back({ 0, 0, 0 }); + rgb_truth.push_back({ 255, 255, 255 }); + rgb_truth.push_back({ 255, 0, 0 }); + rgb_truth.push_back({ 0, 255, 0 }); + rgb_truth.push_back({ 0, 0, 255 }); + rgb_truth.push_back({ 255, 255, 0 }); + rgb_truth.push_back({ 0, 255, 255 }); + rgb_truth.push_back({ 255, 0, 255 }); + rgb_truth.push_back({ 128, 128, 128 }); + rgb_truth.push_back({ 128, 128, 0 }); + rgb_truth.push_back({ 128, 0, 0 }); + rgb_truth.push_back({ 0, 128, 0 }); + rgb_truth.push_back({ 0, 128, 128 }); + rgb_truth.push_back({ 0, 0, 128 }); + rgb_truth.push_back({ 128, 0, 128 }); + + double h_max; + bool h_full; + + for (size_t test = 0; test < 2; ++test) { + if (test == 0) { + h_max = 255; + h_full = true; + } + else { + h_max = 180; + h_full = false; + } + std::vector< std::vector > hsv_truth; + // See https://www.rapidtables.com/convert/color/hsv-to-rgb.html + hsv_truth.push_back({ 0, 0, 0 }); + hsv_truth.push_back({ 0, 0, 255 }); + hsv_truth.push_back({ 0, 255, 255 }); + hsv_truth.push_back({ h_max * 120 / 360, 255, 255 }); + hsv_truth.push_back({ h_max * 240 / 360, 255, 255 }); + hsv_truth.push_back({ h_max * 60 / 360, 255, 255 }); + hsv_truth.push_back({ h_max * 180 / 360, 255, 255 }); + hsv_truth.push_back({ h_max * 300 / 360, 255, 255 }); + hsv_truth.push_back({ 0, 0, 128 }); + hsv_truth.push_back({ h_max * 60 / 360, 255, 128 }); + hsv_truth.push_back({ 0, 255, 128 }); + hsv_truth.push_back({ h_max * 120 / 360, 255, 128 }); + hsv_truth.push_back({ h_max * 180 / 360, 255, 128 }); + hsv_truth.push_back({ h_max * 240 / 360, 255, 128 }); + hsv_truth.push_back({ h_max * 300 / 360, 255, 128 }); + + size_t size = rgb_truth.size(); + + std::vector rgb_truth_continuous; + for (size_t i = 0; i < size; ++i) { + for (size_t j = 0; j < rgb_truth[i].size(); ++j) { + rgb_truth_continuous.push_back(rgb_truth[i][j]); + } + } + std::vector rgba_truth_continuous; + for (size_t i = 0; i < size; ++i) { + for (size_t j = 0; j < rgb_truth[i].size(); ++j) { + rgba_truth_continuous.push_back(rgb_truth[i][j]); + } + rgba_truth_continuous.push_back(vpRGBa::alpha_default); + } + SECTION("RGB -> HSV (unsigned char) -> RGB") + { + std::vector hue(size); + std::vector saturation(size); + std::vector value(size); + std::cout << "Test rgb -> hsv (unsigned char) conversion with h full scale: " << (h_full ? "yes" : "no") << std::endl; + vpImageConvert::RGBToHSV(reinterpret_cast(&rgb_truth_continuous.front()), + reinterpret_cast(&hue.front()), + reinterpret_cast(&saturation.front()), + reinterpret_cast(&value.front()), size, h_full); + CHECK(test_hsv(hue, saturation, value, rgb_truth, hsv_truth, 3, size, 1.)); + + std::cout << "Test hsv (unsigned char) -> rgb conversion with h full scale: " << (h_full ? "yes" : "no") << std::endl; + std::vector< unsigned char> rgb_continuous(rgb_truth_continuous.size() * 3); + vpImageConvert::HSVToRGB(&hue.front(), &saturation.front(), &value.front(), &rgb_continuous.front(), size, h_full); + CHECK(test_rgb(rgb_continuous, rgb_truth, hsv_truth, 3, size, 5.)); + } + SECTION("RGBa -> HSV (unsigned char) -> RGBa") + { + std::vector hue(size); + std::vector saturation(size); + std::vector value(size); + std::cout << "Test rgba -> hsv (unsigned char) conversion with h full scale: " << (h_full ? "yes" : "no") << std::endl; + vpImageConvert::RGBaToHSV(reinterpret_cast(&rgba_truth_continuous.front()), + reinterpret_cast(&hue.front()), + reinterpret_cast(&saturation.front()), + reinterpret_cast(&value.front()), size, h_full); + CHECK(test_hsv(hue, saturation, value, rgb_truth, hsv_truth, 4, size, 1.)); + + std::cout << "Test hsv (unsigned char) -> rgba conversion with h full scale: " << (h_full ? "yes" : "no") << std::endl; + std::vector< unsigned char> rgba_continuous(rgb_truth_continuous.size() * 4); + vpImageConvert::HSVToRGBa(&hue.front(), &saturation.front(), &value.front(), &rgba_continuous.front(), size, h_full); + CHECK(test_rgb(rgba_continuous, rgb_truth, hsv_truth, 4, size, 5.)); + } + if (h_full) { + SECTION("RGB -> HSV (double) -> RGB") + { + std::vector hue(size); + std::vector saturation(size); + std::vector value(size); + std::cout << "Test rgb -> hsv (double) conversion" << std::endl; + vpImageConvert::RGBToHSV(reinterpret_cast(&rgb_truth_continuous.front()), + reinterpret_cast(&hue.front()), + reinterpret_cast(&saturation.front()), + reinterpret_cast(&value.front()), size); + CHECK(test_hsv(hue, saturation, value, rgb_truth, hsv_truth, 3, size, 255.)); + + std::cout << "Test hsv (double) -> rgb conversion" << std::endl; + std::vector< unsigned char> rgb_continuous(rgb_truth_continuous.size()); + vpImageConvert::HSVToRGB(&hue.front(), &saturation.front(), &value.front(), &rgb_continuous.front(), size); + CHECK(test_rgb(rgb_continuous, rgb_truth, hsv_truth, 3, size)); + } + } + + if (h_full) { + SECTION("RGBa -> HSV (double) -> RGBa") + { + std::vector hue(size); + std::vector saturation(size); + std::vector value(size); + std::cout << "Test rgba -> hsv (double) conversion" << std::endl; + vpImageConvert::RGBaToHSV(reinterpret_cast(&rgba_truth_continuous.front()), + reinterpret_cast(&hue.front()), + reinterpret_cast(&saturation.front()), + reinterpret_cast(&value.front()), size); + CHECK(test_hsv(hue, saturation, value, rgb_truth, hsv_truth, 4, size, 255.)); + + std::cout << "Test hsv (double) -> rgba conversion" << std::endl; + std::vector< unsigned char> rgba_continuous(rgb_truth_continuous.size()*4); + vpImageConvert::HSVToRGBa(&hue.front(), &saturation.front(), &value.front(), &rgba_continuous.front(), size); + CHECK(test_rgb(rgba_continuous, rgb_truth, hsv_truth, 4, size)); + } + } + } +} int main(int argc, char *argv[]) { From 62f1e32a674f629aa1137d9b28ceae66d15ba06e Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Tue, 2 Apr 2024 09:20:30 +0200 Subject: [PATCH 07/32] Introduce vpImageTools::inRange() for HSV segmentation --- .../core/include/visp3/core/vpImageTools.h | 2 + modules/core/src/image/vpImageTools.cpp | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/modules/core/include/visp3/core/vpImageTools.h b/modules/core/include/visp3/core/vpImageTools.h index d5afac7536..fc15db6157 100644 --- a/modules/core/include/visp3/core/vpImageTools.h +++ b/modules/core/include/visp3/core/vpImageTools.h @@ -126,6 +126,8 @@ class VISP_EXPORT vpImageTools static void imageSubtract(const vpImage &I1, const vpImage &I2, vpImage &Ires, bool saturate = false); + static void inRange(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, + const vpColVector &hsv_values, unsigned char *mask, unsigned int size); static void initUndistortMap(const vpCameraParameters &cam, unsigned int width, unsigned int height, vpArray2D &mapU, vpArray2D &mapV, vpArray2D &mapDu, vpArray2D &mapDv); diff --git a/modules/core/src/image/vpImageTools.cpp b/modules/core/src/image/vpImageTools.cpp index 8173926f09..9f0365e3fb 100644 --- a/modules/core/src/image/vpImageTools.cpp +++ b/modules/core/src/image/vpImageTools.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #if defined(VISP_HAVE_SIMDLIB) #include @@ -978,3 +979,51 @@ bool vpImageTools::checkFixedPoint(unsigned int x, unsigned int y, const vpMatri const double limit = 1 << 15; return (vpMath::abs(x2) < limit) && (vpMath::abs(y2) < limit); } + +/*! + * Create binary mask by checking if HSV (hue, saturation, value) channels lie between low and high HSV thresholds. + * \param[in] hue : Pointer to an array of hue values. Its dimension is equal to the `size` parameter. + * \param[in] saturation : Pointer to an array of saturation values. Its dimension is equal to the `size` parameter. + * \param[in] value : Pointer to an array of values. Its dimension is equal to the `size` parameter. + * \param[in] hsv_values : 6-dim vector that contains the low/high threshold values for each HSV channel respectively. + * Each element of this vector should be in [0,255] range. Note that there is also tutorial-hsv-tuner.cpp that may help + * to determine low/high HSV values. + * \param[out] mask : Pointer to a resulting mask of dimension `size`. When HSV value is in the boundaries, the mask + * element is set to 255, otherwise to 0. The mask should be allocated prior calling this function. Its dimension + * is equal to the `size` parameter. + * \param[in] size : Size of `hue`, `saturation`, `value` and `mask` arrays. + * + * \sa vpImageConvert::RGBToHSV(const unsigned char *, unsigned char *, unsigned char *, unsigned char *, unsigned int, bool) + * \sa vpImageConvert::RGBaToHSV(const unsigned char *, unsigned char *, unsigned char *, unsigned char *, unsigned int, bool) + */ +void vpImageTools::inRange(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, + const vpColVector &hsv_values, unsigned char *mask, unsigned int size) +{ + if ((hue == nullptr) || (saturation == nullptr) || (value == nullptr)) { + throw(vpImageException(vpImageException::notInitializedError, + "Error in vpImageTools::inRange(): hsv pointer are empty")); + } + else if (hsv_values.size() != 6) { + throw(vpImageException(vpImageException::notInitializedError, + "Error in vpImageTools::inRange(): wrong values vector size (%d)", hsv_values.size())); + } + unsigned char h_low = static_cast(hsv_values[0]); + unsigned char h_high = static_cast(hsv_values[1]); + unsigned char s_low = static_cast(hsv_values[2]); + unsigned char s_high = static_cast(hsv_values[3]); + unsigned char v_low = static_cast(hsv_values[4]); + unsigned char v_high = static_cast(hsv_values[5]); +#if defined(_OPENMP) +#pragma omp parallel for +#endif + for (unsigned int i = 0; i < size; ++i) { + if ((h_low <= hue[i]) && (hue[i] <= h_high) && + (s_low <= saturation[i]) && (saturation[i] <= s_high) && + (v_low <= value[i]) && (value[i] <= v_high)) { + mask[i] = 255; + } + else { + mask[i] = 0; + } + } +} From 36ad32aabc0d68c49eddc08d13375d142ca6ed6c Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Tue, 2 Apr 2024 16:32:50 +0200 Subject: [PATCH 08/32] Introduce helper to convert depth image to point cloud --- .../core/include/visp3/core/vpImageConvert.h | 17 ++- modules/core/src/image/vpImageConvert_pcl.cpp | 128 ++++++++++++++++++ 2 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 modules/core/src/image/vpImageConvert_pcl.cpp diff --git a/modules/core/include/visp3/core/vpImageConvert.h b/modules/core/include/visp3/core/vpImageConvert.h index e4931eb76e..00067e793d 100644 --- a/modules/core/include/visp3/core/vpImageConvert.h +++ b/modules/core/include/visp3/core/vpImageConvert.h @@ -64,6 +64,15 @@ #include #endif +#if defined(VISP_HAVE_PCL) && defined(VISP_HAVE_THREADS) +#include +#include +#include + +#include +#include +#endif + /*! \class vpImageConvert @@ -134,8 +143,14 @@ class VISP_EXPORT vpImageConvert static void convert(const yarp::sig::ImageOf *src, vpImage &dest); #endif +#if defined(VISP_HAVE_PCL) && defined(VISP_HAVE_THREADS) + static void depthToPointCloud(const vpImage &depth_raw, float depth_scale, const vpCameraParameters &cam_depth, + pcl::PointCloud::Ptr pointcloud, + const vpImage *mask = nullptr, float Z_min = 0.2, float Z_max = 2.5); +#endif + static void split(const vpImage &src, vpImage *pR, vpImage *pG, - vpImage *pB, vpImage *pa = nullptr); + vpImage *pB, vpImage *pa = nullptr); static void merge(const vpImage *R, const vpImage *G, const vpImage *B, const vpImage *a, vpImage &RGBa); diff --git a/modules/core/src/image/vpImageConvert_pcl.cpp b/modules/core/src/image/vpImageConvert_pcl.cpp new file mode 100644 index 0000000000..f109d5830b --- /dev/null +++ b/modules/core/src/image/vpImageConvert_pcl.cpp @@ -0,0 +1,128 @@ +/**************************************************************************** + * + * 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: + * Convert image types. + * +*****************************************************************************/ + +/*! + \file vpImageConvert_pcl.cpp + \brief Depth image to point cloud conversion. +*/ + +#include + +#if defined(VISP_HAVE_PCL) && defined(VISP_HAVE_THREADS) + +#include + +#if defined(_OPENMP) +#include +#endif + +/*! + * \param[in] depth_raw : Depth raw image. + * \param[in] depth_scale : Depth scale to apply to data in `depth_raw`. + * \param[in] cam_depth : Depth camera intrinsics. + * \param[out] pointcloud : Computed point cloud. + * \param[in] mask : Optional mask. When set to nullptr, all the pixels in `depth_raw` are considered. Otherwise, + * we consider only pixels that have a mask value that differ from 0. You should ensure that mask size and `depth_raw` + * size are the same. + * \param[in] Z_min : Min Z value to retain the 3D point in the point cloud. + * \param[in] Z_max : Max Z value to retain the 3D point in the point cloud. + */ +void vpImageConvert::depthToPointCloud(const vpImage &depth_raw, float depth_scale, + const vpCameraParameters &cam_depth, + pcl::PointCloud::Ptr pointcloud, + const vpImage *mask, float Z_min, float Z_max) +{ + pointcloud->clear(); + int size = static_cast(depth_raw.getSize()); + unsigned int width = depth_raw.getWidth(); + unsigned int height = depth_raw.getHeight(); + + if (mask) { + if ((width != mask->getWidth()) || (height != mask->getHeight())) { + throw(vpImageException(vpImageException::notInitializedError, "Depth image and mask size differ")); + } +#if defined(_OPENMP) + std::mutex mutex; +#pragma omp parallel for +#endif + for (int p = 0; p < size; ++p) { + if (mask->bitmap[p]) { + if (static_cast(depth_raw.bitmap[p])) { + float Z = static_cast(depth_raw.bitmap[p]) * depth_scale; + if (Z < Z_max) { + double x = 0; + double y = 0; + unsigned int j = p % width; + unsigned int i = (p - j) / width; + vpPixelMeterConversion::convertPoint(cam_depth, j, i, x, y); + vpColVector point_3D({ x * Z, y * Z, Z }); + if (point_3D[2] > Z_min) { +#if defined(_OPENMP) + std::lock_guard lock(mutex); +#endif + pointcloud->push_back(pcl::PointXYZ(point_3D[0], point_3D[1], point_3D[2])); + } + } + } + } + } + } + else { +#if defined(_OPENMP) + std::mutex mutex; +#pragma omp parallel for +#endif + for (int p = 0; p < size; ++p) { + if (static_cast(depth_raw.bitmap[p])) { + float Z = static_cast(depth_raw.bitmap[p]) * depth_scale; + if (Z < 2.5) { + double x = 0; + double y = 0; + unsigned int j = p % width; + unsigned int i = (p - j) / width; + vpPixelMeterConversion::convertPoint(cam_depth, j, i, x, y); + vpColVector point_3D({ x * Z, y * Z, Z, 1 }); + if (point_3D[2] >= 0.1) { +#if defined(_OPENMP) + std::lock_guard lock(mutex); +#endif + pointcloud->push_back(pcl::PointXYZ(point_3D[0], point_3D[1], point_3D[2])); + } + } + } + } + } +} +#endif From a7782c5fd8cf97275a07a3bf952aa2bb45d5e08c Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Tue, 2 Apr 2024 16:33:32 +0200 Subject: [PATCH 09/32] Introduce vpArray2D contructor from std::vector Refactor testArray2D.cpp tu use catch Introduce new tests --- modules/core/include/visp3/core/vpArray2D.h | 184 +++++++++------ modules/core/test/math/testArray2D.cpp | 244 +++++++++++++++----- 2 files changed, 293 insertions(+), 135 deletions(-) diff --git a/modules/core/include/visp3/core/vpArray2D.h b/modules/core/include/visp3/core/vpArray2D.h index 1644e15f37..1bb011dd69 100644 --- a/modules/core/include/visp3/core/vpArray2D.h +++ b/modules/core/include/visp3/core/vpArray2D.h @@ -41,6 +41,7 @@ #include #include #include +#include #include #include @@ -145,7 +146,7 @@ template class vpArray2D vpArray2D() : rowNum(0), colNum(0), rowPtrs(nullptr), dsize(0), data(nullptr) { } /*! - Copy constructor of a 2D array. + Copy constructor of a 2D array. */ vpArray2D(const vpArray2D &A) : @@ -160,10 +161,10 @@ template class vpArray2D } /*! - Constructor that initializes a 2D array with 0. + Constructor that initializes a 2D array with 0. - \param r : Array number of rows. - \param c : Array number of columns. + \param r : Array number of rows. + \param c : Array number of columns. */ vpArray2D(unsigned int r, unsigned int c) : @@ -177,11 +178,11 @@ template class vpArray2D } /*! - Constructor that initialize a 2D array with \e val. + Constructor that initialize a 2D array with \e val. - \param r : Array number of rows. - \param c : Array number of columns. - \param val : Each element of the array is set to \e val. + \param r : Array number of rows. + \param c : Array number of columns. + \param val : Each element of the array is set to \e val. */ vpArray2D(unsigned int r, unsigned int c, Type val) : @@ -195,6 +196,45 @@ template class vpArray2D *this = val; } + /*! + Constructor that initialize a 2D array from a std::vector. + + - When c = 0, create a colum vector with dimension (data.size, 1) + - When r = 0, create a row vector with dimension (1, data.size) + - Otherwise create an array with dimension (r, c) + + \param r : Array number of rows. + \param c : Array number of columns. + \param vec : Data used to initialize the 2D array. + */ + vpArray2D(const std::vector &vec, unsigned int r = 0, unsigned int c = 0) + : +#if ((__cplusplus >= 201103L) || (defined(_MSVC_LANG) && (_MSVC_LANG >= 201103L))) // Check if cxx11 or higher + vpArray2D() +#else + rowNum(0), colNum(0), rowPtrs(nullptr), dsize(0), data(nullptr) +#endif + { + if (r > 0 && c > 0) { + if ((r * c) != vec.size()) { + throw(vpException(vpException::dimensionError, + "Cannot initialize vpArray(%d, %d) from std::vector(%d). Wrong dimension", r, c, vec.size())); + } + resize(r, c, false, false); + } + else if (c == 0) { + resize(vec.size(), 1, false, false); + } + else if (r == 0) { + resize(1, vec.size(), false, false); + } +// #if ((__cplusplus >= 201103L) || (defined(_MSVC_LANG) && (_MSVC_LANG >= 201103L))) // Check if cxx11 or higher +// std::copy(vec.begin(), vec.end(), data); +// #else + memcpy(data, vec.data(), vec.size() * sizeof(Type)); +// #endif + } + #if ((__cplusplus >= 201103L) || (defined(_MSVC_LANG) && (_MSVC_LANG >= 201103L))) // Check if cxx11 or higher vpArray2D(vpArray2D &&A) noexcept { @@ -419,14 +459,14 @@ template class vpArray2D /*! - Insert array A at the given position in the current array. + Insert array A at the given position in the current array. - \warning Throw vpException::dimensionError if the - dimensions of the matrices do not allow the operation. + \warning Throw vpException::dimensionError if the + dimensions of the matrices do not allow the operation. - \param A : The array to insert. - \param r : The index of the row to begin to insert data. - \param c : The index of the column to begin to insert data. + \param A : The array to insert. + \param r : The index of the row to begin to insert data. + \param c : The index of the column to begin to insert data. */ void insert(const vpArray2D &A, unsigned int r, unsigned int c) { @@ -858,39 +898,39 @@ template class vpArray2D \param filename : absolute file name. \param A : array to be saved in the file. \param header : optional lines that will be saved at the beginning of the - file. Should be YAML-formatted and will adapt to the indentation if any. + file. Should be YAML-formatted and will adapt to the indentation if any. \return Returns true if success. Here is an example of outputs. - \code - vpArray2D M(3,4); - vpArray2D::saveYAML("matrix.yml", M, "example: a YAML-formatted header"); - vpArray2D::saveYAML("matrixIndent.yml", M, "example:\n - a YAML-formatted \ - header\n - with inner indentation"); - \endcode - Content of matrix.yml: - \code - example: a YAML-formatted header - rows: 3 - cols: 4 - data: - - [0, 0, 0, 0] - - [0, 0, 0, 0] - - [0, 0, 0, 0] - \endcode - Content of matrixIndent.yml: - \code - example: - - a YAML-formatted header - - with inner indentation - rows: 3 - cols: 4 - data: + \code + vpArray2D M(3,4); + vpArray2D::saveYAML("matrix.yml", M, "example: a YAML-formatted header"); + vpArray2D::saveYAML("matrixIndent.yml", M, "example:\n - a YAML-formatted \ + header\n - with inner indentation"); + \endcode + Content of matrix.yml: + \code + example: a YAML-formatted header + rows: 3 + cols: 4 + data: - [0, 0, 0, 0] - [0, 0, 0, 0] - [0, 0, 0, 0] - \endcode + \endcode + Content of matrixIndent.yml: + \code + example: + - a YAML-formatted header + - with inner indentation + rows: 3 + cols: 4 + data: + - [0, 0, 0, 0] + - [0, 0, 0, 0] + - [0, 0, 0, 0] + \endcode \sa loadYAML() */ @@ -963,65 +1003,65 @@ template class vpArray2D #endif /*! - Perform a 2D convolution similar to Matlab conv2 function: \f$ M \star kernel \f$. + Perform a 2D convolution similar to Matlab conv2 function: \f$ M \star kernel \f$. - \param M : First matrix. - \param kernel : Second matrix. - \param mode : Convolution mode: "full" (default), "same", "valid". + \param M : First matrix. + \param kernel : Second matrix. + \param mode : Convolution mode: "full" (default), "same", "valid". - \image html vpMatrix-conv2-mode.jpg "Convolution mode: full, same, valid (image credit: Theano doc)." + \image html vpMatrix-conv2-mode.jpg "Convolution mode: full, same, valid (image credit: Theano doc)." - \note This is a very basic implementation that does not use FFT. + \note This is a very basic implementation that does not use FFT. */ static vpArray2D conv2(const vpArray2D &M, const vpArray2D &kernel, const std::string &mode); /*! - Perform a 2D convolution similar to Matlab conv2 function: \f$ M \star kernel \f$. + Perform a 2D convolution similar to Matlab conv2 function: \f$ M \star kernel \f$. - \param M : First array. - \param kernel : Second array. - \param res : Result. - \param mode : Convolution mode: "full" (default), "same", "valid". + \param M : First array. + \param kernel : Second array. + \param res : Result. + \param mode : Convolution mode: "full" (default), "same", "valid". - \image html vpMatrix-conv2-mode.jpg "Convolution mode: full, same, valid (image credit: Theano doc)." + \image html vpMatrix-conv2-mode.jpg "Convolution mode: full, same, valid (image credit: Theano doc)." - \note This is a very basic implementation that does not use FFT. + \note This is a very basic implementation that does not use FFT. */ static void conv2(const vpArray2D &M, const vpArray2D &kernel, vpArray2D &res, const std::string &mode); /*! - Insert array B in array A at the given position. + Insert array B in array A at the given position. - \param A : Main array. - \param B : Array to insert. - \param r : Index of the row where to add the array. - \param c : Index of the column where to add the array. - \return Array with B insert in A. + \param A : Main array. + \param B : Array to insert. + \param r : Index of the row where to add the array. + \param c : Index of the column where to add the array. + \return Array with B insert in A. - \warning Throw exception if the sizes of the arrays do not allow the - insertion. + \warning Throw exception if the sizes of the arrays do not allow the + insertion. */ vpArray2D insert(const vpArray2D &A, const vpArray2D &B, unsigned int r, unsigned int c); /*! - \relates vpArray2D - Insert array B in array A at the given position. + \relates vpArray2D + Insert array B in array A at the given position. - \param A : Main array. - \param B : Array to insert. - \param C : Result array. - \param r : Index of the row where to insert array B. - \param c : Index of the column where to insert array B. + \param A : Main array. + \param B : Array to insert. + \param C : Result array. + \param r : Index of the row where to insert array B. + \param c : Index of the column where to insert array B. - \warning Throw exception if the sizes of the arrays do not - allow the insertion. + \warning Throw exception if the sizes of the arrays do not + allow the insertion. */ static void insert(const vpArray2D &A, const vpArray2D &B, vpArray2D &C, unsigned int r, unsigned int c); //@} }; /*! - Return the array min value. + Return the array min value. */ template Type vpArray2D::getMinValue() const { @@ -1038,7 +1078,7 @@ template Type vpArray2D::getMinValue() const } /*! - Return the array max value. + Return the array max value. */ template Type vpArray2D::getMaxValue() const { diff --git a/modules/core/test/math/testArray2D.cpp b/modules/core/test/math/testArray2D.cpp index 382f5ac1c4..f959d2bd8c 100644 --- a/modules/core/test/math/testArray2D.cpp +++ b/modules/core/test/math/testArray2D.cpp @@ -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 @@ -39,6 +39,12 @@ Test some vpArray2D functionalities. */ +#include + +#if defined(VISP_HAVE_CATCH2) +#define CATCH_CONFIG_RUNNER + +#include #include #include #include @@ -64,17 +70,37 @@ template bool test(const std::string &s, const vpArray2D & return true; } -int main() +template +bool test_hadamar(const vpArray2D &A, const vpArray2D &B, const vpArray2D &H) { + if (A.getCols() != B.getCols() || A.getCols() != H.getCols()) { + std::cout << "Test fails: bad columns size" << std::endl; + return false; + } + if (A.getRows() != B.getRows() || A.getRows() != H.getRows()) { + std::cout << "Test fails: bad rows size" << std::endl; + return false; + } + for (unsigned int i = 0; i < A.size(); i++) { + if (std::fabs((A.data[i] * B.data[i]) - H.data[i]) > std::numeric_limits::epsilon()) { + std::cout << "Test fails: bad content" << std::endl; + return false; + } + } + + return true; +} + +TEST_CASE("Test constructors with double", "[constructors]") +{ + SECTION("Default constructor") { - // test default constructor vpArray2D A; std::vector bench; - if (test("A", A, bench) == false) - return EXIT_FAILURE; + CHECK(test("A", A, bench)); } + SECTION("Copy constructor") { - // test copy constructor vpArray2D A(3, 4); std::vector bench(12); @@ -84,42 +110,79 @@ int main() bench[i * 4 + j] = (double)(i + j); } } - if (test("A", A, bench) == false) - return EXIT_FAILURE; + CHECK(test("A", A, bench)); vpArray2D B(A); - if (test("B", B, bench) == false) - return EXIT_FAILURE; - std::cout << "Min/Max: " << B.getMinValue() << " " << B.getMaxValue() << std::endl; + CHECK(test("B", B, bench)); } + SECTION("Constructor with initial value") { - // test constructor with initial value vpArray2D A(3, 4, 2.); std::vector bench1(12, 2); - if (test("A", A, bench1) == false) - return EXIT_FAILURE; + CHECK(test("A", A, bench1)); A.resize(5, 6); std::vector bench2(30, 0); - if (test("A", A, bench2) == false) - return EXIT_FAILURE; + CHECK(test("A", A, bench2)); A = -2.; std::vector bench3(30, -2); - if (test("A", A, bench3) == false) - return EXIT_FAILURE; + CHECK(test("A", A, bench3)); } - // Test with float + SECTION("Constructor from std::vector") + { + std::vector bench(12); + for (unsigned int i = 0; i < 3; i++) { + for (unsigned int j = 0; j < 4; j++) { + bench[i * 4 + j] = (double)(i + j); + } + } + SECTION("Keep default size (r=0, c=0)") + { + vpArray2D A(bench); + std::cout << "A with default size (r=0, c=0):\n" << A << std::endl; + CHECK(test("A", A, bench)); + CHECK(A.getRows() == bench.size()); + CHECK(A.getCols() == 1); + } + SECTION("Keep row size to 0") + { + vpArray2D A(bench, 0, bench.size()); + std::cout << "A with row size to 0:\n" << A << std::endl; + CHECK(test("A", A, bench)); + CHECK(A.getRows() == 1); + CHECK(A.getCols() == bench.size()); + } + SECTION("Keep col size to 0") + { + vpArray2D A(bench, bench.size(), 0); + std::cout << "A with col size to 0:\n" << A << std::endl; + CHECK(test("A", A, bench)); + CHECK(A.getRows() == bench.size()); + CHECK(A.getCols() == 1); + } + SECTION("Set r=3 and c=4") + { + vpArray2D A(bench, 3, 4); + std::cout << "A with r=3 and c=4:\n" << A << std::endl; + CHECK(test("A", A, bench)); + CHECK(A.getRows() == 3); + CHECK(A.getCols() == 4); + } + } +} + +TEST_CASE("Test constructors with float", "[constructors]") +{ + SECTION("Default constructor") { - // test default constructor vpArray2D A; std::vector bench; - if (test("A", A, bench) == false) - return EXIT_FAILURE; + CHECK(test("A", A, bench)); } + SECTION("Copy constructor") { - // test copy constructor vpArray2D A(3, 4); std::vector bench(12); @@ -129,62 +192,117 @@ int main() bench[i * 4 + j] = (float)(i + j); } } - if (test("A", A, bench) == false) - return EXIT_FAILURE; + CHECK(test("A", A, bench)); vpArray2D B(A); - if (test("B", B, bench) == false) - return EXIT_FAILURE; - std::cout << "Min/Max: " << B.getMinValue() << " " << B.getMaxValue() << std::endl; + CHECK(test("B", B, bench)); } + SECTION("Constructor with initial value") { - // test constructor with initial value vpArray2D A(3, 4, 2.); std::vector bench1(12, 2); - if (test("A", A, bench1) == false) - return EXIT_FAILURE; + CHECK(test("A", A, bench1)); A.resize(5, 6); std::vector bench2(30, 0); - if (test("A", A, bench2) == false) - return EXIT_FAILURE; + CHECK(test("A", A, bench2)); A = -2.; std::vector bench3(30, -2); - if (test("A", A, bench3) == false) - return EXIT_FAILURE; + CHECK(test("A", A, bench3)); } + SECTION("Constructor from std::vector") { - // Test Hadamard product - std::cout << "\nTest Hadamard product" << std::endl; - vpArray2D A1(3, 5), A2(3, 5); - vpRowVector R1(15), R2(15); - vpColVector C1(15), C2(15); - - for (unsigned int i = 0; i < A1.size(); i++) { - A1.data[i] = i; - A2.data[i] = i + 2; - R1.data[i] = i; - R2.data[i] = i + 2; - C1.data[i] = i; - C2.data[i] = i + 2; + std::vector bench(12); + for (unsigned int i = 0; i < 3; i++) { + for (unsigned int j = 0; j < 4; j++) { + bench[i * 4 + j] = (float)(i + j); + } } + SECTION("Keep default size (r=0, c=0)") + { + vpArray2D A(bench); + std::cout << "A with default size (r=0, c=0):\n" << A << std::endl; + CHECK(test("A", A, bench)); + CHECK(A.getRows() == bench.size()); + CHECK(A.getCols() == 1); + } + SECTION("Keep row size to 0") + { + vpArray2D A(bench, 0, bench.size()); + std::cout << "A with row size to 0:\n" << A << std::endl; + CHECK(test("A", A, bench)); + CHECK(A.getRows() == 1); + CHECK(A.getCols() == bench.size()); + } + SECTION("Keep col size to 0") + { + vpArray2D A(bench, bench.size(), 0); + std::cout << "A with col size to 0:\n" << A << std::endl; + CHECK(test("A", A, bench)); + CHECK(A.getRows() == bench.size()); + CHECK(A.getCols() == 1); + } + SECTION("Set r=3 and c=4") + { + vpArray2D A(bench, 3, 4); + std::cout << "A with r=3 and c=4:\n" << A << std::endl; + CHECK(test("A", A, bench)); + CHECK(A.getRows() == 3); + CHECK(A.getCols() == 4); + } + } +} - std::cout << "A1:\n" << A1 << std::endl; - std::cout << "\nA2:\n" << A2 << std::endl; - A2 = A1.hadamard(A2); - std::cout << "\nRes:\n" << A2 << std::endl; - - std::cout << "\nR1:\n" << R1 << std::endl; - std::cout << "\nR2:\n" << R2 << std::endl; - R2 = R1.hadamard(R2); - std::cout << "\nRes:\n" << R2 << std::endl; +TEST_CASE("Test Hadamar product", "[hadamar]") +{ + vpArray2D A1(3, 5), A2(3, 5), A3; + vpRowVector R1(15), R2(15), R3; + vpColVector C1(15), C2(15), C3; - std::cout << "\nC1:\n" << C1 << std::endl; - std::cout << "\nC2:\n" << C2 << std::endl; - C2 = C1.hadamard(C2); - std::cout << "\nRes:\n" << C2 << std::endl; + for (unsigned int i = 0; i < A1.size(); i++) { + A1.data[i] = i; + A2.data[i] = i + 2; + R1.data[i] = i; + R2.data[i] = i + 2; + C1.data[i] = i; + C2.data[i] = i + 2; } - std::cout << "All tests succeed" << std::endl; - return EXIT_SUCCESS; + + std::cout << "A1:\n" << A1 << std::endl; + std::cout << "\nA2:\n" << A2 << std::endl; + A3 = A1.hadamard(A2); + CHECK(test_hadamar(A1, A2, A3)); + std::cout << "\nRes hadamar(A1, A2):\n" << A3 << std::endl; + + std::cout << "\nR1:\n" << R1 << std::endl; + std::cout << "\nR2:\n" << R2 << std::endl; + R3 = R1.hadamard(R2); + CHECK(test_hadamar(R1, R2, R3)); + std::cout << "\nRes hadamar(R1, R2):\n" << R3 << std::endl; + + std::cout << "\nC1:\n" << C1 << std::endl; + std::cout << "\nC2:\n" << C2 << std::endl; + C3 = C1.hadamard(C2); + CHECK(test_hadamar(C1, C2, C3)); + std::cout << "\nRes hadamar(C1, C2):\n" << C3 << std::endl; +} + +int main(int argc, char *argv[]) +{ + Catch::Session session; // There must be exactly one instance + + // Let Catch (using Clara) parse the command line + session.applyCommandLine(argc, argv); + + int numFailed = session.run(); + + // numFailed is clamped to 255 as some unices only use the lower 8 bits. + // This clamping has already been applied, so just return it here + // You can also do any post run clean-up here + std::cout << (numFailed ? "Test failed" : "Test succeed") << std::endl; + return numFailed; } +#else +int main() { return EXIT_SUCCESS; } +#endif From 5453a89e1542bca312b9ebf7c5113e8abe9a098a Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Tue, 2 Apr 2024 16:35:00 +0200 Subject: [PATCH 10/32] Introduce new vpImageTools::inRange() where thresholds are in a std::vector --- .../core/include/visp3/core/vpImageTools.h | 2 + modules/core/src/image/vpImageTools.cpp | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/modules/core/include/visp3/core/vpImageTools.h b/modules/core/include/visp3/core/vpImageTools.h index fc15db6157..ed72766a32 100644 --- a/modules/core/include/visp3/core/vpImageTools.h +++ b/modules/core/include/visp3/core/vpImageTools.h @@ -128,6 +128,8 @@ class VISP_EXPORT vpImageTools static void inRange(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, const vpColVector &hsv_values, unsigned char *mask, unsigned int size); + static void inRange(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, + const std::vector &hsv_values, unsigned char *mask, unsigned int size); static void initUndistortMap(const vpCameraParameters &cam, unsigned int width, unsigned int height, vpArray2D &mapU, vpArray2D &mapV, vpArray2D &mapDu, vpArray2D &mapDv); diff --git a/modules/core/src/image/vpImageTools.cpp b/modules/core/src/image/vpImageTools.cpp index 9f0365e3fb..2d9974dc8d 100644 --- a/modules/core/src/image/vpImageTools.cpp +++ b/modules/core/src/image/vpImageTools.cpp @@ -1027,3 +1027,51 @@ void vpImageTools::inRange(const unsigned char *hue, const unsigned char *satura } } } + +/*! + * Create binary mask by checking if HSV (hue, saturation, value) channels lie between low and high HSV thresholds. + * \param[in] hue : Pointer to an array of hue values. Its dimension is equal to the `size` parameter. + * \param[in] saturation : Pointer to an array of saturation values. Its dimension is equal to the `size` parameter. + * \param[in] value : Pointer to an array of values. Its dimension is equal to the `size` parameter. + * \param[in] hsv_values : 6-dim vector that contains the low/high threshold values for each HSV channel respectively. + * Each element of this vector should be in [0,255] range. Note that there is also tutorial-hsv-tuner.cpp that may help + * to determine low/high HSV values. + * \param[out] mask : Pointer to a resulting mask of dimension `size`. When HSV value is in the boundaries, the mask + * element is set to 255, otherwise to 0. The mask should be allocated prior calling this function. Its dimension + * is equal to the `size` parameter. + * \param[in] size : Size of `hue`, `saturation`, `value` and `mask` arrays. + * + * \sa vpImageConvert::RGBToHSV(const unsigned char *, unsigned char *, unsigned char *, unsigned char *, unsigned int, bool) + * \sa vpImageConvert::RGBaToHSV(const unsigned char *, unsigned char *, unsigned char *, unsigned char *, unsigned int, bool) + */ +void vpImageTools::inRange(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, + const std::vector &hsv_values, unsigned char *mask, unsigned int size) +{ + if ((hue == nullptr) || (saturation == nullptr) || (value == nullptr)) { + throw(vpImageException(vpImageException::notInitializedError, + "Error in vpImageTools::inRange(): hsv pointer are empty")); + } + else if (hsv_values.size() != 6) { + throw(vpImageException(vpImageException::notInitializedError, + "Error in vpImageTools::inRange(): wrong values vector size (%d)", hsv_values.size())); + } + unsigned char h_low = static_cast(hsv_values[0]); + unsigned char h_high = static_cast(hsv_values[1]); + unsigned char s_low = static_cast(hsv_values[2]); + unsigned char s_high = static_cast(hsv_values[3]); + unsigned char v_low = static_cast(hsv_values[4]); + unsigned char v_high = static_cast(hsv_values[5]); +#if defined(_OPENMP) +#pragma omp parallel for +#endif + for (unsigned int i = 0; i < size; ++i) { + if ((h_low <= hue[i]) && (hue[i] <= h_high) && + (s_low <= saturation[i]) && (saturation[i] <= s_high) && + (v_low <= value[i]) && (value[i] <= v_high)) { + mask[i] = 255; + } + else { + mask[i] = 0; + } + } +} From 50196062703cf4d3514462506ef0a73ba58424e9 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Tue, 2 Apr 2024 16:36:02 +0200 Subject: [PATCH 11/32] Add pcl as 3rd party in core module for recent changes in vpImageTools::depthToPointCloud() --- modules/core/CMakeLists.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/core/CMakeLists.txt b/modules/core/CMakeLists.txt index 8175cbf5ca..c43a4b9f78 100644 --- a/modules/core/CMakeLists.txt +++ b/modules/core/CMakeLists.txt @@ -271,6 +271,14 @@ if(USE_NLOHMANN_JSON) list(APPEND opt_incs ${_inc_dirs}) endif() +if(USE_PCL) + list(APPEND opt_incs ${PCL_INCLUDE_DIRS}) + # To ensure to build with VTK and other PCL 3rd parties we are not using PCL_LIBRARIES but PCL_DEPS_INCLUDE_DIRS + # and PCL_DEPS_LIBRARIES instead + list(APPEND opt_incs ${PCL_DEPS_INCLUDE_DIRS}) + list(APPEND opt_libs ${PCL_DEPS_LIBRARIES}) +endif() + if(WITH_PUGIXML) # pugixml is private and provides default XML I/O capabilities include_directories(${PUGIXML_INCLUDE_DIRS}) From 8accc4b1fe3b744354480964cf1e0fba74170f6a Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Tue, 2 Apr 2024 16:37:48 +0200 Subject: [PATCH 12/32] Introduce new class vpDisplayPCL able to display a point cloud --- modules/gui/include/visp3/gui/vpDisplayPCL.h | 66 +++++++++++++ modules/gui/src/pointcloud/vpDisplayPCL.cpp | 97 ++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 modules/gui/include/visp3/gui/vpDisplayPCL.h create mode 100644 modules/gui/src/pointcloud/vpDisplayPCL.cpp diff --git a/modules/gui/include/visp3/gui/vpDisplayPCL.h b/modules/gui/include/visp3/gui/vpDisplayPCL.h new file mode 100644 index 0000000000..3937d86519 --- /dev/null +++ b/modules/gui/include/visp3/gui/vpDisplayPCL.h @@ -0,0 +1,66 @@ +/* + * ViSP, open source Visual Servoing Platform software. + * 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 + * 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 a point cloud using PCL library. + */ + +#ifndef _vpDisplayPCL_h_ +#define _vpDisplayPCL_h_ + +#include + +#if defined(VISP_HAVE_PCL) && defined(VISP_HAVE_THREADS) + +#include + +#include +#include + +class VISP_EXPORT vpDisplayPCL +{ +public: + explicit vpDisplayPCL(); + + void flush(); + + void run(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud); + ; + void setVerbose(bool verbose); + void stop(); + +private: + bool m_stop; + bool m_flush_viewer; + bool m_verbose; +}; + +#endif + +#endif diff --git a/modules/gui/src/pointcloud/vpDisplayPCL.cpp b/modules/gui/src/pointcloud/vpDisplayPCL.cpp new file mode 100644 index 0000000000..3637c96760 --- /dev/null +++ b/modules/gui/src/pointcloud/vpDisplayPCL.cpp @@ -0,0 +1,97 @@ +/* + * ViSP, open source Visual Servoing Platform software. + * 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 + * 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 a point cloud using PCL library. + */ + +#include + +#if defined(VISP_HAVE_PCL) && defined(VISP_HAVE_THREADS) + +#include + +vpDisplayPCL::vpDisplayPCL() : m_stop(false), m_flush_viewer(false), m_verbose(false) { } + +void vpDisplayPCL::flush() +{ + m_flush_viewer = true; +} + +void vpDisplayPCL::run(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud) +{ + pcl::PointCloud::Ptr local_pointcloud(new pcl::PointCloud()); + + bool flush_viewer = false; + pcl::visualization::PCLVisualizer::Ptr viewer(new pcl::visualization::PCLVisualizer("3D Viewer")); + viewer->setBackgroundColor(0, 0, 0); + viewer->initCameraParameters(); + viewer->setPosition(640 + 80, 480 + 80); + viewer->setCameraPosition(0, 0, -0.25, 0, -1, 0); + viewer->setSize(640, 480); + + while (!m_stop) { + { + std::lock_guard lock(mutex); + flush_viewer = m_flush_viewer; + m_flush_viewer = false; + local_pointcloud = pointcloud->makeShared(); + + } + + // If updatePointCloud fails, it means that the pcl was not previously known by the viewer + if (!viewer->updatePointCloud(local_pointcloud, "sample cloud")) { + // Add the pcl to the list of pcl known by the viewer + the according legend + viewer->addPointCloud(local_pointcloud, "sample cloud"); + viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "sample cloud"); + } + + viewer->spinOnce(10); + } + + if (m_verbose) { + std::cout << "End of point cloud display thread" << std::endl; + } +} + +void vpDisplayPCL::stop() +{ + m_stop = true; +} + +void vpDisplayPCL::setVerbose(bool verbose) +{ + m_verbose = verbose; +} + +#elif !defined(VISP_BUILD_SHARED_LIBS) +// Work around to avoid warning: libvisp_gui.a(vpDisplayPCL.cpp.o) has no symbols +void dummy_vpDisplayPCL() { }; + +#endif From 35dc9d62d4053ef4e2556ff0f2a317ad3cb1aba6 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Tue, 2 Apr 2024 16:39:35 +0200 Subject: [PATCH 13/32] Introduce new tutorials to achieve HSV based color segmentation --- tutorial/CMakeLists.txt | 1 + tutorial/segmentation/color/CMakeLists.txt | 20 ++ .../tutorial-hsv-segmentation-pcl-viewer.cpp | 221 ++++++++++++++ .../color/tutorial-hsv-segmentation-pcl.cpp | 144 +++++++++ .../color/tutorial-hsv-segmentation.cpp | 126 ++++++++ .../segmentation/color/tutorial-hsv-tuner.cpp | 277 ++++++++++++++++++ 6 files changed, 789 insertions(+) create mode 100644 tutorial/segmentation/color/CMakeLists.txt create mode 100644 tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp create mode 100644 tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp create mode 100644 tutorial/segmentation/color/tutorial-hsv-segmentation.cpp create mode 100644 tutorial/segmentation/color/tutorial-hsv-tuner.cpp diff --git a/tutorial/CMakeLists.txt b/tutorial/CMakeLists.txt index dc23975c96..2441f927e1 100644 --- a/tutorial/CMakeLists.txt +++ b/tutorial/CMakeLists.txt @@ -49,6 +49,7 @@ visp_add_subdirectory(munkres REQUIRED_DEPS visp_co visp_add_subdirectory(robot/flir-ptu REQUIRED_DEPS visp_core visp_robot visp_sensor visp_vision visp_gui visp_vs visp_visual_features visp_detection) visp_add_subdirectory(robot/pioneer REQUIRED_DEPS visp_core visp_robot visp_vs visp_gui) visp_add_subdirectory(robot/mbot/raspberry/visp REQUIRED_DEPS visp_core visp_detection visp_io visp_gui visp_sensor visp_vs) +visp_add_subdirectory(segmentation/color REQUIRED_DEPS visp_core visp_sensor visp_io visp_gui) visp_add_subdirectory(simulator/image REQUIRED_DEPS visp_core visp_robot visp_io visp_gui) visp_add_subdirectory(trace REQUIRED_DEPS visp_core) visp_add_subdirectory(tracking/blob REQUIRED_DEPS visp_core visp_blob visp_io visp_gui visp_sensor) diff --git a/tutorial/segmentation/color/CMakeLists.txt b/tutorial/segmentation/color/CMakeLists.txt new file mode 100644 index 0000000000..d2081ab26a --- /dev/null +++ b/tutorial/segmentation/color/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.5) + +project(color_segmentation) + +find_package(VISP REQUIRED visp_core visp_sensor visp_io visp_gui) + +# set the list of source files +set(tutorial_cpp + tutorial-hsv-segmentation.cpp + tutorial-hsv-segmentation-pcl.cpp + tutorial-hsv-segmentation-pcl-viewer.cpp + tutorial-hsv-tuner.cpp +) + +foreach(cpp ${tutorial_cpp}) + visp_add_target(${cpp}) + if(COMMAND visp_add_dependency) + visp_add_dependency(${cpp} "tutorials") + endif() +endforeach() diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp new file mode 100644 index 0000000000..687e1ea6ef --- /dev/null +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp @@ -0,0 +1,221 @@ +//! \example tutorial-hsv-segmentation-pcl.cpp + +#include +#include + +#if defined(VISP_HAVE_REALSENSE2) && defined(VISP_HAVE_PCL) && defined(VISP_HAVE_THREADS) +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#if 1 +class vpPointCloudViewer +{ +public: + explicit vpPointCloudViewer() : m_stop(false), m_flush_viewer(false) { } + + void flush() + { + m_flush_viewer = true; + } + + void run(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud) + { + pcl::PointCloud::Ptr local_pointcloud(new pcl::PointCloud()); + + bool flush_viewer = false; + pcl::visualization::PCLVisualizer::Ptr viewer(new pcl::visualization::PCLVisualizer("3D Viewer")); + viewer->setBackgroundColor(0, 0, 0); + viewer->initCameraParameters(); + viewer->setPosition(640 + 80, 480 + 80); + viewer->setCameraPosition(0, 0, -0.25, 0, -1, 0); + viewer->setSize(640, 480); + + bool first_init = true; + while (!m_stop) { + { + std::lock_guard lock(mutex); + flush_viewer = m_flush_viewer; + m_flush_viewer = false; + local_pointcloud = pointcloud->makeShared(); + } + + if (flush_viewer) { + if (first_init) { + + viewer->addPointCloud(local_pointcloud, "sample cloud"); + viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "sample cloud"); + first_init = false; + } + else { + viewer->updatePointCloud(local_pointcloud, "sample cloud"); + } + } + + viewer->spinOnce(10); + } + + std::cout << "End of point cloud display thread" << std::endl; + } + + void stop() + { + m_stop = true; + } + +private: + bool m_stop; + bool m_flush_viewer; +}; +#endif + +int main(int argc, char **argv) +{ + std::string opt_hsv_filename = "calib/hsv-thresholds.yml"; + + for (int i = 0; i < argc; i++) { + if (std::string(argv[i]) == "--hsv-thresholds") { + opt_hsv_filename = std::string(argv[++i]); + } + else if (std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") { + std::cout << "\nSYNOPSIS " << std::endl + << argv[0] + << " [--hsv-thresholds ]" + << " [--help,-h]" + << std::endl; + std::cout << "\nOPTIONS " << std::endl + << " --hsv-thresholds " << std::endl + << " Path to a yaml filename that contains H , S , V threshold values." << std::endl + << " An Example of such a file could be:" << std::endl + << " rows: 6" << std::endl + << " cols: 1" << std::endl + << " data:" << std::endl + << " - [0]" << std::endl + << " - [42]" << std::endl + << " - [177]" << std::endl + << " - [237]" << std::endl + << " - [148]" << std::endl + << " - [208]" << std::endl + << std::endl + << " --help, -h" << std::endl + << " Display this helper message." << std::endl + << std::endl; + return EXIT_SUCCESS; + } + } + + vpColVector hsv_values; + if (vpColVector::loadYAML(opt_hsv_filename, hsv_values)) { + std::cout << "Load HSV threshold values from " << opt_hsv_filename << std::endl; + std::cout << "HSV low/high values: " << hsv_values.t() << std::endl; + } + else { + std::cout << "Warning: unable to load HSV thresholds values from " << opt_hsv_filename << std::endl; + return EXIT_FAILURE; + } + + int width = 848, height = 480, fps = 60; + vpRealSense2 rs; + rs2::config config; + config.enable_stream(RS2_STREAM_COLOR, width, height, RS2_FORMAT_RGBA8, fps); + config.enable_stream(RS2_STREAM_DEPTH, width, height, RS2_FORMAT_Z16, fps); + config.disable_stream(RS2_STREAM_INFRARED, 1); + config.disable_stream(RS2_STREAM_INFRARED, 2); + rs2::align align_to(RS2_STREAM_COLOR); + + rs.open(config); + + float depth_scale = rs.getDepthScale(); + vpCameraParameters cam_depth = rs.getCameraParameters(RS2_STREAM_DEPTH, + vpCameraParameters::perspectiveProjWithoutDistortion); + pcl::PointCloud::Ptr pointcloud = pcl::PointCloud::Ptr(new pcl::PointCloud); + + vpImage Ic(height, width); + vpImage H(height, width); + vpImage S(height, width); + vpImage V(height, width); + vpImage Ic_segmented(height, width, 0); + vpImage depth_raw(height, width); + + vpDisplayX d_Ic(Ic, 0, 0, "Current frame"); + vpDisplayX d_Ic_segmented(Ic_segmented, Ic.getWidth()+75, 0, "HSV segmented frame"); + + bool quit = false; + double loop_time = 0., total_loop_time = 0.; + long nb_iter = 0; + float Z_min = 0.2; + float Z_max = 2.5; + int pcl_size = 0; + + vpDisplayPCL pcl_viewer; + std::mutex pcl_viewer_mutex; + std::thread pcl_viewer_thread(&vpDisplayPCL::run, &pcl_viewer, std::ref(pcl_viewer_mutex), pointcloud); + + while (!quit) { + double t = vpTime::measureTimeMs(); + rs.acquire((unsigned char *)Ic.bitmap, (unsigned char *)(depth_raw.bitmap), NULL, NULL, &align_to); + vpImageConvert::RGBaToHSV(reinterpret_cast(Ic.bitmap), + reinterpret_cast(H.bitmap), + reinterpret_cast(S.bitmap), + reinterpret_cast(V.bitmap), Ic.getSize()); + + vpImageTools::inRange(reinterpret_cast(H.bitmap), + reinterpret_cast(S.bitmap), + reinterpret_cast(V.bitmap), + hsv_values, + reinterpret_cast(Ic_segmented.bitmap), + Ic_segmented.getSize()); + + { + std::lock_guard lock(pcl_viewer_mutex); + vpImageConvert::depthToPointCloud(depth_raw, depth_scale, cam_depth, pointcloud, &Ic_segmented, Z_min, Z_max); + pcl_size = pointcloud->size(); + pcl_viewer.flush(); + } + + std::cout << "Segmented point cloud size: " << pcl_size << std::endl; + + vpDisplay::display(Ic); + vpDisplay::display(Ic_segmented); + vpDisplay::displayText(Ic, 20, 20, "Click to quit...", vpColor::red); + + if (vpDisplay::getClick(Ic, false)) { + quit = true; + } + + vpDisplay::flush(Ic); + vpDisplay::flush(Ic_segmented); + nb_iter++; + loop_time = vpTime::measureTimeMs() - t; + total_loop_time += loop_time; + } + + pcl_viewer.stop(); + + if (pcl_viewer_thread.joinable()) { + pcl_viewer_thread.join(); + } + std::cout << "Mean loop time: " << total_loop_time / nb_iter << std::endl; + return EXIT_SUCCESS; +} +#else +int main() +{ +#if !defined(VISP_HAVE_REALSENSE2) + std::cout << "This tutorial needs librealsense as 3rd party." << std::endl; +#endif +#if !defined(VISP_HAVE_PCL) + std::cout << "This tutorial needs pcl library as 3rd party." << std::endl; +#endif + std::cout << "Install missing 3rd party, configure and rebuild ViSP." << std::endl; + return EXIT_SUCCESS; +} +#endif diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp new file mode 100644 index 0000000000..02364500a8 --- /dev/null +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp @@ -0,0 +1,144 @@ +//! \example tutorial-hsv-segmentation-pcl.cpp + +#include +#include + +#if defined(VISP_HAVE_REALSENSE2) && defined(VISP_HAVE_PCL) +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + std::string opt_hsv_filename = "calib/hsv-thresholds.yml"; + + for (int i = 0; i < argc; i++) { + if (std::string(argv[i]) == "--hsv-thresholds") { + opt_hsv_filename = std::string(argv[++i]); + } + else if (std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") { + std::cout << "\nSYNOPSIS " << std::endl + << argv[0] + << " [--hsv-thresholds ]" + << " [--help,-h]" + << std::endl; + std::cout << "\nOPTIONS " << std::endl + << " --hsv-thresholds " << std::endl + << " Path to a yaml filename that contains H , S , V threshold values." << std::endl + << " An Example of such a file could be:" << std::endl + << " rows: 6" << std::endl + << " cols: 1" << std::endl + << " data:" << std::endl + << " - [0]" << std::endl + << " - [42]" << std::endl + << " - [177]" << std::endl + << " - [237]" << std::endl + << " - [148]" << std::endl + << " - [208]" << std::endl + << std::endl + << " --help, -h" << std::endl + << " Display this helper message." << std::endl + << std::endl; + return EXIT_SUCCESS; + } + } + + vpColVector hsv_values; + if (vpColVector::loadYAML(opt_hsv_filename, hsv_values)) { + std::cout << "Load HSV threshold values from " << opt_hsv_filename << std::endl; + std::cout << "HSV low/high values: " << hsv_values.t() << std::endl; + } + else { + std::cout << "Warning: unable to load HSV thresholds values from " << opt_hsv_filename << std::endl; + return EXIT_FAILURE; + } + + int width = 848, height = 480, fps = 60; + vpRealSense2 rs; + rs2::config config; + config.enable_stream(RS2_STREAM_COLOR, width, height, RS2_FORMAT_RGBA8, fps); + config.enable_stream(RS2_STREAM_DEPTH, width, height, RS2_FORMAT_Z16, fps); + config.disable_stream(RS2_STREAM_INFRARED, 1); + config.disable_stream(RS2_STREAM_INFRARED, 2); + rs2::align align_to(RS2_STREAM_COLOR); + + rs.open(config); + + float depth_scale = rs.getDepthScale(); + vpCameraParameters cam_depth = rs.getCameraParameters(RS2_STREAM_DEPTH, + vpCameraParameters::perspectiveProjWithoutDistortion); + pcl::PointCloud::Ptr pointcloud = pcl::PointCloud::Ptr(new pcl::PointCloud); + + vpImage Ic(height, width); + vpImage H(height, width); + vpImage S(height, width); + vpImage V(height, width); + vpImage Ic_segmented(height, width, 0); + vpImage depth_raw(height, width); + + vpDisplayX d_Ic(Ic, 0, 0, "Current frame"); + vpDisplayX d_Ic_segmented(Ic_segmented, Ic.getWidth()+75, 0, "HSV segmented frame"); + + bool quit = false; + double loop_time = 0., total_loop_time = 0.; + long nb_iter = 0; + float Z_min = 0.2; + float Z_max = 2.5; + int pcl_size = 0; + + while (!quit) { + double t = vpTime::measureTimeMs(); + rs.acquire((unsigned char *)Ic.bitmap, (unsigned char *)(depth_raw.bitmap), NULL, NULL, &align_to); + vpImageConvert::RGBaToHSV(reinterpret_cast(Ic.bitmap), + reinterpret_cast(H.bitmap), + reinterpret_cast(S.bitmap), + reinterpret_cast(V.bitmap), Ic.getSize()); + + vpImageTools::inRange(reinterpret_cast(H.bitmap), + reinterpret_cast(S.bitmap), + reinterpret_cast(V.bitmap), + hsv_values, + reinterpret_cast(Ic_segmented.bitmap), + Ic_segmented.getSize()); + + vpImageConvert::depthToPointCloud(depth_raw, depth_scale, cam_depth, pointcloud, &Ic_segmented, Z_min, Z_max); + pcl_size = pointcloud->size(); + + std::cout << "Segmented point cloud size: " << pcl_size << std::endl; + + vpDisplay::display(Ic); + vpDisplay::display(Ic_segmented); + vpDisplay::displayText(Ic, 20, 20, "Click to quit...", vpColor::red); + + if (vpDisplay::getClick(Ic, false)) { + quit = true; + } + + vpDisplay::flush(Ic); + vpDisplay::flush(Ic_segmented); + nb_iter++; + loop_time = vpTime::measureTimeMs() - t; + total_loop_time += loop_time; + } + + + std::cout << "Mean loop time: " << total_loop_time / nb_iter << std::endl; + return EXIT_SUCCESS; +} +#else +int main() +{ +#if !defined(VISP_HAVE_REALSENSE2) + std::cout << "This tutorial needs librealsense as 3rd party." << std::endl; +#endif +#if !defined(VISP_HAVE_PCL) + std::cout << "This tutorial needs pcl library as 3rd party." << std::endl; +#endif + std::cout << "Install missing 3rd party, configure and rebuild ViSP." << std::endl; + return EXIT_SUCCESS; +} +#endif diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp new file mode 100644 index 0000000000..798b9eec23 --- /dev/null +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp @@ -0,0 +1,126 @@ +//! \example tutorial-hsv-segmentation.cpp + +#include +#include + +#if defined(VISP_HAVE_REALSENSE2) +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + std::string opt_hsv_filename = "calib/hsv-thresholds.yml"; + + for (int i = 0; i < argc; i++) { + if (std::string(argv[i]) == "--hsv-thresholds") { + opt_hsv_filename = std::string(argv[++i]); + } + else if (std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") { + std::cout << "\nSYNOPSIS " << std::endl + << argv[0] + << " [--hsv-thresholds ]" + << " [--help,-h]" + << std::endl; + std::cout << "\nOPTIONS " << std::endl + << " --hsv-thresholds " << std::endl + << " Path to a yaml filename that contains H , S , V threshold values." << std::endl + << " An Example of such a file could be:" << std::endl + << " rows: 6" << std::endl + << " cols: 1" << std::endl + << " data:" << std::endl + << " - [0]" << std::endl + << " - [42]" << std::endl + << " - [177]" << std::endl + << " - [237]" << std::endl + << " - [148]" << std::endl + << " - [208]" << std::endl + << std::endl + << " --help, -h" << std::endl + << " Display this helper message." << std::endl + << std::endl; + return EXIT_SUCCESS; + } + } + + vpColVector hsv_values; + if (vpColVector::loadYAML(opt_hsv_filename, hsv_values)) { + std::cout << "Load HSV threshold values from " << opt_hsv_filename << std::endl; + std::cout << "HSV low/high values: " << hsv_values.t() << std::endl; + } + else { + std::cout << "Warning: unable to load HSV thresholds values from " << opt_hsv_filename << std::endl; + return EXIT_FAILURE; + } + + int width = 848, height = 480, fps = 60; + vpRealSense2 rs; + rs2::config config; + config.enable_stream(RS2_STREAM_COLOR, width, height, RS2_FORMAT_RGBA8, fps); + config.disable_stream(RS2_STREAM_DEPTH); + config.disable_stream(RS2_STREAM_INFRARED, 1); + config.disable_stream(RS2_STREAM_INFRARED, 2); + rs.open(config); + + vpImage Ic(height, width); + vpImage H(height, width); + vpImage S(height, width); + vpImage V(height, width); + vpImage Ic_segmented(height, width, 0); + + vpDisplayX d_Ic(Ic, 0, 0, "Current frame"); + vpDisplayX d_Ic_segmented(Ic_segmented, Ic.getWidth()+75, 0, "HSV segmented frame"); + + bool quit = false; + double loop_time = 0., total_loop_time = 0.; + long nb_iter = 0; + + while (!quit) { + double t = vpTime::measureTimeMs(); + rs.acquire(Ic); + vpImageConvert::RGBaToHSV(reinterpret_cast(Ic.bitmap), + reinterpret_cast(H.bitmap), + reinterpret_cast(S.bitmap), + reinterpret_cast(V.bitmap), Ic.getSize()); + + vpImageTools::inRange(reinterpret_cast(H.bitmap), + reinterpret_cast(S.bitmap), + reinterpret_cast(V.bitmap), + hsv_values, + reinterpret_cast(Ic_segmented.bitmap), + Ic_segmented.getSize()); + + vpDisplay::display(Ic); + vpDisplay::display(Ic_segmented); + vpDisplay::displayText(Ic, 20, 20, "Click to quit...", vpColor::red); + + if (vpDisplay::getClick(Ic, false)) { + quit = true; + } + + vpDisplay::flush(Ic); + vpDisplay::flush(Ic_segmented); + nb_iter++; + loop_time = vpTime::measureTimeMs() - t; + total_loop_time += loop_time; + } + + + std::cout << "Mean loop time: " << total_loop_time / nb_iter << std::endl; + return EXIT_SUCCESS; +} +#else +int main() +{ +#if !defined(VISP_HAVE_REALSENSE2) + std::cout << "This tutorial needs librealsense as 3rd party." << std::endl; +#endif + + std::cout << "Install missing 3rd party, configure and rebuild ViSP." << std::endl; + return EXIT_SUCCESS; +} +#endif diff --git a/tutorial/segmentation/color/tutorial-hsv-tuner.cpp b/tutorial/segmentation/color/tutorial-hsv-tuner.cpp new file mode 100644 index 0000000000..07ff2c7077 --- /dev/null +++ b/tutorial/segmentation/color/tutorial-hsv-tuner.cpp @@ -0,0 +1,277 @@ +//! \example tutorial-hsv-tuner.cpp + +#include + +#include + +#if defined(VISP_HAVE_REALSENSE2) && defined(HAVE_OPENCV_HIGHGUI) +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +std::vector hsv_values_trackbar(6); +const cv::String window_detection_name = "Object Detection"; + +void set_trackbar_H_min(int val) +{ + cv::setTrackbarPos("Low H", window_detection_name, val); +} +void set_trackbar_H_max(int val) +{ + cv::setTrackbarPos("High H", window_detection_name, val); +} +void set_trackbar_S_min(int val) +{ + cv::setTrackbarPos("Low S", window_detection_name, val); +} +void set_trackbar_S_max(int val) +{ + cv::setTrackbarPos("High S", window_detection_name, val); +} +void set_trackbar_V_min(int val) +{ + cv::setTrackbarPos("Low V", window_detection_name, val); +} +void set_trackbar_V_max(int val) +{ + cv::setTrackbarPos("High V", window_detection_name, val); +} +static void on_low_H_thresh_trackbar(int, void *) +{ + hsv_values_trackbar[0] = std::min(hsv_values_trackbar[1]-1, hsv_values_trackbar[0]); + set_trackbar_H_min(hsv_values_trackbar[0]); +} +static void on_high_H_thresh_trackbar(int, void *) +{ + hsv_values_trackbar[1] = std::max(hsv_values_trackbar[1], hsv_values_trackbar[0]+1); + set_trackbar_H_max(hsv_values_trackbar[1]); +} +static void on_low_S_thresh_trackbar(int, void *) +{ + hsv_values_trackbar[2] = std::min(hsv_values_trackbar[3]-1, hsv_values_trackbar[2]); + set_trackbar_S_min(hsv_values_trackbar[2]); +} +static void on_high_S_thresh_trackbar(int, void *) +{ + hsv_values_trackbar[3] = std::max(hsv_values_trackbar[3], hsv_values_trackbar[2]+1); + set_trackbar_S_max(hsv_values_trackbar[3]); +} +static void on_low_V_thresh_trackbar(int, void *) +{ + hsv_values_trackbar[4] = std::min(hsv_values_trackbar[5]-1, hsv_values_trackbar[4]); + set_trackbar_V_min(hsv_values_trackbar[4]); +} +static void on_high_V_thresh_trackbar(int, void *) +{ + hsv_values_trackbar[5] = std::max(hsv_values_trackbar[5], hsv_values_trackbar[4]+1); + set_trackbar_V_max(hsv_values_trackbar[5]); +} + +int main(int argc, char *argv[]) +{ + bool opt_save_img = false; + std::string opt_hsv_filename = "calib/hsv-thresholds.yml"; + bool show_helper = false; + for (int i = 1; i < argc; i++) { + if (std::string(argv[i]) == "--hsv-thresholds") { + if ((i+1) < argc) { + opt_hsv_filename = std::string(argv[++i]); + } + else { + show_helper = true; + std::cout << "ERROR \nMissing value after parameter " << std::string(argv[i]) << std::endl; + } + } + else if (std::string(argv[i]) == "--save-img") { + opt_save_img = true; + } + if (show_helper || std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") { + std::cout << "\nSYNOPSIS " << std::endl + << argv[0] + << " [--hsv-thresholds ]" + << " [--save-img]" + << " [--help,-h]" + << std::endl; + std::cout << "\nOPTIONS " << std::endl + << " --hsv-thresholds " << std::endl + << " Name of the output filename that will contain HSV low/high thresholds." << std::endl + << " Default: " << opt_hsv_filename << std::endl + << std::endl + << " --save-img" << std::endl + << " Enable RGB, HSV and segmented image saving" << std::endl + << std::endl + << " --help, -h" << std::endl + << " Display this helper message." << std::endl + << std::endl; + return EXIT_SUCCESS; + } + } + + int max_value_H = 255; + int max_value = 255; + + hsv_values_trackbar[0] = 0; // Low H + hsv_values_trackbar[1] = max_value_H; // High H + hsv_values_trackbar[2] = 0; // Low S + hsv_values_trackbar[3] = max_value; // High S + hsv_values_trackbar[4] = 0; // Low V + hsv_values_trackbar[5] = max_value; // High V + + int width = 848, height = 480, fps = 60; + vpRealSense2 rs; + rs2::config config; + config.enable_stream(RS2_STREAM_COLOR, width, height, RS2_FORMAT_RGBA8, fps); + config.disable_stream(RS2_STREAM_DEPTH); + config.disable_stream(RS2_STREAM_INFRARED, 1); + config.disable_stream(RS2_STREAM_INFRARED, 2); + rs.open(config); + + cv::namedWindow(window_detection_name); + + vpArray2D hsv_values(hsv_values_trackbar), hsv_values_prev; + if (vpArray2D::loadYAML(opt_hsv_filename, hsv_values)) { + std::cout << "Load hsv values from " << opt_hsv_filename << " previous tuning " << std::endl; + std::cout << hsv_values.t() << std::endl; + hsv_values_prev = hsv_values; + for (size_t i = 0; i < hsv_values.size(); ++i) { + hsv_values_trackbar[i] = hsv_values.data[i]; + } + } + + // Trackbars to set thresholds for HSV values + cv::createTrackbar("Low H", window_detection_name, &hsv_values_trackbar[0], max_value_H, on_low_H_thresh_trackbar); + cv::createTrackbar("High H", window_detection_name, &hsv_values_trackbar[1], max_value_H, on_high_H_thresh_trackbar); + cv::createTrackbar("Low S", window_detection_name, &hsv_values_trackbar[2], max_value, on_low_S_thresh_trackbar); + cv::createTrackbar("High S", window_detection_name, &hsv_values_trackbar[3], max_value, on_high_S_thresh_trackbar); + cv::createTrackbar("Low V", window_detection_name, &hsv_values_trackbar[4], max_value, on_low_V_thresh_trackbar); + cv::createTrackbar("High V", window_detection_name, &hsv_values_trackbar[5], max_value, on_high_V_thresh_trackbar); + + vpImage Ic(height, width); + vpImage H(height, width); + vpImage S(height, width); + vpImage V(height, width); + vpImage Ic_segmented(height, width, 0); + + vpDisplayX d_Ic(Ic, 0, 0, "Current frame"); + vpDisplayX d_Ic_segmented(Ic_segmented, Ic.getWidth()+75, 0, "HSV segmented frame"); + bool quit = false; + + while (!quit) { + rs.acquire(Ic); + vpImageConvert::RGBaToHSV(reinterpret_cast(Ic.bitmap), + reinterpret_cast(H.bitmap), + reinterpret_cast(S.bitmap), + reinterpret_cast(V.bitmap), Ic.getSize()); + + vpImageTools::inRange(reinterpret_cast(H.bitmap), + reinterpret_cast(S.bitmap), + reinterpret_cast(V.bitmap), + hsv_values_trackbar, + reinterpret_cast(Ic_segmented.bitmap), + Ic_segmented.getSize()); + + vpDisplay::display(Ic); + vpDisplay::display(Ic_segmented); + vpDisplay::displayText(Ic, 20, 20, "Left click to learn HSV value...", vpColor::red); + vpDisplay::displayText(Ic, 40, 20, "Middle click to get HSV value...", vpColor::red); + vpDisplay::displayText(Ic, 60, 20, "Right click to quit...", vpColor::red); + vpImagePoint ip; + vpMouseButton::vpMouseButtonType button; + if (vpDisplay::getClick(Ic, ip, button, false)) { + if (button == vpMouseButton::button3) { + quit = true; + } + else if (button == vpMouseButton::button2) { + unsigned int i = ip.get_i(); + unsigned int j = ip.get_j(); + int h = static_cast(H[i][j]); + int s = static_cast(S[i][j]); + int v = static_cast(V[i][j]); + std::cout << "RGB[" << i << "][" << j << "]: " << static_cast(Ic[i][j].R) << " " << static_cast(Ic[i][j].G) + << " " << static_cast(Ic[i][j].B) << " -> HSV: " << h << " " << s << " " << v << std::endl; + } + else if (button == vpMouseButton::button1) { + unsigned int i = ip.get_i(); + unsigned int j = ip.get_j(); + int h = static_cast(H[i][j]); + int s = static_cast(S[i][j]); + int v = static_cast(V[i][j]); + int offset = 30; + hsv_values_trackbar[0] = std::max(0, h - offset); + hsv_values_trackbar[1] = std::min(max_value_H, h + offset); + hsv_values_trackbar[2] = std::max(0, s - offset); + hsv_values_trackbar[3] = std::min(max_value, s + offset); + hsv_values_trackbar[4] = std::max(0, v - offset); + hsv_values_trackbar[5] = std::min(max_value, v + offset); + std::cout << "HSV learned: " << h << " " << s << " " << v << std::endl; + set_trackbar_H_min(hsv_values_trackbar[0]); + set_trackbar_H_max(hsv_values_trackbar[1]); + set_trackbar_S_min(hsv_values_trackbar[2]); + set_trackbar_S_max(hsv_values_trackbar[3]); + set_trackbar_V_min(hsv_values_trackbar[4]); + set_trackbar_V_max(hsv_values_trackbar[5]); + } + } + if (quit) { + std::string parent = vpIoTools::getParent(opt_hsv_filename); + if (vpIoTools::checkDirectory(parent) == false) { + std::cout << "Create directory: " << parent << std::endl; + vpIoTools::makeDirectory(parent); + } + + vpArray2D hsv_values_new(hsv_values_trackbar); + if (hsv_values_new != hsv_values_prev) { + if (vpIoTools::checkFilename(opt_hsv_filename)) { + std::string hsv_filename_backup(opt_hsv_filename + std::string(".previous")); + std::cout << "Create a backup of the previous calibration in " << hsv_filename_backup << std::endl; + vpIoTools::copy(opt_hsv_filename, hsv_filename_backup); + } + + std::cout << "Save new calibration in: " << opt_hsv_filename << std::endl; + std::string header = std::string("# File created ") + vpTime::getDateTime(); + vpArray2D::saveYAML(opt_hsv_filename, hsv_values_new, header.c_str()); + } + if (opt_save_img) { + std::string path_img(parent + "/images-visp"); + if (vpIoTools::checkDirectory(path_img) == false) { + std::cout << "Create directory: " << path_img << std::endl; + vpIoTools::makeDirectory(path_img); + } + + std::cout << "Save images in path_img folder..." << std::endl; + vpImage I_HSV; + vpImageConvert::merge(&H, &S, &V, nullptr, I_HSV); + vpImageIo::write(Ic, path_img + "/I.png"); + vpImageIo::write(I_HSV, path_img + "/I-HSV.png"); + vpImageIo::write(Ic_segmented, path_img + "/I-HSV-segmented.png"); + } + break; + } + vpDisplay::flush(Ic); + vpDisplay::flush(Ic_segmented); + cv::waitKey(10); // To display trackbar + } + return EXIT_SUCCESS; +} + +#else +int main() +{ +#if !defined(VISP_HAVE_REALSENSE2) + std::cout << "This tutorial needs librealsense as 3rd party." << std::endl; +#endif +#if !defined(HAVE_OPENCV_HIGHGUI) + std::cout << "This tutorial needs OpenCV highgui module as 3rd party." << std::endl; +#endif + std::cout << "Install missing 3rd parties, configure and rebuild ViSP." << std::endl; + return EXIT_SUCCESS; +} +#endif From 3f9479258923cd3803151c1e91e0f8318073b3cf Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Tue, 2 Apr 2024 17:35:19 +0200 Subject: [PATCH 14/32] Introduce vpDisplayPCL documentation --- modules/gui/include/visp3/gui/vpDisplayPCL.h | 23 +++++--- modules/gui/src/pointcloud/vpDisplayPCL.cpp | 57 ++++++++++++++++---- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/modules/gui/include/visp3/gui/vpDisplayPCL.h b/modules/gui/include/visp3/gui/vpDisplayPCL.h index 3937d86519..a025a3a938 100644 --- a/modules/gui/include/visp3/gui/vpDisplayPCL.h +++ b/modules/gui/include/visp3/gui/vpDisplayPCL.h @@ -39,26 +39,37 @@ #if defined(VISP_HAVE_PCL) && defined(VISP_HAVE_THREADS) #include +#include #include #include +/*! + \class vpDisplayPCL + \ingroup group_gui_plotter + This class enables real time plotting of 3D point clouds. It relies on the PCL library. + To see how to install PCL library, please refer to the \ref soft_tool_pcl section. +*/ class VISP_EXPORT vpDisplayPCL { public: - explicit vpDisplayPCL(); + vpDisplayPCL(); + vpDisplayPCL(unsigned int width, unsigned int height); + ~vpDisplayPCL(); - void flush(); - - void run(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud); - ; void setVerbose(bool verbose); + void startThread(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud); void stop(); private: + void run(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud); + bool m_stop; - bool m_flush_viewer; bool m_verbose; + std::thread m_thread; //!< Non-blocking display thread. + std::mutex m_mutex; + unsigned int m_width; + unsigned int m_height; }; #endif diff --git a/modules/gui/src/pointcloud/vpDisplayPCL.cpp b/modules/gui/src/pointcloud/vpDisplayPCL.cpp index 3637c96760..4598833412 100644 --- a/modules/gui/src/pointcloud/vpDisplayPCL.cpp +++ b/modules/gui/src/pointcloud/vpDisplayPCL.cpp @@ -37,32 +37,44 @@ #include -vpDisplayPCL::vpDisplayPCL() : m_stop(false), m_flush_viewer(false), m_verbose(false) { } +/*! + * Default constructor. + * By default, viewer size is set to 640 x 480. + */ +vpDisplayPCL::vpDisplayPCL() : m_stop(false), m_verbose(false), m_width(640), m_height(480) { } + +/*! + * Constructor able to initialize the display window size. + */ +vpDisplayPCL::vpDisplayPCL(unsigned int width, unsigned int height) : m_stop(false), m_verbose(false), m_width(width), m_height(height) { } -void vpDisplayPCL::flush() +/*! + * Destructor that stops and join the viewer thread if not already done. + */ +vpDisplayPCL::~vpDisplayPCL() { - m_flush_viewer = true; + stop(); } +/*! + * Loop that does the display of the point cloud. + * @param[inout] mutex : Shared mutex. + * @param[in] pointcloud : Point cloud to display. + */ void vpDisplayPCL::run(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud) { pcl::PointCloud::Ptr local_pointcloud(new pcl::PointCloud()); - - bool flush_viewer = false; pcl::visualization::PCLVisualizer::Ptr viewer(new pcl::visualization::PCLVisualizer("3D Viewer")); viewer->setBackgroundColor(0, 0, 0); viewer->initCameraParameters(); - viewer->setPosition(640 + 80, 480 + 80); + viewer->setPosition(m_width + 80, m_height + 80); viewer->setCameraPosition(0, 0, -0.25, 0, -1, 0); - viewer->setSize(640, 480); + viewer->setSize(m_width, m_height); while (!m_stop) { { std::lock_guard lock(mutex); - flush_viewer = m_flush_viewer; - m_flush_viewer = false; local_pointcloud = pointcloud->makeShared(); - } // If updatePointCloud fails, it means that the pcl was not previously known by the viewer @@ -80,11 +92,34 @@ void vpDisplayPCL::run(std::mutex &mutex, pcl::PointCloud::Ptr po } } +/*! + * Start the viewer thread able to display a point cloud. + * @param[inout] mutex : Shared mutex. + * @param[in] pointcloud : Point cloud to display. + */ +void vpDisplayPCL::startThread(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud) +{ + m_thread = std::thread(&vpDisplayPCL::run, this, std::ref(mutex), pointcloud); +} + +/*! + * Stop the viewer thread and join. + */ void vpDisplayPCL::stop() { - m_stop = true; + if (!m_stop) { + m_stop = true; + + if (m_thread.joinable()) { + m_thread.join(); + } + } } +/*! + * Enable/disable verbose mode. + * @param[in] verbose : When true verbose mode is enable. + */ void vpDisplayPCL::setVerbose(bool verbose) { m_verbose = verbose; From 0cd2c7803fb99887a5e6bd2910abdee9b25fadc2 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Tue, 2 Apr 2024 17:36:29 +0200 Subject: [PATCH 15/32] Improve color segmentation tutorials --- .../tutorial-hsv-segmentation-pcl-viewer.cpp | 29 ++++++++----------- .../color/tutorial-hsv-segmentation-pcl.cpp | 17 ++++++----- .../color/tutorial-hsv-segmentation.cpp | 12 ++++---- 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp index 687e1ea6ef..b84800a23c 100644 --- a/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp @@ -1,4 +1,4 @@ -//! \example tutorial-hsv-segmentation-pcl.cpp +//! \example tutorial-hsv-segmentation-pcl-viewer.cpp #include #include @@ -136,17 +136,16 @@ int main(int argc, char **argv) float depth_scale = rs.getDepthScale(); vpCameraParameters cam_depth = rs.getCameraParameters(RS2_STREAM_DEPTH, vpCameraParameters::perspectiveProjWithoutDistortion); - pcl::PointCloud::Ptr pointcloud = pcl::PointCloud::Ptr(new pcl::PointCloud); vpImage Ic(height, width); vpImage H(height, width); vpImage S(height, width); vpImage V(height, width); - vpImage Ic_segmented(height, width, 0); + vpImage Ic_segmented_mask(height, width, 0); vpImage depth_raw(height, width); vpDisplayX d_Ic(Ic, 0, 0, "Current frame"); - vpDisplayX d_Ic_segmented(Ic_segmented, Ic.getWidth()+75, 0, "HSV segmented frame"); + vpDisplayX d_Ic_segmented_mask(Ic_segmented_mask, Ic.getWidth()+75, 0, "HSV segmented frame"); bool quit = false; double loop_time = 0., total_loop_time = 0.; @@ -155,9 +154,11 @@ int main(int argc, char **argv) float Z_max = 2.5; int pcl_size = 0; - vpDisplayPCL pcl_viewer; + pcl::PointCloud::Ptr pointcloud = pcl::PointCloud::Ptr(new pcl::PointCloud); + std::mutex pcl_viewer_mutex; - std::thread pcl_viewer_thread(&vpDisplayPCL::run, &pcl_viewer, std::ref(pcl_viewer_mutex), pointcloud); + vpDisplayPCL pcl_viewer; + pcl_viewer.startThread(std::ref(pcl_viewer_mutex), pointcloud); while (!quit) { double t = vpTime::measureTimeMs(); @@ -171,20 +172,19 @@ int main(int argc, char **argv) reinterpret_cast(S.bitmap), reinterpret_cast(V.bitmap), hsv_values, - reinterpret_cast(Ic_segmented.bitmap), - Ic_segmented.getSize()); + reinterpret_cast(Ic_segmented_mask.bitmap), + Ic_segmented_mask.getSize()); { std::lock_guard lock(pcl_viewer_mutex); - vpImageConvert::depthToPointCloud(depth_raw, depth_scale, cam_depth, pointcloud, &Ic_segmented, Z_min, Z_max); + vpImageConvert::depthToPointCloud(depth_raw, depth_scale, cam_depth, pointcloud, &Ic_segmented_mask, Z_min, Z_max); pcl_size = pointcloud->size(); - pcl_viewer.flush(); } std::cout << "Segmented point cloud size: " << pcl_size << std::endl; vpDisplay::display(Ic); - vpDisplay::display(Ic_segmented); + vpDisplay::display(Ic_segmented_mask); vpDisplay::displayText(Ic, 20, 20, "Click to quit...", vpColor::red); if (vpDisplay::getClick(Ic, false)) { @@ -192,17 +192,12 @@ int main(int argc, char **argv) } vpDisplay::flush(Ic); - vpDisplay::flush(Ic_segmented); + vpDisplay::flush(Ic_segmented_mask); nb_iter++; loop_time = vpTime::measureTimeMs() - t; total_loop_time += loop_time; } - pcl_viewer.stop(); - - if (pcl_viewer_thread.joinable()) { - pcl_viewer_thread.join(); - } std::cout << "Mean loop time: " << total_loop_time / nb_iter << std::endl; return EXIT_SUCCESS; } diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp index 02364500a8..e993ab03d1 100644 --- a/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp @@ -71,17 +71,16 @@ int main(int argc, char **argv) float depth_scale = rs.getDepthScale(); vpCameraParameters cam_depth = rs.getCameraParameters(RS2_STREAM_DEPTH, vpCameraParameters::perspectiveProjWithoutDistortion); - pcl::PointCloud::Ptr pointcloud = pcl::PointCloud::Ptr(new pcl::PointCloud); vpImage Ic(height, width); vpImage H(height, width); vpImage S(height, width); vpImage V(height, width); - vpImage Ic_segmented(height, width, 0); + vpImage Ic_segmented_mask(height, width, 0); vpImage depth_raw(height, width); vpDisplayX d_Ic(Ic, 0, 0, "Current frame"); - vpDisplayX d_Ic_segmented(Ic_segmented, Ic.getWidth()+75, 0, "HSV segmented frame"); + vpDisplayX d_Ic_segmented_mask(Ic_segmented_mask, Ic.getWidth()+75, 0, "HSV segmented frame"); bool quit = false; double loop_time = 0., total_loop_time = 0.; @@ -90,6 +89,8 @@ int main(int argc, char **argv) float Z_max = 2.5; int pcl_size = 0; + pcl::PointCloud::Ptr pointcloud = pcl::PointCloud::Ptr(new pcl::PointCloud); + while (!quit) { double t = vpTime::measureTimeMs(); rs.acquire((unsigned char *)Ic.bitmap, (unsigned char *)(depth_raw.bitmap), NULL, NULL, &align_to); @@ -102,16 +103,16 @@ int main(int argc, char **argv) reinterpret_cast(S.bitmap), reinterpret_cast(V.bitmap), hsv_values, - reinterpret_cast(Ic_segmented.bitmap), - Ic_segmented.getSize()); + reinterpret_cast(Ic_segmented_mask.bitmap), + Ic_segmented_mask.getSize()); - vpImageConvert::depthToPointCloud(depth_raw, depth_scale, cam_depth, pointcloud, &Ic_segmented, Z_min, Z_max); + vpImageConvert::depthToPointCloud(depth_raw, depth_scale, cam_depth, pointcloud, &Ic_segmented_mask, Z_min, Z_max); pcl_size = pointcloud->size(); std::cout << "Segmented point cloud size: " << pcl_size << std::endl; vpDisplay::display(Ic); - vpDisplay::display(Ic_segmented); + vpDisplay::display(Ic_segmented_mask); vpDisplay::displayText(Ic, 20, 20, "Click to quit...", vpColor::red); if (vpDisplay::getClick(Ic, false)) { @@ -119,7 +120,7 @@ int main(int argc, char **argv) } vpDisplay::flush(Ic); - vpDisplay::flush(Ic_segmented); + vpDisplay::flush(Ic_segmented_mask); nb_iter++; loop_time = vpTime::measureTimeMs() - t; total_loop_time += loop_time; diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp index 798b9eec23..31ad53be7c 100644 --- a/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp @@ -70,10 +70,10 @@ int main(int argc, char **argv) vpImage H(height, width); vpImage S(height, width); vpImage V(height, width); - vpImage Ic_segmented(height, width, 0); + vpImage Ic_segmented_mask(height, width, 0); vpDisplayX d_Ic(Ic, 0, 0, "Current frame"); - vpDisplayX d_Ic_segmented(Ic_segmented, Ic.getWidth()+75, 0, "HSV segmented frame"); + vpDisplayX d_Ic_segmented_mask(Ic_segmented_mask, Ic.getWidth()+75, 0, "HSV segmented frame"); bool quit = false; double loop_time = 0., total_loop_time = 0.; @@ -91,11 +91,11 @@ int main(int argc, char **argv) reinterpret_cast(S.bitmap), reinterpret_cast(V.bitmap), hsv_values, - reinterpret_cast(Ic_segmented.bitmap), - Ic_segmented.getSize()); + reinterpret_cast(Ic_segmented_mask.bitmap), + Ic_segmented_mask.getSize()); vpDisplay::display(Ic); - vpDisplay::display(Ic_segmented); + vpDisplay::display(Ic_segmented_mask); vpDisplay::displayText(Ic, 20, 20, "Click to quit...", vpColor::red); if (vpDisplay::getClick(Ic, false)) { @@ -103,7 +103,7 @@ int main(int argc, char **argv) } vpDisplay::flush(Ic); - vpDisplay::flush(Ic_segmented); + vpDisplay::flush(Ic_segmented_mask); nb_iter++; loop_time = vpTime::measureTimeMs() - t; total_loop_time += loop_time; From 8225f5b5de0e1b2eca8fe3da4a892810f7c0bedc Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Tue, 2 Apr 2024 17:36:56 +0200 Subject: [PATCH 16/32] Update with last changes --- ChangeLog.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ChangeLog.txt b/ChangeLog.txt index c7aea44e05..efc07b6e24 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -16,6 +16,7 @@ ViSP 3.x.x (Version in development) . vpStatisticalTestAbstract, vpStatisticalTestEWMA, vpStatisticalTestHinkley, vpStatisticalTestMeanAdjustedCUSUM vpStatisticalTestShewhart and vpStatisticalTestSigma: classes implementing Statistical Control Process methods to detect mean drift / jump of a signal + . vpDisplayPCL to display a point cloud using PCL 3rdparty - Deprecated . vpPlanarObjectDetector, vpFernClassifier deprecated classes are removed . End of supporting c++98 standard. As a consequence, ViSP is no more compatible with Ubuntu 12.04 @@ -31,6 +32,10 @@ ViSP 3.x.x (Version in development) . Speed up build by including only opencv2/opencv_modules.hpp instead of opencv2/opencv.hpp header in vpConfig.h . In imgproc module, implementation of automatic gamma factor computation methods for gamma correction. . Eliminate the use of pthread in favour of std::thread + . RGB or RGBa to/from HSV conversion optimization in vpImageConvert class + . New vpImageTools::inRange() functions to ease binary mask computation by thresholding HSV channels + . New tutorials in tutorial/segmentation/color folder to show how to use HSV color segmentation to + extract the corresponding point cloud - Applications . Migrate eye-to-hand tutorials in apps - Tutorials From 4d24f0e8c62a35e48076769f0ced564c025f7f05 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Wed, 3 Apr 2024 08:23:22 +0200 Subject: [PATCH 17/32] Fix build under windows when OpenMP is enabled error C3016: 'i': index variable in OpenMP 'for' statement must have signed integral type --- modules/core/src/image/vpImageTools.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/core/src/image/vpImageTools.cpp b/modules/core/src/image/vpImageTools.cpp index 2d9974dc8d..e98471b2e6 100644 --- a/modules/core/src/image/vpImageTools.cpp +++ b/modules/core/src/image/vpImageTools.cpp @@ -1013,10 +1013,11 @@ void vpImageTools::inRange(const unsigned char *hue, const unsigned char *satura unsigned char s_high = static_cast(hsv_values[3]); unsigned char v_low = static_cast(hsv_values[4]); unsigned char v_high = static_cast(hsv_values[5]); + int size_ = static_cast(size); #if defined(_OPENMP) #pragma omp parallel for #endif - for (unsigned int i = 0; i < size; ++i) { + for (int i = 0; i < size_; ++i) { if ((h_low <= hue[i]) && (hue[i] <= h_high) && (s_low <= saturation[i]) && (saturation[i] <= s_high) && (v_low <= value[i]) && (value[i] <= v_high)) { @@ -1061,10 +1062,11 @@ void vpImageTools::inRange(const unsigned char *hue, const unsigned char *satura unsigned char s_high = static_cast(hsv_values[3]); unsigned char v_low = static_cast(hsv_values[4]); unsigned char v_high = static_cast(hsv_values[5]); + int size_ = static_cast(size); #if defined(_OPENMP) #pragma omp parallel for #endif - for (unsigned int i = 0; i < size; ++i) { + for (int i = 0; i < size_; ++i) { if ((h_low <= hue[i]) && (hue[i] <= h_high) && (s_low <= saturation[i]) && (saturation[i] <= s_high) && (v_low <= value[i]) && (value[i] <= v_high)) { From ecc0cf931728ce833891f16a9d91492b89eb3106 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Wed, 3 Apr 2024 08:24:19 +0200 Subject: [PATCH 18/32] Fix warning in vpArray2D detected on windows warning C4267: 'argument': conversion from 'size_t' to 'unsigned int', possible loss of data --- modules/core/include/visp3/core/vpArray2D.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/core/include/visp3/core/vpArray2D.h b/modules/core/include/visp3/core/vpArray2D.h index 1bb011dd69..753444f346 100644 --- a/modules/core/include/visp3/core/vpArray2D.h +++ b/modules/core/include/visp3/core/vpArray2D.h @@ -223,16 +223,16 @@ template class vpArray2D resize(r, c, false, false); } else if (c == 0) { - resize(vec.size(), 1, false, false); + resize(static_cast(vec.size()), 1, false, false); } else if (r == 0) { - resize(1, vec.size(), false, false); + resize(1, static_cast(vec.size()), false, false); } -// #if ((__cplusplus >= 201103L) || (defined(_MSVC_LANG) && (_MSVC_LANG >= 201103L))) // Check if cxx11 or higher -// std::copy(vec.begin(), vec.end(), data); -// #else +#if ((__cplusplus >= 201103L) || (defined(_MSVC_LANG) && (_MSVC_LANG >= 201103L))) // Check if cxx11 or higher + std::copy(vec.begin(), vec.end(), data); +#else memcpy(data, vec.data(), vec.size() * sizeof(Type)); -// #endif +#endif } #if ((__cplusplus >= 201103L) || (defined(_MSVC_LANG) && (_MSVC_LANG >= 201103L))) // Check if cxx11 or higher From 648e96bc04ce9c739a8e6395dd5575f058ac816f Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Wed, 3 Apr 2024 09:08:54 +0200 Subject: [PATCH 19/32] Fix build when c++98 is enabled --- modules/sensor/test/force-torque/testComedi.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/modules/sensor/test/force-torque/testComedi.cpp b/modules/sensor/test/force-torque/testComedi.cpp index 99d851d17f..09c62c09b1 100644 --- a/modules/sensor/test/force-torque/testComedi.cpp +++ b/modules/sensor/test/force-torque/testComedi.cpp @@ -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 @@ -30,13 +30,12 @@ * * Description: * Test force/torque ATI sensor. - * -*****************************************************************************/ + */ /*! \example testComedi.cpp This example shows how to retrieve data from a sensor connected to a DAQ - board. He we have 1 signe main threads that acquires physical values at 100 + board. Here we have 1 single main threads that acquires physical values at 100 Hz (10 ms) and records data in recorded-physical-data-sync.txt file. */ @@ -58,9 +57,11 @@ int main() vpPlot scope(1, 700, 700, 100, 200, std::string("ATI physical sensor data (") + comedi.getPhyDataUnits() + std::string(")")); scope.initGraph(0, comedi.getNChannel()); +#if (VISP_CXX_STANDARD >= VISP_CXX_STANDARD_11) for (unsigned int i = 0; i < comedi.getNChannel(); i++) { scope.setLegend(0, i, "G" + ((std::ostringstream() << i)).str()); } +#endif #endif std::string file("recorded-physical-data-sync.txt"); From 99514383dbe23e6bc1dba0e5b30eadd3c16f2247 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Wed, 3 Apr 2024 15:06:38 +0200 Subject: [PATCH 20/32] Introduce vpImageTools::inMask() to create an image from a mask Modify inRange() to return the number of pixels in the HSV ranges --- .../core/include/visp3/core/vpImageTools.h | 11 +- modules/core/src/image/vpImageTools.cpp | 120 ++++++++++++++---- 2 files changed, 103 insertions(+), 28 deletions(-) diff --git a/modules/core/include/visp3/core/vpImageTools.h b/modules/core/include/visp3/core/vpImageTools.h index ed72766a32..3e20d04e77 100644 --- a/modules/core/include/visp3/core/vpImageTools.h +++ b/modules/core/include/visp3/core/vpImageTools.h @@ -126,10 +126,13 @@ class VISP_EXPORT vpImageTools static void imageSubtract(const vpImage &I1, const vpImage &I2, vpImage &Ires, bool saturate = false); - static void inRange(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, - const vpColVector &hsv_values, unsigned char *mask, unsigned int size); - static void inRange(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, - const std::vector &hsv_values, unsigned char *mask, unsigned int size); + static int inMask(const vpImage &I, const vpImage &mask, vpImage &I_mask); + static int inMask(const vpImage &I, const vpImage &mask, vpImage &I_mask); + + static int inRange(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, + const vpColVector &hsv_range, unsigned char *mask, unsigned int size); + static int inRange(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, + const std::vector &hsv_range, unsigned char *mask, unsigned int size); static void initUndistortMap(const vpCameraParameters &cam, unsigned int width, unsigned int height, vpArray2D &mapU, vpArray2D &mapV, vpArray2D &mapDu, vpArray2D &mapDv); diff --git a/modules/core/src/image/vpImageTools.cpp b/modules/core/src/image/vpImageTools.cpp index e98471b2e6..1853b89e7a 100644 --- a/modules/core/src/image/vpImageTools.cpp +++ b/modules/core/src/image/vpImageTools.cpp @@ -980,12 +980,77 @@ bool vpImageTools::checkFixedPoint(unsigned int x, unsigned int y, const vpMatri return (vpMath::abs(x2) < limit) && (vpMath::abs(y2) < limit); } +/*! + * Keep the part of an image that is in the mask. + * @param[in] I : Input image. + * @param[in] mask : Mask where pixels to consider have values that differ from 0. + * @param[out] I_mask : Resulting image where pixels that are in the mask are kept. + * @return The number of pixels that are in the mask. + */ +int vpImageTools::inMask(const vpImage &I, const vpImage &mask, vpImage &I_mask) +{ + if ((I.getHeight() != mask.getHeight()) || (I.getWidth() != mask.getWidth())) { + throw(vpImageException(vpImageException::incorrectInitializationError, + "Error in vpImageTools::inMask(): image (%dx%d) and mask (%dx%d) size doesn't match", + I.getWidth(), I.getHeight(), mask.getWidth(), mask.getHeight())); + } + vpRGBa black(0, 0, 0); + I_mask.resize(I.getHeight(), I.getWidth()); + int cpt_in_mask = 0; + int size_ = static_cast(I.getSize()); +#if defined(_OPENMP) +#pragma omp parallel for reduction(+:cpt_in_mask) +#endif + for (int i = 0; i < size_; ++i) { + if (mask.bitmap[i] == 0) { + I_mask.bitmap[i] = black; + } + else { + I_mask.bitmap[i] = I.bitmap[i]; + ++cpt_in_mask; + } + } + return cpt_in_mask; +} + +/*! + * Keep the part of an image that is in the mask. + * @param[in] I : Input image. + * @param[in] mask : Mask where pixels to consider have values that differ from 0. + * @param[out] I_mask : Resulting image where pixels that are in the mask are kept. + * @return The number of pixels that are in the mask. + */ +int vpImageTools::inMask(const vpImage &I, const vpImage &mask, vpImage &I_mask) +{ + if ((I.getHeight() != mask.getHeight()) || (I.getWidth() != mask.getWidth())) { + throw(vpImageException(vpImageException::incorrectInitializationError, + "Error in vpImageTools::inMask(): image (%dx%d) and mask (%dx%d) size doesn't match", + I.getWidth(), I.getHeight(), mask.getWidth(), mask.getHeight())); + } + I_mask.resize(I.getHeight(), I.getWidth()); + int cpt_in_mask = 0; + int size_ = static_cast(I.getSize()); +#if defined(_OPENMP) +#pragma omp parallel for reduction(+:cpt_in_mask) +#endif + for (int i = 0; i < size_; ++i) { + if (mask.bitmap[i] == 0) { + I_mask.bitmap[i] = 0; + } + else { + I_mask.bitmap[i] = I.bitmap[i]; + ++cpt_in_mask; + } + } + return cpt_in_mask; +} + /*! * Create binary mask by checking if HSV (hue, saturation, value) channels lie between low and high HSV thresholds. * \param[in] hue : Pointer to an array of hue values. Its dimension is equal to the `size` parameter. * \param[in] saturation : Pointer to an array of saturation values. Its dimension is equal to the `size` parameter. * \param[in] value : Pointer to an array of values. Its dimension is equal to the `size` parameter. - * \param[in] hsv_values : 6-dim vector that contains the low/high threshold values for each HSV channel respectively. + * \param[in] hsv_range : 6-dim vector that contains the low/high range values for each HSV channel respectively. * Each element of this vector should be in [0,255] range. Note that there is also tutorial-hsv-tuner.cpp that may help * to determine low/high HSV values. * \param[out] mask : Pointer to a resulting mask of dimension `size`. When HSV value is in the boundaries, the mask @@ -996,37 +1061,40 @@ bool vpImageTools::checkFixedPoint(unsigned int x, unsigned int y, const vpMatri * \sa vpImageConvert::RGBToHSV(const unsigned char *, unsigned char *, unsigned char *, unsigned char *, unsigned int, bool) * \sa vpImageConvert::RGBaToHSV(const unsigned char *, unsigned char *, unsigned char *, unsigned char *, unsigned int, bool) */ -void vpImageTools::inRange(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, - const vpColVector &hsv_values, unsigned char *mask, unsigned int size) +int vpImageTools::inRange(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, + const vpColVector &hsv_range, unsigned char *mask, unsigned int size) { if ((hue == nullptr) || (saturation == nullptr) || (value == nullptr)) { throw(vpImageException(vpImageException::notInitializedError, "Error in vpImageTools::inRange(): hsv pointer are empty")); } - else if (hsv_values.size() != 6) { + else if (hsv_range.size() != 6) { throw(vpImageException(vpImageException::notInitializedError, - "Error in vpImageTools::inRange(): wrong values vector size (%d)", hsv_values.size())); + "Error in vpImageTools::inRange(): wrong values vector size (%d)", hsv_range.size())); } - unsigned char h_low = static_cast(hsv_values[0]); - unsigned char h_high = static_cast(hsv_values[1]); - unsigned char s_low = static_cast(hsv_values[2]); - unsigned char s_high = static_cast(hsv_values[3]); - unsigned char v_low = static_cast(hsv_values[4]); - unsigned char v_high = static_cast(hsv_values[5]); + unsigned char h_low = static_cast(hsv_range[0]); + unsigned char h_high = static_cast(hsv_range[1]); + unsigned char s_low = static_cast(hsv_range[2]); + unsigned char s_high = static_cast(hsv_range[3]); + unsigned char v_low = static_cast(hsv_range[4]); + unsigned char v_high = static_cast(hsv_range[5]); int size_ = static_cast(size); + int cpt_in_range = 0; #if defined(_OPENMP) -#pragma omp parallel for +#pragma omp parallel for reduction(+:cpt_in_range) #endif for (int i = 0; i < size_; ++i) { if ((h_low <= hue[i]) && (hue[i] <= h_high) && (s_low <= saturation[i]) && (saturation[i] <= s_high) && (v_low <= value[i]) && (value[i] <= v_high)) { mask[i] = 255; + ++cpt_in_range; } else { mask[i] = 0; } } + return cpt_in_range; } /*! @@ -1034,46 +1102,50 @@ void vpImageTools::inRange(const unsigned char *hue, const unsigned char *satura * \param[in] hue : Pointer to an array of hue values. Its dimension is equal to the `size` parameter. * \param[in] saturation : Pointer to an array of saturation values. Its dimension is equal to the `size` parameter. * \param[in] value : Pointer to an array of values. Its dimension is equal to the `size` parameter. - * \param[in] hsv_values : 6-dim vector that contains the low/high threshold values for each HSV channel respectively. + * \param[in] hsv_range : 6-dim vector that contains the low/high range values for each HSV channel respectively. * Each element of this vector should be in [0,255] range. Note that there is also tutorial-hsv-tuner.cpp that may help * to determine low/high HSV values. * \param[out] mask : Pointer to a resulting mask of dimension `size`. When HSV value is in the boundaries, the mask * element is set to 255, otherwise to 0. The mask should be allocated prior calling this function. Its dimension * is equal to the `size` parameter. * \param[in] size : Size of `hue`, `saturation`, `value` and `mask` arrays. + * \return The number of pixels that are in the HSV range. * * \sa vpImageConvert::RGBToHSV(const unsigned char *, unsigned char *, unsigned char *, unsigned char *, unsigned int, bool) * \sa vpImageConvert::RGBaToHSV(const unsigned char *, unsigned char *, unsigned char *, unsigned char *, unsigned int, bool) */ -void vpImageTools::inRange(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, - const std::vector &hsv_values, unsigned char *mask, unsigned int size) +int vpImageTools::inRange(const unsigned char *hue, const unsigned char *saturation, const unsigned char *value, + const std::vector &hsv_range, unsigned char *mask, unsigned int size) { if ((hue == nullptr) || (saturation == nullptr) || (value == nullptr)) { throw(vpImageException(vpImageException::notInitializedError, "Error in vpImageTools::inRange(): hsv pointer are empty")); } - else if (hsv_values.size() != 6) { + else if (hsv_range.size() != 6) { throw(vpImageException(vpImageException::notInitializedError, - "Error in vpImageTools::inRange(): wrong values vector size (%d)", hsv_values.size())); + "Error in vpImageTools::inRange(): wrong values vector size (%d)", hsv_range.size())); } - unsigned char h_low = static_cast(hsv_values[0]); - unsigned char h_high = static_cast(hsv_values[1]); - unsigned char s_low = static_cast(hsv_values[2]); - unsigned char s_high = static_cast(hsv_values[3]); - unsigned char v_low = static_cast(hsv_values[4]); - unsigned char v_high = static_cast(hsv_values[5]); + unsigned char h_low = static_cast(hsv_range[0]); + unsigned char h_high = static_cast(hsv_range[1]); + unsigned char s_low = static_cast(hsv_range[2]); + unsigned char s_high = static_cast(hsv_range[3]); + unsigned char v_low = static_cast(hsv_range[4]); + unsigned char v_high = static_cast(hsv_range[5]); int size_ = static_cast(size); + int cpt_in_range = 0; #if defined(_OPENMP) -#pragma omp parallel for +#pragma omp parallel for reduction(+:cpt_in_range) #endif for (int i = 0; i < size_; ++i) { if ((h_low <= hue[i]) && (hue[i] <= h_high) && (s_low <= saturation[i]) && (saturation[i] <= s_high) && (v_low <= value[i]) && (value[i] <= v_high)) { mask[i] = 255; + ++cpt_in_range; } else { mask[i] = 0; } } + return cpt_in_range; } From 3555dbc29ae317212e52daf7cab04e4f740afdbe Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Wed, 3 Apr 2024 15:08:48 +0200 Subject: [PATCH 21/32] Rename tuner tutorial and introduce a new tutorial to explain the HSV segmentation basics --- tutorial/segmentation/color/CMakeLists.txt | 10 +++- tutorial/segmentation/color/ballons.jpg | Bin 0 -> 22516 bytes ...tuner.cpp => tutorial-hsv-range-tuner.cpp} | 19 ++++--- .../color/tutorial-hsv-segmentation-basic.cpp | 52 ++++++++++++++++++ 4 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 tutorial/segmentation/color/ballons.jpg rename tutorial/segmentation/color/{tutorial-hsv-tuner.cpp => tutorial-hsv-range-tuner.cpp} (94%) create mode 100644 tutorial/segmentation/color/tutorial-hsv-segmentation-basic.cpp diff --git a/tutorial/segmentation/color/CMakeLists.txt b/tutorial/segmentation/color/CMakeLists.txt index d2081ab26a..2d8f4b04d4 100644 --- a/tutorial/segmentation/color/CMakeLists.txt +++ b/tutorial/segmentation/color/CMakeLists.txt @@ -6,15 +6,23 @@ find_package(VISP REQUIRED visp_core visp_sensor visp_io visp_gui) # set the list of source files set(tutorial_cpp + tutorial-hsv-range-tuner.cpp tutorial-hsv-segmentation.cpp + tutorial-hsv-segmentation-basic.cpp tutorial-hsv-segmentation-pcl.cpp tutorial-hsv-segmentation-pcl-viewer.cpp - tutorial-hsv-tuner.cpp ) +file(GLOB tutorial_data "*.jpg") + foreach(cpp ${tutorial_cpp}) visp_add_target(${cpp}) if(COMMAND visp_add_dependency) visp_add_dependency(${cpp} "tutorials") endif() endforeach() + +# Copy the data files to the same location than the target +foreach(data ${tutorial_data}) + visp_copy_data(tutorial-hsv-segmentation-basic.cpp ${data}) +endforeach() diff --git a/tutorial/segmentation/color/ballons.jpg b/tutorial/segmentation/color/ballons.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9cc0c01f600589d204302697fd08bf15a6b68054 GIT binary patch literal 22516 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 literal 0 HcmV?d00001 diff --git a/tutorial/segmentation/color/tutorial-hsv-tuner.cpp b/tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp similarity index 94% rename from tutorial/segmentation/color/tutorial-hsv-tuner.cpp rename to tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp index 07ff2c7077..e3097db390 100644 --- a/tutorial/segmentation/color/tutorial-hsv-tuner.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp @@ -1,4 +1,4 @@ -//! \example tutorial-hsv-tuner.cpp +//! \example tutorial-hsv-range-tuner.cpp #include @@ -158,10 +158,11 @@ int main(int argc, char *argv[]) vpImage H(height, width); vpImage S(height, width); vpImage V(height, width); - vpImage Ic_segmented(height, width, 0); + vpImage mask(height, width); + vpImage Ic_segmented(height, width); vpDisplayX d_Ic(Ic, 0, 0, "Current frame"); - vpDisplayX d_Ic_segmented(Ic_segmented, Ic.getWidth()+75, 0, "HSV segmented frame"); + vpDisplayX d_Ic_segmented(Ic_segmented, Ic.getWidth()+75, 0, "Segmented frame"); bool quit = false; while (!quit) { @@ -172,11 +173,13 @@ int main(int argc, char *argv[]) reinterpret_cast(V.bitmap), Ic.getSize()); vpImageTools::inRange(reinterpret_cast(H.bitmap), - reinterpret_cast(S.bitmap), - reinterpret_cast(V.bitmap), - hsv_values_trackbar, - reinterpret_cast(Ic_segmented.bitmap), - Ic_segmented.getSize()); + reinterpret_cast(S.bitmap), + reinterpret_cast(V.bitmap), + hsv_values_trackbar, + reinterpret_cast(mask.bitmap), + mask.getSize()); + + vpImageTools::inMask(Ic, mask, Ic_segmented); vpDisplay::display(Ic); vpDisplay::display(Ic_segmented); diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation-basic.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation-basic.cpp new file mode 100644 index 0000000000..c1ebbb399e --- /dev/null +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation-basic.cpp @@ -0,0 +1,52 @@ +#include +#include +#include +#include + +int main() +{ + vpImage I; + vpImageIo::read(I, "ballons.jpg"); + + unsigned int width = I.getWidth(); + unsigned int height = I.getHeight(); + + vpImage H(height, width); + vpImage S(height, width); + vpImage V(height, width); + + vpImageConvert::RGBaToHSV(reinterpret_cast(I.bitmap), + reinterpret_cast(H.bitmap), + reinterpret_cast(S.bitmap), + reinterpret_cast(V.bitmap), I.getSize()); + + int h = 14, s = 255, v = 209; + int offset = 30; + int h_low = std::max(0, h - offset), h_high = std::min(h + offset, 255); + int s_low = std::max(0, s - offset), s_high = std::min(s + offset, 255); + int v_low = std::max(0, v - offset), v_high = std::min(v + offset, 255); + std::vector hsv_range({ h_low, h_high, s_low, s_high, v_low, v_high }); + + vpImage mask(height, width); + vpImageTools::inRange(reinterpret_cast(H.bitmap), + reinterpret_cast(S.bitmap), + reinterpret_cast(V.bitmap), + hsv_range, + reinterpret_cast(mask.bitmap), + mask.getSize()); + + vpImage I_segmented(height, width); + vpImageTools::inMask(I, mask, I_segmented); + + vpDisplayX d_I(I, 0, 0, "Current frame"); + vpDisplayX d_mask(mask, I.getWidth()+75, 0, "HSV mask"); + vpDisplayX d_I_segmented(I_segmented, 2*mask.getWidth()+80, 0, "Segmented frame"); + + vpDisplay::display(I); + vpDisplay::display(mask); + vpDisplay::display(I_segmented); + vpDisplay::flush(I); + vpDisplay::flush(mask); + vpDisplay::flush(I_segmented); + vpDisplay::getClick(I); +} From 35245d25091292a8573abb8138914230b64947ad Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Wed, 3 Apr 2024 15:11:42 +0200 Subject: [PATCH 22/32] Introduce new doxygen tutorial for hsv segmentation --- ChangeLog.txt | 2 + .../segmentation/color/ballons-segmented.jpg | Bin 0 -> 43306 bytes .../color/tutorial-hsv-segmentation.dox | 128 ++++++++++++++++++ doc/tutorial/tutorial-users.dox | 10 ++ 4 files changed, 140 insertions(+) create mode 100644 doc/image/tutorial/segmentation/color/ballons-segmented.jpg create mode 100644 doc/tutorial/segmentation/color/tutorial-hsv-segmentation.dox diff --git a/ChangeLog.txt b/ChangeLog.txt index efc07b6e24..93b7a05c05 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -48,6 +48,8 @@ ViSP 3.x.x (Version in development) https://visp-doc.inria.fr/doxygen/visp-daily/tutorial-spc.html . New tutorial: Installing ViSP Python bindings https://visp-doc.inria.fr/doxygen/visp-daily/tutorial-install-python-bindings.html + . New tutorial: Color segmentation using HSV color scale + https://visp-doc.inria.fr/doxygen/visp-daily/tutorial-hsv-segmentation.html - Bug fixed . [#1251] Bug in vpDisplay::displayFrame() . [#1270] Build issue around std::clamp and optional header which are not found with cxx17 diff --git a/doc/image/tutorial/segmentation/color/ballons-segmented.jpg b/doc/image/tutorial/segmentation/color/ballons-segmented.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6b67203c3a49d39031f2919ce4def1d599b0fe03 GIT binary patch literal 43306 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 Paj>r!Y*CkB{C^Vw$pj<6 literal 0 HcmV?d00001 diff --git a/doc/tutorial/segmentation/color/tutorial-hsv-segmentation.dox b/doc/tutorial/segmentation/color/tutorial-hsv-segmentation.dox new file mode 100644 index 0000000000..bfb4660e18 --- /dev/null +++ b/doc/tutorial/segmentation/color/tutorial-hsv-segmentation.dox @@ -0,0 +1,128 @@ +/** + \page tutorial-hsv-segmentation Tutorial: Color segmentation using HSV color scale + \tableofcontents + +\section hsv_intro Introduction + +The HSV scale, which stands for Hue Saturation and Value, provides a numerical readout of your color image that +corresponds to the color names contained therein. Hue is measured in degrees from 0 to 360, while Saturation and Value +of a color are both analyzed on a scale of 0 to 100 percent. + +Hue, Saturation, and Value are the main color properties that allow us to distinguish between different colors. +In this tutorial, you will learn how to use HSV color scale to segment a specific color in an image. + +Note that all the material (source code and images) described in this tutorial is part of ViSP source code +(in `tutorial/segmentation/color` folder) and could be found in +https://github.com/lagadic/visp/tree/master/tutorial/segmentation/color. + +\section hsv_converter RGB to HSV color scale conversion + +In ViSP, color images can be read and converted to the RGB color scale. The RGB color scale is based on the color +theory that all visible colors can be obtained from the additive primary colors red, green and blue. In ViSP, +we introduce an additional Alpha channel to add color transparency. The RGB + Alpha channels are therefore +implemented in the vpRGBa class. The following snippet shows how to load a color image in ViSP: +\code +#include + +int main() +{ + vpImage I; + vpImageIo::read(I, "ballons.jpg"); +} +\endcode + +The color conversion from RGB to HSV or from RGBa to HSV color scale is performed in ViSP using one of the following +functions: +- vpImageConvert::RGBToHSV() +- vpImageConvert::RGBaToHSV() + +The following snippet shows how to convert to HSV color scale: +\code +#include +#include + +int main() +{ + vpImage I; + vpImageIo::read(I, "ballons.jpg"); + + unsigned int width = I.getWidth(); + unsigned int height = I.getHeight(); + + vpImage H(height, width); + vpImage S(height, width); + vpImage V(height, width); + + vpImageConvert::RGBaToHSV(reinterpret_cast(I.bitmap), + reinterpret_cast(H.bitmap), + reinterpret_cast(S.bitmap), + reinterpret_cast(V.bitmap), I.getSize()); +} +\endcode +In the previous example, we obtained for each pixel: +- Hue in `H` image where values are scaled from 0 to 255. here 255 stands for 360 degrees. +- Saturation in `S` image where values are scaled from 0 to 255. Here 255 stands for 100%. +- Value in `V` image where values are scaled from 0 to 255. Here 255 stands for 100%. + +\section hsv_segmentation HSV color segmentation + +It's easy to segment a given color if we select the range of hue, saturation and value we're interested in. + +In the image `ballons.jpg`, the pixel at coordinates [93][164] has an RGB value (209, 72, 0) which corresponds to +an HSV value (14, 255, 209). We can use these HSV values and an additional offset to determine the low and high +values of the HSV ranges used to create a mask corresponding to the segmented color. + +\code +#include +#include +#include + +int main() +{ + vpImage I; + vpImageIo::read(I, "ballons.jpg"); + + unsigned int width = I.getWidth(); + unsigned int height = I.getHeight(); + + vpImage H(height, width); + vpImage S(height, width); + vpImage V(height, width); + + vpImageConvert::RGBaToHSV(reinterpret_cast(I.bitmap), + reinterpret_cast(H.bitmap), + reinterpret_cast(S.bitmap), + reinterpret_cast(V.bitmap), I.getSize()); + + int h = 14, s = 255, v = 209; + int offset = 30; + int h_low = std::max(0, h - offset), h_high = std::min(h + offset, 255); + int s_low = std::max(0, s - offset), s_high = std::min(s + offset, 255); + int v_low = std::max(0, v - offset), v_high = std::min(v + offset, 255); + std::vector hsv_range({ h_low, h_high, s_low, s_high, v_low, v_high }); + + vpImage mask(height, width); + vpImageTools::inRange(reinterpret_cast(H.bitmap), + reinterpret_cast(S.bitmap), + reinterpret_cast(V.bitmap), + hsv_range, + reinterpret_cast(mask.bitmap), + mask.getSize()); +} +\endcode + +Using the mask we can create a segmented color image. The following snippet shows how to combine the mask and the +color image using vpImageTools::inMask() to create the segmented image as given in the next snippet also available in +tutorial-hsv-segmentation-basic.cpp + +\include tutorial-hsv-segmentation-basic.cpp + +The end of the previous snippet shows also how to display the following images. + +\image html ballons-segmented.jpg + +\section hsv_next Next tutorial + +You are now ready to see how to continue with \ref tutorial-grabber. + +*/ diff --git a/doc/tutorial/tutorial-users.dox b/doc/tutorial/tutorial-users.dox index 9dc2c0359f..d98444d245 100644 --- a/doc/tutorial/tutorial-users.dox +++ b/doc/tutorial/tutorial-users.dox @@ -17,6 +17,8 @@ This page references all the tutorials to use and contribute to ViSP. - \subpage tutorial_detection_dnn +- \subpage tutorial_segmentation + - \subpage tutorial_computer_vision - \subpage tutorial_vs @@ -123,6 +125,14 @@ This page introduces the user to the way to detect features or objects in images */ +/*! \page tutorial_segmentation Segmentation +This page introduces the user to the way to achieve image and object segmentation. + +- \subpage tutorial-hsv-segmentation
This tutorial shows how to use HSV color scale to segment a given color in + an image. + +*/ + /*! \page tutorial_computer_vision Computer vision This page introduces the user to the way to estimate a pose or an homography. From 2bb9ae223ee6d9007f0a4dfd039b9d845b033bee Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Wed, 3 Apr 2024 18:51:14 +0200 Subject: [PATCH 23/32] Introduce new doxygen tutorial for the HSV range tuner tool --- ChangeLog.txt | 2 + .../segmentation/color/ballons-hsv-tuner.jpg | Bin 0 -> 66262 bytes .../color/tutorial-hsv-range-tuner.dox | 61 ++++++++++++ .../color/tutorial-hsv-segmentation.dox | 2 +- doc/tutorial/tutorial-users.dox | 3 + .../color/tutorial-hsv-range-tuner.cpp | 89 ++++++++++++++---- .../color/tutorial-hsv-segmentation-basic.cpp | 4 + 7 files changed, 143 insertions(+), 18 deletions(-) create mode 100644 doc/image/tutorial/segmentation/color/ballons-hsv-tuner.jpg create mode 100644 doc/tutorial/segmentation/color/tutorial-hsv-range-tuner.dox diff --git a/ChangeLog.txt b/ChangeLog.txt index 93b7a05c05..e17dbedd2f 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -50,6 +50,8 @@ ViSP 3.x.x (Version in development) https://visp-doc.inria.fr/doxygen/visp-daily/tutorial-install-python-bindings.html . New tutorial: Color segmentation using HSV color scale https://visp-doc.inria.fr/doxygen/visp-daily/tutorial-hsv-segmentation.html + . New tutorial: HSV low/high range tuner tool + https://visp-doc.inria.fr/doxygen/visp-daily/tutorial-hsv-range-tuner.html - Bug fixed . [#1251] Bug in vpDisplay::displayFrame() . [#1270] Build issue around std::clamp and optional header which are not found with cxx17 diff --git a/doc/image/tutorial/segmentation/color/ballons-hsv-tuner.jpg b/doc/image/tutorial/segmentation/color/ballons-hsv-tuner.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8d9b1ee4e5f282da98a864d7d81da7c8a9d80fbe GIT binary patch literal 66262 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 tutorial shows how to use HSV color scale to segment a given color in an image. +- \subpage tutorial-hsv-range-tuner
This tutorial shows how to use HSV tuner tool to ease defining HSV + how/high values used to select a given color either in a single image, or in a live stream provided by a Realsense + camera. */ diff --git a/tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp b/tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp index e3097db390..c278a170e6 100644 --- a/tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp @@ -4,7 +4,7 @@ #include -#if defined(VISP_HAVE_REALSENSE2) && defined(HAVE_OPENCV_HIGHGUI) +#if defined(HAVE_OPENCV_HIGHGUI) #include #include @@ -79,6 +79,7 @@ int main(int argc, char *argv[]) { bool opt_save_img = false; std::string opt_hsv_filename = "calib/hsv-thresholds.yml"; + std::string opt_img_filename; bool show_helper = false; for (int i = 1; i < argc; i++) { if (std::string(argv[i]) == "--hsv-thresholds") { @@ -87,7 +88,16 @@ int main(int argc, char *argv[]) } else { show_helper = true; - std::cout << "ERROR \nMissing value after parameter " << std::string(argv[i]) << std::endl; + std::cout << "ERROR \nMissing yaml filename after parameter " << std::string(argv[i]) << std::endl; + } + } + else if (std::string(argv[i]) == "--image") { + if ((i+1) < argc) { + opt_img_filename = std::string(argv[++i]); + } + else { + show_helper = true; + std::cout << "ERROR \nMissing input image name after parameter " << std::string(argv[i]) << std::endl; } } else if (std::string(argv[i]) == "--save-img") { @@ -96,13 +106,18 @@ int main(int argc, char *argv[]) if (show_helper || std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") { std::cout << "\nSYNOPSIS " << std::endl << argv[0] - << " [--hsv-thresholds ]" + << " [--image ]" + << " [--hsv-thresholds ]" << " [--save-img]" << " [--help,-h]" << std::endl; std::cout << "\nOPTIONS " << std::endl - << " --hsv-thresholds " << std::endl - << " Name of the output filename that will contain HSV low/high thresholds." << std::endl + << " --image " << std::endl + << " Name of the input image filename." << std::endl + << " When this option is not set, we use librealsense to stream images from a Realsense camera. " << std::endl + << std::endl + << " --hsv-thresholds " << std::endl + << " Name of the output filename with yaml extension that will contain HSV low/high thresholds." << std::endl << " Default: " << opt_hsv_filename << std::endl << std::endl << " --save-img" << std::endl @@ -115,6 +130,20 @@ int main(int argc, char *argv[]) } } + bool use_realsense = false; +#if defined(VISP_HAVE_REALSENSE2) + use_realsense = true; +#endif + if (use_realsense) { + if (!opt_img_filename.empty()) { + use_realsense = false; + } + } + else if (opt_img_filename.empty()) { + std::cout << "Error: you should use --image option to specify an input image..." << std::endl; + return EXIT_FAILURE; + } + int max_value_H = 255; int max_value = 255; @@ -125,14 +154,37 @@ int main(int argc, char *argv[]) hsv_values_trackbar[4] = 0; // Low V hsv_values_trackbar[5] = max_value; // High V - int width = 848, height = 480, fps = 60; + vpImage Ic; + int width, height; + +#if defined(VISP_HAVE_REALSENSE2) vpRealSense2 rs; - rs2::config config; - config.enable_stream(RS2_STREAM_COLOR, width, height, RS2_FORMAT_RGBA8, fps); - config.disable_stream(RS2_STREAM_DEPTH); - config.disable_stream(RS2_STREAM_INFRARED, 1); - config.disable_stream(RS2_STREAM_INFRARED, 2); - rs.open(config); +#endif + + if (use_realsense) { +#if defined(VISP_HAVE_REALSENSE2) + width = 848; height = 480; + int fps = 60; + rs2::config config; + config.enable_stream(RS2_STREAM_COLOR, width, height, RS2_FORMAT_RGBA8, fps); + config.disable_stream(RS2_STREAM_DEPTH); + config.disable_stream(RS2_STREAM_INFRARED, 1); + config.disable_stream(RS2_STREAM_INFRARED, 2); + rs.open(config); + rs.acquire(Ic); +#endif + } + else { + try { + vpImageIo::read(Ic, opt_img_filename); + } + catch (const vpException &e) { + std::cout << e.getStringMessage() << std::endl; + return EXIT_FAILURE; + } + width = Ic.getWidth(); + height = Ic.getHeight(); + } cv::namedWindow(window_detection_name); @@ -154,7 +206,6 @@ int main(int argc, char *argv[]) cv::createTrackbar("Low V", window_detection_name, &hsv_values_trackbar[4], max_value, on_low_V_thresh_trackbar); cv::createTrackbar("High V", window_detection_name, &hsv_values_trackbar[5], max_value, on_high_V_thresh_trackbar); - vpImage Ic(height, width); vpImage H(height, width); vpImage S(height, width); vpImage V(height, width); @@ -166,7 +217,14 @@ int main(int argc, char *argv[]) bool quit = false; while (!quit) { - rs.acquire(Ic); + if (use_realsense) { +#if defined(VISP_HAVE_REALSENSE2) + rs.acquire(Ic); +#endif + } + else { + vpImageIo::read(Ic, opt_img_filename); + } vpImageConvert::RGBaToHSV(reinterpret_cast(Ic.bitmap), reinterpret_cast(H.bitmap), reinterpret_cast(S.bitmap), @@ -268,9 +326,6 @@ int main(int argc, char *argv[]) #else int main() { -#if !defined(VISP_HAVE_REALSENSE2) - std::cout << "This tutorial needs librealsense as 3rd party." << std::endl; -#endif #if !defined(HAVE_OPENCV_HIGHGUI) std::cout << "This tutorial needs OpenCV highgui module as 3rd party." << std::endl; #endif diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation-basic.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation-basic.cpp index c1ebbb399e..149eb4466a 100644 --- a/tutorial/segmentation/color/tutorial-hsv-segmentation-basic.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation-basic.cpp @@ -20,13 +20,16 @@ int main() reinterpret_cast(S.bitmap), reinterpret_cast(V.bitmap), I.getSize()); + //! [Set HSV range] int h = 14, s = 255, v = 209; int offset = 30; int h_low = std::max(0, h - offset), h_high = std::min(h + offset, 255); int s_low = std::max(0, s - offset), s_high = std::min(s + offset, 255); int v_low = std::max(0, v - offset), v_high = std::min(v + offset, 255); std::vector hsv_range({ h_low, h_high, s_low, s_high, v_low, v_high }); + //! [Set HSV range] + //! [Create HSV mask] vpImage mask(height, width); vpImageTools::inRange(reinterpret_cast(H.bitmap), reinterpret_cast(S.bitmap), @@ -34,6 +37,7 @@ int main() hsv_range, reinterpret_cast(mask.bitmap), mask.getSize()); + //! [Create HSV mask] vpImage I_segmented(height, width); vpImageTools::inMask(I, mask, I_segmented); From 12cd8ae614ee28a2e1eb55c3e41f701cff364866 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Wed, 3 Apr 2024 19:17:08 +0200 Subject: [PATCH 24/32] Clean code --- .../color/tutorial-hsv-range-tuner.cpp | 50 +++++----- .../tutorial-hsv-segmentation-pcl-viewer.cpp | 98 ++++--------------- .../color/tutorial-hsv-segmentation-pcl.cpp | 38 +++---- .../color/tutorial-hsv-segmentation.cpp | 34 ++++--- 4 files changed, 83 insertions(+), 137 deletions(-) diff --git a/tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp b/tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp index c278a170e6..73b21d9466 100644 --- a/tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp @@ -154,7 +154,7 @@ int main(int argc, char *argv[]) hsv_values_trackbar[4] = 0; // Low V hsv_values_trackbar[5] = max_value; // High V - vpImage Ic; + vpImage I; int width, height; #if defined(VISP_HAVE_REALSENSE2) @@ -171,19 +171,19 @@ int main(int argc, char *argv[]) config.disable_stream(RS2_STREAM_INFRARED, 1); config.disable_stream(RS2_STREAM_INFRARED, 2); rs.open(config); - rs.acquire(Ic); + rs.acquire(I); #endif } else { try { - vpImageIo::read(Ic, opt_img_filename); + vpImageIo::read(I, opt_img_filename); } catch (const vpException &e) { std::cout << e.getStringMessage() << std::endl; return EXIT_FAILURE; } - width = Ic.getWidth(); - height = Ic.getHeight(); + width = I.getWidth(); + height = I.getHeight(); } cv::namedWindow(window_detection_name); @@ -210,25 +210,25 @@ int main(int argc, char *argv[]) vpImage S(height, width); vpImage V(height, width); vpImage mask(height, width); - vpImage Ic_segmented(height, width); + vpImage I_segmented(height, width); - vpDisplayX d_Ic(Ic, 0, 0, "Current frame"); - vpDisplayX d_Ic_segmented(Ic_segmented, Ic.getWidth()+75, 0, "Segmented frame"); + vpDisplayX d_I(I, 0, 0, "Current frame"); + vpDisplayX d_I_segmented(I_segmented, I.getWidth()+75, 0, "Segmented frame"); bool quit = false; while (!quit) { if (use_realsense) { #if defined(VISP_HAVE_REALSENSE2) - rs.acquire(Ic); + rs.acquire(I); #endif } else { - vpImageIo::read(Ic, opt_img_filename); + vpImageIo::read(I, opt_img_filename); } - vpImageConvert::RGBaToHSV(reinterpret_cast(Ic.bitmap), + vpImageConvert::RGBaToHSV(reinterpret_cast(I.bitmap), reinterpret_cast(H.bitmap), reinterpret_cast(S.bitmap), - reinterpret_cast(V.bitmap), Ic.getSize()); + reinterpret_cast(V.bitmap), I.getSize()); vpImageTools::inRange(reinterpret_cast(H.bitmap), reinterpret_cast(S.bitmap), @@ -237,16 +237,16 @@ int main(int argc, char *argv[]) reinterpret_cast(mask.bitmap), mask.getSize()); - vpImageTools::inMask(Ic, mask, Ic_segmented); + vpImageTools::inMask(I, mask, I_segmented); - vpDisplay::display(Ic); - vpDisplay::display(Ic_segmented); - vpDisplay::displayText(Ic, 20, 20, "Left click to learn HSV value...", vpColor::red); - vpDisplay::displayText(Ic, 40, 20, "Middle click to get HSV value...", vpColor::red); - vpDisplay::displayText(Ic, 60, 20, "Right click to quit...", vpColor::red); + vpDisplay::display(I); + vpDisplay::display(I_segmented); + vpDisplay::displayText(I, 20, 20, "Left click to learn HSV value...", vpColor::red); + vpDisplay::displayText(I, 40, 20, "Middle click to get HSV value...", vpColor::red); + vpDisplay::displayText(I, 60, 20, "Right click to quit...", vpColor::red); vpImagePoint ip; vpMouseButton::vpMouseButtonType button; - if (vpDisplay::getClick(Ic, ip, button, false)) { + if (vpDisplay::getClick(I, ip, button, false)) { if (button == vpMouseButton::button3) { quit = true; } @@ -256,8 +256,8 @@ int main(int argc, char *argv[]) int h = static_cast(H[i][j]); int s = static_cast(S[i][j]); int v = static_cast(V[i][j]); - std::cout << "RGB[" << i << "][" << j << "]: " << static_cast(Ic[i][j].R) << " " << static_cast(Ic[i][j].G) - << " " << static_cast(Ic[i][j].B) << " -> HSV: " << h << " " << s << " " << v << std::endl; + std::cout << "RGB[" << i << "][" << j << "]: " << static_cast(I[i][j].R) << " " << static_cast(I[i][j].G) + << " " << static_cast(I[i][j].B) << " -> HSV: " << h << " " << s << " " << v << std::endl; } else if (button == vpMouseButton::button1) { unsigned int i = ip.get_i(); @@ -310,14 +310,14 @@ int main(int argc, char *argv[]) std::cout << "Save images in path_img folder..." << std::endl; vpImage I_HSV; vpImageConvert::merge(&H, &S, &V, nullptr, I_HSV); - vpImageIo::write(Ic, path_img + "/I.png"); + vpImageIo::write(I, path_img + "/I.png"); vpImageIo::write(I_HSV, path_img + "/I-HSV.png"); - vpImageIo::write(Ic_segmented, path_img + "/I-HSV-segmented.png"); + vpImageIo::write(I_segmented, path_img + "/I-HSV-segmented.png"); } break; } - vpDisplay::flush(Ic); - vpDisplay::flush(Ic_segmented); + vpDisplay::flush(I); + vpDisplay::flush(I_segmented); cv::waitKey(10); // To display trackbar } return EXIT_SUCCESS; diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp index b84800a23c..63bf896196 100644 --- a/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp @@ -16,67 +16,6 @@ #include #include -#if 1 -class vpPointCloudViewer -{ -public: - explicit vpPointCloudViewer() : m_stop(false), m_flush_viewer(false) { } - - void flush() - { - m_flush_viewer = true; - } - - void run(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud) - { - pcl::PointCloud::Ptr local_pointcloud(new pcl::PointCloud()); - - bool flush_viewer = false; - pcl::visualization::PCLVisualizer::Ptr viewer(new pcl::visualization::PCLVisualizer("3D Viewer")); - viewer->setBackgroundColor(0, 0, 0); - viewer->initCameraParameters(); - viewer->setPosition(640 + 80, 480 + 80); - viewer->setCameraPosition(0, 0, -0.25, 0, -1, 0); - viewer->setSize(640, 480); - - bool first_init = true; - while (!m_stop) { - { - std::lock_guard lock(mutex); - flush_viewer = m_flush_viewer; - m_flush_viewer = false; - local_pointcloud = pointcloud->makeShared(); - } - - if (flush_viewer) { - if (first_init) { - - viewer->addPointCloud(local_pointcloud, "sample cloud"); - viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "sample cloud"); - first_init = false; - } - else { - viewer->updatePointCloud(local_pointcloud, "sample cloud"); - } - } - - viewer->spinOnce(10); - } - - std::cout << "End of point cloud display thread" << std::endl; - } - - void stop() - { - m_stop = true; - } - -private: - bool m_stop; - bool m_flush_viewer; -}; -#endif - int main(int argc, char **argv) { std::string opt_hsv_filename = "calib/hsv-thresholds.yml"; @@ -137,20 +76,21 @@ int main(int argc, char **argv) vpCameraParameters cam_depth = rs.getCameraParameters(RS2_STREAM_DEPTH, vpCameraParameters::perspectiveProjWithoutDistortion); - vpImage Ic(height, width); + vpImage I(height, width); vpImage H(height, width); vpImage S(height, width); vpImage V(height, width); - vpImage Ic_segmented_mask(height, width, 0); + vpImage mask(height, width); vpImage depth_raw(height, width); + vpImage I_segmented(height, width); - vpDisplayX d_Ic(Ic, 0, 0, "Current frame"); - vpDisplayX d_Ic_segmented_mask(Ic_segmented_mask, Ic.getWidth()+75, 0, "HSV segmented frame"); + vpDisplayX d_I(I, 0, 0, "Current frame"); + vpDisplayX d_I_segmented(I_segmented, I.getWidth()+75, 0, "HSV segmented frame"); bool quit = false; double loop_time = 0., total_loop_time = 0.; long nb_iter = 0; - float Z_min = 0.2; + float Z_min = 0.1; float Z_max = 2.5; int pcl_size = 0; @@ -162,37 +102,39 @@ int main(int argc, char **argv) while (!quit) { double t = vpTime::measureTimeMs(); - rs.acquire((unsigned char *)Ic.bitmap, (unsigned char *)(depth_raw.bitmap), NULL, NULL, &align_to); - vpImageConvert::RGBaToHSV(reinterpret_cast(Ic.bitmap), + rs.acquire((unsigned char *)I.bitmap, (unsigned char *)(depth_raw.bitmap), NULL, NULL, &align_to); + vpImageConvert::RGBaToHSV(reinterpret_cast(I.bitmap), reinterpret_cast(H.bitmap), reinterpret_cast(S.bitmap), - reinterpret_cast(V.bitmap), Ic.getSize()); + reinterpret_cast(V.bitmap), I.getSize()); vpImageTools::inRange(reinterpret_cast(H.bitmap), reinterpret_cast(S.bitmap), reinterpret_cast(V.bitmap), hsv_values, - reinterpret_cast(Ic_segmented_mask.bitmap), - Ic_segmented_mask.getSize()); + reinterpret_cast(mask.bitmap), + mask.getSize()); + + vpImageTools::inMask(I, mask, I_segmented); { std::lock_guard lock(pcl_viewer_mutex); - vpImageConvert::depthToPointCloud(depth_raw, depth_scale, cam_depth, pointcloud, &Ic_segmented_mask, Z_min, Z_max); + vpImageConvert::depthToPointCloud(depth_raw, depth_scale, cam_depth, pointcloud, &mask, Z_min, Z_max); pcl_size = pointcloud->size(); } std::cout << "Segmented point cloud size: " << pcl_size << std::endl; - vpDisplay::display(Ic); - vpDisplay::display(Ic_segmented_mask); - vpDisplay::displayText(Ic, 20, 20, "Click to quit...", vpColor::red); + vpDisplay::display(I); + vpDisplay::display(I_segmented); + vpDisplay::displayText(I, 20, 20, "Click to quit...", vpColor::red); - if (vpDisplay::getClick(Ic, false)) { + if (vpDisplay::getClick(I, false)) { quit = true; } - vpDisplay::flush(Ic); - vpDisplay::flush(Ic_segmented_mask); + vpDisplay::flush(I); + vpDisplay::flush(I_segmented); nb_iter++; loop_time = vpTime::measureTimeMs() - t; total_loop_time += loop_time; diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp index e993ab03d1..919a99bc6a 100644 --- a/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp @@ -72,20 +72,21 @@ int main(int argc, char **argv) vpCameraParameters cam_depth = rs.getCameraParameters(RS2_STREAM_DEPTH, vpCameraParameters::perspectiveProjWithoutDistortion); - vpImage Ic(height, width); + vpImage I(height, width); vpImage H(height, width); vpImage S(height, width); vpImage V(height, width); - vpImage Ic_segmented_mask(height, width, 0); + vpImage mask(height, width, 0); vpImage depth_raw(height, width); + vpImage I_segmented(height, width); - vpDisplayX d_Ic(Ic, 0, 0, "Current frame"); - vpDisplayX d_Ic_segmented_mask(Ic_segmented_mask, Ic.getWidth()+75, 0, "HSV segmented frame"); + vpDisplayX d_I(I, 0, 0, "Current frame"); + vpDisplayX d_I_segmented(I_segmented, I.getWidth()+75, 0, "HSV segmented frame"); bool quit = false; double loop_time = 0., total_loop_time = 0.; long nb_iter = 0; - float Z_min = 0.2; + float Z_min = 0.1; float Z_max = 2.5; int pcl_size = 0; @@ -93,40 +94,41 @@ int main(int argc, char **argv) while (!quit) { double t = vpTime::measureTimeMs(); - rs.acquire((unsigned char *)Ic.bitmap, (unsigned char *)(depth_raw.bitmap), NULL, NULL, &align_to); - vpImageConvert::RGBaToHSV(reinterpret_cast(Ic.bitmap), + rs.acquire((unsigned char *)I.bitmap, (unsigned char *)(depth_raw.bitmap), NULL, NULL, &align_to); + vpImageConvert::RGBaToHSV(reinterpret_cast(I.bitmap), reinterpret_cast(H.bitmap), reinterpret_cast(S.bitmap), - reinterpret_cast(V.bitmap), Ic.getSize()); + reinterpret_cast(V.bitmap), I.getSize()); vpImageTools::inRange(reinterpret_cast(H.bitmap), reinterpret_cast(S.bitmap), reinterpret_cast(V.bitmap), hsv_values, - reinterpret_cast(Ic_segmented_mask.bitmap), - Ic_segmented_mask.getSize()); + reinterpret_cast(mask.bitmap), + mask.getSize()); - vpImageConvert::depthToPointCloud(depth_raw, depth_scale, cam_depth, pointcloud, &Ic_segmented_mask, Z_min, Z_max); + vpImageTools::inMask(I, mask, I_segmented); + + vpImageConvert::depthToPointCloud(depth_raw, depth_scale, cam_depth, pointcloud, &mask, Z_min, Z_max); pcl_size = pointcloud->size(); std::cout << "Segmented point cloud size: " << pcl_size << std::endl; - vpDisplay::display(Ic); - vpDisplay::display(Ic_segmented_mask); - vpDisplay::displayText(Ic, 20, 20, "Click to quit...", vpColor::red); + vpDisplay::display(I); + vpDisplay::display(I_segmented); + vpDisplay::displayText(I, 20, 20, "Click to quit...", vpColor::red); - if (vpDisplay::getClick(Ic, false)) { + if (vpDisplay::getClick(I, false)) { quit = true; } - vpDisplay::flush(Ic); - vpDisplay::flush(Ic_segmented_mask); + vpDisplay::flush(I); + vpDisplay::flush(I_segmented); nb_iter++; loop_time = vpTime::measureTimeMs() - t; total_loop_time += loop_time; } - std::cout << "Mean loop time: " << total_loop_time / nb_iter << std::endl; return EXIT_SUCCESS; } diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp index 31ad53be7c..b9f4bb2255 100644 --- a/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp @@ -66,14 +66,15 @@ int main(int argc, char **argv) config.disable_stream(RS2_STREAM_INFRARED, 2); rs.open(config); - vpImage Ic(height, width); + vpImage I(height, width); vpImage H(height, width); vpImage S(height, width); vpImage V(height, width); - vpImage Ic_segmented_mask(height, width, 0); + vpImage mask(height, width); + vpImage I_segmented(height, width); - vpDisplayX d_Ic(Ic, 0, 0, "Current frame"); - vpDisplayX d_Ic_segmented_mask(Ic_segmented_mask, Ic.getWidth()+75, 0, "HSV segmented frame"); + vpDisplayX d_I(I, 0, 0, "Current frame"); + vpDisplayX d_I_segmented(I_segmented, I.getWidth()+75, 0, "HSV segmented frame"); bool quit = false; double loop_time = 0., total_loop_time = 0.; @@ -81,35 +82,36 @@ int main(int argc, char **argv) while (!quit) { double t = vpTime::measureTimeMs(); - rs.acquire(Ic); - vpImageConvert::RGBaToHSV(reinterpret_cast(Ic.bitmap), + rs.acquire(I); + vpImageConvert::RGBaToHSV(reinterpret_cast(I.bitmap), reinterpret_cast(H.bitmap), reinterpret_cast(S.bitmap), - reinterpret_cast(V.bitmap), Ic.getSize()); + reinterpret_cast(V.bitmap), I.getSize()); vpImageTools::inRange(reinterpret_cast(H.bitmap), reinterpret_cast(S.bitmap), reinterpret_cast(V.bitmap), hsv_values, - reinterpret_cast(Ic_segmented_mask.bitmap), - Ic_segmented_mask.getSize()); + reinterpret_cast(mask.bitmap), + mask.getSize()); - vpDisplay::display(Ic); - vpDisplay::display(Ic_segmented_mask); - vpDisplay::displayText(Ic, 20, 20, "Click to quit...", vpColor::red); + vpImageTools::inMask(I, mask, I_segmented); - if (vpDisplay::getClick(Ic, false)) { + vpDisplay::display(I); + vpDisplay::display(I_segmented); + vpDisplay::displayText(I, 20, 20, "Click to quit...", vpColor::red); + + if (vpDisplay::getClick(I, false)) { quit = true; } - vpDisplay::flush(Ic); - vpDisplay::flush(Ic_segmented_mask); + vpDisplay::flush(I); + vpDisplay::flush(I_segmented); nb_iter++; loop_time = vpTime::measureTimeMs() - t; total_loop_time += loop_time; } - std::cout << "Mean loop time: " << total_loop_time / nb_iter << std::endl; return EXIT_SUCCESS; } From 387ce8274b991cdaefad24b06da0ca86d3029ad0 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Thu, 4 Apr 2024 14:47:32 +0200 Subject: [PATCH 25/32] Introduce new doxygen tutorials for pcl segmentation --- .vscode/settings.json | 25 +++- ChangeLog.txt | 8 +- .../color/tutorial-hsv-range-tuner.dox | 6 +- ...ox => tutorial-hsv-segmentation-intro.dox} | 2 +- .../color/tutorial-hsv-segmentation-live.dox | 68 ++++++++++ .../color/tutorial-hsv-segmentation-pcl.dox | 118 ++++++++++++++++++ doc/tutorial/tutorial-users.dox | 9 +- .../color/tutorial-hsv-range-tuner.cpp | 1 + .../color/tutorial-hsv-segmentation-basic.cpp | 8 +- .../tutorial-hsv-segmentation-pcl-viewer.cpp | 9 +- .../color/tutorial-hsv-segmentation-pcl.cpp | 23 +++- .../color/tutorial-hsv-segmentation.cpp | 99 +++++++++++---- 12 files changed, 336 insertions(+), 40 deletions(-) rename doc/tutorial/segmentation/color/{tutorial-hsv-segmentation.dox => tutorial-hsv-segmentation-intro.dox} (97%) create mode 100644 doc/tutorial/segmentation/color/tutorial-hsv-segmentation-live.dox create mode 100644 doc/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.dox diff --git a/.vscode/settings.json b/.vscode/settings.json index 6d0919271e..26ebfdc369 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -138,7 +138,30 @@ "xtr1common": "cpp", "xtree": "cpp", "xutility": "cpp", - "execution": "cpp" + "execution": "cpp", + "bufferobject": "cpp", + "image": "cpp", + "texture": "cpp", + "vertexarraystate": "cpp", + "buffered_value": "cpp", + "fast_back_stack": "cpp", + "graphicscontext": "cpp", + "program": "cpp", + "shader": "cpp", + "shape": "cpp", + "camera": "cpp", + "viewport": "cpp", + "types": "cpp", + "callback": "cpp", + "graphicsthread": "cpp", + "operationthread": "cpp", + "stateset": "cpp", + "export": "cpp", + "observer_ptr": "cpp", + "primitiveset": "cpp", + "stateattribute": "cpp", + "uniform": "cpp", + "polytope": "cpp" }, "C_Cpp.vcFormat.indent.namespaceContents": false, "editor.formatOnSave": true, diff --git a/ChangeLog.txt b/ChangeLog.txt index e17dbedd2f..7ecda86a55 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -48,10 +48,14 @@ ViSP 3.x.x (Version in development) https://visp-doc.inria.fr/doxygen/visp-daily/tutorial-spc.html . New tutorial: Installing ViSP Python bindings https://visp-doc.inria.fr/doxygen/visp-daily/tutorial-install-python-bindings.html - . New tutorial: Color segmentation using HSV color scale - https://visp-doc.inria.fr/doxygen/visp-daily/tutorial-hsv-segmentation.html + . New tutorial: Introduction to color segmentation using HSV color scale + https://visp-doc.inria.fr/doxygen/visp-daily/tutorial-hsv-segmentation-intro.html . New tutorial: HSV low/high range tuner tool https://visp-doc.inria.fr/doxygen/visp-daily/tutorial-hsv-range-tuner.html + . New tutorial: Live color segmentation using HSV color scale + https://visp-doc.inria.fr/doxygen/visp-daily/tutorial-hsv-segmentation-live.html + . New tutorial: Point cloud segmentation using HSV color scale + https://visp-doc.inria.fr/doxygen/visp-daily/tutorial-hsv-segmentation-pcl.html - Bug fixed . [#1251] Bug in vpDisplay::displayFrame() . [#1270] Build issue around std::clamp and optional header which are not found with cxx17 diff --git a/doc/tutorial/segmentation/color/tutorial-hsv-range-tuner.dox b/doc/tutorial/segmentation/color/tutorial-hsv-range-tuner.dox index dc3d114de7..21c98cae31 100644 --- a/doc/tutorial/segmentation/color/tutorial-hsv-range-tuner.dox +++ b/doc/tutorial/segmentation/color/tutorial-hsv-range-tuner.dox @@ -4,7 +4,7 @@ \section hsv_range_tuner_intro Introduction -This tutorial follows \ref tutorial-hsv-segmentation. +This tutorial follows \ref tutorial-hsv-segmentation-intro. Note that all the material (source code and images) described in this tutorial is part of ViSP source code (in `tutorial/segmentation/color` folder) and could be found in @@ -12,7 +12,7 @@ https://github.com/lagadic/visp/tree/master/tutorial/segmentation/color. \section hsv_range_tuner HSV Range tuner tool -In the previous tutorial (see \ref tutorial-hsv-segmentation), we used the following lines to determine +In the previous tutorial (see \ref tutorial-hsv-segmentation-intro), we used the following lines to determine HSV low/high range values in `hsv_range` vector: \snippet tutorial-hsv-segmentation-basic.cpp Set HSV range @@ -56,6 +56,6 @@ data: \section hsv_range_tuner_next Next tutorial -You are now ready to see how to continue with \ref tutorial-grabber. +You are now ready to see how to continue with \ref tutorial-hsv-segmentation-live. */ diff --git a/doc/tutorial/segmentation/color/tutorial-hsv-segmentation.dox b/doc/tutorial/segmentation/color/tutorial-hsv-segmentation-intro.dox similarity index 97% rename from doc/tutorial/segmentation/color/tutorial-hsv-segmentation.dox rename to doc/tutorial/segmentation/color/tutorial-hsv-segmentation-intro.dox index 397cac8762..1d18a63cc4 100644 --- a/doc/tutorial/segmentation/color/tutorial-hsv-segmentation.dox +++ b/doc/tutorial/segmentation/color/tutorial-hsv-segmentation-intro.dox @@ -1,5 +1,5 @@ /** - \page tutorial-hsv-segmentation Tutorial: Color segmentation using HSV color scale + \page tutorial-hsv-segmentation-intro Tutorial: Introduction to color segmentation using HSV color scale \tableofcontents \section hsv_intro Introduction diff --git a/doc/tutorial/segmentation/color/tutorial-hsv-segmentation-live.dox b/doc/tutorial/segmentation/color/tutorial-hsv-segmentation-live.dox new file mode 100644 index 0000000000..c6acac9153 --- /dev/null +++ b/doc/tutorial/segmentation/color/tutorial-hsv-segmentation-live.dox @@ -0,0 +1,68 @@ +/** + \page tutorial-hsv-segmentation-live Tutorial: Live color segmentation using HSV color scale + \tableofcontents + +\section hsv_video_intro Introduction + +This tutorial follows \ref tutorial-hsv-range-tuner. + +To run this tutorial you will need: +- a Realsense camera like a D435 device +- ViSP build with librealsense and PCL libraries as 3rd parties + +We suppose here that you already set the HSV low/high ranges using the range tuner tool explained in +\ref tutorial-hsv-range-tuner. + +Note that all the material (source code and images) described in this tutorial is part of ViSP source code +(in `tutorial/segmentation/color` folder) and could be found in +https://github.com/lagadic/visp/tree/master/tutorial/segmentation/color. + +\section hsv_video_recorded Color segmentation on a recorded video + +- If not already done, the first step is to record a video as described in \ref tutorial-grabber. + Let us consider the case of a Realsense camera. To record a video you may run: +\verbatim +$ cd $VISP_WS/visp-build/tutorial/grabber +$ ./tutorial-grabber-realsense --seqname /tmp/seq/image-%04d.png --record 0 +\endverbatim + - Use left click to start recording, and then right click to stop and quit. + - The sequence of successive images is recorded in `/tmp/seq/` folder. + +- Use the HSV range tuner tool explained in \ref tutorial-hsv-range-tuner to select the color you want to segment. + We select the HSV low/high range on the first image of the sequence with +\verbatim +$ cd $VISP_WS/visp-build/tutorial/segmentation/color +$ ./tutorial-hsv-range-tuner --image /tmp/seq/image-0001.png +\endverbatim + - As a result you will find the learned HSV low/high ranges in `calib/hsv-thresholds.yml` + +- Now you are ready to process all the recorded video in order to perform color segmentation based on the + content of `calib/hsv-thresholds.yml` +\verbatim +$ ./tutorial-hsv-segmentation --video /tmp/seq/image-%04d.png --hsv-thresholds calib/hsv-thresholds.yml +\endverbatim + +\section hsv_video_live Color segmentation on a live stream + +- We suppose here that you have a Realsense camera and that you install librealsense before building ViSP with + librealsense 3rdparty support. If you are not familiar with these steps, follow one of the \ref tutorial_install_src + tutorials. +- Plug your Realsense camera and use the HSV range tuner tool explained in \ref tutorial-hsv-range-tuner to select + the color you want to segment. +\verbatim +$ cd $VISP_WS/visp-build/tutorial/segmentation/color +$ ./tutorial-hsv-range-tuner --hsv-thresholds calib/hsv-thresholds.yml +\endverbatim + - As a result you will find the learned HSV low/high ranges in `calib/hsv-thresholds.yml` + +- Now you are ready to process the Realsense live stream in order to perform color segmentation based on the + content of `calib/hsv-thresholds.yml` +\verbatim +$ ./tutorial-hsv-segmentation --hsv-thresholds calib/hsv-thresholds.yml +\endverbatim + +\section hsv_video_next Next tutorial + +You are now ready to see how to continue with \ref tutorial-hsv-segmentation-pcl. + +*/ diff --git a/doc/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.dox b/doc/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.dox new file mode 100644 index 0000000000..aad1a64ca4 --- /dev/null +++ b/doc/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.dox @@ -0,0 +1,118 @@ +/** + \page tutorial-hsv-segmentation-pcl Tutorial: Point cloud segmentation using HSV color scale + \tableofcontents + +\section hsv_pcl_intro Introduction + +This tutorial follows \ref tutorial-hsv-segmentation-live. + +To run this tutorial you will need: +- a Realsense camera like a D435 device +- ViSP build with librealsense and PCL libraries as 3rd parties + +We suppose here that you already set the HSV low/high ranges using the range tuner tool explained in +\ref tutorial-hsv-range-tuner. + +Note that all the material (source code and images) described in this tutorial is part of ViSP source code +(in `tutorial/segmentation/color` folder) and could be found in +https://github.com/lagadic/visp/tree/master/tutorial/segmentation/color. + +\section hsv_pcl_segmentation Point cloud segmentation + +In \ref tutorial-hsv-segmentation-intro the pipeline to achieve the HSV color segmentation was to: +- load or acquire an RGB image +- convert from RGB to HSV channels using vpImageConvert::RGBaToHSV() +- create a mask with HSV values that are in the low/high HSV ranges using vpImageTools::inRange() +- using the mask and the RGB image create a segmented image using vpImageTools::inMask() + +To extend this pipeline to create a point cloud that contains only the 3D points that are in the mask you need a RGB-D +device. In our case we will consider a Realsense camera. The pipeline becomes: +- configure the realsense grabber to acquire color and depth aligned images +\snippet tutorial-hsv-segmentation-pcl.cpp Config RS2 RGB and depth +- get depth scale associated to the depth images and device intrisics +\snippet tutorial-hsv-segmentation-pcl.cpp Get RS2 intrinsics +- grab color and depth images +\snippet tutorial-hsv-segmentation-pcl.cpp Grab color and depth +- convert from RGB to HSV channels using vpImageConvert::RGBaToHSV() +\snippet tutorial-hsv-segmentation-pcl.cpp RGB to HSV +- create a mask with pixels that are in the low/high HSV ranges using vpImageTools::inRange() +\snippet tutorial-hsv-segmentation-pcl.cpp Create mask +- create the point cloud object +\snippet tutorial-hsv-segmentation-pcl.cpp Allocate point cloud +- using the mask and the depth image update the point cloud using vpImageConvert::depthToPointCloud(). + The corresponding point cloud is available in `pointcloud` variable. +\snippet tutorial-hsv-segmentation-pcl.cpp Update point cloud +- to know the size of the point cloud use +\snippet tutorial-hsv-segmentation-pcl.cpp Get point cloud size +. + +All these steps are implemented in tutorial-hsv-segmentation-pcl.cpp + +To run this tutorial +\verbatim +$ cd $VISP_WS/visp-build/tutorial/segmentation/color +$ ./tutorial-hsv-segmentation-pcl --hsv-thresholds calib/hsv-thresholds.yml +\endverbatim + +In the next section we show how to display the point cloud. + +\section hsv_pcl-viewer Point could viewer + +In tutorial-hsv-segmentation-pcl-viewer.cpp you will find an extension of the previous code with the introduction +of vpDisplayPCL class that allows to visualize the point cloud in 3D. + +To add the point cloud viewer feature: +- First you need to include vpDisplayPCL header +\snippet tutorial-hsv-segmentation-pcl-viewer.cpp Include vpDisplayPCL header +- Next, you need to create a mutex and the PCL viewer object before launching the viewer thread +\snippet tutorial-hsv-segmentation-pcl-viewer.cpp Create pcl viewer object +- In the `while()` loop we update the point cloud using the mutex shared with the pcl viewer +\snippet tutorial-hsv-segmentation-pcl-viewer.cpp Update point cloud with mutex protection + +To run this tutorial +\verbatim +$ cd $VISP_WS/visp-build/tutorial/segmentation/color +$ ./tutorial-hsv-segmentation-pcl-viewer --hsv-thresholds calib/hsv-thresholds.yml +\endverbatim + +\section hsv_pcl_issue Known issue +\subsection hsv_pcl_issue_segfault Segfault in PCL viewer + +There is a known issue related to the call to PCLVisualizer::spinOnce() function that is used in vpDisplayPCL class. +This issue is reported in https://github.com/PointCloudLibrary/pcl/issues/5237 and occurs with PCL 1.12.1 and +VTK 9.1 on Ubuntu 22.04. + +The workaround consists in installing the lastest PCL release. + +- First uninstall `libpcl-dev` package +\verbatim +$ sudo apt remove libpcl-dev +\endverbatim + +- Then download and install latest PCL release from source. In our case we installed PCL 1.14.0 on Ubuntu 22.04 with: +\verbatim +$ mkdir -p $VISP_WS/3rdparty/pcl +$ cd $VISP_WS/3rdparty/pcl +$ git clone --branch pcl-1.14.0 https://github.com/PointCloudLibrary/pcl pcl-1.14.0 +$ mkdir pcl-1.14.0/build +$ cd pcl-1.14.0/build +$ make -j$(nproc) +$ sudo make install +\endverbatim + +- Finally, create a fresh ViSP configuration and build. + \warning It could be nice to create a backup of the `visp-build` folder before removing it. + +\verbatim +$ cd $VISP_WS +$ rm -rf visp-build +$ mkdir visp-build && cd visp-build +$ cmake ../visp +$ make -j$(nproc) +\endverbatim + +\section hsv_pcl_next Next tutorial + +You are now ready to see how to continue with \ref tutorial-grabber. + +*/ diff --git a/doc/tutorial/tutorial-users.dox b/doc/tutorial/tutorial-users.dox index f29ad7cee7..e0d08a9370 100644 --- a/doc/tutorial/tutorial-users.dox +++ b/doc/tutorial/tutorial-users.dox @@ -128,12 +128,15 @@ This page introduces the user to the way to detect features or objects in images /*! \page tutorial_segmentation Segmentation This page introduces the user to the way to achieve image and object segmentation. -- \subpage tutorial-hsv-segmentation
This tutorial shows how to use HSV color scale to segment a given color in - an image. +- \subpage tutorial-hsv-segmentation-intro
This tutorial introduces how to use HSV color scale to segment a given + color in an image. - \subpage tutorial-hsv-range-tuner
This tutorial shows how to use HSV tuner tool to ease defining HSV how/high values used to select a given color either in a single image, or in a live stream provided by a Realsense camera. - +- \subpage tutorial-hsv-segmentation-live
This tutorial illustrates how to use the HSV color scale to segment + a given color in recorded or live video stream from a Realsense camera. +- \subpage tutorial-hsv-segmentation-pcl
This tutorial shows how to use the HSV color scale to segment + a given color in recorded or live video stream from a Realsense camera and then compute the corresponding point cloud. */ /*! \page tutorial_computer_vision Computer vision diff --git a/tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp b/tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp index 73b21d9466..77b869c717 100644 --- a/tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp @@ -115,6 +115,7 @@ int main(int argc, char *argv[]) << " --image " << std::endl << " Name of the input image filename." << std::endl << " When this option is not set, we use librealsense to stream images from a Realsense camera. " << std::endl + << " Example: --image ballons.jpg" << std::endl << std::endl << " --hsv-thresholds " << std::endl << " Name of the output filename with yaml extension that will contain HSV low/high thresholds." << std::endl diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation-basic.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation-basic.cpp index 149eb4466a..20a25cd611 100644 --- a/tutorial/segmentation/color/tutorial-hsv-segmentation-basic.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation-basic.cpp @@ -26,7 +26,13 @@ int main() int h_low = std::max(0, h - offset), h_high = std::min(h + offset, 255); int s_low = std::max(0, s - offset), s_high = std::min(s + offset, 255); int v_low = std::max(0, v - offset), v_high = std::min(v + offset, 255); - std::vector hsv_range({ h_low, h_high, s_low, s_high, v_low, v_high }); + std::vector hsv_range; + hsv_range.push_back(h_low); + hsv_range.push_back(h_high); + hsv_range.push_back(s_low); + hsv_range.push_back(s_high); + hsv_range.push_back(v_low); + hsv_range.push_back(v_high); //! [Set HSV range] //! [Create HSV mask] diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp index 63bf896196..e756891ac5 100644 --- a/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp @@ -10,12 +10,11 @@ #include #include #include +//! [Include vpDisplayPCL header] #include +//! [Include vpDisplayPCL header] #include -#include -#include - int main(int argc, char **argv) { std::string opt_hsv_filename = "calib/hsv-thresholds.yml"; @@ -96,9 +95,11 @@ int main(int argc, char **argv) pcl::PointCloud::Ptr pointcloud = pcl::PointCloud::Ptr(new pcl::PointCloud); + //! [Create pcl viewer object] std::mutex pcl_viewer_mutex; vpDisplayPCL pcl_viewer; pcl_viewer.startThread(std::ref(pcl_viewer_mutex), pointcloud); + //! [Create pcl viewer object] while (!quit) { double t = vpTime::measureTimeMs(); @@ -118,9 +119,11 @@ int main(int argc, char **argv) vpImageTools::inMask(I, mask, I_segmented); { + //! [Update point cloud with mutex protection] std::lock_guard lock(pcl_viewer_mutex); vpImageConvert::depthToPointCloud(depth_raw, depth_scale, cam_depth, pointcloud, &mask, Z_min, Z_max); pcl_size = pointcloud->size(); + //! [Update point cloud with mutex protection] } std::cout << "Segmented point cloud size: " << pcl_size << std::endl; diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp index 919a99bc6a..ddb0a01abb 100644 --- a/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp @@ -58,6 +58,7 @@ int main(int argc, char **argv) } int width = 848, height = 480, fps = 60; + //! [Config RS2 RGB and depth] vpRealSense2 rs; rs2::config config; config.enable_stream(RS2_STREAM_COLOR, width, height, RS2_FORMAT_RGBA8, fps); @@ -65,12 +66,15 @@ int main(int argc, char **argv) config.disable_stream(RS2_STREAM_INFRARED, 1); config.disable_stream(RS2_STREAM_INFRARED, 2); rs2::align align_to(RS2_STREAM_COLOR); + //! [Config RS2 RGB and depth] rs.open(config); + //! [Get RS2 intrinsics] float depth_scale = rs.getDepthScale(); vpCameraParameters cam_depth = rs.getCameraParameters(RS2_STREAM_DEPTH, vpCameraParameters::perspectiveProjWithoutDistortion); + //! [Get RS2 intrinsics] vpImage I(height, width); vpImage H(height, width); @@ -86,31 +90,44 @@ int main(int argc, char **argv) bool quit = false; double loop_time = 0., total_loop_time = 0.; long nb_iter = 0; + + //! [Allocate point cloud] float Z_min = 0.1; float Z_max = 2.5; - int pcl_size = 0; - pcl::PointCloud::Ptr pointcloud = pcl::PointCloud::Ptr(new pcl::PointCloud); + //! [Allocate point cloud] while (!quit) { double t = vpTime::measureTimeMs(); + //! [Grab color and depth] rs.acquire((unsigned char *)I.bitmap, (unsigned char *)(depth_raw.bitmap), NULL, NULL, &align_to); + //! [Grab color and depth] + + //! [RGB to HSV] vpImageConvert::RGBaToHSV(reinterpret_cast(I.bitmap), reinterpret_cast(H.bitmap), reinterpret_cast(S.bitmap), reinterpret_cast(V.bitmap), I.getSize()); + //! [RGB to HSV] + //! [Create mask] vpImageTools::inRange(reinterpret_cast(H.bitmap), reinterpret_cast(S.bitmap), reinterpret_cast(V.bitmap), hsv_values, reinterpret_cast(mask.bitmap), mask.getSize()); + //! [Create mask] vpImageTools::inMask(I, mask, I_segmented); + //! [Update point cloud] vpImageConvert::depthToPointCloud(depth_raw, depth_scale, cam_depth, pointcloud, &mask, Z_min, Z_max); - pcl_size = pointcloud->size(); + //! [Update point cloud] + + //! [Get point cloud size] + int pcl_size = pointcloud->size(); + //! [Get point cloud size] std::cout << "Segmented point cloud size: " << pcl_size << std::endl; diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp index b9f4bb2255..2c928c864c 100644 --- a/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp @@ -3,30 +3,47 @@ #include #include -#if defined(VISP_HAVE_REALSENSE2) #include #include #include #include #include #include +#include #include int main(int argc, char **argv) { std::string opt_hsv_filename = "calib/hsv-thresholds.yml"; + std::string opt_video_filename; + bool show_helper = false; for (int i = 0; i < argc; i++) { if (std::string(argv[i]) == "--hsv-thresholds") { opt_hsv_filename = std::string(argv[++i]); } - else if (std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") { + else if (std::string(argv[i]) == "--video") { + if ((i+1) < argc) { + opt_video_filename = std::string(argv[++i]); + } + else { + show_helper = true; + std::cout << "ERROR \nMissing input video name after parameter " << std::string(argv[i]) << std::endl; + } + } + else if (show_helper || std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") { std::cout << "\nSYNOPSIS " << std::endl << argv[0] + << " [--video ]" << " [--hsv-thresholds ]" << " [--help,-h]" << std::endl; std::cout << "\nOPTIONS " << std::endl + << " --video " << std::endl + << " Name of the input video filename." << std::endl + << " When this option is not set, we use librealsense to stream images from a Realsense camera. " << std::endl + << " Example: --video image-%04d.jpg" << std::endl + << std::endl << " --hsv-thresholds " << std::endl << " Path to a yaml filename that contains H , S , V threshold values." << std::endl << " An Example of such a file could be:" << std::endl @@ -47,6 +64,20 @@ int main(int argc, char **argv) } } + bool use_realsense = false; +#if defined(VISP_HAVE_REALSENSE2) + use_realsense = true; +#endif + if (use_realsense) { + if (!opt_video_filename.empty()) { + use_realsense = false; + } + } + else if (opt_video_filename.empty()) { + std::cout << "Error: you should use --image option to specify an input image..." << std::endl; + return EXIT_FAILURE; + } + vpColVector hsv_values; if (vpColVector::loadYAML(opt_hsv_filename, hsv_values)) { std::cout << "Load HSV threshold values from " << opt_hsv_filename << std::endl; @@ -57,16 +88,40 @@ int main(int argc, char **argv) return EXIT_FAILURE; } - int width = 848, height = 480, fps = 60; + vpImage I; + int width, height; + + vpVideoReader g; +#if defined(VISP_HAVE_REALSENSE2) vpRealSense2 rs; - rs2::config config; - config.enable_stream(RS2_STREAM_COLOR, width, height, RS2_FORMAT_RGBA8, fps); - config.disable_stream(RS2_STREAM_DEPTH); - config.disable_stream(RS2_STREAM_INFRARED, 1); - config.disable_stream(RS2_STREAM_INFRARED, 2); - rs.open(config); - - vpImage I(height, width); +#endif + + if (use_realsense) { +#if defined(VISP_HAVE_REALSENSE2) + width = 848; height = 480; + int fps = 60; + rs2::config config; + config.enable_stream(RS2_STREAM_COLOR, width, height, RS2_FORMAT_RGBA8, fps); + config.disable_stream(RS2_STREAM_DEPTH); + config.disable_stream(RS2_STREAM_INFRARED, 1); + config.disable_stream(RS2_STREAM_INFRARED, 2); + rs.open(config); + rs.acquire(I); +#endif + } + else { + try { + g.setFileName(opt_video_filename); + g.open(I); + } + catch (const vpException &e) { + std::cout << e.getStringMessage() << std::endl; + return EXIT_FAILURE; + } + width = I.getWidth(); + height = I.getHeight(); + } + vpImage H(height, width); vpImage S(height, width); vpImage V(height, width); @@ -82,7 +137,16 @@ int main(int argc, char **argv) while (!quit) { double t = vpTime::measureTimeMs(); - rs.acquire(I); + if (use_realsense) { +#if defined(VISP_HAVE_REALSENSE2) + rs.acquire(I); +#endif + } + else { + if (!g.end()) { + g.acquire(I); + } + } vpImageConvert::RGBaToHSV(reinterpret_cast(I.bitmap), reinterpret_cast(H.bitmap), reinterpret_cast(S.bitmap), @@ -115,14 +179,3 @@ int main(int argc, char **argv) std::cout << "Mean loop time: " << total_loop_time / nb_iter << std::endl; return EXIT_SUCCESS; } -#else -int main() -{ -#if !defined(VISP_HAVE_REALSENSE2) - std::cout << "This tutorial needs librealsense as 3rd party." << std::endl; -#endif - - std::cout << "Install missing 3rd party, configure and rebuild ViSP." << std::endl; - return EXIT_SUCCESS; -} -#endif From 509132b477674a9c937765ffb41eae7e6ecc3553 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Fri, 5 Apr 2024 12:39:58 +0200 Subject: [PATCH 26/32] Introduce the possibility to visualize a textured point cloud --- modules/gui/include/visp3/gui/vpDisplayPCL.h | 2 + modules/gui/src/pointcloud/vpDisplayPCL.cpp | 83 ++++++++++++++++++-- 2 files changed, 77 insertions(+), 8 deletions(-) diff --git a/modules/gui/include/visp3/gui/vpDisplayPCL.h b/modules/gui/include/visp3/gui/vpDisplayPCL.h index a025a3a938..3d693b9878 100644 --- a/modules/gui/include/visp3/gui/vpDisplayPCL.h +++ b/modules/gui/include/visp3/gui/vpDisplayPCL.h @@ -59,10 +59,12 @@ class VISP_EXPORT vpDisplayPCL void setVerbose(bool verbose); void startThread(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud); + void startThread(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud); void stop(); private: void run(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud); + void run_color(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud); bool m_stop; bool m_verbose; diff --git a/modules/gui/src/pointcloud/vpDisplayPCL.cpp b/modules/gui/src/pointcloud/vpDisplayPCL.cpp index 4598833412..b265a14553 100644 --- a/modules/gui/src/pointcloud/vpDisplayPCL.cpp +++ b/modules/gui/src/pointcloud/vpDisplayPCL.cpp @@ -41,15 +41,23 @@ * Default constructor. * By default, viewer size is set to 640 x 480. */ -vpDisplayPCL::vpDisplayPCL() : m_stop(false), m_verbose(false), m_width(640), m_height(480) { } +vpDisplayPCL::vpDisplayPCL() + : m_stop(false), m_verbose(false), m_width(640), m_height(480) +{ } /*! * Constructor able to initialize the display window size. + * \param[in] width : Point cloud viewer width. + * \param[in] height : Point cloud viewer height. */ -vpDisplayPCL::vpDisplayPCL(unsigned int width, unsigned int height) : m_stop(false), m_verbose(false), m_width(width), m_height(height) { } +vpDisplayPCL::vpDisplayPCL(unsigned int width, unsigned int height) + : m_stop(false), m_verbose(false), m_width(width), m_height(height) +{ } /*! * Destructor that stops and join the viewer thread if not already done. + * + * \sa stop(), startThread() */ vpDisplayPCL::~vpDisplayPCL() { @@ -58,10 +66,10 @@ vpDisplayPCL::~vpDisplayPCL() /*! * Loop that does the display of the point cloud. - * @param[inout] mutex : Shared mutex. + * @param[inout] pointcloud_mutex : Shared mutex to protect from concurrent access to `pointcloud` object. * @param[in] pointcloud : Point cloud to display. */ -void vpDisplayPCL::run(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud) +void vpDisplayPCL::run(std::mutex &pointcloud_mutex, pcl::PointCloud::Ptr pointcloud) { pcl::PointCloud::Ptr local_pointcloud(new pcl::PointCloud()); pcl::visualization::PCLVisualizer::Ptr viewer(new pcl::visualization::PCLVisualizer("3D Viewer")); @@ -70,18 +78,63 @@ void vpDisplayPCL::run(std::mutex &mutex, pcl::PointCloud::Ptr po viewer->setPosition(m_width + 80, m_height + 80); viewer->setCameraPosition(0, 0, -0.25, 0, -1, 0); viewer->setSize(m_width, m_height); + bool init = true; while (!m_stop) { { - std::lock_guard lock(mutex); + std::lock_guard lock(pointcloud_mutex); local_pointcloud = pointcloud->makeShared(); } - // If updatePointCloud fails, it means that the pcl was not previously known by the viewer - if (!viewer->updatePointCloud(local_pointcloud, "sample cloud")) { - // Add the pcl to the list of pcl known by the viewer + the according legend + if (init) { viewer->addPointCloud(local_pointcloud, "sample cloud"); viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "sample cloud"); + + init = false; + } + else { + viewer->updatePointCloud(local_pointcloud, "sample cloud"); + } + + viewer->spinOnce(10); + } + + if (m_verbose) { + std::cout << "End of point cloud display thread" << std::endl; + } +} + +/*! + * Loop that does the display of the textured point cloud. + * @param[inout] mutex : Shared mutex. + * @param[in] pointcloud : Textured point cloud to display. + */ +void vpDisplayPCL::run_color(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud_color) +{ + pcl::PointCloud::Ptr local_pointcloud(new pcl::PointCloud()); + pcl::visualization::PointCloudColorHandlerRGBField rgb(pointcloud_color); + pcl::visualization::PCLVisualizer::Ptr viewer(new pcl::visualization::PCLVisualizer("3D Viewer")); + viewer->setBackgroundColor(0, 0, 0); + viewer->initCameraParameters(); + viewer->setPosition(m_width + 80, m_height + 80); + viewer->setCameraPosition(0, 0, -0.25, 0, -1, 0); + viewer->setSize(m_width, m_height); + bool init = true; + + while (!m_stop) { + { + std::lock_guard lock(mutex); + local_pointcloud = pointcloud_color->makeShared(); + } + + if (init) { + viewer->addPointCloud(local_pointcloud, rgb, "RGB sample cloud"); + viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "RGB sample cloud"); + + init = false; + } + else { + viewer->updatePointCloud(local_pointcloud, rgb, "RGB sample cloud"); } viewer->spinOnce(10); @@ -96,12 +149,26 @@ void vpDisplayPCL::run(std::mutex &mutex, pcl::PointCloud::Ptr po * Start the viewer thread able to display a point cloud. * @param[inout] mutex : Shared mutex. * @param[in] pointcloud : Point cloud to display. + * + * \sa stop() */ void vpDisplayPCL::startThread(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud) { m_thread = std::thread(&vpDisplayPCL::run, this, std::ref(mutex), pointcloud); } +/*! + * Start the viewer thread able to display a textured point cloud. + * @param[inout] mutex : Shared mutex. + * @param[in] pointcloud_color : Textured point cloud to display. + * + * \sa stop() + */ +void vpDisplayPCL::startThread(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud_color) +{ + m_thread = std::thread(&vpDisplayPCL::run_color, this, std::ref(mutex), pointcloud_color); +} + /*! * Stop the viewer thread and join. */ From 196f3be9f01c9f117e209de1136305f8f3c95549 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Fri, 5 Apr 2024 12:40:49 +0200 Subject: [PATCH 27/32] Introduce the possibility to convert aligned color + depth to a textured point cloud --- .../core/include/visp3/core/vpImageConvert.h | 13 +- modules/core/src/image/vpImageConvert_pcl.cpp | 162 ++++++++++++++++-- 2 files changed, 161 insertions(+), 14 deletions(-) diff --git a/modules/core/include/visp3/core/vpImageConvert.h b/modules/core/include/visp3/core/vpImageConvert.h index 00067e793d..08738343ed 100644 --- a/modules/core/include/visp3/core/vpImageConvert.h +++ b/modules/core/include/visp3/core/vpImageConvert.h @@ -144,9 +144,16 @@ class VISP_EXPORT vpImageConvert #endif #if defined(VISP_HAVE_PCL) && defined(VISP_HAVE_THREADS) - static void depthToPointCloud(const vpImage &depth_raw, float depth_scale, const vpCameraParameters &cam_depth, - pcl::PointCloud::Ptr pointcloud, - const vpImage *mask = nullptr, float Z_min = 0.2, float Z_max = 2.5); + static int depthToPointCloud(const vpImage &depth_raw, + float depth_scale, const vpCameraParameters &cam_depth, + pcl::PointCloud::Ptr pointcloud, + std::mutex *pointcloud_mutex = nullptr, + const vpImage *mask = nullptr, float Z_min = 0.2, float Z_max = 2.5); + static int depthToPointCloud(const vpImage &color, const vpImage &depth_raw, + float depth_scale, const vpCameraParameters &cam_depth, + pcl::PointCloud::Ptr pointcloud, + std::mutex *pointcloud_mutex = nullptr, + const vpImage *mask = nullptr, float Z_min = 0.2, float Z_max = 2.5); #endif static void split(const vpImage &src, vpImage *pR, vpImage *pG, diff --git a/modules/core/src/image/vpImageConvert_pcl.cpp b/modules/core/src/image/vpImageConvert_pcl.cpp index f109d5830b..a359dce4c7 100644 --- a/modules/core/src/image/vpImageConvert_pcl.cpp +++ b/modules/core/src/image/vpImageConvert_pcl.cpp @@ -49,36 +49,52 @@ #endif /*! + * Create a point cloud from a depth image. + * * \param[in] depth_raw : Depth raw image. * \param[in] depth_scale : Depth scale to apply to data in `depth_raw`. * \param[in] cam_depth : Depth camera intrinsics. - * \param[out] pointcloud : Computed point cloud. - * \param[in] mask : Optional mask. When set to nullptr, all the pixels in `depth_raw` are considered. Otherwise, - * we consider only pixels that have a mask value that differ from 0. You should ensure that mask size and `depth_raw` - * size are the same. + * \param[out] pointcloud : Computed 3D point cloud. + * The 3D points reconstructed from the raw depth image are those + * that have their corresponding 2D projection in the depth mask and have a Z value within ]Z_min, Z_max[ range. + * When the depth mask is set to nullptr, we reconstruct all 3D points from the complete depth raw image and + * retain only those whose Z value lies between ]Z_min, Z_max[ range. + * You must also ensure that the size of the depth mask and the size of the depth raw image are the same. + * \param[inout] pointcloud_mutex : Optional mutex to protect from concurrent access to `pointcloud`. When set to + * nullptr, you should ensure that there is no thread that wants to access to `pointcloud`, like for example + * the one used in vpDisplayPCL. + * \param[in] depth_mask : Optional depth_mask. When set to nullptr, all the pixels in `depth_raw` are considered. Otherwise, + * we consider only pixels that have a mask value that differ from 0. * \param[in] Z_min : Min Z value to retain the 3D point in the point cloud. * \param[in] Z_max : Max Z value to retain the 3D point in the point cloud. + * + * \return The size of the point cloud. */ -void vpImageConvert::depthToPointCloud(const vpImage &depth_raw, float depth_scale, +int vpImageConvert::depthToPointCloud(const vpImage &depth_raw, float depth_scale, const vpCameraParameters &cam_depth, - pcl::PointCloud::Ptr pointcloud, - const vpImage *mask, float Z_min, float Z_max) + pcl::PointCloud::Ptr pointcloud, std::mutex *pointcloud_mutex, + const vpImage *depth_mask, float Z_min, float Z_max) { - pointcloud->clear(); + int size = static_cast(depth_raw.getSize()); unsigned int width = depth_raw.getWidth(); unsigned int height = depth_raw.getHeight(); + int pcl_size = 0; - if (mask) { - if ((width != mask->getWidth()) || (height != mask->getHeight())) { + if (depth_mask) { + if ((width != depth_mask->getWidth()) || (height != depth_mask->getHeight())) { throw(vpImageException(vpImageException::notInitializedError, "Depth image and mask size differ")); } + if (pointcloud_mutex) { + pointcloud_mutex->lock(); + } + pointcloud->clear(); #if defined(_OPENMP) std::mutex mutex; #pragma omp parallel for #endif for (int p = 0; p < size; ++p) { - if (mask->bitmap[p]) { + if (depth_mask->bitmap[p]) { if (static_cast(depth_raw.bitmap[p])) { float Z = static_cast(depth_raw.bitmap[p]) * depth_scale; if (Z < Z_max) { @@ -98,8 +114,16 @@ void vpImageConvert::depthToPointCloud(const vpImage &depth_raw, float } } } + pcl_size = pointcloud->size(); + if (pointcloud_mutex) { + pointcloud_mutex->unlock(); + } } else { + if (pointcloud_mutex) { + pointcloud_mutex->lock(); + } + pointcloud->clear(); #if defined(_OPENMP) std::mutex mutex; #pragma omp parallel for @@ -123,6 +147,122 @@ void vpImageConvert::depthToPointCloud(const vpImage &depth_raw, float } } } + pcl_size = pointcloud->size(); + if (pointcloud_mutex) { + pointcloud_mutex->unlock(); + } } + + return pcl_size; +} + +/*! + * Create a textured point cloud from an aligned color and depth image. + * + * \param[in] color : Color image. + * \param[in] depth_raw : Depth raw image. + * \param[in] depth_scale : Depth scale to apply to data in `depth_raw`. + * \param[in] cam_depth : Depth camera intrinsics. + * \param[out] pointcloud : Computed 3D point cloud with RGB information. + * The 3D points reconstructed from the raw depth image are those + * that have their corresponding 2D projection in the depth mask and have a Z value within ]Z_min, Z_max[ range. + * When the depth mask is set to nullptr, we reconstruct all 3D points from the complete depth raw image and + * retain only those whose Z value lies between ]Z_min, Z_max[ range. + * \param[inout] pointcloud_mutex : Optional mutex to protect from concurrent access to `pointcloud`. When set to + * nullptr, you should ensure that there is no thread that wants to access to `pointcloud`, like for example + * the one used in vpDisplayPCL. + * \param[in] depth_mask : Optional depth_mask. When set to nullptr, all the pixels in `depth_raw` are considered. Otherwise, + * we consider only pixels that have a mask value that differ from 0 and that a Z value in ]Z_min, Z_max[] range. + * You should also ensure that mask size and `depth_raw` size are the same. + * \param[in] Z_min : Min Z value to retain the 3D point in the point cloud. + * \param[in] Z_max : Max Z value to retain the 3D point in the point cloud. + * + * \return The size of the point cloud. + */ +int vpImageConvert::depthToPointCloud(const vpImage &color, const vpImage &depth_raw, + float depth_scale, const vpCameraParameters &cam_depth, + pcl::PointCloud::Ptr pointcloud, std::mutex *pointcloud_mutex, + const vpImage *depth_mask, float Z_min, float Z_max) +{ + int size = static_cast(depth_raw.getSize()); + unsigned int width = depth_raw.getWidth(); + unsigned int height = depth_raw.getHeight(); + int pcl_size = 0; + + if (depth_mask) { + if ((width != depth_mask->getWidth()) || (height != depth_mask->getHeight())) { + throw(vpImageException(vpImageException::notInitializedError, "Depth image and mask size differ")); + } + if (pointcloud_mutex) { + pointcloud_mutex->lock(); + } + pointcloud->clear(); +#if defined(_OPENMP) + std::mutex mutex; +#pragma omp parallel for +#endif + for (int p = 0; p < size; ++p) { + if (depth_mask->bitmap[p]) { + if (static_cast(depth_raw.bitmap[p])) { + float Z = static_cast(depth_raw.bitmap[p]) * depth_scale; + if (Z < Z_max) { + double x = 0; + double y = 0; + unsigned int j = p % width; + unsigned int i = (p - j) / width; + vpPixelMeterConversion::convertPoint(cam_depth, j, i, x, y); + vpColVector point_3D({ x * Z, y * Z, Z }); + if (point_3D[2] > Z_min) { +#if defined(_OPENMP) + std::lock_guard lock(mutex); +#endif + pointcloud->push_back(pcl::PointXYZRGB(point_3D[0], point_3D[1], point_3D[2], + color.bitmap[p].R, color.bitmap[p].G, color.bitmap[p].B)); + } + } + } + } + } + pcl_size = pointcloud->size(); + if (pointcloud_mutex) { + pointcloud_mutex->unlock(); + } + } + else { + if (pointcloud_mutex) { + pointcloud_mutex->lock(); + } + pointcloud->clear(); +#if defined(_OPENMP) + std::mutex mutex; +#pragma omp parallel for +#endif + for (int p = 0; p < size; ++p) { + if (static_cast(depth_raw.bitmap[p])) { + float Z = static_cast(depth_raw.bitmap[p]) * depth_scale; + if (Z < 2.5) { + double x = 0; + double y = 0; + unsigned int j = p % width; + unsigned int i = (p - j) / width; + vpPixelMeterConversion::convertPoint(cam_depth, j, i, x, y); + vpColVector point_3D({ x * Z, y * Z, Z, 1 }); + if (point_3D[2] >= 0.1) { +#if defined(_OPENMP) + std::lock_guard lock(mutex); +#endif + pointcloud->push_back(pcl::PointXYZRGB(point_3D[0], point_3D[1], point_3D[2], + color.bitmap[p].R, color.bitmap[p].G, color.bitmap[p].B)); + } + } + } + } + pcl_size = pointcloud->size(); + if (pointcloud_mutex) { + pointcloud_mutex->unlock(); + } + } + + return pcl_size; } #endif From a1ca3e06b7f1d32a2c17282112cfe407683aa3a2 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Fri, 5 Apr 2024 12:42:06 +0200 Subject: [PATCH 28/32] Improve hsv tutorials - new command line options - protect against libx11 enabled or not - introduce textured and untextures point cloud visualization - introduce mutex to protect point cloud access --- .../color/tutorial-hsv-range-tuner.cpp | 15 +-- .../color/tutorial-hsv-segmentation-basic.cpp | 2 + .../tutorial-hsv-segmentation-pcl-viewer.cpp | 112 +++++++++++++----- .../color/tutorial-hsv-segmentation-pcl.cpp | 15 ++- .../color/tutorial-hsv-segmentation.cpp | 8 +- 5 files changed, 107 insertions(+), 45 deletions(-) diff --git a/tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp b/tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp index 77b869c717..6577fa8465 100644 --- a/tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-range-tuner.cpp @@ -4,7 +4,7 @@ #include -#if defined(HAVE_OPENCV_HIGHGUI) +#if defined(HAVE_OPENCV_HIGHGUI) && defined(VISP_HAVE_X11) #include #include @@ -82,14 +82,8 @@ int main(int argc, char *argv[]) std::string opt_img_filename; bool show_helper = false; for (int i = 1; i < argc; i++) { - if (std::string(argv[i]) == "--hsv-thresholds") { - if ((i+1) < argc) { - opt_hsv_filename = std::string(argv[++i]); - } - else { - show_helper = true; - std::cout << "ERROR \nMissing yaml filename after parameter " << std::string(argv[i]) << std::endl; - } + if ((std::string(argv[i]) == "--hsv-thresholds") && ((i+1) < argc)) { + opt_hsv_filename = std::string(argv[++i]); } else if (std::string(argv[i]) == "--image") { if ((i+1) < argc) { @@ -329,6 +323,9 @@ int main() { #if !defined(HAVE_OPENCV_HIGHGUI) std::cout << "This tutorial needs OpenCV highgui module as 3rd party." << std::endl; +#endif +#if !defined(VISP_HAVE_X11) + std::cout << "This tutorial needs X11 3rd party enabled." << std::endl; #endif std::cout << "Install missing 3rd parties, configure and rebuild ViSP." << std::endl; return EXIT_SUCCESS; diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation-basic.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation-basic.cpp index 20a25cd611..e5ed2863ae 100644 --- a/tutorial/segmentation/color/tutorial-hsv-segmentation-basic.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation-basic.cpp @@ -48,6 +48,7 @@ int main() vpImage I_segmented(height, width); vpImageTools::inMask(I, mask, I_segmented); +#if defined(VISP_HAVE_X11) vpDisplayX d_I(I, 0, 0, "Current frame"); vpDisplayX d_mask(mask, I.getWidth()+75, 0, "HSV mask"); vpDisplayX d_I_segmented(I_segmented, 2*mask.getWidth()+80, 0, "Segmented frame"); @@ -59,4 +60,5 @@ int main() vpDisplay::flush(mask); vpDisplay::flush(I_segmented); vpDisplay::getClick(I); +#endif } diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp index e756891ac5..a7ceae34e4 100644 --- a/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl-viewer.cpp @@ -3,7 +3,7 @@ #include #include -#if defined(VISP_HAVE_REALSENSE2) && defined(VISP_HAVE_PCL) && defined(VISP_HAVE_THREADS) +#if defined(VISP_HAVE_REALSENSE2) && defined(VISP_HAVE_PCL) && defined(VISP_HAVE_THREADS) && defined(VISP_HAVE_X11) #include #include #include @@ -18,18 +18,58 @@ int main(int argc, char **argv) { std::string opt_hsv_filename = "calib/hsv-thresholds.yml"; - - for (int i = 0; i < argc; i++) { - if (std::string(argv[i]) == "--hsv-thresholds") { + bool opt_pcl_textured = false; + bool opt_verbose = false; + int opt_width = 848; + int opt_height = 480; + int opt_fps = 60; + + for (int i = 1; i < argc; i++) { + if (((std::string(argv[i]) == "--width") || (std::string(argv[i]) == "-v")) && ((i+1) < argc)) { + opt_width = std::atoi(argv[++i]); + } + else if (((std::string(argv[i]) == "--height") || (std::string(argv[i]) == "-h")) && ((i+1) < argc)) { + opt_height = std::atoi(argv[++i]); + } + else if ((std::string(argv[i]) == "--fps") && ((i+1) < argc)) { + opt_fps = std::atoi(argv[++i]); + } + else if (std::string(argv[i]) == "--texture") { + opt_pcl_textured = true; + } + else if ((std::string(argv[i]) == "--hsv-thresholds") && ((i+1) < argc)) { opt_hsv_filename = std::string(argv[++i]); } - else if (std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") { + else if ((std::string(argv[i]) == "--verbose") || (std::string(argv[i]) == "-v")) { + opt_verbose = true; + } + else if ((std::string(argv[i]) == "--help") || (std::string(argv[i]) == "-h")) { std::cout << "\nSYNOPSIS " << std::endl << argv[0] + << " [--width,-w ]" + << " [--height,-h ]" + << " [--fps ]" + << " [--texture]" << " [--hsv-thresholds ]" + << " [--verbose,-v]" << " [--help,-h]" << std::endl; std::cout << "\nOPTIONS " << std::endl + << " --width,-w " << std::endl + << " Realsense camera image width." << std::endl + << " Default: " << opt_width << std::endl + << std::endl + << " --height,-h " << std::endl + << " Realsense camera image height." << std::endl + << " Default: " << opt_height << std::endl + << std::endl + << " --fps " << std::endl + << " Realsense camera framerate." << std::endl + << " Default: " << opt_fps << std::endl + << std::endl + << " --texture" << std::endl + << " Enable textured point cloud adding RGB information to the 3D point." << std::endl + << std::endl << " --hsv-thresholds " << std::endl << " Path to a yaml filename that contains H , S , V threshold values." << std::endl << " An Example of such a file could be:" << std::endl @@ -43,6 +83,9 @@ int main(int argc, char **argv) << " - [148]" << std::endl << " - [208]" << std::endl << std::endl + << " --verbose, -v" << std::endl + << " Enable verbose mode." << std::endl + << std::endl << " --help, -h" << std::endl << " Display this helper message." << std::endl << std::endl; @@ -60,11 +103,10 @@ int main(int argc, char **argv) return EXIT_FAILURE; } - int width = 848, height = 480, fps = 60; vpRealSense2 rs; rs2::config config; - config.enable_stream(RS2_STREAM_COLOR, width, height, RS2_FORMAT_RGBA8, fps); - config.enable_stream(RS2_STREAM_DEPTH, width, height, RS2_FORMAT_Z16, fps); + config.enable_stream(RS2_STREAM_COLOR, opt_width, opt_height, RS2_FORMAT_RGBA8, opt_fps); + config.enable_stream(RS2_STREAM_DEPTH, opt_width, opt_height, RS2_FORMAT_Z16, opt_fps); config.disable_stream(RS2_STREAM_INFRARED, 1); config.disable_stream(RS2_STREAM_INFRARED, 2); rs2::align align_to(RS2_STREAM_COLOR); @@ -75,13 +117,13 @@ int main(int argc, char **argv) vpCameraParameters cam_depth = rs.getCameraParameters(RS2_STREAM_DEPTH, vpCameraParameters::perspectiveProjWithoutDistortion); - vpImage I(height, width); - vpImage H(height, width); - vpImage S(height, width); - vpImage V(height, width); - vpImage mask(height, width); - vpImage depth_raw(height, width); - vpImage I_segmented(height, width); + vpImage I(opt_height, opt_width); + vpImage H(opt_height, opt_width); + vpImage S(opt_height, opt_width); + vpImage V(opt_height, opt_width); + vpImage mask(opt_height, opt_width); + vpImage depth_raw(opt_height, opt_width); + vpImage I_segmented(opt_height, opt_width); vpDisplayX d_I(I, 0, 0, "Current frame"); vpDisplayX d_I_segmented(I_segmented, I.getWidth()+75, 0, "HSV segmented frame"); @@ -91,19 +133,27 @@ int main(int argc, char **argv) long nb_iter = 0; float Z_min = 0.1; float Z_max = 2.5; - int pcl_size = 0; - pcl::PointCloud::Ptr pointcloud = pcl::PointCloud::Ptr(new pcl::PointCloud); + //! [Create point cloud] + pcl::PointCloud::Ptr pointcloud_color(new pcl::PointCloud()); + pcl::PointCloud::Ptr pointcloud(new pcl::PointCloud()); + //! [Create point cloud] //! [Create pcl viewer object] - std::mutex pcl_viewer_mutex; - vpDisplayPCL pcl_viewer; - pcl_viewer.startThread(std::ref(pcl_viewer_mutex), pointcloud); + std::mutex pointcloud_mutex; + vpDisplayPCL pcl_viewer(opt_width, opt_height); + if (opt_pcl_textured) { + pcl_viewer.startThread(std::ref(pointcloud_mutex), pointcloud_color); + } + else { + pcl_viewer.startThread(std::ref(pointcloud_mutex), pointcloud); + } //! [Create pcl viewer object] while (!quit) { double t = vpTime::measureTimeMs(); rs.acquire((unsigned char *)I.bitmap, (unsigned char *)(depth_raw.bitmap), NULL, NULL, &align_to); + vpImageConvert::RGBaToHSV(reinterpret_cast(I.bitmap), reinterpret_cast(H.bitmap), reinterpret_cast(S.bitmap), @@ -118,15 +168,18 @@ int main(int argc, char **argv) vpImageTools::inMask(I, mask, I_segmented); - { - //! [Update point cloud with mutex protection] - std::lock_guard lock(pcl_viewer_mutex); - vpImageConvert::depthToPointCloud(depth_raw, depth_scale, cam_depth, pointcloud, &mask, Z_min, Z_max); - pcl_size = pointcloud->size(); - //! [Update point cloud with mutex protection] + //! [Update point cloud with mutex protection] + int pcl_size; + if (opt_pcl_textured) { + pcl_size = vpImageConvert::depthToPointCloud(I, depth_raw, depth_scale, cam_depth, pointcloud_color, &pointcloud_mutex, &mask, Z_min, Z_max); } - - std::cout << "Segmented point cloud size: " << pcl_size << std::endl; + else { + pcl_size = vpImageConvert::depthToPointCloud(depth_raw, depth_scale, cam_depth, pointcloud, &pointcloud_mutex, &mask, Z_min, Z_max); + } + if (opt_verbose) { + std::cout << "Segmented point cloud size: " << pcl_size << std::endl; + } + //! [Update point cloud with mutex protection] vpDisplay::display(I); vpDisplay::display(I_segmented); @@ -154,6 +207,9 @@ int main() #endif #if !defined(VISP_HAVE_PCL) std::cout << "This tutorial needs pcl library as 3rd party." << std::endl; +#endif +#if !defined(VISP_HAVE_X11) + std::cout << "This tutorial needs X11 3rd party enabled." << std::endl; #endif std::cout << "Install missing 3rd party, configure and rebuild ViSP." << std::endl; return EXIT_SUCCESS; diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp index ddb0a01abb..4742cc148d 100644 --- a/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.cpp @@ -3,7 +3,7 @@ #include #include -#if defined(VISP_HAVE_REALSENSE2) && defined(VISP_HAVE_PCL) +#if defined(VISP_HAVE_REALSENSE2) && defined(VISP_HAVE_PCL) && defined(VISP_HAVE_X11) #include #include #include @@ -16,8 +16,8 @@ int main(int argc, char **argv) { std::string opt_hsv_filename = "calib/hsv-thresholds.yml"; - for (int i = 0; i < argc; i++) { - if (std::string(argv[i]) == "--hsv-thresholds") { + for (int i = 1; i < argc; i++) { + if ((std::string(argv[i]) == "--hsv-thresholds") && ((i+1) < argc)) { opt_hsv_filename = std::string(argv[++i]); } else if (std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") { @@ -57,8 +57,8 @@ int main(int argc, char **argv) return EXIT_FAILURE; } - int width = 848, height = 480, fps = 60; //! [Config RS2 RGB and depth] + int width = 848, height = 480, fps = 60; vpRealSense2 rs; rs2::config config; config.enable_stream(RS2_STREAM_COLOR, width, height, RS2_FORMAT_RGBA8, fps); @@ -94,7 +94,7 @@ int main(int argc, char **argv) //! [Allocate point cloud] float Z_min = 0.1; float Z_max = 2.5; - pcl::PointCloud::Ptr pointcloud = pcl::PointCloud::Ptr(new pcl::PointCloud); + pcl::PointCloud::Ptr pointcloud(new pcl::PointCloud()); //! [Allocate point cloud] while (!quit) { @@ -122,7 +122,7 @@ int main(int argc, char **argv) vpImageTools::inMask(I, mask, I_segmented); //! [Update point cloud] - vpImageConvert::depthToPointCloud(depth_raw, depth_scale, cam_depth, pointcloud, &mask, Z_min, Z_max); + vpImageConvert::depthToPointCloud(depth_raw, depth_scale, cam_depth, pointcloud, nullptr, &mask, Z_min, Z_max); //! [Update point cloud] //! [Get point cloud size] @@ -157,6 +157,9 @@ int main() #endif #if !defined(VISP_HAVE_PCL) std::cout << "This tutorial needs pcl library as 3rd party." << std::endl; +#endif +#if !defined(VISP_HAVE_X11) + std::cout << "This tutorial needs X11 3rd party enabled." << std::endl; #endif std::cout << "Install missing 3rd party, configure and rebuild ViSP." << std::endl; return EXIT_SUCCESS; diff --git a/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp b/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp index 2c928c864c..b9f0b6002d 100644 --- a/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp +++ b/tutorial/segmentation/color/tutorial-hsv-segmentation.cpp @@ -14,12 +14,13 @@ int main(int argc, char **argv) { +#if defined(VISP_HAVE_X11) std::string opt_hsv_filename = "calib/hsv-thresholds.yml"; std::string opt_video_filename; bool show_helper = false; - for (int i = 0; i < argc; i++) { - if (std::string(argv[i]) == "--hsv-thresholds") { + for (int i = 1; i < argc; i++) { + if ((std::string(argv[i]) == "--hsv-thresholds") && ((i+1) < argc)) { opt_hsv_filename = std::string(argv[++i]); } else if (std::string(argv[i]) == "--video") { @@ -177,5 +178,8 @@ int main(int argc, char **argv) } std::cout << "Mean loop time: " << total_loop_time / nb_iter << std::endl; +#else + std::cout << "This tutorial needs X11 3rdparty that is not enabled" << std::endl; +#endif return EXIT_SUCCESS; } From 8e7b1b8d96a499897f2626c0140d2b1a21149247 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Fri, 5 Apr 2024 12:44:36 +0200 Subject: [PATCH 29/32] Update and introduce a video to illustrate pcl viewer --- .../color/tutorial-hsv-segmentation-pcl.dox | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/doc/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.dox b/doc/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.dox index aad1a64ca4..5fc3292827 100644 --- a/doc/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.dox +++ b/doc/tutorial/segmentation/color/tutorial-hsv-segmentation-pcl.dox @@ -31,19 +31,21 @@ device. In our case we will consider a Realsense camera. The pipeline becomes: \snippet tutorial-hsv-segmentation-pcl.cpp Config RS2 RGB and depth - get depth scale associated to the depth images and device intrisics \snippet tutorial-hsv-segmentation-pcl.cpp Get RS2 intrinsics -- grab color and depth images +- grab color and depth aligned images \snippet tutorial-hsv-segmentation-pcl.cpp Grab color and depth - convert from RGB to HSV channels using vpImageConvert::RGBaToHSV() \snippet tutorial-hsv-segmentation-pcl.cpp RGB to HSV - create a mask with pixels that are in the low/high HSV ranges using vpImageTools::inRange() \snippet tutorial-hsv-segmentation-pcl.cpp Create mask -- create the point cloud object +- create the point cloud object. Note here the `Z_min` and `Z_max` variables, which are used further in the code + to remove 3D points that are too close or too far from the camera \snippet tutorial-hsv-segmentation-pcl.cpp Allocate point cloud - using the mask and the depth image update the point cloud using vpImageConvert::depthToPointCloud(). The corresponding point cloud is available in `pointcloud` variable. \snippet tutorial-hsv-segmentation-pcl.cpp Update point cloud - to know the size of the point cloud use \snippet tutorial-hsv-segmentation-pcl.cpp Get point cloud size + Instead, you can also use the value returned by vpImageConvert::depthToPointCloud() to get the point cloud size. . All these steps are implemented in tutorial-hsv-segmentation-pcl.cpp @@ -59,21 +61,40 @@ In the next section we show how to display the point cloud. \section hsv_pcl-viewer Point could viewer In tutorial-hsv-segmentation-pcl-viewer.cpp you will find an extension of the previous code with the introduction -of vpDisplayPCL class that allows to visualize the point cloud in 3D. +of vpDisplayPCL class that allows to visualize the point cloud in 3D with optionally additional texture information +taken from the color image. To add the point cloud viewer feature: -- First you need to include vpDisplayPCL header +- Fisrt you need to include vpDisplayPCL header \snippet tutorial-hsv-segmentation-pcl-viewer.cpp Include vpDisplayPCL header -- Next, you need to create a mutex and the PCL viewer object before launching the viewer thread +- Then, we declare two pcl objects for textured and untextured point clouds respectively +\snippet tutorial-hsv-segmentation-pcl-viewer.cpp Create point cloud +- Since the point cloud viewer will run in a separate thread, to avoid concurrent access to `pointcloud` or + `pointcloud_color` objects, we introduce a mutex, declare the PCL viewer object and launch the viewer thread, either + for textured or untextured point clouds \snippet tutorial-hsv-segmentation-pcl-viewer.cpp Create pcl viewer object - In the `while()` loop we update the point cloud using the mutex shared with the pcl viewer \snippet tutorial-hsv-segmentation-pcl-viewer.cpp Update point cloud with mutex protection - +. To run this tutorial +- Learn the color of the object from which you want to extract the corresponding point cloud \verbatim $ cd $VISP_WS/visp-build/tutorial/segmentation/color -$ ./tutorial-hsv-segmentation-pcl-viewer --hsv-thresholds calib/hsv-thresholds.yml +$ ./tutorial-hsv-range-tuner --hsv-thresholds calib/hsv-thresholds.yml +\endverbatim +- Visualize to corresponding point cloud +\verbatim +$ ./tutorial-hsv-segmentation-pcl-viewer --hsv-thresholds calib/hsv-thresholds.yml --texture \endverbatim + At this point, you should see something similar to the following video, where the top left image corresponds to the + live stream provided by a Realsense D435 camera, the top right image to the HSV yellow color segmentation, and the + bottom right image to the PCL point cloud viewer used to visualize the textured point cloud corresponding to the + yellow pixels in real time. +\htmlonly +

+ +

+\endhtmlonly \section hsv_pcl_issue Known issue \subsection hsv_pcl_issue_segfault Segfault in PCL viewer From e34367a432113d9711fcf527d645fde10c026ca1 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Fri, 5 Apr 2024 16:36:33 +0200 Subject: [PATCH 30/32] Replace/remove ViewerWorker with vpDisplayPCL new class --- .../device/framegrabber/grabRealSense2.cpp | 116 +----------- .../device/framegrabber/readRealSenseData.cpp | 170 ++++++------------ .../device/framegrabber/saveRealSenseData.cpp | 114 ++++++------ modules/gui/include/visp3/gui/vpDisplayPCL.h | 12 +- modules/gui/src/pointcloud/vpDisplayPCL.cpp | 82 ++++++--- .../rgb-depth/testRealSense2_D435_pcl.cpp | 166 ++++++----------- 6 files changed, 247 insertions(+), 413 deletions(-) diff --git a/example/device/framegrabber/grabRealSense2.cpp b/example/device/framegrabber/grabRealSense2.cpp index 19f3f5bd15..64a6633658 100644 --- a/example/device/framegrabber/grabRealSense2.cpp +++ b/example/device/framegrabber/grabRealSense2.cpp @@ -42,106 +42,14 @@ #include #include -#include #if defined(VISP_HAVE_REALSENSE2) && (defined(VISP_HAVE_X11) || defined(VISP_HAVE_GDI)) -#if defined(VISP_HAVE_PCL) && defined(VISP_HAVE_THREADS) -#include -#include - -#include -#include - -namespace -{ -// Global variables -pcl::PointCloud::Ptr pointcloud(new pcl::PointCloud()); -pcl::PointCloud::Ptr pointcloud_color(new pcl::PointCloud()); -bool cancelled = false, update_pointcloud = false; - -class ViewerWorker -{ -public: - explicit ViewerWorker(bool color_mode, std::mutex &mutex) : m_colorMode(color_mode), m_mutex(mutex) { } - - void run() - { - std::string date = vpTime::getDateTime(); - pcl::visualization::PCLVisualizer::Ptr viewer(new pcl::visualization::PCLVisualizer("3D Viewer " + date)); - pcl::visualization::PointCloudColorHandlerRGBField rgb(pointcloud_color); - pcl::PointCloud::Ptr local_pointcloud(new pcl::PointCloud()); - pcl::PointCloud::Ptr local_pointcloud_color(new pcl::PointCloud()); - - viewer->setBackgroundColor(0, 0, 0); - viewer->initCameraParameters(); - viewer->setPosition(640 + 80, 480 + 80); - viewer->setCameraPosition(0, 0, -0.25, 0, -1, 0); - viewer->setSize(640, 480); - - bool init = true; - bool local_update = false, local_cancelled = false; - while (!local_cancelled) { - { - std::unique_lock lock(m_mutex, std::try_to_lock); - - if (lock.owns_lock()) { - local_update = update_pointcloud; - update_pointcloud = false; - local_cancelled = cancelled; - - if (local_update) { - if (m_colorMode) { - local_pointcloud_color = pointcloud_color->makeShared(); - } - else { - local_pointcloud = pointcloud->makeShared(); - } - } - } - } - - if (local_update && !local_cancelled) { - local_update = false; - - if (init) { - if (m_colorMode) { - viewer->addPointCloud(local_pointcloud_color, rgb, "RGB sample cloud"); - viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, - "RGB sample cloud"); - } - else { - viewer->addPointCloud(local_pointcloud, "sample cloud"); - viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "sample cloud"); - } - init = false; - } - else { - if (m_colorMode) { - viewer->updatePointCloud(local_pointcloud_color, rgb, "RGB sample cloud"); - } - else { - viewer->updatePointCloud(local_pointcloud, "sample cloud"); - } - } - } - - viewer->spinOnce(5); - } - - std::cout << "End of point cloud display thread" << std::endl; - } - -private: - bool m_colorMode; - std::mutex &m_mutex; -}; -} // namespace -#endif - +#include #include #include #include +#include #include int main() @@ -181,18 +89,19 @@ int main() #if defined(VISP_HAVE_PCL) && defined(VISP_HAVE_THREADS) std::mutex mutex; - ViewerWorker viewer(true, mutex); - std::thread viewer_thread(&ViewerWorker::run, &viewer); + pcl::PointCloud::Ptr pointcloud_color(new pcl::PointCloud()); + vpDisplayPCL pcl_viewer(color.getWidth() + 80, color.getHeight() + 70, "3D viewer " + vpTime::getDateTime()); + pcl_viewer.startThread(std::ref(mutex), pointcloud_color); #endif #if defined(VISP_HAVE_X11) vpDisplayX dc(color, 10, 10, "Color image"); vpDisplayX di(infrared, (int)color.getWidth() + 80, 10, "Infrared image"); - vpDisplayX dd(depth_display, 10, (int)color.getHeight() + 80, "Depth image"); + vpDisplayX dd(depth_display, 10, (int)color.getHeight() + 70, "Depth image"); #elif defined(VISP_HAVE_GDI) vpDisplayGDI dc(color, 10, 10, "Color image"); vpDisplayGDI di(infrared, color.getWidth() + 80, 10, "Infrared image"); - vpDisplayGDI dd(depth_display, 10, color.getHeight() + 80, "Depth image"); + vpDisplayGDI dd(depth_display, 10, color.getHeight() + 70, "Depth image"); #endif while (true) { @@ -203,7 +112,6 @@ int main() std::lock_guard lock(mutex); rs.acquire((unsigned char *)color.bitmap, (unsigned char *)depth.bitmap, nullptr, pointcloud_color, (unsigned char *)infrared.bitmap); - update_pointcloud = true; } #else rs.acquire((unsigned char *)color.bitmap, (unsigned char *)depth.bitmap, nullptr, (unsigned char *)infrared.bitmap); @@ -228,16 +136,6 @@ int main() } std::cout << "RealSense sensor characteristics: \n" << rs << std::endl; - -#if defined(VISP_HAVE_PCL) && defined(VISP_HAVE_THREADS) - { - std::lock_guard lock(mutex); - cancelled = true; - } - - viewer_thread.join(); -#endif - } catch (const vpException &e) { std::cerr << "RealSense error " << e.what() << std::endl; diff --git a/example/device/framegrabber/readRealSenseData.cpp b/example/device/framegrabber/readRealSenseData.cpp index 5fe2cda11e..2219719383 100644 --- a/example/device/framegrabber/readRealSenseData.cpp +++ b/example/device/framegrabber/readRealSenseData.cpp @@ -47,22 +47,19 @@ #include #include -#ifdef VISP_HAVE_PCL -#include -#include -#include - -#define USE_PCL_VIEWER -#endif - #include #include #include #include +#include #include #include #include +#if defined(VISP_HAVE_PCL) +#include +#endif + #define GETOPTARGS "ci:bodh" namespace @@ -70,35 +67,45 @@ namespace void usage(const char *name, const char *badparam) { - fprintf(stdout, "\n\ - Read RealSense data.\n\ - \n\ - %s\ - OPTIONS: \n\ - -i \n\ - Input directory.\n\ - \n\ - -c \n\ - Click enable.\n\ - \n\ - -b \n\ - Pointcloud is in binary format.\n\ - \n\ - -o \n\ - Save color and depth side by side to image sequence.\n\ - \n\ - -d \n\ - Display depth in color.\n\ - \n\ - -h \n\ - Print the help.\n\n", - name); - - if (badparam) - fprintf(stdout, "\nERROR: Bad parameter [%s]\n", badparam); + std::cout << "\nNAME " << std::endl + << " " << vpIoTools::getName(name) + << " - Read data acquired with a Realsense device." << std::endl + << std::endl + << "SYNOPSIS " << std::endl + << " " << name + << " [--i ]" + << " [-c]" + << " [-b]" + << " [-o]" + << " [-d]" + << " [--help,-h]" + << std::endl; + std::cout << "\nOPTIONS " << std::endl + << " --i " << std::endl + << " Input folder that contains the data to read." << std::endl + << std::endl + << " -c" << std::endl + << " Flag to display data in step by step mode triggered by a user click." << std::endl + << std::endl + << " -b" << std::endl + << " Point cloud stream is saved in binary format." << std::endl + << std::endl + << " -o" << std::endl + << " Save color images in png format in a new folder." << std::endl + << std::endl + << " -d" << std::endl + << " Display depth in color." << std::endl + << std::endl + << " --help, -h" << std::endl + << " Display this helper message." << std::endl + << std::endl; + + if (badparam) { + std::cout << "\nERROR: Bad parameter " << badparam << std::endl; + } } -bool getOptions(int argc, char **argv, std::string &input_directory, bool &click, bool &pointcloud_binary_format, +bool getOptions(int argc, const char *argv[], std::string &input_directory, bool &click, bool &pointcloud_binary_format, bool &save_video, bool &color_depth) { const char *optarg; @@ -146,69 +153,10 @@ bool getOptions(int argc, char **argv, std::string &input_directory, bool &click return true; } -#ifdef USE_PCL_VIEWER -pcl::PointCloud::Ptr pointcloud(new pcl::PointCloud()); -bool cancelled = false, update_pointcloud = false; - -class ViewerWorker -{ -public: - explicit ViewerWorker(std::mutex &mutex) : m_mutex(mutex) { } - - void run() - { - pcl::PointCloud::Ptr local_pointcloud(new pcl::PointCloud()); - - bool local_update = false, local_cancelled = false; - pcl::visualization::PCLVisualizer::Ptr viewer(new pcl::visualization::PCLVisualizer("3D Viewer")); - viewer->setBackgroundColor(0, 0, 0); - viewer->initCameraParameters(); - viewer->setPosition(640 + 80, 480 + 80); - viewer->setCameraPosition(0, 0, -0.25, 0, -1, 0); - viewer->setSize(640, 480); - - bool first_init = true; - while (!local_cancelled) { - { - std::unique_lock lock(m_mutex, std::try_to_lock); - - if (lock.owns_lock()) { - local_update = update_pointcloud; - update_pointcloud = false; - local_cancelled = cancelled; - local_pointcloud = pointcloud->makeShared(); - } - } - - if (local_update && !local_cancelled) { - local_update = false; - - if (first_init) { - viewer->addPointCloud(local_pointcloud, "sample cloud"); - viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "sample cloud"); - first_init = false; - } - else { - viewer->updatePointCloud(local_pointcloud, "sample cloud"); - } - } - - viewer->spinOnce(10); - } - - std::cout << "End of point cloud display thread" << std::endl; - } - -private: - std::mutex &m_mutex; -}; -#endif - bool readData(int cpt, const std::string &input_directory, vpImage &I_color, vpImage &I_depth_raw, bool pointcloud_binary_format -#ifdef USE_PCL_VIEWER - , - pcl::PointCloud::Ptr point_cloud +#if defined(VISP_HAVE_PCL) + , pcl::PointCloud::Ptr point_cloud #endif ) { @@ -256,7 +204,7 @@ bool readData(int cpt, const std::string &input_directory, vpImage &I_co } // Read pointcloud -#ifdef USE_PCL_VIEWER +#if defined(VISP_HAVE_PCL) if (pointcloud_binary_format) { std::ifstream file_pointcloud(filename_pointcloud.c_str(), std::ios::in | std::ios::binary); if (!file_pointcloud.is_open()) { @@ -298,7 +246,7 @@ bool readData(int cpt, const std::string &input_directory, vpImage &I_co } } // Namespace -int main(int argc, char *argv[]) +int main(int argc, const char *argv[]) { std::string input_directory = ""; bool click = false; @@ -322,10 +270,10 @@ int main(int argc, char *argv[]) #endif bool init_display = false; -#ifdef USE_PCL_VIEWER +#if defined(VISP_HAVE_PCL) std::mutex mutex; - ViewerWorker viewer(mutex); - std::thread viewer_thread(&ViewerWorker::run, &viewer); + pcl::PointCloud::Ptr pointcloud(new pcl::PointCloud()); + vpDisplayPCL pcl_viewer; #endif vpVideoWriter writer; @@ -341,10 +289,9 @@ int main(int argc, char *argv[]) while (!quit) { double t = vpTime::measureTimeMs(); -#ifdef USE_PCL_VIEWER +#if defined(VISP_HAVE_PCL) { std::lock_guard lock(mutex); - update_pointcloud = true; quit = !readData(cpt_frame, input_directory, I_color, I_depth_raw, pointcloud_binary_format, pointcloud); } #else @@ -358,10 +305,15 @@ int main(int argc, char *argv[]) if (!init_display) { init_display = true; d1.init(I_color, 0, 0, "Color image"); - if (color_depth) + if (color_depth) { d2.init(I_depth_color, I_color.getWidth() + 10, 0, "Depth image"); - else + } + else { d2.init(I_depth, I_color.getWidth() + 10, 0, "Depth image"); + } + pcl_viewer.setPosition(I_color.getWidth() + 10, I_color.getHeight() + 70); + pcl_viewer.setWindowName("3D point cloud"); + pcl_viewer.startThread(std::ref(mutex), pointcloud); } vpDisplay::display(I_color); @@ -418,14 +370,6 @@ int main(int argc, char *argv[]) cpt_frame++; } -#ifdef USE_PCL_VIEWER - { - std::lock_guard lock(mutex); - cancelled = true; - } - viewer_thread.join(); -#endif - return EXIT_SUCCESS; } #else diff --git a/example/device/framegrabber/saveRealSenseData.cpp b/example/device/framegrabber/saveRealSenseData.cpp index 35263c266b..32961cfe5d 100644 --- a/example/device/framegrabber/saveRealSenseData.cpp +++ b/example/device/framegrabber/saveRealSenseData.cpp @@ -72,52 +72,66 @@ namespace { -void usage(const char *name, const char *badparam) +void usage(const char *name, const char *badparam, int fps) { - fprintf(stdout, "\n\ - Save RealSense data.\n\ - \n\ - %s\ - OPTIONS: \n\ - -s \n\ - Save data.\n\ - \n\ - -o \n\ - Output directory.\n\ - \n\ - -a \n\ - Use aligned streams.\n\ - \n\ - -c \n\ - Save color stream.\n\ - \n\ - -d \n\ - Save depth stream.\n\ - \n\ - -p \n\ - Save pointcloud.\n\ - \n\ - -i \n\ - Save infrared stream.\n\ - \n\ - -C \n\ - Click to save.\n\ - \n\ - -f \n\ - Set FPS.\n\ - \n\ - -b \n\ - Save point cloud in binary format.\n\ - \n\ - -h \n\ - Print the help.\n\n", - name); - - if (badparam) - fprintf(stdout, "\nERROR: Bad parameter [%s]\n", badparam); + std::cout << "\nSYNOPSIS " << std::endl + << " " << name + << " [-s]" + << " [-a]" + << " [-c]" + << " [-d]" + << " [-p]" + << " [-b]" + << " [-i]" + << " [-C]" + << " [-f ]" + << " [-o ]" + << " [--help,-h]" + << std::endl; + std::cout << "\nOPTIONS " << std::endl + << " -s" << std::endl + << " Flag to enable data saving." << std::endl + << std::endl + << " -a" << std::endl + << " Color and depth are aligned." << std::endl + << std::endl + << " -c" << std::endl + << " Add color stream to saved data when -s option is enable." << std::endl + << std::endl + << " -d" << std::endl + << " Add depth stream to saved data when -s option is enable." << std::endl + << std::endl + << " -p" << std::endl + << " Add point cloud stream to saved data when -s option is enabled." << std::endl + << " By default, the point cloud is saved in Point Cloud Data file format (.PCD extension file)." << std::endl + << " You can also use -b option to save the point cloud in binary format." << std::endl + << std::endl + << " -b" << std::endl + << " Point cloud stream is saved in binary format." << std::endl + << std::endl + << " -i" << std::endl + << " Add infrared stream to saved data when -s option is enabled." << std::endl + << std::endl + << " -C" << std::endl + << " Trigger one shot data saver after each user click." << std::endl + << std::endl + << " -f " << std::endl + << " Set camera framerate." << std::endl + << " Default: " << fps << std::endl + << std::endl + << " -o " << std::endl + << " Output directory that will host saved data." << std::endl + << std::endl + << " --help, -h" << std::endl + << " Display this helper message." << std::endl + << std::endl; + + if (badparam) { + std::cout << "\nERROR: Bad parameter " << badparam << std::endl; + } } -bool getOptions(int argc, char **argv, bool &save, std::string &output_directory, bool &use_aligned_stream, +bool getOptions(int argc, const char *argv[], bool &save, std::string &output_directory, bool &use_aligned_stream, bool &save_color, bool &save_depth, bool &save_pointcloud, bool &save_infrared, bool &click_to_save, int &stream_fps, bool &save_pointcloud_binary_format) { @@ -159,12 +173,12 @@ bool getOptions(int argc, char **argv, bool &save, std::string &output_directory break; case 'h': - usage(argv[0], nullptr); + usage(argv[0], nullptr, stream_fps); return false; break; default: - usage(argv[0], optarg); + usage(argv[0], optarg, stream_fps); return false; break; } @@ -172,7 +186,7 @@ bool getOptions(int argc, char **argv, bool &save, std::string &output_directory if ((c == 1) || (c == -1)) { // standalone param or error - usage(argv[0], nullptr); + usage(argv[0], nullptr, stream_fps); std::cerr << "ERROR: " << std::endl; std::cerr << " Bad argument " << optarg << std::endl << std::endl; @@ -458,7 +472,7 @@ class vpStorageWorker }; } // Namespace -int main(int argc, char *argv[]) +int main(int argc, const char *argv[]) { bool save = false; std::string output_directory = vpTime::getDateTime("%Y_%m_%d_%H.%M.%S"); @@ -532,7 +546,7 @@ int main(int argc, char *argv[]) #endif d1.init(I_gray, 0, 0, "RealSense color stream"); d2.init(I_depth, I_gray.getWidth() + 80, 0, "RealSense depth stream"); - d3.init(I_infrared, I_gray.getWidth() + 80, I_gray.getHeight() + 10, "RealSense infrared stream"); + d3.init(I_infrared, I_gray.getWidth() + 80, I_gray.getHeight() + 70, "RealSense infrared stream"); while (true) { realsense.acquire((unsigned char *)I_color.bitmap, (unsigned char *)I_depth_raw.bitmap, nullptr, nullptr); @@ -616,7 +630,7 @@ int main(int argc, char *argv[]) rs2::align align_to(RS2_STREAM_COLOR); if (use_aligned_stream && save_infrared) { std::cerr << "Cannot use aligned streams with infrared acquisition currently." - "\nInfrared stream acquisition is disabled!" + << "\nInfrared stream acquisition is disabled!" << std::endl; } #endif @@ -672,7 +686,7 @@ int main(int argc, char *argv[]) } else { std::stringstream ss; - ss << "Images saved:" << nb_saves; + ss << "Images saved: " << nb_saves; vpDisplay::displayText(I_gray, 20, 20, ss.str(), vpColor::red); } diff --git a/modules/gui/include/visp3/gui/vpDisplayPCL.h b/modules/gui/include/visp3/gui/vpDisplayPCL.h index 3d693b9878..b32267b8fe 100644 --- a/modules/gui/include/visp3/gui/vpDisplayPCL.h +++ b/modules/gui/include/visp3/gui/vpDisplayPCL.h @@ -39,7 +39,7 @@ #if defined(VISP_HAVE_PCL) && defined(VISP_HAVE_THREADS) #include -#include +#include #include #include @@ -53,13 +53,15 @@ class VISP_EXPORT vpDisplayPCL { public: - vpDisplayPCL(); - vpDisplayPCL(unsigned int width, unsigned int height); + vpDisplayPCL(int posx = 0, int posy = 0, const std::string &window_name = ""); + vpDisplayPCL(unsigned int width, unsigned int height, int posx = 0, int posy = 0, const std::string &window_name = ""); ~vpDisplayPCL(); void setVerbose(bool verbose); void startThread(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud); void startThread(std::mutex &mutex, pcl::PointCloud::Ptr pointcloud); + void setPosition(int posx, int posy); + void setWindowName(const std::string &window_name); void stop(); private: @@ -72,6 +74,10 @@ class VISP_EXPORT vpDisplayPCL std::mutex m_mutex; unsigned int m_width; unsigned int m_height; + int m_posx; + int m_posy; + std::string m_window_name; + pcl::visualization::PCLVisualizer::Ptr m_viewer; }; #endif diff --git a/modules/gui/src/pointcloud/vpDisplayPCL.cpp b/modules/gui/src/pointcloud/vpDisplayPCL.cpp index b265a14553..e942990332 100644 --- a/modules/gui/src/pointcloud/vpDisplayPCL.cpp +++ b/modules/gui/src/pointcloud/vpDisplayPCL.cpp @@ -35,14 +35,17 @@ #if defined(VISP_HAVE_PCL) && defined(VISP_HAVE_THREADS) +#include + #include /*! * Default constructor. * By default, viewer size is set to 640 x 480. */ -vpDisplayPCL::vpDisplayPCL() - : m_stop(false), m_verbose(false), m_width(640), m_height(480) +vpDisplayPCL::vpDisplayPCL(int posx, int posy, const std::string &window_name) + : m_stop(false), m_verbose(false), m_width(640), m_height(480), m_posx(posx), m_posy(posy), + m_window_name(window_name), m_viewer(nullptr) { } /*! @@ -50,8 +53,9 @@ vpDisplayPCL::vpDisplayPCL() * \param[in] width : Point cloud viewer width. * \param[in] height : Point cloud viewer height. */ -vpDisplayPCL::vpDisplayPCL(unsigned int width, unsigned int height) - : m_stop(false), m_verbose(false), m_width(width), m_height(height) +vpDisplayPCL::vpDisplayPCL(unsigned int width, unsigned int height, int posx, int posy, const std::string &window_name) + : m_stop(false), m_verbose(false), m_width(width), m_height(height), m_posx(posx), m_posy(posy), + m_window_name(window_name), m_viewer(nullptr) { } /*! @@ -72,12 +76,13 @@ vpDisplayPCL::~vpDisplayPCL() void vpDisplayPCL::run(std::mutex &pointcloud_mutex, pcl::PointCloud::Ptr pointcloud) { pcl::PointCloud::Ptr local_pointcloud(new pcl::PointCloud()); - pcl::visualization::PCLVisualizer::Ptr viewer(new pcl::visualization::PCLVisualizer("3D Viewer")); - viewer->setBackgroundColor(0, 0, 0); - viewer->initCameraParameters(); - viewer->setPosition(m_width + 80, m_height + 80); - viewer->setCameraPosition(0, 0, -0.25, 0, -1, 0); - viewer->setSize(m_width, m_height); + m_viewer = pcl::visualization::PCLVisualizer::Ptr(new pcl::visualization::PCLVisualizer()); + m_viewer->setBackgroundColor(0, 0, 0); + m_viewer->initCameraParameters(); + m_viewer->setPosition(m_posx, m_posy); + m_viewer->setCameraPosition(0, 0, -0.25, 0, -1, 0); + m_viewer->setSize(m_width, m_height); + m_viewer->setWindowName(m_window_name); bool init = true; while (!m_stop) { @@ -87,16 +92,16 @@ void vpDisplayPCL::run(std::mutex &pointcloud_mutex, pcl::PointCloudaddPointCloud(local_pointcloud, "sample cloud"); - viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "sample cloud"); + m_viewer->addPointCloud(local_pointcloud, "sample cloud"); + m_viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "sample cloud"); init = false; } else { - viewer->updatePointCloud(local_pointcloud, "sample cloud"); + m_viewer->updatePointCloud(local_pointcloud, "sample cloud"); } - viewer->spinOnce(10); + m_viewer->spinOnce(10); } if (m_verbose) { @@ -113,12 +118,13 @@ void vpDisplayPCL::run_color(std::mutex &mutex, pcl::PointCloud::Ptr local_pointcloud(new pcl::PointCloud()); pcl::visualization::PointCloudColorHandlerRGBField rgb(pointcloud_color); - pcl::visualization::PCLVisualizer::Ptr viewer(new pcl::visualization::PCLVisualizer("3D Viewer")); - viewer->setBackgroundColor(0, 0, 0); - viewer->initCameraParameters(); - viewer->setPosition(m_width + 80, m_height + 80); - viewer->setCameraPosition(0, 0, -0.25, 0, -1, 0); - viewer->setSize(m_width, m_height); + m_viewer = pcl::visualization::PCLVisualizer::Ptr(new pcl::visualization::PCLVisualizer()); + m_viewer->setBackgroundColor(0, 0, 0); + m_viewer->initCameraParameters(); + m_viewer->setPosition(m_posx, m_posy); + m_viewer->setCameraPosition(0, 0, -0.25, 0, -1, 0); + m_viewer->setSize(m_width, m_height); + m_viewer->setWindowName(m_window_name); bool init = true; while (!m_stop) { @@ -128,16 +134,16 @@ void vpDisplayPCL::run_color(std::mutex &mutex, pcl::PointCloudaddPointCloud(local_pointcloud, rgb, "RGB sample cloud"); - viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "RGB sample cloud"); + m_viewer->addPointCloud(local_pointcloud, rgb, "RGB sample cloud"); + m_viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "RGB sample cloud"); init = false; } else { - viewer->updatePointCloud(local_pointcloud, rgb, "RGB sample cloud"); + m_viewer->updatePointCloud(local_pointcloud, rgb, "RGB sample cloud"); } - viewer->spinOnce(10); + m_viewer->spinOnce(10); } if (m_verbose) { @@ -169,6 +175,34 @@ void vpDisplayPCL::startThread(std::mutex &mutex, pcl::PointCloud -#include - -#include -#include - #include #include +#include #include #include +#include #include -namespace -{ -// Global variables -pcl::PointCloud::Ptr pointcloud(new pcl::PointCloud()); -pcl::PointCloud::Ptr pointcloud_color(new pcl::PointCloud()); -bool cancelled = false, update_pointcloud = false; - -class ViewerWorker -{ -public: - explicit ViewerWorker(bool color_mode, std::mutex &mutex) : m_colorMode(color_mode), m_mutex(mutex) { } - - void run() - { - std::string date = vpTime::getDateTime(); - pcl::visualization::PCLVisualizer::Ptr viewer(new pcl::visualization::PCLVisualizer("3D Viewer " + date)); - pcl::visualization::PointCloudColorHandlerRGBField rgb(pointcloud_color); - pcl::PointCloud::Ptr local_pointcloud(new pcl::PointCloud()); - pcl::PointCloud::Ptr local_pointcloud_color(new pcl::PointCloud()); - - viewer->setBackgroundColor(0, 0, 0); - viewer->initCameraParameters(); - viewer->setPosition(640 + 80, 480 + 80); - viewer->setCameraPosition(0, 0, -0.25, 0, -1, 0); - viewer->setSize(640, 480); - - bool init = true; - bool local_update = false, local_cancelled = false; - while (!local_cancelled) { - { - std::unique_lock lock(m_mutex, std::try_to_lock); - - if (lock.owns_lock()) { - local_update = update_pointcloud; - update_pointcloud = false; - local_cancelled = cancelled; - - if (local_update) { - if (m_colorMode) { - local_pointcloud_color = pointcloud_color->makeShared(); - } - else { - local_pointcloud = pointcloud->makeShared(); - } - } - } - } - - if (local_update && !local_cancelled) { - local_update = false; - - if (init) { - if (m_colorMode) { - viewer->addPointCloud(local_pointcloud_color, rgb, "RGB sample cloud"); - viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, - "RGB sample cloud"); - } - else { - viewer->addPointCloud(local_pointcloud, "sample cloud"); - viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "sample cloud"); - } - init = false; - } - else { - if (m_colorMode) { - viewer->updatePointCloud(local_pointcloud_color, rgb, "RGB sample cloud"); - } - else { - viewer->updatePointCloud(local_pointcloud, "sample cloud"); - } - } - } - - viewer->spinOnce(5); - } - - std::cout << "End of point cloud display thread" << std::endl; - } - -private: - bool m_colorMode; - std::mutex &m_mutex; -}; -} // namespace - int main(int argc, char *argv[]) { - bool pcl_color = false; - bool show_infrared2 = false; + bool opt_pcl_color = false; + bool opt_show_infrared2 = false; + bool display_helper = false; + for (int i = 1; i < argc; i++) { - if (std::string(argv[i]) == "--pcl_color") { - pcl_color = true; + if (std::string(argv[i]) == "--pcl-color") { + opt_pcl_color = true; + } + else if (std::string(argv[i]) == "--show-infrared2") { + opt_show_infrared2 = true; + } + else if ((std::string(argv[i]) == "--help") || (std::string(argv[i]) == "-h")) { + display_helper = true; } - else if (std::string(argv[i]) == "--show_infrared2") { - show_infrared2 = true; + else { + display_helper = true; + std::cout << "\nERROR" << std::endl; + std::cout << " Wrong command line option." << std::endl; + } + if (display_helper) { + std::cout << "\nSYNOPSIS " << std::endl + << " " << argv[0] + << " [--pcl-color]" + << " [--show-infrared2]" + << " [--help,-h]" + << std::endl; + std::cout << "\nOPTIONS " << std::endl + << " --pcl-color" << std::endl + << " Enable textured point cloud visualization." << std::endl + << std::endl + << " --show-infrared2" << std::endl + << " Display also the infrared2 stream." << std::endl + << std::endl + << " --help, -h" << std::endl + << " Display this helper message." << std::endl + << std::endl; + return EXIT_SUCCESS; } } @@ -175,16 +115,22 @@ int main(int argc, char *argv[]) vpDisplayGDI d1, d2, d3, d4; #endif d1.init(color, 0, 0, "Color"); - d2.init(depth_color, color.getWidth(), 0, "Depth"); - d3.init(infrared1, 0, color.getHeight() + 100, "Infrared left"); - if (show_infrared2) { + d2.init(depth_color, color.getWidth() + 80, 0, "Depth"); + d3.init(infrared1, 0, color.getHeight() + 70, "Infrared left"); + if (opt_show_infrared2) { d4.init(infrared2, color.getWidth(), color.getHeight() + 100, "Infrared right"); } std::mutex mutex; - ViewerWorker viewer_pointcloud(pcl_color, mutex); - std::thread viewer_thread(&ViewerWorker::run, &viewer_pointcloud); - + pcl::PointCloud::Ptr pointcloud(new pcl::PointCloud()); + pcl::PointCloud::Ptr pointcloud_color(new pcl::PointCloud()); + vpDisplayPCL pcl_viewer(color.getWidth() + 80, color.getHeight() + 70, "3D viewer " + vpTime::getDateTime()); + if (opt_pcl_color) { + pcl_viewer.startThread(std::ref(mutex), pointcloud_color); + } + else { + pcl_viewer.startThread(std::ref(mutex), pointcloud); + } std::vector time_vector; vpChrono chrono; while (true) { @@ -192,18 +138,16 @@ int main(int argc, char *argv[]) { std::lock_guard lock(mutex); - if (pcl_color) { + if (opt_pcl_color) { rs.acquire(reinterpret_cast(color.bitmap), reinterpret_cast(depth_raw.bitmap), nullptr, pointcloud_color, reinterpret_cast(infrared1.bitmap), - show_infrared2 ? reinterpret_cast(infrared2.bitmap) : nullptr, nullptr); + opt_show_infrared2 ? reinterpret_cast(infrared2.bitmap) : nullptr, nullptr); } else { rs.acquire(reinterpret_cast(color.bitmap), reinterpret_cast(depth_raw.bitmap), nullptr, pointcloud, reinterpret_cast(infrared1.bitmap), - show_infrared2 ? reinterpret_cast(infrared2.bitmap) : nullptr, nullptr); + opt_show_infrared2 ? reinterpret_cast(infrared2.bitmap) : nullptr, nullptr); } - - update_pointcloud = true; } vpImageConvert::createDepthHistogram(depth_raw, depth_color); @@ -231,12 +175,6 @@ int main(int argc, char *argv[]) } } - { - std::lock_guard lock(mutex); - cancelled = true; - } - viewer_thread.join(); - std::cout << "Acquisition - Mean time: " << vpMath::getMean(time_vector) << " ms ; Median time: " << vpMath::getMedian(time_vector) << " ms" << std::endl; From 99ff41eaa274ab5eb7e0097488cc15149d8ad197 Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Fri, 5 Apr 2024 17:15:46 +0200 Subject: [PATCH 31/32] Realsense tutorial saves also camera parameters --- .../grabber/tutorial-grabber-realsense.cpp | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tutorial/grabber/tutorial-grabber-realsense.cpp b/tutorial/grabber/tutorial-grabber-realsense.cpp index f038a51c09..958b866b5f 100644 --- a/tutorial/grabber/tutorial-grabber-realsense.cpp +++ b/tutorial/grabber/tutorial-grabber-realsense.cpp @@ -1,5 +1,6 @@ /*! \example tutorial-grabber-realsense.cpp */ #include +#include #include #include #include @@ -157,6 +158,26 @@ int main(int argc, const char *argv[]) std::cout << "Image size : " << I.getWidth() << " " << I.getHeight() << std::endl; + vpCameraParameters cam = g.getCameraParameters(RS2_STREAM_COLOR, vpCameraParameters::perspectiveProjWithoutDistortion); + vpXmlParserCamera p; + std::string output_folder = vpIoTools::getParent(opt_seqname); + if (!vpIoTools::checkDirectory(output_folder)) { + try { + std::cout << "Create output folder: " << output_folder << std::endl; + vpIoTools::makeDirectory(output_folder); + } + catch (const vpException &e) { + std::cout << e.getStringMessage(); + return EXIT_FAILURE; + } + } + std::string cam_filename = output_folder + "/camera.xml"; + + std::cout << "Save camera intrinsics in: " << cam_filename << std::endl; + if (p.save(cam, cam_filename, "camera")) { + std::cout << "Cannot save camera parameters in " << cam_filename << std::endl; + } + vpDisplay *d = nullptr; if (opt_display) { #if !(defined(VISP_HAVE_X11) || defined(VISP_HAVE_GDI) || defined(VISP_HAVE_OPENCV)) From 4187915d44374ed7f7ef31812dde94341b665a0a Mon Sep 17 00:00:00 2001 From: Fabien Spindler Date: Fri, 5 Apr 2024 18:41:41 +0200 Subject: [PATCH 32/32] Fix build when PCL not enabled --- example/device/framegrabber/readRealSenseData.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/example/device/framegrabber/readRealSenseData.cpp b/example/device/framegrabber/readRealSenseData.cpp index 2219719383..7cc493073a 100644 --- a/example/device/framegrabber/readRealSenseData.cpp +++ b/example/device/framegrabber/readRealSenseData.cpp @@ -311,9 +311,11 @@ int main(int argc, const char *argv[]) else { d2.init(I_depth, I_color.getWidth() + 10, 0, "Depth image"); } +#if defined(VISP_HAVE_PCL) pcl_viewer.setPosition(I_color.getWidth() + 10, I_color.getHeight() + 70); pcl_viewer.setWindowName("3D point cloud"); pcl_viewer.startThread(std::ref(mutex), pointcloud); +#endif } vpDisplay::display(I_color);