Added SensorCapture and SensorCaptureThread doc and tests

This commit is contained in:
matlabbe
2025-12-24 17:33:18 -08:00
parent d0fb4ab810
commit ffb3be7ba3
5 changed files with 1628 additions and 67 deletions
+194 -13
View File
@@ -43,50 +43,231 @@ namespace rtabmap
{ {
/** /**
* Class Camera * @class SensorCapture
* * @brief Abstract base class for sensor data capture (cameras, lidars, etc.)
*
* SensorCapture provides a unified interface for capturing sensor data from various
* sensor types including cameras (RGB-D, stereo, mono) and lidars. It handles frame
* rate control, local transform management, and provides a common API for sensor
* initialization and data capture.
*
* The class implements the Template Method pattern:
* - `takeData()` handles frame rate control and timing, then calls the pure virtual
* `captureData()` method implemented by derived classes
* - Derived classes must implement `captureData()` to perform the actual sensor capture
*
* Key features:
* - **Frame rate control**: Automatic throttling to maintain target frame rate
* - **Local transform**: Transform from robot base frame to sensor frame
* - **Capture timing**: Tracks capture time and provides it via SensorCaptureInfo
* - **Sequence IDs**: Automatic sequence number generation for captured data
*
* Derived classes include:
* - **Camera**: Base class for all camera types (RGB-D, stereo, mono, file readers, etc.)
* - **Lidar**: Base class for lidar sensors
*
* @note This is an abstract class. Use concrete implementations like CameraRGBD,
* CameraStereo, LidarVLP16, etc., or create custom derived classes.
*
* @see Camera
* @see Lidar
* @see SensorData
* @see SensorCaptureInfo
* @see SensorCaptureThread
*/ */
class RTABMAP_CORE_EXPORT SensorCapture class RTABMAP_CORE_EXPORT SensorCapture
{ {
public: public:
/**
* @brief Virtual destructor
*/
virtual ~SensorCapture(); virtual ~SensorCapture();
/**
* @brief Captures sensor data with frame rate control
*
* This method handles frame rate throttling and timing, then calls the
* pure virtual `captureData()` method to perform the actual capture.
*
* The method:
* - Enforces the target frame rate by sleeping if necessary
* - Measures capture time and stores it in SensorCaptureInfo
* - Assigns sequence IDs to captured data
* - Warns if the target frame rate cannot be reached
*
* @param info Optional pointer to SensorCaptureInfo to fill with capture metadata
* (ID, timestamp, capture time). If null, no info is filled.
* @return SensorData containing the captured sensor data
*
* @note If frame rate is 0, data is captured as fast as possible without throttling.
* @note The returned SensorData should have rectified images if calibration was loaded.
*
* @see captureData()
*/
SensorData takeData(SensorCaptureInfo * info = 0); SensorData takeData(SensorCaptureInfo * info = 0);
/**
* @brief Initializes the sensor
*
* Pure virtual method that must be implemented by derived classes to initialize
* the sensor hardware or data source. This typically involves:
* - Opening device connections or file streams
* - Loading camera calibration parameters
* - Configuring sensor settings
*
* @param calibrationFolder Directory path where calibration files are located
* (default: current directory ".")
* @param cameraName Base name of the camera for loading calibration files
* (default: empty string)
* @return True if initialization was successful, false otherwise
*
* @note Must be called before calling takeData() or captureData()
*/
virtual bool init(const std::string & calibrationFolder = ".", const std::string & cameraName = "") = 0; virtual bool init(const std::string & calibrationFolder = ".", const std::string & cameraName = "") = 0;
/**
* @brief Returns the sensor's serial number or unique identifier
*
* Pure virtual method that must be implemented by derived classes to return
* a unique identifier for the sensor (e.g., device serial number, file path,
* or other identifier).
*
* @return String identifier for the sensor
*/
virtual std::string getSerial() const = 0; virtual std::string getSerial() const = 0;
/**
* @brief Checks if the sensor provides odometry poses
*
* Some sensors (e.g., visual-inertial cameras) can provide pose estimates
* directly. This method indicates whether the sensor supports pose queries.
*
* @return True if the sensor provides odometry poses, false otherwise
*
* @note Default implementation returns false. Derived classes should override
* if they support pose estimation.
*
* @see getPose()
*/
virtual bool odomProvided() const { return false; } virtual bool odomProvided() const { return false; }
/**
* @brief Gets the sensor's pose estimate at a specific timestamp
*
* Queries the sensor for its pose estimate at the given timestamp. This is
* typically used for sensors that provide visual-inertial odometry or other
* pose estimation capabilities.
*
* @param stamp Timestamp in seconds for which to query the pose
* @param[out] pose Output transform representing the sensor pose
* @param[out] covariance Output covariance matrix (6x6) representing twist uncertainty
* @param maxWaitTime Maximum time in seconds to wait for pose data (default: 0.06)
* @return True if pose was successfully retrieved, false otherwise
*
* @note Default implementation returns false. Derived classes should override
* if they support pose estimation.
*
* @see odomProvided()
*/
virtual bool getPose(double stamp, Transform & pose, cv::Mat & covariance, double maxWaitTime = 0.06) { return false; } virtual bool getPose(double stamp, Transform & pose, cv::Mat & covariance, double maxWaitTime = 0.06) { return false; }
//getters // Getters
/**
* @brief Returns the target frame rate
* @return Frame rate in Hz (0 = unlimited, capture as fast as possible)
*/
float getFrameRate() const {return _frameRate;} float getFrameRate() const {return _frameRate;}
/**
* @brief Returns the local transform from base frame to sensor frame
* @return Const reference to the local transform
*/
const Transform & getLocalTransform() const {return _localTransform;} const Transform & getLocalTransform() const {return _localTransform;}
//setters // Setters
/**
* @brief Sets the target frame rate
*
* Controls how often `takeData()` will capture data. The method will throttle
* captures to maintain the target rate.
*
* @param frameRate Target frame rate in Hz (0 = unlimited, capture as fast as possible)
*
* @note Setting frame rate to 0 disables throttling and captures as fast as possible.
*/
void setFrameRate(float frameRate) {_frameRate = frameRate;} void setFrameRate(float frameRate) {_frameRate = frameRate;}
/**
* @brief Sets the local transform from base frame to sensor frame
*
* The local transform represents the pose of the sensor relative to the robot's
* base frame. This is used to transform sensor data into the robot's coordinate system.
*
* @param localTransform Transform from base frame to sensor frame
*/
void setLocalTransform(const Transform & localTransform) {_localTransform= localTransform;} void setLocalTransform(const Transform & localTransform) {_localTransform= localTransform;}
/**
* @brief Resets the frame rate timer
*
* Resets the internal timer used for frame rate control. This is useful when
* starting a new capture session or after a pause.
*/
void resetTimer(); void resetTimer();
protected: protected:
/** /**
* Constructor * @brief Protected constructor
* *
* @param frameRate the frame rate (Hz), 0 for fast as the sensor can * Creates a SensorCapture instance with the specified frame rate and local transform.
* @param localTransform the transform from base frame to sensor frame * This constructor is protected because SensorCapture is an abstract base class
* and should not be instantiated directly.
*
* @param frameRate Target frame rate in Hz (0 = unlimited, capture as fast as possible)
* @param localTransform Transform from base frame to sensor frame (default: identity)
*/ */
SensorCapture(float frameRate = 0, const Transform & localTransform = Transform::getIdentity()); SensorCapture(float frameRate = 0, const Transform & localTransform = Transform::getIdentity());
/** /**
* returned rgb and depth images should be already rectified if calibration was loaded * @brief Pure virtual method to capture sensor data
*
* This method must be implemented by derived classes to perform the actual
* sensor data capture. It is called by `takeData()` after frame rate throttling.
*
* The returned SensorData should:
* - Have rectified images if calibration was loaded during `init()`
* - Include proper timestamps
* - Contain valid sensor data (images, depth, laser scans, etc.)
*
* @param info Optional pointer to SensorCaptureInfo to fill with capture metadata.
* The base class will fill ID, timestamp, and capture time, but derived
* classes can add additional information.
* @return SensorData containing the captured sensor data
*
* @note If capture fails, return an empty SensorData (id=0, stamp=0.0).
* @note RGB and depth images should be already rectified if calibration was loaded.
*
* @see takeData()
*/ */
virtual SensorData captureData(SensorCaptureInfo * info = 0) = 0; virtual SensorData captureData(SensorCaptureInfo * info = 0) = 0;
/**
* @brief Gets the next sequence ID
*
* Returns and increments the internal sequence counter. This is used to assign
* unique sequence numbers to captured data.
*
* @return Next sequence ID (starts at 1, increments with each call)
*/
int getNextSeqID() {return ++_seq;} int getNextSeqID() {return ++_seq;}
private: private:
float _frameRate; float _frameRate; ///< Target frame rate in Hz (0 = unlimited)
Transform _localTransform; Transform _localTransform; ///< Transform from base frame to sensor frame
UTimer * _frameRateTimer; UTimer * _frameRateTimer; ///< Timer for frame rate control
int _seq; int _seq; ///< Sequence counter for captured data
}; };
@@ -52,23 +52,86 @@ class IMUFilter;
class Feature2D; class Feature2D;
/** /**
* Class CameraThread * @class SensorCaptureThread
* * @brief Thread-based sensor data capture and event posting for RTAB-Map
*
* SensorCaptureThread is a multi-threaded class that continuously captures sensor
* data from cameras and/or lidars in a background thread. It processes the data,
* optionally applies filtering and transformations, and posts SensorEvent events
* to registered handlers through RTAB-Map's event system.
*
* The class supports various sensor configurations:
* - **Camera-only**: Single or multiple cameras for RGB-D or stereo imaging
* - **Lidar-only**: Single lidar for 3D point cloud capture
* - **Camera + Lidar**: Combined visual and range sensing
* - **With odometry**: Optional odometry sensor for pose estimation and deskewing
*
* Key features:
* - **Automatic data synchronization**: Synchronizes camera and lidar data by timestamp
* - **Deskewing**: Corrects lidar scans using odometry poses
* - **Image processing**: Supports mirroring, decimation, histogram equalization, stereo-to-depth conversion
* - **Depth filtering**: Optional bilateral filtering for depth images
* - **IMU filtering**: Optional IMU data filtering and fusion
* - **Feature detection**: Optional automatic feature detection on captured images
* - **Frame rate control**: Configurable capture rate
* - **Event-driven architecture**: Posts SensorEvent events for downstream processing
*
* The class inherits from UThread (for threading) and UEventsSender (for event posting).
*
* @note Ownership of Camera, Lidar, and SensorCapture pointers is transferred to this class.
* @note The thread must be started with start() and stopped with kill() or join().
* @warning **Thread Safety**: All configuration parameters (setters) should be called
* **before** starting the thread with start(). Changing configuration after the
* thread is started is not thread-safe and may lead to race conditions or
* undefined behavior. If you need to change parameters at runtime, stop the
* thread first, modify the configuration, then restart it.
*
* @see Camera
* @see Lidar
* @see SensorCapture
* @see SensorEvent
* @see SensorData
* @see UThread
* @see UEventsSender
*/ */
class RTABMAP_CORE_EXPORT SensorCaptureThread : class RTABMAP_CORE_EXPORT SensorCaptureThread :
public UThread, public UThread,
public UEventsSender public UEventsSender
{ {
public: public:
// ownership transferred /**
* @brief Constructor for camera-only capture
*
* Creates a SensorCaptureThread that captures images from a single camera.
* The camera can be RGB-D, stereo, or mono.
*
* @param camera Pointer to the camera to capture from (ownership transferred)
* @param parameters Optional parameters map for configuration
*
* @note The camera pointer is owned by this class and will be deleted on destruction.
*/
SensorCaptureThread( SensorCaptureThread(
Camera * camera, Camera * camera,
const ParametersMap & parameters = ParametersMap()); const ParametersMap & parameters = ParametersMap());
/** /**
* @param camera the camera to take images from * @brief Constructor for camera with odometry sensor
* @param odomSensor an odometry sensor to get a pose (can be again the camera) *
* @param odomAsGt set odometry sensor pose as ground truth instead of odometry * Creates a SensorCaptureThread that captures images from a camera and uses
* @param extrinsics the static transform between odometry sensor's left lens frame to camera's left lens frame (without optical rotation) * an odometry sensor for pose estimation. The odometry sensor can be the same
* as the camera or a different sensor.
*
* @param camera Pointer to the camera to capture images from (ownership transferred)
* @param odomSensor Pointer to the odometry sensor for pose estimation (can be the same as camera)
* @param extrinsics Static transform from odometry sensor's left lens frame to camera's left lens frame
* (without optical rotation applied)
* @param poseTimeOffset Time offset in seconds to add to data timestamp when querying pose (default: 0.0)
* @param poseScaleFactor Scale factor to apply to pose translation (default: 1.0, no scaling)
* @param poseWaitTime Maximum time in seconds to wait for pose data (default: 0.1)
* @param parameters Optional parameters map for configuration
*
* @note If odomAsGt is set to true via setOdomAsGroundTruth(), the odometry pose
* will be used as ground truth instead of odometry.
*/ */
SensorCaptureThread( SensorCaptureThread(
Camera * camera, Camera * camera,
@@ -78,23 +141,55 @@ public:
float poseScaleFactor = 1.0f, float poseScaleFactor = 1.0f,
double poseWaitTime = 0.1, double poseWaitTime = 0.1,
const ParametersMap & parameters = ParametersMap()); const ParametersMap & parameters = ParametersMap());
/** /**
* @param lidar the lidar to take scans from * @brief Constructor for lidar-only capture
*
* Creates a SensorCaptureThread that captures 3D point clouds from a lidar.
*
* @param lidar Pointer to the lidar to capture scans from (ownership transferred)
* @param parameters Optional parameters map for configuration
*
* @note The lidar pointer is owned by this class and will be deleted on destruction.
*/ */
SensorCaptureThread( SensorCaptureThread(
Lidar * lidar, Lidar * lidar,
const ParametersMap & parameters = ParametersMap()); const ParametersMap & parameters = ParametersMap());
/** /**
* @param lidar the lidar to take scans from * @brief Constructor for lidar with camera
* @param camera the camera to take images from. If the camera is providing a pose, it can be used for deskewing *
* Creates a SensorCaptureThread that captures both lidar scans and camera images.
* If the camera provides pose information, it can be used for deskewing lidar scans.
*
* @param lidar Pointer to the lidar to capture scans from (ownership transferred)
* @param camera Pointer to the camera to capture images from (ownership transferred).
* If the camera provides pose, it can be used for deskewing.
* @param parameters Optional parameters map for configuration
*/ */
SensorCaptureThread( SensorCaptureThread(
Lidar * lidar, Lidar * lidar,
Camera * camera, Camera * camera,
const ParametersMap & parameters = ParametersMap()); const ParametersMap & parameters = ParametersMap());
/** /**
* @param lidar the lidar to take scans from * @brief Constructor for lidar with odometry sensor
* @param odomSensor an odometry sensor to get a pose and used for deskewing (can be again the lidar) *
* Creates a SensorCaptureThread that captures lidar scans and uses an odometry
* sensor for pose estimation and deskewing. The odometry sensor can be the same
* as the lidar or a different sensor.
*
* @param lidar Pointer to the lidar to capture scans from (ownership transferred)
* @param odomSensor Pointer to the odometry sensor for pose estimation and deskewing
* (can be the same as lidar)
* @param poseTimeOffset Time offset in seconds to add to data timestamp when querying pose (default: 0.0)
* @param poseScaleFactor Scale factor to apply to pose translation (default: 1.0, no scaling)
* @param poseWaitTime Maximum time in seconds to wait for pose data (default: 0.1)
* @param parameters Optional parameters map for configuration
*
* @note Deskewing is enabled by default when an odometry sensor is provided.
* Use setScanParameters() to configure deskewing behavior.
* @note The lidar's local transform should be set as the extrinsics between odometry sensor's frame and lidar's frame.
*/ */
SensorCaptureThread( SensorCaptureThread(
Lidar * lidar, Lidar * lidar,
@@ -103,11 +198,28 @@ public:
float poseScaleFactor = 1.0f, float poseScaleFactor = 1.0f,
double poseWaitTime = 0.1, double poseWaitTime = 0.1,
const ParametersMap & parameters = ParametersMap()); const ParametersMap & parameters = ParametersMap());
/** /**
* @param lidar the lidar to take scans from * @brief Constructor for lidar with camera and odometry sensor
* @param camera the camera to take images from *
* @param odomSensor an odometry sensor to get a pose and used for deskewing (can be again the camera or lidar) * Creates a SensorCaptureThread that captures both lidar scans and camera images,
* @param extrinsics the static transform between odometry frame to camera frame (without optical rotation) * and uses an odometry sensor for pose estimation and deskewing. This is the most
* comprehensive configuration, supporting multi-modal sensing with pose correction.
*
* @param lidar Pointer to the lidar to capture scans from (ownership transferred)
* @param camera Pointer to the camera to capture images from (ownership transferred)
* @param odomSensor Pointer to the odometry sensor for pose estimation and deskewing
* (can be the same as camera or lidar)
* @param extrinsics Static transform from odometry frame to camera frame
* (without optical rotation applied)
* @param poseTimeOffset Time offset in seconds to add to data timestamp when querying pose (default: 0.0)
* @param poseScaleFactor Scale factor to apply to pose translation (default: 1.0, no scaling)
* @param poseWaitTime Maximum time in seconds to wait for pose data (default: 0.1)
* @param parameters Optional parameters map for configuration
*
* @note The camera and lidar data are synchronized by timestamp, with the camera
* frame being at least as recent as the lidar frame.
* @note The lidar's local transform should be set as the extrinsics between odometry sensor's frame and lidar's frame.
*/ */
SensorCaptureThread( SensorCaptureThread(
Lidar * lidar, Lidar * lidar,
@@ -118,26 +230,182 @@ public:
float poseScaleFactor = 1.0f, float poseScaleFactor = 1.0f,
double poseWaitTime = 0.1, double poseWaitTime = 0.1,
const ParametersMap & parameters = ParametersMap()); const ParametersMap & parameters = ParametersMap());
/**
* @brief Virtual destructor
*
* Stops the capture thread if running and deletes owned sensor pointers.
*/
virtual ~SensorCaptureThread(); virtual ~SensorCaptureThread();
/**
* @brief Enables or disables image mirroring (horizontal flip)
* @param enabled True to enable mirroring, false to disable
*/
void setMirroringEnabled(bool enabled) {_mirroring = enabled;} void setMirroringEnabled(bool enabled) {_mirroring = enabled;}
/**
* @brief Enables or disables stereo exposure compensation
*
* When enabled, adjusts exposure between left and right stereo cameras
* to improve matching quality.
*
* @param enabled True to enable exposure compensation, false to disable
*/
void setStereoExposureCompensation(bool enabled) {_stereoExposureCompensation = enabled;} void setStereoExposureCompensation(bool enabled) {_stereoExposureCompensation = enabled;}
/**
* @brief Sets whether to capture color images only (skip depth)
* @param colorOnly True to capture color only, false to capture both color and depth
*/
void setColorOnly(bool colorOnly) {_colorOnly = colorOnly;} void setColorOnly(bool colorOnly) {_colorOnly = colorOnly;}
/**
* @brief Sets image decimation factor
*
* Decimation reduces image resolution by skipping pixels. A decimation of N
* means every Nth pixel is kept (e.g., 2 = half resolution, 4 = quarter resolution).
*
* @param decimation Decimation factor (1 = no decimation, 2 = half resolution, etc.)
*/
void setImageDecimation(int decimation) {_imageDecimation = decimation;} void setImageDecimation(int decimation) {_imageDecimation = decimation;}
/**
* @brief Sets histogram equalization method
*
* Controls the type of histogram equalization applied to captured images.
* Histogram equalization improves image contrast by redistributing pixel intensities.
*
* @param histogramMethod Histogram equalization method:
* - 0 = None (disabled)
* - 1 = Standard histogram equalization (cv::equalizeHist)
* - 2 = CLAHE - Contrast Limited Adaptive Histogram Equalization (cv::createCLAHE)
*
* @note For color images, histogram equalization is applied only to the luminance channel (Y in YCrCb color space).
* @note For stereo images, histogram equalization is applied to both left and right images.
*/
void setHistogramMethod(int histogramMethod) {_histogramMethod = histogramMethod;} void setHistogramMethod(int histogramMethod) {_histogramMethod = histogramMethod;}
/**
* @brief Enables or disables stereo-to-depth conversion
*
* When enabled, converts stereo images to depth images using dense stereo matching.
*
* @param enabled True to enable stereo-to-depth conversion, false to disable
*/
void setStereoToDepth(bool enabled) {_stereoToDepth = enabled;} void setStereoToDepth(bool enabled) {_stereoToDepth = enabled;}
/**
* @brief Sets the target frame rate for capture
*
* Controls how often the capture loop runs. A frame rate of 0 means capture
* as fast as possible.
*
* @param frameRate Target frame rate in Hz (0 = unlimited)
*/
void setFrameRate(float frameRate); void setFrameRate(float frameRate);
/**
* @deprecated Use setFrameRate() instead
* @brief Sets the target image capture rate (deprecated)
*/
RTABMAP_DEPRECATED void setImageRate(float frameRate) {setFrameRate(frameRate);} RTABMAP_DEPRECATED void setImageRate(float frameRate) {setFrameRate(frameRate);}
/**
* @brief Sets the depth distortion model file path
*
* Loads a discrete depth distortion model from the specified file path.
* This is used to correct systematic depth errors at longer ranges using
* the CLAMS (Calibration, Localization, And Mapping System) approach.
*
* The distortion model is typically created using RTAB-Map's depth calibration
* tool, which uses visual odometry and 3D mapping to generate ground truth
* depth for calibration.
*
* @param path Path to the distortion model file
*
* @see https://github.com/introlab/rtabmap/wiki/Depth-Calibration
* for detailed instructions on how to create and use depth distortion models
*/
void setDistortionModel(const std::string & path); void setDistortionModel(const std::string & path);
/**
* @brief Sets whether odometry poses should be treated as ground truth
*
* When enabled, odometry poses from the odometry sensor are stored as ground
* truth poses instead of odometry poses in the SensorData.
*
* @param enabled True to use odometry as ground truth, false to use as odometry
*/
void setOdomAsGroundTruth(bool enabled) {_odomAsGt = enabled;} void setOdomAsGroundTruth(bool enabled) {_odomAsGt = enabled;}
/**
* @brief Enables bilateral filtering for depth images
*
* Bilateral filtering reduces noise in depth images while preserving edges.
*
* @param sigmaS Spatial standard deviation (pixels)
* @param sigmaR Range standard deviation (depth units)
*/
void enableBilateralFiltering(float sigmaS, float sigmaR); void enableBilateralFiltering(float sigmaS, float sigmaR);
/**
* @brief Disables bilateral filtering
*/
void disableBilateralFiltering() {_bilateralFiltering = false;} void disableBilateralFiltering() {_bilateralFiltering = false;}
/**
* @brief Enables IMU data filtering
*
* Enables filtering and fusion of IMU data from the sensor. The filtering
* strategy determines how IMU data is processed to estimate orientation.
*
* @param filteringStrategy Filtering strategy:
* - 0 = Madgwick filter (requires RTAB-Map built with RTABMAP_MADGWICK option)
* - 1 = Complementary filter (default)
* @param parameters Optional parameters map for IMU filter configuration
* @param baseFrameConversion If true, converts IMU data to base frame
*
* @note If Madgwick filter is requested but RTAB-Map is not built with the option enabled,
* the Complementary filter will be used instead.
*
* @see IMUFilter
*/
void enableIMUFiltering(int filteringStrategy=1, const ParametersMap & parameters = ParametersMap(), bool baseFrameConversion = false); void enableIMUFiltering(int filteringStrategy=1, const ParametersMap & parameters = ParametersMap(), bool baseFrameConversion = false);
/**
* @brief Disables IMU data filtering
*/
void disableIMUFiltering(); void disableIMUFiltering();
/**
* @brief Enables automatic feature detection on captured images
*
* When enabled, automatically detects and extracts visual features (keypoints,
* descriptors) from captured images.
*
* @param parameters Optional parameters map for feature detector configuration.
* Parameters from the "Vis/" group (e.g., Vis/FeatureType, Vis/MaxFeatures)
* are automatically converted to "Kp/" parameters internally.
* If both Vis/ and Kp/ parameters are provided, Vis/ parameters take precedence.
*
* @note The function converts Vis/ parameters to Kp/ parameters before creating the Feature2D detector.
*/
void enableFeatureDetection(const ParametersMap & parameters = ParametersMap()); void enableFeatureDetection(const ParametersMap & parameters = ParametersMap());
/**
* @brief Disables automatic feature detection
*/
void disableFeatureDetection(); void disableFeatureDetection();
// Use new version of this function with groundNormalsUp=0.8 for forceGroundNormalsUp=True and groundNormalsUp=0.0 for forceGroundNormalsUp=False. /**
* @deprecated Use the new version with groundNormalsUp parameter instead
* @brief Sets lidar scan processing parameters (deprecated)
*
* Use the new version with groundNormalsUp parameter:
* - groundNormalsUp=0.8 for forceGroundNormalsUp=True
* - groundNormalsUp=0.0 for forceGroundNormalsUp=False
*/
RTABMAP_DEPRECATED void setScanParameters( RTABMAP_DEPRECATED void setScanParameters(
bool fromDepth, bool fromDepth,
int downsampleStep, // decimation of the depth image in case the scan is from depth image int downsampleStep, // decimation of the depth image in case the scan is from depth image
@@ -148,6 +416,27 @@ public:
float normalsRadius, float normalsRadius,
bool forceGroundNormalsUp, bool forceGroundNormalsUp,
bool deskewing); bool deskewing);
/**
* @brief Sets lidar scan processing parameters
*
* Configures how lidar scans are processed, including conversion from depth images,
* filtering, normal estimation, and deskewing.
*
* @param fromDepth If true, generate scans from depth images instead of raw lidar data
* @param downsampleStep Downsampling step:
* - If fromDepth is true: Image decimation step for depth images
* (1 = no decimation, 2 = every other pixel in both dimensions, etc.)
* - If fromDepth is false: Point skipping step for laser scan data
* (1 = no skipping, 2 = every other point, etc.)
* @param rangeMin Minimum range in meters (points closer are filtered out, 0 = no minimum)
* @param rangeMax Maximum range in meters (points farther are filtered out, 0 = no maximum)
* @param voxelSize Voxel size in meters for downsampling (0 = no voxelization)
* @param normalsK Number of neighbors for K-nearest neighbors normal estimation (0 = no normals)
* @param normalsRadius Radius in meters for radius-based normal estimation (0 = use K-nearest)
* @param groundNormalsUp Threshold for forcing ground normals upward (0.0-1.0, 0.8 = strong, 0.0 = disabled)
* @param deskewing If true, apply deskewing using odometry poses
*/
void setScanParameters( void setScanParameters(
bool fromDepth, bool fromDepth,
int downsampleStep=1, // decimation of the depth image in case the scan is from depth image int downsampleStep=1, // decimation of the depth image in case the scan is from depth image
@@ -159,58 +448,120 @@ public:
float groundNormalsUp = 0.0f, float groundNormalsUp = 0.0f,
bool deskewing = false); bool deskewing = false);
/**
* @brief Post-processing hook called before posting SensorEvent
*
* This method is called after all processing is complete but before the
* SensorEvent is posted. Derived classes can override this to perform
* additional processing or modifications to the data.
*
* @param data Pointer to the processed sensor data (can be modified)
* @param info Pointer to the sensor capture info (can be modified, may be null)
*/
void postUpdate(SensorData * data, SensorCaptureInfo * info = 0) const; void postUpdate(SensorData * data, SensorCaptureInfo * info = 0) const;
//getters // Getters
/**
* @brief Checks if the capture thread is paused
* @return True if paused (not running), false if running
*/
bool isPaused() const {return !this->isRunning();} bool isPaused() const {return !this->isRunning();}
/**
* @brief Checks if the capture thread is actively capturing
* @return True if capturing (running), false if not running
*/
bool isCapturing() const {return this->isRunning();} bool isCapturing() const {return this->isRunning();}
/**
* @brief Checks if odometry is provided by the sensors
* @return True if an odometry sensor is configured, false otherwise
*/
bool odomProvided() const; bool odomProvided() const;
Camera * camera() {return _camera;} // return null if not set, valid until CameraThread is deleted /**
SensorCapture * odomSensor() {return _odomSensor;} // return null if not set, valid until CameraThread is deleted * @brief Returns the camera pointer
Lidar * lidar() {return _lidar;} // return null if not set, valid until CameraThread is deleted * @return Pointer to the camera (null if not set). Valid until SensorCaptureThread is deleted.
*/
Camera * camera() {return _camera;}
/**
* @brief Returns the odometry sensor pointer
* @return Pointer to the odometry sensor (null if not set). Valid until SensorCaptureThread is deleted.
*/
SensorCapture * odomSensor() {return _odomSensor;}
/**
* @brief Returns the lidar pointer
* @return Pointer to the lidar (null if not set). Valid until SensorCaptureThread is deleted.
*/
Lidar * lidar() {return _lidar;}
private: private:
/**
* @brief Called once when the thread starts (before mainLoop)
*
* Initializes resources needed for the capture loop.
*/
virtual void mainLoopBegin(); virtual void mainLoopBegin();
/**
* @brief Main capture loop executed in the thread
*
* Continuously captures sensor data, processes it, and posts SensorEvent events.
* This method runs until the thread is killed.
*/
virtual void mainLoop(); virtual void mainLoop();
/**
* @brief Called when the thread is being killed
*
* Performs cleanup and ensures proper shutdown of sensors.
*/
virtual void mainLoopKill(); virtual void mainLoopKill();
private: private:
Camera * _camera; Camera * _camera; ///< Camera for image capture (owned, null if not used)
SensorCapture * _odomSensor; SensorCapture * _odomSensor; ///< Odometry sensor for pose estimation (owned, null if not used)
Lidar * _lidar; Lidar * _lidar; ///< Lidar for scan capture (owned, null if not used)
Transform _extrinsicsOdomToCamera; Transform _extrinsicsOdomToCamera; ///< Static transform from odometry frame to camera frame
bool _odomAsGt; bool _odomAsGt; ///< If true, use odometry poses as ground truth instead of odometry
double _poseTimeOffset; double _poseTimeOffset; ///< Time offset in seconds when querying poses
float _poseScaleFactor; float _poseScaleFactor; ///< Scale factor to apply to pose translation
double _poseWaitTime; double _poseWaitTime; ///< Maximum time to wait for pose data
bool _mirroring; bool _mirroring; ///< Enable horizontal image mirroring
bool _stereoExposureCompensation; bool _stereoExposureCompensation; ///< Enable stereo exposure compensation
bool _colorOnly; bool _colorOnly; ///< Capture color images only (skip depth)
int _imageDecimation; int _imageDecimation; ///< Image decimation factor (1 = no decimation)
int _histogramMethod; int _histogramMethod; ///< Histogram equalization method
bool _stereoToDepth; bool _stereoToDepth; ///< Convert stereo images to depth using dense matching
bool _scanDeskewing; bool _scanDeskewing; ///< Enable lidar scan deskewing using odometry
bool _scanFromDepth; bool _scanFromDepth; ///< Generate scans from depth images instead of raw lidar
int _scanDownsampleStep; int _scanDownsampleStep; ///< Decimation step for depth-to-scan conversion
float _scanRangeMin; float _scanRangeMin; ///< Minimum scan range in meters (0 = no minimum)
float _scanRangeMax; float _scanRangeMax; ///< Maximum scan range in meters (0 = no maximum)
float _scanVoxelSize; float _scanVoxelSize; ///< Voxel size for scan downsampling (0 = no voxelization)
int _scanNormalsK; int _scanNormalsK; ///< K-nearest neighbors for normal estimation (0 = disabled)
float _scanNormalsRadius; float _scanNormalsRadius; ///< Radius for normal estimation (0 = use K-nearest)
float _scanForceGroundNormalsUp; float _scanForceGroundNormalsUp; ///< Threshold for forcing ground normals upward (0.0-1.0)
StereoDense * _stereoDense; StereoDense * _stereoDense; ///< Dense stereo matcher for stereo-to-depth conversion (owned)
clams::DiscreteDepthDistortionModel * _distortionModel; clams::DiscreteDepthDistortionModel * _distortionModel; ///< Depth distortion correction model (owned)
bool _bilateralFiltering; bool _bilateralFiltering; ///< Enable bilateral filtering for depth images
float _bilateralSigmaS; float _bilateralSigmaS; ///< Spatial standard deviation for bilateral filtering
float _bilateralSigmaR; float _bilateralSigmaR; ///< Range standard deviation for bilateral filtering
IMUFilter * _imuFilter; IMUFilter * _imuFilter; ///< IMU data filter (owned, null if disabled)
bool _imuBaseFrameConversion; bool _imuBaseFrameConversion; ///< Convert IMU data to base frame
Feature2D * _featureDetector; Feature2D * _featureDetector; ///< Feature detector for automatic feature extraction (owned, null if disabled)
bool _depthAsMask; bool _depthAsMask; ///< Use depth as mask for feature detection
}; };
//backward compatibility /**
* @deprecated Use SensorCaptureThread instead
* @brief Backward compatibility typedef
*
* CameraThread is deprecated. Use SensorCaptureThread instead.
*/
RTABMAP_DEPRECATED typedef SensorCaptureThread CameraThread; RTABMAP_DEPRECATED typedef SensorCaptureThread CameraThread;
} // namespace rtabmap } // namespace rtabmap
+10
View File
@@ -105,3 +105,13 @@ gtest_discover_tests(test_sensorevent)
add_executable(test_sensordata test_sensordata.cpp) add_executable(test_sensordata test_sensordata.cpp)
target_link_libraries(test_sensordata gtest_main rtabmap_core) target_link_libraries(test_sensordata gtest_main rtabmap_core)
gtest_discover_tests(test_sensordata) gtest_discover_tests(test_sensordata)
#SensorCapture.h
add_executable(test_sensorcapture test_sensorcapture.cpp)
target_link_libraries(test_sensorcapture gtest_main rtabmap_core)
gtest_discover_tests(test_sensorcapture)
#SensorCaptureThread.h
add_executable(test_sensorcapturethread test_sensorcapturethread.cpp)
target_link_libraries(test_sensorcapturethread gtest_main rtabmap_core)
gtest_discover_tests(test_sensorcapturethread)
+447
View File
@@ -0,0 +1,447 @@
#include <gtest/gtest.h>
#include <rtabmap/core/SensorCapture.h>
#include <rtabmap/core/SensorData.h>
#include <rtabmap/core/SensorCaptureInfo.h>
#include <rtabmap/core/Transform.h>
#include <rtabmap/utilite/UTimer.h>
#include <opencv2/core.hpp>
using namespace rtabmap;
// Mock SensorCapture implementation for testing
class MockSensorCapture : public SensorCapture
{
public:
MockSensorCapture(float frameRate = 0, const Transform & localTransform = Transform::getIdentity()) :
SensorCapture(frameRate, localTransform),
initCalled_(false),
initResult_(true),
serial_("MOCK_SENSOR_001"),
odomProvided_(false),
captureCount_(0)
{
}
virtual ~MockSensorCapture() {}
virtual bool init(const std::string & calibrationFolder = ".", const std::string & cameraName = "")
{
initCalled_ = true;
calibrationFolder_ = calibrationFolder;
cameraName_ = cameraName;
return initResult_;
}
virtual std::string getSerial() const
{
return serial_;
}
virtual bool odomProvided() const
{
return odomProvided_;
}
virtual bool getPose(double stamp, Transform & pose, cv::Mat & covariance, double maxWaitTime = 0.06)
{
if (odomProvided_)
{
pose = Transform(1, 0, 0, 0, 0, 0, 1);
covariance = cv::Mat::eye(6, 6, CV_64FC1);
return true;
}
return false;
}
// Test helpers
void setInitResult(bool result) { initResult_ = result; }
void setSerial(const std::string & serial) { serial_ = serial; }
void setOdomProvided(bool provided) { odomProvided_ = true; }
bool wasInitCalled() const { return initCalled_; }
std::string getCalibrationFolder() const { return calibrationFolder_; }
std::string getCameraName() const { return cameraName_; }
int getCaptureCount() const { return captureCount_; }
protected:
virtual SensorData captureData(SensorCaptureInfo * info = 0)
{
++captureCount_;
cv::Mat image = cv::Mat::ones(480, 640, CV_8UC3) * 128;
SensorData data(image, getNextSeqID(), UTimer::now());
return data;
}
private:
bool initCalled_;
bool initResult_;
std::string serial_;
bool odomProvided_;
std::string calibrationFolder_;
std::string cameraName_;
int captureCount_;
};
// Constructor Tests
TEST(SensorCaptureTest, DefaultConstructor)
{
MockSensorCapture sensor;
EXPECT_EQ(sensor.getFrameRate(), 0.0f);
EXPECT_TRUE(sensor.getLocalTransform().isIdentity());
EXPECT_FALSE(sensor.wasInitCalled());
EXPECT_EQ(sensor.getSerial(), "MOCK_SENSOR_001");
EXPECT_FALSE(sensor.odomProvided());
}
TEST(SensorCaptureTest, ConstructorWithFrameRate)
{
float frameRate = 30.0f;
MockSensorCapture sensor(frameRate);
EXPECT_FLOAT_EQ(sensor.getFrameRate(), frameRate);
EXPECT_TRUE(sensor.getLocalTransform().isIdentity());
}
TEST(SensorCaptureTest, ConstructorWithLocalTransform)
{
Transform localTransform(1, 0, 0, 0, 0, 0, 1);
MockSensorCapture sensor(0, localTransform);
EXPECT_EQ(sensor.getLocalTransform(), localTransform);
}
TEST(SensorCaptureTest, ConstructorWithFrameRateAndTransform)
{
float frameRate = 15.0f;
Transform localTransform(0, 0, 1, 0, -1, 0, 0, 0, 0, -1, 0, 0);
MockSensorCapture sensor(frameRate, localTransform);
EXPECT_FLOAT_EQ(sensor.getFrameRate(), frameRate);
EXPECT_EQ(sensor.getLocalTransform(), localTransform);
}
// Init Tests
TEST(SensorCaptureTest, InitDefault)
{
MockSensorCapture sensor;
EXPECT_TRUE(sensor.init());
EXPECT_TRUE(sensor.wasInitCalled());
EXPECT_EQ(sensor.getCalibrationFolder(), ".");
EXPECT_EQ(sensor.getCameraName(), "");
}
TEST(SensorCaptureTest, InitWithCalibrationFolder)
{
MockSensorCapture sensor;
std::string folder = "/path/to/calibration";
EXPECT_TRUE(sensor.init(folder));
EXPECT_EQ(sensor.getCalibrationFolder(), folder);
EXPECT_EQ(sensor.getCameraName(), "");
}
TEST(SensorCaptureTest, InitWithCalibrationFolderAndCameraName)
{
MockSensorCapture sensor;
std::string folder = "/path/to/calibration";
std::string cameraName = "camera1";
EXPECT_TRUE(sensor.init(folder, cameraName));
EXPECT_EQ(sensor.getCalibrationFolder(), folder);
EXPECT_EQ(sensor.getCameraName(), cameraName);
}
TEST(SensorCaptureTest, InitFailure)
{
MockSensorCapture sensor;
sensor.setInitResult(false);
EXPECT_FALSE(sensor.init());
EXPECT_TRUE(sensor.wasInitCalled());
}
// Serial Tests
TEST(SensorCaptureTest, GetSerial)
{
MockSensorCapture sensor;
EXPECT_EQ(sensor.getSerial(), "MOCK_SENSOR_001");
sensor.setSerial("CUSTOM_SERIAL_123");
EXPECT_EQ(sensor.getSerial(), "CUSTOM_SERIAL_123");
}
// Frame Rate Tests
TEST(SensorCaptureTest, SetGetFrameRate)
{
MockSensorCapture sensor;
EXPECT_EQ(sensor.getFrameRate(), 0.0f);
sensor.setFrameRate(30.0f);
EXPECT_FLOAT_EQ(sensor.getFrameRate(), 30.0f);
sensor.setFrameRate(0.0f);
EXPECT_FLOAT_EQ(sensor.getFrameRate(), 0.0f);
sensor.setFrameRate(60.0f);
EXPECT_FLOAT_EQ(sensor.getFrameRate(), 60.0f);
}
// Local Transform Tests
TEST(SensorCaptureTest, SetGetLocalTransform)
{
MockSensorCapture sensor;
EXPECT_TRUE(sensor.getLocalTransform().isIdentity());
Transform transform(1, 0, 0, 0, 0, 0, 1);
sensor.setLocalTransform(transform);
EXPECT_EQ(sensor.getLocalTransform(), transform);
Transform transform2(0, 0, 1, 0, -1, 0, 0, 0, 0, -1, 0, 0);
sensor.setLocalTransform(transform2);
EXPECT_EQ(sensor.getLocalTransform(), transform2);
}
// Reset Timer Tests
TEST(SensorCaptureTest, ResetTimer)
{
MockSensorCapture sensor;
// Reset should not throw
EXPECT_NO_THROW(sensor.resetTimer());
// After reset, timer should be ready for frame rate control
sensor.setFrameRate(10.0f);
sensor.resetTimer();
// First capture should be fast (no throttling needed)
SensorData data = sensor.takeData();
EXPECT_TRUE(data.isValid());
}
// TakeData Tests
TEST(SensorCaptureTest, TakeDataBasic)
{
MockSensorCapture sensor;
SensorData data = sensor.takeData();
EXPECT_TRUE(data.isValid());
EXPECT_GT(data.id(), 0);
EXPECT_GT(data.stamp(), 0.0);
EXPECT_FALSE(data.imageRaw().empty());
EXPECT_EQ(sensor.getCaptureCount(), 1);
}
TEST(SensorCaptureTest, TakeDataWithInfo)
{
MockSensorCapture sensor;
SensorCaptureInfo info;
SensorData data = sensor.takeData(&info);
EXPECT_TRUE(data.isValid());
EXPECT_EQ(info.id, data.id());
EXPECT_DOUBLE_EQ(info.stamp, data.stamp());
EXPECT_GT(info.timeCapture, 0.0);
EXPECT_LE(info.timeCapture, 0.1); // Should be very fast for mock
}
TEST(SensorCaptureTest, TakeDataSequenceIDs)
{
MockSensorCapture sensor;
SensorData data1 = sensor.takeData();
SensorData data2 = sensor.takeData();
SensorData data3 = sensor.takeData();
EXPECT_EQ(data1.id() + 1, data2.id());
EXPECT_EQ(data2.id() + 1, data3.id());
EXPECT_EQ(sensor.getCaptureCount(), 3);
}
TEST(SensorCaptureTest, TakeDataNoFrameRateThrottling)
{
MockSensorCapture sensor;
sensor.setFrameRate(0.0f); // Unlimited
UTimer timer;
SensorData data1 = sensor.takeData();
SensorData data2 = sensor.takeData();
double elapsed = timer.ticks();
// Without throttling, captures should be very fast
EXPECT_LT(elapsed, 0.1);
EXPECT_TRUE(data1.isValid());
EXPECT_TRUE(data2.isValid());
}
TEST(SensorCaptureTest, TakeDataWithFrameRateThrottling)
{
MockSensorCapture sensor;
sensor.setFrameRate(10.0f); // 10 Hz = 100ms per frame
sensor.resetTimer();
UTimer timer;
SensorData data1 = sensor.takeData();
SensorData data2 = sensor.takeData();
double elapsed = timer.ticks();
// With 10 Hz throttling, two captures should take at least ~100ms
EXPECT_GE(elapsed, 0.2); // Should be at least 200 ms for two frames
EXPECT_LE(elapsed, 0.21); // Slight margin
EXPECT_TRUE(data1.isValid());
EXPECT_TRUE(data2.isValid());
}
// OdomProvided Tests
TEST(SensorCaptureTest, OdomProvidedDefault)
{
MockSensorCapture sensor;
EXPECT_FALSE(sensor.odomProvided());
}
TEST(SensorCaptureTest, OdomProvidedEnabled)
{
MockSensorCapture sensor;
sensor.setOdomProvided(true);
EXPECT_TRUE(sensor.odomProvided());
}
// GetPose Tests
TEST(SensorCaptureTest, GetPoseDefault)
{
MockSensorCapture sensor;
Transform pose;
cv::Mat covariance;
EXPECT_FALSE(sensor.getPose(0.0, pose, covariance));
}
TEST(SensorCaptureTest, GetPoseWithOdom)
{
MockSensorCapture sensor;
sensor.setOdomProvided(true);
Transform pose;
cv::Mat covariance;
EXPECT_TRUE(sensor.getPose(0.0, pose, covariance));
EXPECT_FALSE(pose.isNull());
EXPECT_FALSE(covariance.empty());
EXPECT_EQ(covariance.rows, 6);
EXPECT_EQ(covariance.cols, 6);
}
// Comprehensive Usage Test
TEST(SensorCaptureTest, ComprehensiveUsage)
{
// Create sensor with frame rate and transform
float frameRate = 20.0f;
Transform localTransform(0, 0, 1, 0, -1, 0, 0, 0, 0, -1, 0, 0);
MockSensorCapture sensor(frameRate, localTransform);
// Initialize
EXPECT_TRUE(sensor.init("/calib", "test_camera"));
EXPECT_EQ(sensor.getCalibrationFolder(), "/calib");
EXPECT_EQ(sensor.getCameraName(), "test_camera");
// Verify settings
EXPECT_FLOAT_EQ(sensor.getFrameRate(), frameRate);
EXPECT_EQ(sensor.getLocalTransform(), localTransform);
EXPECT_EQ(sensor.getSerial(), "MOCK_SENSOR_001");
// Change settings
sensor.setFrameRate(30.0f);
Transform newTransform = Transform::getIdentity();
sensor.setLocalTransform(newTransform);
sensor.setSerial("NEW_SERIAL");
EXPECT_FLOAT_EQ(sensor.getFrameRate(), 30.0f);
EXPECT_EQ(sensor.getLocalTransform(), newTransform);
EXPECT_EQ(sensor.getSerial(), "NEW_SERIAL");
// Capture data
sensor.resetTimer();
SensorCaptureInfo info;
SensorData data = sensor.takeData(&info);
EXPECT_TRUE(data.isValid());
EXPECT_EQ(info.id, data.id());
EXPECT_DOUBLE_EQ(info.stamp, data.stamp());
EXPECT_GT(info.timeCapture, 0.0);
EXPECT_EQ(sensor.getCaptureCount(), 1);
}
// Edge Cases
TEST(SensorCaptureTest, TakeDataMultipleCalls)
{
MockSensorCapture sensor;
for (int i = 0; i < 10; ++i)
{
SensorData data = sensor.takeData();
EXPECT_TRUE(data.isValid());
}
EXPECT_EQ(sensor.getCaptureCount(), 10);
}
TEST(SensorCaptureTest, TakeDataWithNullInfo)
{
MockSensorCapture sensor;
// Should not crash with null info
SensorData data = sensor.takeData(nullptr);
EXPECT_TRUE(data.isValid());
}
TEST(SensorCaptureTest, HighFrameRate)
{
MockSensorCapture sensor;
sensor.setFrameRate(1000.0f); // Very high frame rate
sensor.resetTimer();
// Should handle high frame rate gracefully
UTimer timer;
SensorData data = sensor.takeData();
double elapsed = timer.ticks();
EXPECT_TRUE(data.isValid());
EXPECT_GE(elapsed, 0.001);
EXPECT_LT(elapsed, 0.01);
}
TEST(SensorCaptureTest, VeryLowFrameRate)
{
MockSensorCapture sensor;
sensor.setFrameRate(2.0f); // Low frame rate (500 ms seconds per frame)
sensor.resetTimer();
UTimer timer;
SensorData data = sensor.takeData();
double elapsed = timer.ticks();
EXPECT_TRUE(data.isValid());
EXPECT_GE(elapsed, 0.5);
EXPECT_LT(elapsed, 0.51);
}
+572
View File
@@ -0,0 +1,572 @@
#include <gtest/gtest.h>
#include <rtabmap/core/SensorCaptureThread.h>
#include <rtabmap/core/Camera.h>
#include <rtabmap/core/Lidar.h>
#include <rtabmap/core/SensorData.h>
#include <rtabmap/core/SensorCaptureInfo.h>
#include <rtabmap/core/Transform.h>
#include <rtabmap/core/Parameters.h>
#include <rtabmap/core/LaserScan.h>
#include <rtabmap/utilite/UTimer.h>
#include <rtabmap/utilite/UThread.h>
#include <opencv2/core.hpp>
using namespace rtabmap;
// Mock Camera implementation for testing
class MockCamera : public Camera
{
public:
MockCamera(float imageRate = 0, const Transform & localTransform = Transform::getIdentity()) :
Camera(imageRate, localTransform),
initCalled_(false),
initResult_(true),
serial_("MOCK_CAMERA_001"),
calibrated_(false),
odomProvided_(false),
captureCount_(0)
{
}
virtual ~MockCamera() {}
virtual bool init(const std::string & calibrationFolder = ".", const std::string & cameraName = "")
{
initCalled_ = true;
calibrationFolder_ = calibrationFolder;
cameraName_ = cameraName;
return initResult_;
}
virtual std::string getSerial() const
{
return serial_;
}
virtual bool isCalibrated() const
{
return calibrated_;
}
virtual bool odomProvided() const
{
return odomProvided_;
}
virtual bool getPose(double stamp, Transform & pose, cv::Mat & covariance, double maxWaitTime = 0.06)
{
if (odomProvided_)
{
pose = Transform(1, 0, 0, 0, 0, 0, 1);
covariance = cv::Mat::eye(6, 6, CV_64FC1);
return true;
}
return false;
}
// Test helpers
void setInitResult(bool result) { initResult_ = result; }
void setSerial(const std::string & serial) { serial_ = serial; }
void setCalibrated(bool calibrated) { calibrated_ = calibrated; }
void setOdomProvided(bool provided) { odomProvided_ = provided; }
bool wasInitCalled() const { return initCalled_; }
std::string getCalibrationFolder() const { return calibrationFolder_; }
std::string getCameraName() const { return cameraName_; }
int getCaptureCount() const { return captureCount_; }
protected:
virtual SensorData captureImage(SensorCaptureInfo * info = 0)
{
++captureCount_;
cv::Mat image = cv::Mat::ones(480, 640, CV_8UC3) * 128;
SensorData data(image, getNextSeqID(), UTimer::now());
return data;
}
private:
bool initCalled_;
bool initResult_;
std::string serial_;
bool calibrated_;
bool odomProvided_;
std::string calibrationFolder_;
std::string cameraName_;
int captureCount_;
};
// Mock Lidar implementation for testing
class MockLidar : public Lidar
{
public:
MockLidar(float lidarRate = 0, const Transform & localTransform = Transform::getIdentity()) :
Lidar(lidarRate, localTransform),
initCalled_(false),
initResult_(true),
serial_("MOCK_LIDAR_001"),
captureCount_(0)
{
}
virtual ~MockLidar() {}
virtual bool init(const std::string & calibrationFolder = ".", const std::string & cameraName = "")
{
initCalled_ = true;
calibrationFolder_ = calibrationFolder;
cameraName_ = cameraName;
return initResult_;
}
virtual std::string getSerial() const
{
return serial_;
}
// Test helpers
void setInitResult(bool result) { initResult_ = result; }
void setSerial(const std::string & serial) { serial_ = serial; }
bool wasInitCalled() const { return initCalled_; }
std::string getCalibrationFolder() const { return calibrationFolder_; }
std::string getCameraName() const { return cameraName_; }
int getCaptureCount() const { return captureCount_; }
protected:
virtual SensorData captureData(SensorCaptureInfo * info = 0)
{
++captureCount_;
// Create a simple laser scan
LaserScan scan = LaserScan::backwardCompatibility(cv::Mat(1, 100, CV_32FC2));
SensorData data;
data.setLaserScan(scan);
data.setStamp(UTimer::now());
return data;
}
private:
bool initCalled_;
bool initResult_;
std::string serial_;
std::string calibrationFolder_;
std::string cameraName_;
int captureCount_;
};
// Constructor Tests
TEST(SensorCaptureThreadTest, ConstructorCameraOnly)
{
MockCamera * camera = new MockCamera();
SensorCaptureThread thread(camera);
EXPECT_FALSE(thread.isCapturing());
EXPECT_TRUE(thread.isPaused());
EXPECT_EQ(thread.camera(), camera);
EXPECT_EQ(thread.lidar(), nullptr);
EXPECT_EQ(thread.odomSensor(), nullptr);
EXPECT_FALSE(thread.odomProvided());
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, ConstructorLidarOnly)
{
MockLidar * lidar = new MockLidar();
SensorCaptureThread thread(lidar);
EXPECT_FALSE(thread.isCapturing());
EXPECT_TRUE(thread.isPaused());
EXPECT_EQ(thread.lidar(), lidar);
EXPECT_EQ(thread.camera(), nullptr);
EXPECT_EQ(thread.odomSensor(), nullptr);
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, ConstructorLidarWithCamera)
{
MockLidar * lidar = new MockLidar();
MockCamera * camera = new MockCamera();
SensorCaptureThread thread(lidar, camera);
EXPECT_FALSE(thread.isCapturing());
EXPECT_EQ(thread.lidar(), lidar);
EXPECT_EQ(thread.camera(), camera);
EXPECT_EQ(thread.odomSensor(), nullptr);
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, ConstructorCameraWithOdomSensor)
{
MockCamera * camera = new MockCamera();
MockCamera * odomSensor = new MockCamera();
Transform extrinsics = Transform::getIdentity();
SensorCaptureThread thread(camera, odomSensor, extrinsics, 0.0, 1.0f, 0.1);
EXPECT_FALSE(thread.isCapturing());
EXPECT_EQ(thread.camera(), camera);
EXPECT_EQ(thread.odomSensor(), odomSensor);
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, ConstructorLidarWithOdomSensor)
{
MockLidar * lidar = new MockLidar();
MockCamera * odomSensor = new MockCamera();
SensorCaptureThread thread(lidar, odomSensor, 0.0, 1.0f, 0.1);
EXPECT_FALSE(thread.isCapturing());
EXPECT_EQ(thread.lidar(), lidar);
EXPECT_EQ(thread.odomSensor(), odomSensor);
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, ConstructorLidarCameraOdom)
{
MockLidar * lidar = new MockLidar();
MockCamera * camera = new MockCamera();
MockCamera * odomSensor = new MockCamera();
Transform extrinsics = Transform::getIdentity();
SensorCaptureThread thread(lidar, camera, odomSensor, extrinsics, 0.0, 1.0f, 0.1);
EXPECT_FALSE(thread.isCapturing());
EXPECT_EQ(thread.lidar(), lidar);
EXPECT_EQ(thread.camera(), camera);
EXPECT_EQ(thread.odomSensor(), odomSensor);
// Thread was never started, destructor will handle cleanup
}
// Configuration Tests
TEST(SensorCaptureThreadTest, SetGetMirroring)
{
MockCamera * camera = new MockCamera();
SensorCaptureThread thread(camera);
thread.setMirroringEnabled(true);
// No getter, so we can't verify directly, but it shouldn't crash
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, SetGetStereoExposureCompensation)
{
MockCamera * camera = new MockCamera();
SensorCaptureThread thread(camera);
thread.setStereoExposureCompensation(true);
// No getter, so we can't verify directly, but it shouldn't crash
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, SetGetColorOnly)
{
MockCamera * camera = new MockCamera();
SensorCaptureThread thread(camera);
thread.setColorOnly(true);
// No getter, so we can't verify directly, but it shouldn't crash
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, SetGetImageDecimation)
{
MockCamera * camera = new MockCamera();
SensorCaptureThread thread(camera);
thread.setImageDecimation(2);
// No getter, so we can't verify directly, but it shouldn't crash
thread.setImageDecimation(4);
thread.setImageDecimation(1);
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, SetGetHistogramMethod)
{
MockCamera * camera = new MockCamera();
SensorCaptureThread thread(camera);
thread.setHistogramMethod(0); // None
thread.setHistogramMethod(1); // EqualizeHist
thread.setHistogramMethod(2); // CLAHE
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, SetGetStereoToDepth)
{
MockCamera * camera = new MockCamera();
SensorCaptureThread thread(camera);
thread.setStereoToDepth(true);
thread.setStereoToDepth(false);
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, SetGetFrameRate)
{
MockCamera * camera = new MockCamera();
SensorCaptureThread thread(camera);
thread.setFrameRate(30.0f);
thread.setFrameRate(0.0f);
thread.setFrameRate(60.0f);
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, SetOdomAsGroundTruth)
{
MockCamera * camera = new MockCamera();
MockCamera * odomSensor = new MockCamera();
Transform extrinsics = Transform::getIdentity();
SensorCaptureThread thread(camera, odomSensor, extrinsics);
thread.setOdomAsGroundTruth(true);
thread.setOdomAsGroundTruth(false);
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, EnableDisableBilateralFiltering)
{
MockCamera * camera = new MockCamera();
SensorCaptureThread thread(camera);
thread.enableBilateralFiltering(5.0f, 50.0f);
thread.disableBilateralFiltering();
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, EnableDisableIMUFiltering)
{
MockCamera * camera = new MockCamera();
SensorCaptureThread thread(camera);
ParametersMap params;
thread.enableIMUFiltering(1, params, false); // Complementary filter
thread.disableIMUFiltering();
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, EnableDisableFeatureDetection)
{
MockCamera * camera = new MockCamera();
SensorCaptureThread thread(camera);
ParametersMap params;
thread.enableFeatureDetection(params);
thread.disableFeatureDetection();
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, SetScanParameters)
{
MockLidar * lidar = new MockLidar();
SensorCaptureThread thread(lidar);
thread.setScanParameters(
false, // fromDepth
1, // downsampleStep
0.0f, // rangeMin
10.0f, // rangeMax
0.05f, // voxelSize
10, // normalsK
0.1f, // normalsRadius
0.8f, // groundNormalsUp
true // deskewing
);
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, SetScanParametersFromDepth)
{
MockLidar * lidar = new MockLidar();
SensorCaptureThread thread(lidar);
thread.setScanParameters(
true, // fromDepth
2, // downsampleStep (image decimation)
0.0f, // rangeMin
0.0f, // rangeMax
0.0f, // voxelSize
0, // normalsK
0.0f, // normalsRadius
0.0f, // groundNormalsUp
false // deskewing
);
// Thread was never started, destructor will handle cleanup
}
// Thread State Tests
TEST(SensorCaptureThreadTest, InitialState)
{
MockCamera * camera = new MockCamera();
SensorCaptureThread thread(camera);
EXPECT_FALSE(thread.isCapturing());
EXPECT_TRUE(thread.isPaused());
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, StartStopThread)
{
MockCamera * camera = new MockCamera();
camera->init(); // Initialize camera before starting thread
SensorCaptureThread thread(camera);
// Start thread
thread.start();
// Give it a moment to start
uSleep(50);
EXPECT_TRUE(thread.isCapturing());
EXPECT_FALSE(thread.isPaused());
// Stop thread
thread.kill();
thread.join();
EXPECT_FALSE(thread.isCapturing());
EXPECT_TRUE(thread.isPaused());
}
TEST(SensorCaptureThreadTest, OdomProvided)
{
MockCamera * camera = new MockCamera();
MockCamera * odomSensor = new MockCamera();
odomSensor->setOdomProvided(true);
Transform extrinsics = Transform::getIdentity();
SensorCaptureThread thread(camera, odomSensor, extrinsics);
EXPECT_TRUE(thread.odomProvided());
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, OdomNotProvided)
{
MockCamera * camera = new MockCamera();
SensorCaptureThread thread(camera);
EXPECT_FALSE(thread.odomProvided());
// Thread was never started, destructor will handle cleanup
}
// Comprehensive Usage Test
TEST(SensorCaptureThreadTest, ComprehensiveConfiguration)
{
MockCamera * camera = new MockCamera();
ParametersMap params;
SensorCaptureThread thread(camera, params);
// Configure all settings
thread.setMirroringEnabled(true);
thread.setStereoExposureCompensation(false);
thread.setColorOnly(false);
thread.setImageDecimation(2);
thread.setHistogramMethod(2); // CLAHE
thread.setStereoToDepth(false);
thread.setFrameRate(30.0f);
thread.setOdomAsGroundTruth(false);
thread.enableBilateralFiltering(5.0f, 50.0f);
// Verify thread state
EXPECT_FALSE(thread.isCapturing());
EXPECT_TRUE(thread.isPaused());
EXPECT_EQ(thread.camera(), camera);
// Thread was never started, destructor will handle cleanup
}
// Edge Cases
TEST(SensorCaptureThreadTest, MultipleConfigurationChanges)
{
MockCamera * camera = new MockCamera();
SensorCaptureThread thread(camera);
// Change settings multiple times
for (int i = 0; i < 10; ++i)
{
thread.setImageDecimation(i % 4 + 1);
thread.setHistogramMethod(i % 3);
thread.setFrameRate((float)(i * 10));
}
// Should not crash
EXPECT_FALSE(thread.isCapturing());
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, ZeroFrameRate)
{
MockCamera * camera = new MockCamera();
SensorCaptureThread thread(camera);
thread.setFrameRate(0.0f); // Unlimited
// Should handle zero frame rate gracefully
EXPECT_FALSE(thread.isCapturing());
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, VeryHighFrameRate)
{
MockCamera * camera = new MockCamera();
SensorCaptureThread thread(camera);
thread.setFrameRate(1000.0f); // Very high frame rate
// Should handle high frame rate gracefully
EXPECT_FALSE(thread.isCapturing());
// Thread was never started, destructor will handle cleanup
}
TEST(SensorCaptureThreadTest, ScanParametersEdgeCases)
{
MockLidar * lidar = new MockLidar();
SensorCaptureThread thread(lidar);
// Test with all zeros (disabled features)
thread.setScanParameters(false, 1, 0.0f, 0.0f, 0.0f, 0, 0.0f, 0.0f, false);
// Test with maximum values
thread.setScanParameters(false, 10, 100.0f, 1000.0f, 1.0f, 100, 5.0f, 1.0f, true);
// Should not crash
EXPECT_FALSE(thread.isCapturing());
// Thread was never started, destructor will handle cleanup
}