From 049f156becbc951ff2312173d205b609537d4e32 Mon Sep 17 00:00:00 2001 From: Marco Randazzo Date: Fri, 30 Aug 2024 10:05:17 +0200 Subject: [PATCH] Added OpenCVGrabber paramparser + test --- src/devices/opencvGrabber/CMakeLists.txt | 8 + src/devices/opencvGrabber/OpenCVGrabber.cpp | 96 +++----- src/devices/opencvGrabber/OpenCVGrabber.h | 35 +-- src/devices/opencvGrabber/OpenCVGrabber.md | 11 + .../OpenCVGrabber_ParamsParser.cpp | 231 ++++++++++++++++++ .../OpenCVGrabber_ParamsParser.h | 93 +++++++ .../opencvGrabber/OpenCVGrabber_params.md | 11 + .../opencvGrabber/tests/CMakeLists.txt | 10 + .../tests/opencvGrabber_test.cpp | 49 ++++ src/devices/opencvGrabber/tests/test.avi | Bin 0 -> 303614 bytes 10 files changed, 456 insertions(+), 88 deletions(-) create mode 100644 src/devices/opencvGrabber/OpenCVGrabber.md create mode 100644 src/devices/opencvGrabber/OpenCVGrabber_ParamsParser.cpp create mode 100644 src/devices/opencvGrabber/OpenCVGrabber_ParamsParser.h create mode 100644 src/devices/opencvGrabber/OpenCVGrabber_params.md create mode 100644 src/devices/opencvGrabber/tests/CMakeLists.txt create mode 100644 src/devices/opencvGrabber/tests/opencvGrabber_test.cpp create mode 100644 src/devices/opencvGrabber/tests/test.avi diff --git a/src/devices/opencvGrabber/CMakeLists.txt b/src/devices/opencvGrabber/CMakeLists.txt index 62e37e8a87..93ef40440f 100644 --- a/src/devices/opencvGrabber/CMakeLists.txt +++ b/src/devices/opencvGrabber/CMakeLists.txt @@ -6,6 +6,7 @@ yarp_prepare_plugin(opencv_grabber CATEGORY device TYPE OpenCVGrabber INCLUDE OpenCVGrabber.h + GENERATE_PARSER EXTRA_CONFIG WRAPPER=frameGrabber_nws_yarp DEPENDS "YARP_HAS_OpenCV" @@ -18,6 +19,8 @@ if(NOT SKIP_opencv_grabber) PRIVATE OpenCVGrabber.cpp OpenCVGrabber.h + OpenCVGrabber_ParamsParser.cpp + OpenCVGrabber_ParamsParser.h ) target_link_libraries(yarp_opencv @@ -52,6 +55,11 @@ if(NOT SKIP_opencv_grabber) set(YARP_${YARP_PLUGIN_MASTER}_PRIVATE_DEPS ${YARP_${YARP_PLUGIN_MASTER}_PRIVATE_DEPS} PARENT_SCOPE) set_property(TARGET yarp_opencv PROPERTY FOLDER "Plugins/Device") + + if(YARP_COMPILE_TESTS) + add_subdirectory(tests) + endif() + endif() include(YarpRemoveFile) diff --git a/src/devices/opencvGrabber/OpenCVGrabber.cpp b/src/devices/opencvGrabber/OpenCVGrabber.cpp index f25db6b41b..1231b3b8ea 100644 --- a/src/devices/opencvGrabber/OpenCVGrabber.cpp +++ b/src/devices/opencvGrabber/OpenCVGrabber.cpp @@ -53,36 +53,25 @@ YARP_LOG_COMPONENT(OPENCVGRABBER, "yarp.device.opencv_grabber") } -bool OpenCVGrabber::open(Searchable & config) { - m_saidSize = false; - m_saidResize = false; - m_transpose = false; - m_flip_x = false; - m_flip_y = false; +bool OpenCVGrabber::open(Searchable & config) +{ + if (!parseParams(config)) { + return false; + } // Are we capturing from a file or a camera ? - std::string file = config.check("movie", Value(""), - "if present, read from specified file rather than camera").asString(); - fromFile = (file!=""); - if (fromFile) { - + fromFile = (m_movie != ""); + if (fromFile) + { // Try to open a capture object for the file - m_cap.open(file.c_str()); + m_cap.open(m_movie.c_str()); if (!m_cap.isOpened()) { - yCError(OPENCVGRABBER, "Unable to open file '%s' for capture!", file.c_str()); + yCError(OPENCVGRABBER, "Unable to open file '%s' for capture!", m_movie.c_str()); return false; } - - // Should we loop? - m_loop = config.check("loop","if present, loop movie"); - - } else { - - m_loop = false; - int camera_idx = - config.check("camera", - Value(cv::VideoCaptureAPIs::CAP_ANY), - "if present, read from camera identified by this index").asInt32(); + } else + { + int camera_idx = m_camera; // Try to open a capture object for the first camera m_cap.open(camera_idx); if (!m_cap.isOpened()) { @@ -94,42 +83,25 @@ bool OpenCVGrabber::open(Searchable & config) { yCInfo(OPENCVGRABBER, "Capturing from camera: %d",camera_idx); } - if ( config.check("framerate","if present, specifies desired camera device framerate") ) { - double m_fps = config.check("framerate", Value(-1)).asFloat64(); - m_cap.set(cv::VideoCaptureProperties::CAP_PROP_FPS, m_fps); + if ( m_framerate != -1 ) { + m_cap.set(cv::VideoCaptureProperties::CAP_PROP_FPS, m_framerate); } } - if (config.check("flip_x", "if present, flip the image along the x-axis")) { - m_flip_x = true; - } - - if (config.check("flip_y", "if present, flip the image along the y-axis")) { - m_flip_y = true; - } - - if (config.check("transpose", "if present, rotate the image along of 90 degrees")) { - m_transpose = true; - } - // Extract the desired image size from the configuration if // present, otherwise query the capture device - if (config.check("width","if present, specifies desired image width")) { - m_w = config.check("width", Value(0)).asInt32(); - if (!fromFile && m_w>0) { - m_cap.set(cv::VideoCaptureProperties::CAP_PROP_FRAME_WIDTH, m_w); - } - } else { - m_w = m_cap.get(cv::VideoCaptureProperties::CAP_PROP_FRAME_WIDTH); + if (!fromFile && m_width > 0) { + m_cap.set(cv::VideoCaptureProperties::CAP_PROP_FRAME_WIDTH, m_width); + } + else { + m_width = m_cap.get(cv::VideoCaptureProperties::CAP_PROP_FRAME_WIDTH); } - if (config.check("height","if present, specifies desired image height")) { - m_h = config.check("height", Value(0)).asInt32(); - if (!fromFile && m_h>0) { - m_cap.set(cv::VideoCaptureProperties::CAP_PROP_FRAME_HEIGHT, m_h); - } - } else { - m_h = m_cap.get(cv::VideoCaptureProperties::CAP_PROP_FRAME_HEIGHT); + if (!fromFile && m_height> 0) { + m_cap.set(cv::VideoCaptureProperties::CAP_PROP_FRAME_HEIGHT, m_height); + } + else { + m_height = m_cap.get(cv::VideoCaptureProperties::CAP_PROP_FRAME_HEIGHT); } // Ignore capture properties - they are unreliable @@ -179,8 +151,8 @@ bool OpenCVGrabber::getImage(ImageOf & image) { } // Callers may have not initialized the image dimensions (may happen if this device is not wrapped) - if (static_cast(image.width()) != m_w || static_cast(image.height()) != m_h) { - image.resize(m_w, m_h); + if (static_cast(image.width()) != m_width || static_cast(image.height()) != m_height) { + image.resize(m_width, m_height); } // Grab and retrieve a frame, @@ -230,22 +202,22 @@ bool OpenCVGrabber::getImage(ImageOf & image) { // create the timestamp m_laststamp.update(); - if (m_w == 0) { - m_w = frame.cols; + if (m_width == 0) { + m_width = frame.cols; } - if (m_h == 0) { - m_h = frame.rows; + if (m_height == 0) { + m_height = frame.rows; } - if (fromFile && (frame.cols != (!m_transpose ? m_w : m_h) || frame.rows != (!m_transpose ? m_h : m_w))) { + if (fromFile && (frame.cols != (!m_transpose ? m_width : m_height) || frame.rows != (!m_transpose ? m_height : m_width))) { if (!m_saidResize) { - yCDebug(OPENCVGRABBER, "Software scaling from %dx%d to %dx%d", frame.cols, frame.rows, m_w, m_h); + yCDebug(OPENCVGRABBER, "Software scaling from %dx%d to %dx%d", frame.cols, frame.rows, m_width, m_height); m_saidResize = true; } cv::Mat resized; - cv::resize(frame, resized, {m_w, m_h}); + cv::resize(frame, resized, {m_width, m_height}); image.resize(resized.cols, resized.rows); // erases previous content frame = cv::Mat(image.height(), image.width(), CV_8UC3, image.getRawImage(), image.getRowSize()); resized.copyTo(frame); diff --git a/src/devices/opencvGrabber/OpenCVGrabber.h b/src/devices/opencvGrabber/OpenCVGrabber.h index c8db8d3f7d..684b6f9d0c 100644 --- a/src/devices/opencvGrabber/OpenCVGrabber.h +++ b/src/devices/opencvGrabber/OpenCVGrabber.h @@ -19,6 +19,7 @@ #include #include #include +#include "OpenCVGrabber_ParamsParser.h" #include @@ -31,7 +32,8 @@ class OpenCVGrabber : public yarp::dev::IFrameGrabberImage, public yarp::dev::DeviceDriver, - public yarp::dev::IPreciselyTimed + public yarp::dev::IPreciselyTimed, + public OpenCVGrabber_ParamsParser { public: @@ -41,16 +43,10 @@ class OpenCVGrabber : * open(). */ OpenCVGrabber() : - m_w(0), - m_h(0), - m_loop(false), m_saidSize(false), m_saidResize(false), fromFile(false), - m_cap(), - m_transpose(false), - m_flip_x(false), - m_flip_y(false) + m_cap() {} /** Destroy an OpenCV image grabber. */ @@ -67,11 +63,11 @@ class OpenCVGrabber : /** Get the height of images a grabber produces. * @return The image height. */ - inline int height() const override { return m_h; } + inline int height() const override { return m_height; } /** Get the width of images a grabber produces. * @return The image width. */ - inline int width() const override { return m_w; } + inline int width() const override { return m_width; } /** * Implements the IPreciselyTimed interface. @@ -81,28 +77,15 @@ class OpenCVGrabber : protected: - /** Width of the images a grabber produces. */ - int m_w; - /** Height of the images a grabber produces. */ - int m_h; - - /** Whether to loop or not. */ - bool m_loop; - - bool m_saidSize; - bool m_saidResize; + bool m_saidSize = false; + bool m_saidResize = false; /** Whether reading from file or camera. */ - bool fromFile; + bool fromFile = false; /** OpenCV image capture object. */ cv::VideoCapture m_cap; - /* optional image modifiers */ - bool m_transpose; - bool m_flip_x; - bool m_flip_y; - /** Saved copy of the device configuration. */ yarp::os::Property m_config; diff --git a/src/devices/opencvGrabber/OpenCVGrabber.md b/src/devices/opencvGrabber/OpenCVGrabber.md new file mode 100644 index 0000000000..57a740585c --- /dev/null +++ b/src/devices/opencvGrabber/OpenCVGrabber.md @@ -0,0 +1,11 @@ + * | Group | Name | Type | Units | Default Value | Required | Description | Notes | + * |:--------:|:---------------------:|:-------:|:--------:|:-------------------:|:------------:|:-----------------------------------------------------------------:|:-----:| + * | | movie | string | - | - | No | if present, read an .avi file instead of opening a camera | | + * | | loop | bool | - | false | No | if true, and movie parameter is set, enable the loop playback of the file | | + * | | camera | int | - | 0 | No | Id of the camera hardware device | | + * | | framerate | double | - | -1 | No | Framerate. Default value obtained by the hardware | | + * | | width | int | - | 0 | No | Width of the frame. Default value obtained by the hardware | | + * | | height | int | - | 0 | No | Height of the frame. Default value obtained by the hardware | | + * | | flip_x | bool | - | false | No | Flip along the x axis | | + * | | flip_y | bool | - | false | No | flip along the y axis | | + * | | transpose | bool | - | false | No | Rotate the image by 90 degrees | | diff --git a/src/devices/opencvGrabber/OpenCVGrabber_ParamsParser.cpp b/src/devices/opencvGrabber/OpenCVGrabber_ParamsParser.cpp new file mode 100644 index 0000000000..cd1669a338 --- /dev/null +++ b/src/devices/opencvGrabber/OpenCVGrabber_ParamsParser.cpp @@ -0,0 +1,231 @@ +/* + * SPDX-FileCopyrightText: 2023-2023 Istituto Italiano di Tecnologia (IIT) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + + +// Generated by yarpDeviceParamParserGenerator (1.0) +// This is an automatically generated file. Please do not edit it. +// It will be re-generated if the cmake flag ALLOW_DEVICE_PARAM_PARSER_GERNERATION is ON. + +// Generated on: Wed Aug 28 15:36:19 2024 + + +#include "OpenCVGrabber_ParamsParser.h" +#include +#include + +namespace { + YARP_LOG_COMPONENT(OpenCVGrabberParamsCOMPONENT, "yarp.device.OpenCVGrabber") +} + + +OpenCVGrabber_ParamsParser::OpenCVGrabber_ParamsParser() +{ +} + + +std::vector OpenCVGrabber_ParamsParser::getListOfParams() const +{ + std::vector params; + params.push_back("movie"); + params.push_back("loop"); + params.push_back("camera"); + params.push_back("framerate"); + params.push_back("width"); + params.push_back("height"); + params.push_back("flip_x"); + params.push_back("flip_y"); + params.push_back("transpose"); + return params; +} + + +bool OpenCVGrabber_ParamsParser::parseParams(const yarp::os::Searchable & config) +{ + //Check for --help option + if (config.check("help")) + { + yCInfo(OpenCVGrabberParamsCOMPONENT) << getDocumentationOfDeviceParams(); + } + + std::string config_string = config.toString(); + yarp::os::Property prop_check(config_string.c_str()); + //Parser of parameter movie + { + if (config.check("movie")) + { + m_movie = config.find("movie").asString(); + yCInfo(OpenCVGrabberParamsCOMPONENT) << "Parameter 'movie' using value:" << m_movie; + } + else + { + yCInfo(OpenCVGrabberParamsCOMPONENT) << "Parameter 'movie' using DEFAULT value:" << m_movie; + } + prop_check.unput("movie"); + } + + //Parser of parameter loop + { + if (config.check("loop")) + { + m_loop = config.find("loop").asBool(); + yCInfo(OpenCVGrabberParamsCOMPONENT) << "Parameter 'loop' using value:" << m_loop; + } + else + { + yCInfo(OpenCVGrabberParamsCOMPONENT) << "Parameter 'loop' using DEFAULT value:" << m_loop; + } + prop_check.unput("loop"); + } + + //Parser of parameter camera + { + if (config.check("camera")) + { + m_camera = config.find("camera").asInt64(); + yCInfo(OpenCVGrabberParamsCOMPONENT) << "Parameter 'camera' using value:" << m_camera; + } + else + { + yCInfo(OpenCVGrabberParamsCOMPONENT) << "Parameter 'camera' using DEFAULT value:" << m_camera; + } + prop_check.unput("camera"); + } + + //Parser of parameter framerate + { + if (config.check("framerate")) + { + m_framerate = config.find("framerate").asFloat64(); + yCInfo(OpenCVGrabberParamsCOMPONENT) << "Parameter 'framerate' using value:" << m_framerate; + } + else + { + yCInfo(OpenCVGrabberParamsCOMPONENT) << "Parameter 'framerate' using DEFAULT value:" << m_framerate; + } + prop_check.unput("framerate"); + } + + //Parser of parameter width + { + if (config.check("width")) + { + m_width = config.find("width").asInt64(); + yCInfo(OpenCVGrabberParamsCOMPONENT) << "Parameter 'width' using value:" << m_width; + } + else + { + yCInfo(OpenCVGrabberParamsCOMPONENT) << "Parameter 'width' using DEFAULT value:" << m_width; + } + prop_check.unput("width"); + } + + //Parser of parameter height + { + if (config.check("height")) + { + m_height = config.find("height").asInt64(); + yCInfo(OpenCVGrabberParamsCOMPONENT) << "Parameter 'height' using value:" << m_height; + } + else + { + yCInfo(OpenCVGrabberParamsCOMPONENT) << "Parameter 'height' using DEFAULT value:" << m_height; + } + prop_check.unput("height"); + } + + //Parser of parameter flip_x + { + if (config.check("flip_x")) + { + m_flip_x = config.find("flip_x").asBool(); + yCInfo(OpenCVGrabberParamsCOMPONENT) << "Parameter 'flip_x' using value:" << m_flip_x; + } + else + { + yCInfo(OpenCVGrabberParamsCOMPONENT) << "Parameter 'flip_x' using DEFAULT value:" << m_flip_x; + } + prop_check.unput("flip_x"); + } + + //Parser of parameter flip_y + { + if (config.check("flip_y")) + { + m_flip_y = config.find("flip_y").asBool(); + yCInfo(OpenCVGrabberParamsCOMPONENT) << "Parameter 'flip_y' using value:" << m_flip_y; + } + else + { + yCInfo(OpenCVGrabberParamsCOMPONENT) << "Parameter 'flip_y' using DEFAULT value:" << m_flip_y; + } + prop_check.unput("flip_y"); + } + + //Parser of parameter transpose + { + if (config.check("transpose")) + { + m_transpose = config.find("transpose").asBool(); + yCInfo(OpenCVGrabberParamsCOMPONENT) << "Parameter 'transpose' using value:" << m_transpose; + } + else + { + yCInfo(OpenCVGrabberParamsCOMPONENT) << "Parameter 'transpose' using DEFAULT value:" << m_transpose; + } + prop_check.unput("transpose"); + } + + /* + //This code check if the user set some parameter which are not check by the parser + //If the parser is set in strict mode, this will generate an error + if (prop_check.size() > 0) + { + bool extra_params_found = false; + for (auto it=prop_check.begin(); it!=prop_check.end(); it++) + { + if (m_parser_is_strict) + { + yCError(OpenCVGrabberParamsCOMPONENT) << "User asking for parameter: "<name <<" which is unknown to this parser!"; + extra_params_found = true; + } + else + { + yCWarning(OpenCVGrabberParamsCOMPONENT) << "User asking for parameter: "<< it->name <<" which is unknown to this parser!"; + } + } + + if (m_parser_is_strict && extra_params_found) + { + return false; + } + } + */ + return true; +} + + +std::string OpenCVGrabber_ParamsParser::getDocumentationOfDeviceParams() const +{ + std::string doc; + doc = doc + std::string("\n=============================================\n"); + doc = doc + std::string("This is the help for device: OpenCVGrabber\n"); + doc = doc + std::string("\n"); + doc = doc + std::string("This is the list of the parameters accepted by the device:\n"); + doc = doc + std::string("'movie': if present, read an .avi file instead of opening a camera\n"); + doc = doc + std::string("'loop': if true, and movie parameter is set, enable the loop playback of the file\n"); + doc = doc + std::string("'camera': Id of the camera hardware device\n"); + doc = doc + std::string("'framerate': Framerate. Default value obtained by the hardware\n"); + doc = doc + std::string("'width': Width of the frame. Default value obtained by the hardware\n"); + doc = doc + std::string("'height': Height of the frame. Default value obtained by the hardware\n"); + doc = doc + std::string("'flip_x': Flip along the x axis\n"); + doc = doc + std::string("'flip_y': flip along the y axis\n"); + doc = doc + std::string("'transpose': Rotate the image by 90 degrees\n"); + doc = doc + std::string("\n"); + doc = doc + std::string("Here are some examples of invocation command with yarpdev, with all params:\n"); + doc = doc + " yarpdev --device opencv_grabber --movie --loop false --camera 0 --framerate -1 --width 0 --height 0 --flip_x false --flip_y false --transpose false\n"; + doc = doc + std::string("Using only mandatory params:\n"); + doc = doc + " yarpdev --device opencv_grabber\n"; + doc = doc + std::string("=============================================\n\n"); return doc; +} diff --git a/src/devices/opencvGrabber/OpenCVGrabber_ParamsParser.h b/src/devices/opencvGrabber/OpenCVGrabber_ParamsParser.h new file mode 100644 index 0000000000..a22dd82cc4 --- /dev/null +++ b/src/devices/opencvGrabber/OpenCVGrabber_ParamsParser.h @@ -0,0 +1,93 @@ +/* + * SPDX-FileCopyrightText: 2023-2023 Istituto Italiano di Tecnologia (IIT) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + + +// Generated by yarpDeviceParamParserGenerator (1.0) +// This is an automatically generated file. Please do not edit it. +// It will be re-generated if the cmake flag ALLOW_DEVICE_PARAM_PARSER_GERNERATION is ON. + +// Generated on: Wed Aug 28 15:36:19 2024 + + +#ifndef OPENCVGRABBER_PARAMSPARSER_H +#define OPENCVGRABBER_PARAMSPARSER_H + +#include +#include +#include +#include + +/** +* This class is the parameters parser for class OpenCVGrabber. +* +* These are the used parameters: +* | Group name | Parameter name | Type | Units | Default Value | Required | Description | Notes | +* |:----------:|:--------------:|:------:|:-----:|:-------------:|:--------:|:-------------------------------------------------------------------------:|:-----:| +* | - | movie | string | - | - | 0 | if present, read an .avi file instead of opening a camera | - | +* | - | loop | bool | - | false | 0 | if true, and movie parameter is set, enable the loop playback of the file | - | +* | - | camera | int | - | 0 | 0 | Id of the camera hardware device | - | +* | - | framerate | double | - | -1 | 0 | Framerate. Default value obtained by the hardware | - | +* | - | width | int | - | 0 | 0 | Width of the frame. Default value obtained by the hardware | - | +* | - | height | int | - | 0 | 0 | Height of the frame. Default value obtained by the hardware | - | +* | - | flip_x | bool | - | false | 0 | Flip along the x axis | - | +* | - | flip_y | bool | - | false | 0 | flip along the y axis | - | +* | - | transpose | bool | - | false | 0 | Rotate the image by 90 degrees | - | +* +* The device can be launched by yarpdev using one of the following examples (with and without all optional parameters): +* \code{.unparsed} +* yarpdev --device opencv_grabber --movie --loop false --camera 0 --framerate -1 --width 0 --height 0 --flip_x false --flip_y false --transpose false +* \endcode +* +* \code{.unparsed} +* yarpdev --device opencv_grabber +* \endcode +* +*/ + +class OpenCVGrabber_ParamsParser : public yarp::dev::IDeviceDriverParams +{ +public: + OpenCVGrabber_ParamsParser(); + ~OpenCVGrabber_ParamsParser() override = default; + +public: + const std::string m_device_classname = {"OpenCVGrabber"}; + const std::string m_device_name = {"opencv_grabber"}; + bool m_parser_is_strict = false; + struct parser_version_type + { + int major = 1; + int minor = 0; + }; + const parser_version_type m_parser_version = {}; + + const std::string m_movie_defaultValue = {""}; + const std::string m_loop_defaultValue = {"false"}; + const std::string m_camera_defaultValue = {"0"}; + const std::string m_framerate_defaultValue = {"-1"}; + const std::string m_width_defaultValue = {"0"}; + const std::string m_height_defaultValue = {"0"}; + const std::string m_flip_x_defaultValue = {"false"}; + const std::string m_flip_y_defaultValue = {"false"}; + const std::string m_transpose_defaultValue = {"false"}; + + std::string m_movie = {}; //This default value of this string is an empty string. It is highly recommended to provide a suggested value also for optional string parameters. + bool m_loop = {false}; + int m_camera = {0}; + double m_framerate = {-1}; + int m_width = {0}; + int m_height = {0}; + bool m_flip_x = {false}; + bool m_flip_y = {false}; + bool m_transpose = {false}; + + bool parseParams(const yarp::os::Searchable & config) override; + std::string getDeviceClassName() const override { return m_device_classname; } + std::string getDeviceName() const override { return m_device_name; } + std::string getDocumentationOfDeviceParams() const override; + std::vector getListOfParams() const override; +}; + +#endif diff --git a/src/devices/opencvGrabber/OpenCVGrabber_params.md b/src/devices/opencvGrabber/OpenCVGrabber_params.md new file mode 100644 index 0000000000..57a740585c --- /dev/null +++ b/src/devices/opencvGrabber/OpenCVGrabber_params.md @@ -0,0 +1,11 @@ + * | Group | Name | Type | Units | Default Value | Required | Description | Notes | + * |:--------:|:---------------------:|:-------:|:--------:|:-------------------:|:------------:|:-----------------------------------------------------------------:|:-----:| + * | | movie | string | - | - | No | if present, read an .avi file instead of opening a camera | | + * | | loop | bool | - | false | No | if true, and movie parameter is set, enable the loop playback of the file | | + * | | camera | int | - | 0 | No | Id of the camera hardware device | | + * | | framerate | double | - | -1 | No | Framerate. Default value obtained by the hardware | | + * | | width | int | - | 0 | No | Width of the frame. Default value obtained by the hardware | | + * | | height | int | - | 0 | No | Height of the frame. Default value obtained by the hardware | | + * | | flip_x | bool | - | false | No | Flip along the x axis | | + * | | flip_y | bool | - | false | No | flip along the y axis | | + * | | transpose | bool | - | false | No | Rotate the image by 90 degrees | | diff --git a/src/devices/opencvGrabber/tests/CMakeLists.txt b/src/devices/opencvGrabber/tests/CMakeLists.txt new file mode 100644 index 0000000000..eaa43ace97 --- /dev/null +++ b/src/devices/opencvGrabber/tests/CMakeLists.txt @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2024-2024 Istituto Italiano di Tecnologia (IIT) +# SPDX-License-Identifier: BSD-3-Clause + +create_device_test(opencvGrabber) + +yarp_install( + FILES + ./test.avi + DESTINATION ${YARP_CONTEXTS_INSTALL_DIR}/tests/opencvGrabber +) diff --git a/src/devices/opencvGrabber/tests/opencvGrabber_test.cpp b/src/devices/opencvGrabber/tests/opencvGrabber_test.cpp new file mode 100644 index 0000000000..0a19dcb2dd --- /dev/null +++ b/src/devices/opencvGrabber/tests/opencvGrabber_test.cpp @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2006-2021 Istituto Italiano di Tecnologia (IIT) + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include +#include +#include +#include +#include + +#include +#include + +using namespace yarp::dev; +using namespace yarp::os; + +TEST_CASE("dev::opencvGrabberTest", "[yarp::dev]") +{ + YARP_REQUIRE_PLUGIN("opencv_grabber", "device"); + + Network::setLocalMode(true); + + SECTION("Checking opencv_grabber device, opening an avi file") + { + PolyDriver dd; + + yarp::os::ResourceFinder res; + res.setDefaultContext("tests/opencvGrabber"); + std::string filepath = res.findFileByName("test.avi"); + + ////////"Checking opening polydriver" + { + Property cfg; + cfg.put("device", "opencv_grabber"); + cfg.put("movie", filepath); + REQUIRE(dd.open(cfg)); + } + + yarp::os::Time::delay(1.0); + + //"Close all polydrivers and check" + { + CHECK(dd.close()); + } + } + + Network::setLocalMode(false); +} diff --git a/src/devices/opencvGrabber/tests/test.avi b/src/devices/opencvGrabber/tests/test.avi new file mode 100644 index 0000000000000000000000000000000000000000..ce1b692b9bd11e0dba561f4b85088bebbf4f2396 GIT binary patch literal 303614 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 literal 0 HcmV?d00001